@toon-protocol/client-mcp 0.12.1 → 0.13.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 +1 -1
- package/dist/{chunk-HLATGATX.js → chunk-GQENVTED.js} +1525 -10
- package/dist/chunk-GQENVTED.js.map +1 -0
- package/dist/{chunk-GTSWCJOE.js → chunk-SL7UGVOC.js} +293 -3
- package/dist/chunk-SL7UGVOC.js.map +1 -0
- package/dist/{chunk-W6T6ZK7U.js → chunk-UXCFHAUC.js} +88 -7
- package/dist/chunk-UXCFHAUC.js.map +1 -0
- package/dist/daemon.js +3 -3
- package/dist/daemon.js.map +1 -1
- package/dist/index.d.ts +303 -10
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +4 -4
- package/dist/mcp.js.map +1 -1
- package/package.json +5 -4
- package/dist/chunk-GTSWCJOE.js.map +0 -1
- package/dist/chunk-HLATGATX.js.map +0 -1
- package/dist/chunk-W6T6ZK7U.js.map +0 -1
|
@@ -10,12 +10,13 @@ import {
|
|
|
10
10
|
defaultConfigPath,
|
|
11
11
|
deriveFullIdentity,
|
|
12
12
|
encodeEventToToon,
|
|
13
|
+
extractArweaveTxId,
|
|
13
14
|
fundWallet,
|
|
14
15
|
generateKeystore,
|
|
15
16
|
isEventExpired,
|
|
16
17
|
parseIlpPeerInfo,
|
|
17
18
|
readConfigFile
|
|
18
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-UXCFHAUC.js";
|
|
19
20
|
import {
|
|
20
21
|
__require
|
|
21
22
|
} from "./chunk-F22GNSF6.js";
|
|
@@ -355,10 +356,1077 @@ function defaultWebSocketFactory() {
|
|
|
355
356
|
}
|
|
356
357
|
|
|
357
358
|
// src/daemon/client-runner.ts
|
|
358
|
-
import { readFile } from "fs/promises";
|
|
359
|
+
import { readFile, stat } from "fs/promises";
|
|
359
360
|
import { resolve, sep } from "path";
|
|
360
361
|
import { generateSecretKey as generateSecretKey2 } from "nostr-tools/pure";
|
|
361
362
|
|
|
363
|
+
// ../../node_modules/.pnpm/@toon-protocol+core@1.4.2_@toon-protocol+connector@3.13.0_typescript@5.9.3/node_modules/@toon-protocol/core/dist/nip34/index.js
|
|
364
|
+
var REPOSITORY_ANNOUNCEMENT_KIND = 30617;
|
|
365
|
+
var STATUS_OPEN_KIND = 1630;
|
|
366
|
+
var STATUS_APPLIED_KIND = 1631;
|
|
367
|
+
var STATUS_CLOSED_KIND = 1632;
|
|
368
|
+
var STATUS_DRAFT_KIND = 1633;
|
|
369
|
+
|
|
370
|
+
// ../../node_modules/.pnpm/@toon-protocol+core@1.6.0_@toon-protocol+connector@3.13.0_typescript@5.9.3/node_modules/@toon-protocol/core/dist/nip34/index.js
|
|
371
|
+
var REPOSITORY_ANNOUNCEMENT_KIND2 = 30617;
|
|
372
|
+
var PATCH_KIND = 1617;
|
|
373
|
+
var ISSUE_KIND = 1621;
|
|
374
|
+
|
|
375
|
+
// ../git/dist/chunk-KXXHAUXL.js
|
|
376
|
+
var REPOSITORY_STATE_KIND = 30618;
|
|
377
|
+
var COMMENT_KIND = 1622;
|
|
378
|
+
function buildRepoAnnouncement(repoId, name, description) {
|
|
379
|
+
return {
|
|
380
|
+
kind: REPOSITORY_ANNOUNCEMENT_KIND2,
|
|
381
|
+
content: "",
|
|
382
|
+
tags: [
|
|
383
|
+
["d", repoId],
|
|
384
|
+
["name", name],
|
|
385
|
+
["description", description]
|
|
386
|
+
],
|
|
387
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
function buildRepoRefs(repoId, refs, arweaveMap = {}) {
|
|
391
|
+
const tags = [["d", repoId]];
|
|
392
|
+
for (const [refPath, commitSha] of Object.entries(refs)) {
|
|
393
|
+
tags.push(["r", refPath, commitSha]);
|
|
394
|
+
}
|
|
395
|
+
const firstRef = Object.keys(refs)[0];
|
|
396
|
+
if (firstRef) {
|
|
397
|
+
tags.push(["HEAD", `ref: ${firstRef}`]);
|
|
398
|
+
}
|
|
399
|
+
for (const [sha, txId] of Object.entries(arweaveMap)) {
|
|
400
|
+
tags.push(["arweave", sha, txId]);
|
|
401
|
+
}
|
|
402
|
+
return {
|
|
403
|
+
kind: REPOSITORY_STATE_KIND,
|
|
404
|
+
content: "",
|
|
405
|
+
tags,
|
|
406
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
function buildIssue(repoOwnerPubkey, repoId, title, body, labels = []) {
|
|
410
|
+
const tags = [
|
|
411
|
+
["a", `${REPOSITORY_ANNOUNCEMENT_KIND2}:${repoOwnerPubkey}:${repoId}`],
|
|
412
|
+
["p", repoOwnerPubkey],
|
|
413
|
+
["subject", title],
|
|
414
|
+
...labels.map((label) => ["t", label])
|
|
415
|
+
];
|
|
416
|
+
return {
|
|
417
|
+
kind: ISSUE_KIND,
|
|
418
|
+
content: body,
|
|
419
|
+
tags,
|
|
420
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
function buildComment(repoOwnerPubkey, repoId, issueOrPrEventId, authorPubkey, body, marker = "reply") {
|
|
424
|
+
return {
|
|
425
|
+
kind: COMMENT_KIND,
|
|
426
|
+
content: body,
|
|
427
|
+
tags: [
|
|
428
|
+
["a", `${REPOSITORY_ANNOUNCEMENT_KIND2}:${repoOwnerPubkey}:${repoId}`],
|
|
429
|
+
["e", issueOrPrEventId, "", marker],
|
|
430
|
+
["p", authorPubkey]
|
|
431
|
+
],
|
|
432
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
function buildPatch(repoOwnerPubkey, repoId, title, commits, branchTag, content = "") {
|
|
436
|
+
const tags = [
|
|
437
|
+
["a", `${REPOSITORY_ANNOUNCEMENT_KIND2}:${repoOwnerPubkey}:${repoId}`],
|
|
438
|
+
["p", repoOwnerPubkey],
|
|
439
|
+
["subject", title]
|
|
440
|
+
];
|
|
441
|
+
for (const commit of commits) {
|
|
442
|
+
tags.push(["commit", commit.sha]);
|
|
443
|
+
tags.push(["parent-commit", commit.parentSha]);
|
|
444
|
+
}
|
|
445
|
+
if (branchTag) {
|
|
446
|
+
tags.push(["t", branchTag]);
|
|
447
|
+
}
|
|
448
|
+
return {
|
|
449
|
+
kind: PATCH_KIND,
|
|
450
|
+
content,
|
|
451
|
+
tags,
|
|
452
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
function buildStatus(targetEventId, statusKind, targetPubkey) {
|
|
456
|
+
const tags = [["e", targetEventId]];
|
|
457
|
+
if (targetPubkey) {
|
|
458
|
+
tags.push(["p", targetPubkey]);
|
|
459
|
+
}
|
|
460
|
+
return {
|
|
461
|
+
kind: statusKind,
|
|
462
|
+
content: "",
|
|
463
|
+
tags,
|
|
464
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ../git/dist/chunk-M7O4SEVW.js
|
|
469
|
+
var MAX_OBJECT_SIZE = 95 * 1024;
|
|
470
|
+
|
|
471
|
+
// ../git/dist/chunk-4WFGAICZ.js
|
|
472
|
+
import { execFile, spawn } from "child_process";
|
|
473
|
+
import { promisify } from "util";
|
|
474
|
+
var execFileAsync = promisify(execFile);
|
|
475
|
+
var MAX_BUFFER = 256 * 1024 * 1024;
|
|
476
|
+
var GitError = class extends Error {
|
|
477
|
+
constructor(message, exitCode, stderr) {
|
|
478
|
+
super(message);
|
|
479
|
+
this.exitCode = exitCode;
|
|
480
|
+
this.stderr = stderr;
|
|
481
|
+
this.name = "GitError";
|
|
482
|
+
}
|
|
483
|
+
exitCode;
|
|
484
|
+
stderr;
|
|
485
|
+
};
|
|
486
|
+
var FULL_SHA_RE = /^[0-9a-f]{40}$/;
|
|
487
|
+
var REV_TOKEN_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]*(?:[~^][0-9]*)*$/;
|
|
488
|
+
function isValidRevision(rev) {
|
|
489
|
+
if (rev.length === 0 || rev.length > 1024) return false;
|
|
490
|
+
if (!REV_TOKEN_RE.test(rev)) return false;
|
|
491
|
+
if (rev.includes("..")) return false;
|
|
492
|
+
if (rev.endsWith(".lock") || rev.endsWith("/") || rev.endsWith(".")) return false;
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
function assertRevision(rev, what) {
|
|
496
|
+
if (!isValidRevision(rev)) {
|
|
497
|
+
throw new Error(
|
|
498
|
+
`${what} is not a valid git revision (got ${JSON.stringify(rev)}); expected a SHA or simple refname \u2014 options/ranges are rejected`
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
function assertFullSha(sha, what) {
|
|
503
|
+
if (!FULL_SHA_RE.test(sha)) {
|
|
504
|
+
throw new Error(
|
|
505
|
+
`${what} is not a full 40-hex SHA-1 (got ${JSON.stringify(sha)})`
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
function assertRange(range, what) {
|
|
510
|
+
const parts = range.split(/\.{2,3}/);
|
|
511
|
+
const separators = range.match(/\.{2,3}/g) ?? [];
|
|
512
|
+
const ok = parts.length <= 2 && separators.length === parts.length - 1 && parts.every((p) => isValidRevision(p));
|
|
513
|
+
if (!ok) {
|
|
514
|
+
throw new Error(
|
|
515
|
+
`${what} is not a valid revision range (got ${JSON.stringify(range)}); expected <rev>, <rev>..<rev>, or <rev>...<rev>`
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
var OBJECT_TYPES = /* @__PURE__ */ new Set(["blob", "tree", "commit", "tag"]);
|
|
520
|
+
var BatchParser = class {
|
|
521
|
+
buf = Buffer.alloc(0);
|
|
522
|
+
pending = null;
|
|
523
|
+
objects = [];
|
|
524
|
+
missing = [];
|
|
525
|
+
push(chunk) {
|
|
526
|
+
this.buf = this.buf.length === 0 ? chunk : Buffer.concat([this.buf, chunk]);
|
|
527
|
+
this.drain();
|
|
528
|
+
}
|
|
529
|
+
/** True when no partially-parsed record remains. */
|
|
530
|
+
isComplete() {
|
|
531
|
+
return this.pending === null && this.buf.length === 0;
|
|
532
|
+
}
|
|
533
|
+
drain() {
|
|
534
|
+
for (; ; ) {
|
|
535
|
+
if (this.pending) {
|
|
536
|
+
const needed = this.pending.size + 1;
|
|
537
|
+
if (this.buf.length < needed) return;
|
|
538
|
+
const body = Buffer.from(this.buf.subarray(0, this.pending.size));
|
|
539
|
+
this.objects.push({ sha: this.pending.sha, type: this.pending.type, body });
|
|
540
|
+
this.buf = this.buf.subarray(needed);
|
|
541
|
+
this.pending = null;
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
const nl = this.buf.indexOf(10);
|
|
545
|
+
if (nl === -1) return;
|
|
546
|
+
const header = this.buf.subarray(0, nl).toString("utf-8");
|
|
547
|
+
this.buf = this.buf.subarray(nl + 1);
|
|
548
|
+
const [name, second, third] = header.split(" ");
|
|
549
|
+
if (name && second === "missing" && third === void 0) {
|
|
550
|
+
this.missing.push(name);
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
if (name && second && third !== void 0 && OBJECT_TYPES.has(second)) {
|
|
554
|
+
const size = Number.parseInt(third, 10);
|
|
555
|
+
if (Number.isSafeInteger(size) && size >= 0) {
|
|
556
|
+
this.pending = { sha: name, type: second, size };
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
throw new GitError(
|
|
561
|
+
`unexpected cat-file --batch header: ${JSON.stringify(header)}`,
|
|
562
|
+
void 0,
|
|
563
|
+
""
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
var GitRepoReader = class {
|
|
569
|
+
constructor(repoPath) {
|
|
570
|
+
this.repoPath = repoPath;
|
|
571
|
+
}
|
|
572
|
+
repoPath;
|
|
573
|
+
/** Run git with argument-array safety; resolves stdout as UTF-8. */
|
|
574
|
+
async git(args, opts = {}) {
|
|
575
|
+
try {
|
|
576
|
+
const { stdout } = await execFileAsync("git", args, {
|
|
577
|
+
cwd: this.repoPath,
|
|
578
|
+
maxBuffer: MAX_BUFFER,
|
|
579
|
+
encoding: "utf-8"
|
|
580
|
+
});
|
|
581
|
+
return { stdout, exitCode: 0 };
|
|
582
|
+
} catch (err) {
|
|
583
|
+
const e = err;
|
|
584
|
+
const exitCode = typeof e.code === "number" ? e.code : void 0;
|
|
585
|
+
if (exitCode !== void 0 && opts.allowExitCodes?.includes(exitCode)) {
|
|
586
|
+
return { stdout: e.stdout ?? "", exitCode };
|
|
587
|
+
}
|
|
588
|
+
throw new GitError(
|
|
589
|
+
`git ${args[0]} failed${exitCode !== void 0 ? ` (exit ${exitCode})` : ""}: ${(e.stderr ?? e.message ?? "").trim()}`,
|
|
590
|
+
exitCode,
|
|
591
|
+
(e.stderr ?? "").trim()
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* List all branches and tags plus the symbolic HEAD.
|
|
597
|
+
*
|
|
598
|
+
* Annotated tags report the tag object's SHA/type with the peeled target
|
|
599
|
+
* in `peeledSha`. A detached HEAD is tolerated (`head` is `undefined`).
|
|
600
|
+
*/
|
|
601
|
+
async listRefs() {
|
|
602
|
+
const format = "%(refname)%00%(objectname)%00%(objecttype)%00%(*objectname)";
|
|
603
|
+
const [refsRes, headRes] = await Promise.all([
|
|
604
|
+
this.git(["for-each-ref", `--format=${format}`, "refs/heads", "refs/tags"]),
|
|
605
|
+
// Exit 1 = detached HEAD (or unborn branch pointer oddities) — tolerated.
|
|
606
|
+
this.git(["symbolic-ref", "--quiet", "HEAD"], { allowExitCodes: [1] })
|
|
607
|
+
]);
|
|
608
|
+
const refs = [];
|
|
609
|
+
for (const line of refsRes.stdout.split("\n")) {
|
|
610
|
+
if (!line) continue;
|
|
611
|
+
const [refname, sha, objecttype, peeled] = line.split("\0");
|
|
612
|
+
if (!refname || !sha || !objecttype || !OBJECT_TYPES.has(objecttype)) {
|
|
613
|
+
throw new GitError(`unexpected for-each-ref line: ${JSON.stringify(line)}`, void 0, "");
|
|
614
|
+
}
|
|
615
|
+
refs.push({
|
|
616
|
+
refname,
|
|
617
|
+
sha,
|
|
618
|
+
type: objecttype,
|
|
619
|
+
...peeled ? { peeledSha: peeled } : {}
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
const head = headRes.exitCode === 0 ? headRes.stdout.trim() || void 0 : void 0;
|
|
623
|
+
return { head, refs };
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* SHAs of every object reachable from `want` but not from `have`
|
|
627
|
+
* (`git rev-list --objects <want…> --not <have…>`), i.e. the push delta.
|
|
628
|
+
*
|
|
629
|
+
* Haves that don't exist locally (e.g. remote tips we never fetched) are
|
|
630
|
+
* filtered out first via one `cat-file --batch-check` pass — rev-list
|
|
631
|
+
* would otherwise die on them.
|
|
632
|
+
*/
|
|
633
|
+
async objectsBetween(want, have) {
|
|
634
|
+
const objects = await this.objectsBetweenWithPaths(want, have);
|
|
635
|
+
return objects.map((o) => o.sha);
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Like {@link objectsBetween} but keeps the path each object was reached
|
|
639
|
+
* by (`rev-list --objects` emits `<sha> <path>` for blobs and non-root
|
|
640
|
+
* trees) — used by push planning to report actionable oversize errors.
|
|
641
|
+
*/
|
|
642
|
+
async objectsBetweenWithPaths(want, have) {
|
|
643
|
+
for (const w of want) assertRevision(w, "want");
|
|
644
|
+
for (const h of have) assertRevision(h, "have");
|
|
645
|
+
if (want.length === 0) return [];
|
|
646
|
+
const knownHaves = await this.filterExisting(have);
|
|
647
|
+
const args = ["rev-list", "--objects", ...want];
|
|
648
|
+
if (knownHaves.length > 0) args.push("--not", ...knownHaves);
|
|
649
|
+
args.push("--");
|
|
650
|
+
const { stdout } = await this.git(args);
|
|
651
|
+
const objects = [];
|
|
652
|
+
for (const line of stdout.split("\n")) {
|
|
653
|
+
if (!line) continue;
|
|
654
|
+
const spaceIdx = line.indexOf(" ");
|
|
655
|
+
if (spaceIdx === -1) {
|
|
656
|
+
objects.push({ sha: line });
|
|
657
|
+
} else {
|
|
658
|
+
const path = line.slice(spaceIdx + 1);
|
|
659
|
+
objects.push({
|
|
660
|
+
sha: line.slice(0, spaceIdx),
|
|
661
|
+
...path ? { path } : {}
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return objects;
|
|
666
|
+
}
|
|
667
|
+
/** Run git feeding `input` on stdin; resolves collected stdout bytes. */
|
|
668
|
+
runWithStdin(args, input) {
|
|
669
|
+
return new Promise((resolve2, reject) => {
|
|
670
|
+
const child = spawn("git", args, {
|
|
671
|
+
cwd: this.repoPath,
|
|
672
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
673
|
+
});
|
|
674
|
+
const out = [];
|
|
675
|
+
let stderr = "";
|
|
676
|
+
child.stdout.on("data", (chunk) => out.push(chunk));
|
|
677
|
+
child.stderr.on("data", (chunk) => {
|
|
678
|
+
stderr += chunk.toString("utf-8");
|
|
679
|
+
});
|
|
680
|
+
child.on("error", (err) => {
|
|
681
|
+
reject(new GitError(`failed to spawn git ${args[0]}: ${err.message}`, void 0, ""));
|
|
682
|
+
});
|
|
683
|
+
child.on("close", (code) => {
|
|
684
|
+
if (code !== 0) {
|
|
685
|
+
return reject(
|
|
686
|
+
new GitError(`git ${args[0]} failed (exit ${code}): ${stderr.trim()}`, code ?? void 0, stderr.trim())
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
resolve2(Buffer.concat(out));
|
|
690
|
+
});
|
|
691
|
+
child.stdin.on("error", () => {
|
|
692
|
+
});
|
|
693
|
+
child.stdin.write(input);
|
|
694
|
+
child.stdin.end();
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
/** Of the given revisions, keep only those resolvable locally. */
|
|
698
|
+
async filterExisting(revs) {
|
|
699
|
+
if (revs.length === 0) return [];
|
|
700
|
+
const { missing } = await this.batchCheck(revs);
|
|
701
|
+
const missingSet = new Set(missing);
|
|
702
|
+
return revs.filter((r) => !missingSet.has(r));
|
|
703
|
+
}
|
|
704
|
+
/** One `cat-file --batch-check` pass; returns names reported missing. */
|
|
705
|
+
async batchCheck(names) {
|
|
706
|
+
const stdout = await this.runWithStdin(
|
|
707
|
+
["cat-file", "--batch-check"],
|
|
708
|
+
names.join("\n") + "\n"
|
|
709
|
+
);
|
|
710
|
+
const missing = [];
|
|
711
|
+
for (const line of stdout.toString("utf-8").split("\n")) {
|
|
712
|
+
if (line.endsWith(" missing")) missing.push(line.slice(0, -" missing".length));
|
|
713
|
+
}
|
|
714
|
+
return { missing };
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Object metadata (type + body size) for a batch of SHAs via one
|
|
718
|
+
* `cat-file --batch-check` pass — no bodies are read. Missing objects are
|
|
719
|
+
* reported, not thrown.
|
|
720
|
+
*/
|
|
721
|
+
async statObjects(shas) {
|
|
722
|
+
for (const sha of shas) assertFullSha(sha, "sha");
|
|
723
|
+
if (shas.length === 0) return { objects: [], missing: [] };
|
|
724
|
+
const stdout = await this.runWithStdin(
|
|
725
|
+
["cat-file", "--batch-check"],
|
|
726
|
+
shas.join("\n") + "\n"
|
|
727
|
+
);
|
|
728
|
+
const objects = [];
|
|
729
|
+
const missing = [];
|
|
730
|
+
for (const line of stdout.toString("utf-8").split("\n")) {
|
|
731
|
+
if (!line) continue;
|
|
732
|
+
if (line.endsWith(" missing")) {
|
|
733
|
+
missing.push(line.slice(0, -" missing".length));
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
const [sha, type, sizeStr] = line.split(" ");
|
|
737
|
+
const size = Number.parseInt(sizeStr ?? "", 10);
|
|
738
|
+
if (!sha || !type || !OBJECT_TYPES.has(type) || !Number.isSafeInteger(size) || size < 0) {
|
|
739
|
+
throw new GitError(
|
|
740
|
+
`unexpected cat-file --batch-check line: ${JSON.stringify(line)}`,
|
|
741
|
+
void 0,
|
|
742
|
+
""
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
objects.push({ sha, type, size });
|
|
746
|
+
}
|
|
747
|
+
return { objects, missing };
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Read raw object bodies via a single streaming `git cat-file --batch`
|
|
751
|
+
* child process. Bodies may be binary and are parsed size-driven across
|
|
752
|
+
* chunk boundaries. Missing objects are reported, not thrown.
|
|
753
|
+
*/
|
|
754
|
+
async readObjects(shas) {
|
|
755
|
+
for (const sha of shas) assertFullSha(sha, "sha");
|
|
756
|
+
if (shas.length === 0) return { objects: [], missing: [] };
|
|
757
|
+
const parser = new BatchParser();
|
|
758
|
+
await new Promise((resolve2, reject) => {
|
|
759
|
+
const child = spawn("git", ["cat-file", "--batch"], {
|
|
760
|
+
cwd: this.repoPath,
|
|
761
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
762
|
+
});
|
|
763
|
+
let stderr = "";
|
|
764
|
+
let parseError = null;
|
|
765
|
+
child.stderr.on("data", (chunk) => {
|
|
766
|
+
stderr += chunk.toString("utf-8");
|
|
767
|
+
});
|
|
768
|
+
child.stdout.on("data", (chunk) => {
|
|
769
|
+
if (parseError) return;
|
|
770
|
+
try {
|
|
771
|
+
parser.push(chunk);
|
|
772
|
+
} catch (err) {
|
|
773
|
+
parseError = err;
|
|
774
|
+
child.kill();
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
child.on("error", (err) => {
|
|
778
|
+
reject(new GitError(`failed to spawn git cat-file: ${err.message}`, void 0, ""));
|
|
779
|
+
});
|
|
780
|
+
child.on("close", (code) => {
|
|
781
|
+
if (parseError) return reject(parseError);
|
|
782
|
+
if (code !== 0) {
|
|
783
|
+
return reject(
|
|
784
|
+
new GitError(`git cat-file --batch failed (exit ${code}): ${stderr.trim()}`, code ?? void 0, stderr.trim())
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
if (!parser.isComplete()) {
|
|
788
|
+
return reject(
|
|
789
|
+
new GitError("git cat-file --batch output ended mid-record", code ?? void 0, stderr.trim())
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
resolve2();
|
|
793
|
+
});
|
|
794
|
+
child.stdin.on("error", () => {
|
|
795
|
+
});
|
|
796
|
+
child.stdin.write(shas.join("\n") + "\n");
|
|
797
|
+
child.stdin.end();
|
|
798
|
+
});
|
|
799
|
+
return { objects: parser.objects, missing: parser.missing };
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* `git merge-base --is-ancestor <a> <b>` — true when `a` is an ancestor
|
|
803
|
+
* of `b` (fast-forward check / force detection). Exit codes other than
|
|
804
|
+
* 0/1 (e.g. unknown revisions) throw.
|
|
805
|
+
*/
|
|
806
|
+
async isAncestor(a, b) {
|
|
807
|
+
assertRevision(a, "ancestor candidate");
|
|
808
|
+
assertRevision(b, "descendant candidate");
|
|
809
|
+
const { exitCode } = await this.git(
|
|
810
|
+
["merge-base", "--is-ancestor", a, b],
|
|
811
|
+
{ allowExitCodes: [1] }
|
|
812
|
+
);
|
|
813
|
+
return exitCode === 0;
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* `git format-patch --stdout <range>` — the full mbox-formatted patch
|
|
817
|
+
* series text (empty string when the range selects no commits).
|
|
818
|
+
*/
|
|
819
|
+
async formatPatch(range) {
|
|
820
|
+
assertRange(range, "range");
|
|
821
|
+
const { stdout } = await this.git(["format-patch", "--stdout", range, "--"]);
|
|
822
|
+
return stdout;
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Parent SHAs for a batch of commit SHAs via one
|
|
826
|
+
* `git rev-list --no-walk=unsorted --parents` pass. Root commits map to an
|
|
827
|
+
* empty array. Used to derive the kind:1617 `commit`/`parent-commit` tag
|
|
828
|
+
* pairs for exactly the commits a format-patch series carries.
|
|
829
|
+
*/
|
|
830
|
+
async commitParents(shas) {
|
|
831
|
+
for (const sha of shas) assertFullSha(sha, "sha");
|
|
832
|
+
if (shas.length === 0) return /* @__PURE__ */ new Map();
|
|
833
|
+
const { stdout } = await this.git([
|
|
834
|
+
"rev-list",
|
|
835
|
+
"--no-walk=unsorted",
|
|
836
|
+
"--parents",
|
|
837
|
+
...shas,
|
|
838
|
+
"--"
|
|
839
|
+
]);
|
|
840
|
+
const parents = /* @__PURE__ */ new Map();
|
|
841
|
+
for (const line of stdout.split("\n")) {
|
|
842
|
+
if (!line) continue;
|
|
843
|
+
const [sha, ...rest] = line.split(" ");
|
|
844
|
+
if (!sha || !FULL_SHA_RE.test(sha) || rest.some((p) => !FULL_SHA_RE.test(p))) {
|
|
845
|
+
throw new GitError(
|
|
846
|
+
`unexpected rev-list --parents line: ${JSON.stringify(line)}`,
|
|
847
|
+
void 0,
|
|
848
|
+
""
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
parents.set(sha, rest);
|
|
852
|
+
}
|
|
853
|
+
return parents;
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Resolve a ref/revision to a full SHA via `git rev-parse --verify`.
|
|
857
|
+
* Throws {@link GitError} when the name doesn't resolve.
|
|
858
|
+
*/
|
|
859
|
+
async resolveRef(name) {
|
|
860
|
+
assertRevision(name, "ref name");
|
|
861
|
+
const { stdout } = await this.git(["rev-parse", "--verify", "--quiet", name]);
|
|
862
|
+
const sha = stdout.trim();
|
|
863
|
+
if (!FULL_SHA_RE.test(sha)) {
|
|
864
|
+
throw new GitError(
|
|
865
|
+
`rev-parse --verify returned unexpected output for ${JSON.stringify(name)}: ${JSON.stringify(sha)}`,
|
|
866
|
+
void 0,
|
|
867
|
+
""
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
return sha;
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
var NonFastForwardError = class extends Error {
|
|
874
|
+
constructor(refs) {
|
|
875
|
+
super(
|
|
876
|
+
`non-fast-forward update rejected for ${refs.map((r) => r.refname).join(", ")} \u2014 re-run with force to overwrite the remote ref(s)`
|
|
877
|
+
);
|
|
878
|
+
this.refs = refs;
|
|
879
|
+
this.name = "NonFastForwardError";
|
|
880
|
+
}
|
|
881
|
+
refs;
|
|
882
|
+
};
|
|
883
|
+
var OversizeObjectsError = class extends Error {
|
|
884
|
+
constructor(objects) {
|
|
885
|
+
super(
|
|
886
|
+
`${objects.length} object(s) exceed the ${MAX_OBJECT_SIZE} byte upload limit: ` + objects.map((o) => `${o.path ?? o.sha} (${o.size} bytes)`).join(", ")
|
|
887
|
+
);
|
|
888
|
+
this.objects = objects;
|
|
889
|
+
this.name = "OversizeObjectsError";
|
|
890
|
+
}
|
|
891
|
+
objects;
|
|
892
|
+
};
|
|
893
|
+
async function planPush(options) {
|
|
894
|
+
const { repoReader, remoteState, feeRates, repoId, force = false } = options;
|
|
895
|
+
const resolveMissing = options.resolveMissing ?? remoteState.resolveMissing.bind(remoteState);
|
|
896
|
+
const { head, refs: localRefs } = await repoReader.listRefs();
|
|
897
|
+
const localByName = new Map(localRefs.map((r) => [r.refname, r]));
|
|
898
|
+
let selected;
|
|
899
|
+
if (options.refs !== void 0) {
|
|
900
|
+
selected = options.refs.map((name) => {
|
|
901
|
+
const ref = localByName.get(name);
|
|
902
|
+
if (!ref) {
|
|
903
|
+
throw new Error(
|
|
904
|
+
`ref ${JSON.stringify(name)} does not exist locally (ref deletion is out of scope in v1)`
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
return ref;
|
|
908
|
+
});
|
|
909
|
+
} else {
|
|
910
|
+
selected = localRefs;
|
|
911
|
+
}
|
|
912
|
+
const refUpdates = [];
|
|
913
|
+
const rejected = [];
|
|
914
|
+
for (const ref of selected) {
|
|
915
|
+
const remoteSha = remoteState.refs.get(ref.refname) ?? null;
|
|
916
|
+
if (remoteSha === null) {
|
|
917
|
+
refUpdates.push({ refname: ref.refname, localSha: ref.sha, remoteSha, kind: "new" });
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
if (remoteSha === ref.sha) {
|
|
921
|
+
refUpdates.push({ refname: ref.refname, localSha: ref.sha, remoteSha, kind: "up-to-date" });
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
let fastForward = false;
|
|
925
|
+
try {
|
|
926
|
+
fastForward = await repoReader.isAncestor(remoteSha, ref.sha);
|
|
927
|
+
} catch (err) {
|
|
928
|
+
if (!(err instanceof GitError)) throw err;
|
|
929
|
+
fastForward = false;
|
|
930
|
+
}
|
|
931
|
+
if (fastForward) {
|
|
932
|
+
refUpdates.push({ refname: ref.refname, localSha: ref.sha, remoteSha, kind: "fast-forward" });
|
|
933
|
+
} else if (force) {
|
|
934
|
+
refUpdates.push({ refname: ref.refname, localSha: ref.sha, remoteSha, kind: "forced" });
|
|
935
|
+
} else {
|
|
936
|
+
rejected.push({ refname: ref.refname, localSha: ref.sha, remoteSha });
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
if (rejected.length > 0) throw new NonFastForwardError(rejected);
|
|
940
|
+
const updates = refUpdates.filter((u) => u.kind !== "up-to-date");
|
|
941
|
+
const wantTips = [...new Set(updates.map((u) => u.localSha))];
|
|
942
|
+
const haveTips = [...new Set(remoteState.refs.values())];
|
|
943
|
+
const delta = wantTips.length > 0 ? await repoReader.objectsBetweenWithPaths(wantTips, haveTips) : [];
|
|
944
|
+
const knownShaToTxId = new Map(remoteState.shaToTxId);
|
|
945
|
+
let candidates = delta.filter((o) => !knownShaToTxId.has(o.sha));
|
|
946
|
+
if (candidates.length > 0) {
|
|
947
|
+
const resolved = await resolveMissing(candidates.map((o) => o.sha));
|
|
948
|
+
for (const [sha, txId] of resolved) knownShaToTxId.set(sha, txId);
|
|
949
|
+
candidates = candidates.filter((o) => !knownShaToTxId.has(o.sha));
|
|
950
|
+
}
|
|
951
|
+
const pathBySha = new Map(candidates.map((c) => [c.sha, c.path]));
|
|
952
|
+
const { objects: stats, missing } = await repoReader.statObjects(
|
|
953
|
+
candidates.map((c) => c.sha)
|
|
954
|
+
);
|
|
955
|
+
if (missing.length > 0) {
|
|
956
|
+
throw new Error(
|
|
957
|
+
`objects vanished from the local repository during planning: ${missing.join(", ")}`
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
const oversize = [];
|
|
961
|
+
for (const stat2 of stats) {
|
|
962
|
+
if (stat2.size > MAX_OBJECT_SIZE) {
|
|
963
|
+
const path = pathBySha.get(stat2.sha);
|
|
964
|
+
oversize.push({ ...stat2, ...path ? { path } : {} });
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
if (oversize.length > 0) throw new OversizeObjectsError(oversize);
|
|
968
|
+
const tipShas = new Set(updates.map((u) => u.localSha));
|
|
969
|
+
const planned = stats.map((stat2) => {
|
|
970
|
+
const path = pathBySha.get(stat2.sha);
|
|
971
|
+
return { ...stat2, ...path ? { path } : {}, isRefTip: tipShas.has(stat2.sha) };
|
|
972
|
+
});
|
|
973
|
+
const objects = [
|
|
974
|
+
...planned.filter((o) => !o.isRefTip),
|
|
975
|
+
...planned.filter((o) => o.isRefTip)
|
|
976
|
+
];
|
|
977
|
+
const newRefsMap = new Map(remoteState.refs);
|
|
978
|
+
for (const update of updates) newRefsMap.set(update.refname, update.localSha);
|
|
979
|
+
const headSymref = head && newRefsMap.has(head) ? head : remoteState.headSymref && newRefsMap.has(remoteState.headSymref) ? remoteState.headSymref : [...newRefsMap.keys()][0] ?? null;
|
|
980
|
+
const newRefs = {};
|
|
981
|
+
const headSha = headSymref ? newRefsMap.get(headSymref) : void 0;
|
|
982
|
+
if (headSymref && headSha) newRefs[headSymref] = headSha;
|
|
983
|
+
for (const [refname, sha] of newRefsMap) {
|
|
984
|
+
if (refname !== headSymref) newRefs[refname] = sha;
|
|
985
|
+
}
|
|
986
|
+
const announceNeeded = !remoteState.announced;
|
|
987
|
+
const totalObjectBytes = objects.reduce((sum, o) => sum + o.size, 0);
|
|
988
|
+
const uploadFee = BigInt(totalObjectBytes) * feeRates.uploadFeePerByte;
|
|
989
|
+
const eventCount = 1 + (announceNeeded ? 1 : 0);
|
|
990
|
+
const eventFees = BigInt(eventCount) * feeRates.eventFee;
|
|
991
|
+
return {
|
|
992
|
+
repoId,
|
|
993
|
+
refUpdates,
|
|
994
|
+
newRefs,
|
|
995
|
+
headSymref,
|
|
996
|
+
objects,
|
|
997
|
+
knownShaToTxId,
|
|
998
|
+
announceNeeded,
|
|
999
|
+
announcement: {
|
|
1000
|
+
name: options.announcement?.name ?? repoId,
|
|
1001
|
+
description: options.announcement?.description ?? ""
|
|
1002
|
+
},
|
|
1003
|
+
estimate: {
|
|
1004
|
+
objectCount: objects.length,
|
|
1005
|
+
totalObjectBytes,
|
|
1006
|
+
uploadFee,
|
|
1007
|
+
eventCount,
|
|
1008
|
+
eventFees,
|
|
1009
|
+
totalFee: uploadFee + eventFees
|
|
1010
|
+
}
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
var READ_BATCH_SIZE = 100;
|
|
1014
|
+
async function executePush(options) {
|
|
1015
|
+
const { plan, publisher, remoteState, repoReader, relayUrls } = options;
|
|
1016
|
+
const merged = new Map([...remoteState.shaToTxId, ...plan.knownShaToTxId]);
|
|
1017
|
+
const resultBySha = /* @__PURE__ */ new Map();
|
|
1018
|
+
let totalFeePaid = 0n;
|
|
1019
|
+
const pending = [];
|
|
1020
|
+
for (const object of plan.objects) {
|
|
1021
|
+
const knownTxId = merged.get(object.sha);
|
|
1022
|
+
if (knownTxId !== void 0) {
|
|
1023
|
+
resultBySha.set(object.sha, {
|
|
1024
|
+
sha: object.sha,
|
|
1025
|
+
txId: knownTxId,
|
|
1026
|
+
feePaid: 0n,
|
|
1027
|
+
skipped: true
|
|
1028
|
+
});
|
|
1029
|
+
} else {
|
|
1030
|
+
pending.push(object);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
for (let i = 0; i < pending.length; i += READ_BATCH_SIZE) {
|
|
1034
|
+
const batch = pending.slice(i, i + READ_BATCH_SIZE);
|
|
1035
|
+
const { objects: read, missing } = await repoReader.readObjects(
|
|
1036
|
+
batch.map((o) => o.sha)
|
|
1037
|
+
);
|
|
1038
|
+
if (missing.length > 0) {
|
|
1039
|
+
throw new Error(
|
|
1040
|
+
`objects vanished from the local repository during push: ${missing.join(", ")}`
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
1043
|
+
const bodyBySha = new Map(read.map((r) => [r.sha, r.body]));
|
|
1044
|
+
for (const object of batch) {
|
|
1045
|
+
const body = bodyBySha.get(object.sha);
|
|
1046
|
+
if (!body) {
|
|
1047
|
+
throw new Error(
|
|
1048
|
+
`internal: cat-file returned no body for ${object.sha}`
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
const receipt = await publisher.uploadGitObject({
|
|
1052
|
+
sha: object.sha,
|
|
1053
|
+
type: object.type,
|
|
1054
|
+
body,
|
|
1055
|
+
repoId: plan.repoId
|
|
1056
|
+
});
|
|
1057
|
+
merged.set(object.sha, receipt.txId);
|
|
1058
|
+
totalFeePaid += receipt.feePaid;
|
|
1059
|
+
resultBySha.set(object.sha, {
|
|
1060
|
+
sha: object.sha,
|
|
1061
|
+
txId: receipt.txId,
|
|
1062
|
+
feePaid: receipt.feePaid,
|
|
1063
|
+
skipped: false
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
let announceReceipt = null;
|
|
1068
|
+
if (plan.announceNeeded && !remoteState.announced) {
|
|
1069
|
+
const announceEvent = buildRepoAnnouncement(
|
|
1070
|
+
plan.repoId,
|
|
1071
|
+
plan.announcement.name,
|
|
1072
|
+
plan.announcement.description
|
|
1073
|
+
);
|
|
1074
|
+
announceReceipt = await publisher.publishEvent(announceEvent, relayUrls);
|
|
1075
|
+
totalFeePaid += announceReceipt.feePaid;
|
|
1076
|
+
}
|
|
1077
|
+
const refsEvent = buildRepoRefs(
|
|
1078
|
+
plan.repoId,
|
|
1079
|
+
plan.newRefs,
|
|
1080
|
+
Object.fromEntries(merged)
|
|
1081
|
+
);
|
|
1082
|
+
const refsReceipt = await publisher.publishEvent(refsEvent, relayUrls);
|
|
1083
|
+
totalFeePaid += refsReceipt.feePaid;
|
|
1084
|
+
const uploads = plan.objects.map((o) => {
|
|
1085
|
+
const step = resultBySha.get(o.sha);
|
|
1086
|
+
if (!step) {
|
|
1087
|
+
throw new Error(`internal: no upload result recorded for ${o.sha}`);
|
|
1088
|
+
}
|
|
1089
|
+
return step;
|
|
1090
|
+
});
|
|
1091
|
+
return {
|
|
1092
|
+
uploads,
|
|
1093
|
+
announceReceipt,
|
|
1094
|
+
refsReceipt,
|
|
1095
|
+
arweaveMap: merged,
|
|
1096
|
+
totalFeePaid
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// ../git/dist/chunk-R3JVS6SX.js
|
|
1101
|
+
import { decode as decodeToon } from "@toon-format/toon";
|
|
1102
|
+
var ARWEAVE_FETCH_TIMEOUT_MS = 15e3;
|
|
1103
|
+
var ARWEAVE_GRAPHQL_URL = "https://arweave.net/graphql";
|
|
1104
|
+
var SHA_CACHE_MAX_SIZE = 1e4;
|
|
1105
|
+
var shaToTxIdCache = /* @__PURE__ */ new Map();
|
|
1106
|
+
function isValidGitSha(sha) {
|
|
1107
|
+
return /^[0-9a-f]{40}$/i.test(sha);
|
|
1108
|
+
}
|
|
1109
|
+
function sanitizeGraphQLValue(value) {
|
|
1110
|
+
return value.replace(/["\\\n\r\u0000-\u001f`]/g, "");
|
|
1111
|
+
}
|
|
1112
|
+
function shaCacheKey(sha, repo) {
|
|
1113
|
+
return `${sha}:${repo}`;
|
|
1114
|
+
}
|
|
1115
|
+
function seedShaCache(mappings) {
|
|
1116
|
+
const entries = mappings instanceof Map ? mappings.entries() : mappings;
|
|
1117
|
+
for (const [key2, txId] of entries) {
|
|
1118
|
+
if (shaToTxIdCache.size >= SHA_CACHE_MAX_SIZE) {
|
|
1119
|
+
const firstKey = shaToTxIdCache.keys().next().value;
|
|
1120
|
+
if (firstKey !== void 0) {
|
|
1121
|
+
shaToTxIdCache.delete(firstKey);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
shaToTxIdCache.set(key2, txId);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
var ARWEAVE_TX_ID_RE = /^[a-zA-Z0-9_-]{43}$/;
|
|
1128
|
+
function isValidArweaveTxId(txId) {
|
|
1129
|
+
return ARWEAVE_TX_ID_RE.test(txId);
|
|
1130
|
+
}
|
|
1131
|
+
async function resolveGitSha(sha, repo) {
|
|
1132
|
+
if (!isValidGitSha(sha)) {
|
|
1133
|
+
return null;
|
|
1134
|
+
}
|
|
1135
|
+
const cacheKey = shaCacheKey(sha, repo);
|
|
1136
|
+
const cached = shaToTxIdCache.get(cacheKey);
|
|
1137
|
+
if (cached !== void 0) {
|
|
1138
|
+
return cached;
|
|
1139
|
+
}
|
|
1140
|
+
const safeSha = sanitizeGraphQLValue(sha);
|
|
1141
|
+
const safeRepo = sanitizeGraphQLValue(repo);
|
|
1142
|
+
const query = `query {
|
|
1143
|
+
transactions(tags: [
|
|
1144
|
+
{ name: "Git-SHA", values: ["${safeSha}"] },
|
|
1145
|
+
{ name: "Repo", values: ["${safeRepo}"] }
|
|
1146
|
+
]) {
|
|
1147
|
+
edges { node { id } }
|
|
1148
|
+
}
|
|
1149
|
+
}`;
|
|
1150
|
+
try {
|
|
1151
|
+
const response = await fetch(ARWEAVE_GRAPHQL_URL, {
|
|
1152
|
+
method: "POST",
|
|
1153
|
+
headers: { "Content-Type": "application/json" },
|
|
1154
|
+
body: JSON.stringify({ query }),
|
|
1155
|
+
signal: AbortSignal.timeout(ARWEAVE_FETCH_TIMEOUT_MS)
|
|
1156
|
+
});
|
|
1157
|
+
if (!response.ok) {
|
|
1158
|
+
return null;
|
|
1159
|
+
}
|
|
1160
|
+
const json = await response.json();
|
|
1161
|
+
const edges = json.data?.transactions?.edges;
|
|
1162
|
+
if (!edges || edges.length === 0) {
|
|
1163
|
+
return null;
|
|
1164
|
+
}
|
|
1165
|
+
const txId = edges[0]?.node?.id;
|
|
1166
|
+
if (!txId || !isValidArweaveTxId(txId)) {
|
|
1167
|
+
return null;
|
|
1168
|
+
}
|
|
1169
|
+
if (shaToTxIdCache.size >= SHA_CACHE_MAX_SIZE) {
|
|
1170
|
+
const firstKey = shaToTxIdCache.keys().next().value;
|
|
1171
|
+
if (firstKey !== void 0) {
|
|
1172
|
+
shaToTxIdCache.delete(firstKey);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
shaToTxIdCache.set(cacheKey, txId);
|
|
1176
|
+
return txId;
|
|
1177
|
+
} catch {
|
|
1178
|
+
return null;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
function isValidRelayUrl(url) {
|
|
1182
|
+
return /^wss?:\/\//i.test(url);
|
|
1183
|
+
}
|
|
1184
|
+
var WS_OPEN = 1;
|
|
1185
|
+
function defaultWebSocketFactory2(url) {
|
|
1186
|
+
const ctor = globalThis.WebSocket;
|
|
1187
|
+
if (!ctor) {
|
|
1188
|
+
throw new Error(
|
|
1189
|
+
"No global WebSocket constructor (Node >= 22 required) \u2014 pass webSocketFactory"
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
return new ctor(url);
|
|
1193
|
+
}
|
|
1194
|
+
function decodeEventPayload(payload) {
|
|
1195
|
+
if (payload !== null && typeof payload === "object") {
|
|
1196
|
+
return payload;
|
|
1197
|
+
}
|
|
1198
|
+
if (typeof payload !== "string") {
|
|
1199
|
+
return null;
|
|
1200
|
+
}
|
|
1201
|
+
try {
|
|
1202
|
+
const parsed = JSON.parse(payload);
|
|
1203
|
+
if (parsed !== null && typeof parsed === "object") {
|
|
1204
|
+
return parsed;
|
|
1205
|
+
}
|
|
1206
|
+
} catch {
|
|
1207
|
+
}
|
|
1208
|
+
try {
|
|
1209
|
+
return decodeToon(payload);
|
|
1210
|
+
} catch {
|
|
1211
|
+
return null;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
function queryRelay(relayUrl, filter, timeoutMs, webSocketFactory) {
|
|
1215
|
+
return new Promise((resolve2, reject) => {
|
|
1216
|
+
const events = [];
|
|
1217
|
+
const subId = `git-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1218
|
+
let ws;
|
|
1219
|
+
let timeoutHandle;
|
|
1220
|
+
let settled = false;
|
|
1221
|
+
const settle = (outcome, error) => {
|
|
1222
|
+
if (settled) return;
|
|
1223
|
+
settled = true;
|
|
1224
|
+
clearTimeout(timeoutHandle);
|
|
1225
|
+
try {
|
|
1226
|
+
if (ws.readyState === WS_OPEN) {
|
|
1227
|
+
ws.send(JSON.stringify(["CLOSE", subId]));
|
|
1228
|
+
}
|
|
1229
|
+
ws.close();
|
|
1230
|
+
} catch {
|
|
1231
|
+
}
|
|
1232
|
+
if (outcome === "reject") {
|
|
1233
|
+
reject(error);
|
|
1234
|
+
} else {
|
|
1235
|
+
resolve2(events);
|
|
1236
|
+
}
|
|
1237
|
+
};
|
|
1238
|
+
if (!isValidRelayUrl(relayUrl)) {
|
|
1239
|
+
reject(
|
|
1240
|
+
new Error(
|
|
1241
|
+
`Invalid relay URL protocol (must be ws:// or wss://): ${relayUrl}`
|
|
1242
|
+
)
|
|
1243
|
+
);
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
try {
|
|
1247
|
+
ws = webSocketFactory(relayUrl);
|
|
1248
|
+
} catch (err) {
|
|
1249
|
+
reject(new Error(`Failed to connect to relay ${relayUrl}: ${String(err)}`));
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
timeoutHandle = setTimeout(() => {
|
|
1253
|
+
settle("resolve");
|
|
1254
|
+
}, timeoutMs);
|
|
1255
|
+
ws.addEventListener("open", () => {
|
|
1256
|
+
ws.send(JSON.stringify(["REQ", subId, filter]));
|
|
1257
|
+
});
|
|
1258
|
+
ws.addEventListener("message", (msgEvent) => {
|
|
1259
|
+
try {
|
|
1260
|
+
const msg = JSON.parse(String(msgEvent.data));
|
|
1261
|
+
if (!Array.isArray(msg) || msg.length < 2) return;
|
|
1262
|
+
const msgType = msg[0];
|
|
1263
|
+
if (msgType === "EVENT" && msg[1] === subId && msg[2] !== void 0) {
|
|
1264
|
+
const event = decodeEventPayload(msg[2]);
|
|
1265
|
+
if (event) events.push(event);
|
|
1266
|
+
} else if (msgType === "EOSE" && msg[1] === subId) {
|
|
1267
|
+
settle("resolve");
|
|
1268
|
+
}
|
|
1269
|
+
} catch {
|
|
1270
|
+
}
|
|
1271
|
+
});
|
|
1272
|
+
ws.addEventListener("error", (event) => {
|
|
1273
|
+
const detail = typeof event === "object" && event !== null && "message" in event ? String(event.message) : "unknown";
|
|
1274
|
+
settle(
|
|
1275
|
+
"reject",
|
|
1276
|
+
new Error(`WebSocket error connecting to ${relayUrl}: ${detail}`)
|
|
1277
|
+
);
|
|
1278
|
+
});
|
|
1279
|
+
ws.addEventListener("close", () => {
|
|
1280
|
+
settle("resolve");
|
|
1281
|
+
});
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
function latestReplaceable(events) {
|
|
1285
|
+
let winner = null;
|
|
1286
|
+
for (const event of events) {
|
|
1287
|
+
if (winner === null || event.created_at > winner.created_at || event.created_at === winner.created_at && event.id < winner.id) {
|
|
1288
|
+
winner = event;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
return winner;
|
|
1292
|
+
}
|
|
1293
|
+
function getTagValue(tags, name) {
|
|
1294
|
+
const tag = tags.find((t) => t[0] === name);
|
|
1295
|
+
return tag?.[1];
|
|
1296
|
+
}
|
|
1297
|
+
var MAX_REFS_PER_EVENT = 1e3;
|
|
1298
|
+
var SYMREF_PREFIX = "ref: ";
|
|
1299
|
+
function parseRefsEvent(event) {
|
|
1300
|
+
const refs = /* @__PURE__ */ new Map();
|
|
1301
|
+
const shaToTxId = /* @__PURE__ */ new Map();
|
|
1302
|
+
let headSymref = null;
|
|
1303
|
+
for (const tag of event.tags) {
|
|
1304
|
+
const [tagName, v1, v2] = tag;
|
|
1305
|
+
if (tagName === "r" && v1 && v2) {
|
|
1306
|
+
if (v1 === "HEAD" && v2.startsWith(SYMREF_PREFIX)) {
|
|
1307
|
+
headSymref = v2.slice(SYMREF_PREFIX.length);
|
|
1308
|
+
continue;
|
|
1309
|
+
}
|
|
1310
|
+
if (refs.size >= MAX_REFS_PER_EVENT) continue;
|
|
1311
|
+
refs.set(v1, v2);
|
|
1312
|
+
} else if (tagName === "HEAD" && v1?.startsWith(SYMREF_PREFIX)) {
|
|
1313
|
+
headSymref = v1.slice(SYMREF_PREFIX.length);
|
|
1314
|
+
} else if (tagName === "arweave" && v1 && v2) {
|
|
1315
|
+
shaToTxId.set(v1, v2);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
return { refs, headSymref, shaToTxId };
|
|
1319
|
+
}
|
|
1320
|
+
async function fetchRemoteState(options) {
|
|
1321
|
+
const {
|
|
1322
|
+
relayUrls,
|
|
1323
|
+
ownerPubkey,
|
|
1324
|
+
repoId,
|
|
1325
|
+
timeoutMs = 1e4,
|
|
1326
|
+
resolveSha = resolveGitSha,
|
|
1327
|
+
webSocketFactory = defaultWebSocketFactory2
|
|
1328
|
+
} = options;
|
|
1329
|
+
if (relayUrls.length === 0) {
|
|
1330
|
+
throw new Error("fetchRemoteState: relayUrls must not be empty");
|
|
1331
|
+
}
|
|
1332
|
+
if (!ownerPubkey) {
|
|
1333
|
+
throw new Error("fetchRemoteState: ownerPubkey is required");
|
|
1334
|
+
}
|
|
1335
|
+
if (!repoId) {
|
|
1336
|
+
throw new Error("fetchRemoteState: repoId is required");
|
|
1337
|
+
}
|
|
1338
|
+
const filter = {
|
|
1339
|
+
kinds: [REPOSITORY_ANNOUNCEMENT_KIND2, REPOSITORY_STATE_KIND],
|
|
1340
|
+
authors: [ownerPubkey],
|
|
1341
|
+
"#d": [repoId]
|
|
1342
|
+
};
|
|
1343
|
+
const results = await Promise.allSettled(
|
|
1344
|
+
relayUrls.map((url) => queryRelay(url, filter, timeoutMs, webSocketFactory))
|
|
1345
|
+
);
|
|
1346
|
+
const failures = [];
|
|
1347
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1348
|
+
for (const result of results) {
|
|
1349
|
+
if (result.status === "rejected") {
|
|
1350
|
+
failures.push(String(result.reason?.message ?? result.reason));
|
|
1351
|
+
continue;
|
|
1352
|
+
}
|
|
1353
|
+
for (const event of result.value) {
|
|
1354
|
+
if (event.pubkey !== ownerPubkey) continue;
|
|
1355
|
+
if (getTagValue(event.tags, "d") !== repoId) continue;
|
|
1356
|
+
if (typeof event.id === "string" && !byId.has(event.id)) {
|
|
1357
|
+
byId.set(event.id, event);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
if (failures.length === relayUrls.length) {
|
|
1362
|
+
throw new Error(
|
|
1363
|
+
`fetchRemoteState: all ${relayUrls.length} relay(s) failed: ${failures.join("; ")}`
|
|
1364
|
+
);
|
|
1365
|
+
}
|
|
1366
|
+
const events = [...byId.values()];
|
|
1367
|
+
const refsEvent = latestReplaceable(
|
|
1368
|
+
events.filter((e) => e.kind === REPOSITORY_STATE_KIND)
|
|
1369
|
+
);
|
|
1370
|
+
const announceEvent = latestReplaceable(
|
|
1371
|
+
events.filter((e) => e.kind === REPOSITORY_ANNOUNCEMENT_KIND2)
|
|
1372
|
+
);
|
|
1373
|
+
const { refs, headSymref, shaToTxId } = refsEvent ? parseRefsEvent(refsEvent) : {
|
|
1374
|
+
refs: /* @__PURE__ */ new Map(),
|
|
1375
|
+
headSymref: null,
|
|
1376
|
+
shaToTxId: /* @__PURE__ */ new Map()
|
|
1377
|
+
};
|
|
1378
|
+
if (shaToTxId.size > 0) {
|
|
1379
|
+
seedShaCache(
|
|
1380
|
+
[...shaToTxId].map(
|
|
1381
|
+
([sha, txId]) => [shaCacheKey(sha, repoId), txId]
|
|
1382
|
+
)
|
|
1383
|
+
);
|
|
1384
|
+
}
|
|
1385
|
+
const name = announceEvent ? getTagValue(announceEvent.tags, "name") ?? null : null;
|
|
1386
|
+
const description = announceEvent ? getTagValue(announceEvent.tags, "description") ?? announceEvent.content : null;
|
|
1387
|
+
const relays = [];
|
|
1388
|
+
if (announceEvent) {
|
|
1389
|
+
for (const tag of announceEvent.tags) {
|
|
1390
|
+
if (tag[0] === "relays") {
|
|
1391
|
+
relays.push(...tag.slice(1).filter((url) => url.length > 0));
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
const resolveMissing = async (shas) => {
|
|
1396
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
1397
|
+
const missing = [];
|
|
1398
|
+
for (const sha of new Set(shas)) {
|
|
1399
|
+
const known = shaToTxId.get(sha);
|
|
1400
|
+
if (known !== void 0) {
|
|
1401
|
+
resolved.set(sha, known);
|
|
1402
|
+
} else {
|
|
1403
|
+
missing.push(sha);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
const lookups = await Promise.all(
|
|
1407
|
+
missing.map(
|
|
1408
|
+
async (sha) => [sha, await resolveSha(sha, repoId)]
|
|
1409
|
+
)
|
|
1410
|
+
);
|
|
1411
|
+
for (const [sha, txId] of lookups) {
|
|
1412
|
+
if (txId) resolved.set(sha, txId);
|
|
1413
|
+
}
|
|
1414
|
+
return resolved;
|
|
1415
|
+
};
|
|
1416
|
+
return {
|
|
1417
|
+
announced: announceEvent !== null,
|
|
1418
|
+
refs,
|
|
1419
|
+
headSymref,
|
|
1420
|
+
shaToTxId,
|
|
1421
|
+
refsEvent,
|
|
1422
|
+
announceEvent,
|
|
1423
|
+
name,
|
|
1424
|
+
description,
|
|
1425
|
+
relays,
|
|
1426
|
+
resolveMissing
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
|
|
362
1430
|
// ../../node_modules/.pnpm/@toon-protocol+sdk@0.5.1_@toon-protocol+connector@3.13.0_mina-signer@3.1.0_typescript@5.9.3/node_modules/@toon-protocol/sdk/dist/chunk-UP2VWCW5.js
|
|
363
1431
|
var __require2 = /* @__PURE__ */ ((x) => typeof __require !== "undefined" ? __require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
364
1432
|
get: (a, b) => (typeof __require !== "undefined" ? __require : a)[b]
|
|
@@ -1406,6 +2474,10 @@ var ClientRunner = class {
|
|
|
1406
2474
|
createRelay;
|
|
1407
2475
|
log;
|
|
1408
2476
|
targetsPath;
|
|
2477
|
+
/** Remote-state reader for `/git/*` (injectable — opens relay sockets). */
|
|
2478
|
+
fetchGitRemoteState;
|
|
2479
|
+
/** Local-repo reader factory for `/git/*` (injectable for tests). */
|
|
2480
|
+
createRepoReader;
|
|
1409
2481
|
/**
|
|
1410
2482
|
* Identity-level chain-read client. Reading your OWN on-chain wallet balance is
|
|
1411
2483
|
* a pure (wallet keys + chain RPC) operation that has nothing to do with the
|
|
@@ -1448,6 +2520,8 @@ var ClientRunner = class {
|
|
|
1448
2520
|
this.createClient = deps.createClient;
|
|
1449
2521
|
this.log = deps.logger ?? (() => void 0);
|
|
1450
2522
|
if (deps.targetsPath !== void 0) this.targetsPath = deps.targetsPath;
|
|
2523
|
+
this.fetchGitRemoteState = deps.gitDeps?.fetchRemoteState ?? fetchRemoteState;
|
|
2524
|
+
this.createRepoReader = deps.gitDeps?.createRepoReader ?? ((repoPath) => new GitRepoReader(repoPath));
|
|
1451
2525
|
this.defaultBtpUrl = deps.config.toonClientConfig.btpUrl ?? "";
|
|
1452
2526
|
this.defaultRelayUrl = deps.config.relayUrl;
|
|
1453
2527
|
this.createRelay = deps.createRelay ?? ((opts) => new RelaySubscription({
|
|
@@ -2032,18 +3106,26 @@ var ClientRunner = class {
|
|
|
2032
3106
|
return { relays, apexes };
|
|
2033
3107
|
}
|
|
2034
3108
|
// ── Paid operations ──────────────────────────────────────────────────────
|
|
2035
|
-
/**
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
3109
|
+
/**
|
|
3110
|
+
* Lazily open the apex channel on first paid write (deferred at bootstrap so
|
|
3111
|
+
* the wallet can be funded after start, #69) and persist it for resume.
|
|
3112
|
+
*/
|
|
3113
|
+
async ensureApexChannel(apex, destination) {
|
|
2039
3114
|
let channelId = apex.apexChannelId;
|
|
2040
3115
|
if (!channelId) {
|
|
2041
|
-
channelId = await apex.client.openChannel(
|
|
2042
|
-
if (!
|
|
3116
|
+
channelId = await apex.client.openChannel(destination);
|
|
3117
|
+
if (!destination || destination === apex.destination) {
|
|
2043
3118
|
apex.apexChannelId = channelId;
|
|
2044
3119
|
this.persistApexChannel(apex, channelId);
|
|
2045
3120
|
}
|
|
2046
3121
|
}
|
|
3122
|
+
return channelId;
|
|
3123
|
+
}
|
|
3124
|
+
/** Pay-to-write a single event through the selected (or default) apex. */
|
|
3125
|
+
async publish(req) {
|
|
3126
|
+
const apex = this.selectApex(req.btpUrl);
|
|
3127
|
+
this.assertApexReady(apex);
|
|
3128
|
+
const channelId = await this.ensureApexChannel(apex, req.destination);
|
|
2047
3129
|
const fee = req.fee !== void 0 ? BigInt(req.fee) : apex.feePerEvent;
|
|
2048
3130
|
const claim = await apex.client.signBalanceProof(channelId, fee);
|
|
2049
3131
|
const result = await apex.client.publishEvent(req.event, {
|
|
@@ -2276,7 +3358,7 @@ var ClientRunner = class {
|
|
|
2276
3358
|
* toon_status means it is WIRED, not that its RPC is live). A stall here used
|
|
2277
3359
|
* to block the whole control request until the client aborted, surfacing as a
|
|
2278
3360
|
* misleading "relay/apex unreachable" timeout (#199). Bound each attempt well
|
|
2279
|
-
* under the control
|
|
3361
|
+
* under the control API timeout and retry once so a single transient
|
|
2280
3362
|
* provider stall FAST-FAILS with an honest "balances handler / provider
|
|
2281
3363
|
* stalled" error instead of hanging.
|
|
2282
3364
|
*/
|
|
@@ -2387,6 +3469,238 @@ var ClientRunner = class {
|
|
|
2387
3469
|
body: await res.text()
|
|
2388
3470
|
};
|
|
2389
3471
|
}
|
|
3472
|
+
// ── Git write path (/git/*, epic #222 ticket #227) ────────────────────────
|
|
3473
|
+
/**
|
|
3474
|
+
* The daemon `Publisher` implementation (see @toon-protocol/git) for one
|
|
3475
|
+
* apex. Maps the interface onto the runner's production paid-write
|
|
3476
|
+
* machinery:
|
|
3477
|
+
*
|
|
3478
|
+
* - `getFeeRates`: flat `apex.feePerEvent` per publish + the network
|
|
3479
|
+
* per-byte upload rate.
|
|
3480
|
+
* - `uploadGitObject`: kind:5094 store write with Git-SHA/Git-Type/Repo
|
|
3481
|
+
* tags (the proven seed-pipeline shape), signed with the daemon key,
|
|
3482
|
+
* paid via signBalanceProof on the apex channel, routed to the store
|
|
3483
|
+
* destination (`POST /store`); the Arweave txId is decoded from the
|
|
3484
|
+
* FULFILL HTTP envelope.
|
|
3485
|
+
* - `publishEvent`: sign with the daemon key + the standard paid publish
|
|
3486
|
+
* path (signBalanceProof → publishEvent → feePaid). The daemon owns its
|
|
3487
|
+
* write routing (config-seeded relay via the apex), so the advisory
|
|
3488
|
+
* `relayUrls` list is not consulted here — remote-state reads DO use it.
|
|
3489
|
+
*/
|
|
3490
|
+
gitPublisher(apex) {
|
|
3491
|
+
return {
|
|
3492
|
+
getFeeRates: async () => ({
|
|
3493
|
+
uploadFeePerByte: UPLOAD_FEE_PER_BYTE,
|
|
3494
|
+
eventFee: apex.feePerEvent
|
|
3495
|
+
}),
|
|
3496
|
+
uploadGitObject: (upload) => this.gitUploadObject(apex, upload),
|
|
3497
|
+
publishEvent: (event) => this.gitPublishEvent(apex, event)
|
|
3498
|
+
};
|
|
3499
|
+
}
|
|
3500
|
+
/** Upload one git object body as a paid kind:5094 store write. */
|
|
3501
|
+
async gitUploadObject(apex, upload) {
|
|
3502
|
+
const channelId = await this.ensureApexChannel(apex);
|
|
3503
|
+
const fee = BigInt(upload.body.length) * UPLOAD_FEE_PER_BYTE;
|
|
3504
|
+
const claim = await apex.client.signBalanceProof(channelId, fee);
|
|
3505
|
+
const signed = await apex.client.signEvent({
|
|
3506
|
+
kind: 5094,
|
|
3507
|
+
content: "",
|
|
3508
|
+
tags: [
|
|
3509
|
+
["i", upload.body.toString("base64"), "blob"],
|
|
3510
|
+
["bid", fee.toString(), "usdc"],
|
|
3511
|
+
["output", "application/octet-stream"],
|
|
3512
|
+
["Git-SHA", upload.sha],
|
|
3513
|
+
["Git-Type", upload.type],
|
|
3514
|
+
["Repo", upload.repoId]
|
|
3515
|
+
],
|
|
3516
|
+
created_at: nowSeconds()
|
|
3517
|
+
});
|
|
3518
|
+
const result = await apex.client.publishEvent(signed, {
|
|
3519
|
+
destination: this.config.storeDestination,
|
|
3520
|
+
claim,
|
|
3521
|
+
ilpAmount: fee,
|
|
3522
|
+
// The store/DVM backend serves POST /store (not the relay's /write).
|
|
3523
|
+
proxyPath: "/store"
|
|
3524
|
+
});
|
|
3525
|
+
if (!result.success) {
|
|
3526
|
+
throw new PublishRejectedError(
|
|
3527
|
+
`git object ${upload.sha} upload failed (store ${this.config.storeDestination}): ${result.error ?? "store rejected the write"}`
|
|
3528
|
+
);
|
|
3529
|
+
}
|
|
3530
|
+
if (!result.data) {
|
|
3531
|
+
throw new PublishRejectedError(
|
|
3532
|
+
`git object ${upload.sha} upload FULFILL carried no data \u2014 expected the Arweave tx ID`
|
|
3533
|
+
);
|
|
3534
|
+
}
|
|
3535
|
+
let txId;
|
|
3536
|
+
try {
|
|
3537
|
+
txId = extractArweaveTxId(result.data);
|
|
3538
|
+
} catch (err) {
|
|
3539
|
+
throw new PublishRejectedError(
|
|
3540
|
+
`git object ${upload.sha} upload: ${errMsg2(err)}`
|
|
3541
|
+
);
|
|
3542
|
+
}
|
|
3543
|
+
return { txId, feePaid: fee };
|
|
3544
|
+
}
|
|
3545
|
+
/** Sign (daemon key) + pay-to-publish one NIP-34 event via the apex. */
|
|
3546
|
+
async gitPublishEvent(apex, event) {
|
|
3547
|
+
const signed = await apex.client.signEvent(event);
|
|
3548
|
+
const pub = await this.publish({
|
|
3549
|
+
event: signed,
|
|
3550
|
+
...apex.btpUrl ? { btpUrl: apex.btpUrl } : {}
|
|
3551
|
+
});
|
|
3552
|
+
return { eventId: pub.eventId, feePaid: BigInt(pub.feePaid) };
|
|
3553
|
+
}
|
|
3554
|
+
/**
|
|
3555
|
+
* Plan a push: read the local repo + the remote NIP-34 state, classify ref
|
|
3556
|
+
* updates, compute the object delta, and price it. Shared by
|
|
3557
|
+
* estimate (returns the plan) and push (executes it).
|
|
3558
|
+
*/
|
|
3559
|
+
async planGitPush(apex, req) {
|
|
3560
|
+
await assertRepoPath(req.repoPath);
|
|
3561
|
+
if (typeof req.repoId !== "string" || req.repoId === "") {
|
|
3562
|
+
throw new InvalidPayloadError("repoId is required.");
|
|
3563
|
+
}
|
|
3564
|
+
const relayUrls = req.relayUrls && req.relayUrls.length > 0 ? req.relayUrls : [this.defaultRelayUrl];
|
|
3565
|
+
const ownerPubkey = apex.client.getPublicKey();
|
|
3566
|
+
const repoReader = this.createRepoReader(req.repoPath);
|
|
3567
|
+
const remoteState = await this.fetchGitRemoteState({
|
|
3568
|
+
relayUrls,
|
|
3569
|
+
ownerPubkey,
|
|
3570
|
+
repoId: req.repoId
|
|
3571
|
+
});
|
|
3572
|
+
const publisher = this.gitPublisher(apex);
|
|
3573
|
+
const feeRates = await publisher.getFeeRates();
|
|
3574
|
+
const plan = await planPush({
|
|
3575
|
+
repoReader,
|
|
3576
|
+
remoteState,
|
|
3577
|
+
feeRates,
|
|
3578
|
+
repoId: req.repoId,
|
|
3579
|
+
...req.refspecs !== void 0 ? { refs: req.refspecs } : {},
|
|
3580
|
+
...req.force !== void 0 ? { force: req.force } : {},
|
|
3581
|
+
...req.announcement !== void 0 ? { announcement: req.announcement } : {}
|
|
3582
|
+
});
|
|
3583
|
+
return { plan, remoteState, repoReader, relayUrls, publisher };
|
|
3584
|
+
}
|
|
3585
|
+
/** Plan + price a push WITHOUT paying (backs `POST /git/estimate`). */
|
|
3586
|
+
async gitEstimate(req) {
|
|
3587
|
+
const apex = this.selectApex();
|
|
3588
|
+
this.assertApexReady(apex);
|
|
3589
|
+
const { plan } = await this.planGitPush(apex, req);
|
|
3590
|
+
return serializePushPlan2(plan);
|
|
3591
|
+
}
|
|
3592
|
+
/** Plan + EXECUTE a push: paid uploads + paid publishes (`POST /git/push`). */
|
|
3593
|
+
async gitPush(req) {
|
|
3594
|
+
if (req.confirm !== true) {
|
|
3595
|
+
throw new InvalidPayloadError(
|
|
3596
|
+
"a push uploads objects to Arweave and publishes events \u2014 permanent and paid. Run /git/estimate first, then set confirm: true to proceed."
|
|
3597
|
+
);
|
|
3598
|
+
}
|
|
3599
|
+
const apex = this.selectApex();
|
|
3600
|
+
this.assertApexReady(apex);
|
|
3601
|
+
const { plan, remoteState, repoReader, relayUrls, publisher } = await this.planGitPush(apex, req);
|
|
3602
|
+
const result = await executePush({
|
|
3603
|
+
plan,
|
|
3604
|
+
publisher,
|
|
3605
|
+
remoteState,
|
|
3606
|
+
repoReader,
|
|
3607
|
+
relayUrls
|
|
3608
|
+
});
|
|
3609
|
+
return serializePushResult2(plan, result);
|
|
3610
|
+
}
|
|
3611
|
+
/** Build, sign, and pay-to-publish a kind:1621 issue. */
|
|
3612
|
+
async gitIssue(req) {
|
|
3613
|
+
const addr = validateRepoAddr(req.repoAddr);
|
|
3614
|
+
assertNonEmptyString(req.title, "title");
|
|
3615
|
+
assertNonEmptyString(req.body, "body");
|
|
3616
|
+
const event = buildIssue(
|
|
3617
|
+
addr.ownerPubkey,
|
|
3618
|
+
addr.repoId,
|
|
3619
|
+
req.title,
|
|
3620
|
+
req.body,
|
|
3621
|
+
req.labels ?? []
|
|
3622
|
+
);
|
|
3623
|
+
return this.gitPublishSigned(event);
|
|
3624
|
+
}
|
|
3625
|
+
/** Build, sign, and pay-to-publish a kind:1622 comment on an issue/patch. */
|
|
3626
|
+
async gitComment(req) {
|
|
3627
|
+
const addr = validateRepoAddr(req.repoAddr);
|
|
3628
|
+
assertNonEmptyString(req.rootEventId, "rootEventId");
|
|
3629
|
+
assertNonEmptyString(req.body, "body");
|
|
3630
|
+
const event = buildComment(
|
|
3631
|
+
addr.ownerPubkey,
|
|
3632
|
+
addr.repoId,
|
|
3633
|
+
req.rootEventId,
|
|
3634
|
+
req.parentAuthorPubkey ?? addr.ownerPubkey,
|
|
3635
|
+
req.body,
|
|
3636
|
+
req.marker ?? "root"
|
|
3637
|
+
);
|
|
3638
|
+
return this.gitPublishSigned(event);
|
|
3639
|
+
}
|
|
3640
|
+
/**
|
|
3641
|
+
* Build, sign, and pay-to-publish a kind:1617 patch. Content is either the
|
|
3642
|
+
* supplied `patchText` or real `git format-patch --stdout <range>` output
|
|
3643
|
+
* from a local repository — exactly one source must be given.
|
|
3644
|
+
*/
|
|
3645
|
+
async gitPatch(req) {
|
|
3646
|
+
const addr = validateRepoAddr(req.repoAddr);
|
|
3647
|
+
assertNonEmptyString(req.title, "title");
|
|
3648
|
+
const hasText = typeof req.patchText === "string" && req.patchText !== "";
|
|
3649
|
+
const hasRange = typeof req.repoPath === "string" && req.repoPath !== "" && typeof req.range === "string" && req.range !== "";
|
|
3650
|
+
if (hasText === hasRange) {
|
|
3651
|
+
throw new InvalidPayloadError(
|
|
3652
|
+
"exactly one of patchText | repoPath+range is required."
|
|
3653
|
+
);
|
|
3654
|
+
}
|
|
3655
|
+
let content;
|
|
3656
|
+
if (hasRange) {
|
|
3657
|
+
await assertRepoPath(req.repoPath);
|
|
3658
|
+
content = await this.createRepoReader(req.repoPath).formatPatch(
|
|
3659
|
+
req.range
|
|
3660
|
+
);
|
|
3661
|
+
if (content === "") {
|
|
3662
|
+
throw new InvalidPayloadError(
|
|
3663
|
+
`range ${JSON.stringify(req.range)} selects no commits \u2014 nothing to publish.`
|
|
3664
|
+
);
|
|
3665
|
+
}
|
|
3666
|
+
} else {
|
|
3667
|
+
content = req.patchText;
|
|
3668
|
+
}
|
|
3669
|
+
const event = buildPatch(
|
|
3670
|
+
addr.ownerPubkey,
|
|
3671
|
+
addr.repoId,
|
|
3672
|
+
req.title,
|
|
3673
|
+
req.commits ?? [],
|
|
3674
|
+
req.branch,
|
|
3675
|
+
content
|
|
3676
|
+
);
|
|
3677
|
+
return this.gitPublishSigned(event);
|
|
3678
|
+
}
|
|
3679
|
+
/** Build, sign, and pay-to-publish a kind:1630-1633 status event. */
|
|
3680
|
+
async gitStatus(req) {
|
|
3681
|
+
const addr = validateRepoAddr(req.repoAddr);
|
|
3682
|
+
assertNonEmptyString(req.targetEventId, "targetEventId");
|
|
3683
|
+
const kind = STATUS_KIND_BY_VALUE[req.status];
|
|
3684
|
+
if (kind === void 0) {
|
|
3685
|
+
throw new InvalidPayloadError(
|
|
3686
|
+
"status must be one of open | applied | closed | draft."
|
|
3687
|
+
);
|
|
3688
|
+
}
|
|
3689
|
+
const event = buildStatus(req.targetEventId, kind, req.targetPubkey);
|
|
3690
|
+
event.tags.push([
|
|
3691
|
+
"a",
|
|
3692
|
+
`${REPOSITORY_ANNOUNCEMENT_KIND}:${addr.ownerPubkey}:${addr.repoId}`
|
|
3693
|
+
]);
|
|
3694
|
+
return this.gitPublishSigned(event);
|
|
3695
|
+
}
|
|
3696
|
+
/** Sign a built NIP-34 event with the daemon key and pay-to-publish it. */
|
|
3697
|
+
async gitPublishSigned(event) {
|
|
3698
|
+
const apex = this.selectApex();
|
|
3699
|
+
this.assertApexReady(apex);
|
|
3700
|
+
const signed = await apex.client.signEvent(event);
|
|
3701
|
+
const pub = await this.publish({ event: signed });
|
|
3702
|
+
return { ...pub, kind: event.kind };
|
|
3703
|
+
}
|
|
2390
3704
|
/** Graceful teardown: close every relay + stop every apex client. */
|
|
2391
3705
|
async stop() {
|
|
2392
3706
|
if (this.stopped) return;
|
|
@@ -2459,6 +3773,89 @@ var BalancesUnavailableError = class extends Error {
|
|
|
2459
3773
|
if (providerError !== void 0) this.providerError = providerError;
|
|
2460
3774
|
}
|
|
2461
3775
|
};
|
|
3776
|
+
var UPLOAD_FEE_PER_BYTE = 10n;
|
|
3777
|
+
var STATUS_KIND_BY_VALUE = {
|
|
3778
|
+
open: STATUS_OPEN_KIND,
|
|
3779
|
+
applied: STATUS_APPLIED_KIND,
|
|
3780
|
+
closed: STATUS_CLOSED_KIND,
|
|
3781
|
+
draft: STATUS_DRAFT_KIND
|
|
3782
|
+
};
|
|
3783
|
+
async function assertRepoPath(repoPath) {
|
|
3784
|
+
if (typeof repoPath !== "string" || repoPath === "") {
|
|
3785
|
+
throw new InvalidPayloadError("repoPath is required.");
|
|
3786
|
+
}
|
|
3787
|
+
let stats;
|
|
3788
|
+
try {
|
|
3789
|
+
stats = await stat(resolve(repoPath));
|
|
3790
|
+
} catch {
|
|
3791
|
+
throw new InvalidPayloadError(`repoPath does not exist: ${repoPath}`);
|
|
3792
|
+
}
|
|
3793
|
+
if (!stats.isDirectory()) {
|
|
3794
|
+
throw new InvalidPayloadError(`repoPath is not a directory: ${repoPath}`);
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3797
|
+
function assertNonEmptyString(value, what) {
|
|
3798
|
+
if (typeof value !== "string" || value === "") {
|
|
3799
|
+
throw new InvalidPayloadError(`${what} is required.`);
|
|
3800
|
+
}
|
|
3801
|
+
}
|
|
3802
|
+
function validateRepoAddr(addr) {
|
|
3803
|
+
if (!addr || typeof addr.ownerPubkey !== "string" || !/^[0-9a-f]{64}$/.test(addr.ownerPubkey)) {
|
|
3804
|
+
throw new InvalidPayloadError(
|
|
3805
|
+
"repoAddr.ownerPubkey must be a 64-char lowercase hex Nostr pubkey."
|
|
3806
|
+
);
|
|
3807
|
+
}
|
|
3808
|
+
if (typeof addr.repoId !== "string" || addr.repoId === "") {
|
|
3809
|
+
throw new InvalidPayloadError("repoAddr.repoId is required.");
|
|
3810
|
+
}
|
|
3811
|
+
return addr;
|
|
3812
|
+
}
|
|
3813
|
+
function serializePushPlan2(plan) {
|
|
3814
|
+
return {
|
|
3815
|
+
repoId: plan.repoId,
|
|
3816
|
+
refUpdates: plan.refUpdates,
|
|
3817
|
+
newRefs: plan.newRefs,
|
|
3818
|
+
headSymref: plan.headSymref,
|
|
3819
|
+
objects: plan.objects,
|
|
3820
|
+
knownShaToTxId: Object.fromEntries(plan.knownShaToTxId),
|
|
3821
|
+
announceNeeded: plan.announceNeeded,
|
|
3822
|
+
announcement: plan.announcement,
|
|
3823
|
+
estimate: serializeFeeEstimate2(plan)
|
|
3824
|
+
};
|
|
3825
|
+
}
|
|
3826
|
+
function serializeFeeEstimate2(plan) {
|
|
3827
|
+
return {
|
|
3828
|
+
objectCount: plan.estimate.objectCount,
|
|
3829
|
+
totalObjectBytes: plan.estimate.totalObjectBytes,
|
|
3830
|
+
uploadFee: plan.estimate.uploadFee.toString(),
|
|
3831
|
+
eventCount: plan.estimate.eventCount,
|
|
3832
|
+
eventFees: plan.estimate.eventFees.toString(),
|
|
3833
|
+
totalFee: plan.estimate.totalFee.toString()
|
|
3834
|
+
};
|
|
3835
|
+
}
|
|
3836
|
+
function serializePushResult2(plan, result) {
|
|
3837
|
+
return {
|
|
3838
|
+
repoId: plan.repoId,
|
|
3839
|
+
refUpdates: plan.refUpdates,
|
|
3840
|
+
uploads: result.uploads.map((u) => ({
|
|
3841
|
+
sha: u.sha,
|
|
3842
|
+
txId: u.txId,
|
|
3843
|
+
feePaid: u.feePaid.toString(),
|
|
3844
|
+
skipped: u.skipped
|
|
3845
|
+
})),
|
|
3846
|
+
announceReceipt: result.announceReceipt ? {
|
|
3847
|
+
eventId: result.announceReceipt.eventId,
|
|
3848
|
+
feePaid: result.announceReceipt.feePaid.toString()
|
|
3849
|
+
} : null,
|
|
3850
|
+
refsReceipt: {
|
|
3851
|
+
eventId: result.refsReceipt.eventId,
|
|
3852
|
+
feePaid: result.refsReceipt.feePaid.toString()
|
|
3853
|
+
},
|
|
3854
|
+
arweaveMap: Object.fromEntries(result.arweaveMap),
|
|
3855
|
+
totalFeePaid: result.totalFeePaid.toString(),
|
|
3856
|
+
estimate: serializeFeeEstimate2(plan)
|
|
3857
|
+
};
|
|
3858
|
+
}
|
|
2462
3859
|
function nowSeconds() {
|
|
2463
3860
|
return Math.floor(Date.now() / 1e3);
|
|
2464
3861
|
}
|
|
@@ -2747,6 +4144,107 @@ function registerRoutes(app, runner) {
|
|
|
2747
4144
|
return mapError(reply, err);
|
|
2748
4145
|
}
|
|
2749
4146
|
});
|
|
4147
|
+
app.post("/git/estimate", async (req, reply) => {
|
|
4148
|
+
const body = req.body;
|
|
4149
|
+
if (!body || !isNonEmptyString(body.repoPath) || !isNonEmptyString(body.repoId)) {
|
|
4150
|
+
return sendError(reply, 400, "invalid_git_request", {
|
|
4151
|
+
detail: "body.repoPath (local repository path) and body.repoId are required."
|
|
4152
|
+
});
|
|
4153
|
+
}
|
|
4154
|
+
try {
|
|
4155
|
+
return await runner.gitEstimate(body);
|
|
4156
|
+
} catch (err) {
|
|
4157
|
+
return mapGitError(reply, err);
|
|
4158
|
+
}
|
|
4159
|
+
});
|
|
4160
|
+
app.post("/git/push", async (req, reply) => {
|
|
4161
|
+
const body = req.body;
|
|
4162
|
+
if (!body || !isNonEmptyString(body.repoPath) || !isNonEmptyString(body.repoId)) {
|
|
4163
|
+
return sendError(reply, 400, "invalid_git_request", {
|
|
4164
|
+
detail: "body.repoPath (local repository path) and body.repoId are required."
|
|
4165
|
+
});
|
|
4166
|
+
}
|
|
4167
|
+
try {
|
|
4168
|
+
return await runner.gitPush(body);
|
|
4169
|
+
} catch (err) {
|
|
4170
|
+
return mapGitError(reply, err);
|
|
4171
|
+
}
|
|
4172
|
+
});
|
|
4173
|
+
app.post("/git/issue", async (req, reply) => {
|
|
4174
|
+
const body = req.body;
|
|
4175
|
+
if (!body || !body.repoAddr || !isNonEmptyString(body.title) || !isNonEmptyString(body.body)) {
|
|
4176
|
+
return sendError(reply, 400, "invalid_git_request", {
|
|
4177
|
+
detail: "body.repoAddr ({ ownerPubkey, repoId }), body.title, and body.body are required."
|
|
4178
|
+
});
|
|
4179
|
+
}
|
|
4180
|
+
try {
|
|
4181
|
+
return await runner.gitIssue(body);
|
|
4182
|
+
} catch (err) {
|
|
4183
|
+
return mapGitError(reply, err);
|
|
4184
|
+
}
|
|
4185
|
+
});
|
|
4186
|
+
app.post("/git/comment", async (req, reply) => {
|
|
4187
|
+
const body = req.body;
|
|
4188
|
+
if (!body || !body.repoAddr || !isNonEmptyString(body.rootEventId) || !isNonEmptyString(body.body)) {
|
|
4189
|
+
return sendError(reply, 400, "invalid_git_request", {
|
|
4190
|
+
detail: "body.repoAddr ({ ownerPubkey, repoId }), body.rootEventId, and body.body are required."
|
|
4191
|
+
});
|
|
4192
|
+
}
|
|
4193
|
+
try {
|
|
4194
|
+
return await runner.gitComment(body);
|
|
4195
|
+
} catch (err) {
|
|
4196
|
+
return mapGitError(reply, err);
|
|
4197
|
+
}
|
|
4198
|
+
});
|
|
4199
|
+
app.post("/git/patch", async (req, reply) => {
|
|
4200
|
+
const body = req.body;
|
|
4201
|
+
if (!body || !body.repoAddr || !isNonEmptyString(body.title)) {
|
|
4202
|
+
return sendError(reply, 400, "invalid_git_request", {
|
|
4203
|
+
detail: "body.repoAddr ({ ownerPubkey, repoId }) and body.title are required (plus exactly one of patchText | repoPath+range)."
|
|
4204
|
+
});
|
|
4205
|
+
}
|
|
4206
|
+
try {
|
|
4207
|
+
return await runner.gitPatch(body);
|
|
4208
|
+
} catch (err) {
|
|
4209
|
+
return mapGitError(reply, err);
|
|
4210
|
+
}
|
|
4211
|
+
});
|
|
4212
|
+
app.post("/git/status", async (req, reply) => {
|
|
4213
|
+
const body = req.body;
|
|
4214
|
+
if (!body || !body.repoAddr || !isNonEmptyString(body.targetEventId) || !isNonEmptyString(body.status)) {
|
|
4215
|
+
return sendError(reply, 400, "invalid_git_request", {
|
|
4216
|
+
detail: "body.repoAddr ({ ownerPubkey, repoId }), body.targetEventId, and body.status (open | applied | closed | draft) are required."
|
|
4217
|
+
});
|
|
4218
|
+
}
|
|
4219
|
+
try {
|
|
4220
|
+
return await runner.gitStatus(body);
|
|
4221
|
+
} catch (err) {
|
|
4222
|
+
return mapGitError(reply, err);
|
|
4223
|
+
}
|
|
4224
|
+
});
|
|
4225
|
+
}
|
|
4226
|
+
function isNonEmptyString(value) {
|
|
4227
|
+
return typeof value === "string" && value !== "";
|
|
4228
|
+
}
|
|
4229
|
+
function mapGitError(reply, err) {
|
|
4230
|
+
if (err instanceof NonFastForwardError) {
|
|
4231
|
+
return reply.status(409).send({
|
|
4232
|
+
error: "non_fast_forward",
|
|
4233
|
+
detail: err.message,
|
|
4234
|
+
refs: err.refs
|
|
4235
|
+
});
|
|
4236
|
+
}
|
|
4237
|
+
if (err instanceof OversizeObjectsError) {
|
|
4238
|
+
return reply.status(413).send({
|
|
4239
|
+
error: "oversize_objects",
|
|
4240
|
+
detail: err.message,
|
|
4241
|
+
objects: err.objects
|
|
4242
|
+
});
|
|
4243
|
+
}
|
|
4244
|
+
if (err instanceof GitError) {
|
|
4245
|
+
return sendError(reply, 400, "git_error", { detail: err.message });
|
|
4246
|
+
}
|
|
4247
|
+
return mapError(reply, err);
|
|
2750
4248
|
}
|
|
2751
4249
|
function isSignedEvent(event) {
|
|
2752
4250
|
if (typeof event !== "object" || event === null) return false;
|
|
@@ -2785,10 +4283,27 @@ function mapError(reply, err) {
|
|
|
2785
4283
|
if (err instanceof Error && err.name === "SettleTooEarlyError") {
|
|
2786
4284
|
return sendError(reply, 425, "settle_too_early", { detail: err.message, retryable: true });
|
|
2787
4285
|
}
|
|
4286
|
+
const funding = findChannelFundingError(err);
|
|
4287
|
+
if (funding) {
|
|
4288
|
+
return sendError(reply, 402, "insufficient_gas", {
|
|
4289
|
+
detail: funding.message,
|
|
4290
|
+
retryable: true
|
|
4291
|
+
});
|
|
4292
|
+
}
|
|
2788
4293
|
return sendError(reply, 500, "internal_error", {
|
|
2789
4294
|
detail: err instanceof Error ? err.message : String(err)
|
|
2790
4295
|
});
|
|
2791
4296
|
}
|
|
4297
|
+
function findChannelFundingError(err) {
|
|
4298
|
+
let cur = err;
|
|
4299
|
+
for (let i = 0; i < 10 && cur != null; i++) {
|
|
4300
|
+
if (cur instanceof Error && cur.name === "ChannelFundingError") {
|
|
4301
|
+
return cur;
|
|
4302
|
+
}
|
|
4303
|
+
cur = cur instanceof Error ? cur.cause : void 0;
|
|
4304
|
+
}
|
|
4305
|
+
return void 0;
|
|
4306
|
+
}
|
|
2792
4307
|
function sendError(reply, status, error, extra = {}) {
|
|
2793
4308
|
return reply.status(status).send({
|
|
2794
4309
|
error,
|
|
@@ -2807,4 +4322,4 @@ export {
|
|
|
2807
4322
|
PublishRejectedError,
|
|
2808
4323
|
registerRoutes
|
|
2809
4324
|
};
|
|
2810
|
-
//# sourceMappingURL=chunk-
|
|
4325
|
+
//# sourceMappingURL=chunk-GQENVTED.js.map
|