@cross-deck/node 1.3.1 → 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,107 @@ 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
+
60
+ ## [1.4.2] — 2026-05-26
61
+
62
+ Patch — fix `tests/shutdown-flush.test.ts` compile error under
63
+ strict tsc. The five `s.track("name", { props })` calls used the
64
+ web/RN positional-args shape; Node SDK's track takes a single
65
+ `ServerEvent` object. Switched to `s.track({ name, properties })`.
66
+ Plus a non-null assertion on `sent[0].length` for
67
+ `noUncheckedIndexedAccess`. v1.4.1 was tagged on the public
68
+ crossdeck-node repo but its publish workflow aborted on these
69
+ errors. v1.4.2 is the first 1.4.x line to land on the npm
70
+ registry. **No SDK code changes vs v1.4.0 / v1.4.1**.
71
+
72
+ ## [1.4.1] — 2026-05-26
73
+
74
+ Patch — add automated npm publish workflow to the public
75
+ `crossdeck-node` repo so future `vX.Y.Z` tag pushes auto-publish
76
+ to npm via OIDC Trusted Publishing (matches the existing
77
+ `crossdeck-web` pattern). Also strips `test:e2e` from
78
+ `prepublishOnly` — the publish workflow runs lint + unit tests +
79
+ build which covers the release gate. No SDK code changes vs
80
+ v1.4.0.
81
+
82
+ **Operator note:** npmjs.com Trusted Publisher rule must be
83
+ configured for `crossdeck-node` (owner: VistaApps-za,
84
+ workflow: publish.yml) before the OIDC publish succeeds. First
85
+ publish after this lands will fail with an auth error if the
86
+ rule is missing — that's the prompt to configure it.
87
+
88
+ ## [1.4.0] — 2026-05-26
89
+
90
+ **Bank-grade reconciliation release.** 6-pillar KPMG-style audit closed across SDK + backend. Every behavioural guarantee registered in the monorepo's `contracts/` directory with a CI-enforced audit job.
91
+
92
+ ### Added
93
+
94
+ - **PII scrubber applied on `track()` enqueue path** — parity with Web/RN/Swift. Pre-1.4.0 Node was the ONLY SDK that skipped this, shipping payloads UNREDACTED. New `scrubPii?: boolean` option (default true); explicit false opt-out preserves raw payloads for regulator-required audit trails.
95
+ - **Deterministic `Idempotency-Key` on `syncPurchases()`** — same JWS/purchaseToken → same key. New `options.idempotencyKey` override for outer orchestrators.
96
+ - **`PurchaseResult.idempotent_replay?: boolean`** — true when the backend replayed a cached response.
97
+ - **`purchase.completed` event on every successful `syncPurchases()`** — funnel parity with Swift/Android auto-track.
98
+ - **Distinguishable webhook verifier error codes** — pre-1.4.0 collapsed everything into `webhook_invalid_signature`. New: `webhook_signature_mismatch` (wrong-secret signal), `webhook_timestamp_outside_tolerance` (replay-attack signal — alert separately), `webhook_timestamp_missing`, `webhook_payload_not_json`, `webhook_invalid_tolerance`. Legacy codes deprecated with migration notes.
99
+ - **Webhook verifier rejects footgun tolerances** — `Infinity` / `NaN` / negative / above-24h-cap now throw `webhook_invalid_tolerance` instead of silently disabling replay protection.
100
+ - **15 backend-emitted error codes** added to the `crossdeck-error-codes.json` catalogue with Stripe-style remediation guidance.
101
+
102
+ ### Changed (breaking)
103
+
104
+ - **`shutdown()` signature changed from `(reason) => void` to `(reason) => Promise<void>`.** Awaits `flush()` before tearing down the queue. Pre-1.4.0 it called `eventQueue.reset()` synchronously — every event between the last flush and shutdown was silently dropped. New `shutdownSync()` for callers that genuinely cannot await (signal handlers); it logs `console.warn` with the dropped-event count if the buffer is non-empty.
105
+ - **Default event-queue flush interval is now 2000ms** (was 1500ms) — cross-SDK parity.
106
+ - **`[Symbol.dispose]` now warns when dropping queued events.** Use `await using` + `[Symbol.asyncDispose]` (or `await server.shutdown()`) for proper drainage.
107
+
7
108
  ## [1.3.1] — 2026-05-24
8
109
 
9
110
  Patch fix for the 1.3.0 dist-load contract. Mirrors the
package/README.md CHANGED
@@ -397,6 +397,14 @@ new CrossdeckServer({
397
397
 
398
398
  POST methods (`track`/`ingest`/`syncPurchases`/`grantEntitlement`/`revokeEntitlement`) DO NOT auto-retry at the HTTP layer. Retries happen via the event queue with per-batch `Idempotency-Key` reuse — the server can dedupe replays.
399
399
 
400
+ **v1.4.0 — `syncPurchases` deterministic key.** The Idempotency-Key
401
+ on `syncPurchases` is derived from the request body (UUID-shaped
402
+ SHA-256 of `crossdeck:purchases/sync:<rail>:<jws|token>`). Two retries
403
+ of the same Apple transaction land on the same key, so the backend
404
+ short-circuits with `idempotent_replay: true` instead of
405
+ double-processing. Override via `options.idempotencyKey` only when
406
+ an outer orchestrator needs a different idempotency window.
407
+
400
408
  ### AbortSignal — caller-controlled cancellation
401
409
 
402
410
  Every async method accepts a final `RequestOptions?` with `{ signal, timeoutMs }`:
@@ -414,6 +422,60 @@ try {
414
422
  }
415
423
  ```
416
424
 
425
+ ### PII scrubber (v1.4.0 — parity with Web/RN/Swift)
426
+
427
+ Every `track()` payload runs through `scrubPiiFromProperties`
428
+ before enqueue — email-shaped and card-number-shaped substrings
429
+ are rewritten to `<email>` / `<card>` sentinels recursively
430
+ across nested objects + arrays. **Default: on.** Pre-v1.4.0 the
431
+ Node SDK was the only one that skipped this, shipping payloads
432
+ UNREDACTED despite the README promising parity.
433
+
434
+ Opt out only for regulator-required audit trails where the raw
435
+ value must be preserved:
436
+
437
+ ```ts
438
+ new CrossdeckServer({ secretKey, scrubPii: false });
439
+ ```
440
+
441
+ **Blast radius:** every `track()` payload — event names with
442
+ embedded emails, trait values, group memberships, error context
443
+ blobs — ships verbatim to Crossdeck and downstream warehouses /
444
+ analytics exports. Document the decision at the call site.
445
+
446
+ ### Shutdown — flush before exit (v1.4.0 contract)
447
+
448
+ The server holds a buffered event queue. A clean teardown MUST
449
+ flush the buffer before dropping it, otherwise events queued
450
+ between the last flush and shutdown are silently lost.
451
+
452
+ **Three teardown paths, three contracts:**
453
+
454
+ | Method | Flushes? | Use when |
455
+ | ------ | -------- | -------- |
456
+ | `await server.shutdown()` | YES — awaits internal `flush()` then tears down | Default. Use this in graceful-shutdown handlers. |
457
+ | `await using server = ...` + `[Symbol.asyncDispose]` | YES — equivalent to `await server.shutdown()` | TC39 explicit-resource-management blocks. |
458
+ | `server.shutdownSync()` / `using` + `[Symbol.dispose]` | NO — drops the buffer | ONLY when the runtime cannot await (signal handlers, process.exit fallthrough). |
459
+
460
+ ```ts
461
+ // Graceful shutdown (recommended)
462
+ process.on("SIGTERM", async () => {
463
+ await server.shutdown();
464
+ process.exit(0);
465
+ });
466
+
467
+ // Explicit-resource-management (Node 20+ / TS 5.2+)
468
+ {
469
+ await using server = new CrossdeckServer({ secretKey });
470
+ // ... use server ...
471
+ } // [Symbol.asyncDispose] fires here, awaits flush
472
+ ```
473
+
474
+ `shutdownSync()` (and the sync `[Symbol.dispose]` that wraps it)
475
+ logs a `console.warn` with the dropped-event count whenever the
476
+ buffer is non-empty at sync-teardown time — silent loss is
477
+ incompatible with the bank-grade contract.
478
+
417
479
  ### EventEmitter — internal events
418
480
 
419
481
  `CrossdeckServer extends EventEmitter`. Subscribe to internal lifecycle events with typed listeners:
@@ -507,6 +569,64 @@ const failed = results.filter((r) => !r.ok);
507
569
 
508
570
  Symmetric `bulkRevokeEntitlement(revokes[], options?)`.
509
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
+
510
630
  ## Node version
511
631
 
512
632
  Node 18+. Uses the platform `fetch` and `node:crypto` — zero runtime dependencies.
@@ -1,4 +1,4 @@
1
- import { p as CrossdeckServer } from '../crossdeck-server-DhnHvUhh.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 { p as CrossdeckServer } from '../crossdeck-server-DhnHvUhh.js';
1
+ import { w as CrossdeckServer } from '../crossdeck-server-CY4PZk-j.js';
2
2
  import 'node:events';
3
3
 
4
4
  /**