@toon-protocol/rig 2.0.0 → 2.1.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 +10 -3
- package/dist/chunk-3EKP7PMM.js +727 -0
- package/dist/chunk-3EKP7PMM.js.map +1 -0
- package/dist/{chunk-QD437XAW.js → chunk-CW4HJNMU.js} +2 -8
- package/dist/chunk-CW4HJNMU.js.map +1 -0
- package/dist/chunk-O6TXHKWG.js +217 -0
- package/dist/chunk-O6TXHKWG.js.map +1 -0
- package/dist/{chunk-G4W4MH6G.js → chunk-PLKZAUTG.js} +2 -1
- package/dist/chunk-PLKZAUTG.js.map +1 -0
- package/dist/cli/rig.js +1286 -398
- package/dist/cli/rig.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/{publisher-CClD9OIZ.d.ts → publisher-Cdr1H1hg.d.ts} +1 -1
- package/dist/standalone/index.d.ts +404 -3
- package/dist/standalone/index.js +16 -2
- package/dist/standalone-mode-FCKTQ33Y.js +595 -0
- package/dist/standalone-mode-FCKTQ33Y.js.map +1 -0
- package/package.json +3 -11
- package/dist/chunk-G4W4MH6G.js.map +0 -1
- package/dist/chunk-QD437XAW.js.map +0 -1
- package/dist/chunk-XRRU6YBQ.js +0 -402
- package/dist/chunk-XRRU6YBQ.js.map +0 -1
- package/dist/standalone-mode-CAYWOURK.js +0 -111
- package/dist/standalone-mode-CAYWOURK.js.map +0 -1
package/dist/chunk-XRRU6YBQ.js
DELETED
|
@@ -1,402 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
MAX_OBJECT_SIZE
|
|
3
|
-
} from "./chunk-X2CZPPDM.js";
|
|
4
|
-
|
|
5
|
-
// src/standalone/nonce-guard.ts
|
|
6
|
-
import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
7
|
-
import { homedir } from "os";
|
|
8
|
-
import { join } from "path";
|
|
9
|
-
var DEFAULT_DAEMON_PORT = 8787;
|
|
10
|
-
function defaultDaemonPort() {
|
|
11
|
-
const env = process.env["TOON_CLIENT_HTTP_PORT"];
|
|
12
|
-
const parsed = env ? Number(env) : NaN;
|
|
13
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_DAEMON_PORT;
|
|
14
|
-
}
|
|
15
|
-
function defaultLockDir() {
|
|
16
|
-
return process.env["TOON_CLIENT_HOME"] ?? join(homedir(), ".toon-client");
|
|
17
|
-
}
|
|
18
|
-
var DaemonIdentityConflictError = class extends Error {
|
|
19
|
-
constructor(pubkey, daemonUrl) {
|
|
20
|
-
super(
|
|
21
|
-
`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).`
|
|
22
|
-
);
|
|
23
|
-
this.pubkey = pubkey;
|
|
24
|
-
this.daemonUrl = daemonUrl;
|
|
25
|
-
this.name = "DaemonIdentityConflictError";
|
|
26
|
-
}
|
|
27
|
-
pubkey;
|
|
28
|
-
daemonUrl;
|
|
29
|
-
};
|
|
30
|
-
var StandaloneLockError = class extends Error {
|
|
31
|
-
constructor(pubkey, lockPath, holderPid) {
|
|
32
|
-
super(
|
|
33
|
-
`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.`
|
|
34
|
-
);
|
|
35
|
-
this.pubkey = pubkey;
|
|
36
|
-
this.lockPath = lockPath;
|
|
37
|
-
this.holderPid = holderPid;
|
|
38
|
-
this.name = "StandaloneLockError";
|
|
39
|
-
}
|
|
40
|
-
pubkey;
|
|
41
|
-
lockPath;
|
|
42
|
-
holderPid;
|
|
43
|
-
};
|
|
44
|
-
async function checkDaemonIdentity(pubkey, options = {}) {
|
|
45
|
-
const port = options.port ?? defaultDaemonPort();
|
|
46
|
-
const fetchImpl = options.fetchImpl ?? fetch;
|
|
47
|
-
const url = `http://127.0.0.1:${port}/status`;
|
|
48
|
-
let daemonPubkey;
|
|
49
|
-
try {
|
|
50
|
-
const res = await fetchImpl(url, {
|
|
51
|
-
signal: AbortSignal.timeout(options.timeoutMs ?? 1500)
|
|
52
|
-
});
|
|
53
|
-
if (!res.ok) return;
|
|
54
|
-
const body = await res.json();
|
|
55
|
-
const candidate = body?.identity?.nostrPubkey;
|
|
56
|
-
if (typeof candidate === "string") daemonPubkey = candidate;
|
|
57
|
-
} catch {
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
if (daemonPubkey !== void 0 && daemonPubkey === pubkey) {
|
|
61
|
-
throw new DaemonIdentityConflictError(pubkey, url);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
function pidAlive(pid) {
|
|
65
|
-
try {
|
|
66
|
-
process.kill(pid, 0);
|
|
67
|
-
return true;
|
|
68
|
-
} catch (err) {
|
|
69
|
-
return err.code === "EPERM";
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
var NonceLock = class _NonceLock {
|
|
73
|
-
constructor(pubkey, lockPath) {
|
|
74
|
-
this.pubkey = pubkey;
|
|
75
|
-
this.lockPath = lockPath;
|
|
76
|
-
this.exitHandler = () => {
|
|
77
|
-
try {
|
|
78
|
-
unlinkSync(this.lockPath);
|
|
79
|
-
} catch {
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
|
-
process.once("exit", this.exitHandler);
|
|
83
|
-
}
|
|
84
|
-
pubkey;
|
|
85
|
-
lockPath;
|
|
86
|
-
released = false;
|
|
87
|
-
exitHandler;
|
|
88
|
-
static async acquire(pubkey, options = {}) {
|
|
89
|
-
const dir = options.dir ?? defaultLockDir();
|
|
90
|
-
const pid = options.pid ?? process.pid;
|
|
91
|
-
const lockPath = join(dir, `standalone-${pubkey}.lock`);
|
|
92
|
-
mkdirSync(dir, { recursive: true });
|
|
93
|
-
const payload = JSON.stringify(
|
|
94
|
-
{
|
|
95
|
-
pid,
|
|
96
|
-
pubkey,
|
|
97
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
98
|
-
},
|
|
99
|
-
null,
|
|
100
|
-
2
|
|
101
|
-
);
|
|
102
|
-
for (let attempt = 0; attempt < 2; attempt++) {
|
|
103
|
-
try {
|
|
104
|
-
writeFileSync(lockPath, payload, { flag: "wx" });
|
|
105
|
-
return new _NonceLock(pubkey, lockPath);
|
|
106
|
-
} catch (err) {
|
|
107
|
-
if (err.code !== "EEXIST") throw err;
|
|
108
|
-
let holderPid;
|
|
109
|
-
try {
|
|
110
|
-
const parsed = JSON.parse(
|
|
111
|
-
readFileSync(lockPath, "utf8")
|
|
112
|
-
);
|
|
113
|
-
if (typeof parsed.pid === "number") holderPid = parsed.pid;
|
|
114
|
-
} catch {
|
|
115
|
-
}
|
|
116
|
-
if (holderPid !== void 0 && holderPid !== pid && pidAlive(holderPid)) {
|
|
117
|
-
throw new StandaloneLockError(pubkey, lockPath, holderPid);
|
|
118
|
-
}
|
|
119
|
-
try {
|
|
120
|
-
unlinkSync(lockPath);
|
|
121
|
-
} catch {
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
throw new StandaloneLockError(pubkey, lockPath, -1);
|
|
126
|
-
}
|
|
127
|
-
/** Remove the lockfile and detach the exit hook. Idempotent. */
|
|
128
|
-
release() {
|
|
129
|
-
if (this.released) return;
|
|
130
|
-
this.released = true;
|
|
131
|
-
process.removeListener("exit", this.exitHandler);
|
|
132
|
-
try {
|
|
133
|
-
unlinkSync(this.lockPath);
|
|
134
|
-
} catch {
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
// src/standalone/standalone-publisher.ts
|
|
140
|
-
import { ToonClient, parseFulfillHttp } from "@toon-protocol/client";
|
|
141
|
-
var StandalonePublishError = class extends Error {
|
|
142
|
-
constructor(message) {
|
|
143
|
-
super(message);
|
|
144
|
-
this.name = "StandalonePublishError";
|
|
145
|
-
}
|
|
146
|
-
};
|
|
147
|
-
function deriveRouteDestinations(anchor) {
|
|
148
|
-
const segs = anchor.split(".");
|
|
149
|
-
if (segs.at(-1) === "store" && segs.at(-2) === "relay") {
|
|
150
|
-
const base = segs.slice(0, -2).join(".");
|
|
151
|
-
return { publish: `${base}.relay`, store: `${base}.store` };
|
|
152
|
-
}
|
|
153
|
-
return { publish: anchor, store: anchor };
|
|
154
|
-
}
|
|
155
|
-
var ARWEAVE_TX_ID_REGEX = /^[A-Za-z0-9_-]{43}$/;
|
|
156
|
-
function extractArweaveTxId(base64Data) {
|
|
157
|
-
const http = parseFulfillHttp(base64Data);
|
|
158
|
-
if (!http.isHttp) {
|
|
159
|
-
const legacy = Buffer.from(base64Data, "base64").toString("utf8");
|
|
160
|
-
if (!ARWEAVE_TX_ID_REGEX.test(legacy)) {
|
|
161
|
-
throw new StandalonePublishError(
|
|
162
|
-
`FULFILL data is not a valid Arweave tx ID: "${legacy}"`
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
return legacy;
|
|
166
|
-
}
|
|
167
|
-
if (http.status < 200 || http.status >= 300) {
|
|
168
|
-
throw new StandalonePublishError(
|
|
169
|
-
`git-object upload failed: store returned HTTP ${http.status}` + (http.body ? ` - ${http.body}` : "")
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
let parsed;
|
|
173
|
-
try {
|
|
174
|
-
parsed = JSON.parse(http.body);
|
|
175
|
-
} catch {
|
|
176
|
-
throw new StandalonePublishError(
|
|
177
|
-
`git-object upload response body was not valid JSON: "${http.body}"`
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
if (parsed.accept === false) {
|
|
181
|
-
const reason = typeof parsed.error === "string" ? `: ${parsed.error}` : "";
|
|
182
|
-
throw new StandalonePublishError(
|
|
183
|
-
`git-object upload rejected by store (accept:false)${reason}`
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
if (typeof parsed.txId === "string" && ARWEAVE_TX_ID_REGEX.test(parsed.txId)) {
|
|
187
|
-
return parsed.txId;
|
|
188
|
-
}
|
|
189
|
-
if (typeof parsed.data === "string" && parsed.data.length > 0) {
|
|
190
|
-
const decoded = Buffer.from(parsed.data, "base64").toString("utf8");
|
|
191
|
-
if (ARWEAVE_TX_ID_REGEX.test(decoded)) return decoded;
|
|
192
|
-
}
|
|
193
|
-
throw new StandalonePublishError(
|
|
194
|
-
`git-object upload response did not contain a valid Arweave tx ID: "${http.body}"`
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
var StandalonePublisher = class {
|
|
198
|
-
client;
|
|
199
|
-
ownsClient;
|
|
200
|
-
publishDestination;
|
|
201
|
-
storeDestination;
|
|
202
|
-
channelDestination;
|
|
203
|
-
eventFee;
|
|
204
|
-
uploadFeePerByte;
|
|
205
|
-
daemonPort;
|
|
206
|
-
lockDir;
|
|
207
|
-
fetchImpl;
|
|
208
|
-
lock;
|
|
209
|
-
channelId;
|
|
210
|
-
readyPromise;
|
|
211
|
-
constructor(options) {
|
|
212
|
-
if (options.client && options.clientConfig) {
|
|
213
|
-
throw new Error(
|
|
214
|
-
"StandalonePublisher: provide either `clientConfig` or `client`, not both"
|
|
215
|
-
);
|
|
216
|
-
}
|
|
217
|
-
if (options.client) {
|
|
218
|
-
this.client = options.client;
|
|
219
|
-
this.ownsClient = false;
|
|
220
|
-
} else if (options.clientConfig) {
|
|
221
|
-
this.client = new ToonClient(options.clientConfig);
|
|
222
|
-
this.ownsClient = true;
|
|
223
|
-
} else {
|
|
224
|
-
throw new Error(
|
|
225
|
-
"StandalonePublisher: one of `clientConfig` (mnemonic-based ToonClient config) or `client` is required"
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
const anchor = options.channelDestination ?? options.clientConfig?.destinationAddress;
|
|
229
|
-
const routes = anchor ? deriveRouteDestinations(anchor) : void 0;
|
|
230
|
-
this.publishDestination = options.publishDestination ?? routes?.publish;
|
|
231
|
-
this.storeDestination = options.storeDestination ?? routes?.store;
|
|
232
|
-
this.channelDestination = options.channelDestination;
|
|
233
|
-
this.eventFee = options.eventFee ?? 1n;
|
|
234
|
-
this.uploadFeePerByte = options.uploadFeePerByte ?? 10n;
|
|
235
|
-
this.daemonPort = options.daemonPort;
|
|
236
|
-
this.lockDir = options.lockDir;
|
|
237
|
-
this.fetchImpl = options.fetchImpl;
|
|
238
|
-
}
|
|
239
|
-
/** Hex Nostr pubkey of the embedded identity (available before start). */
|
|
240
|
-
getPublicKey() {
|
|
241
|
-
return this.client.getPublicKey();
|
|
242
|
-
}
|
|
243
|
-
/**
|
|
244
|
-
* Run the nonce guard, start the embedded client, and open (or resume) the
|
|
245
|
-
* payment channel. Called lazily by the first paid operation; safe to call
|
|
246
|
-
* eagerly to fail fast. Idempotent.
|
|
247
|
-
*/
|
|
248
|
-
start() {
|
|
249
|
-
this.readyPromise ??= this.doStart().catch((err) => {
|
|
250
|
-
this.readyPromise = void 0;
|
|
251
|
-
throw err;
|
|
252
|
-
});
|
|
253
|
-
return this.readyPromise;
|
|
254
|
-
}
|
|
255
|
-
async doStart() {
|
|
256
|
-
const pubkey = this.client.getPublicKey();
|
|
257
|
-
await checkDaemonIdentity(pubkey, {
|
|
258
|
-
...this.daemonPort !== void 0 ? { port: this.daemonPort } : {},
|
|
259
|
-
...this.fetchImpl ? { fetchImpl: this.fetchImpl } : {}
|
|
260
|
-
});
|
|
261
|
-
this.lock = await NonceLock.acquire(pubkey, {
|
|
262
|
-
...this.lockDir !== void 0 ? { dir: this.lockDir } : {}
|
|
263
|
-
});
|
|
264
|
-
try {
|
|
265
|
-
if (this.client.isStarted?.() !== true) {
|
|
266
|
-
await this.client.start();
|
|
267
|
-
}
|
|
268
|
-
this.channelId = await this.client.openChannel(this.channelDestination);
|
|
269
|
-
} catch (err) {
|
|
270
|
-
this.lock.release();
|
|
271
|
-
this.lock = void 0;
|
|
272
|
-
throw err;
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
/** Release the identity lock and stop the embedded client (if we own it). */
|
|
276
|
-
async stop() {
|
|
277
|
-
this.lock?.release();
|
|
278
|
-
this.lock = void 0;
|
|
279
|
-
this.readyPromise = void 0;
|
|
280
|
-
this.channelId = void 0;
|
|
281
|
-
if (this.ownsClient) {
|
|
282
|
-
await this.client.stop();
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
// ── Publisher ─────────────────────────────────────────────────────────────
|
|
286
|
-
/**
|
|
287
|
-
* Fee rates for `planPush` estimation: the flat per-event fee and the
|
|
288
|
-
* per-byte upload rate this publisher pays (daemon `feePerEvent` and seed
|
|
289
|
-
* bid-rate conventions; override via options).
|
|
290
|
-
*/
|
|
291
|
-
getFeeRates() {
|
|
292
|
-
return Promise.resolve({
|
|
293
|
-
uploadFeePerByte: this.uploadFeePerByte,
|
|
294
|
-
eventFee: this.eventFee
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
/**
|
|
298
|
-
* Upload one git object as a kind:5094 store write (Git-SHA/Git-Type/Repo
|
|
299
|
-
* tagged — the proven seed-pipeline shape), signing one balance-proof claim
|
|
300
|
-
* for `body.length × uploadFeePerByte`.
|
|
301
|
-
*/
|
|
302
|
-
async uploadGitObject(upload) {
|
|
303
|
-
if (upload.body.length > MAX_OBJECT_SIZE) {
|
|
304
|
-
throw new StandalonePublishError(
|
|
305
|
-
`git object ${upload.sha} exceeds the ${MAX_OBJECT_SIZE}-byte limit: ${upload.body.length} bytes`
|
|
306
|
-
);
|
|
307
|
-
}
|
|
308
|
-
await this.start();
|
|
309
|
-
const channelId = this.requireChannel();
|
|
310
|
-
const fee = BigInt(upload.body.length) * this.uploadFeePerByte;
|
|
311
|
-
const event = this.client.signEvent({
|
|
312
|
-
kind: 5094,
|
|
313
|
-
content: "",
|
|
314
|
-
created_at: nowSeconds(),
|
|
315
|
-
tags: [
|
|
316
|
-
["i", upload.body.toString("base64"), "blob"],
|
|
317
|
-
["bid", fee.toString(), "usdc"],
|
|
318
|
-
["output", "application/octet-stream"],
|
|
319
|
-
["Git-SHA", upload.sha],
|
|
320
|
-
["Git-Type", upload.type],
|
|
321
|
-
["Repo", upload.repoId]
|
|
322
|
-
]
|
|
323
|
-
});
|
|
324
|
-
const claim = await this.client.signBalanceProof(channelId, fee);
|
|
325
|
-
const result = await this.client.publishEvent(event, {
|
|
326
|
-
...this.storeDestination ? { destination: this.storeDestination } : {},
|
|
327
|
-
claim,
|
|
328
|
-
ilpAmount: fee,
|
|
329
|
-
// The store backend serves POST /store (not the relay's /write).
|
|
330
|
-
proxyPath: "/store"
|
|
331
|
-
});
|
|
332
|
-
if (!result.success) {
|
|
333
|
-
throw new StandalonePublishError(
|
|
334
|
-
`git-object upload rejected (${upload.sha}): ${result.error ?? "store rejected the write"}`
|
|
335
|
-
);
|
|
336
|
-
}
|
|
337
|
-
if (!result.data) {
|
|
338
|
-
throw new StandalonePublishError(
|
|
339
|
-
`git-object upload FULFILL carried no data (${upload.sha}); expected the Arweave tx ID`
|
|
340
|
-
);
|
|
341
|
-
}
|
|
342
|
-
return { txId: extractArweaveTxId(result.data), feePaid: fee };
|
|
343
|
-
}
|
|
344
|
-
/**
|
|
345
|
-
* Sign the event with the embedded identity and pay-to-publish it through
|
|
346
|
-
* the relay write route, one claim for the flat per-event fee.
|
|
347
|
-
*
|
|
348
|
-
* `relayUrls` is the interface's plural forward-compat surface (parked
|
|
349
|
-
* #84): the standalone impl routes over ILP to its single configured
|
|
350
|
-
* publish destination, so more than one relay is refused rather than
|
|
351
|
-
* silently half-published.
|
|
352
|
-
*/
|
|
353
|
-
async publishEvent(event, relayUrls) {
|
|
354
|
-
if (relayUrls.length > 1) {
|
|
355
|
-
throw new StandalonePublishError(
|
|
356
|
-
`multi-relay publish is not supported yet (got ${relayUrls.length} relays) \u2014 the standalone publisher routes to a single relay destination (#84 parked)`
|
|
357
|
-
);
|
|
358
|
-
}
|
|
359
|
-
await this.start();
|
|
360
|
-
const channelId = this.requireChannel();
|
|
361
|
-
const signed = this.client.signEvent(event);
|
|
362
|
-
const fee = this.eventFee;
|
|
363
|
-
const claim = await this.client.signBalanceProof(channelId, fee);
|
|
364
|
-
const result = await this.client.publishEvent(signed, {
|
|
365
|
-
...this.publishDestination ? { destination: this.publishDestination } : {},
|
|
366
|
-
claim,
|
|
367
|
-
ilpAmount: fee
|
|
368
|
-
});
|
|
369
|
-
if (!result.success) {
|
|
370
|
-
throw new StandalonePublishError(
|
|
371
|
-
`publish rejected (kind ${event.kind}): ${result.error ?? "relay rejected the event"}`
|
|
372
|
-
);
|
|
373
|
-
}
|
|
374
|
-
return { eventId: result.eventId ?? signed.id, feePaid: fee };
|
|
375
|
-
}
|
|
376
|
-
requireChannel() {
|
|
377
|
-
if (!this.channelId) {
|
|
378
|
-
throw new StandalonePublishError(
|
|
379
|
-
"no payment channel open \u2014 start() did not complete"
|
|
380
|
-
);
|
|
381
|
-
}
|
|
382
|
-
return this.channelId;
|
|
383
|
-
}
|
|
384
|
-
};
|
|
385
|
-
function nowSeconds() {
|
|
386
|
-
return Math.floor(Date.now() / 1e3);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
export {
|
|
390
|
-
DEFAULT_DAEMON_PORT,
|
|
391
|
-
defaultDaemonPort,
|
|
392
|
-
defaultLockDir,
|
|
393
|
-
DaemonIdentityConflictError,
|
|
394
|
-
StandaloneLockError,
|
|
395
|
-
checkDaemonIdentity,
|
|
396
|
-
NonceLock,
|
|
397
|
-
StandalonePublishError,
|
|
398
|
-
deriveRouteDestinations,
|
|
399
|
-
extractArweaveTxId,
|
|
400
|
-
StandalonePublisher
|
|
401
|
-
};
|
|
402
|
-
//# sourceMappingURL=chunk-XRRU6YBQ.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/standalone/nonce-guard.ts","../src/standalone/standalone-publisher.ts"],"sourcesContent":["/**\n * Nonce-ownership guard for the STANDALONE embedded Publisher (#228).\n *\n * Why this exists: a payment channel's balance proof is a CUMULATIVE\n * watermark — the ChannelManager auto-increments the nonce and cumulative\n * amount on every `signBalanceProof`. Two writers signing claims on the same\n * channel from separate processes (a running `toon-clientd` daemon plus a\n * standalone embedded client, or two standalone processes) each keep their\n * own cumulative counter, so their claims race: the connector sees\n * non-monotonic watermarks and a re-signed claim can double-charge (the\n * hazard documented in packages/rig-web/tests/e2e/seed/lib/publish.ts).\n *\n * Two independent defenses, both keyed by the Nostr pubkey (one identity =\n * one channel set):\n *\n * 1. Daemon detection — probe the toon-clientd loopback control API\n * (`GET /status`) and REFUSE when it reports the SAME identity. A daemon\n * on a different identity holds different channels and is harmless.\n * 2. Advisory lockfile — an exclusive per-pubkey lockfile under the shared\n * `~/.toon-client` state dir so two STANDALONE processes can't race each\n * other (the daemon check only covers the daemon). Stale locks (dead pid)\n * are reclaimed.\n *\n * The daemon port and state-dir conventions are DUPLICATED from\n * `packages/client-mcp/src/daemon/config.ts` (default port 8787 /\n * `TOON_CLIENT_HTTP_PORT`; `~/.toon-client` / `TOON_CLIENT_HOME`).\n * `@toon-protocol/rig` must not depend on `@toon-protocol/client-mcp`\n * (the daemon package depends on this one for the #227 Publisher — the\n * import would be circular), so the constants live here with this note.\n * Keep them in sync.\n */\n\nimport { mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\n\n// ---------------------------------------------------------------------------\n// Shared conventions (duplicated from client-mcp — see module doc)\n// ---------------------------------------------------------------------------\n\n/** Default toon-clientd loopback control API port (client-mcp `httpPort`). */\nexport const DEFAULT_DAEMON_PORT = 8787;\n\n/** Daemon control API port: `TOON_CLIENT_HTTP_PORT` env, else 8787. */\nexport function defaultDaemonPort(): number {\n const env = process.env['TOON_CLIENT_HTTP_PORT'];\n const parsed = env ? Number(env) : NaN;\n return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_DAEMON_PORT;\n}\n\n/**\n * Shared client state dir: `TOON_CLIENT_HOME` env, else `~/.toon-client` —\n * the same dir the daemon keeps its config/channel stores in, so daemon and\n * standalone processes agree on where the advisory locks live.\n */\nexport function defaultLockDir(): string {\n return process.env['TOON_CLIENT_HOME'] ?? join(homedir(), '.toon-client');\n}\n\n// ---------------------------------------------------------------------------\n// Errors\n// ---------------------------------------------------------------------------\n\n/** A running toon-clientd holds the same identity (channel watermark owner). */\nexport class DaemonIdentityConflictError extends Error {\n constructor(\n /** The shared Nostr pubkey (hex). */\n public readonly pubkey: string,\n /** The daemon control API URL that answered. */\n public readonly daemonUrl: string\n ) {\n super(\n `toon-clientd is running with this identity (${pubkey.slice(0, 8)}…) at ` +\n `${daemonUrl} — use daemon mode or stop the daemon. Two writers on one ` +\n `identity would race the payment channel's cumulative-claim watermark ` +\n `(double-charge hazard).`\n );\n this.name = 'DaemonIdentityConflictError';\n }\n}\n\n/** Another standalone process already holds the per-identity lock. */\nexport class StandaloneLockError extends Error {\n constructor(\n public readonly pubkey: string,\n public readonly lockPath: string,\n public readonly holderPid: number\n ) {\n super(\n `another standalone process (pid ${holderPid}) already holds the ` +\n `payment-channel lock for identity ${pubkey.slice(0, 8)}… ` +\n `(${lockPath}) — wait for it to finish or stop it. Two writers on one ` +\n `identity would race the cumulative-claim watermark.`\n );\n this.name = 'StandaloneLockError';\n }\n}\n\n// ---------------------------------------------------------------------------\n// Daemon detection\n// ---------------------------------------------------------------------------\n\nexport interface CheckDaemonOptions {\n /** Control API port (default: `TOON_CLIENT_HTTP_PORT` env, else 8787). */\n port?: number;\n /** Probe timeout, ms (default 1500 — loopback, so fast). */\n timeoutMs?: number;\n /** Inject a fetch implementation (tests). Defaults to global `fetch`. */\n fetchImpl?: typeof fetch;\n}\n\n/**\n * Probe the toon-clientd loopback control API and throw\n * {@link DaemonIdentityConflictError} when a daemon responds on `/status`\n * with `identity.nostrPubkey === pubkey`.\n *\n * Anything short of a positive identity match lets the caller proceed: no\n * listener, a timeout, a non-JSON response (some other local service on the\n * port), or a daemon on a DIFFERENT identity (its channels are keyed to its\n * own pubkey — no shared watermark).\n */\nexport async function checkDaemonIdentity(\n pubkey: string,\n options: CheckDaemonOptions = {}\n): Promise<void> {\n const port = options.port ?? defaultDaemonPort();\n const fetchImpl = options.fetchImpl ?? fetch;\n const url = `http://127.0.0.1:${port}/status`;\n\n let daemonPubkey: string | undefined;\n try {\n const res = await fetchImpl(url, {\n signal: AbortSignal.timeout(options.timeoutMs ?? 1500),\n });\n if (!res.ok) return; // listening, but not a healthy daemon status\n const body = (await res.json()) as {\n identity?: { nostrPubkey?: unknown };\n };\n const candidate = body?.identity?.nostrPubkey;\n if (typeof candidate === 'string') daemonPubkey = candidate;\n } catch {\n // Unreachable / timed out / not JSON → no same-identity daemon detected.\n return;\n }\n\n if (daemonPubkey !== undefined && daemonPubkey === pubkey) {\n throw new DaemonIdentityConflictError(pubkey, url);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Advisory per-identity lockfile\n// ---------------------------------------------------------------------------\n\ninterface LockFileContents {\n pid: number;\n pubkey: string;\n createdAt: string;\n}\n\nexport interface AcquireLockOptions {\n /** Directory the lockfile lives in (default: {@link defaultLockDir}). */\n dir?: string;\n /** Override the recorded pid (tests). Defaults to `process.pid`. */\n pid?: number;\n}\n\n/** True when `pid` refers to a live process we can see. */\nfunction pidAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (err) {\n // EPERM = alive but not ours; ESRCH (and anything else) = not running.\n return (err as NodeJS.ErrnoException).code === 'EPERM';\n }\n}\n\n/**\n * Exclusive advisory lock for one identity's payment-channel watermark.\n *\n * Acquired with an atomic `wx` create of `standalone-<pubkey>.lock` (JSON:\n * pid + pubkey + timestamp) under the shared state dir. A pre-existing lock\n * whose pid is dead (or whose contents are unreadable) is STALE and gets\n * reclaimed; a live holder throws {@link StandaloneLockError}. Released\n * explicitly via {@link release} and best-effort on process exit.\n */\nexport class NonceLock {\n private released = false;\n private readonly exitHandler: () => void;\n\n private constructor(\n public readonly pubkey: string,\n public readonly lockPath: string\n ) {\n this.exitHandler = () => {\n try {\n unlinkSync(this.lockPath);\n } catch {\n // best-effort — the pid check makes a leftover lock reclaimable anyway\n }\n };\n process.once('exit', this.exitHandler);\n }\n\n static async acquire(\n pubkey: string,\n options: AcquireLockOptions = {}\n ): Promise<NonceLock> {\n const dir = options.dir ?? defaultLockDir();\n const pid = options.pid ?? process.pid;\n const lockPath = join(dir, `standalone-${pubkey}.lock`);\n mkdirSync(dir, { recursive: true });\n\n const payload = JSON.stringify(\n {\n pid,\n pubkey,\n createdAt: new Date().toISOString(),\n } satisfies LockFileContents,\n null,\n 2\n );\n\n // Two attempts: initial exclusive create, then one retry after reclaiming\n // a stale lock. A live holder on either attempt is a hard refusal.\n for (let attempt = 0; attempt < 2; attempt++) {\n try {\n writeFileSync(lockPath, payload, { flag: 'wx' });\n return new NonceLock(pubkey, lockPath);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== 'EEXIST') throw err;\n\n let holderPid: number | undefined;\n try {\n const parsed = JSON.parse(\n readFileSync(lockPath, 'utf8')\n ) as Partial<LockFileContents>;\n if (typeof parsed.pid === 'number') holderPid = parsed.pid;\n } catch {\n // Unreadable/corrupt lock → treat as stale.\n }\n\n // Same process re-acquiring (e.g. a retried push in one CLI run) is\n // not a race — the ChannelManager watermark is shared in-process.\n if (\n holderPid !== undefined &&\n holderPid !== pid &&\n pidAlive(holderPid)\n ) {\n throw new StandaloneLockError(pubkey, lockPath, holderPid);\n }\n\n // Stale (dead pid / corrupt / our own pid): reclaim and retry.\n try {\n unlinkSync(lockPath);\n } catch {\n // Lost a reclaim race with another process — the retry's exclusive\n // create settles the winner.\n }\n }\n }\n // Both attempts hit EEXIST → another process is actively (re)creating it.\n throw new StandaloneLockError(pubkey, lockPath, -1);\n }\n\n /** Remove the lockfile and detach the exit hook. Idempotent. */\n release(): void {\n if (this.released) return;\n this.released = true;\n process.removeListener('exit', this.exitHandler);\n try {\n unlinkSync(this.lockPath);\n } catch {\n // already gone — fine\n }\n }\n}\n","/**\n * StandalonePublisher — impl 2 of the {@link Publisher} seam (#228): an\n * EMBEDDED ToonClient constructed from the caller's config (mnemonic +\n * account index, the exact `packages/client/src/config.ts` shape) instead of\n * routing through a running toon-clientd. Made for CI jobs, servers, and\n * one-shot CLI runs where no daemon exists.\n *\n * Paid-write mechanics mirror the production daemon path\n * (`client-mcp/src/daemon/client-runner.ts`) and the proven seed pipeline\n * (`rig/tests/e2e/seed/lib/{publish,git-builder}.ts`):\n *\n * - one signed balance-proof CLAIM per write (`signBalanceProof(channelId,\n * fee)` — the ChannelManager accumulates the cumulative watermark),\n * - publishes route to the relay write destination with a flat per-event\n * fee (the daemon's `feePerEvent` convention),\n * - git objects upload as kind:5094 store writes tagged\n * Git-SHA/Git-Type/Repo, priced at bytes × per-byte rate (the seed\n * pipeline's bid), routed to the store destination via `proxyPath:\n * '/store'`, with the Arweave txId decoded from the FULFILL data.\n *\n * Because the embedded client signs claims on the SAME channels a daemon on\n * the same identity would, every paid operation is preceded by the nonce\n * guard (./nonce-guard.ts): refuse if a toon-clientd holds this identity,\n * and hold an exclusive per-pubkey lockfile against other standalone\n * processes for the lifetime of this publisher.\n */\n\nimport type {\n PublishEventResult,\n SignedBalanceProof,\n ToonClientConfig,\n} from '@toon-protocol/client';\nimport { ToonClient, parseFulfillHttp } from '@toon-protocol/client';\nimport { MAX_OBJECT_SIZE } from '../objects.js';\nimport type { UnsignedEvent } from '../nip34-events.js';\nimport type {\n FeeRates,\n GitObjectUpload,\n PublishReceipt,\n Publisher,\n UploadReceipt,\n} from '../publisher.js';\nimport { checkDaemonIdentity, NonceLock } from './nonce-guard.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** A fully-signed Nostr event (structural subset of nostr-tools' NostrEvent). */\nexport interface SignedNostrEvent extends UnsignedEvent {\n id: string;\n pubkey: string;\n sig: string;\n}\n\n/**\n * The slice of `ToonClient` the publisher drives. Kept structural so tests\n * inject a mock and alternative client builds stay compatible.\n */\nexport interface ToonClientLike {\n start(): Promise<unknown>;\n stop(): Promise<void>;\n isStarted?(): boolean;\n getPublicKey(): string;\n signEvent(template: UnsignedEvent): SignedNostrEvent;\n openChannel(destination?: string): Promise<string>;\n signBalanceProof(\n channelId: string,\n amount: bigint\n ): Promise<SignedBalanceProof>;\n publishEvent(\n event: SignedNostrEvent,\n options?: {\n destination?: string;\n claim?: SignedBalanceProof;\n ilpAmount?: bigint;\n proxyPath?: string;\n }\n ): Promise<PublishEventResult>;\n}\n\nexport interface StandalonePublisherOptions {\n /**\n * ToonClient config (mnemonic + mnemonicAccountIndex + proxy/BTP uplink +\n * settlement fields — see `packages/client/src/config.ts`). Exactly one of\n * `clientConfig` | `client` is required.\n */\n clientConfig?: ToonClientConfig;\n /** Pre-built client (tests / advanced callers). */\n client?: ToonClientLike;\n /**\n * ILP route for event publishes (relay `/write`). Default: derived from the\n * config's `destinationAddress` anchor (`<base>.relay.store` → `<base>.relay`,\n * matching the daemon's route derivation), else the anchor itself.\n */\n publishDestination?: string;\n /**\n * ILP route for git-object uploads (store `/store` → Arweave). Default:\n * derived like `publishDestination` (`<base>.relay.store` → `<base>.store`).\n */\n storeDestination?: string;\n /**\n * ILP destination the payment channel anchors to. Default: the client\n * config's `destinationAddress`.\n */\n channelDestination?: string;\n /** Flat fee per published event (daemon `feePerEvent` convention). Default 1n. */\n eventFee?: bigint;\n /** Upload fee per object body byte (seed pipeline's bid rate). Default 10n. */\n uploadFeePerByte?: bigint;\n /** Daemon control API port probed by the nonce guard. */\n daemonPort?: number;\n /** Directory for the per-identity advisory lockfile. */\n lockDir?: string;\n /** Fetch impl for the daemon probe (tests). */\n fetchImpl?: typeof fetch;\n}\n\n/** A relay/store rejected a paid write (fee NOT spent iff the claim failed too). */\nexport class StandalonePublishError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'StandalonePublishError';\n }\n}\n\n// ---------------------------------------------------------------------------\n// Route derivation (duplicated from client-mcp daemon/config.ts — that\n// package depends on this one for #227, so importing it back would be\n// circular; keep in sync)\n// ---------------------------------------------------------------------------\n\n/**\n * Derive publish/store routes from the channel anchor. Behind the devnet\n * proxy the anchor is `<base>.relay.store` (e.g. `g.proxy.relay.store`):\n * publishes terminate at `<base>.relay`, uploads at `<base>.store`. Anchors\n * not matching the convention pass through unchanged.\n */\nexport function deriveRouteDestinations(anchor: string): {\n publish: string;\n store: string;\n} {\n const segs = anchor.split('.');\n if (segs.at(-1) === 'store' && segs.at(-2) === 'relay') {\n const base = segs.slice(0, -2).join('.');\n return { publish: `${base}.relay`, store: `${base}.store` };\n }\n return { publish: anchor, store: anchor };\n}\n\n// ---------------------------------------------------------------------------\n// FULFILL → Arweave txId (mirrors @toon-protocol/client blob-storage.ts,\n// whose extractor is not exported; uses the exported parseFulfillHttp)\n// ---------------------------------------------------------------------------\n\n/** Arweave tx IDs are base64url-encoded 32-byte values (43 chars). */\nconst ARWEAVE_TX_ID_REGEX = /^[A-Za-z0-9_-]{43}$/;\n\n/**\n * Decode the Arweave txId from a store-write FULFILL. The deployed payment\n * proxy returns the store's verbatim HTTP/1.1 response\n * (`{\"accept\":true,\"txId\":…}` body); legacy non-proxy providers return bare\n * `base64(utf8(txId))`.\n *\n * @throws {StandalonePublishError} when no valid txId can be extracted.\n */\nexport function extractArweaveTxId(base64Data: string): string {\n const http = parseFulfillHttp(base64Data);\n\n if (!http.isHttp) {\n const legacy = Buffer.from(base64Data, 'base64').toString('utf8');\n if (!ARWEAVE_TX_ID_REGEX.test(legacy)) {\n throw new StandalonePublishError(\n `FULFILL data is not a valid Arweave tx ID: \"${legacy}\"`\n );\n }\n return legacy;\n }\n\n if (http.status < 200 || http.status >= 300) {\n throw new StandalonePublishError(\n `git-object upload failed: store returned HTTP ${http.status}` +\n (http.body ? ` - ${http.body}` : '')\n );\n }\n\n let parsed: { accept?: boolean; txId?: unknown; data?: unknown; error?: unknown };\n try {\n parsed = JSON.parse(http.body) as typeof parsed;\n } catch {\n throw new StandalonePublishError(\n `git-object upload response body was not valid JSON: \"${http.body}\"`\n );\n }\n\n if (parsed.accept === false) {\n const reason = typeof parsed.error === 'string' ? `: ${parsed.error}` : '';\n throw new StandalonePublishError(\n `git-object upload rejected by store (accept:false)${reason}`\n );\n }\n\n if (typeof parsed.txId === 'string' && ARWEAVE_TX_ID_REGEX.test(parsed.txId)) {\n return parsed.txId;\n }\n if (typeof parsed.data === 'string' && parsed.data.length > 0) {\n const decoded = Buffer.from(parsed.data, 'base64').toString('utf8');\n if (ARWEAVE_TX_ID_REGEX.test(decoded)) return decoded;\n }\n\n throw new StandalonePublishError(\n `git-object upload response did not contain a valid Arweave tx ID: \"${http.body}\"`\n );\n}\n\n// ---------------------------------------------------------------------------\n// StandalonePublisher\n// ---------------------------------------------------------------------------\n\nexport class StandalonePublisher implements Publisher {\n private readonly client: ToonClientLike;\n private readonly ownsClient: boolean;\n private readonly publishDestination: string | undefined;\n private readonly storeDestination: string | undefined;\n private readonly channelDestination: string | undefined;\n private readonly eventFee: bigint;\n private readonly uploadFeePerByte: bigint;\n private readonly daemonPort: number | undefined;\n private readonly lockDir: string | undefined;\n private readonly fetchImpl: typeof fetch | undefined;\n\n private lock: NonceLock | undefined;\n private channelId: string | undefined;\n private readyPromise: Promise<void> | undefined;\n\n constructor(options: StandalonePublisherOptions) {\n if (options.client && options.clientConfig) {\n throw new Error(\n 'StandalonePublisher: provide either `clientConfig` or `client`, not both'\n );\n }\n if (options.client) {\n this.client = options.client;\n this.ownsClient = false;\n } else if (options.clientConfig) {\n this.client = new ToonClient(options.clientConfig);\n this.ownsClient = true;\n } else {\n throw new Error(\n 'StandalonePublisher: one of `clientConfig` (mnemonic-based ToonClient config) or `client` is required'\n );\n }\n\n // Routes: explicit option → derived from the channel anchor (same\n // `<base>.relay.store` convention the daemon resolves).\n const anchor =\n options.channelDestination ?? options.clientConfig?.destinationAddress;\n const routes = anchor ? deriveRouteDestinations(anchor) : undefined;\n this.publishDestination = options.publishDestination ?? routes?.publish;\n this.storeDestination = options.storeDestination ?? routes?.store;\n this.channelDestination = options.channelDestination;\n\n this.eventFee = options.eventFee ?? 1n;\n this.uploadFeePerByte = options.uploadFeePerByte ?? 10n;\n this.daemonPort = options.daemonPort;\n this.lockDir = options.lockDir;\n this.fetchImpl = options.fetchImpl;\n }\n\n /** Hex Nostr pubkey of the embedded identity (available before start). */\n getPublicKey(): string {\n return this.client.getPublicKey();\n }\n\n /**\n * Run the nonce guard, start the embedded client, and open (or resume) the\n * payment channel. Called lazily by the first paid operation; safe to call\n * eagerly to fail fast. Idempotent.\n */\n start(): Promise<void> {\n this.readyPromise ??= this.doStart().catch((err: unknown) => {\n // Let a later call retry (e.g. after the conflicting daemon stops).\n this.readyPromise = undefined;\n throw err;\n });\n return this.readyPromise;\n }\n\n private async doStart(): Promise<void> {\n const pubkey = this.client.getPublicKey();\n\n // Guard 1: refuse while a toon-clientd holds this identity.\n await checkDaemonIdentity(pubkey, {\n ...(this.daemonPort !== undefined ? { port: this.daemonPort } : {}),\n ...(this.fetchImpl ? { fetchImpl: this.fetchImpl } : {}),\n });\n\n // Guard 2: exclusive advisory lock against other standalone processes.\n this.lock = await NonceLock.acquire(pubkey, {\n ...(this.lockDir !== undefined ? { dir: this.lockDir } : {}),\n });\n\n try {\n if (this.client.isStarted?.() !== true) {\n await this.client.start();\n }\n // Idempotent — returns the existing channel for the peer if one is open.\n this.channelId = await this.client.openChannel(this.channelDestination);\n } catch (err) {\n this.lock.release();\n this.lock = undefined;\n throw err;\n }\n }\n\n /** Release the identity lock and stop the embedded client (if we own it). */\n async stop(): Promise<void> {\n this.lock?.release();\n this.lock = undefined;\n this.readyPromise = undefined;\n this.channelId = undefined;\n if (this.ownsClient) {\n await this.client.stop();\n }\n }\n\n // ── Publisher ─────────────────────────────────────────────────────────────\n\n /**\n * Fee rates for `planPush` estimation: the flat per-event fee and the\n * per-byte upload rate this publisher pays (daemon `feePerEvent` and seed\n * bid-rate conventions; override via options).\n */\n getFeeRates(): Promise<FeeRates> {\n return Promise.resolve({\n uploadFeePerByte: this.uploadFeePerByte,\n eventFee: this.eventFee,\n });\n }\n\n /**\n * Upload one git object as a kind:5094 store write (Git-SHA/Git-Type/Repo\n * tagged — the proven seed-pipeline shape), signing one balance-proof claim\n * for `body.length × uploadFeePerByte`.\n */\n async uploadGitObject(upload: GitObjectUpload): Promise<UploadReceipt> {\n if (upload.body.length > MAX_OBJECT_SIZE) {\n throw new StandalonePublishError(\n `git object ${upload.sha} exceeds the ${MAX_OBJECT_SIZE}-byte limit: ${upload.body.length} bytes`\n );\n }\n await this.start();\n const channelId = this.requireChannel();\n\n const fee = BigInt(upload.body.length) * this.uploadFeePerByte;\n const event = this.client.signEvent({\n kind: 5094,\n content: '',\n created_at: nowSeconds(),\n tags: [\n ['i', upload.body.toString('base64'), 'blob'],\n ['bid', fee.toString(), 'usdc'],\n ['output', 'application/octet-stream'],\n ['Git-SHA', upload.sha],\n ['Git-Type', upload.type],\n ['Repo', upload.repoId],\n ],\n });\n\n const claim = await this.client.signBalanceProof(channelId, fee);\n const result = await this.client.publishEvent(event, {\n ...(this.storeDestination ? { destination: this.storeDestination } : {}),\n claim,\n ilpAmount: fee,\n // The store backend serves POST /store (not the relay's /write).\n proxyPath: '/store',\n });\n if (!result.success) {\n throw new StandalonePublishError(\n `git-object upload rejected (${upload.sha}): ${result.error ?? 'store rejected the write'}`\n );\n }\n if (!result.data) {\n throw new StandalonePublishError(\n `git-object upload FULFILL carried no data (${upload.sha}); expected the Arweave tx ID`\n );\n }\n return { txId: extractArweaveTxId(result.data), feePaid: fee };\n }\n\n /**\n * Sign the event with the embedded identity and pay-to-publish it through\n * the relay write route, one claim for the flat per-event fee.\n *\n * `relayUrls` is the interface's plural forward-compat surface (parked\n * #84): the standalone impl routes over ILP to its single configured\n * publish destination, so more than one relay is refused rather than\n * silently half-published.\n */\n async publishEvent(\n event: UnsignedEvent,\n relayUrls: string[]\n ): Promise<PublishReceipt> {\n if (relayUrls.length > 1) {\n throw new StandalonePublishError(\n `multi-relay publish is not supported yet (got ${relayUrls.length} relays) — the standalone publisher routes to a single relay destination (#84 parked)`\n );\n }\n await this.start();\n const channelId = this.requireChannel();\n\n const signed = this.client.signEvent(event);\n const fee = this.eventFee;\n const claim = await this.client.signBalanceProof(channelId, fee);\n const result = await this.client.publishEvent(signed, {\n ...(this.publishDestination\n ? { destination: this.publishDestination }\n : {}),\n claim,\n ilpAmount: fee,\n });\n if (!result.success) {\n throw new StandalonePublishError(\n `publish rejected (kind ${event.kind}): ${result.error ?? 'relay rejected the event'}`\n );\n }\n return { eventId: result.eventId ?? signed.id, feePaid: fee };\n }\n\n private requireChannel(): string {\n if (!this.channelId) {\n throw new StandalonePublishError(\n 'no payment channel open — start() did not complete'\n );\n }\n return this.channelId;\n }\n}\n\nfunction nowSeconds(): number {\n return Math.floor(Date.now() / 1000);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
fetchRemoteState
|
|
3
|
-
} from "./chunk-G4W4MH6G.js";
|
|
4
|
-
import {
|
|
5
|
-
resolveIdentity
|
|
6
|
-
} from "./chunk-QD437XAW.js";
|
|
7
|
-
import "./chunk-HPSOQP7Q.js";
|
|
8
|
-
import {
|
|
9
|
-
StandalonePublisher
|
|
10
|
-
} from "./chunk-XRRU6YBQ.js";
|
|
11
|
-
import "./chunk-X2CZPPDM.js";
|
|
12
|
-
|
|
13
|
-
// src/cli/standalone-mode.ts
|
|
14
|
-
import { readFileSync } from "fs";
|
|
15
|
-
import { homedir } from "os";
|
|
16
|
-
import { join } from "path";
|
|
17
|
-
import {
|
|
18
|
-
GenesisPeerLoader,
|
|
19
|
-
decodeEventFromToon,
|
|
20
|
-
encodeEventToToon
|
|
21
|
-
} from "@toon-protocol/core";
|
|
22
|
-
var MissingUplinkError = class extends Error {
|
|
23
|
-
constructor(configPath) {
|
|
24
|
-
super(
|
|
25
|
-
`no write uplink configured: set TOON_CLIENT_PROXY_URL (connector payment proxy) or TOON_CLIENT_BTP_URL, or add proxyUrl/btpUrl to ${configPath}`
|
|
26
|
-
);
|
|
27
|
-
this.name = "MissingUplinkError";
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
function configDir(env) {
|
|
31
|
-
return env["TOON_CLIENT_HOME"] ?? join(homedir(), ".toon-client");
|
|
32
|
-
}
|
|
33
|
-
function readClientConfig(path) {
|
|
34
|
-
try {
|
|
35
|
-
return JSON.parse(readFileSync(path, "utf8"));
|
|
36
|
-
} catch (err) {
|
|
37
|
-
if (err.code === "ENOENT") return {};
|
|
38
|
-
throw new Error(
|
|
39
|
-
`failed to read client config at ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
async function createStandaloneContext(options) {
|
|
44
|
-
const { env } = options;
|
|
45
|
-
const dir = configDir(env);
|
|
46
|
-
const configPath = join(dir, "config.json");
|
|
47
|
-
const file = readClientConfig(configPath);
|
|
48
|
-
const identity = await resolveIdentity(options);
|
|
49
|
-
const proxyUrl = env["TOON_CLIENT_PROXY_URL"] ?? file.proxyUrl;
|
|
50
|
-
const btpUrl = env["TOON_CLIENT_BTP_URL"] ?? file.btpUrl;
|
|
51
|
-
if (!proxyUrl && !btpUrl) throw new MissingUplinkError(configPath);
|
|
52
|
-
const genesisSeed = GenesisPeerLoader.loadGenesisPeers()[0];
|
|
53
|
-
const relayUrl = env["TOON_CLIENT_RELAY_URL"] ?? file.relayUrl ?? genesisSeed?.relayUrl ?? "ws://localhost:7100";
|
|
54
|
-
const destination = env["TOON_CLIENT_DESTINATION"] ?? file.destination ?? genesisSeed?.ilpAddress ?? "g.proxy";
|
|
55
|
-
const publishDestination = env["TOON_CLIENT_PUBLISH_DESTINATION"] ?? file.publishDestination;
|
|
56
|
-
const storeDestination = env["TOON_CLIENT_STORE_DESTINATION"] ?? file.storeDestination;
|
|
57
|
-
const network = env["TOON_CLIENT_NETWORK"] ?? file.network;
|
|
58
|
-
const channelStorePath = file.channelStorePath ?? join(dir, "channels.json");
|
|
59
|
-
const eventFee = BigInt(file.feePerEvent ?? "1");
|
|
60
|
-
const clientConfig = {
|
|
61
|
-
// validateConfig requires connectorUrl OR proxyUrl; with BTP-only config a
|
|
62
|
-
// dummy connectorUrl satisfies it (unused at runtime — same convention as
|
|
63
|
-
// the daemon).
|
|
64
|
-
...proxyUrl ? { proxyUrl } : { connectorUrl: "http://127.0.0.1:1" },
|
|
65
|
-
mnemonic: identity.mnemonic,
|
|
66
|
-
mnemonicAccountIndex: identity.accountIndex,
|
|
67
|
-
ilpInfo: {
|
|
68
|
-
pubkey: "00".repeat(32),
|
|
69
|
-
ilpAddress: "g.toon.client",
|
|
70
|
-
btpEndpoint: btpUrl ?? "",
|
|
71
|
-
assetCode: "USD",
|
|
72
|
-
assetScale: 6
|
|
73
|
-
},
|
|
74
|
-
toonEncoder: encodeEventToToon,
|
|
75
|
-
toonDecoder: decodeEventFromToon,
|
|
76
|
-
...btpUrl ? { btpUrl, btpAuthToken: "" } : {},
|
|
77
|
-
destinationAddress: destination,
|
|
78
|
-
relayUrl: "",
|
|
79
|
-
// remote state uses fetchRemoteState, not bootstrap discovery
|
|
80
|
-
knownPeers: [],
|
|
81
|
-
channelStorePath,
|
|
82
|
-
...network ? { network } : {},
|
|
83
|
-
...file.supportedChains ? { supportedChains: file.supportedChains } : {},
|
|
84
|
-
...file.settlementAddresses ? { settlementAddresses: file.settlementAddresses } : {},
|
|
85
|
-
...file.preferredTokens ? { preferredTokens: file.preferredTokens } : {},
|
|
86
|
-
...file.tokenNetworks ? { tokenNetworks: file.tokenNetworks } : {},
|
|
87
|
-
...file.chainRpcUrls ? { chainRpcUrls: file.chainRpcUrls } : {},
|
|
88
|
-
...file.solanaChannel ? { solanaChannel: file.solanaChannel } : {},
|
|
89
|
-
...file.minaChannel ? { minaChannel: file.minaChannel } : {}
|
|
90
|
-
};
|
|
91
|
-
const publisher = new StandalonePublisher({
|
|
92
|
-
clientConfig,
|
|
93
|
-
eventFee,
|
|
94
|
-
...publishDestination ? { publishDestination } : {},
|
|
95
|
-
...storeDestination ? { storeDestination } : {}
|
|
96
|
-
});
|
|
97
|
-
return {
|
|
98
|
-
ownerPubkey: publisher.getPublicKey(),
|
|
99
|
-
identitySource: identity.source,
|
|
100
|
-
identitySourceLabel: identity.sourceLabel,
|
|
101
|
-
publisher,
|
|
102
|
-
defaultRelayUrls: [relayUrl],
|
|
103
|
-
fetchRemote: (args) => fetchRemoteState(args),
|
|
104
|
-
stop: () => publisher.stop()
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
export {
|
|
108
|
-
MissingUplinkError,
|
|
109
|
-
createStandaloneContext
|
|
110
|
-
};
|
|
111
|
-
//# sourceMappingURL=standalone-mode-CAYWOURK.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli/standalone-mode.ts"],"sourcesContent":["/**\n * The embedded-client (standalone) publisher backing every paid `rig`\n * command: build a nonce-guarded {@link StandalonePublisher} from the\n * caller's own identity and config.\n *\n * Identity comes from the #248 precedence chain (`./identity.ts`:\n * RIG_MNEMONIC env → TOON_CLIENT_MNEMONIC env alias → project `.env` →\n * `~/.toon-client` keystore/config). The remaining config resolution\n * DUPLICATES the toon-clientd conventions\n * (`packages/client-mcp/src/daemon/config.ts`) the same way\n * `../standalone/nonce-guard.ts` does — this package must not import\n * `@toon-protocol/client-mcp` (circular; see that module's doc). Keep in sync:\n *\n * - state dir: `TOON_CLIENT_HOME`, else `~/.toon-client`; config `config.json`\n * - env overrides: `TOON_CLIENT_PROXY_URL`, `TOON_CLIENT_BTP_URL`,\n * `TOON_CLIENT_RELAY_URL`, `TOON_CLIENT_DESTINATION`,\n * `TOON_CLIENT_PUBLISH_DESTINATION`, `TOON_CLIENT_STORE_DESTINATION`,\n * `TOON_CLIENT_NETWORK`\n * - defaults bootstrap from the committed genesis peer seed\n * (`@toon-protocol/core` GenesisPeerLoader)\n *\n * This module statically imports the OPTIONAL `@toon-protocol/client` peer\n * dependency, so it must only ever be reached through the dynamic import in\n * `push.ts` (see `./standalone-context.ts`).\n */\n\nimport { readFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport type { ToonClientConfig } from '@toon-protocol/client';\nimport {\n GenesisPeerLoader,\n decodeEventFromToon,\n encodeEventToToon,\n} from '@toon-protocol/core';\nimport { StandalonePublisher } from '../standalone/standalone-publisher.js';\nimport { fetchRemoteState } from '../remote-state.js';\nimport { resolveIdentity } from './identity.js';\nimport type {\n StandaloneContext,\n StandaloneLoadOptions,\n} from './standalone-context.js';\n\n/** The subset of the shared client config file standalone mode consumes. */\ninterface ClientConfigFile {\n network?: 'mainnet' | 'testnet' | 'devnet' | 'custom';\n mnemonicAccountIndex?: number;\n btpUrl?: string;\n proxyUrl?: string;\n relayUrl?: string;\n destination?: string;\n publishDestination?: string;\n storeDestination?: string;\n feePerEvent?: string;\n channelStorePath?: string;\n supportedChains?: string[];\n settlementAddresses?: Record<string, string>;\n preferredTokens?: Record<string, string>;\n tokenNetworks?: Record<string, string>;\n chainRpcUrls?: Record<string, string>;\n solanaChannel?: ToonClientConfig['solanaChannel'];\n minaChannel?: ToonClientConfig['minaChannel'];\n}\n\n/** An identity was resolved, but there is no way to send paid writes. */\nexport class MissingUplinkError extends Error {\n constructor(configPath: string) {\n super(\n 'no write uplink configured: set TOON_CLIENT_PROXY_URL ' +\n '(connector payment proxy) or TOON_CLIENT_BTP_URL, or add ' +\n `proxyUrl/btpUrl to ${configPath}`\n );\n this.name = 'MissingUplinkError';\n }\n}\n\nfunction configDir(env: NodeJS.ProcessEnv): string {\n return env['TOON_CLIENT_HOME'] ?? join(homedir(), '.toon-client');\n}\n\nfunction readClientConfig(path: string): ClientConfigFile {\n try {\n return JSON.parse(readFileSync(path, 'utf8')) as ClientConfigFile;\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return {};\n throw new Error(\n `failed to read client config at ${path}: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n}\n\n/**\n * Assemble an embedded-client standalone context: resolved identity + config\n * → ToonClientConfig → nonce-guarded StandalonePublisher (guard + client\n * start + channel open happen lazily on the first paid call, or eagerly via\n * the publisher's own `start`).\n */\nexport async function createStandaloneContext(\n options: StandaloneLoadOptions\n): Promise<StandaloneContext> {\n const { env } = options;\n const dir = configDir(env);\n const configPath = join(dir, 'config.json');\n const file = readClientConfig(configPath);\n const identity = await resolveIdentity(options);\n\n const proxyUrl = env['TOON_CLIENT_PROXY_URL'] ?? file.proxyUrl;\n const btpUrl = env['TOON_CLIENT_BTP_URL'] ?? file.btpUrl;\n if (!proxyUrl && !btpUrl) throw new MissingUplinkError(configPath);\n\n const genesisSeed = GenesisPeerLoader.loadGenesisPeers()[0];\n const relayUrl =\n env['TOON_CLIENT_RELAY_URL'] ??\n file.relayUrl ??\n genesisSeed?.relayUrl ??\n 'ws://localhost:7100';\n const destination =\n env['TOON_CLIENT_DESTINATION'] ??\n file.destination ??\n genesisSeed?.ilpAddress ??\n 'g.proxy';\n const publishDestination =\n env['TOON_CLIENT_PUBLISH_DESTINATION'] ?? file.publishDestination;\n const storeDestination =\n env['TOON_CLIENT_STORE_DESTINATION'] ?? file.storeDestination;\n const network = (env['TOON_CLIENT_NETWORK'] ?? file.network) as\n | ToonClientConfig['network']\n | undefined;\n const channelStorePath = file.channelStorePath ?? join(dir, 'channels.json');\n const eventFee = BigInt(file.feePerEvent ?? '1');\n\n const clientConfig: ToonClientConfig = {\n // validateConfig requires connectorUrl OR proxyUrl; with BTP-only config a\n // dummy connectorUrl satisfies it (unused at runtime — same convention as\n // the daemon).\n ...(proxyUrl ? { proxyUrl } : { connectorUrl: 'http://127.0.0.1:1' }),\n mnemonic: identity.mnemonic,\n mnemonicAccountIndex: identity.accountIndex,\n ilpInfo: {\n pubkey: '00'.repeat(32),\n ilpAddress: 'g.toon.client',\n btpEndpoint: btpUrl ?? '',\n assetCode: 'USD',\n assetScale: 6,\n },\n toonEncoder: encodeEventToToon,\n toonDecoder: decodeEventFromToon,\n ...(btpUrl ? { btpUrl, btpAuthToken: '' } : {}),\n destinationAddress: destination,\n relayUrl: '', // remote state uses fetchRemoteState, not bootstrap discovery\n knownPeers: [],\n channelStorePath,\n ...(network ? { network } : {}),\n ...(file.supportedChains ? { supportedChains: file.supportedChains } : {}),\n ...(file.settlementAddresses\n ? { settlementAddresses: file.settlementAddresses }\n : {}),\n ...(file.preferredTokens ? { preferredTokens: file.preferredTokens } : {}),\n ...(file.tokenNetworks ? { tokenNetworks: file.tokenNetworks } : {}),\n ...(file.chainRpcUrls ? { chainRpcUrls: file.chainRpcUrls } : {}),\n ...(file.solanaChannel ? { solanaChannel: file.solanaChannel } : {}),\n ...(file.minaChannel ? { minaChannel: file.minaChannel } : {}),\n };\n\n const publisher = new StandalonePublisher({\n clientConfig,\n eventFee,\n ...(publishDestination ? { publishDestination } : {}),\n ...(storeDestination ? { storeDestination } : {}),\n });\n\n return {\n ownerPubkey: publisher.getPublicKey(),\n identitySource: identity.source,\n identitySourceLabel: identity.sourceLabel,\n publisher,\n defaultRelayUrls: [relayUrl],\n fetchRemote: (args) => fetchRemoteState(args),\n stop: () => publisher.stop(),\n };\n}\n"],"mappings":"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","names":[]}
|