import type { IndexedDbDatasetScope } from "../../../application/ports/browser-file-storage/indexeddb-port.ts"; import { assertValidStoragePolicy, type BrowserStoragePolicy, } from "../../../application/ports/browser-file-storage/shared.ts"; export const INDEXEDDB_DATASET_BINDING_KEY = "dataset-binding"; export const INDEXEDDB_DATASET_BUDGET_KEY = "dataset-budget"; const OPAQUE_SCOPE_TOKEN = /^[A-Za-z0-9_-]{16,48}$/u; type StoredDatasetBinding = Readonly<{ bindingKey: typeof INDEXEDDB_DATASET_BINDING_KEY; bindingVersion: 1; scope: IndexedDbDatasetScope; storagePolicy: BrowserStoragePolicy; }>; export type IndexedDbBindingVerification = | Readonly<{ ok: true }> | Readonly<{ ok: false; reason: "ABORTED" | "CORRUPT" | "MISMATCH" | "MISSING" | "NATIVE_ERROR"; error?: unknown; }>; function validOpaqueToken(value: unknown): value is string { return typeof value === "string" && OPAQUE_SCOPE_TOKEN.test(value); } export function assertValidIndexedDbDatasetGovernance( scope: IndexedDbDatasetScope, storagePolicy: BrowserStoragePolicy, ): void { assertValidStoragePolicy(storagePolicy); if ( !scope || typeof scope !== "object" || !validOpaqueToken(scope.authorityToken) || !validOpaqueToken(scope.namespaceToken) || !validOpaqueToken(scope.partitionToken) || new Set([ scope.authorityToken, scope.namespaceToken, scope.partitionToken, ]).size !== 3 || scope.accountScope !== storagePolicy.accountScope || scope.authorityToken === storagePolicy.owner || scope.namespaceToken === storagePolicy.namespace || scope.partitionToken === storagePolicy.namespace || (storagePolicy.classification === "PERSONAL" && scope.accountScope !== "OPAQUE_PARTITION") || (storagePolicy.classification === "CONFIDENTIAL" && scope.accountScope !== "OPAQUE_PARTITION") ) { throw new TypeError("IndexedDB dataset governance is invalid."); } } /** * Physical identity is derived exclusively from opaque registry tokens. The * readable policy namespace and all business/account identifiers are excluded. */ export function indexedDbPhysicalDatabaseName( scope: IndexedDbDatasetScope, ): string { if ( !scope || typeof scope !== "object" || !validOpaqueToken(scope.authorityToken) || !validOpaqueToken(scope.namespaceToken) || !validOpaqueToken(scope.partitionToken) ) { throw new TypeError("IndexedDB dataset scope is invalid."); } return `ca-idb-v1:${scope.authorityToken}.${scope.namespaceToken}.${scope.partitionToken}`; } export function createIndexedDbDatasetBinding( scope: IndexedDbDatasetScope, storagePolicy: BrowserStoragePolicy, ): StoredDatasetBinding { assertValidIndexedDbDatasetGovernance(scope, storagePolicy); return Object.freeze({ bindingKey: INDEXEDDB_DATASET_BINDING_KEY, bindingVersion: 1, scope: Object.freeze({ ...scope }), storagePolicy: Object.freeze({ ...storagePolicy, retention: Object.freeze({ ...storagePolicy.retention }), }), }); } function isStoredDatasetBinding( value: unknown, ): value is StoredDatasetBinding { if (!value || typeof value !== "object") return false; const binding = value as Partial; if ( binding.bindingKey !== INDEXEDDB_DATASET_BINDING_KEY || binding.bindingVersion !== 1 || !binding.scope || !binding.storagePolicy ) { return false; } try { assertValidIndexedDbDatasetGovernance( binding.scope, binding.storagePolicy, ); return true; } catch { return false; } } function canonicalPolicy(policy: BrowserStoragePolicy): string { return JSON.stringify([ policy.owner, policy.namespace, policy.classification, policy.authority, policy.accountScope, policy.retention.kind, policy.retention.kind === "TTL" ? policy.retention.maxAgeMs : null, policy.softBudgetBytes, policy.hardBudgetBytes, policy.evictionPriority, policy.logoutAction, policy.accountDeletionAction, policy.pressureAction, policy.unavailableFallback, ]); } export function sameIndexedDbDatasetBinding( value: unknown, expected: StoredDatasetBinding, ): boolean { if (!isStoredDatasetBinding(value)) return false; return ( value.scope.authorityToken === expected.scope.authorityToken && value.scope.namespaceToken === expected.scope.namespaceToken && value.scope.partitionToken === expected.scope.partitionToken && value.scope.accountScope === expected.scope.accountScope && canonicalPolicy(value.storagePolicy) === canonicalPolicy(expected.storagePolicy) ); } /** * Queues binding validation inside the versionchange transaction. Any mismatch * aborts that transaction, so schema changes cannot commit under the wrong * namespace or policy. */ export function queueIndexedDbUpgradeBinding( transaction: IDBTransaction, governanceStore: string, expected: StoredDatasetBinding, oldVersion: number, onRejected: () => void, ): void { const store = transaction.objectStore(governanceStore); if (oldVersion === 0) { let addRequest: IDBRequest; try { addRequest = store.add(expected); } catch { onRejected(); transaction.abort(); return; } addRequest.onerror = () => onRejected(); let budgetRequest: IDBRequest; try { budgetRequest = store.add( Object.freeze({ bindingKey: INDEXEDDB_DATASET_BUDGET_KEY, budgetVersion: 1, usedBytes: 0, receiptCount: 0, }), ); } catch { onRejected(); transaction.abort(); return; } budgetRequest.onerror = () => onRejected(); return; } const request = store.get(INDEXEDDB_DATASET_BINDING_KEY); request.onerror = () => { onRejected(); try { transaction.abort(); } catch { // The native request/transaction error owns the terminal state. } }; request.onsuccess = () => { if (!sameIndexedDbDatasetBinding(request.result, expected)) { onRejected(); try { transaction.abort(); } catch { // The mismatch remains fail-closed even if abort already won. } } }; } /** * Post-open verification protects non-upgrade opens and maintenance callers. */ export function verifyIndexedDbDatasetBinding( database: IDBDatabase, governanceStore: string, expected: StoredDatasetBinding, signal?: AbortSignal, ): Promise { if (signal?.aborted) { return Promise.resolve({ ok: false, reason: "ABORTED" }); } let transaction: IDBTransaction; try { transaction = database.transaction(governanceStore, "readonly"); } catch (error) { return Promise.resolve({ ok: false, reason: "NATIVE_ERROR", error, }); } return new Promise((resolve) => { let settled = false; let observed: unknown; let observedBudget: unknown; let requestError: unknown; let callerAborted = false; const finish = (result: IndexedDbBindingVerification) => { if (settled) return; settled = true; signal?.removeEventListener("abort", onAbort); resolve(result); }; function onAbort(): void { callerAborted = true; try { transaction.abort(); } catch { // Completion determines the race. } } signal?.addEventListener("abort", onAbort, { once: true }); transaction.onerror = () => { requestError ??= transaction.error; }; transaction.onabort = () => finish( callerAborted ? { ok: false, reason: "ABORTED" } : { ok: false, reason: "NATIVE_ERROR", error: requestError ?? transaction.error, }, ); transaction.oncomplete = () => { if (observed === undefined) { finish({ ok: false, reason: "MISSING" }); } else if (!isStoredDatasetBinding(observed)) { finish({ ok: false, reason: "CORRUPT" }); } else if (!sameIndexedDbDatasetBinding(observed, expected)) { finish({ ok: false, reason: "MISMATCH" }); } else if ( !observedBudget || typeof observedBudget !== "object" || (observedBudget as { bindingKey?: unknown }).bindingKey !== INDEXEDDB_DATASET_BUDGET_KEY || (observedBudget as { budgetVersion?: unknown }).budgetVersion !== 1 || !Number.isSafeInteger( (observedBudget as { usedBytes?: unknown }).usedBytes, ) || typeof (observedBudget as { usedBytes?: unknown }).usedBytes !== "number" || (observedBudget as { usedBytes: number }).usedBytes < 0 || (observedBudget as { usedBytes: number }).usedBytes > expected.storagePolicy.hardBudgetBytes || !Number.isSafeInteger( (observedBudget as { receiptCount?: unknown }).receiptCount, ) || typeof (observedBudget as { receiptCount?: unknown }) .receiptCount !== "number" || (observedBudget as { receiptCount: number }).receiptCount < 0 ) { finish({ ok: false, reason: "CORRUPT" }); } else { finish({ ok: true }); } }; try { const request = transaction .objectStore(governanceStore) .get(INDEXEDDB_DATASET_BINDING_KEY); request.onerror = () => { requestError ??= request.error; }; request.onsuccess = () => { observed = request.result; }; const budgetRequest = transaction .objectStore(governanceStore) .get(INDEXEDDB_DATASET_BUDGET_KEY); budgetRequest.onerror = () => { requestError ??= budgetRequest.error; }; budgetRequest.onsuccess = () => { observedBudget = budgetRequest.result; }; } catch (error) { requestError = error; try { transaction.abort(); } catch { finish({ ok: false, reason: "NATIVE_ERROR", error }); } } }); }