@abraca/plugin 2.15.0 → 2.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/abracadabra-plugin.cjs.map +1 -1
- package/dist/abracadabra-plugin.esm.js.map +1 -1
- package/dist/index.d.ts +90 -56
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/source-spec.ts +56 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"abracadabra-plugin.cjs","names":["extractHost"],"sources":["../src/registry.ts","../src/source-spec.ts","../src/host.ts","../src/sandbox.ts"],"sourcesContent":["/**\n * `PluginRegistry` — the host-side manager for `AbraPlugin` instances.\n *\n * One registry per host (cou-shell, an `@abraca/nuxt` app, …). Generic over\n * the plugin type so apps that narrow `AbraPlugin` to their own interface\n * (e.g. `CouPlugin`, `AbracadabraPlugin`) get correctly-typed aggregator\n * methods without casts.\n *\n * Two tiers:\n *\n * - **Built-in plugins**: registered before `freeze()`. Frozen at boot;\n * `register()` after that point logs a warning and returns.\n * - **Space plugins**: registered/unregistered any time. Reactive consumers\n * poll `getSpacePluginVersion()` to detect changes.\n *\n * Aggregator methods return contributions from both tiers in registration\n * order, built-ins first.\n */\n\nimport type {\n\tAbraPlugin,\n\tAbraTiptapExtension,\n\tEditorPluginCtx,\n\tDragHandlePluginCtx,\n\tCommandPaletteCtx,\n\tAbraMentionProvider,\n\tAbraAwarenessContribution,\n\tAbraNodePanelSlot,\n\tAbraSettingsPanel,\n\tAbraKeyboardShortcut,\n\tAbraCommandItem,\n} from \"./types.ts\";\n\n// ── Type helpers for narrowed plugin generics ─────────────────────────────────\n\n/** Extract the value type from a `Record<string, V>`-shaped optional field. */\ntype RecordValueOf<T> = T extends Record<string, infer V> ? V : never;\n\n/** Extract the item type from a `(ctx) => readonly (readonly T[])[]` field. */\ntype GroupedItemOf<T> = T extends\n\t(...args: never[]) => readonly (infer Group)[]\n\t? Group extends readonly (infer Item)[]\n\t\t? Item\n\t\t: never\n\t: never;\n\n/** Extract the array element type from an optional array field. */\ntype ElementOf<T> = T extends readonly (infer V)[] ? V : never;\n\n/** Result-shape of `commandPaletteItems` collapsed to its item type. */\ntype CommandItemOf<T> = T extends\n\t(...args: never[]) => infer R\n\t? R extends Promise<readonly (infer Item)[]>\n\t\t? Item\n\t\t: R extends readonly (infer Item)[]\n\t\t\t? Item\n\t\t\t: never\n\t: never;\n\n// ── Aggregator return types — derived from the plugin generic ─────────────────\n\ntype PageTypeOf<P> = P extends { pageTypes?: infer R } ? RecordValueOf<R> : never;\ntype CustomHandlerOf<P> = P extends { customHandlers?: () => infer R } ? R : never;\ntype ToolbarItemOf<P> = P extends { toolbarItems?: infer F } ? GroupedItemOf<F> : never;\ntype BubbleItemOf<P> = P extends { bubbleMenuItems?: infer F } ? GroupedItemOf<F> : never;\ntype SuggestionItemOf<P> = P extends { suggestionItems?: infer F } ? GroupedItemOf<F> : never;\ntype DragHandleItemOf<P> = P extends { dragHandleItems?: infer F } ? GroupedItemOf<F> : never;\ntype MentionProviderOf<P> = P extends { mentionProviders?: infer A }\n\t? ElementOf<A>\n\t: never;\ntype AwarenessContributionOf<P> = P extends { awarenessContributions?: infer A }\n\t? ElementOf<A>\n\t: never;\ntype CommandPaletteItemOf<P> = P extends { commandPaletteItems?: infer F }\n\t? CommandItemOf<F>\n\t: never;\ntype NodePanelSlotOf<P> = P extends { nodePanelSlots?: infer A } ? ElementOf<A> : never;\ntype SettingsPanelOf<P> = P extends { settingsPanel?: infer V } ? NonNullable<V> : never;\ntype KeyboardShortcutOf<P> = P extends { keyboardShortcuts?: infer A } ? ElementOf<A> : never;\n\n// ── Registry implementation ───────────────────────────────────────────────────\n\nexport class PluginRegistry<P extends AbraPlugin = AbraPlugin> {\n\tprivate _builtins: P[] = [];\n\tprivate _spacePlugins: P[] = [];\n\tprivate _frozen = false;\n\tprivate _spaceVersion = 0;\n\n\t/**\n\t * Register a built-in plugin. No-ops with a warning after `freeze()`.\n\t * Use `registerSpacePlugin()` for plugins that arrive after boot.\n\t */\n\tregister(plugin: P): void {\n\t\tif (this._frozen) {\n\t\t\tconsole.warn(\n\t\t\t\t`[@abraca/plugin] Registry frozen — cannot register \"${plugin.name}\". Use registerSpacePlugin() for post-boot plugins.`,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tif (this._builtins.some((p) => p.name === plugin.name)) {\n\t\t\tconsole.warn(\n\t\t\t\t`[@abraca/plugin] Plugin \"${plugin.name}\" already registered`,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tthis._builtins.push(plugin);\n\t}\n\n\t/** Lock the registry. Called once after all built-in + external plugins are loaded. */\n\tfreeze(): void {\n\t\tthis._frozen = true;\n\t}\n\n\tisFrozen(): boolean {\n\t\treturn this._frozen;\n\t}\n\n\t/** Reactive registration channel for space-driven plugins (bypasses freeze). */\n\tregisterSpacePlugin(plugin: P): void {\n\t\tif (\n\t\t\tthis._spacePlugins.some((p) => p.name === plugin.name) ||\n\t\t\tthis._builtins.some((p) => p.name === plugin.name)\n\t\t) {\n\t\t\tconsole.warn(\n\t\t\t\t`[@abraca/plugin] Plugin \"${plugin.name}\" already registered`,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tthis._spacePlugins.push(plugin);\n\t\tthis._spaceVersion++;\n\t}\n\n\tunregisterSpacePlugin(name: string): void {\n\t\tconst idx = this._spacePlugins.findIndex((p) => p.name === name);\n\t\tif (idx === -1) return;\n\t\tthis._spacePlugins.splice(idx, 1);\n\t\tthis._spaceVersion++;\n\t}\n\n\tclearSpacePlugins(): void {\n\t\tif (this._spacePlugins.length === 0) return;\n\t\tthis._spacePlugins.length = 0;\n\t\tthis._spaceVersion++;\n\t}\n\n\t/** Monotonically increasing counter — bump when space plugins change. */\n\tgetSpacePluginVersion(): number {\n\t\treturn this._spaceVersion;\n\t}\n\n\tgetBuiltinPlugins(): readonly P[] {\n\t\treturn this._builtins;\n\t}\n\n\tgetSpacePlugins(): readonly P[] {\n\t\treturn this._spacePlugins;\n\t}\n\n\t/** All active plugins — built-ins first, then space plugins. */\n\tgetPlugins(): readonly P[] {\n\t\treturn [...this._builtins, ...this._spacePlugins];\n\t}\n\n\t// ── Aggregators ──────────────────────────────────────────────────────────\n\n\tgetAllExtensions(): AbraTiptapExtension[] {\n\t\treturn this.getPlugins().flatMap((p) => p.extensions?.() ?? []);\n\t}\n\n\tgetServerExtensions(): AbraTiptapExtension[] {\n\t\treturn this.getPlugins().flatMap((p) => p.serverExtensions?.() ?? []);\n\t}\n\n\t/** Resolves once every plugin with `extensionsReady` has settled. */\n\tasync waitForExtensions(): Promise<void> {\n\t\tawait Promise.all(\n\t\t\tthis.getPlugins().map((p) => p.extensionsReady ?? Promise.resolve()),\n\t\t);\n\t}\n\n\tgetAllPageTypes(): Record<string, PageTypeOf<P>> {\n\t\tconst merged: Record<string, PageTypeOf<P>> = {};\n\t\tfor (const p of this.getPlugins()) {\n\t\t\tif (p.pageTypes) {\n\t\t\t\tObject.assign(merged, p.pageTypes as Record<string, PageTypeOf<P>>);\n\t\t\t}\n\t\t}\n\t\treturn merged;\n\t}\n\n\tgetAllCustomHandlers(): CustomHandlerOf<P> {\n\t\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\t\tconst merged: Record<string, any> = {};\n\t\tfor (const p of this.getPlugins()) {\n\t\t\tif (p.customHandlers) Object.assign(merged, p.customHandlers());\n\t\t}\n\t\treturn merged as CustomHandlerOf<P>;\n\t}\n\n\tgetAllToolbarItems(ctx: EditorPluginCtx): ToolbarItemOf<P>[][] {\n\t\treturn this.getPlugins().flatMap(\n\t\t\t(p) =>\n\t\t\t\t(p.toolbarItems?.(ctx) ?? []) as readonly (readonly ToolbarItemOf<P>[])[],\n\t\t) as ToolbarItemOf<P>[][];\n\t}\n\n\tgetAllBubbleMenuItems(ctx: EditorPluginCtx): BubbleItemOf<P>[][] {\n\t\treturn this.getPlugins().flatMap(\n\t\t\t(p) =>\n\t\t\t\t(p.bubbleMenuItems?.(ctx) ?? []) as readonly (readonly BubbleItemOf<P>[])[],\n\t\t) as BubbleItemOf<P>[][];\n\t}\n\n\tgetAllSuggestionItems(ctx: EditorPluginCtx): SuggestionItemOf<P>[][] {\n\t\treturn this.getPlugins().flatMap(\n\t\t\t(p) =>\n\t\t\t\t(p.suggestionItems?.(ctx) ?? []) as readonly (readonly SuggestionItemOf<P>[])[],\n\t\t) as SuggestionItemOf<P>[][];\n\t}\n\n\tgetAllDragHandleItems(ctx: DragHandlePluginCtx): DragHandleItemOf<P>[][] {\n\t\treturn this.getPlugins().flatMap(\n\t\t\t(p) =>\n\t\t\t\t(p.dragHandleItems?.(ctx) ?? []) as readonly (readonly DragHandleItemOf<P>[])[],\n\t\t) as DragHandleItemOf<P>[][];\n\t}\n\n\tgetAllMentionProviders(): MentionProviderOf<P>[] {\n\t\treturn this.getPlugins().flatMap(\n\t\t\t(p) => (p.mentionProviders ?? []) as unknown as readonly MentionProviderOf<P>[],\n\t\t) as MentionProviderOf<P>[];\n\t}\n\n\tgetAllAwarenessContributions(): AwarenessContributionOf<P>[] {\n\t\treturn this.getPlugins().flatMap(\n\t\t\t(p) => (p.awarenessContributions ?? []) as unknown as readonly AwarenessContributionOf<P>[],\n\t\t) as AwarenessContributionOf<P>[];\n\t}\n\n\tasync getAllCommandPaletteItems(\n\t\tctx: CommandPaletteCtx,\n\t): Promise<CommandPaletteItemOf<P>[]> {\n\t\tconst results = await Promise.all(\n\t\t\tthis.getPlugins()\n\t\t\t\t.filter((p) => p.commandPaletteItems)\n\t\t\t\t.map((p) => Promise.resolve(p.commandPaletteItems!(ctx))),\n\t\t);\n\t\tconst flat = results.flat() as CommandPaletteItemOf<P>[];\n\t\treturn flat.filter((item) => {\n\t\t\tconst w = (item as AbraCommandItem).when;\n\t\t\treturn !w || w(ctx);\n\t\t});\n\t}\n\n\tgetAllNodePanelSlots(): NodePanelSlotOf<P>[] {\n\t\treturn this.getPlugins().flatMap(\n\t\t\t(p) => (p.nodePanelSlots ?? []) as unknown as readonly NodePanelSlotOf<P>[],\n\t\t) as NodePanelSlotOf<P>[];\n\t}\n\n\tgetSettingsPanels(): SettingsPanelOf<P>[] {\n\t\treturn this.getPlugins()\n\t\t\t.map((p) => p.settingsPanel)\n\t\t\t.filter((v): v is NonNullable<typeof v> => v !== undefined) as SettingsPanelOf<P>[];\n\t}\n\n\tgetAllKeyboardShortcuts(): KeyboardShortcutOf<P>[] {\n\t\treturn this.getPlugins().flatMap(\n\t\t\t(p) => (p.keyboardShortcuts ?? []) as unknown as readonly KeyboardShortcutOf<P>[],\n\t\t) as KeyboardShortcutOf<P>[];\n\t}\n}\n\n// ── Re-export aggregator types so consumer-side aliases can name them ─────────\n\nexport type {\n\tAbraPlugin,\n\tEditorPluginCtx,\n\tDragHandlePluginCtx,\n\tCommandPaletteCtx,\n\tAbraMentionProvider,\n\tAbraAwarenessContribution,\n\tAbraNodePanelSlot,\n\tAbraSettingsPanel,\n\tAbraKeyboardShortcut,\n\tAbraCommandItem,\n};\n","/**\n * Plugin source specification — the user-typed shorthand that resolves to a\n * loadable URL.\n *\n * Today the registry server (Phase C) hosts artifacts at canonical URLs.\n * Until then, both `cou-shell` and `@abraca/nuxt` accept three forms:\n *\n * - `npm:<package>[@version]` → jsDelivr npm CDN\n * - `github:<user>/<repo>[@ref]` → jsDelivr GitHub CDN\n * - any other string → returned as-is (must be `https://` or `http://localhost`)\n *\n * Once Phase C ships we add:\n *\n * - `abra:<plugin-id>[@version]` → the official registry\n *\n * The output URL points at the plugin's bundle entry. Convention is\n * `<root>/dist/plugin.js`.\n */\n\n/** A locally-installed external plugin record (persisted to host storage). */\nexport interface ExternalPluginEntry {\n\t/** Resolved fetch URL — the output of `normalizePluginUrl`. */\n\turl: string;\n\t/** Plugin `name` from the loaded bundle's manifest / default export. */\n\tname: string;\n\tlabel?: string;\n\tversion?: string;\n\tdescription?: string;\n\tenabled: boolean;\n\t/** Most recent load error, if any. Cleared on next successful load. */\n\terror?: string;\n\tinstalledAt: number;\n\t/**\n\t * Integrity hash from the plugin's manifest, if known. Verified on every\n\t * load — a silent change indicates supply-chain tampering and the host\n\t * should refuse to instantiate the plugin.\n\t */\n\tintegrity?: string;\n}\n\n/**\n * Resolve a user-typed plugin spec to a fetchable URL.\n *\n * - `npm:foo@1.2.3` → `https://cdn.jsdelivr.net/npm/foo@1.2.3/dist/plugin.js`\n * - `github:org/repo@main` → `https://cdn.jsdelivr.net/gh/org/repo@main/dist/plugin.js`\n * - anything else → returned trimmed, unchanged\n */\nexport function normalizePluginUrl(input: string): string {\n\tconst trimmed = input.trim();\n\tif (trimmed.startsWith(\"npm:\")) {\n\t\tconst pkg = trimmed.slice(4);\n\t\treturn `https://cdn.jsdelivr.net/npm/${pkg}/dist/plugin.js`;\n\t}\n\tif (trimmed.startsWith(\"github:\")) {\n\t\tconst repo = trimmed.slice(7);\n\t\treturn `https://cdn.jsdelivr.net/gh/${repo}/dist/plugin.js`;\n\t}\n\treturn trimmed;\n}\n\n/**\n * Reject any URL that isn't HTTPS or localhost over HTTP. Hosts should call\n * this before importing — `normalizePluginUrl` does not enforce the policy\n * (npm:/github: shorthand always produces HTTPS, so callers only need this\n * for raw third-party URLs).\n */\nexport function isSafePluginUrl(url: string): boolean {\n\ttry {\n\t\tconst u = new URL(url);\n\t\tif (u.protocol === \"https:\") return true;\n\t\tif (\n\t\t\tu.protocol === \"http:\" &&\n\t\t\t(u.hostname === \"localhost\" || u.hostname === \"127.0.0.1\")\n\t\t) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t} catch {\n\t\treturn false;\n\t}\n}\n","/**\n * `PluginHost` — capability-guarded wrapper around the browser globals\n * a plugin is allowed to call. Each guard consults the plugin's\n * manifest before forwarding to the underlying API. A plugin that\n * declared `network:api.example.com` can only `fetch` against\n * `api.example.com`; a plugin without `clipboard:write` cannot write\n * the clipboard; etc.\n *\n * Phase 1 contract: this is opt-in. Plugins that import `createPluginHost`\n * and use its members get the gate for free. Plugins that grab\n * `globalThis.fetch` directly bypass the gate — sandboxing (iframe / Web\n * Worker / Wasm) lands in Phase F.2 and closes that loophole.\n *\n * The `CapabilityDenied` error class lets hosts (and tests) distinguish\n * policy failures from other runtime errors.\n */\n\nimport type { PluginCapability, PluginManifest } from \"./manifest.ts\";\n\n// ── Errors ────────────────────────────────────────────────────────────────────\n\n/**\n * Thrown when a plugin tries to use an API it didn't declare in its\n * manifest. Distinct from runtime failures (network 500, file not found,\n * etc.) so the host can show the user \"the plugin tried to do X without\n * permission\" instead of a generic error.\n */\nexport class CapabilityDenied extends Error {\n\treadonly capability: PluginCapability | string;\n\treadonly pluginId: string;\n\n\tconstructor(pluginId: string, capability: PluginCapability | string, detail?: string) {\n\t\tsuper(\n\t\t\t`plugin '${pluginId}' is not authorised to use '${capability}'${detail ? ` (${detail})` : \"\"}`,\n\t\t);\n\t\tthis.name = \"CapabilityDenied\";\n\t\tthis.capability = capability;\n\t\tthis.pluginId = pluginId;\n\t}\n}\n\n// ── Host shape ────────────────────────────────────────────────────────────────\n\n/** Subset of the global `fetch` signature the host exposes. */\nexport type GuardedFetch = (\n\tinput: RequestInfo | URL,\n\tinit?: RequestInit,\n) => Promise<Response>;\n\n/** Subset of `navigator.clipboard` the host exposes. */\nexport interface GuardedClipboard {\n\treadText(): Promise<string>;\n\twriteText(text: string): Promise<void>;\n}\n\n/** Permission-aware desktop notification. */\nexport interface GuardedNotifications {\n\tshow(title: string, options?: NotificationOptions): Promise<void>;\n}\n\n/**\n * Everything a plugin is allowed to touch through the host contract.\n * Each plugin gets its own instance via `createPluginHost(manifest)`.\n */\nexport interface PluginHost {\n\treadonly pluginId: string;\n\treadonly capabilities: ReadonlySet<PluginCapability>;\n\tfetch: GuardedFetch;\n\tclipboard: GuardedClipboard;\n\tnotifications: GuardedNotifications;\n\t/** Test whether a capability is granted without invoking the wrapped API. */\n\tcan(capability: PluginCapability | string): boolean;\n}\n\n// ── Constructor ───────────────────────────────────────────────────────────────\n\nexport interface PluginHostOptions {\n\t/**\n\t * Override the underlying `fetch`. Tests inject a stub here; production\n\t * hosts leave this unset and use the global.\n\t */\n\tfetch?: typeof fetch;\n\t/** Override `navigator.clipboard`. */\n\tclipboard?: {\n\t\treadText?(): Promise<string>;\n\t\twriteText?(text: string): Promise<void>;\n\t};\n\t/**\n\t * Override the host's notification surface. The default uses the\n\t * browser's `Notification` API — fine for web hosts; native hosts\n\t * (Tauri, Electron) inject their own.\n\t */\n\tshowNotification?(title: string, options?: NotificationOptions): Promise<void>;\n}\n\n/**\n * Build a `PluginHost` from a manifest. The set of granted capabilities\n * is `manifest.capabilities.required ∪ optional`; runtime gates check\n * membership before forwarding to the underlying API.\n *\n * Network capabilities support host patterns:\n * - `network` — any host\n * - `network:api.foo.io` — exact match\n * - `network:*.foo.io` — wildcard subdomain match\n * - `network:foo.io,bar.io` — comma-separated list\n */\nexport function createPluginHost(\n\tmanifest: Pick<PluginManifest, \"id\" | \"capabilities\">,\n\toptions: PluginHostOptions = {},\n): PluginHost {\n\tconst granted = new Set<PluginCapability>([\n\t\t...(manifest.capabilities.required ?? []),\n\t\t...(manifest.capabilities.optional ?? []),\n\t]);\n\tconst pluginId = manifest.id;\n\n\tconst can = (cap: PluginCapability | string): boolean => granted.has(cap as PluginCapability);\n\n\tconst guardedFetch: GuardedFetch = async (input, init) => {\n\t\tconst url = typeof input === \"string\" ? input : input instanceof URL ? input.toString() : input.url;\n\t\tconst host = extractHost(url);\n\t\tif (!host) {\n\t\t\tthrow new CapabilityDenied(pluginId, \"network\", \"could not parse URL host\");\n\t\t}\n\t\tif (!matchesNetworkCapability(granted, host)) {\n\t\t\tthrow new CapabilityDenied(\n\t\t\t\tpluginId,\n\t\t\t\t`network:${host}`,\n\t\t\t\t\"declare it in capabilities.required or capabilities.optional\",\n\t\t\t);\n\t\t}\n\t\tconst real = options.fetch ?? globalThis.fetch;\n\t\tif (!real) throw new Error(\"fetch is not available in this runtime\");\n\t\treturn real(input, init);\n\t};\n\n\tconst guardedClipboard: GuardedClipboard = {\n\t\tasync readText() {\n\t\t\tif (!granted.has(\"clipboard:read\")) {\n\t\t\t\tthrow new CapabilityDenied(pluginId, \"clipboard:read\");\n\t\t\t}\n\t\t\tif (options.clipboard?.readText) return options.clipboard.readText();\n\t\t\tif (typeof navigator !== \"undefined\" && navigator.clipboard?.readText) {\n\t\t\t\treturn navigator.clipboard.readText();\n\t\t\t}\n\t\t\tthrow new Error(\"clipboard.readText is not available in this runtime\");\n\t\t},\n\t\tasync writeText(text: string) {\n\t\t\tif (!granted.has(\"clipboard:write\")) {\n\t\t\t\tthrow new CapabilityDenied(pluginId, \"clipboard:write\");\n\t\t\t}\n\t\t\tif (options.clipboard?.writeText) return options.clipboard.writeText(text);\n\t\t\tif (typeof navigator !== \"undefined\" && navigator.clipboard?.writeText) {\n\t\t\t\treturn navigator.clipboard.writeText(text);\n\t\t\t}\n\t\t\tthrow new Error(\"clipboard.writeText is not available in this runtime\");\n\t\t},\n\t};\n\n\tconst guardedNotifications: GuardedNotifications = {\n\t\tasync show(title: string, opts?: NotificationOptions) {\n\t\t\tif (!granted.has(\"notifications:show\")) {\n\t\t\t\tthrow new CapabilityDenied(pluginId, \"notifications:show\");\n\t\t\t}\n\t\t\tif (options.showNotification) return options.showNotification(title, opts);\n\t\t\tif (typeof Notification !== \"undefined\") {\n\t\t\t\tnew Notification(title, opts);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthrow new Error(\"notifications are not available in this runtime\");\n\t\t},\n\t};\n\n\treturn {\n\t\tpluginId,\n\t\tcapabilities: granted,\n\t\tfetch: guardedFetch,\n\t\tclipboard: guardedClipboard,\n\t\tnotifications: guardedNotifications,\n\t\tcan,\n\t};\n}\n\n// ── Helpers ──────────────────────────────────────────────────────────────────\n\nfunction extractHost(rawUrl: string): string | null {\n\ttry {\n\t\treturn new URL(rawUrl, \"http://placeholder\").host;\n\t}\n\tcatch {\n\t\treturn null;\n\t}\n}\n\n/**\n * Network capability matcher — `network`, `network:exact.host`,\n * `network:*.wildcard.host`, `network:a,b,c` comma-list. Exported so\n * the iframe sandbox can reuse the same rules without forking the\n * vocabulary.\n */\nexport function matchesNetworkCapability(granted: ReadonlySet<PluginCapability>, host: string): boolean {\n\tif (granted.has(\"network\")) return true;\n\tfor (const cap of granted) {\n\t\tif (!cap.startsWith(\"network:\")) continue;\n\t\tconst patterns = cap.slice(\"network:\".length).split(\",\");\n\t\tfor (const pattern of patterns) {\n\t\t\tif (matchesHostPattern(pattern.trim(), host)) return true;\n\t\t}\n\t}\n\treturn false;\n}\n\nfunction matchesHostPattern(pattern: string, host: string): boolean {\n\tif (!pattern) return false;\n\tif (pattern === host) return true;\n\tif (pattern.startsWith(\"*.\")) {\n\t\tconst suffix = pattern.slice(2);\n\t\treturn host === suffix || host.endsWith(`.${suffix}`);\n\t}\n\treturn false;\n}\n","/**\n * Iframe-based sandbox for external plugins.\n *\n * Plugins from outside the built-in set are loaded into a sandboxed\n * `<iframe sandbox=\"allow-scripts\">` — same origin restricted to the\n * iframe's null origin, no access to the parent's globals, no cookies,\n * no localStorage. The plugin module evaluates inside the iframe and\n * communicates back via `postMessage`.\n *\n * The host side of the bridge (this file) exposes the same surface as\n * `createPluginHost(manifest)` — fetch, clipboard, notifications —\n * gated by the same manifest capabilities. The difference: in Phase F\n * the contract was advisory (a plugin could grab `globalThis.fetch`);\n * here the iframe doesn't have access to those globals at all, so the\n * only path is through `postMessage`.\n *\n * Wire protocol (versioned via `v: 1` envelope):\n *\n * host → frame:\n * { v:1, kind:'init', pluginId, capabilities, artifactUrl }\n * { v:1, kind:'fetch.response', id, ok, status, body }\n * { v:1, kind:'clipboard.response', id, value? }\n * { v:1, kind:'notification.response', id }\n * { v:1, kind:'error', id, message }\n *\n * frame → host:\n * { v:1, kind:'ready' }\n * { v:1, kind:'fetch.request', id, url, init? }\n * { v:1, kind:'clipboard.read.request', id }\n * { v:1, kind:'clipboard.write.request', id, text }\n * { v:1, kind:'notification.request', id, title, options? }\n * { v:1, kind:'plugin.export', value } // the plugin's default export\n * { v:1, kind:'plugin.error', message }\n *\n * Phase F.2 ships the host side only. The frame-side runtime that\n * imports plugin bundles + speaks this protocol is `sandbox-runtime.ts`\n * (sibling file).\n */\n\nimport {\n\tCapabilityDenied,\n\tmatchesNetworkCapability,\n\ttype GuardedClipboard,\n\ttype GuardedFetch,\n\ttype GuardedNotifications,\n\ttype PluginHostOptions,\n} from \"./host.ts\";\nimport type { PluginCapability, PluginManifest } from \"./manifest.ts\";\n\n/** Public result of `loadSandboxedPlugin` — what the host actually keeps. */\nexport interface SandboxedPlugin {\n\t/** Stable id from the manifest. */\n\tpluginId: string;\n\t/** Capabilities granted to this sandbox. */\n\tcapabilities: ReadonlySet<PluginCapability>;\n\t/** The plugin's default export, copied across the postMessage boundary. */\n\texported: unknown;\n\t/** Tear down: removes the iframe + closes the bridge. */\n\tdispose(): void;\n}\n\nexport interface SandboxOptions extends PluginHostOptions {\n\t/**\n\t * Override the document the iframe attaches to. Tests inject a jsdom\n\t * document; production hosts use the default `document` global.\n\t */\n\tdoc?: Document;\n\t/**\n\t * Hard timeout (ms) on the plugin's `ready` handshake. Defaults to\n\t * 10 s. Plugins that don't post `ready` within the window get the\n\t * sandbox torn down + a load failure.\n\t */\n\treadyTimeoutMs?: number;\n}\n\ninterface InboundEnvelope {\n\tv: 1;\n\tkind: string;\n\tid?: string;\n\t[k: string]: unknown;\n}\n\n/**\n * Spin up a sandboxed iframe, load the plugin bundle inside it, wait\n * for the plugin to post `ready`, then return a handle. The host bridges\n * capability-gated APIs to the iframe via postMessage.\n */\nexport async function loadSandboxedPlugin(\n\tmanifest: Pick<PluginManifest, \"id\" | \"capabilities\">,\n\tartifactUrl: string,\n\toptions: SandboxOptions = {},\n): Promise<SandboxedPlugin> {\n\tconst doc = options.doc ?? (typeof document !== \"undefined\" ? document : null);\n\tif (!doc) {\n\t\tthrow new Error(\"loadSandboxedPlugin requires a Document — call from a browser context\");\n\t}\n\n\tconst granted = new Set<PluginCapability>([\n\t\t...(manifest.capabilities.required ?? []),\n\t\t...(manifest.capabilities.optional ?? []),\n\t]);\n\tconst pluginId = manifest.id;\n\n\tconst iframe = doc.createElement(\"iframe\");\n\t// `allow-scripts` lets the plugin run; we deliberately omit\n\t// `allow-same-origin` so the iframe has a null origin — it can't\n\t// reach our cookies, localStorage, IndexedDB, or service worker.\n\tiframe.setAttribute(\"sandbox\", \"allow-scripts\");\n\tiframe.setAttribute(\"aria-hidden\", \"true\");\n\tiframe.style.display = \"none\";\n\tiframe.style.width = \"0\";\n\tiframe.style.height = \"0\";\n\tiframe.style.border = \"0\";\n\n\t// The runtime that the plugin runs inside the frame. Imports the\n\t// plugin bundle by URL, listens for messages, and proxies the\n\t// gated APIs through postMessage.\n\tconst frameSrc = buildFrameSource(artifactUrl);\n\tconst blob = new Blob([frameSrc], { type: \"text/html\" });\n\tiframe.src = URL.createObjectURL(blob);\n\n\tdoc.body.appendChild(iframe);\n\n\tconst pendingFetch = new Map<string, (resp: Response) => void>();\n\tconst pendingFetchErr = new Map<string, (err: Error) => void>();\n\tconst pendingClip = new Map<string, (v: unknown) => void>();\n\tconst pendingClipErr = new Map<string, (err: Error) => void>();\n\tconst pendingNotif = new Map<string, () => void>();\n\tconst pendingNotifErr = new Map<string, (err: Error) => void>();\n\n\tlet resolveReady!: (value: unknown) => void;\n\tlet rejectReady!: (err: Error) => void;\n\tconst ready = new Promise<unknown>((res, rej) => {\n\t\tresolveReady = res;\n\t\trejectReady = rej;\n\t});\n\n\tconst post = (envelope: Record<string, unknown>) => {\n\t\tiframe.contentWindow?.postMessage({ v: 1, ...envelope }, \"*\");\n\t};\n\n\tconst messageHandler = async (e: MessageEvent) => {\n\t\tif (e.source !== iframe.contentWindow) return;\n\t\tconst msg = e.data as InboundEnvelope | null;\n\t\tif (!msg || msg.v !== 1) return;\n\n\t\tswitch (msg.kind) {\n\t\t\tcase \"ready\": {\n\t\t\t\tpost({ kind: \"init\", pluginId, capabilities: [...granted], artifactUrl });\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"plugin.export\": {\n\t\t\t\tresolveReady(msg.value);\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"plugin.error\": {\n\t\t\t\trejectReady(new Error(String(msg.message ?? \"plugin failed to load\")));\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"fetch.request\": {\n\t\t\t\tconst id = String(msg.id);\n\t\t\t\tconst url = String(msg.url);\n\t\t\t\tconst init = msg.init as RequestInit | undefined;\n\t\t\t\ttry {\n\t\t\t\t\tconst host = extractHost(url);\n\t\t\t\t\tif (!host || !matchesNetworkCapability(granted, host)) {\n\t\t\t\t\t\tthrow new CapabilityDenied(\n\t\t\t\t\t\t\tpluginId,\n\t\t\t\t\t\t\thost ? `network:${host}` : \"network\",\n\t\t\t\t\t\t\t\"declare in capabilities.required or capabilities.optional\",\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tconst real = options.fetch ?? globalThis.fetch;\n\t\t\t\t\tconst resp = await real(url, init);\n\t\t\t\t\tconst body = await resp.text();\n\t\t\t\t\tpost({\n\t\t\t\t\t\tkind: \"fetch.response\",\n\t\t\t\t\t\tid,\n\t\t\t\t\t\tok: resp.ok,\n\t\t\t\t\t\tstatus: resp.status,\n\t\t\t\t\t\tbody,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tcatch (err) {\n\t\t\t\t\tpost({ kind: \"error\", id, message: (err as Error).message });\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"clipboard.read.request\": {\n\t\t\t\tconst id = String(msg.id);\n\t\t\t\ttry {\n\t\t\t\t\tif (!granted.has(\"clipboard:read\")) {\n\t\t\t\t\t\tthrow new CapabilityDenied(pluginId, \"clipboard:read\");\n\t\t\t\t\t}\n\t\t\t\t\tconst value = options.clipboard?.readText\n\t\t\t\t\t\t? await options.clipboard.readText()\n\t\t\t\t\t\t: typeof navigator !== \"undefined\" && navigator.clipboard?.readText\n\t\t\t\t\t\t\t? await navigator.clipboard.readText()\n\t\t\t\t\t\t\t: \"\";\n\t\t\t\t\tpost({ kind: \"clipboard.response\", id, value });\n\t\t\t\t}\n\t\t\t\tcatch (err) {\n\t\t\t\t\tpost({ kind: \"error\", id, message: (err as Error).message });\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"clipboard.write.request\": {\n\t\t\t\tconst id = String(msg.id);\n\t\t\t\ttry {\n\t\t\t\t\tif (!granted.has(\"clipboard:write\")) {\n\t\t\t\t\t\tthrow new CapabilityDenied(pluginId, \"clipboard:write\");\n\t\t\t\t\t}\n\t\t\t\t\tconst text = String(msg.text ?? \"\");\n\t\t\t\t\tif (options.clipboard?.writeText) await options.clipboard.writeText(text);\n\t\t\t\t\telse if (typeof navigator !== \"undefined\" && navigator.clipboard?.writeText) {\n\t\t\t\t\t\tawait navigator.clipboard.writeText(text);\n\t\t\t\t\t}\n\t\t\t\t\tpost({ kind: \"clipboard.response\", id });\n\t\t\t\t}\n\t\t\t\tcatch (err) {\n\t\t\t\t\tpost({ kind: \"error\", id, message: (err as Error).message });\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"notification.request\": {\n\t\t\t\tconst id = String(msg.id);\n\t\t\t\ttry {\n\t\t\t\t\tif (!granted.has(\"notifications:show\")) {\n\t\t\t\t\t\tthrow new CapabilityDenied(pluginId, \"notifications:show\");\n\t\t\t\t\t}\n\t\t\t\t\tconst title = String(msg.title ?? \"\");\n\t\t\t\t\tconst opts = msg.options as NotificationOptions | undefined;\n\t\t\t\t\tif (options.showNotification) await options.showNotification(title, opts);\n\t\t\t\t\telse if (typeof Notification !== \"undefined\") new Notification(title, opts);\n\t\t\t\t\tpost({ kind: \"notification.response\", id });\n\t\t\t\t}\n\t\t\t\tcatch (err) {\n\t\t\t\t\tpost({ kind: \"error\", id, message: (err as Error).message });\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t};\n\n\t(typeof window !== \"undefined\" ? window : (doc.defaultView as Window))\n\t\t?.addEventListener(\"message\", messageHandler);\n\n\tconst timeoutMs = options.readyTimeoutMs ?? 10_000;\n\tconst timeoutHandle = setTimeout(\n\t\t() => rejectReady(new Error(`plugin '${pluginId}' did not become ready within ${timeoutMs}ms`)),\n\t\ttimeoutMs,\n\t);\n\n\tlet exported: unknown;\n\ttry {\n\t\texported = await ready;\n\t}\n\tcatch (err) {\n\t\t// Tear down on failure — never leak the iframe.\n\t\t(typeof window !== \"undefined\" ? window : (doc.defaultView as Window))\n\t\t\t?.removeEventListener(\"message\", messageHandler);\n\t\tiframe.remove();\n\t\tURL.revokeObjectURL(iframe.src);\n\t\tclearTimeout(timeoutHandle);\n\t\tthrow err;\n\t}\n\tclearTimeout(timeoutHandle);\n\n\t// Drain any in-flight requests so they don't hang refs to disposed iframe.\n\tconst dispose = () => {\n\t\tfor (const reject of pendingFetchErr.values()) reject(new Error(\"sandbox disposed\"));\n\t\tfor (const reject of pendingClipErr.values()) reject(new Error(\"sandbox disposed\"));\n\t\tfor (const reject of pendingNotifErr.values()) reject(new Error(\"sandbox disposed\"));\n\t\tpendingFetch.clear();\n\t\tpendingFetchErr.clear();\n\t\tpendingClip.clear();\n\t\tpendingClipErr.clear();\n\t\tpendingNotif.clear();\n\t\tpendingNotifErr.clear();\n\t\t(typeof window !== \"undefined\" ? window : (doc.defaultView as Window))\n\t\t\t?.removeEventListener(\"message\", messageHandler);\n\t\tiframe.remove();\n\t\tURL.revokeObjectURL(iframe.src);\n\t};\n\n\treturn {\n\t\tpluginId,\n\t\tcapabilities: granted,\n\t\texported,\n\t\tdispose,\n\t};\n}\n\n// ── Helpers (shared with host.ts but inlined here to avoid export churn) ─────\n\nfunction extractHost(rawUrl: string): string | null {\n\ttry {\n\t\treturn new URL(rawUrl, \"http://placeholder\").host;\n\t}\n\tcatch {\n\t\treturn null;\n\t}\n}\n\n// ── Frame runtime ────────────────────────────────────────────────────────────\n\n/**\n * HTML+JS that runs inside the iframe. Imports the plugin bundle by URL\n * (in the iframe's null origin — no cookies, no localStorage), proxies\n * fetch/clipboard/notifications back to the host via postMessage, and\n * posts the plugin's default export once it loads.\n *\n * Kept inline so the package has zero runtime files — every consumer\n * picks up the latest version on a single import.\n */\nfunction buildFrameSource(artifactUrl: string): string {\n\t// The frame's globals: a guarded `pluginHost` mirror, ready for\n\t// plugins to import. The plugin code reads `globalThis.pluginHost`.\n\treturn `<!doctype html>\n<html><head><meta charset=\"utf-8\"></head><body>\n<script type=\"module\">\nconst pending = new Map();\nlet nextId = 0;\nfunction rpc(kind, payload) {\n return new Promise((resolve, reject) => {\n const id = String(++nextId);\n pending.set(id, { resolve, reject });\n parent.postMessage({ v: 1, kind, id, ...payload }, '*');\n });\n}\nwindow.addEventListener('message', (e) => {\n const msg = e.data;\n if (!msg || msg.v !== 1) return;\n if (msg.kind === 'init') {\n globalThis.__abraPluginContext = {\n pluginId: msg.pluginId,\n capabilities: new Set(msg.capabilities),\n };\n return;\n }\n if (msg.id && pending.has(msg.id)) {\n const { resolve, reject } = pending.get(msg.id);\n pending.delete(msg.id);\n if (msg.kind === 'error') reject(new Error(msg.message));\n else resolve(msg);\n }\n});\nglobalThis.pluginHost = {\n async fetch(url, init) {\n const r = await rpc('fetch.request', { url: String(url), init });\n return new Response(r.body, { status: r.status });\n },\n clipboard: {\n readText() { return rpc('clipboard.read.request', {}).then(r => r.value); },\n writeText(text) { return rpc('clipboard.write.request', { text }).then(() => undefined); },\n },\n notifications: {\n show(title, options) { return rpc('notification.request', { title, options }).then(() => undefined); },\n },\n};\nparent.postMessage({ v: 1, kind: 'ready' }, '*');\ntry {\n const mod = await import(${JSON.stringify(artifactUrl)});\n const value = mod && (mod.default ?? mod);\n // Strip functions before posting — they don't survive postMessage.\n // We send the plain serialisable bits; functions stay in the iframe\n // and the host calls them via separate RPC channels (future work).\n const serialisable = JSON.parse(JSON.stringify(value, (_, v) =>\n typeof v === 'function' ? '[Function]' : v));\n parent.postMessage({ v: 1, kind: 'plugin.export', value: serialisable }, '*');\n} catch (e) {\n parent.postMessage({ v: 1, kind: 'plugin.error', message: String((e && e.message) || e) }, '*');\n}\n</script></body></html>`;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"abracadabra-plugin.cjs","names":["extractHost"],"sources":["../src/registry.ts","../src/source-spec.ts","../src/host.ts","../src/sandbox.ts"],"sourcesContent":["/**\n * `PluginRegistry` — the host-side manager for `AbraPlugin` instances.\n *\n * One registry per host (cou-shell, an `@abraca/nuxt` app, …). Generic over\n * the plugin type so apps that narrow `AbraPlugin` to their own interface\n * (e.g. `CouPlugin`, `AbracadabraPlugin`) get correctly-typed aggregator\n * methods without casts.\n *\n * Two tiers:\n *\n * - **Built-in plugins**: registered before `freeze()`. Frozen at boot;\n * `register()` after that point logs a warning and returns.\n * - **Space plugins**: registered/unregistered any time. Reactive consumers\n * poll `getSpacePluginVersion()` to detect changes.\n *\n * Aggregator methods return contributions from both tiers in registration\n * order, built-ins first.\n */\n\nimport type {\n\tAbraPlugin,\n\tAbraTiptapExtension,\n\tEditorPluginCtx,\n\tDragHandlePluginCtx,\n\tCommandPaletteCtx,\n\tAbraMentionProvider,\n\tAbraAwarenessContribution,\n\tAbraNodePanelSlot,\n\tAbraSettingsPanel,\n\tAbraKeyboardShortcut,\n\tAbraCommandItem,\n} from \"./types.ts\";\n\n// ── Type helpers for narrowed plugin generics ─────────────────────────────────\n\n/** Extract the value type from a `Record<string, V>`-shaped optional field. */\ntype RecordValueOf<T> = T extends Record<string, infer V> ? V : never;\n\n/** Extract the item type from a `(ctx) => readonly (readonly T[])[]` field. */\ntype GroupedItemOf<T> = T extends\n\t(...args: never[]) => readonly (infer Group)[]\n\t? Group extends readonly (infer Item)[]\n\t\t? Item\n\t\t: never\n\t: never;\n\n/** Extract the array element type from an optional array field. */\ntype ElementOf<T> = T extends readonly (infer V)[] ? V : never;\n\n/** Result-shape of `commandPaletteItems` collapsed to its item type. */\ntype CommandItemOf<T> = T extends\n\t(...args: never[]) => infer R\n\t? R extends Promise<readonly (infer Item)[]>\n\t\t? Item\n\t\t: R extends readonly (infer Item)[]\n\t\t\t? Item\n\t\t\t: never\n\t: never;\n\n// ── Aggregator return types — derived from the plugin generic ─────────────────\n\ntype PageTypeOf<P> = P extends { pageTypes?: infer R } ? RecordValueOf<R> : never;\ntype CustomHandlerOf<P> = P extends { customHandlers?: () => infer R } ? R : never;\ntype ToolbarItemOf<P> = P extends { toolbarItems?: infer F } ? GroupedItemOf<F> : never;\ntype BubbleItemOf<P> = P extends { bubbleMenuItems?: infer F } ? GroupedItemOf<F> : never;\ntype SuggestionItemOf<P> = P extends { suggestionItems?: infer F } ? GroupedItemOf<F> : never;\ntype DragHandleItemOf<P> = P extends { dragHandleItems?: infer F } ? GroupedItemOf<F> : never;\ntype MentionProviderOf<P> = P extends { mentionProviders?: infer A }\n\t? ElementOf<A>\n\t: never;\ntype AwarenessContributionOf<P> = P extends { awarenessContributions?: infer A }\n\t? ElementOf<A>\n\t: never;\ntype CommandPaletteItemOf<P> = P extends { commandPaletteItems?: infer F }\n\t? CommandItemOf<F>\n\t: never;\ntype NodePanelSlotOf<P> = P extends { nodePanelSlots?: infer A } ? ElementOf<A> : never;\ntype SettingsPanelOf<P> = P extends { settingsPanel?: infer V } ? NonNullable<V> : never;\ntype KeyboardShortcutOf<P> = P extends { keyboardShortcuts?: infer A } ? ElementOf<A> : never;\n\n// ── Registry implementation ───────────────────────────────────────────────────\n\nexport class PluginRegistry<P extends AbraPlugin = AbraPlugin> {\n\tprivate _builtins: P[] = [];\n\tprivate _spacePlugins: P[] = [];\n\tprivate _frozen = false;\n\tprivate _spaceVersion = 0;\n\n\t/**\n\t * Register a built-in plugin. No-ops with a warning after `freeze()`.\n\t * Use `registerSpacePlugin()` for plugins that arrive after boot.\n\t */\n\tregister(plugin: P): void {\n\t\tif (this._frozen) {\n\t\t\tconsole.warn(\n\t\t\t\t`[@abraca/plugin] Registry frozen — cannot register \"${plugin.name}\". Use registerSpacePlugin() for post-boot plugins.`,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tif (this._builtins.some((p) => p.name === plugin.name)) {\n\t\t\tconsole.warn(\n\t\t\t\t`[@abraca/plugin] Plugin \"${plugin.name}\" already registered`,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tthis._builtins.push(plugin);\n\t}\n\n\t/** Lock the registry. Called once after all built-in + external plugins are loaded. */\n\tfreeze(): void {\n\t\tthis._frozen = true;\n\t}\n\n\tisFrozen(): boolean {\n\t\treturn this._frozen;\n\t}\n\n\t/** Reactive registration channel for space-driven plugins (bypasses freeze). */\n\tregisterSpacePlugin(plugin: P): void {\n\t\tif (\n\t\t\tthis._spacePlugins.some((p) => p.name === plugin.name) ||\n\t\t\tthis._builtins.some((p) => p.name === plugin.name)\n\t\t) {\n\t\t\tconsole.warn(\n\t\t\t\t`[@abraca/plugin] Plugin \"${plugin.name}\" already registered`,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tthis._spacePlugins.push(plugin);\n\t\tthis._spaceVersion++;\n\t}\n\n\tunregisterSpacePlugin(name: string): void {\n\t\tconst idx = this._spacePlugins.findIndex((p) => p.name === name);\n\t\tif (idx === -1) return;\n\t\tthis._spacePlugins.splice(idx, 1);\n\t\tthis._spaceVersion++;\n\t}\n\n\tclearSpacePlugins(): void {\n\t\tif (this._spacePlugins.length === 0) return;\n\t\tthis._spacePlugins.length = 0;\n\t\tthis._spaceVersion++;\n\t}\n\n\t/** Monotonically increasing counter — bump when space plugins change. */\n\tgetSpacePluginVersion(): number {\n\t\treturn this._spaceVersion;\n\t}\n\n\tgetBuiltinPlugins(): readonly P[] {\n\t\treturn this._builtins;\n\t}\n\n\tgetSpacePlugins(): readonly P[] {\n\t\treturn this._spacePlugins;\n\t}\n\n\t/** All active plugins — built-ins first, then space plugins. */\n\tgetPlugins(): readonly P[] {\n\t\treturn [...this._builtins, ...this._spacePlugins];\n\t}\n\n\t// ── Aggregators ──────────────────────────────────────────────────────────\n\n\tgetAllExtensions(): AbraTiptapExtension[] {\n\t\treturn this.getPlugins().flatMap((p) => p.extensions?.() ?? []);\n\t}\n\n\tgetServerExtensions(): AbraTiptapExtension[] {\n\t\treturn this.getPlugins().flatMap((p) => p.serverExtensions?.() ?? []);\n\t}\n\n\t/** Resolves once every plugin with `extensionsReady` has settled. */\n\tasync waitForExtensions(): Promise<void> {\n\t\tawait Promise.all(\n\t\t\tthis.getPlugins().map((p) => p.extensionsReady ?? Promise.resolve()),\n\t\t);\n\t}\n\n\tgetAllPageTypes(): Record<string, PageTypeOf<P>> {\n\t\tconst merged: Record<string, PageTypeOf<P>> = {};\n\t\tfor (const p of this.getPlugins()) {\n\t\t\tif (p.pageTypes) {\n\t\t\t\tObject.assign(merged, p.pageTypes as Record<string, PageTypeOf<P>>);\n\t\t\t}\n\t\t}\n\t\treturn merged;\n\t}\n\n\tgetAllCustomHandlers(): CustomHandlerOf<P> {\n\t\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\t\tconst merged: Record<string, any> = {};\n\t\tfor (const p of this.getPlugins()) {\n\t\t\tif (p.customHandlers) Object.assign(merged, p.customHandlers());\n\t\t}\n\t\treturn merged as CustomHandlerOf<P>;\n\t}\n\n\tgetAllToolbarItems(ctx: EditorPluginCtx): ToolbarItemOf<P>[][] {\n\t\treturn this.getPlugins().flatMap(\n\t\t\t(p) =>\n\t\t\t\t(p.toolbarItems?.(ctx) ?? []) as readonly (readonly ToolbarItemOf<P>[])[],\n\t\t) as ToolbarItemOf<P>[][];\n\t}\n\n\tgetAllBubbleMenuItems(ctx: EditorPluginCtx): BubbleItemOf<P>[][] {\n\t\treturn this.getPlugins().flatMap(\n\t\t\t(p) =>\n\t\t\t\t(p.bubbleMenuItems?.(ctx) ?? []) as readonly (readonly BubbleItemOf<P>[])[],\n\t\t) as BubbleItemOf<P>[][];\n\t}\n\n\tgetAllSuggestionItems(ctx: EditorPluginCtx): SuggestionItemOf<P>[][] {\n\t\treturn this.getPlugins().flatMap(\n\t\t\t(p) =>\n\t\t\t\t(p.suggestionItems?.(ctx) ?? []) as readonly (readonly SuggestionItemOf<P>[])[],\n\t\t) as SuggestionItemOf<P>[][];\n\t}\n\n\tgetAllDragHandleItems(ctx: DragHandlePluginCtx): DragHandleItemOf<P>[][] {\n\t\treturn this.getPlugins().flatMap(\n\t\t\t(p) =>\n\t\t\t\t(p.dragHandleItems?.(ctx) ?? []) as readonly (readonly DragHandleItemOf<P>[])[],\n\t\t) as DragHandleItemOf<P>[][];\n\t}\n\n\tgetAllMentionProviders(): MentionProviderOf<P>[] {\n\t\treturn this.getPlugins().flatMap(\n\t\t\t(p) => (p.mentionProviders ?? []) as unknown as readonly MentionProviderOf<P>[],\n\t\t) as MentionProviderOf<P>[];\n\t}\n\n\tgetAllAwarenessContributions(): AwarenessContributionOf<P>[] {\n\t\treturn this.getPlugins().flatMap(\n\t\t\t(p) => (p.awarenessContributions ?? []) as unknown as readonly AwarenessContributionOf<P>[],\n\t\t) as AwarenessContributionOf<P>[];\n\t}\n\n\tasync getAllCommandPaletteItems(\n\t\tctx: CommandPaletteCtx,\n\t): Promise<CommandPaletteItemOf<P>[]> {\n\t\tconst results = await Promise.all(\n\t\t\tthis.getPlugins()\n\t\t\t\t.filter((p) => p.commandPaletteItems)\n\t\t\t\t.map((p) => Promise.resolve(p.commandPaletteItems!(ctx))),\n\t\t);\n\t\tconst flat = results.flat() as CommandPaletteItemOf<P>[];\n\t\treturn flat.filter((item) => {\n\t\t\tconst w = (item as AbraCommandItem).when;\n\t\t\treturn !w || w(ctx);\n\t\t});\n\t}\n\n\tgetAllNodePanelSlots(): NodePanelSlotOf<P>[] {\n\t\treturn this.getPlugins().flatMap(\n\t\t\t(p) => (p.nodePanelSlots ?? []) as unknown as readonly NodePanelSlotOf<P>[],\n\t\t) as NodePanelSlotOf<P>[];\n\t}\n\n\tgetSettingsPanels(): SettingsPanelOf<P>[] {\n\t\treturn this.getPlugins()\n\t\t\t.map((p) => p.settingsPanel)\n\t\t\t.filter((v): v is NonNullable<typeof v> => v !== undefined) as SettingsPanelOf<P>[];\n\t}\n\n\tgetAllKeyboardShortcuts(): KeyboardShortcutOf<P>[] {\n\t\treturn this.getPlugins().flatMap(\n\t\t\t(p) => (p.keyboardShortcuts ?? []) as unknown as readonly KeyboardShortcutOf<P>[],\n\t\t) as KeyboardShortcutOf<P>[];\n\t}\n}\n\n// ── Re-export aggregator types so consumer-side aliases can name them ─────────\n\nexport type {\n\tAbraPlugin,\n\tEditorPluginCtx,\n\tDragHandlePluginCtx,\n\tCommandPaletteCtx,\n\tAbraMentionProvider,\n\tAbraAwarenessContribution,\n\tAbraNodePanelSlot,\n\tAbraSettingsPanel,\n\tAbraKeyboardShortcut,\n\tAbraCommandItem,\n};\n","/**\n * Plugin source specification — the user-typed shorthand that resolves to a\n * loadable URL.\n *\n * Today the registry server (Phase C) hosts artifacts at canonical URLs.\n * Until then, both `cou-shell` and `@abraca/nuxt` accept three forms:\n *\n * - `npm:<package>[@version]` → jsDelivr npm CDN\n * - `github:<user>/<repo>[@ref]` → jsDelivr GitHub CDN\n * - any other string → returned as-is (must be `https://` or `http://localhost`)\n *\n * Once Phase C ships we add:\n *\n * - `abra:<plugin-id>[@version]` → the official registry\n *\n * The output URL points at the plugin's bundle entry. Convention is\n * `<root>/dist/plugin.js`.\n */\n\nimport type { PluginCapability } from \"./manifest.ts\";\n\n/**\n * How an entry got onto the user's machine. Drives the trust UI:\n * `registry` is silently trusted; `url` and `upload` require the explicit\n * untrusted-code dialog; `space` was declared by the active space and gets\n * auto-loaded only when the same id exists in the registry — otherwise it\n * routes through the same untrusted gate as `url`/`upload`.\n */\nexport type PluginOrigin = \"registry\" | \"url\" | \"upload\" | \"space\";\n\n/** A locally-installed external plugin record (persisted to host storage). */\nexport interface ExternalPluginEntry {\n\t/** Resolved fetch URL — the output of `normalizePluginUrl`. */\n\turl: string;\n\t/** Plugin `name` from the loaded bundle's manifest / default export. */\n\tname: string;\n\tlabel?: string;\n\tversion?: string;\n\tdescription?: string;\n\tenabled: boolean;\n\t/** Most recent load error, if any. Cleared on next successful load. */\n\terror?: string;\n\tinstalledAt: number;\n\t/**\n\t * Integrity hash from the plugin's manifest, if known. Verified on every\n\t * load — a silent change indicates supply-chain tampering and the host\n\t * should refuse to instantiate the plugin.\n\t */\n\tintegrity?: string;\n\n\t/**\n\t * How this entry was installed. Absent on records written before the\n\t * trust-origin field was introduced — treat as `\"url\"` (the only pre-\n\t * existing external-install path) when migrating.\n\t */\n\torigin?: PluginOrigin;\n\t/**\n\t * Registry id (`PluginManifest.id`), set on `origin: \"registry\"` and\n\t * for `\"space\"` entries the host has reconciled against the catalog.\n\t * Lets the catalog's auto-update + decline-memory keys be id-based\n\t * instead of url-based.\n\t */\n\tid?: string;\n\t/**\n\t * SHA-256 of the uploaded bundle bytes (hex). Set on `origin: \"upload\"`\n\t * only — registry entries use `integrity`, URL entries have no hash.\n\t * Used as the IndexedDB blob key for restoring the uploaded artifact\n\t * across reloads.\n\t */\n\tsha256?: string;\n\t/**\n\t * Wall-clock when the user accepted the untrusted-code warning for\n\t * this entry. Set on every `\"url\"` / `\"upload\"` install and on every\n\t * `\"space\"` install that fell back to the untrusted path. Absent on\n\t * `\"registry\"` installs (registry trust is implicit).\n\t *\n\t * Cleared together with `acknowledgedVersion` /\n\t * `acknowledgedCapabilities` whenever the dialog re-prompts.\n\t */\n\ttrustAcknowledgedAt?: number;\n\t/**\n\t * Version the user acknowledged when last accepting the warning. The\n\t * trust dialog re-prompts whenever the manifest version no longer\n\t * matches this — see the `\"Per id+version, AND when capabilities grow\"`\n\t * trust-scope decision in the plugin-browser plan.\n\t */\n\tacknowledgedVersion?: string;\n\t/**\n\t * Required-capability set the user acknowledged at install time. The\n\t * dialog re-prompts whenever the current manifest declares a required\n\t * capability not in this set, even within the same version (mirrors\n\t * `require_review_on_cap_growth` in the server policy).\n\t */\n\tacknowledgedCapabilities?: readonly PluginCapability[];\n}\n\n/**\n * Resolve a user-typed plugin spec to a fetchable URL.\n *\n * - `npm:foo@1.2.3` → `https://cdn.jsdelivr.net/npm/foo@1.2.3/dist/plugin.js`\n * - `github:org/repo@main` → `https://cdn.jsdelivr.net/gh/org/repo@main/dist/plugin.js`\n * - anything else → returned trimmed, unchanged\n */\nexport function normalizePluginUrl(input: string): string {\n\tconst trimmed = input.trim();\n\tif (trimmed.startsWith(\"npm:\")) {\n\t\tconst pkg = trimmed.slice(4);\n\t\treturn `https://cdn.jsdelivr.net/npm/${pkg}/dist/plugin.js`;\n\t}\n\tif (trimmed.startsWith(\"github:\")) {\n\t\tconst repo = trimmed.slice(7);\n\t\treturn `https://cdn.jsdelivr.net/gh/${repo}/dist/plugin.js`;\n\t}\n\treturn trimmed;\n}\n\n/**\n * Reject any URL that isn't HTTPS or localhost over HTTP. Hosts should call\n * this before importing — `normalizePluginUrl` does not enforce the policy\n * (npm:/github: shorthand always produces HTTPS, so callers only need this\n * for raw third-party URLs).\n */\nexport function isSafePluginUrl(url: string): boolean {\n\ttry {\n\t\tconst u = new URL(url);\n\t\tif (u.protocol === \"https:\") return true;\n\t\tif (\n\t\t\tu.protocol === \"http:\" &&\n\t\t\t(u.hostname === \"localhost\" || u.hostname === \"127.0.0.1\")\n\t\t) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t} catch {\n\t\treturn false;\n\t}\n}\n","/**\n * `PluginHost` — capability-guarded wrapper around the browser globals\n * a plugin is allowed to call. Each guard consults the plugin's\n * manifest before forwarding to the underlying API. A plugin that\n * declared `network:api.example.com` can only `fetch` against\n * `api.example.com`; a plugin without `clipboard:write` cannot write\n * the clipboard; etc.\n *\n * Phase 1 contract: this is opt-in. Plugins that import `createPluginHost`\n * and use its members get the gate for free. Plugins that grab\n * `globalThis.fetch` directly bypass the gate — sandboxing (iframe / Web\n * Worker / Wasm) lands in Phase F.2 and closes that loophole.\n *\n * The `CapabilityDenied` error class lets hosts (and tests) distinguish\n * policy failures from other runtime errors.\n */\n\nimport type { PluginCapability, PluginManifest } from \"./manifest.ts\";\n\n// ── Errors ────────────────────────────────────────────────────────────────────\n\n/**\n * Thrown when a plugin tries to use an API it didn't declare in its\n * manifest. Distinct from runtime failures (network 500, file not found,\n * etc.) so the host can show the user \"the plugin tried to do X without\n * permission\" instead of a generic error.\n */\nexport class CapabilityDenied extends Error {\n\treadonly capability: PluginCapability | string;\n\treadonly pluginId: string;\n\n\tconstructor(pluginId: string, capability: PluginCapability | string, detail?: string) {\n\t\tsuper(\n\t\t\t`plugin '${pluginId}' is not authorised to use '${capability}'${detail ? ` (${detail})` : \"\"}`,\n\t\t);\n\t\tthis.name = \"CapabilityDenied\";\n\t\tthis.capability = capability;\n\t\tthis.pluginId = pluginId;\n\t}\n}\n\n// ── Host shape ────────────────────────────────────────────────────────────────\n\n/** Subset of the global `fetch` signature the host exposes. */\nexport type GuardedFetch = (\n\tinput: RequestInfo | URL,\n\tinit?: RequestInit,\n) => Promise<Response>;\n\n/** Subset of `navigator.clipboard` the host exposes. */\nexport interface GuardedClipboard {\n\treadText(): Promise<string>;\n\twriteText(text: string): Promise<void>;\n}\n\n/** Permission-aware desktop notification. */\nexport interface GuardedNotifications {\n\tshow(title: string, options?: NotificationOptions): Promise<void>;\n}\n\n/**\n * Everything a plugin is allowed to touch through the host contract.\n * Each plugin gets its own instance via `createPluginHost(manifest)`.\n */\nexport interface PluginHost {\n\treadonly pluginId: string;\n\treadonly capabilities: ReadonlySet<PluginCapability>;\n\tfetch: GuardedFetch;\n\tclipboard: GuardedClipboard;\n\tnotifications: GuardedNotifications;\n\t/** Test whether a capability is granted without invoking the wrapped API. */\n\tcan(capability: PluginCapability | string): boolean;\n}\n\n// ── Constructor ───────────────────────────────────────────────────────────────\n\nexport interface PluginHostOptions {\n\t/**\n\t * Override the underlying `fetch`. Tests inject a stub here; production\n\t * hosts leave this unset and use the global.\n\t */\n\tfetch?: typeof fetch;\n\t/** Override `navigator.clipboard`. */\n\tclipboard?: {\n\t\treadText?(): Promise<string>;\n\t\twriteText?(text: string): Promise<void>;\n\t};\n\t/**\n\t * Override the host's notification surface. The default uses the\n\t * browser's `Notification` API — fine for web hosts; native hosts\n\t * (Tauri, Electron) inject their own.\n\t */\n\tshowNotification?(title: string, options?: NotificationOptions): Promise<void>;\n}\n\n/**\n * Build a `PluginHost` from a manifest. The set of granted capabilities\n * is `manifest.capabilities.required ∪ optional`; runtime gates check\n * membership before forwarding to the underlying API.\n *\n * Network capabilities support host patterns:\n * - `network` — any host\n * - `network:api.foo.io` — exact match\n * - `network:*.foo.io` — wildcard subdomain match\n * - `network:foo.io,bar.io` — comma-separated list\n */\nexport function createPluginHost(\n\tmanifest: Pick<PluginManifest, \"id\" | \"capabilities\">,\n\toptions: PluginHostOptions = {},\n): PluginHost {\n\tconst granted = new Set<PluginCapability>([\n\t\t...(manifest.capabilities.required ?? []),\n\t\t...(manifest.capabilities.optional ?? []),\n\t]);\n\tconst pluginId = manifest.id;\n\n\tconst can = (cap: PluginCapability | string): boolean => granted.has(cap as PluginCapability);\n\n\tconst guardedFetch: GuardedFetch = async (input, init) => {\n\t\tconst url = typeof input === \"string\" ? input : input instanceof URL ? input.toString() : input.url;\n\t\tconst host = extractHost(url);\n\t\tif (!host) {\n\t\t\tthrow new CapabilityDenied(pluginId, \"network\", \"could not parse URL host\");\n\t\t}\n\t\tif (!matchesNetworkCapability(granted, host)) {\n\t\t\tthrow new CapabilityDenied(\n\t\t\t\tpluginId,\n\t\t\t\t`network:${host}`,\n\t\t\t\t\"declare it in capabilities.required or capabilities.optional\",\n\t\t\t);\n\t\t}\n\t\tconst real = options.fetch ?? globalThis.fetch;\n\t\tif (!real) throw new Error(\"fetch is not available in this runtime\");\n\t\treturn real(input, init);\n\t};\n\n\tconst guardedClipboard: GuardedClipboard = {\n\t\tasync readText() {\n\t\t\tif (!granted.has(\"clipboard:read\")) {\n\t\t\t\tthrow new CapabilityDenied(pluginId, \"clipboard:read\");\n\t\t\t}\n\t\t\tif (options.clipboard?.readText) return options.clipboard.readText();\n\t\t\tif (typeof navigator !== \"undefined\" && navigator.clipboard?.readText) {\n\t\t\t\treturn navigator.clipboard.readText();\n\t\t\t}\n\t\t\tthrow new Error(\"clipboard.readText is not available in this runtime\");\n\t\t},\n\t\tasync writeText(text: string) {\n\t\t\tif (!granted.has(\"clipboard:write\")) {\n\t\t\t\tthrow new CapabilityDenied(pluginId, \"clipboard:write\");\n\t\t\t}\n\t\t\tif (options.clipboard?.writeText) return options.clipboard.writeText(text);\n\t\t\tif (typeof navigator !== \"undefined\" && navigator.clipboard?.writeText) {\n\t\t\t\treturn navigator.clipboard.writeText(text);\n\t\t\t}\n\t\t\tthrow new Error(\"clipboard.writeText is not available in this runtime\");\n\t\t},\n\t};\n\n\tconst guardedNotifications: GuardedNotifications = {\n\t\tasync show(title: string, opts?: NotificationOptions) {\n\t\t\tif (!granted.has(\"notifications:show\")) {\n\t\t\t\tthrow new CapabilityDenied(pluginId, \"notifications:show\");\n\t\t\t}\n\t\t\tif (options.showNotification) return options.showNotification(title, opts);\n\t\t\tif (typeof Notification !== \"undefined\") {\n\t\t\t\tnew Notification(title, opts);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthrow new Error(\"notifications are not available in this runtime\");\n\t\t},\n\t};\n\n\treturn {\n\t\tpluginId,\n\t\tcapabilities: granted,\n\t\tfetch: guardedFetch,\n\t\tclipboard: guardedClipboard,\n\t\tnotifications: guardedNotifications,\n\t\tcan,\n\t};\n}\n\n// ── Helpers ──────────────────────────────────────────────────────────────────\n\nfunction extractHost(rawUrl: string): string | null {\n\ttry {\n\t\treturn new URL(rawUrl, \"http://placeholder\").host;\n\t}\n\tcatch {\n\t\treturn null;\n\t}\n}\n\n/**\n * Network capability matcher — `network`, `network:exact.host`,\n * `network:*.wildcard.host`, `network:a,b,c` comma-list. Exported so\n * the iframe sandbox can reuse the same rules without forking the\n * vocabulary.\n */\nexport function matchesNetworkCapability(granted: ReadonlySet<PluginCapability>, host: string): boolean {\n\tif (granted.has(\"network\")) return true;\n\tfor (const cap of granted) {\n\t\tif (!cap.startsWith(\"network:\")) continue;\n\t\tconst patterns = cap.slice(\"network:\".length).split(\",\");\n\t\tfor (const pattern of patterns) {\n\t\t\tif (matchesHostPattern(pattern.trim(), host)) return true;\n\t\t}\n\t}\n\treturn false;\n}\n\nfunction matchesHostPattern(pattern: string, host: string): boolean {\n\tif (!pattern) return false;\n\tif (pattern === host) return true;\n\tif (pattern.startsWith(\"*.\")) {\n\t\tconst suffix = pattern.slice(2);\n\t\treturn host === suffix || host.endsWith(`.${suffix}`);\n\t}\n\treturn false;\n}\n","/**\n * Iframe-based sandbox for external plugins.\n *\n * Plugins from outside the built-in set are loaded into a sandboxed\n * `<iframe sandbox=\"allow-scripts\">` — same origin restricted to the\n * iframe's null origin, no access to the parent's globals, no cookies,\n * no localStorage. The plugin module evaluates inside the iframe and\n * communicates back via `postMessage`.\n *\n * The host side of the bridge (this file) exposes the same surface as\n * `createPluginHost(manifest)` — fetch, clipboard, notifications —\n * gated by the same manifest capabilities. The difference: in Phase F\n * the contract was advisory (a plugin could grab `globalThis.fetch`);\n * here the iframe doesn't have access to those globals at all, so the\n * only path is through `postMessage`.\n *\n * Wire protocol (versioned via `v: 1` envelope):\n *\n * host → frame:\n * { v:1, kind:'init', pluginId, capabilities, artifactUrl }\n * { v:1, kind:'fetch.response', id, ok, status, body }\n * { v:1, kind:'clipboard.response', id, value? }\n * { v:1, kind:'notification.response', id }\n * { v:1, kind:'error', id, message }\n *\n * frame → host:\n * { v:1, kind:'ready' }\n * { v:1, kind:'fetch.request', id, url, init? }\n * { v:1, kind:'clipboard.read.request', id }\n * { v:1, kind:'clipboard.write.request', id, text }\n * { v:1, kind:'notification.request', id, title, options? }\n * { v:1, kind:'plugin.export', value } // the plugin's default export\n * { v:1, kind:'plugin.error', message }\n *\n * Phase F.2 ships the host side only. The frame-side runtime that\n * imports plugin bundles + speaks this protocol is `sandbox-runtime.ts`\n * (sibling file).\n */\n\nimport {\n\tCapabilityDenied,\n\tmatchesNetworkCapability,\n\ttype GuardedClipboard,\n\ttype GuardedFetch,\n\ttype GuardedNotifications,\n\ttype PluginHostOptions,\n} from \"./host.ts\";\nimport type { PluginCapability, PluginManifest } from \"./manifest.ts\";\n\n/** Public result of `loadSandboxedPlugin` — what the host actually keeps. */\nexport interface SandboxedPlugin {\n\t/** Stable id from the manifest. */\n\tpluginId: string;\n\t/** Capabilities granted to this sandbox. */\n\tcapabilities: ReadonlySet<PluginCapability>;\n\t/** The plugin's default export, copied across the postMessage boundary. */\n\texported: unknown;\n\t/** Tear down: removes the iframe + closes the bridge. */\n\tdispose(): void;\n}\n\nexport interface SandboxOptions extends PluginHostOptions {\n\t/**\n\t * Override the document the iframe attaches to. Tests inject a jsdom\n\t * document; production hosts use the default `document` global.\n\t */\n\tdoc?: Document;\n\t/**\n\t * Hard timeout (ms) on the plugin's `ready` handshake. Defaults to\n\t * 10 s. Plugins that don't post `ready` within the window get the\n\t * sandbox torn down + a load failure.\n\t */\n\treadyTimeoutMs?: number;\n}\n\ninterface InboundEnvelope {\n\tv: 1;\n\tkind: string;\n\tid?: string;\n\t[k: string]: unknown;\n}\n\n/**\n * Spin up a sandboxed iframe, load the plugin bundle inside it, wait\n * for the plugin to post `ready`, then return a handle. The host bridges\n * capability-gated APIs to the iframe via postMessage.\n */\nexport async function loadSandboxedPlugin(\n\tmanifest: Pick<PluginManifest, \"id\" | \"capabilities\">,\n\tartifactUrl: string,\n\toptions: SandboxOptions = {},\n): Promise<SandboxedPlugin> {\n\tconst doc = options.doc ?? (typeof document !== \"undefined\" ? document : null);\n\tif (!doc) {\n\t\tthrow new Error(\"loadSandboxedPlugin requires a Document — call from a browser context\");\n\t}\n\n\tconst granted = new Set<PluginCapability>([\n\t\t...(manifest.capabilities.required ?? []),\n\t\t...(manifest.capabilities.optional ?? []),\n\t]);\n\tconst pluginId = manifest.id;\n\n\tconst iframe = doc.createElement(\"iframe\");\n\t// `allow-scripts` lets the plugin run; we deliberately omit\n\t// `allow-same-origin` so the iframe has a null origin — it can't\n\t// reach our cookies, localStorage, IndexedDB, or service worker.\n\tiframe.setAttribute(\"sandbox\", \"allow-scripts\");\n\tiframe.setAttribute(\"aria-hidden\", \"true\");\n\tiframe.style.display = \"none\";\n\tiframe.style.width = \"0\";\n\tiframe.style.height = \"0\";\n\tiframe.style.border = \"0\";\n\n\t// The runtime that the plugin runs inside the frame. Imports the\n\t// plugin bundle by URL, listens for messages, and proxies the\n\t// gated APIs through postMessage.\n\tconst frameSrc = buildFrameSource(artifactUrl);\n\tconst blob = new Blob([frameSrc], { type: \"text/html\" });\n\tiframe.src = URL.createObjectURL(blob);\n\n\tdoc.body.appendChild(iframe);\n\n\tconst pendingFetch = new Map<string, (resp: Response) => void>();\n\tconst pendingFetchErr = new Map<string, (err: Error) => void>();\n\tconst pendingClip = new Map<string, (v: unknown) => void>();\n\tconst pendingClipErr = new Map<string, (err: Error) => void>();\n\tconst pendingNotif = new Map<string, () => void>();\n\tconst pendingNotifErr = new Map<string, (err: Error) => void>();\n\n\tlet resolveReady!: (value: unknown) => void;\n\tlet rejectReady!: (err: Error) => void;\n\tconst ready = new Promise<unknown>((res, rej) => {\n\t\tresolveReady = res;\n\t\trejectReady = rej;\n\t});\n\n\tconst post = (envelope: Record<string, unknown>) => {\n\t\tiframe.contentWindow?.postMessage({ v: 1, ...envelope }, \"*\");\n\t};\n\n\tconst messageHandler = async (e: MessageEvent) => {\n\t\tif (e.source !== iframe.contentWindow) return;\n\t\tconst msg = e.data as InboundEnvelope | null;\n\t\tif (!msg || msg.v !== 1) return;\n\n\t\tswitch (msg.kind) {\n\t\t\tcase \"ready\": {\n\t\t\t\tpost({ kind: \"init\", pluginId, capabilities: [...granted], artifactUrl });\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"plugin.export\": {\n\t\t\t\tresolveReady(msg.value);\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"plugin.error\": {\n\t\t\t\trejectReady(new Error(String(msg.message ?? \"plugin failed to load\")));\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"fetch.request\": {\n\t\t\t\tconst id = String(msg.id);\n\t\t\t\tconst url = String(msg.url);\n\t\t\t\tconst init = msg.init as RequestInit | undefined;\n\t\t\t\ttry {\n\t\t\t\t\tconst host = extractHost(url);\n\t\t\t\t\tif (!host || !matchesNetworkCapability(granted, host)) {\n\t\t\t\t\t\tthrow new CapabilityDenied(\n\t\t\t\t\t\t\tpluginId,\n\t\t\t\t\t\t\thost ? `network:${host}` : \"network\",\n\t\t\t\t\t\t\t\"declare in capabilities.required or capabilities.optional\",\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tconst real = options.fetch ?? globalThis.fetch;\n\t\t\t\t\tconst resp = await real(url, init);\n\t\t\t\t\tconst body = await resp.text();\n\t\t\t\t\tpost({\n\t\t\t\t\t\tkind: \"fetch.response\",\n\t\t\t\t\t\tid,\n\t\t\t\t\t\tok: resp.ok,\n\t\t\t\t\t\tstatus: resp.status,\n\t\t\t\t\t\tbody,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tcatch (err) {\n\t\t\t\t\tpost({ kind: \"error\", id, message: (err as Error).message });\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"clipboard.read.request\": {\n\t\t\t\tconst id = String(msg.id);\n\t\t\t\ttry {\n\t\t\t\t\tif (!granted.has(\"clipboard:read\")) {\n\t\t\t\t\t\tthrow new CapabilityDenied(pluginId, \"clipboard:read\");\n\t\t\t\t\t}\n\t\t\t\t\tconst value = options.clipboard?.readText\n\t\t\t\t\t\t? await options.clipboard.readText()\n\t\t\t\t\t\t: typeof navigator !== \"undefined\" && navigator.clipboard?.readText\n\t\t\t\t\t\t\t? await navigator.clipboard.readText()\n\t\t\t\t\t\t\t: \"\";\n\t\t\t\t\tpost({ kind: \"clipboard.response\", id, value });\n\t\t\t\t}\n\t\t\t\tcatch (err) {\n\t\t\t\t\tpost({ kind: \"error\", id, message: (err as Error).message });\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"clipboard.write.request\": {\n\t\t\t\tconst id = String(msg.id);\n\t\t\t\ttry {\n\t\t\t\t\tif (!granted.has(\"clipboard:write\")) {\n\t\t\t\t\t\tthrow new CapabilityDenied(pluginId, \"clipboard:write\");\n\t\t\t\t\t}\n\t\t\t\t\tconst text = String(msg.text ?? \"\");\n\t\t\t\t\tif (options.clipboard?.writeText) await options.clipboard.writeText(text);\n\t\t\t\t\telse if (typeof navigator !== \"undefined\" && navigator.clipboard?.writeText) {\n\t\t\t\t\t\tawait navigator.clipboard.writeText(text);\n\t\t\t\t\t}\n\t\t\t\t\tpost({ kind: \"clipboard.response\", id });\n\t\t\t\t}\n\t\t\t\tcatch (err) {\n\t\t\t\t\tpost({ kind: \"error\", id, message: (err as Error).message });\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"notification.request\": {\n\t\t\t\tconst id = String(msg.id);\n\t\t\t\ttry {\n\t\t\t\t\tif (!granted.has(\"notifications:show\")) {\n\t\t\t\t\t\tthrow new CapabilityDenied(pluginId, \"notifications:show\");\n\t\t\t\t\t}\n\t\t\t\t\tconst title = String(msg.title ?? \"\");\n\t\t\t\t\tconst opts = msg.options as NotificationOptions | undefined;\n\t\t\t\t\tif (options.showNotification) await options.showNotification(title, opts);\n\t\t\t\t\telse if (typeof Notification !== \"undefined\") new Notification(title, opts);\n\t\t\t\t\tpost({ kind: \"notification.response\", id });\n\t\t\t\t}\n\t\t\t\tcatch (err) {\n\t\t\t\t\tpost({ kind: \"error\", id, message: (err as Error).message });\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t};\n\n\t(typeof window !== \"undefined\" ? window : (doc.defaultView as Window))\n\t\t?.addEventListener(\"message\", messageHandler);\n\n\tconst timeoutMs = options.readyTimeoutMs ?? 10_000;\n\tconst timeoutHandle = setTimeout(\n\t\t() => rejectReady(new Error(`plugin '${pluginId}' did not become ready within ${timeoutMs}ms`)),\n\t\ttimeoutMs,\n\t);\n\n\tlet exported: unknown;\n\ttry {\n\t\texported = await ready;\n\t}\n\tcatch (err) {\n\t\t// Tear down on failure — never leak the iframe.\n\t\t(typeof window !== \"undefined\" ? window : (doc.defaultView as Window))\n\t\t\t?.removeEventListener(\"message\", messageHandler);\n\t\tiframe.remove();\n\t\tURL.revokeObjectURL(iframe.src);\n\t\tclearTimeout(timeoutHandle);\n\t\tthrow err;\n\t}\n\tclearTimeout(timeoutHandle);\n\n\t// Drain any in-flight requests so they don't hang refs to disposed iframe.\n\tconst dispose = () => {\n\t\tfor (const reject of pendingFetchErr.values()) reject(new Error(\"sandbox disposed\"));\n\t\tfor (const reject of pendingClipErr.values()) reject(new Error(\"sandbox disposed\"));\n\t\tfor (const reject of pendingNotifErr.values()) reject(new Error(\"sandbox disposed\"));\n\t\tpendingFetch.clear();\n\t\tpendingFetchErr.clear();\n\t\tpendingClip.clear();\n\t\tpendingClipErr.clear();\n\t\tpendingNotif.clear();\n\t\tpendingNotifErr.clear();\n\t\t(typeof window !== \"undefined\" ? window : (doc.defaultView as Window))\n\t\t\t?.removeEventListener(\"message\", messageHandler);\n\t\tiframe.remove();\n\t\tURL.revokeObjectURL(iframe.src);\n\t};\n\n\treturn {\n\t\tpluginId,\n\t\tcapabilities: granted,\n\t\texported,\n\t\tdispose,\n\t};\n}\n\n// ── Helpers (shared with host.ts but inlined here to avoid export churn) ─────\n\nfunction extractHost(rawUrl: string): string | null {\n\ttry {\n\t\treturn new URL(rawUrl, \"http://placeholder\").host;\n\t}\n\tcatch {\n\t\treturn null;\n\t}\n}\n\n// ── Frame runtime ────────────────────────────────────────────────────────────\n\n/**\n * HTML+JS that runs inside the iframe. Imports the plugin bundle by URL\n * (in the iframe's null origin — no cookies, no localStorage), proxies\n * fetch/clipboard/notifications back to the host via postMessage, and\n * posts the plugin's default export once it loads.\n *\n * Kept inline so the package has zero runtime files — every consumer\n * picks up the latest version on a single import.\n */\nfunction buildFrameSource(artifactUrl: string): string {\n\t// The frame's globals: a guarded `pluginHost` mirror, ready for\n\t// plugins to import. The plugin code reads `globalThis.pluginHost`.\n\treturn `<!doctype html>\n<html><head><meta charset=\"utf-8\"></head><body>\n<script type=\"module\">\nconst pending = new Map();\nlet nextId = 0;\nfunction rpc(kind, payload) {\n return new Promise((resolve, reject) => {\n const id = String(++nextId);\n pending.set(id, { resolve, reject });\n parent.postMessage({ v: 1, kind, id, ...payload }, '*');\n });\n}\nwindow.addEventListener('message', (e) => {\n const msg = e.data;\n if (!msg || msg.v !== 1) return;\n if (msg.kind === 'init') {\n globalThis.__abraPluginContext = {\n pluginId: msg.pluginId,\n capabilities: new Set(msg.capabilities),\n };\n return;\n }\n if (msg.id && pending.has(msg.id)) {\n const { resolve, reject } = pending.get(msg.id);\n pending.delete(msg.id);\n if (msg.kind === 'error') reject(new Error(msg.message));\n else resolve(msg);\n }\n});\nglobalThis.pluginHost = {\n async fetch(url, init) {\n const r = await rpc('fetch.request', { url: String(url), init });\n return new Response(r.body, { status: r.status });\n },\n clipboard: {\n readText() { return rpc('clipboard.read.request', {}).then(r => r.value); },\n writeText(text) { return rpc('clipboard.write.request', { text }).then(() => undefined); },\n },\n notifications: {\n show(title, options) { return rpc('notification.request', { title, options }).then(() => undefined); },\n },\n};\nparent.postMessage({ v: 1, kind: 'ready' }, '*');\ntry {\n const mod = await import(${JSON.stringify(artifactUrl)});\n const value = mod && (mod.default ?? mod);\n // Strip functions before posting — they don't survive postMessage.\n // We send the plain serialisable bits; functions stay in the iframe\n // and the host calls them via separate RPC channels (future work).\n const serialisable = JSON.parse(JSON.stringify(value, (_, v) =>\n typeof v === 'function' ? '[Function]' : v));\n parent.postMessage({ v: 1, kind: 'plugin.export', value: serialisable }, '*');\n} catch (e) {\n parent.postMessage({ v: 1, kind: 'plugin.error', message: String((e && e.message) || e) }, '*');\n}\n</script></body></html>`;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"abracadabra-plugin.esm.js","names":["extractHost"],"sources":["../src/registry.ts","../src/source-spec.ts","../src/host.ts","../src/sandbox.ts"],"sourcesContent":["/**\n * `PluginRegistry` — the host-side manager for `AbraPlugin` instances.\n *\n * One registry per host (cou-shell, an `@abraca/nuxt` app, …). Generic over\n * the plugin type so apps that narrow `AbraPlugin` to their own interface\n * (e.g. `CouPlugin`, `AbracadabraPlugin`) get correctly-typed aggregator\n * methods without casts.\n *\n * Two tiers:\n *\n * - **Built-in plugins**: registered before `freeze()`. Frozen at boot;\n * `register()` after that point logs a warning and returns.\n * - **Space plugins**: registered/unregistered any time. Reactive consumers\n * poll `getSpacePluginVersion()` to detect changes.\n *\n * Aggregator methods return contributions from both tiers in registration\n * order, built-ins first.\n */\n\nimport type {\n\tAbraPlugin,\n\tAbraTiptapExtension,\n\tEditorPluginCtx,\n\tDragHandlePluginCtx,\n\tCommandPaletteCtx,\n\tAbraMentionProvider,\n\tAbraAwarenessContribution,\n\tAbraNodePanelSlot,\n\tAbraSettingsPanel,\n\tAbraKeyboardShortcut,\n\tAbraCommandItem,\n} from \"./types.ts\";\n\n// ── Type helpers for narrowed plugin generics ─────────────────────────────────\n\n/** Extract the value type from a `Record<string, V>`-shaped optional field. */\ntype RecordValueOf<T> = T extends Record<string, infer V> ? V : never;\n\n/** Extract the item type from a `(ctx) => readonly (readonly T[])[]` field. */\ntype GroupedItemOf<T> = T extends\n\t(...args: never[]) => readonly (infer Group)[]\n\t? Group extends readonly (infer Item)[]\n\t\t? Item\n\t\t: never\n\t: never;\n\n/** Extract the array element type from an optional array field. */\ntype ElementOf<T> = T extends readonly (infer V)[] ? V : never;\n\n/** Result-shape of `commandPaletteItems` collapsed to its item type. */\ntype CommandItemOf<T> = T extends\n\t(...args: never[]) => infer R\n\t? R extends Promise<readonly (infer Item)[]>\n\t\t? Item\n\t\t: R extends readonly (infer Item)[]\n\t\t\t? Item\n\t\t\t: never\n\t: never;\n\n// ── Aggregator return types — derived from the plugin generic ─────────────────\n\ntype PageTypeOf<P> = P extends { pageTypes?: infer R } ? RecordValueOf<R> : never;\ntype CustomHandlerOf<P> = P extends { customHandlers?: () => infer R } ? R : never;\ntype ToolbarItemOf<P> = P extends { toolbarItems?: infer F } ? GroupedItemOf<F> : never;\ntype BubbleItemOf<P> = P extends { bubbleMenuItems?: infer F } ? GroupedItemOf<F> : never;\ntype SuggestionItemOf<P> = P extends { suggestionItems?: infer F } ? GroupedItemOf<F> : never;\ntype DragHandleItemOf<P> = P extends { dragHandleItems?: infer F } ? GroupedItemOf<F> : never;\ntype MentionProviderOf<P> = P extends { mentionProviders?: infer A }\n\t? ElementOf<A>\n\t: never;\ntype AwarenessContributionOf<P> = P extends { awarenessContributions?: infer A }\n\t? ElementOf<A>\n\t: never;\ntype CommandPaletteItemOf<P> = P extends { commandPaletteItems?: infer F }\n\t? CommandItemOf<F>\n\t: never;\ntype NodePanelSlotOf<P> = P extends { nodePanelSlots?: infer A } ? ElementOf<A> : never;\ntype SettingsPanelOf<P> = P extends { settingsPanel?: infer V } ? NonNullable<V> : never;\ntype KeyboardShortcutOf<P> = P extends { keyboardShortcuts?: infer A } ? ElementOf<A> : never;\n\n// ── Registry implementation ───────────────────────────────────────────────────\n\nexport class PluginRegistry<P extends AbraPlugin = AbraPlugin> {\n\tprivate _builtins: P[] = [];\n\tprivate _spacePlugins: P[] = [];\n\tprivate _frozen = false;\n\tprivate _spaceVersion = 0;\n\n\t/**\n\t * Register a built-in plugin. No-ops with a warning after `freeze()`.\n\t * Use `registerSpacePlugin()` for plugins that arrive after boot.\n\t */\n\tregister(plugin: P): void {\n\t\tif (this._frozen) {\n\t\t\tconsole.warn(\n\t\t\t\t`[@abraca/plugin] Registry frozen — cannot register \"${plugin.name}\". Use registerSpacePlugin() for post-boot plugins.`,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tif (this._builtins.some((p) => p.name === plugin.name)) {\n\t\t\tconsole.warn(\n\t\t\t\t`[@abraca/plugin] Plugin \"${plugin.name}\" already registered`,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tthis._builtins.push(plugin);\n\t}\n\n\t/** Lock the registry. Called once after all built-in + external plugins are loaded. */\n\tfreeze(): void {\n\t\tthis._frozen = true;\n\t}\n\n\tisFrozen(): boolean {\n\t\treturn this._frozen;\n\t}\n\n\t/** Reactive registration channel for space-driven plugins (bypasses freeze). */\n\tregisterSpacePlugin(plugin: P): void {\n\t\tif (\n\t\t\tthis._spacePlugins.some((p) => p.name === plugin.name) ||\n\t\t\tthis._builtins.some((p) => p.name === plugin.name)\n\t\t) {\n\t\t\tconsole.warn(\n\t\t\t\t`[@abraca/plugin] Plugin \"${plugin.name}\" already registered`,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tthis._spacePlugins.push(plugin);\n\t\tthis._spaceVersion++;\n\t}\n\n\tunregisterSpacePlugin(name: string): void {\n\t\tconst idx = this._spacePlugins.findIndex((p) => p.name === name);\n\t\tif (idx === -1) return;\n\t\tthis._spacePlugins.splice(idx, 1);\n\t\tthis._spaceVersion++;\n\t}\n\n\tclearSpacePlugins(): void {\n\t\tif (this._spacePlugins.length === 0) return;\n\t\tthis._spacePlugins.length = 0;\n\t\tthis._spaceVersion++;\n\t}\n\n\t/** Monotonically increasing counter — bump when space plugins change. */\n\tgetSpacePluginVersion(): number {\n\t\treturn this._spaceVersion;\n\t}\n\n\tgetBuiltinPlugins(): readonly P[] {\n\t\treturn this._builtins;\n\t}\n\n\tgetSpacePlugins(): readonly P[] {\n\t\treturn this._spacePlugins;\n\t}\n\n\t/** All active plugins — built-ins first, then space plugins. */\n\tgetPlugins(): readonly P[] {\n\t\treturn [...this._builtins, ...this._spacePlugins];\n\t}\n\n\t// ── Aggregators ──────────────────────────────────────────────────────────\n\n\tgetAllExtensions(): AbraTiptapExtension[] {\n\t\treturn this.getPlugins().flatMap((p) => p.extensions?.() ?? []);\n\t}\n\n\tgetServerExtensions(): AbraTiptapExtension[] {\n\t\treturn this.getPlugins().flatMap((p) => p.serverExtensions?.() ?? []);\n\t}\n\n\t/** Resolves once every plugin with `extensionsReady` has settled. */\n\tasync waitForExtensions(): Promise<void> {\n\t\tawait Promise.all(\n\t\t\tthis.getPlugins().map((p) => p.extensionsReady ?? Promise.resolve()),\n\t\t);\n\t}\n\n\tgetAllPageTypes(): Record<string, PageTypeOf<P>> {\n\t\tconst merged: Record<string, PageTypeOf<P>> = {};\n\t\tfor (const p of this.getPlugins()) {\n\t\t\tif (p.pageTypes) {\n\t\t\t\tObject.assign(merged, p.pageTypes as Record<string, PageTypeOf<P>>);\n\t\t\t}\n\t\t}\n\t\treturn merged;\n\t}\n\n\tgetAllCustomHandlers(): CustomHandlerOf<P> {\n\t\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\t\tconst merged: Record<string, any> = {};\n\t\tfor (const p of this.getPlugins()) {\n\t\t\tif (p.customHandlers) Object.assign(merged, p.customHandlers());\n\t\t}\n\t\treturn merged as CustomHandlerOf<P>;\n\t}\n\n\tgetAllToolbarItems(ctx: EditorPluginCtx): ToolbarItemOf<P>[][] {\n\t\treturn this.getPlugins().flatMap(\n\t\t\t(p) =>\n\t\t\t\t(p.toolbarItems?.(ctx) ?? []) as readonly (readonly ToolbarItemOf<P>[])[],\n\t\t) as ToolbarItemOf<P>[][];\n\t}\n\n\tgetAllBubbleMenuItems(ctx: EditorPluginCtx): BubbleItemOf<P>[][] {\n\t\treturn this.getPlugins().flatMap(\n\t\t\t(p) =>\n\t\t\t\t(p.bubbleMenuItems?.(ctx) ?? []) as readonly (readonly BubbleItemOf<P>[])[],\n\t\t) as BubbleItemOf<P>[][];\n\t}\n\n\tgetAllSuggestionItems(ctx: EditorPluginCtx): SuggestionItemOf<P>[][] {\n\t\treturn this.getPlugins().flatMap(\n\t\t\t(p) =>\n\t\t\t\t(p.suggestionItems?.(ctx) ?? []) as readonly (readonly SuggestionItemOf<P>[])[],\n\t\t) as SuggestionItemOf<P>[][];\n\t}\n\n\tgetAllDragHandleItems(ctx: DragHandlePluginCtx): DragHandleItemOf<P>[][] {\n\t\treturn this.getPlugins().flatMap(\n\t\t\t(p) =>\n\t\t\t\t(p.dragHandleItems?.(ctx) ?? []) as readonly (readonly DragHandleItemOf<P>[])[],\n\t\t) as DragHandleItemOf<P>[][];\n\t}\n\n\tgetAllMentionProviders(): MentionProviderOf<P>[] {\n\t\treturn this.getPlugins().flatMap(\n\t\t\t(p) => (p.mentionProviders ?? []) as unknown as readonly MentionProviderOf<P>[],\n\t\t) as MentionProviderOf<P>[];\n\t}\n\n\tgetAllAwarenessContributions(): AwarenessContributionOf<P>[] {\n\t\treturn this.getPlugins().flatMap(\n\t\t\t(p) => (p.awarenessContributions ?? []) as unknown as readonly AwarenessContributionOf<P>[],\n\t\t) as AwarenessContributionOf<P>[];\n\t}\n\n\tasync getAllCommandPaletteItems(\n\t\tctx: CommandPaletteCtx,\n\t): Promise<CommandPaletteItemOf<P>[]> {\n\t\tconst results = await Promise.all(\n\t\t\tthis.getPlugins()\n\t\t\t\t.filter((p) => p.commandPaletteItems)\n\t\t\t\t.map((p) => Promise.resolve(p.commandPaletteItems!(ctx))),\n\t\t);\n\t\tconst flat = results.flat() as CommandPaletteItemOf<P>[];\n\t\treturn flat.filter((item) => {\n\t\t\tconst w = (item as AbraCommandItem).when;\n\t\t\treturn !w || w(ctx);\n\t\t});\n\t}\n\n\tgetAllNodePanelSlots(): NodePanelSlotOf<P>[] {\n\t\treturn this.getPlugins().flatMap(\n\t\t\t(p) => (p.nodePanelSlots ?? []) as unknown as readonly NodePanelSlotOf<P>[],\n\t\t) as NodePanelSlotOf<P>[];\n\t}\n\n\tgetSettingsPanels(): SettingsPanelOf<P>[] {\n\t\treturn this.getPlugins()\n\t\t\t.map((p) => p.settingsPanel)\n\t\t\t.filter((v): v is NonNullable<typeof v> => v !== undefined) as SettingsPanelOf<P>[];\n\t}\n\n\tgetAllKeyboardShortcuts(): KeyboardShortcutOf<P>[] {\n\t\treturn this.getPlugins().flatMap(\n\t\t\t(p) => (p.keyboardShortcuts ?? []) as unknown as readonly KeyboardShortcutOf<P>[],\n\t\t) as KeyboardShortcutOf<P>[];\n\t}\n}\n\n// ── Re-export aggregator types so consumer-side aliases can name them ─────────\n\nexport type {\n\tAbraPlugin,\n\tEditorPluginCtx,\n\tDragHandlePluginCtx,\n\tCommandPaletteCtx,\n\tAbraMentionProvider,\n\tAbraAwarenessContribution,\n\tAbraNodePanelSlot,\n\tAbraSettingsPanel,\n\tAbraKeyboardShortcut,\n\tAbraCommandItem,\n};\n","/**\n * Plugin source specification — the user-typed shorthand that resolves to a\n * loadable URL.\n *\n * Today the registry server (Phase C) hosts artifacts at canonical URLs.\n * Until then, both `cou-shell` and `@abraca/nuxt` accept three forms:\n *\n * - `npm:<package>[@version]` → jsDelivr npm CDN\n * - `github:<user>/<repo>[@ref]` → jsDelivr GitHub CDN\n * - any other string → returned as-is (must be `https://` or `http://localhost`)\n *\n * Once Phase C ships we add:\n *\n * - `abra:<plugin-id>[@version]` → the official registry\n *\n * The output URL points at the plugin's bundle entry. Convention is\n * `<root>/dist/plugin.js`.\n */\n\n/** A locally-installed external plugin record (persisted to host storage). */\nexport interface ExternalPluginEntry {\n\t/** Resolved fetch URL — the output of `normalizePluginUrl`. */\n\turl: string;\n\t/** Plugin `name` from the loaded bundle's manifest / default export. */\n\tname: string;\n\tlabel?: string;\n\tversion?: string;\n\tdescription?: string;\n\tenabled: boolean;\n\t/** Most recent load error, if any. Cleared on next successful load. */\n\terror?: string;\n\tinstalledAt: number;\n\t/**\n\t * Integrity hash from the plugin's manifest, if known. Verified on every\n\t * load — a silent change indicates supply-chain tampering and the host\n\t * should refuse to instantiate the plugin.\n\t */\n\tintegrity?: string;\n}\n\n/**\n * Resolve a user-typed plugin spec to a fetchable URL.\n *\n * - `npm:foo@1.2.3` → `https://cdn.jsdelivr.net/npm/foo@1.2.3/dist/plugin.js`\n * - `github:org/repo@main` → `https://cdn.jsdelivr.net/gh/org/repo@main/dist/plugin.js`\n * - anything else → returned trimmed, unchanged\n */\nexport function normalizePluginUrl(input: string): string {\n\tconst trimmed = input.trim();\n\tif (trimmed.startsWith(\"npm:\")) {\n\t\tconst pkg = trimmed.slice(4);\n\t\treturn `https://cdn.jsdelivr.net/npm/${pkg}/dist/plugin.js`;\n\t}\n\tif (trimmed.startsWith(\"github:\")) {\n\t\tconst repo = trimmed.slice(7);\n\t\treturn `https://cdn.jsdelivr.net/gh/${repo}/dist/plugin.js`;\n\t}\n\treturn trimmed;\n}\n\n/**\n * Reject any URL that isn't HTTPS or localhost over HTTP. Hosts should call\n * this before importing — `normalizePluginUrl` does not enforce the policy\n * (npm:/github: shorthand always produces HTTPS, so callers only need this\n * for raw third-party URLs).\n */\nexport function isSafePluginUrl(url: string): boolean {\n\ttry {\n\t\tconst u = new URL(url);\n\t\tif (u.protocol === \"https:\") return true;\n\t\tif (\n\t\t\tu.protocol === \"http:\" &&\n\t\t\t(u.hostname === \"localhost\" || u.hostname === \"127.0.0.1\")\n\t\t) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t} catch {\n\t\treturn false;\n\t}\n}\n","/**\n * `PluginHost` — capability-guarded wrapper around the browser globals\n * a plugin is allowed to call. Each guard consults the plugin's\n * manifest before forwarding to the underlying API. A plugin that\n * declared `network:api.example.com` can only `fetch` against\n * `api.example.com`; a plugin without `clipboard:write` cannot write\n * the clipboard; etc.\n *\n * Phase 1 contract: this is opt-in. Plugins that import `createPluginHost`\n * and use its members get the gate for free. Plugins that grab\n * `globalThis.fetch` directly bypass the gate — sandboxing (iframe / Web\n * Worker / Wasm) lands in Phase F.2 and closes that loophole.\n *\n * The `CapabilityDenied` error class lets hosts (and tests) distinguish\n * policy failures from other runtime errors.\n */\n\nimport type { PluginCapability, PluginManifest } from \"./manifest.ts\";\n\n// ── Errors ────────────────────────────────────────────────────────────────────\n\n/**\n * Thrown when a plugin tries to use an API it didn't declare in its\n * manifest. Distinct from runtime failures (network 500, file not found,\n * etc.) so the host can show the user \"the plugin tried to do X without\n * permission\" instead of a generic error.\n */\nexport class CapabilityDenied extends Error {\n\treadonly capability: PluginCapability | string;\n\treadonly pluginId: string;\n\n\tconstructor(pluginId: string, capability: PluginCapability | string, detail?: string) {\n\t\tsuper(\n\t\t\t`plugin '${pluginId}' is not authorised to use '${capability}'${detail ? ` (${detail})` : \"\"}`,\n\t\t);\n\t\tthis.name = \"CapabilityDenied\";\n\t\tthis.capability = capability;\n\t\tthis.pluginId = pluginId;\n\t}\n}\n\n// ── Host shape ────────────────────────────────────────────────────────────────\n\n/** Subset of the global `fetch` signature the host exposes. */\nexport type GuardedFetch = (\n\tinput: RequestInfo | URL,\n\tinit?: RequestInit,\n) => Promise<Response>;\n\n/** Subset of `navigator.clipboard` the host exposes. */\nexport interface GuardedClipboard {\n\treadText(): Promise<string>;\n\twriteText(text: string): Promise<void>;\n}\n\n/** Permission-aware desktop notification. */\nexport interface GuardedNotifications {\n\tshow(title: string, options?: NotificationOptions): Promise<void>;\n}\n\n/**\n * Everything a plugin is allowed to touch through the host contract.\n * Each plugin gets its own instance via `createPluginHost(manifest)`.\n */\nexport interface PluginHost {\n\treadonly pluginId: string;\n\treadonly capabilities: ReadonlySet<PluginCapability>;\n\tfetch: GuardedFetch;\n\tclipboard: GuardedClipboard;\n\tnotifications: GuardedNotifications;\n\t/** Test whether a capability is granted without invoking the wrapped API. */\n\tcan(capability: PluginCapability | string): boolean;\n}\n\n// ── Constructor ───────────────────────────────────────────────────────────────\n\nexport interface PluginHostOptions {\n\t/**\n\t * Override the underlying `fetch`. Tests inject a stub here; production\n\t * hosts leave this unset and use the global.\n\t */\n\tfetch?: typeof fetch;\n\t/** Override `navigator.clipboard`. */\n\tclipboard?: {\n\t\treadText?(): Promise<string>;\n\t\twriteText?(text: string): Promise<void>;\n\t};\n\t/**\n\t * Override the host's notification surface. The default uses the\n\t * browser's `Notification` API — fine for web hosts; native hosts\n\t * (Tauri, Electron) inject their own.\n\t */\n\tshowNotification?(title: string, options?: NotificationOptions): Promise<void>;\n}\n\n/**\n * Build a `PluginHost` from a manifest. The set of granted capabilities\n * is `manifest.capabilities.required ∪ optional`; runtime gates check\n * membership before forwarding to the underlying API.\n *\n * Network capabilities support host patterns:\n * - `network` — any host\n * - `network:api.foo.io` — exact match\n * - `network:*.foo.io` — wildcard subdomain match\n * - `network:foo.io,bar.io` — comma-separated list\n */\nexport function createPluginHost(\n\tmanifest: Pick<PluginManifest, \"id\" | \"capabilities\">,\n\toptions: PluginHostOptions = {},\n): PluginHost {\n\tconst granted = new Set<PluginCapability>([\n\t\t...(manifest.capabilities.required ?? []),\n\t\t...(manifest.capabilities.optional ?? []),\n\t]);\n\tconst pluginId = manifest.id;\n\n\tconst can = (cap: PluginCapability | string): boolean => granted.has(cap as PluginCapability);\n\n\tconst guardedFetch: GuardedFetch = async (input, init) => {\n\t\tconst url = typeof input === \"string\" ? input : input instanceof URL ? input.toString() : input.url;\n\t\tconst host = extractHost(url);\n\t\tif (!host) {\n\t\t\tthrow new CapabilityDenied(pluginId, \"network\", \"could not parse URL host\");\n\t\t}\n\t\tif (!matchesNetworkCapability(granted, host)) {\n\t\t\tthrow new CapabilityDenied(\n\t\t\t\tpluginId,\n\t\t\t\t`network:${host}`,\n\t\t\t\t\"declare it in capabilities.required or capabilities.optional\",\n\t\t\t);\n\t\t}\n\t\tconst real = options.fetch ?? globalThis.fetch;\n\t\tif (!real) throw new Error(\"fetch is not available in this runtime\");\n\t\treturn real(input, init);\n\t};\n\n\tconst guardedClipboard: GuardedClipboard = {\n\t\tasync readText() {\n\t\t\tif (!granted.has(\"clipboard:read\")) {\n\t\t\t\tthrow new CapabilityDenied(pluginId, \"clipboard:read\");\n\t\t\t}\n\t\t\tif (options.clipboard?.readText) return options.clipboard.readText();\n\t\t\tif (typeof navigator !== \"undefined\" && navigator.clipboard?.readText) {\n\t\t\t\treturn navigator.clipboard.readText();\n\t\t\t}\n\t\t\tthrow new Error(\"clipboard.readText is not available in this runtime\");\n\t\t},\n\t\tasync writeText(text: string) {\n\t\t\tif (!granted.has(\"clipboard:write\")) {\n\t\t\t\tthrow new CapabilityDenied(pluginId, \"clipboard:write\");\n\t\t\t}\n\t\t\tif (options.clipboard?.writeText) return options.clipboard.writeText(text);\n\t\t\tif (typeof navigator !== \"undefined\" && navigator.clipboard?.writeText) {\n\t\t\t\treturn navigator.clipboard.writeText(text);\n\t\t\t}\n\t\t\tthrow new Error(\"clipboard.writeText is not available in this runtime\");\n\t\t},\n\t};\n\n\tconst guardedNotifications: GuardedNotifications = {\n\t\tasync show(title: string, opts?: NotificationOptions) {\n\t\t\tif (!granted.has(\"notifications:show\")) {\n\t\t\t\tthrow new CapabilityDenied(pluginId, \"notifications:show\");\n\t\t\t}\n\t\t\tif (options.showNotification) return options.showNotification(title, opts);\n\t\t\tif (typeof Notification !== \"undefined\") {\n\t\t\t\tnew Notification(title, opts);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthrow new Error(\"notifications are not available in this runtime\");\n\t\t},\n\t};\n\n\treturn {\n\t\tpluginId,\n\t\tcapabilities: granted,\n\t\tfetch: guardedFetch,\n\t\tclipboard: guardedClipboard,\n\t\tnotifications: guardedNotifications,\n\t\tcan,\n\t};\n}\n\n// ── Helpers ──────────────────────────────────────────────────────────────────\n\nfunction extractHost(rawUrl: string): string | null {\n\ttry {\n\t\treturn new URL(rawUrl, \"http://placeholder\").host;\n\t}\n\tcatch {\n\t\treturn null;\n\t}\n}\n\n/**\n * Network capability matcher — `network`, `network:exact.host`,\n * `network:*.wildcard.host`, `network:a,b,c` comma-list. Exported so\n * the iframe sandbox can reuse the same rules without forking the\n * vocabulary.\n */\nexport function matchesNetworkCapability(granted: ReadonlySet<PluginCapability>, host: string): boolean {\n\tif (granted.has(\"network\")) return true;\n\tfor (const cap of granted) {\n\t\tif (!cap.startsWith(\"network:\")) continue;\n\t\tconst patterns = cap.slice(\"network:\".length).split(\",\");\n\t\tfor (const pattern of patterns) {\n\t\t\tif (matchesHostPattern(pattern.trim(), host)) return true;\n\t\t}\n\t}\n\treturn false;\n}\n\nfunction matchesHostPattern(pattern: string, host: string): boolean {\n\tif (!pattern) return false;\n\tif (pattern === host) return true;\n\tif (pattern.startsWith(\"*.\")) {\n\t\tconst suffix = pattern.slice(2);\n\t\treturn host === suffix || host.endsWith(`.${suffix}`);\n\t}\n\treturn false;\n}\n","/**\n * Iframe-based sandbox for external plugins.\n *\n * Plugins from outside the built-in set are loaded into a sandboxed\n * `<iframe sandbox=\"allow-scripts\">` — same origin restricted to the\n * iframe's null origin, no access to the parent's globals, no cookies,\n * no localStorage. The plugin module evaluates inside the iframe and\n * communicates back via `postMessage`.\n *\n * The host side of the bridge (this file) exposes the same surface as\n * `createPluginHost(manifest)` — fetch, clipboard, notifications —\n * gated by the same manifest capabilities. The difference: in Phase F\n * the contract was advisory (a plugin could grab `globalThis.fetch`);\n * here the iframe doesn't have access to those globals at all, so the\n * only path is through `postMessage`.\n *\n * Wire protocol (versioned via `v: 1` envelope):\n *\n * host → frame:\n * { v:1, kind:'init', pluginId, capabilities, artifactUrl }\n * { v:1, kind:'fetch.response', id, ok, status, body }\n * { v:1, kind:'clipboard.response', id, value? }\n * { v:1, kind:'notification.response', id }\n * { v:1, kind:'error', id, message }\n *\n * frame → host:\n * { v:1, kind:'ready' }\n * { v:1, kind:'fetch.request', id, url, init? }\n * { v:1, kind:'clipboard.read.request', id }\n * { v:1, kind:'clipboard.write.request', id, text }\n * { v:1, kind:'notification.request', id, title, options? }\n * { v:1, kind:'plugin.export', value } // the plugin's default export\n * { v:1, kind:'plugin.error', message }\n *\n * Phase F.2 ships the host side only. The frame-side runtime that\n * imports plugin bundles + speaks this protocol is `sandbox-runtime.ts`\n * (sibling file).\n */\n\nimport {\n\tCapabilityDenied,\n\tmatchesNetworkCapability,\n\ttype GuardedClipboard,\n\ttype GuardedFetch,\n\ttype GuardedNotifications,\n\ttype PluginHostOptions,\n} from \"./host.ts\";\nimport type { PluginCapability, PluginManifest } from \"./manifest.ts\";\n\n/** Public result of `loadSandboxedPlugin` — what the host actually keeps. */\nexport interface SandboxedPlugin {\n\t/** Stable id from the manifest. */\n\tpluginId: string;\n\t/** Capabilities granted to this sandbox. */\n\tcapabilities: ReadonlySet<PluginCapability>;\n\t/** The plugin's default export, copied across the postMessage boundary. */\n\texported: unknown;\n\t/** Tear down: removes the iframe + closes the bridge. */\n\tdispose(): void;\n}\n\nexport interface SandboxOptions extends PluginHostOptions {\n\t/**\n\t * Override the document the iframe attaches to. Tests inject a jsdom\n\t * document; production hosts use the default `document` global.\n\t */\n\tdoc?: Document;\n\t/**\n\t * Hard timeout (ms) on the plugin's `ready` handshake. Defaults to\n\t * 10 s. Plugins that don't post `ready` within the window get the\n\t * sandbox torn down + a load failure.\n\t */\n\treadyTimeoutMs?: number;\n}\n\ninterface InboundEnvelope {\n\tv: 1;\n\tkind: string;\n\tid?: string;\n\t[k: string]: unknown;\n}\n\n/**\n * Spin up a sandboxed iframe, load the plugin bundle inside it, wait\n * for the plugin to post `ready`, then return a handle. The host bridges\n * capability-gated APIs to the iframe via postMessage.\n */\nexport async function loadSandboxedPlugin(\n\tmanifest: Pick<PluginManifest, \"id\" | \"capabilities\">,\n\tartifactUrl: string,\n\toptions: SandboxOptions = {},\n): Promise<SandboxedPlugin> {\n\tconst doc = options.doc ?? (typeof document !== \"undefined\" ? document : null);\n\tif (!doc) {\n\t\tthrow new Error(\"loadSandboxedPlugin requires a Document — call from a browser context\");\n\t}\n\n\tconst granted = new Set<PluginCapability>([\n\t\t...(manifest.capabilities.required ?? []),\n\t\t...(manifest.capabilities.optional ?? []),\n\t]);\n\tconst pluginId = manifest.id;\n\n\tconst iframe = doc.createElement(\"iframe\");\n\t// `allow-scripts` lets the plugin run; we deliberately omit\n\t// `allow-same-origin` so the iframe has a null origin — it can't\n\t// reach our cookies, localStorage, IndexedDB, or service worker.\n\tiframe.setAttribute(\"sandbox\", \"allow-scripts\");\n\tiframe.setAttribute(\"aria-hidden\", \"true\");\n\tiframe.style.display = \"none\";\n\tiframe.style.width = \"0\";\n\tiframe.style.height = \"0\";\n\tiframe.style.border = \"0\";\n\n\t// The runtime that the plugin runs inside the frame. Imports the\n\t// plugin bundle by URL, listens for messages, and proxies the\n\t// gated APIs through postMessage.\n\tconst frameSrc = buildFrameSource(artifactUrl);\n\tconst blob = new Blob([frameSrc], { type: \"text/html\" });\n\tiframe.src = URL.createObjectURL(blob);\n\n\tdoc.body.appendChild(iframe);\n\n\tconst pendingFetch = new Map<string, (resp: Response) => void>();\n\tconst pendingFetchErr = new Map<string, (err: Error) => void>();\n\tconst pendingClip = new Map<string, (v: unknown) => void>();\n\tconst pendingClipErr = new Map<string, (err: Error) => void>();\n\tconst pendingNotif = new Map<string, () => void>();\n\tconst pendingNotifErr = new Map<string, (err: Error) => void>();\n\n\tlet resolveReady!: (value: unknown) => void;\n\tlet rejectReady!: (err: Error) => void;\n\tconst ready = new Promise<unknown>((res, rej) => {\n\t\tresolveReady = res;\n\t\trejectReady = rej;\n\t});\n\n\tconst post = (envelope: Record<string, unknown>) => {\n\t\tiframe.contentWindow?.postMessage({ v: 1, ...envelope }, \"*\");\n\t};\n\n\tconst messageHandler = async (e: MessageEvent) => {\n\t\tif (e.source !== iframe.contentWindow) return;\n\t\tconst msg = e.data as InboundEnvelope | null;\n\t\tif (!msg || msg.v !== 1) return;\n\n\t\tswitch (msg.kind) {\n\t\t\tcase \"ready\": {\n\t\t\t\tpost({ kind: \"init\", pluginId, capabilities: [...granted], artifactUrl });\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"plugin.export\": {\n\t\t\t\tresolveReady(msg.value);\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"plugin.error\": {\n\t\t\t\trejectReady(new Error(String(msg.message ?? \"plugin failed to load\")));\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"fetch.request\": {\n\t\t\t\tconst id = String(msg.id);\n\t\t\t\tconst url = String(msg.url);\n\t\t\t\tconst init = msg.init as RequestInit | undefined;\n\t\t\t\ttry {\n\t\t\t\t\tconst host = extractHost(url);\n\t\t\t\t\tif (!host || !matchesNetworkCapability(granted, host)) {\n\t\t\t\t\t\tthrow new CapabilityDenied(\n\t\t\t\t\t\t\tpluginId,\n\t\t\t\t\t\t\thost ? `network:${host}` : \"network\",\n\t\t\t\t\t\t\t\"declare in capabilities.required or capabilities.optional\",\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tconst real = options.fetch ?? globalThis.fetch;\n\t\t\t\t\tconst resp = await real(url, init);\n\t\t\t\t\tconst body = await resp.text();\n\t\t\t\t\tpost({\n\t\t\t\t\t\tkind: \"fetch.response\",\n\t\t\t\t\t\tid,\n\t\t\t\t\t\tok: resp.ok,\n\t\t\t\t\t\tstatus: resp.status,\n\t\t\t\t\t\tbody,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tcatch (err) {\n\t\t\t\t\tpost({ kind: \"error\", id, message: (err as Error).message });\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"clipboard.read.request\": {\n\t\t\t\tconst id = String(msg.id);\n\t\t\t\ttry {\n\t\t\t\t\tif (!granted.has(\"clipboard:read\")) {\n\t\t\t\t\t\tthrow new CapabilityDenied(pluginId, \"clipboard:read\");\n\t\t\t\t\t}\n\t\t\t\t\tconst value = options.clipboard?.readText\n\t\t\t\t\t\t? await options.clipboard.readText()\n\t\t\t\t\t\t: typeof navigator !== \"undefined\" && navigator.clipboard?.readText\n\t\t\t\t\t\t\t? await navigator.clipboard.readText()\n\t\t\t\t\t\t\t: \"\";\n\t\t\t\t\tpost({ kind: \"clipboard.response\", id, value });\n\t\t\t\t}\n\t\t\t\tcatch (err) {\n\t\t\t\t\tpost({ kind: \"error\", id, message: (err as Error).message });\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"clipboard.write.request\": {\n\t\t\t\tconst id = String(msg.id);\n\t\t\t\ttry {\n\t\t\t\t\tif (!granted.has(\"clipboard:write\")) {\n\t\t\t\t\t\tthrow new CapabilityDenied(pluginId, \"clipboard:write\");\n\t\t\t\t\t}\n\t\t\t\t\tconst text = String(msg.text ?? \"\");\n\t\t\t\t\tif (options.clipboard?.writeText) await options.clipboard.writeText(text);\n\t\t\t\t\telse if (typeof navigator !== \"undefined\" && navigator.clipboard?.writeText) {\n\t\t\t\t\t\tawait navigator.clipboard.writeText(text);\n\t\t\t\t\t}\n\t\t\t\t\tpost({ kind: \"clipboard.response\", id });\n\t\t\t\t}\n\t\t\t\tcatch (err) {\n\t\t\t\t\tpost({ kind: \"error\", id, message: (err as Error).message });\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"notification.request\": {\n\t\t\t\tconst id = String(msg.id);\n\t\t\t\ttry {\n\t\t\t\t\tif (!granted.has(\"notifications:show\")) {\n\t\t\t\t\t\tthrow new CapabilityDenied(pluginId, \"notifications:show\");\n\t\t\t\t\t}\n\t\t\t\t\tconst title = String(msg.title ?? \"\");\n\t\t\t\t\tconst opts = msg.options as NotificationOptions | undefined;\n\t\t\t\t\tif (options.showNotification) await options.showNotification(title, opts);\n\t\t\t\t\telse if (typeof Notification !== \"undefined\") new Notification(title, opts);\n\t\t\t\t\tpost({ kind: \"notification.response\", id });\n\t\t\t\t}\n\t\t\t\tcatch (err) {\n\t\t\t\t\tpost({ kind: \"error\", id, message: (err as Error).message });\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t};\n\n\t(typeof window !== \"undefined\" ? window : (doc.defaultView as Window))\n\t\t?.addEventListener(\"message\", messageHandler);\n\n\tconst timeoutMs = options.readyTimeoutMs ?? 10_000;\n\tconst timeoutHandle = setTimeout(\n\t\t() => rejectReady(new Error(`plugin '${pluginId}' did not become ready within ${timeoutMs}ms`)),\n\t\ttimeoutMs,\n\t);\n\n\tlet exported: unknown;\n\ttry {\n\t\texported = await ready;\n\t}\n\tcatch (err) {\n\t\t// Tear down on failure — never leak the iframe.\n\t\t(typeof window !== \"undefined\" ? window : (doc.defaultView as Window))\n\t\t\t?.removeEventListener(\"message\", messageHandler);\n\t\tiframe.remove();\n\t\tURL.revokeObjectURL(iframe.src);\n\t\tclearTimeout(timeoutHandle);\n\t\tthrow err;\n\t}\n\tclearTimeout(timeoutHandle);\n\n\t// Drain any in-flight requests so they don't hang refs to disposed iframe.\n\tconst dispose = () => {\n\t\tfor (const reject of pendingFetchErr.values()) reject(new Error(\"sandbox disposed\"));\n\t\tfor (const reject of pendingClipErr.values()) reject(new Error(\"sandbox disposed\"));\n\t\tfor (const reject of pendingNotifErr.values()) reject(new Error(\"sandbox disposed\"));\n\t\tpendingFetch.clear();\n\t\tpendingFetchErr.clear();\n\t\tpendingClip.clear();\n\t\tpendingClipErr.clear();\n\t\tpendingNotif.clear();\n\t\tpendingNotifErr.clear();\n\t\t(typeof window !== \"undefined\" ? window : (doc.defaultView as Window))\n\t\t\t?.removeEventListener(\"message\", messageHandler);\n\t\tiframe.remove();\n\t\tURL.revokeObjectURL(iframe.src);\n\t};\n\n\treturn {\n\t\tpluginId,\n\t\tcapabilities: granted,\n\t\texported,\n\t\tdispose,\n\t};\n}\n\n// ── Helpers (shared with host.ts but inlined here to avoid export churn) ─────\n\nfunction extractHost(rawUrl: string): string | null {\n\ttry {\n\t\treturn new URL(rawUrl, \"http://placeholder\").host;\n\t}\n\tcatch {\n\t\treturn null;\n\t}\n}\n\n// ── Frame runtime ────────────────────────────────────────────────────────────\n\n/**\n * HTML+JS that runs inside the iframe. Imports the plugin bundle by URL\n * (in the iframe's null origin — no cookies, no localStorage), proxies\n * fetch/clipboard/notifications back to the host via postMessage, and\n * posts the plugin's default export once it loads.\n *\n * Kept inline so the package has zero runtime files — every consumer\n * picks up the latest version on a single import.\n */\nfunction buildFrameSource(artifactUrl: string): string {\n\t// The frame's globals: a guarded `pluginHost` mirror, ready for\n\t// plugins to import. The plugin code reads `globalThis.pluginHost`.\n\treturn `<!doctype html>\n<html><head><meta charset=\"utf-8\"></head><body>\n<script type=\"module\">\nconst pending = new Map();\nlet nextId = 0;\nfunction rpc(kind, payload) {\n return new Promise((resolve, reject) => {\n const id = String(++nextId);\n pending.set(id, { resolve, reject });\n parent.postMessage({ v: 1, kind, id, ...payload }, '*');\n });\n}\nwindow.addEventListener('message', (e) => {\n const msg = e.data;\n if (!msg || msg.v !== 1) return;\n if (msg.kind === 'init') {\n globalThis.__abraPluginContext = {\n pluginId: msg.pluginId,\n capabilities: new Set(msg.capabilities),\n };\n return;\n }\n if (msg.id && pending.has(msg.id)) {\n const { resolve, reject } = pending.get(msg.id);\n pending.delete(msg.id);\n if (msg.kind === 'error') reject(new Error(msg.message));\n else resolve(msg);\n }\n});\nglobalThis.pluginHost = {\n async fetch(url, init) {\n const r = await rpc('fetch.request', { url: String(url), init });\n return new Response(r.body, { status: r.status });\n },\n clipboard: {\n readText() { return rpc('clipboard.read.request', {}).then(r => r.value); },\n writeText(text) { return rpc('clipboard.write.request', { text }).then(() => undefined); },\n },\n notifications: {\n show(title, options) { return rpc('notification.request', { title, options }).then(() => undefined); },\n },\n};\nparent.postMessage({ v: 1, kind: 'ready' }, '*');\ntry {\n const mod = await import(${JSON.stringify(artifactUrl)});\n const value = mod && (mod.default ?? mod);\n // Strip functions before posting — they don't survive postMessage.\n // We send the plain serialisable bits; functions stay in the iframe\n // and the host calls them via separate RPC channels (future work).\n const serialisable = JSON.parse(JSON.stringify(value, (_, v) =>\n typeof v === 'function' ? '[Function]' : v));\n parent.postMessage({ v: 1, kind: 'plugin.export', value: serialisable }, '*');\n} catch (e) {\n parent.postMessage({ v: 1, kind: 'plugin.error', message: String((e && e.message) || e) }, '*');\n}\n</script></body></html>`;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"abracadabra-plugin.esm.js","names":["extractHost"],"sources":["../src/registry.ts","../src/source-spec.ts","../src/host.ts","../src/sandbox.ts"],"sourcesContent":["/**\n * `PluginRegistry` — the host-side manager for `AbraPlugin` instances.\n *\n * One registry per host (cou-shell, an `@abraca/nuxt` app, …). Generic over\n * the plugin type so apps that narrow `AbraPlugin` to their own interface\n * (e.g. `CouPlugin`, `AbracadabraPlugin`) get correctly-typed aggregator\n * methods without casts.\n *\n * Two tiers:\n *\n * - **Built-in plugins**: registered before `freeze()`. Frozen at boot;\n * `register()` after that point logs a warning and returns.\n * - **Space plugins**: registered/unregistered any time. Reactive consumers\n * poll `getSpacePluginVersion()` to detect changes.\n *\n * Aggregator methods return contributions from both tiers in registration\n * order, built-ins first.\n */\n\nimport type {\n\tAbraPlugin,\n\tAbraTiptapExtension,\n\tEditorPluginCtx,\n\tDragHandlePluginCtx,\n\tCommandPaletteCtx,\n\tAbraMentionProvider,\n\tAbraAwarenessContribution,\n\tAbraNodePanelSlot,\n\tAbraSettingsPanel,\n\tAbraKeyboardShortcut,\n\tAbraCommandItem,\n} from \"./types.ts\";\n\n// ── Type helpers for narrowed plugin generics ─────────────────────────────────\n\n/** Extract the value type from a `Record<string, V>`-shaped optional field. */\ntype RecordValueOf<T> = T extends Record<string, infer V> ? V : never;\n\n/** Extract the item type from a `(ctx) => readonly (readonly T[])[]` field. */\ntype GroupedItemOf<T> = T extends\n\t(...args: never[]) => readonly (infer Group)[]\n\t? Group extends readonly (infer Item)[]\n\t\t? Item\n\t\t: never\n\t: never;\n\n/** Extract the array element type from an optional array field. */\ntype ElementOf<T> = T extends readonly (infer V)[] ? V : never;\n\n/** Result-shape of `commandPaletteItems` collapsed to its item type. */\ntype CommandItemOf<T> = T extends\n\t(...args: never[]) => infer R\n\t? R extends Promise<readonly (infer Item)[]>\n\t\t? Item\n\t\t: R extends readonly (infer Item)[]\n\t\t\t? Item\n\t\t\t: never\n\t: never;\n\n// ── Aggregator return types — derived from the plugin generic ─────────────────\n\ntype PageTypeOf<P> = P extends { pageTypes?: infer R } ? RecordValueOf<R> : never;\ntype CustomHandlerOf<P> = P extends { customHandlers?: () => infer R } ? R : never;\ntype ToolbarItemOf<P> = P extends { toolbarItems?: infer F } ? GroupedItemOf<F> : never;\ntype BubbleItemOf<P> = P extends { bubbleMenuItems?: infer F } ? GroupedItemOf<F> : never;\ntype SuggestionItemOf<P> = P extends { suggestionItems?: infer F } ? GroupedItemOf<F> : never;\ntype DragHandleItemOf<P> = P extends { dragHandleItems?: infer F } ? GroupedItemOf<F> : never;\ntype MentionProviderOf<P> = P extends { mentionProviders?: infer A }\n\t? ElementOf<A>\n\t: never;\ntype AwarenessContributionOf<P> = P extends { awarenessContributions?: infer A }\n\t? ElementOf<A>\n\t: never;\ntype CommandPaletteItemOf<P> = P extends { commandPaletteItems?: infer F }\n\t? CommandItemOf<F>\n\t: never;\ntype NodePanelSlotOf<P> = P extends { nodePanelSlots?: infer A } ? ElementOf<A> : never;\ntype SettingsPanelOf<P> = P extends { settingsPanel?: infer V } ? NonNullable<V> : never;\ntype KeyboardShortcutOf<P> = P extends { keyboardShortcuts?: infer A } ? ElementOf<A> : never;\n\n// ── Registry implementation ───────────────────────────────────────────────────\n\nexport class PluginRegistry<P extends AbraPlugin = AbraPlugin> {\n\tprivate _builtins: P[] = [];\n\tprivate _spacePlugins: P[] = [];\n\tprivate _frozen = false;\n\tprivate _spaceVersion = 0;\n\n\t/**\n\t * Register a built-in plugin. No-ops with a warning after `freeze()`.\n\t * Use `registerSpacePlugin()` for plugins that arrive after boot.\n\t */\n\tregister(plugin: P): void {\n\t\tif (this._frozen) {\n\t\t\tconsole.warn(\n\t\t\t\t`[@abraca/plugin] Registry frozen — cannot register \"${plugin.name}\". Use registerSpacePlugin() for post-boot plugins.`,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tif (this._builtins.some((p) => p.name === plugin.name)) {\n\t\t\tconsole.warn(\n\t\t\t\t`[@abraca/plugin] Plugin \"${plugin.name}\" already registered`,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tthis._builtins.push(plugin);\n\t}\n\n\t/** Lock the registry. Called once after all built-in + external plugins are loaded. */\n\tfreeze(): void {\n\t\tthis._frozen = true;\n\t}\n\n\tisFrozen(): boolean {\n\t\treturn this._frozen;\n\t}\n\n\t/** Reactive registration channel for space-driven plugins (bypasses freeze). */\n\tregisterSpacePlugin(plugin: P): void {\n\t\tif (\n\t\t\tthis._spacePlugins.some((p) => p.name === plugin.name) ||\n\t\t\tthis._builtins.some((p) => p.name === plugin.name)\n\t\t) {\n\t\t\tconsole.warn(\n\t\t\t\t`[@abraca/plugin] Plugin \"${plugin.name}\" already registered`,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tthis._spacePlugins.push(plugin);\n\t\tthis._spaceVersion++;\n\t}\n\n\tunregisterSpacePlugin(name: string): void {\n\t\tconst idx = this._spacePlugins.findIndex((p) => p.name === name);\n\t\tif (idx === -1) return;\n\t\tthis._spacePlugins.splice(idx, 1);\n\t\tthis._spaceVersion++;\n\t}\n\n\tclearSpacePlugins(): void {\n\t\tif (this._spacePlugins.length === 0) return;\n\t\tthis._spacePlugins.length = 0;\n\t\tthis._spaceVersion++;\n\t}\n\n\t/** Monotonically increasing counter — bump when space plugins change. */\n\tgetSpacePluginVersion(): number {\n\t\treturn this._spaceVersion;\n\t}\n\n\tgetBuiltinPlugins(): readonly P[] {\n\t\treturn this._builtins;\n\t}\n\n\tgetSpacePlugins(): readonly P[] {\n\t\treturn this._spacePlugins;\n\t}\n\n\t/** All active plugins — built-ins first, then space plugins. */\n\tgetPlugins(): readonly P[] {\n\t\treturn [...this._builtins, ...this._spacePlugins];\n\t}\n\n\t// ── Aggregators ──────────────────────────────────────────────────────────\n\n\tgetAllExtensions(): AbraTiptapExtension[] {\n\t\treturn this.getPlugins().flatMap((p) => p.extensions?.() ?? []);\n\t}\n\n\tgetServerExtensions(): AbraTiptapExtension[] {\n\t\treturn this.getPlugins().flatMap((p) => p.serverExtensions?.() ?? []);\n\t}\n\n\t/** Resolves once every plugin with `extensionsReady` has settled. */\n\tasync waitForExtensions(): Promise<void> {\n\t\tawait Promise.all(\n\t\t\tthis.getPlugins().map((p) => p.extensionsReady ?? Promise.resolve()),\n\t\t);\n\t}\n\n\tgetAllPageTypes(): Record<string, PageTypeOf<P>> {\n\t\tconst merged: Record<string, PageTypeOf<P>> = {};\n\t\tfor (const p of this.getPlugins()) {\n\t\t\tif (p.pageTypes) {\n\t\t\t\tObject.assign(merged, p.pageTypes as Record<string, PageTypeOf<P>>);\n\t\t\t}\n\t\t}\n\t\treturn merged;\n\t}\n\n\tgetAllCustomHandlers(): CustomHandlerOf<P> {\n\t\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\t\tconst merged: Record<string, any> = {};\n\t\tfor (const p of this.getPlugins()) {\n\t\t\tif (p.customHandlers) Object.assign(merged, p.customHandlers());\n\t\t}\n\t\treturn merged as CustomHandlerOf<P>;\n\t}\n\n\tgetAllToolbarItems(ctx: EditorPluginCtx): ToolbarItemOf<P>[][] {\n\t\treturn this.getPlugins().flatMap(\n\t\t\t(p) =>\n\t\t\t\t(p.toolbarItems?.(ctx) ?? []) as readonly (readonly ToolbarItemOf<P>[])[],\n\t\t) as ToolbarItemOf<P>[][];\n\t}\n\n\tgetAllBubbleMenuItems(ctx: EditorPluginCtx): BubbleItemOf<P>[][] {\n\t\treturn this.getPlugins().flatMap(\n\t\t\t(p) =>\n\t\t\t\t(p.bubbleMenuItems?.(ctx) ?? []) as readonly (readonly BubbleItemOf<P>[])[],\n\t\t) as BubbleItemOf<P>[][];\n\t}\n\n\tgetAllSuggestionItems(ctx: EditorPluginCtx): SuggestionItemOf<P>[][] {\n\t\treturn this.getPlugins().flatMap(\n\t\t\t(p) =>\n\t\t\t\t(p.suggestionItems?.(ctx) ?? []) as readonly (readonly SuggestionItemOf<P>[])[],\n\t\t) as SuggestionItemOf<P>[][];\n\t}\n\n\tgetAllDragHandleItems(ctx: DragHandlePluginCtx): DragHandleItemOf<P>[][] {\n\t\treturn this.getPlugins().flatMap(\n\t\t\t(p) =>\n\t\t\t\t(p.dragHandleItems?.(ctx) ?? []) as readonly (readonly DragHandleItemOf<P>[])[],\n\t\t) as DragHandleItemOf<P>[][];\n\t}\n\n\tgetAllMentionProviders(): MentionProviderOf<P>[] {\n\t\treturn this.getPlugins().flatMap(\n\t\t\t(p) => (p.mentionProviders ?? []) as unknown as readonly MentionProviderOf<P>[],\n\t\t) as MentionProviderOf<P>[];\n\t}\n\n\tgetAllAwarenessContributions(): AwarenessContributionOf<P>[] {\n\t\treturn this.getPlugins().flatMap(\n\t\t\t(p) => (p.awarenessContributions ?? []) as unknown as readonly AwarenessContributionOf<P>[],\n\t\t) as AwarenessContributionOf<P>[];\n\t}\n\n\tasync getAllCommandPaletteItems(\n\t\tctx: CommandPaletteCtx,\n\t): Promise<CommandPaletteItemOf<P>[]> {\n\t\tconst results = await Promise.all(\n\t\t\tthis.getPlugins()\n\t\t\t\t.filter((p) => p.commandPaletteItems)\n\t\t\t\t.map((p) => Promise.resolve(p.commandPaletteItems!(ctx))),\n\t\t);\n\t\tconst flat = results.flat() as CommandPaletteItemOf<P>[];\n\t\treturn flat.filter((item) => {\n\t\t\tconst w = (item as AbraCommandItem).when;\n\t\t\treturn !w || w(ctx);\n\t\t});\n\t}\n\n\tgetAllNodePanelSlots(): NodePanelSlotOf<P>[] {\n\t\treturn this.getPlugins().flatMap(\n\t\t\t(p) => (p.nodePanelSlots ?? []) as unknown as readonly NodePanelSlotOf<P>[],\n\t\t) as NodePanelSlotOf<P>[];\n\t}\n\n\tgetSettingsPanels(): SettingsPanelOf<P>[] {\n\t\treturn this.getPlugins()\n\t\t\t.map((p) => p.settingsPanel)\n\t\t\t.filter((v): v is NonNullable<typeof v> => v !== undefined) as SettingsPanelOf<P>[];\n\t}\n\n\tgetAllKeyboardShortcuts(): KeyboardShortcutOf<P>[] {\n\t\treturn this.getPlugins().flatMap(\n\t\t\t(p) => (p.keyboardShortcuts ?? []) as unknown as readonly KeyboardShortcutOf<P>[],\n\t\t) as KeyboardShortcutOf<P>[];\n\t}\n}\n\n// ── Re-export aggregator types so consumer-side aliases can name them ─────────\n\nexport type {\n\tAbraPlugin,\n\tEditorPluginCtx,\n\tDragHandlePluginCtx,\n\tCommandPaletteCtx,\n\tAbraMentionProvider,\n\tAbraAwarenessContribution,\n\tAbraNodePanelSlot,\n\tAbraSettingsPanel,\n\tAbraKeyboardShortcut,\n\tAbraCommandItem,\n};\n","/**\n * Plugin source specification — the user-typed shorthand that resolves to a\n * loadable URL.\n *\n * Today the registry server (Phase C) hosts artifacts at canonical URLs.\n * Until then, both `cou-shell` and `@abraca/nuxt` accept three forms:\n *\n * - `npm:<package>[@version]` → jsDelivr npm CDN\n * - `github:<user>/<repo>[@ref]` → jsDelivr GitHub CDN\n * - any other string → returned as-is (must be `https://` or `http://localhost`)\n *\n * Once Phase C ships we add:\n *\n * - `abra:<plugin-id>[@version]` → the official registry\n *\n * The output URL points at the plugin's bundle entry. Convention is\n * `<root>/dist/plugin.js`.\n */\n\nimport type { PluginCapability } from \"./manifest.ts\";\n\n/**\n * How an entry got onto the user's machine. Drives the trust UI:\n * `registry` is silently trusted; `url` and `upload` require the explicit\n * untrusted-code dialog; `space` was declared by the active space and gets\n * auto-loaded only when the same id exists in the registry — otherwise it\n * routes through the same untrusted gate as `url`/`upload`.\n */\nexport type PluginOrigin = \"registry\" | \"url\" | \"upload\" | \"space\";\n\n/** A locally-installed external plugin record (persisted to host storage). */\nexport interface ExternalPluginEntry {\n\t/** Resolved fetch URL — the output of `normalizePluginUrl`. */\n\turl: string;\n\t/** Plugin `name` from the loaded bundle's manifest / default export. */\n\tname: string;\n\tlabel?: string;\n\tversion?: string;\n\tdescription?: string;\n\tenabled: boolean;\n\t/** Most recent load error, if any. Cleared on next successful load. */\n\terror?: string;\n\tinstalledAt: number;\n\t/**\n\t * Integrity hash from the plugin's manifest, if known. Verified on every\n\t * load — a silent change indicates supply-chain tampering and the host\n\t * should refuse to instantiate the plugin.\n\t */\n\tintegrity?: string;\n\n\t/**\n\t * How this entry was installed. Absent on records written before the\n\t * trust-origin field was introduced — treat as `\"url\"` (the only pre-\n\t * existing external-install path) when migrating.\n\t */\n\torigin?: PluginOrigin;\n\t/**\n\t * Registry id (`PluginManifest.id`), set on `origin: \"registry\"` and\n\t * for `\"space\"` entries the host has reconciled against the catalog.\n\t * Lets the catalog's auto-update + decline-memory keys be id-based\n\t * instead of url-based.\n\t */\n\tid?: string;\n\t/**\n\t * SHA-256 of the uploaded bundle bytes (hex). Set on `origin: \"upload\"`\n\t * only — registry entries use `integrity`, URL entries have no hash.\n\t * Used as the IndexedDB blob key for restoring the uploaded artifact\n\t * across reloads.\n\t */\n\tsha256?: string;\n\t/**\n\t * Wall-clock when the user accepted the untrusted-code warning for\n\t * this entry. Set on every `\"url\"` / `\"upload\"` install and on every\n\t * `\"space\"` install that fell back to the untrusted path. Absent on\n\t * `\"registry\"` installs (registry trust is implicit).\n\t *\n\t * Cleared together with `acknowledgedVersion` /\n\t * `acknowledgedCapabilities` whenever the dialog re-prompts.\n\t */\n\ttrustAcknowledgedAt?: number;\n\t/**\n\t * Version the user acknowledged when last accepting the warning. The\n\t * trust dialog re-prompts whenever the manifest version no longer\n\t * matches this — see the `\"Per id+version, AND when capabilities grow\"`\n\t * trust-scope decision in the plugin-browser plan.\n\t */\n\tacknowledgedVersion?: string;\n\t/**\n\t * Required-capability set the user acknowledged at install time. The\n\t * dialog re-prompts whenever the current manifest declares a required\n\t * capability not in this set, even within the same version (mirrors\n\t * `require_review_on_cap_growth` in the server policy).\n\t */\n\tacknowledgedCapabilities?: readonly PluginCapability[];\n}\n\n/**\n * Resolve a user-typed plugin spec to a fetchable URL.\n *\n * - `npm:foo@1.2.3` → `https://cdn.jsdelivr.net/npm/foo@1.2.3/dist/plugin.js`\n * - `github:org/repo@main` → `https://cdn.jsdelivr.net/gh/org/repo@main/dist/plugin.js`\n * - anything else → returned trimmed, unchanged\n */\nexport function normalizePluginUrl(input: string): string {\n\tconst trimmed = input.trim();\n\tif (trimmed.startsWith(\"npm:\")) {\n\t\tconst pkg = trimmed.slice(4);\n\t\treturn `https://cdn.jsdelivr.net/npm/${pkg}/dist/plugin.js`;\n\t}\n\tif (trimmed.startsWith(\"github:\")) {\n\t\tconst repo = trimmed.slice(7);\n\t\treturn `https://cdn.jsdelivr.net/gh/${repo}/dist/plugin.js`;\n\t}\n\treturn trimmed;\n}\n\n/**\n * Reject any URL that isn't HTTPS or localhost over HTTP. Hosts should call\n * this before importing — `normalizePluginUrl` does not enforce the policy\n * (npm:/github: shorthand always produces HTTPS, so callers only need this\n * for raw third-party URLs).\n */\nexport function isSafePluginUrl(url: string): boolean {\n\ttry {\n\t\tconst u = new URL(url);\n\t\tif (u.protocol === \"https:\") return true;\n\t\tif (\n\t\t\tu.protocol === \"http:\" &&\n\t\t\t(u.hostname === \"localhost\" || u.hostname === \"127.0.0.1\")\n\t\t) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t} catch {\n\t\treturn false;\n\t}\n}\n","/**\n * `PluginHost` — capability-guarded wrapper around the browser globals\n * a plugin is allowed to call. Each guard consults the plugin's\n * manifest before forwarding to the underlying API. A plugin that\n * declared `network:api.example.com` can only `fetch` against\n * `api.example.com`; a plugin without `clipboard:write` cannot write\n * the clipboard; etc.\n *\n * Phase 1 contract: this is opt-in. Plugins that import `createPluginHost`\n * and use its members get the gate for free. Plugins that grab\n * `globalThis.fetch` directly bypass the gate — sandboxing (iframe / Web\n * Worker / Wasm) lands in Phase F.2 and closes that loophole.\n *\n * The `CapabilityDenied` error class lets hosts (and tests) distinguish\n * policy failures from other runtime errors.\n */\n\nimport type { PluginCapability, PluginManifest } from \"./manifest.ts\";\n\n// ── Errors ────────────────────────────────────────────────────────────────────\n\n/**\n * Thrown when a plugin tries to use an API it didn't declare in its\n * manifest. Distinct from runtime failures (network 500, file not found,\n * etc.) so the host can show the user \"the plugin tried to do X without\n * permission\" instead of a generic error.\n */\nexport class CapabilityDenied extends Error {\n\treadonly capability: PluginCapability | string;\n\treadonly pluginId: string;\n\n\tconstructor(pluginId: string, capability: PluginCapability | string, detail?: string) {\n\t\tsuper(\n\t\t\t`plugin '${pluginId}' is not authorised to use '${capability}'${detail ? ` (${detail})` : \"\"}`,\n\t\t);\n\t\tthis.name = \"CapabilityDenied\";\n\t\tthis.capability = capability;\n\t\tthis.pluginId = pluginId;\n\t}\n}\n\n// ── Host shape ────────────────────────────────────────────────────────────────\n\n/** Subset of the global `fetch` signature the host exposes. */\nexport type GuardedFetch = (\n\tinput: RequestInfo | URL,\n\tinit?: RequestInit,\n) => Promise<Response>;\n\n/** Subset of `navigator.clipboard` the host exposes. */\nexport interface GuardedClipboard {\n\treadText(): Promise<string>;\n\twriteText(text: string): Promise<void>;\n}\n\n/** Permission-aware desktop notification. */\nexport interface GuardedNotifications {\n\tshow(title: string, options?: NotificationOptions): Promise<void>;\n}\n\n/**\n * Everything a plugin is allowed to touch through the host contract.\n * Each plugin gets its own instance via `createPluginHost(manifest)`.\n */\nexport interface PluginHost {\n\treadonly pluginId: string;\n\treadonly capabilities: ReadonlySet<PluginCapability>;\n\tfetch: GuardedFetch;\n\tclipboard: GuardedClipboard;\n\tnotifications: GuardedNotifications;\n\t/** Test whether a capability is granted without invoking the wrapped API. */\n\tcan(capability: PluginCapability | string): boolean;\n}\n\n// ── Constructor ───────────────────────────────────────────────────────────────\n\nexport interface PluginHostOptions {\n\t/**\n\t * Override the underlying `fetch`. Tests inject a stub here; production\n\t * hosts leave this unset and use the global.\n\t */\n\tfetch?: typeof fetch;\n\t/** Override `navigator.clipboard`. */\n\tclipboard?: {\n\t\treadText?(): Promise<string>;\n\t\twriteText?(text: string): Promise<void>;\n\t};\n\t/**\n\t * Override the host's notification surface. The default uses the\n\t * browser's `Notification` API — fine for web hosts; native hosts\n\t * (Tauri, Electron) inject their own.\n\t */\n\tshowNotification?(title: string, options?: NotificationOptions): Promise<void>;\n}\n\n/**\n * Build a `PluginHost` from a manifest. The set of granted capabilities\n * is `manifest.capabilities.required ∪ optional`; runtime gates check\n * membership before forwarding to the underlying API.\n *\n * Network capabilities support host patterns:\n * - `network` — any host\n * - `network:api.foo.io` — exact match\n * - `network:*.foo.io` — wildcard subdomain match\n * - `network:foo.io,bar.io` — comma-separated list\n */\nexport function createPluginHost(\n\tmanifest: Pick<PluginManifest, \"id\" | \"capabilities\">,\n\toptions: PluginHostOptions = {},\n): PluginHost {\n\tconst granted = new Set<PluginCapability>([\n\t\t...(manifest.capabilities.required ?? []),\n\t\t...(manifest.capabilities.optional ?? []),\n\t]);\n\tconst pluginId = manifest.id;\n\n\tconst can = (cap: PluginCapability | string): boolean => granted.has(cap as PluginCapability);\n\n\tconst guardedFetch: GuardedFetch = async (input, init) => {\n\t\tconst url = typeof input === \"string\" ? input : input instanceof URL ? input.toString() : input.url;\n\t\tconst host = extractHost(url);\n\t\tif (!host) {\n\t\t\tthrow new CapabilityDenied(pluginId, \"network\", \"could not parse URL host\");\n\t\t}\n\t\tif (!matchesNetworkCapability(granted, host)) {\n\t\t\tthrow new CapabilityDenied(\n\t\t\t\tpluginId,\n\t\t\t\t`network:${host}`,\n\t\t\t\t\"declare it in capabilities.required or capabilities.optional\",\n\t\t\t);\n\t\t}\n\t\tconst real = options.fetch ?? globalThis.fetch;\n\t\tif (!real) throw new Error(\"fetch is not available in this runtime\");\n\t\treturn real(input, init);\n\t};\n\n\tconst guardedClipboard: GuardedClipboard = {\n\t\tasync readText() {\n\t\t\tif (!granted.has(\"clipboard:read\")) {\n\t\t\t\tthrow new CapabilityDenied(pluginId, \"clipboard:read\");\n\t\t\t}\n\t\t\tif (options.clipboard?.readText) return options.clipboard.readText();\n\t\t\tif (typeof navigator !== \"undefined\" && navigator.clipboard?.readText) {\n\t\t\t\treturn navigator.clipboard.readText();\n\t\t\t}\n\t\t\tthrow new Error(\"clipboard.readText is not available in this runtime\");\n\t\t},\n\t\tasync writeText(text: string) {\n\t\t\tif (!granted.has(\"clipboard:write\")) {\n\t\t\t\tthrow new CapabilityDenied(pluginId, \"clipboard:write\");\n\t\t\t}\n\t\t\tif (options.clipboard?.writeText) return options.clipboard.writeText(text);\n\t\t\tif (typeof navigator !== \"undefined\" && navigator.clipboard?.writeText) {\n\t\t\t\treturn navigator.clipboard.writeText(text);\n\t\t\t}\n\t\t\tthrow new Error(\"clipboard.writeText is not available in this runtime\");\n\t\t},\n\t};\n\n\tconst guardedNotifications: GuardedNotifications = {\n\t\tasync show(title: string, opts?: NotificationOptions) {\n\t\t\tif (!granted.has(\"notifications:show\")) {\n\t\t\t\tthrow new CapabilityDenied(pluginId, \"notifications:show\");\n\t\t\t}\n\t\t\tif (options.showNotification) return options.showNotification(title, opts);\n\t\t\tif (typeof Notification !== \"undefined\") {\n\t\t\t\tnew Notification(title, opts);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthrow new Error(\"notifications are not available in this runtime\");\n\t\t},\n\t};\n\n\treturn {\n\t\tpluginId,\n\t\tcapabilities: granted,\n\t\tfetch: guardedFetch,\n\t\tclipboard: guardedClipboard,\n\t\tnotifications: guardedNotifications,\n\t\tcan,\n\t};\n}\n\n// ── Helpers ──────────────────────────────────────────────────────────────────\n\nfunction extractHost(rawUrl: string): string | null {\n\ttry {\n\t\treturn new URL(rawUrl, \"http://placeholder\").host;\n\t}\n\tcatch {\n\t\treturn null;\n\t}\n}\n\n/**\n * Network capability matcher — `network`, `network:exact.host`,\n * `network:*.wildcard.host`, `network:a,b,c` comma-list. Exported so\n * the iframe sandbox can reuse the same rules without forking the\n * vocabulary.\n */\nexport function matchesNetworkCapability(granted: ReadonlySet<PluginCapability>, host: string): boolean {\n\tif (granted.has(\"network\")) return true;\n\tfor (const cap of granted) {\n\t\tif (!cap.startsWith(\"network:\")) continue;\n\t\tconst patterns = cap.slice(\"network:\".length).split(\",\");\n\t\tfor (const pattern of patterns) {\n\t\t\tif (matchesHostPattern(pattern.trim(), host)) return true;\n\t\t}\n\t}\n\treturn false;\n}\n\nfunction matchesHostPattern(pattern: string, host: string): boolean {\n\tif (!pattern) return false;\n\tif (pattern === host) return true;\n\tif (pattern.startsWith(\"*.\")) {\n\t\tconst suffix = pattern.slice(2);\n\t\treturn host === suffix || host.endsWith(`.${suffix}`);\n\t}\n\treturn false;\n}\n","/**\n * Iframe-based sandbox for external plugins.\n *\n * Plugins from outside the built-in set are loaded into a sandboxed\n * `<iframe sandbox=\"allow-scripts\">` — same origin restricted to the\n * iframe's null origin, no access to the parent's globals, no cookies,\n * no localStorage. The plugin module evaluates inside the iframe and\n * communicates back via `postMessage`.\n *\n * The host side of the bridge (this file) exposes the same surface as\n * `createPluginHost(manifest)` — fetch, clipboard, notifications —\n * gated by the same manifest capabilities. The difference: in Phase F\n * the contract was advisory (a plugin could grab `globalThis.fetch`);\n * here the iframe doesn't have access to those globals at all, so the\n * only path is through `postMessage`.\n *\n * Wire protocol (versioned via `v: 1` envelope):\n *\n * host → frame:\n * { v:1, kind:'init', pluginId, capabilities, artifactUrl }\n * { v:1, kind:'fetch.response', id, ok, status, body }\n * { v:1, kind:'clipboard.response', id, value? }\n * { v:1, kind:'notification.response', id }\n * { v:1, kind:'error', id, message }\n *\n * frame → host:\n * { v:1, kind:'ready' }\n * { v:1, kind:'fetch.request', id, url, init? }\n * { v:1, kind:'clipboard.read.request', id }\n * { v:1, kind:'clipboard.write.request', id, text }\n * { v:1, kind:'notification.request', id, title, options? }\n * { v:1, kind:'plugin.export', value } // the plugin's default export\n * { v:1, kind:'plugin.error', message }\n *\n * Phase F.2 ships the host side only. The frame-side runtime that\n * imports plugin bundles + speaks this protocol is `sandbox-runtime.ts`\n * (sibling file).\n */\n\nimport {\n\tCapabilityDenied,\n\tmatchesNetworkCapability,\n\ttype GuardedClipboard,\n\ttype GuardedFetch,\n\ttype GuardedNotifications,\n\ttype PluginHostOptions,\n} from \"./host.ts\";\nimport type { PluginCapability, PluginManifest } from \"./manifest.ts\";\n\n/** Public result of `loadSandboxedPlugin` — what the host actually keeps. */\nexport interface SandboxedPlugin {\n\t/** Stable id from the manifest. */\n\tpluginId: string;\n\t/** Capabilities granted to this sandbox. */\n\tcapabilities: ReadonlySet<PluginCapability>;\n\t/** The plugin's default export, copied across the postMessage boundary. */\n\texported: unknown;\n\t/** Tear down: removes the iframe + closes the bridge. */\n\tdispose(): void;\n}\n\nexport interface SandboxOptions extends PluginHostOptions {\n\t/**\n\t * Override the document the iframe attaches to. Tests inject a jsdom\n\t * document; production hosts use the default `document` global.\n\t */\n\tdoc?: Document;\n\t/**\n\t * Hard timeout (ms) on the plugin's `ready` handshake. Defaults to\n\t * 10 s. Plugins that don't post `ready` within the window get the\n\t * sandbox torn down + a load failure.\n\t */\n\treadyTimeoutMs?: number;\n}\n\ninterface InboundEnvelope {\n\tv: 1;\n\tkind: string;\n\tid?: string;\n\t[k: string]: unknown;\n}\n\n/**\n * Spin up a sandboxed iframe, load the plugin bundle inside it, wait\n * for the plugin to post `ready`, then return a handle. The host bridges\n * capability-gated APIs to the iframe via postMessage.\n */\nexport async function loadSandboxedPlugin(\n\tmanifest: Pick<PluginManifest, \"id\" | \"capabilities\">,\n\tartifactUrl: string,\n\toptions: SandboxOptions = {},\n): Promise<SandboxedPlugin> {\n\tconst doc = options.doc ?? (typeof document !== \"undefined\" ? document : null);\n\tif (!doc) {\n\t\tthrow new Error(\"loadSandboxedPlugin requires a Document — call from a browser context\");\n\t}\n\n\tconst granted = new Set<PluginCapability>([\n\t\t...(manifest.capabilities.required ?? []),\n\t\t...(manifest.capabilities.optional ?? []),\n\t]);\n\tconst pluginId = manifest.id;\n\n\tconst iframe = doc.createElement(\"iframe\");\n\t// `allow-scripts` lets the plugin run; we deliberately omit\n\t// `allow-same-origin` so the iframe has a null origin — it can't\n\t// reach our cookies, localStorage, IndexedDB, or service worker.\n\tiframe.setAttribute(\"sandbox\", \"allow-scripts\");\n\tiframe.setAttribute(\"aria-hidden\", \"true\");\n\tiframe.style.display = \"none\";\n\tiframe.style.width = \"0\";\n\tiframe.style.height = \"0\";\n\tiframe.style.border = \"0\";\n\n\t// The runtime that the plugin runs inside the frame. Imports the\n\t// plugin bundle by URL, listens for messages, and proxies the\n\t// gated APIs through postMessage.\n\tconst frameSrc = buildFrameSource(artifactUrl);\n\tconst blob = new Blob([frameSrc], { type: \"text/html\" });\n\tiframe.src = URL.createObjectURL(blob);\n\n\tdoc.body.appendChild(iframe);\n\n\tconst pendingFetch = new Map<string, (resp: Response) => void>();\n\tconst pendingFetchErr = new Map<string, (err: Error) => void>();\n\tconst pendingClip = new Map<string, (v: unknown) => void>();\n\tconst pendingClipErr = new Map<string, (err: Error) => void>();\n\tconst pendingNotif = new Map<string, () => void>();\n\tconst pendingNotifErr = new Map<string, (err: Error) => void>();\n\n\tlet resolveReady!: (value: unknown) => void;\n\tlet rejectReady!: (err: Error) => void;\n\tconst ready = new Promise<unknown>((res, rej) => {\n\t\tresolveReady = res;\n\t\trejectReady = rej;\n\t});\n\n\tconst post = (envelope: Record<string, unknown>) => {\n\t\tiframe.contentWindow?.postMessage({ v: 1, ...envelope }, \"*\");\n\t};\n\n\tconst messageHandler = async (e: MessageEvent) => {\n\t\tif (e.source !== iframe.contentWindow) return;\n\t\tconst msg = e.data as InboundEnvelope | null;\n\t\tif (!msg || msg.v !== 1) return;\n\n\t\tswitch (msg.kind) {\n\t\t\tcase \"ready\": {\n\t\t\t\tpost({ kind: \"init\", pluginId, capabilities: [...granted], artifactUrl });\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"plugin.export\": {\n\t\t\t\tresolveReady(msg.value);\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"plugin.error\": {\n\t\t\t\trejectReady(new Error(String(msg.message ?? \"plugin failed to load\")));\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"fetch.request\": {\n\t\t\t\tconst id = String(msg.id);\n\t\t\t\tconst url = String(msg.url);\n\t\t\t\tconst init = msg.init as RequestInit | undefined;\n\t\t\t\ttry {\n\t\t\t\t\tconst host = extractHost(url);\n\t\t\t\t\tif (!host || !matchesNetworkCapability(granted, host)) {\n\t\t\t\t\t\tthrow new CapabilityDenied(\n\t\t\t\t\t\t\tpluginId,\n\t\t\t\t\t\t\thost ? `network:${host}` : \"network\",\n\t\t\t\t\t\t\t\"declare in capabilities.required or capabilities.optional\",\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tconst real = options.fetch ?? globalThis.fetch;\n\t\t\t\t\tconst resp = await real(url, init);\n\t\t\t\t\tconst body = await resp.text();\n\t\t\t\t\tpost({\n\t\t\t\t\t\tkind: \"fetch.response\",\n\t\t\t\t\t\tid,\n\t\t\t\t\t\tok: resp.ok,\n\t\t\t\t\t\tstatus: resp.status,\n\t\t\t\t\t\tbody,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tcatch (err) {\n\t\t\t\t\tpost({ kind: \"error\", id, message: (err as Error).message });\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"clipboard.read.request\": {\n\t\t\t\tconst id = String(msg.id);\n\t\t\t\ttry {\n\t\t\t\t\tif (!granted.has(\"clipboard:read\")) {\n\t\t\t\t\t\tthrow new CapabilityDenied(pluginId, \"clipboard:read\");\n\t\t\t\t\t}\n\t\t\t\t\tconst value = options.clipboard?.readText\n\t\t\t\t\t\t? await options.clipboard.readText()\n\t\t\t\t\t\t: typeof navigator !== \"undefined\" && navigator.clipboard?.readText\n\t\t\t\t\t\t\t? await navigator.clipboard.readText()\n\t\t\t\t\t\t\t: \"\";\n\t\t\t\t\tpost({ kind: \"clipboard.response\", id, value });\n\t\t\t\t}\n\t\t\t\tcatch (err) {\n\t\t\t\t\tpost({ kind: \"error\", id, message: (err as Error).message });\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"clipboard.write.request\": {\n\t\t\t\tconst id = String(msg.id);\n\t\t\t\ttry {\n\t\t\t\t\tif (!granted.has(\"clipboard:write\")) {\n\t\t\t\t\t\tthrow new CapabilityDenied(pluginId, \"clipboard:write\");\n\t\t\t\t\t}\n\t\t\t\t\tconst text = String(msg.text ?? \"\");\n\t\t\t\t\tif (options.clipboard?.writeText) await options.clipboard.writeText(text);\n\t\t\t\t\telse if (typeof navigator !== \"undefined\" && navigator.clipboard?.writeText) {\n\t\t\t\t\t\tawait navigator.clipboard.writeText(text);\n\t\t\t\t\t}\n\t\t\t\t\tpost({ kind: \"clipboard.response\", id });\n\t\t\t\t}\n\t\t\t\tcatch (err) {\n\t\t\t\t\tpost({ kind: \"error\", id, message: (err as Error).message });\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"notification.request\": {\n\t\t\t\tconst id = String(msg.id);\n\t\t\t\ttry {\n\t\t\t\t\tif (!granted.has(\"notifications:show\")) {\n\t\t\t\t\t\tthrow new CapabilityDenied(pluginId, \"notifications:show\");\n\t\t\t\t\t}\n\t\t\t\t\tconst title = String(msg.title ?? \"\");\n\t\t\t\t\tconst opts = msg.options as NotificationOptions | undefined;\n\t\t\t\t\tif (options.showNotification) await options.showNotification(title, opts);\n\t\t\t\t\telse if (typeof Notification !== \"undefined\") new Notification(title, opts);\n\t\t\t\t\tpost({ kind: \"notification.response\", id });\n\t\t\t\t}\n\t\t\t\tcatch (err) {\n\t\t\t\t\tpost({ kind: \"error\", id, message: (err as Error).message });\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t};\n\n\t(typeof window !== \"undefined\" ? window : (doc.defaultView as Window))\n\t\t?.addEventListener(\"message\", messageHandler);\n\n\tconst timeoutMs = options.readyTimeoutMs ?? 10_000;\n\tconst timeoutHandle = setTimeout(\n\t\t() => rejectReady(new Error(`plugin '${pluginId}' did not become ready within ${timeoutMs}ms`)),\n\t\ttimeoutMs,\n\t);\n\n\tlet exported: unknown;\n\ttry {\n\t\texported = await ready;\n\t}\n\tcatch (err) {\n\t\t// Tear down on failure — never leak the iframe.\n\t\t(typeof window !== \"undefined\" ? window : (doc.defaultView as Window))\n\t\t\t?.removeEventListener(\"message\", messageHandler);\n\t\tiframe.remove();\n\t\tURL.revokeObjectURL(iframe.src);\n\t\tclearTimeout(timeoutHandle);\n\t\tthrow err;\n\t}\n\tclearTimeout(timeoutHandle);\n\n\t// Drain any in-flight requests so they don't hang refs to disposed iframe.\n\tconst dispose = () => {\n\t\tfor (const reject of pendingFetchErr.values()) reject(new Error(\"sandbox disposed\"));\n\t\tfor (const reject of pendingClipErr.values()) reject(new Error(\"sandbox disposed\"));\n\t\tfor (const reject of pendingNotifErr.values()) reject(new Error(\"sandbox disposed\"));\n\t\tpendingFetch.clear();\n\t\tpendingFetchErr.clear();\n\t\tpendingClip.clear();\n\t\tpendingClipErr.clear();\n\t\tpendingNotif.clear();\n\t\tpendingNotifErr.clear();\n\t\t(typeof window !== \"undefined\" ? window : (doc.defaultView as Window))\n\t\t\t?.removeEventListener(\"message\", messageHandler);\n\t\tiframe.remove();\n\t\tURL.revokeObjectURL(iframe.src);\n\t};\n\n\treturn {\n\t\tpluginId,\n\t\tcapabilities: granted,\n\t\texported,\n\t\tdispose,\n\t};\n}\n\n// ── Helpers (shared with host.ts but inlined here to avoid export churn) ─────\n\nfunction extractHost(rawUrl: string): string | null {\n\ttry {\n\t\treturn new URL(rawUrl, \"http://placeholder\").host;\n\t}\n\tcatch {\n\t\treturn null;\n\t}\n}\n\n// ── Frame runtime ────────────────────────────────────────────────────────────\n\n/**\n * HTML+JS that runs inside the iframe. Imports the plugin bundle by URL\n * (in the iframe's null origin — no cookies, no localStorage), proxies\n * fetch/clipboard/notifications back to the host via postMessage, and\n * posts the plugin's default export once it loads.\n *\n * Kept inline so the package has zero runtime files — every consumer\n * picks up the latest version on a single import.\n */\nfunction buildFrameSource(artifactUrl: string): string {\n\t// The frame's globals: a guarded `pluginHost` mirror, ready for\n\t// plugins to import. The plugin code reads `globalThis.pluginHost`.\n\treturn `<!doctype html>\n<html><head><meta charset=\"utf-8\"></head><body>\n<script type=\"module\">\nconst pending = new Map();\nlet nextId = 0;\nfunction rpc(kind, payload) {\n return new Promise((resolve, reject) => {\n const id = String(++nextId);\n pending.set(id, { resolve, reject });\n parent.postMessage({ v: 1, kind, id, ...payload }, '*');\n });\n}\nwindow.addEventListener('message', (e) => {\n const msg = e.data;\n if (!msg || msg.v !== 1) return;\n if (msg.kind === 'init') {\n globalThis.__abraPluginContext = {\n pluginId: msg.pluginId,\n capabilities: new Set(msg.capabilities),\n };\n return;\n }\n if (msg.id && pending.has(msg.id)) {\n const { resolve, reject } = pending.get(msg.id);\n pending.delete(msg.id);\n if (msg.kind === 'error') reject(new Error(msg.message));\n else resolve(msg);\n }\n});\nglobalThis.pluginHost = {\n async fetch(url, init) {\n const r = await rpc('fetch.request', { url: String(url), init });\n return new Response(r.body, { status: r.status });\n },\n clipboard: {\n readText() { return rpc('clipboard.read.request', {}).then(r => r.value); },\n writeText(text) { return rpc('clipboard.write.request', { text }).then(() => undefined); },\n },\n notifications: {\n show(title, options) { return rpc('notification.request', { title, options }).then(() => undefined); },\n },\n};\nparent.postMessage({ v: 1, kind: 'ready' }, '*');\ntry {\n const mod = await import(${JSON.stringify(artifactUrl)});\n const value = mod && (mod.default ?? mod);\n // Strip functions before posting — they don't survive postMessage.\n // We send the plain serialisable bits; functions stay in the iframe\n // and the host calls them via separate RPC channels (future work).\n const serialisable = JSON.parse(JSON.stringify(value, (_, v) =>\n typeof v === 'function' ? '[Function]' : v));\n parent.postMessage({ v: 1, kind: 'plugin.export', value: serialisable }, '*');\n} catch (e) {\n parent.postMessage({ v: 1, kind: 'plugin.error', message: String((e && e.message) || e) }, '*');\n}\n</script></body></html>`;\n}\n"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -368,61 +368,6 @@ declare class PluginRegistry<P extends AbraPlugin = AbraPlugin> {
|
|
|
368
368
|
getAllKeyboardShortcuts(): KeyboardShortcutOf<P>[];
|
|
369
369
|
}
|
|
370
370
|
//#endregion
|
|
371
|
-
//#region packages/plugin/src/source-spec.d.ts
|
|
372
|
-
/**
|
|
373
|
-
* Plugin source specification — the user-typed shorthand that resolves to a
|
|
374
|
-
* loadable URL.
|
|
375
|
-
*
|
|
376
|
-
* Today the registry server (Phase C) hosts artifacts at canonical URLs.
|
|
377
|
-
* Until then, both `cou-shell` and `@abraca/nuxt` accept three forms:
|
|
378
|
-
*
|
|
379
|
-
* - `npm:<package>[@version]` → jsDelivr npm CDN
|
|
380
|
-
* - `github:<user>/<repo>[@ref]` → jsDelivr GitHub CDN
|
|
381
|
-
* - any other string → returned as-is (must be `https://` or `http://localhost`)
|
|
382
|
-
*
|
|
383
|
-
* Once Phase C ships we add:
|
|
384
|
-
*
|
|
385
|
-
* - `abra:<plugin-id>[@version]` → the official registry
|
|
386
|
-
*
|
|
387
|
-
* The output URL points at the plugin's bundle entry. Convention is
|
|
388
|
-
* `<root>/dist/plugin.js`.
|
|
389
|
-
*/
|
|
390
|
-
/** A locally-installed external plugin record (persisted to host storage). */
|
|
391
|
-
interface ExternalPluginEntry {
|
|
392
|
-
/** Resolved fetch URL — the output of `normalizePluginUrl`. */
|
|
393
|
-
url: string;
|
|
394
|
-
/** Plugin `name` from the loaded bundle's manifest / default export. */
|
|
395
|
-
name: string;
|
|
396
|
-
label?: string;
|
|
397
|
-
version?: string;
|
|
398
|
-
description?: string;
|
|
399
|
-
enabled: boolean;
|
|
400
|
-
/** Most recent load error, if any. Cleared on next successful load. */
|
|
401
|
-
error?: string;
|
|
402
|
-
installedAt: number;
|
|
403
|
-
/**
|
|
404
|
-
* Integrity hash from the plugin's manifest, if known. Verified on every
|
|
405
|
-
* load — a silent change indicates supply-chain tampering and the host
|
|
406
|
-
* should refuse to instantiate the plugin.
|
|
407
|
-
*/
|
|
408
|
-
integrity?: string;
|
|
409
|
-
}
|
|
410
|
-
/**
|
|
411
|
-
* Resolve a user-typed plugin spec to a fetchable URL.
|
|
412
|
-
*
|
|
413
|
-
* - `npm:foo@1.2.3` → `https://cdn.jsdelivr.net/npm/foo@1.2.3/dist/plugin.js`
|
|
414
|
-
* - `github:org/repo@main` → `https://cdn.jsdelivr.net/gh/org/repo@main/dist/plugin.js`
|
|
415
|
-
* - anything else → returned trimmed, unchanged
|
|
416
|
-
*/
|
|
417
|
-
declare function normalizePluginUrl(input: string): string;
|
|
418
|
-
/**
|
|
419
|
-
* Reject any URL that isn't HTTPS or localhost over HTTP. Hosts should call
|
|
420
|
-
* this before importing — `normalizePluginUrl` does not enforce the policy
|
|
421
|
-
* (npm:/github: shorthand always produces HTTPS, so callers only need this
|
|
422
|
-
* for raw third-party URLs).
|
|
423
|
-
*/
|
|
424
|
-
declare function isSafePluginUrl(url: string): boolean;
|
|
425
|
-
//#endregion
|
|
426
371
|
//#region packages/plugin/src/manifest.d.ts
|
|
427
372
|
/**
|
|
428
373
|
* Plugin manifest v1 — the declarative `manifest.json` colocated with a
|
|
@@ -533,6 +478,95 @@ interface PluginRegistryEntry {
|
|
|
533
478
|
scorecard: unknown | null;
|
|
534
479
|
}
|
|
535
480
|
//#endregion
|
|
481
|
+
//#region packages/plugin/src/source-spec.d.ts
|
|
482
|
+
/**
|
|
483
|
+
* How an entry got onto the user's machine. Drives the trust UI:
|
|
484
|
+
* `registry` is silently trusted; `url` and `upload` require the explicit
|
|
485
|
+
* untrusted-code dialog; `space` was declared by the active space and gets
|
|
486
|
+
* auto-loaded only when the same id exists in the registry — otherwise it
|
|
487
|
+
* routes through the same untrusted gate as `url`/`upload`.
|
|
488
|
+
*/
|
|
489
|
+
type PluginOrigin = "registry" | "url" | "upload" | "space";
|
|
490
|
+
/** A locally-installed external plugin record (persisted to host storage). */
|
|
491
|
+
interface ExternalPluginEntry {
|
|
492
|
+
/** Resolved fetch URL — the output of `normalizePluginUrl`. */
|
|
493
|
+
url: string;
|
|
494
|
+
/** Plugin `name` from the loaded bundle's manifest / default export. */
|
|
495
|
+
name: string;
|
|
496
|
+
label?: string;
|
|
497
|
+
version?: string;
|
|
498
|
+
description?: string;
|
|
499
|
+
enabled: boolean;
|
|
500
|
+
/** Most recent load error, if any. Cleared on next successful load. */
|
|
501
|
+
error?: string;
|
|
502
|
+
installedAt: number;
|
|
503
|
+
/**
|
|
504
|
+
* Integrity hash from the plugin's manifest, if known. Verified on every
|
|
505
|
+
* load — a silent change indicates supply-chain tampering and the host
|
|
506
|
+
* should refuse to instantiate the plugin.
|
|
507
|
+
*/
|
|
508
|
+
integrity?: string;
|
|
509
|
+
/**
|
|
510
|
+
* How this entry was installed. Absent on records written before the
|
|
511
|
+
* trust-origin field was introduced — treat as `"url"` (the only pre-
|
|
512
|
+
* existing external-install path) when migrating.
|
|
513
|
+
*/
|
|
514
|
+
origin?: PluginOrigin;
|
|
515
|
+
/**
|
|
516
|
+
* Registry id (`PluginManifest.id`), set on `origin: "registry"` and
|
|
517
|
+
* for `"space"` entries the host has reconciled against the catalog.
|
|
518
|
+
* Lets the catalog's auto-update + decline-memory keys be id-based
|
|
519
|
+
* instead of url-based.
|
|
520
|
+
*/
|
|
521
|
+
id?: string;
|
|
522
|
+
/**
|
|
523
|
+
* SHA-256 of the uploaded bundle bytes (hex). Set on `origin: "upload"`
|
|
524
|
+
* only — registry entries use `integrity`, URL entries have no hash.
|
|
525
|
+
* Used as the IndexedDB blob key for restoring the uploaded artifact
|
|
526
|
+
* across reloads.
|
|
527
|
+
*/
|
|
528
|
+
sha256?: string;
|
|
529
|
+
/**
|
|
530
|
+
* Wall-clock when the user accepted the untrusted-code warning for
|
|
531
|
+
* this entry. Set on every `"url"` / `"upload"` install and on every
|
|
532
|
+
* `"space"` install that fell back to the untrusted path. Absent on
|
|
533
|
+
* `"registry"` installs (registry trust is implicit).
|
|
534
|
+
*
|
|
535
|
+
* Cleared together with `acknowledgedVersion` /
|
|
536
|
+
* `acknowledgedCapabilities` whenever the dialog re-prompts.
|
|
537
|
+
*/
|
|
538
|
+
trustAcknowledgedAt?: number;
|
|
539
|
+
/**
|
|
540
|
+
* Version the user acknowledged when last accepting the warning. The
|
|
541
|
+
* trust dialog re-prompts whenever the manifest version no longer
|
|
542
|
+
* matches this — see the `"Per id+version, AND when capabilities grow"`
|
|
543
|
+
* trust-scope decision in the plugin-browser plan.
|
|
544
|
+
*/
|
|
545
|
+
acknowledgedVersion?: string;
|
|
546
|
+
/**
|
|
547
|
+
* Required-capability set the user acknowledged at install time. The
|
|
548
|
+
* dialog re-prompts whenever the current manifest declares a required
|
|
549
|
+
* capability not in this set, even within the same version (mirrors
|
|
550
|
+
* `require_review_on_cap_growth` in the server policy).
|
|
551
|
+
*/
|
|
552
|
+
acknowledgedCapabilities?: readonly PluginCapability[];
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Resolve a user-typed plugin spec to a fetchable URL.
|
|
556
|
+
*
|
|
557
|
+
* - `npm:foo@1.2.3` → `https://cdn.jsdelivr.net/npm/foo@1.2.3/dist/plugin.js`
|
|
558
|
+
* - `github:org/repo@main` → `https://cdn.jsdelivr.net/gh/org/repo@main/dist/plugin.js`
|
|
559
|
+
* - anything else → returned trimmed, unchanged
|
|
560
|
+
*/
|
|
561
|
+
declare function normalizePluginUrl(input: string): string;
|
|
562
|
+
/**
|
|
563
|
+
* Reject any URL that isn't HTTPS or localhost over HTTP. Hosts should call
|
|
564
|
+
* this before importing — `normalizePluginUrl` does not enforce the policy
|
|
565
|
+
* (npm:/github: shorthand always produces HTTPS, so callers only need this
|
|
566
|
+
* for raw third-party URLs).
|
|
567
|
+
*/
|
|
568
|
+
declare function isSafePluginUrl(url: string): boolean;
|
|
569
|
+
//#endregion
|
|
536
570
|
//#region packages/plugin/src/host.d.ts
|
|
537
571
|
/**
|
|
538
572
|
* Thrown when a plugin tries to use an API it didn't declare in its
|
|
@@ -639,4 +673,4 @@ interface SandboxOptions extends PluginHostOptions {
|
|
|
639
673
|
*/
|
|
640
674
|
declare function loadSandboxedPlugin(manifest: Pick<PluginManifest, "id" | "capabilities">, artifactUrl: string, options?: SandboxOptions): Promise<SandboxedPlugin>;
|
|
641
675
|
//#endregion
|
|
642
|
-
export { type AbraAwarenessContribution, type AbraCommandItem, type AbraKeyboardShortcut, type AbraMentionItem, type AbraMentionProvider, type AbraNodePanelSlot, type AbraPlugin, type AbraSettingsPanel, type AbraTiptapExtension, type AbraYDoc, CapabilityDenied, type ClientPluginCtx, type CollaborationUser, type CommandPaletteCtx, type DragHandlePluginCtx, type EditorPluginCtx, type EditorRefLike, type ExternalPluginEntry, type GuardedClipboard, type GuardedFetch, type GuardedNotifications, type MentionPluginCtx, type NodePanelCtx, type PluginCapability, type PluginHost, type PluginHostOptions, type PluginManifest, type PluginManifestAuthor, type PluginManifestContributes, type PluginPricing, PluginRegistry, type PluginRegistryEntry, type PluginVersionStatus, type RefLike, type RunnerCleanup, type SandboxOptions, type SandboxedPlugin, type ServerRunnerContextBase, type ServerRunnerDefinition, createPluginHost, isSafePluginUrl, loadSandboxedPlugin, matchesNetworkCapability, normalizePluginUrl };
|
|
676
|
+
export { type AbraAwarenessContribution, type AbraCommandItem, type AbraKeyboardShortcut, type AbraMentionItem, type AbraMentionProvider, type AbraNodePanelSlot, type AbraPlugin, type AbraSettingsPanel, type AbraTiptapExtension, type AbraYDoc, CapabilityDenied, type ClientPluginCtx, type CollaborationUser, type CommandPaletteCtx, type DragHandlePluginCtx, type EditorPluginCtx, type EditorRefLike, type ExternalPluginEntry, type GuardedClipboard, type GuardedFetch, type GuardedNotifications, type MentionPluginCtx, type NodePanelCtx, type PluginCapability, type PluginHost, type PluginHostOptions, type PluginManifest, type PluginManifestAuthor, type PluginManifestContributes, type PluginOrigin, type PluginPricing, PluginRegistry, type PluginRegistryEntry, type PluginVersionStatus, type RefLike, type RunnerCleanup, type SandboxOptions, type SandboxedPlugin, type ServerRunnerContextBase, type ServerRunnerDefinition, createPluginHost, isSafePluginUrl, loadSandboxedPlugin, matchesNetworkCapability, normalizePluginUrl };
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
package/src/source-spec.ts
CHANGED
|
@@ -17,6 +17,17 @@
|
|
|
17
17
|
* `<root>/dist/plugin.js`.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
+
import type { PluginCapability } from "./manifest.ts";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* How an entry got onto the user's machine. Drives the trust UI:
|
|
24
|
+
* `registry` is silently trusted; `url` and `upload` require the explicit
|
|
25
|
+
* untrusted-code dialog; `space` was declared by the active space and gets
|
|
26
|
+
* auto-loaded only when the same id exists in the registry — otherwise it
|
|
27
|
+
* routes through the same untrusted gate as `url`/`upload`.
|
|
28
|
+
*/
|
|
29
|
+
export type PluginOrigin = "registry" | "url" | "upload" | "space";
|
|
30
|
+
|
|
20
31
|
/** A locally-installed external plugin record (persisted to host storage). */
|
|
21
32
|
export interface ExternalPluginEntry {
|
|
22
33
|
/** Resolved fetch URL — the output of `normalizePluginUrl`. */
|
|
@@ -36,6 +47,51 @@ export interface ExternalPluginEntry {
|
|
|
36
47
|
* should refuse to instantiate the plugin.
|
|
37
48
|
*/
|
|
38
49
|
integrity?: string;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* How this entry was installed. Absent on records written before the
|
|
53
|
+
* trust-origin field was introduced — treat as `"url"` (the only pre-
|
|
54
|
+
* existing external-install path) when migrating.
|
|
55
|
+
*/
|
|
56
|
+
origin?: PluginOrigin;
|
|
57
|
+
/**
|
|
58
|
+
* Registry id (`PluginManifest.id`), set on `origin: "registry"` and
|
|
59
|
+
* for `"space"` entries the host has reconciled against the catalog.
|
|
60
|
+
* Lets the catalog's auto-update + decline-memory keys be id-based
|
|
61
|
+
* instead of url-based.
|
|
62
|
+
*/
|
|
63
|
+
id?: string;
|
|
64
|
+
/**
|
|
65
|
+
* SHA-256 of the uploaded bundle bytes (hex). Set on `origin: "upload"`
|
|
66
|
+
* only — registry entries use `integrity`, URL entries have no hash.
|
|
67
|
+
* Used as the IndexedDB blob key for restoring the uploaded artifact
|
|
68
|
+
* across reloads.
|
|
69
|
+
*/
|
|
70
|
+
sha256?: string;
|
|
71
|
+
/**
|
|
72
|
+
* Wall-clock when the user accepted the untrusted-code warning for
|
|
73
|
+
* this entry. Set on every `"url"` / `"upload"` install and on every
|
|
74
|
+
* `"space"` install that fell back to the untrusted path. Absent on
|
|
75
|
+
* `"registry"` installs (registry trust is implicit).
|
|
76
|
+
*
|
|
77
|
+
* Cleared together with `acknowledgedVersion` /
|
|
78
|
+
* `acknowledgedCapabilities` whenever the dialog re-prompts.
|
|
79
|
+
*/
|
|
80
|
+
trustAcknowledgedAt?: number;
|
|
81
|
+
/**
|
|
82
|
+
* Version the user acknowledged when last accepting the warning. The
|
|
83
|
+
* trust dialog re-prompts whenever the manifest version no longer
|
|
84
|
+
* matches this — see the `"Per id+version, AND when capabilities grow"`
|
|
85
|
+
* trust-scope decision in the plugin-browser plan.
|
|
86
|
+
*/
|
|
87
|
+
acknowledgedVersion?: string;
|
|
88
|
+
/**
|
|
89
|
+
* Required-capability set the user acknowledged at install time. The
|
|
90
|
+
* dialog re-prompts whenever the current manifest declares a required
|
|
91
|
+
* capability not in this set, even within the same version (mirrors
|
|
92
|
+
* `require_review_on_cap_growth` in the server policy).
|
|
93
|
+
*/
|
|
94
|
+
acknowledgedCapabilities?: readonly PluginCapability[];
|
|
39
95
|
}
|
|
40
96
|
|
|
41
97
|
/**
|