- 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
55 lines
2.8 KiB
TypeScript
55 lines
2.8 KiB
TypeScript
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
|
import { KVStorageHttp } from '../../src/kv-storage/kv-storage-http.js'
|
|
import { KVStorageHttpServer } from '../../src/kv-storage/kv-storage-http-server.js'
|
|
import { KVStorageInMemory } from '../../src/kv-storage/kv-storage-in-memory.js'
|
|
import { EventStorageKV } from '../../src/event-storage/event-storage-kv.js'
|
|
import { DocumentStorageKV } from '../../src/document-storage/document-storage-kv.js'
|
|
import { EventSourcedDocumentStorage } from '../../src/document-storage/event-sourced-document-storage.js'
|
|
import { runKVStorageConformance } from './kv-storage.conformance.js'
|
|
import { serve } from './node-server.js'
|
|
|
|
runKVStorageConformance('KVStorageHttp (in-process)', () => {
|
|
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)) })
|
|
})
|
|
|
|
describe('over a real socket', () => {
|
|
let server: Awaited<ReturnType<typeof serve>>
|
|
const stores: KVStorageHttpServer[] = []
|
|
beforeAll(async () => {
|
|
// One socket; each test storage gets its own backend under /<n>.
|
|
server = await serve((request) => stores[Number(new URL(request.url).pathname.split('/')[1])]!.handle(request))
|
|
})
|
|
afterAll(() => server.close())
|
|
const fresh = () => {
|
|
stores.push(new KVStorageHttpServer(new KVStorageInMemory(), { auth: 'none', basePath: `/${stores.length}` }))
|
|
return new KVStorageHttp({ url: `${server.url}/${stores.length - 1}` })
|
|
}
|
|
|
|
runKVStorageConformance('KVStorageHttp (node:http)', fresh)
|
|
|
|
it('stops a long scan early', async () => {
|
|
const db = fresh()
|
|
await db.writeMany(Array.from({ length: 5000 }, (_, i) => [`k${String(i).padStart(5, '0')}`, i] as [string, number]))
|
|
const seen: string[] = []
|
|
for await (const [key] of db.scan()) if (seen.push(key) === 3) break
|
|
expect(seen).toEqual(['k00000', 'k00001', 'k00002'])
|
|
})
|
|
|
|
it('runs events and event-sourced documents over HTTP', async () => {
|
|
const kv = fresh()
|
|
const docs = new EventSourcedDocumentStorage(new EventStorageKV(kv), new DocumentStorageKV(kv.child('projections')))
|
|
const id = await docs.child('people').insert({ email: 'a@x', n: 1 })
|
|
await docs.child('people').update(id, { $inc: { n: 1 } })
|
|
expect(await docs.child('people').findOne({ email: 'a@x' })).toEqual({ key: id, data: { email: 'a@x', n: 2 } })
|
|
})
|
|
})
|
|
|
|
describe('server', () => {
|
|
it('maps errors to 404 / 500', async () => {
|
|
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)
|
|
})
|
|
})
|