@rubytech/create-maxy-code 0.1.387 → 0.1.388
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
CHANGED
|
@@ -101,13 +101,15 @@ The platform enforces this at multiple levels:
|
|
|
101
101
|
| Term | Definition | Code |
|
|
102
102
|
|------|-----------|------|
|
|
103
103
|
| **Self phone** | The WhatsApp account's own phone number (the phone that scanned the QR code). Self-chat only. | `phonesMatch(senderPhone, selfPhone)` in `access-control.ts` |
|
|
104
|
-
| **Admin phones** | Phones listed in `adminPhones` in `account.json`. The user's personal phone is auto-registered on QR link; additional phones are added via `whatsapp-config action: "add-admin-phone"`. Their admin session is scoped to the **house** account. | `isAdminPhone()` in `access-control.ts` — iterates `adminPhones` |
|
|
105
|
-
| **Account managers** | Phones in the `accountManagers` map (`account.json`, phone → sub-account UUID), added via `whatsapp-config action: "add-account-manager"`. Their admin session is scoped to the **bound sub-account**, not the house. Disjoint from `adminPhones` — a phone is a house admin or a sub-account manager, never both. | `managedAccountFor()` in `access-control.ts
|
|
104
|
+
| **Admin phones** | Phones listed in `adminPhones` in `account.json`. The user's personal phone is auto-registered on QR link; additional phones are added via `whatsapp-config action: "add-admin-phone"`. Their admin session is scoped to the **house** account. `adminPhones` is authoritative only on the account that owns the WhatsApp paired socket (the house): `add-admin-phone` is refused on any other account, and a list left on a non-socket (sub-)account is inert and purged at boot with `[admin-identity] op=purge-nonsocket-adminphone`. To answer "is this phone a manager?" read the house account's `adminPhones` + `accountManagers` only — never a sub-account's list. | `isAdminPhone()` in `access-control.ts` — iterates `adminPhones` |
|
|
105
|
+
| **Account managers** | Phones in the `accountManagers` map (`account.json`, phone → sub-account UUID), added via `whatsapp-config action: "add-account-manager"`. Their admin session is scoped to the **bound sub-account**, not the house. Disjoint from `adminPhones` — a phone is a house admin or a sub-account manager, never both. | `managedAccountFor()` in `access-control.ts` resolves the sub-account **once**; `checkDmAccess` returns it as `effectiveAccountId`, and `ensureChannelSession` consumes that value without re-resolving |
|
|
106
106
|
| **Public/unknown** | Any phone that is not self, not in `adminPhones`, and not in `accountManagers`. Subject to DM policy gating. | Everything else in `checkDmAccess()` |
|
|
107
107
|
|
|
108
108
|
**Critical distinction:** The *self phone* is the paired device's number (the WhatsApp account Maxy controls). The *admin phone* is the user's personal phone (the phone they message *from*). These are typically different numbers. If the user's personal phone is not in `adminPhones` or `accountManagers`, their messages route as public — not admin.
|
|
109
109
|
|
|
110
|
-
**Account-manager scoping — the two-account split.** A designated manager's inbound spawns an admin session scoped to their sub-account (its graph, skills, and `${USER_ID}` attribution). Replies and file delivery still go out over the **house's** paired WhatsApp device — the only WhatsApp socket — so the sub-account scope never redirects the transport.
|
|
110
|
+
**Account-manager scoping — the two-account split.** A designated manager's inbound spawns an admin session scoped to their sub-account (its graph, skills, and `${USER_ID}` attribution). Replies and file delivery still go out over the **house's** paired WhatsApp device — the only WhatsApp socket — so the sub-account scope never redirects the transport. The connection (house) account is kept for `startNativeFileFollower` and the reply path. A stale binding (a phone wrongly also on `adminPhones`) is surfaced at boot by `account-manager-drift` and never silently mis-routes.
|
|
111
|
+
|
|
112
|
+
**Single source of truth + fail-closed.** The effective account is resolved **exactly once**, in the gate: `checkDmAccess` returns `effectiveAccountId` (the house account for owner/`adminPhones`/public, the bound sub-account for a manager), and that value threads through the inbound payload → gateway → `ensureChannelSession`, which spawns into it **without re-reading** the `accountManagers` map. There is no second resolution and no `?? accountId` house fallback: if a manager's bound sub-account is not a valid account, the inbound is **rejected** (no session spawned, no reply), never routed to the house. This closes the escalation where a divergence between two independent map reads handed a scoped manager a house-owner admin session. Observable signals: `op=account-manager-route … effectiveAccount=… source=gate` on a routed manager inbound; `op=account-manager-reject … reason=unresolved-effective-account` on the fail-closed drop; a standing `op=escalation-tripwire` belt that can only fire if a future change reintroduces the divergence.
|
|
111
113
|
|
|
112
114
|
**`dmPolicy` behaviour (per-account):**
|
|
113
115
|
|
|
@@ -31,6 +31,8 @@ Call `whatsapp-config action: list-admin-phones` and present the current list. A
|
|
|
31
31
|
|
|
32
32
|
Each add/remove fires immediately — admin phones are managed per-action, independent of the form's batch submit.
|
|
33
33
|
|
|
34
|
+
Admin phones are authoritative only on the account that owns the WhatsApp paired socket (the house). Adding an admin phone always applies to that account; the write is refused on any other account, because a phone in a sub-account's admin list is never consulted. To route a person to a specific sub-account, bind them as an account manager with `add-account-manager` instead — do not read or edit a sub-account's admin list to answer "is this phone a manager?".
|
|
35
|
+
|
|
34
36
|
**Account managers:**
|
|
35
37
|
|
|
36
38
|
An account manager is a phone bound to a specific sub-account. Its WhatsApp messages spawn an admin session scoped to that sub-account's agent, not the house. Use this when a person who manages one client sub-account should reach that sub-account's agent over WhatsApp.
|
package/payload/server/server.js
CHANGED
|
@@ -1307,7 +1307,7 @@ var serveStatic = (options = { root: "" }) => {
|
|
|
1307
1307
|
// server/index.ts
|
|
1308
1308
|
import { readFileSync as readFileSync35, existsSync as existsSync33, watchFile } from "fs";
|
|
1309
1309
|
import { spawn as spawn3 } from "child_process";
|
|
1310
|
-
import { resolve as resolve33, join as join34, basename as
|
|
1310
|
+
import { resolve as resolve33, join as join34, basename as basename13 } from "path";
|
|
1311
1311
|
import { homedir as homedir3 } from "os";
|
|
1312
1312
|
import { monitorEventLoopDelay } from "perf_hooks";
|
|
1313
1313
|
|
|
@@ -1726,7 +1726,7 @@ var WhatsAppAccountSchema = z.object({
|
|
|
1726
1726
|
var WhatsAppConfigSchema = z.object({
|
|
1727
1727
|
accounts: z.record(z.string(), WhatsAppAccountSchema.optional()).optional().describe("Per-account config keyed by account ID."),
|
|
1728
1728
|
dmPolicy: DmPolicySchema.optional().default("disabled").describe("Who can message your public agent via WhatsApp DM. 'Open' lets anyone message. 'Allowlist' restricts to approved phone numbers. 'Disabled' blocks all public messages."),
|
|
1729
|
-
adminPhones: z.array(z.string()).optional().describe("Phone numbers (E.164) that route to the admin agent. Managed via add-admin-phone / remove-admin-phone."),
|
|
1729
|
+
adminPhones: z.array(z.string()).optional().describe("Phone numbers (E.164) that route to the admin agent. Managed via add-admin-phone / remove-admin-phone. Authoritative only on the account that owns the WhatsApp paired socket; a list on any other account is inert and purged at boot \u2014 use accountManagers for sub-account routing."),
|
|
1730
1730
|
accountManagers: z.record(z.string(), z.string()).optional().describe("Account-manager bindings: E.164 phone \u2192 sub-account UUID. A designated phone spawns an admin session scoped to that sub-account rather than the house. Managed via add-account-manager / remove-account-manager. A phone here must not also be in adminPhones."),
|
|
1731
1731
|
allowFrom: z.array(z.string()).optional().describe("Phone numbers allowed to message the public agent when DM policy is 'allowlist'. Add '*' for open access. Does not affect admin routing \u2014 use adminPhones for that."),
|
|
1732
1732
|
sendReadReceipts: z.boolean().optional().default(true).describe("Whether to send read receipts (blue ticks) when messages are received. Disabling may feel less responsive but preserves privacy."),
|
|
@@ -2628,27 +2628,31 @@ function isAdminPhone(phone, adminPhones) {
|
|
|
2628
2628
|
return adminPhones.some((entry) => phonesMatch(entry, normalized));
|
|
2629
2629
|
}
|
|
2630
2630
|
function checkDmAccess(params) {
|
|
2631
|
-
const { senderPhone, selfPhone } = params;
|
|
2631
|
+
const { senderPhone, selfPhone, accountId } = params;
|
|
2632
2632
|
const cfg = resolveAccountConfig(params.config, params.accountConfig);
|
|
2633
2633
|
if (phonesMatch(senderPhone, selfPhone)) {
|
|
2634
|
-
return { allowed: true, reason: "self-phone", agentType: "admin" };
|
|
2634
|
+
return { allowed: true, reason: "self-phone", agentType: "admin", effectiveAccountId: accountId };
|
|
2635
2635
|
}
|
|
2636
2636
|
if (isAdminPhone(senderPhone, cfg.adminPhones)) {
|
|
2637
|
-
return { allowed: true, reason: "admin-binding", agentType: "admin" };
|
|
2637
|
+
return { allowed: true, reason: "admin-binding", agentType: "admin", effectiveAccountId: accountId };
|
|
2638
2638
|
}
|
|
2639
|
-
|
|
2640
|
-
|
|
2639
|
+
const managedSub = managedAccountFor(cfg.accountManagers, senderPhone);
|
|
2640
|
+
if (managedSub) {
|
|
2641
|
+
if (params.isValidAccount && !params.isValidAccount(managedSub)) {
|
|
2642
|
+
return { allowed: false, reason: "account-manager-unresolved", agentType: "admin" };
|
|
2643
|
+
}
|
|
2644
|
+
return { allowed: true, reason: "account-manager", agentType: "admin", effectiveAccountId: managedSub };
|
|
2641
2645
|
}
|
|
2642
2646
|
switch (cfg.dmPolicy) {
|
|
2643
2647
|
case "open":
|
|
2644
|
-
return { allowed: true, reason: "dm-policy-open", agentType: "public" };
|
|
2648
|
+
return { allowed: true, reason: "dm-policy-open", agentType: "public", effectiveAccountId: accountId };
|
|
2645
2649
|
case "allowlist": {
|
|
2646
2650
|
const inList = cfg.allowFrom.some((entry) => {
|
|
2647
2651
|
if (entry === "*") return true;
|
|
2648
2652
|
return phonesMatch(entry, senderPhone);
|
|
2649
2653
|
});
|
|
2650
2654
|
if (inList) {
|
|
2651
|
-
return { allowed: true, reason: "allowlist-match", agentType: "public" };
|
|
2655
|
+
return { allowed: true, reason: "allowlist-match", agentType: "public", effectiveAccountId: accountId };
|
|
2652
2656
|
}
|
|
2653
2657
|
return { allowed: false, reason: "not-in-allowlist", agentType: "public" };
|
|
2654
2658
|
}
|
|
@@ -4100,7 +4104,9 @@ function buildSelfChatDispatch(input) {
|
|
|
4100
4104
|
senderPhone,
|
|
4101
4105
|
isGroup: false
|
|
4102
4106
|
}),
|
|
4103
|
-
media: []
|
|
4107
|
+
media: [],
|
|
4108
|
+
// Self-chat is the owner: the session scopes to the house account (Task 1383).
|
|
4109
|
+
effectiveAccountId: input.accountId
|
|
4104
4110
|
};
|
|
4105
4111
|
}
|
|
4106
4112
|
async function init(opts) {
|
|
@@ -4748,12 +4754,24 @@ async function handleInboundMessage(conn, msg) {
|
|
|
4748
4754
|
config: whatsAppConfig,
|
|
4749
4755
|
accountConfig: whatsAppConfig.accounts?.[conn.accountId],
|
|
4750
4756
|
accountId: conn.accountId,
|
|
4757
|
+
isValidAccount: (id) => listValidAccounts().some((a) => a.accountId === id),
|
|
4751
4758
|
resolvePersonByPhone
|
|
4752
4759
|
});
|
|
4753
4760
|
}
|
|
4754
4761
|
console.error(
|
|
4755
4762
|
`${TAG13} inbound account=${conn.accountId} from=${senderPhone} group=${isGroup} access=${accessResult.allowed ? "allowed" : "blocked"}(${accessResult.reason}) agent=${accessResult.agentType}` + (extracted.mediaType ? ` media=${extracted.mediaType}` : "") + (mediaResult ? ` mediaPath=${mediaResult.path}` : "") + (extracted.quotedMessage ? ` replyTo=${extracted.quotedMessage.id}` : "")
|
|
4756
4763
|
);
|
|
4764
|
+
if (!accessResult.allowed && accessResult.reason === "account-manager-unresolved") {
|
|
4765
|
+
console.error(
|
|
4766
|
+
`[whatsapp-native] op=account-manager-reject senderId=${senderPhone} reason=unresolved-effective-account`
|
|
4767
|
+
);
|
|
4768
|
+
}
|
|
4769
|
+
if (accessResult.allowed && accessResult.reason === "account-manager" && accessResult.effectiveAccountId === conn.accountId) {
|
|
4770
|
+
console.error(
|
|
4771
|
+
`[whatsapp-native] op=escalation-tripwire senderId=${senderPhone} reason=account-manager-routed-to-house effectiveAccount=${accessResult.effectiveAccountId}`
|
|
4772
|
+
);
|
|
4773
|
+
return;
|
|
4774
|
+
}
|
|
4757
4775
|
if (!accessResult.allowed) return;
|
|
4758
4776
|
if (isCustomerTurnSuppressed(accessResult.agentType, isHouseManagedService())) {
|
|
4759
4777
|
console.error(
|
|
@@ -4801,7 +4819,10 @@ async function handleInboundMessage(conn, msg) {
|
|
|
4801
4819
|
composing,
|
|
4802
4820
|
cacheKey,
|
|
4803
4821
|
media: mediaResult?.path ? [{ path: mediaResult.path, type: extracted.mediaType, mimetype: mediaResult.mimetype }] : [],
|
|
4804
|
-
replyContext: extracted.quotedMessage
|
|
4822
|
+
replyContext: extracted.quotedMessage,
|
|
4823
|
+
// Task 1383 — the gate's single resolution feeds the spawn (house or the
|
|
4824
|
+
// bound sub-account); the router never re-resolves.
|
|
4825
|
+
effectiveAccountId: accessResult.effectiveAccountId
|
|
4805
4826
|
};
|
|
4806
4827
|
if (accessResult.agentType === "public") {
|
|
4807
4828
|
const hoursResult = await isBusinessOpen(conn.accountId);
|
|
@@ -6050,6 +6071,12 @@ function createScheduleInjectRoutes(deps) {
|
|
|
6050
6071
|
try {
|
|
6051
6072
|
await deps.waHandleInbound({
|
|
6052
6073
|
accountId: houseAccountId,
|
|
6074
|
+
// Task 1383 — the router (ensureChannelSession) no longer re-resolves
|
|
6075
|
+
// the accountManagers map; it consumes this value. The scheduler
|
|
6076
|
+
// supplies its own resolution (effectiveAccountFor, computed above) so
|
|
6077
|
+
// a manager destination scopes the spawned session to the sub-account,
|
|
6078
|
+
// exactly as a real inbound now does via the gate.
|
|
6079
|
+
effectiveAccountId: effectiveAccount2,
|
|
6053
6080
|
senderId: destination,
|
|
6054
6081
|
role: "admin",
|
|
6055
6082
|
personId: null,
|
|
@@ -6120,7 +6147,7 @@ import { randomUUID as randomUUID6 } from "crypto";
|
|
|
6120
6147
|
|
|
6121
6148
|
// app/lib/whatsapp/config-persist.ts
|
|
6122
6149
|
import { readFileSync as readFileSync10, writeFileSync as writeFileSync4, existsSync as existsSync5 } from "fs";
|
|
6123
|
-
import { resolve as resolve8, join as join8 } from "path";
|
|
6150
|
+
import { resolve as resolve8, join as join8, dirname as dirname2, basename as basename2 } from "path";
|
|
6124
6151
|
var TAG17 = "[whatsapp:config]";
|
|
6125
6152
|
function configPath(accountDir) {
|
|
6126
6153
|
return resolve8(accountDir, "account.json");
|
|
@@ -6195,6 +6222,16 @@ function addAdminPhone(accountDir, phone) {
|
|
|
6195
6222
|
}
|
|
6196
6223
|
try {
|
|
6197
6224
|
const config = readConfig(accountDir);
|
|
6225
|
+
const targetId = typeof config.accountId === "string" && config.accountId ? config.accountId : basename2(resolve8(accountDir));
|
|
6226
|
+
const socketOwner = resolveHouseOrSoleAccountId(dirname2(resolve8(accountDir)));
|
|
6227
|
+
if (targetId !== socketOwner) {
|
|
6228
|
+
console.error(`${TAG17} add-admin-phone rejected accountId=${targetId} reason=not-socket-account`);
|
|
6229
|
+
return {
|
|
6230
|
+
ok: false,
|
|
6231
|
+
reason: "not-socket-account",
|
|
6232
|
+
error: `${targetId} does not own the WhatsApp paired socket, so its admin phones are never consulted. To route a phone to a sub-account, bind it as an account manager (add-account-manager) instead.`
|
|
6233
|
+
};
|
|
6234
|
+
}
|
|
6198
6235
|
if (!config.whatsapp || typeof config.whatsapp !== "object") {
|
|
6199
6236
|
config.whatsapp = {};
|
|
6200
6237
|
}
|
|
@@ -6270,6 +6307,44 @@ function readAdminPhones(accountDir) {
|
|
|
6270
6307
|
return [];
|
|
6271
6308
|
}
|
|
6272
6309
|
}
|
|
6310
|
+
function purgeNonSocketAdminPhones(accountDir, socketOwnerId) {
|
|
6311
|
+
try {
|
|
6312
|
+
const config = readConfig(accountDir);
|
|
6313
|
+
const wa = config.whatsapp;
|
|
6314
|
+
if (!wa || typeof wa !== "object") return { removed: [] };
|
|
6315
|
+
const targetId = typeof config.accountId === "string" && config.accountId ? config.accountId : basename2(resolve8(accountDir));
|
|
6316
|
+
if (targetId === socketOwnerId) return { removed: [] };
|
|
6317
|
+
const adminPhones = Array.isArray(wa.adminPhones) ? wa.adminPhones.filter((p) => typeof p === "string") : [];
|
|
6318
|
+
if (adminPhones.length === 0) return { removed: [] };
|
|
6319
|
+
delete wa.adminPhones;
|
|
6320
|
+
writeConfig(accountDir, config);
|
|
6321
|
+
for (const phone of adminPhones) {
|
|
6322
|
+
console.error(`[admin-identity] op=purge-nonsocket-adminphone accountId=${targetId} phone=${phone} removed=true`);
|
|
6323
|
+
}
|
|
6324
|
+
return { removed: adminPhones };
|
|
6325
|
+
} catch (err) {
|
|
6326
|
+
console.error(`[admin-identity] purge-nonsocket-adminphone failed dir=${accountDir}: ${String(err)}`);
|
|
6327
|
+
return { removed: [] };
|
|
6328
|
+
}
|
|
6329
|
+
}
|
|
6330
|
+
function purgeNonSocketAdminPhonesAtBoot() {
|
|
6331
|
+
let socketOwner;
|
|
6332
|
+
try {
|
|
6333
|
+
socketOwner = resolveHouseOrSoleAccountId(ACCOUNTS_DIR);
|
|
6334
|
+
} catch (err) {
|
|
6335
|
+
console.error(`[admin-identity] purge-nonsocket-adminphone skipped \u2014 cannot resolve socket owner: ${String(err)}`);
|
|
6336
|
+
return;
|
|
6337
|
+
}
|
|
6338
|
+
let accounts;
|
|
6339
|
+
try {
|
|
6340
|
+
accounts = listValidAccounts();
|
|
6341
|
+
} catch {
|
|
6342
|
+
return;
|
|
6343
|
+
}
|
|
6344
|
+
for (const acct of accounts) {
|
|
6345
|
+
purgeNonSocketAdminPhones(acct.accountDir, socketOwner);
|
|
6346
|
+
}
|
|
6347
|
+
}
|
|
6273
6348
|
function setAccountManager(accountDir, phone, subAccountId) {
|
|
6274
6349
|
const normalized = phone.trim();
|
|
6275
6350
|
if (!E164_PATTERN.test(normalized)) {
|
|
@@ -6798,7 +6873,7 @@ function listActiveLoginAccountIds() {
|
|
|
6798
6873
|
// app/lib/whatsapp/outbound/send-document.ts
|
|
6799
6874
|
import { realpathSync as realpathSync3 } from "fs";
|
|
6800
6875
|
import { readFile, stat as stat2 } from "fs/promises";
|
|
6801
|
-
import { resolve as resolve9, basename as
|
|
6876
|
+
import { resolve as resolve9, basename as basename3 } from "path";
|
|
6802
6877
|
var TAG19 = "[whatsapp:outbound]";
|
|
6803
6878
|
var WHATSAPP_DOCUMENT_MAX_BYTES = 100 * 1024 * 1024;
|
|
6804
6879
|
var lastDocumentOutboundAt = /* @__PURE__ */ new Map();
|
|
@@ -6853,7 +6928,7 @@ async function sendWhatsAppDocument(input) {
|
|
|
6853
6928
|
error: `File exceeds 100 MB limit (${(fileStat.size / 1024 / 1024).toFixed(1)} MB)`
|
|
6854
6929
|
};
|
|
6855
6930
|
}
|
|
6856
|
-
const filename =
|
|
6931
|
+
const filename = basename3(resolvedPath);
|
|
6857
6932
|
const jid = normalizeJid2(to);
|
|
6858
6933
|
const sock = getSocket(accountId);
|
|
6859
6934
|
if (!sock) {
|
|
@@ -7142,7 +7217,8 @@ app2.post("/config", async (c) => {
|
|
|
7142
7217
|
return c.json({ ok: false, error: 'Missing required field "phone" (E.164 format, e.g. +441234567890).' }, 400);
|
|
7143
7218
|
}
|
|
7144
7219
|
const result = addAdminPhone(account.accountDir, phone);
|
|
7145
|
-
|
|
7220
|
+
const reasonTail = !result.ok && result.reason ? ` reason=${result.reason}` : "";
|
|
7221
|
+
console.error(`${TAG21} config action=add-admin-phone accountId=${account.accountId} phone=${phone} ok=${result.ok}${reasonTail}`);
|
|
7146
7222
|
return c.json(result, result.ok ? 200 : 400);
|
|
7147
7223
|
}
|
|
7148
7224
|
case "remove-admin-phone": {
|
|
@@ -7501,11 +7577,11 @@ var whatsapp_default = app2;
|
|
|
7501
7577
|
|
|
7502
7578
|
// server/routes/whatsapp-reader.ts
|
|
7503
7579
|
import { readFileSync as readFileSync15, watch, statSync as statSync5, openSync, readSync, closeSync, existsSync as existsSync8, readdirSync as readdirSync8, realpathSync as realpathSync4 } from "fs";
|
|
7504
|
-
import { basename as
|
|
7580
|
+
import { basename as basename5, dirname as dirname4, isAbsolute, join as join14, relative as relative2, resolve as resolve12, sep as sep3 } from "path";
|
|
7505
7581
|
|
|
7506
7582
|
// server/routes/admin/sidebar-sessions.ts
|
|
7507
7583
|
import { readdirSync as readdirSync5, readFileSync as readFileSync12, statSync as statSync3 } from "fs";
|
|
7508
|
-
import { basename as
|
|
7584
|
+
import { basename as basename4, dirname as dirname3, join as join11, resolve as resolve11 } from "path";
|
|
7509
7585
|
|
|
7510
7586
|
// app/lib/whatsapp-reader/select-sessions.ts
|
|
7511
7587
|
function isReaderChannelSession(role, channel) {
|
|
@@ -7586,7 +7662,7 @@ function findSessionProjectDir(sessionId) {
|
|
|
7586
7662
|
const cfg = claudeConfigDir();
|
|
7587
7663
|
if (!cfg) return null;
|
|
7588
7664
|
for (const { path: path2 } of enumerateJsonls(join11(cfg, "projects"))) {
|
|
7589
|
-
if (
|
|
7665
|
+
if (basename4(path2) === `${sessionId}.jsonl`) return dirname3(path2);
|
|
7590
7666
|
}
|
|
7591
7667
|
return null;
|
|
7592
7668
|
}
|
|
@@ -8315,8 +8391,8 @@ app4.get("/conversations", requireAdminSession, async (c) => {
|
|
|
8315
8391
|
const rows = [];
|
|
8316
8392
|
let sessionRowsExcludedUntagged = 0;
|
|
8317
8393
|
for (const { path: path2 } of enumerateJsonls(projectsRoot)) {
|
|
8318
|
-
const sessionId =
|
|
8319
|
-
const projectDir =
|
|
8394
|
+
const sessionId = basename5(path2).replace(/\.jsonl$/, "");
|
|
8395
|
+
const projectDir = dirname4(path2);
|
|
8320
8396
|
const meta = readSidecarMeta(join14(projectDir, `${sessionId}.meta.json`));
|
|
8321
8397
|
if (!isReaderChannelSession(meta.role, meta.channel)) continue;
|
|
8322
8398
|
if (multiAccount) {
|
|
@@ -8846,7 +8922,7 @@ var whatsapp_reader_default = app4;
|
|
|
8846
8922
|
|
|
8847
8923
|
// server/routes/public-reader.ts
|
|
8848
8924
|
import { statSync as statSync6, openSync as openSync2, readSync as readSync2, closeSync as closeSync2, watch as watch2, readFileSync as readFileSync16, readdirSync as readdirSync9 } from "fs";
|
|
8849
|
-
import { basename as
|
|
8925
|
+
import { basename as basename6, dirname as dirname5, join as join15, resolve as resolve13, sep as sep4 } from "path";
|
|
8850
8926
|
|
|
8851
8927
|
// app/lib/whatsapp-reader/delivered-kinds.ts
|
|
8852
8928
|
var PUBLIC_DELIVERED_KINDS = /* @__PURE__ */ new Set([
|
|
@@ -8921,8 +8997,8 @@ function enumeratePublicRows() {
|
|
|
8921
8997
|
if (!cfg) return [];
|
|
8922
8998
|
const rows = [];
|
|
8923
8999
|
for (const { path: path2 } of enumerateJsonls(join15(cfg, "projects"))) {
|
|
8924
|
-
const sessionId =
|
|
8925
|
-
const projectDir =
|
|
9000
|
+
const sessionId = basename6(path2).replace(/\.jsonl$/, "");
|
|
9001
|
+
const projectDir = dirname5(path2);
|
|
8926
9002
|
const meta = readSidecarMeta(join15(projectDir, `${sessionId}.meta.json`));
|
|
8927
9003
|
rows.push({
|
|
8928
9004
|
sessionId,
|
|
@@ -9140,7 +9216,7 @@ app5.get("/attachment/:attachmentId", (c) => {
|
|
|
9140
9216
|
var public_reader_default = app5;
|
|
9141
9217
|
|
|
9142
9218
|
// server/routes/webchat.ts
|
|
9143
|
-
import { basename as
|
|
9219
|
+
import { basename as basename7, dirname as dirname6 } from "path";
|
|
9144
9220
|
import { join as join18 } from "path";
|
|
9145
9221
|
import { existsSync as existsSync10, readdirSync as readdirSync11, readFileSync as readFileSync19, renameSync as renameSync3, statSync as statSync7, writeFileSync as writeFileSync7 } from "fs";
|
|
9146
9222
|
|
|
@@ -9538,8 +9614,8 @@ function latestAdminWebchatSessionId(requesterUserId, primaryUserId, scopeAccoun
|
|
|
9538
9614
|
let best = null;
|
|
9539
9615
|
for (const { path: path2, isSubagent, archived } of enumerateJsonls(join18(cfg, "projects"))) {
|
|
9540
9616
|
if (isSubagent || archived) continue;
|
|
9541
|
-
const id =
|
|
9542
|
-
const meta = readSidecarMeta(join18(
|
|
9617
|
+
const id = basename7(path2).slice(0, -".jsonl".length);
|
|
9618
|
+
const meta = readSidecarMeta(join18(dirname6(path2), `${id}.meta.json`));
|
|
9543
9619
|
if (meta.role !== "admin" || meta.channel !== "webchat") continue;
|
|
9544
9620
|
if (multiAccount && meta.accountId !== scopeAccountId) continue;
|
|
9545
9621
|
let mtimeMs;
|
|
@@ -9562,7 +9638,7 @@ function latestAdminWebchatSessionId(requesterUserId, primaryUserId, scopeAccoun
|
|
|
9562
9638
|
function overrideKnownNonArchived(id) {
|
|
9563
9639
|
const { projectDir } = locateSession(id);
|
|
9564
9640
|
if (projectDir === null) return sessionSidecarExists(id);
|
|
9565
|
-
return
|
|
9641
|
+
return basename7(projectDir) !== "archive";
|
|
9566
9642
|
}
|
|
9567
9643
|
function resolveCanonical(accountId, accountDir, requesterUserId, primaryUserId, scopeAccountId, multiAccount) {
|
|
9568
9644
|
const bootstrapAccountId = scopeAccountId ?? accountId;
|
|
@@ -9930,8 +10006,8 @@ ${note}` : note;
|
|
|
9930
10006
|
let projectDir = null;
|
|
9931
10007
|
if (cfg) {
|
|
9932
10008
|
for (const { path: path2 } of enumerateJsonls(join18(cfg, "projects"))) {
|
|
9933
|
-
if (
|
|
9934
|
-
projectDir =
|
|
10009
|
+
if (basename7(path2) === `${sessionId}.jsonl`) {
|
|
10010
|
+
projectDir = dirname6(path2);
|
|
9935
10011
|
break;
|
|
9936
10012
|
}
|
|
9937
10013
|
}
|
|
@@ -10369,7 +10445,7 @@ import { createHash as createHash3, randomUUID as randomUUID7 } from "crypto";
|
|
|
10369
10445
|
|
|
10370
10446
|
// ../lib/admins-write/src/index.ts
|
|
10371
10447
|
import { existsSync as existsSync14, readFileSync as readFileSync23, writeFileSync as writeFileSync9, renameSync as renameSync5, mkdirSync as mkdirSync5, readdirSync as readdirSync13, statSync as statSync8, appendFileSync as appendFileSync2 } from "fs";
|
|
10372
|
-
import { dirname as
|
|
10448
|
+
import { dirname as dirname7, join as join22 } from "path";
|
|
10373
10449
|
function id8(value) {
|
|
10374
10450
|
return value.slice(0, 8);
|
|
10375
10451
|
}
|
|
@@ -10382,7 +10458,7 @@ function appendUsersAuditLine(audit, fields) {
|
|
|
10382
10458
|
const line = `[users-audit] action=${fields.action} actor=${actor}${sess} field=${fields.field} rowsBefore=${fields.rowsBefore} rowsAfter=${fields.rowsAfter} ts=${(/* @__PURE__ */ new Date()).toISOString()}
|
|
10383
10459
|
`;
|
|
10384
10460
|
try {
|
|
10385
|
-
mkdirSync5(
|
|
10461
|
+
mkdirSync5(dirname7(audit.logFile), { recursive: true, mode: 448 });
|
|
10386
10462
|
appendFileSync2(audit.logFile, line, { mode: 384 });
|
|
10387
10463
|
} catch (err) {
|
|
10388
10464
|
console.error(
|
|
@@ -10400,7 +10476,7 @@ function logLine(input, result) {
|
|
|
10400
10476
|
);
|
|
10401
10477
|
}
|
|
10402
10478
|
function writeFileAtomic(filePath, contents, mode) {
|
|
10403
|
-
mkdirSync5(
|
|
10479
|
+
mkdirSync5(dirname7(filePath), { recursive: true });
|
|
10404
10480
|
const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
10405
10481
|
writeFileSync9(tempPath, contents, mode !== void 0 ? { mode } : void 0);
|
|
10406
10482
|
renameSync5(tempPath, filePath);
|
|
@@ -11249,7 +11325,7 @@ var accounts_default = app12;
|
|
|
11249
11325
|
|
|
11250
11326
|
// server/routes/admin/logs.ts
|
|
11251
11327
|
import { existsSync as existsSync18, readdirSync as readdirSync14, readFileSync as readFileSync25, statSync as statSync10 } from "fs";
|
|
11252
|
-
import { resolve as resolve16, basename as
|
|
11328
|
+
import { resolve as resolve16, basename as basename8 } from "path";
|
|
11253
11329
|
|
|
11254
11330
|
// app/lib/logs-read-resolve.ts
|
|
11255
11331
|
import { existsSync as existsSync17 } from "fs";
|
|
@@ -11281,7 +11357,7 @@ app13.get("/", async (c) => {
|
|
|
11281
11357
|
if (accountLogDir) logDirs.push(accountLogDir);
|
|
11282
11358
|
logDirs.push(LOG_DIR);
|
|
11283
11359
|
if (fileParam) {
|
|
11284
|
-
const safe =
|
|
11360
|
+
const safe = basename8(fileParam);
|
|
11285
11361
|
const searched = [];
|
|
11286
11362
|
for (const dir of logDirs) {
|
|
11287
11363
|
const filePath = resolve16(dir, safe);
|
|
@@ -11351,7 +11427,7 @@ app13.get("/", async (c) => {
|
|
|
11351
11427
|
if (hit) {
|
|
11352
11428
|
console.info(`[admin/logs] resolved cacheKey=${cacheKeySlice} sessionId=${sessionIdSlice} via=${primaryId === cacheKey ? "cacheKey" : primaryId === sessionKeyFromConv ? "reverse-lookup" : "sessionId-fallback"}`);
|
|
11353
11429
|
try {
|
|
11354
|
-
const filename =
|
|
11430
|
+
const filename = basename8(hit.path);
|
|
11355
11431
|
const buffer = readFileSync25(hit.path);
|
|
11356
11432
|
const onDiskBytes = statSync10(hit.path).size;
|
|
11357
11433
|
const headers = {
|
|
@@ -12285,7 +12361,7 @@ var sessions_default = app17;
|
|
|
12285
12361
|
|
|
12286
12362
|
// app/lib/claude-agent/spawn-context.ts
|
|
12287
12363
|
import { existsSync as existsSync22, readFileSync as readFileSync27, readdirSync as readdirSync16, statSync as statSync11 } from "fs";
|
|
12288
|
-
import { dirname as
|
|
12364
|
+
import { dirname as dirname8, resolve as resolve19, join as join25 } from "path";
|
|
12289
12365
|
async function resolveOwnerProfileBlock(accountId, userId) {
|
|
12290
12366
|
if (!userId) return { ok: false, reason: "missing-user-id" };
|
|
12291
12367
|
try {
|
|
@@ -12392,7 +12468,7 @@ function listPluginDirs() {
|
|
|
12392
12468
|
}
|
|
12393
12469
|
}
|
|
12394
12470
|
}
|
|
12395
|
-
const premiumRoot = resolve19(
|
|
12471
|
+
const premiumRoot = resolve19(dirname8(PLATFORM_ROOT), "premium-plugins");
|
|
12396
12472
|
if (existsSync22(premiumRoot)) {
|
|
12397
12473
|
for (const bundle of readdirSync16(premiumRoot)) {
|
|
12398
12474
|
const bundlePlugins = join25(premiumRoot, bundle, "plugins");
|
|
@@ -12841,7 +12917,7 @@ import { createReadStream as createReadStream2, createWriteStream as createWrite
|
|
|
12841
12917
|
import { readdir as readdir3, readFile as readFile4, stat as stat4, mkdir as mkdir3, unlink as unlink2, rename } from "fs/promises";
|
|
12842
12918
|
import { realpathSync as realpathSync5 } from "fs";
|
|
12843
12919
|
import { randomUUID as randomUUID9 } from "crypto";
|
|
12844
|
-
import { basename as
|
|
12920
|
+
import { basename as basename10, dirname as dirname9, join as join27, relative as relative4, resolve as resolve22, sep as sep6 } from "path";
|
|
12845
12921
|
import { Readable as Readable2 } from "stream";
|
|
12846
12922
|
|
|
12847
12923
|
// ../lib/graph-trash/src/index.ts
|
|
@@ -13159,7 +13235,7 @@ async function cascadeDeleteDocument(params) {
|
|
|
13159
13235
|
|
|
13160
13236
|
// app/lib/file-index.ts
|
|
13161
13237
|
import * as fsp from "fs/promises";
|
|
13162
|
-
import { resolve as resolve21, relative as relative3, join as join26, basename as
|
|
13238
|
+
import { resolve as resolve21, relative as relative3, join as join26, basename as basename9, extname as extname2, sep as sep5 } from "path";
|
|
13163
13239
|
import { tmpdir as tmpdir2 } from "os";
|
|
13164
13240
|
import { execFile as execFile2 } from "child_process";
|
|
13165
13241
|
import { promisify as promisify2 } from "util";
|
|
@@ -13297,8 +13373,8 @@ async function extractFileContent(absolute) {
|
|
|
13297
13373
|
}
|
|
13298
13374
|
}
|
|
13299
13375
|
async function readDisplayName(absolute) {
|
|
13300
|
-
const dir = absolute.slice(0, absolute.length -
|
|
13301
|
-
const base =
|
|
13376
|
+
const dir = absolute.slice(0, absolute.length - basename9(absolute).length);
|
|
13377
|
+
const base = basename9(absolute);
|
|
13302
13378
|
const dot = base.lastIndexOf(".");
|
|
13303
13379
|
const stem = dot === -1 ? base : base.slice(0, dot);
|
|
13304
13380
|
if (base === `${stem}.meta.json`) return null;
|
|
@@ -13436,7 +13512,7 @@ async function cascadeTrashLinkedKnowledgeDocs(session, accountId, relativePaths
|
|
|
13436
13512
|
return { kds, sections, chunks };
|
|
13437
13513
|
}
|
|
13438
13514
|
async function buildArtifact(accountId, wf, embed2) {
|
|
13439
|
-
const name =
|
|
13515
|
+
const name = basename9(wf.absolute);
|
|
13440
13516
|
const { content, route } = await extractFileContent(wf.absolute);
|
|
13441
13517
|
const displayName = await readDisplayName(wf.absolute);
|
|
13442
13518
|
const embedInput = `${name}
|
|
@@ -13966,7 +14042,7 @@ app21.get("/download", requireAdminSession, async (c) => {
|
|
|
13966
14042
|
if (!info.isFile()) {
|
|
13967
14043
|
return c.json({ error: "Path is not a file" }, 400);
|
|
13968
14044
|
}
|
|
13969
|
-
const filename =
|
|
14045
|
+
const filename = basename10(absolute);
|
|
13970
14046
|
const mimeType = detectMimeType(absolute);
|
|
13971
14047
|
const nodeStream = createReadStream2(absolute);
|
|
13972
14048
|
const webStream = Readable2.toWeb(nodeStream);
|
|
@@ -14071,7 +14147,7 @@ app21.get("/download-zip", requireAdminSession, async (c) => {
|
|
|
14071
14147
|
const info = await stat4(absolute);
|
|
14072
14148
|
if (info.isDirectory()) {
|
|
14073
14149
|
dirsWalked++;
|
|
14074
|
-
const parentAbs =
|
|
14150
|
+
const parentAbs = dirname9(absolute);
|
|
14075
14151
|
for await (const fileAbs of walkRegularFiles(absolute)) {
|
|
14076
14152
|
const within = relative4(absolute, fileAbs).split(sep6).join("/");
|
|
14077
14153
|
const fileRel = relPath === "" || relPath === "." ? within : `${relPath}/${within}`;
|
|
@@ -14082,7 +14158,7 @@ app21.get("/download-zip", requireAdminSession, async (c) => {
|
|
|
14082
14158
|
if (over) return over;
|
|
14083
14159
|
}
|
|
14084
14160
|
} else if (info.isFile()) {
|
|
14085
|
-
const over = await addFile(absolute, info.size,
|
|
14161
|
+
const over = await addFile(absolute, info.size, basename10(absolute));
|
|
14086
14162
|
if (over) return over;
|
|
14087
14163
|
} else {
|
|
14088
14164
|
return c.json({ error: "Path is not a file or directory" }, 400);
|
|
@@ -14111,12 +14187,12 @@ function dataUploadCeiling() {
|
|
|
14111
14187
|
}
|
|
14112
14188
|
async function resolveUploadTarget(c, accountId, uid) {
|
|
14113
14189
|
const rawName = c.req.query("filename") ?? "";
|
|
14114
|
-
const safeName =
|
|
14190
|
+
const safeName = basename10(rawName).replace(/[\0/\\]/g, "_");
|
|
14115
14191
|
if (!safeName) return { ok: false, status: 400, error: "filename query param required" };
|
|
14116
14192
|
const rawRelpath = c.req.query("relpath") ?? "";
|
|
14117
14193
|
const relpath = rawRelpath !== "" ? rawRelpath : null;
|
|
14118
14194
|
const token = c.req.query("token") ?? "";
|
|
14119
|
-
const finalName = relpath ?
|
|
14195
|
+
const finalName = relpath ? basename10(relpath).replace(/[\0/\\]/g, "_") : `${Date.now()}-${safeName}`;
|
|
14120
14196
|
const mimeType = (c.req.header("content-type") ?? "").split(";")[0].trim();
|
|
14121
14197
|
if (!SUPPORTED_MIME_TYPES.has(mimeType)) {
|
|
14122
14198
|
console.error(`[data-upload] op=reject-mime uid=${uid} mime="${mimeType}" token=${token} rel="${relpath ?? ""}"`);
|
|
@@ -14125,7 +14201,7 @@ async function resolveUploadTarget(c, accountId, uid) {
|
|
|
14125
14201
|
const rawDir = c.req.query("path") ?? "";
|
|
14126
14202
|
const base = resolveOwnAccountWrite(rawDir, accountId, "POST /api/admin/files/upload");
|
|
14127
14203
|
if (!base.ok) return { ok: false, status: base.status, error: base.error };
|
|
14128
|
-
const relDir = relpath ?
|
|
14204
|
+
const relDir = relpath ? dirname9(relpath) : ".";
|
|
14129
14205
|
const destRel = relDir === "." ? base.relative : join27(base.relative, relDir);
|
|
14130
14206
|
if (crossesForeignAccountPartition(destRel, accountId)) {
|
|
14131
14207
|
console.error(`[data] account-scope-blocked endpoint="POST /api/admin/files/upload" path="${destRel}" account=${accountId.slice(0, 8)}\u2026`);
|
|
@@ -14486,7 +14562,7 @@ app21.delete("/", requireAdminSession, async (c) => {
|
|
|
14486
14562
|
console.error(`[data] account-scope-blocked endpoint="DELETE /api/admin/files" path="${relPath}" account=${accountId.slice(0, 8)}\u2026`);
|
|
14487
14563
|
return c.json({ error: "Not found" }, 404);
|
|
14488
14564
|
}
|
|
14489
|
-
const base =
|
|
14565
|
+
const base = basename10(absolute);
|
|
14490
14566
|
const protection = isProtectedFromDeletion(relPath);
|
|
14491
14567
|
if (protection.protected) {
|
|
14492
14568
|
console.error(`[data] file-delete blocked path="${relPath}" reason="protected" rule="${protection.rule}"`);
|
|
@@ -14502,7 +14578,7 @@ app21.delete("/", requireAdminSession, async (c) => {
|
|
|
14502
14578
|
}
|
|
14503
14579
|
const dot = base.lastIndexOf(".");
|
|
14504
14580
|
const stem = dot === -1 ? base : base.slice(0, dot);
|
|
14505
|
-
const sidecarPath = UUID_RE3.test(stem) && base !== `${stem}.meta.json` ? join27(
|
|
14581
|
+
const sidecarPath = UUID_RE3.test(stem) && base !== `${stem}.meta.json` ? join27(dirname9(absolute), `${stem}.meta.json`) : null;
|
|
14506
14582
|
await unlink2(absolute);
|
|
14507
14583
|
if (sidecarPath) {
|
|
14508
14584
|
try {
|
|
@@ -14598,8 +14674,8 @@ app21.post("/rename", requireAdminSession, async (c) => {
|
|
|
14598
14674
|
console.error(`[data] file-rename blocked path="${src.relative}" reason="protected"`);
|
|
14599
14675
|
return c.json({ error: "Protected entry \u2014 refusing to rename" }, 403);
|
|
14600
14676
|
}
|
|
14601
|
-
const destAbs = resolve22(
|
|
14602
|
-
const newRel = `${
|
|
14677
|
+
const destAbs = resolve22(dirname9(src.absolute), newName);
|
|
14678
|
+
const newRel = `${dirname9(src.relative)}/${newName}`.replace(/^\.\//, "");
|
|
14603
14679
|
if (isProtectedFromRename(newRel)) {
|
|
14604
14680
|
console.error(`[data] file-rename blocked path="${newRel}" reason="protected-target"`);
|
|
14605
14681
|
return c.json({ error: "That name is reserved" }, 403);
|
|
@@ -16730,7 +16806,7 @@ var graph_default_view_default = app27;
|
|
|
16730
16806
|
|
|
16731
16807
|
// server/routes/admin/sidebar-artefacts.ts
|
|
16732
16808
|
import { readdir as readdir4, stat as stat5 } from "fs/promises";
|
|
16733
|
-
import { resolve as resolve23, relative as relative5, isAbsolute as isAbsolute2, sep as sep7, basename as
|
|
16809
|
+
import { resolve as resolve23, relative as relative5, isAbsolute as isAbsolute2, sep as sep7, basename as basename11 } from "path";
|
|
16734
16810
|
import { existsSync as existsSync25 } from "fs";
|
|
16735
16811
|
var LIMIT = 50;
|
|
16736
16812
|
var ADMIN_AGENT_FILES = ["IDENTITY.md", "SOUL.md", "KNOWLEDGE.md"];
|
|
@@ -16783,7 +16859,7 @@ async function fetchAccountFileArtefacts(accountId) {
|
|
|
16783
16859
|
const modifiedAt = r.get("modifiedAt") ?? "";
|
|
16784
16860
|
return {
|
|
16785
16861
|
id: `account-file:${relativePath}`,
|
|
16786
|
-
name: displayName ??
|
|
16862
|
+
name: displayName ?? basename11(relativePath),
|
|
16787
16863
|
kind: "account-file",
|
|
16788
16864
|
updatedAt: modifiedAt,
|
|
16789
16865
|
mimeType,
|
|
@@ -19125,7 +19201,7 @@ var sites_default = app49;
|
|
|
19125
19201
|
// app/lib/visitor-token.ts
|
|
19126
19202
|
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
19127
19203
|
import { mkdirSync as mkdirSync7, readFileSync as readFileSync32, writeFileSync as writeFileSync11 } from "fs";
|
|
19128
|
-
import { dirname as
|
|
19204
|
+
import { dirname as dirname10 } from "path";
|
|
19129
19205
|
var TOKEN_PREFIX = "v1.";
|
|
19130
19206
|
var SECRET_BYTES = 32;
|
|
19131
19207
|
var DEFAULT_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
@@ -19142,7 +19218,7 @@ function getSecret() {
|
|
|
19142
19218
|
}
|
|
19143
19219
|
const fresh = randomBytes(SECRET_BYTES).toString("hex");
|
|
19144
19220
|
try {
|
|
19145
|
-
mkdirSync7(
|
|
19221
|
+
mkdirSync7(dirname10(VISITOR_TOKEN_SECRET_FILE), { recursive: true, mode: 448 });
|
|
19146
19222
|
writeFileSync11(VISITOR_TOKEN_SECRET_FILE, fresh, { mode: 384, flag: "wx" });
|
|
19147
19223
|
console.log(`[visitor-token] secret minted path=${VISITOR_TOKEN_SECRET_FILE}`);
|
|
19148
19224
|
} catch {
|
|
@@ -20250,7 +20326,7 @@ async function startFileWatcher(opts = {}) {
|
|
|
20250
20326
|
|
|
20251
20327
|
// app/lib/migrate-uploads.ts
|
|
20252
20328
|
import { mkdir as mkdir5, readdir as readdir5, rename as rename2, rm as rm3 } from "fs/promises";
|
|
20253
|
-
import { dirname as
|
|
20329
|
+
import { dirname as dirname11, relative as relative6, resolve as resolve29 } from "path";
|
|
20254
20330
|
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;
|
|
20255
20331
|
async function walkFiles(dir) {
|
|
20256
20332
|
const out = [];
|
|
@@ -20282,7 +20358,7 @@ async function relocateTree(srcDir, destDir, dataRoot, accountId, session) {
|
|
|
20282
20358
|
for (const oldAbs of files) {
|
|
20283
20359
|
const suffix = relative6(srcDir, oldAbs);
|
|
20284
20360
|
const newAbs = resolve29(destDir, suffix);
|
|
20285
|
-
await mkdir5(
|
|
20361
|
+
await mkdir5(dirname11(newAbs), { recursive: true });
|
|
20286
20362
|
await rename2(oldAbs, newAbs);
|
|
20287
20363
|
moved++;
|
|
20288
20364
|
const oldRel = relative6(dataRoot, oldAbs);
|
|
@@ -21011,6 +21087,7 @@ var WaGateway = class {
|
|
|
21011
21087
|
try {
|
|
21012
21088
|
await this.deps.ensureChannelSession({
|
|
21013
21089
|
accountId: input.accountId,
|
|
21090
|
+
effectiveAccountId: input.effectiveAccountId,
|
|
21014
21091
|
senderId: input.senderId,
|
|
21015
21092
|
role: input.role ?? "admin",
|
|
21016
21093
|
personId: input.personId ?? null,
|
|
@@ -22375,7 +22452,7 @@ function buildTelegramSpawnRequest(input) {
|
|
|
22375
22452
|
// app/lib/telegram/outbound/send-document.ts
|
|
22376
22453
|
import { realpathSync as realpathSync7 } from "fs";
|
|
22377
22454
|
import { readFile as readFile7, stat as stat7 } from "fs/promises";
|
|
22378
|
-
import { resolve as resolve30, basename as
|
|
22455
|
+
import { resolve as resolve30, basename as basename12 } from "path";
|
|
22379
22456
|
var TAG36 = "[telegram:outbound]";
|
|
22380
22457
|
var TELEGRAM_DOCUMENT_MAX_BYTES = 50 * 1024 * 1024;
|
|
22381
22458
|
async function sendTelegramDocument(input) {
|
|
@@ -22412,7 +22489,7 @@ async function sendTelegramDocument(input) {
|
|
|
22412
22489
|
error: `File exceeds 50 MB limit (${(fileStat.size / 1024 / 1024).toFixed(1)} MB)`
|
|
22413
22490
|
};
|
|
22414
22491
|
}
|
|
22415
|
-
const filename =
|
|
22492
|
+
const filename = basename12(resolvedPath);
|
|
22416
22493
|
const buffer = Buffer.from(await readFile7(resolvedPath));
|
|
22417
22494
|
const form = new FormData();
|
|
22418
22495
|
form.append("chat_id", String(chatId));
|
|
@@ -22705,6 +22782,10 @@ function findChannelAdminBindingDrift(adminPhones, admins, users) {
|
|
|
22705
22782
|
}
|
|
22706
22783
|
return drift;
|
|
22707
22784
|
}
|
|
22785
|
+
function classifyAdminPhonesForAccount(isSocketOwner, adminPhones, admins, users) {
|
|
22786
|
+
if (!isSocketOwner) return { nonSocketPhones: [...adminPhones], drift: [] };
|
|
22787
|
+
return { nonSocketPhones: [], drift: findChannelAdminBindingDrift(adminPhones, admins, users) };
|
|
22788
|
+
}
|
|
22708
22789
|
function warnOnChannelAdminBindingDrift() {
|
|
22709
22790
|
let users;
|
|
22710
22791
|
try {
|
|
@@ -22718,10 +22799,27 @@ function warnOnChannelAdminBindingDrift() {
|
|
|
22718
22799
|
} catch {
|
|
22719
22800
|
return;
|
|
22720
22801
|
}
|
|
22802
|
+
let socketOwner;
|
|
22803
|
+
try {
|
|
22804
|
+
socketOwner = resolveHouseOrSoleAccountId(ACCOUNTS_DIR);
|
|
22805
|
+
} catch {
|
|
22806
|
+
socketOwner = null;
|
|
22807
|
+
}
|
|
22721
22808
|
const validAccountIds = accounts.map((a) => a.accountId);
|
|
22722
22809
|
for (const acct of accounts) {
|
|
22723
22810
|
const adminPhones = readAdminPhones(acct.accountDir);
|
|
22724
|
-
const
|
|
22811
|
+
const isSocketOwner = socketOwner === null || acct.accountId === socketOwner;
|
|
22812
|
+
const { nonSocketPhones, drift } = classifyAdminPhonesForAccount(
|
|
22813
|
+
isSocketOwner,
|
|
22814
|
+
adminPhones,
|
|
22815
|
+
acct.config.admins ?? [],
|
|
22816
|
+
users
|
|
22817
|
+
);
|
|
22818
|
+
for (const phone of nonSocketPhones) {
|
|
22819
|
+
console.error(
|
|
22820
|
+
`[admin-identity] adminphones-on-non-socket-account accountId=${acct.accountId} phone=${phone} count=${nonSocketPhones.length}`
|
|
22821
|
+
);
|
|
22822
|
+
}
|
|
22725
22823
|
for (const d of drift) {
|
|
22726
22824
|
const tail = d.reason === "userid-not-admin" ? ` userId=${d.userId}` : "";
|
|
22727
22825
|
console.error(
|
|
@@ -23097,12 +23195,10 @@ var waGateway = new WaGateway({
|
|
|
23097
23195
|
});
|
|
23098
23196
|
return result.ok ? { ok: true, messageId: result.messageId } : { ok: false, error: result.error };
|
|
23099
23197
|
},
|
|
23100
|
-
ensureChannelSession: async ({ accountId, senderId, role, personId, gatewayUrl, serverPath }) => {
|
|
23101
|
-
const houseDir = role === "admin" ? listValidAccounts().find((a) => a.accountId === accountId)?.accountDir ?? null : null;
|
|
23102
|
-
const managed = houseDir ? managedAccountFor(readAccountManagers(houseDir), senderId) : null;
|
|
23103
|
-
const effectiveAccountId = managed ?? accountId;
|
|
23198
|
+
ensureChannelSession: async ({ accountId, effectiveAccountId, senderId, role, personId, gatewayUrl, serverPath }) => {
|
|
23104
23199
|
if (role === "admin") {
|
|
23105
|
-
|
|
23200
|
+
const managesAccount = effectiveAccountId !== accountId ? effectiveAccountId : "none";
|
|
23201
|
+
console.error(`[whatsapp-native] op=account-manager-route senderId=${senderId} managesAccount=${managesAccount} effectiveAccount=${effectiveAccountId} source=gate`);
|
|
23106
23202
|
}
|
|
23107
23203
|
const req = buildWaSpawnRequest({ accountId: effectiveAccountId, senderId, role, personId, gatewayUrl, serverPath });
|
|
23108
23204
|
const userId = role === "admin" ? resolveAdminUserId({ accountId: effectiveAccountId, senderPhone: senderId }) ?? void 0 : void 0;
|
|
@@ -24243,7 +24339,7 @@ try {
|
|
|
24243
24339
|
} catch (err) {
|
|
24244
24340
|
console.error(`[graph-health] account-enumeration unavailable reason=${err instanceof Error ? err.message : String(err)}`);
|
|
24245
24341
|
}
|
|
24246
|
-
var configDirForWhatsApp =
|
|
24342
|
+
var configDirForWhatsApp = basename13(MAXY_DIR) || ".maxy";
|
|
24247
24343
|
var bootAccount = resolveAccount();
|
|
24248
24344
|
if (bootAccount) {
|
|
24249
24345
|
migrateRemovedConfigKeys(bootAccount.accountDir);
|
|
@@ -24263,6 +24359,7 @@ reconcileEnabledPlugins(bootAccount?.accountDir, bootAccount?.config, bootAccoun
|
|
|
24263
24359
|
for (const acct of listValidAccounts()) {
|
|
24264
24360
|
cleanupLeakedPremiumSubs(acct.accountDir, acct.config, acct.accountId);
|
|
24265
24361
|
}
|
|
24362
|
+
purgeNonSocketAdminPhonesAtBoot();
|
|
24266
24363
|
warnOnChannelAdminBindingDrift();
|
|
24267
24364
|
var bootEnabled = Array.isArray(bootAccountConfig?.enabledPlugins) ? bootAccountConfig.enabledPlugins : [];
|
|
24268
24365
|
var bootDelivered = [];
|
|
@@ -24322,6 +24419,10 @@ init({
|
|
|
24322
24419
|
console.error(`[whatsapp:route] dropped reason=no-text-no-media senderId=${msg.senderPhone} mediaCount=${msg.media.length}`);
|
|
24323
24420
|
return;
|
|
24324
24421
|
}
|
|
24422
|
+
if (!msg.effectiveAccountId) {
|
|
24423
|
+
console.error(`[whatsapp:route] op=dropped reason=no-effective-account senderId=${msg.senderPhone}`);
|
|
24424
|
+
return;
|
|
24425
|
+
}
|
|
24325
24426
|
if (decision.role === "public") {
|
|
24326
24427
|
console.error(`[whatsapp:route] op=routed agentType=public personId=${decision.personId} senderId=${msg.senderPhone}`);
|
|
24327
24428
|
}
|
|
@@ -24330,6 +24431,7 @@ init({
|
|
|
24330
24431
|
});
|
|
24331
24432
|
await waGateway.handleInbound({
|
|
24332
24433
|
accountId: msg.accountId,
|
|
24434
|
+
effectiveAccountId: msg.effectiveAccountId,
|
|
24333
24435
|
senderId: msg.senderPhone,
|
|
24334
24436
|
role: decision.role,
|
|
24335
24437
|
personId: decision.personId,
|