@rubytech/create-maxy-code 0.1.505 → 0.1.506
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/account-schema-regions/dist/index.d.ts +60 -0
- package/payload/platform/lib/account-schema-regions/dist/index.d.ts.map +1 -0
- package/payload/platform/lib/account-schema-regions/dist/index.js +101 -0
- package/payload/platform/lib/account-schema-regions/dist/index.js.map +1 -0
- package/payload/platform/lib/account-schema-regions/package.json +7 -0
- package/payload/platform/lib/account-schema-regions/src/index.ts +121 -0
- package/payload/platform/lib/account-schema-regions/tsconfig.json +9 -0
- package/payload/platform/package.json +3 -3
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +59 -17
- package/payload/platform/plugins/admin/skills/public-agent-manager/SKILL.md +3 -3
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +6 -0
- package/payload/platform/plugins/docs/references/admin-ui.md +58 -16
- package/payload/platform/scripts/check-no-esm-require.mjs +5 -0
- package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.d.ts +1 -1
- package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.js +50 -3
- package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.js.map +1 -1
- package/payload/server/{chunk-HN4HFTXE.js → chunk-5ADBNEJA.js} +1410 -1409
- package/payload/server/maxy-edge.js +1 -1
- package/payload/server/public/activity.html +5 -5
- package/payload/server/public/agents.html +4 -4
- package/payload/server/public/assets/{AdminLoginScreens-DeWt3H7l.js → AdminLoginScreens-D5KOQA8S.js} +1 -1
- package/payload/server/public/assets/{AdminShell-u32d8puE.js → AdminShell-C8wazpnz.js} +2 -2
- package/payload/server/public/assets/{activity-DjP45-DD.js → activity-CFqkDhTs.js} +1 -1
- package/payload/server/public/assets/admin-B-EGUWS4.js +1 -0
- package/payload/server/public/assets/agents-DPulfmrd.js +1 -0
- package/payload/server/public/assets/{browser-ULvl_K5x.js → browser-T_5xbnJh.js} +1 -1
- package/payload/server/public/assets/{calendar-CGMrT6WZ.js → calendar-CKjuiG8c.js} +1 -1
- package/payload/server/public/assets/chat-DWLYAA70.js +1 -0
- package/payload/server/public/assets/chevron-left-DBMCTKlT.js +1 -0
- package/payload/server/public/assets/chevron-right-SBAr6zzc.js +1 -0
- package/payload/server/public/assets/clock-kkzt7Scf.js +1 -0
- package/payload/server/public/assets/data-CV8N9qcy.js +1 -0
- package/payload/server/public/assets/{file-text-C5K6DMcK.js → file-text-c6cA4Evd.js} +1 -1
- package/payload/server/public/assets/{graph-CO6e1bgD.js → graph-DIk-o45J.js} +2 -2
- package/payload/server/public/assets/{graph-labels-iliQjlfE.js → graph-labels-5bjxhC_p.js} +1 -1
- package/payload/server/public/assets/{maximize-2-D-64XK6V.js → maximize-2-BCQkFb1_.js} +1 -1
- package/payload/server/public/assets/{operator-DyXgogJ1.js → operator-0UnNo2ms.js} +1 -1
- package/payload/server/public/assets/page-HVtUfub2.js +32 -0
- package/payload/server/public/assets/page-JJa2xdzf.js +1 -0
- package/payload/server/public/assets/{public-B9NVDBxW.js → public-CKEYyIPn.js} +1 -1
- package/payload/server/public/assets/{rotate-ccw-P35h7ogS.js → rotate-ccw-dTpO5dhL.js} +1 -1
- package/payload/server/public/assets/{routines-BeiynLF7.js → routines-C6P1-Dto.js} +1 -1
- package/payload/server/public/assets/{skills-C5H7ohLp.js → skills-ClrpVqlz.js} +1 -1
- package/payload/server/public/assets/tasks-C8SNhSFC.js +1 -0
- package/payload/server/public/assets/{time-entry-format-CDhHCMrW.js → time-entry-format-CCO6N6XZ.js} +1 -1
- package/payload/server/public/assets/{triangle-alert-OgY71rDV.js → triangle-alert-CHjdKpag.js} +1 -1
- package/payload/server/public/assets/{useCopyFeedback-jOGmWwa3.js → useCopyFeedback-BTUu3-l8.js} +1 -1
- package/payload/server/public/assets/{useSubAccountSwitcher-6AFuRawP.js → useSubAccountSwitcher-DSW9vDi2.js} +1 -1
- package/payload/server/public/assets/{useSubAccountSwitcher-AFSwtthL.css → useSubAccountSwitcher-DyrH8DA8.css} +1 -1
- package/payload/server/public/assets/{useVoiceRecorder-CnE3fhfV.js → useVoiceRecorder-BeNqxmm2.js} +1 -1
- package/payload/server/public/assets/{wrench-B1hsCj0R.js → wrench-CeNnc7oO.js} +1 -1
- package/payload/server/public/browser.html +4 -4
- package/payload/server/public/calendar.html +7 -7
- package/payload/server/public/chat.html +13 -13
- package/payload/server/public/data.html +11 -11
- package/payload/server/public/graph.html +9 -9
- package/payload/server/public/index.html +14 -14
- package/payload/server/public/operator.html +14 -14
- package/payload/server/public/public.html +13 -13
- package/payload/server/public/routines.html +6 -6
- package/payload/server/public/skills.html +5 -5
- package/payload/server/public/tasks.html +6 -6
- package/payload/server/server.js +1329 -826
- package/payload/server/public/assets/admin-Bo-Y6Vjx.js +0 -1
- package/payload/server/public/assets/agents-C80aCXd8.js +0 -1
- package/payload/server/public/assets/chat-DnWXaWD4.js +0 -1
- package/payload/server/public/assets/chevron-left-BB5Iljw-.js +0 -1
- package/payload/server/public/assets/chevron-right-DijU96_j.js +0 -1
- package/payload/server/public/assets/clock-DsmnhhJh.js +0 -1
- package/payload/server/public/assets/data-JPlhOLGC.js +0 -1
- package/payload/server/public/assets/page-DrbiAFS-.js +0 -32
- package/payload/server/public/assets/page-WeM2ry61.js +0 -1
- package/payload/server/public/assets/tasks-CI74_b6H.js +0 -1
|
@@ -2657,234 +2657,10 @@ var serve = (options, listeningListener) => {
|
|
|
2657
2657
|
return server;
|
|
2658
2658
|
};
|
|
2659
2659
|
|
|
2660
|
-
// server/loop-registry.ts
|
|
2661
|
-
import { readFileSync } from "fs";
|
|
2662
|
-
var TAG = "[loop-registry]";
|
|
2663
|
-
var STALL_INTERVALS = 3;
|
|
2664
|
-
var entries = /* @__PURE__ */ new Map();
|
|
2665
|
-
var defaultCpuIo = {
|
|
2666
|
-
ownCpuUs: () => {
|
|
2667
|
-
const u = process.cpuUsage();
|
|
2668
|
-
return u.user + u.system;
|
|
2669
|
-
},
|
|
2670
|
-
childCpuUs: (pid) => readPidCpuUs(pid)
|
|
2671
|
-
};
|
|
2672
|
-
var cpuIo = defaultCpuIo;
|
|
2673
|
-
function parsePidStatCpuUs(raw2) {
|
|
2674
|
-
const close = raw2.lastIndexOf(")");
|
|
2675
|
-
if (close === -1) return null;
|
|
2676
|
-
const after = raw2.slice(close + 2).split(" ");
|
|
2677
|
-
const utime = Number(after[11]);
|
|
2678
|
-
const stime = Number(after[12]);
|
|
2679
|
-
if (!Number.isFinite(utime) || !Number.isFinite(stime)) return null;
|
|
2680
|
-
return (utime + stime) / 100 * 1e6;
|
|
2681
|
-
}
|
|
2682
|
-
function readPidCpuUs(pid) {
|
|
2683
|
-
if (process.platform !== "linux") return null;
|
|
2684
|
-
try {
|
|
2685
|
-
return parsePidStatCpuUs(readFileSync(`/proc/${pid}/stat`, "utf8"));
|
|
2686
|
-
} catch {
|
|
2687
|
-
return null;
|
|
2688
|
-
}
|
|
2689
|
-
}
|
|
2690
|
-
function sampleCpu(nowMs = Date.now()) {
|
|
2691
|
-
for (const e of entries.values()) {
|
|
2692
|
-
if (!e.cpuAttributable) {
|
|
2693
|
-
e.cpuPct = null;
|
|
2694
|
-
continue;
|
|
2695
|
-
}
|
|
2696
|
-
let childUs = 0;
|
|
2697
|
-
for (const pid of [...e.children]) {
|
|
2698
|
-
const us = cpuIo.childCpuUs(pid);
|
|
2699
|
-
if (us === null) {
|
|
2700
|
-
e.children.delete(pid);
|
|
2701
|
-
continue;
|
|
2702
|
-
}
|
|
2703
|
-
childUs += us;
|
|
2704
|
-
}
|
|
2705
|
-
e.lastChildCpuUs = Math.max(e.lastChildCpuUs, childUs);
|
|
2706
|
-
const totalUs = e.cumulativeCpuUs + e.lastChildCpuUs;
|
|
2707
|
-
const p = e.prior;
|
|
2708
|
-
if (p) {
|
|
2709
|
-
const wallUs = (nowMs - p.atMs) * 1e3;
|
|
2710
|
-
e.cpuPct = wallUs > 0 && totalUs >= p.cpuUs ? (totalUs - p.cpuUs) / wallUs * 100 : null;
|
|
2711
|
-
} else {
|
|
2712
|
-
e.cpuPct = null;
|
|
2713
|
-
}
|
|
2714
|
-
e.prior = { cpuUs: totalUs, atMs: nowMs };
|
|
2715
|
-
}
|
|
2716
|
-
}
|
|
2717
|
-
var processPrior = null;
|
|
2718
|
-
function sampleProcessCpu(nowMs = Date.now()) {
|
|
2719
|
-
const cpuUs = cpuIo.ownCpuUs();
|
|
2720
|
-
let processPct = null;
|
|
2721
|
-
if (processPrior) {
|
|
2722
|
-
const wallUs = (nowMs - processPrior.atMs) * 1e3;
|
|
2723
|
-
if (wallUs > 0) processPct = (cpuUs - processPrior.cpuUs) / wallUs * 100;
|
|
2724
|
-
}
|
|
2725
|
-
processPrior = { cpuUs, atMs: nowMs };
|
|
2726
|
-
const rows = [...entries.values()];
|
|
2727
|
-
const measured = rows.filter((e) => e.cpuPct !== null);
|
|
2728
|
-
const attributedPct = measured.length === 0 ? null : measured.reduce((sum, e) => sum + (e.cpuPct ?? 0), 0);
|
|
2729
|
-
const unattributedPct = processPct === null || attributedPct === null ? null : Math.max(0, processPct - attributedPct);
|
|
2730
|
-
return { processPct, attributedPct, unattributedPct };
|
|
2731
|
-
}
|
|
2732
|
-
function messageOf(err) {
|
|
2733
|
-
return err instanceof Error ? err.message : String(err);
|
|
2734
|
-
}
|
|
2735
|
-
function finish(e, startedAt, cpuStart, cpuAttributable, note, err) {
|
|
2736
|
-
const cpu = process.cpuUsage(cpuStart);
|
|
2737
|
-
e.running = false;
|
|
2738
|
-
e.lastDurationMs = Date.now() - startedAt;
|
|
2739
|
-
e.lastCpuMs = cpuAttributable ? (cpu.user + cpu.system) / 1e3 : null;
|
|
2740
|
-
if (err !== null) {
|
|
2741
|
-
e.lastOutcome = "error";
|
|
2742
|
-
e.lastError = messageOf(err);
|
|
2743
|
-
e.lastNote = null;
|
|
2744
|
-
e.consecutiveFailures += 1;
|
|
2745
|
-
console.error(
|
|
2746
|
-
`${TAG} op=run name=${e.spec.name} outcome=error streak=${e.consecutiveFailures} ms=${e.lastDurationMs} err="${e.lastError}"`
|
|
2747
|
-
);
|
|
2748
|
-
return;
|
|
2749
|
-
}
|
|
2750
|
-
e.lastOutcome = "ok";
|
|
2751
|
-
e.lastError = null;
|
|
2752
|
-
e.lastNote = typeof note === "string" && note.length > 0 ? note : null;
|
|
2753
|
-
e.lastSuccessMs = Date.now();
|
|
2754
|
-
e.successCount += 1;
|
|
2755
|
-
e.consecutiveFailures = 0;
|
|
2756
|
-
}
|
|
2757
|
-
function invoke(e) {
|
|
2758
|
-
if (e.running) {
|
|
2759
|
-
console.warn(`${TAG} op=skip name=${e.spec.name} reason=still-running`);
|
|
2760
|
-
return;
|
|
2761
|
-
}
|
|
2762
|
-
e.running = true;
|
|
2763
|
-
e.runCount += 1;
|
|
2764
|
-
e.lastStartMs = Date.now();
|
|
2765
|
-
const startedAt = e.lastStartMs;
|
|
2766
|
-
const cpuStart = process.cpuUsage();
|
|
2767
|
-
const ownStartUs = cpuIo.ownCpuUs();
|
|
2768
|
-
const ctx = {
|
|
2769
|
-
child: (pid) => {
|
|
2770
|
-
if (typeof pid === "number" && Number.isFinite(pid)) e.children.add(pid);
|
|
2771
|
-
}
|
|
2772
|
-
};
|
|
2773
|
-
let result;
|
|
2774
|
-
try {
|
|
2775
|
-
result = e.spec.run(ctx);
|
|
2776
|
-
} catch (err) {
|
|
2777
|
-
e.cumulativeCpuUs += Math.max(0, cpuIo.ownCpuUs() - ownStartUs);
|
|
2778
|
-
finish(e, startedAt, cpuStart, true, null, err);
|
|
2779
|
-
return;
|
|
2780
|
-
}
|
|
2781
|
-
if (result instanceof Promise) {
|
|
2782
|
-
e.cpuAttributable = false;
|
|
2783
|
-
result.then(
|
|
2784
|
-
(note) => finish(e, startedAt, cpuStart, false, note, null),
|
|
2785
|
-
(err) => finish(e, startedAt, cpuStart, false, null, err)
|
|
2786
|
-
);
|
|
2787
|
-
return;
|
|
2788
|
-
}
|
|
2789
|
-
e.cumulativeCpuUs += Math.max(0, cpuIo.ownCpuUs() - ownStartUs);
|
|
2790
|
-
finish(e, startedAt, cpuStart, true, result, null);
|
|
2791
|
-
}
|
|
2792
|
-
function registerLoop(spec) {
|
|
2793
|
-
if (entries.has(spec.name)) {
|
|
2794
|
-
throw new Error(`${TAG} loop "${spec.name}" is already registered`);
|
|
2795
|
-
}
|
|
2796
|
-
const e = {
|
|
2797
|
-
spec,
|
|
2798
|
-
armedAtMs: Date.now(),
|
|
2799
|
-
lastStartMs: null,
|
|
2800
|
-
lastSuccessMs: null,
|
|
2801
|
-
lastDurationMs: null,
|
|
2802
|
-
lastCpuMs: null,
|
|
2803
|
-
lastOutcome: null,
|
|
2804
|
-
lastError: null,
|
|
2805
|
-
lastNote: null,
|
|
2806
|
-
runCount: 0,
|
|
2807
|
-
successCount: 0,
|
|
2808
|
-
consecutiveFailures: 0,
|
|
2809
|
-
running: false,
|
|
2810
|
-
timers: [],
|
|
2811
|
-
cumulativeCpuUs: 0,
|
|
2812
|
-
cpuAttributable: true,
|
|
2813
|
-
children: /* @__PURE__ */ new Set(),
|
|
2814
|
-
lastChildCpuUs: 0,
|
|
2815
|
-
prior: null,
|
|
2816
|
-
cpuPct: null
|
|
2817
|
-
};
|
|
2818
|
-
entries.set(spec.name, e);
|
|
2819
|
-
const interval = setInterval(() => invoke(e), spec.intervalMs);
|
|
2820
|
-
if (typeof interval.unref === "function") interval.unref();
|
|
2821
|
-
e.timers.push(interval);
|
|
2822
|
-
console.log(
|
|
2823
|
-
`${TAG} op=arm name=${spec.name} intervalMs=${spec.intervalMs} firstRunMs=${spec.firstRunDelayMs ?? "interval"} detached=${spec.detached === true} armed=${entries.size}`
|
|
2824
|
-
);
|
|
2825
|
-
if (spec.firstRunDelayMs === 0) {
|
|
2826
|
-
invoke(e);
|
|
2827
|
-
} else if (spec.firstRunDelayMs !== void 0) {
|
|
2828
|
-
const first = setTimeout(() => invoke(e), spec.firstRunDelayMs);
|
|
2829
|
-
if (typeof first.unref === "function") first.unref();
|
|
2830
|
-
e.timers.push(first);
|
|
2831
|
-
}
|
|
2832
|
-
let stopped = false;
|
|
2833
|
-
return () => {
|
|
2834
|
-
if (stopped) return;
|
|
2835
|
-
stopped = true;
|
|
2836
|
-
for (const t of e.timers) clearInterval(t);
|
|
2837
|
-
if (entries.get(spec.name) === e) entries.delete(spec.name);
|
|
2838
|
-
console.log(`${TAG} op=disarm name=${spec.name} armed=${entries.size}`);
|
|
2839
|
-
};
|
|
2840
|
-
}
|
|
2841
|
-
function stateOf(e, nowMs) {
|
|
2842
|
-
if (e.lastStartMs === null) {
|
|
2843
|
-
return nowMs - e.armedAtMs < e.spec.intervalMs ? "waiting" : "never-run";
|
|
2844
|
-
}
|
|
2845
|
-
const overdue = nowMs - e.lastStartMs > e.spec.intervalMs * STALL_INTERVALS;
|
|
2846
|
-
if (overdue) return "stalled";
|
|
2847
|
-
if (e.running) return "running";
|
|
2848
|
-
if (e.successCount === 0) return "never-succeeded";
|
|
2849
|
-
if (e.consecutiveFailures > 0) return "failing";
|
|
2850
|
-
return "ok";
|
|
2851
|
-
}
|
|
2852
|
-
function nextDueOf(e, nowMs) {
|
|
2853
|
-
const elapsed = Math.max(0, nowMs - e.armedAtMs);
|
|
2854
|
-
const ticks = Math.floor(elapsed / e.spec.intervalMs) + 1;
|
|
2855
|
-
return e.armedAtMs + ticks * e.spec.intervalMs;
|
|
2856
|
-
}
|
|
2857
|
-
function listLoops(nowMs = Date.now()) {
|
|
2858
|
-
return [...entries.values()].map((e) => ({
|
|
2859
|
-
name: e.spec.name,
|
|
2860
|
-
intervalMs: e.spec.intervalMs,
|
|
2861
|
-
detached: e.spec.detached === true,
|
|
2862
|
-
armedAtMs: e.armedAtMs,
|
|
2863
|
-
lastStartMs: e.lastStartMs,
|
|
2864
|
-
lastSuccessMs: e.lastSuccessMs,
|
|
2865
|
-
lastDurationMs: e.lastDurationMs,
|
|
2866
|
-
lastCpuMs: e.lastCpuMs,
|
|
2867
|
-
cpuPct: e.cpuPct,
|
|
2868
|
-
cpuAttributable: e.cpuAttributable,
|
|
2869
|
-
lastOutcome: e.lastOutcome,
|
|
2870
|
-
lastError: e.lastError,
|
|
2871
|
-
lastNote: e.lastNote,
|
|
2872
|
-
runCount: e.runCount,
|
|
2873
|
-
successCount: e.successCount,
|
|
2874
|
-
consecutiveFailures: e.consecutiveFailures,
|
|
2875
|
-
running: e.running,
|
|
2876
|
-
state: stateOf(e, nowMs),
|
|
2877
|
-
nextDueMs: nextDueOf(e, nowMs)
|
|
2878
|
-
}));
|
|
2879
|
-
}
|
|
2880
|
-
function armedCount() {
|
|
2881
|
-
return entries.size;
|
|
2882
|
-
}
|
|
2883
|
-
|
|
2884
2660
|
// app/lib/paths.ts
|
|
2885
2661
|
import { homedir } from "os";
|
|
2886
2662
|
import { resolve, join } from "path";
|
|
2887
|
-
import { existsSync, readFileSync
|
|
2663
|
+
import { existsSync, readFileSync } from "fs";
|
|
2888
2664
|
var configDirName = ".maxy";
|
|
2889
2665
|
var commercialMode = false;
|
|
2890
2666
|
var bundleMode = "none";
|
|
@@ -2900,7 +2676,7 @@ if (platformRoot) {
|
|
|
2900
2676
|
if (existsSync(brandPath)) {
|
|
2901
2677
|
let brand;
|
|
2902
2678
|
try {
|
|
2903
|
-
brand = JSON.parse(
|
|
2679
|
+
brand = JSON.parse(readFileSync(brandPath, "utf-8"));
|
|
2904
2680
|
} catch (err) {
|
|
2905
2681
|
const detail = (err instanceof Error ? err.message : String(err)).slice(0, 160);
|
|
2906
2682
|
console.error(`[paths] error reason=brand-config-missing path=${brandPath} detail="parse failed: ${detail.replace(/"/g, "'")}"`);
|
|
@@ -2970,1278 +2746,1502 @@ var TELEGRAM_ADMIN_WEBHOOK_SECRET_FILE = resolve(MAXY_DIR, ".telegram-admin-webh
|
|
|
2970
2746
|
var VISITOR_TOKEN_SECRET_FILE = resolve(MAXY_DIR, "credentials", "visitor-token-secret");
|
|
2971
2747
|
var CLAUDE_CREDENTIALS_FILE = resolve(MAXY_DIR, ".claude", ".credentials.json");
|
|
2972
2748
|
|
|
2973
|
-
// app/lib/
|
|
2974
|
-
|
|
2749
|
+
// app/lib/system-path-slugs.ts
|
|
2750
|
+
var SYSTEM_PATH_SLUGS = ["public", "public-chat", "chat", "graph", "data", "browser"];
|
|
2751
|
+
|
|
2752
|
+
// app/lib/claude-agent/account.ts
|
|
2975
2753
|
import { resolve as resolve2 } from "path";
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
}
|
|
2980
|
-
|
|
2981
|
-
}
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
const
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
appendFileSync(VNC_LOG_FILE, line);
|
|
2990
|
-
} catch (err) {
|
|
2991
|
-
console.error(`[vnc-log-fail] ${err.message} \u2014 dropped: ${line.slice(0, 300).trim()}`);
|
|
2754
|
+
import { readFileSync as readFileSync3, readdirSync, existsSync as existsSync3, statSync } from "fs";
|
|
2755
|
+
|
|
2756
|
+
// ../lib/brand-templating/src/index.ts
|
|
2757
|
+
import { join as join2 } from "path";
|
|
2758
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
2759
|
+
var PLACEHOLDER = "{{productName}}";
|
|
2760
|
+
var cachedProductName = null;
|
|
2761
|
+
function brandJsonPath() {
|
|
2762
|
+
const platformRoot2 = process.env.MAXY_PLATFORM_ROOT;
|
|
2763
|
+
if (!platformRoot2) {
|
|
2764
|
+
throw new Error(
|
|
2765
|
+
"[skill-loader] MAXY_PLATFORM_ROOT not set \u2014 cannot resolve brand.json"
|
|
2766
|
+
);
|
|
2992
2767
|
}
|
|
2768
|
+
return join2(platformRoot2, "config", "brand.json");
|
|
2993
2769
|
}
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
function sanitizeClientCorrId(raw2) {
|
|
3002
|
-
if (!raw2) return null;
|
|
3003
|
-
if (raw2.length > 32) return null;
|
|
3004
|
-
if (!/^[A-Za-z0-9_-]+$/.test(raw2)) return null;
|
|
3005
|
-
return raw2;
|
|
3006
|
-
}
|
|
3007
|
-
function formatKv(fields) {
|
|
3008
|
-
return Object.entries(fields).filter(([, v]) => v !== void 0).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(" ");
|
|
3009
|
-
}
|
|
3010
|
-
function httpLog(fields) {
|
|
3011
|
-
console.error(`[http] ${formatKv(fields)}`);
|
|
3012
|
-
}
|
|
3013
|
-
function websockifyLog(event, fields = {}) {
|
|
3014
|
-
console.error(`[websockify] ${formatKv({ event, ...fields })}`);
|
|
3015
|
-
}
|
|
3016
|
-
|
|
3017
|
-
// ../lib/admin-access-password/src/index.ts
|
|
3018
|
-
import { scrypt, randomBytes, timingSafeEqual } from "crypto";
|
|
3019
|
-
import { existsSync as existsSync2, readFileSync as readFileSync3, writeFileSync, renameSync, unlinkSync, appendFileSync as appendFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
3020
|
-
import { dirname } from "path";
|
|
3021
|
-
var SCRYPT_N = 16384;
|
|
3022
|
-
var SCRYPT_R = 8;
|
|
3023
|
-
var SCRYPT_P = 1;
|
|
3024
|
-
var SCRYPT_KEYLEN = 64;
|
|
3025
|
-
function id8(value) {
|
|
3026
|
-
return value.slice(0, 8);
|
|
3027
|
-
}
|
|
3028
|
-
function rowIds(users) {
|
|
3029
|
-
return users.map((u) => id8(u.userId)).join(",");
|
|
3030
|
-
}
|
|
3031
|
-
function appendUsersAuditLine(audit, fields) {
|
|
3032
|
-
const sess = audit.session ? ` session=${id8(audit.session)}` : "";
|
|
3033
|
-
const actor = audit.actor.includes("-") ? id8(audit.actor) : audit.actor;
|
|
3034
|
-
const targetFields = audit.target !== void 0 ? ` target=${audit.target.includes("-") ? id8(audit.target) : audit.target} explicitUserId=${audit.explicitUserId ? "true" : "false"}` : "";
|
|
3035
|
-
const line = `[users-audit] action=${fields.action} actor=${actor}${targetFields}${sess} field=${fields.field} rowsBefore=${fields.rowsBefore} rowsAfter=${fields.rowsAfter} ts=${(/* @__PURE__ */ new Date()).toISOString()}
|
|
3036
|
-
`;
|
|
2770
|
+
function getBrandProductName() {
|
|
2771
|
+
if (cachedProductName !== null) return cachedProductName;
|
|
2772
|
+
const path = brandJsonPath();
|
|
2773
|
+
if (!existsSync2(path)) {
|
|
2774
|
+
throw new Error(`[skill-loader] brand.json missing at ${path}`);
|
|
2775
|
+
}
|
|
2776
|
+
let parsed;
|
|
3037
2777
|
try {
|
|
3038
|
-
|
|
3039
|
-
appendFileSync2(audit.logFile, line, { mode: 384 });
|
|
2778
|
+
parsed = JSON.parse(readFileSync2(path, "utf-8"));
|
|
3040
2779
|
} catch (err) {
|
|
3041
|
-
|
|
3042
|
-
`[
|
|
2780
|
+
throw new Error(
|
|
2781
|
+
`[skill-loader] brand.json unreadable at ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
3043
2782
|
);
|
|
3044
2783
|
}
|
|
2784
|
+
const value = parsed.productName;
|
|
2785
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
2786
|
+
throw new Error(
|
|
2787
|
+
`[skill-loader] brand.json at ${path} has missing or empty productName`
|
|
2788
|
+
);
|
|
2789
|
+
}
|
|
2790
|
+
cachedProductName = value;
|
|
2791
|
+
return value;
|
|
3045
2792
|
}
|
|
3046
|
-
function
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
if (err) reject(err);
|
|
3050
|
-
else resolve7(key);
|
|
3051
|
-
});
|
|
3052
|
-
});
|
|
3053
|
-
}
|
|
3054
|
-
async function hashAccessPassword(password) {
|
|
3055
|
-
const salt = randomBytes(32);
|
|
3056
|
-
const hash = await scryptAsync(password, salt);
|
|
3057
|
-
return `${salt.toString("hex")}:${hash.toString("hex")}`;
|
|
3058
|
-
}
|
|
3059
|
-
async function matchesHash(password, stored) {
|
|
3060
|
-
const colon = stored.indexOf(":");
|
|
3061
|
-
if (colon === -1) return false;
|
|
3062
|
-
const salt = Buffer.from(stored.slice(0, colon), "hex");
|
|
3063
|
-
const storedHash = Buffer.from(stored.slice(colon + 1), "hex");
|
|
3064
|
-
if (storedHash.length !== SCRYPT_KEYLEN) return false;
|
|
3065
|
-
let computed;
|
|
2793
|
+
function substituteBrandPlaceholders(content, sourcePath) {
|
|
2794
|
+
if (!content.includes(PLACEHOLDER)) return content;
|
|
2795
|
+
let productName;
|
|
3066
2796
|
try {
|
|
3067
|
-
|
|
3068
|
-
} catch {
|
|
3069
|
-
|
|
2797
|
+
productName = getBrandProductName();
|
|
2798
|
+
} catch (err) {
|
|
2799
|
+
console.error(
|
|
2800
|
+
`[skill-loader] ERROR: brand.json missing \u2014 cannot resolve productName for skill ${sourcePath}`
|
|
2801
|
+
);
|
|
2802
|
+
throw err;
|
|
3070
2803
|
}
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
const parsed = JSON.parse(raw2);
|
|
3078
|
-
if (!Array.isArray(parsed)) throw new Error("users.json is not an array");
|
|
3079
|
-
return parsed;
|
|
2804
|
+
const occurrences = content.split(PLACEHOLDER).length - 1;
|
|
2805
|
+
const substituted = content.split(PLACEHOLDER).join(productName);
|
|
2806
|
+
console.log(
|
|
2807
|
+
`[skill-loader] brand-substituted productName=${productName} skill=${sourcePath} occurrences=${occurrences}`
|
|
2808
|
+
);
|
|
2809
|
+
return substituted;
|
|
3080
2810
|
}
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
2811
|
+
|
|
2812
|
+
// app/lib/claude-agent/account.ts
|
|
2813
|
+
var PLATFORM_ROOT2 = process.env.MAXY_PLATFORM_ROOT ?? resolve2(process.cwd(), "..");
|
|
2814
|
+
var ACCOUNTS_DIR = resolve2(PLATFORM_ROOT2, "..", "data/accounts");
|
|
2815
|
+
if (!existsSync3(PLATFORM_ROOT2)) {
|
|
2816
|
+
throw new Error(
|
|
2817
|
+
`PLATFORM_ROOT does not exist: ${PLATFORM_ROOT2}
|
|
2818
|
+
Set the MAXY_PLATFORM_ROOT environment variable to the absolute path of the platform directory.`
|
|
2819
|
+
);
|
|
3085
2820
|
}
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
2821
|
+
var _stubDirsLoggedThisBoot = /* @__PURE__ */ new Set();
|
|
2822
|
+
function listValidAccounts() {
|
|
2823
|
+
if (!existsSync3(ACCOUNTS_DIR)) return [];
|
|
2824
|
+
const out = [];
|
|
2825
|
+
const entries2 = readdirSync(ACCOUNTS_DIR, { withFileTypes: true });
|
|
2826
|
+
for (const entry of entries2) {
|
|
2827
|
+
if (!entry.isDirectory()) continue;
|
|
2828
|
+
if (entry.name.startsWith(".")) continue;
|
|
2829
|
+
const accountDir = resolve2(ACCOUNTS_DIR, entry.name);
|
|
2830
|
+
const configPath = resolve2(accountDir, "account.json");
|
|
2831
|
+
if (!existsSync3(configPath)) {
|
|
2832
|
+
if (!_stubDirsLoggedThisBoot.has(entry.name)) {
|
|
2833
|
+
console.error(
|
|
2834
|
+
`[platform] accounts-state STUB-DIR id=${entry.name} \u2014 onboarding leak; remove or repair`
|
|
2835
|
+
);
|
|
2836
|
+
_stubDirsLoggedThisBoot.add(entry.name);
|
|
2837
|
+
}
|
|
2838
|
+
continue;
|
|
2839
|
+
}
|
|
2840
|
+
let config;
|
|
2841
|
+
try {
|
|
2842
|
+
config = JSON.parse(readFileSync3(configPath, "utf-8"));
|
|
2843
|
+
} catch {
|
|
2844
|
+
console.error(
|
|
2845
|
+
`[platform] accounts-state CORRUPT-JSON id=${entry.name} \u2014 account.json failed to parse; treating as stub`
|
|
2846
|
+
);
|
|
2847
|
+
continue;
|
|
2848
|
+
}
|
|
2849
|
+
out.push({ accountId: config.accountId, accountDir, config });
|
|
3092
2850
|
}
|
|
3093
|
-
|
|
3094
|
-
users[idx] = { ...users[idx], accessHash: await hashAccessPassword(password) };
|
|
3095
|
-
writeUsersAtomic(usersFile, users);
|
|
3096
|
-
appendUsersAuditLine(audit, { action: "set-access", field: "accessHash", rowsBefore: rows, rowsAfter: rows });
|
|
2851
|
+
return out;
|
|
3097
2852
|
}
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
2853
|
+
function isValidAccountId(id) {
|
|
2854
|
+
return listValidAccounts().some((a) => a.accountId === id);
|
|
2855
|
+
}
|
|
2856
|
+
function resolveAccount() {
|
|
2857
|
+
if (!existsSync3(ACCOUNTS_DIR)) return null;
|
|
2858
|
+
let usersJsonUserId = null;
|
|
2859
|
+
if (existsSync3(USERS_FILE)) {
|
|
2860
|
+
try {
|
|
2861
|
+
const raw2 = readFileSync3(USERS_FILE, "utf-8").trim();
|
|
2862
|
+
if (raw2) {
|
|
2863
|
+
const users = JSON.parse(raw2);
|
|
2864
|
+
if (users.length > 0) {
|
|
2865
|
+
usersJsonUserId = users[0].userId;
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
} catch {
|
|
2869
|
+
}
|
|
3104
2870
|
}
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
2871
|
+
const entries2 = readdirSync(ACCOUNTS_DIR, { withFileTypes: true });
|
|
2872
|
+
let fallback = null;
|
|
2873
|
+
for (const entry of entries2) {
|
|
2874
|
+
if (!entry.isDirectory()) continue;
|
|
2875
|
+
const configPath = resolve2(ACCOUNTS_DIR, entry.name, "account.json");
|
|
2876
|
+
if (!existsSync3(configPath)) continue;
|
|
2877
|
+
const raw2 = readFileSync3(configPath, "utf-8");
|
|
2878
|
+
let config;
|
|
2879
|
+
try {
|
|
2880
|
+
config = JSON.parse(raw2);
|
|
2881
|
+
} catch {
|
|
2882
|
+
console.error(`[maxy] account.json is corrupt at ${configPath} \u2014 skipping`);
|
|
2883
|
+
continue;
|
|
2884
|
+
}
|
|
2885
|
+
if (!config.adminModel || !config.publicModel) {
|
|
2886
|
+
throw new Error(
|
|
2887
|
+
`[maxy] account.json at ${configPath} is missing required model fields (adminModel / publicModel). Update account.json with valid model identifiers.`
|
|
2888
|
+
);
|
|
2889
|
+
}
|
|
2890
|
+
const result = {
|
|
2891
|
+
accountId: config.accountId,
|
|
2892
|
+
accountDir: resolve2(ACCOUNTS_DIR, entry.name),
|
|
2893
|
+
config
|
|
2894
|
+
};
|
|
2895
|
+
if (usersJsonUserId && config.admins?.some((a) => a.userId === usersJsonUserId)) {
|
|
2896
|
+
return result;
|
|
2897
|
+
}
|
|
2898
|
+
if (!fallback) {
|
|
2899
|
+
fallback = result;
|
|
3108
2900
|
}
|
|
3109
2901
|
}
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
try {
|
|
3115
|
-
users = readUsers(usersFile);
|
|
3116
|
-
} catch {
|
|
3117
|
-
return false;
|
|
2902
|
+
if (usersJsonUserId && fallback) {
|
|
2903
|
+
console.warn(
|
|
2904
|
+
`[maxy] resolveAccount: no account matches users.json userId ${usersJsonUserId} \u2014 falling back to ${fallback.accountId}`
|
|
2905
|
+
);
|
|
3118
2906
|
}
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
2907
|
+
return fallback;
|
|
2908
|
+
}
|
|
2909
|
+
function readAgentFile(accountDir, agentName, filename) {
|
|
2910
|
+
const filePath = resolve2(accountDir, "agents", agentName, filename);
|
|
2911
|
+
if (!existsSync3(filePath)) return null;
|
|
2912
|
+
const raw2 = readFileSync3(filePath, "utf-8");
|
|
2913
|
+
if (filename.endsWith(".md")) {
|
|
2914
|
+
return substituteBrandPlaceholders(raw2, filePath);
|
|
3124
2915
|
}
|
|
3125
|
-
return
|
|
2916
|
+
return raw2;
|
|
3126
2917
|
}
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
2918
|
+
var RESERVED_SLUGS = /* @__PURE__ */ new Set(["admin", "api", "assets", "agent-assets", "brand", "bot", "privacy"]);
|
|
2919
|
+
var SHADOWED_SLUGS = new Set(SYSTEM_PATH_SLUGS);
|
|
2920
|
+
var SLUG_PATTERN = /^[a-z][a-z0-9-]{2,49}$/;
|
|
2921
|
+
function validateAgentSlug(slug) {
|
|
2922
|
+
if (!SLUG_PATTERN.test(slug)) return false;
|
|
2923
|
+
if (RESERVED_SLUGS.has(slug)) {
|
|
2924
|
+
console.error(`[agent-slug] op=reject slug=${slug} reason=reserved`);
|
|
3132
2925
|
return false;
|
|
3133
2926
|
}
|
|
3134
|
-
|
|
3135
|
-
}
|
|
3136
|
-
function accessStoreUnreadable(usersFile) {
|
|
3137
|
-
if (!existsSync2(usersFile)) return false;
|
|
3138
|
-
try {
|
|
3139
|
-
readUsers(usersFile);
|
|
2927
|
+
if (SHADOWED_SLUGS.has(slug)) {
|
|
2928
|
+
console.error(`[agent-slug] op=reject slug=${slug} reason=shadowed`);
|
|
3140
2929
|
return false;
|
|
3141
|
-
} catch {
|
|
3142
|
-
return true;
|
|
3143
2930
|
}
|
|
2931
|
+
return true;
|
|
3144
2932
|
}
|
|
3145
|
-
function
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
if (idx === -1) return "noop-no-owner";
|
|
3151
|
-
const existing = users[idx].accessHash;
|
|
3152
|
-
if (typeof existing === "string" && existing.length > 0) {
|
|
3153
|
-
unlinkSync(legacyFile);
|
|
3154
|
-
return "noop-owner-has-hash";
|
|
2933
|
+
function resolveDefaultAgentSlug(accountDir) {
|
|
2934
|
+
const configPath = resolve2(accountDir, "account.json");
|
|
2935
|
+
if (!existsSync3(configPath)) {
|
|
2936
|
+
console.error("[agent-resolve] account.json not found \u2014 cannot resolve defaultAgent");
|
|
2937
|
+
return null;
|
|
3155
2938
|
}
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
2939
|
+
let config;
|
|
2940
|
+
try {
|
|
2941
|
+
config = JSON.parse(readFileSync3(configPath, "utf-8"));
|
|
2942
|
+
} catch (err) {
|
|
2943
|
+
console.error("[agent-resolve] failed to read account.json:", err);
|
|
2944
|
+
return null;
|
|
3159
2945
|
}
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
}
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
return raw2.includes(":") ? raw2 : null;
|
|
3171
|
-
}
|
|
3172
|
-
|
|
3173
|
-
// app/lib/password-strength.ts
|
|
3174
|
-
function validatePasswordStrength(password) {
|
|
3175
|
-
return [
|
|
3176
|
-
{ key: "length", label: "At least 8 characters", met: password.length >= 8 },
|
|
3177
|
-
{ key: "number", label: "Contains a number", met: /\d/.test(password) },
|
|
3178
|
-
{ key: "special", label: "Contains a special character", met: /[^A-Za-z0-9]/.test(password) },
|
|
3179
|
-
{ key: "whitespace", label: "No spaces", met: password.length > 0 && !/\s/.test(password) }
|
|
3180
|
-
];
|
|
3181
|
-
}
|
|
3182
|
-
function isPasswordValid(password) {
|
|
3183
|
-
return validatePasswordStrength(password).every((r) => r.met);
|
|
3184
|
-
}
|
|
3185
|
-
|
|
3186
|
-
// app/lib/remote-auth.ts
|
|
3187
|
-
import { scrypt as scrypt2, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual2, createHmac } from "crypto";
|
|
3188
|
-
import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
3189
|
-
import { dirname as dirname2 } from "path";
|
|
3190
|
-
var SCRYPT_N2 = 16384;
|
|
3191
|
-
var SCRYPT_R2 = 8;
|
|
3192
|
-
var SCRYPT_P2 = 1;
|
|
3193
|
-
var SCRYPT_KEYLEN2 = 64;
|
|
3194
|
-
function scryptAsync2(password, salt) {
|
|
3195
|
-
return new Promise((resolve7, reject) => {
|
|
3196
|
-
scrypt2(password, salt, SCRYPT_KEYLEN2, { N: SCRYPT_N2, r: SCRYPT_R2, p: SCRYPT_P2 }, (err, key) => {
|
|
3197
|
-
if (err) reject(err);
|
|
3198
|
-
else resolve7(key);
|
|
3199
|
-
});
|
|
3200
|
-
});
|
|
3201
|
-
}
|
|
3202
|
-
async function hashPassword(password) {
|
|
3203
|
-
const salt = randomBytes2(32);
|
|
3204
|
-
const hash = await scryptAsync2(password, salt);
|
|
3205
|
-
return `${salt.toString("hex")}:${hash.toString("hex")}`;
|
|
3206
|
-
}
|
|
3207
|
-
function isRemoteAuthConfigured() {
|
|
3208
|
-
if (anyAccessPasswordConfigured(USERS_FILE)) return true;
|
|
3209
|
-
if (accessStoreUnreadable(USERS_FILE)) return true;
|
|
3210
|
-
return readLegacyRemoteHash(REMOTE_PASSWORD_FILE) !== null;
|
|
2946
|
+
if (!config.defaultAgent) {
|
|
2947
|
+
console.error("[agent-resolve] defaultAgent not configured in account.json \u2014 set it via the connect-whatsapp skill");
|
|
2948
|
+
return null;
|
|
2949
|
+
}
|
|
2950
|
+
const agentConfigPath = resolve2(accountDir, "agents", config.defaultAgent, "config.json");
|
|
2951
|
+
if (!existsSync3(agentConfigPath)) {
|
|
2952
|
+
console.error(`[agent-resolve] defaultAgent="${config.defaultAgent}" has no config.json at ${agentConfigPath}`);
|
|
2953
|
+
return null;
|
|
2954
|
+
}
|
|
2955
|
+
return config.defaultAgent;
|
|
3211
2956
|
}
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
writeFileSync2(REMOTE_PASSWORD_FILE, hash, "utf-8");
|
|
2957
|
+
function estimateTokens(text) {
|
|
2958
|
+
return Math.ceil(text.length / 4);
|
|
3215
2959
|
}
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
2960
|
+
function resolveAgentConfig(accountDir, agentName) {
|
|
2961
|
+
let model = null;
|
|
2962
|
+
let status = null;
|
|
2963
|
+
let displayName = null;
|
|
2964
|
+
let image = null;
|
|
2965
|
+
let imageShape = null;
|
|
2966
|
+
let showAgentName = false;
|
|
2967
|
+
let knowledgeKeywords = null;
|
|
2968
|
+
let accessMode = "open";
|
|
2969
|
+
const MAX_KNOWLEDGE_KEYWORDS = 5;
|
|
2970
|
+
const configRaw = readAgentFile(accountDir, agentName, "config.json");
|
|
2971
|
+
if (configRaw) {
|
|
2972
|
+
let parsed;
|
|
2973
|
+
try {
|
|
2974
|
+
parsed = JSON.parse(configRaw);
|
|
2975
|
+
} catch {
|
|
2976
|
+
console.warn(`[agent-config] ${agentName}/config.json: invalid JSON \u2014 using defaults`);
|
|
2977
|
+
parsed = {};
|
|
2978
|
+
}
|
|
2979
|
+
model = typeof parsed.model === "string" ? parsed.model : null;
|
|
2980
|
+
status = typeof parsed.status === "string" ? parsed.status : null;
|
|
2981
|
+
displayName = typeof parsed.displayName === "string" ? parsed.displayName : null;
|
|
2982
|
+
image = typeof parsed.image === "string" ? parsed.image : null;
|
|
2983
|
+
if (typeof parsed.imageShape === "string" && ["circle", "rounded"].includes(parsed.imageShape)) {
|
|
2984
|
+
imageShape = parsed.imageShape;
|
|
2985
|
+
}
|
|
2986
|
+
if (parsed.showAgentName === true) {
|
|
2987
|
+
showAgentName = true;
|
|
2988
|
+
} else if (parsed.showAgentName === "none") {
|
|
2989
|
+
showAgentName = "none";
|
|
2990
|
+
}
|
|
2991
|
+
if (image || imageShape || showAgentName) {
|
|
2992
|
+
console.log(`[agent-config] ${agentName}: image=${image || "(none)"} imageShape=${imageShape || "(none)"} showAgentName=${showAgentName}`);
|
|
2993
|
+
}
|
|
2994
|
+
if (typeof parsed.accessMode === "string" && ["gated", "paid"].includes(parsed.accessMode)) {
|
|
2995
|
+
accessMode = parsed.accessMode;
|
|
2996
|
+
}
|
|
2997
|
+
if (Array.isArray(parsed.knowledgeKeywords) && parsed.knowledgeKeywords.length > 0) {
|
|
2998
|
+
const filtered = parsed.knowledgeKeywords.filter((k) => typeof k === "string" && k.trim()).map((k) => k.replace(/,/g, "").trim().toLowerCase()).filter(Boolean);
|
|
2999
|
+
if (filtered.length > MAX_KNOWLEDGE_KEYWORDS) {
|
|
3000
|
+
console.warn(`[agent-config] ${agentName}: knowledgeKeywords has ${filtered.length} entries \u2014 capping at ${MAX_KNOWLEDGE_KEYWORDS}`);
|
|
3001
|
+
}
|
|
3002
|
+
knowledgeKeywords = filtered.length > 0 ? filtered.slice(0, MAX_KNOWLEDGE_KEYWORDS) : null;
|
|
3003
|
+
}
|
|
3222
3004
|
}
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3005
|
+
let knowledge = null;
|
|
3006
|
+
let knowledgeBaked = false;
|
|
3007
|
+
const agentDir = resolve2(accountDir, "agents", agentName);
|
|
3008
|
+
const knowledgePath = resolve2(agentDir, "KNOWLEDGE.md");
|
|
3009
|
+
const summaryPath = resolve2(agentDir, "KNOWLEDGE-SUMMARY.md");
|
|
3010
|
+
const hasKnowledge = existsSync3(knowledgePath);
|
|
3011
|
+
const hasSummary = existsSync3(summaryPath);
|
|
3012
|
+
if (hasKnowledge && hasSummary) {
|
|
3013
|
+
const knowledgeMtime = statSync(knowledgePath).mtimeMs;
|
|
3014
|
+
const summaryMtime = statSync(summaryPath).mtimeMs;
|
|
3015
|
+
if (summaryMtime >= knowledgeMtime) {
|
|
3016
|
+
knowledge = readFileSync3(summaryPath, "utf-8");
|
|
3017
|
+
} else {
|
|
3018
|
+
console.warn(`[agent-config] ${agentName}: KNOWLEDGE-SUMMARY.md is stale (KNOWLEDGE.md is newer) \u2014 using full knowledge`);
|
|
3019
|
+
knowledge = readFileSync3(knowledgePath, "utf-8");
|
|
3238
3020
|
}
|
|
3239
|
-
|
|
3021
|
+
knowledgeBaked = true;
|
|
3022
|
+
} else if (hasKnowledge) {
|
|
3023
|
+
knowledge = readFileSync3(knowledgePath, "utf-8");
|
|
3024
|
+
knowledgeBaked = true;
|
|
3240
3025
|
}
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3026
|
+
let budget = null;
|
|
3027
|
+
const identityRaw = readAgentFile(accountDir, agentName, "IDENTITY.md");
|
|
3028
|
+
const soulRaw = readAgentFile(accountDir, agentName, "SOUL.md");
|
|
3029
|
+
if (identityRaw || soulRaw || knowledge) {
|
|
3030
|
+
const identityTokens = identityRaw ? estimateTokens(identityRaw) : 0;
|
|
3031
|
+
const soulTokens = soulRaw ? estimateTokens(soulRaw) : 0;
|
|
3032
|
+
const knowledgeTokens = knowledge ? estimateTokens(knowledge) : 0;
|
|
3033
|
+
budget = {
|
|
3034
|
+
identity: identityTokens,
|
|
3035
|
+
soul: soulTokens,
|
|
3036
|
+
knowledge: knowledgeTokens,
|
|
3037
|
+
plugins: 0,
|
|
3038
|
+
total: identityTokens + soulTokens + knowledgeTokens
|
|
3039
|
+
};
|
|
3246
3040
|
}
|
|
3247
|
-
|
|
3248
|
-
cachedSecret = Buffer.from(hex, "hex");
|
|
3249
|
-
return cachedSecret;
|
|
3250
|
-
}
|
|
3251
|
-
function base64urlEncode(buf) {
|
|
3252
|
-
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
3041
|
+
return { model, status, displayName, image, imageShape, showAgentName, knowledge, knowledgeBaked, knowledgeKeywords, budget, accessMode };
|
|
3253
3042
|
}
|
|
3254
|
-
function
|
|
3255
|
-
|
|
3256
|
-
return Buffer.from(padded, "base64");
|
|
3043
|
+
function isActiveAgentSlug(accountDir, slug) {
|
|
3044
|
+
return resolveAgentConfig(accountDir, slug).status === "active";
|
|
3257
3045
|
}
|
|
3258
|
-
function
|
|
3259
|
-
return
|
|
3046
|
+
function getDefaultAccountId() {
|
|
3047
|
+
return resolveAccount()?.accountId ?? null;
|
|
3260
3048
|
}
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3049
|
+
|
|
3050
|
+
// server/loop-registry.ts
|
|
3051
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
3052
|
+
var TAG = "[loop-registry]";
|
|
3053
|
+
var STALL_INTERVALS = 3;
|
|
3054
|
+
var entries = /* @__PURE__ */ new Map();
|
|
3055
|
+
var defaultCpuIo = {
|
|
3056
|
+
ownCpuUs: () => {
|
|
3057
|
+
const u = process.cpuUsage();
|
|
3058
|
+
return u.user + u.system;
|
|
3059
|
+
},
|
|
3060
|
+
childCpuUs: (pid) => readPidCpuUs(pid)
|
|
3061
|
+
};
|
|
3062
|
+
var cpuIo = defaultCpuIo;
|
|
3063
|
+
function parsePidStatCpuUs(raw2) {
|
|
3064
|
+
const close = raw2.lastIndexOf(")");
|
|
3065
|
+
if (close === -1) return null;
|
|
3066
|
+
const after = raw2.slice(close + 2).split(" ");
|
|
3067
|
+
const utime = Number(after[11]);
|
|
3068
|
+
const stime = Number(after[12]);
|
|
3069
|
+
if (!Number.isFinite(utime) || !Number.isFinite(stime)) return null;
|
|
3070
|
+
return (utime + stime) / 100 * 1e6;
|
|
3267
3071
|
}
|
|
3268
|
-
function
|
|
3269
|
-
if (
|
|
3270
|
-
const rest = token.slice(TOKEN_PREFIX.length);
|
|
3271
|
-
const dot = rest.indexOf(".");
|
|
3272
|
-
if (dot === -1) return null;
|
|
3273
|
-
const payloadB64 = rest.slice(0, dot);
|
|
3274
|
-
const sigB64 = rest.slice(dot + 1);
|
|
3275
|
-
if (!payloadB64 || !sigB64) return null;
|
|
3276
|
-
let payloadJson;
|
|
3277
|
-
try {
|
|
3278
|
-
payloadJson = base64urlDecode(payloadB64).toString("utf-8");
|
|
3279
|
-
} catch {
|
|
3280
|
-
return null;
|
|
3281
|
-
}
|
|
3282
|
-
const expected = signPayload(payloadJson, getSecret());
|
|
3283
|
-
const expectedBuf = Buffer.from(expected, "utf-8");
|
|
3284
|
-
const provBuf = Buffer.from(sigB64, "utf-8");
|
|
3285
|
-
const signatureValid = expectedBuf.length === provBuf.length && timingSafeEqual2(expectedBuf, provBuf);
|
|
3286
|
-
let payload;
|
|
3072
|
+
function readPidCpuUs(pid) {
|
|
3073
|
+
if (process.platform !== "linux") return null;
|
|
3287
3074
|
try {
|
|
3288
|
-
|
|
3075
|
+
return parsePidStatCpuUs(readFileSync4(`/proc/${pid}/stat`, "utf8"));
|
|
3289
3076
|
} catch {
|
|
3290
3077
|
return null;
|
|
3291
3078
|
}
|
|
3292
|
-
if (!payload || typeof payload !== "object" || typeof payload.c !== "number" || typeof payload.n !== "string") {
|
|
3293
|
-
return null;
|
|
3294
|
-
}
|
|
3295
|
-
return { payload, signatureValid };
|
|
3296
|
-
}
|
|
3297
|
-
function validateRemoteSession(token, now = Date.now()) {
|
|
3298
|
-
if (!token) return false;
|
|
3299
|
-
const parsed = parseToken(token);
|
|
3300
|
-
if (!parsed || !parsed.signatureValid) return false;
|
|
3301
|
-
return now - parsed.payload.c <= SESSION_TTL_MS;
|
|
3302
|
-
}
|
|
3303
|
-
function describeRemoteSession(token, now = Date.now()) {
|
|
3304
|
-
if (!token) return { present: false, expired: false };
|
|
3305
|
-
const parsed = parseToken(token);
|
|
3306
|
-
if (!parsed || !parsed.signatureValid) return { present: false, expired: false };
|
|
3307
|
-
return { present: true, expired: now - parsed.payload.c > SESSION_TTL_MS };
|
|
3308
|
-
}
|
|
3309
|
-
function normalizeIp(ip) {
|
|
3310
|
-
if (!ip) return void 0;
|
|
3311
|
-
const trimmed = ip.trim();
|
|
3312
|
-
if (trimmed.startsWith("::ffff:")) return trimmed.slice(7);
|
|
3313
|
-
return trimmed;
|
|
3314
|
-
}
|
|
3315
|
-
function isLoopback(ip) {
|
|
3316
|
-
return ip === "127.0.0.1" || ip.startsWith("127.") || ip === "::1";
|
|
3317
3079
|
}
|
|
3318
|
-
function
|
|
3319
|
-
const
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3080
|
+
function sampleCpu(nowMs = Date.now()) {
|
|
3081
|
+
for (const e of entries.values()) {
|
|
3082
|
+
if (!e.cpuAttributable) {
|
|
3083
|
+
e.cpuPct = null;
|
|
3084
|
+
continue;
|
|
3085
|
+
}
|
|
3086
|
+
let childUs = 0;
|
|
3087
|
+
for (const pid of [...e.children]) {
|
|
3088
|
+
const us = cpuIo.childCpuUs(pid);
|
|
3089
|
+
if (us === null) {
|
|
3090
|
+
e.children.delete(pid);
|
|
3091
|
+
continue;
|
|
3092
|
+
}
|
|
3093
|
+
childUs += us;
|
|
3094
|
+
}
|
|
3095
|
+
e.lastChildCpuUs = Math.max(e.lastChildCpuUs, childUs);
|
|
3096
|
+
const totalUs = e.cumulativeCpuUs + e.lastChildCpuUs;
|
|
3097
|
+
const p = e.prior;
|
|
3098
|
+
if (p) {
|
|
3099
|
+
const wallUs = (nowMs - p.atMs) * 1e3;
|
|
3100
|
+
e.cpuPct = wallUs > 0 && totalUs >= p.cpuUs ? (totalUs - p.cpuUs) / wallUs * 100 : null;
|
|
3101
|
+
} else {
|
|
3102
|
+
e.cpuPct = null;
|
|
3103
|
+
}
|
|
3104
|
+
e.prior = { cpuUs: totalUs, atMs: nowMs };
|
|
3105
|
+
}
|
|
3329
3106
|
}
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3107
|
+
var processPrior = null;
|
|
3108
|
+
function sampleProcessCpu(nowMs = Date.now()) {
|
|
3109
|
+
const cpuUs = cpuIo.ownCpuUs();
|
|
3110
|
+
let processPct = null;
|
|
3111
|
+
if (processPrior) {
|
|
3112
|
+
const wallUs = (nowMs - processPrior.atMs) * 1e3;
|
|
3113
|
+
if (wallUs > 0) processPct = (cpuUs - processPrior.cpuUs) / wallUs * 100;
|
|
3114
|
+
}
|
|
3115
|
+
processPrior = { cpuUs, atMs: nowMs };
|
|
3116
|
+
const rows = [...entries.values()];
|
|
3117
|
+
const measured = rows.filter((e) => e.cpuPct !== null);
|
|
3118
|
+
const attributedPct = measured.length === 0 ? null : measured.reduce((sum, e) => sum + (e.cpuPct ?? 0), 0);
|
|
3119
|
+
const unattributedPct = processPct === null || attributedPct === null ? null : Math.max(0, processPct - attributedPct);
|
|
3120
|
+
return { processPct, attributedPct, unattributedPct };
|
|
3333
3121
|
}
|
|
3334
|
-
function
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3122
|
+
function messageOf(err) {
|
|
3123
|
+
return err instanceof Error ? err.message : String(err);
|
|
3124
|
+
}
|
|
3125
|
+
function finish(e, startedAt, cpuStart, cpuAttributable, note, err) {
|
|
3126
|
+
const cpu = process.cpuUsage(cpuStart);
|
|
3127
|
+
e.running = false;
|
|
3128
|
+
e.lastDurationMs = Date.now() - startedAt;
|
|
3129
|
+
e.lastCpuMs = cpuAttributable ? (cpu.user + cpu.system) / 1e3 : null;
|
|
3130
|
+
if (err !== null) {
|
|
3131
|
+
e.lastOutcome = "error";
|
|
3132
|
+
e.lastError = messageOf(err);
|
|
3133
|
+
e.lastNote = null;
|
|
3134
|
+
e.consecutiveFailures += 1;
|
|
3135
|
+
console.error(
|
|
3136
|
+
`${TAG} op=run name=${e.spec.name} outcome=error streak=${e.consecutiveFailures} ms=${e.lastDurationMs} err="${e.lastError}"`
|
|
3137
|
+
);
|
|
3138
|
+
return;
|
|
3342
3139
|
}
|
|
3343
|
-
|
|
3140
|
+
e.lastOutcome = "ok";
|
|
3141
|
+
e.lastError = null;
|
|
3142
|
+
e.lastNote = typeof note === "string" && note.length > 0 ? note : null;
|
|
3143
|
+
e.lastSuccessMs = Date.now();
|
|
3144
|
+
e.successCount += 1;
|
|
3145
|
+
e.consecutiveFailures = 0;
|
|
3344
3146
|
}
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3147
|
+
function invoke(e) {
|
|
3148
|
+
if (e.running) {
|
|
3149
|
+
console.warn(`${TAG} op=skip name=${e.spec.name} reason=still-running`);
|
|
3150
|
+
return;
|
|
3151
|
+
}
|
|
3152
|
+
e.running = true;
|
|
3153
|
+
e.runCount += 1;
|
|
3154
|
+
e.lastStartMs = Date.now();
|
|
3155
|
+
const startedAt = e.lastStartMs;
|
|
3156
|
+
const cpuStart = process.cpuUsage();
|
|
3157
|
+
const ownStartUs = cpuIo.ownCpuUs();
|
|
3158
|
+
const ctx = {
|
|
3159
|
+
child: (pid) => {
|
|
3160
|
+
if (typeof pid === "number" && Number.isFinite(pid)) e.children.add(pid);
|
|
3161
|
+
}
|
|
3162
|
+
};
|
|
3163
|
+
let result;
|
|
3164
|
+
try {
|
|
3165
|
+
result = e.spec.run(ctx);
|
|
3166
|
+
} catch (err) {
|
|
3167
|
+
e.cumulativeCpuUs += Math.max(0, cpuIo.ownCpuUs() - ownStartUs);
|
|
3168
|
+
finish(e, startedAt, cpuStart, true, null, err);
|
|
3169
|
+
return;
|
|
3170
|
+
}
|
|
3171
|
+
if (result instanceof Promise) {
|
|
3172
|
+
e.cpuAttributable = false;
|
|
3173
|
+
result.then(
|
|
3174
|
+
(note) => finish(e, startedAt, cpuStart, false, note, null),
|
|
3175
|
+
(err) => finish(e, startedAt, cpuStart, false, null, err)
|
|
3176
|
+
);
|
|
3177
|
+
return;
|
|
3178
|
+
}
|
|
3179
|
+
e.cumulativeCpuUs += Math.max(0, cpuIo.ownCpuUs() - ownStartUs);
|
|
3180
|
+
finish(e, startedAt, cpuStart, true, result, null);
|
|
3351
3181
|
}
|
|
3352
|
-
function
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
if (!rec) return null;
|
|
3356
|
-
if (rec.lockedUntil > Date.now()) {
|
|
3357
|
-
const remaining = Math.ceil((rec.lockedUntil - Date.now()) / 1e3);
|
|
3358
|
-
return `Too many attempts. Try again in ${remaining}s`;
|
|
3182
|
+
function registerLoop(spec) {
|
|
3183
|
+
if (entries.has(spec.name)) {
|
|
3184
|
+
throw new Error(`${TAG} loop "${spec.name}" is already registered`);
|
|
3359
3185
|
}
|
|
3360
|
-
|
|
3361
|
-
|
|
3186
|
+
const e = {
|
|
3187
|
+
spec,
|
|
3188
|
+
armedAtMs: Date.now(),
|
|
3189
|
+
lastStartMs: null,
|
|
3190
|
+
lastSuccessMs: null,
|
|
3191
|
+
lastDurationMs: null,
|
|
3192
|
+
lastCpuMs: null,
|
|
3193
|
+
lastOutcome: null,
|
|
3194
|
+
lastError: null,
|
|
3195
|
+
lastNote: null,
|
|
3196
|
+
runCount: 0,
|
|
3197
|
+
successCount: 0,
|
|
3198
|
+
consecutiveFailures: 0,
|
|
3199
|
+
running: false,
|
|
3200
|
+
timers: [],
|
|
3201
|
+
cumulativeCpuUs: 0,
|
|
3202
|
+
cpuAttributable: true,
|
|
3203
|
+
children: /* @__PURE__ */ new Set(),
|
|
3204
|
+
lastChildCpuUs: 0,
|
|
3205
|
+
prior: null,
|
|
3206
|
+
cpuPct: null
|
|
3207
|
+
};
|
|
3208
|
+
entries.set(spec.name, e);
|
|
3209
|
+
const interval = setInterval(() => invoke(e), spec.intervalMs);
|
|
3210
|
+
if (typeof interval.unref === "function") interval.unref();
|
|
3211
|
+
e.timers.push(interval);
|
|
3212
|
+
console.log(
|
|
3213
|
+
`${TAG} op=arm name=${spec.name} intervalMs=${spec.intervalMs} firstRunMs=${spec.firstRunDelayMs ?? "interval"} detached=${spec.detached === true} armed=${entries.size}`
|
|
3214
|
+
);
|
|
3215
|
+
if (spec.firstRunDelayMs === 0) {
|
|
3216
|
+
invoke(e);
|
|
3217
|
+
} else if (spec.firstRunDelayMs !== void 0) {
|
|
3218
|
+
const first = setTimeout(() => invoke(e), spec.firstRunDelayMs);
|
|
3219
|
+
if (typeof first.unref === "function") first.unref();
|
|
3220
|
+
e.timers.push(first);
|
|
3362
3221
|
}
|
|
3363
|
-
|
|
3222
|
+
let stopped = false;
|
|
3223
|
+
return () => {
|
|
3224
|
+
if (stopped) return;
|
|
3225
|
+
stopped = true;
|
|
3226
|
+
for (const t of e.timers) clearInterval(t);
|
|
3227
|
+
if (entries.get(spec.name) === e) entries.delete(spec.name);
|
|
3228
|
+
console.log(`${TAG} op=disarm name=${spec.name} armed=${entries.size}`);
|
|
3229
|
+
};
|
|
3364
3230
|
}
|
|
3365
|
-
function
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
rec.count++;
|
|
3369
|
-
if (rec.count >= MAX_ATTEMPTS) {
|
|
3370
|
-
rec.lockedUntil = Date.now() + LOCKOUT_MS;
|
|
3371
|
-
rec.count = 0;
|
|
3231
|
+
function stateOf(e, nowMs) {
|
|
3232
|
+
if (e.lastStartMs === null) {
|
|
3233
|
+
return nowMs - e.armedAtMs < e.spec.intervalMs ? "waiting" : "never-run";
|
|
3372
3234
|
}
|
|
3373
|
-
|
|
3235
|
+
const overdue = nowMs - e.lastStartMs > e.spec.intervalMs * STALL_INTERVALS;
|
|
3236
|
+
if (overdue) return "stalled";
|
|
3237
|
+
if (e.running) return "running";
|
|
3238
|
+
if (e.successCount === 0) return "never-succeeded";
|
|
3239
|
+
if (e.consecutiveFailures > 0) return "failing";
|
|
3240
|
+
return "ok";
|
|
3374
3241
|
}
|
|
3375
|
-
function
|
|
3376
|
-
|
|
3242
|
+
function nextDueOf(e, nowMs) {
|
|
3243
|
+
const elapsed = Math.max(0, nowMs - e.armedAtMs);
|
|
3244
|
+
const ticks = Math.floor(elapsed / e.spec.intervalMs) + 1;
|
|
3245
|
+
return e.armedAtMs + ticks * e.spec.intervalMs;
|
|
3246
|
+
}
|
|
3247
|
+
function listLoops(nowMs = Date.now()) {
|
|
3248
|
+
return [...entries.values()].map((e) => ({
|
|
3249
|
+
name: e.spec.name,
|
|
3250
|
+
intervalMs: e.spec.intervalMs,
|
|
3251
|
+
detached: e.spec.detached === true,
|
|
3252
|
+
armedAtMs: e.armedAtMs,
|
|
3253
|
+
lastStartMs: e.lastStartMs,
|
|
3254
|
+
lastSuccessMs: e.lastSuccessMs,
|
|
3255
|
+
lastDurationMs: e.lastDurationMs,
|
|
3256
|
+
lastCpuMs: e.lastCpuMs,
|
|
3257
|
+
cpuPct: e.cpuPct,
|
|
3258
|
+
cpuAttributable: e.cpuAttributable,
|
|
3259
|
+
lastOutcome: e.lastOutcome,
|
|
3260
|
+
lastError: e.lastError,
|
|
3261
|
+
lastNote: e.lastNote,
|
|
3262
|
+
runCount: e.runCount,
|
|
3263
|
+
successCount: e.successCount,
|
|
3264
|
+
consecutiveFailures: e.consecutiveFailures,
|
|
3265
|
+
running: e.running,
|
|
3266
|
+
state: stateOf(e, nowMs),
|
|
3267
|
+
nextDueMs: nextDueOf(e, nowMs)
|
|
3268
|
+
}));
|
|
3269
|
+
}
|
|
3270
|
+
function armedCount() {
|
|
3271
|
+
return entries.size;
|
|
3377
3272
|
}
|
|
3378
|
-
registerLoop({
|
|
3379
|
-
name: "rate-limit-cleanup",
|
|
3380
|
-
intervalMs: 15 * 60 * 1e3,
|
|
3381
|
-
run: () => {
|
|
3382
|
-
const now = Date.now();
|
|
3383
|
-
let purged = 0;
|
|
3384
|
-
for (const [key, rec] of rateLimitMap) {
|
|
3385
|
-
if (rec.lockedUntil > 0 && rec.lockedUntil <= now) {
|
|
3386
|
-
rateLimitMap.delete(key);
|
|
3387
|
-
purged += 1;
|
|
3388
|
-
}
|
|
3389
|
-
}
|
|
3390
|
-
return `purged ${purged}, ${rateLimitMap.size} tracked`;
|
|
3391
|
-
}
|
|
3392
|
-
});
|
|
3393
|
-
function renderLoginPage(opts) {
|
|
3394
|
-
const error = opts?.error ?? "";
|
|
3395
|
-
const lockout = opts?.lockoutSeconds ?? 0;
|
|
3396
|
-
const redirect = opts?.redirect ?? "/";
|
|
3397
|
-
const mode = opts?.mode ?? "login";
|
|
3398
|
-
const changeError = opts?.changeError ?? "";
|
|
3399
|
-
const success = opts?.success ?? "";
|
|
3400
|
-
const setupError = opts?.setupError ?? "";
|
|
3401
|
-
const primaryColor = opts?.primaryColor ?? "#7C8C72";
|
|
3402
|
-
const primaryHoverColor = opts?.primaryHoverColor ?? "#6A7A62";
|
|
3403
|
-
const primarySubtle = opts?.primarySubtle ?? "rgba(124,140,114,0.08)";
|
|
3404
|
-
const productName = opts?.productName ?? "Maxy";
|
|
3405
|
-
const logoPath = opts?.logoPath ?? "/brand/maxy-monochrome.png";
|
|
3406
|
-
const faviconPath = opts?.faviconPath ?? "/favicon.ico";
|
|
3407
|
-
const displayFont = opts?.displayFont ?? "'Newsreader', Georgia, serif";
|
|
3408
|
-
const bodyFont = opts?.bodyFont ?? "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
|
|
3409
|
-
const logoContainsName = opts?.logoContainsName ?? false;
|
|
3410
|
-
const tagline = opts?.tagline ?? "";
|
|
3411
|
-
const origin = opts?.origin ?? "";
|
|
3412
|
-
const ogCardPath = opts?.ogCardPath ?? "";
|
|
3413
|
-
const manifestPath = opts?.manifestPath ?? "";
|
|
3414
|
-
const appleTouchIconPath = opts?.appleTouchIconPath ?? "";
|
|
3415
|
-
const pwaThemeColor = opts?.themeColor ?? "";
|
|
3416
|
-
const ogBlock = origin && ogCardPath ? [
|
|
3417
|
-
`<meta property="og:title" content="Sign in \u2014 ${escapeHtml(productName)}">`,
|
|
3418
|
-
`<meta property="og:description" content="${escapeHtml(tagline)}">`,
|
|
3419
|
-
`<meta property="og:type" content="website">`,
|
|
3420
|
-
`<meta property="og:url" content="${escapeHtml(origin)}">`,
|
|
3421
|
-
`<meta property="og:image" content="${escapeHtml(origin + ogCardPath)}">`,
|
|
3422
|
-
`<meta property="og:image:width" content="1200">`,
|
|
3423
|
-
`<meta property="og:image:height" content="630">`,
|
|
3424
|
-
`<meta name="twitter:card" content="summary_large_image">`,
|
|
3425
|
-
`<meta name="twitter:title" content="Sign in \u2014 ${escapeHtml(productName)}">`,
|
|
3426
|
-
`<meta name="twitter:description" content="${escapeHtml(tagline)}">`,
|
|
3427
|
-
`<meta name="twitter:image" content="${escapeHtml(origin + ogCardPath)}">`
|
|
3428
|
-
].join("\n ") : "";
|
|
3429
|
-
const pwaInstallBlock = manifestPath && appleTouchIconPath ? [
|
|
3430
|
-
`<link rel="manifest" href="${escapeHtml(manifestPath)}">`,
|
|
3431
|
-
`<link rel="apple-touch-icon" href="${escapeHtml(appleTouchIconPath)}">`,
|
|
3432
|
-
`<meta name="mobile-web-app-capable" content="yes">`,
|
|
3433
|
-
`<meta name="apple-mobile-web-app-capable" content="yes">`,
|
|
3434
|
-
pwaThemeColor ? `<meta name="theme-color" content="${escapeHtml(pwaThemeColor)}">` : ""
|
|
3435
|
-
].filter(Boolean).join("\n ") : "";
|
|
3436
|
-
const errorHtml = error ? `<p class="msg msg--error">${escapeHtml(error)}</p>` : "";
|
|
3437
|
-
const changeErrorHtml = changeError ? `<p class="msg msg--error">${escapeHtml(changeError)}</p>` : "";
|
|
3438
|
-
const successHtml = success ? `<p class="msg msg--success">${escapeHtml(success)}</p>` : "";
|
|
3439
|
-
const lockoutHtml = lockout > 0 ? `<p class="msg msg--error">Too many attempts. Try again in <span id="countdown">${lockout}</span>s</p>
|
|
3440
|
-
<script>
|
|
3441
|
-
(function() {
|
|
3442
|
-
var s = ${lockout};
|
|
3443
|
-
var el = document.getElementById('countdown');
|
|
3444
|
-
var iv = setInterval(function() { s--; el.textContent = s; if (s <= 0) { clearInterval(iv); location.reload(); } }, 1000);
|
|
3445
|
-
})();
|
|
3446
|
-
</script>` : "";
|
|
3447
|
-
const setupErrorHtml = setupError ? `<p class="msg msg--error">${escapeHtml(setupError)}</p>` : "";
|
|
3448
|
-
const formDisabled = lockout > 0 ? "disabled" : "";
|
|
3449
|
-
const loginDisplay = mode === "login" ? "block" : "none";
|
|
3450
|
-
const changeDisplay = mode === "change" ? "block" : "none";
|
|
3451
|
-
const setupDisplay = mode === "setup" ? "block" : "none";
|
|
3452
|
-
const successDisplay = mode === "success" ? "block" : "none";
|
|
3453
|
-
const subtitleText = mode === "setup" ? "Set your remote password" : "Remote access";
|
|
3454
|
-
const displayFontName = displayFont.match(/^'([^']+)'/)?.[1] ?? "";
|
|
3455
|
-
const bodyFontName = bodyFont.match(/^'([^']+)'/)?.[1] ?? "";
|
|
3456
|
-
const googleFontsLink = displayFontName || bodyFontName ? [
|
|
3457
|
-
'<link rel="preconnect" href="https://fonts.googleapis.com">',
|
|
3458
|
-
'<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>',
|
|
3459
|
-
'<link href="https://fonts.googleapis.com/css2?' + [
|
|
3460
|
-
displayFontName ? `family=${displayFontName.replace(/ /g, "+")}:wght@300;400` : "",
|
|
3461
|
-
bodyFontName ? `family=${bodyFontName.replace(/ /g, "+")}:wght@400;500` : ""
|
|
3462
|
-
].filter(Boolean).join("&") + '&display=swap" rel="stylesheet">'
|
|
3463
|
-
].join("\n ") : "";
|
|
3464
|
-
return `<!DOCTYPE html>
|
|
3465
|
-
<html lang="en">
|
|
3466
|
-
<head>
|
|
3467
|
-
<meta charset="utf-8">
|
|
3468
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
3469
|
-
<title>Sign in \u2014 ${escapeHtml(productName)}</title>
|
|
3470
|
-
<link rel="icon" href="${escapeHtml(faviconPath)}">
|
|
3471
|
-
${pwaInstallBlock}
|
|
3472
|
-
${ogBlock}
|
|
3473
|
-
${googleFontsLink}
|
|
3474
|
-
<style>
|
|
3475
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
3476
|
-
body {
|
|
3477
|
-
font-family: ${bodyFont};
|
|
3478
|
-
background: #F6F5F2;
|
|
3479
|
-
color: #2A2D38;
|
|
3480
|
-
min-height: 100vh;
|
|
3481
|
-
display: flex;
|
|
3482
|
-
align-items: center;
|
|
3483
|
-
justify-content: center;
|
|
3484
|
-
}
|
|
3485
|
-
.page {
|
|
3486
|
-
display: flex;
|
|
3487
|
-
flex-direction: column;
|
|
3488
|
-
align-items: center;
|
|
3489
|
-
gap: 24px;
|
|
3490
|
-
max-width: 420px;
|
|
3491
|
-
width: 100%;
|
|
3492
|
-
padding: 40px 20px;
|
|
3493
|
-
}
|
|
3494
|
-
.logo-img {
|
|
3495
|
-
width: 110px;
|
|
3496
|
-
height: 110px;
|
|
3497
|
-
margin: -11px;
|
|
3498
|
-
object-fit: contain;
|
|
3499
|
-
}
|
|
3500
|
-
.title {
|
|
3501
|
-
font-family: ${displayFont};
|
|
3502
|
-
font-weight: 300;
|
|
3503
|
-
font-size: 28px;
|
|
3504
|
-
color: #2A2D38;
|
|
3505
|
-
text-align: center;
|
|
3506
|
-
letter-spacing: -0.01em;
|
|
3507
|
-
line-height: 1.3;
|
|
3508
|
-
}
|
|
3509
|
-
.subtitle {
|
|
3510
|
-
font-size: 15px;
|
|
3511
|
-
color: #4A4D58;
|
|
3512
|
-
text-align: center;
|
|
3513
|
-
margin-top: -8px;
|
|
3514
|
-
}
|
|
3515
|
-
.form-wrap {
|
|
3516
|
-
width: 100%;
|
|
3517
|
-
max-width: 300px;
|
|
3518
|
-
}
|
|
3519
|
-
.field { margin-bottom: 12px; }
|
|
3520
|
-
.field-label {
|
|
3521
|
-
display: block;
|
|
3522
|
-
font-size: 13px;
|
|
3523
|
-
font-weight: 500;
|
|
3524
|
-
color: #4A4D58;
|
|
3525
|
-
margin-bottom: 6px;
|
|
3526
|
-
}
|
|
3527
|
-
.field-input {
|
|
3528
|
-
width: 100%;
|
|
3529
|
-
padding: 10px 12px;
|
|
3530
|
-
border: 1px solid #E4E3DE;
|
|
3531
|
-
border-radius: 8px;
|
|
3532
|
-
font-size: 15px;
|
|
3533
|
-
font-family: inherit;
|
|
3534
|
-
outline: none;
|
|
3535
|
-
background: #fff;
|
|
3536
|
-
transition: border-color 0.15s, box-shadow 0.15s;
|
|
3537
|
-
}
|
|
3538
|
-
.field-input:focus {
|
|
3539
|
-
border-color: ${primaryColor};
|
|
3540
|
-
box-shadow: 0 0 0 3px ${primarySubtle};
|
|
3541
|
-
}
|
|
3542
|
-
.options {
|
|
3543
|
-
display: flex;
|
|
3544
|
-
align-items: center;
|
|
3545
|
-
justify-content: space-between;
|
|
3546
|
-
gap: 12px;
|
|
3547
|
-
margin-top: 8px;
|
|
3548
|
-
}
|
|
3549
|
-
|
|
3550
|
-
/* Checkbox \u2014 matches Maxy shared checkbox (asterisk-in-square) */
|
|
3551
|
-
.check {
|
|
3552
|
-
position: relative;
|
|
3553
|
-
display: flex;
|
|
3554
|
-
align-items: center;
|
|
3555
|
-
gap: 8px;
|
|
3556
|
-
cursor: pointer;
|
|
3557
|
-
user-select: none;
|
|
3558
|
-
}
|
|
3559
|
-
.check input { position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none; }
|
|
3560
|
-
.check-box {
|
|
3561
|
-
width: 14px;
|
|
3562
|
-
height: 14px;
|
|
3563
|
-
border: 1px solid #E4E3DE;
|
|
3564
|
-
border-radius: 2px;
|
|
3565
|
-
display: flex;
|
|
3566
|
-
align-items: center;
|
|
3567
|
-
justify-content: center;
|
|
3568
|
-
font-size: 10px;
|
|
3569
|
-
color: transparent;
|
|
3570
|
-
transition: border-color 0.15s, color 0.15s;
|
|
3571
|
-
flex-shrink: 0;
|
|
3572
|
-
}
|
|
3573
|
-
.check input:checked + .check-box {
|
|
3574
|
-
border-color: ${primaryColor};
|
|
3575
|
-
color: ${primaryColor};
|
|
3576
|
-
}
|
|
3577
|
-
.check-label {
|
|
3578
|
-
font-size: 13px;
|
|
3579
|
-
color: #4A4D58;
|
|
3580
|
-
}
|
|
3581
|
-
|
|
3582
|
-
/* Ghost button \u2014 matches Maxy ghost variant */
|
|
3583
|
-
.ghost {
|
|
3584
|
-
background: none;
|
|
3585
|
-
border: none;
|
|
3586
|
-
font-family: inherit;
|
|
3587
|
-
font-size: 13px;
|
|
3588
|
-
color: #4A4D58;
|
|
3589
|
-
cursor: pointer;
|
|
3590
|
-
padding: 4px 0;
|
|
3591
|
-
transition: color 0.15s;
|
|
3592
|
-
}
|
|
3593
|
-
.ghost:hover { color: #2A2D38; }
|
|
3594
|
-
|
|
3595
|
-
/* Primary button */
|
|
3596
|
-
.btn {
|
|
3597
|
-
width: 100%;
|
|
3598
|
-
padding: 12px;
|
|
3599
|
-
margin-top: 16px;
|
|
3600
|
-
background: ${primaryColor};
|
|
3601
|
-
color: #fff;
|
|
3602
|
-
border: none;
|
|
3603
|
-
border-radius: 10px;
|
|
3604
|
-
font-size: 15px;
|
|
3605
|
-
font-weight: 500;
|
|
3606
|
-
font-family: inherit;
|
|
3607
|
-
cursor: pointer;
|
|
3608
|
-
transition: background 0.15s;
|
|
3609
|
-
}
|
|
3610
|
-
.btn:hover:not(:disabled) { background: ${primaryHoverColor}; }
|
|
3611
|
-
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
3612
|
-
|
|
3613
|
-
/* Messages */
|
|
3614
|
-
.msg {
|
|
3615
|
-
font-size: 13px;
|
|
3616
|
-
text-align: center;
|
|
3617
|
-
margin-top: 8px;
|
|
3618
|
-
}
|
|
3619
|
-
.msg--error { color: #c44; }
|
|
3620
|
-
.msg--success { color: ${primaryColor}; }
|
|
3621
|
-
|
|
3622
|
-
/* Strength checklist (setup mode) */
|
|
3623
|
-
.strength-checklist { margin: 8px 0; }
|
|
3624
|
-
.strength-item {
|
|
3625
|
-
font-size: 13px;
|
|
3626
|
-
color: #4A4D58;
|
|
3627
|
-
padding: 2px 0;
|
|
3628
|
-
transition: color 0.15s;
|
|
3629
|
-
}
|
|
3630
|
-
.strength-icon {
|
|
3631
|
-
display: inline-block;
|
|
3632
|
-
width: 16px;
|
|
3633
|
-
text-align: center;
|
|
3634
|
-
}
|
|
3635
|
-
|
|
3636
|
-
@media (max-width: 440px) {
|
|
3637
|
-
.page { padding: 24px 16px; }
|
|
3638
|
-
}
|
|
3639
|
-
</style>
|
|
3640
|
-
</head>
|
|
3641
|
-
<body>
|
|
3642
|
-
<div class="page">
|
|
3643
|
-
<img src="${escapeHtml(logoPath)}" alt="${escapeHtml(productName)}" class="logo-img">
|
|
3644
|
-
<h1 class="title"${logoContainsName ? ' style="display:none"' : ""}>${escapeHtml(productName)}</h1>
|
|
3645
|
-
<p class="subtitle">${escapeHtml(subtitleText)}</p>
|
|
3646
|
-
|
|
3647
|
-
${successHtml}
|
|
3648
|
-
|
|
3649
|
-
<!-- Login -->
|
|
3650
|
-
<div id="login-section" class="form-wrap" style="display:${loginDisplay}">
|
|
3651
|
-
<form method="POST" action="/__remote-auth/login">
|
|
3652
|
-
<input type="hidden" name="redirect" value="${escapeHtml(redirect)}">
|
|
3653
|
-
<div class="field">
|
|
3654
|
-
<label class="field-label" for="password">Password</label>
|
|
3655
|
-
<input class="field-input" type="password" id="password" name="password" required autofocus ${formDisabled}>
|
|
3656
|
-
</div>
|
|
3657
|
-
<div class="options">
|
|
3658
|
-
<label class="check">
|
|
3659
|
-
<input type="checkbox" id="show-login-pw">
|
|
3660
|
-
<span class="check-box">\u2731</span>
|
|
3661
|
-
<span class="check-label">Show password</span>
|
|
3662
|
-
</label>
|
|
3663
|
-
<button type="button" class="ghost" onclick="toggleMode('change')">Change password</button>
|
|
3664
|
-
</div>
|
|
3665
|
-
<button type="submit" class="btn" ${formDisabled}>Sign in</button>
|
|
3666
|
-
</form>
|
|
3667
|
-
${errorHtml}
|
|
3668
|
-
${lockoutHtml}
|
|
3669
|
-
</div>
|
|
3670
|
-
|
|
3671
|
-
<!-- Change password -->
|
|
3672
|
-
<div id="change-section" class="form-wrap" style="display:${changeDisplay}">
|
|
3673
|
-
<form method="POST" action="/__remote-auth/change-password">
|
|
3674
|
-
<input type="hidden" name="redirect" value="${escapeHtml(redirect)}">
|
|
3675
|
-
<div class="field">
|
|
3676
|
-
<label class="field-label" for="current_password">Current password</label>
|
|
3677
|
-
<input class="field-input" type="password" id="current_password" name="current_password" required ${formDisabled}>
|
|
3678
|
-
</div>
|
|
3679
|
-
<div class="field">
|
|
3680
|
-
<label class="field-label" for="new_password">New password</label>
|
|
3681
|
-
<input class="field-input" type="password" id="new_password" name="new_password" required ${formDisabled}>
|
|
3682
|
-
</div>
|
|
3683
|
-
<div class="field">
|
|
3684
|
-
<label class="field-label" for="confirm_password">Confirm new password</label>
|
|
3685
|
-
<input class="field-input" type="password" id="confirm_password" name="confirm_password" required ${formDisabled}>
|
|
3686
|
-
</div>
|
|
3687
|
-
<div class="options">
|
|
3688
|
-
<label class="check">
|
|
3689
|
-
<input type="checkbox" id="show-change-pw">
|
|
3690
|
-
<span class="check-box">\u2731</span>
|
|
3691
|
-
<span class="check-label">Show passwords</span>
|
|
3692
|
-
</label>
|
|
3693
|
-
<button type="button" class="ghost" onclick="toggleMode('login')">Back to sign in</button>
|
|
3694
|
-
</div>
|
|
3695
|
-
<button type="submit" class="btn" ${formDisabled}>Change password</button>
|
|
3696
|
-
</form>
|
|
3697
|
-
${changeErrorHtml}
|
|
3698
|
-
</div>
|
|
3699
|
-
|
|
3700
|
-
<!-- Setup (initial password) -->
|
|
3701
|
-
<div id="setup-section" class="form-wrap" style="display:${setupDisplay}">
|
|
3702
|
-
<form method="POST" action="/__remote-auth/set-initial-password">
|
|
3703
|
-
<div class="field">
|
|
3704
|
-
<label class="field-label" for="setup_password">Password</label>
|
|
3705
|
-
<input class="field-input" type="password" id="setup_password" name="password" required autofocus>
|
|
3706
|
-
</div>
|
|
3707
|
-
<div class="field">
|
|
3708
|
-
<label class="field-label" for="setup_confirm">Confirm password</label>
|
|
3709
|
-
<input class="field-input" type="password" id="setup_confirm" name="confirm_password" required>
|
|
3710
|
-
</div>
|
|
3711
|
-
<div id="strength-checklist" class="strength-checklist">
|
|
3712
|
-
<div class="strength-item" id="req-length"><span class="strength-icon">\u25CB</span> At least 8 characters</div>
|
|
3713
|
-
<div class="strength-item" id="req-number"><span class="strength-icon">\u25CB</span> Contains a number</div>
|
|
3714
|
-
<div class="strength-item" id="req-special"><span class="strength-icon">\u25CB</span> Contains a special character</div>
|
|
3715
|
-
<div class="strength-item" id="req-spaces"><span class="strength-icon">\u25CB</span> No spaces</div>
|
|
3716
|
-
</div>
|
|
3717
|
-
<div class="options">
|
|
3718
|
-
<label class="check">
|
|
3719
|
-
<input type="checkbox" id="show-setup-pw">
|
|
3720
|
-
<span class="check-box">\u2731</span>
|
|
3721
|
-
<span class="check-label">Show passwords</span>
|
|
3722
|
-
</label>
|
|
3723
|
-
</div>
|
|
3724
|
-
<button type="submit" class="btn" id="setup-submit" disabled>Set password</button>
|
|
3725
|
-
</form>
|
|
3726
|
-
${setupErrorHtml}
|
|
3727
|
-
</div>
|
|
3728
|
-
|
|
3729
|
-
<!-- Success (after initial password set) -->
|
|
3730
|
-
<div id="success-section" class="form-wrap" style="display:${successDisplay}">
|
|
3731
|
-
<p class="msg msg--success" style="font-size:15px;margin-bottom:4px;">\u2713 Password set</p>
|
|
3732
|
-
<p style="font-size:14px;color:#4A4D58;text-align:center;">You can close this tab and return to the onboarding tab.</p>
|
|
3733
|
-
<a href="/" style="display:block;text-align:center;margin-top:16px;font-size:13px;color:#4A4D58;text-decoration:none;" onmouseover="this.style.color='#2A2D38'" onmouseout="this.style.color='#4A4D58'">Sign in \u2192</a>
|
|
3734
|
-
<script>
|
|
3735
|
-
(function() {
|
|
3736
|
-
try {
|
|
3737
|
-
var ch = new BroadcastChannel('platform-onboarding');
|
|
3738
|
-
setTimeout(function() {
|
|
3739
|
-
ch.postMessage({ type: 'remote-password-set' });
|
|
3740
|
-
ch.close();
|
|
3741
|
-
}, 2500);
|
|
3742
|
-
} catch(e) {}
|
|
3743
|
-
})();
|
|
3744
|
-
</script>
|
|
3745
|
-
</div>
|
|
3746
|
-
</div>
|
|
3747
|
-
|
|
3748
|
-
<script>
|
|
3749
|
-
/* Toggle between login and change-password modes */
|
|
3750
|
-
function toggleMode(mode) {
|
|
3751
|
-
document.getElementById('login-section').style.display = mode === 'login' ? 'block' : 'none';
|
|
3752
|
-
document.getElementById('change-section').style.display = mode === 'change' ? 'block' : 'none';
|
|
3753
|
-
/* Focus first input in the active section */
|
|
3754
|
-
var id = mode === 'login' ? 'password' : 'current_password';
|
|
3755
|
-
var el = document.getElementById(id);
|
|
3756
|
-
if (el) el.focus();
|
|
3757
|
-
}
|
|
3758
|
-
|
|
3759
|
-
/* Show/hide password toggles */
|
|
3760
|
-
function bindShowHide(checkboxId, inputIds) {
|
|
3761
|
-
var cb = document.getElementById(checkboxId);
|
|
3762
|
-
if (!cb) return;
|
|
3763
|
-
cb.addEventListener('change', function() {
|
|
3764
|
-
var type = cb.checked ? 'text' : 'password';
|
|
3765
|
-
inputIds.forEach(function(id) {
|
|
3766
|
-
var el = document.getElementById(id);
|
|
3767
|
-
if (el) el.type = type;
|
|
3768
|
-
});
|
|
3769
|
-
});
|
|
3770
|
-
}
|
|
3771
|
-
bindShowHide('show-login-pw', ['password']);
|
|
3772
|
-
bindShowHide('show-change-pw', ['current_password', 'new_password', 'confirm_password']);
|
|
3773
|
-
bindShowHide('show-setup-pw', ['setup_password', 'setup_confirm']);
|
|
3774
|
-
|
|
3775
|
-
/* Explicit Enter-to-submit for the login password field */
|
|
3776
|
-
var loginPw = document.getElementById('password');
|
|
3777
|
-
if (loginPw) {
|
|
3778
|
-
loginPw.addEventListener('keydown', function(e) {
|
|
3779
|
-
if (e.key === 'Enter') {
|
|
3780
|
-
e.preventDefault();
|
|
3781
|
-
this.closest('form').requestSubmit();
|
|
3782
|
-
}
|
|
3783
|
-
});
|
|
3784
|
-
}
|
|
3785
|
-
|
|
3786
|
-
/* Live password strength validation (setup mode) */
|
|
3787
|
-
(function() {
|
|
3788
|
-
var pw = document.getElementById('setup_password');
|
|
3789
|
-
var confirm = document.getElementById('setup_confirm');
|
|
3790
|
-
var btn = document.getElementById('setup-submit');
|
|
3791
|
-
if (!pw || !confirm || !btn) return;
|
|
3792
|
-
|
|
3793
|
-
var rules = [
|
|
3794
|
-
{ id: 'req-length', test: function(p) { return p.length >= 8; } },
|
|
3795
|
-
{ id: 'req-number', test: function(p) { return /\\d/.test(p); } },
|
|
3796
|
-
{ id: 'req-special', test: function(p) { return /[^A-Za-z0-9]/.test(p); } },
|
|
3797
|
-
{ id: 'req-spaces', test: function(p) { return p.length > 0 && !/\\s/.test(p); } }
|
|
3798
|
-
];
|
|
3799
|
-
|
|
3800
|
-
function check() {
|
|
3801
|
-
var p = pw.value;
|
|
3802
|
-
var allMet = true;
|
|
3803
|
-
rules.forEach(function(r) {
|
|
3804
|
-
var el = document.getElementById(r.id);
|
|
3805
|
-
var met = r.test(p);
|
|
3806
|
-
if (!met) allMet = false;
|
|
3807
|
-
el.querySelector('.strength-icon').textContent = met ? '\\u2713' : '\\u25CB';
|
|
3808
|
-
el.style.color = met ? '${primaryColor}' : '#4A4D58';
|
|
3809
|
-
});
|
|
3810
|
-
var match = p.length > 0 && p === confirm.value;
|
|
3811
|
-
btn.disabled = !(allMet && match);
|
|
3812
|
-
}
|
|
3813
3273
|
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3274
|
+
// app/lib/vnc-logger.ts
|
|
3275
|
+
import { appendFileSync, mkdirSync } from "fs";
|
|
3276
|
+
import { resolve as resolve3 } from "path";
|
|
3277
|
+
var VNC_LOG_FILE = resolve3(LOG_DIR, "vnc-boot.log");
|
|
3278
|
+
try {
|
|
3279
|
+
mkdirSync(LOG_DIR, { recursive: true });
|
|
3280
|
+
} catch (err) {
|
|
3281
|
+
console.error(`[vnc-log-fail] mkdir ${LOG_DIR} failed: ${err.message}`);
|
|
3820
3282
|
}
|
|
3821
|
-
function
|
|
3822
|
-
|
|
3283
|
+
function vncLog(phase, fields = {}) {
|
|
3284
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
3285
|
+
const kv = Object.entries(fields).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(" ");
|
|
3286
|
+
const line = kv.length > 0 ? `[${ts}] [${phase}] ${kv}
|
|
3287
|
+
` : `[${ts}] [${phase}]
|
|
3288
|
+
`;
|
|
3289
|
+
try {
|
|
3290
|
+
appendFileSync(VNC_LOG_FILE, line);
|
|
3291
|
+
} catch (err) {
|
|
3292
|
+
console.error(`[vnc-log-fail] ${err.message} \u2014 dropped: ${line.slice(0, 300).trim()}`);
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
var corrCounter = 0;
|
|
3296
|
+
function newCorrId() {
|
|
3297
|
+
corrCounter = corrCounter + 1 & 4294967295;
|
|
3298
|
+
const counterHex = corrCounter.toString(16).padStart(8, "0");
|
|
3299
|
+
const randomHex = Math.floor(Math.random() * 16777215).toString(16).padStart(6, "0");
|
|
3300
|
+
return `${counterHex}-${randomHex}`;
|
|
3301
|
+
}
|
|
3302
|
+
function sanitizeClientCorrId(raw2) {
|
|
3303
|
+
if (!raw2) return null;
|
|
3304
|
+
if (raw2.length > 32) return null;
|
|
3305
|
+
if (!/^[A-Za-z0-9_-]+$/.test(raw2)) return null;
|
|
3306
|
+
return raw2;
|
|
3307
|
+
}
|
|
3308
|
+
function formatKv(fields) {
|
|
3309
|
+
return Object.entries(fields).filter(([, v]) => v !== void 0).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(" ");
|
|
3310
|
+
}
|
|
3311
|
+
function httpLog(fields) {
|
|
3312
|
+
console.error(`[http] ${formatKv(fields)}`);
|
|
3313
|
+
}
|
|
3314
|
+
function websockifyLog(event, fields = {}) {
|
|
3315
|
+
console.error(`[websockify] ${formatKv({ event, ...fields })}`);
|
|
3823
3316
|
}
|
|
3824
3317
|
|
|
3825
|
-
//
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3318
|
+
// ../lib/admin-access-password/src/index.ts
|
|
3319
|
+
import { scrypt, randomBytes, timingSafeEqual } from "crypto";
|
|
3320
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5, writeFileSync, renameSync, unlinkSync, appendFileSync as appendFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
3321
|
+
import { dirname } from "path";
|
|
3322
|
+
var SCRYPT_N = 16384;
|
|
3323
|
+
var SCRYPT_R = 8;
|
|
3324
|
+
var SCRYPT_P = 1;
|
|
3325
|
+
var SCRYPT_KEYLEN = 64;
|
|
3326
|
+
function id8(value) {
|
|
3327
|
+
return value.slice(0, 8);
|
|
3328
|
+
}
|
|
3329
|
+
function rowIds(users) {
|
|
3330
|
+
return users.map((u) => id8(u.userId)).join(",");
|
|
3331
|
+
}
|
|
3332
|
+
function appendUsersAuditLine(audit, fields) {
|
|
3333
|
+
const sess = audit.session ? ` session=${id8(audit.session)}` : "";
|
|
3334
|
+
const actor = audit.actor.includes("-") ? id8(audit.actor) : audit.actor;
|
|
3335
|
+
const targetFields = audit.target !== void 0 ? ` target=${audit.target.includes("-") ? id8(audit.target) : audit.target} explicitUserId=${audit.explicitUserId ? "true" : "false"}` : "";
|
|
3336
|
+
const line = `[users-audit] action=${fields.action} actor=${actor}${targetFields}${sess} field=${fields.field} rowsBefore=${fields.rowsBefore} rowsAfter=${fields.rowsAfter} ts=${(/* @__PURE__ */ new Date()).toISOString()}
|
|
3337
|
+
`;
|
|
3338
|
+
try {
|
|
3339
|
+
mkdirSync2(dirname(audit.logFile), { recursive: true, mode: 448 });
|
|
3340
|
+
appendFileSync2(audit.logFile, line, { mode: 384 });
|
|
3341
|
+
} catch (err) {
|
|
3342
|
+
console.error(
|
|
3343
|
+
`[users-audit] op=write-failed logFile=${audit.logFile} error=${err instanceof Error ? err.message : String(err)}`
|
|
3344
|
+
);
|
|
3830
3345
|
}
|
|
3831
|
-
|
|
3832
|
-
|
|
3346
|
+
}
|
|
3347
|
+
function scryptAsync(password, salt) {
|
|
3348
|
+
return new Promise((resolve7, reject) => {
|
|
3349
|
+
scrypt(password, salt, SCRYPT_KEYLEN, { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P }, (err, key) => {
|
|
3350
|
+
if (err) reject(err);
|
|
3351
|
+
else resolve7(key);
|
|
3352
|
+
});
|
|
3353
|
+
});
|
|
3354
|
+
}
|
|
3355
|
+
async function hashAccessPassword(password) {
|
|
3356
|
+
const salt = randomBytes(32);
|
|
3357
|
+
const hash = await scryptAsync(password, salt);
|
|
3358
|
+
return `${salt.toString("hex")}:${hash.toString("hex")}`;
|
|
3359
|
+
}
|
|
3360
|
+
async function matchesHash(password, stored) {
|
|
3361
|
+
const colon = stored.indexOf(":");
|
|
3362
|
+
if (colon === -1) return false;
|
|
3363
|
+
const salt = Buffer.from(stored.slice(0, colon), "hex");
|
|
3364
|
+
const storedHash = Buffer.from(stored.slice(colon + 1), "hex");
|
|
3365
|
+
if (storedHash.length !== SCRYPT_KEYLEN) return false;
|
|
3366
|
+
let computed;
|
|
3367
|
+
try {
|
|
3368
|
+
computed = await scryptAsync(password, salt);
|
|
3369
|
+
} catch {
|
|
3370
|
+
return false;
|
|
3833
3371
|
}
|
|
3834
|
-
|
|
3835
|
-
|
|
3372
|
+
return timingSafeEqual(computed, storedHash);
|
|
3373
|
+
}
|
|
3374
|
+
function readUsers(usersFile) {
|
|
3375
|
+
if (!existsSync4(usersFile)) return [];
|
|
3376
|
+
const raw2 = readFileSync5(usersFile, "utf-8").trim();
|
|
3377
|
+
if (!raw2) return [];
|
|
3378
|
+
const parsed = JSON.parse(raw2);
|
|
3379
|
+
if (!Array.isArray(parsed)) throw new Error("users.json is not an array");
|
|
3380
|
+
return parsed;
|
|
3381
|
+
}
|
|
3382
|
+
function writeUsersAtomic(usersFile, users) {
|
|
3383
|
+
const tmp = `${usersFile}.tmp-${process.pid}`;
|
|
3384
|
+
writeFileSync(tmp, JSON.stringify(users, null, 2) + "\n", { mode: 384 });
|
|
3385
|
+
renameSync(tmp, usersFile);
|
|
3386
|
+
}
|
|
3387
|
+
async function setAccessPassword(userId, password, usersFile, audit) {
|
|
3388
|
+
const users = readUsers(usersFile);
|
|
3389
|
+
const idx = users.findIndex((u) => u.userId === userId);
|
|
3390
|
+
if (idx === -1) throw new Error(`no users.json record for userId=${userId}`);
|
|
3391
|
+
if (await accessPasswordCollides(password, usersFile, userId)) {
|
|
3392
|
+
throw new Error("accessHash collision: password already in use by another admin");
|
|
3836
3393
|
}
|
|
3837
|
-
const
|
|
3838
|
-
|
|
3839
|
-
|
|
3394
|
+
const rows = rowIds(users);
|
|
3395
|
+
users[idx] = { ...users[idx], accessHash: await hashAccessPassword(password) };
|
|
3396
|
+
writeUsersAtomic(usersFile, users);
|
|
3397
|
+
appendUsersAuditLine(audit, { action: "set-access", field: "accessHash", rowsBefore: rows, rowsAfter: rows });
|
|
3398
|
+
}
|
|
3399
|
+
async function verifyAccessPassword(password, usersFile) {
|
|
3400
|
+
let users;
|
|
3401
|
+
try {
|
|
3402
|
+
users = readUsers(usersFile);
|
|
3403
|
+
} catch {
|
|
3404
|
+
return null;
|
|
3405
|
+
}
|
|
3406
|
+
for (const u of users) {
|
|
3407
|
+
if (typeof u.accessHash === "string" && await matchesHash(password, u.accessHash)) {
|
|
3408
|
+
return u.userId;
|
|
3409
|
+
}
|
|
3410
|
+
}
|
|
3411
|
+
return null;
|
|
3412
|
+
}
|
|
3413
|
+
async function accessPasswordCollides(password, usersFile, excludeUserId) {
|
|
3414
|
+
let users;
|
|
3415
|
+
try {
|
|
3416
|
+
users = readUsers(usersFile);
|
|
3417
|
+
} catch {
|
|
3418
|
+
return false;
|
|
3419
|
+
}
|
|
3420
|
+
for (const u of users) {
|
|
3421
|
+
if (u.userId === excludeUserId) continue;
|
|
3422
|
+
if (typeof u.accessHash === "string" && await matchesHash(password, u.accessHash)) {
|
|
3423
|
+
return true;
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3426
|
+
return false;
|
|
3427
|
+
}
|
|
3428
|
+
function anyAccessPasswordConfigured(usersFile) {
|
|
3429
|
+
let users;
|
|
3430
|
+
try {
|
|
3431
|
+
users = readUsers(usersFile);
|
|
3432
|
+
} catch {
|
|
3433
|
+
return false;
|
|
3434
|
+
}
|
|
3435
|
+
return users.some((u) => typeof u.accessHash === "string" && u.accessHash.length > 0);
|
|
3436
|
+
}
|
|
3437
|
+
function accessStoreUnreadable(usersFile) {
|
|
3438
|
+
if (!existsSync4(usersFile)) return false;
|
|
3439
|
+
try {
|
|
3440
|
+
readUsers(usersFile);
|
|
3441
|
+
return false;
|
|
3442
|
+
} catch {
|
|
3443
|
+
return true;
|
|
3444
|
+
}
|
|
3445
|
+
}
|
|
3446
|
+
function migrateLegacyRemotePassword(usersFile, legacyFile, ownerUserId, audit) {
|
|
3447
|
+
if (!existsSync4(legacyFile)) return "noop-no-legacy";
|
|
3448
|
+
const legacy = readFileSync5(legacyFile, "utf-8").trim();
|
|
3449
|
+
const users = readUsers(usersFile);
|
|
3450
|
+
const idx = users.findIndex((u) => u.userId === ownerUserId);
|
|
3451
|
+
if (idx === -1) return "noop-no-owner";
|
|
3452
|
+
const existing = users[idx].accessHash;
|
|
3453
|
+
if (typeof existing === "string" && existing.length > 0) {
|
|
3454
|
+
unlinkSync(legacyFile);
|
|
3455
|
+
return "noop-owner-has-hash";
|
|
3456
|
+
}
|
|
3457
|
+
if (!legacy.includes(":")) {
|
|
3458
|
+
unlinkSync(legacyFile);
|
|
3459
|
+
return "noop-no-legacy";
|
|
3460
|
+
}
|
|
3461
|
+
const rows = rowIds(users);
|
|
3462
|
+
users[idx] = { ...users[idx], accessHash: legacy };
|
|
3463
|
+
writeUsersAtomic(usersFile, users);
|
|
3464
|
+
unlinkSync(legacyFile);
|
|
3465
|
+
appendUsersAuditLine(audit, { action: "migrate", field: "accessHash", rowsBefore: rows, rowsAfter: rows });
|
|
3466
|
+
return "migrated";
|
|
3467
|
+
}
|
|
3468
|
+
function readLegacyRemoteHash(legacyFile) {
|
|
3469
|
+
if (!existsSync4(legacyFile)) return null;
|
|
3470
|
+
const raw2 = readFileSync5(legacyFile, "utf-8").trim();
|
|
3471
|
+
return raw2.includes(":") ? raw2 : null;
|
|
3472
|
+
}
|
|
3473
|
+
|
|
3474
|
+
// app/lib/password-strength.ts
|
|
3475
|
+
function validatePasswordStrength(password) {
|
|
3476
|
+
return [
|
|
3477
|
+
{ key: "length", label: "At least 8 characters", met: password.length >= 8 },
|
|
3478
|
+
{ key: "number", label: "Contains a number", met: /\d/.test(password) },
|
|
3479
|
+
{ key: "special", label: "Contains a special character", met: /[^A-Za-z0-9]/.test(password) },
|
|
3480
|
+
{ key: "whitespace", label: "No spaces", met: password.length > 0 && !/\s/.test(password) }
|
|
3481
|
+
];
|
|
3482
|
+
}
|
|
3483
|
+
function isPasswordValid(password) {
|
|
3484
|
+
return validatePasswordStrength(password).every((r) => r.met);
|
|
3485
|
+
}
|
|
3486
|
+
|
|
3487
|
+
// app/lib/remote-auth.ts
|
|
3488
|
+
import { scrypt as scrypt2, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual2, createHmac } from "crypto";
|
|
3489
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
3490
|
+
import { dirname as dirname2 } from "path";
|
|
3491
|
+
var SCRYPT_N2 = 16384;
|
|
3492
|
+
var SCRYPT_R2 = 8;
|
|
3493
|
+
var SCRYPT_P2 = 1;
|
|
3494
|
+
var SCRYPT_KEYLEN2 = 64;
|
|
3495
|
+
function scryptAsync2(password, salt) {
|
|
3496
|
+
return new Promise((resolve7, reject) => {
|
|
3497
|
+
scrypt2(password, salt, SCRYPT_KEYLEN2, { N: SCRYPT_N2, r: SCRYPT_R2, p: SCRYPT_P2 }, (err, key) => {
|
|
3498
|
+
if (err) reject(err);
|
|
3499
|
+
else resolve7(key);
|
|
3500
|
+
});
|
|
3501
|
+
});
|
|
3502
|
+
}
|
|
3503
|
+
async function hashPassword(password) {
|
|
3504
|
+
const salt = randomBytes2(32);
|
|
3505
|
+
const hash = await scryptAsync2(password, salt);
|
|
3506
|
+
return `${salt.toString("hex")}:${hash.toString("hex")}`;
|
|
3507
|
+
}
|
|
3508
|
+
function isRemoteAuthConfigured() {
|
|
3509
|
+
if (anyAccessPasswordConfigured(USERS_FILE)) return true;
|
|
3510
|
+
if (accessStoreUnreadable(USERS_FILE)) return true;
|
|
3511
|
+
return readLegacyRemoteHash(REMOTE_PASSWORD_FILE) !== null;
|
|
3512
|
+
}
|
|
3513
|
+
async function setRemotePassword(password) {
|
|
3514
|
+
const hash = await hashPassword(password);
|
|
3515
|
+
writeFileSync2(REMOTE_PASSWORD_FILE, hash, "utf-8");
|
|
3516
|
+
}
|
|
3517
|
+
async function verifyRemotePassword(password) {
|
|
3518
|
+
const matchedUserId = await verifyAccessPassword(password, USERS_FILE);
|
|
3519
|
+
if (matchedUserId) return { ok: true, hashBytes: SCRYPT_KEYLEN2, userId: matchedUserId };
|
|
3520
|
+
const legacy = readLegacyRemoteHash(REMOTE_PASSWORD_FILE);
|
|
3521
|
+
if (legacy && await matchesHash(password, legacy)) {
|
|
3522
|
+
return { ok: true, hashBytes: SCRYPT_KEYLEN2, userId: null };
|
|
3840
3523
|
}
|
|
3841
|
-
|
|
3524
|
+
const configured = anyAccessPasswordConfigured(USERS_FILE) || legacy !== null;
|
|
3525
|
+
return { ok: false, why: configured ? "password-mismatch" : "no-password-file", hashBytes: 0 };
|
|
3842
3526
|
}
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3527
|
+
var SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
3528
|
+
var TOKEN_PREFIX = "v1.";
|
|
3529
|
+
var SECRET_BYTES = 32;
|
|
3530
|
+
var NONCE_BYTES = 16;
|
|
3531
|
+
var cachedSecret = null;
|
|
3532
|
+
function getSecret() {
|
|
3533
|
+
if (cachedSecret) return cachedSecret;
|
|
3847
3534
|
try {
|
|
3848
|
-
|
|
3535
|
+
const hex2 = readFileSync6(REMOTE_SESSION_SECRET_FILE, "utf-8").trim();
|
|
3536
|
+
if (hex2.length === SECRET_BYTES * 2) {
|
|
3537
|
+
cachedSecret = Buffer.from(hex2, "hex");
|
|
3538
|
+
return cachedSecret;
|
|
3539
|
+
}
|
|
3540
|
+
} catch {
|
|
3541
|
+
}
|
|
3542
|
+
const fresh = randomBytes2(SECRET_BYTES).toString("hex");
|
|
3543
|
+
try {
|
|
3544
|
+
mkdirSync3(dirname2(REMOTE_SESSION_SECRET_FILE), { recursive: true, mode: 448 });
|
|
3545
|
+
writeFileSync2(REMOTE_SESSION_SECRET_FILE, fresh, { mode: 384, flag: "wx" });
|
|
3849
3546
|
} catch {
|
|
3850
|
-
return null;
|
|
3851
3547
|
}
|
|
3548
|
+
const hex = readFileSync6(REMOTE_SESSION_SECRET_FILE, "utf-8").trim();
|
|
3549
|
+
cachedSecret = Buffer.from(hex, "hex");
|
|
3550
|
+
return cachedSecret;
|
|
3852
3551
|
}
|
|
3853
|
-
function
|
|
3854
|
-
return
|
|
3552
|
+
function base64urlEncode(buf) {
|
|
3553
|
+
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
3855
3554
|
}
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
// app/lib/system-path-slugs.ts
|
|
3861
|
-
var SYSTEM_PATH_SLUGS = ["public", "public-chat", "chat", "graph", "data", "browser"];
|
|
3862
|
-
|
|
3863
|
-
// server/public-host-gate.ts
|
|
3864
|
-
var PUBLIC_ALLOWED_PREFIXES = [
|
|
3865
|
-
"/api/session",
|
|
3866
|
-
"/api/chat",
|
|
3867
|
-
// Task 873 — the detooled public surface's visitor-authed reader (pointer +
|
|
3868
|
-
// server-filtered SSE stream). Reached by the visitor browser on the public
|
|
3869
|
-
// host; authenticated by the __access_session grant inside the route.
|
|
3870
|
-
"/api/public-reader/",
|
|
3871
|
-
"/api/access/",
|
|
3872
|
-
"/api/health",
|
|
3873
|
-
// Public free/busy for the booking page. Open slots only, no :Meeting detail
|
|
3874
|
-
// (server/routes/calendar-public.ts). Admitted on public hosts; on the brand
|
|
3875
|
-
// host the remote-auth gate bypasses the same path (server/index.ts).
|
|
3876
|
-
"/api/calendar/free-busy",
|
|
3877
|
-
"/api/telegram/",
|
|
3878
|
-
// Task 1881 — the data portal's device-held-file fetch. Reached by the edge
|
|
3879
|
-
// Pages Function's signed HEAD/GET probe on the public host; authenticated by
|
|
3880
|
-
// the per-account HMAC signature the route re-verifies (portal-fetch.ts), not
|
|
3881
|
-
// the remote-auth gate. Pointing PORTAL_INSTALL_ORIGIN at the admin host
|
|
3882
|
-
// instead made every download 503 (remote-auth-required on the probe).
|
|
3883
|
-
"/api/portal/fetch",
|
|
3884
|
-
// QuickBooks OAuth consent callback. Intuit redirects the operator's browser
|
|
3885
|
-
// here with no admin cookie; the route authenticates by validating the OAuth
|
|
3886
|
-
// `state` nonce, not the remote-auth gate (same model as /api/telegram/).
|
|
3887
|
-
"/api/quickbooks/",
|
|
3888
|
-
"/assets/",
|
|
3889
|
-
"/brand/",
|
|
3890
|
-
"/agent-assets/",
|
|
3891
|
-
"/generated/",
|
|
3892
|
-
"/g/",
|
|
3893
|
-
"/sites/"
|
|
3894
|
-
];
|
|
3895
|
-
var PUBLIC_ALLOWED_EXACT = [
|
|
3896
|
-
"/favicon.ico",
|
|
3897
|
-
"/brand-defaults.css"
|
|
3898
|
-
];
|
|
3899
|
-
function isPublicPathAllowed(path) {
|
|
3900
|
-
if (path === "/") return true;
|
|
3901
|
-
if (PUBLIC_ALLOWED_EXACT.includes(path)) return true;
|
|
3902
|
-
if (PUBLIC_ALLOWED_PREFIXES.some((prefix) => path.startsWith(prefix))) return true;
|
|
3903
|
-
return AGENT_SLUG_PATTERN.test(path);
|
|
3555
|
+
function base64urlDecode(s) {
|
|
3556
|
+
const padded = s.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - s.length % 4) % 4);
|
|
3557
|
+
return Buffer.from(padded, "base64");
|
|
3904
3558
|
}
|
|
3905
|
-
function
|
|
3906
|
-
return
|
|
3559
|
+
function signPayload(payloadJson, secret) {
|
|
3560
|
+
return base64urlEncode(createHmac("sha256", secret).update(payloadJson).digest());
|
|
3907
3561
|
}
|
|
3908
|
-
function
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3562
|
+
function createRemoteSession(opts = {}) {
|
|
3563
|
+
const payload = { c: opts.now ?? Date.now(), n: randomBytes2(NONCE_BYTES).toString("hex") };
|
|
3564
|
+
const payloadJson = JSON.stringify(payload);
|
|
3565
|
+
const payloadB64 = base64urlEncode(Buffer.from(payloadJson, "utf-8"));
|
|
3566
|
+
const sig = signPayload(payloadJson, getSecret());
|
|
3567
|
+
return `${TOKEN_PREFIX}${payloadB64}.${sig}`;
|
|
3912
3568
|
}
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
if (!existsSync3(OPERATOR_DOMAINS_PATH)) return /* @__PURE__ */ new Set();
|
|
3569
|
+
function parseToken(token) {
|
|
3570
|
+
if (!token.startsWith(TOKEN_PREFIX)) return null;
|
|
3571
|
+
const rest = token.slice(TOKEN_PREFIX.length);
|
|
3572
|
+
const dot = rest.indexOf(".");
|
|
3573
|
+
if (dot === -1) return null;
|
|
3574
|
+
const payloadB64 = rest.slice(0, dot);
|
|
3575
|
+
const sigB64 = rest.slice(dot + 1);
|
|
3576
|
+
if (!payloadB64 || !sigB64) return null;
|
|
3577
|
+
let payloadJson;
|
|
3923
3578
|
try {
|
|
3924
|
-
|
|
3925
|
-
if (!Array.isArray(parsed)) return /* @__PURE__ */ new Set();
|
|
3926
|
-
return new Set(parsed.filter((h) => typeof h === "string"));
|
|
3579
|
+
payloadJson = base64urlDecode(payloadB64).toString("utf-8");
|
|
3927
3580
|
} catch {
|
|
3928
|
-
return
|
|
3581
|
+
return null;
|
|
3582
|
+
}
|
|
3583
|
+
const expected = signPayload(payloadJson, getSecret());
|
|
3584
|
+
const expectedBuf = Buffer.from(expected, "utf-8");
|
|
3585
|
+
const provBuf = Buffer.from(sigB64, "utf-8");
|
|
3586
|
+
const signatureValid = expectedBuf.length === provBuf.length && timingSafeEqual2(expectedBuf, provBuf);
|
|
3587
|
+
let payload;
|
|
3588
|
+
try {
|
|
3589
|
+
payload = JSON.parse(payloadJson);
|
|
3590
|
+
} catch {
|
|
3591
|
+
return null;
|
|
3592
|
+
}
|
|
3593
|
+
if (!payload || typeof payload !== "object" || typeof payload.c !== "number" || typeof payload.n !== "string") {
|
|
3594
|
+
return null;
|
|
3929
3595
|
}
|
|
3596
|
+
return { payload, signatureValid };
|
|
3930
3597
|
}
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
if (
|
|
3935
|
-
|
|
3936
|
-
`[operator-domains] loaded ${operatorDomains.size} operator domain(s): ${[...operatorDomains].join(", ")}`
|
|
3937
|
-
);
|
|
3598
|
+
function validateRemoteSession(token, now = Date.now()) {
|
|
3599
|
+
if (!token) return false;
|
|
3600
|
+
const parsed = parseToken(token);
|
|
3601
|
+
if (!parsed || !parsed.signatureValid) return false;
|
|
3602
|
+
return now - parsed.payload.c <= SESSION_TTL_MS;
|
|
3938
3603
|
}
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
});
|
|
3945
|
-
function getOperatorDomains() {
|
|
3946
|
-
return operatorDomains;
|
|
3604
|
+
function describeRemoteSession(token, now = Date.now()) {
|
|
3605
|
+
if (!token) return { present: false, expired: false };
|
|
3606
|
+
const parsed = parseToken(token);
|
|
3607
|
+
if (!parsed || !parsed.signatureValid) return { present: false, expired: false };
|
|
3608
|
+
return { present: true, expired: now - parsed.payload.c > SESSION_TTL_MS };
|
|
3947
3609
|
}
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
// ../lib/brand-templating/src/index.ts
|
|
3954
|
-
import { join as join2 } from "path";
|
|
3955
|
-
import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
|
|
3956
|
-
var PLACEHOLDER = "{{productName}}";
|
|
3957
|
-
var cachedProductName = null;
|
|
3958
|
-
function brandJsonPath() {
|
|
3959
|
-
const platformRoot2 = process.env.MAXY_PLATFORM_ROOT;
|
|
3960
|
-
if (!platformRoot2) {
|
|
3961
|
-
throw new Error(
|
|
3962
|
-
"[skill-loader] MAXY_PLATFORM_ROOT not set \u2014 cannot resolve brand.json"
|
|
3963
|
-
);
|
|
3964
|
-
}
|
|
3965
|
-
return join2(platformRoot2, "config", "brand.json");
|
|
3610
|
+
function normalizeIp(ip) {
|
|
3611
|
+
if (!ip) return void 0;
|
|
3612
|
+
const trimmed = ip.trim();
|
|
3613
|
+
if (trimmed.startsWith("::ffff:")) return trimmed.slice(7);
|
|
3614
|
+
return trimmed;
|
|
3966
3615
|
}
|
|
3967
|
-
function
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3616
|
+
function isLoopback(ip) {
|
|
3617
|
+
return ip === "127.0.0.1" || ip.startsWith("127.") || ip === "::1";
|
|
3618
|
+
}
|
|
3619
|
+
function isPrivateIp(ip) {
|
|
3620
|
+
const parts = ip.split(".");
|
|
3621
|
+
if (parts.length !== 4) return false;
|
|
3622
|
+
const a = parseInt(parts[0], 10);
|
|
3623
|
+
const b = parseInt(parts[1], 10);
|
|
3624
|
+
if (Number.isNaN(a) || Number.isNaN(b)) return false;
|
|
3625
|
+
if (a === 10) return true;
|
|
3626
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
3627
|
+
if (a === 192 && b === 168) return true;
|
|
3628
|
+
if (a === 169 && b === 254) return true;
|
|
3629
|
+
return false;
|
|
3630
|
+
}
|
|
3631
|
+
function classifyNormalizedIp(ip) {
|
|
3632
|
+
if (isLoopback(ip) || isPrivateIp(ip)) return "lan";
|
|
3633
|
+
return "external";
|
|
3634
|
+
}
|
|
3635
|
+
function resolveClientIp(remoteAddress, xForwardedFor) {
|
|
3636
|
+
const remote = normalizeIp(remoteAddress);
|
|
3637
|
+
if (!remote) return { ip: "", kind: "unknown" };
|
|
3638
|
+
if (isLoopback(remote) && xForwardedFor) {
|
|
3639
|
+
const firstIp = normalizeIp(xForwardedFor.split(",")[0]?.trim());
|
|
3640
|
+
if (firstIp) {
|
|
3641
|
+
return { ip: firstIp, kind: classifyNormalizedIp(firstIp) };
|
|
3642
|
+
}
|
|
3972
3643
|
}
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3644
|
+
return { ip: remote, kind: classifyNormalizedIp(remote) };
|
|
3645
|
+
}
|
|
3646
|
+
var MAX_ATTEMPTS = 5;
|
|
3647
|
+
var LOCKOUT_MS = 15 * 60 * 1e3;
|
|
3648
|
+
var LOCAL_BUCKET = "__local__";
|
|
3649
|
+
var rateLimitMap = /* @__PURE__ */ new Map();
|
|
3650
|
+
function bucketKey(client) {
|
|
3651
|
+
return client.kind === "external" ? client.ip : LOCAL_BUCKET;
|
|
3652
|
+
}
|
|
3653
|
+
function checkRateLimit(client) {
|
|
3654
|
+
const key = bucketKey(client);
|
|
3655
|
+
const rec = rateLimitMap.get(key);
|
|
3656
|
+
if (!rec) return null;
|
|
3657
|
+
if (rec.lockedUntil > Date.now()) {
|
|
3658
|
+
const remaining = Math.ceil((rec.lockedUntil - Date.now()) / 1e3);
|
|
3659
|
+
return `Too many attempts. Try again in ${remaining}s`;
|
|
3980
3660
|
}
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
throw new Error(
|
|
3984
|
-
`[skill-loader] brand.json at ${path} has missing or empty productName`
|
|
3985
|
-
);
|
|
3661
|
+
if (rec.lockedUntil > 0) {
|
|
3662
|
+
rateLimitMap.delete(key);
|
|
3986
3663
|
}
|
|
3987
|
-
|
|
3988
|
-
return value;
|
|
3664
|
+
return null;
|
|
3989
3665
|
}
|
|
3990
|
-
function
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
`[skill-loader] ERROR: brand.json missing \u2014 cannot resolve productName for skill ${sourcePath}`
|
|
3998
|
-
);
|
|
3999
|
-
throw err;
|
|
3666
|
+
function recordFailedAttempt(client) {
|
|
3667
|
+
const key = bucketKey(client);
|
|
3668
|
+
const rec = rateLimitMap.get(key) ?? { count: 0, lockedUntil: 0 };
|
|
3669
|
+
rec.count++;
|
|
3670
|
+
if (rec.count >= MAX_ATTEMPTS) {
|
|
3671
|
+
rec.lockedUntil = Date.now() + LOCKOUT_MS;
|
|
3672
|
+
rec.count = 0;
|
|
4000
3673
|
}
|
|
4001
|
-
|
|
4002
|
-
const substituted = content.split(PLACEHOLDER).join(productName);
|
|
4003
|
-
console.log(
|
|
4004
|
-
`[skill-loader] brand-substituted productName=${productName} skill=${sourcePath} occurrences=${occurrences}`
|
|
4005
|
-
);
|
|
4006
|
-
return substituted;
|
|
3674
|
+
rateLimitMap.set(key, rec);
|
|
4007
3675
|
}
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
var PLATFORM_ROOT2 = process.env.MAXY_PLATFORM_ROOT ?? resolve4(process.cwd(), "..");
|
|
4011
|
-
var ACCOUNTS_DIR = resolve4(PLATFORM_ROOT2, "..", "data/accounts");
|
|
4012
|
-
if (!existsSync5(PLATFORM_ROOT2)) {
|
|
4013
|
-
throw new Error(
|
|
4014
|
-
`PLATFORM_ROOT does not exist: ${PLATFORM_ROOT2}
|
|
4015
|
-
Set the MAXY_PLATFORM_ROOT environment variable to the absolute path of the platform directory.`
|
|
4016
|
-
);
|
|
3676
|
+
function clearRateLimit(client) {
|
|
3677
|
+
rateLimitMap.delete(bucketKey(client));
|
|
4017
3678
|
}
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
if (!existsSync5(configPath)) {
|
|
4029
|
-
if (!_stubDirsLoggedThisBoot.has(entry.name)) {
|
|
4030
|
-
console.error(
|
|
4031
|
-
`[platform] accounts-state STUB-DIR id=${entry.name} \u2014 onboarding leak; remove or repair`
|
|
4032
|
-
);
|
|
4033
|
-
_stubDirsLoggedThisBoot.add(entry.name);
|
|
3679
|
+
registerLoop({
|
|
3680
|
+
name: "rate-limit-cleanup",
|
|
3681
|
+
intervalMs: 15 * 60 * 1e3,
|
|
3682
|
+
run: () => {
|
|
3683
|
+
const now = Date.now();
|
|
3684
|
+
let purged = 0;
|
|
3685
|
+
for (const [key, rec] of rateLimitMap) {
|
|
3686
|
+
if (rec.lockedUntil > 0 && rec.lockedUntil <= now) {
|
|
3687
|
+
rateLimitMap.delete(key);
|
|
3688
|
+
purged += 1;
|
|
4034
3689
|
}
|
|
4035
|
-
continue;
|
|
4036
3690
|
}
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
3691
|
+
return `purged ${purged}, ${rateLimitMap.size} tracked`;
|
|
3692
|
+
}
|
|
3693
|
+
});
|
|
3694
|
+
function renderLoginPage(opts) {
|
|
3695
|
+
const error = opts?.error ?? "";
|
|
3696
|
+
const lockout = opts?.lockoutSeconds ?? 0;
|
|
3697
|
+
const redirect = opts?.redirect ?? "/";
|
|
3698
|
+
const mode = opts?.mode ?? "login";
|
|
3699
|
+
const changeError = opts?.changeError ?? "";
|
|
3700
|
+
const success = opts?.success ?? "";
|
|
3701
|
+
const setupError = opts?.setupError ?? "";
|
|
3702
|
+
const primaryColor = opts?.primaryColor ?? "#7C8C72";
|
|
3703
|
+
const primaryHoverColor = opts?.primaryHoverColor ?? "#6A7A62";
|
|
3704
|
+
const primarySubtle = opts?.primarySubtle ?? "rgba(124,140,114,0.08)";
|
|
3705
|
+
const productName = opts?.productName ?? "Maxy";
|
|
3706
|
+
const logoPath = opts?.logoPath ?? "/brand/maxy-monochrome.png";
|
|
3707
|
+
const faviconPath = opts?.faviconPath ?? "/favicon.ico";
|
|
3708
|
+
const displayFont = opts?.displayFont ?? "'Newsreader', Georgia, serif";
|
|
3709
|
+
const bodyFont = opts?.bodyFont ?? "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
|
|
3710
|
+
const logoContainsName = opts?.logoContainsName ?? false;
|
|
3711
|
+
const tagline = opts?.tagline ?? "";
|
|
3712
|
+
const origin = opts?.origin ?? "";
|
|
3713
|
+
const ogCardPath = opts?.ogCardPath ?? "";
|
|
3714
|
+
const manifestPath = opts?.manifestPath ?? "";
|
|
3715
|
+
const appleTouchIconPath = opts?.appleTouchIconPath ?? "";
|
|
3716
|
+
const pwaThemeColor = opts?.themeColor ?? "";
|
|
3717
|
+
const ogBlock = origin && ogCardPath ? [
|
|
3718
|
+
`<meta property="og:title" content="Sign in \u2014 ${escapeHtml(productName)}">`,
|
|
3719
|
+
`<meta property="og:description" content="${escapeHtml(tagline)}">`,
|
|
3720
|
+
`<meta property="og:type" content="website">`,
|
|
3721
|
+
`<meta property="og:url" content="${escapeHtml(origin)}">`,
|
|
3722
|
+
`<meta property="og:image" content="${escapeHtml(origin + ogCardPath)}">`,
|
|
3723
|
+
`<meta property="og:image:width" content="1200">`,
|
|
3724
|
+
`<meta property="og:image:height" content="630">`,
|
|
3725
|
+
`<meta name="twitter:card" content="summary_large_image">`,
|
|
3726
|
+
`<meta name="twitter:title" content="Sign in \u2014 ${escapeHtml(productName)}">`,
|
|
3727
|
+
`<meta name="twitter:description" content="${escapeHtml(tagline)}">`,
|
|
3728
|
+
`<meta name="twitter:image" content="${escapeHtml(origin + ogCardPath)}">`
|
|
3729
|
+
].join("\n ") : "";
|
|
3730
|
+
const pwaInstallBlock = manifestPath && appleTouchIconPath ? [
|
|
3731
|
+
`<link rel="manifest" href="${escapeHtml(manifestPath)}">`,
|
|
3732
|
+
`<link rel="apple-touch-icon" href="${escapeHtml(appleTouchIconPath)}">`,
|
|
3733
|
+
`<meta name="mobile-web-app-capable" content="yes">`,
|
|
3734
|
+
`<meta name="apple-mobile-web-app-capable" content="yes">`,
|
|
3735
|
+
pwaThemeColor ? `<meta name="theme-color" content="${escapeHtml(pwaThemeColor)}">` : ""
|
|
3736
|
+
].filter(Boolean).join("\n ") : "";
|
|
3737
|
+
const errorHtml = error ? `<p class="msg msg--error">${escapeHtml(error)}</p>` : "";
|
|
3738
|
+
const changeErrorHtml = changeError ? `<p class="msg msg--error">${escapeHtml(changeError)}</p>` : "";
|
|
3739
|
+
const successHtml = success ? `<p class="msg msg--success">${escapeHtml(success)}</p>` : "";
|
|
3740
|
+
const lockoutHtml = lockout > 0 ? `<p class="msg msg--error">Too many attempts. Try again in <span id="countdown">${lockout}</span>s</p>
|
|
3741
|
+
<script>
|
|
3742
|
+
(function() {
|
|
3743
|
+
var s = ${lockout};
|
|
3744
|
+
var el = document.getElementById('countdown');
|
|
3745
|
+
var iv = setInterval(function() { s--; el.textContent = s; if (s <= 0) { clearInterval(iv); location.reload(); } }, 1000);
|
|
3746
|
+
})();
|
|
3747
|
+
</script>` : "";
|
|
3748
|
+
const setupErrorHtml = setupError ? `<p class="msg msg--error">${escapeHtml(setupError)}</p>` : "";
|
|
3749
|
+
const formDisabled = lockout > 0 ? "disabled" : "";
|
|
3750
|
+
const loginDisplay = mode === "login" ? "block" : "none";
|
|
3751
|
+
const changeDisplay = mode === "change" ? "block" : "none";
|
|
3752
|
+
const setupDisplay = mode === "setup" ? "block" : "none";
|
|
3753
|
+
const successDisplay = mode === "success" ? "block" : "none";
|
|
3754
|
+
const subtitleText = mode === "setup" ? "Set your remote password" : "Remote access";
|
|
3755
|
+
const displayFontName = displayFont.match(/^'([^']+)'/)?.[1] ?? "";
|
|
3756
|
+
const bodyFontName = bodyFont.match(/^'([^']+)'/)?.[1] ?? "";
|
|
3757
|
+
const googleFontsLink = displayFontName || bodyFontName ? [
|
|
3758
|
+
'<link rel="preconnect" href="https://fonts.googleapis.com">',
|
|
3759
|
+
'<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>',
|
|
3760
|
+
'<link href="https://fonts.googleapis.com/css2?' + [
|
|
3761
|
+
displayFontName ? `family=${displayFontName.replace(/ /g, "+")}:wght@300;400` : "",
|
|
3762
|
+
bodyFontName ? `family=${bodyFontName.replace(/ /g, "+")}:wght@400;500` : ""
|
|
3763
|
+
].filter(Boolean).join("&") + '&display=swap" rel="stylesheet">'
|
|
3764
|
+
].join("\n ") : "";
|
|
3765
|
+
return `<!DOCTYPE html>
|
|
3766
|
+
<html lang="en">
|
|
3767
|
+
<head>
|
|
3768
|
+
<meta charset="utf-8">
|
|
3769
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
3770
|
+
<title>Sign in \u2014 ${escapeHtml(productName)}</title>
|
|
3771
|
+
<link rel="icon" href="${escapeHtml(faviconPath)}">
|
|
3772
|
+
${pwaInstallBlock}
|
|
3773
|
+
${ogBlock}
|
|
3774
|
+
${googleFontsLink}
|
|
3775
|
+
<style>
|
|
3776
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
3777
|
+
body {
|
|
3778
|
+
font-family: ${bodyFont};
|
|
3779
|
+
background: #F6F5F2;
|
|
3780
|
+
color: #2A2D38;
|
|
3781
|
+
min-height: 100vh;
|
|
3782
|
+
display: flex;
|
|
3783
|
+
align-items: center;
|
|
3784
|
+
justify-content: center;
|
|
3785
|
+
}
|
|
3786
|
+
.page {
|
|
3787
|
+
display: flex;
|
|
3788
|
+
flex-direction: column;
|
|
3789
|
+
align-items: center;
|
|
3790
|
+
gap: 24px;
|
|
3791
|
+
max-width: 420px;
|
|
3792
|
+
width: 100%;
|
|
3793
|
+
padding: 40px 20px;
|
|
3794
|
+
}
|
|
3795
|
+
.logo-img {
|
|
3796
|
+
width: 110px;
|
|
3797
|
+
height: 110px;
|
|
3798
|
+
margin: -11px;
|
|
3799
|
+
object-fit: contain;
|
|
3800
|
+
}
|
|
3801
|
+
.title {
|
|
3802
|
+
font-family: ${displayFont};
|
|
3803
|
+
font-weight: 300;
|
|
3804
|
+
font-size: 28px;
|
|
3805
|
+
color: #2A2D38;
|
|
3806
|
+
text-align: center;
|
|
3807
|
+
letter-spacing: -0.01em;
|
|
3808
|
+
line-height: 1.3;
|
|
3809
|
+
}
|
|
3810
|
+
.subtitle {
|
|
3811
|
+
font-size: 15px;
|
|
3812
|
+
color: #4A4D58;
|
|
3813
|
+
text-align: center;
|
|
3814
|
+
margin-top: -8px;
|
|
3815
|
+
}
|
|
3816
|
+
.form-wrap {
|
|
3817
|
+
width: 100%;
|
|
3818
|
+
max-width: 300px;
|
|
3819
|
+
}
|
|
3820
|
+
.field { margin-bottom: 12px; }
|
|
3821
|
+
.field-label {
|
|
3822
|
+
display: block;
|
|
3823
|
+
font-size: 13px;
|
|
3824
|
+
font-weight: 500;
|
|
3825
|
+
color: #4A4D58;
|
|
3826
|
+
margin-bottom: 6px;
|
|
3827
|
+
}
|
|
3828
|
+
.field-input {
|
|
3829
|
+
width: 100%;
|
|
3830
|
+
padding: 10px 12px;
|
|
3831
|
+
border: 1px solid #E4E3DE;
|
|
3832
|
+
border-radius: 8px;
|
|
3833
|
+
font-size: 15px;
|
|
3834
|
+
font-family: inherit;
|
|
3835
|
+
outline: none;
|
|
3836
|
+
background: #fff;
|
|
3837
|
+
transition: border-color 0.15s, box-shadow 0.15s;
|
|
3838
|
+
}
|
|
3839
|
+
.field-input:focus {
|
|
3840
|
+
border-color: ${primaryColor};
|
|
3841
|
+
box-shadow: 0 0 0 3px ${primarySubtle};
|
|
3842
|
+
}
|
|
3843
|
+
.options {
|
|
3844
|
+
display: flex;
|
|
3845
|
+
align-items: center;
|
|
3846
|
+
justify-content: space-between;
|
|
3847
|
+
gap: 12px;
|
|
3848
|
+
margin-top: 8px;
|
|
3849
|
+
}
|
|
3850
|
+
|
|
3851
|
+
/* Checkbox \u2014 matches Maxy shared checkbox (asterisk-in-square) */
|
|
3852
|
+
.check {
|
|
3853
|
+
position: relative;
|
|
3854
|
+
display: flex;
|
|
3855
|
+
align-items: center;
|
|
3856
|
+
gap: 8px;
|
|
3857
|
+
cursor: pointer;
|
|
3858
|
+
user-select: none;
|
|
3859
|
+
}
|
|
3860
|
+
.check input { position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none; }
|
|
3861
|
+
.check-box {
|
|
3862
|
+
width: 14px;
|
|
3863
|
+
height: 14px;
|
|
3864
|
+
border: 1px solid #E4E3DE;
|
|
3865
|
+
border-radius: 2px;
|
|
3866
|
+
display: flex;
|
|
3867
|
+
align-items: center;
|
|
3868
|
+
justify-content: center;
|
|
3869
|
+
font-size: 10px;
|
|
3870
|
+
color: transparent;
|
|
3871
|
+
transition: border-color 0.15s, color 0.15s;
|
|
3872
|
+
flex-shrink: 0;
|
|
3873
|
+
}
|
|
3874
|
+
.check input:checked + .check-box {
|
|
3875
|
+
border-color: ${primaryColor};
|
|
3876
|
+
color: ${primaryColor};
|
|
3877
|
+
}
|
|
3878
|
+
.check-label {
|
|
3879
|
+
font-size: 13px;
|
|
3880
|
+
color: #4A4D58;
|
|
3881
|
+
}
|
|
3882
|
+
|
|
3883
|
+
/* Ghost button \u2014 matches Maxy ghost variant */
|
|
3884
|
+
.ghost {
|
|
3885
|
+
background: none;
|
|
3886
|
+
border: none;
|
|
3887
|
+
font-family: inherit;
|
|
3888
|
+
font-size: 13px;
|
|
3889
|
+
color: #4A4D58;
|
|
3890
|
+
cursor: pointer;
|
|
3891
|
+
padding: 4px 0;
|
|
3892
|
+
transition: color 0.15s;
|
|
3893
|
+
}
|
|
3894
|
+
.ghost:hover { color: #2A2D38; }
|
|
3895
|
+
|
|
3896
|
+
/* Primary button */
|
|
3897
|
+
.btn {
|
|
3898
|
+
width: 100%;
|
|
3899
|
+
padding: 12px;
|
|
3900
|
+
margin-top: 16px;
|
|
3901
|
+
background: ${primaryColor};
|
|
3902
|
+
color: #fff;
|
|
3903
|
+
border: none;
|
|
3904
|
+
border-radius: 10px;
|
|
3905
|
+
font-size: 15px;
|
|
3906
|
+
font-weight: 500;
|
|
3907
|
+
font-family: inherit;
|
|
3908
|
+
cursor: pointer;
|
|
3909
|
+
transition: background 0.15s;
|
|
3910
|
+
}
|
|
3911
|
+
.btn:hover:not(:disabled) { background: ${primaryHoverColor}; }
|
|
3912
|
+
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
3913
|
+
|
|
3914
|
+
/* Messages */
|
|
3915
|
+
.msg {
|
|
3916
|
+
font-size: 13px;
|
|
3917
|
+
text-align: center;
|
|
3918
|
+
margin-top: 8px;
|
|
4045
3919
|
}
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
if (existsSync5(USERS_FILE)) {
|
|
4057
|
-
try {
|
|
4058
|
-
const raw2 = readFileSync7(USERS_FILE, "utf-8").trim();
|
|
4059
|
-
if (raw2) {
|
|
4060
|
-
const users = JSON.parse(raw2);
|
|
4061
|
-
if (users.length > 0) {
|
|
4062
|
-
usersJsonUserId = users[0].userId;
|
|
4063
|
-
}
|
|
4064
|
-
}
|
|
4065
|
-
} catch {
|
|
3920
|
+
.msg--error { color: #c44; }
|
|
3921
|
+
.msg--success { color: ${primaryColor}; }
|
|
3922
|
+
|
|
3923
|
+
/* Strength checklist (setup mode) */
|
|
3924
|
+
.strength-checklist { margin: 8px 0; }
|
|
3925
|
+
.strength-item {
|
|
3926
|
+
font-size: 13px;
|
|
3927
|
+
color: #4A4D58;
|
|
3928
|
+
padding: 2px 0;
|
|
3929
|
+
transition: color 0.15s;
|
|
4066
3930
|
}
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
if (!entry.isDirectory()) continue;
|
|
4072
|
-
const configPath = resolve4(ACCOUNTS_DIR, entry.name, "account.json");
|
|
4073
|
-
if (!existsSync5(configPath)) continue;
|
|
4074
|
-
const raw2 = readFileSync7(configPath, "utf-8");
|
|
4075
|
-
let config;
|
|
4076
|
-
try {
|
|
4077
|
-
config = JSON.parse(raw2);
|
|
4078
|
-
} catch {
|
|
4079
|
-
console.error(`[maxy] account.json is corrupt at ${configPath} \u2014 skipping`);
|
|
4080
|
-
continue;
|
|
3931
|
+
.strength-icon {
|
|
3932
|
+
display: inline-block;
|
|
3933
|
+
width: 16px;
|
|
3934
|
+
text-align: center;
|
|
4081
3935
|
}
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
);
|
|
3936
|
+
|
|
3937
|
+
@media (max-width: 440px) {
|
|
3938
|
+
.page { padding: 24px 16px; }
|
|
4086
3939
|
}
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
}
|
|
4092
|
-
|
|
4093
|
-
|
|
3940
|
+
</style>
|
|
3941
|
+
</head>
|
|
3942
|
+
<body>
|
|
3943
|
+
<div class="page">
|
|
3944
|
+
<img src="${escapeHtml(logoPath)}" alt="${escapeHtml(productName)}" class="logo-img">
|
|
3945
|
+
<h1 class="title"${logoContainsName ? ' style="display:none"' : ""}>${escapeHtml(productName)}</h1>
|
|
3946
|
+
<p class="subtitle">${escapeHtml(subtitleText)}</p>
|
|
3947
|
+
|
|
3948
|
+
${successHtml}
|
|
3949
|
+
|
|
3950
|
+
<!-- Login -->
|
|
3951
|
+
<div id="login-section" class="form-wrap" style="display:${loginDisplay}">
|
|
3952
|
+
<form method="POST" action="/__remote-auth/login">
|
|
3953
|
+
<input type="hidden" name="redirect" value="${escapeHtml(redirect)}">
|
|
3954
|
+
<div class="field">
|
|
3955
|
+
<label class="field-label" for="password">Password</label>
|
|
3956
|
+
<input class="field-input" type="password" id="password" name="password" required autofocus ${formDisabled}>
|
|
3957
|
+
</div>
|
|
3958
|
+
<div class="options">
|
|
3959
|
+
<label class="check">
|
|
3960
|
+
<input type="checkbox" id="show-login-pw">
|
|
3961
|
+
<span class="check-box">\u2731</span>
|
|
3962
|
+
<span class="check-label">Show password</span>
|
|
3963
|
+
</label>
|
|
3964
|
+
<button type="button" class="ghost" onclick="toggleMode('change')">Change password</button>
|
|
3965
|
+
</div>
|
|
3966
|
+
<button type="submit" class="btn" ${formDisabled}>Sign in</button>
|
|
3967
|
+
</form>
|
|
3968
|
+
${errorHtml}
|
|
3969
|
+
${lockoutHtml}
|
|
3970
|
+
</div>
|
|
3971
|
+
|
|
3972
|
+
<!-- Change password -->
|
|
3973
|
+
<div id="change-section" class="form-wrap" style="display:${changeDisplay}">
|
|
3974
|
+
<form method="POST" action="/__remote-auth/change-password">
|
|
3975
|
+
<input type="hidden" name="redirect" value="${escapeHtml(redirect)}">
|
|
3976
|
+
<div class="field">
|
|
3977
|
+
<label class="field-label" for="current_password">Current password</label>
|
|
3978
|
+
<input class="field-input" type="password" id="current_password" name="current_password" required ${formDisabled}>
|
|
3979
|
+
</div>
|
|
3980
|
+
<div class="field">
|
|
3981
|
+
<label class="field-label" for="new_password">New password</label>
|
|
3982
|
+
<input class="field-input" type="password" id="new_password" name="new_password" required ${formDisabled}>
|
|
3983
|
+
</div>
|
|
3984
|
+
<div class="field">
|
|
3985
|
+
<label class="field-label" for="confirm_password">Confirm new password</label>
|
|
3986
|
+
<input class="field-input" type="password" id="confirm_password" name="confirm_password" required ${formDisabled}>
|
|
3987
|
+
</div>
|
|
3988
|
+
<div class="options">
|
|
3989
|
+
<label class="check">
|
|
3990
|
+
<input type="checkbox" id="show-change-pw">
|
|
3991
|
+
<span class="check-box">\u2731</span>
|
|
3992
|
+
<span class="check-label">Show passwords</span>
|
|
3993
|
+
</label>
|
|
3994
|
+
<button type="button" class="ghost" onclick="toggleMode('login')">Back to sign in</button>
|
|
3995
|
+
</div>
|
|
3996
|
+
<button type="submit" class="btn" ${formDisabled}>Change password</button>
|
|
3997
|
+
</form>
|
|
3998
|
+
${changeErrorHtml}
|
|
3999
|
+
</div>
|
|
4000
|
+
|
|
4001
|
+
<!-- Setup (initial password) -->
|
|
4002
|
+
<div id="setup-section" class="form-wrap" style="display:${setupDisplay}">
|
|
4003
|
+
<form method="POST" action="/__remote-auth/set-initial-password">
|
|
4004
|
+
<div class="field">
|
|
4005
|
+
<label class="field-label" for="setup_password">Password</label>
|
|
4006
|
+
<input class="field-input" type="password" id="setup_password" name="password" required autofocus>
|
|
4007
|
+
</div>
|
|
4008
|
+
<div class="field">
|
|
4009
|
+
<label class="field-label" for="setup_confirm">Confirm password</label>
|
|
4010
|
+
<input class="field-input" type="password" id="setup_confirm" name="confirm_password" required>
|
|
4011
|
+
</div>
|
|
4012
|
+
<div id="strength-checklist" class="strength-checklist">
|
|
4013
|
+
<div class="strength-item" id="req-length"><span class="strength-icon">\u25CB</span> At least 8 characters</div>
|
|
4014
|
+
<div class="strength-item" id="req-number"><span class="strength-icon">\u25CB</span> Contains a number</div>
|
|
4015
|
+
<div class="strength-item" id="req-special"><span class="strength-icon">\u25CB</span> Contains a special character</div>
|
|
4016
|
+
<div class="strength-item" id="req-spaces"><span class="strength-icon">\u25CB</span> No spaces</div>
|
|
4017
|
+
</div>
|
|
4018
|
+
<div class="options">
|
|
4019
|
+
<label class="check">
|
|
4020
|
+
<input type="checkbox" id="show-setup-pw">
|
|
4021
|
+
<span class="check-box">\u2731</span>
|
|
4022
|
+
<span class="check-label">Show passwords</span>
|
|
4023
|
+
</label>
|
|
4024
|
+
</div>
|
|
4025
|
+
<button type="submit" class="btn" id="setup-submit" disabled>Set password</button>
|
|
4026
|
+
</form>
|
|
4027
|
+
${setupErrorHtml}
|
|
4028
|
+
</div>
|
|
4029
|
+
|
|
4030
|
+
<!-- Success (after initial password set) -->
|
|
4031
|
+
<div id="success-section" class="form-wrap" style="display:${successDisplay}">
|
|
4032
|
+
<p class="msg msg--success" style="font-size:15px;margin-bottom:4px;">\u2713 Password set</p>
|
|
4033
|
+
<p style="font-size:14px;color:#4A4D58;text-align:center;">You can close this tab and return to the onboarding tab.</p>
|
|
4034
|
+
<a href="/" style="display:block;text-align:center;margin-top:16px;font-size:13px;color:#4A4D58;text-decoration:none;" onmouseover="this.style.color='#2A2D38'" onmouseout="this.style.color='#4A4D58'">Sign in \u2192</a>
|
|
4035
|
+
<script>
|
|
4036
|
+
(function() {
|
|
4037
|
+
try {
|
|
4038
|
+
var ch = new BroadcastChannel('platform-onboarding');
|
|
4039
|
+
setTimeout(function() {
|
|
4040
|
+
ch.postMessage({ type: 'remote-password-set' });
|
|
4041
|
+
ch.close();
|
|
4042
|
+
}, 2500);
|
|
4043
|
+
} catch(e) {}
|
|
4044
|
+
})();
|
|
4045
|
+
</script>
|
|
4046
|
+
</div>
|
|
4047
|
+
</div>
|
|
4048
|
+
|
|
4049
|
+
<script>
|
|
4050
|
+
/* Toggle between login and change-password modes */
|
|
4051
|
+
function toggleMode(mode) {
|
|
4052
|
+
document.getElementById('login-section').style.display = mode === 'login' ? 'block' : 'none';
|
|
4053
|
+
document.getElementById('change-section').style.display = mode === 'change' ? 'block' : 'none';
|
|
4054
|
+
/* Focus first input in the active section */
|
|
4055
|
+
var id = mode === 'login' ? 'password' : 'current_password';
|
|
4056
|
+
var el = document.getElementById(id);
|
|
4057
|
+
if (el) el.focus();
|
|
4094
4058
|
}
|
|
4095
|
-
|
|
4096
|
-
|
|
4059
|
+
|
|
4060
|
+
/* Show/hide password toggles */
|
|
4061
|
+
function bindShowHide(checkboxId, inputIds) {
|
|
4062
|
+
var cb = document.getElementById(checkboxId);
|
|
4063
|
+
if (!cb) return;
|
|
4064
|
+
cb.addEventListener('change', function() {
|
|
4065
|
+
var type = cb.checked ? 'text' : 'password';
|
|
4066
|
+
inputIds.forEach(function(id) {
|
|
4067
|
+
var el = document.getElementById(id);
|
|
4068
|
+
if (el) el.type = type;
|
|
4069
|
+
});
|
|
4070
|
+
});
|
|
4097
4071
|
}
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4072
|
+
bindShowHide('show-login-pw', ['password']);
|
|
4073
|
+
bindShowHide('show-change-pw', ['current_password', 'new_password', 'confirm_password']);
|
|
4074
|
+
bindShowHide('show-setup-pw', ['setup_password', 'setup_confirm']);
|
|
4075
|
+
|
|
4076
|
+
/* Explicit Enter-to-submit for the login password field */
|
|
4077
|
+
var loginPw = document.getElementById('password');
|
|
4078
|
+
if (loginPw) {
|
|
4079
|
+
loginPw.addEventListener('keydown', function(e) {
|
|
4080
|
+
if (e.key === 'Enter') {
|
|
4081
|
+
e.preventDefault();
|
|
4082
|
+
this.closest('form').requestSubmit();
|
|
4083
|
+
}
|
|
4084
|
+
});
|
|
4085
|
+
}
|
|
4086
|
+
|
|
4087
|
+
/* Live password strength validation (setup mode) */
|
|
4088
|
+
(function() {
|
|
4089
|
+
var pw = document.getElementById('setup_password');
|
|
4090
|
+
var confirm = document.getElementById('setup_confirm');
|
|
4091
|
+
var btn = document.getElementById('setup-submit');
|
|
4092
|
+
if (!pw || !confirm || !btn) return;
|
|
4093
|
+
|
|
4094
|
+
var rules = [
|
|
4095
|
+
{ id: 'req-length', test: function(p) { return p.length >= 8; } },
|
|
4096
|
+
{ id: 'req-number', test: function(p) { return /\\d/.test(p); } },
|
|
4097
|
+
{ id: 'req-special', test: function(p) { return /[^A-Za-z0-9]/.test(p); } },
|
|
4098
|
+
{ id: 'req-spaces', test: function(p) { return p.length > 0 && !/\\s/.test(p); } }
|
|
4099
|
+
];
|
|
4100
|
+
|
|
4101
|
+
function check() {
|
|
4102
|
+
var p = pw.value;
|
|
4103
|
+
var allMet = true;
|
|
4104
|
+
rules.forEach(function(r) {
|
|
4105
|
+
var el = document.getElementById(r.id);
|
|
4106
|
+
var met = r.test(p);
|
|
4107
|
+
if (!met) allMet = false;
|
|
4108
|
+
el.querySelector('.strength-icon').textContent = met ? '\\u2713' : '\\u25CB';
|
|
4109
|
+
el.style.color = met ? '${primaryColor}' : '#4A4D58';
|
|
4110
|
+
});
|
|
4111
|
+
var match = p.length > 0 && p === confirm.value;
|
|
4112
|
+
btn.disabled = !(allMet && match);
|
|
4113
|
+
}
|
|
4114
|
+
|
|
4115
|
+
pw.addEventListener('input', check);
|
|
4116
|
+
confirm.addEventListener('input', check);
|
|
4117
|
+
})();
|
|
4118
|
+
</script>
|
|
4119
|
+
</body>
|
|
4120
|
+
</html>`;
|
|
4105
4121
|
}
|
|
4106
|
-
function
|
|
4107
|
-
|
|
4108
|
-
if (!existsSync5(filePath)) return null;
|
|
4109
|
-
const raw2 = readFileSync7(filePath, "utf-8");
|
|
4110
|
-
if (filename.endsWith(".md")) {
|
|
4111
|
-
return substituteBrandPlaceholders(raw2, filePath);
|
|
4112
|
-
}
|
|
4113
|
-
return raw2;
|
|
4122
|
+
function escapeHtml(str) {
|
|
4123
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
4114
4124
|
}
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
if (
|
|
4120
|
-
|
|
4121
|
-
console.error(`[agent-slug] op=reject slug=${slug} reason=reserved`);
|
|
4122
|
-
return false;
|
|
4123
|
-
}
|
|
4124
|
-
if (SHADOWED_SLUGS.has(slug)) {
|
|
4125
|
-
console.error(`[agent-slug] op=reject slug=${slug} reason=shadowed`);
|
|
4126
|
-
return false;
|
|
4125
|
+
|
|
4126
|
+
// app/lib/auth-gate.ts
|
|
4127
|
+
function canAccessAdmin(input) {
|
|
4128
|
+
const client = resolveClientIp(input.remoteAddress, input.xForwardedFor);
|
|
4129
|
+
if (input.isPublicHost(input.host)) {
|
|
4130
|
+
return { allow: false, reason: "public-host", client };
|
|
4127
4131
|
}
|
|
4128
|
-
|
|
4129
|
-
}
|
|
4130
|
-
function resolveDefaultAgentSlug(accountDir) {
|
|
4131
|
-
const configPath = resolve4(accountDir, "account.json");
|
|
4132
|
-
if (!existsSync5(configPath)) {
|
|
4133
|
-
console.error("[agent-resolve] account.json not found \u2014 cannot resolve defaultAgent");
|
|
4134
|
-
return null;
|
|
4132
|
+
if (client.kind !== "external") {
|
|
4133
|
+
return { allow: true, client };
|
|
4135
4134
|
}
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
config = JSON.parse(readFileSync7(configPath, "utf-8"));
|
|
4139
|
-
} catch (err) {
|
|
4140
|
-
console.error("[agent-resolve] failed to read account.json:", err);
|
|
4141
|
-
return null;
|
|
4135
|
+
if (!isRemoteAuthConfigured()) {
|
|
4136
|
+
return { allow: false, reason: "remote-unconfigured", client };
|
|
4142
4137
|
}
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
return
|
|
4138
|
+
const token = parseCookieValue(input.cookieHeader, "__remote_session");
|
|
4139
|
+
if (token && validateRemoteSession(token)) {
|
|
4140
|
+
return { allow: true, client };
|
|
4146
4141
|
}
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4142
|
+
return { allow: false, reason: "unauthorized", client };
|
|
4143
|
+
}
|
|
4144
|
+
function parseCookieValue(cookieHeader, name) {
|
|
4145
|
+
if (!cookieHeader) return null;
|
|
4146
|
+
const match2 = cookieHeader.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`));
|
|
4147
|
+
if (!match2) return null;
|
|
4148
|
+
try {
|
|
4149
|
+
return decodeURIComponent(match2[1]);
|
|
4150
|
+
} catch {
|
|
4150
4151
|
return null;
|
|
4151
4152
|
}
|
|
4152
|
-
return config.defaultAgent;
|
|
4153
4153
|
}
|
|
4154
|
-
function
|
|
4155
|
-
return
|
|
4154
|
+
function isApiRequestPath(path) {
|
|
4155
|
+
return path.startsWith("/api/");
|
|
4156
4156
|
}
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
const identityTokens = identityRaw ? estimateTokens(identityRaw) : 0;
|
|
4228
|
-
const soulTokens = soulRaw ? estimateTokens(soulRaw) : 0;
|
|
4229
|
-
const knowledgeTokens = knowledge ? estimateTokens(knowledge) : 0;
|
|
4230
|
-
budget = {
|
|
4231
|
-
identity: identityTokens,
|
|
4232
|
-
soul: soulTokens,
|
|
4233
|
-
knowledge: knowledgeTokens,
|
|
4234
|
-
plugins: 0,
|
|
4235
|
-
total: identityTokens + soulTokens + knowledgeTokens
|
|
4236
|
-
};
|
|
4157
|
+
|
|
4158
|
+
// app/lib/agent-slug-pattern.ts
|
|
4159
|
+
var AGENT_SLUG_PATTERN = /^\/([a-z][a-z0-9-]{2,49})$/;
|
|
4160
|
+
|
|
4161
|
+
// server/public-host-gate.ts
|
|
4162
|
+
var PUBLIC_ALLOWED_PREFIXES = [
|
|
4163
|
+
"/api/session",
|
|
4164
|
+
"/api/chat",
|
|
4165
|
+
// Task 873 — the detooled public surface's visitor-authed reader (pointer +
|
|
4166
|
+
// server-filtered SSE stream). Reached by the visitor browser on the public
|
|
4167
|
+
// host; authenticated by the __access_session grant inside the route.
|
|
4168
|
+
"/api/public-reader/",
|
|
4169
|
+
"/api/access/",
|
|
4170
|
+
"/api/health",
|
|
4171
|
+
// Public free/busy for the booking page. Open slots only, no :Meeting detail
|
|
4172
|
+
// (server/routes/calendar-public.ts). Admitted on public hosts; on the brand
|
|
4173
|
+
// host the remote-auth gate bypasses the same path (server/index.ts).
|
|
4174
|
+
"/api/calendar/free-busy",
|
|
4175
|
+
"/api/telegram/",
|
|
4176
|
+
// Task 1881 — the data portal's device-held-file fetch. Reached by the edge
|
|
4177
|
+
// Pages Function's signed HEAD/GET probe on the public host; authenticated by
|
|
4178
|
+
// the per-account HMAC signature the route re-verifies (portal-fetch.ts), not
|
|
4179
|
+
// the remote-auth gate. Pointing PORTAL_INSTALL_ORIGIN at the admin host
|
|
4180
|
+
// instead made every download 503 (remote-auth-required on the probe).
|
|
4181
|
+
"/api/portal/fetch",
|
|
4182
|
+
// QuickBooks OAuth consent callback. Intuit redirects the operator's browser
|
|
4183
|
+
// here with no admin cookie; the route authenticates by validating the OAuth
|
|
4184
|
+
// `state` nonce, not the remote-auth gate (same model as /api/telegram/).
|
|
4185
|
+
"/api/quickbooks/",
|
|
4186
|
+
"/assets/",
|
|
4187
|
+
"/brand/",
|
|
4188
|
+
"/agent-assets/",
|
|
4189
|
+
"/generated/",
|
|
4190
|
+
"/g/",
|
|
4191
|
+
"/sites/"
|
|
4192
|
+
];
|
|
4193
|
+
var PUBLIC_ALLOWED_EXACT = [
|
|
4194
|
+
"/favicon.ico",
|
|
4195
|
+
"/brand-defaults.css"
|
|
4196
|
+
];
|
|
4197
|
+
function isPublicPathAllowed(path) {
|
|
4198
|
+
if (path === "/") return true;
|
|
4199
|
+
if (PUBLIC_ALLOWED_EXACT.includes(path)) return true;
|
|
4200
|
+
if (PUBLIC_ALLOWED_PREFIXES.some((prefix) => path.startsWith(prefix))) return true;
|
|
4201
|
+
return AGENT_SLUG_PATTERN.test(path);
|
|
4202
|
+
}
|
|
4203
|
+
function isOperatorHost(host, operatorDomains2) {
|
|
4204
|
+
return operatorDomains2.has(host);
|
|
4205
|
+
}
|
|
4206
|
+
function classifyHost(host, operatorDomains2, isPublicHost) {
|
|
4207
|
+
if (operatorDomains2.has(host)) return "operator";
|
|
4208
|
+
if (isPublicHost(host)) return "public";
|
|
4209
|
+
return "admin";
|
|
4210
|
+
}
|
|
4211
|
+
|
|
4212
|
+
// server/operator-domains-reader.ts
|
|
4213
|
+
import { watchFile } from "fs";
|
|
4214
|
+
|
|
4215
|
+
// app/lib/operator-domains.ts
|
|
4216
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "fs";
|
|
4217
|
+
import { dirname as dirname3, resolve as resolve4 } from "path";
|
|
4218
|
+
var OPERATOR_DOMAINS_PATH = resolve4(MAXY_DIR, "operator-domains.json");
|
|
4219
|
+
function loadOperatorDomains() {
|
|
4220
|
+
if (!existsSync5(OPERATOR_DOMAINS_PATH)) return /* @__PURE__ */ new Set();
|
|
4221
|
+
try {
|
|
4222
|
+
const parsed = JSON.parse(readFileSync7(OPERATOR_DOMAINS_PATH, "utf-8"));
|
|
4223
|
+
if (!Array.isArray(parsed)) return /* @__PURE__ */ new Set();
|
|
4224
|
+
return new Set(parsed.filter((h) => typeof h === "string"));
|
|
4225
|
+
} catch {
|
|
4226
|
+
return /* @__PURE__ */ new Set();
|
|
4237
4227
|
}
|
|
4238
|
-
return { model, status, displayName, image, imageShape, showAgentName, knowledge, knowledgeBaked, knowledgeKeywords, budget, accessMode };
|
|
4239
4228
|
}
|
|
4240
|
-
|
|
4241
|
-
|
|
4229
|
+
|
|
4230
|
+
// server/operator-domains-reader.ts
|
|
4231
|
+
var operatorDomains = loadOperatorDomains();
|
|
4232
|
+
if (operatorDomains.size > 0) {
|
|
4233
|
+
console.log(
|
|
4234
|
+
`[operator-domains] loaded ${operatorDomains.size} operator domain(s): ${[...operatorDomains].join(", ")}`
|
|
4235
|
+
);
|
|
4242
4236
|
}
|
|
4243
|
-
|
|
4244
|
-
|
|
4237
|
+
watchFile(OPERATOR_DOMAINS_PATH, { interval: 2e3 }, () => {
|
|
4238
|
+
operatorDomains = loadOperatorDomains();
|
|
4239
|
+
console.log(
|
|
4240
|
+
`[operator-domains] reloaded ${operatorDomains.size} operator domain(s): ${[...operatorDomains].join(", ")}`
|
|
4241
|
+
);
|
|
4242
|
+
});
|
|
4243
|
+
function getOperatorDomains() {
|
|
4244
|
+
return operatorDomains;
|
|
4245
4245
|
}
|
|
4246
4246
|
|
|
4247
4247
|
// app/lib/claude-agent/session-store.ts
|
|
@@ -6208,11 +6208,6 @@ export {
|
|
|
6208
6208
|
Hono2 as Hono,
|
|
6209
6209
|
getRequestListener,
|
|
6210
6210
|
serve,
|
|
6211
|
-
sampleCpu,
|
|
6212
|
-
sampleProcessCpu,
|
|
6213
|
-
registerLoop,
|
|
6214
|
-
listLoops,
|
|
6215
|
-
armedCount,
|
|
6216
6211
|
MAXY_DIR,
|
|
6217
6212
|
BRAND_NAME,
|
|
6218
6213
|
COMMERCIAL_MODE,
|
|
@@ -6230,6 +6225,22 @@ export {
|
|
|
6230
6225
|
INSTALL_OWNER_FILE,
|
|
6231
6226
|
VISITOR_TOKEN_SECRET_FILE,
|
|
6232
6227
|
CLAUDE_CREDENTIALS_FILE,
|
|
6228
|
+
SYSTEM_PATH_SLUGS,
|
|
6229
|
+
PLATFORM_ROOT2 as PLATFORM_ROOT,
|
|
6230
|
+
ACCOUNTS_DIR,
|
|
6231
|
+
listValidAccounts,
|
|
6232
|
+
isValidAccountId,
|
|
6233
|
+
resolveAccount,
|
|
6234
|
+
validateAgentSlug,
|
|
6235
|
+
resolveDefaultAgentSlug,
|
|
6236
|
+
resolveAgentConfig,
|
|
6237
|
+
isActiveAgentSlug,
|
|
6238
|
+
getDefaultAccountId,
|
|
6239
|
+
sampleCpu,
|
|
6240
|
+
sampleProcessCpu,
|
|
6241
|
+
registerLoop,
|
|
6242
|
+
listLoops,
|
|
6243
|
+
armedCount,
|
|
6233
6244
|
vncLog,
|
|
6234
6245
|
newCorrId,
|
|
6235
6246
|
sanitizeClientCorrId,
|
|
@@ -6254,23 +6265,12 @@ export {
|
|
|
6254
6265
|
parseCookieValue,
|
|
6255
6266
|
isApiRequestPath,
|
|
6256
6267
|
AGENT_SLUG_PATTERN,
|
|
6257
|
-
SYSTEM_PATH_SLUGS,
|
|
6258
6268
|
PUBLIC_ALLOWED_PREFIXES,
|
|
6259
6269
|
PUBLIC_ALLOWED_EXACT,
|
|
6260
6270
|
isPublicPathAllowed,
|
|
6261
6271
|
isOperatorHost,
|
|
6262
6272
|
classifyHost,
|
|
6263
6273
|
getOperatorDomains,
|
|
6264
|
-
PLATFORM_ROOT2 as PLATFORM_ROOT,
|
|
6265
|
-
ACCOUNTS_DIR,
|
|
6266
|
-
listValidAccounts,
|
|
6267
|
-
isValidAccountId,
|
|
6268
|
-
resolveAccount,
|
|
6269
|
-
validateAgentSlug,
|
|
6270
|
-
resolveDefaultAgentSlug,
|
|
6271
|
-
resolveAgentConfig,
|
|
6272
|
-
isActiveAgentSlug,
|
|
6273
|
-
getDefaultAccountId,
|
|
6274
6274
|
fingerprintSessionKey,
|
|
6275
6275
|
registerSession,
|
|
6276
6276
|
validateSession,
|
|
@@ -6321,6 +6321,7 @@ export {
|
|
|
6321
6321
|
projectAgent,
|
|
6322
6322
|
deleteAgentProjection,
|
|
6323
6323
|
safeJson,
|
|
6324
|
+
resolveCacheKey,
|
|
6324
6325
|
requireAdminSession,
|
|
6325
6326
|
tryCookieBridgeForConversation,
|
|
6326
6327
|
requireAdminCookie,
|