@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.
Files changed (75) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/account-schema-regions/dist/index.d.ts +60 -0
  3. package/payload/platform/lib/account-schema-regions/dist/index.d.ts.map +1 -0
  4. package/payload/platform/lib/account-schema-regions/dist/index.js +101 -0
  5. package/payload/platform/lib/account-schema-regions/dist/index.js.map +1 -0
  6. package/payload/platform/lib/account-schema-regions/package.json +7 -0
  7. package/payload/platform/lib/account-schema-regions/src/index.ts +121 -0
  8. package/payload/platform/lib/account-schema-regions/tsconfig.json +9 -0
  9. package/payload/platform/package.json +3 -3
  10. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +59 -17
  11. package/payload/platform/plugins/admin/skills/public-agent-manager/SKILL.md +3 -3
  12. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +6 -0
  13. package/payload/platform/plugins/docs/references/admin-ui.md +58 -16
  14. package/payload/platform/scripts/check-no-esm-require.mjs +5 -0
  15. package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.d.ts +1 -1
  16. package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.d.ts.map +1 -1
  17. package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.js +50 -3
  18. package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.js.map +1 -1
  19. package/payload/server/{chunk-HN4HFTXE.js → chunk-5ADBNEJA.js} +1410 -1409
  20. package/payload/server/maxy-edge.js +1 -1
  21. package/payload/server/public/activity.html +5 -5
  22. package/payload/server/public/agents.html +4 -4
  23. package/payload/server/public/assets/{AdminLoginScreens-DeWt3H7l.js → AdminLoginScreens-D5KOQA8S.js} +1 -1
  24. package/payload/server/public/assets/{AdminShell-u32d8puE.js → AdminShell-C8wazpnz.js} +2 -2
  25. package/payload/server/public/assets/{activity-DjP45-DD.js → activity-CFqkDhTs.js} +1 -1
  26. package/payload/server/public/assets/admin-B-EGUWS4.js +1 -0
  27. package/payload/server/public/assets/agents-DPulfmrd.js +1 -0
  28. package/payload/server/public/assets/{browser-ULvl_K5x.js → browser-T_5xbnJh.js} +1 -1
  29. package/payload/server/public/assets/{calendar-CGMrT6WZ.js → calendar-CKjuiG8c.js} +1 -1
  30. package/payload/server/public/assets/chat-DWLYAA70.js +1 -0
  31. package/payload/server/public/assets/chevron-left-DBMCTKlT.js +1 -0
  32. package/payload/server/public/assets/chevron-right-SBAr6zzc.js +1 -0
  33. package/payload/server/public/assets/clock-kkzt7Scf.js +1 -0
  34. package/payload/server/public/assets/data-CV8N9qcy.js +1 -0
  35. package/payload/server/public/assets/{file-text-C5K6DMcK.js → file-text-c6cA4Evd.js} +1 -1
  36. package/payload/server/public/assets/{graph-CO6e1bgD.js → graph-DIk-o45J.js} +2 -2
  37. package/payload/server/public/assets/{graph-labels-iliQjlfE.js → graph-labels-5bjxhC_p.js} +1 -1
  38. package/payload/server/public/assets/{maximize-2-D-64XK6V.js → maximize-2-BCQkFb1_.js} +1 -1
  39. package/payload/server/public/assets/{operator-DyXgogJ1.js → operator-0UnNo2ms.js} +1 -1
  40. package/payload/server/public/assets/page-HVtUfub2.js +32 -0
  41. package/payload/server/public/assets/page-JJa2xdzf.js +1 -0
  42. package/payload/server/public/assets/{public-B9NVDBxW.js → public-CKEYyIPn.js} +1 -1
  43. package/payload/server/public/assets/{rotate-ccw-P35h7ogS.js → rotate-ccw-dTpO5dhL.js} +1 -1
  44. package/payload/server/public/assets/{routines-BeiynLF7.js → routines-C6P1-Dto.js} +1 -1
  45. package/payload/server/public/assets/{skills-C5H7ohLp.js → skills-ClrpVqlz.js} +1 -1
  46. package/payload/server/public/assets/tasks-C8SNhSFC.js +1 -0
  47. package/payload/server/public/assets/{time-entry-format-CDhHCMrW.js → time-entry-format-CCO6N6XZ.js} +1 -1
  48. package/payload/server/public/assets/{triangle-alert-OgY71rDV.js → triangle-alert-CHjdKpag.js} +1 -1
  49. package/payload/server/public/assets/{useCopyFeedback-jOGmWwa3.js → useCopyFeedback-BTUu3-l8.js} +1 -1
  50. package/payload/server/public/assets/{useSubAccountSwitcher-6AFuRawP.js → useSubAccountSwitcher-DSW9vDi2.js} +1 -1
  51. package/payload/server/public/assets/{useSubAccountSwitcher-AFSwtthL.css → useSubAccountSwitcher-DyrH8DA8.css} +1 -1
  52. package/payload/server/public/assets/{useVoiceRecorder-CnE3fhfV.js → useVoiceRecorder-BeNqxmm2.js} +1 -1
  53. package/payload/server/public/assets/{wrench-B1hsCj0R.js → wrench-CeNnc7oO.js} +1 -1
  54. package/payload/server/public/browser.html +4 -4
  55. package/payload/server/public/calendar.html +7 -7
  56. package/payload/server/public/chat.html +13 -13
  57. package/payload/server/public/data.html +11 -11
  58. package/payload/server/public/graph.html +9 -9
  59. package/payload/server/public/index.html +14 -14
  60. package/payload/server/public/operator.html +14 -14
  61. package/payload/server/public/public.html +13 -13
  62. package/payload/server/public/routines.html +6 -6
  63. package/payload/server/public/skills.html +5 -5
  64. package/payload/server/public/tasks.html +6 -6
  65. package/payload/server/server.js +1329 -826
  66. package/payload/server/public/assets/admin-Bo-Y6Vjx.js +0 -1
  67. package/payload/server/public/assets/agents-C80aCXd8.js +0 -1
  68. package/payload/server/public/assets/chat-DnWXaWD4.js +0 -1
  69. package/payload/server/public/assets/chevron-left-BB5Iljw-.js +0 -1
  70. package/payload/server/public/assets/chevron-right-DijU96_j.js +0 -1
  71. package/payload/server/public/assets/clock-DsmnhhJh.js +0 -1
  72. package/payload/server/public/assets/data-JPlhOLGC.js +0 -1
  73. package/payload/server/public/assets/page-DrbiAFS-.js +0 -32
  74. package/payload/server/public/assets/page-WeM2ry61.js +0 -1
  75. 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 as readFileSync2 } from "fs";
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(readFileSync2(brandPath, "utf-8"));
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/vnc-logger.ts
2974
- import { appendFileSync, mkdirSync } from "fs";
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
- var VNC_LOG_FILE = resolve2(LOG_DIR, "vnc-boot.log");
2977
- try {
2978
- mkdirSync(LOG_DIR, { recursive: true });
2979
- } catch (err) {
2980
- console.error(`[vnc-log-fail] mkdir ${LOG_DIR} failed: ${err.message}`);
2981
- }
2982
- function vncLog(phase, fields = {}) {
2983
- const ts = (/* @__PURE__ */ new Date()).toISOString();
2984
- const kv = Object.entries(fields).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(" ");
2985
- const line = kv.length > 0 ? `[${ts}] [${phase}] ${kv}
2986
- ` : `[${ts}] [${phase}]
2987
- `;
2988
- try {
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
- var corrCounter = 0;
2995
- function newCorrId() {
2996
- corrCounter = corrCounter + 1 & 4294967295;
2997
- const counterHex = corrCounter.toString(16).padStart(8, "0");
2998
- const randomHex = Math.floor(Math.random() * 16777215).toString(16).padStart(6, "0");
2999
- return `${counterHex}-${randomHex}`;
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
- mkdirSync2(dirname(audit.logFile), { recursive: true, mode: 448 });
3039
- appendFileSync2(audit.logFile, line, { mode: 384 });
2778
+ parsed = JSON.parse(readFileSync2(path, "utf-8"));
3040
2779
  } catch (err) {
3041
- console.error(
3042
- `[users-audit] op=write-failed logFile=${audit.logFile} error=${err instanceof Error ? err.message : String(err)}`
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 scryptAsync(password, salt) {
3047
- return new Promise((resolve7, reject) => {
3048
- scrypt(password, salt, SCRYPT_KEYLEN, { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P }, (err, key) => {
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
- computed = await scryptAsync(password, salt);
3068
- } catch {
3069
- return false;
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
- return timingSafeEqual(computed, storedHash);
3072
- }
3073
- function readUsers(usersFile) {
3074
- if (!existsSync2(usersFile)) return [];
3075
- const raw2 = readFileSync3(usersFile, "utf-8").trim();
3076
- if (!raw2) return [];
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
- function writeUsersAtomic(usersFile, users) {
3082
- const tmp = `${usersFile}.tmp-${process.pid}`;
3083
- writeFileSync(tmp, JSON.stringify(users, null, 2) + "\n", { mode: 384 });
3084
- renameSync(tmp, usersFile);
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
- async function setAccessPassword(userId, password, usersFile, audit) {
3087
- const users = readUsers(usersFile);
3088
- const idx = users.findIndex((u) => u.userId === userId);
3089
- if (idx === -1) throw new Error(`no users.json record for userId=${userId}`);
3090
- if (await accessPasswordCollides(password, usersFile, userId)) {
3091
- throw new Error("accessHash collision: password already in use by another admin");
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
- const rows = rowIds(users);
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
- async function verifyAccessPassword(password, usersFile) {
3099
- let users;
3100
- try {
3101
- users = readUsers(usersFile);
3102
- } catch {
3103
- return null;
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
- for (const u of users) {
3106
- if (typeof u.accessHash === "string" && await matchesHash(password, u.accessHash)) {
3107
- return u.userId;
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
- return null;
3111
- }
3112
- async function accessPasswordCollides(password, usersFile, excludeUserId) {
3113
- let users;
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
- for (const u of users) {
3120
- if (u.userId === excludeUserId) continue;
3121
- if (typeof u.accessHash === "string" && await matchesHash(password, u.accessHash)) {
3122
- return true;
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 false;
2916
+ return raw2;
3126
2917
  }
3127
- function anyAccessPasswordConfigured(usersFile) {
3128
- let users;
3129
- try {
3130
- users = readUsers(usersFile);
3131
- } catch {
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
- return users.some((u) => typeof u.accessHash === "string" && u.accessHash.length > 0);
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 migrateLegacyRemotePassword(usersFile, legacyFile, ownerUserId, audit) {
3146
- if (!existsSync2(legacyFile)) return "noop-no-legacy";
3147
- const legacy = readFileSync3(legacyFile, "utf-8").trim();
3148
- const users = readUsers(usersFile);
3149
- const idx = users.findIndex((u) => u.userId === ownerUserId);
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
- if (!legacy.includes(":")) {
3157
- unlinkSync(legacyFile);
3158
- return "noop-no-legacy";
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
- const rows = rowIds(users);
3161
- users[idx] = { ...users[idx], accessHash: legacy };
3162
- writeUsersAtomic(usersFile, users);
3163
- unlinkSync(legacyFile);
3164
- appendUsersAuditLine(audit, { action: "migrate", field: "accessHash", rowsBefore: rows, rowsAfter: rows });
3165
- return "migrated";
3166
- }
3167
- function readLegacyRemoteHash(legacyFile) {
3168
- if (!existsSync2(legacyFile)) return null;
3169
- const raw2 = readFileSync3(legacyFile, "utf-8").trim();
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
- async function setRemotePassword(password) {
3213
- const hash = await hashPassword(password);
3214
- writeFileSync2(REMOTE_PASSWORD_FILE, hash, "utf-8");
2957
+ function estimateTokens(text) {
2958
+ return Math.ceil(text.length / 4);
3215
2959
  }
3216
- async function verifyRemotePassword(password) {
3217
- const matchedUserId = await verifyAccessPassword(password, USERS_FILE);
3218
- if (matchedUserId) return { ok: true, hashBytes: SCRYPT_KEYLEN2, userId: matchedUserId };
3219
- const legacy = readLegacyRemoteHash(REMOTE_PASSWORD_FILE);
3220
- if (legacy && await matchesHash(password, legacy)) {
3221
- return { ok: true, hashBytes: SCRYPT_KEYLEN2, userId: null };
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
- const configured = anyAccessPasswordConfigured(USERS_FILE) || legacy !== null;
3224
- return { ok: false, why: configured ? "password-mismatch" : "no-password-file", hashBytes: 0 };
3225
- }
3226
- var SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
3227
- var TOKEN_PREFIX = "v1.";
3228
- var SECRET_BYTES = 32;
3229
- var NONCE_BYTES = 16;
3230
- var cachedSecret = null;
3231
- function getSecret() {
3232
- if (cachedSecret) return cachedSecret;
3233
- try {
3234
- const hex2 = readFileSync4(REMOTE_SESSION_SECRET_FILE, "utf-8").trim();
3235
- if (hex2.length === SECRET_BYTES * 2) {
3236
- cachedSecret = Buffer.from(hex2, "hex");
3237
- return cachedSecret;
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
- } catch {
3021
+ knowledgeBaked = true;
3022
+ } else if (hasKnowledge) {
3023
+ knowledge = readFileSync3(knowledgePath, "utf-8");
3024
+ knowledgeBaked = true;
3240
3025
  }
3241
- const fresh = randomBytes2(SECRET_BYTES).toString("hex");
3242
- try {
3243
- mkdirSync3(dirname2(REMOTE_SESSION_SECRET_FILE), { recursive: true, mode: 448 });
3244
- writeFileSync2(REMOTE_SESSION_SECRET_FILE, fresh, { mode: 384, flag: "wx" });
3245
- } catch {
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
- const hex = readFileSync4(REMOTE_SESSION_SECRET_FILE, "utf-8").trim();
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 base64urlDecode(s) {
3255
- const padded = s.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - s.length % 4) % 4);
3256
- return Buffer.from(padded, "base64");
3043
+ function isActiveAgentSlug(accountDir, slug) {
3044
+ return resolveAgentConfig(accountDir, slug).status === "active";
3257
3045
  }
3258
- function signPayload(payloadJson, secret) {
3259
- return base64urlEncode(createHmac("sha256", secret).update(payloadJson).digest());
3046
+ function getDefaultAccountId() {
3047
+ return resolveAccount()?.accountId ?? null;
3260
3048
  }
3261
- function createRemoteSession(opts = {}) {
3262
- const payload = { c: opts.now ?? Date.now(), n: randomBytes2(NONCE_BYTES).toString("hex") };
3263
- const payloadJson = JSON.stringify(payload);
3264
- const payloadB64 = base64urlEncode(Buffer.from(payloadJson, "utf-8"));
3265
- const sig = signPayload(payloadJson, getSecret());
3266
- return `${TOKEN_PREFIX}${payloadB64}.${sig}`;
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 parseToken(token) {
3269
- if (!token.startsWith(TOKEN_PREFIX)) return null;
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
- payload = JSON.parse(payloadJson);
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 isPrivateIp(ip) {
3319
- const parts = ip.split(".");
3320
- if (parts.length !== 4) return false;
3321
- const a = parseInt(parts[0], 10);
3322
- const b = parseInt(parts[1], 10);
3323
- if (Number.isNaN(a) || Number.isNaN(b)) return false;
3324
- if (a === 10) return true;
3325
- if (a === 172 && b >= 16 && b <= 31) return true;
3326
- if (a === 192 && b === 168) return true;
3327
- if (a === 169 && b === 254) return true;
3328
- return false;
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
- function classifyNormalizedIp(ip) {
3331
- if (isLoopback(ip) || isPrivateIp(ip)) return "lan";
3332
- return "external";
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 resolveClientIp(remoteAddress, xForwardedFor) {
3335
- const remote = normalizeIp(remoteAddress);
3336
- if (!remote) return { ip: "", kind: "unknown" };
3337
- if (isLoopback(remote) && xForwardedFor) {
3338
- const firstIp = normalizeIp(xForwardedFor.split(",")[0]?.trim());
3339
- if (firstIp) {
3340
- return { ip: firstIp, kind: classifyNormalizedIp(firstIp) };
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
- return { ip: remote, kind: classifyNormalizedIp(remote) };
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
- var MAX_ATTEMPTS = 5;
3346
- var LOCKOUT_MS = 15 * 60 * 1e3;
3347
- var LOCAL_BUCKET = "__local__";
3348
- var rateLimitMap = /* @__PURE__ */ new Map();
3349
- function bucketKey(client) {
3350
- return client.kind === "external" ? client.ip : LOCAL_BUCKET;
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 checkRateLimit(client) {
3353
- const key = bucketKey(client);
3354
- const rec = rateLimitMap.get(key);
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
- if (rec.lockedUntil > 0) {
3361
- rateLimitMap.delete(key);
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
- return null;
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 recordFailedAttempt(client) {
3366
- const key = bucketKey(client);
3367
- const rec = rateLimitMap.get(key) ?? { count: 0, lockedUntil: 0 };
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
- rateLimitMap.set(key, rec);
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 clearRateLimit(client) {
3376
- rateLimitMap.delete(bucketKey(client));
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
- pw.addEventListener('input', check);
3815
- confirm.addEventListener('input', check);
3816
- })();
3817
- </script>
3818
- </body>
3819
- </html>`;
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 escapeHtml(str) {
3822
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
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
- // app/lib/auth-gate.ts
3826
- function canAccessAdmin(input) {
3827
- const client = resolveClientIp(input.remoteAddress, input.xForwardedFor);
3828
- if (input.isPublicHost(input.host)) {
3829
- return { allow: false, reason: "public-host", client };
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
- if (client.kind !== "external") {
3832
- return { allow: true, client };
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
- if (!isRemoteAuthConfigured()) {
3835
- return { allow: false, reason: "remote-unconfigured", client };
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 token = parseCookieValue(input.cookieHeader, "__remote_session");
3838
- if (token && validateRemoteSession(token)) {
3839
- return { allow: true, client };
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
- return { allow: false, reason: "unauthorized", client };
3524
+ const configured = anyAccessPasswordConfigured(USERS_FILE) || legacy !== null;
3525
+ return { ok: false, why: configured ? "password-mismatch" : "no-password-file", hashBytes: 0 };
3842
3526
  }
3843
- function parseCookieValue(cookieHeader, name) {
3844
- if (!cookieHeader) return null;
3845
- const match2 = cookieHeader.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`));
3846
- if (!match2) return null;
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
- return decodeURIComponent(match2[1]);
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 isApiRequestPath(path) {
3854
- return path.startsWith("/api/");
3552
+ function base64urlEncode(buf) {
3553
+ return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
3855
3554
  }
3856
-
3857
- // app/lib/agent-slug-pattern.ts
3858
- var AGENT_SLUG_PATTERN = /^\/([a-z][a-z0-9-]{2,49})$/;
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 isOperatorHost(host, operatorDomains2) {
3906
- return operatorDomains2.has(host);
3559
+ function signPayload(payloadJson, secret) {
3560
+ return base64urlEncode(createHmac("sha256", secret).update(payloadJson).digest());
3907
3561
  }
3908
- function classifyHost(host, operatorDomains2, isPublicHost) {
3909
- if (operatorDomains2.has(host)) return "operator";
3910
- if (isPublicHost(host)) return "public";
3911
- return "admin";
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
- // server/operator-domains-reader.ts
3915
- import { watchFile } from "fs";
3916
-
3917
- // app/lib/operator-domains.ts
3918
- import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
3919
- import { dirname as dirname3, resolve as resolve3 } from "path";
3920
- var OPERATOR_DOMAINS_PATH = resolve3(MAXY_DIR, "operator-domains.json");
3921
- function loadOperatorDomains() {
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
- const parsed = JSON.parse(readFileSync5(OPERATOR_DOMAINS_PATH, "utf-8"));
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 /* @__PURE__ */ new Set();
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
- // server/operator-domains-reader.ts
3933
- var operatorDomains = loadOperatorDomains();
3934
- if (operatorDomains.size > 0) {
3935
- console.log(
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
- watchFile(OPERATOR_DOMAINS_PATH, { interval: 2e3 }, () => {
3940
- operatorDomains = loadOperatorDomains();
3941
- console.log(
3942
- `[operator-domains] reloaded ${operatorDomains.size} operator domain(s): ${[...operatorDomains].join(", ")}`
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
- // app/lib/claude-agent/account.ts
3950
- import { resolve as resolve4 } from "path";
3951
- import { readFileSync as readFileSync7, readdirSync, existsSync as existsSync5, statSync } from "fs";
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 getBrandProductName() {
3968
- if (cachedProductName !== null) return cachedProductName;
3969
- const path = brandJsonPath();
3970
- if (!existsSync4(path)) {
3971
- throw new Error(`[skill-loader] brand.json missing at ${path}`);
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
- let parsed;
3974
- try {
3975
- parsed = JSON.parse(readFileSync6(path, "utf-8"));
3976
- } catch (err) {
3977
- throw new Error(
3978
- `[skill-loader] brand.json unreadable at ${path}: ${err instanceof Error ? err.message : String(err)}`
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
- const value = parsed.productName;
3982
- if (typeof value !== "string" || value.trim() === "") {
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
- cachedProductName = value;
3988
- return value;
3664
+ return null;
3989
3665
  }
3990
- function substituteBrandPlaceholders(content, sourcePath) {
3991
- if (!content.includes(PLACEHOLDER)) return content;
3992
- let productName;
3993
- try {
3994
- productName = getBrandProductName();
3995
- } catch (err) {
3996
- console.error(
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
- const occurrences = content.split(PLACEHOLDER).length - 1;
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
- // app/lib/claude-agent/account.ts
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
- var _stubDirsLoggedThisBoot = /* @__PURE__ */ new Set();
4019
- function listValidAccounts() {
4020
- if (!existsSync5(ACCOUNTS_DIR)) return [];
4021
- const out = [];
4022
- const entries2 = readdirSync(ACCOUNTS_DIR, { withFileTypes: true });
4023
- for (const entry of entries2) {
4024
- if (!entry.isDirectory()) continue;
4025
- if (entry.name.startsWith(".")) continue;
4026
- const accountDir = resolve4(ACCOUNTS_DIR, entry.name);
4027
- const configPath = resolve4(accountDir, "account.json");
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
- let config;
4038
- try {
4039
- config = JSON.parse(readFileSync7(configPath, "utf-8"));
4040
- } catch {
4041
- console.error(
4042
- `[platform] accounts-state CORRUPT-JSON id=${entry.name} \u2014 account.json failed to parse; treating as stub`
4043
- );
4044
- continue;
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
- out.push({ accountId: config.accountId, accountDir, config });
4047
- }
4048
- return out;
4049
- }
4050
- function isValidAccountId(id) {
4051
- return listValidAccounts().some((a) => a.accountId === id);
4052
- }
4053
- function resolveAccount() {
4054
- if (!existsSync5(ACCOUNTS_DIR)) return null;
4055
- let usersJsonUserId = null;
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
- const entries2 = readdirSync(ACCOUNTS_DIR, { withFileTypes: true });
4069
- let fallback = null;
4070
- for (const entry of entries2) {
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
- if (!config.adminModel || !config.publicModel) {
4083
- throw new Error(
4084
- `[maxy] account.json at ${configPath} is missing required model fields (adminModel / publicModel). Update account.json with valid model identifiers.`
4085
- );
3936
+
3937
+ @media (max-width: 440px) {
3938
+ .page { padding: 24px 16px; }
4086
3939
  }
4087
- const result = {
4088
- accountId: config.accountId,
4089
- accountDir: resolve4(ACCOUNTS_DIR, entry.name),
4090
- config
4091
- };
4092
- if (usersJsonUserId && config.admins?.some((a) => a.userId === usersJsonUserId)) {
4093
- return result;
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
- if (!fallback) {
4096
- fallback = result;
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
- if (usersJsonUserId && fallback) {
4100
- console.warn(
4101
- `[maxy] resolveAccount: no account matches users.json userId ${usersJsonUserId} \u2014 falling back to ${fallback.accountId}`
4102
- );
4103
- }
4104
- return fallback;
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 readAgentFile(accountDir, agentName, filename) {
4107
- const filePath = resolve4(accountDir, "agents", agentName, filename);
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
4114
4124
  }
4115
- var RESERVED_SLUGS = /* @__PURE__ */ new Set(["admin", "api", "assets", "agent-assets", "brand", "bot", "privacy"]);
4116
- var SHADOWED_SLUGS = new Set(SYSTEM_PATH_SLUGS);
4117
- var SLUG_PATTERN = /^[a-z][a-z0-9-]{2,49}$/;
4118
- function validateAgentSlug(slug) {
4119
- if (!SLUG_PATTERN.test(slug)) return false;
4120
- if (RESERVED_SLUGS.has(slug)) {
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
- return true;
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
- let config;
4137
- try {
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
- if (!config.defaultAgent) {
4144
- console.error("[agent-resolve] defaultAgent not configured in account.json \u2014 set it via the connect-whatsapp skill");
4145
- return null;
4138
+ const token = parseCookieValue(input.cookieHeader, "__remote_session");
4139
+ if (token && validateRemoteSession(token)) {
4140
+ return { allow: true, client };
4146
4141
  }
4147
- const agentConfigPath = resolve4(accountDir, "agents", config.defaultAgent, "config.json");
4148
- if (!existsSync5(agentConfigPath)) {
4149
- console.error(`[agent-resolve] defaultAgent="${config.defaultAgent}" has no config.json at ${agentConfigPath}`);
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 estimateTokens(text) {
4155
- return Math.ceil(text.length / 4);
4154
+ function isApiRequestPath(path) {
4155
+ return path.startsWith("/api/");
4156
4156
  }
4157
- function resolveAgentConfig(accountDir, agentName) {
4158
- let model = null;
4159
- let status = null;
4160
- let displayName = null;
4161
- let image = null;
4162
- let imageShape = null;
4163
- let showAgentName = false;
4164
- let knowledgeKeywords = null;
4165
- let accessMode = "open";
4166
- const MAX_KNOWLEDGE_KEYWORDS = 5;
4167
- const configRaw = readAgentFile(accountDir, agentName, "config.json");
4168
- if (configRaw) {
4169
- let parsed;
4170
- try {
4171
- parsed = JSON.parse(configRaw);
4172
- } catch {
4173
- console.warn(`[agent-config] ${agentName}/config.json: invalid JSON \u2014 using defaults`);
4174
- parsed = {};
4175
- }
4176
- model = typeof parsed.model === "string" ? parsed.model : null;
4177
- status = typeof parsed.status === "string" ? parsed.status : null;
4178
- displayName = typeof parsed.displayName === "string" ? parsed.displayName : null;
4179
- image = typeof parsed.image === "string" ? parsed.image : null;
4180
- if (typeof parsed.imageShape === "string" && ["circle", "rounded"].includes(parsed.imageShape)) {
4181
- imageShape = parsed.imageShape;
4182
- }
4183
- if (parsed.showAgentName === true) {
4184
- showAgentName = true;
4185
- } else if (parsed.showAgentName === "none") {
4186
- showAgentName = "none";
4187
- }
4188
- if (image || imageShape || showAgentName) {
4189
- console.log(`[agent-config] ${agentName}: image=${image || "(none)"} imageShape=${imageShape || "(none)"} showAgentName=${showAgentName}`);
4190
- }
4191
- if (typeof parsed.accessMode === "string" && ["gated", "paid"].includes(parsed.accessMode)) {
4192
- accessMode = parsed.accessMode;
4193
- }
4194
- if (Array.isArray(parsed.knowledgeKeywords) && parsed.knowledgeKeywords.length > 0) {
4195
- const filtered = parsed.knowledgeKeywords.filter((k) => typeof k === "string" && k.trim()).map((k) => k.replace(/,/g, "").trim().toLowerCase()).filter(Boolean);
4196
- if (filtered.length > MAX_KNOWLEDGE_KEYWORDS) {
4197
- console.warn(`[agent-config] ${agentName}: knowledgeKeywords has ${filtered.length} entries \u2014 capping at ${MAX_KNOWLEDGE_KEYWORDS}`);
4198
- }
4199
- knowledgeKeywords = filtered.length > 0 ? filtered.slice(0, MAX_KNOWLEDGE_KEYWORDS) : null;
4200
- }
4201
- }
4202
- let knowledge = null;
4203
- let knowledgeBaked = false;
4204
- const agentDir = resolve4(accountDir, "agents", agentName);
4205
- const knowledgePath = resolve4(agentDir, "KNOWLEDGE.md");
4206
- const summaryPath = resolve4(agentDir, "KNOWLEDGE-SUMMARY.md");
4207
- const hasKnowledge = existsSync5(knowledgePath);
4208
- const hasSummary = existsSync5(summaryPath);
4209
- if (hasKnowledge && hasSummary) {
4210
- const knowledgeMtime = statSync(knowledgePath).mtimeMs;
4211
- const summaryMtime = statSync(summaryPath).mtimeMs;
4212
- if (summaryMtime >= knowledgeMtime) {
4213
- knowledge = readFileSync7(summaryPath, "utf-8");
4214
- } else {
4215
- console.warn(`[agent-config] ${agentName}: KNOWLEDGE-SUMMARY.md is stale (KNOWLEDGE.md is newer) \u2014 using full knowledge`);
4216
- knowledge = readFileSync7(knowledgePath, "utf-8");
4217
- }
4218
- knowledgeBaked = true;
4219
- } else if (hasKnowledge) {
4220
- knowledge = readFileSync7(knowledgePath, "utf-8");
4221
- knowledgeBaked = true;
4222
- }
4223
- let budget = null;
4224
- const identityRaw = readAgentFile(accountDir, agentName, "IDENTITY.md");
4225
- const soulRaw = readAgentFile(accountDir, agentName, "SOUL.md");
4226
- if (identityRaw || soulRaw || knowledge) {
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
- function isActiveAgentSlug(accountDir, slug) {
4241
- return resolveAgentConfig(accountDir, slug).status === "active";
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
- function getDefaultAccountId() {
4244
- return resolveAccount()?.accountId ?? null;
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,