diff --git a/sdk-feature-matrix.md b/sdk-feature-matrix.md index 977bb10..47356e3 100644 --- a/sdk-feature-matrix.md +++ b/sdk-feature-matrix.md @@ -76,7 +76,7 @@ | Feature | Python | Rust | TypeScript | PHP | | :--- | :---: | :---: | :---: | :---: | -| AES-256-GCM | ✅ via Rust FFI | ✅ ring (native) / aes-gcm (wasm32) — assumes the `encryption` feature¹⁹ | ✅ via NAPI (Rust) | 🔜 Planned | +| AES-256-GCM | ✅ via Rust FFI | ✅ ring (native) / aes-gcm (wasm32) — assumes the `encryption` feature¹⁹ | ✅ via NAPI (Rust); WebCrypto on Workers¹ | 🔜 Planned | | HKDF-SHA256 key derivation | ✅ via Rust FFI | ✅ | ✅ via NAPI (Rust) | 🔜 Planned | | Per-tenant key isolation | ✅ | ✅ | ✅ via TenantKeys NAPI | 🔜 Planned | | AAD v0x03 (cache_key binding) | ✅ 0.23.0+ — the AAD `cache_key` is the [logical backend key](spec/encryption.md#additional-authenticated-data-aad), a prefixing backend's key prefix included (`MemcachedBackend` `key_prefix`, the tenant-scoped Redis backend's `t:{tenant}:`); wheel inspected and `tests/critical/test_aad_backend_key_prefix.py` run against the 0.23.0 and 0.24.0 wheels 2026-10-19. Through 0.22.0 that prefix was left out ([cachekit-py#518](https://github.com/cachekit-io/cachekit-py/pull/518)). Interop mode, which refuses prefixing backends, binds the bare key | ✅ | ✅ | ❌ | @@ -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). 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`) on the raw wasm binding. Since [cachekit-ts#271](https://github.com/cachekit-io/cachekit-ts/pull/271) the Workers `EncryptionManager.isHardwareAccelerated()` returns `null` (unknown) instead: AES-GCM runs through WebCrypto there, and an isolate cannot see whether that uses AES instructions (`webcrypto.workers.test.ts` pins it). 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. --- @@ -121,7 +121,7 @@ | Cache API (Cloudflare) | N/A | ❌ | ✅ `workersCacheAPI` on the `/workers` entry (LAB-750)¹ | N/A | | DynamoDB | ❌² | ❌ | ❌ | ❌ | -> ¹ Shipped in LAB-595 (2026-07-24), following spike LAB-431's GO verdict: the `@cachekit-io/cachekit/workers` subpath (also the `workerd` condition on the root export) runs crypto and the ByteStorage envelope on a **wasm32 build of cachekit-core** (`@cachekit-io/cachekit-core-wasm`, 137 KB raw / ~55 KB gzipped + ~10 KB JS glue, wasm-bindgen `--target web` + wasm-opt `-Oz`) — counter nonces and the envelope carry over unchanged and the crypto stays single-touchpoint in the Rust core; byte-verified against the Python-ground-truth `test-vectors/encryption.json` and `wire-format.json` suites inside real workerd (`wire-format.json` 1.4.0 `vectors`, `constructed_vectors` including the `bin16` → `bin32` edge, and, since npm 0.2.1, all fourteen `reject_vectors` and both `payload_reject_vectors` on the envelope read path — [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); see the Wire format row). Backend surface (phase 2, LAB-750): CachekitIO (pure `fetch`), **Workers KV** (`workersKV({ kv })` over a `KVNamespace` binding; native `expirationTtl` — KV rejects values under its 60s minimum, so the SDK clamps shorter TTLs up to 60s before the `put`, and by an SDK rule (not KV's) `ttl <= 0` stores without expiry; eventually consistent ~60s), the **Cache API** (`workersCacheAPI()` over `caches.default`/named caches; per-data-center only, `Cache-Control: max-age` to the second, best-effort eviction, keys mapped to synthetic never-fetched URLs), or a custom `Backend` instance — all storage transports over the unchanged opaque ByteStorage payload (encryption above the backend; secure caches store ciphertext only). Backend instances are accepted by the `minimal`/`production`/`secure` intents (`{ backend }` in place of `{ url }`, both entrypoints); Redis-URL intents, Redis Pub/Sub invalidation, and Prometheus metrics stay Node-only and are excluded from the edge bundle (CI-guarded: no `node:*`, no NAPI, no ioredis/prom-client — no `nodejs_compat` flag needed); SWR background refresh requires binding the request's `ExecutionContext` — `cache.withExecutionContext(ctx)` returns a cheap per-request view whose refreshes ride `ctx.waitUntil`, because workerd cancels fire-and-forget work at response return; a read on a cache with no bound context fails safe to a plain (no-SWR) L1 get (LAB-751, the module doc comment of `packages/cachekit/src/workers/index.ts`; `workersRuntime.swrRequiresWaitUntil` and `WorkersCache.withExecutionContext` in `workers/runtime.ts`). Semantics delta: keys live in wasm linear memory (a host-readable ArrayBuffer), weaker isolation than NAPI's Rust heap but ~JS-heap-equivalent on Workers where the host is your own isolate; zeroized deterministically on `dispose()`. WebCrypto (AES-256-GCM + HKDF-SHA256, random-nonce fallback per [encryption.md → Nonce Generation](spec/encryption.md#nonce-generation)) remains the documented fallback if the wasm path ever hits a wall. +> ¹ Shipped in LAB-595 (2026-07-24), following spike LAB-431's GO verdict: the `@cachekit-io/cachekit/workers` subpath (also the `workerd` condition on the root export) runs key derivation and the ByteStorage envelope on a **wasm32 build of cachekit-core** (`@cachekit-io/cachekit-core-wasm`, 137 KB raw / ~55 KB gzipped + ~10 KB JS glue, wasm-bindgen `--target web` + wasm-opt `-Oz`) — the envelope carries over unchanged. Since [cachekit-ts#271](https://github.com/cachekit-io/cachekit-ts/pull/271), AES-256-GCM runs through the runtime's WebCrypto (`crypto.subtle`, native code; wasm32 has no AES instructions) on keys the wasm core derives, and the core's counter-nonce scheme (8-byte instance id ‖ 4-byte counter, exhausted at 2^32−1) and keyring decrypt loop are reproduced in JS; HKDF-SHA256, the key fingerprint and the tenant-id bounds stay in the Rust core; byte-verified against the Python-ground-truth `test-vectors/encryption.json` and `wire-format.json` suites inside real workerd (`wire-format.json` 1.4.0 `vectors`, `constructed_vectors` including the `bin16` → `bin32` edge, and, since npm 0.2.1, all fourteen `reject_vectors` and both `payload_reject_vectors` on the envelope read path — [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); see the Wire format row). Backend surface (phase 2, LAB-750): CachekitIO (pure `fetch`), **Workers KV** (`workersKV({ kv })` over a `KVNamespace` binding; native `expirationTtl` — KV rejects values under its 60s minimum, so the SDK clamps shorter TTLs up to 60s before the `put`, and by an SDK rule (not KV's) `ttl <= 0` stores without expiry; eventually consistent ~60s), the **Cache API** (`workersCacheAPI()` over `caches.default`/named caches; per-data-center only, `Cache-Control: max-age` to the second, best-effort eviction, keys mapped to synthetic never-fetched URLs), or a custom `Backend` instance — all storage transports over the unchanged opaque ByteStorage payload (encryption above the backend; secure caches store ciphertext only). Backend instances are accepted by the `minimal`/`production`/`secure` intents (`{ backend }` in place of `{ url }`, both entrypoints); Redis-URL intents, Redis Pub/Sub invalidation, and Prometheus metrics stay Node-only and are excluded from the edge bundle (CI-guarded: no `node:*`, no NAPI, no ioredis/prom-client — no `nodejs_compat` flag needed); SWR background refresh requires binding the request's `ExecutionContext` — `cache.withExecutionContext(ctx)` returns a cheap per-request view whose refreshes ride `ctx.waitUntil`, because workerd cancels fire-and-forget work at response return; a read on a cache with no bound context fails safe to a plain (no-SWR) L1 get (LAB-751, the module doc comment of `packages/cachekit/src/workers/index.ts`; `workersRuntime.swrRequiresWaitUntil` and `WorkersCache.withExecutionContext` in `workers/runtime.ts`). Semantics delta: derived AES keys are imported once as non-extractable WebCrypto `CryptoKey`s (decrypt-only for previous keys); the wasm key handle and every JS copy of derived key bytes are wiped at init, leaving only HKDF's working buffers on the wasm stack in linear memory (a host-readable ArrayBuffer) until later calls overwrite them; a `CryptoKey` cannot be zeroized, so `dispose()` drops it for the garbage collector. Counter nonces stay; the random-nonce option in [encryption.md → Nonce Generation](spec/encryption.md#nonce-generation) is not used. > > ² DynamoDB has never shipped in any SDK. The previous Python ✅ traced to the [custom-backend tutorial](https://github.com/cachekit-io/cachekit-py/blob/main/docs/backends/custom.md), which shows how a *user* can implement the backend protocol against DynamoDB — that is an extension point, not shipped support (LAB-273). >