@mostra/sdk 0.0.0-stage → 0.2.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.
@@ -0,0 +1,1010 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { link, lstat, mkdir, open, readFile, readdir, readlink, rename, rm, stat, statfs, utimes, writeFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { dirname, isAbsolute, join, resolve, sep } from "node:path";
5
+ import { BUNDLE_PLATFORMS, BUNDLE_SETUP_MARKER, BundleAsset, BundleFiles, BundleInfo, BundlePins, BundleSetup, RpcError, bundleAssetName, createDiagnostic } from "@mostra/protocol";
6
+ import { createReadStream, createWriteStream, existsSync, readFileSync } from "node:fs";
7
+ import { Readable } from "node:stream";
8
+ import { pipeline } from "node:stream/promises";
9
+ import { fileURLToPath } from "node:url";
10
+ import { EnvHttpProxyAgent, fetch } from "undici";
11
+ import { x } from "tar";
12
+ import { spawn } from "node:child_process";
13
+ //#region src/bundle/download.ts
14
+ /**
15
+ * Fetching a pinned archive (RT-01): streamed to disk while it is hashed, never longer than the
16
+ * pin says, through the environment's proxy (`HTTPS_PROXY`, `HTTP_PROXY`, `NO_PROXY`), retried on
17
+ * failures that a retry can cure, and checked against the pin before anyone reads it.
18
+ */
19
+ /** A failure a retry won't cure (an HTTP 4xx other than 408 and 429, a wrong size or digest). */
20
+ var Final = class extends Error {};
21
+ /**
22
+ * Downloads `url` (`https:`, `http:` or `file:`) to `dest` and checks its size and SHA-256.
23
+ * Throws `MOSTRA_ENV_BUNDLE_INVALID` when the bytes aren't the pinned ones and
24
+ * `MOSTRA_ENV_BUNDLE_DOWNLOAD_FAILED` otherwise; `dest` never survives a failure.
25
+ */
26
+ async function download(url, dest, expected, options = {}) {
27
+ const attempts = options.attempts ?? 3;
28
+ let last;
29
+ for (let attempt = 1; attempt <= attempts; attempt++) try {
30
+ const actual = await once(url, dest, expected, options);
31
+ if (actual !== expected.sha256) throw invalid$1(`The download from ${redactUrl(url)} has SHA-256 ${actual}, not the pinned ${expected.sha256}.`);
32
+ return;
33
+ } catch (error) {
34
+ await rm(dest, { force: true });
35
+ last = error;
36
+ if (error instanceof RpcError || error instanceof Final || options.signal?.aborted) break;
37
+ if (error.code === "ENOSPC") break;
38
+ if (attempt < attempts) await delay(1e3 * 4 ** (attempt - 1), options.signal);
39
+ }
40
+ if (last instanceof RpcError) throw last;
41
+ if (options.signal?.aborted) throw options.signal.reason;
42
+ throw new RpcError(createDiagnostic("MOSTRA_ENV_BUNDLE_DOWNLOAD_FAILED", { message: `Couldn't download ${redactUrl(url)}: ${scrubUrls(describe(last))}` }));
43
+ }
44
+ async function once(url, dest, expected, options) {
45
+ const hash = createHash("sha256");
46
+ let received = 0;
47
+ const stallMs = options.stallMs ?? 6e4;
48
+ const controller = new AbortController();
49
+ const abort = () => controller.abort(options.signal?.reason);
50
+ options.signal?.addEventListener("abort", abort, { once: true });
51
+ let timer;
52
+ const arm = () => {
53
+ if (timer !== void 0) clearTimeout(timer);
54
+ timer = setTimeout(() => controller.abort(/* @__PURE__ */ new Error(`no data for ${stallMs / 1e3} s`)), stallMs);
55
+ };
56
+ try {
57
+ arm();
58
+ const body = await open$1(url, controller.signal, options.env ?? process.env);
59
+ const counter = async function* (source) {
60
+ for await (const chunk of source) {
61
+ arm();
62
+ received += chunk.length;
63
+ if (received > expected.size) throw invalid$1(`The download from ${redactUrl(url)} is larger than the pinned ${expected.size} bytes.`);
64
+ hash.update(chunk);
65
+ options.onProgress?.({
66
+ received,
67
+ total: expected.size
68
+ });
69
+ yield chunk;
70
+ }
71
+ };
72
+ await pipeline(body, counter, createWriteStream(dest, { mode: 384 }), { signal: controller.signal });
73
+ if (received !== expected.size) throw invalid$1(`The download from ${redactUrl(url)} has ${received} bytes, not the pinned ${expected.size}.`);
74
+ return hash.digest("hex");
75
+ } catch (error) {
76
+ if (controller.signal.aborted && !(error instanceof RpcError)) {
77
+ const reason = controller.signal.reason;
78
+ throw reason instanceof Error ? reason : new Error(String(reason));
79
+ }
80
+ throw error;
81
+ } finally {
82
+ if (timer !== void 0) clearTimeout(timer);
83
+ options.signal?.removeEventListener("abort", abort);
84
+ }
85
+ }
86
+ /** The body of `url` as a stream. */
87
+ async function open$1(url, signal, env) {
88
+ const parsed = new URL(url);
89
+ if (parsed.protocol === "file:") return createReadStream(fileURLToPath(parsed), { signal });
90
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") throw new Final(`${parsed.protocol} URLs aren't supported`);
91
+ const headers = {
92
+ "user-agent": "mostra-sdk",
93
+ accept: "application/octet-stream"
94
+ };
95
+ if (parsed.username !== "" || parsed.password !== "") {
96
+ const user = decodeURIComponent(parsed.username);
97
+ const password = decodeURIComponent(parsed.password);
98
+ headers["authorization"] = `Basic ${Buffer.from(`${user}:${password}`).toString("base64")}`;
99
+ parsed.username = "";
100
+ parsed.password = "";
101
+ }
102
+ const response = await fetch(parsed, {
103
+ dispatcher: proxyAgent(env),
104
+ redirect: "follow",
105
+ signal,
106
+ headers
107
+ });
108
+ if (!response.ok || response.body === null) {
109
+ const status = response.status;
110
+ await response.body?.cancel().catch(() => void 0);
111
+ const message = `HTTP ${status}${response.statusText ? ` ${response.statusText}` : ""}`;
112
+ if (status >= 400 && status < 500 && status !== 408 && status !== 429) throw new Final(message);
113
+ throw new Error(message);
114
+ }
115
+ return Readable.fromWeb(response.body);
116
+ }
117
+ const agents = /* @__PURE__ */ new Map();
118
+ /**
119
+ * The agent for `env`'s `HTTPS_PROXY`, `HTTP_PROXY` and `NO_PROXY` (lowercase first, as curl
120
+ * reads them), one per distinct setting. Each is passed explicitly, an empty one included, so
121
+ * undici never falls back to `process.env` (GPT review finding 6).
122
+ */
123
+ function proxyAgent(env) {
124
+ const settings = {
125
+ httpProxy: env["http_proxy"] ?? env["HTTP_PROXY"] ?? "",
126
+ httpsProxy: env["https_proxy"] ?? env["HTTPS_PROXY"] ?? "",
127
+ noProxy: env["no_proxy"] ?? env["NO_PROXY"] ?? ""
128
+ };
129
+ const key = JSON.stringify(settings);
130
+ let agent = agents.get(key);
131
+ if (agent === void 0) {
132
+ agent = new EnvHttpProxyAgent(settings);
133
+ agents.set(key, agent);
134
+ }
135
+ return agent;
136
+ }
137
+ /**
138
+ * `text` with every URL in it reduced to {@link redactUrl}'s form: an error from the network stack
139
+ * repeats the URL it failed on, credentials, query and all (a mirror's or a proxy's), and a
140
+ * diagnostic is printed (GPT review finding 2).
141
+ */
142
+ function scrubUrls(text) {
143
+ return text.replace(/\b[a-z][a-z0-9+.-]*:\/\/[^\s"'<>()]+/gi, (url) => redactUrl(url));
144
+ }
145
+ function invalid$1(message) {
146
+ return new RpcError(createDiagnostic("MOSTRA_ENV_BUNDLE_INVALID", { message }));
147
+ }
148
+ /** A URL without credentials or a query, safe to print. */
149
+ function redactUrl(url) {
150
+ try {
151
+ const parsed = new URL(url);
152
+ parsed.username = "";
153
+ parsed.password = "";
154
+ parsed.search = "";
155
+ parsed.hash = "";
156
+ return parsed.toString();
157
+ } catch {
158
+ return "(an invalid URL)";
159
+ }
160
+ }
161
+ function describe(error) {
162
+ if (!(error instanceof Error)) return String(error);
163
+ const code = error.cause?.code ?? error.code;
164
+ return code !== void 0 && !error.message.includes(code) ? `${error.message} (${code})` : error.message;
165
+ }
166
+ function delay(ms, signal) {
167
+ return new Promise((done) => {
168
+ const timer = setTimeout(done, ms);
169
+ signal?.addEventListener("abort", () => {
170
+ clearTimeout(timer);
171
+ done();
172
+ }, { once: true });
173
+ });
174
+ }
175
+ //#endregion
176
+ //#region src/bundle/extract.ts
177
+ /**
178
+ * Unpacking a downloaded archive (RT-01). Only regular files, directories and symbolic links are
179
+ * accepted, all under the archive's one top directory, which is stripped. node-tar refuses
180
+ * absolute paths, `..` and writing through a link; it does create a link whatever its target, so
181
+ * setup then checks the whole tree against the bundle's file list (`checkTree`), which refuses any
182
+ * file or link it doesn't list and any link that leaves the bundle. A pinned archive's SHA-256 is
183
+ * checked before any of this.
184
+ */
185
+ const ACCEPTED = /* @__PURE__ */ new Set([
186
+ "File",
187
+ "OldFile",
188
+ "ContiguousFile",
189
+ "Directory",
190
+ "SymbolicLink"
191
+ ]);
192
+ /** Extracts `archive` (a `.tar.gz` with one top directory `top`) into the empty directory `into`. */
193
+ async function extractBundle(archive, into, top) {
194
+ let refused;
195
+ try {
196
+ await x({
197
+ file: archive,
198
+ cwd: into,
199
+ strip: 1,
200
+ strict: true,
201
+ preservePaths: false,
202
+ preserveOwner: false,
203
+ noChmod: false,
204
+ filter: (path, entry) => {
205
+ const type = "type" in entry ? String(entry.type) : "File";
206
+ if (!(path === `${top}/` || path === top || path.startsWith(`${top}/`)) || !ACCEPTED.has(type)) {
207
+ refused ??= `${path} (${type})`;
208
+ return false;
209
+ }
210
+ return true;
211
+ }
212
+ });
213
+ } catch (error) {
214
+ const code = error.code;
215
+ if (typeof code === "string" && /^E[A-Z]+$/.test(code) && code !== "EOF") throw new RpcError(createDiagnostic("MOSTRA_ENV_BUNDLE_DOWNLOAD_FAILED", { message: `The runtime bundle couldn't be written to ${into}: ${error.message}${code === "ENOSPC" ? " (the disk is full)" : ""}` }));
216
+ throw new RpcError(createDiagnostic("MOSTRA_ENV_BUNDLE_INVALID", { message: `The runtime bundle's archive couldn't be read: ${error.message}` }));
217
+ }
218
+ if (refused !== void 0) throw new RpcError(createDiagnostic("MOSTRA_ENV_BUNDLE_INVALID", { message: `The runtime bundle's archive holds an entry a bundle never has: ${refused}.` }));
219
+ }
220
+ //#endregion
221
+ //#region src/bundle/tree.ts
222
+ /**
223
+ * A bundle's files against its manifest (RT-01): one walk that both writes `files.json` (the
224
+ * assembler) and checks an extracted bundle against it (setup), so the two can't disagree about
225
+ * what counts. Links must stay inside the bundle; nothing may be missing, changed or extra.
226
+ */
227
+ /** The SHA-256 and size of one file, read as a stream. */
228
+ async function hashFile(path) {
229
+ const hash = createHash("sha256");
230
+ let size = 0;
231
+ for await (const chunk of createReadStream(path)) {
232
+ hash.update(chunk);
233
+ size += chunk.length;
234
+ }
235
+ return {
236
+ sha256: hash.digest("hex"),
237
+ size
238
+ };
239
+ }
240
+ /** Every file and link under `root`, sorted by path, skipping `exclude`d paths (and below). */
241
+ async function walk(root, exclude) {
242
+ const out = [];
243
+ const visit = async (dir) => {
244
+ const entries = await readdir(join(root, dir), { withFileTypes: true });
245
+ for (const entry of entries) {
246
+ const path = dir === "" ? entry.name : `${dir}/${entry.name}`;
247
+ if (exclude(path)) continue;
248
+ if (entry.isDirectory()) await visit(path);
249
+ else if (entry.isSymbolicLink()) out.push({
250
+ kind: "link",
251
+ path,
252
+ target: await readlink(join(root, path))
253
+ });
254
+ else if (entry.isFile()) {
255
+ const stats = await lstat(join(root, path));
256
+ out.push({
257
+ kind: "file",
258
+ path,
259
+ executable: (stats.mode & 64) !== 0
260
+ });
261
+ } else out.push({
262
+ kind: "other",
263
+ path
264
+ });
265
+ }
266
+ };
267
+ await visit("");
268
+ return out.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
269
+ }
270
+ /** Runs `task` over `items`, at most `limit` at a time, keeping the order of results. */
271
+ async function mapLimit(items, limit, task) {
272
+ const results = new Array(items.length);
273
+ let next = 0;
274
+ const worker = async () => {
275
+ while (next < items.length) {
276
+ const index = next++;
277
+ results[index] = await task(items[index]);
278
+ }
279
+ };
280
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
281
+ return results;
282
+ }
283
+ /** Whether a link's target is relative and resolves inside `root`. */
284
+ function linkStaysInside(root, path, target) {
285
+ if (isAbsolute(target)) return false;
286
+ const base = resolve(root);
287
+ const resolved = resolve(base, dirname(path), target);
288
+ return resolved === base || resolved.startsWith(base + sep);
289
+ }
290
+ /** Whether a manifest path is a plain relative path: no absolute start, no `.` or `..` segment. */
291
+ function safeRelative(path) {
292
+ if (path === "" || isAbsolute(path) || path.includes("\\") || path.includes("\0")) return false;
293
+ return path.split("/").every((segment) => segment !== "" && segment !== "." && segment !== "..");
294
+ }
295
+ /**
296
+ * The manifest of the tree under `root`: every regular file with its SHA-256, size and execute
297
+ * bit, every link with its target. Throws on anything else (a device, a socket, a link leaving
298
+ * the tree), which a bundle must never contain.
299
+ */
300
+ async function describeTree(root, exclude = () => false) {
301
+ const walked = await walk(root, exclude);
302
+ const files = [];
303
+ const links = [];
304
+ const regular = walked.filter((entry) => entry.kind === "file");
305
+ const hashed = await mapLimit(regular, 32, (entry) => hashFile(join(root, entry.path)));
306
+ regular.forEach((entry, index) => {
307
+ const { sha256, size } = hashed[index];
308
+ files.push({
309
+ path: entry.path,
310
+ size,
311
+ sha256,
312
+ executable: entry.executable
313
+ });
314
+ });
315
+ for (const entry of walked) {
316
+ if (entry.kind === "other") throw new Error(`${entry.path} is neither a file nor a link.`);
317
+ if (entry.kind !== "link") continue;
318
+ if (!linkStaysInside(root, entry.path, entry.target)) throw new Error(`${entry.path} links outside the tree (${entry.target}).`);
319
+ links.push({
320
+ path: entry.path,
321
+ target: entry.target
322
+ });
323
+ }
324
+ return {
325
+ schemaVersion: 1,
326
+ files,
327
+ links
328
+ };
329
+ }
330
+ /**
331
+ * Checks the tree under `root` against `manifest`: each listed file present, regular, with its
332
+ * size, SHA-256 and execute bit; each link present with its target, staying inside; and nothing
333
+ * else besides what `allowExtra` accepts. Reports at most `limit` problems.
334
+ */
335
+ async function checkTree(root, manifest, options = {}) {
336
+ const limit = options.limit ?? 20;
337
+ const allowExtra = options.allowExtra ?? (() => false);
338
+ const problems = [];
339
+ const add = (problem) => {
340
+ if (problems.length < limit) problems.push(problem);
341
+ };
342
+ const expectedFiles = new Map(manifest.files.map((entry) => [entry.path, entry]));
343
+ const expectedLinks = new Map(manifest.links.map((entry) => [entry.path, entry]));
344
+ for (const path of [...expectedFiles.keys(), ...expectedLinks.keys()]) if (!safeRelative(path)) add(`${path}: not a plain relative path`);
345
+ if (problems.length > 0) return problems;
346
+ const listedDirs = /* @__PURE__ */ new Set();
347
+ for (const path of [...expectedFiles.keys(), ...expectedLinks.keys()]) {
348
+ const segments = path.split("/");
349
+ for (let index = 1; index < segments.length; index++) listedDirs.add(segments.slice(0, index).join("/"));
350
+ }
351
+ const walked = await walk(root, (path) => allowExtra(path) && !expectedFiles.has(path) && !expectedLinks.has(path) && !listedDirs.has(path));
352
+ const found = new Map(walked.map((entry) => [entry.path, entry]));
353
+ for (const entry of walked) if (!expectedFiles.has(entry.path) && !expectedLinks.has(entry.path)) {
354
+ if (!allowExtra(entry.path)) add(`${entry.path}: not in the bundle's file list`);
355
+ }
356
+ for (const [path, link] of expectedLinks) {
357
+ const actual = found.get(path);
358
+ if (actual?.kind !== "link") add(`${path}: missing link`);
359
+ else if (actual.target !== link.target) add(`${path}: links to ${actual.target}, not ${link.target}`);
360
+ else if (!linkStaysInside(root, path, link.target)) add(`${path}: links outside the bundle`);
361
+ }
362
+ const results = await mapLimit([...expectedFiles.values()], 32, async (entry) => {
363
+ const actual = found.get(entry.path);
364
+ if (actual?.kind !== "file") return `${entry.path}: missing file`;
365
+ if (actual.executable !== entry.executable) return `${entry.path}: ${entry.executable ? "not executable" : "executable"}`;
366
+ const { sha256, size } = await hashFile(join(root, entry.path));
367
+ if (size !== entry.size) return `${entry.path}: ${size} bytes, not ${entry.size}`;
368
+ if (sha256 !== entry.sha256) return `${entry.path}: its SHA-256 differs`;
369
+ });
370
+ for (const result of results) if (result !== void 0) add(result);
371
+ return problems;
372
+ }
373
+ //#endregion
374
+ //#region src/bundle/setup.ts
375
+ /**
376
+ * Making an extracted bundle usable (RT-01, D142): its files checked against `files.json`, which
377
+ * `bundle.json` pins by SHA-256; the verify browser fetched through the bundle's own Playwright
378
+ * and checked against the bundle's pin; then `.mostra-setup.json`, the one marker every reader
379
+ * trusts. The SDK runs it after a download; `bin/mostra-setup` runs it for a bundle extracted by
380
+ * hand, and for the Rust crate (WP-12), so there is one implementation of what "ready" means.
381
+ */
382
+ /** The directory Playwright's browsers live in, inside a bundle. */
383
+ function browsersDir(root) {
384
+ return join(root, "browsers");
385
+ }
386
+ /** Whether `MOSTRA_OFFLINE` says Mostra is offline: any value but empty, `0`, `false` or `no`. */
387
+ function offline(env) {
388
+ const value = env["MOSTRA_OFFLINE"]?.trim().toLowerCase();
389
+ return value !== void 0 && value !== "" && value !== "0" && value !== "false" && value !== "no";
390
+ }
391
+ function invalid(message) {
392
+ return new RpcError(createDiagnostic("MOSTRA_ENV_BUNDLE_INVALID", { message }));
393
+ }
394
+ /** Reads `bundle.json` at `root`. Throws `MOSTRA_ENV_BUNDLE_INVALID` when it isn't a bundle's. */
395
+ async function readBundle(root) {
396
+ let text;
397
+ try {
398
+ text = await readFile(join(root, "bundle.json"), "utf8");
399
+ } catch (error) {
400
+ throw invalid(`${root} has no readable bundle.json (${error.message}).`);
401
+ }
402
+ let parsed;
403
+ try {
404
+ parsed = JSON.parse(text);
405
+ } catch {
406
+ throw invalid(`${join(root, "bundle.json")} isn't JSON.`);
407
+ }
408
+ const result = BundleInfo.safeParse(parsed);
409
+ if (!result.success) throw invalid(`${join(root, "bundle.json")} isn't a runtime bundle's.`);
410
+ const info = result.data;
411
+ if (![
412
+ info.node.path,
413
+ ...Object.values(info.entries),
414
+ info.files.path,
415
+ info.browser.installer
416
+ ].every(safeRelative)) throw invalid(`${join(root, "bundle.json")} names a path outside the bundle.`);
417
+ return {
418
+ root,
419
+ info
420
+ };
421
+ }
422
+ /** The setup marker, when it describes exactly this bundle. */
423
+ async function readSetup(bundle) {
424
+ try {
425
+ const marker = BundleSetup.parse(JSON.parse(await readFile(join(bundle.root, BUNDLE_SETUP_MARKER), "utf8")));
426
+ const { info } = bundle;
427
+ return marker.files === info.files.sha256 && marker.version === info.version && marker.platform === info.platform && marker.browser.revision === info.browser.revision ? marker : void 0;
428
+ } catch {
429
+ return;
430
+ }
431
+ }
432
+ /** What `checkTree` may find besides the listed files: the bundle's own records and browsers. */
433
+ function setupExtras(path) {
434
+ return path === "bundle.json" || path === "files.json" || path === "browsers" || path.startsWith("browsers/") || path === BUNDLE_SETUP_MARKER || path.startsWith(`${BUNDLE_SETUP_MARKER}.tmp-`);
435
+ }
436
+ /** Checks the bundle's files; throws `MOSTRA_ENV_BUNDLE_INVALID` naming what differs. */
437
+ async function checkFiles(bundle) {
438
+ const { root, info } = bundle;
439
+ const listing = join(root, info.files.path);
440
+ const { sha256 } = await hashFile(listing).catch(() => ({ sha256: "" }));
441
+ if (sha256 !== info.files.sha256) throw invalid(`${info.files.path} doesn't match the SHA-256 that bundle.json pins.`);
442
+ const manifest = BundleFiles.safeParse(JSON.parse(await readFile(listing, "utf8")));
443
+ if (!manifest.success) throw invalid(`${info.files.path} isn't a bundle's file list.`);
444
+ const problems = await checkTree(root, manifest.data, { allowExtra: setupExtras });
445
+ if (problems.length > 0) throw invalid(`Mostra ${info.version}'s runtime bundle for ${info.platform} differs from its file list: ${problems.join("; ")}.`);
446
+ }
447
+ /** The browser files that differ from the pin, relative to `browsers/`. */
448
+ async function checkBrowser(bundle) {
449
+ return await checkTree(browsersDir(bundle.root), {
450
+ schemaVersion: 1,
451
+ files: bundle.info.browser.files,
452
+ links: []
453
+ }, { allowExtra: () => true }).catch((error) => [`browsers: ${error.message}`]);
454
+ }
455
+ /** The environment the browser install runs in: proxies and Playwright's mirror pass through. */
456
+ function installEnvironment(bundle, env) {
457
+ const out = {};
458
+ for (const [name, value] of Object.entries(env)) if (value !== void 0 && !name.startsWith("MOSTRA_") && name !== "PLAYWRIGHT_BROWSERS_PATH") out[name] = value;
459
+ out["PLAYWRIGHT_BROWSERS_PATH"] = browsersDir(bundle.root);
460
+ return out;
461
+ }
462
+ /** The variables whose URLs may carry credentials the browser's fetch uses. */
463
+ const CREDENTIAL_URLS = [
464
+ "PLAYWRIGHT_DOWNLOAD_HOST",
465
+ "HTTPS_PROXY",
466
+ "https_proxy",
467
+ "HTTP_PROXY",
468
+ "http_proxy",
469
+ "ALL_PROXY",
470
+ "all_proxy"
471
+ ];
472
+ /** `text` without URLs' credentials or queries, and without any password `env`'s URLs carry. */
473
+ function scrubSecrets(text, env) {
474
+ let out = scrubUrls(text);
475
+ for (const name of CREDENTIAL_URLS) {
476
+ const value = env[name];
477
+ if (!value) continue;
478
+ const spelled = value.includes("://") ? value : `http://${value}`;
479
+ const parsed = URL.canParse(spelled) ? new URL(spelled) : void 0;
480
+ for (const secret of [parsed?.password, parsed?.username]) {
481
+ if (!secret) continue;
482
+ for (const form of /* @__PURE__ */ new Set([secret, decodeURIComponent(secret)])) if (form.length >= 3) out = out.split(form).join("***");
483
+ }
484
+ }
485
+ return out;
486
+ }
487
+ /** Runs the bundle's Playwright to fetch the pinned browser; at most 15 minutes. */
488
+ async function installBrowser(bundle, options) {
489
+ const { root, info } = bundle;
490
+ const output = [];
491
+ const code = await new Promise((resolve) => {
492
+ const child = spawn(join(root, info.node.path), [
493
+ join(root, info.browser.installer),
494
+ "install",
495
+ "chromium-headless-shell"
496
+ ], {
497
+ cwd: root,
498
+ env: installEnvironment(bundle, options.env ?? process.env),
499
+ stdio: [
500
+ "ignore",
501
+ "pipe",
502
+ "pipe"
503
+ ],
504
+ ...options.signal ? { signal: options.signal } : {},
505
+ timeout: 9e5
506
+ });
507
+ const keep = (chunk) => {
508
+ output.push(chunk.toString("utf8"));
509
+ if (output.length > 40) output.shift();
510
+ };
511
+ child.stdout.on("data", keep);
512
+ child.stderr.on("data", keep);
513
+ child.on("error", (error) => resolve(error.message));
514
+ child.on("close", (exit, signal) => resolve(exit ?? signal ?? "unknown"));
515
+ });
516
+ if (code === 0) return;
517
+ const tail = scrubSecrets(output.join("").trim().split("\n").slice(-6).join(" | "), options.env ?? process.env);
518
+ throw new RpcError(createDiagnostic("MOSTRA_ENV_BUNDLE_DOWNLOAD_FAILED", { message: `Fetching the verify browser (${info.browser.name} ${info.browser.browserVersion}) failed (${code}): ${tail}` }));
519
+ }
520
+ /**
521
+ * Makes the bundle at `root` ready: checks its files, fetches and checks the browser when it is
522
+ * missing or differs, and writes `.mostra-setup.json`. Idempotent: a ready bundle returns its
523
+ * marker after the checks. Throws `MOSTRA_ENV_BUNDLE_INVALID`, `MOSTRA_ENV_BUNDLE_UNAVAILABLE`
524
+ * (offline with no browser) or `MOSTRA_ENV_BUNDLE_DOWNLOAD_FAILED`.
525
+ */
526
+ async function setupBundle(root, options = {}) {
527
+ const env = options.env ?? process.env;
528
+ const bundle = await readBundle(root);
529
+ if (!options.filesChecked) {
530
+ options.onStep?.("files");
531
+ await checkFiles(bundle);
532
+ }
533
+ options.onStep?.("browser");
534
+ if ((await checkBrowser(bundle)).length > 0) {
535
+ if (offline(env)) throw new RpcError(createDiagnostic("MOSTRA_ENV_BUNDLE_UNAVAILABLE", { message: `The runtime bundle at ${root} has no verify browser yet, and MOSTRA_OFFLINE keeps Mostra from fetching it.` }));
536
+ await rm(browsersDir(root), {
537
+ recursive: true,
538
+ force: true
539
+ });
540
+ await installBrowser(bundle, options);
541
+ const problems = await checkBrowser(bundle);
542
+ if (problems.length > 0) throw invalid(`The verify browser Playwright fetched differs from the bundle's pin: ${problems.join("; ")}.`);
543
+ }
544
+ const marker = {
545
+ schemaVersion: 1,
546
+ version: bundle.info.version,
547
+ platform: bundle.info.platform,
548
+ files: bundle.info.files.sha256,
549
+ browser: { revision: bundle.info.browser.revision },
550
+ at: (/* @__PURE__ */ new Date()).toISOString()
551
+ };
552
+ const temporary = join(root, `${BUNDLE_SETUP_MARKER}.tmp-${process.pid}`);
553
+ await writeFile(temporary, `${JSON.stringify(marker, null, 2)}\n`);
554
+ await rename(temporary, join(root, BUNDLE_SETUP_MARKER));
555
+ return marker;
556
+ }
557
+ //#endregion
558
+ //#region src/bundle/cache.ts
559
+ /**
560
+ * The local cache of runtime bundles (RT-01): one directory per archive digest, so a bundle is
561
+ * found by what it is, not by what it's called. A bundle enters the cache only through
562
+ * {@link installPinned}: downloaded against its pin, extracted into a temporary directory, its
563
+ * files checked, renamed into place, its browser fetched, and only then marked ready. Concurrent
564
+ * installers of one bundle share a lock, so it is downloaded once; a crash leaves a temporary
565
+ * directory the next install removes, never a half-ready bundle.
566
+ */
567
+ /** Where the cache is: `MOSTRA_CACHE_DIR`, else the platform's per-user cache directory. */
568
+ function cacheRoot(env, platform = process.platform) {
569
+ const override = env["MOSTRA_CACHE_DIR"];
570
+ if (override) return resolve(override);
571
+ const home = env["HOME"] || homedir();
572
+ if (platform === "darwin") return join(home, "Library", "Caches", "mostra");
573
+ const xdg = env["XDG_CACHE_HOME"];
574
+ return join(xdg?.startsWith("/") ? xdg : join(home, ".cache"), "mostra");
575
+ }
576
+ /** The directory a bundle with this archive digest lives in. */
577
+ function bundleDir(env, sha256) {
578
+ return join(cacheRoot(env), "bundles", sha256);
579
+ }
580
+ /** The space an install needs beyond the archive and its files: the browser and some margin. */
581
+ const BROWSER_ALLOWANCE = 524288e3;
582
+ /**
583
+ * The pinned bundle for this platform, ready to run: from the cache when it is there and ready,
584
+ * else installed. `MOSTRA_BUNDLE_MIRROR` replaces the release URL (files keep their names).
585
+ */
586
+ async function installPinned(pins, options) {
587
+ const asset = pins.assets[options.platform];
588
+ if (asset !== void 0 && (!BundleAsset.safeParse(asset).success || asset.file !== bundleAssetName(pins.version, options.platform))) throw new RpcError(createDiagnostic("MOSTRA_ENV_BUNDLE_INVALID", { message: `The pins name ${JSON.stringify(asset.file)} for ${options.platform}, not Mostra ${pins.version}'s archive ${bundleAssetName(pins.version, options.platform)}.` }));
589
+ if (asset === void 0) throw new RpcError(createDiagnostic("MOSTRA_ENV_BUNDLE_UNAVAILABLE", { message: `Mostra ${pins.version} has no runtime bundle for ${options.platform}.` }));
590
+ const dir = bundleDir(options.env, asset.sha256);
591
+ const ready = await readyAt(dir);
592
+ if (ready) return ready;
593
+ if (offline(options.env)) throw new RpcError(createDiagnostic("MOSTRA_ENV_BUNDLE_UNAVAILABLE", { message: `Mostra ${pins.version}'s runtime bundle isn't in the cache (${dir}), and MOSTRA_OFFLINE keeps Mostra from fetching it.` }));
594
+ const bundles = join(cacheRoot(options.env), "bundles");
595
+ await mkdir(bundles, {
596
+ recursive: true,
597
+ mode: 448
598
+ });
599
+ options.onProgress?.({ step: "lock" });
600
+ const release = await lock(join(bundles, `${asset.sha256}.lock`), options.signal);
601
+ try {
602
+ const raced = await readyAt(dir);
603
+ if (raced) return raced;
604
+ await sweep(bundles);
605
+ if (await exists(dir)) {
606
+ if (await filesIntact(dir)) return await finish(dir, {
607
+ ...options,
608
+ filesChecked: true
609
+ });
610
+ await rm(dir, {
611
+ recursive: true,
612
+ force: true
613
+ });
614
+ }
615
+ await fetchInto(pins, asset, dir, bundles, options);
616
+ return await finish(dir, {
617
+ ...options,
618
+ filesChecked: true
619
+ });
620
+ } finally {
621
+ await release();
622
+ }
623
+ }
624
+ /**
625
+ * Whether the bundle at `dir` holds exactly what its `files.json` lists. `false` only for
626
+ * `MOSTRA_ENV_BUNDLE_INVALID` (a missing or changed file, a `bundle.json` that doesn't read);
627
+ * every other error is thrown.
628
+ */
629
+ async function filesIntact(dir) {
630
+ try {
631
+ await checkFiles(await readBundle(dir));
632
+ return true;
633
+ } catch (error) {
634
+ if (error instanceof RpcError && error.diagnostic.code === "MOSTRA_ENV_BUNDLE_INVALID") return false;
635
+ throw error;
636
+ }
637
+ }
638
+ /** The bundle at `dir` when it exists and its setup marker matches it. */
639
+ async function readyAt(dir) {
640
+ const bundle = await readBundle(dir).catch(() => void 0);
641
+ if (bundle === void 0) return void 0;
642
+ return await readSetup(bundle) ? bundle : void 0;
643
+ }
644
+ async function finish(dir, options) {
645
+ await setupBundle(dir, {
646
+ env: options.env,
647
+ signal: options.signal,
648
+ filesChecked: options.filesChecked,
649
+ onStep: (step) => options.onProgress?.({ step })
650
+ });
651
+ return readBundle(dir);
652
+ }
653
+ /** Downloads, extracts and checks the archive, then renames it into `dir`. */
654
+ async function fetchInto(pins, asset, dir, bundles, options) {
655
+ await requireSpace(bundles, asset);
656
+ const scratch = join(bundles, `.tmp-${process.pid}-${randomBytes(6).toString("hex")}`);
657
+ await mkdir(scratch, { mode: 448 });
658
+ try {
659
+ const archive = join(scratch, asset.file);
660
+ await download(`${withoutTrailingSlashes(options.env["MOSTRA_BUNDLE_MIRROR"] || pins.release)}/${encodeURIComponent(asset.file)}`, archive, asset, {
661
+ env: options.env,
662
+ signal: options.signal,
663
+ onProgress: (progress) => options.onProgress?.({
664
+ step: "download",
665
+ ...progress
666
+ })
667
+ });
668
+ options.onProgress?.({ step: "extract" });
669
+ const root = join(scratch, "root");
670
+ await mkdir(root);
671
+ const top = asset.file.replace(/\.tar\.gz$/, "");
672
+ if (top !== bundleAssetName(pins.version, options.platform).replace(/\.tar\.gz$/, "")) throw new RpcError(createDiagnostic("MOSTRA_ENV_BUNDLE_INVALID", { message: `The pinned archive ${asset.file} isn't Mostra ${pins.version}'s bundle for ${options.platform}.` }));
673
+ await extractBundle(archive, root, top);
674
+ await rm(archive, { force: true });
675
+ const bundle = await readBundle(root);
676
+ if (bundle.info.version !== pins.version || bundle.info.platform !== options.platform) throw new RpcError(createDiagnostic("MOSTRA_ENV_BUNDLE_INVALID", { message: `The archive holds Mostra ${bundle.info.version} for ${bundle.info.platform}, not ${pins.version} for ${options.platform}.` }));
677
+ options.onProgress?.({ step: "files" });
678
+ await checkFiles(bundle);
679
+ try {
680
+ await rename(root, dir);
681
+ } catch (error) {
682
+ const code = error.code;
683
+ if (!(code === "ENOTEMPTY" || code === "EEXIST") || !await exists(dir)) throw error;
684
+ }
685
+ } finally {
686
+ await rm(scratch, {
687
+ recursive: true,
688
+ force: true
689
+ });
690
+ }
691
+ }
692
+ /** Fails early when the cache's file system can't hold the bundle. */
693
+ async function requireSpace(dir, asset) {
694
+ const fs = await statfs(dir).catch(() => void 0);
695
+ if (fs === void 0) return;
696
+ const free = Number(fs.bavail) * Number(fs.bsize);
697
+ const needed = asset.size + asset.unpackedSize + BROWSER_ALLOWANCE;
698
+ if (free < needed) {
699
+ const gib = (bytes) => `${(bytes / 1024 ** 3).toFixed(1)} GiB`;
700
+ throw new RpcError(createDiagnostic("MOSTRA_ENV_BUNDLE_DOWNLOAD_FAILED", { message: `The runtime bundle needs about ${gib(needed)} free in ${dir}, and ${gib(free)} is free (the disk is full).` }));
701
+ }
702
+ }
703
+ /** Removes temporary directories an interrupted install left, when their process is gone. */
704
+ async function sweep(bundles) {
705
+ for (const name of await readdir(bundles).catch(() => [])) {
706
+ const match = /^\.tmp-(\d+)-/.exec(name);
707
+ if (match && !alive(Number(match[1]))) await rm(join(bundles, name), {
708
+ recursive: true,
709
+ force: true
710
+ });
711
+ }
712
+ }
713
+ async function exists(path) {
714
+ return stat(path).then(() => true, () => false);
715
+ }
716
+ function alive(pid) {
717
+ try {
718
+ process.kill(pid, 0);
719
+ return true;
720
+ } catch (error) {
721
+ return error.code === "EPERM";
722
+ }
723
+ }
724
+ /** How old a lock may get before another installer takes it over whatever its owner. */
725
+ const STALE_LOCK_MS = 36e5;
726
+ /**
727
+ * How often a held lock is touched, so an install longer than {@link STALE_LOCK_MS} (a slow
728
+ * network, a large browser) is never taken from its live owner (Opus review of WP-12, finding 10).
729
+ */
730
+ const LOCK_TOUCH_MS = 6e4;
731
+ /** `url` without trailing slashes, in linear time (a regex would be quadratic on many slashes). */
732
+ function withoutTrailingSlashes(url) {
733
+ let end = url.length;
734
+ while (end > 0 && url[end - 1] === "/") end -= 1;
735
+ return url.slice(0, end);
736
+ }
737
+ /**
738
+ * Removes the lock at `path` when it is stale (its owner gone, or older than an hour) and says
739
+ * whether it did. The lock is read and judged through one descriptor, then moved aside, and only
740
+ * the file judged is removed: a fresh lock another installer took in between is put back (or,
741
+ * if a third took one meanwhile, left to the next round). The lock only saves a second download;
742
+ * the scratch directory and its rename keep the cache correct whatever happens here.
743
+ */
744
+ async function takeOverIfStale(path) {
745
+ let handle;
746
+ try {
747
+ handle = await open(path, "r");
748
+ } catch {
749
+ return true;
750
+ }
751
+ let judged;
752
+ try {
753
+ const info = await handle.stat();
754
+ const owner = Number.parseInt(await handle.readFile("utf8"), 10);
755
+ if (!(Date.now() - info.mtimeMs > STALE_LOCK_MS || Number.isInteger(owner) && owner > 0 && !alive(owner))) return false;
756
+ judged = {
757
+ ino: info.ino,
758
+ dev: info.dev
759
+ };
760
+ } finally {
761
+ await handle.close();
762
+ }
763
+ const aside = `${path}.stale-${randomBytes(6).toString("hex")}`;
764
+ try {
765
+ await rename(path, aside);
766
+ } catch {
767
+ return true;
768
+ }
769
+ const moved = await stat(aside).catch(() => void 0);
770
+ if (moved && (moved.ino !== judged.ino || moved.dev !== judged.dev)) await link(aside, path).catch(() => void 0);
771
+ await rm(aside, { force: true });
772
+ return true;
773
+ }
774
+ /**
775
+ * Takes the install lock at `path`: an exclusive create holding the owner's PID. A lock whose
776
+ * owner is gone, or older than an hour, is taken over; otherwise the caller waits for it.
777
+ */
778
+ async function lock(path, signal) {
779
+ const token = randomBytes(8).toString("hex");
780
+ for (;;) {
781
+ signal?.throwIfAborted();
782
+ try {
783
+ const handle = await open(path, "wx", 384);
784
+ await handle.writeFile(`${process.pid} ${token}\n`);
785
+ await handle.close();
786
+ const touch = setInterval(() => {
787
+ readFile(path, "utf8").then((text) => {
788
+ if (text.includes(token)) return utimes(path, /* @__PURE__ */ new Date(), /* @__PURE__ */ new Date());
789
+ }).catch(() => void 0);
790
+ }, LOCK_TOUCH_MS);
791
+ touch.unref();
792
+ return async () => {
793
+ clearInterval(touch);
794
+ const aside = `${path}.done-${randomBytes(6).toString("hex")}`;
795
+ if (!await rename(path, aside).then(() => true, () => false)) return;
796
+ if (!(await readFile(aside, "utf8").catch(() => "")).includes(token)) await link(aside, path).catch(() => void 0);
797
+ await rm(aside, { force: true });
798
+ };
799
+ } catch (error) {
800
+ if (error.code !== "EEXIST") throw error;
801
+ }
802
+ if (await takeOverIfStale(path)) continue;
803
+ await new Promise((done) => setTimeout(done, 250));
804
+ }
805
+ }
806
+ //#endregion
807
+ //#region src/platform.ts
808
+ /**
809
+ * Which runtime bundle this machine takes (RT-01, D35): one per operating system and architecture
810
+ * of v1, glibc only on Linux. Anything else fails closed before a download starts.
811
+ */
812
+ /**
813
+ * The glibc release Node.js reports, read from its diagnostic report's header. The report runs in
814
+ * the caller's process (a host's server, say) and by default resolves the name of every open TCP
815
+ * handle, synchronously; with `excludeNetwork` it does none of that (Opus review finding 4), as
816
+ * detect-libc does. The setting is restored after.
817
+ */
818
+ function glibcRuntime(report) {
819
+ if (report === void 0) return void 0;
820
+ const before = report.excludeNetwork ?? false;
821
+ report.excludeNetwork = true;
822
+ try {
823
+ return report.getReport().header?.glibcVersionRuntime;
824
+ } finally {
825
+ report.excludeNetwork = before;
826
+ }
827
+ }
828
+ let facts;
829
+ /** The facts of the running process, read once. */
830
+ function currentPlatform() {
831
+ facts ??= process.platform === "linux" ? {
832
+ platform: process.platform,
833
+ arch: process.arch,
834
+ glibc: glibcRuntime(process.report)
835
+ } : {
836
+ platform: process.platform,
837
+ arch: process.arch
838
+ };
839
+ return facts;
840
+ }
841
+ /**
842
+ * The bundle platform for these facts. Throws `MOSTRA_PLATFORM_UNSUPPORTED` for Windows, other
843
+ * architectures and Linux without glibc, whose libraries the bundle's Node.js and browser need.
844
+ */
845
+ function bundlePlatform(facts = currentPlatform()) {
846
+ const name = `${facts.platform}-${facts.arch}`;
847
+ const known = BUNDLE_PLATFORMS.includes(name);
848
+ if (known && facts.platform === "linux" && facts.glibc === void 0) throw unsupported(`Mostra's Linux bundle needs glibc, and this system has none (musl, as on Alpine Linux); it supports macOS and glibc-based Linux on x64 and arm64.`);
849
+ if (!known) throw unsupported(`Mostra doesn't run on ${facts.platform === "win32" ? `Windows (${name})` : name}; it supports macOS and Linux on x64 and arm64.`);
850
+ return name;
851
+ }
852
+ function unsupported(message) {
853
+ return new RpcError(createDiagnostic("MOSTRA_PLATFORM_UNSUPPORTED", { message }));
854
+ }
855
+ //#endregion
856
+ //#region src/version.ts
857
+ /** The SDK's own package: its root directory and its release. */
858
+ let found;
859
+ function own() {
860
+ if (found) return found;
861
+ let dir = dirname(fileURLToPath(import.meta.url));
862
+ for (;;) {
863
+ try {
864
+ const manifest = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
865
+ if (manifest.name === "@mostra/sdk" && manifest.version) {
866
+ found = {
867
+ root: dir,
868
+ version: manifest.version
869
+ };
870
+ return found;
871
+ }
872
+ } catch {}
873
+ const parent = dirname(dir);
874
+ if (parent === dir) throw new Error("Couldn't find @mostra/sdk's package.json.");
875
+ dir = parent;
876
+ }
877
+ }
878
+ /** This SDK's release: the client version an SDK host reports by default. */
879
+ function sdkVersion() {
880
+ return own().version;
881
+ }
882
+ /** The SDK package's directory; `dist/bundles.json` lives under it in a release. */
883
+ function packageRoot() {
884
+ return own().root;
885
+ }
886
+ //#endregion
887
+ //#region src/runtime.ts
888
+ /**
889
+ * Which runtime a host runs (RT-01, D85). In order:
890
+ *
891
+ * 1. `bundle`, a directory the caller names (the SDK's option), then `MOSTRA_BUNDLE`;
892
+ * 2. `MOSTRA_RUNTIME_BIN`, a `mostra-runtime` entry run with this process's Node (tests and
893
+ * development);
894
+ * 3. the bundle this package itself sits in (the CLI inside a bundle);
895
+ * 4. the bundle this package's release pins (`bundles.json`), from the cache or downloaded;
896
+ * 5. a development checkout's built runtime beside this package.
897
+ *
898
+ * Anything a bundle supplies is checked first: its setup marker must match it, or setup runs.
899
+ */
900
+ /**
901
+ * Where the pins are read from: `<package>/dist/bundles.json`, which the release writes before
902
+ * publishing. `MOSTRA_TEST_BUNDLE_PINS` names another file only for a package that runs from a
903
+ * development checkout (its real path outside any `node_modules`), where the tests use it: an
904
+ * installed copy (npm's `node_modules`, or a bundle's `lib/node_modules`) ignores it, so a
905
+ * variable leaked into a user's environment can't swap the pins npm's provenance covers (Opus
906
+ * review finding 8).
907
+ */
908
+ function pinsFile(env, root) {
909
+ const override = env["MOSTRA_TEST_BUNDLE_PINS"];
910
+ const installed = root.split(sep).includes("node_modules");
911
+ return override && !installed ? override : join(root, "dist", "bundles.json");
912
+ }
913
+ /** The pins this build carries: `dist/bundles.json` (a test override applies only in a development checkout). */
914
+ function readPins(env) {
915
+ const file = pinsFile(env, packageRoot());
916
+ if (!existsSync(file)) return void 0;
917
+ let data;
918
+ try {
919
+ data = JSON.parse(readFileSync(file, "utf8"));
920
+ } catch {
921
+ data = void 0;
922
+ }
923
+ const parsed = BundlePins.safeParse(data);
924
+ if (!parsed.success) throw new RpcError(createDiagnostic("MOSTRA_ENV_BUNDLE_INVALID", { message: `${file} isn't a release's bundle pins.` }));
925
+ return parsed.data;
926
+ }
927
+ /** The bundle this package was installed into, if any: `<root>/lib/node_modules/@mostra/sdk`. */
928
+ function enclosingBundle() {
929
+ const root = resolve(packageRoot(), "..", "..", "..", "..");
930
+ return existsSync(join(root, "bundle.json")) && existsSync(join(root, "lib", "node_modules", "@mostra", "sdk")) ? root : void 0;
931
+ }
932
+ /** The runtime described by a bundle, once it is set up. */
933
+ async function fromBundle(root, options) {
934
+ let bundle = await readBundle(resolve(root));
935
+ if (!await readSetup(bundle)) {
936
+ await setupBundle(bundle.root, {
937
+ env: options.env,
938
+ signal: options.signal,
939
+ onStep: (step) => options.onProgress?.({ step })
940
+ });
941
+ bundle = await readBundle(bundle.root);
942
+ }
943
+ return runtimeOf(bundle);
944
+ }
945
+ /** A bundle's entry points as a runtime. */
946
+ function runtimeOf(bundle) {
947
+ const { root, info } = bundle;
948
+ return {
949
+ source: "bundle",
950
+ node: join(root, info.node.path),
951
+ runtimeEntry: join(root, info.entries.runtime),
952
+ cliEntry: join(root, info.entries.cli),
953
+ bundle
954
+ };
955
+ }
956
+ /** A development checkout's runtime: `packages/runtime/dist/bin.js` beside this package. */
957
+ function development() {
958
+ const root = packageRoot();
959
+ const entry = resolve(root, "..", "runtime", "dist", "bin.js");
960
+ if (!existsSync(entry)) return void 0;
961
+ const cli = resolve(root, "..", "cli", "dist", "bin.js");
962
+ return {
963
+ source: "development",
964
+ node: process.execPath,
965
+ runtimeEntry: entry,
966
+ ...existsSync(cli) ? { cliEntry: cli } : {}
967
+ };
968
+ }
969
+ /**
970
+ * Finds the runtime to spawn, downloading and setting up the pinned bundle when nothing nearer
971
+ * exists. Throws `MOSTRA_PLATFORM_UNSUPPORTED`, `MOSTRA_ENV_RUNTIME_MISSING` (a
972
+ * `MOSTRA_RUNTIME_BIN` that doesn't exist), `MOSTRA_ENV_BUNDLE_UNAVAILABLE`,
973
+ * `MOSTRA_ENV_BUNDLE_INVALID` or `MOSTRA_ENV_BUNDLE_DOWNLOAD_FAILED`.
974
+ */
975
+ async function resolveRuntime(options = {}) {
976
+ const env = options.env ?? process.env;
977
+ const named = options.bundle ?? (env["MOSTRA_BUNDLE"] || void 0);
978
+ if (named !== void 0) return fromBundle(named, {
979
+ ...options,
980
+ env
981
+ });
982
+ const override = env["MOSTRA_RUNTIME_BIN"];
983
+ if (override) {
984
+ if (!existsSync(override)) throw new RpcError(createDiagnostic("MOSTRA_ENV_RUNTIME_MISSING", { message: `MOSTRA_RUNTIME_BIN names ${override}, which doesn't exist.` }));
985
+ return {
986
+ source: "override",
987
+ node: process.execPath,
988
+ runtimeEntry: override
989
+ };
990
+ }
991
+ const enclosing = enclosingBundle();
992
+ if (enclosing !== void 0) return fromBundle(enclosing, {
993
+ ...options,
994
+ env
995
+ });
996
+ const pins = readPins(env);
997
+ if (pins !== void 0) return runtimeOf(await installPinned(pins, {
998
+ env,
999
+ platform: bundlePlatform(),
1000
+ signal: options.signal,
1001
+ onProgress: options.onProgress
1002
+ }));
1003
+ const checkout = development();
1004
+ if (checkout !== void 0) return checkout;
1005
+ throw new RpcError(createDiagnostic("MOSTRA_ENV_BUNDLE_UNAVAILABLE", { message: "This build of @mostra/sdk pins no runtime bundle and has no runtime beside it; set MOSTRA_BUNDLE to an extracted bundle." }));
1006
+ }
1007
+ //#endregion
1008
+ export { safeRelative as C, linkStaysInside as S, setupBundle as _, sdkVersion as a, describeTree as b, bundleDir as c, browsersDir as d, checkBrowser as f, readSetup as g, readBundle as h, runtimeOf as i, cacheRoot as l, offline as m, readPins as n, bundlePlatform as o, checkFiles as p, resolveRuntime as r, currentPlatform as s, enclosingBundle as t, installPinned as u, setupExtras as v, redactUrl as w, hashFile as x, checkTree as y };
1009
+
1010
+ //# sourceMappingURL=runtime-CUbnK5Za.js.map