@stackable-labs/sdk-extension-host 1.0.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/LICENSE ADDED
@@ -0,0 +1,29 @@
1
+ LICENSE
2
+
3
+ Copyright (c) 2026-present UNIQUELY PARTICULAR LLC, STACKABLE LABS, LLC, agnoStack, Inc., and Adam Grohs ("Author" herein)
4
+
5
+ Permission is hereby granted to use, copy, and modify this software
6
+ (the "Software") solely for the purpose of developing, testing,
7
+ or maintaining integration with Author's products and services.
8
+
9
+ The Software may not be used:
10
+ - as a standalone product or service,
11
+ - to integrate with any platform or service other than Author's,
12
+ - to build or enhance a competing product or service,
13
+ - or for any purpose unrelated to integrations with Author.
14
+
15
+ Redistribution of the Software, in whole or in part, is permitted
16
+ only as part of an application or service that integrates with Author
17
+ and does not expose the Software as a general-purpose library.
18
+
19
+ This Software is provided "AS IS", without warranty of any kind, express
20
+ or implied, including but not limited to the warranties of merchantability,
21
+ fitness for a particular purpose, and noninfringement.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ PUBLISHER, AUTHORS, ANY CONTRIBUTOR OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
27
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
28
+ OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
29
+ USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # @stackable-labs/sdk-extension-host
2
+
3
+ Host-side SDK for creating an app that allows embeddable slots via Stackable extensions.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @stackable-labs/sdk-extension-host
9
+ ```
10
+
11
+ ## Peer dependencies
12
+
13
+ ```
14
+ react >= 18.0.0 < 19.0.0
15
+ react-dom >= 18.0.0 < 19.0.0
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ Wrap your app with `ExtensionProvider` and place `ExtensionSlot` wherever extension UI should appear:
21
+
22
+ ```tsx
23
+ import { ExtensionProvider, ExtensionSlot, registerComponents } from '@stackable-labs/sdk-extension-host';
24
+
25
+ // Register host components once at app startup (before any ExtensionSlot renders)
26
+ registerComponents({ 'ui-button': Button, 'ui-text': Text });
27
+
28
+ function App() {
29
+ return (
30
+ <ExtensionProvider extensions={extensions} capabilityHandlers={handlers}>
31
+ <ExtensionSlot target="slot.header" />
32
+ <MainContent />
33
+ <ExtensionSlot target="slot.content" />
34
+ </ExtensionProvider>
35
+ );
36
+ }
37
+ ```
38
+
39
+ ## Key exports
40
+
41
+ - **`ExtensionProvider`** — React context provider; manages extension sandboxes
42
+ - **`ExtensionSlot`** — renders extension-produced UI into a named host slot
43
+ - **`registerComponents`** — register allowed host components extensions may render
44
+ - **`CapabilityRPCHandler`** — handles capability requests from extension sandboxes
45
+ - **`SandboxManager`** — creates and destroys hidden iframe sandboxes per extension
46
+
47
+ ## Changelog
48
+
49
+ See [npm version history](https://www.npmjs.com/package/@stackable-labs/sdk-extension-host?activeTab=versions).
50
+
51
+ ## License
52
+
53
+ SEE LICENSE IN [LICENSE](./LICENSE)
@@ -0,0 +1,15 @@
1
+ /**
2
+ * CapabilityRPCHandler — receives capability calls from sandbox,
3
+ * enforces permissions, and executes them on the host side.
4
+ */
5
+ import type { ApiRequest, ToastPayload, ActionInvokePayload } from '@stackable-labs/sdk-extension-contracts';
6
+ export interface CapabilityHandlers {
7
+ 'data.query': (payload: ApiRequest) => Promise<unknown>;
8
+ 'actions.toast': (payload: ToastPayload) => Promise<void>;
9
+ 'actions.invoke': (payload: ActionInvokePayload) => Promise<unknown>;
10
+ 'context.read': () => Promise<Record<string, unknown>>;
11
+ }
12
+ /**
13
+ * Create an RPC handler that listens for capability requests from sandboxes.
14
+ */
15
+ export declare const createCapabilityRPCHandler: (handlers: CapabilityHandlers) => () => void;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * CapabilityRPCHandler — receives capability calls from sandbox,
3
+ * enforces permissions, and executes them on the host side.
4
+ */
5
+ import { CAPABILITY_PERMISSION_MAP } from '@stackable-labs/sdk-extension-contracts';
6
+ import { getAllSandboxes, getSandbox, postToSandbox } from './SandboxManager';
7
+ /**
8
+ * Create an RPC handler that listens for capability requests from sandboxes.
9
+ */
10
+ export const createCapabilityRPCHandler = (handlers) => {
11
+ const handleMessage = async (event) => {
12
+ const msg = event.data;
13
+ if (msg?.type !== 'capability-request')
14
+ return;
15
+ const request = msg;
16
+ // Find which sandbox sent this request
17
+ let extensionId = null;
18
+ for (const [id, sandbox] of getAllSandboxes()) {
19
+ if (event.source === sandbox.iframe.contentWindow) {
20
+ extensionId = id;
21
+ break;
22
+ }
23
+ }
24
+ if (!extensionId) {
25
+ console.warn('Received capability request from unknown source');
26
+ return;
27
+ }
28
+ const sandbox = getSandbox(extensionId);
29
+ if (!sandbox)
30
+ return;
31
+ // Check permissions
32
+ const requiredPermission = CAPABILITY_PERMISSION_MAP[request.capability];
33
+ if (requiredPermission && !sandbox.manifest.permissions.includes(requiredPermission)) {
34
+ const response = {
35
+ type: 'capability-response',
36
+ id: request.id,
37
+ success: false,
38
+ error: `Permission denied: '${requiredPermission}' not granted for extension '${extensionId}'`,
39
+ };
40
+ postToSandbox(extensionId, response);
41
+ return;
42
+ }
43
+ // Execute the capability
44
+ try {
45
+ let result;
46
+ switch (request.capability) {
47
+ case 'data.query':
48
+ result = await handlers['data.query'](request.payload);
49
+ break;
50
+ case 'actions.toast':
51
+ result = await handlers['actions.toast'](request.payload);
52
+ break;
53
+ case 'actions.invoke':
54
+ result = await handlers['actions.invoke'](request.payload);
55
+ break;
56
+ case 'context.read':
57
+ result = await handlers['context.read']();
58
+ break;
59
+ default:
60
+ throw new Error(`Unknown capability: ${request.capability}`);
61
+ }
62
+ const response = {
63
+ type: 'capability-response',
64
+ id: request.id,
65
+ success: true,
66
+ data: result,
67
+ };
68
+ postToSandbox(extensionId, response);
69
+ }
70
+ catch (err) {
71
+ const response = {
72
+ type: 'capability-response',
73
+ id: request.id,
74
+ success: false,
75
+ error: err instanceof Error ? err.message : 'Unknown error',
76
+ };
77
+ postToSandbox(extensionId, response);
78
+ }
79
+ };
80
+ window.addEventListener('message', handleMessage);
81
+ return () => {
82
+ window.removeEventListener('message', handleMessage);
83
+ };
84
+ };
85
+ //# sourceMappingURL=CapabilityRPCHandler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CapabilityRPCHandler.js","sourceRoot":"","sources":["../src/CapabilityRPCHandler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * ComponentRegistry — maps Remote DOM custom element tags to host React components.
3
+ * Unknown tags are rejected (dropped or replaced with a warning stub).
4
+ */
5
+ import type { UITag } from '@stackable-labs/sdk-extension-contracts';
6
+ type ComponentMap = Map<string, React.ComponentType<Record<string, unknown>>>;
7
+ /**
8
+ * Register a host React component for a UI tag.
9
+ */
10
+ export declare const registerComponent: (tag: UITag, component: React.ComponentType<Record<string, unknown>>) => void;
11
+ /**
12
+ * Register multiple components at once.
13
+ */
14
+ export declare const registerComponents: (components: Partial<Record<UITag, React.ComponentType<Record<string, unknown>>>>) => void;
15
+ /**
16
+ * Check if a tag is in the UI contract.
17
+ */
18
+ export declare const isValidTag: (tag: string) => tag is UITag;
19
+ /**
20
+ * Look up the host component for a given tag.
21
+ * Returns undefined if the tag is not in the contract (unknown tag → rejected).
22
+ */
23
+ export declare const getComponent: (tag: string) => React.ComponentType<Record<string, unknown>> | undefined;
24
+ /**
25
+ * Get all registered components.
26
+ */
27
+ export declare const getRegistry: () => ComponentMap;
28
+ export {};
@@ -0,0 +1,43 @@
1
+ /**
2
+ * ComponentRegistry — maps Remote DOM custom element tags to host React components.
3
+ * Unknown tags are rejected (dropped or replaced with a warning stub).
4
+ */
5
+ import { UI_TAGS } from '@stackable-labs/sdk-extension-contracts';
6
+ const registry = new Map();
7
+ /**
8
+ * Register a host React component for a UI tag.
9
+ */
10
+ export const registerComponent = (tag, component) => {
11
+ registry.set(tag, component);
12
+ };
13
+ /**
14
+ * Register multiple components at once.
15
+ */
16
+ export const registerComponents = (components) => {
17
+ for (const [tag, component] of Object.entries(components)) {
18
+ if (component) {
19
+ registry.set(tag, component);
20
+ }
21
+ }
22
+ };
23
+ /**
24
+ * Check if a tag is in the UI contract.
25
+ */
26
+ export const isValidTag = (tag) => UI_TAGS.includes(tag);
27
+ /**
28
+ * Look up the host component for a given tag.
29
+ * Returns undefined if the tag is not in the contract (unknown tag → rejected).
30
+ */
31
+ export const getComponent = (tag) => {
32
+ // Only allow tags that are in the contract
33
+ if (!isValidTag(tag)) {
34
+ console.warn(`[ExtensionHost] Unknown UI tag rejected: <${tag}>`);
35
+ return undefined;
36
+ }
37
+ return registry.get(tag);
38
+ };
39
+ /**
40
+ * Get all registered components.
41
+ */
42
+ export const getRegistry = () => registry;
43
+ //# sourceMappingURL=ComponentRegistry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ComponentRegistry.js","sourceRoot":"","sources":["../src/ComponentRegistry.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAA;AAIjE,MAAM,QAAQ,GAAiB,IAAI,GAAG,EAAE,CAAA;AAExC;;GAEG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,GAAU,EAAE,SAAuD,EAAQ,EAAE;IAC7G,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,SAAS,CAAC,CAAA;AAC9B,CAAC,CAAA;AAED;;GAEG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,UAAgF,EAAQ,EAAE;IAC3H,KAAK,MAAM,CAAC,GAAG,EAAE,SAAS,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC;QAC1D,IAAI,SAAS,EAAE,CAAC;YACd,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,SAAS,CAAC,CAAA;QAC9B,CAAC;IACH,CAAC;AACH,CAAC,CAAA;AAED;;GAEG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,GAAW,EAAgB,EAAE,CAAE,OAA6B,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAA;AAErG;;;GAGG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,GAAW,EAA4D,EAAE;IACpG,2CAA2C;IAC3C,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACrB,OAAO,CAAC,IAAI,CAAC,6CAA6C,GAAG,GAAG,CAAC,CAAA;QACjE,OAAO,SAAS,CAAA;IAClB,CAAC;IACD,OAAO,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;AAC1B,CAAC,CAAA;AAED;;GAEG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,GAAiB,EAAE,CAAC,QAAQ,CAAA"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * ExtensionProvider — React context provider that manages extension sandboxes.
3
+ * Place at the app root so runtimes persist across navigation.
4
+ */
5
+ import React from 'react';
6
+ import type { ExtensionRegistryEntry } from '@stackable-labs/sdk-extension-contracts';
7
+ import type { CapabilityHandlers } from './CapabilityRPCHandler';
8
+ interface ExtensionProviderContextValue {
9
+ extensions: ExtensionRegistryEntry[];
10
+ ready: boolean;
11
+ }
12
+ interface ExtensionProviderProps {
13
+ extensions: ExtensionRegistryEntry[];
14
+ capabilityHandlers: CapabilityHandlers;
15
+ children: React.ReactNode;
16
+ }
17
+ export declare const ExtensionProvider: ({ extensions, capabilityHandlers, children, }: ExtensionProviderProps) => import("react/jsx-runtime").JSX.Element;
18
+ export declare const useExtensionProvider: () => ExtensionProviderContextValue;
19
+ export {};
@@ -0,0 +1,55 @@
1
+ /**
2
+ * ExtensionProvider — React context provider that manages extension sandboxes.
3
+ * Place at the app root so runtimes persist across navigation.
4
+ */
5
+ 'use client';
6
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
7
+ import { createContext, useContext, useEffect, useRef, useState } from 'react';
8
+ import { createSandbox, destroySandbox } from './SandboxManager';
9
+ import { createCapabilityRPCHandler } from './CapabilityRPCHandler';
10
+ const ExtensionProviderContext = createContext({
11
+ extensions: [],
12
+ ready: false,
13
+ });
14
+ export const ExtensionProvider = ({ extensions, capabilityHandlers, children, }) => {
15
+ const [ready, setReady] = useState(false);
16
+ const sandboxContainerRef = useRef(null);
17
+ const cleanupRef = useRef(null);
18
+ useEffect(() => {
19
+ if (!sandboxContainerRef.current)
20
+ return;
21
+ let cancelled = false;
22
+ const init = async () => {
23
+ const container = sandboxContainerRef.current;
24
+ if (!container)
25
+ return;
26
+ console.log('[ExtensionProvider] Initializing with', extensions.length, 'extensions');
27
+ // Create sandboxes for each enabled extension
28
+ for (const ext of extensions) {
29
+ if (ext.enabled && !cancelled) {
30
+ console.log('[ExtensionProvider] Creating sandbox for', ext.id, 'bundleUrl:', ext.bundleUrl);
31
+ await createSandbox(ext, container);
32
+ console.log('[ExtensionProvider] Sandbox created for', ext.id);
33
+ }
34
+ }
35
+ // Set up the capability RPC handler
36
+ cleanupRef.current = createCapabilityRPCHandler(capabilityHandlers);
37
+ if (!cancelled) {
38
+ console.log('[ExtensionProvider] Ready');
39
+ setReady(true);
40
+ }
41
+ };
42
+ init();
43
+ return () => {
44
+ cancelled = true;
45
+ // Clean up sandboxes and RPC handler
46
+ for (const ext of extensions) {
47
+ destroySandbox(ext.id);
48
+ }
49
+ cleanupRef.current?.();
50
+ };
51
+ }, [extensions, capabilityHandlers]);
52
+ return (_jsxs(ExtensionProviderContext.Provider, { value: { extensions, ready }, children: [_jsx("div", { ref: sandboxContainerRef, style: { display: 'none' } }), children] }));
53
+ };
54
+ export const useExtensionProvider = () => useContext(ExtensionProviderContext);
55
+ //# sourceMappingURL=ExtensionProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExtensionProvider.js","sourceRoot":"","sources":["../src/ExtensionProvider.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,YAAY,CAAA;;AAEZ,OAAc,EAAE,aAAa,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAErF,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAChE,OAAO,EAAE,0BAA0B,EAAE,MAAM,wBAAwB,CAAA;AAQnE,MAAM,wBAAwB,GAAG,aAAa,CAAgC;IAC5E,UAAU,EAAE,EAAE;IACd,KAAK,EAAE,KAAK;CACb,CAAC,CAAA;AAQF,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,EAChC,UAAU,EACV,kBAAkB,EAClB,QAAQ,GACe,EAAE,EAAE;IAC3B,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAU,KAAK,CAAC,CAAA;IAClD,MAAM,mBAAmB,GAAG,MAAM,CAAiB,IAAI,CAAC,CAAA;IACxD,MAAM,UAAU,GAAG,MAAM,CAAsB,IAAI,CAAC,CAAA;IAEpD,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,mBAAmB,CAAC,OAAO;YAAE,OAAM;QAExC,IAAI,SAAS,GAAG,KAAK,CAAA;QAErB,MAAM,IAAI,GAAG,KAAK,IAAI,EAAE;YACtB,MAAM,SAAS,GAAG,mBAAmB,CAAC,OAAO,CAAA;YAC7C,IAAI,CAAC,SAAS;gBAAE,OAAM;YAEtB,OAAO,CAAC,GAAG,CAAC,uCAAuC,EAAE,UAAU,CAAC,MAAM,EAAE,YAAY,CAAC,CAAA;YAErF,8CAA8C;YAC9C,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;gBAC7B,IAAI,GAAG,CAAC,OAAO,IAAI,CAAC,SAAS,EAAE,CAAC;oBAC9B,OAAO,CAAC,GAAG,CAAC,0CAA0C,EAAE,GAAG,CAAC,EAAE,EAAE,YAAY,EAAE,GAAG,CAAC,SAAS,CAAC,CAAA;oBAC5F,MAAM,aAAa,CAAC,GAAG,EAAE,SAAS,CAAC,CAAA;oBACnC,OAAO,CAAC,GAAG,CAAC,yCAAyC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAA;gBAChE,CAAC;YACH,CAAC;YAED,oCAAoC;YACpC,UAAU,CAAC,OAAO,GAAG,0BAA0B,CAAC,kBAAkB,CAAC,CAAA;YAEnE,IAAI,CAAC,SAAS,EAAE,CAAC;gBACf,OAAO,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAA;gBACxC,QAAQ,CAAC,IAAI,CAAC,CAAA;YAChB,CAAC;QACH,CAAC,CAAA;QAED,IAAI,EAAE,CAAA;QAEN,OAAO,GAAG,EAAE;YACV,SAAS,GAAG,IAAI,CAAA;YAChB,qCAAqC;YACrC,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;gBAC7B,cAAc,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;YACxB,CAAC;YACD,UAAU,CAAC,OAAO,EAAE,EAAE,CAAA;QACxB,CAAC,CAAA;IACH,CAAC,EAAE,CAAC,UAAU,EAAE,kBAAkB,CAAC,CAAC,CAAA;IAEpC,OAAO,CACL,MAAC,wBAAwB,CAAC,QAAQ,IAAC,KAAK,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,aAE7D,cAAK,GAAG,EAAE,mBAAmB,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,GAAI,EAC5D,QAAQ,IACyB,CACrC,CAAA;AACH,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,oBAAoB,GAAG,GAAG,EAAE,CAAC,UAAU,CAAC,wBAAwB,CAAC,CAAA"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * ExtensionSlot — renders the output of an extension surface at a specific target.
3
+ * The host places these in its layout where extension UI should appear.
4
+ *
5
+ * Receives serialized DOM trees from the extension via postMessage (cross-origin safe)
6
+ * and renders them using the host component registry.
7
+ */
8
+ import React from 'react';
9
+ interface ExtensionSlotProps {
10
+ /** The extension point target (e.g., "slot.header") */
11
+ target: string;
12
+ /** Optional context to pass to the surface */
13
+ context?: Record<string, unknown>;
14
+ /** Optional className for the slot container */
15
+ className?: string;
16
+ /** Optional fallback content when no extension output is available */
17
+ fallback?: React.ReactNode;
18
+ /** Optional wrapper for each extension's rendered content */
19
+ render?: (params: {
20
+ extensionId: string;
21
+ children: React.ReactNode;
22
+ index: number;
23
+ total: number;
24
+ }) => React.ReactNode;
25
+ /** Optional separator rendered between extension outputs */
26
+ separator?: React.ReactNode | ((params: {
27
+ index: number;
28
+ total: number;
29
+ previousExtensionId: string;
30
+ extensionId: string;
31
+ }) => React.ReactNode);
32
+ }
33
+ /**
34
+ * Renders extension UI for a given target.
35
+ * Listens for serialized DOM trees from the extension sandbox via postMessage.
36
+ */
37
+ export declare const ExtensionSlot: ({ target, context, className, separator, fallback, render, }: ExtensionSlotProps) => import("react/jsx-runtime").JSX.Element | null;
38
+ export {};
@@ -0,0 +1,164 @@
1
+ /**
2
+ * ExtensionSlot — renders the output of an extension surface at a specific target.
3
+ * The host places these in its layout where extension UI should appear.
4
+ *
5
+ * Receives serialized DOM trees from the extension via postMessage (cross-origin safe)
6
+ * and renders them using the host component registry.
7
+ */
8
+ 'use client';
9
+ import { createElement as _createElement } from "react";
10
+ import { jsx as _jsx } from "react/jsx-runtime";
11
+ import React, { useEffect, useState, useCallback, useMemo, useRef } from 'react';
12
+ import { useExtensionProvider } from './ExtensionProvider';
13
+ import { getSandbox } from './SandboxManager';
14
+ import { getComponent } from './ComponentRegistry';
15
+ /**
16
+ * Renders extension UI for a given target.
17
+ * Listens for serialized DOM trees from the extension sandbox via postMessage.
18
+ */
19
+ export const ExtensionSlot = ({ target, context, className, separator, fallback = null, render, }) => {
20
+ const { extensions, ready } = useExtensionProvider();
21
+ const [surfaceContentByExtension, setSurfaceContentByExtension] = useState({});
22
+ const previousMatchingIdsRef = useRef('');
23
+ // Find extensions that target this slot — stabilize reference by ID list
24
+ const matchingExtensions = useMemo(() => extensions.filter((ext) => ext.enabled && ext.manifest.targets.includes(target)),
25
+ // eslint-disable-next-line react-hooks/exhaustive-deps
26
+ [extensions.map((e) => e.id).join(','), target]);
27
+ const renderSerializedNode = useCallback((node, sourceWindow, key = 0) => {
28
+ if (node.type === 'text') {
29
+ return node.text || null;
30
+ }
31
+ if (!node.tag)
32
+ return null;
33
+ // Fragment wrapper from Surface serialization
34
+ if (node.tag === '__fragment') {
35
+ return (_jsx(React.Fragment, { children: node.children?.map((child, i) => renderSerializedNode(child, sourceWindow, i)) }, key));
36
+ }
37
+ const HostComponent = getComponent(node.tag);
38
+ if (!HostComponent) {
39
+ // Unknown tag — rejected by contract
40
+ if (node.tag.startsWith('ui-')) {
41
+ console.warn(`[ExtensionSlot] Unknown UI tag rejected: <${node.tag}>`);
42
+ return null;
43
+ }
44
+ // For non-ui tags, just render children
45
+ return (_jsx(React.Fragment, { children: node.children?.map((child, i) => renderSerializedNode(child, sourceWindow, i)) }, key));
46
+ }
47
+ const props = { ...node.attrs };
48
+ // Wire up click handler proxy if the node has an actionId
49
+ if (node.actionId) {
50
+ const actionId = node.actionId;
51
+ props.onClick = () => {
52
+ const postTarget = sourceWindow;
53
+ postTarget?.postMessage({ type: 'action-invoke', surfaceId: target, actionId }, '*');
54
+ };
55
+ }
56
+ // Wire up onChange handler proxy if the node has a data-onchange-id
57
+ if (props['data-onchange-id']) {
58
+ const onchangeId = props['data-onchange-id'];
59
+ delete props['data-onchange-id'];
60
+ props.onChange = (e) => {
61
+ const postTarget = sourceWindow;
62
+ postTarget?.postMessage({ type: 'action-invoke', surfaceId: target, actionId: onchangeId, value: e.target.value }, '*');
63
+ };
64
+ }
65
+ const children = node.children?.map((child, i) => renderSerializedNode(child, sourceWindow, i));
66
+ return (_createElement(HostComponent, { ...props, key: key }, children && children.length > 0 ? children : undefined));
67
+ }, [target]);
68
+ useEffect(() => {
69
+ const matchingIds = matchingExtensions.map((ext) => ext.id).join(',');
70
+ if (previousMatchingIdsRef.current !== matchingIds) {
71
+ previousMatchingIdsRef.current = matchingIds;
72
+ setSurfaceContentByExtension({});
73
+ }
74
+ if (!ready || matchingExtensions.length === 0)
75
+ return;
76
+ // Listen for surface-update messages from extension sandboxes
77
+ const handleMessage = (event) => {
78
+ const msg = event.data;
79
+ if (!msg || typeof msg !== 'object')
80
+ return;
81
+ // Verify the message comes from one of our sandboxes
82
+ const isFromSandbox = matchingExtensions.some((ext) => {
83
+ const sandbox = getSandbox(ext.id);
84
+ return sandbox && event.source === sandbox.iframe.contentWindow;
85
+ });
86
+ if (!isFromSandbox)
87
+ return;
88
+ if (msg.type === 'surface-update' && msg.surfaceId === target) {
89
+ const senderExtension = matchingExtensions.find((ext) => {
90
+ const sandbox = getSandbox(ext.id);
91
+ return sandbox && event.source === sandbox.iframe.contentWindow;
92
+ });
93
+ if (!senderExtension)
94
+ return;
95
+ const rendered = renderSerializedNode(msg.tree, event.source ?? null);
96
+ setSurfaceContentByExtension((prev) => ({
97
+ ...prev,
98
+ [senderExtension.id]: rendered,
99
+ }));
100
+ }
101
+ if (msg.type === 'surface-ready' && msg.surfaceId === target) {
102
+ // Surface is ready, send initial context
103
+ if (context) {
104
+ matchingExtensions.forEach((ext) => {
105
+ const sandbox = getSandbox(ext.id);
106
+ sandbox?.iframe.contentWindow?.postMessage({ type: 'context-update', surfaceId: target, context }, '*');
107
+ });
108
+ }
109
+ }
110
+ };
111
+ window.addEventListener('message', handleMessage);
112
+ // Request the extension to re-send its current surface tree.
113
+ // This handles the case where the slot mounts after the extension
114
+ // has already sent its initial surface-update (e.g. widget popup).
115
+ // console.log(`[ExtensionSlot] Mounted for target="${target}", requesting re-send from ${matchingExtensions.length} extension(s)`);
116
+ matchingExtensions.forEach((ext) => {
117
+ const sandbox = getSandbox(ext.id);
118
+ sandbox?.iframe.contentWindow?.postMessage({ type: 'surface-render', surfaceId: target, context }, '*');
119
+ });
120
+ return () => {
121
+ window.removeEventListener('message', handleMessage);
122
+ };
123
+ }, [ready, matchingExtensions, target, context, renderSerializedNode]);
124
+ if (!ready) {
125
+ return null;
126
+ }
127
+ if (matchingExtensions.length === 0) {
128
+ return fallback ? (_jsx("div", { "data-extension-slot": target, title: target, className: className, children: fallback })) : null;
129
+ }
130
+ const extensionContent = matchingExtensions
131
+ .map((ext) => ({ id: ext.id, children: surfaceContentByExtension[ext.id] }))
132
+ .filter((entry) => Boolean(entry.children));
133
+ const content = extensionContent.length > 0
134
+ ? extensionContent.flatMap((entry, index, entries) => {
135
+ const renderedItem = render
136
+ ? render({
137
+ extensionId: entry.id,
138
+ children: entry.children,
139
+ index,
140
+ total: entries.length,
141
+ })
142
+ : _jsx(React.Fragment, { children: entry.children }, entry.id);
143
+ const item = _jsx(React.Fragment, { children: renderedItem }, `extension:${entry.id}`);
144
+ if (index === 0 || !separator) {
145
+ return [item];
146
+ }
147
+ const previous = entries[index - 1];
148
+ const separatorNode = typeof separator === 'function'
149
+ ? separator({
150
+ index,
151
+ total: entries.length,
152
+ previousExtensionId: previous.id,
153
+ extensionId: entry.id,
154
+ })
155
+ : separator;
156
+ return [
157
+ _jsx(React.Fragment, { children: separatorNode }, `separator:${previous.id}:${entry.id}:${index}`),
158
+ item
159
+ ];
160
+ })
161
+ : fallback;
162
+ return (_jsx("div", { "data-extension-slot": target, title: target, className: className, children: content }));
163
+ };
164
+ //# sourceMappingURL=ExtensionSlot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExtensionSlot.js","sourceRoot":"","sources":["../src/ExtensionSlot.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * SandboxManager — creates and manages one iframe per extension per tab.
3
+ */
4
+ import type { ExtensionRegistryEntry, HostToSandboxMessage, SandboxToHostMessage } from '@stackable-labs/sdk-extension-contracts';
5
+ export interface SandboxInstance {
6
+ extensionId: string;
7
+ iframe: HTMLIFrameElement;
8
+ manifest: ExtensionRegistryEntry['manifest'];
9
+ ready: boolean;
10
+ messageHandlers: Set<(msg: SandboxToHostMessage) => void>;
11
+ }
12
+ /**
13
+ * Create a sandbox iframe for an extension and load its bundle.
14
+ *
15
+ * If bundleUrl is a full URL (http://...), uses iframe.src pointing to
16
+ * a sandbox HTML page on that origin (dev mode with Vite).
17
+ * If bundleUrl is a relative path, fetches the bundle and inlines it in srcdoc.
18
+ */
19
+ export declare const createSandbox: (entry: ExtensionRegistryEntry, container: HTMLElement) => Promise<SandboxInstance>;
20
+ /**
21
+ * Send a message to a sandbox.
22
+ */
23
+ export declare const postToSandbox: (extensionId: string, message: HostToSandboxMessage) => void;
24
+ /**
25
+ * Get a sandbox instance by extension ID.
26
+ */
27
+ export declare const getSandbox: (extensionId: string) => SandboxInstance | undefined;
28
+ /**
29
+ * Destroy a sandbox and clean up.
30
+ */
31
+ export declare const destroySandbox: (extensionId: string) => void;
32
+ /**
33
+ * Get all active sandbox instances.
34
+ */
35
+ export declare const getAllSandboxes: () => Map<string, SandboxInstance>;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * SandboxManager — creates and manages one iframe per extension per tab.
3
+ */
4
+ const sandboxes = new Map();
5
+ /**
6
+ * Create a sandbox iframe for an extension and load its bundle.
7
+ *
8
+ * If bundleUrl is a full URL (http://...), uses iframe.src pointing to
9
+ * a sandbox HTML page on that origin (dev mode with Vite).
10
+ * If bundleUrl is a relative path, fetches the bundle and inlines it in srcdoc.
11
+ */
12
+ export const createSandbox = async (entry, container) => {
13
+ const existing = sandboxes.get(entry.id);
14
+ if (existing)
15
+ return existing;
16
+ const iframe = document.createElement('iframe');
17
+ iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin');
18
+ iframe.style.display = 'none';
19
+ iframe.style.width = '0';
20
+ iframe.style.height = '0';
21
+ iframe.style.border = 'none';
22
+ const isAbsoluteUrl = entry.bundleUrl.startsWith('http');
23
+ if (isAbsoluteUrl) {
24
+ // Dev mode: point iframe.src to a sandbox page served by the extension's dev server
25
+ iframe.src = entry.bundleUrl;
26
+ }
27
+ else {
28
+ // Production mode: fetch bundle and inline it
29
+ let bundleCode = '';
30
+ try {
31
+ const res = await fetch(entry.bundleUrl);
32
+ bundleCode = await res.text();
33
+ }
34
+ catch (err) {
35
+ console.error(`[SandboxManager] Failed to fetch bundle for ${entry.id}:`, err);
36
+ }
37
+ const sandboxHtml = `
38
+ <!DOCTYPE html>
39
+ <html>
40
+ <head><meta charset="utf-8"></head>
41
+ <body>
42
+ <div id="extension-root"></div>
43
+ <script type="module">${bundleCode}</script>
44
+ </body>
45
+ </html>
46
+ `;
47
+ iframe.srcdoc = sandboxHtml;
48
+ }
49
+ container.appendChild(iframe);
50
+ const instance = {
51
+ extensionId: entry.id,
52
+ iframe,
53
+ manifest: entry.manifest,
54
+ ready: false,
55
+ messageHandlers: new Set(),
56
+ };
57
+ // Listen for messages from this sandbox
58
+ const handleMessage = (event) => {
59
+ // Only accept messages from this iframe
60
+ if (event.source !== iframe.contentWindow)
61
+ return;
62
+ const msg = event.data;
63
+ if (msg?.type === 'extension-ready') {
64
+ instance.ready = true;
65
+ }
66
+ instance.messageHandlers.forEach((handler) => handler(msg));
67
+ };
68
+ window.addEventListener('message', handleMessage);
69
+ sandboxes.set(entry.id, instance);
70
+ return instance;
71
+ };
72
+ /**
73
+ * Send a message to a sandbox.
74
+ */
75
+ export const postToSandbox = (extensionId, message) => {
76
+ const instance = sandboxes.get(extensionId);
77
+ if (!instance?.iframe.contentWindow) {
78
+ console.warn(`Sandbox not found for extension: ${extensionId}`);
79
+ return;
80
+ }
81
+ instance.iframe.contentWindow.postMessage(message, '*');
82
+ };
83
+ /**
84
+ * Get a sandbox instance by extension ID.
85
+ */
86
+ export const getSandbox = (extensionId) => sandboxes.get(extensionId);
87
+ /**
88
+ * Destroy a sandbox and clean up.
89
+ */
90
+ export const destroySandbox = (extensionId) => {
91
+ const instance = sandboxes.get(extensionId);
92
+ if (instance) {
93
+ instance.iframe.remove();
94
+ instance.messageHandlers.clear();
95
+ sandboxes.delete(extensionId);
96
+ }
97
+ };
98
+ /**
99
+ * Get all active sandbox instances.
100
+ */
101
+ export const getAllSandboxes = () => sandboxes;
102
+ //# sourceMappingURL=SandboxManager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SandboxManager.js","sourceRoot":"","sources":["../src/SandboxManager.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ export { ExtensionProvider, useExtensionProvider } from './ExtensionProvider';
2
+ export { ExtensionSlot } from './ExtensionSlot';
3
+ export { createSandbox, getSandbox, destroySandbox, postToSandbox, } from './SandboxManager';
4
+ export { createCapabilityRPCHandler, type CapabilityHandlers, } from './CapabilityRPCHandler';
5
+ export { registerComponent, registerComponents, getComponent, isValidTag, } from './ComponentRegistry';
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { ExtensionProvider, useExtensionProvider } from './ExtensionProvider';
2
+ export { ExtensionSlot } from './ExtensionSlot';
3
+ export { createSandbox, getSandbox, destroySandbox, postToSandbox, } from './SandboxManager';
4
+ export { createCapabilityRPCHandler, } from './CapabilityRPCHandler';
5
+ export { registerComponent, registerComponents, getComponent, isValidTag, } from './ComponentRegistry';
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAA;AAC7E,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAC/C,OAAO,EACL,aAAa,EACb,UAAU,EACV,cAAc,EACd,aAAa,GACd,MAAM,kBAAkB,CAAA;AACzB,OAAO,EACL,0BAA0B,GAE3B,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,YAAY,EACZ,UAAU,GACX,MAAM,qBAAqB,CAAA"}
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@stackable-labs/sdk-extension-host",
3
+ "version": "1.0.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "dependencies": {
15
+ "@stackable-labs/sdk-extension-contracts": "1.0.0",
16
+ "@remote-dom/core": "1.x",
17
+ "@remote-dom/react": "1.x"
18
+ },
19
+ "peerDependencies": {
20
+ "react": ">=18.0.0 <19.0.0",
21
+ "react-dom": ">=18.0.0 <19.0.0"
22
+ },
23
+ "description": "Host-side SDK for embedding Stackable extensions.",
24
+ "license": "SEE LICENSE IN LICENSE",
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "homepage": "https://www.npmjs.com/package/@stackable-labs/sdk-extension-host",
29
+ "files": [
30
+ "dist/",
31
+ "README.md",
32
+ "LICENSE"
33
+ ]
34
+ }