import { PERMS } from "@/lib/permission-slugs"; import { HOUSEKEEPING_DOMAIN_IDS } from "../migration/types"; import { satisfiesCapability } from "./capability-context"; import type { CapabilityRequirement, HousekeepingCapabilityContext, HousekeepingDomainManifest, HousekeepingErrorCode, HousekeepingWidgetDefinition, } from "./contracts"; import { ok } from "./contracts"; import { reconcilePreferences } from "./preferences/reconcile"; import type { HousekeepingPreferences } from "./preferences/schema"; import { type HousekeepingProvider, runProvider, } from "./providers/run-provider"; const approvedDomainIds = new Set(HOUSEKEEPING_DOMAIN_IDS); const knownCapabilitySlugs = new Set(Object.values(PERMS)); interface OwnedRegistryEntry { id: string; owner: HousekeepingDomainManifest["id"]; capability: CapabilityRequirement; } export interface HousekeepingRegistry { domains: readonly HousekeepingDomainManifest[]; } export const WIDGET_POLICY = Object.freeze({ timeoutMs: 2_000 } as const); export interface HousekeepingWidgetLoadResult { readonly widgets: readonly { readonly id: string; readonly data: unknown }[]; readonly errors: readonly { readonly widgetId: string; readonly code: HousekeepingErrorCode; }[]; } function widgetProvider( widget: HousekeepingWidgetDefinition, ): HousekeepingProvider { return { id: widget.id, capability: widget.capability, async run(context, signal) { const result = await widget.load(context, signal); return result.ok ? ok([result.data], result.correlationId) : result; }, }; } export async function loadHousekeepingWidgets( registry: HousekeepingRegistry, preferences: HousekeepingPreferences, context: HousekeepingCapabilityContext, timeoutMs: number = WIDGET_POLICY.timeoutMs, ): Promise { const reconciled = reconcilePreferences(preferences, registry, context); const enabledOptionalIds = new Set(reconciled.enabledOptionalWidgetIds); const selected = registry.domains .filter((domain) => satisfiesCapability(context, domain.capability)) .flatMap((domain) => domain.widgets) .filter( (widget) => satisfiesCapability(context, widget.capability) && (widget.kind === "mandatory" || enabledOptionalIds.has(widget.id)), ); const selectedById = new Map(selected.map((widget) => [widget.id, widget])); const orderedIds = reconciled.widgetOrder.filter((id) => selectedById.has(id), ); for (const widget of selected) { if (!orderedIds.includes(widget.id)) orderedIds.push(widget.id); } const ordered = orderedIds.flatMap((id) => { const widget = selectedById.get(id); return widget ? [widget] : []; }); const outcomes = await Promise.all( ordered.map((widget) => runProvider(widgetProvider(widget), context, timeoutMs), ), ); return { widgets: outcomes.flatMap((outcome) => outcome.error === undefined ? [{ id: outcome.providerId, data: outcome.items[0] }] : [], ), errors: outcomes.flatMap((outcome) => outcome.error === undefined ? [] : [{ widgetId: outcome.providerId, code: outcome.error }], ), }; } export function createHousekeepingRegistry( manifests: readonly HousekeepingDomainManifest[], ): HousekeepingRegistry { const domainIds = new Set(); const routeIds = new Set(); const routeHrefs = new Set(); const routeShapes = new Set(); const searchProviderIds = new Set(); const inboxSourceIds = new Set(); const widgetIds = new Set(); for (const manifest of manifests) { if (!approvedDomainIds.has(manifest.id)) { throw new Error(`unknown domain id: ${manifest.id}`); } if (domainIds.has(manifest.id)) { throw new Error(`duplicate domain id: ${manifest.id}`); } domainIds.add(manifest.id); if ( manifest.canonicalHref !== (manifest.id === "operations" ? "/ase" : `/ase/${manifest.id}`) ) { throw new Error(`invalid canonical href: ${manifest.canonicalHref}`); } validateNonEmpty(manifest.labelKey, "label key"); validateNonEmpty(manifest.descriptionKey, "description key"); validateCapability(manifest.capability); validateOwnedRegistryEntries( manifest.searchProviders, manifest.id, "search provider", searchProviderIds, ); validateOwnedRegistryEntries( manifest.inboxSources, manifest.id, "inbox source", inboxSourceIds, ); validateOwnedRegistryEntries( manifest.widgets, manifest.id, "widget", widgetIds, ); for (const widget of manifest.widgets) { if (widget.kind !== "mandatory" && widget.kind !== "optional") { throw new Error(`invalid widget kind: ${widget.id}`); } } for (const route of manifest.routes) { validateNonEmpty(route.id, "route id"); if (routeIds.has(route.id)) { throw new Error(`duplicate route id: ${route.id}`); } routeIds.add(route.id); validateNonEmpty(route.href, "route href"); if (route.href !== "/ase" && !route.href.startsWith("/ase/")) { throw new Error(`invalid canonical route href: ${route.href}`); } if (routeHrefs.has(route.href)) { throw new Error(`duplicate route href: ${route.href}`); } routeHrefs.add(route.href); const routeShape = dynamicRouteShape(route.href); if (routeShape && routeShapes.has(routeShape)) { throw new Error(`duplicate dynamic route shape: ${routeShape}`); } if (routeShape) routeShapes.add(routeShape); validateNonEmpty(route.labelKey, "route label key"); validateCapability(route.capability); } } return { domains: Object.freeze([...manifests]) }; } function dynamicRouteShape(href: string): string | null { let hasDynamicSegment = false; const shape = href .split("/") .map((segment) => { if (!segment.startsWith(":")) return segment; hasDynamicSegment = true; return ":"; }) .join("/"); return hasDynamicSegment ? shape : null; } function validateOwnedRegistryEntries( entries: readonly OwnedRegistryEntry[], owner: HousekeepingDomainManifest["id"], kind: "search provider" | "inbox source" | "widget", ids: Set, ): void { for (const entry of entries) { validateNonEmpty(entry.id, `${kind} id`); if (ids.has(entry.id)) { throw new Error(`duplicate ${kind} id: ${entry.id}`); } ids.add(entry.id); if (entry.owner !== owner) { throw new Error(`${kind} owner mismatch: ${entry.id}`); } validateCapability(entry.capability); } } function validateNonEmpty(value: string, name: string): void { if (typeof value !== "string" || !value.trim()) { throw new Error(`empty ${name}`); } } function validateCapability(requirement: CapabilityRequirement): void { if ( typeof requirement !== "object" || requirement === null || (requirement.mode !== "any" && requirement.mode !== "all") || !Array.isArray(requirement.slugs) ) { throw new Error("invalid capability requirement"); } if (requirement.slugs.length === 0) { throw new Error("empty capability requirement"); } for (const slug of requirement.slugs) { if (typeof slug !== "string") { throw new Error("invalid capability requirement"); } if (!knownCapabilitySlugs.has(slug)) { throw new Error(`unknown capability slug: ${slug}`); } } }