dewee-webchat 0.5.0-beta.1 → 0.5.0-beta.3

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.
@@ -1,4 +1,4 @@
1
- import { A as AgentInfo } from './protocol-BbjGSfZa.js';
1
+ import { A as AgentInfo } from './protocol-DjEdC6QU.js';
2
2
 
3
3
  interface VisitorClaims {
4
4
  /** Visitor id (random, 22 chars; derived from the principal for a signed-in user). */
@@ -290,6 +290,17 @@ interface DeweeHandlerOptions {
290
290
  allowedHeaders?: string[];
291
291
  /** HTTPS directory prefixes from which the backend may proxy images; disabled when unset. */
292
292
  imageSources?: string[];
293
+ /** Enable signed-in hosted credential requests, pinning the Dewee form's HTTPS origin. */
294
+ credentialRequests?: {
295
+ formOrigin: string;
296
+ };
297
+ /** Explicitly public directories in this Worker's Assets binding. Bypasses site middleware. */
298
+ cloudflarePublicImageAssets?: {
299
+ binding: {
300
+ fetch(request: Request): Promise<Response>;
301
+ };
302
+ prefixes: string[];
303
+ };
293
304
  /**
294
305
  * Legacy gateway transport: let visitors bring their own gateway URL + token (sent per request, never stored).
295
306
  * Meant for playgrounds. `allowedHosts` restricts which gateways may be targeted.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { d as ActionSensitivity, P as PublicConfig, S as SessionResponse, L as LinkResponse, H as HistoryMessage, e as ChatRequestBody, C as ChatStreamEvent, R as Remediation, g as PageContextAttachment } from './protocol-BbjGSfZa.js';
2
- export { a as ActionManifestEntry, b as AdmissionErrorCode, A as AgentInfo, I as IdentityErrorCode, f as LinkTarget, T as ToolCallView } from './protocol-BbjGSfZa.js';
1
+ import { d as ActionSensitivity, P as PublicConfig, S as SessionResponse, L as LinkResponse, H as HistoryMessage, g as CredentialRequestView, h as CredentialHandoff, e as ChatRequestBody, C as ChatStreamEvent, R as Remediation, j as PageContextAttachment } from './protocol-DjEdC6QU.js';
2
+ export { a as ActionManifestEntry, b as AdmissionErrorCode, A as AgentInfo, I as IdentityErrorCode, f as LinkTarget, T as ToolCallView } from './protocol-DjEdC6QU.js';
3
3
 
4
4
  type ActionResultStatus = "ok" | "denied" | "error";
5
5
  interface ActionResult {
@@ -137,6 +137,12 @@ declare class ApiClient {
137
137
  /** Starts a web-first link of a chat account for the signed-in user (identity backends only). */
138
138
  link(): Promise<LinkResponse>;
139
139
  history(agent: string, conversationId: string): Promise<HistoryMessage[]>;
140
+ historyDetails(agent: string, conversationId: string): Promise<{
141
+ messages: HistoryMessage[];
142
+ credentialRequests: CredentialRequestView[];
143
+ }>;
144
+ /** Never retry a credential action under a newly authenticated identity. */
145
+ credentialRequest(route: "credential-request" | "credential-request-handoff" | "credential-request-cancel", agent: string, conversationId: string, credentialRequestId: string, signal?: AbortSignal): Promise<CredentialRequestView | CredentialHandoff>;
140
146
  /**
141
147
  * Sends a request with the current session. A signed-in session the backend refuses (expired,
142
148
  * revoked, or another user signed in) is dropped and renewed once, as chat() does. An anonymous
@@ -260,6 +266,11 @@ declare class DeweeChatElement extends ElementBase {
260
266
  private messages;
261
267
  private running;
262
268
  private jobAccessLaunch;
269
+ private credentialCards;
270
+ private credentialGeneration;
271
+ private credentialBusy;
272
+ private credentialPopups;
273
+ private credentialRequestsInFlight;
263
274
  private activated;
264
275
  private probedPage;
265
276
  private strings;
@@ -365,6 +376,14 @@ declare class DeweeChatElement extends ElementBase {
365
376
  private switchAgent;
366
377
  private loadConversation;
367
378
  private renderLog;
379
+ private clearCredentials;
380
+ private closeCredentialPopup;
381
+ private discardCredential;
382
+ private credentialButton;
383
+ private renderCredential;
384
+ private updateCredential;
385
+ private openCredential;
386
+ private resumeCredential;
368
387
  private renderWelcome;
369
388
  private pushMessage;
370
389
  private renderMessage;
package/dist/index.js CHANGED
@@ -7,8 +7,8 @@ import {
7
7
  defineDeweeChat,
8
8
  mount,
9
9
  renderMarkdown
10
- } from "./chunk-M6J3IXEI.js";
11
- import "./chunk-WHHQ6FKK.js";
10
+ } from "./chunk-Q5RDSRZG.js";
11
+ import "./chunk-N7BJKIPT.js";
12
12
  export {
13
13
  ApiClient,
14
14
  ApiError,
@@ -13,6 +13,34 @@ interface PageContextAttachment {
13
13
  };
14
14
  }
15
15
 
16
+ /** A value-free, server-authorized credential request. Never accept form values here. */
17
+ interface CredentialRequestView {
18
+ id: string;
19
+ schema_version: "webchat-credential-request.v1";
20
+ status: "pending" | "applying" | "resolved" | "cancelled" | "expired" | "failed";
21
+ title: string;
22
+ purpose: string;
23
+ target: {
24
+ type: string;
25
+ mode: string;
26
+ };
27
+ fields: Array<{
28
+ key: string;
29
+ label: string;
30
+ kind: string;
31
+ required: boolean;
32
+ }>;
33
+ expires_at: string;
34
+ agent_key: string;
35
+ completion: "hosted";
36
+ resumable: boolean;
37
+ }
38
+ interface CredentialHandoff {
39
+ credential_request_id: string;
40
+ url: string;
41
+ expires_at: string;
42
+ }
43
+
16
44
  /** Server-Sent Events emitted by `POST {basePath}/chat`. */
17
45
  type ChatStreamEvent = {
18
46
  type: "start";
@@ -37,6 +65,9 @@ type ChatStreamEvent = {
37
65
  name: string;
38
66
  isError: boolean;
39
67
  result?: string;
68
+ } | {
69
+ type: "credential_request";
70
+ credentialRequest: CredentialRequestView;
40
71
  } | {
41
72
  type: "done";
42
73
  content: string;
@@ -95,6 +126,8 @@ interface PublicConfig {
95
126
  pageContextVersions?: number[];
96
127
  /** Present (true) when the backend signs users in through Dewee: the widget then asks `/session` on every page load. */
97
128
  identity?: boolean;
129
+ /** Trusted origin of the hosted credential form, present only when explicitly enabled. */
130
+ credentialFormOrigin?: string;
98
131
  /**
99
132
  * Present (true) when a returning visitor's `/session` refresh (a valid anonymous `token` in the
100
133
  * body) needs no Turnstile token (ccp transport); only new visitors are challenged then. With
@@ -184,6 +217,7 @@ interface ChatRequestBody {
184
217
  pageContext?: unknown;
185
218
  /** Ephemeral, untrusted content for this turn; distinct from the legacy page-action context. */
186
219
  page_context?: PageContextAttachment;
220
+ credentialRequestId?: string;
187
221
  }
188
222
 
189
- export { type AgentInfo as A, type ChatStreamEvent as C, type ErrorBody as E, type HistoryMessage as H, type IdentityErrorCode as I, type LinkResponse as L, type PublicConfig as P, type Remediation as R, type SessionResponse as S, type ToolCallView as T, type ActionManifestEntry as a, type AdmissionErrorCode as b, ADMISSION_ERROR_CODES as c, type ActionSensitivity as d, type ChatRequestBody as e, type LinkTarget as f, type PageContextAttachment as g, isAdmissionErrorCode as i };
223
+ export { type AgentInfo as A, type ChatStreamEvent as C, type ErrorBody as E, type HistoryMessage as H, type IdentityErrorCode as I, type LinkResponse as L, type PublicConfig as P, type Remediation as R, type SessionResponse as S, type ToolCallView as T, type ActionManifestEntry as a, type AdmissionErrorCode as b, ADMISSION_ERROR_CODES as c, type ActionSensitivity as d, type ChatRequestBody as e, type LinkTarget as f, type CredentialRequestView as g, type CredentialHandoff as h, isAdmissionErrorCode as i, type PageContextAttachment as j };
package/dist/react.d.ts CHANGED
@@ -2,7 +2,7 @@ import * as react from 'react';
2
2
  import { CSSProperties } from 'react';
3
3
  import { DeweeChatConfig, ActionEventDetail, DeweeChatElement } from './index.js';
4
4
  export { ActionConfirmHook, DomActionsRoot, PageAction, PageActionContext, RequestHeadersOption } from './index.js';
5
- import './protocol-BbjGSfZa.js';
5
+ import './protocol-DjEdC6QU.js';
6
6
 
7
7
  interface DeweeChatProps extends Partial<DeweeChatConfig> {
8
8
  className?: string;
package/dist/react.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  defineDeweeChat
3
- } from "./chunk-M6J3IXEI.js";
4
- import "./chunk-WHHQ6FKK.js";
3
+ } from "./chunk-Q5RDSRZG.js";
4
+ import "./chunk-N7BJKIPT.js";
5
5
 
6
6
  // src/react.ts
7
7
  import { createElement, forwardRef, useEffect, useImperativeHandle, useRef } from "react";
@@ -1,5 +1,5 @@
1
- import { D as DeweeIdentitySdk } from './handler-B-c6tDr7.js';
2
- import './protocol-BbjGSfZa.js';
1
+ import { D as DeweeIdentitySdk } from './handler-BchS1kD4.js';
2
+ import './protocol-DjEdC6QU.js';
3
3
 
4
4
  declare const deweeIdentitySdk: DeweeIdentitySdk;
5
5
 
package/dist/server.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- export { A as AuthenticatedUser, C as CcpError, a as CcpOptions, b as DeweeHandler, c as DeweeHandlerOptions, d as DeweeIdentityOptions, D as DeweeIdentitySdk, e as DeweeTransport, I as IdentityPrivateJwk, f as IdentitySessionStore, g as IdentitySigner, M as MemoryRateLimits, R as RateLimitBucket, h as RateLimiter, S as StoredIdentitySession, V as VisitorClaims, i as createDeweeHandler, m as memoryIdentitySessionStore, j as memoryRateLimiter, o as optionsFromEnv, s as signVisitorToken, v as verifyVisitorToken } from './handler-B-c6tDr7.js';
1
+ export { A as AuthenticatedUser, C as CcpError, a as CcpOptions, b as DeweeHandler, c as DeweeHandlerOptions, d as DeweeIdentityOptions, D as DeweeIdentitySdk, e as DeweeTransport, I as IdentityPrivateJwk, f as IdentitySessionStore, g as IdentitySigner, M as MemoryRateLimits, R as RateLimitBucket, h as RateLimiter, S as StoredIdentitySession, V as VisitorClaims, i as createDeweeHandler, m as memoryIdentitySessionStore, j as memoryRateLimiter, o as optionsFromEnv, s as signVisitorToken, v as verifyVisitorToken } from './handler-BchS1kD4.js';
2
2
  import { IncomingMessage, ServerResponse } from 'node:http';
3
- import { R as Remediation, H as HistoryMessage, A as AgentInfo, C as ChatStreamEvent, a as ActionManifestEntry, b as AdmissionErrorCode } from './protocol-BbjGSfZa.js';
4
- export { c as ADMISSION_ERROR_CODES, d as ActionSensitivity, e as ChatRequestBody, E as ErrorBody, I as IdentityErrorCode, L as LinkResponse, f as LinkTarget, P as PublicConfig, S as SessionResponse, i as isAdmissionErrorCode } from './protocol-BbjGSfZa.js';
3
+ import { R as Remediation, H as HistoryMessage, A as AgentInfo, C as ChatStreamEvent, a as ActionManifestEntry, b as AdmissionErrorCode } from './protocol-DjEdC6QU.js';
4
+ export { c as ADMISSION_ERROR_CODES, d as ActionSensitivity, e as ChatRequestBody, E as ErrorBody, I as IdentityErrorCode, L as LinkResponse, f as LinkTarget, P as PublicConfig, S as SessionResponse, i as isAdmissionErrorCode } from './protocol-DjEdC6QU.js';
5
5
 
6
6
  type FetchHandler = (request: Request) => Promise<Response>;
7
7
  /** Wraps a fetch-style handler for `http.createServer`, Express (`app.all("/api/dewee/*", ...)`), etc. */
package/dist/server.js CHANGED
@@ -10,10 +10,12 @@ import {
10
10
  isAdmissionErrorCode,
11
11
  isIdentityErrorCode,
12
12
  normalizeManifest,
13
+ parseCredentialHandoff,
14
+ parseCredentialRequest,
13
15
  parseJobAccessLaunch,
14
16
  serializePageContext,
15
17
  stripPageBlock
16
- } from "./chunk-WHHQ6FKK.js";
18
+ } from "./chunk-N7BJKIPT.js";
17
19
 
18
20
  // src/server/admission-errors.ts
19
21
  var MESSAGES = {
@@ -192,6 +194,9 @@ var GatewayConnection = class _GatewayConnection {
192
194
  function buildSessionKey(agent, conversationId) {
193
195
  return `agent:${agent}:ws:direct:${conversationId}`;
194
196
  }
197
+ function credentialTool(name) {
198
+ return typeof name === "string" && /(?:^|__)request_credentials$/i.test(name);
199
+ }
195
200
  function actorOf(opts) {
196
201
  if (opts.identitySession) return { identitySession: opts.identitySession };
197
202
  if (opts.userId) return opts.userId;
@@ -202,6 +207,8 @@ async function* streamChat(opts) {
202
207
  const queue = [];
203
208
  let wake = null;
204
209
  let finished = false;
210
+ const toolNames = /* @__PURE__ */ new Map();
211
+ const sensitiveToolIds = /* @__PURE__ */ new Set();
205
212
  const push = (e) => {
206
213
  queue.push(e);
207
214
  wake?.();
@@ -225,12 +232,21 @@ async function* streamChat(opts) {
225
232
  case "chunk":
226
233
  if (p.content) push({ type: "delta", text: p.content });
227
234
  break;
228
- case "tool.call":
229
- push({ type: "tool_call", id: p.id || "", name: p.name || "tool", arguments: p.arguments });
235
+ case "tool.call": {
236
+ const id = typeof p.id === "string" ? p.id : "";
237
+ const name = typeof p.name === "string" ? p.name : "";
238
+ if (id && credentialTool(name)) sensitiveToolIds.add(id);
239
+ if (id && name) toolNames.set(id, name);
240
+ if (name && !sensitiveToolIds.has(id) && !credentialTool(name)) push({ type: "tool_call", id, name, arguments: p.arguments });
230
241
  break;
231
- case "tool.result":
232
- push({ type: "tool_result", id: p.id || "", name: p.name || "tool", isError: !!p.is_error, result: clip(p.result ?? p.content) });
242
+ }
243
+ case "tool.result": {
244
+ const id = typeof p.id === "string" ? p.id : "";
245
+ const known = toolNames.get(id);
246
+ const name = typeof p.name === "string" && p.name ? p.name : known;
247
+ if (id && known && name === known && !sensitiveToolIds.has(id) && !credentialTool(name)) push({ type: "tool_result", id, name, isError: !!p.is_error, result: clip(p.result ?? p.content) });
233
248
  break;
249
+ }
234
250
  case "run.failed":
235
251
  push({ type: "error", message: p.error || p.message || "Agent run failed", code: "run_failed" });
236
252
  end();
@@ -288,6 +304,10 @@ async function fetchHistory(creds, actor, agent, sessionKey) {
288
304
  }
289
305
  function foldHistory(raw) {
290
306
  const out = [];
307
+ const sensitiveIds = /* @__PURE__ */ new Set();
308
+ for (const m of raw) for (const tc of Array.isArray(m?.tool_calls) ? m.tool_calls : []) {
309
+ if (credentialTool(tc?.name ?? tc?.function?.name) && typeof tc?.id === "string") sensitiveIds.add(tc.id);
310
+ }
291
311
  let current = null;
292
312
  for (const m of raw) {
293
313
  if (m.role === "user") {
@@ -303,12 +323,14 @@ function foldHistory(raw) {
303
323
 
304
324
  ${m.content}` : String(m.content);
305
325
  for (const tc of m.tool_calls ?? []) {
306
- const view = { id: tc.id, name: tc.name ?? tc.function?.name ?? "tool", arguments: tc.arguments ?? tc.function?.arguments, status: "done" };
326
+ const name = tc.name ?? tc.function?.name;
327
+ if (typeof tc.id !== "string" || !tc.id || !name || sensitiveIds.has(tc.id) || credentialTool(name)) continue;
328
+ const view = { id: tc.id, name, arguments: tc.arguments ?? tc.function?.arguments, status: "done" };
307
329
  (current.toolCalls ??= []).push(view);
308
330
  }
309
331
  } else if (m.role === "tool" && current?.toolCalls) {
310
332
  const tc = current.toolCalls.find((t) => t.id === m.tool_call_id);
311
- if (tc) {
333
+ if (typeof m.tool_call_id === "string" && m.tool_call_id && tc && !sensitiveIds.has(m.tool_call_id)) {
312
334
  tc.result = clip(m.content);
313
335
  if (m.is_error) {
314
336
  tc.isError = true;
@@ -642,6 +664,7 @@ var CcpClient = class {
642
664
  if (req.visitorIp && VISITOR_IP_RE.test(req.visitorIp)) headers["x-dewee-visitor-ip"] = req.visitorIp;
643
665
  if (req.requestId) headers["x-request-id"] = req.requestId;
644
666
  if (req.idempotencyKey) headers["idempotency-key"] = req.idempotencyKey;
667
+ if (req.credentialRequests) headers["x-dewee-webchat-features"] = "credential-requests";
645
668
  let res;
646
669
  try {
647
670
  res = await this.fetchImpl(url, {
@@ -893,6 +916,13 @@ function browserErrorFor(e, route) {
893
916
  if (e.code === "rate_limited" || e.code === "concurrency_limited" || e.status === 429) return { status: 429, code: "RATE_LIMITED", message: "", admission: "RATE_LIMITED", retryAfterMs };
894
917
  }
895
918
  switch (e.code) {
919
+ case "credential_requests_unavailable":
920
+ return { status: 403, code: e.code, message: "Credential requests are unavailable" };
921
+ case "credential_request_not_found":
922
+ return { status: 404, code: e.code, message: "Credential request not found" };
923
+ case "credential_request_not_ready":
924
+ case "credential_request_resumed":
925
+ return { status: 409, code: e.code, message: "Credential request cannot be resumed" };
896
926
  case "page_context_invalid":
897
927
  return { status: 400, code: e.code, message: "Invalid page context" };
898
928
  case "page_context_unsupported":
@@ -989,7 +1019,7 @@ function numericUsage(raw) {
989
1019
  for (const [k, v] of Object.entries(raw).slice(0, 32)) if (typeof v === "number" && Number.isFinite(v) && /^[a-z0-9_]{1,64}$/i.test(k)) out[k] = v;
990
1020
  return Object.keys(out).length ? out : void 0;
991
1021
  }
992
- function createCcpTransport(client, identity) {
1022
+ function createCcpTransport(client, identity, credentialRequests = false) {
993
1023
  let config;
994
1024
  let pendingConfig;
995
1025
  const edges = /* @__PURE__ */ new Map();
@@ -1066,16 +1096,17 @@ function createCcpTransport(client, identity) {
1066
1096
  conversation_id: input.conversationId,
1067
1097
  ...withAgent(input.agent),
1068
1098
  ...input.identitySession ? { identity_session: input.identitySession } : {},
1069
- ...input.pageContext !== void 0 ? { page_context: input.pageContext } : {}
1099
+ ...input.pageContext !== void 0 ? { page_context: input.pageContext } : {},
1100
+ ...input.credentialRequestId ? { credential_request_id: input.credentialRequestId } : {}
1070
1101
  };
1071
- return withEdge(ctx, visitorId, sealed, (edge) => client.send("chat", { ...base(ctx), body, session: edge.token, accept: "text/event-stream", signal: input.signal }));
1102
+ return withEdge(ctx, visitorId, sealed, (edge) => client.send("chat", { ...base(ctx), body, session: edge.token, accept: "text/event-stream", signal: input.signal, credentialRequests: credentialRequests && !!input.identitySession }));
1072
1103
  },
1073
1104
  /**
1074
1105
  * Browser events of an accepted turn: exactly one `start` first and exactly one terminal event
1075
1106
  * (`done` or `error`) last. Reasoning and tool frames pass only when the integration shows them,
1076
1107
  * tool arguments and results never; unknown frames are dropped.
1077
1108
  */
1078
- async *chatEvents(res, cfg, conversationId, requestedAgent) {
1109
+ async *chatEvents(res, cfg, conversationId, requestedAgent, verified = false) {
1079
1110
  let started = false;
1080
1111
  const start = (f) => {
1081
1112
  started = true;
@@ -1108,6 +1139,11 @@ function createCcpTransport(client, identity) {
1108
1139
  case "tool_result":
1109
1140
  if (cfg.showToolActivity && typeof f.id === "string" && typeof f.name === "string") yield { type: "tool_result", id: f.id.slice(0, 128), name: f.name.slice(0, 128), isError: f.isError === true };
1110
1141
  break;
1142
+ case "credential_request": {
1143
+ const view = credentialRequests && verified ? parseCredentialRequest(f.credential_request, requestedAgent) : null;
1144
+ if (view) yield { type: "credential_request", credentialRequest: view };
1145
+ break;
1146
+ }
1111
1147
  case "done": {
1112
1148
  const usage = numericUsage(f.usage);
1113
1149
  yield { type: "done", content: typeof f.content === "string" ? f.content : "", ...usage ? { usage } : {} };
@@ -1145,9 +1181,9 @@ function createCcpTransport(client, identity) {
1145
1181
  };
1146
1182
  let data;
1147
1183
  try {
1148
- data = await withEdge(ctx, visitorId, sealed, (edge) => client.json("history", { ...base(ctx), body, session: edge.token }));
1184
+ data = await withEdge(ctx, visitorId, sealed, (edge) => client.json("history", { ...base(ctx), body, session: edge.token, credentialRequests: credentialRequests && !!input.identitySession }));
1149
1185
  } catch (e) {
1150
- if (e instanceof CcpError && e.code === "not_found") return [];
1186
+ if (e instanceof CcpError && e.code === "not_found") return { messages: [] };
1151
1187
  throw e;
1152
1188
  }
1153
1189
  const out = [];
@@ -1169,7 +1205,16 @@ function createCcpTransport(client, identity) {
1169
1205
  if (!msg.content && !msg.thinking && !msg.toolCalls) continue;
1170
1206
  out.push(msg);
1171
1207
  }
1172
- return out;
1208
+ const cards = credentialRequests && input.identitySession && Array.isArray(data.credential_requests) ? data.credential_requests.slice(0, 100).map((view) => parseCredentialRequest(view, input.agent || cfg.defaultAgent)).filter((view) => view !== null) : [];
1209
+ return { messages: out, ...cards.length ? { credentialRequests: cards } : {} };
1210
+ },
1211
+ async credentialRequest(ctx, visitorId, sealed, route, input) {
1212
+ return withEdge(ctx, visitorId, sealed, (edge) => client.json(route, {
1213
+ ...base(ctx),
1214
+ session: edge.token,
1215
+ credentialRequests: true,
1216
+ body: { conversation_id: input.conversationId, credential_request_id: input.requestId, identity_session: input.identitySession, agent: input.agent }
1217
+ }));
1173
1218
  },
1174
1219
  /**
1175
1220
  * Signs a partner assertion for `subject` and exchanges it at CCP for an identity session. `sealed`
@@ -1325,7 +1370,7 @@ async function verifyImageTicket(secret, namespace, ticket) {
1325
1370
  return null;
1326
1371
  }
1327
1372
  }
1328
- function allowedImageSource(raw, prefixes, endpoint, cloudflare) {
1373
+ function allowedImageSource(raw, prefixes, endpoint, cloudflare, publicAssets = []) {
1329
1374
  try {
1330
1375
  if (!raw || raw.length > 4096 || /[\u0000-\u0020\u007f\\]/.test(raw)) return null;
1331
1376
  const match = /^https:\/\/[^/?#]+([^?#]*)/i.exec(raw);
@@ -1337,7 +1382,9 @@ function allowedImageSource(raw, prefixes, endpoint, cloudflare) {
1337
1382
  if (!parent && /^\/(?:image|image-ticket)(?:\/|$)/.test(source.pathname)) return null;
1338
1383
  if (cloudflare) {
1339
1384
  const suffix = endpoint.hostname.split(".").slice(-2).join(".");
1340
- if (source.hostname === suffix || source.hostname.endsWith(`.${suffix}`)) return null;
1385
+ const sameZone = source.hostname === suffix || source.hostname.endsWith(`.${suffix}`);
1386
+ const isPublicAsset = source.origin === endpoint.origin && publicAssets.some((prefix) => source.pathname.startsWith(prefix));
1387
+ if (sameZone && !isPublicAsset) return null;
1341
1388
  }
1342
1389
  return prefixes.some((prefix) => prefix.origin === source.origin && source.pathname.startsWith(prefix.pathname)) ? source : null;
1343
1390
  } catch {
@@ -1664,6 +1711,18 @@ function createDeweeHandler(options) {
1664
1711
  ...options
1665
1712
  };
1666
1713
  const transport = resolveTransport(o);
1714
+ let credentialFormOrigin;
1715
+ if (o.credentialRequests) {
1716
+ if (transport !== "ccp" || !o.identity) throw new Error("[dewee-webchat] credentialRequests requires CCP transport and identity");
1717
+ try {
1718
+ if (typeof o.credentialRequests.formOrigin !== "string" || /[^\x21-\x7e]|[\\%]/.test(o.credentialRequests.formOrigin)) throw new Error();
1719
+ const u = new URL(o.credentialRequests.formOrigin);
1720
+ if (u.protocol !== "https:" || u.username || u.password || u.pathname !== "/" || u.search || u.hash) throw new Error();
1721
+ credentialFormOrigin = u.origin;
1722
+ } catch {
1723
+ throw new Error("[dewee-webchat] credentialRequests.formOrigin must be an ASCII HTTPS origin (use punycode for internationalized domains)");
1724
+ }
1725
+ }
1667
1726
  if (o.imageSources !== void 0 && !Array.isArray(o.imageSources)) throw new Error("[dewee-webchat] imageSources must be an array");
1668
1727
  const imageSources = (o.imageSources ?? []).map((origin) => {
1669
1728
  let url;
@@ -1675,6 +1734,11 @@ function createDeweeHandler(options) {
1675
1734
  if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash || url.port || !url.pathname.endsWith("/") || /%(?:2f|5c|25|2e|0[0-9a-f]|1[0-9a-f]|7f)/i.test(url.pathname)) throw new Error("[dewee-webchat] Image sources must be HTTPS directory prefixes on the default port");
1676
1735
  return url;
1677
1736
  });
1737
+ const publicAssets = o.cloudflarePublicImageAssets;
1738
+ if (publicAssets && (typeof publicAssets.binding?.fetch !== "function" || !Array.isArray(publicAssets.prefixes) || !publicAssets.prefixes.length || publicAssets.prefixes.some((path) => typeof path !== "string" || !/^\/(?:[A-Za-z0-9_-]+\/)+$/.test(path)))) {
1739
+ throw new Error("[dewee-webchat] Public image asset prefixes must be explicit absolute directory paths");
1740
+ }
1741
+ const assetPrefixes = publicAssets?.prefixes ?? [];
1678
1742
  let activeImages = 0;
1679
1743
  const imageNamespace = `${o.ccp?.chatUrl ?? o.gatewayUrl ?? ""}
1680
1744
  ${o.ccp?.integrationId ?? o.tenantId ?? ""}`;
@@ -1696,7 +1760,8 @@ ${o.ccp?.integrationId ?? o.tenantId ?? ""}`;
1696
1760
  const identityAgent = idn ? checkIdentityOptions(o, staticAgents, transport) : "";
1697
1761
  const ccp = transport === "ccp" ? createCcpTransport(
1698
1762
  new CcpClient(parseCcpOptions(o.ccp)),
1699
- idn ? { createAssertion: (a) => idn.sdk.createAssertion(a), signer: () => identitySigner(), ttlSeconds: idn.sessionTtlSeconds } : void 0
1763
+ idn ? { createAssertion: (a) => idn.sdk.createAssertion(a), signer: () => identitySigner(), ttlSeconds: idn.sessionTtlSeconds } : void 0,
1764
+ !!credentialFormOrigin
1700
1765
  ) : null;
1701
1766
  function visitorContext(req) {
1702
1767
  const ip = clientIp(req);
@@ -2072,6 +2137,7 @@ ${o.ccp?.integrationId ?? o.tenantId ?? ""}`;
2072
2137
  ...o.turnstile ? { refreshWithoutTurnstile: true } : {},
2073
2138
  maxMessageLength: Math.min(o.maxMessageLength, cfg.maxMessageChars),
2074
2139
  ...cfg.pageContextVersions.length ? { pageContextVersions: cfg.pageContextVersions } : {},
2140
+ ...credentialFormOrigin ? { credentialFormOrigin } : {},
2075
2141
  version: VERSION
2076
2142
  };
2077
2143
  if (idn) {
@@ -2173,7 +2239,19 @@ ${o.ccp?.integrationId ?? o.tenantId ?? ""}`;
2173
2239
  if (!input.body || typeof input.body !== "object" || Array.isArray(input.body)) return fail(req, 400, "Invalid request", "bad_request");
2174
2240
  const body = input.body;
2175
2241
  const message = typeof body?.message === "string" ? body.message.trim() : "";
2176
- if (!message) return fail(req, 400, "Message is required");
2242
+ const resumeId = body?.credentialRequestId;
2243
+ if (resumeId !== void 0) {
2244
+ if (!ccp || !credentialFormOrigin || !claims.ids || typeof resumeId !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(resumeId) || body.message !== void 0 && typeof body.message !== "string" || message || body?.actions != null || body?.pageContext != null || body?.page_context != null || typeof body?.conversationId !== "string" || !CCP_ID_RE.test(body.conversationId)) return fail(req, 400, "Invalid request", "bad_request");
2245
+ try {
2246
+ const origin = req.headers.get("origin");
2247
+ const u = new URL(origin ?? "");
2248
+ if (u.protocol !== "https:" || u.origin !== origin) throw new Error();
2249
+ } catch {
2250
+ return fail(req, 403, "Origin not allowed", "forbidden_origin");
2251
+ }
2252
+ const rebound = await rebindRefusal(req, claims, true);
2253
+ if (rebound) return rebound;
2254
+ } else if (!message) return fail(req, 400, "Message is required");
2177
2255
  if (message.length > o.maxMessageLength) return fail(req, 413, `Message exceeds ${o.maxMessageLength} characters`, "too_long");
2178
2256
  const idRe = ccp ? CCP_ID_RE : ID_RE;
2179
2257
  const conversationId = typeof body?.conversationId === "string" && idRe.test(body.conversationId) ? body.conversationId : randomId(9);
@@ -2195,7 +2273,7 @@ ${o.ccp?.integrationId ?? o.tenantId ?? ""}`;
2195
2273
  if (typeof body.page_context === "object" && Number.isInteger(body.page_context.version) && body.page_context.version !== 1) return fail(req, 400, "Page context is not supported", "page_context_unsupported");
2196
2274
  if (!validPageContext(body.page_context)) return fail(req, 400, "Invalid page context", "page_context_invalid");
2197
2275
  }
2198
- if (ccp) return ccpChat(req, claims, { message: pageBlock + message, rawMessage: message, conversationId, agent, actionsHash, pageContext: body?.page_context });
2276
+ if (ccp) return ccpChat(req, claims, { message: pageBlock + message, rawMessage: message, conversationId, agent, actionsHash, pageContext: body?.page_context, credentialRequestId: resumeId });
2199
2277
  if (!creds) return fail(req, 500, "Gateway is not configured", "not_configured");
2200
2278
  let connection;
2201
2279
  if (claims.ids || body?.page_context != null) {
@@ -2310,6 +2388,7 @@ ${o.ccp?.integrationId ?? o.tenantId ?? ""}`;
2310
2388
  agent: turn.agent || void 0,
2311
2389
  identitySession: claims.ids,
2312
2390
  pageContext: turn.pageContext ?? void 0,
2391
+ credentialRequestId: turn.credentialRequestId,
2313
2392
  signal: abort.signal
2314
2393
  });
2315
2394
  } catch (e) {
@@ -2342,7 +2421,7 @@ ${o.ccp?.integrationId ?? o.tenantId ?? ""}`;
2342
2421
  let errMsg;
2343
2422
  let agent = turn.agent || cfg.defaultAgent;
2344
2423
  try {
2345
- for await (const ev of ccp.chatEvents(upstream, cfg, turn.conversationId, agent)) {
2424
+ for await (const ev of ccp.chatEvents(upstream, cfg, turn.conversationId, agent, !!claims.ids)) {
2346
2425
  if (ev.type === "start") {
2347
2426
  runId = ev.runId;
2348
2427
  agent = ev.agent;
@@ -2387,7 +2466,7 @@ ${o.ccp?.integrationId ?? o.tenantId ?? ""}`;
2387
2466
  return new Response(readable, {
2388
2467
  headers: {
2389
2468
  "content-type": "text/event-stream; charset=utf-8",
2390
- "cache-control": "no-cache, no-transform",
2469
+ "cache-control": "no-store, no-transform",
2391
2470
  "x-accel-buffering": "no",
2392
2471
  ...corsHeaders(req)
2393
2472
  }
@@ -2401,8 +2480,8 @@ ${o.ccp?.integrationId ?? o.tenantId ?? ""}`;
2401
2480
  if (agent instanceof Response) return agent;
2402
2481
  if (!CCP_ID_RE.test(conversationId)) return json(req, { messages: [] });
2403
2482
  try {
2404
- const messages = await ccp.history(visitorContext(req), claims.v, sealedEdge(claims), { conversationId, agent: agent || void 0, identitySession: claims.ids });
2405
- return json(req, { messages });
2483
+ const history = await ccp.history(visitorContext(req), claims.v, sealedEdge(claims), { conversationId, agent: agent || void 0, identitySession: claims.ids });
2484
+ return json(req, history);
2406
2485
  } catch (e) {
2407
2486
  return ccpFail(req, e, "history", claims.ids);
2408
2487
  }
@@ -2463,6 +2542,43 @@ ${o.ccp?.integrationId ?? o.tenantId ?? ""}`;
2463
2542
  return fail(req, 502, "Job access is unavailable. Try again later.", "job_access_unavailable");
2464
2543
  }
2465
2544
  }
2545
+ async function handleCredentialRequest(req, route) {
2546
+ if (!ccp || !credentialFormOrigin || !idn) return fail(req, 404, "Not found", "not_found");
2547
+ const origin = req.headers.get("origin");
2548
+ try {
2549
+ const u = new URL(origin ?? "");
2550
+ if (u.protocol !== "https:" || u.origin !== origin) throw new Error();
2551
+ } catch {
2552
+ return fail(req, 403, "Origin not allowed", "forbidden_origin");
2553
+ }
2554
+ const claims = await visitorFrom(req);
2555
+ if (!claims?.ids || !claims.ida) return identityFail(req, "IDENTITY_REQUIRED");
2556
+ const refused = await rebindRefusal(req, claims, true);
2557
+ if (refused) return refused;
2558
+ if (limiter && !await limiter(`credential:${claims.v}`, route === "credential-request-handoff" ? "link" : "preauth")) return fail(req, 429, "Too many requests", "rate_limited");
2559
+ const raw = await readImageTicketBody(req);
2560
+ let body;
2561
+ try {
2562
+ body = raw && JSON.parse(raw);
2563
+ } catch {
2564
+ return fail(req, 400, "Invalid request", "bad_request");
2565
+ }
2566
+ if (!body || typeof body !== "object" || Array.isArray(body) || Object.keys(body).some((key) => !["conversationId", "credentialRequestId", "agent"].includes(key)) || typeof body.conversationId !== "string" || !CCP_ID_RE.test(body.conversationId) || typeof body.credentialRequestId !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(body.credentialRequestId)) return fail(req, 400, "Invalid request", "bad_request");
2567
+ const agent = ccpAgentFor(req, claims, body.agent);
2568
+ if (agent instanceof Response) return agent;
2569
+ if (!agent) return fail(req, 403, "Agent not allowed", "agent_not_allowed");
2570
+ try {
2571
+ const result = await ccp.credentialRequest(visitorContext(req), claims.v, sealedEdge(claims), route, { conversationId: body.conversationId, requestId: body.credentialRequestId, agent, identitySession: claims.ids });
2572
+ if (route === "credential-request-handoff") {
2573
+ const handoff = parseCredentialHandoff(result, body.credentialRequestId, credentialFormOrigin);
2574
+ return handoff ? json(req, handoff) : fail(req, 502, "Credential request is unavailable", "credential_request_unavailable");
2575
+ }
2576
+ const view = parseCredentialRequest(result?.credential_request, agent);
2577
+ return view && view.id === body.credentialRequestId ? json(req, { credentialRequest: view }) : fail(req, 502, "Credential request is unavailable", "credential_request_unavailable");
2578
+ } catch (e) {
2579
+ return ccpFail(req, e, route, claims.ids);
2580
+ }
2581
+ }
2466
2582
  async function handleLink(req) {
2467
2583
  const claims = await visitorFrom(req);
2468
2584
  if (!claims?.ids) return identityFail(req, "IDENTITY_REQUIRED");
@@ -2545,22 +2661,32 @@ ${o.ccp?.integrationId ?? o.tenantId ?? ""}`;
2545
2661
  }
2546
2662
  const raw = parsed?.url;
2547
2663
  const worker = globalThis.navigator?.userAgent === "Cloudflare-Workers";
2548
- const source = typeof raw === "string" ? allowedImageSource(raw, imageSources, new URL(req.url), worker) : null;
2664
+ const source = typeof raw === "string" ? allowedImageSource(raw, imageSources, new URL(req.url), worker, worker ? assetPrefixes : []) : null;
2549
2665
  if (!source) return fail(req, 403, "Image not allowed", "forbidden_image");
2550
- return json(req, { ticket: await issueImageTicket(secret, imageNamespace, claims.v, source.href) });
2666
+ const endpoint = new URL(req.url);
2667
+ const asset = publicAssets && source.origin === endpoint.origin && assetPrefixes.some((prefix) => source.pathname.startsWith(prefix));
2668
+ const namespace = asset ? `${imageNamespace}
2669
+ assets:${endpoint.origin}` : imageNamespace;
2670
+ return json(req, { ticket: await issueImageTicket(secret, namespace, claims.v, source.href) });
2551
2671
  }
2552
2672
  if (req.method === "GET" && action === "image" && imageSources.length) {
2553
2673
  const ticket = new URL(req.url).searchParams.get("ticket") ?? "";
2554
- const claims = await verifyImageTicket(secret, imageNamespace, ticket);
2674
+ const endpoint = new URL(req.url);
2675
+ const assetClaims = await verifyImageTicket(secret, `${imageNamespace}
2676
+ assets:${endpoint.origin}`, ticket);
2677
+ const claims = assetClaims ?? await verifyImageTicket(secret, imageNamespace, ticket);
2555
2678
  if (!claims) return fail(req, 401, "Image ticket expired", "unauthorized");
2556
2679
  const worker = globalThis.navigator?.userAgent === "Cloudflare-Workers";
2557
- const source = allowedImageSource(claims.source, imageSources, new URL(req.url), worker);
2680
+ const source = allowedImageSource(claims.source, imageSources, endpoint, worker, worker ? assetPrefixes : []);
2558
2681
  if (!source) return fail(req, 403, "Image not allowed", "forbidden_image");
2682
+ const asset = publicAssets && source.origin === endpoint.origin && assetPrefixes.some((prefix) => source.pathname.startsWith(prefix));
2683
+ if (Boolean(asset) !== Boolean(assetClaims)) return fail(req, 403, "Image not allowed", "forbidden_image");
2559
2684
  if (limiter && !await limiter(`image:${claims.visitor}`, "preauth")) return fail(req, 429, "Too many requests, try again later", "rate_limited");
2560
2685
  if (activeImages >= 8) return fail(req, 429, "Too many requests, try again later", "rate_limited");
2561
2686
  activeImages++;
2562
2687
  try {
2563
- const result = await fetchRasterImage(source.href, worker ? globalThis.fetch : publicImageFetchNode, worker ? "manual" : "error");
2688
+ const assetFetch = (input, init) => publicAssets.binding.fetch(new Request(new URL(new URL(String(input)).pathname, "https://assets.invalid"), init));
2689
+ const result = await fetchRasterImage(source.href, asset ? assetFetch : worker ? globalThis.fetch : publicImageFetchNode, worker ? "manual" : "error");
2564
2690
  return result ?? fail(req, 502, "Image unavailable", "image_unavailable");
2565
2691
  } finally {
2566
2692
  activeImages--;
@@ -2572,6 +2698,7 @@ ${o.ccp?.integrationId ?? o.tenantId ?? ""}`;
2572
2698
  if (req.method === "GET" && action === "history") return await handleHistory(req);
2573
2699
  if (req.method === "POST" && action === "link" && idn) return await handleLink(req);
2574
2700
  if (req.method === "POST" && action === "job-access" && idn) return await handleJobAccess(req);
2701
+ if (req.method === "POST" && (action === "credential-request" || action === "credential-request-handoff" || action === "credential-request-cancel")) return await handleCredentialRequest(req, action);
2575
2702
  return fail(req, 404, "Not found", "not_found");
2576
2703
  } catch (e) {
2577
2704
  if (e instanceof IdentityStoreError) return identityFail(req, "SOURCE_UNAVAILABLE", { action: "retry_later", audience: "end_user" });
package/dist/vue.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as vue from 'vue';
2
2
  import { PropType } from 'vue';
3
3
  export { ActionConfirmHook, ActionEventDetail, DeweeChatConfig, DeweeChatElement, DomActionsRoot, PageAction, PageActionContext, RequestHeadersOption } from './index.js';
4
- import './protocol-BbjGSfZa.js';
4
+ import './protocol-DjEdC6QU.js';
5
5
 
6
6
  declare const DeweeChat: vue.DefineComponent<vue.ExtractPropTypes<{
7
7
  endpoint: {
package/dist/vue.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  defineDeweeChat
3
- } from "./chunk-M6J3IXEI.js";
4
- import "./chunk-WHHQ6FKK.js";
3
+ } from "./chunk-Q5RDSRZG.js";
4
+ import "./chunk-N7BJKIPT.js";
5
5
 
6
6
  // src/vue.ts
7
7
  import { defineComponent, h, onMounted, ref, watch } from "vue";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dewee-webchat",
3
- "version": "0.5.0-beta.1",
3
+ "version": "0.5.0-beta.3",
4
4
  "description": "Embeddable chat widget for Dewee AI agents — web component, React/Vue/Astro wrappers and a secure backend handler (Workers, Next.js, Node, Bun, Deno).",
5
5
  "license": "MIT",
6
6
  "type": "module",