/** * Web (browser / Next.js) host adapters for {@link useAgentInvoke}. * * Studio builds its bundle via {@link createWebAdapters}. The implementations * touch `window.localStorage`, `crypto`, and `fetch` only inside their * functions — never at module load — so this file is import-safe under SSR * (the functions guard on `typeof window`). */ import { type McpToolCallResult, type ResolvedViewMount, type UiActionRequest } from "@guuey/mcp-apps-host"; import type { AgHitlAnswer } from "@silverprotocol/core"; import type { AgentInvokeAdapters, ThreadIdStore } from "./types.js"; import type { SaturationRetryOptions } from "./saturation-retry.js"; /** Persists the threadId in `window.localStorage` (synchronously). */ export declare const localStorageThreadStore: ThreadIdStore; /** Crypto-strong client-message id, with a non-crypto fallback. */ export declare function webGenerateId(): string; export interface CreateWebAdaptersOptions { /** * Public read-plane base (ending in `/v1`) for transcript history. When * omitted, no history adapter is installed and reloads start empty. */ apiBaseUrl?: string; /** * Total send attempts on a saturated pod (guuey#406) — forwarded to * {@link SaturationRetryOptions.attempts}. End-user surfaces facing * capacity-1 pods (demo fixtures, xs plans) budget higher than the * 2-attempt default and pair it with {@link onSaturationWait} so the * wait is a visible busy state. */ saturationAttempts?: number; /** Forwarded to {@link SaturationRetryOptions.onSaturationWait}. */ onSaturationWait?: SaturationRetryOptions["onSaturationWait"]; /** * Resolve the caller's Cognito access token (fresh), or `null` when signed * out. When a token is present the chat transport AND the history read * authenticate as that user, so a reload restores the transcript. Without * a token, identity falls to {@link getGuestSecret} (if supplied) and then * to the guest cookie. * * Called with `{ forceRefresh: true }` exactly once: when the history read * gets a 401 on a token this resolver already returned (a token cached * before the mount-time history read fired can be stale by the time it * runs — the same window the send path's own 401-retry closes). A resolver * that caches (Amplify's `fetchAuthSession` does, and so does the widget's * `createHostTokenProvider`) MUST bypass that cache for a forced call and * obtain a genuinely fresh token — returning the SAME stale value would * make the retry indistinguishable from not retrying at all. A resolver * with nothing fresher to offer returns `null`, and the read surfaces the * ORIGINAL 401 rather than replaying the value that just failed. */ getAccessToken?: (opts?: { forceRefresh?: boolean; }) => Promise; /** * Resolve the caller's own persisted anonymous guest secret (64 lowercase * hex chars), or `null` when there is none. Supply this on hosts whose * cookie jar can't carry the pod's HttpOnly `guuey_guest` — notably the * embedded widget, a third-party iframe whose cookies browsers partition * or block outright. * * With a secret, BOTH the chat transport and the history read send * `x-guuey-guest`, so an anonymous transcript replays on reload the same * way a signed-in one does — the read plane identifies a guest by that * header (it cannot see the HttpOnly cookie, which is why a cookie-only * caller still gets no history). * * Called once per request, so a rotated secret takes effect immediately. * A value that isn't 64 lowercase hex is ignored (never sent) and the * request falls through to cookie mode. * * **Supply at most ONE identity resolver per mode.** Anonymous hosts pass * this one; identified hosts pass {@link getAccessToken} and surface a token * failure rather than continuing. Passing BOTH is a hazard, not a fallback * chain: `getAccessToken` resolving `null` is indistinguishable here from * "signed out on purpose", so a merely *expired or unavailable* token * silently downgrades the caller to the anonymous identity. The request then * SUCCEEDS — the pod accepts anonymous invokes unconditionally — but the * turns land in a different thread (the pod forks on an owner mismatch * rather than appending), unreachable from the identified session, which * gets its own transcript back minus those turns on the next good load. A * 401-then-re-request-token retry loop is exactly this window. * * MUST be synchronous and MUST NOT throw: a throw propagates and fails the * invoke. This is a real hazard for the widget, not a formality — * `localStorage` access raises `SecurityError` in a third-party iframe with * storage blocked (Safari's default for embedded content), which is normal * operation here. A host reading storage owns that handling and MUST return * `null` on a blocked read, the way {@link localStorageThreadStore} does for * the threadId; `null` degrades to cookie mode, whereas a throw takes the * chat down. Deliberately NOT caught at this seam: catching a host-supplied * callback would also swallow ordinary host bugs into a silent anonymous * downgrade — the same failure this docblock warns about above. */ getGuestSecret?: () => string | null; } /** * Build the web host-adapter bundle for {@link useAgentInvoke}. Pass an * access-token resolver and/or a guest-secret resolver (plus the read-plane * base) to give the chat transport an identity the read plane can also see, * which is what enables transcript restore on reload; omit both for a * cookie-only, history-less bundle. */ export declare function createWebAdapters(opts?: CreateWebAdaptersOptions): AgentInvokeAdapters; /** Options for {@link createUiResourceReader} — same credential surface as the history adapter. */ export interface CreateUiResourceReaderOptions { /** * The guuey public API base (`…/v1`) — the PLATFORM door's half. * Optional since guuey#368's local-dev fix: a surface with only a pod * (a `guuey dev` scaffold, any podful host pre-link) builds a POD-ONLY * reader — live locators resolve; persisted-door reads are honest * misses. The platform door needs BOTH this and `threadId`. */ apiBaseUrl?: string; /** * The thread whose persisted locators this reader may resolve — the * platform door's scope. Optional for the same pod-only case: before a * thread exists (or where the transport never mints one — local dev), * the pod door alone serves live turns, which is exactly the window * where a threadId cannot exist yet. The OLD gate ("no thread ⇒ no * read") starved that window to zero requests — the docs-lab repro. */ threadId?: string; /** * The pod base (or full invoke URL — same normalization as the invoke * transport). When set, the reader tries the POD door first * (`GET /agent/ui-resource`, guuey#209 C1): the pod is the only * party that can vouch for a locator whose turn is still streaming — * persisted `kind:'card'` rows land at turn COMPLETION, so the platform * door 404s mid-turn by construction. Completed turns 404 on the pod * (past its grace window) and resolve on the platform door instead: one * authority per lifecycle phase, and this reader tries both in that * order. Absent → platform door only (pre-#209 behavior) — which means * **a card produced mid-turn cannot resolve until its turn completes**: * under a producer that inlines no mount material (any plain-locator MCP * server, ggui's read-plane-only posture) every fresh card renders * "expired" until reload. A live surface that holds an invoke endpoint * MUST pass it here; omitting it is only correct for a pure history * viewer with no pod (SelfHostedThreadViewer). The reader warns once at * construction when a platform door is configured without a pod door, * because the failure it prevents is silent by nature (guuey#209 / * ggui cac966a2d — both first external embeds shipped without it). */ endpointUrl?: string | null; /** Signed-in bearer — wins over the guest secret (same rule as the transport). */ getAccessToken?: (opts?: { forceRefresh?: boolean; }) => Promise; /** Caller-owned anonymous guest secret (widget / guest chat). */ guestSecret?: string | null; /** Injectable for tests. */ fetchImpl?: typeof fetch; } /** @internal test seam — the once-flag is module state; suites reset it between cases. */ export declare function __resetReaderEndpointWarning(): void; /** @internal test seam — the once-flag is module state; suites reset it between cases. */ export declare function __resetRelayEndpointWarning(): void; /** * Build a `UiResourceReader` over guuey's authenticated resources/read * doors — the pod door for LIVE turns (guuey#209 C1: * `GET /agent/ui-resource?uri=…`, when {@link CreateUiResourceReaderOptions.endpointUrl} * is set) and the platform proxy for persisted locators (guuey#122 Gap 1: * `GET /v1/threads/:threadId/ui-resource?uri=…`). Both doors answer the * same body and speak the same identity (bearer wins, guest header * otherwise — the pod's `resolveIdentity` and the proxy's identity chain * accept the identical carriers), so one parse serves both. * * This is `@guuey/mcp-apps-host`'s `createMcpUiResourceReader` assembly over * a guuey-platform transport (guuey#127) — channel resolution and payload * narrowing live in the host package; only the transport is guuey-shaped. * The doors own EVERYTHING trust-shaped: caller identity (the same three * families as the history read), tenancy (the pod's live-card ledger; the * proxy's thread-ownership + locator-to-thread scope guard), and the * per-user federation mint. This transport only carries the surface's * existing credential and maps EVERY non-OK — 401/403/404/502 alike — to * "try the next door", and a miss on the last door to `undefined`: deny is * byte-identical to a miss, and a miss renders the host's placeholder, * never an error surface. */ export declare function createUiResourceReader(options: CreateUiResourceReaderOptions): (resourceUri: string, hints?: { origin?: "live" | "history"; }) => Promise; /** Options for {@link createUiActionRelay} — same credential surface as the reader. */ export interface CreateUiActionRelayOptions { /** The guuey public API base (`…/v1`). */ apiBaseUrl: string; /** The thread whose persisted cards this relay may act for. */ threadId: string; /** * The surface's invoke endpoint (pod base URL or full `/agent/invoke` * URL). When set, actions POST to the POD's live door first * (`POST /agent/ui-action`, guuey#222) — the only authority that * can relay a click for a card whose turn is still streaming (persisted * `kind:'card'` rows land at turn COMPLETION, so the platform door 404s * mid-turn by construction). A pod 404 (not live, or past the ledger's * grace window) falls through to the platform door; every other pod * answer is terminal for the same reason it would be on the platform * door. Absent → platform door only (pre-#222 behavior): **a click on a * card produced mid-turn cannot reach the agent until its turn * completes** — the exact "no moment where a click both resolves AND * finds a live consumer" defect. A live surface MUST pass it; omitting it * is only correct for a pure history viewer with no pod. The relay warns * once at construction when a platform door is configured without a pod * door (same guardrail as {@link createUiResourceReader}). */ endpointUrl?: string | null; /** Signed-in bearer — wins over the guest secret (same rule as the transport). */ getAccessToken?: (opts?: { forceRefresh?: boolean; }) => Promise; /** Caller-owned anonymous guest secret (widget / guest chat). */ guestSecret?: string | null; /** * Fired ONCE when a card's `ggui_runtime_pull` circuit opens (guuey#1249 * item 4) — the live session is unrestorable. Threaded straight to * {@link createMcpUiActionRelay}; the surface shows a visible "session * ended" state + drops the stale thread so a bounded circuit isn't a * silent frozen card. */ onSessionUnrestorable?: (resourceUri: string) => void; /** Injectable for tests. */ fetchImpl?: typeof fetch; } /** * Build the card action relay over guuey's authenticated `tools/call` proxy * (guuey#158: `POST /v1/threads/:threadId/ui-action`) — the mirror of * {@link createUiResourceReader}. Allowlisting, arm narrowing, and the * never-reject contract live in `@guuey/mcp-apps-host`'s * `createMcpUiActionRelay`; only the transport is guuey-shaped. The proxy * owns EVERYTHING trust-shaped (identity, thread ownership, the * locator-to-thread guard, its own server-side allowlist, the per-user * federation mint) — and every non-OK here collapses to `undefined`, which * the host relay answers in-band as an `isError` result, never a thrown * error into the sandbox bridge. */ export declare function createUiActionRelay(options: CreateUiActionRelayOptions): (request: UiActionRequest) => Promise; /** Options for {@link createHitlAnswerRelay} — the same credential surface as the card relays. */ export interface CreateHitlAnswerRelayOptions { /** The surface's invoke endpoint (pod base URL or full `/agent/invoke` URL) — the answer door lives on the pod. */ endpointUrl: string; /** Signed-in bearer — wins over the guest secret (same rule as the transport). */ getAccessToken?: (opts?: { forceRefresh?: boolean; }) => Promise; /** Caller-owned anonymous guest secret (widget / guest chat). */ guestSecret?: string | null; /** Injectable for tests. */ fetchImpl?: typeof fetch; } /** * The pod's answer to a delivered {@link AgHitlAnswer}. `ok` carries the * body the door returns (`askId`, echoed `status`, and — for a recorded * consent — the grant `mode` written); every non-2xx collapses to the pod's * `{ code, message }` envelope (the same vocabulary as `AGENT_ERROR_CODES`, * e.g. `NOT_FOUND` for an ask this pod did not mint, `INVALID_REQUEST` for a * spec-invalid answer) with the HTTP status; a transport failure is * `status: 0` with a null code. */ export type HitlAnswerRelayResult = { ok: true; body: { askId: string; status: AgHitlAnswer["status"]; mode?: string; }; } | { ok: false; status: number; code: string | null; message: string; }; /** * Build the client→pod channel for AgJSON HITL answers (guuey#207): `POST * /agent/hitl-answer` with the spec {@link AgHitlAnswer} the kit's * `answerHitlPrompt` constructed (already validated against the ask's * persisted declaration). The pod owns EVERYTHING trust-shaped — caller * identity (the same three families as the invoke), which ask it minted, * the thread a `once` grant binds to, the access level written — this * transport only carries the surface's existing credential under the * one-carrier rule (bearer → guest header → cookie), with the card relays' * single 401 forceRefresh retry. * * Today the only producer is the pod's cross-app profile consent ask (the * three-mode grant), whose answer resolves into the caller's own * `ProfileGrant` row; the channel is generic by construction — any future * `hitl.ask` the runtime emits is answered through this same door. */ export declare function createHitlAnswerRelay(options: CreateHitlAnswerRelayOptions): (answer: AgHitlAnswer) => Promise; /** Options for {@link deleteThread} — the same credential surface as the sibling platform doors. */ export interface DeleteThreadOptions { /** The guuey public API base (`…/v1`) — the same base the transcript reads use. */ apiBaseUrl: string; /** The thread to erase. Must be the id the SAME identity created. */ threadId: string; /** Signed-in bearer — wins over the guest secret (same rule as the transport). */ getAccessToken?: (opts?: { forceRefresh?: boolean; }) => Promise; /** * Caller-owned anonymous guest secret (widget / guest chat). Callers that * rotate the secret as part of a clear MUST capture this value BEFORE * rotating — the delete authenticates as the identity that OWNS the * thread, and a post-rotation secret is a stranger to it (403). */ guestSecret?: string | null; /** Injectable for tests. */ fetchImpl?: typeof fetch; } /** * The delete door's outcome, collapsed to what a caller can act on: * `"deleted"` — the server no longer holds the thread (200, or the * contract's 404-idempotent arm: the row is already gone); `"denied"` — * the credential did not own the thread (401/403; guuey#526's * unlinkability fallback applies: clear locally anyway, surface nothing * louder than a debug line); `"failed"` — transport or server failure * (the thread may still exist server-side). */ export type DeleteThreadResult = "deleted" | "denied" | "failed"; /** * `DELETE /v1/threads/:threadId` — the guest-erasure door (guuey#526). * * Real, child-first server-side deletion (messages → fold snapshot → * thread row last; replayable on partial failure — the server half's * contract, c6056b679). Identity is EXACTLY the transcript read's: * one carrier per call, bearer → guest header → else cookie credentials, * with the reader's single forceRefresh retry on a 401 bearer. Never * throws — every failure collapses into the result union. */ export declare function deleteThread(options: DeleteThreadOptions): Promise; //# sourceMappingURL=web-adapters.d.ts.map