Add encryption, Chromium tests and a README
CI / Node 24 (push) Successful in 15s
CI / Node 26 (push) Successful in 16s
CI / Node 22 (pull_request) Successful in 13s
CI / Node 24 (pull_request) Failing after 3s
CI / Node 26 (pull_request) Failing after 4s
CI / Chromium (push) Failing after 1m8s
CI / Publish to staging registry (push) Skipped
CI / Chromium (pull_request) Failing after 1m3s
CI / Publish to staging registry (pull_request) Skipped
CI / Node 22 (push) Failing after 4s

- KVStorageEncrypted: AES-GCM per value (fresh IV, storage key as AAD), keys stay
  readable; decrypts and re-emits pushed changes and passes subscriptions through, so it
  can be a composite source. generateKey / exportKey / importKey / deriveKey (PBKDF2).
- KVStorageHttpServer: allowOrigins (CORS), including cache-control for SSE preflights.
- Chromium tests (Vitest browser mode + Playwright) against a real cross-origin server:
  conformance for IndexedDB, encrypted, HTTP and composite stores; signed requests and
  SSE; two devices syncing, offline writes surviving a reload. New CI job.
- Composite fixes found by those tests: sync() during a running sync schedules one more
  (changes between the first scan and the stream connecting were missed); a queued write
  the source refuses is dropped, restored and reported (onError) instead of blocking the
  queue forever.
- README with examples, mirrored in test/readme.test.ts.

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 14:25:22 +00:00
parent 3525dbc622
commit 68d1a1c8e9
21 changed files with 1387 additions and 24 deletions
+37 -5
View File
@@ -5,7 +5,7 @@ import { KVStorageHttpServer } from '../../src/kv-storage/kv-storage-http-server
import { KVStorageInMemory } from '../../src/kv-storage/kv-storage-in-memory.js'
import { AuthError } from '../../src/utils/errors.js'
import { Ed25519Signer, Ed25519Verifier } from '../../src/utils/request-auth.js'
import { collect } from '../helpers.js'
import { collect, KVStorageCounting } from '../helpers.js'
import { TestNetwork } from './sync-helpers.js'
let network: TestNetwork
@@ -24,6 +24,11 @@ beforeEach(async () => {
})
afterEach(() => network.close())
/** Swaps in a server that rejects every signature (as if the user's access was revoked). */
function refuseEverySignature() {
network.server = new KVStorageHttpServer(network.store, { auth: { verifiers: [{ algorithm: 'ed25519', verify: async () => false }] } })
}
describe('authenticated KVStorageHttpServer', () => {
it('lets a user read, write and list their own keys', async () => {
const mine = network.client(alice).child(alice.publicKey)
@@ -82,13 +87,40 @@ describe('authenticated KVStorageHttpServer', () => {
})
it('a composite undoes a local write the server refuses', async () => {
const replica = new KVStorageInMemory()
const db = new KVStorageComposite({ source: network.client(alice), prefix: alice.publicKey, replicas: [replica] })
await db.sync()
// A source without subscriptions, so no follow-up sync is running when the write is made.
const db = new KVStorageComposite({ source: new KVStorageCounting(network.client(alice)), prefix: alice.publicKey, replicas: [new KVStorageInMemory()] })
await db.child(alice.publicKey).set('x', 'before')
network.server = new KVStorageHttpServer(network.store, { auth: { verifiers: [{ algorithm: 'ed25519', verify: async () => false }] } }) // now rejects every signature
await db.sync()
refuseEverySignature()
await expect(db.child(alice.publicKey).set('x', 'after')).rejects.toMatchObject({ status: 401 })
expect(await db.child(alice.publicKey).get('x')).toBe('before')
await db.close()
})
it('a composite drops a queued write the server refuses, restores the server value, and reports it', async () => {
const errors: Error[] = []
const db = new KVStorageComposite({
source: network.client(alice),
prefix: alice.publicKey,
replicas: [new KVStorageInMemory()],
onError: (e) => errors.push(e),
})
const mine = db.child(alice.publicKey)
await mine.set('x', 'before')
await db.sync()
network.online = false
await mine.set('x', 'queued')
await mine.set('y', 'queued after')
expect(db.queued).toBe(2)
const server = network.server
refuseEverySignature()
network.online = true
await db.sync()
network.server = server
expect(db.queued).toBe(0) // dropped, not retried forever
expect(errors.filter((e) => e.message === 'A queued write was refused by the source and dropped')).toHaveLength(2) // the sync's own scan is reported too
await db.sync()
expect(await mine.getMany(['x', 'y'])).toEqual(['before', undefined])
await db.close()
})
})