@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.mjs CHANGED
@@ -67,7 +67,7 @@ function typeMapForStatus(status) {
67
67
  }
68
68
 
69
69
  // src/_version.ts
70
- var SDK_VERSION = "1.6.3";
70
+ var SDK_VERSION = "1.6.4";
71
71
  var SDK_NAME = "@cross-deck/web";
72
72
 
73
73
  // src/http.ts
@@ -2833,6 +2833,15 @@ var VerifierReporter = class {
2833
2833
  constructor(ctx) {
2834
2834
  this.ctx = ctx;
2835
2835
  this.reentrancyDepth = 0;
2836
+ /** The live verifier set — attached by the SDK right after init so the
2837
+ * emit path can schema-lock its OWN outgoing telemetry against the real
2838
+ * payload. Null until attached (boot-time reports still work without it). */
2839
+ this.verifiers = null;
2840
+ }
2841
+ /** Hand the reporter the live verifier set so `reportFail` can run the
2842
+ * `onReportContractFailure` hook on the exact payload it's about to emit. */
2843
+ attachVerifiers(verifiers) {
2844
+ this.verifiers = verifiers;
2836
2845
  }
2837
2846
  /**
2838
2847
  * Report a verifier result. Pass `operation` for hot-path results
@@ -2861,7 +2870,7 @@ var VerifierReporter = class {
2861
2870
  if (this.reentrancyDepth > 0) return;
2862
2871
  this.reentrancyDepth += 1;
2863
2872
  try {
2864
- this.ctx.emitTelemetry({
2873
+ const payload = {
2865
2874
  contract_id: result.contractId,
2866
2875
  sdk_version: this.ctx.sdkVersion,
2867
2876
  sdk_platform: "web",
@@ -2869,7 +2878,13 @@ var VerifierReporter = class {
2869
2878
  run_context: this.ctx.runContext,
2870
2879
  run_id: this.ctx.runId,
2871
2880
  verification_phase: phase
2872
- });
2881
+ };
2882
+ if (this.verifiers) {
2883
+ runOnReportContractFailure(this.verifiers, this, this.ctx, {
2884
+ outgoingPayload: payload
2885
+ });
2886
+ }
2887
+ this.ctx.emitTelemetry(payload);
2873
2888
  } finally {
2874
2889
  this.reentrancyDepth -= 1;
2875
2890
  }
@@ -3159,9 +3174,31 @@ var VERIFIER_FLUSH_INTERVAL_PARITY = {
3159
3174
  "eventFlushIntervalMs default = 2000ms (Web/Node/RN/Swift/Android parity)",
3160
3175
  nowMs() - t0
3161
3176
  );
3177
+ },
3178
+ // Hot-path hook — on every real track() we re-affirm the LIVE configured
3179
+ // flush interval (carried on the observation), so the parity guarantee is
3180
+ // verified against the running SDK in the customer flow, not just a
3181
+ // canonical constant at boot. It can't change per-event, but observing it
3182
+ // per-event is what makes the contract genuinely runtime-verified.
3183
+ hooks: {
3184
+ onTrack(obs) {
3185
+ const t0 = nowMs();
3186
+ const CANONICAL_DEFAULT_MS = 2e3;
3187
+ const ms = obs.flushIntervalMs;
3188
+ if (typeof ms !== "number" || ms < 100 || ms > 6e4) {
3189
+ return fail(
3190
+ "flush-interval-parity",
3191
+ `live eventFlushIntervalMs=${ms} outside reasonable bounds [100, 60000]`,
3192
+ nowMs() - t0
3193
+ );
3194
+ }
3195
+ return pass(
3196
+ "flush-interval-parity",
3197
+ ms === CANONICAL_DEFAULT_MS ? "live eventFlushIntervalMs = 2000ms (canonical parity value)" : `live eventFlushIntervalMs = ${ms}ms (permitted override; canonical default 2000ms)`,
3198
+ nowMs() - t0
3199
+ );
3200
+ }
3162
3201
  }
3163
- // No hot-path hook — the flush interval is set once at start() and
3164
- // never changes per-operation.
3165
3202
  };
3166
3203
  function buildFlushIntervalVerifier(configuredIntervalMs) {
3167
3204
  return {
@@ -3288,6 +3325,26 @@ var CONTRACT_FAILED_FORBIDDEN_FIELDS = [
3288
3325
  "referrer",
3289
3326
  "deviceId"
3290
3327
  ];
3328
+ function contractFailedSchemaViolation(keys) {
3329
+ const allowed = /* @__PURE__ */ new Set([
3330
+ ...CONTRACT_FAILED_REQUIRED_FIELDS,
3331
+ ...CONTRACT_FAILED_OPTIONAL_FIELDS
3332
+ ]);
3333
+ const forbidden = new Set(CONTRACT_FAILED_FORBIDDEN_FIELDS);
3334
+ for (const required of CONTRACT_FAILED_REQUIRED_FIELDS) {
3335
+ if (!keys.includes(required)) return `missing required field: ${required}`;
3336
+ }
3337
+ for (const k of keys) {
3338
+ if (forbidden.has(k)) return `forbidden field on wire: ${k}`;
3339
+ }
3340
+ for (const k of keys) {
3341
+ if (!allowed.has(k)) {
3342
+ return `unrecognised field on wire: ${k} (not in required \u222A optional)`;
3343
+ }
3344
+ }
3345
+ return null;
3346
+ }
3347
+ 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`;
3291
3348
  var VERIFIER_CONTRACT_FAILED_PAYLOAD_SCHEMA_LOCK = {
3292
3349
  contractId: "contract-failed-payload-schema-lock",
3293
3350
  bootTest() {
@@ -3302,43 +3359,35 @@ var VERIFIER_CONTRACT_FAILED_PAYLOAD_SCHEMA_LOCK = {
3302
3359
  verification_phase: "boot"
3303
3360
  };
3304
3361
  const keys = Object.keys(syntheticPayload);
3305
- const allowed = /* @__PURE__ */ new Set([
3306
- ...CONTRACT_FAILED_REQUIRED_FIELDS,
3307
- ...CONTRACT_FAILED_OPTIONAL_FIELDS
3308
- ]);
3309
- const forbidden = new Set(CONTRACT_FAILED_FORBIDDEN_FIELDS);
3310
- for (const required of CONTRACT_FAILED_REQUIRED_FIELDS) {
3311
- if (!keys.includes(required)) {
3312
- return fail(
3313
- "contract-failed-payload-schema-lock",
3314
- `missing required field: ${required}`,
3315
- nowMs() - t0
3316
- );
3317
- }
3318
- }
3319
- for (const k of keys) {
3320
- if (forbidden.has(k)) {
3321
- return fail(
3322
- "contract-failed-payload-schema-lock",
3323
- `forbidden field on wire: ${k}`,
3324
- nowMs() - t0
3325
- );
3326
- }
3327
- }
3328
- for (const k of keys) {
3329
- if (!allowed.has(k)) {
3330
- return fail(
3331
- "contract-failed-payload-schema-lock",
3332
- `unrecognised field on wire: ${k} (not in required \u222A optional)`,
3333
- nowMs() - t0
3334
- );
3335
- }
3362
+ const violation = contractFailedSchemaViolation(keys);
3363
+ if (violation) {
3364
+ return fail("contract-failed-payload-schema-lock", violation, nowMs() - t0);
3336
3365
  }
3337
3366
  return pass(
3338
3367
  "contract-failed-payload-schema-lock",
3339
- `${keys.length} fields \u2286 required(${CONTRACT_FAILED_REQUIRED_FIELDS.length}) \u222A optional(${CONTRACT_FAILED_OPTIONAL_FIELDS.length}); ${CONTRACT_FAILED_FORBIDDEN_FIELDS.length} forbidden absent`,
3368
+ `${keys.length} ${CONTRACT_FAILED_OK_EVIDENCE}`,
3340
3369
  nowMs() - t0
3341
3370
  );
3371
+ },
3372
+ // Hot-path hook — fires on the REAL reportFail emit (VerifierReporter,
3373
+ // inside the re-entrancy guard) against the exact payload about to go on
3374
+ // the wire. This is the assertion the guard at reportFail was built in
3375
+ // anticipation of: every reliability-channel write is schema-locked in the
3376
+ // field, against real data, not just the synthetic boot mirror.
3377
+ hooks: {
3378
+ onReportContractFailure(obs) {
3379
+ const t0 = nowMs();
3380
+ const keys = Object.keys(obs.outgoingPayload);
3381
+ const violation = contractFailedSchemaViolation(keys);
3382
+ if (violation) {
3383
+ return fail("contract-failed-payload-schema-lock", violation, nowMs() - t0);
3384
+ }
3385
+ return pass(
3386
+ "contract-failed-payload-schema-lock",
3387
+ `${keys.length} ${CONTRACT_FAILED_OK_EVIDENCE}`,
3388
+ nowMs() - t0
3389
+ );
3390
+ }
3342
3391
  }
3343
3392
  };
3344
3393
  var BACKEND_WIRE_CODES = Object.freeze([
@@ -3389,6 +3438,39 @@ var VERIFIER_SDK_ERROR_CODES_CATALOGUE = {
3389
3438
  nowMs() - t0
3390
3439
  );
3391
3440
  }
3441
+ },
3442
+ // Hot-path hook — when the SDK parses a real wire error, assert the code
3443
+ // the customer actually hit carries usable remediation in the shipped
3444
+ // catalogue. Scoped to BACKEND_WIRE_CODES (the contract's set); other
3445
+ // codes are out of scope and pass. This turns the catalogue from a
3446
+ // boot-only completeness claim into a per-error field assertion: a backend
3447
+ // code that ships without remediation is caught the first time a customer
3448
+ // hits it, not only if the off-by-default boot self-test ran.
3449
+ hooks: {
3450
+ onErrorParse(obs) {
3451
+ const t0 = nowMs();
3452
+ const code = obs.errorCode;
3453
+ if (!code || !BACKEND_WIRE_CODES.includes(code)) {
3454
+ return pass(
3455
+ "sdk-error-codes-catalogue",
3456
+ `wire code "${code || "(none)"}" is out of the backend-catalogue scope`,
3457
+ nowMs() - t0
3458
+ );
3459
+ }
3460
+ const entry = getErrorCode(code);
3461
+ if (!entry || !entry.description || entry.description.trim().length === 0 || !entry.resolution || entry.resolution.trim().length === 0) {
3462
+ return fail(
3463
+ "sdk-error-codes-catalogue",
3464
+ `backend wire code "${code}" hit in the field has no description+resolution in the shipped catalogue`,
3465
+ nowMs() - t0
3466
+ );
3467
+ }
3468
+ return pass(
3469
+ "sdk-error-codes-catalogue",
3470
+ `backend wire code "${code}" carries description + resolution`,
3471
+ nowMs() - t0
3472
+ );
3473
+ }
3392
3474
  }
3393
3475
  };
3394
3476
  var STATIC_VERIFIERS = Object.freeze([
@@ -3512,6 +3594,24 @@ function runOnErrorParse(verifiers, reporter, ctx, obs) {
3512
3594
  reporter.report(result, "hot_path", "errorParse");
3513
3595
  }
3514
3596
  }
3597
+ function runOnReportContractFailure(verifiers, reporter, ctx, obs) {
3598
+ if (ctx.disableContractAssertions) return;
3599
+ for (const verifier of verifiers) {
3600
+ const hook = verifier.hooks?.onReportContractFailure;
3601
+ if (!hook) continue;
3602
+ let result;
3603
+ try {
3604
+ result = hook(obs);
3605
+ } catch (err) {
3606
+ result = fail(
3607
+ verifier.contractId,
3608
+ `hook threw: ${err.message?.slice(0, 80) ?? "unknown"}`,
3609
+ 0
3610
+ );
3611
+ }
3612
+ reporter.report(result, "hot_path", "reportContractFailure");
3613
+ }
3614
+ }
3515
3615
  function defaultDebugModeFlag() {
3516
3616
  try {
3517
3617
  if (typeof process !== "undefined" && process.env) {
@@ -4344,6 +4444,9 @@ var CrossdeckClient = class {
4344
4444
  this.verifiers = null;
4345
4445
  this.verifierReporter = null;
4346
4446
  this.verifierCtx = null;
4447
+ // The live configured event-flush interval, surfaced to the track-path
4448
+ // verifier so flush-interval-parity validates the real cadence per event.
4449
+ this.flushIntervalMs = 2e3;
4347
4450
  }
4348
4451
  /**
4349
4452
  * Boot the SDK. Idempotent — calling init twice with the same options
@@ -4623,6 +4726,8 @@ var CrossdeckClient = class {
4623
4726
  this.verifierReporter = new VerifierReporter(this.verifierCtx);
4624
4727
  const flushVerifier = buildFlushIntervalVerifier(opts.eventFlushIntervalMs);
4625
4728
  this.verifiers = Object.freeze([...STATIC_VERIFIERS, flushVerifier]);
4729
+ this.flushIntervalMs = opts.eventFlushIntervalMs;
4730
+ this.verifierReporter.attachVerifiers(this.verifiers);
4626
4731
  if (localDevMode) {
4627
4732
  const verifyAtBootLocal = options.verifyContractsAtBoot ?? debugDefault;
4628
4733
  if (verifyAtBootLocal && this.verifierReporter) {
@@ -5197,9 +5302,7 @@ var CrossdeckClient = class {
5197
5302
  }
5198
5303
  }
5199
5304
  const supers = s.superProps.getSuperProperties();
5200
- for (const k of Object.keys(supers)) {
5201
- if (!(k in enriched)) enriched[k] = supers[k];
5202
- }
5305
+ Object.assign(enriched, supers);
5203
5306
  const groupIds = s.superProps.getGroupIds();
5204
5307
  if (Object.keys(groupIds).length > 0) {
5205
5308
  enriched.$groups = groupIds;
@@ -5219,7 +5322,8 @@ var CrossdeckClient = class {
5219
5322
  callerProperties: validation.properties,
5220
5323
  superProperties: supers,
5221
5324
  deviceProperties: s.deviceInfo,
5222
- mergedProperties: enriched
5325
+ mergedProperties: enriched,
5326
+ flushIntervalMs: this.flushIntervalMs
5223
5327
  });
5224
5328
  }
5225
5329
  if (!isError && !isWebVital) {