promigence 0.1.0-beta.12
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/NOTICE +14 -0
- package/README.md +88 -0
- package/dist/account.js +850 -0
- package/dist/agent.js +374 -0
- package/dist/argv.js +159 -0
- package/dist/bin.js +332 -0
- package/dist/bundle.js +133 -0
- package/dist/capacity.js +69 -0
- package/dist/commands.d.ts +110 -0
- package/dist/commands.js +2675 -0
- package/dist/connectreq.js +125 -0
- package/dist/credfile.js +188 -0
- package/dist/credits.js +197 -0
- package/dist/devcontainer.d.ts +99 -0
- package/dist/devcontainer.js +744 -0
- package/dist/device.js +129 -0
- package/dist/dirsnap.js +378 -0
- package/dist/errfix.js +262 -0
- package/dist/generated.d.ts +35 -0
- package/dist/generated.js +307 -0
- package/dist/github.js +307 -0
- package/dist/gitlab.js +210 -0
- package/dist/guide.d.ts +19 -0
- package/dist/guide.js +366 -0
- package/dist/handlers.js +511 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +7 -0
- package/dist/init.d.ts +40 -0
- package/dist/init.js +352 -0
- package/dist/install.d.ts +23 -0
- package/dist/install.js +127 -0
- package/dist/io.d.ts +37 -0
- package/dist/io.js +318 -0
- package/dist/live.js +469 -0
- package/dist/login.js +1063 -0
- package/dist/mcp.js +889 -0
- package/dist/money.js +5 -0
- package/dist/orgs.js +475 -0
- package/dist/pricing.d.ts +52 -0
- package/dist/pricing.js +181 -0
- package/dist/report.js +132 -0
- package/dist/routes.js +135 -0
- package/dist/runners-github.js +257 -0
- package/dist/runners-gitlab.js +125 -0
- package/dist/runners-spec.js +137 -0
- package/dist/runners.js +1458 -0
- package/dist/sdk/blake3.d.ts +10 -0
- package/dist/sdk/blake3.js +111 -0
- package/dist/sdk/bundle.d.ts +96 -0
- package/dist/sdk/bundle.js +227 -0
- package/dist/sdk/ca-guard.js +166 -0
- package/dist/sdk/caller.d.ts +73 -0
- package/dist/sdk/caller.js +47 -0
- package/dist/sdk/client.d.ts +3015 -0
- package/dist/sdk/client.js +2583 -0
- package/dist/sdk/command-errors.d.ts +41 -0
- package/dist/sdk/command-errors.js +36 -0
- package/dist/sdk/context.d.ts +119 -0
- package/dist/sdk/context.js +588 -0
- package/dist/sdk/core.d.ts +285 -0
- package/dist/sdk/core.js +266 -0
- package/dist/sdk/credstore.d.ts +90 -0
- package/dist/sdk/credstore.js +206 -0
- package/dist/sdk/dispatcher.js +86 -0
- package/dist/sdk/github.d.ts +158 -0
- package/dist/sdk/github.js +78 -0
- package/dist/sdk/gitlab.d.ts +104 -0
- package/dist/sdk/gitlab.js +20 -0
- package/dist/sdk/handles.d.ts +82 -0
- package/dist/sdk/handles.js +135 -0
- package/dist/sdk/hint.js +7 -0
- package/dist/sdk/http2.js +1921 -0
- package/dist/sdk/index.d.ts +34 -0
- package/dist/sdk/index.js +15 -0
- package/dist/sdk/keyed.gen.js +7 -0
- package/dist/sdk/models.gen.d.ts +6162 -0
- package/dist/sdk/models.gen.js +1 -0
- package/dist/sdk/proxy.js +249 -0
- package/dist/sdk/pty.d.ts +171 -0
- package/dist/sdk/pty.js +496 -0
- package/dist/sdk/sandbox.d.ts +537 -0
- package/dist/sdk/sandbox.js +725 -0
- package/dist/sdk/ssh.d.ts +25 -0
- package/dist/sdk/ssh.js +114 -0
- package/dist/sdk/trace.js +80 -0
- package/dist/sdk/transport-worker.js +466 -0
- package/dist/sdk/warm.js +40 -0
- package/dist/ssh.js +267 -0
- package/dist/surface.d.ts +95 -0
- package/dist/surface.js +1452 -0
- package/dist/templates.js +53 -0
- package/dist/term.js +50 -0
- package/dist/timing.js +71 -0
- package/dist/untar.js +324 -0
- package/dist/uploadguard.js +75 -0
- package/llms.txt +209 -0
- package/package.json +52 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { PromigenceClient } from "./sdk/index.js";
|
|
2
|
+
import { bool, clientOpts, nexts, positional } from "./commands.js";
|
|
3
|
+
import { emit, okEnvelope, recordNotice } from "./io.js";
|
|
4
|
+
import { sh } from "./term.js";
|
|
5
|
+
function client(ctx) {
|
|
6
|
+
return new PromigenceClient({ ...clientOpts(ctx), onNotice: recordNotice });
|
|
7
|
+
}
|
|
8
|
+
const templatesList = async (ctx) => {
|
|
9
|
+
const all = await client(ctx).templates.list();
|
|
10
|
+
const first = all[0];
|
|
11
|
+
const next = nexts(...(first
|
|
12
|
+
? [[sh `promigence templates show ${first.name}`, "what it has installed, and its versions"],
|
|
13
|
+
[sh `promigence fork --snapshot ${first.name} --count 1 --cap 1`, "start a sandbox from it"]]
|
|
14
|
+
: [["promigence examples", "what forks with no build"]]));
|
|
15
|
+
const width = Math.max(4, ...all.map((t) => t.name.length));
|
|
16
|
+
const lines = [
|
|
17
|
+
all.length ? `templates: ${all.length}` : "templates: none published yet",
|
|
18
|
+
...all.map((t) => ` ${t.name.padEnd(width)} v${t.version} ${t.category.padEnd(9)} ${t.summary}`),
|
|
19
|
+
];
|
|
20
|
+
return emit(ctx.mode, okEnvelope(ctx.cmd.name, { templates: all, count: all.length }, next), lines, 0, ctx.pointer);
|
|
21
|
+
};
|
|
22
|
+
const templatesShow = async (ctx) => {
|
|
23
|
+
const name = positional(ctx, "a template's name", "promigence templates show python-3.12");
|
|
24
|
+
const t = await client(ctx).templates.get(name);
|
|
25
|
+
const all = bool(ctx, "--packages");
|
|
26
|
+
const deps = t.dependencies;
|
|
27
|
+
const managers = Object.entries(deps?.packages ?? {});
|
|
28
|
+
const lines = [
|
|
29
|
+
`${t.title} (${t.name}) — version ${t.version}`,
|
|
30
|
+
` ${t.summary}`,
|
|
31
|
+
` starts from ${t.image}`,
|
|
32
|
+
` tier ${t.tier}`,
|
|
33
|
+
` verify ${t.verify}`,
|
|
34
|
+
...(t.setup.length ? [" setup", ...t.setup.map((s) => ` $ ${s}`)] : []),
|
|
35
|
+
...(deps
|
|
36
|
+
? [
|
|
37
|
+
` installed ${deps.os}${deps.arch ? ` (${deps.arch})` : ""}`,
|
|
38
|
+
...Object.entries(deps.tools ?? {}).map(([k, v]) => ` ${k.padEnd(12)} ${v}`),
|
|
39
|
+
...managers.map(([m, pkgs]) => all
|
|
40
|
+
? ` ${m}: ${pkgs.map((p) => `${p.name} ${p.version}`).join(", ")}`
|
|
41
|
+
: ` ${m}: ${pkgs.length} package(s)${pkgs.length ? " (--packages lists them)" : ""}`),
|
|
42
|
+
]
|
|
43
|
+
: [" installed not recorded for this version"]),
|
|
44
|
+
" versions",
|
|
45
|
+
...t.versions.map((v) => ` v${v.version}${v.current ? " (current)" : ""}${v.security ? " security" : ""} ${v.published_at ?? ""} ${v.notes}`),
|
|
46
|
+
];
|
|
47
|
+
const next = nexts([sh `promigence fork --snapshot ${t.name} --count 1 --cap 1`, "start a sandbox from it"], ["promigence templates", "every template"]);
|
|
48
|
+
return emit(ctx.mode, okEnvelope(ctx.cmd.name, { template: t }, next), lines, 0, ctx.pointer);
|
|
49
|
+
};
|
|
50
|
+
export const TEMPLATES_HANDLERS = {
|
|
51
|
+
"templates": templatesList,
|
|
52
|
+
"templates show": templatesShow,
|
|
53
|
+
};
|
package/dist/term.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const BIDI = "\u202a-\u202e\u2066-\u2069";
|
|
2
|
+
const CONTROLS = new RegExp(`[\\u0000-\\u0008\\u000b-\\u001f\\u007f-\\u009f${BIDI}]`, "g");
|
|
3
|
+
const JSON_RAW = new RegExp(`[\\u007f-\\u009f${BIDI}]`, "g");
|
|
4
|
+
export function escapeControls(text) {
|
|
5
|
+
return text.replace(CONTROLS, (c) => {
|
|
6
|
+
const n = c.charCodeAt(0);
|
|
7
|
+
if (n === 0x0d)
|
|
8
|
+
return "\\r";
|
|
9
|
+
if (n === 0x7f)
|
|
10
|
+
return "^?";
|
|
11
|
+
if (n < 0x20)
|
|
12
|
+
return `^${String.fromCharCode(n + 0x40)}`;
|
|
13
|
+
if (n <= 0x9f)
|
|
14
|
+
return `\\x${n.toString(16)}`;
|
|
15
|
+
return `\\u${n.toString(16)}`;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export function escapeField(text) {
|
|
19
|
+
return escapeControls(text).replace(/\n/g, "\\n");
|
|
20
|
+
}
|
|
21
|
+
export function fieldForTerminal(stream, text) {
|
|
22
|
+
return stream.isTTY ? escapeField(text) : text;
|
|
23
|
+
}
|
|
24
|
+
export function jsonForTerminal(stream, json) {
|
|
25
|
+
if (!stream.isTTY)
|
|
26
|
+
return json;
|
|
27
|
+
return json.replace(JSON_RAW, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
28
|
+
}
|
|
29
|
+
export function jsonLine(stream, value) {
|
|
30
|
+
return jsonForTerminal(stream, JSON.stringify(value)) + "\n";
|
|
31
|
+
}
|
|
32
|
+
export function forTerminal(stream, text) {
|
|
33
|
+
return stream.isTTY ? escapeControls(text) : text;
|
|
34
|
+
}
|
|
35
|
+
export function escapeLine(line) {
|
|
36
|
+
return escapeControls(line.endsWith("\r") ? line.slice(0, -1) : line);
|
|
37
|
+
}
|
|
38
|
+
const PLAIN_ARG = /^[A-Za-z0-9_./:@,][A-Za-z0-9_./:@,=-]*$/;
|
|
39
|
+
export function shellArg(value) {
|
|
40
|
+
const s = String(value);
|
|
41
|
+
if (PLAIN_ARG.test(s))
|
|
42
|
+
return s;
|
|
43
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
44
|
+
}
|
|
45
|
+
export function sh(strings, ...values) {
|
|
46
|
+
let out = strings[0];
|
|
47
|
+
for (let i = 0; i < values.length; i++)
|
|
48
|
+
out += shellArg(values[i]) + strings[i + 1];
|
|
49
|
+
return out;
|
|
50
|
+
}
|
package/dist/timing.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
const seen = [];
|
|
5
|
+
export function recordTiming(t) {
|
|
6
|
+
seen.push(t);
|
|
7
|
+
}
|
|
8
|
+
export function timings() {
|
|
9
|
+
return seen.filter((t) => t.ourMs !== null);
|
|
10
|
+
}
|
|
11
|
+
export function networkMs(list = timings()) {
|
|
12
|
+
let best;
|
|
13
|
+
for (const t of list) {
|
|
14
|
+
if (t.ourMs === null)
|
|
15
|
+
continue;
|
|
16
|
+
const n = Math.max(0, t.totalMs - t.ourMs);
|
|
17
|
+
if (best === undefined || n < best)
|
|
18
|
+
best = n;
|
|
19
|
+
}
|
|
20
|
+
return best;
|
|
21
|
+
}
|
|
22
|
+
export function ourSideLine(ourMs, totalMs) {
|
|
23
|
+
const ours = `${fmtMs(ourMs)} on our side`;
|
|
24
|
+
return totalMs === undefined ? ours : `${ours} · ${fmtMs(totalMs)} round trip`;
|
|
25
|
+
}
|
|
26
|
+
export function fmtMs(ms) {
|
|
27
|
+
return ms >= 10 ? `${Math.round(ms)} ms` : `${Math.round(ms * 10) / 10} ms`;
|
|
28
|
+
}
|
|
29
|
+
export function where(location) {
|
|
30
|
+
return location ? `our servers on the ${location}` : "our servers";
|
|
31
|
+
}
|
|
32
|
+
export const FAR_MS = 150;
|
|
33
|
+
export function farMarker(env = process.env) {
|
|
34
|
+
const base = env["XDG_CONFIG_HOME"] ? join(env["XDG_CONFIG_HOME"], "promigence") : join(homedir(), ".config", "promigence");
|
|
35
|
+
return join(base, "far-notice-shown");
|
|
36
|
+
}
|
|
37
|
+
export function farNoticeText(ms, location) {
|
|
38
|
+
return `promigence: You're about ${Math.round(ms)} ms from ${where(location)}. ` +
|
|
39
|
+
"Times marked 'on our side' are the platform's own; the rest is your network. " +
|
|
40
|
+
"(Shown once. `promigence ping` measures it again.)\n";
|
|
41
|
+
}
|
|
42
|
+
export async function maybeFarNotice(opts) {
|
|
43
|
+
const env = opts.env ?? process.env;
|
|
44
|
+
if (opts.json || env["PROMIGENCE_QUIET"] === "1")
|
|
45
|
+
return undefined;
|
|
46
|
+
if (!(opts.isTTY ?? process.stderr.isTTY))
|
|
47
|
+
return undefined;
|
|
48
|
+
const ms = networkMs(opts.list ?? timings());
|
|
49
|
+
if (ms === undefined || ms < FAR_MS)
|
|
50
|
+
return undefined;
|
|
51
|
+
const marker = farMarker(env);
|
|
52
|
+
if (existsSync(marker))
|
|
53
|
+
return undefined;
|
|
54
|
+
const loc = await opts.location().catch(() => undefined);
|
|
55
|
+
const text = farNoticeText(ms, loc);
|
|
56
|
+
(opts.write ?? ((s) => process.stderr.write(s)))(text);
|
|
57
|
+
try {
|
|
58
|
+
mkdirSync(join(marker, ".."), { recursive: true, mode: 0o700 });
|
|
59
|
+
writeFileSync(marker, `${new Date().toISOString()} ${Math.round(ms)}\n`, { mode: 0o600 });
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
}
|
|
63
|
+
return text;
|
|
64
|
+
}
|
|
65
|
+
export function median(xs) {
|
|
66
|
+
if (!xs.length)
|
|
67
|
+
return undefined;
|
|
68
|
+
const s = [...xs].sort((a, b) => a - b);
|
|
69
|
+
const m = s.length >> 1;
|
|
70
|
+
return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
|
|
71
|
+
}
|
package/dist/untar.js
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { closeSync, constants, futimesSync, lstatSync, mkdirSync, openSync, readSync, readlinkSync, realpathSync, symlinkSync, unlinkSync, utimesSync, writeSync, } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
const BLOCK = 512;
|
|
4
|
+
const MAX_META = 64 * 1024;
|
|
5
|
+
const MAX_HOPS = 40;
|
|
6
|
+
export class UnsafeArchiveError extends Error {
|
|
7
|
+
refused;
|
|
8
|
+
constructor(refused) {
|
|
9
|
+
super(`the archive holds ${refused.length} entr${refused.length === 1 ? "y" : "ies"} that would write outside the destination or are not plain files: ` +
|
|
10
|
+
refused.slice(0, 5).map((r) => `${r.path} (${r.reason})`).join("; ") +
|
|
11
|
+
(refused.length > 5 ? `; and ${refused.length - 5} more` : ""));
|
|
12
|
+
this.refused = refused;
|
|
13
|
+
this.name = "UnsafeArchiveError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function cstr(b, from, to) {
|
|
17
|
+
const s = b.subarray(from, to);
|
|
18
|
+
const z = s.indexOf(0);
|
|
19
|
+
return (z < 0 ? s : s.subarray(0, z)).toString("utf8");
|
|
20
|
+
}
|
|
21
|
+
function num(b, from, to) {
|
|
22
|
+
const f = b.subarray(from, to);
|
|
23
|
+
if (f.length > 0 && (f[0] & 0x80) !== 0) {
|
|
24
|
+
let v = f[0] & 0x7f;
|
|
25
|
+
for (let i = 1; i < f.length; i++)
|
|
26
|
+
v = v * 256 + f[i];
|
|
27
|
+
return v;
|
|
28
|
+
}
|
|
29
|
+
const s = cstr(b, from, to).trim();
|
|
30
|
+
if (s === "")
|
|
31
|
+
return 0;
|
|
32
|
+
if (!/^[0-7]+$/.test(s))
|
|
33
|
+
throw new Error(`a tar header field is not a number: ${JSON.stringify(s)}`);
|
|
34
|
+
return parseInt(s, 8);
|
|
35
|
+
}
|
|
36
|
+
function checksumOk(h) {
|
|
37
|
+
const want = num(h, 148, 156);
|
|
38
|
+
let sum = 0;
|
|
39
|
+
for (let i = 0; i < BLOCK; i++)
|
|
40
|
+
sum += i >= 148 && i < 156 ? 0x20 : h[i];
|
|
41
|
+
return sum === want;
|
|
42
|
+
}
|
|
43
|
+
function pax(data) {
|
|
44
|
+
const out = {};
|
|
45
|
+
let i = 0;
|
|
46
|
+
while (i < data.length) {
|
|
47
|
+
const sp = data.indexOf(0x20, i);
|
|
48
|
+
if (sp < 0)
|
|
49
|
+
break;
|
|
50
|
+
const len = parseInt(data.subarray(i, sp).toString("ascii"), 10);
|
|
51
|
+
if (!Number.isFinite(len) || len <= 0 || i + len > data.length)
|
|
52
|
+
break;
|
|
53
|
+
const rec = data.subarray(sp + 1, i + len - 1).toString("utf8");
|
|
54
|
+
const eq = rec.indexOf("=");
|
|
55
|
+
if (eq > 0)
|
|
56
|
+
out[rec.slice(0, eq)] = rec.slice(eq + 1);
|
|
57
|
+
i += len;
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
function readAt(fd, len, pos) {
|
|
62
|
+
const b = Buffer.alloc(len);
|
|
63
|
+
let got = 0;
|
|
64
|
+
while (got < len) {
|
|
65
|
+
const n = readSync(fd, b, got, len - got, pos + got);
|
|
66
|
+
if (n === 0)
|
|
67
|
+
break;
|
|
68
|
+
got += n;
|
|
69
|
+
}
|
|
70
|
+
return got === len ? b : b.subarray(0, got);
|
|
71
|
+
}
|
|
72
|
+
function relName(raw, strip) {
|
|
73
|
+
if (raw.startsWith("/") || /^[A-Za-z]:[\\/]/.test(raw))
|
|
74
|
+
return { reason: "an absolute name" };
|
|
75
|
+
if (raw.includes("\\"))
|
|
76
|
+
return { reason: "a backslash in the name" };
|
|
77
|
+
const parts = raw.split("/").filter((p) => p !== "" && p !== ".");
|
|
78
|
+
if (parts.some((p) => p === ".."))
|
|
79
|
+
return { reason: "a `..` in the name" };
|
|
80
|
+
const kept = parts.slice(strip);
|
|
81
|
+
return kept.length === 0 ? null : kept.join("/");
|
|
82
|
+
}
|
|
83
|
+
function scan(fd, strip) {
|
|
84
|
+
const entries = [];
|
|
85
|
+
const refused = [];
|
|
86
|
+
let pos = 0;
|
|
87
|
+
let longName = null;
|
|
88
|
+
let longLink = null;
|
|
89
|
+
let paxRec = {};
|
|
90
|
+
for (;;) {
|
|
91
|
+
const h = readAt(fd, BLOCK, pos);
|
|
92
|
+
if (h.length < BLOCK)
|
|
93
|
+
throw new Error("the archive ends in the middle of a header");
|
|
94
|
+
if (h.every((x) => x === 0))
|
|
95
|
+
break;
|
|
96
|
+
if (!checksumOk(h))
|
|
97
|
+
throw new Error(`a tar header at byte ${pos} has a bad checksum`);
|
|
98
|
+
const flag = String.fromCharCode(h[156]);
|
|
99
|
+
let size = num(h, 124, 136);
|
|
100
|
+
const data = pos + BLOCK;
|
|
101
|
+
const advance = (n) => data + Math.ceil(n / BLOCK) * BLOCK;
|
|
102
|
+
if (flag === "L" || flag === "K" || flag === "x" || flag === "g") {
|
|
103
|
+
if (size > MAX_META)
|
|
104
|
+
throw new Error(`a tar metadata record of ${size} bytes is over ${MAX_META}`);
|
|
105
|
+
const meta = readAt(fd, size, data);
|
|
106
|
+
if (flag === "L")
|
|
107
|
+
longName = cstr(meta, 0, meta.length);
|
|
108
|
+
else if (flag === "K")
|
|
109
|
+
longLink = cstr(meta, 0, meta.length);
|
|
110
|
+
else if (flag === "x")
|
|
111
|
+
paxRec = pax(meta);
|
|
112
|
+
pos = advance(size);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const prefix = h.subarray(257, 262).toString("latin1") === "ustar" ? cstr(h, 345, 500) : "";
|
|
116
|
+
const short = cstr(h, 0, 100);
|
|
117
|
+
const raw = paxRec["path"] ?? longName ?? (prefix ? `${prefix}/${short}` : short);
|
|
118
|
+
const link = paxRec["linkpath"] ?? longLink ?? cstr(h, 157, 257);
|
|
119
|
+
if (paxRec["size"] !== undefined && /^\d+$/.test(paxRec["size"]))
|
|
120
|
+
size = Number(paxRec["size"]);
|
|
121
|
+
longName = null;
|
|
122
|
+
longLink = null;
|
|
123
|
+
paxRec = {};
|
|
124
|
+
const mode = num(h, 100, 108) & 0o777;
|
|
125
|
+
const mtime = num(h, 136, 148);
|
|
126
|
+
const name = relName(raw, strip);
|
|
127
|
+
const kind = flag === "0" || flag === "\0" || flag === "7" ? "file"
|
|
128
|
+
: flag === "5" ? "dir"
|
|
129
|
+
: flag === "2" ? "symlink"
|
|
130
|
+
: flag === "1" ? { reason: "a hard link" }
|
|
131
|
+
: flag === "3" || flag === "4" ? { reason: "a device" }
|
|
132
|
+
: flag === "6" ? { reason: "a fifo" }
|
|
133
|
+
: { reason: `tar entry type ${JSON.stringify(flag)}` };
|
|
134
|
+
pos = advance(flag === "1" || flag === "2" ? 0 : size);
|
|
135
|
+
if (typeof kind === "object") {
|
|
136
|
+
refused.push({ path: raw, reason: kind.reason });
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (name === null)
|
|
140
|
+
continue;
|
|
141
|
+
if (typeof name === "object") {
|
|
142
|
+
refused.push({ path: raw, reason: name.reason });
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
entries.push({ kind, path: name, link, mode, mtime, size: kind === "file" ? size : 0, offset: data });
|
|
146
|
+
}
|
|
147
|
+
return { entries, refused };
|
|
148
|
+
}
|
|
149
|
+
function resolvesInside(root, at, target, links) {
|
|
150
|
+
if (target.startsWith("/") || /^[A-Za-z]:[\\/]/.test(target))
|
|
151
|
+
return false;
|
|
152
|
+
const stack = at.split("/").slice(0, -1);
|
|
153
|
+
const queue = target.split("/");
|
|
154
|
+
let hops = 0;
|
|
155
|
+
while (queue.length > 0) {
|
|
156
|
+
const c = queue.shift();
|
|
157
|
+
if (c === "" || c === ".")
|
|
158
|
+
continue;
|
|
159
|
+
if (c === "..") {
|
|
160
|
+
if (stack.length === 0)
|
|
161
|
+
return false;
|
|
162
|
+
stack.pop();
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
stack.push(c);
|
|
166
|
+
const cur = stack.join("/");
|
|
167
|
+
let next = links.get(cur);
|
|
168
|
+
if (next === undefined) {
|
|
169
|
+
try {
|
|
170
|
+
if (lstatSync(join(root, cur)).isSymbolicLink())
|
|
171
|
+
next = readlinkSync(join(root, cur));
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (next !== undefined) {
|
|
177
|
+
if (++hops > MAX_HOPS)
|
|
178
|
+
return false;
|
|
179
|
+
if (next.startsWith("/") || /^[A-Za-z]:[\\/]/.test(next))
|
|
180
|
+
return false;
|
|
181
|
+
stack.pop();
|
|
182
|
+
queue.unshift(...next.split("/"));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
function ensureDir(root, rel, made) {
|
|
188
|
+
if (rel === "" || made.has(rel))
|
|
189
|
+
return;
|
|
190
|
+
let cur = "";
|
|
191
|
+
for (const c of rel.split("/")) {
|
|
192
|
+
cur = cur ? `${cur}/${c}` : c;
|
|
193
|
+
if (made.has(cur))
|
|
194
|
+
continue;
|
|
195
|
+
const full = join(root, cur);
|
|
196
|
+
let st;
|
|
197
|
+
try {
|
|
198
|
+
st = lstatSync(full);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
mkdirSync(full, { mode: 0o755 });
|
|
202
|
+
made.add(cur);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (st.isSymbolicLink())
|
|
206
|
+
throw new UnsafeArchiveError([{ path: cur, reason: "the destination already holds a link here" }]);
|
|
207
|
+
if (!st.isDirectory())
|
|
208
|
+
throw new UnsafeArchiveError([{ path: cur, reason: "the destination already holds a file where the archive has a directory" }]);
|
|
209
|
+
made.add(cur);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function clearName(full, rel) {
|
|
213
|
+
let st;
|
|
214
|
+
try {
|
|
215
|
+
st = lstatSync(full);
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (st.isDirectory())
|
|
221
|
+
throw new UnsafeArchiveError([{ path: rel, reason: "the destination already holds a directory here" }]);
|
|
222
|
+
if (st.isSymbolicLink())
|
|
223
|
+
unlinkSync(full);
|
|
224
|
+
}
|
|
225
|
+
export function untarSafe(tarPath, dst, opts) {
|
|
226
|
+
const fd = openSync(tarPath, "r");
|
|
227
|
+
try {
|
|
228
|
+
const { entries, refused } = scan(fd, opts.strip);
|
|
229
|
+
const links = new Map();
|
|
230
|
+
for (const e of entries)
|
|
231
|
+
if (e.kind === "symlink")
|
|
232
|
+
links.set(e.path, e.link);
|
|
233
|
+
mkdirSync(dst, { recursive: true });
|
|
234
|
+
const root = realpathSync(dst);
|
|
235
|
+
for (const e of entries) {
|
|
236
|
+
const parts = e.path.split("/");
|
|
237
|
+
for (let i = 1; i < parts.length; i++) {
|
|
238
|
+
if (links.has(parts.slice(0, i).join("/"))) {
|
|
239
|
+
refused.push({ path: e.path, reason: "it is written through a link the same archive makes" });
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (e.kind === "symlink") {
|
|
244
|
+
if (entries.some((o) => o !== e && o.path === e.path && o.kind !== "symlink")) {
|
|
245
|
+
refused.push({ path: e.path, reason: "the archive names it both as a link and as a file or directory" });
|
|
246
|
+
}
|
|
247
|
+
else if (!opts.keepLinks && !resolvesInside(root, e.path, e.link, links)) {
|
|
248
|
+
refused.push({ path: e.path, reason: `a link to ${e.link}, outside the destination` });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (refused.length > 0)
|
|
253
|
+
throw new UnsafeArchiveError(refused);
|
|
254
|
+
const made = new Set();
|
|
255
|
+
const out = { files: 0, dirs: 0, links: 0, bytes: 0 };
|
|
256
|
+
const dirs = [];
|
|
257
|
+
const buf = Buffer.alloc(1 << 20);
|
|
258
|
+
const NOFOLLOW = constants["O_NOFOLLOW"] ?? 0;
|
|
259
|
+
for (const e of entries) {
|
|
260
|
+
if (e.kind === "dir") {
|
|
261
|
+
ensureDir(root, e.path, made);
|
|
262
|
+
dirs.push(e);
|
|
263
|
+
out.dirs++;
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (e.kind !== "file")
|
|
267
|
+
continue;
|
|
268
|
+
const parent = e.path.split("/").slice(0, -1).join("/");
|
|
269
|
+
ensureDir(root, parent, made);
|
|
270
|
+
const full = join(root, e.path);
|
|
271
|
+
clearName(full, e.path);
|
|
272
|
+
const w = openSync(full, constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | NOFOLLOW, e.mode | 0o600);
|
|
273
|
+
try {
|
|
274
|
+
let left = e.size;
|
|
275
|
+
let at = e.offset;
|
|
276
|
+
while (left > 0) {
|
|
277
|
+
const n = readSync(fd, buf, 0, Math.min(buf.length, left), at);
|
|
278
|
+
if (n === 0)
|
|
279
|
+
throw new Error(`the archive ends inside ${e.path}`);
|
|
280
|
+
let wrote = 0;
|
|
281
|
+
while (wrote < n)
|
|
282
|
+
wrote += writeSync(w, buf, wrote, n - wrote);
|
|
283
|
+
left -= n;
|
|
284
|
+
at += n;
|
|
285
|
+
}
|
|
286
|
+
if (e.mtime > 0)
|
|
287
|
+
futimesSync(w, e.mtime, e.mtime);
|
|
288
|
+
}
|
|
289
|
+
finally {
|
|
290
|
+
closeSync(w);
|
|
291
|
+
}
|
|
292
|
+
out.files++;
|
|
293
|
+
out.bytes += e.size;
|
|
294
|
+
}
|
|
295
|
+
for (const e of entries) {
|
|
296
|
+
if (e.kind !== "symlink")
|
|
297
|
+
continue;
|
|
298
|
+
const parent = e.path.split("/").slice(0, -1).join("/");
|
|
299
|
+
ensureDir(root, parent, made);
|
|
300
|
+
const full = join(root, e.path);
|
|
301
|
+
clearName(full, e.path);
|
|
302
|
+
try {
|
|
303
|
+
unlinkSync(full);
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
}
|
|
307
|
+
symlinkSync(e.link, full);
|
|
308
|
+
out.links++;
|
|
309
|
+
}
|
|
310
|
+
for (const d of dirs.reverse()) {
|
|
311
|
+
if (d.mtime > 0) {
|
|
312
|
+
try {
|
|
313
|
+
utimesSync(join(root, d.path), d.mtime, d.mtime);
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return out;
|
|
320
|
+
}
|
|
321
|
+
finally {
|
|
322
|
+
closeSync(fd);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
import { CREDENTIAL_CHECK_NOTE, credentialRefusalMessage } from "./sdk/index.js";
|
|
4
|
+
import { bool } from "./commands.js";
|
|
5
|
+
import { emit, okEnvelope, usageError, write } from "./io.js";
|
|
6
|
+
import { jsonLine } from "./term.js";
|
|
7
|
+
export const CREDENTIAL_REMEDY = {
|
|
8
|
+
dir: "Remove them from git (`git rm --cached FILE` and add them to .gitignore), pass the values as secrets (`promigence secrets set`), or pass --allow-secrets if they are not real credentials.",
|
|
9
|
+
dockerfile: "Exclude them in .dockerignore, pass the values as secrets (`promigence secrets set`), or pass --allow-secrets if they are not real credentials.",
|
|
10
|
+
};
|
|
11
|
+
export function credentialRefusal(hits, remedy) {
|
|
12
|
+
return usageError(credentialRefusalMessage(hits), `A snapshot keeps every file it holds, and every sandbox started from it has them. ${remedy} ${CREDENTIAL_CHECK_NOTE} --list-files prints everything that would be sent.`);
|
|
13
|
+
}
|
|
14
|
+
async function ask(prompt) {
|
|
15
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
16
|
+
try {
|
|
17
|
+
return await new Promise((resolve) => rl.question(prompt, resolve));
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
rl.close();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function confirmOverride(ctx, o, io = {}) {
|
|
24
|
+
if (o.items.length === 0)
|
|
25
|
+
return [];
|
|
26
|
+
const tty = io.tty ?? Boolean(process.stdin.isTTY && process.stderr.isTTY);
|
|
27
|
+
await write(process.stderr, ctx.mode === "json"
|
|
28
|
+
? jsonLine(process.stderr, { hint: "override", flag: o.flag, sending: o.items })
|
|
29
|
+
: `${o.flag}: sending ${o.items.length} ${o.what}:\n${o.items.map((i) => ` ${i}`).join("\n")}\n`);
|
|
30
|
+
if (!tty || bool(ctx, "--yes"))
|
|
31
|
+
return [...o.items];
|
|
32
|
+
const answer = await (io.ask ?? ask)(`Send ${o.items.length === 1 ? "it" : `these ${o.items.length}`}? [y/N] `);
|
|
33
|
+
if (/^\s*y(es)?\s*$/i.test(answer))
|
|
34
|
+
return [...o.items];
|
|
35
|
+
throw usageError(`not sent: ${o.flag} was not confirmed; nothing was created`, `Answer y to send them, or pass --yes with ${o.flag} to send them without the question.`);
|
|
36
|
+
}
|
|
37
|
+
function human(n) {
|
|
38
|
+
if (n < 1024)
|
|
39
|
+
return `${n} B`;
|
|
40
|
+
const u = ["KiB", "MiB", "GiB", "TiB"];
|
|
41
|
+
let v = n / 1024;
|
|
42
|
+
let i = 0;
|
|
43
|
+
while (v >= 1024 && i < u.length - 1) {
|
|
44
|
+
v /= 1024;
|
|
45
|
+
i++;
|
|
46
|
+
}
|
|
47
|
+
return `${v.toFixed(v < 10 ? 1 : 0)} ${u[i]}`;
|
|
48
|
+
}
|
|
49
|
+
export async function emitUploadList(ctx, l) {
|
|
50
|
+
const flagged = new Set(l.secrets);
|
|
51
|
+
return emit(ctx.mode, okEnvelope(ctx.cmd.name, {
|
|
52
|
+
sent: false,
|
|
53
|
+
list_files: {
|
|
54
|
+
kind: l.kind, root: l.root, files: l.files, bytes: l.bytes, paths: l.paths, secrets: l.secrets, notes: l.notes,
|
|
55
|
+
...(l.ignoreFile === undefined ? {} : { ignore_file: l.ignoreFile }),
|
|
56
|
+
},
|
|
57
|
+
}, []), [
|
|
58
|
+
`would upload ${l.files} file${l.files === 1 ? "" : "s"} (${human(l.bytes)}) from ${l.root}${l.ignoreFile ? `, after ${l.ignoreFile}` : ""}; nothing was sent:`,
|
|
59
|
+
...l.paths.map((p) => ` ${p}${flagged.has(p) ? " <- looks like a credential (refused unless --allow-secrets)" : ""}`),
|
|
60
|
+
...l.notes.map((n) => ` note: ${n}`),
|
|
61
|
+
...(l.secrets.length
|
|
62
|
+
? [` ${l.secrets.length} of them look${l.secrets.length === 1 ? "s" : ""} like credentials, refused unless --allow-secrets; the check goes by file name and is best-effort.`]
|
|
63
|
+
: []),
|
|
64
|
+
], 0, false);
|
|
65
|
+
}
|
|
66
|
+
export function tarNoMetadataArgs(version, platform = process.platform) {
|
|
67
|
+
if (!/bsdtar/i.test(version))
|
|
68
|
+
return [];
|
|
69
|
+
return platform === "darwin" ? ["--no-xattrs", "--no-mac-metadata"] : ["--no-xattrs"];
|
|
70
|
+
}
|
|
71
|
+
let localTarVersion;
|
|
72
|
+
export function localTarNoMetadataArgs() {
|
|
73
|
+
localTarVersion ??= spawnSync("tar", ["--version"], { encoding: "utf8" }).stdout ?? "";
|
|
74
|
+
return tarNoMetadataArgs(localTarVersion);
|
|
75
|
+
}
|