@guuey/agent-client 0.7.0 → 0.7.2
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/web-adapters.d.ts +20 -0
- package/dist/web-adapters.d.ts.map +1 -1
- package/dist/web-adapters.js +66 -6
- package/package.json +2 -2
- package/src/web-adapters.ts +96 -10
package/dist/web-adapters.d.ts
CHANGED
|
@@ -127,6 +127,8 @@ export interface CreateUiResourceReaderOptions {
|
|
|
127
127
|
}
|
|
128
128
|
/** @internal test seam — the once-flag is module state; suites reset it between cases. */
|
|
129
129
|
export declare function __resetReaderEndpointWarning(): void;
|
|
130
|
+
/** @internal test seam — the once-flag is module state; suites reset it between cases. */
|
|
131
|
+
export declare function __resetRelayEndpointWarning(): void;
|
|
130
132
|
/**
|
|
131
133
|
* Build a `UiResourceReader` over guuey's authenticated resources/read
|
|
132
134
|
* doors — the pod door for LIVE turns (guuey#209 C1:
|
|
@@ -156,6 +158,24 @@ export interface CreateUiActionRelayOptions {
|
|
|
156
158
|
apiBaseUrl: string;
|
|
157
159
|
/** The thread whose persisted cards this relay may act for. */
|
|
158
160
|
threadId: string;
|
|
161
|
+
/**
|
|
162
|
+
* The surface's invoke endpoint (pod base URL or full `/agent/invoke`
|
|
163
|
+
* URL). When set, actions POST to the POD's live door first
|
|
164
|
+
* (`POST <pod>/agent/ui-action`, guuey#222) — the only authority that
|
|
165
|
+
* can relay a click for a card whose turn is still streaming (persisted
|
|
166
|
+
* `kind:'card'` rows land at turn COMPLETION, so the platform door 404s
|
|
167
|
+
* mid-turn by construction). A pod 404 (not live, or past the ledger's
|
|
168
|
+
* grace window) falls through to the platform door; every other pod
|
|
169
|
+
* answer is terminal for the same reason it would be on the platform
|
|
170
|
+
* door. Absent → platform door only (pre-#222 behavior): **a click on a
|
|
171
|
+
* card produced mid-turn cannot reach the agent until its turn
|
|
172
|
+
* completes** — the exact "no moment where a click both resolves AND
|
|
173
|
+
* finds a live consumer" defect. A live surface MUST pass it; omitting it
|
|
174
|
+
* is only correct for a pure history viewer with no pod. The relay warns
|
|
175
|
+
* once at construction when a platform door is configured without a pod
|
|
176
|
+
* door (same guardrail as {@link createUiResourceReader}).
|
|
177
|
+
*/
|
|
178
|
+
endpointUrl?: string | null;
|
|
159
179
|
/** Signed-in bearer — wins over the guest secret (same rule as the transport). */
|
|
160
180
|
getAccessToken?: (opts?: {
|
|
161
181
|
forceRefresh?: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web-adapters.d.ts","sourceRoot":"","sources":["../src/web-adapters.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAIL,KAAK,iBAAiB,EAEtB,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACrB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,mBAAmB,EAAmB,aAAa,EAAE,MAAM,YAAY,CAAC;AAKtF,sEAAsE;AACtE,eAAO,MAAM,uBAAuB,EAAE,aAiBrC,CAAC;AAEF,mEAAmE;AACnE,wBAAgB,aAAa,IAAI,MAAM,CAKtC;AAED,MAAM,WAAW,wBAAwB;IACvC;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;;;;;;;;;;;;;OAiBG;IACH,cAAc,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC/E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAuCG;IACH,cAAc,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;CACtC;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,GAAE,wBAA6B,GAClC,mBAAmB,CA8ErB;AAED,mGAAmG;AACnG,MAAM,WAAW,6BAA6B;IAC5C,0CAA0C;IAC1C,UAAU,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,kFAAkF;IAClF,cAAc,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC/E,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,4BAA4B;IAC5B,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAID,0FAA0F;AAC1F,wBAAgB,4BAA4B,IAAI,IAAI,CAEnD;
|
|
1
|
+
{"version":3,"file":"web-adapters.d.ts","sourceRoot":"","sources":["../src/web-adapters.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAIL,KAAK,iBAAiB,EAEtB,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACrB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,mBAAmB,EAAmB,aAAa,EAAE,MAAM,YAAY,CAAC;AAKtF,sEAAsE;AACtE,eAAO,MAAM,uBAAuB,EAAE,aAiBrC,CAAC;AAEF,mEAAmE;AACnE,wBAAgB,aAAa,IAAI,MAAM,CAKtC;AAED,MAAM,WAAW,wBAAwB;IACvC;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;;;;;;;;;;;;;OAiBG;IACH,cAAc,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC/E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAuCG;IACH,cAAc,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;CACtC;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,GAAE,wBAA6B,GAClC,mBAAmB,CA8ErB;AAED,mGAAmG;AACnG,MAAM,WAAW,6BAA6B;IAC5C,0CAA0C;IAC1C,UAAU,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,kFAAkF;IAClF,cAAc,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC/E,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,4BAA4B;IAC5B,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAID,0FAA0F;AAC1F,wBAAgB,4BAA4B,IAAI,IAAI,CAEnD;AAcD,0FAA0F;AAC1F,wBAAgB,2BAA2B,IAAI,IAAI,CAElD;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,6BAA6B,GACrC,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC,CAsFjE;AAED,uFAAuF;AACvF,MAAM,WAAW,0BAA0B;IACzC,0CAA0C;IAC1C,UAAU,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;;;;;;;;;;;;OAgBG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,kFAAkF;IAClF,cAAc,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC/E,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,4BAA4B;IAC5B,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,0BAA0B,GAClC,CAAC,OAAO,EAAE,eAAe,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAuG1D"}
|
package/dist/web-adapters.js
CHANGED
|
@@ -135,6 +135,16 @@ export function __resetReaderEndpointWarning() {
|
|
|
135
135
|
function toUiResourceUrl(endpointUrl) {
|
|
136
136
|
return toInvokeUrl(endpointUrl).replace(/\/agent\/invoke$/, "/agent/ui-resource");
|
|
137
137
|
}
|
|
138
|
+
/** `<pod base>/agent/ui-action` — the live ACTION door (guuey#222), the read door's twin. */
|
|
139
|
+
function toUiActionUrl(endpointUrl) {
|
|
140
|
+
return toInvokeUrl(endpointUrl).replace(/\/agent\/invoke$/, "/agent/ui-action");
|
|
141
|
+
}
|
|
142
|
+
/** Warn once per module load — sibling of the reader's flag; per-surface, not per-click. */
|
|
143
|
+
let relayEndpointWarned = false;
|
|
144
|
+
/** @internal test seam — the once-flag is module state; suites reset it between cases. */
|
|
145
|
+
export function __resetRelayEndpointWarning() {
|
|
146
|
+
relayEndpointWarned = false;
|
|
147
|
+
}
|
|
138
148
|
/**
|
|
139
149
|
* Build a `UiResourceReader` over guuey's authenticated resources/read
|
|
140
150
|
* doors — the pod door for LIVE turns (guuey#209 C1:
|
|
@@ -261,8 +271,37 @@ export function createUiResourceReader(options) {
|
|
|
261
271
|
*/
|
|
262
272
|
export function createUiActionRelay(options) {
|
|
263
273
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
264
|
-
|
|
274
|
+
// A platform door without a pod door is almost always a live surface
|
|
275
|
+
// that forgot `endpointUrl` — its cards' clicks would go nowhere for the
|
|
276
|
+
// whole mid-turn window (guuey#222). `null` is the explicit "history-only
|
|
277
|
+
// viewer, there is no pod" opt-out; `undefined` is the forgotten case.
|
|
278
|
+
if (options.endpointUrl === undefined && !relayEndpointWarned) {
|
|
279
|
+
relayEndpointWarned = true;
|
|
280
|
+
console.warn("createUiActionRelay: no `endpointUrl` — a click on a card produced mid-turn cannot reach the agent until the turn completes (the pod door is the only authority while a turn streams; post-turn clicks reach the platform door). Pass the surface's invoke endpoint, or `endpointUrl: null` to declare a history-only viewer.");
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* One door: POST + the reader's 401-forceRefresh recovery. Returns the
|
|
284
|
+
* parsed result on 2xx, `"miss"` on 404 (the pod's "not live / not yours /
|
|
285
|
+
* past grace" — deny==miss, so the NEXT door may still answer), and
|
|
286
|
+
* `undefined` for every other failure (terminal: the host relay answers
|
|
287
|
+
* in-band as an `isError` result, never a thrown error into the sandbox
|
|
288
|
+
* bridge). A pod 502 UPSTREAM_UNAVAILABLE is a real failure, not a miss —
|
|
289
|
+
* the persisted door cannot relay a mid-turn click either, so falling
|
|
290
|
+
* through would only trade one honest error for a misleading 404.
|
|
291
|
+
*/
|
|
292
|
+
const postDoor = async (requestUrl, body) => {
|
|
293
|
+
// Exactly ONE identity carrier per call — the reader's rule verbatim:
|
|
294
|
+
// bearer → guest header → else cookie credentials (the HttpOnly
|
|
295
|
+
// `guuey_guest` cookie the pod mints for anonymous browser callers).
|
|
296
|
+
// Without the third arm a cookie-mode guest POSTed identity-less and
|
|
297
|
+
// every click failed auth (the guuey#221 class, on the relay). A JSON
|
|
298
|
+
// POST is always preflighted, so unlike the reader's GET this arm can
|
|
299
|
+
// never be a CORS "simple request" — which is fine because both doors
|
|
300
|
+
// answer a credentialed preflight: the pod echoes origin +
|
|
301
|
+
// `Access-Control-Allow-Credentials` on OPTIONS and every status, and
|
|
302
|
+
// the platform door's own OPTIONS branch does the same (guuey#224).
|
|
265
303
|
const headers = { "content-type": "application/json" };
|
|
304
|
+
const init = { method: "POST", headers, body };
|
|
266
305
|
const token = options.getAccessToken ? await options.getAccessToken() : null;
|
|
267
306
|
const guest = sendableGuestSecret(options.guestSecret);
|
|
268
307
|
if (token) {
|
|
@@ -271,17 +310,19 @@ export function createUiActionRelay(options) {
|
|
|
271
310
|
else if (guest) {
|
|
272
311
|
headers[GUEST_HEADER] = guest;
|
|
273
312
|
}
|
|
274
|
-
|
|
275
|
-
|
|
313
|
+
else {
|
|
314
|
+
init.credentials = "include";
|
|
315
|
+
}
|
|
276
316
|
let res;
|
|
277
317
|
try {
|
|
278
|
-
res = await fetchImpl(requestUrl,
|
|
318
|
+
res = await fetchImpl(requestUrl, init);
|
|
279
319
|
}
|
|
280
320
|
catch {
|
|
281
321
|
return undefined; // transport failure — the host relay answers in-band
|
|
282
322
|
}
|
|
283
323
|
// One forceRefresh retry on 401 with a bearer in play — the same
|
|
284
|
-
// expired-but-refreshable recovery the reader performs.
|
|
324
|
+
// expired-but-refreshable recovery the reader performs. The retry
|
|
325
|
+
// carries the fresh bearer and nothing else (same one-carrier rule).
|
|
285
326
|
if (res.status === 401 && options.getAccessToken) {
|
|
286
327
|
const fresh = await options.getAccessToken({ forceRefresh: true }).catch(() => null);
|
|
287
328
|
if (fresh) {
|
|
@@ -297,14 +338,33 @@ export function createUiActionRelay(options) {
|
|
|
297
338
|
}
|
|
298
339
|
}
|
|
299
340
|
}
|
|
341
|
+
if (res.status === 404)
|
|
342
|
+
return "miss";
|
|
300
343
|
if (!res.ok)
|
|
301
344
|
return undefined;
|
|
302
345
|
try {
|
|
303
|
-
return (await res.json());
|
|
346
|
+
return { kind: "result", value: (await res.json()) };
|
|
304
347
|
}
|
|
305
348
|
catch {
|
|
306
349
|
return undefined;
|
|
307
350
|
}
|
|
308
351
|
};
|
|
352
|
+
const podUrl = options.endpointUrl ? toUiActionUrl(options.endpointUrl) : null;
|
|
353
|
+
const callTool = async (uri, name, args) => {
|
|
354
|
+
// The kit sends only what the click carries; the pod overwrites any
|
|
355
|
+
// sessionId/appId from the authorized locator + its own binding.
|
|
356
|
+
const body = JSON.stringify({ uri, name, ...(args !== undefined ? { arguments: args } : {}) });
|
|
357
|
+
if (podUrl !== null) {
|
|
358
|
+
const live = await postDoor(podUrl, body);
|
|
359
|
+
if (live === undefined)
|
|
360
|
+
return undefined; // terminal on the pod — no fall-through
|
|
361
|
+
if (live !== "miss")
|
|
362
|
+
return live.value;
|
|
363
|
+
// 404 → not live here (completed turn past grace, or never live):
|
|
364
|
+
// the persisted door owns it.
|
|
365
|
+
}
|
|
366
|
+
const persisted = await postDoor(`${options.apiBaseUrl}/threads/${encodeURIComponent(options.threadId)}/ui-action`, body);
|
|
367
|
+
return persisted === undefined || persisted === "miss" ? undefined : persisted.value;
|
|
368
|
+
};
|
|
309
369
|
return createMcpUiActionRelay({ callTool });
|
|
310
370
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@guuey/agent-client",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Client SDK for Guuey's agent runtime: the `useAgentInvoke` React hook + pure SSE helpers that speak the /agent/invoke streaming contract, plus the paginated thread-history read plane. Host adapters (storage / id / transport) are injected, so it runs on web (Next) and React Native alike.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@silverprotocol/core": "0.5.0",
|
|
38
|
-
"@guuey/mcp-apps-host": "0.7.
|
|
38
|
+
"@guuey/mcp-apps-host": "0.7.2"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"react": ">=18"
|
package/src/web-adapters.ts
CHANGED
|
@@ -252,6 +252,18 @@ function toUiResourceUrl(endpointUrl: string): string {
|
|
|
252
252
|
return toInvokeUrl(endpointUrl).replace(/\/agent\/invoke$/, "/agent/ui-resource");
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
+
/** `<pod base>/agent/ui-action` — the live ACTION door (guuey#222), the read door's twin. */
|
|
256
|
+
function toUiActionUrl(endpointUrl: string): string {
|
|
257
|
+
return toInvokeUrl(endpointUrl).replace(/\/agent\/invoke$/, "/agent/ui-action");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Warn once per module load — sibling of the reader's flag; per-surface, not per-click. */
|
|
261
|
+
let relayEndpointWarned = false;
|
|
262
|
+
/** @internal test seam — the once-flag is module state; suites reset it between cases. */
|
|
263
|
+
export function __resetRelayEndpointWarning(): void {
|
|
264
|
+
relayEndpointWarned = false;
|
|
265
|
+
}
|
|
266
|
+
|
|
255
267
|
/**
|
|
256
268
|
* Build a `UiResourceReader` over guuey's authenticated resources/read
|
|
257
269
|
* doors — the pod door for LIVE turns (guuey#209 C1:
|
|
@@ -370,6 +382,24 @@ export interface CreateUiActionRelayOptions {
|
|
|
370
382
|
apiBaseUrl: string;
|
|
371
383
|
/** The thread whose persisted cards this relay may act for. */
|
|
372
384
|
threadId: string;
|
|
385
|
+
/**
|
|
386
|
+
* The surface's invoke endpoint (pod base URL or full `/agent/invoke`
|
|
387
|
+
* URL). When set, actions POST to the POD's live door first
|
|
388
|
+
* (`POST <pod>/agent/ui-action`, guuey#222) — the only authority that
|
|
389
|
+
* can relay a click for a card whose turn is still streaming (persisted
|
|
390
|
+
* `kind:'card'` rows land at turn COMPLETION, so the platform door 404s
|
|
391
|
+
* mid-turn by construction). A pod 404 (not live, or past the ledger's
|
|
392
|
+
* grace window) falls through to the platform door; every other pod
|
|
393
|
+
* answer is terminal for the same reason it would be on the platform
|
|
394
|
+
* door. Absent → platform door only (pre-#222 behavior): **a click on a
|
|
395
|
+
* card produced mid-turn cannot reach the agent until its turn
|
|
396
|
+
* completes** — the exact "no moment where a click both resolves AND
|
|
397
|
+
* finds a live consumer" defect. A live surface MUST pass it; omitting it
|
|
398
|
+
* is only correct for a pure history viewer with no pod. The relay warns
|
|
399
|
+
* once at construction when a platform door is configured without a pod
|
|
400
|
+
* door (same guardrail as {@link createUiResourceReader}).
|
|
401
|
+
*/
|
|
402
|
+
endpointUrl?: string | null;
|
|
373
403
|
/** Signed-in bearer — wins over the guest secret (same rule as the transport). */
|
|
374
404
|
getAccessToken?: (opts?: { forceRefresh?: boolean }) => Promise<string | null>;
|
|
375
405
|
/** Caller-owned anonymous guest secret (widget / guest chat). */
|
|
@@ -394,29 +424,61 @@ export function createUiActionRelay(
|
|
|
394
424
|
options: CreateUiActionRelayOptions,
|
|
395
425
|
): (request: UiActionRequest) => Promise<McpToolCallResult> {
|
|
396
426
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
427
|
+
// A platform door without a pod door is almost always a live surface
|
|
428
|
+
// that forgot `endpointUrl` — its cards' clicks would go nowhere for the
|
|
429
|
+
// whole mid-turn window (guuey#222). `null` is the explicit "history-only
|
|
430
|
+
// viewer, there is no pod" opt-out; `undefined` is the forgotten case.
|
|
431
|
+
if (options.endpointUrl === undefined && !relayEndpointWarned) {
|
|
432
|
+
relayEndpointWarned = true;
|
|
433
|
+
console.warn(
|
|
434
|
+
"createUiActionRelay: no `endpointUrl` — a click on a card produced mid-turn cannot reach the agent until the turn completes (the pod door is the only authority while a turn streams; post-turn clicks reach the platform door). Pass the surface's invoke endpoint, or `endpointUrl: null` to declare a history-only viewer.",
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* One door: POST + the reader's 401-forceRefresh recovery. Returns the
|
|
440
|
+
* parsed result on 2xx, `"miss"` on 404 (the pod's "not live / not yours /
|
|
441
|
+
* past grace" — deny==miss, so the NEXT door may still answer), and
|
|
442
|
+
* `undefined` for every other failure (terminal: the host relay answers
|
|
443
|
+
* in-band as an `isError` result, never a thrown error into the sandbox
|
|
444
|
+
* bridge). A pod 502 UPSTREAM_UNAVAILABLE is a real failure, not a miss —
|
|
445
|
+
* the persisted door cannot relay a mid-turn click either, so falling
|
|
446
|
+
* through would only trade one honest error for a misleading 404.
|
|
447
|
+
*/
|
|
448
|
+
const postDoor = async (
|
|
449
|
+
requestUrl: string,
|
|
450
|
+
body: string,
|
|
451
|
+
): Promise<{ kind: "result"; value: unknown } | "miss" | undefined> => {
|
|
452
|
+
// Exactly ONE identity carrier per call — the reader's rule verbatim:
|
|
453
|
+
// bearer → guest header → else cookie credentials (the HttpOnly
|
|
454
|
+
// `guuey_guest` cookie the pod mints for anonymous browser callers).
|
|
455
|
+
// Without the third arm a cookie-mode guest POSTed identity-less and
|
|
456
|
+
// every click failed auth (the guuey#221 class, on the relay). A JSON
|
|
457
|
+
// POST is always preflighted, so unlike the reader's GET this arm can
|
|
458
|
+
// never be a CORS "simple request" — which is fine because both doors
|
|
459
|
+
// answer a credentialed preflight: the pod echoes origin +
|
|
460
|
+
// `Access-Control-Allow-Credentials` on OPTIONS and every status, and
|
|
461
|
+
// the platform door's own OPTIONS branch does the same (guuey#224).
|
|
402
462
|
const headers: Record<string, string> = { "content-type": "application/json" };
|
|
463
|
+
const init: RequestInit = { method: "POST", headers, body };
|
|
403
464
|
const token = options.getAccessToken ? await options.getAccessToken() : null;
|
|
404
465
|
const guest = sendableGuestSecret(options.guestSecret);
|
|
405
466
|
if (token) {
|
|
406
467
|
headers["authorization"] = `Bearer ${token}`;
|
|
407
468
|
} else if (guest) {
|
|
408
469
|
headers[GUEST_HEADER] = guest;
|
|
470
|
+
} else {
|
|
471
|
+
init.credentials = "include";
|
|
409
472
|
}
|
|
410
|
-
const requestUrl = `${options.apiBaseUrl}/threads/${encodeURIComponent(options.threadId)}/ui-action`;
|
|
411
|
-
const body = JSON.stringify({ uri, name, ...(args !== undefined ? { arguments: args } : {}) });
|
|
412
473
|
let res: Response;
|
|
413
474
|
try {
|
|
414
|
-
res = await fetchImpl(requestUrl,
|
|
475
|
+
res = await fetchImpl(requestUrl, init);
|
|
415
476
|
} catch {
|
|
416
477
|
return undefined; // transport failure — the host relay answers in-band
|
|
417
478
|
}
|
|
418
479
|
// One forceRefresh retry on 401 with a bearer in play — the same
|
|
419
|
-
// expired-but-refreshable recovery the reader performs.
|
|
480
|
+
// expired-but-refreshable recovery the reader performs. The retry
|
|
481
|
+
// carries the fresh bearer and nothing else (same one-carrier rule).
|
|
420
482
|
if (res.status === 401 && options.getAccessToken) {
|
|
421
483
|
const fresh = await options.getAccessToken({ forceRefresh: true }).catch(() => null);
|
|
422
484
|
if (fresh) {
|
|
@@ -431,12 +493,36 @@ export function createUiActionRelay(
|
|
|
431
493
|
}
|
|
432
494
|
}
|
|
433
495
|
}
|
|
496
|
+
if (res.status === 404) return "miss";
|
|
434
497
|
if (!res.ok) return undefined;
|
|
435
498
|
try {
|
|
436
|
-
return (await res.json()) as unknown;
|
|
499
|
+
return { kind: "result", value: (await res.json()) as unknown };
|
|
437
500
|
} catch {
|
|
438
501
|
return undefined;
|
|
439
502
|
}
|
|
440
503
|
};
|
|
504
|
+
|
|
505
|
+
const podUrl = options.endpointUrl ? toUiActionUrl(options.endpointUrl) : null;
|
|
506
|
+
const callTool = async (
|
|
507
|
+
uri: string,
|
|
508
|
+
name: string,
|
|
509
|
+
args: McpToolStructuredContent | undefined,
|
|
510
|
+
): Promise<unknown> => {
|
|
511
|
+
// The kit sends only what the click carries; the pod overwrites any
|
|
512
|
+
// sessionId/appId from the authorized locator + its own binding.
|
|
513
|
+
const body = JSON.stringify({ uri, name, ...(args !== undefined ? { arguments: args } : {}) });
|
|
514
|
+
if (podUrl !== null) {
|
|
515
|
+
const live = await postDoor(podUrl, body);
|
|
516
|
+
if (live === undefined) return undefined; // terminal on the pod — no fall-through
|
|
517
|
+
if (live !== "miss") return live.value;
|
|
518
|
+
// 404 → not live here (completed turn past grace, or never live):
|
|
519
|
+
// the persisted door owns it.
|
|
520
|
+
}
|
|
521
|
+
const persisted = await postDoor(
|
|
522
|
+
`${options.apiBaseUrl}/threads/${encodeURIComponent(options.threadId)}/ui-action`,
|
|
523
|
+
body,
|
|
524
|
+
);
|
|
525
|
+
return persisted === undefined || persisted === "miss" ? undefined : persisted.value;
|
|
526
|
+
};
|
|
441
527
|
return createMcpUiActionRelay({ callTool });
|
|
442
528
|
}
|