@rubytech/create-realagent 1.0.854 → 1.0.856

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 (40) hide show
  1. package/dist/__tests__/account-id-env.test.js +47 -0
  2. package/dist/__tests__/port-canonicalisation.test.js +1 -0
  3. package/dist/index.js +51 -17
  4. package/dist/port-resolution.js +9 -0
  5. package/package.json +1 -1
  6. package/payload/platform/lib/account-enumeration/dist/__tests__/validate-env.test.d.ts +2 -0
  7. package/payload/platform/lib/account-enumeration/dist/__tests__/validate-env.test.d.ts.map +1 -0
  8. package/payload/platform/lib/account-enumeration/dist/__tests__/validate-env.test.js +55 -0
  9. package/payload/platform/lib/account-enumeration/dist/__tests__/validate-env.test.js.map +1 -0
  10. package/payload/platform/lib/account-enumeration/dist/index.d.ts +26 -0
  11. package/payload/platform/lib/account-enumeration/dist/index.d.ts.map +1 -1
  12. package/payload/platform/lib/account-enumeration/dist/index.js +13 -0
  13. package/payload/platform/lib/account-enumeration/dist/index.js.map +1 -1
  14. package/payload/platform/lib/account-enumeration/src/__tests__/validate-env.test.ts +57 -0
  15. package/payload/platform/lib/account-enumeration/src/index.ts +44 -0
  16. package/payload/platform/plugins/cloudflare/references/manual-setup.md +2 -0
  17. package/payload/platform/plugins/cloudflare/scripts/list-cf-domains.sh +39 -10
  18. package/payload/platform/plugins/cloudflare/scripts/list-cf-domains.ts +112 -20
  19. package/payload/platform/plugins/docs/references/cloudflare.md +1 -0
  20. package/payload/platform/plugins/docs/references/internals.md +2 -0
  21. package/payload/platform/plugins/whatsapp/PLUGIN.md +1 -1
  22. package/payload/server/public/assets/{Checkbox-U-H3_oQu.js → Checkbox-BySsatDO.js} +1 -1
  23. package/payload/server/public/assets/{admin-DZ8Ke7t3.js → admin-CZpefPcA.js} +2 -2
  24. package/payload/server/public/assets/data-BuuqlV4L.js +1 -0
  25. package/payload/server/public/assets/graph-CtVITeok.js +1 -0
  26. package/payload/server/public/assets/jsx-runtime-O5ef8xK8.css +1 -0
  27. package/payload/server/public/assets/{page-D_6h4ZZy.js → page-Ddc_nKh8.js} +1 -1
  28. package/payload/server/public/assets/{page-CNKytKTe.js → page-IQBQoOdT.js} +1 -1
  29. package/payload/server/public/assets/{public-DApUXgoq.js → public-BhyNH7eq.js} +1 -1
  30. package/payload/server/public/assets/{useAdminFetch-Cex4bYm7.js → useAdminFetch-B3MO55eB.js} +1 -1
  31. package/payload/server/public/assets/{useVoiceRecorder-CI8GpxfU.js → useVoiceRecorder-B_zVS4Oe.js} +1 -1
  32. package/payload/server/public/data.html +5 -5
  33. package/payload/server/public/graph.html +6 -6
  34. package/payload/server/public/index.html +8 -8
  35. package/payload/server/public/public.html +5 -5
  36. package/payload/server/server.js +32 -3
  37. package/payload/server/public/assets/data-BbczthXl.js +0 -1
  38. package/payload/server/public/assets/graph-DzK_bDyH.js +0 -1
  39. package/payload/server/public/assets/jsx-runtime-OD2WKrlG.css +0 -1
  40. /package/payload/server/public/assets/{jsx-runtime-BjkIZEse.js → jsx-runtime-DnY0498s.js} +0 -0
@@ -0,0 +1,47 @@
1
+ // Task 955 — acceptance gate for ACCOUNT_ID stamping in the brand systemd unit.
2
+ //
3
+ // Two invariants this test protects:
4
+ // (a) buildMaxyUnitFile emits a literal `Environment=ACCOUNT_ID=<uuid>` line
5
+ // in the [Service] block when accountId is provided. Pi recovery via
6
+ // `npx -y @rubytech/create-maxy@latest` depends on this line landing —
7
+ // without it, process.env.ACCOUNT_ID is undefined and the
8
+ // writeNodeWithEdges gate rejects every CF-setup graph write at
9
+ // cloudflare-task-tracker.ts:187/326/347/559.
10
+ // (b) buildMaxyUnitFile throws on an empty accountId. Falling through with
11
+ // "" would emit a unit with no ACCOUNT_ID line; the boot validator at
12
+ // platform/ui/server/index.ts would then FATAL reason=missing on every
13
+ // restart, surfacing the same regression class behind a different log
14
+ // prefix. Throwing at build time aborts the install loud, before the
15
+ // broken unit is written.
16
+ import test from "node:test";
17
+ import assert from "node:assert/strict";
18
+ import { buildMaxyUnitFile } from "../port-resolution.js";
19
+ const VALID_UUID = "12345678-9abc-def0-1234-56789abcdef0";
20
+ function callBuildUnit(accountId) {
21
+ return buildMaxyUnitFile({
22
+ productName: "Maxy",
23
+ brandHostname: "maxy",
24
+ neo4jDedicated: false,
25
+ installDir: "/home/me/maxy",
26
+ persistDir: "/home/me/.maxy",
27
+ port: 19200,
28
+ maxyUiInternalPort: 19201,
29
+ vncDisplay: 99,
30
+ rfbPort: 5900,
31
+ websockifyPort: 6080,
32
+ cdpPort: 9222,
33
+ chromiumBin: "/usr/bin/chromium",
34
+ accountId,
35
+ });
36
+ }
37
+ test("buildMaxyUnitFile emits Environment=ACCOUNT_ID=<uuid> for a valid accountId", () => {
38
+ const unit = callBuildUnit(VALID_UUID);
39
+ assert.match(unit, /^Environment=ACCOUNT_ID=12345678-9abc-def0-1234-56789abcdef0$/m, "ACCOUNT_ID line missing or malformed");
40
+ // Placement matters: identity (NODE_ENV → ACCOUNT_ID → PORT) so the most
41
+ // fundamental brand identity sits next to the env that selects the brand
42
+ // runtime. Anchored regex against the three adjacent lines.
43
+ assert.match(unit, /Environment=NODE_ENV=production\nEnvironment=ACCOUNT_ID=12345678-9abc-def0-1234-56789abcdef0\nEnvironment=PORT=19200/, "ACCOUNT_ID line is not placed between NODE_ENV and PORT");
44
+ });
45
+ test("buildMaxyUnitFile throws when accountId is empty", () => {
46
+ assert.throws(() => callBuildUnit(""), /accountId is required/, "expected throw on empty accountId, did not throw");
47
+ });
@@ -33,6 +33,7 @@ function makeUnitFile(port, internal) {
33
33
  websockifyPort: 6080,
34
34
  cdpPort: 9222,
35
35
  chromiumBin: "/usr/bin/chromium",
36
+ accountId: "00000000-0000-0000-0000-000000000001",
36
37
  });
37
38
  }
38
39
  function makeEdgeFile(edgePort) {
package/dist/index.js CHANGED
@@ -2130,6 +2130,33 @@ function installTunnelScripts() {
2130
2130
  createTunnelSymlink(listLink, listSrc);
2131
2131
  }
2132
2132
  // ---------------------------------------------------------------------------
2133
+ // Account discovery (shared between installService + installCrons)
2134
+ //
2135
+ // Task 955 — `installService` stamps `Environment=ACCOUNT_ID=` into the brand
2136
+ // systemd unit so the writeNodeWithEdges gate has a non-undefined identity to
2137
+ // compare against; `installCrons` needs the same value to scope cron stdout
2138
+ // to the per-account log dir + stamp ACCOUNT_ID into the cron entry env.
2139
+ // Both pull from `INSTALL_DIR/data/accounts/<uuid>/account.json` written by
2140
+ // seed-neo4j.sh during setupAccount(). One reader, one shape, one source of
2141
+ // truth — without sharing, the two callers would drift on classification of
2142
+ // `corrupt account.json` (one might count it, the other might not) and the
2143
+ // gate would reject writes the cron's "scoped" log was already happily
2144
+ // publishing under that uuid.
2145
+ // ---------------------------------------------------------------------------
2146
+ function resolveInstallAccountId() {
2147
+ const accountsDir = join(INSTALL_DIR, "data/accounts");
2148
+ if (!existsSync(accountsDir))
2149
+ return "";
2150
+ try {
2151
+ for (const d of readdirSync(accountsDir)) {
2152
+ if (existsSync(join(accountsDir, d, "account.json")))
2153
+ return d;
2154
+ }
2155
+ }
2156
+ catch { /* directory unreadable */ }
2157
+ return "";
2158
+ }
2159
+ // ---------------------------------------------------------------------------
2133
2160
  // Cron Registration
2134
2161
  //
2135
2162
  // Registers platform cron jobs (heartbeat, email-fetch, email-auto-respond).
@@ -2144,21 +2171,9 @@ function installCrons() {
2144
2171
  return;
2145
2172
  const nodeBin = spawnSync("which", ["node"], { encoding: "utf-8" }).stdout.trim() || "/usr/bin/node";
2146
2173
  const platformRoot = join(INSTALL_DIR, "platform");
2147
- // Discover the account required for log directory and ACCOUNT_ID env
2174
+ // Account discovery shared with installService see resolveInstallAccountId.
2175
+ const accountId = resolveInstallAccountId();
2148
2176
  const accountsDir = join(INSTALL_DIR, "data/accounts");
2149
- let accountId = "";
2150
- if (existsSync(accountsDir)) {
2151
- try {
2152
- const dirs = readdirSync(accountsDir);
2153
- for (const d of dirs) {
2154
- if (existsSync(join(accountsDir, d, "account.json"))) {
2155
- accountId = d;
2156
- break;
2157
- }
2158
- }
2159
- }
2160
- catch { /* directory unreadable */ }
2161
- }
2162
2177
  if (!accountId) {
2163
2178
  console.error(" Cron jobs: skipped — no account found. Crons will register on the next install after account creation.");
2164
2179
  logFile(" cron registration skipped: no account directory with account.json found");
@@ -2450,9 +2465,15 @@ function installService() {
2450
2465
  // Mirrors paths.ts and vnc.sh so all four sites compute the same numbers
2451
2466
  // regardless of which one reads brand.json first.
2452
2467
  const VNC_OFFSET = VNC_DISPLAY - 99;
2453
- const RFB_PORT = BRAND.rfbPort ?? 5900 + VNC_OFFSET;
2454
- const WEBSOCKIFY_PORT_BRAND = BRAND.websockifyPort ?? 6080 + VNC_OFFSET;
2455
- const CDP_PORT_BRAND = BRAND.cdpPort ?? 9222 + VNC_OFFSET;
2468
+ // Parenthesise the deterministic derivation. Operator-precedence already
2469
+ // groups (offset + base) ahead of nullish-coalesce, so this is purely a
2470
+ // textual change for readability and to satisfy Task 954's grep audit
2471
+ // (the runtime path was the real silent default; these are install-time
2472
+ // deterministic fallbacks but the audit is a single regex over the
2473
+ // codebase).
2474
+ const RFB_PORT = BRAND.rfbPort ?? (5900 + VNC_OFFSET);
2475
+ const WEBSOCKIFY_PORT_BRAND = BRAND.websockifyPort ?? (6080 + VNC_OFFSET);
2476
+ const CDP_PORT_BRAND = BRAND.cdpPort ?? (9222 + VNC_OFFSET);
2456
2477
  // Task 924/938 pre-flight — refuse to write service files if any of the
2457
2478
  // three brand-scoped ports is already held by a process that is NOT this
2458
2479
  // brand's own on-demand browser nor a peer brand's edge stack.
@@ -2640,6 +2661,18 @@ function installService() {
2640
2661
  checkInstallPortFree("rfbPort", RFB_PORT);
2641
2662
  checkInstallPortFree("websockifyPort", WEBSOCKIFY_PORT_BRAND);
2642
2663
  checkInstallPortFree("cdpPort", CDP_PORT_BRAND);
2664
+ // Task 955 — ACCOUNT_ID stamped into the brand unit so the writeNodeWithEdges
2665
+ // gate at platform/lib/graph-write/src/index.ts:170 has a real identity to
2666
+ // compare against (instead of process.env.ACCOUNT_ID === undefined). Resolved
2667
+ // here AFTER setupAccount() ran upstream — seed-neo4j.sh wrote account.json,
2668
+ // so an empty resolution at this point is a corrupted install (e.g. the seed
2669
+ // failed silently, or accounts/ was wiped between setup and unit-write).
2670
+ const installAccountId = resolveInstallAccountId();
2671
+ if (!installAccountId) {
2672
+ throw new Error(`installService: no account discovered at ${INSTALL_DIR}/data/accounts/<uuid>/account.json — ` +
2673
+ `setupAccount() (seed-neo4j.sh) should have created one. Refusing to write a systemd unit ` +
2674
+ `without ACCOUNT_ID; the boot validator would FATAL on every restart (Task 955).`);
2675
+ }
2643
2676
  const serviceFile = buildMaxyUnitFile({
2644
2677
  productName: BRAND.productName,
2645
2678
  brandHostname: BRAND.hostname,
@@ -2653,6 +2686,7 @@ function installService() {
2653
2686
  websockifyPort: WEBSOCKIFY_PORT_BRAND,
2654
2687
  cdpPort: CDP_PORT_BRAND,
2655
2688
  chromiumBin: RESOLVED_CHROMIUM_BIN, // Task 929
2689
+ accountId: installAccountId, // Task 955
2656
2690
  });
2657
2691
  writeFileSync(join(serviceDir, BRAND.serviceName), serviceFile);
2658
2692
  // Task 647 — the edge service: always-on front door that owns the public
@@ -70,6 +70,14 @@ export function resolveInstallPortFromFs(opts) {
70
70
  });
71
71
  }
72
72
  export function buildMaxyUnitFile(o) {
73
+ if (!o.accountId) {
74
+ // Caller (installService) must resolve the on-disk accountId before
75
+ // calling. Falling through with an empty value would emit a unit file
76
+ // with no Environment=ACCOUNT_ID line — bootValidator would FATAL on
77
+ // every restart with reason=missing. Throw at build time so the install
78
+ // aborts loudly instead of bricking the boot loop.
79
+ throw new Error("buildMaxyUnitFile: accountId is required — caller must resolve the on-disk account UUID before stamping the systemd unit (Task 955).");
80
+ }
73
81
  const neo4jServiceDep = o.neo4jDedicated
74
82
  ? `neo4j-${o.brandHostname}.service`
75
83
  : "neo4j.service";
@@ -91,6 +99,7 @@ WatchdogSec=30
91
99
  TimeoutStartSec=60
92
100
  TimeoutStopSec=10
93
101
  Environment=NODE_ENV=production
102
+ Environment=ACCOUNT_ID=${o.accountId}
94
103
  Environment=PORT=${o.port}
95
104
  Environment=MAXY_UI_INTERNAL_PORT=${o.maxyUiInternalPort}
96
105
  Environment=HOSTNAME=127.0.0.1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-realagent",
3
- "version": "1.0.854",
3
+ "version": "1.0.856",
4
4
  "description": "Install Real Agent — Built for agents. By agents.",
5
5
  "bin": {
6
6
  "create-realagent": "./dist/index.js"
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=validate-env.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate-env.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/validate-env.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ // Task 955 — acceptance gate for the env-vs-disk validator. Pure-function
3
+ // shape (no I/O, no env read, no exit) keeps the test fast and lets the
4
+ // caller (platform/ui/server/index.ts) own the side-effects (console.error +
5
+ // process.exit). The four cases cover every observable boot state — missing,
6
+ // no-on-disk-account, mismatch, ok — with no fallback path.
7
+ var __importDefault = (this && this.__importDefault) || function (mod) {
8
+ return (mod && mod.__esModule) ? mod : { "default": mod };
9
+ };
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ const node_test_1 = __importDefault(require("node:test"));
12
+ const strict_1 = __importDefault(require("node:assert/strict"));
13
+ const index_js_1 = require("../index.js");
14
+ const UUID_A = "11111111-2222-3333-4444-555555555555";
15
+ const UUID_B = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
16
+ (0, node_test_1.default)("validateAccountIdEnv ok when env matches a disk account", () => {
17
+ const result = (0, index_js_1.validateAccountIdEnv)(UUID_A, [UUID_A]);
18
+ strict_1.default.deepEqual(result, { ok: true, envId: UUID_A, diskIds: [UUID_A] });
19
+ });
20
+ (0, node_test_1.default)("validateAccountIdEnv FATAL reason=missing when env is undefined", () => {
21
+ const result = (0, index_js_1.validateAccountIdEnv)(undefined, [UUID_A]);
22
+ strict_1.default.equal(result.ok, false);
23
+ strict_1.default.equal(result.ok ? null : result.reason, "missing");
24
+ strict_1.default.equal(result.envId, null);
25
+ });
26
+ (0, node_test_1.default)("validateAccountIdEnv FATAL reason=missing when env is the empty string", () => {
27
+ // Empty string is the systemd shape when `Environment=ACCOUNT_ID=` lands
28
+ // without a value (e.g. an installer regression that interpolates an empty
29
+ // template variable). Same FATAL classification as undefined — we never
30
+ // accept a falsy env value and silently degrade.
31
+ const result = (0, index_js_1.validateAccountIdEnv)("", [UUID_A]);
32
+ strict_1.default.equal(result.ok, false);
33
+ strict_1.default.equal(result.ok ? null : result.reason, "missing");
34
+ });
35
+ (0, node_test_1.default)("validateAccountIdEnv FATAL reason=no-on-disk-account when disk is empty", () => {
36
+ const result = (0, index_js_1.validateAccountIdEnv)(UUID_A, []);
37
+ strict_1.default.equal(result.ok, false);
38
+ strict_1.default.equal(result.ok ? null : result.reason, "no-on-disk-account");
39
+ strict_1.default.equal(result.envId, UUID_A);
40
+ strict_1.default.deepEqual(result.diskIds, []);
41
+ });
42
+ (0, node_test_1.default)("validateAccountIdEnv FATAL reason=mismatch when env not in disk list", () => {
43
+ const result = (0, index_js_1.validateAccountIdEnv)(UUID_A, [UUID_B]);
44
+ strict_1.default.equal(result.ok, false);
45
+ strict_1.default.equal(result.ok ? null : result.reason, "mismatch");
46
+ strict_1.default.equal(result.envId, UUID_A);
47
+ strict_1.default.deepEqual(result.diskIds, [UUID_B]);
48
+ });
49
+ (0, node_test_1.default)("validateAccountIdEnv ok when env matches one of multiple disk accounts", () => {
50
+ // Phase 0 invariant is single-account, but the validator is shape-agnostic;
51
+ // future multi-account expansion should not require a code change here.
52
+ const result = (0, index_js_1.validateAccountIdEnv)(UUID_B, [UUID_A, UUID_B]);
53
+ strict_1.default.equal(result.ok, true);
54
+ });
55
+ //# sourceMappingURL=validate-env.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate-env.test.js","sourceRoot":"","sources":["../../src/__tests__/validate-env.test.ts"],"names":[],"mappings":"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"}
@@ -20,4 +20,30 @@ export declare function getAccountsDirFromEnv(): string;
20
20
  * is a deliberate boot-time invariant (see module doc).
21
21
  */
22
22
  export declare function _resetEnumerationCache(): void;
23
+ /**
24
+ * Task 955 — boot-time env-vs-disk validator. Compares `process.env.ACCOUNT_ID`
25
+ * (stamped by the brand systemd unit's `Environment=ACCOUNT_ID=` line) against
26
+ * the on-disk account set returned by `enumerateValidAccountIds`. The Hono
27
+ * server calls this once at boot before binding the listener; on FATAL it
28
+ * emits a structured `[graph-health] account-id-env FATAL` line and exits 1
29
+ * so systemd's restart loop surfaces the misconfiguration in journalctl.
30
+ *
31
+ * Pure function — no I/O, no env reads, no exits — caller passes both inputs.
32
+ * Reasons enumerate the four observable boot states (success + three failures)
33
+ * with no fallback path; the writeNodeWithEdges gate cannot trust an env that
34
+ * does not match disk, and silently degrading would re-create the silent-leak
35
+ * class the gate exists to close (`.docs/neo4j.md` "Account isolation invariant").
36
+ */
37
+ export type AccountIdEnvValidationFailureReason = "missing" | "no-on-disk-account" | "mismatch";
38
+ export type AccountIdEnvValidation = {
39
+ ok: true;
40
+ envId: string;
41
+ diskIds: string[];
42
+ } | {
43
+ ok: false;
44
+ reason: AccountIdEnvValidationFailureReason;
45
+ envId: string | null;
46
+ diskIds: string[];
47
+ };
48
+ export declare function validateAccountIdEnv(envValue: string | undefined, diskIds: string[]): AccountIdEnvValidation;
23
49
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA6BA;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,CAgCtE;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,CAS9C;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,IAAI,CAE7C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA6BA;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,CAgCtE;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,CAS9C;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,IAAI,CAE7C;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,mCAAmC,GAC3C,SAAS,GACT,oBAAoB,GACpB,UAAU,CAAC;AAEf,MAAM,MAAM,sBAAsB,GAC9B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,GAC9C;IACE,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EAAE,mCAAmC,CAAC;IAC5C,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB,CAAC;AAEN,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,OAAO,EAAE,MAAM,EAAE,GAChB,sBAAsB,CAWxB"}
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.enumerateValidAccountIds = enumerateValidAccountIds;
4
4
  exports.getAccountsDirFromEnv = getAccountsDirFromEnv;
5
5
  exports._resetEnumerationCache = _resetEnumerationCache;
6
+ exports.validateAccountIdEnv = validateAccountIdEnv;
6
7
  /**
7
8
  * account-enumeration — single source of truth for "which accountIds are
8
9
  * provisioned on disk for this install?".
@@ -93,4 +94,16 @@ function getAccountsDirFromEnv() {
93
94
  function _resetEnumerationCache() {
94
95
  cache.clear();
95
96
  }
97
+ function validateAccountIdEnv(envValue, diskIds) {
98
+ if (!envValue) {
99
+ return { ok: false, reason: "missing", envId: null, diskIds };
100
+ }
101
+ if (diskIds.length === 0) {
102
+ return { ok: false, reason: "no-on-disk-account", envId: envValue, diskIds };
103
+ }
104
+ if (!diskIds.includes(envValue)) {
105
+ return { ok: false, reason: "mismatch", envId: envValue, diskIds };
106
+ }
107
+ return { ok: true, envId: envValue, diskIds };
108
+ }
96
109
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;AAoCA,4DAgCC;AAUD,sDASC;AAMD,wDAEC;AA/FD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qCAAoD;AACpD,yCAAoC;AAEpC,MAAM,OAAO,GACX,iEAAiE,CAAC;AAEpE,MAAM,KAAK,GAAG,IAAI,GAAG,EAAoB,CAAC;AAE1C;;;;;;GAMG;AACH,SAAgB,wBAAwB,CAAC,WAAmB;IAC1D,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IACtC,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC;IAExC,IAAI,KAAe,CAAC;IACpB,IAAI,CAAC;QACH,KAAK,GAAG,IAAA,qBAAW,EAAC,WAAW,CAAC,CAAC;IACnC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YACrD,KAAK,CAAC,GAAG,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;YAC3B,OAAO,EAAE,CAAC;QACZ,CAAC;QACD,MAAM,GAAG,CAAC;IACZ,CAAC;IAED,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,SAAS;QAClC,MAAM,UAAU,GAAG,IAAA,mBAAO,EAAC,WAAW,EAAE,IAAI,EAAE,cAAc,CAAC,CAAC;QAC9D,IAAI,CAAC;YACH,IAAI,CAAC,KAAK,CAAC,IAAA,sBAAY,EAAC,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC;YAC9C,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,IAAI,GAAI,GAA6B,CAAC,IAAI,CAAC;YACjD,IAAI,IAAI,KAAK,QAAQ;gBAAE,SAAS;YAChC,kEAAkE;YAClE,kCAAkC;QACpC,CAAC;IACH,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;IAC9B,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;GAOG;AACH,SAAgB,qBAAqB;IACnC,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC;IAC5C,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CACb,4EAA4E;YAC1E,kFAAkF,CACrF,CAAC;IACJ,CAAC;IACD,OAAO,IAAA,mBAAO,EAAC,IAAI,EAAE,IAAI,EAAE,eAAe,CAAC,CAAC;AAC9C,CAAC;AAED;;;GAGG;AACH,SAAgB,sBAAsB;IACpC,KAAK,CAAC,KAAK,EAAE,CAAC;AAChB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,57 @@
1
+ // Task 955 — acceptance gate for the env-vs-disk validator. Pure-function
2
+ // shape (no I/O, no env read, no exit) keeps the test fast and lets the
3
+ // caller (platform/ui/server/index.ts) own the side-effects (console.error +
4
+ // process.exit). The four cases cover every observable boot state — missing,
5
+ // no-on-disk-account, mismatch, ok — with no fallback path.
6
+
7
+ import test from "node:test";
8
+ import assert from "node:assert/strict";
9
+ import { validateAccountIdEnv } from "../index.js";
10
+
11
+ const UUID_A = "11111111-2222-3333-4444-555555555555";
12
+ const UUID_B = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
13
+
14
+ test("validateAccountIdEnv ok when env matches a disk account", () => {
15
+ const result = validateAccountIdEnv(UUID_A, [UUID_A]);
16
+ assert.deepEqual(result, { ok: true, envId: UUID_A, diskIds: [UUID_A] });
17
+ });
18
+
19
+ test("validateAccountIdEnv FATAL reason=missing when env is undefined", () => {
20
+ const result = validateAccountIdEnv(undefined, [UUID_A]);
21
+ assert.equal(result.ok, false);
22
+ assert.equal(result.ok ? null : result.reason, "missing");
23
+ assert.equal(result.envId, null);
24
+ });
25
+
26
+ test("validateAccountIdEnv FATAL reason=missing when env is the empty string", () => {
27
+ // Empty string is the systemd shape when `Environment=ACCOUNT_ID=` lands
28
+ // without a value (e.g. an installer regression that interpolates an empty
29
+ // template variable). Same FATAL classification as undefined — we never
30
+ // accept a falsy env value and silently degrade.
31
+ const result = validateAccountIdEnv("", [UUID_A]);
32
+ assert.equal(result.ok, false);
33
+ assert.equal(result.ok ? null : result.reason, "missing");
34
+ });
35
+
36
+ test("validateAccountIdEnv FATAL reason=no-on-disk-account when disk is empty", () => {
37
+ const result = validateAccountIdEnv(UUID_A, []);
38
+ assert.equal(result.ok, false);
39
+ assert.equal(result.ok ? null : result.reason, "no-on-disk-account");
40
+ assert.equal(result.envId, UUID_A);
41
+ assert.deepEqual(result.diskIds, []);
42
+ });
43
+
44
+ test("validateAccountIdEnv FATAL reason=mismatch when env not in disk list", () => {
45
+ const result = validateAccountIdEnv(UUID_A, [UUID_B]);
46
+ assert.equal(result.ok, false);
47
+ assert.equal(result.ok ? null : result.reason, "mismatch");
48
+ assert.equal(result.envId, UUID_A);
49
+ assert.deepEqual(result.diskIds, [UUID_B]);
50
+ });
51
+
52
+ test("validateAccountIdEnv ok when env matches one of multiple disk accounts", () => {
53
+ // Phase 0 invariant is single-account, but the validator is shape-agnostic;
54
+ // future multi-account expansion should not require a code change here.
55
+ const result = validateAccountIdEnv(UUID_B, [UUID_A, UUID_B]);
56
+ assert.equal(result.ok, true);
57
+ });
@@ -94,3 +94,47 @@ export function getAccountsDirFromEnv(): string {
94
94
  export function _resetEnumerationCache(): void {
95
95
  cache.clear();
96
96
  }
97
+
98
+ /**
99
+ * Task 955 — boot-time env-vs-disk validator. Compares `process.env.ACCOUNT_ID`
100
+ * (stamped by the brand systemd unit's `Environment=ACCOUNT_ID=` line) against
101
+ * the on-disk account set returned by `enumerateValidAccountIds`. The Hono
102
+ * server calls this once at boot before binding the listener; on FATAL it
103
+ * emits a structured `[graph-health] account-id-env FATAL` line and exits 1
104
+ * so systemd's restart loop surfaces the misconfiguration in journalctl.
105
+ *
106
+ * Pure function — no I/O, no env reads, no exits — caller passes both inputs.
107
+ * Reasons enumerate the four observable boot states (success + three failures)
108
+ * with no fallback path; the writeNodeWithEdges gate cannot trust an env that
109
+ * does not match disk, and silently degrading would re-create the silent-leak
110
+ * class the gate exists to close (`.docs/neo4j.md` "Account isolation invariant").
111
+ */
112
+ export type AccountIdEnvValidationFailureReason =
113
+ | "missing"
114
+ | "no-on-disk-account"
115
+ | "mismatch";
116
+
117
+ export type AccountIdEnvValidation =
118
+ | { ok: true; envId: string; diskIds: string[] }
119
+ | {
120
+ ok: false;
121
+ reason: AccountIdEnvValidationFailureReason;
122
+ envId: string | null;
123
+ diskIds: string[];
124
+ };
125
+
126
+ export function validateAccountIdEnv(
127
+ envValue: string | undefined,
128
+ diskIds: string[],
129
+ ): AccountIdEnvValidation {
130
+ if (!envValue) {
131
+ return { ok: false, reason: "missing", envId: null, diskIds };
132
+ }
133
+ if (diskIds.length === 0) {
134
+ return { ok: false, reason: "no-on-disk-account", envId: envValue, diskIds };
135
+ }
136
+ if (!diskIds.includes(envValue)) {
137
+ return { ok: false, reason: "mismatch", envId: envValue, diskIds };
138
+ }
139
+ return { ok: true, envId: envValue, diskIds };
140
+ }
@@ -43,6 +43,8 @@ Mode B skips Steps 1–5 of this runbook entirely — the user receives a token
43
43
 
44
44
  The manual walkthrough below exists so an operator can execute every step by hand when the system is broken or the automation is absent. For a normal setup — and for validating that the runbook's steps produce a working tunnel end-to-end — use the two scripts at `platform/plugins/cloudflare/scripts/`:
45
45
 
46
+ > **Task 954 — `list-cf-domains` is brand-arg + brand.json driven.** `list-cf-domains.sh` requires the brand name as `$1`; the .ts helper reads `cdpPort` from `${MAXY_PLATFORM_ROOT}/config/brand.json` (no silent default). Missing brand arg, missing brand.json, or missing `cdpPort` field each exit 1 with a named `reason=` token, and the route maps them to `field=config` (no Retry button — fix the install instead).
47
+
46
48
  ```
47
49
  setup-tunnel.sh <brand> <port> <hostname> [<hostname> ...]
48
50
  ```
@@ -5,11 +5,19 @@
5
5
  # token on stderr tagged `[list-cf-domains]`.
6
6
  #
7
7
  # Usage:
8
- # list-cf-domains.sh [<brand>]
8
+ # list-cf-domains.sh <brand>
9
9
  #
10
- # The brand arg (default: maxy) names the `${HOME}/.${BRAND}/logs/` directory
11
- # where selector-drift body dumps land. The script does not read brand.json
12
- # the directory is the only brand-derived path it needs.
10
+ # Task 954: brand arg is REQUIRED the prior silent default (string
11
+ # fallback to "maxy" when $1 was empty) made every non-Maxy brand's CF
12
+ # setup form fail on first use because the wrapper's child node helper
13
+ # read the Maxy CDP port from a hardcoded fallback. Missing arg now exits
14
+ # 1 with `phase=error reason=brand-arg-missing`.
15
+ #
16
+ # The wrapper also resolves and exports MAXY_PLATFORM_ROOT from its own
17
+ # script location so the .ts helper can read `<root>/config/brand.json` for
18
+ # the brand's `cdpPort` (Task 924's source of truth). Direct-SSH invocation
19
+ # works because the script lives at `<install>/platform/plugins/cloudflare/
20
+ # scripts/`, making platform root three dirs up from the resolved script.
13
21
  #
14
22
  # Runtime: Node 22's `--experimental-strip-types` runs the .ts helper
15
23
  # directly, so no tsx / playwright / ws dependency exists.
@@ -24,16 +32,29 @@ set -euo pipefail
24
32
  # Resolve symlinks before dirname — ~/list-cf-domains.sh is installed as a
25
33
  # symlink into $HOME, so the raw BASH_SOURCE[0] points at $HOME, not the
26
34
  # scripts directory where _stream-log.sh lives.
27
- source "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/_stream-log.sh"
35
+ SCRIPT_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
36
+ source "${SCRIPT_DIR}/_stream-log.sh"
28
37
  require_stream_log_path list-cf-domains
29
38
 
30
- BRAND="${1:-maxy}"
39
+ if [ "$#" -lt 1 ] || [ -z "${1:-}" ]; then
40
+ phase_line list-cf-domains phase=error reason=brand-arg-missing
41
+ exit 1
42
+ fi
43
+
44
+ BRAND="${1}"
31
45
  CONFIG_DIR=".${BRAND}"
32
46
  mkdir -p "${HOME}/${CONFIG_DIR}/logs"
33
47
 
48
+ # MAXY_PLATFORM_ROOT is set by the systemd service when the admin server
49
+ # spawns this script via runFormSpawn. Direct-SSH invocation has no such env
50
+ # so derive it from the resolved script location: scripts/ → cloudflare/ →
51
+ # plugins/ → platform → install root (three dirs up).
52
+ if [ -z "${MAXY_PLATFORM_ROOT:-}" ]; then
53
+ MAXY_PLATFORM_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
54
+ fi
55
+
34
56
  phase_line list-cf-domains phase=script-start brand="${BRAND}" config_dir="${CONFIG_DIR}"
35
57
 
36
- SCRIPT_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
37
58
  TS_ENTRY="${SCRIPT_DIR}/list-cf-domains.ts"
38
59
 
39
60
  if [ ! -f "${TS_ENTRY}" ]; then
@@ -47,15 +68,23 @@ fi
47
68
  # (124) from a helper-reported failure (1).
48
69
  HARD_TIMEOUT_SECS=30
49
70
 
50
- # CONFIG_DIR is consumed by the node helper when writing selector-drift dumps
51
- # to ~/.${BRAND}/logs/list-cf-domains-<ts>.html.
71
+ # Env passed through:
72
+ # - CONFIG_DIR — consumed by the node helper when writing selector-drift
73
+ # dumps to ~/.${BRAND}/logs/list-cf-domains-<ts>.html.
74
+ # - BRAND — names the brand for log lines naming the brand on
75
+ # config-class failures (helper does not derive it from
76
+ # CONFIG_DIR to keep the two concerns independent).
77
+ # - MAXY_PLATFORM_ROOT — install root the helper reads `config/brand.json`
78
+ # from for `cdpPort`. Resolved above so direct-SSH
79
+ # invocation matches the systemd-spawned path.
52
80
  #
53
81
  # `node --experimental-strip-types` (stable in Node 22.22) runs the .ts file
54
82
  # natively: type annotations are stripped, no enums / decorators are used, so
55
83
  # no TS type-transform is needed. `--no-warnings` suppresses the experimental
56
84
  # banner which would otherwise leak into stderr and confuse the route parser.
57
85
  set +e
58
- CONFIG_DIR="${CONFIG_DIR}" timeout --preserve-status --signal=TERM "${HARD_TIMEOUT_SECS}" \
86
+ CONFIG_DIR="${CONFIG_DIR}" BRAND="${BRAND}" MAXY_PLATFORM_ROOT="${MAXY_PLATFORM_ROOT}" \
87
+ timeout --preserve-status --signal=TERM "${HARD_TIMEOUT_SECS}" \
59
88
  node --experimental-strip-types --no-warnings "${TS_ENTRY}"
60
89
  EXIT_CODE=$?
61
90
  set -e