export const CORRECTIVE_RESOURCE_SCHEMA = 1 as const; export type CatalogSourceKind = "LOCAL" | "EXTERNAL" | "PACKED"; export type ResourceProviderKind = "LOCAL" | "EXTERNAL_LIBRARY" | "PACKED_BLEND"; export interface ActiveLibraryContext { libraryId: string; sourceKind: CatalogSourceKind; sourceId: string; generation: number; revision: number; readOnly: boolean; } export interface ResourceRequest { schemaVersion: typeof CORRECTIVE_RESOURCE_SCHEMA; requestId: string; sourceKind: CatalogSourceKind; sourceId: string; sha256: string; } export interface ResourceRecord { schemaVersion: typeof CORRECTIVE_RESOURCE_SCHEMA; provider: ResourceProviderKind; sourceKind: CatalogSourceKind; sourceId: string; sha256: string; bytes: Uint8Array; } export class CorrectiveResourceError extends Error { readonly code: "RESOURCE_MALFORMED" | "RESOURCE_REQUIRED" | "RESOURCE_NOT_FOUND" | "RESOURCE_HASH_MISMATCH" | "CANCELLED" | "CONTEXT_INVALID"; constructor(code: CorrectiveResourceError["code"], message: string) { super(`${code}: ${message}`); this.name = "CorrectiveResourceError"; this.code = code; } } const SHA256 = /^[a-f0-9]{64}$/u; const REQUEST_ID = /^[A-Za-z0-9._:-]{1,256}$/u; const SOURCE_ID = /^[A-Za-z0-9._:/-]{1,256}$/u; const LIBRARY_ID = /^[A-Za-z0-9._:/-]{1,256}$/u; const SOURCE_KINDS = new Set(["LOCAL", "EXTERNAL", "PACKED"]); function assertText(value: unknown, pattern: RegExp, label: string): string { if (typeof value !== "string" || !pattern.test(value)) throw new CorrectiveResourceError("RESOURCE_MALFORMED", `${label} is invalid`); return value; } function assertInteger(value: unknown, label: string): number { if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new CorrectiveResourceError("RESOURCE_MALFORMED", `${label} is invalid`); return value; } function bytesCopy(value: Uint8Array): Uint8Array { return new Uint8Array(value); } async function digest(bytes: Uint8Array): Promise { const hash = await globalThis.crypto.subtle.digest("SHA-256", bytes as Uint8Array); return [...new Uint8Array(hash)].map((value) => value.toString(16).padStart(2, "0")).join(""); } export function validateActiveLibraryContext(value: unknown): ActiveLibraryContext { if (!value || typeof value !== "object" || Array.isArray(value)) throw new CorrectiveResourceError("CONTEXT_INVALID", "active library context must be an object"); const input = value as Record; const keys = Object.keys(input); if (keys.some((key) => !["libraryId", "sourceKind", "sourceId", "generation", "revision", "readOnly"].includes(key))) throw new CorrectiveResourceError("CONTEXT_INVALID", "active library context has unknown fields"); const libraryId = assertText(input.libraryId, LIBRARY_ID, "libraryId"); const sourceKind = input.sourceKind; if (typeof sourceKind !== "string" || !SOURCE_KINDS.has(sourceKind as CatalogSourceKind)) throw new CorrectiveResourceError("CONTEXT_INVALID", "sourceKind is invalid"); const sourceId = assertText(input.sourceId, SOURCE_ID, "sourceId"); const generation = assertInteger(input.generation, "generation"); const revision = assertInteger(input.revision, "revision"); if (typeof input.readOnly !== "boolean") throw new CorrectiveResourceError("CONTEXT_INVALID", "readOnly is invalid"); return { libraryId, sourceKind: sourceKind as CatalogSourceKind, sourceId, generation, revision, readOnly: input.readOnly }; } export function validateResourceRequest(value: unknown): ResourceRequest { if (!value || typeof value !== "object" || Array.isArray(value)) throw new CorrectiveResourceError("RESOURCE_MALFORMED", "resource request must be an object"); const input = value as Record; if (Object.keys(input).some((key) => !["schemaVersion", "requestId", "sourceKind", "sourceId", "sha256"].includes(key))) throw new CorrectiveResourceError("RESOURCE_MALFORMED", "resource request has unknown fields"); if (input.schemaVersion !== CORRECTIVE_RESOURCE_SCHEMA) throw new CorrectiveResourceError("RESOURCE_MALFORMED", "schemaVersion is unsupported"); const requestId = assertText(input.requestId, REQUEST_ID, "requestId"); if (typeof input.sourceKind !== "string" || !SOURCE_KINDS.has(input.sourceKind as CatalogSourceKind)) throw new CorrectiveResourceError("RESOURCE_MALFORMED", "sourceKind is invalid"); const sourceId = assertText(input.sourceId, SOURCE_ID, "sourceId"); const sha256 = assertText(input.sha256, SHA256, "sha256"); return { schemaVersion: CORRECTIVE_RESOURCE_SCHEMA, requestId, sourceKind: input.sourceKind as CatalogSourceKind, sourceId, sha256 }; } export class InMemoryCatalogProvider { readonly provider: ResourceProviderKind; private readonly records = new Map(); constructor(provider: ResourceProviderKind) { this.provider = provider; } async put(sourceId: string, bytes: Uint8Array): Promise { assertText(sourceId, SOURCE_ID, "sourceId"); if (!(bytes instanceof Uint8Array)) throw new CorrectiveResourceError("RESOURCE_MALFORMED", "bytes must be Uint8Array"); const copy = bytesCopy(bytes); const hash = await digest(copy); this.records.set(sourceId, copy); return hash; } async load(sourceId: string, signal?: AbortSignal): Promise { if (signal?.aborted) throw new CorrectiveResourceError("CANCELLED", "resource lookup cancelled"); const value = this.records.get(sourceId); if (!value) throw new CorrectiveResourceError("RESOURCE_NOT_FOUND", `resource ${sourceId} is not available`); await Promise.resolve(); if (signal?.aborted) throw new CorrectiveResourceError("CANCELLED", "resource lookup cancelled"); return bytesCopy(value); } } export class AssetCatalogResourceResolver { private readonly providers = new Map(); private active: ActiveLibraryContext | null = null; setActiveLibraryContext(value: unknown): ActiveLibraryContext { this.active = validateActiveLibraryContext(value); return { ...this.active }; } registerProvider(provider: InMemoryCatalogProvider): this { this.providers.set(provider.provider, provider); return this; } activeLibraryContext(): ActiveLibraryContext | null { return this.active ? { ...this.active } : null; } async resolve(value: unknown, signal?: AbortSignal): Promise { const request = validateResourceRequest(value); if (signal?.aborted) throw new CorrectiveResourceError("CANCELLED", "resource lookup cancelled"); if (!this.active || this.active.sourceKind !== request.sourceKind || this.active.sourceId !== request.sourceId) { throw new CorrectiveResourceError("CONTEXT_INVALID", "request does not match active library context"); } const providerKind: ResourceProviderKind = request.sourceKind === "LOCAL" ? "LOCAL" : request.sourceKind === "EXTERNAL" ? "EXTERNAL_LIBRARY" : "PACKED_BLEND"; const provider = this.providers.get(providerKind); if (!provider) throw new CorrectiveResourceError("RESOURCE_REQUIRED", `${providerKind} provider is required`); const bytes = await provider.load(request.sourceId, signal); const observed = await digest(bytes); if (observed !== request.sha256) throw new CorrectiveResourceError("RESOURCE_HASH_MISMATCH", `resource hash ${observed} does not match request`); return { schemaVersion: CORRECTIVE_RESOURCE_SCHEMA, provider: providerKind, sourceKind: request.sourceKind, sourceId: request.sourceId, sha256: observed, bytes }; } }