@rubytech/create-realagent 1.0.646 → 1.0.648
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 -1
- package/package.json +1 -1
- package/payload/platform/plugins/admin/skills/onboarding/SKILL.md +1 -11
- package/payload/platform/plugins/cloudflare/PLUGIN.md +3 -2
- package/payload/platform/plugins/cloudflare/scripts/_cdp-authorize.mjs +274 -0
- package/payload/platform/plugins/cloudflare/scripts/list-cf-domains.sh +69 -0
- package/payload/platform/plugins/cloudflare/scripts/list-cf-domains.ts +401 -0
- package/payload/platform/plugins/cloudflare/scripts/setup-tunnel.sh +73 -3
- package/payload/platform/plugins/cloudflare/skills/setup-tunnel/SKILL.md +4 -2
- package/payload/platform/plugins/docs/references/cloudflare.md +1 -0
- package/payload/server/public/assets/{admin-Cahp6spj.js → admin-uiqCo17I.js} +4 -4
- package/payload/server/public/assets/{data-B4mICUUf.js → data-D8kol1ed.js} +1 -1
- package/payload/server/public/assets/jsx-runtime-BQmd8XDE.css +1 -0
- package/payload/server/public/assets/{public-DUzYc3_6.js → public-CWuf8cLU.js} +1 -1
- package/payload/server/public/assets/{share-2-Dl0LQGkm.js → share-2-wGga_ldi.js} +1 -1
- package/payload/server/public/assets/{useVoiceRecorder-0mmp40A4.js → useVoiceRecorder-C0Fvv_Bt.js} +1 -1
- package/payload/server/public/data.html +4 -4
- package/payload/server/public/index.html +5 -5
- package/payload/server/public/public.html +4 -4
- package/payload/server/server.js +1944 -2153
- package/payload/platform/scripts/hooks/pre-commit +0 -111
- package/payload/server/public/assets/jsx-runtime-DzyKTi89.css +0 -1
- /package/payload/server/public/assets/{jsx-runtime-BYz1l0sM.js → jsx-runtime-DwoXvzmf.js} +0 -0
package/dist/index.js
CHANGED
|
@@ -1400,9 +1400,17 @@ function createTunnelSymlink(linkPath, target) {
|
|
|
1400
1400
|
function installTunnelScripts() {
|
|
1401
1401
|
const setupSrc = join(INSTALL_DIR, "platform/plugins/cloudflare/scripts/setup-tunnel.sh");
|
|
1402
1402
|
const resetSrc = join(INSTALL_DIR, "platform/plugins/cloudflare/scripts/reset-tunnel.sh");
|
|
1403
|
+
const listSrc = join(INSTALL_DIR, "platform/plugins/cloudflare/scripts/list-cf-domains.sh");
|
|
1404
|
+
// _cdp-authorize.mjs is invoked by setup-tunnel.sh via `node <path>` and
|
|
1405
|
+
// resolved via readlink -f → same dir as the script. It does NOT get a
|
|
1406
|
+
// $HOME symlink — it's a helper, not a top-level operator command. We do
|
|
1407
|
+
// chmod +x defensively so `ls -l` and any ad-hoc `~/setup-tunnel.sh` copy
|
|
1408
|
+
// flow sees it as executable (Task 588).
|
|
1409
|
+
const cdpAuthorizeSrc = join(INSTALL_DIR, "platform/plugins/cloudflare/scripts/_cdp-authorize.mjs");
|
|
1403
1410
|
const setupLink = resolve(process.env.HOME ?? "/root", "setup-tunnel.sh");
|
|
1404
1411
|
const resetLink = resolve(process.env.HOME ?? "/root", "reset-tunnel.sh");
|
|
1405
|
-
|
|
1412
|
+
const listLink = resolve(process.env.HOME ?? "/root", "list-cf-domains.sh");
|
|
1413
|
+
for (const src of [setupSrc, resetSrc, listSrc, cdpAuthorizeSrc]) {
|
|
1406
1414
|
try {
|
|
1407
1415
|
chmodSync(src, 0o755);
|
|
1408
1416
|
}
|
|
@@ -1415,6 +1423,7 @@ function installTunnelScripts() {
|
|
|
1415
1423
|
}
|
|
1416
1424
|
createTunnelSymlink(setupLink, setupSrc);
|
|
1417
1425
|
createTunnelSymlink(resetLink, resetSrc);
|
|
1426
|
+
createTunnelSymlink(listLink, listSrc);
|
|
1418
1427
|
}
|
|
1419
1428
|
// ---------------------------------------------------------------------------
|
|
1420
1429
|
// Cron Registration
|
|
@@ -1716,6 +1725,7 @@ WantedBy=multi-user.target
|
|
|
1716
1725
|
for (const linkPath of [
|
|
1717
1726
|
resolve(process.env.HOME ?? "/root", "setup-tunnel.sh"),
|
|
1718
1727
|
resolve(process.env.HOME ?? "/root", "reset-tunnel.sh"),
|
|
1728
|
+
resolve(process.env.HOME ?? "/root", "list-cf-domains.sh"),
|
|
1719
1729
|
]) {
|
|
1720
1730
|
try {
|
|
1721
1731
|
accessSync(linkPath, fsConstants.X_OK);
|
package/package.json
CHANGED
|
@@ -129,22 +129,12 @@ If the user skips, let them know they can set up Cloudflare at any time by askin
|
|
|
129
129
|
|
|
130
130
|
If the user wants to proceed, first confirm the domain state in one sentence: "Is your domain already on a Cloudflare account?" If not, quote the click-path from `plugins/cloudflare/references/dashboard-guide.md` § "Add a domain to a Cloudflare account" and wait for the user to confirm the zone is **Active**.
|
|
131
131
|
|
|
132
|
-
Then call `render-component` with `name: "cloudflare-setup-form"` and data containing the domain
|
|
133
|
-
|
|
134
|
-
Data shape (select the option lists by brand — read `brand.json` first to confirm which brand is installed):
|
|
135
|
-
|
|
136
|
-
- **Maxy brand**: `adminDomainOptions: ["maxy.bot"]`, `publicDomainOptions: ["maxy.bot", "maxy.chat"]`, `apexDomainOptions: ["maxy.chat"]`.
|
|
137
|
-
- **Real Agent brand**: `adminDomainOptions: ["realagent.network"]`, `publicDomainOptions: ["realagent.network"]`, `apexDomainOptions: []`.
|
|
132
|
+
Then call `render-component` with `name: "cloudflare-setup-form"` and data containing only the form copy — the form self-discovers the admin/public/apex domain options from the operator's logged-in Cloudflare dashboard (Task 589) via `/api/admin/cloudflare/domains`. Do not read `brand.json` for domain options; brand identity has no authority over what zones the operator's Cloudflare account holds. The form collects admin label + admin domain + optional public label + public domain + optional apex + admin password in one submission, then the submit handler runs `setRemotePassword`, `~/setup-tunnel.sh`, and alias-domain writes server-side and relays the script's output — including any `ACTION REQUIRED` block — back as the component's submitted payload.
|
|
138
133
|
|
|
139
134
|
```
|
|
140
135
|
{
|
|
141
136
|
title: "Set up remote access",
|
|
142
137
|
description: "Connect your device to a custom domain.",
|
|
143
|
-
adminDomainOptions: [...],
|
|
144
|
-
publicDomainOptions: [...],
|
|
145
|
-
apexDomainOptions: [...],
|
|
146
|
-
defaultAdminDomain: "<first admin domain>",
|
|
147
|
-
defaultPublicDomain: "<first public domain>",
|
|
148
138
|
submitLabel: "Set up Cloudflare"
|
|
149
139
|
}
|
|
150
140
|
```
|
|
@@ -30,8 +30,9 @@ The plugin registers no agent-facing MCP tools (Task 554). Every Cloudflare oper
|
|
|
30
30
|
|
|
31
31
|
| Script | Purpose |
|
|
32
32
|
|---|---|
|
|
33
|
-
| [`scripts/setup-tunnel.sh`](scripts/setup-tunnel.sh) | Autonomous end-to-end setup: OAuth login, tunnel create, DNS route, config + state, service restart, post-restart verification. Invocation: `~/setup-tunnel.sh <brand> <port> <admin-hostname> [<public-hostname>] [<apex-hostname>]`. Apex hostnames print an `ACTION REQUIRED` block for the dashboard record the CLI cannot create. |
|
|
34
|
-
| [`scripts/reset-tunnel.sh`](scripts/reset-tunnel.sh) | Deletes every tunnel on the brand's CF account and wipes `${CFG_DIR}`. Does not touch the platform service, stray CNAMEs, or token-mode connectors — those require dashboard cleanup or `pkill`. Invocation: `~/reset-tunnel.sh <brand>`. |
|
|
33
|
+
| [`scripts/setup-tunnel.sh`](scripts/setup-tunnel.sh) | Autonomous end-to-end setup: OAuth login, tunnel create, DNS route, config + state, service restart, post-restart verification. Invocation: `~/setup-tunnel.sh <brand> <port> <admin-hostname> [<public-hostname>] [<apex-hostname>]`. Apex hostnames print an `ACTION REQUIRED` block for the dashboard record the CLI cannot create. Task 588 drives the Cloudflare Authorize click via CDP WebSocket (`_cdp-authorize.mjs`, below) — no human click required when the VNC browser is already signed into Cloudflare. The OAuth cert-wait after the click is bounded at 20 s with a 2-second `step=oauth-login result=awaiting-cert elapsed=<N>s` heartbeat so no poll exceeds ~2 s silent. |
|
|
34
|
+
| [`scripts/reset-tunnel.sh`](scripts/reset-tunnel.sh) | Deletes every tunnel on the brand's CF account and wipes `${CFG_DIR}`. Does not touch the platform service, stray CNAMEs, or token-mode connectors — those require dashboard cleanup or `pkill`. Invocation: `~/reset-tunnel.sh <brand>`. No polling blocks — every long-wait is bounded by `cloudflared`'s network round-trip, so no heartbeat contract applies. |
|
|
35
|
+
| [`scripts/_cdp-authorize.mjs`](scripts/_cdp-authorize.mjs) | Node 22 ESM helper driving the Cloudflare argotunnel Authorize click via CDP WebSocket. Self-contained (native `fetch` + `WebSocket`), invoked by `setup-tunnel.sh` with the target id returned from the `PUT /json/new?<url>` step. Exits 0 on successful click; 1 on `authorize-button-not-found` (the happy-path loud failure when the VNC browser isn't signed into Cloudflare); 2/3/4/5 on CDP/protocol errors. Emits one `cdp-authorize result=<…> reason=<…>` line to stdout per invocation, teed into the stream log under the `[setup-tunnel:cdp-click]` tag. |
|
|
35
36
|
|
|
36
37
|
### Skills
|
|
37
38
|
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Drive the Cloudflare argotunnel Authorize click via CDP WebSocket (Task 588).
|
|
3
|
+
//
|
|
4
|
+
// Why this exists: setup-tunnel.sh navigates the VNC browser to the argotunnel
|
|
5
|
+
// consent URL via `PUT /json/new?<url>`. That opens the page but does not
|
|
6
|
+
// press the Authorize button. Pre-Task 588 the script then polled for
|
|
7
|
+
// cert.pem for up to 180 seconds, waiting for a human click in VNC. This
|
|
8
|
+
// helper does the click from code, collapsing the wait to ~1-3 seconds.
|
|
9
|
+
//
|
|
10
|
+
// Protocol: Chrome DevTools Protocol over WebSocket (https://chromedevtools.github.io/devtools-protocol/).
|
|
11
|
+
// 1. GET http://127.0.0.1:9222/json/list → find the target with matching id;
|
|
12
|
+
// extract webSocketDebuggerUrl.
|
|
13
|
+
// 2. Open WS, enable Page + Runtime domains.
|
|
14
|
+
// 3. Wait for Page.loadEventFired (or observe document.readyState==='complete').
|
|
15
|
+
// 4. Poll Runtime.evaluate every 200 ms for up to ~3 s, looking for a
|
|
16
|
+
// button or input[type=submit] whose trimmed text/value matches
|
|
17
|
+
// /^(authorize|connect)$/i. When found, click via JS (click() on the
|
|
18
|
+
// matched node). Exit 0 on success, 1 with structured stderr otherwise.
|
|
19
|
+
//
|
|
20
|
+
// Exit codes:
|
|
21
|
+
// 0 - clicked successfully
|
|
22
|
+
// 1 - authorize-button-not-found (the happy-path loud failure)
|
|
23
|
+
// 2 - cdp-ws-unreachable or node-websocket-unavailable
|
|
24
|
+
// 3 - target-not-found (the target_id isn't in /json/list)
|
|
25
|
+
// 4 - click-evaluate-threw (Runtime.evaluate returned wasThrown=true)
|
|
26
|
+
// 5 - protocol-error (malformed CDP response)
|
|
27
|
+
//
|
|
28
|
+
// Each structured failure prints ONE line to stdout in phase_line-compatible
|
|
29
|
+
// shape so setup-tunnel.sh's tee_subprocess picks it up into the stream log:
|
|
30
|
+
// cdp-authorize result=<ok|error> reason=<…> elapsed_ms=<…> [detail=<…>]
|
|
31
|
+
|
|
32
|
+
const CDP_HOST = '127.0.0.1';
|
|
33
|
+
const CDP_PORT = 9222;
|
|
34
|
+
const HTTP_TIMEOUT_MS = 3000;
|
|
35
|
+
const BUTTON_POLL_TIMEOUT_MS = 3000;
|
|
36
|
+
const BUTTON_POLL_INTERVAL_MS = 200;
|
|
37
|
+
const LOAD_WAIT_TIMEOUT_MS = 5000;
|
|
38
|
+
const WS_CONNECT_TIMEOUT_MS = 3000;
|
|
39
|
+
const RESULT_WAIT_TIMEOUT_MS = 3000;
|
|
40
|
+
|
|
41
|
+
function emit(result, reason, extra = {}) {
|
|
42
|
+
const parts = [`cdp-authorize result=${result} reason=${reason}`];
|
|
43
|
+
for (const [k, v] of Object.entries(extra)) {
|
|
44
|
+
if (v === undefined || v === null) continue;
|
|
45
|
+
const s = String(v).replace(/\s+/g, ' ');
|
|
46
|
+
parts.push(`${k}="${s.slice(0, 200)}"`);
|
|
47
|
+
}
|
|
48
|
+
process.stdout.write(parts.join(' ') + '\n');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function die(code, reason, extra) {
|
|
52
|
+
emit('error', reason, extra);
|
|
53
|
+
process.exit(code);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (typeof WebSocket === 'undefined') {
|
|
57
|
+
die(2, 'node-websocket-unavailable', {
|
|
58
|
+
detail: `Node ${process.version} has no global WebSocket — upgrade to Node 22+`,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const targetId = process.argv[2];
|
|
63
|
+
if (!targetId) {
|
|
64
|
+
die(2, 'missing-target-id', { detail: 'usage: _cdp-authorize.mjs <target-id>' });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const started = Date.now();
|
|
68
|
+
|
|
69
|
+
async function fetchTargets() {
|
|
70
|
+
const ac = new AbortController();
|
|
71
|
+
const timer = setTimeout(() => ac.abort(), HTTP_TIMEOUT_MS);
|
|
72
|
+
try {
|
|
73
|
+
const res = await fetch(`http://${CDP_HOST}:${CDP_PORT}/json/list`, { signal: ac.signal });
|
|
74
|
+
if (!res.ok) throw new Error(`/json/list returned ${res.status}`);
|
|
75
|
+
return await res.json();
|
|
76
|
+
} finally {
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let targets;
|
|
82
|
+
try {
|
|
83
|
+
targets = await fetchTargets();
|
|
84
|
+
} catch (err) {
|
|
85
|
+
die(2, 'cdp-ws-unreachable', { detail: err instanceof Error ? err.message : String(err) });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const target = Array.isArray(targets) ? targets.find((t) => t && t.id === targetId) : undefined;
|
|
89
|
+
if (!target || typeof target.webSocketDebuggerUrl !== 'string') {
|
|
90
|
+
die(3, 'target-not-found', { target_id: targetId, count: Array.isArray(targets) ? targets.length : -1 });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Open WebSocket. Native Node 22 global.
|
|
94
|
+
let ws;
|
|
95
|
+
try {
|
|
96
|
+
ws = new WebSocket(target.webSocketDebuggerUrl);
|
|
97
|
+
} catch (err) {
|
|
98
|
+
die(2, 'cdp-ws-unreachable', { detail: err instanceof Error ? err.message : String(err) });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// WS open with race guards. Three outcomes are possible:
|
|
102
|
+
// 1. `open` fires before `error` / timeout → resolve and continue.
|
|
103
|
+
// 2. `error` fires before `open` → reject; caller emits die() and exits.
|
|
104
|
+
// 3. Timeout fires before either → reject; caller emits die() and exits.
|
|
105
|
+
// The `.once` on `open`/`error` listeners handles the race where both fire
|
|
106
|
+
// in quick succession (some proxy-protocol mismatches can emit `error`
|
|
107
|
+
// before the open event is dispatched). The catch below awaits the die()
|
|
108
|
+
// call and then `throw`s to guarantee execution does not fall through to
|
|
109
|
+
// the Page.enable call with a dead socket — even if a future refactor
|
|
110
|
+
// removes process.exit() from die().
|
|
111
|
+
try {
|
|
112
|
+
await new Promise((resolveOpen, rejectOpen) => {
|
|
113
|
+
const timer = setTimeout(() => rejectOpen(new Error('ws open timeout')), WS_CONNECT_TIMEOUT_MS);
|
|
114
|
+
ws.addEventListener('open', () => { clearTimeout(timer); resolveOpen(); }, { once: true });
|
|
115
|
+
ws.addEventListener('error', (e) => { clearTimeout(timer); rejectOpen(new Error(`ws error: ${e.message ?? 'unknown'}`)); }, { once: true });
|
|
116
|
+
});
|
|
117
|
+
} catch (err) {
|
|
118
|
+
die(2, 'cdp-ws-unreachable', { detail: err instanceof Error ? err.message : String(err) });
|
|
119
|
+
throw err; // unreachable — die() exits, but guards the type system and future refactors.
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Message router: CDP replies come back keyed by the `id` we sent. Events
|
|
123
|
+
// arrive with no `id` but a `method` (e.g. "Page.loadEventFired").
|
|
124
|
+
let nextId = 1;
|
|
125
|
+
const pending = new Map();
|
|
126
|
+
const eventListeners = new Map();
|
|
127
|
+
|
|
128
|
+
ws.addEventListener('message', (evt) => {
|
|
129
|
+
let msg;
|
|
130
|
+
try {
|
|
131
|
+
msg = JSON.parse(typeof evt.data === 'string' ? evt.data : evt.data.toString());
|
|
132
|
+
} catch {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (typeof msg.id === 'number' && pending.has(msg.id)) {
|
|
136
|
+
const { resolveIt, rejectIt } = pending.get(msg.id);
|
|
137
|
+
pending.delete(msg.id);
|
|
138
|
+
if (msg.error) rejectIt(new Error(`CDP error: ${msg.error.message ?? JSON.stringify(msg.error)}`));
|
|
139
|
+
else resolveIt(msg.result);
|
|
140
|
+
} else if (typeof msg.method === 'string') {
|
|
141
|
+
const listeners = eventListeners.get(msg.method);
|
|
142
|
+
if (listeners) for (const l of listeners) l(msg.params);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
ws.addEventListener('close', () => {
|
|
147
|
+
for (const { rejectIt } of pending.values()) {
|
|
148
|
+
rejectIt(new Error('ws closed while awaiting response'));
|
|
149
|
+
}
|
|
150
|
+
pending.clear();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
function send(method, params = {}) {
|
|
154
|
+
const id = nextId++;
|
|
155
|
+
return new Promise((resolveIt, rejectIt) => {
|
|
156
|
+
const timer = setTimeout(() => {
|
|
157
|
+
pending.delete(id);
|
|
158
|
+
rejectIt(new Error(`CDP ${method} timed out after ${RESULT_WAIT_TIMEOUT_MS}ms`));
|
|
159
|
+
}, RESULT_WAIT_TIMEOUT_MS);
|
|
160
|
+
pending.set(id, {
|
|
161
|
+
resolveIt: (r) => { clearTimeout(timer); resolveIt(r); },
|
|
162
|
+
rejectIt: (e) => { clearTimeout(timer); rejectIt(e); },
|
|
163
|
+
});
|
|
164
|
+
ws.send(JSON.stringify({ id, method, params }));
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function onEvent(method, listener) {
|
|
169
|
+
let set = eventListeners.get(method);
|
|
170
|
+
if (!set) { set = new Set(); eventListeners.set(method, set); }
|
|
171
|
+
set.add(listener);
|
|
172
|
+
return () => set.delete(listener);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Enable the two domains we need. Page.enable fires events; Runtime.enable
|
|
176
|
+
// lets us run expressions in the page context.
|
|
177
|
+
try {
|
|
178
|
+
await send('Page.enable');
|
|
179
|
+
await send('Runtime.enable');
|
|
180
|
+
} catch (err) {
|
|
181
|
+
die(5, 'protocol-error', { detail: err.message });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Wait for load — if the page is already loaded (navigated before this helper
|
|
185
|
+
// ran), Page.loadEventFired may never fire again; poll document.readyState
|
|
186
|
+
// as a fallback. The first of the two to resolve wins.
|
|
187
|
+
async function waitForLoad() {
|
|
188
|
+
const loadFired = new Promise((resolveIt) => {
|
|
189
|
+
const off = onEvent('Page.loadEventFired', () => { off(); resolveIt('loadEvent'); });
|
|
190
|
+
});
|
|
191
|
+
const readyStatePoll = (async () => {
|
|
192
|
+
const deadline = Date.now() + LOAD_WAIT_TIMEOUT_MS;
|
|
193
|
+
while (Date.now() < deadline) {
|
|
194
|
+
try {
|
|
195
|
+
const r = await send('Runtime.evaluate', {
|
|
196
|
+
expression: 'document.readyState',
|
|
197
|
+
returnByValue: true,
|
|
198
|
+
});
|
|
199
|
+
if (r?.result?.value === 'complete' || r?.result?.value === 'interactive') return 'readyState';
|
|
200
|
+
} catch { /* keep polling */ }
|
|
201
|
+
await new Promise((res) => setTimeout(res, 200));
|
|
202
|
+
}
|
|
203
|
+
throw new Error('load-wait-timeout');
|
|
204
|
+
})();
|
|
205
|
+
return Promise.race([loadFired, readyStatePoll]);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
await waitForLoad();
|
|
210
|
+
} catch (err) {
|
|
211
|
+
die(1, 'page-load-timeout', { detail: err.message, elapsed_ms: Date.now() - started });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Poll for the Authorize button. The expression finds the first
|
|
215
|
+
// button/input[type=submit] whose trimmed textContent/value matches
|
|
216
|
+
// /^(authorize|connect)$/i, clicks it, and returns a descriptor. Returns
|
|
217
|
+
// null when no match yet.
|
|
218
|
+
const CLICK_EXPR = `
|
|
219
|
+
(() => {
|
|
220
|
+
const candidates = Array.from(document.querySelectorAll('button, input[type="submit"]'));
|
|
221
|
+
const match = candidates.find((el) => {
|
|
222
|
+
const text = (el.textContent ?? el.value ?? '').trim();
|
|
223
|
+
return /^(authorize|connect)$/i.test(text) && !el.disabled;
|
|
224
|
+
});
|
|
225
|
+
if (!match) return null;
|
|
226
|
+
const descriptor = {
|
|
227
|
+
tag: match.tagName.toLowerCase(),
|
|
228
|
+
text: (match.textContent ?? match.value ?? '').trim().slice(0, 40),
|
|
229
|
+
disabled: Boolean(match.disabled),
|
|
230
|
+
};
|
|
231
|
+
match.click();
|
|
232
|
+
return descriptor;
|
|
233
|
+
})()
|
|
234
|
+
`;
|
|
235
|
+
|
|
236
|
+
const clickDeadline = Date.now() + BUTTON_POLL_TIMEOUT_MS;
|
|
237
|
+
let clicked = null;
|
|
238
|
+
while (Date.now() < clickDeadline) {
|
|
239
|
+
let r;
|
|
240
|
+
try {
|
|
241
|
+
r = await send('Runtime.evaluate', {
|
|
242
|
+
expression: CLICK_EXPR,
|
|
243
|
+
returnByValue: true,
|
|
244
|
+
awaitPromise: false,
|
|
245
|
+
});
|
|
246
|
+
} catch (err) {
|
|
247
|
+
die(5, 'protocol-error', { detail: err.message });
|
|
248
|
+
}
|
|
249
|
+
if (r?.exceptionDetails) {
|
|
250
|
+
die(4, 'click-evaluate-threw', {
|
|
251
|
+
detail: r.exceptionDetails.text ?? 'unknown exception',
|
|
252
|
+
elapsed_ms: Date.now() - started,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
const val = r?.result?.value;
|
|
256
|
+
if (val && typeof val === 'object') {
|
|
257
|
+
clicked = val;
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
await new Promise((res) => setTimeout(res, BUTTON_POLL_INTERVAL_MS));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (!clicked) {
|
|
264
|
+
die(1, 'authorize-button-not-found', { elapsed_ms: Date.now() - started });
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
emit('ok', 'clicked', {
|
|
268
|
+
tag: clicked.tag,
|
|
269
|
+
text: clicked.text,
|
|
270
|
+
elapsed_ms: Date.now() - started,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
try { ws.close(); } catch { /* best-effort */ }
|
|
274
|
+
process.exit(0);
|
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
# The brand arg (default: maxy) names the `${HOME}/.${BRAND}/logs/` directory
|
|
11
|
+
# where selector-drift body dumps land. The script does not read brand.json —
|
|
12
|
+
# the directory is the only brand-derived path it needs.
|
|
13
|
+
#
|
|
14
|
+
# Runtime: Node 22's `--experimental-strip-types` runs the .ts helper
|
|
15
|
+
# directly, so no tsx / playwright / ws dependency exists.
|
|
16
|
+
|
|
17
|
+
set -euo pipefail
|
|
18
|
+
|
|
19
|
+
# --------------------------------------------------------------------------
|
|
20
|
+
# Shared stream-log helpers (require STREAM_LOG_PATH, phase_line, …).
|
|
21
|
+
# --------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
# shellcheck source=_stream-log.sh
|
|
24
|
+
# Resolve symlinks before dirname — ~/list-cf-domains.sh is installed as a
|
|
25
|
+
# symlink into $HOME, so the raw BASH_SOURCE[0] points at $HOME, not the
|
|
26
|
+
# scripts directory where _stream-log.sh lives.
|
|
27
|
+
source "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/_stream-log.sh"
|
|
28
|
+
require_stream_log_path list-cf-domains
|
|
29
|
+
|
|
30
|
+
BRAND="${1:-maxy}"
|
|
31
|
+
CONFIG_DIR=".${BRAND}"
|
|
32
|
+
mkdir -p "${HOME}/${CONFIG_DIR}/logs"
|
|
33
|
+
|
|
34
|
+
phase_line list-cf-domains phase=script-start brand="${BRAND}" config_dir="${CONFIG_DIR}"
|
|
35
|
+
|
|
36
|
+
SCRIPT_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
|
|
37
|
+
TS_ENTRY="${SCRIPT_DIR}/list-cf-domains.ts"
|
|
38
|
+
|
|
39
|
+
if [ ! -f "${TS_ENTRY}" ]; then
|
|
40
|
+
phase_line list-cf-domains phase=error reason=entry-missing path="${TS_ENTRY}"
|
|
41
|
+
exit 1
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# 30s hard ceiling. The node helper has its own per-phase budgets totalling
|
|
45
|
+
# ~27s; the wrapper's extra 3s absorbs process startup and CF network jitter.
|
|
46
|
+
# Use `timeout --preserve-status` so the exit code of a timeout is distinct
|
|
47
|
+
# (124) from a helper-reported failure (1).
|
|
48
|
+
HARD_TIMEOUT_SECS=30
|
|
49
|
+
|
|
50
|
+
# CONFIG_DIR is consumed by the node helper when writing selector-drift dumps
|
|
51
|
+
# to ~/.${BRAND}/logs/list-cf-domains-<ts>.html.
|
|
52
|
+
#
|
|
53
|
+
# `node --experimental-strip-types` (stable in Node 22.22) runs the .ts file
|
|
54
|
+
# natively: type annotations are stripped, no enums / decorators are used, so
|
|
55
|
+
# no TS type-transform is needed. `--no-warnings` suppresses the experimental
|
|
56
|
+
# banner which would otherwise leak into stderr and confuse the route parser.
|
|
57
|
+
set +e
|
|
58
|
+
CONFIG_DIR="${CONFIG_DIR}" timeout --preserve-status --signal=TERM "${HARD_TIMEOUT_SECS}" \
|
|
59
|
+
node --experimental-strip-types --no-warnings "${TS_ENTRY}"
|
|
60
|
+
EXIT_CODE=$?
|
|
61
|
+
set -e
|
|
62
|
+
|
|
63
|
+
if [ "${EXIT_CODE}" -eq 124 ]; then
|
|
64
|
+
phase_line list-cf-domains phase=script-exit code=124 reason=wrapper-timeout budget_secs="${HARD_TIMEOUT_SECS}"
|
|
65
|
+
exit 1
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
phase_line list-cf-domains phase=script-exit code="${EXIT_CODE}"
|
|
69
|
+
exit "${EXIT_CODE}"
|