@rubytech/create-maxy 1.0.798 → 1.0.800
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 +7 -1
- package/package.json +1 -1
- package/payload/platform/neo4j/schema.cypher +34 -0
- package/payload/platform/plugins/cloudflare/references/manual-setup.md +12 -0
- package/payload/platform/plugins/cloudflare/scripts/_cdp-authorize-matcher.mjs +74 -0
- package/payload/platform/plugins/cloudflare/scripts/_cdp-authorize.mjs +60 -50
- package/payload/platform/plugins/cloudflare/scripts/setup-tunnel.sh +118 -22
- package/payload/platform/plugins/cloudflare/skills/setup-tunnel/SKILL.md +4 -0
- package/payload/server/chunk-SBHI2NMF.js +9910 -0
- package/payload/server/chunk-WHF6YXJ5.js +3456 -0
- package/payload/server/client-pool-4YDRTKAT.js +29 -0
- package/payload/server/maxy-edge.js +2 -2
- package/payload/server/public/assets/{admin-Cz8hUAqx.js → admin-Sa301b8q.js} +6 -6
- package/payload/server/public/index.html +1 -1
- package/payload/server/server.js +90 -33
package/dist/index.js
CHANGED
|
@@ -1765,11 +1765,17 @@ function installTunnelScripts() {
|
|
|
1765
1765
|
// $HOME symlink — it's a helper, not a top-level operator command. We do
|
|
1766
1766
|
// chmod +x defensively so `ls -l` and any ad-hoc `~/setup-tunnel.sh` copy
|
|
1767
1767
|
// flow sees it as executable (Task 588).
|
|
1768
|
+
//
|
|
1769
|
+
// _cdp-authorize-matcher.mjs is imported by _cdp-authorize.mjs (Task 855)
|
|
1770
|
+
// — the tri-state matcher's MATCH_EXPR + findMatch live in this side
|
|
1771
|
+
// module so JSDOM tests run identical logic to the live page. chmod +x
|
|
1772
|
+
// is defensive symmetry; the file is read by Node, not exec'd.
|
|
1768
1773
|
const cdpAuthorizeSrc = join(INSTALL_DIR, "platform/plugins/cloudflare/scripts/_cdp-authorize.mjs");
|
|
1774
|
+
const cdpMatcherSrc = join(INSTALL_DIR, "platform/plugins/cloudflare/scripts/_cdp-authorize-matcher.mjs");
|
|
1769
1775
|
const setupLink = resolve(process.env.HOME ?? "/root", "setup-tunnel.sh");
|
|
1770
1776
|
const resetLink = resolve(process.env.HOME ?? "/root", "reset-tunnel.sh");
|
|
1771
1777
|
const listLink = resolve(process.env.HOME ?? "/root", "list-cf-domains.sh");
|
|
1772
|
-
for (const src of [setupSrc, resetSrc, listSrc, cdpAuthorizeSrc]) {
|
|
1778
|
+
for (const src of [setupSrc, resetSrc, listSrc, cdpAuthorizeSrc, cdpMatcherSrc]) {
|
|
1773
1779
|
try {
|
|
1774
1780
|
chmodSync(src, 0o755);
|
|
1775
1781
|
}
|
package/package.json
CHANGED
|
@@ -919,6 +919,40 @@ FOR (c:Component) ON (c.messageId);
|
|
|
919
919
|
CREATE INDEX component_account IF NOT EXISTS
|
|
920
920
|
FOR (c:Component) ON (c.accountId);
|
|
921
921
|
|
|
922
|
+
// ----------------------------------------------------------
|
|
923
|
+
// :Attachment — Task 854. Sibling node hung off a :Message via
|
|
924
|
+
// [:HAS_ATTACHMENT] capturing user-side file uploads (image, pdf,
|
|
925
|
+
// text/markdown/csv/html, calendar, zip; see attachments.ts
|
|
926
|
+
// SUPPORTED_MIME_TYPES). Mirrors :Component (Task 815).
|
|
927
|
+
//
|
|
928
|
+
// Properties:
|
|
929
|
+
// attachmentId — UUID assigned at storeAttachment time; matches
|
|
930
|
+
// the on-disk directory + Neo4j node id.
|
|
931
|
+
// conversationId — denormalised for filtered scans + cascade delete.
|
|
932
|
+
// accountId — account-isolation. Reads MUST scope by this.
|
|
933
|
+
// messageId — back-pointer to parent :Message.
|
|
934
|
+
// filename — original upload name (kept for chip render).
|
|
935
|
+
// mimeType — validated MIME (assertSupportedMime).
|
|
936
|
+
// sizeBytes — original byte size at upload.
|
|
937
|
+
// storagePath — absolute on-disk path. Server-only — never
|
|
938
|
+
// surfaced to the client (chip click resolves
|
|
939
|
+
// via attachmentId, not path).
|
|
940
|
+
// ordinal — upload-order index within the parent turn (0-based).
|
|
941
|
+
// Reader uses it to re-render chips in upload order.
|
|
942
|
+
// createdAt — datetime when persisted.
|
|
943
|
+
//
|
|
944
|
+
// (:Message)-[:HAS_ATTACHMENT]->(:Attachment)
|
|
945
|
+
// ----------------------------------------------------------
|
|
946
|
+
|
|
947
|
+
CREATE CONSTRAINT attachment_id_unique IF NOT EXISTS
|
|
948
|
+
FOR (a:Attachment) REQUIRE a.attachmentId IS UNIQUE;
|
|
949
|
+
|
|
950
|
+
CREATE INDEX attachment_message IF NOT EXISTS
|
|
951
|
+
FOR (a:Attachment) ON (a.messageId);
|
|
952
|
+
|
|
953
|
+
CREATE INDEX attachment_account IF NOT EXISTS
|
|
954
|
+
FOR (a:Attachment) ON (a.accountId);
|
|
955
|
+
|
|
922
956
|
// ----------------------------------------------------------
|
|
923
957
|
// :Trashed — Task 576 soft-delete primitive.
|
|
924
958
|
//
|
|
@@ -131,6 +131,18 @@ ls /tmp/.X11-unix/
|
|
|
131
131
|
|
|
132
132
|
You should see `X99`. If not, start VNC before retrying Step 1.
|
|
133
133
|
|
|
134
|
+
### Three states the consent page can render
|
|
135
|
+
|
|
136
|
+
The dashboard at `dash.cloudflare.com/argotunnel?...` shows one of three states. The automation script's CDP helper (`_cdp-authorize.mjs`) detects each one; the same triage applies when you walk the flow manually.
|
|
137
|
+
|
|
138
|
+
1. **Pre-authorize — the Authorize button is visible.** This is the normal first run. Click the zone you want, then click **Authorize**. Cloudflare's callback fires and `~/.cloudflared/cert.pem` lands a second or two later. Continue with the `mv` above.
|
|
139
|
+
|
|
140
|
+
2. **Post-success — "Cloudflared has installed a certificate" modal.** The bound account already has a cert authorised for the zones the dashboard knows about. The OAuth callback is idempotent — navigating to a fresh `cloudflared tunnel login` URL still fires the callback, so `~/.cloudflared/cert.pem` will land. Wait a second or two, then run the `mv`.
|
|
141
|
+
|
|
142
|
+
If the cert *also* exists at `${CFG_DIR}/cert.pem` from a prior partial run, Step 1 is already complete — skip to Step 2.
|
|
143
|
+
|
|
144
|
+
3. **Blank or button-less — neither the Authorize button nor the success modal is on the page.** The browser may be mid-load, signed out of Cloudflare, blocking on captcha, or showing some other state. Sign in to Cloudflare in the same browser, navigate back to the URL `cloudflared tunnel login` printed, and complete the click manually. If the URL has expired, kill `cloudflared` (`Ctrl+C`) and re-run Step 1 to get a fresh URL.
|
|
145
|
+
|
|
134
146
|
---
|
|
135
147
|
|
|
136
148
|
## Step 2 — List existing tunnels
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Tri-state DOM matcher for the Cloudflare argotunnel consent page (Task 855).
|
|
2
|
+
//
|
|
3
|
+
// `dash.cloudflare.com/argotunnel?...` legitimately renders three observable
|
|
4
|
+
// states for our flow:
|
|
5
|
+
//
|
|
6
|
+
// 1. Pre-authorize — a <button> or <input type="submit"> whose trimmed text
|
|
7
|
+
// matches /^(authorize|connect)$/i (and is not disabled). Click it; the
|
|
8
|
+
// callback fires, cloudflared writes ~/.cloudflared/cert.pem.
|
|
9
|
+
//
|
|
10
|
+
// 2. Post-success — the dashboard renders a Success modal containing the
|
|
11
|
+
// stable substring "Cloudflared has installed a certificate". This shape
|
|
12
|
+
// is reached when the user already authorised this account before (or
|
|
13
|
+
// did so manually in VNC between cloudflared spawn and helper poll).
|
|
14
|
+
// The OAuth callback is idempotent on the cert side: when the helper
|
|
15
|
+
// sees this state, the running cloudflared subprocess will still write
|
|
16
|
+
// ~/.cloudflared/cert.pem because the callback URL is exercised by the
|
|
17
|
+
// navigation. The wrapper drops into the existing cert-poll afterwards.
|
|
18
|
+
//
|
|
19
|
+
// 3. Neither — the page is mid-load, blank, or in some other state. The
|
|
20
|
+
// caller polls again until BUTTON_POLL_TIMEOUT_MS, then exits 1.
|
|
21
|
+
//
|
|
22
|
+
// The matcher returns a discriminated union from a single DOM read:
|
|
23
|
+
//
|
|
24
|
+
// { kind: 'button', descriptor: {tag,text,disabled} } | { kind: 'success' } | null
|
|
25
|
+
//
|
|
26
|
+
// PRIORITY: button > success modal. Page transitions can briefly render both
|
|
27
|
+
// (e.g. during the success animation). If a clickable Authorize button exists,
|
|
28
|
+
// click it — the click produces a fresh callback regardless of any leftover
|
|
29
|
+
// modal text. Only when no button is present do we trust the modal as the
|
|
30
|
+
// terminal state.
|
|
31
|
+
//
|
|
32
|
+
// EXPORTED SHAPES:
|
|
33
|
+
//
|
|
34
|
+
// * findMatch(doc) — JS function form. Used by JSDOM-based unit tests
|
|
35
|
+
// (platform/ui/scripts/__tests__/cdp-authorize-matcher.test.ts) so future
|
|
36
|
+
// matcher edits replay against captured DOM fixtures offline (Success
|
|
37
|
+
// criterion 8 of Task 855).
|
|
38
|
+
//
|
|
39
|
+
// * MATCH_EXPR — string form, evaluated by Chrome DevTools Protocol's
|
|
40
|
+
// Runtime.evaluate in _cdp-authorize.mjs's polling loop. Built from
|
|
41
|
+
// findMatch.toString() so the live page and the tests run identical
|
|
42
|
+
// logic — single source of truth.
|
|
43
|
+
//
|
|
44
|
+
// CONTRACT — DO NOT loosen the success-modal anchor. Tighter anchors regress
|
|
45
|
+
// on Cloudflare copy edits; "Cloudflared has installed a certificate" is the
|
|
46
|
+
// load-bearing claim of the modal. Wider anchors (e.g. matching just
|
|
47
|
+
// "certificate") would false-positive on the pre-authorize page.
|
|
48
|
+
|
|
49
|
+
export function findMatch(doc) {
|
|
50
|
+
const candidates = Array.from(doc.querySelectorAll('button, input[type="submit"]'));
|
|
51
|
+
const match = candidates.find((el) => {
|
|
52
|
+
const text = (el.textContent ?? el.value ?? '').trim();
|
|
53
|
+
return /^(authorize|connect)$/i.test(text) && !el.disabled;
|
|
54
|
+
});
|
|
55
|
+
if (match) {
|
|
56
|
+
const descriptor = {
|
|
57
|
+
tag: match.tagName.toLowerCase(),
|
|
58
|
+
text: (match.textContent ?? match.value ?? '').trim().slice(0, 40),
|
|
59
|
+
disabled: Boolean(match.disabled),
|
|
60
|
+
};
|
|
61
|
+
match.click();
|
|
62
|
+
return { kind: 'button', descriptor };
|
|
63
|
+
}
|
|
64
|
+
// textContent over innerText: jsdom's innerText is partial; the dashboard's
|
|
65
|
+
// success-modal text is plain DOM content (not visibility-gated by CSS for
|
|
66
|
+
// anyone reading the page) so textContent finds it on both runtimes.
|
|
67
|
+
const bodyText = doc.body ? doc.body.textContent || '' : '';
|
|
68
|
+
if (bodyText.includes('Cloudflared has installed a certificate')) {
|
|
69
|
+
return { kind: 'success' };
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const MATCH_EXPR = `(${findMatch.toString()})(document)`;
|
|
@@ -1,33 +1,51 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Drive the Cloudflare argotunnel
|
|
2
|
+
// Drive the Cloudflare argotunnel consent page to a deterministic terminal
|
|
3
|
+
// state via CDP WebSocket. Originally Task 588 (Authorize click from code,
|
|
4
|
+
// collapsing a 180 s human-latency window). Task 855 widens the contract to
|
|
5
|
+
// three states because the dashboard does not always render a button — once
|
|
6
|
+
// the bound account has authorized this device's zones, navigating the same
|
|
7
|
+
// (or a fresh) consent URL renders a Success modal verbatim, and the
|
|
8
|
+
// pre-855 helper conflated "no button visible yet" with "no button will
|
|
9
|
+
// ever appear" and exited error.
|
|
3
10
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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
|
+
// Protocol: Chrome DevTools Protocol over WebSocket
|
|
12
|
+
// (https://chromedevtools.github.io/devtools-protocol/):
|
|
11
13
|
// 1. GET http://127.0.0.1:9222/json/list → find the target with matching id;
|
|
12
14
|
// extract webSocketDebuggerUrl.
|
|
13
15
|
// 2. Open WS, enable Page + Runtime domains.
|
|
14
16
|
// 3. Wait for Page.loadEventFired (or observe document.readyState==='complete').
|
|
15
|
-
// 4. Poll Runtime.evaluate every 200 ms for up to ~3 s,
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
17
|
+
// 4. Poll Runtime.evaluate every 200 ms for up to ~3 s, evaluating the
|
|
18
|
+
// tri-state matcher from _cdp-authorize-matcher.mjs. The matcher
|
|
19
|
+
// returns one of:
|
|
20
|
+
// - { kind:'button', descriptor } — Authorize/Connect button found,
|
|
21
|
+
// click() fired in-page; emit
|
|
22
|
+
// reason=clicked, exit 0.
|
|
23
|
+
// - { kind:'success' } — Success modal text detected
|
|
24
|
+
// ("Cloudflared has installed a
|
|
25
|
+
// certificate"); cert is bound to
|
|
26
|
+
// the account. Emit reason=
|
|
27
|
+
// cert-already-installed, exit 0.
|
|
28
|
+
// Caller's existing cert-poll picks
|
|
29
|
+
// up the cert (callback is
|
|
30
|
+
// idempotent on this navigation).
|
|
31
|
+
// - null — Neither anchor matched yet; loop.
|
|
32
|
+
// After BUTTON_POLL_TIMEOUT_MS with no terminal match, emit
|
|
33
|
+
// reason=authorize-button-not-found, exit 1.
|
|
19
34
|
//
|
|
20
35
|
// Exit codes:
|
|
21
|
-
// 0 - clicked
|
|
22
|
-
// 1 - authorize-button-not-found (
|
|
36
|
+
// 0 - terminal success (reason=clicked OR reason=cert-already-installed)
|
|
37
|
+
// 1 - authorize-button-not-found (loud failure — button AND modal absent)
|
|
23
38
|
// 2 - cdp-ws-unreachable or node-websocket-unavailable
|
|
24
39
|
// 3 - target-not-found (the target_id isn't in /json/list)
|
|
25
40
|
// 4 - click-evaluate-threw (Runtime.evaluate returned wasThrown=true)
|
|
26
41
|
// 5 - protocol-error (malformed CDP response)
|
|
27
42
|
//
|
|
28
|
-
//
|
|
29
|
-
// shape so setup-tunnel.sh's tee_subprocess picks it up into the stream log:
|
|
43
|
+
// Stdout contract (read by setup-tunnel.sh's awk-based reason parser):
|
|
30
44
|
// cdp-authorize result=<ok|error> reason=<…> elapsed_ms=<…> [detail=<…>]
|
|
45
|
+
// Exactly ONE such line per invocation; the wrapper takes the LAST
|
|
46
|
+
// `result=ok` line via awk so future debug-line additions cannot mis-route.
|
|
47
|
+
|
|
48
|
+
import { MATCH_EXPR } from './_cdp-authorize-matcher.mjs';
|
|
31
49
|
|
|
32
50
|
const CDP_HOST = '127.0.0.1';
|
|
33
51
|
const CDP_PORT = 9222;
|
|
@@ -211,35 +229,16 @@ try {
|
|
|
211
229
|
die(1, 'page-load-timeout', { detail: err.message, elapsed_ms: Date.now() - started });
|
|
212
230
|
}
|
|
213
231
|
|
|
214
|
-
// Poll
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
|
|
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) {
|
|
232
|
+
// Poll the consent page until one of the three terminal states resolves.
|
|
233
|
+
// MATCH_EXPR is built from _cdp-authorize-matcher.mjs's findMatch so the
|
|
234
|
+
// JSDOM unit tests and the live CDP path execute literally identical logic.
|
|
235
|
+
const matchDeadline = Date.now() + BUTTON_POLL_TIMEOUT_MS;
|
|
236
|
+
let matched = null;
|
|
237
|
+
while (Date.now() < matchDeadline) {
|
|
239
238
|
let r;
|
|
240
239
|
try {
|
|
241
240
|
r = await send('Runtime.evaluate', {
|
|
242
|
-
expression:
|
|
241
|
+
expression: MATCH_EXPR,
|
|
243
242
|
returnByValue: true,
|
|
244
243
|
awaitPromise: false,
|
|
245
244
|
});
|
|
@@ -253,22 +252,33 @@ while (Date.now() < clickDeadline) {
|
|
|
253
252
|
});
|
|
254
253
|
}
|
|
255
254
|
const val = r?.result?.value;
|
|
256
|
-
if (val && typeof val === 'object') {
|
|
257
|
-
|
|
255
|
+
if (val && typeof val === 'object' && (val.kind === 'button' || val.kind === 'success')) {
|
|
256
|
+
matched = val;
|
|
258
257
|
break;
|
|
259
258
|
}
|
|
260
259
|
await new Promise((res) => setTimeout(res, BUTTON_POLL_INTERVAL_MS));
|
|
261
260
|
}
|
|
262
261
|
|
|
263
|
-
if (!
|
|
262
|
+
if (!matched) {
|
|
264
263
|
die(1, 'authorize-button-not-found', { elapsed_ms: Date.now() - started });
|
|
265
264
|
}
|
|
266
265
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
266
|
+
if (matched.kind === 'button') {
|
|
267
|
+
const d = matched.descriptor ?? {};
|
|
268
|
+
emit('ok', 'clicked', {
|
|
269
|
+
tag: d.tag,
|
|
270
|
+
text: d.text,
|
|
271
|
+
elapsed_ms: Date.now() - started,
|
|
272
|
+
});
|
|
273
|
+
} else {
|
|
274
|
+
// matched.kind === 'success' — Success modal text is on the page; the
|
|
275
|
+
// bound account already has a cert. Caller drops into the cert-poll loop;
|
|
276
|
+
// the in-flight cloudflared subprocess receives the idempotent callback
|
|
277
|
+
// and writes ~/.cloudflared/cert.pem.
|
|
278
|
+
emit('ok', 'cert-already-installed', {
|
|
279
|
+
elapsed_ms: Date.now() - started,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
272
282
|
|
|
273
283
|
try { ws.close(); } catch { /* best-effort */ }
|
|
274
284
|
process.exit(0);
|
|
@@ -60,22 +60,74 @@ CFG_DIR="${HOME}/.${BRAND}/cloudflared"
|
|
|
60
60
|
mkdir -p "${CFG_DIR}"
|
|
61
61
|
|
|
62
62
|
# --------------------------------------------------------------------------
|
|
63
|
-
# Step 1: OAuth login
|
|
63
|
+
# Step 1: OAuth login. Corresponds to runbook Step 1.
|
|
64
64
|
#
|
|
65
|
-
#
|
|
65
|
+
# Step 1 is a state machine over three observable variables — brand-scoped
|
|
66
|
+
# cert path, default cert path, and the helper's outcome (Task 855):
|
|
67
|
+
#
|
|
68
|
+
# ${CFG_DIR}/cert.pem present → Step 1 already done; skip.
|
|
69
|
+
# ${CFG_DIR}/cert.pem missing AND
|
|
70
|
+
# ~/.cloudflared/cert.pem present → pre-flight: a prior run
|
|
71
|
+
# reached the OAuth callback
|
|
72
|
+
# but failed before the mv;
|
|
73
|
+
# promote and skip the
|
|
74
|
+
# helper. Idempotent.
|
|
75
|
+
# both missing → spawn cloudflared, drive
|
|
76
|
+
# the consent page via CDP,
|
|
77
|
+
# poll for cert.pem, mv.
|
|
78
|
+
#
|
|
79
|
+
# Control flow when both certs are missing (Task 556 / 588 / 855):
|
|
66
80
|
# 1. CDP precheck on 127.0.0.1:9222 — loud failure if Chromium isn't up.
|
|
67
81
|
# 2. Spawn cloudflared with stdout+stderr teed line-by-line to
|
|
68
82
|
# $STREAM_LOG_PATH with prefix [script:setup-tunnel:cloudflared]
|
|
69
83
|
# (Task 605's chat-surface namespace — see _stream-log.sh header).
|
|
70
84
|
# 3. Extract the authorize URL with a tolerant regex as it streams.
|
|
71
85
|
# 4. Drive the VNC Chromium to that URL via CDP `PUT /json/new?<url>`.
|
|
72
|
-
# 5.
|
|
73
|
-
#
|
|
86
|
+
# 5. Run _cdp-authorize.mjs — tri-state matcher returns one of:
|
|
87
|
+
# reason=clicked → button matched + clicked
|
|
88
|
+
# reason=cert-already-installed → Success modal detected
|
|
89
|
+
# reason=authorize-button-not-found → loud failure, exit 1
|
|
90
|
+
# (a)+(b) drop into the cert-pem poll; (c) bubbles up.
|
|
91
|
+
# 6. Wait for ~/.cloudflared/cert.pem to land with bounded timeout.
|
|
92
|
+
# 7. Move cert.pem into the brand-scoped path.
|
|
74
93
|
#
|
|
75
|
-
# Every branch exits 1 loudly naming the
|
|
94
|
+
# Every failure branch exits 1 loudly naming the cause — no silent retries,
|
|
76
95
|
# no xdg-open race, no cloudflared-internal browser-spawn path.
|
|
77
96
|
# --------------------------------------------------------------------------
|
|
78
97
|
|
|
98
|
+
# Pre-flight cert-promotion (Task 855). When ${CFG_DIR}/cert.pem is missing
|
|
99
|
+
# but the OAuth-default ~/.cloudflared/cert.pem is present, a prior run
|
|
100
|
+
# reached the cloudflared callback but did not survive to the brand-scoped
|
|
101
|
+
# mv at the end of Step 1. The cert is bound to the account already; the
|
|
102
|
+
# only remediation is the move. Doing it before re-spawning cloudflared is
|
|
103
|
+
# what stops Step 1 from looping forever on a dashboard that has moved past
|
|
104
|
+
# a button-bearing state.
|
|
105
|
+
#
|
|
106
|
+
# The mv exit code is checked: if it fails (EACCES, ENOSPC, weird FS state),
|
|
107
|
+
# loud-fail with reason=cert-promote-failed instead of pretending Step 1
|
|
108
|
+
# succeeded — the next steps would die opaquely on the missing brand cert.
|
|
109
|
+
# stderr is captured into the phase_line so the operator sees the actual
|
|
110
|
+
# failure cause (mv exit-code 1 alone cannot disambiguate EACCES from ENOSPC).
|
|
111
|
+
if [ ! -f "${CFG_DIR}/cert.pem" ] && [ -f "${HOME}/.cloudflared/cert.pem" ]; then
|
|
112
|
+
MV_ERR="$(mktemp -t maxy-cert-promote-err.XXXXXX)"
|
|
113
|
+
if mv "${HOME}/.cloudflared/cert.pem" "${CFG_DIR}/cert.pem" 2>"${MV_ERR}"; then
|
|
114
|
+
rm -f "${MV_ERR}"
|
|
115
|
+
phase_line setup-tunnel step=oauth-login result=ok \
|
|
116
|
+
reason=cert-promoted-from-default-path waited="0s"
|
|
117
|
+
else
|
|
118
|
+
MV_RC=$?
|
|
119
|
+
MV_STDERR="$(tr '\n' ' ' < "${MV_ERR}" 2>/dev/null | head -c 200 || echo unavailable)"
|
|
120
|
+
rm -f "${MV_ERR}"
|
|
121
|
+
phase_line setup-tunnel step=oauth-login result=error \
|
|
122
|
+
reason=cert-promote-failed mv_rc="${MV_RC}" stderr="${MV_STDERR}" \
|
|
123
|
+
from="${HOME}/.cloudflared/cert.pem" to="${CFG_DIR}/cert.pem"
|
|
124
|
+
echo "ERROR: failed to promote ~/.cloudflared/cert.pem to ${CFG_DIR}/cert.pem (mv exit=${MV_RC})." >&2
|
|
125
|
+
echo " mv stderr: ${MV_STDERR}" >&2
|
|
126
|
+
echo " Step 1 cannot proceed without the cert in the brand-scoped path." >&2
|
|
127
|
+
exit 1
|
|
128
|
+
fi
|
|
129
|
+
fi
|
|
130
|
+
|
|
79
131
|
if [ ! -f "${CFG_DIR}/cert.pem" ]; then
|
|
80
132
|
phase_line setup-tunnel step=oauth-login cert_path="${CFG_DIR}/cert.pem" display="${DISPLAY:-:99}"
|
|
81
133
|
|
|
@@ -215,22 +267,61 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
|
|
|
215
267
|
echo "ERROR: _cdp-authorize.mjs helper missing or not executable at ${AUTH_HELPER}" >&2
|
|
216
268
|
exit 1
|
|
217
269
|
fi
|
|
218
|
-
#
|
|
219
|
-
# stream log under the setup-tunnel:cdp-click tag
|
|
220
|
-
#
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
270
|
+
# tee_subprocess_capture pipes the helper's `cdp-authorize result=…` line
|
|
271
|
+
# into the stream log under the setup-tunnel:cdp-click tag (live-tailable)
|
|
272
|
+
# AND through this shell's stdout so the wrapper can capture it for the
|
|
273
|
+
# tri-state reason discriminator (Task 855). The helper writes exactly
|
|
274
|
+
# one such line per invocation and exits; the shape is its contract:
|
|
275
|
+
# `cdp-authorize result=<ok|error> reason=<token> [k=v …]`.
|
|
276
|
+
# The wrapper's awk takes the LAST `cdp-authorize result=ok ` line so that
|
|
277
|
+
# any future emit() additions cannot mis-route the discriminator — the
|
|
278
|
+
# success line is always the last one before exit.
|
|
279
|
+
HELPER_OUT="$(mktemp -t maxy-cdp-authorize-out.XXXXXX)"
|
|
280
|
+
if tee_subprocess_capture setup-tunnel:cdp-click -- \
|
|
281
|
+
node "${AUTH_HELPER}" "${CDP_TARGET_ID}" \
|
|
282
|
+
> "${HELPER_OUT}"; then
|
|
283
|
+
HELPER_REASON="$(awk '/^cdp-authorize result=ok / {m=$0} END {if (m) {match(m, /reason=[a-z0-9-]+/); print substr(m, RSTART+7, RLENGTH-7)}}' "${HELPER_OUT}")"
|
|
284
|
+
rm -f "${HELPER_OUT}"
|
|
285
|
+
case "${HELPER_REASON}" in
|
|
286
|
+
clicked)
|
|
287
|
+
phase_line setup-tunnel step=browser-drive result=ok \
|
|
288
|
+
reason=clicked target_id="${CDP_TARGET_ID}"
|
|
289
|
+
;;
|
|
290
|
+
cert-already-installed)
|
|
291
|
+
# The dashboard rendered the Success modal — the bound account
|
|
292
|
+
# already has a cert. The OAuth callback URL was exercised by this
|
|
293
|
+
# navigation, so the in-flight cloudflared subprocess receives the
|
|
294
|
+
# idempotent callback and writes ~/.cloudflared/cert.pem; the poll
|
|
295
|
+
# below picks it up.
|
|
296
|
+
phase_line setup-tunnel step=browser-drive result=ok \
|
|
297
|
+
reason=cert-already-installed target_id="${CDP_TARGET_ID}"
|
|
298
|
+
;;
|
|
299
|
+
*)
|
|
300
|
+
# Helper exited 0 but emitted a reason this wrapper does not
|
|
301
|
+
# recognise — protocol drift between helper and wrapper. Loud-fail
|
|
302
|
+
# rather than silently fall through to the cert-poll, which would
|
|
303
|
+
# mask the cause if the cert never lands.
|
|
304
|
+
kill "${CF_PIPELINE_PID}" 2>/dev/null || true
|
|
305
|
+
phase_line setup-tunnel step=browser-drive result=error \
|
|
306
|
+
reason=helper-unknown-success-reason raw="${HELPER_REASON:-empty}"
|
|
307
|
+
echo "ERROR: CDP helper exited 0 with unrecognised reason: ${HELPER_REASON:-empty}" >&2
|
|
308
|
+
echo " Expected reason=clicked or reason=cert-already-installed." >&2
|
|
309
|
+
echo " Helper / wrapper protocol drift — investigate stream log." >&2
|
|
310
|
+
exit 1
|
|
311
|
+
;;
|
|
312
|
+
esac
|
|
225
313
|
else
|
|
226
314
|
CLICK_RC=$?
|
|
315
|
+
rm -f "${HELPER_OUT}"
|
|
227
316
|
kill "${CF_PIPELINE_PID}" 2>/dev/null || true
|
|
228
|
-
# Helper exit codes:
|
|
229
|
-
#
|
|
230
|
-
#
|
|
231
|
-
#
|
|
232
|
-
#
|
|
233
|
-
#
|
|
317
|
+
# Helper exit codes (mapped 1:1 to reason= so stream-log grep is precise;
|
|
318
|
+
# a catch-all reason would defeat the observability contract):
|
|
319
|
+
# 1 = authorize-button-not-found (loud — neither button nor success
|
|
320
|
+
# modal matched before BUTTON_POLL_TIMEOUT_MS; the form surfaces
|
|
321
|
+
# a verbatim quote of references/dashboard-guide.md "Authorise a
|
|
322
|
+
# new tunnel" so the operator can recover without SSH).
|
|
323
|
+
# 2 = cdp-ws-unreachable, 3 = target-not-found, 4 = click-evaluate-threw,
|
|
324
|
+
# 5 = protocol-error. All are final — no retry, no silent fallback.
|
|
234
325
|
case "${CLICK_RC}" in
|
|
235
326
|
1) CLICK_REASON=authorize-button-not-found ;;
|
|
236
327
|
2) CLICK_REASON=cdp-ws-unreachable ;;
|
|
@@ -242,8 +333,13 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
|
|
|
242
333
|
phase_line setup-tunnel step=browser-drive result=error \
|
|
243
334
|
reason="${CLICK_REASON}" click_rc="${CLICK_RC}"
|
|
244
335
|
echo "ERROR: CDP-driven Authorize click failed (helper exit=${CLICK_RC}, reason=${CLICK_REASON})." >&2
|
|
245
|
-
|
|
246
|
-
|
|
336
|
+
if [ "${CLICK_REASON}" = "authorize-button-not-found" ]; then
|
|
337
|
+
echo " The dashboard did not present an Authorize button or a Success modal." >&2
|
|
338
|
+
echo " The form will surface the dashboard click-path for manual recovery." >&2
|
|
339
|
+
else
|
|
340
|
+
echo " The CDP helper failed before reaching a terminal page state." >&2
|
|
341
|
+
echo " Re-run setup — or run ~/reset-tunnel.sh first if state is corrupt." >&2
|
|
342
|
+
fi
|
|
247
343
|
exit 1
|
|
248
344
|
fi
|
|
249
345
|
fi # end CDP-available branch (Task 664)
|
|
@@ -281,7 +377,7 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
|
|
|
281
377
|
echo "ERROR: Timed out after ${LOGIN_WAIT}s waiting for cert.pem to land." >&2
|
|
282
378
|
exit 1
|
|
283
379
|
fi
|
|
284
|
-
# Heartbeat every 2 s after the click. t=0 is the
|
|
380
|
+
# Heartbeat every 2 s after the click. t=0 is the browser-drive
|
|
285
381
|
# phase line above; the first heartbeat fires at t=2. Without this line
|
|
286
382
|
# the tailer sees silence for the full 1-20 s round-trip — the exact
|
|
287
383
|
# state Task 588 forbids.
|
|
@@ -294,7 +390,7 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
|
|
|
294
390
|
done
|
|
295
391
|
|
|
296
392
|
mv "${HOME}/.cloudflared/cert.pem" "${CFG_DIR}/cert.pem"
|
|
297
|
-
phase_line setup-tunnel step=oauth-login result=
|
|
393
|
+
phase_line setup-tunnel step=oauth-login result=ok \
|
|
298
394
|
path="${CFG_DIR}/cert.pem" waited="${LOGIN_WAIT}s"
|
|
299
395
|
fi
|
|
300
396
|
|
|
@@ -22,6 +22,8 @@ Any Cloudflare action outside these four surfaces is a discipline violation —
|
|
|
22
22
|
|
|
23
23
|
Use this when the operator wants Cloudflare set up (or re-set up) end-to-end on the device. The script handles OAuth login, tunnel creation, DNS routing for each subdomain, config.yml + tunnel.state, and dispatches the `${BRAND}.service` restart to a transient `systemd-run` unit (Task 558) — all in one invocation. 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. Post-restart hostname verification is out of scope for the script (connector is not up when the script exits) — verify via the next admin turn or manually with `curl -I https://<hostname>`. Apex hostnames cannot be routed by the CLI; when one is passed, the script prints an `ACTION REQUIRED` block naming the exact dashboard record to edit.
|
|
24
24
|
|
|
25
|
+
Step 1's OAuth flow is a state machine over three observable variables: the brand-scoped cert path (`${CFG_DIR}/cert.pem`), the OAuth-default cert path (`~/.cloudflared/cert.pem`), and the consent-page DOM via the CDP helper `_cdp-authorize.mjs` (Task 855). When the brand-scoped cert is missing but the default-path cert is present from any prior partial run, the wrapper promotes it (`mv`) and emits `step=oauth-login result=ok reason=cert-promoted-from-default-path` without re-spawning cloudflared. When both are missing, the helper polls `dash.cloudflare.com/argotunnel` and resolves to one of three terminal outcomes — `reason=clicked` (button matched and clicked), `reason=cert-already-installed` (Success modal text detected — the bound account already has a cert, callback is idempotent so the in-flight cloudflared subprocess writes the cert anyway), or `reason=authorize-button-not-found` (neither anchor matched; loud failure, exit 1). The first two paths drop into the existing brand-scoped cert poll; the third surfaces a remediation card in the form so the operator can complete the consent click in their own browser without SSH or `~/reset-tunnel.sh`.
|
|
26
|
+
|
|
25
27
|
### How inputs reach the script
|
|
26
28
|
|
|
27
29
|
Inputs arrive through the `cloudflare-setup-form` component, not agent Q&A. The onboarding skill renders the form, the user submits admin/public/apex labels and the admin password in one action, and the `/api/admin/cloudflare/setup` endpoint runs (in this order):
|
|
@@ -50,6 +52,8 @@ The agent does not invoke the script directly during onboarding — the endpoint
|
|
|
50
52
|
|
|
51
53
|
The endpoint returns `{ ok: false, field: "script", message, output }` and the form surfaces the error inline. Relay the output to the user, name the exit code, and cite `references/reset-guide.md` for the next action. Offer to re-render the form after any manual steps the script's error output named. Do not attempt a second invocation outside the form, a Playwright-driven dashboard inspection, or an alternative `cloudflared` command sequence. The discipline rule below applies.
|
|
52
54
|
|
|
55
|
+
When the failure reason is `authorize-button-not-found` (Task 855), the form renders a verbatim quote of `references/dashboard-guide.md` § "Authorise a new tunnel (pick the right zone)" alongside a "Try again" button — the operator completes the consent click in their own browser, then re-submits the same form. No agent action is required; relay the form's chat acknowledgement when it arrives. Do not suggest `~/reset-tunnel.sh` for this reason — the cert path is intact and a fresh consent click is the only remediation needed.
|
|
56
|
+
|
|
53
57
|
---
|
|
54
58
|
|
|
55
59
|
## 2. Manual fallback — `references/manual-setup.md`
|