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
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:
@@ -1,7 +1,7 @@
|
||||
import { AuthError } from '../utils/errors.js'
|
||||
import { ExtendedJson } from '../utils/extended-json.js'
|
||||
import { KeyRange } from '../utils/key-range.js'
|
||||
import { RequestAuth, type RequestAuthOptions, type SignatureVerifier } from '../utils/request-auth.js'
|
||||
import { AUTH_HEADERS, RequestAuth, type RequestAuthOptions, type SignatureVerifier } from '../utils/request-auth.js'
|
||||
import type { BaseKVStorage, KVChange, ScanOptions } from './base-kv-storage.js'
|
||||
|
||||
export interface KVStorageHttpServerOptions {
|
||||
@@ -15,6 +15,11 @@ export interface KVStorageHttpServerOptions {
|
||||
basePath?: string
|
||||
/** Interval of SSE keep-alive comments. Default 15 000 ms. */
|
||||
keepAliveMs?: number
|
||||
/**
|
||||
* Origins allowed to call the server from a browser on another origin (CORS), e.g.
|
||||
* `['https://app.example.com']`, or `'*'` for any. Default: none (same origin only).
|
||||
*/
|
||||
allowOrigins?: string[] | '*'
|
||||
}
|
||||
|
||||
type RequestBody = { keys: string[]; changes: KVChange[]; paths: string[] } & ScanOptions
|
||||
@@ -39,6 +44,7 @@ export class KVStorageHttpServer {
|
||||
private readonly auth: RequestAuth | undefined
|
||||
private readonly basePath: string
|
||||
private readonly keepAliveMs: number
|
||||
private readonly allowOrigins: string[] | '*'
|
||||
private readonly subscribers = new Set<{ ranges: KeyRange[]; send: (frame: string) => void }>()
|
||||
|
||||
constructor(
|
||||
@@ -48,6 +54,7 @@ export class KVStorageHttpServer {
|
||||
this.auth = options.auth === 'none' ? undefined : new RequestAuth(options.auth.verifiers, options.auth)
|
||||
this.basePath = options.basePath ?? ''
|
||||
this.keepAliveMs = options.keepAliveMs ?? 15_000
|
||||
this.allowOrigins = options.allowOrigins ?? []
|
||||
storage.on('change', ({ changes }) => {
|
||||
for (const subscriber of this.subscribers) {
|
||||
const matching = changes.filter(({ key }) => subscriber.ranges.some((range) => range.contains(key)))
|
||||
@@ -57,7 +64,24 @@ export class KVStorageHttpServer {
|
||||
}
|
||||
|
||||
async handle(request: Request): Promise<Response> {
|
||||
const response = await this.route(request)
|
||||
const origin = request.headers.get('origin')
|
||||
if (!origin || !(this.allowOrigins === '*' || this.allowOrigins.includes(origin))) return response
|
||||
response.headers.set('access-control-allow-origin', origin)
|
||||
response.headers.append('vary', 'origin')
|
||||
return response
|
||||
}
|
||||
|
||||
private async route(request: Request): Promise<Response> {
|
||||
const path = new URL(request.url).pathname
|
||||
if (request.method === 'OPTIONS') {
|
||||
// CORS preflight (signed requests carry custom headers). `handle` adds the allowed origin.
|
||||
const headers = ['content-type', 'cache-control', 'last-event-id', ...Object.values(AUTH_HEADERS)].join(', ')
|
||||
return new Response(null, {
|
||||
status: 204,
|
||||
headers: { 'access-control-allow-methods': 'POST', 'access-control-allow-headers': headers, 'access-control-max-age': '600' },
|
||||
})
|
||||
}
|
||||
if (request.method !== 'POST' || !path.startsWith(this.basePath)) return KVStorageHttpServer.error(404, 'Not found')
|
||||
try {
|
||||
const text = await request.text()
|
||||
|
||||
Reference in New Issue
Block a user