@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/CHANGELOG.md +150 -0
- package/README.md +62 -0
- package/dist/auto-events/index.d.mts +1 -1
- package/dist/auto-events/index.d.ts +1 -1
- package/dist/contracts.json +430 -0
- package/dist/{crossdeck-server-BZVZEuS-.d.mts → crossdeck-server-oAaKBnUU.d.mts} +219 -32
- package/dist/{crossdeck-server-BZVZEuS-.d.ts → crossdeck-server-oAaKBnUU.d.ts} +219 -32
- package/dist/index.cjs +957 -122
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +182 -26
- package/dist/index.d.ts +182 -26
- package/dist/index.mjs +952 -118
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
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
|
|
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: "
|
|
161
|
+
readonly code: "webhook_signature_mismatch";
|
|
143
162
|
readonly type: "authentication_error";
|
|
144
|
-
readonly description: "
|
|
145
|
-
readonly resolution: "Confirm the secret matches
|
|
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: "
|
|
167
|
+
readonly code: "webhook_timestamp_outside_tolerance";
|
|
149
168
|
readonly type: "authentication_error";
|
|
150
|
-
readonly description: "
|
|
151
|
-
readonly resolution: "
|
|
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
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
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 (
|
|
229
|
-
* as a replay.
|
|
230
|
-
*
|
|
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`
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
* -
|
|
246
|
-
* -
|
|
247
|
-
*
|
|
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
|
|
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
|
-
*
|
|
300
|
-
* the original string (===) when nothing matched
|
|
301
|
-
*
|
|
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
|
|
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: "
|
|
161
|
+
readonly code: "webhook_signature_mismatch";
|
|
143
162
|
readonly type: "authentication_error";
|
|
144
|
-
readonly description: "
|
|
145
|
-
readonly resolution: "Confirm the secret matches
|
|
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: "
|
|
167
|
+
readonly code: "webhook_timestamp_outside_tolerance";
|
|
149
168
|
readonly type: "authentication_error";
|
|
150
|
-
readonly description: "
|
|
151
|
-
readonly resolution: "
|
|
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
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
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 (
|
|
229
|
-
* as a replay.
|
|
230
|
-
*
|
|
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`
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
* -
|
|
246
|
-
* -
|
|
247
|
-
*
|
|
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
|
|
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
|
-
*
|
|
300
|
-
* the original string (===) when nothing matched
|
|
301
|
-
*
|
|
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 };
|