import type { Db } from "@wrnexus/db"; import { decrypt, encrypt } from "./index.ts"; export interface SecretResourceOptions> { db: () => Db; table: string; key: string | (() => string); encrypted: readonly (keyof T & string)[]; hidden?: readonly (keyof T & string)[]; id?: string; } const SAFE = /^[A-Za-z_][A-Za-z0-9_]*$/; /** Encrypt selected database fields and guarantee hidden fields never leave `safe()`. */ export function defineSecretResource>( options: SecretResourceOptions, ) { const checked = (value: string) => { if (!SAFE.test(value)) throw new TypeError(`Unsafe secret resource identifier: ${value}`); return value; }; const table = checked(options.table); const id = checked(options.id ?? "id"); const encrypted = options.encrypted.map(checked); const hidden = new Set((options.hidden ?? options.encrypted).map(checked)); const key = () => (typeof options.key === "function" ? options.key() : options.key); return { async seal(input: T): Promise { const output = { ...input }; const writable = output as Record; for (const field of encrypted) if (writable[field] !== undefined) writable[field] = await encrypt(String(writable[field]), key()); return output; }, async open(input: T): Promise { const output = { ...input }; const writable = output as Record; for (const field of encrypted) if (writable[field]) writable[field] = await decrypt(String(writable[field]), key()); return output; }, safe(input: T): Partial { return Object.fromEntries( Object.entries(input).filter(([field]) => !hidden.has(field)), ) as Partial; }, async find(value: string | number): Promise { return options.db().one(`SELECT * FROM ${table} WHERE ${id} = ?`, [value]); }, }; }