feat(helpers): restore webhook signature verification - #65
Merged
Conversation
The Stainless gem shipped client.webhooks.unwrap through 0.0.41. Fern generates from OpenAPI paths and unwrap was never a path, so the Fern client dropped it and callers are left hand-rolling HMAC comparison. Adds WhopSDK::Helpers::VerifyWebhook.unwrap, ported from that method, alongside the existing verify_user_token helper. Verification only: the Stainless version also coerced into a union of 42 typed event models, which Fern does not generate, so the parsed body is returned as-is. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XV1533iUUxKJxfn4FXptWz
antonwhop
marked this pull request as ready for review
August 22, 2026 23:58
Whop's backend signs with the literal bytes of the secret it issues — WebhooksManager::Create mints "ws_" + SecureRandom.hex(32), and SignWebhook hands webhook.webhook_secret straight to OpenSSL::HMAC. The helper passed that secret to StandardWebhooks, which strips a whsec_ prefix and base64-decodes the remainder to derive its key, so it derived the wrong key and rejected every genuine Whop delivery. Base64-encode inside the helper instead, cancelling that decode out. Callers now pass the secret exactly as Whop shows it, which is what the docs were papering over with a caller-side btoa/b64encode. The whole secret is encoded, prefix included, because the backend never strips one either. The tests generated their fixture with the same library they verified with — self-consistent, and wrong against the real producer. They now sign the way sign_webhook.rb does, over the raw body bytes, and add coverage for a multi-signature header and for the v1n nonce scheme. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XV1533iUUxKJxfn4FXptWz
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The Stainless gem shipped
client.webhooks.unwrapthrough 0.0.41. Fern generates from OpenAPI paths andunwrapwas never a path, so all three Fern SDKs silently lost it and consumers are hand-rolling HMAC comparison — exactly the thing not to hand-roll.What changed?
WhopSDK::Helpers::VerifyWebhook.unwrap(payload, headers:, key:), ported fromwhop_sdk0.0.41lib/whop_sdk/resources/webhooks.rb:173. Standalone, besideverify_user_token, so nothing generated is patched. Verification only — the Stainless version also coerced into a union of 42 typed event models, which Fern does not generate..fernignorekeeps the helper and its tests.ci.yml's guard now loops over both helpers, asserting per helper that the file is present, thatrequire "whop_sdk"reaches its module, and that the gemspec declares its gem.StandardWebhooks::Webhook#verifyreads its three headers by exact lowercase key, and a plain Rack headers Hash arrives capitalized.The
require_relativeandadd_dependency "standardwebhooks"in this diff are re-emitted by whopio/whop-monorepo#24922 — merge that first, or the next regeneration strips both andrequire "whop_sdk"raisesLoadErrorfor every caller.Verified by executing
bundle exec rake test(586 runs, 0 failures — 15 of them new),bundle exec rubocopclean repo-wide, the new ci.yml guard run locally, andunwrapexercised against an isolatedGEM_HOME/GEM_PATHinstall of the built gem (valid signature parses, tampered payload rejected,standardwebhooksresolved from the gemspec).