@ggui-ai/iframe-runtime 0.1.0-rc.1
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 +201 -0
- package/README.md +33 -0
- package/dist/bootstrap.d.ts +72 -0
- package/dist/bootstrap.js +500 -0
- package/dist/canvas/animator/animator-host.d.ts +63 -0
- package/dist/canvas/animator/animator-host.js +105 -0
- package/dist/canvas/animator/animator-pill.d.ts +42 -0
- package/dist/canvas/animator/animator-pill.js +119 -0
- package/dist/canvas/animator/state-machine.d.ts +106 -0
- package/dist/canvas/animator/state-machine.js +150 -0
- package/dist/canvas/canvas-shell.d.ts +77 -0
- package/dist/canvas/canvas-shell.js +117 -0
- package/dist/canvas/display-mode-policy.d.ts +104 -0
- package/dist/canvas/display-mode-policy.js +98 -0
- package/dist/canvas/mount.d.ts +150 -0
- package/dist/canvas/mount.js +202 -0
- package/dist/canvas/nav-stack.d.ts +133 -0
- package/dist/canvas/nav-stack.js +172 -0
- package/dist/channel-transport.d.ts +182 -0
- package/dist/channel-transport.js +344 -0
- package/dist/channels/channel-payload.d.ts +20 -0
- package/dist/channels/channel-payload.js +34 -0
- package/dist/channels/data.d.ts +48 -0
- package/dist/channels/data.js +103 -0
- package/dist/channels/drain-ack.d.ts +13 -0
- package/dist/channels/drain-ack.js +15 -0
- package/dist/channels/feedback.d.ts +14 -0
- package/dist/channels/feedback.js +19 -0
- package/dist/channels/index.d.ts +7 -0
- package/dist/channels/index.js +7 -0
- package/dist/channels/props-update.d.ts +53 -0
- package/dist/channels/props-update.js +130 -0
- package/dist/channels/push.d.ts +47 -0
- package/dist/channels/push.js +54 -0
- package/dist/channels/system.d.ts +26 -0
- package/dist/channels/system.js +44 -0
- package/dist/compiled-validators.d.ts +40 -0
- package/dist/compiled-validators.js +74 -0
- package/dist/context-observer.d.ts +205 -0
- package/dist/context-observer.js +312 -0
- package/dist/gadget-loader.d.ts +118 -0
- package/dist/gadget-loader.js +136 -0
- package/dist/globals.d.ts +234 -0
- package/dist/globals.js +101 -0
- package/dist/host-context-emitter.d.ts +91 -0
- package/dist/host-context-emitter.js +213 -0
- package/dist/iframe-runtime.js +261 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +4 -0
- package/dist/lifecycle.d.ts +66 -0
- package/dist/lifecycle.js +49 -0
- package/dist/mcp-app-iframe-host.d.ts +69 -0
- package/dist/mcp-app-iframe-host.js +214 -0
- package/dist/observability.d.ts +225 -0
- package/dist/observability.js +28 -0
- package/dist/protocol-error.d.ts +199 -0
- package/dist/protocol-error.js +116 -0
- package/dist/provisional-renderer.d.ts +62 -0
- package/dist/provisional-renderer.js +323 -0
- package/dist/react-renderer.d.ts +95 -0
- package/dist/react-renderer.js +343 -0
- package/dist/registry-subscribe.d.ts +102 -0
- package/dist/registry-subscribe.js +319 -0
- package/dist/runtime.d.ts +531 -0
- package/dist/runtime.js +2623 -0
- package/dist/server.d.ts +36 -0
- package/dist/server.js +72 -0
- package/dist/stack-item-renderer.d.ts +103 -0
- package/dist/stack-item-renderer.js +270 -0
- package/dist/stack.d.ts +90 -0
- package/dist/stack.js +118 -0
- package/dist/status-dom.d.ts +72 -0
- package/dist/status-dom.js +51 -0
- package/dist/system-cards/NoCredentialsCard.d.ts +33 -0
- package/dist/system-cards/NoCredentialsCard.js +189 -0
- package/dist/system-cards/ProtocolProbeCard.d.ts +64 -0
- package/dist/system-cards/ProtocolProbeCard.js +463 -0
- package/dist/system-cards/QrCode.d.ts +46 -0
- package/dist/system-cards/QrCode.js +55 -0
- package/dist/system-cards/host-detect.d.ts +31 -0
- package/dist/system-cards/host-detect.js +108 -0
- package/dist/system-cards/host-intents.d.ts +85 -0
- package/dist/system-cards/host-intents.js +170 -0
- package/dist/system-cards/index.d.ts +70 -0
- package/dist/system-cards/index.js +134 -0
- package/dist/types.d.ts +69 -0
- package/dist/types.js +3 -0
- package/dist/validation.d.ts +83 -0
- package/dist/validation.js +106 -0
- package/dist/wire-config.d.ts +214 -0
- package/dist/wire-config.js +290 -0
- package/package.json +81 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry-driven subscribe orchestration — owns the renderer's
|
|
3
|
+
* "I have a parsed bootstrap, now drive the live-channel handshake to
|
|
4
|
+
* first ack" flow on top of `ChannelRegistry.bind()`.
|
|
5
|
+
*
|
|
6
|
+
* Replaces the pre-B3b `subscribe()` helper + the `RendererWebSocketManager`
|
|
7
|
+
* lifecycle class. The two retired modules paired a hand-rolled WS
|
|
8
|
+
* manager with a separate handshake helper; B3b unifies both behind
|
|
9
|
+
* the `@ggui-ai/channel-client` library so every WS frame type (push,
|
|
10
|
+
* data, props_update, drain_ack, channel_payload, channel_error,
|
|
11
|
+
* system, feedback) flows through one registry-owned transport.
|
|
12
|
+
*
|
|
13
|
+
* Composition:
|
|
14
|
+
*
|
|
15
|
+
* 1. Caller-supplied `ChannelRegistry` (populated by `runtime.ts`'s
|
|
16
|
+
* triad-wiring `setup()` with every handler the iframe routes).
|
|
17
|
+
* 2. Compose the WS URL with the bootstrap token threaded as a
|
|
18
|
+
* `?bootstrap=<encoded>` query string (mirrors the pre-B3b
|
|
19
|
+
* `composeWsUrl` shape so server-side upgrade-path auth keeps
|
|
20
|
+
* working).
|
|
21
|
+
* 3. Register `ack` + `error` handlers on the registry BEFORE bind
|
|
22
|
+
* so the first inbound frame after socket open hits the
|
|
23
|
+
* handshake-resolver closure.
|
|
24
|
+
* 4. Call `registry.bind({bootstrap, onStatusChange})` and capture
|
|
25
|
+
* the returned typed `WsTransportHandle` (narrow on
|
|
26
|
+
* `handle.kind === 'ws'`).
|
|
27
|
+
*
|
|
28
|
+
* The handshake resolves on the first `ack` (with the symmetric
|
|
29
|
+
* `CLIENT_SUPPORTED_VERSIONS` check) or rejects on:
|
|
30
|
+
* - `UpgradeRequiredError` (pre-ack `error` with code UPGRADE_REQUIRED
|
|
31
|
+
* OR ack carrying an unrecognised `serverVersion`)
|
|
32
|
+
* - Plain `Error(payload.message)` (other pre-ack errors — typed
|
|
33
|
+
* ProtocolError emitted in parallel)
|
|
34
|
+
* - Plain `Error('subscribe-failed: transport went to failed state
|
|
35
|
+
* before ack')` when the transport status transitions to `'failed'`
|
|
36
|
+
* pre-ack.
|
|
37
|
+
*
|
|
38
|
+
* No timeout is enforced here — the runtime owns the visible "still
|
|
39
|
+
* connecting…" UX and decides when to give up. Surface this module is
|
|
40
|
+
* deliberately thin so the runtime can layer policy on top.
|
|
41
|
+
*/
|
|
42
|
+
import { CLIENT_SUPPORTED_VERSIONS, UPGRADE_REQUIRED } from '@ggui-ai/protocol/version';
|
|
43
|
+
import { UpgradeRequiredError } from '@ggui-ai/protocol/errors/version-mismatch';
|
|
44
|
+
import { fromAuthFailure, fromTransportFailure, fromUpgradeRequired, } from './protocol-error.js';
|
|
45
|
+
/**
|
|
46
|
+
* Compose the WebSocket URL with the bootstrap token threaded as a
|
|
47
|
+
* query parameter. Servers that authenticate the upgrade path read
|
|
48
|
+
* the token from here; the same value also rides inside the subscribe
|
|
49
|
+
* payload so server-side handlers that consume credentials on the
|
|
50
|
+
* subscribe frame see it too.
|
|
51
|
+
*/
|
|
52
|
+
function composeWsUrl(base, bootstrap) {
|
|
53
|
+
const sep = base.indexOf('?') >= 0 ? '&' : '?';
|
|
54
|
+
return `${base}${sep}bootstrap=${encodeURIComponent(bootstrap)}`;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Lift a server-emitted `{type:'error'}` frame into the appropriate
|
|
58
|
+
* Error subclass. Returns `null` for a frame whose code is not
|
|
59
|
+
* UPGRADE_REQUIRED — the caller decides how to surface non-upgrade
|
|
60
|
+
* errors (typically a plain `Error` with the server's message).
|
|
61
|
+
*/
|
|
62
|
+
function liftUpgradeRequiredFromError(payload) {
|
|
63
|
+
if (payload.code !== UPGRADE_REQUIRED)
|
|
64
|
+
return null;
|
|
65
|
+
const details = payload.details;
|
|
66
|
+
let observedVersion;
|
|
67
|
+
if (details !== null &&
|
|
68
|
+
typeof details === 'object' &&
|
|
69
|
+
!Array.isArray(details) &&
|
|
70
|
+
typeof details.serverVersion === 'string') {
|
|
71
|
+
observedVersion = details.serverVersion;
|
|
72
|
+
}
|
|
73
|
+
return new UpgradeRequiredError({
|
|
74
|
+
...(observedVersion !== undefined ? { observedVersion } : {}),
|
|
75
|
+
acceptedVersions: CLIENT_SUPPORTED_VERSIONS,
|
|
76
|
+
observedBy: 'server',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Collapse the `UpgradeRequiredError.observedVersion` variant
|
|
81
|
+
* (`string | readonly string[] | undefined`) onto the single string
|
|
82
|
+
* shape that `SchemaVersionMismatchEvent.observedVersion` carries.
|
|
83
|
+
* Undefined falls back to the sentinel `'unknown'` so hosts always
|
|
84
|
+
* have a rendered value.
|
|
85
|
+
*/
|
|
86
|
+
function normalizeObservedVersion(value) {
|
|
87
|
+
if (value === undefined)
|
|
88
|
+
return 'unknown';
|
|
89
|
+
if (typeof value === 'string')
|
|
90
|
+
return value;
|
|
91
|
+
return value.join(', ');
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Map a pre-ack `error` frame onto a non-UPGRADE_REQUIRED ProtocolError.
|
|
95
|
+
* `SESSION_NOT_FOUND` / `AUTH_REJECTED` → `'auth'`; every other code
|
|
96
|
+
* → `'protocol'` with the extensibly-closed tail carrying the wire code
|
|
97
|
+
* verbatim.
|
|
98
|
+
*/
|
|
99
|
+
function classifyPreAckError(payload) {
|
|
100
|
+
if (payload.code === 'SESSION_NOT_FOUND' || payload.code === 'AUTH_REJECTED') {
|
|
101
|
+
return fromAuthFailure(payload.code, payload.message);
|
|
102
|
+
}
|
|
103
|
+
if (payload.code === 'TOKEN_EXPIRED') {
|
|
104
|
+
return fromAuthFailure('TOKEN_EXPIRED', payload.message);
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
kind: 'protocol',
|
|
108
|
+
code: payload.code,
|
|
109
|
+
...(payload.message !== undefined ? { message: payload.message } : {}),
|
|
110
|
+
...(payload.details !== undefined ? { details: payload.details } : {}),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Open the WS via the registry, send subscribe (composed by the
|
|
115
|
+
* registry's `subscribeFrameBuilder`), await the first `ack`. Resolves
|
|
116
|
+
* with `{ handle, ack }` on success; rejects with
|
|
117
|
+
* `UpgradeRequiredError` on a version-handshake failure (either side);
|
|
118
|
+
* rejects with a plain `Error` on any other server-emitted pre-ack
|
|
119
|
+
* error.
|
|
120
|
+
*
|
|
121
|
+
* Once the promise settles, subsequent server frames flow through the
|
|
122
|
+
* registered channel handlers (props_update, drain_ack, push, data,
|
|
123
|
+
* etc.) — there is no longer a separate `onMessage` callback fanning
|
|
124
|
+
* frames across multiple handler chains.
|
|
125
|
+
*/
|
|
126
|
+
export function connectViaRegistry(opts) {
|
|
127
|
+
return new Promise((resolve, reject) => {
|
|
128
|
+
let receivedAck = false;
|
|
129
|
+
let settled = false;
|
|
130
|
+
let seenOpen = false;
|
|
131
|
+
const emitProtocolError = opts.onProtocolError ?? (() => { });
|
|
132
|
+
const emitObserve = opts.onObserve ?? (() => { });
|
|
133
|
+
// Defense-in-depth — live mode requires both wsUrl + token. The
|
|
134
|
+
// caller validates this before invoking; reject early with a
|
|
135
|
+
// typed transport failure if either is missing.
|
|
136
|
+
const wsUrl = opts.bootstrap.wsUrl;
|
|
137
|
+
const token = opts.bootstrap.token;
|
|
138
|
+
if (typeof wsUrl !== 'string' || wsUrl.length === 0 ||
|
|
139
|
+
typeof token !== 'string' || token.length === 0) {
|
|
140
|
+
const err = new Error('connectViaRegistry: bootstrap missing wsUrl/token — live-mode required');
|
|
141
|
+
emitProtocolError(fromTransportFailure('DISCONNECTED', false, err.message));
|
|
142
|
+
reject(err);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// Register `ack` + `error` handlers BEFORE bind so the first
|
|
146
|
+
// inbound frame after socket open hits the handshake-resolver
|
|
147
|
+
// closure. The registry guards against double-registration; the
|
|
148
|
+
// caller must NOT have pre-registered handlers of these types.
|
|
149
|
+
opts.registry.register({
|
|
150
|
+
type: 'ack',
|
|
151
|
+
onMessage: (payload) => {
|
|
152
|
+
const ack = payload;
|
|
153
|
+
// Post-handshake ack — the WS reconnected and the server
|
|
154
|
+
// re-sent the subscribe ack with a fresh `stack` snapshot.
|
|
155
|
+
// Fan it onto the caller's resubscribe callback so the
|
|
156
|
+
// runtime can reapply the authoritative stack (a push or
|
|
157
|
+
// update that landed during the WS dropout window now
|
|
158
|
+
// restores). Skip the version handshake / settle bookkeeping
|
|
159
|
+
// — that already fired on the original ack.
|
|
160
|
+
if (settled) {
|
|
161
|
+
opts.onResubscribeAck?.(ack);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const serverVersion = ack.serverVersion;
|
|
165
|
+
if (typeof serverVersion === 'string' &&
|
|
166
|
+
!CLIENT_SUPPORTED_VERSIONS.includes(serverVersion)) {
|
|
167
|
+
settled = true;
|
|
168
|
+
receivedAck = true;
|
|
169
|
+
const upgradeErr = new UpgradeRequiredError({
|
|
170
|
+
observedVersion: serverVersion,
|
|
171
|
+
acceptedVersions: CLIENT_SUPPORTED_VERSIONS,
|
|
172
|
+
observedBy: 'client',
|
|
173
|
+
});
|
|
174
|
+
emitProtocolError(fromUpgradeRequired(upgradeErr));
|
|
175
|
+
emitObserve({
|
|
176
|
+
kind: 'schema-version-mismatch',
|
|
177
|
+
observedVersion: serverVersion,
|
|
178
|
+
acceptedVersions: CLIENT_SUPPORTED_VERSIONS,
|
|
179
|
+
observedBy: 'client',
|
|
180
|
+
});
|
|
181
|
+
reject(upgradeErr);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
settled = true;
|
|
185
|
+
receivedAck = true;
|
|
186
|
+
// Resolution is deferred — the typed handle is set by the
|
|
187
|
+
// `bind()` resolution path below. We just record the ack and
|
|
188
|
+
// let the bind continuation finish wiring.
|
|
189
|
+
ackResolverState.payload = ack;
|
|
190
|
+
ackResolverState.onAck?.();
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
opts.registry.register({
|
|
194
|
+
type: 'error',
|
|
195
|
+
onMessage: (payload) => {
|
|
196
|
+
// Post-ack error frames are NOT handled here — the runtime
|
|
197
|
+
// doesn't currently route post-ack `error` frames through a
|
|
198
|
+
// registered handler (they used to surface via the legacy
|
|
199
|
+
// `onMessage` callback; B3b drops that channel since
|
|
200
|
+
// post-ack errors are rare + the host inspector logs them
|
|
201
|
+
// through the typed ProtocolError emitter).
|
|
202
|
+
if (settled)
|
|
203
|
+
return;
|
|
204
|
+
const errPayload = payload;
|
|
205
|
+
const upgrade = liftUpgradeRequiredFromError(errPayload);
|
|
206
|
+
if (upgrade !== null) {
|
|
207
|
+
settled = true;
|
|
208
|
+
emitProtocolError(fromUpgradeRequired(upgrade));
|
|
209
|
+
emitObserve({
|
|
210
|
+
kind: 'schema-version-mismatch',
|
|
211
|
+
observedVersion: normalizeObservedVersion(upgrade.observedVersion),
|
|
212
|
+
acceptedVersions: upgrade.acceptedVersions,
|
|
213
|
+
observedBy: 'server',
|
|
214
|
+
});
|
|
215
|
+
reject(upgrade);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
settled = true;
|
|
219
|
+
emitProtocolError(classifyPreAckError(errPayload));
|
|
220
|
+
reject(new Error(errPayload.message ?? errPayload.code));
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
// Captured-state closure so the ack handler can hand the resolved
|
|
224
|
+
// payload to the bind continuation without sharing a mutable
|
|
225
|
+
// local with the outer Promise body.
|
|
226
|
+
const ackResolverState = { payload: null, onAck: null };
|
|
227
|
+
const mappedStatusCallback = (status) => {
|
|
228
|
+
// `connecting` is the initial state from WSTransport — first
|
|
229
|
+
// surfaced concurrent with `start()`. Subsequent `connecting`
|
|
230
|
+
// transitions mean the reconnect ladder is firing (after a
|
|
231
|
+
// prior `open` → `closed`).
|
|
232
|
+
if (status === 'connecting') {
|
|
233
|
+
if (!seenOpen) {
|
|
234
|
+
opts.onStatusChange('connecting');
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
opts.onStatusChange('reconnecting');
|
|
238
|
+
emitObserve({
|
|
239
|
+
kind: 'subscribe-failed',
|
|
240
|
+
reason: 'transport-reconnecting',
|
|
241
|
+
message: 'renderer WebSocket dropped; reconnect ladder running',
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (status === 'open') {
|
|
247
|
+
seenOpen = true;
|
|
248
|
+
opts.onStatusChange('connected');
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (status === 'closed') {
|
|
252
|
+
// Graceful close — propagate as `disconnected` so the status
|
|
253
|
+
// DOM reflects the drop. WSTransport will fire `connecting`
|
|
254
|
+
// again on its reconnect path; we don't pre-empt.
|
|
255
|
+
opts.onStatusChange('disconnected');
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
// `failed` — terminal. Map to `disconnected` for the
|
|
259
|
+
// protocol-level status; reject the handshake if we never
|
|
260
|
+
// saw an ack so callers don't hang on a dead transport.
|
|
261
|
+
opts.onStatusChange('disconnected');
|
|
262
|
+
emitProtocolError(fromTransportFailure('DISCONNECTED', false, 'transport went to failed state'));
|
|
263
|
+
if (!settled && !receivedAck) {
|
|
264
|
+
settled = true;
|
|
265
|
+
reject(new Error('connectViaRegistry: transport went to failed state before ack'));
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
const composedUrl = composeWsUrl(wsUrl, token);
|
|
269
|
+
// `bind()` selects the transport (WS for wsUrl+token bootstraps;
|
|
270
|
+
// the registry's selection logic narrows on bootstrap shape) and
|
|
271
|
+
// starts it. We override `bootstrap.wsUrl` with the composed URL
|
|
272
|
+
// so the registry's WS transport gets the token-threaded variant.
|
|
273
|
+
void opts.registry
|
|
274
|
+
.bind({
|
|
275
|
+
bootstrap: {
|
|
276
|
+
wsUrl: composedUrl,
|
|
277
|
+
token,
|
|
278
|
+
sessionId: opts.bootstrap.sessionId,
|
|
279
|
+
appId: opts.bootstrap.appId,
|
|
280
|
+
},
|
|
281
|
+
onStatusChange: mappedStatusCallback,
|
|
282
|
+
})
|
|
283
|
+
.then((bound) => {
|
|
284
|
+
// bound is `AnyTransportHandle`. Composed URL + token means
|
|
285
|
+
// the registry selected WSTransport — `kind === 'ws'` is the
|
|
286
|
+
// structural invariant. Narrow defensively.
|
|
287
|
+
if (bound.kind !== 'ws') {
|
|
288
|
+
if (!settled) {
|
|
289
|
+
settled = true;
|
|
290
|
+
reject(new Error("connectViaRegistry: registry returned non-ws transport despite wsUrl+token bootstrap"));
|
|
291
|
+
}
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const wsHandle = bound;
|
|
295
|
+
// If we already received the ack while bind was resolving,
|
|
296
|
+
// settle the outer Promise now.
|
|
297
|
+
if (ackResolverState.payload !== null) {
|
|
298
|
+
resolve({ handle: wsHandle, ack: ackResolverState.payload });
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
// Otherwise wire the ack handler's continuation so it
|
|
302
|
+
// resolves with the handle when the first ack lands.
|
|
303
|
+
ackResolverState.onAck = () => {
|
|
304
|
+
const ack = ackResolverState.payload;
|
|
305
|
+
if (ack !== null) {
|
|
306
|
+
resolve({ handle: wsHandle, ack });
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
})
|
|
310
|
+
.catch((err) => {
|
|
311
|
+
if (settled)
|
|
312
|
+
return;
|
|
313
|
+
settled = true;
|
|
314
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
315
|
+
emitProtocolError(fromTransportFailure('DISCONNECTED', false, message));
|
|
316
|
+
reject(err instanceof Error ? err : new Error(message));
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
}
|