@deeeed/metamask-harness 0.36.0 → 0.37.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 +24 -0
- package/adapters/extension/artifact-runtime-state.cjs +128 -0
- package/adapters/extension/inject.mjs +1 -0
- package/adapters/extension/launch-browser.cjs +22 -0
- package/adapters/extension/live.sh +82 -12
- package/adapters/extension/readiness.mjs +77 -36
- package/adapters/extension/snapshot-dist.sh +88 -3
- package/adapters/extension/verify.sh +6 -2
- package/adapters/manifest.json +9 -1
- package/adapters/shared/log-tui.mjs +1 -1
- package/dist/adapters/extension/artifact-integrity.js +38 -0
- package/dist/adapters/extension/extension-id.js +23 -4
- package/dist/adapters/extension/release-artifact.js +386 -0
- package/dist/adapters/extension/runtime-decision.js +161 -20
- package/dist/adapters/extension/runtime.js +127 -0
- package/dist/adapters/mobile/release-artifact-state.js +124 -0
- package/dist/adapters/mobile/release-artifact.js +295 -0
- package/dist/adapters.js +11 -4
- package/dist/command-contract.js +16 -0
- package/dist/commands/call.js +2 -1
- package/dist/commands/launch/extension.js +55 -6
- package/dist/commands/launch/mobile.js +2 -0
- package/dist/commands/provision.js +2 -0
- package/dist/commands/run-engine.js +89 -5
- package/dist/commands/run.js +3 -1
- package/dist/commands/runtime-launch.js +178 -10
- package/dist/heal-bounds.js +1 -1
- package/dist/live-adapter-contract.js +3 -1
- package/dist/metamask-action-validation.js +47 -1
- package/dist/mm-harness-cli.js +31 -1
- package/dist/run-diagnostics.js +1 -1
- package/docs/RELEASE-QA-CAPABILITY-MAP.md +150 -0
- package/library/actions/extension/perps/perps.mjs +2 -0
- package/library/actions/extension/perps/read_snapshot.mjs +470 -0
- package/library/actions/extension/platform/cdp.mjs +6 -3
- package/library/actions/extension/wallet/import.mjs +13 -46
- package/library/actions/extension/wallet/secret-input.mjs +98 -0
- package/library/actions/mobile/platform/observe-ui.mjs +84 -2
- package/library/actions/mobile/ui/native-navigation.mjs +225 -0
- package/library/actions/mobile/ui/navigate.mjs +7 -0
- package/library/actions/mobile/wallet/import.mjs +71 -2
- package/library/actions/mobile/wallet/native-ui.mjs +493 -0
- package/library/actions/mobile/wallet/read_state.mjs +16 -0
- package/library/actions/mobile/wallet/reset.mjs +17 -4
- package/library/actions/shared/ui/locators.mjs +7 -0
- package/library/manifests/extension.action-manifest.json +116 -0
- package/library/manifests/mobile.action-manifest.json +16 -0
- package/library/recipes/extension/runner/action-validation.recipe.json +12 -1
- package/library/recipes/wallet/import.recipe.json +20 -1
- package/library/recipes/wallet/reset-import.recipe.json +20 -1
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@ import { execFileSync } from "node:child_process";
|
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { extensionTreeSha256 } from "./artifact-integrity.js";
|
|
5
6
|
import {
|
|
6
7
|
depsCheck,
|
|
7
8
|
INSTALL_MARKERS
|
|
@@ -151,21 +152,106 @@ function latestDistMtime(target) {
|
|
|
151
152
|
walk2(distDir);
|
|
152
153
|
return latest;
|
|
153
154
|
}
|
|
154
|
-
function
|
|
155
|
-
const
|
|
155
|
+
function releaseArtifactState(target) {
|
|
156
|
+
const runtimeRoot = path.join(target, recipeRuntimeDir());
|
|
157
|
+
const statePath = path.join(runtimeRoot, "extension-release-artifact.json");
|
|
158
|
+
if (!fs.existsSync(statePath)) return { status: "none" };
|
|
159
|
+
try {
|
|
160
|
+
const stateStat = fs.lstatSync(statePath);
|
|
161
|
+
if (!stateStat.isFile() || stateStat.isSymbolicLink() || stateStat.size <= 0 || stateStat.size > 64 * 1024) {
|
|
162
|
+
return { status: "invalid", reason: "artifact runtime identity is not a regular bounded file" };
|
|
163
|
+
}
|
|
164
|
+
const state = JSON.parse(fs.readFileSync(statePath, "utf8"));
|
|
165
|
+
const sourceDir = typeof state.sourceDir === "string" ? path.resolve(state.sourceDir) : "";
|
|
166
|
+
const runtimeDist = typeof state.runtimeDist === "string" ? path.resolve(state.runtimeDist) : "";
|
|
167
|
+
const provenancePath = typeof state.provenancePath === "string" ? path.resolve(state.provenancePath) : "";
|
|
168
|
+
const expectedRuntimeDist = path.join(
|
|
169
|
+
runtimeRoot,
|
|
170
|
+
process.env.RECIPE_RUNTIME_DIST_DIR || "runtime-dist"
|
|
171
|
+
);
|
|
172
|
+
if (state.schemaVersion !== 1 || !sourceDir || runtimeDist !== expectedRuntimeDist || !provenancePath) {
|
|
173
|
+
return { status: "invalid", reason: "artifact runtime identity has invalid paths or schema" };
|
|
174
|
+
}
|
|
175
|
+
const provenanceStat = fs.lstatSync(provenancePath);
|
|
176
|
+
if (!provenanceStat.isFile() || provenanceStat.isSymbolicLink() || provenanceStat.size <= 0 || provenanceStat.size > 64 * 1024) {
|
|
177
|
+
return { status: "invalid", reason: "artifact provenance is not a regular bounded file" };
|
|
178
|
+
}
|
|
179
|
+
const provenance = JSON.parse(fs.readFileSync(provenancePath, "utf8"));
|
|
180
|
+
const relativeExtensionDir = typeof provenance.extensionDir === "string" ? provenance.extensionDir : "";
|
|
181
|
+
const boundSource = relativeExtensionDir && !path.isAbsolute(relativeExtensionDir) && !relativeExtensionDir.split(/[\\/]+/u).includes("..") ? path.resolve(path.dirname(provenancePath), relativeExtensionDir) : "";
|
|
182
|
+
const sha256 = typeof provenance.sha256 === "string" ? provenance.sha256 : "";
|
|
183
|
+
const expectedVersion = typeof provenance.expectedVersion === "string" ? provenance.expectedVersion : "";
|
|
184
|
+
const manifestVersion = typeof provenance.manifestVersion === "string" ? provenance.manifestVersion : "";
|
|
185
|
+
const treeSha256 = typeof provenance.treeSha256 === "string" ? provenance.treeSha256 : "";
|
|
186
|
+
if (provenance.schemaVersion !== 1 || boundSource !== sourceDir || !/^[a-f0-9]{64}$/u.test(sha256) || !/^[a-f0-9]{64}$/u.test(treeSha256) || !/^\d+\.\d+\.\d+$/u.test(expectedVersion) || state.sha256 !== sha256 || state.treeSha256 !== treeSha256 || state.expectedVersion !== expectedVersion || state.manifestVersion !== manifestVersion) {
|
|
187
|
+
return { status: "invalid", reason: "artifact runtime identity does not match its provenance" };
|
|
188
|
+
}
|
|
189
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(runtimeDist, "manifest.json"), "utf8"));
|
|
190
|
+
const runtimeVersion = typeof manifest.version === "string" ? manifest.version : "";
|
|
191
|
+
if (normalizeReleaseVersion(runtimeVersion) !== expectedVersion || runtimeVersion !== manifestVersion) {
|
|
192
|
+
return { status: "invalid", reason: "artifact runtime manifest does not match its provenance" };
|
|
193
|
+
}
|
|
194
|
+
if (extensionTreeSha256(sourceDir) !== treeSha256 || extensionTreeSha256(runtimeDist) !== treeSha256) {
|
|
195
|
+
return { status: "invalid", reason: "artifact source or runtime tree does not match its provenance" };
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
status: "valid",
|
|
199
|
+
sourceDir,
|
|
200
|
+
runtimeDist,
|
|
201
|
+
provenancePath,
|
|
202
|
+
sha256,
|
|
203
|
+
treeSha256,
|
|
204
|
+
expectedVersion,
|
|
205
|
+
manifestVersion
|
|
206
|
+
};
|
|
207
|
+
} catch (error) {
|
|
208
|
+
return {
|
|
209
|
+
status: "invalid",
|
|
210
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function normalizeReleaseVersion(version) {
|
|
215
|
+
if (!/^\d+\.\d+\.\d+(?:\.\d+)?$/u.test(version)) return null;
|
|
216
|
+
const parts = version.split(".");
|
|
217
|
+
if (parts.length === 4 && parts[3] !== "0") return null;
|
|
218
|
+
return parts.slice(0, 3).join(".");
|
|
219
|
+
}
|
|
220
|
+
function releaseArtifactDistCheck(artifact) {
|
|
221
|
+
return {
|
|
222
|
+
status: "fresh",
|
|
223
|
+
source: "release-artifact",
|
|
224
|
+
manifestPath: path.join(artifact.runtimeDist, "manifest.json"),
|
|
225
|
+
manifestVersion: artifact.manifestVersion,
|
|
226
|
+
artifactSha256: artifact.sha256
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
function runtimeDistCheck(target, artifact = releaseArtifactState(target)) {
|
|
230
|
+
if (artifact.status === "invalid") {
|
|
231
|
+
return {
|
|
232
|
+
status: "stale",
|
|
233
|
+
source: "release-artifact",
|
|
234
|
+
artifactError: artifact.reason,
|
|
235
|
+
modified: ["<artifact identity invalid>"]
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
const dist = artifact.status === "valid" ? artifact.sourceDir : path.join(target, "dist/chrome");
|
|
156
239
|
const runtimeDist = path.join(
|
|
157
240
|
target,
|
|
158
241
|
recipeRuntimeDir(),
|
|
159
242
|
process.env.RECIPE_RUNTIME_DIST_DIR || "runtime-dist"
|
|
160
243
|
);
|
|
161
|
-
|
|
244
|
+
const identity = artifact.status === "valid" ? {
|
|
245
|
+
source: "release-artifact",
|
|
246
|
+
artifactSha256: artifact.sha256,
|
|
247
|
+
provenancePath: artifact.provenancePath
|
|
248
|
+
} : {};
|
|
249
|
+
if (!fs.existsSync(path.join(runtimeDist, "manifest.json"))) return { status: "missing", ...identity };
|
|
162
250
|
let output;
|
|
163
251
|
try {
|
|
164
|
-
|
|
252
|
+
const rsyncArgs = [
|
|
165
253
|
"-rnic",
|
|
166
254
|
"--exclude",
|
|
167
|
-
"_metadata",
|
|
168
|
-
"--exclude",
|
|
169
255
|
"home.html",
|
|
170
256
|
"--exclude",
|
|
171
257
|
"sidepanel.html",
|
|
@@ -174,21 +260,34 @@ function runtimeDistCheck(target) {
|
|
|
174
260
|
"--out-format=%n",
|
|
175
261
|
`${dist}${path.sep}`,
|
|
176
262
|
`${runtimeDist}${path.sep}`
|
|
177
|
-
]
|
|
263
|
+
];
|
|
264
|
+
if (artifact.status !== "valid") rsyncArgs.splice(1, 0, "--exclude", "_metadata");
|
|
265
|
+
output = execFileSync("rsync", rsyncArgs, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
178
266
|
} catch {
|
|
179
|
-
return { status: "stale", modified: ["<snapshot comparison failed>"] };
|
|
267
|
+
return { status: "stale", modified: ["<snapshot comparison failed>"], ...identity };
|
|
180
268
|
}
|
|
181
269
|
const modified = output.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
270
|
+
if (artifact.status === "valid") {
|
|
271
|
+
for (const name of ["home.html", "sidepanel.html", "manifest.json"]) {
|
|
272
|
+
const source = path.join(dist, name);
|
|
273
|
+
const loaded = path.join(runtimeDist, name);
|
|
274
|
+
if (fileDigest(source) !== fileDigest(loaded)) modified.push(name);
|
|
275
|
+
}
|
|
276
|
+
} else {
|
|
277
|
+
for (const name of ["home.html", "sidepanel.html"]) {
|
|
278
|
+
const source = path.join(dist, name);
|
|
279
|
+
const loaded = path.join(runtimeDist, name);
|
|
280
|
+
if (normalizedRuntimeHtml(source) !== normalizedRuntimeHtml(loaded)) modified.push(name);
|
|
281
|
+
}
|
|
282
|
+
if (normalizedRuntimeManifest(path.join(dist, "manifest.json")) !== normalizedRuntimeManifest(path.join(runtimeDist, "manifest.json"))) {
|
|
283
|
+
modified.push("manifest.json");
|
|
284
|
+
}
|
|
189
285
|
}
|
|
190
286
|
const boundedModified = [...new Set(modified)].slice(0, 10);
|
|
191
|
-
return boundedModified.length > 0 ? { status: "stale", modified: boundedModified } : { status: "fresh" };
|
|
287
|
+
return boundedModified.length > 0 ? { status: "stale", modified: boundedModified, ...identity } : { status: "fresh", ...identity };
|
|
288
|
+
}
|
|
289
|
+
function fileDigest(file) {
|
|
290
|
+
return fs.existsSync(file) ? crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex") : null;
|
|
192
291
|
}
|
|
193
292
|
function normalizedRuntimeHtml(file) {
|
|
194
293
|
if (!fs.existsSync(file)) return null;
|
|
@@ -269,10 +368,11 @@ async function decideExtensionReadiness(target, options = {}) {
|
|
|
269
368
|
const deps = depsCheck(resolved);
|
|
270
369
|
const webpackCache = webpackCacheCheck(resolved);
|
|
271
370
|
const buildLog = buildLogCheck(resolved, options.watchLog);
|
|
272
|
-
const
|
|
273
|
-
const
|
|
371
|
+
const releaseArtifact = releaseArtifactState(resolved);
|
|
372
|
+
const dist = releaseArtifact.status === "valid" ? releaseArtifactDistCheck(releaseArtifact) : distCheck(resolved);
|
|
373
|
+
const runtimeDist = runtimeDistCheck(resolved, releaseArtifact);
|
|
274
374
|
const cdp = await cdpCheck(resolved, options.cdpPort, options.pageMode);
|
|
275
|
-
const checks = { deps, webpackCache, buildLog, dist, runtimeDist, cdp };
|
|
375
|
+
const checks = { deps, webpackCache, buildLog, dist, runtimeDist, releaseArtifact, cdp };
|
|
276
376
|
const install = [{ id: "yarn-install", argv: ["yarn", "install", "--immutable"], cwd: resolved }];
|
|
277
377
|
const relaunch = [{ id: "relaunch-browser" }];
|
|
278
378
|
const cacheStale = buildLog.reason === "stale-cache";
|
|
@@ -355,7 +455,47 @@ async function decideExtensionReadiness(target, options = {}) {
|
|
|
355
455
|
reasons: ["Build is fresh; browser liveness unverified (pass --cdp-port to confirm `ready`)."],
|
|
356
456
|
actions: relaunch
|
|
357
457
|
};
|
|
358
|
-
|
|
458
|
+
let core;
|
|
459
|
+
if (releaseArtifact.status === "invalid") {
|
|
460
|
+
core = {
|
|
461
|
+
decision: "blocked",
|
|
462
|
+
reasonCode: "release-artifact-identity-invalid",
|
|
463
|
+
reasons: [`The loaded release artifact identity is invalid: ${releaseArtifact.reason ?? "unknown reason"}.`],
|
|
464
|
+
actions: []
|
|
465
|
+
};
|
|
466
|
+
} else if (releaseArtifact.status === "valid") {
|
|
467
|
+
const artifactRules = [
|
|
468
|
+
{
|
|
469
|
+
when: runtimeDist.status !== "fresh",
|
|
470
|
+
decision: "relaunch",
|
|
471
|
+
reasonCode: runtimeDist.status === "missing" ? "release-artifact-runtime-missing" : "release-artifact-runtime-stale",
|
|
472
|
+
reasons: [runtimeDist.status === "missing" ? "The loaded release artifact snapshot is missing." : `The loaded runtime snapshot differs from release artifact ${releaseArtifact.sha256?.slice(0, 12)}.`],
|
|
473
|
+
actions: relaunch
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
when: cdp.status === "pass",
|
|
477
|
+
decision: "ready",
|
|
478
|
+
reasonCode: "release-artifact-healthy",
|
|
479
|
+
reasons: [`Release artifact ${releaseArtifact.expectedVersion} is intact and healthy over CDP.`],
|
|
480
|
+
actions: []
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
when: cdp.status === "fail",
|
|
484
|
+
decision: "relaunch",
|
|
485
|
+
reasonCode: "runtime-unhealthy",
|
|
486
|
+
reasons: ["The release artifact is intact but the live extension is unhealthy.", ...cdp.findings?.slice(0, 3) ?? []],
|
|
487
|
+
actions: relaunch
|
|
488
|
+
}
|
|
489
|
+
];
|
|
490
|
+
core = artifactRules.find((rule) => rule.when) ?? {
|
|
491
|
+
decision: "relaunch",
|
|
492
|
+
reasonCode: "cdp-unknown",
|
|
493
|
+
reasons: [`Release artifact ${releaseArtifact.expectedVersion} is intact; browser liveness is unverified.`],
|
|
494
|
+
actions: relaunch
|
|
495
|
+
};
|
|
496
|
+
} else {
|
|
497
|
+
core = rules.find((rule) => rule.when) ?? fallback;
|
|
498
|
+
}
|
|
359
499
|
return {
|
|
360
500
|
schemaVersion: 1,
|
|
361
501
|
adapter: "extension",
|
|
@@ -378,5 +518,6 @@ export {
|
|
|
378
518
|
decideExtensionReadiness,
|
|
379
519
|
isExtensionDistStale,
|
|
380
520
|
recordWebpackBaseline,
|
|
521
|
+
releaseArtifactState,
|
|
381
522
|
runtimeDistCheck
|
|
382
523
|
};
|
|
@@ -147,6 +147,12 @@ async function checkExtensionRuntimeHealth(projectRoot, cdpPort, options = {}) {
|
|
|
147
147
|
};
|
|
148
148
|
}
|
|
149
149
|
const runtime = await evaluateHealth(session, cdpCallTimeoutMs);
|
|
150
|
+
if (runtime.hasSubmitRequest !== true) {
|
|
151
|
+
const providerProbe = await probeUiEthereumProvider(session, cdpCallTimeoutMs);
|
|
152
|
+
runtime.evmRpcProbeOk = providerProbe.ok;
|
|
153
|
+
runtime.evmRpcProbeError = providerProbe.error;
|
|
154
|
+
runtime.evmRpcProbeSource = providerProbe.source;
|
|
155
|
+
}
|
|
150
156
|
if (runtime.href && !String(runtime.href).startsWith("chrome-extension://")) {
|
|
151
157
|
findings.push(`Extension page href is not an extension URL: ${runtime.href}`);
|
|
152
158
|
}
|
|
@@ -316,6 +322,127 @@ async function evaluateHealth(session, timeoutMs) {
|
|
|
316
322
|
}
|
|
317
323
|
return result.result?.value ?? {};
|
|
318
324
|
}
|
|
325
|
+
async function probeUiEthereumProvider(session, timeoutMs) {
|
|
326
|
+
const bridgeKey = "mm-harness.runtime.evm-provider-probe";
|
|
327
|
+
const preload = await boundedCdpCall(
|
|
328
|
+
session,
|
|
329
|
+
"Page.addScriptToEvaluateOnNewDocument",
|
|
330
|
+
{
|
|
331
|
+
source: `(() => {
|
|
332
|
+
const bridgeKey = Symbol.for(${JSON.stringify(bridgeKey)});
|
|
333
|
+
let provider;
|
|
334
|
+
Object.defineProperty(globalThis, 'ethereumProvider', {
|
|
335
|
+
configurable: true,
|
|
336
|
+
get() { return provider; },
|
|
337
|
+
set(value) { provider = value; },
|
|
338
|
+
});
|
|
339
|
+
Object.defineProperty(globalThis, bridgeKey, {
|
|
340
|
+
configurable: true,
|
|
341
|
+
value: {
|
|
342
|
+
async probe() {
|
|
343
|
+
if (!provider || typeof provider.request !== 'function') {
|
|
344
|
+
return { ok: false, error: 'Ethereum provider is unavailable' };
|
|
345
|
+
}
|
|
346
|
+
try {
|
|
347
|
+
const code = await provider.request({
|
|
348
|
+
method: 'eth_getCode',
|
|
349
|
+
params: ['0x0000000000000000000000000000000000000000', 'latest'],
|
|
350
|
+
});
|
|
351
|
+
const ok = typeof code === 'string' && /^0x[0-9a-f]*$/iu.test(code);
|
|
352
|
+
return {
|
|
353
|
+
ok,
|
|
354
|
+
error: ok ? null : 'Ethereum provider returned invalid contract code',
|
|
355
|
+
};
|
|
356
|
+
} catch (error) {
|
|
357
|
+
return { ok: false, error: String(error?.message || error) };
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
cleanup() {
|
|
361
|
+
const descriptor = Object.getOwnPropertyDescriptor(globalThis, 'ethereumProvider');
|
|
362
|
+
if (descriptor?.configurable === true) {
|
|
363
|
+
Object.defineProperty(globalThis, 'ethereumProvider', {
|
|
364
|
+
configurable: true,
|
|
365
|
+
writable: true,
|
|
366
|
+
value: provider,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
return delete globalThis[bridgeKey];
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
})();`
|
|
374
|
+
},
|
|
375
|
+
timeoutMs
|
|
376
|
+
);
|
|
377
|
+
if (typeof preload?.identifier !== "string" || preload.identifier.length === 0) {
|
|
378
|
+
return {
|
|
379
|
+
ok: false,
|
|
380
|
+
error: "Ethereum provider preload was not installed",
|
|
381
|
+
source: "ui-ethereum-provider"
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
let outcome = {
|
|
385
|
+
ok: false,
|
|
386
|
+
error: "Ethereum provider is unavailable",
|
|
387
|
+
source: "ui-ethereum-provider"
|
|
388
|
+
};
|
|
389
|
+
const cleanupErrors = [];
|
|
390
|
+
try {
|
|
391
|
+
await boundedCdpCall(session, "Page.reload", { ignoreCache: false }, timeoutMs);
|
|
392
|
+
const deadline = Date.now() + timeoutMs;
|
|
393
|
+
while (Date.now() <= deadline) {
|
|
394
|
+
try {
|
|
395
|
+
const result = await boundedCdpCall(session, "Runtime.evaluate", {
|
|
396
|
+
expression: `globalThis[Symbol.for(${JSON.stringify(bridgeKey)})]?.probe()`,
|
|
397
|
+
awaitPromise: true,
|
|
398
|
+
returnByValue: true
|
|
399
|
+
}, Math.max(1, deadline - Date.now()));
|
|
400
|
+
const value = result.result?.value;
|
|
401
|
+
if (value?.ok === true) {
|
|
402
|
+
outcome = { ok: true, error: null, source: "ui-ethereum-provider" };
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
if (typeof value?.error === "string") outcome.error = value.error;
|
|
406
|
+
} catch (error) {
|
|
407
|
+
outcome.error = messageOf(error);
|
|
408
|
+
}
|
|
409
|
+
if (outcome.ok) break;
|
|
410
|
+
await sleep(100);
|
|
411
|
+
}
|
|
412
|
+
} catch (error) {
|
|
413
|
+
outcome = { ok: false, error: messageOf(error), source: "ui-ethereum-provider" };
|
|
414
|
+
} finally {
|
|
415
|
+
try {
|
|
416
|
+
const cleanup = await boundedCdpCall(session, "Runtime.evaluate", {
|
|
417
|
+
expression: `(() => {
|
|
418
|
+
const key = Symbol.for(${JSON.stringify(bridgeKey)});
|
|
419
|
+
const bridge = globalThis[key];
|
|
420
|
+
if (!bridge) return true;
|
|
421
|
+
return bridge.cleanup() === true && !(key in globalThis);
|
|
422
|
+
})()`,
|
|
423
|
+
returnByValue: true
|
|
424
|
+
}, timeoutMs);
|
|
425
|
+
if (cleanup.result?.value !== true) cleanupErrors.push("page bridge removal was not confirmed");
|
|
426
|
+
} catch (error) {
|
|
427
|
+
cleanupErrors.push(messageOf(error));
|
|
428
|
+
}
|
|
429
|
+
try {
|
|
430
|
+
await boundedCdpCall(
|
|
431
|
+
session,
|
|
432
|
+
"Page.removeScriptToEvaluateOnNewDocument",
|
|
433
|
+
{ identifier: preload.identifier },
|
|
434
|
+
timeoutMs
|
|
435
|
+
);
|
|
436
|
+
} catch (error) {
|
|
437
|
+
cleanupErrors.push(messageOf(error));
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return cleanupErrors.length > 0 ? {
|
|
441
|
+
ok: false,
|
|
442
|
+
error: `Ethereum provider probe cleanup failed: ${cleanupErrors.join("; ")}`,
|
|
443
|
+
source: "ui-ethereum-provider"
|
|
444
|
+
} : outcome;
|
|
445
|
+
}
|
|
319
446
|
function extensionBackgroundProbeTimeoutMs(cdpCallTimeoutMs) {
|
|
320
447
|
return Math.max(1, Math.floor(cdpCallTimeoutMs * 0.8));
|
|
321
448
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { recipeRuntimePath } from "../../paths.js";
|
|
5
|
+
import {
|
|
6
|
+
assertAndroidReleaseArtifactRuntime
|
|
7
|
+
} from "./release-artifact.js";
|
|
8
|
+
function mobileReleaseArtifactStatePath(target) {
|
|
9
|
+
return recipeRuntimePath(target, "mobile-release-artifact.json");
|
|
10
|
+
}
|
|
11
|
+
function writeMobileReleaseArtifactState(target, report) {
|
|
12
|
+
const state = {
|
|
13
|
+
schemaVersion: 1,
|
|
14
|
+
adapter: "mobile",
|
|
15
|
+
platform: "android",
|
|
16
|
+
packageId: report.packageId,
|
|
17
|
+
versionName: report.versionName,
|
|
18
|
+
versionCode: report.versionCode,
|
|
19
|
+
sha256: report.sha256,
|
|
20
|
+
deviceSerial: report.deviceSerial,
|
|
21
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
22
|
+
};
|
|
23
|
+
const file = mobileReleaseArtifactStatePath(target);
|
|
24
|
+
const directory = ensureSafeRuntimeDirectory(target, file);
|
|
25
|
+
const existing = fs.lstatSync(file, { throwIfNoEntry: false });
|
|
26
|
+
if (existing && (!existing.isFile() || existing.isSymbolicLink())) throw invalidState(file);
|
|
27
|
+
const temporary = path.join(
|
|
28
|
+
directory,
|
|
29
|
+
`.mobile-release-artifact.${process.pid}.${randomBytes(8).toString("hex")}.tmp`
|
|
30
|
+
);
|
|
31
|
+
let descriptor;
|
|
32
|
+
try {
|
|
33
|
+
descriptor = fs.openSync(temporary, "wx", 384);
|
|
34
|
+
fs.writeFileSync(descriptor, `${JSON.stringify(state, null, 2)}
|
|
35
|
+
`, "utf8");
|
|
36
|
+
fs.fsyncSync(descriptor);
|
|
37
|
+
fs.closeSync(descriptor);
|
|
38
|
+
descriptor = void 0;
|
|
39
|
+
fs.renameSync(temporary, file);
|
|
40
|
+
} finally {
|
|
41
|
+
if (descriptor !== void 0) fs.closeSync(descriptor);
|
|
42
|
+
const temporaryStat = fs.lstatSync(temporary, { throwIfNoEntry: false });
|
|
43
|
+
if (temporaryStat) fs.unlinkSync(temporary);
|
|
44
|
+
}
|
|
45
|
+
return state;
|
|
46
|
+
}
|
|
47
|
+
function requireCurrentMobileReleaseArtifactState(target, verify = assertAndroidReleaseArtifactRuntime) {
|
|
48
|
+
const state = readMobileReleaseArtifactState(target);
|
|
49
|
+
if (state) verify(state);
|
|
50
|
+
return state;
|
|
51
|
+
}
|
|
52
|
+
function clearMobileReleaseArtifactState(target) {
|
|
53
|
+
const file = mobileReleaseArtifactStatePath(target);
|
|
54
|
+
const existing = fs.lstatSync(file, { throwIfNoEntry: false });
|
|
55
|
+
if (!existing) return;
|
|
56
|
+
ensureSafeRuntimeDirectory(target, file);
|
|
57
|
+
if (!existing.isFile() || existing.isSymbolicLink()) throw invalidState(file);
|
|
58
|
+
fs.unlinkSync(file);
|
|
59
|
+
}
|
|
60
|
+
function readMobileReleaseArtifactState(target) {
|
|
61
|
+
const file = mobileReleaseArtifactStatePath(target);
|
|
62
|
+
let stat;
|
|
63
|
+
try {
|
|
64
|
+
stat = fs.lstatSync(file);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (isMissing(error)) return null;
|
|
67
|
+
throw invalidState(file);
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size <= 0 || stat.size > 16 * 1024) {
|
|
71
|
+
throw invalidState(file);
|
|
72
|
+
}
|
|
73
|
+
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
74
|
+
if (value.schemaVersion !== 1 || value.adapter !== "mobile" || value.platform !== "android" || !text(value.packageId) || !text(value.versionName) || !text(value.versionCode) || !/^[a-f0-9]{64}$/u.test(value.sha256 ?? "") || !text(value.deviceSerial) || !text(value.installedAt)) throw invalidState(file);
|
|
75
|
+
return value;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error instanceof Error && error.message.startsWith("The Mobile release artifact state is invalid")) {
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
throw invalidState(file);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function text(value) {
|
|
84
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
85
|
+
}
|
|
86
|
+
function isMissing(error) {
|
|
87
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
88
|
+
}
|
|
89
|
+
function invalidState(file) {
|
|
90
|
+
return new Error(`The Mobile release artifact state is invalid: ${file}. Next: rerun runtime-launch with the same artifact source.`);
|
|
91
|
+
}
|
|
92
|
+
function ensureSafeRuntimeDirectory(target, file) {
|
|
93
|
+
const root = path.resolve(target);
|
|
94
|
+
const directory = path.dirname(path.resolve(file));
|
|
95
|
+
const relative = path.relative(root, directory);
|
|
96
|
+
if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
97
|
+
throw invalidState(file);
|
|
98
|
+
}
|
|
99
|
+
let current = root;
|
|
100
|
+
const rootStat = fs.lstatSync(root, { throwIfNoEntry: false });
|
|
101
|
+
if (!rootStat?.isDirectory() || rootStat.isSymbolicLink()) throw invalidState(file);
|
|
102
|
+
for (const segment of relative.split(path.sep)) {
|
|
103
|
+
current = path.join(current, segment);
|
|
104
|
+
const stat = fs.lstatSync(current, { throwIfNoEntry: false });
|
|
105
|
+
if (stat) {
|
|
106
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw invalidState(file);
|
|
107
|
+
} else {
|
|
108
|
+
fs.mkdirSync(current, { mode: 448 });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const realRoot = fs.realpathSync(root);
|
|
112
|
+
const realDirectory = fs.realpathSync(directory);
|
|
113
|
+
if (realDirectory !== realRoot && !realDirectory.startsWith(`${realRoot}${path.sep}`)) {
|
|
114
|
+
throw invalidState(file);
|
|
115
|
+
}
|
|
116
|
+
return directory;
|
|
117
|
+
}
|
|
118
|
+
export {
|
|
119
|
+
clearMobileReleaseArtifactState,
|
|
120
|
+
mobileReleaseArtifactStatePath,
|
|
121
|
+
readMobileReleaseArtifactState,
|
|
122
|
+
requireCurrentMobileReleaseArtifactState,
|
|
123
|
+
writeMobileReleaseArtifactState
|
|
124
|
+
};
|