@cross-deck/web 1.6.4 → 1.8.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/dist/index.mjs CHANGED
@@ -8,6 +8,8 @@ var CrossdeckError = class _CrossdeckError extends Error {
8
8
  this.requestId = payload.requestId;
9
9
  this.status = payload.status;
10
10
  this.retryAfterMs = payload.retryAfterMs;
11
+ this.minVersion = payload.minVersion;
12
+ this.surface = payload.surface;
11
13
  Object.setPrototypeOf(this, _CrossdeckError.prototype);
12
14
  }
13
15
  };
@@ -28,7 +30,10 @@ async function crossdeckErrorFromResponse(res) {
28
30
  message: envelope.message ?? `HTTP ${res.status}`,
29
31
  requestId: envelope.request_id ?? requestId,
30
32
  status: res.status,
31
- retryAfterMs
33
+ retryAfterMs,
34
+ // PARK metadata, present only on a 426 / sdk_version_unsupported body.
35
+ minVersion: typeof envelope.minVersion === "string" ? envelope.minVersion : void 0,
36
+ surface: typeof envelope.surface === "string" ? envelope.surface : void 0
32
37
  });
33
38
  }
34
39
  return new CrossdeckError({
@@ -64,7 +69,7 @@ function typeMapForStatus(status) {
64
69
  }
65
70
 
66
71
  // src/_version.ts
67
- var SDK_VERSION = "1.6.4";
72
+ var SDK_VERSION = "1.8.0";
68
73
  var SDK_NAME = "@cross-deck/web";
69
74
 
70
75
  // src/http.ts
@@ -904,6 +909,18 @@ var EventQueue = class {
904
909
  this.cancelTimer = null;
905
910
  this.firstFlushFired = false;
906
911
  this.nextRetryAt = null;
912
+ /**
913
+ * PARK state (HTTP 426 / `sdk_version_unsupported`). Once parked, the
914
+ * queue stops flushing — the server has told us our wire dialect is too
915
+ * old, and retrying the same payload only burns the user's battery and
916
+ * bandwidth and drips pointless rejects into the server logs until the
917
+ * app ships an upgraded SDK. The held events stay durable (persisted)
918
+ * and are delivered by the next boot's rehydrate, post-upgrade. Parked
919
+ * is per-instance: a fresh boot (the upgraded build) starts unparked.
920
+ */
921
+ this.parked = false;
922
+ /** One developer-facing console warning per instance — never per-event spam. */
923
+ this.parkWarned = false;
907
924
  this.retry = new RetryPolicy(cfg.retry ?? {});
908
925
  this.persistent = cfg.persistentStore ?? null;
909
926
  if (this.persistent) {
@@ -959,6 +976,7 @@ var EventQueue = class {
959
976
  * flushes (pagehide / visibilitychange→hidden / beforeunload).
960
977
  */
961
978
  async flush(options = {}) {
979
+ if (this.parked) return null;
962
980
  let batch;
963
981
  let batchId;
964
982
  if (this.pendingBatch !== null && this.pendingBatchId !== null) {
@@ -980,9 +998,13 @@ var EventQueue = class {
980
998
  const env = this.cfg.envelope();
981
999
  const result = await this.cfg.http.request("POST", "/events", {
982
1000
  body: {
983
- // NorthStar §13.1 batch envelope. The backend validates these
984
- // against the API-key-resolved app and rejects mismatches
985
- // loudly (env_mismatch).
1001
+ // Event Envelope v1 batch envelope (backend/docs/
1002
+ // event-envelope-spec-v1.md §1). `envelopeVersion` is the
1003
+ // schema/wire version the server parses against ("can I parse
1004
+ // this?") and is DISTINCT from `sdk.version` ("which build is in
1005
+ // the wild?") — two questions, two fields, never conflated. The
1006
+ // backend refuses payloads with a missing/unknown envelopeVersion.
1007
+ envelopeVersion: 1,
986
1008
  appId: env.appId,
987
1009
  environment: env.environment,
988
1010
  sdk: env.sdk,
@@ -1006,6 +1028,29 @@ var EventQueue = class {
1006
1028
  } catch (err) {
1007
1029
  const message = err instanceof Error ? err.message : String(err);
1008
1030
  this.lastError = message;
1031
+ if (isVersionRejected(err)) {
1032
+ this.parked = true;
1033
+ this.buffer = [...batch, ...this.buffer];
1034
+ if (this.buffer.length > HARD_BUFFER_CAP) {
1035
+ const overflow = this.buffer.length - HARD_BUFFER_CAP;
1036
+ this.buffer.splice(0, overflow);
1037
+ this.dropped += overflow;
1038
+ }
1039
+ this.pendingBatch = null;
1040
+ this.pendingBatchId = null;
1041
+ this.inFlight -= batch.length;
1042
+ this.persistAll();
1043
+ this.cfg.onBufferChange?.(this.buffer.length);
1044
+ const minVersion = versionRejectionFloor(err);
1045
+ if (!this.parkWarned) {
1046
+ this.parkWarned = true;
1047
+ console.warn(
1048
+ `[Crossdeck] SDK outdated \u2014 the server is no longer accepting this version's event format. Your events are PARKED on-device (held, not lost) and will deliver automatically once you update @cross-deck/web${minVersion ? ` to >= ${minVersion}` : ""} and redeploy.`
1049
+ );
1050
+ }
1051
+ this.cfg.onParked?.({ minVersion, surface: versionRejectionSurface(err) });
1052
+ return null;
1053
+ }
1009
1054
  if (isPermanent4xx(err)) {
1010
1055
  const droppedCount = batch.length;
1011
1056
  this.pendingBatch = null;
@@ -1124,8 +1169,22 @@ function isPermanent4xx(err) {
1124
1169
  if (typeof status !== "number" || !Number.isFinite(status)) return false;
1125
1170
  if (status < 400 || status >= 500) return false;
1126
1171
  if (status === 408 || status === 429) return false;
1172
+ if (status === 426) return false;
1127
1173
  return true;
1128
1174
  }
1175
+ function isVersionRejected(err) {
1176
+ if (!err || typeof err !== "object") return false;
1177
+ if (err.status === 426) return true;
1178
+ return err.code === "sdk_version_unsupported";
1179
+ }
1180
+ function versionRejectionFloor(err) {
1181
+ const v = err?.minVersion;
1182
+ return typeof v === "string" && v.length > 0 ? v : void 0;
1183
+ }
1184
+ function versionRejectionSurface(err) {
1185
+ const v = err?.surface;
1186
+ return typeof v === "string" && v.length > 0 ? v : void 0;
1187
+ }
1129
1188
  function defaultScheduler(fn, ms) {
1130
1189
  const id = setTimeout(fn, ms);
1131
1190
  if (typeof id.unref === "function") {
@@ -1437,6 +1496,15 @@ var AutoTracker = class {
1437
1496
  this.cleanups = [];
1438
1497
  /** Last time we flushed lastActivityAt to storage (throttle gate). */
1439
1498
  this.lastPersistAt = 0;
1499
+ /**
1500
+ * Event Envelope v1 §3 — per-session monotonic sequence counter.
1501
+ * The single owner of the seq counter lives here, alongside all other
1502
+ * session state. Incremented atomically at track() time (via nextSeq()),
1503
+ * reset to 0 whenever a new session boundary is crossed (startNewSession
1504
+ * path + resetSession). Persists across background/foreground within a
1505
+ * session — only a real session boundary (not a tab hide/show) resets it.
1506
+ */
1507
+ this._sessionSeq = 0;
1440
1508
  /**
1441
1509
  * Stable per-page-view identifier. Minted at every `page.viewed`
1442
1510
  * emission and attached to every subsequent event until the next
@@ -1521,6 +1589,20 @@ var AutoTracker = class {
1521
1589
  get currentAcquisition() {
1522
1590
  return this.session?.acquisition ?? EMPTY_ACQUISITION;
1523
1591
  }
1592
+ /**
1593
+ * Event Envelope v1 §3 — return the next seq value for the current session
1594
+ * and advance the counter. Called SYNCHRONOUSLY at track() time, before any
1595
+ * async dispatch, so the seq assignment order is deterministic (call order,
1596
+ * not scheduler luck). The counter is reset to 0 at every session boundary;
1597
+ * it is NOT reset by tab hide/show (spec §3 clause 1: backgrounding does not
1598
+ * reset seq). When no session is active (Node, before init, after uninstall),
1599
+ * returns 0 and leaves the counter unchanged — fallback, not an error.
1600
+ */
1601
+ nextSeq() {
1602
+ const seq = this._sessionSeq;
1603
+ this._sessionSeq += 1;
1604
+ return seq;
1605
+ }
1524
1606
  // ---------- sessions ----------
1525
1607
  installSessionTracking() {
1526
1608
  const now = Date.now();
@@ -1572,6 +1654,7 @@ var AutoTracker = class {
1572
1654
  }
1573
1655
  startNewSession() {
1574
1656
  const now = Date.now();
1657
+ this._sessionSeq = 0;
1575
1658
  return {
1576
1659
  sessionId: mintSessionId(),
1577
1660
  startedAt: now,
@@ -2300,6 +2383,40 @@ function writeJson(storage, key, value) {
2300
2383
  }
2301
2384
  }
2302
2385
 
2386
+ // src/internal-opt-out.ts
2387
+ var INTERNAL_OPT_OUT_PROPERTY = "$crossdeck_internal";
2388
+ var STORAGE_KEY = "crossdeck.internalOptOut";
2389
+ function localStore() {
2390
+ try {
2391
+ return typeof localStorage !== "undefined" ? localStorage : null;
2392
+ } catch {
2393
+ return null;
2394
+ }
2395
+ }
2396
+ function processInternalOptOutUrl() {
2397
+ try {
2398
+ const search = typeof location !== "undefined" ? location.search : "";
2399
+ const params = new URLSearchParams(search || "");
2400
+ if (!params.has("crossdeck_internal")) return;
2401
+ const store = localStore();
2402
+ if (!store) return;
2403
+ const v = params.get("crossdeck_internal");
2404
+ if (v === "1" || v === "true") {
2405
+ store.setItem(STORAGE_KEY, "1");
2406
+ } else if (v === "0" || v === "false") {
2407
+ store.removeItem(STORAGE_KEY);
2408
+ }
2409
+ } catch {
2410
+ }
2411
+ }
2412
+ function isInternalOptOut() {
2413
+ try {
2414
+ return localStore()?.getItem(STORAGE_KEY) === "1";
2415
+ } catch {
2416
+ return false;
2417
+ }
2418
+ }
2419
+
2303
2420
  // src/web-vitals.ts
2304
2421
  var WebVitalsTracker = class {
2305
2422
  constructor(cfg, report) {
@@ -2808,6 +2925,13 @@ var CROSSDECK_ERROR_CODES = Object.freeze([
2808
2925
  description: "A field is present but the value failed validation (wrong shape, wrong length, wrong enum value).",
2809
2926
  resolution: "Read error.message for the specific field + reason. SDK-managed call sites should never emit this \u2014 file a bug if you do.",
2810
2927
  retryable: false
2928
+ },
2929
+ {
2930
+ code: "sdk_version_unsupported",
2931
+ type: "version_error",
2932
+ description: "HTTP 426 \u2014 your installed SDK sends an event format the server no longer accepts. The data is good; only the wire dialect is too old. The SDK PARKS automatically: events are held on-device (paused, not lost) and deliver on the next launch after you upgrade.",
2933
+ resolution: "Update @cross-deck/web to at least the version in error.minVersion and redeploy \u2014 the parked queue backfills automatically. See https://cross-deck.com/docs/sdk-event-durability/.",
2934
+ retryable: false
2811
2935
  }
2812
2936
  ]);
2813
2937
  function getErrorCode(code) {
@@ -2830,15 +2954,6 @@ var VerifierReporter = class {
2830
2954
  constructor(ctx) {
2831
2955
  this.ctx = ctx;
2832
2956
  this.reentrancyDepth = 0;
2833
- /** The live verifier set — attached by the SDK right after init so the
2834
- * emit path can schema-lock its OWN outgoing telemetry against the real
2835
- * payload. Null until attached (boot-time reports still work without it). */
2836
- this.verifiers = null;
2837
- }
2838
- /** Hand the reporter the live verifier set so `reportFail` can run the
2839
- * `onReportContractFailure` hook on the exact payload it's about to emit. */
2840
- attachVerifiers(verifiers) {
2841
- this.verifiers = verifiers;
2842
2957
  }
2843
2958
  /**
2844
2959
  * Report a verifier result. Pass `operation` for hot-path results
@@ -2867,7 +2982,7 @@ var VerifierReporter = class {
2867
2982
  if (this.reentrancyDepth > 0) return;
2868
2983
  this.reentrancyDepth += 1;
2869
2984
  try {
2870
- const payload = {
2985
+ this.ctx.emitTelemetry({
2871
2986
  contract_id: result.contractId,
2872
2987
  sdk_version: this.ctx.sdkVersion,
2873
2988
  sdk_platform: "web",
@@ -2875,13 +2990,7 @@ var VerifierReporter = class {
2875
2990
  run_context: this.ctx.runContext,
2876
2991
  run_id: this.ctx.runId,
2877
2992
  verification_phase: phase
2878
- };
2879
- if (this.verifiers) {
2880
- runOnReportContractFailure(this.verifiers, this, this.ctx, {
2881
- outgoingPayload: payload
2882
- });
2883
- }
2884
- this.ctx.emitTelemetry(payload);
2993
+ });
2885
2994
  } finally {
2886
2995
  this.reentrancyDepth -= 1;
2887
2996
  }
@@ -3171,31 +3280,9 @@ var VERIFIER_FLUSH_INTERVAL_PARITY = {
3171
3280
  "eventFlushIntervalMs default = 2000ms (Web/Node/RN/Swift/Android parity)",
3172
3281
  nowMs() - t0
3173
3282
  );
3174
- },
3175
- // Hot-path hook — on every real track() we re-affirm the LIVE configured
3176
- // flush interval (carried on the observation), so the parity guarantee is
3177
- // verified against the running SDK in the customer flow, not just a
3178
- // canonical constant at boot. It can't change per-event, but observing it
3179
- // per-event is what makes the contract genuinely runtime-verified.
3180
- hooks: {
3181
- onTrack(obs) {
3182
- const t0 = nowMs();
3183
- const CANONICAL_DEFAULT_MS = 2e3;
3184
- const ms = obs.flushIntervalMs;
3185
- if (typeof ms !== "number" || ms < 100 || ms > 6e4) {
3186
- return fail(
3187
- "flush-interval-parity",
3188
- `live eventFlushIntervalMs=${ms} outside reasonable bounds [100, 60000]`,
3189
- nowMs() - t0
3190
- );
3191
- }
3192
- return pass(
3193
- "flush-interval-parity",
3194
- ms === CANONICAL_DEFAULT_MS ? "live eventFlushIntervalMs = 2000ms (canonical parity value)" : `live eventFlushIntervalMs = ${ms}ms (permitted override; canonical default 2000ms)`,
3195
- nowMs() - t0
3196
- );
3197
- }
3198
3283
  }
3284
+ // No hot-path hook — the flush interval is set once at start() and
3285
+ // never changes per-operation.
3199
3286
  };
3200
3287
  function buildFlushIntervalVerifier(configuredIntervalMs) {
3201
3288
  return {
@@ -3322,26 +3409,6 @@ var CONTRACT_FAILED_FORBIDDEN_FIELDS = [
3322
3409
  "referrer",
3323
3410
  "deviceId"
3324
3411
  ];
3325
- function contractFailedSchemaViolation(keys) {
3326
- const allowed = /* @__PURE__ */ new Set([
3327
- ...CONTRACT_FAILED_REQUIRED_FIELDS,
3328
- ...CONTRACT_FAILED_OPTIONAL_FIELDS
3329
- ]);
3330
- const forbidden = new Set(CONTRACT_FAILED_FORBIDDEN_FIELDS);
3331
- for (const required of CONTRACT_FAILED_REQUIRED_FIELDS) {
3332
- if (!keys.includes(required)) return `missing required field: ${required}`;
3333
- }
3334
- for (const k of keys) {
3335
- if (forbidden.has(k)) return `forbidden field on wire: ${k}`;
3336
- }
3337
- for (const k of keys) {
3338
- if (!allowed.has(k)) {
3339
- return `unrecognised field on wire: ${k} (not in required \u222A optional)`;
3340
- }
3341
- }
3342
- return null;
3343
- }
3344
- var CONTRACT_FAILED_OK_EVIDENCE = `fields \u2286 required(${CONTRACT_FAILED_REQUIRED_FIELDS.length}) \u222A optional(${CONTRACT_FAILED_OPTIONAL_FIELDS.length}); ${CONTRACT_FAILED_FORBIDDEN_FIELDS.length} forbidden absent`;
3345
3412
  var VERIFIER_CONTRACT_FAILED_PAYLOAD_SCHEMA_LOCK = {
3346
3413
  contractId: "contract-failed-payload-schema-lock",
3347
3414
  bootTest() {
@@ -3356,35 +3423,43 @@ var VERIFIER_CONTRACT_FAILED_PAYLOAD_SCHEMA_LOCK = {
3356
3423
  verification_phase: "boot"
3357
3424
  };
3358
3425
  const keys = Object.keys(syntheticPayload);
3359
- const violation = contractFailedSchemaViolation(keys);
3360
- if (violation) {
3361
- return fail("contract-failed-payload-schema-lock", violation, nowMs() - t0);
3426
+ const allowed = /* @__PURE__ */ new Set([
3427
+ ...CONTRACT_FAILED_REQUIRED_FIELDS,
3428
+ ...CONTRACT_FAILED_OPTIONAL_FIELDS
3429
+ ]);
3430
+ const forbidden = new Set(CONTRACT_FAILED_FORBIDDEN_FIELDS);
3431
+ for (const required of CONTRACT_FAILED_REQUIRED_FIELDS) {
3432
+ if (!keys.includes(required)) {
3433
+ return fail(
3434
+ "contract-failed-payload-schema-lock",
3435
+ `missing required field: ${required}`,
3436
+ nowMs() - t0
3437
+ );
3438
+ }
3439
+ }
3440
+ for (const k of keys) {
3441
+ if (forbidden.has(k)) {
3442
+ return fail(
3443
+ "contract-failed-payload-schema-lock",
3444
+ `forbidden field on wire: ${k}`,
3445
+ nowMs() - t0
3446
+ );
3447
+ }
3448
+ }
3449
+ for (const k of keys) {
3450
+ if (!allowed.has(k)) {
3451
+ return fail(
3452
+ "contract-failed-payload-schema-lock",
3453
+ `unrecognised field on wire: ${k} (not in required \u222A optional)`,
3454
+ nowMs() - t0
3455
+ );
3456
+ }
3362
3457
  }
3363
3458
  return pass(
3364
3459
  "contract-failed-payload-schema-lock",
3365
- `${keys.length} ${CONTRACT_FAILED_OK_EVIDENCE}`,
3460
+ `${keys.length} fields \u2286 required(${CONTRACT_FAILED_REQUIRED_FIELDS.length}) \u222A optional(${CONTRACT_FAILED_OPTIONAL_FIELDS.length}); ${CONTRACT_FAILED_FORBIDDEN_FIELDS.length} forbidden absent`,
3366
3461
  nowMs() - t0
3367
3462
  );
3368
- },
3369
- // Hot-path hook — fires on the REAL reportFail emit (VerifierReporter,
3370
- // inside the re-entrancy guard) against the exact payload about to go on
3371
- // the wire. This is the assertion the guard at reportFail was built in
3372
- // anticipation of: every reliability-channel write is schema-locked in the
3373
- // field, against real data, not just the synthetic boot mirror.
3374
- hooks: {
3375
- onReportContractFailure(obs) {
3376
- const t0 = nowMs();
3377
- const keys = Object.keys(obs.outgoingPayload);
3378
- const violation = contractFailedSchemaViolation(keys);
3379
- if (violation) {
3380
- return fail("contract-failed-payload-schema-lock", violation, nowMs() - t0);
3381
- }
3382
- return pass(
3383
- "contract-failed-payload-schema-lock",
3384
- `${keys.length} ${CONTRACT_FAILED_OK_EVIDENCE}`,
3385
- nowMs() - t0
3386
- );
3387
- }
3388
3463
  }
3389
3464
  };
3390
3465
  var BACKEND_WIRE_CODES = Object.freeze([
@@ -3402,7 +3477,8 @@ var BACKEND_WIRE_CODES = Object.freeze([
3402
3477
  "google_not_supported",
3403
3478
  "stripe_not_supported",
3404
3479
  "missing_required_param",
3405
- "invalid_param_value"
3480
+ "invalid_param_value",
3481
+ "sdk_version_unsupported"
3406
3482
  ]);
3407
3483
  var VERIFIER_SDK_ERROR_CODES_CATALOGUE = {
3408
3484
  contractId: "sdk-error-codes-catalogue",
@@ -3435,39 +3511,6 @@ var VERIFIER_SDK_ERROR_CODES_CATALOGUE = {
3435
3511
  nowMs() - t0
3436
3512
  );
3437
3513
  }
3438
- },
3439
- // Hot-path hook — when the SDK parses a real wire error, assert the code
3440
- // the customer actually hit carries usable remediation in the shipped
3441
- // catalogue. Scoped to BACKEND_WIRE_CODES (the contract's set); other
3442
- // codes are out of scope and pass. This turns the catalogue from a
3443
- // boot-only completeness claim into a per-error field assertion: a backend
3444
- // code that ships without remediation is caught the first time a customer
3445
- // hits it, not only if the off-by-default boot self-test ran.
3446
- hooks: {
3447
- onErrorParse(obs) {
3448
- const t0 = nowMs();
3449
- const code = obs.errorCode;
3450
- if (!code || !BACKEND_WIRE_CODES.includes(code)) {
3451
- return pass(
3452
- "sdk-error-codes-catalogue",
3453
- `wire code "${code || "(none)"}" is out of the backend-catalogue scope`,
3454
- nowMs() - t0
3455
- );
3456
- }
3457
- const entry = getErrorCode(code);
3458
- if (!entry || !entry.description || entry.description.trim().length === 0 || !entry.resolution || entry.resolution.trim().length === 0) {
3459
- return fail(
3460
- "sdk-error-codes-catalogue",
3461
- `backend wire code "${code}" hit in the field has no description+resolution in the shipped catalogue`,
3462
- nowMs() - t0
3463
- );
3464
- }
3465
- return pass(
3466
- "sdk-error-codes-catalogue",
3467
- `backend wire code "${code}" carries description + resolution`,
3468
- nowMs() - t0
3469
- );
3470
- }
3471
3514
  }
3472
3515
  };
3473
3516
  var STATIC_VERIFIERS = Object.freeze([
@@ -3591,24 +3634,6 @@ function runOnErrorParse(verifiers, reporter, ctx, obs) {
3591
3634
  reporter.report(result, "hot_path", "errorParse");
3592
3635
  }
3593
3636
  }
3594
- function runOnReportContractFailure(verifiers, reporter, ctx, obs) {
3595
- if (ctx.disableContractAssertions) return;
3596
- for (const verifier of verifiers) {
3597
- const hook = verifier.hooks?.onReportContractFailure;
3598
- if (!hook) continue;
3599
- let result;
3600
- try {
3601
- result = hook(obs);
3602
- } catch (err) {
3603
- result = fail(
3604
- verifier.contractId,
3605
- `hook threw: ${err.message?.slice(0, 80) ?? "unknown"}`,
3606
- 0
3607
- );
3608
- }
3609
- reporter.report(result, "hot_path", "reportContractFailure");
3610
- }
3611
- }
3612
3637
  function defaultDebugModeFlag() {
3613
3638
  try {
3614
3639
  if (typeof process !== "undefined" && process.env) {
@@ -3760,18 +3785,18 @@ function isInAppFrame(filename) {
3760
3785
  if (/\/crossdeck\.umd\.min\.js$/.test(filename)) return false;
3761
3786
  return true;
3762
3787
  }
3763
- function fingerprintError(message, frames, location) {
3788
+ function fingerprintError(message, frames, location2) {
3764
3789
  const inAppFrames = frames.filter((f) => f.in_app).slice(0, 3);
3765
3790
  const parts = [
3766
3791
  (message || "").slice(0, 200),
3767
3792
  ...inAppFrames.map((f) => `${f.function}@${f.filename}:${f.lineno}`)
3768
3793
  ];
3769
- if (inAppFrames.length === 0 && location) {
3794
+ if (inAppFrames.length === 0 && location2) {
3770
3795
  const loc = [
3771
- location.errorType ?? "",
3772
- location.filename ?? "",
3773
- location.lineno ?? "",
3774
- location.colno ?? ""
3796
+ location2.errorType ?? "",
3797
+ location2.filename ?? "",
3798
+ location2.lineno ?? "",
3799
+ location2.colno ?? ""
3775
3800
  ].join(":");
3776
3801
  if (loc !== ":::") parts.push(loc);
3777
3802
  }
@@ -4441,9 +4466,6 @@ var CrossdeckClient = class {
4441
4466
  this.verifiers = null;
4442
4467
  this.verifierReporter = null;
4443
4468
  this.verifierCtx = null;
4444
- // The live configured event-flush interval, surfaced to the track-path
4445
- // verifier so flush-interval-parity validates the real cadence per event.
4446
- this.flushIntervalMs = 2e3;
4447
4469
  }
4448
4470
  /**
4449
4471
  * Boot the SDK. Idempotent — calling init twice with the same options
@@ -4613,6 +4635,13 @@ var CrossdeckClient = class {
4613
4635
  headline,
4614
4636
  { ...info }
4615
4637
  );
4638
+ },
4639
+ onParked: (info) => {
4640
+ debug.emit(
4641
+ "sdk.parked",
4642
+ `[crossdeck] SDK parked \u2014 server no longer accepts this version's event format. Events held on-device (paused, not lost); update @cross-deck/web${info.minVersion ? ` to >= ${info.minVersion}` : ""} to resume.`,
4643
+ { ...info }
4644
+ );
4616
4645
  }
4617
4646
  });
4618
4647
  const deviceInfo = autoTrack.deviceInfo ? collectDeviceInfo({ appVersion: opts.appVersion ?? void 0 }) : opts.appVersion ? { appVersion: opts.appVersion } : {};
@@ -4620,6 +4649,7 @@ var CrossdeckClient = class {
4620
4649
  persistIdentity ? effectiveStorage : new MemoryStorage(),
4621
4650
  opts.storagePrefix
4622
4651
  );
4652
+ processInternalOptOutUrl();
4623
4653
  const consent = new ConsentManager({ respectDnt: options.respectDnt === true });
4624
4654
  if (consent.isDntDenied) {
4625
4655
  debug.emit(
@@ -4723,8 +4753,6 @@ var CrossdeckClient = class {
4723
4753
  this.verifierReporter = new VerifierReporter(this.verifierCtx);
4724
4754
  const flushVerifier = buildFlushIntervalVerifier(opts.eventFlushIntervalMs);
4725
4755
  this.verifiers = Object.freeze([...STATIC_VERIFIERS, flushVerifier]);
4726
- this.flushIntervalMs = opts.eventFlushIntervalMs;
4727
- this.verifierReporter.attachVerifiers(this.verifiers);
4728
4756
  if (localDevMode) {
4729
4757
  const verifyAtBootLocal = options.verifyContractsAtBoot ?? debugDefault;
4730
4758
  if (verifyAtBootLocal && this.verifierReporter) {
@@ -5275,8 +5303,27 @@ var CrossdeckClient = class {
5275
5303
  );
5276
5304
  }
5277
5305
  }
5306
+ const seq = s.autoTracker?.nextSeq() ?? 0;
5307
+ const occurrenceTimestamp = Date.now();
5278
5308
  s.autoTracker?.markActivity();
5279
- const enriched = { ...s.deviceInfo };
5309
+ const wireContext = {};
5310
+ const di = s.deviceInfo;
5311
+ if (di.os) wireContext.os = di.os;
5312
+ if (di.osVersion) wireContext.osVersion = di.osVersion;
5313
+ const appVer = s.options.appVersion;
5314
+ if (appVer) wireContext.appVersion = appVer;
5315
+ wireContext.sdkName = SDK_NAME;
5316
+ wireContext.sdkVersion = s.options.sdkVersion;
5317
+ if (di.locale) wireContext.locale = di.locale;
5318
+ if (di.timezone) wireContext.timezone = di.timezone;
5319
+ if (di.browser) wireContext.browser = di.browser;
5320
+ if (di.browserVersion) wireContext.browserVersion = di.browserVersion;
5321
+ const enriched = {};
5322
+ if (di.screenWidth !== void 0) enriched.screenWidth = di.screenWidth;
5323
+ if (di.screenHeight !== void 0) enriched.screenHeight = di.screenHeight;
5324
+ if (di.viewportWidth !== void 0) enriched.viewportWidth = di.viewportWidth;
5325
+ if (di.viewportHeight !== void 0) enriched.viewportHeight = di.viewportHeight;
5326
+ if (di.devicePixelRatio !== void 0) enriched.devicePixelRatio = di.devicePixelRatio;
5280
5327
  const sessionId = s.autoTracker?.currentSessionId;
5281
5328
  if (sessionId) enriched.sessionId = sessionId;
5282
5329
  const pageviewId = s.autoTracker?.currentPageviewId;
@@ -5299,17 +5346,24 @@ var CrossdeckClient = class {
5299
5346
  }
5300
5347
  }
5301
5348
  const supers = s.superProps.getSuperProperties();
5302
- Object.assign(enriched, supers);
5349
+ for (const k of Object.keys(supers)) {
5350
+ if (!(k in enriched)) enriched[k] = supers[k];
5351
+ }
5303
5352
  const groupIds = s.superProps.getGroupIds();
5304
5353
  if (Object.keys(groupIds).length > 0) {
5305
5354
  enriched.$groups = groupIds;
5306
5355
  }
5307
5356
  Object.assign(enriched, validation.properties);
5357
+ if (isInternalOptOut()) {
5358
+ enriched[INTERNAL_OPT_OUT_PROPERTY] = true;
5359
+ }
5308
5360
  const finalProperties = s.scrubPii ? scrubPiiFromProperties(enriched) : enriched;
5309
5361
  const event = {
5310
5362
  eventId: this.mintEventId(),
5311
5363
  name,
5312
- timestamp: Date.now(),
5364
+ timestamp: occurrenceTimestamp,
5365
+ seq,
5366
+ context: wireContext,
5313
5367
  properties: finalProperties
5314
5368
  };
5315
5369
  Object.assign(event, this.identityHintForEvent());
@@ -5319,8 +5373,7 @@ var CrossdeckClient = class {
5319
5373
  callerProperties: validation.properties,
5320
5374
  superProperties: supers,
5321
5375
  deviceProperties: s.deviceInfo,
5322
- mergedProperties: enriched,
5323
- flushIntervalMs: this.flushIntervalMs
5376
+ mergedProperties: enriched
5324
5377
  });
5325
5378
  }
5326
5379
  if (!isError && !isWebVital) {
@@ -5665,8 +5718,8 @@ function installUnloadFlush(onUnload) {
5665
5718
  }
5666
5719
 
5667
5720
  // src/_contracts-bundled.ts
5668
- var BUNDLED_IN = "@cross-deck/web@1.6.4";
5669
- var SDK_VERSION2 = "1.6.4";
5721
+ var BUNDLED_IN = "@cross-deck/web@1.8.0";
5722
+ var SDK_VERSION2 = "1.8.0";
5670
5723
  var BUNDLED_CONTRACTS = Object.freeze([
5671
5724
  {
5672
5725
  "id": "contract-failed-payload-schema-lock",
@@ -5788,7 +5841,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
5788
5841
  "legal/security/index.html#diagnostic",
5789
5842
  "legal/sdk-data/index.html#b-diagnostic"
5790
5843
  ],
5791
- "bundledIn": "@cross-deck/web@1.6.4",
5844
+ "bundledIn": "@cross-deck/web@1.8.0",
5792
5845
  "runtimeVerified": true
5793
5846
  },
5794
5847
  {
@@ -5828,7 +5881,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
5828
5881
  ],
5829
5882
  "registeredAt": "2026-05-26",
5830
5883
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 8 (codifies existing contract)",
5831
- "bundledIn": "@cross-deck/web@1.6.4",
5884
+ "bundledIn": "@cross-deck/web@1.8.0",
5832
5885
  "runtimeVerified": true
5833
5886
  },
5834
5887
  {
@@ -5874,7 +5927,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
5874
5927
  ],
5875
5928
  "registeredAt": "2026-05-26",
5876
5929
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 3.3",
5877
- "bundledIn": "@cross-deck/web@1.6.4",
5930
+ "bundledIn": "@cross-deck/web@1.8.0",
5878
5931
  "runtimeVerified": true
5879
5932
  },
5880
5933
  {
@@ -5980,7 +6033,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
5980
6033
  ],
5981
6034
  "registeredAt": "2026-05-26",
5982
6035
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 2.2.a + 2.2.b + 2.2.c",
5983
- "bundledIn": "@cross-deck/web@1.6.4",
6036
+ "bundledIn": "@cross-deck/web@1.8.0",
5984
6037
  "runtimeVerified": true
5985
6038
  },
5986
6039
  {
@@ -6008,7 +6061,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
6008
6061
  ],
6009
6062
  "registeredAt": "2026-05-26",
6010
6063
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 5.5",
6011
- "bundledIn": "@cross-deck/web@1.6.4",
6064
+ "bundledIn": "@cross-deck/web@1.8.0",
6012
6065
  "runtimeVerified": false
6013
6066
  },
6014
6067
  {
@@ -6088,7 +6141,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
6088
6141
  ],
6089
6142
  "registeredAt": "2026-05-26",
6090
6143
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 1.3 (web/RN) + dogfood-gap fix (swift + android)",
6091
- "bundledIn": "@cross-deck/web@1.6.4",
6144
+ "bundledIn": "@cross-deck/web@1.8.0",
6092
6145
  "runtimeVerified": true
6093
6146
  },
6094
6147
  {
@@ -6134,7 +6187,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
6134
6187
  ],
6135
6188
  "registeredAt": "2026-05-26",
6136
6189
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 6.2",
6137
- "bundledIn": "@cross-deck/web@1.6.4",
6190
+ "bundledIn": "@cross-deck/web@1.8.0",
6138
6191
  "runtimeVerified": true
6139
6192
  },
6140
6193
  {
@@ -6176,7 +6229,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
6176
6229
  ],
6177
6230
  "registeredAt": "2026-05-26",
6178
6231
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 3.2",
6179
- "bundledIn": "@cross-deck/web@1.6.4",
6232
+ "bundledIn": "@cross-deck/web@1.8.0",
6180
6233
  "runtimeVerified": true
6181
6234
  },
6182
6235
  {
@@ -6210,7 +6263,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
6210
6263
  ],
6211
6264
  "registeredAt": "2026-05-26",
6212
6265
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 3.5",
6213
- "bundledIn": "@cross-deck/web@1.6.4",
6266
+ "bundledIn": "@cross-deck/web@1.8.0",
6214
6267
  "runtimeVerified": false
6215
6268
  }
6216
6269
  ]);