Skip to content

Add Standard Webhooks SDK support - #228

Draft
Tom Tang (qwerzl) wants to merge 6 commits into
mainfrom
agent/standard-webhooks
Draft

Add Standard Webhooks SDK support#228
Tom Tang (qwerzl) wants to merge 6 commits into
mainfrom
agent/standard-webhooks

Conversation

@qwerzl

@qwerzl Tom Tang (qwerzl) commented Aug 4, 2026

Copy link
Copy Markdown
Member

Summary

  • add Standard Webhooks v1 verification, payload parsing, and app.webhook() dispatch for message.received project fanout
  • add typed Spectrum Cloud methods for endpoint registration/list/update/delete, event subscriptions, enable/disable, secret rotation, and stable egress IPs
  • expose normalized-events.v1 and raw-inbound.v1 endpoint schema versions; creation accepts either and schema updates are raw-only
  • keep the existing webhookSecret / SPECTRUM_WEBHOOK_SECRET configuration shape; whsec_ values select Standard verification and unprefixed values retain legacy verification
  • document registration, the full whsec_ secret, zero-downtime rotation, raw-body verification, management APIs, and schema pinning

Why

The delivery and control-plane PRs expose Standard Webhooks behavior, but the TypeScript SDK also needs a first-class consumer and management surface. Without this change, SDK users would need to hand-roll signature verification, reconstruct the preserved provider request, and call the REST endpoints directly.

Standard project fanout verifies webhook-id.webhook-timestamp.rawBody with HMAC-SHA256, accepts space-delimited rotation signatures, validates the event envelope, restores the original provider bytes, and routes them through the configured provider.

Compatibility and rollout

  • Existing native normalized webhooks continue using webhookSecret.
  • Standard fanout selects verification from webhookSecret: whsec_ uses Standard Webhooks, while an unprefixed secret uses the co-delivered legacy Spectrum signature.
  • Endpoint creation supports both public schema IDs; an existing normalized endpoint can only upgrade to raw-inbound.v1 and must retain the one-time Standard secret returned at creation.
  • Pass and store the complete whsec_... value; the SDK strips the prefix and Base64-decodes it internally.
  • Customer-facing delivery history and manual replay are intentionally deferred.
  • Payload-size behavior is intentionally unchanged.

Depends on the Standard Webhooks API in spectrum-cloud#119, the schema-version contract in spectrum-cloud#122, and the delivery format in fusor#69. Related documentation: docs#128.

Validation

  • bun run fix
  • bun run check
  • bun run typecheck — 13/13 tasks
  • bun run test — 32/32 Node and Bun tasks
  • focused Standard Webhooks tests — 28/28 under Node and 28/28 under Bun
  • focused endpoint-management tests — 3 passed
  • bun run build — 12/12 tasks

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Summary by CodeRabbit

  • New Features

    • Added Standard Webhooks support with signature verification, timestamp validation, secret rotation, and duplicate detection.
    • Added cloud webhook management for creating, listing, updating, rotating, retrieving egress IPs, and deleting webhooks.
    • Added public webhook types and helpers for parsing and verifying events.
    • Added fallback support for standard and legacy webhook secrets.
  • Documentation

    • Expanded webhook and getting-started guides with supported formats, authentication, configuration, management, and verification examples.
  • Bug Fixes

    • Improved webhook detection and handling across delivery formats.
    • Forwarded webhook processing errors to Express error middleware.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds Standard Webhooks verification and event conversion, integrates Standard project deliveries into Spectrum routing, adds cloud webhook lifecycle APIs, exports new types and helpers, forwards Express errors, and updates tests and documentation.

Changes

Standard Webhooks support

Layer / File(s) Summary
Standard event verification and conversion
packages/core/src/webhook/standard.ts, packages/core/src/index.ts, packages/core/test/webhook/standard.test.ts
Adds Standard Webhook types, HMAC signature verification, timestamp validation, secret rotation support, event parsing, raw request reconstruction, and RawInboundEvent conversion.
Spectrum Standard Webhook routing
packages/core/src/spectrum.ts, packages/core/src/fusor/types.ts, packages/core/test/webhook/spectrum.test.ts, docs/getting-started.mdx.vel
Adds separate Standard secret configuration, header-based detection, signature verification, legacy fallback, event-ID checks, and Fusor routing.
Cloud webhook lifecycle API
packages/core/src/utils/cloud.ts, packages/core/src/index.ts, packages/core/test/utils/cloud.webhooks.test.ts
Adds webhook lifecycle types and authenticated methods for listing, creating, updating, rotating, inspecting egress IPs, and deleting project webhooks.
Express webhook error forwarding
packages/express/src/index.ts, packages/express/test/express.test.ts
Forwards webhook processing errors to Express error middleware and tests the resulting response.
Webhook documentation
docs/webhooks.mdx.vel
Documents Standard Webhook formats, secret configuration, management, verification, framework setup, and delivery semantics.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Provider
  participant Spectrum
  participant StandardVerifier
  participant Fusor
  Provider->>Spectrum: Send signed Standard Webhook request
  Spectrum->>StandardVerifier: Verify signature and parse event
  StandardVerifier-->>Spectrum: Return validated event
  Spectrum->>Fusor: Process converted inbound event
  Fusor-->>Spectrum: Return provider response
Loading

Suggested labels: release

Poem

A rabbit checks the signed request,
Hops through headers, neat and bright.
Secrets turn and events pass,
Cloud paths stay encoded right.
Fusor catches each new beat,
Tests guard every hop and byte.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary change: adding Standard Webhooks support across the SDK.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/standard-webhooks

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-new Bot commented Aug 4, 2026

Copy link
Copy Markdown

Open in StackBlitz

@spectrum-ts/core

npm i https://pkg.pr.new/@spectrum-ts/core@228

@spectrum-ts/elysia

npm i https://pkg.pr.new/@spectrum-ts/elysia@228

@spectrum-ts/express

npm i https://pkg.pr.new/@spectrum-ts/express@228

@spectrum-ts/fastify

npm i https://pkg.pr.new/@spectrum-ts/fastify@228

@spectrum-ts/hono

npm i https://pkg.pr.new/@spectrum-ts/hono@228

@spectrum-ts/imessage-local

npm i https://pkg.pr.new/@spectrum-ts/imessage-local@228

@spectrum-ts/imessage

npm i https://pkg.pr.new/@spectrum-ts/imessage@228

@spectrum-ts/slack

npm i https://pkg.pr.new/@spectrum-ts/slack@228

spectrum-ts

npm i https://pkg.pr.new/spectrum-ts@228

@spectrum-ts/telegram

npm i https://pkg.pr.new/@spectrum-ts/telegram@228

@spectrum-ts/terminal

npm i https://pkg.pr.new/@spectrum-ts/terminal@228

@spectrum-ts/whatsapp-business

npm i https://pkg.pr.new/@spectrum-ts/whatsapp-business@228

commit: 186de0d

Copy link
Copy Markdown
Member Author

CodeRabbit (@coderabbitai) full review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (11)
packages/core/src/webhook/standard.ts (2)

266-278: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value

Consider dropping framing headers during reconstruction.

encodeRawRequest copies every provider header, including content-length and transfer-encoding. The restored body comes from rawBodyBase64, so a stale content-length can disagree with the actual body length. parseHttpRequest ignores both headers, so the current consumer is unaffected. A provider verify() that reads content-length would see an inconsistent value.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/webhook/standard.ts` around lines 266 - 278, Update
encodeRawRequest to omit framing headers such as content-length and
transfer-encoding when reconstructing the request; preserve all other provider
headers and append the rawBodyBase64-decoded body unchanged.

229-254: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Tighten the request schema so the parsed type matches the declared interface.

request.body uses z.unknown(), so the parsed value can be undefined while MessageReceivedWebhookRequest.body declares it as required. parseStandardWebhookEvent then casts with as StandardWebhookEvent, which hides the gap. The reconstruction path only reads rawBodyBase64, so there is no current runtime defect. Consider validating body explicitly to keep the public type honest.

♻️ Proposed refactor
   request: z.looseObject({
-    body: z.unknown(),
+    body: z.union([z.string(), z.record(z.string(), z.unknown()), z.array(z.unknown()), z.number(), z.boolean(), z.null()]),
     bodyEncoding: z.enum(["json", "form", "text", "base64"]),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/webhook/standard.ts` around lines 229 - 254, Update the
request schema in messageReceivedWebhookSchema to validate body as a required
value compatible with MessageReceivedWebhookRequest.body instead of using
z.unknown(), ensuring parsing cannot produce undefined while preserving the
existing request fields and reconstruction behavior in
parseStandardWebhookEvent.
packages/core/src/spectrum.ts (3)

1313-1318: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Detection triggers on any single webhook-* header.

hasStandardWebhookHeaders returns true when only one of the three headers is present. A JSON delivery that carries a stray webhook-id header is then routed to the Standard path and answered 400 missing-headers, even when it is a valid native normalized webhook. Requiring all three headers keeps native deliveries on the native path and still rejects genuinely truncated Standard deliveries at the verifier.

♻️ Proposed refactor
   const hasStandardWebhookHeaders = (
     headers: Record<string, string>
   ): boolean =>
-    headers["webhook-id"] !== undefined ||
-    headers["webhook-timestamp"] !== undefined ||
-    headers["webhook-signature"] !== undefined;
+    headers["webhook-id"] !== undefined &&
+    headers["webhook-timestamp"] !== undefined &&
+    headers["webhook-signature"] !== undefined;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/spectrum.ts` around lines 1313 - 1318, Update
hasStandardWebhookHeaders to return true only when webhook-id,
webhook-timestamp, and webhook-signature are all present, preserving native
routing for deliveries with stray webhook-* headers while allowing the Standard
verifier to reject genuinely incomplete Standard deliveries.

1353-1374: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse the shared status mapping for the legacy verifier.

Lines 1363-1369 repeat the missing-headers → 400, otherwise 401 mapping already present in handleSpectrumWebhook at lines 1256-1259. Extract one helper so both paths stay consistent when a new verifySpectrumSignature reason is added.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/spectrum.ts` around lines 1353 - 1374, Extract the
verification-reason-to-HTTP-status mapping currently duplicated in
handleSpectrumWebhook and verifyProjectLegacySignature into a shared helper.
Update both paths to use that helper, preserving the existing 400 status for
missing-headers and 401 fallback behavior so future verifySpectrumSignature
reasons remain consistent.

256-260: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Align the two secret resolvers.

resolveStandardWebhookSecret is a named function, while the legacy secret is resolved inline at lines 333-334. Both apply the same explicit-wins-over-env rule. One shared helper keeps the two paths from drifting.

♻️ Proposed refactor
-function resolveStandardWebhookSecret(
-  value: string | undefined
-): string | undefined {
-  return value ?? process.env[envFor("STANDARD_WEBHOOK", "SECRET")];
-}
+function resolveSecret(
+  value: string | undefined,
+  channel: string
+): string | undefined {
+  return value ?? process.env[envFor(channel, "SECRET")];
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/spectrum.ts` around lines 256 - 260, Extract the shared
explicit-value-or-environment fallback logic from resolveStandardWebhookSecret
and the inline legacy secret resolution into one helper. Update both secret
resolution paths to call that helper while preserving explicit values taking
precedence over the corresponding environment variable.
docs/webhooks.mdx.vel (1)

16-22: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document what happens when a Fusor provider is absent.

The table states that a Standard project webhook requires a Fusor provider. handleWebhook in packages/core/src/spectrum.ts lines 1446-1450 throws an Error in that case instead of returning an HTTP status. Readers who compare this row with the documented 500 for a missing secret will expect a status code. One sentence naming the throw prevents that confusion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/webhooks.mdx.vel` around lines 16 - 22, Update the webhook documentation
near the Standard project webhook behavior to state that handleWebhook throws an
Error when the required Fusor provider is absent, rather than returning an HTTP
status. Keep the existing table and delivery distinctions unchanged.
packages/core/src/fusor/types.ts (1)

90-97: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Mention the legacy migration fallback.

The comment states that Standard project webhooks use Spectrum({ standardWebhookSecret }). verifyProjectWebhookSignature in packages/core/src/spectrum.ts lines 1387-1395 also accepts webhookSecret for a Standard delivery when no Standard secret is configured. Adding one sentence makes the doc match that fallback.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/fusor/types.ts` around lines 90 - 97, The documentation for
Standard project webhooks should mention the legacy migration fallback: when
standardWebhookSecret is not configured, verifyProjectWebhookSignature also
accepts webhookSecret for Standard deliveries. Add this clarification near the
existing standardWebhookSecret description without changing the authentication
behavior.
packages/core/test/webhook/spectrum.test.ts (2)

23-28: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer scoped env handling over a module-level assignment.

Line 23 mutates process.env.SPECTRUM_STANDARD_WEBHOOK_SECRET at import time and never restores the original value. vi.stubEnv with vi.unstubAllEnvs in afterEach keeps the change local to this file and removes the manual reset at line 261.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/test/webhook/spectrum.test.ts` around lines 23 - 28, Replace
the module-level assignment to process.env.SPECTRUM_STANDARD_WEBHOOK_SECRET with
Vitest scoped environment handling, using vi.stubEnv in the relevant test setup
and vi.unstubAllEnvs in afterEach. Remove the corresponding manual reset near
the test cleanup while preserving the tests’ expected empty-secret behavior.

377-413: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the inverse case: a legacy-only signature must fail when standardWebhookSecret is set.

verifyProjectWebhookSignature prefers the Standard secret and never falls back once it is configured (packages/core/src/spectrum.ts lines 1380-1396). That is the property that prevents a signature downgrade. No test asserts it. A delivery signed only with SPECTRUM_WEBHOOK_SECRET, sent to an instance configured with standardWebhookSecret, must be answered 401.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/test/webhook/spectrum.test.ts` around lines 377 - 413, Extend
the webhook migration tests near “keeps legacy webhookSecret working during
migration” with an inverse case: configure Spectrum with standardWebhookSecret,
send a delivery containing only the legacy SPECTRUM_WEBHOOK_SECRET signature,
and assert spectrum.webhook returns status 401 without accepting the message.
Keep the existing cleanup via spectrum.stop and use the existing signing helpers
and payload setup.
packages/core/test/utils/cloud.webhooks.test.ts (1)

65-75: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Replace the as FetchCall assertions with narrowing.

call is FetchCall | undefined. Lines 70 and 73 use as FetchCall to silence that. An explicit check narrows the type and also fails with a clear message if the call is missing.

♻️ Proposed refactor
     const call = calls[0];
+    if (!call) {
+      throw new Error("expected one fetch call");
+    }
-    expect(call?.url).toBe(
+    expect(call.url).toBe(
       `${SPECTRUM_CLOUD_URL}/projects/project%2Fwith%20path/webhooks/`
     );
-    expect(call?.init?.method).toBe("POST");
-    expect(requestHeaders(call as FetchCall).get("authorization")).toBe(
+    expect(call.init?.method).toBe("POST");
+    expect(requestHeaders(call).get("authorization")).toBe(
       `Basic ${btoa(`${PROJECT_ID}:${PROJECT_SECRET}`)}`
     );
-    expect(requestHeaders(call as FetchCall).get("content-type")).toBe(
+    expect(requestHeaders(call).get("content-type")).toBe(
       "application/json"
     );

As per coding guidelines: "rely on TypeScript narrowing instead of assertions".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/test/utils/cloud.webhooks.test.ts` around lines 65 - 75, Update
the test assertions around the `calls[0]` result to explicitly verify that
`call` exists before accessing its request headers, allowing TypeScript to
narrow `call` to `FetchCall`. Remove both `as FetchCall` assertions while
preserving the existing URL, method, authorization, and content-type checks.

Source: Coding guidelines

packages/core/test/webhook/standard.test.ts (1)

134-172: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a case for a signature header with no v1, token.

verifyStandardWebhookSignature returns invalid-headers when signatureBytes finds no usable signature (standard.ts lines 167-170). No test covers that branch. A header such as v2,abc or a wrong-length v1, digest exercises it.

💚 Proposed test addition
     expect(
       verifyStandardWebhookSignature({
         headers: headersFor(body, { "webhook-id": "event.with.period" }),
         now: NOW_MS,
         rawBody: body,
         secret: SECRET,
       })
     ).toEqual({ ok: false, reason: "invalid-headers" });
+    expect(
+      verifyStandardWebhookSignature({
+        headers: headersFor(body, { "webhook-signature": "v2,abc" }),
+        now: NOW_MS,
+        rawBody: body,
+        secret: SECRET,
+      })
+    ).toEqual({ ok: false, reason: "invalid-headers" });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/test/webhook/standard.test.ts` around lines 134 - 172, Add a
test case in the “rejects missing or malformed headers” suite for
verifyStandardWebhookSignature using a signature header without a usable v1
token, such as “v2,abc” or an invalid-length “v1,” digest, and assert that it
returns { ok: false, reason: "invalid-headers" }.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/src/spectrum.ts`:
- Around line 1445-1458: Update the Express adapter’s Standard webhook handling
around handleStandardWebhook and buildWebhookResult so rejected async processing
is forwarded to Express error middleware instead of escaping as an unhandled
promise rejection. Catch failures from app.webhook(), including the missing
fusorCore error, and pass them to the adapter’s next callback while preserving
successful response handling.

---

Nitpick comments:
In `@docs/webhooks.mdx.vel`:
- Around line 16-22: Update the webhook documentation near the Standard project
webhook behavior to state that handleWebhook throws an Error when the required
Fusor provider is absent, rather than returning an HTTP status. Keep the
existing table and delivery distinctions unchanged.

In `@packages/core/src/fusor/types.ts`:
- Around line 90-97: The documentation for Standard project webhooks should
mention the legacy migration fallback: when standardWebhookSecret is not
configured, verifyProjectWebhookSignature also accepts webhookSecret for
Standard deliveries. Add this clarification near the existing
standardWebhookSecret description without changing the authentication behavior.

In `@packages/core/src/spectrum.ts`:
- Around line 1313-1318: Update hasStandardWebhookHeaders to return true only
when webhook-id, webhook-timestamp, and webhook-signature are all present,
preserving native routing for deliveries with stray webhook-* headers while
allowing the Standard verifier to reject genuinely incomplete Standard
deliveries.
- Around line 1353-1374: Extract the verification-reason-to-HTTP-status mapping
currently duplicated in handleSpectrumWebhook and verifyProjectLegacySignature
into a shared helper. Update both paths to use that helper, preserving the
existing 400 status for missing-headers and 401 fallback behavior so future
verifySpectrumSignature reasons remain consistent.
- Around line 256-260: Extract the shared explicit-value-or-environment fallback
logic from resolveStandardWebhookSecret and the inline legacy secret resolution
into one helper. Update both secret resolution paths to call that helper while
preserving explicit values taking precedence over the corresponding environment
variable.

In `@packages/core/src/webhook/standard.ts`:
- Around line 266-278: Update encodeRawRequest to omit framing headers such as
content-length and transfer-encoding when reconstructing the request; preserve
all other provider headers and append the rawBodyBase64-decoded body unchanged.
- Around line 229-254: Update the request schema in messageReceivedWebhookSchema
to validate body as a required value compatible with
MessageReceivedWebhookRequest.body instead of using z.unknown(), ensuring
parsing cannot produce undefined while preserving the existing request fields
and reconstruction behavior in parseStandardWebhookEvent.

In `@packages/core/test/utils/cloud.webhooks.test.ts`:
- Around line 65-75: Update the test assertions around the `calls[0]` result to
explicitly verify that `call` exists before accessing its request headers,
allowing TypeScript to narrow `call` to `FetchCall`. Remove both `as FetchCall`
assertions while preserving the existing URL, method, authorization, and
content-type checks.

In `@packages/core/test/webhook/spectrum.test.ts`:
- Around line 23-28: Replace the module-level assignment to
process.env.SPECTRUM_STANDARD_WEBHOOK_SECRET with Vitest scoped environment
handling, using vi.stubEnv in the relevant test setup and vi.unstubAllEnvs in
afterEach. Remove the corresponding manual reset near the test cleanup while
preserving the tests’ expected empty-secret behavior.
- Around line 377-413: Extend the webhook migration tests near “keeps legacy
webhookSecret working during migration” with an inverse case: configure Spectrum
with standardWebhookSecret, send a delivery containing only the legacy
SPECTRUM_WEBHOOK_SECRET signature, and assert spectrum.webhook returns status
401 without accepting the message. Keep the existing cleanup via spectrum.stop
and use the existing signing helpers and payload setup.

In `@packages/core/test/webhook/standard.test.ts`:
- Around line 134-172: Add a test case in the “rejects missing or malformed
headers” suite for verifyStandardWebhookSignature using a signature header
without a usable v1 token, such as “v2,abc” or an invalid-length “v1,” digest,
and assert that it returns { ok: false, reason: "invalid-headers" }.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 42a8b403-3cb7-4ad5-a68f-615976f318e9

📥 Commits

Reviewing files that changed from the base of the PR and between 66e4d25 and a4e8254.

📒 Files selected for processing (10)
  • docs/getting-started.mdx.vel
  • docs/webhooks.mdx.vel
  • packages/core/src/fusor/types.ts
  • packages/core/src/index.ts
  • packages/core/src/spectrum.ts
  • packages/core/src/utils/cloud.ts
  • packages/core/src/webhook/standard.ts
  • packages/core/test/utils/cloud.webhooks.test.ts
  • packages/core/test/webhook/spectrum.test.ts
  • packages/core/test/webhook/standard.test.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use explicit function parameter and return types when they improve clarity; prefer unknown over any; use as const for immutable literal values; and rely on TypeScript narrowing instead of assertions.

Files:

  • packages/core/test/webhook/standard.test.ts
  • packages/core/src/fusor/types.ts
  • packages/core/test/utils/cloud.webhooks.test.ts
  • packages/core/src/index.ts
  • packages/core/test/webhook/spectrum.test.ts
  • packages/core/src/utils/cloud.ts
  • packages/core/src/spectrum.ts
  • packages/core/src/webhook/standard.ts
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,jsx,ts,tsx}: Use meaningful variable names and extract magic numbers into descriptively named constants.
Use arrow functions for callbacks and short functions.
Prefer for...of loops over .forEach() and indexed for loops.
Use optional chaining and nullish coalescing for safer property access.
Prefer template literals over string concatenation and use destructuring for object and array assignments.
Use const by default, let only when reassignment is needed, and never use var.
Always await promises in async functions and use the returned value; prefer async/await over promise chains.
Handle async errors appropriately with try-catch blocks and do not use async functions as Promise executors.
Remove console.log, debugger, and alert statements from production code.
Throw Error objects with descriptive messages rather than strings or other values.
Use try-catch blocks meaningfully and do not catch errors solely to rethrow them.
Prefer early returns for error cases and to reduce nesting; use simple conditionals instead of nested ternaries.
Keep functions focused and within reasonable cognitive-complexity limits, extract complex conditions into named booleans, and group related code while separating concerns.
Avoid dangerouslySetInnerHTML unless absolutely necessary; do not use eval() or assign directly to document.cookie; validate and sanitize user input.
Avoid spread syntax in accumulators within loops, use top-level regex literals instead of creating them in loops, prefer specific imports over namespace imports, and avoid barrel files that re-export everything.

Files:

  • packages/core/test/webhook/standard.test.ts
  • packages/core/src/fusor/types.ts
  • packages/core/test/utils/cloud.webhooks.test.ts
  • packages/core/src/index.ts
  • packages/core/test/webhook/spectrum.test.ts
  • packages/core/src/utils/cloud.ts
  • packages/core/src/spectrum.ts
  • packages/core/src/webhook/standard.ts
**/*.{tsx,ts}

📄 CodeRabbit inference engine (AGENTS.md)

In Next.js, use Server Components for async data fetching instead of async Client Components.

Files:

  • packages/core/test/webhook/standard.test.ts
  • packages/core/src/fusor/types.ts
  • packages/core/test/utils/cloud.webhooks.test.ts
  • packages/core/src/index.ts
  • packages/core/test/webhook/spectrum.test.ts
  • packages/core/src/utils/cloud.ts
  • packages/core/src/spectrum.ts
  • packages/core/src/webhook/standard.ts
**/*.{test,spec}.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{test,spec}.{js,jsx,ts,tsx}: Write assertions inside it() or test() blocks, avoid done callbacks in async tests, and do not commit tests containing .only or .skip.
Keep test suites reasonably flat and avoid excessive describe nesting.

Files:

  • packages/core/test/webhook/standard.test.ts
  • packages/core/test/utils/cloud.webhooks.test.ts
  • packages/core/test/webhook/spectrum.test.ts
🪛 Betterleaks (1.7.3)
packages/core/test/webhook/standard.test.ts

[high] 54-54: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🔇 Additional comments (11)
packages/core/src/webhook/standard.ts (1)

112-184: LGTM!

packages/core/test/webhook/standard.test.ts (1)

44-61: LGTM!

packages/core/src/index.ts (1)

111-135: LGTM!

Also applies to: 151-163

packages/core/src/utils/cloud.ts (3)

68-131: LGTM!


232-247: LGTM!


372-380: 🩺 Stability & Availability

No change needed.

The API documents DELETE as returning 200 with { "succeed": true, "data": { "id": "..." } }. The test's JSON mock matches this contract.

			> Likely an incorrect or invalid review comment.
packages/core/test/utils/cloud.webhooks.test.ts (1)

83-134: LGTM!

packages/core/src/spectrum.ts (1)

1324-1351: LGTM!

Also applies to: 1407-1435

packages/core/test/webhook/spectrum.test.ts (1)

266-339: LGTM!

Also applies to: 415-430

docs/getting-started.mdx.vel (1)

85-87: LGTM!

docs/webhooks.mdx.vel (1)

24-91: LGTM!

Also applies to: 262-270

Comment on lines +1445 to +1458
if (looksLikeJsonPayload(bodyBytes) && hasStandardWebhookHeaders(headers)) {
if (!fusorCore) {
throw new Error(
"spectrum.webhook() received a Standard project webhook but no fusor provider is configured"
);
}
const standardResult = await handleStandardWebhook(
fusorCore,
bodyBytes,
headers,
handler
);
return buildWebhookResult(asWeb, standardResult);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check whether framework adapters wrap app.webhook() in error handling.
set -euo pipefail

fd -t f -e ts . packages --exec rg -ln 'webhook\(' | rg -v '/test/' | while IFS= read -r file; do
  rg -n -C6 '\.webhook\(' "$file"
done

Repository: photon-hq/spectrum-ts

Length of output: 5709


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- adapter files and webhook call sites ---'
fd -t f \( -e ts -o -e tsx \) packages | while IFS= read -r file; do
  if rg -q '\.webhook\(' "$file"; then
    printf '\n### %s\n' "$file"
    rg -n -C12 '\.webhook\(' "$file"
  fi
done

printf '%s\n' '--- spectrum webhook implementation ---'
sed -n '1380,1495p' packages/core/src/spectrum.ts

printf '%s\n' '--- adapter package metadata and nearby error handlers ---'
fd -t f -i 'package.json' packages --exec sh -c 'echo "### $1"; rg -n "\"(express|hono|elysia)|\"scripts\"|\"dependencies\"" "$1"' sh {}

Repository: photon-hq/spectrum-ts

Length of output: 317


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- adapter files and webhook call sites ---'
git ls-files 'packages/**' | rg '\.(ts|tsx)$' | while IFS= read -r file; do
  if rg -q '\.webhook\(' "$file"; then
    printf '\n### %s\n' "$file"
    rg -n -C12 '\.webhook\(' "$file"
  fi
done

printf '%s\n' '--- spectrum webhook implementation ---'
sed -n '1380,1495p' packages/core/src/spectrum.ts

printf '%s\n' '--- package metadata ---'
git ls-files 'packages/**/package.json' | while IFS= read -r file; do
  printf '\n### %s\n' "$file"
  rg -n '"(express|hono|elysia)|"scripts"|"dependencies"|"peerDependencies"' "$file" || true
done

Repository: photon-hq/spectrum-ts

Length of output: 1522


🏁 Script executed:

#!/bin/bash
set -u

printf '%s\n' '--- tracked package files ---'
git ls-files | rg '(^|/)packages/' | head -200

printf '%s\n' '--- webhook call sites across tracked source files ---'
git ls-files | rg '\.(ts|tsx|js|jsx)$' | while IFS= read -r file; do
  if rg -q '\.webhook\(' "$file"; then
    printf '\n### %s\n' "$file"
    rg -n -C12 '\.webhook\(' "$file"
  fi
done

printf '%s\n' '--- spectrum source location and size ---'
find . -type f -path '*/packages/core/src/spectrum.ts' -print -exec wc -l {} \;

Repository: photon-hq/spectrum-ts

Length of output: 45078


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- dispatch branch and handler behavior ---'
sed -n '1400,1490p' packages/core/src/spectrum.ts
sed -n '1,260p' packages/core/src/webhook/standard.ts

printf '%s\n' '--- adapter package versions ---'
for file in packages/express/package.json packages/fastify/package.json packages/hono/package.json packages/elysia/package.json; do
  printf '\n### %s\n' "$file"
  cat "$file"
done

printf '%s\n' '--- adapter error-path tests and route setup ---'
for file in packages/express/test/express.test.ts packages/fastify/test/fastify.test.ts packages/hono/test/hono.test.ts packages/elysia/test/elysia.test.ts; do
  printf '\n### %s\n' "$file"
  rg -n -C10 'throw|reject|error|webhook|listen|inject|fetch|request' "$file" || true
done

Repository: photon-hq/spectrum-ts

Length of output: 30438


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import json
import re

adapters = {
    "express": Path("packages/express"),
    "fastify": Path("packages/fastify"),
    "hono": Path("packages/hono"),
    "elysia": Path("packages/elysia"),
}

for name, root in adapters.items():
    source = (root / "src/index.ts").read_text()
    package = json.loads((root / "package.json").read_text())
    calls = len(re.findall(r"\bapp\.webhook\s*\(", source))
    has_try = "try" in source and "catch" in source
    peer = package["peerDependencies"]
    print(f"{name}: calls={calls}, local_try_catch={has_try}, peer={peer}")

core = Path("packages/core/src/spectrum.ts").read_text()
standard_guard = (
    "looksLikeJsonPayload(bodyBytes) && hasStandardWebhookHeaders(headers)"
    in core
)
standard_throw = (
    'spectrum.webhook() received a Standard project webhook but no fusor provider is configured'
    in core
)
protobuf_throw = (
    'spectrum.webhook() received a non-Spectrum (fusor) request but no fusor provider is configured'
    in core
)
print(
    "core: standard_guard=%s, standard_no_fusor_throw=%s, "
    "protobuf_no_fusor_throw=%s"
    % (standard_guard, standard_throw, protobuf_throw)
)

for name, root in adapters.items():
    test = (root / "test" / f"{name}.test.ts").read_text()
    print(
        f"{name}: tests_standard_delivery={('webhook-id' in test or 'signStandardDelivery' in test)}, "
        f"tests_no_fusor_standard={('no fusor' in test.lower() and 'standard' in test.lower())}"
    )
PY

Repository: photon-hq/spectrum-ts

Length of output: 1029


Handle rejected Standard deliveries in the Express adapter.

app.webhook() throws when a JSON body has webhook-* headers and no Fusor provider exists. The adapter supports Express 4, which does not forward rejected promises from async route handlers. Forward this rejection to Express error middleware.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/spectrum.ts` around lines 1445 - 1458, Update the Express
adapter’s Standard webhook handling around handleStandardWebhook and
buildWebhookResult so rejected async processing is forwarded to Express error
middleware instead of escaping as an unhandled promise rejection. Catch failures
from app.webhook(), including the missing fusorCore error, and pass them to the
adapter’s next callback while preserving successful response handling.

@qwerzl

Copy link
Copy Markdown
Member Author

CodeRabbit (@coderabbitai) review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot added the release Just as it is label Aug 4, 2026

Copy link
Copy Markdown
Member Author

CodeRabbit (@coderabbitai) review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/core/src/utils/cloud.ts (1)

232-248: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Sensitive Data Exposure (CWE-319): Cleartext Transmission of Sensitive Information

Reachability: Internal · Exploitability: Moderate

Restrict the Cloud API origin before sending credentials.

SPECTRUM_CLOUD_URL is configurable through process.env and is concatenated directly into every request URL. If it contains an HTTP or unintended origin, authenticatedRequest sends the project secret there. No redirect policy is set, so reject redirects and validate or allowlist an HTTPS origin. webhookUrl is only request data and is not used as the fetch destination.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/utils/cloud.ts` around lines 232 - 248, Validate the
configurable SPECTRUM_CLOUD_URL before constructing authenticated webhook
requests, requiring an HTTPS origin from the approved Cloud API allowlist and
rejecting invalid or unintended origins. Ensure fetch requests do not follow
redirects, while keeping webhookUrl limited to request data and never treating
it as the destination.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/core/src/utils/cloud.ts`:
- Around line 232-248: Validate the configurable SPECTRUM_CLOUD_URL before
constructing authenticated webhook requests, requiring an HTTPS origin from the
approved Cloud API allowlist and rejecting invalid or unintended origins. Ensure
fetch requests do not follow redirects, while keeping webhookUrl limited to
request data and never treating it as the destination.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b0189d63-7378-4a17-83d9-8615c20b8777

📥 Commits

Reviewing files that changed from the base of the PR and between 3a0d7d8 and bd4e4f3.

📒 Files selected for processing (4)
  • docs/webhooks.mdx.vel
  • packages/core/src/index.ts
  • packages/core/src/utils/cloud.ts
  • packages/core/test/utils/cloud.webhooks.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • docs/webhooks.mdx.vel
  • packages/core/src/index.ts
  • packages/core/test/utils/cloud.webhooks.test.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use explicit function parameter and return types when they improve clarity; prefer unknown over any; use as const for immutable literal values; and rely on TypeScript narrowing instead of assertions.

Files:

  • packages/core/src/utils/cloud.ts
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,jsx,ts,tsx}: Use meaningful variable names and extract magic numbers into descriptively named constants.
Use arrow functions for callbacks and short functions.
Prefer for...of loops over .forEach() and indexed for loops.
Use optional chaining and nullish coalescing for safer property access.
Prefer template literals over string concatenation and use destructuring for object and array assignments.
Use const by default, let only when reassignment is needed, and never use var.
Always await promises in async functions and use the returned value; prefer async/await over promise chains.
Handle async errors appropriately with try-catch blocks and do not use async functions as Promise executors.
Remove console.log, debugger, and alert statements from production code.
Throw Error objects with descriptive messages rather than strings or other values.
Use try-catch blocks meaningfully and do not catch errors solely to rethrow them.
Prefer early returns for error cases and to reduce nesting; use simple conditionals instead of nested ternaries.
Keep functions focused and within reasonable cognitive-complexity limits, extract complex conditions into named booleans, and group related code while separating concerns.
Avoid dangerouslySetInnerHTML unless absolutely necessary; do not use eval() or assign directly to document.cookie; validate and sanitize user input.
Avoid spread syntax in accumulators within loops, use top-level regex literals instead of creating them in loops, prefer specific imports over namespace imports, and avoid barrel files that re-export everything.

Files:

  • packages/core/src/utils/cloud.ts
**/*.{tsx,ts}

📄 CodeRabbit inference engine (AGENTS.md)

In Next.js, use Server Components for async data fetching instead of async Client Components.

Files:

  • packages/core/src/utils/cloud.ts
🔇 Additional comments (4)
packages/core/src/utils/cloud.ts (4)

68-68: LGTM!

Also applies to: 99-104


111-132: 🔒 Security & Privacy

Keep the current webhook response types. createWebhook and rotateWebhookSecret use dedicated secret-bearing types. listWebhooks and updateWebhook return WebhookData without secret fields.


85-85: 🗄️ Data Integrity & Integration

Confirm the Cloud response contract for schemaVersion.

The one-way "normalized-events.v1""raw-inbound.v1" migration justifies UpdateWebhookInput. The public contract does not establish that every legacy WebhookData response includes schemaVersion. If Cloud can omit it, normalize legacy responses or adjust the response type.


321-380: 🗄️ Data Integrity & Integration

Align the webhook methods with the published Cloud API contract.

The published contract defines only GET/POST /projects/{projectId}/webhooks/ and DELETE /projects/{projectId}/webhooks/{webhookId}/. It states that secret rotation requires delete and re-registration. It does not define update or egress-IP endpoints. The delete path also requires a trailing slash, which this client omits. Confirm that the deployed API version supports the added methods and response types before shipping them.

Copy link
Copy Markdown
Member Author

CodeRabbit (@coderabbitai) review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Member Author

CodeRabbit (@coderabbitai) review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Already reviewed.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release Just as it is

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant