@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.
- package/package.json +1 -1
- package/payload/platform/plugins/cloudflare/PLUGIN.md +1 -2
- package/payload/platform/plugins/cloudflare/references/manual-setup.md +7 -5
- package/payload/platform/plugins/cloudflare/scripts/setup-tunnel.sh +65 -186
- package/payload/platform/plugins/cloudflare/skills/setup-tunnel/SKILL.md +2 -2
- package/payload/platform/plugins/whatsapp/PLUGIN.md +4 -2
- package/payload/server/chunk-3SCIVS2T.js +9910 -0
- package/payload/server/chunk-5ABJJQ5K.js +3465 -0
- package/payload/server/client-pool-S4UZCYDJ.js +29 -0
- package/payload/server/maxy-edge.js +2 -2
- package/payload/server/public/assets/{admin-Sa301b8q.js → admin-B41Gr-FL.js} +1 -1
- package/payload/server/public/index.html +1 -1
- package/payload/server/server.js +376 -176
- package/payload/platform/plugins/cloudflare/scripts/_cdp-authorize-matcher.mjs +0 -74
- package/payload/platform/plugins/cloudflare/scripts/_cdp-authorize.mjs +0 -284
|
@@ -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);
|