N-05: the conditional-validator key was a colon join over components that may themselves contain colons, so two distinct valid bindings could collide and one definition's ETag could be prepared for another. The key is now a validated, byte-bounded fixed tuple encoded with JSON.stringify. N-09: capture localStorage exactly once and compare StorageEvent.storageArea against that object identity, so a pulse from sessionStorage or any other area is rejected instead of matching on key and value alone. The pulse key is registered in the storage registry as CACHE_INVALIDATION_PULSE. N-10: race loadPage against the caller signal and re-check before observing a page, so a non-cooperative loader can neither hold loadAll forever nor have a post-abort completion accumulated into a successful result. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
170 lines
4.6 KiB
TypeScript
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;
|
|
}
|
|
}
|