@cross-deck/web 1.6.3 → 1.6.4

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/vue.cjs CHANGED
@@ -92,7 +92,7 @@ function typeMapForStatus(status) {
92
92
  }
93
93
 
94
94
  // src/_version.ts
95
- var SDK_VERSION = "1.6.3";
95
+ var SDK_VERSION = "1.6.4";
96
96
  var SDK_NAME = "@cross-deck/web";
97
97
 
98
98
  // src/http.ts
@@ -2858,6 +2858,15 @@ var VerifierReporter = class {
2858
2858
  constructor(ctx) {
2859
2859
  this.ctx = ctx;
2860
2860
  this.reentrancyDepth = 0;
2861
+ /** The live verifier set — attached by the SDK right after init so the
2862
+ * emit path can schema-lock its OWN outgoing telemetry against the real
2863
+ * payload. Null until attached (boot-time reports still work without it). */
2864
+ this.verifiers = null;
2865
+ }
2866
+ /** Hand the reporter the live verifier set so `reportFail` can run the
2867
+ * `onReportContractFailure` hook on the exact payload it's about to emit. */
2868
+ attachVerifiers(verifiers) {
2869
+ this.verifiers = verifiers;
2861
2870
  }
2862
2871
  /**
2863
2872
  * Report a verifier result. Pass `operation` for hot-path results
@@ -2886,7 +2895,7 @@ var VerifierReporter = class {
2886
2895
  if (this.reentrancyDepth > 0) return;
2887
2896
  this.reentrancyDepth += 1;
2888
2897
  try {
2889
- this.ctx.emitTelemetry({
2898
+ const payload = {
2890
2899
  contract_id: result.contractId,
2891
2900
  sdk_version: this.ctx.sdkVersion,
2892
2901
  sdk_platform: "web",
@@ -2894,7 +2903,13 @@ var VerifierReporter = class {
2894
2903
  run_context: this.ctx.runContext,
2895
2904
  run_id: this.ctx.runId,
2896
2905
  verification_phase: phase
2897
- });
2906
+ };
2907
+ if (this.verifiers) {
2908
+ runOnReportContractFailure(this.verifiers, this, this.ctx, {
2909
+ outgoingPayload: payload
2910
+ });
2911
+ }
2912
+ this.ctx.emitTelemetry(payload);
2898
2913
  } finally {
2899
2914
  this.reentrancyDepth -= 1;
2900
2915
  }
@@ -3184,9 +3199,31 @@ var VERIFIER_FLUSH_INTERVAL_PARITY = {
3184
3199
  "eventFlushIntervalMs default = 2000ms (Web/Node/RN/Swift/Android parity)",
3185
3200
  nowMs() - t0
3186
3201
  );
3202
+ },
3203
+ // Hot-path hook — on every real track() we re-affirm the LIVE configured
3204
+ // flush interval (carried on the observation), so the parity guarantee is
3205
+ // verified against the running SDK in the customer flow, not just a
3206
+ // canonical constant at boot. It can't change per-event, but observing it
3207
+ // per-event is what makes the contract genuinely runtime-verified.
3208
+ hooks: {
3209
+ onTrack(obs) {
3210
+ const t0 = nowMs();
3211
+ const CANONICAL_DEFAULT_MS = 2e3;
3212
+ const ms = obs.flushIntervalMs;
3213
+ if (typeof ms !== "number" || ms < 100 || ms > 6e4) {
3214
+ return fail(
3215
+ "flush-interval-parity",
3216
+ `live eventFlushIntervalMs=${ms} outside reasonable bounds [100, 60000]`,
3217
+ nowMs() - t0
3218
+ );
3219
+ }
3220
+ return pass(
3221
+ "flush-interval-parity",
3222
+ ms === CANONICAL_DEFAULT_MS ? "live eventFlushIntervalMs = 2000ms (canonical parity value)" : `live eventFlushIntervalMs = ${ms}ms (permitted override; canonical default 2000ms)`,
3223
+ nowMs() - t0
3224
+ );
3225
+ }
3187
3226
  }
3188
- // No hot-path hook — the flush interval is set once at start() and
3189
- // never changes per-operation.
3190
3227
  };
3191
3228
  function buildFlushIntervalVerifier(configuredIntervalMs) {
3192
3229
  return {
@@ -3313,6 +3350,26 @@ var CONTRACT_FAILED_FORBIDDEN_FIELDS = [
3313
3350
  "referrer",
3314
3351
  "deviceId"
3315
3352
  ];
3353
+ function contractFailedSchemaViolation(keys) {
3354
+ const allowed = /* @__PURE__ */ new Set([
3355
+ ...CONTRACT_FAILED_REQUIRED_FIELDS,
3356
+ ...CONTRACT_FAILED_OPTIONAL_FIELDS
3357
+ ]);
3358
+ const forbidden = new Set(CONTRACT_FAILED_FORBIDDEN_FIELDS);
3359
+ for (const required of CONTRACT_FAILED_REQUIRED_FIELDS) {
3360
+ if (!keys.includes(required)) return `missing required field: ${required}`;
3361
+ }
3362
+ for (const k of keys) {
3363
+ if (forbidden.has(k)) return `forbidden field on wire: ${k}`;
3364
+ }
3365
+ for (const k of keys) {
3366
+ if (!allowed.has(k)) {
3367
+ return `unrecognised field on wire: ${k} (not in required \u222A optional)`;
3368
+ }
3369
+ }
3370
+ return null;
3371
+ }
3372
+ 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`;
3316
3373
  var VERIFIER_CONTRACT_FAILED_PAYLOAD_SCHEMA_LOCK = {
3317
3374
  contractId: "contract-failed-payload-schema-lock",
3318
3375
  bootTest() {
@@ -3327,43 +3384,35 @@ var VERIFIER_CONTRACT_FAILED_PAYLOAD_SCHEMA_LOCK = {
3327
3384
  verification_phase: "boot"
3328
3385
  };
3329
3386
  const keys = Object.keys(syntheticPayload);
3330
- const allowed = /* @__PURE__ */ new Set([
3331
- ...CONTRACT_FAILED_REQUIRED_FIELDS,
3332
- ...CONTRACT_FAILED_OPTIONAL_FIELDS
3333
- ]);
3334
- const forbidden = new Set(CONTRACT_FAILED_FORBIDDEN_FIELDS);
3335
- for (const required of CONTRACT_FAILED_REQUIRED_FIELDS) {
3336
- if (!keys.includes(required)) {
3337
- return fail(
3338
- "contract-failed-payload-schema-lock",
3339
- `missing required field: ${required}`,
3340
- nowMs() - t0
3341
- );
3342
- }
3343
- }
3344
- for (const k of keys) {
3345
- if (forbidden.has(k)) {
3346
- return fail(
3347
- "contract-failed-payload-schema-lock",
3348
- `forbidden field on wire: ${k}`,
3349
- nowMs() - t0
3350
- );
3351
- }
3352
- }
3353
- for (const k of keys) {
3354
- if (!allowed.has(k)) {
3355
- return fail(
3356
- "contract-failed-payload-schema-lock",
3357
- `unrecognised field on wire: ${k} (not in required \u222A optional)`,
3358
- nowMs() - t0
3359
- );
3360
- }
3387
+ const violation = contractFailedSchemaViolation(keys);
3388
+ if (violation) {
3389
+ return fail("contract-failed-payload-schema-lock", violation, nowMs() - t0);
3361
3390
  }
3362
3391
  return pass(
3363
3392
  "contract-failed-payload-schema-lock",
3364
- `${keys.length} fields \u2286 required(${CONTRACT_FAILED_REQUIRED_FIELDS.length}) \u222A optional(${CONTRACT_FAILED_OPTIONAL_FIELDS.length}); ${CONTRACT_FAILED_FORBIDDEN_FIELDS.length} forbidden absent`,
3393
+ `${keys.length} ${CONTRACT_FAILED_OK_EVIDENCE}`,
3365
3394
  nowMs() - t0
3366
3395
  );
3396
+ },
3397
+ // Hot-path hook — fires on the REAL reportFail emit (VerifierReporter,
3398
+ // inside the re-entrancy guard) against the exact payload about to go on
3399
+ // the wire. This is the assertion the guard at reportFail was built in
3400
+ // anticipation of: every reliability-channel write is schema-locked in the
3401
+ // field, against real data, not just the synthetic boot mirror.
3402
+ hooks: {
3403
+ onReportContractFailure(obs) {
3404
+ const t0 = nowMs();
3405
+ const keys = Object.keys(obs.outgoingPayload);
3406
+ const violation = contractFailedSchemaViolation(keys);
3407
+ if (violation) {
3408
+ return fail("contract-failed-payload-schema-lock", violation, nowMs() - t0);
3409
+ }
3410
+ return pass(
3411
+ "contract-failed-payload-schema-lock",
3412
+ `${keys.length} ${CONTRACT_FAILED_OK_EVIDENCE}`,
3413
+ nowMs() - t0
3414
+ );
3415
+ }
3367
3416
  }
3368
3417
  };
3369
3418
  var BACKEND_WIRE_CODES = Object.freeze([
@@ -3414,6 +3463,39 @@ var VERIFIER_SDK_ERROR_CODES_CATALOGUE = {
3414
3463
  nowMs() - t0
3415
3464
  );
3416
3465
  }
3466
+ },
3467
+ // Hot-path hook — when the SDK parses a real wire error, assert the code
3468
+ // the customer actually hit carries usable remediation in the shipped
3469
+ // catalogue. Scoped to BACKEND_WIRE_CODES (the contract's set); other
3470
+ // codes are out of scope and pass. This turns the catalogue from a
3471
+ // boot-only completeness claim into a per-error field assertion: a backend
3472
+ // code that ships without remediation is caught the first time a customer
3473
+ // hits it, not only if the off-by-default boot self-test ran.
3474
+ hooks: {
3475
+ onErrorParse(obs) {
3476
+ const t0 = nowMs();
3477
+ const code = obs.errorCode;
3478
+ if (!code || !BACKEND_WIRE_CODES.includes(code)) {
3479
+ return pass(
3480
+ "sdk-error-codes-catalogue",
3481
+ `wire code "${code || "(none)"}" is out of the backend-catalogue scope`,
3482
+ nowMs() - t0
3483
+ );
3484
+ }
3485
+ const entry = getErrorCode(code);
3486
+ if (!entry || !entry.description || entry.description.trim().length === 0 || !entry.resolution || entry.resolution.trim().length === 0) {
3487
+ return fail(
3488
+ "sdk-error-codes-catalogue",
3489
+ `backend wire code "${code}" hit in the field has no description+resolution in the shipped catalogue`,
3490
+ nowMs() - t0
3491
+ );
3492
+ }
3493
+ return pass(
3494
+ "sdk-error-codes-catalogue",
3495
+ `backend wire code "${code}" carries description + resolution`,
3496
+ nowMs() - t0
3497
+ );
3498
+ }
3417
3499
  }
3418
3500
  };
3419
3501
  var STATIC_VERIFIERS = Object.freeze([
@@ -3537,6 +3619,24 @@ function runOnErrorParse(verifiers, reporter, ctx, obs) {
3537
3619
  reporter.report(result, "hot_path", "errorParse");
3538
3620
  }
3539
3621
  }
3622
+ function runOnReportContractFailure(verifiers, reporter, ctx, obs) {
3623
+ if (ctx.disableContractAssertions) return;
3624
+ for (const verifier of verifiers) {
3625
+ const hook = verifier.hooks?.onReportContractFailure;
3626
+ if (!hook) continue;
3627
+ let result;
3628
+ try {
3629
+ result = hook(obs);
3630
+ } catch (err) {
3631
+ result = fail(
3632
+ verifier.contractId,
3633
+ `hook threw: ${err.message?.slice(0, 80) ?? "unknown"}`,
3634
+ 0
3635
+ );
3636
+ }
3637
+ reporter.report(result, "hot_path", "reportContractFailure");
3638
+ }
3639
+ }
3540
3640
  function defaultDebugModeFlag() {
3541
3641
  try {
3542
3642
  if (typeof process !== "undefined" && process.env) {
@@ -4369,6 +4469,9 @@ var CrossdeckClient = class {
4369
4469
  this.verifiers = null;
4370
4470
  this.verifierReporter = null;
4371
4471
  this.verifierCtx = null;
4472
+ // The live configured event-flush interval, surfaced to the track-path
4473
+ // verifier so flush-interval-parity validates the real cadence per event.
4474
+ this.flushIntervalMs = 2e3;
4372
4475
  }
4373
4476
  /**
4374
4477
  * Boot the SDK. Idempotent — calling init twice with the same options
@@ -4648,6 +4751,8 @@ var CrossdeckClient = class {
4648
4751
  this.verifierReporter = new VerifierReporter(this.verifierCtx);
4649
4752
  const flushVerifier = buildFlushIntervalVerifier(opts.eventFlushIntervalMs);
4650
4753
  this.verifiers = Object.freeze([...STATIC_VERIFIERS, flushVerifier]);
4754
+ this.flushIntervalMs = opts.eventFlushIntervalMs;
4755
+ this.verifierReporter.attachVerifiers(this.verifiers);
4651
4756
  if (localDevMode) {
4652
4757
  const verifyAtBootLocal = options.verifyContractsAtBoot ?? debugDefault;
4653
4758
  if (verifyAtBootLocal && this.verifierReporter) {
@@ -5222,9 +5327,7 @@ var CrossdeckClient = class {
5222
5327
  }
5223
5328
  }
5224
5329
  const supers = s.superProps.getSuperProperties();
5225
- for (const k of Object.keys(supers)) {
5226
- if (!(k in enriched)) enriched[k] = supers[k];
5227
- }
5330
+ Object.assign(enriched, supers);
5228
5331
  const groupIds = s.superProps.getGroupIds();
5229
5332
  if (Object.keys(groupIds).length > 0) {
5230
5333
  enriched.$groups = groupIds;
@@ -5244,7 +5347,8 @@ var CrossdeckClient = class {
5244
5347
  callerProperties: validation.properties,
5245
5348
  superProperties: supers,
5246
5349
  deviceProperties: s.deviceInfo,
5247
- mergedProperties: enriched
5350
+ mergedProperties: enriched,
5351
+ flushIntervalMs: this.flushIntervalMs
5248
5352
  });
5249
5353
  }
5250
5354
  if (!isError && !isWebVital) {