@rubytech/create-maxy-code 0.1.353 → 0.1.354
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/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/troubleshooting.md +15 -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-CzdOQKyO.js → AdminLoginScreens-6WgO-Z3m.js} +1 -1
- package/payload/server/public/assets/{AdminShell-CC-CuN6Y.js → AdminShell-GGnPXCOe.js} +1 -1
- package/payload/server/public/assets/{Checkbox-sQBc9xSy.js → Checkbox-D3XFVUMT.js} +1 -1
- package/payload/server/public/assets/{OperatorConversations-DPVKGsRS.css → OperatorConversations-CFWXdnjB.css} +1 -1
- package/payload/server/public/assets/{admin-qW6jM6l0.js → admin-B2yNhVvH.js} +1 -1
- package/payload/server/public/assets/{browser-BsW6KaxZ.js → browser-ByaQ0ItQ.js} +1 -1
- package/payload/server/public/assets/{calendar-BPAiN1cL.js → calendar-BjqCT8cW.js} +1 -1
- package/payload/server/public/assets/chat-kl-Ips-o.js +1 -0
- package/payload/server/public/assets/{data-D_cR_NCZ.js → data-p9p-181v.js} +1 -1
- package/payload/server/public/assets/{graph-CmpAy2WP.js → graph-BuunC8eP.js} +1 -1
- package/payload/server/public/assets/{graph-labels-C0SLq_VP.js → graph-labels-BF3Ak8q0.js} +1 -1
- package/payload/server/public/assets/{operator-CrjtJcEY.js → operator-D7c8mN4J.js} +1 -1
- package/payload/server/public/assets/{page-Cf3fVDSo.js → page-C6uMZdT1.js} +3 -3
- package/payload/server/public/assets/{public-BkNtvSYp.js → public-B9_UhjEa.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 +1010 -966
- package/payload/server/public/assets/chat-DVD9yoVL.js +0 -1
- /package/payload/server/public/assets/{OperatorConversations-CA_2c3sp.js → OperatorConversations-DeZ2wDEK.js} +0 -0
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"}`);
|
|
@@ -9858,12 +9891,16 @@ app9.delete("/set-pin", async (c) => {
|
|
|
9858
9891
|
writeFileSync10(USERS_FILE, JSON.stringify(remaining), { mode: 384 });
|
|
9859
9892
|
console.log(`[set-pin] removed entry from users.json: userId=${matchedUser.userId.slice(0, 8)}\u2026 remaining=${remaining.length}`);
|
|
9860
9893
|
}
|
|
9894
|
+
logUsersAudit(
|
|
9895
|
+
{ actor: matchedUser.userId, logFile: USERS_AUDIT_LOG },
|
|
9896
|
+
{ action: "remove", field: "row", rowsBefore: formatAuditRowIds(users.map((u) => u.userId)), rowsAfter: formatAuditRowIds(remaining.map((u) => u.userId)) }
|
|
9897
|
+
);
|
|
9861
9898
|
return c.json({ ok: true });
|
|
9862
9899
|
});
|
|
9863
9900
|
var onboarding_default = app9;
|
|
9864
9901
|
|
|
9865
9902
|
// server/routes/client-error.ts
|
|
9866
|
-
import { appendFileSync, existsSync as existsSync15, renameSync as renameSync6, statSync as statSync8 } from "fs";
|
|
9903
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync15, renameSync as renameSync6, statSync as statSync8 } from "fs";
|
|
9867
9904
|
import { join as join21 } from "path";
|
|
9868
9905
|
var CLIENT_ERRORS_LOG = join21(LOG_DIR, "client-errors.log");
|
|
9869
9906
|
var MAX_LOG_SIZE = 10 * 1024 * 1024;
|
|
@@ -10012,7 +10049,7 @@ app10.post("/", async (c) => {
|
|
|
10012
10049
|
tag: typeof body.tag === "string" ? truncate(body.tag, 32) : void 0,
|
|
10013
10050
|
status: typeof body.status === "number" ? body.status : void 0
|
|
10014
10051
|
};
|
|
10015
|
-
|
|
10052
|
+
appendFileSync2(CLIENT_ERRORS_LOG, JSON.stringify(payload) + "\n", "utf-8");
|
|
10016
10053
|
} catch (err) {
|
|
10017
10054
|
console.error(`[client-error] append failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
10018
10055
|
}
|
|
@@ -10511,7 +10548,7 @@ import { existsSync as existsSync20, mkdirSync as mkdirSync5, createWriteStream
|
|
|
10511
10548
|
async function purgeAdminConversationJsonls(args) {
|
|
10512
10549
|
const fetchImpl = args.fetchImpl ?? fetch;
|
|
10513
10550
|
const log = args.logger ?? ((line) => console.log(line));
|
|
10514
|
-
const
|
|
10551
|
+
const id82 = args.sessionId.slice(0, 8);
|
|
10515
10552
|
const purged = [];
|
|
10516
10553
|
for (const sessionId of args.claudeSessionIds) {
|
|
10517
10554
|
const url = `${args.managerBase}/${encodeURIComponent(sessionId)}`;
|
|
@@ -10521,7 +10558,7 @@ async function purgeAdminConversationJsonls(args) {
|
|
|
10521
10558
|
} catch (err) {
|
|
10522
10559
|
const message2 = err instanceof Error ? err.message : String(err);
|
|
10523
10560
|
log(
|
|
10524
|
-
`[admin-conversation-delete] sessionId=${
|
|
10561
|
+
`[admin-conversation-delete] sessionId=${id82} reason=jsonl-purge-failed claudeSessionId=${sessionId.slice(0, 8)} message=${message2}`
|
|
10525
10562
|
);
|
|
10526
10563
|
return {
|
|
10527
10564
|
ok: false,
|
|
@@ -10538,7 +10575,7 @@ async function purgeAdminConversationJsonls(args) {
|
|
|
10538
10575
|
}
|
|
10539
10576
|
if (res.status === 409) {
|
|
10540
10577
|
log(
|
|
10541
|
-
`[admin-conversation-delete] sessionId=${
|
|
10578
|
+
`[admin-conversation-delete] sessionId=${id82} reason=pty-still-alive claudeSessionId=${sessionId.slice(0, 8)}`
|
|
10542
10579
|
);
|
|
10543
10580
|
return {
|
|
10544
10581
|
ok: false,
|
|
@@ -10555,7 +10592,7 @@ async function purgeAdminConversationJsonls(args) {
|
|
|
10555
10592
|
} catch {
|
|
10556
10593
|
}
|
|
10557
10594
|
log(
|
|
10558
|
-
`[admin-conversation-delete] sessionId=${
|
|
10595
|
+
`[admin-conversation-delete] sessionId=${id82} reason=jsonl-purge-failed claudeSessionId=${sessionId.slice(0, 8)} message=${message}`
|
|
10559
10596
|
);
|
|
10560
10597
|
return {
|
|
10561
10598
|
ok: false,
|
|
@@ -10574,13 +10611,13 @@ async function cascadeAdminConversationDelete(args) {
|
|
|
10574
10611
|
const started = Date.now();
|
|
10575
10612
|
const fetchImpl = args.fetchImpl ?? fetch;
|
|
10576
10613
|
const log = args.logger ?? ((line) => console.log(line));
|
|
10577
|
-
const
|
|
10614
|
+
const id82 = args.sessionId.slice(0, 8);
|
|
10578
10615
|
let claudeSessionIds;
|
|
10579
10616
|
try {
|
|
10580
10617
|
claudeSessionIds = args.claudeSessionIds ?? await getConversationClaudeSessionIds(args.sessionId, args.accountId);
|
|
10581
10618
|
} catch (err) {
|
|
10582
10619
|
const message = err instanceof Error ? err.message : String(err);
|
|
10583
|
-
log(`[admin-conversation-delete] sessionId=${
|
|
10620
|
+
log(`[admin-conversation-delete] sessionId=${id82} reason=graph-delete-failed claudeSessionIds-purged= message=transcript-resolve:${message}`);
|
|
10584
10621
|
return { ok: false, reason: "graph-delete-failed", claudeSessionIdsPurged: [], message };
|
|
10585
10622
|
}
|
|
10586
10623
|
const purgeOutcome = await purgeAdminConversationJsonls({
|
|
@@ -10611,7 +10648,7 @@ async function cascadeAdminConversationDelete(args) {
|
|
|
10611
10648
|
} catch (err) {
|
|
10612
10649
|
const message = err instanceof Error ? err.message : String(err);
|
|
10613
10650
|
const csv2 = purged.map((s) => s.slice(0, 8)).join(",");
|
|
10614
|
-
log(`[admin-conversation-delete] sessionId=${
|
|
10651
|
+
log(`[admin-conversation-delete] sessionId=${id82} reason=graph-delete-failed claudeSessionIds-purged=${csv2} message=${message}`);
|
|
10615
10652
|
return {
|
|
10616
10653
|
ok: false,
|
|
10617
10654
|
reason: "graph-delete-failed",
|
|
@@ -10624,7 +10661,7 @@ async function cascadeAdminConversationDelete(args) {
|
|
|
10624
10661
|
}
|
|
10625
10662
|
const ms = Date.now() - started;
|
|
10626
10663
|
const csv = purged.map((s) => s.slice(0, 8)).join(",");
|
|
10627
|
-
log(`[admin-conversation-delete] sessionId=${
|
|
10664
|
+
log(`[admin-conversation-delete] sessionId=${id82} claudeSessionIds=${csv} ms=${ms}`);
|
|
10628
10665
|
return { ok: true, claudeSessionIds: purged, ms };
|
|
10629
10666
|
}
|
|
10630
10667
|
|
|
@@ -16607,777 +16644,158 @@ app38.get("/", async (c) => {
|
|
|
16607
16644
|
});
|
|
16608
16645
|
var public_session_context_default = app38;
|
|
16609
16646
|
|
|
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;
|
|
16647
|
+
// app/lib/webchat/gateway/instance.ts
|
|
16648
|
+
var instance2 = null;
|
|
16649
|
+
function setWebchatGateway(gw) {
|
|
16650
|
+
instance2 = gw;
|
|
16626
16651
|
}
|
|
16627
|
-
function
|
|
16628
|
-
|
|
16629
|
-
return `http://127.0.0.1:${port2}`;
|
|
16652
|
+
function getWebchatGateway() {
|
|
16653
|
+
return instance2;
|
|
16630
16654
|
}
|
|
16631
|
-
|
|
16632
|
-
|
|
16633
|
-
|
|
16655
|
+
|
|
16656
|
+
// server/routes/admin/public-session-exit.ts
|
|
16657
|
+
var TAG29 = "[public-session-exit-route]";
|
|
16658
|
+
function isLoopbackAddr4(addr) {
|
|
16659
|
+
return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
|
|
16634
16660
|
}
|
|
16635
|
-
|
|
16661
|
+
var app39 = new Hono();
|
|
16662
|
+
app39.post("/", async (c) => {
|
|
16663
|
+
const remoteAddr = c.env?.incoming?.socket?.remoteAddress ?? "";
|
|
16664
|
+
if (!isLoopbackAddr4(remoteAddr)) {
|
|
16665
|
+
console.error(`${TAG29} reject reason=non-loopback remoteAddr=${remoteAddr}`);
|
|
16666
|
+
return c.json({ error: "public-session-exit-loopback-only" }, 403);
|
|
16667
|
+
}
|
|
16668
|
+
const xffHeader = c.env?.incoming?.headers?.["x-forwarded-for"];
|
|
16669
|
+
const xffRaw = Array.isArray(xffHeader) ? xffHeader.join(",") : typeof xffHeader === "string" ? xffHeader : "";
|
|
16670
|
+
if (xffRaw.length > 0) {
|
|
16671
|
+
const tokens = xffRaw.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
|
|
16672
|
+
const offender = tokens.find((t) => !isLoopbackAddr4(t));
|
|
16673
|
+
if (offender !== void 0) {
|
|
16674
|
+
console.error(`${TAG29} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
|
|
16675
|
+
return c.json({ error: "public-session-exit-loopback-only" }, 403);
|
|
16676
|
+
}
|
|
16677
|
+
}
|
|
16678
|
+
let body;
|
|
16636
16679
|
try {
|
|
16637
|
-
|
|
16638
|
-
|
|
16639
|
-
return
|
|
16640
|
-
}
|
|
16641
|
-
|
|
16642
|
-
|
|
16643
|
-
|
|
16644
|
-
|
|
16680
|
+
body = await c.req.json();
|
|
16681
|
+
} catch {
|
|
16682
|
+
return c.json({ error: "invalid-json" }, 400);
|
|
16683
|
+
}
|
|
16684
|
+
const sessionId = typeof body.sessionId === "string" ? body.sessionId.trim() : "";
|
|
16685
|
+
if (!sessionId) return c.json({ error: "sessionId required" }, 400);
|
|
16686
|
+
const gateway = getWebchatGateway();
|
|
16687
|
+
if (!gateway) {
|
|
16688
|
+
console.error(`${TAG29} reject reason=gateway-unset sessionId=${sessionId.slice(0, 8)}`);
|
|
16689
|
+
return c.json({ error: "webchat-gateway-unset" }, 503);
|
|
16645
16690
|
}
|
|
16691
|
+
gateway.handlePublicSessionExit(sessionId);
|
|
16692
|
+
return c.json({ ok: true });
|
|
16693
|
+
});
|
|
16694
|
+
var public_session_exit_default = app39;
|
|
16695
|
+
|
|
16696
|
+
// server/routes/admin/access-session-evict.ts
|
|
16697
|
+
var TAG30 = "[access-session-evict]";
|
|
16698
|
+
function isLoopbackAddr5(addr) {
|
|
16699
|
+
return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
|
|
16646
16700
|
}
|
|
16647
|
-
|
|
16648
|
-
|
|
16649
|
-
|
|
16650
|
-
|
|
16651
|
-
|
|
16652
|
-
|
|
16653
|
-
|
|
16654
|
-
|
|
16655
|
-
|
|
16701
|
+
var app40 = new Hono();
|
|
16702
|
+
app40.post("/", async (c) => {
|
|
16703
|
+
const remoteAddr = c.env?.incoming?.socket?.remoteAddress ?? "";
|
|
16704
|
+
if (!isLoopbackAddr5(remoteAddr)) {
|
|
16705
|
+
console.error(`${TAG30} reject reason=non-loopback remoteAddr=${remoteAddr}`);
|
|
16706
|
+
return c.json({ error: "access-session-evict-loopback-only" }, 403);
|
|
16707
|
+
}
|
|
16708
|
+
const xffHeader = c.env?.incoming?.headers?.["x-forwarded-for"];
|
|
16709
|
+
const xffRaw = Array.isArray(xffHeader) ? xffHeader.join(",") : typeof xffHeader === "string" ? xffHeader : "";
|
|
16710
|
+
if (xffRaw.length > 0) {
|
|
16711
|
+
const tokens = xffRaw.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
|
|
16712
|
+
const offender = tokens.find((t) => !isLoopbackAddr5(t));
|
|
16713
|
+
if (offender !== void 0) {
|
|
16714
|
+
console.error(`${TAG30} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
|
|
16715
|
+
return c.json({ error: "access-session-evict-loopback-only" }, 403);
|
|
16656
16716
|
}
|
|
16657
16717
|
}
|
|
16658
|
-
|
|
16659
|
-
}
|
|
16660
|
-
async function fetchPriorWrites(sliceToken, accountId) {
|
|
16661
|
-
const url = `${uiBase()}/api/admin/public-session-context?sliceToken=${encodeURIComponent(sliceToken)}&accountId=${encodeURIComponent(accountId)}`;
|
|
16718
|
+
let body;
|
|
16662
16719
|
try {
|
|
16663
|
-
|
|
16664
|
-
|
|
16665
|
-
|
|
16666
|
-
`${TAG29} prior-writes-fetch-failed sliceToken=${sliceToken.slice(0, 8)} status=${res.status}`
|
|
16667
|
-
);
|
|
16668
|
-
return [];
|
|
16669
|
-
}
|
|
16670
|
-
const body = await res.json();
|
|
16671
|
-
return Array.isArray(body.writes) ? body.writes : [];
|
|
16672
|
-
} catch (err) {
|
|
16673
|
-
console.error(
|
|
16674
|
-
`${TAG29} prior-writes-fetch-threw sliceToken=${sliceToken.slice(0, 8)} err="${err instanceof Error ? err.message : String(err)}"`
|
|
16675
|
-
);
|
|
16676
|
-
return [];
|
|
16720
|
+
body = await c.req.json();
|
|
16721
|
+
} catch {
|
|
16722
|
+
return c.json({ error: "invalid-json" }, 400);
|
|
16677
16723
|
}
|
|
16724
|
+
const grantId = typeof body.grantId === "string" ? body.grantId.trim() : "";
|
|
16725
|
+
if (!grantId) return c.json({ error: "grantId required" }, 400);
|
|
16726
|
+
const dropped = evictAccessSessionsByGrant(grantId);
|
|
16727
|
+
console.log(`${TAG30} grantId=${grantId} dropped=${dropped}`);
|
|
16728
|
+
return c.json({ ok: true, dropped });
|
|
16729
|
+
});
|
|
16730
|
+
var access_session_evict_default = app40;
|
|
16731
|
+
|
|
16732
|
+
// server/routes/admin/enrol-person.ts
|
|
16733
|
+
var TAG31 = "[enrol]";
|
|
16734
|
+
function isLoopbackAddr6(addr) {
|
|
16735
|
+
return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
|
|
16678
16736
|
}
|
|
16679
|
-
|
|
16680
|
-
|
|
16681
|
-
|
|
16682
|
-
|
|
16683
|
-
|
|
16684
|
-
|
|
16685
|
-
|
|
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}` };
|
|
16737
|
+
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
16738
|
+
var app41 = new Hono();
|
|
16739
|
+
app41.post("/", async (c) => {
|
|
16740
|
+
const remoteAddr = c.env?.incoming?.socket?.remoteAddress ?? "";
|
|
16741
|
+
if (!isLoopbackAddr6(remoteAddr)) {
|
|
16742
|
+
console.error(`${TAG31} reject reason=non-loopback remoteAddr=${remoteAddr}`);
|
|
16743
|
+
return c.json({ error: "enrol-person-loopback-only" }, 403);
|
|
16714
16744
|
}
|
|
16715
|
-
|
|
16716
|
-
|
|
16717
|
-
|
|
16718
|
-
|
|
16719
|
-
|
|
16720
|
-
|
|
16721
|
-
|
|
16722
|
-
|
|
16723
|
-
|
|
16724
|
-
);
|
|
16725
|
-
return;
|
|
16745
|
+
const xffHeader = c.env?.incoming?.headers?.["x-forwarded-for"];
|
|
16746
|
+
const xffRaw = Array.isArray(xffHeader) ? xffHeader.join(",") : typeof xffHeader === "string" ? xffHeader : "";
|
|
16747
|
+
if (xffRaw.length > 0) {
|
|
16748
|
+
const tokens = xffRaw.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
|
|
16749
|
+
const offender = tokens.find((t) => !isLoopbackAddr6(t));
|
|
16750
|
+
if (offender !== void 0) {
|
|
16751
|
+
console.error(`${TAG31} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
|
|
16752
|
+
return c.json({ error: "enrol-person-loopback-only" }, 403);
|
|
16753
|
+
}
|
|
16726
16754
|
}
|
|
16727
|
-
|
|
16728
|
-
|
|
16729
|
-
|
|
16730
|
-
|
|
16731
|
-
);
|
|
16732
|
-
return;
|
|
16755
|
+
let body;
|
|
16756
|
+
try {
|
|
16757
|
+
body = await c.req.json();
|
|
16758
|
+
} catch {
|
|
16759
|
+
return c.json({ error: "invalid-json" }, 400);
|
|
16733
16760
|
}
|
|
16734
|
-
const
|
|
16735
|
-
|
|
16736
|
-
|
|
16737
|
-
|
|
16738
|
-
);
|
|
16739
|
-
return;
|
|
16761
|
+
const rawPhone = typeof body.phone === "string" ? body.phone : "";
|
|
16762
|
+
const phone = normalizeE164(rawPhone);
|
|
16763
|
+
if (phone.length <= 1) {
|
|
16764
|
+
return c.json({ error: "invalid-phone" }, 400);
|
|
16740
16765
|
}
|
|
16741
|
-
const
|
|
16742
|
-
|
|
16743
|
-
|
|
16744
|
-
|
|
16745
|
-
|
|
16746
|
-
|
|
16747
|
-
|
|
16748
|
-
}
|
|
16749
|
-
|
|
16750
|
-
|
|
16751
|
-
|
|
16766
|
+
const email = typeof body.email === "string" ? body.email.trim().toLowerCase() : "";
|
|
16767
|
+
if (!EMAIL_RE.test(email)) {
|
|
16768
|
+
return c.json({ error: "invalid-email" }, 400);
|
|
16769
|
+
}
|
|
16770
|
+
const agentSlug = typeof body.agentSlug === "string" ? body.agentSlug.trim() : "";
|
|
16771
|
+
if (!agentSlug) {
|
|
16772
|
+
return c.json({ error: "agentSlug required" }, 400);
|
|
16773
|
+
}
|
|
16774
|
+
const accountId = process.env.ACCOUNT_ID ?? "";
|
|
16775
|
+
if (!accountId) {
|
|
16776
|
+
console.error(`${TAG31} op=person result=no-account-id`);
|
|
16777
|
+
return c.json({ error: "no-account-id" }, 500);
|
|
16778
|
+
}
|
|
16779
|
+
let personId;
|
|
16780
|
+
try {
|
|
16781
|
+
personId = await enrolPerson({ accountId, phone, email, agentSlug });
|
|
16782
|
+
} catch (err) {
|
|
16783
|
+
console.error(
|
|
16784
|
+
`${TAG31} op=person result=write-failed agentSlug=${agentSlug} contact=${maskContact(email)} err="${err instanceof Error ? err.message : String(err)}"`
|
|
16752
16785
|
);
|
|
16753
|
-
return;
|
|
16786
|
+
return c.json({ error: "enrol-failed" }, 500);
|
|
16754
16787
|
}
|
|
16755
|
-
|
|
16756
|
-
|
|
16788
|
+
let grant = null;
|
|
16789
|
+
try {
|
|
16790
|
+
const g = await findGrantByContact(email, agentSlug, accountId);
|
|
16791
|
+
if (g) grant = { grantId: g.grantId, status: g.status, contactValue: g.contactValue };
|
|
16792
|
+
} catch (err) {
|
|
16757
16793
|
console.error(
|
|
16758
|
-
`${
|
|
16794
|
+
`${TAG31} op=person result=grant-lookup-failed contact=${maskContact(email)} err="${err instanceof Error ? err.message : String(err)}"`
|
|
16759
16795
|
);
|
|
16760
|
-
return;
|
|
16761
16796
|
}
|
|
16762
16797
|
console.log(
|
|
16763
|
-
`${
|
|
16764
|
-
);
|
|
16765
|
-
}
|
|
16766
|
-
|
|
16767
|
-
// app/lib/channel-pty-bridge/follower.ts
|
|
16768
|
-
function followerPendingMaxMs() {
|
|
16769
|
-
return Number(process.env.CHANNEL_PTY_FOLLOWER_PENDING_MAX_MS ?? String(3e5));
|
|
16770
|
-
}
|
|
16771
|
-
function followerRetryMs() {
|
|
16772
|
-
return Number(process.env.CHANNEL_PTY_FOLLOWER_RETRY_MS ?? String(1e3));
|
|
16773
|
-
}
|
|
16774
|
-
function startFollower(opts) {
|
|
16775
|
-
const abort = new AbortController();
|
|
16776
|
-
const { entry, tag } = opts;
|
|
16777
|
-
void (async () => {
|
|
16778
|
-
try {
|
|
16779
|
-
const sid = entry.sessionId.slice(0, 8);
|
|
16780
|
-
const deadline = Date.now() + followerPendingMaxMs();
|
|
16781
|
-
const retryMs = followerRetryMs();
|
|
16782
|
-
let res;
|
|
16783
|
-
let attempt = 0;
|
|
16784
|
-
for (; ; ) {
|
|
16785
|
-
res = await fetch(
|
|
16786
|
-
managerLogFollowUrl(entry.sessionId, { boundary: opts.suppressResumeReplay === true }),
|
|
16787
|
-
{ signal: abort.signal }
|
|
16788
|
-
);
|
|
16789
|
-
console.error(`${tag} follower-connect sessionId=${sid} status=${res.status}`);
|
|
16790
|
-
if (res.status === 202) {
|
|
16791
|
-
attempt += 1;
|
|
16792
|
-
await res.body?.cancel().catch(() => {
|
|
16793
|
-
});
|
|
16794
|
-
if (abort.signal.aborted) return;
|
|
16795
|
-
if (Date.now() >= deadline) {
|
|
16796
|
-
console.error(`${tag} follower-give-up sessionId=${sid} reason=pending-timeout attempts=${attempt}`);
|
|
16797
|
-
opts.onError?.("follow-pending-timeout");
|
|
16798
|
-
return;
|
|
16799
|
-
}
|
|
16800
|
-
console.error(`${tag} follower-retry sessionId=${sid} attempt=${attempt} reason=pending`);
|
|
16801
|
-
await new Promise((r) => setTimeout(r, retryMs));
|
|
16802
|
-
if (abort.signal.aborted) return;
|
|
16803
|
-
continue;
|
|
16804
|
-
}
|
|
16805
|
-
break;
|
|
16806
|
-
}
|
|
16807
|
-
if (!res.ok || !res.body) {
|
|
16808
|
-
opts.onError?.(`follow-status-${res.status}`);
|
|
16809
|
-
return;
|
|
16810
|
-
}
|
|
16811
|
-
console.error(`${tag} follower-open sessionId=${sid}`);
|
|
16812
|
-
const reader = res.body.getReader();
|
|
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)}`
|
|
16798
|
+
`${TAG31} op=person personId=${personId} account=${accountId} agentSlug=${agentSlug} contact=${maskContact(email)}`
|
|
17381
16799
|
);
|
|
17382
16800
|
return c.json({ personId, grant }, 200);
|
|
17383
16801
|
});
|
|
@@ -17945,7 +17363,7 @@ app44.route("/calendar", calendar_default);
|
|
|
17945
17363
|
var admin_default = app44;
|
|
17946
17364
|
|
|
17947
17365
|
// server/routes/access/verify-token.ts
|
|
17948
|
-
var
|
|
17366
|
+
var TAG32 = "[access-verify]";
|
|
17949
17367
|
var MINT_TAG = "[access-session-mint]";
|
|
17950
17368
|
var COOKIE_NAME = "__access_session";
|
|
17951
17369
|
var app45 = new Hono();
|
|
@@ -17964,39 +17382,39 @@ app45.post("/", async (c) => {
|
|
|
17964
17382
|
}
|
|
17965
17383
|
const rateMsg = checkAccessRateLimit(ip, agentSlug);
|
|
17966
17384
|
if (rateMsg) {
|
|
17967
|
-
console.error(`${
|
|
17385
|
+
console.error(`${TAG32} grantId=- agentSlug=${agentSlug} result=rate-limited ip=${ip}`);
|
|
17968
17386
|
return c.json({ error: rateMsg }, 429);
|
|
17969
17387
|
}
|
|
17970
17388
|
const grant = await findGrantByMagicToken(token);
|
|
17971
17389
|
if (!grant) {
|
|
17972
17390
|
recordAccessFailedAttempt(ip, agentSlug);
|
|
17973
|
-
console.error(`${
|
|
17391
|
+
console.error(`${TAG32} grantId=- agentSlug=${agentSlug} result=notfound ip=${ip}`);
|
|
17974
17392
|
return c.json({ error: "invalid-or-expired-link" }, 401);
|
|
17975
17393
|
}
|
|
17976
17394
|
if (grant.agentSlug !== agentSlug) {
|
|
17977
17395
|
recordAccessFailedAttempt(ip, agentSlug);
|
|
17978
17396
|
console.error(
|
|
17979
|
-
`${
|
|
17397
|
+
`${TAG32} grantId=${grant.grantId} agentSlug=${agentSlug} result=agent-mismatch grantAgent=${grant.agentSlug} ip=${ip}`
|
|
17980
17398
|
);
|
|
17981
17399
|
return c.json({ error: "invalid-or-expired-link" }, 401);
|
|
17982
17400
|
}
|
|
17983
17401
|
if (grant.status === "expired" || grant.status === "revoked") {
|
|
17984
17402
|
recordAccessFailedAttempt(ip, agentSlug);
|
|
17985
17403
|
console.error(
|
|
17986
|
-
`${
|
|
17404
|
+
`${TAG32} grantId=${grant.grantId} agentSlug=${agentSlug} result=expired status=${grant.status} ip=${ip}`
|
|
17987
17405
|
);
|
|
17988
17406
|
return c.json({ error: "access-no-longer-valid" }, 401);
|
|
17989
17407
|
}
|
|
17990
17408
|
if (grant.expiresAt !== null && grant.expiresAt < Date.now()) {
|
|
17991
17409
|
recordAccessFailedAttempt(ip, agentSlug);
|
|
17992
17410
|
console.error(
|
|
17993
|
-
`${
|
|
17411
|
+
`${TAG32} grantId=${grant.grantId} agentSlug=${agentSlug} result=expired reason=expiresAt-past ip=${ip}`
|
|
17994
17412
|
);
|
|
17995
17413
|
return c.json({ error: "access-no-longer-valid" }, 401);
|
|
17996
17414
|
}
|
|
17997
17415
|
if (!grant.sliceToken) {
|
|
17998
17416
|
console.error(
|
|
17999
|
-
`${
|
|
17417
|
+
`${TAG32} grantId=${grant.grantId} agentSlug=${agentSlug} result=no-slice-token reason=schema-violation`
|
|
18000
17418
|
);
|
|
18001
17419
|
return c.json({ error: "grant-misconfigured" }, 500);
|
|
18002
17420
|
}
|
|
@@ -18012,12 +17430,12 @@ app45.post("/", async (c) => {
|
|
|
18012
17430
|
await consumeMagicTokenAndActivate(grant.grantId);
|
|
18013
17431
|
} catch (err) {
|
|
18014
17432
|
console.error(
|
|
18015
|
-
`${
|
|
17433
|
+
`${TAG32} grantId=${grant.grantId} agentSlug=${agentSlug} result=consume-failed err="${err instanceof Error ? err.message : String(err)}"`
|
|
18016
17434
|
);
|
|
18017
17435
|
return c.json({ error: "verification-failed" }, 500);
|
|
18018
17436
|
}
|
|
18019
17437
|
clearAccessRateLimit(ip, agentSlug);
|
|
18020
|
-
console.log(`${
|
|
17438
|
+
console.log(`${TAG32} grantId=${grant.grantId} agentSlug=${agentSlug} result=ok ip=${ip}`);
|
|
18021
17439
|
console.log(
|
|
18022
17440
|
`${MINT_TAG} grantId=${grant.grantId} sliceToken=${grant.sliceToken.slice(0, 8)} agentSlug=${agentSlug} personId=${grant.personId ?? "none"}`
|
|
18023
17441
|
);
|
|
@@ -18035,9 +17453,9 @@ var verify_token_default = app45;
|
|
|
18035
17453
|
|
|
18036
17454
|
// app/lib/access-email.ts
|
|
18037
17455
|
import { spawn as spawn2 } from "child_process";
|
|
18038
|
-
import { resolve as
|
|
18039
|
-
var PLATFORM_ROOT7 = process.env.MAXY_PLATFORM_ROOT ??
|
|
18040
|
-
var SEND_SCRIPT =
|
|
17456
|
+
import { resolve as resolve24 } from "path";
|
|
17457
|
+
var PLATFORM_ROOT7 = process.env.MAXY_PLATFORM_ROOT ?? resolve24(process.cwd(), "..");
|
|
17458
|
+
var SEND_SCRIPT = resolve24(
|
|
18041
17459
|
PLATFORM_ROOT7,
|
|
18042
17460
|
"plugins",
|
|
18043
17461
|
"email",
|
|
@@ -18093,7 +17511,7 @@ async function sendMagicLinkEmail(payload) {
|
|
|
18093
17511
|
}
|
|
18094
17512
|
|
|
18095
17513
|
// server/routes/access/request-magic-link.ts
|
|
18096
|
-
var
|
|
17514
|
+
var TAG33 = "[access-request-link]";
|
|
18097
17515
|
var app46 = new Hono();
|
|
18098
17516
|
var VISITOR_MESSAGE = "If that email is on the invite list, a fresh link is on the way.";
|
|
18099
17517
|
app46.post("/", async (c) => {
|
|
@@ -18110,18 +17528,18 @@ app46.post("/", async (c) => {
|
|
|
18110
17528
|
}
|
|
18111
17529
|
const rateMsg = checkRequestLinkRateLimit(contactValue);
|
|
18112
17530
|
if (rateMsg) {
|
|
18113
|
-
console.error(`${
|
|
17531
|
+
console.error(`${TAG33} contactValue=${maskContact(contactValue)} result=rate-limited`);
|
|
18114
17532
|
return c.json({ error: rateMsg }, 429);
|
|
18115
17533
|
}
|
|
18116
17534
|
recordRequestLinkAttempt(contactValue);
|
|
18117
17535
|
const accountId = process.env.ACCOUNT_ID ?? "";
|
|
18118
17536
|
if (!accountId) {
|
|
18119
|
-
console.error(`${
|
|
17537
|
+
console.error(`${TAG33} contactValue=${maskContact(contactValue)} result=no-account-id`);
|
|
18120
17538
|
return c.json({ message: VISITOR_MESSAGE }, 200);
|
|
18121
17539
|
}
|
|
18122
17540
|
const grant = await findActiveGrantByContact(contactValue, agentSlug, accountId);
|
|
18123
17541
|
if (!grant) {
|
|
18124
|
-
console.log(`${
|
|
17542
|
+
console.log(`${TAG33} contactValue=${maskContact(contactValue)} result=notfound`);
|
|
18125
17543
|
return c.json({ message: VISITOR_MESSAGE }, 200);
|
|
18126
17544
|
}
|
|
18127
17545
|
let token;
|
|
@@ -18129,7 +17547,7 @@ app46.post("/", async (c) => {
|
|
|
18129
17547
|
token = await generateNewMagicToken(grant.grantId);
|
|
18130
17548
|
} catch (err) {
|
|
18131
17549
|
console.error(
|
|
18132
|
-
`${
|
|
17550
|
+
`${TAG33} contactValue=${maskContact(contactValue)} result=mint-failed err="${err instanceof Error ? err.message : String(err)}"`
|
|
18133
17551
|
);
|
|
18134
17552
|
return c.json({ message: VISITOR_MESSAGE }, 200);
|
|
18135
17553
|
}
|
|
@@ -18161,12 +17579,12 @@ app46.post("/", async (c) => {
|
|
|
18161
17579
|
});
|
|
18162
17580
|
if (!sendResult.ok) {
|
|
18163
17581
|
console.error(
|
|
18164
|
-
`${
|
|
17582
|
+
`${TAG33} contactValue=${maskContact(contactValue)} result=send-failed err="${sendResult.error}"`
|
|
18165
17583
|
);
|
|
18166
17584
|
return c.json({ message: VISITOR_MESSAGE }, 200);
|
|
18167
17585
|
}
|
|
18168
17586
|
console.log(
|
|
18169
|
-
`${
|
|
17587
|
+
`${TAG33} contactValue=${maskContact(contactValue)} result=ok messageId=${sendResult.messageId}`
|
|
18170
17588
|
);
|
|
18171
17589
|
return c.json({ message: VISITOR_MESSAGE }, 200);
|
|
18172
17590
|
});
|
|
@@ -18179,8 +17597,8 @@ app47.route("/request-magic-link", request_magic_link_default);
|
|
|
18179
17597
|
var access_default = app47;
|
|
18180
17598
|
|
|
18181
17599
|
// server/routes/sites.ts
|
|
18182
|
-
import { existsSync as existsSync27, readFileSync as readFileSync29, realpathSync as
|
|
18183
|
-
import { resolve as
|
|
17600
|
+
import { existsSync as existsSync27, readFileSync as readFileSync29, realpathSync as realpathSync5, statSync as statSync11 } from "fs";
|
|
17601
|
+
import { resolve as resolve25 } from "path";
|
|
18184
17602
|
var SAFE_SEG_RE = /^[a-z0-9_][a-z0-9_.-]{0,99}$/i;
|
|
18185
17603
|
var MIME = {
|
|
18186
17604
|
".html": "text/html; charset=utf-8",
|
|
@@ -18237,8 +17655,8 @@ app48.get("/:rel{.*}", (c) => {
|
|
|
18237
17655
|
}
|
|
18238
17656
|
segments.push(seg);
|
|
18239
17657
|
}
|
|
18240
|
-
const rootDir =
|
|
18241
|
-
let filePath = segments.length === 0 ? rootDir :
|
|
17658
|
+
const rootDir = resolve25(account.accountDir, "sites");
|
|
17659
|
+
let filePath = segments.length === 0 ? rootDir : resolve25(rootDir, ...segments);
|
|
18242
17660
|
if (filePath !== rootDir && !filePath.startsWith(rootDir + "/")) {
|
|
18243
17661
|
console.error(`[sites] path-traversal-rejected path=${reqPath} reason=escape status=403`);
|
|
18244
17662
|
return c.text("Forbidden", 403);
|
|
@@ -18258,7 +17676,7 @@ app48.get("/:rel{.*}", (c) => {
|
|
|
18258
17676
|
return c.redirect(target, 301);
|
|
18259
17677
|
}
|
|
18260
17678
|
if (stat8?.isDirectory()) {
|
|
18261
|
-
filePath =
|
|
17679
|
+
filePath = resolve25(filePath, "index.html");
|
|
18262
17680
|
}
|
|
18263
17681
|
if (!filePath.startsWith(rootDir + "/")) {
|
|
18264
17682
|
console.error(`[sites] path-traversal-rejected path=${reqPath} reason=escape status=403`);
|
|
@@ -18271,8 +17689,8 @@ app48.get("/:rel{.*}", (c) => {
|
|
|
18271
17689
|
let realPath;
|
|
18272
17690
|
let realRoot;
|
|
18273
17691
|
try {
|
|
18274
|
-
realPath =
|
|
18275
|
-
realRoot =
|
|
17692
|
+
realPath = realpathSync5(filePath);
|
|
17693
|
+
realRoot = realpathSync5(rootDir);
|
|
18276
17694
|
} catch {
|
|
18277
17695
|
console.error(`[sites] not-found path=${reqPath} status=404`);
|
|
18278
17696
|
return c.text("Not found", 404);
|
|
@@ -18895,14 +18313,14 @@ async function writeEvent(opts) {
|
|
|
18895
18313
|
var visitor_event_default = app51;
|
|
18896
18314
|
|
|
18897
18315
|
// server/routes/session.ts
|
|
18898
|
-
import { resolve as
|
|
18316
|
+
import { resolve as resolve26 } from "path";
|
|
18899
18317
|
import { existsSync as existsSync29, writeFileSync as writeFileSync12, mkdirSync as mkdirSync7 } from "fs";
|
|
18900
18318
|
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
18319
|
function writeBrandingCache(accountId, agentSlug, branding) {
|
|
18902
18320
|
try {
|
|
18903
|
-
const cacheDir =
|
|
18321
|
+
const cacheDir = resolve26(MAXY_DIR, "branding-cache", accountId);
|
|
18904
18322
|
mkdirSync7(cacheDir, { recursive: true });
|
|
18905
|
-
writeFileSync12(
|
|
18323
|
+
writeFileSync12(resolve26(cacheDir, `${agentSlug}.json`), JSON.stringify(branding), "utf-8");
|
|
18906
18324
|
} catch (err) {
|
|
18907
18325
|
console.error(`[branding] cache write failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
18908
18326
|
}
|
|
@@ -18979,8 +18397,8 @@ app52.post("/", async (c) => {
|
|
|
18979
18397
|
}
|
|
18980
18398
|
let agentConfig = null;
|
|
18981
18399
|
if (account) {
|
|
18982
|
-
const agentDir =
|
|
18983
|
-
if (!existsSync29(agentDir) || !existsSync29(
|
|
18400
|
+
const agentDir = resolve26(account.accountDir, "agents", agentSlug);
|
|
18401
|
+
if (!existsSync29(agentDir) || !existsSync29(resolve26(agentDir, "config.json"))) {
|
|
18984
18402
|
return c.json({ error: "Agent not found" }, 404);
|
|
18985
18403
|
}
|
|
18986
18404
|
agentConfig = resolveAgentConfig(account.accountDir, agentSlug);
|
|
@@ -19342,7 +18760,7 @@ function startGraphHealthTimer() {
|
|
|
19342
18760
|
|
|
19343
18761
|
// app/lib/file-watcher.ts
|
|
19344
18762
|
import * as fsp2 from "fs/promises";
|
|
19345
|
-
import { resolve as
|
|
18763
|
+
import { resolve as resolve27, sep as sep8 } from "path";
|
|
19346
18764
|
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
18765
|
var DEFAULT_COALESCE_MS = 500;
|
|
19348
18766
|
var ROOTS = ["accounts"];
|
|
@@ -19361,7 +18779,7 @@ async function startFileWatcher(opts = {}) {
|
|
|
19361
18779
|
const dropFn = opts.drop ?? dropFileIndex;
|
|
19362
18780
|
for (const r of ROOTS) {
|
|
19363
18781
|
try {
|
|
19364
|
-
await fsp2.mkdir(
|
|
18782
|
+
await fsp2.mkdir(resolve27(dataRoot, r), { recursive: true });
|
|
19365
18783
|
} catch (err) {
|
|
19366
18784
|
console.error(
|
|
19367
18785
|
`[file-watcher] start-failed root="${r}" err="${err.message}" \u2014 index will be maintained by the 5-min reconcile backstop only`
|
|
@@ -19382,7 +18800,7 @@ async function startFileWatcher(opts = {}) {
|
|
|
19382
18800
|
timers.set(relativePath, t);
|
|
19383
18801
|
}
|
|
19384
18802
|
async function runHook(relativePath, accountId) {
|
|
19385
|
-
const absolute =
|
|
18803
|
+
const absolute = resolve27(dataRoot, relativePath);
|
|
19386
18804
|
let exists = false;
|
|
19387
18805
|
try {
|
|
19388
18806
|
const st = await fsp2.stat(absolute);
|
|
@@ -19406,7 +18824,7 @@ async function startFileWatcher(opts = {}) {
|
|
|
19406
18824
|
}
|
|
19407
18825
|
}
|
|
19408
18826
|
async function watchRoot(rootName) {
|
|
19409
|
-
const absRoot =
|
|
18827
|
+
const absRoot = resolve27(dataRoot, rootName);
|
|
19410
18828
|
try {
|
|
19411
18829
|
const iter = fsp2.watch(absRoot, { recursive: true, signal: controller.signal });
|
|
19412
18830
|
for await (const event of iter) {
|
|
@@ -19445,7 +18863,7 @@ async function startFileWatcher(opts = {}) {
|
|
|
19445
18863
|
|
|
19446
18864
|
// app/lib/migrate-uploads.ts
|
|
19447
18865
|
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
|
|
18866
|
+
import { dirname as dirname8, relative as relative6, resolve as resolve28 } from "path";
|
|
19449
18867
|
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
18868
|
async function walkFiles(dir) {
|
|
19451
18869
|
const out = [];
|
|
@@ -19456,7 +18874,7 @@ async function walkFiles(dir) {
|
|
|
19456
18874
|
return out;
|
|
19457
18875
|
}
|
|
19458
18876
|
for (const e of entries) {
|
|
19459
|
-
const abs =
|
|
18877
|
+
const abs = resolve28(dir, e.name);
|
|
19460
18878
|
if (e.isDirectory()) {
|
|
19461
18879
|
out.push(...await walkFiles(abs));
|
|
19462
18880
|
} else if (e.isFile()) {
|
|
@@ -19476,7 +18894,7 @@ async function relocateTree(srcDir, destDir, dataRoot, accountId, session) {
|
|
|
19476
18894
|
let moved = 0;
|
|
19477
18895
|
for (const oldAbs of files) {
|
|
19478
18896
|
const suffix = relative6(srcDir, oldAbs);
|
|
19479
|
-
const newAbs =
|
|
18897
|
+
const newAbs = resolve28(destDir, suffix);
|
|
19480
18898
|
await mkdir5(dirname8(newAbs), { recursive: true });
|
|
19481
18899
|
await rename2(oldAbs, newAbs);
|
|
19482
18900
|
moved++;
|
|
@@ -19503,7 +18921,7 @@ async function relocateTree(srcDir, destDir, dataRoot, accountId, session) {
|
|
|
19503
18921
|
}
|
|
19504
18922
|
async function migrateUploads(opts = {}) {
|
|
19505
18923
|
const dataRoot = opts.dataRoot ?? DATA_ROOT;
|
|
19506
|
-
const oldRoot =
|
|
18924
|
+
const oldRoot = resolve28(dataRoot, "uploads");
|
|
19507
18925
|
let topEntries;
|
|
19508
18926
|
try {
|
|
19509
18927
|
topEntries = await readdir5(oldRoot, { withFileTypes: true });
|
|
@@ -19524,8 +18942,8 @@ async function migrateUploads(opts = {}) {
|
|
|
19524
18942
|
const name = entry.name;
|
|
19525
18943
|
if (ACCOUNT_UUID_RE4.test(name)) {
|
|
19526
18944
|
moved += await relocateTree(
|
|
19527
|
-
|
|
19528
|
-
|
|
18945
|
+
resolve28(oldRoot, name),
|
|
18946
|
+
resolve28(dataRoot, "accounts", name, "uploads"),
|
|
19529
18947
|
dataRoot,
|
|
19530
18948
|
name,
|
|
19531
18949
|
session
|
|
@@ -19537,8 +18955,8 @@ async function migrateUploads(opts = {}) {
|
|
|
19537
18955
|
continue;
|
|
19538
18956
|
}
|
|
19539
18957
|
moved += await relocateTree(
|
|
19540
|
-
|
|
19541
|
-
|
|
18958
|
+
resolve28(oldRoot, "public"),
|
|
18959
|
+
resolve28(dataRoot, "accounts", installAccountId, "uploads", "public"),
|
|
19542
18960
|
dataRoot,
|
|
19543
18961
|
null,
|
|
19544
18962
|
// public uploads carry no graph nodes
|
|
@@ -19567,7 +18985,7 @@ async function migrateUploads(opts = {}) {
|
|
|
19567
18985
|
}
|
|
19568
18986
|
|
|
19569
18987
|
// app/lib/migrate-admin-webchat-sidecars.ts
|
|
19570
|
-
import { readdir as readdir6, readFile as
|
|
18988
|
+
import { readdir as readdir6, readFile as readFile6, rename as rename3, writeFile as writeFile4, unlink as unlink3 } from "fs/promises";
|
|
19571
18989
|
import { existsSync as existsSync30 } from "fs";
|
|
19572
18990
|
import { join as join30 } from "path";
|
|
19573
18991
|
var ADMIN_ROLE2 = "admin";
|
|
@@ -19580,7 +18998,7 @@ var defaultResolveName = async (accountId, userId) => {
|
|
|
19580
18998
|
async function readRaw2(path2) {
|
|
19581
18999
|
let raw;
|
|
19582
19000
|
try {
|
|
19583
|
-
raw = await
|
|
19001
|
+
raw = await readFile6(path2, "utf8");
|
|
19584
19002
|
} catch {
|
|
19585
19003
|
return null;
|
|
19586
19004
|
}
|
|
@@ -20549,16 +19967,16 @@ var WebchatGateway = class _WebchatGateway {
|
|
|
20549
19967
|
* The public /api/chat route awaits this to return the reply on the same SSE
|
|
20550
19968
|
* response, preserving the pre-756 POST→single-blob browser contract. */
|
|
20551
19969
|
awaitReply(key, timeoutMs) {
|
|
20552
|
-
return new Promise((
|
|
19970
|
+
return new Promise((resolve33) => {
|
|
20553
19971
|
const timer2 = setTimeout(() => {
|
|
20554
19972
|
this.replyAwaiters.delete(key);
|
|
20555
|
-
|
|
19973
|
+
resolve33({ timeout: true });
|
|
20556
19974
|
}, timeoutMs);
|
|
20557
19975
|
if (timer2.unref) timer2.unref();
|
|
20558
19976
|
this.replyAwaiters.set(key, (r) => {
|
|
20559
19977
|
clearTimeout(timer2);
|
|
20560
19978
|
this.replyAwaiters.delete(key);
|
|
20561
|
-
|
|
19979
|
+
resolve33(r);
|
|
20562
19980
|
});
|
|
20563
19981
|
});
|
|
20564
19982
|
}
|
|
@@ -20569,11 +19987,11 @@ var WebchatGateway = class _WebchatGateway {
|
|
|
20569
19987
|
* terminal dialog stays answerable). */
|
|
20570
19988
|
awaitPermissionVerdict(p, timeoutMs) {
|
|
20571
19989
|
const k = _WebchatGateway.promptKey(p.key, p.requestId);
|
|
20572
|
-
return new Promise((
|
|
19990
|
+
return new Promise((resolve33) => {
|
|
20573
19991
|
this.pendingPrompts.get(k)?.resolve({ timeout: true });
|
|
20574
19992
|
const timer2 = setTimeout(() => {
|
|
20575
19993
|
this.pendingPrompts.delete(k);
|
|
20576
|
-
|
|
19994
|
+
resolve33({ timeout: true });
|
|
20577
19995
|
}, timeoutMs);
|
|
20578
19996
|
if (timer2.unref) timer2.unref();
|
|
20579
19997
|
this.pendingPrompts.set(k, {
|
|
@@ -20582,7 +20000,7 @@ var WebchatGateway = class _WebchatGateway {
|
|
|
20582
20000
|
resolve: (r) => {
|
|
20583
20001
|
clearTimeout(timer2);
|
|
20584
20002
|
this.pendingPrompts.delete(k);
|
|
20585
|
-
|
|
20003
|
+
resolve33(r);
|
|
20586
20004
|
}
|
|
20587
20005
|
});
|
|
20588
20006
|
console.error(`[webchat:perm] op=open key=${keyDisplay(p.key)} id=${p.requestId} tool=${p.toolName}`);
|
|
@@ -20654,6 +20072,45 @@ var WebchatGateway = class _WebchatGateway {
|
|
|
20654
20072
|
if (handle.unref) handle.unref();
|
|
20655
20073
|
return handle;
|
|
20656
20074
|
}
|
|
20075
|
+
/** A gated public PTY exited explicitly on the manager side (operator stop,
|
|
20076
|
+
* claude crash, manager DELETE), not via the idle reaper. The manager POSTs
|
|
20077
|
+
* the sessionId to the loopback /api/admin/public-session-exit route, which
|
|
20078
|
+
* reaches this through the process-wide gateway handle. The slice ringfence
|
|
20079
|
+
* the reviewer needs lives only in this gateway's publicSessions map — the
|
|
20080
|
+
* manager has none of it — so resolve the record here and dispatch the same
|
|
20081
|
+
* reviewer the idle reaper uses, tagged dispatchFor='exit'. Dedup is internal
|
|
20082
|
+
* to firePublicSessionEndReview, so a session already reviewed by the reaper
|
|
20083
|
+
* (or a racing respawn) is a no-op.
|
|
20084
|
+
*
|
|
20085
|
+
* Only gated records (non-empty slice) are reviewed and evicted here, the
|
|
20086
|
+
* same gate the reaper applies. An open-mode public record is left for the
|
|
20087
|
+
* idle reaper. No deleteSession — the PTY is already gone and the JSONL is
|
|
20088
|
+
* the audit record the reviewer just read. */
|
|
20089
|
+
handlePublicSessionExit(sessionId) {
|
|
20090
|
+
let found = null;
|
|
20091
|
+
for (const [key2, rec2] of this.publicSessions) {
|
|
20092
|
+
if (rec2.sessionId === sessionId) {
|
|
20093
|
+
found = { key: key2, rec: rec2 };
|
|
20094
|
+
break;
|
|
20095
|
+
}
|
|
20096
|
+
}
|
|
20097
|
+
if (!found) {
|
|
20098
|
+
console.error(`[webchat:inbound] op=public-exit sessionId=${sessionId.slice(0, 8)} result=no-record`);
|
|
20099
|
+
return;
|
|
20100
|
+
}
|
|
20101
|
+
const { key, rec } = found;
|
|
20102
|
+
if (!rec.sliceToken || rec.sliceToken.length === 0) return;
|
|
20103
|
+
this.publicSessions.delete(key);
|
|
20104
|
+
console.error(`[webchat:inbound] op=public-exit key=${keyDisplay(key)} sessionId=${sessionId.slice(0, 8)} result=review`);
|
|
20105
|
+
void this.deps.firePublicSessionEndReview?.({
|
|
20106
|
+
sessionId: rec.sessionId,
|
|
20107
|
+
sliceToken: rec.sliceToken,
|
|
20108
|
+
personId: rec.personId ?? null,
|
|
20109
|
+
accountId: rec.accountId,
|
|
20110
|
+
agentSlug: rec.agentSlug,
|
|
20111
|
+
dispatchFor: "exit"
|
|
20112
|
+
});
|
|
20113
|
+
}
|
|
20657
20114
|
/** Hono sub-app exposing the loopback channel routes; mount on the UI app. */
|
|
20658
20115
|
routes() {
|
|
20659
20116
|
return createWebchatChannelRoutes({
|
|
@@ -20770,97 +20227,385 @@ var WebchatGateway = class _WebchatGateway {
|
|
|
20770
20227
|
);
|
|
20771
20228
|
}
|
|
20772
20229
|
}
|
|
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
|
-
}
|
|
20230
|
+
for (const key of this.recoverable.keys()) {
|
|
20231
|
+
if (!this.hub.hasInFlight(key)) this.recoverable.delete(key);
|
|
20232
|
+
}
|
|
20233
|
+
if (this.recoveryAttempted.size > 0) {
|
|
20234
|
+
const liveIds = this.hub.inFlightIds();
|
|
20235
|
+
for (const attemptKey of this.recoveryAttempted) {
|
|
20236
|
+
if (!liveIds.has(attemptKey)) this.recoveryAttempted.delete(attemptKey);
|
|
20237
|
+
}
|
|
20238
|
+
}
|
|
20239
|
+
} finally {
|
|
20240
|
+
this.recovering = false;
|
|
20241
|
+
}
|
|
20242
|
+
}
|
|
20243
|
+
/** Handle one inbound webchat message (browser POST → here). */
|
|
20244
|
+
async handleInbound(input) {
|
|
20245
|
+
const bytes = Buffer.byteLength(input.text, "utf8");
|
|
20246
|
+
const hadSubscriber = this.hub.hasSubscriber(input.key);
|
|
20247
|
+
const willDeliverNow = this.hub.isReady(input.key);
|
|
20248
|
+
this.hub.deliver(
|
|
20249
|
+
{ key: input.key, text: input.text, messageId: `wc-${++this.seq}` },
|
|
20250
|
+
Date.now()
|
|
20251
|
+
);
|
|
20252
|
+
if (willDeliverNow) this.lastDeliveredAt.set(input.key, Date.now());
|
|
20253
|
+
console.error(
|
|
20254
|
+
`[webchat:inbound] op=received key=${keyDisplay(input.key)} role=${input.role} bytes=${bytes} delivery=${willDeliverNow ? "immediate" : "queued"}`
|
|
20255
|
+
);
|
|
20256
|
+
if (input.role === "admin") {
|
|
20257
|
+
this.recoverable.set(input.key, {
|
|
20258
|
+
accountId: input.accountId,
|
|
20259
|
+
key: input.key,
|
|
20260
|
+
role: input.role,
|
|
20261
|
+
...input.sessionId !== void 0 ? { sessionId: input.sessionId } : {},
|
|
20262
|
+
...input.adminUserId !== void 0 ? { adminUserId: input.adminUserId } : {}
|
|
20263
|
+
});
|
|
20264
|
+
}
|
|
20265
|
+
const existing = this.publicSessions.get(input.key);
|
|
20266
|
+
if (existing) existing.lastActivityAt = Date.now();
|
|
20267
|
+
if (!hadSubscriber && !this.spawning.has(input.key)) {
|
|
20268
|
+
this.spawning.add(input.key);
|
|
20269
|
+
console.error(`[webchat:inbound] op=spawn-trigger key=${input.key} role=${input.role}`);
|
|
20270
|
+
try {
|
|
20271
|
+
const spawned = await this.deps.ensureChannelSession({
|
|
20272
|
+
accountId: input.accountId,
|
|
20273
|
+
key: input.key,
|
|
20274
|
+
role: input.role,
|
|
20275
|
+
gatewayUrl: this.deps.gatewayUrl,
|
|
20276
|
+
serverPath: this.deps.serverPath,
|
|
20277
|
+
// Task 800 — only present on a session-targeted launch, so the
|
|
20278
|
+
// default-path call shape stays byte-identical.
|
|
20279
|
+
...input.sessionId !== void 0 ? { sessionId: input.sessionId } : {},
|
|
20280
|
+
// The authenticated admin's userId on an admin bootstrap inbound; the
|
|
20281
|
+
// default-path call shape stays byte-identical when absent.
|
|
20282
|
+
...input.adminUserId !== void 0 ? { adminUserId: input.adminUserId } : {},
|
|
20283
|
+
// Task 756 — public spawn params on a role:'public' inbound.
|
|
20284
|
+
...input.public !== void 0 ? { public: input.public } : {}
|
|
20285
|
+
});
|
|
20286
|
+
if (input.role === "public" && input.public && spawned && "sessionId" in spawned) {
|
|
20287
|
+
this.publicSessions.set(input.key, {
|
|
20288
|
+
sessionId: spawned.sessionId,
|
|
20289
|
+
accountId: input.accountId,
|
|
20290
|
+
agentSlug: input.public.agentSlug,
|
|
20291
|
+
sliceToken: input.public.sliceToken,
|
|
20292
|
+
personId: input.public.personId,
|
|
20293
|
+
lastActivityAt: Date.now()
|
|
20294
|
+
});
|
|
20295
|
+
}
|
|
20296
|
+
} finally {
|
|
20297
|
+
this.spawning.delete(input.key);
|
|
20298
|
+
}
|
|
20299
|
+
}
|
|
20300
|
+
}
|
|
20301
|
+
};
|
|
20302
|
+
|
|
20303
|
+
// app/lib/webchat/gateway/public-spawn-request.ts
|
|
20304
|
+
function buildPublicWebchatSpawnRequest(input) {
|
|
20305
|
+
const { agentSlug, sliceToken, personId, visitorId, name } = input.public;
|
|
20306
|
+
return {
|
|
20307
|
+
senderId: input.key,
|
|
20308
|
+
role: "public",
|
|
20309
|
+
channel: "webchat",
|
|
20310
|
+
accountId: input.accountId,
|
|
20311
|
+
agentSlug,
|
|
20312
|
+
model: SONNET_MODEL,
|
|
20313
|
+
// sliceToken/personId are present only for a gated visitor; open-mode
|
|
20314
|
+
// public agents carry neither (mirrors the bridge's open-mode key).
|
|
20315
|
+
...sliceToken ? { sliceToken } : {},
|
|
20316
|
+
...personId ? { personId } : {},
|
|
20317
|
+
...visitorId ? { visitorId } : {},
|
|
20318
|
+
...input.previousContext ? { previousContext: input.previousContext } : {},
|
|
20319
|
+
name,
|
|
20320
|
+
webchatChannel: { key: input.key, gatewayUrl: input.gatewayUrl, serverPath: input.serverPath }
|
|
20321
|
+
};
|
|
20322
|
+
}
|
|
20323
|
+
|
|
20324
|
+
// app/lib/channel-pty-bridge/follower.ts
|
|
20325
|
+
function followerPendingMaxMs() {
|
|
20326
|
+
return Number(process.env.CHANNEL_PTY_FOLLOWER_PENDING_MAX_MS ?? String(3e5));
|
|
20327
|
+
}
|
|
20328
|
+
function followerRetryMs() {
|
|
20329
|
+
return Number(process.env.CHANNEL_PTY_FOLLOWER_RETRY_MS ?? String(1e3));
|
|
20330
|
+
}
|
|
20331
|
+
function startFollower(opts) {
|
|
20332
|
+
const abort = new AbortController();
|
|
20333
|
+
const { entry, tag } = opts;
|
|
20334
|
+
void (async () => {
|
|
20335
|
+
try {
|
|
20336
|
+
const sid = entry.sessionId.slice(0, 8);
|
|
20337
|
+
const deadline = Date.now() + followerPendingMaxMs();
|
|
20338
|
+
const retryMs = followerRetryMs();
|
|
20339
|
+
let res;
|
|
20340
|
+
let attempt = 0;
|
|
20341
|
+
for (; ; ) {
|
|
20342
|
+
res = await fetch(
|
|
20343
|
+
managerLogFollowUrl(entry.sessionId, { boundary: opts.suppressResumeReplay === true }),
|
|
20344
|
+
{ signal: abort.signal }
|
|
20345
|
+
);
|
|
20346
|
+
console.error(`${tag} follower-connect sessionId=${sid} status=${res.status}`);
|
|
20347
|
+
if (res.status === 202) {
|
|
20348
|
+
attempt += 1;
|
|
20349
|
+
await res.body?.cancel().catch(() => {
|
|
20350
|
+
});
|
|
20351
|
+
if (abort.signal.aborted) return;
|
|
20352
|
+
if (Date.now() >= deadline) {
|
|
20353
|
+
console.error(`${tag} follower-give-up sessionId=${sid} reason=pending-timeout attempts=${attempt}`);
|
|
20354
|
+
opts.onError?.("follow-pending-timeout");
|
|
20355
|
+
return;
|
|
20356
|
+
}
|
|
20357
|
+
console.error(`${tag} follower-retry sessionId=${sid} attempt=${attempt} reason=pending`);
|
|
20358
|
+
await new Promise((r) => setTimeout(r, retryMs));
|
|
20359
|
+
if (abort.signal.aborted) return;
|
|
20360
|
+
continue;
|
|
20361
|
+
}
|
|
20362
|
+
break;
|
|
20363
|
+
}
|
|
20364
|
+
if (!res.ok || !res.body) {
|
|
20365
|
+
opts.onError?.(`follow-status-${res.status}`);
|
|
20366
|
+
return;
|
|
20367
|
+
}
|
|
20368
|
+
console.error(`${tag} follower-open sessionId=${sid}`);
|
|
20369
|
+
const reader = res.body.getReader();
|
|
20370
|
+
const decoder = new TextDecoder("utf8");
|
|
20371
|
+
let buffered = "";
|
|
20372
|
+
const fileDelivery = opts.fileDelivery ?? null;
|
|
20373
|
+
let firedFileTools = [];
|
|
20374
|
+
let suppressing = opts.suppressResumeReplay === true;
|
|
20375
|
+
let discardedTurns = 0;
|
|
20376
|
+
let discardedFileTools = 0;
|
|
20377
|
+
while (!abort.signal.aborted) {
|
|
20378
|
+
const { value, done } = await reader.read();
|
|
20379
|
+
if (done) break;
|
|
20380
|
+
buffered += decoder.decode(value, { stream: true });
|
|
20381
|
+
let nl = buffered.indexOf("\n");
|
|
20382
|
+
while (nl !== -1) {
|
|
20383
|
+
const line = buffered.slice(0, nl);
|
|
20384
|
+
buffered = buffered.slice(nl + 1);
|
|
20385
|
+
nl = buffered.indexOf("\n");
|
|
20386
|
+
if (!line) continue;
|
|
20387
|
+
let event;
|
|
20388
|
+
try {
|
|
20389
|
+
event = JSON.parse(line);
|
|
20390
|
+
} catch (err) {
|
|
20391
|
+
console.error(
|
|
20392
|
+
`${tag} jsonl-parse-skip sessionId=${entry.sessionId.slice(0, 8)} bytes=${line.length} message=${err instanceof Error ? err.message : String(err)}`
|
|
20393
|
+
);
|
|
20394
|
+
continue;
|
|
20395
|
+
}
|
|
20396
|
+
if (event.type === "__channel_resume_boundary__") {
|
|
20397
|
+
if (suppressing) {
|
|
20398
|
+
suppressing = false;
|
|
20399
|
+
console.error(
|
|
20400
|
+
`${tag} follower-resume-boundary sessionId=${sid} discarded-head-turns=${discardedTurns} discarded-head-filetools=${discardedFileTools}`
|
|
20401
|
+
);
|
|
20402
|
+
}
|
|
20403
|
+
continue;
|
|
20404
|
+
}
|
|
20405
|
+
if (event.type === "user") {
|
|
20406
|
+
entry.pendingTurnText = "";
|
|
20407
|
+
firedFileTools = [];
|
|
20408
|
+
continue;
|
|
20409
|
+
}
|
|
20410
|
+
if (event.type !== "assistant") continue;
|
|
20411
|
+
const msg = event.message;
|
|
20412
|
+
if (!msg) continue;
|
|
20413
|
+
let sawContent = false;
|
|
20414
|
+
if (Array.isArray(msg.content)) {
|
|
20415
|
+
for (const block of msg.content) {
|
|
20416
|
+
if (block?.type === "text" && typeof block.text === "string") {
|
|
20417
|
+
entry.pendingTurnText += block.text;
|
|
20418
|
+
sawContent = true;
|
|
20419
|
+
} else if (block?.type === "tool_use") {
|
|
20420
|
+
sawContent = true;
|
|
20421
|
+
if (fileDelivery && typeof block.name === "string" && fileDelivery.isFileDeliveryTool(block.name)) {
|
|
20422
|
+
if (suppressing) {
|
|
20423
|
+
discardedFileTools += 1;
|
|
20424
|
+
continue;
|
|
20425
|
+
}
|
|
20426
|
+
firedFileTools.push(block.name);
|
|
20427
|
+
try {
|
|
20428
|
+
await fileDelivery.onFileToolUse({ toolName: block.name, input: block.input });
|
|
20429
|
+
} catch (err) {
|
|
20430
|
+
console.error(
|
|
20431
|
+
`${tag} file-delivery-error sessionId=${sid} tool=${block.name} message=${err instanceof Error ? err.message : String(err)}`
|
|
20432
|
+
);
|
|
20433
|
+
}
|
|
20434
|
+
}
|
|
20435
|
+
}
|
|
20436
|
+
}
|
|
20437
|
+
}
|
|
20438
|
+
if (msg.stop_reason === "end_turn") {
|
|
20439
|
+
if (suppressing) {
|
|
20440
|
+
entry.pendingTurnText = "";
|
|
20441
|
+
firedFileTools = [];
|
|
20442
|
+
discardedTurns += 1;
|
|
20443
|
+
continue;
|
|
20444
|
+
}
|
|
20445
|
+
const flush = entry.pendingTurnText;
|
|
20446
|
+
entry.pendingTurnText = "";
|
|
20447
|
+
if (flush.trim()) {
|
|
20448
|
+
await fanOut(entry.subscribers, flush, opts.onError, tag);
|
|
20449
|
+
}
|
|
20450
|
+
if (fileDelivery && firedFileTools.length > 0) {
|
|
20451
|
+
const fired = firedFileTools;
|
|
20452
|
+
firedFileTools = [];
|
|
20453
|
+
try {
|
|
20454
|
+
fileDelivery.onTurnEnd(fired);
|
|
20455
|
+
} catch (err) {
|
|
20456
|
+
console.error(
|
|
20457
|
+
`${tag} file-delivery-error sessionId=${sid} phase=turn-end message=${err instanceof Error ? err.message : String(err)}`
|
|
20458
|
+
);
|
|
20459
|
+
}
|
|
20460
|
+
}
|
|
20461
|
+
opts.onTurnComplete?.();
|
|
20462
|
+
} else if (sawContent && !suppressing) {
|
|
20463
|
+
opts.onActivity?.();
|
|
20464
|
+
}
|
|
20465
|
+
}
|
|
20466
|
+
}
|
|
20467
|
+
} catch (err) {
|
|
20468
|
+
if (!abort.signal.aborted) {
|
|
20469
|
+
opts.onError?.(`follower-error: ${err instanceof Error ? err.message : String(err)}`);
|
|
20781
20470
|
}
|
|
20782
20471
|
} 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
|
-
});
|
|
20472
|
+
opts.onClose();
|
|
20807
20473
|
}
|
|
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
|
-
|
|
20474
|
+
})();
|
|
20475
|
+
return abort;
|
|
20476
|
+
}
|
|
20477
|
+
async function fanOut(subscribers, text, onError, tag) {
|
|
20478
|
+
const callbacks = Array.from(subscribers);
|
|
20479
|
+
await Promise.all(
|
|
20480
|
+
callbacks.map(
|
|
20481
|
+
(cb) => Promise.resolve().then(() => cb(text)).catch((err) => {
|
|
20482
|
+
const m = err instanceof Error ? err.message : String(err);
|
|
20483
|
+
console.error(`${tag} subscriber-error message=${m}`);
|
|
20484
|
+
onError?.(`subscriber-error: ${m}`);
|
|
20485
|
+
})
|
|
20486
|
+
)
|
|
20487
|
+
);
|
|
20488
|
+
}
|
|
20489
|
+
|
|
20490
|
+
// app/lib/channel-pty-bridge/file-delivery.ts
|
|
20491
|
+
var SEND_USER_FILE = "SendUserFile";
|
|
20492
|
+
function makeFileDelivery(opts) {
|
|
20493
|
+
const { entry, tag, channel, sendFile } = opts;
|
|
20494
|
+
let failedFiles = [];
|
|
20495
|
+
let attempts = 0;
|
|
20496
|
+
return {
|
|
20497
|
+
isFileDeliveryTool(toolName) {
|
|
20498
|
+
return toolName === SEND_USER_FILE;
|
|
20499
|
+
},
|
|
20500
|
+
async onFileToolUse(use) {
|
|
20501
|
+
if (use.toolName !== SEND_USER_FILE) return;
|
|
20502
|
+
const input = use.input ?? {};
|
|
20503
|
+
const files = Array.isArray(input.files) ? input.files.filter((f) => typeof f === "string") : [];
|
|
20504
|
+
const caption = typeof input.caption === "string" ? input.caption : void 0;
|
|
20505
|
+
for (let i = 0; i < files.length; i++) {
|
|
20506
|
+
attempts++;
|
|
20507
|
+
const result = await sendFile(files[i], i === 0 ? caption : void 0);
|
|
20508
|
+
const name = files[i].split("/").pop() ?? files[i];
|
|
20509
|
+
console.error(`${tag} op=file-forward channel=${channel} file=${name} outcome=${result.ok ? "ok" : "fail"}`);
|
|
20510
|
+
if (!result.ok) failedFiles.push(files[i]);
|
|
20511
|
+
}
|
|
20512
|
+
},
|
|
20513
|
+
onTurnEnd(firedTools) {
|
|
20514
|
+
const failed = failedFiles;
|
|
20515
|
+
const tried = attempts;
|
|
20516
|
+
failedFiles = [];
|
|
20517
|
+
attempts = 0;
|
|
20518
|
+
const sid = entry.sessionId.slice(0, 8);
|
|
20519
|
+
for (const file of failed) {
|
|
20520
|
+
console.error(
|
|
20521
|
+
`${tag} file-delivery-unreconciled sender=${entry.senderId} sessionId=${sid} tool=${SEND_USER_FILE} file=${file}`
|
|
20522
|
+
);
|
|
20523
|
+
}
|
|
20524
|
+
if (firedTools.includes(SEND_USER_FILE) && tried === 0) {
|
|
20525
|
+
console.error(
|
|
20526
|
+
`${tag} file-delivery-unreconciled sender=${entry.senderId} sessionId=${sid} tool=${SEND_USER_FILE}`
|
|
20527
|
+
);
|
|
20841
20528
|
}
|
|
20842
20529
|
}
|
|
20843
|
-
}
|
|
20844
|
-
}
|
|
20530
|
+
};
|
|
20531
|
+
}
|
|
20845
20532
|
|
|
20846
|
-
// app/lib/
|
|
20847
|
-
|
|
20848
|
-
|
|
20533
|
+
// app/lib/whatsapp/inbound/file-delivery-bridge.ts
|
|
20534
|
+
var TAG34 = "[whatsapp-adaptor]";
|
|
20535
|
+
var SEND_USER_FILE2 = "SendUserFile";
|
|
20536
|
+
var WHATSAPP_SEND_DOCUMENT = "whatsapp-send-document";
|
|
20537
|
+
function platformRoot() {
|
|
20538
|
+
return process.env.MAXY_PLATFORM_ROOT || "";
|
|
20539
|
+
}
|
|
20540
|
+
function makeWhatsAppSendFile(entry) {
|
|
20541
|
+
return async (filePath, caption) => {
|
|
20542
|
+
let maxyAccountId;
|
|
20543
|
+
try {
|
|
20544
|
+
maxyAccountId = resolvePlatformAccountId();
|
|
20545
|
+
} catch (err) {
|
|
20546
|
+
console.error(
|
|
20547
|
+
`${TAG34} file-delivery reject reason=account-unresolved sender=${entry.senderId} message=${err instanceof Error ? err.message : String(err)}`
|
|
20548
|
+
);
|
|
20549
|
+
return { ok: false, error: "account-unresolved" };
|
|
20550
|
+
}
|
|
20551
|
+
const result = await sendWhatsAppDocument({
|
|
20552
|
+
to: entry.senderId,
|
|
20553
|
+
filePath,
|
|
20554
|
+
caption,
|
|
20555
|
+
accountId: entry.accountId,
|
|
20556
|
+
maxyAccountId,
|
|
20557
|
+
platformRoot: platformRoot()
|
|
20558
|
+
});
|
|
20559
|
+
if (result.ok) return { ok: true };
|
|
20560
|
+
console.error(
|
|
20561
|
+
`${TAG34} file-delivery reject reason=send-failed sender=${entry.senderId} status=${result.status} message=${result.error}`
|
|
20562
|
+
);
|
|
20563
|
+
return { ok: false, error: result.error };
|
|
20564
|
+
};
|
|
20565
|
+
}
|
|
20566
|
+
function makeWhatsAppFileDelivery(entry) {
|
|
20567
|
+
const shared = makeFileDelivery({
|
|
20568
|
+
entry,
|
|
20569
|
+
tag: TAG34,
|
|
20570
|
+
channel: "whatsapp",
|
|
20571
|
+
sendFile: makeWhatsAppSendFile(entry)
|
|
20572
|
+
});
|
|
20573
|
+
let turnStartedAt = null;
|
|
20574
|
+
let routeCalls = [];
|
|
20849
20575
|
return {
|
|
20850
|
-
|
|
20851
|
-
|
|
20852
|
-
|
|
20853
|
-
|
|
20854
|
-
|
|
20855
|
-
|
|
20856
|
-
|
|
20857
|
-
|
|
20858
|
-
|
|
20859
|
-
|
|
20860
|
-
|
|
20861
|
-
|
|
20862
|
-
|
|
20863
|
-
|
|
20576
|
+
isFileDeliveryTool(toolName) {
|
|
20577
|
+
return toolName === SEND_USER_FILE2 || toolName === WHATSAPP_SEND_DOCUMENT;
|
|
20578
|
+
},
|
|
20579
|
+
async onFileToolUse(use) {
|
|
20580
|
+
if (turnStartedAt === null) turnStartedAt = Date.now();
|
|
20581
|
+
if (use.toolName === WHATSAPP_SEND_DOCUMENT) {
|
|
20582
|
+
const input = use.input ?? {};
|
|
20583
|
+
routeCalls.push({
|
|
20584
|
+
to: typeof input.to === "string" ? input.to : void 0,
|
|
20585
|
+
filePath: typeof input.filePath === "string" ? input.filePath : void 0
|
|
20586
|
+
});
|
|
20587
|
+
return;
|
|
20588
|
+
}
|
|
20589
|
+
await shared.onFileToolUse(use);
|
|
20590
|
+
},
|
|
20591
|
+
onTurnEnd(firedTools) {
|
|
20592
|
+
const startedAt = turnStartedAt ?? 0;
|
|
20593
|
+
const routes = routeCalls;
|
|
20594
|
+
turnStartedAt = null;
|
|
20595
|
+
routeCalls = [];
|
|
20596
|
+
const sid = entry.sessionId.slice(0, 8);
|
|
20597
|
+
shared.onTurnEnd(firedTools);
|
|
20598
|
+
for (const call of routes) {
|
|
20599
|
+
const routeAt = call.to !== void 0 && call.filePath !== void 0 ? routeDocumentOutboundAt(call.to, call.filePath) : void 0;
|
|
20600
|
+
const delivered = routeAt !== void 0 && routeAt >= startedAt;
|
|
20601
|
+
if (!delivered) {
|
|
20602
|
+
const fileField = call.filePath !== void 0 ? ` file=${call.filePath}` : "";
|
|
20603
|
+
console.error(
|
|
20604
|
+
`${TAG34} file-delivery-unreconciled sender=${entry.senderId} sessionId=${sid} tool=${WHATSAPP_SEND_DOCUMENT}${fileField}`
|
|
20605
|
+
);
|
|
20606
|
+
}
|
|
20607
|
+
}
|
|
20608
|
+
}
|
|
20864
20609
|
};
|
|
20865
20610
|
}
|
|
20866
20611
|
|
|
@@ -21211,6 +20956,122 @@ function buildTelegramSpawnRequest(input) {
|
|
|
21211
20956
|
};
|
|
21212
20957
|
}
|
|
21213
20958
|
|
|
20959
|
+
// app/lib/telegram/outbound/send-document.ts
|
|
20960
|
+
import { realpathSync as realpathSync6 } from "fs";
|
|
20961
|
+
import { readFile as readFile7, stat as stat7 } from "fs/promises";
|
|
20962
|
+
import { resolve as resolve29, basename as basename9 } from "path";
|
|
20963
|
+
var TAG35 = "[telegram:outbound]";
|
|
20964
|
+
var TELEGRAM_DOCUMENT_MAX_BYTES = 50 * 1024 * 1024;
|
|
20965
|
+
async function sendTelegramDocument(input) {
|
|
20966
|
+
const { botToken, chatId, filePath, caption, maxyAccountId, platformRoot: platformRoot3 } = input;
|
|
20967
|
+
if (!botToken || !filePath) {
|
|
20968
|
+
return { ok: false, status: 400, error: "Missing required fields: botToken, filePath" };
|
|
20969
|
+
}
|
|
20970
|
+
if (!maxyAccountId || !platformRoot3) {
|
|
20971
|
+
return { ok: false, status: 400, error: "Cannot validate file path: missing account or platform context" };
|
|
20972
|
+
}
|
|
20973
|
+
const accountDir = resolve29(platformRoot3, "..", "data/accounts", maxyAccountId);
|
|
20974
|
+
let resolvedPath;
|
|
20975
|
+
try {
|
|
20976
|
+
resolvedPath = realpathSync6(filePath);
|
|
20977
|
+
const accountResolved = realpathSync6(accountDir);
|
|
20978
|
+
if (!resolvedPath.startsWith(accountResolved + "/")) {
|
|
20979
|
+
console.error(`${TAG35} document REJECTED reason=outside_account_directory`);
|
|
20980
|
+
return { ok: false, status: 403, error: "Access denied: file is outside the account directory" };
|
|
20981
|
+
}
|
|
20982
|
+
} catch (err) {
|
|
20983
|
+
const code = err.code;
|
|
20984
|
+
if (code === "ENOENT") {
|
|
20985
|
+
console.error(`${TAG35} document ENOENT path=${filePath}`);
|
|
20986
|
+
return { ok: false, status: 404, error: `File not found: ${filePath}` };
|
|
20987
|
+
}
|
|
20988
|
+
console.error(`${TAG35} document path error: ${String(err)}`);
|
|
20989
|
+
return { ok: false, status: 500, error: String(err) };
|
|
20990
|
+
}
|
|
20991
|
+
const fileStat = await stat7(resolvedPath);
|
|
20992
|
+
if (fileStat.size > TELEGRAM_DOCUMENT_MAX_BYTES) {
|
|
20993
|
+
return {
|
|
20994
|
+
ok: false,
|
|
20995
|
+
status: 400,
|
|
20996
|
+
error: `File exceeds 50 MB limit (${(fileStat.size / 1024 / 1024).toFixed(1)} MB)`
|
|
20997
|
+
};
|
|
20998
|
+
}
|
|
20999
|
+
const filename = basename9(resolvedPath);
|
|
21000
|
+
const buffer = Buffer.from(await readFile7(resolvedPath));
|
|
21001
|
+
const form = new FormData();
|
|
21002
|
+
form.append("chat_id", String(chatId));
|
|
21003
|
+
if (caption) form.append("caption", caption);
|
|
21004
|
+
form.append("document", new File([buffer], filename, { type: detectMimeType(resolvedPath) }));
|
|
21005
|
+
let ok = false;
|
|
21006
|
+
let messageId;
|
|
21007
|
+
let error = "send failed";
|
|
21008
|
+
try {
|
|
21009
|
+
const res = await fetch(`https://api.telegram.org/bot${botToken}/sendDocument`, {
|
|
21010
|
+
method: "POST",
|
|
21011
|
+
body: form
|
|
21012
|
+
});
|
|
21013
|
+
const data = await res.json();
|
|
21014
|
+
ok = data.ok;
|
|
21015
|
+
messageId = data.result?.message_id;
|
|
21016
|
+
if (!data.ok) error = data.description ?? "Unknown Telegram error";
|
|
21017
|
+
} catch (e) {
|
|
21018
|
+
error = e instanceof Error ? e.message : String(e);
|
|
21019
|
+
}
|
|
21020
|
+
console.error(
|
|
21021
|
+
`${TAG35} sent document to=${chatId} file=${filename} bytes=${fileStat.size} ok=${ok}` + (messageId !== void 0 ? ` id=${messageId}` : "")
|
|
21022
|
+
);
|
|
21023
|
+
return ok ? { ok: true, messageId } : { ok: false, status: 500, error };
|
|
21024
|
+
}
|
|
21025
|
+
|
|
21026
|
+
// app/lib/telegram/outbound/file-delivery.ts
|
|
21027
|
+
var TAG36 = "[telegram:outbound]";
|
|
21028
|
+
function platformRoot2() {
|
|
21029
|
+
return process.env.MAXY_PLATFORM_ROOT || "";
|
|
21030
|
+
}
|
|
21031
|
+
function makeTelegramSendFile(entry) {
|
|
21032
|
+
let botToken;
|
|
21033
|
+
return async (filePath, caption) => {
|
|
21034
|
+
if (botToken === void 0) {
|
|
21035
|
+
const tg = resolveAccount()?.config.telegram;
|
|
21036
|
+
botToken = (entry.role === "admin" ? tg?.adminBotToken : tg?.publicBotToken) ?? null;
|
|
21037
|
+
}
|
|
21038
|
+
if (!botToken) {
|
|
21039
|
+
console.error(`${TAG36} file-delivery reject reason=no-bot-token sender=${entry.senderId} role=${entry.role}`);
|
|
21040
|
+
return { ok: false, error: "no-bot-token" };
|
|
21041
|
+
}
|
|
21042
|
+
if (entry.replyTarget == null) {
|
|
21043
|
+
console.error(`${TAG36} file-delivery reject reason=no-reply-target sender=${entry.senderId} role=${entry.role}`);
|
|
21044
|
+
return { ok: false, error: "no-reply-target" };
|
|
21045
|
+
}
|
|
21046
|
+
let maxyAccountId;
|
|
21047
|
+
try {
|
|
21048
|
+
maxyAccountId = resolvePlatformAccountId();
|
|
21049
|
+
} catch (err) {
|
|
21050
|
+
console.error(
|
|
21051
|
+
`${TAG36} file-delivery reject reason=account-unresolved sender=${entry.senderId} message=${err instanceof Error ? err.message : String(err)}`
|
|
21052
|
+
);
|
|
21053
|
+
return { ok: false, error: "account-unresolved" };
|
|
21054
|
+
}
|
|
21055
|
+
const result = await sendTelegramDocument({
|
|
21056
|
+
botToken,
|
|
21057
|
+
chatId: Number(entry.replyTarget),
|
|
21058
|
+
filePath,
|
|
21059
|
+
caption,
|
|
21060
|
+
maxyAccountId,
|
|
21061
|
+
platformRoot: platformRoot2()
|
|
21062
|
+
});
|
|
21063
|
+
return result.ok ? { ok: true } : { ok: false, error: result.error };
|
|
21064
|
+
};
|
|
21065
|
+
}
|
|
21066
|
+
function makeTelegramFileDelivery(entry) {
|
|
21067
|
+
return makeFileDelivery({
|
|
21068
|
+
entry,
|
|
21069
|
+
tag: TAG36,
|
|
21070
|
+
channel: "telegram",
|
|
21071
|
+
sendFile: makeTelegramSendFile(entry)
|
|
21072
|
+
});
|
|
21073
|
+
}
|
|
21074
|
+
|
|
21214
21075
|
// app/lib/telegram/gateway/native-file-follower.ts
|
|
21215
21076
|
function startTelegramNativeFileFollower(input) {
|
|
21216
21077
|
const entry = {
|
|
@@ -21245,12 +21106,166 @@ function startTelegramNativeFileFollower(input) {
|
|
|
21245
21106
|
}
|
|
21246
21107
|
|
|
21247
21108
|
// server/telegram-descriptor.ts
|
|
21248
|
-
import { resolve as
|
|
21109
|
+
import { resolve as resolve30, join as join31 } from "path";
|
|
21249
21110
|
function telegramGatewayUrl() {
|
|
21250
21111
|
return `http://127.0.0.1:${process.env.MAXY_UI_INTERNAL_PORT ?? ""}`;
|
|
21251
21112
|
}
|
|
21252
21113
|
function telegramServerPath() {
|
|
21253
|
-
return process.env.MAXY_TELEGRAM_CHANNEL_SERVER_PATH ??
|
|
21114
|
+
return process.env.MAXY_TELEGRAM_CHANNEL_SERVER_PATH ?? resolve30(process.env.MAXY_PLATFORM_ROOT ?? join31(__dirname, ".."), "services/telegram-channel/dist/server.js");
|
|
21115
|
+
}
|
|
21116
|
+
|
|
21117
|
+
// app/lib/channel-pty-bridge/public-session-end-review.ts
|
|
21118
|
+
import { randomUUID as randomUUID13 } from "crypto";
|
|
21119
|
+
var TAG37 = "[public-session-review]";
|
|
21120
|
+
var CONSUMED_CAP = 500;
|
|
21121
|
+
var consumed = /* @__PURE__ */ new Set();
|
|
21122
|
+
function consumeOnce(sessionId) {
|
|
21123
|
+
if (consumed.has(sessionId)) return false;
|
|
21124
|
+
consumed.add(sessionId);
|
|
21125
|
+
if (consumed.size > CONSUMED_CAP) {
|
|
21126
|
+
const oldest = consumed.values().next().value;
|
|
21127
|
+
if (oldest !== void 0) consumed.delete(oldest);
|
|
21128
|
+
}
|
|
21129
|
+
return true;
|
|
21130
|
+
}
|
|
21131
|
+
function managerBase3() {
|
|
21132
|
+
const port2 = requirePortEnv("CLAUDE_SESSION_MANAGER_PORT", { tag: "public-session-end-review:manager" });
|
|
21133
|
+
return `http://127.0.0.1:${port2}`;
|
|
21134
|
+
}
|
|
21135
|
+
function uiBase() {
|
|
21136
|
+
const port2 = requirePortEnv("MAXY_UI_INTERNAL_PORT", { tag: "public-session-end-review:ui" });
|
|
21137
|
+
return `http://127.0.0.1:${port2}`;
|
|
21138
|
+
}
|
|
21139
|
+
async function fetchJsonl(sessionId) {
|
|
21140
|
+
try {
|
|
21141
|
+
const res = await fetch(`${managerBase3()}/${sessionId}/log?download=1`);
|
|
21142
|
+
if (!res.ok) return null;
|
|
21143
|
+
return await res.text();
|
|
21144
|
+
} catch (err) {
|
|
21145
|
+
console.error(
|
|
21146
|
+
`${TAG37} jsonl-fetch-failed sessionId=${sessionId} err="${err instanceof Error ? err.message : String(err)}"`
|
|
21147
|
+
);
|
|
21148
|
+
return null;
|
|
21149
|
+
}
|
|
21150
|
+
}
|
|
21151
|
+
function countOperatorTurns(jsonl) {
|
|
21152
|
+
let count = 0;
|
|
21153
|
+
for (const raw of jsonl.split("\n")) {
|
|
21154
|
+
const line = raw.trim();
|
|
21155
|
+
if (!line) continue;
|
|
21156
|
+
try {
|
|
21157
|
+
const obj = JSON.parse(line);
|
|
21158
|
+
if (obj.type === "user") count += 1;
|
|
21159
|
+
} catch {
|
|
21160
|
+
}
|
|
21161
|
+
}
|
|
21162
|
+
return count;
|
|
21163
|
+
}
|
|
21164
|
+
async function fetchPriorWrites(sliceToken, accountId) {
|
|
21165
|
+
const url = `${uiBase()}/api/admin/public-session-context?sliceToken=${encodeURIComponent(sliceToken)}&accountId=${encodeURIComponent(accountId)}`;
|
|
21166
|
+
try {
|
|
21167
|
+
const res = await fetch(url);
|
|
21168
|
+
if (!res.ok) {
|
|
21169
|
+
console.error(
|
|
21170
|
+
`${TAG37} prior-writes-fetch-failed sliceToken=${sliceToken.slice(0, 8)} status=${res.status}`
|
|
21171
|
+
);
|
|
21172
|
+
return [];
|
|
21173
|
+
}
|
|
21174
|
+
const body = await res.json();
|
|
21175
|
+
return Array.isArray(body.writes) ? body.writes : [];
|
|
21176
|
+
} catch (err) {
|
|
21177
|
+
console.error(
|
|
21178
|
+
`${TAG37} prior-writes-fetch-threw sliceToken=${sliceToken.slice(0, 8)} err="${err instanceof Error ? err.message : String(err)}"`
|
|
21179
|
+
);
|
|
21180
|
+
return [];
|
|
21181
|
+
}
|
|
21182
|
+
}
|
|
21183
|
+
function composeInitialMessage(input) {
|
|
21184
|
+
const priorJson = JSON.stringify(input.priorWrites, null, 2);
|
|
21185
|
+
return [
|
|
21186
|
+
`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.`,
|
|
21187
|
+
"",
|
|
21188
|
+
"<slice-token>",
|
|
21189
|
+
input.sliceToken,
|
|
21190
|
+
"</slice-token>",
|
|
21191
|
+
"",
|
|
21192
|
+
"<person-id>",
|
|
21193
|
+
input.personId ?? "(unknown)",
|
|
21194
|
+
"</person-id>",
|
|
21195
|
+
"",
|
|
21196
|
+
"<prior-writes>",
|
|
21197
|
+
priorJson,
|
|
21198
|
+
"</prior-writes>",
|
|
21199
|
+
"",
|
|
21200
|
+
"<transcript>",
|
|
21201
|
+
input.jsonl,
|
|
21202
|
+
"</transcript>"
|
|
21203
|
+
].join("\n");
|
|
21204
|
+
}
|
|
21205
|
+
async function dispatchReviewer(input, initialMessage) {
|
|
21206
|
+
const sessionId = randomUUID13();
|
|
21207
|
+
console.log(`${TAG37} route target=rc-spawn sessionId=${sessionId.slice(0, 8)} sourceSession=${input.sessionId.slice(0, 8)}`);
|
|
21208
|
+
const spawned = await managerRcSpawn({
|
|
21209
|
+
sessionId,
|
|
21210
|
+
initialMessage,
|
|
21211
|
+
closeAfterTurn: true,
|
|
21212
|
+
sliceToken: input.sliceToken,
|
|
21213
|
+
personId: input.personId ?? void 0,
|
|
21214
|
+
logContext: `public-session-end sourceSession=${input.sessionId.slice(0, 8)}`
|
|
21215
|
+
});
|
|
21216
|
+
if ("error" in spawned) {
|
|
21217
|
+
return { ok: false, reason: `rc-spawn-${spawned.status}-${spawned.error}` };
|
|
21218
|
+
}
|
|
21219
|
+
return { ok: true, managerSessionId: spawned.sessionId };
|
|
21220
|
+
}
|
|
21221
|
+
async function firePublicSessionEndReview(input) {
|
|
21222
|
+
const sliceShort = input.sliceToken.slice(0, 8);
|
|
21223
|
+
const personField = input.personId ?? "none";
|
|
21224
|
+
const dispatchedAt = Date.now();
|
|
21225
|
+
if (consumed.has(input.sessionId)) {
|
|
21226
|
+
console.log(
|
|
21227
|
+
`${TAG37} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-already-reviewed`
|
|
21228
|
+
);
|
|
21229
|
+
return;
|
|
21230
|
+
}
|
|
21231
|
+
const jsonl = await fetchJsonl(input.sessionId);
|
|
21232
|
+
if (!jsonl) {
|
|
21233
|
+
console.log(
|
|
21234
|
+
`${TAG37} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-jsonl-missing`
|
|
21235
|
+
);
|
|
21236
|
+
return;
|
|
21237
|
+
}
|
|
21238
|
+
const operatorTurns = countOperatorTurns(jsonl);
|
|
21239
|
+
if (operatorTurns === 0) {
|
|
21240
|
+
console.log(
|
|
21241
|
+
`${TAG37} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-no-turns`
|
|
21242
|
+
);
|
|
21243
|
+
return;
|
|
21244
|
+
}
|
|
21245
|
+
const priorWrites = await fetchPriorWrites(input.sliceToken, input.accountId);
|
|
21246
|
+
const initialMessage = composeInitialMessage({
|
|
21247
|
+
agentSlug: input.agentSlug,
|
|
21248
|
+
sliceToken: input.sliceToken,
|
|
21249
|
+
personId: input.personId,
|
|
21250
|
+
jsonl,
|
|
21251
|
+
priorWrites
|
|
21252
|
+
});
|
|
21253
|
+
if (!consumeOnce(input.sessionId)) {
|
|
21254
|
+
console.log(
|
|
21255
|
+
`${TAG37} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-already-reviewed`
|
|
21256
|
+
);
|
|
21257
|
+
return;
|
|
21258
|
+
}
|
|
21259
|
+
const dispatched = await dispatchReviewer(input, initialMessage);
|
|
21260
|
+
if (!dispatched.ok) {
|
|
21261
|
+
console.error(
|
|
21262
|
+
`${TAG37} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-spawn-failed reason=${dispatched.reason}`
|
|
21263
|
+
);
|
|
21264
|
+
return;
|
|
21265
|
+
}
|
|
21266
|
+
console.log(
|
|
21267
|
+
`${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}`
|
|
21268
|
+
);
|
|
21254
21269
|
}
|
|
21255
21270
|
|
|
21256
21271
|
// app/lib/whatsapp/inbound/channel-admin-binding-drift.ts
|
|
@@ -21319,7 +21334,7 @@ function broadcastAdminShutdown(reason) {
|
|
|
21319
21334
|
// ../lib/entitlement/src/index.ts
|
|
21320
21335
|
import { createPublicKey, createHash as createHash5, verify as cryptoVerify } from "crypto";
|
|
21321
21336
|
import { existsSync as existsSync31, readFileSync as readFileSync32, statSync as statSync12 } from "fs";
|
|
21322
|
-
import { resolve as
|
|
21337
|
+
import { resolve as resolve31 } from "path";
|
|
21323
21338
|
|
|
21324
21339
|
// ../lib/entitlement/src/canonicalize.ts
|
|
21325
21340
|
function canonicalize(value) {
|
|
@@ -21354,7 +21369,7 @@ var PUBKEY_SHA256 = "8eee6bcb33545fd13b16d3199a5735ca5db5062834c7b49dfe4f23801d9
|
|
|
21354
21369
|
var GRACE_DAYS = 7;
|
|
21355
21370
|
var GRACE_MS = GRACE_DAYS * 24 * 60 * 60 * 1e3;
|
|
21356
21371
|
function pubkeyPath(brand) {
|
|
21357
|
-
return
|
|
21372
|
+
return resolve31(brand.platformRoot, "lib", "entitlement", "rubytech-pubkey.pem");
|
|
21358
21373
|
}
|
|
21359
21374
|
var memo = null;
|
|
21360
21375
|
function memoKey(mtimeMs, account) {
|
|
@@ -21366,7 +21381,7 @@ function resolveEntitlement(brand, account) {
|
|
|
21366
21381
|
if (brand.commercialMode !== true) {
|
|
21367
21382
|
return logResolved(implicitTrust(account), null);
|
|
21368
21383
|
}
|
|
21369
|
-
const entitlementPath =
|
|
21384
|
+
const entitlementPath = resolve31(brand.configDir, "entitlement.json");
|
|
21370
21385
|
if (!existsSync31(entitlementPath)) {
|
|
21371
21386
|
return logResolved(anonymousFallback("missing"), { reason: "missing" });
|
|
21372
21387
|
}
|
|
@@ -21628,7 +21643,7 @@ var app54 = new Hono();
|
|
|
21628
21643
|
var nativeFileFollowers = /* @__PURE__ */ new Map();
|
|
21629
21644
|
var waGateway = new WaGateway({
|
|
21630
21645
|
gatewayUrl: `http://127.0.0.1:${process.env.MAXY_UI_INTERNAL_PORT ?? ""}`,
|
|
21631
|
-
serverPath: process.env.MAXY_WA_CHANNEL_SERVER_PATH ??
|
|
21646
|
+
serverPath: process.env.MAXY_WA_CHANNEL_SERVER_PATH ?? resolve32(process.env.MAXY_PLATFORM_ROOT ?? join32(__dirname, ".."), "services/whatsapp-channel/dist/server.js"),
|
|
21632
21647
|
// Task 751 — file delivery on the native channel: resolve the platform
|
|
21633
21648
|
// account + path validation here and funnel through the shared send core.
|
|
21634
21649
|
sendDocument: async ({ senderId, accountId, filePath, caption }) => {
|
|
@@ -21644,7 +21659,7 @@ var waGateway = new WaGateway({
|
|
|
21644
21659
|
caption,
|
|
21645
21660
|
accountId,
|
|
21646
21661
|
maxyAccountId,
|
|
21647
|
-
platformRoot:
|
|
21662
|
+
platformRoot: resolve32(process.env.MAXY_PLATFORM_ROOT ?? join32(__dirname, ".."))
|
|
21648
21663
|
});
|
|
21649
21664
|
return result.ok ? { ok: true, messageId: result.messageId } : { ok: false, error: result.error };
|
|
21650
21665
|
},
|
|
@@ -21747,6 +21762,7 @@ var webchatGateway = new WebchatGateway({
|
|
|
21747
21762
|
deleteSession: (sessionId) => managerDelete(sessionId),
|
|
21748
21763
|
firePublicSessionEndReview: (input) => firePublicSessionEndReview(input)
|
|
21749
21764
|
});
|
|
21765
|
+
setWebchatGateway(webchatGateway);
|
|
21750
21766
|
app54.route("/", webchatGateway.routes());
|
|
21751
21767
|
webchatGateway.startSweeper();
|
|
21752
21768
|
webchatGateway.startPublicReaper();
|
|
@@ -21958,7 +21974,7 @@ app54.post("/__remote-auth/change-password", async (c) => {
|
|
|
21958
21974
|
}
|
|
21959
21975
|
try {
|
|
21960
21976
|
if (targetUserId) {
|
|
21961
|
-
await setAccessPassword(targetUserId, newPassword, USERS_FILE);
|
|
21977
|
+
await setAccessPassword(targetUserId, newPassword, USERS_FILE, { actor: targetUserId, logFile: USERS_AUDIT_LOG });
|
|
21962
21978
|
} else {
|
|
21963
21979
|
await setRemotePassword(newPassword);
|
|
21964
21980
|
}
|
|
@@ -22045,7 +22061,7 @@ app54.post("/api/remote-auth/set-password", async (c) => {
|
|
|
22045
22061
|
}
|
|
22046
22062
|
try {
|
|
22047
22063
|
if (sessionUserId) {
|
|
22048
|
-
await setAccessPassword(sessionUserId, body.password, USERS_FILE);
|
|
22064
|
+
await setAccessPassword(sessionUserId, body.password, USERS_FILE, { actor: sessionUserId, logFile: USERS_AUDIT_LOG });
|
|
22049
22065
|
} else {
|
|
22050
22066
|
await setRemotePassword(body.password);
|
|
22051
22067
|
}
|
|
@@ -22150,8 +22166,8 @@ app54.get("/agent-assets/:slug/:filename", (c) => {
|
|
|
22150
22166
|
console.error(`[agent-assets] no-account slug=${slug} file=${filename}`);
|
|
22151
22167
|
return c.text("Not found", 404);
|
|
22152
22168
|
}
|
|
22153
|
-
const filePath =
|
|
22154
|
-
const expectedDir =
|
|
22169
|
+
const filePath = resolve32(account.accountDir, "agents", slug, "assets", filename);
|
|
22170
|
+
const expectedDir = resolve32(account.accountDir, "agents", slug, "assets");
|
|
22155
22171
|
if (!filePath.startsWith(expectedDir + "/")) {
|
|
22156
22172
|
console.error(`[agent-assets] path-traversal-rejected slug=${slug} file=${filename}`);
|
|
22157
22173
|
return c.text("Forbidden", 403);
|
|
@@ -22180,8 +22196,8 @@ app54.get("/generated/:filename", (c) => {
|
|
|
22180
22196
|
console.error(`[generated] serve file=${filename} status=404`);
|
|
22181
22197
|
return c.text("Not found", 404);
|
|
22182
22198
|
}
|
|
22183
|
-
const filePath =
|
|
22184
|
-
const expectedDir =
|
|
22199
|
+
const filePath = resolve32(account.accountDir, "generated", filename);
|
|
22200
|
+
const expectedDir = resolve32(account.accountDir, "generated");
|
|
22185
22201
|
if (!filePath.startsWith(expectedDir + "/")) {
|
|
22186
22202
|
console.error(`[generated] serve file=${filename} status=403`);
|
|
22187
22203
|
return c.text("Forbidden", 403);
|
|
@@ -22235,11 +22251,11 @@ var brandScript = `<script>window.__BRAND__=${JSON.stringify({
|
|
|
22235
22251
|
var brandThemeColor = BRAND.defaultColors?.primary ?? "#000000";
|
|
22236
22252
|
var brandBackgroundColor = BRAND.defaultColors?.background ?? "#ffffff";
|
|
22237
22253
|
var brandAppIconsExist = [brandAppIcon192Path, brandAppIcon512Path, brandMaskableIconPath].every(
|
|
22238
|
-
(p) => existsSync32(
|
|
22254
|
+
(p) => existsSync32(resolve32(process.cwd(), "public", p.replace(/^\//, "")))
|
|
22239
22255
|
);
|
|
22240
22256
|
var SW_SOURCE = (() => {
|
|
22241
22257
|
try {
|
|
22242
|
-
return readFileSync33(
|
|
22258
|
+
return readFileSync33(resolve32(process.cwd(), "public", "sw.js"), "utf-8");
|
|
22243
22259
|
} catch {
|
|
22244
22260
|
return null;
|
|
22245
22261
|
}
|
|
@@ -22290,7 +22306,7 @@ var clientErrorReporterScript = `<script>
|
|
|
22290
22306
|
function cachedHtml(file) {
|
|
22291
22307
|
let html = htmlCache.get(file);
|
|
22292
22308
|
if (!html) {
|
|
22293
|
-
html = readFileSync33(
|
|
22309
|
+
html = readFileSync33(resolve32(process.cwd(), "public", file), "utf-8");
|
|
22294
22310
|
const productNameEsc = escapeHtml(BRAND.productName);
|
|
22295
22311
|
html = html.replace(/<title>([^<]*)<\/title>/, (_match, inner) => `<title>${escapeHtml(inner).replace(/Maxy/g, productNameEsc)}</title>`);
|
|
22296
22312
|
html = html.replace('href="/favicon.ico"', `href="${escapeHtml(brandFaviconPath)}"`);
|
|
@@ -22408,7 +22424,7 @@ app54.use("/vnc-popout.html", logViewerFetch);
|
|
|
22408
22424
|
app54.get("/vnc-popout.html", (c) => {
|
|
22409
22425
|
let html = htmlCache.get("vnc-popout.html");
|
|
22410
22426
|
if (!html) {
|
|
22411
|
-
html = readFileSync33(
|
|
22427
|
+
html = readFileSync33(resolve32(process.cwd(), "public", "vnc-popout.html"), "utf-8");
|
|
22412
22428
|
const name = escapeHtml(BRAND.productName);
|
|
22413
22429
|
html = html.replace("<title>Browser \u2014 Maxy</title>", `<title>${name}</title>`);
|
|
22414
22430
|
html = html.replace("</head>", ` ${brandScript}
|
|
@@ -22532,7 +22548,7 @@ var httpServer = serve({ fetch: app54.fetch, port, hostname });
|
|
|
22532
22548
|
console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
|
|
22533
22549
|
{
|
|
22534
22550
|
const reconcilePlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join32(__dirname, "..");
|
|
22535
|
-
const reconcileScript =
|
|
22551
|
+
const reconcileScript = resolve32(reconcilePlatformRoot, "plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js");
|
|
22536
22552
|
const RECONCILE_INTERVAL_MS = 12e4;
|
|
22537
22553
|
const runReconcile = () => {
|
|
22538
22554
|
if (!existsSync32(reconcileScript)) return;
|
|
@@ -22554,7 +22570,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
|
|
|
22554
22570
|
}
|
|
22555
22571
|
{
|
|
22556
22572
|
const auditPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join32(__dirname, "..");
|
|
22557
|
-
const auditScript =
|
|
22573
|
+
const auditScript = resolve32(auditPlatformRoot, "plugins/connector/mcp/dist/scripts/audit-connectors.js");
|
|
22558
22574
|
const auditLogDir = process.env.LOG_DIR ?? LOG_DIR;
|
|
22559
22575
|
const CONNECTOR_AUDIT_INTERVAL_MS = 3e5;
|
|
22560
22576
|
const runConnectorAudit = () => {
|
|
@@ -22657,7 +22673,7 @@ try {
|
|
|
22657
22673
|
const migAccount = resolveAccount();
|
|
22658
22674
|
const ownerUserId = migAccount?.config.admins?.find((a) => a.role === "owner")?.userId;
|
|
22659
22675
|
if (ownerUserId) {
|
|
22660
|
-
const result = migrateLegacyRemotePassword(USERS_FILE, REMOTE_PASSWORD_FILE, ownerUserId);
|
|
22676
|
+
const result = migrateLegacyRemotePassword(USERS_FILE, REMOTE_PASSWORD_FILE, ownerUserId, { actor: "boot", logFile: USERS_AUDIT_LOG });
|
|
22661
22677
|
console.error(`[remote-access-migrate] result=${result} ownerUserId=${ownerUserId.slice(0, 8)}`);
|
|
22662
22678
|
} else {
|
|
22663
22679
|
console.error("[remote-access-migrate] result=noop-no-owner ownerUserId=none");
|
|
@@ -22698,6 +22714,35 @@ var adminUserReconcileTimer = setInterval(() => {
|
|
|
22698
22714
|
void runAdminUserReconcileTick();
|
|
22699
22715
|
}, ADMINUSER_RECONCILE_INTERVAL_MS);
|
|
22700
22716
|
if (typeof adminUserReconcileTimer.unref === "function") adminUserReconcileTimer.unref();
|
|
22717
|
+
var USERS_RECONCILE_INTERVAL_MS = 60 * 60 * 1e3;
|
|
22718
|
+
function countUsersRows() {
|
|
22719
|
+
if (!existsSync32(USERS_FILE)) return 0;
|
|
22720
|
+
const raw = readFileSync33(USERS_FILE, "utf-8").trim();
|
|
22721
|
+
if (!raw) return 0;
|
|
22722
|
+
const users = JSON.parse(raw);
|
|
22723
|
+
return users.filter((u) => typeof u.userId === "string").length;
|
|
22724
|
+
}
|
|
22725
|
+
function runUsersReconcileTick() {
|
|
22726
|
+
try {
|
|
22727
|
+
const d = computeAdminStoreDivergence({ usersFile: USERS_FILE, accountsDir: ACCOUNTS_DIR });
|
|
22728
|
+
if (d.errors.length > 0) {
|
|
22729
|
+
console.error(`[users-reconcile] op=error detail=${d.errors.map((e) => `${e.source}:${e.detail}`).join(";")}`);
|
|
22730
|
+
return;
|
|
22731
|
+
}
|
|
22732
|
+
if (d.divergences === 0) {
|
|
22733
|
+
console.error(`[users-reconcile] op=ok admins=${countUsersRows()}`);
|
|
22734
|
+
return;
|
|
22735
|
+
}
|
|
22736
|
+
const accountOnly = d.accountWithoutUsers.map((a) => a.userId.slice(0, 8)).join(",");
|
|
22737
|
+
const usersOnly = d.usersWithoutAccount.map((u) => u.userId.slice(0, 8)).join(",");
|
|
22738
|
+
console.error(`[users-reconcile] op=divergence accountOnly=${accountOnly} usersOnly=${usersOnly}`);
|
|
22739
|
+
} catch (err) {
|
|
22740
|
+
console.error(`[users-reconcile] op=error detail=${err instanceof Error ? err.message : String(err)}`);
|
|
22741
|
+
}
|
|
22742
|
+
}
|
|
22743
|
+
runUsersReconcileTick();
|
|
22744
|
+
var usersReconcileTimer = setInterval(runUsersReconcileTick, USERS_RECONCILE_INTERVAL_MS);
|
|
22745
|
+
if (typeof usersReconcileTimer.unref === "function") usersReconcileTimer.unref();
|
|
22701
22746
|
startGraphHealthTimer();
|
|
22702
22747
|
void migrateUploads().catch((err) => console.error(`[uploads-migrate] failed err="${err instanceof Error ? err.message : String(err)}"`)).finally(() => {
|
|
22703
22748
|
void startFileWatcher().catch((err) => {
|
|
@@ -22786,7 +22831,7 @@ if (bootAccountConfig?.whatsapp) {
|
|
|
22786
22831
|
}
|
|
22787
22832
|
init({
|
|
22788
22833
|
configDir: configDirForWhatsApp,
|
|
22789
|
-
platformRoot:
|
|
22834
|
+
platformRoot: resolve32(process.env.MAXY_PLATFORM_ROOT ?? join32(__dirname, "..")),
|
|
22790
22835
|
accountConfig: bootAccountConfig,
|
|
22791
22836
|
onMessage: async (msg) => {
|
|
22792
22837
|
if (msg.isOwnerMirror) {
|
|
@@ -22827,7 +22872,6 @@ init({
|
|
|
22827
22872
|
}).catch((err) => {
|
|
22828
22873
|
console.error(`[whatsapp] init failed: ${String(err)}`);
|
|
22829
22874
|
});
|
|
22830
|
-
startReaper();
|
|
22831
22875
|
var shuttingDown = false;
|
|
22832
22876
|
process.on("SIGTERM", async () => {
|
|
22833
22877
|
if (shuttingDown) {
|