@cross-deck/node 1.10.1 → 1.12.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
@@ -6,6 +6,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.12.0] — 2026-07-01
10
+
11
+ **Added — `blockEventId`: the Crossdeck-hosted block page (v2 preview).**
12
+ On a block, `resolve()` and `gate()` now return `blockEventId` — the id of the
13
+ canonical block interstitial Crossdeck hosts for that exact verdict. The
14
+ recommended dead-end becomes one line:
15
+
16
+ ```ts
17
+ if (blocked && blockEventId) {
18
+ res.redirect(`https://api.cross-deck.com/v1/trust/page/${blockEventId}`);
19
+ }
20
+ ```
21
+
22
+ Crossdeck serves the branded "Access paused" page — a verification receipt
23
+ (application name, timestamp, the quotable `reference`) and a **Contact
24
+ support** action that mails the project owner's real recorded email — so you
25
+ never hand-build, fork, or drift the block screen. The id is opaque
26
+ (`cd_blk_…`, no PII in the URL) and present only on an explicit block;
27
+ fail-open behaviour is unchanged (never redirect on error/timeout).
28
+ `ResolveResult` and `GateVerdict` carry the new optional field — additive,
29
+ non-breaking. Still `@experimental` until the Crossdeck v2 launch.
30
+
31
+ ## [1.11.0] — 2026-06-30
32
+
33
+ **Added — block `reference` and `supportEmail` on the trust surface (still v2 preview).**
34
+ `resolve()` and `gate()` now surface two fields on a block:
35
+
36
+ - `reference` — a short, opaque, human-quotable support handle (the "Ray ID", `xxxx-xxxx`).
37
+ Show it on your suspended screen; Crossdeck stores the same string on the block event, so
38
+ support resolves a quoted reference to the exact block in the admin Caught feed.
39
+ - `supportEmail` — the email you registered with Crossdeck at signup, so your suspended
40
+ screen's "Contact support" reaches the real operator by default instead of a placeholder.
41
+
42
+ Both were typed on `ResolveResult` / `GateVerdict` but the client previously **dropped**
43
+ them when building the result object — now passed through. Additive, non-breaking. Opaque +
44
+ non-PII (never the score or rule logic).
45
+
9
46
  ## [1.10.1] — 2026-06-30
10
47
 
11
48
  **Fixed — the blocking surface now works for the documented paths (still v2 preview).**
package/README.md CHANGED
@@ -223,16 +223,29 @@ Blocking is **entitlements, inverted**: `getEntitlements()` asks *"can this user
223
223
  There are three doors. Pick by who's at it:
224
224
 
225
225
  ```ts
226
+ // The Crossdeck-hosted block page — the recommended dead-end for doors 1 + 2. One
227
+ // constant today; swaps to https://block.cross-deck.com when the alias goes live.
228
+ const BLOCK_PAGE = "https://api.cross-deck.com/v1/trust/page";
229
+
226
230
  // 1. A user you ALREADY KNOW — a bare userId rides the backbone (no token, no setup).
227
231
  // Crossdeck matches on the email it already holds. The common case.
228
- const { blocked, blockReason } = await crossdeck.resolve({ userId, ip: req.ip });
229
- if (blocked) { await signOut(); return showSuspended(blockReason); }
232
+ const { blocked, blockEventId } = await crossdeck.resolve({ userId, ip: req.ip });
233
+ if (blocked) {
234
+ await signOut();
235
+ return blockEventId
236
+ ? res.redirect(`${BLOCK_PAGE}/${blockEventId}`) // Crossdeck serves the branded page
237
+ : res.redirect("/suspended"); // rare: mint unavailable → your fallback
238
+ }
230
239
  // or just the boolean: if (await crossdeck.isBlocked({ userId })) …
231
240
 
232
241
  // 2. A BRAND-NEW signup Crossdeck has never seen — block by the two strings you have
233
242
  // at signup, their email + ip. Call it BEFORE you create the account.
234
243
  const gate = await crossdeck.gate({ email, ip: req.ip });
235
- if (gate.action === "block") return rejectSignup(gate.blockReason);
244
+ if (gate.action === "block") {
245
+ return gate.blockEventId
246
+ ? res.redirect(`${BLOCK_PAGE}/${gate.blockEventId}`)
247
+ : rejectSignup(gate.blockReason);
248
+ }
236
249
 
237
250
  // 3. A PUBLIC PAGE — "is the owner of this page blocked?" (no session token). Poll on a
238
251
  // short TTL to invalidate your cache; the ip is the owner's CREATION ip.
@@ -240,6 +253,15 @@ const owner = await crossdeck.getOwnerStatus({ userId: ownerId });
240
253
  if (owner.blocked) await takePageOffline(ownerId);
241
254
  ```
242
255
 
256
+ **Why redirect instead of building your own screen?** Crossdeck hosts the canonical block
257
+ interstitial — "Access paused", a verification receipt (application name, timestamp, an
258
+ opaque `reference` the person can quote to support), and a **Contact support** action that
259
+ mails the project owner's real recorded email. One page, centrally maintained, updated for
260
+ every integrator at once — you never fork it, restyle it, or let it drift. Redirect **only
261
+ on an explicit block** (`blocked === true` / `action === "block"`): on error or timeout the
262
+ SDK fails open and you let the user through — never send a real user to a block page over
263
+ a network blip.
264
+
243
265
  **Recommended server pattern (no issuer registration):** if your backend runs Firebase
244
266
  Admin, verify the ID token **locally** for the authoritative uid, then send that as a bare
245
267
  `userId` — you keep the cryptographic proof on your side and Crossdeck needs zero setup:
@@ -1,4 +1,4 @@
1
- import { x as CrossdeckServer } from '../crossdeck-server-BpBVFC4O.mjs';
1
+ import { x as CrossdeckServer } from '../crossdeck-server-CuTGwrwx.mjs';
2
2
  import 'node:events';
3
3
 
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { x as CrossdeckServer } from '../crossdeck-server-BpBVFC4O.js';
1
+ import { x as CrossdeckServer } from '../crossdeck-server-CuTGwrwx.js';
2
2
  import 'node:events';
3
3
 
4
4
  /**
@@ -183,8 +183,8 @@ declare const CrossdeckContracts: {
183
183
  readonly byId: (id: string) => Contract | undefined;
184
184
  readonly byPillar: (pillar: ContractPillar) => readonly Contract[];
185
185
  readonly withStatus: (status: ContractStatus) => readonly Contract[];
186
- readonly sdkVersion: "1.10.1";
187
- readonly bundledIn: "@cross-deck/node@1.10.1";
186
+ readonly sdkVersion: "1.12.0";
187
+ readonly bundledIn: "@cross-deck/node@1.12.0";
188
188
  /**
189
189
  * Resolve a failing test back to the contract it exercises.
190
190
  * Used by test-framework hooks to find the contract id of a
@@ -472,6 +472,26 @@ interface BlockVerdict {
472
472
  blockReason: string | null;
473
473
  /** The exact rule that fired: `"domain:spartan.net"` | `"ip:1.2.3.4"` | `"manual"` | null. */
474
474
  blockedKey: string | null;
475
+ /**
476
+ * Human-quotable support handle for this verdict (the "Ray ID"), formatted `xxxx-xxxx`.
477
+ * Show it on your suspended screen ("reference: blocklist:domain · a1f9-7c2e"); Crossdeck
478
+ * stores the same string on the block event, so support resolves it in the Caught feed.
479
+ */
480
+ reference?: string | null;
481
+ /**
482
+ * The recorded support contact (the project owner's signup email), present only when
483
+ * blocked — render it as the suspended screen's "Contact support" so appeals reach the
484
+ * real operator by default. Override with a dedicated address if you have one.
485
+ */
486
+ supportEmail?: string | null;
487
+ /**
488
+ * The id of the CD-hosted block interstitial minted for THIS verdict — present only when
489
+ * blocked. The recommended dead-end is one line:
490
+ * `redirect(\`https://api.cross-deck.com/v1/trust/page/${blockEventId}\`)` — Crossdeck
491
+ * serves the canonical branded page (receipt + reference + faithful support contact), so
492
+ * you never hand-build or fork the block screen. Opaque (`cd_blk_…`), no PII in the URL.
493
+ */
494
+ blockEventId?: string | null;
475
495
  /**
476
496
  * True when this is the fail-open default (Crossdeck unreachable / identity unresolved).
477
497
  * `blocked` is `false`; log it for observability, never treat it as a real "not blocked".
@@ -546,6 +566,12 @@ interface GateVerdict {
546
566
  blockReason: string | null;
547
567
  /** The exact rule/key that fired, when blocked. */
548
568
  blockedKey: string | null;
569
+ /** Human-quotable support handle (the "Ray ID"), formatted `xxxx-xxxx` — show it on the blocked screen. */
570
+ reference?: string | null;
571
+ /** Recorded support contact (project owner's signup email), present only on `block` — the blocked screen's "Contact support" default. */
572
+ supportEmail?: string | null;
573
+ /** CD-hosted block-page id (`cd_blk_…`), present only on `block` — redirect the rejected signup to `https://api.cross-deck.com/v1/trust/page/${blockEventId}` and Crossdeck serves the canonical branded dead-end. */
574
+ blockEventId?: string | null;
549
575
  /** True when this is the fail-open default (Crossdeck unreachable). `allow` is `true`. */
550
576
  degraded?: boolean;
551
577
  }
@@ -183,8 +183,8 @@ declare const CrossdeckContracts: {
183
183
  readonly byId: (id: string) => Contract | undefined;
184
184
  readonly byPillar: (pillar: ContractPillar) => readonly Contract[];
185
185
  readonly withStatus: (status: ContractStatus) => readonly Contract[];
186
- readonly sdkVersion: "1.10.1";
187
- readonly bundledIn: "@cross-deck/node@1.10.1";
186
+ readonly sdkVersion: "1.12.0";
187
+ readonly bundledIn: "@cross-deck/node@1.12.0";
188
188
  /**
189
189
  * Resolve a failing test back to the contract it exercises.
190
190
  * Used by test-framework hooks to find the contract id of a
@@ -472,6 +472,26 @@ interface BlockVerdict {
472
472
  blockReason: string | null;
473
473
  /** The exact rule that fired: `"domain:spartan.net"` | `"ip:1.2.3.4"` | `"manual"` | null. */
474
474
  blockedKey: string | null;
475
+ /**
476
+ * Human-quotable support handle for this verdict (the "Ray ID"), formatted `xxxx-xxxx`.
477
+ * Show it on your suspended screen ("reference: blocklist:domain · a1f9-7c2e"); Crossdeck
478
+ * stores the same string on the block event, so support resolves it in the Caught feed.
479
+ */
480
+ reference?: string | null;
481
+ /**
482
+ * The recorded support contact (the project owner's signup email), present only when
483
+ * blocked — render it as the suspended screen's "Contact support" so appeals reach the
484
+ * real operator by default. Override with a dedicated address if you have one.
485
+ */
486
+ supportEmail?: string | null;
487
+ /**
488
+ * The id of the CD-hosted block interstitial minted for THIS verdict — present only when
489
+ * blocked. The recommended dead-end is one line:
490
+ * `redirect(\`https://api.cross-deck.com/v1/trust/page/${blockEventId}\`)` — Crossdeck
491
+ * serves the canonical branded page (receipt + reference + faithful support contact), so
492
+ * you never hand-build or fork the block screen. Opaque (`cd_blk_…`), no PII in the URL.
493
+ */
494
+ blockEventId?: string | null;
475
495
  /**
476
496
  * True when this is the fail-open default (Crossdeck unreachable / identity unresolved).
477
497
  * `blocked` is `false`; log it for observability, never treat it as a real "not blocked".
@@ -546,6 +566,12 @@ interface GateVerdict {
546
566
  blockReason: string | null;
547
567
  /** The exact rule/key that fired, when blocked. */
548
568
  blockedKey: string | null;
569
+ /** Human-quotable support handle (the "Ray ID"), formatted `xxxx-xxxx` — show it on the blocked screen. */
570
+ reference?: string | null;
571
+ /** Recorded support contact (project owner's signup email), present only on `block` — the blocked screen's "Contact support" default. */
572
+ supportEmail?: string | null;
573
+ /** CD-hosted block-page id (`cd_blk_…`), present only on `block` — redirect the rejected signup to `https://api.cross-deck.com/v1/trust/page/${blockEventId}` and Crossdeck serves the canonical branded dead-end. */
574
+ blockEventId?: string | null;
549
575
  /** True when this is the fail-open default (Crossdeck unreachable). `allow` is `true`. */
550
576
  degraded?: boolean;
551
577
  }
package/dist/index.cjs CHANGED
@@ -387,7 +387,7 @@ function byteLength(s) {
387
387
  var https = __toESM(require("https"));
388
388
 
389
389
  // src/_version.ts
390
- var SDK_VERSION = "1.10.1";
390
+ var SDK_VERSION = "1.12.0";
391
391
  var SDK_NAME = "@cross-deck/node";
392
392
 
393
393
  // src/_diagnostic-telemetry.ts
@@ -3207,6 +3207,9 @@ var CrossdeckServer = class extends import_node_events.EventEmitter {
3207
3207
  blocked: raw.blocked === true,
3208
3208
  blockReason: raw.blockReason ?? null,
3209
3209
  blockedKey: raw.blockedKey ?? null,
3210
+ reference: raw.reference ?? null,
3211
+ supportEmail: raw.supportEmail ?? null,
3212
+ blockEventId: raw.blockEventId ?? null,
3210
3213
  status: typeof raw.status === "string" ? raw.status : "active",
3211
3214
  entitlements: Array.isArray(raw.entitlements) ? raw.entitlements.filter((e) => typeof e === "string") : [],
3212
3215
  crossdeckCustomerId: raw.crossdeckCustomerId ?? raw.user?.id ?? null,
@@ -3316,7 +3319,10 @@ var CrossdeckServer = class extends import_node_events.EventEmitter {
3316
3319
  action,
3317
3320
  allow: raw.allow !== false && action !== "block",
3318
3321
  blockReason: raw.blockReason ?? null,
3319
- blockedKey: raw.blockedKey ?? null
3322
+ blockedKey: raw.blockedKey ?? null,
3323
+ reference: raw.reference ?? null,
3324
+ supportEmail: raw.supportEmail ?? null,
3325
+ blockEventId: raw.blockEventId ?? null
3320
3326
  };
3321
3327
  } catch (err) {
3322
3328
  this.debug.emit(
@@ -4774,8 +4780,8 @@ function normaliseSecrets(input) {
4774
4780
  }
4775
4781
 
4776
4782
  // src/_contracts-bundled.ts
4777
- var BUNDLED_IN = "@cross-deck/node@1.10.1";
4778
- var SDK_VERSION2 = "1.10.1";
4783
+ var BUNDLED_IN = "@cross-deck/node@1.12.0";
4784
+ var SDK_VERSION2 = "1.12.0";
4779
4785
  var BUNDLED_CONTRACTS = Object.freeze([
4780
4786
  {
4781
4787
  "id": "contract-failed-payload-schema-lock",
@@ -4897,7 +4903,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
4897
4903
  "legal/security/index.html#diagnostic",
4898
4904
  "legal/sdk-data/index.html#b-diagnostic"
4899
4905
  ],
4900
- "bundledIn": "@cross-deck/node@1.10.1"
4906
+ "bundledIn": "@cross-deck/node@1.12.0"
4901
4907
  },
4902
4908
  {
4903
4909
  "id": "documentation-honesty",
@@ -4929,7 +4935,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
4929
4935
  ],
4930
4936
  "registeredAt": "2026-05-26",
4931
4937
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 7.1",
4932
- "bundledIn": "@cross-deck/node@1.10.1"
4938
+ "bundledIn": "@cross-deck/node@1.12.0"
4933
4939
  },
4934
4940
  {
4935
4941
  "id": "error-envelope-shape",
@@ -4968,7 +4974,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
4968
4974
  ],
4969
4975
  "registeredAt": "2026-05-26",
4970
4976
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 8 (codifies existing contract)",
4971
- "bundledIn": "@cross-deck/node@1.10.1"
4977
+ "bundledIn": "@cross-deck/node@1.12.0"
4972
4978
  },
4973
4979
  {
4974
4980
  "id": "flush-interval-parity",
@@ -5013,7 +5019,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
5013
5019
  ],
5014
5020
  "registeredAt": "2026-05-26",
5015
5021
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 3.3",
5016
- "bundledIn": "@cross-deck/node@1.10.1"
5022
+ "bundledIn": "@cross-deck/node@1.12.0"
5017
5023
  },
5018
5024
  {
5019
5025
  "id": "idempotency-key-deterministic",
@@ -5118,7 +5124,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
5118
5124
  ],
5119
5125
  "registeredAt": "2026-05-26",
5120
5126
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 2.2.a + 2.2.b + 2.2.c",
5121
- "bundledIn": "@cross-deck/node@1.10.1"
5127
+ "bundledIn": "@cross-deck/node@1.12.0"
5122
5128
  },
5123
5129
  {
5124
5130
  "id": "invalid-input-rejected-natively",
@@ -5174,7 +5180,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
5174
5180
  ],
5175
5181
  "registeredAt": "2026-06-11",
5176
5182
  "firstRegisteredIn": "swift trap-on-input class fix \u2014 first machine-tested Swift release",
5177
- "bundledIn": "@cross-deck/node@1.10.1"
5183
+ "bundledIn": "@cross-deck/node@1.12.0"
5178
5184
  },
5179
5185
  {
5180
5186
  "id": "node-pii-scrubber",
@@ -5213,7 +5219,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
5213
5219
  ],
5214
5220
  "registeredAt": "2026-05-26",
5215
5221
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 3.1",
5216
- "bundledIn": "@cross-deck/node@1.10.1"
5222
+ "bundledIn": "@cross-deck/node@1.12.0"
5217
5223
  },
5218
5224
  {
5219
5225
  "id": "node-shutdown-awaits-flush",
@@ -5246,7 +5252,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
5246
5252
  ],
5247
5253
  "registeredAt": "2026-05-26",
5248
5254
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 5.4",
5249
- "bundledIn": "@cross-deck/node@1.10.1"
5255
+ "bundledIn": "@cross-deck/node@1.12.0"
5250
5256
  },
5251
5257
  {
5252
5258
  "id": "sdk-error-codes-catalogue",
@@ -5291,7 +5297,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
5291
5297
  ],
5292
5298
  "registeredAt": "2026-05-26",
5293
5299
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 6.2",
5294
- "bundledIn": "@cross-deck/node@1.10.1"
5300
+ "bundledIn": "@cross-deck/node@1.12.0"
5295
5301
  },
5296
5302
  {
5297
5303
  "id": "sync-purchases-funnel-parity",
@@ -5324,7 +5330,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
5324
5330
  ],
5325
5331
  "registeredAt": "2026-05-26",
5326
5332
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 3.5",
5327
- "bundledIn": "@cross-deck/node@1.10.1"
5333
+ "bundledIn": "@cross-deck/node@1.12.0"
5328
5334
  },
5329
5335
  {
5330
5336
  "id": "verifier-timestamp-mandatory",
@@ -5378,7 +5384,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
5378
5384
  ],
5379
5385
  "registeredAt": "2026-05-26",
5380
5386
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 7.2",
5381
- "bundledIn": "@cross-deck/node@1.10.1"
5387
+ "bundledIn": "@cross-deck/node@1.12.0"
5382
5388
  }
5383
5389
  ]);
5384
5390