@rubytech/create-maxy 1.0.801 → 1.0.803

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.
@@ -1,74 +0,0 @@
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,284 +0,0 @@
1
- #!/usr/bin/env node
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.
10
- //
11
- // Protocol: Chrome DevTools Protocol over WebSocket
12
- // (https://chromedevtools.github.io/devtools-protocol/):
13
- // 1. GET http://127.0.0.1:9222/json/list → find the target with matching id;
14
- // extract webSocketDebuggerUrl.
15
- // 2. Open WS, enable Page + Runtime domains.
16
- // 3. Wait for Page.loadEventFired (or observe document.readyState==='complete').
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.
34
- //
35
- // Exit codes:
36
- // 0 - terminal success (reason=clicked OR reason=cert-already-installed)
37
- // 1 - authorize-button-not-found (loud failure — button AND modal absent)
38
- // 2 - cdp-ws-unreachable or node-websocket-unavailable
39
- // 3 - target-not-found (the target_id isn't in /json/list)
40
- // 4 - click-evaluate-threw (Runtime.evaluate returned wasThrown=true)
41
- // 5 - protocol-error (malformed CDP response)
42
- //
43
- // Stdout contract (read by setup-tunnel.sh's awk-based reason parser):
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';
49
-
50
- const CDP_HOST = '127.0.0.1';
51
- const CDP_PORT = 9222;
52
- const HTTP_TIMEOUT_MS = 3000;
53
- const BUTTON_POLL_TIMEOUT_MS = 3000;
54
- const BUTTON_POLL_INTERVAL_MS = 200;
55
- const LOAD_WAIT_TIMEOUT_MS = 5000;
56
- const WS_CONNECT_TIMEOUT_MS = 3000;
57
- const RESULT_WAIT_TIMEOUT_MS = 3000;
58
-
59
- function emit(result, reason, extra = {}) {
60
- const parts = [`cdp-authorize result=${result} reason=${reason}`];
61
- for (const [k, v] of Object.entries(extra)) {
62
- if (v === undefined || v === null) continue;
63
- const s = String(v).replace(/\s+/g, ' ');
64
- parts.push(`${k}="${s.slice(0, 200)}"`);
65
- }
66
- process.stdout.write(parts.join(' ') + '\n');
67
- }
68
-
69
- function die(code, reason, extra) {
70
- emit('error', reason, extra);
71
- process.exit(code);
72
- }
73
-
74
- if (typeof WebSocket === 'undefined') {
75
- die(2, 'node-websocket-unavailable', {
76
- detail: `Node ${process.version} has no global WebSocket — upgrade to Node 22+`,
77
- });
78
- }
79
-
80
- const targetId = process.argv[2];
81
- if (!targetId) {
82
- die(2, 'missing-target-id', { detail: 'usage: _cdp-authorize.mjs <target-id>' });
83
- }
84
-
85
- const started = Date.now();
86
-
87
- async function fetchTargets() {
88
- const ac = new AbortController();
89
- const timer = setTimeout(() => ac.abort(), HTTP_TIMEOUT_MS);
90
- try {
91
- const res = await fetch(`http://${CDP_HOST}:${CDP_PORT}/json/list`, { signal: ac.signal });
92
- if (!res.ok) throw new Error(`/json/list returned ${res.status}`);
93
- return await res.json();
94
- } finally {
95
- clearTimeout(timer);
96
- }
97
- }
98
-
99
- let targets;
100
- try {
101
- targets = await fetchTargets();
102
- } catch (err) {
103
- die(2, 'cdp-ws-unreachable', { detail: err instanceof Error ? err.message : String(err) });
104
- }
105
-
106
- const target = Array.isArray(targets) ? targets.find((t) => t && t.id === targetId) : undefined;
107
- if (!target || typeof target.webSocketDebuggerUrl !== 'string') {
108
- die(3, 'target-not-found', { target_id: targetId, count: Array.isArray(targets) ? targets.length : -1 });
109
- }
110
-
111
- // Open WebSocket. Native Node 22 global.
112
- let ws;
113
- try {
114
- ws = new WebSocket(target.webSocketDebuggerUrl);
115
- } catch (err) {
116
- die(2, 'cdp-ws-unreachable', { detail: err instanceof Error ? err.message : String(err) });
117
- }
118
-
119
- // WS open with race guards. Three outcomes are possible:
120
- // 1. `open` fires before `error` / timeout → resolve and continue.
121
- // 2. `error` fires before `open` → reject; caller emits die() and exits.
122
- // 3. Timeout fires before either → reject; caller emits die() and exits.
123
- // The `.once` on `open`/`error` listeners handles the race where both fire
124
- // in quick succession (some proxy-protocol mismatches can emit `error`
125
- // before the open event is dispatched). The catch below awaits the die()
126
- // call and then `throw`s to guarantee execution does not fall through to
127
- // the Page.enable call with a dead socket — even if a future refactor
128
- // removes process.exit() from die().
129
- try {
130
- await new Promise((resolveOpen, rejectOpen) => {
131
- const timer = setTimeout(() => rejectOpen(new Error('ws open timeout')), WS_CONNECT_TIMEOUT_MS);
132
- ws.addEventListener('open', () => { clearTimeout(timer); resolveOpen(); }, { once: true });
133
- ws.addEventListener('error', (e) => { clearTimeout(timer); rejectOpen(new Error(`ws error: ${e.message ?? 'unknown'}`)); }, { once: true });
134
- });
135
- } catch (err) {
136
- die(2, 'cdp-ws-unreachable', { detail: err instanceof Error ? err.message : String(err) });
137
- throw err; // unreachable — die() exits, but guards the type system and future refactors.
138
- }
139
-
140
- // Message router: CDP replies come back keyed by the `id` we sent. Events
141
- // arrive with no `id` but a `method` (e.g. "Page.loadEventFired").
142
- let nextId = 1;
143
- const pending = new Map();
144
- const eventListeners = new Map();
145
-
146
- ws.addEventListener('message', (evt) => {
147
- let msg;
148
- try {
149
- msg = JSON.parse(typeof evt.data === 'string' ? evt.data : evt.data.toString());
150
- } catch {
151
- return;
152
- }
153
- if (typeof msg.id === 'number' && pending.has(msg.id)) {
154
- const { resolveIt, rejectIt } = pending.get(msg.id);
155
- pending.delete(msg.id);
156
- if (msg.error) rejectIt(new Error(`CDP error: ${msg.error.message ?? JSON.stringify(msg.error)}`));
157
- else resolveIt(msg.result);
158
- } else if (typeof msg.method === 'string') {
159
- const listeners = eventListeners.get(msg.method);
160
- if (listeners) for (const l of listeners) l(msg.params);
161
- }
162
- });
163
-
164
- ws.addEventListener('close', () => {
165
- for (const { rejectIt } of pending.values()) {
166
- rejectIt(new Error('ws closed while awaiting response'));
167
- }
168
- pending.clear();
169
- });
170
-
171
- function send(method, params = {}) {
172
- const id = nextId++;
173
- return new Promise((resolveIt, rejectIt) => {
174
- const timer = setTimeout(() => {
175
- pending.delete(id);
176
- rejectIt(new Error(`CDP ${method} timed out after ${RESULT_WAIT_TIMEOUT_MS}ms`));
177
- }, RESULT_WAIT_TIMEOUT_MS);
178
- pending.set(id, {
179
- resolveIt: (r) => { clearTimeout(timer); resolveIt(r); },
180
- rejectIt: (e) => { clearTimeout(timer); rejectIt(e); },
181
- });
182
- ws.send(JSON.stringify({ id, method, params }));
183
- });
184
- }
185
-
186
- function onEvent(method, listener) {
187
- let set = eventListeners.get(method);
188
- if (!set) { set = new Set(); eventListeners.set(method, set); }
189
- set.add(listener);
190
- return () => set.delete(listener);
191
- }
192
-
193
- // Enable the two domains we need. Page.enable fires events; Runtime.enable
194
- // lets us run expressions in the page context.
195
- try {
196
- await send('Page.enable');
197
- await send('Runtime.enable');
198
- } catch (err) {
199
- die(5, 'protocol-error', { detail: err.message });
200
- }
201
-
202
- // Wait for load — if the page is already loaded (navigated before this helper
203
- // ran), Page.loadEventFired may never fire again; poll document.readyState
204
- // as a fallback. The first of the two to resolve wins.
205
- async function waitForLoad() {
206
- const loadFired = new Promise((resolveIt) => {
207
- const off = onEvent('Page.loadEventFired', () => { off(); resolveIt('loadEvent'); });
208
- });
209
- const readyStatePoll = (async () => {
210
- const deadline = Date.now() + LOAD_WAIT_TIMEOUT_MS;
211
- while (Date.now() < deadline) {
212
- try {
213
- const r = await send('Runtime.evaluate', {
214
- expression: 'document.readyState',
215
- returnByValue: true,
216
- });
217
- if (r?.result?.value === 'complete' || r?.result?.value === 'interactive') return 'readyState';
218
- } catch { /* keep polling */ }
219
- await new Promise((res) => setTimeout(res, 200));
220
- }
221
- throw new Error('load-wait-timeout');
222
- })();
223
- return Promise.race([loadFired, readyStatePoll]);
224
- }
225
-
226
- try {
227
- await waitForLoad();
228
- } catch (err) {
229
- die(1, 'page-load-timeout', { detail: err.message, elapsed_ms: Date.now() - started });
230
- }
231
-
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) {
238
- let r;
239
- try {
240
- r = await send('Runtime.evaluate', {
241
- expression: MATCH_EXPR,
242
- returnByValue: true,
243
- awaitPromise: false,
244
- });
245
- } catch (err) {
246
- die(5, 'protocol-error', { detail: err.message });
247
- }
248
- if (r?.exceptionDetails) {
249
- die(4, 'click-evaluate-threw', {
250
- detail: r.exceptionDetails.text ?? 'unknown exception',
251
- elapsed_ms: Date.now() - started,
252
- });
253
- }
254
- const val = r?.result?.value;
255
- if (val && typeof val === 'object' && (val.kind === 'button' || val.kind === 'success')) {
256
- matched = val;
257
- break;
258
- }
259
- await new Promise((res) => setTimeout(res, BUTTON_POLL_INTERVAL_MS));
260
- }
261
-
262
- if (!matched) {
263
- die(1, 'authorize-button-not-found', { elapsed_ms: Date.now() - started });
264
- }
265
-
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
- }
282
-
283
- try { ws.close(); } catch { /* best-effort */ }
284
- process.exit(0);