@cross-deck/node 1.0.0 → 1.1.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,29 @@ 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.1.0] — 2026-05-13
8
+
9
+ ### Added
10
+
11
+ - **Auto-heartbeat on construction.** `new CrossdeckServer({...})` now
12
+ fires a heartbeat in the background the moment the SDK is
13
+ constructed, fire-and-forget. The dashboard's row flips LIVE within
14
+ ~200 ms of the customer's process boot — no explicit `.heartbeat()`
15
+ call required in the bootstrap. Solves the cold-start serverless
16
+ verification problem at its root (function boot triggers SDK
17
+ construction triggers heartbeat; the install-verifier's URL probe
18
+ doubles as a cold-start waker).
19
+ - New option `bootHeartbeat?: boolean` (default `true`). Set `false`
20
+ for latency-sensitive cold paths that want the prior v1.0.0
21
+ caller-controlled behaviour. Implicitly disabled in `testMode`.
22
+
23
+ ### Why this is non-breaking
24
+
25
+ The boot heartbeat is fire-and-forget and swallows its own errors —
26
+ the caller's code never blocks on it, never throws, and a failure
27
+ (bad key, network blip, firewall) has zero effect on subsequent
28
+ event flushes. Equivalent to Sentry's `Sentry.init()` boot session.
29
+
7
30
  ## [1.0.0] — 2026-05-13
8
31
 
9
32
  Full three-USP server SDK release. Version-aligned with `@cross-deck/web@1.0.0`. Bank-grade quality bar — Stripe + Apple + Google VP-level QA review across two passes. 6,796 LOC of source / 6,230 LOC of tests / 398 unit tests + 19 e2e todos passing / Gate 3 fixture verifying the snippet against the built bundle. Web-SDK parity at the capability level: every Web SDK guarantee that has a server-side analogue ships here.
@@ -1,4 +1,4 @@
1
- import { p as CrossdeckServer } from '../crossdeck-server-LvQwPKu5.mjs';
1
+ import { p as CrossdeckServer } from '../crossdeck-server-BXQaFjVx.mjs';
2
2
  import 'node:events';
3
3
 
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { p as CrossdeckServer } from '../crossdeck-server-LvQwPKu5.js';
1
+ import { p as CrossdeckServer } from '../crossdeck-server-BXQaFjVx.js';
2
2
  import 'node:events';
3
3
 
4
4
  /**
@@ -236,7 +236,7 @@ interface RuntimeInfo {
236
236
  }
237
237
 
238
238
  declare const SDK_NAME = "@cross-deck/node";
239
- declare const SDK_VERSION = "1.0.0";
239
+ declare const SDK_VERSION = "1.1.0";
240
240
  declare const DEFAULT_BASE_URL = "https://api.cross-deck.com/v1";
241
241
  declare const DEFAULT_TIMEOUT_MS = 15000;
242
242
  /**
@@ -430,6 +430,27 @@ interface CrossdeckServerOptions {
430
430
  * platform's own SIGKILL (typically 5-10s after SIGTERM) preempts us.
431
431
  */
432
432
  flushOnExitTimeoutMs?: number;
433
+ /**
434
+ * Fire a heartbeat in the background the moment the SDK is
435
+ * constructed. Default `true`.
436
+ *
437
+ * This is what makes the dashboard's "Verify install" surface
438
+ * actually work in cold-start serverless: the moment the customer's
439
+ * process boots and runs `new CrossdeckServer({...})`, we phone
440
+ * home, the dashboard row flips LIVE, and the caller doesn't have
441
+ * to add an explicit `await server.heartbeat()` to their bootstrap.
442
+ *
443
+ * Fire-and-forget. Failures are swallowed (the SDK still works for
444
+ * events even if this boot ping can't reach the backend). The
445
+ * caller's process never blocks on this.
446
+ *
447
+ * Set `false` if you want the prior v1.0.0 behaviour where the
448
+ * caller controlled when (or whether) the first network ping fired
449
+ * — e.g., very latency-sensitive cold paths, or environments where
450
+ * the very first request must not race with an SDK-initiated call.
451
+ * `testMode: true` also disables this implicitly.
452
+ */
453
+ bootHeartbeat?: boolean;
433
454
  /**
434
455
  * TTL for the entitlement cache (ms). Default 60_000 (60s).
435
456
  *
@@ -236,7 +236,7 @@ interface RuntimeInfo {
236
236
  }
237
237
 
238
238
  declare const SDK_NAME = "@cross-deck/node";
239
- declare const SDK_VERSION = "1.0.0";
239
+ declare const SDK_VERSION = "1.1.0";
240
240
  declare const DEFAULT_BASE_URL = "https://api.cross-deck.com/v1";
241
241
  declare const DEFAULT_TIMEOUT_MS = 15000;
242
242
  /**
@@ -430,6 +430,27 @@ interface CrossdeckServerOptions {
430
430
  * platform's own SIGKILL (typically 5-10s after SIGTERM) preempts us.
431
431
  */
432
432
  flushOnExitTimeoutMs?: number;
433
+ /**
434
+ * Fire a heartbeat in the background the moment the SDK is
435
+ * constructed. Default `true`.
436
+ *
437
+ * This is what makes the dashboard's "Verify install" surface
438
+ * actually work in cold-start serverless: the moment the customer's
439
+ * process boots and runs `new CrossdeckServer({...})`, we phone
440
+ * home, the dashboard row flips LIVE, and the caller doesn't have
441
+ * to add an explicit `await server.heartbeat()` to their bootstrap.
442
+ *
443
+ * Fire-and-forget. Failures are swallowed (the SDK still works for
444
+ * events even if this boot ping can't reach the backend). The
445
+ * caller's process never blocks on this.
446
+ *
447
+ * Set `false` if you want the prior v1.0.0 behaviour where the
448
+ * caller controlled when (or whether) the first network ping fired
449
+ * — e.g., very latency-sensitive cold paths, or environments where
450
+ * the very first request must not race with an SDK-initiated call.
451
+ * `testMode: true` also disables this implicitly.
452
+ */
453
+ bootHeartbeat?: boolean;
433
454
  /**
434
455
  * TTL for the entitlement cache (ms). Default 60_000 (60s).
435
456
  *
package/dist/index.cjs CHANGED
@@ -365,7 +365,7 @@ function byteLength(s) {
365
365
 
366
366
  // src/http.ts
367
367
  var SDK_NAME = "@cross-deck/node";
368
- var SDK_VERSION = "1.0.0";
368
+ var SDK_VERSION = "1.1.0";
369
369
  var DEFAULT_BASE_URL = "https://api.cross-deck.com/v1";
370
370
  var DEFAULT_TIMEOUT_MS = 15e3;
371
371
  var CROSSDECK_API_VERSION = "2025-01-01";
@@ -1084,13 +1084,21 @@ function isInAppFrame(filename) {
1084
1084
  if (/^internal[\\/]/.test(filename)) return false;
1085
1085
  return true;
1086
1086
  }
1087
- function fingerprintError(message, frames) {
1087
+ function fingerprintError(message, frames, location) {
1088
1088
  const inAppFrames = frames.filter((f) => f.in_app).slice(0, 3);
1089
- const key = [
1089
+ const parts = [
1090
1090
  (message || "").slice(0, 200),
1091
1091
  ...inAppFrames.map((f) => `${f.function}@${f.filename}:${f.lineno}`)
1092
- ].join("|");
1093
- return djb2Hex(key);
1092
+ ];
1093
+ if (inAppFrames.length === 0 && location) {
1094
+ const loc = [
1095
+ location.errorType ?? "",
1096
+ location.filename ?? "",
1097
+ location.lineno ?? ""
1098
+ ].join(":");
1099
+ if (loc !== "::") parts.push(loc);
1100
+ }
1101
+ return djb2Hex(parts.join("|"));
1094
1102
  }
1095
1103
  function djb2Hex(input) {
1096
1104
  let h = 5381;
@@ -1321,34 +1329,30 @@ var ErrorTracker = class {
1321
1329
  * runtime-agnostic.
1322
1330
  */
1323
1331
  buildFromUnknown(err, kind, level) {
1324
- if (err instanceof Error) {
1325
- const frames = parseStack(err.stack);
1326
- return {
1327
- timestamp: Date.now(),
1328
- kind,
1329
- level,
1330
- message: String(err.message).slice(0, 1024),
1331
- errorType: err.name,
1332
- frames,
1333
- rawStack: err.stack ?? null,
1334
- fingerprint: fingerprintError(err.message, frames),
1335
- breadcrumbs: this.opts.breadcrumbs.snapshot(),
1336
- context: this.opts.getContext(),
1337
- tags: this.opts.getTags()
1338
- };
1339
- }
1340
- const message = safeStringify3(err).slice(0, 1024);
1332
+ const payload = coerceErrorPayload(err);
1333
+ const message = (payload.message || "Unknown error").slice(0, 1024);
1334
+ const stack = err instanceof Error ? err.stack ?? null : null;
1335
+ const frames = parseStack(stack);
1336
+ const errorType = payload.errorType ?? null;
1337
+ const context = payload.extras ? { ...this.opts.getContext(), __error_extras: payload.extras } : this.opts.getContext();
1341
1338
  return {
1342
1339
  timestamp: Date.now(),
1343
1340
  kind,
1344
1341
  level,
1345
1342
  message,
1346
- errorType: null,
1347
- frames: [],
1348
- rawStack: null,
1349
- fingerprint: fingerprintError(message, []),
1343
+ errorType,
1344
+ frames,
1345
+ rawStack: stack,
1346
+ // Location fallback ensures distinct call sites stay separate
1347
+ // even when the message is generic and there are no parseable
1348
+ // frames (e.g. `throw "boom"` from a middleware).
1349
+ fingerprint: fingerprintError(message, frames, {
1350
+ filename: frames[0]?.filename ?? null,
1351
+ lineno: frames[0]?.lineno ?? null,
1352
+ errorType
1353
+ }),
1350
1354
  breadcrumbs: this.opts.breadcrumbs.snapshot(),
1351
- context: this.opts.getContext(),
1355
+ context,
1352
1356
  tags: this.opts.getTags()
1353
1357
  };
1354
1358
  }
@@ -1363,7 +1367,10 @@ var ErrorTracker = class {
1363
1367
  errorType: "HTTPError",
1364
1368
  frames: [],
1365
1369
  rawStack: null,
1366
- fingerprint: fingerprintError(`HTTP ${info.status} ${info.method}`, []),
1370
+ fingerprint: fingerprintError(`HTTP ${info.status} ${info.method}`, [], {
1371
+ filename: info.url,
1372
+ errorType: "HTTPError"
1373
+ }),
1367
1374
  breadcrumbs: this.opts.breadcrumbs.snapshot(),
1368
1375
  context: this.opts.getContext(),
1369
1376
  tags: this.opts.getTags(),
@@ -1466,16 +1473,151 @@ var ErrorTracker = class {
1466
1473
  }
1467
1474
  }
1468
1475
  };
1469
- function safeStringify3(v) {
1470
- if (v == null) return String(v);
1471
- if (typeof v === "string") return v;
1472
- if (typeof v === "number" || typeof v === "boolean") return String(v);
1476
+ function coerceErrorPayload(v) {
1477
+ if (v === null) return { message: "(thrown: null)", errorType: null, extras: null };
1478
+ if (v === void 0) return { message: "(thrown: undefined)", errorType: null, extras: null };
1479
+ if (typeof v === "string") {
1480
+ return { message: v, errorType: null, extras: null };
1481
+ }
1482
+ if (typeof v === "number" || typeof v === "boolean" || typeof v === "bigint") {
1483
+ return { message: String(v), errorType: typeof v, extras: null };
1484
+ }
1485
+ if (typeof v === "symbol") {
1486
+ return { message: v.toString(), errorType: "symbol", extras: null };
1487
+ }
1488
+ if (typeof v === "function") {
1489
+ return { message: `(thrown function: ${v.name || "anonymous"})`, errorType: "function", extras: null };
1490
+ }
1491
+ if (v instanceof Error) {
1492
+ const errorType = v.name || v.constructor?.name || "Error";
1493
+ const message = typeof v.message === "string" && v.message.length > 0 ? v.message : safeToString(v) || errorType;
1494
+ const extras = {};
1495
+ const causeChain = collectCauseChain(v);
1496
+ if (causeChain.length > 0) extras.cause = causeChain;
1497
+ const aggErrors = v.errors;
1498
+ if (Array.isArray(aggErrors)) {
1499
+ extras.aggregatedErrors = aggErrors.slice(0, 10).map((inner) => {
1500
+ if (inner instanceof Error) {
1501
+ return { name: inner.name || "Error", message: inner.message || "" };
1502
+ }
1503
+ return { name: "non-Error", message: safeToString(inner) };
1504
+ });
1505
+ }
1506
+ for (const key of [
1507
+ "code",
1508
+ "errno",
1509
+ "syscall",
1510
+ "path",
1511
+ "status",
1512
+ "statusCode",
1513
+ "response",
1514
+ "data",
1515
+ "detail",
1516
+ "details"
1517
+ ]) {
1518
+ const val = v[key];
1519
+ if (val !== void 0 && typeof val !== "function") {
1520
+ extras[key] = safeClone(val);
1521
+ }
1522
+ }
1523
+ for (const key of Object.keys(v)) {
1524
+ if (key === "message" || key === "stack" || key === "name" || key === "cause" || key === "errors") continue;
1525
+ if (key in extras) continue;
1526
+ const val = v[key];
1527
+ if (typeof val === "function") continue;
1528
+ extras[key] = safeClone(val);
1529
+ }
1530
+ return {
1531
+ message,
1532
+ errorType,
1533
+ extras: Object.keys(extras).length > 0 ? extras : null
1534
+ };
1535
+ }
1536
+ if (typeof Response !== "undefined" && v instanceof Response) {
1537
+ return {
1538
+ message: `HTTP ${v.status} ${v.statusText || ""}${v.url ? ` ${v.url}` : ""}`.trim(),
1539
+ errorType: "Response",
1540
+ extras: { status: v.status, statusText: v.statusText, url: v.url, type: v.type }
1541
+ };
1542
+ }
1543
+ if (typeof v === "object") {
1544
+ const obj = v;
1545
+ const ctorName = obj.constructor && typeof obj.constructor === "function" && obj.constructor.name || null;
1546
+ const ownMessage = typeof obj.message === "string" && obj.message ? obj.message : null;
1547
+ const ownName = typeof obj.name === "string" && obj.name ? obj.name : null;
1548
+ let jsonForm = null;
1549
+ try {
1550
+ const serialised = JSON.stringify(obj);
1551
+ jsonForm = serialised === "{}" ? null : serialised;
1552
+ } catch {
1553
+ jsonForm = null;
1554
+ }
1555
+ const fallbackString = safeToString(obj);
1556
+ const message = ownMessage ?? jsonForm ?? (fallbackString && fallbackString !== "[object Object]" ? fallbackString : null) ?? (ctorName ? `(thrown ${ctorName} with no message)` : "(thrown object with no message)");
1557
+ const errorType = ownName ?? ctorName ?? null;
1558
+ const extras = {};
1559
+ let count = 0;
1560
+ for (const key of Object.keys(obj)) {
1561
+ if (count >= 20) break;
1562
+ if (key === "message" || key === "name") continue;
1563
+ const val = obj[key];
1564
+ if (typeof val === "function") continue;
1565
+ extras[key] = safeClone(val);
1566
+ count++;
1567
+ }
1568
+ return {
1569
+ message,
1570
+ errorType,
1571
+ extras: Object.keys(extras).length > 0 ? extras : null
1572
+ };
1573
+ }
1574
+ return { message: safeToString(v) || "(unstringifiable thrown value)", errorType: null, extras: null };
1575
+ }
1576
+ function collectCauseChain(err) {
1577
+ const out = [];
1578
+ let cur = err.cause;
1579
+ let depth = 0;
1580
+ while (cur != null && depth < 5) {
1581
+ if (cur instanceof Error) {
1582
+ out.push({ name: cur.name || "Error", message: cur.message || "" });
1583
+ cur = cur.cause;
1584
+ } else {
1585
+ out.push({ name: "non-Error", message: safeToString(cur) });
1586
+ cur = null;
1587
+ }
1588
+ depth++;
1589
+ }
1590
+ return out;
1591
+ }
1592
+ function safeToString(v) {
1593
+ try {
1594
+ const s = Object.prototype.toString.call(v);
1595
+ if (s !== "[object Object]") return s;
1596
+ const own = v?.toString;
1597
+ if (typeof own === "function" && own !== Object.prototype.toString) {
1598
+ const r = own.call(v);
1599
+ if (typeof r === "string") return r;
1600
+ }
1601
+ return s;
1602
+ } catch {
1603
+ return "(throwing toString)";
1604
+ }
1605
+ }
1606
+ function safeClone(v) {
1607
+ if (v == null) return v;
1608
+ const t = typeof v;
1609
+ if (t === "string" || t === "number" || t === "boolean") return v;
1610
+ if (t === "bigint") return String(v);
1473
1611
  try {
1474
- return JSON.stringify(v);
1612
+ const s = JSON.stringify(v);
1613
+ return s === void 0 ? safeToString(v) : JSON.parse(s);
1475
1614
  } catch {
1476
- return Object.prototype.toString.call(v);
1615
+ return safeToString(v);
1477
1616
  }
1478
1617
  }
1618
+ function safeStringify3(v) {
1619
+ return coerceErrorPayload(v).message;
1620
+ }
1479
1621
 
1480
1622
  // src/runtime-info.ts
1481
1623
  var import_node_os = require("os");
@@ -2239,6 +2381,17 @@ var CrossdeckServer = class extends import_node_events.EventEmitter {
2239
2381
  });
2240
2382
  this.flushOnExit.install();
2241
2383
  }
2384
+ if (options.testMode !== true && options.bootHeartbeat !== false) {
2385
+ setImmediate(() => {
2386
+ void this.heartbeat().catch((err) => {
2387
+ this.debug.emit(
2388
+ "sdk.boot_heartbeat_failed",
2389
+ "Boot heartbeat failed (non-fatal \u2014 events will still flush).",
2390
+ { message: err instanceof Error ? err.message : String(err) }
2391
+ );
2392
+ });
2393
+ });
2394
+ }
2242
2395
  }
2243
2396
  // ============================================================
2244
2397
  // Identity — direct HTTP (transactional, not telemetry)