/** * Relay-side storage for a vault. * * The interface is deliberately dumb: put bytes under a hash, list a log, * append to it. Nothing here can decrypt, and nothing needs to — which is * what makes a SQLite, Postgres or object-store implementation equivalent. */ import type { VaultLogEntry } from './types.js'; export interface VaultStore { /** Ed25519 pubkey pinned on first write (TOFU), or null for a fresh vault. */ getOwner(vaultId: string): Promise; setOwner(vaultId: string, publicKey: Uint8Array): Promise; hasObject(vaultId: string, hash: string): Promise; putObject(vaultId: string, hash: string, bytes: Uint8Array): Promise; getObject(vaultId: string, hash: string): Promise; /** Highest committed seq, or 0 when empty. */ head(vaultId: string): Promise; /** Newest last. `limit` counts back from the head. */ log(vaultId: string, limit?: number): Promise; appendLog(vaultId: string, entry: VaultLogEntry): Promise; /** Total stored bytes, for quota decisions. */ usage(vaultId: string): Promise; } /** * In-memory store. Real for tests, a footgun in production — the same shape * as `MemoryBlobStore`, and the same warning applies: a restart loses * everything, and it will do so without an error. */ export class MemoryVaultStore implements VaultStore { private owners = new Map(); private objects = new Map>(); private logs = new Map(); async getOwner(vaultId: string): Promise { return this.owners.get(vaultId) ?? null; } async setOwner(vaultId: string, publicKey: Uint8Array): Promise { this.owners.set(vaultId, publicKey); } private bucket(vaultId: string): Map { let b = this.objects.get(vaultId); if (!b) { b = new Map(); this.objects.set(vaultId, b); } return b; } async hasObject(vaultId: string, hash: string): Promise { return this.bucket(vaultId).has(hash); } async putObject(vaultId: string, hash: string, bytes: Uint8Array): Promise { this.bucket(vaultId).set(hash, bytes); } async getObject(vaultId: string, hash: string): Promise { return this.bucket(vaultId).get(hash) ?? null; } async head(vaultId: string): Promise { const l = this.logs.get(vaultId); return l && l.length > 0 ? l[l.length - 1]!.seq : 0; } async log(vaultId: string, limit?: number): Promise { const l = this.logs.get(vaultId) ?? []; return limit === undefined ? [...l] : l.slice(Math.max(0, l.length - limit)); } async appendLog(vaultId: string, entry: VaultLogEntry): Promise { const l = this.logs.get(vaultId) ?? []; l.push(entry); this.logs.set(vaultId, l); } async usage(vaultId: string): Promise { let total = 0; for (const bytes of this.bucket(vaultId).values()) total += bytes.length; return total; } }