@toon-protocol/rig 4.1.0 → 4.3.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/README.md +194 -4
- package/dist/chunk-3RWM7PBM.js +3705 -0
- package/dist/chunk-3RWM7PBM.js.map +1 -0
- package/dist/chunk-5HFOPKAB.js +7668 -0
- package/dist/chunk-5HFOPKAB.js.map +1 -0
- package/dist/{chunk-NCEMQUPA.js → chunk-GWA3CSUR.js} +198 -6
- package/dist/chunk-GWA3CSUR.js.map +1 -0
- package/dist/chunk-U5A7MHBP.js +198 -0
- package/dist/chunk-U5A7MHBP.js.map +1 -0
- package/dist/{chunk-CDSFVZDV.js → chunk-UE5TAFEU.js} +3 -14
- package/dist/chunk-UE5TAFEU.js.map +1 -0
- package/dist/{chunk-YZRR2UO6.js → chunk-XCLUB2CY.js} +1 -183
- package/dist/chunk-XCLUB2CY.js.map +1 -0
- package/dist/ci-serve-ZVJBZ3W3.js +2303 -0
- package/dist/ci-serve-ZVJBZ3W3.js.map +1 -0
- package/dist/cli/rig.js +10 -7815
- package/dist/cli/rig.js.map +1 -1
- package/dist/index.d.ts +754 -6
- package/dist/index.js +170 -10
- package/dist/index.js.map +1 -1
- package/dist/{publisher-DMJ06cuL.d.ts → publisher-Cf6tKnZ3.d.ts} +4 -3
- package/dist/standalone/index.d.ts +1 -1
- package/dist/standalone/index.js +11 -10
- package/dist/{standalone-mode-KTYRTUS6.js → standalone-mode-JIEECDTI.js} +10 -11
- package/dist/{standalone-mode-KTYRTUS6.js.map → standalone-mode-JIEECDTI.js.map} +1 -1
- package/package.json +3 -2
- package/dist/chunk-7VCVR4GK.js +0 -1250
- package/dist/chunk-7VCVR4GK.js.map +0 -1
- package/dist/chunk-CDSFVZDV.js.map +0 -1
- package/dist/chunk-GVXBUL3C.js +0 -71
- package/dist/chunk-GVXBUL3C.js.map +0 -1
- package/dist/chunk-LTFMPOHU.js +0 -194
- package/dist/chunk-LTFMPOHU.js.map +0 -1
- package/dist/chunk-NCEMQUPA.js.map +0 -1
- package/dist/chunk-YZRR2UO6.js.map +0 -1
|
@@ -0,0 +1,3705 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ARWEAVE_FETCH_TIMEOUT_MS,
|
|
3
|
+
ARWEAVE_GATEWAYS,
|
|
4
|
+
MissingIdentityError,
|
|
5
|
+
authorizedStatusAuthors,
|
|
6
|
+
buildRepoAnnouncement,
|
|
7
|
+
buildRepoRefs,
|
|
8
|
+
defaultWebSocketFactory,
|
|
9
|
+
isValidArweaveTxId,
|
|
10
|
+
queryRelay,
|
|
11
|
+
resolveIdentity
|
|
12
|
+
} from "./chunk-GWA3CSUR.js";
|
|
13
|
+
import {
|
|
14
|
+
defaultDaemonPort,
|
|
15
|
+
standaloneForced,
|
|
16
|
+
uploadChargeFor
|
|
17
|
+
} from "./chunk-U5A7MHBP.js";
|
|
18
|
+
|
|
19
|
+
// src/objects.ts
|
|
20
|
+
import { createHash } from "crypto";
|
|
21
|
+
var FREE_TIER_MAX_ITEM_BYTES = 107520;
|
|
22
|
+
var MAX_OBJECT_SIZE = FREE_TIER_MAX_ITEM_BYTES;
|
|
23
|
+
var EMPTY_BLOB_SHA = "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391";
|
|
24
|
+
function hashGitObject(type, body) {
|
|
25
|
+
const header = Buffer.from(`${type} ${body.length}\0`);
|
|
26
|
+
const fullObject = Buffer.concat([header, body]);
|
|
27
|
+
const sha = createHash("sha1").update(fullObject).digest("hex");
|
|
28
|
+
return { sha, buffer: fullObject, body };
|
|
29
|
+
}
|
|
30
|
+
function createGitBlob(content) {
|
|
31
|
+
return hashGitObject("blob", Buffer.from(content, "utf-8"));
|
|
32
|
+
}
|
|
33
|
+
function createGitTree(entries) {
|
|
34
|
+
const sorted = [...entries].sort(
|
|
35
|
+
(a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0
|
|
36
|
+
);
|
|
37
|
+
const entryBuffers = [];
|
|
38
|
+
for (const entry of sorted) {
|
|
39
|
+
const modeAndName = Buffer.from(`${entry.mode} ${entry.name}\0`);
|
|
40
|
+
const rawSha = Buffer.from(entry.sha, "hex");
|
|
41
|
+
entryBuffers.push(Buffer.concat([modeAndName, rawSha]));
|
|
42
|
+
}
|
|
43
|
+
return hashGitObject("tree", Buffer.concat(entryBuffers));
|
|
44
|
+
}
|
|
45
|
+
function createGitCommit(opts) {
|
|
46
|
+
const lines = [
|
|
47
|
+
`tree ${opts.treeSha}`,
|
|
48
|
+
...opts.parentSha ? [`parent ${opts.parentSha}`] : [],
|
|
49
|
+
`author ${opts.authorName} <${opts.authorPubkey}@nostr> ${opts.timestamp} +0000`,
|
|
50
|
+
`committer ${opts.authorName} <${opts.authorPubkey}@nostr> ${opts.timestamp} +0000`,
|
|
51
|
+
"",
|
|
52
|
+
opts.message
|
|
53
|
+
];
|
|
54
|
+
return hashGitObject("commit", Buffer.from(lines.join("\n"), "utf-8"));
|
|
55
|
+
}
|
|
56
|
+
function createGitTag(opts) {
|
|
57
|
+
const lines = [
|
|
58
|
+
`object ${opts.objectSha}`,
|
|
59
|
+
`type ${opts.objectType}`,
|
|
60
|
+
`tag ${opts.tagName}`,
|
|
61
|
+
`tagger ${opts.taggerName} <${opts.taggerPubkey}@nostr> ${opts.timestamp} +0000`,
|
|
62
|
+
"",
|
|
63
|
+
opts.message
|
|
64
|
+
];
|
|
65
|
+
return hashGitObject("tag", Buffer.from(lines.join("\n"), "utf-8"));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/repo-reader.ts
|
|
69
|
+
import { execFile, spawn } from "child_process";
|
|
70
|
+
import { promisify } from "util";
|
|
71
|
+
var execFileAsync = promisify(execFile);
|
|
72
|
+
var MAX_BUFFER = 256 * 1024 * 1024;
|
|
73
|
+
var GitError = class extends Error {
|
|
74
|
+
constructor(message, exitCode, stderr) {
|
|
75
|
+
super(message);
|
|
76
|
+
this.exitCode = exitCode;
|
|
77
|
+
this.stderr = stderr;
|
|
78
|
+
this.name = "GitError";
|
|
79
|
+
}
|
|
80
|
+
exitCode;
|
|
81
|
+
stderr;
|
|
82
|
+
};
|
|
83
|
+
var FULL_SHA_RE = /^[0-9a-f]{40}$/;
|
|
84
|
+
var REV_TOKEN_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]*(?:[~^][0-9]*)*$/;
|
|
85
|
+
function isValidRevision(rev) {
|
|
86
|
+
if (rev.length === 0 || rev.length > 1024) return false;
|
|
87
|
+
if (!REV_TOKEN_RE.test(rev)) return false;
|
|
88
|
+
if (rev.includes("..")) return false;
|
|
89
|
+
if (rev.endsWith(".lock") || rev.endsWith("/") || rev.endsWith(".")) return false;
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
function assertRevision(rev, what) {
|
|
93
|
+
if (!isValidRevision(rev)) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`${what} is not a valid git revision (got ${JSON.stringify(rev)}); expected a SHA or simple refname \u2014 options/ranges are rejected`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function assertFullSha(sha, what) {
|
|
100
|
+
if (!FULL_SHA_RE.test(sha)) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`${what} is not a full 40-hex SHA-1 (got ${JSON.stringify(sha)})`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function assertRange(range, what) {
|
|
107
|
+
const parts = range.split(/\.{2,3}/);
|
|
108
|
+
const separators = range.match(/\.{2,3}/g) ?? [];
|
|
109
|
+
const ok = parts.length <= 2 && separators.length === parts.length - 1 && parts.every((p) => isValidRevision(p));
|
|
110
|
+
if (!ok) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
`${what} is not a valid revision range (got ${JSON.stringify(range)}); expected <rev>, <rev>..<rev>, or <rev>...<rev>`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
var OBJECT_TYPES = /* @__PURE__ */ new Set(["blob", "tree", "commit", "tag"]);
|
|
117
|
+
var BatchParser = class {
|
|
118
|
+
buf = Buffer.alloc(0);
|
|
119
|
+
pending = null;
|
|
120
|
+
objects = [];
|
|
121
|
+
missing = [];
|
|
122
|
+
push(chunk) {
|
|
123
|
+
this.buf = this.buf.length === 0 ? chunk : Buffer.concat([this.buf, chunk]);
|
|
124
|
+
this.drain();
|
|
125
|
+
}
|
|
126
|
+
/** True when no partially-parsed record remains. */
|
|
127
|
+
isComplete() {
|
|
128
|
+
return this.pending === null && this.buf.length === 0;
|
|
129
|
+
}
|
|
130
|
+
drain() {
|
|
131
|
+
for (; ; ) {
|
|
132
|
+
if (this.pending) {
|
|
133
|
+
const needed = this.pending.size + 1;
|
|
134
|
+
if (this.buf.length < needed) return;
|
|
135
|
+
const body = Buffer.from(this.buf.subarray(0, this.pending.size));
|
|
136
|
+
this.objects.push({ sha: this.pending.sha, type: this.pending.type, body });
|
|
137
|
+
this.buf = this.buf.subarray(needed);
|
|
138
|
+
this.pending = null;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const nl = this.buf.indexOf(10);
|
|
142
|
+
if (nl === -1) return;
|
|
143
|
+
const header = this.buf.subarray(0, nl).toString("utf-8");
|
|
144
|
+
this.buf = this.buf.subarray(nl + 1);
|
|
145
|
+
const [name, second, third] = header.split(" ");
|
|
146
|
+
if (name && second === "missing" && third === void 0) {
|
|
147
|
+
this.missing.push(name);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (name && second && third !== void 0 && OBJECT_TYPES.has(second)) {
|
|
151
|
+
const size = Number.parseInt(third, 10);
|
|
152
|
+
if (Number.isSafeInteger(size) && size >= 0) {
|
|
153
|
+
this.pending = { sha: name, type: second, size };
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
throw new GitError(
|
|
158
|
+
`unexpected cat-file --batch header: ${JSON.stringify(header)}`,
|
|
159
|
+
void 0,
|
|
160
|
+
""
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
var GitRepoReader = class {
|
|
166
|
+
constructor(repoPath) {
|
|
167
|
+
this.repoPath = repoPath;
|
|
168
|
+
}
|
|
169
|
+
repoPath;
|
|
170
|
+
/** Run git with argument-array safety; resolves stdout as UTF-8. */
|
|
171
|
+
async git(args, opts = {}) {
|
|
172
|
+
try {
|
|
173
|
+
const { stdout } = await execFileAsync("git", args, {
|
|
174
|
+
cwd: this.repoPath,
|
|
175
|
+
maxBuffer: MAX_BUFFER,
|
|
176
|
+
encoding: "utf-8"
|
|
177
|
+
});
|
|
178
|
+
return { stdout, exitCode: 0 };
|
|
179
|
+
} catch (err) {
|
|
180
|
+
const e = err;
|
|
181
|
+
const exitCode = typeof e.code === "number" ? e.code : void 0;
|
|
182
|
+
if (exitCode !== void 0 && opts.allowExitCodes?.includes(exitCode)) {
|
|
183
|
+
return { stdout: e.stdout ?? "", exitCode };
|
|
184
|
+
}
|
|
185
|
+
throw new GitError(
|
|
186
|
+
`git ${args[0]} failed${exitCode !== void 0 ? ` (exit ${exitCode})` : ""}: ${(e.stderr ?? e.message ?? "").trim()}`,
|
|
187
|
+
exitCode,
|
|
188
|
+
(e.stderr ?? "").trim()
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* List all branches and tags plus the symbolic HEAD.
|
|
194
|
+
*
|
|
195
|
+
* Annotated tags report the tag object's SHA/type with the peeled target
|
|
196
|
+
* in `peeledSha`. A detached HEAD is tolerated (`head` is `undefined`).
|
|
197
|
+
*/
|
|
198
|
+
async listRefs() {
|
|
199
|
+
const format = "%(refname)%00%(objectname)%00%(objecttype)%00%(*objectname)";
|
|
200
|
+
const [refsRes, headRes] = await Promise.all([
|
|
201
|
+
this.git(["for-each-ref", `--format=${format}`, "refs/heads", "refs/tags"]),
|
|
202
|
+
// Exit 1 = detached HEAD (or unborn branch pointer oddities) — tolerated.
|
|
203
|
+
this.git(["symbolic-ref", "--quiet", "HEAD"], { allowExitCodes: [1] })
|
|
204
|
+
]);
|
|
205
|
+
const refs = [];
|
|
206
|
+
for (const line of refsRes.stdout.split("\n")) {
|
|
207
|
+
if (!line) continue;
|
|
208
|
+
const [refname, sha, objecttype, peeled] = line.split("\0");
|
|
209
|
+
if (!refname || !sha || !objecttype || !OBJECT_TYPES.has(objecttype)) {
|
|
210
|
+
throw new GitError(`unexpected for-each-ref line: ${JSON.stringify(line)}`, void 0, "");
|
|
211
|
+
}
|
|
212
|
+
refs.push({
|
|
213
|
+
refname,
|
|
214
|
+
sha,
|
|
215
|
+
type: objecttype,
|
|
216
|
+
...peeled ? { peeledSha: peeled } : {}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
const head = headRes.exitCode === 0 ? headRes.stdout.trim() || void 0 : void 0;
|
|
220
|
+
return { head, refs };
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* SHAs of every object reachable from `want` but not from `have`
|
|
224
|
+
* (`git rev-list --objects <want…> --not <have…>`), i.e. the push delta.
|
|
225
|
+
*
|
|
226
|
+
* Haves that don't exist locally (e.g. remote tips we never fetched) are
|
|
227
|
+
* filtered out first via one `cat-file --batch-check` pass — rev-list
|
|
228
|
+
* would otherwise die on them.
|
|
229
|
+
*/
|
|
230
|
+
async objectsBetween(want, have) {
|
|
231
|
+
const objects = await this.objectsBetweenWithPaths(want, have);
|
|
232
|
+
return objects.map((o) => o.sha);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Like {@link objectsBetween} but keeps the path each object was reached
|
|
236
|
+
* by (`rev-list --objects` emits `<sha> <path>` for blobs and non-root
|
|
237
|
+
* trees) — used by push planning to report actionable oversize errors.
|
|
238
|
+
*/
|
|
239
|
+
async objectsBetweenWithPaths(want, have) {
|
|
240
|
+
for (const w of want) assertRevision(w, "want");
|
|
241
|
+
for (const h of have) assertRevision(h, "have");
|
|
242
|
+
if (want.length === 0) return [];
|
|
243
|
+
const knownHaves = await this.filterExisting(have);
|
|
244
|
+
const args = ["rev-list", "--objects", ...want];
|
|
245
|
+
if (knownHaves.length > 0) args.push("--not", ...knownHaves);
|
|
246
|
+
args.push("--");
|
|
247
|
+
const { stdout } = await this.git(args);
|
|
248
|
+
const objects = [];
|
|
249
|
+
for (const line of stdout.split("\n")) {
|
|
250
|
+
if (!line) continue;
|
|
251
|
+
const spaceIdx = line.indexOf(" ");
|
|
252
|
+
if (spaceIdx === -1) {
|
|
253
|
+
objects.push({ sha: line });
|
|
254
|
+
} else {
|
|
255
|
+
const path = line.slice(spaceIdx + 1);
|
|
256
|
+
objects.push({
|
|
257
|
+
sha: line.slice(0, spaceIdx),
|
|
258
|
+
...path ? { path } : {}
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return objects;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* List every blob (file) reachable from a ref's root tree, recursively,
|
|
266
|
+
* with the path it is served at (#368: the ar.io site manifest join key).
|
|
267
|
+
* Uses `git ls-tree -r -z` — NUL-terminated records so binary/spaced paths
|
|
268
|
+
* survive verbatim, and no path quoting to undo. Submodule (`commit`)
|
|
269
|
+
* gitlink entries and directories are excluded; only real file blobs remain.
|
|
270
|
+
*/
|
|
271
|
+
async listBlobs(rev) {
|
|
272
|
+
assertRevision(rev, "ref");
|
|
273
|
+
const { stdout } = await this.git(["ls-tree", "-r", "-z", rev, "--"]);
|
|
274
|
+
const blobs = [];
|
|
275
|
+
for (const record of stdout.split("\0")) {
|
|
276
|
+
if (!record) continue;
|
|
277
|
+
const tab = record.indexOf(" ");
|
|
278
|
+
if (tab === -1) {
|
|
279
|
+
throw new GitError(
|
|
280
|
+
`unexpected ls-tree record: ${JSON.stringify(record)}`,
|
|
281
|
+
void 0,
|
|
282
|
+
""
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
const meta = record.slice(0, tab);
|
|
286
|
+
const path = record.slice(tab + 1);
|
|
287
|
+
const [, type, sha] = meta.split(" ");
|
|
288
|
+
if (type !== "blob") continue;
|
|
289
|
+
if (!sha || !FULL_SHA_RE.test(sha)) {
|
|
290
|
+
throw new GitError(
|
|
291
|
+
`unexpected ls-tree object id: ${JSON.stringify(record)}`,
|
|
292
|
+
void 0,
|
|
293
|
+
""
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
blobs.push({ path, sha });
|
|
297
|
+
}
|
|
298
|
+
return blobs;
|
|
299
|
+
}
|
|
300
|
+
/** Run git feeding `input` on stdin; resolves collected stdout bytes. */
|
|
301
|
+
runWithStdin(args, input) {
|
|
302
|
+
return new Promise((resolve, reject) => {
|
|
303
|
+
const child = spawn("git", args, {
|
|
304
|
+
cwd: this.repoPath,
|
|
305
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
306
|
+
});
|
|
307
|
+
const out = [];
|
|
308
|
+
let stderr = "";
|
|
309
|
+
child.stdout.on("data", (chunk) => out.push(chunk));
|
|
310
|
+
child.stderr.on("data", (chunk) => {
|
|
311
|
+
stderr += chunk.toString("utf-8");
|
|
312
|
+
});
|
|
313
|
+
child.on("error", (err) => {
|
|
314
|
+
reject(new GitError(`failed to spawn git ${args[0]}: ${err.message}`, void 0, ""));
|
|
315
|
+
});
|
|
316
|
+
child.on("close", (code) => {
|
|
317
|
+
if (code !== 0) {
|
|
318
|
+
return reject(
|
|
319
|
+
new GitError(`git ${args[0]} failed (exit ${code}): ${stderr.trim()}`, code ?? void 0, stderr.trim())
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
resolve(Buffer.concat(out));
|
|
323
|
+
});
|
|
324
|
+
child.stdin.on("error", () => {
|
|
325
|
+
});
|
|
326
|
+
child.stdin.write(input);
|
|
327
|
+
child.stdin.end();
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
/** Of the given revisions, keep only those resolvable locally. */
|
|
331
|
+
async filterExisting(revs) {
|
|
332
|
+
if (revs.length === 0) return [];
|
|
333
|
+
const { missing } = await this.batchCheck(revs);
|
|
334
|
+
const missingSet = new Set(missing);
|
|
335
|
+
return revs.filter((r) => !missingSet.has(r));
|
|
336
|
+
}
|
|
337
|
+
/** One `cat-file --batch-check` pass; returns names reported missing. */
|
|
338
|
+
async batchCheck(names) {
|
|
339
|
+
const stdout = await this.runWithStdin(
|
|
340
|
+
["cat-file", "--batch-check"],
|
|
341
|
+
names.join("\n") + "\n"
|
|
342
|
+
);
|
|
343
|
+
const missing = [];
|
|
344
|
+
for (const line of stdout.toString("utf-8").split("\n")) {
|
|
345
|
+
if (line.endsWith(" missing")) missing.push(line.slice(0, -" missing".length));
|
|
346
|
+
}
|
|
347
|
+
return { missing };
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Object metadata (type + body size) for a batch of SHAs via one
|
|
351
|
+
* `cat-file --batch-check` pass — no bodies are read. Missing objects are
|
|
352
|
+
* reported, not thrown.
|
|
353
|
+
*/
|
|
354
|
+
async statObjects(shas) {
|
|
355
|
+
for (const sha of shas) assertFullSha(sha, "sha");
|
|
356
|
+
if (shas.length === 0) return { objects: [], missing: [] };
|
|
357
|
+
const stdout = await this.runWithStdin(
|
|
358
|
+
["cat-file", "--batch-check"],
|
|
359
|
+
shas.join("\n") + "\n"
|
|
360
|
+
);
|
|
361
|
+
const objects = [];
|
|
362
|
+
const missing = [];
|
|
363
|
+
for (const line of stdout.toString("utf-8").split("\n")) {
|
|
364
|
+
if (!line) continue;
|
|
365
|
+
if (line.endsWith(" missing")) {
|
|
366
|
+
missing.push(line.slice(0, -" missing".length));
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
const [sha, type, sizeStr] = line.split(" ");
|
|
370
|
+
const size = Number.parseInt(sizeStr ?? "", 10);
|
|
371
|
+
if (!sha || !type || !OBJECT_TYPES.has(type) || !Number.isSafeInteger(size) || size < 0) {
|
|
372
|
+
throw new GitError(
|
|
373
|
+
`unexpected cat-file --batch-check line: ${JSON.stringify(line)}`,
|
|
374
|
+
void 0,
|
|
375
|
+
""
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
objects.push({ sha, type, size });
|
|
379
|
+
}
|
|
380
|
+
return { objects, missing };
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Read raw object bodies via a single streaming `git cat-file --batch`
|
|
384
|
+
* child process. Bodies may be binary and are parsed size-driven across
|
|
385
|
+
* chunk boundaries. Missing objects are reported, not thrown.
|
|
386
|
+
*/
|
|
387
|
+
async readObjects(shas) {
|
|
388
|
+
for (const sha of shas) assertFullSha(sha, "sha");
|
|
389
|
+
if (shas.length === 0) return { objects: [], missing: [] };
|
|
390
|
+
const parser = new BatchParser();
|
|
391
|
+
await new Promise((resolve, reject) => {
|
|
392
|
+
const child = spawn("git", ["cat-file", "--batch"], {
|
|
393
|
+
cwd: this.repoPath,
|
|
394
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
395
|
+
});
|
|
396
|
+
let stderr = "";
|
|
397
|
+
let parseError = null;
|
|
398
|
+
child.stderr.on("data", (chunk) => {
|
|
399
|
+
stderr += chunk.toString("utf-8");
|
|
400
|
+
});
|
|
401
|
+
child.stdout.on("data", (chunk) => {
|
|
402
|
+
if (parseError) return;
|
|
403
|
+
try {
|
|
404
|
+
parser.push(chunk);
|
|
405
|
+
} catch (err) {
|
|
406
|
+
parseError = err;
|
|
407
|
+
child.kill();
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
child.on("error", (err) => {
|
|
411
|
+
reject(new GitError(`failed to spawn git cat-file: ${err.message}`, void 0, ""));
|
|
412
|
+
});
|
|
413
|
+
child.on("close", (code) => {
|
|
414
|
+
if (parseError) return reject(parseError);
|
|
415
|
+
if (code !== 0) {
|
|
416
|
+
return reject(
|
|
417
|
+
new GitError(`git cat-file --batch failed (exit ${code}): ${stderr.trim()}`, code ?? void 0, stderr.trim())
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
if (!parser.isComplete()) {
|
|
421
|
+
return reject(
|
|
422
|
+
new GitError("git cat-file --batch output ended mid-record", code ?? void 0, stderr.trim())
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
resolve();
|
|
426
|
+
});
|
|
427
|
+
child.stdin.on("error", () => {
|
|
428
|
+
});
|
|
429
|
+
child.stdin.write(shas.join("\n") + "\n");
|
|
430
|
+
child.stdin.end();
|
|
431
|
+
});
|
|
432
|
+
return { objects: parser.objects, missing: parser.missing };
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* `git merge-base --is-ancestor <a> <b>` — true when `a` is an ancestor
|
|
436
|
+
* of `b` (fast-forward check / force detection). Exit codes other than
|
|
437
|
+
* 0/1 (e.g. unknown revisions) throw.
|
|
438
|
+
*/
|
|
439
|
+
async isAncestor(a, b) {
|
|
440
|
+
assertRevision(a, "ancestor candidate");
|
|
441
|
+
assertRevision(b, "descendant candidate");
|
|
442
|
+
const { exitCode } = await this.git(
|
|
443
|
+
["merge-base", "--is-ancestor", a, b],
|
|
444
|
+
{ allowExitCodes: [1] }
|
|
445
|
+
);
|
|
446
|
+
return exitCode === 0;
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* `git format-patch --stdout <range>` — the full mbox-formatted patch
|
|
450
|
+
* series text (empty string when the range selects no commits).
|
|
451
|
+
*/
|
|
452
|
+
async formatPatch(range) {
|
|
453
|
+
assertRange(range, "range");
|
|
454
|
+
const { stdout } = await this.git(["format-patch", "--stdout", range, "--"]);
|
|
455
|
+
return stdout;
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Parent SHAs for a batch of commit SHAs via one
|
|
459
|
+
* `git rev-list --no-walk=unsorted --parents` pass. Root commits map to an
|
|
460
|
+
* empty array. Used to derive the kind:1617 `commit`/`parent-commit` tag
|
|
461
|
+
* pairs for exactly the commits a format-patch series carries.
|
|
462
|
+
*/
|
|
463
|
+
async commitParents(shas) {
|
|
464
|
+
for (const sha of shas) assertFullSha(sha, "sha");
|
|
465
|
+
if (shas.length === 0) return /* @__PURE__ */ new Map();
|
|
466
|
+
const { stdout } = await this.git([
|
|
467
|
+
"rev-list",
|
|
468
|
+
"--no-walk=unsorted",
|
|
469
|
+
"--parents",
|
|
470
|
+
...shas,
|
|
471
|
+
"--"
|
|
472
|
+
]);
|
|
473
|
+
const parents = /* @__PURE__ */ new Map();
|
|
474
|
+
for (const line of stdout.split("\n")) {
|
|
475
|
+
if (!line) continue;
|
|
476
|
+
const [sha, ...rest] = line.split(" ");
|
|
477
|
+
if (!sha || !FULL_SHA_RE.test(sha) || rest.some((p) => !FULL_SHA_RE.test(p))) {
|
|
478
|
+
throw new GitError(
|
|
479
|
+
`unexpected rev-list --parents line: ${JSON.stringify(line)}`,
|
|
480
|
+
void 0,
|
|
481
|
+
""
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
parents.set(sha, rest);
|
|
485
|
+
}
|
|
486
|
+
return parents;
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Resolve a ref/revision to a full SHA via `git rev-parse --verify`.
|
|
490
|
+
* Throws {@link GitError} when the name doesn't resolve.
|
|
491
|
+
*/
|
|
492
|
+
async resolveRef(name) {
|
|
493
|
+
assertRevision(name, "ref name");
|
|
494
|
+
const { stdout } = await this.git(["rev-parse", "--verify", "--quiet", name]);
|
|
495
|
+
const sha = stdout.trim();
|
|
496
|
+
if (!FULL_SHA_RE.test(sha)) {
|
|
497
|
+
throw new GitError(
|
|
498
|
+
`rev-parse --verify returned unexpected output for ${JSON.stringify(name)}: ${JSON.stringify(sha)}`,
|
|
499
|
+
void 0,
|
|
500
|
+
""
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
return sha;
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
// src/object-fetch.ts
|
|
508
|
+
var DEFAULT_CONCURRENCY = 8;
|
|
509
|
+
var ObjectIntegrityError = class extends Error {
|
|
510
|
+
constructor(objects) {
|
|
511
|
+
super(
|
|
512
|
+
`${objects.length} downloaded object(s) failed SHA-1 verification \u2014 the gateway content does not match the announced git SHA(s): ` + objects.map((o) => `${o.sha} (tx ${o.txId})`).join(", ") + ". Refusing to write corrupt/tampered objects."
|
|
513
|
+
);
|
|
514
|
+
this.objects = objects;
|
|
515
|
+
this.name = "ObjectIntegrityError";
|
|
516
|
+
}
|
|
517
|
+
objects;
|
|
518
|
+
};
|
|
519
|
+
var OBJECT_TYPES2 = [
|
|
520
|
+
"blob",
|
|
521
|
+
"tree",
|
|
522
|
+
"commit",
|
|
523
|
+
"tag"
|
|
524
|
+
];
|
|
525
|
+
function verifyObjectBody(expectedSha, bytes) {
|
|
526
|
+
const body = Buffer.from(bytes);
|
|
527
|
+
for (const type of OBJECT_TYPES2) {
|
|
528
|
+
if (hashGitObject(type, body).sha === expectedSha) {
|
|
529
|
+
return { sha: expectedSha, type, body };
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
async function fetchTxBytes(txId, options = {}) {
|
|
535
|
+
if (!isValidArweaveTxId(txId)) return null;
|
|
536
|
+
const gateways = options.gateways ?? ARWEAVE_GATEWAYS;
|
|
537
|
+
const fetchFn = options.fetchFn ?? fetch;
|
|
538
|
+
const timeoutMs = options.timeoutMs ?? ARWEAVE_FETCH_TIMEOUT_MS;
|
|
539
|
+
for (const gateway of gateways) {
|
|
540
|
+
try {
|
|
541
|
+
const response = await fetchFn(`${gateway}/${txId}`, {
|
|
542
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
543
|
+
});
|
|
544
|
+
if (!response.ok) continue;
|
|
545
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
546
|
+
} catch {
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
async function downloadGitObjects(entries, options = {}) {
|
|
552
|
+
const queue = [...entries];
|
|
553
|
+
const total = queue.length;
|
|
554
|
+
const concurrency = Math.max(1, options.concurrency ?? DEFAULT_CONCURRENCY);
|
|
555
|
+
const objects = /* @__PURE__ */ new Map();
|
|
556
|
+
const unavailable = [];
|
|
557
|
+
const corrupt = [];
|
|
558
|
+
let done = 0;
|
|
559
|
+
const worker = async () => {
|
|
560
|
+
for (; ; ) {
|
|
561
|
+
const next = queue.shift();
|
|
562
|
+
if (!next) return;
|
|
563
|
+
const [sha, txId] = next;
|
|
564
|
+
const bytes = await fetchTxBytes(txId, options);
|
|
565
|
+
if (bytes === null) {
|
|
566
|
+
unavailable.push({ sha, txId });
|
|
567
|
+
} else {
|
|
568
|
+
const verified = verifyObjectBody(sha, bytes);
|
|
569
|
+
if (verified === null) {
|
|
570
|
+
corrupt.push({ sha, txId });
|
|
571
|
+
} else {
|
|
572
|
+
objects.set(sha, verified);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
done += 1;
|
|
576
|
+
options.onObject?.(done, total);
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
await Promise.all(
|
|
580
|
+
Array.from({ length: Math.min(concurrency, total) }, () => worker())
|
|
581
|
+
);
|
|
582
|
+
if (corrupt.length > 0) throw new ObjectIntegrityError(corrupt);
|
|
583
|
+
return { objects, unavailable };
|
|
584
|
+
}
|
|
585
|
+
var FULL_SHA_RE2 = /^[0-9a-f]{40}$/;
|
|
586
|
+
var GITLINK_MODE = "160000";
|
|
587
|
+
function bytesToHex(bytes) {
|
|
588
|
+
let hex = "";
|
|
589
|
+
for (const byte of bytes) hex += byte.toString(16).padStart(2, "0");
|
|
590
|
+
return hex;
|
|
591
|
+
}
|
|
592
|
+
function referencedShas(object) {
|
|
593
|
+
switch (object.type) {
|
|
594
|
+
case "blob":
|
|
595
|
+
return [];
|
|
596
|
+
case "tree": {
|
|
597
|
+
const refs = [];
|
|
598
|
+
const data = object.body;
|
|
599
|
+
let offset = 0;
|
|
600
|
+
while (offset < data.length) {
|
|
601
|
+
const spaceIdx = data.indexOf(32, offset);
|
|
602
|
+
if (spaceIdx === -1) break;
|
|
603
|
+
const mode = data.subarray(offset, spaceIdx).toString("utf-8");
|
|
604
|
+
const nulIdx = data.indexOf(0, spaceIdx + 1);
|
|
605
|
+
if (nulIdx === -1 || nulIdx + 21 > data.length) break;
|
|
606
|
+
const sha = bytesToHex(data.subarray(nulIdx + 1, nulIdx + 21));
|
|
607
|
+
if (mode !== GITLINK_MODE) refs.push(sha);
|
|
608
|
+
offset = nulIdx + 21;
|
|
609
|
+
}
|
|
610
|
+
return refs;
|
|
611
|
+
}
|
|
612
|
+
case "commit": {
|
|
613
|
+
const refs = [];
|
|
614
|
+
const text = object.body.toString("utf-8");
|
|
615
|
+
const headerEnd = text.indexOf("\n\n");
|
|
616
|
+
const header = headerEnd === -1 ? text : text.slice(0, headerEnd);
|
|
617
|
+
for (const line of header.split("\n")) {
|
|
618
|
+
if (line.startsWith("tree ")) refs.push(line.slice(5).trim());
|
|
619
|
+
else if (line.startsWith("parent ")) refs.push(line.slice(7).trim());
|
|
620
|
+
}
|
|
621
|
+
return refs.filter((sha) => FULL_SHA_RE2.test(sha));
|
|
622
|
+
}
|
|
623
|
+
case "tag": {
|
|
624
|
+
const text = object.body.toString("utf-8");
|
|
625
|
+
const match = /^object ([0-9a-f]{40})$/m.exec(text);
|
|
626
|
+
return match?.[1] ? [match[1]] : [];
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
function walkClosure(tips, objects, presentLocally = /* @__PURE__ */ new Set()) {
|
|
631
|
+
const reachable = /* @__PURE__ */ new Set();
|
|
632
|
+
const missing = /* @__PURE__ */ new Set();
|
|
633
|
+
const stack = [...new Set(tips)];
|
|
634
|
+
while (stack.length > 0) {
|
|
635
|
+
const sha = stack.pop();
|
|
636
|
+
if (reachable.has(sha) || missing.has(sha)) continue;
|
|
637
|
+
if (presentLocally.has(sha)) continue;
|
|
638
|
+
const object = objects.get(sha);
|
|
639
|
+
if (!object) {
|
|
640
|
+
missing.add(sha);
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
reachable.add(sha);
|
|
644
|
+
for (const ref of referencedShas(object)) {
|
|
645
|
+
if (!reachable.has(ref) && !missing.has(ref)) stack.push(ref);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return { reachable, missing: [...missing] };
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// src/read-pipeline.ts
|
|
652
|
+
var MAX_PASSES = 64;
|
|
653
|
+
async function collectRepoObjects(options) {
|
|
654
|
+
const { tips, shaToTxId, resolveMissing } = options;
|
|
655
|
+
const present = options.presentLocally ?? /* @__PURE__ */ new Set();
|
|
656
|
+
const txIds = new Map(shaToTxId);
|
|
657
|
+
const resolverAsked = /* @__PURE__ */ new Set();
|
|
658
|
+
const undownloadable = /* @__PURE__ */ new Map();
|
|
659
|
+
const objects = /* @__PURE__ */ new Map();
|
|
660
|
+
const initial = [];
|
|
661
|
+
for (const [sha, txId] of txIds) {
|
|
662
|
+
if (!present.has(sha)) initial.push([sha, txId]);
|
|
663
|
+
}
|
|
664
|
+
const bulk = await downloadGitObjects(initial, options);
|
|
665
|
+
for (const [sha, object] of bulk.objects) objects.set(sha, object);
|
|
666
|
+
for (const { sha, txId } of bulk.unavailable) undownloadable.set(sha, txId);
|
|
667
|
+
for (let pass = 0; pass < MAX_PASSES; pass++) {
|
|
668
|
+
const closure = walkClosure(tips, objects, present);
|
|
669
|
+
const open = closure.missing.filter((sha) => !undownloadable.has(sha));
|
|
670
|
+
if (open.length === 0) break;
|
|
671
|
+
const toResolve = open.filter(
|
|
672
|
+
(sha) => !txIds.has(sha) && !resolverAsked.has(sha)
|
|
673
|
+
);
|
|
674
|
+
for (const sha of toResolve) resolverAsked.add(sha);
|
|
675
|
+
if (toResolve.length > 0) {
|
|
676
|
+
const resolved = await resolveMissing(toResolve);
|
|
677
|
+
for (const [sha, txId] of resolved) txIds.set(sha, txId);
|
|
678
|
+
}
|
|
679
|
+
const batch = [];
|
|
680
|
+
for (const sha of open) {
|
|
681
|
+
const txId = txIds.get(sha);
|
|
682
|
+
if (txId !== void 0 && !objects.has(sha)) batch.push([sha, txId]);
|
|
683
|
+
}
|
|
684
|
+
if (batch.length === 0) break;
|
|
685
|
+
const result = await downloadGitObjects(batch, options);
|
|
686
|
+
for (const [sha, object] of result.objects) objects.set(sha, object);
|
|
687
|
+
for (const { sha, txId } of result.unavailable)
|
|
688
|
+
undownloadable.set(sha, txId);
|
|
689
|
+
if (result.objects.size === 0) break;
|
|
690
|
+
}
|
|
691
|
+
let finalClosure = walkClosure(tips, objects, present);
|
|
692
|
+
if (finalClosure.missing.includes(EMPTY_BLOB_SHA) && !present.has(EMPTY_BLOB_SHA)) {
|
|
693
|
+
objects.set(EMPTY_BLOB_SHA, {
|
|
694
|
+
sha: EMPTY_BLOB_SHA,
|
|
695
|
+
type: "blob",
|
|
696
|
+
body: Buffer.alloc(0)
|
|
697
|
+
});
|
|
698
|
+
finalClosure = walkClosure(tips, objects, present);
|
|
699
|
+
}
|
|
700
|
+
const missing = finalClosure.missing.map((sha) => ({
|
|
701
|
+
sha,
|
|
702
|
+
txId: txIds.get(sha) ?? null
|
|
703
|
+
}));
|
|
704
|
+
const reachable = finalClosure.reachable;
|
|
705
|
+
const skippedUnavailable = [...undownloadable].filter(
|
|
706
|
+
([sha]) => !reachable.has(sha) && !finalClosure.missing.includes(sha)
|
|
707
|
+
).map(([sha, txId]) => ({ sha, txId }));
|
|
708
|
+
return { objects, missing, skippedUnavailable };
|
|
709
|
+
}
|
|
710
|
+
function missingObjectsMessage(missing, context) {
|
|
711
|
+
const listed = missing.slice(0, 20).map(
|
|
712
|
+
(m) => ` ${m.sha}${m.txId ? ` (tx ${m.txId})` : " (no Arweave tx found)"}`
|
|
713
|
+
).join("\n");
|
|
714
|
+
const more = missing.length > 20 ? `
|
|
715
|
+
\u2026 and ${missing.length - 20} more` : "";
|
|
716
|
+
return `${context}: ${missing.length} required object(s) could not be downloaded:
|
|
717
|
+
${listed}${more}
|
|
718
|
+
Recently pushed objects can take 10-20 minutes to become fetchable from Arweave gateways \u2014 if this repo was just pushed, retry in a few minutes. Nothing was written.`;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// src/materialize.ts
|
|
722
|
+
import { execFile as execFile2, spawn as spawn2 } from "child_process";
|
|
723
|
+
import { promisify as promisify2 } from "util";
|
|
724
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
725
|
+
var ObjectWriteMismatchError = class extends Error {
|
|
726
|
+
constructor(expectedSha, writtenSha) {
|
|
727
|
+
super(
|
|
728
|
+
`git hash-object wrote ${writtenSha} where ${expectedSha} was expected \u2014 object content does not round-trip; aborting`
|
|
729
|
+
);
|
|
730
|
+
this.expectedSha = expectedSha;
|
|
731
|
+
this.writtenSha = writtenSha;
|
|
732
|
+
this.name = "ObjectWriteMismatchError";
|
|
733
|
+
}
|
|
734
|
+
expectedSha;
|
|
735
|
+
writtenSha;
|
|
736
|
+
};
|
|
737
|
+
var FULL_SHA_RE3 = /^[0-9a-f]{40}$/;
|
|
738
|
+
function isSafeRefname(refname) {
|
|
739
|
+
if (!refname.startsWith("refs/") || refname.length > 1024) return false;
|
|
740
|
+
if (/[\u0000-\u0020~^:?*[\\\u007f]/.test(refname)) return false;
|
|
741
|
+
if (refname.includes("..") || refname.includes("@{")) return false;
|
|
742
|
+
if (refname.endsWith("/") || refname.endsWith(".")) return false;
|
|
743
|
+
for (const part of refname.split("/")) {
|
|
744
|
+
if (part === "" || part.startsWith(".") || part.startsWith("-"))
|
|
745
|
+
return false;
|
|
746
|
+
if (part.endsWith(".lock")) return false;
|
|
747
|
+
}
|
|
748
|
+
return true;
|
|
749
|
+
}
|
|
750
|
+
function assertSafeRefname(refname) {
|
|
751
|
+
if (!isSafeRefname(refname)) {
|
|
752
|
+
throw new Error(
|
|
753
|
+
`unsafe ref name from remote state: ${JSON.stringify(refname)} \u2014 refusing`
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
function assertFullSha2(sha) {
|
|
758
|
+
if (!FULL_SHA_RE3.test(sha)) {
|
|
759
|
+
throw new Error(`not a full 40-hex SHA-1: ${JSON.stringify(sha)}`);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
async function runGit(repoPath, args) {
|
|
763
|
+
try {
|
|
764
|
+
const { stdout } = await execFileAsync2("git", args, {
|
|
765
|
+
cwd: repoPath,
|
|
766
|
+
encoding: "utf-8",
|
|
767
|
+
maxBuffer: 64 * 1024 * 1024
|
|
768
|
+
});
|
|
769
|
+
return stdout;
|
|
770
|
+
} catch (err) {
|
|
771
|
+
const e = err;
|
|
772
|
+
throw new Error(
|
|
773
|
+
`git ${args[0]} failed${typeof e.code === "number" ? ` (exit ${e.code})` : ""}: ${(e.stderr ?? e.message ?? "").trim()}`
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
async function writeGitObject(repoPath, object) {
|
|
778
|
+
assertFullSha2(object.sha);
|
|
779
|
+
const written = await new Promise((resolve, reject) => {
|
|
780
|
+
const child = spawn2(
|
|
781
|
+
"git",
|
|
782
|
+
["hash-object", "-w", "--stdin", "-t", object.type],
|
|
783
|
+
{ cwd: repoPath, stdio: ["pipe", "pipe", "pipe"] }
|
|
784
|
+
);
|
|
785
|
+
let stdout = "";
|
|
786
|
+
let stderr = "";
|
|
787
|
+
child.stdout.on("data", (chunk) => {
|
|
788
|
+
stdout += chunk.toString("utf-8");
|
|
789
|
+
});
|
|
790
|
+
child.stderr.on("data", (chunk) => {
|
|
791
|
+
stderr += chunk.toString("utf-8");
|
|
792
|
+
});
|
|
793
|
+
child.on("error", (err) => {
|
|
794
|
+
reject(new Error(`failed to spawn git hash-object: ${err.message}`));
|
|
795
|
+
});
|
|
796
|
+
child.on("close", (code) => {
|
|
797
|
+
if (code !== 0) {
|
|
798
|
+
return reject(
|
|
799
|
+
new Error(`git hash-object failed (exit ${code}): ${stderr.trim()}`)
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
resolve(stdout.trim());
|
|
803
|
+
});
|
|
804
|
+
child.stdin.on("error", () => {
|
|
805
|
+
});
|
|
806
|
+
child.stdin.write(object.body);
|
|
807
|
+
child.stdin.end();
|
|
808
|
+
});
|
|
809
|
+
if (written !== object.sha) {
|
|
810
|
+
throw new ObjectWriteMismatchError(object.sha, written);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
async function writeGitObjects(repoPath, objects) {
|
|
814
|
+
let count = 0;
|
|
815
|
+
for (const object of objects) {
|
|
816
|
+
await writeGitObject(repoPath, object);
|
|
817
|
+
count += 1;
|
|
818
|
+
}
|
|
819
|
+
return count;
|
|
820
|
+
}
|
|
821
|
+
async function updateRef(repoPath, refname, sha) {
|
|
822
|
+
assertSafeRefname(refname);
|
|
823
|
+
assertFullSha2(sha);
|
|
824
|
+
await runGit(repoPath, ["update-ref", refname, sha]);
|
|
825
|
+
}
|
|
826
|
+
async function setHeadSymref(repoPath, refname) {
|
|
827
|
+
assertSafeRefname(refname);
|
|
828
|
+
await runGit(repoPath, ["symbolic-ref", "HEAD", refname]);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// src/npub.ts
|
|
832
|
+
var BECH32_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
|
833
|
+
function bech32Polymod(values2) {
|
|
834
|
+
const GEN = [996825010, 642813549, 513874426, 1027748829, 705979059];
|
|
835
|
+
let chk = 1;
|
|
836
|
+
for (const v of values2) {
|
|
837
|
+
const b = chk >> 25;
|
|
838
|
+
chk = (chk & 33554431) << 5 ^ v;
|
|
839
|
+
for (let i = 0; i < 5; i++) {
|
|
840
|
+
chk ^= b >> i & 1 ? GEN[i] : 0;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
return chk;
|
|
844
|
+
}
|
|
845
|
+
function bech32HrpExpand(hrp) {
|
|
846
|
+
const ret = [];
|
|
847
|
+
for (let i = 0; i < hrp.length; i++) {
|
|
848
|
+
ret.push(hrp.charCodeAt(i) >> 5);
|
|
849
|
+
}
|
|
850
|
+
ret.push(0);
|
|
851
|
+
for (let i = 0; i < hrp.length; i++) {
|
|
852
|
+
ret.push(hrp.charCodeAt(i) & 31);
|
|
853
|
+
}
|
|
854
|
+
return ret;
|
|
855
|
+
}
|
|
856
|
+
function bech32CreateChecksum(hrp, data) {
|
|
857
|
+
const values2 = bech32HrpExpand(hrp).concat(data).concat([0, 0, 0, 0, 0, 0]);
|
|
858
|
+
const polymod = bech32Polymod(values2) ^ 1;
|
|
859
|
+
const ret = [];
|
|
860
|
+
for (let i = 0; i < 6; i++) {
|
|
861
|
+
ret.push(polymod >> 5 * (5 - i) & 31);
|
|
862
|
+
}
|
|
863
|
+
return ret;
|
|
864
|
+
}
|
|
865
|
+
function convertBits(data, fromBits, toBits, pad) {
|
|
866
|
+
let acc = 0;
|
|
867
|
+
let bits = 0;
|
|
868
|
+
const ret = [];
|
|
869
|
+
const maxv = (1 << toBits) - 1;
|
|
870
|
+
for (const value of data) {
|
|
871
|
+
acc = acc << fromBits | value;
|
|
872
|
+
bits += fromBits;
|
|
873
|
+
while (bits >= toBits) {
|
|
874
|
+
bits -= toBits;
|
|
875
|
+
ret.push(acc >> bits & maxv);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
if (pad && bits > 0) {
|
|
879
|
+
ret.push(acc << toBits - bits & maxv);
|
|
880
|
+
}
|
|
881
|
+
return ret;
|
|
882
|
+
}
|
|
883
|
+
function hexToBytes(hex) {
|
|
884
|
+
const bytes = [];
|
|
885
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
886
|
+
bytes.push(parseInt(hex.slice(i, i + 2), 16));
|
|
887
|
+
}
|
|
888
|
+
return bytes;
|
|
889
|
+
}
|
|
890
|
+
function hexToNpub(hexPubkey) {
|
|
891
|
+
const hrp = "npub";
|
|
892
|
+
const bytes = hexToBytes(hexPubkey);
|
|
893
|
+
const words = convertBits(bytes, 8, 5, true);
|
|
894
|
+
const checksum = bech32CreateChecksum(hrp, words);
|
|
895
|
+
const combined = words.concat(checksum);
|
|
896
|
+
return hrp + "1" + combined.map((d) => BECH32_CHARSET[d]).join("");
|
|
897
|
+
}
|
|
898
|
+
function npubToHex(npub) {
|
|
899
|
+
const lower = npub.toLowerCase();
|
|
900
|
+
if (lower !== npub && npub.toUpperCase() !== npub) {
|
|
901
|
+
throw new Error("npub: mixed case");
|
|
902
|
+
}
|
|
903
|
+
if (!lower.startsWith("npub1")) {
|
|
904
|
+
throw new Error("npub: invalid prefix");
|
|
905
|
+
}
|
|
906
|
+
if (lower.length !== 63) {
|
|
907
|
+
throw new Error("npub: invalid length");
|
|
908
|
+
}
|
|
909
|
+
const data = [];
|
|
910
|
+
for (let i = 5; i < lower.length; i++) {
|
|
911
|
+
const idx = BECH32_CHARSET.indexOf(lower[i]);
|
|
912
|
+
if (idx === -1) throw new Error("npub: invalid character");
|
|
913
|
+
data.push(idx);
|
|
914
|
+
}
|
|
915
|
+
const hrpExpanded = bech32HrpExpand("npub");
|
|
916
|
+
if (bech32Polymod(hrpExpanded.concat(data)) !== 1) {
|
|
917
|
+
throw new Error("npub: invalid checksum");
|
|
918
|
+
}
|
|
919
|
+
const words = data.slice(0, -6);
|
|
920
|
+
const bytes = convertBits(words, 5, 8, false);
|
|
921
|
+
if (bytes.length !== 32) {
|
|
922
|
+
throw new Error("npub: invalid data length");
|
|
923
|
+
}
|
|
924
|
+
const totalBits = words.length * 5;
|
|
925
|
+
const trailingBits = totalBits - bytes.length * 8;
|
|
926
|
+
if (trailingBits > 0) {
|
|
927
|
+
const lastWord = words[words.length - 1];
|
|
928
|
+
const mask = (1 << trailingBits) - 1;
|
|
929
|
+
if ((lastWord & mask) !== 0) {
|
|
930
|
+
throw new Error("npub: non-zero padding bits");
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
return bytes.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
934
|
+
}
|
|
935
|
+
var HEX64_RE = /^[0-9a-f]{64}$/;
|
|
936
|
+
function ownerToHex(owner) {
|
|
937
|
+
if (HEX64_RE.test(owner)) return owner;
|
|
938
|
+
if (owner.startsWith("npub1")) {
|
|
939
|
+
try {
|
|
940
|
+
return npubToHex(owner);
|
|
941
|
+
} catch (err) {
|
|
942
|
+
throw new Error(
|
|
943
|
+
`invalid owner ${JSON.stringify(owner)}: ${err instanceof Error ? err.message : String(err)}`
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
throw new Error(
|
|
948
|
+
`invalid owner ${JSON.stringify(owner)}: expected a 64-char lowercase hex pubkey or an npub1\u2026 string`
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// src/routes.ts
|
|
953
|
+
function serializeFeeEstimate(plan) {
|
|
954
|
+
return {
|
|
955
|
+
objectCount: plan.estimate.objectCount,
|
|
956
|
+
totalObjectBytes: plan.estimate.totalObjectBytes,
|
|
957
|
+
uploadFee: plan.estimate.uploadFee.toString(),
|
|
958
|
+
eventCount: plan.estimate.eventCount,
|
|
959
|
+
eventFees: plan.estimate.eventFees.toString(),
|
|
960
|
+
totalFee: plan.estimate.totalFee.toString(),
|
|
961
|
+
...plan.estimate.skippedEmptyCount > 0 ? { skippedEmptyCount: plan.estimate.skippedEmptyCount } : {}
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
function serializePushPlan(plan) {
|
|
965
|
+
return {
|
|
966
|
+
repoId: plan.repoId,
|
|
967
|
+
refUpdates: plan.refUpdates,
|
|
968
|
+
newRefs: plan.newRefs,
|
|
969
|
+
headSymref: plan.headSymref,
|
|
970
|
+
objects: plan.objects,
|
|
971
|
+
knownShaToTxId: Object.fromEntries(plan.knownShaToTxId),
|
|
972
|
+
announceNeeded: plan.announceNeeded,
|
|
973
|
+
announcement: plan.announcement,
|
|
974
|
+
estimate: serializeFeeEstimate(plan)
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
function serializeEventReceipt(kind, receipt) {
|
|
978
|
+
return {
|
|
979
|
+
eventId: receipt.eventId,
|
|
980
|
+
feePaid: receipt.feePaid.toString(),
|
|
981
|
+
kind
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
function serializePushResult(plan, result) {
|
|
985
|
+
return {
|
|
986
|
+
repoId: plan.repoId,
|
|
987
|
+
refUpdates: plan.refUpdates,
|
|
988
|
+
uploads: result.uploads.map((u) => ({
|
|
989
|
+
sha: u.sha,
|
|
990
|
+
txId: u.txId,
|
|
991
|
+
feePaid: u.feePaid.toString(),
|
|
992
|
+
skipped: u.skipped
|
|
993
|
+
})),
|
|
994
|
+
announceReceipt: result.announceReceipt ? {
|
|
995
|
+
eventId: result.announceReceipt.eventId,
|
|
996
|
+
feePaid: result.announceReceipt.feePaid.toString()
|
|
997
|
+
} : null,
|
|
998
|
+
refsReceipt: {
|
|
999
|
+
eventId: result.refsReceipt.eventId,
|
|
1000
|
+
feePaid: result.refsReceipt.feePaid.toString()
|
|
1001
|
+
},
|
|
1002
|
+
arweaveMap: Object.fromEntries(result.arweaveMap),
|
|
1003
|
+
totalFeePaid: result.totalFeePaid.toString(),
|
|
1004
|
+
estimate: serializeFeeEstimate(plan)
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// src/push.ts
|
|
1009
|
+
var NonFastForwardError = class extends Error {
|
|
1010
|
+
constructor(refs) {
|
|
1011
|
+
super(
|
|
1012
|
+
`non-fast-forward update rejected for ${refs.map((r) => r.refname).join(", ")} \u2014 re-run with force to overwrite the remote ref(s)`
|
|
1013
|
+
);
|
|
1014
|
+
this.refs = refs;
|
|
1015
|
+
this.name = "NonFastForwardError";
|
|
1016
|
+
}
|
|
1017
|
+
refs;
|
|
1018
|
+
};
|
|
1019
|
+
var OversizeObjectsError = class extends Error {
|
|
1020
|
+
constructor(objects) {
|
|
1021
|
+
super(
|
|
1022
|
+
`${objects.length} object(s) exceed the ${MAX_OBJECT_SIZE} byte upload limit: ` + objects.map((o) => `${o.path ?? o.sha} (${o.size} bytes)`).join(", ")
|
|
1023
|
+
);
|
|
1024
|
+
this.objects = objects;
|
|
1025
|
+
this.name = "OversizeObjectsError";
|
|
1026
|
+
}
|
|
1027
|
+
objects;
|
|
1028
|
+
};
|
|
1029
|
+
async function planPush(options) {
|
|
1030
|
+
const { repoReader, remoteState, feeRates, repoId, force = false } = options;
|
|
1031
|
+
const resolveMissing = options.resolveMissing ?? remoteState.resolveMissing.bind(remoteState);
|
|
1032
|
+
const { head, refs: localRefs } = await repoReader.listRefs();
|
|
1033
|
+
const localByName = new Map(localRefs.map((r) => [r.refname, r]));
|
|
1034
|
+
let selected;
|
|
1035
|
+
if (options.refs !== void 0) {
|
|
1036
|
+
selected = options.refs.map((name) => {
|
|
1037
|
+
const ref = localByName.get(name);
|
|
1038
|
+
if (!ref) {
|
|
1039
|
+
throw new Error(
|
|
1040
|
+
`ref ${JSON.stringify(name)} does not exist locally (ref deletion is out of scope in v1)`
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
1043
|
+
return ref;
|
|
1044
|
+
});
|
|
1045
|
+
} else {
|
|
1046
|
+
selected = localRefs;
|
|
1047
|
+
}
|
|
1048
|
+
const refUpdates = [];
|
|
1049
|
+
const rejected = [];
|
|
1050
|
+
for (const ref of selected) {
|
|
1051
|
+
const remoteSha = remoteState.refs.get(ref.refname) ?? null;
|
|
1052
|
+
if (remoteSha === null) {
|
|
1053
|
+
refUpdates.push({
|
|
1054
|
+
refname: ref.refname,
|
|
1055
|
+
localSha: ref.sha,
|
|
1056
|
+
remoteSha,
|
|
1057
|
+
kind: "new"
|
|
1058
|
+
});
|
|
1059
|
+
continue;
|
|
1060
|
+
}
|
|
1061
|
+
if (remoteSha === ref.sha) {
|
|
1062
|
+
refUpdates.push({
|
|
1063
|
+
refname: ref.refname,
|
|
1064
|
+
localSha: ref.sha,
|
|
1065
|
+
remoteSha,
|
|
1066
|
+
kind: "up-to-date"
|
|
1067
|
+
});
|
|
1068
|
+
continue;
|
|
1069
|
+
}
|
|
1070
|
+
let fastForward = false;
|
|
1071
|
+
try {
|
|
1072
|
+
fastForward = await repoReader.isAncestor(remoteSha, ref.sha);
|
|
1073
|
+
} catch (err) {
|
|
1074
|
+
if (!(err instanceof GitError)) throw err;
|
|
1075
|
+
fastForward = false;
|
|
1076
|
+
}
|
|
1077
|
+
if (fastForward) {
|
|
1078
|
+
refUpdates.push({
|
|
1079
|
+
refname: ref.refname,
|
|
1080
|
+
localSha: ref.sha,
|
|
1081
|
+
remoteSha,
|
|
1082
|
+
kind: "fast-forward"
|
|
1083
|
+
});
|
|
1084
|
+
} else if (force) {
|
|
1085
|
+
refUpdates.push({
|
|
1086
|
+
refname: ref.refname,
|
|
1087
|
+
localSha: ref.sha,
|
|
1088
|
+
remoteSha,
|
|
1089
|
+
kind: "forced"
|
|
1090
|
+
});
|
|
1091
|
+
} else {
|
|
1092
|
+
rejected.push({ refname: ref.refname, localSha: ref.sha, remoteSha });
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
if (rejected.length > 0) throw new NonFastForwardError(rejected);
|
|
1096
|
+
const updates = refUpdates.filter((u) => u.kind !== "up-to-date");
|
|
1097
|
+
const wantTips = [...new Set(updates.map((u) => u.localSha))];
|
|
1098
|
+
const haveTips = [...new Set(remoteState.refs.values())];
|
|
1099
|
+
const delta = wantTips.length > 0 ? await repoReader.objectsBetweenWithPaths(wantTips, haveTips) : [];
|
|
1100
|
+
const knownShaToTxId = new Map(remoteState.shaToTxId);
|
|
1101
|
+
let candidates = delta.filter((o) => !knownShaToTxId.has(o.sha));
|
|
1102
|
+
if (candidates.length > 0) {
|
|
1103
|
+
const resolved = await resolveMissing(candidates.map((o) => o.sha));
|
|
1104
|
+
for (const [sha, txId] of resolved) knownShaToTxId.set(sha, txId);
|
|
1105
|
+
candidates = candidates.filter((o) => !knownShaToTxId.has(o.sha));
|
|
1106
|
+
}
|
|
1107
|
+
const pathBySha = new Map(candidates.map((c) => [c.sha, c.path]));
|
|
1108
|
+
const { objects: stats, missing } = await repoReader.statObjects(
|
|
1109
|
+
candidates.map((c) => c.sha)
|
|
1110
|
+
);
|
|
1111
|
+
if (missing.length > 0) {
|
|
1112
|
+
throw new Error(
|
|
1113
|
+
`objects vanished from the local repository during planning: ${missing.join(", ")}`
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
const tipShas = new Set(updates.map((u) => u.localSha));
|
|
1117
|
+
const planned = [];
|
|
1118
|
+
const skippedEmptyObjects = [];
|
|
1119
|
+
for (const stat of stats) {
|
|
1120
|
+
const path = pathBySha.get(stat.sha);
|
|
1121
|
+
const object = {
|
|
1122
|
+
...stat,
|
|
1123
|
+
...path ? { path } : {},
|
|
1124
|
+
isRefTip: tipShas.has(stat.sha)
|
|
1125
|
+
};
|
|
1126
|
+
if (stat.sha === EMPTY_BLOB_SHA) skippedEmptyObjects.push(object);
|
|
1127
|
+
else planned.push(object);
|
|
1128
|
+
}
|
|
1129
|
+
const objects = [
|
|
1130
|
+
...planned.filter((o) => !o.isRefTip),
|
|
1131
|
+
...planned.filter((o) => o.isRefTip)
|
|
1132
|
+
];
|
|
1133
|
+
const newRefsMap = new Map(remoteState.refs);
|
|
1134
|
+
for (const update of updates) newRefsMap.set(update.refname, update.localSha);
|
|
1135
|
+
const headSymref = head && newRefsMap.has(head) ? head : remoteState.headSymref && newRefsMap.has(remoteState.headSymref) ? remoteState.headSymref : [...newRefsMap.keys()][0] ?? null;
|
|
1136
|
+
const newRefs = {};
|
|
1137
|
+
const headSha = headSymref ? newRefsMap.get(headSymref) : void 0;
|
|
1138
|
+
if (headSymref && headSha) newRefs[headSymref] = headSha;
|
|
1139
|
+
for (const [refname, sha] of newRefsMap) {
|
|
1140
|
+
if (refname !== headSymref) newRefs[refname] = sha;
|
|
1141
|
+
}
|
|
1142
|
+
const announceNeeded = !remoteState.announced;
|
|
1143
|
+
const totalObjectBytes = objects.reduce((sum, o) => sum + o.size, 0);
|
|
1144
|
+
const uploadFee = objects.reduce(
|
|
1145
|
+
(sum, o) => sum + uploadChargeFor(feeRates, o.size),
|
|
1146
|
+
0n
|
|
1147
|
+
);
|
|
1148
|
+
const eventCount = 1 + (announceNeeded ? 1 : 0);
|
|
1149
|
+
const eventFees = BigInt(eventCount) * feeRates.eventFee;
|
|
1150
|
+
return {
|
|
1151
|
+
repoId,
|
|
1152
|
+
refUpdates,
|
|
1153
|
+
newRefs,
|
|
1154
|
+
headSymref,
|
|
1155
|
+
objects,
|
|
1156
|
+
skippedEmptyObjects,
|
|
1157
|
+
knownShaToTxId,
|
|
1158
|
+
announceNeeded,
|
|
1159
|
+
announcement: {
|
|
1160
|
+
name: options.announcement?.name ?? repoId,
|
|
1161
|
+
description: options.announcement?.description ?? ""
|
|
1162
|
+
},
|
|
1163
|
+
estimate: {
|
|
1164
|
+
objectCount: objects.length,
|
|
1165
|
+
totalObjectBytes,
|
|
1166
|
+
uploadFee,
|
|
1167
|
+
eventCount,
|
|
1168
|
+
eventFees,
|
|
1169
|
+
totalFee: uploadFee + eventFees,
|
|
1170
|
+
skippedEmptyCount: skippedEmptyObjects.length
|
|
1171
|
+
}
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
var READ_BATCH_SIZE = 100;
|
|
1175
|
+
async function executePush(options) {
|
|
1176
|
+
const { plan, publisher, remoteState, repoReader, relayUrls } = options;
|
|
1177
|
+
const merged = new Map([...remoteState.shaToTxId, ...plan.knownShaToTxId]);
|
|
1178
|
+
const resultBySha = /* @__PURE__ */ new Map();
|
|
1179
|
+
let totalFeePaid = 0n;
|
|
1180
|
+
const pending = [];
|
|
1181
|
+
for (const object of plan.objects) {
|
|
1182
|
+
const knownTxId = merged.get(object.sha);
|
|
1183
|
+
if (knownTxId !== void 0) {
|
|
1184
|
+
resultBySha.set(object.sha, {
|
|
1185
|
+
sha: object.sha,
|
|
1186
|
+
txId: knownTxId,
|
|
1187
|
+
feePaid: 0n,
|
|
1188
|
+
skipped: true
|
|
1189
|
+
});
|
|
1190
|
+
} else {
|
|
1191
|
+
pending.push(object);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
for (let i = 0; i < pending.length; i += READ_BATCH_SIZE) {
|
|
1195
|
+
const batch = pending.slice(i, i + READ_BATCH_SIZE);
|
|
1196
|
+
const { objects: read, missing } = await repoReader.readObjects(
|
|
1197
|
+
batch.map((o) => o.sha)
|
|
1198
|
+
);
|
|
1199
|
+
if (missing.length > 0) {
|
|
1200
|
+
throw new Error(
|
|
1201
|
+
`objects vanished from the local repository during push: ${missing.join(", ")}`
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
const bodyBySha = new Map(read.map((r) => [r.sha, r.body]));
|
|
1205
|
+
for (const object of batch) {
|
|
1206
|
+
const body = bodyBySha.get(object.sha);
|
|
1207
|
+
if (!body) {
|
|
1208
|
+
throw new Error(
|
|
1209
|
+
`internal: cat-file returned no body for ${object.sha}`
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
const receipt = await publisher.uploadGitObject({
|
|
1213
|
+
sha: object.sha,
|
|
1214
|
+
type: object.type,
|
|
1215
|
+
body,
|
|
1216
|
+
repoId: plan.repoId,
|
|
1217
|
+
// #368: the path the blob was reached by drives its Content-Type; a
|
|
1218
|
+
// non-blob object (no path) uploads as octet-stream.
|
|
1219
|
+
...object.path ? { path: object.path } : {}
|
|
1220
|
+
});
|
|
1221
|
+
merged.set(object.sha, receipt.txId);
|
|
1222
|
+
totalFeePaid += receipt.feePaid;
|
|
1223
|
+
resultBySha.set(object.sha, {
|
|
1224
|
+
sha: object.sha,
|
|
1225
|
+
txId: receipt.txId,
|
|
1226
|
+
feePaid: receipt.feePaid,
|
|
1227
|
+
skipped: false
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
let announceReceipt = null;
|
|
1232
|
+
if (plan.announceNeeded && !remoteState.announced) {
|
|
1233
|
+
const announceEvent = buildRepoAnnouncement(
|
|
1234
|
+
plan.repoId,
|
|
1235
|
+
plan.announcement.name,
|
|
1236
|
+
plan.announcement.description
|
|
1237
|
+
);
|
|
1238
|
+
announceReceipt = await publisher.publishEvent(announceEvent, relayUrls);
|
|
1239
|
+
totalFeePaid += announceReceipt.feePaid;
|
|
1240
|
+
}
|
|
1241
|
+
const refsEvent = buildRepoRefs(
|
|
1242
|
+
plan.repoId,
|
|
1243
|
+
plan.newRefs,
|
|
1244
|
+
Object.fromEntries(merged)
|
|
1245
|
+
);
|
|
1246
|
+
const refsReceipt = await publisher.publishEvent(refsEvent, relayUrls);
|
|
1247
|
+
totalFeePaid += refsReceipt.feePaid;
|
|
1248
|
+
const uploads = plan.objects.map((o) => {
|
|
1249
|
+
const step = resultBySha.get(o.sha);
|
|
1250
|
+
if (!step) {
|
|
1251
|
+
throw new Error(`internal: no upload result recorded for ${o.sha}`);
|
|
1252
|
+
}
|
|
1253
|
+
return step;
|
|
1254
|
+
});
|
|
1255
|
+
return {
|
|
1256
|
+
uploads,
|
|
1257
|
+
announceReceipt,
|
|
1258
|
+
refsReceipt,
|
|
1259
|
+
arweaveMap: merged,
|
|
1260
|
+
totalFeePaid
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// src/ci/nip-c1-events.ts
|
|
1265
|
+
var CI_ADVERTISEMENT_KIND = 19843;
|
|
1266
|
+
var CI_SERVICE_REQUEST_KIND = 9843;
|
|
1267
|
+
var CI_SERVICE_STOP_KIND = 9844;
|
|
1268
|
+
var CI_SECRET_UPDATE_KIND = 29846;
|
|
1269
|
+
var CI_MANUAL_TRIGGER_KIND = 9840;
|
|
1270
|
+
var CI_JOB_RESULT_KIND = 9841;
|
|
1271
|
+
var CI_WORKFLOW_RESULT_KIND = 9842;
|
|
1272
|
+
var CI_WORKFLOW_PROGRESS_KIND = 39842;
|
|
1273
|
+
var CI_RUNNER_FAMILY = "act";
|
|
1274
|
+
var CI_SOFTWARE = "rig";
|
|
1275
|
+
var CI_MAX_ADVERTISEMENT_TTL = 30 * 60;
|
|
1276
|
+
var CI_MAX_PROGRESS_TTL = 30 * 60;
|
|
1277
|
+
var REPO_ANNOUNCEMENT_KIND = 30617;
|
|
1278
|
+
var CONCLUSIONS = /* @__PURE__ */ new Set([
|
|
1279
|
+
"success",
|
|
1280
|
+
"failure",
|
|
1281
|
+
"neutral",
|
|
1282
|
+
"cancelled",
|
|
1283
|
+
"skipped",
|
|
1284
|
+
"timed_out",
|
|
1285
|
+
"startup_failure"
|
|
1286
|
+
]);
|
|
1287
|
+
function isCiConclusion(value) {
|
|
1288
|
+
return CONCLUSIONS.has(value);
|
|
1289
|
+
}
|
|
1290
|
+
var PROGRESS_STATUSES = /* @__PURE__ */ new Set([
|
|
1291
|
+
"queued",
|
|
1292
|
+
"in_progress",
|
|
1293
|
+
"concluded"
|
|
1294
|
+
]);
|
|
1295
|
+
var TRIGGER_REASONS = /* @__PURE__ */ new Set([
|
|
1296
|
+
"push",
|
|
1297
|
+
"pull_request",
|
|
1298
|
+
"manual"
|
|
1299
|
+
]);
|
|
1300
|
+
var ADMISSIONS = /* @__PURE__ */ new Set([
|
|
1301
|
+
"operator-selected",
|
|
1302
|
+
"maintainer-request",
|
|
1303
|
+
"open"
|
|
1304
|
+
]);
|
|
1305
|
+
var EXECUTIONS = /* @__PURE__ */ new Set([
|
|
1306
|
+
"automatic",
|
|
1307
|
+
"request-required"
|
|
1308
|
+
]);
|
|
1309
|
+
var BILLINGS = /* @__PURE__ */ new Set([
|
|
1310
|
+
"not-required",
|
|
1311
|
+
"out-of-band"
|
|
1312
|
+
]);
|
|
1313
|
+
var HEX64 = /^[0-9a-f]{64}$/;
|
|
1314
|
+
var GIT_OID = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;
|
|
1315
|
+
var REPO_ADDR_RE = /^30617:([0-9a-fA-F]{64}):(.+)$/;
|
|
1316
|
+
function repoAddress(ownerPubkey, repoId) {
|
|
1317
|
+
return `${REPO_ANNOUNCEMENT_KIND}:${ownerPubkey.toLowerCase()}:${repoId}`;
|
|
1318
|
+
}
|
|
1319
|
+
function parseRepoAddress(a) {
|
|
1320
|
+
const m = REPO_ADDR_RE.exec(a);
|
|
1321
|
+
if (!m) return null;
|
|
1322
|
+
return {
|
|
1323
|
+
ownerPubkey: m[1].toLowerCase(),
|
|
1324
|
+
repoId: m[2]
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
var PROVENANCE_MARKERS = /* @__PURE__ */ new Set([
|
|
1328
|
+
"service-request",
|
|
1329
|
+
"manual-trigger"
|
|
1330
|
+
]);
|
|
1331
|
+
function triggerTags(trigger, opts = {}) {
|
|
1332
|
+
const tags = [["a", trigger.repoAddr]];
|
|
1333
|
+
for (const a of trigger.extraRepoAddrs ?? []) tags.push(["a", a]);
|
|
1334
|
+
tags.push(["c", trigger.commit]);
|
|
1335
|
+
for (const c of trigger.tagObjectIds ?? []) tags.push(["c", c]);
|
|
1336
|
+
tags.push(["w", trigger.workflow.path, trigger.workflow.sha256]);
|
|
1337
|
+
if (!opts.omitReason && trigger.reason !== void 0) {
|
|
1338
|
+
tags.push(["o", trigger.reason]);
|
|
1339
|
+
}
|
|
1340
|
+
if (trigger.pr) {
|
|
1341
|
+
tags.push(["E", trigger.pr.prEventId]);
|
|
1342
|
+
tags.push(["K", String(trigger.pr.prKind)]);
|
|
1343
|
+
tags.push(["P", trigger.pr.prAuthor]);
|
|
1344
|
+
tags.push(["e", trigger.pr.sourceEventId]);
|
|
1345
|
+
tags.push(["k", String(trigger.pr.sourceKind)]);
|
|
1346
|
+
if (!opts.omitPrParentAuthor) tags.push(["p", trigger.pr.sourceAuthor]);
|
|
1347
|
+
} else if (trigger.ref !== void 0) {
|
|
1348
|
+
tags.push(["r", trigger.ref]);
|
|
1349
|
+
}
|
|
1350
|
+
return tags;
|
|
1351
|
+
}
|
|
1352
|
+
function commonTriggerTags(trigger) {
|
|
1353
|
+
return triggerTags(trigger);
|
|
1354
|
+
}
|
|
1355
|
+
function values(tags, name) {
|
|
1356
|
+
return tags.filter((t) => t[0] === name);
|
|
1357
|
+
}
|
|
1358
|
+
function single(tags, name) {
|
|
1359
|
+
const found = values(tags, name);
|
|
1360
|
+
return found.length === 1 ? found[0] : null;
|
|
1361
|
+
}
|
|
1362
|
+
function optionalSingle(tags, name) {
|
|
1363
|
+
const found = values(tags, name);
|
|
1364
|
+
if (found.length === 0) return void 0;
|
|
1365
|
+
return found.length === 1 ? found[0] : null;
|
|
1366
|
+
}
|
|
1367
|
+
function parsePrKind(value, allowUpdate) {
|
|
1368
|
+
if (value === "1617") return 1617;
|
|
1369
|
+
if (value === "1618") return 1618;
|
|
1370
|
+
if (allowUpdate && value === "1619") return 1619;
|
|
1371
|
+
return null;
|
|
1372
|
+
}
|
|
1373
|
+
function parseCiTriggerContext(tags, opts = {}) {
|
|
1374
|
+
const aTags = values(tags, "a");
|
|
1375
|
+
if (aTags.length === 0) return null;
|
|
1376
|
+
const addrs = [];
|
|
1377
|
+
let repoId = null;
|
|
1378
|
+
for (const t of aTags) {
|
|
1379
|
+
const parsed = t[1] === void 0 ? null : parseRepoAddress(t[1]);
|
|
1380
|
+
if (!parsed) return null;
|
|
1381
|
+
if (repoId !== null && parsed.repoId !== repoId) return null;
|
|
1382
|
+
repoId = parsed.repoId;
|
|
1383
|
+
addrs.push(repoAddress(parsed.ownerPubkey, parsed.repoId));
|
|
1384
|
+
}
|
|
1385
|
+
const cTags = values(tags, "c");
|
|
1386
|
+
if (cTags.length === 0) return null;
|
|
1387
|
+
const commits = [];
|
|
1388
|
+
for (const t of cTags) {
|
|
1389
|
+
const c = t[1]?.toLowerCase();
|
|
1390
|
+
if (c === void 0 || !GIT_OID.test(c)) return null;
|
|
1391
|
+
commits.push(c);
|
|
1392
|
+
}
|
|
1393
|
+
const w = single(tags, "w");
|
|
1394
|
+
if (!w || w.length < 3 || !w[1] || !HEX64.test(w[2].toLowerCase()))
|
|
1395
|
+
return null;
|
|
1396
|
+
const o = optionalSingle(tags, "o");
|
|
1397
|
+
if (o === null) return null;
|
|
1398
|
+
let reason;
|
|
1399
|
+
if (o === void 0) {
|
|
1400
|
+
if (opts.defaultReason === void 0) return null;
|
|
1401
|
+
reason = opts.defaultReason;
|
|
1402
|
+
} else {
|
|
1403
|
+
const v = o[1] ?? "";
|
|
1404
|
+
if (!TRIGGER_REASONS.has(v)) return null;
|
|
1405
|
+
reason = v;
|
|
1406
|
+
}
|
|
1407
|
+
const trigger = {
|
|
1408
|
+
repoAddr: addrs[0],
|
|
1409
|
+
commit: commits[0],
|
|
1410
|
+
workflow: { path: w[1], sha256: w[2].toLowerCase() },
|
|
1411
|
+
reason
|
|
1412
|
+
};
|
|
1413
|
+
if (addrs.length > 1) trigger.extraRepoAddrs = addrs.slice(1);
|
|
1414
|
+
if (commits.length > 1) trigger.tagObjectIds = commits.slice(1);
|
|
1415
|
+
const E = optionalSingle(tags, "E");
|
|
1416
|
+
if (E === null) return null;
|
|
1417
|
+
if (E !== void 0) {
|
|
1418
|
+
const K = single(tags, "K");
|
|
1419
|
+
const P = single(tags, "P");
|
|
1420
|
+
const prKind = parsePrKind(K?.[1], false);
|
|
1421
|
+
const prEventId = E[1]?.toLowerCase();
|
|
1422
|
+
const prAuthor = P?.[1]?.toLowerCase();
|
|
1423
|
+
if (prKind === null || prEventId === void 0 || !HEX64.test(prEventId) || prAuthor === void 0 || !HEX64.test(prAuthor)) {
|
|
1424
|
+
return null;
|
|
1425
|
+
}
|
|
1426
|
+
const e = optionalSingle(tags, "e");
|
|
1427
|
+
const k = optionalSingle(tags, "k");
|
|
1428
|
+
const p = optionalSingle(tags, "p");
|
|
1429
|
+
if (e === null || k === null || p === null) return null;
|
|
1430
|
+
let sourceEventId = prEventId;
|
|
1431
|
+
let sourceKind = prKind;
|
|
1432
|
+
if (e !== void 0) {
|
|
1433
|
+
const id = e[1]?.toLowerCase();
|
|
1434
|
+
if (id === void 0 || !HEX64.test(id)) return null;
|
|
1435
|
+
sourceEventId = id;
|
|
1436
|
+
const parsedKind = parsePrKind(k?.[1], true);
|
|
1437
|
+
if (k !== void 0 && parsedKind === null) return null;
|
|
1438
|
+
sourceKind = parsedKind ?? prKind;
|
|
1439
|
+
}
|
|
1440
|
+
let sourceAuthor = prAuthor;
|
|
1441
|
+
if (p !== void 0) {
|
|
1442
|
+
const author = p[1]?.toLowerCase();
|
|
1443
|
+
if (author === void 0 || !HEX64.test(author)) return null;
|
|
1444
|
+
sourceAuthor = author;
|
|
1445
|
+
}
|
|
1446
|
+
trigger.pr = {
|
|
1447
|
+
prEventId,
|
|
1448
|
+
prAuthor,
|
|
1449
|
+
prKind,
|
|
1450
|
+
sourceEventId,
|
|
1451
|
+
sourceAuthor,
|
|
1452
|
+
sourceKind
|
|
1453
|
+
};
|
|
1454
|
+
if (values(tags, "r").some((t) => t[1]?.startsWith("refs/"))) return null;
|
|
1455
|
+
} else {
|
|
1456
|
+
const refs = values(tags, "r").filter((t) => t[1]?.startsWith("refs/"));
|
|
1457
|
+
if (refs.length > 1) return null;
|
|
1458
|
+
if (refs.length === 1) trigger.ref = refs[0][1];
|
|
1459
|
+
}
|
|
1460
|
+
return trigger;
|
|
1461
|
+
}
|
|
1462
|
+
function provenanceTag(p) {
|
|
1463
|
+
return ["q", p.eventId, p.relayUrl, p.pubkey, p.kind];
|
|
1464
|
+
}
|
|
1465
|
+
function parseProvenance(tags) {
|
|
1466
|
+
const quotes = values(tags, "q").filter(
|
|
1467
|
+
(t) => t[4] !== void 0 && PROVENANCE_MARKERS.has(t[4])
|
|
1468
|
+
);
|
|
1469
|
+
if (quotes.length === 0) return void 0;
|
|
1470
|
+
if (quotes.length > 1) return null;
|
|
1471
|
+
const q = quotes[0];
|
|
1472
|
+
const eventId = q[1]?.toLowerCase();
|
|
1473
|
+
const pubkey = q[3]?.toLowerCase();
|
|
1474
|
+
if (eventId === void 0 || !HEX64.test(eventId) || pubkey === void 0 || !HEX64.test(pubkey)) {
|
|
1475
|
+
return null;
|
|
1476
|
+
}
|
|
1477
|
+
return {
|
|
1478
|
+
kind: q[4],
|
|
1479
|
+
eventId,
|
|
1480
|
+
relayUrl: q[2] ?? "",
|
|
1481
|
+
pubkey
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
function now() {
|
|
1485
|
+
return Math.floor(Date.now() / 1e3);
|
|
1486
|
+
}
|
|
1487
|
+
function assertExpiration(createdAt, expiresAt, maxTtl) {
|
|
1488
|
+
if (!Number.isInteger(expiresAt) || expiresAt <= createdAt) {
|
|
1489
|
+
throw new Error("expiration must be later than created_at");
|
|
1490
|
+
}
|
|
1491
|
+
if (expiresAt - createdAt > maxTtl) {
|
|
1492
|
+
throw new Error(
|
|
1493
|
+
`expiration must be no more than ${maxTtl}s after created_at`
|
|
1494
|
+
);
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
function foldUnique(items) {
|
|
1498
|
+
const out = [];
|
|
1499
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1500
|
+
for (const raw of items) {
|
|
1501
|
+
const v = raw.toLowerCase();
|
|
1502
|
+
if (!seen.has(v)) {
|
|
1503
|
+
seen.add(v);
|
|
1504
|
+
out.push(v);
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
return out;
|
|
1508
|
+
}
|
|
1509
|
+
function buildCiAdvertisement(input, createdAt = now()) {
|
|
1510
|
+
const selectors = foldUnique(input.selectors);
|
|
1511
|
+
if (selectors.length === 0) {
|
|
1512
|
+
throw new Error("an advertisement needs at least one runner selector");
|
|
1513
|
+
}
|
|
1514
|
+
if (input.secretsKey && input.secretsKey.inboxRelays.length === 0) {
|
|
1515
|
+
throw new Error("secrets-key needs at least one secret-inbox relay");
|
|
1516
|
+
}
|
|
1517
|
+
assertExpiration(createdAt, input.expiresAt, CI_MAX_ADVERTISEMENT_TTL);
|
|
1518
|
+
const tags = [
|
|
1519
|
+
["software", CI_SOFTWARE, input.version],
|
|
1520
|
+
["W", CI_RUNNER_FAMILY],
|
|
1521
|
+
...selectors.map((s) => ["R", `${CI_RUNNER_FAMILY}:${s}`]),
|
|
1522
|
+
["M", input.admission],
|
|
1523
|
+
["X", input.execution]
|
|
1524
|
+
];
|
|
1525
|
+
if (input.billing) tags.push(["B", input.billing]);
|
|
1526
|
+
if (input.secretsKey) {
|
|
1527
|
+
tags.push([
|
|
1528
|
+
"secrets-key",
|
|
1529
|
+
"nip44-v2",
|
|
1530
|
+
input.secretsKey.pubkey.toLowerCase(),
|
|
1531
|
+
...input.secretsKey.inboxRelays
|
|
1532
|
+
]);
|
|
1533
|
+
}
|
|
1534
|
+
tags.push(["expiration", String(input.expiresAt)]);
|
|
1535
|
+
return {
|
|
1536
|
+
kind: CI_ADVERTISEMENT_KIND,
|
|
1537
|
+
content: "",
|
|
1538
|
+
tags,
|
|
1539
|
+
created_at: createdAt
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
function parseCiAdvertisement(ev) {
|
|
1543
|
+
if (ev.kind !== CI_ADVERTISEMENT_KIND || ev.content !== "") return null;
|
|
1544
|
+
const { tags } = ev;
|
|
1545
|
+
if (values(tags, "d").length > 0) return null;
|
|
1546
|
+
const M = single(tags, "M");
|
|
1547
|
+
const X = single(tags, "X");
|
|
1548
|
+
const exp = single(tags, "expiration");
|
|
1549
|
+
if (!M || !X || !exp) return null;
|
|
1550
|
+
if (!ADMISSIONS.has(M[1] ?? "") || !EXECUTIONS.has(X[1] ?? "")) return null;
|
|
1551
|
+
const B = optionalSingle(tags, "B");
|
|
1552
|
+
if (B === null || B !== void 0 && !BILLINGS.has(B[1] ?? "")) return null;
|
|
1553
|
+
const expiresAt = Number(exp[1]);
|
|
1554
|
+
if (!Number.isInteger(expiresAt) || expiresAt <= ev.created_at) return null;
|
|
1555
|
+
const families = foldUnique(
|
|
1556
|
+
values(tags, "W").map((t) => t[1] ?? "").filter((v) => v !== "")
|
|
1557
|
+
);
|
|
1558
|
+
const selectors = foldUnique(
|
|
1559
|
+
values(tags, "R").map((t) => t[1] ?? "").filter((v) => v !== "")
|
|
1560
|
+
);
|
|
1561
|
+
if (families.length === 0 || selectors.length === 0) return null;
|
|
1562
|
+
const covered = /* @__PURE__ */ new Set();
|
|
1563
|
+
for (const s of selectors) {
|
|
1564
|
+
const family = s.slice(0, s.indexOf(":"));
|
|
1565
|
+
if (!family || !families.includes(family)) return null;
|
|
1566
|
+
covered.add(family);
|
|
1567
|
+
}
|
|
1568
|
+
if (covered.size !== families.length) return null;
|
|
1569
|
+
const software = optionalSingle(tags, "software");
|
|
1570
|
+
if (software === null) return null;
|
|
1571
|
+
const sk = optionalSingle(tags, "secrets-key");
|
|
1572
|
+
if (sk === null) return null;
|
|
1573
|
+
let secretsKey;
|
|
1574
|
+
if (sk !== void 0) {
|
|
1575
|
+
const pubkey = sk[2]?.toLowerCase();
|
|
1576
|
+
const inboxRelays = sk.slice(3).filter((u) => /^wss?:\/\//i.test(u));
|
|
1577
|
+
if (sk[1] !== "nip44-v2" || pubkey === void 0 || !HEX64.test(pubkey) || inboxRelays.length === 0) {
|
|
1578
|
+
return null;
|
|
1579
|
+
}
|
|
1580
|
+
secretsKey = { pubkey, inboxRelays };
|
|
1581
|
+
}
|
|
1582
|
+
const result = {
|
|
1583
|
+
eventId: ev.id,
|
|
1584
|
+
pubkey: ev.pubkey.toLowerCase(),
|
|
1585
|
+
createdAt: ev.created_at,
|
|
1586
|
+
version: software?.[2] ?? "",
|
|
1587
|
+
families,
|
|
1588
|
+
selectors,
|
|
1589
|
+
admission: M[1],
|
|
1590
|
+
execution: X[1],
|
|
1591
|
+
expiresAt
|
|
1592
|
+
};
|
|
1593
|
+
if (B !== void 0) result.billing = B[1];
|
|
1594
|
+
if (secretsKey) result.secretsKey = secretsKey;
|
|
1595
|
+
return result;
|
|
1596
|
+
}
|
|
1597
|
+
function buildServiceControl(kind, repoAddr, coordinatorPubkey, relayHint, createdAt) {
|
|
1598
|
+
const a = relayHint ? ["a", repoAddr, relayHint] : ["a", repoAddr];
|
|
1599
|
+
return {
|
|
1600
|
+
kind,
|
|
1601
|
+
content: "",
|
|
1602
|
+
tags: [a, ["p", coordinatorPubkey.toLowerCase()]],
|
|
1603
|
+
created_at: createdAt
|
|
1604
|
+
};
|
|
1605
|
+
}
|
|
1606
|
+
function buildCiServiceRequest(repoAddr, coordinatorPubkey, relayHint, createdAt = now()) {
|
|
1607
|
+
return buildServiceControl(
|
|
1608
|
+
CI_SERVICE_REQUEST_KIND,
|
|
1609
|
+
repoAddr,
|
|
1610
|
+
coordinatorPubkey,
|
|
1611
|
+
relayHint,
|
|
1612
|
+
createdAt
|
|
1613
|
+
);
|
|
1614
|
+
}
|
|
1615
|
+
function buildCiServiceStop(repoAddr, coordinatorPubkey, relayHint, createdAt = now()) {
|
|
1616
|
+
return buildServiceControl(
|
|
1617
|
+
CI_SERVICE_STOP_KIND,
|
|
1618
|
+
repoAddr,
|
|
1619
|
+
coordinatorPubkey,
|
|
1620
|
+
relayHint,
|
|
1621
|
+
createdAt
|
|
1622
|
+
);
|
|
1623
|
+
}
|
|
1624
|
+
function parseCiServiceControl(ev) {
|
|
1625
|
+
if (ev.kind !== CI_SERVICE_REQUEST_KIND && ev.kind !== CI_SERVICE_STOP_KIND)
|
|
1626
|
+
return null;
|
|
1627
|
+
if (ev.content !== "") return null;
|
|
1628
|
+
const { tags } = ev;
|
|
1629
|
+
if (values(tags, "d").length > 0 || values(tags, "expiration").length > 0)
|
|
1630
|
+
return null;
|
|
1631
|
+
const a = single(tags, "a");
|
|
1632
|
+
const p = single(tags, "p");
|
|
1633
|
+
if (!a || !p) return null;
|
|
1634
|
+
const addr = a[1] === void 0 ? null : parseRepoAddress(a[1]);
|
|
1635
|
+
const coordinator = p[1]?.toLowerCase();
|
|
1636
|
+
if (!addr || coordinator === void 0 || !HEX64.test(coordinator))
|
|
1637
|
+
return null;
|
|
1638
|
+
const control = {
|
|
1639
|
+
kind: ev.kind === CI_SERVICE_REQUEST_KIND ? "request" : "stop",
|
|
1640
|
+
eventId: ev.id,
|
|
1641
|
+
pubkey: ev.pubkey.toLowerCase(),
|
|
1642
|
+
createdAt: ev.created_at,
|
|
1643
|
+
repoAddr: repoAddress(addr.ownerPubkey, addr.repoId),
|
|
1644
|
+
coordinatorPubkey: coordinator
|
|
1645
|
+
};
|
|
1646
|
+
if (a[2]) control.relayHint = a[2];
|
|
1647
|
+
return control;
|
|
1648
|
+
}
|
|
1649
|
+
function controlOrder(a, b) {
|
|
1650
|
+
if (a.createdAt !== b.createdAt) return a.createdAt - b.createdAt;
|
|
1651
|
+
if (a.eventId === b.eventId) return 0;
|
|
1652
|
+
return a.eventId < b.eventId ? 1 : -1;
|
|
1653
|
+
}
|
|
1654
|
+
function selectServiceRequests(controls, opts) {
|
|
1655
|
+
const coordinator = opts.coordinatorPubkey.toLowerCase();
|
|
1656
|
+
const authorized = new Set([...opts.authorized].map((p) => p.toLowerCase()));
|
|
1657
|
+
const accepted = /* @__PURE__ */ new Set([
|
|
1658
|
+
...authorized,
|
|
1659
|
+
...[...opts.acceptedRequesters ?? []].map((p) => p.toLowerCase())
|
|
1660
|
+
]);
|
|
1661
|
+
const relevant = controls.filter(
|
|
1662
|
+
(c) => c.coordinatorPubkey.toLowerCase() === coordinator && (opts.repoAddr === void 0 || c.repoAddr === opts.repoAddr)
|
|
1663
|
+
).sort(controlOrder);
|
|
1664
|
+
let open = [];
|
|
1665
|
+
for (const control of relevant) {
|
|
1666
|
+
const author = control.pubkey.toLowerCase();
|
|
1667
|
+
if (control.kind === "request") {
|
|
1668
|
+
if (accepted.has(author)) open.push(control);
|
|
1669
|
+
continue;
|
|
1670
|
+
}
|
|
1671
|
+
open = authorized.has(author) ? [] : open.filter((r) => r.pubkey.toLowerCase() !== author);
|
|
1672
|
+
}
|
|
1673
|
+
return {
|
|
1674
|
+
active: open.length > 0 ? open[open.length - 1] : null,
|
|
1675
|
+
open
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1678
|
+
function buildCiManualTrigger(coordinatorPubkey, trigger, createdAt = now()) {
|
|
1679
|
+
return {
|
|
1680
|
+
kind: CI_MANUAL_TRIGGER_KIND,
|
|
1681
|
+
content: "",
|
|
1682
|
+
tags: [
|
|
1683
|
+
["p", coordinatorPubkey.toLowerCase()],
|
|
1684
|
+
...triggerTags(trigger, { omitReason: true, omitPrParentAuthor: true })
|
|
1685
|
+
],
|
|
1686
|
+
created_at: createdAt
|
|
1687
|
+
};
|
|
1688
|
+
}
|
|
1689
|
+
function parseCiManualTrigger(ev, coordinatorPubkey) {
|
|
1690
|
+
if (ev.kind !== CI_MANUAL_TRIGGER_KIND || ev.content !== "") return null;
|
|
1691
|
+
const p = single(ev.tags, "p");
|
|
1692
|
+
const coordinator = p?.[1]?.toLowerCase();
|
|
1693
|
+
if (!p || coordinator === void 0 || !HEX64.test(coordinator)) return null;
|
|
1694
|
+
if (coordinatorPubkey !== void 0 && coordinator !== coordinatorPubkey.toLowerCase())
|
|
1695
|
+
return null;
|
|
1696
|
+
if (values(ev.tags, "o").length > 0) return null;
|
|
1697
|
+
const rest = ev.tags.filter((t) => t[0] !== "p");
|
|
1698
|
+
const parsed = parseCiTriggerContext(rest, { defaultReason: "manual" });
|
|
1699
|
+
if (!parsed) return null;
|
|
1700
|
+
const { reason: _reason, ...trigger } = parsed;
|
|
1701
|
+
return {
|
|
1702
|
+
eventId: ev.id,
|
|
1703
|
+
pubkey: ev.pubkey.toLowerCase(),
|
|
1704
|
+
createdAt: ev.created_at,
|
|
1705
|
+
coordinatorPubkey: coordinator,
|
|
1706
|
+
trigger
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
var SECRET_UPDATE_TAGS = ["a", "p", "e", "sender", "recipient", "encryption"];
|
|
1710
|
+
function buildCiSecretUpdate(args) {
|
|
1711
|
+
return {
|
|
1712
|
+
kind: CI_SECRET_UPDATE_KIND,
|
|
1713
|
+
content: args.ciphertext,
|
|
1714
|
+
tags: [
|
|
1715
|
+
["a", args.repoAddr],
|
|
1716
|
+
["p", args.coordinatorPubkey.toLowerCase()],
|
|
1717
|
+
[
|
|
1718
|
+
"e",
|
|
1719
|
+
args.advertisementId,
|
|
1720
|
+
args.advertisementRelayHint ?? "",
|
|
1721
|
+
"secrets-key"
|
|
1722
|
+
],
|
|
1723
|
+
["sender", args.senderPubkey.toLowerCase()],
|
|
1724
|
+
["recipient", args.recipientPubkey.toLowerCase()],
|
|
1725
|
+
["encryption", "nip44-v2"]
|
|
1726
|
+
],
|
|
1727
|
+
created_at: args.createdAt
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1730
|
+
function parseCiSecretUpdate(ev) {
|
|
1731
|
+
if (ev.kind !== CI_SECRET_UPDATE_KIND) return null;
|
|
1732
|
+
if (ev.content === "" || ev.content.length > 100 * 1024) return null;
|
|
1733
|
+
const { tags } = ev;
|
|
1734
|
+
if (tags.length !== SECRET_UPDATE_TAGS.length) return null;
|
|
1735
|
+
for (const name of SECRET_UPDATE_TAGS) {
|
|
1736
|
+
if (values(tags, name).length !== 1) return null;
|
|
1737
|
+
}
|
|
1738
|
+
const a = single(tags, "a");
|
|
1739
|
+
const p = single(tags, "p");
|
|
1740
|
+
const e = single(tags, "e");
|
|
1741
|
+
const sender = single(tags, "sender");
|
|
1742
|
+
const recipient = single(tags, "recipient");
|
|
1743
|
+
const encryption = single(tags, "encryption");
|
|
1744
|
+
const addr = a[1] === void 0 ? null : parseRepoAddress(a[1]);
|
|
1745
|
+
if (!addr || addr.ownerPubkey !== ev.pubkey.toLowerCase()) return null;
|
|
1746
|
+
const coordinator = p[1]?.toLowerCase();
|
|
1747
|
+
const advertisementId = e[1]?.toLowerCase();
|
|
1748
|
+
const senderPubkey = sender[1]?.toLowerCase();
|
|
1749
|
+
const recipientPubkey = recipient[1]?.toLowerCase();
|
|
1750
|
+
if (coordinator === void 0 || !HEX64.test(coordinator) || advertisementId === void 0 || !HEX64.test(advertisementId) || e[3] !== "secrets-key" || senderPubkey === void 0 || !HEX64.test(senderPubkey) || recipientPubkey === void 0 || !HEX64.test(recipientPubkey) || encryption[1] !== "nip44-v2") {
|
|
1751
|
+
return null;
|
|
1752
|
+
}
|
|
1753
|
+
return {
|
|
1754
|
+
eventId: ev.id,
|
|
1755
|
+
pubkey: ev.pubkey.toLowerCase(),
|
|
1756
|
+
createdAt: ev.created_at,
|
|
1757
|
+
repoAddr: repoAddress(addr.ownerPubkey, addr.repoId),
|
|
1758
|
+
coordinatorPubkey: coordinator,
|
|
1759
|
+
advertisementId,
|
|
1760
|
+
senderPubkey,
|
|
1761
|
+
recipientPubkey,
|
|
1762
|
+
ciphertext: ev.content
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
var LOG_TAIL_HEADER = /^\[log-tail omitted=(\d+)\]\n?/;
|
|
1766
|
+
var PROGRESS_ADDR_RE = /^39842:[0-9a-f]{64}:.+$/i;
|
|
1767
|
+
function buildCiJobResult(input, createdAt = now()) {
|
|
1768
|
+
const tags = [
|
|
1769
|
+
...commonTriggerTags(input.trigger),
|
|
1770
|
+
["q", input.progressAddress, input.relayUrl],
|
|
1771
|
+
["job", input.jobId]
|
|
1772
|
+
];
|
|
1773
|
+
if (input.name !== void 0) tags.push(["name", input.name]);
|
|
1774
|
+
tags.push(["conclusion", input.conclusion]);
|
|
1775
|
+
tags.push(["logs", input.logsUrl]);
|
|
1776
|
+
for (const a of input.artifacts ?? []) {
|
|
1777
|
+
tags.push(["artifact", a.url, a.filename, a.name]);
|
|
1778
|
+
}
|
|
1779
|
+
if (input.queuedAt !== void 0)
|
|
1780
|
+
tags.push(["queued_at", String(input.queuedAt)]);
|
|
1781
|
+
if (input.startedAt !== void 0)
|
|
1782
|
+
tags.push(["started_at", String(input.startedAt)]);
|
|
1783
|
+
if (input.exitCode !== void 0)
|
|
1784
|
+
tags.push(["exit_code", String(input.exitCode)]);
|
|
1785
|
+
if (input.runsOn && input.runsOn.length > 0)
|
|
1786
|
+
tags.push(["runs_on", ...input.runsOn]);
|
|
1787
|
+
if (input.provenance) tags.push(provenanceTag(input.provenance));
|
|
1788
|
+
return {
|
|
1789
|
+
kind: CI_JOB_RESULT_KIND,
|
|
1790
|
+
content: `[log-tail omitted=${input.logOmittedBytes}]
|
|
1791
|
+
${input.logTail}`,
|
|
1792
|
+
tags,
|
|
1793
|
+
created_at: createdAt
|
|
1794
|
+
};
|
|
1795
|
+
}
|
|
1796
|
+
function optionalInt(tags, name) {
|
|
1797
|
+
const t = optionalSingle(tags, name);
|
|
1798
|
+
if (t === null) return null;
|
|
1799
|
+
if (t === void 0) return void 0;
|
|
1800
|
+
const n = Number(t[1]);
|
|
1801
|
+
return Number.isInteger(n) ? n : null;
|
|
1802
|
+
}
|
|
1803
|
+
function parseCiJobResult(ev) {
|
|
1804
|
+
if (ev.kind !== CI_JOB_RESULT_KIND) return null;
|
|
1805
|
+
const { tags } = ev;
|
|
1806
|
+
const trigger = parseCiTriggerContext(tags);
|
|
1807
|
+
if (!trigger) return null;
|
|
1808
|
+
const progress = values(tags, "q").filter(
|
|
1809
|
+
(t) => t[1] !== void 0 && PROGRESS_ADDR_RE.test(t[1])
|
|
1810
|
+
);
|
|
1811
|
+
if (progress.length !== 1) return null;
|
|
1812
|
+
const job = single(tags, "job");
|
|
1813
|
+
const conclusion = single(tags, "conclusion");
|
|
1814
|
+
if (!job || !job[1] || !conclusion || !isCiConclusion(conclusion[1] ?? ""))
|
|
1815
|
+
return null;
|
|
1816
|
+
const name = optionalSingle(tags, "name");
|
|
1817
|
+
const logs = optionalSingle(tags, "logs");
|
|
1818
|
+
if (name === null || logs === null) return null;
|
|
1819
|
+
const queuedAt = optionalInt(tags, "queued_at");
|
|
1820
|
+
const startedAt = optionalInt(tags, "started_at");
|
|
1821
|
+
const exitCode = optionalInt(tags, "exit_code");
|
|
1822
|
+
if (queuedAt === null || startedAt === null || exitCode === null) return null;
|
|
1823
|
+
const runsOn = optionalSingle(tags, "runs_on");
|
|
1824
|
+
if (runsOn === null) return null;
|
|
1825
|
+
const provenance = parseProvenance(tags);
|
|
1826
|
+
if (provenance === null) return null;
|
|
1827
|
+
const artifacts = [];
|
|
1828
|
+
for (const t of values(tags, "artifact")) {
|
|
1829
|
+
if (!t[1] || t[2] === void 0 || t[3] === void 0) return null;
|
|
1830
|
+
artifacts.push({ url: t[1], filename: t[2], name: t[3] });
|
|
1831
|
+
}
|
|
1832
|
+
const header = LOG_TAIL_HEADER.exec(ev.content);
|
|
1833
|
+
const logOmittedBytes = header ? Number(header[1]) : 0;
|
|
1834
|
+
const logTail = header ? ev.content.slice(header[0].length) : ev.content;
|
|
1835
|
+
const result = {
|
|
1836
|
+
eventId: ev.id,
|
|
1837
|
+
pubkey: ev.pubkey.toLowerCase(),
|
|
1838
|
+
createdAt: ev.created_at,
|
|
1839
|
+
trigger,
|
|
1840
|
+
progressAddress: progress[0][1],
|
|
1841
|
+
jobId: job[1],
|
|
1842
|
+
conclusion: conclusion[1],
|
|
1843
|
+
logTail,
|
|
1844
|
+
logOmittedBytes,
|
|
1845
|
+
artifacts,
|
|
1846
|
+
runsOn: runsOn ? runsOn.slice(1) : []
|
|
1847
|
+
};
|
|
1848
|
+
if (name?.[1] !== void 0) result.name = name[1];
|
|
1849
|
+
if (logs?.[1]) result.logsUrl = logs[1];
|
|
1850
|
+
if (queuedAt !== void 0) result.queuedAt = queuedAt;
|
|
1851
|
+
if (startedAt !== void 0) result.startedAt = startedAt;
|
|
1852
|
+
if (exitCode !== void 0) result.exitCode = exitCode;
|
|
1853
|
+
if (provenance) result.provenance = provenance;
|
|
1854
|
+
return result;
|
|
1855
|
+
}
|
|
1856
|
+
function jobQuoteTag(j) {
|
|
1857
|
+
return ["q", j.eventId, j.relayUrl, j.pubkey, j.jobId];
|
|
1858
|
+
}
|
|
1859
|
+
function parseJobQuotes(tags) {
|
|
1860
|
+
const jobs = [];
|
|
1861
|
+
for (const t of values(tags, "q")) {
|
|
1862
|
+
const marker = t[4];
|
|
1863
|
+
if (marker === void 0 || PROVENANCE_MARKERS.has(marker)) continue;
|
|
1864
|
+
if (t[1] !== void 0 && PROGRESS_ADDR_RE.test(t[1])) continue;
|
|
1865
|
+
const eventId = t[1]?.toLowerCase();
|
|
1866
|
+
const pubkey = t[3]?.toLowerCase();
|
|
1867
|
+
if (eventId === void 0 || !HEX64.test(eventId) || pubkey === void 0 || !HEX64.test(pubkey) || marker === "") {
|
|
1868
|
+
return null;
|
|
1869
|
+
}
|
|
1870
|
+
jobs.push({ eventId, relayUrl: t[2] ?? "", pubkey, jobId: marker });
|
|
1871
|
+
}
|
|
1872
|
+
return jobs;
|
|
1873
|
+
}
|
|
1874
|
+
function buildCiWorkflowResult(input, createdAt = now()) {
|
|
1875
|
+
const tags = [
|
|
1876
|
+
...commonTriggerTags(input.trigger),
|
|
1877
|
+
["r", input.runId],
|
|
1878
|
+
["conclusion", input.conclusion]
|
|
1879
|
+
];
|
|
1880
|
+
if (input.queuedAt !== void 0)
|
|
1881
|
+
tags.push(["queued_at", String(input.queuedAt)]);
|
|
1882
|
+
if (input.startedAt !== void 0)
|
|
1883
|
+
tags.push(["started_at", String(input.startedAt)]);
|
|
1884
|
+
if (input.provenance) tags.push(provenanceTag(input.provenance));
|
|
1885
|
+
for (const j of input.jobs) tags.push(jobQuoteTag(j));
|
|
1886
|
+
return {
|
|
1887
|
+
kind: CI_WORKFLOW_RESULT_KIND,
|
|
1888
|
+
content: "",
|
|
1889
|
+
tags,
|
|
1890
|
+
created_at: createdAt
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1893
|
+
function parseCiWorkflowResult(ev) {
|
|
1894
|
+
if (ev.kind !== CI_WORKFLOW_RESULT_KIND || ev.content !== "") return null;
|
|
1895
|
+
const { tags } = ev;
|
|
1896
|
+
const trigger = parseCiTriggerContext(tags);
|
|
1897
|
+
if (!trigger) return null;
|
|
1898
|
+
const runIds = values(tags, "r").filter(
|
|
1899
|
+
(t) => t[1] && !t[1].startsWith("refs/")
|
|
1900
|
+
);
|
|
1901
|
+
if (runIds.length !== 1) return null;
|
|
1902
|
+
const conclusion = single(tags, "conclusion");
|
|
1903
|
+
if (!conclusion || !isCiConclusion(conclusion[1] ?? "")) return null;
|
|
1904
|
+
const queuedAt = optionalInt(tags, "queued_at");
|
|
1905
|
+
const startedAt = optionalInt(tags, "started_at");
|
|
1906
|
+
if (queuedAt === null || startedAt === null) return null;
|
|
1907
|
+
const provenance = parseProvenance(tags);
|
|
1908
|
+
if (provenance === null) return null;
|
|
1909
|
+
const jobs = parseJobQuotes(tags);
|
|
1910
|
+
if (!jobs) return null;
|
|
1911
|
+
const result = {
|
|
1912
|
+
eventId: ev.id,
|
|
1913
|
+
pubkey: ev.pubkey.toLowerCase(),
|
|
1914
|
+
createdAt: ev.created_at,
|
|
1915
|
+
trigger,
|
|
1916
|
+
runId: runIds[0][1],
|
|
1917
|
+
conclusion: conclusion[1],
|
|
1918
|
+
jobs
|
|
1919
|
+
};
|
|
1920
|
+
if (queuedAt !== void 0) result.queuedAt = queuedAt;
|
|
1921
|
+
if (startedAt !== void 0) result.startedAt = startedAt;
|
|
1922
|
+
if (provenance) result.provenance = provenance;
|
|
1923
|
+
return result;
|
|
1924
|
+
}
|
|
1925
|
+
function buildCiWorkflowProgress(input, createdAt = now()) {
|
|
1926
|
+
if (input.status === "concluded" && input.conclusion === void 0) {
|
|
1927
|
+
throw new Error("a concluded progress event needs a conclusion");
|
|
1928
|
+
}
|
|
1929
|
+
if (input.status !== "concluded" && input.conclusion !== void 0) {
|
|
1930
|
+
throw new Error("only a concluded progress event may carry a conclusion");
|
|
1931
|
+
}
|
|
1932
|
+
assertExpiration(createdAt, input.expiresAt, CI_MAX_PROGRESS_TTL);
|
|
1933
|
+
const tags = [
|
|
1934
|
+
...commonTriggerTags(input.trigger),
|
|
1935
|
+
["d", input.runId],
|
|
1936
|
+
["status", input.status]
|
|
1937
|
+
];
|
|
1938
|
+
if (input.queue !== void 0) tags.push(["queue", String(input.queue)]);
|
|
1939
|
+
if (input.inProgress && input.inProgress.length > 0) {
|
|
1940
|
+
tags.push(["in-progress", ...input.inProgress]);
|
|
1941
|
+
}
|
|
1942
|
+
if (input.conclusion !== void 0)
|
|
1943
|
+
tags.push(["conclusion", input.conclusion]);
|
|
1944
|
+
if (input.queuedAt !== void 0)
|
|
1945
|
+
tags.push(["queued_at", String(input.queuedAt)]);
|
|
1946
|
+
if (input.startedAt !== void 0)
|
|
1947
|
+
tags.push(["started_at", String(input.startedAt)]);
|
|
1948
|
+
tags.push(["expiration", String(input.expiresAt)]);
|
|
1949
|
+
if (input.provenance && !(input.status === "queued" && input.provenance.kind === "service-request")) {
|
|
1950
|
+
tags.push(provenanceTag(input.provenance));
|
|
1951
|
+
}
|
|
1952
|
+
for (const j of input.jobs) tags.push(jobQuoteTag(j));
|
|
1953
|
+
return {
|
|
1954
|
+
kind: CI_WORKFLOW_PROGRESS_KIND,
|
|
1955
|
+
content: "",
|
|
1956
|
+
tags,
|
|
1957
|
+
created_at: createdAt
|
|
1958
|
+
};
|
|
1959
|
+
}
|
|
1960
|
+
function parseCiWorkflowProgress(ev) {
|
|
1961
|
+
if (ev.kind !== CI_WORKFLOW_PROGRESS_KIND || ev.content !== "") return null;
|
|
1962
|
+
const { tags } = ev;
|
|
1963
|
+
const trigger = parseCiTriggerContext(tags);
|
|
1964
|
+
if (!trigger) return null;
|
|
1965
|
+
const d = single(tags, "d");
|
|
1966
|
+
const status = single(tags, "status");
|
|
1967
|
+
const exp = single(tags, "expiration");
|
|
1968
|
+
if (!d || !d[1] || !status || !PROGRESS_STATUSES.has(status[1] ?? "") || !exp)
|
|
1969
|
+
return null;
|
|
1970
|
+
const expiresAt = Number(exp[1]);
|
|
1971
|
+
if (!Number.isInteger(expiresAt)) return null;
|
|
1972
|
+
const conclusion = optionalSingle(tags, "conclusion");
|
|
1973
|
+
if (conclusion === null) return null;
|
|
1974
|
+
if (status[1] === "concluded") {
|
|
1975
|
+
if (!conclusion || !isCiConclusion(conclusion[1] ?? "")) return null;
|
|
1976
|
+
} else if (conclusion !== void 0) {
|
|
1977
|
+
return null;
|
|
1978
|
+
}
|
|
1979
|
+
const queue = optionalInt(tags, "queue");
|
|
1980
|
+
const queuedAt = optionalInt(tags, "queued_at");
|
|
1981
|
+
const startedAt = optionalInt(tags, "started_at");
|
|
1982
|
+
if (queue === null || queuedAt === null || startedAt === null) return null;
|
|
1983
|
+
const inProgress = optionalSingle(tags, "in-progress");
|
|
1984
|
+
if (inProgress === null) return null;
|
|
1985
|
+
const provenance = parseProvenance(tags);
|
|
1986
|
+
if (provenance === null) return null;
|
|
1987
|
+
const jobs = parseJobQuotes(tags);
|
|
1988
|
+
if (!jobs) return null;
|
|
1989
|
+
const result = {
|
|
1990
|
+
eventId: ev.id,
|
|
1991
|
+
pubkey: ev.pubkey.toLowerCase(),
|
|
1992
|
+
createdAt: ev.created_at,
|
|
1993
|
+
trigger,
|
|
1994
|
+
runId: d[1],
|
|
1995
|
+
status: status[1],
|
|
1996
|
+
inProgress: inProgress ? inProgress.slice(1) : [],
|
|
1997
|
+
expiresAt,
|
|
1998
|
+
jobs
|
|
1999
|
+
};
|
|
2000
|
+
if (conclusion?.[1] !== void 0)
|
|
2001
|
+
result.conclusion = conclusion[1];
|
|
2002
|
+
if (queue !== void 0) result.queue = queue;
|
|
2003
|
+
if (queuedAt !== void 0) result.queuedAt = queuedAt;
|
|
2004
|
+
if (startedAt !== void 0) result.startedAt = startedAt;
|
|
2005
|
+
if (provenance) result.provenance = provenance;
|
|
2006
|
+
return result;
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
// src/ci/secrets.ts
|
|
2010
|
+
import { v2 as nip44 } from "nostr-tools/nip44";
|
|
2011
|
+
import { generateSecretKey, getPublicKey } from "nostr-tools/pure";
|
|
2012
|
+
var SECRET_NAME_RE = /^[A-Z_][A-Z0-9_]*$/;
|
|
2013
|
+
var RESERVED_SECRET_NAMES = [
|
|
2014
|
+
"WORKFLOW_SECRETS_DECRYPTION_BUNKER"
|
|
2015
|
+
];
|
|
2016
|
+
var MAX_SECRET_NAMES = 100;
|
|
2017
|
+
var MAX_SECRET_VALUE_BYTES = 16384;
|
|
2018
|
+
var MAX_SECRET_PLAINTEXT_BYTES = 65535;
|
|
2019
|
+
var MAX_SECRET_CIPHERTEXT_BYTES = 100 * 1024;
|
|
2020
|
+
var HEX642 = /^[0-9a-f]{64}$/i;
|
|
2021
|
+
function utf8Bytes(s) {
|
|
2022
|
+
return Buffer.byteLength(s, "utf-8");
|
|
2023
|
+
}
|
|
2024
|
+
function assertName(name) {
|
|
2025
|
+
if (!SECRET_NAME_RE.test(name)) {
|
|
2026
|
+
throw new Error(`invalid secret name ${JSON.stringify(name)}`);
|
|
2027
|
+
}
|
|
2028
|
+
if (RESERVED_SECRET_NAMES.includes(name)) {
|
|
2029
|
+
throw new Error(`secret name ${name} is reserved`);
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
function assertSecretUpdateShape(value) {
|
|
2033
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
2034
|
+
throw new Error("secret update plaintext must be a JSON object");
|
|
2035
|
+
}
|
|
2036
|
+
const keys = Object.keys(value).sort();
|
|
2037
|
+
if (keys.join(",") !== "author,created_at,remove,set") {
|
|
2038
|
+
throw new Error(
|
|
2039
|
+
"secret update plaintext must contain exactly author, created_at, set, and remove"
|
|
2040
|
+
);
|
|
2041
|
+
}
|
|
2042
|
+
const { author, created_at, set, remove } = value;
|
|
2043
|
+
if (typeof author !== "string" || !HEX642.test(author)) {
|
|
2044
|
+
throw new Error("secret update author must be a 64-hex pubkey");
|
|
2045
|
+
}
|
|
2046
|
+
if (typeof created_at !== "number" || !Number.isInteger(created_at)) {
|
|
2047
|
+
throw new Error("secret update created_at must be an integer");
|
|
2048
|
+
}
|
|
2049
|
+
if (set === null || typeof set !== "object" || Array.isArray(set)) {
|
|
2050
|
+
throw new Error("secret update set must be an object");
|
|
2051
|
+
}
|
|
2052
|
+
if (!Array.isArray(remove) || !remove.every((n) => typeof n === "string")) {
|
|
2053
|
+
throw new Error("secret update remove must be an array of names");
|
|
2054
|
+
}
|
|
2055
|
+
const setNames = Object.keys(set);
|
|
2056
|
+
const removeNames = remove;
|
|
2057
|
+
if (setNames.length === 0 && removeNames.length === 0) {
|
|
2058
|
+
throw new Error("secret update needs a non-empty set or remove");
|
|
2059
|
+
}
|
|
2060
|
+
if (setNames.length + removeNames.length > MAX_SECRET_NAMES) {
|
|
2061
|
+
throw new Error(
|
|
2062
|
+
`secret update may name at most ${MAX_SECRET_NAMES} secrets`
|
|
2063
|
+
);
|
|
2064
|
+
}
|
|
2065
|
+
for (const name of [...setNames, ...removeNames]) assertName(name);
|
|
2066
|
+
const removeSet = new Set(removeNames);
|
|
2067
|
+
if (removeSet.size !== removeNames.length) {
|
|
2068
|
+
throw new Error("secret update remove lists a name twice");
|
|
2069
|
+
}
|
|
2070
|
+
for (const name of setNames) {
|
|
2071
|
+
if (removeSet.has(name)) {
|
|
2072
|
+
throw new Error(`secret ${name} appears in both set and remove`);
|
|
2073
|
+
}
|
|
2074
|
+
const v = set[name];
|
|
2075
|
+
if (typeof v !== "string" || v === "") {
|
|
2076
|
+
throw new Error(`secret ${name} value must be a non-empty string`);
|
|
2077
|
+
}
|
|
2078
|
+
if (utf8Bytes(v) > MAX_SECRET_VALUE_BYTES) {
|
|
2079
|
+
throw new Error(
|
|
2080
|
+
`secret ${name} value exceeds ${MAX_SECRET_VALUE_BYTES} bytes`
|
|
2081
|
+
);
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
return {
|
|
2085
|
+
author: author.toLowerCase(),
|
|
2086
|
+
created_at,
|
|
2087
|
+
set,
|
|
2088
|
+
remove: removeNames
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2091
|
+
function validateSecretUpdate(plain, outer) {
|
|
2092
|
+
if (plain.author.toLowerCase() !== outer.pubkey.toLowerCase()) {
|
|
2093
|
+
throw new Error("secret update author does not match the signing pubkey");
|
|
2094
|
+
}
|
|
2095
|
+
if (plain.created_at !== outer.created_at) {
|
|
2096
|
+
throw new Error(
|
|
2097
|
+
"secret update created_at does not match the event timestamp"
|
|
2098
|
+
);
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
function encryptSecretUpdate(plain, senderSecretKey, recipientPubkey) {
|
|
2102
|
+
const checked = assertSecretUpdateShape(plain);
|
|
2103
|
+
const json = JSON.stringify(checked);
|
|
2104
|
+
if (utf8Bytes(json) > MAX_SECRET_PLAINTEXT_BYTES) {
|
|
2105
|
+
throw new Error(
|
|
2106
|
+
`secret update plaintext exceeds ${MAX_SECRET_PLAINTEXT_BYTES} bytes`
|
|
2107
|
+
);
|
|
2108
|
+
}
|
|
2109
|
+
const key = nip44.utils.getConversationKey(senderSecretKey, recipientPubkey);
|
|
2110
|
+
return nip44.encrypt(json, key);
|
|
2111
|
+
}
|
|
2112
|
+
function decryptSecretUpdate(ciphertext, recipientSecretKey, senderPubkey) {
|
|
2113
|
+
if (utf8Bytes(ciphertext) > MAX_SECRET_CIPHERTEXT_BYTES) {
|
|
2114
|
+
throw new Error("secret update ciphertext exceeds 100 KiB");
|
|
2115
|
+
}
|
|
2116
|
+
const key = nip44.utils.getConversationKey(recipientSecretKey, senderPubkey);
|
|
2117
|
+
const json = nip44.decrypt(ciphertext, key);
|
|
2118
|
+
if (utf8Bytes(json) > MAX_SECRET_PLAINTEXT_BYTES) {
|
|
2119
|
+
throw new Error(
|
|
2120
|
+
`secret update plaintext exceeds ${MAX_SECRET_PLAINTEXT_BYTES} bytes`
|
|
2121
|
+
);
|
|
2122
|
+
}
|
|
2123
|
+
let parsed;
|
|
2124
|
+
try {
|
|
2125
|
+
parsed = JSON.parse(json);
|
|
2126
|
+
} catch {
|
|
2127
|
+
throw new Error("secret update plaintext is not JSON");
|
|
2128
|
+
}
|
|
2129
|
+
return assertSecretUpdateShape(parsed);
|
|
2130
|
+
}
|
|
2131
|
+
function applySecretUpdate(inventory, plain, eventId) {
|
|
2132
|
+
const next = { ...inventory };
|
|
2133
|
+
const position = { createdAt: plain.created_at, eventId };
|
|
2134
|
+
const isLater = (name) => {
|
|
2135
|
+
const existing = next[name];
|
|
2136
|
+
return existing === void 0 || controlOrder(position, existing) > 0;
|
|
2137
|
+
};
|
|
2138
|
+
for (const [name, value] of Object.entries(plain.set)) {
|
|
2139
|
+
if (isLater(name)) {
|
|
2140
|
+
next[name] = { value, ...position, origin: plain.author };
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
for (const name of plain.remove) {
|
|
2144
|
+
if (isLater(name)) {
|
|
2145
|
+
next[name] = { value: null, ...position, origin: plain.author };
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
return next;
|
|
2149
|
+
}
|
|
2150
|
+
function effectiveSecrets(inventory) {
|
|
2151
|
+
const out = {};
|
|
2152
|
+
for (const [name, entry] of Object.entries(inventory)) {
|
|
2153
|
+
if (entry.value !== null) out[name] = entry.value;
|
|
2154
|
+
}
|
|
2155
|
+
return out;
|
|
2156
|
+
}
|
|
2157
|
+
function generateSecretsKey() {
|
|
2158
|
+
const secretKey = generateSecretKey();
|
|
2159
|
+
return { secretKey, pubkey: getPublicKey(secretKey) };
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
// src/ci/trust.ts
|
|
2163
|
+
var CI_TRUST_ORDER = [
|
|
2164
|
+
"maintainer-directed",
|
|
2165
|
+
"operationally-associated",
|
|
2166
|
+
"seen-in-network",
|
|
2167
|
+
"no-known-context"
|
|
2168
|
+
];
|
|
2169
|
+
function isCiTrustLevel(value) {
|
|
2170
|
+
return CI_TRUST_ORDER.includes(value);
|
|
2171
|
+
}
|
|
2172
|
+
function deriveTrustLevel(args) {
|
|
2173
|
+
const publisher = args.publisherPubkey.toLowerCase();
|
|
2174
|
+
const authorized = new Set([...args.authorized].map((p) => p.toLowerCase()));
|
|
2175
|
+
const forPublisher = args.controls.filter(
|
|
2176
|
+
(c) => c.coordinatorPubkey.toLowerCase() === publisher
|
|
2177
|
+
);
|
|
2178
|
+
const prov = args.provenance;
|
|
2179
|
+
if (prov && authorized.has(prov.pubkey.toLowerCase())) {
|
|
2180
|
+
if (prov.kind === "manual-trigger") return "maintainer-directed";
|
|
2181
|
+
const quoted = prov.eventId.toLowerCase();
|
|
2182
|
+
if (forPublisher.some(
|
|
2183
|
+
(c) => c.kind === "request" && c.eventId.toLowerCase() === quoted && c.pubkey.toLowerCase() === prov.pubkey.toLowerCase()
|
|
2184
|
+
)) {
|
|
2185
|
+
return "maintainer-directed";
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
const standing = selectServiceRequests(
|
|
2189
|
+
forPublisher.filter((c) => c.createdAt <= args.runCreatedAt),
|
|
2190
|
+
{ coordinatorPubkey: publisher, authorized }
|
|
2191
|
+
);
|
|
2192
|
+
if (standing.active) return "operationally-associated";
|
|
2193
|
+
if (authorized.has(publisher)) return "seen-in-network";
|
|
2194
|
+
if (forPublisher.some((c) => c.kind === "request")) return "seen-in-network";
|
|
2195
|
+
return "no-known-context";
|
|
2196
|
+
}
|
|
2197
|
+
function trustAtLeast(level, required) {
|
|
2198
|
+
return CI_TRUST_ORDER.indexOf(level) <= CI_TRUST_ORDER.indexOf(required);
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
// src/ci/workflows.ts
|
|
2202
|
+
import { createHash as createHash2 } from "crypto";
|
|
2203
|
+
import { readdir, readFile } from "fs/promises";
|
|
2204
|
+
import { join } from "path";
|
|
2205
|
+
import { parse as parseYaml } from "yaml";
|
|
2206
|
+
var WORKFLOW_DIRS = [
|
|
2207
|
+
".github/workflows",
|
|
2208
|
+
".ngit/act/workflows"
|
|
2209
|
+
];
|
|
2210
|
+
var WORKFLOW_FILE_RE = /\.ya?ml$/i;
|
|
2211
|
+
function sha256Hex(bytes) {
|
|
2212
|
+
return createHash2("sha256").update(bytes).digest("hex");
|
|
2213
|
+
}
|
|
2214
|
+
function isRecord(value) {
|
|
2215
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2216
|
+
}
|
|
2217
|
+
function stringList(value) {
|
|
2218
|
+
if (typeof value === "string") return [value];
|
|
2219
|
+
if (Array.isArray(value)) {
|
|
2220
|
+
return value.filter((v) => typeof v === "string");
|
|
2221
|
+
}
|
|
2222
|
+
return void 0;
|
|
2223
|
+
}
|
|
2224
|
+
function refFilters(value) {
|
|
2225
|
+
if (!isRecord(value)) return {};
|
|
2226
|
+
const out = {};
|
|
2227
|
+
const branches = stringList(value["branches"]);
|
|
2228
|
+
const tags = stringList(value["tags"]);
|
|
2229
|
+
if (branches) out.branches = branches;
|
|
2230
|
+
if (tags) out.tags = tags;
|
|
2231
|
+
return out;
|
|
2232
|
+
}
|
|
2233
|
+
function parseTriggers(on) {
|
|
2234
|
+
const triggers = {};
|
|
2235
|
+
const enable = (name, options) => {
|
|
2236
|
+
if (name === "push") triggers.push = refFilters(options);
|
|
2237
|
+
else if (name === "pull_request")
|
|
2238
|
+
triggers.pull_request = refFilters(options);
|
|
2239
|
+
else if (name === "workflow_dispatch") triggers.workflow_dispatch = true;
|
|
2240
|
+
};
|
|
2241
|
+
if (typeof on === "string") enable(on, void 0);
|
|
2242
|
+
else if (Array.isArray(on)) for (const name of on) enable(name, void 0);
|
|
2243
|
+
else if (isRecord(on)) {
|
|
2244
|
+
for (const [name, options] of Object.entries(on)) enable(name, options);
|
|
2245
|
+
}
|
|
2246
|
+
return triggers;
|
|
2247
|
+
}
|
|
2248
|
+
function parseJobs(jobs) {
|
|
2249
|
+
if (!isRecord(jobs)) return [];
|
|
2250
|
+
const out = [];
|
|
2251
|
+
for (const [id, spec] of Object.entries(jobs)) {
|
|
2252
|
+
const job = { id };
|
|
2253
|
+
if (isRecord(spec)) {
|
|
2254
|
+
if (typeof spec["name"] === "string") job.name = spec["name"];
|
|
2255
|
+
const runsOn = stringList(spec["runs-on"]);
|
|
2256
|
+
if (runsOn) job.runsOn = runsOn;
|
|
2257
|
+
}
|
|
2258
|
+
out.push(job);
|
|
2259
|
+
}
|
|
2260
|
+
return out;
|
|
2261
|
+
}
|
|
2262
|
+
function parseWorkflow(path, content) {
|
|
2263
|
+
const text = typeof content === "string" ? content : Buffer.from(content).toString("utf-8");
|
|
2264
|
+
const base = {
|
|
2265
|
+
path,
|
|
2266
|
+
sha256: sha256Hex(content),
|
|
2267
|
+
triggers: {},
|
|
2268
|
+
jobs: []
|
|
2269
|
+
};
|
|
2270
|
+
let doc;
|
|
2271
|
+
try {
|
|
2272
|
+
doc = parseYaml(text, { strict: false, uniqueKeys: false });
|
|
2273
|
+
} catch (err) {
|
|
2274
|
+
return {
|
|
2275
|
+
...base,
|
|
2276
|
+
parseError: err instanceof Error ? err.message : String(err)
|
|
2277
|
+
};
|
|
2278
|
+
}
|
|
2279
|
+
if (!isRecord(doc)) {
|
|
2280
|
+
return { ...base, parseError: "workflow is not a YAML mapping" };
|
|
2281
|
+
}
|
|
2282
|
+
const on = doc["on"] ?? doc["true"];
|
|
2283
|
+
if (on === void 0 || on === null) {
|
|
2284
|
+
return { ...base, parseError: "workflow has no `on:` trigger clause" };
|
|
2285
|
+
}
|
|
2286
|
+
const out = {
|
|
2287
|
+
...base,
|
|
2288
|
+
triggers: parseTriggers(on),
|
|
2289
|
+
jobs: parseJobs(doc["jobs"])
|
|
2290
|
+
};
|
|
2291
|
+
if (typeof doc["name"] === "string") out.name = doc["name"];
|
|
2292
|
+
return out;
|
|
2293
|
+
}
|
|
2294
|
+
async function discoverWorkflows(checkoutDir) {
|
|
2295
|
+
const found = [];
|
|
2296
|
+
for (const dir of WORKFLOW_DIRS) {
|
|
2297
|
+
let names;
|
|
2298
|
+
try {
|
|
2299
|
+
names = await readdir(join(checkoutDir, dir));
|
|
2300
|
+
} catch {
|
|
2301
|
+
continue;
|
|
2302
|
+
}
|
|
2303
|
+
for (const name of names.filter((n) => WORKFLOW_FILE_RE.test(n)).sort()) {
|
|
2304
|
+
const rel = `${dir}/${name}`;
|
|
2305
|
+
const bytes = await readFile(join(checkoutDir, dir, name));
|
|
2306
|
+
found.push(parseWorkflow(rel, bytes));
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
return found;
|
|
2310
|
+
}
|
|
2311
|
+
function patternToRegExp(pattern) {
|
|
2312
|
+
let re = "";
|
|
2313
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
2314
|
+
const ch = pattern[i];
|
|
2315
|
+
if (ch === "*") {
|
|
2316
|
+
if (pattern[i + 1] === "*") {
|
|
2317
|
+
re += ".*";
|
|
2318
|
+
i++;
|
|
2319
|
+
} else {
|
|
2320
|
+
re += "[^/]*";
|
|
2321
|
+
}
|
|
2322
|
+
} else if (ch === "?") {
|
|
2323
|
+
re += "[^/]";
|
|
2324
|
+
} else if (ch === "+") {
|
|
2325
|
+
re += "+";
|
|
2326
|
+
} else {
|
|
2327
|
+
re += ch.replace(/[.\\^$|()[\]{}]/g, "\\$&");
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
return new RegExp(`^${re}$`);
|
|
2331
|
+
}
|
|
2332
|
+
function refMatchesPatterns(shortName, patterns) {
|
|
2333
|
+
let included = false;
|
|
2334
|
+
for (const pattern of patterns) {
|
|
2335
|
+
const negated = pattern.startsWith("!");
|
|
2336
|
+
const body = negated ? pattern.slice(1) : pattern;
|
|
2337
|
+
if (patternToRegExp(body).test(shortName)) included = !negated;
|
|
2338
|
+
}
|
|
2339
|
+
return included;
|
|
2340
|
+
}
|
|
2341
|
+
function matchesPush(wf, ref) {
|
|
2342
|
+
if (wf.parseError !== void 0 || !wf.triggers.push) return false;
|
|
2343
|
+
const { branches, tags } = wf.triggers.push;
|
|
2344
|
+
if (!branches && !tags) return true;
|
|
2345
|
+
if (ref.startsWith("refs/heads/")) {
|
|
2346
|
+
return branches !== void 0 && refMatchesPatterns(ref.slice("refs/heads/".length), branches);
|
|
2347
|
+
}
|
|
2348
|
+
if (ref.startsWith("refs/tags/")) {
|
|
2349
|
+
return tags !== void 0 && refMatchesPatterns(ref.slice("refs/tags/".length), tags);
|
|
2350
|
+
}
|
|
2351
|
+
return false;
|
|
2352
|
+
}
|
|
2353
|
+
function matchesPullRequest(wf, baseRef) {
|
|
2354
|
+
if (wf.parseError !== void 0 || !wf.triggers.pull_request) return false;
|
|
2355
|
+
const { branches } = wf.triggers.pull_request;
|
|
2356
|
+
if (!branches) return true;
|
|
2357
|
+
if (baseRef === void 0) return false;
|
|
2358
|
+
const short = baseRef.startsWith("refs/heads/") ? baseRef.slice("refs/heads/".length) : baseRef;
|
|
2359
|
+
return refMatchesPatterns(short, branches);
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
// src/cli/daemon-session.ts
|
|
2363
|
+
function daemonSupportsGit(probe) {
|
|
2364
|
+
return Array.isArray(probe.capabilities) && probe.capabilities.includes("git");
|
|
2365
|
+
}
|
|
2366
|
+
function daemonBaseUrl(env) {
|
|
2367
|
+
const raw = env["TOON_CLIENT_HTTP_PORT"];
|
|
2368
|
+
const parsed = raw ? Number(raw) : NaN;
|
|
2369
|
+
const port = Number.isFinite(parsed) && parsed > 0 ? parsed : defaultDaemonPort();
|
|
2370
|
+
return `http://127.0.0.1:${port}`;
|
|
2371
|
+
}
|
|
2372
|
+
async function probeDaemon(env, fetchImpl, timeoutMs = 1500) {
|
|
2373
|
+
const baseUrl = daemonBaseUrl(env);
|
|
2374
|
+
try {
|
|
2375
|
+
const res = await fetchImpl(`${baseUrl}/status`, {
|
|
2376
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
2377
|
+
});
|
|
2378
|
+
if (!res.ok) return { baseUrl, reachable: false };
|
|
2379
|
+
const body = await res.json();
|
|
2380
|
+
const probe = { baseUrl, reachable: true };
|
|
2381
|
+
const pubkey = body?.identity?.nostrPubkey;
|
|
2382
|
+
if (typeof pubkey === "string" && pubkey !== "") probe.identity = pubkey;
|
|
2383
|
+
if (typeof body?.ready === "boolean") probe.ready = body.ready;
|
|
2384
|
+
if (typeof body?.relay?.url === "string" && body.relay.url !== "") {
|
|
2385
|
+
probe.relayUrl = body.relay.url;
|
|
2386
|
+
}
|
|
2387
|
+
if (typeof body?.feePerEvent === "string" && body.feePerEvent !== "") {
|
|
2388
|
+
probe.feePerEvent = body.feePerEvent;
|
|
2389
|
+
}
|
|
2390
|
+
if (Array.isArray(body?.capabilities)) {
|
|
2391
|
+
probe.capabilities = body.capabilities.filter(
|
|
2392
|
+
(c) => typeof c === "string"
|
|
2393
|
+
);
|
|
2394
|
+
}
|
|
2395
|
+
return probe;
|
|
2396
|
+
} catch {
|
|
2397
|
+
return { baseUrl, reachable: false };
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
var DaemonRouteError = class extends Error {
|
|
2401
|
+
constructor(status, envelope) {
|
|
2402
|
+
super(envelope.detail ?? envelope.error);
|
|
2403
|
+
this.status = status;
|
|
2404
|
+
this.envelope = envelope;
|
|
2405
|
+
this.name = "DaemonRouteError";
|
|
2406
|
+
}
|
|
2407
|
+
status;
|
|
2408
|
+
envelope;
|
|
2409
|
+
};
|
|
2410
|
+
var DaemonTooOldForGitError = class extends Error {
|
|
2411
|
+
constructor(baseUrl, pubkey) {
|
|
2412
|
+
const holds = pubkey !== void 0 && pubkey !== "" ? ` holds this identity (${pubkey.slice(0, 8)}\u2026)` : " holds this identity";
|
|
2413
|
+
super(
|
|
2414
|
+
`toon-clientd at ${baseUrl}${holds} but is too old to handle git operations (missing /git routes). Upgrade the daemon (npm i -g @toon-protocol/client-mcp@latest, then restart it) \u2014 or stop it to let rig run standalone.`
|
|
2415
|
+
);
|
|
2416
|
+
this.baseUrl = baseUrl;
|
|
2417
|
+
this.pubkey = pubkey;
|
|
2418
|
+
this.name = "DaemonTooOldForGitError";
|
|
2419
|
+
}
|
|
2420
|
+
baseUrl;
|
|
2421
|
+
pubkey;
|
|
2422
|
+
};
|
|
2423
|
+
var DaemonUnreachableError = class extends Error {
|
|
2424
|
+
constructor(baseUrl, cause) {
|
|
2425
|
+
super(
|
|
2426
|
+
`toon-clientd stopped answering at ${baseUrl} after the identity probe \u2014 nothing was paid. Re-run: rig falls back to standalone automatically when no daemon responds` + (cause instanceof Error ? ` (${cause.message})` : "")
|
|
2427
|
+
);
|
|
2428
|
+
this.baseUrl = baseUrl;
|
|
2429
|
+
this.name = "DaemonUnreachableError";
|
|
2430
|
+
}
|
|
2431
|
+
baseUrl;
|
|
2432
|
+
};
|
|
2433
|
+
var DaemonGitClient = class {
|
|
2434
|
+
constructor(baseUrl, fetchImpl, pubkey) {
|
|
2435
|
+
this.baseUrl = baseUrl;
|
|
2436
|
+
this.fetchImpl = fetchImpl;
|
|
2437
|
+
this.pubkey = pubkey;
|
|
2438
|
+
}
|
|
2439
|
+
baseUrl;
|
|
2440
|
+
fetchImpl;
|
|
2441
|
+
pubkey;
|
|
2442
|
+
gitEstimate(req) {
|
|
2443
|
+
return this.post("/git/estimate", req);
|
|
2444
|
+
}
|
|
2445
|
+
gitPush(req) {
|
|
2446
|
+
return this.post("/git/push", req);
|
|
2447
|
+
}
|
|
2448
|
+
gitIssue(req) {
|
|
2449
|
+
return this.post("/git/issue", req);
|
|
2450
|
+
}
|
|
2451
|
+
gitComment(req) {
|
|
2452
|
+
return this.post("/git/comment", req);
|
|
2453
|
+
}
|
|
2454
|
+
gitPatch(req) {
|
|
2455
|
+
return this.post("/git/patch", req);
|
|
2456
|
+
}
|
|
2457
|
+
gitStatus(req) {
|
|
2458
|
+
return this.post("/git/status", req);
|
|
2459
|
+
}
|
|
2460
|
+
async post(path, body) {
|
|
2461
|
+
let res;
|
|
2462
|
+
try {
|
|
2463
|
+
res = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
2464
|
+
method: "POST",
|
|
2465
|
+
headers: { "content-type": "application/json" },
|
|
2466
|
+
body: JSON.stringify(body)
|
|
2467
|
+
});
|
|
2468
|
+
} catch (err) {
|
|
2469
|
+
throw new DaemonUnreachableError(this.baseUrl, err);
|
|
2470
|
+
}
|
|
2471
|
+
if (res.status === 404) {
|
|
2472
|
+
throw new DaemonTooOldForGitError(this.baseUrl, this.pubkey);
|
|
2473
|
+
}
|
|
2474
|
+
const text = await res.text();
|
|
2475
|
+
let parsed;
|
|
2476
|
+
try {
|
|
2477
|
+
parsed = text === "" ? {} : JSON.parse(text);
|
|
2478
|
+
} catch {
|
|
2479
|
+
throw new DaemonRouteError(res.status, {
|
|
2480
|
+
error: "invalid_response",
|
|
2481
|
+
detail: `daemon returned non-JSON (HTTP ${res.status}): ${text.slice(0, 200)}`
|
|
2482
|
+
});
|
|
2483
|
+
}
|
|
2484
|
+
if (!res.ok) {
|
|
2485
|
+
const envelope = parsed && typeof parsed === "object" && "error" in parsed ? parsed : { error: "http_error", detail: `HTTP ${res.status}` };
|
|
2486
|
+
throw new DaemonRouteError(res.status, envelope);
|
|
2487
|
+
}
|
|
2488
|
+
return parsed;
|
|
2489
|
+
}
|
|
2490
|
+
};
|
|
2491
|
+
async function resolvePaidSession(options) {
|
|
2492
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
2493
|
+
if (standaloneForced(options.env)) {
|
|
2494
|
+
options.warn(
|
|
2495
|
+
"rig: paid path: standalone (RIG_STANDALONE set \u2014 toon-clientd bypassed)"
|
|
2496
|
+
);
|
|
2497
|
+
const ctx2 = await options.loadStandalone({
|
|
2498
|
+
env: options.env,
|
|
2499
|
+
cwd: options.cwd,
|
|
2500
|
+
warn: options.warn,
|
|
2501
|
+
...options.relayUrl !== void 0 ? { relayUrl: options.relayUrl } : {}
|
|
2502
|
+
});
|
|
2503
|
+
return { path: "standalone", ctx: ctx2 };
|
|
2504
|
+
}
|
|
2505
|
+
const probe = await (options.probeDaemon ?? probeDaemon)(
|
|
2506
|
+
options.env,
|
|
2507
|
+
fetchImpl
|
|
2508
|
+
);
|
|
2509
|
+
if (probe.reachable && probe.identity !== void 0) {
|
|
2510
|
+
const identity = await resolveIdentity({
|
|
2511
|
+
env: options.env,
|
|
2512
|
+
cwd: options.cwd,
|
|
2513
|
+
warn: options.warn
|
|
2514
|
+
});
|
|
2515
|
+
if (identity.pubkey === probe.identity) {
|
|
2516
|
+
if (!daemonSupportsGit(probe)) {
|
|
2517
|
+
throw new DaemonTooOldForGitError(probe.baseUrl, identity.pubkey);
|
|
2518
|
+
}
|
|
2519
|
+
options.warn(
|
|
2520
|
+
`rig: paid path: daemon \u2014 toon-clientd at ${probe.baseUrl} holds this identity (${identity.pubkey.slice(0, 8)}\u2026), delegating`
|
|
2521
|
+
);
|
|
2522
|
+
return {
|
|
2523
|
+
path: "daemon",
|
|
2524
|
+
client: new DaemonGitClient(probe.baseUrl, fetchImpl, identity.pubkey),
|
|
2525
|
+
baseUrl: probe.baseUrl,
|
|
2526
|
+
identity: {
|
|
2527
|
+
pubkey: identity.pubkey,
|
|
2528
|
+
source: identity.source,
|
|
2529
|
+
sourceLabel: identity.sourceLabel
|
|
2530
|
+
},
|
|
2531
|
+
...probe.feePerEvent !== void 0 ? { feePerEvent: probe.feePerEvent } : {},
|
|
2532
|
+
...probe.relayUrl !== void 0 ? { daemonRelayUrl: probe.relayUrl } : {}
|
|
2533
|
+
};
|
|
2534
|
+
}
|
|
2535
|
+
options.warn(
|
|
2536
|
+
`rig: paid path: standalone \u2014 the toon-clientd at ${probe.baseUrl} runs a different identity (${probe.identity.slice(0, 8)}\u2026), no shared channel state`
|
|
2537
|
+
);
|
|
2538
|
+
} else {
|
|
2539
|
+
options.warn(
|
|
2540
|
+
`rig: paid path: standalone (no toon-clientd at ${probe.baseUrl})`
|
|
2541
|
+
);
|
|
2542
|
+
}
|
|
2543
|
+
const ctx = await options.loadStandalone({
|
|
2544
|
+
env: options.env,
|
|
2545
|
+
cwd: options.cwd,
|
|
2546
|
+
warn: options.warn,
|
|
2547
|
+
...options.relayUrl !== void 0 ? { relayUrl: options.relayUrl } : {}
|
|
2548
|
+
});
|
|
2549
|
+
return { path: "standalone", ctx };
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
// src/cli/errors.ts
|
|
2553
|
+
var UnconfiguredRepoAddressError = class extends Error {
|
|
2554
|
+
constructor(missing) {
|
|
2555
|
+
super(
|
|
2556
|
+
`no ${missing} configured \u2014 this command addresses the repo as 30617:<ownerPubkey>:<repoId>. Run \`rig init\` once inside the repo (it writes toon.repoid/toon.owner to the local git config), or pass ${missing === "repository id" ? "--repo-id <id>" : "--owner <pubkey>"} explicitly (use --owner for repos you don't own).`
|
|
2557
|
+
);
|
|
2558
|
+
this.missing = missing;
|
|
2559
|
+
this.name = "UnconfiguredRepoAddressError";
|
|
2560
|
+
}
|
|
2561
|
+
missing;
|
|
2562
|
+
};
|
|
2563
|
+
var InvalidRelayUrlError = class extends Error {
|
|
2564
|
+
constructor(url, context) {
|
|
2565
|
+
super(
|
|
2566
|
+
`${context}: ${JSON.stringify(url)} is not a relay URL \u2014 relays are ws://, wss://, http://, or https://`
|
|
2567
|
+
);
|
|
2568
|
+
this.url = url;
|
|
2569
|
+
this.name = "InvalidRelayUrlError";
|
|
2570
|
+
}
|
|
2571
|
+
url;
|
|
2572
|
+
};
|
|
2573
|
+
var UnknownRemoteError = class extends Error {
|
|
2574
|
+
constructor(remote) {
|
|
2575
|
+
super(
|
|
2576
|
+
`no remote named ${JSON.stringify(remote)} is configured \u2014 \`rig remote list\` shows configured remotes; add it with \`rig remote add ${remote} <relay-url>\``
|
|
2577
|
+
);
|
|
2578
|
+
this.remote = remote;
|
|
2579
|
+
this.name = "UnknownRemoteError";
|
|
2580
|
+
}
|
|
2581
|
+
remote;
|
|
2582
|
+
};
|
|
2583
|
+
var MultiUrlRemoteError = class extends Error {
|
|
2584
|
+
constructor(remote, urls) {
|
|
2585
|
+
super(
|
|
2586
|
+
`remote ${JSON.stringify(remote)} has ${urls.length} URLs (${urls.join(", ")}) \u2014 rig supports one relay URL per remote. Fix it with \`git remote set-url ${remote} <relay-url>\`. Nothing was uploaded, published, or paid.`
|
|
2587
|
+
);
|
|
2588
|
+
this.remote = remote;
|
|
2589
|
+
this.urls = urls;
|
|
2590
|
+
this.name = "MultiUrlRemoteError";
|
|
2591
|
+
}
|
|
2592
|
+
remote;
|
|
2593
|
+
urls;
|
|
2594
|
+
};
|
|
2595
|
+
var NoOriginConfiguredError = class extends Error {
|
|
2596
|
+
constructor(nonRelayOriginUrl) {
|
|
2597
|
+
super(
|
|
2598
|
+
"no origin configured \u2014 run `rig remote add origin <relay-url>` (or pass --relay <url> for a one-off publish)." + (nonRelayOriginUrl !== void 0 ? `
|
|
2599
|
+
The existing "origin" remote (${nonRelayOriginUrl}) is not a relay URL, so rig ignores it \u2014 add the relay under another name (\`rig remote add toon <relay-url>\`) and target it explicitly (\`rig push toon\` / \`--remote toon\`).` : "")
|
|
2600
|
+
);
|
|
2601
|
+
this.name = "NoOriginConfiguredError";
|
|
2602
|
+
}
|
|
2603
|
+
};
|
|
2604
|
+
var NotAGitRepositoryError = class extends Error {
|
|
2605
|
+
constructor(cwd) {
|
|
2606
|
+
super(
|
|
2607
|
+
`not a git repository: ${cwd}
|
|
2608
|
+
rig can create one for you: re-run \`rig init --git-init\` (in a terminal, plain \`rig init\` offers the same via a prompt). Or run \`git init\` yourself first, then re-run.`
|
|
2609
|
+
);
|
|
2610
|
+
this.name = "NotAGitRepositoryError";
|
|
2611
|
+
}
|
|
2612
|
+
};
|
|
2613
|
+
function nonFastForwardLines(refs) {
|
|
2614
|
+
return [
|
|
2615
|
+
"Push rejected: non-fast-forward update for:",
|
|
2616
|
+
...refs.map(
|
|
2617
|
+
(r) => ` ${r.refname} remote ${r.remoteSha.slice(0, 7)} is not an ancestor of local ${r.localSha.slice(0, 7)}`
|
|
2618
|
+
),
|
|
2619
|
+
"The remote moved since your last push. Re-run with --force to overwrite it",
|
|
2620
|
+
"WARNING: --force rewrites the published ref history for every reader of this repo."
|
|
2621
|
+
];
|
|
2622
|
+
}
|
|
2623
|
+
function oversizeLines(objects) {
|
|
2624
|
+
return [
|
|
2625
|
+
`Push rejected: ${objects.length} object(s) exceed the ${MAX_OBJECT_SIZE}-byte (95KB) upload limit:`,
|
|
2626
|
+
...objects.map((o) => ` ${o.path ?? o.sha} ${o.type}, ${o.size} bytes`),
|
|
2627
|
+
"Objects over 95KB are a hard error in v1 \u2014 split or remove the file(s) from history to push.",
|
|
2628
|
+
"Large-object support is tracked in toon-client#235."
|
|
2629
|
+
];
|
|
2630
|
+
}
|
|
2631
|
+
function describeError(err, command = "push") {
|
|
2632
|
+
if (err instanceof UnconfiguredRepoAddressError) {
|
|
2633
|
+
return {
|
|
2634
|
+
code: "unconfigured_repo_address",
|
|
2635
|
+
lines: err.message.split("\n"),
|
|
2636
|
+
json: { error: "unconfigured_repo_address", detail: err.message }
|
|
2637
|
+
};
|
|
2638
|
+
}
|
|
2639
|
+
if (err instanceof NotAGitRepositoryError) {
|
|
2640
|
+
return {
|
|
2641
|
+
code: "not_a_git_repository",
|
|
2642
|
+
lines: err.message.split("\n"),
|
|
2643
|
+
json: { error: "not_a_git_repository", detail: err.message }
|
|
2644
|
+
};
|
|
2645
|
+
}
|
|
2646
|
+
if (err instanceof InvalidRelayUrlError) {
|
|
2647
|
+
return {
|
|
2648
|
+
code: "invalid_relay_url",
|
|
2649
|
+
lines: err.message.split("\n"),
|
|
2650
|
+
json: { error: "invalid_relay_url", detail: err.message, url: err.url }
|
|
2651
|
+
};
|
|
2652
|
+
}
|
|
2653
|
+
if (err instanceof UnknownRemoteError) {
|
|
2654
|
+
return {
|
|
2655
|
+
code: "unknown_remote",
|
|
2656
|
+
lines: err.message.split("\n"),
|
|
2657
|
+
json: {
|
|
2658
|
+
error: "unknown_remote",
|
|
2659
|
+
detail: err.message,
|
|
2660
|
+
remote: err.remote
|
|
2661
|
+
}
|
|
2662
|
+
};
|
|
2663
|
+
}
|
|
2664
|
+
if (err instanceof MultiUrlRemoteError) {
|
|
2665
|
+
return {
|
|
2666
|
+
code: "multi_url_remote",
|
|
2667
|
+
lines: err.message.split("\n"),
|
|
2668
|
+
json: {
|
|
2669
|
+
error: "multi_url_remote",
|
|
2670
|
+
detail: err.message,
|
|
2671
|
+
remote: err.remote,
|
|
2672
|
+
urls: err.urls
|
|
2673
|
+
}
|
|
2674
|
+
};
|
|
2675
|
+
}
|
|
2676
|
+
if (err instanceof NoOriginConfiguredError) {
|
|
2677
|
+
return {
|
|
2678
|
+
code: "no_origin_configured",
|
|
2679
|
+
lines: err.message.split("\n"),
|
|
2680
|
+
json: { error: "no_origin_configured", detail: err.message }
|
|
2681
|
+
};
|
|
2682
|
+
}
|
|
2683
|
+
if (err instanceof MissingIdentityError) {
|
|
2684
|
+
return {
|
|
2685
|
+
code: "missing_identity",
|
|
2686
|
+
lines: err.message.split("\n"),
|
|
2687
|
+
json: { error: "missing_identity", detail: err.message }
|
|
2688
|
+
};
|
|
2689
|
+
}
|
|
2690
|
+
if (err instanceof NonFastForwardError) {
|
|
2691
|
+
return {
|
|
2692
|
+
code: "non_fast_forward",
|
|
2693
|
+
lines: nonFastForwardLines(err.refs),
|
|
2694
|
+
json: { error: "non_fast_forward", detail: err.message, refs: err.refs }
|
|
2695
|
+
};
|
|
2696
|
+
}
|
|
2697
|
+
if (err instanceof OversizeObjectsError) {
|
|
2698
|
+
return {
|
|
2699
|
+
code: "oversize_objects",
|
|
2700
|
+
lines: oversizeLines(err.objects),
|
|
2701
|
+
json: {
|
|
2702
|
+
error: "oversize_objects",
|
|
2703
|
+
detail: err.message,
|
|
2704
|
+
objects: err.objects
|
|
2705
|
+
}
|
|
2706
|
+
};
|
|
2707
|
+
}
|
|
2708
|
+
if (err instanceof GitError) {
|
|
2709
|
+
return {
|
|
2710
|
+
code: "git_error",
|
|
2711
|
+
lines: [`git failed: ${err.message}`],
|
|
2712
|
+
json: { error: "git_error", detail: err.message }
|
|
2713
|
+
};
|
|
2714
|
+
}
|
|
2715
|
+
if (err instanceof Error && err.name === "ArnsSdkUnavailableError") {
|
|
2716
|
+
return {
|
|
2717
|
+
code: "arns_sdk_unavailable",
|
|
2718
|
+
lines: err.message.split("\n"),
|
|
2719
|
+
json: { error: "arns_sdk_unavailable", detail: err.message }
|
|
2720
|
+
};
|
|
2721
|
+
}
|
|
2722
|
+
if (err instanceof Error && err.name === "ArnsSdkIncompatibleError") {
|
|
2723
|
+
return {
|
|
2724
|
+
code: "arns_sdk_incompatible",
|
|
2725
|
+
lines: err.message.split("\n"),
|
|
2726
|
+
json: { error: "arns_sdk_incompatible", detail: err.message }
|
|
2727
|
+
};
|
|
2728
|
+
}
|
|
2729
|
+
if (err instanceof DaemonRouteError) {
|
|
2730
|
+
const envelope = err.envelope;
|
|
2731
|
+
if (envelope.error === "non_fast_forward" && Array.isArray(envelope["refs"])) {
|
|
2732
|
+
return {
|
|
2733
|
+
code: "non_fast_forward",
|
|
2734
|
+
lines: nonFastForwardLines(envelope["refs"]),
|
|
2735
|
+
json: { ...envelope }
|
|
2736
|
+
};
|
|
2737
|
+
}
|
|
2738
|
+
if (envelope.error === "oversize_objects" && Array.isArray(envelope["objects"])) {
|
|
2739
|
+
return {
|
|
2740
|
+
code: "oversize_objects",
|
|
2741
|
+
lines: oversizeLines(envelope["objects"]),
|
|
2742
|
+
json: { ...envelope }
|
|
2743
|
+
};
|
|
2744
|
+
}
|
|
2745
|
+
const retryHint = envelope.retryable === true ? ["The daemon reports this as retryable \u2014 re-run shortly."] : [];
|
|
2746
|
+
return {
|
|
2747
|
+
code: envelope.error,
|
|
2748
|
+
lines: [
|
|
2749
|
+
`daemon rejected the operation (HTTP ${err.status}): ` + (envelope.detail ?? envelope.error),
|
|
2750
|
+
...retryHint
|
|
2751
|
+
],
|
|
2752
|
+
json: { ...envelope }
|
|
2753
|
+
};
|
|
2754
|
+
}
|
|
2755
|
+
if (err instanceof DaemonUnreachableError) {
|
|
2756
|
+
return {
|
|
2757
|
+
code: "daemon_unreachable",
|
|
2758
|
+
lines: err.message.split("\n"),
|
|
2759
|
+
json: { error: "daemon_unreachable", detail: err.message }
|
|
2760
|
+
};
|
|
2761
|
+
}
|
|
2762
|
+
if (err instanceof DaemonTooOldForGitError) {
|
|
2763
|
+
return {
|
|
2764
|
+
code: "daemon_too_old_for_git",
|
|
2765
|
+
lines: err.message.split("\n"),
|
|
2766
|
+
json: {
|
|
2767
|
+
error: "daemon_too_old_for_git",
|
|
2768
|
+
detail: err.message,
|
|
2769
|
+
baseUrl: err.baseUrl,
|
|
2770
|
+
...err.pubkey !== void 0 ? { pubkey: err.pubkey } : {}
|
|
2771
|
+
}
|
|
2772
|
+
};
|
|
2773
|
+
}
|
|
2774
|
+
const name = err instanceof Error ? err.name : "";
|
|
2775
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2776
|
+
if (name === "DaemonIdentityConflictError") {
|
|
2777
|
+
return {
|
|
2778
|
+
code: "daemon_identity_conflict",
|
|
2779
|
+
lines: [
|
|
2780
|
+
message,
|
|
2781
|
+
"Paid writes delegate to a same-identity daemon automatically; seeing this means the daemon appeared mid-run or this operation has no daemon route \u2014 stop the daemon and re-run."
|
|
2782
|
+
],
|
|
2783
|
+
json: { error: "daemon_identity_conflict", detail: message }
|
|
2784
|
+
};
|
|
2785
|
+
}
|
|
2786
|
+
if (name === "MissingUplinkError") {
|
|
2787
|
+
return {
|
|
2788
|
+
code: "missing_uplink",
|
|
2789
|
+
lines: [message],
|
|
2790
|
+
json: { error: "missing_uplink", detail: message }
|
|
2791
|
+
};
|
|
2792
|
+
}
|
|
2793
|
+
if (name === "ChannelMapCorruptError") {
|
|
2794
|
+
return {
|
|
2795
|
+
code: "channel_map_corrupt",
|
|
2796
|
+
lines: [message],
|
|
2797
|
+
json: { error: "channel_map_corrupt", detail: message }
|
|
2798
|
+
};
|
|
2799
|
+
}
|
|
2800
|
+
if (name === "SettleTooEarlyError") {
|
|
2801
|
+
const settleableAt = err.settleableAt;
|
|
2802
|
+
return {
|
|
2803
|
+
code: "settle_too_early",
|
|
2804
|
+
lines: [
|
|
2805
|
+
message,
|
|
2806
|
+
"The settlement challenge window is still open \u2014 nothing was spent. Re-run `rig channel settle` after the settleable time."
|
|
2807
|
+
],
|
|
2808
|
+
json: {
|
|
2809
|
+
error: "settle_too_early",
|
|
2810
|
+
detail: message,
|
|
2811
|
+
...typeof settleableAt === "string" ? { settleableAt } : {}
|
|
2812
|
+
}
|
|
2813
|
+
};
|
|
2814
|
+
}
|
|
2815
|
+
if (name === "RepoNotFoundError") {
|
|
2816
|
+
return {
|
|
2817
|
+
code: "repo_not_found",
|
|
2818
|
+
lines: message.split("\n"),
|
|
2819
|
+
json: { error: "repo_not_found", detail: message }
|
|
2820
|
+
};
|
|
2821
|
+
}
|
|
2822
|
+
if (name === "MissingRemoteObjectsError") {
|
|
2823
|
+
const missing = err.missing;
|
|
2824
|
+
return {
|
|
2825
|
+
code: "missing_remote_objects",
|
|
2826
|
+
lines: message.split("\n"),
|
|
2827
|
+
json: {
|
|
2828
|
+
error: "missing_remote_objects",
|
|
2829
|
+
detail: message,
|
|
2830
|
+
...Array.isArray(missing) ? { missing } : {}
|
|
2831
|
+
}
|
|
2832
|
+
};
|
|
2833
|
+
}
|
|
2834
|
+
if (name === "ObjectIntegrityError" || name === "ObjectWriteMismatchError") {
|
|
2835
|
+
return {
|
|
2836
|
+
code: "object_integrity",
|
|
2837
|
+
lines: message.split("\n"),
|
|
2838
|
+
json: { error: "object_integrity", detail: message }
|
|
2839
|
+
};
|
|
2840
|
+
}
|
|
2841
|
+
return {
|
|
2842
|
+
code: "error",
|
|
2843
|
+
lines: [`rig ${command} failed: ${message}`],
|
|
2844
|
+
json: { error: "error", detail: message }
|
|
2845
|
+
};
|
|
2846
|
+
}
|
|
2847
|
+
function emitCliError(io, json, command, err) {
|
|
2848
|
+
const described = describeError(err, command);
|
|
2849
|
+
if (json) io.emitJson({ command, ...described.json });
|
|
2850
|
+
for (const line of described.lines) io.err(line);
|
|
2851
|
+
return 1;
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2854
|
+
// src/cli/ci-status.ts
|
|
2855
|
+
import { parseArgs as parseArgs2 } from "util";
|
|
2856
|
+
import { REPOSITORY_ANNOUNCEMENT_KIND } from "@toon-protocol/core/nip34";
|
|
2857
|
+
|
|
2858
|
+
// src/cli/git-config.ts
|
|
2859
|
+
import { execFile as execFile3 } from "child_process";
|
|
2860
|
+
import { promisify as promisify3 } from "util";
|
|
2861
|
+
var execFileAsync3 = promisify3(execFile3);
|
|
2862
|
+
async function git(repoPath, args, allowExitCodes = []) {
|
|
2863
|
+
try {
|
|
2864
|
+
const { stdout } = await execFileAsync3("git", args, {
|
|
2865
|
+
cwd: repoPath,
|
|
2866
|
+
encoding: "utf-8"
|
|
2867
|
+
});
|
|
2868
|
+
return { stdout, exitCode: 0 };
|
|
2869
|
+
} catch (err) {
|
|
2870
|
+
const e = err;
|
|
2871
|
+
const exitCode = typeof e.code === "number" ? e.code : void 0;
|
|
2872
|
+
if (exitCode !== void 0 && allowExitCodes.includes(exitCode)) {
|
|
2873
|
+
return { stdout: e.stdout ?? "", exitCode };
|
|
2874
|
+
}
|
|
2875
|
+
throw new Error(
|
|
2876
|
+
`git ${args.join(" ")} failed${exitCode !== void 0 ? ` (exit ${exitCode})` : ""}: ${(e.stderr ?? e.message ?? "").trim()}`
|
|
2877
|
+
);
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
async function resolveRepoRoot(cwd) {
|
|
2881
|
+
try {
|
|
2882
|
+
const { stdout } = await execFileAsync3(
|
|
2883
|
+
"git",
|
|
2884
|
+
["rev-parse", "--show-toplevel"],
|
|
2885
|
+
{ cwd, encoding: "utf-8" }
|
|
2886
|
+
);
|
|
2887
|
+
const root = stdout.trim();
|
|
2888
|
+
if (!root) throw new Error("empty rev-parse output");
|
|
2889
|
+
return root;
|
|
2890
|
+
} catch (err) {
|
|
2891
|
+
const e = err;
|
|
2892
|
+
throw new Error(
|
|
2893
|
+
`not a git repository (run rig inside a repo): ${(e.stderr ?? e.message ?? "").trim()}`
|
|
2894
|
+
);
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
async function initGitRepository(dir) {
|
|
2898
|
+
try {
|
|
2899
|
+
await git(dir, ["init", "--initial-branch=main"]);
|
|
2900
|
+
} catch {
|
|
2901
|
+
await git(dir, ["init"]);
|
|
2902
|
+
await git(dir, ["symbolic-ref", "HEAD", "refs/heads/main"]);
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
async function readToonConfig(repoPath) {
|
|
2906
|
+
const [repoId, owner, relays] = await Promise.all([
|
|
2907
|
+
git(repoPath, ["config", "--get", "toon.repoid"], [1]),
|
|
2908
|
+
git(repoPath, ["config", "--get", "toon.owner"], [1]),
|
|
2909
|
+
git(repoPath, ["config", "--get-all", "toon.relay"], [1])
|
|
2910
|
+
]);
|
|
2911
|
+
const config = {
|
|
2912
|
+
relays: relays.stdout.split("\n").map((l) => l.trim()).filter(Boolean)
|
|
2913
|
+
};
|
|
2914
|
+
const id = repoId.stdout.trim();
|
|
2915
|
+
if (repoId.exitCode === 0 && id) config.repoId = id;
|
|
2916
|
+
const own = owner.stdout.trim();
|
|
2917
|
+
if (owner.exitCode === 0 && own) config.owner = own;
|
|
2918
|
+
return config;
|
|
2919
|
+
}
|
|
2920
|
+
async function getGitRemoteUrls(repoPath, name) {
|
|
2921
|
+
const { stdout } = await git(
|
|
2922
|
+
repoPath,
|
|
2923
|
+
["config", "--get-all", `remote.${name}.url`],
|
|
2924
|
+
[1]
|
|
2925
|
+
);
|
|
2926
|
+
return stdout.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
2927
|
+
}
|
|
2928
|
+
async function listGitRemotes(repoPath) {
|
|
2929
|
+
const { stdout } = await git(repoPath, ["remote"]);
|
|
2930
|
+
const names = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
2931
|
+
const remotes = [];
|
|
2932
|
+
for (const name of names) {
|
|
2933
|
+
remotes.push({ name, urls: await getGitRemoteUrls(repoPath, name) });
|
|
2934
|
+
}
|
|
2935
|
+
return remotes;
|
|
2936
|
+
}
|
|
2937
|
+
async function setGitAuthor(repoPath, author) {
|
|
2938
|
+
await git(repoPath, ["config", "--local", "user.name", author.name]);
|
|
2939
|
+
await git(repoPath, ["config", "--local", "user.email", author.email]);
|
|
2940
|
+
}
|
|
2941
|
+
async function addGitRemote(repoPath, name, url) {
|
|
2942
|
+
await git(repoPath, ["remote", "add", name, url]);
|
|
2943
|
+
}
|
|
2944
|
+
async function removeGitRemote(repoPath, name) {
|
|
2945
|
+
await git(repoPath, ["remote", "remove", name]);
|
|
2946
|
+
}
|
|
2947
|
+
async function writeToonConfig(repoPath, config) {
|
|
2948
|
+
if (config.repoId !== void 0) {
|
|
2949
|
+
await git(repoPath, ["config", "toon.repoid", config.repoId]);
|
|
2950
|
+
}
|
|
2951
|
+
if (config.owner !== void 0) {
|
|
2952
|
+
await git(repoPath, ["config", "toon.owner", config.owner]);
|
|
2953
|
+
}
|
|
2954
|
+
if (config.relays !== void 0 && config.relays.length > 0) {
|
|
2955
|
+
await git(repoPath, ["config", "--unset-all", "toon.relay"], [5]);
|
|
2956
|
+
for (const relay of config.relays) {
|
|
2957
|
+
await git(repoPath, ["config", "--add", "toon.relay", relay]);
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2962
|
+
// src/cli/remote.ts
|
|
2963
|
+
import { parseArgs } from "util";
|
|
2964
|
+
var RELAY_PROTOCOLS = /* @__PURE__ */ new Set(["ws:", "wss:", "http:", "https:"]);
|
|
2965
|
+
function isRelayUrl(url) {
|
|
2966
|
+
try {
|
|
2967
|
+
return RELAY_PROTOCOLS.has(new URL(url).protocol);
|
|
2968
|
+
} catch {
|
|
2969
|
+
return false;
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
function assertRelayUrl(url, context) {
|
|
2973
|
+
if (!isRelayUrl(url)) throw new InvalidRelayUrlError(url, context);
|
|
2974
|
+
}
|
|
2975
|
+
async function resolveRelays(opts) {
|
|
2976
|
+
if (opts.relayFlags.length > 0) {
|
|
2977
|
+
return { relays: opts.relayFlags, source: "relay-flag" };
|
|
2978
|
+
}
|
|
2979
|
+
if (opts.remoteName !== void 0) {
|
|
2980
|
+
const urls = opts.repoRoot !== void 0 ? await getGitRemoteUrls(opts.repoRoot, opts.remoteName) : [];
|
|
2981
|
+
if (urls.length === 0) throw new UnknownRemoteError(opts.remoteName);
|
|
2982
|
+
if (urls.length > 1) throw new MultiUrlRemoteError(opts.remoteName, urls);
|
|
2983
|
+
const url = urls[0];
|
|
2984
|
+
assertRelayUrl(url, `remote ${JSON.stringify(opts.remoteName)}`);
|
|
2985
|
+
return { relays: urls, source: "remote", remoteName: opts.remoteName };
|
|
2986
|
+
}
|
|
2987
|
+
let nonRelayOriginUrl;
|
|
2988
|
+
if (opts.repoRoot !== void 0) {
|
|
2989
|
+
const urls = await getGitRemoteUrls(opts.repoRoot, "origin");
|
|
2990
|
+
if (urls.length === 1 && isRelayUrl(urls[0])) {
|
|
2991
|
+
return { relays: urls, source: "remote", remoteName: "origin" };
|
|
2992
|
+
}
|
|
2993
|
+
if (urls.length > 1 && urls.some(isRelayUrl)) {
|
|
2994
|
+
throw new MultiUrlRemoteError("origin", urls);
|
|
2995
|
+
}
|
|
2996
|
+
if (urls.length > 0) nonRelayOriginUrl = urls[0];
|
|
2997
|
+
}
|
|
2998
|
+
if (opts.toonRelays.length > 0) {
|
|
2999
|
+
return {
|
|
3000
|
+
relays: opts.toonRelays,
|
|
3001
|
+
source: "toon.relay",
|
|
3002
|
+
nudge: `note: git config toon.relay is deprecated (removed in v0.3) \u2014 migrate: rig remote add origin ${opts.toonRelays[0]}`
|
|
3003
|
+
};
|
|
3004
|
+
}
|
|
3005
|
+
throw new NoOriginConfiguredError(nonRelayOriginUrl);
|
|
3006
|
+
}
|
|
3007
|
+
function singleRelayRefusal(resolved, nothingHappened) {
|
|
3008
|
+
const fix = resolved.source === "relay-flag" ? "pass exactly one --relay <url>" : "trim git config toon.relay to one URL (better: migrate \u2014 `rig remote add origin <relay-url>`)";
|
|
3009
|
+
return `rig publishes to a single relay, but ${resolved.relays.length} are configured (${resolved.relays.join(", ")}) \u2014 ${fix}. ${nothingHappened}`;
|
|
3010
|
+
}
|
|
3011
|
+
var REMOTE_USAGE = `Usage: rig remote <add|remove|list> [args] [options]
|
|
3012
|
+
|
|
3013
|
+
Manage the relays this repo publishes to. Remotes are stored as REAL git
|
|
3014
|
+
remotes (\`git remote -v\` shows them; plain git tooling round-trips). Paid
|
|
3015
|
+
commands publish via the "origin" remote by default; \`rig push <remote>\`
|
|
3016
|
+
and \`--remote <name>\` target another one. Free \u2014 nothing is published or
|
|
3017
|
+
paid.
|
|
3018
|
+
|
|
3019
|
+
Commands:
|
|
3020
|
+
add <name> <relay-url> add a remote pointing at a relay (the URL must be
|
|
3021
|
+
ws://, wss://, http://, or https://)
|
|
3022
|
+
remove <name> remove a remote
|
|
3023
|
+
list list remote names + URLs (the default subcommand)
|
|
3024
|
+
|
|
3025
|
+
Options:
|
|
3026
|
+
--json machine-readable envelope (all subcommands)
|
|
3027
|
+
-h, --help show this help`;
|
|
3028
|
+
async function runRemote(args, deps) {
|
|
3029
|
+
const { io } = deps;
|
|
3030
|
+
let sub;
|
|
3031
|
+
let rest;
|
|
3032
|
+
let json;
|
|
3033
|
+
try {
|
|
3034
|
+
const { values: values2, positionals } = parseArgs({
|
|
3035
|
+
args,
|
|
3036
|
+
options: {
|
|
3037
|
+
json: { type: "boolean", default: false },
|
|
3038
|
+
help: { type: "boolean", short: "h", default: false }
|
|
3039
|
+
},
|
|
3040
|
+
allowPositionals: true
|
|
3041
|
+
});
|
|
3042
|
+
if (values2.help) {
|
|
3043
|
+
io.out(REMOTE_USAGE);
|
|
3044
|
+
return 0;
|
|
3045
|
+
}
|
|
3046
|
+
json = values2.json ?? false;
|
|
3047
|
+
[sub, ...rest] = positionals;
|
|
3048
|
+
} catch (err) {
|
|
3049
|
+
io.err(err instanceof Error ? err.message : String(err));
|
|
3050
|
+
io.err(REMOTE_USAGE);
|
|
3051
|
+
return 2;
|
|
3052
|
+
}
|
|
3053
|
+
switch (sub) {
|
|
3054
|
+
case void 0:
|
|
3055
|
+
case "list":
|
|
3056
|
+
if (rest.length > 0) {
|
|
3057
|
+
io.err(`rig remote list takes no arguments (got ${rest.join(" ")})`);
|
|
3058
|
+
io.err(REMOTE_USAGE);
|
|
3059
|
+
return 2;
|
|
3060
|
+
}
|
|
3061
|
+
break;
|
|
3062
|
+
case "add": {
|
|
3063
|
+
if (rest.length !== 2) {
|
|
3064
|
+
io.err("usage: rig remote add <name> <relay-url>");
|
|
3065
|
+
io.err(REMOTE_USAGE);
|
|
3066
|
+
return 2;
|
|
3067
|
+
}
|
|
3068
|
+
const url = rest[1];
|
|
3069
|
+
if (!isRelayUrl(url)) {
|
|
3070
|
+
io.err(
|
|
3071
|
+
`cannot add remote: ${JSON.stringify(url)} is not a relay URL \u2014 relays are ws://, wss://, http://, or https://`
|
|
3072
|
+
);
|
|
3073
|
+
return 2;
|
|
3074
|
+
}
|
|
3075
|
+
break;
|
|
3076
|
+
}
|
|
3077
|
+
case "remove":
|
|
3078
|
+
if (rest.length !== 1) {
|
|
3079
|
+
io.err("usage: rig remote remove <name>");
|
|
3080
|
+
io.err(REMOTE_USAGE);
|
|
3081
|
+
return 2;
|
|
3082
|
+
}
|
|
3083
|
+
break;
|
|
3084
|
+
default:
|
|
3085
|
+
io.err(`unknown rig remote subcommand: ${sub}`);
|
|
3086
|
+
io.err(REMOTE_USAGE);
|
|
3087
|
+
return 2;
|
|
3088
|
+
}
|
|
3089
|
+
try {
|
|
3090
|
+
let repoRoot;
|
|
3091
|
+
try {
|
|
3092
|
+
repoRoot = await resolveRepoRoot(deps.cwd);
|
|
3093
|
+
} catch {
|
|
3094
|
+
throw new NotAGitRepositoryError(deps.cwd);
|
|
3095
|
+
}
|
|
3096
|
+
switch (sub) {
|
|
3097
|
+
case void 0:
|
|
3098
|
+
case "list": {
|
|
3099
|
+
const remotes = await listGitRemotes(repoRoot);
|
|
3100
|
+
if (json) {
|
|
3101
|
+
io.emitJson({ command: "remote", remotes });
|
|
3102
|
+
return 0;
|
|
3103
|
+
}
|
|
3104
|
+
if (remotes.length === 0) {
|
|
3105
|
+
io.out(
|
|
3106
|
+
"no remotes configured \u2014 add one: rig remote add origin <relay-url>"
|
|
3107
|
+
);
|
|
3108
|
+
return 0;
|
|
3109
|
+
}
|
|
3110
|
+
for (const remote of remotes) {
|
|
3111
|
+
for (const url of remote.urls) {
|
|
3112
|
+
io.out(
|
|
3113
|
+
`${remote.name} ${url}` + (isRelayUrl(url) ? "" : " (not a relay URL \u2014 ignored by rig)")
|
|
3114
|
+
);
|
|
3115
|
+
}
|
|
3116
|
+
if (remote.urls.length > 1) {
|
|
3117
|
+
io.err(
|
|
3118
|
+
`warning: remote "${remote.name}" has ${remote.urls.length} URLs \u2014 rig supports one relay URL per remote (fix with \`git remote set-url ${remote.name} <relay-url>\`)`
|
|
3119
|
+
);
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
return 0;
|
|
3123
|
+
}
|
|
3124
|
+
case "add": {
|
|
3125
|
+
const [name, url] = rest;
|
|
3126
|
+
const existing = await getGitRemoteUrls(repoRoot, name);
|
|
3127
|
+
if (existing.length > 0) {
|
|
3128
|
+
const detail = `remote ${JSON.stringify(name)} already exists (${existing.join(", ")}) \u2014 nothing changed. Point it somewhere else with \`git remote set-url ${name} <relay-url>\`, or \`rig remote remove ${name}\` first.`;
|
|
3129
|
+
if (json) {
|
|
3130
|
+
io.emitJson({
|
|
3131
|
+
command: "remote",
|
|
3132
|
+
error: "remote_exists",
|
|
3133
|
+
detail,
|
|
3134
|
+
remote: name,
|
|
3135
|
+
urls: existing
|
|
3136
|
+
});
|
|
3137
|
+
}
|
|
3138
|
+
io.err(detail);
|
|
3139
|
+
return 1;
|
|
3140
|
+
}
|
|
3141
|
+
await addGitRemote(repoRoot, name, url);
|
|
3142
|
+
if (!json) io.out(`Added remote ${name} \u2192 ${url}`);
|
|
3143
|
+
if (name === "origin") {
|
|
3144
|
+
if (!json) {
|
|
3145
|
+
io.out(
|
|
3146
|
+
"`rig push` and the event commands now publish here by default."
|
|
3147
|
+
);
|
|
3148
|
+
}
|
|
3149
|
+
const toonConfig = await readToonConfig(repoRoot);
|
|
3150
|
+
if (toonConfig.relays.length > 0) {
|
|
3151
|
+
io.err(
|
|
3152
|
+
`note: git config toon.relay (${toonConfig.relays.join(", ")}) is deprecated and now shadowed by the origin remote \u2014 drop it with \`git config --unset-all toon.relay\` (the key is removed in v0.3).`
|
|
3153
|
+
);
|
|
3154
|
+
}
|
|
3155
|
+
}
|
|
3156
|
+
if (json) {
|
|
3157
|
+
io.emitJson({ command: "remote", action: "add", name, url });
|
|
3158
|
+
}
|
|
3159
|
+
return 0;
|
|
3160
|
+
}
|
|
3161
|
+
case "remove": {
|
|
3162
|
+
const name = rest[0];
|
|
3163
|
+
const existing = await getGitRemoteUrls(repoRoot, name);
|
|
3164
|
+
if (existing.length === 0) {
|
|
3165
|
+
const detail = `no remote named ${JSON.stringify(name)} \u2014 \`rig remote list\` shows configured remotes.`;
|
|
3166
|
+
if (json) {
|
|
3167
|
+
io.emitJson({
|
|
3168
|
+
command: "remote",
|
|
3169
|
+
error: "unknown_remote",
|
|
3170
|
+
detail,
|
|
3171
|
+
remote: name
|
|
3172
|
+
});
|
|
3173
|
+
}
|
|
3174
|
+
io.err(detail);
|
|
3175
|
+
return 1;
|
|
3176
|
+
}
|
|
3177
|
+
await removeGitRemote(repoRoot, name);
|
|
3178
|
+
if (json) {
|
|
3179
|
+
io.emitJson({ command: "remote", action: "remove", name });
|
|
3180
|
+
} else {
|
|
3181
|
+
io.out(`Removed remote ${name}`);
|
|
3182
|
+
}
|
|
3183
|
+
return 0;
|
|
3184
|
+
}
|
|
3185
|
+
/* v8 ignore next 2 -- unreachable: validated above */
|
|
3186
|
+
default:
|
|
3187
|
+
return 2;
|
|
3188
|
+
}
|
|
3189
|
+
} catch (err) {
|
|
3190
|
+
return emitCliError(io, json, "remote", err);
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
|
|
3194
|
+
// src/cli/ci-status.ts
|
|
3195
|
+
var CI_STATUS_USAGE = `Usage: rig ci status <commit> [options]
|
|
3196
|
+
|
|
3197
|
+
Show every CI run and job for <commit> \u2014 FREE (relay reads only). <commit>
|
|
3198
|
+
is a full 40-hex SHA, or any local revision (HEAD, a branch, a short SHA)
|
|
3199
|
+
when run inside the repository. Exit status is the gate: 0 when every
|
|
3200
|
+
counted run concluded green (success/neutral/skipped); 1 when any counted
|
|
3201
|
+
run failed, timed out, was cancelled, or failed to start, when a run is
|
|
3202
|
+
still queued/in progress, or when the commit has no concluded run at all.
|
|
3203
|
+
|
|
3204
|
+
A run counts when it is its publisher's latest attempt for its workflow.
|
|
3205
|
+
Trust levels (NIP-C1, derived from the repo's maintainers and Service
|
|
3206
|
+
Requests): maintainer-directed > operationally-associated > seen-in-network
|
|
3207
|
+
> no-known-context. With --require-ci-trust, runs below that level are
|
|
3208
|
+
dropped before counting \u2014 so an unknown coordinator's green never gates.
|
|
3209
|
+
|
|
3210
|
+
Options:
|
|
3211
|
+
--require-ci-trust <level> minimum trust level to count a run
|
|
3212
|
+
--workflow <path> only runs of this workflow file
|
|
3213
|
+
--repo-id <id> repository id / NIP-34 d-tag (default: git config)
|
|
3214
|
+
--owner <pubkey> repository owner (npub or 64-char hex; default: git config)
|
|
3215
|
+
--remote <name> read via this configured git remote (default: origin)
|
|
3216
|
+
--relay <url> ad-hoc relay override; repeatable \u2014 reads are merged
|
|
3217
|
+
--json machine-readable envelope (exactly one JSON document)
|
|
3218
|
+
-h, --help show this help`;
|
|
3219
|
+
var GREEN = /* @__PURE__ */ new Set([
|
|
3220
|
+
"success",
|
|
3221
|
+
"neutral",
|
|
3222
|
+
"skipped"
|
|
3223
|
+
]);
|
|
3224
|
+
var STATUS_OPTIONS = {
|
|
3225
|
+
json: { type: "boolean", default: false },
|
|
3226
|
+
relay: { type: "string", multiple: true },
|
|
3227
|
+
remote: { type: "string" },
|
|
3228
|
+
"repo-id": { type: "string" },
|
|
3229
|
+
owner: { type: "string" },
|
|
3230
|
+
"require-ci-trust": { type: "string" },
|
|
3231
|
+
workflow: { type: "string" },
|
|
3232
|
+
help: { type: "boolean", short: "h", default: false }
|
|
3233
|
+
};
|
|
3234
|
+
var FULL_SHA_RE4 = /^[0-9a-f]{40}$/;
|
|
3235
|
+
var WS_URL_RE = /^wss?:\/\//i;
|
|
3236
|
+
var RELAY_TIMEOUT_MS = 1e4;
|
|
3237
|
+
async function queryAll(relays, filters, webSocketFactory) {
|
|
3238
|
+
const jobs = relays.flatMap(
|
|
3239
|
+
(relay) => filters.map(
|
|
3240
|
+
(filter) => queryRelay(relay, filter, RELAY_TIMEOUT_MS, webSocketFactory)
|
|
3241
|
+
)
|
|
3242
|
+
);
|
|
3243
|
+
const results = await Promise.allSettled(jobs);
|
|
3244
|
+
const byId = /* @__PURE__ */ new Map();
|
|
3245
|
+
let failures = 0;
|
|
3246
|
+
let firstError;
|
|
3247
|
+
for (const result of results) {
|
|
3248
|
+
if (result.status === "rejected") {
|
|
3249
|
+
failures += 1;
|
|
3250
|
+
firstError ??= result.reason;
|
|
3251
|
+
continue;
|
|
3252
|
+
}
|
|
3253
|
+
for (const event of result.value) {
|
|
3254
|
+
if (typeof event.id === "string" && !byId.has(event.id))
|
|
3255
|
+
byId.set(event.id, event);
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
3258
|
+
if (failures === results.length && results.length > 0) {
|
|
3259
|
+
throw firstError instanceof Error ? firstError : new Error(String(firstError));
|
|
3260
|
+
}
|
|
3261
|
+
return [...byId.values()];
|
|
3262
|
+
}
|
|
3263
|
+
function assembleCiStatus(opts) {
|
|
3264
|
+
const repoAddr = repoAddress(opts.ownerPubkey, opts.repoId);
|
|
3265
|
+
const commit = opts.commit.toLowerCase();
|
|
3266
|
+
const controls = [];
|
|
3267
|
+
const results = /* @__PURE__ */ new Map();
|
|
3268
|
+
const progresses = /* @__PURE__ */ new Map();
|
|
3269
|
+
const jobs = [];
|
|
3270
|
+
for (const ev of opts.events) {
|
|
3271
|
+
switch (ev.kind) {
|
|
3272
|
+
case CI_SERVICE_REQUEST_KIND:
|
|
3273
|
+
case CI_SERVICE_STOP_KIND: {
|
|
3274
|
+
const c = parseCiServiceControl(ev);
|
|
3275
|
+
if (c && c.repoAddr === repoAddr) controls.push(c);
|
|
3276
|
+
break;
|
|
3277
|
+
}
|
|
3278
|
+
case CI_WORKFLOW_RESULT_KIND: {
|
|
3279
|
+
const r = parseCiWorkflowResult(ev);
|
|
3280
|
+
if (!r || r.trigger.repoAddr !== repoAddr || r.trigger.commit !== commit)
|
|
3281
|
+
break;
|
|
3282
|
+
const key = `${r.pubkey}:${r.runId}`;
|
|
3283
|
+
const prev = results.get(key);
|
|
3284
|
+
if (!prev || r.createdAt > prev.createdAt || r.createdAt === prev.createdAt && r.eventId < prev.eventId) {
|
|
3285
|
+
results.set(key, r);
|
|
3286
|
+
}
|
|
3287
|
+
break;
|
|
3288
|
+
}
|
|
3289
|
+
case CI_WORKFLOW_PROGRESS_KIND: {
|
|
3290
|
+
const p = parseCiWorkflowProgress(ev);
|
|
3291
|
+
if (!p || p.trigger.repoAddr !== repoAddr || p.trigger.commit !== commit)
|
|
3292
|
+
break;
|
|
3293
|
+
const key = `${p.pubkey}:${p.runId}`;
|
|
3294
|
+
const prev = progresses.get(key);
|
|
3295
|
+
if (!prev || p.createdAt > prev.createdAt || p.createdAt === prev.createdAt && p.eventId < prev.eventId) {
|
|
3296
|
+
progresses.set(key, p);
|
|
3297
|
+
}
|
|
3298
|
+
break;
|
|
3299
|
+
}
|
|
3300
|
+
case CI_JOB_RESULT_KIND: {
|
|
3301
|
+
const j = parseCiJobResult(ev);
|
|
3302
|
+
if (j && j.trigger.repoAddr === repoAddr && j.trigger.commit === commit)
|
|
3303
|
+
jobs.push(j);
|
|
3304
|
+
break;
|
|
3305
|
+
}
|
|
3306
|
+
default:
|
|
3307
|
+
break;
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
const runs = [];
|
|
3311
|
+
const keys = /* @__PURE__ */ new Set([...results.keys(), ...progresses.keys()]);
|
|
3312
|
+
for (const key of keys) {
|
|
3313
|
+
const result = results.get(key) ?? null;
|
|
3314
|
+
const progress = progresses.get(key) ?? null;
|
|
3315
|
+
const base = result ?? progress;
|
|
3316
|
+
if (!base) continue;
|
|
3317
|
+
const progressAddress = `${CI_WORKFLOW_PROGRESS_KIND}:${base.pubkey}:${base.runId}`;
|
|
3318
|
+
const runJobs = jobs.filter(
|
|
3319
|
+
(j) => j.progressAddress.toLowerCase() === progressAddress.toLowerCase()
|
|
3320
|
+
).sort(
|
|
3321
|
+
(a, b) => (a.startedAt ?? a.createdAt) - (b.startedAt ?? b.createdAt)
|
|
3322
|
+
).map((j) => ({
|
|
3323
|
+
jobId: j.jobId,
|
|
3324
|
+
...j.name !== void 0 ? { name: j.name } : {},
|
|
3325
|
+
conclusion: j.conclusion,
|
|
3326
|
+
...j.logsUrl !== void 0 ? { logsUrl: j.logsUrl } : {},
|
|
3327
|
+
artifacts: j.artifacts,
|
|
3328
|
+
...j.exitCode !== void 0 ? { exitCode: j.exitCode } : {},
|
|
3329
|
+
...j.startedAt !== void 0 ? { startedAt: j.startedAt } : {},
|
|
3330
|
+
eventId: j.eventId,
|
|
3331
|
+
publisher: j.pubkey
|
|
3332
|
+
}));
|
|
3333
|
+
const provenance = result?.provenance ?? progress?.provenance;
|
|
3334
|
+
const trust = deriveTrustLevel({
|
|
3335
|
+
publisherPubkey: base.pubkey,
|
|
3336
|
+
...provenance ? { provenance } : {},
|
|
3337
|
+
authorized: opts.authorized,
|
|
3338
|
+
controls,
|
|
3339
|
+
runCreatedAt: base.createdAt
|
|
3340
|
+
});
|
|
3341
|
+
const status = result ? "concluded" : progress.status;
|
|
3342
|
+
const conclusion = result ? result.conclusion : progress?.conclusion;
|
|
3343
|
+
const startedAt = result?.startedAt ?? progress?.startedAt;
|
|
3344
|
+
const queuedAt = result?.queuedAt ?? progress?.queuedAt;
|
|
3345
|
+
runs.push({
|
|
3346
|
+
runId: base.runId,
|
|
3347
|
+
coordinator: base.pubkey,
|
|
3348
|
+
workflow: base.trigger.workflow,
|
|
3349
|
+
reason: base.trigger.reason,
|
|
3350
|
+
status,
|
|
3351
|
+
...conclusion !== void 0 ? { conclusion } : {},
|
|
3352
|
+
trust,
|
|
3353
|
+
createdAt: base.createdAt,
|
|
3354
|
+
...startedAt !== void 0 ? { startedAt } : {},
|
|
3355
|
+
...queuedAt !== void 0 ? { queuedAt } : {},
|
|
3356
|
+
...provenance ? { provenance } : {},
|
|
3357
|
+
jobs: runJobs,
|
|
3358
|
+
eventId: base.eventId
|
|
3359
|
+
});
|
|
3360
|
+
}
|
|
3361
|
+
runs.sort(
|
|
3362
|
+
(a, b) => b.createdAt - a.createdAt || (a.eventId < b.eventId ? -1 : 1)
|
|
3363
|
+
);
|
|
3364
|
+
const latestByWorkflow = /* @__PURE__ */ new Map();
|
|
3365
|
+
for (const run of runs) {
|
|
3366
|
+
const k = `${run.coordinator}:${run.workflow.path}`;
|
|
3367
|
+
if (!latestByWorkflow.has(k)) latestByWorkflow.set(k, run);
|
|
3368
|
+
}
|
|
3369
|
+
const counted = [...latestByWorkflow.values()].filter(
|
|
3370
|
+
(run) => (opts.workflow === void 0 || run.workflow.path === opts.workflow) && (opts.requireTrust === void 0 || trustAtLeast(run.trust, opts.requireTrust))
|
|
3371
|
+
);
|
|
3372
|
+
let green = 0;
|
|
3373
|
+
let red = 0;
|
|
3374
|
+
let pending = 0;
|
|
3375
|
+
for (const run of counted) {
|
|
3376
|
+
if (run.status !== "concluded" || run.conclusion === void 0)
|
|
3377
|
+
pending += 1;
|
|
3378
|
+
else if (GREEN.has(run.conclusion)) green += 1;
|
|
3379
|
+
else red += 1;
|
|
3380
|
+
}
|
|
3381
|
+
const summary = {
|
|
3382
|
+
counted: counted.length,
|
|
3383
|
+
green,
|
|
3384
|
+
red,
|
|
3385
|
+
pending
|
|
3386
|
+
};
|
|
3387
|
+
let reason;
|
|
3388
|
+
if (counted.length === 0) {
|
|
3389
|
+
reason = runs.length === 0 ? "no CI runs found for this commit" : "no run meets the workflow/trust requirement";
|
|
3390
|
+
} else if (red > 0) {
|
|
3391
|
+
reason = `${red} run(s) concluded red`;
|
|
3392
|
+
} else if (pending > 0) {
|
|
3393
|
+
reason = `${pending} run(s) not concluded yet`;
|
|
3394
|
+
}
|
|
3395
|
+
const ok = counted.length > 0 && red === 0 && pending === 0;
|
|
3396
|
+
return { runs, summary, ok, ...reason !== void 0 ? { reason } : {} };
|
|
3397
|
+
}
|
|
3398
|
+
async function runCiStatus(args, deps) {
|
|
3399
|
+
const { io } = deps;
|
|
3400
|
+
let flags;
|
|
3401
|
+
let commitArg;
|
|
3402
|
+
try {
|
|
3403
|
+
const { values: values2, positionals } = parseArgs2({
|
|
3404
|
+
args,
|
|
3405
|
+
options: STATUS_OPTIONS,
|
|
3406
|
+
allowPositionals: true
|
|
3407
|
+
});
|
|
3408
|
+
flags = {
|
|
3409
|
+
json: values2.json === true,
|
|
3410
|
+
help: values2.help === true,
|
|
3411
|
+
relay: values2.relay ?? []
|
|
3412
|
+
};
|
|
3413
|
+
if (values2.remote !== void 0) flags.remote = values2.remote;
|
|
3414
|
+
if (values2["repo-id"] !== void 0) flags.repoId = values2["repo-id"];
|
|
3415
|
+
if (values2.owner !== void 0) flags.owner = ownerToHex(values2.owner);
|
|
3416
|
+
if (values2.workflow !== void 0)
|
|
3417
|
+
flags.workflow = values2.workflow.replace(/^\.\//, "");
|
|
3418
|
+
if (values2["require-ci-trust"] !== void 0) {
|
|
3419
|
+
const level = values2["require-ci-trust"];
|
|
3420
|
+
if (!isCiTrustLevel(level)) {
|
|
3421
|
+
throw new Error(
|
|
3422
|
+
`--require-ci-trust must be one of ${CI_TRUST_ORDER.join(" | ")} (got ${JSON.stringify(level)})`
|
|
3423
|
+
);
|
|
3424
|
+
}
|
|
3425
|
+
flags.requireTrust = level;
|
|
3426
|
+
}
|
|
3427
|
+
if (flags.help) {
|
|
3428
|
+
io.out(CI_STATUS_USAGE);
|
|
3429
|
+
return 0;
|
|
3430
|
+
}
|
|
3431
|
+
if (positionals.length !== 1) {
|
|
3432
|
+
throw new Error(
|
|
3433
|
+
positionals.length === 0 ? "<commit> is required" : `expected exactly one <commit>, got ${positionals.length}`
|
|
3434
|
+
);
|
|
3435
|
+
}
|
|
3436
|
+
commitArg = positionals[0];
|
|
3437
|
+
} catch (err) {
|
|
3438
|
+
io.err(err instanceof Error ? err.message : String(err));
|
|
3439
|
+
io.err(CI_STATUS_USAGE);
|
|
3440
|
+
return 2;
|
|
3441
|
+
}
|
|
3442
|
+
try {
|
|
3443
|
+
let repoRoot;
|
|
3444
|
+
let toonConfig = {
|
|
3445
|
+
relays: []
|
|
3446
|
+
};
|
|
3447
|
+
try {
|
|
3448
|
+
repoRoot = await resolveRepoRoot(deps.cwd);
|
|
3449
|
+
toonConfig = await readToonConfig(repoRoot);
|
|
3450
|
+
} catch {
|
|
3451
|
+
}
|
|
3452
|
+
const repoId = flags.repoId ?? toonConfig.repoId;
|
|
3453
|
+
if (!repoId) throw new UnconfiguredRepoAddressError("repository id");
|
|
3454
|
+
const owner = flags.owner ?? toonConfig.owner;
|
|
3455
|
+
if (!owner) throw new UnconfiguredRepoAddressError("repository owner");
|
|
3456
|
+
let commit = commitArg.toLowerCase();
|
|
3457
|
+
if (!FULL_SHA_RE4.test(commit)) {
|
|
3458
|
+
if (repoRoot === void 0) {
|
|
3459
|
+
throw new Error(
|
|
3460
|
+
`<commit> must be a full 40-hex SHA outside a git repository (got ${JSON.stringify(commitArg)})`
|
|
3461
|
+
);
|
|
3462
|
+
}
|
|
3463
|
+
commit = (await runGit(repoRoot, [
|
|
3464
|
+
"rev-parse",
|
|
3465
|
+
"--verify",
|
|
3466
|
+
`${commitArg}^{commit}`
|
|
3467
|
+
])).trim();
|
|
3468
|
+
}
|
|
3469
|
+
const resolved = await resolveRelays({
|
|
3470
|
+
relayFlags: flags.relay,
|
|
3471
|
+
remoteName: flags.remote,
|
|
3472
|
+
repoRoot,
|
|
3473
|
+
toonRelays: toonConfig.relays
|
|
3474
|
+
});
|
|
3475
|
+
const relays = resolved.relays.filter((url) => WS_URL_RE.test(url));
|
|
3476
|
+
if (relays.length === 0) {
|
|
3477
|
+
throw new InvalidRelayUrlError(
|
|
3478
|
+
resolved.relays[0] ?? "",
|
|
3479
|
+
"reads need a ws:// or wss:// relay"
|
|
3480
|
+
);
|
|
3481
|
+
}
|
|
3482
|
+
const webSocketFactory = deps.webSocketFactory ?? defaultWebSocketFactory;
|
|
3483
|
+
const repoAddr = repoAddress(owner, repoId);
|
|
3484
|
+
const events = await queryAll(
|
|
3485
|
+
relays,
|
|
3486
|
+
[
|
|
3487
|
+
{
|
|
3488
|
+
kinds: [
|
|
3489
|
+
CI_WORKFLOW_RESULT_KIND,
|
|
3490
|
+
CI_WORKFLOW_PROGRESS_KIND,
|
|
3491
|
+
CI_JOB_RESULT_KIND
|
|
3492
|
+
],
|
|
3493
|
+
"#a": [repoAddr],
|
|
3494
|
+
"#c": [commit]
|
|
3495
|
+
},
|
|
3496
|
+
{
|
|
3497
|
+
kinds: [CI_SERVICE_REQUEST_KIND, CI_SERVICE_STOP_KIND],
|
|
3498
|
+
"#a": [repoAddr]
|
|
3499
|
+
},
|
|
3500
|
+
{
|
|
3501
|
+
kinds: [REPOSITORY_ANNOUNCEMENT_KIND],
|
|
3502
|
+
authors: [owner],
|
|
3503
|
+
"#d": [repoId]
|
|
3504
|
+
}
|
|
3505
|
+
],
|
|
3506
|
+
webSocketFactory
|
|
3507
|
+
);
|
|
3508
|
+
let announce = null;
|
|
3509
|
+
for (const ev of events) {
|
|
3510
|
+
if (ev.kind !== REPOSITORY_ANNOUNCEMENT_KIND || ev.pubkey.toLowerCase() !== owner)
|
|
3511
|
+
continue;
|
|
3512
|
+
if (!ev.tags.some((t) => t[0] === "d" && t[1] === repoId)) continue;
|
|
3513
|
+
if (announce === null || ev.created_at > announce.created_at || ev.created_at === announce.created_at && ev.id < announce.id) {
|
|
3514
|
+
announce = ev;
|
|
3515
|
+
}
|
|
3516
|
+
}
|
|
3517
|
+
const authorized = authorizedStatusAuthors(owner, announce?.tags ?? []);
|
|
3518
|
+
const assembled = assembleCiStatus({
|
|
3519
|
+
commit,
|
|
3520
|
+
ownerPubkey: owner,
|
|
3521
|
+
repoId,
|
|
3522
|
+
authorized,
|
|
3523
|
+
events,
|
|
3524
|
+
...flags.requireTrust !== void 0 ? { requireTrust: flags.requireTrust } : {},
|
|
3525
|
+
...flags.workflow !== void 0 ? { workflow: flags.workflow } : {}
|
|
3526
|
+
});
|
|
3527
|
+
const report = {
|
|
3528
|
+
command: "ci status",
|
|
3529
|
+
commit,
|
|
3530
|
+
repo: { owner, repoId },
|
|
3531
|
+
relays,
|
|
3532
|
+
requiredTrust: flags.requireTrust ?? null,
|
|
3533
|
+
runs: assembled.runs,
|
|
3534
|
+
summary: assembled.summary,
|
|
3535
|
+
ok: assembled.ok,
|
|
3536
|
+
...assembled.reason !== void 0 ? { reason: assembled.reason } : {}
|
|
3537
|
+
};
|
|
3538
|
+
if (flags.json) {
|
|
3539
|
+
io.emitJson(report);
|
|
3540
|
+
} else {
|
|
3541
|
+
for (const line of renderStatus(report)) io.out(line);
|
|
3542
|
+
}
|
|
3543
|
+
return report.ok ? 0 : 1;
|
|
3544
|
+
} catch (err) {
|
|
3545
|
+
return emitCliError(io, flags.json, "ci status", err);
|
|
3546
|
+
}
|
|
3547
|
+
}
|
|
3548
|
+
function glyph(run) {
|
|
3549
|
+
if (run.status !== "concluded" || run.conclusion === void 0) return "\u2026";
|
|
3550
|
+
return GREEN.has(run.conclusion) ? "\u2713" : "\u2717";
|
|
3551
|
+
}
|
|
3552
|
+
function shortNpub(hex) {
|
|
3553
|
+
try {
|
|
3554
|
+
const npub = hexToNpub(hex);
|
|
3555
|
+
return `${npub.slice(0, 12)}\u2026${npub.slice(-4)}`;
|
|
3556
|
+
} catch {
|
|
3557
|
+
return `${hex.slice(0, 12)}\u2026`;
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3560
|
+
function renderStatus(report) {
|
|
3561
|
+
const lines = [];
|
|
3562
|
+
lines.push(
|
|
3563
|
+
`CI for ${report.commit.slice(0, 12)} in 30617:${report.repo.owner}:${report.repo.repoId}`
|
|
3564
|
+
);
|
|
3565
|
+
if (report.runs.length === 0) {
|
|
3566
|
+
lines.push(" (no runs)");
|
|
3567
|
+
}
|
|
3568
|
+
for (const run of report.runs) {
|
|
3569
|
+
const state = run.status === "concluded" ? run.conclusion ?? "concluded" : run.status;
|
|
3570
|
+
lines.push(
|
|
3571
|
+
`${glyph(run)} ${state.padEnd(15)} ${run.workflow.path} ${run.reason} coordinator ${shortNpub(run.coordinator)} trust ${run.trust}`
|
|
3572
|
+
);
|
|
3573
|
+
for (const job of run.jobs) {
|
|
3574
|
+
const extra = job.exitCode !== void 0 ? ` (exit ${job.exitCode})` : "";
|
|
3575
|
+
lines.push(
|
|
3576
|
+
` ${job.conclusion.padEnd(15)} ${job.name ?? job.jobId}${extra}`
|
|
3577
|
+
);
|
|
3578
|
+
}
|
|
3579
|
+
}
|
|
3580
|
+
const s = report.summary;
|
|
3581
|
+
const verdict = report.ok ? "GREEN" : `NOT GREEN \u2014 ${report.reason ?? "see runs"}`;
|
|
3582
|
+
lines.push(
|
|
3583
|
+
`${verdict} (counted ${s.counted}: ${s.green} green, ${s.red} red, ${s.pending} pending` + (report.requiredTrust ? `; trust \u2265 ${report.requiredTrust}` : "") + ")"
|
|
3584
|
+
);
|
|
3585
|
+
return lines;
|
|
3586
|
+
}
|
|
3587
|
+
|
|
3588
|
+
export {
|
|
3589
|
+
MAX_OBJECT_SIZE,
|
|
3590
|
+
hashGitObject,
|
|
3591
|
+
createGitBlob,
|
|
3592
|
+
createGitTree,
|
|
3593
|
+
createGitCommit,
|
|
3594
|
+
createGitTag,
|
|
3595
|
+
GitError,
|
|
3596
|
+
GitRepoReader,
|
|
3597
|
+
DEFAULT_CONCURRENCY,
|
|
3598
|
+
ObjectIntegrityError,
|
|
3599
|
+
verifyObjectBody,
|
|
3600
|
+
fetchTxBytes,
|
|
3601
|
+
downloadGitObjects,
|
|
3602
|
+
referencedShas,
|
|
3603
|
+
walkClosure,
|
|
3604
|
+
collectRepoObjects,
|
|
3605
|
+
missingObjectsMessage,
|
|
3606
|
+
ObjectWriteMismatchError,
|
|
3607
|
+
isSafeRefname,
|
|
3608
|
+
runGit,
|
|
3609
|
+
writeGitObject,
|
|
3610
|
+
writeGitObjects,
|
|
3611
|
+
updateRef,
|
|
3612
|
+
setHeadSymref,
|
|
3613
|
+
hexToNpub,
|
|
3614
|
+
npubToHex,
|
|
3615
|
+
ownerToHex,
|
|
3616
|
+
serializeFeeEstimate,
|
|
3617
|
+
serializePushPlan,
|
|
3618
|
+
serializeEventReceipt,
|
|
3619
|
+
serializePushResult,
|
|
3620
|
+
NonFastForwardError,
|
|
3621
|
+
OversizeObjectsError,
|
|
3622
|
+
planPush,
|
|
3623
|
+
executePush,
|
|
3624
|
+
CI_ADVERTISEMENT_KIND,
|
|
3625
|
+
CI_SERVICE_REQUEST_KIND,
|
|
3626
|
+
CI_SERVICE_STOP_KIND,
|
|
3627
|
+
CI_SECRET_UPDATE_KIND,
|
|
3628
|
+
CI_MANUAL_TRIGGER_KIND,
|
|
3629
|
+
CI_JOB_RESULT_KIND,
|
|
3630
|
+
CI_WORKFLOW_RESULT_KIND,
|
|
3631
|
+
CI_WORKFLOW_PROGRESS_KIND,
|
|
3632
|
+
CI_RUNNER_FAMILY,
|
|
3633
|
+
CI_SOFTWARE,
|
|
3634
|
+
CI_MAX_ADVERTISEMENT_TTL,
|
|
3635
|
+
CI_MAX_PROGRESS_TTL,
|
|
3636
|
+
isCiConclusion,
|
|
3637
|
+
repoAddress,
|
|
3638
|
+
parseRepoAddress,
|
|
3639
|
+
commonTriggerTags,
|
|
3640
|
+
parseCiTriggerContext,
|
|
3641
|
+
buildCiAdvertisement,
|
|
3642
|
+
parseCiAdvertisement,
|
|
3643
|
+
buildCiServiceRequest,
|
|
3644
|
+
buildCiServiceStop,
|
|
3645
|
+
parseCiServiceControl,
|
|
3646
|
+
controlOrder,
|
|
3647
|
+
selectServiceRequests,
|
|
3648
|
+
buildCiManualTrigger,
|
|
3649
|
+
parseCiManualTrigger,
|
|
3650
|
+
buildCiSecretUpdate,
|
|
3651
|
+
parseCiSecretUpdate,
|
|
3652
|
+
buildCiJobResult,
|
|
3653
|
+
parseCiJobResult,
|
|
3654
|
+
buildCiWorkflowResult,
|
|
3655
|
+
parseCiWorkflowResult,
|
|
3656
|
+
buildCiWorkflowProgress,
|
|
3657
|
+
parseCiWorkflowProgress,
|
|
3658
|
+
SECRET_NAME_RE,
|
|
3659
|
+
RESERVED_SECRET_NAMES,
|
|
3660
|
+
MAX_SECRET_NAMES,
|
|
3661
|
+
MAX_SECRET_VALUE_BYTES,
|
|
3662
|
+
MAX_SECRET_PLAINTEXT_BYTES,
|
|
3663
|
+
MAX_SECRET_CIPHERTEXT_BYTES,
|
|
3664
|
+
assertSecretUpdateShape,
|
|
3665
|
+
validateSecretUpdate,
|
|
3666
|
+
encryptSecretUpdate,
|
|
3667
|
+
decryptSecretUpdate,
|
|
3668
|
+
applySecretUpdate,
|
|
3669
|
+
effectiveSecrets,
|
|
3670
|
+
generateSecretsKey,
|
|
3671
|
+
CI_TRUST_ORDER,
|
|
3672
|
+
isCiTrustLevel,
|
|
3673
|
+
deriveTrustLevel,
|
|
3674
|
+
trustAtLeast,
|
|
3675
|
+
WORKFLOW_DIRS,
|
|
3676
|
+
sha256Hex,
|
|
3677
|
+
parseWorkflow,
|
|
3678
|
+
discoverWorkflows,
|
|
3679
|
+
refMatchesPatterns,
|
|
3680
|
+
matchesPush,
|
|
3681
|
+
matchesPullRequest,
|
|
3682
|
+
resolvePaidSession,
|
|
3683
|
+
UnconfiguredRepoAddressError,
|
|
3684
|
+
InvalidRelayUrlError,
|
|
3685
|
+
UnknownRemoteError,
|
|
3686
|
+
MultiUrlRemoteError,
|
|
3687
|
+
NotAGitRepositoryError,
|
|
3688
|
+
emitCliError,
|
|
3689
|
+
resolveRepoRoot,
|
|
3690
|
+
initGitRepository,
|
|
3691
|
+
readToonConfig,
|
|
3692
|
+
getGitRemoteUrls,
|
|
3693
|
+
listGitRemotes,
|
|
3694
|
+
setGitAuthor,
|
|
3695
|
+
addGitRemote,
|
|
3696
|
+
writeToonConfig,
|
|
3697
|
+
isRelayUrl,
|
|
3698
|
+
resolveRelays,
|
|
3699
|
+
singleRelayRefusal,
|
|
3700
|
+
runRemote,
|
|
3701
|
+
CI_STATUS_USAGE,
|
|
3702
|
+
assembleCiStatus,
|
|
3703
|
+
runCiStatus
|
|
3704
|
+
};
|
|
3705
|
+
//# sourceMappingURL=chunk-3RWM7PBM.js.map
|