dewee-webchat 0.4.1 → 0.5.0-beta.1
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 +103 -7
- package/astro/DeweeChat.astro +2 -2
- package/dist/{chunk-VU2M42GA.js → chunk-M6J3IXEI.js} +669 -73
- package/dist/{chunk-EVZ5K7BN.js → chunk-WHHQ6FKK.js} +1 -1
- package/dist/dewee-webchat.js +37 -30
- package/dist/{handler-DunrbnSD.d.ts → handler-B-c6tDr7.d.ts} +50 -5
- package/dist/index.d.ts +56 -7
- package/dist/index.js +4 -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 +4 -2
- 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 +631 -61
- package/dist/vue.d.ts +68 -3
- package/dist/vue.js +12 -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-WHHQ6FKK.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,401 @@ 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) {
|
|
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
|
+
if (source.hostname === suffix || source.hostname.endsWith(`.${suffix}`)) return null;
|
|
1341
|
+
}
|
|
1342
|
+
return prefixes.some((prefix) => prefix.origin === source.origin && source.pathname.startsWith(prefix.pathname)) ? source : null;
|
|
1343
|
+
} catch {
|
|
1344
|
+
return null;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
function safeRasterDimensions(bytes, type) {
|
|
1348
|
+
const ascii = (start, end) => String.fromCharCode(...bytes.slice(start, end));
|
|
1349
|
+
const u16 = (at, little = false) => little ? bytes[at] + bytes[at + 1] * 256 : bytes[at] * 256 + bytes[at + 1];
|
|
1350
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
1351
|
+
const u32 = (at, little = false) => view.getUint32(at, little);
|
|
1352
|
+
const sizeOK = (w, h) => w > 0 && h > 0 && w <= 4096 && h <= 4096 && w * h <= 16e6;
|
|
1353
|
+
let width = 0, height = 0;
|
|
1354
|
+
if (type === "image/png") {
|
|
1355
|
+
let pos = 8, header = false, data = false, end = false;
|
|
1356
|
+
while (pos + 12 <= bytes.length) {
|
|
1357
|
+
const length = u32(pos), name = ascii(pos + 4, pos + 8);
|
|
1358
|
+
if (length > bytes.length - pos - 12) return false;
|
|
1359
|
+
if (name === "IHDR") {
|
|
1360
|
+
if (header || pos !== 8 || length !== 13) return false;
|
|
1361
|
+
header = true;
|
|
1362
|
+
width = u32(pos + 8);
|
|
1363
|
+
height = u32(pos + 12);
|
|
1364
|
+
} else if (name === "acTL" || name === "fcTL" || name === "fdAT") return false;
|
|
1365
|
+
else if (name === "IDAT") data = true;
|
|
1366
|
+
else if (name === "IEND") {
|
|
1367
|
+
end = length === 0;
|
|
1368
|
+
break;
|
|
1369
|
+
}
|
|
1370
|
+
pos += length + 12;
|
|
1371
|
+
}
|
|
1372
|
+
if (!header || !data || !end) return false;
|
|
1373
|
+
} else if (type === "image/gif" && bytes.length >= 13) {
|
|
1374
|
+
width = u16(6, true);
|
|
1375
|
+
height = u16(8, true);
|
|
1376
|
+
let pos = 13 + (bytes[10] & 128 ? 3 * (1 << (bytes[10] & 7) + 1) : 0), frames = 0, end = false;
|
|
1377
|
+
const blocks = () => {
|
|
1378
|
+
while (pos < bytes.length) {
|
|
1379
|
+
const n = bytes[pos++];
|
|
1380
|
+
if (n === 0) return true;
|
|
1381
|
+
if (n > bytes.length - pos) return false;
|
|
1382
|
+
pos += n;
|
|
1383
|
+
}
|
|
1384
|
+
return false;
|
|
1385
|
+
};
|
|
1386
|
+
while (pos < bytes.length) {
|
|
1387
|
+
const marker = bytes[pos++];
|
|
1388
|
+
if (marker === 59) {
|
|
1389
|
+
end = true;
|
|
1390
|
+
break;
|
|
1391
|
+
}
|
|
1392
|
+
if (marker === 33) {
|
|
1393
|
+
if (pos >= bytes.length) return false;
|
|
1394
|
+
pos++;
|
|
1395
|
+
if (!blocks()) return false;
|
|
1396
|
+
continue;
|
|
1397
|
+
}
|
|
1398
|
+
if (marker !== 44 || pos + 9 > bytes.length || ++frames > 1) return false;
|
|
1399
|
+
const left = u16(pos, true), top = u16(pos + 2, true), w = u16(pos + 4, true), h = u16(pos + 6, true), flags = bytes[pos + 8];
|
|
1400
|
+
if (!sizeOK(w, h) || left + w > width || top + h > height) return false;
|
|
1401
|
+
pos += 9 + (flags & 128 ? 3 * (1 << (flags & 7) + 1) : 0);
|
|
1402
|
+
if (pos >= bytes.length) return false;
|
|
1403
|
+
pos++;
|
|
1404
|
+
if (!blocks()) return false;
|
|
1405
|
+
}
|
|
1406
|
+
if (frames !== 1 || !end) return false;
|
|
1407
|
+
} else if (type === "image/jpeg") {
|
|
1408
|
+
let pos = 2, frames = 0, end = false;
|
|
1409
|
+
while (pos + 1 < bytes.length && bytes[pos] === 255) {
|
|
1410
|
+
while (bytes[pos] === 255) pos++;
|
|
1411
|
+
const marker = bytes[pos++];
|
|
1412
|
+
if (marker === 217) {
|
|
1413
|
+
end = true;
|
|
1414
|
+
break;
|
|
1415
|
+
}
|
|
1416
|
+
if (marker === 1 || marker >= 208 && marker <= 215) continue;
|
|
1417
|
+
if (pos + 2 > bytes.length) return false;
|
|
1418
|
+
const length = u16(pos);
|
|
1419
|
+
if (length < 2 || pos + length > bytes.length) return false;
|
|
1420
|
+
if ([192, 193, 194, 195, 197, 198, 199, 201, 202, 203, 205, 206, 207].includes(marker)) {
|
|
1421
|
+
if (++frames !== 1 || length < 7) return false;
|
|
1422
|
+
height = u16(pos + 3);
|
|
1423
|
+
width = u16(pos + 5);
|
|
1424
|
+
}
|
|
1425
|
+
pos += length;
|
|
1426
|
+
if (marker === 218) {
|
|
1427
|
+
while (pos + 1 < bytes.length) {
|
|
1428
|
+
if (bytes[pos] !== 255) {
|
|
1429
|
+
pos++;
|
|
1430
|
+
continue;
|
|
1431
|
+
}
|
|
1432
|
+
const next = bytes[pos + 1];
|
|
1433
|
+
if (next === 0 || next >= 208 && next <= 215) {
|
|
1434
|
+
pos += 2;
|
|
1435
|
+
continue;
|
|
1436
|
+
}
|
|
1437
|
+
break;
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
if (frames !== 1 || !end) return false;
|
|
1442
|
+
} else if (type === "image/webp" && bytes.length >= 20) {
|
|
1443
|
+
const total = u32(4, true) + 8;
|
|
1444
|
+
if (total > bytes.length || total < 20) return false;
|
|
1445
|
+
let pos = 12, images = 0, canvasW = 0, canvasH = 0;
|
|
1446
|
+
while (pos + 8 <= total) {
|
|
1447
|
+
const chunk = ascii(pos, pos + 4), length = u32(pos + 4, true), start = pos + 8;
|
|
1448
|
+
if (length > total - start || start + length + (length & 1) > total || chunk === "ANIM" || chunk === "ANMF") return false;
|
|
1449
|
+
if (chunk === "VP8X") {
|
|
1450
|
+
if (length !== 10 || canvasW || bytes[start] & 2) return false;
|
|
1451
|
+
canvasW = 1 + bytes[start + 4] + (bytes[start + 5] << 8) + (bytes[start + 6] << 16);
|
|
1452
|
+
canvasH = 1 + bytes[start + 7] + (bytes[start + 8] << 8) + (bytes[start + 9] << 16);
|
|
1453
|
+
} else if (chunk === "VP8L" && length >= 5 && bytes[start] === 47) {
|
|
1454
|
+
images++;
|
|
1455
|
+
width = 1 + bytes[start + 1] + ((bytes[start + 2] & 63) << 8);
|
|
1456
|
+
height = 1 + (bytes[start + 2] >> 6) + (bytes[start + 3] << 2) + ((bytes[start + 4] & 15) << 10);
|
|
1457
|
+
} else if (chunk === "VP8 " && length >= 10 && bytes[start + 3] === 157 && bytes[start + 4] === 1 && bytes[start + 5] === 42) {
|
|
1458
|
+
images++;
|
|
1459
|
+
width = u16(start + 6, true) & 16383;
|
|
1460
|
+
height = u16(start + 8, true) & 16383;
|
|
1461
|
+
}
|
|
1462
|
+
pos = start + length + (length & 1);
|
|
1463
|
+
}
|
|
1464
|
+
if (pos !== total || images !== 1 || canvasW && (canvasW !== width || canvasH !== height)) return false;
|
|
1465
|
+
}
|
|
1466
|
+
return sizeOK(width, height);
|
|
1467
|
+
}
|
|
1468
|
+
async function fetchRasterImage(url, fetcher, redirect = "error") {
|
|
1469
|
+
const controller = new AbortController();
|
|
1470
|
+
const timer = setTimeout(() => controller.abort(), 1e4);
|
|
1471
|
+
try {
|
|
1472
|
+
const response = await fetcher(url, { method: "GET", redirect, credentials: "omit", signal: controller.signal, headers: { accept: "image/png,image/jpeg,image/gif,image/webp" } });
|
|
1473
|
+
if (!response.ok || !response.body) {
|
|
1474
|
+
await response.body?.cancel();
|
|
1475
|
+
return null;
|
|
1476
|
+
}
|
|
1477
|
+
const type = response.headers.get("content-type")?.split(";", 1)[0].trim().toLowerCase();
|
|
1478
|
+
if (!type || !["image/png", "image/jpeg", "image/gif", "image/webp"].includes(type)) {
|
|
1479
|
+
await response.body.cancel();
|
|
1480
|
+
return null;
|
|
1481
|
+
}
|
|
1482
|
+
const length = response.headers.get("content-length");
|
|
1483
|
+
const maxBytes = 2 * 1024 * 1024;
|
|
1484
|
+
if (length && (!/^\d+$/.test(length) || Number(length) > maxBytes)) {
|
|
1485
|
+
await response.body.cancel();
|
|
1486
|
+
return null;
|
|
1487
|
+
}
|
|
1488
|
+
const reader = response.body.getReader();
|
|
1489
|
+
const chunks = [];
|
|
1490
|
+
let size = 0;
|
|
1491
|
+
try {
|
|
1492
|
+
for (; ; ) {
|
|
1493
|
+
const { done, value } = await reader.read();
|
|
1494
|
+
if (done) break;
|
|
1495
|
+
size += value.byteLength;
|
|
1496
|
+
if (size > maxBytes) {
|
|
1497
|
+
await reader.cancel();
|
|
1498
|
+
return null;
|
|
1499
|
+
}
|
|
1500
|
+
chunks.push(value);
|
|
1501
|
+
}
|
|
1502
|
+
} finally {
|
|
1503
|
+
reader.releaseLock();
|
|
1504
|
+
}
|
|
1505
|
+
const bytes = new Uint8Array(size);
|
|
1506
|
+
let offset = 0;
|
|
1507
|
+
for (const chunk of chunks) {
|
|
1508
|
+
bytes.set(chunk, offset);
|
|
1509
|
+
offset += chunk.length;
|
|
1510
|
+
}
|
|
1511
|
+
const starts = (...signature) => signature.every((value, index) => bytes[index] === value);
|
|
1512
|
+
const ascii = (start, end) => String.fromCharCode(...bytes.slice(start, end));
|
|
1513
|
+
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";
|
|
1514
|
+
if (!valid || !safeRasterDimensions(bytes, type)) return null;
|
|
1515
|
+
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" } });
|
|
1516
|
+
} catch {
|
|
1517
|
+
return null;
|
|
1518
|
+
} finally {
|
|
1519
|
+
clearTimeout(timer);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// src/server/public-image-fetch-node.ts
|
|
1524
|
+
async function publicImageFetchNode(input, init) {
|
|
1525
|
+
if (typeof input !== "string") throw new Error("Invalid image URL");
|
|
1526
|
+
const url = new URL(input);
|
|
1527
|
+
if (url.protocol !== "https:" || url.username || url.password || url.port && url.port !== "443") throw new Error("Invalid image URL");
|
|
1528
|
+
const prefix = "node:";
|
|
1529
|
+
const dns = await import(prefix + "dns/promises");
|
|
1530
|
+
const net = await import(prefix + "net");
|
|
1531
|
+
const https = await import(prefix + "https");
|
|
1532
|
+
if (net.isIP(url.hostname)) throw new Error("IP literals are not allowed");
|
|
1533
|
+
const lookup = dns.lookup(url.hostname, { all: true, verbatim: true });
|
|
1534
|
+
const signal = init?.signal;
|
|
1535
|
+
const addresses = signal ? await new Promise((resolve, reject) => {
|
|
1536
|
+
if (signal.aborted) {
|
|
1537
|
+
reject(new Error("Image request timed out"));
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1540
|
+
const abort = () => reject(new Error("Image request timed out"));
|
|
1541
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
1542
|
+
lookup.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
|
|
1543
|
+
}) : await lookup;
|
|
1544
|
+
if (!addresses.length) throw new Error("No image address");
|
|
1545
|
+
if (!publicImageAddresses(addresses, net.BlockList)) throw new Error("Non-public image address");
|
|
1546
|
+
const chosen = addresses.find((entry) => entry.family === 4) ?? addresses[0];
|
|
1547
|
+
return new Promise((resolve, reject) => {
|
|
1548
|
+
const request = https.request(url, {
|
|
1549
|
+
method: "GET",
|
|
1550
|
+
headers: { accept: "image/png,image/jpeg,image/gif,image/webp" },
|
|
1551
|
+
signal: init?.signal ?? void 0,
|
|
1552
|
+
family: chosen.family,
|
|
1553
|
+
lookup: (_host, _options, callback) => callback(null, chosen.address, chosen.family)
|
|
1554
|
+
}, (response) => {
|
|
1555
|
+
if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 300 || response.statusCode === 204 || response.statusCode === 205) {
|
|
1556
|
+
response.destroy();
|
|
1557
|
+
reject(new Error("Image upstream refused"));
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
const headers = new Headers();
|
|
1561
|
+
for (const [name, value] of Object.entries(response.headers)) if (typeof value === "string") headers.set(name, value);
|
|
1562
|
+
let received = 0;
|
|
1563
|
+
let settled = false;
|
|
1564
|
+
const body = new ReadableStream({
|
|
1565
|
+
start(controller) {
|
|
1566
|
+
response.pause();
|
|
1567
|
+
response.on("data", (chunk) => {
|
|
1568
|
+
response.pause();
|
|
1569
|
+
received += chunk.length;
|
|
1570
|
+
if (received > 2 * 1024 * 1024) {
|
|
1571
|
+
settled = true;
|
|
1572
|
+
response.destroy();
|
|
1573
|
+
controller.error(new Error("Image too large"));
|
|
1574
|
+
} else controller.enqueue(new Uint8Array(chunk));
|
|
1575
|
+
});
|
|
1576
|
+
response.on("end", () => {
|
|
1577
|
+
if (!settled) {
|
|
1578
|
+
settled = true;
|
|
1579
|
+
controller.close();
|
|
1580
|
+
}
|
|
1581
|
+
});
|
|
1582
|
+
response.on("error", (error) => {
|
|
1583
|
+
if (!settled) {
|
|
1584
|
+
settled = true;
|
|
1585
|
+
controller.error(error);
|
|
1586
|
+
}
|
|
1587
|
+
});
|
|
1588
|
+
},
|
|
1589
|
+
pull() {
|
|
1590
|
+
response.resume();
|
|
1591
|
+
},
|
|
1592
|
+
cancel() {
|
|
1593
|
+
settled = true;
|
|
1594
|
+
response.destroy();
|
|
1595
|
+
}
|
|
1596
|
+
});
|
|
1597
|
+
resolve(new Response(body, { status: response.statusCode ?? 502, headers }));
|
|
1598
|
+
});
|
|
1599
|
+
request.on("error", reject);
|
|
1600
|
+
request.end();
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
function publicImageAddresses(addresses, BlockList) {
|
|
1604
|
+
const denied4 = new BlockList();
|
|
1605
|
+
const denied6 = new BlockList();
|
|
1606
|
+
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");
|
|
1607
|
+
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");
|
|
1608
|
+
const publicV6 = new BlockList();
|
|
1609
|
+
publicV6.addSubnet("2000::", 3, "ipv6");
|
|
1610
|
+
return addresses.length > 0 && addresses.every(({ address, family }) => family === 6 ? !denied6.check(address, "ipv6") && publicV6.check(address, "ipv6") : family === 4 && !denied4.check(address, "ipv4"));
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1155
1613
|
// src/server/handler.ts
|
|
1614
|
+
var IdentityStoreError = class extends Error {
|
|
1615
|
+
};
|
|
1156
1616
|
var ID_RE = /^[A-Za-z0-9_-]{6,64}$/;
|
|
1157
1617
|
var AGENT_RE = /^[A-Za-z0-9_.:-]{1,64}$/;
|
|
1158
1618
|
var IDEMPOTENCY_KEY_RE = /^[\x21-\x7E]{1,255}$/;
|
|
1159
1619
|
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
1620
|
var DEFAULT_IDENTITY_TTL_MS = 36e5;
|
|
1161
1621
|
var MAX_IDENTITY_TTL_MS = 432e5;
|
|
1162
|
-
var MAX_CACHED_SESSIONS = 1e3;
|
|
1163
1622
|
var ANON_REUSE_MS = 864e5;
|
|
1164
1623
|
var VERIFIED_REUSE_MIN_MS = 12e4;
|
|
1165
1624
|
function checkIdentityOptions(o, staticAgents, transport) {
|
|
@@ -1205,11 +1664,30 @@ function createDeweeHandler(options) {
|
|
|
1205
1664
|
...options
|
|
1206
1665
|
};
|
|
1207
1666
|
const transport = resolveTransport(o);
|
|
1667
|
+
if (o.imageSources !== void 0 && !Array.isArray(o.imageSources)) throw new Error("[dewee-webchat] imageSources must be an array");
|
|
1668
|
+
const imageSources = (o.imageSources ?? []).map((origin) => {
|
|
1669
|
+
let url;
|
|
1670
|
+
try {
|
|
1671
|
+
url = new URL(origin);
|
|
1672
|
+
} catch {
|
|
1673
|
+
throw new Error("[dewee-webchat] Invalid image origin");
|
|
1674
|
+
}
|
|
1675
|
+
if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash || url.port || !url.pathname.endsWith("/") || /%(?:2f|5c|25|2e|0[0-9a-f]|1[0-9a-f]|7f)/i.test(url.pathname)) throw new Error("[dewee-webchat] Image sources must be HTTPS directory prefixes on the default port");
|
|
1676
|
+
return url;
|
|
1677
|
+
});
|
|
1678
|
+
let activeImages = 0;
|
|
1679
|
+
const imageNamespace = `${o.ccp?.chatUrl ?? o.gatewayUrl ?? ""}
|
|
1680
|
+
${o.ccp?.integrationId ?? o.tenantId ?? ""}`;
|
|
1681
|
+
if (o.allowedHeaders !== void 0 && (!Array.isArray(o.allowedHeaders) || o.allowedHeaders.some((name) => typeof name !== "string" || !/^[A-Za-z0-9-]+$/.test(name)))) {
|
|
1682
|
+
throw new Error("[dewee-webchat] allowedHeaders must contain valid header names (letters, digits and hyphens)");
|
|
1683
|
+
}
|
|
1208
1684
|
let secret = o.secret;
|
|
1209
1685
|
if (!secret) {
|
|
1686
|
+
if (o.allowEphemeralSecret !== true) throw new Error("[dewee-webchat] Configure secret (DEWEE_WEBCHAT_SECRET), or explicitly enable allowEphemeralSecret for development");
|
|
1210
1687
|
secret = randomId(32);
|
|
1211
1688
|
console.warn("[dewee-webchat] No secret configured: using an ephemeral one. Visitor sessions reset on restart.");
|
|
1212
1689
|
}
|
|
1690
|
+
if (new TextEncoder().encode(secret).length < 32) throw new Error("[dewee-webchat] secret (DEWEE_WEBCHAT_SECRET) must be at least 32 bytes");
|
|
1213
1691
|
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
1692
|
const staticAgents = o.agents === "*" ? [] : (o.agents ?? []).map((a) => typeof a === "string" ? { id: a } : a);
|
|
1215
1693
|
const wildcardAgents = o.agents === "*";
|
|
@@ -1280,7 +1758,7 @@ function createDeweeHandler(options) {
|
|
|
1280
1758
|
return {
|
|
1281
1759
|
"Access-Control-Allow-Origin": origin,
|
|
1282
1760
|
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
1283
|
-
"Access-Control-Allow-Headers":
|
|
1761
|
+
"Access-Control-Allow-Headers": [.../* @__PURE__ */ new Set(["content-type", "authorization", HEADER_GATEWAY_URL, HEADER_GATEWAY_TOKEN, ...(o.allowedHeaders ?? []).map((h) => h.toLowerCase())])].join(", "),
|
|
1284
1762
|
"Access-Control-Max-Age": "600",
|
|
1285
1763
|
Vary: "Origin"
|
|
1286
1764
|
};
|
|
@@ -1300,27 +1778,27 @@ function createDeweeHandler(options) {
|
|
|
1300
1778
|
return json(req, { error: { message: e.message, code, remediation: remediation ?? { action: e.action, audience: "end_user" } } }, e.status);
|
|
1301
1779
|
};
|
|
1302
1780
|
const misconfigured = (req) => fail(req, 500, "Signed-in chat is misconfigured on this server", "identity_misconfigured");
|
|
1303
|
-
function ccpFail(req, e, route, sessionToken) {
|
|
1781
|
+
async function ccpFail(req, e, route, sessionToken) {
|
|
1304
1782
|
const b = browserErrorFor(e, route);
|
|
1305
1783
|
if (b.admission) {
|
|
1306
1784
|
const r = admissionErrorResponse(b.admission, b.retryAfterMs);
|
|
1307
1785
|
return json(req, r.body, r.status, r.headers);
|
|
1308
1786
|
}
|
|
1309
1787
|
if (b.identity) {
|
|
1310
|
-
if (sessionToken && DEAD_SESSION_CODES.has(b.identity)) forgetCachedSession(sessionToken);
|
|
1788
|
+
if (sessionToken && DEAD_SESSION_CODES.has(b.identity)) await forgetCachedSession(sessionToken);
|
|
1311
1789
|
return identityFail(req, b.identity, b.remediation);
|
|
1312
1790
|
}
|
|
1313
1791
|
const res = fail(req, b.status, b.message, b.code);
|
|
1314
1792
|
if (b.retryAfterSeconds) res.headers.set("retry-after", String(Math.ceil(b.retryAfterSeconds)));
|
|
1315
1793
|
return res;
|
|
1316
1794
|
}
|
|
1317
|
-
const gatewayFail = (req, e, sessionToken) => {
|
|
1795
|
+
const gatewayFail = async (req, e, sessionToken) => {
|
|
1318
1796
|
if (e instanceof GatewayError && isAdmissionErrorCode(e.code)) {
|
|
1319
1797
|
const r = admissionErrorResponse(e.code, e.retryAfterMs);
|
|
1320
1798
|
return json(req, r.body, r.status, r.headers);
|
|
1321
1799
|
}
|
|
1322
1800
|
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);
|
|
1801
|
+
if (sessionToken && DEAD_SESSION_CODES.has(e.code)) await forgetCachedSession(sessionToken);
|
|
1324
1802
|
if (e.remediation?.audience === "operator") console.error(`[dewee-webchat] identity: ${e.message} (${e.code})`);
|
|
1325
1803
|
return identityFail(req, e.code, e.remediation);
|
|
1326
1804
|
};
|
|
@@ -1354,7 +1832,7 @@ function createDeweeHandler(options) {
|
|
|
1354
1832
|
}
|
|
1355
1833
|
return ccpFail(req, e, "identity/exchange");
|
|
1356
1834
|
};
|
|
1357
|
-
const linkFail = (req, e, sessionToken) => {
|
|
1835
|
+
const linkFail = async (req, e, sessionToken) => {
|
|
1358
1836
|
const code = e?.code;
|
|
1359
1837
|
if (code === "IDEMPOTENCY_KEY_REUSED") return fail(req, 409, "This idempotency key was already used for a different link request", "idempotency_key_reused");
|
|
1360
1838
|
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 +1840,7 @@ function createDeweeHandler(options) {
|
|
|
1362
1840
|
console.error("[dewee-webchat] identity link start: the gateway has identity exchange switched off");
|
|
1363
1841
|
return fail(req, 503, "Linking is not available right now", "linking_unavailable");
|
|
1364
1842
|
}
|
|
1365
|
-
if (typeof code === "string" && DEAD_SESSION_CODES.has(code)) forgetCachedSession(sessionToken);
|
|
1843
|
+
if (typeof code === "string" && DEAD_SESSION_CODES.has(code)) await forgetCachedSession(sessionToken);
|
|
1366
1844
|
return sdkFail(req, e, "link start");
|
|
1367
1845
|
};
|
|
1368
1846
|
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 +1873,7 @@ function createDeweeHandler(options) {
|
|
|
1395
1873
|
const current = !!user && typeof user.subject === "string" && user.subject !== "" ? await subjectDigest(secret, user.subject) : null;
|
|
1396
1874
|
if (current && claims.sub && constantTimeEqual(current, claims.sub)) return null;
|
|
1397
1875
|
if (ccp && current && claims.sub) {
|
|
1398
|
-
forgetCachedSession(claims.ids);
|
|
1876
|
+
await forgetCachedSession(claims.ids);
|
|
1399
1877
|
await ccp.revoke(visitorContext(req), claims.v, sealedEdge(claims), claims.ids, claims.ida);
|
|
1400
1878
|
}
|
|
1401
1879
|
return identityFail(req, "IDENTITY_REQUIRED");
|
|
@@ -1409,22 +1887,20 @@ function createDeweeHandler(options) {
|
|
|
1409
1887
|
if (idn.assertionSecret) return { applicationId: idn.appId, issuer: idn.issuer, secret: idn.assertionSecret };
|
|
1410
1888
|
return { applicationId: idn.appId, issuer: idn.issuer, privateKeyJwk: idn.privateKeyJwk, algorithm: idn.algorithm };
|
|
1411
1889
|
}
|
|
1412
|
-
const
|
|
1890
|
+
const sessionStore = idn?.sessionStore ?? memoryIdentitySessionStore();
|
|
1413
1891
|
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);
|
|
1892
|
+
async function storeOperation(operation) {
|
|
1893
|
+
try {
|
|
1894
|
+
return await operation();
|
|
1895
|
+
} catch {
|
|
1896
|
+
throw new IdentityStoreError("Identity session store unavailable");
|
|
1426
1897
|
}
|
|
1427
1898
|
}
|
|
1899
|
+
async function forgetCachedSession(sessionToken) {
|
|
1900
|
+
await storeOperation(async () => sessionStore.deleteToken(await storeNamespace(), sessionToken));
|
|
1901
|
+
}
|
|
1902
|
+
const storeNamespace = () => subjectDigest(secret, JSON.stringify([o.ccp?.chatUrl ?? o.gatewayUrl ?? "", o.ccp?.integrationId ?? o.tenantId ?? "", idn.appId, idn.issuer]));
|
|
1903
|
+
const storeSubject = async (subject) => `${await storeNamespace()}:${await subjectDigest(secret, subject)}`;
|
|
1428
1904
|
async function exchangeFor(subject, req, sealed) {
|
|
1429
1905
|
if (ccp) return ccpExchangeFor(subject, req, sealed);
|
|
1430
1906
|
const session = await idn.sdk.exchangeSession({
|
|
@@ -1477,28 +1953,44 @@ function createDeweeHandler(options) {
|
|
|
1477
1953
|
} catch (e) {
|
|
1478
1954
|
return ccpFail(req, e, "config");
|
|
1479
1955
|
}
|
|
1480
|
-
const
|
|
1481
|
-
|
|
1482
|
-
|
|
1956
|
+
const subjectKey = await storeSubject(subject);
|
|
1957
|
+
const key = JSON.stringify([subjectKey, agent]);
|
|
1958
|
+
const observed = await storeOperation(() => sessionStore.read(subjectKey, agent));
|
|
1959
|
+
let s = observed.session;
|
|
1483
1960
|
const userVisitor = ccp ? await deriveVisitorId(secret, `subject:${subject}`) : void 0;
|
|
1484
1961
|
const sealed = hint && hint.ids && hint.v === userVisitor ? sealedEdge(hint) : void 0;
|
|
1485
1962
|
if (!s || s.deadline - Date.now() <= s.margin) {
|
|
1486
|
-
|
|
1487
|
-
if (
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1963
|
+
let pending = exchanges.get(key);
|
|
1964
|
+
if (!pending) {
|
|
1965
|
+
pending = (async () => {
|
|
1966
|
+
const reused = await verifiedReuse(req, hint, subject, agent, userVisitor);
|
|
1967
|
+
const candidate = reused ?? await exchangeFor(subject, req, sealed);
|
|
1968
|
+
const revokeMinted = async () => {
|
|
1969
|
+
if (ccp && !reused) await ccp.revoke(visitorContext(req), candidate.visitorId, sealed, candidate.sessionToken, agent).catch(() => {
|
|
1970
|
+
});
|
|
1971
|
+
};
|
|
1972
|
+
try {
|
|
1973
|
+
const committed = await storeOperation(() => sessionStore.commit(subjectKey, agent, observed.revision, candidate));
|
|
1974
|
+
if (!committed) {
|
|
1975
|
+
const current = await storeOperation(() => sessionStore.read(subjectKey, agent));
|
|
1976
|
+
if (current.session?.sessionToken !== candidate.sessionToken) throw new IdentityStoreError("Identity session changed");
|
|
1977
|
+
}
|
|
1978
|
+
return candidate;
|
|
1979
|
+
} catch (error) {
|
|
1980
|
+
await revokeMinted();
|
|
1981
|
+
throw error;
|
|
1982
|
+
}
|
|
1983
|
+
})().finally(() => exchanges.delete(key));
|
|
1984
|
+
exchanges.set(key, pending);
|
|
1985
|
+
}
|
|
1986
|
+
try {
|
|
1987
|
+
s = await pending;
|
|
1988
|
+
} catch (e) {
|
|
1989
|
+
if (e instanceof IdentityStoreError) throw e;
|
|
1990
|
+
return ccp ? ccpExchangeFail(req, e) : exchangeFail(req, e);
|
|
1500
1991
|
}
|
|
1501
|
-
|
|
1992
|
+
} else if (!await storeOperation(() => sessionStore.check(subjectKey, observed.revision))) {
|
|
1993
|
+
return identityFail(req, "SOURCE_UNAVAILABLE", { action: "retry_later", audience: "end_user" });
|
|
1502
1994
|
}
|
|
1503
1995
|
const now = Date.now();
|
|
1504
1996
|
const expiresAt = Math.min(s.deadline, now + o.sessionTtlMs);
|
|
@@ -1546,6 +2038,15 @@ ${subject}`;
|
|
|
1546
2038
|
maxMessageLength: o.maxMessageLength,
|
|
1547
2039
|
version: VERSION
|
|
1548
2040
|
};
|
|
2041
|
+
if (creds && new URL(req.url).searchParams.get("pageContext") === "1") {
|
|
2042
|
+
if (limiter && !await limiter(clientIp(req), "preauth")) return fail(req, 429, "Too many requests, slow down", "rate_limited");
|
|
2043
|
+
try {
|
|
2044
|
+
const probe = await GatewayConnection.connect(creds, o.userIdPrefix + "capability-probe");
|
|
2045
|
+
if (probe.pageContextVersions.length) body.pageContextVersions = probe.pageContextVersions;
|
|
2046
|
+
probe.close();
|
|
2047
|
+
} catch {
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
1549
2050
|
if (idn) {
|
|
1550
2051
|
body.defaultAgent = identityAgent;
|
|
1551
2052
|
body.allowAgentSwitch = false;
|
|
@@ -1570,6 +2071,7 @@ ${subject}`;
|
|
|
1570
2071
|
turnstileSiteKey: o.turnstile?.siteKey,
|
|
1571
2072
|
...o.turnstile ? { refreshWithoutTurnstile: true } : {},
|
|
1572
2073
|
maxMessageLength: Math.min(o.maxMessageLength, cfg.maxMessageChars),
|
|
2074
|
+
...cfg.pageContextVersions.length ? { pageContextVersions: cfg.pageContextVersions } : {},
|
|
1573
2075
|
version: VERSION
|
|
1574
2076
|
};
|
|
1575
2077
|
if (idn) {
|
|
@@ -1605,7 +2107,7 @@ ${subject}`;
|
|
|
1605
2107
|
if (!(ccp && previous) && !await turnstilePassed(body, ip)) return fail(req, 403, "Verification failed", "turnstile_failed");
|
|
1606
2108
|
const visitorId = previous?.v ?? randomId(16);
|
|
1607
2109
|
const expiresAt = Date.now() + o.sessionTtlMs;
|
|
1608
|
-
if (!ccp) return json(req, { visitorId, token: await signVisitorToken(secret, { v: visitorId, exp: expiresAt }), expiresAt });
|
|
2110
|
+
if (!ccp) return json(req, { visitorId, token: await signVisitorToken(secret, { v: visitorId, exp: expiresAt }), expiresAt, expiresIn: Math.max(0, expiresAt - Date.now()) });
|
|
1609
2111
|
let edge;
|
|
1610
2112
|
try {
|
|
1611
2113
|
edge = await ccp.edgeFor(visitorContext(req), visitorId, sealedEdge(previous), void 0, EDGE_RESEAL_MS);
|
|
@@ -1642,7 +2144,7 @@ ${subject}`;
|
|
|
1642
2144
|
const previous = await anonymousPrevious(body);
|
|
1643
2145
|
if (previous && previous.exp - Date.now() > ANON_REUSE_MS) {
|
|
1644
2146
|
if (!ccp) {
|
|
1645
|
-
const reused = { visitorId: previous.v, token: body.token, expiresAt: previous.exp };
|
|
2147
|
+
const reused = { visitorId: previous.v, token: body.token, expiresAt: previous.exp, expiresIn: Math.max(0, previous.exp - Date.now()) };
|
|
1646
2148
|
return json(req, reused);
|
|
1647
2149
|
}
|
|
1648
2150
|
const edge = sealedEdge(previous);
|
|
@@ -1666,7 +2168,10 @@ ${subject}`;
|
|
|
1666
2168
|
const { creds, byo: isByo, error } = resolveCreds(req);
|
|
1667
2169
|
if (error) return fail(req, 400, error, "bad_credentials");
|
|
1668
2170
|
if (!creds && !ccp) return fail(req, 500, "Gateway is not configured", "not_configured");
|
|
1669
|
-
const
|
|
2171
|
+
const input = await readChatBody(req);
|
|
2172
|
+
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");
|
|
2173
|
+
if (!input.body || typeof input.body !== "object" || Array.isArray(input.body)) return fail(req, 400, "Invalid request", "bad_request");
|
|
2174
|
+
const body = input.body;
|
|
1670
2175
|
const message = typeof body?.message === "string" ? body.message.trim() : "";
|
|
1671
2176
|
if (!message) return fail(req, 400, "Message is required");
|
|
1672
2177
|
if (message.length > o.maxMessageLength) return fail(req, 413, `Message exceeds ${o.maxMessageLength} characters`, "too_long");
|
|
@@ -1686,15 +2191,23 @@ ${subject}`;
|
|
|
1686
2191
|
if (e instanceof ActionInputError) return fail(req, e.status, e.message, e.code);
|
|
1687
2192
|
throw e;
|
|
1688
2193
|
}
|
|
1689
|
-
if (
|
|
2194
|
+
if (body?.page_context != null) {
|
|
2195
|
+
if (typeof body.page_context === "object" && Number.isInteger(body.page_context.version) && body.page_context.version !== 1) return fail(req, 400, "Page context is not supported", "page_context_unsupported");
|
|
2196
|
+
if (!validPageContext(body.page_context)) return fail(req, 400, "Invalid page context", "page_context_invalid");
|
|
2197
|
+
}
|
|
2198
|
+
if (ccp) return ccpChat(req, claims, { message: pageBlock + message, rawMessage: message, conversationId, agent, actionsHash, pageContext: body?.page_context });
|
|
1690
2199
|
if (!creds) return fail(req, 500, "Gateway is not configured", "not_configured");
|
|
1691
2200
|
let connection;
|
|
1692
|
-
if (claims.ids) {
|
|
2201
|
+
if (claims.ids || body?.page_context != null) {
|
|
1693
2202
|
try {
|
|
1694
|
-
connection = await GatewayConnection.connect(creds, { identitySession: claims.ids });
|
|
2203
|
+
connection = await GatewayConnection.connect(creds, claims.ids ? { identitySession: claims.ids } : o.userIdPrefix + visitorId);
|
|
1695
2204
|
} catch (e) {
|
|
1696
2205
|
return gatewayFail(req, e, claims.ids);
|
|
1697
2206
|
}
|
|
2207
|
+
if (body?.page_context != null && !connection.pageContextVersions.includes(1)) {
|
|
2208
|
+
connection.close();
|
|
2209
|
+
return fail(req, 400, "Page context is not supported", "page_context_unsupported");
|
|
2210
|
+
}
|
|
1698
2211
|
}
|
|
1699
2212
|
const sessionKey = buildSessionKey(agent, `webchat-${visitorId}-${conversationId}`);
|
|
1700
2213
|
const abort = new AbortController();
|
|
@@ -1712,12 +2225,12 @@ ${subject}`;
|
|
|
1712
2225
|
try {
|
|
1713
2226
|
await send(actionsHash ? { type: "start", conversationId, agent, actionsHash } : { type: "start", conversationId, agent });
|
|
1714
2227
|
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 })) {
|
|
2228
|
+
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
2229
|
if (ev.type === "start") continue;
|
|
1717
2230
|
if (ev.type === "done") reply = ev.content;
|
|
1718
2231
|
if (ev.type === "error") {
|
|
1719
2232
|
errMsg = gatewayReasonOf(ev) ?? ev.message;
|
|
1720
|
-
if (claims.ids && ev.code && DEAD_SESSION_CODES.has(ev.code)) forgetCachedSession(claims.ids);
|
|
2233
|
+
if (claims.ids && ev.code && DEAD_SESSION_CODES.has(ev.code)) await forgetCachedSession(claims.ids);
|
|
1721
2234
|
}
|
|
1722
2235
|
await send(ev);
|
|
1723
2236
|
if (abort.signal.aborted) break;
|
|
@@ -1773,6 +2286,20 @@ ${subject}`;
|
|
|
1773
2286
|
let upstream;
|
|
1774
2287
|
try {
|
|
1775
2288
|
cfg = await ccp.config();
|
|
2289
|
+
if (turn.pageContext != null) {
|
|
2290
|
+
if (!cfg.pageContextVersions.includes(1)) {
|
|
2291
|
+
cleanup();
|
|
2292
|
+
return fail(req, 400, "Page context is not supported", "page_context_unsupported");
|
|
2293
|
+
}
|
|
2294
|
+
if (typeof turn.pageContext === "object" && "version" in turn.pageContext && Number.isInteger(turn.pageContext.version) && turn.pageContext.version !== 1) {
|
|
2295
|
+
cleanup();
|
|
2296
|
+
return fail(req, 400, "Page context is not supported", "page_context_unsupported");
|
|
2297
|
+
}
|
|
2298
|
+
if (!validPageContext(turn.pageContext)) {
|
|
2299
|
+
cleanup();
|
|
2300
|
+
return fail(req, 400, "Invalid page context", "page_context_invalid");
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
1776
2303
|
if (turn.rawMessage.length > cfg.maxMessageChars) {
|
|
1777
2304
|
cleanup();
|
|
1778
2305
|
return fail(req, 413, `Message exceeds ${cfg.maxMessageChars} characters`, "too_long");
|
|
@@ -1782,6 +2309,7 @@ ${subject}`;
|
|
|
1782
2309
|
conversationId: turn.conversationId,
|
|
1783
2310
|
agent: turn.agent || void 0,
|
|
1784
2311
|
identitySession: claims.ids,
|
|
2312
|
+
pageContext: turn.pageContext ?? void 0,
|
|
1785
2313
|
signal: abort.signal
|
|
1786
2314
|
});
|
|
1787
2315
|
} catch (e) {
|
|
@@ -1828,7 +2356,7 @@ ${subject}`;
|
|
|
1828
2356
|
if (ev.type === "error") {
|
|
1829
2357
|
errMsg = ev.message;
|
|
1830
2358
|
finished = true;
|
|
1831
|
-
if (claims.ids && ev.code && DEAD_SESSION_CODES.has(ev.code)) forgetCachedSession(claims.ids);
|
|
2359
|
+
if (claims.ids && ev.code && DEAD_SESSION_CODES.has(ev.code)) await forgetCachedSession(claims.ids);
|
|
1832
2360
|
}
|
|
1833
2361
|
await send(ev);
|
|
1834
2362
|
if (abort.signal.aborted) break;
|
|
@@ -1969,10 +2497,12 @@ ${subject}`;
|
|
|
1969
2497
|
async function revoke(subject, request, token) {
|
|
1970
2498
|
if (!idn || typeof subject !== "string" || subject === "") return;
|
|
1971
2499
|
try {
|
|
1972
|
-
const
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
2500
|
+
const subjectKey = await storeSubject(subject);
|
|
2501
|
+
let held = [];
|
|
2502
|
+
try {
|
|
2503
|
+
held = await storeOperation(() => sessionStore.takeSubject(subjectKey));
|
|
2504
|
+
} catch {
|
|
2505
|
+
console.warn("[dewee-webchat] identity session store unavailable during revoke");
|
|
1976
2506
|
}
|
|
1977
2507
|
if (ccp && typeof token === "string" && token !== "") {
|
|
1978
2508
|
const c = await verifyReuseHint(secret, token) ?? await verifyVisitorToken(secret, token);
|
|
@@ -1980,7 +2510,6 @@ ${subject}`;
|
|
|
1980
2510
|
held.push({ agent: c.ida, session: { sessionToken: c.ids, visitorId: c.v, deadline: c.exp, margin: 0 }, edge: sealedEdge(c) });
|
|
1981
2511
|
}
|
|
1982
2512
|
}
|
|
1983
|
-
for (const { session } of held) forgetCachedSession(session.sessionToken);
|
|
1984
2513
|
if (!ccp) return;
|
|
1985
2514
|
const fromRequest = request ? visitorContext(request) : null;
|
|
1986
2515
|
await Promise.all(
|
|
@@ -1999,6 +2528,44 @@ ${subject}`;
|
|
|
1999
2528
|
if (!originAllowed(req)) return fail(req, 403, "Origin not allowed", "forbidden_origin");
|
|
2000
2529
|
const action = new URL(req.url).pathname.replace(/\/+$/, "").split("/").pop();
|
|
2001
2530
|
try {
|
|
2531
|
+
if (req.method === "POST" && action === "image-ticket" && imageSources.length) {
|
|
2532
|
+
const claims = await visitorFrom(req);
|
|
2533
|
+
if (!claims) return fail(req, 401, "Missing or expired visitor session", "unauthorized");
|
|
2534
|
+
if (idn?.required && !claims.ids) return identityFail(req, "IDENTITY_REQUIRED");
|
|
2535
|
+
const refused = await rebindRefusal(req, claims);
|
|
2536
|
+
if (refused) return refused;
|
|
2537
|
+
if (limiter && !await limiter(`image-ticket:${claims.v}`, "preauth")) return fail(req, 429, "Too many requests, try again later", "rate_limited");
|
|
2538
|
+
const body = await readImageTicketBody(req);
|
|
2539
|
+
if (body === null) return fail(req, 413, "Invalid image", "invalid_request");
|
|
2540
|
+
let parsed;
|
|
2541
|
+
try {
|
|
2542
|
+
parsed = JSON.parse(body);
|
|
2543
|
+
} catch {
|
|
2544
|
+
return fail(req, 400, "Invalid image", "invalid_request");
|
|
2545
|
+
}
|
|
2546
|
+
const raw = parsed?.url;
|
|
2547
|
+
const worker = globalThis.navigator?.userAgent === "Cloudflare-Workers";
|
|
2548
|
+
const source = typeof raw === "string" ? allowedImageSource(raw, imageSources, new URL(req.url), worker) : null;
|
|
2549
|
+
if (!source) return fail(req, 403, "Image not allowed", "forbidden_image");
|
|
2550
|
+
return json(req, { ticket: await issueImageTicket(secret, imageNamespace, claims.v, source.href) });
|
|
2551
|
+
}
|
|
2552
|
+
if (req.method === "GET" && action === "image" && imageSources.length) {
|
|
2553
|
+
const ticket = new URL(req.url).searchParams.get("ticket") ?? "";
|
|
2554
|
+
const claims = await verifyImageTicket(secret, imageNamespace, ticket);
|
|
2555
|
+
if (!claims) return fail(req, 401, "Image ticket expired", "unauthorized");
|
|
2556
|
+
const worker = globalThis.navigator?.userAgent === "Cloudflare-Workers";
|
|
2557
|
+
const source = allowedImageSource(claims.source, imageSources, new URL(req.url), worker);
|
|
2558
|
+
if (!source) return fail(req, 403, "Image not allowed", "forbidden_image");
|
|
2559
|
+
if (limiter && !await limiter(`image:${claims.visitor}`, "preauth")) return fail(req, 429, "Too many requests, try again later", "rate_limited");
|
|
2560
|
+
if (activeImages >= 8) return fail(req, 429, "Too many requests, try again later", "rate_limited");
|
|
2561
|
+
activeImages++;
|
|
2562
|
+
try {
|
|
2563
|
+
const result = await fetchRasterImage(source.href, worker ? globalThis.fetch : publicImageFetchNode, worker ? "manual" : "error");
|
|
2564
|
+
return result ?? fail(req, 502, "Image unavailable", "image_unavailable");
|
|
2565
|
+
} finally {
|
|
2566
|
+
activeImages--;
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2002
2569
|
if (req.method === "GET" && action === "config") return await handleConfig(req);
|
|
2003
2570
|
if (req.method === "POST" && action === "session") return await handleSession(req);
|
|
2004
2571
|
if (req.method === "POST" && action === "chat") return await handleChat(req);
|
|
@@ -2007,6 +2574,7 @@ ${subject}`;
|
|
|
2007
2574
|
if (req.method === "POST" && action === "job-access" && idn) return await handleJobAccess(req);
|
|
2008
2575
|
return fail(req, 404, "Not found", "not_found");
|
|
2009
2576
|
} catch (e) {
|
|
2577
|
+
if (e instanceof IdentityStoreError) return identityFail(req, "SOURCE_UNAVAILABLE", { action: "retry_later", audience: "end_user" });
|
|
2010
2578
|
if (!ccp) return fail(req, 500, e.message || "Internal error", "internal");
|
|
2011
2579
|
console.error("[dewee-webchat] internal error:", e instanceof CcpError ? e.code : e);
|
|
2012
2580
|
return fail(req, 500, "Internal error", "internal");
|
|
@@ -2037,6 +2605,7 @@ function optionsFromEnv(env, overrides = {}) {
|
|
|
2037
2605
|
agents,
|
|
2038
2606
|
defaultAgent: env.DEWEE_DEFAULT_AGENT,
|
|
2039
2607
|
allowedOrigins: origins,
|
|
2608
|
+
imageSources: list(env.DEEWEE_IMAGE_SOURCES),
|
|
2040
2609
|
allowClientCredentials: env.DEWEE_ALLOW_CLIENT_CREDENTIALS === "true" ? byoHosts?.length ? { allowedHosts: byoHosts } : true : false,
|
|
2041
2610
|
turnstile: env.TURNSTILE_SITE_KEY && env.TURNSTILE_SECRET_KEY ? { siteKey: env.TURNSTILE_SITE_KEY, secretKey: env.TURNSTILE_SECRET_KEY } : void 0,
|
|
2042
2611
|
// undefined overrides must not erase env-derived values
|
|
@@ -2110,6 +2679,7 @@ export {
|
|
|
2110
2679
|
hashManifest,
|
|
2111
2680
|
isAdmissionErrorCode,
|
|
2112
2681
|
listAgents,
|
|
2682
|
+
memoryIdentitySessionStore,
|
|
2113
2683
|
memoryRateLimiter,
|
|
2114
2684
|
normalizeManifest,
|
|
2115
2685
|
optionsFromEnv,
|