Files
tech-log-frontend/src/contracts/storage-keys.ts
T
DongHyeonkaandClaude Opus 5 4bff9ca151 chore: sync the frontend template from 4dc033c to 8157ad4
The product was materialized from the template at `4dc033c` and has stayed
on it through 43 template commits, so it was missing all three rounds of
adapter remediation — including files it never had, such as the shared
`abortable-operation` primitive and the `exact-snapshot` decoder that
later fixes are written against. Taking only the newest round was not
possible for that reason: the delta is coherent only as a whole.

The product had not touched `src/adapters` at all since materialization,
so the 140-file delta applied with a three-way merge and no conflicts.
`package.json` was the single overlap and merged cleanly: the product owns
`name`, the template contributed `check:adapter-inventory`,
`check:remediation-ledger` and the image-resolve-signal type fixture.
All 24 product-owned files — README, index.html, CI workflow, i18n
catalog, home page, generated schemas, evidence scripts, component and
visual snapshots — are byte-identical to `main`.

`template.lock.json` now pins the synced revision and tree.

Verified in this repository, not inherited from the template: six type
projects, lint, nine gates (adapter inventory, remediation ledger,
registries, diagnostics, realtime boundaries, architecture, browser
file/storage boundaries, optional recipes, documentation), the production
build, and 2,054 of 2,073 tests. The 19 failures are all in
`tests/unit/ci-artifact-contract.test.ts` and are the same pre-existing
sandbox RLIMIT, EMFILE, umask and `/tmp` permission behaviour the template
records; four suites that failed once under parallel load pass in
isolation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 12:04:58 +09:00

170 lines
4.6 KiB
TypeScript

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 extends StorageKeyInput>(
definition: Definition,
): Readonly<Definition & { physicalKey: string }> {
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<Record<string, StorageDefinition>> = 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;
}
}