Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions sdk-feature-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,21 @@

> ⁰ TypeScript Arrow was listed 🔜 Planned from 2026-06-06 with no code, stub, or tracking issue behind it; corrected to ❌, with LAB-524 owning the implement-or-decline decision. Orjson and Arrow live behind cachekit-py's `[json]` / `[data]` extras (`pyproject.toml:73-81`).

### Namespace semantics (per-SDK divergence)

*Audited against code 2026-09-09 (LAB-646): py `cachekit-py/src/cachekit/key_generator.py`, rs `cachekit-rs/crates/cachekit/src/client.rs`, ts `cachekit-ts/packages/cachekit/src/serialization/key-generator.ts` + `types/cache.ts` + `cache-core.ts`, server `saas/apps/cache/src/cache-key-validator.ts` + `namespace-validator.ts` (all `main`). PHP has no namespace implementation to audit. Records what each SDK actually puts on the wire, not what the protocol prescribes — no new key-format requirement is stated or implied here.*

| Semantic | Python | Rust | TypeScript | SaaS server |
| :--- | :--- | :--- | :--- | :--- |
| Key-prefix shape (auto mode) | `ns:{namespace}:func:...` — namespace is its own delimited segment (`spec/cache-key-format.md:36`, `key_generator.py:94-95`) | `{namespace}:{key}` when `.namespace()` is set on the builder — one colon, no `ns:` token (`client.rs:246-250`) | `{namespace}:{blake2b-hex}` — same shape as Rust, no `ns:` token (`key-generator.ts:33-44`) | Only a leading `ns:` or `nsapi:` token is parsed as a namespace prefix; anything else is opaque (`cache-key-validator.ts:50,105-120`) |
| Default namespace (nothing configured) | No SDK-level default — omitting `namespace` drops the `ns:` segment entirely (`key_generator.py:94-95`) | No SDK-level default — `namespace: Option<String>`, `None` → bare key (`client.rs:246-250`) | No SDK-level default — `namespace` is a required, non-optional field with nothing to fall back to (`types/cache.ts:56`) | Any key without a `ns:`/`nsapi:` prefix — every TS/Rust auto-mode key, every interop key, any bare hash — resolves to the literal namespace `default` (`cache-key-validator.ts:118-120`) |
| Empty/unset behaviour | `None` and `""` are both falsy → identical: no `ns:` segment emitted (`key_generator.py:94`) | `None` (never call `.namespace()`) → bare key; `.namespace("")` is accepted unchecked and produces a leading-colon key `:{key}` — a distinct path, not rejected (`client.rs:246-250,913-916`) | `namespace: string` is required at the type level, so "unset" isn't a legal call; an empty string still reaches `generateKey` unchecked and produces a leading-colon key `:{hash}` (`types/cache.ts:56`, `key-generator.ts:44`) | An all-empty raw key is rejected by the length check before namespace shape is inspected (`cache-key-validator.ts:81-83`); a non-empty key with a leading colon isn't `ns:`/`nsapi:`-shaped, so it falls through to the unprefixed/`default` path (`cache-key-validator.ts:118-120`) |
| Charset validation on the namespace value | None — the raw string is spliced into the key; only the unrelated `func:` segment is sanitized (`key_generator.py:364-381`) | None — `.namespace()` accepts any `impl Into<String>` (`client.rs:913-916`); `validate_key` only checks the per-call key argument, never the namespace (`client.rs:76-96`). Because the namespace is spliced directly ahead of the key with one colon (`client.rs:246-250`), a namespace value that itself begins `ns:` or `nsapi:` (e.g. `.namespace("ns:tenant")`) produces a key the SaaS parses as that literal named namespace — see Server-side isolation | None in auto mode (`key-generator.ts:44`) — same splice, same risk as Rust (`${namespace}:${hash}`); interop mode's namespace/operation segments ARE validated against `^[a-z0-9][a-z0-9._-]{0,63}$` at wrap time (`validateInteropSegment`, `cache-core.ts:915`, `types/cache.ts:85-86`), which closes this gap for interop keys only | Enforced only when the key claims a prefix — a `ns:`/`nsapi:` namespace segment must be 1-64 chars of `[a-zA-Z0-9_-]` or the whole key is rejected (`cache-key-validator.ts:50,106-114`); unprefixed keys skip this check entirely. The parser has no notion of which SDK produced the string — it re-splits on `:` from scratch |
| Server-side isolation | `ns:`-prefixed keys are checked against the API key's `allowed_namespaces` grant (`namespace-validator.ts:17-31`, called from `saas/apps/cache/src/index.ts:770`) | A namespace value with no embedded `ns:`/`nsapi:` colon resolves to `default`, checked against the same `allowed_namespaces` grant as any other key; a namespace value that itself starts with `ns:`/`nsapi:` (unvalidated client-side — see Charset validation) instead lands in that literal named namespace, since the SaaS re-parses the finished key with no knowledge of SDK-side intent | Same as Rust — resolves to `default` under the same `allowed_namespaces` check, unless the unvalidated namespace value itself begins `ns:`/`nsapi:`, in which case it lands in that literal named namespace instead | The same `allowed_namespaces` check applies to whatever namespace a key resolves to, `default` included (`namespace-validator.ts:17-31`); `allowed_namespaces` is unrestricted (`IS NULL`) by default, so in practice every writer that stays clear of a `ns:`/`nsapi:`-shaped namespace value shares one `default` bucket with no further authorization boundary between them |

> [!NOTE]
> Unprefixed keys — every TS or Rust SDK key whose namespace value doesn't itself start `ns:`/`nsapi:`, every interop-mode key, any bare hash — resolve to the single server-side `default` namespace and share its `keyClass: 'open'` write space, open to both `ck_sdk_` and `ck_api_` callers alike (`cache-key-validator.ts:56-59,118-120`). They are still subject to whatever `allowed_namespaces` grant applies to `default`; "open" describes the writer-class boundary, not an absence of authorization. Because neither Rust nor TypeScript validates the namespace value's charset, a namespace string that itself begins `ns:`/`nsapi:` is not rejected client-side and is instead parsed by the SaaS as that literal named namespace — an unintended escape from `default`, not a supported way to opt in. This is current behavior, not a recommendation — whether TS/Rust should validate/reject such values, or adopt `ns:`-style prefixing outright, is LAB-640's decision, not this document's.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

---

## Encryption
Expand Down
Loading