@toon-protocol/rig 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -13
- package/dist/{chunk-PLKZAUTG.js → chunk-JBB7HBQC.js} +119 -6
- package/dist/chunk-JBB7HBQC.js.map +1 -0
- package/dist/{chunk-3EKP7PMM.js → chunk-OIJNRACA.js} +5 -144
- package/dist/chunk-OIJNRACA.js.map +1 -0
- package/dist/{chunk-LFGDLD6J.js → chunk-PS5QOT62.js} +464 -4
- package/dist/chunk-PS5QOT62.js.map +1 -0
- package/dist/{chunk-O6TXHKWG.js → chunk-SW7ZHMGS.js} +143 -2
- package/dist/chunk-SW7ZHMGS.js.map +1 -0
- package/dist/cli/rig.js +1515 -149
- package/dist/cli/rig.js.map +1 -1
- package/dist/index.d.ts +271 -3
- package/dist/index.js +44 -8
- package/dist/{publisher-Cdr1H1hg.d.ts → publisher-CrwaNQrK.d.ts} +11 -2
- package/dist/standalone/index.d.ts +7 -1
- package/dist/standalone/index.js +9 -9
- package/dist/{standalone-mode-FCKTQ33Y.js → standalone-mode-64CKUVU2.js} +367 -97
- package/dist/standalone-mode-64CKUVU2.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-3EKP7PMM.js.map +0 -1
- package/dist/chunk-HPSOQP7Q.js +0 -109
- package/dist/chunk-HPSOQP7Q.js.map +0 -1
- package/dist/chunk-LFGDLD6J.js.map +0 -1
- package/dist/chunk-O6TXHKWG.js.map +0 -1
- package/dist/chunk-PLKZAUTG.js.map +0 -1
- package/dist/standalone-mode-FCKTQ33Y.js.map +0 -1
package/README.md
CHANGED
|
@@ -6,7 +6,11 @@ Git-to-TOON write path core — build git objects and NIP-34 events for the Rig
|
|
|
6
6
|
| --- | --- | --- |
|
|
7
7
|
| `rig init` | rig (free) | one-shot repo setup: identity + `toon.*` git config |
|
|
8
8
|
| `rig remote add/remove/list` | rig (free) | relays as REAL git remotes (`origin` = default publish target) |
|
|
9
|
+
| `rig clone <relay-url> <owner>/<repo-id> [dir]` | rig (free) | bootstrap a repo from TOON: relay state + SHA-verified Arweave objects → a real, push-capable git repository. Shadows `git clone` |
|
|
10
|
+
| `rig fetch [remote]` | rig (free) | download the missing object delta + update `refs/remotes/<remote>/*` (no merge — `rig merge origin/main`). Shadows `git fetch` |
|
|
9
11
|
| `rig push [remote] [refspecs...]` | rig (paid) | the TOON push: Arweave upload + NIP-34 refs publish. Shadows `git push` — plain-git pushes stay available by running `git push` directly |
|
|
12
|
+
| `rig issue list` / `rig issue show <id>` | rig (free) | the repo's issues + comments from the terminal (state derived from kind:1630-1633, latest wins) |
|
|
13
|
+
| `rig pr list` / `rig pr show <id>` | rig (free) | the repo's patches/PRs; `show` prints the full patch text (pipe it to `git am`) |
|
|
10
14
|
| `rig issue create` | rig (paid) | file an issue (kind:1621) |
|
|
11
15
|
| `rig comment <root-event-id>` | rig (paid) | comment (kind:1622) on an issue/patch |
|
|
12
16
|
| `rig pr create` | rig (paid) | publish a patch (kind:1617) from real `git format-patch` |
|
|
@@ -57,6 +61,42 @@ rig pr status <event-id> applied # kind:1631
|
|
|
57
61
|
# (bare `rig status` is git's)
|
|
58
62
|
```
|
|
59
63
|
|
|
64
|
+
### The second contributor (reads are FREE)
|
|
65
|
+
|
|
66
|
+
Someone published a repo on TOON? You can pick it up with **nothing
|
|
67
|
+
configured** — no identity, no wallet, no channel. Reads are free:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
npm install -g @toon-protocol/rig
|
|
71
|
+
|
|
72
|
+
# 1. clone — relay state + SHA-verified Arweave objects → a real git repo
|
|
73
|
+
# (owner as npub1… or 64-char hex; dir defaults to the repo id)
|
|
74
|
+
rig clone wss://relay.example npub1…/their-repo
|
|
75
|
+
cd their-repo # toon.* config + origin remote already set
|
|
76
|
+
|
|
77
|
+
# 2. read the tracker from the terminal
|
|
78
|
+
rig issue list # issues + derived state (latest status wins)
|
|
79
|
+
rig issue show <event-id> # one issue + its comments
|
|
80
|
+
rig pr list --state open # patches/PRs
|
|
81
|
+
rig pr show <event-id> --json | jq -r .pr.content | git am # apply a patch
|
|
82
|
+
|
|
83
|
+
# 3. stay current — fetch downloads only the missing delta
|
|
84
|
+
rig fetch # updates refs/remotes/origin/*, like git fetch
|
|
85
|
+
rig merge origin/main # integrating is plain git (passthrough)
|
|
86
|
+
|
|
87
|
+
# 4. ready to contribute back? writes are paid — add an identity + funds:
|
|
88
|
+
export RIG_MNEMONIC="abandon abandon … about"
|
|
89
|
+
export TOON_CLIENT_NETWORK=devnet # shared devnet: enables `rig fund` (no faucet URL needed)
|
|
90
|
+
rig fund # devnet faucet drip
|
|
91
|
+
rig issue create --title "found a bug" --body "details" # kind:1621
|
|
92
|
+
rig push # or publish commits (your own repos)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Note: freshly pushed objects can take **10-20 minutes** to become fetchable
|
|
96
|
+
from Arweave gateways — `rig clone`/`rig fetch` right after a push reports
|
|
97
|
+
the missing SHAs honestly; retry after propagation. A failed clone never
|
|
98
|
+
leaves a partial repository behind.
|
|
99
|
+
|
|
60
100
|
### Strict `--json` stdout (machine consumers)
|
|
61
101
|
|
|
62
102
|
With `--json`, stdout carries **exactly one JSON document** — everything human-facing (identity reports, deprecation nudges, migration hints, chain-selection rationales, discovery warnings, progress lines, even stray `console.log` output from dependencies) is routed to stderr, so `rig <command> --json | jq` always parses. Errors emit one machine envelope (`{"error": "<code>", "detail": …}`) on stdout with the human detail on stderr and a non-zero exit; runs that fail before producing output (usage errors, pre-payment refusals) still emit a backstop error envelope. `--json` is a per-subcommand flag on the commands rig owns, **not** a global rig flag — see the passthrough note below.
|
|
@@ -69,10 +109,10 @@ The passthrough is exempt from the `--json` contract: `rig status --json` runs `
|
|
|
69
109
|
|
|
70
110
|
### Identity
|
|
71
111
|
|
|
72
|
-
The CLI is **standalone
|
|
73
|
-
your seed phrase (`@toon-protocol/client` is a regular dependency,
|
|
74
|
-
automatically with the package)
|
|
75
|
-
precedence chain — highest first:
|
|
112
|
+
The CLI is **standalone by default**: it embeds its own payment client built
|
|
113
|
+
from your seed phrase (`@toon-protocol/client` is a regular dependency,
|
|
114
|
+
installed automatically with the package) — no daemon is ever required. The
|
|
115
|
+
mnemonic is resolved along one precedence chain — highest first:
|
|
76
116
|
|
|
77
117
|
1. `RIG_MNEMONIC` environment variable
|
|
78
118
|
2. `TOON_CLIENT_MNEMONIC` environment variable — deprecated alias, warns on
|
|
@@ -90,11 +130,42 @@ Every paid command reports which source is active and the derived pubkey
|
|
|
90
130
|
output) — the phrase itself is never printed and never written to git config
|
|
91
131
|
or any repo file.
|
|
92
132
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
133
|
+
### Daemon as accelerator (#279)
|
|
134
|
+
|
|
135
|
+
Every standalone paid command pays a fixed bootstrap cost (relay discovery,
|
|
136
|
+
peer negotiation, channel resume). Two things remove most of it:
|
|
137
|
+
|
|
138
|
+
- **Automatic daemon delegation** — when a running `toon-clientd` on the
|
|
139
|
+
loopback control port (`TOON_CLIENT_HTTP_PORT`, default 8787) holds the
|
|
140
|
+
**same identity**, paid write commands (`push`, `issue`, `comment`,
|
|
141
|
+
`pr create`, `pr status`) delegate to its `/git/*` routes instead of
|
|
142
|
+
bootstrapping an embedded client. The daemon already owns the payment
|
|
143
|
+
channel's cumulative-claim watermark, so one process signs all claims —
|
|
144
|
+
the exact safety property the pre-#279 nonce-guard *refusal* protected,
|
|
145
|
+
achieved by delegation instead. The identity match is confirmed against
|
|
146
|
+
`GET /status` **before** anything is sent. A daemon on a different
|
|
147
|
+
identity, or no daemon at all, runs standalone as always. The chosen path
|
|
148
|
+
prints on stderr (`rig: paid path: …`) and lands in `--json` envelopes as
|
|
149
|
+
`"path": "daemon" | "standalone"`. Note: the single-event daemon routes
|
|
150
|
+
publish via the daemon's configured relay route; a resolved relay that
|
|
151
|
+
differs from it draws a warning. Commands the daemon has **no route
|
|
152
|
+
for** — `rig fund`, `rig balance`, `rig channel open|close|settle` — always
|
|
153
|
+
run standalone; the on-chain channel mutations among them still refuse
|
|
154
|
+
while a same-identity daemon runs (they must not race its live claims):
|
|
155
|
+
stop the daemon for those.
|
|
156
|
+
- **Standalone topology cache** — the resolved network topology (announce
|
|
157
|
+
discovery, payment-peer pick, settlement-chain selection incl. the
|
|
158
|
+
funded-chain probes) is cached under `TOON_CLIENT_HOME`
|
|
159
|
+
(`rig-topology-cache.json`), keyed by relay + identity + explicit config,
|
|
160
|
+
for 15 minutes (`RIG_TOPOLOGY_TTL_MS` overrides; `0` disables). A cached
|
|
161
|
+
topology that fails to bootstrap is invalidated and re-resolved live
|
|
162
|
+
automatically. Money state (claim watermarks, channel map) is never
|
|
163
|
+
cached.
|
|
164
|
+
|
|
165
|
+
The `rig` bin also exits as soon as a command finishes and stdio is flushed:
|
|
166
|
+
the embedded client can leave a keep-alive socket that would otherwise hold
|
|
167
|
+
the process open for ~30 more seconds — which was, in fact, the bulk of the
|
|
168
|
+
uniform "~32s per paid command" the #279 study measured.
|
|
98
169
|
|
|
99
170
|
### Pushing
|
|
100
171
|
|
|
@@ -117,11 +188,17 @@ The single-event subcommands follow the same paid-write discipline as push — t
|
|
|
117
188
|
|
|
118
189
|
- `rig issue create --title <t> [--body <b> | --body-file <f> | stdin] [--label <l>]…` — kind:1621.
|
|
119
190
|
- `rig comment <root-event-id> --body <b> [--parent-author <pubkey>] [--marker root|reply]` — kind:1622.
|
|
120
|
-
- `rig pr create --title <t> (--range <A..B> | --patch-file <f>) [--branch <name>]` — kind:1617; `--range` runs real `git format-patch --stdout` locally and derives the `commit`/`parent-commit` tags. A multi-commit range publishes ONE event carrying the whole series (cover-letter threading is out of scope in v1).
|
|
191
|
+
- `rig pr create --title <t> (--range <A..B> | --patch-file <f>) [--body <b> | --body-file <f>] [--branch <name>]` — kind:1617; `--range` runs real `git format-patch --stdout` locally and derives the `commit`/`parent-commit` tags. A multi-commit range publishes ONE event carrying the whole series (cover-letter threading is out of scope in v1). `--body`/`--body-file` attach the PR description in a dedicated `description` tag — the event content stays pure format-patch output, so `rig pr show`'s patch text still pipes straight into `git am`.
|
|
121
192
|
- `rig pr status <target-event-id> <open|applied|closed|draft>` — kind:1630–1633, with the repo `a` tag attached. (This was top-level `rig status` before v2; bare `rig status` now passes through to `git status`.)
|
|
122
193
|
|
|
123
194
|
`--repo-id`/`--owner` override the git config address (use `--owner` for repos you don't own).
|
|
124
195
|
|
|
196
|
+
### Cloning & fetching (free reads, #278)
|
|
197
|
+
|
|
198
|
+
`rig clone <relay-url> <owner>/<repo-id> [dir]` reconstructs the repository from public data alone: the kind:30618 `arweave` sha→txId map drives parallel downloads across the gateway fallback chain (SHAs the map misses resolve via the Arweave GraphQL `Git-SHA` tag index), **every body is verified against its SHA-1 before it is written** (verification doubles as object-type discovery; corrupt/tampered content is rejected), and the repository is materialized through git's own plumbing — `git hash-object -w --stdin -t <type>` (written SHA re-checked), `git update-ref`, HEAD from the 30618 symref, checked-out worktree. Everything happens in a temp dir moved into place on success, so a failed clone never leaves a partial repo. `rig fetch [remote]` is the same pipeline as a delta: only locally-missing objects are downloaded, and `refs/remotes/<remote>/*` (tags → `refs/tags/*`) move with a `git fetch`-style report.
|
|
199
|
+
|
|
200
|
+
`rig issue list|show` and `rig pr list|show` are pure relay reads (kind:1621/1617 by the repo `#a` tag; state from kind:1630-1633, latest wins; kind:1622 comments under `show`), tolerant of the devnet relay's non-canonical EVENT payload encodings.
|
|
201
|
+
|
|
125
202
|
## Library
|
|
126
203
|
|
|
127
204
|
No signing or payment code lives in the core — that stays behind the `Publisher` seam:
|
|
@@ -129,10 +206,12 @@ No signing or payment code lives in the core — that stays behind the `Publishe
|
|
|
129
206
|
- `objects.ts` — git object construction with SHA-1 envelope hashing: `createGitBlob`, `createGitTree`, `createGitCommit`, `createGitTag` (annotated tags), the `GitObject`/`GitObjectType` types, `hashGitObject`, and the `MAX_OBJECT_SIZE` (95KB) upload guard constant.
|
|
130
207
|
- `nip34-events.ts` — NIP-34 event builders returning `UnsignedEvent` (caller signs and publishes): `buildRepoAnnouncement` (30617), `buildRepoRefs` (30618, incl. `arweave` sha→txId tags), `buildIssue` (1621), `buildComment` (1622), `buildPatch` (1617, optional real `git format-patch` content), `buildStatus` (1630–1633).
|
|
131
208
|
- `repo-reader.ts` — `GitRepoReader`, read-only local-repo access via injection-safe `execFile` git plumbing: `listRefs`, `objectsBetween`(+`WithPaths`), `readObjects`, `statObjects`, `isAncestor`, `formatPatch`, `resolveRef`.
|
|
132
|
-
- `remote-state.ts` — `fetchRemoteState`, the "what does the remote have?" reader: kind:30617/30618 relay fetch (NIP-33 latest-wins across a plural relay list) + `resolveMissing` Arweave GraphQL Git-SHA fallback.
|
|
209
|
+
- `remote-state.ts` — `fetchRemoteState`, the "what does the remote have?" reader: kind:30617/30618 relay fetch (NIP-33 latest-wins across a plural relay list) + `resolveMissing` Arweave GraphQL Git-SHA fallback; `queryRelay` (tolerant NIP-01 REQ→EOSE) is exported for other readers.
|
|
210
|
+
- `object-fetch.ts` / `read-pipeline.ts` / `materialize.ts` (#278) — the read path: `fetchTxBytes`/`downloadGitObjects` (gateway fallback + concurrency cap + SHA-1 verification-as-type-discovery, `ObjectIntegrityError` on mismatch), `referencedShas`/`walkClosure` (object-graph closure, gitlink-aware), `collectRepoObjects` (the clone/fetch collection engine separating fatal gaps from unreachable ones), and `writeGitObject(s)`/`updateRef`/`setHeadSymref` (git plumbing writers with hostile-refname gating).
|
|
211
|
+
- `npub.ts` — dependency-free bech32 `npubToHex`/`hexToNpub`/`ownerToHex` (NIP-19 pubkey addressing for `rig clone`).
|
|
133
212
|
- `publisher.ts` — the `Publisher` interface (paid transport seam): `getFeeRates`, `uploadGitObject`, `publishEvent`. Implemented by the daemon (#227) and the standalone embedded client (#228).
|
|
134
213
|
- `push.ts` — `planPush` (ref classification, object delta minus known sha→txId hints, oversize hard error, fee estimate) and `executePush` (uploads ref tips last, then ONE cumulative kind:30618 merging the full arweave map, kind:30617 first on first push; crash-resume safe via content-addressed skip).
|
|
135
|
-
- `routes.ts` — the JSON wire shapes of the daemon's `/git/*` control routes (bigints as decimal strings, Maps as records) + the matching `serializePushPlan`/`serializePushResult` helpers, shared with `@toon-protocol/client-mcp`
|
|
136
|
-
- `cli/` — the `rig` bin: `init` (#248), `remote` (#249, relays as real git remotes + the shared relay resolution), `push` (#229), the single-event `issue`/`comment`/`pr create`/`pr status` subcommands (#231, nested under `pr` since #250),
|
|
214
|
+
- `routes.ts` — the JSON wire shapes of the daemon's `/git/*` control routes (bigints as decimal strings, Maps as records) + the matching `serializePushPlan`/`serializePushResult` helpers, shared with `@toon-protocol/client-mcp` and consumed by the CLI's #279 delegated fast path (`cli/daemon-session.ts`).
|
|
215
|
+
- `cli/` — the `rig` bin: `init` (#248), `remote` (#249, relays as real git remotes + the shared relay resolution), `push` (#229), the single-event `issue`/`comment`/`pr create`/`pr status` subcommands (#231, nested under `pr` since #250), the git passthrough (#250, `dispatch.ts` + `git-passthrough.ts`), and the #279 daemon-as-accelerator delegation (`daemon-session.ts`), standalone by default with the `identity.ts` resolution chain.
|
|
137
216
|
|
|
138
217
|
Pure builders promoted from the proven Rig E2E seed pipeline (`packages/rig-web/tests/e2e/seed/lib`). Part of [epic #222](https://github.com/toon-protocol/toon-client/issues/222) and [epic #246](https://github.com/toon-protocol/toon-client/issues/246).
|
|
@@ -1,11 +1,113 @@
|
|
|
1
|
+
// src/nip34-events.ts
|
|
1
2
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
3
|
+
ISSUE_KIND,
|
|
4
|
+
PATCH_KIND,
|
|
5
|
+
REPOSITORY_ANNOUNCEMENT_KIND
|
|
6
|
+
} from "@toon-protocol/core/nip34";
|
|
7
|
+
var REPOSITORY_STATE_KIND = 30618;
|
|
8
|
+
var COMMENT_KIND = 1622;
|
|
9
|
+
function buildRepoAnnouncement(repoId, name, description) {
|
|
10
|
+
return {
|
|
11
|
+
kind: REPOSITORY_ANNOUNCEMENT_KIND,
|
|
12
|
+
content: "",
|
|
13
|
+
tags: [
|
|
14
|
+
["d", repoId],
|
|
15
|
+
["name", name],
|
|
16
|
+
["description", description]
|
|
17
|
+
],
|
|
18
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function buildRepoRefs(repoId, refs, arweaveMap = {}) {
|
|
22
|
+
const tags = [["d", repoId]];
|
|
23
|
+
for (const [refPath, commitSha] of Object.entries(refs)) {
|
|
24
|
+
tags.push(["r", refPath, commitSha]);
|
|
25
|
+
}
|
|
26
|
+
const firstRef = Object.keys(refs)[0];
|
|
27
|
+
if (firstRef) {
|
|
28
|
+
tags.push(["HEAD", `ref: ${firstRef}`]);
|
|
29
|
+
}
|
|
30
|
+
for (const [sha, txId] of Object.entries(arweaveMap)) {
|
|
31
|
+
tags.push(["arweave", sha, txId]);
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
kind: REPOSITORY_STATE_KIND,
|
|
35
|
+
content: "",
|
|
36
|
+
tags,
|
|
37
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function buildIssue(repoOwnerPubkey, repoId, title, body, labels = []) {
|
|
41
|
+
const tags = [
|
|
42
|
+
["a", `${REPOSITORY_ANNOUNCEMENT_KIND}:${repoOwnerPubkey}:${repoId}`],
|
|
43
|
+
["p", repoOwnerPubkey],
|
|
44
|
+
["subject", title],
|
|
45
|
+
...labels.map((label) => ["t", label])
|
|
46
|
+
];
|
|
47
|
+
return {
|
|
48
|
+
kind: ISSUE_KIND,
|
|
49
|
+
content: body,
|
|
50
|
+
tags,
|
|
51
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function buildComment(repoOwnerPubkey, repoId, issueOrPrEventId, authorPubkey, body, marker = "reply") {
|
|
55
|
+
return {
|
|
56
|
+
kind: COMMENT_KIND,
|
|
57
|
+
content: body,
|
|
58
|
+
tags: [
|
|
59
|
+
["a", `${REPOSITORY_ANNOUNCEMENT_KIND}:${repoOwnerPubkey}:${repoId}`],
|
|
60
|
+
["e", issueOrPrEventId, "", marker],
|
|
61
|
+
["p", authorPubkey]
|
|
62
|
+
],
|
|
63
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function buildPatch(repoOwnerPubkey, repoId, title, commits, branchTag, content = "", description) {
|
|
67
|
+
const tags = [
|
|
68
|
+
["a", `${REPOSITORY_ANNOUNCEMENT_KIND}:${repoOwnerPubkey}:${repoId}`],
|
|
69
|
+
["p", repoOwnerPubkey],
|
|
70
|
+
["subject", title]
|
|
71
|
+
];
|
|
72
|
+
if (description !== void 0 && description !== "") {
|
|
73
|
+
tags.push(["description", description]);
|
|
74
|
+
}
|
|
75
|
+
for (const commit of commits) {
|
|
76
|
+
tags.push(["commit", commit.sha]);
|
|
77
|
+
tags.push(["parent-commit", commit.parentSha]);
|
|
78
|
+
}
|
|
79
|
+
if (branchTag) {
|
|
80
|
+
tags.push(["t", branchTag]);
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
kind: PATCH_KIND,
|
|
84
|
+
content,
|
|
85
|
+
tags,
|
|
86
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function buildStatus(targetEventId, statusKind, targetPubkey) {
|
|
90
|
+
const tags = [["e", targetEventId]];
|
|
91
|
+
if (targetPubkey) {
|
|
92
|
+
tags.push(["p", targetPubkey]);
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
kind: statusKind,
|
|
96
|
+
content: "",
|
|
97
|
+
tags,
|
|
98
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
99
|
+
};
|
|
100
|
+
}
|
|
4
101
|
|
|
5
102
|
// src/remote-state.ts
|
|
6
103
|
import { decode as decodeToon } from "@toon-format/toon";
|
|
7
104
|
|
|
8
105
|
// ../arweave/dist/gateways.js
|
|
106
|
+
var ARWEAVE_GATEWAYS = [
|
|
107
|
+
"https://ar-io.dev",
|
|
108
|
+
"https://arweave.net",
|
|
109
|
+
"https://permagate.io"
|
|
110
|
+
];
|
|
9
111
|
var ARWEAVE_FETCH_TIMEOUT_MS = 15e3;
|
|
10
112
|
|
|
11
113
|
// ../arweave/dist/git-sha.js
|
|
@@ -89,7 +191,7 @@ async function resolveGitSha(sha, repo) {
|
|
|
89
191
|
}
|
|
90
192
|
|
|
91
193
|
// src/remote-state.ts
|
|
92
|
-
import { REPOSITORY_ANNOUNCEMENT_KIND } from "@toon-protocol/core/nip34";
|
|
194
|
+
import { REPOSITORY_ANNOUNCEMENT_KIND as REPOSITORY_ANNOUNCEMENT_KIND2 } from "@toon-protocol/core/nip34";
|
|
93
195
|
function isValidRelayUrl(url) {
|
|
94
196
|
return /^wss?:\/\//i.test(url);
|
|
95
197
|
}
|
|
@@ -248,7 +350,7 @@ async function fetchRemoteState(options) {
|
|
|
248
350
|
throw new Error("fetchRemoteState: repoId is required");
|
|
249
351
|
}
|
|
250
352
|
const filter = {
|
|
251
|
-
kinds: [
|
|
353
|
+
kinds: [REPOSITORY_ANNOUNCEMENT_KIND2, REPOSITORY_STATE_KIND],
|
|
252
354
|
authors: [ownerPubkey],
|
|
253
355
|
"#d": [repoId]
|
|
254
356
|
};
|
|
@@ -280,7 +382,7 @@ async function fetchRemoteState(options) {
|
|
|
280
382
|
events.filter((e) => e.kind === REPOSITORY_STATE_KIND)
|
|
281
383
|
);
|
|
282
384
|
const announceEvent = latestReplaceable(
|
|
283
|
-
events.filter((e) => e.kind ===
|
|
385
|
+
events.filter((e) => e.kind === REPOSITORY_ANNOUNCEMENT_KIND2)
|
|
284
386
|
);
|
|
285
387
|
const { refs, headSymref, shaToTxId } = refsEvent ? parseRefsEvent(refsEvent) : {
|
|
286
388
|
refs: /* @__PURE__ */ new Map(),
|
|
@@ -340,7 +442,18 @@ async function fetchRemoteState(options) {
|
|
|
340
442
|
}
|
|
341
443
|
|
|
342
444
|
export {
|
|
445
|
+
REPOSITORY_STATE_KIND,
|
|
446
|
+
COMMENT_KIND,
|
|
447
|
+
buildRepoAnnouncement,
|
|
448
|
+
buildRepoRefs,
|
|
449
|
+
buildIssue,
|
|
450
|
+
buildComment,
|
|
451
|
+
buildPatch,
|
|
452
|
+
buildStatus,
|
|
453
|
+
ARWEAVE_GATEWAYS,
|
|
454
|
+
ARWEAVE_FETCH_TIMEOUT_MS,
|
|
455
|
+
isValidArweaveTxId,
|
|
343
456
|
queryRelay,
|
|
344
457
|
fetchRemoteState
|
|
345
458
|
};
|
|
346
|
-
//# sourceMappingURL=chunk-
|
|
459
|
+
//# sourceMappingURL=chunk-JBB7HBQC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/nip34-events.ts","../src/remote-state.ts","../../arweave/src/gateways.ts","../../arweave/src/git-sha.ts"],"sourcesContent":["/**\n * Pure NIP-34 event builders for the Git-to-TOON write path.\n *\n * Promoted from `packages/rig-web/tests/e2e/seed/lib/event-builders.ts` (#223).\n * All builders return UnsignedEvent — the caller signs with their keypair\n * via finalizeEvent() and publishes through a Publisher (#226). Tag\n * structures follow the NIP-34 spec and `@toon-protocol/core/nip34`.\n */\n\nimport {\n ISSUE_KIND,\n PATCH_KIND,\n REPOSITORY_ANNOUNCEMENT_KIND,\n} from '@toon-protocol/core/nip34';\nimport type {\n STATUS_APPLIED_KIND,\n STATUS_CLOSED_KIND,\n STATUS_DRAFT_KIND,\n STATUS_OPEN_KIND,\n} from '@toon-protocol/core/nip34';\n\n// Kinds not (yet) exported by @toon-protocol/core/nip34:\n/** Repository State (refs) — replaceable, pairs with kind:30617 via `d` tag. */\nexport const REPOSITORY_STATE_KIND = 30618;\n/** Comment on an issue or patch (NIP-22 style threading within NIP-34). */\nexport const COMMENT_KIND = 1622;\n\n// ---------------------------------------------------------------------------\n// UnsignedEvent type (subset of nostr-tools — no id, sig, or pubkey)\n// ---------------------------------------------------------------------------\n\nexport interface UnsignedEvent {\n kind: number;\n content: string;\n tags: string[][];\n created_at: number;\n}\n\n// ---------------------------------------------------------------------------\n// kind:30617 — Repository Announcement\n// ---------------------------------------------------------------------------\n\n/**\n * Build a kind:30617 repository announcement event.\n *\n * @param repoId - Repository identifier (d tag)\n * @param name - Human-readable repository name\n * @param description - Repository description\n */\nexport function buildRepoAnnouncement(\n repoId: string,\n name: string,\n description: string\n): UnsignedEvent {\n return {\n kind: REPOSITORY_ANNOUNCEMENT_KIND,\n content: '',\n tags: [\n ['d', repoId],\n ['name', name],\n ['description', description],\n ],\n created_at: Math.floor(Date.now() / 1000),\n };\n}\n\n// ---------------------------------------------------------------------------\n// kind:30618 — Repository Refs/State\n// ---------------------------------------------------------------------------\n\n/**\n * Build a kind:30618 repository refs/state event.\n *\n * @param repoId - Repository identifier (d tag, matches kind:30617)\n * @param refs - Map of ref paths to commit SHAs (e.g., { 'refs/heads/main': 'abc123' })\n * @param arweaveMap - Map of git SHAs to Arweave transaction IDs\n */\nexport function buildRepoRefs(\n repoId: string,\n refs: Record<string, string>,\n arweaveMap: Record<string, string> = {}\n): UnsignedEvent {\n const tags: string[][] = [['d', repoId]];\n\n // Add ref tags\n for (const [refPath, commitSha] of Object.entries(refs)) {\n tags.push(['r', refPath, commitSha]);\n }\n\n // Default HEAD to first ref (typically refs/heads/main)\n const firstRef = Object.keys(refs)[0];\n if (firstRef) {\n tags.push(['HEAD', `ref: ${firstRef}`]);\n }\n\n // Add arweave SHA-to-txId mapping tags\n for (const [sha, txId] of Object.entries(arweaveMap)) {\n tags.push(['arweave', sha, txId]);\n }\n\n return {\n kind: REPOSITORY_STATE_KIND,\n content: '',\n tags,\n created_at: Math.floor(Date.now() / 1000),\n };\n}\n\n// ---------------------------------------------------------------------------\n// kind:1621 — Issue\n// ---------------------------------------------------------------------------\n\n/**\n * Build a kind:1621 issue event.\n *\n * @param repoOwnerPubkey - Pubkey of the repository owner\n * @param repoId - Repository identifier\n * @param title - Issue title (subject tag)\n * @param body - Issue body (Markdown content)\n * @param labels - Optional labels (t tags)\n */\nexport function buildIssue(\n repoOwnerPubkey: string,\n repoId: string,\n title: string,\n body: string,\n labels: string[] = []\n): UnsignedEvent {\n const tags: string[][] = [\n ['a', `${REPOSITORY_ANNOUNCEMENT_KIND}:${repoOwnerPubkey}:${repoId}`],\n ['p', repoOwnerPubkey],\n ['subject', title],\n ...labels.map((label) => ['t', label]),\n ];\n\n return {\n kind: ISSUE_KIND,\n content: body,\n tags,\n created_at: Math.floor(Date.now() / 1000),\n };\n}\n\n// ---------------------------------------------------------------------------\n// kind:1622 — Comment (on issue or PR)\n// ---------------------------------------------------------------------------\n\n/**\n * Build a kind:1622 comment event.\n *\n * @param repoOwnerPubkey - Pubkey of the repository owner\n * @param repoId - Repository identifier\n * @param issueOrPrEventId - Event ID of the issue or PR being commented on\n * @param authorPubkey - Pubkey of the issue/PR author (NIP-34 `p` tag for threading), NOT the comment author\n * @param body - Comment body (Markdown content)\n * @param marker - Event reference marker: 'root' or 'reply' (default: 'reply')\n */\nexport function buildComment(\n repoOwnerPubkey: string,\n repoId: string,\n issueOrPrEventId: string,\n authorPubkey: string,\n body: string,\n marker: 'root' | 'reply' = 'reply'\n): UnsignedEvent {\n return {\n kind: COMMENT_KIND,\n content: body,\n tags: [\n ['a', `${REPOSITORY_ANNOUNCEMENT_KIND}:${repoOwnerPubkey}:${repoId}`],\n ['e', issueOrPrEventId, '', marker],\n ['p', authorPubkey],\n ],\n created_at: Math.floor(Date.now() / 1000),\n };\n}\n\n// ---------------------------------------------------------------------------\n// kind:1617 — Patch / PR\n// ---------------------------------------------------------------------------\n\n/**\n * Build a kind:1617 patch event.\n *\n * The PR body/description travels in a dedicated `description` tag, NEVER in\n * `content` (#280): `content` is real `git format-patch` output that readers\n * pipe straight into `git am`, and git's patch-format detection hard-fails on\n * any leading prose (verified: \"Patch format detection failed.\"). The tag\n * route keeps `git am` consumption intact while `rig pr show` and the\n * rig-web/views `parsePR` renderers surface the description.\n *\n * @param repoOwnerPubkey - Pubkey of the repository owner\n * @param repoId - Repository identifier\n * @param title - Patch/PR title (subject tag)\n * @param commits - Array of { sha, parentSha } for commit and parent-commit tags\n * @param branchTag - Branch name for the t tag\n * @param content - Real `git format-patch` text (NIP-34 patch body); defaults\n * to '' for callers that only reference commits by tag\n * @param description - PR body/cover text (`description` tag) — kept out of\n * `content` so `git am` still applies it\n */\nexport function buildPatch(\n repoOwnerPubkey: string,\n repoId: string,\n title: string,\n commits: { sha: string; parentSha: string }[],\n branchTag?: string,\n content = '',\n description?: string\n): UnsignedEvent {\n const tags: string[][] = [\n ['a', `${REPOSITORY_ANNOUNCEMENT_KIND}:${repoOwnerPubkey}:${repoId}`],\n ['p', repoOwnerPubkey],\n ['subject', title],\n ];\n\n if (description !== undefined && description !== '') {\n tags.push(['description', description]);\n }\n\n for (const commit of commits) {\n tags.push(['commit', commit.sha]);\n tags.push(['parent-commit', commit.parentSha]);\n }\n\n if (branchTag) {\n tags.push(['t', branchTag]);\n }\n\n return {\n kind: PATCH_KIND,\n content,\n tags,\n created_at: Math.floor(Date.now() / 1000),\n };\n}\n\n// ---------------------------------------------------------------------------\n// kind:1630-1633 — Status\n// ---------------------------------------------------------------------------\n\n/** Status kinds: 1630 open, 1631 applied/merged, 1632 closed, 1633 draft. */\nexport type StatusKind =\n | typeof STATUS_OPEN_KIND\n | typeof STATUS_APPLIED_KIND\n | typeof STATUS_CLOSED_KIND\n | typeof STATUS_DRAFT_KIND;\n\n/**\n * Build a status event (kind 1630-1633).\n *\n * @param targetEventId - Event ID of the patch, PR, or issue being updated\n * @param statusKind - One of 1630 (open), 1631 (applied), 1632 (closed), 1633 (draft)\n * @param targetPubkey - Optional pubkey of the target event author (p tag per NIP-34 StatusEvent)\n */\nexport function buildStatus(\n targetEventId: string,\n statusKind: StatusKind,\n targetPubkey?: string\n): UnsignedEvent {\n const tags: string[][] = [['e', targetEventId]];\n if (targetPubkey) {\n tags.push(['p', targetPubkey]);\n }\n return {\n kind: statusKind,\n content: '',\n tags,\n created_at: Math.floor(Date.now() / 1000),\n };\n}\n","/**\n * Remote repository state reader — the \"what does the remote have?\" half of\n * `rig push` (epic #222, ticket #225).\n *\n * Queries relay(s) over NIP-01 WebSocket for the repository's NIP-34 state:\n * - kind:30618 (repository state): `r` tags → ref map, `HEAD` symref,\n * `arweave` tags → git SHA → Arweave txId hints.\n * - kind:30617 (repository announcement): presence = the repo exists on\n * TOON (first-push detection) + name/description/relays metadata.\n *\n * Both kinds are NIP-33 parameterized-replaceable: per relay only the latest\n * event per (kind, author, d) survives, but we still pick the newest across\n * relays (highest created_at, ties broken by lowest id per NIP-01).\n *\n * SHAs missing from the `arweave` tag map can be resolved via the shared\n * Arweave GraphQL Git-SHA resolver (@toon-protocol/arweave — the same module\n * the rig SPA uses) through {@link RemoteState.resolveMissing}.\n *\n * Relay payload encodings (mirrors rig's `web/relay-client.ts` decode logic):\n * EVENT payloads arrive as an inline JSON object (standard NIP-01), as a\n * double-JSON-encoded string (devnet relay quirk: a JSON string containing\n * the event JSON), or as a TOON-encoded string. All three are tolerated.\n */\n\nimport { decode as decodeToon } from '@toon-format/toon';\nimport {\n resolveGitSha,\n seedShaCache,\n shaCacheKey,\n} from '@toon-protocol/arweave';\nimport { REPOSITORY_ANNOUNCEMENT_KIND } from '@toon-protocol/core/nip34';\n\nimport { REPOSITORY_STATE_KIND } from './nip34-events.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** A signed Nostr event as seen on a relay (read side). */\nexport interface NostrEvent {\n id: string;\n pubkey: string;\n created_at: number;\n kind: number;\n tags: string[][];\n content: string;\n sig: string;\n}\n\n/** NIP-01 subscription filter (only the fields rig's readers send). */\nexport interface NostrFilter {\n ids?: string[];\n kinds?: number[];\n authors?: string[];\n '#d'?: string[];\n /** Repo address tag filter, e.g. `30617:<owner>:<repoId>` (#278 tracker). */\n '#a'?: string[];\n /** Event-reference tag filter (#278 tracker: statuses + comments). */\n '#e'?: string[];\n limit?: number;\n}\n\n/**\n * Minimal structural WebSocket type — satisfied by the WHATWG WebSocket\n * global (Node >= 22 / undici, browsers) and by the `ws` package.\n */\nexport interface WebSocketLike {\n readyState: number;\n send(data: string): void;\n close(): void;\n addEventListener(type: string, listener: (event: never) => void): void;\n}\n\n/** Factory for WebSocket connections (injectable for tests / `ws` fallback). */\nexport type WebSocketFactory = (url: string) => WebSocketLike;\n\nexport interface FetchRemoteStateOptions {\n /**\n * Relay WebSocket URLs to query. Plural from day one (forward-compat with\n * multi-relay routing); size 1 is typical today. Results are merged and the\n * newest replaceable event across all relays wins.\n */\n relayUrls: string[];\n /** Repository owner's pubkey (hex) — the author of 30617/30618. */\n ownerPubkey: string;\n /** Repository identifier (NIP-34 `d` tag). */\n repoId: string;\n /** Per-relay timeout in milliseconds (default 10000). On timeout the relay contributes whatever it sent so far. */\n timeoutMs?: number;\n /**\n * Git-SHA → Arweave txId resolver used by {@link RemoteState.resolveMissing}.\n * Defaults to the shared GraphQL resolver from @toon-protocol/arweave;\n * injectable for tests.\n */\n resolveSha?: (sha: string, repo: string) => Promise<string | null>;\n /** WebSocket constructor override (defaults to the global WebSocket). */\n webSocketFactory?: WebSocketFactory;\n}\n\n/** Remote repository state assembled from relay events. */\nexport interface RemoteState {\n /** True when a kind:30617 announcement exists (false ⇒ first push). */\n announced: boolean;\n /** Ref map from the latest kind:30618: refname → commit SHA. */\n refs: Map<string, string>;\n /** HEAD symref target (e.g. `refs/heads/main`), or null if unset. */\n headSymref: string | null;\n /** Git SHA → Arweave txId hints from the latest kind:30618 `arweave` tags. */\n shaToTxId: Map<string, string>;\n /** The latest kind:30618 event, or null if the repo has no state yet. */\n refsEvent: NostrEvent | null;\n /** The latest kind:30617 event, or null if the repo is unannounced. */\n announceEvent: NostrEvent | null;\n /** Repository name from the announcement `name` tag. */\n name: string | null;\n /** Announcement `description` tag (falls back to event content). */\n description: string | null;\n /** Relay URLs advertised in the announcement `relays` tag(s). */\n relays: string[];\n /**\n * Resolve SHAs to Arweave txIds: served from the `arweave` tag map when\n * present, otherwise via the GraphQL Git-SHA resolver. SHAs that resolve\n * nowhere are omitted from the returned map.\n */\n resolveMissing(shas: string[]): Promise<Map<string, string>>;\n}\n\n// ---------------------------------------------------------------------------\n// Relay query (NIP-01 REQ → EVENT* → EOSE)\n// ---------------------------------------------------------------------------\n\n/** Validate that a relay URL uses a WebSocket scheme (mirrors rig's url-utils). */\nfunction isValidRelayUrl(url: string): boolean {\n return /^wss?:\\/\\//i.test(url);\n}\n\n/** WHATWG WebSocket OPEN ready state. */\nconst WS_OPEN = 1;\n\nfunction defaultWebSocketFactory(url: string): WebSocketLike {\n const ctor = (\n globalThis as { WebSocket?: new (url: string) => WebSocketLike }\n ).WebSocket;\n if (!ctor) {\n throw new Error(\n 'No global WebSocket constructor (Node >= 22 required) — pass webSocketFactory'\n );\n }\n return new ctor(url);\n}\n\n/**\n * Decode a relay EVENT payload, tolerating every encoding seen in the wild:\n * inline object (standard NIP-01), double-JSON-encoded string (devnet relay\n * serves the event as a JSON string containing the event JSON), or a\n * TOON-encoded string (rig's `decodeToonMessage` path).\n */\nfunction decodeEventPayload(payload: unknown): NostrEvent | null {\n if (payload !== null && typeof payload === 'object') {\n return payload as NostrEvent;\n }\n if (typeof payload !== 'string') {\n return null;\n }\n try {\n const parsed: unknown = JSON.parse(payload);\n if (parsed !== null && typeof parsed === 'object') {\n return parsed as NostrEvent;\n }\n } catch {\n // Not JSON — fall through to TOON\n }\n try {\n return decodeToon(payload) as unknown as NostrEvent;\n } catch {\n return null;\n }\n}\n\n/**\n * Query one relay: send a REQ, collect EVENTs until EOSE, then CLOSE.\n * Mirrors rig's `queryRelay` (partial results on timeout / early close).\n * Exported for reuse by the network-bootstrap discovery (kind:10032).\n */\nexport function queryRelay(\n relayUrl: string,\n filter: NostrFilter,\n timeoutMs: number,\n webSocketFactory: WebSocketFactory\n): Promise<NostrEvent[]> {\n return new Promise((resolve, reject) => {\n const events: NostrEvent[] = [];\n const subId = `git-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;\n let ws: WebSocketLike;\n // eslint-disable-next-line prefer-const -- assigned after `settle` is defined\n let timeoutHandle: ReturnType<typeof setTimeout>;\n let settled = false;\n\n const settle = (outcome: 'resolve' | 'reject', error?: Error) => {\n if (settled) return;\n settled = true;\n clearTimeout(timeoutHandle);\n try {\n if (ws.readyState === WS_OPEN) {\n ws.send(JSON.stringify(['CLOSE', subId]));\n }\n ws.close();\n } catch {\n // Ignore close errors\n }\n if (outcome === 'reject') {\n reject(error);\n } else {\n resolve(events);\n }\n };\n\n if (!isValidRelayUrl(relayUrl)) {\n reject(\n new Error(\n `Invalid relay URL protocol (must be ws:// or wss://): ${relayUrl}`\n )\n );\n return;\n }\n\n try {\n ws = webSocketFactory(relayUrl);\n } catch (err) {\n reject(new Error(`Failed to connect to relay ${relayUrl}: ${String(err)}`));\n return;\n }\n\n timeoutHandle = setTimeout(() => {\n // Resolve with whatever we collected so far (partial results)\n settle('resolve');\n }, timeoutMs);\n\n ws.addEventListener('open', () => {\n ws.send(JSON.stringify(['REQ', subId, filter]));\n });\n\n ws.addEventListener('message', (msgEvent: { data?: unknown }) => {\n try {\n const msg = JSON.parse(String(msgEvent.data)) as unknown[];\n if (!Array.isArray(msg) || msg.length < 2) return;\n\n const msgType = msg[0];\n if (msgType === 'EVENT' && msg[1] === subId && msg[2] !== undefined) {\n const event = decodeEventPayload(msg[2]);\n if (event) events.push(event);\n } else if (msgType === 'EOSE' && msg[1] === subId) {\n settle('resolve');\n }\n } catch {\n // Ignore parse errors for individual messages\n }\n });\n\n ws.addEventListener('error', (event: { message?: unknown }) => {\n const detail =\n typeof event === 'object' && event !== null && 'message' in event\n ? String(event.message)\n : 'unknown';\n settle(\n 'reject',\n new Error(`WebSocket error connecting to ${relayUrl}: ${detail}`)\n );\n });\n\n ws.addEventListener('close', () => {\n // If we haven't settled yet, resolve with what we have\n settle('resolve');\n });\n });\n}\n\n// ---------------------------------------------------------------------------\n// NIP-33 replaceable selection + tag parsing\n// ---------------------------------------------------------------------------\n\n/**\n * Pick the winning replaceable event: highest created_at, ties broken by\n * lowest id (NIP-01 replaceable-event convention).\n */\nfunction latestReplaceable(events: NostrEvent[]): NostrEvent | null {\n let winner: NostrEvent | null = null;\n for (const event of events) {\n if (\n winner === null ||\n event.created_at > winner.created_at ||\n (event.created_at === winner.created_at && event.id < winner.id)\n ) {\n winner = event;\n }\n }\n return winner;\n}\n\n/** Get the first value for a tag name. */\nfunction getTagValue(tags: string[][], name: string): string | undefined {\n const tag = tags.find((t) => t[0] === name);\n return tag?.[1];\n}\n\n/** Maximum number of refs parsed from a single kind:30618 event (mirrors views). */\nconst MAX_REFS_PER_EVENT = 1000;\n\n/** Symref prefix used in `HEAD` tags: `[\"HEAD\", \"ref: refs/heads/main\"]`. */\nconst SYMREF_PREFIX = 'ref: ';\n\ninterface ParsedRefs {\n refs: Map<string, string>;\n headSymref: string | null;\n shaToTxId: Map<string, string>;\n}\n\n/** Parse a kind:30618 event's `r` / `HEAD` / `arweave` tags. */\nfunction parseRefsEvent(event: NostrEvent): ParsedRefs {\n const refs = new Map<string, string>();\n const shaToTxId = new Map<string, string>();\n let headSymref: string | null = null;\n\n for (const tag of event.tags) {\n const [tagName, v1, v2] = tag;\n if (tagName === 'r' && v1 && v2) {\n if (v1 === 'HEAD' && v2.startsWith(SYMREF_PREFIX)) {\n // Alternate symref spelling: [\"r\", \"HEAD\", \"ref: refs/heads/main\"]\n headSymref = v2.slice(SYMREF_PREFIX.length);\n continue;\n }\n if (refs.size >= MAX_REFS_PER_EVENT) continue;\n refs.set(v1, v2);\n } else if (tagName === 'HEAD' && v1?.startsWith(SYMREF_PREFIX)) {\n // NIP-34 symref tag: [\"HEAD\", \"ref: refs/heads/main\"]\n headSymref = v1.slice(SYMREF_PREFIX.length);\n } else if (tagName === 'arweave' && v1 && v2) {\n shaToTxId.set(v1, v2);\n }\n }\n\n return { refs, headSymref, shaToTxId };\n}\n\n// ---------------------------------------------------------------------------\n// fetchRemoteState\n// ---------------------------------------------------------------------------\n\n/**\n * Fetch the remote repository state from the relay(s).\n *\n * Sends one REQ per relay for kind:30617 + kind:30618 with\n * `authors=[ownerPubkey]`, `#d=[repoId]`, collects until EOSE (or timeout),\n * and reduces to the latest replaceable event per kind.\n *\n * Resolves as long as at least one relay answers; throws only when every\n * relay fails. Events from other authors / repos are ignored (defense\n * against misbehaving relays that over-return).\n */\nexport async function fetchRemoteState(\n options: FetchRemoteStateOptions\n): Promise<RemoteState> {\n const {\n relayUrls,\n ownerPubkey,\n repoId,\n timeoutMs = 10000,\n resolveSha = resolveGitSha,\n webSocketFactory = defaultWebSocketFactory,\n } = options;\n\n if (relayUrls.length === 0) {\n throw new Error('fetchRemoteState: relayUrls must not be empty');\n }\n if (!ownerPubkey) {\n throw new Error('fetchRemoteState: ownerPubkey is required');\n }\n if (!repoId) {\n throw new Error('fetchRemoteState: repoId is required');\n }\n\n const filter: NostrFilter = {\n kinds: [REPOSITORY_ANNOUNCEMENT_KIND, REPOSITORY_STATE_KIND],\n authors: [ownerPubkey],\n '#d': [repoId],\n };\n\n const results = await Promise.allSettled(\n relayUrls.map((url) => queryRelay(url, filter, timeoutMs, webSocketFactory))\n );\n\n const failures: string[] = [];\n const byId = new Map<string, NostrEvent>();\n for (const result of results) {\n if (result.status === 'rejected') {\n failures.push(String((result.reason as Error)?.message ?? result.reason));\n continue;\n }\n for (const event of result.value) {\n // Only trust events from the repo owner for this repo — relays are\n // untrusted and may over-return.\n if (event.pubkey !== ownerPubkey) continue;\n if (getTagValue(event.tags, 'd') !== repoId) continue;\n if (typeof event.id === 'string' && !byId.has(event.id)) {\n byId.set(event.id, event);\n }\n }\n }\n\n if (failures.length === relayUrls.length) {\n throw new Error(\n `fetchRemoteState: all ${relayUrls.length} relay(s) failed: ${failures.join('; ')}`\n );\n }\n\n const events = [...byId.values()];\n const refsEvent = latestReplaceable(\n events.filter((e) => e.kind === REPOSITORY_STATE_KIND)\n );\n const announceEvent = latestReplaceable(\n events.filter((e) => e.kind === REPOSITORY_ANNOUNCEMENT_KIND)\n );\n\n const { refs, headSymref, shaToTxId } = refsEvent\n ? parseRefsEvent(refsEvent)\n : {\n refs: new Map<string, string>(),\n headSymref: null,\n shaToTxId: new Map<string, string>(),\n };\n\n // Seed the shared resolver cache so later resolveGitSha calls (here or in\n // any other consumer of @toon-protocol/arweave) skip GraphQL for known SHAs.\n if (shaToTxId.size > 0) {\n seedShaCache(\n [...shaToTxId].map(\n ([sha, txId]) => [shaCacheKey(sha, repoId), txId] as [string, string]\n )\n );\n }\n\n // Announcement metadata (mirrors views' parseRepoAnnouncement defaults).\n const name = announceEvent\n ? (getTagValue(announceEvent.tags, 'name') ?? null)\n : null;\n const description = announceEvent\n ? (getTagValue(announceEvent.tags, 'description') ?? announceEvent.content)\n : null;\n // NIP-34 announcement relays tag is multi-valued: [\"relays\", url1, url2, …]\n const relays: string[] = [];\n if (announceEvent) {\n for (const tag of announceEvent.tags) {\n if (tag[0] === 'relays') {\n relays.push(...tag.slice(1).filter((url) => url.length > 0));\n }\n }\n }\n\n const resolveMissing = async (\n shas: string[]\n ): Promise<Map<string, string>> => {\n const resolved = new Map<string, string>();\n const missing: string[] = [];\n for (const sha of new Set(shas)) {\n const known = shaToTxId.get(sha);\n if (known !== undefined) {\n resolved.set(sha, known);\n } else {\n missing.push(sha);\n }\n }\n const lookups = await Promise.all(\n missing.map(\n async (sha) => [sha, await resolveSha(sha, repoId)] as const\n )\n );\n for (const [sha, txId] of lookups) {\n if (txId) resolved.set(sha, txId);\n }\n return resolved;\n };\n\n return {\n announced: announceEvent !== null,\n refs,\n headSymref,\n shaToTxId,\n refsEvent,\n announceEvent,\n name,\n description,\n relays,\n resolveMissing,\n };\n}\n","/**\n * Arweave gateway redundancy — single source of truth.\n *\n * Media bytes are content-addressed by Arweave tx id, so every gateway serves\n * the same bytes. This module owns the ordered gateway preference list and the\n * URL helpers used on BOTH sides of the wire:\n * - upload (client-mcp daemon): stamp a primary `url` + `fallback` mirrors.\n * - render (views/rig browser): re-point imeta URLs + fail over on error.\n *\n * Previously hand-duplicated in `views`, `rig`, and `client-mcp`; those now all\n * import from here. The default list can be overridden per call (e.g. from a\n * daemon env var) — pass `gateways` to the helpers.\n */\n\n/** Ordered Arweave gateways to try (primary first, then fallbacks). */\nexport const ARWEAVE_GATEWAYS = [\n 'https://ar-io.dev',\n 'https://arweave.net',\n 'https://permagate.io',\n] as const;\n\n/** Timeout for individual Arweave fetch requests in milliseconds. */\nexport const ARWEAVE_FETCH_TIMEOUT_MS = 15000;\n\n/** Arweave transaction IDs are 43-character base64url strings. */\nconst TX_ID_RE = /^[a-zA-Z0-9_-]{43}$/;\n\n/** Hosts recognized as Arweave gateways (path-addressed). */\nconst ARWEAVE_HOST_RE =\n /(^|\\.)(arweave\\.net|ar-io\\.dev|permagate\\.io|g8way\\.io|ar\\.io)$/i;\n\n/**\n * Extract an Arweave tx id from a media URL, or null if it is not\n * Arweave-addressable. Handles `ar://<txid>` and path-style\n * `https://<gateway>/<txid>`.\n *\n * Sandbox-subdomain URLs (`https://<txid>.<gateway>`) are deliberately NOT\n * decoded: tx ids are case-sensitive base64url, but `URL` (and DNS) lower-case\n * the hostname, which would corrupt the id. Real gateway sandboxing uses a\n * base32 label, not the raw id — the canonical id always travels in the path.\n */\nexport function arweaveTxId(rawUrl: string): string | null {\n const ar = /^ar:\\/\\/([a-zA-Z0-9_-]{43})(?:[/?#]|$)/.exec(rawUrl);\n if (ar?.[1]) return ar[1];\n\n let u: URL;\n try {\n u = new URL(rawUrl);\n } catch {\n return null;\n }\n // Only re-point hosts we actually recognize as Arweave gateways, so a stray\n // 43-char path segment on some other CDN is never misread as a tx id.\n if (!ARWEAVE_HOST_RE.test(u.hostname)) return null;\n\n // Path style: https://arweave.net/<txid>\n const seg = u.pathname.split('/').find(Boolean);\n if (seg && TX_ID_RE.test(seg)) return seg;\n\n return null;\n}\n\n/**\n * Primary URL + fallback mirror URLs for an Arweave tx id, one per gateway in\n * preference order. Used by the upload path to stamp `imeta` `url` + `fallback`.\n */\nexport function arweaveUrls(\n txId: string,\n gateways: readonly string[] = ARWEAVE_GATEWAYS\n): { url: string; fallbacks: string[] } {\n const all = (gateways.length ? gateways : ARWEAVE_GATEWAYS).map(\n (g) => `${g}/${txId}`\n );\n const [url, ...fallbacks] = all;\n return { url: url ?? `${ARWEAVE_GATEWAYS[0]}/${txId}`, fallbacks };\n}\n\n/**\n * Ordered candidate URLs for a media URL. Arweave-addressable URLs expand to the\n * full gateway-preference list (primary first); anything else is returned\n * unchanged. `extraFallbacks` (e.g. publisher-supplied `imeta` mirrors) are\n * appended last, de-duplicated. Used by the render path to fail over on error.\n */\nexport function arweaveGatewayCandidates(\n rawUrl: string,\n extraFallbacks: string[] = [],\n gateways: readonly string[] = ARWEAVE_GATEWAYS\n): string[] {\n const txId = arweaveTxId(rawUrl);\n const candidates = txId\n ? (gateways.length ? gateways : ARWEAVE_GATEWAYS).map((g) => `${g}/${txId}`)\n : [rawUrl];\n const seen = new Set(candidates);\n for (const f of extraFallbacks) {\n if (f && !seen.has(f)) {\n seen.add(f);\n candidates.push(f);\n }\n }\n return candidates;\n}\n","/**\n * Git-SHA → Arweave transaction ID resolution.\n *\n * Git objects uploaded to Arweave are tagged with `Git-SHA` (the object's\n * SHA-1) and `Repo` (the repository identifier, matching the NIP-34 `d` tag).\n * This module resolves a git SHA to its Arweave tx id via the Arweave GraphQL\n * gateway, with a bounded in-memory cache that can be pre-seeded from relay\n * state (kind:30618 `arweave` tags) to skip the GraphQL indexing delay.\n *\n * Extracted verbatim from rig's `web/arweave-client.ts` (#225) so the browser\n * SPA (rig-web) and the Node write path (@toon-protocol/rig) share ONE resolver.\n * Uses only WHATWG fetch + AbortSignal.timeout — browser and Node compatible.\n */\n\nimport { ARWEAVE_FETCH_TIMEOUT_MS } from './gateways.js';\n\n/** Arweave GraphQL endpoint used for Git-SHA tag lookups. */\nconst ARWEAVE_GRAPHQL_URL = 'https://arweave.net/graphql';\n\n/** Maximum number of entries in the SHA-to-txId cache to prevent unbounded memory growth. */\nconst SHA_CACHE_MAX_SIZE = 10000;\n\n/** In-memory cache for SHA-to-txId resolution. Bounded to prevent memory leaks. */\nconst shaToTxIdCache = new Map<string, string>();\n\n/**\n * Validate a git SHA-1 hash format (40-character hex string).\n */\nfunction isValidGitSha(sha: string): boolean {\n return /^[0-9a-f]{40}$/i.test(sha);\n}\n\n/**\n * Sanitize a string for safe inclusion in a GraphQL query.\n * Removes characters that could break out of a GraphQL string literal,\n * including backticks which some GraphQL parsers may interpret.\n */\nfunction sanitizeGraphQLValue(value: string): string {\n // eslint-disable-next-line no-control-regex -- intentional: strip control chars for GraphQL safety\n return value.replace(/[\"\\\\\\n\\r\\u0000-\\u001f`]/g, '');\n}\n\n/**\n * Build the cache key used by {@link resolveGitSha} / {@link seedShaCache}.\n *\n * The cache is keyed on `\"sha:repo\"` so the same SHA in different repos\n * resolves independently (uploads are tagged per-repo).\n */\nexport function shaCacheKey(sha: string, repo: string): string {\n return `${sha}:${repo}`;\n}\n\n/**\n * Clear the SHA-to-txId cache. Used for test isolation.\n */\nexport function clearShaCache(): void {\n shaToTxIdCache.clear();\n}\n\n/**\n * Pre-seed the SHA-to-txId cache with known mappings.\n *\n * Used when txId mappings are available from relay events (e.g., kind:30618\n * `arweave` tags) to avoid the GraphQL indexing delay after Turbo/Irys uploads.\n *\n * @param mappings - Map of \"sha:repo\" cache keys to Arweave transaction IDs\n */\nexport function seedShaCache(\n mappings: Map<string, string> | [string, string][]\n): void {\n const entries = mappings instanceof Map ? mappings.entries() : mappings;\n for (const [key, txId] of entries) {\n if (shaToTxIdCache.size >= SHA_CACHE_MAX_SIZE) {\n const firstKey = shaToTxIdCache.keys().next().value;\n if (firstKey !== undefined) {\n shaToTxIdCache.delete(firstKey);\n }\n }\n shaToTxIdCache.set(key, txId);\n }\n}\n\n/** Arweave transaction IDs are 43-character base64url strings. */\nconst ARWEAVE_TX_ID_RE = /^[a-zA-Z0-9_-]{43}$/;\n\n/**\n * Validate an Arweave transaction ID format.\n * Arweave tx IDs are 43-character base64url-encoded strings.\n */\nexport function isValidArweaveTxId(txId: string): boolean {\n return ARWEAVE_TX_ID_RE.test(txId);\n}\n\n/**\n * Resolve a git SHA to an Arweave transaction ID via GraphQL.\n *\n * Queries the Arweave GraphQL endpoint for transactions tagged with\n * the given Git-SHA and Repo values. Results are cached in-memory.\n *\n * @param sha - Git object SHA-1 hash (hex)\n * @param repo - Repository identifier (matches d tag)\n * @returns Arweave transaction ID, or null if not found\n */\nexport async function resolveGitSha(\n sha: string,\n repo: string\n): Promise<string | null> {\n // Validate SHA format to prevent injection of arbitrary strings into GraphQL\n if (!isValidGitSha(sha)) {\n return null;\n }\n\n const cacheKey = shaCacheKey(sha, repo);\n const cached = shaToTxIdCache.get(cacheKey);\n if (cached !== undefined) {\n return cached;\n }\n\n const safeSha = sanitizeGraphQLValue(sha);\n const safeRepo = sanitizeGraphQLValue(repo);\n const query = `query {\n transactions(tags: [\n { name: \"Git-SHA\", values: [\"${safeSha}\"] },\n { name: \"Repo\", values: [\"${safeRepo}\"] }\n ]) {\n edges { node { id } }\n }\n}`;\n\n try {\n const response = await fetch(ARWEAVE_GRAPHQL_URL, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ query }),\n signal: AbortSignal.timeout(ARWEAVE_FETCH_TIMEOUT_MS),\n });\n\n if (!response.ok) {\n return null;\n }\n\n const json = (await response.json()) as {\n data?: {\n transactions?: {\n edges?: { node?: { id?: string } }[];\n };\n };\n };\n\n const edges = json.data?.transactions?.edges;\n if (!edges || edges.length === 0) {\n return null;\n }\n\n const txId = edges[0]?.node?.id;\n if (!txId || !isValidArweaveTxId(txId)) {\n return null;\n }\n\n // Evict oldest entries if cache exceeds max size\n if (shaToTxIdCache.size >= SHA_CACHE_MAX_SIZE) {\n const firstKey = shaToTxIdCache.keys().next().value;\n if (firstKey !== undefined) {\n shaToTxIdCache.delete(firstKey);\n }\n }\n shaToTxIdCache.set(cacheKey, txId);\n return txId;\n } catch {\n return null;\n }\n}\n"],"mappings":";AASA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAUA,IAAM,wBAAwB;AAE9B,IAAM,eAAe;AAwBrB,SAAS,sBACd,QACA,MACA,aACe;AACf,SAAO;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,IACT,MAAM;AAAA,MACJ,CAAC,KAAK,MAAM;AAAA,MACZ,CAAC,QAAQ,IAAI;AAAA,MACb,CAAC,eAAe,WAAW;AAAA,IAC7B;AAAA,IACA,YAAY,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAAA,EAC1C;AACF;AAaO,SAAS,cACd,QACA,MACA,aAAqC,CAAC,GACvB;AACf,QAAM,OAAmB,CAAC,CAAC,KAAK,MAAM,CAAC;AAGvC,aAAW,CAAC,SAAS,SAAS,KAAK,OAAO,QAAQ,IAAI,GAAG;AACvD,SAAK,KAAK,CAAC,KAAK,SAAS,SAAS,CAAC;AAAA,EACrC;AAGA,QAAM,WAAW,OAAO,KAAK,IAAI,EAAE,CAAC;AACpC,MAAI,UAAU;AACZ,SAAK,KAAK,CAAC,QAAQ,QAAQ,QAAQ,EAAE,CAAC;AAAA,EACxC;AAGA,aAAW,CAAC,KAAK,IAAI,KAAK,OAAO,QAAQ,UAAU,GAAG;AACpD,SAAK,KAAK,CAAC,WAAW,KAAK,IAAI,CAAC;AAAA,EAClC;AAEA,SAAO;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,IACT;AAAA,IACA,YAAY,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAAA,EAC1C;AACF;AAeO,SAAS,WACd,iBACA,QACA,OACA,MACA,SAAmB,CAAC,GACL;AACf,QAAM,OAAmB;AAAA,IACvB,CAAC,KAAK,GAAG,4BAA4B,IAAI,eAAe,IAAI,MAAM,EAAE;AAAA,IACpE,CAAC,KAAK,eAAe;AAAA,IACrB,CAAC,WAAW,KAAK;AAAA,IACjB,GAAG,OAAO,IAAI,CAAC,UAAU,CAAC,KAAK,KAAK,CAAC;AAAA,EACvC;AAEA,SAAO;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,IACT;AAAA,IACA,YAAY,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAAA,EAC1C;AACF;AAgBO,SAAS,aACd,iBACA,QACA,kBACA,cACA,MACA,SAA2B,SACZ;AACf,SAAO;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,IACT,MAAM;AAAA,MACJ,CAAC,KAAK,GAAG,4BAA4B,IAAI,eAAe,IAAI,MAAM,EAAE;AAAA,MACpE,CAAC,KAAK,kBAAkB,IAAI,MAAM;AAAA,MAClC,CAAC,KAAK,YAAY;AAAA,IACpB;AAAA,IACA,YAAY,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAAA,EAC1C;AACF;AA0BO,SAAS,WACd,iBACA,QACA,OACA,SACA,WACA,UAAU,IACV,aACe;AACf,QAAM,OAAmB;AAAA,IACvB,CAAC,KAAK,GAAG,4BAA4B,IAAI,eAAe,IAAI,MAAM,EAAE;AAAA,IACpE,CAAC,KAAK,eAAe;AAAA,IACrB,CAAC,WAAW,KAAK;AAAA,EACnB;AAEA,MAAI,gBAAgB,UAAa,gBAAgB,IAAI;AACnD,SAAK,KAAK,CAAC,eAAe,WAAW,CAAC;AAAA,EACxC;AAEA,aAAW,UAAU,SAAS;AAC5B,SAAK,KAAK,CAAC,UAAU,OAAO,GAAG,CAAC;AAChC,SAAK,KAAK,CAAC,iBAAiB,OAAO,SAAS,CAAC;AAAA,EAC/C;AAEA,MAAI,WAAW;AACb,SAAK,KAAK,CAAC,KAAK,SAAS,CAAC;AAAA,EAC5B;AAEA,SAAO;AAAA,IACL,MAAM;AAAA,IACN;AAAA,IACA;AAAA,IACA,YAAY,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAAA,EAC1C;AACF;AAoBO,SAAS,YACd,eACA,YACA,cACe;AACf,QAAM,OAAmB,CAAC,CAAC,KAAK,aAAa,CAAC;AAC9C,MAAI,cAAc;AAChB,SAAK,KAAK,CAAC,KAAK,YAAY,CAAC;AAAA,EAC/B;AACA,SAAO;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,IACT;AAAA,IACA,YAAY,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAAA,EAC1C;AACF;;;ACtPA,SAAS,UAAU,kBAAkB;;;ACT9B,IAAM,mBAAmB;EAC9B;EACA;EACA;;AAIK,IAAM,2BAA2B;;;ACLxC,IAAM,sBAAsB;AAG5B,IAAM,qBAAqB;AAG3B,IAAM,iBAAiB,oBAAI,IAAG;AAK9B,SAAS,cAAc,KAAW;AAChC,SAAO,kBAAkB,KAAK,GAAG;AACnC;AAOA,SAAS,qBAAqB,OAAa;AAEzC,SAAO,MAAM,QAAQ,4BAA4B,EAAE;AACrD;AAQM,SAAU,YAAY,KAAa,MAAY;AACnD,SAAO,GAAG,GAAG,IAAI,IAAI;AACvB;AAiBM,SAAU,aACd,UAAkD;AAElD,QAAM,UAAU,oBAAoB,MAAM,SAAS,QAAO,IAAK;AAC/D,aAAW,CAAC,KAAK,IAAI,KAAK,SAAS;AACjC,QAAI,eAAe,QAAQ,oBAAoB;AAC7C,YAAM,WAAW,eAAe,KAAI,EAAG,KAAI,EAAG;AAC9C,UAAI,aAAa,QAAW;AAC1B,uBAAe,OAAO,QAAQ;MAChC;IACF;AACA,mBAAe,IAAI,KAAK,IAAI;EAC9B;AACF;AAGA,IAAM,mBAAmB;AAMnB,SAAU,mBAAmB,MAAY;AAC7C,SAAO,iBAAiB,KAAK,IAAI;AACnC;AAYA,eAAsB,cACpB,KACA,MAAY;AAGZ,MAAI,CAAC,cAAc,GAAG,GAAG;AACvB,WAAO;EACT;AAEA,QAAM,WAAW,YAAY,KAAK,IAAI;AACtC,QAAM,SAAS,eAAe,IAAI,QAAQ;AAC1C,MAAI,WAAW,QAAW;AACxB,WAAO;EACT;AAEA,QAAM,UAAU,qBAAqB,GAAG;AACxC,QAAM,WAAW,qBAAqB,IAAI;AAC1C,QAAM,QAAQ;;mCAEmB,OAAO;gCACV,QAAQ;;;;;AAMtC,MAAI;AACF,UAAM,WAAW,MAAM,MAAM,qBAAqB;MAChD,QAAQ;MACR,SAAS,EAAE,gBAAgB,mBAAkB;MAC7C,MAAM,KAAK,UAAU,EAAE,MAAK,CAAE;MAC9B,QAAQ,YAAY,QAAQ,wBAAwB;KACrD;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,aAAO;IACT;AAEA,UAAM,OAAQ,MAAM,SAAS,KAAI;AAQjC,UAAM,QAAQ,KAAK,MAAM,cAAc;AACvC,QAAI,CAAC,SAAS,MAAM,WAAW,GAAG;AAChC,aAAO;IACT;AAEA,UAAM,OAAO,MAAM,CAAC,GAAG,MAAM;AAC7B,QAAI,CAAC,QAAQ,CAAC,mBAAmB,IAAI,GAAG;AACtC,aAAO;IACT;AAGA,QAAI,eAAe,QAAQ,oBAAoB;AAC7C,YAAM,WAAW,eAAe,KAAI,EAAG,KAAI,EAAG;AAC9C,UAAI,aAAa,QAAW;AAC1B,uBAAe,OAAO,QAAQ;MAChC;IACF;AACA,mBAAe,IAAI,UAAU,IAAI;AACjC,WAAO;EACT,QAAQ;AACN,WAAO;EACT;AACF;;;AF7IA,SAAS,gCAAAA,qCAAoC;AAsG7C,SAAS,gBAAgB,KAAsB;AAC7C,SAAO,cAAc,KAAK,GAAG;AAC/B;AAGA,IAAM,UAAU;AAEhB,SAAS,wBAAwB,KAA4B;AAC3D,QAAM,OACJ,WACA;AACF,MAAI,CAAC,MAAM;AACT,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,SAAO,IAAI,KAAK,GAAG;AACrB;AAQA,SAAS,mBAAmB,SAAqC;AAC/D,MAAI,YAAY,QAAQ,OAAO,YAAY,UAAU;AACnD,WAAO;AAAA,EACT;AACA,MAAI,OAAO,YAAY,UAAU;AAC/B,WAAO;AAAA,EACT;AACA,MAAI;AACF,UAAM,SAAkB,KAAK,MAAM,OAAO;AAC1C,QAAI,WAAW,QAAQ,OAAO,WAAW,UAAU;AACjD,aAAO;AAAA,IACT;AAAA,EACF,QAAQ;AAAA,EAER;AACA,MAAI;AACF,WAAO,WAAW,OAAO;AAAA,EAC3B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAOO,SAAS,WACd,UACA,QACA,WACA,kBACuB;AACvB,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,SAAuB,CAAC;AAC9B,UAAM,QAAQ,OAAO,KAAK,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,CAAC,CAAC;AACzE,QAAI;AAEJ,QAAI;AACJ,QAAI,UAAU;AAEd,UAAM,SAAS,CAAC,SAA+B,UAAkB;AAC/D,UAAI,QAAS;AACb,gBAAU;AACV,mBAAa,aAAa;AAC1B,UAAI;AACF,YAAI,GAAG,eAAe,SAAS;AAC7B,aAAG,KAAK,KAAK,UAAU,CAAC,SAAS,KAAK,CAAC,CAAC;AAAA,QAC1C;AACA,WAAG,MAAM;AAAA,MACX,QAAQ;AAAA,MAER;AACA,UAAI,YAAY,UAAU;AACxB,eAAO,KAAK;AAAA,MACd,OAAO;AACL,gBAAQ,MAAM;AAAA,MAChB;AAAA,IACF;AAEA,QAAI,CAAC,gBAAgB,QAAQ,GAAG;AAC9B;AAAA,QACE,IAAI;AAAA,UACF,yDAAyD,QAAQ;AAAA,QACnE;AAAA,MACF;AACA;AAAA,IACF;AAEA,QAAI;AACF,WAAK,iBAAiB,QAAQ;AAAA,IAChC,SAAS,KAAK;AACZ,aAAO,IAAI,MAAM,8BAA8B,QAAQ,KAAK,OAAO,GAAG,CAAC,EAAE,CAAC;AAC1E;AAAA,IACF;AAEA,oBAAgB,WAAW,MAAM;AAE/B,aAAO,SAAS;AAAA,IAClB,GAAG,SAAS;AAEZ,OAAG,iBAAiB,QAAQ,MAAM;AAChC,SAAG,KAAK,KAAK,UAAU,CAAC,OAAO,OAAO,MAAM,CAAC,CAAC;AAAA,IAChD,CAAC;AAED,OAAG,iBAAiB,WAAW,CAAC,aAAiC;AAC/D,UAAI;AACF,cAAM,MAAM,KAAK,MAAM,OAAO,SAAS,IAAI,CAAC;AAC5C,YAAI,CAAC,MAAM,QAAQ,GAAG,KAAK,IAAI,SAAS,EAAG;AAE3C,cAAM,UAAU,IAAI,CAAC;AACrB,YAAI,YAAY,WAAW,IAAI,CAAC,MAAM,SAAS,IAAI,CAAC,MAAM,QAAW;AACnE,gBAAM,QAAQ,mBAAmB,IAAI,CAAC,CAAC;AACvC,cAAI,MAAO,QAAO,KAAK,KAAK;AAAA,QAC9B,WAAW,YAAY,UAAU,IAAI,CAAC,MAAM,OAAO;AACjD,iBAAO,SAAS;AAAA,QAClB;AAAA,MACF,QAAQ;AAAA,MAER;AAAA,IACF,CAAC;AAED,OAAG,iBAAiB,SAAS,CAAC,UAAiC;AAC7D,YAAM,SACJ,OAAO,UAAU,YAAY,UAAU,QAAQ,aAAa,QACxD,OAAO,MAAM,OAAO,IACpB;AACN;AAAA,QACE;AAAA,QACA,IAAI,MAAM,iCAAiC,QAAQ,KAAK,MAAM,EAAE;AAAA,MAClE;AAAA,IACF,CAAC;AAED,OAAG,iBAAiB,SAAS,MAAM;AAEjC,aAAO,SAAS;AAAA,IAClB,CAAC;AAAA,EACH,CAAC;AACH;AAUA,SAAS,kBAAkB,QAAyC;AAClE,MAAI,SAA4B;AAChC,aAAW,SAAS,QAAQ;AAC1B,QACE,WAAW,QACX,MAAM,aAAa,OAAO,cACzB,MAAM,eAAe,OAAO,cAAc,MAAM,KAAK,OAAO,IAC7D;AACA,eAAS;AAAA,IACX;AAAA,EACF;AACA,SAAO;AACT;AAGA,SAAS,YAAY,MAAkB,MAAkC;AACvE,QAAM,MAAM,KAAK,KAAK,CAAC,MAAM,EAAE,CAAC,MAAM,IAAI;AAC1C,SAAO,MAAM,CAAC;AAChB;AAGA,IAAM,qBAAqB;AAG3B,IAAM,gBAAgB;AAStB,SAAS,eAAe,OAA+B;AACrD,QAAM,OAAO,oBAAI,IAAoB;AACrC,QAAM,YAAY,oBAAI,IAAoB;AAC1C,MAAI,aAA4B;AAEhC,aAAW,OAAO,MAAM,MAAM;AAC5B,UAAM,CAAC,SAAS,IAAI,EAAE,IAAI;AAC1B,QAAI,YAAY,OAAO,MAAM,IAAI;AAC/B,UAAI,OAAO,UAAU,GAAG,WAAW,aAAa,GAAG;AAEjD,qBAAa,GAAG,MAAM,cAAc,MAAM;AAC1C;AAAA,MACF;AACA,UAAI,KAAK,QAAQ,mBAAoB;AACrC,WAAK,IAAI,IAAI,EAAE;AAAA,IACjB,WAAW,YAAY,UAAU,IAAI,WAAW,aAAa,GAAG;AAE9D,mBAAa,GAAG,MAAM,cAAc,MAAM;AAAA,IAC5C,WAAW,YAAY,aAAa,MAAM,IAAI;AAC5C,gBAAU,IAAI,IAAI,EAAE;AAAA,IACtB;AAAA,EACF;AAEA,SAAO,EAAE,MAAM,YAAY,UAAU;AACvC;AAiBA,eAAsB,iBACpB,SACsB;AACtB,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA,YAAY;AAAA,IACZ,aAAa;AAAA,IACb,mBAAmB;AAAA,EACrB,IAAI;AAEJ,MAAI,UAAU,WAAW,GAAG;AAC1B,UAAM,IAAI,MAAM,+CAA+C;AAAA,EACjE;AACA,MAAI,CAAC,aAAa;AAChB,UAAM,IAAI,MAAM,2CAA2C;AAAA,EAC7D;AACA,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,MAAM,sCAAsC;AAAA,EACxD;AAEA,QAAM,SAAsB;AAAA,IAC1B,OAAO,CAACC,+BAA8B,qBAAqB;AAAA,IAC3D,SAAS,CAAC,WAAW;AAAA,IACrB,MAAM,CAAC,MAAM;AAAA,EACf;AAEA,QAAM,UAAU,MAAM,QAAQ;AAAA,IAC5B,UAAU,IAAI,CAAC,QAAQ,WAAW,KAAK,QAAQ,WAAW,gBAAgB,CAAC;AAAA,EAC7E;AAEA,QAAM,WAAqB,CAAC;AAC5B,QAAM,OAAO,oBAAI,IAAwB;AACzC,aAAW,UAAU,SAAS;AAC5B,QAAI,OAAO,WAAW,YAAY;AAChC,eAAS,KAAK,OAAQ,OAAO,QAAkB,WAAW,OAAO,MAAM,CAAC;AACxE;AAAA,IACF;AACA,eAAW,SAAS,OAAO,OAAO;AAGhC,UAAI,MAAM,WAAW,YAAa;AAClC,UAAI,YAAY,MAAM,MAAM,GAAG,MAAM,OAAQ;AAC7C,UAAI,OAAO,MAAM,OAAO,YAAY,CAAC,KAAK,IAAI,MAAM,EAAE,GAAG;AACvD,aAAK,IAAI,MAAM,IAAI,KAAK;AAAA,MAC1B;AAAA,IACF;AAAA,EACF;AAEA,MAAI,SAAS,WAAW,UAAU,QAAQ;AACxC,UAAM,IAAI;AAAA,MACR,yBAAyB,UAAU,MAAM,qBAAqB,SAAS,KAAK,IAAI,CAAC;AAAA,IACnF;AAAA,EACF;AAEA,QAAM,SAAS,CAAC,GAAG,KAAK,OAAO,CAAC;AAChC,QAAM,YAAY;AAAA,IAChB,OAAO,OAAO,CAAC,MAAM,EAAE,SAAS,qBAAqB;AAAA,EACvD;AACA,QAAM,gBAAgB;AAAA,IACpB,OAAO,OAAO,CAAC,MAAM,EAAE,SAASA,6BAA4B;AAAA,EAC9D;AAEA,QAAM,EAAE,MAAM,YAAY,UAAU,IAAI,YACpC,eAAe,SAAS,IACxB;AAAA,IACE,MAAM,oBAAI,IAAoB;AAAA,IAC9B,YAAY;AAAA,IACZ,WAAW,oBAAI,IAAoB;AAAA,EACrC;AAIJ,MAAI,UAAU,OAAO,GAAG;AACtB;AAAA,MACE,CAAC,GAAG,SAAS,EAAE;AAAA,QACb,CAAC,CAAC,KAAK,IAAI,MAAM,CAAC,YAAY,KAAK,MAAM,GAAG,IAAI;AAAA,MAClD;AAAA,IACF;AAAA,EACF;AAGA,QAAM,OAAO,gBACR,YAAY,cAAc,MAAM,MAAM,KAAK,OAC5C;AACJ,QAAM,cAAc,gBACf,YAAY,cAAc,MAAM,aAAa,KAAK,cAAc,UACjE;AAEJ,QAAM,SAAmB,CAAC;AAC1B,MAAI,eAAe;AACjB,eAAW,OAAO,cAAc,MAAM;AACpC,UAAI,IAAI,CAAC,MAAM,UAAU;AACvB,eAAO,KAAK,GAAG,IAAI,MAAM,CAAC,EAAE,OAAO,CAAC,QAAQ,IAAI,SAAS,CAAC,CAAC;AAAA,MAC7D;AAAA,IACF;AAAA,EACF;AAEA,QAAM,iBAAiB,OACrB,SACiC;AACjC,UAAM,WAAW,oBAAI,IAAoB;AACzC,UAAM,UAAoB,CAAC;AAC3B,eAAW,OAAO,IAAI,IAAI,IAAI,GAAG;AAC/B,YAAM,QAAQ,UAAU,IAAI,GAAG;AAC/B,UAAI,UAAU,QAAW;AACvB,iBAAS,IAAI,KAAK,KAAK;AAAA,MACzB,OAAO;AACL,gBAAQ,KAAK,GAAG;AAAA,MAClB;AAAA,IACF;AACA,UAAM,UAAU,MAAM,QAAQ;AAAA,MAC5B,QAAQ;AAAA,QACN,OAAO,QAAQ,CAAC,KAAK,MAAM,WAAW,KAAK,MAAM,CAAC;AAAA,MACpD;AAAA,IACF;AACA,eAAW,CAAC,KAAK,IAAI,KAAK,SAAS;AACjC,UAAI,KAAM,UAAS,IAAI,KAAK,IAAI;AAAA,IAClC;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,WAAW,kBAAkB;AAAA,IAC7B;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":["REPOSITORY_ANNOUNCEMENT_KIND","REPOSITORY_ANNOUNCEMENT_KIND"]}
|
|
@@ -1,144 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
|
+
NonceLock,
|
|
3
|
+
checkDaemonIdentity,
|
|
2
4
|
recordKey
|
|
3
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-SW7ZHMGS.js";
|
|
4
6
|
import {
|
|
5
7
|
MAX_OBJECT_SIZE
|
|
6
8
|
} from "./chunk-X2CZPPDM.js";
|
|
7
9
|
|
|
8
|
-
// src/standalone/nonce-guard.ts
|
|
9
|
-
import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
10
|
-
import { homedir } from "os";
|
|
11
|
-
import { join } from "path";
|
|
12
|
-
var DEFAULT_DAEMON_PORT = 8787;
|
|
13
|
-
function defaultDaemonPort() {
|
|
14
|
-
const env = process.env["TOON_CLIENT_HTTP_PORT"];
|
|
15
|
-
const parsed = env ? Number(env) : NaN;
|
|
16
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_DAEMON_PORT;
|
|
17
|
-
}
|
|
18
|
-
function defaultLockDir() {
|
|
19
|
-
return process.env["TOON_CLIENT_HOME"] ?? join(homedir(), ".toon-client");
|
|
20
|
-
}
|
|
21
|
-
var DaemonIdentityConflictError = class extends Error {
|
|
22
|
-
constructor(pubkey, daemonUrl) {
|
|
23
|
-
super(
|
|
24
|
-
`toon-clientd is running with this identity (${pubkey.slice(0, 8)}\u2026) at ${daemonUrl} \u2014 use daemon mode or stop the daemon. Two writers on one identity would race the payment channel's cumulative-claim watermark (double-charge hazard).`
|
|
25
|
-
);
|
|
26
|
-
this.pubkey = pubkey;
|
|
27
|
-
this.daemonUrl = daemonUrl;
|
|
28
|
-
this.name = "DaemonIdentityConflictError";
|
|
29
|
-
}
|
|
30
|
-
pubkey;
|
|
31
|
-
daemonUrl;
|
|
32
|
-
};
|
|
33
|
-
var StandaloneLockError = class extends Error {
|
|
34
|
-
constructor(pubkey, lockPath, holderPid) {
|
|
35
|
-
super(
|
|
36
|
-
`another standalone process (pid ${holderPid}) already holds the payment-channel lock for identity ${pubkey.slice(0, 8)}\u2026 (${lockPath}) \u2014 wait for it to finish or stop it. Two writers on one identity would race the cumulative-claim watermark.`
|
|
37
|
-
);
|
|
38
|
-
this.pubkey = pubkey;
|
|
39
|
-
this.lockPath = lockPath;
|
|
40
|
-
this.holderPid = holderPid;
|
|
41
|
-
this.name = "StandaloneLockError";
|
|
42
|
-
}
|
|
43
|
-
pubkey;
|
|
44
|
-
lockPath;
|
|
45
|
-
holderPid;
|
|
46
|
-
};
|
|
47
|
-
async function checkDaemonIdentity(pubkey, options = {}) {
|
|
48
|
-
const port = options.port ?? defaultDaemonPort();
|
|
49
|
-
const fetchImpl = options.fetchImpl ?? fetch;
|
|
50
|
-
const url = `http://127.0.0.1:${port}/status`;
|
|
51
|
-
let daemonPubkey;
|
|
52
|
-
try {
|
|
53
|
-
const res = await fetchImpl(url, {
|
|
54
|
-
signal: AbortSignal.timeout(options.timeoutMs ?? 1500)
|
|
55
|
-
});
|
|
56
|
-
if (!res.ok) return;
|
|
57
|
-
const body = await res.json();
|
|
58
|
-
const candidate = body?.identity?.nostrPubkey;
|
|
59
|
-
if (typeof candidate === "string") daemonPubkey = candidate;
|
|
60
|
-
} catch {
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
if (daemonPubkey !== void 0 && daemonPubkey === pubkey) {
|
|
64
|
-
throw new DaemonIdentityConflictError(pubkey, url);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
function pidAlive(pid) {
|
|
68
|
-
try {
|
|
69
|
-
process.kill(pid, 0);
|
|
70
|
-
return true;
|
|
71
|
-
} catch (err) {
|
|
72
|
-
return err.code === "EPERM";
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
var NonceLock = class _NonceLock {
|
|
76
|
-
constructor(pubkey, lockPath) {
|
|
77
|
-
this.pubkey = pubkey;
|
|
78
|
-
this.lockPath = lockPath;
|
|
79
|
-
this.exitHandler = () => {
|
|
80
|
-
try {
|
|
81
|
-
unlinkSync(this.lockPath);
|
|
82
|
-
} catch {
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
process.once("exit", this.exitHandler);
|
|
86
|
-
}
|
|
87
|
-
pubkey;
|
|
88
|
-
lockPath;
|
|
89
|
-
released = false;
|
|
90
|
-
exitHandler;
|
|
91
|
-
static async acquire(pubkey, options = {}) {
|
|
92
|
-
const dir = options.dir ?? defaultLockDir();
|
|
93
|
-
const pid = options.pid ?? process.pid;
|
|
94
|
-
const lockPath = join(dir, `standalone-${pubkey}.lock`);
|
|
95
|
-
mkdirSync(dir, { recursive: true });
|
|
96
|
-
const payload = JSON.stringify(
|
|
97
|
-
{
|
|
98
|
-
pid,
|
|
99
|
-
pubkey,
|
|
100
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
101
|
-
},
|
|
102
|
-
null,
|
|
103
|
-
2
|
|
104
|
-
);
|
|
105
|
-
for (let attempt = 0; attempt < 2; attempt++) {
|
|
106
|
-
try {
|
|
107
|
-
writeFileSync(lockPath, payload, { flag: "wx" });
|
|
108
|
-
return new _NonceLock(pubkey, lockPath);
|
|
109
|
-
} catch (err) {
|
|
110
|
-
if (err.code !== "EEXIST") throw err;
|
|
111
|
-
let holderPid;
|
|
112
|
-
try {
|
|
113
|
-
const parsed = JSON.parse(
|
|
114
|
-
readFileSync(lockPath, "utf8")
|
|
115
|
-
);
|
|
116
|
-
if (typeof parsed.pid === "number") holderPid = parsed.pid;
|
|
117
|
-
} catch {
|
|
118
|
-
}
|
|
119
|
-
if (holderPid !== void 0 && holderPid !== pid && pidAlive(holderPid)) {
|
|
120
|
-
throw new StandaloneLockError(pubkey, lockPath, holderPid);
|
|
121
|
-
}
|
|
122
|
-
try {
|
|
123
|
-
unlinkSync(lockPath);
|
|
124
|
-
} catch {
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
throw new StandaloneLockError(pubkey, lockPath, -1);
|
|
129
|
-
}
|
|
130
|
-
/** Remove the lockfile and detach the exit hook. Idempotent. */
|
|
131
|
-
release() {
|
|
132
|
-
if (this.released) return;
|
|
133
|
-
this.released = true;
|
|
134
|
-
process.removeListener("exit", this.exitHandler);
|
|
135
|
-
try {
|
|
136
|
-
unlinkSync(this.lockPath);
|
|
137
|
-
} catch {
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
};
|
|
141
|
-
|
|
142
10
|
// src/standalone/standalone-publisher.ts
|
|
143
11
|
import { ToonClient, parseFulfillHttp } from "@toon-protocol/client";
|
|
144
12
|
var StandalonePublishError = class extends Error {
|
|
@@ -417,7 +285,7 @@ var StandalonePublisher = class {
|
|
|
417
285
|
}
|
|
418
286
|
cm.trackChannel(record.channelId, record.context);
|
|
419
287
|
cm.peerChannels.set(record.peerId, record.channelId);
|
|
420
|
-
if (record.context.chainType === "evm" && this.client.rehydrateChannelDeposit) {
|
|
288
|
+
if (record.context.chainType === "evm" && record.depositTotal === void 0 && this.client.rehydrateChannelDeposit) {
|
|
421
289
|
try {
|
|
422
290
|
const deposit = await this.client.rehydrateChannelDeposit(
|
|
423
291
|
record.channelId,
|
|
@@ -712,16 +580,9 @@ function nowSeconds() {
|
|
|
712
580
|
}
|
|
713
581
|
|
|
714
582
|
export {
|
|
715
|
-
DEFAULT_DAEMON_PORT,
|
|
716
|
-
defaultDaemonPort,
|
|
717
|
-
defaultLockDir,
|
|
718
|
-
DaemonIdentityConflictError,
|
|
719
|
-
StandaloneLockError,
|
|
720
|
-
checkDaemonIdentity,
|
|
721
|
-
NonceLock,
|
|
722
583
|
StandalonePublishError,
|
|
723
584
|
deriveRouteDestinations,
|
|
724
585
|
extractArweaveTxId,
|
|
725
586
|
StandalonePublisher
|
|
726
587
|
};
|
|
727
|
-
//# sourceMappingURL=chunk-
|
|
588
|
+
//# sourceMappingURL=chunk-OIJNRACA.js.map
|