@deeeed/metamask-harness 0.70.1 → 0.72.0
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/CHANGELOG.md +25 -0
- package/adapters/extension/console-tail.mjs +38 -6
- package/adapters/manifest.json +128 -0
- package/adapters/terminal/cleanup.mjs +68 -0
- package/adapters/terminal/inject.mjs +74 -0
- package/adapters/terminal/launch.mjs +628 -0
- package/adapters/terminal/lib/cdp-client.mjs +84 -0
- package/adapters/terminal/lib/display.mjs +115 -0
- package/adapters/terminal/lib/hud.mjs +148 -0
- package/adapters/terminal/lib/mainnet-guard.mjs +177 -0
- package/adapters/terminal/lib/origin.mjs +30 -0
- package/adapters/terminal/lib/page-script.mjs +246 -0
- package/adapters/terminal/lib/processes.mjs +187 -0
- package/adapters/terminal/lib/readiness.mjs +274 -0
- package/adapters/terminal/lib/strict-wallet.mjs +138 -0
- package/adapters/terminal/stop.mjs +37 -0
- package/adapters/terminal/verify.mjs +48 -0
- package/adapters/terminal/wallet-host.mjs +703 -0
- package/dist/adapters/console-capture.js +122 -0
- package/dist/adapters/extension/console-capture.js +22 -85
- package/dist/adapters/mobile/verify.js +8 -3
- package/dist/adapters/surface.js +3 -1
- package/dist/adapters/terminal/console-capture.js +46 -0
- package/dist/adapters/terminal/stop.js +19 -0
- package/dist/adapters/terminal/surface.js +104 -0
- package/dist/adapters.js +27 -4
- package/dist/cli.js +2 -2
- package/dist/command-contract.js +12 -4
- package/dist/commands/check.js +1 -1
- package/dist/commands/config.js +1 -1
- package/dist/commands/doctor.js +24 -1
- package/dist/commands/domain.js +1 -1
- package/dist/commands/farmslot-ready.js +2 -0
- package/dist/commands/help.js +2 -2
- package/dist/commands/launch/index.js +97 -2
- package/dist/commands/manifest.js +3 -3
- package/dist/commands/recipe-advice.js +1 -1
- package/dist/commands/review.js +1 -1
- package/dist/commands/run-engine.js +1 -1
- package/dist/commands/shared.js +1 -1
- package/dist/doctor.js +3 -0
- package/dist/harness.js +17 -11
- package/dist/manifest.js +5 -0
- package/dist/mm-harness-cli.js +34 -20
- package/dist/paths.js +2 -2
- package/dist/recipe-security.js +12 -3
- package/dist/review/knowledge.js +1 -1
- package/dist/run-diagnostics.js +150 -33
- package/dist/runner.js +1 -1
- package/dist/runtime-context.js +3 -3
- package/docs/CONTRIBUTING.md +1 -1
- package/docs/cli-contract.md +163 -0
- package/library/actions/shared/console-allowlist-add.mjs +137 -0
- package/library/actions/shared/console-collector.mjs +230 -0
- package/library/actions/shared/console-findings.mjs +511 -0
- package/library/actions/terminal/app/assert_no_console_errors.mjs +119 -0
- package/library/actions/terminal/app/launch.mjs +45 -0
- package/library/actions/terminal/perps/_venue.mjs +242 -0
- package/library/actions/terminal/perps/assert_orders.mjs +54 -0
- package/library/actions/terminal/perps/assert_positions.mjs +40 -0
- package/library/actions/terminal/perps/read_orders.mjs +26 -0
- package/library/actions/terminal/perps/read_positions.mjs +25 -0
- package/library/actions/terminal/perps/read_snapshot.mjs +40 -0
- package/library/actions/terminal/perps/revoke_agent.mjs +58 -0
- package/library/actions/terminal/perps/teardown_state.mjs +104 -0
- package/library/actions/terminal/platform/hud.mjs +71 -0
- package/library/actions/terminal/platform/page.mjs +247 -0
- package/library/actions/terminal/platform/runtime.mjs +200 -0
- package/library/actions/terminal/ui/navigate.mjs +31 -0
- package/library/actions/terminal/wallet/_signature-log.mjs +177 -0
- package/library/actions/terminal/wallet/assert_signatures.mjs +43 -0
- package/library/actions/terminal/wallet/list_accounts.mjs +27 -0
- package/library/actions/terminal/wallet/read_signatures.mjs +37 -0
- package/library/manifests/terminal.action-manifest.json +1786 -0
- package/package.json +2 -1
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
// The one console matcher: classify application console events and apply a
|
|
2
|
+
// recipe-library allowlist of known noise. Used by run diagnostics
|
|
3
|
+
// (src/run-diagnostics.ts), the assert_no_console_errors action, the
|
|
4
|
+
// allowlist generator, and the recipe library's checks/console-guard.mjs
|
|
5
|
+
// (which imports this file), so every caller judges an event the same way.
|
|
6
|
+
//
|
|
7
|
+
// Captures: console-tail.mjs logs (a "HH:MM:SS [label] [browser:<source>]
|
|
8
|
+
// LEVEL text (at [fn ]location)" record, the browser tag only on browser
|
|
9
|
+
// Log entries, plus "│ "-prefixed continuation lines), Metro or
|
|
10
|
+
// webpack logs (one line per record), and JSONL captures from a CDP watcher
|
|
11
|
+
// ({ kind: "console" | "exception" | "log", level, text, source | url }).
|
|
12
|
+
//
|
|
13
|
+
// Allowlist files: <recipe library>/diagnostics/console-allowlist.json
|
|
14
|
+
// { "schemaVersion": 3,
|
|
15
|
+
// "entries": [{ "signature": "<sig of the first line>", "continuation": "<sig of the rest>",
|
|
16
|
+
// "first_frame": "<sig of the location>", "source": "page|extension|browser|any",
|
|
17
|
+
// "reason": "<why it is noise>", "temporary": true }] }
|
|
18
|
+
// An entry matches one event (a first line and every line that continues it)
|
|
19
|
+
// by string equality of each part, no patterns:
|
|
20
|
+
// signature the first line's text, without its location suffix;
|
|
21
|
+
// continuation every continuation line, trimmed, blank lines dropped,
|
|
22
|
+
// each line's signature joined with "\n" ("" for none);
|
|
23
|
+
// first_frame optional: the event's location (a URL or path, no position).
|
|
24
|
+
//
|
|
25
|
+
// Normalisation is structural: only fields this file parses are changed.
|
|
26
|
+
// URLs (parsed with new URL(); Next.js scope only on a plain path, see
|
|
27
|
+
// isPlainPath): a loopback host's port → <port>; an asset under
|
|
28
|
+
// /_next/static/ → /_next/static/[chunks|media|css/]<chunk>.<ext>; the dpl and v query
|
|
29
|
+
// values on /_next/ paths → <build>; an unpacked extension id →
|
|
30
|
+
// <extension>. Everything else in a URL stays as written.
|
|
31
|
+
// Stack frame lines ("at [fn (]URL or path:line:col[)]") and event
|
|
32
|
+
// locations: the position is dropped and the URL normalised as above.
|
|
33
|
+
// Other text: ISO and clock times → <time>, UUIDs → <uuid>, 0x and 9 or
|
|
34
|
+
// more hex digits → 0x…, nothing else.
|
|
35
|
+
// A matching event is downgraded to level "info" with the reason; it no
|
|
36
|
+
// longer makes a run REVIEW. Entries are written from real captures with
|
|
37
|
+
// console-allowlist-add.mjs.
|
|
38
|
+
|
|
39
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
40
|
+
import path from 'node:path';
|
|
41
|
+
|
|
42
|
+
export const ALLOWLIST_RELATIVE_PATH = path.join('diagnostics', 'console-allowlist.json');
|
|
43
|
+
// Requests the harness itself makes (the launch probe of the mainnet block).
|
|
44
|
+
export const HARNESS_PROBE_MARK = 'mm-harness-probe=1';
|
|
45
|
+
const HARNESS_PROBE_PARAM = 'mm-harness-probe';
|
|
46
|
+
const SOURCES = new Set(['page', 'extension', 'browser', 'any']);
|
|
47
|
+
const SEVERITY = ['warning', 'error', 'exception'];
|
|
48
|
+
|
|
49
|
+
// ---- severity -------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
// The level word console-tail writes (the CDP console type, Log entry level,
|
|
52
|
+
// or EXCEPTION). LOG, INFO, DEBUG and VERBOSE are not findings.
|
|
53
|
+
const LEVEL_WORDS = { WARNING: 'warning', WARN: 'warning', ERROR: 'error', ASSERT: 'error', EXCEPTION: 'exception' };
|
|
54
|
+
// A JSONL capture's console and Log entry levels.
|
|
55
|
+
const JSONL_LEVELS = { warning: 'warning', warn: 'warning', error: 'error', assert: 'error' };
|
|
56
|
+
// Text that makes any event an exception. It only ever raises a level.
|
|
57
|
+
const EXCEPTION_TEXT = /\b(?:Uncaught|UnhandledPromiseRejection|FATAL EXCEPTION)\b/iu;
|
|
58
|
+
|
|
59
|
+
function worst(...levels) {
|
|
60
|
+
return levels.filter(Boolean).reduce((a, b) => (a && SEVERITY.indexOf(a) >= SEVERITY.indexOf(b) ? a : b), null);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// A line with no recorded level (Metro/webpack output, a continuation line):
|
|
64
|
+
// the level its own markers show, or null.
|
|
65
|
+
export function lineLevel(trimmed) {
|
|
66
|
+
if (/\bEXCEPTION\s{2,}/u.test(trimmed) || EXCEPTION_TEXT.test(trimmed)) return 'exception';
|
|
67
|
+
if (/^(?:ERROR\s{2,}|\[error\]\s*)/iu.test(trimmed) || /\]\s+ERROR\s{2,}/u.test(trimmed)) return 'error';
|
|
68
|
+
if (/^(?:WARN(?:ING)?\s{2,}|\[warn(?:ing)?\]\s*)/iu.test(trimmed) || /\]\s+WARN(?:ING)?\s{2,}/u.test(trimmed)) return 'warning';
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---- normalisation --------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]']);
|
|
75
|
+
const BUILD_PARAMS = new Set(['dpl', 'v']);
|
|
76
|
+
const URL_TOKEN = /\b[a-z][\w+.-]*:\/\/[^\s'"()]+/giu;
|
|
77
|
+
|
|
78
|
+
// A path the matcher may read as Next.js structure: every segment, as
|
|
79
|
+
// written, is made of letters, digits, ".", "_", "@", "-" and the escape %40
|
|
80
|
+
// (turbopack writes "@" in chunk names as %40), nothing else, and is never
|
|
81
|
+
// "." or "..". No encoding, at any depth, can then stand for a separator or
|
|
82
|
+
// a dot segment; any other path stays literal.
|
|
83
|
+
const PLAIN_SEGMENT = /^(?:[A-Za-z0-9._@-]|%40)+$/u;
|
|
84
|
+
function isPlainPath(raw) {
|
|
85
|
+
return raw.split('/').slice(1).every((segment) => PLAIN_SEGMENT.test(segment) && segment !== '.' && segment !== '..');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// One URL, changed only in the fields listed at the top of this file; any
|
|
89
|
+
// string new URL() rejects is left as it is.
|
|
90
|
+
export function urlSignature(token) {
|
|
91
|
+
const value = String(token);
|
|
92
|
+
const schemeEnd = value.indexOf('://');
|
|
93
|
+
if (schemeEnd <= 0) return value;
|
|
94
|
+
// Parse with placeholders a signature already has, so a signature is its
|
|
95
|
+
// own signature.
|
|
96
|
+
let url;
|
|
97
|
+
try {
|
|
98
|
+
url = new URL(value.replace(/^([^/?#]*:\/\/[^/?#]*):<port>/u, '$1:1').replace('://<extension>', `://${'a'.repeat(32)}`));
|
|
99
|
+
} catch {
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
const authorityStart = schemeEnd + 3;
|
|
103
|
+
const authorityLength = value.slice(authorityStart).search(/[/?#]/u);
|
|
104
|
+
const authorityEnd = authorityLength < 0 ? value.length : authorityStart + authorityLength;
|
|
105
|
+
let authority = value.slice(authorityStart, authorityEnd);
|
|
106
|
+
let rest = value.slice(authorityEnd);
|
|
107
|
+
const hashAt = rest.indexOf('#');
|
|
108
|
+
const fragment = hashAt < 0 ? '' : rest.slice(hashAt);
|
|
109
|
+
if (hashAt >= 0) rest = rest.slice(0, hashAt);
|
|
110
|
+
const queryAt = rest.indexOf('?');
|
|
111
|
+
let pathname = queryAt < 0 ? rest : rest.slice(0, queryAt);
|
|
112
|
+
let query = queryAt < 0 ? '' : rest.slice(queryAt);
|
|
113
|
+
|
|
114
|
+
// An unpacked extension's id depends on its directory.
|
|
115
|
+
if (url.protocol === 'chrome-extension:' && /^[a-p]{32}$/u.test(authority)) authority = '<extension>';
|
|
116
|
+
// The slot's ports are assigned per slot.
|
|
117
|
+
if (LOOPBACK_HOSTS.has(url.hostname)) authority = authority.replace(/:\d+$/u, ':<port>');
|
|
118
|
+
// Next.js names its build assets per build, and versions them with
|
|
119
|
+
// dpl/v parameters. Only a plain path (isPlainPath) is in scope.
|
|
120
|
+
const nextPath = (prefix) => isPlainPath(pathname) && pathname.startsWith(prefix);
|
|
121
|
+
if (nextPath('/_next/')) {
|
|
122
|
+
query = query && `?${query.slice(1).split('&').map((part) => {
|
|
123
|
+
const equals = part.indexOf('=');
|
|
124
|
+
return equals > 0 && BUILD_PARAMS.has(part.slice(0, equals)) ? `${part.slice(0, equals)}=<build>` : part;
|
|
125
|
+
}).join('&')}`;
|
|
126
|
+
}
|
|
127
|
+
// The asset's name changes per build; its directory (chunks, media, css)
|
|
128
|
+
// and extension say what kind of asset it is, and stay.
|
|
129
|
+
// A name without an extension (or with anything after it, such as a
|
|
130
|
+
// position) stays as written.
|
|
131
|
+
const asset = nextPath('/_next/static/') ? /^(?:(chunks|media|css)\/)?.*\.([A-Za-z0-9]{1,8})$/u.exec(pathname.slice('/_next/static/'.length)) : null;
|
|
132
|
+
if (asset) pathname = `/_next/static/${asset[1] ? `${asset[1]}/` : ''}<chunk>.${asset[2]}`;
|
|
133
|
+
return `${value.slice(0, authorityStart)}${authority}${pathname}${query}${fragment}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Text outside URLs: times, UUIDs and long hex only.
|
|
137
|
+
function plainTextSignature(value) {
|
|
138
|
+
return value
|
|
139
|
+
.replace(/\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?/gu, '<time>')
|
|
140
|
+
.replace(/\b\d{2}:\d{2}:\d{2}(?:\.\d+)?\b/gu, '<time>')
|
|
141
|
+
.replace(/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/giu, '<uuid>')
|
|
142
|
+
// Addresses and hashes; hex up to 8 digits (chain ids, selectors) stays.
|
|
143
|
+
.replace(/(?<![0-9])0x[0-9a-fA-F]{9,}\b/gu, '0x…');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// The signature of one line of message text.
|
|
147
|
+
export function signature(value) {
|
|
148
|
+
const text = String(value ?? '').trim();
|
|
149
|
+
let result = '';
|
|
150
|
+
let last = 0;
|
|
151
|
+
for (const match of text.matchAll(URL_TOKEN)) {
|
|
152
|
+
result += plainTextSignature(text.slice(last, match.index)) + urlSignature(match[0]);
|
|
153
|
+
last = match.index + match[0].length;
|
|
154
|
+
}
|
|
155
|
+
return result + plainTextSignature(text.slice(last));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// A location: a URL or an absolute path, already without its position.
|
|
159
|
+
export function locationSignature(location) {
|
|
160
|
+
const value = String(location ?? '').trim();
|
|
161
|
+
return /^[a-z][\w+.-]*:\/\//iu.test(value) ? urlSignature(value) : value;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// "at fn (URL or path:line:col)" or "at URL or path:line:col".
|
|
165
|
+
const FRAME_LINE = /^at (?:([^\s()][^()]*) \()?((?:[a-z][\w+.-]*:\/\/|\/)\S*?):\d+:\d+(\)?)$/iu;
|
|
166
|
+
// The signature of one continuation line of an event.
|
|
167
|
+
export function continuationLineSignature(line) {
|
|
168
|
+
const trimmed = String(line ?? '').trim();
|
|
169
|
+
const frame = FRAME_LINE.exec(trimmed);
|
|
170
|
+
if (frame && Boolean(frame[1]) === Boolean(frame[3])) {
|
|
171
|
+
const where = locationSignature(frame[2]);
|
|
172
|
+
return frame[1] ? `at ${frame[1]} (${where})` : `at ${where}`;
|
|
173
|
+
}
|
|
174
|
+
return signature(trimmed);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function continuationSignature(lines) {
|
|
178
|
+
return lines.map(continuationLineSignature).join('\n');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---- events ---------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
// The event a record or JSONL entry describes, with the key entries match.
|
|
184
|
+
function makeEvent({ level, source, logSource = null, text, location = '', continuation = [] }) {
|
|
185
|
+
const lines = continuation.map((line) => String(line).trim()).filter(Boolean);
|
|
186
|
+
const raised = worst(level, EXCEPTION_TEXT.test(text) ? 'exception' : null, ...lines.map(lineLevel));
|
|
187
|
+
if (!raised) return null;
|
|
188
|
+
return {
|
|
189
|
+
level: raised,
|
|
190
|
+
source,
|
|
191
|
+
logSource,
|
|
192
|
+
text,
|
|
193
|
+
location,
|
|
194
|
+
continuation: lines.join('\n'),
|
|
195
|
+
key: {
|
|
196
|
+
signature: signature(text),
|
|
197
|
+
continuation: continuationSignature(lines),
|
|
198
|
+
first_frame: locationSignature(location),
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// A frame as console-tail and CDP watchers write it, "fn URL:line:col" or
|
|
204
|
+
// "URL:line[:col] fn": its location without the position, which follows the
|
|
205
|
+
// path (so a port is never one). A bare URL (a Log entry's) is its own
|
|
206
|
+
// location.
|
|
207
|
+
function frameLocation(where) {
|
|
208
|
+
const match = /^(.*?)((?::\d+){1,2})$/u.exec(where);
|
|
209
|
+
if (!match) return where;
|
|
210
|
+
const hasPath = match[1].startsWith('/') || /^[a-z][\w+.-]*:\/\/[^/]*\//iu.test(match[1]);
|
|
211
|
+
return hasPath ? match[1] : where;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const RECORD_START = /^\d{2}:\d{2}:\d{2}(?:\.\d+)?\s+\[[^\]]+\]/u;
|
|
215
|
+
// "HH:MM:SS [label] [browser:<Log source>] LEVEL ": every field before the
|
|
216
|
+
// message comes from the collector; the browser tag only ever here.
|
|
217
|
+
const RECORD_HEAD = /^\d{2}:\d{2}:\d{2}(?:\.\d+)?\s+\[([^\]]+)\]\s+(?:\[browser:([a-z-]+)\]\s+)?(?:([A-Z]+)\s{2,})?/u;
|
|
218
|
+
// The collector's " (at [fn ]location)" suffix: a URL or an absolute path,
|
|
219
|
+
// after a function name when it is a stack frame.
|
|
220
|
+
const LOCATION_SUFFIX = /\s+\(at (?:([^\s()]+) )?((?:[a-z][\w+.-]*:\/\/|\/)\S*)\)$/iu;
|
|
221
|
+
|
|
222
|
+
// The collector (console-tail.mjs) writes every line after an event's first
|
|
223
|
+
// with this prefix, so event boundaries come from the collector, never from
|
|
224
|
+
// message text.
|
|
225
|
+
export const CONTINUATION_PREFIX = '│ ';
|
|
226
|
+
|
|
227
|
+
// Group a log into records: a console-tail line (time + [label]) plus the
|
|
228
|
+
// prefixed continuation lines the collector wrote after it. Every other
|
|
229
|
+
// non-empty line is a record of its own; in a console-tail log, where the
|
|
230
|
+
// collector frames every line, such a line is at least an error (an older
|
|
231
|
+
// collector's continuation, or text that did not come from the collector).
|
|
232
|
+
// Prefixed lines at the start of a slice continue a record before it. A last
|
|
233
|
+
// line without its newline is judged like any other (a reader that cut the
|
|
234
|
+
// capture at a size limit drops the partial line first).
|
|
235
|
+
export function consoleRecords(text) {
|
|
236
|
+
const records = [];
|
|
237
|
+
let current = null;
|
|
238
|
+
let consoleTail = false;
|
|
239
|
+
for (const line of String(text ?? '').split(/\r?\n/u)) {
|
|
240
|
+
if (line.startsWith(CONTINUATION_PREFIX)) {
|
|
241
|
+
const continued = line.slice(CONTINUATION_PREFIX.length).trim();
|
|
242
|
+
if (current && continued) current.continuation.push(continued);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const trimmed = line.trim();
|
|
246
|
+
if (!trimmed) continue;
|
|
247
|
+
const start = RECORD_START.test(trimmed);
|
|
248
|
+
consoleTail ||= start;
|
|
249
|
+
current = { line, continuation: [], start, unframed: !start && consoleTail };
|
|
250
|
+
records.push(current);
|
|
251
|
+
if (!start) current = null;
|
|
252
|
+
}
|
|
253
|
+
return records;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function labelSource(label) {
|
|
257
|
+
if (label.startsWith('dapp:')) return 'page';
|
|
258
|
+
if (label.startsWith('extension:') || label === 'sw' || label.startsWith('page:')) return 'extension';
|
|
259
|
+
return 'app';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// One record → its event (probes included), or null when it is not a finding.
|
|
263
|
+
export function recordEvent(record) {
|
|
264
|
+
const trimmed = String(record.line ?? '').trim();
|
|
265
|
+
const head = record.start === false ? null : RECORD_HEAD.exec(trimmed);
|
|
266
|
+
let level;
|
|
267
|
+
let source;
|
|
268
|
+
let logSource = null;
|
|
269
|
+
let message;
|
|
270
|
+
let raw;
|
|
271
|
+
if (head) {
|
|
272
|
+
level = LEVEL_WORDS[head[3]] ?? null;
|
|
273
|
+
source = head[2] ? 'browser' : labelSource(head[1]);
|
|
274
|
+
logSource = head[2] ?? null;
|
|
275
|
+
message = trimmed.slice(head[0].length).trim();
|
|
276
|
+
raw = message;
|
|
277
|
+
} else {
|
|
278
|
+
// No recorded level: Metro/webpack output, or an unframed line.
|
|
279
|
+
level = worst(lineLevel(trimmed), record.unframed ? 'error' : null);
|
|
280
|
+
source = 'app';
|
|
281
|
+
const marker = /\b(?:EXCEPTION|ERROR|WARN(?:ING)?)\s{2,}/u.exec(trimmed);
|
|
282
|
+
message = (marker ? trimmed.slice(marker.index + marker[0].length) : trimmed).trim();
|
|
283
|
+
raw = message;
|
|
284
|
+
}
|
|
285
|
+
const suffix = LOCATION_SUFFIX.exec(message);
|
|
286
|
+
const event = makeEvent({
|
|
287
|
+
level,
|
|
288
|
+
source,
|
|
289
|
+
logSource,
|
|
290
|
+
text: suffix ? message.slice(0, suffix.index).trim() : message,
|
|
291
|
+
location: suffix ? (suffix[1] ? frameLocation(suffix[2]) : suffix[2]) : '',
|
|
292
|
+
continuation: record.continuation ?? [],
|
|
293
|
+
});
|
|
294
|
+
// The message as logged (with its location), for display.
|
|
295
|
+
return event && { ...event, message: raw };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Events from a console-tail (or Metro/webpack) log.
|
|
299
|
+
export function parseConsoleLog(text) {
|
|
300
|
+
return consoleRecords(text).map(recordEvent).filter(Boolean);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Events from a JSONL capture. console: { level, text, source: "URL:line:col
|
|
304
|
+
// fn" }; exception: { text, source }; log (a CDP Log entry): { level?, text,
|
|
305
|
+
// logSource or source: the Log source, e.g. "network", url }. A Log entry
|
|
306
|
+
// without a level is an error.
|
|
307
|
+
export function parseJsonl(content) {
|
|
308
|
+
const events = [];
|
|
309
|
+
for (const line of String(content ?? '').split('\n')) {
|
|
310
|
+
if (!line.trim()) continue;
|
|
311
|
+
const entry = JSON.parse(line);
|
|
312
|
+
if (entry.kind !== 'console' && entry.kind !== 'exception' && entry.kind !== 'log') continue;
|
|
313
|
+
const [first = '', ...continuation] = String(entry.text ?? '').split(/\r?\n/u);
|
|
314
|
+
const event = entry.kind === 'log'
|
|
315
|
+
? makeEvent({
|
|
316
|
+
level: entry.level === undefined ? 'error' : JSONL_LEVELS[entry.level] ?? null,
|
|
317
|
+
source: 'browser',
|
|
318
|
+
logSource: [entry.logSource, entry.source].find((value) => typeof value === 'string' && /^[a-z-]+$/u.test(value)) ?? null,
|
|
319
|
+
text: first.trim(),
|
|
320
|
+
location: typeof entry.url === 'string' ? entry.url : '',
|
|
321
|
+
continuation,
|
|
322
|
+
})
|
|
323
|
+
: makeEvent({
|
|
324
|
+
level: entry.kind === 'exception' ? 'exception' : JSONL_LEVELS[entry.level] ?? null,
|
|
325
|
+
source: 'page',
|
|
326
|
+
text: first.trim(),
|
|
327
|
+
location: frameLocation(String(entry.source ?? '').split(' ')[0]),
|
|
328
|
+
continuation,
|
|
329
|
+
});
|
|
330
|
+
if (event) events.push(event);
|
|
331
|
+
}
|
|
332
|
+
return events;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ---- exemptions -----------------------------------------------------------
|
|
336
|
+
|
|
337
|
+
// The failure a launch probe of the mainnet block produces, and nothing
|
|
338
|
+
// else: an error-level browser Log entry from the network source, with no
|
|
339
|
+
// further lines and no exception wording, whose text is exactly a failed
|
|
340
|
+
// load (net::ERR_*) or a failed WebSocket, of a URL with exactly one
|
|
341
|
+
// mm-harness-probe parameter, equal to 1. Probes fail before any HTTP
|
|
342
|
+
// response, so a status line is never one.
|
|
343
|
+
const RESOURCE_PROBE_TEXT = /^Failed to load resource: net::ERR_[A-Z_]+(?:\.Inspector)?$/u;
|
|
344
|
+
const WEBSOCKET_PROBE_TEXT = /^WebSocket connection to '([^'\s]+)' failed:(?: Error in connection establishment: net::ERR_[A-Z_]+)?$/u;
|
|
345
|
+
const EXCEPTION_WORDING = /uncaught|unhandled|exception|fatal|error:/iu;
|
|
346
|
+
|
|
347
|
+
function isProbeUrl(url) {
|
|
348
|
+
try {
|
|
349
|
+
const values = new URL(url).searchParams.getAll(HARNESS_PROBE_PARAM);
|
|
350
|
+
return values.length === 1 && values[0] === '1';
|
|
351
|
+
} catch {
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export function isHarnessProbe(event) {
|
|
357
|
+
if (event?.source !== 'browser' || event.logSource !== 'network' || event.level !== 'error' || event.continuation !== '') return false;
|
|
358
|
+
if (EXCEPTION_WORDING.test(event.text) || EXCEPTION_WORDING.test(event.location)) return false;
|
|
359
|
+
const socket = WEBSOCKET_PROBE_TEXT.exec(event.text);
|
|
360
|
+
if (socket) return isProbeUrl(socket[1]);
|
|
361
|
+
return RESOURCE_PROBE_TEXT.test(event.text) && isProbeUrl(event.location);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// A capture's control line (a watcher proving its capture works): exactly
|
|
365
|
+
// "[mm-harness-capture-control] <id>" or "[cdp-watch-control][ <id>]",
|
|
366
|
+
// alone, not an exception. Never a finding.
|
|
367
|
+
const CONTROL_TEXT = /^\[(?:mm-harness-capture-control|cdp-watch-control)\](?: [\w-]+)?$/u;
|
|
368
|
+
export function isControlEvent(event) {
|
|
369
|
+
return event?.level !== 'exception' && event?.continuation === '' && CONTROL_TEXT.test(event.text);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ---- console-tail findings (run diagnostics, assert_no_console_errors) ----
|
|
373
|
+
|
|
374
|
+
// The message with run-specific values replaced and query strings dropped,
|
|
375
|
+
// so repeats of one problem group together; for display, not matching.
|
|
376
|
+
export function messageSignature(message) {
|
|
377
|
+
const suffix = LOCATION_SUFFIX.exec(message);
|
|
378
|
+
const text = suffix ? message.slice(0, suffix.index) : message;
|
|
379
|
+
return signature(text).replace(/([a-z][\w+.-]*:\/\/[^\s?#'"()]+)[?#][^\s'"()]*/giu, '$1').slice(0, 240);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// A record → its finding, or null for ordinary output, the launch probe's own
|
|
383
|
+
// failure and a capture control line: { level, source, logSource, message, signature (grouping),
|
|
384
|
+
// firstFrame, text, location, continuation, key }.
|
|
385
|
+
export function classifyConsoleRecord(record) {
|
|
386
|
+
const event = recordEvent(record);
|
|
387
|
+
if (!event || isHarnessProbe(event) || isControlEvent(event)) return null;
|
|
388
|
+
const frame = LOCATION_SUFFIX.exec(event.message);
|
|
389
|
+
return { ...event, signature: messageSignature(event.message), firstFrame: frame ? `${frame[1] ? `${frame[1]} ` : ''}${frame[2]}` : null };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// One console line → its finding, or null.
|
|
393
|
+
export function classifyConsoleLine(line) {
|
|
394
|
+
return classifyConsoleRecord({ line, continuation: [], start: RECORD_START.test(String(line ?? '').trim()), unframed: false });
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ---- allowlist ------------------------------------------------------------
|
|
398
|
+
|
|
399
|
+
const ALLOWLIST_SCHEMA_VERSION = 3;
|
|
400
|
+
|
|
401
|
+
// Why an entry cannot be applied, or null. Entries are compared with
|
|
402
|
+
// signatures, so they must be signatures.
|
|
403
|
+
export function entryProblem(entry) {
|
|
404
|
+
if (!entry || typeof entry.signature !== 'string' || !entry.signature || typeof entry.continuation !== 'string' ||
|
|
405
|
+
!SOURCES.has(entry.source ?? 'any') || typeof entry.reason !== 'string' || !entry.reason.trim()) {
|
|
406
|
+
return 'needs signature, continuation ("" for none), source (page|extension|browser|any) and reason';
|
|
407
|
+
}
|
|
408
|
+
if (entry.first_frame !== undefined && typeof entry.first_frame !== 'string') return 'first_frame must be a string';
|
|
409
|
+
if (signature(entry.signature) !== entry.signature) return `signature is not normalised (expected ${JSON.stringify(signature(entry.signature))})`;
|
|
410
|
+
if (typeof entry.first_frame === 'string' && locationSignature(entry.first_frame) !== entry.first_frame) {
|
|
411
|
+
return `first_frame is not normalised (expected ${JSON.stringify(locationSignature(entry.first_frame))})`;
|
|
412
|
+
}
|
|
413
|
+
const lines = entry.continuation.split('\n');
|
|
414
|
+
if (entry.continuation && (lines.some((line) => !line) || continuationSignature(lines) !== entry.continuation)) {
|
|
415
|
+
return 'continuation is not normalised (one signature per line, no blank lines)';
|
|
416
|
+
}
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// { entries, problems } from an allowlist document; throws on another
|
|
421
|
+
// schemaVersion. Bad entries are reported, never applied.
|
|
422
|
+
export function compileAllowlist(document) {
|
|
423
|
+
if (document?.schemaVersion !== ALLOWLIST_SCHEMA_VERSION) {
|
|
424
|
+
throw new Error(`schemaVersion must be ${ALLOWLIST_SCHEMA_VERSION} (exact signatures)`);
|
|
425
|
+
}
|
|
426
|
+
const problems = [];
|
|
427
|
+
const entries = (Array.isArray(document.entries) ? document.entries : []).flatMap((entry, index) => {
|
|
428
|
+
const problem = entryProblem(entry);
|
|
429
|
+
if (problem) {
|
|
430
|
+
problems.push(`entry ${index} ${problem}`);
|
|
431
|
+
return [];
|
|
432
|
+
}
|
|
433
|
+
return [{
|
|
434
|
+
...entry,
|
|
435
|
+
first_frame: entry.first_frame ?? null,
|
|
436
|
+
source: entry.source ?? 'any',
|
|
437
|
+
reason: entry.reason.trim(),
|
|
438
|
+
temporary: entry.temporary === true,
|
|
439
|
+
index,
|
|
440
|
+
}];
|
|
441
|
+
});
|
|
442
|
+
return { entries, problems };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Allowlist entries from every library root that has the file. A malformed
|
|
446
|
+
// file or entry is reported, never silently applied.
|
|
447
|
+
export function loadConsoleAllowlist(libraryRoots = []) {
|
|
448
|
+
const entries = [];
|
|
449
|
+
const problems = [];
|
|
450
|
+
for (const root of libraryRoots) {
|
|
451
|
+
const file = path.join(root, ALLOWLIST_RELATIVE_PATH);
|
|
452
|
+
if (!existsSync(file)) continue;
|
|
453
|
+
let compiled;
|
|
454
|
+
try {
|
|
455
|
+
compiled = compileAllowlist(JSON.parse(readFileSync(file, 'utf8')));
|
|
456
|
+
} catch (error) {
|
|
457
|
+
problems.push(`${file}: ${error.message}${/schemaVersion/u.test(error.message) ? '; no entry applied' : ''}`);
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
entries.push(...compiled.entries.map((entry) => ({ ...entry, file })));
|
|
461
|
+
problems.push(...compiled.problems.map((problem) => `${file}: ${problem}`));
|
|
462
|
+
}
|
|
463
|
+
return { entries, problems };
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// The first entry equal to the event (by source and key), or null.
|
|
467
|
+
export function matchEntry(event, entries) {
|
|
468
|
+
const { key } = event;
|
|
469
|
+
return entries.find((entry) =>
|
|
470
|
+
(entry.source === 'any' || entry.source === event.source) &&
|
|
471
|
+
entry.signature === key.signature &&
|
|
472
|
+
entry.continuation === key.continuation &&
|
|
473
|
+
(entry.first_frame === null || entry.first_frame === key.first_frame)) ?? null;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export function allowlistMatch(finding, allowlist) {
|
|
477
|
+
return matchEntry(finding, allowlist.entries);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Events split into allowlisted (entry → count) and unexpected ones; probes
|
|
481
|
+
// and control lines are neither.
|
|
482
|
+
export function classify(events, allowlist) {
|
|
483
|
+
const allowed = new Map();
|
|
484
|
+
const unexpected = [];
|
|
485
|
+
for (const event of events) {
|
|
486
|
+
if (isHarnessProbe(event) || isControlEvent(event)) continue;
|
|
487
|
+
const entry = matchEntry(event, allowlist.entries);
|
|
488
|
+
if (entry) allowed.set(entry, (allowed.get(entry) ?? 0) + 1);
|
|
489
|
+
else unexpected.push(event);
|
|
490
|
+
}
|
|
491
|
+
return { allowed, unexpected };
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// The entry an event would need, for the generator and for review output.
|
|
495
|
+
export function entryFor(event) {
|
|
496
|
+
return {
|
|
497
|
+
signature: event.key.signature,
|
|
498
|
+
continuation: event.key.continuation,
|
|
499
|
+
...(event.key.first_frame ? { first_frame: event.key.first_frame } : {}),
|
|
500
|
+
source: event.source,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
export const allowlistEntryFor = entryFor;
|
|
504
|
+
|
|
505
|
+
// RECIPE_LIBRARY_PATH: "name=/path:name=/path" (or bare paths).
|
|
506
|
+
export function libraryRootsFromEnv(env = process.env) {
|
|
507
|
+
return String(env.RECIPE_LIBRARY_PATH ?? '')
|
|
508
|
+
.split(':')
|
|
509
|
+
.map((entry) => entry.includes('=') ? entry.slice(entry.indexOf('=') + 1) : entry)
|
|
510
|
+
.filter(Boolean);
|
|
511
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Fail the recipe when the terminal page logged an error or uncaught exception
|
|
2
|
+
// that the recipe libraries' console allowlist does not mark as known
|
|
3
|
+
// third-party noise. Reads the slot console capture (app-console.log, and
|
|
4
|
+
// extension-console.log when asked) from where the current slot browser's
|
|
5
|
+
// launch began. The same classifier and allowlist feed run diagnostics.
|
|
6
|
+
// It never passes on missing evidence: the slot's own console collector must
|
|
7
|
+
// be running on the browser's CDP port, and for every capture file a
|
|
8
|
+
// requested source reads, a control line logged in its target (the app page,
|
|
9
|
+
// or MetaMask for the extension source) must reach that file; otherwise the
|
|
10
|
+
// action fails as unavailable.
|
|
11
|
+
|
|
12
|
+
import { closeSync, fstatSync, openSync, readFileSync } from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
allowlistMatch,
|
|
17
|
+
classifyConsoleRecord,
|
|
18
|
+
consoleRecords,
|
|
19
|
+
libraryRootsFromEnv,
|
|
20
|
+
loadConsoleAllowlist,
|
|
21
|
+
} from '../../shared/console-findings.mjs';
|
|
22
|
+
import { collectorOwnerMarker, collectorStatus, verifyCapture } from '../../shared/console-collector.mjs';
|
|
23
|
+
import { isDirectRun, readBrowserState, runAdapter, terminalRuntimeDir } from '../platform/runtime.mjs';
|
|
24
|
+
|
|
25
|
+
const LOGS = Object.freeze({ page: 'app-console.log', extension: 'extension-console.log' });
|
|
26
|
+
|
|
27
|
+
// The capture from a recorded offset. A file now shorter than the offset, or
|
|
28
|
+
// a different file, was replaced since: read it whole and say so, rather than
|
|
29
|
+
// reading nothing.
|
|
30
|
+
function readFrom(file, mark) {
|
|
31
|
+
let fd;
|
|
32
|
+
try {
|
|
33
|
+
fd = openSync(file, 'r');
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (error?.code === 'ENOENT') return { text: '', reset: false };
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
// One open file: its size and inode are those of the content read.
|
|
40
|
+
const stat = fstatSync(fd);
|
|
41
|
+
const offset = Number(mark.offset ?? 0);
|
|
42
|
+
const reset = offset > stat.size || (mark.inode != null && mark.inode !== stat.ino);
|
|
43
|
+
return { text: readFileSync(fd).subarray(reset ? 0 : offset).toString('utf8'), reset };
|
|
44
|
+
} finally {
|
|
45
|
+
closeSync(fd);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function unavailable(detail) {
|
|
50
|
+
return new Error(`console capture unavailable: ${detail}. The page console was not proven to be captured, so its absence of errors proves nothing.`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function assertNoConsoleErrors(input, env = process.env, deps = {}) {
|
|
54
|
+
const node = input.node ?? {};
|
|
55
|
+
const projectRoot = input.context?.projectRoot;
|
|
56
|
+
const sources = Array.isArray(node.sources) && node.sources.length ? node.sources.map(String) : ['page', 'browser'];
|
|
57
|
+
const levels = new Set(Array.isArray(node.levels) && node.levels.length ? node.levels.map(String) : ['error', 'exception']);
|
|
58
|
+
const runtime = terminalRuntimeDir(projectRoot);
|
|
59
|
+
const state = await readBrowserState(projectRoot);
|
|
60
|
+
if (!state?.cdpPort || !state.appOrigin) throw unavailable('no slot browser state (browser.json)');
|
|
61
|
+
const capture = (deps.collectorStatus ?? collectorStatus)({
|
|
62
|
+
pidFile: path.join(runtime, 'console-tail.pid'),
|
|
63
|
+
extensionLog: path.join(runtime, LOGS.extension),
|
|
64
|
+
marker: collectorOwnerMarker(runtime),
|
|
65
|
+
cdpPort: state.cdpPort,
|
|
66
|
+
});
|
|
67
|
+
if (!capture.ok) throw unavailable(capture.detail);
|
|
68
|
+
for (const source of sources) {
|
|
69
|
+
if (!['page', 'extension', 'browser'].includes(source)) throw new Error(`sources must be page, extension or browser, got ${JSON.stringify(source)}.`);
|
|
70
|
+
}
|
|
71
|
+
// Browser log entries come from the page's own capture file.
|
|
72
|
+
const files = [...new Set(sources.map((source) => (source === 'extension' ? 'extension' : 'page')))];
|
|
73
|
+
const control = {};
|
|
74
|
+
for (const file of files) {
|
|
75
|
+
const result = await (deps.verifyCapture ?? verifyCapture)({ cdpPort: state.cdpPort, log: path.join(runtime, LOGS[file]), target: file, appOrigin: state.appOrigin, extensionId: state.extension?.id ?? null });
|
|
76
|
+
if (!result.ok) throw unavailable(result.detail);
|
|
77
|
+
control[file] = result.detail;
|
|
78
|
+
}
|
|
79
|
+
const offsets = node.since === 'start-of-log' ? {} : state?.consoleOffsets ?? {};
|
|
80
|
+
const roots = [...libraryRootsFromEnv(env), ...(typeof node.allowlist_root === 'string' ? [node.allowlist_root] : [])];
|
|
81
|
+
const allowlist = loadConsoleAllowlist(roots);
|
|
82
|
+
const failing = new Map();
|
|
83
|
+
const allowlisted = new Map();
|
|
84
|
+
const offsetResets = [];
|
|
85
|
+
for (const file of files) {
|
|
86
|
+
const { text, reset } = readFrom(path.join(runtime, LOGS[file]), { offset: offsets[file] ?? 0, inode: offsets.inodes?.[file] });
|
|
87
|
+
if (reset) offsetResets.push(file);
|
|
88
|
+
for (const record of consoleRecords(text)) {
|
|
89
|
+
const finding = classifyConsoleRecord(record);
|
|
90
|
+
if (!finding || !levels.has(finding.level) || !sources.includes(finding.source)) continue;
|
|
91
|
+
const match = allowlistMatch(finding, allowlist);
|
|
92
|
+
const bucket = match ? allowlisted : failing;
|
|
93
|
+
const key = `${finding.level}|${finding.source}|${finding.signature}`;
|
|
94
|
+
const entry = bucket.get(key) ?? { level: finding.level, source: finding.source, signature: finding.signature, example: finding.message, firstFrame: finding.firstFrame, count: 0, ...(match ? { reason: match.reason } : {}) };
|
|
95
|
+
entry.count += 1;
|
|
96
|
+
bucket.set(key, entry);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const output = {
|
|
100
|
+
action: input.action,
|
|
101
|
+
sources,
|
|
102
|
+
levels: [...levels],
|
|
103
|
+
findings: [...failing.values()],
|
|
104
|
+
allowlisted: [...allowlisted.values()],
|
|
105
|
+
allowlistProblems: allowlist.problems,
|
|
106
|
+
capture: { collector: capture.detail, control },
|
|
107
|
+
// The recorded launch offset was past the end of a replaced capture
|
|
108
|
+
// file; that file was read from its start.
|
|
109
|
+
offsetResets,
|
|
110
|
+
proofPath: 'terminal-console-capture',
|
|
111
|
+
};
|
|
112
|
+
if (failing.size > 0) {
|
|
113
|
+
const list = [...failing.values()].map((item) => `${item.level} [${item.source}] ${item.example} ×${item.count}`).join('\n ');
|
|
114
|
+
throw new Error(`console errors not on the allowlist:\n ${list}`);
|
|
115
|
+
}
|
|
116
|
+
return output;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (isDirectRun(import.meta.url)) runAdapter((input) => assertNoConsoleErrors(input));
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Explicit lifecycle step: start (or reuse) the terminal slot browser with the
|
|
2
|
+
// requested signer (MetaMask extension or injected strict wallet) and fixture
|
|
3
|
+
// account, and the wallet host that records wallet requests. A fresh_profile
|
|
4
|
+
// start forgets the app's stored agent key and the extension's dapp permission.
|
|
5
|
+
|
|
6
|
+
import { appPort, cdpPort, isDirectRun, resolveSigner, runAdapter } from '../platform/runtime.mjs';
|
|
7
|
+
import { launchTerminalBrowser } from '../../../../adapters/terminal/launch.mjs';
|
|
8
|
+
|
|
9
|
+
export async function launch(input) {
|
|
10
|
+
const node = input.node ?? {};
|
|
11
|
+
const state = await launchTerminalBrowser({
|
|
12
|
+
target: input.context?.projectRoot,
|
|
13
|
+
'cdp-port': cdpPort(input),
|
|
14
|
+
'app-port': appPort(input),
|
|
15
|
+
signer: resolveSigner(node.signer),
|
|
16
|
+
account: String(node.account ?? node.account_name ?? 'dev1'),
|
|
17
|
+
'fresh-profile': node.fresh_profile === true,
|
|
18
|
+
// headless: true|false wins over env TERMINAL_HEADLESS; headful: true is the
|
|
19
|
+
// older spelling of headless: false.
|
|
20
|
+
headless: node.headless === true,
|
|
21
|
+
headful: node.headless === false || (node.headless !== true && node.headful === true),
|
|
22
|
+
'slow-mo': node.slow_mo_ms,
|
|
23
|
+
network: node.network,
|
|
24
|
+
'mainnet-confirmation': node.mainnet_confirmation,
|
|
25
|
+
'start-chain': node.start_chain ?? 42161,
|
|
26
|
+
});
|
|
27
|
+
return {
|
|
28
|
+
action: input.action,
|
|
29
|
+
status: 'ready',
|
|
30
|
+
reused: state.reused,
|
|
31
|
+
signer: state.signer,
|
|
32
|
+
network: state.network ?? 'testnet',
|
|
33
|
+
account: state.account,
|
|
34
|
+
cdpPort: state.cdpPort,
|
|
35
|
+
appOrigin: state.appOrigin,
|
|
36
|
+
browser: state.browser,
|
|
37
|
+
window: state.window ?? null,
|
|
38
|
+
slowMoMs: state.slowMoMs ?? 0,
|
|
39
|
+
extension: state.extension ? { id: state.extension.id, version: state.extension.version } : null,
|
|
40
|
+
freshProfile: state.freshProfile,
|
|
41
|
+
proofPath: 'terminal-browser-launch',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (isDirectRun(import.meta.url)) runAdapter((input) => launch(input));
|