import { describe, expect, inject, it, vi } from 'vitest' import { DocumentStorageKV } from '../../src/document-storage/document-storage-kv.js' import type { BaseKVStorage } from '../../src/kv-storage/base-kv-storage.js' import { KVStorageComposite } from '../../src/kv-storage/kv-storage-composite.js' import { KVStorageEncrypted } from '../../src/kv-storage/kv-storage-encrypted.js' import { KVStorageHttp } from '../../src/kv-storage/kv-storage-http.js' import { KVStorageInMemory } from '../../src/kv-storage/kv-storage-in-memory.js' import { KVStorageIndexedDB } from '../../src/kv-storage/kv-storage-indexeddb.js' import { Ed25519Signer, type RequestSigner } from '../../src/utils/request-auth.js' import { collect } from '../helpers.js' import { runKVStorageConformance } from '../kv-storage/kv-storage.conformance.js' const serverUrl = inject('serverUrl') const key = await KVStorageEncrypted.generateKey() let n = 0 const unique = (name: string) => `${name}-${Date.now()}-${n++}` /** A fresh store on the test server: `open/` (no auth) or `signed/` (Ed25519). */ const space = (auth: 'open' | 'signed') => `${auth}/${unique('space')}` /** A client for a store on the test server (another origin, so CORS applies). */ function http(path = space('open'), options: { signer?: RequestSigner; network?: { online: boolean } } = {}) { return new KVStorageHttp({ url: `${serverUrl}/${path}`, signer: options.signer, fetch: (url, init) => (options.network?.online === false ? Promise.reject(new TypeError('Failed to fetch')) : fetch(url, init)), }) } runKVStorageConformance('KVStorageIndexedDB (Chromium)', () => new KVStorageIndexedDB({ name: unique('idb') })) runKVStorageConformance('KVStorageEncrypted over IndexedDB (Chromium)', () => new KVStorageEncrypted(new KVStorageIndexedDB({ name: unique('idb') }), { key })) runKVStorageConformance('KVStorageHttp (Chromium, cross-origin)', () => http()) runKVStorageConformance( 'KVStorageComposite: encrypted HTTP + memory + IndexedDB (Chromium)', () => new KVStorageComposite({ source: new KVStorageEncrypted(http(), { key }), replicas: [new KVStorageInMemory(), new KVStorageIndexedDB({ name: unique('idb') })], }), ) describe('signed requests (Chromium)', () => { it('signs with Ed25519 and only allows the signer’s own keys', async () => { const path = space('signed') const alice = await Ed25519Signer.generate() const bob = await Ed25519Signer.generate() const client = http(path, { signer: alice }) await client.child(alice.publicKey).set('profile', { name: 'Alice' }) expect(await client.child(alice.publicKey).get('profile')).toEqual({ name: 'Alice' }) await expect(client.child(bob.publicKey).set('profile', {})).rejects.toMatchObject({ status: 403 }) await expect(http(path).child(alice.publicKey).get('profile')).rejects.toMatchObject({ status: 401 }) }) it('streams changes over SSE across origins', async () => { const path = space('signed') const alice = await Ed25519Signer.generate() const watcher = http(path, { signer: alice }) await watcher.child(alice.publicKey).subscribe() const pushed = watcher.waitFor('change', (e) => e.origin === 'remote') await http(path, { signer: alice }).child(alice.publicKey).set('x', 1n) expect((await pushed).changes).toEqual([{ key: `${alice.publicKey}/x`, value: 1n }]) await watcher.close() }) }) describe('two devices syncing through the server (Chromium)', () => { /** One device: an encrypted, signed source and a persistent IndexedDB replica and queue. */ function device(path: string, signer: Ed25519Signer, dbName: string, network = { online: true }) { const indexedDb = new KVStorageIndexedDB({ name: dbName }) const db = new KVStorageComposite({ source: new KVStorageEncrypted(http(path, { signer, network }), { key }), replicas: [new KVStorageInMemory(), indexedDb], queue: indexedDb.child('~sync'), prefix: signer.publicKey, retryMs: 50, }) return { db, user: db.child(signer.publicKey), network } } it('pushes writes from one device to the other', async () => { const path = space('signed') const signer = await Ed25519Signer.generate() const a = device(path, signer, unique('a')) const b = device(path, signer, unique('b')) await Promise.all([a.db.sync(), b.db.sync()]) const seen = b.db.waitFor('change', (e) => e.origin === 'remote') await a.user.child('notes').set('1', { text: 'from A' }) expect((await seen).changes).toEqual([{ key: `${signer.publicKey}/notes/1`, value: { text: 'from A' } }]) expect(await b.user.child('notes').get('1')).toEqual({ text: 'from A' }) await Promise.all([a.db.close(), b.db.close()]) }) it('works offline, survives a reload with queued writes, and syncs when back online', async () => { const path = space('signed') const signer = await Ed25519Signer.generate() const dbName = unique('a') const first = device(path, signer, dbName) const docs = new DocumentStorageKV(first.user).child('todos') await docs.set('1', { title: 'Synced', done: false }) await first.db.sync() first.network.online = false await docs.update('1', { $set: { done: true } }) await docs.set('2', { title: 'Written offline', done: false }) expect(first.db.queued).toBe(2) await first.db.close() // "Reload": new instances over the same IndexedDB database, still offline. const network = { online: false } const reloaded = device(path, signer, dbName, network) const reloadedDocs = new DocumentStorageKV(reloaded.user).child('todos') expect(await collect(reloadedDocs.find({ done: false }))).toEqual([{ key: '2', data: { title: 'Written offline', done: false } }]) network.online = true await reloaded.db.sync() expect(reloaded.db.queued).toBe(0) const other = device(path, signer, unique('b')) await other.db.sync() expect(await collect(new DocumentStorageKV(other.user).child('todos').find())).toEqual([ { key: '1', data: { title: 'Synced', done: true } }, { key: '2', data: { title: 'Written offline', done: false } }, ]) await Promise.all([reloaded.db.close(), other.db.close()]) }) it('stores only ciphertext on the server', async () => { const path = space('signed') const signer = await Ed25519Signer.generate() const a = device(path, signer, unique('a')) await a.user.set('secret', 'the plans') const raw: BaseKVStorage = http(path, { signer }) const [stored] = await raw.readMany([`${signer.publicKey}/secret`]) expect(stored).toBeInstanceOf(Uint8Array) await vi.waitFor(async () => expect(await a.user.get('secret')).toBe('the plans')) await a.db.close() }) })