@cross-deck/react-native 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,57 @@ All notable changes to `@cross-deck/react-native` will be documented
4
4
  here. The 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/react-native@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
+ - **`Crossdeck.reportContractFailure(input)` method** — fires a typed
34
+ `crossdeck.contract_failed` event through the standard `track()`
35
+ pipeline when a contract test asserts and fails. Wire properties:
36
+ `contract_id`, `sdk_version` (auto-stamped), `sdk_platform`
37
+ (auto-stamped to `"react-native"`), `failure_reason`, `run_context`
38
+ (`ci` | `dogfood` | `customer-app`), `run_id`, plus optional
39
+ `test_file` / `test_name`.
40
+
41
+ **Fixed:**
42
+
43
+ - Pre-hydration `track()` calls now correctly snapshot the
44
+ call-time `sessionId` and thread it through the deferred
45
+ enrichment body. Previously, two `track()` calls separated by
46
+ `setSessionId(...)` BEFORE hydration completed would both pick up
47
+ whatever `sessionId` was current at hydration resolution — silently
48
+ rewriting the first event with the second event's state. This
49
+ contract is RN-specific (Web/Node have no hydration window).
50
+
51
+ **Changed:**
52
+
53
+ - Contract registry source files migrated to camelCase keys
54
+ (`appliesTo`, `codeRef`, `testRef`, `registeredAt`,
55
+ `firstRegisteredIn`). The bundled `contracts.json` sidecar uses
56
+ the new keys; `bundledIn` is build-stamped, never in source.
57
+
7
58
  ## [1.4.2] — 2026-05-26
8
59
 
9
60
  Patch — wire `bundleId` + `packageName` (per-platform identity-
package/README.md CHANGED
@@ -157,6 +157,60 @@ Crossdeck.diagnostics();
157
157
  // }
158
158
  ```
159
159
 
160
+ ## Bank-grade contracts
161
+
162
+ 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.
163
+
164
+ ### `CrossdeckContracts` — typed access to the bundled registry
165
+
166
+ ```ts
167
+ import { CrossdeckContracts } from "@cross-deck/react-native";
168
+
169
+ CrossdeckContracts.all(); // enforced contracts only
170
+ CrossdeckContracts.allIncludingHistorical(); // + proposed + retired
171
+ CrossdeckContracts.byId("per-user-cache-isolation");
172
+ CrossdeckContracts.byPillar("entitlements");
173
+ CrossdeckContracts.withStatus("proposed");
174
+ CrossdeckContracts.findByTestName("identify(B) makes A's entitlements unreachable from in-memory");
175
+ CrossdeckContracts.sdkVersion; // "1.5.0"
176
+ CrossdeckContracts.bundledIn; // "@cross-deck/react-native@1.5.0"
177
+ ```
178
+
179
+ 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).
180
+
181
+ ### `Crossdeck.reportContractFailure(input)` — surface contract test failures
182
+
183
+ 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):
184
+
185
+ ```ts
186
+ Crossdeck.reportContractFailure({
187
+ contractId: "per-user-cache-isolation",
188
+ failureReason: "expected isolation across user switch, got cross-read",
189
+ runContext: __DEV__ ? "dogfood" : "ci",
190
+ runId: process.env.GITHUB_RUN_ID ?? Date.now().toString(36),
191
+ testRef: {
192
+ file: "tests/entitlement-cache-isolation.test.ts",
193
+ name: "identify(B) makes A's entitlements unreachable from in-memory",
194
+ },
195
+ });
196
+ ```
197
+
198
+ No new endpoint, no special ingest path — the event lands in the same pipeline every other `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`.
199
+
200
+ Properties stamped on the wire:
201
+
202
+ | Property | Source |
203
+ |----------|--------|
204
+ | `contract_id` | caller |
205
+ | `sdk_version`, `sdk_platform` | auto-stamped (`@cross-deck/react-native` ships `sdk_platform: "react-native"`) |
206
+ | `failure_reason`, `run_context`, `run_id` | caller |
207
+ | `test_file`, `test_name` | set when `testRef` is provided |
208
+ | `device_class` | optional, set by caller (categorical bucket — e.g. `"ios-phone"`, `"android-tablet"`) |
209
+
210
+ 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.
211
+
212
+ 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).
213
+
160
214
  ## Documentation
161
215
 
162
216
  - [Full SDK reference](https://cross-deck.com/docs/react-native-sdk)
@@ -1,11 +1,133 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "generatedAt": "2026-05-26T13:09:42.134Z",
3
+ "generatedAt": "2026-05-27T10:40:46.099Z",
4
4
  "sdk": "@cross-deck/react-native",
5
- "sdkVersion": "1.5.0",
6
- "bundledIn": "@cross-deck/react-native@1.5.0",
7
- "count": 7,
5
+ "sdkVersion": "1.5.1",
6
+ "bundledIn": "@cross-deck/react-native@1.5.1",
7
+ "count": 8,
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/react-native@1.5.1"
130
+ },
9
131
  {
10
132
  "id": "error-envelope-shape",
11
133
  "pillar": "errors",
@@ -43,7 +165,7 @@
43
165
  ],
44
166
  "registeredAt": "2026-05-26",
45
167
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 — phase 8 (codifies existing contract)",
46
- "bundledIn": "@cross-deck/react-native@1.5.0"
168
+ "bundledIn": "@cross-deck/react-native@1.5.1"
47
169
  },
48
170
  {
49
171
  "id": "flush-interval-parity",
@@ -88,7 +210,7 @@
88
210
  ],
89
211
  "registeredAt": "2026-05-26",
90
212
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 — phase 3.3",
91
- "bundledIn": "@cross-deck/react-native@1.5.0"
213
+ "bundledIn": "@cross-deck/react-native@1.5.1"
92
214
  },
93
215
  {
94
216
  "id": "idempotency-key-deterministic",
@@ -193,7 +315,7 @@
193
315
  ],
194
316
  "registeredAt": "2026-05-26",
195
317
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 — phase 2.2.a + 2.2.b + 2.2.c",
196
- "bundledIn": "@cross-deck/react-native@1.5.0"
318
+ "bundledIn": "@cross-deck/react-native@1.5.1"
197
319
  },
198
320
  {
199
321
  "id": "init-reentry-drains-prior-queue",
@@ -220,7 +342,7 @@
220
342
  ],
221
343
  "registeredAt": "2026-05-26",
222
344
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 — phase 5.5",
223
- "bundledIn": "@cross-deck/react-native@1.5.0"
345
+ "bundledIn": "@cross-deck/react-native@1.5.1"
224
346
  },
225
347
  {
226
348
  "id": "per-user-cache-isolation",
@@ -299,7 +421,7 @@
299
421
  ],
300
422
  "registeredAt": "2026-05-26",
301
423
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 — phase 1.3 (web/RN) + dogfood-gap fix (swift + android)",
302
- "bundledIn": "@cross-deck/react-native@1.5.0"
424
+ "bundledIn": "@cross-deck/react-native@1.5.1"
303
425
  },
304
426
  {
305
427
  "id": "rn-session-id-enrichment",
@@ -332,7 +454,7 @@
332
454
  ],
333
455
  "registeredAt": "2026-05-26",
334
456
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 — phase 3.4",
335
- "bundledIn": "@cross-deck/react-native@1.5.0"
457
+ "bundledIn": "@cross-deck/react-native@1.5.1"
336
458
  },
337
459
  {
338
460
  "id": "sync-purchases-funnel-parity",
@@ -365,7 +487,7 @@
365
487
  ],
366
488
  "registeredAt": "2026-05-26",
367
489
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 — phase 3.5",
368
- "bundledIn": "@cross-deck/react-native@1.5.0"
490
+ "bundledIn": "@cross-deck/react-native@1.5.1"
369
491
  }
370
492
  ]
371
493
  }
package/dist/index.cjs CHANGED
@@ -101,7 +101,7 @@ function typeMapForStatus(status) {
101
101
  }
102
102
 
103
103
  // src/_version.ts
104
- var SDK_VERSION = "1.4.2";
104
+ var SDK_VERSION = "1.5.1";
105
105
  var SDK_NAME = "@cross-deck/react-native";
106
106
 
107
107
  // src/http.ts
@@ -1722,6 +1722,56 @@ var BreadcrumbBuffer = class {
1722
1722
  }
1723
1723
  };
1724
1724
 
1725
+ // src/_diagnostic-telemetry.ts
1726
+ var DIAGNOSTIC_TELEMETRY_ENDPOINT = "https://api.cross-deck.com/v1/sdk/diagnostic";
1727
+ var DIAGNOSTIC_TELEMETRY_PUBLISHABLE_KEY = "cd_pub_RELIABILITY_PLACEHOLDER_TO_BE_PROVISIONED";
1728
+ function isDiagnosticTelemetryEnabled() {
1729
+ return !DIAGNOSTIC_TELEMETRY_PUBLISHABLE_KEY.startsWith(
1730
+ "cd_pub_RELIABILITY_PLACEHOLDER"
1731
+ );
1732
+ }
1733
+ var DIAGNOSTIC_TELEMETRY_ALLOWED_KEYS = /* @__PURE__ */ new Set([
1734
+ "contract_id",
1735
+ "sdk_version",
1736
+ "sdk_platform",
1737
+ "failure_reason",
1738
+ "run_context",
1739
+ "run_id",
1740
+ "test_file",
1741
+ "test_name",
1742
+ "device_class"
1743
+ ]);
1744
+ function filterDiagnosticPayload(payload) {
1745
+ const filtered = {};
1746
+ for (const [k, v] of Object.entries(payload)) {
1747
+ if (DIAGNOSTIC_TELEMETRY_ALLOWED_KEYS.has(k) && typeof v === "string") {
1748
+ filtered[k] = v;
1749
+ }
1750
+ }
1751
+ return filtered;
1752
+ }
1753
+ function sendDiagnosticTelemetry(payload) {
1754
+ if (!isDiagnosticTelemetryEnabled()) return;
1755
+ const filtered = filterDiagnosticPayload(payload);
1756
+ if (Object.keys(filtered).length === 0) return;
1757
+ const body = JSON.stringify(filtered);
1758
+ const f = globalThis.fetch;
1759
+ if (typeof f !== "function") return;
1760
+ try {
1761
+ void f(DIAGNOSTIC_TELEMETRY_ENDPOINT, {
1762
+ method: "POST",
1763
+ headers: {
1764
+ "Content-Type": "application/json",
1765
+ Authorization: `Bearer ${DIAGNOSTIC_TELEMETRY_PUBLISHABLE_KEY}`,
1766
+ "Crossdeck-Sdk-Version": `${SDK_NAME}@${SDK_VERSION}`
1767
+ },
1768
+ body
1769
+ }).catch(() => {
1770
+ });
1771
+ } catch {
1772
+ }
1773
+ }
1774
+
1725
1775
  // src/stack-parser.ts
1726
1776
  function parseStack(stack) {
1727
1777
  if (!stack || typeof stack !== "string") return [];
@@ -2654,13 +2704,17 @@ var CrossdeckClient = class {
2654
2704
  * synchronously.
2655
2705
  */
2656
2706
  /**
2657
- * Emit `crossdeck.contract_failed` with the canonical property
2658
- * shape. Same wire shape every Crossdeck SDK uses for contract
2659
- * verification telemetry see `contracts/README.md`. No new
2660
- * endpoint, goes through the standard track() pipeline.
2707
+ * Emit `crossdeck.contract_failed` to the Crossdeck reliability
2708
+ * endpoint single-fire, one-way, never visible in the customer's
2709
+ * dashboard. Goes over a dedicated HTTP path with the reliability
2710
+ * publishable key embedded at build time; the customer's track()
2711
+ * pipeline never carries `crossdeck.*` events. This is the
2712
+ * independent-controller flow described in Privacy Policy §6
2713
+ * ("Flow B"). The wire shape is fixed by the schema-lock contract
2714
+ * at `contracts/diagnostics/contract-failed-payload-schema-lock.json`.
2661
2715
  */
2662
2716
  reportContractFailure(input) {
2663
- const props = {
2717
+ const payload = {
2664
2718
  contract_id: input.contractId,
2665
2719
  sdk_version: SDK_VERSION,
2666
2720
  sdk_platform: "react-native",
@@ -2669,15 +2723,13 @@ var CrossdeckClient = class {
2669
2723
  run_id: input.runId
2670
2724
  };
2671
2725
  if (input.testRef) {
2672
- props.test_file = input.testRef.file;
2673
- props.test_name = input.testRef.name;
2726
+ payload.test_file = input.testRef.file;
2727
+ payload.test_name = input.testRef.name;
2674
2728
  }
2675
- if (input.extra) {
2676
- for (const [k, v] of Object.entries(input.extra)) {
2677
- if (props[k] === void 0) props[k] = v;
2678
- }
2729
+ if (input.deviceClass) {
2730
+ payload.device_class = input.deviceClass;
2679
2731
  }
2680
- this.track("crossdeck.contract_failed", props);
2732
+ sendDiagnosticTelemetry(payload);
2681
2733
  }
2682
2734
  track(name, properties) {
2683
2735
  const s = this.requireStarted();
@@ -3033,9 +3085,131 @@ function detectPlatform() {
3033
3085
  }
3034
3086
 
3035
3087
  // src/_contracts-bundled.ts
3036
- var BUNDLED_IN = "@cross-deck/react-native@1.5.0";
3037
- var SDK_VERSION2 = "1.5.0";
3088
+ var BUNDLED_IN = "@cross-deck/react-native@1.5.1";
3089
+ var SDK_VERSION2 = "1.5.1";
3038
3090
  var BUNDLED_CONTRACTS = Object.freeze([
3091
+ {
3092
+ "id": "contract-failed-payload-schema-lock",
3093
+ "pillar": "diagnostics",
3094
+ "status": "enforced",
3095
+ "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 \u2014 adding a new field requires (1) a pull request that updates this contract's `allowedFields` set, (2) a Privacy Policy \xA76 amendment, and (3) the Customer Disclosure Template / SDK Data Collection Reference \xA7B updates. Per-SDK assertion tests enforce the field set on every release. The `verification_phase` field is a categorical bucket \u2014 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.",
3096
+ "appliesTo": [
3097
+ "web",
3098
+ "node",
3099
+ "swift",
3100
+ "android",
3101
+ "react-native"
3102
+ ],
3103
+ "allowedFields": {
3104
+ "required": [
3105
+ "contract_id",
3106
+ "sdk_version",
3107
+ "sdk_platform",
3108
+ "failure_reason",
3109
+ "run_context",
3110
+ "run_id"
3111
+ ],
3112
+ "optional": [
3113
+ "test_file",
3114
+ "test_name",
3115
+ "device_class",
3116
+ "verification_phase"
3117
+ ],
3118
+ "forbidden": [
3119
+ "anonymousId",
3120
+ "developerUserId",
3121
+ "crossdeckCustomerId",
3122
+ "email",
3123
+ "ip",
3124
+ "user_agent",
3125
+ "message",
3126
+ "stack",
3127
+ "stack_trace",
3128
+ "frames",
3129
+ "exception_message",
3130
+ "url",
3131
+ "path",
3132
+ "screen",
3133
+ "title",
3134
+ "label",
3135
+ "text",
3136
+ "ariaLabel",
3137
+ "accessibilityLabel",
3138
+ "contentDescription",
3139
+ "session_id",
3140
+ "sessionId"
3141
+ ]
3142
+ },
3143
+ "transport": "Telemetry is single-fire to the Crossdeck reliability endpoint only \u2014 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 \xA76). 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.",
3144
+ "codeRef": [
3145
+ "sdks/web/src/crossdeck.ts",
3146
+ "sdks/node/src/crossdeck-server.ts",
3147
+ "sdks/swift/Sources/Crossdeck/Crossdeck.swift",
3148
+ "sdks/swift/Sources/Crossdeck/_DiagnosticTelemetry.swift",
3149
+ "sdks/android/crossdeck/src/main/kotlin/com/crossdeck/Crossdeck.kt",
3150
+ "sdks/android/crossdeck/src/main/kotlin/com/crossdeck/_DiagnosticTelemetry.kt",
3151
+ "sdks/react-native/src/crossdeck.ts",
3152
+ "backend/src/api/v1-sdk-diagnostic.ts",
3153
+ "sdks/web/src/_diagnostic-telemetry.ts",
3154
+ "sdks/node/src/_diagnostic-telemetry.ts",
3155
+ "sdks/react-native/src/_diagnostic-telemetry.ts"
3156
+ ],
3157
+ "testRef": [
3158
+ {
3159
+ "file": "sdks/web/tests/contract-failed-schema-lock.test.ts",
3160
+ "name": "reportContractFailure payload conforms to schema-lock"
3161
+ },
3162
+ {
3163
+ "file": "sdks/node/tests/contract-failed-schema-lock.test.ts",
3164
+ "name": "reportContractFailure payload conforms to schema-lock"
3165
+ },
3166
+ {
3167
+ "file": "sdks/swift/Tests/CrossdeckTests/ContractFailedSchemaLockTests.swift",
3168
+ "name": "test_reportContractFailure_payloadFieldsAreInAllowList"
3169
+ },
3170
+ {
3171
+ "file": "sdks/swift/Tests/CrossdeckTests/ContractFailedSchemaLockTests.swift",
3172
+ "name": "test_reportContractFailure_doesNotEnterCustomerTrackPipeline"
3173
+ },
3174
+ {
3175
+ "file": "sdks/android/crossdeck/src/test/kotlin/com/crossdeck/ContractFailedSchemaLockTest.kt",
3176
+ "name": "reportContractFailure payload conforms to schema-lock"
3177
+ },
3178
+ {
3179
+ "file": "sdks/android/crossdeck/src/test/kotlin/com/crossdeck/ContractFailedSchemaLockTest.kt",
3180
+ "name": "reportContractFailure does not enter customer track pipeline"
3181
+ },
3182
+ {
3183
+ "file": "sdks/react-native/tests/contract-failed-schema-lock.test.ts",
3184
+ "name": "reportContractFailure payload conforms to schema-lock"
3185
+ },
3186
+ {
3187
+ "file": "backend/tests/unit/v1-sdk-diagnostic.test.ts",
3188
+ "name": "forbidden fields are enumerated in the schema-lock contract"
3189
+ },
3190
+ {
3191
+ "file": "backend/tests/unit/v1-sdk-diagnostic.test.ts",
3192
+ "name": "required fields are enumerated in the schema-lock contract"
3193
+ },
3194
+ {
3195
+ "file": "backend/tests/unit/v1-sdk-diagnostic.test.ts",
3196
+ "name": "regression guard: never returns a raw IP"
3197
+ },
3198
+ {
3199
+ "file": "backend/tests/unit/v1-sdk-diagnostic.test.ts",
3200
+ "name": "verification_phase is in the optional field set"
3201
+ }
3202
+ ],
3203
+ "registeredAt": "2026-05-27",
3204
+ "firstRegisteredIn": "Diagnostic telemetry single-fire + schema-lock \u2014 independent-controller flow",
3205
+ "privacyReferences": [
3206
+ "legal/privacy/index.html#sdk-diagnostic",
3207
+ "legal/customer-disclosure/index.html#flow-b",
3208
+ "legal/security/index.html#diagnostic",
3209
+ "legal/sdk-data/index.html#b-diagnostic"
3210
+ ],
3211
+ "bundledIn": "@cross-deck/react-native@1.5.1"
3212
+ },
3039
3213
  {
3040
3214
  "id": "error-envelope-shape",
3041
3215
  "pillar": "errors",
@@ -3073,7 +3247,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
3073
3247
  ],
3074
3248
  "registeredAt": "2026-05-26",
3075
3249
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 8 (codifies existing contract)",
3076
- "bundledIn": "@cross-deck/react-native@1.5.0"
3250
+ "bundledIn": "@cross-deck/react-native@1.5.1"
3077
3251
  },
3078
3252
  {
3079
3253
  "id": "flush-interval-parity",
@@ -3118,7 +3292,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
3118
3292
  ],
3119
3293
  "registeredAt": "2026-05-26",
3120
3294
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 3.3",
3121
- "bundledIn": "@cross-deck/react-native@1.5.0"
3295
+ "bundledIn": "@cross-deck/react-native@1.5.1"
3122
3296
  },
3123
3297
  {
3124
3298
  "id": "idempotency-key-deterministic",
@@ -3223,7 +3397,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
3223
3397
  ],
3224
3398
  "registeredAt": "2026-05-26",
3225
3399
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 2.2.a + 2.2.b + 2.2.c",
3226
- "bundledIn": "@cross-deck/react-native@1.5.0"
3400
+ "bundledIn": "@cross-deck/react-native@1.5.1"
3227
3401
  },
3228
3402
  {
3229
3403
  "id": "init-reentry-drains-prior-queue",
@@ -3250,7 +3424,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
3250
3424
  ],
3251
3425
  "registeredAt": "2026-05-26",
3252
3426
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 5.5",
3253
- "bundledIn": "@cross-deck/react-native@1.5.0"
3427
+ "bundledIn": "@cross-deck/react-native@1.5.1"
3254
3428
  },
3255
3429
  {
3256
3430
  "id": "per-user-cache-isolation",
@@ -3329,7 +3503,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
3329
3503
  ],
3330
3504
  "registeredAt": "2026-05-26",
3331
3505
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 1.3 (web/RN) + dogfood-gap fix (swift + android)",
3332
- "bundledIn": "@cross-deck/react-native@1.5.0"
3506
+ "bundledIn": "@cross-deck/react-native@1.5.1"
3333
3507
  },
3334
3508
  {
3335
3509
  "id": "rn-session-id-enrichment",
@@ -3362,7 +3536,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
3362
3536
  ],
3363
3537
  "registeredAt": "2026-05-26",
3364
3538
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 3.4",
3365
- "bundledIn": "@cross-deck/react-native@1.5.0"
3539
+ "bundledIn": "@cross-deck/react-native@1.5.1"
3366
3540
  },
3367
3541
  {
3368
3542
  "id": "sync-purchases-funnel-parity",
@@ -3395,7 +3569,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
3395
3569
  ],
3396
3570
  "registeredAt": "2026-05-26",
3397
3571
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 3.5",
3398
- "bundledIn": "@cross-deck/react-native@1.5.0"
3572
+ "bundledIn": "@cross-deck/react-native@1.5.1"
3399
3573
  }
3400
3574
  ]);
3401
3575