Add Standard Webhooks SDK support - #228
Conversation
📝 WalkthroughWalkthroughThe 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. ChangesStandard Webhooks support
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
Suggested labels: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
@spectrum-ts/core
@spectrum-ts/elysia
@spectrum-ts/express
@spectrum-ts/fastify
@spectrum-ts/hono
@spectrum-ts/imessage-local
@spectrum-ts/imessage
@spectrum-ts/slack
spectrum-ts
@spectrum-ts/telegram
@spectrum-ts/terminal
@spectrum-ts/whatsapp-business
commit: |
|
CodeRabbit (@coderabbitai) full review |
✅ Action performedFull review finished. |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (11)
packages/core/src/webhook/standard.ts (2)
266-278: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low valueConsider dropping framing headers during reconstruction.
encodeRawRequestcopies every provider header, includingcontent-lengthandtransfer-encoding. The restored body comes fromrawBodyBase64, so a stalecontent-lengthcan disagree with the actual body length.parseHttpRequestignores both headers, so the current consumer is unaffected. A providerverify()that readscontent-lengthwould 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 valueTighten the
requestschema so the parsed type matches the declared interface.
request.bodyusesz.unknown(), so the parsed value can beundefinedwhileMessageReceivedWebhookRequest.bodydeclares it as required.parseStandardWebhookEventthen casts withas StandardWebhookEvent, which hides the gap. The reconstruction path only readsrawBodyBase64, so there is no current runtime defect. Consider validatingbodyexplicitly 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 winDetection triggers on any single
webhook-*header.
hasStandardWebhookHeadersreturnstruewhen only one of the three headers is present. A JSON delivery that carries a straywebhook-idheader is then routed to the Standard path and answered400 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 valueReuse the shared status mapping for the legacy verifier.
Lines 1363-1369 repeat the
missing-headers → 400, otherwise401mapping already present inhandleSpectrumWebhookat lines 1256-1259. Extract one helper so both paths stay consistent when a newverifySpectrumSignaturereason 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 valueAlign the two secret resolvers.
resolveStandardWebhookSecretis 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 valueDocument what happens when a Fusor provider is absent.
The table states that a Standard project webhook requires a Fusor provider.
handleWebhookinpackages/core/src/spectrum.tslines 1446-1450 throws anErrorin that case instead of returning an HTTP status. Readers who compare this row with the documented500for 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 valueMention the legacy migration fallback.
The comment states that Standard project webhooks use
Spectrum({ standardWebhookSecret }).verifyProjectWebhookSignatureinpackages/core/src/spectrum.tslines 1387-1395 also acceptswebhookSecretfor 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 valuePrefer scoped env handling over a module-level assignment.
Line 23 mutates
process.env.SPECTRUM_STANDARD_WEBHOOK_SECRETat import time and never restores the original value.vi.stubEnvwithvi.unstubAllEnvsinafterEachkeeps 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 winAdd the inverse case: a legacy-only signature must fail when
standardWebhookSecretis set.
verifyProjectWebhookSignatureprefers the Standard secret and never falls back once it is configured (packages/core/src/spectrum.tslines 1380-1396). That is the property that prevents a signature downgrade. No test asserts it. A delivery signed only withSPECTRUM_WEBHOOK_SECRET, sent to an instance configured withstandardWebhookSecret, must be answered401.🤖 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 valueReplace the
as FetchCallassertions with narrowing.
callisFetchCall | undefined. Lines 70 and 73 useas FetchCallto 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 winAdd a case for a signature header with no
v1,token.
verifyStandardWebhookSignaturereturnsinvalid-headerswhensignatureBytesfinds no usable signature (standard.ts lines 167-170). No test covers that branch. A header such asv2,abcor a wrong-lengthv1,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
📒 Files selected for processing (10)
docs/getting-started.mdx.veldocs/webhooks.mdx.velpackages/core/src/fusor/types.tspackages/core/src/index.tspackages/core/src/spectrum.tspackages/core/src/utils/cloud.tspackages/core/src/webhook/standard.tspackages/core/test/utils/cloud.webhooks.test.tspackages/core/test/webhook/spectrum.test.tspackages/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
unknownoverany; useas constfor immutable literal values; and rely on TypeScript narrowing instead of assertions.
Files:
packages/core/test/webhook/standard.test.tspackages/core/src/fusor/types.tspackages/core/test/utils/cloud.webhooks.test.tspackages/core/src/index.tspackages/core/test/webhook/spectrum.test.tspackages/core/src/utils/cloud.tspackages/core/src/spectrum.tspackages/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.
Preferfor...ofloops over.forEach()and indexedforloops.
Use optional chaining and nullish coalescing for safer property access.
Prefer template literals over string concatenation and use destructuring for object and array assignments.
Useconstby default,letonly when reassignment is needed, and never usevar.
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.
Removeconsole.log,debugger, andalertstatements from production code.
ThrowErrorobjects 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.
AvoiddangerouslySetInnerHTMLunless absolutely necessary; do not useeval()or assign directly todocument.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.tspackages/core/src/fusor/types.tspackages/core/test/utils/cloud.webhooks.test.tspackages/core/src/index.tspackages/core/test/webhook/spectrum.test.tspackages/core/src/utils/cloud.tspackages/core/src/spectrum.tspackages/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.tspackages/core/src/fusor/types.tspackages/core/test/utils/cloud.webhooks.test.tspackages/core/src/index.tspackages/core/test/webhook/spectrum.test.tspackages/core/src/utils/cloud.tspackages/core/src/spectrum.tspackages/core/src/webhook/standard.ts
**/*.{test,spec}.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{test,spec}.{js,jsx,ts,tsx}: Write assertions insideit()ortest()blocks, avoid done callbacks in async tests, and do not commit tests containing.onlyor.skip.
Keep test suites reasonably flat and avoid excessivedescribenesting.
Files:
packages/core/test/webhook/standard.test.tspackages/core/test/utils/cloud.webhooks.test.tspackages/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 & AvailabilityNo change needed.
The API documents
DELETEas returning200with{ "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
| 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); | ||
| } |
There was a problem hiding this comment.
🩺 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"
doneRepository: 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
doneRepository: 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
doneRepository: 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())}"
)
PYRepository: 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.
|
CodeRabbit (@coderabbitai) review |
✅ Action performedReview finished.
|
|
CodeRabbit (@coderabbitai) review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
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 winSensitive Data Exposure (CWE-319): Cleartext Transmission of Sensitive Information
Reachability: Internal · Exploitability: Moderate
Restrict the Cloud API origin before sending credentials.
SPECTRUM_CLOUD_URLis configurable throughprocess.envand is concatenated directly into every request URL. If it contains an HTTP or unintended origin,authenticatedRequestsends the project secret there. No redirect policy is set, so reject redirects and validate or allowlist an HTTPS origin.webhookUrlis 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
📒 Files selected for processing (4)
docs/webhooks.mdx.velpackages/core/src/index.tspackages/core/src/utils/cloud.tspackages/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
unknownoverany; useas constfor 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.
Preferfor...ofloops over.forEach()and indexedforloops.
Use optional chaining and nullish coalescing for safer property access.
Prefer template literals over string concatenation and use destructuring for object and array assignments.
Useconstby default,letonly when reassignment is needed, and never usevar.
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.
Removeconsole.log,debugger, andalertstatements from production code.
ThrowErrorobjects 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.
AvoiddangerouslySetInnerHTMLunless absolutely necessary; do not useeval()or assign directly todocument.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 & PrivacyKeep the current webhook response types.
createWebhookandrotateWebhookSecretuse dedicated secret-bearing types.listWebhooksandupdateWebhookreturnWebhookDatawithout secret fields.
85-85: 🗄️ Data Integrity & IntegrationConfirm the Cloud response contract for
schemaVersion.The one-way
"normalized-events.v1"→"raw-inbound.v1"migration justifiesUpdateWebhookInput. The public contract does not establish that every legacyWebhookDataresponse includesschemaVersion. If Cloud can omit it, normalize legacy responses or adjust the response type.
321-380: 🗄️ Data Integrity & IntegrationAlign the webhook methods with the published Cloud API contract.
The published contract defines only
GET/POST /projects/{projectId}/webhooks/andDELETE /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.
|
CodeRabbit (@coderabbitai) review |
✅ Action performedReview finished.
|
|
CodeRabbit (@coderabbitai) review |
|
Summary
app.webhook()dispatch formessage.receivedproject fanoutnormalized-events.v1andraw-inbound.v1endpoint schema versions; creation accepts either and schema updates are raw-onlywebhookSecret/SPECTRUM_WEBHOOK_SECRETconfiguration shape;whsec_values select Standard verification and unprefixed values retain legacy verificationwhsec_secret, zero-downtime rotation, raw-body verification, management APIs, and schema pinningWhy
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.rawBodywith 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
webhookSecret.webhookSecret:whsec_uses Standard Webhooks, while an unprefixed secret uses the co-delivered legacy Spectrum signature.raw-inbound.v1and must retain the one-time Standard secret returned at creation.whsec_...value; the SDK strips the prefix and Base64-decodes it internally.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 fixbun run checkbun run typecheck— 13/13 tasksbun run test— 32/32 Node and Bun tasksbun run build— 12/12 tasksNeed help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.Summary by CodeRabbit
New Features
Documentation
Bug Fixes