@rubytech/create-maxy-code 0.1.353 → 0.1.355
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/dist/__tests__/preflight-port-classifier.test.js +52 -0
- package/dist/preflight-port-classifier.js +31 -2
- package/package.json +1 -1
- package/payload/platform/lib/admin-access-password/__tests__/index.test.ts +96 -14
- package/payload/platform/lib/admin-access-password/dist/index.d.ts +20 -4
- package/payload/platform/lib/admin-access-password/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/admin-access-password/dist/index.js +43 -4
- package/payload/platform/lib/admin-access-password/dist/index.js.map +1 -1
- package/payload/platform/lib/admin-access-password/src/index.ts +67 -4
- package/payload/platform/lib/admins-write/__tests__/index.test.ts +182 -0
- package/payload/platform/lib/admins-write/dist/index.d.ts +65 -11
- package/payload/platform/lib/admins-write/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/admins-write/dist/index.js +93 -31
- package/payload/platform/lib/admins-write/dist/index.js.map +1 -1
- package/payload/platform/lib/admins-write/src/index.ts +133 -36
- package/payload/platform/plugins/admin/PLUGIN.md +4 -0
- package/payload/platform/plugins/admin/mcp/dist/index.js +15 -3
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +16 -1
- package/payload/platform/plugins/docs/references/admin-session.md +1 -1
- package/payload/platform/plugins/docs/references/troubleshooting.md +15 -0
- package/payload/platform/scripts/check-no-esm-require.mjs +4 -0
- package/payload/server/{chunk-QAKVFSEJ.js → chunk-7GNXBVGY.js} +38 -11
- package/payload/server/maxy-edge.js +1 -1
- package/payload/server/public/assets/AdminLoginScreens-4I5CdIWl.js +1 -0
- package/payload/server/public/assets/{AdminShell-CC-CuN6Y.js → AdminShell-Bc9F7X_N.js} +1 -1
- package/payload/server/public/assets/{Checkbox-sQBc9xSy.js → Checkbox-DoQV18Ow.js} +1 -1
- package/payload/server/public/assets/OperatorConversations-BJY9FZbr.js +9 -0
- package/payload/server/public/assets/OperatorConversations-C1I78hON.css +1 -0
- package/payload/server/public/assets/{admin-qW6jM6l0.js → admin-DLsMA09F.js} +1 -1
- package/payload/server/public/assets/{browser-BsW6KaxZ.js → browser-w6rY_KAO.js} +1 -1
- package/payload/server/public/assets/{calendar-BPAiN1cL.js → calendar-CvEdDFYd.js} +1 -1
- package/payload/server/public/assets/chat-BlOWMUVI.js +1 -0
- package/payload/server/public/assets/{data-D_cR_NCZ.js → data-p3T_EZFq.js} +1 -1
- package/payload/server/public/assets/{graph-CmpAy2WP.js → graph-BrwfYOr3.js} +1 -1
- package/payload/server/public/assets/{graph-labels-C0SLq_VP.js → graph-labels-BNZ8u8TE.js} +1 -1
- package/payload/server/public/assets/{operator-CrjtJcEY.js → operator-BNY1P_mH.js} +1 -1
- package/payload/server/public/assets/{page-Cf3fVDSo.js → page-CtygoIXt.js} +3 -3
- package/payload/server/public/assets/{public-BkNtvSYp.js → public-CDzw6yjH.js} +1 -1
- package/payload/server/public/browser.html +4 -4
- package/payload/server/public/calendar.html +4 -4
- package/payload/server/public/chat.html +5 -5
- package/payload/server/public/data.html +4 -4
- package/payload/server/public/graph.html +6 -6
- package/payload/server/public/index.html +6 -6
- package/payload/server/public/operator.html +7 -7
- package/payload/server/public/public.html +5 -5
- package/payload/server/server.js +1289 -1150
- package/payload/server/public/assets/AdminLoginScreens-CzdOQKyO.js +0 -1
- package/payload/server/public/assets/OperatorConversations-CA_2c3sp.js +0 -9
- package/payload/server/public/assets/OperatorConversations-DPVKGsRS.css +0 -1
- package/payload/server/public/assets/chat-DVD9yoVL.js +0 -1
package/payload/server/server.js
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
REMOTE_PASSWORD_FILE,
|
|
22
22
|
RFB_PORT,
|
|
23
23
|
SYSTEM_PATH_SLUGS,
|
|
24
|
+
USERS_AUDIT_LOG,
|
|
24
25
|
USERS_FILE,
|
|
25
26
|
VISITOR_TOKEN_SECRET_FILE,
|
|
26
27
|
VNC_DISPLAY,
|
|
@@ -105,7 +106,7 @@ import {
|
|
|
105
106
|
vncLog,
|
|
106
107
|
walkPremiumBundles,
|
|
107
108
|
writeAdminUserAndPerson
|
|
108
|
-
} from "./chunk-
|
|
109
|
+
} from "./chunk-7GNXBVGY.js";
|
|
109
110
|
import {
|
|
110
111
|
__commonJS,
|
|
111
112
|
__toESM
|
|
@@ -361,9 +362,9 @@ var require_dist = __commonJS({
|
|
|
361
362
|
try {
|
|
362
363
|
await onEmpty(c);
|
|
363
364
|
} catch (err) {
|
|
364
|
-
const
|
|
365
|
+
const id82 = c.elementId.slice(0, 8);
|
|
365
366
|
const message = err instanceof Error ? err.message : String(err);
|
|
366
|
-
process.stderr.write(`[trash:empty-run] reason=onEmpty-failed elementId=${
|
|
367
|
+
process.stderr.write(`[trash:empty-run] reason=onEmpty-failed elementId=${id82} labels=${c.labels.join(",")} message=${message}
|
|
367
368
|
`);
|
|
368
369
|
continue;
|
|
369
370
|
}
|
|
@@ -1303,7 +1304,7 @@ var serveStatic = (options = { root: "" }) => {
|
|
|
1303
1304
|
// server/index.ts
|
|
1304
1305
|
import { readFileSync as readFileSync33, existsSync as existsSync32, watchFile } from "fs";
|
|
1305
1306
|
import { spawn as spawn3 } from "child_process";
|
|
1306
|
-
import { resolve as
|
|
1307
|
+
import { resolve as resolve32, join as join32, basename as basename10 } from "path";
|
|
1307
1308
|
import { homedir as homedir3 } from "os";
|
|
1308
1309
|
import { monitorEventLoopDelay } from "perf_hooks";
|
|
1309
1310
|
|
|
@@ -1961,7 +1962,7 @@ var credsSaveQueue = Promise.resolve();
|
|
|
1961
1962
|
async function drainCredsSaveQueue(timeoutMs = 5e3) {
|
|
1962
1963
|
console.error(`${TAG2} draining credential save queue\u2026`);
|
|
1963
1964
|
const timer2 = new Promise(
|
|
1964
|
-
(
|
|
1965
|
+
(resolve33) => setTimeout(() => resolve33("timeout"), timeoutMs)
|
|
1965
1966
|
);
|
|
1966
1967
|
const result = await Promise.race([
|
|
1967
1968
|
credsSaveQueue.then(() => "drained"),
|
|
@@ -2089,11 +2090,11 @@ async function createWaSocket(opts) {
|
|
|
2089
2090
|
return sock;
|
|
2090
2091
|
}
|
|
2091
2092
|
async function waitForConnection(sock) {
|
|
2092
|
-
return new Promise((
|
|
2093
|
+
return new Promise((resolve33, reject) => {
|
|
2093
2094
|
const handler = (update) => {
|
|
2094
2095
|
if (update.connection === "open") {
|
|
2095
2096
|
sock.ev.off("connection.update", handler);
|
|
2096
|
-
|
|
2097
|
+
resolve33();
|
|
2097
2098
|
}
|
|
2098
2099
|
if (update.connection === "close") {
|
|
2099
2100
|
sock.ev.off("connection.update", handler);
|
|
@@ -2207,14 +2208,14 @@ ${inspected}`;
|
|
|
2207
2208
|
return inspect2(err, INSPECT_OPTS2);
|
|
2208
2209
|
}
|
|
2209
2210
|
function withTimeout(label, promise, timeoutMs) {
|
|
2210
|
-
return new Promise((
|
|
2211
|
+
return new Promise((resolve33, reject) => {
|
|
2211
2212
|
const timer2 = setTimeout(() => {
|
|
2212
2213
|
reject(new Error(`${label} timed out after ${timeoutMs}ms`));
|
|
2213
2214
|
}, timeoutMs);
|
|
2214
2215
|
promise.then(
|
|
2215
2216
|
(value) => {
|
|
2216
2217
|
clearTimeout(timer2);
|
|
2217
|
-
|
|
2218
|
+
resolve33(value);
|
|
2218
2219
|
},
|
|
2219
2220
|
(err) => {
|
|
2220
2221
|
clearTimeout(timer2);
|
|
@@ -3098,8 +3099,8 @@ async function persistWhatsAppMessage(input) {
|
|
|
3098
3099
|
const { givenName, familyName } = splitName(input.pushName);
|
|
3099
3100
|
const prev = sessionWriteLocks.get(input.cacheKey);
|
|
3100
3101
|
let release;
|
|
3101
|
-
const mine = new Promise((
|
|
3102
|
-
release =
|
|
3102
|
+
const mine = new Promise((resolve33) => {
|
|
3103
|
+
release = resolve33;
|
|
3103
3104
|
});
|
|
3104
3105
|
const chained = (prev ?? Promise.resolve()).then(() => mine);
|
|
3105
3106
|
sessionWriteLocks.set(input.cacheKey, chained);
|
|
@@ -4209,11 +4210,11 @@ async function connectWithReconnect(conn) {
|
|
|
4209
4210
|
console.error(
|
|
4210
4211
|
`${TAG13} reconnecting account=${conn.accountId} in ${delay}ms (attempt ${decision.nextAttempts}/${maxAttempts})`
|
|
4211
4212
|
);
|
|
4212
|
-
await new Promise((
|
|
4213
|
-
const timer2 = setTimeout(
|
|
4213
|
+
await new Promise((resolve33) => {
|
|
4214
|
+
const timer2 = setTimeout(resolve33, delay);
|
|
4214
4215
|
conn.abortController.signal.addEventListener("abort", () => {
|
|
4215
4216
|
clearTimeout(timer2);
|
|
4216
|
-
|
|
4217
|
+
resolve33();
|
|
4217
4218
|
}, { once: true });
|
|
4218
4219
|
});
|
|
4219
4220
|
}
|
|
@@ -4221,16 +4222,16 @@ async function connectWithReconnect(conn) {
|
|
|
4221
4222
|
}
|
|
4222
4223
|
}
|
|
4223
4224
|
function waitForDisconnectEvent(conn) {
|
|
4224
|
-
return new Promise((
|
|
4225
|
+
return new Promise((resolve33) => {
|
|
4225
4226
|
if (!conn.sock) {
|
|
4226
|
-
|
|
4227
|
+
resolve33();
|
|
4227
4228
|
return;
|
|
4228
4229
|
}
|
|
4229
4230
|
const sock = conn.sock;
|
|
4230
4231
|
const handler = (update) => {
|
|
4231
4232
|
if (update.connection === "close") {
|
|
4232
4233
|
sock.ev.off("connection.update", handler);
|
|
4233
|
-
|
|
4234
|
+
resolve33();
|
|
4234
4235
|
}
|
|
4235
4236
|
};
|
|
4236
4237
|
sock.ev.on("connection.update", handler);
|
|
@@ -4490,8 +4491,8 @@ async function handleInboundMessage(conn, msg) {
|
|
|
4490
4491
|
const conversationKey = isGroup ? remoteJid : senderPhone;
|
|
4491
4492
|
const debounceKey = `${conn.accountId}:${conversationKey}:${senderPhone}`;
|
|
4492
4493
|
let resolvePending;
|
|
4493
|
-
const sttPending = new Promise((
|
|
4494
|
-
resolvePending =
|
|
4494
|
+
const sttPending = new Promise((resolve33) => {
|
|
4495
|
+
resolvePending = resolve33;
|
|
4495
4496
|
});
|
|
4496
4497
|
if (conn.debouncer) conn.debouncer.registerPending(debounceKey, sttPending);
|
|
4497
4498
|
try {
|
|
@@ -4891,20 +4892,20 @@ function buildX11Env(chromiumWrapperPath, transport = "vnc") {
|
|
|
4891
4892
|
|
|
4892
4893
|
// server/routes/health.ts
|
|
4893
4894
|
function checkPort(port2, timeoutMs = 500) {
|
|
4894
|
-
return new Promise((
|
|
4895
|
+
return new Promise((resolve33) => {
|
|
4895
4896
|
const socket = createConnection2(port2, "127.0.0.1");
|
|
4896
4897
|
socket.setTimeout(timeoutMs);
|
|
4897
4898
|
socket.once("connect", () => {
|
|
4898
4899
|
socket.destroy();
|
|
4899
|
-
|
|
4900
|
+
resolve33(true);
|
|
4900
4901
|
});
|
|
4901
4902
|
socket.once("error", () => {
|
|
4902
4903
|
socket.destroy();
|
|
4903
|
-
|
|
4904
|
+
resolve33(false);
|
|
4904
4905
|
});
|
|
4905
4906
|
socket.once("timeout", () => {
|
|
4906
4907
|
socket.destroy();
|
|
4907
|
-
|
|
4908
|
+
resolve33(false);
|
|
4908
4909
|
});
|
|
4909
4910
|
});
|
|
4910
4911
|
}
|
|
@@ -6044,8 +6045,8 @@ async function startLogin(opts) {
|
|
|
6044
6045
|
await clearAuth(authDir);
|
|
6045
6046
|
let resolveCode = null;
|
|
6046
6047
|
let rejectCode = null;
|
|
6047
|
-
const codePromise = new Promise((
|
|
6048
|
-
resolveCode =
|
|
6048
|
+
const codePromise = new Promise((resolve33, reject) => {
|
|
6049
|
+
resolveCode = resolve33;
|
|
6049
6050
|
rejectCode = reject;
|
|
6050
6051
|
});
|
|
6051
6052
|
const codeTimer = setTimeout(
|
|
@@ -9471,8 +9472,31 @@ import { openSync as openSync3, closeSync as closeSync3, writeFileSync as writeF
|
|
|
9471
9472
|
import { createHash as createHash3, randomUUID as randomUUID7 } from "crypto";
|
|
9472
9473
|
|
|
9473
9474
|
// ../lib/admins-write/src/index.ts
|
|
9474
|
-
import { existsSync as existsSync13, readFileSync as readFileSync21, writeFileSync as writeFileSync9, renameSync as renameSync5, mkdirSync as mkdirSync4, readdirSync as readdirSync12, statSync as statSync7 } from "fs";
|
|
9475
|
+
import { existsSync as existsSync13, readFileSync as readFileSync21, writeFileSync as writeFileSync9, renameSync as renameSync5, mkdirSync as mkdirSync4, readdirSync as readdirSync12, statSync as statSync7, appendFileSync } from "fs";
|
|
9475
9476
|
import { dirname as dirname4, join as join20 } from "path";
|
|
9477
|
+
function id8(value) {
|
|
9478
|
+
return value.slice(0, 8);
|
|
9479
|
+
}
|
|
9480
|
+
function formatAuditRowIds(userIds) {
|
|
9481
|
+
return userIds.map(id8).join(",");
|
|
9482
|
+
}
|
|
9483
|
+
function appendUsersAuditLine(audit, fields) {
|
|
9484
|
+
const sess = audit.session ? ` session=${id8(audit.session)}` : "";
|
|
9485
|
+
const actor = audit.actor.includes("-") ? id8(audit.actor) : audit.actor;
|
|
9486
|
+
const line = `[users-audit] action=${fields.action} actor=${actor}${sess} field=${fields.field} rowsBefore=${fields.rowsBefore} rowsAfter=${fields.rowsAfter} ts=${(/* @__PURE__ */ new Date()).toISOString()}
|
|
9487
|
+
`;
|
|
9488
|
+
try {
|
|
9489
|
+
mkdirSync4(dirname4(audit.logFile), { recursive: true, mode: 448 });
|
|
9490
|
+
appendFileSync(audit.logFile, line, { mode: 384 });
|
|
9491
|
+
} catch (err) {
|
|
9492
|
+
console.error(
|
|
9493
|
+
`[users-audit] op=write-failed logFile=${audit.logFile} error=${err instanceof Error ? err.message : String(err)}`
|
|
9494
|
+
);
|
|
9495
|
+
}
|
|
9496
|
+
}
|
|
9497
|
+
function logUsersAudit(audit, fields) {
|
|
9498
|
+
appendUsersAuditLine(audit, fields);
|
|
9499
|
+
}
|
|
9476
9500
|
function logLine(input, result) {
|
|
9477
9501
|
const userIdShort = input.userId.slice(0, 8);
|
|
9478
9502
|
console.error(
|
|
@@ -9499,6 +9523,7 @@ function writeAdminEntry(input) {
|
|
|
9499
9523
|
users = parsed;
|
|
9500
9524
|
}
|
|
9501
9525
|
}
|
|
9526
|
+
const rowsBefore = users.map((u) => id8(u.userId)).join(",");
|
|
9502
9527
|
const existing = users.findIndex((u) => u.userId === input.userId);
|
|
9503
9528
|
if (existing === -1) {
|
|
9504
9529
|
users.push({ userId: input.userId, pin: input.pin });
|
|
@@ -9507,6 +9532,12 @@ function writeAdminEntry(input) {
|
|
|
9507
9532
|
}
|
|
9508
9533
|
writeFileAtomic(input.usersFile, JSON.stringify(users, null, 2) + "\n", 384);
|
|
9509
9534
|
result.usersJsonResult = "ok";
|
|
9535
|
+
appendUsersAuditLine(input.audit, {
|
|
9536
|
+
action: existing === -1 ? "add" : "set-pin",
|
|
9537
|
+
field: existing === -1 ? "row" : "pin",
|
|
9538
|
+
rowsBefore,
|
|
9539
|
+
rowsAfter: users.map((u) => id8(u.userId)).join(",")
|
|
9540
|
+
});
|
|
9510
9541
|
} catch (err) {
|
|
9511
9542
|
result.usersJsonResult = "fail";
|
|
9512
9543
|
result.usersError = err instanceof Error ? err.message : String(err);
|
|
@@ -9535,12 +9566,8 @@ function writeAdminEntry(input) {
|
|
|
9535
9566
|
logLine(input, result);
|
|
9536
9567
|
return result;
|
|
9537
9568
|
}
|
|
9538
|
-
function
|
|
9539
|
-
const result = {
|
|
9540
|
-
divergences: 0,
|
|
9541
|
-
accountWithoutUsers: [],
|
|
9542
|
-
usersWithoutAccount: []
|
|
9543
|
-
};
|
|
9569
|
+
function computeAdminStoreDivergence(input) {
|
|
9570
|
+
const result = { divergences: 0, accountWithoutUsers: [], usersWithoutAccount: [], errors: [] };
|
|
9544
9571
|
const usersUserIds = /* @__PURE__ */ new Set();
|
|
9545
9572
|
if (existsSync13(input.usersFile)) {
|
|
9546
9573
|
try {
|
|
@@ -9552,8 +9579,7 @@ function checkAdminAuthInvariant(input) {
|
|
|
9552
9579
|
}
|
|
9553
9580
|
}
|
|
9554
9581
|
} catch (err) {
|
|
9555
|
-
|
|
9556
|
-
console.error(`[${input.tag}] users.json unreadable usersFile=${input.usersFile} error=${msg}`);
|
|
9582
|
+
result.errors.push({ source: input.usersFile, detail: err instanceof Error ? err.message : String(err) });
|
|
9557
9583
|
}
|
|
9558
9584
|
}
|
|
9559
9585
|
const accountUserIds = /* @__PURE__ */ new Set();
|
|
@@ -9562,9 +9588,7 @@ function checkAdminAuthInvariant(input) {
|
|
|
9562
9588
|
try {
|
|
9563
9589
|
entries = readdirSync12(input.accountsDir);
|
|
9564
9590
|
} catch (err) {
|
|
9565
|
-
|
|
9566
|
-
console.error(`[${input.tag}] accounts-dir unreadable accountsDir=${input.accountsDir} error=${msg}`);
|
|
9567
|
-
console.error(`[${input.tag}] check complete divergences=0 (accounts-dir unreadable)`);
|
|
9591
|
+
result.errors.push({ source: input.accountsDir, detail: err instanceof Error ? err.message : String(err) });
|
|
9568
9592
|
return result;
|
|
9569
9593
|
}
|
|
9570
9594
|
for (const entry of entries) {
|
|
@@ -9582,18 +9606,13 @@ function checkAdminAuthInvariant(input) {
|
|
|
9582
9606
|
const config = JSON.parse(readFileSync21(accountJsonPath, "utf-8"));
|
|
9583
9607
|
admins = config.admins ?? [];
|
|
9584
9608
|
} catch (err) {
|
|
9585
|
-
|
|
9586
|
-
console.error(`[${input.tag}] account.json unreadable source=${accountJsonPath} error=${msg}`);
|
|
9609
|
+
result.errors.push({ source: accountJsonPath, detail: err instanceof Error ? err.message : String(err) });
|
|
9587
9610
|
continue;
|
|
9588
9611
|
}
|
|
9589
9612
|
for (const a of admins) {
|
|
9590
9613
|
if (typeof a.userId !== "string") continue;
|
|
9591
9614
|
accountUserIds.add(a.userId);
|
|
9592
9615
|
if (!usersUserIds.has(a.userId)) {
|
|
9593
|
-
const userIdShort = a.userId.slice(0, 8);
|
|
9594
|
-
console.error(
|
|
9595
|
-
`[${input.tag}] direction=account-without-users userId=${userIdShort} source=${accountJsonPath}`
|
|
9596
|
-
);
|
|
9597
9616
|
result.accountWithoutUsers.push({ userId: a.userId, source: accountJsonPath });
|
|
9598
9617
|
result.divergences += 1;
|
|
9599
9618
|
}
|
|
@@ -9602,17 +9621,30 @@ function checkAdminAuthInvariant(input) {
|
|
|
9602
9621
|
}
|
|
9603
9622
|
for (const uid of usersUserIds) {
|
|
9604
9623
|
if (!accountUserIds.has(uid)) {
|
|
9605
|
-
const userIdShort = uid.slice(0, 8);
|
|
9606
|
-
console.error(
|
|
9607
|
-
`[${input.tag}] direction=users-without-account userId=${userIdShort} source=${input.usersFile}`
|
|
9608
|
-
);
|
|
9609
9624
|
result.usersWithoutAccount.push({ userId: uid });
|
|
9610
9625
|
result.divergences += 1;
|
|
9611
9626
|
}
|
|
9612
9627
|
}
|
|
9613
|
-
console.error(`[${input.tag}] check complete divergences=${result.divergences}`);
|
|
9614
9628
|
return result;
|
|
9615
9629
|
}
|
|
9630
|
+
function checkAdminAuthInvariant(input) {
|
|
9631
|
+
const d = computeAdminStoreDivergence({ usersFile: input.usersFile, accountsDir: input.accountsDir });
|
|
9632
|
+
for (const e of d.errors) {
|
|
9633
|
+
console.error(`[${input.tag}] unreadable source=${e.source} error=${e.detail}`);
|
|
9634
|
+
}
|
|
9635
|
+
for (const a of d.accountWithoutUsers) {
|
|
9636
|
+
console.error(`[${input.tag}] direction=account-without-users userId=${a.userId.slice(0, 8)} source=${a.source}`);
|
|
9637
|
+
}
|
|
9638
|
+
for (const u of d.usersWithoutAccount) {
|
|
9639
|
+
console.error(`[${input.tag}] direction=users-without-account userId=${u.userId.slice(0, 8)} source=${input.usersFile}`);
|
|
9640
|
+
}
|
|
9641
|
+
console.error(`[${input.tag}] check complete divergences=${d.divergences}`);
|
|
9642
|
+
return {
|
|
9643
|
+
divergences: d.divergences,
|
|
9644
|
+
accountWithoutUsers: d.accountWithoutUsers,
|
|
9645
|
+
usersWithoutAccount: d.usersWithoutAccount
|
|
9646
|
+
};
|
|
9647
|
+
}
|
|
9616
9648
|
|
|
9617
9649
|
// server/routes/onboarding.ts
|
|
9618
9650
|
function hashPin2(pin) {
|
|
@@ -9767,7 +9799,8 @@ app9.post("/set-pin", async (c) => {
|
|
|
9767
9799
|
role: "owner",
|
|
9768
9800
|
usersFile: USERS_FILE,
|
|
9769
9801
|
accountDir: account.accountDir,
|
|
9770
|
-
caller: "set-pin"
|
|
9802
|
+
caller: "set-pin",
|
|
9803
|
+
audit: { actor: userId, logFile: USERS_AUDIT_LOG }
|
|
9771
9804
|
});
|
|
9772
9805
|
if (result.usersJsonResult !== "ok") {
|
|
9773
9806
|
console.error(`[set-pin] users.json write failed: ${result.usersError ?? "unknown"}`);
|
|
@@ -9826,7 +9859,12 @@ ${body.pin}
|
|
|
9826
9859
|
}
|
|
9827
9860
|
return c.json({ ok: true });
|
|
9828
9861
|
});
|
|
9829
|
-
app9.delete("/set-pin", async (c) => {
|
|
9862
|
+
app9.delete("/set-pin", requireAdminSession, async (c) => {
|
|
9863
|
+
const cacheKey = c.get("cacheKey");
|
|
9864
|
+
const sessionUserId = cacheKey ? getUserIdForSession(cacheKey) : void 0;
|
|
9865
|
+
if (!sessionUserId) {
|
|
9866
|
+
return c.json({ error: "Session has no user binding." }, 401);
|
|
9867
|
+
}
|
|
9830
9868
|
let users = null;
|
|
9831
9869
|
try {
|
|
9832
9870
|
users = readUsersFile2();
|
|
@@ -9844,26 +9882,120 @@ app9.delete("/set-pin", async (c) => {
|
|
|
9844
9882
|
return c.json({ error: "Invalid request" }, 400);
|
|
9845
9883
|
}
|
|
9846
9884
|
if (!body.currentPin) return c.json({ error: "Current PIN required." }, 400);
|
|
9847
|
-
const
|
|
9848
|
-
|
|
9849
|
-
|
|
9850
|
-
|
|
9885
|
+
const ownRow = users.find((u) => u.userId === sessionUserId);
|
|
9886
|
+
if (!ownRow) {
|
|
9887
|
+
console.log(`[set-pin] DELETE no row for session user: userId=${sessionUserId.slice(0, 8)}\u2026`);
|
|
9888
|
+
return c.json({ error: "No PIN configured for this user." }, 404);
|
|
9889
|
+
}
|
|
9890
|
+
if (ownRow.pin !== hashPin2(body.currentPin)) {
|
|
9891
|
+
console.log(`[set-pin] DELETE PIN mismatch for session user: userId=${sessionUserId.slice(0, 8)}\u2026`);
|
|
9851
9892
|
return c.json({ error: "Current PIN is incorrect." }, 401);
|
|
9852
9893
|
}
|
|
9853
|
-
const remaining = users.filter((u) => u.userId !==
|
|
9894
|
+
const remaining = users.filter((u) => u.userId !== sessionUserId);
|
|
9854
9895
|
if (remaining.length === 0) {
|
|
9855
9896
|
unlinkSync2(USERS_FILE);
|
|
9856
|
-
console.log(`[set-pin] cleared users.json (last entry removed): userId=${
|
|
9897
|
+
console.log(`[set-pin] cleared users.json (last entry removed): userId=${sessionUserId.slice(0, 8)}\u2026`);
|
|
9857
9898
|
} else {
|
|
9858
9899
|
writeFileSync10(USERS_FILE, JSON.stringify(remaining), { mode: 384 });
|
|
9859
|
-
console.log(`[set-pin] removed entry from users.json: userId=${
|
|
9900
|
+
console.log(`[set-pin] removed entry from users.json: userId=${sessionUserId.slice(0, 8)}\u2026 remaining=${remaining.length}`);
|
|
9901
|
+
}
|
|
9902
|
+
logUsersAudit(
|
|
9903
|
+
{ actor: sessionUserId, logFile: USERS_AUDIT_LOG },
|
|
9904
|
+
{ action: "remove", field: "row", rowsBefore: formatAuditRowIds(users.map((u) => u.userId)), rowsAfter: formatAuditRowIds(remaining.map((u) => u.userId)) }
|
|
9905
|
+
);
|
|
9906
|
+
return c.json({ ok: true });
|
|
9907
|
+
});
|
|
9908
|
+
app9.put("/set-pin", requireAdminSession, async (c) => {
|
|
9909
|
+
const cacheKey = c.get("cacheKey");
|
|
9910
|
+
const sessionUserId = cacheKey ? getUserIdForSession(cacheKey) : void 0;
|
|
9911
|
+
if (!sessionUserId) {
|
|
9912
|
+
return c.json({ error: "Session has no user binding." }, 401);
|
|
9913
|
+
}
|
|
9914
|
+
let body;
|
|
9915
|
+
try {
|
|
9916
|
+
body = await c.req.json();
|
|
9917
|
+
} catch {
|
|
9918
|
+
return c.json({ error: "Invalid request" }, 400);
|
|
9919
|
+
}
|
|
9920
|
+
if (!body.currentPin) return c.json({ error: "Current PIN required." }, 400);
|
|
9921
|
+
if (!body.newPin || body.newPin.length < 4) {
|
|
9922
|
+
return c.json({ error: "New PIN must be at least 4 characters." }, 400);
|
|
9923
|
+
}
|
|
9924
|
+
let users = null;
|
|
9925
|
+
try {
|
|
9926
|
+
users = readUsersFile2();
|
|
9927
|
+
} catch (err) {
|
|
9928
|
+
console.error(`[set-pin] users.json corrupt on PUT: ${err instanceof Error ? err.message : String(err)}`);
|
|
9929
|
+
return c.json({ error: "User configuration is corrupt." }, 400);
|
|
9930
|
+
}
|
|
9931
|
+
if (users === null || users.length === 0) {
|
|
9932
|
+
return c.json({ error: "No PIN configured." }, 404);
|
|
9933
|
+
}
|
|
9934
|
+
const ownRow = users.find((u) => u.userId === sessionUserId);
|
|
9935
|
+
if (!ownRow) {
|
|
9936
|
+
return c.json({ error: "No PIN configured for this user." }, 404);
|
|
9937
|
+
}
|
|
9938
|
+
if (ownRow.pin !== hashPin2(body.currentPin)) {
|
|
9939
|
+
console.log(`[set-pin] PUT PIN mismatch for session user: userId=${sessionUserId.slice(0, 8)}\u2026`);
|
|
9940
|
+
return c.json({ error: "Current PIN is incorrect." }, 401);
|
|
9941
|
+
}
|
|
9942
|
+
const account = resolveAccount();
|
|
9943
|
+
if (!account) {
|
|
9944
|
+
console.error(`[set-pin] PUT no account resolvable, refusing in-place PIN write`);
|
|
9945
|
+
return c.json({ error: "No account is configured on this device. Re-run the installer." }, 503);
|
|
9946
|
+
}
|
|
9947
|
+
const role = cacheKey && getRoleForSession(cacheKey) === "owner" ? "owner" : "admin";
|
|
9948
|
+
const result = writeAdminEntry({
|
|
9949
|
+
userId: sessionUserId,
|
|
9950
|
+
pin: hashPin2(body.newPin),
|
|
9951
|
+
role,
|
|
9952
|
+
usersFile: USERS_FILE,
|
|
9953
|
+
accountDir: account.accountDir,
|
|
9954
|
+
caller: "change-pin",
|
|
9955
|
+
audit: { actor: sessionUserId, logFile: USERS_AUDIT_LOG }
|
|
9956
|
+
});
|
|
9957
|
+
if (result.usersJsonResult !== "ok") {
|
|
9958
|
+
console.error(`[set-pin] PUT users.json write failed: ${result.usersError ?? "unknown"}`);
|
|
9959
|
+
return c.json({ error: "Failed to write user configuration. See server log." }, 500);
|
|
9960
|
+
}
|
|
9961
|
+
console.log(`[set-pin] changed PIN in place: userId=${sessionUserId.slice(0, 8)}\u2026`);
|
|
9962
|
+
if (process.platform === "linux") {
|
|
9963
|
+
let installOwner = null;
|
|
9964
|
+
if (existsSync14(INSTALL_OWNER_FILE)) {
|
|
9965
|
+
try {
|
|
9966
|
+
const raw = readFileSync22(INSTALL_OWNER_FILE, "utf-8").trim();
|
|
9967
|
+
if (raw.length > 0) installOwner = raw;
|
|
9968
|
+
} catch (err) {
|
|
9969
|
+
console.error(`[set-pin] install-owner-read-failed path=${INSTALL_OWNER_FILE} error=${err instanceof Error ? err.message : String(err)}`);
|
|
9970
|
+
}
|
|
9971
|
+
}
|
|
9972
|
+
if (!installOwner) {
|
|
9973
|
+
console.error(`[set-pin] smbpasswd sync skipped owner=<unknown> reason=install-owner-file-missing path=${INSTALL_OWNER_FILE}`);
|
|
9974
|
+
} else {
|
|
9975
|
+
const smbProc = spawnSync2("sudo", ["-n", "smbpasswd", "-a", "-s", installOwner], {
|
|
9976
|
+
input: `${body.newPin}
|
|
9977
|
+
${body.newPin}
|
|
9978
|
+
`,
|
|
9979
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
9980
|
+
encoding: "utf-8",
|
|
9981
|
+
timeout: 1e4
|
|
9982
|
+
});
|
|
9983
|
+
if (smbProc.status === 0) {
|
|
9984
|
+
console.log(`[set-pin] smbpasswd sync ok owner=${installOwner} userId=${sessionUserId.slice(0, 8)}\u2026`);
|
|
9985
|
+
} else {
|
|
9986
|
+
const stderr = (smbProc.stderr ?? "").trim().slice(0, 200);
|
|
9987
|
+
console.error(`[set-pin] smbpasswd sync failed owner=${installOwner} rc=${smbProc.status} stderr=${JSON.stringify(stderr)}`);
|
|
9988
|
+
}
|
|
9989
|
+
}
|
|
9990
|
+
} else {
|
|
9991
|
+
console.log(`[set-pin] smb-password-sync-skipped reason=non-linux platform=${process.platform}`);
|
|
9860
9992
|
}
|
|
9861
9993
|
return c.json({ ok: true });
|
|
9862
9994
|
});
|
|
9863
9995
|
var onboarding_default = app9;
|
|
9864
9996
|
|
|
9865
9997
|
// server/routes/client-error.ts
|
|
9866
|
-
import { appendFileSync, existsSync as existsSync15, renameSync as renameSync6, statSync as statSync8 } from "fs";
|
|
9998
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync15, renameSync as renameSync6, statSync as statSync8 } from "fs";
|
|
9867
9999
|
import { join as join21 } from "path";
|
|
9868
10000
|
var CLIENT_ERRORS_LOG = join21(LOG_DIR, "client-errors.log");
|
|
9869
10001
|
var MAX_LOG_SIZE = 10 * 1024 * 1024;
|
|
@@ -10012,7 +10144,7 @@ app10.post("/", async (c) => {
|
|
|
10012
10144
|
tag: typeof body.tag === "string" ? truncate(body.tag, 32) : void 0,
|
|
10013
10145
|
status: typeof body.status === "number" ? body.status : void 0
|
|
10014
10146
|
};
|
|
10015
|
-
|
|
10147
|
+
appendFileSync2(CLIENT_ERRORS_LOG, JSON.stringify(payload) + "\n", "utf-8");
|
|
10016
10148
|
} catch (err) {
|
|
10017
10149
|
console.error(`[client-error] append failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
10018
10150
|
}
|
|
@@ -10511,7 +10643,7 @@ import { existsSync as existsSync20, mkdirSync as mkdirSync5, createWriteStream
|
|
|
10511
10643
|
async function purgeAdminConversationJsonls(args) {
|
|
10512
10644
|
const fetchImpl = args.fetchImpl ?? fetch;
|
|
10513
10645
|
const log = args.logger ?? ((line) => console.log(line));
|
|
10514
|
-
const
|
|
10646
|
+
const id82 = args.sessionId.slice(0, 8);
|
|
10515
10647
|
const purged = [];
|
|
10516
10648
|
for (const sessionId of args.claudeSessionIds) {
|
|
10517
10649
|
const url = `${args.managerBase}/${encodeURIComponent(sessionId)}`;
|
|
@@ -10521,7 +10653,7 @@ async function purgeAdminConversationJsonls(args) {
|
|
|
10521
10653
|
} catch (err) {
|
|
10522
10654
|
const message2 = err instanceof Error ? err.message : String(err);
|
|
10523
10655
|
log(
|
|
10524
|
-
`[admin-conversation-delete] sessionId=${
|
|
10656
|
+
`[admin-conversation-delete] sessionId=${id82} reason=jsonl-purge-failed claudeSessionId=${sessionId.slice(0, 8)} message=${message2}`
|
|
10525
10657
|
);
|
|
10526
10658
|
return {
|
|
10527
10659
|
ok: false,
|
|
@@ -10538,7 +10670,7 @@ async function purgeAdminConversationJsonls(args) {
|
|
|
10538
10670
|
}
|
|
10539
10671
|
if (res.status === 409) {
|
|
10540
10672
|
log(
|
|
10541
|
-
`[admin-conversation-delete] sessionId=${
|
|
10673
|
+
`[admin-conversation-delete] sessionId=${id82} reason=pty-still-alive claudeSessionId=${sessionId.slice(0, 8)}`
|
|
10542
10674
|
);
|
|
10543
10675
|
return {
|
|
10544
10676
|
ok: false,
|
|
@@ -10555,7 +10687,7 @@ async function purgeAdminConversationJsonls(args) {
|
|
|
10555
10687
|
} catch {
|
|
10556
10688
|
}
|
|
10557
10689
|
log(
|
|
10558
|
-
`[admin-conversation-delete] sessionId=${
|
|
10690
|
+
`[admin-conversation-delete] sessionId=${id82} reason=jsonl-purge-failed claudeSessionId=${sessionId.slice(0, 8)} message=${message}`
|
|
10559
10691
|
);
|
|
10560
10692
|
return {
|
|
10561
10693
|
ok: false,
|
|
@@ -10574,13 +10706,13 @@ async function cascadeAdminConversationDelete(args) {
|
|
|
10574
10706
|
const started = Date.now();
|
|
10575
10707
|
const fetchImpl = args.fetchImpl ?? fetch;
|
|
10576
10708
|
const log = args.logger ?? ((line) => console.log(line));
|
|
10577
|
-
const
|
|
10709
|
+
const id82 = args.sessionId.slice(0, 8);
|
|
10578
10710
|
let claudeSessionIds;
|
|
10579
10711
|
try {
|
|
10580
10712
|
claudeSessionIds = args.claudeSessionIds ?? await getConversationClaudeSessionIds(args.sessionId, args.accountId);
|
|
10581
10713
|
} catch (err) {
|
|
10582
10714
|
const message = err instanceof Error ? err.message : String(err);
|
|
10583
|
-
log(`[admin-conversation-delete] sessionId=${
|
|
10715
|
+
log(`[admin-conversation-delete] sessionId=${id82} reason=graph-delete-failed claudeSessionIds-purged= message=transcript-resolve:${message}`);
|
|
10584
10716
|
return { ok: false, reason: "graph-delete-failed", claudeSessionIdsPurged: [], message };
|
|
10585
10717
|
}
|
|
10586
10718
|
const purgeOutcome = await purgeAdminConversationJsonls({
|
|
@@ -10611,7 +10743,7 @@ async function cascadeAdminConversationDelete(args) {
|
|
|
10611
10743
|
} catch (err) {
|
|
10612
10744
|
const message = err instanceof Error ? err.message : String(err);
|
|
10613
10745
|
const csv2 = purged.map((s) => s.slice(0, 8)).join(",");
|
|
10614
|
-
log(`[admin-conversation-delete] sessionId=${
|
|
10746
|
+
log(`[admin-conversation-delete] sessionId=${id82} reason=graph-delete-failed claudeSessionIds-purged=${csv2} message=${message}`);
|
|
10615
10747
|
return {
|
|
10616
10748
|
ok: false,
|
|
10617
10749
|
reason: "graph-delete-failed",
|
|
@@ -10624,7 +10756,7 @@ async function cascadeAdminConversationDelete(args) {
|
|
|
10624
10756
|
}
|
|
10625
10757
|
const ms = Date.now() - started;
|
|
10626
10758
|
const csv = purged.map((s) => s.slice(0, 8)).join(",");
|
|
10627
|
-
log(`[admin-conversation-delete] sessionId=${
|
|
10759
|
+
log(`[admin-conversation-delete] sessionId=${id82} claudeSessionIds=${csv} ms=${ms}`);
|
|
10628
10760
|
return { ok: true, claudeSessionIds: purged, ms };
|
|
10629
10761
|
}
|
|
10630
10762
|
|
|
@@ -16607,955 +16739,336 @@ app38.get("/", async (c) => {
|
|
|
16607
16739
|
});
|
|
16608
16740
|
var public_session_context_default = app38;
|
|
16609
16741
|
|
|
16610
|
-
// app/lib/
|
|
16611
|
-
|
|
16612
|
-
|
|
16613
|
-
|
|
16614
|
-
import { randomUUID as randomUUID13 } from "crypto";
|
|
16615
|
-
var TAG29 = "[public-session-review]";
|
|
16616
|
-
var CONSUMED_CAP = 500;
|
|
16617
|
-
var consumed = /* @__PURE__ */ new Set();
|
|
16618
|
-
function consumeOnce(sessionId) {
|
|
16619
|
-
if (consumed.has(sessionId)) return false;
|
|
16620
|
-
consumed.add(sessionId);
|
|
16621
|
-
if (consumed.size > CONSUMED_CAP) {
|
|
16622
|
-
const oldest = consumed.values().next().value;
|
|
16623
|
-
if (oldest !== void 0) consumed.delete(oldest);
|
|
16624
|
-
}
|
|
16625
|
-
return true;
|
|
16742
|
+
// app/lib/webchat/gateway/instance.ts
|
|
16743
|
+
var instance2 = null;
|
|
16744
|
+
function setWebchatGateway(gw) {
|
|
16745
|
+
instance2 = gw;
|
|
16626
16746
|
}
|
|
16627
|
-
function
|
|
16628
|
-
|
|
16629
|
-
return `http://127.0.0.1:${port2}`;
|
|
16747
|
+
function getWebchatGateway() {
|
|
16748
|
+
return instance2;
|
|
16630
16749
|
}
|
|
16631
|
-
|
|
16632
|
-
|
|
16633
|
-
|
|
16750
|
+
|
|
16751
|
+
// server/routes/admin/public-session-exit.ts
|
|
16752
|
+
var TAG29 = "[public-session-exit-route]";
|
|
16753
|
+
function isLoopbackAddr4(addr) {
|
|
16754
|
+
return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
|
|
16634
16755
|
}
|
|
16635
|
-
|
|
16756
|
+
var app39 = new Hono();
|
|
16757
|
+
app39.post("/", async (c) => {
|
|
16758
|
+
const remoteAddr = c.env?.incoming?.socket?.remoteAddress ?? "";
|
|
16759
|
+
if (!isLoopbackAddr4(remoteAddr)) {
|
|
16760
|
+
console.error(`${TAG29} reject reason=non-loopback remoteAddr=${remoteAddr}`);
|
|
16761
|
+
return c.json({ error: "public-session-exit-loopback-only" }, 403);
|
|
16762
|
+
}
|
|
16763
|
+
const xffHeader = c.env?.incoming?.headers?.["x-forwarded-for"];
|
|
16764
|
+
const xffRaw = Array.isArray(xffHeader) ? xffHeader.join(",") : typeof xffHeader === "string" ? xffHeader : "";
|
|
16765
|
+
if (xffRaw.length > 0) {
|
|
16766
|
+
const tokens = xffRaw.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
|
|
16767
|
+
const offender = tokens.find((t) => !isLoopbackAddr4(t));
|
|
16768
|
+
if (offender !== void 0) {
|
|
16769
|
+
console.error(`${TAG29} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
|
|
16770
|
+
return c.json({ error: "public-session-exit-loopback-only" }, 403);
|
|
16771
|
+
}
|
|
16772
|
+
}
|
|
16773
|
+
let body;
|
|
16636
16774
|
try {
|
|
16637
|
-
|
|
16638
|
-
|
|
16639
|
-
return
|
|
16640
|
-
}
|
|
16641
|
-
|
|
16642
|
-
|
|
16643
|
-
|
|
16644
|
-
|
|
16775
|
+
body = await c.req.json();
|
|
16776
|
+
} catch {
|
|
16777
|
+
return c.json({ error: "invalid-json" }, 400);
|
|
16778
|
+
}
|
|
16779
|
+
const sessionId = typeof body.sessionId === "string" ? body.sessionId.trim() : "";
|
|
16780
|
+
if (!sessionId) return c.json({ error: "sessionId required" }, 400);
|
|
16781
|
+
const gateway = getWebchatGateway();
|
|
16782
|
+
if (!gateway) {
|
|
16783
|
+
console.error(`${TAG29} reject reason=gateway-unset sessionId=${sessionId.slice(0, 8)}`);
|
|
16784
|
+
return c.json({ error: "webchat-gateway-unset" }, 503);
|
|
16645
16785
|
}
|
|
16786
|
+
gateway.handlePublicSessionExit(sessionId);
|
|
16787
|
+
return c.json({ ok: true });
|
|
16788
|
+
});
|
|
16789
|
+
var public_session_exit_default = app39;
|
|
16790
|
+
|
|
16791
|
+
// server/routes/admin/access-session-evict.ts
|
|
16792
|
+
var TAG30 = "[access-session-evict]";
|
|
16793
|
+
function isLoopbackAddr5(addr) {
|
|
16794
|
+
return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
|
|
16646
16795
|
}
|
|
16647
|
-
|
|
16648
|
-
|
|
16649
|
-
|
|
16650
|
-
|
|
16651
|
-
|
|
16652
|
-
|
|
16653
|
-
|
|
16654
|
-
|
|
16655
|
-
|
|
16796
|
+
var app40 = new Hono();
|
|
16797
|
+
app40.post("/", async (c) => {
|
|
16798
|
+
const remoteAddr = c.env?.incoming?.socket?.remoteAddress ?? "";
|
|
16799
|
+
if (!isLoopbackAddr5(remoteAddr)) {
|
|
16800
|
+
console.error(`${TAG30} reject reason=non-loopback remoteAddr=${remoteAddr}`);
|
|
16801
|
+
return c.json({ error: "access-session-evict-loopback-only" }, 403);
|
|
16802
|
+
}
|
|
16803
|
+
const xffHeader = c.env?.incoming?.headers?.["x-forwarded-for"];
|
|
16804
|
+
const xffRaw = Array.isArray(xffHeader) ? xffHeader.join(",") : typeof xffHeader === "string" ? xffHeader : "";
|
|
16805
|
+
if (xffRaw.length > 0) {
|
|
16806
|
+
const tokens = xffRaw.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
|
|
16807
|
+
const offender = tokens.find((t) => !isLoopbackAddr5(t));
|
|
16808
|
+
if (offender !== void 0) {
|
|
16809
|
+
console.error(`${TAG30} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
|
|
16810
|
+
return c.json({ error: "access-session-evict-loopback-only" }, 403);
|
|
16656
16811
|
}
|
|
16657
16812
|
}
|
|
16658
|
-
|
|
16659
|
-
}
|
|
16660
|
-
async function fetchPriorWrites(sliceToken, accountId) {
|
|
16661
|
-
const url = `${uiBase()}/api/admin/public-session-context?sliceToken=${encodeURIComponent(sliceToken)}&accountId=${encodeURIComponent(accountId)}`;
|
|
16813
|
+
let body;
|
|
16662
16814
|
try {
|
|
16663
|
-
|
|
16664
|
-
|
|
16665
|
-
|
|
16666
|
-
|
|
16667
|
-
|
|
16668
|
-
|
|
16815
|
+
body = await c.req.json();
|
|
16816
|
+
} catch {
|
|
16817
|
+
return c.json({ error: "invalid-json" }, 400);
|
|
16818
|
+
}
|
|
16819
|
+
const grantId = typeof body.grantId === "string" ? body.grantId.trim() : "";
|
|
16820
|
+
if (!grantId) return c.json({ error: "grantId required" }, 400);
|
|
16821
|
+
const dropped = evictAccessSessionsByGrant(grantId);
|
|
16822
|
+
console.log(`${TAG30} grantId=${grantId} dropped=${dropped}`);
|
|
16823
|
+
return c.json({ ok: true, dropped });
|
|
16824
|
+
});
|
|
16825
|
+
var access_session_evict_default = app40;
|
|
16826
|
+
|
|
16827
|
+
// server/routes/admin/enrol-person.ts
|
|
16828
|
+
var TAG31 = "[enrol]";
|
|
16829
|
+
function isLoopbackAddr6(addr) {
|
|
16830
|
+
return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
|
|
16831
|
+
}
|
|
16832
|
+
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
16833
|
+
var app41 = new Hono();
|
|
16834
|
+
app41.post("/", async (c) => {
|
|
16835
|
+
const remoteAddr = c.env?.incoming?.socket?.remoteAddress ?? "";
|
|
16836
|
+
if (!isLoopbackAddr6(remoteAddr)) {
|
|
16837
|
+
console.error(`${TAG31} reject reason=non-loopback remoteAddr=${remoteAddr}`);
|
|
16838
|
+
return c.json({ error: "enrol-person-loopback-only" }, 403);
|
|
16839
|
+
}
|
|
16840
|
+
const xffHeader = c.env?.incoming?.headers?.["x-forwarded-for"];
|
|
16841
|
+
const xffRaw = Array.isArray(xffHeader) ? xffHeader.join(",") : typeof xffHeader === "string" ? xffHeader : "";
|
|
16842
|
+
if (xffRaw.length > 0) {
|
|
16843
|
+
const tokens = xffRaw.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
|
|
16844
|
+
const offender = tokens.find((t) => !isLoopbackAddr6(t));
|
|
16845
|
+
if (offender !== void 0) {
|
|
16846
|
+
console.error(`${TAG31} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
|
|
16847
|
+
return c.json({ error: "enrol-person-loopback-only" }, 403);
|
|
16669
16848
|
}
|
|
16670
|
-
|
|
16671
|
-
|
|
16849
|
+
}
|
|
16850
|
+
let body;
|
|
16851
|
+
try {
|
|
16852
|
+
body = await c.req.json();
|
|
16853
|
+
} catch {
|
|
16854
|
+
return c.json({ error: "invalid-json" }, 400);
|
|
16855
|
+
}
|
|
16856
|
+
const rawPhone = typeof body.phone === "string" ? body.phone : "";
|
|
16857
|
+
const phone = normalizeE164(rawPhone);
|
|
16858
|
+
if (phone.length <= 1) {
|
|
16859
|
+
return c.json({ error: "invalid-phone" }, 400);
|
|
16860
|
+
}
|
|
16861
|
+
const email = typeof body.email === "string" ? body.email.trim().toLowerCase() : "";
|
|
16862
|
+
if (!EMAIL_RE.test(email)) {
|
|
16863
|
+
return c.json({ error: "invalid-email" }, 400);
|
|
16864
|
+
}
|
|
16865
|
+
const agentSlug = typeof body.agentSlug === "string" ? body.agentSlug.trim() : "";
|
|
16866
|
+
if (!agentSlug) {
|
|
16867
|
+
return c.json({ error: "agentSlug required" }, 400);
|
|
16868
|
+
}
|
|
16869
|
+
const accountId = process.env.ACCOUNT_ID ?? "";
|
|
16870
|
+
if (!accountId) {
|
|
16871
|
+
console.error(`${TAG31} op=person result=no-account-id`);
|
|
16872
|
+
return c.json({ error: "no-account-id" }, 500);
|
|
16873
|
+
}
|
|
16874
|
+
let personId;
|
|
16875
|
+
try {
|
|
16876
|
+
personId = await enrolPerson({ accountId, phone, email, agentSlug });
|
|
16672
16877
|
} catch (err) {
|
|
16673
16878
|
console.error(
|
|
16674
|
-
`${
|
|
16879
|
+
`${TAG31} op=person result=write-failed agentSlug=${agentSlug} contact=${maskContact(email)} err="${err instanceof Error ? err.message : String(err)}"`
|
|
16675
16880
|
);
|
|
16676
|
-
return
|
|
16881
|
+
return c.json({ error: "enrol-failed" }, 500);
|
|
16677
16882
|
}
|
|
16678
|
-
|
|
16679
|
-
|
|
16680
|
-
|
|
16681
|
-
|
|
16682
|
-
|
|
16683
|
-
|
|
16684
|
-
|
|
16685
|
-
input.sliceToken,
|
|
16686
|
-
"</slice-token>",
|
|
16687
|
-
"",
|
|
16688
|
-
"<person-id>",
|
|
16689
|
-
input.personId ?? "(unknown)",
|
|
16690
|
-
"</person-id>",
|
|
16691
|
-
"",
|
|
16692
|
-
"<prior-writes>",
|
|
16693
|
-
priorJson,
|
|
16694
|
-
"</prior-writes>",
|
|
16695
|
-
"",
|
|
16696
|
-
"<transcript>",
|
|
16697
|
-
input.jsonl,
|
|
16698
|
-
"</transcript>"
|
|
16699
|
-
].join("\n");
|
|
16700
|
-
}
|
|
16701
|
-
async function dispatchReviewer(input, initialMessage) {
|
|
16702
|
-
const sessionId = randomUUID13();
|
|
16703
|
-
console.log(`${TAG29} route target=rc-spawn sessionId=${sessionId.slice(0, 8)} sourceSession=${input.sessionId.slice(0, 8)}`);
|
|
16704
|
-
const spawned = await managerRcSpawn({
|
|
16705
|
-
sessionId,
|
|
16706
|
-
initialMessage,
|
|
16707
|
-
closeAfterTurn: true,
|
|
16708
|
-
sliceToken: input.sliceToken,
|
|
16709
|
-
personId: input.personId ?? void 0,
|
|
16710
|
-
logContext: `public-session-end sourceSession=${input.sessionId.slice(0, 8)}`
|
|
16711
|
-
});
|
|
16712
|
-
if ("error" in spawned) {
|
|
16713
|
-
return { ok: false, reason: `rc-spawn-${spawned.status}-${spawned.error}` };
|
|
16714
|
-
}
|
|
16715
|
-
return { ok: true, managerSessionId: spawned.sessionId };
|
|
16716
|
-
}
|
|
16717
|
-
async function firePublicSessionEndReview(input) {
|
|
16718
|
-
const sliceShort = input.sliceToken.slice(0, 8);
|
|
16719
|
-
const personField = input.personId ?? "none";
|
|
16720
|
-
const dispatchedAt = Date.now();
|
|
16721
|
-
if (consumed.has(input.sessionId)) {
|
|
16722
|
-
console.log(
|
|
16723
|
-
`${TAG29} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-already-reviewed`
|
|
16883
|
+
let grant = null;
|
|
16884
|
+
try {
|
|
16885
|
+
const g = await findGrantByContact(email, agentSlug, accountId);
|
|
16886
|
+
if (g) grant = { grantId: g.grantId, status: g.status, contactValue: g.contactValue };
|
|
16887
|
+
} catch (err) {
|
|
16888
|
+
console.error(
|
|
16889
|
+
`${TAG31} op=person result=grant-lookup-failed contact=${maskContact(email)} err="${err instanceof Error ? err.message : String(err)}"`
|
|
16724
16890
|
);
|
|
16725
|
-
return;
|
|
16726
16891
|
}
|
|
16727
|
-
|
|
16728
|
-
|
|
16729
|
-
|
|
16730
|
-
|
|
16892
|
+
console.log(
|
|
16893
|
+
`${TAG31} op=person personId=${personId} account=${accountId} agentSlug=${agentSlug} contact=${maskContact(email)}`
|
|
16894
|
+
);
|
|
16895
|
+
return c.json({ personId, grant }, 200);
|
|
16896
|
+
});
|
|
16897
|
+
var enrol_person_default = app41;
|
|
16898
|
+
|
|
16899
|
+
// server/routes/admin/browser.ts
|
|
16900
|
+
var app42 = new Hono();
|
|
16901
|
+
app42.post("/launch", requireAdminSession, async (c) => {
|
|
16902
|
+
try {
|
|
16903
|
+
const transport = resolveBrowserTransport(c.req.raw, c.env?.incoming?.socket?.remoteAddress);
|
|
16904
|
+
console.error(`[admin/browser/launch] op=request transport=${transport}`);
|
|
16905
|
+
if (transport === "vnc") {
|
|
16906
|
+
const vncOk = await ensureVnc();
|
|
16907
|
+
console.error(`[admin/browser/launch] op=vnc-ensured ok=${vncOk}`);
|
|
16908
|
+
if (!vncOk) {
|
|
16909
|
+
console.error("[admin/browser/launch] op=vnc-failed");
|
|
16910
|
+
return c.json({ ok: false, error: "VNC failed to start" }, 502);
|
|
16911
|
+
}
|
|
16912
|
+
}
|
|
16913
|
+
const cdpOk = await ensureCdp(transport);
|
|
16914
|
+
console.error(`[admin/browser/launch] op=cdp-ensured ok=${cdpOk}`);
|
|
16915
|
+
if (!cdpOk) {
|
|
16916
|
+
console.error("[admin/browser/launch] op=cdp-failed");
|
|
16917
|
+
return c.json({ ok: false, error: "Chrome failed to start" }, 502);
|
|
16918
|
+
}
|
|
16919
|
+
console.error(`[admin/browser/launch] op=ready transport=${transport}`);
|
|
16920
|
+
return c.json({ ok: true, transport });
|
|
16921
|
+
} catch (err) {
|
|
16922
|
+
console.error("[admin/browser/launch] Failed to start browser:", err);
|
|
16923
|
+
return c.json(
|
|
16924
|
+
{ ok: false, error: err instanceof Error ? err.message : "Unknown error" },
|
|
16925
|
+
500
|
|
16731
16926
|
);
|
|
16732
|
-
return;
|
|
16733
16927
|
}
|
|
16734
|
-
|
|
16735
|
-
|
|
16736
|
-
|
|
16737
|
-
|
|
16738
|
-
|
|
16739
|
-
|
|
16928
|
+
});
|
|
16929
|
+
var browser_default = app42;
|
|
16930
|
+
|
|
16931
|
+
// server/routes/admin/calendar.ts
|
|
16932
|
+
import { existsSync as existsSync26 } from "fs";
|
|
16933
|
+
import { join as join28 } from "path";
|
|
16934
|
+
|
|
16935
|
+
// server/lib/calendar-ics.ts
|
|
16936
|
+
var CRLF = "\r\n";
|
|
16937
|
+
var PRODID = "-//Maxy//Calendar//EN";
|
|
16938
|
+
var MAX_LINE_OCTETS = 75;
|
|
16939
|
+
function escapeText(value) {
|
|
16940
|
+
return value.replace(/\\/g, "\\\\").replace(/;/g, "\\;").replace(/,/g, "\\,").replace(/\r?\n/g, "\\n");
|
|
16941
|
+
}
|
|
16942
|
+
function foldLine(line) {
|
|
16943
|
+
const buf = Buffer.from(line, "utf-8");
|
|
16944
|
+
if (buf.length <= MAX_LINE_OCTETS) return line;
|
|
16945
|
+
const parts = [];
|
|
16946
|
+
let offset = 0;
|
|
16947
|
+
let first = true;
|
|
16948
|
+
while (offset < buf.length) {
|
|
16949
|
+
const maxChunk = first ? MAX_LINE_OCTETS : MAX_LINE_OCTETS - 1;
|
|
16950
|
+
let end = Math.min(offset + maxChunk, buf.length);
|
|
16951
|
+
while (end < buf.length && end > offset && (buf[end] & 192) === 128) end--;
|
|
16952
|
+
const chunk = buf.subarray(offset, end).toString("utf-8");
|
|
16953
|
+
parts.push(first ? chunk : " " + chunk);
|
|
16954
|
+
offset = end;
|
|
16955
|
+
first = false;
|
|
16740
16956
|
}
|
|
16741
|
-
|
|
16742
|
-
|
|
16743
|
-
|
|
16744
|
-
|
|
16745
|
-
|
|
16746
|
-
|
|
16747
|
-
|
|
16748
|
-
|
|
16749
|
-
|
|
16750
|
-
|
|
16751
|
-
|
|
16752
|
-
|
|
16753
|
-
|
|
16957
|
+
return parts.join(CRLF);
|
|
16958
|
+
}
|
|
16959
|
+
function toICSDateTime(iso) {
|
|
16960
|
+
const d = new Date(iso);
|
|
16961
|
+
if (isNaN(d.getTime())) throw new Error(`Invalid date: ${iso}`);
|
|
16962
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
16963
|
+
return `${d.getUTCFullYear()}${pad(d.getUTCMonth() + 1)}${pad(d.getUTCDate())}T${pad(d.getUTCHours())}${pad(d.getUTCMinutes())}${pad(d.getUTCSeconds())}Z`;
|
|
16964
|
+
}
|
|
16965
|
+
function toICSDate(iso) {
|
|
16966
|
+
const d = new Date(iso);
|
|
16967
|
+
if (isNaN(d.getTime())) throw new Error(`Invalid date: ${iso}`);
|
|
16968
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
16969
|
+
return `${d.getUTCFullYear()}${pad(d.getUTCMonth() + 1)}${pad(d.getUTCDate())}`;
|
|
16970
|
+
}
|
|
16971
|
+
function attendeeLine(a) {
|
|
16972
|
+
if (!a.email) return null;
|
|
16973
|
+
const cn = a.name ? `;CN=${escapeText(a.name)}` : "";
|
|
16974
|
+
return `ATTENDEE${cn}:mailto:${a.email}`;
|
|
16975
|
+
}
|
|
16976
|
+
function serializeMeetingICS(m, opts) {
|
|
16977
|
+
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
16978
|
+
const lines = [];
|
|
16979
|
+
lines.push("BEGIN:VCALENDAR");
|
|
16980
|
+
lines.push("VERSION:2.0");
|
|
16981
|
+
lines.push(`PRODID:${PRODID}`);
|
|
16982
|
+
lines.push("CALSCALE:GREGORIAN");
|
|
16983
|
+
lines.push("METHOD:PUBLISH");
|
|
16984
|
+
lines.push("BEGIN:VEVENT");
|
|
16985
|
+
lines.push(`UID:${m.uid ?? m.id}`);
|
|
16986
|
+
lines.push(`DTSTAMP:${toICSDateTime(now.toISOString())}`);
|
|
16987
|
+
if (m.isAllDay) {
|
|
16988
|
+
lines.push(`DTSTART;VALUE=DATE:${toICSDate(m.startsAt)}`);
|
|
16989
|
+
if (m.endsAt) lines.push(`DTEND;VALUE=DATE:${toICSDate(m.endsAt)}`);
|
|
16990
|
+
} else {
|
|
16991
|
+
lines.push(`DTSTART:${toICSDateTime(m.startsAt)}`);
|
|
16992
|
+
if (m.endsAt) lines.push(`DTEND:${toICSDateTime(m.endsAt)}`);
|
|
16754
16993
|
}
|
|
16755
|
-
|
|
16756
|
-
if (
|
|
16757
|
-
|
|
16758
|
-
|
|
16759
|
-
|
|
16760
|
-
|
|
16994
|
+
lines.push(`SUMMARY:${escapeText(m.title ?? "")}`);
|
|
16995
|
+
if (m.location) lines.push(`LOCATION:${escapeText(m.location)}`);
|
|
16996
|
+
if (opts.includeAttendees) {
|
|
16997
|
+
for (const a of m.attendees) {
|
|
16998
|
+
const line = attendeeLine(a);
|
|
16999
|
+
if (line) lines.push(line);
|
|
17000
|
+
}
|
|
16761
17001
|
}
|
|
16762
|
-
|
|
16763
|
-
|
|
16764
|
-
);
|
|
17002
|
+
lines.push("END:VEVENT");
|
|
17003
|
+
lines.push("END:VCALENDAR");
|
|
17004
|
+
return lines.map(foldLine).join(CRLF) + CRLF;
|
|
16765
17005
|
}
|
|
16766
|
-
|
|
16767
|
-
|
|
16768
|
-
|
|
16769
|
-
|
|
17006
|
+
function countVEvents(ics) {
|
|
17007
|
+
const unfolded = ics.replace(/\r?\n[ \t]/g, "");
|
|
17008
|
+
const lines = unfolded.split(/\r?\n/);
|
|
17009
|
+
let count = 0;
|
|
17010
|
+
let inEvent = false;
|
|
17011
|
+
let nested = 0;
|
|
17012
|
+
for (const raw of lines) {
|
|
17013
|
+
const line = raw.trim();
|
|
17014
|
+
if (line === "BEGIN:VEVENT") {
|
|
17015
|
+
inEvent = true;
|
|
17016
|
+
nested = 0;
|
|
17017
|
+
continue;
|
|
17018
|
+
}
|
|
17019
|
+
if (!inEvent) continue;
|
|
17020
|
+
if (line === "END:VEVENT") {
|
|
17021
|
+
inEvent = false;
|
|
17022
|
+
count++;
|
|
17023
|
+
continue;
|
|
17024
|
+
}
|
|
17025
|
+
if (line.startsWith("BEGIN:")) nested++;
|
|
17026
|
+
else if (line.startsWith("END:") && nested > 0) nested--;
|
|
17027
|
+
}
|
|
17028
|
+
return count;
|
|
16770
17029
|
}
|
|
16771
|
-
|
|
16772
|
-
|
|
16773
|
-
}
|
|
16774
|
-
|
|
16775
|
-
|
|
16776
|
-
|
|
16777
|
-
|
|
16778
|
-
|
|
16779
|
-
|
|
16780
|
-
|
|
16781
|
-
|
|
16782
|
-
|
|
16783
|
-
|
|
16784
|
-
|
|
16785
|
-
|
|
16786
|
-
|
|
16787
|
-
|
|
16788
|
-
|
|
16789
|
-
|
|
16790
|
-
|
|
16791
|
-
|
|
16792
|
-
|
|
16793
|
-
|
|
16794
|
-
|
|
16795
|
-
|
|
16796
|
-
|
|
16797
|
-
|
|
16798
|
-
|
|
16799
|
-
|
|
16800
|
-
|
|
16801
|
-
|
|
16802
|
-
|
|
16803
|
-
|
|
16804
|
-
|
|
16805
|
-
|
|
16806
|
-
|
|
16807
|
-
|
|
16808
|
-
|
|
16809
|
-
|
|
16810
|
-
|
|
16811
|
-
|
|
16812
|
-
|
|
16813
|
-
const decoder = new TextDecoder("utf8");
|
|
16814
|
-
let buffered = "";
|
|
16815
|
-
const fileDelivery = opts.fileDelivery ?? null;
|
|
16816
|
-
let firedFileTools = [];
|
|
16817
|
-
let suppressing = opts.suppressResumeReplay === true;
|
|
16818
|
-
let discardedTurns = 0;
|
|
16819
|
-
let discardedFileTools = 0;
|
|
16820
|
-
while (!abort.signal.aborted) {
|
|
16821
|
-
const { value, done } = await reader.read();
|
|
16822
|
-
if (done) break;
|
|
16823
|
-
buffered += decoder.decode(value, { stream: true });
|
|
16824
|
-
let nl = buffered.indexOf("\n");
|
|
16825
|
-
while (nl !== -1) {
|
|
16826
|
-
const line = buffered.slice(0, nl);
|
|
16827
|
-
buffered = buffered.slice(nl + 1);
|
|
16828
|
-
nl = buffered.indexOf("\n");
|
|
16829
|
-
if (!line) continue;
|
|
16830
|
-
let event;
|
|
16831
|
-
try {
|
|
16832
|
-
event = JSON.parse(line);
|
|
16833
|
-
} catch (err) {
|
|
16834
|
-
console.error(
|
|
16835
|
-
`${tag} jsonl-parse-skip sessionId=${entry.sessionId.slice(0, 8)} bytes=${line.length} message=${err instanceof Error ? err.message : String(err)}`
|
|
16836
|
-
);
|
|
16837
|
-
continue;
|
|
16838
|
-
}
|
|
16839
|
-
if (event.type === "__channel_resume_boundary__") {
|
|
16840
|
-
if (suppressing) {
|
|
16841
|
-
suppressing = false;
|
|
16842
|
-
console.error(
|
|
16843
|
-
`${tag} follower-resume-boundary sessionId=${sid} discarded-head-turns=${discardedTurns} discarded-head-filetools=${discardedFileTools}`
|
|
16844
|
-
);
|
|
16845
|
-
}
|
|
16846
|
-
continue;
|
|
16847
|
-
}
|
|
16848
|
-
if (event.type === "user") {
|
|
16849
|
-
entry.pendingTurnText = "";
|
|
16850
|
-
firedFileTools = [];
|
|
16851
|
-
continue;
|
|
16852
|
-
}
|
|
16853
|
-
if (event.type !== "assistant") continue;
|
|
16854
|
-
const msg = event.message;
|
|
16855
|
-
if (!msg) continue;
|
|
16856
|
-
let sawContent = false;
|
|
16857
|
-
if (Array.isArray(msg.content)) {
|
|
16858
|
-
for (const block of msg.content) {
|
|
16859
|
-
if (block?.type === "text" && typeof block.text === "string") {
|
|
16860
|
-
entry.pendingTurnText += block.text;
|
|
16861
|
-
sawContent = true;
|
|
16862
|
-
} else if (block?.type === "tool_use") {
|
|
16863
|
-
sawContent = true;
|
|
16864
|
-
if (fileDelivery && typeof block.name === "string" && fileDelivery.isFileDeliveryTool(block.name)) {
|
|
16865
|
-
if (suppressing) {
|
|
16866
|
-
discardedFileTools += 1;
|
|
16867
|
-
continue;
|
|
16868
|
-
}
|
|
16869
|
-
firedFileTools.push(block.name);
|
|
16870
|
-
try {
|
|
16871
|
-
await fileDelivery.onFileToolUse({ toolName: block.name, input: block.input });
|
|
16872
|
-
} catch (err) {
|
|
16873
|
-
console.error(
|
|
16874
|
-
`${tag} file-delivery-error sessionId=${sid} tool=${block.name} message=${err instanceof Error ? err.message : String(err)}`
|
|
16875
|
-
);
|
|
16876
|
-
}
|
|
16877
|
-
}
|
|
16878
|
-
}
|
|
16879
|
-
}
|
|
16880
|
-
}
|
|
16881
|
-
if (msg.stop_reason === "end_turn") {
|
|
16882
|
-
if (suppressing) {
|
|
16883
|
-
entry.pendingTurnText = "";
|
|
16884
|
-
firedFileTools = [];
|
|
16885
|
-
discardedTurns += 1;
|
|
16886
|
-
continue;
|
|
16887
|
-
}
|
|
16888
|
-
const flush = entry.pendingTurnText;
|
|
16889
|
-
entry.pendingTurnText = "";
|
|
16890
|
-
if (flush.trim()) {
|
|
16891
|
-
await fanOut(entry.subscribers, flush, opts.onError, tag);
|
|
16892
|
-
}
|
|
16893
|
-
if (fileDelivery && firedFileTools.length > 0) {
|
|
16894
|
-
const fired = firedFileTools;
|
|
16895
|
-
firedFileTools = [];
|
|
16896
|
-
try {
|
|
16897
|
-
fileDelivery.onTurnEnd(fired);
|
|
16898
|
-
} catch (err) {
|
|
16899
|
-
console.error(
|
|
16900
|
-
`${tag} file-delivery-error sessionId=${sid} phase=turn-end message=${err instanceof Error ? err.message : String(err)}`
|
|
16901
|
-
);
|
|
16902
|
-
}
|
|
16903
|
-
}
|
|
16904
|
-
opts.onTurnComplete?.();
|
|
16905
|
-
} else if (sawContent && !suppressing) {
|
|
16906
|
-
opts.onActivity?.();
|
|
16907
|
-
}
|
|
16908
|
-
}
|
|
16909
|
-
}
|
|
16910
|
-
} catch (err) {
|
|
16911
|
-
if (!abort.signal.aborted) {
|
|
16912
|
-
opts.onError?.(`follower-error: ${err instanceof Error ? err.message : String(err)}`);
|
|
16913
|
-
}
|
|
16914
|
-
} finally {
|
|
16915
|
-
opts.onClose();
|
|
16916
|
-
}
|
|
16917
|
-
})();
|
|
16918
|
-
return abort;
|
|
16919
|
-
}
|
|
16920
|
-
async function fanOut(subscribers, text, onError, tag) {
|
|
16921
|
-
const callbacks = Array.from(subscribers);
|
|
16922
|
-
await Promise.all(
|
|
16923
|
-
callbacks.map(
|
|
16924
|
-
(cb) => Promise.resolve().then(() => cb(text)).catch((err) => {
|
|
16925
|
-
const m = err instanceof Error ? err.message : String(err);
|
|
16926
|
-
console.error(`${tag} subscriber-error message=${m}`);
|
|
16927
|
-
onError?.(`subscriber-error: ${m}`);
|
|
16928
|
-
})
|
|
16929
|
-
)
|
|
16930
|
-
);
|
|
16931
|
-
}
|
|
16932
|
-
|
|
16933
|
-
// app/lib/channel-pty-bridge/file-delivery.ts
|
|
16934
|
-
var SEND_USER_FILE = "SendUserFile";
|
|
16935
|
-
function makeFileDelivery(opts) {
|
|
16936
|
-
const { entry, tag, channel, sendFile } = opts;
|
|
16937
|
-
let failedFiles = [];
|
|
16938
|
-
let attempts = 0;
|
|
16939
|
-
return {
|
|
16940
|
-
isFileDeliveryTool(toolName) {
|
|
16941
|
-
return toolName === SEND_USER_FILE;
|
|
16942
|
-
},
|
|
16943
|
-
async onFileToolUse(use) {
|
|
16944
|
-
if (use.toolName !== SEND_USER_FILE) return;
|
|
16945
|
-
const input = use.input ?? {};
|
|
16946
|
-
const files = Array.isArray(input.files) ? input.files.filter((f) => typeof f === "string") : [];
|
|
16947
|
-
const caption = typeof input.caption === "string" ? input.caption : void 0;
|
|
16948
|
-
for (let i = 0; i < files.length; i++) {
|
|
16949
|
-
attempts++;
|
|
16950
|
-
const result = await sendFile(files[i], i === 0 ? caption : void 0);
|
|
16951
|
-
const name = files[i].split("/").pop() ?? files[i];
|
|
16952
|
-
console.error(`${tag} op=file-forward channel=${channel} file=${name} outcome=${result.ok ? "ok" : "fail"}`);
|
|
16953
|
-
if (!result.ok) failedFiles.push(files[i]);
|
|
16954
|
-
}
|
|
16955
|
-
},
|
|
16956
|
-
onTurnEnd(firedTools) {
|
|
16957
|
-
const failed = failedFiles;
|
|
16958
|
-
const tried = attempts;
|
|
16959
|
-
failedFiles = [];
|
|
16960
|
-
attempts = 0;
|
|
16961
|
-
const sid = entry.sessionId.slice(0, 8);
|
|
16962
|
-
for (const file of failed) {
|
|
16963
|
-
console.error(
|
|
16964
|
-
`${tag} file-delivery-unreconciled sender=${entry.senderId} sessionId=${sid} tool=${SEND_USER_FILE} file=${file}`
|
|
16965
|
-
);
|
|
16966
|
-
}
|
|
16967
|
-
if (firedTools.includes(SEND_USER_FILE) && tried === 0) {
|
|
16968
|
-
console.error(
|
|
16969
|
-
`${tag} file-delivery-unreconciled sender=${entry.senderId} sessionId=${sid} tool=${SEND_USER_FILE}`
|
|
16970
|
-
);
|
|
16971
|
-
}
|
|
16972
|
-
}
|
|
16973
|
-
};
|
|
16974
|
-
}
|
|
16975
|
-
|
|
16976
|
-
// app/lib/whatsapp/inbound/file-delivery-bridge.ts
|
|
16977
|
-
var TAG30 = "[whatsapp-adaptor]";
|
|
16978
|
-
var SEND_USER_FILE2 = "SendUserFile";
|
|
16979
|
-
var WHATSAPP_SEND_DOCUMENT = "whatsapp-send-document";
|
|
16980
|
-
function platformRoot() {
|
|
16981
|
-
return process.env.MAXY_PLATFORM_ROOT || "";
|
|
16982
|
-
}
|
|
16983
|
-
function makeWhatsAppSendFile(entry) {
|
|
16984
|
-
return async (filePath, caption) => {
|
|
16985
|
-
let maxyAccountId;
|
|
16986
|
-
try {
|
|
16987
|
-
maxyAccountId = resolvePlatformAccountId();
|
|
16988
|
-
} catch (err) {
|
|
16989
|
-
console.error(
|
|
16990
|
-
`${TAG30} file-delivery reject reason=account-unresolved sender=${entry.senderId} message=${err instanceof Error ? err.message : String(err)}`
|
|
16991
|
-
);
|
|
16992
|
-
return { ok: false, error: "account-unresolved" };
|
|
16993
|
-
}
|
|
16994
|
-
const result = await sendWhatsAppDocument({
|
|
16995
|
-
to: entry.senderId,
|
|
16996
|
-
filePath,
|
|
16997
|
-
caption,
|
|
16998
|
-
accountId: entry.accountId,
|
|
16999
|
-
maxyAccountId,
|
|
17000
|
-
platformRoot: platformRoot()
|
|
17001
|
-
});
|
|
17002
|
-
if (result.ok) return { ok: true };
|
|
17003
|
-
console.error(
|
|
17004
|
-
`${TAG30} file-delivery reject reason=send-failed sender=${entry.senderId} status=${result.status} message=${result.error}`
|
|
17005
|
-
);
|
|
17006
|
-
return { ok: false, error: result.error };
|
|
17007
|
-
};
|
|
17008
|
-
}
|
|
17009
|
-
function makeWhatsAppFileDelivery(entry) {
|
|
17010
|
-
const shared = makeFileDelivery({
|
|
17011
|
-
entry,
|
|
17012
|
-
tag: TAG30,
|
|
17013
|
-
channel: "whatsapp",
|
|
17014
|
-
sendFile: makeWhatsAppSendFile(entry)
|
|
17015
|
-
});
|
|
17016
|
-
let turnStartedAt = null;
|
|
17017
|
-
let routeCalls = [];
|
|
17018
|
-
return {
|
|
17019
|
-
isFileDeliveryTool(toolName) {
|
|
17020
|
-
return toolName === SEND_USER_FILE2 || toolName === WHATSAPP_SEND_DOCUMENT;
|
|
17021
|
-
},
|
|
17022
|
-
async onFileToolUse(use) {
|
|
17023
|
-
if (turnStartedAt === null) turnStartedAt = Date.now();
|
|
17024
|
-
if (use.toolName === WHATSAPP_SEND_DOCUMENT) {
|
|
17025
|
-
const input = use.input ?? {};
|
|
17026
|
-
routeCalls.push({
|
|
17027
|
-
to: typeof input.to === "string" ? input.to : void 0,
|
|
17028
|
-
filePath: typeof input.filePath === "string" ? input.filePath : void 0
|
|
17029
|
-
});
|
|
17030
|
-
return;
|
|
17031
|
-
}
|
|
17032
|
-
await shared.onFileToolUse(use);
|
|
17033
|
-
},
|
|
17034
|
-
onTurnEnd(firedTools) {
|
|
17035
|
-
const startedAt = turnStartedAt ?? 0;
|
|
17036
|
-
const routes = routeCalls;
|
|
17037
|
-
turnStartedAt = null;
|
|
17038
|
-
routeCalls = [];
|
|
17039
|
-
const sid = entry.sessionId.slice(0, 8);
|
|
17040
|
-
shared.onTurnEnd(firedTools);
|
|
17041
|
-
for (const call of routes) {
|
|
17042
|
-
const routeAt = call.to !== void 0 && call.filePath !== void 0 ? routeDocumentOutboundAt(call.to, call.filePath) : void 0;
|
|
17043
|
-
const delivered = routeAt !== void 0 && routeAt >= startedAt;
|
|
17044
|
-
if (!delivered) {
|
|
17045
|
-
const fileField = call.filePath !== void 0 ? ` file=${call.filePath}` : "";
|
|
17046
|
-
console.error(
|
|
17047
|
-
`${TAG30} file-delivery-unreconciled sender=${entry.senderId} sessionId=${sid} tool=${WHATSAPP_SEND_DOCUMENT}${fileField}`
|
|
17048
|
-
);
|
|
17049
|
-
}
|
|
17050
|
-
}
|
|
17051
|
-
}
|
|
17052
|
-
};
|
|
17053
|
-
}
|
|
17054
|
-
|
|
17055
|
-
// app/lib/telegram/outbound/send-document.ts
|
|
17056
|
-
import { realpathSync as realpathSync5 } from "fs";
|
|
17057
|
-
import { readFile as readFile6, stat as stat6 } from "fs/promises";
|
|
17058
|
-
import { resolve as resolve24, basename as basename9 } from "path";
|
|
17059
|
-
var TAG31 = "[telegram:outbound]";
|
|
17060
|
-
var TELEGRAM_DOCUMENT_MAX_BYTES = 50 * 1024 * 1024;
|
|
17061
|
-
async function sendTelegramDocument(input) {
|
|
17062
|
-
const { botToken, chatId, filePath, caption, maxyAccountId, platformRoot: platformRoot3 } = input;
|
|
17063
|
-
if (!botToken || !filePath) {
|
|
17064
|
-
return { ok: false, status: 400, error: "Missing required fields: botToken, filePath" };
|
|
17065
|
-
}
|
|
17066
|
-
if (!maxyAccountId || !platformRoot3) {
|
|
17067
|
-
return { ok: false, status: 400, error: "Cannot validate file path: missing account or platform context" };
|
|
17068
|
-
}
|
|
17069
|
-
const accountDir = resolve24(platformRoot3, "..", "data/accounts", maxyAccountId);
|
|
17070
|
-
let resolvedPath;
|
|
17071
|
-
try {
|
|
17072
|
-
resolvedPath = realpathSync5(filePath);
|
|
17073
|
-
const accountResolved = realpathSync5(accountDir);
|
|
17074
|
-
if (!resolvedPath.startsWith(accountResolved + "/")) {
|
|
17075
|
-
console.error(`${TAG31} document REJECTED reason=outside_account_directory`);
|
|
17076
|
-
return { ok: false, status: 403, error: "Access denied: file is outside the account directory" };
|
|
17077
|
-
}
|
|
17078
|
-
} catch (err) {
|
|
17079
|
-
const code = err.code;
|
|
17080
|
-
if (code === "ENOENT") {
|
|
17081
|
-
console.error(`${TAG31} document ENOENT path=${filePath}`);
|
|
17082
|
-
return { ok: false, status: 404, error: `File not found: ${filePath}` };
|
|
17083
|
-
}
|
|
17084
|
-
console.error(`${TAG31} document path error: ${String(err)}`);
|
|
17085
|
-
return { ok: false, status: 500, error: String(err) };
|
|
17086
|
-
}
|
|
17087
|
-
const fileStat = await stat6(resolvedPath);
|
|
17088
|
-
if (fileStat.size > TELEGRAM_DOCUMENT_MAX_BYTES) {
|
|
17089
|
-
return {
|
|
17090
|
-
ok: false,
|
|
17091
|
-
status: 400,
|
|
17092
|
-
error: `File exceeds 50 MB limit (${(fileStat.size / 1024 / 1024).toFixed(1)} MB)`
|
|
17093
|
-
};
|
|
17094
|
-
}
|
|
17095
|
-
const filename = basename9(resolvedPath);
|
|
17096
|
-
const buffer = Buffer.from(await readFile6(resolvedPath));
|
|
17097
|
-
const form = new FormData();
|
|
17098
|
-
form.append("chat_id", String(chatId));
|
|
17099
|
-
if (caption) form.append("caption", caption);
|
|
17100
|
-
form.append("document", new File([buffer], filename, { type: detectMimeType(resolvedPath) }));
|
|
17101
|
-
let ok = false;
|
|
17102
|
-
let messageId;
|
|
17103
|
-
let error = "send failed";
|
|
17104
|
-
try {
|
|
17105
|
-
const res = await fetch(`https://api.telegram.org/bot${botToken}/sendDocument`, {
|
|
17106
|
-
method: "POST",
|
|
17107
|
-
body: form
|
|
17108
|
-
});
|
|
17109
|
-
const data = await res.json();
|
|
17110
|
-
ok = data.ok;
|
|
17111
|
-
messageId = data.result?.message_id;
|
|
17112
|
-
if (!data.ok) error = data.description ?? "Unknown Telegram error";
|
|
17113
|
-
} catch (e) {
|
|
17114
|
-
error = e instanceof Error ? e.message : String(e);
|
|
17115
|
-
}
|
|
17116
|
-
console.error(
|
|
17117
|
-
`${TAG31} sent document to=${chatId} file=${filename} bytes=${fileStat.size} ok=${ok}` + (messageId !== void 0 ? ` id=${messageId}` : "")
|
|
17118
|
-
);
|
|
17119
|
-
return ok ? { ok: true, messageId } : { ok: false, status: 500, error };
|
|
17120
|
-
}
|
|
17121
|
-
|
|
17122
|
-
// app/lib/telegram/outbound/file-delivery.ts
|
|
17123
|
-
var TAG32 = "[telegram:outbound]";
|
|
17124
|
-
function platformRoot2() {
|
|
17125
|
-
return process.env.MAXY_PLATFORM_ROOT || "";
|
|
17126
|
-
}
|
|
17127
|
-
function makeTelegramSendFile(entry) {
|
|
17128
|
-
let botToken;
|
|
17129
|
-
return async (filePath, caption) => {
|
|
17130
|
-
if (botToken === void 0) {
|
|
17131
|
-
const tg = resolveAccount()?.config.telegram;
|
|
17132
|
-
botToken = (entry.role === "admin" ? tg?.adminBotToken : tg?.publicBotToken) ?? null;
|
|
17133
|
-
}
|
|
17134
|
-
if (!botToken) {
|
|
17135
|
-
console.error(`${TAG32} file-delivery reject reason=no-bot-token sender=${entry.senderId} role=${entry.role}`);
|
|
17136
|
-
return { ok: false, error: "no-bot-token" };
|
|
17137
|
-
}
|
|
17138
|
-
if (entry.replyTarget == null) {
|
|
17139
|
-
console.error(`${TAG32} file-delivery reject reason=no-reply-target sender=${entry.senderId} role=${entry.role}`);
|
|
17140
|
-
return { ok: false, error: "no-reply-target" };
|
|
17141
|
-
}
|
|
17142
|
-
let maxyAccountId;
|
|
17143
|
-
try {
|
|
17144
|
-
maxyAccountId = resolvePlatformAccountId();
|
|
17145
|
-
} catch (err) {
|
|
17146
|
-
console.error(
|
|
17147
|
-
`${TAG32} file-delivery reject reason=account-unresolved sender=${entry.senderId} message=${err instanceof Error ? err.message : String(err)}`
|
|
17148
|
-
);
|
|
17149
|
-
return { ok: false, error: "account-unresolved" };
|
|
17150
|
-
}
|
|
17151
|
-
const result = await sendTelegramDocument({
|
|
17152
|
-
botToken,
|
|
17153
|
-
chatId: Number(entry.replyTarget),
|
|
17154
|
-
filePath,
|
|
17155
|
-
caption,
|
|
17156
|
-
maxyAccountId,
|
|
17157
|
-
platformRoot: platformRoot2()
|
|
17158
|
-
});
|
|
17159
|
-
return result.ok ? { ok: true } : { ok: false, error: result.error };
|
|
17160
|
-
};
|
|
17161
|
-
}
|
|
17162
|
-
function makeTelegramFileDelivery(entry) {
|
|
17163
|
-
return makeFileDelivery({
|
|
17164
|
-
entry,
|
|
17165
|
-
tag: TAG32,
|
|
17166
|
-
channel: "telegram",
|
|
17167
|
-
sendFile: makeTelegramSendFile(entry)
|
|
17168
|
-
});
|
|
17169
|
-
}
|
|
17170
|
-
|
|
17171
|
-
// app/lib/webchat/file-delivery.ts
|
|
17172
|
-
import { realpathSync as realpathSync6 } from "fs";
|
|
17173
|
-
import { resolve as resolve25 } from "path";
|
|
17174
|
-
|
|
17175
|
-
// app/lib/channel-pty-bridge/bridge.ts
|
|
17176
|
-
function tagFor(channel) {
|
|
17177
|
-
return `[${channel}-adaptor]`;
|
|
17178
|
-
}
|
|
17179
|
-
function publicIdleMs() {
|
|
17180
|
-
return Number(process.env.CHANNEL_PTY_IDLE_MS ?? String(5 * 6e4));
|
|
17181
|
-
}
|
|
17182
|
-
var index = /* @__PURE__ */ new Map();
|
|
17183
|
-
var reaperHandle = null;
|
|
17184
|
-
function startReaper() {
|
|
17185
|
-
if (reaperHandle) return;
|
|
17186
|
-
reaperHandle = setInterval(() => {
|
|
17187
|
-
const now = Date.now();
|
|
17188
|
-
const cutoff = now - publicIdleMs();
|
|
17189
|
-
for (const [key, entry] of index) {
|
|
17190
|
-
if (entry.role !== "public") continue;
|
|
17191
|
-
if (entry.subscribers.size > 0) continue;
|
|
17192
|
-
if (entry.lastInboundAt > cutoff) continue;
|
|
17193
|
-
const idleMs = now - entry.lastInboundAt;
|
|
17194
|
-
const tag = tagFor(entry.channel);
|
|
17195
|
-
console.error(
|
|
17196
|
-
`${tag} reap senderId=${entry.senderId} idle-ms=${idleMs} sessionId=${entry.sessionId.slice(0, 8)}`
|
|
17197
|
-
);
|
|
17198
|
-
if (entry.followerAbort) entry.followerAbort.abort();
|
|
17199
|
-
index.delete(key);
|
|
17200
|
-
if (entry.role === "public" && entry.sliceToken.length > 0) {
|
|
17201
|
-
void firePublicSessionEndReview({
|
|
17202
|
-
sessionId: entry.sessionId,
|
|
17203
|
-
sliceToken: entry.sliceToken,
|
|
17204
|
-
personId: entry.personId,
|
|
17205
|
-
accountId: entry.accountId,
|
|
17206
|
-
agentSlug: entry.agentSlug,
|
|
17207
|
-
dispatchFor: "eviction"
|
|
17208
|
-
});
|
|
17209
|
-
}
|
|
17210
|
-
void managerDelete(entry.sessionId);
|
|
17211
|
-
}
|
|
17212
|
-
}, 6e4);
|
|
17213
|
-
if (reaperHandle.unref) reaperHandle.unref();
|
|
17214
|
-
}
|
|
17215
|
-
function handlePublicSessionExit(sessionId) {
|
|
17216
|
-
let found = null;
|
|
17217
|
-
for (const [key2, entry2] of index) {
|
|
17218
|
-
if (entry2.sessionId === sessionId) {
|
|
17219
|
-
found = { key: key2, entry: entry2 };
|
|
17220
|
-
break;
|
|
17221
|
-
}
|
|
17222
|
-
}
|
|
17223
|
-
if (!found) {
|
|
17224
|
-
console.error(`[public-session-exit] sessionId=${sessionId} result=no-entry`);
|
|
17225
|
-
return;
|
|
17226
|
-
}
|
|
17227
|
-
const { key, entry } = found;
|
|
17228
|
-
if (entry.role !== "public" || entry.sliceToken.length === 0) {
|
|
17229
|
-
return;
|
|
17230
|
-
}
|
|
17231
|
-
if (entry.followerAbort) entry.followerAbort.abort();
|
|
17232
|
-
index.delete(key);
|
|
17233
|
-
void firePublicSessionEndReview({
|
|
17234
|
-
sessionId: entry.sessionId,
|
|
17235
|
-
sliceToken: entry.sliceToken,
|
|
17236
|
-
personId: entry.personId,
|
|
17237
|
-
accountId: entry.accountId,
|
|
17238
|
-
agentSlug: entry.agentSlug,
|
|
17239
|
-
dispatchFor: "exit"
|
|
17240
|
-
});
|
|
17241
|
-
}
|
|
17242
|
-
|
|
17243
|
-
// server/routes/admin/public-session-exit.ts
|
|
17244
|
-
var TAG33 = "[public-session-exit-route]";
|
|
17245
|
-
function isLoopbackAddr4(addr) {
|
|
17246
|
-
return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
|
|
17247
|
-
}
|
|
17248
|
-
var app39 = new Hono();
|
|
17249
|
-
app39.post("/", async (c) => {
|
|
17250
|
-
const remoteAddr = c.env?.incoming?.socket?.remoteAddress ?? "";
|
|
17251
|
-
if (!isLoopbackAddr4(remoteAddr)) {
|
|
17252
|
-
console.error(`${TAG33} reject reason=non-loopback remoteAddr=${remoteAddr}`);
|
|
17253
|
-
return c.json({ error: "public-session-exit-loopback-only" }, 403);
|
|
17254
|
-
}
|
|
17255
|
-
const xffHeader = c.env?.incoming?.headers?.["x-forwarded-for"];
|
|
17256
|
-
const xffRaw = Array.isArray(xffHeader) ? xffHeader.join(",") : typeof xffHeader === "string" ? xffHeader : "";
|
|
17257
|
-
if (xffRaw.length > 0) {
|
|
17258
|
-
const tokens = xffRaw.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
|
|
17259
|
-
const offender = tokens.find((t) => !isLoopbackAddr4(t));
|
|
17260
|
-
if (offender !== void 0) {
|
|
17261
|
-
console.error(`${TAG33} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
|
|
17262
|
-
return c.json({ error: "public-session-exit-loopback-only" }, 403);
|
|
17263
|
-
}
|
|
17264
|
-
}
|
|
17265
|
-
let body;
|
|
17266
|
-
try {
|
|
17267
|
-
body = await c.req.json();
|
|
17268
|
-
} catch {
|
|
17269
|
-
return c.json({ error: "invalid-json" }, 400);
|
|
17270
|
-
}
|
|
17271
|
-
const sessionId = typeof body.sessionId === "string" ? body.sessionId.trim() : "";
|
|
17272
|
-
if (!sessionId) return c.json({ error: "sessionId required" }, 400);
|
|
17273
|
-
handlePublicSessionExit(sessionId);
|
|
17274
|
-
return c.json({ ok: true });
|
|
17275
|
-
});
|
|
17276
|
-
var public_session_exit_default = app39;
|
|
17277
|
-
|
|
17278
|
-
// server/routes/admin/access-session-evict.ts
|
|
17279
|
-
var TAG34 = "[access-session-evict]";
|
|
17280
|
-
function isLoopbackAddr5(addr) {
|
|
17281
|
-
return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
|
|
17282
|
-
}
|
|
17283
|
-
var app40 = new Hono();
|
|
17284
|
-
app40.post("/", async (c) => {
|
|
17285
|
-
const remoteAddr = c.env?.incoming?.socket?.remoteAddress ?? "";
|
|
17286
|
-
if (!isLoopbackAddr5(remoteAddr)) {
|
|
17287
|
-
console.error(`${TAG34} reject reason=non-loopback remoteAddr=${remoteAddr}`);
|
|
17288
|
-
return c.json({ error: "access-session-evict-loopback-only" }, 403);
|
|
17289
|
-
}
|
|
17290
|
-
const xffHeader = c.env?.incoming?.headers?.["x-forwarded-for"];
|
|
17291
|
-
const xffRaw = Array.isArray(xffHeader) ? xffHeader.join(",") : typeof xffHeader === "string" ? xffHeader : "";
|
|
17292
|
-
if (xffRaw.length > 0) {
|
|
17293
|
-
const tokens = xffRaw.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
|
|
17294
|
-
const offender = tokens.find((t) => !isLoopbackAddr5(t));
|
|
17295
|
-
if (offender !== void 0) {
|
|
17296
|
-
console.error(`${TAG34} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
|
|
17297
|
-
return c.json({ error: "access-session-evict-loopback-only" }, 403);
|
|
17298
|
-
}
|
|
17299
|
-
}
|
|
17300
|
-
let body;
|
|
17301
|
-
try {
|
|
17302
|
-
body = await c.req.json();
|
|
17303
|
-
} catch {
|
|
17304
|
-
return c.json({ error: "invalid-json" }, 400);
|
|
17305
|
-
}
|
|
17306
|
-
const grantId = typeof body.grantId === "string" ? body.grantId.trim() : "";
|
|
17307
|
-
if (!grantId) return c.json({ error: "grantId required" }, 400);
|
|
17308
|
-
const dropped = evictAccessSessionsByGrant(grantId);
|
|
17309
|
-
console.log(`${TAG34} grantId=${grantId} dropped=${dropped}`);
|
|
17310
|
-
return c.json({ ok: true, dropped });
|
|
17311
|
-
});
|
|
17312
|
-
var access_session_evict_default = app40;
|
|
17313
|
-
|
|
17314
|
-
// server/routes/admin/enrol-person.ts
|
|
17315
|
-
var TAG35 = "[enrol]";
|
|
17316
|
-
function isLoopbackAddr6(addr) {
|
|
17317
|
-
return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
|
|
17318
|
-
}
|
|
17319
|
-
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
17320
|
-
var app41 = new Hono();
|
|
17321
|
-
app41.post("/", async (c) => {
|
|
17322
|
-
const remoteAddr = c.env?.incoming?.socket?.remoteAddress ?? "";
|
|
17323
|
-
if (!isLoopbackAddr6(remoteAddr)) {
|
|
17324
|
-
console.error(`${TAG35} reject reason=non-loopback remoteAddr=${remoteAddr}`);
|
|
17325
|
-
return c.json({ error: "enrol-person-loopback-only" }, 403);
|
|
17326
|
-
}
|
|
17327
|
-
const xffHeader = c.env?.incoming?.headers?.["x-forwarded-for"];
|
|
17328
|
-
const xffRaw = Array.isArray(xffHeader) ? xffHeader.join(",") : typeof xffHeader === "string" ? xffHeader : "";
|
|
17329
|
-
if (xffRaw.length > 0) {
|
|
17330
|
-
const tokens = xffRaw.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
|
|
17331
|
-
const offender = tokens.find((t) => !isLoopbackAddr6(t));
|
|
17332
|
-
if (offender !== void 0) {
|
|
17333
|
-
console.error(`${TAG35} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
|
|
17334
|
-
return c.json({ error: "enrol-person-loopback-only" }, 403);
|
|
17335
|
-
}
|
|
17336
|
-
}
|
|
17337
|
-
let body;
|
|
17338
|
-
try {
|
|
17339
|
-
body = await c.req.json();
|
|
17340
|
-
} catch {
|
|
17341
|
-
return c.json({ error: "invalid-json" }, 400);
|
|
17342
|
-
}
|
|
17343
|
-
const rawPhone = typeof body.phone === "string" ? body.phone : "";
|
|
17344
|
-
const phone = normalizeE164(rawPhone);
|
|
17345
|
-
if (phone.length <= 1) {
|
|
17346
|
-
return c.json({ error: "invalid-phone" }, 400);
|
|
17347
|
-
}
|
|
17348
|
-
const email = typeof body.email === "string" ? body.email.trim().toLowerCase() : "";
|
|
17349
|
-
if (!EMAIL_RE.test(email)) {
|
|
17350
|
-
return c.json({ error: "invalid-email" }, 400);
|
|
17351
|
-
}
|
|
17352
|
-
const agentSlug = typeof body.agentSlug === "string" ? body.agentSlug.trim() : "";
|
|
17353
|
-
if (!agentSlug) {
|
|
17354
|
-
return c.json({ error: "agentSlug required" }, 400);
|
|
17355
|
-
}
|
|
17356
|
-
const accountId = process.env.ACCOUNT_ID ?? "";
|
|
17357
|
-
if (!accountId) {
|
|
17358
|
-
console.error(`${TAG35} op=person result=no-account-id`);
|
|
17359
|
-
return c.json({ error: "no-account-id" }, 500);
|
|
17360
|
-
}
|
|
17361
|
-
let personId;
|
|
17362
|
-
try {
|
|
17363
|
-
personId = await enrolPerson({ accountId, phone, email, agentSlug });
|
|
17364
|
-
} catch (err) {
|
|
17365
|
-
console.error(
|
|
17366
|
-
`${TAG35} op=person result=write-failed agentSlug=${agentSlug} contact=${maskContact(email)} err="${err instanceof Error ? err.message : String(err)}"`
|
|
17367
|
-
);
|
|
17368
|
-
return c.json({ error: "enrol-failed" }, 500);
|
|
17369
|
-
}
|
|
17370
|
-
let grant = null;
|
|
17371
|
-
try {
|
|
17372
|
-
const g = await findGrantByContact(email, agentSlug, accountId);
|
|
17373
|
-
if (g) grant = { grantId: g.grantId, status: g.status, contactValue: g.contactValue };
|
|
17374
|
-
} catch (err) {
|
|
17375
|
-
console.error(
|
|
17376
|
-
`${TAG35} op=person result=grant-lookup-failed contact=${maskContact(email)} err="${err instanceof Error ? err.message : String(err)}"`
|
|
17377
|
-
);
|
|
17378
|
-
}
|
|
17379
|
-
console.log(
|
|
17380
|
-
`${TAG35} op=person personId=${personId} account=${accountId} agentSlug=${agentSlug} contact=${maskContact(email)}`
|
|
17381
|
-
);
|
|
17382
|
-
return c.json({ personId, grant }, 200);
|
|
17383
|
-
});
|
|
17384
|
-
var enrol_person_default = app41;
|
|
17385
|
-
|
|
17386
|
-
// server/routes/admin/browser.ts
|
|
17387
|
-
var app42 = new Hono();
|
|
17388
|
-
app42.post("/launch", requireAdminSession, async (c) => {
|
|
17389
|
-
try {
|
|
17390
|
-
const transport = resolveBrowserTransport(c.req.raw, c.env?.incoming?.socket?.remoteAddress);
|
|
17391
|
-
console.error(`[admin/browser/launch] op=request transport=${transport}`);
|
|
17392
|
-
if (transport === "vnc") {
|
|
17393
|
-
const vncOk = await ensureVnc();
|
|
17394
|
-
console.error(`[admin/browser/launch] op=vnc-ensured ok=${vncOk}`);
|
|
17395
|
-
if (!vncOk) {
|
|
17396
|
-
console.error("[admin/browser/launch] op=vnc-failed");
|
|
17397
|
-
return c.json({ ok: false, error: "VNC failed to start" }, 502);
|
|
17398
|
-
}
|
|
17399
|
-
}
|
|
17400
|
-
const cdpOk = await ensureCdp(transport);
|
|
17401
|
-
console.error(`[admin/browser/launch] op=cdp-ensured ok=${cdpOk}`);
|
|
17402
|
-
if (!cdpOk) {
|
|
17403
|
-
console.error("[admin/browser/launch] op=cdp-failed");
|
|
17404
|
-
return c.json({ ok: false, error: "Chrome failed to start" }, 502);
|
|
17405
|
-
}
|
|
17406
|
-
console.error(`[admin/browser/launch] op=ready transport=${transport}`);
|
|
17407
|
-
return c.json({ ok: true, transport });
|
|
17408
|
-
} catch (err) {
|
|
17409
|
-
console.error("[admin/browser/launch] Failed to start browser:", err);
|
|
17410
|
-
return c.json(
|
|
17411
|
-
{ ok: false, error: err instanceof Error ? err.message : "Unknown error" },
|
|
17412
|
-
500
|
|
17413
|
-
);
|
|
17414
|
-
}
|
|
17415
|
-
});
|
|
17416
|
-
var browser_default = app42;
|
|
17417
|
-
|
|
17418
|
-
// server/routes/admin/calendar.ts
|
|
17419
|
-
import { existsSync as existsSync26 } from "fs";
|
|
17420
|
-
import { join as join28 } from "path";
|
|
17421
|
-
|
|
17422
|
-
// server/lib/calendar-ics.ts
|
|
17423
|
-
var CRLF = "\r\n";
|
|
17424
|
-
var PRODID = "-//Maxy//Calendar//EN";
|
|
17425
|
-
var MAX_LINE_OCTETS = 75;
|
|
17426
|
-
function escapeText(value) {
|
|
17427
|
-
return value.replace(/\\/g, "\\\\").replace(/;/g, "\\;").replace(/,/g, "\\,").replace(/\r?\n/g, "\\n");
|
|
17428
|
-
}
|
|
17429
|
-
function foldLine(line) {
|
|
17430
|
-
const buf = Buffer.from(line, "utf-8");
|
|
17431
|
-
if (buf.length <= MAX_LINE_OCTETS) return line;
|
|
17432
|
-
const parts = [];
|
|
17433
|
-
let offset = 0;
|
|
17434
|
-
let first = true;
|
|
17435
|
-
while (offset < buf.length) {
|
|
17436
|
-
const maxChunk = first ? MAX_LINE_OCTETS : MAX_LINE_OCTETS - 1;
|
|
17437
|
-
let end = Math.min(offset + maxChunk, buf.length);
|
|
17438
|
-
while (end < buf.length && end > offset && (buf[end] & 192) === 128) end--;
|
|
17439
|
-
const chunk = buf.subarray(offset, end).toString("utf-8");
|
|
17440
|
-
parts.push(first ? chunk : " " + chunk);
|
|
17441
|
-
offset = end;
|
|
17442
|
-
first = false;
|
|
17443
|
-
}
|
|
17444
|
-
return parts.join(CRLF);
|
|
17445
|
-
}
|
|
17446
|
-
function toICSDateTime(iso) {
|
|
17447
|
-
const d = new Date(iso);
|
|
17448
|
-
if (isNaN(d.getTime())) throw new Error(`Invalid date: ${iso}`);
|
|
17449
|
-
const pad = (n) => String(n).padStart(2, "0");
|
|
17450
|
-
return `${d.getUTCFullYear()}${pad(d.getUTCMonth() + 1)}${pad(d.getUTCDate())}T${pad(d.getUTCHours())}${pad(d.getUTCMinutes())}${pad(d.getUTCSeconds())}Z`;
|
|
17451
|
-
}
|
|
17452
|
-
function toICSDate(iso) {
|
|
17453
|
-
const d = new Date(iso);
|
|
17454
|
-
if (isNaN(d.getTime())) throw new Error(`Invalid date: ${iso}`);
|
|
17455
|
-
const pad = (n) => String(n).padStart(2, "0");
|
|
17456
|
-
return `${d.getUTCFullYear()}${pad(d.getUTCMonth() + 1)}${pad(d.getUTCDate())}`;
|
|
17457
|
-
}
|
|
17458
|
-
function attendeeLine(a) {
|
|
17459
|
-
if (!a.email) return null;
|
|
17460
|
-
const cn = a.name ? `;CN=${escapeText(a.name)}` : "";
|
|
17461
|
-
return `ATTENDEE${cn}:mailto:${a.email}`;
|
|
17462
|
-
}
|
|
17463
|
-
function serializeMeetingICS(m, opts) {
|
|
17464
|
-
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
17465
|
-
const lines = [];
|
|
17466
|
-
lines.push("BEGIN:VCALENDAR");
|
|
17467
|
-
lines.push("VERSION:2.0");
|
|
17468
|
-
lines.push(`PRODID:${PRODID}`);
|
|
17469
|
-
lines.push("CALSCALE:GREGORIAN");
|
|
17470
|
-
lines.push("METHOD:PUBLISH");
|
|
17471
|
-
lines.push("BEGIN:VEVENT");
|
|
17472
|
-
lines.push(`UID:${m.uid ?? m.id}`);
|
|
17473
|
-
lines.push(`DTSTAMP:${toICSDateTime(now.toISOString())}`);
|
|
17474
|
-
if (m.isAllDay) {
|
|
17475
|
-
lines.push(`DTSTART;VALUE=DATE:${toICSDate(m.startsAt)}`);
|
|
17476
|
-
if (m.endsAt) lines.push(`DTEND;VALUE=DATE:${toICSDate(m.endsAt)}`);
|
|
17477
|
-
} else {
|
|
17478
|
-
lines.push(`DTSTART:${toICSDateTime(m.startsAt)}`);
|
|
17479
|
-
if (m.endsAt) lines.push(`DTEND:${toICSDateTime(m.endsAt)}`);
|
|
17480
|
-
}
|
|
17481
|
-
lines.push(`SUMMARY:${escapeText(m.title ?? "")}`);
|
|
17482
|
-
if (m.location) lines.push(`LOCATION:${escapeText(m.location)}`);
|
|
17483
|
-
if (opts.includeAttendees) {
|
|
17484
|
-
for (const a of m.attendees) {
|
|
17485
|
-
const line = attendeeLine(a);
|
|
17486
|
-
if (line) lines.push(line);
|
|
17487
|
-
}
|
|
17488
|
-
}
|
|
17489
|
-
lines.push("END:VEVENT");
|
|
17490
|
-
lines.push("END:VCALENDAR");
|
|
17491
|
-
return lines.map(foldLine).join(CRLF) + CRLF;
|
|
17492
|
-
}
|
|
17493
|
-
function countVEvents(ics) {
|
|
17494
|
-
const unfolded = ics.replace(/\r?\n[ \t]/g, "");
|
|
17495
|
-
const lines = unfolded.split(/\r?\n/);
|
|
17496
|
-
let count = 0;
|
|
17497
|
-
let inEvent = false;
|
|
17498
|
-
let nested = 0;
|
|
17499
|
-
for (const raw of lines) {
|
|
17500
|
-
const line = raw.trim();
|
|
17501
|
-
if (line === "BEGIN:VEVENT") {
|
|
17502
|
-
inEvent = true;
|
|
17503
|
-
nested = 0;
|
|
17504
|
-
continue;
|
|
17505
|
-
}
|
|
17506
|
-
if (!inEvent) continue;
|
|
17507
|
-
if (line === "END:VEVENT") {
|
|
17508
|
-
inEvent = false;
|
|
17509
|
-
count++;
|
|
17510
|
-
continue;
|
|
17511
|
-
}
|
|
17512
|
-
if (line.startsWith("BEGIN:")) nested++;
|
|
17513
|
-
else if (line.startsWith("END:") && nested > 0) nested--;
|
|
17514
|
-
}
|
|
17515
|
-
return count;
|
|
17516
|
-
}
|
|
17517
|
-
|
|
17518
|
-
// server/lib/calendar-availability.ts
|
|
17519
|
-
import { readFileSync as readFileSync28 } from "fs";
|
|
17520
|
-
import { join as join27 } from "path";
|
|
17521
|
-
var AVAILABILITY_FILENAME = "calendar-availability.json";
|
|
17522
|
-
var REQUIRED_KEYS = [
|
|
17523
|
-
"timezone",
|
|
17524
|
-
"durationMins",
|
|
17525
|
-
"bufferMins",
|
|
17526
|
-
"weekly"
|
|
17527
|
-
];
|
|
17528
|
-
function readAvailabilityConfig(accountDir) {
|
|
17529
|
-
const path2 = join27(accountDir, AVAILABILITY_FILENAME);
|
|
17530
|
-
let raw;
|
|
17531
|
-
try {
|
|
17532
|
-
raw = readFileSync28(path2, "utf-8");
|
|
17533
|
-
} catch {
|
|
17534
|
-
throw new Error(`availability not configured: ${path2} not found`);
|
|
17535
|
-
}
|
|
17536
|
-
let parsed;
|
|
17537
|
-
try {
|
|
17538
|
-
parsed = JSON.parse(raw);
|
|
17539
|
-
} catch (err) {
|
|
17540
|
-
throw new Error(`invalid JSON in ${path2}: ${err.message}`);
|
|
17541
|
-
}
|
|
17542
|
-
const cfg = parsed;
|
|
17543
|
-
for (const key of REQUIRED_KEYS) {
|
|
17544
|
-
if (cfg[key] === void 0 || cfg[key] === null) {
|
|
17545
|
-
throw new Error(`availability config ${path2} missing required key: ${key}`);
|
|
17546
|
-
}
|
|
17547
|
-
}
|
|
17548
|
-
if (cfg.daysAhead !== void 0 && cfg.daysAhead !== null) {
|
|
17549
|
-
if (typeof cfg.daysAhead !== "number" || !Number.isInteger(cfg.daysAhead) || cfg.daysAhead < 1) {
|
|
17550
|
-
throw new Error(`availability config ${path2} invalid daysAhead: must be a positive integer`);
|
|
17551
|
-
}
|
|
17552
|
-
}
|
|
17553
|
-
if (cfg.allowMultiSlot !== void 0 && cfg.allowMultiSlot !== null) {
|
|
17554
|
-
if (typeof cfg.allowMultiSlot !== "boolean") {
|
|
17555
|
-
throw new Error(`availability config ${path2} invalid allowMultiSlot: must be a boolean`);
|
|
17556
|
-
}
|
|
17557
|
-
}
|
|
17558
|
-
return cfg;
|
|
17030
|
+
|
|
17031
|
+
// server/lib/calendar-availability.ts
|
|
17032
|
+
import { readFileSync as readFileSync28 } from "fs";
|
|
17033
|
+
import { join as join27 } from "path";
|
|
17034
|
+
var AVAILABILITY_FILENAME = "calendar-availability.json";
|
|
17035
|
+
var REQUIRED_KEYS = [
|
|
17036
|
+
"timezone",
|
|
17037
|
+
"durationMins",
|
|
17038
|
+
"bufferMins",
|
|
17039
|
+
"weekly"
|
|
17040
|
+
];
|
|
17041
|
+
function readAvailabilityConfig(accountDir) {
|
|
17042
|
+
const path2 = join27(accountDir, AVAILABILITY_FILENAME);
|
|
17043
|
+
let raw;
|
|
17044
|
+
try {
|
|
17045
|
+
raw = readFileSync28(path2, "utf-8");
|
|
17046
|
+
} catch {
|
|
17047
|
+
throw new Error(`availability not configured: ${path2} not found`);
|
|
17048
|
+
}
|
|
17049
|
+
let parsed;
|
|
17050
|
+
try {
|
|
17051
|
+
parsed = JSON.parse(raw);
|
|
17052
|
+
} catch (err) {
|
|
17053
|
+
throw new Error(`invalid JSON in ${path2}: ${err.message}`);
|
|
17054
|
+
}
|
|
17055
|
+
const cfg = parsed;
|
|
17056
|
+
for (const key of REQUIRED_KEYS) {
|
|
17057
|
+
if (cfg[key] === void 0 || cfg[key] === null) {
|
|
17058
|
+
throw new Error(`availability config ${path2} missing required key: ${key}`);
|
|
17059
|
+
}
|
|
17060
|
+
}
|
|
17061
|
+
if (cfg.daysAhead !== void 0 && cfg.daysAhead !== null) {
|
|
17062
|
+
if (typeof cfg.daysAhead !== "number" || !Number.isInteger(cfg.daysAhead) || cfg.daysAhead < 1) {
|
|
17063
|
+
throw new Error(`availability config ${path2} invalid daysAhead: must be a positive integer`);
|
|
17064
|
+
}
|
|
17065
|
+
}
|
|
17066
|
+
if (cfg.allowMultiSlot !== void 0 && cfg.allowMultiSlot !== null) {
|
|
17067
|
+
if (typeof cfg.allowMultiSlot !== "boolean") {
|
|
17068
|
+
throw new Error(`availability config ${path2} invalid allowMultiSlot: must be a boolean`);
|
|
17069
|
+
}
|
|
17070
|
+
}
|
|
17071
|
+
return cfg;
|
|
17559
17072
|
}
|
|
17560
17073
|
|
|
17561
17074
|
// server/routes/admin/calendar.ts
|
|
@@ -17945,7 +17458,7 @@ app44.route("/calendar", calendar_default);
|
|
|
17945
17458
|
var admin_default = app44;
|
|
17946
17459
|
|
|
17947
17460
|
// server/routes/access/verify-token.ts
|
|
17948
|
-
var
|
|
17461
|
+
var TAG32 = "[access-verify]";
|
|
17949
17462
|
var MINT_TAG = "[access-session-mint]";
|
|
17950
17463
|
var COOKIE_NAME = "__access_session";
|
|
17951
17464
|
var app45 = new Hono();
|
|
@@ -17964,39 +17477,39 @@ app45.post("/", async (c) => {
|
|
|
17964
17477
|
}
|
|
17965
17478
|
const rateMsg = checkAccessRateLimit(ip, agentSlug);
|
|
17966
17479
|
if (rateMsg) {
|
|
17967
|
-
console.error(`${
|
|
17480
|
+
console.error(`${TAG32} grantId=- agentSlug=${agentSlug} result=rate-limited ip=${ip}`);
|
|
17968
17481
|
return c.json({ error: rateMsg }, 429);
|
|
17969
17482
|
}
|
|
17970
17483
|
const grant = await findGrantByMagicToken(token);
|
|
17971
17484
|
if (!grant) {
|
|
17972
17485
|
recordAccessFailedAttempt(ip, agentSlug);
|
|
17973
|
-
console.error(`${
|
|
17486
|
+
console.error(`${TAG32} grantId=- agentSlug=${agentSlug} result=notfound ip=${ip}`);
|
|
17974
17487
|
return c.json({ error: "invalid-or-expired-link" }, 401);
|
|
17975
17488
|
}
|
|
17976
17489
|
if (grant.agentSlug !== agentSlug) {
|
|
17977
17490
|
recordAccessFailedAttempt(ip, agentSlug);
|
|
17978
17491
|
console.error(
|
|
17979
|
-
`${
|
|
17492
|
+
`${TAG32} grantId=${grant.grantId} agentSlug=${agentSlug} result=agent-mismatch grantAgent=${grant.agentSlug} ip=${ip}`
|
|
17980
17493
|
);
|
|
17981
17494
|
return c.json({ error: "invalid-or-expired-link" }, 401);
|
|
17982
17495
|
}
|
|
17983
17496
|
if (grant.status === "expired" || grant.status === "revoked") {
|
|
17984
17497
|
recordAccessFailedAttempt(ip, agentSlug);
|
|
17985
17498
|
console.error(
|
|
17986
|
-
`${
|
|
17499
|
+
`${TAG32} grantId=${grant.grantId} agentSlug=${agentSlug} result=expired status=${grant.status} ip=${ip}`
|
|
17987
17500
|
);
|
|
17988
17501
|
return c.json({ error: "access-no-longer-valid" }, 401);
|
|
17989
17502
|
}
|
|
17990
17503
|
if (grant.expiresAt !== null && grant.expiresAt < Date.now()) {
|
|
17991
17504
|
recordAccessFailedAttempt(ip, agentSlug);
|
|
17992
17505
|
console.error(
|
|
17993
|
-
`${
|
|
17506
|
+
`${TAG32} grantId=${grant.grantId} agentSlug=${agentSlug} result=expired reason=expiresAt-past ip=${ip}`
|
|
17994
17507
|
);
|
|
17995
17508
|
return c.json({ error: "access-no-longer-valid" }, 401);
|
|
17996
17509
|
}
|
|
17997
17510
|
if (!grant.sliceToken) {
|
|
17998
17511
|
console.error(
|
|
17999
|
-
`${
|
|
17512
|
+
`${TAG32} grantId=${grant.grantId} agentSlug=${agentSlug} result=no-slice-token reason=schema-violation`
|
|
18000
17513
|
);
|
|
18001
17514
|
return c.json({ error: "grant-misconfigured" }, 500);
|
|
18002
17515
|
}
|
|
@@ -18012,12 +17525,12 @@ app45.post("/", async (c) => {
|
|
|
18012
17525
|
await consumeMagicTokenAndActivate(grant.grantId);
|
|
18013
17526
|
} catch (err) {
|
|
18014
17527
|
console.error(
|
|
18015
|
-
`${
|
|
17528
|
+
`${TAG32} grantId=${grant.grantId} agentSlug=${agentSlug} result=consume-failed err="${err instanceof Error ? err.message : String(err)}"`
|
|
18016
17529
|
);
|
|
18017
17530
|
return c.json({ error: "verification-failed" }, 500);
|
|
18018
17531
|
}
|
|
18019
17532
|
clearAccessRateLimit(ip, agentSlug);
|
|
18020
|
-
console.log(`${
|
|
17533
|
+
console.log(`${TAG32} grantId=${grant.grantId} agentSlug=${agentSlug} result=ok ip=${ip}`);
|
|
18021
17534
|
console.log(
|
|
18022
17535
|
`${MINT_TAG} grantId=${grant.grantId} sliceToken=${grant.sliceToken.slice(0, 8)} agentSlug=${agentSlug} personId=${grant.personId ?? "none"}`
|
|
18023
17536
|
);
|
|
@@ -18035,9 +17548,9 @@ var verify_token_default = app45;
|
|
|
18035
17548
|
|
|
18036
17549
|
// app/lib/access-email.ts
|
|
18037
17550
|
import { spawn as spawn2 } from "child_process";
|
|
18038
|
-
import { resolve as
|
|
18039
|
-
var PLATFORM_ROOT7 = process.env.MAXY_PLATFORM_ROOT ??
|
|
18040
|
-
var SEND_SCRIPT =
|
|
17551
|
+
import { resolve as resolve24 } from "path";
|
|
17552
|
+
var PLATFORM_ROOT7 = process.env.MAXY_PLATFORM_ROOT ?? resolve24(process.cwd(), "..");
|
|
17553
|
+
var SEND_SCRIPT = resolve24(
|
|
18041
17554
|
PLATFORM_ROOT7,
|
|
18042
17555
|
"plugins",
|
|
18043
17556
|
"email",
|
|
@@ -18093,7 +17606,7 @@ async function sendMagicLinkEmail(payload) {
|
|
|
18093
17606
|
}
|
|
18094
17607
|
|
|
18095
17608
|
// server/routes/access/request-magic-link.ts
|
|
18096
|
-
var
|
|
17609
|
+
var TAG33 = "[access-request-link]";
|
|
18097
17610
|
var app46 = new Hono();
|
|
18098
17611
|
var VISITOR_MESSAGE = "If that email is on the invite list, a fresh link is on the way.";
|
|
18099
17612
|
app46.post("/", async (c) => {
|
|
@@ -18110,18 +17623,18 @@ app46.post("/", async (c) => {
|
|
|
18110
17623
|
}
|
|
18111
17624
|
const rateMsg = checkRequestLinkRateLimit(contactValue);
|
|
18112
17625
|
if (rateMsg) {
|
|
18113
|
-
console.error(`${
|
|
17626
|
+
console.error(`${TAG33} contactValue=${maskContact(contactValue)} result=rate-limited`);
|
|
18114
17627
|
return c.json({ error: rateMsg }, 429);
|
|
18115
17628
|
}
|
|
18116
17629
|
recordRequestLinkAttempt(contactValue);
|
|
18117
17630
|
const accountId = process.env.ACCOUNT_ID ?? "";
|
|
18118
17631
|
if (!accountId) {
|
|
18119
|
-
console.error(`${
|
|
17632
|
+
console.error(`${TAG33} contactValue=${maskContact(contactValue)} result=no-account-id`);
|
|
18120
17633
|
return c.json({ message: VISITOR_MESSAGE }, 200);
|
|
18121
17634
|
}
|
|
18122
17635
|
const grant = await findActiveGrantByContact(contactValue, agentSlug, accountId);
|
|
18123
17636
|
if (!grant) {
|
|
18124
|
-
console.log(`${
|
|
17637
|
+
console.log(`${TAG33} contactValue=${maskContact(contactValue)} result=notfound`);
|
|
18125
17638
|
return c.json({ message: VISITOR_MESSAGE }, 200);
|
|
18126
17639
|
}
|
|
18127
17640
|
let token;
|
|
@@ -18129,7 +17642,7 @@ app46.post("/", async (c) => {
|
|
|
18129
17642
|
token = await generateNewMagicToken(grant.grantId);
|
|
18130
17643
|
} catch (err) {
|
|
18131
17644
|
console.error(
|
|
18132
|
-
`${
|
|
17645
|
+
`${TAG33} contactValue=${maskContact(contactValue)} result=mint-failed err="${err instanceof Error ? err.message : String(err)}"`
|
|
18133
17646
|
);
|
|
18134
17647
|
return c.json({ message: VISITOR_MESSAGE }, 200);
|
|
18135
17648
|
}
|
|
@@ -18161,12 +17674,12 @@ app46.post("/", async (c) => {
|
|
|
18161
17674
|
});
|
|
18162
17675
|
if (!sendResult.ok) {
|
|
18163
17676
|
console.error(
|
|
18164
|
-
`${
|
|
17677
|
+
`${TAG33} contactValue=${maskContact(contactValue)} result=send-failed err="${sendResult.error}"`
|
|
18165
17678
|
);
|
|
18166
17679
|
return c.json({ message: VISITOR_MESSAGE }, 200);
|
|
18167
17680
|
}
|
|
18168
17681
|
console.log(
|
|
18169
|
-
`${
|
|
17682
|
+
`${TAG33} contactValue=${maskContact(contactValue)} result=ok messageId=${sendResult.messageId}`
|
|
18170
17683
|
);
|
|
18171
17684
|
return c.json({ message: VISITOR_MESSAGE }, 200);
|
|
18172
17685
|
});
|
|
@@ -18179,8 +17692,8 @@ app47.route("/request-magic-link", request_magic_link_default);
|
|
|
18179
17692
|
var access_default = app47;
|
|
18180
17693
|
|
|
18181
17694
|
// server/routes/sites.ts
|
|
18182
|
-
import { existsSync as existsSync27, readFileSync as readFileSync29, realpathSync as
|
|
18183
|
-
import { resolve as
|
|
17695
|
+
import { existsSync as existsSync27, readFileSync as readFileSync29, realpathSync as realpathSync5, statSync as statSync11 } from "fs";
|
|
17696
|
+
import { resolve as resolve25 } from "path";
|
|
18184
17697
|
var SAFE_SEG_RE = /^[a-z0-9_][a-z0-9_.-]{0,99}$/i;
|
|
18185
17698
|
var MIME = {
|
|
18186
17699
|
".html": "text/html; charset=utf-8",
|
|
@@ -18237,8 +17750,8 @@ app48.get("/:rel{.*}", (c) => {
|
|
|
18237
17750
|
}
|
|
18238
17751
|
segments.push(seg);
|
|
18239
17752
|
}
|
|
18240
|
-
const rootDir =
|
|
18241
|
-
let filePath = segments.length === 0 ? rootDir :
|
|
17753
|
+
const rootDir = resolve25(account.accountDir, "sites");
|
|
17754
|
+
let filePath = segments.length === 0 ? rootDir : resolve25(rootDir, ...segments);
|
|
18242
17755
|
if (filePath !== rootDir && !filePath.startsWith(rootDir + "/")) {
|
|
18243
17756
|
console.error(`[sites] path-traversal-rejected path=${reqPath} reason=escape status=403`);
|
|
18244
17757
|
return c.text("Forbidden", 403);
|
|
@@ -18258,7 +17771,7 @@ app48.get("/:rel{.*}", (c) => {
|
|
|
18258
17771
|
return c.redirect(target, 301);
|
|
18259
17772
|
}
|
|
18260
17773
|
if (stat8?.isDirectory()) {
|
|
18261
|
-
filePath =
|
|
17774
|
+
filePath = resolve25(filePath, "index.html");
|
|
18262
17775
|
}
|
|
18263
17776
|
if (!filePath.startsWith(rootDir + "/")) {
|
|
18264
17777
|
console.error(`[sites] path-traversal-rejected path=${reqPath} reason=escape status=403`);
|
|
@@ -18271,8 +17784,8 @@ app48.get("/:rel{.*}", (c) => {
|
|
|
18271
17784
|
let realPath;
|
|
18272
17785
|
let realRoot;
|
|
18273
17786
|
try {
|
|
18274
|
-
realPath =
|
|
18275
|
-
realRoot =
|
|
17787
|
+
realPath = realpathSync5(filePath);
|
|
17788
|
+
realRoot = realpathSync5(rootDir);
|
|
18276
17789
|
} catch {
|
|
18277
17790
|
console.error(`[sites] not-found path=${reqPath} status=404`);
|
|
18278
17791
|
return c.text("Not found", 404);
|
|
@@ -18895,14 +18408,14 @@ async function writeEvent(opts) {
|
|
|
18895
18408
|
var visitor_event_default = app51;
|
|
18896
18409
|
|
|
18897
18410
|
// server/routes/session.ts
|
|
18898
|
-
import { resolve as
|
|
18411
|
+
import { resolve as resolve26 } from "path";
|
|
18899
18412
|
import { existsSync as existsSync29, writeFileSync as writeFileSync12, mkdirSync as mkdirSync7 } from "fs";
|
|
18900
18413
|
var UUID_RE4 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
18901
18414
|
function writeBrandingCache(accountId, agentSlug, branding) {
|
|
18902
18415
|
try {
|
|
18903
|
-
const cacheDir =
|
|
18416
|
+
const cacheDir = resolve26(MAXY_DIR, "branding-cache", accountId);
|
|
18904
18417
|
mkdirSync7(cacheDir, { recursive: true });
|
|
18905
|
-
writeFileSync12(
|
|
18418
|
+
writeFileSync12(resolve26(cacheDir, `${agentSlug}.json`), JSON.stringify(branding), "utf-8");
|
|
18906
18419
|
} catch (err) {
|
|
18907
18420
|
console.error(`[branding] cache write failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
18908
18421
|
}
|
|
@@ -18979,8 +18492,8 @@ app52.post("/", async (c) => {
|
|
|
18979
18492
|
}
|
|
18980
18493
|
let agentConfig = null;
|
|
18981
18494
|
if (account) {
|
|
18982
|
-
const agentDir =
|
|
18983
|
-
if (!existsSync29(agentDir) || !existsSync29(
|
|
18495
|
+
const agentDir = resolve26(account.accountDir, "agents", agentSlug);
|
|
18496
|
+
if (!existsSync29(agentDir) || !existsSync29(resolve26(agentDir, "config.json"))) {
|
|
18984
18497
|
return c.json({ error: "Agent not found" }, 404);
|
|
18985
18498
|
}
|
|
18986
18499
|
agentConfig = resolveAgentConfig(account.accountDir, agentSlug);
|
|
@@ -19342,7 +18855,7 @@ function startGraphHealthTimer() {
|
|
|
19342
18855
|
|
|
19343
18856
|
// app/lib/file-watcher.ts
|
|
19344
18857
|
import * as fsp2 from "fs/promises";
|
|
19345
|
-
import { resolve as
|
|
18858
|
+
import { resolve as resolve27, sep as sep8 } from "path";
|
|
19346
18859
|
var ACCOUNT_UUID_RE3 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
19347
18860
|
var DEFAULT_COALESCE_MS = 500;
|
|
19348
18861
|
var ROOTS = ["accounts"];
|
|
@@ -19361,7 +18874,7 @@ async function startFileWatcher(opts = {}) {
|
|
|
19361
18874
|
const dropFn = opts.drop ?? dropFileIndex;
|
|
19362
18875
|
for (const r of ROOTS) {
|
|
19363
18876
|
try {
|
|
19364
|
-
await fsp2.mkdir(
|
|
18877
|
+
await fsp2.mkdir(resolve27(dataRoot, r), { recursive: true });
|
|
19365
18878
|
} catch (err) {
|
|
19366
18879
|
console.error(
|
|
19367
18880
|
`[file-watcher] start-failed root="${r}" err="${err.message}" \u2014 index will be maintained by the 5-min reconcile backstop only`
|
|
@@ -19382,7 +18895,7 @@ async function startFileWatcher(opts = {}) {
|
|
|
19382
18895
|
timers.set(relativePath, t);
|
|
19383
18896
|
}
|
|
19384
18897
|
async function runHook(relativePath, accountId) {
|
|
19385
|
-
const absolute =
|
|
18898
|
+
const absolute = resolve27(dataRoot, relativePath);
|
|
19386
18899
|
let exists = false;
|
|
19387
18900
|
try {
|
|
19388
18901
|
const st = await fsp2.stat(absolute);
|
|
@@ -19406,7 +18919,7 @@ async function startFileWatcher(opts = {}) {
|
|
|
19406
18919
|
}
|
|
19407
18920
|
}
|
|
19408
18921
|
async function watchRoot(rootName) {
|
|
19409
|
-
const absRoot =
|
|
18922
|
+
const absRoot = resolve27(dataRoot, rootName);
|
|
19410
18923
|
try {
|
|
19411
18924
|
const iter = fsp2.watch(absRoot, { recursive: true, signal: controller.signal });
|
|
19412
18925
|
for await (const event of iter) {
|
|
@@ -19445,7 +18958,7 @@ async function startFileWatcher(opts = {}) {
|
|
|
19445
18958
|
|
|
19446
18959
|
// app/lib/migrate-uploads.ts
|
|
19447
18960
|
import { mkdir as mkdir5, readdir as readdir5, rename as rename2, rm as rm3 } from "fs/promises";
|
|
19448
|
-
import { dirname as dirname8, relative as relative6, resolve as
|
|
18961
|
+
import { dirname as dirname8, relative as relative6, resolve as resolve28 } from "path";
|
|
19449
18962
|
var ACCOUNT_UUID_RE4 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
19450
18963
|
async function walkFiles(dir) {
|
|
19451
18964
|
const out = [];
|
|
@@ -19456,7 +18969,7 @@ async function walkFiles(dir) {
|
|
|
19456
18969
|
return out;
|
|
19457
18970
|
}
|
|
19458
18971
|
for (const e of entries) {
|
|
19459
|
-
const abs =
|
|
18972
|
+
const abs = resolve28(dir, e.name);
|
|
19460
18973
|
if (e.isDirectory()) {
|
|
19461
18974
|
out.push(...await walkFiles(abs));
|
|
19462
18975
|
} else if (e.isFile()) {
|
|
@@ -19476,7 +18989,7 @@ async function relocateTree(srcDir, destDir, dataRoot, accountId, session) {
|
|
|
19476
18989
|
let moved = 0;
|
|
19477
18990
|
for (const oldAbs of files) {
|
|
19478
18991
|
const suffix = relative6(srcDir, oldAbs);
|
|
19479
|
-
const newAbs =
|
|
18992
|
+
const newAbs = resolve28(destDir, suffix);
|
|
19480
18993
|
await mkdir5(dirname8(newAbs), { recursive: true });
|
|
19481
18994
|
await rename2(oldAbs, newAbs);
|
|
19482
18995
|
moved++;
|
|
@@ -19503,7 +19016,7 @@ async function relocateTree(srcDir, destDir, dataRoot, accountId, session) {
|
|
|
19503
19016
|
}
|
|
19504
19017
|
async function migrateUploads(opts = {}) {
|
|
19505
19018
|
const dataRoot = opts.dataRoot ?? DATA_ROOT;
|
|
19506
|
-
const oldRoot =
|
|
19019
|
+
const oldRoot = resolve28(dataRoot, "uploads");
|
|
19507
19020
|
let topEntries;
|
|
19508
19021
|
try {
|
|
19509
19022
|
topEntries = await readdir5(oldRoot, { withFileTypes: true });
|
|
@@ -19524,8 +19037,8 @@ async function migrateUploads(opts = {}) {
|
|
|
19524
19037
|
const name = entry.name;
|
|
19525
19038
|
if (ACCOUNT_UUID_RE4.test(name)) {
|
|
19526
19039
|
moved += await relocateTree(
|
|
19527
|
-
|
|
19528
|
-
|
|
19040
|
+
resolve28(oldRoot, name),
|
|
19041
|
+
resolve28(dataRoot, "accounts", name, "uploads"),
|
|
19529
19042
|
dataRoot,
|
|
19530
19043
|
name,
|
|
19531
19044
|
session
|
|
@@ -19537,8 +19050,8 @@ async function migrateUploads(opts = {}) {
|
|
|
19537
19050
|
continue;
|
|
19538
19051
|
}
|
|
19539
19052
|
moved += await relocateTree(
|
|
19540
|
-
|
|
19541
|
-
|
|
19053
|
+
resolve28(oldRoot, "public"),
|
|
19054
|
+
resolve28(dataRoot, "accounts", installAccountId, "uploads", "public"),
|
|
19542
19055
|
dataRoot,
|
|
19543
19056
|
null,
|
|
19544
19057
|
// public uploads carry no graph nodes
|
|
@@ -19567,7 +19080,7 @@ async function migrateUploads(opts = {}) {
|
|
|
19567
19080
|
}
|
|
19568
19081
|
|
|
19569
19082
|
// app/lib/migrate-admin-webchat-sidecars.ts
|
|
19570
|
-
import { readdir as readdir6, readFile as
|
|
19083
|
+
import { readdir as readdir6, readFile as readFile6, rename as rename3, writeFile as writeFile4, unlink as unlink3 } from "fs/promises";
|
|
19571
19084
|
import { existsSync as existsSync30 } from "fs";
|
|
19572
19085
|
import { join as join30 } from "path";
|
|
19573
19086
|
var ADMIN_ROLE2 = "admin";
|
|
@@ -19580,7 +19093,7 @@ var defaultResolveName = async (accountId, userId) => {
|
|
|
19580
19093
|
async function readRaw2(path2) {
|
|
19581
19094
|
let raw;
|
|
19582
19095
|
try {
|
|
19583
|
-
raw = await
|
|
19096
|
+
raw = await readFile6(path2, "utf8");
|
|
19584
19097
|
} catch {
|
|
19585
19098
|
return null;
|
|
19586
19099
|
}
|
|
@@ -20549,16 +20062,16 @@ var WebchatGateway = class _WebchatGateway {
|
|
|
20549
20062
|
* The public /api/chat route awaits this to return the reply on the same SSE
|
|
20550
20063
|
* response, preserving the pre-756 POST→single-blob browser contract. */
|
|
20551
20064
|
awaitReply(key, timeoutMs) {
|
|
20552
|
-
return new Promise((
|
|
20065
|
+
return new Promise((resolve33) => {
|
|
20553
20066
|
const timer2 = setTimeout(() => {
|
|
20554
20067
|
this.replyAwaiters.delete(key);
|
|
20555
|
-
|
|
20068
|
+
resolve33({ timeout: true });
|
|
20556
20069
|
}, timeoutMs);
|
|
20557
20070
|
if (timer2.unref) timer2.unref();
|
|
20558
20071
|
this.replyAwaiters.set(key, (r) => {
|
|
20559
20072
|
clearTimeout(timer2);
|
|
20560
20073
|
this.replyAwaiters.delete(key);
|
|
20561
|
-
|
|
20074
|
+
resolve33(r);
|
|
20562
20075
|
});
|
|
20563
20076
|
});
|
|
20564
20077
|
}
|
|
@@ -20569,11 +20082,11 @@ var WebchatGateway = class _WebchatGateway {
|
|
|
20569
20082
|
* terminal dialog stays answerable). */
|
|
20570
20083
|
awaitPermissionVerdict(p, timeoutMs) {
|
|
20571
20084
|
const k = _WebchatGateway.promptKey(p.key, p.requestId);
|
|
20572
|
-
return new Promise((
|
|
20085
|
+
return new Promise((resolve33) => {
|
|
20573
20086
|
this.pendingPrompts.get(k)?.resolve({ timeout: true });
|
|
20574
20087
|
const timer2 = setTimeout(() => {
|
|
20575
20088
|
this.pendingPrompts.delete(k);
|
|
20576
|
-
|
|
20089
|
+
resolve33({ timeout: true });
|
|
20577
20090
|
}, timeoutMs);
|
|
20578
20091
|
if (timer2.unref) timer2.unref();
|
|
20579
20092
|
this.pendingPrompts.set(k, {
|
|
@@ -20582,7 +20095,7 @@ var WebchatGateway = class _WebchatGateway {
|
|
|
20582
20095
|
resolve: (r) => {
|
|
20583
20096
|
clearTimeout(timer2);
|
|
20584
20097
|
this.pendingPrompts.delete(k);
|
|
20585
|
-
|
|
20098
|
+
resolve33(r);
|
|
20586
20099
|
}
|
|
20587
20100
|
});
|
|
20588
20101
|
console.error(`[webchat:perm] op=open key=${keyDisplay(p.key)} id=${p.requestId} tool=${p.toolName}`);
|
|
@@ -20654,6 +20167,45 @@ var WebchatGateway = class _WebchatGateway {
|
|
|
20654
20167
|
if (handle.unref) handle.unref();
|
|
20655
20168
|
return handle;
|
|
20656
20169
|
}
|
|
20170
|
+
/** A gated public PTY exited explicitly on the manager side (operator stop,
|
|
20171
|
+
* claude crash, manager DELETE), not via the idle reaper. The manager POSTs
|
|
20172
|
+
* the sessionId to the loopback /api/admin/public-session-exit route, which
|
|
20173
|
+
* reaches this through the process-wide gateway handle. The slice ringfence
|
|
20174
|
+
* the reviewer needs lives only in this gateway's publicSessions map — the
|
|
20175
|
+
* manager has none of it — so resolve the record here and dispatch the same
|
|
20176
|
+
* reviewer the idle reaper uses, tagged dispatchFor='exit'. Dedup is internal
|
|
20177
|
+
* to firePublicSessionEndReview, so a session already reviewed by the reaper
|
|
20178
|
+
* (or a racing respawn) is a no-op.
|
|
20179
|
+
*
|
|
20180
|
+
* Only gated records (non-empty slice) are reviewed and evicted here, the
|
|
20181
|
+
* same gate the reaper applies. An open-mode public record is left for the
|
|
20182
|
+
* idle reaper. No deleteSession — the PTY is already gone and the JSONL is
|
|
20183
|
+
* the audit record the reviewer just read. */
|
|
20184
|
+
handlePublicSessionExit(sessionId) {
|
|
20185
|
+
let found = null;
|
|
20186
|
+
for (const [key2, rec2] of this.publicSessions) {
|
|
20187
|
+
if (rec2.sessionId === sessionId) {
|
|
20188
|
+
found = { key: key2, rec: rec2 };
|
|
20189
|
+
break;
|
|
20190
|
+
}
|
|
20191
|
+
}
|
|
20192
|
+
if (!found) {
|
|
20193
|
+
console.error(`[webchat:inbound] op=public-exit sessionId=${sessionId.slice(0, 8)} result=no-record`);
|
|
20194
|
+
return;
|
|
20195
|
+
}
|
|
20196
|
+
const { key, rec } = found;
|
|
20197
|
+
if (!rec.sliceToken || rec.sliceToken.length === 0) return;
|
|
20198
|
+
this.publicSessions.delete(key);
|
|
20199
|
+
console.error(`[webchat:inbound] op=public-exit key=${keyDisplay(key)} sessionId=${sessionId.slice(0, 8)} result=review`);
|
|
20200
|
+
void this.deps.firePublicSessionEndReview?.({
|
|
20201
|
+
sessionId: rec.sessionId,
|
|
20202
|
+
sliceToken: rec.sliceToken,
|
|
20203
|
+
personId: rec.personId ?? null,
|
|
20204
|
+
accountId: rec.accountId,
|
|
20205
|
+
agentSlug: rec.agentSlug,
|
|
20206
|
+
dispatchFor: "exit"
|
|
20207
|
+
});
|
|
20208
|
+
}
|
|
20657
20209
|
/** Hono sub-app exposing the loopback channel routes; mount on the UI app. */
|
|
20658
20210
|
routes() {
|
|
20659
20211
|
return createWebchatChannelRoutes({
|
|
@@ -20770,97 +20322,385 @@ var WebchatGateway = class _WebchatGateway {
|
|
|
20770
20322
|
);
|
|
20771
20323
|
}
|
|
20772
20324
|
}
|
|
20773
|
-
for (const key of this.recoverable.keys()) {
|
|
20774
|
-
if (!this.hub.hasInFlight(key)) this.recoverable.delete(key);
|
|
20775
|
-
}
|
|
20776
|
-
if (this.recoveryAttempted.size > 0) {
|
|
20777
|
-
const liveIds = this.hub.inFlightIds();
|
|
20778
|
-
for (const attemptKey of this.recoveryAttempted) {
|
|
20779
|
-
if (!liveIds.has(attemptKey)) this.recoveryAttempted.delete(attemptKey);
|
|
20780
|
-
}
|
|
20325
|
+
for (const key of this.recoverable.keys()) {
|
|
20326
|
+
if (!this.hub.hasInFlight(key)) this.recoverable.delete(key);
|
|
20327
|
+
}
|
|
20328
|
+
if (this.recoveryAttempted.size > 0) {
|
|
20329
|
+
const liveIds = this.hub.inFlightIds();
|
|
20330
|
+
for (const attemptKey of this.recoveryAttempted) {
|
|
20331
|
+
if (!liveIds.has(attemptKey)) this.recoveryAttempted.delete(attemptKey);
|
|
20332
|
+
}
|
|
20333
|
+
}
|
|
20334
|
+
} finally {
|
|
20335
|
+
this.recovering = false;
|
|
20336
|
+
}
|
|
20337
|
+
}
|
|
20338
|
+
/** Handle one inbound webchat message (browser POST → here). */
|
|
20339
|
+
async handleInbound(input) {
|
|
20340
|
+
const bytes = Buffer.byteLength(input.text, "utf8");
|
|
20341
|
+
const hadSubscriber = this.hub.hasSubscriber(input.key);
|
|
20342
|
+
const willDeliverNow = this.hub.isReady(input.key);
|
|
20343
|
+
this.hub.deliver(
|
|
20344
|
+
{ key: input.key, text: input.text, messageId: `wc-${++this.seq}` },
|
|
20345
|
+
Date.now()
|
|
20346
|
+
);
|
|
20347
|
+
if (willDeliverNow) this.lastDeliveredAt.set(input.key, Date.now());
|
|
20348
|
+
console.error(
|
|
20349
|
+
`[webchat:inbound] op=received key=${keyDisplay(input.key)} role=${input.role} bytes=${bytes} delivery=${willDeliverNow ? "immediate" : "queued"}`
|
|
20350
|
+
);
|
|
20351
|
+
if (input.role === "admin") {
|
|
20352
|
+
this.recoverable.set(input.key, {
|
|
20353
|
+
accountId: input.accountId,
|
|
20354
|
+
key: input.key,
|
|
20355
|
+
role: input.role,
|
|
20356
|
+
...input.sessionId !== void 0 ? { sessionId: input.sessionId } : {},
|
|
20357
|
+
...input.adminUserId !== void 0 ? { adminUserId: input.adminUserId } : {}
|
|
20358
|
+
});
|
|
20359
|
+
}
|
|
20360
|
+
const existing = this.publicSessions.get(input.key);
|
|
20361
|
+
if (existing) existing.lastActivityAt = Date.now();
|
|
20362
|
+
if (!hadSubscriber && !this.spawning.has(input.key)) {
|
|
20363
|
+
this.spawning.add(input.key);
|
|
20364
|
+
console.error(`[webchat:inbound] op=spawn-trigger key=${input.key} role=${input.role}`);
|
|
20365
|
+
try {
|
|
20366
|
+
const spawned = await this.deps.ensureChannelSession({
|
|
20367
|
+
accountId: input.accountId,
|
|
20368
|
+
key: input.key,
|
|
20369
|
+
role: input.role,
|
|
20370
|
+
gatewayUrl: this.deps.gatewayUrl,
|
|
20371
|
+
serverPath: this.deps.serverPath,
|
|
20372
|
+
// Task 800 — only present on a session-targeted launch, so the
|
|
20373
|
+
// default-path call shape stays byte-identical.
|
|
20374
|
+
...input.sessionId !== void 0 ? { sessionId: input.sessionId } : {},
|
|
20375
|
+
// The authenticated admin's userId on an admin bootstrap inbound; the
|
|
20376
|
+
// default-path call shape stays byte-identical when absent.
|
|
20377
|
+
...input.adminUserId !== void 0 ? { adminUserId: input.adminUserId } : {},
|
|
20378
|
+
// Task 756 — public spawn params on a role:'public' inbound.
|
|
20379
|
+
...input.public !== void 0 ? { public: input.public } : {}
|
|
20380
|
+
});
|
|
20381
|
+
if (input.role === "public" && input.public && spawned && "sessionId" in spawned) {
|
|
20382
|
+
this.publicSessions.set(input.key, {
|
|
20383
|
+
sessionId: spawned.sessionId,
|
|
20384
|
+
accountId: input.accountId,
|
|
20385
|
+
agentSlug: input.public.agentSlug,
|
|
20386
|
+
sliceToken: input.public.sliceToken,
|
|
20387
|
+
personId: input.public.personId,
|
|
20388
|
+
lastActivityAt: Date.now()
|
|
20389
|
+
});
|
|
20390
|
+
}
|
|
20391
|
+
} finally {
|
|
20392
|
+
this.spawning.delete(input.key);
|
|
20393
|
+
}
|
|
20394
|
+
}
|
|
20395
|
+
}
|
|
20396
|
+
};
|
|
20397
|
+
|
|
20398
|
+
// app/lib/webchat/gateway/public-spawn-request.ts
|
|
20399
|
+
function buildPublicWebchatSpawnRequest(input) {
|
|
20400
|
+
const { agentSlug, sliceToken, personId, visitorId, name } = input.public;
|
|
20401
|
+
return {
|
|
20402
|
+
senderId: input.key,
|
|
20403
|
+
role: "public",
|
|
20404
|
+
channel: "webchat",
|
|
20405
|
+
accountId: input.accountId,
|
|
20406
|
+
agentSlug,
|
|
20407
|
+
model: SONNET_MODEL,
|
|
20408
|
+
// sliceToken/personId are present only for a gated visitor; open-mode
|
|
20409
|
+
// public agents carry neither (mirrors the bridge's open-mode key).
|
|
20410
|
+
...sliceToken ? { sliceToken } : {},
|
|
20411
|
+
...personId ? { personId } : {},
|
|
20412
|
+
...visitorId ? { visitorId } : {},
|
|
20413
|
+
...input.previousContext ? { previousContext: input.previousContext } : {},
|
|
20414
|
+
name,
|
|
20415
|
+
webchatChannel: { key: input.key, gatewayUrl: input.gatewayUrl, serverPath: input.serverPath }
|
|
20416
|
+
};
|
|
20417
|
+
}
|
|
20418
|
+
|
|
20419
|
+
// app/lib/channel-pty-bridge/follower.ts
|
|
20420
|
+
function followerPendingMaxMs() {
|
|
20421
|
+
return Number(process.env.CHANNEL_PTY_FOLLOWER_PENDING_MAX_MS ?? String(3e5));
|
|
20422
|
+
}
|
|
20423
|
+
function followerRetryMs() {
|
|
20424
|
+
return Number(process.env.CHANNEL_PTY_FOLLOWER_RETRY_MS ?? String(1e3));
|
|
20425
|
+
}
|
|
20426
|
+
function startFollower(opts) {
|
|
20427
|
+
const abort = new AbortController();
|
|
20428
|
+
const { entry, tag } = opts;
|
|
20429
|
+
void (async () => {
|
|
20430
|
+
try {
|
|
20431
|
+
const sid = entry.sessionId.slice(0, 8);
|
|
20432
|
+
const deadline = Date.now() + followerPendingMaxMs();
|
|
20433
|
+
const retryMs = followerRetryMs();
|
|
20434
|
+
let res;
|
|
20435
|
+
let attempt = 0;
|
|
20436
|
+
for (; ; ) {
|
|
20437
|
+
res = await fetch(
|
|
20438
|
+
managerLogFollowUrl(entry.sessionId, { boundary: opts.suppressResumeReplay === true }),
|
|
20439
|
+
{ signal: abort.signal }
|
|
20440
|
+
);
|
|
20441
|
+
console.error(`${tag} follower-connect sessionId=${sid} status=${res.status}`);
|
|
20442
|
+
if (res.status === 202) {
|
|
20443
|
+
attempt += 1;
|
|
20444
|
+
await res.body?.cancel().catch(() => {
|
|
20445
|
+
});
|
|
20446
|
+
if (abort.signal.aborted) return;
|
|
20447
|
+
if (Date.now() >= deadline) {
|
|
20448
|
+
console.error(`${tag} follower-give-up sessionId=${sid} reason=pending-timeout attempts=${attempt}`);
|
|
20449
|
+
opts.onError?.("follow-pending-timeout");
|
|
20450
|
+
return;
|
|
20451
|
+
}
|
|
20452
|
+
console.error(`${tag} follower-retry sessionId=${sid} attempt=${attempt} reason=pending`);
|
|
20453
|
+
await new Promise((r) => setTimeout(r, retryMs));
|
|
20454
|
+
if (abort.signal.aborted) return;
|
|
20455
|
+
continue;
|
|
20456
|
+
}
|
|
20457
|
+
break;
|
|
20458
|
+
}
|
|
20459
|
+
if (!res.ok || !res.body) {
|
|
20460
|
+
opts.onError?.(`follow-status-${res.status}`);
|
|
20461
|
+
return;
|
|
20462
|
+
}
|
|
20463
|
+
console.error(`${tag} follower-open sessionId=${sid}`);
|
|
20464
|
+
const reader = res.body.getReader();
|
|
20465
|
+
const decoder = new TextDecoder("utf8");
|
|
20466
|
+
let buffered = "";
|
|
20467
|
+
const fileDelivery = opts.fileDelivery ?? null;
|
|
20468
|
+
let firedFileTools = [];
|
|
20469
|
+
let suppressing = opts.suppressResumeReplay === true;
|
|
20470
|
+
let discardedTurns = 0;
|
|
20471
|
+
let discardedFileTools = 0;
|
|
20472
|
+
while (!abort.signal.aborted) {
|
|
20473
|
+
const { value, done } = await reader.read();
|
|
20474
|
+
if (done) break;
|
|
20475
|
+
buffered += decoder.decode(value, { stream: true });
|
|
20476
|
+
let nl = buffered.indexOf("\n");
|
|
20477
|
+
while (nl !== -1) {
|
|
20478
|
+
const line = buffered.slice(0, nl);
|
|
20479
|
+
buffered = buffered.slice(nl + 1);
|
|
20480
|
+
nl = buffered.indexOf("\n");
|
|
20481
|
+
if (!line) continue;
|
|
20482
|
+
let event;
|
|
20483
|
+
try {
|
|
20484
|
+
event = JSON.parse(line);
|
|
20485
|
+
} catch (err) {
|
|
20486
|
+
console.error(
|
|
20487
|
+
`${tag} jsonl-parse-skip sessionId=${entry.sessionId.slice(0, 8)} bytes=${line.length} message=${err instanceof Error ? err.message : String(err)}`
|
|
20488
|
+
);
|
|
20489
|
+
continue;
|
|
20490
|
+
}
|
|
20491
|
+
if (event.type === "__channel_resume_boundary__") {
|
|
20492
|
+
if (suppressing) {
|
|
20493
|
+
suppressing = false;
|
|
20494
|
+
console.error(
|
|
20495
|
+
`${tag} follower-resume-boundary sessionId=${sid} discarded-head-turns=${discardedTurns} discarded-head-filetools=${discardedFileTools}`
|
|
20496
|
+
);
|
|
20497
|
+
}
|
|
20498
|
+
continue;
|
|
20499
|
+
}
|
|
20500
|
+
if (event.type === "user") {
|
|
20501
|
+
entry.pendingTurnText = "";
|
|
20502
|
+
firedFileTools = [];
|
|
20503
|
+
continue;
|
|
20504
|
+
}
|
|
20505
|
+
if (event.type !== "assistant") continue;
|
|
20506
|
+
const msg = event.message;
|
|
20507
|
+
if (!msg) continue;
|
|
20508
|
+
let sawContent = false;
|
|
20509
|
+
if (Array.isArray(msg.content)) {
|
|
20510
|
+
for (const block of msg.content) {
|
|
20511
|
+
if (block?.type === "text" && typeof block.text === "string") {
|
|
20512
|
+
entry.pendingTurnText += block.text;
|
|
20513
|
+
sawContent = true;
|
|
20514
|
+
} else if (block?.type === "tool_use") {
|
|
20515
|
+
sawContent = true;
|
|
20516
|
+
if (fileDelivery && typeof block.name === "string" && fileDelivery.isFileDeliveryTool(block.name)) {
|
|
20517
|
+
if (suppressing) {
|
|
20518
|
+
discardedFileTools += 1;
|
|
20519
|
+
continue;
|
|
20520
|
+
}
|
|
20521
|
+
firedFileTools.push(block.name);
|
|
20522
|
+
try {
|
|
20523
|
+
await fileDelivery.onFileToolUse({ toolName: block.name, input: block.input });
|
|
20524
|
+
} catch (err) {
|
|
20525
|
+
console.error(
|
|
20526
|
+
`${tag} file-delivery-error sessionId=${sid} tool=${block.name} message=${err instanceof Error ? err.message : String(err)}`
|
|
20527
|
+
);
|
|
20528
|
+
}
|
|
20529
|
+
}
|
|
20530
|
+
}
|
|
20531
|
+
}
|
|
20532
|
+
}
|
|
20533
|
+
if (msg.stop_reason === "end_turn") {
|
|
20534
|
+
if (suppressing) {
|
|
20535
|
+
entry.pendingTurnText = "";
|
|
20536
|
+
firedFileTools = [];
|
|
20537
|
+
discardedTurns += 1;
|
|
20538
|
+
continue;
|
|
20539
|
+
}
|
|
20540
|
+
const flush = entry.pendingTurnText;
|
|
20541
|
+
entry.pendingTurnText = "";
|
|
20542
|
+
if (flush.trim()) {
|
|
20543
|
+
await fanOut(entry.subscribers, flush, opts.onError, tag);
|
|
20544
|
+
}
|
|
20545
|
+
if (fileDelivery && firedFileTools.length > 0) {
|
|
20546
|
+
const fired = firedFileTools;
|
|
20547
|
+
firedFileTools = [];
|
|
20548
|
+
try {
|
|
20549
|
+
fileDelivery.onTurnEnd(fired);
|
|
20550
|
+
} catch (err) {
|
|
20551
|
+
console.error(
|
|
20552
|
+
`${tag} file-delivery-error sessionId=${sid} phase=turn-end message=${err instanceof Error ? err.message : String(err)}`
|
|
20553
|
+
);
|
|
20554
|
+
}
|
|
20555
|
+
}
|
|
20556
|
+
opts.onTurnComplete?.();
|
|
20557
|
+
} else if (sawContent && !suppressing) {
|
|
20558
|
+
opts.onActivity?.();
|
|
20559
|
+
}
|
|
20560
|
+
}
|
|
20561
|
+
}
|
|
20562
|
+
} catch (err) {
|
|
20563
|
+
if (!abort.signal.aborted) {
|
|
20564
|
+
opts.onError?.(`follower-error: ${err instanceof Error ? err.message : String(err)}`);
|
|
20781
20565
|
}
|
|
20782
20566
|
} finally {
|
|
20783
|
-
|
|
20784
|
-
}
|
|
20785
|
-
}
|
|
20786
|
-
/** Handle one inbound webchat message (browser POST → here). */
|
|
20787
|
-
async handleInbound(input) {
|
|
20788
|
-
const bytes = Buffer.byteLength(input.text, "utf8");
|
|
20789
|
-
const hadSubscriber = this.hub.hasSubscriber(input.key);
|
|
20790
|
-
const willDeliverNow = this.hub.isReady(input.key);
|
|
20791
|
-
this.hub.deliver(
|
|
20792
|
-
{ key: input.key, text: input.text, messageId: `wc-${++this.seq}` },
|
|
20793
|
-
Date.now()
|
|
20794
|
-
);
|
|
20795
|
-
if (willDeliverNow) this.lastDeliveredAt.set(input.key, Date.now());
|
|
20796
|
-
console.error(
|
|
20797
|
-
`[webchat:inbound] op=received key=${keyDisplay(input.key)} role=${input.role} bytes=${bytes} delivery=${willDeliverNow ? "immediate" : "queued"}`
|
|
20798
|
-
);
|
|
20799
|
-
if (input.role === "admin") {
|
|
20800
|
-
this.recoverable.set(input.key, {
|
|
20801
|
-
accountId: input.accountId,
|
|
20802
|
-
key: input.key,
|
|
20803
|
-
role: input.role,
|
|
20804
|
-
...input.sessionId !== void 0 ? { sessionId: input.sessionId } : {},
|
|
20805
|
-
...input.adminUserId !== void 0 ? { adminUserId: input.adminUserId } : {}
|
|
20806
|
-
});
|
|
20567
|
+
opts.onClose();
|
|
20807
20568
|
}
|
|
20808
|
-
|
|
20809
|
-
|
|
20810
|
-
|
|
20811
|
-
|
|
20812
|
-
|
|
20813
|
-
|
|
20814
|
-
|
|
20815
|
-
|
|
20816
|
-
|
|
20817
|
-
|
|
20818
|
-
|
|
20819
|
-
|
|
20820
|
-
|
|
20821
|
-
|
|
20822
|
-
|
|
20823
|
-
|
|
20824
|
-
|
|
20825
|
-
|
|
20826
|
-
|
|
20827
|
-
|
|
20828
|
-
|
|
20829
|
-
|
|
20830
|
-
|
|
20831
|
-
|
|
20832
|
-
|
|
20833
|
-
|
|
20834
|
-
|
|
20835
|
-
|
|
20836
|
-
|
|
20837
|
-
|
|
20838
|
-
|
|
20839
|
-
|
|
20840
|
-
|
|
20569
|
+
})();
|
|
20570
|
+
return abort;
|
|
20571
|
+
}
|
|
20572
|
+
async function fanOut(subscribers, text, onError, tag) {
|
|
20573
|
+
const callbacks = Array.from(subscribers);
|
|
20574
|
+
await Promise.all(
|
|
20575
|
+
callbacks.map(
|
|
20576
|
+
(cb) => Promise.resolve().then(() => cb(text)).catch((err) => {
|
|
20577
|
+
const m = err instanceof Error ? err.message : String(err);
|
|
20578
|
+
console.error(`${tag} subscriber-error message=${m}`);
|
|
20579
|
+
onError?.(`subscriber-error: ${m}`);
|
|
20580
|
+
})
|
|
20581
|
+
)
|
|
20582
|
+
);
|
|
20583
|
+
}
|
|
20584
|
+
|
|
20585
|
+
// app/lib/channel-pty-bridge/file-delivery.ts
|
|
20586
|
+
var SEND_USER_FILE = "SendUserFile";
|
|
20587
|
+
function makeFileDelivery(opts) {
|
|
20588
|
+
const { entry, tag, channel, sendFile } = opts;
|
|
20589
|
+
let failedFiles = [];
|
|
20590
|
+
let attempts = 0;
|
|
20591
|
+
return {
|
|
20592
|
+
isFileDeliveryTool(toolName) {
|
|
20593
|
+
return toolName === SEND_USER_FILE;
|
|
20594
|
+
},
|
|
20595
|
+
async onFileToolUse(use) {
|
|
20596
|
+
if (use.toolName !== SEND_USER_FILE) return;
|
|
20597
|
+
const input = use.input ?? {};
|
|
20598
|
+
const files = Array.isArray(input.files) ? input.files.filter((f) => typeof f === "string") : [];
|
|
20599
|
+
const caption = typeof input.caption === "string" ? input.caption : void 0;
|
|
20600
|
+
for (let i = 0; i < files.length; i++) {
|
|
20601
|
+
attempts++;
|
|
20602
|
+
const result = await sendFile(files[i], i === 0 ? caption : void 0);
|
|
20603
|
+
const name = files[i].split("/").pop() ?? files[i];
|
|
20604
|
+
console.error(`${tag} op=file-forward channel=${channel} file=${name} outcome=${result.ok ? "ok" : "fail"}`);
|
|
20605
|
+
if (!result.ok) failedFiles.push(files[i]);
|
|
20606
|
+
}
|
|
20607
|
+
},
|
|
20608
|
+
onTurnEnd(firedTools) {
|
|
20609
|
+
const failed = failedFiles;
|
|
20610
|
+
const tried = attempts;
|
|
20611
|
+
failedFiles = [];
|
|
20612
|
+
attempts = 0;
|
|
20613
|
+
const sid = entry.sessionId.slice(0, 8);
|
|
20614
|
+
for (const file of failed) {
|
|
20615
|
+
console.error(
|
|
20616
|
+
`${tag} file-delivery-unreconciled sender=${entry.senderId} sessionId=${sid} tool=${SEND_USER_FILE} file=${file}`
|
|
20617
|
+
);
|
|
20618
|
+
}
|
|
20619
|
+
if (firedTools.includes(SEND_USER_FILE) && tried === 0) {
|
|
20620
|
+
console.error(
|
|
20621
|
+
`${tag} file-delivery-unreconciled sender=${entry.senderId} sessionId=${sid} tool=${SEND_USER_FILE}`
|
|
20622
|
+
);
|
|
20841
20623
|
}
|
|
20842
20624
|
}
|
|
20843
|
-
}
|
|
20844
|
-
}
|
|
20625
|
+
};
|
|
20626
|
+
}
|
|
20845
20627
|
|
|
20846
|
-
// app/lib/
|
|
20847
|
-
|
|
20848
|
-
|
|
20628
|
+
// app/lib/whatsapp/inbound/file-delivery-bridge.ts
|
|
20629
|
+
var TAG34 = "[whatsapp-adaptor]";
|
|
20630
|
+
var SEND_USER_FILE2 = "SendUserFile";
|
|
20631
|
+
var WHATSAPP_SEND_DOCUMENT = "whatsapp-send-document";
|
|
20632
|
+
function platformRoot() {
|
|
20633
|
+
return process.env.MAXY_PLATFORM_ROOT || "";
|
|
20634
|
+
}
|
|
20635
|
+
function makeWhatsAppSendFile(entry) {
|
|
20636
|
+
return async (filePath, caption) => {
|
|
20637
|
+
let maxyAccountId;
|
|
20638
|
+
try {
|
|
20639
|
+
maxyAccountId = resolvePlatformAccountId();
|
|
20640
|
+
} catch (err) {
|
|
20641
|
+
console.error(
|
|
20642
|
+
`${TAG34} file-delivery reject reason=account-unresolved sender=${entry.senderId} message=${err instanceof Error ? err.message : String(err)}`
|
|
20643
|
+
);
|
|
20644
|
+
return { ok: false, error: "account-unresolved" };
|
|
20645
|
+
}
|
|
20646
|
+
const result = await sendWhatsAppDocument({
|
|
20647
|
+
to: entry.senderId,
|
|
20648
|
+
filePath,
|
|
20649
|
+
caption,
|
|
20650
|
+
accountId: entry.accountId,
|
|
20651
|
+
maxyAccountId,
|
|
20652
|
+
platformRoot: platformRoot()
|
|
20653
|
+
});
|
|
20654
|
+
if (result.ok) return { ok: true };
|
|
20655
|
+
console.error(
|
|
20656
|
+
`${TAG34} file-delivery reject reason=send-failed sender=${entry.senderId} status=${result.status} message=${result.error}`
|
|
20657
|
+
);
|
|
20658
|
+
return { ok: false, error: result.error };
|
|
20659
|
+
};
|
|
20660
|
+
}
|
|
20661
|
+
function makeWhatsAppFileDelivery(entry) {
|
|
20662
|
+
const shared = makeFileDelivery({
|
|
20663
|
+
entry,
|
|
20664
|
+
tag: TAG34,
|
|
20665
|
+
channel: "whatsapp",
|
|
20666
|
+
sendFile: makeWhatsAppSendFile(entry)
|
|
20667
|
+
});
|
|
20668
|
+
let turnStartedAt = null;
|
|
20669
|
+
let routeCalls = [];
|
|
20849
20670
|
return {
|
|
20850
|
-
|
|
20851
|
-
|
|
20852
|
-
|
|
20853
|
-
|
|
20854
|
-
|
|
20855
|
-
|
|
20856
|
-
|
|
20857
|
-
|
|
20858
|
-
|
|
20859
|
-
|
|
20860
|
-
|
|
20861
|
-
|
|
20862
|
-
|
|
20863
|
-
|
|
20671
|
+
isFileDeliveryTool(toolName) {
|
|
20672
|
+
return toolName === SEND_USER_FILE2 || toolName === WHATSAPP_SEND_DOCUMENT;
|
|
20673
|
+
},
|
|
20674
|
+
async onFileToolUse(use) {
|
|
20675
|
+
if (turnStartedAt === null) turnStartedAt = Date.now();
|
|
20676
|
+
if (use.toolName === WHATSAPP_SEND_DOCUMENT) {
|
|
20677
|
+
const input = use.input ?? {};
|
|
20678
|
+
routeCalls.push({
|
|
20679
|
+
to: typeof input.to === "string" ? input.to : void 0,
|
|
20680
|
+
filePath: typeof input.filePath === "string" ? input.filePath : void 0
|
|
20681
|
+
});
|
|
20682
|
+
return;
|
|
20683
|
+
}
|
|
20684
|
+
await shared.onFileToolUse(use);
|
|
20685
|
+
},
|
|
20686
|
+
onTurnEnd(firedTools) {
|
|
20687
|
+
const startedAt = turnStartedAt ?? 0;
|
|
20688
|
+
const routes = routeCalls;
|
|
20689
|
+
turnStartedAt = null;
|
|
20690
|
+
routeCalls = [];
|
|
20691
|
+
const sid = entry.sessionId.slice(0, 8);
|
|
20692
|
+
shared.onTurnEnd(firedTools);
|
|
20693
|
+
for (const call of routes) {
|
|
20694
|
+
const routeAt = call.to !== void 0 && call.filePath !== void 0 ? routeDocumentOutboundAt(call.to, call.filePath) : void 0;
|
|
20695
|
+
const delivered = routeAt !== void 0 && routeAt >= startedAt;
|
|
20696
|
+
if (!delivered) {
|
|
20697
|
+
const fileField = call.filePath !== void 0 ? ` file=${call.filePath}` : "";
|
|
20698
|
+
console.error(
|
|
20699
|
+
`${TAG34} file-delivery-unreconciled sender=${entry.senderId} sessionId=${sid} tool=${WHATSAPP_SEND_DOCUMENT}${fileField}`
|
|
20700
|
+
);
|
|
20701
|
+
}
|
|
20702
|
+
}
|
|
20703
|
+
}
|
|
20864
20704
|
};
|
|
20865
20705
|
}
|
|
20866
20706
|
|
|
@@ -21211,6 +21051,122 @@ function buildTelegramSpawnRequest(input) {
|
|
|
21211
21051
|
};
|
|
21212
21052
|
}
|
|
21213
21053
|
|
|
21054
|
+
// app/lib/telegram/outbound/send-document.ts
|
|
21055
|
+
import { realpathSync as realpathSync6 } from "fs";
|
|
21056
|
+
import { readFile as readFile7, stat as stat7 } from "fs/promises";
|
|
21057
|
+
import { resolve as resolve29, basename as basename9 } from "path";
|
|
21058
|
+
var TAG35 = "[telegram:outbound]";
|
|
21059
|
+
var TELEGRAM_DOCUMENT_MAX_BYTES = 50 * 1024 * 1024;
|
|
21060
|
+
async function sendTelegramDocument(input) {
|
|
21061
|
+
const { botToken, chatId, filePath, caption, maxyAccountId, platformRoot: platformRoot3 } = input;
|
|
21062
|
+
if (!botToken || !filePath) {
|
|
21063
|
+
return { ok: false, status: 400, error: "Missing required fields: botToken, filePath" };
|
|
21064
|
+
}
|
|
21065
|
+
if (!maxyAccountId || !platformRoot3) {
|
|
21066
|
+
return { ok: false, status: 400, error: "Cannot validate file path: missing account or platform context" };
|
|
21067
|
+
}
|
|
21068
|
+
const accountDir = resolve29(platformRoot3, "..", "data/accounts", maxyAccountId);
|
|
21069
|
+
let resolvedPath;
|
|
21070
|
+
try {
|
|
21071
|
+
resolvedPath = realpathSync6(filePath);
|
|
21072
|
+
const accountResolved = realpathSync6(accountDir);
|
|
21073
|
+
if (!resolvedPath.startsWith(accountResolved + "/")) {
|
|
21074
|
+
console.error(`${TAG35} document REJECTED reason=outside_account_directory`);
|
|
21075
|
+
return { ok: false, status: 403, error: "Access denied: file is outside the account directory" };
|
|
21076
|
+
}
|
|
21077
|
+
} catch (err) {
|
|
21078
|
+
const code = err.code;
|
|
21079
|
+
if (code === "ENOENT") {
|
|
21080
|
+
console.error(`${TAG35} document ENOENT path=${filePath}`);
|
|
21081
|
+
return { ok: false, status: 404, error: `File not found: ${filePath}` };
|
|
21082
|
+
}
|
|
21083
|
+
console.error(`${TAG35} document path error: ${String(err)}`);
|
|
21084
|
+
return { ok: false, status: 500, error: String(err) };
|
|
21085
|
+
}
|
|
21086
|
+
const fileStat = await stat7(resolvedPath);
|
|
21087
|
+
if (fileStat.size > TELEGRAM_DOCUMENT_MAX_BYTES) {
|
|
21088
|
+
return {
|
|
21089
|
+
ok: false,
|
|
21090
|
+
status: 400,
|
|
21091
|
+
error: `File exceeds 50 MB limit (${(fileStat.size / 1024 / 1024).toFixed(1)} MB)`
|
|
21092
|
+
};
|
|
21093
|
+
}
|
|
21094
|
+
const filename = basename9(resolvedPath);
|
|
21095
|
+
const buffer = Buffer.from(await readFile7(resolvedPath));
|
|
21096
|
+
const form = new FormData();
|
|
21097
|
+
form.append("chat_id", String(chatId));
|
|
21098
|
+
if (caption) form.append("caption", caption);
|
|
21099
|
+
form.append("document", new File([buffer], filename, { type: detectMimeType(resolvedPath) }));
|
|
21100
|
+
let ok = false;
|
|
21101
|
+
let messageId;
|
|
21102
|
+
let error = "send failed";
|
|
21103
|
+
try {
|
|
21104
|
+
const res = await fetch(`https://api.telegram.org/bot${botToken}/sendDocument`, {
|
|
21105
|
+
method: "POST",
|
|
21106
|
+
body: form
|
|
21107
|
+
});
|
|
21108
|
+
const data = await res.json();
|
|
21109
|
+
ok = data.ok;
|
|
21110
|
+
messageId = data.result?.message_id;
|
|
21111
|
+
if (!data.ok) error = data.description ?? "Unknown Telegram error";
|
|
21112
|
+
} catch (e) {
|
|
21113
|
+
error = e instanceof Error ? e.message : String(e);
|
|
21114
|
+
}
|
|
21115
|
+
console.error(
|
|
21116
|
+
`${TAG35} sent document to=${chatId} file=${filename} bytes=${fileStat.size} ok=${ok}` + (messageId !== void 0 ? ` id=${messageId}` : "")
|
|
21117
|
+
);
|
|
21118
|
+
return ok ? { ok: true, messageId } : { ok: false, status: 500, error };
|
|
21119
|
+
}
|
|
21120
|
+
|
|
21121
|
+
// app/lib/telegram/outbound/file-delivery.ts
|
|
21122
|
+
var TAG36 = "[telegram:outbound]";
|
|
21123
|
+
function platformRoot2() {
|
|
21124
|
+
return process.env.MAXY_PLATFORM_ROOT || "";
|
|
21125
|
+
}
|
|
21126
|
+
function makeTelegramSendFile(entry) {
|
|
21127
|
+
let botToken;
|
|
21128
|
+
return async (filePath, caption) => {
|
|
21129
|
+
if (botToken === void 0) {
|
|
21130
|
+
const tg = resolveAccount()?.config.telegram;
|
|
21131
|
+
botToken = (entry.role === "admin" ? tg?.adminBotToken : tg?.publicBotToken) ?? null;
|
|
21132
|
+
}
|
|
21133
|
+
if (!botToken) {
|
|
21134
|
+
console.error(`${TAG36} file-delivery reject reason=no-bot-token sender=${entry.senderId} role=${entry.role}`);
|
|
21135
|
+
return { ok: false, error: "no-bot-token" };
|
|
21136
|
+
}
|
|
21137
|
+
if (entry.replyTarget == null) {
|
|
21138
|
+
console.error(`${TAG36} file-delivery reject reason=no-reply-target sender=${entry.senderId} role=${entry.role}`);
|
|
21139
|
+
return { ok: false, error: "no-reply-target" };
|
|
21140
|
+
}
|
|
21141
|
+
let maxyAccountId;
|
|
21142
|
+
try {
|
|
21143
|
+
maxyAccountId = resolvePlatformAccountId();
|
|
21144
|
+
} catch (err) {
|
|
21145
|
+
console.error(
|
|
21146
|
+
`${TAG36} file-delivery reject reason=account-unresolved sender=${entry.senderId} message=${err instanceof Error ? err.message : String(err)}`
|
|
21147
|
+
);
|
|
21148
|
+
return { ok: false, error: "account-unresolved" };
|
|
21149
|
+
}
|
|
21150
|
+
const result = await sendTelegramDocument({
|
|
21151
|
+
botToken,
|
|
21152
|
+
chatId: Number(entry.replyTarget),
|
|
21153
|
+
filePath,
|
|
21154
|
+
caption,
|
|
21155
|
+
maxyAccountId,
|
|
21156
|
+
platformRoot: platformRoot2()
|
|
21157
|
+
});
|
|
21158
|
+
return result.ok ? { ok: true } : { ok: false, error: result.error };
|
|
21159
|
+
};
|
|
21160
|
+
}
|
|
21161
|
+
function makeTelegramFileDelivery(entry) {
|
|
21162
|
+
return makeFileDelivery({
|
|
21163
|
+
entry,
|
|
21164
|
+
tag: TAG36,
|
|
21165
|
+
channel: "telegram",
|
|
21166
|
+
sendFile: makeTelegramSendFile(entry)
|
|
21167
|
+
});
|
|
21168
|
+
}
|
|
21169
|
+
|
|
21214
21170
|
// app/lib/telegram/gateway/native-file-follower.ts
|
|
21215
21171
|
function startTelegramNativeFileFollower(input) {
|
|
21216
21172
|
const entry = {
|
|
@@ -21245,12 +21201,166 @@ function startTelegramNativeFileFollower(input) {
|
|
|
21245
21201
|
}
|
|
21246
21202
|
|
|
21247
21203
|
// server/telegram-descriptor.ts
|
|
21248
|
-
import { resolve as
|
|
21204
|
+
import { resolve as resolve30, join as join31 } from "path";
|
|
21249
21205
|
function telegramGatewayUrl() {
|
|
21250
21206
|
return `http://127.0.0.1:${process.env.MAXY_UI_INTERNAL_PORT ?? ""}`;
|
|
21251
21207
|
}
|
|
21252
21208
|
function telegramServerPath() {
|
|
21253
|
-
return process.env.MAXY_TELEGRAM_CHANNEL_SERVER_PATH ??
|
|
21209
|
+
return process.env.MAXY_TELEGRAM_CHANNEL_SERVER_PATH ?? resolve30(process.env.MAXY_PLATFORM_ROOT ?? join31(__dirname, ".."), "services/telegram-channel/dist/server.js");
|
|
21210
|
+
}
|
|
21211
|
+
|
|
21212
|
+
// app/lib/channel-pty-bridge/public-session-end-review.ts
|
|
21213
|
+
import { randomUUID as randomUUID13 } from "crypto";
|
|
21214
|
+
var TAG37 = "[public-session-review]";
|
|
21215
|
+
var CONSUMED_CAP = 500;
|
|
21216
|
+
var consumed = /* @__PURE__ */ new Set();
|
|
21217
|
+
function consumeOnce(sessionId) {
|
|
21218
|
+
if (consumed.has(sessionId)) return false;
|
|
21219
|
+
consumed.add(sessionId);
|
|
21220
|
+
if (consumed.size > CONSUMED_CAP) {
|
|
21221
|
+
const oldest = consumed.values().next().value;
|
|
21222
|
+
if (oldest !== void 0) consumed.delete(oldest);
|
|
21223
|
+
}
|
|
21224
|
+
return true;
|
|
21225
|
+
}
|
|
21226
|
+
function managerBase3() {
|
|
21227
|
+
const port2 = requirePortEnv("CLAUDE_SESSION_MANAGER_PORT", { tag: "public-session-end-review:manager" });
|
|
21228
|
+
return `http://127.0.0.1:${port2}`;
|
|
21229
|
+
}
|
|
21230
|
+
function uiBase() {
|
|
21231
|
+
const port2 = requirePortEnv("MAXY_UI_INTERNAL_PORT", { tag: "public-session-end-review:ui" });
|
|
21232
|
+
return `http://127.0.0.1:${port2}`;
|
|
21233
|
+
}
|
|
21234
|
+
async function fetchJsonl(sessionId) {
|
|
21235
|
+
try {
|
|
21236
|
+
const res = await fetch(`${managerBase3()}/${sessionId}/log?download=1`);
|
|
21237
|
+
if (!res.ok) return null;
|
|
21238
|
+
return await res.text();
|
|
21239
|
+
} catch (err) {
|
|
21240
|
+
console.error(
|
|
21241
|
+
`${TAG37} jsonl-fetch-failed sessionId=${sessionId} err="${err instanceof Error ? err.message : String(err)}"`
|
|
21242
|
+
);
|
|
21243
|
+
return null;
|
|
21244
|
+
}
|
|
21245
|
+
}
|
|
21246
|
+
function countOperatorTurns(jsonl) {
|
|
21247
|
+
let count = 0;
|
|
21248
|
+
for (const raw of jsonl.split("\n")) {
|
|
21249
|
+
const line = raw.trim();
|
|
21250
|
+
if (!line) continue;
|
|
21251
|
+
try {
|
|
21252
|
+
const obj = JSON.parse(line);
|
|
21253
|
+
if (obj.type === "user") count += 1;
|
|
21254
|
+
} catch {
|
|
21255
|
+
}
|
|
21256
|
+
}
|
|
21257
|
+
return count;
|
|
21258
|
+
}
|
|
21259
|
+
async function fetchPriorWrites(sliceToken, accountId) {
|
|
21260
|
+
const url = `${uiBase()}/api/admin/public-session-context?sliceToken=${encodeURIComponent(sliceToken)}&accountId=${encodeURIComponent(accountId)}`;
|
|
21261
|
+
try {
|
|
21262
|
+
const res = await fetch(url);
|
|
21263
|
+
if (!res.ok) {
|
|
21264
|
+
console.error(
|
|
21265
|
+
`${TAG37} prior-writes-fetch-failed sliceToken=${sliceToken.slice(0, 8)} status=${res.status}`
|
|
21266
|
+
);
|
|
21267
|
+
return [];
|
|
21268
|
+
}
|
|
21269
|
+
const body = await res.json();
|
|
21270
|
+
return Array.isArray(body.writes) ? body.writes : [];
|
|
21271
|
+
} catch (err) {
|
|
21272
|
+
console.error(
|
|
21273
|
+
`${TAG37} prior-writes-fetch-threw sliceToken=${sliceToken.slice(0, 8)} err="${err instanceof Error ? err.message : String(err)}"`
|
|
21274
|
+
);
|
|
21275
|
+
return [];
|
|
21276
|
+
}
|
|
21277
|
+
}
|
|
21278
|
+
function composeInitialMessage(input) {
|
|
21279
|
+
const priorJson = JSON.stringify(input.priorWrites, null, 2);
|
|
21280
|
+
return [
|
|
21281
|
+
`The gated public-agent session for agent "${input.agentSlug}" has just been reaped. Review the transcript and decide what is worth saving to the visitor's per-visitor memory slice. Dispatch \`database-operator\` for each individual write via the Task tool. Do not call memory-write or memory-update directly.`,
|
|
21282
|
+
"",
|
|
21283
|
+
"<slice-token>",
|
|
21284
|
+
input.sliceToken,
|
|
21285
|
+
"</slice-token>",
|
|
21286
|
+
"",
|
|
21287
|
+
"<person-id>",
|
|
21288
|
+
input.personId ?? "(unknown)",
|
|
21289
|
+
"</person-id>",
|
|
21290
|
+
"",
|
|
21291
|
+
"<prior-writes>",
|
|
21292
|
+
priorJson,
|
|
21293
|
+
"</prior-writes>",
|
|
21294
|
+
"",
|
|
21295
|
+
"<transcript>",
|
|
21296
|
+
input.jsonl,
|
|
21297
|
+
"</transcript>"
|
|
21298
|
+
].join("\n");
|
|
21299
|
+
}
|
|
21300
|
+
async function dispatchReviewer(input, initialMessage) {
|
|
21301
|
+
const sessionId = randomUUID13();
|
|
21302
|
+
console.log(`${TAG37} route target=rc-spawn sessionId=${sessionId.slice(0, 8)} sourceSession=${input.sessionId.slice(0, 8)}`);
|
|
21303
|
+
const spawned = await managerRcSpawn({
|
|
21304
|
+
sessionId,
|
|
21305
|
+
initialMessage,
|
|
21306
|
+
closeAfterTurn: true,
|
|
21307
|
+
sliceToken: input.sliceToken,
|
|
21308
|
+
personId: input.personId ?? void 0,
|
|
21309
|
+
logContext: `public-session-end sourceSession=${input.sessionId.slice(0, 8)}`
|
|
21310
|
+
});
|
|
21311
|
+
if ("error" in spawned) {
|
|
21312
|
+
return { ok: false, reason: `rc-spawn-${spawned.status}-${spawned.error}` };
|
|
21313
|
+
}
|
|
21314
|
+
return { ok: true, managerSessionId: spawned.sessionId };
|
|
21315
|
+
}
|
|
21316
|
+
async function firePublicSessionEndReview(input) {
|
|
21317
|
+
const sliceShort = input.sliceToken.slice(0, 8);
|
|
21318
|
+
const personField = input.personId ?? "none";
|
|
21319
|
+
const dispatchedAt = Date.now();
|
|
21320
|
+
if (consumed.has(input.sessionId)) {
|
|
21321
|
+
console.log(
|
|
21322
|
+
`${TAG37} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-already-reviewed`
|
|
21323
|
+
);
|
|
21324
|
+
return;
|
|
21325
|
+
}
|
|
21326
|
+
const jsonl = await fetchJsonl(input.sessionId);
|
|
21327
|
+
if (!jsonl) {
|
|
21328
|
+
console.log(
|
|
21329
|
+
`${TAG37} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-jsonl-missing`
|
|
21330
|
+
);
|
|
21331
|
+
return;
|
|
21332
|
+
}
|
|
21333
|
+
const operatorTurns = countOperatorTurns(jsonl);
|
|
21334
|
+
if (operatorTurns === 0) {
|
|
21335
|
+
console.log(
|
|
21336
|
+
`${TAG37} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-no-turns`
|
|
21337
|
+
);
|
|
21338
|
+
return;
|
|
21339
|
+
}
|
|
21340
|
+
const priorWrites = await fetchPriorWrites(input.sliceToken, input.accountId);
|
|
21341
|
+
const initialMessage = composeInitialMessage({
|
|
21342
|
+
agentSlug: input.agentSlug,
|
|
21343
|
+
sliceToken: input.sliceToken,
|
|
21344
|
+
personId: input.personId,
|
|
21345
|
+
jsonl,
|
|
21346
|
+
priorWrites
|
|
21347
|
+
});
|
|
21348
|
+
if (!consumeOnce(input.sessionId)) {
|
|
21349
|
+
console.log(
|
|
21350
|
+
`${TAG37} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-already-reviewed`
|
|
21351
|
+
);
|
|
21352
|
+
return;
|
|
21353
|
+
}
|
|
21354
|
+
const dispatched = await dispatchReviewer(input, initialMessage);
|
|
21355
|
+
if (!dispatched.ok) {
|
|
21356
|
+
console.error(
|
|
21357
|
+
`${TAG37} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-spawn-failed reason=${dispatched.reason}`
|
|
21358
|
+
);
|
|
21359
|
+
return;
|
|
21360
|
+
}
|
|
21361
|
+
console.log(
|
|
21362
|
+
`${TAG37} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=dispatched managerSessionId=${dispatched.managerSessionId} operatorTurns=${operatorTurns} priorWrites=${priorWrites.length} ms=${Date.now() - dispatchedAt}`
|
|
21363
|
+
);
|
|
21254
21364
|
}
|
|
21255
21365
|
|
|
21256
21366
|
// app/lib/whatsapp/inbound/channel-admin-binding-drift.ts
|
|
@@ -21319,7 +21429,7 @@ function broadcastAdminShutdown(reason) {
|
|
|
21319
21429
|
// ../lib/entitlement/src/index.ts
|
|
21320
21430
|
import { createPublicKey, createHash as createHash5, verify as cryptoVerify } from "crypto";
|
|
21321
21431
|
import { existsSync as existsSync31, readFileSync as readFileSync32, statSync as statSync12 } from "fs";
|
|
21322
|
-
import { resolve as
|
|
21432
|
+
import { resolve as resolve31 } from "path";
|
|
21323
21433
|
|
|
21324
21434
|
// ../lib/entitlement/src/canonicalize.ts
|
|
21325
21435
|
function canonicalize(value) {
|
|
@@ -21354,7 +21464,7 @@ var PUBKEY_SHA256 = "8eee6bcb33545fd13b16d3199a5735ca5db5062834c7b49dfe4f23801d9
|
|
|
21354
21464
|
var GRACE_DAYS = 7;
|
|
21355
21465
|
var GRACE_MS = GRACE_DAYS * 24 * 60 * 60 * 1e3;
|
|
21356
21466
|
function pubkeyPath(brand) {
|
|
21357
|
-
return
|
|
21467
|
+
return resolve31(brand.platformRoot, "lib", "entitlement", "rubytech-pubkey.pem");
|
|
21358
21468
|
}
|
|
21359
21469
|
var memo = null;
|
|
21360
21470
|
function memoKey(mtimeMs, account) {
|
|
@@ -21366,7 +21476,7 @@ function resolveEntitlement(brand, account) {
|
|
|
21366
21476
|
if (brand.commercialMode !== true) {
|
|
21367
21477
|
return logResolved(implicitTrust(account), null);
|
|
21368
21478
|
}
|
|
21369
|
-
const entitlementPath =
|
|
21479
|
+
const entitlementPath = resolve31(brand.configDir, "entitlement.json");
|
|
21370
21480
|
if (!existsSync31(entitlementPath)) {
|
|
21371
21481
|
return logResolved(anonymousFallback("missing"), { reason: "missing" });
|
|
21372
21482
|
}
|
|
@@ -21628,7 +21738,7 @@ var app54 = new Hono();
|
|
|
21628
21738
|
var nativeFileFollowers = /* @__PURE__ */ new Map();
|
|
21629
21739
|
var waGateway = new WaGateway({
|
|
21630
21740
|
gatewayUrl: `http://127.0.0.1:${process.env.MAXY_UI_INTERNAL_PORT ?? ""}`,
|
|
21631
|
-
serverPath: process.env.MAXY_WA_CHANNEL_SERVER_PATH ??
|
|
21741
|
+
serverPath: process.env.MAXY_WA_CHANNEL_SERVER_PATH ?? resolve32(process.env.MAXY_PLATFORM_ROOT ?? join32(__dirname, ".."), "services/whatsapp-channel/dist/server.js"),
|
|
21632
21742
|
// Task 751 — file delivery on the native channel: resolve the platform
|
|
21633
21743
|
// account + path validation here and funnel through the shared send core.
|
|
21634
21744
|
sendDocument: async ({ senderId, accountId, filePath, caption }) => {
|
|
@@ -21644,7 +21754,7 @@ var waGateway = new WaGateway({
|
|
|
21644
21754
|
caption,
|
|
21645
21755
|
accountId,
|
|
21646
21756
|
maxyAccountId,
|
|
21647
|
-
platformRoot:
|
|
21757
|
+
platformRoot: resolve32(process.env.MAXY_PLATFORM_ROOT ?? join32(__dirname, ".."))
|
|
21648
21758
|
});
|
|
21649
21759
|
return result.ok ? { ok: true, messageId: result.messageId } : { ok: false, error: result.error };
|
|
21650
21760
|
},
|
|
@@ -21747,6 +21857,7 @@ var webchatGateway = new WebchatGateway({
|
|
|
21747
21857
|
deleteSession: (sessionId) => managerDelete(sessionId),
|
|
21748
21858
|
firePublicSessionEndReview: (input) => firePublicSessionEndReview(input)
|
|
21749
21859
|
});
|
|
21860
|
+
setWebchatGateway(webchatGateway);
|
|
21750
21861
|
app54.route("/", webchatGateway.routes());
|
|
21751
21862
|
webchatGateway.startSweeper();
|
|
21752
21863
|
webchatGateway.startPublicReaper();
|
|
@@ -21958,7 +22069,7 @@ app54.post("/__remote-auth/change-password", async (c) => {
|
|
|
21958
22069
|
}
|
|
21959
22070
|
try {
|
|
21960
22071
|
if (targetUserId) {
|
|
21961
|
-
await setAccessPassword(targetUserId, newPassword, USERS_FILE);
|
|
22072
|
+
await setAccessPassword(targetUserId, newPassword, USERS_FILE, { actor: targetUserId, logFile: USERS_AUDIT_LOG });
|
|
21962
22073
|
} else {
|
|
21963
22074
|
await setRemotePassword(newPassword);
|
|
21964
22075
|
}
|
|
@@ -22045,7 +22156,7 @@ app54.post("/api/remote-auth/set-password", async (c) => {
|
|
|
22045
22156
|
}
|
|
22046
22157
|
try {
|
|
22047
22158
|
if (sessionUserId) {
|
|
22048
|
-
await setAccessPassword(sessionUserId, body.password, USERS_FILE);
|
|
22159
|
+
await setAccessPassword(sessionUserId, body.password, USERS_FILE, { actor: sessionUserId, logFile: USERS_AUDIT_LOG });
|
|
22049
22160
|
} else {
|
|
22050
22161
|
await setRemotePassword(body.password);
|
|
22051
22162
|
}
|
|
@@ -22150,8 +22261,8 @@ app54.get("/agent-assets/:slug/:filename", (c) => {
|
|
|
22150
22261
|
console.error(`[agent-assets] no-account slug=${slug} file=${filename}`);
|
|
22151
22262
|
return c.text("Not found", 404);
|
|
22152
22263
|
}
|
|
22153
|
-
const filePath =
|
|
22154
|
-
const expectedDir =
|
|
22264
|
+
const filePath = resolve32(account.accountDir, "agents", slug, "assets", filename);
|
|
22265
|
+
const expectedDir = resolve32(account.accountDir, "agents", slug, "assets");
|
|
22155
22266
|
if (!filePath.startsWith(expectedDir + "/")) {
|
|
22156
22267
|
console.error(`[agent-assets] path-traversal-rejected slug=${slug} file=${filename}`);
|
|
22157
22268
|
return c.text("Forbidden", 403);
|
|
@@ -22180,8 +22291,8 @@ app54.get("/generated/:filename", (c) => {
|
|
|
22180
22291
|
console.error(`[generated] serve file=${filename} status=404`);
|
|
22181
22292
|
return c.text("Not found", 404);
|
|
22182
22293
|
}
|
|
22183
|
-
const filePath =
|
|
22184
|
-
const expectedDir =
|
|
22294
|
+
const filePath = resolve32(account.accountDir, "generated", filename);
|
|
22295
|
+
const expectedDir = resolve32(account.accountDir, "generated");
|
|
22185
22296
|
if (!filePath.startsWith(expectedDir + "/")) {
|
|
22186
22297
|
console.error(`[generated] serve file=${filename} status=403`);
|
|
22187
22298
|
return c.text("Forbidden", 403);
|
|
@@ -22235,11 +22346,11 @@ var brandScript = `<script>window.__BRAND__=${JSON.stringify({
|
|
|
22235
22346
|
var brandThemeColor = BRAND.defaultColors?.primary ?? "#000000";
|
|
22236
22347
|
var brandBackgroundColor = BRAND.defaultColors?.background ?? "#ffffff";
|
|
22237
22348
|
var brandAppIconsExist = [brandAppIcon192Path, brandAppIcon512Path, brandMaskableIconPath].every(
|
|
22238
|
-
(p) => existsSync32(
|
|
22349
|
+
(p) => existsSync32(resolve32(process.cwd(), "public", p.replace(/^\//, "")))
|
|
22239
22350
|
);
|
|
22240
22351
|
var SW_SOURCE = (() => {
|
|
22241
22352
|
try {
|
|
22242
|
-
return readFileSync33(
|
|
22353
|
+
return readFileSync33(resolve32(process.cwd(), "public", "sw.js"), "utf-8");
|
|
22243
22354
|
} catch {
|
|
22244
22355
|
return null;
|
|
22245
22356
|
}
|
|
@@ -22290,7 +22401,7 @@ var clientErrorReporterScript = `<script>
|
|
|
22290
22401
|
function cachedHtml(file) {
|
|
22291
22402
|
let html = htmlCache.get(file);
|
|
22292
22403
|
if (!html) {
|
|
22293
|
-
html = readFileSync33(
|
|
22404
|
+
html = readFileSync33(resolve32(process.cwd(), "public", file), "utf-8");
|
|
22294
22405
|
const productNameEsc = escapeHtml(BRAND.productName);
|
|
22295
22406
|
html = html.replace(/<title>([^<]*)<\/title>/, (_match, inner) => `<title>${escapeHtml(inner).replace(/Maxy/g, productNameEsc)}</title>`);
|
|
22296
22407
|
html = html.replace('href="/favicon.ico"', `href="${escapeHtml(brandFaviconPath)}"`);
|
|
@@ -22408,7 +22519,7 @@ app54.use("/vnc-popout.html", logViewerFetch);
|
|
|
22408
22519
|
app54.get("/vnc-popout.html", (c) => {
|
|
22409
22520
|
let html = htmlCache.get("vnc-popout.html");
|
|
22410
22521
|
if (!html) {
|
|
22411
|
-
html = readFileSync33(
|
|
22522
|
+
html = readFileSync33(resolve32(process.cwd(), "public", "vnc-popout.html"), "utf-8");
|
|
22412
22523
|
const name = escapeHtml(BRAND.productName);
|
|
22413
22524
|
html = html.replace("<title>Browser \u2014 Maxy</title>", `<title>${name}</title>`);
|
|
22414
22525
|
html = html.replace("</head>", ` ${brandScript}
|
|
@@ -22532,7 +22643,7 @@ var httpServer = serve({ fetch: app54.fetch, port, hostname });
|
|
|
22532
22643
|
console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
|
|
22533
22644
|
{
|
|
22534
22645
|
const reconcilePlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join32(__dirname, "..");
|
|
22535
|
-
const reconcileScript =
|
|
22646
|
+
const reconcileScript = resolve32(reconcilePlatformRoot, "plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js");
|
|
22536
22647
|
const RECONCILE_INTERVAL_MS = 12e4;
|
|
22537
22648
|
const runReconcile = () => {
|
|
22538
22649
|
if (!existsSync32(reconcileScript)) return;
|
|
@@ -22554,7 +22665,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
|
|
|
22554
22665
|
}
|
|
22555
22666
|
{
|
|
22556
22667
|
const auditPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join32(__dirname, "..");
|
|
22557
|
-
const auditScript =
|
|
22668
|
+
const auditScript = resolve32(auditPlatformRoot, "plugins/connector/mcp/dist/scripts/audit-connectors.js");
|
|
22558
22669
|
const auditLogDir = process.env.LOG_DIR ?? LOG_DIR;
|
|
22559
22670
|
const CONNECTOR_AUDIT_INTERVAL_MS = 3e5;
|
|
22560
22671
|
const runConnectorAudit = () => {
|
|
@@ -22657,7 +22768,7 @@ try {
|
|
|
22657
22768
|
const migAccount = resolveAccount();
|
|
22658
22769
|
const ownerUserId = migAccount?.config.admins?.find((a) => a.role === "owner")?.userId;
|
|
22659
22770
|
if (ownerUserId) {
|
|
22660
|
-
const result = migrateLegacyRemotePassword(USERS_FILE, REMOTE_PASSWORD_FILE, ownerUserId);
|
|
22771
|
+
const result = migrateLegacyRemotePassword(USERS_FILE, REMOTE_PASSWORD_FILE, ownerUserId, { actor: "boot", logFile: USERS_AUDIT_LOG });
|
|
22661
22772
|
console.error(`[remote-access-migrate] result=${result} ownerUserId=${ownerUserId.slice(0, 8)}`);
|
|
22662
22773
|
} else {
|
|
22663
22774
|
console.error("[remote-access-migrate] result=noop-no-owner ownerUserId=none");
|
|
@@ -22698,6 +22809,35 @@ var adminUserReconcileTimer = setInterval(() => {
|
|
|
22698
22809
|
void runAdminUserReconcileTick();
|
|
22699
22810
|
}, ADMINUSER_RECONCILE_INTERVAL_MS);
|
|
22700
22811
|
if (typeof adminUserReconcileTimer.unref === "function") adminUserReconcileTimer.unref();
|
|
22812
|
+
var USERS_RECONCILE_INTERVAL_MS = 60 * 60 * 1e3;
|
|
22813
|
+
function countUsersRows() {
|
|
22814
|
+
if (!existsSync32(USERS_FILE)) return 0;
|
|
22815
|
+
const raw = readFileSync33(USERS_FILE, "utf-8").trim();
|
|
22816
|
+
if (!raw) return 0;
|
|
22817
|
+
const users = JSON.parse(raw);
|
|
22818
|
+
return users.filter((u) => typeof u.userId === "string").length;
|
|
22819
|
+
}
|
|
22820
|
+
function runUsersReconcileTick() {
|
|
22821
|
+
try {
|
|
22822
|
+
const d = computeAdminStoreDivergence({ usersFile: USERS_FILE, accountsDir: ACCOUNTS_DIR });
|
|
22823
|
+
if (d.errors.length > 0) {
|
|
22824
|
+
console.error(`[users-reconcile] op=error detail=${d.errors.map((e) => `${e.source}:${e.detail}`).join(";")}`);
|
|
22825
|
+
return;
|
|
22826
|
+
}
|
|
22827
|
+
if (d.divergences === 0) {
|
|
22828
|
+
console.error(`[users-reconcile] op=ok admins=${countUsersRows()}`);
|
|
22829
|
+
return;
|
|
22830
|
+
}
|
|
22831
|
+
const accountOnly = d.accountWithoutUsers.map((a) => a.userId.slice(0, 8)).join(",");
|
|
22832
|
+
const usersOnly = d.usersWithoutAccount.map((u) => u.userId.slice(0, 8)).join(",");
|
|
22833
|
+
console.error(`[users-reconcile] op=divergence accountOnly=${accountOnly} usersOnly=${usersOnly}`);
|
|
22834
|
+
} catch (err) {
|
|
22835
|
+
console.error(`[users-reconcile] op=error detail=${err instanceof Error ? err.message : String(err)}`);
|
|
22836
|
+
}
|
|
22837
|
+
}
|
|
22838
|
+
runUsersReconcileTick();
|
|
22839
|
+
var usersReconcileTimer = setInterval(runUsersReconcileTick, USERS_RECONCILE_INTERVAL_MS);
|
|
22840
|
+
if (typeof usersReconcileTimer.unref === "function") usersReconcileTimer.unref();
|
|
22701
22841
|
startGraphHealthTimer();
|
|
22702
22842
|
void migrateUploads().catch((err) => console.error(`[uploads-migrate] failed err="${err instanceof Error ? err.message : String(err)}"`)).finally(() => {
|
|
22703
22843
|
void startFileWatcher().catch((err) => {
|
|
@@ -22786,7 +22926,7 @@ if (bootAccountConfig?.whatsapp) {
|
|
|
22786
22926
|
}
|
|
22787
22927
|
init({
|
|
22788
22928
|
configDir: configDirForWhatsApp,
|
|
22789
|
-
platformRoot:
|
|
22929
|
+
platformRoot: resolve32(process.env.MAXY_PLATFORM_ROOT ?? join32(__dirname, "..")),
|
|
22790
22930
|
accountConfig: bootAccountConfig,
|
|
22791
22931
|
onMessage: async (msg) => {
|
|
22792
22932
|
if (msg.isOwnerMirror) {
|
|
@@ -22827,7 +22967,6 @@ init({
|
|
|
22827
22967
|
}).catch((err) => {
|
|
22828
22968
|
console.error(`[whatsapp] init failed: ${String(err)}`);
|
|
22829
22969
|
});
|
|
22830
|
-
startReaper();
|
|
22831
22970
|
var shuttingDown = false;
|
|
22832
22971
|
process.on("SIGTERM", async () => {
|
|
22833
22972
|
if (shuttingDown) {
|