@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.
@@ -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-W6T6ZK7U.js";
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
- /** Pay-to-write a single event through the selected (or default) apex. */
2036
- async publish(req) {
2037
- const apex = this.selectApex(req.btpUrl);
2038
- this.assertApexReady(apex);
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(req.destination);
2042
- if (!req.destination || req.destination === apex.destination) {
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-plane timeout and retry once so a single transient
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-HLATGATX.js.map
4325
+ //# sourceMappingURL=chunk-GQENVTED.js.map