dewee-webchat 0.5.0-beta.2 → 0.5.0-beta.4
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/README.md +34 -1
- package/dist/{chunk-KXW4EWXA.js → chunk-4EASDTWO.js} +46 -2
- package/dist/{chunk-XCO4TCKF.js → chunk-OMRLBPEM.js} +261 -8
- package/dist/dewee-webchat.js +33 -28
- package/dist/{handler-SsDyK0TG.d.ts → handler-_GEhpm1W.d.ts} +13 -1
- package/dist/index.d.ts +21 -2
- package/dist/index.js +2 -2
- package/dist/{protocol-BbjGSfZa.d.ts → protocol-DjEdC6QU.d.ts} +35 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +2 -2
- package/dist/server-identity.d.ts +2 -2
- package/dist/server.d.ts +3 -3
- package/dist/server.js +181 -30
- package/dist/vue.d.ts +52 -52
- package/dist/vue.js +2 -2
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as AgentInfo } from './protocol-
|
|
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,10 @@ 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
|
+
};
|
|
293
297
|
/** Explicitly public directories in this Worker's Assets binding. Bypasses site middleware. */
|
|
294
298
|
cloudflarePublicImageAssets?: {
|
|
295
299
|
binding: {
|
|
@@ -297,6 +301,14 @@ interface DeweeHandlerOptions {
|
|
|
297
301
|
};
|
|
298
302
|
prefixes: string[];
|
|
299
303
|
};
|
|
304
|
+
/** Public-only service bindings for approved HTTPS image directory prefixes. Bypasses host security rules. */
|
|
305
|
+
cloudflarePublicImageServices?: Array<{
|
|
306
|
+
id: string;
|
|
307
|
+
binding: {
|
|
308
|
+
fetch(request: Request): Promise<Response>;
|
|
309
|
+
};
|
|
310
|
+
prefixes: string[];
|
|
311
|
+
}>;
|
|
300
312
|
/**
|
|
301
313
|
* Legacy gateway transport: let visitors bring their own gateway URL + token (sent per request, never stored).
|
|
302
314
|
* 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,
|
|
2
|
-
export { a as ActionManifestEntry, b as AdmissionErrorCode, A as AgentInfo, I as IdentityErrorCode, f as LinkTarget, T as ToolCallView } from './protocol-
|
|
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
|
@@ -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
|
|
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-
|
|
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-
|
|
4
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-OMRLBPEM.js";
|
|
4
|
+
import "./chunk-4EASDTWO.js";
|
|
5
5
|
|
|
6
6
|
// src/react.ts
|
|
7
7
|
import { createElement, forwardRef, useEffect, useImperativeHandle, useRef } from "react";
|
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-
|
|
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-_GEhpm1W.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-
|
|
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-
|
|
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-
|
|
18
|
+
} from "./chunk-4EASDTWO.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
|
-
|
|
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
|
-
|
|
232
|
-
|
|
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
|
|
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
|
-
|
|
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, publicAssets = []) {
|
|
1373
|
+
function allowedImageSource(raw, prefixes, endpoint, cloudflare, publicAssets = [], publicServices = []) {
|
|
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);
|
|
@@ -1339,7 +1384,8 @@ function allowedImageSource(raw, prefixes, endpoint, cloudflare, publicAssets =
|
|
|
1339
1384
|
const suffix = endpoint.hostname.split(".").slice(-2).join(".");
|
|
1340
1385
|
const sameZone = source.hostname === suffix || source.hostname.endsWith(`.${suffix}`);
|
|
1341
1386
|
const isPublicAsset = source.origin === endpoint.origin && publicAssets.some((prefix) => source.pathname.startsWith(prefix));
|
|
1342
|
-
|
|
1387
|
+
const isPublicService = publicServices.some((prefix) => prefix.origin === source.origin && source.pathname.startsWith(prefix.pathname));
|
|
1388
|
+
if (sameZone && !isPublicAsset && !isPublicService) return null;
|
|
1343
1389
|
}
|
|
1344
1390
|
return prefixes.some((prefix) => prefix.origin === source.origin && source.pathname.startsWith(prefix.pathname)) ? source : null;
|
|
1345
1391
|
} catch {
|
|
@@ -1666,6 +1712,18 @@ function createDeweeHandler(options) {
|
|
|
1666
1712
|
...options
|
|
1667
1713
|
};
|
|
1668
1714
|
const transport = resolveTransport(o);
|
|
1715
|
+
let credentialFormOrigin;
|
|
1716
|
+
if (o.credentialRequests) {
|
|
1717
|
+
if (transport !== "ccp" || !o.identity) throw new Error("[dewee-webchat] credentialRequests requires CCP transport and identity");
|
|
1718
|
+
try {
|
|
1719
|
+
if (typeof o.credentialRequests.formOrigin !== "string" || /[^\x21-\x7e]|[\\%]/.test(o.credentialRequests.formOrigin)) throw new Error();
|
|
1720
|
+
const u = new URL(o.credentialRequests.formOrigin);
|
|
1721
|
+
if (u.protocol !== "https:" || u.username || u.password || u.pathname !== "/" || u.search || u.hash) throw new Error();
|
|
1722
|
+
credentialFormOrigin = u.origin;
|
|
1723
|
+
} catch {
|
|
1724
|
+
throw new Error("[dewee-webchat] credentialRequests.formOrigin must be an ASCII HTTPS origin (use punycode for internationalized domains)");
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1669
1727
|
if (o.imageSources !== void 0 && !Array.isArray(o.imageSources)) throw new Error("[dewee-webchat] imageSources must be an array");
|
|
1670
1728
|
const imageSources = (o.imageSources ?? []).map((origin) => {
|
|
1671
1729
|
let url;
|
|
@@ -1682,6 +1740,35 @@ function createDeweeHandler(options) {
|
|
|
1682
1740
|
throw new Error("[dewee-webchat] Public image asset prefixes must be explicit absolute directory paths");
|
|
1683
1741
|
}
|
|
1684
1742
|
const assetPrefixes = publicAssets?.prefixes ?? [];
|
|
1743
|
+
if (o.cloudflarePublicImageServices !== void 0 && (!Array.isArray(o.cloudflarePublicImageServices) || o.cloudflarePublicImageServices.length > 16)) {
|
|
1744
|
+
throw new Error("[dewee-webchat] Public image services must be an array");
|
|
1745
|
+
}
|
|
1746
|
+
const publicServices = (o.cloudflarePublicImageServices ?? []).map((service) => {
|
|
1747
|
+
if (!service || typeof service.id !== "string" || !/^[A-Za-z0-9_-]{1,64}$/.test(service.id) || typeof service.binding?.fetch !== "function" || !Array.isArray(service.prefixes) || !service.prefixes.length) {
|
|
1748
|
+
throw new Error("[dewee-webchat] Public image services require a binding and directory prefixes");
|
|
1749
|
+
}
|
|
1750
|
+
const prefixes = service.prefixes.map((raw) => {
|
|
1751
|
+
let url;
|
|
1752
|
+
if (typeof raw !== "string" || !/^https:\/\/[^/?#]+\/(?:[A-Za-z0-9_-]+\/)+$/.test(raw)) {
|
|
1753
|
+
throw new Error("[dewee-webchat] Invalid public image service prefix");
|
|
1754
|
+
}
|
|
1755
|
+
try {
|
|
1756
|
+
url = new URL(raw);
|
|
1757
|
+
} catch {
|
|
1758
|
+
throw new Error("[dewee-webchat] Invalid public image service prefix");
|
|
1759
|
+
}
|
|
1760
|
+
if (url.protocol !== "https:" || url.username || url.password || url.port || url.search || url.hash || !/^\/(?:[A-Za-z0-9_-]+\/)+$/.test(url.pathname)) {
|
|
1761
|
+
throw new Error("[dewee-webchat] Public image service prefixes must be HTTPS origins with explicit directory paths");
|
|
1762
|
+
}
|
|
1763
|
+
return url;
|
|
1764
|
+
});
|
|
1765
|
+
return { id: service.id, binding: service.binding, prefixes };
|
|
1766
|
+
});
|
|
1767
|
+
const servicePrefixes = publicServices.flatMap((service) => service.prefixes);
|
|
1768
|
+
if (new Set(publicServices.map((service) => service.id)).size !== publicServices.length || servicePrefixes.some((prefix, index) => servicePrefixes.slice(index + 1).some((other) => prefix.origin === other.origin && (prefix.pathname.startsWith(other.pathname) || other.pathname.startsWith(prefix.pathname))))) {
|
|
1769
|
+
throw new Error("[dewee-webchat] Public image service ids and directory prefixes must not overlap");
|
|
1770
|
+
}
|
|
1771
|
+
const serviceFor = (source) => publicServices.find((service) => service.prefixes.some((prefix) => prefix.origin === source.origin && source.pathname.startsWith(prefix.pathname)));
|
|
1685
1772
|
let activeImages = 0;
|
|
1686
1773
|
const imageNamespace = `${o.ccp?.chatUrl ?? o.gatewayUrl ?? ""}
|
|
1687
1774
|
${o.ccp?.integrationId ?? o.tenantId ?? ""}`;
|
|
@@ -1703,7 +1790,8 @@ ${o.ccp?.integrationId ?? o.tenantId ?? ""}`;
|
|
|
1703
1790
|
const identityAgent = idn ? checkIdentityOptions(o, staticAgents, transport) : "";
|
|
1704
1791
|
const ccp = transport === "ccp" ? createCcpTransport(
|
|
1705
1792
|
new CcpClient(parseCcpOptions(o.ccp)),
|
|
1706
|
-
idn ? { createAssertion: (a) => idn.sdk.createAssertion(a), signer: () => identitySigner(), ttlSeconds: idn.sessionTtlSeconds } : void 0
|
|
1793
|
+
idn ? { createAssertion: (a) => idn.sdk.createAssertion(a), signer: () => identitySigner(), ttlSeconds: idn.sessionTtlSeconds } : void 0,
|
|
1794
|
+
!!credentialFormOrigin
|
|
1707
1795
|
) : null;
|
|
1708
1796
|
function visitorContext(req) {
|
|
1709
1797
|
const ip = clientIp(req);
|
|
@@ -2079,6 +2167,7 @@ ${o.ccp?.integrationId ?? o.tenantId ?? ""}`;
|
|
|
2079
2167
|
...o.turnstile ? { refreshWithoutTurnstile: true } : {},
|
|
2080
2168
|
maxMessageLength: Math.min(o.maxMessageLength, cfg.maxMessageChars),
|
|
2081
2169
|
...cfg.pageContextVersions.length ? { pageContextVersions: cfg.pageContextVersions } : {},
|
|
2170
|
+
...credentialFormOrigin ? { credentialFormOrigin } : {},
|
|
2082
2171
|
version: VERSION
|
|
2083
2172
|
};
|
|
2084
2173
|
if (idn) {
|
|
@@ -2180,7 +2269,19 @@ ${o.ccp?.integrationId ?? o.tenantId ?? ""}`;
|
|
|
2180
2269
|
if (!input.body || typeof input.body !== "object" || Array.isArray(input.body)) return fail(req, 400, "Invalid request", "bad_request");
|
|
2181
2270
|
const body = input.body;
|
|
2182
2271
|
const message = typeof body?.message === "string" ? body.message.trim() : "";
|
|
2183
|
-
|
|
2272
|
+
const resumeId = body?.credentialRequestId;
|
|
2273
|
+
if (resumeId !== void 0) {
|
|
2274
|
+
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");
|
|
2275
|
+
try {
|
|
2276
|
+
const origin = req.headers.get("origin");
|
|
2277
|
+
const u = new URL(origin ?? "");
|
|
2278
|
+
if (u.protocol !== "https:" || u.origin !== origin) throw new Error();
|
|
2279
|
+
} catch {
|
|
2280
|
+
return fail(req, 403, "Origin not allowed", "forbidden_origin");
|
|
2281
|
+
}
|
|
2282
|
+
const rebound = await rebindRefusal(req, claims, true);
|
|
2283
|
+
if (rebound) return rebound;
|
|
2284
|
+
} else if (!message) return fail(req, 400, "Message is required");
|
|
2184
2285
|
if (message.length > o.maxMessageLength) return fail(req, 413, `Message exceeds ${o.maxMessageLength} characters`, "too_long");
|
|
2185
2286
|
const idRe = ccp ? CCP_ID_RE : ID_RE;
|
|
2186
2287
|
const conversationId = typeof body?.conversationId === "string" && idRe.test(body.conversationId) ? body.conversationId : randomId(9);
|
|
@@ -2202,7 +2303,7 @@ ${o.ccp?.integrationId ?? o.tenantId ?? ""}`;
|
|
|
2202
2303
|
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");
|
|
2203
2304
|
if (!validPageContext(body.page_context)) return fail(req, 400, "Invalid page context", "page_context_invalid");
|
|
2204
2305
|
}
|
|
2205
|
-
if (ccp) return ccpChat(req, claims, { message: pageBlock + message, rawMessage: message, conversationId, agent, actionsHash, pageContext: body?.page_context });
|
|
2306
|
+
if (ccp) return ccpChat(req, claims, { message: pageBlock + message, rawMessage: message, conversationId, agent, actionsHash, pageContext: body?.page_context, credentialRequestId: resumeId });
|
|
2206
2307
|
if (!creds) return fail(req, 500, "Gateway is not configured", "not_configured");
|
|
2207
2308
|
let connection;
|
|
2208
2309
|
if (claims.ids || body?.page_context != null) {
|
|
@@ -2317,6 +2418,7 @@ ${o.ccp?.integrationId ?? o.tenantId ?? ""}`;
|
|
|
2317
2418
|
agent: turn.agent || void 0,
|
|
2318
2419
|
identitySession: claims.ids,
|
|
2319
2420
|
pageContext: turn.pageContext ?? void 0,
|
|
2421
|
+
credentialRequestId: turn.credentialRequestId,
|
|
2320
2422
|
signal: abort.signal
|
|
2321
2423
|
});
|
|
2322
2424
|
} catch (e) {
|
|
@@ -2349,7 +2451,7 @@ ${o.ccp?.integrationId ?? o.tenantId ?? ""}`;
|
|
|
2349
2451
|
let errMsg;
|
|
2350
2452
|
let agent = turn.agent || cfg.defaultAgent;
|
|
2351
2453
|
try {
|
|
2352
|
-
for await (const ev of ccp.chatEvents(upstream, cfg, turn.conversationId, agent)) {
|
|
2454
|
+
for await (const ev of ccp.chatEvents(upstream, cfg, turn.conversationId, agent, !!claims.ids)) {
|
|
2353
2455
|
if (ev.type === "start") {
|
|
2354
2456
|
runId = ev.runId;
|
|
2355
2457
|
agent = ev.agent;
|
|
@@ -2394,7 +2496,7 @@ ${o.ccp?.integrationId ?? o.tenantId ?? ""}`;
|
|
|
2394
2496
|
return new Response(readable, {
|
|
2395
2497
|
headers: {
|
|
2396
2498
|
"content-type": "text/event-stream; charset=utf-8",
|
|
2397
|
-
"cache-control": "no-
|
|
2499
|
+
"cache-control": "no-store, no-transform",
|
|
2398
2500
|
"x-accel-buffering": "no",
|
|
2399
2501
|
...corsHeaders(req)
|
|
2400
2502
|
}
|
|
@@ -2408,8 +2510,8 @@ ${o.ccp?.integrationId ?? o.tenantId ?? ""}`;
|
|
|
2408
2510
|
if (agent instanceof Response) return agent;
|
|
2409
2511
|
if (!CCP_ID_RE.test(conversationId)) return json(req, { messages: [] });
|
|
2410
2512
|
try {
|
|
2411
|
-
const
|
|
2412
|
-
return json(req,
|
|
2513
|
+
const history = await ccp.history(visitorContext(req), claims.v, sealedEdge(claims), { conversationId, agent: agent || void 0, identitySession: claims.ids });
|
|
2514
|
+
return json(req, history);
|
|
2413
2515
|
} catch (e) {
|
|
2414
2516
|
return ccpFail(req, e, "history", claims.ids);
|
|
2415
2517
|
}
|
|
@@ -2470,6 +2572,43 @@ ${o.ccp?.integrationId ?? o.tenantId ?? ""}`;
|
|
|
2470
2572
|
return fail(req, 502, "Job access is unavailable. Try again later.", "job_access_unavailable");
|
|
2471
2573
|
}
|
|
2472
2574
|
}
|
|
2575
|
+
async function handleCredentialRequest(req, route) {
|
|
2576
|
+
if (!ccp || !credentialFormOrigin || !idn) return fail(req, 404, "Not found", "not_found");
|
|
2577
|
+
const origin = req.headers.get("origin");
|
|
2578
|
+
try {
|
|
2579
|
+
const u = new URL(origin ?? "");
|
|
2580
|
+
if (u.protocol !== "https:" || u.origin !== origin) throw new Error();
|
|
2581
|
+
} catch {
|
|
2582
|
+
return fail(req, 403, "Origin not allowed", "forbidden_origin");
|
|
2583
|
+
}
|
|
2584
|
+
const claims = await visitorFrom(req);
|
|
2585
|
+
if (!claims?.ids || !claims.ida) return identityFail(req, "IDENTITY_REQUIRED");
|
|
2586
|
+
const refused = await rebindRefusal(req, claims, true);
|
|
2587
|
+
if (refused) return refused;
|
|
2588
|
+
if (limiter && !await limiter(`credential:${claims.v}`, route === "credential-request-handoff" ? "link" : "preauth")) return fail(req, 429, "Too many requests", "rate_limited");
|
|
2589
|
+
const raw = await readImageTicketBody(req);
|
|
2590
|
+
let body;
|
|
2591
|
+
try {
|
|
2592
|
+
body = raw && JSON.parse(raw);
|
|
2593
|
+
} catch {
|
|
2594
|
+
return fail(req, 400, "Invalid request", "bad_request");
|
|
2595
|
+
}
|
|
2596
|
+
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");
|
|
2597
|
+
const agent = ccpAgentFor(req, claims, body.agent);
|
|
2598
|
+
if (agent instanceof Response) return agent;
|
|
2599
|
+
if (!agent) return fail(req, 403, "Agent not allowed", "agent_not_allowed");
|
|
2600
|
+
try {
|
|
2601
|
+
const result = await ccp.credentialRequest(visitorContext(req), claims.v, sealedEdge(claims), route, { conversationId: body.conversationId, requestId: body.credentialRequestId, agent, identitySession: claims.ids });
|
|
2602
|
+
if (route === "credential-request-handoff") {
|
|
2603
|
+
const handoff = parseCredentialHandoff(result, body.credentialRequestId, credentialFormOrigin);
|
|
2604
|
+
return handoff ? json(req, handoff) : fail(req, 502, "Credential request is unavailable", "credential_request_unavailable");
|
|
2605
|
+
}
|
|
2606
|
+
const view = parseCredentialRequest(result?.credential_request, agent);
|
|
2607
|
+
return view && view.id === body.credentialRequestId ? json(req, { credentialRequest: view }) : fail(req, 502, "Credential request is unavailable", "credential_request_unavailable");
|
|
2608
|
+
} catch (e) {
|
|
2609
|
+
return ccpFail(req, e, route, claims.ids);
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2473
2612
|
async function handleLink(req) {
|
|
2474
2613
|
const claims = await visitorFrom(req);
|
|
2475
2614
|
if (!claims?.ids) return identityFail(req, "IDENTITY_REQUIRED");
|
|
@@ -2552,32 +2691,43 @@ ${o.ccp?.integrationId ?? o.tenantId ?? ""}`;
|
|
|
2552
2691
|
}
|
|
2553
2692
|
const raw = parsed?.url;
|
|
2554
2693
|
const worker = globalThis.navigator?.userAgent === "Cloudflare-Workers";
|
|
2555
|
-
const source = typeof raw === "string" ? allowedImageSource(raw, imageSources, new URL(req.url), worker, worker ? assetPrefixes : []) : null;
|
|
2694
|
+
const source = typeof raw === "string" ? allowedImageSource(raw, imageSources, new URL(req.url), worker, worker ? assetPrefixes : [], worker ? servicePrefixes : []) : null;
|
|
2556
2695
|
if (!source) return fail(req, 403, "Image not allowed", "forbidden_image");
|
|
2557
2696
|
const endpoint = new URL(req.url);
|
|
2558
2697
|
const asset = publicAssets && source.origin === endpoint.origin && assetPrefixes.some((prefix) => source.pathname.startsWith(prefix));
|
|
2698
|
+
const service = asset ? void 0 : serviceFor(source);
|
|
2559
2699
|
const namespace = asset ? `${imageNamespace}
|
|
2560
|
-
assets:${endpoint.origin}` : imageNamespace
|
|
2561
|
-
|
|
2700
|
+
assets:${endpoint.origin}` : service ? `${imageNamespace}
|
|
2701
|
+
service:${endpoint.origin}:${service.id}` : imageNamespace;
|
|
2702
|
+
const ticket = await issueImageTicket(secret, namespace, claims.v, source.href);
|
|
2703
|
+
return json(req, { ticket: service ? `s.${service.id}.${ticket}` : ticket });
|
|
2562
2704
|
}
|
|
2563
2705
|
if (req.method === "GET" && action === "image" && imageSources.length) {
|
|
2564
2706
|
const ticket = new URL(req.url).searchParams.get("ticket") ?? "";
|
|
2565
2707
|
const endpoint = new URL(req.url);
|
|
2566
|
-
const
|
|
2708
|
+
const tagged = /^s\.([A-Za-z0-9_-]{1,64})\.([A-Za-z0-9_-]{1,8192})$/.exec(ticket);
|
|
2709
|
+
const serviceId = tagged?.[1];
|
|
2710
|
+
if (serviceId && !publicServices.some((service2) => service2.id === serviceId)) return fail(req, 401, "Image ticket expired", "unauthorized");
|
|
2711
|
+
const serviceClaims = tagged ? await verifyImageTicket(secret, `${imageNamespace}
|
|
2712
|
+
service:${endpoint.origin}:${serviceId}`, tagged[2]) : null;
|
|
2713
|
+
const assetClaims = tagged ? null : await verifyImageTicket(secret, `${imageNamespace}
|
|
2567
2714
|
assets:${endpoint.origin}`, ticket);
|
|
2568
|
-
const claims = assetClaims ?? await verifyImageTicket(secret, imageNamespace, ticket);
|
|
2715
|
+
const claims = tagged ? serviceClaims : assetClaims ?? await verifyImageTicket(secret, imageNamespace, ticket);
|
|
2569
2716
|
if (!claims) return fail(req, 401, "Image ticket expired", "unauthorized");
|
|
2570
2717
|
const worker = globalThis.navigator?.userAgent === "Cloudflare-Workers";
|
|
2571
|
-
const source = allowedImageSource(claims.source, imageSources, endpoint, worker, worker ? assetPrefixes : []);
|
|
2718
|
+
const source = allowedImageSource(claims.source, imageSources, endpoint, worker, worker ? assetPrefixes : [], worker ? servicePrefixes : []);
|
|
2572
2719
|
if (!source) return fail(req, 403, "Image not allowed", "forbidden_image");
|
|
2573
2720
|
const asset = publicAssets && source.origin === endpoint.origin && assetPrefixes.some((prefix) => source.pathname.startsWith(prefix));
|
|
2574
2721
|
if (Boolean(asset) !== Boolean(assetClaims)) return fail(req, 403, "Image not allowed", "forbidden_image");
|
|
2722
|
+
const service = asset ? void 0 : serviceFor(source);
|
|
2723
|
+
if (service?.id !== serviceId) return fail(req, 403, "Image not allowed", "forbidden_image");
|
|
2575
2724
|
if (limiter && !await limiter(`image:${claims.visitor}`, "preauth")) return fail(req, 429, "Too many requests, try again later", "rate_limited");
|
|
2576
2725
|
if (activeImages >= 8) return fail(req, 429, "Too many requests, try again later", "rate_limited");
|
|
2577
2726
|
activeImages++;
|
|
2578
2727
|
try {
|
|
2579
2728
|
const assetFetch = (input, init) => publicAssets.binding.fetch(new Request(new URL(new URL(String(input)).pathname, "https://assets.invalid"), init));
|
|
2580
|
-
const
|
|
2729
|
+
const serviceFetch = (input, init) => service.binding.fetch(new Request(new URL(new URL(String(input)).pathname, "https://public-images.invalid"), init));
|
|
2730
|
+
const result = await fetchRasterImage(source.href, asset ? assetFetch : service ? serviceFetch : worker ? globalThis.fetch : publicImageFetchNode, worker || service ? "manual" : "error");
|
|
2581
2731
|
return result ?? fail(req, 502, "Image unavailable", "image_unavailable");
|
|
2582
2732
|
} finally {
|
|
2583
2733
|
activeImages--;
|
|
@@ -2589,6 +2739,7 @@ assets:${endpoint.origin}`, ticket);
|
|
|
2589
2739
|
if (req.method === "GET" && action === "history") return await handleHistory(req);
|
|
2590
2740
|
if (req.method === "POST" && action === "link" && idn) return await handleLink(req);
|
|
2591
2741
|
if (req.method === "POST" && action === "job-access" && idn) return await handleJobAccess(req);
|
|
2742
|
+
if (req.method === "POST" && (action === "credential-request" || action === "credential-request-handoff" || action === "credential-request-cancel")) return await handleCredentialRequest(req, action);
|
|
2592
2743
|
return fail(req, 404, "Not found", "not_found");
|
|
2593
2744
|
} catch (e) {
|
|
2594
2745
|
if (e instanceof IdentityStoreError) return identityFail(req, "SOURCE_UNAVAILABLE", { action: "retry_later", audience: "end_user" });
|