@cross-deck/node 1.2.0 → 1.5.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 CHANGED
@@ -4,6 +4,156 @@ 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.4.2] — 2026-05-26
8
+
9
+ Patch — fix `tests/shutdown-flush.test.ts` compile error under
10
+ strict tsc. The five `s.track("name", { props })` calls used the
11
+ web/RN positional-args shape; Node SDK's track takes a single
12
+ `ServerEvent` object. Switched to `s.track({ name, properties })`.
13
+ Plus a non-null assertion on `sent[0].length` for
14
+ `noUncheckedIndexedAccess`. v1.4.1 was tagged on the public
15
+ crossdeck-node repo but its publish workflow aborted on these
16
+ errors. v1.4.2 is the first 1.4.x line to land on the npm
17
+ registry. **No SDK code changes vs v1.4.0 / v1.4.1**.
18
+
19
+ ## [1.4.1] — 2026-05-26
20
+
21
+ Patch — add automated npm publish workflow to the public
22
+ `crossdeck-node` repo so future `vX.Y.Z` tag pushes auto-publish
23
+ to npm via OIDC Trusted Publishing (matches the existing
24
+ `crossdeck-web` pattern). Also strips `test:e2e` from
25
+ `prepublishOnly` — the publish workflow runs lint + unit tests +
26
+ build which covers the release gate. No SDK code changes vs
27
+ v1.4.0.
28
+
29
+ **Operator note:** npmjs.com Trusted Publisher rule must be
30
+ configured for `crossdeck-node` (owner: VistaApps-za,
31
+ workflow: publish.yml) before the OIDC publish succeeds. First
32
+ publish after this lands will fail with an auth error if the
33
+ rule is missing — that's the prompt to configure it.
34
+
35
+ ## [1.4.0] — 2026-05-26
36
+
37
+ **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.
38
+
39
+ ### Added
40
+
41
+ - **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.
42
+ - **Deterministic `Idempotency-Key` on `syncPurchases()`** — same JWS/purchaseToken → same key. New `options.idempotencyKey` override for outer orchestrators.
43
+ - **`PurchaseResult.idempotent_replay?: boolean`** — true when the backend replayed a cached response.
44
+ - **`purchase.completed` event on every successful `syncPurchases()`** — funnel parity with Swift/Android auto-track.
45
+ - **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.
46
+ - **Webhook verifier rejects footgun tolerances** — `Infinity` / `NaN` / negative / above-24h-cap now throw `webhook_invalid_tolerance` instead of silently disabling replay protection.
47
+ - **15 backend-emitted error codes** added to the `crossdeck-error-codes.json` catalogue with Stripe-style remediation guidance.
48
+
49
+ ### Changed (breaking)
50
+
51
+ - **`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.
52
+ - **Default event-queue flush interval is now 2000ms** (was 1500ms) — cross-SDK parity.
53
+ - **`[Symbol.dispose]` now warns when dropping queued events.** Use `await using` + `[Symbol.asyncDispose]` (or `await server.shutdown()`) for proper drainage.
54
+
55
+ ## [1.3.1] — 2026-05-24
56
+
57
+ Patch fix for the 1.3.0 dist-load contract. Mirrors the
58
+ `@cross-deck/web@1.3.1` patch — `SDK_VERSION` is now sourced from a
59
+ generated `src/_version.ts` file (produced by
60
+ `scripts/sync-sdk-versions.mjs` from `package.json`) instead of a
61
+ runtime `import { version } from "../package.json"` that needs a
62
+ `with { type: "json" }` assertion to load as ESM. Wire contract is
63
+ unchanged. 1.3.0 was never published to npm; 1.3.1 is the first
64
+ 1.3.x line to reach npm.
65
+
66
+ ## [1.3.0] — 2026-05-24
67
+
68
+ KPMG bank-grade audit closure. Six review batches landed five SDK PRs
69
+ and a backend wiring fix that closes every P0 plus 12 of 13 P1 findings.
70
+ No public method renames; one internal contract change
71
+ (`ErrorTracker.beforeSend` is now a getter) that also removes the
72
+ `Object.defineProperty` workaround the node SDK shipped to compensate
73
+ for the same broken contract on web. Behavioural changes to the queue
74
+ and the PII scrub strictly improve correctness. The wire
75
+ `Crossdeck-Sdk-Version` header now reads from `package.json` so it
76
+ cannot drift from the published bundle.
77
+
78
+ ### Fixed (P0)
79
+
80
+ - **PII scrub sentinel tokens aligned with the backend.** `[email]` /
81
+ `[card]` → `<email>` / `<card>`, matching `backend/src/api/lib/scrub.ts`.
82
+ The same event scrubbed by SDK + backend now carries the same
83
+ sentinel — dashboard aggregation works again.
84
+ - **`setErrorBeforeSend` contract cleaned up.** The
85
+ `ErrorTracker.beforeSend` field is now a getter
86
+ (`() => fn | null`). Removed the `Object.defineProperty` hack on
87
+ `tracker.opts` that worked around the old captured-by-value bug —
88
+ cleaner contract, lockstep with web.
89
+ - **Event queue drops 4xx batches.** Pre-fix every `catch` triggered
90
+ `scheduleRetry` with the same `Idempotency-Key`. A 401 (key revoked),
91
+ 400/422 (malformed batch), 403 (permission), 404 (wrong baseUrl)
92
+ spun the retry timer indefinitely while the backlog grew silently.
93
+ New `isPermanent4xx()` helper hard-stops on any 4xx EXCEPT 408 / 429
94
+ (transient by spec). On permanent failure: drop the batch, increment
95
+ `dropped`, fire `onPermanentFailure(info)`, emit
96
+ `queue.permanent_failure` on the EventEmitter, log via
97
+ `console.error` regardless of debug mode.
98
+ - **Error-capture self-skip derived from `baseUrl`.** Pre-fix hardcoded
99
+ to `api.cross-deck.com`; customers on staging / regional / self-hosted
100
+ base URLs recursed (5xx → captureHttp → enqueue → /events →
101
+ captureHttp → ∞). Now strict-hostname compare against `selfHostname`
102
+ extracted from constructor `baseUrl`. Closes the substring-match
103
+ bypass (`api.cross-deck.com.attacker.example` would have matched).
104
+
105
+ ### Added
106
+
107
+ - **`onPermanentFailure` callback** on `EventQueueConfig`, surfaced
108
+ via `CrossdeckServer.on("queue.permanent_failure", …)` for host-app
109
+ paging.
110
+ - **`sdk.flush_permanent_failure` debug signal** in the
111
+ `DebugSignal` vocabulary.
112
+
113
+ ### Changed
114
+
115
+ - **`SDK_VERSION` is now imported from `package.json`.** The
116
+ `Crossdeck-Sdk-Version` header always matches the published bundle.
117
+ Single source of truth.
118
+ - **Event ingest envelope now ships `environment`.** Pre-fix web sent
119
+ it and node didn't; backend `v1-events.ts` cross-checks it against
120
+ the API-key-derived env and rejects mismatches loudly
121
+ (`env_mismatch`). Defence-in-depth so a "live key, env: sandbox"
122
+ misconfig fails fast instead of polluting the wrong dashboard.
123
+ - **`syncPurchases` body spread bug.** Pre-fix
124
+ `{ rail: input.rail ?? "apple", ...input }` — the `...input` ran
125
+ LAST and overrode the default when the caller passed
126
+ `rail: undefined` explicitly. Reversed: `{ ...input, rail }`.
127
+ - **PII scrub regex uses `.replace()` unconditionally.** Dropped the
128
+ `.test()`-gating that carried `lastIndex` state between calls.
129
+ - **`bootHeartbeat: false` no longer silences the
130
+ `sdk.no_durable_store` warning.** Pre-fix the warning lived inside
131
+ `emitBootTelemetry()` which sat inside the `bootHeartbeat` gate, so
132
+ the opt-out silenced the entire reason `entitlementStore` exists.
133
+ Split into two methods: `emitDurabilityWarning()` (local-only,
134
+ unconditional) and `emitBootTelemetryEvent()` (phone-home, still
135
+ gated).
136
+ - **`isEntitled(string)` requires the `cdcust_` prefix** for canonical-
137
+ path resolution. Pre-fix any string with a cache entry resolved
138
+ through the canonical path — a small cross-tenant primitive if a
139
+ tenant's userId collided with another tenant's `crossdeckCustomerId`.
140
+ Non-prefixed strings now drop to alias lookup only.
141
+ - **Self-skip applies to breadcrumbs too**, not just `captureHttp`.
142
+ Error reports no longer carry noisy `POST https://api.cross-deck.com/v1/events`
143
+ crumb entries.
144
+
145
+ ### Wiring (backend, paired)
146
+
147
+ - **`v1-events` ingest now honours the per-project `piiAllowList`.**
148
+ The admin management surface (`v1-pii-allow-list.ts`) was persisted +
149
+ audit-logged but the hot ingest path never read it. The new
150
+ `backend/src/api/lib/pii-allow-list-cache.ts` (60s TTL,
151
+ single-flight) feeds the project's allow-list to `scrubProperties()`
152
+ on every batch. `HARD_LOCKED_PATTERNS` are always stripped from the
153
+ effective list regardless of what's in storage. (Backend-only —
154
+ listed here so server-SDK consumers know defence-in-depth is fully
155
+ closed.)
156
+
7
157
  ## [1.2.0] — 2026-05-18
8
158
 
9
159
  ### Added
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:
@@ -1,4 +1,4 @@
1
- import { p as CrossdeckServer } from '../crossdeck-server-BZVZEuS-.mjs';
1
+ import { w as CrossdeckServer } from '../crossdeck-server-oAaKBnUU.mjs';
2
2
  import 'node:events';
3
3
 
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { p as CrossdeckServer } from '../crossdeck-server-BZVZEuS-.js';
1
+ import { w as CrossdeckServer } from '../crossdeck-server-oAaKBnUU.js';
2
2
  import 'node:events';
3
3
 
4
4
  /**