const APP_NAMESPACE = "ca-frontend"; export type StorageBackend = | "memory" | "sessionStorage" | "localStorage" | "disabled" | "forbidden"; export type StorageValueCodec = | "color-scheme-v1" | "opaque-string-v1" | "none"; export type StorageKeyInput = Readonly<{ logicalName: string; scope: string; name: string; backend: StorageBackend; classification: | "public-preference" | "opaque-cache" | "sensitive-forbidden"; schemaVersion: number; valueCodec: StorageValueCodec; ttl: number | "session" | null; migration: "discard"; quotaFallback: "memory" | "no-persist" | "feature-disable"; }>; export type StorageDefinition = Readonly< StorageKeyInput & { physicalKey: string } >; export const STORAGE_REGISTRY = Object.freeze({ COLOR_SCHEME: defineStorageKey({ logicalName: "COLOR_SCHEME", scope: "preference", name: "color-scheme", backend: "localStorage", classification: "public-preference", schemaVersion: 1, valueCodec: "color-scheme-v1", ttl: null, migration: "discard", quotaFallback: "memory", }), CHUNK_RELOAD_GUARD: defineStorageKey({ logicalName: "CHUNK_RELOAD_GUARD", scope: "release", name: "chunk-reload-guard", backend: "sessionStorage", classification: "opaque-cache", schemaVersion: 1, valueCodec: "opaque-string-v1", ttl: "session", migration: "discard", quotaFallback: "no-persist", }), QUERY_PERSISTENCE: defineStorageKey({ logicalName: "QUERY_PERSISTENCE", scope: "cache", name: "query-persistence", backend: "disabled", classification: "sensitive-forbidden", schemaVersion: 1, valueCodec: "none", ttl: null, migration: "discard", quotaFallback: "feature-disable", }), CACHE_INVALIDATION_PULSE: defineStorageKey({ logicalName: "CACHE_INVALIDATION_PULSE", scope: "cache-invalidation", name: "pulse", backend: "localStorage", classification: "opaque-cache", schemaVersion: 1, valueCodec: "opaque-string-v1", ttl: null, migration: "discard", quotaFallback: "no-persist", }), AUTH_TOKEN: defineStorageKey({ logicalName: "AUTH_TOKEN", scope: "auth", name: "auth-token", backend: "forbidden", classification: "sensitive-forbidden", schemaVersion: 1, valueCodec: "none", ttl: null, migration: "discard", quotaFallback: "feature-disable", }), }); export function defineStorageKey( definition: Definition, ): Readonly { if ( !["color-scheme-v1", "opaque-string-v1", "none"].includes( definition.valueCodec, ) ) { throw new Error("Unknown client storage value codec"); } if (definition.migration !== "discard") { throw new Error("Unsupported client storage migration policy"); } if (definition.classification === "sensitive-forbidden") { if (!["disabled", "forbidden"].includes(definition.backend)) { throw new Error("Sensitive client storage registration is forbidden"); } if (definition.valueCodec !== "none") { throw new Error("Sensitive client storage codec is forbidden"); } } else if (definition.valueCodec === "none") { throw new Error("Persisted storage keys require a value codec"); } if (!Number.isInteger(definition.schemaVersion) || definition.schemaVersion < 1) { throw new Error("Storage schemaVersion must be a positive integer"); } return Object.freeze({ ...definition, physicalKey: buildPhysicalKey( definition.scope, definition.schemaVersion, definition.name, ), }); } export function buildPhysicalKey( scope: string, schemaVersion: number, name: string, ): string { return `${APP_NAMESPACE}:${scope}:v${schemaVersion}:${name}`; } export function getStorageDefinition(logicalName: string): StorageDefinition { const registry: Readonly> = STORAGE_REGISTRY; const definition = registry[logicalName]; if (!definition) throw new Error(`Unregistered storage key: ${logicalName}`); if (definition.classification === "sensitive-forbidden") { throw new Error(`Forbidden storage key: ${logicalName}`); } return definition; } export function isStorageValueAllowed( definition: StorageDefinition, value: unknown, ): boolean { switch (definition.valueCodec) { case "color-scheme-v1": return value === "light" || value === "dark" || value === "system"; case "opaque-string-v1": return ( typeof value === "string" && value.length >= 1 && value.length <= 2_048 ); default: return false; } }