Authenticated, scoped sync: signed requests and per-path subscriptions
CI / Node 24 (pull_request) Successful in 12s
CI / Node 22 (pull_request) Successful in 13s
CI / Node 26 (pull_request) Successful in 13s

- RequestAuth: clients sign `${timestamp}:${path}:${body}` (as in
  sync-server-example); headers carry public key, timestamp, signature and
  algorithm. Pluggable signers/verifiers; Ed25519 via WebCrypto built in.
  Timestamp window and replay protection.
- KVStorageHttpServer requires auth ('none' must be explicit). With auth, every
  key, scan range and subscription path must be under the caller's public key.
- POST /events with { paths }: subscribers only receive (and on resume only
  replay) changes under their authorized paths.
- KVStorageHttp: signer option; subscribe(path)/unsubscribe(path)/close()
  replace watch(); 401/403 reject instead of retrying.
- KVStorageComposite: prefix option scopes sync, cache and subscription.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qxbkp4tA7G5SCWjzWuxmE
This commit is contained in:
Claude
2026-10-23 10:01:06 +00:00
parent df7de27e20
commit a8c9539ad9
14 changed files with 696 additions and 129 deletions
+3 -3
View File
@@ -9,7 +9,7 @@ import { runKVStorageConformance } from './kv-storage.conformance.js'
import { serve } from './node-server.js'
runKVStorageConformance('KVStorageHttp (in-process)', () => {
const server = new KVStorageHttpServer(new KVStorageInMemory(), { basePath: '/kv' })
const server = new KVStorageHttpServer(new KVStorageInMemory(), { auth: 'none', basePath: '/kv' })
return new KVStorageHttp({ url: 'http://test/kv', fetch: async (input, init) => server.handle(new Request(input, init)) })
})
@@ -22,7 +22,7 @@ describe('over a real socket', () => {
})
afterAll(() => server.close())
const fresh = () => {
stores.push(new KVStorageHttpServer(new KVStorageInMemory(), { basePath: `/${stores.length}` }))
stores.push(new KVStorageHttpServer(new KVStorageInMemory(), { auth: 'none', basePath: `/${stores.length}` }))
return new KVStorageHttp({ url: `${server.url}/${stores.length - 1}` })
}
@@ -47,7 +47,7 @@ describe('over a real socket', () => {
describe('server', () => {
it('maps errors to 404 / 500', async () => {
const server = new KVStorageHttpServer(new KVStorageInMemory())
const server = new KVStorageHttpServer(new KVStorageInMemory(), { auth: 'none' })
expect((await server.handle(new Request('http://x/nope', { method: 'POST', body: '{}' }))).status).toBe(404)
expect((await server.handle(new Request('http://x/readMany', { method: 'POST', body: '{"keys": 5}' }))).status).toBe(500)
})