Skip to content

feat(helpers): restore webhook signature verification - #65

Merged
antonwhop merged 2 commits into
mainfrom
anton/webhook-verify-helper
Aug 23, 2026
Merged

feat(helpers): restore webhook signature verification#65
antonwhop merged 2 commits into
mainfrom
anton/webhook-verify-helper

Conversation

@antonwhop

Copy link
Copy Markdown
Collaborator

Why

The Stainless gem shipped client.webhooks.unwrap through 0.0.41. Fern generates from OpenAPI paths and unwrap was 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 from whop_sdk 0.0.41 lib/whop_sdk/resources/webhooks.rb:173. Standalone, beside verify_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.
  • .fernignore keeps the helper and its tests. ci.yml's guard now loops over both helpers, asserting per helper that the file is present, that require "whop_sdk" reaches its module, and that the gemspec declares its gem.
  • Header lookup is case-insensitive: StandardWebhooks::Webhook#verify reads its three headers by exact lowercase key, and a plain Rack headers Hash arrives capitalized.

The require_relative and add_dependency "standardwebhooks" in this diff are re-emitted by whopio/whop-monorepo#24922 — merge that first, or the next regeneration strips both and require "whop_sdk" raises LoadError for every caller.

Verified by executing

bundle exec rake test (586 runs, 0 failures — 15 of them new), bundle exec rubocop clean repo-wide, the new ci.yml guard run locally, and unwrap exercised against an isolated GEM_HOME/GEM_PATH install of the built gem (valid signature parses, tampered payload rejected, standardwebhooks resolved from the gemspec).

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
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
@antonwhop
antonwhop merged commit 6405dbc into main Aug 23, 2026
8 checks passed
@antonwhop antonwhop mentioned this pull request Aug 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant