@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 +66 -0
- package/dist/auto-events/index.d.mts +1 -1
- package/dist/auto-events/index.d.ts +1 -1
- package/dist/{crossdeck-server-BFTTwCEP.d.mts → crossdeck-server-DYawt4eT.d.mts} +32 -3
- package/dist/{crossdeck-server-BFTTwCEP.d.ts → crossdeck-server-DYawt4eT.d.ts} +32 -3
- package/dist/index.cjs +146 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +10 -4
- package/dist/index.d.ts +10 -4
- package/dist/index.mjs +146 -17
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/dist/contracts.json +0 -552
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,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.
|
|
177
|
-
readonly bundledIn: "@cross-deck/node@1.
|
|
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.
|
|
177
|
-
readonly bundledIn: "@cross-deck/node@1.
|
|
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.
|
|
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
|
|
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.
|
|
4453
|
-
var SDK_VERSION2 = "1.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
5124
|
+
"bundledIn": "@cross-deck/node@1.6.0"
|
|
4996
5125
|
}
|
|
4997
5126
|
]);
|
|
4998
5127
|
|