@cross-deck/web 1.4.2 → 1.5.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 +74 -0
- package/README.md +94 -0
- package/dist/contracts 2.json +378 -0
- package/dist/contracts.json +378 -0
- package/dist/crossdeck.umd.min 2.js +3 -0
- package/dist/crossdeck.umd.min.js +2 -2
- package/dist/crossdeck.umd.min.js 2.map +1 -0
- package/dist/crossdeck.umd.min.js.map +1 -1
- package/dist/error-codes 2.json +196 -0
- package/dist/error-codes.json +1 -1
- package/dist/index 2.cjs +4778 -0
- package/dist/index 2.mjs +4742 -0
- package/dist/index.cjs +457 -0
- package/dist/index.cjs 2.map +1 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d 2.mts +938 -0
- package/dist/index.d 2.ts +938 -0
- package/dist/index.d.mts +174 -1
- package/dist/index.d.ts +174 -1
- package/dist/index.mjs +456 -0
- package/dist/index.mjs 2.map +1 -0
- package/dist/index.mjs.map +1 -1
- package/dist/react 2.cjs +4220 -0
- package/dist/react 2.mjs +4193 -0
- package/dist/react.cjs +31 -0
- package/dist/react.cjs 2.map +1 -0
- package/dist/react.cjs.map +1 -1
- package/dist/react.d 2.mts +91 -0
- package/dist/react.d 2.ts +91 -0
- package/dist/react.mjs +31 -0
- package/dist/react.mjs 2.map +1 -0
- package/dist/react.mjs.map +1 -1
- package/dist/types-Bu3jbmdq.d 2.mts +314 -0
- package/dist/types-Bu3jbmdq.d 2.ts +314 -0
- package/dist/vue 2.cjs +4185 -0
- package/dist/vue 2.mjs +4159 -0
- package/dist/vue.cjs +31 -0
- package/dist/vue.cjs 2.map +1 -0
- package/dist/vue.cjs.map +1 -1
- package/dist/vue.d 2.mts +37 -0
- package/dist/vue.d 2.ts +37 -0
- package/dist/vue.mjs +31 -0
- package/dist/vue.mjs 2.map +1 -0
- package/dist/vue.mjs.map +1 -1
- package/package.json +3 -2
package/dist/index.mjs
CHANGED
|
@@ -3673,6 +3673,37 @@ var CrossdeckClient = class {
|
|
|
3673
3673
|
* trip happens in the background. To flush before the page unloads,
|
|
3674
3674
|
* call flush().
|
|
3675
3675
|
*/
|
|
3676
|
+
/**
|
|
3677
|
+
* Emit `crossdeck.contract_failed` with the canonical property
|
|
3678
|
+
* shape (`contract_id`, `sdk_version`, `sdk_platform`,
|
|
3679
|
+
* `failure_reason`, `run_context`, `run_id`). Goes through the
|
|
3680
|
+
* standard track() pipeline — same consent gate, same queue,
|
|
3681
|
+
* same ingest, no new endpoint.
|
|
3682
|
+
*
|
|
3683
|
+
* Wire the call from a test hook, dogfood failure path, or
|
|
3684
|
+
* customer contract-verification harness; see
|
|
3685
|
+
* `contracts/README.md` for the per-test-framework hook recipes.
|
|
3686
|
+
*/
|
|
3687
|
+
reportContractFailure(input) {
|
|
3688
|
+
const props = {
|
|
3689
|
+
contract_id: input.contractId,
|
|
3690
|
+
sdk_version: SDK_VERSION,
|
|
3691
|
+
sdk_platform: "web",
|
|
3692
|
+
failure_reason: input.failureReason,
|
|
3693
|
+
run_context: input.runContext,
|
|
3694
|
+
run_id: input.runId
|
|
3695
|
+
};
|
|
3696
|
+
if (input.testRef) {
|
|
3697
|
+
props.test_file = input.testRef.file;
|
|
3698
|
+
props.test_name = input.testRef.name;
|
|
3699
|
+
}
|
|
3700
|
+
if (input.extra) {
|
|
3701
|
+
for (const [k, v] of Object.entries(input.extra)) {
|
|
3702
|
+
if (props[k] === void 0) props[k] = v;
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3705
|
+
this.track("crossdeck.contract_failed", props);
|
|
3706
|
+
}
|
|
3676
3707
|
track(name, properties) {
|
|
3677
3708
|
const s = this.requireStarted();
|
|
3678
3709
|
if (!name) {
|
|
@@ -4272,10 +4303,435 @@ var CROSSDECK_ERROR_CODES = Object.freeze([
|
|
|
4272
4303
|
function getErrorCode(code) {
|
|
4273
4304
|
return CROSSDECK_ERROR_CODES.find((e) => e.code === code);
|
|
4274
4305
|
}
|
|
4306
|
+
|
|
4307
|
+
// src/_contracts-bundled.ts
|
|
4308
|
+
var BUNDLED_IN = "@cross-deck/web@1.5.0";
|
|
4309
|
+
var SDK_VERSION2 = "1.5.0";
|
|
4310
|
+
var BUNDLED_CONTRACTS = Object.freeze([
|
|
4311
|
+
{
|
|
4312
|
+
"id": "error-envelope-shape",
|
|
4313
|
+
"pillar": "errors",
|
|
4314
|
+
"status": "enforced",
|
|
4315
|
+
"claim": "Every v1 REST endpoint returns errors in a Stripe-shape envelope: `{ error: { type, code, message, request_id } }` where `type` is one of authentication_error / permission_error / invalid_request_error / rate_limit_error / internal_error (the wire vocabulary in backend/src/api/v1-errors.ts ApiErrorType). HTTP status parity: invalid_request_error \u2192 400, authentication_error \u2192 401, permission_error \u2192 403, rate_limit_error \u2192 429, internal_error \u2192 500. SDK-side clients parse this shape via `crossdeckErrorFromResponse` (Web/Node/RN) / `crossdeckErrorFrom(response:)` (Swift) / `crossdeckErrorFromResponse` (Android) and surface the request_id verbatim so support traces are end-to-end joinable. Firebase callable endpoints (managed-keys / dashboard auth) use the Firebase HttpsError envelope instead \u2014 this contract applies to REST /v1/* only.",
|
|
4316
|
+
"appliesTo": [
|
|
4317
|
+
"web",
|
|
4318
|
+
"node",
|
|
4319
|
+
"react-native",
|
|
4320
|
+
"swift",
|
|
4321
|
+
"android",
|
|
4322
|
+
"backend"
|
|
4323
|
+
],
|
|
4324
|
+
"codeRef": [
|
|
4325
|
+
"backend/src/api/v1-errors.ts",
|
|
4326
|
+
"sdks/web/src/errors.ts",
|
|
4327
|
+
"sdks/node/src/errors.ts",
|
|
4328
|
+
"sdks/react-native/src/errors.ts",
|
|
4329
|
+
"sdks/swift/Sources/Crossdeck/Errors.swift",
|
|
4330
|
+
"sdks/android/crossdeck/src/main/kotlin/com/crossdeck/Errors.kt"
|
|
4331
|
+
],
|
|
4332
|
+
"testRef": [
|
|
4333
|
+
{
|
|
4334
|
+
"file": "sdks/swift/Tests/CrossdeckTests/ErrorsTests.swift",
|
|
4335
|
+
"name": "test_errorEnvelope_fallsBackOnGarbageBody"
|
|
4336
|
+
},
|
|
4337
|
+
{
|
|
4338
|
+
"file": "sdks/swift/Tests/CrossdeckTests/ErrorsTests.swift",
|
|
4339
|
+
"name": "test_errorEnvelope_reads_XRequestId_fallback"
|
|
4340
|
+
},
|
|
4341
|
+
{
|
|
4342
|
+
"file": "sdks/android/crossdeck/src/test/kotlin/com/crossdeck/ErrorTypeWireVocabTest.kt",
|
|
4343
|
+
"name": "backend 500 response parses to INTERNAL_ERROR"
|
|
4344
|
+
}
|
|
4345
|
+
],
|
|
4346
|
+
"registeredAt": "2026-05-26",
|
|
4347
|
+
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 8 (codifies existing contract)",
|
|
4348
|
+
"bundledIn": "@cross-deck/web@1.5.0"
|
|
4349
|
+
},
|
|
4350
|
+
{
|
|
4351
|
+
"id": "flush-interval-parity",
|
|
4352
|
+
"pillar": "analytics",
|
|
4353
|
+
"status": "enforced",
|
|
4354
|
+
"claim": "Every Crossdeck SDK defaults its event-queue flush interval to 2000ms \u2014 the Stripe-adjacent industry norm. Pre-v1.4.0 the defaults disagreed (Web/Node 1500ms; RN/Swift/Android 5000ms), so cross-platform funnels saw events landing at different cadences. Per-instance override stays \u2014 call sites can still tune it freely.",
|
|
4355
|
+
"appliesTo": [
|
|
4356
|
+
"web",
|
|
4357
|
+
"node",
|
|
4358
|
+
"react-native",
|
|
4359
|
+
"swift",
|
|
4360
|
+
"android"
|
|
4361
|
+
],
|
|
4362
|
+
"codeRef": [
|
|
4363
|
+
"sdks/web/src/crossdeck.ts",
|
|
4364
|
+
"sdks/node/src/crossdeck-server.ts",
|
|
4365
|
+
"sdks/react-native/src/crossdeck.ts",
|
|
4366
|
+
"sdks/swift/Sources/Crossdeck/EventQueue.swift",
|
|
4367
|
+
"sdks/android/crossdeck/src/main/kotlin/com/crossdeck/EventQueue.kt"
|
|
4368
|
+
],
|
|
4369
|
+
"testRef": [
|
|
4370
|
+
{
|
|
4371
|
+
"file": "sdks/swift/Sources/Crossdeck/EventQueue.swift",
|
|
4372
|
+
"name": "flushIntervalMs: Int = 2_000"
|
|
4373
|
+
},
|
|
4374
|
+
{
|
|
4375
|
+
"file": "sdks/android/crossdeck/src/main/kotlin/com/crossdeck/EventQueue.kt",
|
|
4376
|
+
"name": "flushIntervalMs: Long = 2_000L"
|
|
4377
|
+
},
|
|
4378
|
+
{
|
|
4379
|
+
"file": "sdks/web/src/crossdeck.ts",
|
|
4380
|
+
"name": "options.eventFlushIntervalMs ?? 2000"
|
|
4381
|
+
},
|
|
4382
|
+
{
|
|
4383
|
+
"file": "sdks/node/src/crossdeck-server.ts",
|
|
4384
|
+
"name": "options.eventFlushIntervalMs ?? 2000"
|
|
4385
|
+
},
|
|
4386
|
+
{
|
|
4387
|
+
"file": "sdks/react-native/src/crossdeck.ts",
|
|
4388
|
+
"name": "options.eventFlushIntervalMs ?? 2000"
|
|
4389
|
+
}
|
|
4390
|
+
],
|
|
4391
|
+
"registeredAt": "2026-05-26",
|
|
4392
|
+
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 3.3",
|
|
4393
|
+
"bundledIn": "@cross-deck/web@1.5.0"
|
|
4394
|
+
},
|
|
4395
|
+
{
|
|
4396
|
+
"id": "idempotency-key-deterministic",
|
|
4397
|
+
"pillar": "revenue",
|
|
4398
|
+
"status": "enforced",
|
|
4399
|
+
"claim": "syncPurchases() on every SDK derives a deterministic Idempotency-Key from the request body (UUID-shaped SHA-256 of `crossdeck:purchases/sync:<rail>:<jws|token>`). Same input -> same key. Backend short-circuits same-key-same-body retries by returning the cached response (status + body) with `idempotent_replay: true` flag in the body AND `Idempotent-Replayed: true` response header. Same-key-different-body returns 400 `idempotency_key_in_use`. 24-hour TTL matches Stripe. Cache only stores 2xx responses \u2014 4xx/5xx pass through so callers can fix bugs and retry. Helper returns nil/throws on missing identifier (no silent random fallback). Cross-SDK parity is CI-pinned: deriveForPurchase('apple', 'eyJ.jws.sig') MUST equal 'a66b1640-efaf-bb4d-1261-6650033bf111' on every SDK.",
|
|
4400
|
+
"appliesTo": [
|
|
4401
|
+
"web",
|
|
4402
|
+
"node",
|
|
4403
|
+
"react-native",
|
|
4404
|
+
"swift",
|
|
4405
|
+
"android",
|
|
4406
|
+
"backend"
|
|
4407
|
+
],
|
|
4408
|
+
"codeRef": [
|
|
4409
|
+
"sdks/web/src/idempotency-key.ts",
|
|
4410
|
+
"sdks/web/src/crossdeck.ts",
|
|
4411
|
+
"sdks/react-native/src/idempotency-key.ts",
|
|
4412
|
+
"sdks/react-native/src/crossdeck.ts",
|
|
4413
|
+
"sdks/node/src/idempotency-key.ts",
|
|
4414
|
+
"sdks/node/src/crossdeck-server.ts",
|
|
4415
|
+
"sdks/swift/Sources/Crossdeck/IdempotencyKey.swift",
|
|
4416
|
+
"sdks/swift/Sources/Crossdeck/Crossdeck.swift",
|
|
4417
|
+
"sdks/android/crossdeck/src/main/kotlin/com/crossdeck/IdempotencyKey.kt",
|
|
4418
|
+
"sdks/android/crossdeck/src/main/kotlin/com/crossdeck/Crossdeck.kt",
|
|
4419
|
+
"backend/src/lib/idempotency-response-cache.ts",
|
|
4420
|
+
"backend/src/api/v1-purchases.ts"
|
|
4421
|
+
],
|
|
4422
|
+
"testRef": [
|
|
4423
|
+
{
|
|
4424
|
+
"file": "sdks/web/tests/idempotency-key.test.ts",
|
|
4425
|
+
"name": "cross-SDK oracle \u2014 apple JWS pins canonical vector"
|
|
4426
|
+
},
|
|
4427
|
+
{
|
|
4428
|
+
"file": "sdks/web/tests/idempotency-key.test.ts",
|
|
4429
|
+
"name": "is deterministic: same body twice -> identical key"
|
|
4430
|
+
},
|
|
4431
|
+
{
|
|
4432
|
+
"file": "sdks/web/tests/idempotency-key.test.ts",
|
|
4433
|
+
"name": "same identifier under different rails -> different keys"
|
|
4434
|
+
},
|
|
4435
|
+
{
|
|
4436
|
+
"file": "sdks/web/tests/idempotency-key.test.ts",
|
|
4437
|
+
"name": "never silently falls back to a random key on missing identifier"
|
|
4438
|
+
},
|
|
4439
|
+
{
|
|
4440
|
+
"file": "sdks/react-native/tests/idempotency-key.test.ts",
|
|
4441
|
+
"name": "is deterministic"
|
|
4442
|
+
},
|
|
4443
|
+
{
|
|
4444
|
+
"file": "sdks/react-native/tests/idempotency-key.test.ts",
|
|
4445
|
+
"name": "cross-SDK oracle \u2014 apple JWS pins canonical vector"
|
|
4446
|
+
},
|
|
4447
|
+
{
|
|
4448
|
+
"file": "sdks/node/tests/idempotency-key.test.ts",
|
|
4449
|
+
"name": "is deterministic"
|
|
4450
|
+
},
|
|
4451
|
+
{
|
|
4452
|
+
"file": "sdks/node/tests/idempotency-key.test.ts",
|
|
4453
|
+
"name": "rail namespacing prevents cross-rail collisions"
|
|
4454
|
+
},
|
|
4455
|
+
{
|
|
4456
|
+
"file": "sdks/node/tests/idempotency-key.test.ts",
|
|
4457
|
+
"name": "apple JWS produces the canonical pinned UUID across all 5 SDKs"
|
|
4458
|
+
},
|
|
4459
|
+
{
|
|
4460
|
+
"file": "backend/tests/unit/idempotency-response-cache.test.ts",
|
|
4461
|
+
"name": "is deterministic for the same input"
|
|
4462
|
+
},
|
|
4463
|
+
{
|
|
4464
|
+
"file": "backend/tests/unit/idempotency-response-cache.test.ts",
|
|
4465
|
+
"name": "injects idempotent_replay: true into a JSON object body"
|
|
4466
|
+
},
|
|
4467
|
+
{
|
|
4468
|
+
"file": "backend/tests/unit/idempotency-response-cache.test.ts",
|
|
4469
|
+
"name": "matches Stripe's 24-hour idempotency window"
|
|
4470
|
+
},
|
|
4471
|
+
{
|
|
4472
|
+
"file": "sdks/swift/Tests/CrossdeckTests/IdempotencyKeyTests.swift",
|
|
4473
|
+
"name": "test_crossSdkOracle_appleJWS"
|
|
4474
|
+
},
|
|
4475
|
+
{
|
|
4476
|
+
"file": "sdks/swift/Tests/CrossdeckTests/IdempotencyKeyTests.swift",
|
|
4477
|
+
"name": "test_railNamespacing_preventsCrossRailCollisions"
|
|
4478
|
+
},
|
|
4479
|
+
{
|
|
4480
|
+
"file": "sdks/swift/Tests/CrossdeckTests/IdempotencyKeyTests.swift",
|
|
4481
|
+
"name": "test_missingIdentifier_returnsNil"
|
|
4482
|
+
},
|
|
4483
|
+
{
|
|
4484
|
+
"file": "sdks/android/crossdeck/src/test/kotlin/com/crossdeck/IdempotencyKeyTest.kt",
|
|
4485
|
+
"name": "cross-SDK oracle for apple JWS"
|
|
4486
|
+
},
|
|
4487
|
+
{
|
|
4488
|
+
"file": "sdks/android/crossdeck/src/test/kotlin/com/crossdeck/IdempotencyKeyTest.kt",
|
|
4489
|
+
"name": "rail namespacing prevents cross-rail collisions"
|
|
4490
|
+
},
|
|
4491
|
+
{
|
|
4492
|
+
"file": "sdks/android/crossdeck/src/test/kotlin/com/crossdeck/IdempotencyKeyTest.kt",
|
|
4493
|
+
"name": "missing identifier returns null - never silent random fallback"
|
|
4494
|
+
}
|
|
4495
|
+
],
|
|
4496
|
+
"registeredAt": "2026-05-26",
|
|
4497
|
+
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 2.2.a + 2.2.b + 2.2.c",
|
|
4498
|
+
"bundledIn": "@cross-deck/web@1.5.0"
|
|
4499
|
+
},
|
|
4500
|
+
{
|
|
4501
|
+
"id": "init-reentry-drains-prior-queue",
|
|
4502
|
+
"pillar": "lifecycle",
|
|
4503
|
+
"status": "enforced",
|
|
4504
|
+
"claim": "Web + RN init() re-entry drains the prior EventQueue's pending setTimeout BEFORE replacing this.state. Pre-v1.4.0 the teardown handled autoTracker/webVitals/errors/unloadFlush but NOT events, so the prior queue's timer would fire AFTER the state swap \u2014 sending old-init events against new-init http + identity references (cross-identity leak during HMR / config swap / multi-tenant SDK shells). The teardown CANNOT call persistent.clear() \u2014 the durable queue belongs to the SDK lifetime, not the init() lifetime, and a survived crash mid-flush re-hydrates on the next init.",
|
|
4505
|
+
"appliesTo": [
|
|
4506
|
+
"web",
|
|
4507
|
+
"react-native"
|
|
4508
|
+
],
|
|
4509
|
+
"codeRef": [
|
|
4510
|
+
"sdks/web/src/crossdeck.ts",
|
|
4511
|
+
"sdks/react-native/src/crossdeck.ts"
|
|
4512
|
+
],
|
|
4513
|
+
"testRef": [
|
|
4514
|
+
{
|
|
4515
|
+
"file": "sdks/web/tests/init-reentry.test.ts",
|
|
4516
|
+
"name": "re-init drains the prior queue's pending timer before swapping state"
|
|
4517
|
+
},
|
|
4518
|
+
{
|
|
4519
|
+
"file": "sdks/web/tests/init-reentry.test.ts",
|
|
4520
|
+
"name": "re-init does NOT wipe the durable event store"
|
|
4521
|
+
}
|
|
4522
|
+
],
|
|
4523
|
+
"registeredAt": "2026-05-26",
|
|
4524
|
+
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 5.5",
|
|
4525
|
+
"bundledIn": "@cross-deck/web@1.5.0"
|
|
4526
|
+
},
|
|
4527
|
+
{
|
|
4528
|
+
"id": "per-user-cache-isolation",
|
|
4529
|
+
"pillar": "entitlements",
|
|
4530
|
+
"status": "enforced",
|
|
4531
|
+
"claim": "Every identify(userId) switches the entitlement cache to a physically separate per-user storage slot \u2014 `crossdeck:entitlements:<sha256(userId)>` \u2014 and unconditionally wipes the in-memory snapshot. A user-switch on a shared device CANNOT cross-read a prior user's cached entitlements, even if the in-memory clear is somehow skipped, because the storage keys are physically separate. reset() wipes every per-user slot via the persisted index.",
|
|
4532
|
+
"appliesTo": [
|
|
4533
|
+
"web",
|
|
4534
|
+
"react-native",
|
|
4535
|
+
"swift",
|
|
4536
|
+
"android"
|
|
4537
|
+
],
|
|
4538
|
+
"codeRef": [
|
|
4539
|
+
"sdks/web/src/entitlement-cache.ts",
|
|
4540
|
+
"sdks/web/src/hash.ts",
|
|
4541
|
+
"sdks/web/src/crossdeck.ts",
|
|
4542
|
+
"sdks/react-native/src/entitlement-cache.ts",
|
|
4543
|
+
"sdks/react-native/src/hash.ts",
|
|
4544
|
+
"sdks/react-native/src/crossdeck.ts",
|
|
4545
|
+
"sdks/swift/Sources/Crossdeck/EntitlementCache.swift",
|
|
4546
|
+
"sdks/swift/Sources/Crossdeck/IdempotencyKey.swift",
|
|
4547
|
+
"sdks/swift/Sources/Crossdeck/Crossdeck.swift",
|
|
4548
|
+
"sdks/android/crossdeck/src/main/kotlin/com/crossdeck/EntitlementCache.kt",
|
|
4549
|
+
"sdks/android/crossdeck/src/main/kotlin/com/crossdeck/IdempotencyKey.kt",
|
|
4550
|
+
"sdks/android/crossdeck/src/main/kotlin/com/crossdeck/Crossdeck.kt"
|
|
4551
|
+
],
|
|
4552
|
+
"testRef": [
|
|
4553
|
+
{
|
|
4554
|
+
"file": "sdks/web/tests/entitlement-cache-isolation.test.ts",
|
|
4555
|
+
"name": "identify(B) makes A's entitlements unreachable from in-memory"
|
|
4556
|
+
},
|
|
4557
|
+
{
|
|
4558
|
+
"file": "sdks/web/tests/entitlement-cache-isolation.test.ts",
|
|
4559
|
+
"name": "clearAll() removes every per-user storage key plus the index"
|
|
4560
|
+
},
|
|
4561
|
+
{
|
|
4562
|
+
"file": "sdks/web/tests/entitlement-cache-isolation.test.ts",
|
|
4563
|
+
"name": "a second cache instance reading A's storage suffix CANNOT see B's data"
|
|
4564
|
+
},
|
|
4565
|
+
{
|
|
4566
|
+
"file": "sdks/react-native/tests/entitlement-cache-isolation.test.ts",
|
|
4567
|
+
"name": "identify(B) makes A's entitlements unreachable from in-memory"
|
|
4568
|
+
},
|
|
4569
|
+
{
|
|
4570
|
+
"file": "sdks/react-native/tests/entitlement-cache-isolation.test.ts",
|
|
4571
|
+
"name": "removes every per-user storage key plus the index"
|
|
4572
|
+
},
|
|
4573
|
+
{
|
|
4574
|
+
"file": "sdks/swift/Tests/CrossdeckTests/EntitlementCacheIsolationTests.swift",
|
|
4575
|
+
"name": "test_identifyB_makesAEntitlementsUnreachable"
|
|
4576
|
+
},
|
|
4577
|
+
{
|
|
4578
|
+
"file": "sdks/swift/Tests/CrossdeckTests/EntitlementCacheIsolationTests.swift",
|
|
4579
|
+
"name": "test_identifiedWritesLandUnderPerUserSha256Key"
|
|
4580
|
+
},
|
|
4581
|
+
{
|
|
4582
|
+
"file": "sdks/swift/Tests/CrossdeckTests/EntitlementCacheIsolationTests.swift",
|
|
4583
|
+
"name": "test_clearAll_removesEveryPerUserStorageKeyPlusIndex"
|
|
4584
|
+
},
|
|
4585
|
+
{
|
|
4586
|
+
"file": "sdks/android/crossdeck/src/test/kotlin/com/crossdeck/EntitlementCacheIsolationTest.kt",
|
|
4587
|
+
"name": "identified writes land under per-user sha256 key"
|
|
4588
|
+
},
|
|
4589
|
+
{
|
|
4590
|
+
"file": "sdks/android/crossdeck/src/test/kotlin/com/crossdeck/EntitlementCacheIsolationTest.kt",
|
|
4591
|
+
"name": "identify B makes A entitlements unreachable from in-memory"
|
|
4592
|
+
},
|
|
4593
|
+
{
|
|
4594
|
+
"file": "sdks/android/crossdeck/src/test/kotlin/com/crossdeck/EntitlementCacheIsolationTest.kt",
|
|
4595
|
+
"name": "clearAll removes every per-user storage key plus the index"
|
|
4596
|
+
},
|
|
4597
|
+
{
|
|
4598
|
+
"file": "sdks/android/crossdeck/src/test/kotlin/com/crossdeck/EntitlementCacheIsolationTest.kt",
|
|
4599
|
+
"name": "a fresh cache bound to A's key CANNOT read B's blob"
|
|
4600
|
+
}
|
|
4601
|
+
],
|
|
4602
|
+
"registeredAt": "2026-05-26",
|
|
4603
|
+
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 1.3 (web/RN) + dogfood-gap fix (swift + android)",
|
|
4604
|
+
"bundledIn": "@cross-deck/web@1.5.0"
|
|
4605
|
+
},
|
|
4606
|
+
{
|
|
4607
|
+
"id": "sdk-error-codes-catalogue",
|
|
4608
|
+
"pillar": "errors",
|
|
4609
|
+
"status": "enforced",
|
|
4610
|
+
"claim": "Web + Node SDK error-codes catalogues include EVERY backend-emitted ApiErrorCode with a description + resolution. Pre-v1.4.0 the catalogues documented codes the SDK threw ITSELF but ZERO backend codes \u2014 a developer hitting `invalid_api_key` / `origin_not_allowed` / `bundle_id_not_allowed` / `env_mismatch` / `idempotency_key_in_use` etc. got `undefined` from getErrorCode() and had to hunt for guidance. v1.4.0 backfills the catalogue from backend/src/api/v1-errors.ts so every wire code has a canonical 'what does this mean / what should I do' answer Stripe-style.",
|
|
4611
|
+
"appliesTo": [
|
|
4612
|
+
"web",
|
|
4613
|
+
"node"
|
|
4614
|
+
],
|
|
4615
|
+
"codeRef": [
|
|
4616
|
+
"sdks/web/src/error-codes.ts",
|
|
4617
|
+
"sdks/node/src/error-codes.ts",
|
|
4618
|
+
"backend/src/api/v1-errors.ts"
|
|
4619
|
+
],
|
|
4620
|
+
"testRef": [
|
|
4621
|
+
{
|
|
4622
|
+
"file": "sdks/web/tests/error-codes-backfill.test.ts",
|
|
4623
|
+
"name": "includes backend code"
|
|
4624
|
+
},
|
|
4625
|
+
{
|
|
4626
|
+
"file": "sdks/web/tests/error-codes-backfill.test.ts",
|
|
4627
|
+
"name": "invalid_api_key resolution points at the dashboard"
|
|
4628
|
+
},
|
|
4629
|
+
{
|
|
4630
|
+
"file": "sdks/web/tests/error-codes-backfill.test.ts",
|
|
4631
|
+
"name": "idempotency_key_in_use resolution mentions Stripe-grade contract"
|
|
4632
|
+
},
|
|
4633
|
+
{
|
|
4634
|
+
"file": "sdks/web/tests/error-codes-backfill.test.ts",
|
|
4635
|
+
"name": "identity-lock codes carry permission_error type"
|
|
4636
|
+
},
|
|
4637
|
+
{
|
|
4638
|
+
"file": "sdks/web/tests/error-codes-backfill.test.ts",
|
|
4639
|
+
"name": "no entry has an empty description or resolution"
|
|
4640
|
+
}
|
|
4641
|
+
],
|
|
4642
|
+
"registeredAt": "2026-05-26",
|
|
4643
|
+
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 6.2",
|
|
4644
|
+
"bundledIn": "@cross-deck/web@1.5.0"
|
|
4645
|
+
},
|
|
4646
|
+
{
|
|
4647
|
+
"id": "sync-purchases-funnel-parity",
|
|
4648
|
+
"pillar": "analytics",
|
|
4649
|
+
"status": "enforced",
|
|
4650
|
+
"claim": "Manual syncPurchases() emits a `purchase.completed` analytics event on success across ALL SDKs (Web / Node / RN / Swift / Android). Pre-v1.4.0 only Swift/Android auto-track emitted it \u2014 Web/Node/RN manual calls + Swift/Android manual calls fired ZERO analytics. Schema mirrors the auto-track event name + rail/productId/subscriptionId so cross-platform funnels reconcile on every payment path. When the backend short-circuits via the v1.4.0 idempotency cache, the event also carries `idempotent_replay: true`.",
|
|
4651
|
+
"appliesTo": [
|
|
4652
|
+
"web",
|
|
4653
|
+
"node",
|
|
4654
|
+
"react-native",
|
|
4655
|
+
"swift",
|
|
4656
|
+
"android"
|
|
4657
|
+
],
|
|
4658
|
+
"codeRef": [
|
|
4659
|
+
"sdks/web/src/crossdeck.ts",
|
|
4660
|
+
"sdks/node/src/crossdeck-server.ts",
|
|
4661
|
+
"sdks/react-native/src/crossdeck.ts",
|
|
4662
|
+
"sdks/swift/Sources/Crossdeck/Crossdeck.swift",
|
|
4663
|
+
"sdks/android/crossdeck/src/main/kotlin/com/crossdeck/Crossdeck.kt"
|
|
4664
|
+
],
|
|
4665
|
+
"testRef": [
|
|
4666
|
+
{
|
|
4667
|
+
"file": "sdks/web/tests/sync-purchases-funnel.test.ts",
|
|
4668
|
+
"name": "emits purchase.completed after a successful sync"
|
|
4669
|
+
},
|
|
4670
|
+
{
|
|
4671
|
+
"file": "sdks/web/tests/sync-purchases-funnel.test.ts",
|
|
4672
|
+
"name": "carries idempotent_replay=true when backend replied from cache"
|
|
4673
|
+
}
|
|
4674
|
+
],
|
|
4675
|
+
"registeredAt": "2026-05-26",
|
|
4676
|
+
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 3.5",
|
|
4677
|
+
"bundledIn": "@cross-deck/web@1.5.0"
|
|
4678
|
+
}
|
|
4679
|
+
]);
|
|
4680
|
+
|
|
4681
|
+
// src/contracts.ts
|
|
4682
|
+
var CrossdeckContracts = {
|
|
4683
|
+
/** Every contract that applies to this SDK and is currently enforced. */
|
|
4684
|
+
all() {
|
|
4685
|
+
return BUNDLED_CONTRACTS.filter((c) => c.status === "enforced");
|
|
4686
|
+
},
|
|
4687
|
+
/**
|
|
4688
|
+
* Every contract bundled with this SDK release, including
|
|
4689
|
+
* `proposed` and `retired` entries. Use `all()` for the
|
|
4690
|
+
* enforced-only view.
|
|
4691
|
+
*/
|
|
4692
|
+
allIncludingHistorical() {
|
|
4693
|
+
return BUNDLED_CONTRACTS;
|
|
4694
|
+
},
|
|
4695
|
+
/** Look up a contract by its stable `id`. */
|
|
4696
|
+
byId(id) {
|
|
4697
|
+
return BUNDLED_CONTRACTS.find((c) => c.id === id);
|
|
4698
|
+
},
|
|
4699
|
+
/** Every enforced contract within a pillar. */
|
|
4700
|
+
byPillar(pillar) {
|
|
4701
|
+
return BUNDLED_CONTRACTS.filter(
|
|
4702
|
+
(c) => c.pillar === pillar && c.status === "enforced"
|
|
4703
|
+
);
|
|
4704
|
+
},
|
|
4705
|
+
/** Filter by lifecycle status. */
|
|
4706
|
+
withStatus(status) {
|
|
4707
|
+
return BUNDLED_CONTRACTS.filter((c) => c.status === status);
|
|
4708
|
+
},
|
|
4709
|
+
/** Semver of the SDK release these contracts were bundled with. */
|
|
4710
|
+
sdkVersion: SDK_VERSION2,
|
|
4711
|
+
/** Fully-qualified bundle identifier — e.g. `@cross-deck/web@1.4.2`. */
|
|
4712
|
+
bundledIn: BUNDLED_IN,
|
|
4713
|
+
/**
|
|
4714
|
+
* Resolve a failing test back to the contract it exercises.
|
|
4715
|
+
* Used by test-framework hooks (Vitest `afterEach`, XCTest
|
|
4716
|
+
* observation, JUnit `TestWatcher`) to find the contract id of
|
|
4717
|
+
* a failed contract test so `reportContractFailure(...)` can
|
|
4718
|
+
* stamp the right `contract_id` on the emitted event.
|
|
4719
|
+
*
|
|
4720
|
+
* Match is on `testRef.name` (case-sensitive, exact). Returns
|
|
4721
|
+
* the first contract whose `testRef` list contains a matching
|
|
4722
|
+
* entry, regardless of pillar or status.
|
|
4723
|
+
*/
|
|
4724
|
+
findByTestName(name) {
|
|
4725
|
+
return BUNDLED_CONTRACTS.find(
|
|
4726
|
+
(c) => c.testRef.some((ref) => ref.name === name)
|
|
4727
|
+
);
|
|
4728
|
+
}
|
|
4729
|
+
};
|
|
4275
4730
|
export {
|
|
4276
4731
|
CROSSDECK_ERROR_CODES,
|
|
4277
4732
|
Crossdeck,
|
|
4278
4733
|
CrossdeckClient,
|
|
4734
|
+
CrossdeckContracts,
|
|
4279
4735
|
CrossdeckError,
|
|
4280
4736
|
DEFAULT_BASE_URL,
|
|
4281
4737
|
MemoryStorage,
|