@rubytech/create-maxy-code 0.1.109 → 0.1.111
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/dist/index.js +50 -150
- package/dist/snap-chromium.js +1 -2
- package/dist/uninstall.js +10 -9
- package/package.json +1 -1
- package/payload/platform/neo4j/schema.cypher +6 -3
- package/payload/platform/plugins/admin/PLUGIN.md +1 -0
- package/payload/platform/plugins/admin/mcp/dist/index.js +1 -1
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/admin/skills/upgrade/SKILL.md +34 -0
- package/payload/platform/plugins/cloudflare/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/cloudflare/PLUGIN.md +9 -16
- package/payload/platform/plugins/cloudflare/mcp/dist/index.js +7 -12
- package/payload/platform/plugins/cloudflare/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/cloudflare/references/dashboard-guide.md +3 -3
- package/payload/platform/plugins/cloudflare/references/manual-setup.md +19 -54
- package/payload/platform/plugins/cloudflare/references/reset-guide.md +29 -28
- package/payload/platform/plugins/cloudflare/skills/setup-tunnel/SKILL.md +29 -149
- package/payload/platform/plugins/docs/references/admin-session.md +1 -1
- package/payload/platform/plugins/docs/references/admin-ui.md +1 -1
- package/payload/platform/plugins/docs/references/cloudflare.md +20 -29
- package/payload/platform/plugins/docs/references/platform.md +1 -1
- package/payload/platform/plugins/docs/references/plugins-guide.md +1 -1
- package/payload/platform/plugins/docs/references/troubleshooting.md +1 -1
- package/payload/platform/scripts/check-no-task-id-leaks.mjs +1 -1
- package/payload/platform/templates/agents/admin/IDENTITY.md +1 -1
- package/payload/platform/templates/specialists/agents/personal-assistant.md +1 -1
- package/payload/platform/plugins/cloudflare/scripts/__tests__/tunnel-ingress.test.ts +0 -241
- package/payload/platform/plugins/cloudflare/scripts/list-cf-domains.sh +0 -98
- package/payload/platform/plugins/cloudflare/scripts/list-cf-domains.ts +0 -715
- package/payload/platform/plugins/cloudflare/scripts/reset-tunnel.sh +0 -107
- package/payload/platform/plugins/cloudflare/scripts/setup-tunnel.sh +0 -852
- package/payload/platform/plugins/cloudflare/scripts/tunnel-ingress.ts +0 -291
|
@@ -1,291 +0,0 @@
|
|
|
1
|
-
// Pure tunnel-ingress rendering + state I/O.
|
|
2
|
-
//
|
|
3
|
-
// Mirror of `samba-provision.ts`: pure decision functions in this file with
|
|
4
|
-
// no side effects; `setup-tunnel.sh` orchestrates I/O around them. Invoked
|
|
5
|
-
// from bash via `node --experimental-strip-types`. The CLI shim at the
|
|
6
|
-
// bottom of this file takes a JSON spec on argv and prints rendered YAML
|
|
7
|
-
// (or the requested state snippet) on stdout so the shell can `>` it into
|
|
8
|
-
// the brand's config.yml or tunnel.state.
|
|
9
|
-
//
|
|
10
|
-
// Why a pure module:
|
|
11
|
-
// - The existing setup-tunnel.sh rebuilds config.yml from scratch every
|
|
12
|
-
// run. Adding SSH and SMB conditionally inline doubles the rewrite
|
|
13
|
-
// logic and makes it untestable. Extracting to a pure function with a
|
|
14
|
-
// test grid locks the YAML shape against future drift.
|
|
15
|
-
// - tunnel.state needs additive sshHostname / smbHostname fields so a
|
|
16
|
-
// re-run with no env vars rehydrates the ingress instead of silently
|
|
17
|
-
// dropping it. The persistence shape lives here so the test grid
|
|
18
|
-
// covers round-trip parity.
|
|
19
|
-
//
|
|
20
|
-
// Out of scope:
|
|
21
|
-
// - Cloudflare API calls (banned per `feedback_cf_api_total_eradication`).
|
|
22
|
-
// - Access policy creation (operator authors it in the dashboard).
|
|
23
|
-
// - DNS routing (the bash wrapper invokes `cloudflared tunnel route dns`).
|
|
24
|
-
|
|
25
|
-
import { readFileSync, existsSync, appendFileSync } from "node:fs";
|
|
26
|
-
|
|
27
|
-
// STREAM_LOG_PATH writer (contract). The four CLI
|
|
28
|
-
// subcommands invoked from setup-tunnel.sh (probe-samba, render-config,
|
|
29
|
-
// render-state, action-req) emit a `phase=<cmd>-start` line on entry and
|
|
30
|
-
// `phase=<cmd>-ok` / `phase=<cmd>-fail` on exit. Pattern ported verbatim
|
|
31
|
-
// from list-cf-domains.ts:94-133: sticky-flag on first write failure to
|
|
32
|
-
// avoid stderr spam if STREAM_LOG_PATH is unwritable; observability
|
|
33
|
-
// failure must not mask the subcommand's real signal.
|
|
34
|
-
let streamLogWriteFailed = false;
|
|
35
|
-
|
|
36
|
-
function logPhase(line: string): void {
|
|
37
|
-
const streamLogPath = process.env.STREAM_LOG_PATH;
|
|
38
|
-
if (!streamLogPath || streamLogWriteFailed) return;
|
|
39
|
-
try {
|
|
40
|
-
appendFileSync(
|
|
41
|
-
streamLogPath,
|
|
42
|
-
`[${new Date().toISOString()}] [script:tunnel-ingress] ${line}\n`,
|
|
43
|
-
);
|
|
44
|
-
} catch (err) {
|
|
45
|
-
streamLogWriteFailed = true;
|
|
46
|
-
const detail = (err instanceof Error ? err.message : String(err))
|
|
47
|
-
.slice(0, 200)
|
|
48
|
-
.replace(/"/g, "'");
|
|
49
|
-
process.stderr.write(
|
|
50
|
-
`[tunnel-ingress] phase=stream-log-write-failed detail="${detail}"\n`,
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// ---------------------------------------------------------------------------
|
|
56
|
-
// Types
|
|
57
|
-
// ---------------------------------------------------------------------------
|
|
58
|
-
|
|
59
|
-
export interface IngressSpec {
|
|
60
|
-
tunnelId: string;
|
|
61
|
-
credentialsPath: string;
|
|
62
|
-
httpPort: number;
|
|
63
|
-
httpHostnames: string[];
|
|
64
|
-
sshHostname?: string | null;
|
|
65
|
-
smbHostname?: string | null;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export interface TunnelState {
|
|
69
|
-
tunnelId: string;
|
|
70
|
-
tunnelName: string;
|
|
71
|
-
domain: string;
|
|
72
|
-
configPath: string;
|
|
73
|
-
credentialsPath: string;
|
|
74
|
-
sshHostname?: string | null;
|
|
75
|
-
smbHostname?: string | null;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// ---------------------------------------------------------------------------
|
|
79
|
-
// Pure renderer
|
|
80
|
-
// ---------------------------------------------------------------------------
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Render config.yml from a fully-resolved IngressSpec. Order matters:
|
|
84
|
-
* cloudflared matches ingress rules top-to-bottom, so the catch-all
|
|
85
|
-
* `http_status:404` MUST be last. SSH and SMB are inserted between the
|
|
86
|
-
* HTTPS hostnames and the catch-all — they carry non-HTTP services, so
|
|
87
|
-
* placing them after HTTPS keeps the existing HTTP routing behaviour
|
|
88
|
-
* unchanged when no SSH/SMB hostname is configured.
|
|
89
|
-
*/
|
|
90
|
-
export function renderConfigYml(spec: IngressSpec): string {
|
|
91
|
-
const lines: string[] = [];
|
|
92
|
-
lines.push(`tunnel: ${spec.tunnelId}`);
|
|
93
|
-
lines.push(`credentials-file: ${spec.credentialsPath}`);
|
|
94
|
-
lines.push(`ingress:`);
|
|
95
|
-
for (const h of spec.httpHostnames) {
|
|
96
|
-
lines.push(` - hostname: ${h}`);
|
|
97
|
-
lines.push(` service: http://localhost:${spec.httpPort}`);
|
|
98
|
-
}
|
|
99
|
-
if (spec.sshHostname) {
|
|
100
|
-
lines.push(` - hostname: ${spec.sshHostname}`);
|
|
101
|
-
lines.push(` service: ssh://localhost:22`);
|
|
102
|
-
}
|
|
103
|
-
if (spec.smbHostname) {
|
|
104
|
-
lines.push(` - hostname: ${spec.smbHostname}`);
|
|
105
|
-
lines.push(` service: tcp://localhost:445`);
|
|
106
|
-
}
|
|
107
|
-
lines.push(` - service: http_status:404`);
|
|
108
|
-
lines.push(``);
|
|
109
|
-
return lines.join("\n");
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Render the tunnel.state JSON, preserving SSH/SMB hostnames as additive
|
|
114
|
-
* fields so re-runs can rehydrate them. The base shape (tunnelId,
|
|
115
|
-
* tunnelName, domain, configPath, credentialsPath) is a superset that
|
|
116
|
-
* consumers without ssh/smb knowledge continue to read correctly.
|
|
117
|
-
*/
|
|
118
|
-
export function renderTunnelState(state: TunnelState): string {
|
|
119
|
-
const out: Record<string, string> = {
|
|
120
|
-
tunnelId: state.tunnelId,
|
|
121
|
-
tunnelName: state.tunnelName,
|
|
122
|
-
domain: state.domain,
|
|
123
|
-
configPath: state.configPath,
|
|
124
|
-
credentialsPath: state.credentialsPath,
|
|
125
|
-
};
|
|
126
|
-
if (state.sshHostname) out.sshHostname = state.sshHostname;
|
|
127
|
-
if (state.smbHostname) out.smbHostname = state.smbHostname;
|
|
128
|
-
return JSON.stringify(out, null, 2) + "\n";
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Read an existing tunnel.state from disk, returning the persisted
|
|
133
|
-
* sshHostname / smbHostname if present. Used to rehydrate on re-runs
|
|
134
|
-
* where the operator did not pass the env vars again. Missing file or
|
|
135
|
-
* malformed JSON returns nulls — the caller decides whether to fail.
|
|
136
|
-
*/
|
|
137
|
-
export function readPersistedHostnames(statePath: string): {
|
|
138
|
-
sshHostname: string | null;
|
|
139
|
-
smbHostname: string | null;
|
|
140
|
-
} {
|
|
141
|
-
if (!existsSync(statePath)) return { sshHostname: null, smbHostname: null };
|
|
142
|
-
try {
|
|
143
|
-
const raw = readFileSync(statePath, "utf8");
|
|
144
|
-
const parsed = JSON.parse(raw) as Partial<TunnelState>;
|
|
145
|
-
return {
|
|
146
|
-
sshHostname: typeof parsed.sshHostname === "string" ? parsed.sshHostname : null,
|
|
147
|
-
smbHostname: typeof parsed.smbHostname === "string" ? parsed.smbHostname : null,
|
|
148
|
-
};
|
|
149
|
-
} catch {
|
|
150
|
-
return { sshHostname: null, smbHostname: null };
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// ---------------------------------------------------------------------------
|
|
155
|
-
// Samba presence probe
|
|
156
|
-
// ---------------------------------------------------------------------------
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Probe whether the per-brand Samba stanza exists in /etc/samba/smb.conf.
|
|
160
|
-
* The brand stanza header is `[<brand>]` at the start of a line (the
|
|
161
|
-
* `renderBrandStanza` output). Source of truth is the Pi filesystem, not
|
|
162
|
-
* brand.json — Samba is provisioned by the installer's post-install step,
|
|
163
|
-
* not declared up-front.
|
|
164
|
-
*/
|
|
165
|
-
export function probeSambaStanza(brand: string, smbConfPath = "/etc/samba/smb.conf"): boolean {
|
|
166
|
-
if (!existsSync(smbConfPath)) return false;
|
|
167
|
-
try {
|
|
168
|
-
const raw = readFileSync(smbConfPath, "utf8");
|
|
169
|
-
const re = new RegExp(`^\\[${escapeRegex(brand)}\\]\\s*$`, "m");
|
|
170
|
-
return re.test(raw);
|
|
171
|
-
} catch {
|
|
172
|
-
return false;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function escapeRegex(s: string): string {
|
|
177
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// ---------------------------------------------------------------------------
|
|
181
|
-
// ACTION REQUIRED block
|
|
182
|
-
// ---------------------------------------------------------------------------
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Render the dashboard click-path the operator follows to author the Access
|
|
186
|
-
* policy. Same shape as the existing apex-CNAME ACTION REQUIRED block in
|
|
187
|
-
* setup-tunnel.sh. The script does NOT create the policy — Cloudflare API
|
|
188
|
-
* is banned (`feedback_cf_api_total_eradication`) and `cloudflared` CLI
|
|
189
|
-
* has no Access-application create subcommand. The operator clicks through
|
|
190
|
-
* Zero Trust → Access → Applications → Add → Self-hosted.
|
|
191
|
-
*/
|
|
192
|
-
export function renderAccessPolicyActionRequired(input: {
|
|
193
|
-
sshHostname?: string | null;
|
|
194
|
-
smbHostname?: string | null;
|
|
195
|
-
operatorEmail: string;
|
|
196
|
-
}): string {
|
|
197
|
-
const hosts: Array<{ kind: "SSH" | "SMB"; hostname: string }> = [];
|
|
198
|
-
if (input.sshHostname) hosts.push({ kind: "SSH", hostname: input.sshHostname });
|
|
199
|
-
if (input.smbHostname) hosts.push({ kind: "SMB", hostname: input.smbHostname });
|
|
200
|
-
if (hosts.length === 0) return "";
|
|
201
|
-
|
|
202
|
-
const lines: string[] = [];
|
|
203
|
-
lines.push("");
|
|
204
|
-
lines.push("============================================================");
|
|
205
|
-
lines.push("ACTION REQUIRED — Cloudflare Zero Trust Access policy");
|
|
206
|
-
lines.push("============================================================");
|
|
207
|
-
for (const { kind, hostname } of hosts) {
|
|
208
|
-
lines.push(` ${kind}: ${hostname}`);
|
|
209
|
-
lines.push(` Cloudflare dashboard → Zero Trust → Access → Applications`);
|
|
210
|
-
lines.push(` Add → Self-hosted`);
|
|
211
|
-
lines.push(` Application name: ${hostname}`);
|
|
212
|
-
lines.push(` Application domain: ${hostname}`);
|
|
213
|
-
lines.push(` Policy → Action: Allow`);
|
|
214
|
-
lines.push(` Include → Emails: ${input.operatorEmail}`);
|
|
215
|
-
lines.push("");
|
|
216
|
-
}
|
|
217
|
-
lines.push("Until each Access application is created, off-LAN clients see");
|
|
218
|
-
lines.push("Cloudflare's identity-rejection page and bytes never reach the Pi.");
|
|
219
|
-
lines.push("============================================================");
|
|
220
|
-
return lines.join("\n");
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// ---------------------------------------------------------------------------
|
|
224
|
-
// CLI shim
|
|
225
|
-
// ---------------------------------------------------------------------------
|
|
226
|
-
|
|
227
|
-
// Bash invokes this file with one of three subcommands:
|
|
228
|
-
// render-config <spec.json> → stdout YAML
|
|
229
|
-
// render-state <state.json> → stdout JSON
|
|
230
|
-
// read-state <state-path> → stdout JSON {sshHostname,smbHostname}
|
|
231
|
-
// probe-samba <brand> [smb.conf] → exit 0 if present, 1 if absent
|
|
232
|
-
// action-req <input.json> → stdout ACTION REQUIRED block
|
|
233
|
-
|
|
234
|
-
function readJsonArg(path: string): unknown {
|
|
235
|
-
const raw = readFileSync(path, "utf8");
|
|
236
|
-
return JSON.parse(raw);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const isCli =
|
|
240
|
-
typeof process !== "undefined" &&
|
|
241
|
-
Array.isArray(process.argv) &&
|
|
242
|
-
process.argv[1] !== undefined &&
|
|
243
|
-
/tunnel-ingress\.(ts|js|mjs|mts)$/.test(process.argv[1]);
|
|
244
|
-
|
|
245
|
-
if (isCli) {
|
|
246
|
-
const [, , cmd, ...rest] = process.argv;
|
|
247
|
-
// Track which subcommands carry the stream-log contract — for these
|
|
248
|
-
// we emit start/ok/fail phase pairs. Unknown subcommands and read-state are
|
|
249
|
-
// out of scope per the task (read-state has no setup-tunnel.sh caller line).
|
|
250
|
-
const instrumented = new Set(["probe-samba", "render-config", "render-state", "action-req"]);
|
|
251
|
-
if (instrumented.has(cmd)) logPhase(`phase=${cmd}-start`);
|
|
252
|
-
try {
|
|
253
|
-
if (cmd === "render-config") {
|
|
254
|
-
const spec = readJsonArg(rest[0]) as IngressSpec;
|
|
255
|
-
process.stdout.write(renderConfigYml(spec));
|
|
256
|
-
logPhase(`phase=${cmd}-ok`);
|
|
257
|
-
} else if (cmd === "render-state") {
|
|
258
|
-
const state = readJsonArg(rest[0]) as TunnelState;
|
|
259
|
-
process.stdout.write(renderTunnelState(state));
|
|
260
|
-
logPhase(`phase=${cmd}-ok`);
|
|
261
|
-
} else if (cmd === "read-state") {
|
|
262
|
-
const persisted = readPersistedHostnames(rest[0]);
|
|
263
|
-
process.stdout.write(JSON.stringify(persisted));
|
|
264
|
-
} else if (cmd === "probe-samba") {
|
|
265
|
-
const brand = rest[0];
|
|
266
|
-
const smbConf = rest[1] ?? "/etc/samba/smb.conf";
|
|
267
|
-
const present = probeSambaStanza(brand, smbConf);
|
|
268
|
-
logPhase(`phase=${cmd}-ok brand=${brand} present=${present}`);
|
|
269
|
-
process.exit(present ? 0 : 1);
|
|
270
|
-
} else if (cmd === "action-req") {
|
|
271
|
-
const input = readJsonArg(rest[0]) as {
|
|
272
|
-
sshHostname?: string | null;
|
|
273
|
-
smbHostname?: string | null;
|
|
274
|
-
operatorEmail: string;
|
|
275
|
-
};
|
|
276
|
-
process.stdout.write(renderAccessPolicyActionRequired(input));
|
|
277
|
-
logPhase(`phase=${cmd}-ok`);
|
|
278
|
-
} else {
|
|
279
|
-
process.stderr.write(`tunnel-ingress: unknown subcommand: ${cmd}\n`);
|
|
280
|
-
process.exit(2);
|
|
281
|
-
}
|
|
282
|
-
} catch (err) {
|
|
283
|
-
const msg = (err as Error).message;
|
|
284
|
-
if (instrumented.has(cmd)) {
|
|
285
|
-
const detail = msg.slice(0, 200).replace(/"/g, "'");
|
|
286
|
-
logPhase(`phase=${cmd}-fail error="${detail}"`);
|
|
287
|
-
}
|
|
288
|
-
process.stderr.write(`tunnel-ingress: ${msg}\n`);
|
|
289
|
-
process.exit(1);
|
|
290
|
-
}
|
|
291
|
-
}
|