@frontera-sdk/chat 1.52.45 → 1.52.46

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 (51) hide show
  1. package/dist/auth.d.ts +20 -0
  2. package/dist/auth.js +11 -0
  3. package/dist/callback-page.d.ts +49 -0
  4. package/dist/callback-page.js +84 -0
  5. package/dist/chat-client.d.ts +221 -0
  6. package/dist/chat-client.js +5 -0
  7. package/dist/chat-errors.d.ts +30 -0
  8. package/dist/chat-errors.js +3 -0
  9. package/dist/chunk-CMMWTCYS.js +116 -0
  10. package/dist/chunk-KZEGEV3P.js +35 -0
  11. package/dist/chunk-LS26G2N6.js +120 -0
  12. package/dist/chunk-RLHHRJWS.js +371 -0
  13. package/dist/chunk-UCUVHMJC.js +62 -0
  14. package/dist/chunk-VWUR3ECR.js +115 -0
  15. package/dist/chunk-W25Q2DQA.js +32 -0
  16. package/dist/chunk-YKQ5Z5EK.js +32 -0
  17. package/dist/connect-popup.d.ts +150 -0
  18. package/dist/connect-popup.js +138 -0
  19. package/dist/doctor.d.ts +71 -0
  20. package/dist/doctor.js +3 -0
  21. package/dist/element.d.ts +23 -0
  22. package/dist/element.js +175 -0
  23. package/dist/frontera-chat.iife.js +27 -0
  24. package/dist/hooks.d.ts +88 -0
  25. package/dist/hooks.js +3 -0
  26. package/dist/provider.d.ts +20 -0
  27. package/dist/provider.js +13 -0
  28. package/dist/public-chat.d.ts +50 -0
  29. package/dist/public-chat.js +6 -0
  30. package/dist/public-token.d.ts +46 -0
  31. package/dist/public-token.js +3 -0
  32. package/dist/reply-text.d.ts +25 -0
  33. package/dist/reply-text.js +3 -0
  34. package/dist/stream.d.ts +25 -0
  35. package/dist/stream.js +3 -0
  36. package/dist/token-session.d.ts +66 -0
  37. package/dist/token-session.js +99 -0
  38. package/dist/transport.d.ts +60 -0
  39. package/dist/transport.js +103 -0
  40. package/dist/types.d.ts +361 -0
  41. package/dist/types.js +3 -0
  42. package/dist/ui-stream.d.ts +25 -0
  43. package/dist/ui-stream.js +26 -0
  44. package/package.json +70 -30
  45. package/src/chat-errors.ts +43 -0
  46. package/src/element.ts +201 -0
  47. package/src/frontera-chat.global.ts +13 -0
  48. package/src/public-chat.ts +154 -0
  49. package/src/public-token.ts +36 -4
  50. package/src/reply-text.ts +49 -0
  51. package/src/transport.ts +7 -1
@@ -0,0 +1,120 @@
1
+ import { mintPublicChatToken } from './chunk-UCUVHMJC.js';
2
+ import { startChat } from './chunk-KZEGEV3P.js';
3
+ import { chatErrorCode } from './chunk-YKQ5Z5EK.js';
4
+ import { FronteraClient } from '@frontera-sdk/core/client';
5
+ import { FronteraError } from '@frontera-sdk/core/errors';
6
+
7
+ function newId() {
8
+ return typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `m-${Date.now()}-${Math.random().toString(36).slice(2)}`;
9
+ }
10
+ async function* chatEvents(response) {
11
+ if (!response.body) return;
12
+ const reader = response.body.getReader();
13
+ const decoder = new TextDecoder();
14
+ let buffer = "";
15
+ const parse = (block) => {
16
+ const data = block.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n");
17
+ if (!data || data === "[DONE]") return null;
18
+ try {
19
+ const value = JSON.parse(data);
20
+ return value && typeof value === "object" ? value : null;
21
+ } catch {
22
+ return null;
23
+ }
24
+ };
25
+ for (; ; ) {
26
+ const { done, value } = await reader.read();
27
+ if (value) buffer += decoder.decode(value, { stream: true });
28
+ let end = buffer.indexOf("\n\n");
29
+ while (end !== -1) {
30
+ const event = parse(buffer.slice(0, end));
31
+ buffer = buffer.slice(end + 2);
32
+ if (event) yield event;
33
+ end = buffer.indexOf("\n\n");
34
+ }
35
+ if (done) break;
36
+ }
37
+ const last = parse(buffer);
38
+ if (last) yield last;
39
+ }
40
+ var PublicChat = class {
41
+ constructor(options) {
42
+ this.options = options;
43
+ }
44
+ options;
45
+ token = null;
46
+ conversationId = null;
47
+ /** Shown before the first message; sent back with it, so the conversation keeps it. */
48
+ openingMessage = null;
49
+ opened = false;
50
+ /** Mint the visitor's token, and learn the Agent's opening message. Called by `send` when needed. */
51
+ async open() {
52
+ const minted = await mintPublicChatToken({ ...this.options, ...this.options.fetchImpl ? { fetchImpl: this.options.fetchImpl } : {} });
53
+ this.token = minted.token;
54
+ if (!this.opened) this.openingMessage = minted.openingMessage ?? null;
55
+ this.opened = true;
56
+ return { openingMessage: this.openingMessage };
57
+ }
58
+ client() {
59
+ return new FronteraClient(
60
+ { apiBaseUrl: this.options.apiBaseUrl, credential: { kind: "token", token: this.token } },
61
+ this.options.fetchImpl ?? fetch
62
+ );
63
+ }
64
+ messagesFor(text) {
65
+ const user = { id: newId(), role: "user", parts: [{ type: "text", text }] };
66
+ if (this.conversationId || !this.openingMessage) return [user];
67
+ return [{ id: newId(), role: "assistant", parts: [{ type: "text", text: this.openingMessage }] }, user];
68
+ }
69
+ /** Send one message and read the reply: `waiting`, then the reply's text as it grows, then `done` (or `error`). */
70
+ async *send(text, signal) {
71
+ yield { type: "waiting" };
72
+ let response;
73
+ try {
74
+ if (!this.token) await this.open();
75
+ const start = () => startChat(this.client(), { messages: this.messagesFor(text), ...this.conversationId ? { conversationId: this.conversationId } : {} }, { signal });
76
+ let run;
77
+ try {
78
+ run = await start();
79
+ } catch (err) {
80
+ if (chatErrorCode(err) !== "UNAUTHORIZED") throw err;
81
+ await this.open();
82
+ run = await start();
83
+ }
84
+ this.conversationId = run.conversationId;
85
+ response = run.response;
86
+ } catch (err) {
87
+ yield { type: "error", error: asFronteraError(err) };
88
+ return;
89
+ }
90
+ const parts = /* @__PURE__ */ new Map();
91
+ const order = [];
92
+ const whole = () => order.map((id) => parts.get(id) ?? "").filter(Boolean).join("\n\n");
93
+ try {
94
+ for await (const event of chatEvents(response)) {
95
+ if (event.type === "text-delta" && typeof event.delta === "string") {
96
+ const id = typeof event.id === "string" ? event.id : "text";
97
+ if (!parts.has(id)) order.push(id);
98
+ parts.set(id, (parts.get(id) ?? "") + event.delta);
99
+ yield { type: "text", text: whole() };
100
+ } else if (event.type === "error") {
101
+ const message = typeof event.errorText === "string" ? event.errorText : "The reply failed.";
102
+ yield { type: "error", error: new FronteraError(message, { code: "INTERNAL_ERROR" }) };
103
+ return;
104
+ }
105
+ }
106
+ } catch (err) {
107
+ yield { type: "error", error: asFronteraError(err) };
108
+ return;
109
+ }
110
+ yield { type: "done" };
111
+ }
112
+ };
113
+ function asFronteraError(err) {
114
+ if (err instanceof FronteraError) return err;
115
+ return new FronteraError(err instanceof Error ? err.message : String(err), { code: "INTERNAL_ERROR" });
116
+ }
117
+
118
+ export { PublicChat, chatEvents };
119
+ //# sourceMappingURL=chunk-LS26G2N6.js.map
120
+ //# sourceMappingURL=chunk-LS26G2N6.js.map
@@ -0,0 +1,371 @@
1
+ import { chatDoctor } from './chunk-VWUR3ECR.js';
2
+ import { startChat, resumeStream } from './chunk-KZEGEV3P.js';
3
+ import { FronteraError } from '@frontera-sdk/core/errors';
4
+
5
+ var ChatClient = class {
6
+ constructor(client, options = {}) {
7
+ this.client = client;
8
+ this.options = options;
9
+ }
10
+ client;
11
+ options;
12
+ async authorize(conversationId) {
13
+ const gate = this.options.authorizeConversation;
14
+ if (!gate) return;
15
+ if (!await gate(conversationId)) {
16
+ throw new FronteraError(`access to conversation ${conversationId} denied locally`, {
17
+ code: "FORBIDDEN_LOCAL"
18
+ });
19
+ }
20
+ }
21
+ // --- chat runs ---
22
+ /** Start (or continue) a run. See `stream.ts` for stream semantics. */
23
+ async startChat(request, options) {
24
+ if (request.conversationId) await this.authorize(request.conversationId);
25
+ return startChat(this.client, request, options);
26
+ }
27
+ /**
28
+ * Re-attach to a run's stream. Run ids are capabilities: they only leave the
29
+ * server through a response the caller was already authorized to receive, so
30
+ * there is no conversation gate here — treat them accordingly.
31
+ */
32
+ resumeStream(runId, options) {
33
+ return resumeStream(this.client, runId, options);
34
+ }
35
+ cancelRun(runId) {
36
+ return this.client.request(`/v1/chat/${encodeURIComponent(runId)}/cancel`, { method: "POST" });
37
+ }
38
+ steerRun(runId, message) {
39
+ return this.client.request(`/v1/chat/${encodeURIComponent(runId)}/steer`, {
40
+ method: "POST",
41
+ body: { message }
42
+ });
43
+ }
44
+ // --- approvals ---
45
+ /**
46
+ * The tool calls this conversation has parked, waiting on a person.
47
+ *
48
+ * Poll or call this on mount: a run blocked on an approval stays blocked
49
+ * across a page reload, and the `tool-approval-request` chunk that announced
50
+ * it is not replayed by a fresh `useChat`. Without reading them back, the
51
+ * conversation looks like an agent that stopped for no reason.
52
+ *
53
+ * End-user (`cht_`) tokens only — see `respondToApproval`.
54
+ */
55
+ async listPendingApprovals(conversationId) {
56
+ chatDoctor.resolved("approval");
57
+ chatDoctor.resolved("connect");
58
+ await this.authorize(conversationId);
59
+ const envelope = await this.client.requestEnvelope(
60
+ "/v1/chat/approvals",
61
+ { query: { conversationId } }
62
+ );
63
+ return envelope.data;
64
+ }
65
+ /**
66
+ * Approve or deny a parked tool call, releasing (or refusing) the run.
67
+ *
68
+ * Requires an end-user `cht_` token: the decision is recorded against the
69
+ * person who made it, and an `sak_` key is one credential shared by every
70
+ * user of your product — there is nobody it could be deciding as, so the
71
+ * server answers 403 (`FORBIDDEN`). An integration that wants its agent to
72
+ * act unattended sets the agent's approval mode to `auto` instead.
73
+ *
74
+ * Deciding twice is not an error: the first decision wins and this reports
75
+ * the settled status. A lapsed request answers `expired`.
76
+ */
77
+ respondToApproval(approvalId, decision) {
78
+ chatDoctor.resolved("approval");
79
+ return this.client.request(`/v1/chat/approvals/${encodeURIComponent(approvalId)}`, {
80
+ method: "POST",
81
+ body: { decision }
82
+ });
83
+ }
84
+ // --- app connections ---
85
+ /**
86
+ * Begin connecting the end user's own account for a parked `needs_connect`
87
+ * call, and get the URL to send them to.
88
+ *
89
+ * Three things must line up before this answers, and all three are one-time
90
+ * setup rather than per-user work:
91
+ * 1. the workspace has ITS OWN OAuth client for the provider (the
92
+ * deployment-managed one cannot serve this — your users are authorising
93
+ * your application, and only its owner can register your callback);
94
+ * 2. `redirectUri` is registered with that client AND on the workspace's
95
+ * embed allowlist;
96
+ * 3. the install allows external users, and this user's token carries the
97
+ * `plugin-connect` capability you granted when you minted it.
98
+ *
99
+ * Open the returned `authUrl` in a popup. Your callback receives `code` and
100
+ * `state` — hand them to `completeAppConnection`, then `resumeToolCall`.
101
+ */
102
+ startAppConnection(request) {
103
+ chatDoctor.resolved("connect");
104
+ return this.client.request("/v1/chat/app-connections/start", {
105
+ method: "POST",
106
+ body: request
107
+ });
108
+ }
109
+ /**
110
+ * Finish a connect from the code your own callback received.
111
+ *
112
+ * `state` is signed by us and names the install, the user and the redirect
113
+ * URI, so none of the three can be swapped between the authorize request and
114
+ * this call. Pass it back exactly as it arrived.
115
+ */
116
+ completeAppConnection(request) {
117
+ return this.client.request("/v1/chat/app-connections/exchange", {
118
+ method: "POST",
119
+ body: request
120
+ });
121
+ }
122
+ /**
123
+ * Re-run the tool call that was waiting on the connection.
124
+ *
125
+ * Not optional: the agent is blocked on a tool RESULT, not on an account, so
126
+ * a connect without this leaves the conversation exactly as stuck as before.
127
+ */
128
+ resumeToolCall(pendingToolCallId) {
129
+ return this.client.request("/v1/chat/app-connections/resume", {
130
+ method: "POST",
131
+ body: { pendingToolCallId }
132
+ });
133
+ }
134
+ /**
135
+ * The agents this credential may talk to — what an agent picker renders.
136
+ *
137
+ * Scoped to the credential, so a token bound to one agent lists exactly that
138
+ * agent while a workspace-wide token lists them all. The Console listing is
139
+ * session-only and returns configuration an end user has no business seeing,
140
+ * which is why this exists separately.
141
+ */
142
+ listAgents() {
143
+ return this.client.request("/v1/chat-agents");
144
+ }
145
+ /**
146
+ * Upload a file for this user, then reference the result in
147
+ * `startChat({ fileAttachments: [uploaded] })`.
148
+ *
149
+ * A chat-scoped route, not the platform's `/v1/upload`: that one authorises
150
+ * through workspace membership, which an end user of a customer's product
151
+ * does not have and should not need.
152
+ */
153
+ async upload(file, filename) {
154
+ const form = new FormData();
155
+ form.append("file", file, filename ?? file.name ?? "upload");
156
+ const response = await this.client.requestRaw("/v1/chat-uploads", {
157
+ method: "POST",
158
+ body: form
159
+ });
160
+ const payload = await response.json();
161
+ return payload.data ?? payload;
162
+ }
163
+ /**
164
+ * A live URL for a stored upload.
165
+ *
166
+ * The URL saved on a message is presigned and expires about an hour after
167
+ * upload, so anything rendered later re-signs from the stored path — without
168
+ * this, attachments in yesterday's messages become broken images.
169
+ */
170
+ async signUpload(storagePath) {
171
+ const { url } = await this.client.request("/v1/chat-uploads/sign", {
172
+ method: "POST",
173
+ body: { path: storagePath }
174
+ });
175
+ return url;
176
+ }
177
+ /** The artifacts this user owns — documents, decks and images the agent made. */
178
+ async listArtifacts(query = {}) {
179
+ const envelope = await this.client.requestEnvelope("/v1/chat-artifacts", { query });
180
+ return { data: envelope.data, nextCursor: envelope.nextCursor ?? void 0 };
181
+ }
182
+ getArtifact(id) {
183
+ return this.client.request(`/v1/chat-artifacts/${encodeURIComponent(id)}`);
184
+ }
185
+ /**
186
+ * The artifact's contents as TEXT.
187
+ *
188
+ * Only for artifacts that are text — markdown, csv, code. Reading a PNG or a
189
+ * spreadsheet through here destroys it: `.text()` decodes the bytes as UTF-8,
190
+ * and every byte that is not valid UTF-8 becomes a replacement character on
191
+ * the way. Use `getArtifactFile` for anything binary.
192
+ */
193
+ async getArtifactContent(id) {
194
+ const response = await this.client.requestRaw(
195
+ `/v1/chat-artifacts/${encodeURIComponent(id)}/file`
196
+ );
197
+ return response.text();
198
+ }
199
+ /**
200
+ * The artifact's raw bytes, with the server's own content type.
201
+ *
202
+ * The Blob carries the type the route reported rather than one guessed from
203
+ * the format, so an <img> or a PDF reader gets what it needs without the
204
+ * caller restating it — and a wrong guess here is an image that silently
205
+ * will not display.
206
+ */
207
+ async getArtifactFile(id) {
208
+ const response = await this.client.requestRaw(
209
+ `/v1/chat-artifacts/${encodeURIComponent(id)}/file`
210
+ );
211
+ return response.blob();
212
+ }
213
+ /**
214
+ * A PDF rendering of an artifact a browser cannot open itself.
215
+ *
216
+ * `.docx` and `.pptx` are bytes with no source to fall back on, so the
217
+ * service renders a PDF beside them and this returns it — displaying one
218
+ * without shipping an OOXML reader.
219
+ *
220
+ * Only artifacts with `previewKey` set have one; anything else answers 404
221
+ * as `NOT_FOUND`. Check the field before asking, or catch it.
222
+ */
223
+ /**
224
+ * Save an edit made in the viewer — a sheet cell, a document's text.
225
+ *
226
+ * Produces a new REVISION rather than overwriting: the service re-renders to
227
+ * a versioned storage key and bumps the version, so the artifact keeps the
228
+ * history an edit implies.
229
+ */
230
+ async updateArtifact(id, patch) {
231
+ return this.client.request(
232
+ `/v1/chat-artifacts/${encodeURIComponent(id)}`,
233
+ { method: "PATCH", body: patch }
234
+ );
235
+ }
236
+ /** One revision's content — what the inline "viewing v1" panel shows. */
237
+ async getArtifactVersion(id, version) {
238
+ return this.client.request(
239
+ `/v1/chat-artifacts/${encodeURIComponent(id)}/versions/${encodeURIComponent(String(version))}`
240
+ );
241
+ }
242
+ /**
243
+ * Roll back to a revision.
244
+ *
245
+ * Writes a new revision holding the old content — nothing is deleted, so a
246
+ * restore made in error is itself undoable.
247
+ */
248
+ async restoreArtifactVersion(id, version) {
249
+ return this.client.request(
250
+ `/v1/chat-artifacts/${encodeURIComponent(id)}/versions/${encodeURIComponent(String(version))}/restore`,
251
+ { method: "POST" }
252
+ );
253
+ }
254
+ /** Revisions of one artifact, newest first. Metadata only. */
255
+ async listArtifactVersions(id) {
256
+ return this.client.request(
257
+ `/v1/chat-artifacts/${encodeURIComponent(id)}/versions`
258
+ );
259
+ }
260
+ /**
261
+ * The rendition of a PAST version, for the viewer's version picker.
262
+ *
263
+ * Separate from `getArtifactPreview` rather than an optional argument: the
264
+ * current preview lives at a different path, and a version of `0` or `NaN`
265
+ * silently falling back to "current" is how a reader ends up looking at
266
+ * today's document under yesterday's heading.
267
+ */
268
+ async getArtifactVersionPreview(id, version) {
269
+ const response = await this.client.requestRaw(
270
+ `/v1/chat-artifacts/${encodeURIComponent(id)}/versions/${encodeURIComponent(String(version))}/preview`
271
+ );
272
+ return response.blob();
273
+ }
274
+ async getArtifactPreview(id) {
275
+ const response = await this.client.requestRaw(
276
+ `/v1/chat-artifacts/${encodeURIComponent(id)}/preview`
277
+ );
278
+ return response.blob();
279
+ }
280
+ // --- sessions ---
281
+ async listSessions(query = {}) {
282
+ const envelope = await this.client.requestEnvelope("/v1/sessions", {
283
+ query: {
284
+ cursor: query.cursor,
285
+ limit: query.limit,
286
+ status: query.status,
287
+ label: query.label,
288
+ group: query.group,
289
+ search: query.search,
290
+ pinned: query.pinned,
291
+ source: query.source,
292
+ agentName: query.agentName,
293
+ agentId: query.agentId,
294
+ updatedAfter: query.updatedAfter,
295
+ updatedBefore: query.updatedBefore,
296
+ surface: query.surface
297
+ }
298
+ });
299
+ return { data: envelope.data, nextCursor: envelope.nextCursor ?? void 0 };
300
+ }
301
+ createSession(request = {}) {
302
+ return this.client.request("/v1/sessions", { method: "POST", body: request });
303
+ }
304
+ async getSession(id) {
305
+ await this.authorize(id);
306
+ return this.client.request(`/v1/sessions/${encodeURIComponent(id)}`);
307
+ }
308
+ async getMessages(id, options = {}) {
309
+ await this.authorize(id);
310
+ return this.client.request(`/v1/sessions/${encodeURIComponent(id)}/messages`, {
311
+ query: { limit: options.limit }
312
+ });
313
+ }
314
+ // The four below act on ONE person's own session, so they need a credential
315
+ // that names one: a `cht_` end-user token works, an `sak_` key answers 403
316
+ // (`FORBIDDEN`) because it stands for every user of your product at once.
317
+ //
318
+ // `getShare`/`createShare` are deliberately absent: a share link works with
319
+ // no credential at all, and whether your end users may publish a
320
+ // conversation is your product's decision to implement, not a default they
321
+ // inherit from us. Those routes are platform-only and answer 403.
322
+ /**
323
+ * Rename, pin, or restatus a conversation.
324
+ *
325
+ * Answers `{ success: true }` rather than the updated row — the service
326
+ * confirms the write and nothing more. Typed as what actually arrives: a
327
+ * `Session` here would be a promise the server never made, and the first
328
+ * caller to read `.title` off it would get `undefined` with no explanation.
329
+ * Re-read with `getSession` when you need the new state.
330
+ */
331
+ async updateSession(id, patch) {
332
+ await this.authorize(id);
333
+ return this.client.request(`/v1/sessions/${encodeURIComponent(id)}`, {
334
+ method: "PATCH",
335
+ body: patch
336
+ });
337
+ }
338
+ async deleteSession(id) {
339
+ await this.authorize(id);
340
+ await this.client.request(`/v1/sessions/${encodeURIComponent(id)}`, { method: "DELETE" });
341
+ }
342
+ async forkSession(id, messageId) {
343
+ await this.authorize(id);
344
+ return this.client.request(`/v1/sessions/${encodeURIComponent(id)}/fork`, {
345
+ method: "POST",
346
+ body: { messageId }
347
+ });
348
+ }
349
+ /** Returns the export as a raw string — this route has no JSON envelope. */
350
+ async exportSession(id, format) {
351
+ await this.authorize(id);
352
+ const response = await this.client.requestRaw(
353
+ `/v1/sessions/${encodeURIComponent(id)}/export`,
354
+ { query: { format } }
355
+ );
356
+ return response.text();
357
+ }
358
+ // --- aux ---
359
+ async getFollowUps(conversationId, messageId) {
360
+ await this.authorize(conversationId);
361
+ return this.client.request("/v1/follow-ups", { query: { conversationId, messageId } });
362
+ }
363
+ async getMessageMetadata(conversationId, messageId) {
364
+ await this.authorize(conversationId);
365
+ return this.client.request("/v1/message-metadata", { query: { conversationId, messageId } });
366
+ }
367
+ };
368
+
369
+ export { ChatClient };
370
+ //# sourceMappingURL=chunk-RLHHRJWS.js.map
371
+ //# sourceMappingURL=chunk-RLHHRJWS.js.map
@@ -0,0 +1,62 @@
1
+ import { errorFromResponse } from '@frontera-sdk/core/errors';
2
+
3
+ // src/public-token.ts
4
+ function browserLanguage() {
5
+ try {
6
+ return typeof navigator === "undefined" ? void 0 : navigator.language || void 0;
7
+ } catch {
8
+ return void 0;
9
+ }
10
+ }
11
+ function visitorPassStorageKey(publishableKey) {
12
+ return `frontera:visitor:${publishableKey.slice(0, 12)}`;
13
+ }
14
+ function defaultStorage() {
15
+ try {
16
+ return typeof localStorage === "undefined" ? null : localStorage;
17
+ } catch {
18
+ return null;
19
+ }
20
+ }
21
+ function read(storage, key) {
22
+ try {
23
+ return storage?.getItem(key) ?? null;
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+ function write(storage, key, value) {
29
+ try {
30
+ storage?.setItem(key, value);
31
+ } catch {
32
+ }
33
+ }
34
+ async function mintPublicChatToken(options) {
35
+ const storage = options.storage === void 0 ? defaultStorage() : options.storage;
36
+ const passKey = visitorPassStorageKey(options.publishableKey);
37
+ const visitorPass = read(storage, passKey);
38
+ const language = options.language ?? browserLanguage();
39
+ const response = await (options.fetchImpl ?? fetch)(`${options.apiBaseUrl}/v1/chat-tokens/public`, {
40
+ method: "POST",
41
+ headers: { "content-type": "application/json" },
42
+ // Never `include`: this call carries the key, not anyone's cookies.
43
+ credentials: "omit",
44
+ body: JSON.stringify({
45
+ publishableKey: options.publishableKey,
46
+ ...visitorPass ? { visitorPass } : {},
47
+ ...language ? { language } : {}
48
+ })
49
+ });
50
+ const body = await response.json().catch(() => null);
51
+ if (!response.ok || !body?.data) throw errorFromResponse(response.status, body);
52
+ if (body.data.visitorPass) write(storage, passKey, body.data.visitorPass);
53
+ return {
54
+ token: body.data.token,
55
+ expiresAt: body.data.expiresAt,
56
+ ...body.data.openingMessage ? { openingMessage: body.data.openingMessage } : {}
57
+ };
58
+ }
59
+
60
+ export { mintPublicChatToken, visitorPassStorageKey };
61
+ //# sourceMappingURL=chunk-UCUVHMJC.js.map
62
+ //# sourceMappingURL=chunk-UCUVHMJC.js.map
@@ -0,0 +1,115 @@
1
+ // src/doctor.ts
2
+ var DEFAULT_GRACE_MS = 1e4;
3
+ function defaultEnabled() {
4
+ const mode = typeof process !== "undefined" ? "development" : void 0;
5
+ return mode !== "production";
6
+ }
7
+ var ADVICE = {
8
+ approval: [
9
+ "A tool call is parked waiting for a person to approve it, and nothing has answered.",
10
+ "The run is alive and BLOCKED \u2014 it will not continue until it is settled or expires.",
11
+ "",
12
+ " const pending = await chat.listPendingApprovals(conversationId)",
13
+ " await chat.respondToApproval(pending[0].id, 'approve')",
14
+ "",
15
+ "Using @frontera-sdk/chat-ui, render <PendingCards conversationId={\u2026} />.",
16
+ "Read the pending list on mount too: a reload loses the stream chunk, not the block."
17
+ ].join("\n"),
18
+ connect: [
19
+ "A tool needs this user's own account and returned `needs_connect`; nothing has started a connection.",
20
+ "The agent has been told NOT to ask in prose, so if your UI stays silent, nothing is said.",
21
+ "",
22
+ " await runAppConnection(chat, { pendingToolCallId, installId, redirectUri })",
23
+ "",
24
+ "Using @frontera-sdk/chat-ui, pass `connectRedirectUri` to <FronteraChat>."
25
+ ].join("\n"),
26
+ unauthorized: [
27
+ "A request was refused with UNAUTHORIZED and no refresh is wired, so the next send will fail too.",
28
+ "",
29
+ " const session = createTokenSession({ apiBaseUrl, getToken })",
30
+ " createChatTransport(session.getClient, { onUnauthorized: session.refresh })"
31
+ ].join("\n")
32
+ };
33
+ var ChatDoctor = class {
34
+ options = {};
35
+ timers = /* @__PURE__ */ new Map();
36
+ configure(options) {
37
+ this.options = { ...this.options, ...options };
38
+ }
39
+ get enabled() {
40
+ return this.options.enabled ?? defaultEnabled();
41
+ }
42
+ warn(concern) {
43
+ const message = `[frontera-chat] ${ADVICE[concern]}`;
44
+ if (this.options.onWarn) this.options.onWarn(concern, message);
45
+ else console.warn(message);
46
+ }
47
+ /**
48
+ * Something parked. Start counting; `resolved` before the grace period ends
49
+ * cancels it.
50
+ */
51
+ parked(concern) {
52
+ if (!this.enabled || this.timers.has(concern)) return;
53
+ const timer = setTimeout(() => {
54
+ this.timers.delete(concern);
55
+ this.warn(concern);
56
+ }, this.options.graceMs ?? DEFAULT_GRACE_MS);
57
+ timer.unref?.();
58
+ this.timers.set(concern, timer);
59
+ }
60
+ /** The integration did the thing. Nothing to warn about. */
61
+ resolved(concern) {
62
+ const timer = this.timers.get(concern);
63
+ if (!timer) return;
64
+ clearTimeout(timer);
65
+ this.timers.delete(concern);
66
+ }
67
+ /** Immediate, not timed: nothing about a missing refresh improves by waiting. */
68
+ noticed(concern) {
69
+ if (this.enabled) this.warn(concern);
70
+ }
71
+ /** For tests, and for a host that unmounts a chat mid-run. */
72
+ reset() {
73
+ for (const timer of this.timers.values()) clearTimeout(timer);
74
+ this.timers.clear();
75
+ }
76
+ /**
77
+ * Watch a chat stream for the two things that park a run.
78
+ *
79
+ * The body is teed rather than parsed: this reads a copy as text and matches
80
+ * two markers, leaving the real stream untouched for the AI SDK. A full parse
81
+ * here would be a second implementation of the protocol, kept in step with
82
+ * the first by hope.
83
+ */
84
+ observe(response) {
85
+ if (!this.enabled || !response.body) return response;
86
+ const [forCaller, forDoctor] = response.body.tee();
87
+ void this.scan(forDoctor);
88
+ return new Response(forCaller, {
89
+ status: response.status,
90
+ statusText: response.statusText,
91
+ headers: response.headers
92
+ });
93
+ }
94
+ async scan(stream) {
95
+ const reader = stream.getReader();
96
+ const decoder = new TextDecoder();
97
+ try {
98
+ for (; ; ) {
99
+ const { done, value } = await reader.read();
100
+ if (done) break;
101
+ const text = decoder.decode(value, { stream: true });
102
+ if (text.includes("tool-approval-request")) this.parked("approval");
103
+ if (text.includes("needs_connect")) this.parked("connect");
104
+ }
105
+ } catch {
106
+ } finally {
107
+ reader.releaseLock();
108
+ }
109
+ }
110
+ };
111
+ var chatDoctor = new ChatDoctor();
112
+
113
+ export { chatDoctor };
114
+ //# sourceMappingURL=chunk-VWUR3ECR.js.map
115
+ //# sourceMappingURL=chunk-VWUR3ECR.js.map