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