Skip to content
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
ceolson01 merged 4 commits into
mainfrom
chore/initial-impl
May 7, 2026
Merged

feat(ngp-338): @openloop/webhook-receiver — Wave 29d partner-receive verifier#1
ceolson01 merged 4 commits into
mainfrom
chore/initial-impl

Conversation

@ceolson01
Copy link
Copy Markdown
Contributor

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/sdk will re-export verifyWebhookSignature with a typed-payload overload.

  • Public package: @openloop/webhook-receiver@0.1.0, MIT, published to public npm with SLSA provenance.
  • API: one function (verifyWebhookSignature), one error class (WebhookSignatureError with stable code), one envelope type (WebhookEvent).
  • Security: crypto.timingSafeEqual with 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.
  • Size: 6.6 kB packaged tarball, zero runtime deps. Uses Node 22 built-in crypto.

Test plan

  • RED commit shows 17 failing tests with the function's behaviour pinned before any implementation
  • GREEN commit lands implementation; all 17 tests pass
  • pnpm typecheck clean
  • pnpm lint clean (uses @openloop/eslint-config strictTypeChecked + stylisticTypeChecked)
  • pnpm format:check clean
  • pnpm build emits dist/ with type declarations + source maps
  • npm pack --dry-run confirms 6.6 kB tarball, 14 files, no node_modules leak
  • Cross-repo contract test asserts the canonical fixture's HMAC matches the signer adapter in openloop-webhook-delivery/src/adapters/crypto-hmac-signer.ts

RED commit failing output (per feedback_tdd_red_green_required)

Test Files  1 failed (1)
     Tests  17 failed (17)

AssertionError: expected [Function] to throw error matching
/malformed-header/ but got 'not implemented'

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:

  1. Confirm @openloop scope ownership on public npm under the OpenLoopHealth org account. If not yet claimed, claim it before tagging.
  2. Generate a granular publish access token at npmjs.com scoped to @openloop/webhook-receiver only.
  3. Add the token as NPM_TOKEN repo secret on this repository (gh secret set NPM_TOKEN --repo OpenLoopHealth/openloop-webhook-receiver).

If NPM_TOKEN is missing when v0.1.0 is tagged, the publish workflow fails at the npm publish step 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 quoted
  • feedback_codeartifact_login_global_registry_trap — project .npmrc declares @openloop scope routing only; no global registry= line
  • feedback_pr_base_main — explicit --base main
  • feedback_main_push_permission — initial branch pushed for per-push approval before merge
  • feedback_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-readme so main exists; this PR opens against existing main
  • feedback_correct_domain_openloophealth — every URL uses openloophealth.com
  • feedback_zod_latest — devDep zod@^4.0.0

Architecture decisions (locked from /concepts/lets-tackle-some-big-bubbly-teacup.md)

  • New dedicated repo, not platform-libs: avoids retrofitting the CodeArtifact-only Changesets publish flow with multi-registry support (would risk the 18 internal packages); greenfield repo = simple npm publish workflow.
  • Public npm: partners discover via npm view; github.com/OpenLoopHealth/openloop-webhook-receiver is 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

ceolson01 and others added 2 commits May 6, 2026 19:40
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-5455c38bf8
Copy link
Copy Markdown

wiz-5455c38bf8 Bot commented May 7, 2026

Wiz Scan Summary

Scanner Findings
Vulnerability Finding Vulnerabilities -
Data Finding Sensitive Data -
Secret Finding Secrets -
IaC Misconfiguration IaC Misconfigurations 6 Low
SAST Finding SAST Findings 2 Low
Software Management Finding Software Management Findings -
Total 8 Low

View scan details in Wiz

To detect these findings earlier in the dev lifecycle, try using Wiz Code VS Code Extension.

ceolson01 and others added 2 commits May 6, 2026 19:50
…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>
@ceolson01 ceolson01 merged commit c279006 into main May 7, 2026
6 checks passed
@ceolson01 ceolson01 deleted the chore/initial-impl branch May 7, 2026 01:02
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant