@toon-protocol/git 1.0.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 +52 -0
- package/dist/chunk-4WFGAICZ.js +707 -0
- package/dist/chunk-4WFGAICZ.js.map +1 -0
- package/dist/chunk-KXXHAUXL.js +109 -0
- package/dist/chunk-KXXHAUXL.js.map +1 -0
- package/dist/chunk-LJA7PPZI.js +144 -0
- package/dist/chunk-LJA7PPZI.js.map +1 -0
- package/dist/chunk-M7O4SEVW.js +56 -0
- package/dist/chunk-M7O4SEVW.js.map +1 -0
- package/dist/chunk-R3JVS6SX.js +345 -0
- package/dist/chunk-R3JVS6SX.js.map +1 -0
- package/dist/chunk-SBMFWVCP.js +265 -0
- package/dist/chunk-SBMFWVCP.js.map +1 -0
- package/dist/cli/rig.d.ts +1 -0
- package/dist/cli/rig.js +1430 -0
- package/dist/cli/rig.js.map +1 -0
- package/dist/index.d.ts +742 -0
- package/dist/index.js +61 -0
- package/dist/index.js.map +1 -0
- package/dist/publisher-VEIEQHl6.d.ts +254 -0
- package/dist/standalone/index.d.ts +272 -0
- package/dist/standalone/index.js +30 -0
- package/dist/standalone/index.js.map +1 -0
- package/dist/standalone-mode-UFMHGUOM.js +132 -0
- package/dist/standalone-mode-UFMHGUOM.js.map +1 -0
- package/package.json +71 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import {
|
|
2
|
+
REPOSITORY_STATE_KIND
|
|
3
|
+
} from "./chunk-KXXHAUXL.js";
|
|
4
|
+
|
|
5
|
+
// src/remote-state.ts
|
|
6
|
+
import { decode as decodeToon } from "@toon-format/toon";
|
|
7
|
+
|
|
8
|
+
// ../arweave/dist/gateways.js
|
|
9
|
+
var ARWEAVE_FETCH_TIMEOUT_MS = 15e3;
|
|
10
|
+
|
|
11
|
+
// ../arweave/dist/git-sha.js
|
|
12
|
+
var ARWEAVE_GRAPHQL_URL = "https://arweave.net/graphql";
|
|
13
|
+
var SHA_CACHE_MAX_SIZE = 1e4;
|
|
14
|
+
var shaToTxIdCache = /* @__PURE__ */ new Map();
|
|
15
|
+
function isValidGitSha(sha) {
|
|
16
|
+
return /^[0-9a-f]{40}$/i.test(sha);
|
|
17
|
+
}
|
|
18
|
+
function sanitizeGraphQLValue(value) {
|
|
19
|
+
return value.replace(/["\\\n\r\u0000-\u001f`]/g, "");
|
|
20
|
+
}
|
|
21
|
+
function shaCacheKey(sha, repo) {
|
|
22
|
+
return `${sha}:${repo}`;
|
|
23
|
+
}
|
|
24
|
+
function seedShaCache(mappings) {
|
|
25
|
+
const entries = mappings instanceof Map ? mappings.entries() : mappings;
|
|
26
|
+
for (const [key, txId] of entries) {
|
|
27
|
+
if (shaToTxIdCache.size >= SHA_CACHE_MAX_SIZE) {
|
|
28
|
+
const firstKey = shaToTxIdCache.keys().next().value;
|
|
29
|
+
if (firstKey !== void 0) {
|
|
30
|
+
shaToTxIdCache.delete(firstKey);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
shaToTxIdCache.set(key, txId);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
var ARWEAVE_TX_ID_RE = /^[a-zA-Z0-9_-]{43}$/;
|
|
37
|
+
function isValidArweaveTxId(txId) {
|
|
38
|
+
return ARWEAVE_TX_ID_RE.test(txId);
|
|
39
|
+
}
|
|
40
|
+
async function resolveGitSha(sha, repo) {
|
|
41
|
+
if (!isValidGitSha(sha)) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const cacheKey = shaCacheKey(sha, repo);
|
|
45
|
+
const cached = shaToTxIdCache.get(cacheKey);
|
|
46
|
+
if (cached !== void 0) {
|
|
47
|
+
return cached;
|
|
48
|
+
}
|
|
49
|
+
const safeSha = sanitizeGraphQLValue(sha);
|
|
50
|
+
const safeRepo = sanitizeGraphQLValue(repo);
|
|
51
|
+
const query = `query {
|
|
52
|
+
transactions(tags: [
|
|
53
|
+
{ name: "Git-SHA", values: ["${safeSha}"] },
|
|
54
|
+
{ name: "Repo", values: ["${safeRepo}"] }
|
|
55
|
+
]) {
|
|
56
|
+
edges { node { id } }
|
|
57
|
+
}
|
|
58
|
+
}`;
|
|
59
|
+
try {
|
|
60
|
+
const response = await fetch(ARWEAVE_GRAPHQL_URL, {
|
|
61
|
+
method: "POST",
|
|
62
|
+
headers: { "Content-Type": "application/json" },
|
|
63
|
+
body: JSON.stringify({ query }),
|
|
64
|
+
signal: AbortSignal.timeout(ARWEAVE_FETCH_TIMEOUT_MS)
|
|
65
|
+
});
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const json = await response.json();
|
|
70
|
+
const edges = json.data?.transactions?.edges;
|
|
71
|
+
if (!edges || edges.length === 0) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const txId = edges[0]?.node?.id;
|
|
75
|
+
if (!txId || !isValidArweaveTxId(txId)) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
if (shaToTxIdCache.size >= SHA_CACHE_MAX_SIZE) {
|
|
79
|
+
const firstKey = shaToTxIdCache.keys().next().value;
|
|
80
|
+
if (firstKey !== void 0) {
|
|
81
|
+
shaToTxIdCache.delete(firstKey);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
shaToTxIdCache.set(cacheKey, txId);
|
|
85
|
+
return txId;
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/remote-state.ts
|
|
92
|
+
import { REPOSITORY_ANNOUNCEMENT_KIND } from "@toon-protocol/core/nip34";
|
|
93
|
+
function isValidRelayUrl(url) {
|
|
94
|
+
return /^wss?:\/\//i.test(url);
|
|
95
|
+
}
|
|
96
|
+
var WS_OPEN = 1;
|
|
97
|
+
function defaultWebSocketFactory(url) {
|
|
98
|
+
const ctor = globalThis.WebSocket;
|
|
99
|
+
if (!ctor) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
"No global WebSocket constructor (Node >= 22 required) \u2014 pass webSocketFactory"
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return new ctor(url);
|
|
105
|
+
}
|
|
106
|
+
function decodeEventPayload(payload) {
|
|
107
|
+
if (payload !== null && typeof payload === "object") {
|
|
108
|
+
return payload;
|
|
109
|
+
}
|
|
110
|
+
if (typeof payload !== "string") {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const parsed = JSON.parse(payload);
|
|
115
|
+
if (parsed !== null && typeof parsed === "object") {
|
|
116
|
+
return parsed;
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
return decodeToon(payload);
|
|
122
|
+
} catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function queryRelay(relayUrl, filter, timeoutMs, webSocketFactory) {
|
|
127
|
+
return new Promise((resolve, reject) => {
|
|
128
|
+
const events = [];
|
|
129
|
+
const subId = `git-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
130
|
+
let ws;
|
|
131
|
+
let timeoutHandle;
|
|
132
|
+
let settled = false;
|
|
133
|
+
const settle = (outcome, error) => {
|
|
134
|
+
if (settled) return;
|
|
135
|
+
settled = true;
|
|
136
|
+
clearTimeout(timeoutHandle);
|
|
137
|
+
try {
|
|
138
|
+
if (ws.readyState === WS_OPEN) {
|
|
139
|
+
ws.send(JSON.stringify(["CLOSE", subId]));
|
|
140
|
+
}
|
|
141
|
+
ws.close();
|
|
142
|
+
} catch {
|
|
143
|
+
}
|
|
144
|
+
if (outcome === "reject") {
|
|
145
|
+
reject(error);
|
|
146
|
+
} else {
|
|
147
|
+
resolve(events);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
if (!isValidRelayUrl(relayUrl)) {
|
|
151
|
+
reject(
|
|
152
|
+
new Error(
|
|
153
|
+
`Invalid relay URL protocol (must be ws:// or wss://): ${relayUrl}`
|
|
154
|
+
)
|
|
155
|
+
);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
ws = webSocketFactory(relayUrl);
|
|
160
|
+
} catch (err) {
|
|
161
|
+
reject(new Error(`Failed to connect to relay ${relayUrl}: ${String(err)}`));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
timeoutHandle = setTimeout(() => {
|
|
165
|
+
settle("resolve");
|
|
166
|
+
}, timeoutMs);
|
|
167
|
+
ws.addEventListener("open", () => {
|
|
168
|
+
ws.send(JSON.stringify(["REQ", subId, filter]));
|
|
169
|
+
});
|
|
170
|
+
ws.addEventListener("message", (msgEvent) => {
|
|
171
|
+
try {
|
|
172
|
+
const msg = JSON.parse(String(msgEvent.data));
|
|
173
|
+
if (!Array.isArray(msg) || msg.length < 2) return;
|
|
174
|
+
const msgType = msg[0];
|
|
175
|
+
if (msgType === "EVENT" && msg[1] === subId && msg[2] !== void 0) {
|
|
176
|
+
const event = decodeEventPayload(msg[2]);
|
|
177
|
+
if (event) events.push(event);
|
|
178
|
+
} else if (msgType === "EOSE" && msg[1] === subId) {
|
|
179
|
+
settle("resolve");
|
|
180
|
+
}
|
|
181
|
+
} catch {
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
ws.addEventListener("error", (event) => {
|
|
185
|
+
const detail = typeof event === "object" && event !== null && "message" in event ? String(event.message) : "unknown";
|
|
186
|
+
settle(
|
|
187
|
+
"reject",
|
|
188
|
+
new Error(`WebSocket error connecting to ${relayUrl}: ${detail}`)
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
ws.addEventListener("close", () => {
|
|
192
|
+
settle("resolve");
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
function latestReplaceable(events) {
|
|
197
|
+
let winner = null;
|
|
198
|
+
for (const event of events) {
|
|
199
|
+
if (winner === null || event.created_at > winner.created_at || event.created_at === winner.created_at && event.id < winner.id) {
|
|
200
|
+
winner = event;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return winner;
|
|
204
|
+
}
|
|
205
|
+
function getTagValue(tags, name) {
|
|
206
|
+
const tag = tags.find((t) => t[0] === name);
|
|
207
|
+
return tag?.[1];
|
|
208
|
+
}
|
|
209
|
+
var MAX_REFS_PER_EVENT = 1e3;
|
|
210
|
+
var SYMREF_PREFIX = "ref: ";
|
|
211
|
+
function parseRefsEvent(event) {
|
|
212
|
+
const refs = /* @__PURE__ */ new Map();
|
|
213
|
+
const shaToTxId = /* @__PURE__ */ new Map();
|
|
214
|
+
let headSymref = null;
|
|
215
|
+
for (const tag of event.tags) {
|
|
216
|
+
const [tagName, v1, v2] = tag;
|
|
217
|
+
if (tagName === "r" && v1 && v2) {
|
|
218
|
+
if (v1 === "HEAD" && v2.startsWith(SYMREF_PREFIX)) {
|
|
219
|
+
headSymref = v2.slice(SYMREF_PREFIX.length);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (refs.size >= MAX_REFS_PER_EVENT) continue;
|
|
223
|
+
refs.set(v1, v2);
|
|
224
|
+
} else if (tagName === "HEAD" && v1?.startsWith(SYMREF_PREFIX)) {
|
|
225
|
+
headSymref = v1.slice(SYMREF_PREFIX.length);
|
|
226
|
+
} else if (tagName === "arweave" && v1 && v2) {
|
|
227
|
+
shaToTxId.set(v1, v2);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return { refs, headSymref, shaToTxId };
|
|
231
|
+
}
|
|
232
|
+
async function fetchRemoteState(options) {
|
|
233
|
+
const {
|
|
234
|
+
relayUrls,
|
|
235
|
+
ownerPubkey,
|
|
236
|
+
repoId,
|
|
237
|
+
timeoutMs = 1e4,
|
|
238
|
+
resolveSha = resolveGitSha,
|
|
239
|
+
webSocketFactory = defaultWebSocketFactory
|
|
240
|
+
} = options;
|
|
241
|
+
if (relayUrls.length === 0) {
|
|
242
|
+
throw new Error("fetchRemoteState: relayUrls must not be empty");
|
|
243
|
+
}
|
|
244
|
+
if (!ownerPubkey) {
|
|
245
|
+
throw new Error("fetchRemoteState: ownerPubkey is required");
|
|
246
|
+
}
|
|
247
|
+
if (!repoId) {
|
|
248
|
+
throw new Error("fetchRemoteState: repoId is required");
|
|
249
|
+
}
|
|
250
|
+
const filter = {
|
|
251
|
+
kinds: [REPOSITORY_ANNOUNCEMENT_KIND, REPOSITORY_STATE_KIND],
|
|
252
|
+
authors: [ownerPubkey],
|
|
253
|
+
"#d": [repoId]
|
|
254
|
+
};
|
|
255
|
+
const results = await Promise.allSettled(
|
|
256
|
+
relayUrls.map((url) => queryRelay(url, filter, timeoutMs, webSocketFactory))
|
|
257
|
+
);
|
|
258
|
+
const failures = [];
|
|
259
|
+
const byId = /* @__PURE__ */ new Map();
|
|
260
|
+
for (const result of results) {
|
|
261
|
+
if (result.status === "rejected") {
|
|
262
|
+
failures.push(String(result.reason?.message ?? result.reason));
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
for (const event of result.value) {
|
|
266
|
+
if (event.pubkey !== ownerPubkey) continue;
|
|
267
|
+
if (getTagValue(event.tags, "d") !== repoId) continue;
|
|
268
|
+
if (typeof event.id === "string" && !byId.has(event.id)) {
|
|
269
|
+
byId.set(event.id, event);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (failures.length === relayUrls.length) {
|
|
274
|
+
throw new Error(
|
|
275
|
+
`fetchRemoteState: all ${relayUrls.length} relay(s) failed: ${failures.join("; ")}`
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
const events = [...byId.values()];
|
|
279
|
+
const refsEvent = latestReplaceable(
|
|
280
|
+
events.filter((e) => e.kind === REPOSITORY_STATE_KIND)
|
|
281
|
+
);
|
|
282
|
+
const announceEvent = latestReplaceable(
|
|
283
|
+
events.filter((e) => e.kind === REPOSITORY_ANNOUNCEMENT_KIND)
|
|
284
|
+
);
|
|
285
|
+
const { refs, headSymref, shaToTxId } = refsEvent ? parseRefsEvent(refsEvent) : {
|
|
286
|
+
refs: /* @__PURE__ */ new Map(),
|
|
287
|
+
headSymref: null,
|
|
288
|
+
shaToTxId: /* @__PURE__ */ new Map()
|
|
289
|
+
};
|
|
290
|
+
if (shaToTxId.size > 0) {
|
|
291
|
+
seedShaCache(
|
|
292
|
+
[...shaToTxId].map(
|
|
293
|
+
([sha, txId]) => [shaCacheKey(sha, repoId), txId]
|
|
294
|
+
)
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
const name = announceEvent ? getTagValue(announceEvent.tags, "name") ?? null : null;
|
|
298
|
+
const description = announceEvent ? getTagValue(announceEvent.tags, "description") ?? announceEvent.content : null;
|
|
299
|
+
const relays = [];
|
|
300
|
+
if (announceEvent) {
|
|
301
|
+
for (const tag of announceEvent.tags) {
|
|
302
|
+
if (tag[0] === "relays") {
|
|
303
|
+
relays.push(...tag.slice(1).filter((url) => url.length > 0));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const resolveMissing = async (shas) => {
|
|
308
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
309
|
+
const missing = [];
|
|
310
|
+
for (const sha of new Set(shas)) {
|
|
311
|
+
const known = shaToTxId.get(sha);
|
|
312
|
+
if (known !== void 0) {
|
|
313
|
+
resolved.set(sha, known);
|
|
314
|
+
} else {
|
|
315
|
+
missing.push(sha);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const lookups = await Promise.all(
|
|
319
|
+
missing.map(
|
|
320
|
+
async (sha) => [sha, await resolveSha(sha, repoId)]
|
|
321
|
+
)
|
|
322
|
+
);
|
|
323
|
+
for (const [sha, txId] of lookups) {
|
|
324
|
+
if (txId) resolved.set(sha, txId);
|
|
325
|
+
}
|
|
326
|
+
return resolved;
|
|
327
|
+
};
|
|
328
|
+
return {
|
|
329
|
+
announced: announceEvent !== null,
|
|
330
|
+
refs,
|
|
331
|
+
headSymref,
|
|
332
|
+
shaToTxId,
|
|
333
|
+
refsEvent,
|
|
334
|
+
announceEvent,
|
|
335
|
+
name,
|
|
336
|
+
description,
|
|
337
|
+
relays,
|
|
338
|
+
resolveMissing
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export {
|
|
343
|
+
fetchRemoteState
|
|
344
|
+
};
|
|
345
|
+
//# sourceMappingURL=chunk-R3JVS6SX.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/remote-state.ts","../../arweave/src/gateways.ts","../../arweave/src/git-sha.ts"],"sourcesContent":["/**\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 this module sends). */\ninterface NostrFilter {\n kinds?: number[];\n authors?: string[];\n '#d'?: string[];\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 */\nfunction 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) and the Node write path (@toon-protocol/git) 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":"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","names":[]}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import {
|
|
2
|
+
NonceLock,
|
|
3
|
+
checkDaemonIdentity
|
|
4
|
+
} from "./chunk-LJA7PPZI.js";
|
|
5
|
+
import {
|
|
6
|
+
MAX_OBJECT_SIZE
|
|
7
|
+
} from "./chunk-M7O4SEVW.js";
|
|
8
|
+
|
|
9
|
+
// src/standalone/standalone-publisher.ts
|
|
10
|
+
import { ToonClient, parseFulfillHttp } from "@toon-protocol/client";
|
|
11
|
+
var StandalonePublishError = class extends Error {
|
|
12
|
+
constructor(message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "StandalonePublishError";
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
function deriveRouteDestinations(anchor) {
|
|
18
|
+
const segs = anchor.split(".");
|
|
19
|
+
if (segs.at(-1) === "store" && segs.at(-2) === "relay") {
|
|
20
|
+
const base = segs.slice(0, -2).join(".");
|
|
21
|
+
return { publish: `${base}.relay`, store: `${base}.store` };
|
|
22
|
+
}
|
|
23
|
+
return { publish: anchor, store: anchor };
|
|
24
|
+
}
|
|
25
|
+
var ARWEAVE_TX_ID_REGEX = /^[A-Za-z0-9_-]{43}$/;
|
|
26
|
+
function extractArweaveTxId(base64Data) {
|
|
27
|
+
const http = parseFulfillHttp(base64Data);
|
|
28
|
+
if (!http.isHttp) {
|
|
29
|
+
const legacy = Buffer.from(base64Data, "base64").toString("utf8");
|
|
30
|
+
if (!ARWEAVE_TX_ID_REGEX.test(legacy)) {
|
|
31
|
+
throw new StandalonePublishError(
|
|
32
|
+
`FULFILL data is not a valid Arweave tx ID: "${legacy}"`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
return legacy;
|
|
36
|
+
}
|
|
37
|
+
if (http.status < 200 || http.status >= 300) {
|
|
38
|
+
throw new StandalonePublishError(
|
|
39
|
+
`git-object upload failed: store returned HTTP ${http.status}` + (http.body ? ` - ${http.body}` : "")
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
let parsed;
|
|
43
|
+
try {
|
|
44
|
+
parsed = JSON.parse(http.body);
|
|
45
|
+
} catch {
|
|
46
|
+
throw new StandalonePublishError(
|
|
47
|
+
`git-object upload response body was not valid JSON: "${http.body}"`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
if (parsed.accept === false) {
|
|
51
|
+
const reason = typeof parsed.error === "string" ? `: ${parsed.error}` : "";
|
|
52
|
+
throw new StandalonePublishError(
|
|
53
|
+
`git-object upload rejected by store (accept:false)${reason}`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
if (typeof parsed.txId === "string" && ARWEAVE_TX_ID_REGEX.test(parsed.txId)) {
|
|
57
|
+
return parsed.txId;
|
|
58
|
+
}
|
|
59
|
+
if (typeof parsed.data === "string" && parsed.data.length > 0) {
|
|
60
|
+
const decoded = Buffer.from(parsed.data, "base64").toString("utf8");
|
|
61
|
+
if (ARWEAVE_TX_ID_REGEX.test(decoded)) return decoded;
|
|
62
|
+
}
|
|
63
|
+
throw new StandalonePublishError(
|
|
64
|
+
`git-object upload response did not contain a valid Arweave tx ID: "${http.body}"`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
var StandalonePublisher = class {
|
|
68
|
+
client;
|
|
69
|
+
ownsClient;
|
|
70
|
+
publishDestination;
|
|
71
|
+
storeDestination;
|
|
72
|
+
channelDestination;
|
|
73
|
+
eventFee;
|
|
74
|
+
uploadFeePerByte;
|
|
75
|
+
daemonPort;
|
|
76
|
+
lockDir;
|
|
77
|
+
fetchImpl;
|
|
78
|
+
lock;
|
|
79
|
+
channelId;
|
|
80
|
+
readyPromise;
|
|
81
|
+
constructor(options) {
|
|
82
|
+
if (options.client && options.clientConfig) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
"StandalonePublisher: provide either `clientConfig` or `client`, not both"
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
if (options.client) {
|
|
88
|
+
this.client = options.client;
|
|
89
|
+
this.ownsClient = false;
|
|
90
|
+
} else if (options.clientConfig) {
|
|
91
|
+
this.client = new ToonClient(options.clientConfig);
|
|
92
|
+
this.ownsClient = true;
|
|
93
|
+
} else {
|
|
94
|
+
throw new Error(
|
|
95
|
+
"StandalonePublisher: one of `clientConfig` (mnemonic-based ToonClient config) or `client` is required"
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
const anchor = options.channelDestination ?? options.clientConfig?.destinationAddress;
|
|
99
|
+
const routes = anchor ? deriveRouteDestinations(anchor) : void 0;
|
|
100
|
+
this.publishDestination = options.publishDestination ?? routes?.publish;
|
|
101
|
+
this.storeDestination = options.storeDestination ?? routes?.store;
|
|
102
|
+
this.channelDestination = options.channelDestination;
|
|
103
|
+
this.eventFee = options.eventFee ?? 1n;
|
|
104
|
+
this.uploadFeePerByte = options.uploadFeePerByte ?? 10n;
|
|
105
|
+
this.daemonPort = options.daemonPort;
|
|
106
|
+
this.lockDir = options.lockDir;
|
|
107
|
+
this.fetchImpl = options.fetchImpl;
|
|
108
|
+
}
|
|
109
|
+
/** Hex Nostr pubkey of the embedded identity (available before start). */
|
|
110
|
+
getPublicKey() {
|
|
111
|
+
return this.client.getPublicKey();
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Run the nonce guard, start the embedded client, and open (or resume) the
|
|
115
|
+
* payment channel. Called lazily by the first paid operation; safe to call
|
|
116
|
+
* eagerly to fail fast. Idempotent.
|
|
117
|
+
*/
|
|
118
|
+
start() {
|
|
119
|
+
this.readyPromise ??= this.doStart().catch((err) => {
|
|
120
|
+
this.readyPromise = void 0;
|
|
121
|
+
throw err;
|
|
122
|
+
});
|
|
123
|
+
return this.readyPromise;
|
|
124
|
+
}
|
|
125
|
+
async doStart() {
|
|
126
|
+
const pubkey = this.client.getPublicKey();
|
|
127
|
+
await checkDaemonIdentity(pubkey, {
|
|
128
|
+
...this.daemonPort !== void 0 ? { port: this.daemonPort } : {},
|
|
129
|
+
...this.fetchImpl ? { fetchImpl: this.fetchImpl } : {}
|
|
130
|
+
});
|
|
131
|
+
this.lock = await NonceLock.acquire(pubkey, {
|
|
132
|
+
...this.lockDir !== void 0 ? { dir: this.lockDir } : {}
|
|
133
|
+
});
|
|
134
|
+
try {
|
|
135
|
+
if (this.client.isStarted?.() !== true) {
|
|
136
|
+
await this.client.start();
|
|
137
|
+
}
|
|
138
|
+
this.channelId = await this.client.openChannel(this.channelDestination);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
this.lock.release();
|
|
141
|
+
this.lock = void 0;
|
|
142
|
+
throw err;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** Release the identity lock and stop the embedded client (if we own it). */
|
|
146
|
+
async stop() {
|
|
147
|
+
this.lock?.release();
|
|
148
|
+
this.lock = void 0;
|
|
149
|
+
this.readyPromise = void 0;
|
|
150
|
+
this.channelId = void 0;
|
|
151
|
+
if (this.ownsClient) {
|
|
152
|
+
await this.client.stop();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// ── Publisher ─────────────────────────────────────────────────────────────
|
|
156
|
+
/**
|
|
157
|
+
* Fee rates for `planPush` estimation: the flat per-event fee and the
|
|
158
|
+
* per-byte upload rate this publisher pays (daemon `feePerEvent` and seed
|
|
159
|
+
* bid-rate conventions; override via options).
|
|
160
|
+
*/
|
|
161
|
+
getFeeRates() {
|
|
162
|
+
return Promise.resolve({
|
|
163
|
+
uploadFeePerByte: this.uploadFeePerByte,
|
|
164
|
+
eventFee: this.eventFee
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Upload one git object as a kind:5094 store write (Git-SHA/Git-Type/Repo
|
|
169
|
+
* tagged — the proven seed-pipeline shape), signing one balance-proof claim
|
|
170
|
+
* for `body.length × uploadFeePerByte`.
|
|
171
|
+
*/
|
|
172
|
+
async uploadGitObject(upload) {
|
|
173
|
+
if (upload.body.length > MAX_OBJECT_SIZE) {
|
|
174
|
+
throw new StandalonePublishError(
|
|
175
|
+
`git object ${upload.sha} exceeds the ${MAX_OBJECT_SIZE}-byte limit: ${upload.body.length} bytes`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
await this.start();
|
|
179
|
+
const channelId = this.requireChannel();
|
|
180
|
+
const fee = BigInt(upload.body.length) * this.uploadFeePerByte;
|
|
181
|
+
const event = this.client.signEvent({
|
|
182
|
+
kind: 5094,
|
|
183
|
+
content: "",
|
|
184
|
+
created_at: nowSeconds(),
|
|
185
|
+
tags: [
|
|
186
|
+
["i", upload.body.toString("base64"), "blob"],
|
|
187
|
+
["bid", fee.toString(), "usdc"],
|
|
188
|
+
["output", "application/octet-stream"],
|
|
189
|
+
["Git-SHA", upload.sha],
|
|
190
|
+
["Git-Type", upload.type],
|
|
191
|
+
["Repo", upload.repoId]
|
|
192
|
+
]
|
|
193
|
+
});
|
|
194
|
+
const claim = await this.client.signBalanceProof(channelId, fee);
|
|
195
|
+
const result = await this.client.publishEvent(event, {
|
|
196
|
+
...this.storeDestination ? { destination: this.storeDestination } : {},
|
|
197
|
+
claim,
|
|
198
|
+
ilpAmount: fee,
|
|
199
|
+
// The store backend serves POST /store (not the relay's /write).
|
|
200
|
+
proxyPath: "/store"
|
|
201
|
+
});
|
|
202
|
+
if (!result.success) {
|
|
203
|
+
throw new StandalonePublishError(
|
|
204
|
+
`git-object upload rejected (${upload.sha}): ${result.error ?? "store rejected the write"}`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
if (!result.data) {
|
|
208
|
+
throw new StandalonePublishError(
|
|
209
|
+
`git-object upload FULFILL carried no data (${upload.sha}); expected the Arweave tx ID`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
return { txId: extractArweaveTxId(result.data), feePaid: fee };
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Sign the event with the embedded identity and pay-to-publish it through
|
|
216
|
+
* the relay write route, one claim for the flat per-event fee.
|
|
217
|
+
*
|
|
218
|
+
* `relayUrls` is the interface's plural forward-compat surface (parked
|
|
219
|
+
* #84): the standalone impl routes over ILP to its single configured
|
|
220
|
+
* publish destination, so more than one relay is refused rather than
|
|
221
|
+
* silently half-published.
|
|
222
|
+
*/
|
|
223
|
+
async publishEvent(event, relayUrls) {
|
|
224
|
+
if (relayUrls.length > 1) {
|
|
225
|
+
throw new StandalonePublishError(
|
|
226
|
+
`multi-relay publish is not supported yet (got ${relayUrls.length} relays) \u2014 the standalone publisher routes to a single relay destination (#84 parked)`
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
await this.start();
|
|
230
|
+
const channelId = this.requireChannel();
|
|
231
|
+
const signed = this.client.signEvent(event);
|
|
232
|
+
const fee = this.eventFee;
|
|
233
|
+
const claim = await this.client.signBalanceProof(channelId, fee);
|
|
234
|
+
const result = await this.client.publishEvent(signed, {
|
|
235
|
+
...this.publishDestination ? { destination: this.publishDestination } : {},
|
|
236
|
+
claim,
|
|
237
|
+
ilpAmount: fee
|
|
238
|
+
});
|
|
239
|
+
if (!result.success) {
|
|
240
|
+
throw new StandalonePublishError(
|
|
241
|
+
`publish rejected (kind ${event.kind}): ${result.error ?? "relay rejected the event"}`
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
return { eventId: result.eventId ?? signed.id, feePaid: fee };
|
|
245
|
+
}
|
|
246
|
+
requireChannel() {
|
|
247
|
+
if (!this.channelId) {
|
|
248
|
+
throw new StandalonePublishError(
|
|
249
|
+
"no payment channel open \u2014 start() did not complete"
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
return this.channelId;
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
function nowSeconds() {
|
|
256
|
+
return Math.floor(Date.now() / 1e3);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export {
|
|
260
|
+
StandalonePublishError,
|
|
261
|
+
deriveRouteDestinations,
|
|
262
|
+
extractArweaveTxId,
|
|
263
|
+
StandalonePublisher
|
|
264
|
+
};
|
|
265
|
+
//# sourceMappingURL=chunk-SBMFWVCP.js.map
|