@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 +101 -0
- package/README.md +120 -0
- package/dist/auto-events/index.d.mts +1 -1
- package/dist/auto-events/index.d.ts +1 -1
- package/dist/contracts.json +552 -0
- package/dist/{crossdeck-server-DhnHvUhh.d.mts → crossdeck-server-CY4PZk-j.d.mts} +200 -12
- package/dist/{crossdeck-server-DhnHvUhh.d.ts → crossdeck-server-CY4PZk-j.d.ts} +200 -12
- package/dist/index.cjs +1020 -73
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +150 -21
- package/dist/index.d.ts +150 -21
- package/dist/index.mjs +1005 -69
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
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.
|