@ggui-ai/iframe-runtime 0.27.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ladders.d.ts CHANGED
@@ -2,7 +2,7 @@ import type { ChannelHandler, ChannelLogger, WsTransportHandle } from '@ggui-ai/
2
2
  import type { AckPayload } from '@ggui-ai/protocol/wire';
3
3
  import type { ConnectionStatus } from '@ggui-ai/protocol/transport/websocket';
4
4
  import type { McpAppAiGguiRenderMeta } from '@ggui-ai/protocol/integrations/mcp-apps';
5
- import type { CredentialController, ExpirySource, RefreshOutcome } from './credential-controller.js';
5
+ import { type CredentialController, type ExpirySource, type RefreshOutcome } from './credential-controller.js';
6
6
  import { type BuildBridgePollingOptions, type SequenceCursor } from './events-polling.js';
7
7
  import { type ConnectFn, type RegistrySubscribeHandle, type PostAckErrorPayload } from './registry-subscribe.js';
8
8
  import type { StreamSeqTracker } from './stream-seq.js';
@@ -58,8 +58,6 @@ export interface LadderSetOptions {
58
58
  readonly webSocketFactory?: (url: string) => WebSocket;
59
59
  readonly eventSourceFactory?: (url: string) => EventSource;
60
60
  readonly fetchImpl?: typeof fetch;
61
- /** Test seam for the bridge-entry expiry check. */
62
- readonly now?: () => number;
63
61
  }
64
62
  export interface LadderSet {
65
63
  /** Bind the boot ladder. The caller handles its first ack, or its refusal. */
package/dist/ladders.js CHANGED
@@ -21,8 +21,11 @@
21
21
  * is disposed at that switch, so only one bridge loop runs at a time.
22
22
  * - **Expiry** is reported to the credential controller from three places:
23
23
  * a WS `BOOTSTRAP_EXPIRED` frame (the only auth code that triggers), the
24
- * first polling `410` whose body is not a replay horizon, and entering
25
- * the bridge rung while the ladder's credential is past `expiresAt`. An
24
+ * first polling `410` whose body is not a replay horizon, and a bridge
25
+ * pull while the controller's attempt clock reads the ladder's credential
26
+ * as due — past `expiresAt` by the view's clock, one interval since the
27
+ * last attempt (ggui#1734; a bridge report re-sends on a relay error as the
28
+ * boot does, since the ladder has no other way to a live channel). An
26
29
  * adopted credential builds the next ladder.
27
30
  *
28
31
  * A new ladder omits WS when the one it replaces left WS without ever
@@ -38,6 +41,7 @@
38
41
  * caller is told which confirmation fired. Anything else keeps its backoff.
39
42
  */
40
43
  import { ChannelRegistry } from '@ggui-ai/live-channel';
44
+ import { REFRESH_RELAY_ERROR_RETRIES, } from './credential-controller.js';
41
45
  import { buildBridgePolling, buildEventsPolling, } from './events-polling.js';
42
46
  import { classifyChannelFrame, } from './registry-subscribe.js';
43
47
  import { domainErrorCodeOf, isErrorToolResult } from './tool-result-error.js';
@@ -65,7 +69,6 @@ async function isReplayHorizon(response) {
65
69
  }
66
70
  }
67
71
  export function createLadderSet(opts) {
68
- const now = opts.now ?? Date.now;
69
72
  const baseFetch = opts.fetchImpl ?? ((input, init) => globalThis.fetch(input, init));
70
73
  let nextId = 0;
71
74
  const built = new Map();
@@ -157,7 +160,7 @@ export function createLadderSet(opts) {
157
160
  const controller = opts.controller;
158
161
  if (controller === null || credential === undefined || ladder.disposed || ended)
159
162
  return;
160
- const pending = controller.onExpired(credential, source);
163
+ const pending = controller.onExpired(credential, source, source === 'bridge' ? { retries: REFRESH_RELAY_ERROR_RETRIES } : undefined);
161
164
  if (ladder.refresh === undefined)
162
165
  ladder.refresh = pending;
163
166
  const outcome = await pending;
@@ -280,7 +283,7 @@ export function createLadderSet(opts) {
280
283
  const bridge = callTool !== undefined && view !== undefined
281
284
  ? buildBridgePolling({
282
285
  callTool: async (name, args) => {
283
- enterBridge(ladder);
286
+ onBridgePull(ladder);
284
287
  let result;
285
288
  try {
286
289
  result = await callTool(name, args);
@@ -319,15 +322,22 @@ export function createLadderSet(opts) {
319
322
  ...(bridge !== undefined ? { bridge } : {}),
320
323
  };
321
324
  };
322
- /** The ladder's first bridge pull: it has left its token rungs (R3 (b), fact 8). */
323
- const enterBridge = (ladder) => {
324
- if (ladder.bridgeEntered)
325
- return;
326
- ladder.bridgeEntered = true;
327
- switchTo(ladder);
328
- const expiresAt = ladder.spec.credential?.expiresAt;
329
- if (expiresAt !== undefined && Date.parse(expiresAt) <= now())
325
+ /**
326
+ * Every bridge pull. The first one is the ladder leaving its token rungs
327
+ * (R3 (b), fact 8); each one asks the controller's attempt clock whether the
328
+ * credential is due a refresh (ggui#1734) — expired by the view's clock, one
329
+ * interval since the last attempt — so a view parked here keeps asking, once
330
+ * per interval, until a refresh is granted or the channel ends.
331
+ */
332
+ const onBridgePull = (ladder) => {
333
+ if (!ladder.bridgeEntered) {
334
+ ladder.bridgeEntered = true;
335
+ switchTo(ladder);
336
+ }
337
+ const credential = ladder.spec.credential;
338
+ if (credential !== undefined && opts.controller?.dueOnBridge(credential) === true) {
330
339
  void reportExpired(ladder, 'bridge');
340
+ }
331
341
  };
332
342
  // The handle arrives through `onBound` as soon as the ladder is bound;
333
343
  // the settled result carries the same one, for a `connectFn` that
package/dist/runtime.js CHANGED
@@ -34,7 +34,7 @@ import { extractLocatorFromToolResult, parseMetaFromGlobal, parseMetaFromToolRes
34
34
  import { documentViewRoots, userActivationIsActive, withViewProof } from './view-origin.js';
35
35
  import { createStreamSeqTracker } from './stream-seq.js';
36
36
  import { createLadderSet } from './ladders.js';
37
- import { BOOT_REFRESH_RETRIES, createCredentialController, } from './credential-controller.js';
37
+ import { REFRESH_RELAY_ERROR_RETRIES, createCredentialController, } from './credential-controller.js';
38
38
  import { projectHostContext } from '@ggui-ai/protocol/wire';
39
39
  import { installDismissIntentListener } from './dismiss-intent.js';
40
40
  import { App, PostMessageTransport } from '@modelcontextprotocol/ext-apps';
@@ -832,7 +832,7 @@ export async function bootSequence(opts) {
832
832
  // to a live channel, so a relay error there is re-sent (F6); a slice with
833
833
  // static content goes on with whatever the refresh answered.
834
834
  if (held !== undefined && credentialController !== null) {
835
- const outcome = await credentialController.onExpired(held, 'boot', hasStaticContent ? undefined : { retries: BOOT_REFRESH_RETRIES });
835
+ const outcome = await credentialController.onExpired(held, 'boot', hasStaticContent ? undefined : { retries: REFRESH_RELAY_ERROR_RETRIES });
836
836
  telemetry?.record('boot.credential_refresh', outcome.kind === 'refused' ? `refused:${outcome.code}` : outcome.kind);
837
837
  if (outcome.kind === 'adopted')
838
838
  liveMeta = withLiveCredential(meta, outcome.credential);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ggui-ai/iframe-runtime",
3
- "version": "0.27.0",
4
- "description": "The iframe-local ggui runtime: a self-contained ESM bundle that boots inside an MCP Apps host iframe, runs the protocol-version handshake, opens the live-channel WebSocket, and mounts the rendered UI. Bundles React, ReactDOM, and the ggui design system so generated component code has a single seam to read from.",
3
+ "version": "0.28.0",
4
+ "description": "The iframe-local GGUI runtime: a self-contained ESM bundle that boots inside an MCP Apps host iframe, runs the protocol-version handshake, opens the live-channel WebSocket, and mounts the rendered UI. Bundles React, ReactDOM, and the GGUI design system so generated component code has a single seam to read from.",
5
5
  "keywords": [
6
6
  "ggui",
7
7
  "mcp",
@@ -42,12 +42,12 @@
42
42
  "react": "^19.0.0",
43
43
  "react-dom": "^19.0.0",
44
44
  "zod": "^4.3.6",
45
- "@ggui-ai/design": "0.27.0",
46
- "@ggui-ai/gadgets": "0.27.0",
47
- "@ggui-ai/live-channel": "0.27.0",
48
- "@ggui-ai/preview-a2ui": "0.27.0",
49
- "@ggui-ai/protocol": "0.27.0",
50
- "@ggui-ai/wire": "0.27.0"
45
+ "@ggui-ai/design": "0.28.0",
46
+ "@ggui-ai/gadgets": "0.28.0",
47
+ "@ggui-ai/live-channel": "0.28.0",
48
+ "@ggui-ai/preview-a2ui": "0.28.0",
49
+ "@ggui-ai/protocol": "0.28.0",
50
+ "@ggui-ai/wire": "0.28.0"
51
51
  },
52
52
  "peerDependencies": {
53
53
  "@modelcontextprotocol/sdk": "^1.27.0"