@rubytech/create-realagent 1.0.616 → 1.0.618

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 (28) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/config/brand.json +0 -4
  3. package/payload/platform/package-lock.json +1547 -1
  4. package/payload/platform/plugins/admin/PLUGIN.md +1 -0
  5. package/payload/platform/plugins/admin/hooks/webfetch-preflight.mjs +363 -0
  6. package/payload/platform/plugins/admin/skills/stream-log-review/SKILL.md +4 -1
  7. package/payload/platform/plugins/cloudflare/PLUGIN.md +2 -2
  8. package/payload/platform/plugins/cloudflare/mcp/__tests__/auth-binding.test.ts +0 -1
  9. package/payload/platform/plugins/cloudflare/mcp/dist/index.js +158 -99
  10. package/payload/platform/plugins/cloudflare/mcp/dist/index.js.map +1 -1
  11. package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.d.ts +103 -70
  12. package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.d.ts.map +1 -1
  13. package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.js +300 -529
  14. package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.js.map +1 -1
  15. package/payload/platform/plugins/cloudflare/references/setup-guide.md +10 -13
  16. package/payload/platform/plugins/cloudflare/skills/setup-tunnel/SKILL.md +18 -14
  17. package/payload/platform/plugins/docs/references/cloudflare.md +32 -49
  18. package/payload/platform/plugins/docs/references/plugins-guide.md +2 -0
  19. package/payload/platform/scripts/seed-neo4j.sh +12 -0
  20. package/payload/platform/templates/agents/admin/IDENTITY.md +2 -0
  21. package/payload/platform/templates/specialists/agents/personal-assistant.md +6 -6
  22. package/payload/server/public/assets/{admin-Df1liz4Y.js → admin-D7LRdkYB.js} +30 -30
  23. package/payload/server/public/index.html +1 -1
  24. package/payload/server/server.js +88 -23
  25. package/payload/platform/plugins/cloudflare/mcp/__tests__/brand-load.test.ts +0 -81
  26. package/payload/platform/plugins/cloudflare/mcp/__tests__/manifest-scope.test.ts +0 -65
  27. package/payload/platform/plugins/cloudflare/mcp/__tests__/verify-scenario-0.test.ts +0 -70
  28. package/payload/platform/plugins/cloudflare/mcp/__tests__/verify-scenario-B.test.ts +0 -124
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Real Agent</title>
7
7
  <link rel="icon" href="/favicon.ico">
8
- <script type="module" crossorigin src="/assets/admin-Df1liz4Y.js"></script>
8
+ <script type="module" crossorigin src="/assets/admin-D7LRdkYB.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/chunk-Be6NvmcD.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/preload-helper-rov5CBGT.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/useVoiceRecorder-OB_Gtr0e.js">
@@ -6211,14 +6211,22 @@ function purgeOldLogs(logDir, prefix) {
6211
6211
  }
6212
6212
  }
6213
6213
  function teeProcStderrToStreamLog(proc, streamLog) {
6214
+ const pid = proc.pid;
6214
6215
  if (!proc.stderr) {
6215
- streamLog.write(`[${isoTs()}] [subproc-stderr-skip] reason=no-stderr
6216
+ streamLog.write(`[${isoTs()}] [subproc-stderr-skip] reason=no-stderr pid=${pid}
6216
6217
  `);
6217
6218
  return;
6218
6219
  }
6220
+ if (!streamLog.destroyed && !streamLog.writableEnded) {
6221
+ streamLog.write(`[${isoTs()}] [subproc-stderr-tee-attached] pid=${pid}
6222
+ `);
6223
+ }
6219
6224
  const utf8 = new StringDecoder("utf8");
6220
6225
  let buffer = "";
6226
+ let bytesSeen = 0;
6227
+ let linesEmitted = 0;
6221
6228
  proc.stderr.on("data", (chunk) => {
6229
+ bytesSeen += typeof chunk === "string" ? Buffer.byteLength(chunk, "utf8") : chunk.length;
6222
6230
  const text = typeof chunk === "string" ? chunk : utf8.write(chunk);
6223
6231
  buffer += text;
6224
6232
  let idx;
@@ -6229,12 +6237,18 @@ function teeProcStderrToStreamLog(proc, streamLog) {
6229
6237
  if (streamLog.destroyed || streamLog.writableEnded) continue;
6230
6238
  streamLog.write(`[${isoTs()}] [subproc-stderr] ${line}
6231
6239
  `);
6240
+ linesEmitted++;
6232
6241
  }
6233
6242
  });
6234
6243
  proc.stderr.on("end", () => {
6235
6244
  const tail = (buffer + utf8.end()).trim();
6236
6245
  if (tail.length > 0 && !streamLog.destroyed && !streamLog.writableEnded) {
6237
6246
  streamLog.write(`[${isoTs()}] [subproc-stderr] ${tail}
6247
+ `);
6248
+ linesEmitted++;
6249
+ }
6250
+ if (!streamLog.destroyed && !streamLog.writableEnded) {
6251
+ streamLog.write(`[${isoTs()}] [subproc-stderr-tee-detached] pid=${pid} bytes=${bytesSeen} lines=${linesEmitted}
6238
6252
  `);
6239
6253
  }
6240
6254
  buffer = "";
@@ -7678,6 +7692,7 @@ var ADMIN_CORE_TOOLS = [
7678
7692
  "mcp__cloudflare__tunnel-status",
7679
7693
  "mcp__cloudflare__tunnel-install",
7680
7694
  "mcp__cloudflare__tunnel-login",
7695
+ "mcp__cloudflare__tunnel-create",
7681
7696
  "mcp__cloudflare__tunnel-enable",
7682
7697
  "mcp__cloudflare__tunnel-disable",
7683
7698
  "mcp__cloudflare__tunnel-add-hostname",
@@ -8283,10 +8298,11 @@ async function* runCompactionTurn(accountDir, accountId, systemPrompt, resumeSes
8283
8298
  env: {
8284
8299
  ...process.env,
8285
8300
  PLATFORM_ROOT: PLATFORM_ROOT4,
8286
- ACCOUNT_DIR: accountDir,
8287
- // Task 532: network traces on the compaction subprocess too its tool
8288
- // calls are part of the same conversation's record.
8289
- NODE_DEBUG: "http,http2,net,tls,undici,dns"
8301
+ ACCOUNT_DIR: accountDir
8302
+ // Task 535: NODE_DEBUG removed. The Claude Code CLI is a bundled Bun
8303
+ // binary and Bun ignores Node's NODE_DEBUG flag, so setting it here was
8304
+ // a no-op that misled future readers. The [subproc-debug-unavailable]
8305
+ // line below records the source-of-silence explicitly.
8290
8306
  }
8291
8307
  });
8292
8308
  const stderrLog = agentLogStream("claude-agent-compaction-stderr", accountDir, conversationId);
@@ -8297,6 +8313,8 @@ async function* runCompactionTurn(accountDir, accountId, systemPrompt, resumeSes
8297
8313
  streamLog.on("error", () => {
8298
8314
  });
8299
8315
  teeProcStderrToStreamLog(proc, streamLog);
8316
+ streamLog.write(`[${isoTs()}] [subproc-debug-unavailable] reason=bundled-bun-binary-ignores-node-debug pid=${proc.pid} cli=claude
8317
+ `);
8300
8318
  streamLog.write(`[${isoTs()}] [compaction-start] resumeSessionId=${resumeSessionId}
8301
8319
  `);
8302
8320
  proc.on("error", (err) => {
@@ -9170,11 +9188,11 @@ async function* invokeAdminAgent(message, systemPrompt, accountDir, accountId, a
9170
9188
  env: {
9171
9189
  ...process.env,
9172
9190
  PLATFORM_ROOT: PLATFORM_ROOT4,
9173
- ACCOUNT_DIR: accountDir,
9174
- // Task 532: tee subprocess network activity into the per-conversation
9175
- // stream log via the existing stderr pipe. Closes the Claude Code
9176
- // "what was the tool actually doing?" black box during tool waits.
9177
- NODE_DEBUG: "http,http2,net,tls,undici,dns"
9191
+ ACCOUNT_DIR: accountDir
9192
+ // Task 535: NODE_DEBUG removed. The Claude Code CLI is a bundled Bun
9193
+ // binary and Bun ignores Node's NODE_DEBUG flag, so setting it here was
9194
+ // a no-op that misled future readers. The [subproc-debug-unavailable]
9195
+ // line below records the source-of-silence explicitly.
9178
9196
  }
9179
9197
  });
9180
9198
  const stderrLog = agentLogStream("claude-agent-stderr", accountDir, spawnConvId);
@@ -9185,6 +9203,8 @@ async function* invokeAdminAgent(message, systemPrompt, accountDir, accountId, a
9185
9203
  streamLog.on("error", () => {
9186
9204
  });
9187
9205
  teeProcStderrToStreamLog(proc, streamLog);
9206
+ streamLog.write(`[${isoTs()}] [subproc-debug-unavailable] reason=bundled-bun-binary-ignores-node-debug pid=${proc.pid} cli=claude
9207
+ `);
9188
9208
  if (sessionKey) {
9189
9209
  const prev = activeProcesses.get(sessionKey);
9190
9210
  if (prev) {
@@ -9512,10 +9532,11 @@ async function* invokeManagedAdminAgent(message, systemPrompt, accountDir, accou
9512
9532
  env: {
9513
9533
  ...process.env,
9514
9534
  PLATFORM_ROOT: PLATFORM_ROOT4,
9515
- ACCOUNT_DIR: accountDir,
9516
- // Task 532: tee subprocess network traces into the per-conversation
9517
- // stream log via the stderr pipe + dual-consume listener.
9518
- NODE_DEBUG: "http,http2,net,tls,undici,dns"
9535
+ ACCOUNT_DIR: accountDir
9536
+ // Task 535: NODE_DEBUG removed. The Claude Code CLI is a bundled Bun
9537
+ // binary and Bun ignores Node's NODE_DEBUG flag, so setting it here was
9538
+ // a no-op that misled future readers. The [subproc-debug-unavailable]
9539
+ // line below records the source-of-silence explicitly.
9519
9540
  }
9520
9541
  });
9521
9542
  const stderrLog = agentLogStream("claude-agent-stderr", accountDir, managedConvId);
@@ -9523,6 +9544,8 @@ async function* invokeManagedAdminAgent(message, systemPrompt, accountDir, accou
9523
9544
  });
9524
9545
  proc.stderr?.pipe(stderrLog);
9525
9546
  teeProcStderrToStreamLog(proc, streamLog);
9547
+ streamLog.write(`[${isoTs()}] [subproc-debug-unavailable] reason=bundled-bun-binary-ignores-node-debug pid=${proc.pid} cli=claude
9548
+ `);
9526
9549
  if (sessionKey) {
9527
9550
  const prev = activeProcesses.get(sessionKey);
9528
9551
  if (prev) {
@@ -10479,6 +10502,26 @@ function defaultRules() {
10479
10502
  scope: "session",
10480
10503
  suggestedAction: "A tool call has been pending for 30 seconds without a result. Read the adjacent [tool-wait-diag] and [tool-wait-proc] lines in the conversation's stream log to determine whether the network remained healthy, the subprocess held active sockets, and the HTTP request reached the wire. If diag shows a healthy network but the subprocess has no [subproc-stderr] UNDICI/HTTP activity during the wait window, the tool's internal pipeline is stalled \u2014 do not retry the same request against the same target without a change in approach."
10481
10504
  },
10505
+ {
10506
+ // Task 536: detect agents ignoring the WEBFETCH_CANNOT_READ_JS_SPA
10507
+ // structured failure. A single SPA short-circuit per conversation is
10508
+ // expected — the hook is doing its job. Two or more in the same
10509
+ // conversation within 5 minutes means either (a) the agent retried
10510
+ // WebFetch on the same SPA URL despite the directive, or (b) the
10511
+ // owner is asking about multiple SPA URLs in one session and the
10512
+ // pattern needs surfacing as a recurring class. Both signal that the
10513
+ // IDENTITY.md "Tool Failure Discipline" guidance is not landing in the
10514
+ // prompt — revise the copy rather than add mechanical enforcement.
10515
+ id: "webfetch-spa-short-circuit-recurring",
10516
+ name: "WebFetch JS-SPA short-circuit fired repeatedly in conversation",
10517
+ type: "repeated-error",
10518
+ logSource: "system",
10519
+ pattern: "WEBFETCH_CANNOT_READ_JS_SPA",
10520
+ thresholdCount: 2,
10521
+ thresholdWindowMinutes: 5,
10522
+ scope: "session",
10523
+ suggestedAction: "The WebFetch SPA preflight has fired more than once in this conversation. Either the agent is ignoring the loud-failure directive (retrying WebFetch after seeing WEBFETCH_CANNOT_READ_JS_SPA), or multiple SPA URLs are being asked about. Read the conversation's stream log for the [tool-use] / [tool-result] sequence around each occurrence \u2014 if the agent dispatched WebFetch on the same URL or substituted Playwright silently, revisit the IDENTITY.md `Tool Failure Discipline` paragraph that names structured-error handling."
10524
+ },
10482
10525
  {
10483
10526
  // Task 533: surface every Cloudflare-plugin refusal. The plugin emits
10484
10527
  // exactly one [cloudflare:refuse] line per refusal with a structured
@@ -31171,18 +31214,23 @@ async function GET9(request) {
31171
31214
  const accountLogDir2 = account ? resolve19(account.accountDir, "logs") : null;
31172
31215
  if (fileParam) {
31173
31216
  const safe = basename5(fileParam);
31217
+ const searched = [];
31174
31218
  for (const dir of [accountLogDir2, LOG_DIR]) {
31175
31219
  if (!dir) continue;
31176
31220
  const filePath = resolve19(dir, safe);
31221
+ searched.push(filePath);
31177
31222
  try {
31178
31223
  const content = readFileSync20(filePath, "utf-8");
31179
31224
  const headers = { "Content-Type": "text/plain; charset=utf-8" };
31180
31225
  if (download) headers["Content-Disposition"] = `attachment; filename="${safe}"`;
31181
31226
  return new Response(content, { headers });
31182
- } catch {
31227
+ } catch (err) {
31228
+ const reason = err instanceof Error ? err.message : String(err);
31229
+ console.debug(`[admin/logs] miss dir=${dir} name=${safe} reason=${reason}`);
31183
31230
  }
31184
31231
  }
31185
- return Response.json({ error: `File not found: ${safe}` }, { status: 404 });
31232
+ console.warn(`[admin/logs] not-found name=${safe} searched=[${searched.join(",")}]`);
31233
+ return Response.json({ error: `File not found: ${safe}`, code: "NOT_FOUND" }, { status: 404 });
31186
31234
  }
31187
31235
  if (typeParam) {
31188
31236
  const prefixMap = {
@@ -31194,24 +31242,37 @@ async function GET9(request) {
31194
31242
  };
31195
31243
  const prefix = prefixMap[typeParam];
31196
31244
  if (!prefix) {
31197
- return Response.json({ error: `Unknown type: ${typeParam}. Valid: stream, error, session, sse, public` }, { status: 400 });
31245
+ console.warn(`[admin/logs] rejected reason=unknown-type type=${typeParam}`);
31246
+ return Response.json(
31247
+ { error: `Unknown type: ${typeParam}. Valid: stream, error, session, sse, public`, code: "UNKNOWN_TYPE" },
31248
+ { status: 400 }
31249
+ );
31198
31250
  }
31199
31251
  if (!conversationIdParam) {
31200
- return Response.json({ error: `type=${typeParam} requires conversationId (per-conversation log files, no daily fallback)` }, { status: 400 });
31252
+ console.warn(`[admin/logs] rejected type=${typeParam} reason=no-conversationId`);
31253
+ return Response.json(
31254
+ { error: `type=${typeParam} requires conversationId (per-conversation log files, no daily fallback)`, code: "CONVERSATION_ID_REQUIRED" },
31255
+ { status: 400 }
31256
+ );
31201
31257
  }
31202
31258
  const fileName = `${prefix}-${conversationIdParam}.log`;
31259
+ const searched = [];
31203
31260
  for (const dir of [accountLogDir2, LOG_DIR]) {
31204
31261
  if (!dir) continue;
31205
31262
  const filePath = resolve19(dir, fileName);
31263
+ searched.push(filePath);
31206
31264
  try {
31207
31265
  const content = readFileSync20(filePath, "utf-8");
31208
31266
  const headers = { "Content-Type": "text/plain; charset=utf-8" };
31209
31267
  if (download) headers["Content-Disposition"] = `attachment; filename="${fileName}"`;
31210
31268
  return new Response(content, { headers });
31211
- } catch {
31269
+ } catch (err) {
31270
+ const reason = err instanceof Error ? err.message : String(err);
31271
+ console.debug(`[admin/logs] miss dir=${dir} name=${fileName} reason=${reason}`);
31212
31272
  }
31213
31273
  }
31214
- return Response.json({ error: `Log not found: ${fileName}` }, { status: 404 });
31274
+ console.warn(`[admin/logs] not-found name=${fileName} searched=[${searched.join(",")}]`);
31275
+ return Response.json({ error: `Log not found: ${fileName}`, code: "NOT_FOUND" }, { status: 404 });
31215
31276
  }
31216
31277
  const seen = /* @__PURE__ */ new Set();
31217
31278
  const logs = {};
@@ -31220,7 +31281,9 @@ async function GET9(request) {
31220
31281
  let files;
31221
31282
  try {
31222
31283
  files = readdirSync5(dir).filter((f) => f.endsWith(".log"));
31223
- } catch {
31284
+ } catch (err) {
31285
+ const reason = err instanceof Error ? err.message : String(err);
31286
+ console.warn(`[admin/logs] readdir-fail dir=${dir} reason=${reason}`);
31224
31287
  continue;
31225
31288
  }
31226
31289
  files.filter((f) => !seen.has(f)).map((f) => ({ name: f, mtime: statSync7(resolve19(dir, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime).forEach(({ name }) => {
@@ -31229,8 +31292,10 @@ async function GET9(request) {
31229
31292
  const content = readFileSync20(resolve19(dir, name));
31230
31293
  const tail = content.length > TAIL_BYTES ? content.subarray(content.length - TAIL_BYTES).toString("utf-8") : content.toString("utf-8");
31231
31294
  logs[name] = tail.trim() || "(empty)";
31232
- } catch {
31233
- logs[name] = "(unreadable)";
31295
+ } catch (err) {
31296
+ const reason = err instanceof Error ? err.message : String(err);
31297
+ console.debug(`[admin/logs] read-fail name=${name} reason=${reason}`);
31298
+ logs[name] = `(unreadable: ${reason})`;
31234
31299
  }
31235
31300
  });
31236
31301
  }
@@ -1,81 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
2
- import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import { _resetBrandCache, loadBrand } from "../src/lib/cloudflared.js";
6
-
7
- // loadBrand() reads PLATFORM_ROOT/config/brand.json. We point PLATFORM_ROOT at
8
- // a fresh tmpdir per test, write the brand.json shape we want, reset the
9
- // module-level cache, then assert load behaviour.
10
-
11
- let tmpRoot: string;
12
-
13
- beforeEach(() => {
14
- tmpRoot = mkdtempSync(join(tmpdir(), "maxy-cf-brand-"));
15
- process.env.PLATFORM_ROOT = tmpRoot;
16
- _resetBrandCache();
17
- });
18
-
19
- afterEach(() => {
20
- rmSync(tmpRoot, { recursive: true, force: true });
21
- delete process.env.PLATFORM_ROOT;
22
- });
23
-
24
- function writeBrand(content: object): void {
25
- const dir = join(tmpRoot, "config");
26
- mkdirSync(dir, { recursive: true });
27
- writeFileSync(join(dir, "brand.json"), JSON.stringify(content));
28
- }
29
-
30
- describe("loadBrand cloudflare.zones validation", () => {
31
- it("loads when cloudflare.zones is present", () => {
32
- writeBrand({
33
- productName: "Maxy",
34
- configDir: ".maxy",
35
- cloudflare: { zones: ["maxy.bot", "maxy.chat"] },
36
- });
37
- const brand = loadBrand();
38
- expect(brand.cloudflare.zones).toEqual(["maxy.bot", "maxy.chat"]);
39
- });
40
-
41
- it("hard-fails when cloudflare key is absent", () => {
42
- writeBrand({ productName: "Maxy", configDir: ".maxy" });
43
- expect(() => loadBrand()).toThrow(/cloudflare\.zones/);
44
- });
45
-
46
- it("hard-fails when cloudflare.zones is empty", () => {
47
- writeBrand({
48
- productName: "Maxy",
49
- configDir: ".maxy",
50
- cloudflare: { zones: [] },
51
- });
52
- expect(() => loadBrand()).toThrow(/cloudflare\.zones/);
53
- });
54
-
55
- it("hard-fails when cloudflare.zones is not an array", () => {
56
- writeBrand({
57
- productName: "Maxy",
58
- configDir: ".maxy",
59
- cloudflare: { zones: "maxy.bot" },
60
- });
61
- expect(() => loadBrand()).toThrow(/cloudflare\.zones/);
62
- });
63
-
64
- it("hard-fails when cloudflare.zones contains a non-string", () => {
65
- writeBrand({
66
- productName: "Maxy",
67
- configDir: ".maxy",
68
- cloudflare: { zones: ["maxy.bot", 42] },
69
- });
70
- expect(() => loadBrand()).toThrow(/invalid cloudflare\.zones entry/);
71
- });
72
-
73
- it("hard-fails when PLATFORM_ROOT is unset", () => {
74
- delete process.env.PLATFORM_ROOT;
75
- expect(() => loadBrand()).toThrow(/PLATFORM_ROOT/);
76
- });
77
-
78
- it("hard-fails when brand.json is absent at the configured path", () => {
79
- expect(() => loadBrand()).toThrow(/brand\.json not found/);
80
- });
81
- });
@@ -1,65 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { matchManifestZone } from "../src/lib/cloudflared.js";
3
-
4
- // Pure function — no fs, no network. Tests the registrable-parent suffix-match
5
- // against an explicit declared-zone list. The whole point of declarative zones
6
- // is that we never need a PSL lookup or a "last two labels" heuristic.
7
-
8
- describe("matchManifestZone", () => {
9
- const zones = ["maxy.bot", "maxy.chat", "example.co.uk"];
10
-
11
- it("matches a hostname whose registrable parent is in the zone list", () => {
12
- const r = matchManifestZone("admin.maxy.bot", zones);
13
- expect(r.ok).toBe(true);
14
- expect(r.matchedZone).toBe("maxy.bot");
15
- });
16
-
17
- it("matches the zone itself (no subdomain)", () => {
18
- const r = matchManifestZone("maxy.bot", zones);
19
- expect(r.ok).toBe(true);
20
- expect(r.matchedZone).toBe("maxy.bot");
21
- });
22
-
23
- it("matches deeper subdomains", () => {
24
- const r = matchManifestZone("foo.bar.maxy.bot", zones);
25
- expect(r.ok).toBe(true);
26
- expect(r.matchedZone).toBe("maxy.bot");
27
- });
28
-
29
- it("matches case-insensitively", () => {
30
- const r = matchManifestZone("Admin.MAXY.BOT", zones);
31
- expect(r.ok).toBe(true);
32
- expect(r.matchedZone).toBe("maxy.bot");
33
- });
34
-
35
- it("refuses hostnames outside the zone list", () => {
36
- const r = matchManifestZone("admin.example.org", zones);
37
- expect(r.ok).toBe(false);
38
- expect(r.matchedZone).toBeNull();
39
- expect(r.declaredZones).toEqual(zones);
40
- });
41
-
42
- it("does not partial-match — admin.notmaxy.bot must not match maxy.bot", () => {
43
- // The leading-dot boundary is what prevents this.
44
- const r = matchManifestZone("admin.notmaxy.bot", zones);
45
- expect(r.ok).toBe(false);
46
- });
47
-
48
- it("handles multi-label TLDs (.co.uk) when declared", () => {
49
- const r = matchManifestZone("admin.example.co.uk", zones);
50
- expect(r.ok).toBe(true);
51
- expect(r.matchedZone).toBe("example.co.uk");
52
- });
53
-
54
- it("prefers the longest match when zones overlap", () => {
55
- const overlap = ["bot", "maxy.bot"];
56
- const r = matchManifestZone("admin.maxy.bot", overlap);
57
- expect(r.ok).toBe(true);
58
- expect(r.matchedZone).toBe("maxy.bot");
59
- });
60
-
61
- it("refuses against an empty zone list", () => {
62
- const r = matchManifestZone("admin.maxy.bot", []);
63
- expect(r.ok).toBe(false);
64
- });
65
- });
@@ -1,70 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
2
- import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import {
6
- _resetBrandCache,
7
- cfVerifyCore,
8
- liveScopeContext,
9
- } from "../src/lib/cloudflared.js";
10
-
11
- // Scenario 0 (per task spec): fresh install — no cert.pem, no binding,
12
- // no tunnels, no config.yml. cf-verify must return zero OUT-OF-SCOPE,
13
- // every declared zone tagged MISSING, no throws. Proves the audit runs
14
- // before any login completes.
15
-
16
- let tmpRoot: string;
17
- let homeRoot: string;
18
- let originalHome: string | undefined;
19
-
20
- beforeEach(() => {
21
- tmpRoot = mkdtempSync(join(tmpdir(), "maxy-cf-verify-"));
22
- homeRoot = mkdtempSync(join(tmpdir(), "maxy-cf-home-"));
23
- originalHome = process.env.HOME;
24
- process.env.HOME = homeRoot;
25
- process.env.PLATFORM_ROOT = tmpRoot;
26
- _resetBrandCache();
27
- const cfg = join(tmpRoot, "config");
28
- mkdirSync(cfg, { recursive: true });
29
- writeFileSync(
30
- join(cfg, "brand.json"),
31
- JSON.stringify({
32
- productName: "Maxy",
33
- configDir: ".maxy",
34
- cloudflare: { zones: ["b.test", "c.test"] },
35
- }),
36
- );
37
- });
38
-
39
- afterEach(() => {
40
- if (originalHome !== undefined) process.env.HOME = originalHome;
41
- else delete process.env.HOME;
42
- delete process.env.PLATFORM_ROOT;
43
- rmSync(tmpRoot, { recursive: true, force: true });
44
- rmSync(homeRoot, { recursive: true, force: true });
45
- });
46
-
47
- describe("cfVerifyCore — Scenario 0 (fresh install, no login)", () => {
48
- it("runs without throwing and reports everything as MISSING", async () => {
49
- const ctx = liveScopeContext();
50
- const report = await cfVerifyCore(ctx);
51
-
52
- expect(report.brand).toBe("Maxy");
53
- expect(report.declaredZones).toEqual(["b.test", "c.test"]);
54
- expect(report.bindingPresent).toBe(false);
55
- expect(report.bindingMatchesCert).toBe(false);
56
- expect(report.certPresent).toBe(false);
57
-
58
- // No OUT-OF-SCOPE artefacts on a clean fresh device.
59
- expect(report.counts.outOfScope).toBe(0);
60
-
61
- // Cert, binding, tunnel.state, and each declared zone all MISSING.
62
- const missingTypes = report.artefacts
63
- .filter((a) => a.tag === "missing")
64
- .map((a) => a.type);
65
- expect(missingTypes).toContain("cert.pem");
66
- expect(missingTypes).toContain("binding");
67
- expect(missingTypes).toContain("tunnel.state");
68
- expect(missingTypes.filter((t) => t === "declared-zone").length).toBe(2);
69
- });
70
- });
@@ -1,124 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
2
- import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import {
6
- _resetBrandCache,
7
- cfVerifyCore,
8
- writeAccountBinding,
9
- type ScopeContext,
10
- } from "../src/lib/cloudflared.js";
11
-
12
- // Scenario B (per task spec): stale tunnel state. Brand declares
13
- // cloudflare.zones=["b.test"]. cert.pem and binding agree on account Y.
14
- // But tunnel.state references a deleted tunnel, config.yml names a
15
- // hostname under a zone NOT in declared scope, and alias-domains.json
16
- // has an entry for removed.test (also out of scope).
17
- //
18
- // We bypass the SDK (no live calls) by passing a ScopeContext with
19
- // client=null. The local-artefact analysis runs in full; the account-side
20
- // analysis surfaces declared zones as MISSING (no client = nothing to
21
- // enumerate), which is correct behaviour for this audit-only test.
22
-
23
- let tmpRoot: string;
24
- let homeRoot: string;
25
- let originalHome: string | undefined;
26
- const CONFIG_DIR = ".maxy";
27
-
28
- function home(...p: string[]): string {
29
- return join(homeRoot, ...p);
30
- }
31
-
32
- function writeCertPem(accountId: string, apiToken: string): void {
33
- const certDir = home(CONFIG_DIR, "cloudflared");
34
- mkdirSync(certDir, { recursive: true });
35
- const tokenJson = JSON.stringify({ APIToken: apiToken, AccountTag: accountId });
36
- const b64 = Buffer.from(tokenJson, "utf-8").toString("base64");
37
- const pem = `-----BEGIN ARGO TUNNEL TOKEN-----
38
- ${b64}
39
- -----END ARGO TUNNEL TOKEN-----
40
- `;
41
- writeFileSync(join(certDir, "cert.pem"), pem);
42
- }
43
-
44
- beforeEach(() => {
45
- tmpRoot = mkdtempSync(join(tmpdir(), "maxy-cf-verifyB-"));
46
- homeRoot = mkdtempSync(join(tmpdir(), "maxy-cf-home-"));
47
- originalHome = process.env.HOME;
48
- process.env.HOME = homeRoot;
49
- process.env.PLATFORM_ROOT = tmpRoot;
50
- _resetBrandCache();
51
- const cfg = join(tmpRoot, "config");
52
- mkdirSync(cfg, { recursive: true });
53
- writeFileSync(
54
- join(cfg, "brand.json"),
55
- JSON.stringify({
56
- productName: "Maxy",
57
- configDir: CONFIG_DIR,
58
- cloudflare: { zones: ["b.test"] },
59
- }),
60
- );
61
- });
62
-
63
- afterEach(() => {
64
- if (originalHome !== undefined) process.env.HOME = originalHome;
65
- else delete process.env.HOME;
66
- delete process.env.PLATFORM_ROOT;
67
- rmSync(tmpRoot, { recursive: true, force: true });
68
- rmSync(homeRoot, { recursive: true, force: true });
69
- });
70
-
71
- describe("cfVerifyCore — Scenario B (stale tunnel state)", () => {
72
- it("tags out-of-scope local artefacts even without a live SDK client", async () => {
73
- writeCertPem("acct-Y", "cfut_dummy");
74
- writeAccountBinding("acct-Y");
75
-
76
- // Stale tunnel.state pointing at a deleted tunnel + off-scope hostname.
77
- const cloudflaredDir = home(CONFIG_DIR, "cloudflared");
78
- mkdirSync(cloudflaredDir, { recursive: true });
79
- const configYmlPath = join(cloudflaredDir, "config.yml");
80
- writeFileSync(configYmlPath, "tunnel: stale\n");
81
- writeFileSync(
82
- join(cloudflaredDir, "tunnel.state"),
83
- JSON.stringify({
84
- tunnelId: "stale-tunnel-id",
85
- tunnelName: "stale",
86
- domain: "removed.test", // NOT in brand.cloudflare.zones
87
- configPath: configYmlPath,
88
- credentialsPath: join(cloudflaredDir, "stale-tunnel-id.json"),
89
- adminHostname: "admin.removed.test", // off-scope
90
- publicHostname: null,
91
- pid: null,
92
- startedAt: null,
93
- }),
94
- );
95
-
96
- // alias-domains entry for an off-scope hostname.
97
- writeFileSync(
98
- home(CONFIG_DIR, "alias-domains.json"),
99
- JSON.stringify(["removed.test"]),
100
- );
101
-
102
- // Bypass the SDK — pass a ScopeContext with client=null. The audit
103
- // tags every declared zone as MISSING (cannot enumerate without
104
- // client) but completes the local-artefact analysis in full.
105
- const ctx: ScopeContext = {
106
- declaredZones: ["b.test"],
107
- binding: { accountId: "acct-Y", boundAt: new Date().toISOString() },
108
- client: null,
109
- };
110
- const report = await cfVerifyCore(ctx);
111
-
112
- const tunnelState = report.artefacts.find((a) => a.type === "tunnel.state");
113
- expect(tunnelState?.tag).toBe("out-of-scope");
114
- expect(tunnelState?.reason).toContain("hostnames outside declared scope");
115
-
116
- const aliasEntry = report.artefacts.find(
117
- (a) => a.type === "alias-domain" && a.id === "removed.test",
118
- );
119
- expect(aliasEntry?.tag).toBe("out-of-scope");
120
-
121
- // Counts: at least the two out-of-scope local artefacts identified above.
122
- expect(report.counts.outOfScope).toBeGreaterThanOrEqual(2);
123
- });
124
- });