{"version":3,"file":"adapt-sandbox-entry.mjs","names":[],"sources":["../../src/plugins/adapt-sandbox-entry.ts"],"sourcesContent":["/**\n * In-Process Adapter for Standard-Format Plugins\n *\n * Converts a standard plugin definition ({ hooks, routes }) into a\n * ResolvedPlugin compatible with HookPipeline. This allows standard-format\n * plugins to run in-process when placed in the `plugins: []` config array.\n *\n * The adapter wraps each hook and route handler so that the PluginContextFactory\n * provides the same capability-gated context as the native path.\n *\n */\n\nimport type { PluginDescriptor } from \"../astro/integration/runtime.js\";\nimport type { RouteEntry, RouteHandler, SandboxedPlugin } from \"../plugin-types.js\";\nimport { PLUGIN_CAPABILITIES, HOOK_NAMES } from \"./manifest-schema.js\";\nimport { normalizeCapabilities } from \"./types.js\";\nimport type {\n\tResolvedPlugin,\n\tResolvedPluginHooks,\n\tResolvedHook,\n\tPluginRoute,\n\tPluginCapability,\n\tPluginStorageConfig,\n\tPluginAdminConfig,\n} from \"./types.js\";\n\n/**\n * Loose per-hook entry shape used inside the adapter's iteration loop.\n *\n * `SandboxedPlugin.hooks` is a mapped type keyed by hook name, so each\n * entry's type depends on the key. When the adapter iterates with\n * `Object.entries`, the key is `string` (TypeScript can't see the\n * narrowing), so we need a *union* type that covers every hook entry\n * shape — bare handler or config form. This is that union, kept local\n * because it has no use outside the adapter.\n */\n// eslint-disable-next-line typescript-eslint/no-explicit-any -- must accept handlers with specific event types across all hook names\ntype AnyHookHandler = (...args: any[]) => Promise<any>;\ntype AnyHookEntry =\n\t| AnyHookHandler\n\t| {\n\t\t\thandler: AnyHookHandler;\n\t\t\tpriority?: number;\n\t\t\ttimeout?: number;\n\t\t\tdependencies?: string[];\n\t\t\terrorPolicy?: \"continue\" | \"abort\";\n\t\t\texclusive?: boolean;\n\t  };\n\n/**\n * Default hook configuration values\n */\nconst DEFAULT_PRIORITY = 100;\nconst DEFAULT_TIMEOUT = 5000;\nconst DEFAULT_ERROR_POLICY = \"abort\" as const;\n\n/**\n * Check if a hook entry is the config form (has a `handler` property).\n */\nfunction isHookConfig(entry: AnyHookEntry): entry is Exclude<AnyHookEntry, AnyHookHandler> {\n\treturn typeof entry === \"object\" && entry !== null && \"handler\" in entry;\n}\n\n/**\n * Resolve a single hook entry to a ResolvedHook.\n *\n * Sandboxed-format hooks use the standard two-arg convention:\n *   handler(event, ctx)\n *\n * The HookPipeline dispatch methods also call handlers with (event, ctx),\n * so the handler is compatible as-is — we just normalise the\n * surrounding config (priority, timeout, etc.) to its defaults.\n */\nfunction resolveSandboxedHook(entry: AnyHookEntry, pluginId: string): ResolvedHook<AnyHookHandler> {\n\tif (isHookConfig(entry)) {\n\t\treturn {\n\t\t\tpriority: entry.priority ?? DEFAULT_PRIORITY,\n\t\t\ttimeout: entry.timeout ?? DEFAULT_TIMEOUT,\n\t\t\tdependencies: entry.dependencies ?? [],\n\t\t\terrorPolicy: entry.errorPolicy ?? DEFAULT_ERROR_POLICY,\n\t\t\texclusive: entry.exclusive ?? false,\n\t\t\thandler: entry.handler,\n\t\t\tpluginId,\n\t\t};\n\t}\n\n\t// Bare function handler\n\treturn {\n\t\tpriority: DEFAULT_PRIORITY,\n\t\ttimeout: DEFAULT_TIMEOUT,\n\t\tdependencies: [],\n\t\terrorPolicy: DEFAULT_ERROR_POLICY,\n\t\texclusive: false,\n\t\thandler: entry,\n\t\tpluginId,\n\t};\n}\n\n/**\n * Normalise a `RouteEntry` (bare handler or `{ handler, public?, input? }`\n * config) to the config form. The `input` schema is intentionally typed\n * `unknown` in `RouteEntry` — sandboxed plugins describe it loosely\n * because the strict `z.ZodType<TInput>` constraint of the runtime's\n * `PluginRoute` only narrows once the route is wired into the router.\n * The wider type flows through to the runtime which validates at\n * invocation time.\n */\nfunction normalizeRouteEntry(entry: RouteEntry): {\n\thandler: RouteHandler;\n\tpublic?: boolean;\n\tcacheControl?: string;\n\tinput?: PluginRoute[\"input\"];\n\tpermission?: PluginRoute[\"permission\"];\n} {\n\tif (typeof entry === \"function\") {\n\t\treturn { handler: entry };\n\t}\n\treturn {\n\t\thandler: entry.handler,\n\t\tpublic: entry.public,\n\t\tpermission: entry.permission,\n\t\tcacheControl: entry.cacheControl,\n\t\t// eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- RouteEntry.input is intentionally `unknown` (sandboxed plugins) and validated by the runtime at invocation time\n\t\tinput: entry.input as PluginRoute[\"input\"],\n\t};\n}\n\nconst VALID_CAPABILITIES_SET = new Set<string>(PLUGIN_CAPABILITIES);\n\nconst VALID_HOOK_NAMES_SET = new Set<string>(HOOK_NAMES);\n\n/**\n * Adapt a sandboxed plugin's default export into a ResolvedPlugin.\n *\n * This is the in-process side of sandboxed-format plugins: it takes\n * the `{ hooks, routes }` default export of a sandboxed plugin and\n * produces a `ResolvedPlugin` that enters the HookPipeline alongside\n * native plugins. The descriptor supplies identity (id, version) and\n * the trust contract (capabilities, allowedHosts, storage); the\n * definition supplies behaviour.\n *\n * @param definition - The plugin's default export (matching `SandboxedPlugin` from `emdash/plugin`).\n * @param descriptor - The plugin descriptor with id, version, capabilities, etc.\n * @returns A ResolvedPlugin compatible with HookPipeline.\n */\nexport function adaptSandboxEntry(\n\tdefinition: SandboxedPlugin,\n\tdescriptor: PluginDescriptor,\n): ResolvedPlugin {\n\tconst pluginId = descriptor.id;\n\tconst version = descriptor.version;\n\n\t// A null / array / non-object `definition` would throw a generic\n\t// `TypeError: Cannot read properties of null` further down the\n\t// loop without the plugin id; surface a useful error first.\n\tif (typeof definition !== \"object\" || definition === null || Array.isArray(definition)) {\n\t\tthrow new Error(\n\t\t\t`Plugin \"${pluginId}\" default export must be an object with ` +\n\t\t\t\t`\\`hooks\\` and/or \\`routes\\` (got ${\n\t\t\t\t\tArray.isArray(definition) ? \"array\" : typeof definition\n\t\t\t\t}). Did you forget \\`export default {...} satisfies SandboxedPlugin\\`?`,\n\t\t);\n\t}\n\n\t// Resolve hooks. `SandboxedPlugin.hooks` is keyed by hook name with\n\t// per-key entry types; iterating with `Object.entries` collapses\n\t// keys to `string`, so we treat each entry as the union `AnyHookEntry`\n\t// for the duration of the loop. The widening from the strict mapped\n\t// type to a plain record is sound because each entry still matches\n\t// one of the bare-handler / config-object shapes captured by\n\t// `AnyHookEntry`.\n\tconst resolvedHooks: ResolvedPluginHooks = {};\n\tif (definition.hooks) {\n\t\t// eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- widening the strict mapped type to a string-keyed record for iteration; entries still match AnyHookEntry\n\t\tconst hookMap = definition.hooks as Record<string, AnyHookEntry>;\n\t\tfor (const [hookName, entry] of Object.entries(hookMap)) {\n\t\t\tif (!VALID_HOOK_NAMES_SET.has(hookName)) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Plugin \"${pluginId}\" declares unknown hook \"${hookName}\". ` +\n\t\t\t\t\t\t`Valid hooks: ${[...VALID_HOOK_NAMES_SET].join(\", \")}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\t// The resolved hook has the correct handler type for the hook name.\n\t\t\t// We store it as the generic type and let HookPipeline's typed dispatch\n\t\t\t// methods handle the type narrowing at call time.\n\t\t\t// eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- bridging untyped map to typed interface\n\t\t\t(resolvedHooks as Record<string, unknown>)[hookName] = resolveSandboxedHook(entry, pluginId);\n\t\t}\n\t}\n\n\t// Resolve routes: sandboxed format uses (routeCtx, pluginCtx) two-arg\n\t// pattern. Standard format (`definePlugin(...)` as the default export)\n\t// uses the public (ctx: RouteContext) single-arg pattern where\n\t// RouteContext extends PluginContext with { input, request, requestMeta }.\n\t//\n\t// The two conventions disagree on the FIRST argument (`request` is a\n\t// flattened plain object for sandboxed, a real WHATWG `Request` for\n\t// standard), so the wrapper must know which contract the handler was\n\t// written against. `definePlugin` requires `id`, and sandbox-format\n\t// default exports never carry one (identity comes from the manifest's\n\t// slug + publisher) — the same \"no id\" signal definePlugin itself\n\t// documents. Calling a single-arg standard handler with the two-arg\n\t// convention silently hands it the bare route context (JS drops the\n\t// extra argument), so `ctx.storage` / `ctx.email` / etc. are all\n\t// undefined at runtime (#2079).\n\t//\n\t// Route entries can be bare functions or `{ handler, public?, input? }`\n\t// config objects; normalise to the config shape inside the loop.\n\tconst usesPublicRouteContext = \"id\" in definition && typeof definition.id === \"string\";\n\tconst resolvedRoutes: Record<string, PluginRoute> = {};\n\tif (definition.routes) {\n\t\tfor (const [routeName, rawEntry] of Object.entries(definition.routes)) {\n\t\t\tconst normalized = normalizeRouteEntry(rawEntry);\n\t\t\tconst {\n\t\t\t\thandler,\n\t\t\t\tpublic: publicFlag,\n\t\t\t\tcacheControl,\n\t\t\t\tinput: inputSchema,\n\t\t\t\tpermission,\n\t\t\t} = normalized;\n\t\t\tresolvedRoutes[routeName] = {\n\t\t\t\tinput: inputSchema,\n\t\t\t\tpublic: publicFlag,\n\t\t\t\tpermission,\n\t\t\t\tcacheControl,\n\t\t\t\thandler: async (ctx) => {\n\t\t\t\t\tif (usesPublicRouteContext) {\n\t\t\t\t\t\t// The incoming ctx already IS the public RouteContext\n\t\t\t\t\t\t// (full PluginContext + input / real Request /\n\t\t\t\t\t\t// requestMeta) — pass it through unchanged.\n\t\t\t\t\t\t// eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- standard-format handlers are authored against the public single-arg PluginRoute contract; the sandbox RouteHandler type on `normalized` is the wider authoring union\n\t\t\t\t\t\treturn (handler as unknown as PluginRoute[\"handler\"])(ctx);\n\t\t\t\t\t}\n\t\t\t\t\t// `ctx.request` is a real WHATWG `Request` (this is the\n\t\t\t\t\t// in-process adapter; the worker-sandbox adapter handles\n\t\t\t\t\t// the serialised case). Flatten `Headers` to the plain\n\t\t\t\t\t// `Record<string, string>` shape that author-facing\n\t\t\t\t\t// `SandboxedRequest` promises so handler bodies are\n\t\t\t\t\t// identical across both adapters.\n\t\t\t\t\tconst headers: Record<string, string> = {};\n\t\t\t\t\tctx.request.headers.forEach((value, name) => {\n\t\t\t\t\t\theaders[name] = value;\n\t\t\t\t\t});\n\t\t\t\t\tconst requestShape = {\n\t\t\t\t\t\turl: ctx.request.url,\n\t\t\t\t\t\tmethod: ctx.request.method,\n\t\t\t\t\t\theaders,\n\t\t\t\t\t};\n\t\t\t\t\tconst routeCtx = {\n\t\t\t\t\t\tinput: ctx.input,\n\t\t\t\t\t\trequest: requestShape,\n\t\t\t\t\t\trequestMeta: ctx.requestMeta,\n\t\t\t\t\t\tuser: ctx.user,\n\t\t\t\t\t};\n\t\t\t\t\tconst { input: _, request: __, requestMeta: ___, user: ____, ...pluginCtx } = ctx;\n\t\t\t\t\treturn handler(routeCtx, pluginCtx);\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t}\n\n\t// Build capabilities from descriptor.\n\t// Validate against the known set (same as defineNativePlugin). Both\n\t// current and deprecated names are accepted; deprecated names are\n\t// silently normalized to current names below so the runtime only ever\n\t// sees the canonical form.\n\tconst rawCapabilities = descriptor.capabilities ?? [];\n\tfor (const cap of rawCapabilities) {\n\t\tif (!VALID_CAPABILITIES_SET.has(cap)) {\n\t\t\tthrow new Error(\n\t\t\t\t`Invalid capability \"${cap}\" in plugin \"${pluginId}\". ` +\n\t\t\t\t\t`Valid capabilities: ${[...VALID_CAPABILITIES_SET].join(\", \")}`,\n\t\t\t);\n\t\t}\n\t}\n\n\t// Silent normalization: rewrite deprecated names to current names.\n\t// Safe assertion — `normalizeCapabilities` only emits validated input\n\t// plus current names from the rename map, all of which are in the union.\n\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- validated above; normalizeCapabilities only returns capabilities from the union\n\tconst capabilities = normalizeCapabilities(rawCapabilities) as PluginCapability[];\n\tconst allowedHosts = descriptor.allowedHosts ?? [];\n\n\t// Capability implications: broader capabilities imply narrower ones\n\t// (mirrors the normalization in define-plugin.ts for native format).\n\t// Operates on canonical names only.\n\tif (capabilities.includes(\"content:write\") && !capabilities.includes(\"content:read\")) {\n\t\tcapabilities.push(\"content:read\");\n\t}\n\tif (capabilities.includes(\"media:write\") && !capabilities.includes(\"media:read\")) {\n\t\tcapabilities.push(\"media:read\");\n\t}\n\tif (\n\t\tcapabilities.includes(\"network:request:unrestricted\") &&\n\t\t!capabilities.includes(\"network:request\")\n\t) {\n\t\tcapabilities.push(\"network:request\");\n\t}\n\n\t// Build storage config from descriptor.\n\t// StorageCollectionDeclaration uses optional indexes, but PluginStorageConfig\n\t// requires them. Ensure every collection has an indexes array.\n\tconst rawStorage = descriptor.storage ?? {};\n\tconst storage: PluginStorageConfig = {};\n\tfor (const [name, config] of Object.entries(rawStorage)) {\n\t\tstorage[name] = {\n\t\t\tindexes: config.indexes ?? [],\n\t\t\tuniqueIndexes: config.uniqueIndexes,\n\t\t};\n\t}\n\n\t// Build admin config from descriptor.\n\t// Portable Text blocks and field widgets are declarative (Block Kit), so they\n\t// are forwarded for standard/sandboxed plugins just like pages and widgets —\n\t// the admin editor consumes them from the manifest. Only the site-side render\n\t// component (`componentsEntry`) stays native-only.\n\tconst admin: PluginAdminConfig = {};\n\tif (descriptor.adminPages) {\n\t\tadmin.pages = descriptor.adminPages;\n\t}\n\tif (descriptor.adminWidgets) {\n\t\tadmin.widgets = descriptor.adminWidgets;\n\t}\n\tif (descriptor.settingsSchema) {\n\t\tadmin.settingsSchema = descriptor.settingsSchema;\n\t}\n\tif (descriptor.portableTextBlocks) {\n\t\tadmin.portableTextBlocks = descriptor.portableTextBlocks;\n\t}\n\tif (descriptor.fieldWidgets) {\n\t\tadmin.fieldWidgets = descriptor.fieldWidgets;\n\t}\n\n\treturn {\n\t\tid: pluginId,\n\t\tversion,\n\t\tcapabilities,\n\t\tallowedHosts,\n\t\tstorage,\n\t\thooks: resolvedHooks,\n\t\troutes: resolvedRoutes,\n\t\tmcp: {\n\t\t\ttools: Object.fromEntries(\n\t\t\t\tObject.entries(definition.mcp?.tools ?? {}).map(([name, tool]) => [\n\t\t\t\t\tname,\n\t\t\t\t\t{\n\t\t\t\t\t\tdescription: tool.description,\n\t\t\t\t\t\troute: tool.route,\n\t\t\t\t\t\tinput: tool.input,\n\t\t\t\t\t\toutput: tool.output,\n\t\t\t\t\t\tdestructive: tool.destructive,\n\t\t\t\t\t},\n\t\t\t\t]),\n\t\t\t),\n\t\t},\n\t\tadmin,\n\t};\n}\n"],"mappings":";;;;;;;AAoDA,MAAM,mBAAmB;AACzB,MAAM,kBAAkB;AACxB,MAAM,uBAAuB;;;;AAK7B,SAAS,aAAa,OAAqE;AAC1F,QAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,aAAa;;;;;;;;;;;;AAapE,SAAS,qBAAqB,OAAqB,UAAgD;AAClG,KAAI,aAAa,MAAM,CACtB,QAAO;EACN,UAAU,MAAM,YAAY;EAC5B,SAAS,MAAM,WAAW;EAC1B,cAAc,MAAM,gBAAgB,EAAE;EACtC,aAAa,MAAM,eAAe;EAClC,WAAW,MAAM,aAAa;EAC9B,SAAS,MAAM;EACf;EACA;AAIF,QAAO;EACN,UAAU;EACV,SAAS;EACT,cAAc,EAAE;EAChB,aAAa;EACb,WAAW;EACX,SAAS;EACT;EACA;;;;;;;;;;;AAYF,SAAS,oBAAoB,OAM3B;AACD,KAAI,OAAO,UAAU,WACpB,QAAO,EAAE,SAAS,OAAO;AAE1B,QAAO;EACN,SAAS,MAAM;EACf,QAAQ,MAAM;EACd,YAAY,MAAM;EAClB,cAAc,MAAM;EAEpB,OAAO,MAAM;EACb;;AAGF,MAAM,yBAAyB,IAAI,IAAY,oBAAoB;AAEnE,MAAM,uBAAuB,IAAI,IAAY,WAAW;;;;;;;;;;;;;;;AAgBxD,SAAgB,kBACf,YACA,YACiB;CACjB,MAAM,WAAW,WAAW;CAC5B,MAAM,UAAU,WAAW;AAK3B,KAAI,OAAO,eAAe,YAAY,eAAe,QAAQ,MAAM,QAAQ,WAAW,CACrF,OAAM,IAAI,MACT,WAAW,SAAS,2EAElB,MAAM,QAAQ,WAAW,GAAG,UAAU,OAAO,WAC7C,uEACF;CAUF,MAAM,gBAAqC,EAAE;AAC7C,KAAI,WAAW,OAAO;EAErB,MAAM,UAAU,WAAW;AAC3B,OAAK,MAAM,CAAC,UAAU,UAAU,OAAO,QAAQ,QAAQ,EAAE;AACxD,OAAI,CAAC,qBAAqB,IAAI,SAAS,CACtC,OAAM,IAAI,MACT,WAAW,SAAS,2BAA2B,SAAS,kBACvC,CAAC,GAAG,qBAAqB,CAAC,KAAK,KAAK,GACrD;AAMF,GAAC,cAA0C,YAAY,qBAAqB,OAAO,SAAS;;;CAsB9F,MAAM,yBAAyB,QAAQ,cAAc,OAAO,WAAW,OAAO;CAC9E,MAAM,iBAA8C,EAAE;AACtD,KAAI,WAAW,OACd,MAAK,MAAM,CAAC,WAAW,aAAa,OAAO,QAAQ,WAAW,OAAO,EAAE;EAEtE,MAAM,EACL,SACA,QAAQ,YACR,cACA,OAAO,aACP,eANkB,oBAAoB,SAAS;AAQhD,iBAAe,aAAa;GAC3B,OAAO;GACP,QAAQ;GACR;GACA;GACA,SAAS,OAAO,QAAQ;AACvB,QAAI,uBAKH,QAAQ,QAA8C,IAAI;IAQ3D,MAAM,UAAkC,EAAE;AAC1C,QAAI,QAAQ,QAAQ,SAAS,OAAO,SAAS;AAC5C,aAAQ,QAAQ;MACf;IACF,MAAM,eAAe;KACpB,KAAK,IAAI,QAAQ;KACjB,QAAQ,IAAI,QAAQ;KACpB;KACA;IACD,MAAM,WAAW;KAChB,OAAO,IAAI;KACX,SAAS;KACT,aAAa,IAAI;KACjB,MAAM,IAAI;KACV;IACD,MAAM,EAAE,OAAO,GAAG,SAAS,IAAI,aAAa,KAAK,MAAM,MAAM,GAAG,cAAc;AAC9E,WAAO,QAAQ,UAAU,UAAU;;GAEpC;;CASH,MAAM,kBAAkB,WAAW,gBAAgB,EAAE;AACrD,MAAK,MAAM,OAAO,gBACjB,KAAI,CAAC,uBAAuB,IAAI,IAAI,CACnC,OAAM,IAAI,MACT,uBAAuB,IAAI,eAAe,SAAS,yBAC3B,CAAC,GAAG,uBAAuB,CAAC,KAAK,KAAK,GAC9D;CAQH,MAAM,eAAe,sBAAsB,gBAAgB;CAC3D,MAAM,eAAe,WAAW,gBAAgB,EAAE;AAKlD,KAAI,aAAa,SAAS,gBAAgB,IAAI,CAAC,aAAa,SAAS,eAAe,CACnF,cAAa,KAAK,eAAe;AAElC,KAAI,aAAa,SAAS,cAAc,IAAI,CAAC,aAAa,SAAS,aAAa,CAC/E,cAAa,KAAK,aAAa;AAEhC,KACC,aAAa,SAAS,+BAA+B,IACrD,CAAC,aAAa,SAAS,kBAAkB,CAEzC,cAAa,KAAK,kBAAkB;CAMrC,MAAM,aAAa,WAAW,WAAW,EAAE;CAC3C,MAAM,UAA+B,EAAE;AACvC,MAAK,MAAM,CAAC,MAAM,WAAW,OAAO,QAAQ,WAAW,CACtD,SAAQ,QAAQ;EACf,SAAS,OAAO,WAAW,EAAE;EAC7B,eAAe,OAAO;EACtB;CAQF,MAAM,QAA2B,EAAE;AACnC,KAAI,WAAW,WACd,OAAM,QAAQ,WAAW;AAE1B,KAAI,WAAW,aACd,OAAM,UAAU,WAAW;AAE5B,KAAI,WAAW,eACd,OAAM,iBAAiB,WAAW;AAEnC,KAAI,WAAW,mBACd,OAAM,qBAAqB,WAAW;AAEvC,KAAI,WAAW,aACd,OAAM,eAAe,WAAW;AAGjC,QAAO;EACN,IAAI;EACJ;EACA;EACA;EACA;EACA,OAAO;EACP,QAAQ;EACR,KAAK,EACJ,OAAO,OAAO,YACb,OAAO,QAAQ,WAAW,KAAK,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,UAAU,CACjE,MACA;GACC,aAAa,KAAK;GAClB,OAAO,KAAK;GACZ,OAAO,KAAK;GACZ,QAAQ,KAAK;GACb,aAAa,KAAK;GAClB,CACD,CAAC,CACF,EACD;EACD;EACA"}