diff --git a/changelog.d/20261107_ts-envelope-prescan-aarch64-probe.md b/changelog.d/20261107_ts-envelope-prescan-aarch64-probe.md new file mode 100644 index 0000000..ce84820 --- /dev/null +++ b/changelog.d/20261107_ts-envelope-prescan-aarch64-probe.md @@ -0,0 +1,13 @@ +### SDK feature matrix — TypeScript envelope pre-scan and aarch64 AES probe released in npm 0.2.2 + +- [`sdk-feature-matrix.md`](sdk-feature-matrix.md): `@cachekit-io/cachekit` 0.2.2 is the first release that + pins `cachekit-core-ts` 0.2.1 and `cachekit-core-wasm` 0.2.1, and all five platform `.node` binaries and the + `.wasm` of those bindings embed `cachekit-core` 0.7.2 (`strings`, 2026-11-07). npm 0.2.1 pins the 0.2.0 + bindings, which embed core 0.6.0. +- **Wire format (ByteStorage)**, TypeScript: the envelope pre-scan exception is closed since 0.2.2, and the + "🚧 unreleased — compliant on `main`" clause is gone. The cell stays ⚠️ because the allocation bound for the + size-cap and ratio vectors is still unasserted in cachekit-ts. +- **Test vectors in CI**, TypeScript: every `decode-bounds.json` vector runs through both `unpack` bindings + since 0.2.2 ([cachekit-ts#245](https://github.com/cachekit-io/cachekit-ts/pull/245)), no longer 🚧. +- **Hardware acceleration detection**, TypeScript, and note ⁶: 0.2.2+ probes the Armv8 Crypto Extension on + aarch64; 0.2.1 reports `true` on every aarch64 build. diff --git a/sdk-feature-matrix.md b/sdk-feature-matrix.md index eb243ae..d7903c9 100644 --- a/sdk-feature-matrix.md +++ b/sdk-feature-matrix.md @@ -6,7 +6,7 @@ **Feature parity and compliance status across all CacheKit SDK implementations.** -*Last updated: 2026-10-24 — LAB-4614 TypeScript keyring, hardware-acceleration, backfill, preset, tenancy, SaaS API, path-encoding, wire-format and test-vector cells brought to the published `@cachekit-io/cachekit` 0.2.1, whose `cachekit-core-ts` 0.2.0 and `cachekit-core-wasm` 0.2.0 bindings embed `cachekit-core` 0.6.0, so the TypeScript caveats that wait on core 0.7.0 stay. Every version-keyed claim is verified against the **published artifact** (registry metadata, and the `.crate`/`.tgz` contents where an embedded dependency version decides the answer), not against a repo branch — see [decisions/matrix-version-verification.md](decisions/matrix-version-verification.md) for why and how. Per-PR fold verdicts are in [CHANGELOG.md](CHANGELOG.md); per-row history is `git log sdk-feature-matrix.md`.* +*Last updated: 2026-11-07 — TypeScript wire-format, test-vector and hardware-acceleration cells flipped to the published `@cachekit-io/cachekit` 0.2.2, the first release whose exact pins, `cachekit-core-ts` 0.2.1 and `cachekit-core-wasm` 0.2.1, embed `cachekit-core` 0.7.2, so TypeScript ships the envelope pre-scan and the aarch64 Crypto Extension probe from 0.2.2. Every version-keyed claim is verified against the **published artifact** (registry metadata, and the `.crate`/`.tgz` contents where an embedded dependency version decides the answer), not against a repo branch — see [decisions/matrix-version-verification.md](decisions/matrix-version-verification.md) for why and how. Per-PR fold verdicts are in [CHANGELOG.md](CHANGELOG.md); per-row history is `git log sdk-feature-matrix.md`.* *__Cells that reversed — check these if you built on them:__ Rust sync support (✅ → does not exist) and the Rust `::secure` preset (✅ → spelled `::encrypted` on every published `cachekit-rs` through 0.7.0 → `::secure` from 0.8.0¹¹), Builder API (py/ts ✅ → ❌), Hardware acceleration (rs ✅ → not re-exported → re-exported from 0.8.0, ts N/A → ❌ → surfaced from 0.2.1), TypeScript Arrow (🔜 → ❌), Python's encrypted read path (documented fail-closed → **fail-open by default**), and `cache.secure.wrap()` in TypeScript (implied encryption → no guarantee → **enforced since LAB-513: throws without encryption**). The TypeScript protocol-1.1 `bin` rollout also reversed twice in two days: it was **not** shipped on either ts path through 0.1.5, and ships on both from 0.2.1 (per-artifact evidence in the [cachekit-core architecture note](#architecture-notes)).* @@ -83,7 +83,7 @@ | Key rotation | ✅ 0.18.0+ — keyring; derived-key fingerprint selection⁵ | ✅ `cachekit-rs` 0.8.0+ — keyring; sequential current-first attempts; previous keys via the builder, `::secure_from_env` from 0.9.0, or `.encryption_from_env()` from 0.12.0 (0.8.0's `::secure_from_env` ignores them; `from_env()` applies them through 0.11.x and from 0.12.0 only validates them; `.crate`s inspected 2026-09-30, 2026-10-03 and, for 0.12.0, 2026-10-26)⁵ | ✅ `@cachekit-io/cachekit` 0.2.1+¹⁷ — keyring; sequential current-first attempts; previous keys via `encryption.previousMasterKeys`, `createCache.secure`'s `previousMasterKeys`, or `CACHEKIT_PREVIOUS_MASTER_KEYS` (read by `createCache.secure` only; `createSecure` in `dist/intents-core.js`, `.tgz` inspected 2026-10-24); absent from 0.1.5⁵ | ❌ | | **Tamper / wrong-key failure mode** | ⚠️ **fail-OPEN by default** — warn + recompute; switchable with `CACHEKIT_ENCRYPTION_FAIL_CLOSED=true`¹⁸ | ✅ **Fails closed** — `decrypt_or_evict(…)?` drops the L1 copy and propagates (`SecureCache::decrypt_or_evict` in `client.rs`), and `#[cachekit(secure)]` emits no fail-open arm (`fail_open_arm` in `expand`, `cachekit-macros/src/lib.rs`; 0.8.0 crates). From `cachekit-rs` 0.10.0 every read on a client with encryption configured takes that path, plain `get` and `#[cachekit]` without `secure` included | ⚠️ **fail-OPEN on reads, drops writes with only a rate-limited warning, no fail-closed switch**⁸ | — | | **Does the `secure` API enforce encryption?** | ✅ Raises without a key | ✅ `secure_cache()`¹¹ returns `Err` | ✅ **`cache.secure.wrap()` throws `ConfigurationError` at wrap time** on any instance without `encryption` configured — instance and `withExecutionContext(ctx)` view alike (before this check, an unconditional alias for `wrap()`, so on an instance without configured encryption a "secure" registration cached plaintext, CWE-311); see [Intent-preset semantics](#intent-preset-semantics-parity-not-presence) | — | -| Hardware acceleration detection | ✅ surfaced (`EncryptionWrapper.hardware_acceleration_enabled`)⁶ | ✅ `cachekit-rs` 0.8.0+ — surfaced (`SecureCache::hardware_acceleration_enabled()`; `.crate` inspected 2026-09-30)⁶ | ✅ `@cachekit-io/cachekit` 0.2.1+ — surfaced (`EncryptionManager.isHardwareAccelerated()`; `.tgz` inspected 2026-10-24); `true` on every aarch64 build, since both bindings embed `cachekit-core` 0.6.0⁶ | N/A | +| Hardware acceleration detection | ✅ surfaced (`EncryptionWrapper.hardware_acceleration_enabled`)⁶ | ✅ `cachekit-rs` 0.8.0+ — surfaced (`SecureCache::hardware_acceleration_enabled()`; `.crate` inspected 2026-09-30)⁶ | ✅ `@cachekit-io/cachekit` 0.2.1+ — surfaced (`EncryptionManager.isHardwareAccelerated()`; `.tgz` inspected 2026-10-24); 0.2.2+ probes the Armv8 Crypto Extension on aarch64, since both bindings it pins embed `cachekit-core` 0.7.2 (inspected 2026-11-07); 0.2.1 reports `true` on every aarch64 build, since both its bindings embed `cachekit-core` 0.6.0⁶ | N/A | | Counter-based nonces | ✅ via Rust | ✅ | ✅ via NAPI (Rust) | ❌ use random | > [!IMPORTANT] @@ -104,7 +104,7 @@ > > Degradation is on unless explicitly disabled (`degradationEnabled = …degradation !== false`, read the same way by the `CacheImpl` constructor in `cache-core.ts` and the `ReliabilityExecutor` constructor in `reliability/executor.ts`) and `createCache.secure()` / `.production()` / `.io()` all set it `true` (`PRODUCTION_RELIABILITY` in `intents-core.ts`); only `minimal` sets `false`, and `minimal` carries no encryption. **There is no `failClosed` option anywhere in cachekit-ts** — Python's `CACHEKIT_ENCRYPTION_FAIL_CLOSED` has no counterpart, so the only lever is `reliability: { degradation: false }`, which also gives up backend-outage degradation. If you rely on a thrown error as your tamper, wrong-key or nonce alarm, TypeScript raises none with degradation on. The signals are the `errors_total` counter (metrics on), the rate-limited write warning and the L1 decrypt log; an L2 read that fails to decrypt shows up only in the counter. > -> ⁶ AES hardware detection lives in the shared core — `ZeroKnowledgeEncryptor::hardware_acceleration_enabled()` in `cachekit-core/src/encryption/core.rs`: a runtime `is_x86_feature_detected!("aes")` probe on x86/x86_64; on aarch64 it returns `cfg!(target_feature = "neon")`, which every aarch64 target enables, so it is `true` on every aarch64 build whether or not the CPU has the Crypto Extension — a Raspberry Pi 4 (Cortex-A72 without the Crypto Extension) reports `true` while `ring` runs software AES. That holds for every published `cachekit-core` through 0.6.0 (0.4.0, 0.5.0 and 0.6.0 included). `cachekit-core` 0.7.0 (crates.io, 2026-10-21) ships the fix, [cachekit-core#77](https://github.com/cachekit-io/cachekit-core/pull/77) (LAB-4650): on aarch64 it calls `is_aarch64_feature_detected!("aes")` (`src/encryption/core.rs:263` of the published `.crate`, inspected 2026-10-24). Always `false` on wasm32 (no AES instructions to detect). It is **informational only** — `ring`/`aes-gcm` choose their implementation independently of the flag. Published artifacts (registries checked 2026-09-29; crates.io re-checked 2026-09-30): Python surfaces it (`EncryptionWrapper.hardware_acceleration_enabled`, in `get_encryption_info()` and the init log); `cachekit-rs` 0.8.0+ re-exports it as `EncryptionLayer::hardware_acceleration_enabled()`, forwarded by `SecureCache` ([cachekit-rs#80](https://github.com/cachekit-io/cachekit-rs/pull/80)); 0.8.0 through 0.11.0 require `cachekit-core` `^0.6`, which resolves to 0.6.0 (the only 0.6.x published), so they inherit the aarch64 false positive above, and 0.11.1+ (crates.io, 2026-10-23) requires `^0.7` and inherits the fix (registry dependency lists read 2026-10-24); 0.7.0 and earlier never re-export it. `@cachekit-io/cachekit` 0.2.1+ surfaces it ([cachekit-ts#132](https://github.com/cachekit-io/cachekit-ts/pull/132)): `TenantKeys.hardwareAccelerationEnabled()` on the NAPI and wasm bindings (`index.d.ts` of `cachekit-core-ts` 0.2.0 and of `cachekit-core-wasm` 0.2.0) and `EncryptionManager.isHardwareAccelerated()` (`dist/encryption/manager-core.js`), which initialises on demand and returns `null` — unknown, not `false` — when the installed binding predates the accessor (`.tgz`s inspected 2026-10-24). Both 0.2.1 bindings embed `cachekit-core` 0.6.0 (`strings` on every `.node` and the `.wasm`, 2026-10-24), so TypeScript still reports `true` on every aarch64 build. 0.1.5 and earlier expose nothing on either binding — `N/A` for TypeScript was wrong, since both bindings run the same Rust core. The wasm32 always-`false` claim is asserted by cachekit-ts's workerd test (`encryption.protocol.workers.test.ts`), and the Rust accessor is pinned to `is_x86_feature_detected!("aes")` on x86_64 by the `hardware_acceleration_reports_core_detection` test in the published 0.8.0 crate's `src/encryption.rs`. TypeScript's aarch64 false positive ends with a release whose bindings embed `cachekit-core` 0.7.0 or later. +> ⁶ AES hardware detection lives in the shared core — `ZeroKnowledgeEncryptor::hardware_acceleration_enabled()` in `cachekit-core/src/encryption/core.rs`: a runtime `is_x86_feature_detected!("aes")` probe on x86/x86_64; on aarch64 it returns `cfg!(target_feature = "neon")`, which every aarch64 target enables, so it is `true` on every aarch64 build whether or not the CPU has the Crypto Extension — a Raspberry Pi 4 (Cortex-A72 without the Crypto Extension) reports `true` while `ring` runs software AES. That holds for every published `cachekit-core` through 0.6.0 (0.4.0, 0.5.0 and 0.6.0 included). `cachekit-core` 0.7.0 (crates.io, 2026-10-21) ships the fix, [cachekit-core#77](https://github.com/cachekit-io/cachekit-core/pull/77) (LAB-4650): on aarch64 it calls `is_aarch64_feature_detected!("aes")` (`src/encryption/core.rs:263` of the published `.crate`, inspected 2026-10-24). Always `false` on wasm32 (no AES instructions to detect). It is **informational only** — `ring`/`aes-gcm` choose their implementation independently of the flag. Published artifacts (registries checked 2026-09-29; crates.io re-checked 2026-09-30): Python surfaces it (`EncryptionWrapper.hardware_acceleration_enabled`, in `get_encryption_info()` and the init log); `cachekit-rs` 0.8.0+ re-exports it as `EncryptionLayer::hardware_acceleration_enabled()`, forwarded by `SecureCache` ([cachekit-rs#80](https://github.com/cachekit-io/cachekit-rs/pull/80)); 0.8.0 through 0.11.0 require `cachekit-core` `^0.6`, which resolves to 0.6.0 (the only 0.6.x published), so they inherit the aarch64 false positive above, and 0.11.1+ (crates.io, 2026-10-23) requires `^0.7` and inherits the fix (registry dependency lists read 2026-10-24); 0.7.0 and earlier never re-export it. `@cachekit-io/cachekit` 0.2.1+ surfaces it ([cachekit-ts#132](https://github.com/cachekit-io/cachekit-ts/pull/132)): `TenantKeys.hardwareAccelerationEnabled()` on the NAPI and wasm bindings (`index.d.ts` of `cachekit-core-ts` 0.2.0 and of `cachekit-core-wasm` 0.2.0) and `EncryptionManager.isHardwareAccelerated()` (`dist/encryption/manager-core.js`), which initialises on demand and returns `null` — unknown, not `false` — when the installed binding predates the accessor (`.tgz`s inspected 2026-10-24). npm 0.2.1's bindings (`cachekit-core-ts` 0.2.0 and `cachekit-core-wasm` 0.2.0) embed `cachekit-core` 0.6.0 (`strings` on every `.node` and the `.wasm`, 2026-10-24), so 0.2.1 reports `true` on every aarch64 build. From 0.2.2, `@cachekit-io/cachekit` pins `cachekit-core-ts` 0.2.1 and `cachekit-core-wasm` 0.2.1, which embed `cachekit-core` 0.7.2 (`strings` on all five `.node` binaries, `linux-arm64-gnu` and `darwin-arm64` included, and on the `.wasm`, 2026-11-07), so 0.2.2+ probes the Armv8 Crypto Extension on aarch64. 0.1.5 and earlier expose nothing on either binding — `N/A` for TypeScript was wrong, since both bindings run the same Rust core. The wasm32 always-`false` claim is asserted by cachekit-ts's workerd test (`encryption.protocol.workers.test.ts`), and the Rust accessor is pinned to `is_x86_feature_detected!("aes")` on x86_64 by the `hardware_acceleration_reports_core_detection` test in the published 0.8.0 crate's `src/encryption.rs`. --- @@ -296,14 +296,14 @@ its spec: | Requirement | Python | Rust | TypeScript | PHP | | :--- | :---: | :---: | :---: | :---: | | Key generation (Blake2b) | ✅ Compliant | N/A auto mode¹⁴ — interop/v1 keygen ✅ merged ([#33](https://github.com/cachekit-io/cachekit-rs/pull/33)); `#[cachekit]` mints interop keys ([#35](https://github.com/cachekit-io/cachekit-rs/pull/35)) | ✅ Compliant | ⚠️ Untested | -| Wire format (ByteStorage) | ✅ Compliant¹⁵ since 0.25.0 (wheel run 2026-11-03), whose lockfile pins `cachekit-core` 0.7.2 ([cachekit-py#618](https://github.com/cachekit-io/cachekit-py/pull/618)): its `ByteStorage::retrieve` runs the [Retrieve Flow](spec/wire-format.md#retrieve-flow) step-2 envelope pre-scan ([cachekit-core#80](https://github.com/cachekit-io/cachekit-core/pull/80)), which `cachekit-core` 0.6.0 and older, the versions cachekit-py 0.24.1 and earlier pin, lack; runs all six `wire-format.json` 1.3.0 [Reject vectors](spec/wire-format.md#reject-vectors) through `ByteStorage.retrieve` since 0.21.0 ([cachekit-py#448](https://github.com/cachekit-io/cachekit-py/pull/448); `TestRejectVectors` in `tests/unit/protocol/test_envelope_wire_vectors.py`), asserting the error each row names, except that core shares one error between its zero-length and ratio checks, so the zero-length vector asserts the ratio message; all fourteen `wire-format.json` 1.4.0 reject vectors since 0.23.0 ([cachekit-py#550](https://github.com/cachekit-io/cachekit-py/pull/550)), each asserting the error its row names (the five step-2 typed-decode vectors the typed-decode error, `reject_original_size_sign_bit` the range-checked `u32` decode), and both `payload_reject_vectors` asserting the structural guard's own error after the envelope read; `reject_envelope_slots_overclaim` asserts the pre-scan's own error ([WIRE-9](spec/wire-format.md#wire-9)) since 0.25.0 ([cachekit-py#618](https://github.com/cachekit-io/cachekit-py/pull/618)), a strict expected failure before; the allocation bound for the size-cap and ratio vectors ([WIRE-17](spec/wire-format.md#wire-17)) is relied on through `cachekit-core`'s allocation probe ([cachekit-core#100](https://github.com/cachekit-io/cachekit-core/pull/100)), which core 0.7.0's publish job ran, under the passthrough rule in [Reject vectors](spec/wire-format.md#reject-vectors): `ByteStorage.retrieve` passes the envelope bytes to core with no allocation of its own sized from an envelope field, and a test fails unless the lockfile pins that release ([cachekit-py#627](https://github.com/cachekit-io/cachekit-py/pull/627), since 0.25.0) | ✅ Canonical (`cachekit-core`), unused for stored values¹⁵; step-2 pre-scan since crates.io 0.7.0 ([cachekit-core#80](https://github.com/cachekit-io/cachekit-core/pull/80)): `ByteStorage::retrieve` and `validate` pre-scan the envelope bytes before the typed `rmp_serde::from_slice` into `StorageEnvelope`; 0.6.0 and earlier lack it | ⚠️ Compliant except the envelope pre-scan: payload decodes are pre-scanned, but the envelope goes through `cachekit-core`'s `ByteStorage::retrieve` (`unpack` in both `cachekit-core-ts` and `cachekit-core-wasm`), which has no step-2 pre-scan in `cachekit-core` 0.6.0, the core both npm 0.2.1 bindings embed (`strings` on every `cachekit-core-ts` 0.2.0 `.node` and the `cachekit-core-wasm` 0.2.0 `.wasm`, 2026-10-24); the pre-scan ships in `cachekit-core` 0.7.0 ([cachekit-core#80](https://github.com/cachekit-io/cachekit-core/pull/80)); 🚧 unreleased — compliant on `main`: both bindings pin core 0.7.0 since [cachekit-ts#240](https://github.com/cachekit-io/cachekit-ts/pull/240), and every `decode-bounds.json` vector runs through both `unpack` bindings in [cachekit-ts#245](https://github.com/cachekit-io/cachekit-ts/pull/245) (see the Test vectors row); it stays unreleased until a published `@cachekit-io/cachekit` depends on `cachekit-core-ts` and `cachekit-core-wasm` releases that embed core 0.7.0, because npm 0.2.1 pins both at exactly 0.2.0; ✅ since npm 0.2.1 (PRs in the `cachekit-v0.2.1` tag's history, checked 2026-10-24; the Node lane passes at that tag), runs all fourteen `wire-format.json` 1.4.0 [Reject vectors](spec/wire-format.md#reject-vectors) on the envelope read path, asserting the error each row names, in the Node lane (`packages/cachekit/test/protocol/wire-format.protocol.test.ts`) and the Workers lane (`packages/cachekit/test/workers/wire-format.workers.test.ts`) ([cachekit-ts#198](https://github.com/cachekit-io/cachekit-ts/pull/198); 1.4.0 in [cachekit-ts#229](https://github.com/cachekit-io/cachekit-ts/pull/229)), and both [`payload_reject_vectors`](spec/wire-format.md#payload-decode-bounds), refused by the payload pre-scan; `reject_envelope_slots_overclaim` passes in both lanes since 0.2.1 ([cachekit-ts#232](https://github.com/cachekit-io/cachekit-ts/pull/232); `dist/serialization/envelope.js`): the SDK's own header walk keeps a running total of the declared slots and refuses the vector with the envelope pre-scan's own error ([WIRE-9](spec/wire-format.md#wire-9)), before element 1's type check and before core sees the bytes. The walk allocates nothing and admits only complete envelopes; ⚠️ the allocation bound for the size-cap and ratio vectors is unasserted: `cachekit-core` 0.7.0 asserts it in its own unit tests (`src/read_allocation_probe.rs`), but both 0.2.1 bindings embed 0.6.0 | ⚠️ Untested | +| Wire format (ByteStorage) | ✅ Compliant¹⁵ since 0.25.0 (wheel run 2026-11-03), whose lockfile pins `cachekit-core` 0.7.2 ([cachekit-py#618](https://github.com/cachekit-io/cachekit-py/pull/618)): its `ByteStorage::retrieve` runs the [Retrieve Flow](spec/wire-format.md#retrieve-flow) step-2 envelope pre-scan ([cachekit-core#80](https://github.com/cachekit-io/cachekit-core/pull/80)), which `cachekit-core` 0.6.0 and older, the versions cachekit-py 0.24.1 and earlier pin, lack; runs all six `wire-format.json` 1.3.0 [Reject vectors](spec/wire-format.md#reject-vectors) through `ByteStorage.retrieve` since 0.21.0 ([cachekit-py#448](https://github.com/cachekit-io/cachekit-py/pull/448); `TestRejectVectors` in `tests/unit/protocol/test_envelope_wire_vectors.py`), asserting the error each row names, except that core shares one error between its zero-length and ratio checks, so the zero-length vector asserts the ratio message; all fourteen `wire-format.json` 1.4.0 reject vectors since 0.23.0 ([cachekit-py#550](https://github.com/cachekit-io/cachekit-py/pull/550)), each asserting the error its row names (the five step-2 typed-decode vectors the typed-decode error, `reject_original_size_sign_bit` the range-checked `u32` decode), and both `payload_reject_vectors` asserting the structural guard's own error after the envelope read; `reject_envelope_slots_overclaim` asserts the pre-scan's own error ([WIRE-9](spec/wire-format.md#wire-9)) since 0.25.0 ([cachekit-py#618](https://github.com/cachekit-io/cachekit-py/pull/618)), a strict expected failure before; the allocation bound for the size-cap and ratio vectors ([WIRE-17](spec/wire-format.md#wire-17)) is relied on through `cachekit-core`'s allocation probe ([cachekit-core#100](https://github.com/cachekit-io/cachekit-core/pull/100)), which core 0.7.0's publish job ran, under the passthrough rule in [Reject vectors](spec/wire-format.md#reject-vectors): `ByteStorage.retrieve` passes the envelope bytes to core with no allocation of its own sized from an envelope field, and a test fails unless the lockfile pins that release ([cachekit-py#627](https://github.com/cachekit-io/cachekit-py/pull/627), since 0.25.0) | ✅ Canonical (`cachekit-core`), unused for stored values¹⁵; step-2 pre-scan since crates.io 0.7.0 ([cachekit-core#80](https://github.com/cachekit-io/cachekit-core/pull/80)): `ByteStorage::retrieve` and `validate` pre-scan the envelope bytes before the typed `rmp_serde::from_slice` into `StorageEnvelope`; 0.6.0 and earlier lack it | ⚠️ Compliant except the allocation bound, which is unasserted (last clause); the envelope pre-scan exception is closed since npm 0.2.2: payload decodes are pre-scanned, and the envelope goes through `cachekit-core`'s `ByteStorage::retrieve` (`unpack` in both `cachekit-core-ts` and `cachekit-core-wasm`), whose step-2 pre-scan ships in `cachekit-core` 0.7.0 ([cachekit-core#80](https://github.com/cachekit-io/cachekit-core/pull/80)); npm 0.2.2+ pins both bindings at exactly 0.2.1, built against core 0.7 since [cachekit-ts#240](https://github.com/cachekit-io/cachekit-ts/pull/240), and both embed `cachekit-core` 0.7.2 (`strings` on every `cachekit-core-ts` 0.2.1 `.node` and the `cachekit-core-wasm` 0.2.1 `.wasm`, 2026-11-07); every `decode-bounds.json` vector runs through both `unpack` bindings since 0.2.2 ([cachekit-ts#245](https://github.com/cachekit-io/cachekit-ts/pull/245), in the `cachekit-v0.2.2` tag's history, checked 2026-11-07; see the Test vectors row); npm 0.2.1 lacks the envelope pre-scan, because it pins both bindings at exactly 0.2.0, which embed core 0.6.0 (`strings`, 2026-10-24); ✅ since npm 0.2.1 (PRs in the `cachekit-v0.2.1` tag's history, checked 2026-10-24; the Node lane passes at that tag), runs all fourteen `wire-format.json` 1.4.0 [Reject vectors](spec/wire-format.md#reject-vectors) on the envelope read path, asserting the error each row names, in the Node lane (`packages/cachekit/test/protocol/wire-format.protocol.test.ts`) and the Workers lane (`packages/cachekit/test/workers/wire-format.workers.test.ts`) ([cachekit-ts#198](https://github.com/cachekit-io/cachekit-ts/pull/198); 1.4.0 in [cachekit-ts#229](https://github.com/cachekit-io/cachekit-ts/pull/229)), and both [`payload_reject_vectors`](spec/wire-format.md#payload-decode-bounds), refused by the payload pre-scan; `reject_envelope_slots_overclaim` passes in both lanes since 0.2.1 ([cachekit-ts#232](https://github.com/cachekit-io/cachekit-ts/pull/232); `dist/serialization/envelope.js`): the SDK's own header walk keeps a running total of the declared slots and refuses the vector with the envelope pre-scan's own error ([WIRE-9](spec/wire-format.md#wire-9)), before element 1's type check and before core sees the bytes. The walk allocates nothing and admits only complete envelopes; ⚠️ the allocation bound for the size-cap and ratio vectors is unasserted: `cachekit-core` 0.7.0+ asserts it in its own unit tests (`src/read_allocation_probe.rs`), and npm 0.2.2+'s bindings embed 0.7.2, but no cachekit-ts test asserts the bound or relies on core's probe under the passthrough rule in [Reject vectors](spec/wire-format.md#reject-vectors) (`test/fixtures/wire-vectors.ts` defers it) | ⚠️ Untested | | Storage container (auto mode)¹⁵ | CK v3 frame (Python-internal) | Plain MessagePack (`rmp` named) — no envelope; ⚠️ decodes cachekit-ts's bare ByteStorage envelope ([WIRE-21](spec/wire-format.md#wire-21)), in every release and on `main`¹⁵ | Bare ByteStorage envelope (default) | — | | Temporal sentinels ([spec](spec/wire-format.md#datetime-extension-format)) | ✅ PyPI 0.22.0 (wheel run 2026-10-07): the default path revives `__datetime__`, `__date__` and `__time__` (each `temporal_sentinel_vectors` payload under `StandardSerializer`), as do `AutoSerializer` and interop mode, and all three write them; the three `wire-format.json` 1.4.0 `temporal_sentinel_vectors` run in CI since 0.23.0 ([cachekit-py#550](https://github.com/cachekit-io/cachekit-py/pull/550)) | ❌ crates.io 0.10.0 (`.crate` inspected 2026-10-07): revives none and writes none, in either mode; interop's value profile refuses a datetime; `main` at `b5fb2a6` likewise (no sentinel reviver in `src/`). cachekit-rs vendors no `wire-format.json` | ❌ npm 0.2.1 (`.tgz` inspected 2026-10-24; 0.1.5, inspected 2026-10-07, behaves the same): the default path revives none, and writes a `Date` as its ISO string (`dist/serialization/serializer.js`); interop mode revives only `__datetime__`, leaving `__date__` and `__time__` as maps, since JavaScript has no date-only or time-only type. Since 0.2.1 the `wire-format.json` 1.4.0 `temporal_sentinel_vectors` run in both lanes as expected failures naming [WIRE-20](spec/wire-format.md#wire-20), each pinning the map the read returns ([cachekit-ts#229](https://github.com/cachekit-io/cachekit-ts/pull/229)) | ⚠️ Untested | | Encryption (AES-256-GCM) | ✅ Compliant | ✅ Canonical (cachekit-core) | ✅ Compliant | ⚠️ Untested | | AAD v0x03 | ✅ Compliant 0.23.0+ (5 components — every auto serializer appends `original_type`; interop mode is the sole 4-component path; wheels tested 2026-10-19). Earlier releases fall short: through 0.22.0 the `cache_key` component omits a prefixing backend's key prefix, short of the [logical backend key](spec/encryption.md#additional-authenticated-data-aad) ([cachekit-py#518](https://github.com/cachekit-io/cachekit-py/pull/518); see the Encryption table); and, against [ENC-2](spec/encryption.md#enc-2)/[ENC-3](spec/encryption.md#enc-3), 0.9.0 through 0.22.0 accept bare Arrow IPC on an encrypted Arrow read ([cachekit-py#517](https://github.com/cachekit-io/cachekit-py/pull/517)), 0.9.0 through 0.22.0 sniff the decrypted plaintext on a config-drift read by an `auto` handler with encryption off of an entry stored under the `auto` serializer name ([cachekit-py#521](https://github.com/cachekit-io/cachekit-py/pull/521)), 0.1.0 through 0.22.0 sniff it in a directly built `EncryptionWrapper(serializer=AutoSerializer())` ([cachekit-py#521](https://github.com/cachekit-io/cachekit-py/pull/521)), and 0.1.0 through 0.4.0 also sniff it with the default `EncryptionWrapper` constructor and on every encrypted read by a cache handler built with `encryption=True`, not from 0.6.1 (see the Encryption section) | ✅ Compliant (4 components) | ✅ Compliant (4 components) | ❌ Not implemented | | SaaS API | ⚠️ Compliant (CachekitIO backend) except, through `cachekit` 0.22.0 (wheel inspected 2026-10-05): a PUT with a TTL also sends the legacy `X-TTL` ([API-44](spec/saas-api.md#api-44)); any fractional `ttl`, the window `@cache.io` derives from it, and a fractional `refresh_ttl` value go on the wire unrounded, so the server answers `400` and the write is lost ([API-42](spec/saas-api.md#api-42), [API-54](spec/saas-api.md#api-54)); both fixed in 0.23.0 ([cachekit-py#520](https://github.com/cachekit-io/cachekit-py/pull/520)). Where the [TTL rules](spec/saas-api.md#put-v1cachekey) have the SDK reject a TTL with an error to the caller, `ttl=0` and a TTL over 7,776,000 are sent instead, so the server answers `400` and the write is lost. Through 0.25.1 (wheel inspected 2026-10-31) a decorator refuses a `ttl + stale_ttl` over 2,592,000 with `ConfigurationError`, though the [stale-window range](spec/saas-api.md#validation) now runs to 7,776,000 (`_max_total_ttl` in `decorators/wrapper.py`) | ⚠️ Compliant (CachekitIO backend) except, through `cachekit-rs` 0.10.0 (`.crate` inspected 2026-10-05): `refresh_ttl` on the CachekitIO and Workers backends rejects a sub-second TTL instead of sending 1 ([API-42](spec/saas-api.md#api-42); `PATCH /ttl` takes the `X-CacheKit-TTL` rules), fixed in 0.11.0 ([cachekit-rs#142](https://github.com/cachekit-io/cachekit-rs/pull/142)); and a TTL over 7,776,000 is sent, where the [TTL rules](spec/saas-api.md#put-v1cachekey) have the SDK reject it, so the server answers `400` and the write is lost (`validate_ttl` rejects only zero) | ⚠️ Compliant (CachekitIO backend) from npm 0.2.1 on the [TTL rules](spec/saas-api.md#put-v1cachekey), [API-44](spec/saas-api.md#api-44) and [API-65](spec/saas-api.md#api-65) (`.tgz` inspected 2026-10-24): a PUT sends only `X-CacheKit-TTL`, ceiled to whole seconds, and refuses a zero, negative, non-finite or over-2,592,000 TTL with `ConfigurationError` before sending. That last bound predates the 90-day TTL range, so through 0.2.3 (`.tgz` inspected 2026-10-31) it refuses a TTL from 2,592,001 to 7,776,000 that the spec's TTL range allows (the server takes them once it implements the 90-day range) (`MAX_TTL_SECONDS` in `dist/backends/cachekitio.js`) ([cachekit-ts#110](https://github.com/cachekit-io/cachekit-ts/pull/110); `validateTtl` in `dist/backends/cachekitio.js`); 🚧 unreleased, on `main` for npm 0.3.0 ([cachekit-ts#274](https://github.com/cachekit-io/cachekit-ts/pull/274)): a `wrap()` with `staleTtl` also sends `X-CacheKit-Stale-TTL`, ceiled to whole seconds and checked against [API-54](spec/saas-api.md#api-54) and a 30-day cap before sending (`validateStaleTtl`), where the [stale-window range](spec/saas-api.md#validation) now runs to 7,776,000, and every write of that key sends it again ([API-55](spec/saas-api.md#api-55)); a lock response is a lease only when its `lock_id` is a non-empty string ([API-65](spec/saas-api.md#api-65); [cachekit-ts#215](https://github.com/cachekit-io/cachekit-ts/pull/215); `dist/backends/cachekitio-lockable.js`); `src/backends/cachekitio-ttl.test.ts` and `src/backends/cachekitio-lockable.test.ts` pass at tag `cachekit-v0.2.1`. ⚠️ Through 0.1.5 (`.tgz` inspected 2026-10-05) a PUT sends only the legacy `X-TTL` ([API-44](spec/saas-api.md#api-44)), unrounded, omits it for `ttl` 0, so the entry is stored with no expiry, or with the [plan maximum TTL](spec/saas-api.md#plan-maximum-ttl) on a plan with one, and sends a TTL over 7,776,000 for the server to reject with `400`; and a `200` lock response whose `lock_id` is absent, empty or not a string counts as a lease, so single-flight is off and the release sends `undefined` as the lock id | ❌ Not implemented | | SaaS API — cache-key path encoding ([spec](spec/saas-api.md#cache-key-path-encoding)) | ✅ Compliant — rejects the empty key since 0.21.0 ([cachekit-py#397](https://github.com/cachekit-io/cachekit-py/pull/397)); `.`, `..`, `health`, `ttl` and `lock` since 0.20.0 ([cachekit-py#364](https://github.com/cachekit-io/cachekit-py/pull/364)); rules 1/3/4 since 0.18.0 ([cachekit-py#279](https://github.com/cachekit-io/cachekit-py/pull/279)) | ✅ Compliant — rejects the empty key since `cachekit-rs` 0.9.0 ([cachekit-rs#100](https://github.com/cachekit-io/cachekit-rs/pull/100); `.crate` inspected 2026-10-10); rejects `.`, `..`, `health`, `ttl` and `lock` since `cachekit-rs` 0.8.0 ([cachekit-rs#76](https://github.com/cachekit-io/cachekit-rs/pull/76); `.crate` inspected 2026-09-30) | ✅ Compliant — `@cachekit-io/cachekit` 0.2.1+ ([cachekit-ts#118](https://github.com/cachekit-io/cachekit-ts/pull/118); `encodeKey` in `dist/backends/cachekitio.js`, `.tgz` inspected 2026-10-24; `test/protocol/path-encoding.protocol.test.ts` passes at tag `cachekit-v0.2.1`); absent from 0.1.5 | ❌ Not implemented | -| Test vectors in CI¹⁶ | ✅ interop/v1 (full set, incl. AAD + encryption through the real stack) — fixture 1.1.0 (`ns`/`nsapi` namespace reservation) since PyPI 0.20.0 ([cachekit-py#350](https://github.com/cachekit-io/cachekit-py/pull/350)); fixture 1.2.0 (no `..` in a segment) since 0.21.0 ([cachekit-py#391](https://github.com/cachekit-io/cachekit-py/pull/391)); fixture 1.3.0 vendored byte for byte (sha256-pinned) + CI-executed since 0.23.0 ([cachekit-py#537](https://github.com/cachekit-io/cachekit-py/pull/537)), its key, value and error vectors and both reader groups through the interop key derivation and value reader; `decode-bounds.json` vendored + CI-executed since [cachekit-py#276](https://github.com/cachekit-io/cachekit-py/pull/276) (LAB-2503); `1.1.0` since 0.20.0 ([cachekit-py#363](https://github.com/cachekit-io/cachekit-py/pull/363)); `1.2.0` (the `incomplete` reason) vendored byte for byte (sha256-pinned) + CI-executed since 0.23.0 ([cachekit-py#537](https://github.com/cachekit-io/cachekit-py/pull/537)), both `incomplete` vectors included; every version asserting the structural guard's own error for every reject vector at every payload read path (the [Decode bounds](spec/interop-mode.md#decode-bounds) MUST); at the envelope entry point (`ByteStorage.retrieve`), every `1.2.0` reject vector asserts cachekit-core 0.7.0's pre-scan error, with the reason for each depth-only and overclaim-only vector, and every accept vector gets past the pre-scan, in [cachekit-py#627](https://github.com/cachekit-io/cachekit-py/pull/627) (since 0.25.0; see the Wire format row); `path-encoding.json` vendored + CI-executed since 0.20.0 ([cachekit-py#364](https://github.com/cachekit-io/cachekit-py/pull/364)); fixture 1.1.0 (empty-key row) since 0.21.0 ([cachekit-py#397](https://github.com/cachekit-io/cachekit-py/pull/397)); `path-encoding.json` 1.2.0 and `freshness-headers.json` 1.0.0 vendored byte for byte (sha256-pinned) + CI-executed since 0.23.0 ([cachekit-py#522](https://github.com/cachekit-io/cachekit-py/pull/522)); `cache-keys.json` 1.1.0 ([cachekit-py#460](https://github.com/cachekit-io/cachekit-py/pull/460)), `file-backend.json` 1.1.0 ([cachekit-py#431](https://github.com/cachekit-io/cachekit-py/pull/431)) and `wire-format.json` 1.3.0 ([cachekit-py#448](https://github.com/cachekit-io/cachekit-py/pull/448); its reject vectors: see the Wire format row) vendored byte for byte (sha256-pinned) + CI-executed, each since 0.21.0; `wire-format.json` 1.4.0 (every new accept, constructed, reject, payload-reject and temporal vector), `file-backend.json` 1.2.0 (every `miss_preserve` vector on all five read paths, the expired flagged entries included, and every `return_payload` vector) and `python-frame.json` (first vendor: every frame vector through `SerializationWrapper.unwrap`, every error vector at the check its `rejected_by` names, the CK frame through the interop reader, and every `encrypted_read_vectors` frame failing closed under both tamper policies with the tenant resolved by the reader) vendored byte for byte (sha256-pinned) + CI-executed since 0.23.0 ([cachekit-py#550](https://github.com/cachekit-io/cachekit-py/pull/550)); `encryption.json` `1.5.0` (first vendor: both `master_key_input` accept rows and every reject row, every `keyring.configuration` row, every `aad_reject_vectors` row failing authentication, and the `decrypted_container` rows for its checksummed Arrow and orjson readers returning no value), `cache-keys.json` `1.3.0` (every key, error and serializer-object vector) and the `python-frame.json` revision of [protocol#180](https://github.com/cachekit-io/protocol/pull/180) (11 frame vectors: not yet the [protocol#193](https://github.com/cachekit-io/protocol/pull/193) revision, which adds `padded_header_frame`), each vendored byte for byte (sha256-pinned) + CI-executed since 0.23.0 ([cachekit-py#561](https://github.com/cachekit-io/cachekit-py/pull/561)) — ⚠️ except two `keyring.configuration` cases, `keyring_current_key_decrypt_only` and its uppercase twin, each with the current key given as `master_key=`, which run as strict expected failures naming [ENC-9](spec/encryption.md#enc-9): cachekit-py refuses the current key repeated in `CACHEKIT_PREVIOUS_MASTER_KEYS` only on the first call, not at load | ✅ interop/v1 (full set) since [#33](https://github.com/cachekit-io/cachekit-rs/pull/33) — fixture 1.1.0 since `cachekit-rs` 0.8.0 ([cachekit-rs#89](https://github.com/cachekit-io/cachekit-rs/pull/89); `.crate` inspected 2026-09-30); fixture 1.2.0 since 0.9.0 ([cachekit-rs#98](https://github.com/cachekit-io/cachekit-rs/pull/98); `.crate` inspected 2026-10-06); fixture 1.3.0 vendored byte for byte (sha256-pinned) + CI-executed in [cachekit-rs#152](https://github.com/cachekit-io/cachekit-rs/pull/152), since 0.11.0, both reader groups through `interop::deserialize` and every value vector read back through it; `decode-bounds.json` vendored + CI-executed since [cachekit-rs#73](https://github.com/cachekit-io/cachekit-rs/pull/73) (LAB-2503; default CI green on `main`); `1.1.0` since 0.8.0 ([cachekit-rs#93](https://github.com/cachekit-io/cachekit-rs/pull/93)); `1.2.0` (the `incomplete` reason) in [cachekit-rs#152](https://github.com/cachekit-io/cachekit-rs/pull/152), since 0.11.0, asserting the structural guard's own `decode bound:` error for every reject vector through both decoders and `get` / `interop_get` / `interop_get_swr` — the guard enforces depth itself since [cachekit-rs#93](https://github.com/cachekit-io/cachekit-rs/pull/93) (no envelope entry point¹⁵); `path-encoding.json` 1.1.0 vendored byte for byte (sha256-pinned) + CI-executed since 0.9.0 ([cachekit-rs#100](https://github.com/cachekit-io/cachekit-rs/pull/100); `.crate` inspected 2026-10-10), driving both the `encode_key` and the URL-builder tests; fixture 1.2.0 (encoder-mistake rows) and `freshness-headers.json` 1.0.0, vendored byte for byte (sha256-pinned) + CI-executed in [cachekit-rs#146](https://github.com/cachekit-io/cachekit-rs/pull/146), since 0.11.0, every freshness row run through the `GET` response's header reader (`freshness_from_headers`); `file-backend.json` 1.1.0 vendored byte for byte + CI-executed since 0.9.0 ([cachekit-rs#107](https://github.com/cachekit-io/cachekit-rs/pull/107); `.crate` inspected 2026-10-05), by tests in `src/backend/file.rs` behind the `file` feature, which default CI enables; `1.2.0` re-vendored byte for byte (sha256-pinned) + CI-executed in [cachekit-rs#158](https://github.com/cachekit-io/cachekit-rs/pull/158), since 0.11.0, every vector through both header read paths (`read_entry` behind `get`, `probe_header` behind `exists` and `ttl`), the expired flagged entries included, and the five vectors a version-1 writer can produce through `write_entry`; `python-frame.json` first vendored byte for byte (sha256-pinned) + CI-executed in [cachekit-rs#158](https://github.com/cachekit-io/cachekit-rs/pull/158) and re-vendored in [cachekit-rs#160](https://github.com/cachekit-io/cachekit-rs/pull/160), both since 0.11.0, its 29 CK-prefixed vectors refused by both decoders and `get` / `interop_get` / `interop_get_swr`, on a client without encryption (auto mode by the trailing bytes, interop mode by naming the frame) — ⚠️ except `bare_envelope_fed_to_frame_reader`, which decodes¹⁵; `encryption.json` first vendored byte for byte (sha256-pinned) + CI-executed at `1.3.0` in [cachekit-rs#150](https://github.com/cachekit-io/cachekit-rs/pull/150) and re-vendored at `1.5.0` in [cachekit-rs#160](https://github.com/cachekit-io/cachekit-rs/pull/160), both since 0.11.0: both master key input accept rows accepted by every hex and raw-bytes entry point and decrypted through the `secure` hex and `CACHEKIT_MASTER_KEY` paths, the `keyring.configuration` rows at load, and the `aad_reject_vectors` and `decrypted_container` rows whose AAD this reader builds, each through the reads of `get` / `interop_get` / `interop_get_swr` that can present it (`get` only for the namespaced-prefix row, the interop reads only for the two interop-reader container rows) | ✅ interop/v1 (full set, incl. its key vectors) + inline Python-generated AAD-construction and encryption (decrypt-Python-ciphertext) vectors — every "since 0.2.1" below is a PR in the `cachekit-v0.2.1` tag's history (checked 2026-10-24; the Node lane's `test/protocol` suite passes at that tag against the published `cachekit-core-ts` 0.2.0 binding); fixture 1.1.0 since npm 0.2.1 ([cachekit-ts#143](https://github.com/cachekit-io/cachekit-ts/pull/143); `'nsapi'` in `dist/serialization/interop.js` of the 0.2.1 `.tgz`, absent from 0.1.5's `validateInteropSegment`); fixture 1.2.0 since 0.2.1 ([cachekit-ts#164](https://github.com/cachekit-io/cachekit-ts/pull/164); `must not contain '..'` in `dist/serialization/interop.js`, absent from 0.1.5); fixture 1.3.0 vendored byte for byte (sha256-pinned) + CI-executed in [cachekit-ts#224](https://github.com/cachekit-io/cachekit-ts/pull/224), since 0.2.1: the key, value and error vectors and both reader groups, every value vector read back through `decodeInteropValue` as its value, with the reader fixes two of them needed (a `__proto__` map key reads as an ordinary key, and a leading U+FEFF is kept); decode bounds enforced ([#112](https://github.com/cachekit-io/cachekit-ts/pull/112)); `decode-bounds.json` vendored + CI-executed since [cachekit-ts#121](https://github.com/cachekit-io/cachekit-ts/pull/121) (LAB-2737), `1.1.0` in [cachekit-ts#152](https://github.com/cachekit-io/cachekit-ts/pull/152), since 0.2.1; `1.2.0` (the `incomplete` reason) in [cachekit-ts#224](https://github.com/cachekit-io/cachekit-ts/pull/224), since 0.2.1, both incomplete vectors through all three untrusted decode entry points (`MessagePackSerializer.decode`, `decodeInteropValue`, `deserializeEvent`), asserting the pre-scan's own error; every reject vector asserts the guard error it trips (pre-scan, or the event size cap ahead of it) — ⚠️ except, through npm 0.2.1, the envelope entry point (`unpack` in `cachekit-core-ts` and `cachekit-core-wasm`), whose bindings embed `cachekit-core` 0.6.0 and so have no guard to assert; 🚧 unreleased — on `main`, `unpack` runs every vector too, in the Node lane and inside workerd, in [cachekit-ts#245](https://github.com/cachekit-io/cachekit-ts/pull/245): each reject vector fails with `cachekit-core` 0.7.0's envelope pre-scan error (both bindings on core 0.7.0 since [cachekit-ts#240](https://github.com/cachekit-io/cachekit-ts/pull/240)), pinning the depth bound on depth-only vectors and the byte or slot budget on overclaim-only ones, and each accept vector gets past the pre-scan to core's typed decode; it stays unreleased until a published `@cachekit-io/cachekit` depends on `cachekit-core-ts` and `cachekit-core-wasm` releases that embed core 0.7.0, because npm 0.2.1 pins both at exactly 0.2.0 (see the Wire format row); for the three reject vectors over the invalidation event entry point's (`deserializeEvent`) 4096 B size cap, the test also calls that site's structural guard (`assertDecodeDepth`, at depth 3) directly and asserts its rejection, as [Decode bounds](spec/interop-mode.md#decode-bounds) requires where a size cap rejects first, in [cachekit-ts#182](https://github.com/cachekit-io/cachekit-ts/pull/182), since 0.2.1; `path-encoding.json` vendored + CI-executed since [cachekit-ts#118](https://github.com/cachekit-io/cachekit-ts/pull/118), since 0.2.1; fixture 1.1.0 (empty-key row) in [cachekit-ts#165](https://github.com/cachekit-io/cachekit-ts/pull/165), since 0.2.1; fixture 1.2.0 (encoder-mistake rows) in [cachekit-ts#217](https://github.com/cachekit-io/cachekit-ts/pull/217), since 0.2.1, every row through `encodeKey` and all eight request-path operations; `freshness-headers.json` 1.0.0 vendored byte for byte (sha256-pinned) + CI-executed in [cachekit-ts#217](https://github.com/cachekit-io/cachekit-ts/pull/217), since 0.2.1, each row through a real `Headers` and `freshnessFromHeaders`; `wire-format.json` 1.3.0 vendored byte for byte + CI-executed in the Node and Workers lanes in [cachekit-ts#184](https://github.com/cachekit-io/cachekit-ts/pull/184), since 0.2.1 (its reject vectors: see the Wire format row); `1.4.0` re-vendored byte for byte (sha256-pinned) in [cachekit-ts#229](https://github.com/cachekit-io/cachekit-ts/pull/229), since 0.2.1, every group in both lanes (reject and payload reject vectors: see the Wire format row; temporal sentinels: see their row); `file-backend.json` 1.2.0 in [cachekit-ts#229](https://github.com/cachekit-io/cachekit-ts/pull/229) (footnote ⁴); `python-frame.json` vendored byte for byte (sha256-pinned) + CI-executed in [cachekit-ts#229](https://github.com/cachekit-io/cachekit-ts/pull/229), since 0.2.1, for its container vectors ([WIRE-21](spec/wire-format.md#wire-21)): the envelope read path and the interop reader refuse `ck_frame_fed_to_interop_reader`, the envelope read path refuses `plain_msgpack_fed_to_frame_reader`, and `bare_envelope_fed_to_frame_reader`, cachekit-ts's own default container, is read by every cachekit-ts reader; the [protocol#180](https://github.com/cachekit-io/protocol/pull/180) revision re-vendored byte for byte (sha256-pinned) in [cachekit-ts#230](https://github.com/cachekit-io/cachekit-ts/pull/230), since 0.2.1, where both readers also refuse the error vectors `serializer_name_missing` and `serializer_name_empty`, whole CK v3 frames. Its frame-check and `encrypted_read_vectors` groups target cachekit-py's CK frame reader, which cachekit-ts does not have; `encryption.json` vendored + CI-executed in the Workers lane since 0.1.4 ([cachekit-ts#78](https://github.com/cachekit-io/cachekit-ts/pull/78)); `1.2.0`, with its `keyring` and `default_tenant` groups, re-vendored byte for byte (sha256-pinned) in [cachekit-ts#211](https://github.com/cachekit-io/cachekit-ts/pull/211), since 0.2.1; `1.3.0` re-vendored byte for byte (sha256-pinned) in [cachekit-ts#222](https://github.com/cachekit-io/cachekit-ts/pull/222), since 0.2.1, its accept and eleven hex reject master key input rows driven through `createCache.secure` with the key passed both as `masterKey` and through `CACHEKIT_MASTER_KEY` (cachekit-ts has no raw-key entry point, so the raw-bytes rows do not apply); `1.5.0` re-vendored byte for byte (sha256-pinned) in [cachekit-ts#230](https://github.com/cachekit-io/cachekit-ts/pull/230), since 0.2.1: both master key accept rows, `master_key_first_byte_80` included, through `createCache.secure`; each `keyring.configuration` row accepted or refused as its `verdict` says, through `encryption.previousMasterKeys`, `createCache.secure`'s `previousMasterKeys` and `CACHEKIT_PREVIOUS_MASTER_KEYS`; the five `aad_reject_vectors` rows whose AAD inputs a cachekit-ts reader builds, each failing authentication with degradation off (the other five need an `original_type` or a format no cachekit-ts reader builds); the four `decrypted_container` rows that name a cachekit-ts reader (the Arrow and orjson rows name cachekit-py's); and the raw wasm bindings decrypt `standard_serializer_default` and `standard_serializer_integrity_off` in the Workers lane, each under its own `aad_hex` (no cachekit-ts reader builds their five-component AAD) | ⚠️ Pending | +| Test vectors in CI¹⁶ | ✅ interop/v1 (full set, incl. AAD + encryption through the real stack) — fixture 1.1.0 (`ns`/`nsapi` namespace reservation) since PyPI 0.20.0 ([cachekit-py#350](https://github.com/cachekit-io/cachekit-py/pull/350)); fixture 1.2.0 (no `..` in a segment) since 0.21.0 ([cachekit-py#391](https://github.com/cachekit-io/cachekit-py/pull/391)); fixture 1.3.0 vendored byte for byte (sha256-pinned) + CI-executed since 0.23.0 ([cachekit-py#537](https://github.com/cachekit-io/cachekit-py/pull/537)), its key, value and error vectors and both reader groups through the interop key derivation and value reader; `decode-bounds.json` vendored + CI-executed since [cachekit-py#276](https://github.com/cachekit-io/cachekit-py/pull/276) (LAB-2503); `1.1.0` since 0.20.0 ([cachekit-py#363](https://github.com/cachekit-io/cachekit-py/pull/363)); `1.2.0` (the `incomplete` reason) vendored byte for byte (sha256-pinned) + CI-executed since 0.23.0 ([cachekit-py#537](https://github.com/cachekit-io/cachekit-py/pull/537)), both `incomplete` vectors included; every version asserting the structural guard's own error for every reject vector at every payload read path (the [Decode bounds](spec/interop-mode.md#decode-bounds) MUST); at the envelope entry point (`ByteStorage.retrieve`), every `1.2.0` reject vector asserts cachekit-core 0.7.0's pre-scan error, with the reason for each depth-only and overclaim-only vector, and every accept vector gets past the pre-scan, in [cachekit-py#627](https://github.com/cachekit-io/cachekit-py/pull/627) (since 0.25.0; see the Wire format row); `path-encoding.json` vendored + CI-executed since 0.20.0 ([cachekit-py#364](https://github.com/cachekit-io/cachekit-py/pull/364)); fixture 1.1.0 (empty-key row) since 0.21.0 ([cachekit-py#397](https://github.com/cachekit-io/cachekit-py/pull/397)); `path-encoding.json` 1.2.0 and `freshness-headers.json` 1.0.0 vendored byte for byte (sha256-pinned) + CI-executed since 0.23.0 ([cachekit-py#522](https://github.com/cachekit-io/cachekit-py/pull/522)); `cache-keys.json` 1.1.0 ([cachekit-py#460](https://github.com/cachekit-io/cachekit-py/pull/460)), `file-backend.json` 1.1.0 ([cachekit-py#431](https://github.com/cachekit-io/cachekit-py/pull/431)) and `wire-format.json` 1.3.0 ([cachekit-py#448](https://github.com/cachekit-io/cachekit-py/pull/448); its reject vectors: see the Wire format row) vendored byte for byte (sha256-pinned) + CI-executed, each since 0.21.0; `wire-format.json` 1.4.0 (every new accept, constructed, reject, payload-reject and temporal vector), `file-backend.json` 1.2.0 (every `miss_preserve` vector on all five read paths, the expired flagged entries included, and every `return_payload` vector) and `python-frame.json` (first vendor: every frame vector through `SerializationWrapper.unwrap`, every error vector at the check its `rejected_by` names, the CK frame through the interop reader, and every `encrypted_read_vectors` frame failing closed under both tamper policies with the tenant resolved by the reader) vendored byte for byte (sha256-pinned) + CI-executed since 0.23.0 ([cachekit-py#550](https://github.com/cachekit-io/cachekit-py/pull/550)); `encryption.json` `1.5.0` (first vendor: both `master_key_input` accept rows and every reject row, every `keyring.configuration` row, every `aad_reject_vectors` row failing authentication, and the `decrypted_container` rows for its checksummed Arrow and orjson readers returning no value), `cache-keys.json` `1.3.0` (every key, error and serializer-object vector) and the `python-frame.json` revision of [protocol#180](https://github.com/cachekit-io/protocol/pull/180) (11 frame vectors: not yet the [protocol#193](https://github.com/cachekit-io/protocol/pull/193) revision, which adds `padded_header_frame`), each vendored byte for byte (sha256-pinned) + CI-executed since 0.23.0 ([cachekit-py#561](https://github.com/cachekit-io/cachekit-py/pull/561)) — ⚠️ except two `keyring.configuration` cases, `keyring_current_key_decrypt_only` and its uppercase twin, each with the current key given as `master_key=`, which run as strict expected failures naming [ENC-9](spec/encryption.md#enc-9): cachekit-py refuses the current key repeated in `CACHEKIT_PREVIOUS_MASTER_KEYS` only on the first call, not at load | ✅ interop/v1 (full set) since [#33](https://github.com/cachekit-io/cachekit-rs/pull/33) — fixture 1.1.0 since `cachekit-rs` 0.8.0 ([cachekit-rs#89](https://github.com/cachekit-io/cachekit-rs/pull/89); `.crate` inspected 2026-09-30); fixture 1.2.0 since 0.9.0 ([cachekit-rs#98](https://github.com/cachekit-io/cachekit-rs/pull/98); `.crate` inspected 2026-10-06); fixture 1.3.0 vendored byte for byte (sha256-pinned) + CI-executed in [cachekit-rs#152](https://github.com/cachekit-io/cachekit-rs/pull/152), since 0.11.0, both reader groups through `interop::deserialize` and every value vector read back through it; `decode-bounds.json` vendored + CI-executed since [cachekit-rs#73](https://github.com/cachekit-io/cachekit-rs/pull/73) (LAB-2503; default CI green on `main`); `1.1.0` since 0.8.0 ([cachekit-rs#93](https://github.com/cachekit-io/cachekit-rs/pull/93)); `1.2.0` (the `incomplete` reason) in [cachekit-rs#152](https://github.com/cachekit-io/cachekit-rs/pull/152), since 0.11.0, asserting the structural guard's own `decode bound:` error for every reject vector through both decoders and `get` / `interop_get` / `interop_get_swr` — the guard enforces depth itself since [cachekit-rs#93](https://github.com/cachekit-io/cachekit-rs/pull/93) (no envelope entry point¹⁵); `path-encoding.json` 1.1.0 vendored byte for byte (sha256-pinned) + CI-executed since 0.9.0 ([cachekit-rs#100](https://github.com/cachekit-io/cachekit-rs/pull/100); `.crate` inspected 2026-10-10), driving both the `encode_key` and the URL-builder tests; fixture 1.2.0 (encoder-mistake rows) and `freshness-headers.json` 1.0.0, vendored byte for byte (sha256-pinned) + CI-executed in [cachekit-rs#146](https://github.com/cachekit-io/cachekit-rs/pull/146), since 0.11.0, every freshness row run through the `GET` response's header reader (`freshness_from_headers`); `file-backend.json` 1.1.0 vendored byte for byte + CI-executed since 0.9.0 ([cachekit-rs#107](https://github.com/cachekit-io/cachekit-rs/pull/107); `.crate` inspected 2026-10-05), by tests in `src/backend/file.rs` behind the `file` feature, which default CI enables; `1.2.0` re-vendored byte for byte (sha256-pinned) + CI-executed in [cachekit-rs#158](https://github.com/cachekit-io/cachekit-rs/pull/158), since 0.11.0, every vector through both header read paths (`read_entry` behind `get`, `probe_header` behind `exists` and `ttl`), the expired flagged entries included, and the five vectors a version-1 writer can produce through `write_entry`; `python-frame.json` first vendored byte for byte (sha256-pinned) + CI-executed in [cachekit-rs#158](https://github.com/cachekit-io/cachekit-rs/pull/158) and re-vendored in [cachekit-rs#160](https://github.com/cachekit-io/cachekit-rs/pull/160), both since 0.11.0, its 29 CK-prefixed vectors refused by both decoders and `get` / `interop_get` / `interop_get_swr`, on a client without encryption (auto mode by the trailing bytes, interop mode by naming the frame) — ⚠️ except `bare_envelope_fed_to_frame_reader`, which decodes¹⁵; `encryption.json` first vendored byte for byte (sha256-pinned) + CI-executed at `1.3.0` in [cachekit-rs#150](https://github.com/cachekit-io/cachekit-rs/pull/150) and re-vendored at `1.5.0` in [cachekit-rs#160](https://github.com/cachekit-io/cachekit-rs/pull/160), both since 0.11.0: both master key input accept rows accepted by every hex and raw-bytes entry point and decrypted through the `secure` hex and `CACHEKIT_MASTER_KEY` paths, the `keyring.configuration` rows at load, and the `aad_reject_vectors` and `decrypted_container` rows whose AAD this reader builds, each through the reads of `get` / `interop_get` / `interop_get_swr` that can present it (`get` only for the namespaced-prefix row, the interop reads only for the two interop-reader container rows) | ✅ interop/v1 (full set, incl. its key vectors) + inline Python-generated AAD-construction and encryption (decrypt-Python-ciphertext) vectors — every "since 0.2.1" below is a PR in the `cachekit-v0.2.1` tag's history (checked 2026-10-24; the Node lane's `test/protocol` suite passes at that tag against the published `cachekit-core-ts` 0.2.0 binding); fixture 1.1.0 since npm 0.2.1 ([cachekit-ts#143](https://github.com/cachekit-io/cachekit-ts/pull/143); `'nsapi'` in `dist/serialization/interop.js` of the 0.2.1 `.tgz`, absent from 0.1.5's `validateInteropSegment`); fixture 1.2.0 since 0.2.1 ([cachekit-ts#164](https://github.com/cachekit-io/cachekit-ts/pull/164); `must not contain '..'` in `dist/serialization/interop.js`, absent from 0.1.5); fixture 1.3.0 vendored byte for byte (sha256-pinned) + CI-executed in [cachekit-ts#224](https://github.com/cachekit-io/cachekit-ts/pull/224), since 0.2.1: the key, value and error vectors and both reader groups, every value vector read back through `decodeInteropValue` as its value, with the reader fixes two of them needed (a `__proto__` map key reads as an ordinary key, and a leading U+FEFF is kept); decode bounds enforced ([#112](https://github.com/cachekit-io/cachekit-ts/pull/112)); `decode-bounds.json` vendored + CI-executed since [cachekit-ts#121](https://github.com/cachekit-io/cachekit-ts/pull/121) (LAB-2737), `1.1.0` in [cachekit-ts#152](https://github.com/cachekit-io/cachekit-ts/pull/152), since 0.2.1; `1.2.0` (the `incomplete` reason) in [cachekit-ts#224](https://github.com/cachekit-io/cachekit-ts/pull/224), since 0.2.1, both incomplete vectors through all three untrusted decode entry points (`MessagePackSerializer.decode`, `decodeInteropValue`, `deserializeEvent`), asserting the pre-scan's own error; every reject vector asserts the guard error it trips (pre-scan, or the event size cap ahead of it) — ⚠️ except, through npm 0.2.1, the envelope entry point (`unpack` in `cachekit-core-ts` and `cachekit-core-wasm`), whose bindings embed `cachekit-core` 0.6.0 and so have no guard to assert; ✅ since npm 0.2.2, `unpack` runs every vector too, in the Node lane and inside workerd ([cachekit-ts#245](https://github.com/cachekit-io/cachekit-ts/pull/245)): each reject vector fails with `cachekit-core` 0.7.0's envelope pre-scan error (both bindings on core 0.7.0 since [cachekit-ts#240](https://github.com/cachekit-io/cachekit-ts/pull/240); 0.2.2 is the first release that pins bindings embedding it, see the Wire format row), pinning the depth bound on depth-only vectors and the byte or slot budget on overclaim-only ones, and each accept vector gets past the pre-scan to core's typed decode; for the three reject vectors over the invalidation event entry point's (`deserializeEvent`) 4096 B size cap, the test also calls that site's structural guard (`assertDecodeDepth`, at depth 3) directly and asserts its rejection, as [Decode bounds](spec/interop-mode.md#decode-bounds) requires where a size cap rejects first, in [cachekit-ts#182](https://github.com/cachekit-io/cachekit-ts/pull/182), since 0.2.1; `path-encoding.json` vendored + CI-executed since [cachekit-ts#118](https://github.com/cachekit-io/cachekit-ts/pull/118), since 0.2.1; fixture 1.1.0 (empty-key row) in [cachekit-ts#165](https://github.com/cachekit-io/cachekit-ts/pull/165), since 0.2.1; fixture 1.2.0 (encoder-mistake rows) in [cachekit-ts#217](https://github.com/cachekit-io/cachekit-ts/pull/217), since 0.2.1, every row through `encodeKey` and all eight request-path operations; `freshness-headers.json` 1.0.0 vendored byte for byte (sha256-pinned) + CI-executed in [cachekit-ts#217](https://github.com/cachekit-io/cachekit-ts/pull/217), since 0.2.1, each row through a real `Headers` and `freshnessFromHeaders`; `wire-format.json` 1.3.0 vendored byte for byte + CI-executed in the Node and Workers lanes in [cachekit-ts#184](https://github.com/cachekit-io/cachekit-ts/pull/184), since 0.2.1 (its reject vectors: see the Wire format row); `1.4.0` re-vendored byte for byte (sha256-pinned) in [cachekit-ts#229](https://github.com/cachekit-io/cachekit-ts/pull/229), since 0.2.1, every group in both lanes (reject and payload reject vectors: see the Wire format row; temporal sentinels: see their row); `file-backend.json` 1.2.0 in [cachekit-ts#229](https://github.com/cachekit-io/cachekit-ts/pull/229) (footnote ⁴); `python-frame.json` vendored byte for byte (sha256-pinned) + CI-executed in [cachekit-ts#229](https://github.com/cachekit-io/cachekit-ts/pull/229), since 0.2.1, for its container vectors ([WIRE-21](spec/wire-format.md#wire-21)): the envelope read path and the interop reader refuse `ck_frame_fed_to_interop_reader`, the envelope read path refuses `plain_msgpack_fed_to_frame_reader`, and `bare_envelope_fed_to_frame_reader`, cachekit-ts's own default container, is read by every cachekit-ts reader; the [protocol#180](https://github.com/cachekit-io/protocol/pull/180) revision re-vendored byte for byte (sha256-pinned) in [cachekit-ts#230](https://github.com/cachekit-io/cachekit-ts/pull/230), since 0.2.1, where both readers also refuse the error vectors `serializer_name_missing` and `serializer_name_empty`, whole CK v3 frames. Its frame-check and `encrypted_read_vectors` groups target cachekit-py's CK frame reader, which cachekit-ts does not have; `encryption.json` vendored + CI-executed in the Workers lane since 0.1.4 ([cachekit-ts#78](https://github.com/cachekit-io/cachekit-ts/pull/78)); `1.2.0`, with its `keyring` and `default_tenant` groups, re-vendored byte for byte (sha256-pinned) in [cachekit-ts#211](https://github.com/cachekit-io/cachekit-ts/pull/211), since 0.2.1; `1.3.0` re-vendored byte for byte (sha256-pinned) in [cachekit-ts#222](https://github.com/cachekit-io/cachekit-ts/pull/222), since 0.2.1, its accept and eleven hex reject master key input rows driven through `createCache.secure` with the key passed both as `masterKey` and through `CACHEKIT_MASTER_KEY` (cachekit-ts has no raw-key entry point, so the raw-bytes rows do not apply); `1.5.0` re-vendored byte for byte (sha256-pinned) in [cachekit-ts#230](https://github.com/cachekit-io/cachekit-ts/pull/230), since 0.2.1: both master key accept rows, `master_key_first_byte_80` included, through `createCache.secure`; each `keyring.configuration` row accepted or refused as its `verdict` says, through `encryption.previousMasterKeys`, `createCache.secure`'s `previousMasterKeys` and `CACHEKIT_PREVIOUS_MASTER_KEYS`; the five `aad_reject_vectors` rows whose AAD inputs a cachekit-ts reader builds, each failing authentication with degradation off (the other five need an `original_type` or a format no cachekit-ts reader builds); the four `decrypted_container` rows that name a cachekit-ts reader (the Arrow and orjson rows name cachekit-py's); and the raw wasm bindings decrypt `standard_serializer_default` and `standard_serializer_integrity_off` in the Workers lane, each under its own `aad_hex` (no cachekit-ts reader builds their five-component AAD) | ⚠️ Pending | | Interop mode ([spec](spec/interop-mode.md), opt-in) | ✅ Released — PyPI 0.14.0+¹⁷ ([#220](https://github.com/cachekit-io/cachekit-py/pull/220)) | ✅ Released — crates.io 0.4.0+ ([#33](https://github.com/cachekit-io/cachekit-rs/pull/33)) | ✅ Released — npm 0.1.3+ ([#71](https://github.com/cachekit-io/cachekit-ts/pull/71)) | ❌ Not implemented | > [!NOTE]