Skip to content

feat(helpers): verify webhook signatures from the Rust SDK - #7

Draft
antonwhop wants to merge 1 commit into
mainfrom
anton/verify-webhook-helper
Draft

feat(helpers): verify webhook signatures from the Rust SDK#7
antonwhop wants to merge 1 commit into
mainfrom
anton/verify-webhook-helper

Conversation

@antonwhop

@antonwhop antonwhop commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Why / Prompt

The Stainless SDKs shipped client.webhooks.unwrap. Fern generates from OpenAPI paths and unwrap was never a path, so every Fern SDK lost it — Ruby, Python and TypeScript have it back as hand-written helpers, Rust did not, and a Rust consumer receiving a Whop webhook had nothing in the SDK to verify it with.

What changed?

  • whop_sdk::helpers::verify_webhook(payload, headers, key) -> Result<Value, WebhookVerificationError>, a standalone function so nothing generated is patched. HMACs the secret's literal bytes (ws_ prefix included), which is what the backend signs with — Standard Webhooks libraries base64-decode their key instead. Constant-time via hmac's verify_slice; ±5 min tolerance enforced before the signature so a replay is refused for its timestamp; unknown signature versions skipped, not refused. 13 tests, every fixture signed the way the backend signs, never with the code under test.
  • .fernignore keeps src/helpers and src/lib.rs — the second is load-bearing: lib.rs is generated, Rust compiles no file no mod declares, and the Rust generator has no requirePaths equivalent. A local generation at fern-rust-sdk:0.46.0 reproduces the tree's lib.rs byte for byte apart from the two lines added here. The tests are inline rather than under tests/: an integration test is a second binary linked against the whole crate and cargo test was killed on the runner with it, green without it.
  • Cargo.toml gains base64, hmac, sha2; it is generated, so whop-monorepo#25253 restores them via extraDependencies.

Verified against a genuine backend delivery: all 7 webhook.* e2e scenarios go skip → pass. Nothing published, nothing tagged.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XV1533iUUxKJxfn4FXptWz

Restores the verification half of the `client.webhooks.unwrap` the Stainless
SDKs shipped. Fern generates from OpenAPI paths and `unwrap` was never a path,
so every Fern SDK lost it; Ruby, Python and TypeScript have it back as
hand-written helpers and Rust did not.

`whop_sdk::helpers::verify_webhook(payload, headers, key)` returns the parsed
body or a typed refusal. It computes the HMAC directly on the secret's literal
bytes, which is what the backend signs with — the Standard Webhooks libraries
the other SDKs lean on base64-decode their key instead, and Ruby has to
base64-encode the whole secret to cancel that out.

`.fernignore` keeps `src/helpers`, `tests/verify_webhook.rs`, and `src/lib.rs`
with them: Rust compiles no file that no `mod` declares and the Rust generator
has no `requirePaths` equivalent, so owning the crate root is the only way to
register the module. The crypto dependencies come back from `extraDependencies`
in whop-monorepo's `sdks/fern/generators.yml`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XV1533iUUxKJxfn4FXptWz
@antonwhop
antonwhop force-pushed the anton/verify-webhook-helper branch from 9c1fcfb to f6769fc Compare August 24, 2026 21:49
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