@cross-deck/node 1.5.0 → 1.5.1

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,59 @@ 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.5.1] — 2026-05-27
8
+
9
+ `crossdeck.contract_failed` is now single-fire to a dedicated
10
+ reliability endpoint instead of the customer's `track()` pipeline.
11
+ Independent-controller flow per Privacy Policy §6; schema-locked by
12
+ `contracts/diagnostics/contract-failed-payload-schema-lock.json`.
13
+ `ContractFailureInput.extra` removed (schema-lock forbids unbounded
14
+ fields); `ContractFailureInput.deviceClass` added.
15
+
16
+ ## [1.5.0] — 2026-05-26
17
+
18
+ Minor — `CrossdeckContracts` + `reportContractFailure(...)` ship as a
19
+ new public surface on every SDK simultaneously. Additive only; no
20
+ behavioural change to existing APIs.
21
+
22
+ **Added:**
23
+
24
+ - **`CrossdeckContracts` namespace** — typed access to the bank-grade
25
+ contract registry. Methods: `all()`, `allIncludingHistorical()`,
26
+ `byId(id)`, `byPillar(pillar)`, `withStatus(status)`,
27
+ `findByTestName(name)`. Properties: `sdkVersion`, `bundledIn`
28
+ (e.g. `"@cross-deck/node@1.5.0"`).
29
+ - **`Contract` type + `ContractPillar` / `ContractStatus` /
30
+ `ContractAppliesTo` unions + `ContractTestRef` + `ContractFailureInput`
31
+ interfaces** exported from the top-level entry. Treated as
32
+ binary-stable.
33
+ - **`CrossdeckServer.reportContractFailure(input)` method** — fires a
34
+ typed `crossdeck.contract_failed` server event through the standard
35
+ `track()` pipeline. Wire properties: `contract_id`, `sdk_version`
36
+ (auto-stamped), `sdk_platform` (auto-stamped to `"node"`),
37
+ `failure_reason`, `run_context` (`ci` | `dogfood` | `customer-app`),
38
+ `run_id`, plus optional `test_file` / `test_name` from `input.testRef`.
39
+
40
+ **Fixed:**
41
+
42
+ - `shutdownSync()` now emits the `sdk.shutdown` EventEmitter signal
43
+ with the correct reason — previously only the async `shutdown()`
44
+ path emitted, leaving consumers of `Symbol.dispose` /
45
+ `shutdownSync()` direct-callers blind. Async path is unchanged
46
+ thanks to a private dedup gate so listeners still fire exactly
47
+ once per teardown.
48
+ - Test infrastructure: shutdown-flush + track-PII-scrub tests were
49
+ reading `body.data` from captured fetch payloads but the wire
50
+ shape uses `body.events` (matching backend + Web/RN SDKs). Tests
51
+ fixed to read the correct field; behaviour was already correct.
52
+
53
+ **Changed:**
54
+
55
+ - Contract registry source files migrated to camelCase keys
56
+ (`appliesTo`, `codeRef`, `testRef`, `registeredAt`,
57
+ `firstRegisteredIn`). The bundled `contracts.json` sidecar uses
58
+ the new keys; `bundledIn` is build-stamped, never in source.
59
+
7
60
  ## [1.4.2] — 2026-05-26
8
61
 
9
62
  Patch — fix `tests/shutdown-flush.test.ts` compile error under
package/README.md CHANGED
@@ -569,6 +569,64 @@ const failed = results.filter((r) => !r.ok);
569
569
 
570
570
  Symmetric `bulkRevokeEntitlement(revokes[], options?)`.
571
571
 
572
+ ## Bank-grade contracts
573
+
574
+ The SDK ships its own contracts registry — every behavioural guarantee the SDK makes (per-user cache isolation, deterministic Idempotency-Key, queue durability, etc.) lives in `contracts/**/*.json` at the monorepo root and is **bundled into every release**. The customer's lockfile pins SDK code + contracts atomically — drift between what the SDK does and what it claims is structurally impossible. See [`contracts/README.md`](https://github.com/VistaApps-za/crossdeck/blob/main/contracts/README.md) for the full architecture.
575
+
576
+ ### `CrossdeckContracts` — typed access to the bundled registry
577
+
578
+ ```ts
579
+ import { CrossdeckContracts } from "@cross-deck/node";
580
+
581
+ CrossdeckContracts.all(); // enforced contracts only
582
+ CrossdeckContracts.allIncludingHistorical(); // + proposed + retired
583
+ CrossdeckContracts.byId("idempotency-key-deterministic");
584
+ CrossdeckContracts.byPillar("revenue");
585
+ CrossdeckContracts.withStatus("proposed");
586
+ CrossdeckContracts.findByTestName("rail namespacing prevents cross-rail collisions");
587
+ CrossdeckContracts.sdkVersion; // "1.5.0"
588
+ CrossdeckContracts.bundledIn; // "@cross-deck/node@1.5.0"
589
+ ```
590
+
591
+ The `Contract` type is exported alongside; the binary-stability promise is documented in [`contracts/README.md`](https://github.com/VistaApps-za/crossdeck/blob/main/contracts/README.md).
592
+
593
+ ### `crossdeckServer.reportContractFailure(input)` — surface contract test failures
594
+
595
+ When a contract test asserts and fails — in your CI, a dogfood run, or a customer integration test — fire a typed `crossdeck.contract_failed` event over the **Crossdeck reliability channel**. This is one-way operational telemetry to the Crossdeck operations team (Privacy Policy §6, "Flow B"); it never enters your `track()` pipeline, never shows in your dashboard, never bills against your event quota. The wire shape is schema-locked at [`contracts/diagnostics/contract-failed-payload-schema-lock.json`](https://github.com/VistaApps-za/crossdeck/blob/main/contracts/diagnostics/contract-failed-payload-schema-lock.json):
596
+
597
+ ```ts
598
+ import { CrossdeckServer } from "@cross-deck/node";
599
+
600
+ const cd = new CrossdeckServer({ secretKey: process.env.CROSSDECK_SECRET_KEY! });
601
+
602
+ cd.reportContractFailure({
603
+ contractId: "idempotency-key-deterministic",
604
+ failureReason: "expected cross-SDK oracle to match canonical vector, got drift",
605
+ runContext: process.env.CI ? "ci" : "dogfood",
606
+ runId: process.env.GITHUB_RUN_ID ?? crypto.randomUUID(),
607
+ testRef: {
608
+ file: "tests/idempotency-key.test.ts",
609
+ name: "apple JWS produces the canonical pinned UUID across all 5 SDKs",
610
+ },
611
+ });
612
+ ```
613
+
614
+ No new endpoint, no special ingest path — the event lands in the same pipeline every other server-side `track()` call does. It surfaces immediately in the dashboard's live event feed, the breakdown chart (group by `contract_id`, `sdk_platform`), and any alert rule with `event = crossdeck.contract_failed`.
615
+
616
+ Properties stamped on the wire:
617
+
618
+ | Property | Source |
619
+ |----------|--------|
620
+ | `contract_id` | caller |
621
+ | `sdk_version`, `sdk_platform` | auto-stamped (`@cross-deck/node` ships `sdk_platform: "node"`) |
622
+ | `failure_reason`, `run_context`, `run_id` | caller |
623
+ | `test_file`, `test_name` | set when `testRef` is provided |
624
+ | `device_class` | optional, set by caller (categorical bucket — e.g. `"linux-server"`, `"container"`, `"lambda"`) |
625
+
626
+ The wire shape is schema-locked at [`contracts/diagnostics/contract-failed-payload-schema-lock.json`](https://github.com/VistaApps-za/crossdeck/blob/main/contracts/diagnostics/contract-failed-payload-schema-lock.json); per-SDK assertion tests gate it on every release. Free-form `extra` keys are not accepted — adding a field requires an amendment to the schema-lock contract first.
627
+
628
+ For per-test-framework hooks see [`contracts/README.md` § Reporting contract failures](https://github.com/VistaApps-za/crossdeck/blob/main/contracts/README.md#reporting-contract-failures-back-to-crossdeck).
629
+
572
630
  ## Node version
573
631
 
574
632
  Node 18+. Uses the platform `fetch` and `node:crypto` — zero runtime dependencies.
@@ -1,4 +1,4 @@
1
- import { w as CrossdeckServer } from '../crossdeck-server-oAaKBnUU.mjs';
1
+ import { w as CrossdeckServer } from '../crossdeck-server-CY4PZk-j.mjs';
2
2
  import 'node:events';
3
3
 
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { w as CrossdeckServer } from '../crossdeck-server-oAaKBnUU.js';
1
+ import { w as CrossdeckServer } from '../crossdeck-server-CY4PZk-j.js';
2
2
  import 'node:events';
3
3
 
4
4
  /**
@@ -1,11 +1,133 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "generatedAt": "2026-05-26T13:04:47.333Z",
3
+ "generatedAt": "2026-05-27T10:35:34.672Z",
4
4
  "sdk": "@cross-deck/node",
5
- "sdkVersion": "1.5.0",
6
- "bundledIn": "@cross-deck/node@1.5.0",
7
- "count": 9,
5
+ "sdkVersion": "1.5.1",
6
+ "bundledIn": "@cross-deck/node@1.5.1",
7
+ "count": 10,
8
8
  "contracts": [
9
+ {
10
+ "id": "contract-failed-payload-schema-lock",
11
+ "pillar": "diagnostics",
12
+ "status": "enforced",
13
+ "claim": "The `crossdeck.contract_failed` event payload contains ONLY the named diagnostic fields and never any end-user personal data. The wire shape is fixed — adding a new field requires (1) a pull request that updates this contract's `allowedFields` set, (2) a Privacy Policy §6 amendment, and (3) the Customer Disclosure Template / SDK Data Collection Reference §B updates. Per-SDK assertion tests enforce the field set on every release. The `verification_phase` field is a categorical bucket — values are restricted to `boot` (the SDK self-test ran on Crossdeck.start) or `hot_path` (a verifier observed a real customer-triggered operation). The categorical nature is what preserves the diagnostic-only-not-personal classification. This is the structural guarantee that backs the independent-controller lawful basis in the Privacy Policy: the payload remains diagnostic-only, not personal, so the legitimate-interest analysis stays valid as the SDK evolves.",
14
+ "appliesTo": [
15
+ "web",
16
+ "node",
17
+ "swift",
18
+ "android",
19
+ "react-native"
20
+ ],
21
+ "allowedFields": {
22
+ "required": [
23
+ "contract_id",
24
+ "sdk_version",
25
+ "sdk_platform",
26
+ "failure_reason",
27
+ "run_context",
28
+ "run_id"
29
+ ],
30
+ "optional": [
31
+ "test_file",
32
+ "test_name",
33
+ "device_class",
34
+ "verification_phase"
35
+ ],
36
+ "forbidden": [
37
+ "anonymousId",
38
+ "developerUserId",
39
+ "crossdeckCustomerId",
40
+ "email",
41
+ "ip",
42
+ "user_agent",
43
+ "message",
44
+ "stack",
45
+ "stack_trace",
46
+ "frames",
47
+ "exception_message",
48
+ "url",
49
+ "path",
50
+ "screen",
51
+ "title",
52
+ "label",
53
+ "text",
54
+ "ariaLabel",
55
+ "accessibilityLabel",
56
+ "contentDescription",
57
+ "session_id",
58
+ "sessionId"
59
+ ]
60
+ },
61
+ "transport": "Telemetry is single-fire to the Crossdeck reliability endpoint only — NOT the customer's appId. The customer's track() pipeline never carries `crossdeck.*` events; the customer's dashboard never shows individual contract failures. Operational telemetry flows one-way to the Crossdeck operations team for SDK reliability purposes (legitimate interest, independent-controller flow per Privacy Policy §6). The reliability endpoint is hardcoded at SDK build time; the publishable key for the reliability project is embedded as a constant and rejects writes that don't match the schema.",
62
+ "codeRef": [
63
+ "sdks/web/src/crossdeck.ts",
64
+ "sdks/node/src/crossdeck-server.ts",
65
+ "sdks/swift/Sources/Crossdeck/Crossdeck.swift",
66
+ "sdks/swift/Sources/Crossdeck/_DiagnosticTelemetry.swift",
67
+ "sdks/android/crossdeck/src/main/kotlin/com/crossdeck/Crossdeck.kt",
68
+ "sdks/android/crossdeck/src/main/kotlin/com/crossdeck/_DiagnosticTelemetry.kt",
69
+ "sdks/react-native/src/crossdeck.ts",
70
+ "backend/src/api/v1-sdk-diagnostic.ts",
71
+ "sdks/web/src/_diagnostic-telemetry.ts",
72
+ "sdks/node/src/_diagnostic-telemetry.ts",
73
+ "sdks/react-native/src/_diagnostic-telemetry.ts"
74
+ ],
75
+ "testRef": [
76
+ {
77
+ "file": "sdks/web/tests/contract-failed-schema-lock.test.ts",
78
+ "name": "reportContractFailure payload conforms to schema-lock"
79
+ },
80
+ {
81
+ "file": "sdks/node/tests/contract-failed-schema-lock.test.ts",
82
+ "name": "reportContractFailure payload conforms to schema-lock"
83
+ },
84
+ {
85
+ "file": "sdks/swift/Tests/CrossdeckTests/ContractFailedSchemaLockTests.swift",
86
+ "name": "test_reportContractFailure_payloadFieldsAreInAllowList"
87
+ },
88
+ {
89
+ "file": "sdks/swift/Tests/CrossdeckTests/ContractFailedSchemaLockTests.swift",
90
+ "name": "test_reportContractFailure_doesNotEnterCustomerTrackPipeline"
91
+ },
92
+ {
93
+ "file": "sdks/android/crossdeck/src/test/kotlin/com/crossdeck/ContractFailedSchemaLockTest.kt",
94
+ "name": "reportContractFailure payload conforms to schema-lock"
95
+ },
96
+ {
97
+ "file": "sdks/android/crossdeck/src/test/kotlin/com/crossdeck/ContractFailedSchemaLockTest.kt",
98
+ "name": "reportContractFailure does not enter customer track pipeline"
99
+ },
100
+ {
101
+ "file": "sdks/react-native/tests/contract-failed-schema-lock.test.ts",
102
+ "name": "reportContractFailure payload conforms to schema-lock"
103
+ },
104
+ {
105
+ "file": "backend/tests/unit/v1-sdk-diagnostic.test.ts",
106
+ "name": "forbidden fields are enumerated in the schema-lock contract"
107
+ },
108
+ {
109
+ "file": "backend/tests/unit/v1-sdk-diagnostic.test.ts",
110
+ "name": "required fields are enumerated in the schema-lock contract"
111
+ },
112
+ {
113
+ "file": "backend/tests/unit/v1-sdk-diagnostic.test.ts",
114
+ "name": "regression guard: never returns a raw IP"
115
+ },
116
+ {
117
+ "file": "backend/tests/unit/v1-sdk-diagnostic.test.ts",
118
+ "name": "verification_phase is in the optional field set"
119
+ }
120
+ ],
121
+ "registeredAt": "2026-05-27",
122
+ "firstRegisteredIn": "Diagnostic telemetry single-fire + schema-lock — independent-controller flow",
123
+ "privacyReferences": [
124
+ "legal/privacy/index.html#sdk-diagnostic",
125
+ "legal/customer-disclosure/index.html#flow-b",
126
+ "legal/security/index.html#diagnostic",
127
+ "legal/sdk-data/index.html#b-diagnostic"
128
+ ],
129
+ "bundledIn": "@cross-deck/node@1.5.1"
130
+ },
9
131
  {
10
132
  "id": "documentation-honesty",
11
133
  "pillar": "webhooks",
@@ -36,7 +158,7 @@
36
158
  ],
37
159
  "registeredAt": "2026-05-26",
38
160
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 — phase 7.1",
39
- "bundledIn": "@cross-deck/node@1.5.0"
161
+ "bundledIn": "@cross-deck/node@1.5.1"
40
162
  },
41
163
  {
42
164
  "id": "error-envelope-shape",
@@ -75,7 +197,7 @@
75
197
  ],
76
198
  "registeredAt": "2026-05-26",
77
199
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 — phase 8 (codifies existing contract)",
78
- "bundledIn": "@cross-deck/node@1.5.0"
200
+ "bundledIn": "@cross-deck/node@1.5.1"
79
201
  },
80
202
  {
81
203
  "id": "flush-interval-parity",
@@ -120,7 +242,7 @@
120
242
  ],
121
243
  "registeredAt": "2026-05-26",
122
244
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 — phase 3.3",
123
- "bundledIn": "@cross-deck/node@1.5.0"
245
+ "bundledIn": "@cross-deck/node@1.5.1"
124
246
  },
125
247
  {
126
248
  "id": "idempotency-key-deterministic",
@@ -225,7 +347,7 @@
225
347
  ],
226
348
  "registeredAt": "2026-05-26",
227
349
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 — phase 2.2.a + 2.2.b + 2.2.c",
228
- "bundledIn": "@cross-deck/node@1.5.0"
350
+ "bundledIn": "@cross-deck/node@1.5.1"
229
351
  },
230
352
  {
231
353
  "id": "node-pii-scrubber",
@@ -264,7 +386,7 @@
264
386
  ],
265
387
  "registeredAt": "2026-05-26",
266
388
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 — phase 3.1",
267
- "bundledIn": "@cross-deck/node@1.5.0"
389
+ "bundledIn": "@cross-deck/node@1.5.1"
268
390
  },
269
391
  {
270
392
  "id": "node-shutdown-awaits-flush",
@@ -297,7 +419,7 @@
297
419
  ],
298
420
  "registeredAt": "2026-05-26",
299
421
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 — phase 5.4",
300
- "bundledIn": "@cross-deck/node@1.5.0"
422
+ "bundledIn": "@cross-deck/node@1.5.1"
301
423
  },
302
424
  {
303
425
  "id": "sdk-error-codes-catalogue",
@@ -337,7 +459,7 @@
337
459
  ],
338
460
  "registeredAt": "2026-05-26",
339
461
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 — phase 6.2",
340
- "bundledIn": "@cross-deck/node@1.5.0"
462
+ "bundledIn": "@cross-deck/node@1.5.1"
341
463
  },
342
464
  {
343
465
  "id": "sync-purchases-funnel-parity",
@@ -370,7 +492,7 @@
370
492
  ],
371
493
  "registeredAt": "2026-05-26",
372
494
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 — phase 3.5",
373
- "bundledIn": "@cross-deck/node@1.5.0"
495
+ "bundledIn": "@cross-deck/node@1.5.1"
374
496
  },
375
497
  {
376
498
  "id": "verifier-timestamp-mandatory",
@@ -424,7 +546,7 @@
424
546
  ],
425
547
  "registeredAt": "2026-05-26",
426
548
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 — phase 7.2",
427
- "bundledIn": "@cross-deck/node@1.5.0"
549
+ "bundledIn": "@cross-deck/node@1.5.1"
428
550
  }
429
551
  ]
430
552
  }
@@ -173,8 +173,8 @@ declare const CrossdeckContracts: {
173
173
  readonly byId: (id: string) => Contract | undefined;
174
174
  readonly byPillar: (pillar: ContractPillar) => readonly Contract[];
175
175
  readonly withStatus: (status: ContractStatus) => readonly Contract[];
176
- readonly sdkVersion: "1.5.0";
177
- readonly bundledIn: "@cross-deck/node@1.5.0";
176
+ readonly sdkVersion: "1.5.1";
177
+ readonly bundledIn: "@cross-deck/node@1.5.1";
178
178
  /**
179
179
  * Resolve a failing test back to the contract it exercises.
180
180
  * Used by test-framework hooks to find the contract id of a
@@ -185,12 +185,22 @@ declare const CrossdeckContracts: {
185
185
  };
186
186
  /**
187
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.
188
+ * the per-SDK shape exactly.
189
+ *
190
+ * SCHEMA-LOCK: this interface's field set is exhaustively named. No
191
+ * free-form `extra: Record<string, unknown>` — the schema-lock
192
+ * contract at
193
+ * `contracts/diagnostics/contract-failed-payload-schema-lock.json`
194
+ * forbids unbounded fields. Adding a field requires a PR that
195
+ * amends the contract first, then the public interface.
191
196
  */
192
197
  interface ContractFailureInput {
193
198
  contractId: string;
199
+ /**
200
+ * Short categorical-ish label — the SDK convention is to keep
201
+ * this under 128 chars and stable across runs (so dashboards can
202
+ * group). Never an end-user-supplied string.
203
+ */
194
204
  failureReason: string;
195
205
  runContext: "ci" | "dogfood" | "customer-app";
196
206
  runId: string;
@@ -198,7 +208,11 @@ interface ContractFailureInput {
198
208
  file: string;
199
209
  name: string;
200
210
  };
201
- extra?: Record<string, unknown>;
211
+ /**
212
+ * Optional coarse device class, e.g. "linux-server", "container",
213
+ * "lambda". A categorical bucket, not a host identifier.
214
+ */
215
+ deviceClass?: string;
202
216
  }
203
217
 
204
218
  /**
@@ -1466,11 +1480,14 @@ declare class CrossdeckServer extends EventEmitter {
1466
1480
  * auto-fill, the event would be rejected at queue enqueue.
1467
1481
  */
1468
1482
  /**
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.
1483
+ * Emit `crossdeck.contract_failed` to the Crossdeck reliability
1484
+ * endpoint single-fire, one-way, never visible in the customer's
1485
+ * dashboard. Goes over a dedicated HTTP path with the reliability
1486
+ * publishable key embedded at build time; the customer's track()
1487
+ * pipeline never carries `crossdeck.*` events. This is the
1488
+ * independent-controller flow described in Privacy Policy §6
1489
+ * ("Flow B"). The wire shape is fixed by the schema-lock contract
1490
+ * at `contracts/diagnostics/contract-failed-payload-schema-lock.json`.
1474
1491
  */
1475
1492
  reportContractFailure(input: ContractFailureInput): void;
1476
1493
  track(event: ServerEvent): void;
@@ -173,8 +173,8 @@ declare const CrossdeckContracts: {
173
173
  readonly byId: (id: string) => Contract | undefined;
174
174
  readonly byPillar: (pillar: ContractPillar) => readonly Contract[];
175
175
  readonly withStatus: (status: ContractStatus) => readonly Contract[];
176
- readonly sdkVersion: "1.5.0";
177
- readonly bundledIn: "@cross-deck/node@1.5.0";
176
+ readonly sdkVersion: "1.5.1";
177
+ readonly bundledIn: "@cross-deck/node@1.5.1";
178
178
  /**
179
179
  * Resolve a failing test back to the contract it exercises.
180
180
  * Used by test-framework hooks to find the contract id of a
@@ -185,12 +185,22 @@ declare const CrossdeckContracts: {
185
185
  };
186
186
  /**
187
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.
188
+ * the per-SDK shape exactly.
189
+ *
190
+ * SCHEMA-LOCK: this interface's field set is exhaustively named. No
191
+ * free-form `extra: Record<string, unknown>` — the schema-lock
192
+ * contract at
193
+ * `contracts/diagnostics/contract-failed-payload-schema-lock.json`
194
+ * forbids unbounded fields. Adding a field requires a PR that
195
+ * amends the contract first, then the public interface.
191
196
  */
192
197
  interface ContractFailureInput {
193
198
  contractId: string;
199
+ /**
200
+ * Short categorical-ish label — the SDK convention is to keep
201
+ * this under 128 chars and stable across runs (so dashboards can
202
+ * group). Never an end-user-supplied string.
203
+ */
194
204
  failureReason: string;
195
205
  runContext: "ci" | "dogfood" | "customer-app";
196
206
  runId: string;
@@ -198,7 +208,11 @@ interface ContractFailureInput {
198
208
  file: string;
199
209
  name: string;
200
210
  };
201
- extra?: Record<string, unknown>;
211
+ /**
212
+ * Optional coarse device class, e.g. "linux-server", "container",
213
+ * "lambda". A categorical bucket, not a host identifier.
214
+ */
215
+ deviceClass?: string;
202
216
  }
203
217
 
204
218
  /**
@@ -1466,11 +1480,14 @@ declare class CrossdeckServer extends EventEmitter {
1466
1480
  * auto-fill, the event would be rejected at queue enqueue.
1467
1481
  */
1468
1482
  /**
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.
1483
+ * Emit `crossdeck.contract_failed` to the Crossdeck reliability
1484
+ * endpoint single-fire, one-way, never visible in the customer's
1485
+ * dashboard. Goes over a dedicated HTTP path with the reliability
1486
+ * publishable key embedded at build time; the customer's track()
1487
+ * pipeline never carries `crossdeck.*` events. This is the
1488
+ * independent-controller flow described in Privacy Policy §6
1489
+ * ("Flow B"). The wire shape is fixed by the schema-lock contract
1490
+ * at `contracts/diagnostics/contract-failed-payload-schema-lock.json`.
1474
1491
  */
1475
1492
  reportContractFailure(input: ContractFailureInput): void;
1476
1493
  track(event: ServerEvent): void;