This repository was archived by the owner on May 7, 2026. It is now read-only.
feat(ngp-338): @openloop/webhook-receiver — Wave 29d partner-receive verifier#1
Merged
Conversation
Pin the partner-receiver verifier's behaviour before the implementation
exists. Each test covers a security-critical failure mode for the HMAC
signature scheme defined by Wave 29a (NGP-335):
- parseSignatureHeader (6 tests): missing comma, missing t=,
no supported version, non-numeric timestamp, non-hex v1, empty.
- signature & timestamp (10 tests): valid happy path, stale
timestamp, future-skew within tolerance, future beyond tolerance,
boundary inclusive, toleranceSeconds=0, tampered payload, wrong
secret, invalid JSON body, error-code stability.
- cross-repo contract (1 test): byte-for-byte signature parity
with openloop-webhook-delivery's crypto-hmac-signer.
The canonical fixture (SECRET, BODY, TIMESTAMP, EXPECTED_SIGNATURE)
mirrors the source-of-truth in webhook-delivery so a regression on
either side fails fast in both repos.
Failing output:
Test Files 1 failed (1)
Tests 17 failed (17)
AssertionError: expected [Function] to throw error matching
/malformed-header/ but got 'not implemented'
Per `feedback_tdd_red_green_required` — RED commit precedes GREEN.
Repo bootstrap (package.json, tsconfig, eslint, vitest, .npmrc with
scoped CodeArtifact for the eslint-config devDep only — published
package has zero @openloop deps).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…pass
Drives the partner-receive verifier from RED to GREEN:
- parseSignatureHeader: regex-anchored `t=<digits>,v1=<lowercase hex>`
with explicit no-supported-version branch when only future-version
keys are present. Empty header surfaces malformed-header.
- constantTimeEquals: length-check guards against the
crypto.timingSafeEqual buffer-length-mismatch throw (timing leak)
before invoking the constant-time compare.
- Symmetric absolute drift window with inclusive boundary
(now - t === toleranceSeconds is valid), supporting
toleranceSeconds=0 strict equality.
- HMAC-then-parse ordering: signature verifies before JSON.parse
runs, so attackers can't probe parser internals via tampered bodies.
- WebhookEvent envelope shape-check guards against signed-but-
malformed bodies (signature valid + JSON valid but missing
required fields → invalid-payload-json).
Test result:
Test Files 1 passed (1)
Tests 17 passed (17)
Also lands the package scaffolding:
- dual tsconfig (typecheck includes test files via projectService;
build emits dist/ from src only via tsconfig.build.json)
- .github/workflows/ci.yml — calls platform-workflows reusable CI
with runCdkSynth: false (greenfield package, no CDK)
- .github/workflows/publish.yml — tag-triggered publish with
CodeArtifact OIDC for the eslint-config devDep + NPM_TOKEN for
public registry. Includes --provenance for SLSA attestations.
- README.md — full partner-facing usage guide with manual
verification examples in Python/Go/Ruby, security notes,
and receiver implementation checklist.
- CHANGELOG.md — Keep-A-Changelog 0.1.0 entry.
- LICENSE — MIT.
Per `feedback_tdd_red_green_required` — separate red/green commits
with red commit's failing output quoted in red commit message.
Per `feedback_codeartifact_login_global_registry_trap` — project
.npmrc declares scope routing only; no top-level `registry=` line.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wiz Scan Summary
To detect these findings earlier in the dev lifecycle, try using Wiz Code VS Code Extension. |
…e reusable workflow
The OpenLoopHealth/openloop-platform-workflows repo is private; this
repo (openloop-webhook-receiver) is public so partners can discover
the package via `npm view`. Public repos cannot consume private
reusable workflows — the previous `uses:` reference failed at
workflow scheduling with "workflow file issue".
Inline the equivalent steps so the workflow is self-contained:
- actions/checkout + pnpm/action-setup + actions/setup-node
- aws-actions/configure-aws-credentials (OIDC) for the CodeArtifact
reader role
- inline `aws codeartifact get-authorization-token` + ~/.npmrc
composition (mirror of the platform-workflows
codeartifact-npm-login composite action, scoped to @openloop)
- pnpm install --frozen-lockfile + typecheck + lint + format:check
+ test + build
CodeArtifact auth is still required at lint time because
`@openloop/eslint-config` is CodeArtifact-private. The PUBLISHED
package has zero @openloop runtime deps, so partner installs from
public npm don't need CodeArtifact.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rsion The pnpm setup action requires either an explicit `version:` input or a `packageManager` field in package.json. Without one, the action exits with "No pnpm version is specified" before any install runs. Pin to pnpm@10.32.1 (matches the local-dev environment). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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.
Summary
Wave 29d ships
@openloop/webhook-receiver, a tiny zero-runtime-dependency Node.js package partners use to verify HMAC-SHA256 signatures on incoming OpenLoop webhooks (the canonical Stripe-style scheme locked in 29a). Decouples 29d from Wave 27's BAA-gated Stainless SDK so partner value ships today; the future@openloop/sdkwill re-exportverifyWebhookSignaturewith a typed-payload overload.@openloop/webhook-receiver@0.1.0, MIT, published to public npm with SLSA provenance.verifyWebhookSignature), one error class (WebhookSignatureErrorwith stablecode), one envelope type (WebhookEvent).crypto.timingSafeEqualwith explicit length-check guard (the buffer-length-mismatch throw in Node's API is a timing leak), 5-min replay tolerance default with inclusive boundary, HMAC-before-parse ordering.crypto.Test plan
pnpm typecheckcleanpnpm lintclean (uses@openloop/eslint-configstrictTypeChecked + stylisticTypeChecked)pnpm format:checkcleanpnpm buildemitsdist/with type declarations + source mapsnpm pack --dry-runconfirms 6.6 kB tarball, 14 files, no node_modules leakopenloop-webhook-delivery/src/adapters/crypto-hmac-signer.tsRED commit failing output (per
feedback_tdd_red_green_required)Full output: 17 failures across the parseSignatureHeader cases (6), signature & timestamp cases (10), and the cross-repo contract case (1).
Pre-merge prerequisites for the publish workflow
The publish workflow triggers on tag push matching
v[0-9]+.*. Before tagging v0.1.0:@openloopscope ownership on public npm under the OpenLoopHealth org account. If not yet claimed, claim it before tagging.@openloop/webhook-receiveronly.NPM_TOKENrepo secret on this repository (gh secret set NPM_TOKEN --repo OpenLoopHealth/openloop-webhook-receiver).If
NPM_TOKENis missing when v0.1.0 is tagged, the publish workflow fails at thenpm publishstep with E401. That is intentional — no half-published states. Re-run the workflow after the secret lands.Memory rules followed
feedback_tdd_red_green_required— separate red/green commits with red's failing output quotedfeedback_codeartifact_login_global_registry_trap— project.npmrcdeclares@openloopscope routing only; no globalregistry=linefeedback_pr_base_main— explicit--base mainfeedback_main_push_permission— initial branch pushed for per-push approval before mergefeedback_branch_protection— branch protection deferred (would create self-approval deadlock; configure protective rules later without required code-owner approval)feedback_no_branch_rename_to_main— repo created with--add-readmeso main exists; this PR opens against existing mainfeedback_correct_domain_openloophealth— every URL usesopenloophealth.comfeedback_zod_latest— devDepzod@^4.0.0Architecture decisions (locked from
/concepts/lets-tackle-some-big-bubbly-teacup.md)npm publishworkflow.npm view;github.com/OpenLoopHealth/openloop-webhook-receiveris a clean partner-facing landing page; sibling Python equivalent drops alongside when Wave 27 ships.data: unknown: typed payloads come from Wave 27's auto-generated SDK (BAA-gated). 29d ships the lean verifier today; partners zod-parse downstream.🤖 Generated with Claude Code