@rubytech/create-maxy-code 0.1.109 → 0.1.110
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 +11 -125
- package/dist/snap-chromium.js +1 -2
- package/dist/uninstall.js +10 -9
- package/package.json +1 -1
- 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 +16 -51
- package/payload/platform/plugins/cloudflare/references/reset-guide.md +24 -25
- 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/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,6 +1,6 @@
|
|
|
1
1
|
# Admin Session — restart survival and SDK-resume contract
|
|
2
2
|
|
|
3
|
-
The admin PIN-gated session-store is the in-memory `Map<sessionKey, Session>` at [`platform/ui/app/lib/claude-agent/session-store.ts`](../../../ui/app/lib/claude-agent/session-store.ts). Every `systemctl --user restart {brand}.service` (notably the one
|
|
3
|
+
The admin PIN-gated session-store is the in-memory `Map<sessionKey, Session>` at [`platform/ui/app/lib/claude-agent/session-store.ts`](../../../ui/app/lib/claude-agent/session-store.ts). Every `systemctl --user restart {brand}.service` (notably the one the agent arms 3 s after Cloudflare-setup completion via `systemd-run --on-active=3s` to avoid the cgroup trap) wipes that Map. This reference documents how an admin session survives the restart without forcing PIN re-entry, and how the SDK conversation chain is preserved across the gap.
|
|
4
4
|
|
|
5
5
|
## Signed sessionKey
|
|
6
6
|
|
|
@@ -216,7 +216,7 @@ Two endpoints, two surfaces, two restart-survival roles:
|
|
|
216
216
|
liveness. `processStartedAt` resets on every brand-service restart;
|
|
217
217
|
Neo4j probe is bounded to 1 s and reports
|
|
218
218
|
`conversationDb: 'ok' | 'error'`. Use this to confirm the brand
|
|
219
|
-
process came back after
|
|
219
|
+
process came back after a Cloudflare-setup armed restart.
|
|
220
220
|
- `GET /api/admin/version` (maxy-edge) — installer / brand version
|
|
221
221
|
string. Hosted on `maxy-edge.service` so the Software Update modal
|
|
222
222
|
can read it while the brand service is mid-restart.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Cloudflare Tunnel — the dashboard is the source of truth
|
|
2
2
|
|
|
3
|
-
Each installation has its own Cloudflare account. Sign-in is OAuth: the
|
|
3
|
+
Each installation has its own Cloudflare account. Sign-in is OAuth: the agent invokes `cloudflared tunnel login` via Bash; the Cloudflare Authorize URL streams into the admin chat PTY and the native terminal renders it as a clickable link. Click it, authorise in your own browser, and `cloudflared` writes `cert.pem` to the brand's config directory. The agent never reads or mutates Cloudflare account state directly — whatever you see in your logged-in dashboard is the single source of truth. When something needs doing on the account side (adding a domain, deleting a stray entry, switching accounts), the agent relays the click-paths; you run them in your browser.
|
|
4
4
|
|
|
5
5
|
## Identity model
|
|
6
6
|
|
|
@@ -8,34 +8,25 @@ Each installation has its own Cloudflare account. Sign-in is OAuth: the setup sc
|
|
|
8
8
|
|------|--------|
|
|
9
9
|
| **Product identity** (Maxy vs Real Agent) | `brand.json` (`productName`, `configDir`) — known at install. |
|
|
10
10
|
| **Cloudflare account identity** | `cert.pem` from OAuth. One account per brand per device. |
|
|
11
|
-
| **Domain scope** (which zones the operator can route) | Live Cloudflare dashboard
|
|
12
|
-
| **
|
|
13
|
-
| **Local tunnel state** | `~/{configDir}/cloudflared/` — `cert.pem`, `<UUID>.json`, `config.yml`, `tunnel.state`, `alias-domains.json`. |
|
|
11
|
+
| **Domain scope** (which zones the operator can route) | Live Cloudflare dashboard — the operator picks the zone in the dashboard during OAuth or names it in chat. The agent does not enumerate zones programmatically. |
|
|
12
|
+
| **Local tunnel state** | `~/{configDir}/cloudflared/` — `cert.pem`, `<UUID>.json`, `config.yml`, `alias-domains.json`. |
|
|
14
13
|
|
|
15
|
-
There is no token-based auth for the operator-owned path (Mode A). To switch Cloudflare accounts,
|
|
14
|
+
There is no token-based auth for the operator-owned path (Mode A). To switch Cloudflare accounts, the agent runs the reset flow from `plugins/cloudflare/references/reset-guide.md` (deletes the cert and every tunnel on the current account), then the manual-setup flow again — `cloudflared tunnel login` picks a fresh account when you sign in.
|
|
16
15
|
|
|
17
16
|
## Setup flow
|
|
18
17
|
|
|
19
|
-
Ask the agent to set up Cloudflare. The agent confirms the domain is already on your Cloudflare account (if not, it quotes the dashboard click-path — see below) and
|
|
18
|
+
Ask the agent to set up Cloudflare. The agent confirms the domain is already on your Cloudflare account (if not, it quotes the dashboard click-path — see below) and collects the inputs in plain chat:
|
|
20
19
|
|
|
21
20
|
- **Admin address** — the hostname that will serve the admin chat (e.g. `admin.yourdomain.com`).
|
|
22
21
|
- **Public address** — optional hostname for the public agent (e.g. `public.yourdomain.com` or `chat.yourdomain.com`).
|
|
23
22
|
- **Proxy apex** — optional bare-domain hostname (e.g. `yourdomain.com`) that should also serve the public agent.
|
|
24
23
|
- **Admin password** — the password used to gate remote access to the admin surface.
|
|
25
24
|
|
|
26
|
-
|
|
25
|
+
The agent then sets the admin password via `curl -X POST http://127.0.0.1:${PORT}/api/remote-auth/set-password` (same endpoint the local onboarding form uses), and works through `plugins/cloudflare/references/manual-setup.md` Steps 1–7 directly via the Bash tool. `cloudflared`'s stdout streams into the PTY verbatim. The OAuth URL is linkified by the terminal; click it in your own browser to authorise. After the tunnel is up, the agent appends each non-`public.*` public or apex hostname to `~/{configDir}/alias-domains.json` so `isPublicHost()` classifies it as public, and starts the brand's cloudflared user service.
|
|
27
26
|
|
|
28
|
-
|
|
27
|
+
If any step's `cloudflared` invocation exits non-zero, the agent names the literal exit code, surfaces the stderr verbatim, and cites `reset-guide.md` for the next action — no retry under a different flag, no Playwright-driven dashboard inspection.
|
|
29
28
|
|
|
30
|
-
-
|
|
31
|
-
- Tunnel resolution from operator-supplied identity (operator-selected-tunnel fix). The script requires EXACTLY ONE of `TUNNEL_ID` or `TUNNEL_NAME` in env and exits 1 with a literal error on either-both-set or neither-set. Pre-fix the script derived `${BRAND}-$(hostname -s)` locally; that broke the operator-state-is-authoritative doctrine and silently created orphan tunnels whenever the device hostname changed. Stream log emits `step=tunnel-resolve source=operator-selected|operator-created tunnel_id=… tunnel_name=…` once the UUID is known.
|
|
32
|
-
- **Zone pre-flight** — for every non-apex hostname the script queries `1.1.1.1` for the registrable parent's NS records and refuses the whole run if they don't point at Cloudflare. Stream log: `step=zone-preflight result=ok|error zones_on_account=… missing_parent_for=…`. Catches "domain not on Cloudflare"; does not catch "domain on a different Cloudflare account than `cert.pem` is bound to" — that case surfaces later via `tunnel-status`.
|
|
33
|
-
- `cloudflared tunnel route dns` for each subdomain hostname. Apex hostnames cannot be routed this way — the script prints an **ACTION REQUIRED** block naming the exact dashboard record to add or edit. Stream log emits `step=route-dns hostname=… tunnel_id=…` before the call and `step=route-dns hostname=… result=ok|apex-skip|error` after; on error the bounded cloudflared stderr (≤400 chars) rides in the same phase line. **The script does not parse cloudflared's stdout** — exit code is the sole decision signal, so all three legitimate cloudflared output shapes (new record, overwrite, idempotent "already configured") are treated as success.
|
|
34
|
-
- `config.yml` and `tunnel.state` written under `${CFG_DIR}`.
|
|
35
|
-
- `systemctl --user restart ${BRAND}.service` — restarts the platform service so the new tunnel spawns via the service's `ExecStartPre=resume-tunnel.sh`. The restart fires a few seconds after the script exits so the script does not kill its own cgroup when invoked via the Bash tool; the chat UI receives a `server_shutdown` SSE frame and reconnects automatically.
|
|
36
|
-
- Post-restart verification — `ps -ef | grep '[c]loudflared'` confirms the connector is alive, then `curl -I https://<hostname>` against each subdomain (up to 60 s per host) confirms a non-530 response.
|
|
37
|
-
|
|
38
|
-
The agent streams the script's stdout into chat verbatim as it arrives, including any `ACTION REQUIRED` block. If the script exits non-zero, the agent names the literal exit code and cites `reset-guide.md` for the next action — no retry under a different flag, no Playwright-driven dashboard inspection.
|
|
29
|
+
The setup-done claim only fires after the agent runs `curl -I https://<admin-hostname>` from outside the local network and the response shows a `200` line. That HTTP response is the only success terminal.
|
|
39
30
|
|
|
40
31
|
## Getting a domain on Cloudflare
|
|
41
32
|
|
|
@@ -49,19 +40,19 @@ Existing website traffic continues to work during and after the switch. Only DNS
|
|
|
49
40
|
|
|
50
41
|
## Reset / account switch
|
|
51
42
|
|
|
52
|
-
Ask the agent to reset Cloudflare. The agent
|
|
43
|
+
Ask the agent to reset Cloudflare. The agent executes the reset flow from `plugins/cloudflare/references/reset-guide.md`:
|
|
53
44
|
|
|
54
45
|
- Deletes every tunnel on the brand's current Cloudflare account (via the bound cert).
|
|
55
46
|
- Wipes the brand's `${CFG_DIR}`.
|
|
56
|
-
-
|
|
47
|
+
- Stops the brand's cloudflared user service.
|
|
57
48
|
|
|
58
|
-
The
|
|
49
|
+
The agent does **not** stop token-mode connector processes or delete stray misrouted CNAMEs in the dashboard. If any of those apply, the agent guides you through the manual cleanup — `pkill -f 'cloudflared.*tunnel run --token'` on the device, or deleting the stray CNAME in the dashboard.
|
|
59
50
|
|
|
60
51
|
After reset, run setup again. The fresh `cloudflared tunnel login` will pick whichever Cloudflare account you sign into.
|
|
61
52
|
|
|
62
53
|
## Manual runbook
|
|
63
54
|
|
|
64
|
-
|
|
55
|
+
The step-by-step runbook at `plugins/cloudflare/references/manual-setup.md` is the contract the agent follows. It is also what an operator runs by hand when needed — every numbered step is an isolated `cloudflared` command block with success conditions and troubleshooting.
|
|
65
56
|
|
|
66
57
|
## Dashboard operations the CLI cannot do
|
|
67
58
|
|
|
@@ -71,17 +62,17 @@ The CLI cannot add a domain, switch accounts, edit an apex CNAME, or delete stra
|
|
|
71
62
|
|
|
72
63
|
### Tunnel won't start
|
|
73
64
|
|
|
74
|
-
Ask the agent to check. The agent reads `systemctl --user status ${BRAND}.service` and `~/{configDir}/cloudflared
|
|
65
|
+
Ask the agent to check. The agent reads `systemctl --user status ${BRAND}-cloudflared.service` and the cloudflared log under `~/{configDir}/cloudflared/`. Common states:
|
|
75
66
|
|
|
76
|
-
- **No cloudflared process running** — the service
|
|
77
|
-
- **`tunnel not found`** — the UUID in `config.yml` does not match any tunnel on the currently-bound account. Usually follows an account switch that didn't reset local state. The agent
|
|
67
|
+
- **No cloudflared process running** — the cloudflared service exited or never started. The agent runs the manual-setup flow to re-issue tunnel creation.
|
|
68
|
+
- **`tunnel not found`** — the UUID in `config.yml` does not match any tunnel on the currently-bound account. Usually follows an account switch that didn't reset local state. The agent runs the reset flow and then a fresh setup.
|
|
78
69
|
|
|
79
70
|
### URL returns 530
|
|
80
71
|
|
|
81
72
|
DNS propagation or account mismatch. Wait 30–60 seconds and retry first. If the 530 persists:
|
|
82
73
|
|
|
83
|
-
- The domain may be on a Cloudflare account different from the one `cert.pem` is bound to — re-
|
|
84
|
-
- The UDP buffer for QUIC may be undersized on this device — check
|
|
74
|
+
- The domain may be on a Cloudflare account different from the one `cert.pem` is bound to — the agent re-runs the manual setup steps to re-validate.
|
|
75
|
+
- The UDP buffer for QUIC may be undersized on this device — check the cloudflared log for `failed to sufficiently increase receive buffer size`.
|
|
85
76
|
|
|
86
77
|
### URL returns connection refused
|
|
87
78
|
|
|
@@ -104,8 +95,8 @@ The most common cause is wrong nameservers on the domain. The domain must use Cl
|
|
|
104
95
|
|
|
105
96
|
## What the agent does and does not do
|
|
106
97
|
|
|
107
|
-
**Does:** invokes `
|
|
98
|
+
**Does:** invokes `cloudflared` directly via Bash, following `plugins/cloudflare/references/manual-setup.md` step by step; quotes click-paths from the reference files verbatim; verifies external reachability with `curl -I` and surfaces the response.
|
|
108
99
|
|
|
109
|
-
**Does not:** drive the Cloudflare dashboard via Playwright, synthesise alternative `cloudflared` flag sequences, call any Cloudflare API or SDK, write or edit `cert.pem` / `
|
|
100
|
+
**Does not:** drive the Cloudflare dashboard via Playwright, synthesise alternative `cloudflared` flag sequences not in the runbook, call any Cloudflare API or SDK, write or edit `cert.pem` / `config.yml` directly outside the runbook's instructions.
|
|
110
101
|
|
|
111
|
-
When a
|
|
102
|
+
When a command fails, the agent reports the failure and cites the relevant recovery step. It does not improvise.
|
|
@@ -111,7 +111,7 @@ The Data search panel ranks results by combining vector similarity with keyword
|
|
|
111
111
|
Both flows run on the native Claude Code PTY surface in admin chat (Task 287). There is no in-app upgrade modal and no Cloudflare setup form — the agent invokes the relevant Bash command directly and its stdout streams into chat verbatim.
|
|
112
112
|
|
|
113
113
|
- **Software update.** Re-run the installer (`npx -y @rubytech/create-<brand>@latest`) from a shell; HeaderMenu's version row turns sage when `installed === latest`.
|
|
114
|
-
- **Cloudflare setup.** Operator asks in chat; the agent invokes
|
|
114
|
+
- **Cloudflare setup.** Operator asks in chat; the agent invokes `cloudflared` directly via the Bash tool, following the numbered steps in `plugins/cloudflare/references/manual-setup.md`. cloudflared's stdout and stderr stream into the PTY; the OAuth URL printed by `cloudflared tunnel login` is linkified by the terminal so the operator clicks it and authorises Cloudflare in their own browser.
|
|
115
115
|
|
|
116
116
|
**Mid-turn stream-drop banners.** If a chat turn ends abruptly the bubble shows one of two messages depending on what actually happened. You see "Server is restarting — reconnect will happen automatically." only when the app server itself emits the restart signal — typically during a Software Update or a Cloudflare setup that re-launches the brand service. You see "Lost connection — retrying." when your browser's connection to the Pi dropped mid-stream while the server was still up — typically a flaky Wi-Fi moment or the tunnel hiccupping. Either way the chat resumes once the connection is back; the previously-rendered messages stay on screen so you don't lose context.
|
|
117
117
|
|
|
@@ -152,7 +152,7 @@ After this, every `console.error("[your-tool]...")` from any tool in the plugin
|
|
|
152
152
|
|
|
153
153
|
**How the tee decides which file to write to:** the platform sets `STREAM_LOG_PATH` as an environment variable on every MCP server spawn, pointing to the conversation-scoped stream log. The MCP server does not know about conversations — it just trusts `STREAM_LOG_PATH`. Multiple concurrent conversations produce multiple concurrent MCP server processes, each teeing to its own file; no cross-conversation leakage.
|
|
154
154
|
|
|
155
|
-
**Bash
|
|
155
|
+
**Bash commands stream straight into the PTY.** Maxy Code's admin and public chat run on the native Claude Code PTY (Task 287). The per-conversation server-side stream log that the retired web-UI dispatcher tailed is gone; agent-invoked Bash commands (including direct `cloudflared` invocations for Cloudflare setup — Task 288) print their stdout and stderr directly, and the PTY renders the output in chat verbatim.
|
|
156
156
|
|
|
157
157
|
**Retrieve MCP diagnostic lines for a conversation:**
|
|
158
158
|
|
|
@@ -190,7 +190,7 @@ If the initial Cloudflare login fails during setup, {{productName}} will fall ba
|
|
|
190
190
|
Both flows run on the native Claude Code PTY surface in admin chat (Task 287). The retired action-runner / terminal-modal troubleshooting sections that lived here have been removed because those surfaces no longer exist; failures now manifest as plain stderr from the agent-invoked Bash command, visible in chat.
|
|
191
191
|
|
|
192
192
|
- **Software update.** Re-run `npx -y @rubytech/create-<brand>@latest` from a shell; if the installer fails, its stdout is the diagnostic record. HeaderMenu turns sage when `installed === latest`.
|
|
193
|
-
- **Cloudflare setup.** The agent invokes
|
|
193
|
+
- **Cloudflare setup.** The agent invokes `cloudflared` directly via Bash, following the cloudflare plugin's `references/manual-setup.md`. Failures surface as cloudflared's literal stderr plus a non-zero exit. Recovery paths live in `references/reset-guide.md` and `references/manual-setup.md`.
|
|
194
194
|
|
|
195
195
|
## Orphan Account Directory Archived to `.trash/`
|
|
196
196
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Build-time gate: rejects `(Task NNN)` citations in operator-visible production
|
|
3
3
|
// strings. Citations are allowed in inline comments and test titles/bodies only.
|
|
4
4
|
//
|
|
5
|
-
// Live repro that motivated the gate: setup
|
|
5
|
+
// Live repro that motivated the gate: a setup script on realagent 192.168.88.8
|
|
6
6
|
// emitted `Re-run the installer to provision Chromium.` to the
|
|
7
7
|
// operator's chat surface — the citation made the operator chase task
|
|
8
8
|
// history rather than execute the verb. Operator surfaces strip citations;
|
|
@@ -10,7 +10,7 @@ Your job is to do useful work for the operator. How useful you can be depends on
|
|
|
10
10
|
|
|
11
11
|
## Before you speak
|
|
12
12
|
|
|
13
|
-
Before your first message, do reconnaissance. Treat your starting knowledge as whatever your tools can surface, not what the system prompt names. Search the memory graph for what's known about the operator and their business, read the profile, check today's calendar, active projects, open work, and recent conversations. Infer the domain from the plugins and skills installed; they were chosen for a reason and are evidence of what this operation does.
|
|
13
|
+
Before your first message, do reconnaissance. Treat your starting knowledge as whatever your tools can surface, not what the system prompt names. Search the memory graph for what's known about the operator and their business, read the profile, check today's calendar, active projects, open work, and recent conversations. Infer the domain from the plugins and skills installed; they were chosen for a reason and are evidence of what this operation does. Check whether an upgrade is available for this install (the upgrade skill describes the lookup); if so, offer it in your first message.
|
|
14
14
|
|
|
15
15
|
Then lead with what you found. When you have enough knowledge of the operator and their business to make a useful proposal, follow with one suited to where they are in their day; otherwise, gather more before proposing. Ask only the questions you genuinely could not answer through investigation. You are expected to drive the day, not to solicit instructions for it.
|
|
16
16
|
|
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
// acceptance grid for tunnel-ingress.ts pure layer.
|
|
2
|
-
//
|
|
3
|
-
// Runs under `node --test --experimental-strip-types` (no compile step). No
|
|
4
|
-
// filesystem reads except via temp files written by the test itself; no
|
|
5
|
-
// network; no shell-outs. Mirrors apt-resolve / samba-provision style.
|
|
6
|
-
|
|
7
|
-
import test from "node:test";
|
|
8
|
-
import assert from "node:assert/strict";
|
|
9
|
-
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
|
|
10
|
-
import { join } from "node:path";
|
|
11
|
-
import { tmpdir } from "node:os";
|
|
12
|
-
import {
|
|
13
|
-
renderConfigYml,
|
|
14
|
-
renderTunnelState,
|
|
15
|
-
readPersistedHostnames,
|
|
16
|
-
probeSambaStanza,
|
|
17
|
-
renderAccessPolicyActionRequired,
|
|
18
|
-
type IngressSpec,
|
|
19
|
-
type TunnelState,
|
|
20
|
-
} from "../tunnel-ingress.ts";
|
|
21
|
-
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
// renderConfigYml
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
|
|
26
|
-
test("renderConfigYml: HTTPS-only matches expected shape byte-for-byte", () => {
|
|
27
|
-
const spec: IngressSpec = {
|
|
28
|
-
tunnelId: "abc-123",
|
|
29
|
-
credentialsPath: "/home/admin/.maxy/cloudflared/abc-123.json",
|
|
30
|
-
httpPort: 19200,
|
|
31
|
-
httpHostnames: ["admin.maxy.bot", "public.maxy.bot"],
|
|
32
|
-
};
|
|
33
|
-
assert.equal(
|
|
34
|
-
renderConfigYml(spec),
|
|
35
|
-
[
|
|
36
|
-
"tunnel: abc-123",
|
|
37
|
-
"credentials-file: /home/admin/.maxy/cloudflared/abc-123.json",
|
|
38
|
-
"ingress:",
|
|
39
|
-
" - hostname: admin.maxy.bot",
|
|
40
|
-
" service: http://localhost:19200",
|
|
41
|
-
" - hostname: public.maxy.bot",
|
|
42
|
-
" service: http://localhost:19200",
|
|
43
|
-
" - service: http_status:404",
|
|
44
|
-
"",
|
|
45
|
-
].join("\n"),
|
|
46
|
-
);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test("renderConfigYml: SSH+SMB appear AFTER http hostnames and BEFORE catch-all", () => {
|
|
50
|
-
const spec: IngressSpec = {
|
|
51
|
-
tunnelId: "t-id",
|
|
52
|
-
credentialsPath: "/creds.json",
|
|
53
|
-
httpPort: 19200,
|
|
54
|
-
httpHostnames: ["admin.maxy.bot"],
|
|
55
|
-
sshHostname: "ssh.maxy.bot",
|
|
56
|
-
smbHostname: "smb.maxy.bot",
|
|
57
|
-
};
|
|
58
|
-
const out = renderConfigYml(spec);
|
|
59
|
-
const idxHttp = out.indexOf("admin.maxy.bot");
|
|
60
|
-
const idxSsh = out.indexOf("ssh.maxy.bot");
|
|
61
|
-
const idxSmb = out.indexOf("smb.maxy.bot");
|
|
62
|
-
const idx404 = out.indexOf("http_status:404");
|
|
63
|
-
assert.ok(idxHttp < idxSsh, "HTTP before SSH");
|
|
64
|
-
assert.ok(idxSsh < idxSmb, "SSH before SMB");
|
|
65
|
-
assert.ok(idxSmb < idx404, "SMB before catch-all 404");
|
|
66
|
-
assert.match(out, /\s+service: ssh:\/\/localhost:22$/m);
|
|
67
|
-
assert.match(out, /\s+service: tcp:\/\/localhost:445$/m);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test("renderConfigYml: SSH only — no SMB block when smbHostname is null", () => {
|
|
71
|
-
const spec: IngressSpec = {
|
|
72
|
-
tunnelId: "t-id",
|
|
73
|
-
credentialsPath: "/creds.json",
|
|
74
|
-
httpPort: 19200,
|
|
75
|
-
httpHostnames: ["admin.maxy.bot"],
|
|
76
|
-
sshHostname: "ssh.maxy.bot",
|
|
77
|
-
smbHostname: null,
|
|
78
|
-
};
|
|
79
|
-
const out = renderConfigYml(spec);
|
|
80
|
-
assert.ok(out.includes("ssh.maxy.bot"));
|
|
81
|
-
assert.ok(!out.includes("tcp://localhost:445"));
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test("renderConfigYml: empty string hostnames are treated as absent", () => {
|
|
85
|
-
const spec: IngressSpec = {
|
|
86
|
-
tunnelId: "t-id",
|
|
87
|
-
credentialsPath: "/creds.json",
|
|
88
|
-
httpPort: 19200,
|
|
89
|
-
httpHostnames: ["admin.maxy.bot"],
|
|
90
|
-
sshHostname: "",
|
|
91
|
-
smbHostname: "",
|
|
92
|
-
};
|
|
93
|
-
const out = renderConfigYml(spec);
|
|
94
|
-
assert.ok(!out.includes("ssh://localhost:22"));
|
|
95
|
-
assert.ok(!out.includes("tcp://localhost:445"));
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
// ---------------------------------------------------------------------------
|
|
99
|
-
// renderTunnelState
|
|
100
|
-
// ---------------------------------------------------------------------------
|
|
101
|
-
|
|
102
|
-
test("renderTunnelState: base fields preserved when ssh/smb absent", () => {
|
|
103
|
-
const state: TunnelState = {
|
|
104
|
-
tunnelId: "abc",
|
|
105
|
-
tunnelName: "maxy",
|
|
106
|
-
domain: "admin.maxy.bot",
|
|
107
|
-
configPath: "/home/admin/.maxy/cloudflared/config.yml",
|
|
108
|
-
credentialsPath: "/home/admin/.maxy/cloudflared/abc.json",
|
|
109
|
-
};
|
|
110
|
-
const parsed = JSON.parse(renderTunnelState(state));
|
|
111
|
-
assert.deepEqual(parsed, {
|
|
112
|
-
tunnelId: "abc",
|
|
113
|
-
tunnelName: "maxy",
|
|
114
|
-
domain: "admin.maxy.bot",
|
|
115
|
-
configPath: "/home/admin/.maxy/cloudflared/config.yml",
|
|
116
|
-
credentialsPath: "/home/admin/.maxy/cloudflared/abc.json",
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test("renderTunnelState: ssh/smb hostnames persist when present", () => {
|
|
121
|
-
const state: TunnelState = {
|
|
122
|
-
tunnelId: "abc",
|
|
123
|
-
tunnelName: "maxy",
|
|
124
|
-
domain: "admin.maxy.bot",
|
|
125
|
-
configPath: "/cfg.yml",
|
|
126
|
-
credentialsPath: "/creds.json",
|
|
127
|
-
sshHostname: "ssh.maxy.bot",
|
|
128
|
-
smbHostname: "smb.maxy.bot",
|
|
129
|
-
};
|
|
130
|
-
const parsed = JSON.parse(renderTunnelState(state));
|
|
131
|
-
assert.equal(parsed.sshHostname, "ssh.maxy.bot");
|
|
132
|
-
assert.equal(parsed.smbHostname, "smb.maxy.bot");
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
// ---------------------------------------------------------------------------
|
|
136
|
-
// readPersistedHostnames
|
|
137
|
-
// ---------------------------------------------------------------------------
|
|
138
|
-
|
|
139
|
-
test("readPersistedHostnames: returns nulls when file missing", () => {
|
|
140
|
-
const got = readPersistedHostnames("/nope/does/not/exist.json");
|
|
141
|
-
assert.deepEqual(got, { sshHostname: null, smbHostname: null });
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
test("readPersistedHostnames: round-trips ssh/smb hostnames from disk", () => {
|
|
145
|
-
const dir = mkdtempSync(join(tmpdir(), "tunnel-state-"));
|
|
146
|
-
const path = join(dir, "tunnel.state");
|
|
147
|
-
try {
|
|
148
|
-
writeFileSync(
|
|
149
|
-
path,
|
|
150
|
-
JSON.stringify({
|
|
151
|
-
tunnelId: "abc",
|
|
152
|
-
tunnelName: "maxy",
|
|
153
|
-
domain: "admin.maxy.bot",
|
|
154
|
-
configPath: "/cfg.yml",
|
|
155
|
-
credentialsPath: "/creds.json",
|
|
156
|
-
sshHostname: "ssh.maxy.bot",
|
|
157
|
-
smbHostname: "smb.maxy.bot",
|
|
158
|
-
}),
|
|
159
|
-
);
|
|
160
|
-
const got = readPersistedHostnames(path);
|
|
161
|
-
assert.deepEqual(got, { sshHostname: "ssh.maxy.bot", smbHostname: "smb.maxy.bot" });
|
|
162
|
-
} finally {
|
|
163
|
-
rmSync(dir, { recursive: true, force: true });
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
test("readPersistedHostnames: malformed JSON returns nulls (no throw)", () => {
|
|
168
|
-
const dir = mkdtempSync(join(tmpdir(), "tunnel-state-bad-"));
|
|
169
|
-
const path = join(dir, "tunnel.state");
|
|
170
|
-
try {
|
|
171
|
-
writeFileSync(path, "not json");
|
|
172
|
-
assert.deepEqual(readPersistedHostnames(path), { sshHostname: null, smbHostname: null });
|
|
173
|
-
} finally {
|
|
174
|
-
rmSync(dir, { recursive: true, force: true });
|
|
175
|
-
}
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
// ---------------------------------------------------------------------------
|
|
179
|
-
// probeSambaStanza
|
|
180
|
-
// ---------------------------------------------------------------------------
|
|
181
|
-
|
|
182
|
-
test("probeSambaStanza: true when [<brand>] header present", () => {
|
|
183
|
-
const dir = mkdtempSync(join(tmpdir(), "smb-probe-"));
|
|
184
|
-
const path = join(dir, "smb.conf");
|
|
185
|
-
try {
|
|
186
|
-
writeFileSync(path, "[global]\nworkgroup = WORKGROUP\n\n[maxy]\n path = /home/admin\n");
|
|
187
|
-
assert.equal(probeSambaStanza("maxy", path), true);
|
|
188
|
-
} finally {
|
|
189
|
-
rmSync(dir, { recursive: true, force: true });
|
|
190
|
-
}
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
test("probeSambaStanza: false when only peer brand present (isolation)", () => {
|
|
194
|
-
const dir = mkdtempSync(join(tmpdir(), "smb-probe-peer-"));
|
|
195
|
-
const path = join(dir, "smb.conf");
|
|
196
|
-
try {
|
|
197
|
-
writeFileSync(path, "[global]\n\n[realagent]\n path = /home/admin\n");
|
|
198
|
-
assert.equal(probeSambaStanza("maxy", path), false);
|
|
199
|
-
} finally {
|
|
200
|
-
rmSync(dir, { recursive: true, force: true });
|
|
201
|
-
}
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
test("probeSambaStanza: false when smb.conf missing", () => {
|
|
205
|
-
assert.equal(probeSambaStanza("maxy", "/nope/smb.conf"), false);
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
test("probeSambaStanza: brand names with regex meta characters are escaped", () => {
|
|
209
|
-
const dir = mkdtempSync(join(tmpdir(), "smb-probe-regex-"));
|
|
210
|
-
const path = join(dir, "smb.conf");
|
|
211
|
-
try {
|
|
212
|
-
writeFileSync(path, "[some.brand]\n path = /x\n");
|
|
213
|
-
assert.equal(probeSambaStanza("some.brand", path), true);
|
|
214
|
-
assert.equal(probeSambaStanza("someXbrand", path), false);
|
|
215
|
-
} finally {
|
|
216
|
-
rmSync(dir, { recursive: true, force: true });
|
|
217
|
-
}
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
// ---------------------------------------------------------------------------
|
|
221
|
-
// renderAccessPolicyActionRequired
|
|
222
|
-
// ---------------------------------------------------------------------------
|
|
223
|
-
|
|
224
|
-
test("renderAccessPolicyActionRequired: empty string when neither hostname set", () => {
|
|
225
|
-
assert.equal(
|
|
226
|
-
renderAccessPolicyActionRequired({ operatorEmail: "x@y.com" }),
|
|
227
|
-
"",
|
|
228
|
-
);
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
test("renderAccessPolicyActionRequired: lists each hostname with operator email", () => {
|
|
232
|
-
const out = renderAccessPolicyActionRequired({
|
|
233
|
-
sshHostname: "ssh.maxy.bot",
|
|
234
|
-
smbHostname: "smb.maxy.bot",
|
|
235
|
-
operatorEmail: "joel@example.com",
|
|
236
|
-
});
|
|
237
|
-
assert.match(out, /SSH: ssh\.maxy\.bot/);
|
|
238
|
-
assert.match(out, /SMB: smb\.maxy\.bot/);
|
|
239
|
-
assert.match(out, /Emails: joel@example\.com/);
|
|
240
|
-
assert.match(out, /Zero Trust → Access → Applications/);
|
|
241
|
-
});
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Deterministic scrape of the operator's Cloudflare dashboard to discover
|
|
3
|
-
# the domains on the logged-in account. Output on stdout is a JSON `string[]`,
|
|
4
|
-
# sorted and deduped. Every failure exits non-zero with a `reason=<enum>`
|
|
5
|
-
# token on stderr tagged `[list-cf-domains]`.
|
|
6
|
-
#
|
|
7
|
-
# Usage:
|
|
8
|
-
# list-cf-domains.sh <brand>
|
|
9
|
-
#
|
|
10
|
-
# 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` (the 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.
|
|
21
|
-
#
|
|
22
|
-
# Runtime: Node 22's `--experimental-strip-types` runs the .ts helper
|
|
23
|
-
# directly, so no tsx / playwright / ws dependency exists.
|
|
24
|
-
|
|
25
|
-
set -euo pipefail
|
|
26
|
-
|
|
27
|
-
# Resolve symlinks before dirname — ~/list-cf-domains.sh is installed as a
|
|
28
|
-
# symlink into $HOME, so the raw BASH_SOURCE[0] points at $HOME, not the
|
|
29
|
-
# scripts directory.
|
|
30
|
-
SCRIPT_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
|
|
31
|
-
|
|
32
|
-
# Phase-line helper. Stream-log plumbing retired in Task 287 — every
|
|
33
|
-
# observation goes straight to stdout for the PTY to render.
|
|
34
|
-
phase_line() {
|
|
35
|
-
local scope=$1
|
|
36
|
-
shift
|
|
37
|
-
printf '[%s] %s\n' "${scope}" "$*"
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if [ "$#" -lt 1 ] || [ -z "${1:-}" ]; then
|
|
41
|
-
phase_line list-cf-domains phase=error reason=brand-arg-missing
|
|
42
|
-
exit 1
|
|
43
|
-
fi
|
|
44
|
-
|
|
45
|
-
BRAND="${1}"
|
|
46
|
-
CONFIG_DIR=".${BRAND}"
|
|
47
|
-
mkdir -p "${HOME}/${CONFIG_DIR}/logs"
|
|
48
|
-
|
|
49
|
-
# MAXY_PLATFORM_ROOT may be set by the calling environment. Direct-from-Bash
|
|
50
|
-
# invocation has no such env so derive it from the resolved script location:
|
|
51
|
-
# scripts/ → cloudflare/ → 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
|
-
|
|
56
|
-
phase_line list-cf-domains phase=script-start brand="${BRAND}" config_dir="${CONFIG_DIR}"
|
|
57
|
-
|
|
58
|
-
TS_ENTRY="${SCRIPT_DIR}/list-cf-domains.ts"
|
|
59
|
-
|
|
60
|
-
if [ ! -f "${TS_ENTRY}" ]; then
|
|
61
|
-
phase_line list-cf-domains phase=error reason=entry-missing path="${TS_ENTRY}"
|
|
62
|
-
exit 1
|
|
63
|
-
fi
|
|
64
|
-
|
|
65
|
-
# 30s hard ceiling. The node helper has its own per-phase budgets totalling
|
|
66
|
-
# ~27s; the wrapper's extra 3s absorbs process startup and CF network jitter.
|
|
67
|
-
# Use `timeout --preserve-status` so the exit code of a timeout is distinct
|
|
68
|
-
# (124) from a helper-reported failure (1).
|
|
69
|
-
HARD_TIMEOUT_SECS=30
|
|
70
|
-
|
|
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.
|
|
80
|
-
#
|
|
81
|
-
# `node --experimental-strip-types` (stable in Node 22.22) runs the .ts file
|
|
82
|
-
# natively: type annotations are stripped, no enums / decorators are used, so
|
|
83
|
-
# no TS type-transform is needed. `--no-warnings` suppresses the experimental
|
|
84
|
-
# banner which would otherwise leak into stderr and confuse the route parser.
|
|
85
|
-
set +e
|
|
86
|
-
CONFIG_DIR="${CONFIG_DIR}" BRAND="${BRAND}" MAXY_PLATFORM_ROOT="${MAXY_PLATFORM_ROOT}" \
|
|
87
|
-
timeout --preserve-status --signal=TERM "${HARD_TIMEOUT_SECS}" \
|
|
88
|
-
node --experimental-strip-types --no-warnings "${TS_ENTRY}"
|
|
89
|
-
EXIT_CODE=$?
|
|
90
|
-
set -e
|
|
91
|
-
|
|
92
|
-
if [ "${EXIT_CODE}" -eq 124 ]; then
|
|
93
|
-
phase_line list-cf-domains phase=script-exit code=124 reason=wrapper-timeout budget_secs="${HARD_TIMEOUT_SECS}"
|
|
94
|
-
exit 1
|
|
95
|
-
fi
|
|
96
|
-
|
|
97
|
-
phase_line list-cf-domains phase=script-exit code="${EXIT_CODE}"
|
|
98
|
-
exit "${EXIT_CODE}"
|