@cross-deck/node 1.2.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -1,6 +1,25 @@
1
- export { A as AliasIdentityInput, a as AliasResult, b as AuditDecision, c as AuditEntry, B as Breadcrumb, d as BreadcrumbCategory, e as BreadcrumbLevel, C as CROSSDECK_API_VERSION, f as CapturedError, g as CrossdeckAuthenticationError, h as CrossdeckConfigurationError, i as CrossdeckError, j as CrossdeckErrorPayload, k as CrossdeckErrorType, l as CrossdeckInternalError, m as CrossdeckNetworkError, n as CrossdeckPermissionError, o as CrossdeckRateLimitError, p as CrossdeckServer, q as CrossdeckServerOptions, r as CrossdeckValidationError, D as DEFAULT_BASE_URL, s as DEFAULT_TIMEOUT_MS, t as Diagnostics, E as EntitlementCacheOptions, u as EntitlementMutationResult, v as EntitlementStore, w as EntitlementsListResponse, x as EntitlementsListener, y as Environment, z as ErrorCaptureConfig, F as ErrorLevel, G as EventProperties, H as ForgetResult, I as GrantDuration, J as GrantEntitlementInput, K as GroupMembership, L as HeartbeatResponse, M as HttpRequestInfo, N as HttpResponseInfo, O as HttpRetriesConfig, P as IdentifyOptions, Q as IdentityHints, R as IngestOptions, S as IngestResponse, T as PublicEntitlement, U as PurchaseResult, V as RequestOptions, W as RevokeEntitlementInput, X as RuntimeHost, Y as RuntimeInfo, Z as SDK_NAME, _ as SDK_VERSION, $ as ServerEvent, a0 as StackFrame, a1 as StoredEntitlements, a2 as SyncPurchaseInput, a3 as makeCrossdeckError } from './crossdeck-server-BZVZEuS-.mjs';
1
+ export { A as AliasIdentityInput, a as AliasResult, b as AuditDecision, c as AuditEntry, B as Breadcrumb, d as BreadcrumbCategory, e as BreadcrumbLevel, C as CROSSDECK_API_VERSION, f as CapturedError, g as Contract, h as ContractAppliesTo, i as ContractFailureInput, j as ContractPillar, k as ContractStatus, l as ContractTestRef, m as CrossdeckAuthenticationError, n as CrossdeckConfigurationError, o as CrossdeckContracts, p as CrossdeckError, q as CrossdeckErrorPayload, r as CrossdeckErrorType, s as CrossdeckInternalError, t as CrossdeckNetworkError, u as CrossdeckPermissionError, v as CrossdeckRateLimitError, w as CrossdeckServer, x as CrossdeckServerOptions, y as CrossdeckValidationError, D as DEFAULT_BASE_URL, z as DEFAULT_TIMEOUT_MS, E as Diagnostics, F as EntitlementCacheOptions, G as EntitlementMutationResult, H as EntitlementStore, I as EntitlementsListResponse, J as EntitlementsListener, K as Environment, L as ErrorCaptureConfig, M as ErrorLevel, N as EventProperties, O as ForgetResult, P as GrantDuration, Q as GrantEntitlementInput, R as GroupMembership, S as HeartbeatResponse, T as HttpRequestInfo, U as HttpResponseInfo, V as HttpRetriesConfig, W as IdentifyOptions, X as IdentityHints, Y as IngestOptions, Z as IngestResponse, _ as PublicEntitlement, $ as PurchaseResult, a0 as RequestOptions, a1 as RevokeEntitlementInput, a2 as RuntimeHost, a3 as RuntimeInfo, a4 as ServerEvent, a5 as StackFrame, a6 as StoredEntitlements, a7 as SyncPurchaseInput, a8 as makeCrossdeckError } from './crossdeck-server-oAaKBnUU.mjs';
2
2
  import 'node:events';
3
3
 
4
+ /**
5
+ * SDK version constant — generated by `scripts/sync-sdk-versions.mjs`.
6
+ *
7
+ * Single source of truth: the `version` field in this package's
8
+ * package.json. The sync script writes this file so that
9
+ * `SDK_VERSION` is a plain TypeScript literal at runtime — no
10
+ * runtime JSON-import gotcha (Node ESM requires
11
+ * `with { type: "json" }` to import JSON as ESM, and the published
12
+ * dist file would otherwise fail to load).
13
+ *
14
+ * Drift protection: `node scripts/sync-sdk-versions.mjs --check` (the
15
+ * CI gate) flags this file when it falls out of sync with package.json.
16
+ * Bumping `package.json` without re-running the sync script fails CI.
17
+ *
18
+ * Do NOT edit by hand — `node scripts/sync-sdk-versions.mjs`.
19
+ */
20
+ declare const SDK_VERSION = "1.4.2";
21
+ declare const SDK_NAME = "@cross-deck/node";
22
+
4
23
  /**
5
24
  * Machine-readable index of every error code `@cross-deck/node` can
6
25
  * throw, with a short description and a hint on what action to take.
@@ -139,16 +158,34 @@ declare const _CROSSDECK_ERROR_CODES: readonly [{
139
158
  readonly resolution: "Increase flushOnExitTimeoutMs in CrossdeckServer options. Default is 2000ms; serverless runtimes typically allow 5-10s before SIGKILL. If events are dropping silently in production, raise this.";
140
159
  readonly retryable: false;
141
160
  }, {
142
- readonly code: "webhook_invalid_signature";
161
+ readonly code: "webhook_signature_mismatch";
143
162
  readonly type: "authentication_error";
144
- readonly description: "The webhook signature header did not verify against the supplied secret.";
145
- readonly resolution: "Confirm the secret matches the one in your Crossdeck dashboard → Webhooks page. If the request is genuinely from Crossdeck, the secret is wrong, stale, or recently rotated.";
163
+ readonly description: "Webhook HMAC didn't verify against any configured secret (wrong-secret / stale rotation signal).";
164
+ readonly resolution: "Confirm the secret matches dashboard → Webhooks. If you rotated, include both the old and new secret as an array until receivers cut over.";
146
165
  readonly retryable: false;
147
166
  }, {
148
- readonly code: "webhook_replay_window_exceeded";
167
+ readonly code: "webhook_timestamp_outside_tolerance";
149
168
  readonly type: "authentication_error";
150
- readonly description: "The webhook timestamp is older than the replay-tolerance window (default 5 minutes).";
151
- readonly resolution: "The webhook is either replayed or your receiving clock is wildly skewed. Verify NTP on the receiving host. Increase replayToleranceMs only if you accept the replay-attack risk.";
169
+ readonly description: "Webhook timestamp drift exceeds the configured replay-tolerance window (default 5 minutes; replay-attack signal).";
170
+ readonly resolution: "Verify NTP on the receiving host. A spike on this code warrants its own alert separate from signature_mismatch replay attacks look like this.";
171
+ readonly retryable: false;
172
+ }, {
173
+ readonly code: "webhook_timestamp_missing";
174
+ readonly type: "authentication_error";
175
+ readonly description: "Webhook signature header is absent or has no `t=` timestamp segment — the timestamp gate cannot be verified.";
176
+ readonly resolution: "Confirm the request actually came from Crossdeck (signature headers are always present on real deliveries). A missing header is either a misconfigured intermediary or a forged request.";
177
+ readonly retryable: false;
178
+ }, {
179
+ readonly code: "webhook_payload_not_json";
180
+ readonly type: "authentication_error";
181
+ readonly description: "Webhook signature verified but the body isn't valid JSON — payload tampered post-signing or source bug.";
182
+ readonly resolution: "Inspect the raw payload. If it's not JSON, either the request was modified in transit or the sender has a bug — file a support ticket with the raw body.";
183
+ readonly retryable: false;
184
+ }, {
185
+ readonly code: "webhook_invalid_tolerance";
186
+ readonly type: "configuration_error";
187
+ readonly description: "verifyWebhookSignature() called with a non-finite / negative / above-24h-cap replayToleranceMs (would silently disable replay protection).";
188
+ readonly resolution: "Pass a finite number between 0 and 86_400_000ms (24h). Default (5 minutes) is correct for almost every scenario. Pre-v1.4.0 accepted Infinity/NaN and silently dropped the check.";
152
189
  readonly retryable: false;
153
190
  }, {
154
191
  readonly code: "webhook_missing_secret";
@@ -156,6 +193,84 @@ declare const _CROSSDECK_ERROR_CODES: readonly [{
156
193
  readonly description: "verifyWebhookSignature() was called without a signing secret.";
157
194
  readonly resolution: "Pass the secret from your Crossdeck dashboard → Webhooks page. Never hardcode in source — read from an env var.";
158
195
  readonly retryable: false;
196
+ }, {
197
+ readonly code: "webhook_invalid_signature";
198
+ readonly type: "authentication_error";
199
+ readonly description: "DEPRECATED in v1.4.0 — split into webhook_signature_mismatch / webhook_timestamp_missing / webhook_timestamp_outside_tolerance / webhook_payload_not_json for alerting clarity.";
200
+ readonly resolution: "Migrate alert rules to the more specific v1.4.0 codes — they distinguish replay-attack signals from wrong-secret signals.";
201
+ readonly retryable: false;
202
+ }, {
203
+ readonly code: "webhook_replay_window_exceeded";
204
+ readonly type: "authentication_error";
205
+ readonly description: "DEPRECATED in v1.4.0 — renamed to webhook_timestamp_outside_tolerance.";
206
+ readonly resolution: "Update alerts to webhook_timestamp_outside_tolerance.";
207
+ readonly retryable: false;
208
+ }, {
209
+ readonly code: "missing_api_key";
210
+ readonly type: "authentication_error";
211
+ readonly description: "No Authorization header (or Crossdeck-Api-Key header) on the request.";
212
+ readonly resolution: "Confirm the CrossdeckServer was constructed with a cd_sk_… secretKey. Re-check env vars in production deployments.";
213
+ readonly retryable: false;
214
+ }, {
215
+ readonly code: "invalid_api_key";
216
+ readonly type: "authentication_error";
217
+ readonly description: "The secret key is malformed, unknown, or doesn't resolve to a project.";
218
+ readonly resolution: "Copy the key from Crossdeck dashboard → API keys. Server SDK requires cd_sk_test_ / cd_sk_live_ — client SDK keys (cd_pub_…) won't work on the Node SDK.";
219
+ readonly retryable: false;
220
+ }, {
221
+ readonly code: "key_revoked";
222
+ readonly type: "authentication_error";
223
+ readonly description: "The secret key was revoked in the dashboard.";
224
+ readonly resolution: "Mint a fresh key in dashboard → API keys → Create new. The revoked key cannot be reactivated.";
225
+ readonly retryable: false;
226
+ }, {
227
+ readonly code: "env_mismatch";
228
+ readonly type: "permission_error";
229
+ readonly description: "The key's env prefix doesn't match the resolved app's configured env.";
230
+ readonly resolution: "Use a cd_sk_live_ key with a production app, cd_sk_test_ with a sandbox app. Crossing breaks the env lock.";
231
+ readonly retryable: false;
232
+ }, {
233
+ readonly code: "idempotency_key_in_use";
234
+ readonly type: "invalid_request_error";
235
+ readonly description: "An Idempotency-Key was reused for a request with a different body (Stripe-grade contract).";
236
+ readonly resolution: "Server SDK derives keys deterministically from the body since v1.4.0; this should only fire if you passed options.idempotencyKey explicitly. Use a fresh key per logical operation.";
237
+ readonly retryable: false;
238
+ }, {
239
+ readonly code: "rate_limited";
240
+ readonly type: "rate_limit_error";
241
+ readonly description: "Request rate exceeded the project's per-second cap.";
242
+ readonly resolution: "Honour Retry-After (managed retries do this automatically). For custom paths, throttle to <100 req/s/key.";
243
+ readonly retryable: true;
244
+ }, {
245
+ readonly code: "internal_error";
246
+ readonly type: "internal_error";
247
+ readonly description: "Server-side issue. Safe to retry with backoff.";
248
+ readonly resolution: "Managed retries handle this automatically. If a code path surfaces it to your code, contact support with the requestId.";
249
+ readonly retryable: true;
250
+ }, {
251
+ readonly code: "google_not_supported";
252
+ readonly type: "invalid_request_error";
253
+ readonly description: "POST /purchases/sync with rail=google is gated until the Play Developer API reconciliation worker ships.";
254
+ readonly resolution: "Until v1.5+, Google Play purchases verify via Real-time Developer Notifications. The Android SDK auto-track path handles this transparently.";
255
+ readonly retryable: false;
256
+ }, {
257
+ readonly code: "stripe_not_supported";
258
+ readonly type: "invalid_request_error";
259
+ readonly description: "POST /purchases/sync with rail=stripe is unsupported — Stripe webhooks deliver evidence server-side.";
260
+ readonly resolution: "Wire Stripe via the standard Checkout / Customer Portal flow; Crossdeck reconciles via the platform webhook automatically.";
261
+ readonly retryable: false;
262
+ }, {
263
+ readonly code: "missing_required_param";
264
+ readonly type: "invalid_request_error";
265
+ readonly description: "A required field is absent from the request body.";
266
+ readonly resolution: "The error.message identifies the missing field. Refer to the SDK's TypeScript types for canonical shapes.";
267
+ readonly retryable: false;
268
+ }, {
269
+ readonly code: "invalid_param_value";
270
+ readonly type: "invalid_request_error";
271
+ readonly description: "A field is present but the value failed validation.";
272
+ readonly resolution: "Read error.message for the field + reason. SDK-managed call sites should never emit this — file a bug if you do.";
273
+ readonly retryable: false;
159
274
  }];
160
275
  /**
161
276
  * Literal union of every code documented in `CROSSDECK_ERROR_CODES`.
@@ -182,9 +297,22 @@ declare function getErrorCode(code: string): ErrorCodeEntry | undefined;
182
297
  /**
183
298
  * Webhook signature verification — Stripe pattern.
184
299
  *
185
- * Lets customers verify the events Crossdeck sends to THEM. Table-stakes
186
- * for any backend SDK (Stripe ships `Stripe.webhooks.constructEvent()`
187
- * from day one, Svix ships `Webhook.verify()` from day one).
300
+ * **[ROADMAP v1.4.0 honesty note]:** Crossdeck does NOT yet send
301
+ * outbound webhooks. Outbound delivery (signer + worker + scheduler
302
+ * + dead-letter dashboard) is on the post-v1.5 roadmap. This
303
+ * verifier exists today so customer-side integration code can be
304
+ * written and tested against fixtures (use `signWebhookPayload`
305
+ * to produce signed bodies for your local tests), and so the
306
+ * verification contract surface is locked in BEFORE delivery
307
+ * ships — Phase 7.2 of the bank-grade reconciliation tightened
308
+ * the timestamp-validation footguns here precisely because the
309
+ * helper IS the contract surface for inbound validation,
310
+ * regardless of when first-party delivery lights up.
311
+ *
312
+ * Lets customers verify the events Crossdeck sends to THEM (when
313
+ * delivery ships). Table-stakes for any backend SDK (Stripe ships
314
+ * `Stripe.webhooks.constructEvent()` from day one, Svix ships
315
+ * `Webhook.verify()` from day one).
188
316
  *
189
317
  * Wire format:
190
318
  * Header `Crossdeck-Signature: t=<unix-seconds>,v1=<hex>`
@@ -225,9 +353,21 @@ declare function getErrorCode(code: string): ErrorCodeEntry | undefined;
225
353
  interface VerifyWebhookOptions {
226
354
  /**
227
355
  * Maximum age of the webhook timestamp in milliseconds. Default
228
- * 5 minutes (5 * 60 * 1000). Anything older than this is rejected
229
- * as a replay. Pass 0 to disable the replay window (NOT recommended
230
- * — accept the trade-off only if you have a separate replay defence).
356
+ * 5 minutes (`DEFAULT_REPLAY_TOLERANCE_MS`). Anything older than
357
+ * this is rejected as a replay.
358
+ *
359
+ * **v1.4.0 Phase 7.2 bank-grade contract:** the timestamp window
360
+ * is MANDATORY. Pre-v1.4.0 the helper accepted `tolerance: 0`
361
+ * (silently disables the check) and `tolerance: Infinity` /
362
+ * `null` / `NaN` (silently disables via `Math.abs(...) > Infinity
363
+ * = false`). Customers relying on replay protection silently
364
+ * lost it.
365
+ *
366
+ * The helper now rejects non-finite / negative / above-cap
367
+ * tolerances at the boundary with a typed
368
+ * `webhook_invalid_tolerance` error. Hard upper bound is 24h —
369
+ * sufficient for any plausible clock-skew scenario, prevents
370
+ * "Infinity by typo" from defeating replay protection.
231
371
  */
232
372
  replayToleranceMs?: number;
233
373
  /**
@@ -238,13 +378,21 @@ interface VerifyWebhookOptions {
238
378
  }
239
379
  /**
240
380
  * Verify a Crossdeck-signed webhook. Returns the parsed JSON payload
241
- * on success. Throws `CrossdeckError` on:
242
- * - missing / malformed signature header (`webhook_invalid_signature`)
243
- * - missing secret (`webhook_missing_secret`)
244
- * - timestamp outside replay window (`webhook_replay_window_exceeded`)
245
- * - HMAC mismatch (`webhook_invalid_signature`)
246
- * - non-JSON payload (`webhook_invalid_signature` — same code because
247
- * a tampered payload that breaks JSON parses as invalid)
381
+ * on success. Throws `CrossdeckError` with one of these
382
+ * distinguishable codes (v1.4.0 Phase 7.2 — pre-v1.4.0 conflated
383
+ * everything under `webhook_invalid_signature`; alerting can now
384
+ * separate replay-attack signals from wrong-secret signals):
385
+ * - `webhook_missing_secret` no secret configured.
386
+ * - `webhook_invalid_tolerance` — caller passed Infinity / NaN /
387
+ * negative / above-24h-cap `replayToleranceMs`.
388
+ * - `webhook_timestamp_missing` — header absent or has no `t=`.
389
+ * - `webhook_timestamp_outside_tolerance` — drift > tolerance
390
+ * (replay-attack signal — split this from signature mismatch
391
+ * in your alerting rules).
392
+ * - `webhook_signature_mismatch` — HMAC didn't match any
393
+ * configured secret (wrong-secret / rotation-drift signal).
394
+ * - `webhook_payload_not_json` — signature verified but the body
395
+ * isn't parseable JSON (tampered post-signing or source bug).
248
396
  *
249
397
  * `secret` accepts a single string or an array of strings (for
250
398
  * rotation). Any one match is sufficient.
@@ -289,16 +437,24 @@ declare function signWebhookPayload(payload: string, secret: string, timestampSe
289
437
  * name: "checkout.started",
290
438
  * developerUserId: userId,
291
439
  * properties: scrubPiiFromProperties({
292
- * url: req.url, // might contain "/users/wes@…/" — gets [email]
440
+ * url: req.url, // might contain "/users/wes@…/" — gets <email>
293
441
  * lastError: e.message, // might contain card numbers
294
442
  * }),
295
443
  * });
296
444
  */
297
445
  /**
298
446
  * Scrub a single string value: replace email-shaped substrings with
299
- * `[email]` and card-number-shaped substrings with `[card]`. Returns
300
- * the original string (===) when nothing matched, so callers can do
301
- * an identity-check to skip allocating a new event copy.
447
+ * `<email>` and card-number-shaped substrings with `<card>`. Returns
448
+ * the original string (===) when nothing matched.
449
+ *
450
+ * Implementation note: we call `.replace()` unconditionally rather than
451
+ * gating on `.test()`. The /g regexes are module-level so `.test()`
452
+ * carries `lastIndex` state between calls — a prior match leaves
453
+ * `lastIndex` mid-string and the next `.test()` can falsely return
454
+ * false on a string that DOES match. `.replace(/g)` always scans the
455
+ * full string regardless of `lastIndex`, so dropping the test-guard
456
+ * removes the sharp edge at zero cost (when nothing matches, replace
457
+ * returns the same `(===)` string).
302
458
  */
303
459
  declare function scrubPii(value: string): string;
304
460
  /**
@@ -339,7 +495,7 @@ declare function scrubPiiFromProperties(properties: Record<string, unknown>): Re
339
495
  * - `sdk.no_durable_store`
340
496
  * - `sdk.super_property_registered`
341
497
  */
342
- type DebugSignal = "sdk.configured" | "sdk.first_event_sent" | "sdk.invalid_key" | "sdk.no_identity" | "sdk.entitlement_cache_used" | "sdk.entitlement_cache_warm" | "sdk.entitlement_cache_stale" | "sdk.entitlement_store_recovered" | "sdk.no_durable_store" | "sdk.purchase_evidence_sent" | "sdk.environment_mismatch" | "sdk.sensitive_property_warning" | "sdk.property_coerced" | "sdk.flush_retry_scheduled" | "sdk.flush_on_exit_started" | "sdk.flush_on_exit_completed" | "sdk.webhook_verified" | "sdk.runtime_detected" | "sdk.super_property_registered" | "sdk.boot_heartbeat_failed";
498
+ type DebugSignal = "sdk.configured" | "sdk.first_event_sent" | "sdk.invalid_key" | "sdk.no_identity" | "sdk.entitlement_cache_used" | "sdk.entitlement_cache_warm" | "sdk.entitlement_cache_stale" | "sdk.entitlement_store_recovered" | "sdk.no_durable_store" | "sdk.purchase_evidence_sent" | "sdk.environment_mismatch" | "sdk.sensitive_property_warning" | "sdk.property_coerced" | "sdk.flush_retry_scheduled" | "sdk.flush_permanent_failure" | "sdk.flush_on_exit_started" | "sdk.flush_on_exit_completed" | "sdk.webhook_verified" | "sdk.runtime_detected" | "sdk.super_property_registered" | "sdk.boot_heartbeat_failed";
343
499
  interface DebugContext {
344
500
  [key: string]: unknown;
345
501
  }
@@ -348,4 +504,4 @@ interface DebugLogger {
348
504
  emit(signal: DebugSignal, message: string, context?: DebugContext): void;
349
505
  }
350
506
 
351
- export { CROSSDECK_ERROR_CODES, type CrossdeckErrorCode, type DebugContext, type DebugLogger, type DebugSignal, type ErrorCodeEntry, type VerifyWebhookOptions, getErrorCode, isCrossdeckErrorCode, scrubPii, scrubPiiFromProperties, signWebhookPayload, verifyWebhookSignature };
507
+ 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 };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,25 @@
1
- export { A as AliasIdentityInput, a as AliasResult, b as AuditDecision, c as AuditEntry, B as Breadcrumb, d as BreadcrumbCategory, e as BreadcrumbLevel, C as CROSSDECK_API_VERSION, f as CapturedError, g as CrossdeckAuthenticationError, h as CrossdeckConfigurationError, i as CrossdeckError, j as CrossdeckErrorPayload, k as CrossdeckErrorType, l as CrossdeckInternalError, m as CrossdeckNetworkError, n as CrossdeckPermissionError, o as CrossdeckRateLimitError, p as CrossdeckServer, q as CrossdeckServerOptions, r as CrossdeckValidationError, D as DEFAULT_BASE_URL, s as DEFAULT_TIMEOUT_MS, t as Diagnostics, E as EntitlementCacheOptions, u as EntitlementMutationResult, v as EntitlementStore, w as EntitlementsListResponse, x as EntitlementsListener, y as Environment, z as ErrorCaptureConfig, F as ErrorLevel, G as EventProperties, H as ForgetResult, I as GrantDuration, J as GrantEntitlementInput, K as GroupMembership, L as HeartbeatResponse, M as HttpRequestInfo, N as HttpResponseInfo, O as HttpRetriesConfig, P as IdentifyOptions, Q as IdentityHints, R as IngestOptions, S as IngestResponse, T as PublicEntitlement, U as PurchaseResult, V as RequestOptions, W as RevokeEntitlementInput, X as RuntimeHost, Y as RuntimeInfo, Z as SDK_NAME, _ as SDK_VERSION, $ as ServerEvent, a0 as StackFrame, a1 as StoredEntitlements, a2 as SyncPurchaseInput, a3 as makeCrossdeckError } from './crossdeck-server-BZVZEuS-.js';
1
+ export { A as AliasIdentityInput, a as AliasResult, b as AuditDecision, c as AuditEntry, B as Breadcrumb, d as BreadcrumbCategory, e as BreadcrumbLevel, C as CROSSDECK_API_VERSION, f as CapturedError, g as Contract, h as ContractAppliesTo, i as ContractFailureInput, j as ContractPillar, k as ContractStatus, l as ContractTestRef, m as CrossdeckAuthenticationError, n as CrossdeckConfigurationError, o as CrossdeckContracts, p as CrossdeckError, q as CrossdeckErrorPayload, r as CrossdeckErrorType, s as CrossdeckInternalError, t as CrossdeckNetworkError, u as CrossdeckPermissionError, v as CrossdeckRateLimitError, w as CrossdeckServer, x as CrossdeckServerOptions, y as CrossdeckValidationError, D as DEFAULT_BASE_URL, z as DEFAULT_TIMEOUT_MS, E as Diagnostics, F as EntitlementCacheOptions, G as EntitlementMutationResult, H as EntitlementStore, I as EntitlementsListResponse, J as EntitlementsListener, K as Environment, L as ErrorCaptureConfig, M as ErrorLevel, N as EventProperties, O as ForgetResult, P as GrantDuration, Q as GrantEntitlementInput, R as GroupMembership, S as HeartbeatResponse, T as HttpRequestInfo, U as HttpResponseInfo, V as HttpRetriesConfig, W as IdentifyOptions, X as IdentityHints, Y as IngestOptions, Z as IngestResponse, _ as PublicEntitlement, $ as PurchaseResult, a0 as RequestOptions, a1 as RevokeEntitlementInput, a2 as RuntimeHost, a3 as RuntimeInfo, a4 as ServerEvent, a5 as StackFrame, a6 as StoredEntitlements, a7 as SyncPurchaseInput, a8 as makeCrossdeckError } from './crossdeck-server-oAaKBnUU.js';
2
2
  import 'node:events';
3
3
 
4
+ /**
5
+ * SDK version constant — generated by `scripts/sync-sdk-versions.mjs`.
6
+ *
7
+ * Single source of truth: the `version` field in this package's
8
+ * package.json. The sync script writes this file so that
9
+ * `SDK_VERSION` is a plain TypeScript literal at runtime — no
10
+ * runtime JSON-import gotcha (Node ESM requires
11
+ * `with { type: "json" }` to import JSON as ESM, and the published
12
+ * dist file would otherwise fail to load).
13
+ *
14
+ * Drift protection: `node scripts/sync-sdk-versions.mjs --check` (the
15
+ * CI gate) flags this file when it falls out of sync with package.json.
16
+ * Bumping `package.json` without re-running the sync script fails CI.
17
+ *
18
+ * Do NOT edit by hand — `node scripts/sync-sdk-versions.mjs`.
19
+ */
20
+ declare const SDK_VERSION = "1.4.2";
21
+ declare const SDK_NAME = "@cross-deck/node";
22
+
4
23
  /**
5
24
  * Machine-readable index of every error code `@cross-deck/node` can
6
25
  * throw, with a short description and a hint on what action to take.
@@ -139,16 +158,34 @@ declare const _CROSSDECK_ERROR_CODES: readonly [{
139
158
  readonly resolution: "Increase flushOnExitTimeoutMs in CrossdeckServer options. Default is 2000ms; serverless runtimes typically allow 5-10s before SIGKILL. If events are dropping silently in production, raise this.";
140
159
  readonly retryable: false;
141
160
  }, {
142
- readonly code: "webhook_invalid_signature";
161
+ readonly code: "webhook_signature_mismatch";
143
162
  readonly type: "authentication_error";
144
- readonly description: "The webhook signature header did not verify against the supplied secret.";
145
- readonly resolution: "Confirm the secret matches the one in your Crossdeck dashboard → Webhooks page. If the request is genuinely from Crossdeck, the secret is wrong, stale, or recently rotated.";
163
+ readonly description: "Webhook HMAC didn't verify against any configured secret (wrong-secret / stale rotation signal).";
164
+ readonly resolution: "Confirm the secret matches dashboard → Webhooks. If you rotated, include both the old and new secret as an array until receivers cut over.";
146
165
  readonly retryable: false;
147
166
  }, {
148
- readonly code: "webhook_replay_window_exceeded";
167
+ readonly code: "webhook_timestamp_outside_tolerance";
149
168
  readonly type: "authentication_error";
150
- readonly description: "The webhook timestamp is older than the replay-tolerance window (default 5 minutes).";
151
- readonly resolution: "The webhook is either replayed or your receiving clock is wildly skewed. Verify NTP on the receiving host. Increase replayToleranceMs only if you accept the replay-attack risk.";
169
+ readonly description: "Webhook timestamp drift exceeds the configured replay-tolerance window (default 5 minutes; replay-attack signal).";
170
+ readonly resolution: "Verify NTP on the receiving host. A spike on this code warrants its own alert separate from signature_mismatch replay attacks look like this.";
171
+ readonly retryable: false;
172
+ }, {
173
+ readonly code: "webhook_timestamp_missing";
174
+ readonly type: "authentication_error";
175
+ readonly description: "Webhook signature header is absent or has no `t=` timestamp segment — the timestamp gate cannot be verified.";
176
+ readonly resolution: "Confirm the request actually came from Crossdeck (signature headers are always present on real deliveries). A missing header is either a misconfigured intermediary or a forged request.";
177
+ readonly retryable: false;
178
+ }, {
179
+ readonly code: "webhook_payload_not_json";
180
+ readonly type: "authentication_error";
181
+ readonly description: "Webhook signature verified but the body isn't valid JSON — payload tampered post-signing or source bug.";
182
+ readonly resolution: "Inspect the raw payload. If it's not JSON, either the request was modified in transit or the sender has a bug — file a support ticket with the raw body.";
183
+ readonly retryable: false;
184
+ }, {
185
+ readonly code: "webhook_invalid_tolerance";
186
+ readonly type: "configuration_error";
187
+ readonly description: "verifyWebhookSignature() called with a non-finite / negative / above-24h-cap replayToleranceMs (would silently disable replay protection).";
188
+ readonly resolution: "Pass a finite number between 0 and 86_400_000ms (24h). Default (5 minutes) is correct for almost every scenario. Pre-v1.4.0 accepted Infinity/NaN and silently dropped the check.";
152
189
  readonly retryable: false;
153
190
  }, {
154
191
  readonly code: "webhook_missing_secret";
@@ -156,6 +193,84 @@ declare const _CROSSDECK_ERROR_CODES: readonly [{
156
193
  readonly description: "verifyWebhookSignature() was called without a signing secret.";
157
194
  readonly resolution: "Pass the secret from your Crossdeck dashboard → Webhooks page. Never hardcode in source — read from an env var.";
158
195
  readonly retryable: false;
196
+ }, {
197
+ readonly code: "webhook_invalid_signature";
198
+ readonly type: "authentication_error";
199
+ readonly description: "DEPRECATED in v1.4.0 — split into webhook_signature_mismatch / webhook_timestamp_missing / webhook_timestamp_outside_tolerance / webhook_payload_not_json for alerting clarity.";
200
+ readonly resolution: "Migrate alert rules to the more specific v1.4.0 codes — they distinguish replay-attack signals from wrong-secret signals.";
201
+ readonly retryable: false;
202
+ }, {
203
+ readonly code: "webhook_replay_window_exceeded";
204
+ readonly type: "authentication_error";
205
+ readonly description: "DEPRECATED in v1.4.0 — renamed to webhook_timestamp_outside_tolerance.";
206
+ readonly resolution: "Update alerts to webhook_timestamp_outside_tolerance.";
207
+ readonly retryable: false;
208
+ }, {
209
+ readonly code: "missing_api_key";
210
+ readonly type: "authentication_error";
211
+ readonly description: "No Authorization header (or Crossdeck-Api-Key header) on the request.";
212
+ readonly resolution: "Confirm the CrossdeckServer was constructed with a cd_sk_… secretKey. Re-check env vars in production deployments.";
213
+ readonly retryable: false;
214
+ }, {
215
+ readonly code: "invalid_api_key";
216
+ readonly type: "authentication_error";
217
+ readonly description: "The secret key is malformed, unknown, or doesn't resolve to a project.";
218
+ readonly resolution: "Copy the key from Crossdeck dashboard → API keys. Server SDK requires cd_sk_test_ / cd_sk_live_ — client SDK keys (cd_pub_…) won't work on the Node SDK.";
219
+ readonly retryable: false;
220
+ }, {
221
+ readonly code: "key_revoked";
222
+ readonly type: "authentication_error";
223
+ readonly description: "The secret key was revoked in the dashboard.";
224
+ readonly resolution: "Mint a fresh key in dashboard → API keys → Create new. The revoked key cannot be reactivated.";
225
+ readonly retryable: false;
226
+ }, {
227
+ readonly code: "env_mismatch";
228
+ readonly type: "permission_error";
229
+ readonly description: "The key's env prefix doesn't match the resolved app's configured env.";
230
+ readonly resolution: "Use a cd_sk_live_ key with a production app, cd_sk_test_ with a sandbox app. Crossing breaks the env lock.";
231
+ readonly retryable: false;
232
+ }, {
233
+ readonly code: "idempotency_key_in_use";
234
+ readonly type: "invalid_request_error";
235
+ readonly description: "An Idempotency-Key was reused for a request with a different body (Stripe-grade contract).";
236
+ readonly resolution: "Server SDK derives keys deterministically from the body since v1.4.0; this should only fire if you passed options.idempotencyKey explicitly. Use a fresh key per logical operation.";
237
+ readonly retryable: false;
238
+ }, {
239
+ readonly code: "rate_limited";
240
+ readonly type: "rate_limit_error";
241
+ readonly description: "Request rate exceeded the project's per-second cap.";
242
+ readonly resolution: "Honour Retry-After (managed retries do this automatically). For custom paths, throttle to <100 req/s/key.";
243
+ readonly retryable: true;
244
+ }, {
245
+ readonly code: "internal_error";
246
+ readonly type: "internal_error";
247
+ readonly description: "Server-side issue. Safe to retry with backoff.";
248
+ readonly resolution: "Managed retries handle this automatically. If a code path surfaces it to your code, contact support with the requestId.";
249
+ readonly retryable: true;
250
+ }, {
251
+ readonly code: "google_not_supported";
252
+ readonly type: "invalid_request_error";
253
+ readonly description: "POST /purchases/sync with rail=google is gated until the Play Developer API reconciliation worker ships.";
254
+ readonly resolution: "Until v1.5+, Google Play purchases verify via Real-time Developer Notifications. The Android SDK auto-track path handles this transparently.";
255
+ readonly retryable: false;
256
+ }, {
257
+ readonly code: "stripe_not_supported";
258
+ readonly type: "invalid_request_error";
259
+ readonly description: "POST /purchases/sync with rail=stripe is unsupported — Stripe webhooks deliver evidence server-side.";
260
+ readonly resolution: "Wire Stripe via the standard Checkout / Customer Portal flow; Crossdeck reconciles via the platform webhook automatically.";
261
+ readonly retryable: false;
262
+ }, {
263
+ readonly code: "missing_required_param";
264
+ readonly type: "invalid_request_error";
265
+ readonly description: "A required field is absent from the request body.";
266
+ readonly resolution: "The error.message identifies the missing field. Refer to the SDK's TypeScript types for canonical shapes.";
267
+ readonly retryable: false;
268
+ }, {
269
+ readonly code: "invalid_param_value";
270
+ readonly type: "invalid_request_error";
271
+ readonly description: "A field is present but the value failed validation.";
272
+ readonly resolution: "Read error.message for the field + reason. SDK-managed call sites should never emit this — file a bug if you do.";
273
+ readonly retryable: false;
159
274
  }];
160
275
  /**
161
276
  * Literal union of every code documented in `CROSSDECK_ERROR_CODES`.
@@ -182,9 +297,22 @@ declare function getErrorCode(code: string): ErrorCodeEntry | undefined;
182
297
  /**
183
298
  * Webhook signature verification — Stripe pattern.
184
299
  *
185
- * Lets customers verify the events Crossdeck sends to THEM. Table-stakes
186
- * for any backend SDK (Stripe ships `Stripe.webhooks.constructEvent()`
187
- * from day one, Svix ships `Webhook.verify()` from day one).
300
+ * **[ROADMAP v1.4.0 honesty note]:** Crossdeck does NOT yet send
301
+ * outbound webhooks. Outbound delivery (signer + worker + scheduler
302
+ * + dead-letter dashboard) is on the post-v1.5 roadmap. This
303
+ * verifier exists today so customer-side integration code can be
304
+ * written and tested against fixtures (use `signWebhookPayload`
305
+ * to produce signed bodies for your local tests), and so the
306
+ * verification contract surface is locked in BEFORE delivery
307
+ * ships — Phase 7.2 of the bank-grade reconciliation tightened
308
+ * the timestamp-validation footguns here precisely because the
309
+ * helper IS the contract surface for inbound validation,
310
+ * regardless of when first-party delivery lights up.
311
+ *
312
+ * Lets customers verify the events Crossdeck sends to THEM (when
313
+ * delivery ships). Table-stakes for any backend SDK (Stripe ships
314
+ * `Stripe.webhooks.constructEvent()` from day one, Svix ships
315
+ * `Webhook.verify()` from day one).
188
316
  *
189
317
  * Wire format:
190
318
  * Header `Crossdeck-Signature: t=<unix-seconds>,v1=<hex>`
@@ -225,9 +353,21 @@ declare function getErrorCode(code: string): ErrorCodeEntry | undefined;
225
353
  interface VerifyWebhookOptions {
226
354
  /**
227
355
  * Maximum age of the webhook timestamp in milliseconds. Default
228
- * 5 minutes (5 * 60 * 1000). Anything older than this is rejected
229
- * as a replay. Pass 0 to disable the replay window (NOT recommended
230
- * — accept the trade-off only if you have a separate replay defence).
356
+ * 5 minutes (`DEFAULT_REPLAY_TOLERANCE_MS`). Anything older than
357
+ * this is rejected as a replay.
358
+ *
359
+ * **v1.4.0 Phase 7.2 bank-grade contract:** the timestamp window
360
+ * is MANDATORY. Pre-v1.4.0 the helper accepted `tolerance: 0`
361
+ * (silently disables the check) and `tolerance: Infinity` /
362
+ * `null` / `NaN` (silently disables via `Math.abs(...) > Infinity
363
+ * = false`). Customers relying on replay protection silently
364
+ * lost it.
365
+ *
366
+ * The helper now rejects non-finite / negative / above-cap
367
+ * tolerances at the boundary with a typed
368
+ * `webhook_invalid_tolerance` error. Hard upper bound is 24h —
369
+ * sufficient for any plausible clock-skew scenario, prevents
370
+ * "Infinity by typo" from defeating replay protection.
231
371
  */
232
372
  replayToleranceMs?: number;
233
373
  /**
@@ -238,13 +378,21 @@ interface VerifyWebhookOptions {
238
378
  }
239
379
  /**
240
380
  * Verify a Crossdeck-signed webhook. Returns the parsed JSON payload
241
- * on success. Throws `CrossdeckError` on:
242
- * - missing / malformed signature header (`webhook_invalid_signature`)
243
- * - missing secret (`webhook_missing_secret`)
244
- * - timestamp outside replay window (`webhook_replay_window_exceeded`)
245
- * - HMAC mismatch (`webhook_invalid_signature`)
246
- * - non-JSON payload (`webhook_invalid_signature` — same code because
247
- * a tampered payload that breaks JSON parses as invalid)
381
+ * on success. Throws `CrossdeckError` with one of these
382
+ * distinguishable codes (v1.4.0 Phase 7.2 — pre-v1.4.0 conflated
383
+ * everything under `webhook_invalid_signature`; alerting can now
384
+ * separate replay-attack signals from wrong-secret signals):
385
+ * - `webhook_missing_secret` no secret configured.
386
+ * - `webhook_invalid_tolerance` — caller passed Infinity / NaN /
387
+ * negative / above-24h-cap `replayToleranceMs`.
388
+ * - `webhook_timestamp_missing` — header absent or has no `t=`.
389
+ * - `webhook_timestamp_outside_tolerance` — drift > tolerance
390
+ * (replay-attack signal — split this from signature mismatch
391
+ * in your alerting rules).
392
+ * - `webhook_signature_mismatch` — HMAC didn't match any
393
+ * configured secret (wrong-secret / rotation-drift signal).
394
+ * - `webhook_payload_not_json` — signature verified but the body
395
+ * isn't parseable JSON (tampered post-signing or source bug).
248
396
  *
249
397
  * `secret` accepts a single string or an array of strings (for
250
398
  * rotation). Any one match is sufficient.
@@ -289,16 +437,24 @@ declare function signWebhookPayload(payload: string, secret: string, timestampSe
289
437
  * name: "checkout.started",
290
438
  * developerUserId: userId,
291
439
  * properties: scrubPiiFromProperties({
292
- * url: req.url, // might contain "/users/wes@…/" — gets [email]
440
+ * url: req.url, // might contain "/users/wes@…/" — gets <email>
293
441
  * lastError: e.message, // might contain card numbers
294
442
  * }),
295
443
  * });
296
444
  */
297
445
  /**
298
446
  * Scrub a single string value: replace email-shaped substrings with
299
- * `[email]` and card-number-shaped substrings with `[card]`. Returns
300
- * the original string (===) when nothing matched, so callers can do
301
- * an identity-check to skip allocating a new event copy.
447
+ * `<email>` and card-number-shaped substrings with `<card>`. Returns
448
+ * the original string (===) when nothing matched.
449
+ *
450
+ * Implementation note: we call `.replace()` unconditionally rather than
451
+ * gating on `.test()`. The /g regexes are module-level so `.test()`
452
+ * carries `lastIndex` state between calls — a prior match leaves
453
+ * `lastIndex` mid-string and the next `.test()` can falsely return
454
+ * false on a string that DOES match. `.replace(/g)` always scans the
455
+ * full string regardless of `lastIndex`, so dropping the test-guard
456
+ * removes the sharp edge at zero cost (when nothing matches, replace
457
+ * returns the same `(===)` string).
302
458
  */
303
459
  declare function scrubPii(value: string): string;
304
460
  /**
@@ -339,7 +495,7 @@ declare function scrubPiiFromProperties(properties: Record<string, unknown>): Re
339
495
  * - `sdk.no_durable_store`
340
496
  * - `sdk.super_property_registered`
341
497
  */
342
- type DebugSignal = "sdk.configured" | "sdk.first_event_sent" | "sdk.invalid_key" | "sdk.no_identity" | "sdk.entitlement_cache_used" | "sdk.entitlement_cache_warm" | "sdk.entitlement_cache_stale" | "sdk.entitlement_store_recovered" | "sdk.no_durable_store" | "sdk.purchase_evidence_sent" | "sdk.environment_mismatch" | "sdk.sensitive_property_warning" | "sdk.property_coerced" | "sdk.flush_retry_scheduled" | "sdk.flush_on_exit_started" | "sdk.flush_on_exit_completed" | "sdk.webhook_verified" | "sdk.runtime_detected" | "sdk.super_property_registered" | "sdk.boot_heartbeat_failed";
498
+ type DebugSignal = "sdk.configured" | "sdk.first_event_sent" | "sdk.invalid_key" | "sdk.no_identity" | "sdk.entitlement_cache_used" | "sdk.entitlement_cache_warm" | "sdk.entitlement_cache_stale" | "sdk.entitlement_store_recovered" | "sdk.no_durable_store" | "sdk.purchase_evidence_sent" | "sdk.environment_mismatch" | "sdk.sensitive_property_warning" | "sdk.property_coerced" | "sdk.flush_retry_scheduled" | "sdk.flush_permanent_failure" | "sdk.flush_on_exit_started" | "sdk.flush_on_exit_completed" | "sdk.webhook_verified" | "sdk.runtime_detected" | "sdk.super_property_registered" | "sdk.boot_heartbeat_failed";
343
499
  interface DebugContext {
344
500
  [key: string]: unknown;
345
501
  }
@@ -348,4 +504,4 @@ interface DebugLogger {
348
504
  emit(signal: DebugSignal, message: string, context?: DebugContext): void;
349
505
  }
350
506
 
351
- export { CROSSDECK_ERROR_CODES, type CrossdeckErrorCode, type DebugContext, type DebugLogger, type DebugSignal, type ErrorCodeEntry, type VerifyWebhookOptions, getErrorCode, isCrossdeckErrorCode, scrubPii, scrubPiiFromProperties, signWebhookPayload, verifyWebhookSignature };
507
+ 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 };