/** * `_dict_*` reserved-collection storage engine — `LookupHandle`. * * Verbatim move of `DictionaryHandle` out of `via/i18n/dictionary.ts` * (#650 Task 1 — via-lookup extraction, phase D). The class is renamed to * `LookupHandle` (the dict tier is one backing kind the eventual lookup * layer supports); `via/i18n/dictionary.ts` re-exports it as * `DictionaryHandle` for one release so existing importers compile * unchanged. `DictEntry` / `DictionaryOptions` / `DICT_COLLECTION_PREFIX` / * `dictCollectionName` move with it — they were exclusively consumed by * this class's constructor/methods. * * No behavior change from the pre-move class: same encryption door * (`reservedEnvelopes('_dict_')`, #629 Task 4), same ACL, same ledger * shape, same sync-cache/snapshot contract for dict joins. */ import type { NoydbStore, EncryptedEnvelope } from '../../kernel/types.js'; import type { NoydbEventEmitter } from '../../kernel/events.js'; import type { UnlockedKeyring } from '../../with-party/team/keyring.js'; import type { ViaCryptoCtx } from '../../kernel/via/index.js'; import type { LedgerStore } from '../../with-commit/history/ledger/store.js'; /** Reserved collection name prefix. Never collides with user collections. */ export declare const DICT_COLLECTION_PREFIX = "_dict_"; /** Return the adapter collection name for a named dictionary. */ export declare function dictCollectionName(dictionaryName: string): string; /** * One entry in a `_dict_*` collection. The record `id` (adapter-side * key) IS the stable dictionary key (e.g. `'paid'`). The `labels` * record maps locale codes to display strings. */ export interface DictEntry { /** Stable key — same as the record id in the adapter. */ readonly key: string; /** Locale → label map, e.g. `{ en: 'Paid', th: 'ชำระแล้ว' }`. */ readonly labels: Record; } /** * Options for `vault.dictionary(name, options?)`. * * `writableBy` controls the minimum role for write operations (put, * putAll, delete, rename). Defaults to `'admin'` to match the standard * "dictionary contents are owned by admins" convention; set to * `'operator'` for user-editable dictionaries like custom tags. */ export interface DictionaryOptions { /** Minimum role allowed to write dictionary entries. Default: `'admin'`. */ readonly writableBy?: 'owner' | 'admin' | 'operator'; } /** * Handle to a named dictionary within a vault. * * Obtained via `vault.dictionary(name)`. Provides strongly-typed * CRUD for dictionary entries, plus the `rename()` operation that is the * only sanctioned mass-mutation path for dictKey fields. * * All writes are encrypted under the compartment's DEK for the * `_dict_` collection. Adapters never see plaintext. */ export declare class LookupHandle { private readonly adapter; private readonly compartmentName; private readonly dictionaryName; private readonly keyring; /** * The `reservedEnvelopes('_dict_')` capability — this handle's sanctioned * crypto door onto its `_dict_` collection (#629 Task 4). Bound by * the Vault; never a keyring, raw DEK, or enclave import. */ private readonly reservedEnvelopes; private readonly encrypted; private readonly ledger; private readonly options; /** * Callback provided by the Vault to find and rewrite records * in any registered collection that has a dictKeyField pointing at * this dictionary, used by `rename()`. */ private readonly findAndUpdateReferences; private readonly emitter; /** * #647 fix wave 1 — mints a version-ordered delete-marker envelope (the reserved-tier * mirror of #589's ordinary-collection delete marker). Injected rather than imported: * `buildDeleteMarker` constructs the envelope's protected-body fields (`_iv`/`_data`), and * `via/lookup/**` may not reach `kernel/enclave/` directly (Check 11 * `enclave-body-only` / Check 15 `via-enclave-isolation`) — the Vault binds the real * `kernel/enclave` function at construction time, same pattern as `reservedEnvelopes` above. */ private readonly buildDeleteMarker; /** #650 Task 4 (#647) — dirty-log participation hook (origin `local-write`), threaded from `BuildLookupHandleOptions`. */ private readonly onDirty?; /** #650 Task 4 — thin `graphDispatch.collect`-equivalent; opens/collects/flushes a one-shot wave for this touch. */ private readonly onRecordMutated?; /** #650 Task 5 (#648) — the real reference check `delete()`'s strict branch calls: restrict * throws `DictKeyInUseError`, cascade/nullify apply their propagation. Vault-bound; a no-op * when the dimension has no declared lookup-referencing edges. */ private readonly checkReferencesOnDelete?; private readonly collName; /** * Synchronous write-through cache for dict-join support. * Populated on every `put()`, `delete()`, and `rename()`. The snapshot * is built from this cache by `snapshotEntries()` — the query executor * calls this synchronously inside `.toArray()`. * * `null` means "not yet initialized" — callers should use `list()` * to warm the cache before using dict joins on pre-existing data. */ private readonly _syncCache; /** * Return all cached entries as `{ key, labels, ...labels }` records — * usable synchronously by the join executor's `snapshot()` call. * Returns an empty array when the cache has never been populated. */ snapshotEntries(): readonly Record[]; constructor(adapter: NoydbStore, compartmentName: string, dictionaryName: string, keyring: UnlockedKeyring, /** * The `reservedEnvelopes('_dict_')` capability — this handle's sanctioned * crypto door onto its `_dict_` collection (#629 Task 4). Bound by * the Vault; never a keyring, raw DEK, or enclave import. */ reservedEnvelopes: ReturnType, encrypted: boolean, ledger: LedgerStore | undefined, options: DictionaryOptions, /** * Callback provided by the Vault to find and rewrite records * in any registered collection that has a dictKeyField pointing at * this dictionary, used by `rename()`. */ findAndUpdateReferences: ((dictionaryName: string, oldKey: string, newKey: string) => Promise) | undefined, emitter: NoydbEventEmitter, /** * #647 fix wave 1 — mints a version-ordered delete-marker envelope (the reserved-tier * mirror of #589's ordinary-collection delete marker). Injected rather than imported: * `buildDeleteMarker` constructs the envelope's protected-body fields (`_iv`/`_data`), and * `via/lookup/**` may not reach `kernel/enclave/` directly (Check 11 * `enclave-body-only` / Check 15 `via-enclave-isolation`) — the Vault binds the real * `kernel/enclave` function at construction time, same pattern as `reservedEnvelopes` above. */ buildDeleteMarker: (version: number, actor: string) => EncryptedEnvelope, /** #650 Task 4 (#647) — dirty-log participation hook (origin `local-write`), threaded from `BuildLookupHandleOptions`. */ onDirty?: ((collection: string, id: string, action: "put" | "delete", version: number) => Promise) | undefined, /** #650 Task 4 — thin `graphDispatch.collect`-equivalent; opens/collects/flushes a one-shot wave for this touch. */ onRecordMutated?: ((collection: string, id: string, action: "put" | "delete", version: number) => Promise) | undefined, /** #650 Task 5 (#648) — the real reference check `delete()`'s strict branch calls: restrict * throws `DictKeyInUseError`, cascade/nullify apply their propagation. Vault-bound; a no-op * when the dimension has no declared lookup-referencing edges. */ checkReferencesOnDelete?: ((key: string) => Promise) | undefined); private requireWriteAccess; private encryptEntry; private decryptEntry; /** @internal #650 Task 4 — re-read `key`'s CURRENT adapter truth into `_syncCache` (delete the * cache entry when the key is now gone). Called by `Vault._invalidateSyncApplied` after a * reserved-lookup row applies via sync, so an already-warmed snapshot/membership/altIndex read * never sees a stale verdict for a pulled vocabulary edit — the same invalidation a local * put()/delete()/rename() already triggers via `_syncCache.set`/`delete` above. */ _refreshSyncCache(key: string): Promise; /** * Add or overwrite a single dictionary entry. * * @param key The stable key to store (e.g. `'paid'`). * @param labels Locale → label map (e.g. `{ en: 'Paid', th: 'ชำระแล้ว' }`). */ put(key: Keys, labels: Record): Promise; /** * Batch-add or overwrite multiple dictionary entries in one call. * * @param entries `{ key: { locale: label } }` map. */ putAll(entries: Record>): Promise; /** * Load the label map for a single key. * * @returns The label map, or `null` if the key doesn't exist. */ get(key: Keys): Promise | null>; /** * Delete a dictionary key. * * Default mode is `'strict'` — throws `DictKeyInUseError` if any * registered collection has a record referencing this key. Pass * `{ mode: 'warn' }` to skip the check (dev-mode cleanup only). * * Under sync, deletion writes a version-ordered delete-marker row rather than removing the * adapter key outright (#647 fix wave 1) — markers accumulate over time; GC is future work, * acceptable for vocabulary-scale data. */ delete(key: Keys, opts?: { mode?: 'strict' | 'warn'; }): Promise; /** * Rename a dictionary key — the only sanctioned mass-mutation path. * * Atomically: * 1. Adds the new key with the same labels as the old key. * 2. Updates every registered record that stores the old key to * store the new key instead. * 3. Deletes the old key. * 4. Appends a single ledger entry recording the rename. * * Respects ACL: throws `PermissionDeniedError` before any mutation * if the caller can't write. The cascade is best-effort atomic * within this call — no two-phase commit across adapter calls. * * Cascade-on-delete is NOT supported. Use `rename()` when you need * to change a key that records reference. */ rename(oldKey: Keys, newKey: string): Promise; /** * List all entries in this dictionary. * * @returns Array of `{ key, labels }` objects. */ list(): Promise; /** * Resolve a key to its label for the given locale. * * Used by the collection's locale-aware read path to populate * `Label` virtual fields. Returns `undefined` when the * key doesn't exist or has no label for the requested locale * (after exhausting the fallback chain). */ resolveLabel(key: string, locale: string, fallback?: string | readonly string[]): Promise; } export { LookupHandle as DictionaryHandle };