@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.
Files changed (92) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +33 -0
  3. package/dist/bootstrap.d.ts +72 -0
  4. package/dist/bootstrap.js +500 -0
  5. package/dist/canvas/animator/animator-host.d.ts +63 -0
  6. package/dist/canvas/animator/animator-host.js +105 -0
  7. package/dist/canvas/animator/animator-pill.d.ts +42 -0
  8. package/dist/canvas/animator/animator-pill.js +119 -0
  9. package/dist/canvas/animator/state-machine.d.ts +106 -0
  10. package/dist/canvas/animator/state-machine.js +150 -0
  11. package/dist/canvas/canvas-shell.d.ts +77 -0
  12. package/dist/canvas/canvas-shell.js +117 -0
  13. package/dist/canvas/display-mode-policy.d.ts +104 -0
  14. package/dist/canvas/display-mode-policy.js +98 -0
  15. package/dist/canvas/mount.d.ts +150 -0
  16. package/dist/canvas/mount.js +202 -0
  17. package/dist/canvas/nav-stack.d.ts +133 -0
  18. package/dist/canvas/nav-stack.js +172 -0
  19. package/dist/channel-transport.d.ts +182 -0
  20. package/dist/channel-transport.js +344 -0
  21. package/dist/channels/channel-payload.d.ts +20 -0
  22. package/dist/channels/channel-payload.js +34 -0
  23. package/dist/channels/data.d.ts +48 -0
  24. package/dist/channels/data.js +103 -0
  25. package/dist/channels/drain-ack.d.ts +13 -0
  26. package/dist/channels/drain-ack.js +15 -0
  27. package/dist/channels/feedback.d.ts +14 -0
  28. package/dist/channels/feedback.js +19 -0
  29. package/dist/channels/index.d.ts +7 -0
  30. package/dist/channels/index.js +7 -0
  31. package/dist/channels/props-update.d.ts +53 -0
  32. package/dist/channels/props-update.js +130 -0
  33. package/dist/channels/push.d.ts +47 -0
  34. package/dist/channels/push.js +54 -0
  35. package/dist/channels/system.d.ts +26 -0
  36. package/dist/channels/system.js +44 -0
  37. package/dist/compiled-validators.d.ts +40 -0
  38. package/dist/compiled-validators.js +74 -0
  39. package/dist/context-observer.d.ts +205 -0
  40. package/dist/context-observer.js +312 -0
  41. package/dist/gadget-loader.d.ts +118 -0
  42. package/dist/gadget-loader.js +136 -0
  43. package/dist/globals.d.ts +234 -0
  44. package/dist/globals.js +101 -0
  45. package/dist/host-context-emitter.d.ts +91 -0
  46. package/dist/host-context-emitter.js +213 -0
  47. package/dist/iframe-runtime.js +261 -0
  48. package/dist/index.d.ts +38 -0
  49. package/dist/index.js +4 -0
  50. package/dist/lifecycle.d.ts +66 -0
  51. package/dist/lifecycle.js +49 -0
  52. package/dist/mcp-app-iframe-host.d.ts +69 -0
  53. package/dist/mcp-app-iframe-host.js +214 -0
  54. package/dist/observability.d.ts +225 -0
  55. package/dist/observability.js +28 -0
  56. package/dist/protocol-error.d.ts +199 -0
  57. package/dist/protocol-error.js +116 -0
  58. package/dist/provisional-renderer.d.ts +62 -0
  59. package/dist/provisional-renderer.js +323 -0
  60. package/dist/react-renderer.d.ts +95 -0
  61. package/dist/react-renderer.js +343 -0
  62. package/dist/registry-subscribe.d.ts +102 -0
  63. package/dist/registry-subscribe.js +319 -0
  64. package/dist/runtime.d.ts +531 -0
  65. package/dist/runtime.js +2623 -0
  66. package/dist/server.d.ts +36 -0
  67. package/dist/server.js +72 -0
  68. package/dist/stack-item-renderer.d.ts +103 -0
  69. package/dist/stack-item-renderer.js +270 -0
  70. package/dist/stack.d.ts +90 -0
  71. package/dist/stack.js +118 -0
  72. package/dist/status-dom.d.ts +72 -0
  73. package/dist/status-dom.js +51 -0
  74. package/dist/system-cards/NoCredentialsCard.d.ts +33 -0
  75. package/dist/system-cards/NoCredentialsCard.js +189 -0
  76. package/dist/system-cards/ProtocolProbeCard.d.ts +64 -0
  77. package/dist/system-cards/ProtocolProbeCard.js +463 -0
  78. package/dist/system-cards/QrCode.d.ts +46 -0
  79. package/dist/system-cards/QrCode.js +55 -0
  80. package/dist/system-cards/host-detect.d.ts +31 -0
  81. package/dist/system-cards/host-detect.js +108 -0
  82. package/dist/system-cards/host-intents.d.ts +85 -0
  83. package/dist/system-cards/host-intents.js +170 -0
  84. package/dist/system-cards/index.d.ts +70 -0
  85. package/dist/system-cards/index.js +134 -0
  86. package/dist/types.d.ts +69 -0
  87. package/dist/types.js +3 -0
  88. package/dist/validation.d.ts +83 -0
  89. package/dist/validation.js +106 -0
  90. package/dist/wire-config.d.ts +214 -0
  91. package/dist/wire-config.js +290 -0
  92. 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
+ }