@cross-deck/node 1.5.3 → 1.7.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 CHANGED
@@ -4,6 +4,72 @@ All notable changes to `@cross-deck/node` will be documented here. The
4
4
  format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [1.7.0] — 2026-06-11
8
+
9
+ **PARK on version-rejection — events are held, never dropped.** A third
10
+ event-queue outcome for the day the server stops accepting an outdated event
11
+ format. Purely additive; no public API change.
12
+
13
+ **Added:**
14
+
15
+ - **PARK (HTTP `426` / `sdk_version_unsupported`).** A version-rejection is now
16
+ recognised as its own outcome — distinct from retry (transient) and drop
17
+ (invalid): the data is good, only the wire dialect is stale. The queue
18
+ **holds** the events (folded to the buffer front, FIFO-capped at 1000),
19
+ **hushes** (stops flushing a known-too-old payload), **signals** once (one
20
+ `console.warn` + a typed `sdk.parked` debug event), and delivers on restart
21
+ after you upgrade. Node's queue is in-memory, so a process restart *before*
22
+ upgrade clears the held events — an opt-in disk-backed queue is on the
23
+ roadmap; the messaging says exactly this, never more.
24
+ - **`sdk_version_unsupported`** added to the error-codes catalogue with
25
+ remediation, and `version_error` to `CrossdeckErrorType`. `CrossdeckError`
26
+ carries `minVersion` / `surface` from the 426 body. New `onParked` callback.
27
+
28
+ **Fixed (no public API change):**
29
+
30
+ - The empty-input contract is now codified cross-SDK as
31
+ `invalid-input-rejected-natively`: `track("")` / `aliasIdentity` with a
32
+ missing `userId` reject at the call site by throwing a typed `CrossdeckError`
33
+ (`missing_event_name` / `missing_user_id`) and never reach the wire — the
34
+ Node/JS idiom of the invariant *"invalid input never crashes the app."* No
35
+ behaviour change; the guarantee is now documented and bundled.
36
+ - Standalone-build fix: the `contract-failed` schema-lock test now reads the
37
+ bundled contract (`_contracts-bundled.ts`) instead of the monorepo
38
+ `contracts/` path, so the published-mirror release build no longer fails.
39
+
40
+ See https://cross-deck.com/docs/sdk-event-durability/ for the durability contract.
41
+
42
+ ## [1.6.0] — 2026-06-10
43
+
44
+ Event Envelope v1 conformance — server-enforced contract (spec
45
+ `backend/docs/event-envelope-spec-v1.md`).
46
+
47
+ **Added:**
48
+
49
+ - **`envelopeVersion: 1`** (integer) on every batch POST body. Both the
50
+ queue-flush path (`EventQueue.flush()`) and the direct `ingest()` path
51
+ now emit this field. The server will reject payloads missing this field
52
+ once ingest enforcement lands.
53
+ - **`seq`** (number) on every wire event — per-session monotonic sequence
54
+ number. Captured synchronously with the event's `timestamp` at
55
+ `track()` / enqueue time. Counter starts at 0 when the `CrossdeckServer`
56
+ instance is constructed (session start) and increments once per event.
57
+ Matches spec §3: monotonic within a session, never reset between
58
+ background/foreground (Node has no such lifecycle; the instance lifetime
59
+ IS the session).
60
+ - **`context`** (object) on every wire event — standardized device/platform
61
+ context (spec §4), promoted out of `properties`. Common fields: `os`,
62
+ `osVersion`, `appVersion`, `sdkName`, `sdkVersion`, `locale`, `timezone`.
63
+ Node-specific: `nodeVersion`, `host`, `region` (the existing
64
+ `runtime.*` props, promoted).
65
+
66
+ **Changed:**
67
+
68
+ - `track()` no longer merges `runtime.*` keys into `properties`. Those
69
+ facts now live in the top-level `context` object on the wire event.
70
+ Super-properties registered via `server.register()` continue to appear
71
+ in `properties` unchanged (caller-supplied values are unaffected).
72
+
7
73
  ## [1.5.1] — 2026-05-27
8
74
 
9
75
  `crossdeck.contract_failed` is now single-fire to a dedicated
@@ -1,4 +1,4 @@
1
- import { w as CrossdeckServer } from '../crossdeck-server-BFTTwCEP.mjs';
1
+ import { w as CrossdeckServer } from '../crossdeck-server-DYawt4eT.mjs';
2
2
  import 'node:events';
3
3
 
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { w as CrossdeckServer } from '../crossdeck-server-BFTTwCEP.js';
1
+ import { w as CrossdeckServer } from '../crossdeck-server-DYawt4eT.js';
2
2
  import 'node:events';
3
3
 
4
4
  /**
@@ -1,6 +1,6 @@
1
1
  import { EventEmitter } from 'node:events';
2
2
 
3
- type CrossdeckErrorType = "authentication_error" | "permission_error" | "invalid_request_error" | "rate_limit_error" | "internal_error" | "network_error" | "configuration_error";
3
+ type CrossdeckErrorType = "authentication_error" | "permission_error" | "invalid_request_error" | "rate_limit_error" | "version_error" | "internal_error" | "network_error" | "configuration_error";
4
4
  interface CrossdeckErrorPayload {
5
5
  type: CrossdeckErrorType;
6
6
  /**
@@ -19,6 +19,14 @@ interface CrossdeckErrorPayload {
19
19
  requestId?: string;
20
20
  status?: number;
21
21
  retryAfterMs?: number;
22
+ /**
23
+ * Required SDK version floor — populated only on a `426 Upgrade Required`
24
+ * / `sdk_version_unsupported` response, so the queue's PARK message names
25
+ * the exact version to update to.
26
+ */
27
+ minVersion?: string;
28
+ /** SDK surface the rejection applies to (web/node/swift/…), on a 426. */
29
+ surface?: string;
22
30
  }
23
31
  declare class CrossdeckError extends Error {
24
32
  readonly type: CrossdeckErrorType;
@@ -26,6 +34,8 @@ declare class CrossdeckError extends Error {
26
34
  readonly requestId?: string;
27
35
  readonly status?: number;
28
36
  readonly retryAfterMs?: number;
37
+ readonly minVersion?: string;
38
+ readonly surface?: string;
29
39
  constructor(payload: CrossdeckErrorPayload);
30
40
  /**
31
41
  * JSON representation suitable for structured loggers. Without this,
@@ -173,8 +183,8 @@ declare const CrossdeckContracts: {
173
183
  readonly byId: (id: string) => Contract | undefined;
174
184
  readonly byPillar: (pillar: ContractPillar) => readonly Contract[];
175
185
  readonly withStatus: (status: ContractStatus) => readonly Contract[];
176
- readonly sdkVersion: "1.5.3";
177
- readonly bundledIn: "@cross-deck/node@1.5.3";
186
+ readonly sdkVersion: "1.6.0";
187
+ readonly bundledIn: "@cross-deck/node@1.6.0";
178
188
  /**
179
189
  * Resolve a failing test back to the contract it exercises.
180
190
  * Used by test-framework hooks to find the contract id of a
@@ -1312,6 +1322,8 @@ declare class CrossdeckServer extends EventEmitter {
1312
1322
  private readonly processAnonymousId;
1313
1323
  private readonly runtime;
1314
1324
  private readonly runtimeProperties;
1325
+ /** Envelope v1 §4 context object — built once at SDK init, reused on every event. */
1326
+ private readonly eventContext;
1315
1327
  private readonly breadcrumbs;
1316
1328
  private readonly eventQueue;
1317
1329
  private readonly errorTracker;
@@ -1329,6 +1341,23 @@ declare class CrossdeckServer extends EventEmitter {
1329
1341
  */
1330
1342
  private readonly entitlementStore;
1331
1343
  private readonly debug;
1344
+ /**
1345
+ * Event Envelope v1 §3 — per-session monotonic sequence counter.
1346
+ *
1347
+ * Node has no mobile "session" lifecycle (no app launch / background /
1348
+ * foreground). We model a session as the SDK instance lifetime: the
1349
+ * counter starts at 0 on construction and increments once per
1350
+ * `track()` call. `session.started` is the boot-telemetry event
1351
+ * (the first `track()` called from `emitBootTelemetryEvent()`), so
1352
+ * the seq will be 0 for that event, matching the spec's "reset to 0
1353
+ * at session.started" clause — by construction, it's the zeroth event
1354
+ * of this instance's lifecycle.
1355
+ *
1356
+ * The counter persists for the entire process lifetime of the SDK
1357
+ * instance (spec §3 clause 1: background/foreground does not reset).
1358
+ * A new `CrossdeckServer` construction is the only reset (new session).
1359
+ */
1360
+ private sessionSeq;
1332
1361
  /**
1333
1362
  * Alias map — `developerUserId` / `anonymousId` → canonical
1334
1363
  * `crossdeckCustomerId`. Populated by `getEntitlements()` so a
@@ -1,6 +1,6 @@
1
1
  import { EventEmitter } from 'node:events';
2
2
 
3
- type CrossdeckErrorType = "authentication_error" | "permission_error" | "invalid_request_error" | "rate_limit_error" | "internal_error" | "network_error" | "configuration_error";
3
+ type CrossdeckErrorType = "authentication_error" | "permission_error" | "invalid_request_error" | "rate_limit_error" | "version_error" | "internal_error" | "network_error" | "configuration_error";
4
4
  interface CrossdeckErrorPayload {
5
5
  type: CrossdeckErrorType;
6
6
  /**
@@ -19,6 +19,14 @@ interface CrossdeckErrorPayload {
19
19
  requestId?: string;
20
20
  status?: number;
21
21
  retryAfterMs?: number;
22
+ /**
23
+ * Required SDK version floor — populated only on a `426 Upgrade Required`
24
+ * / `sdk_version_unsupported` response, so the queue's PARK message names
25
+ * the exact version to update to.
26
+ */
27
+ minVersion?: string;
28
+ /** SDK surface the rejection applies to (web/node/swift/…), on a 426. */
29
+ surface?: string;
22
30
  }
23
31
  declare class CrossdeckError extends Error {
24
32
  readonly type: CrossdeckErrorType;
@@ -26,6 +34,8 @@ declare class CrossdeckError extends Error {
26
34
  readonly requestId?: string;
27
35
  readonly status?: number;
28
36
  readonly retryAfterMs?: number;
37
+ readonly minVersion?: string;
38
+ readonly surface?: string;
29
39
  constructor(payload: CrossdeckErrorPayload);
30
40
  /**
31
41
  * JSON representation suitable for structured loggers. Without this,
@@ -173,8 +183,8 @@ declare const CrossdeckContracts: {
173
183
  readonly byId: (id: string) => Contract | undefined;
174
184
  readonly byPillar: (pillar: ContractPillar) => readonly Contract[];
175
185
  readonly withStatus: (status: ContractStatus) => readonly Contract[];
176
- readonly sdkVersion: "1.5.3";
177
- readonly bundledIn: "@cross-deck/node@1.5.3";
186
+ readonly sdkVersion: "1.6.0";
187
+ readonly bundledIn: "@cross-deck/node@1.6.0";
178
188
  /**
179
189
  * Resolve a failing test back to the contract it exercises.
180
190
  * Used by test-framework hooks to find the contract id of a
@@ -1312,6 +1322,8 @@ declare class CrossdeckServer extends EventEmitter {
1312
1322
  private readonly processAnonymousId;
1313
1323
  private readonly runtime;
1314
1324
  private readonly runtimeProperties;
1325
+ /** Envelope v1 §4 context object — built once at SDK init, reused on every event. */
1326
+ private readonly eventContext;
1315
1327
  private readonly breadcrumbs;
1316
1328
  private readonly eventQueue;
1317
1329
  private readonly errorTracker;
@@ -1329,6 +1341,23 @@ declare class CrossdeckServer extends EventEmitter {
1329
1341
  */
1330
1342
  private readonly entitlementStore;
1331
1343
  private readonly debug;
1344
+ /**
1345
+ * Event Envelope v1 §3 — per-session monotonic sequence counter.
1346
+ *
1347
+ * Node has no mobile "session" lifecycle (no app launch / background /
1348
+ * foreground). We model a session as the SDK instance lifetime: the
1349
+ * counter starts at 0 on construction and increments once per
1350
+ * `track()` call. `session.started` is the boot-telemetry event
1351
+ * (the first `track()` called from `emitBootTelemetryEvent()`), so
1352
+ * the seq will be 0 for that event, matching the spec's "reset to 0
1353
+ * at session.started" clause — by construction, it's the zeroth event
1354
+ * of this instance's lifecycle.
1355
+ *
1356
+ * The counter persists for the entire process lifetime of the SDK
1357
+ * instance (spec §3 clause 1: background/foreground does not reset).
1358
+ * A new `CrossdeckServer` construction is the only reset (new session).
1359
+ */
1360
+ private sessionSeq;
1332
1361
  /**
1333
1362
  * Alias map — `developerUserId` / `anonymousId` → canonical
1334
1363
  * `crossdeckCustomerId`. Populated by `getEntitlements()` so a
package/dist/index.cjs CHANGED
@@ -66,6 +66,8 @@ var CrossdeckError = class _CrossdeckError extends Error {
66
66
  requestId;
67
67
  status;
68
68
  retryAfterMs;
69
+ minVersion;
70
+ surface;
69
71
  constructor(payload) {
70
72
  super(payload.message);
71
73
  this.name = "CrossdeckError";
@@ -74,6 +76,8 @@ var CrossdeckError = class _CrossdeckError extends Error {
74
76
  this.requestId = payload.requestId;
75
77
  this.status = payload.status;
76
78
  this.retryAfterMs = payload.retryAfterMs;
79
+ this.minVersion = payload.minVersion;
80
+ this.surface = payload.surface;
77
81
  Object.setPrototypeOf(this, _CrossdeckError.prototype);
78
82
  }
79
83
  /**
@@ -95,6 +99,8 @@ var CrossdeckError = class _CrossdeckError extends Error {
95
99
  requestId: this.requestId,
96
100
  status: this.status,
97
101
  retryAfterMs: this.retryAfterMs,
102
+ minVersion: this.minVersion,
103
+ surface: this.surface,
98
104
  stack: this.stack
99
105
  };
100
106
  }
@@ -185,7 +191,10 @@ async function crossdeckErrorFromResponse(res) {
185
191
  message: envelope.message ?? `HTTP ${res.status}`,
186
192
  requestId: envelope.request_id ?? requestId,
187
193
  status: res.status,
188
- retryAfterMs
194
+ retryAfterMs,
195
+ // PARK metadata, present only on a 426 / sdk_version_unsupported body.
196
+ minVersion: typeof envelope.minVersion === "string" ? envelope.minVersion : void 0,
197
+ surface: typeof envelope.surface === "string" ? envelope.surface : void 0
189
198
  });
190
199
  }
191
200
  return makeCrossdeckError({
@@ -378,7 +387,7 @@ function byteLength(s) {
378
387
  var https = __toESM(require("https"));
379
388
 
380
389
  // src/_version.ts
381
- var SDK_VERSION = "1.5.3";
390
+ var SDK_VERSION = "1.7.0";
382
391
  var SDK_NAME = "@cross-deck/node";
383
392
 
384
393
  // src/_diagnostic-telemetry.ts
@@ -398,7 +407,12 @@ var DIAGNOSTIC_TELEMETRY_ALLOWED_KEYS = /* @__PURE__ */ new Set([
398
407
  "run_id",
399
408
  "test_file",
400
409
  "test_name",
401
- "device_class"
410
+ "device_class",
411
+ // verification_phase — categorical `boot` / `hot_path` bucket set by the
412
+ // runtime contract verifier layer. Declared in the shared diagnostics
413
+ // contract (contracts/diagnostics/contract-failed-payload-schema-lock.json)
414
+ // and carried by web/swift; node had drifted by omitting it.
415
+ "verification_phase"
402
416
  ]);
403
417
  function filterDiagnosticPayload(payload) {
404
418
  const filtered = {};
@@ -891,6 +905,16 @@ var EventQueue = class {
891
905
  cancelTimer = null;
892
906
  firstFlushFired = false;
893
907
  nextRetryAt = null;
908
+ /**
909
+ * PARK state (HTTP 426 / `sdk_version_unsupported`). Once parked, the
910
+ * queue stops flushing — retrying a known-too-old payload only wastes
911
+ * bandwidth and drips pointless rejects into the server logs until the
912
+ * process restarts on an upgraded SDK. In-memory: a fresh process starts
913
+ * unparked, retries once, and either delivers (upgraded) or re-parks.
914
+ */
915
+ parked = false;
916
+ /** One developer-facing console warning per process — never per-event spam. */
917
+ parkWarned = false;
894
918
  retry;
895
919
  /**
896
920
  * Stable Idempotency-Key for the current in-flight batch. Minted
@@ -943,6 +967,7 @@ var EventQueue = class {
943
967
  * this one settles. Strict ordering preserved.
944
968
  */
945
969
  async flush() {
970
+ if (this.parked) return null;
946
971
  let batch;
947
972
  let batchId;
948
973
  if (this.pendingBatch !== null && this.pendingBatchId !== null) {
@@ -962,6 +987,11 @@ var EventQueue = class {
962
987
  try {
963
988
  const env = this.cfg.envelope();
964
989
  const body = {
990
+ // Event Envelope v1 §1 — schema/wire version the server uses to
991
+ // decide whether it can parse this payload. Integer 1; only bumped
992
+ // on a breaking wire change. Distinct from sdk.version (which
993
+ // answers "which build?") — two questions, two fields.
994
+ envelopeVersion: 1,
965
995
  events: batch,
966
996
  sdk: env.sdk
967
997
  };
@@ -985,6 +1015,28 @@ var EventQueue = class {
985
1015
  } catch (err) {
986
1016
  const message = err instanceof Error ? err.message : String(err);
987
1017
  this.lastError = message;
1018
+ if (isVersionRejected(err)) {
1019
+ this.parked = true;
1020
+ this.buffer = [...batch, ...this.buffer];
1021
+ if (this.buffer.length > HARD_BUFFER_CAP) {
1022
+ const overflow = this.buffer.length - HARD_BUFFER_CAP;
1023
+ this.buffer.splice(0, overflow);
1024
+ this.dropped += overflow;
1025
+ }
1026
+ this.pendingBatch = null;
1027
+ this.pendingBatchId = null;
1028
+ this.inFlight -= batch.length;
1029
+ this.cfg.onBufferChange?.(this.buffer.length);
1030
+ const minVersion = versionRejectionFloor(err);
1031
+ if (!this.parkWarned) {
1032
+ this.parkWarned = true;
1033
+ console.warn(
1034
+ `[Crossdeck] SDK outdated \u2014 the server is no longer accepting this version's event format. Events are PARKED in memory (held, not lost across this process) and will deliver once you update @cross-deck/node${minVersion ? ` to >= ${minVersion}` : ""} and restart. A restart before upgrade clears them \u2014 configure a disk queue for cross-restart durability.`
1035
+ );
1036
+ }
1037
+ this.cfg.onParked?.({ minVersion, surface: versionRejectionSurface(err) });
1038
+ return null;
1039
+ }
988
1040
  if (isPermanent4xx(err)) {
989
1041
  const droppedCount = batch.length;
990
1042
  this.pendingBatch = null;
@@ -1083,8 +1135,22 @@ function isPermanent4xx(err) {
1083
1135
  if (typeof status !== "number" || !Number.isFinite(status)) return false;
1084
1136
  if (status < 400 || status >= 500) return false;
1085
1137
  if (status === 408 || status === 429) return false;
1138
+ if (status === 426) return false;
1086
1139
  return true;
1087
1140
  }
1141
+ function isVersionRejected(err) {
1142
+ if (!err || typeof err !== "object") return false;
1143
+ if (err.status === 426) return true;
1144
+ return err.code === "sdk_version_unsupported";
1145
+ }
1146
+ function versionRejectionFloor(err) {
1147
+ const v = err?.minVersion;
1148
+ return typeof v === "string" && v.length > 0 ? v : void 0;
1149
+ }
1150
+ function versionRejectionSurface(err) {
1151
+ const v = err?.surface;
1152
+ return typeof v === "string" && v.length > 0 ? v : void 0;
1153
+ }
1088
1154
  function defaultScheduler(fn, ms) {
1089
1155
  const id = setTimeout(fn, ms);
1090
1156
  if (typeof id.unref === "function") {
@@ -1946,6 +2012,25 @@ function runtimeInfoToProperties(info) {
1946
2012
  if (info.appVersion) out.appVersion = info.appVersion;
1947
2013
  return out;
1948
2014
  }
2015
+ function buildEventContext(info, sdkName, sdkVersion) {
2016
+ const ctx = {
2017
+ // Common fields (spec §4, all platforms)
2018
+ os: info.platform,
2019
+ osVersion: info.platformRelease,
2020
+ appVersion: info.appVersion ?? null,
2021
+ sdkName,
2022
+ sdkVersion,
2023
+ // locale / timezone: Node has no process-level locale by default;
2024
+ // surface them from the environment if available, otherwise null.
2025
+ locale: typeof process !== "undefined" && process.env["LANG"] || null,
2026
+ timezone: typeof Intl !== "undefined" && Intl.DateTimeFormat().resolvedOptions().timeZone || null,
2027
+ // Node-specific runtime context (spec §4 Node section)
2028
+ nodeVersion: info.nodeVersion,
2029
+ host: info.host,
2030
+ region: info.region ?? null
2031
+ };
2032
+ return ctx;
2033
+ }
1949
2034
 
1950
2035
  // src/flush-on-exit.ts
1951
2036
  var SIGNALS = ["SIGTERM", "SIGINT"];
@@ -2613,6 +2698,8 @@ var CrossdeckServer = class extends import_node_events.EventEmitter {
2613
2698
  processAnonymousId;
2614
2699
  runtime;
2615
2700
  runtimeProperties;
2701
+ /** Envelope v1 §4 context object — built once at SDK init, reused on every event. */
2702
+ eventContext;
2616
2703
  breadcrumbs;
2617
2704
  eventQueue;
2618
2705
  errorTracker;
@@ -2630,6 +2717,23 @@ var CrossdeckServer = class extends import_node_events.EventEmitter {
2630
2717
  */
2631
2718
  entitlementStore;
2632
2719
  debug;
2720
+ /**
2721
+ * Event Envelope v1 §3 — per-session monotonic sequence counter.
2722
+ *
2723
+ * Node has no mobile "session" lifecycle (no app launch / background /
2724
+ * foreground). We model a session as the SDK instance lifetime: the
2725
+ * counter starts at 0 on construction and increments once per
2726
+ * `track()` call. `session.started` is the boot-telemetry event
2727
+ * (the first `track()` called from `emitBootTelemetryEvent()`), so
2728
+ * the seq will be 0 for that event, matching the spec's "reset to 0
2729
+ * at session.started" clause — by construction, it's the zeroth event
2730
+ * of this instance's lifecycle.
2731
+ *
2732
+ * The counter persists for the entire process lifetime of the SDK
2733
+ * instance (spec §3 clause 1: background/foreground does not reset).
2734
+ * A new `CrossdeckServer` construction is the only reset (new session).
2735
+ */
2736
+ sessionSeq = 0;
2633
2737
  /**
2634
2738
  * Alias map — `developerUserId` / `anonymousId` → canonical
2635
2739
  * `crossdeckCustomerId`. Populated by `getEntitlements()` so a
@@ -2689,6 +2793,7 @@ var CrossdeckServer = class extends import_node_events.EventEmitter {
2689
2793
  appVersion: options.appVersion
2690
2794
  });
2691
2795
  this.runtimeProperties = runtimeInfoToProperties(this.runtime);
2796
+ this.eventContext = buildEventContext(this.runtime, SDK_NAME, this.sdkVersion);
2692
2797
  this.breadcrumbs = new BreadcrumbBuffer(options.breadcrumbsMaxSize ?? 50);
2693
2798
  this.superProps = new SuperPropertyStore();
2694
2799
  this.entitlementCache = new EntitlementCache({
@@ -2749,6 +2854,13 @@ var CrossdeckServer = class extends import_node_events.EventEmitter {
2749
2854
  error: info.lastError
2750
2855
  });
2751
2856
  },
2857
+ onParked: (info) => {
2858
+ this.debug.emit(
2859
+ "sdk.parked",
2860
+ `[crossdeck] SDK parked \u2014 server no longer accepts this version's event format. Events held (paused, not lost); update @cross-deck/node${info.minVersion ? ` to >= ${info.minVersion}` : ""} and restart to resume.`,
2861
+ { ...info }
2862
+ );
2863
+ },
2752
2864
  onFirstFlushSuccess: () => {
2753
2865
  this.debug.emit("sdk.first_event_sent", "First batch landed.");
2754
2866
  }
@@ -3140,7 +3252,6 @@ var CrossdeckServer = class extends import_node_events.EventEmitter {
3140
3252
  }
3141
3253
  }
3142
3254
  const properties = {
3143
- ...this.runtimeProperties,
3144
3255
  ...this.superProps.getSuperProperties(),
3145
3256
  ...sanitized
3146
3257
  };
@@ -3149,10 +3260,14 @@ var CrossdeckServer = class extends import_node_events.EventEmitter {
3149
3260
  properties.$groups = groupIds;
3150
3261
  }
3151
3262
  const identity = this.resolveIdentity(event);
3263
+ const seq = this.sessionSeq++;
3264
+ const timestamp = event.timestamp ?? Date.now();
3152
3265
  const queued = {
3153
3266
  eventId: event.eventId ?? mintId("evt", 8),
3154
3267
  name: event.name,
3155
- timestamp: event.timestamp ?? Date.now(),
3268
+ timestamp,
3269
+ seq,
3270
+ context: this.eventContext,
3156
3271
  properties,
3157
3272
  ...identity
3158
3273
  };
@@ -3191,6 +3306,8 @@ var CrossdeckServer = class extends import_node_events.EventEmitter {
3191
3306
  }
3192
3307
  const normalized = events.map((event) => this.normalizeIngestEvent(event));
3193
3308
  const body = {
3309
+ // Event Envelope v1 §1 — wire version (parity with the queue path).
3310
+ envelopeVersion: 1,
3194
3311
  events: normalized,
3195
3312
  sdk: { name: SDK_NAME, version: this.sdkVersion },
3196
3313
  // Match the queue's batch envelope (see event-queue.ts) — backend
@@ -4318,6 +4435,13 @@ var _CROSSDECK_ERROR_CODES = Object.freeze([
4318
4435
  description: "A field is present but the value failed validation.",
4319
4436
  resolution: "Read error.message for the field + reason. SDK-managed call sites should never emit this \u2014 file a bug if you do.",
4320
4437
  retryable: false
4438
+ },
4439
+ {
4440
+ code: "sdk_version_unsupported",
4441
+ type: "version_error",
4442
+ description: "HTTP 426 \u2014 your installed SDK sends an event format the server no longer accepts. The data is good; only the wire dialect is too old. The SDK PARKS automatically: events are held in memory and deliver once you upgrade and restart.",
4443
+ resolution: "Update @cross-deck/node to at least the version in error.minVersion and restart \u2014 the held queue backfills. See https://cross-deck.com/docs/sdk-event-durability/.",
4444
+ retryable: false
4321
4445
  }
4322
4446
  ]);
4323
4447
  function isCrossdeckErrorCode(code) {
@@ -4449,8 +4573,8 @@ function normaliseSecrets(input) {
4449
4573
  }
4450
4574
 
4451
4575
  // src/_contracts-bundled.ts
4452
- var BUNDLED_IN = "@cross-deck/node@1.5.3";
4453
- var SDK_VERSION2 = "1.5.3";
4576
+ var BUNDLED_IN = "@cross-deck/node@1.6.0";
4577
+ var SDK_VERSION2 = "1.6.0";
4454
4578
  var BUNDLED_CONTRACTS = Object.freeze([
4455
4579
  {
4456
4580
  "id": "contract-failed-payload-schema-lock",
@@ -4572,7 +4696,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
4572
4696
  "legal/security/index.html#diagnostic",
4573
4697
  "legal/sdk-data/index.html#b-diagnostic"
4574
4698
  ],
4575
- "bundledIn": "@cross-deck/node@1.5.3"
4699
+ "bundledIn": "@cross-deck/node@1.6.0"
4576
4700
  },
4577
4701
  {
4578
4702
  "id": "documentation-honesty",
@@ -4604,7 +4728,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
4604
4728
  ],
4605
4729
  "registeredAt": "2026-05-26",
4606
4730
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 7.1",
4607
- "bundledIn": "@cross-deck/node@1.5.3"
4731
+ "bundledIn": "@cross-deck/node@1.6.0"
4608
4732
  },
4609
4733
  {
4610
4734
  "id": "error-envelope-shape",
@@ -4643,7 +4767,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
4643
4767
  ],
4644
4768
  "registeredAt": "2026-05-26",
4645
4769
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 8 (codifies existing contract)",
4646
- "bundledIn": "@cross-deck/node@1.5.3"
4770
+ "bundledIn": "@cross-deck/node@1.6.0"
4647
4771
  },
4648
4772
  {
4649
4773
  "id": "flush-interval-parity",
@@ -4688,7 +4812,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
4688
4812
  ],
4689
4813
  "registeredAt": "2026-05-26",
4690
4814
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 3.3",
4691
- "bundledIn": "@cross-deck/node@1.5.3"
4815
+ "bundledIn": "@cross-deck/node@1.6.0"
4692
4816
  },
4693
4817
  {
4694
4818
  "id": "idempotency-key-deterministic",
@@ -4793,7 +4917,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
4793
4917
  ],
4794
4918
  "registeredAt": "2026-05-26",
4795
4919
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 2.2.a + 2.2.b + 2.2.c",
4796
- "bundledIn": "@cross-deck/node@1.5.3"
4920
+ "bundledIn": "@cross-deck/node@1.6.0"
4797
4921
  },
4798
4922
  {
4799
4923
  "id": "node-pii-scrubber",
@@ -4832,7 +4956,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
4832
4956
  ],
4833
4957
  "registeredAt": "2026-05-26",
4834
4958
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 3.1",
4835
- "bundledIn": "@cross-deck/node@1.5.3"
4959
+ "bundledIn": "@cross-deck/node@1.6.0"
4836
4960
  },
4837
4961
  {
4838
4962
  "id": "node-shutdown-awaits-flush",
@@ -4865,7 +4989,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
4865
4989
  ],
4866
4990
  "registeredAt": "2026-05-26",
4867
4991
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 5.4",
4868
- "bundledIn": "@cross-deck/node@1.5.3"
4992
+ "bundledIn": "@cross-deck/node@1.6.0"
4869
4993
  },
4870
4994
  {
4871
4995
  "id": "sdk-error-codes-catalogue",
@@ -4879,9 +5003,14 @@ var BUNDLED_CONTRACTS = Object.freeze([
4879
5003
  "codeRef": [
4880
5004
  "sdks/web/src/error-codes.ts",
4881
5005
  "sdks/node/src/error-codes.ts",
5006
+ "sdks/web/src/_contract-verifiers.ts",
4882
5007
  "backend/src/api/v1-errors.ts"
4883
5008
  ],
4884
5009
  "testRef": [
5010
+ {
5011
+ "file": "sdks/web/tests/contract-verifiers.test.ts",
5012
+ "name": "sdk-error-codes-catalogue covers every backend wire code with remediation"
5013
+ },
4885
5014
  {
4886
5015
  "file": "sdks/web/tests/error-codes-backfill.test.ts",
4887
5016
  "name": "includes backend code"
@@ -4905,7 +5034,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
4905
5034
  ],
4906
5035
  "registeredAt": "2026-05-26",
4907
5036
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 6.2",
4908
- "bundledIn": "@cross-deck/node@1.5.3"
5037
+ "bundledIn": "@cross-deck/node@1.6.0"
4909
5038
  },
4910
5039
  {
4911
5040
  "id": "sync-purchases-funnel-parity",
@@ -4938,7 +5067,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
4938
5067
  ],
4939
5068
  "registeredAt": "2026-05-26",
4940
5069
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 3.5",
4941
- "bundledIn": "@cross-deck/node@1.5.3"
5070
+ "bundledIn": "@cross-deck/node@1.6.0"
4942
5071
  },
4943
5072
  {
4944
5073
  "id": "verifier-timestamp-mandatory",
@@ -4992,7 +5121,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
4992
5121
  ],
4993
5122
  "registeredAt": "2026-05-26",
4994
5123
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 7.2",
4995
- "bundledIn": "@cross-deck/node@1.5.3"
5124
+ "bundledIn": "@cross-deck/node@1.6.0"
4996
5125
  }
4997
5126
  ]);
4998
5127