release(v4.9.0): relay-side encrypted blob primitive + SDK Profile namespace
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:
87
CHANGELOG.md
87
CHANGELOG.md
@@ -5,6 +5,93 @@ All notable changes to Shade are documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [4.9.0] — 2026-05-09 — relay-side encrypted blob primitive + SDK `Profile` namespace
|
||||
|
||||
Prism filed a feature request
|
||||
(`encrypted-profile-storage-v4.9.md`) for relay-side encrypted profile
|
||||
storage as the missing cryptographic primitive for Phase 2 of their
|
||||
device-linking work: a brand new browser/device must be able to
|
||||
locate a user's existing E2EE state from credentials alone — no QR,
|
||||
no physical access to a paired device.
|
||||
|
||||
Ships as a *generic* primitive: a deterministically-located,
|
||||
AEAD-sealed blob keyed by a 32-byte slotId derived client-side via
|
||||
HKDF from the user's master key. The relay sees opaque slotIds and
|
||||
opaque ciphertext; it never decrypts and cannot link slots to users.
|
||||
Compare-and-swap via a per-slot etag prevents two devices from
|
||||
silently clobbering each other's writes when a user adds a new
|
||||
paired peer concurrently from two existing devices.
|
||||
|
||||
**Server (`@shade/inbox-server`)**
|
||||
|
||||
- New `BlobStore` interface + `MemoryBlobStore` reference impl. Per-slot
|
||||
layout: `(slotId, ownerPubkey, blob, etag, updatedAt)`. ETag is
|
||||
monotonic per process, clamped against `Date.now()` so values are
|
||||
unique and roughly time-ordered for ops.
|
||||
- New `createBlobRoutes(store, crypto, options)` mounting
|
||||
`GET / PUT / DELETE /v1/blob/:slotId`. SlotId is validated as
|
||||
64 lowercase hex chars (32 bytes); URL-bound into the signed payload
|
||||
to prevent cross-slot signature replay. Owner pubkey is recorded
|
||||
TOFU on the first PUT — subsequent writes verify against it; a
|
||||
different key trying to overwrite an existing slot returns 401.
|
||||
- CAS via `ifMatch`: omitted = create-only (409 on populated slot),
|
||||
numeric etag = strict CAS (412 on mismatch), `'*'` =
|
||||
unconditional overwrite when populated (412 on empty per RFC 7232).
|
||||
- Default per-slot ceiling: 64 KiB. Sized for ~500 host entries in
|
||||
Prism's `hosts[]` JSON form with plenty of headroom.
|
||||
- `createInboxServer` now also mounts the blob primitive on the
|
||||
same Hono app — pass `{ blobStore: null }` to opt out.
|
||||
|
||||
**Storage backends**
|
||||
|
||||
- `SqliteBlobStore` (`@shade/storage-sqlite`) — single-table
|
||||
`shade_blob_slots` with WAL journal mode. Reads CAS state and
|
||||
writes inside a transaction so concurrent CAS attempts can't both
|
||||
observe the same etag. Volume: same path convention as the inbox
|
||||
store; `SHADE_BLOB_DB_PATH` overrides (default `/data/shade-blob.db`).
|
||||
- `PostgresBlobStore` (`@shade/storage-postgres`) — uses
|
||||
`nextval('shade_blob_seq')` so etag ordering is strict across
|
||||
multi-instance deployments. CAS path holds `FOR UPDATE` on the
|
||||
read so the txn serializes against concurrent writers. New
|
||||
`ensureBlobServerTables()` exposed for ops.
|
||||
|
||||
**SDK (`@shade/sdk`)**
|
||||
|
||||
- New `createProfileNamespace({ baseUrl, crypto, masterKey, app })`
|
||||
high-level wrapper. Computes slotId, blobKey, signing seed
|
||||
deterministically; AEAD-seals plaintext with `AAD = "shade-profile-aad-v1:<slotIdHex>"`
|
||||
on every PUT (fresh random nonce); verifies AEAD on every GET.
|
||||
- New low-level `BlobClient` (`@shade/inbox`) — caller-supplied
|
||||
signing key; transports already-sealed ciphertext.
|
||||
- New `ed25519PublicKeyFromSeed(seed)` (`@shade/crypto-web`) for
|
||||
deterministic Ed25519 keypair derivation from a 32-byte seed.
|
||||
- KDF helpers `deriveBlobSlotId / deriveBlobKey / deriveBlobSigningSeed`
|
||||
exposed from `@shade/storage-encrypted/crypto` so apps that want a
|
||||
custom flow (skip the AEAD wrapper, hit a non-Shade relay) don't
|
||||
reimplement the info-string conventions.
|
||||
- `app` namespace string mandatory — distinct apps under the same
|
||||
master MUST pass different values (e.g. `prism-profile-v1`) so
|
||||
they don't collide on the same slot.
|
||||
|
||||
**Standalone server**
|
||||
|
||||
- Boots a `BlobStore` mirroring the inbox-store selection chain:
|
||||
`SHADE_BLOB_PG_URL` > `SHADE_BLOB_DB_PATH` > shared
|
||||
`SHADE_PREKEY_PG_URL` > memory. `SHADE_DISABLE_BLOB=1` opts out.
|
||||
Honors `SHADE_DISABLE_RATE_LIMIT` for single-tenant deployments.
|
||||
|
||||
**Errors**
|
||||
|
||||
- New `ConflictError` → `SHADE_CONFLICT` → HTTP 409.
|
||||
- New `PreconditionFailedError` → `SHADE_PRECONDITION_FAILED` → HTTP 412.
|
||||
|
||||
**Test vectors**
|
||||
|
||||
- `test-vectors/blob-storage.json` — three (masterKey, app) cases
|
||||
with expected slotId/blobKey/signingSeed/ownerPubkey. Lets Prism
|
||||
(and future non-JS implementations) verify HKDF-info-string
|
||||
interop without spinning up a relay.
|
||||
|
||||
## [4.8.5] — 2026-05-08 — `Inbox.flushOnce`: kill the 15 s success-backoff + per-recipient parallel drain
|
||||
|
||||
Prism filed a "typing-into-a-chatty-shell" UX FR pointing at
|
||||
|
||||
Reference in New Issue
Block a user