release(v4.9.0): relay-side encrypted blob primitive + SDK Profile namespace
Some checks failed
Test / test (push) Has been cancelled
Cross-platform vectors / TypeScript vectors (bun) (push) Has been cancelled
Cross-platform vectors / Kotlin vectors (gradle) (push) Has been cancelled

Ships the Prism FR (encrypted-profile-storage-v4.9.md) as a generic
relay-side encrypted blob primitive: deterministically-located,
AEAD-sealed blobs keyed by a 32-byte slotId derived client-side via
HKDF from the user's master key. Unlocks credential-only bootstrap
of new devices into existing E2EE state — no QR, no physical access.

Server: BlobStore interface + Memory/Sqlite/Postgres impls,
createBlobRoutes for GET/PUT/DELETE /v1/blob/:slotId with TOFU pubkey
auth and If-Match CAS (409/412 semantics). Mounted on the same Hono
app as the inbox; SHADE_BLOB_PG_URL / SHADE_BLOB_DB_PATH /
SHADE_DISABLE_BLOB env-var plumbing in standalone.

SDK: createProfileNamespace high-level wrapper (HKDF derivation,
random-nonce AEAD seal, slotId-bound AAD) + low-level BlobClient.
Cross-platform test vectors in test-vectors/blob-storage.json.

New errors: ConflictError (409), PreconditionFailedError (412).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-09 02:44:42 +02:00
parent 3c0db14904
commit 80c410f518
51 changed files with 2138 additions and 58 deletions

View File

@@ -208,6 +208,30 @@ export async function ensureKTLogTables(sql: Sql): Promise<void> {
`;
}
/**
* V4.9 — encrypted-blob primitive (`/v1/blob/<slotId>`). One row per
* slot, keyed on the 64-hex slotId. ETag is a sequence value so it's
* unique and monotonic across writers (matches the inbox `received_at`
* pattern). The blob column holds base64-encoded AEAD ciphertext —
* the relay never decrypts.
*/
export async function ensureBlobServerTables(sql: Sql): Promise<void> {
await sql`CREATE SEQUENCE IF NOT EXISTS shade_blob_seq`;
await sql`
CREATE TABLE IF NOT EXISTS shade_blob_slots (
slot_id TEXT PRIMARY KEY,
owner_pubkey TEXT NOT NULL,
blob TEXT NOT NULL,
etag BIGINT NOT NULL,
updated_at BIGINT NOT NULL
)
`;
await sql`
CREATE INDEX IF NOT EXISTS shade_blob_updated_idx
ON shade_blob_slots(updated_at)
`;
}
export async function ensureInboxServerTables(sql: Sql): Promise<void> {
await sql`
CREATE TABLE IF NOT EXISTS shade_inbox_owners (