dewee-webchat 0.4.2 → 0.5.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -7
- package/astro/DeweeChat.astro +2 -2
- package/dist/{chunk-SAUDVPFH.js → chunk-KXW4EWXA.js} +1 -1
- package/dist/{chunk-UGHJZAKL.js → chunk-XCO4TCKF.js} +351 -50
- package/dist/dewee-webchat.js +34 -27
- package/dist/{handler-DunrbnSD.d.ts → handler-SsDyK0TG.d.ts} +57 -5
- package/dist/index.d.ts +33 -5
- package/dist/index.js +2 -2
- package/dist/page-context-H3ZROFAV.js +137 -0
- package/dist/{protocol-CLbGE9Ns.d.ts → protocol-BbjGSfZa.d.ts} +21 -2
- package/dist/react.d.ts +3 -1
- package/dist/react.js +9 -7
- package/dist/server-identity.d.ts +2 -2
- package/dist/server.d.ts +7 -4
- package/dist/server.js +648 -61
- package/dist/vue.d.ts +49 -2
- package/dist/vue.js +10 -5
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
parseJobAccessLaunch,
|
|
14
14
|
serializePageContext,
|
|
15
15
|
stripPageBlock
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-KXW4EWXA.js";
|
|
17
17
|
|
|
18
18
|
// src/server/admission-errors.ts
|
|
19
19
|
var MESSAGES = {
|
|
@@ -49,15 +49,17 @@ function admissionErrorResponse(code, retryAfterMs) {
|
|
|
49
49
|
// src/server/gateway-client.ts
|
|
50
50
|
var GatewayError = class extends Error {
|
|
51
51
|
/** `retryAfterMs` is the gateway's raw value for retryable refusals (`RATE_LIMITED`), unvalidated. */
|
|
52
|
-
constructor(message, code, remediation, retryAfterMs) {
|
|
52
|
+
constructor(message, code, remediation, retryAfterMs, reason) {
|
|
53
53
|
super(message);
|
|
54
54
|
this.code = code;
|
|
55
55
|
this.remediation = remediation;
|
|
56
56
|
this.retryAfterMs = retryAfterMs;
|
|
57
|
+
this.reason = reason;
|
|
57
58
|
}
|
|
58
59
|
code;
|
|
59
60
|
remediation;
|
|
60
61
|
retryAfterMs;
|
|
62
|
+
reason;
|
|
61
63
|
};
|
|
62
64
|
function parseRemediation(raw) {
|
|
63
65
|
if (raw === null || typeof raw !== "object") return void 0;
|
|
@@ -110,12 +112,14 @@ var GatewayConnection = class _GatewayConnection {
|
|
|
110
112
|
pending = /* @__PURE__ */ new Map();
|
|
111
113
|
listeners = /* @__PURE__ */ new Set();
|
|
112
114
|
closed = false;
|
|
115
|
+
pageContextVersions = [];
|
|
113
116
|
static async connect(creds, actor, timeoutMs = 15e3) {
|
|
114
117
|
const conn = new _GatewayConnection(await openSocket(creds.url, timeoutMs));
|
|
115
118
|
const params = typeof actor === "string" ? { token: creds.token, user_id: actor } : { token: creds.token, identity_session: actor.identitySession };
|
|
116
119
|
if (creds.tenantId) params.tenant_id = creds.tenantId;
|
|
117
120
|
try {
|
|
118
121
|
const res = await conn.call("connect", params, timeoutMs);
|
|
122
|
+
conn.pageContextVersions = Array.isArray(res?.page_context_versions) && res.page_context_versions.includes(1) ? [1] : [];
|
|
119
123
|
if (typeof actor !== "string" && !isIdentityUser(res?.user_id)) {
|
|
120
124
|
throw new GatewayError("Gateway did not apply the identity session", "SOURCE_UNAVAILABLE", { action: "retry_later", audience: "operator" });
|
|
121
125
|
}
|
|
@@ -173,7 +177,7 @@ var GatewayConnection = class _GatewayConnection {
|
|
|
173
177
|
if (frame.ok) p.resolve(frame.payload);
|
|
174
178
|
else {
|
|
175
179
|
const e = frame.error;
|
|
176
|
-
p.reject(new GatewayError(e?.message || "Gateway request failed", e?.code, parseRemediation(e?.details?.remediation), e?.retryAfterMs));
|
|
180
|
+
p.reject(new GatewayError(e?.message || "Gateway request failed", e?.code, parseRemediation(e?.details?.remediation), e?.retryAfterMs, e?.details?.reason));
|
|
177
181
|
}
|
|
178
182
|
} else if (frame.type === "event" && frame.event) {
|
|
179
183
|
for (const cb of this.listeners) cb(frame.event, frame.payload);
|
|
@@ -243,11 +247,12 @@ async function* streamChat(opts) {
|
|
|
243
247
|
end();
|
|
244
248
|
};
|
|
245
249
|
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
246
|
-
conn.call("chat.send", { agentId: opts.agent, sessionKey: opts.sessionKey, message: opts.message, stream: true }, opts.timeoutMs ?? 18e4).then((res) => {
|
|
250
|
+
conn.call("chat.send", { agentId: opts.agent, sessionKey: opts.sessionKey, message: opts.message, stream: true, ...opts.pageContext !== void 0 ? { pageContext: opts.pageContext } : {} }, opts.timeoutMs ?? 18e4).then((res) => {
|
|
247
251
|
push({ type: "done", content: res?.content ?? "", usage: res?.usage });
|
|
248
252
|
end();
|
|
249
253
|
}).catch((err) => {
|
|
250
|
-
if (!finished &&
|
|
254
|
+
if (!finished && (err.reason === "page_context_invalid" || err.reason === "page_context_unsupported")) push({ type: "error", code: err.reason, message: err.reason === "page_context_invalid" ? "Invalid page context" : "Page context is not supported" });
|
|
255
|
+
else if (!finished && isAdmissionErrorCode(err.code)) push(admissionErrorEvent(err.code, err.retryAfterMs, err.message));
|
|
251
256
|
else if (!finished) push(err.remediation ? { type: "error", message: err.message, code: err.code, remediation: err.remediation } : { type: "error", message: err.message, code: err.code });
|
|
252
257
|
end();
|
|
253
258
|
});
|
|
@@ -760,6 +765,67 @@ function resolveTransport(o) {
|
|
|
760
765
|
return transport;
|
|
761
766
|
}
|
|
762
767
|
|
|
768
|
+
// src/server/page-context.ts
|
|
769
|
+
async function readChatBody(request) {
|
|
770
|
+
const limit = 128 * 1024;
|
|
771
|
+
const length = request.headers.get("content-length");
|
|
772
|
+
if (length && (!/^\d+$/.test(length) || Number(length) > limit)) return { error: "too_large" };
|
|
773
|
+
if (!request.body) return { error: "invalid" };
|
|
774
|
+
const reader = request.body.getReader();
|
|
775
|
+
const chunks = [];
|
|
776
|
+
let size = 0;
|
|
777
|
+
try {
|
|
778
|
+
for (; ; ) {
|
|
779
|
+
const { done, value } = await reader.read();
|
|
780
|
+
if (done) break;
|
|
781
|
+
size += value.byteLength;
|
|
782
|
+
if (size > limit) {
|
|
783
|
+
await reader.cancel();
|
|
784
|
+
return { error: "too_large" };
|
|
785
|
+
}
|
|
786
|
+
chunks.push(value);
|
|
787
|
+
}
|
|
788
|
+
const bytes = new Uint8Array(size);
|
|
789
|
+
let offset = 0;
|
|
790
|
+
for (const chunk of chunks) {
|
|
791
|
+
bytes.set(chunk, offset);
|
|
792
|
+
offset += chunk.length;
|
|
793
|
+
}
|
|
794
|
+
return { body: JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)) };
|
|
795
|
+
} catch {
|
|
796
|
+
return { error: "invalid" };
|
|
797
|
+
} finally {
|
|
798
|
+
reader.releaseLock();
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
function validPageContext(value) {
|
|
802
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
803
|
+
const o = value;
|
|
804
|
+
const allowed = /* @__PURE__ */ new Set(["version", "url", "title", "description", "captured_at", "selection", "content", "source", "trust_level"]);
|
|
805
|
+
if (Object.keys(o).some((key) => !allowed.has(key)) || o.version !== 1 || typeof o.url !== "string") return false;
|
|
806
|
+
if (new TextEncoder().encode(JSON.stringify(o)).length > 65536 || new TextEncoder().encode(o.url).length > 2048) return false;
|
|
807
|
+
try {
|
|
808
|
+
const url = new URL(o.url);
|
|
809
|
+
if (!["http:", "https:"].includes(url.protocol) || !url.hostname || url.username || url.password) return false;
|
|
810
|
+
} catch {
|
|
811
|
+
return false;
|
|
812
|
+
}
|
|
813
|
+
const limits = { title: 300, description: 1e3, selection: 2e3, captured_at: 64 };
|
|
814
|
+
for (const [key, max] of Object.entries(limits)) {
|
|
815
|
+
const item = o[key];
|
|
816
|
+
if (item != null && (typeof item !== "string" || Array.from(item).length > max)) return false;
|
|
817
|
+
}
|
|
818
|
+
if (o.content != null) {
|
|
819
|
+
if (typeof o.content !== "object" || Array.isArray(o.content)) return false;
|
|
820
|
+
const c = o.content;
|
|
821
|
+
if (Object.keys(c).some((key) => !["format", "text", "truncated"].includes(key))) return false;
|
|
822
|
+
if (c.format != null && c.format !== "text" && c.format !== "markdown") return false;
|
|
823
|
+
if (c.text != null && (typeof c.text !== "string" || Array.from(c.text).length > 12e3)) return false;
|
|
824
|
+
if (c.truncated != null && typeof c.truncated !== "boolean") return false;
|
|
825
|
+
}
|
|
826
|
+
return true;
|
|
827
|
+
}
|
|
828
|
+
|
|
763
829
|
// src/server/job-access.ts
|
|
764
830
|
var JobAccessRefusal = class extends Error {
|
|
765
831
|
constructor(code) {
|
|
@@ -827,6 +893,10 @@ function browserErrorFor(e, route) {
|
|
|
827
893
|
if (e.code === "rate_limited" || e.code === "concurrency_limited" || e.status === 429) return { status: 429, code: "RATE_LIMITED", message: "", admission: "RATE_LIMITED", retryAfterMs };
|
|
828
894
|
}
|
|
829
895
|
switch (e.code) {
|
|
896
|
+
case "page_context_invalid":
|
|
897
|
+
return { status: 400, code: e.code, message: "Invalid page context" };
|
|
898
|
+
case "page_context_unsupported":
|
|
899
|
+
return { status: 400, code: e.code, message: "Page context is not supported" };
|
|
830
900
|
case "invalid_request":
|
|
831
901
|
log("error", "- the ingress refused the request shape; check the dewee-webchat version");
|
|
832
902
|
return { status: 400, code: "bad_request", message: "Invalid request" };
|
|
@@ -909,7 +979,8 @@ function parseConfig(data) {
|
|
|
909
979
|
// Reasoning and tool activity stay hidden unless the integration turns them on explicitly.
|
|
910
980
|
showThinking: data.show_thinking === true,
|
|
911
981
|
showToolActivity: data.show_tool_activity === true,
|
|
912
|
-
maxMessageChars: Number.isInteger(max) && max > 0 ? max : Number.POSITIVE_INFINITY
|
|
982
|
+
maxMessageChars: Number.isInteger(max) && max > 0 ? max : Number.POSITIVE_INFINITY,
|
|
983
|
+
pageContextVersions: Array.isArray(data.page_context?.versions) && data.page_context.versions.includes(1) ? [1] : []
|
|
913
984
|
};
|
|
914
985
|
}
|
|
915
986
|
function numericUsage(raw) {
|
|
@@ -994,7 +1065,8 @@ function createCcpTransport(client, identity) {
|
|
|
994
1065
|
message: input.message,
|
|
995
1066
|
conversation_id: input.conversationId,
|
|
996
1067
|
...withAgent(input.agent),
|
|
997
|
-
...input.identitySession ? { identity_session: input.identitySession } : {}
|
|
1068
|
+
...input.identitySession ? { identity_session: input.identitySession } : {},
|
|
1069
|
+
...input.pageContext !== void 0 ? { page_context: input.pageContext } : {}
|
|
998
1070
|
};
|
|
999
1071
|
return withEdge(ctx, visitorId, sealed, (edge) => client.send("chat", { ...base(ctx), body, session: edge.token, accept: "text/event-stream", signal: input.signal }));
|
|
1000
1072
|
},
|
|
@@ -1152,14 +1224,403 @@ function createCcpTransport(client, identity) {
|
|
|
1152
1224
|
};
|
|
1153
1225
|
}
|
|
1154
1226
|
|
|
1227
|
+
// src/server/identity-session-store.ts
|
|
1228
|
+
function memoryIdentitySessionStore(limit = 1e3) {
|
|
1229
|
+
const subjects = /* @__PURE__ */ new Map();
|
|
1230
|
+
const revision = () => crypto.randomUUID();
|
|
1231
|
+
const get = (subjectKey) => {
|
|
1232
|
+
let entry = subjects.get(subjectKey);
|
|
1233
|
+
if (!entry) entry = { revision: revision(), sessions: /* @__PURE__ */ new Map() };
|
|
1234
|
+
subjects.delete(subjectKey);
|
|
1235
|
+
subjects.set(subjectKey, entry);
|
|
1236
|
+
while (subjects.size > limit) subjects.delete(subjects.keys().next().value);
|
|
1237
|
+
return entry;
|
|
1238
|
+
};
|
|
1239
|
+
return {
|
|
1240
|
+
async read(subjectKey, agent) {
|
|
1241
|
+
const entry = get(subjectKey);
|
|
1242
|
+
return { revision: entry.revision, session: entry.sessions.get(agent) ?? null };
|
|
1243
|
+
},
|
|
1244
|
+
async check(subjectKey, expected) {
|
|
1245
|
+
return subjects.get(subjectKey)?.revision === expected;
|
|
1246
|
+
},
|
|
1247
|
+
async commit(subjectKey, agent, expected, session) {
|
|
1248
|
+
const entry = subjects.get(subjectKey);
|
|
1249
|
+
if (!entry || entry.revision !== expected) return false;
|
|
1250
|
+
entry.sessions.set(agent, session);
|
|
1251
|
+
entry.revision = revision();
|
|
1252
|
+
return true;
|
|
1253
|
+
},
|
|
1254
|
+
async deleteToken(namespace, token) {
|
|
1255
|
+
for (const [subjectKey, entry] of subjects) if (subjectKey.startsWith(namespace + ":")) {
|
|
1256
|
+
for (const [agent, session] of entry.sessions) if (session.sessionToken === token) {
|
|
1257
|
+
entry.sessions.delete(agent);
|
|
1258
|
+
entry.revision = revision();
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
},
|
|
1262
|
+
async takeSubject(subjectKey) {
|
|
1263
|
+
const entry = get(subjectKey);
|
|
1264
|
+
const held = [...entry.sessions].map(([agent, session]) => ({ agent, session }));
|
|
1265
|
+
entry.sessions.clear();
|
|
1266
|
+
entry.revision = revision();
|
|
1267
|
+
return held;
|
|
1268
|
+
}
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
// src/server/image-proxy.ts
|
|
1273
|
+
var utf8 = new TextEncoder();
|
|
1274
|
+
var encode = (data) => btoa(String.fromCharCode(...data)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1275
|
+
var decode = (data) => Uint8Array.from(atob(data.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((data.length + 3) % 4)), (c) => c.charCodeAt(0));
|
|
1276
|
+
async function readImageTicketBody(request) {
|
|
1277
|
+
if (!request.body) return null;
|
|
1278
|
+
const reader = request.body.getReader();
|
|
1279
|
+
const chunks = [];
|
|
1280
|
+
let size = 0;
|
|
1281
|
+
try {
|
|
1282
|
+
for (; ; ) {
|
|
1283
|
+
const { done, value } = await reader.read();
|
|
1284
|
+
if (done) break;
|
|
1285
|
+
size += value.byteLength;
|
|
1286
|
+
if (size > 8192) {
|
|
1287
|
+
await reader.cancel();
|
|
1288
|
+
return null;
|
|
1289
|
+
}
|
|
1290
|
+
chunks.push(value);
|
|
1291
|
+
}
|
|
1292
|
+
} finally {
|
|
1293
|
+
reader.releaseLock();
|
|
1294
|
+
}
|
|
1295
|
+
const all = new Uint8Array(size);
|
|
1296
|
+
let offset = 0;
|
|
1297
|
+
for (const chunk of chunks) {
|
|
1298
|
+
all.set(chunk, offset);
|
|
1299
|
+
offset += chunk.length;
|
|
1300
|
+
}
|
|
1301
|
+
return new TextDecoder().decode(all);
|
|
1302
|
+
}
|
|
1303
|
+
async function ticketKey(secret) {
|
|
1304
|
+
const base = await crypto.subtle.importKey("raw", utf8.encode(secret), "HKDF", false, ["deriveKey"]);
|
|
1305
|
+
return crypto.subtle.deriveKey({ name: "HKDF", hash: "SHA-256", salt: utf8.encode("dewee-webchat"), info: utf8.encode("dewee-webchat:image-ticket:aes-256-gcm:v1") }, base, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
|
|
1306
|
+
}
|
|
1307
|
+
async function issueImageTicket(secret, namespace, visitor, source) {
|
|
1308
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
1309
|
+
const payload = utf8.encode(JSON.stringify({ v: visitor, u: source, exp: Date.now() + 6e4 }));
|
|
1310
|
+
const ciphertext = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv, additionalData: utf8.encode(`dewee-image-v1:${namespace}`) }, await ticketKey(secret), payload));
|
|
1311
|
+
const result = new Uint8Array(iv.length + ciphertext.length);
|
|
1312
|
+
result.set(iv);
|
|
1313
|
+
result.set(ciphertext, iv.length);
|
|
1314
|
+
return encode(result);
|
|
1315
|
+
}
|
|
1316
|
+
async function verifyImageTicket(secret, namespace, ticket) {
|
|
1317
|
+
try {
|
|
1318
|
+
if (ticket.length > 8192 || !/^[A-Za-z0-9_-]+$/.test(ticket)) return null;
|
|
1319
|
+
const raw = decode(ticket);
|
|
1320
|
+
if (raw.length <= 28) return null;
|
|
1321
|
+
const plaintext = await crypto.subtle.decrypt({ name: "AES-GCM", iv: raw.slice(0, 12), additionalData: utf8.encode(`dewee-image-v1:${namespace}`) }, await ticketKey(secret), raw.slice(12));
|
|
1322
|
+
const data = JSON.parse(new TextDecoder().decode(plaintext));
|
|
1323
|
+
return typeof data.v === "string" && /^[A-Za-z0-9_-]{8,64}$/.test(data.v) && typeof data.u === "string" && data.u.length <= 4096 && Number.isSafeInteger(data.exp) && data.exp > Date.now() && data.exp <= Date.now() + 6e4 ? { visitor: data.v, source: data.u } : null;
|
|
1324
|
+
} catch {
|
|
1325
|
+
return null;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
function allowedImageSource(raw, prefixes, endpoint, cloudflare, publicAssets = []) {
|
|
1329
|
+
try {
|
|
1330
|
+
if (!raw || raw.length > 4096 || /[\u0000-\u0020\u007f\\]/.test(raw)) return null;
|
|
1331
|
+
const match = /^https:\/\/[^/?#]+([^?#]*)/i.exec(raw);
|
|
1332
|
+
if (!match || /%(?:2f|5c|25|2e|0[0-9a-f]|1[0-9a-f]|7f)/i.test(match[1])) return null;
|
|
1333
|
+
const source = new URL(raw);
|
|
1334
|
+
if (source.protocol !== "https:" || source.username || source.password || source.hash || source.port) return null;
|
|
1335
|
+
const parent = endpoint.pathname.replace(/\/+$/, "").replace(/\/(?:image|image-ticket)$/, "");
|
|
1336
|
+
if (parent && (source.pathname === parent || source.pathname.startsWith(`${parent}/`))) return null;
|
|
1337
|
+
if (!parent && /^\/(?:image|image-ticket)(?:\/|$)/.test(source.pathname)) return null;
|
|
1338
|
+
if (cloudflare) {
|
|
1339
|
+
const suffix = endpoint.hostname.split(".").slice(-2).join(".");
|
|
1340
|
+
const sameZone = source.hostname === suffix || source.hostname.endsWith(`.${suffix}`);
|
|
1341
|
+
const isPublicAsset = source.origin === endpoint.origin && publicAssets.some((prefix) => source.pathname.startsWith(prefix));
|
|
1342
|
+
if (sameZone && !isPublicAsset) return null;
|
|
1343
|
+
}
|
|
1344
|
+
return prefixes.some((prefix) => prefix.origin === source.origin && source.pathname.startsWith(prefix.pathname)) ? source : null;
|
|
1345
|
+
} catch {
|
|
1346
|
+
return null;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
function safeRasterDimensions(bytes, type) {
|
|
1350
|
+
const ascii = (start, end) => String.fromCharCode(...bytes.slice(start, end));
|
|
1351
|
+
const u16 = (at, little = false) => little ? bytes[at] + bytes[at + 1] * 256 : bytes[at] * 256 + bytes[at + 1];
|
|
1352
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
1353
|
+
const u32 = (at, little = false) => view.getUint32(at, little);
|
|
1354
|
+
const sizeOK = (w, h) => w > 0 && h > 0 && w <= 4096 && h <= 4096 && w * h <= 16e6;
|
|
1355
|
+
let width = 0, height = 0;
|
|
1356
|
+
if (type === "image/png") {
|
|
1357
|
+
let pos = 8, header = false, data = false, end = false;
|
|
1358
|
+
while (pos + 12 <= bytes.length) {
|
|
1359
|
+
const length = u32(pos), name = ascii(pos + 4, pos + 8);
|
|
1360
|
+
if (length > bytes.length - pos - 12) return false;
|
|
1361
|
+
if (name === "IHDR") {
|
|
1362
|
+
if (header || pos !== 8 || length !== 13) return false;
|
|
1363
|
+
header = true;
|
|
1364
|
+
width = u32(pos + 8);
|
|
1365
|
+
height = u32(pos + 12);
|
|
1366
|
+
} else if (name === "acTL" || name === "fcTL" || name === "fdAT") return false;
|
|
1367
|
+
else if (name === "IDAT") data = true;
|
|
1368
|
+
else if (name === "IEND") {
|
|
1369
|
+
end = length === 0;
|
|
1370
|
+
break;
|
|
1371
|
+
}
|
|
1372
|
+
pos += length + 12;
|
|
1373
|
+
}
|
|
1374
|
+
if (!header || !data || !end) return false;
|
|
1375
|
+
} else if (type === "image/gif" && bytes.length >= 13) {
|
|
1376
|
+
width = u16(6, true);
|
|
1377
|
+
height = u16(8, true);
|
|
1378
|
+
let pos = 13 + (bytes[10] & 128 ? 3 * (1 << (bytes[10] & 7) + 1) : 0), frames = 0, end = false;
|
|
1379
|
+
const blocks = () => {
|
|
1380
|
+
while (pos < bytes.length) {
|
|
1381
|
+
const n = bytes[pos++];
|
|
1382
|
+
if (n === 0) return true;
|
|
1383
|
+
if (n > bytes.length - pos) return false;
|
|
1384
|
+
pos += n;
|
|
1385
|
+
}
|
|
1386
|
+
return false;
|
|
1387
|
+
};
|
|
1388
|
+
while (pos < bytes.length) {
|
|
1389
|
+
const marker = bytes[pos++];
|
|
1390
|
+
if (marker === 59) {
|
|
1391
|
+
end = true;
|
|
1392
|
+
break;
|
|
1393
|
+
}
|
|
1394
|
+
if (marker === 33) {
|
|
1395
|
+
if (pos >= bytes.length) return false;
|
|
1396
|
+
pos++;
|
|
1397
|
+
if (!blocks()) return false;
|
|
1398
|
+
continue;
|
|
1399
|
+
}
|
|
1400
|
+
if (marker !== 44 || pos + 9 > bytes.length || ++frames > 1) return false;
|
|
1401
|
+
const left = u16(pos, true), top = u16(pos + 2, true), w = u16(pos + 4, true), h = u16(pos + 6, true), flags = bytes[pos + 8];
|
|
1402
|
+
if (!sizeOK(w, h) || left + w > width || top + h > height) return false;
|
|
1403
|
+
pos += 9 + (flags & 128 ? 3 * (1 << (flags & 7) + 1) : 0);
|
|
1404
|
+
if (pos >= bytes.length) return false;
|
|
1405
|
+
pos++;
|
|
1406
|
+
if (!blocks()) return false;
|
|
1407
|
+
}
|
|
1408
|
+
if (frames !== 1 || !end) return false;
|
|
1409
|
+
} else if (type === "image/jpeg") {
|
|
1410
|
+
let pos = 2, frames = 0, end = false;
|
|
1411
|
+
while (pos + 1 < bytes.length && bytes[pos] === 255) {
|
|
1412
|
+
while (bytes[pos] === 255) pos++;
|
|
1413
|
+
const marker = bytes[pos++];
|
|
1414
|
+
if (marker === 217) {
|
|
1415
|
+
end = true;
|
|
1416
|
+
break;
|
|
1417
|
+
}
|
|
1418
|
+
if (marker === 1 || marker >= 208 && marker <= 215) continue;
|
|
1419
|
+
if (pos + 2 > bytes.length) return false;
|
|
1420
|
+
const length = u16(pos);
|
|
1421
|
+
if (length < 2 || pos + length > bytes.length) return false;
|
|
1422
|
+
if ([192, 193, 194, 195, 197, 198, 199, 201, 202, 203, 205, 206, 207].includes(marker)) {
|
|
1423
|
+
if (++frames !== 1 || length < 7) return false;
|
|
1424
|
+
height = u16(pos + 3);
|
|
1425
|
+
width = u16(pos + 5);
|
|
1426
|
+
}
|
|
1427
|
+
pos += length;
|
|
1428
|
+
if (marker === 218) {
|
|
1429
|
+
while (pos + 1 < bytes.length) {
|
|
1430
|
+
if (bytes[pos] !== 255) {
|
|
1431
|
+
pos++;
|
|
1432
|
+
continue;
|
|
1433
|
+
}
|
|
1434
|
+
const next = bytes[pos + 1];
|
|
1435
|
+
if (next === 0 || next >= 208 && next <= 215) {
|
|
1436
|
+
pos += 2;
|
|
1437
|
+
continue;
|
|
1438
|
+
}
|
|
1439
|
+
break;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
if (frames !== 1 || !end) return false;
|
|
1444
|
+
} else if (type === "image/webp" && bytes.length >= 20) {
|
|
1445
|
+
const total = u32(4, true) + 8;
|
|
1446
|
+
if (total > bytes.length || total < 20) return false;
|
|
1447
|
+
let pos = 12, images = 0, canvasW = 0, canvasH = 0;
|
|
1448
|
+
while (pos + 8 <= total) {
|
|
1449
|
+
const chunk = ascii(pos, pos + 4), length = u32(pos + 4, true), start = pos + 8;
|
|
1450
|
+
if (length > total - start || start + length + (length & 1) > total || chunk === "ANIM" || chunk === "ANMF") return false;
|
|
1451
|
+
if (chunk === "VP8X") {
|
|
1452
|
+
if (length !== 10 || canvasW || bytes[start] & 2) return false;
|
|
1453
|
+
canvasW = 1 + bytes[start + 4] + (bytes[start + 5] << 8) + (bytes[start + 6] << 16);
|
|
1454
|
+
canvasH = 1 + bytes[start + 7] + (bytes[start + 8] << 8) + (bytes[start + 9] << 16);
|
|
1455
|
+
} else if (chunk === "VP8L" && length >= 5 && bytes[start] === 47) {
|
|
1456
|
+
images++;
|
|
1457
|
+
width = 1 + bytes[start + 1] + ((bytes[start + 2] & 63) << 8);
|
|
1458
|
+
height = 1 + (bytes[start + 2] >> 6) + (bytes[start + 3] << 2) + ((bytes[start + 4] & 15) << 10);
|
|
1459
|
+
} else if (chunk === "VP8 " && length >= 10 && bytes[start + 3] === 157 && bytes[start + 4] === 1 && bytes[start + 5] === 42) {
|
|
1460
|
+
images++;
|
|
1461
|
+
width = u16(start + 6, true) & 16383;
|
|
1462
|
+
height = u16(start + 8, true) & 16383;
|
|
1463
|
+
}
|
|
1464
|
+
pos = start + length + (length & 1);
|
|
1465
|
+
}
|
|
1466
|
+
if (pos !== total || images !== 1 || canvasW && (canvasW !== width || canvasH !== height)) return false;
|
|
1467
|
+
}
|
|
1468
|
+
return sizeOK(width, height);
|
|
1469
|
+
}
|
|
1470
|
+
async function fetchRasterImage(url, fetcher, redirect = "error") {
|
|
1471
|
+
const controller = new AbortController();
|
|
1472
|
+
const timer = setTimeout(() => controller.abort(), 1e4);
|
|
1473
|
+
try {
|
|
1474
|
+
const response = await fetcher(url, { method: "GET", redirect, credentials: "omit", signal: controller.signal, headers: { accept: "image/png,image/jpeg,image/gif,image/webp" } });
|
|
1475
|
+
if (!response.ok || !response.body) {
|
|
1476
|
+
await response.body?.cancel();
|
|
1477
|
+
return null;
|
|
1478
|
+
}
|
|
1479
|
+
const type = response.headers.get("content-type")?.split(";", 1)[0].trim().toLowerCase();
|
|
1480
|
+
if (!type || !["image/png", "image/jpeg", "image/gif", "image/webp"].includes(type)) {
|
|
1481
|
+
await response.body.cancel();
|
|
1482
|
+
return null;
|
|
1483
|
+
}
|
|
1484
|
+
const length = response.headers.get("content-length");
|
|
1485
|
+
const maxBytes = 2 * 1024 * 1024;
|
|
1486
|
+
if (length && (!/^\d+$/.test(length) || Number(length) > maxBytes)) {
|
|
1487
|
+
await response.body.cancel();
|
|
1488
|
+
return null;
|
|
1489
|
+
}
|
|
1490
|
+
const reader = response.body.getReader();
|
|
1491
|
+
const chunks = [];
|
|
1492
|
+
let size = 0;
|
|
1493
|
+
try {
|
|
1494
|
+
for (; ; ) {
|
|
1495
|
+
const { done, value } = await reader.read();
|
|
1496
|
+
if (done) break;
|
|
1497
|
+
size += value.byteLength;
|
|
1498
|
+
if (size > maxBytes) {
|
|
1499
|
+
await reader.cancel();
|
|
1500
|
+
return null;
|
|
1501
|
+
}
|
|
1502
|
+
chunks.push(value);
|
|
1503
|
+
}
|
|
1504
|
+
} finally {
|
|
1505
|
+
reader.releaseLock();
|
|
1506
|
+
}
|
|
1507
|
+
const bytes = new Uint8Array(size);
|
|
1508
|
+
let offset = 0;
|
|
1509
|
+
for (const chunk of chunks) {
|
|
1510
|
+
bytes.set(chunk, offset);
|
|
1511
|
+
offset += chunk.length;
|
|
1512
|
+
}
|
|
1513
|
+
const starts = (...signature) => signature.every((value, index) => bytes[index] === value);
|
|
1514
|
+
const ascii = (start, end) => String.fromCharCode(...bytes.slice(start, end));
|
|
1515
|
+
const valid = type === "image/png" ? starts(137, 80, 78, 71, 13, 10, 26, 10) : type === "image/jpeg" ? starts(255, 216, 255) : type === "image/gif" ? ["GIF87a", "GIF89a"].includes(ascii(0, 6)) : ascii(0, 4) === "RIFF" && ascii(8, 12) === "WEBP";
|
|
1516
|
+
if (!valid || !safeRasterDimensions(bytes, type)) return null;
|
|
1517
|
+
return new Response(bytes, { headers: { "content-type": type, "x-content-type-options": "nosniff", "cache-control": "private, no-store", "content-security-policy": "default-src 'none'; sandbox" } });
|
|
1518
|
+
} catch {
|
|
1519
|
+
return null;
|
|
1520
|
+
} finally {
|
|
1521
|
+
clearTimeout(timer);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// src/server/public-image-fetch-node.ts
|
|
1526
|
+
async function publicImageFetchNode(input, init) {
|
|
1527
|
+
if (typeof input !== "string") throw new Error("Invalid image URL");
|
|
1528
|
+
const url = new URL(input);
|
|
1529
|
+
if (url.protocol !== "https:" || url.username || url.password || url.port && url.port !== "443") throw new Error("Invalid image URL");
|
|
1530
|
+
const prefix = "node:";
|
|
1531
|
+
const dns = await import(prefix + "dns/promises");
|
|
1532
|
+
const net = await import(prefix + "net");
|
|
1533
|
+
const https = await import(prefix + "https");
|
|
1534
|
+
if (net.isIP(url.hostname)) throw new Error("IP literals are not allowed");
|
|
1535
|
+
const lookup = dns.lookup(url.hostname, { all: true, verbatim: true });
|
|
1536
|
+
const signal = init?.signal;
|
|
1537
|
+
const addresses = signal ? await new Promise((resolve, reject) => {
|
|
1538
|
+
if (signal.aborted) {
|
|
1539
|
+
reject(new Error("Image request timed out"));
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
const abort = () => reject(new Error("Image request timed out"));
|
|
1543
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
1544
|
+
lookup.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
|
|
1545
|
+
}) : await lookup;
|
|
1546
|
+
if (!addresses.length) throw new Error("No image address");
|
|
1547
|
+
if (!publicImageAddresses(addresses, net.BlockList)) throw new Error("Non-public image address");
|
|
1548
|
+
const chosen = addresses.find((entry) => entry.family === 4) ?? addresses[0];
|
|
1549
|
+
return new Promise((resolve, reject) => {
|
|
1550
|
+
const request = https.request(url, {
|
|
1551
|
+
method: "GET",
|
|
1552
|
+
headers: { accept: "image/png,image/jpeg,image/gif,image/webp" },
|
|
1553
|
+
signal: init?.signal ?? void 0,
|
|
1554
|
+
family: chosen.family,
|
|
1555
|
+
lookup: (_host, _options, callback) => callback(null, chosen.address, chosen.family)
|
|
1556
|
+
}, (response) => {
|
|
1557
|
+
if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 300 || response.statusCode === 204 || response.statusCode === 205) {
|
|
1558
|
+
response.destroy();
|
|
1559
|
+
reject(new Error("Image upstream refused"));
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
const headers = new Headers();
|
|
1563
|
+
for (const [name, value] of Object.entries(response.headers)) if (typeof value === "string") headers.set(name, value);
|
|
1564
|
+
let received = 0;
|
|
1565
|
+
let settled = false;
|
|
1566
|
+
const body = new ReadableStream({
|
|
1567
|
+
start(controller) {
|
|
1568
|
+
response.pause();
|
|
1569
|
+
response.on("data", (chunk) => {
|
|
1570
|
+
response.pause();
|
|
1571
|
+
received += chunk.length;
|
|
1572
|
+
if (received > 2 * 1024 * 1024) {
|
|
1573
|
+
settled = true;
|
|
1574
|
+
response.destroy();
|
|
1575
|
+
controller.error(new Error("Image too large"));
|
|
1576
|
+
} else controller.enqueue(new Uint8Array(chunk));
|
|
1577
|
+
});
|
|
1578
|
+
response.on("end", () => {
|
|
1579
|
+
if (!settled) {
|
|
1580
|
+
settled = true;
|
|
1581
|
+
controller.close();
|
|
1582
|
+
}
|
|
1583
|
+
});
|
|
1584
|
+
response.on("error", (error) => {
|
|
1585
|
+
if (!settled) {
|
|
1586
|
+
settled = true;
|
|
1587
|
+
controller.error(error);
|
|
1588
|
+
}
|
|
1589
|
+
});
|
|
1590
|
+
},
|
|
1591
|
+
pull() {
|
|
1592
|
+
response.resume();
|
|
1593
|
+
},
|
|
1594
|
+
cancel() {
|
|
1595
|
+
settled = true;
|
|
1596
|
+
response.destroy();
|
|
1597
|
+
}
|
|
1598
|
+
});
|
|
1599
|
+
resolve(new Response(body, { status: response.statusCode ?? 502, headers }));
|
|
1600
|
+
});
|
|
1601
|
+
request.on("error", reject);
|
|
1602
|
+
request.end();
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
function publicImageAddresses(addresses, BlockList) {
|
|
1606
|
+
const denied4 = new BlockList();
|
|
1607
|
+
const denied6 = new BlockList();
|
|
1608
|
+
for (const [ip, bits] of [["0.0.0.0", 8], ["10.0.0.0", 8], ["100.64.0.0", 10], ["127.0.0.0", 8], ["169.254.0.0", 16], ["172.16.0.0", 12], ["192.0.0.0", 24], ["192.0.2.0", 24], ["192.88.99.0", 24], ["192.168.0.0", 16], ["198.18.0.0", 15], ["198.51.100.0", 24], ["203.0.113.0", 24], ["224.0.0.0", 4], ["240.0.0.0", 4]]) denied4.addSubnet(ip, bits, "ipv4");
|
|
1609
|
+
for (const [ip, bits] of [["::", 128], ["::1", 128], ["::ffff:0:0", 96], ["64:ff9b::", 96], ["100::", 64], ["2001::", 23], ["2001:db8::", 32], ["2002::", 16], ["3fff::", 20], ["fc00::", 7], ["fe80::", 10], ["ff00::", 8]]) denied6.addSubnet(ip, bits, "ipv6");
|
|
1610
|
+
const publicV6 = new BlockList();
|
|
1611
|
+
publicV6.addSubnet("2000::", 3, "ipv6");
|
|
1612
|
+
return addresses.length > 0 && addresses.every(({ address, family }) => family === 6 ? !denied6.check(address, "ipv6") && publicV6.check(address, "ipv6") : family === 4 && !denied4.check(address, "ipv4"));
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1155
1615
|
// src/server/handler.ts
|
|
1616
|
+
var IdentityStoreError = class extends Error {
|
|
1617
|
+
};
|
|
1156
1618
|
var ID_RE = /^[A-Za-z0-9_-]{6,64}$/;
|
|
1157
1619
|
var AGENT_RE = /^[A-Za-z0-9_.:-]{1,64}$/;
|
|
1158
1620
|
var IDEMPOTENCY_KEY_RE = /^[\x21-\x7E]{1,255}$/;
|
|
1159
1621
|
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1160
1622
|
var DEFAULT_IDENTITY_TTL_MS = 36e5;
|
|
1161
1623
|
var MAX_IDENTITY_TTL_MS = 432e5;
|
|
1162
|
-
var MAX_CACHED_SESSIONS = 1e3;
|
|
1163
1624
|
var ANON_REUSE_MS = 864e5;
|
|
1164
1625
|
var VERIFIED_REUSE_MIN_MS = 12e4;
|
|
1165
1626
|
function checkIdentityOptions(o, staticAgents, transport) {
|
|
@@ -1205,11 +1666,35 @@ function createDeweeHandler(options) {
|
|
|
1205
1666
|
...options
|
|
1206
1667
|
};
|
|
1207
1668
|
const transport = resolveTransport(o);
|
|
1669
|
+
if (o.imageSources !== void 0 && !Array.isArray(o.imageSources)) throw new Error("[dewee-webchat] imageSources must be an array");
|
|
1670
|
+
const imageSources = (o.imageSources ?? []).map((origin) => {
|
|
1671
|
+
let url;
|
|
1672
|
+
try {
|
|
1673
|
+
url = new URL(origin);
|
|
1674
|
+
} catch {
|
|
1675
|
+
throw new Error("[dewee-webchat] Invalid image origin");
|
|
1676
|
+
}
|
|
1677
|
+
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");
|
|
1678
|
+
return url;
|
|
1679
|
+
});
|
|
1680
|
+
const publicAssets = o.cloudflarePublicImageAssets;
|
|
1681
|
+
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)))) {
|
|
1682
|
+
throw new Error("[dewee-webchat] Public image asset prefixes must be explicit absolute directory paths");
|
|
1683
|
+
}
|
|
1684
|
+
const assetPrefixes = publicAssets?.prefixes ?? [];
|
|
1685
|
+
let activeImages = 0;
|
|
1686
|
+
const imageNamespace = `${o.ccp?.chatUrl ?? o.gatewayUrl ?? ""}
|
|
1687
|
+
${o.ccp?.integrationId ?? o.tenantId ?? ""}`;
|
|
1688
|
+
if (o.allowedHeaders !== void 0 && (!Array.isArray(o.allowedHeaders) || o.allowedHeaders.some((name) => typeof name !== "string" || !/^[A-Za-z0-9-]+$/.test(name)))) {
|
|
1689
|
+
throw new Error("[dewee-webchat] allowedHeaders must contain valid header names (letters, digits and hyphens)");
|
|
1690
|
+
}
|
|
1208
1691
|
let secret = o.secret;
|
|
1209
1692
|
if (!secret) {
|
|
1693
|
+
if (o.allowEphemeralSecret !== true) throw new Error("[dewee-webchat] Configure secret (DEWEE_WEBCHAT_SECRET), or explicitly enable allowEphemeralSecret for development");
|
|
1210
1694
|
secret = randomId(32);
|
|
1211
1695
|
console.warn("[dewee-webchat] No secret configured: using an ephemeral one. Visitor sessions reset on restart.");
|
|
1212
1696
|
}
|
|
1697
|
+
if (new TextEncoder().encode(secret).length < 32) throw new Error("[dewee-webchat] secret (DEWEE_WEBCHAT_SECRET) must be at least 32 bytes");
|
|
1213
1698
|
const limiter = o.rateLimit === false ? null : o.rateLimit ?? memoryRateLimiter({ chat: [20, 6e4], session: [10, 6e5], preauth: [120, 6e4], verified: [30, 6e5], link: [5, 6e5] });
|
|
1214
1699
|
const staticAgents = o.agents === "*" ? [] : (o.agents ?? []).map((a) => typeof a === "string" ? { id: a } : a);
|
|
1215
1700
|
const wildcardAgents = o.agents === "*";
|
|
@@ -1280,7 +1765,7 @@ function createDeweeHandler(options) {
|
|
|
1280
1765
|
return {
|
|
1281
1766
|
"Access-Control-Allow-Origin": origin,
|
|
1282
1767
|
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
1283
|
-
"Access-Control-Allow-Headers":
|
|
1768
|
+
"Access-Control-Allow-Headers": [.../* @__PURE__ */ new Set(["content-type", "authorization", HEADER_GATEWAY_URL, HEADER_GATEWAY_TOKEN, ...(o.allowedHeaders ?? []).map((h) => h.toLowerCase())])].join(", "),
|
|
1284
1769
|
"Access-Control-Max-Age": "600",
|
|
1285
1770
|
Vary: "Origin"
|
|
1286
1771
|
};
|
|
@@ -1300,27 +1785,27 @@ function createDeweeHandler(options) {
|
|
|
1300
1785
|
return json(req, { error: { message: e.message, code, remediation: remediation ?? { action: e.action, audience: "end_user" } } }, e.status);
|
|
1301
1786
|
};
|
|
1302
1787
|
const misconfigured = (req) => fail(req, 500, "Signed-in chat is misconfigured on this server", "identity_misconfigured");
|
|
1303
|
-
function ccpFail(req, e, route, sessionToken) {
|
|
1788
|
+
async function ccpFail(req, e, route, sessionToken) {
|
|
1304
1789
|
const b = browserErrorFor(e, route);
|
|
1305
1790
|
if (b.admission) {
|
|
1306
1791
|
const r = admissionErrorResponse(b.admission, b.retryAfterMs);
|
|
1307
1792
|
return json(req, r.body, r.status, r.headers);
|
|
1308
1793
|
}
|
|
1309
1794
|
if (b.identity) {
|
|
1310
|
-
if (sessionToken && DEAD_SESSION_CODES.has(b.identity)) forgetCachedSession(sessionToken);
|
|
1795
|
+
if (sessionToken && DEAD_SESSION_CODES.has(b.identity)) await forgetCachedSession(sessionToken);
|
|
1311
1796
|
return identityFail(req, b.identity, b.remediation);
|
|
1312
1797
|
}
|
|
1313
1798
|
const res = fail(req, b.status, b.message, b.code);
|
|
1314
1799
|
if (b.retryAfterSeconds) res.headers.set("retry-after", String(Math.ceil(b.retryAfterSeconds)));
|
|
1315
1800
|
return res;
|
|
1316
1801
|
}
|
|
1317
|
-
const gatewayFail = (req, e, sessionToken) => {
|
|
1802
|
+
const gatewayFail = async (req, e, sessionToken) => {
|
|
1318
1803
|
if (e instanceof GatewayError && isAdmissionErrorCode(e.code)) {
|
|
1319
1804
|
const r = admissionErrorResponse(e.code, e.retryAfterMs);
|
|
1320
1805
|
return json(req, r.body, r.status, r.headers);
|
|
1321
1806
|
}
|
|
1322
1807
|
if (!(e instanceof GatewayError && isIdentityErrorCode(e.code))) return fail(req, 502, e.message, "gateway_error");
|
|
1323
|
-
if (sessionToken && DEAD_SESSION_CODES.has(e.code)) forgetCachedSession(sessionToken);
|
|
1808
|
+
if (sessionToken && DEAD_SESSION_CODES.has(e.code)) await forgetCachedSession(sessionToken);
|
|
1324
1809
|
if (e.remediation?.audience === "operator") console.error(`[dewee-webchat] identity: ${e.message} (${e.code})`);
|
|
1325
1810
|
return identityFail(req, e.code, e.remediation);
|
|
1326
1811
|
};
|
|
@@ -1354,7 +1839,7 @@ function createDeweeHandler(options) {
|
|
|
1354
1839
|
}
|
|
1355
1840
|
return ccpFail(req, e, "identity/exchange");
|
|
1356
1841
|
};
|
|
1357
|
-
const linkFail = (req, e, sessionToken) => {
|
|
1842
|
+
const linkFail = async (req, e, sessionToken) => {
|
|
1358
1843
|
const code = e?.code;
|
|
1359
1844
|
if (code === "IDEMPOTENCY_KEY_REUSED") return fail(req, 409, "This idempotency key was already used for a different link request", "idempotency_key_reused");
|
|
1360
1845
|
if (code === "FAILED_PRECONDITION") return fail(req, 409, "A link is already in progress. Use its code or wait until it expires.", "link_in_progress");
|
|
@@ -1362,7 +1847,7 @@ function createDeweeHandler(options) {
|
|
|
1362
1847
|
console.error("[dewee-webchat] identity link start: the gateway has identity exchange switched off");
|
|
1363
1848
|
return fail(req, 503, "Linking is not available right now", "linking_unavailable");
|
|
1364
1849
|
}
|
|
1365
|
-
if (typeof code === "string" && DEAD_SESSION_CODES.has(code)) forgetCachedSession(sessionToken);
|
|
1850
|
+
if (typeof code === "string" && DEAD_SESSION_CODES.has(code)) await forgetCachedSession(sessionToken);
|
|
1366
1851
|
return sdkFail(req, e, "link start");
|
|
1367
1852
|
};
|
|
1368
1853
|
const clientIp = (req) => req.headers.get("cf-connecting-ip") || req.headers.get("x-real-ip") || req.headers.get("x-forwarded-for")?.split(",")[0].trim() || "unknown";
|
|
@@ -1395,7 +1880,7 @@ function createDeweeHandler(options) {
|
|
|
1395
1880
|
const current = !!user && typeof user.subject === "string" && user.subject !== "" ? await subjectDigest(secret, user.subject) : null;
|
|
1396
1881
|
if (current && claims.sub && constantTimeEqual(current, claims.sub)) return null;
|
|
1397
1882
|
if (ccp && current && claims.sub) {
|
|
1398
|
-
forgetCachedSession(claims.ids);
|
|
1883
|
+
await forgetCachedSession(claims.ids);
|
|
1399
1884
|
await ccp.revoke(visitorContext(req), claims.v, sealedEdge(claims), claims.ids, claims.ida);
|
|
1400
1885
|
}
|
|
1401
1886
|
return identityFail(req, "IDENTITY_REQUIRED");
|
|
@@ -1409,22 +1894,20 @@ function createDeweeHandler(options) {
|
|
|
1409
1894
|
if (idn.assertionSecret) return { applicationId: idn.appId, issuer: idn.issuer, secret: idn.assertionSecret };
|
|
1410
1895
|
return { applicationId: idn.appId, issuer: idn.issuer, privateKeyJwk: idn.privateKeyJwk, algorithm: idn.algorithm };
|
|
1411
1896
|
}
|
|
1412
|
-
const
|
|
1897
|
+
const sessionStore = idn?.sessionStore ?? memoryIdentitySessionStore();
|
|
1413
1898
|
const exchanges = /* @__PURE__ */ new Map();
|
|
1414
|
-
function
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
sessionCache.set(key, c);
|
|
1420
|
-
if (sessionCache.size <= MAX_CACHED_SESSIONS) return;
|
|
1421
|
-
const now = Date.now();
|
|
1422
|
-
for (const [k, v] of sessionCache) if (v.deadline - now <= v.margin) sessionCache.delete(k);
|
|
1423
|
-
for (const k of sessionCache.keys()) {
|
|
1424
|
-
if (sessionCache.size <= MAX_CACHED_SESSIONS) break;
|
|
1425
|
-
sessionCache.delete(k);
|
|
1899
|
+
async function storeOperation(operation) {
|
|
1900
|
+
try {
|
|
1901
|
+
return await operation();
|
|
1902
|
+
} catch {
|
|
1903
|
+
throw new IdentityStoreError("Identity session store unavailable");
|
|
1426
1904
|
}
|
|
1427
1905
|
}
|
|
1906
|
+
async function forgetCachedSession(sessionToken) {
|
|
1907
|
+
await storeOperation(async () => sessionStore.deleteToken(await storeNamespace(), sessionToken));
|
|
1908
|
+
}
|
|
1909
|
+
const storeNamespace = () => subjectDigest(secret, JSON.stringify([o.ccp?.chatUrl ?? o.gatewayUrl ?? "", o.ccp?.integrationId ?? o.tenantId ?? "", idn.appId, idn.issuer]));
|
|
1910
|
+
const storeSubject = async (subject) => `${await storeNamespace()}:${await subjectDigest(secret, subject)}`;
|
|
1428
1911
|
async function exchangeFor(subject, req, sealed) {
|
|
1429
1912
|
if (ccp) return ccpExchangeFor(subject, req, sealed);
|
|
1430
1913
|
const session = await idn.sdk.exchangeSession({
|
|
@@ -1477,28 +1960,44 @@ function createDeweeHandler(options) {
|
|
|
1477
1960
|
} catch (e) {
|
|
1478
1961
|
return ccpFail(req, e, "config");
|
|
1479
1962
|
}
|
|
1480
|
-
const
|
|
1481
|
-
|
|
1482
|
-
|
|
1963
|
+
const subjectKey = await storeSubject(subject);
|
|
1964
|
+
const key = JSON.stringify([subjectKey, agent]);
|
|
1965
|
+
const observed = await storeOperation(() => sessionStore.read(subjectKey, agent));
|
|
1966
|
+
let s = observed.session;
|
|
1483
1967
|
const userVisitor = ccp ? await deriveVisitorId(secret, `subject:${subject}`) : void 0;
|
|
1484
1968
|
const sealed = hint && hint.ids && hint.v === userVisitor ? sealedEdge(hint) : void 0;
|
|
1485
1969
|
if (!s || s.deadline - Date.now() <= s.margin) {
|
|
1486
|
-
|
|
1487
|
-
if (
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1970
|
+
let pending = exchanges.get(key);
|
|
1971
|
+
if (!pending) {
|
|
1972
|
+
pending = (async () => {
|
|
1973
|
+
const reused = await verifiedReuse(req, hint, subject, agent, userVisitor);
|
|
1974
|
+
const candidate = reused ?? await exchangeFor(subject, req, sealed);
|
|
1975
|
+
const revokeMinted = async () => {
|
|
1976
|
+
if (ccp && !reused) await ccp.revoke(visitorContext(req), candidate.visitorId, sealed, candidate.sessionToken, agent).catch(() => {
|
|
1977
|
+
});
|
|
1978
|
+
};
|
|
1979
|
+
try {
|
|
1980
|
+
const committed = await storeOperation(() => sessionStore.commit(subjectKey, agent, observed.revision, candidate));
|
|
1981
|
+
if (!committed) {
|
|
1982
|
+
const current = await storeOperation(() => sessionStore.read(subjectKey, agent));
|
|
1983
|
+
if (current.session?.sessionToken !== candidate.sessionToken) throw new IdentityStoreError("Identity session changed");
|
|
1984
|
+
}
|
|
1985
|
+
return candidate;
|
|
1986
|
+
} catch (error) {
|
|
1987
|
+
await revokeMinted();
|
|
1988
|
+
throw error;
|
|
1989
|
+
}
|
|
1990
|
+
})().finally(() => exchanges.delete(key));
|
|
1991
|
+
exchanges.set(key, pending);
|
|
1500
1992
|
}
|
|
1501
|
-
|
|
1993
|
+
try {
|
|
1994
|
+
s = await pending;
|
|
1995
|
+
} catch (e) {
|
|
1996
|
+
if (e instanceof IdentityStoreError) throw e;
|
|
1997
|
+
return ccp ? ccpExchangeFail(req, e) : exchangeFail(req, e);
|
|
1998
|
+
}
|
|
1999
|
+
} else if (!await storeOperation(() => sessionStore.check(subjectKey, observed.revision))) {
|
|
2000
|
+
return identityFail(req, "SOURCE_UNAVAILABLE", { action: "retry_later", audience: "end_user" });
|
|
1502
2001
|
}
|
|
1503
2002
|
const now = Date.now();
|
|
1504
2003
|
const expiresAt = Math.min(s.deadline, now + o.sessionTtlMs);
|
|
@@ -1546,6 +2045,15 @@ ${subject}`;
|
|
|
1546
2045
|
maxMessageLength: o.maxMessageLength,
|
|
1547
2046
|
version: VERSION
|
|
1548
2047
|
};
|
|
2048
|
+
if (creds && new URL(req.url).searchParams.get("pageContext") === "1") {
|
|
2049
|
+
if (limiter && !await limiter(clientIp(req), "preauth")) return fail(req, 429, "Too many requests, slow down", "rate_limited");
|
|
2050
|
+
try {
|
|
2051
|
+
const probe = await GatewayConnection.connect(creds, o.userIdPrefix + "capability-probe");
|
|
2052
|
+
if (probe.pageContextVersions.length) body.pageContextVersions = probe.pageContextVersions;
|
|
2053
|
+
probe.close();
|
|
2054
|
+
} catch {
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
1549
2057
|
if (idn) {
|
|
1550
2058
|
body.defaultAgent = identityAgent;
|
|
1551
2059
|
body.allowAgentSwitch = false;
|
|
@@ -1570,6 +2078,7 @@ ${subject}`;
|
|
|
1570
2078
|
turnstileSiteKey: o.turnstile?.siteKey,
|
|
1571
2079
|
...o.turnstile ? { refreshWithoutTurnstile: true } : {},
|
|
1572
2080
|
maxMessageLength: Math.min(o.maxMessageLength, cfg.maxMessageChars),
|
|
2081
|
+
...cfg.pageContextVersions.length ? { pageContextVersions: cfg.pageContextVersions } : {},
|
|
1573
2082
|
version: VERSION
|
|
1574
2083
|
};
|
|
1575
2084
|
if (idn) {
|
|
@@ -1605,7 +2114,7 @@ ${subject}`;
|
|
|
1605
2114
|
if (!(ccp && previous) && !await turnstilePassed(body, ip)) return fail(req, 403, "Verification failed", "turnstile_failed");
|
|
1606
2115
|
const visitorId = previous?.v ?? randomId(16);
|
|
1607
2116
|
const expiresAt = Date.now() + o.sessionTtlMs;
|
|
1608
|
-
if (!ccp) return json(req, { visitorId, token: await signVisitorToken(secret, { v: visitorId, exp: expiresAt }), expiresAt });
|
|
2117
|
+
if (!ccp) return json(req, { visitorId, token: await signVisitorToken(secret, { v: visitorId, exp: expiresAt }), expiresAt, expiresIn: Math.max(0, expiresAt - Date.now()) });
|
|
1609
2118
|
let edge;
|
|
1610
2119
|
try {
|
|
1611
2120
|
edge = await ccp.edgeFor(visitorContext(req), visitorId, sealedEdge(previous), void 0, EDGE_RESEAL_MS);
|
|
@@ -1642,7 +2151,7 @@ ${subject}`;
|
|
|
1642
2151
|
const previous = await anonymousPrevious(body);
|
|
1643
2152
|
if (previous && previous.exp - Date.now() > ANON_REUSE_MS) {
|
|
1644
2153
|
if (!ccp) {
|
|
1645
|
-
const reused = { visitorId: previous.v, token: body.token, expiresAt: previous.exp };
|
|
2154
|
+
const reused = { visitorId: previous.v, token: body.token, expiresAt: previous.exp, expiresIn: Math.max(0, previous.exp - Date.now()) };
|
|
1646
2155
|
return json(req, reused);
|
|
1647
2156
|
}
|
|
1648
2157
|
const edge = sealedEdge(previous);
|
|
@@ -1666,7 +2175,10 @@ ${subject}`;
|
|
|
1666
2175
|
const { creds, byo: isByo, error } = resolveCreds(req);
|
|
1667
2176
|
if (error) return fail(req, 400, error, "bad_credentials");
|
|
1668
2177
|
if (!creds && !ccp) return fail(req, 500, "Gateway is not configured", "not_configured");
|
|
1669
|
-
const
|
|
2178
|
+
const input = await readChatBody(req);
|
|
2179
|
+
if ("error" in input) return fail(req, input.error === "too_large" ? 413 : 400, input.error === "too_large" ? "Request is too large" : "Invalid request", input.error === "too_large" ? "too_long" : "bad_request");
|
|
2180
|
+
if (!input.body || typeof input.body !== "object" || Array.isArray(input.body)) return fail(req, 400, "Invalid request", "bad_request");
|
|
2181
|
+
const body = input.body;
|
|
1670
2182
|
const message = typeof body?.message === "string" ? body.message.trim() : "";
|
|
1671
2183
|
if (!message) return fail(req, 400, "Message is required");
|
|
1672
2184
|
if (message.length > o.maxMessageLength) return fail(req, 413, `Message exceeds ${o.maxMessageLength} characters`, "too_long");
|
|
@@ -1686,15 +2198,23 @@ ${subject}`;
|
|
|
1686
2198
|
if (e instanceof ActionInputError) return fail(req, e.status, e.message, e.code);
|
|
1687
2199
|
throw e;
|
|
1688
2200
|
}
|
|
1689
|
-
if (
|
|
2201
|
+
if (body?.page_context != null) {
|
|
2202
|
+
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
|
+
if (!validPageContext(body.page_context)) return fail(req, 400, "Invalid page context", "page_context_invalid");
|
|
2204
|
+
}
|
|
2205
|
+
if (ccp) return ccpChat(req, claims, { message: pageBlock + message, rawMessage: message, conversationId, agent, actionsHash, pageContext: body?.page_context });
|
|
1690
2206
|
if (!creds) return fail(req, 500, "Gateway is not configured", "not_configured");
|
|
1691
2207
|
let connection;
|
|
1692
|
-
if (claims.ids) {
|
|
2208
|
+
if (claims.ids || body?.page_context != null) {
|
|
1693
2209
|
try {
|
|
1694
|
-
connection = await GatewayConnection.connect(creds, { identitySession: claims.ids });
|
|
2210
|
+
connection = await GatewayConnection.connect(creds, claims.ids ? { identitySession: claims.ids } : o.userIdPrefix + visitorId);
|
|
1695
2211
|
} catch (e) {
|
|
1696
2212
|
return gatewayFail(req, e, claims.ids);
|
|
1697
2213
|
}
|
|
2214
|
+
if (body?.page_context != null && !connection.pageContextVersions.includes(1)) {
|
|
2215
|
+
connection.close();
|
|
2216
|
+
return fail(req, 400, "Page context is not supported", "page_context_unsupported");
|
|
2217
|
+
}
|
|
1698
2218
|
}
|
|
1699
2219
|
const sessionKey = buildSessionKey(agent, `webchat-${visitorId}-${conversationId}`);
|
|
1700
2220
|
const abort = new AbortController();
|
|
@@ -1712,12 +2232,12 @@ ${subject}`;
|
|
|
1712
2232
|
try {
|
|
1713
2233
|
await send(actionsHash ? { type: "start", conversationId, agent, actionsHash } : { type: "start", conversationId, agent });
|
|
1714
2234
|
const who = connection ? { connection } : { userId: o.userIdPrefix + visitorId };
|
|
1715
|
-
for await (const ev of streamChat({ creds, ...who, agent, sessionKey, message: pageBlock + message, signal: abort.signal, timeoutMs: o.timeoutMs })) {
|
|
2235
|
+
for await (const ev of streamChat({ creds, ...who, agent, sessionKey, message: pageBlock + message, pageContext: body?.page_context ?? void 0, signal: abort.signal, timeoutMs: o.timeoutMs })) {
|
|
1716
2236
|
if (ev.type === "start") continue;
|
|
1717
2237
|
if (ev.type === "done") reply = ev.content;
|
|
1718
2238
|
if (ev.type === "error") {
|
|
1719
2239
|
errMsg = gatewayReasonOf(ev) ?? ev.message;
|
|
1720
|
-
if (claims.ids && ev.code && DEAD_SESSION_CODES.has(ev.code)) forgetCachedSession(claims.ids);
|
|
2240
|
+
if (claims.ids && ev.code && DEAD_SESSION_CODES.has(ev.code)) await forgetCachedSession(claims.ids);
|
|
1721
2241
|
}
|
|
1722
2242
|
await send(ev);
|
|
1723
2243
|
if (abort.signal.aborted) break;
|
|
@@ -1773,6 +2293,20 @@ ${subject}`;
|
|
|
1773
2293
|
let upstream;
|
|
1774
2294
|
try {
|
|
1775
2295
|
cfg = await ccp.config();
|
|
2296
|
+
if (turn.pageContext != null) {
|
|
2297
|
+
if (!cfg.pageContextVersions.includes(1)) {
|
|
2298
|
+
cleanup();
|
|
2299
|
+
return fail(req, 400, "Page context is not supported", "page_context_unsupported");
|
|
2300
|
+
}
|
|
2301
|
+
if (typeof turn.pageContext === "object" && "version" in turn.pageContext && Number.isInteger(turn.pageContext.version) && turn.pageContext.version !== 1) {
|
|
2302
|
+
cleanup();
|
|
2303
|
+
return fail(req, 400, "Page context is not supported", "page_context_unsupported");
|
|
2304
|
+
}
|
|
2305
|
+
if (!validPageContext(turn.pageContext)) {
|
|
2306
|
+
cleanup();
|
|
2307
|
+
return fail(req, 400, "Invalid page context", "page_context_invalid");
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
1776
2310
|
if (turn.rawMessage.length > cfg.maxMessageChars) {
|
|
1777
2311
|
cleanup();
|
|
1778
2312
|
return fail(req, 413, `Message exceeds ${cfg.maxMessageChars} characters`, "too_long");
|
|
@@ -1782,6 +2316,7 @@ ${subject}`;
|
|
|
1782
2316
|
conversationId: turn.conversationId,
|
|
1783
2317
|
agent: turn.agent || void 0,
|
|
1784
2318
|
identitySession: claims.ids,
|
|
2319
|
+
pageContext: turn.pageContext ?? void 0,
|
|
1785
2320
|
signal: abort.signal
|
|
1786
2321
|
});
|
|
1787
2322
|
} catch (e) {
|
|
@@ -1828,7 +2363,7 @@ ${subject}`;
|
|
|
1828
2363
|
if (ev.type === "error") {
|
|
1829
2364
|
errMsg = ev.message;
|
|
1830
2365
|
finished = true;
|
|
1831
|
-
if (claims.ids && ev.code && DEAD_SESSION_CODES.has(ev.code)) forgetCachedSession(claims.ids);
|
|
2366
|
+
if (claims.ids && ev.code && DEAD_SESSION_CODES.has(ev.code)) await forgetCachedSession(claims.ids);
|
|
1832
2367
|
}
|
|
1833
2368
|
await send(ev);
|
|
1834
2369
|
if (abort.signal.aborted) break;
|
|
@@ -1969,10 +2504,12 @@ ${subject}`;
|
|
|
1969
2504
|
async function revoke(subject, request, token) {
|
|
1970
2505
|
if (!idn || typeof subject !== "string" || subject === "") return;
|
|
1971
2506
|
try {
|
|
1972
|
-
const
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
2507
|
+
const subjectKey = await storeSubject(subject);
|
|
2508
|
+
let held = [];
|
|
2509
|
+
try {
|
|
2510
|
+
held = await storeOperation(() => sessionStore.takeSubject(subjectKey));
|
|
2511
|
+
} catch {
|
|
2512
|
+
console.warn("[dewee-webchat] identity session store unavailable during revoke");
|
|
1976
2513
|
}
|
|
1977
2514
|
if (ccp && typeof token === "string" && token !== "") {
|
|
1978
2515
|
const c = await verifyReuseHint(secret, token) ?? await verifyVisitorToken(secret, token);
|
|
@@ -1980,7 +2517,6 @@ ${subject}`;
|
|
|
1980
2517
|
held.push({ agent: c.ida, session: { sessionToken: c.ids, visitorId: c.v, deadline: c.exp, margin: 0 }, edge: sealedEdge(c) });
|
|
1981
2518
|
}
|
|
1982
2519
|
}
|
|
1983
|
-
for (const { session } of held) forgetCachedSession(session.sessionToken);
|
|
1984
2520
|
if (!ccp) return;
|
|
1985
2521
|
const fromRequest = request ? visitorContext(request) : null;
|
|
1986
2522
|
await Promise.all(
|
|
@@ -1999,6 +2535,54 @@ ${subject}`;
|
|
|
1999
2535
|
if (!originAllowed(req)) return fail(req, 403, "Origin not allowed", "forbidden_origin");
|
|
2000
2536
|
const action = new URL(req.url).pathname.replace(/\/+$/, "").split("/").pop();
|
|
2001
2537
|
try {
|
|
2538
|
+
if (req.method === "POST" && action === "image-ticket" && imageSources.length) {
|
|
2539
|
+
const claims = await visitorFrom(req);
|
|
2540
|
+
if (!claims) return fail(req, 401, "Missing or expired visitor session", "unauthorized");
|
|
2541
|
+
if (idn?.required && !claims.ids) return identityFail(req, "IDENTITY_REQUIRED");
|
|
2542
|
+
const refused = await rebindRefusal(req, claims);
|
|
2543
|
+
if (refused) return refused;
|
|
2544
|
+
if (limiter && !await limiter(`image-ticket:${claims.v}`, "preauth")) return fail(req, 429, "Too many requests, try again later", "rate_limited");
|
|
2545
|
+
const body = await readImageTicketBody(req);
|
|
2546
|
+
if (body === null) return fail(req, 413, "Invalid image", "invalid_request");
|
|
2547
|
+
let parsed;
|
|
2548
|
+
try {
|
|
2549
|
+
parsed = JSON.parse(body);
|
|
2550
|
+
} catch {
|
|
2551
|
+
return fail(req, 400, "Invalid image", "invalid_request");
|
|
2552
|
+
}
|
|
2553
|
+
const raw = parsed?.url;
|
|
2554
|
+
const worker = globalThis.navigator?.userAgent === "Cloudflare-Workers";
|
|
2555
|
+
const source = typeof raw === "string" ? allowedImageSource(raw, imageSources, new URL(req.url), worker, worker ? assetPrefixes : []) : null;
|
|
2556
|
+
if (!source) return fail(req, 403, "Image not allowed", "forbidden_image");
|
|
2557
|
+
const endpoint = new URL(req.url);
|
|
2558
|
+
const asset = publicAssets && source.origin === endpoint.origin && assetPrefixes.some((prefix) => source.pathname.startsWith(prefix));
|
|
2559
|
+
const namespace = asset ? `${imageNamespace}
|
|
2560
|
+
assets:${endpoint.origin}` : imageNamespace;
|
|
2561
|
+
return json(req, { ticket: await issueImageTicket(secret, namespace, claims.v, source.href) });
|
|
2562
|
+
}
|
|
2563
|
+
if (req.method === "GET" && action === "image" && imageSources.length) {
|
|
2564
|
+
const ticket = new URL(req.url).searchParams.get("ticket") ?? "";
|
|
2565
|
+
const endpoint = new URL(req.url);
|
|
2566
|
+
const assetClaims = await verifyImageTicket(secret, `${imageNamespace}
|
|
2567
|
+
assets:${endpoint.origin}`, ticket);
|
|
2568
|
+
const claims = assetClaims ?? await verifyImageTicket(secret, imageNamespace, ticket);
|
|
2569
|
+
if (!claims) return fail(req, 401, "Image ticket expired", "unauthorized");
|
|
2570
|
+
const worker = globalThis.navigator?.userAgent === "Cloudflare-Workers";
|
|
2571
|
+
const source = allowedImageSource(claims.source, imageSources, endpoint, worker, worker ? assetPrefixes : []);
|
|
2572
|
+
if (!source) return fail(req, 403, "Image not allowed", "forbidden_image");
|
|
2573
|
+
const asset = publicAssets && source.origin === endpoint.origin && assetPrefixes.some((prefix) => source.pathname.startsWith(prefix));
|
|
2574
|
+
if (Boolean(asset) !== Boolean(assetClaims)) return fail(req, 403, "Image not allowed", "forbidden_image");
|
|
2575
|
+
if (limiter && !await limiter(`image:${claims.visitor}`, "preauth")) return fail(req, 429, "Too many requests, try again later", "rate_limited");
|
|
2576
|
+
if (activeImages >= 8) return fail(req, 429, "Too many requests, try again later", "rate_limited");
|
|
2577
|
+
activeImages++;
|
|
2578
|
+
try {
|
|
2579
|
+
const assetFetch = (input, init) => publicAssets.binding.fetch(new Request(new URL(new URL(String(input)).pathname, "https://assets.invalid"), init));
|
|
2580
|
+
const result = await fetchRasterImage(source.href, asset ? assetFetch : worker ? globalThis.fetch : publicImageFetchNode, worker ? "manual" : "error");
|
|
2581
|
+
return result ?? fail(req, 502, "Image unavailable", "image_unavailable");
|
|
2582
|
+
} finally {
|
|
2583
|
+
activeImages--;
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2002
2586
|
if (req.method === "GET" && action === "config") return await handleConfig(req);
|
|
2003
2587
|
if (req.method === "POST" && action === "session") return await handleSession(req);
|
|
2004
2588
|
if (req.method === "POST" && action === "chat") return await handleChat(req);
|
|
@@ -2007,6 +2591,7 @@ ${subject}`;
|
|
|
2007
2591
|
if (req.method === "POST" && action === "job-access" && idn) return await handleJobAccess(req);
|
|
2008
2592
|
return fail(req, 404, "Not found", "not_found");
|
|
2009
2593
|
} catch (e) {
|
|
2594
|
+
if (e instanceof IdentityStoreError) return identityFail(req, "SOURCE_UNAVAILABLE", { action: "retry_later", audience: "end_user" });
|
|
2010
2595
|
if (!ccp) return fail(req, 500, e.message || "Internal error", "internal");
|
|
2011
2596
|
console.error("[dewee-webchat] internal error:", e instanceof CcpError ? e.code : e);
|
|
2012
2597
|
return fail(req, 500, "Internal error", "internal");
|
|
@@ -2037,6 +2622,7 @@ function optionsFromEnv(env, overrides = {}) {
|
|
|
2037
2622
|
agents,
|
|
2038
2623
|
defaultAgent: env.DEWEE_DEFAULT_AGENT,
|
|
2039
2624
|
allowedOrigins: origins,
|
|
2625
|
+
imageSources: list(env.DEEWEE_IMAGE_SOURCES),
|
|
2040
2626
|
allowClientCredentials: env.DEWEE_ALLOW_CLIENT_CREDENTIALS === "true" ? byoHosts?.length ? { allowedHosts: byoHosts } : true : false,
|
|
2041
2627
|
turnstile: env.TURNSTILE_SITE_KEY && env.TURNSTILE_SECRET_KEY ? { siteKey: env.TURNSTILE_SITE_KEY, secretKey: env.TURNSTILE_SECRET_KEY } : void 0,
|
|
2042
2628
|
// undefined overrides must not erase env-derived values
|
|
@@ -2110,6 +2696,7 @@ export {
|
|
|
2110
2696
|
hashManifest,
|
|
2111
2697
|
isAdmissionErrorCode,
|
|
2112
2698
|
listAgents,
|
|
2699
|
+
memoryIdentitySessionStore,
|
|
2113
2700
|
memoryRateLimiter,
|
|
2114
2701
|
normalizeManifest,
|
|
2115
2702
|
optionsFromEnv,
|