@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.
- package/package.json +1 -1
- package/payload/platform/config/brand.json +0 -4
- package/payload/platform/package-lock.json +1547 -1
- package/payload/platform/plugins/admin/PLUGIN.md +1 -0
- package/payload/platform/plugins/admin/hooks/webfetch-preflight.mjs +363 -0
- package/payload/platform/plugins/admin/skills/stream-log-review/SKILL.md +4 -1
- package/payload/platform/plugins/cloudflare/PLUGIN.md +2 -2
- package/payload/platform/plugins/cloudflare/mcp/__tests__/auth-binding.test.ts +0 -1
- package/payload/platform/plugins/cloudflare/mcp/dist/index.js +158 -99
- package/payload/platform/plugins/cloudflare/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.d.ts +103 -70
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.d.ts.map +1 -1
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.js +300 -529
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.js.map +1 -1
- package/payload/platform/plugins/cloudflare/references/setup-guide.md +10 -13
- package/payload/platform/plugins/cloudflare/skills/setup-tunnel/SKILL.md +18 -14
- package/payload/platform/plugins/docs/references/cloudflare.md +32 -49
- package/payload/platform/plugins/docs/references/plugins-guide.md +2 -0
- package/payload/platform/scripts/seed-neo4j.sh +12 -0
- package/payload/platform/templates/agents/admin/IDENTITY.md +2 -0
- package/payload/platform/templates/specialists/agents/personal-assistant.md +6 -6
- package/payload/server/public/assets/{admin-Df1liz4Y.js → admin-D7LRdkYB.js} +30 -30
- package/payload/server/public/index.html +1 -1
- package/payload/server/server.js +88 -23
- package/payload/platform/plugins/cloudflare/mcp/__tests__/brand-load.test.ts +0 -81
- package/payload/platform/plugins/cloudflare/mcp/__tests__/manifest-scope.test.ts +0 -65
- package/payload/platform/plugins/cloudflare/mcp/__tests__/verify-scenario-0.test.ts +0 -70
- 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-
|
|
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">
|
package/payload/server/server.js
CHANGED
|
@@ -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
|
|
8288
|
-
//
|
|
8289
|
-
|
|
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
|
|
9175
|
-
//
|
|
9176
|
-
//
|
|
9177
|
-
|
|
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
|
|
9517
|
-
//
|
|
9518
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
});
|