91 lines
3.0 KiB
TypeScript
91 lines
3.0 KiB
TypeScript
|
|
/**
|
||
|
|
* 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<Uint8Array | null>;
|
||
|
|
setOwner(vaultId: string, publicKey: Uint8Array): Promise<void>;
|
||
|
|
|
||
|
|
hasObject(vaultId: string, hash: string): Promise<boolean>;
|
||
|
|
putObject(vaultId: string, hash: string, bytes: Uint8Array): Promise<void>;
|
||
|
|
getObject(vaultId: string, hash: string): Promise<Uint8Array | null>;
|
||
|
|
|
||
|
|
/** Highest committed seq, or 0 when empty. */
|
||
|
|
head(vaultId: string): Promise<number>;
|
||
|
|
/** Newest last. `limit` counts back from the head. */
|
||
|
|
log(vaultId: string, limit?: number): Promise<VaultLogEntry[]>;
|
||
|
|
appendLog(vaultId: string, entry: VaultLogEntry): Promise<void>;
|
||
|
|
|
||
|
|
/** Total stored bytes, for quota decisions. */
|
||
|
|
usage(vaultId: string): Promise<number>;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 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<string, Uint8Array>();
|
||
|
|
private objects = new Map<string, Map<string, Uint8Array>>();
|
||
|
|
private logs = new Map<string, VaultLogEntry[]>();
|
||
|
|
|
||
|
|
async getOwner(vaultId: string): Promise<Uint8Array | null> {
|
||
|
|
return this.owners.get(vaultId) ?? null;
|
||
|
|
}
|
||
|
|
|
||
|
|
async setOwner(vaultId: string, publicKey: Uint8Array): Promise<void> {
|
||
|
|
this.owners.set(vaultId, publicKey);
|
||
|
|
}
|
||
|
|
|
||
|
|
private bucket(vaultId: string): Map<string, Uint8Array> {
|
||
|
|
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<boolean> {
|
||
|
|
return this.bucket(vaultId).has(hash);
|
||
|
|
}
|
||
|
|
|
||
|
|
async putObject(vaultId: string, hash: string, bytes: Uint8Array): Promise<void> {
|
||
|
|
this.bucket(vaultId).set(hash, bytes);
|
||
|
|
}
|
||
|
|
|
||
|
|
async getObject(vaultId: string, hash: string): Promise<Uint8Array | null> {
|
||
|
|
return this.bucket(vaultId).get(hash) ?? null;
|
||
|
|
}
|
||
|
|
|
||
|
|
async head(vaultId: string): Promise<number> {
|
||
|
|
const l = this.logs.get(vaultId);
|
||
|
|
return l && l.length > 0 ? l[l.length - 1]!.seq : 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
async log(vaultId: string, limit?: number): Promise<VaultLogEntry[]> {
|
||
|
|
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<void> {
|
||
|
|
const l = this.logs.get(vaultId) ?? [];
|
||
|
|
l.push(entry);
|
||
|
|
this.logs.set(vaultId, l);
|
||
|
|
}
|
||
|
|
|
||
|
|
async usage(vaultId: string): Promise<number> {
|
||
|
|
let total = 0;
|
||
|
|
for (const bytes of this.bucket(vaultId).values()) total += bytes.length;
|
||
|
|
return total;
|
||
|
|
}
|
||
|
|
}
|