import type { JsonObject } from "../protocol"; /** * M2-01 development-time Surface declaration. These records are authored by * the host, bundled with it, and never accepted from a conversation payload. * Remote bundle URLs, hashes and downloads deliberately belong to M4. */ export type SurfaceManifest = Readonly<{ app_id: string; version: string; bundle_id: string; state_schema_version: number; max_state_bytes: number; max_bundle_bytes: number; local_bundle_bytes: number; development_only: true; }>; export type EnabledSurface = Readonly<{ app_id: string; version: string; enabled_at: string; }>; export type SurfaceRegistryDisposition = | "accepted" | "unknown_app" | "version_mismatch" | "disabled" | "invalid_manifest" | "invalid_state" | "state_too_large" | "bundle_override_forbidden"; export type SurfaceRegistryResult = Readonly<{ disposition: SurfaceRegistryDisposition; manifest?: SurfaceManifest; }>; export type SurfaceRegistrySnapshot = Readonly<{ version: 1; enabled: readonly EnabledSurface[]; }>; export type SurfaceOpenRequest = Readonly<{ instance_id: string; app: Readonly<{ app_id: string; version: string }>; state?: JsonObject; }>; /** Shared contract for development and production Surface admission policies. */ export interface SurfaceAdmissionPolicy { validateOpenRequest(payload: unknown): SurfaceRegistryResult; validateState(appID: string, version: string, state: unknown): SurfaceRegistryResult; } const APP_ID = /^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/; const VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/; const BUNDLE_ID = /^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/; const INSTANCE_ID = /^[A-Za-z0-9._:-]{1,128}$/; const MAX_APP_ID_LENGTH = 96; const MAX_VERSION_LENGTH = 64; const MAX_BUNDLE_ID_LENGTH = 128; const MAX_STATE_DEPTH = 16; const MAX_STATE_KEYS = 512; const MAX_DECLARED_STATE_BYTES = 64 * 1024; const MAX_DECLARED_BUNDLE_BYTES = 512 * 1024; /** * The sole surface known in the development build. Declaring it here does * not make it visible to an Agent or openable: App Center must explicitly * enable this exact app/version first. */ export const DEVELOPMENT_SURFACE_MANIFESTS: readonly SurfaceManifest[] = Object.freeze([ Object.freeze({ app_id: "lineup.task-dashboard", version: "0.1.0", bundle_id: "lineup.task-dashboard.dev.v1", state_schema_version: 1, max_state_bytes: 32 * 1024, max_bundle_bytes: 256 * 1024, local_bundle_bytes: 0, development_only: true, }), ]); /** * Local App Center enablement boundary. It is intentionally free of DOM, * storage, transport, iframe and capability concerns so it can be persisted * by a future host adapter without changing its security rules. */ export class SurfaceRegistry { private readonly manifests = new Map(); private readonly enabled = new Map(); public constructor(manifests: readonly SurfaceManifest[] = DEVELOPMENT_SURFACE_MANIFESTS) { for (const candidate of manifests) { const manifest = normalizeManifest(candidate); if (!manifest || this.manifests.has(key(manifest.app_id, manifest.version))) continue; this.manifests.set(key(manifest.app_id, manifest.version), manifest); } } public enable(appID: string, version: string, enabledAt: string): SurfaceRegistryResult { const result = this.find(appID, version); if (result.disposition !== "accepted" || !result.manifest || !validTimestamp(enabledAt)) return result.disposition === "accepted" ? { disposition: "invalid_manifest" } : result; this.enabled.set(key(appID, version), { app_id: appID, version, enabled_at: enabledAt }); return result; } public disable(appID: string, version: string): SurfaceRegistryResult { const result = this.find(appID, version); if (result.disposition !== "accepted") return result; this.enabled.delete(key(appID, version)); return result; } public isEnabled(appID: string, version: string): boolean { return this.enabled.has(key(appID, version)); } /** Resolves only an exact, locally enabled declaration; no inventory leaks. */ public resolveEnabled(appID: string, version: string): SurfaceRegistryResult { const result = this.find(appID, version); if (result.disposition !== "accepted") return result; return this.enabled.has(key(appID, version)) ? result : { disposition: "disabled" }; } /** Validates a trusted host state before Instance Manager can retain it. */ public validateState(appID: string, version: string, state: unknown): SurfaceRegistryResult { const result = this.resolveEnabled(appID, version); if (result.disposition !== "accepted" || !result.manifest) return result; const measured = measureState(state); if (!measured.valid) return { disposition: "invalid_state" }; return measured.bytes <= result.manifest.max_state_bytes ? result : { disposition: "state_too_large" }; } /** * Rejects payload attempts to replace the locally packaged bundle before * they can reach Instance Manager. This validates shape only; M2-02 owns * lifecycle semantics and M2-04 owns the dashboard state schema. */ public validateOpenRequest(payload: unknown): SurfaceRegistryResult { if (!isPlainObject(payload)) return { disposition: "invalid_state" }; if (hasBundleOverride(payload)) return { disposition: "bundle_override_forbidden" }; const instanceID = payload.instance_id; const app = payload.app; if (typeof instanceID !== "string" || !INSTANCE_ID.test(instanceID) || !isPlainObject(app) || !validAppID(app.app_id) || !validVersion(app.version)) return { disposition: "invalid_state" }; if (payload.state !== undefined && !isPlainObject(payload.state)) return { disposition: "invalid_state" }; return this.validateState(app.app_id, app.version, payload.state ?? {}); } public snapshot(): SurfaceRegistrySnapshot { return { version: 1, enabled: [...this.enabled.values()] .sort((left, right) => key(left.app_id, left.version).localeCompare(key(right.app_id, right.version))) .map(entry => ({ ...entry })), }; } /** Restoring never adds an unknown, mismatched, malformed or duplicate app. */ public restore(snapshot: unknown): void { this.enabled.clear(); if (!isPlainObject(snapshot) || snapshot.version !== 1 || !Array.isArray(snapshot.enabled)) return; for (const raw of snapshot.enabled) { if (!isPlainObject(raw) || !validAppID(raw.app_id) || !validVersion(raw.version) || !validTimestamp(raw.enabled_at)) continue; const result = this.find(raw.app_id, raw.version); if (result.disposition !== "accepted") continue; this.enabled.set(key(raw.app_id, raw.version), { app_id: raw.app_id, version: raw.version, enabled_at: raw.enabled_at }); } } private find(appID: string, version: string): SurfaceRegistryResult { if (!validAppID(appID)) return { disposition: "unknown_app" }; const matchingApp = [...this.manifests.values()].some(manifest => manifest.app_id === appID); if (!matchingApp) return { disposition: "unknown_app" }; if (!validVersion(version) || !this.manifests.has(key(appID, version))) return { disposition: "version_mismatch" }; return { disposition: "accepted", manifest: this.manifests.get(key(appID, version))! }; } } function normalizeManifest(value: SurfaceManifest): SurfaceManifest | undefined { if (!value || typeof value !== "object" || !validAppID(value.app_id) || !validVersion(value.version) || !validBundleID(value.bundle_id) || !Number.isSafeInteger(value.state_schema_version) || value.state_schema_version < 1 || !validBound(value.max_state_bytes, MAX_DECLARED_STATE_BYTES) || !validBound(value.max_bundle_bytes, MAX_DECLARED_BUNDLE_BYTES) || !Number.isSafeInteger(value.local_bundle_bytes) || value.local_bundle_bytes < 0 || value.local_bundle_bytes > value.max_bundle_bytes || value.development_only !== true) return undefined; return Object.freeze({ ...value }); } function validAppID(value: unknown): value is string { return typeof value === "string" && value.length <= MAX_APP_ID_LENGTH && APP_ID.test(value); } function validVersion(value: unknown): value is string { return typeof value === "string" && value.length <= MAX_VERSION_LENGTH && VERSION.test(value); } function validBundleID(value: unknown): value is string { return typeof value === "string" && value.length <= MAX_BUNDLE_ID_LENGTH && BUNDLE_ID.test(value); } function validBound(value: unknown, maximum: number): value is number { return typeof value === "number" && Number.isSafeInteger(value) && value > 0 && value <= maximum; } function validTimestamp(value: unknown): value is string { return typeof value === "string" && !Number.isNaN(Date.parse(value)); } function key(appID: string, version: string): string { return `${appID}@${version}`; } function isPlainObject(value: unknown): value is Record { if (value === null || typeof value !== "object" || Array.isArray(value)) return false; const prototype = Object.getPrototypeOf(value); return prototype === Object.prototype || prototype === null; } function hasBundleOverride(payload: Record): boolean { return ["bundle", "bundle_id", "bundle_url", "url", "html", "css", "javascript", "js", "script", "source"].some(field => field in payload); } function measureState(value: unknown): { valid: boolean; bytes: number } { let keys = 0; const visit = (candidate: unknown, depth: number): boolean => { if (depth > MAX_STATE_DEPTH || candidate === null || typeof candidate === "string" || typeof candidate === "boolean") return depth <= MAX_STATE_DEPTH; if (typeof candidate === "number") return Number.isFinite(candidate); if (Array.isArray(candidate)) return candidate.every(entry => visit(entry, depth + 1)); if (!isPlainObject(candidate)) return false; const entries = Object.entries(candidate); keys += entries.length; return keys <= MAX_STATE_KEYS && entries.every(([entryKey, entryValue]) => entryKey.length <= 128 && visit(entryValue, depth + 1)); }; if (!isPlainObject(value) || !visit(value, 0)) return { valid: false, bytes: 0 }; try { return { valid: true, bytes: new TextEncoder().encode(JSON.stringify(value as JsonObject)).byteLength }; } catch { return { valid: false, bytes: 0 }; } } /** A narrow helper for the M2-02 manager; it does not mutate registry state. */ export function parseSurfaceOpenRequest(payload: unknown): SurfaceOpenRequest | undefined { if (!isPlainObject(payload) || hasBundleOverride(payload) || typeof payload.instance_id !== "string" || !INSTANCE_ID.test(payload.instance_id) || !isPlainObject(payload.app) || !validAppID(payload.app.app_id) || !validVersion(payload.app.version) || (payload.state !== undefined && !isPlainObject(payload.state))) return undefined; return { instance_id: payload.instance_id, app: { app_id: payload.app.app_id, version: payload.app.version }, ...(payload.state === undefined ? {} : { state: payload.state as JsonObject }), }; } /** Exposed only for focused state-limit tests; no renderer should need it. */ export function isSurfaceStateJson(value: unknown): value is JsonObject { return measureState(value).valid && isPlainObject(value); }