Sign In

@cross-deck/node

Package Overview
Dependencies
Maintainers
1
Versions
25
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@cross-deck/node - npm Package Compare versions

Comparing version
1.14.0
to
1.15.0
dist/crossdeck-server-D3sUzGKV.d.mts

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

+10
-0

@@ -9,2 +9,12 @@ # Changelog

## [1.15.0] — 2026-07-28
**Outbound webhooks are live — receive and verify them with the Stripe-exact `webhooks.constructEvent()`.** Crossdeck now delivers signed, retried, at-least-once webhooks to endpoints you register (first event types: `trust.rule.added` / `trust.rule.removed`; more ride the same spine). This release ships the receiver side:
- **`webhooks.constructEvent(rawBody, signatureHeader, secret, options?)`** — the Stripe-shape API (`stripe.webhooks.constructEvent`). Verifies the signature and returns the typed `WebhookEvent` envelope (`id`, `type`, `api_version`, `created`, `livemode`, `data`, `reconcile`), or throws. Same constant-time HMAC, mandatory replay-tolerance window, and secret-rotation array as `verifyWebhookSignature()` — it just hands back the structured event instead of `unknown`.
- **Mounted on the client too:** `crossdeck.webhooks.constructEvent(...)` (pure functions, so it's the same object as the standalone `webhooks` export).
- **New exported types:** `WebhookEvent`, `WebhookEventType` (a union of the live types plus an open tail so a new server-side rider compiles without an SDK bump).
- **The nudge rule, documented:** the payload carries the changed key plus a `reconcile` pointer — verify, then `GET event.reconcile.url` for the authoritative state and enforce on that. An at-least-once, possibly-reordered delivery is harmless when you reconcile.
- `verifyWebhookSignature()` / `signWebhookPayload()` are unchanged and still exported. The `[ROADMAP]` disclaimer is gone — delivery ships.
## [1.14.0] — 2026-07-27

@@ -11,0 +21,0 @@

+1
-1

@@ -1,2 +0,2 @@

import { z as CrossdeckServer } from '../crossdeck-server-NL1p6hlR.mjs';
import { z as CrossdeckServer } from '../crossdeck-server-D3sUzGKV.mjs';
import 'node:events';

@@ -3,0 +3,0 @@

@@ -1,2 +0,2 @@

import { z as CrossdeckServer } from '../crossdeck-server-NL1p6hlR.js';
import { z as CrossdeckServer } from '../crossdeck-server-D3sUzGKV.js';
import 'node:events';

@@ -3,0 +3,0 @@

@@ -1,2 +0,2 @@

export { A as AliasIdentityInput, a as AliasResult, b as AuditDecision, c as AuditEntry, B as BlockVerdict, d as Breadcrumb, e as BreadcrumbCategory, f as BreadcrumbLevel, C as CROSSDECK_API_VERSION, g as CampaignLinkInput, h as CampaignLinkResult, i as CapturedError, j as Contract, k as ContractAppliesTo, l as ContractFailureInput, m as ContractPillar, n as ContractStatus, o as ContractTestRef, p as CrossdeckAuthenticationError, q as CrossdeckConfigurationError, r as CrossdeckContracts, s as CrossdeckError, t as CrossdeckErrorPayload, u as CrossdeckErrorType, v as CrossdeckInternalError, w as CrossdeckNetworkError, x as CrossdeckPermissionError, y as CrossdeckRateLimitError, z as CrossdeckServer, D as CrossdeckServerOptions, E as CrossdeckValidationError, F as DEFAULT_BASE_URL, G as DEFAULT_TIMEOUT_MS, H as Diagnostics, I as EntitlementCacheOptions, J as EntitlementMutationResult, K as EntitlementStore, L as EntitlementsListResponse, M as EntitlementsListener, N as Environment, O as ErrorCaptureConfig, P as ErrorLevel, Q as EventProperties, R as ForgetResult, S as GateInput, T as GateVerdict, U as GrantDuration, V as GrantEntitlementInput, W as GroupMembership, X as HeartbeatResponse, Y as HttpRequestInfo, Z as HttpResponseInfo, _ as HttpRetriesConfig, $ as IdentifyOptions, a0 as IdentityHints, a1 as IngestOptions, a2 as IngestResponse, a3 as OwnerStatusInput, a4 as PublicEntitlement, a5 as PurchaseResult, a6 as RequestOptions, a7 as ResolveInput, a8 as ResolveResult, a9 as RevokeEntitlementInput, aa as RuntimeHost, ab as RuntimeInfo, ac as ServerEvent, ad as StackFrame, ae as StoredEntitlements, af as SyncPurchaseInput, ag as makeCrossdeckError } from './crossdeck-server-NL1p6hlR.mjs';
export { A as AliasIdentityInput, a as AliasResult, b as AuditDecision, c as AuditEntry, B as BlockVerdict, d as Breadcrumb, e as BreadcrumbCategory, f as BreadcrumbLevel, C as CROSSDECK_API_VERSION, g as CampaignLinkInput, h as CampaignLinkResult, i as CapturedError, j as Contract, k as ContractAppliesTo, l as ContractFailureInput, m as ContractPillar, n as ContractStatus, o as ContractTestRef, p as CrossdeckAuthenticationError, q as CrossdeckConfigurationError, r as CrossdeckContracts, s as CrossdeckError, t as CrossdeckErrorPayload, u as CrossdeckErrorType, v as CrossdeckInternalError, w as CrossdeckNetworkError, x as CrossdeckPermissionError, y as CrossdeckRateLimitError, z as CrossdeckServer, D as CrossdeckServerOptions, E as CrossdeckValidationError, F as DEFAULT_BASE_URL, G as DEFAULT_TIMEOUT_MS, H as Diagnostics, I as EntitlementCacheOptions, J as EntitlementMutationResult, K as EntitlementStore, L as EntitlementsListResponse, M as EntitlementsListener, N as Environment, O as ErrorCaptureConfig, P as ErrorLevel, Q as EventProperties, R as ForgetResult, S as GateInput, T as GateVerdict, U as GrantDuration, V as GrantEntitlementInput, W as GroupMembership, X as HeartbeatResponse, Y as HttpRequestInfo, Z as HttpResponseInfo, _ as HttpRetriesConfig, $ as IdentifyOptions, a0 as IdentityHints, a1 as IngestOptions, a2 as IngestResponse, a3 as OwnerStatusInput, a4 as PublicEntitlement, a5 as PurchaseResult, a6 as RequestOptions, a7 as ResolveInput, a8 as ResolveResult, a9 as RevokeEntitlementInput, aa as RuntimeHost, ab as RuntimeInfo, ac as ServerEvent, ad as StackFrame, ae as StoredEntitlements, af as SyncPurchaseInput, ag as VerifyWebhookOptions, ah as WebhookEvent, ai as WebhookEventType, aj as constructEvent, ak as makeCrossdeckError, al as signWebhookPayload, am as verifyWebhookSignature, an as webhooks } from './crossdeck-server-D3sUzGKV.mjs';
import 'node:events';

@@ -20,3 +20,3 @@

*/
declare const SDK_VERSION = "1.14.0";
declare const SDK_VERSION = "1.15.0";
declare const SDK_NAME = "@cross-deck/node";

@@ -305,123 +305,2 @@

/**
* Webhook signature verification — Stripe pattern.
*
* **[ROADMAP — v1.4.0 honesty note]:** Crossdeck does NOT yet send
* outbound webhooks. Outbound delivery (signer + worker + scheduler
* + dead-letter dashboard) is on the post-v1.5 roadmap. This
* verifier exists today so customer-side integration code can be
* written and tested against fixtures (use `signWebhookPayload`
* to produce signed bodies for your local tests), and so the
* verification contract surface is locked in BEFORE delivery
* ships — Phase 7.2 of the bank-grade reconciliation tightened
* the timestamp-validation footguns here precisely because the
* helper IS the contract surface for inbound validation,
* regardless of when first-party delivery lights up.
*
* Lets customers verify the events Crossdeck sends to THEM (when
* delivery ships). Table-stakes for any backend SDK (Stripe ships
* `Stripe.webhooks.constructEvent()` from day one, Svix ships
* `Webhook.verify()` from day one).
*
* Wire format:
* Header `Crossdeck-Signature: t=<unix-seconds>,v1=<hex>`
* Where `v1` is HMAC-SHA256(secret, `${t}.${payload}`) — Stripe-compatible.
*
* Customers receive a signing secret from the Crossdeck dashboard
* (one-time reveal at mint time; rotated as needed). Each webhook
* carries the signature header above. The customer's handler:
*
* import { verifyWebhookSignature } from "@cross-deck/node";
*
* app.post("/crossdeck-webhook", express.raw({ type: "application/json" }), (req, res) => {
* try {
* const event = verifyWebhookSignature(
* req.body.toString("utf8"),
* req.headers["crossdeck-signature"],
* process.env.CROSSDECK_WEBHOOK_SECRET,
* );
* // event is the parsed JSON payload
* handleCrossdeckEvent(event);
* res.sendStatus(200);
* } catch (err) {
* res.sendStatus(401);
* }
* });
*
* The signing scheme is constant-time via `crypto.timingSafeEqual` so
* a malicious caller can't extract the signature by measuring response
* timing. Replay defence: timestamps older than `replayToleranceMs`
* (default 5 min) are rejected — required because HMAC-SHA256 is
* stateless and would otherwise allow an attacker to replay an old
* webhook indefinitely.
*
* Supports multiple secrets for rotation: pass an array; the helper
* tries each, accepts on the first match. Lets customers rotate the
* dashboard secret without dropping in-flight webhooks.
*/
interface VerifyWebhookOptions {
/**
* Maximum age of the webhook timestamp in milliseconds. Default
* 5 minutes (`DEFAULT_REPLAY_TOLERANCE_MS`). Anything older than
* this is rejected as a replay.
*
* **v1.4.0 Phase 7.2 bank-grade contract:** the timestamp window
* is MANDATORY. Pre-v1.4.0 the helper accepted `tolerance: 0`
* (silently disables the check) and `tolerance: Infinity` /
* `null` / `NaN` (silently disables via `Math.abs(...) > Infinity
* = false`). Customers relying on replay protection silently
* lost it.
*
* The helper now rejects non-finite / negative / above-cap
* tolerances at the boundary with a typed
* `webhook_invalid_tolerance` error. Hard upper bound is 24h —
* sufficient for any plausible clock-skew scenario, prevents
* "Infinity by typo" from defeating replay protection.
*/
replayToleranceMs?: number;
/**
* Override the current time. Tests use this to verify timestamp
* handling deterministically. Defaults to `Date.now()`.
*/
now?: () => number;
}
/**
* Verify a Crossdeck-signed webhook. Returns the parsed JSON payload
* on success. Throws `CrossdeckError` with one of these
* distinguishable codes (v1.4.0 Phase 7.2 — pre-v1.4.0 conflated
* everything under `webhook_invalid_signature`; alerting can now
* separate replay-attack signals from wrong-secret signals):
* - `webhook_missing_secret` — no secret configured.
* - `webhook_invalid_tolerance` — caller passed Infinity / NaN /
* negative / above-24h-cap `replayToleranceMs`.
* - `webhook_timestamp_missing` — header absent or has no `t=`.
* - `webhook_timestamp_outside_tolerance` — drift > tolerance
* (replay-attack signal — split this from signature mismatch
* in your alerting rules).
* - `webhook_signature_mismatch` — HMAC didn't match any
* configured secret (wrong-secret / rotation-drift signal).
* - `webhook_payload_not_json` — signature verified but the body
* isn't parseable JSON (tampered post-signing or source bug).
*
* `secret` accepts a single string or an array of strings (for
* rotation). Any one match is sufficient.
*/
declare function verifyWebhookSignature(payload: string, signatureHeader: string | string[] | undefined, secret: string | string[] | undefined, options?: VerifyWebhookOptions): unknown;
/**
* Pure-function signing — mirror of what the Crossdeck backend does
* when sending a webhook. Exported so customers building their own
* test fixtures (a service that sends Crossdeck-signed webhooks to
* their own test harness) can re-use the canonical signing scheme
* instead of re-implementing it.
*
* const ts = Math.floor(Date.now() / 1000);
* const sig = signWebhookPayload(payload, secret, ts);
* const header = `t=${ts},v1=${sig}`;
*
* NOT marked as a security primitive for general HMAC — use
* `node:crypto` directly for that. This is only the
* Crossdeck-signature shape.
*/
declare function signWebhookPayload(payload: string, secret: string, timestampSec: number): string;
/**
* PII scrub utilities — Node port of `@cross-deck/web/src/consent.ts`'s

@@ -515,2 +394,2 @@ * regex-based defence layer.

export { CROSSDECK_ERROR_CODES, type CrossdeckErrorCode, type DebugContext, type DebugLogger, type DebugSignal, type ErrorCodeEntry, SDK_NAME, SDK_VERSION, type VerifyWebhookOptions, getErrorCode, isCrossdeckErrorCode, scrubPii, scrubPiiFromProperties, signWebhookPayload, verifyWebhookSignature };
export { CROSSDECK_ERROR_CODES, type CrossdeckErrorCode, type DebugContext, type DebugLogger, type DebugSignal, type ErrorCodeEntry, SDK_NAME, SDK_VERSION, getErrorCode, isCrossdeckErrorCode, scrubPii, scrubPiiFromProperties };

@@ -1,2 +0,2 @@

export { A as AliasIdentityInput, a as AliasResult, b as AuditDecision, c as AuditEntry, B as BlockVerdict, d as Breadcrumb, e as BreadcrumbCategory, f as BreadcrumbLevel, C as CROSSDECK_API_VERSION, g as CampaignLinkInput, h as CampaignLinkResult, i as CapturedError, j as Contract, k as ContractAppliesTo, l as ContractFailureInput, m as ContractPillar, n as ContractStatus, o as ContractTestRef, p as CrossdeckAuthenticationError, q as CrossdeckConfigurationError, r as CrossdeckContracts, s as CrossdeckError, t as CrossdeckErrorPayload, u as CrossdeckErrorType, v as CrossdeckInternalError, w as CrossdeckNetworkError, x as CrossdeckPermissionError, y as CrossdeckRateLimitError, z as CrossdeckServer, D as CrossdeckServerOptions, E as CrossdeckValidationError, F as DEFAULT_BASE_URL, G as DEFAULT_TIMEOUT_MS, H as Diagnostics, I as EntitlementCacheOptions, J as EntitlementMutationResult, K as EntitlementStore, L as EntitlementsListResponse, M as EntitlementsListener, N as Environment, O as ErrorCaptureConfig, P as ErrorLevel, Q as EventProperties, R as ForgetResult, S as GateInput, T as GateVerdict, U as GrantDuration, V as GrantEntitlementInput, W as GroupMembership, X as HeartbeatResponse, Y as HttpRequestInfo, Z as HttpResponseInfo, _ as HttpRetriesConfig, $ as IdentifyOptions, a0 as IdentityHints, a1 as IngestOptions, a2 as IngestResponse, a3 as OwnerStatusInput, a4 as PublicEntitlement, a5 as PurchaseResult, a6 as RequestOptions, a7 as ResolveInput, a8 as ResolveResult, a9 as RevokeEntitlementInput, aa as RuntimeHost, ab as RuntimeInfo, ac as ServerEvent, ad as StackFrame, ae as StoredEntitlements, af as SyncPurchaseInput, ag as makeCrossdeckError } from './crossdeck-server-NL1p6hlR.js';
export { A as AliasIdentityInput, a as AliasResult, b as AuditDecision, c as AuditEntry, B as BlockVerdict, d as Breadcrumb, e as BreadcrumbCategory, f as BreadcrumbLevel, C as CROSSDECK_API_VERSION, g as CampaignLinkInput, h as CampaignLinkResult, i as CapturedError, j as Contract, k as ContractAppliesTo, l as ContractFailureInput, m as ContractPillar, n as ContractStatus, o as ContractTestRef, p as CrossdeckAuthenticationError, q as CrossdeckConfigurationError, r as CrossdeckContracts, s as CrossdeckError, t as CrossdeckErrorPayload, u as CrossdeckErrorType, v as CrossdeckInternalError, w as CrossdeckNetworkError, x as CrossdeckPermissionError, y as CrossdeckRateLimitError, z as CrossdeckServer, D as CrossdeckServerOptions, E as CrossdeckValidationError, F as DEFAULT_BASE_URL, G as DEFAULT_TIMEOUT_MS, H as Diagnostics, I as EntitlementCacheOptions, J as EntitlementMutationResult, K as EntitlementStore, L as EntitlementsListResponse, M as EntitlementsListener, N as Environment, O as ErrorCaptureConfig, P as ErrorLevel, Q as EventProperties, R as ForgetResult, S as GateInput, T as GateVerdict, U as GrantDuration, V as GrantEntitlementInput, W as GroupMembership, X as HeartbeatResponse, Y as HttpRequestInfo, Z as HttpResponseInfo, _ as HttpRetriesConfig, $ as IdentifyOptions, a0 as IdentityHints, a1 as IngestOptions, a2 as IngestResponse, a3 as OwnerStatusInput, a4 as PublicEntitlement, a5 as PurchaseResult, a6 as RequestOptions, a7 as ResolveInput, a8 as ResolveResult, a9 as RevokeEntitlementInput, aa as RuntimeHost, ab as RuntimeInfo, ac as ServerEvent, ad as StackFrame, ae as StoredEntitlements, af as SyncPurchaseInput, ag as VerifyWebhookOptions, ah as WebhookEvent, ai as WebhookEventType, aj as constructEvent, ak as makeCrossdeckError, al as signWebhookPayload, am as verifyWebhookSignature, an as webhooks } from './crossdeck-server-D3sUzGKV.js';
import 'node:events';

@@ -20,3 +20,3 @@

*/
declare const SDK_VERSION = "1.14.0";
declare const SDK_VERSION = "1.15.0";
declare const SDK_NAME = "@cross-deck/node";

@@ -305,123 +305,2 @@

/**
* Webhook signature verification — Stripe pattern.
*
* **[ROADMAP — v1.4.0 honesty note]:** Crossdeck does NOT yet send
* outbound webhooks. Outbound delivery (signer + worker + scheduler
* + dead-letter dashboard) is on the post-v1.5 roadmap. This
* verifier exists today so customer-side integration code can be
* written and tested against fixtures (use `signWebhookPayload`
* to produce signed bodies for your local tests), and so the
* verification contract surface is locked in BEFORE delivery
* ships — Phase 7.2 of the bank-grade reconciliation tightened
* the timestamp-validation footguns here precisely because the
* helper IS the contract surface for inbound validation,
* regardless of when first-party delivery lights up.
*
* Lets customers verify the events Crossdeck sends to THEM (when
* delivery ships). Table-stakes for any backend SDK (Stripe ships
* `Stripe.webhooks.constructEvent()` from day one, Svix ships
* `Webhook.verify()` from day one).
*
* Wire format:
* Header `Crossdeck-Signature: t=<unix-seconds>,v1=<hex>`
* Where `v1` is HMAC-SHA256(secret, `${t}.${payload}`) — Stripe-compatible.
*
* Customers receive a signing secret from the Crossdeck dashboard
* (one-time reveal at mint time; rotated as needed). Each webhook
* carries the signature header above. The customer's handler:
*
* import { verifyWebhookSignature } from "@cross-deck/node";
*
* app.post("/crossdeck-webhook", express.raw({ type: "application/json" }), (req, res) => {
* try {
* const event = verifyWebhookSignature(
* req.body.toString("utf8"),
* req.headers["crossdeck-signature"],
* process.env.CROSSDECK_WEBHOOK_SECRET,
* );
* // event is the parsed JSON payload
* handleCrossdeckEvent(event);
* res.sendStatus(200);
* } catch (err) {
* res.sendStatus(401);
* }
* });
*
* The signing scheme is constant-time via `crypto.timingSafeEqual` so
* a malicious caller can't extract the signature by measuring response
* timing. Replay defence: timestamps older than `replayToleranceMs`
* (default 5 min) are rejected — required because HMAC-SHA256 is
* stateless and would otherwise allow an attacker to replay an old
* webhook indefinitely.
*
* Supports multiple secrets for rotation: pass an array; the helper
* tries each, accepts on the first match. Lets customers rotate the
* dashboard secret without dropping in-flight webhooks.
*/
interface VerifyWebhookOptions {
/**
* Maximum age of the webhook timestamp in milliseconds. Default
* 5 minutes (`DEFAULT_REPLAY_TOLERANCE_MS`). Anything older than
* this is rejected as a replay.
*
* **v1.4.0 Phase 7.2 bank-grade contract:** the timestamp window
* is MANDATORY. Pre-v1.4.0 the helper accepted `tolerance: 0`
* (silently disables the check) and `tolerance: Infinity` /
* `null` / `NaN` (silently disables via `Math.abs(...) > Infinity
* = false`). Customers relying on replay protection silently
* lost it.
*
* The helper now rejects non-finite / negative / above-cap
* tolerances at the boundary with a typed
* `webhook_invalid_tolerance` error. Hard upper bound is 24h —
* sufficient for any plausible clock-skew scenario, prevents
* "Infinity by typo" from defeating replay protection.
*/
replayToleranceMs?: number;
/**
* Override the current time. Tests use this to verify timestamp
* handling deterministically. Defaults to `Date.now()`.
*/
now?: () => number;
}
/**
* Verify a Crossdeck-signed webhook. Returns the parsed JSON payload
* on success. Throws `CrossdeckError` with one of these
* distinguishable codes (v1.4.0 Phase 7.2 — pre-v1.4.0 conflated
* everything under `webhook_invalid_signature`; alerting can now
* separate replay-attack signals from wrong-secret signals):
* - `webhook_missing_secret` — no secret configured.
* - `webhook_invalid_tolerance` — caller passed Infinity / NaN /
* negative / above-24h-cap `replayToleranceMs`.
* - `webhook_timestamp_missing` — header absent or has no `t=`.
* - `webhook_timestamp_outside_tolerance` — drift > tolerance
* (replay-attack signal — split this from signature mismatch
* in your alerting rules).
* - `webhook_signature_mismatch` — HMAC didn't match any
* configured secret (wrong-secret / rotation-drift signal).
* - `webhook_payload_not_json` — signature verified but the body
* isn't parseable JSON (tampered post-signing or source bug).
*
* `secret` accepts a single string or an array of strings (for
* rotation). Any one match is sufficient.
*/
declare function verifyWebhookSignature(payload: string, signatureHeader: string | string[] | undefined, secret: string | string[] | undefined, options?: VerifyWebhookOptions): unknown;
/**
* Pure-function signing — mirror of what the Crossdeck backend does
* when sending a webhook. Exported so customers building their own
* test fixtures (a service that sends Crossdeck-signed webhooks to
* their own test harness) can re-use the canonical signing scheme
* instead of re-implementing it.
*
* const ts = Math.floor(Date.now() / 1000);
* const sig = signWebhookPayload(payload, secret, ts);
* const header = `t=${ts},v1=${sig}`;
*
* NOT marked as a security primitive for general HMAC — use
* `node:crypto` directly for that. This is only the
* Crossdeck-signature shape.
*/
declare function signWebhookPayload(payload: string, secret: string, timestampSec: number): string;
/**
* PII scrub utilities — Node port of `@cross-deck/web/src/consent.ts`'s

@@ -515,2 +394,2 @@ * regex-based defence layer.

export { CROSSDECK_ERROR_CODES, type CrossdeckErrorCode, type DebugContext, type DebugLogger, type DebugSignal, type ErrorCodeEntry, SDK_NAME, SDK_VERSION, type VerifyWebhookOptions, getErrorCode, isCrossdeckErrorCode, scrubPii, scrubPiiFromProperties, signWebhookPayload, verifyWebhookSignature };
export { CROSSDECK_ERROR_CODES, type CrossdeckErrorCode, type DebugContext, type DebugLogger, type DebugSignal, type ErrorCodeEntry, SDK_NAME, SDK_VERSION, getErrorCode, isCrossdeckErrorCode, scrubPii, scrubPiiFromProperties };
{
"name": "@cross-deck/node",
"version": "1.14.0",
"version": "1.15.0",
"description": "Crossdeck server SDK for Node.js — verified subscriptions, entitlements, server-side error capture, and product telemetry in one client.",

@@ -5,0 +5,0 @@ "license": "MIT",

+13
-11

@@ -188,27 +188,29 @@ # @cross-deck/node

#### Webhook signature verification
#### Receiving webhooks — `webhooks.constructEvent()`
Stripe-compatible HMAC-SHA256 with constant-time comparison + replay window. Supports multi-secret rotation.
Crossdeck sends signed, retried, at-least-once outbound webhooks to endpoints you register (`trust.rule.added` / `trust.rule.removed` today; more ride the same spine). Verify each one in one line — Stripe-compatible HMAC-SHA256, constant-time comparison, mandatory replay window, multi-secret rotation. `constructEvent` returns the **typed** `WebhookEvent` envelope; a throw means "do not act."
```ts
import { verifyWebhookSignature } from "@cross-deck/node";
import { webhooks } from "@cross-deck/node";
import express from "express";
app.post("/crossdeck-webhook", express.raw({ type: "application/json" }), (req, res) => {
app.post("/crossdeck-webhook", express.raw({ type: "application/json" }), async (req, res) => {
let event;
try {
const event = verifyWebhookSignature(
req.body.toString("utf8"),
event = webhooks.constructEvent(
req.body.toString("utf8"), // the RAW bytes, unparsed
req.headers["crossdeck-signature"],
[process.env.CROSSDECK_WEBHOOK_SECRET, process.env.CROSSDECK_WEBHOOK_SECRET_OLD],
// 5-min default replay window
);
handleCrossdeckEvent(event);
res.sendStatus(200);
} catch (err) {
res.sendStatus(401);
return res.sendStatus(401); // bad signature / replay — reject
}
res.sendStatus(200); // ACK fast, then reconcile out of band:
const truth = await fetch(event.reconcile.url).then((r) => r.json());
enforceFrom(truth); // a webhook is a NUDGE — read the truth it points at
});
```
For test fixtures that need to mint signed webhooks against the same scheme, `signWebhookPayload(payload, secret, timestampSec)` is exported.
`crossdeck.webhooks.constructEvent(...)` is the same function mounted on the client. The lower-level `verifyWebhookSignature()` (returns the parsed body as `unknown`) and `signWebhookPayload(payload, secret, timestampSec)` (mint signed bodies for test fixtures) are also exported. Full reference: [Receiving Crossdeck webhooks](https://cross-deck.com/docs/webhooks-receive/).

@@ -215,0 +217,0 @@ ### Blocking (Crossdeck Trust) — <sup>v2 preview</sup>

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display