@backdrop-augmentation-protocol/component-sdk 0.1.0 → 0.2.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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [English](README.md) | [简体中文](README.zh-CN.md)
4
4
 
5
- Official MVP SDK for building dual-mode BAP components. A component can run independently in Standalone Mode or inside a compatible host through an Embedded iframe.
5
+ Official SDK version 0.2.0 for building dual-mode BAP components. It implements Core 0.1.0 and supports the versioned composition, configuration, and presentation extensions. A component can run independently in Standalone Mode or inside a compatible host through an Embedded iframe.
6
6
 
7
7
  ## Quick start
8
8
 
@@ -36,4 +36,21 @@ The official Standalone Shell is an optional convenience. The protocol only requ
36
36
  - `PostMessageClient`: request/response and event messaging over `postMessage`.
37
37
  - `ComponentRuntime`: lifecycle (`start`, `pause`, `resume`, `stop`) and configuration APIs.
38
38
 
39
+ ### BAP 0.2.0 extension support
40
+
41
+ Embedded components may explicitly offer optional extensions. Only exact Host/Component version intersections are selected; Core-only Hosts remain supported.
42
+
43
+ ```js
44
+ const entry = createEmbeddedEntry({ manifest, extensions: [
45
+ { id: 'org.bap.composition', versions: ['0.1.0'] },
46
+ { id: 'org.bap.component-config', versions: ['0.1.0'] },
47
+ { id: 'org.bap.presentation', versions: ['0.1.0'] }
48
+ ] });
49
+
50
+ entry.runtime.onConfigChange(config => renderConfig(config));
51
+ entry.runtime.onPresentationChange(state => setVisible(state.visible));
52
+ ```
53
+
54
+ When composition is selected, `runtime.instanceId` is the Host-provided opaque routing context. It is not an authentication or origin claim. Host configuration events replace the complete effective snapshot only when their instance ID matches and their revision is newer. Presentation events are likewise instance-scoped.
55
+
39
56
  Run tests with `npm test`. Run the minimal example with `npm run example`, then open `http://localhost:4173`.
package/README.zh-CN.md CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  [English](README.md) | 简体中文
4
4
 
5
- 用于构建 BAP 双模式组件的官方 MVP SDK。组件可以在 Standalone Mode 中独立
5
+ 用于构建 BAP 双模式组件的官方 SDK 0.2.0,实现 Core 0.1.0 并支持版本化的
6
+ 组合、配置和呈现扩展。组件可以在 Standalone Mode 中独立
6
7
  运行,也可以通过 Embedded iframe 运行在兼容 Host 中。
7
8
 
8
9
  ## 快速开始
@@ -38,5 +39,22 @@ new StandaloneShell({ manifest, render(root, runtime) {
38
39
  - `PostMessageClient`:通过 `postMessage` 实现请求/响应和事件通信;
39
40
  - `ComponentRuntime`:生命周期(`start`、`pause`、`resume`、`stop`)和配置 API。
40
41
 
42
+ ### BAP 0.2.0 扩展支持
43
+
44
+ 嵌入式组件可以显式声明支持的可选扩展。只有 Host 与 Component 版本完全匹配时才会协商选中;仍兼容仅支持 Core 的 Host。
45
+
46
+ ```js
47
+ const entry = createEmbeddedEntry({ manifest, extensions: [
48
+ { id: 'org.bap.composition', versions: ['0.1.0'] },
49
+ { id: 'org.bap.component-config', versions: ['0.1.0'] },
50
+ { id: 'org.bap.presentation', versions: ['0.1.0'] }
51
+ ] });
52
+
53
+ entry.runtime.onConfigChange(config => renderConfig(config));
54
+ entry.runtime.onPresentationChange(state => setVisible(state.visible));
55
+ ```
56
+
57
+ 选中 composition 后,`runtime.instanceId` 是 Host 提供的不透明路由上下文,不代表身份认证或来源证明。Host 配置事件仅在实例 ID 匹配且修订号更新时替换完整有效快照。呈现事件同样按实例隔离。
58
+
41
59
  使用 `npm test` 运行测试。使用 `npm run example` 运行最小示例,然后打开
42
60
  `http://localhost:4173`。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backdrop-augmentation-protocol/component-sdk",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "license": "MIT",
5
5
  "description": "Official BAP Component SDK for dual-mode web components.",
6
6
  "publishConfig": { "access": "public" },
package/src/index.d.ts CHANGED
@@ -1,11 +1,14 @@
1
1
  export type ComponentMode = 'embedded' | 'standalone';
2
2
  export interface ComponentManifest { protocol: { name: 'bap'; version: string; profile: string }; id: string; name: string; version: string; implementationVersion?: string; role: 'component'; capabilities: string[]; permissions?: string[]; modes: ComponentMode[]; entrypoints: { default: string; embedded?: string; standalone?: string }; configScopes?: Array<'global' | 'series' | 'wallpaper' | 'instance'>; description?: string; }
3
3
  export interface ManifestResult { valid: boolean; errors: string[]; }
4
+ export interface ExtensionOffer { id: string; versions: string[]; }
5
+ export interface SelectedExtension { id: string; version: string; }
6
+ export interface PresentationState { visible: boolean; }
4
7
  export declare function validateManifest(value: unknown): ManifestResult;
5
8
  export declare function assertManifest(manifest: unknown): ComponentManifest;
6
9
  export declare class ManifestValidationError extends Error { errors: string[]; }
7
10
  export declare class PostMessageClient { constructor(options: { target: Window; targetOrigin?: string; receiveWindow?: Window; timeoutMs?: number; sessionId?: string }); request(method: string, payload?: unknown): Promise<unknown>; notify(event: string, payload?: unknown, options?: { sessionId?: string }): void; on(event: string, listener: (payload: unknown, message: unknown) => void): () => void; onRequest(handler: (message: unknown) => Promise<Record<string, unknown>> | Record<string, unknown>): () => void; close(): void; }
8
- export declare class ComponentRuntime { manifest: ComponentManifest; mode: ComponentMode; state: string; constructor(options: { manifest: ComponentManifest; mode: ComponentMode; client?: PostMessageClient; config?: Record<string, unknown>; root?: HTMLElement }); start(): Promise<void>; pause(): Promise<void>; resume(): Promise<void>; stop(): Promise<void>; getConfig(key?: string): unknown; setConfig(patch: Record<string, unknown>): Promise<Record<string, unknown>>; onConfigChange(listener: (config: Record<string, unknown>) => void): () => void; onLifecycle(state: string, listener: (event: unknown) => void): () => void; hostRequest(method: string, params?: unknown): Promise<unknown>; }
11
+ export declare class ComponentRuntime { manifest: ComponentManifest; mode: ComponentMode; state: string; instanceId: string | null; selectedExtensions: SelectedExtension[]; presentation: PresentationState; constructor(options: { manifest: ComponentManifest; mode: ComponentMode; client?: PostMessageClient; config?: Record<string, unknown>; extensions?: ExtensionOffer[]; root?: HTMLElement }); start(): Promise<void>; pause(): Promise<void>; resume(): Promise<void>; stop(): Promise<void>; getConfig(key?: string): unknown; setConfig(patch: Record<string, unknown>): Promise<Record<string, unknown>>; onConfigChange(listener: (config: Record<string, unknown>) => void): () => void; onPresentationChange(listener: (state: PresentationState) => void): () => void; onLifecycle(state: string, listener: (event: unknown) => void): () => void; hostRequest(method: string, params?: unknown): Promise<unknown>; }
9
12
  export declare function createEmbeddedEntry(options: Record<string, unknown>): { runtime: ComponentRuntime; start(): Promise<void>; pause(): Promise<void>; resume(): Promise<void>; stop(): Promise<void> };
10
13
  export declare function createStandaloneEntry(options: Record<string, unknown>): { runtime: ComponentRuntime; start(): Promise<void>; pause(): Promise<void>; resume(): Promise<void>; stop(): Promise<void> };
11
14
  export declare class StandaloneShell { constructor(options: { manifest: ComponentManifest; root?: HTMLElement; config?: Record<string, unknown>; render?: (root: HTMLElement, runtime: ComponentRuntime) => void }); start(): Promise<void>; pause(): Promise<void>; resume(): Promise<void>; stop(): Promise<void>; }
package/src/runtime.js CHANGED
@@ -1,77 +1,133 @@
1
- import { assertManifest } from './manifest.js';
2
- import { PostMessageClient } from './post-message-client.js';
3
- import { BAP_PROTOCOL, createId } from './protocol.js';
4
-
5
- export class ComponentRuntime {
6
- constructor({ manifest, mode, client = null, config = {}, root = null } = {}) {
7
- assertManifest(manifest);
8
- if (!['embedded', 'standalone'].includes(mode)) throw new TypeError('mode must be embedded or standalone');
9
- this.manifest = manifest; this.mode = mode; this.client = client; this.root = root;
10
- this.state = 'created'; this.sessionId = null; this.config = globalThis.structuredClone ? globalThis.structuredClone(config) : { ...config }; this.configListeners = new Set();
11
- this.lifecycleListeners = new Map();
12
- if (this.mode === 'embedded' && this.client) this.client.onRequest((message) => this._hostRequest(message));
13
- }
14
- onLifecycle(state, listener) { if (!this.lifecycleListeners.has(state)) this.lifecycleListeners.set(state, new Set()); this.lifecycleListeners.get(state).add(listener); return () => this.lifecycleListeners.get(state)?.delete(listener); }
15
- async start() {
16
- if (this.mode === 'embedded') {
17
- if (!this.client) throw new Error('Embedded mode requires a Host session channel');
18
- return;
19
- }
20
- if (this.state === 'running') return;
21
- if (this.state !== 'created' && this.state !== 'stopped') throw this._invalidState('start');
22
- if (this.state === 'created') {
23
- this.sessionId = `standalone-${Date.now().toString(36)}`;
24
- await this._transition('connected'); await this._requestLifecycle('initialize'); await this._transition('initialized');
25
- } else { await this._requestLifecycle('initialize'); await this._transition('initialized'); }
26
- await this._requestLifecycle('start'); await this._transition('running');
27
- }
28
- async pause() { if (this.state === 'paused') return; if (this.state !== 'running') throw this._invalidState('pause'); await this._requestLifecycle('pause'); await this._transition('paused'); }
29
- async resume() { if (this.state === 'paused') { await this._requestLifecycle('resume'); await this._transition('running'); } }
30
- async stop() { if (this.state === 'stopped') return; if (!['running', 'paused'].includes(this.state)) throw this._invalidState('stop'); await this._requestLifecycle('stop'); await this._transition('stopped'); }
31
- async _requestLifecycle(method) { if (this.client) await this.client.request(method, { componentId: this.manifest.id, mode: this.mode }); }
32
- _invalidState(method) { const error = new Error(`Cannot ${method} from ${this.state}`); error.code = 'INVALID_STATE'; return error; }
33
- async _hostRequest(message) {
34
- const { method, payload } = message;
35
- if (method === 'hello') {
36
- if (this.state !== 'created') throw this._invalidState('hello');
37
- if (message.protocol?.name !== BAP_PROTOCOL.name || message.protocol.profile !== BAP_PROTOCOL.profile) {
38
- const error = new Error('Unsupported Core profile'); error.code = 'UNSUPPORTED_PROFILE'; throw error;
39
- }
40
- if (payload?.role !== 'host' || !payload.identity?.id || !Array.isArray(payload.protocolVersions) || !Array.isArray(payload.capabilities) || !Array.isArray(payload.requestedPermissions)) {
41
- const error = new Error('Invalid hello payload'); error.code = 'INVALID_MESSAGE'; throw error;
42
- }
43
- if (!payload.protocolVersions.includes(BAP_PROTOCOL.version) || message.protocol.version !== BAP_PROTOCOL.version) {
44
- const error = new Error('Unsupported protocol version'); error.code = 'UNSUPPORTED_VERSION'; throw error;
45
- }
46
- this.sessionId = createId(); this.client.sessionId = this.sessionId;
47
- await this._transition('connected');
48
- return { selectedProtocol: BAP_PROTOCOL, role: 'component', capabilities: this.manifest.capabilities.filter(value => payload.capabilities.includes(value)), permissions: (this.manifest.permissions || []).filter(value => payload.requestedPermissions.includes(value)) };
49
- }
50
- if (!this.sessionId || message.sessionId !== this.sessionId) {
51
- const error = new Error('Missing or invalid session'); error.code = 'UNAUTHORIZED'; throw error;
52
- }
53
- if (method === 'initialize' && (this.state === 'connected' || this.state === 'stopped')) { await this._transition('initialized'); return { state: this.state }; }
54
- if (method === 'start' && this.state === 'initialized') { await this._transition('running'); return { state: this.state }; }
55
- if (method === 'pause' && this.state === 'running') { await this._transition('paused'); return { state: this.state }; }
56
- if (method === 'resume' && this.state === 'paused') { await this._transition('running'); return { state: this.state }; }
57
- if (method === 'stop' && ['running', 'paused'].includes(this.state)) { await this._transition('stopped'); return { state: this.state }; }
58
- if ((method === 'pause' && this.state === 'paused') || (method === 'resume' && this.state === 'running') || (method === 'stop' && this.state === 'stopped')) return { state: this.state };
59
- if (['initialize', 'start', 'pause', 'resume', 'stop'].includes(method)) throw this._invalidState(method);
60
- const error = new Error(`Unknown method: ${method}`); error.code = 'NOT_FOUND'; throw error;
61
- }
62
- async _transition(state) { this.state = state; for (const listener of this.lifecycleListeners.get(state) || []) await listener({ state, runtime: this }); if (state !== 'connected') this.client?.notify('state.changed', { role: 'component', state, id: this.manifest.id }, { sessionId: this.sessionId }); }
63
- getConfig(key) { return key === undefined ? this.config : this.config[key]; }
64
- async setConfig(patch) { if (!patch || typeof patch !== 'object' || Array.isArray(patch)) throw new TypeError('config patch must be an object'); this.config = { ...this.config, ...patch }; for (const listener of this.configListeners) await listener(this.config); this.client?.notify('config.changed', { scope: 'wallpaper', config: this.config }, { sessionId: this.sessionId }); return this.config; }
65
- onConfigChange(listener) { this.configListeners.add(listener); return () => this.configListeners.delete(listener); }
66
- hostRequest(method, params) { if (!this.client) return Promise.reject(new Error('Host API is unavailable in standalone mode')); return this.client.request(method, params); }
67
- }
68
-
69
- export function createEmbeddedEntry(options) { return createEntry({ ...options, mode: 'embedded' }); }
70
- export function createStandaloneEntry(options) { return createEntry({ ...options, mode: 'standalone' }); }
71
- function createEntry({ manifest, mode, root = globalThis.document?.body, client, config, targetOrigin = '*' } = {}) {
72
- if (mode === 'embedded' && !client && globalThis.window?.parent && globalThis.window.parent !== globalThis.window) {
73
- client = new PostMessageClient({ target: globalThis.window.parent, targetOrigin, receiveWindow: globalThis.window });
74
- }
75
- const runtime = new ComponentRuntime({ manifest, mode, root, client, config });
76
- return { runtime, start: () => runtime.start(), pause: () => runtime.pause(), resume: () => runtime.resume(), stop: () => runtime.stop() };
77
- }
1
+ import { assertManifest } from './manifest.js';
2
+ import { PostMessageClient } from './post-message-client.js';
3
+ import { BAP_PROTOCOL, createId } from './protocol.js';
4
+
5
+ export class ComponentRuntime {
6
+ constructor({ manifest, mode, client = null, config = {}, root = null, extensions = [] } = {}) {
7
+ assertManifest(manifest);
8
+ if (!['embedded', 'standalone'].includes(mode)) throw new TypeError('mode must be embedded or standalone');
9
+ validateExtensionOffers(extensions);
10
+ this.manifest = manifest; this.mode = mode; this.client = client; this.root = root; this.extensionOffers = clone(extensions);
11
+ this.state = 'created'; this.sessionId = null; this.instanceId = null; this.selectedExtensions = []; this.config = clone(config); this.configRevision = -1; this.configListeners = new Set();
12
+ this.presentation = { visible: true };
13
+ this.presentationListeners = new Set();
14
+ this.lifecycleListeners = new Map();
15
+ if (this.mode === 'embedded' && this.client) {
16
+ this.client.onRequest((message) => this._hostRequest(message));
17
+ this.client.on('component.config.changed', payload => this._hostConfigChanged(payload));
18
+ this.client.on('presentation.visibility.changed', payload => this._visibilityChanged(payload));
19
+ }
20
+ }
21
+ onLifecycle(state, listener) { if (!this.lifecycleListeners.has(state)) this.lifecycleListeners.set(state, new Set()); this.lifecycleListeners.get(state).add(listener); return () => this.lifecycleListeners.get(state)?.delete(listener); }
22
+ onPresentationChange(listener) { this.presentationListeners.add(listener); return () => this.presentationListeners.delete(listener); }
23
+ async start() {
24
+ if (this.mode === 'embedded') {
25
+ if (!this.client) throw new Error('Embedded mode requires a Host session channel');
26
+ return;
27
+ }
28
+ if (this.state === 'running') return;
29
+ if (this.state !== 'created' && this.state !== 'stopped') throw this._invalidState('start');
30
+ if (this.state === 'created') {
31
+ this.sessionId = `standalone-${Date.now().toString(36)}`;
32
+ await this._transition('connected'); await this._requestLifecycle('initialize'); await this._transition('initialized');
33
+ } else { await this._requestLifecycle('initialize'); await this._transition('initialized'); }
34
+ await this._requestLifecycle('start'); await this._transition('running');
35
+ }
36
+ async pause() { if (this.state === 'paused') return; if (this.state !== 'running') throw this._invalidState('pause'); await this._requestLifecycle('pause'); await this._transition('paused'); }
37
+ async resume() { if (this.state === 'paused') { await this._requestLifecycle('resume'); await this._transition('running'); } }
38
+ async stop() { if (this.state === 'stopped') return; if (!['running', 'paused'].includes(this.state)) throw this._invalidState('stop'); await this._requestLifecycle('stop'); await this._transition('stopped'); }
39
+ async _requestLifecycle(method) { if (this.client) await this.client.request(method, { componentId: this.manifest.id, mode: this.mode }); }
40
+ _invalidState(method) { const error = new Error(`Cannot ${method} from ${this.state}`); error.code = 'INVALID_STATE'; return error; }
41
+ async _hostRequest(message) {
42
+ const { method, payload } = message;
43
+ if (method === 'hello') {
44
+ if (this.state !== 'created') throw this._invalidState('hello');
45
+ if (message.protocol?.name !== BAP_PROTOCOL.name || message.protocol.profile !== BAP_PROTOCOL.profile) {
46
+ const error = new Error('Unsupported Core profile'); error.code = 'UNSUPPORTED_PROFILE'; throw error;
47
+ }
48
+ if (payload?.role !== 'host' || !payload.identity?.id || !Array.isArray(payload.protocolVersions) || !Array.isArray(payload.capabilities) || !Array.isArray(payload.requestedPermissions)) {
49
+ const error = new Error('Invalid hello payload'); error.code = 'INVALID_MESSAGE'; throw error;
50
+ }
51
+ if (!payload.protocolVersions.includes(BAP_PROTOCOL.version) || message.protocol.version !== BAP_PROTOCOL.version) {
52
+ const error = new Error('Unsupported protocol version'); error.code = 'UNSUPPORTED_VERSION'; throw error;
53
+ }
54
+ const offered = validateHostExtensionOffers(payload.extensions ?? []);
55
+ const selectedExtensions = negotiateExtensions(offered, this.extensionOffers);
56
+ if (selectedExtensions.some(extension => extension.id === 'org.bap.composition' || extension.id === 'org.bap.component-config')) {
57
+ if (typeof payload.instanceId !== 'string' || !payload.instanceId.trim()) {
58
+ const error = new Error('Composition handshake requires an instanceId'); error.code = 'INVALID_MESSAGE'; throw error;
59
+ }
60
+ this.instanceId = payload.instanceId;
61
+ }
62
+ this.sessionId = createId(); this.client.sessionId = this.sessionId;
63
+ this.selectedExtensions = selectedExtensions;
64
+ await this._transition('connected');
65
+ return { selectedProtocol: BAP_PROTOCOL, role: 'component', capabilities: this.manifest.capabilities.filter(value => payload.capabilities.includes(value)), permissions: (this.manifest.permissions || []).filter(value => payload.requestedPermissions.includes(value)), ...(selectedExtensions.length ? { selectedExtensions } : {}) };
66
+ }
67
+ if (!this.sessionId || message.sessionId !== this.sessionId) {
68
+ const error = new Error('Missing or invalid session'); error.code = 'UNAUTHORIZED'; throw error;
69
+ }
70
+ if (method === 'initialize' && (this.state === 'connected' || this.state === 'stopped')) { await this._transition('initialized'); return { state: this.state }; }
71
+ if (method === 'start' && this.state === 'initialized') { await this._transition('running'); return { state: this.state }; }
72
+ if (method === 'pause' && this.state === 'running') { await this._transition('paused'); return { state: this.state }; }
73
+ if (method === 'resume' && this.state === 'paused') { await this._transition('running'); return { state: this.state }; }
74
+ if (method === 'stop' && ['running', 'paused'].includes(this.state)) { await this._transition('stopped'); return { state: this.state }; }
75
+ if ((method === 'pause' && this.state === 'paused') || (method === 'resume' && this.state === 'running') || (method === 'stop' && this.state === 'stopped')) return { state: this.state };
76
+ if (['initialize', 'start', 'pause', 'resume', 'stop'].includes(method)) throw this._invalidState(method);
77
+ const error = new Error(`Unknown method: ${method}`); error.code = 'NOT_FOUND'; throw error;
78
+ }
79
+ async _transition(state) { this.state = state; for (const listener of this.lifecycleListeners.get(state) || []) await listener({ state, runtime: this }); if (state !== 'connected') this.client?.notify('state.changed', { role: 'component', state, id: this.manifest.id }, { sessionId: this.sessionId }); }
80
+ getConfig(key) { return key === undefined ? this.config : this.config[key]; }
81
+ async setConfig(patch) { if (!patch || typeof patch !== 'object' || Array.isArray(patch)) throw new TypeError('config patch must be an object'); this.config = { ...this.config, ...patch }; for (const listener of this.configListeners) await listener(this.config); this.client?.notify('config.changed', { scope: 'wallpaper', config: this.config }, { sessionId: this.sessionId }); return this.config; }
82
+ onConfigChange(listener) { this.configListeners.add(listener); return () => this.configListeners.delete(listener); }
83
+ _hasExtension(id) { return this.selectedExtensions.some(extension => extension.id === id); }
84
+ _hostConfigChanged(payload) {
85
+ if (!this._hasExtension('org.bap.component-config') || !this._matchesInstance(payload) || !Number.isSafeInteger(payload.revision) || payload.revision < 0 || !isRecord(payload.config) || payload.revision <= this.configRevision) return;
86
+ this.config = clone(payload.config); this.configRevision = payload.revision;
87
+ for (const listener of this.configListeners) listener(this.config);
88
+ }
89
+ _visibilityChanged(payload) {
90
+ if (!this._hasExtension('org.bap.presentation') || !this._matchesInstance(payload) || typeof payload.visible !== 'boolean') return;
91
+ if (payload.visible === this.presentation.visible) return;
92
+ this.presentation = { ...this.presentation, visible: payload.visible };
93
+ this._notifyPresentation();
94
+ }
95
+ _matchesInstance(payload) { return !!payload && typeof payload === 'object' && typeof this.instanceId === 'string' && payload.instanceId === this.instanceId; }
96
+ _notifyPresentation() { const state = { ...this.presentation }; for (const listener of this.presentationListeners) listener(state); }
97
+ hostRequest(method, params) { if (!this.client) return Promise.reject(new Error('Host API is unavailable in standalone mode')); return this.client.request(method, params); }
98
+ }
99
+
100
+ export function createEmbeddedEntry(options) { return createEntry({ ...options, mode: 'embedded' }); }
101
+ export function createStandaloneEntry(options) { return createEntry({ ...options, mode: 'standalone' }); }
102
+ function createEntry({ manifest, mode, root = globalThis.document?.body, client, config, extensions = [], targetOrigin = '*' } = {}) {
103
+ if (mode === 'embedded' && !client && globalThis.window?.parent && globalThis.window.parent !== globalThis.window) {
104
+ client = new PostMessageClient({ target: globalThis.window.parent, targetOrigin, receiveWindow: globalThis.window });
105
+ }
106
+ const runtime = new ComponentRuntime({ manifest, mode, root, client, config, extensions });
107
+ return { runtime, start: () => runtime.start(), pause: () => runtime.pause(), resume: () => runtime.resume(), stop: () => runtime.stop() };
108
+ }
109
+
110
+ function clone(value) { return globalThis.structuredClone ? globalThis.structuredClone(value) : JSON.parse(JSON.stringify(value)); }
111
+ function isRecord(value) { return !!value && typeof value === 'object' && !Array.isArray(value); }
112
+ function validateExtensionOffers(offers) {
113
+ if (!Array.isArray(offers)) throw new TypeError('extensions must be an array');
114
+ const ids = new Set();
115
+ for (const offer of offers) {
116
+ if (!isRecord(offer) || typeof offer.id !== 'string' || !offer.id || ids.has(offer.id) || !Array.isArray(offer.versions) || !offer.versions.length || offer.versions.some(version => typeof version !== 'string' || !version)) throw new TypeError('Invalid extension offer');
117
+ ids.add(offer.id);
118
+ }
119
+ }
120
+ function validateHostExtensionOffers(offers) {
121
+ try { validateExtensionOffers(offers); }
122
+ catch (cause) { const error = new Error(cause.message); error.code = 'INVALID_MESSAGE'; throw error; }
123
+ return offers;
124
+ }
125
+ function negotiateExtensions(hostOffers, componentOffers) {
126
+ const selected = [];
127
+ for (const hostOffer of hostOffers) {
128
+ const componentOffer = componentOffers.find(offer => offer.id === hostOffer.id);
129
+ const version = hostOffer.versions.find(candidate => componentOffer?.versions.includes(candidate));
130
+ if (version) selected.push({ id: hostOffer.id, version });
131
+ }
132
+ return selected;
133
+ }