@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.
@@ -111,6 +111,96 @@ declare class CrossdeckConfigurationError extends CrossdeckError {
111
111
  */
112
112
  declare function makeCrossdeckError(payload: CrossdeckErrorPayload): CrossdeckError;
113
113
 
114
+ /**
115
+ * Public, typed accessor for the bank-grade behavioural contracts
116
+ * this SDK ships. The full architecture — schema, distribution,
117
+ * audit loop, pillar taxonomy — lives in `contracts/README.md`
118
+ * at the monorepo root.
119
+ *
120
+ * Why a typed surface (vs. plain JSON access): contract IDs and
121
+ * pillar names are part of Crossdeck's public commitment to
122
+ * customers. Reading them through `CrossdeckContracts` means the
123
+ * compiler catches drift the moment a contract is renamed or
124
+ * retired. Tools that consume contracts at runtime (dashboards,
125
+ * AI assistants, customer integration tests) get the exact same
126
+ * shape every SDK ships, with no parsing layer to drift.
127
+ *
128
+ * --- BINARY STABILITY ---
129
+ * `Contract` is treated as an evolving — but back-compat — wire
130
+ * shape. Fields may be added in any minor release. Existing
131
+ * fields will not be removed or repurposed except in a major
132
+ * version bump, even if all known contracts stop using them.
133
+ * Customers can rely on `id`, `pillar`, `status`, `appliesTo`,
134
+ * `codeRef`, `testRef`, `registeredAt`, `firstRegisteredIn`,
135
+ * and `bundledIn` being present on every contract in every
136
+ * future minor/patch release of this SDK.
137
+ */
138
+ type ContractPillar = "revenue" | "entitlements" | "analytics" | "webhooks" | "errors" | "lifecycle" | "identity";
139
+ type ContractStatus = "enforced" | "proposed" | "retired";
140
+ type ContractAppliesTo = "web" | "node" | "react-native" | "swift" | "android" | "backend";
141
+ interface ContractTestRef {
142
+ readonly file: string;
143
+ readonly name: string;
144
+ }
145
+ interface Contract {
146
+ readonly id: string;
147
+ readonly pillar: ContractPillar;
148
+ readonly status: ContractStatus;
149
+ readonly claim: string;
150
+ readonly appliesTo: readonly ContractAppliesTo[];
151
+ readonly codeRef: readonly string[];
152
+ readonly testRef: readonly ContractTestRef[];
153
+ readonly registeredAt: string;
154
+ readonly firstRegisteredIn: string;
155
+ readonly bundledIn: string;
156
+ }
157
+ /**
158
+ * Typed entry point to the bank-grade contracts bundled with this
159
+ * SDK release. Stable, side-effect-free, tree-shakeable.
160
+ *
161
+ * @example Audit at app boot
162
+ * ```ts
163
+ * import { CrossdeckContracts } from "@cross-deck/node";
164
+ *
165
+ * for (const c of CrossdeckContracts.all()) {
166
+ * console.log(`[crossdeck] ${c.id} (${c.pillar})`);
167
+ * }
168
+ * ```
169
+ */
170
+ declare const CrossdeckContracts: {
171
+ readonly all: () => readonly Contract[];
172
+ readonly allIncludingHistorical: () => readonly Contract[];
173
+ readonly byId: (id: string) => Contract | undefined;
174
+ readonly byPillar: (pillar: ContractPillar) => readonly Contract[];
175
+ readonly withStatus: (status: ContractStatus) => readonly Contract[];
176
+ readonly sdkVersion: "1.5.0";
177
+ readonly bundledIn: "@cross-deck/node@1.5.0";
178
+ /**
179
+ * Resolve a failing test back to the contract it exercises.
180
+ * Used by test-framework hooks to find the contract id of a
181
+ * failed contract test so `reportContractFailure(...)` can stamp
182
+ * the right `contract_id` on the emitted event.
183
+ */
184
+ readonly findByTestName: (name: string) => Contract | undefined;
185
+ };
186
+ /**
187
+ * Input to {@link CrossdeckServer.reportContractFailure}. Mirrors
188
+ * the per-SDK shape exactly — the Crossdeck dashboard joins
189
+ * `crossdeck.contract_failed` events across every SDK on
190
+ * `contract_id`, so the property bag has to agree.
191
+ */
192
+ interface ContractFailureInput {
193
+ contractId: string;
194
+ failureReason: string;
195
+ runContext: "ci" | "dogfood" | "customer-app";
196
+ runId: string;
197
+ testRef?: {
198
+ file: string;
199
+ name: string;
200
+ };
201
+ extra?: Record<string, unknown>;
202
+ }
203
+
114
204
  /**
115
205
  * Breadcrumb ring buffer — context attached to every error report.
116
206
  *
@@ -251,8 +341,6 @@ interface RuntimeInfo {
251
341
  appVersion: string | null;
252
342
  }
253
343
 
254
- declare const SDK_NAME = "@cross-deck/node";
255
- declare const SDK_VERSION = "1.2.0";
256
344
  declare const DEFAULT_BASE_URL = "https://api.cross-deck.com/v1";
257
345
  declare const DEFAULT_TIMEOUT_MS = 15000;
258
346
  /**
@@ -411,6 +499,10 @@ interface PurchaseResult {
411
499
  crossdeckCustomerId: string;
412
500
  env: Environment;
413
501
  entitlements: PublicEntitlement[];
502
+ /** True when the response came from the backend's idempotency
503
+ * cache instead of fresh processing. Backend also returns
504
+ * `Idempotent-Replayed: true` as a response header (v1.4.0). */
505
+ idempotent_replay?: boolean;
414
506
  }
415
507
  /**
416
508
  * Response shape from `GET /v1/sdk/heartbeat`. Used by
@@ -464,6 +556,25 @@ interface CrossdeckServerOptions {
464
556
  * not the source of truth.
465
557
  */
466
558
  appId?: string;
559
+ /**
560
+ * Apply Crossdeck's PII scrubber to every `track()` payload before
561
+ * enqueue. Default `true` (parity with Web / RN / Swift SDKs — Node
562
+ * pre-v1.4.0 was the odd one out and SHIPPED EMAILS UNREDACTED, a
563
+ * privacy contract drift versus the README claim).
564
+ *
565
+ * The scrubber rewrites email-shaped and card-number-shaped
566
+ * substrings to `<email>` / `<card>` sentinels recursively across
567
+ * nested maps + arrays. See `scrubPii` / `scrubPiiFromProperties`.
568
+ *
569
+ * **Blast radius of setting `false`:** every `track()` payload —
570
+ * including event names with embedded emails ("user wes@example.com
571
+ * upgraded"), trait values, group memberships, error context blobs
572
+ * — ships verbatim to Crossdeck and downstream warehouses /
573
+ * analytics exports. Disable only for explicit compliance use
574
+ * cases (regulator-required audit trails where the raw value MUST
575
+ * be preserved) and document the decision at the call site.
576
+ */
577
+ scrubPii?: boolean;
467
578
  /**
468
579
  * Error capture configuration. Default: ON with `onUncaughtException` +
469
580
  * `onUnhandledRejection` + `wrapFetch` all enabled.
@@ -674,6 +785,14 @@ interface RequestOptions {
674
785
  * `timeoutMs`. Pass `0` to disable.
675
786
  */
676
787
  timeoutMs?: number;
788
+ /**
789
+ * Override the deterministic Idempotency-Key derivation (v1.4.0).
790
+ * The SDK derives a stable key from the request body so retries
791
+ * collapse on the backend. Override only when an outer
792
+ * orchestrator (job runner, retry harness) needs a different
793
+ * idempotency window — and document why at the call site.
794
+ */
795
+ idempotencyKey?: string;
677
796
  }
678
797
  interface IdentityHints {
679
798
  customerId?: string;
@@ -1164,6 +1283,10 @@ declare class CrossdeckServer extends EventEmitter {
1164
1283
  private readonly baseUrl;
1165
1284
  private readonly appId;
1166
1285
  private readonly env;
1286
+ /** PII scrubber toggle. Default true — parity with Web/RN/Swift.
1287
+ * Pre-v1.4.0 the Node SDK shipped track() payloads UNREDACTED,
1288
+ * a privacy contract drift versus the README. */
1289
+ private readonly scrubPii;
1167
1290
  private readonly secretKeyPrefix;
1168
1291
  /**
1169
1292
  * Process-stable pseudo-anonymous ID. Used as the default identity
@@ -1209,22 +1332,22 @@ declare class CrossdeckServer extends EventEmitter {
1209
1332
  private errorContext;
1210
1333
  private errorTags;
1211
1334
  private errorBeforeSend;
1335
+ /**
1336
+ * Dedup gate for `sdk.shutdown`. Both `shutdown()` (async) and
1337
+ * `shutdownSync()` need to emit so direct callers of EITHER see
1338
+ * the event (the async path's listener guarantees pre-launch
1339
+ * tests, the sync path covers `Symbol.dispose` + tests that call
1340
+ * `shutdownSync()` directly). Without this flag, `shutdown()`'s
1341
+ * tail call into `shutdownSync()` would emit twice.
1342
+ */
1343
+ private didEmitShutdown;
1212
1344
  constructor(options: CrossdeckServerOptions);
1213
1345
  /**
1214
- * Emit the one-time `sdk.boot` telemetry event and, when the runtime
1215
- * is serverless with no `entitlementStore`, the honest "no cold-start
1216
- * durability" warning.
1217
- *
1218
- * Why a `track()` event and not the heartbeat: `GET /v1/sdk/heartbeat`
1219
- * carries no request body, so it cannot transport a structured
1220
- * `durability` fact. The event pipeline can — every `track()` event
1221
- * lands as an aggregatable document the backend can query, so
1222
- * Crossdeck can compute fleet-wide "% serverless-with-no-durable-
1223
- * store" from `sdk.boot` events (denominator = all `sdk.boot`,
1224
- * numerator = those with `durability.coldStartDurable === false`).
1225
- * The event rides the existing batched + retried + idempotent queue
1226
- * and is drained by flush-on-exit, so it survives a serverless
1227
- * teardown — it is NOT a local-only debug log.
1346
+ * Emit the honest "no cold-start durability" warning when the runtime
1347
+ * is serverless AND no `entitlementStore` is wired. Local-only debug
1348
+ * signal — no network call, no phone-home. Safe to fire from the
1349
+ * constructor before `setImmediate` because there is no I/O on this
1350
+ * path.
1228
1351
  *
1229
1352
  * `isServerless` AND no store is the gap: a cold start begins with an
1230
1353
  * empty in-memory cache and a brief Crossdeck outage in that window
@@ -1232,11 +1355,29 @@ declare class CrossdeckServer extends EventEmitter {
1232
1355
  * unavoidable without a store — so the SDK STATES it (a
1233
1356
  * `sdk.no_durable_store` debug warning) rather than hiding it.
1234
1357
  *
1235
- * Called once, from the deferred boot block so it inherits the
1236
- * `testMode` / `bootHeartbeat:false` opt-outs and never fires before
1237
- * the constructor returns.
1358
+ * Audit P1 #9: this used to live INSIDE `emitBootTelemetry()` which
1359
+ * itself sat inside the `bootHeartbeat` gate, so any developer who
1360
+ * set `bootHeartbeat: false` silently disabled the entire reason
1361
+ * `entitlementStore` exists. Now split: warning fires
1362
+ * unconditionally; the boot phone-home stays gated.
1238
1363
  */
1239
- private emitBootTelemetry;
1364
+ private emitDurabilityWarning;
1365
+ /**
1366
+ * Emit the one-time `sdk.boot` telemetry event — the aggregatable
1367
+ * fact the backend pivots on (compute fleet-wide
1368
+ * "% serverless-with-no-durable-store"). Rides the batched + retried
1369
+ * + idempotent queue and is drained by flush-on-exit, so it survives
1370
+ * a serverless teardown.
1371
+ *
1372
+ * Why a `track()` event and not the heartbeat: `GET /v1/sdk/heartbeat`
1373
+ * carries no request body, so it cannot transport a structured
1374
+ * `durability` fact.
1375
+ *
1376
+ * Gated by `bootHeartbeat` (and `testMode`) because it IS a phone-
1377
+ * home — the unconditional surface is `emitDurabilityWarning()`,
1378
+ * which has no network call.
1379
+ */
1380
+ private emitBootTelemetryEvent;
1240
1381
  identify(userId: string, anonymousId: string, options?: IdentifyOptions & RequestOptions): Promise<AliasResult>;
1241
1382
  aliasIdentity(input: AliasIdentityInput, options?: RequestOptions): Promise<AliasResult>;
1242
1383
  forget(hints: IdentityHints, options?: RequestOptions): Promise<ForgetResult>;
@@ -1324,6 +1465,14 @@ declare class CrossdeckServer extends EventEmitter {
1324
1465
  * `uncaughtException` has no per-request context; without the
1325
1466
  * auto-fill, the event would be rejected at queue enqueue.
1326
1467
  */
1468
+ /**
1469
+ * Emit `crossdeck.contract_failed` with the canonical property
1470
+ * shape. Same wire shape every Crossdeck SDK uses for contract
1471
+ * verification telemetry — see `contracts/README.md` for the
1472
+ * full pattern. No new endpoint, no special path; goes through
1473
+ * the standard server-side `track()` pipeline.
1474
+ */
1475
+ reportContractFailure(input: ContractFailureInput): void;
1327
1476
  track(event: ServerEvent): void;
1328
1477
  /**
1329
1478
  * Immediate POST of one or more events. For bulk imports / replay
@@ -1534,11 +1683,36 @@ declare class CrossdeckServer extends EventEmitter {
1534
1683
  getGroups(): Record<string, GroupMembership>;
1535
1684
  diagnostics(): Diagnostics;
1536
1685
  /**
1537
- * Tear down handlers and clear in-memory state. Tests + custom
1538
- * lifecycle callers only. Production code should rely on
1539
- * `flush-on-exit` instead.
1686
+ * Tear down handlers and clear in-memory state.
1687
+ *
1688
+ * **v1.4.0 bank-grade contract:** `shutdown()` AWAITS `flush()`
1689
+ * before dropping the queue, so callers don't silently lose
1690
+ * every queued event on a clean shutdown. The pre-v1.4.0
1691
+ * behaviour (sync `eventQueue.reset()` with no flush) was the
1692
+ * default for both `shutdown()` and `[Symbol.dispose]`; only
1693
+ * `await using` + `[Symbol.asyncDispose]` flushed correctly.
1694
+ *
1695
+ * Production servers should still prefer `await server.flush()`
1696
+ * (visible) followed by `server.shutdown()` so the flush
1697
+ * outcome is observable — `shutdown()`'s internal flush swallows
1698
+ * errors as a best-effort drain.
1699
+ *
1700
+ * Use [[shutdownSync]] only when the runtime cannot await
1701
+ * (e.g. inside `Symbol.dispose` — see below).
1702
+ */
1703
+ shutdown(reason?: "shutdown" | "dispose" | "asyncDispose"): Promise<void>;
1704
+ /**
1705
+ * Synchronous teardown — drops the in-memory queue WITHOUT
1706
+ * flushing, then clears all in-memory state. Used by
1707
+ * `[Symbol.dispose]` (which has no await) and tests that need
1708
+ * an unconditional sync wipe. Production code should use
1709
+ * [[shutdown]] (async) instead so queued events are flushed.
1710
+ *
1711
+ * A queue with items at sync-shutdown logs a warning recommending
1712
+ * `[Symbol.asyncDispose]` or `await server.shutdown()` — silent
1713
+ * loss is incompatible with the bank-grade contract.
1540
1714
  */
1541
- shutdown(reason?: "shutdown" | "dispose" | "asyncDispose"): void;
1715
+ shutdownSync(reason?: "shutdown" | "dispose" | "asyncDispose"): void;
1542
1716
  /**
1543
1717
  * Convert a `CapturedError` into a `ServerEvent` and push through
1544
1718
  * `track()`. Goes through the same queue / enrichment / breadcrumb
@@ -1607,17 +1781,21 @@ declare class CrossdeckServer extends EventEmitter {
1607
1781
  * // ... use server ...
1608
1782
  * // at end of block, server[Symbol.dispose]() runs automatically
1609
1783
  *
1610
- * `Symbol.dispose` is synchronous so we can't await `flush()` here
1611
- * for that, use `await using` + `[Symbol.asyncDispose]()`. This
1612
- * sync variant just calls `shutdown()` (handler cleanup +
1613
- * in-memory state wipe).
1784
+ * **`Symbol.dispose` is synchronous so it CANNOT await the queue
1785
+ * flush.** A queue with pending events at sync-dispose time will
1786
+ * be DROPPED `shutdownSync` warns to the console when this
1787
+ * happens. For the common case of "drain the queue before
1788
+ * exit", switch to `await using` + `[Symbol.asyncDispose]` (or
1789
+ * call `await server.shutdown()` explicitly before the variable
1790
+ * goes out of scope).
1614
1791
  */
1615
1792
  [Symbol.dispose](): void;
1616
1793
  /**
1617
1794
  * Async disposal hook — runs when an `await using` declaration
1618
- * exits scope. Awaits `flush()` THEN runs `shutdown()`. Use this
1619
- * variant when the caller needs the queue drained before exit
1620
- * (the common case for serverless handlers).
1795
+ * exits scope. Awaits the bank-grade `shutdown()` which flushes
1796
+ * the queue THEN tears down. Use this variant for any code path
1797
+ * that owns queued events at exit (serverless handlers,
1798
+ * background workers, end-of-request hooks).
1621
1799
  *
1622
1800
  * await using server = new CrossdeckServer({ ... });
1623
1801
  */
@@ -1677,6 +1855,15 @@ declare class CrossdeckServer extends EventEmitter {
1677
1855
  * Resolve any hint shape (canonical customerId / userId hint /
1678
1856
  * anonymousId hint / raw string) to a `crossdeckCustomerId` if we
1679
1857
  * have a cache entry for it.
1858
+ *
1859
+ * String overload is STRICT on the canonical-id shape. Pre-fix
1860
+ * `isFresh(raw)` treated any string with a cache entry as a valid
1861
+ * canonical id — if tenant A's userId happened to collide with
1862
+ * tenant B's crossdeckCustomerId, A's call would resolve to B's
1863
+ * cached entitlements. Bounded by the `cdcust_` prefix convention
1864
+ * (which both SDKs and the backend mint, see
1865
+ * backend/src/lib/customers.ts) — anything else is treated purely
1866
+ * as an alias lookup, never as a canonical id. Audit P1 #19.
1680
1867
  */
1681
1868
  private resolveCacheCustomerId;
1682
1869
  private identityPayload;
@@ -1694,4 +1881,4 @@ declare class CrossdeckServer extends EventEmitter {
1694
1881
  private normalizeIngestEvent;
1695
1882
  }
1696
1883
 
1697
- export { type ServerEvent as $, type AliasIdentityInput as A, type Breadcrumb as B, CROSSDECK_API_VERSION as C, DEFAULT_BASE_URL as D, type EntitlementCacheOptions as E, type ErrorLevel as F, type EventProperties as G, type ForgetResult as H, type GrantDuration as I, type GrantEntitlementInput as J, type GroupMembership as K, type HeartbeatResponse as L, type HttpRequestInfo as M, type HttpResponseInfo as N, type HttpRetriesConfig as O, type IdentifyOptions as P, type IdentityHints as Q, type IngestOptions as R, type IngestResponse as S, type PublicEntitlement as T, type PurchaseResult as U, type RequestOptions as V, type RevokeEntitlementInput as W, type RuntimeHost as X, type RuntimeInfo as Y, SDK_NAME as Z, SDK_VERSION as _, type AliasResult as a, type StackFrame as a0, type StoredEntitlements as a1, type SyncPurchaseInput as a2, makeCrossdeckError as a3, type AuditDecision as b, type AuditEntry as c, type BreadcrumbCategory as d, type BreadcrumbLevel as e, type CapturedError as f, CrossdeckAuthenticationError as g, CrossdeckConfigurationError as h, CrossdeckError as i, type CrossdeckErrorPayload as j, type CrossdeckErrorType as k, CrossdeckInternalError as l, CrossdeckNetworkError as m, CrossdeckPermissionError as n, CrossdeckRateLimitError as o, CrossdeckServer as p, type CrossdeckServerOptions as q, CrossdeckValidationError as r, DEFAULT_TIMEOUT_MS as s, type Diagnostics as t, type EntitlementMutationResult as u, type EntitlementStore as v, type EntitlementsListResponse as w, type EntitlementsListener as x, type Environment as y, type ErrorCaptureConfig as z };
1884
+ export { type PurchaseResult as $, type AliasIdentityInput as A, type Breadcrumb as B, CROSSDECK_API_VERSION as C, DEFAULT_BASE_URL as D, type Diagnostics as E, type EntitlementCacheOptions as F, type EntitlementMutationResult as G, type EntitlementStore as H, type EntitlementsListResponse as I, type EntitlementsListener as J, type Environment as K, type ErrorCaptureConfig as L, type ErrorLevel as M, type EventProperties as N, type ForgetResult as O, type GrantDuration as P, type GrantEntitlementInput as Q, type GroupMembership as R, type HeartbeatResponse as S, type HttpRequestInfo as T, type HttpResponseInfo as U, type HttpRetriesConfig as V, type IdentifyOptions as W, type IdentityHints as X, type IngestOptions as Y, type IngestResponse as Z, type PublicEntitlement as _, type AliasResult as a, type RequestOptions as a0, type RevokeEntitlementInput as a1, type RuntimeHost as a2, type RuntimeInfo as a3, type ServerEvent as a4, type StackFrame as a5, type StoredEntitlements as a6, type SyncPurchaseInput as a7, makeCrossdeckError as a8, type AuditDecision as b, type AuditEntry as c, type BreadcrumbCategory as d, type BreadcrumbLevel as e, type CapturedError as f, type Contract as g, type ContractAppliesTo as h, type ContractFailureInput as i, type ContractPillar as j, type ContractStatus as k, type ContractTestRef as l, CrossdeckAuthenticationError as m, CrossdeckConfigurationError as n, CrossdeckContracts as o, CrossdeckError as p, type CrossdeckErrorPayload as q, type CrossdeckErrorType as r, CrossdeckInternalError as s, CrossdeckNetworkError as t, CrossdeckPermissionError as u, CrossdeckRateLimitError as v, CrossdeckServer as w, type CrossdeckServerOptions as x, CrossdeckValidationError as y, DEFAULT_TIMEOUT_MS as z };