@toon-protocol/rig 3.6.0 → 4.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.
Files changed (34) hide show
  1. package/README.md +112 -152
  2. package/dist/chunk-CDSFVZDV.js +290 -0
  3. package/dist/chunk-CDSFVZDV.js.map +1 -0
  4. package/dist/{chunk-B5ISARMU.js → chunk-GVXBUL3C.js} +15 -2
  5. package/dist/chunk-GVXBUL3C.js.map +1 -0
  6. package/dist/chunk-LTFMPOHU.js +194 -0
  7. package/dist/chunk-LTFMPOHU.js.map +1 -0
  8. package/dist/{chunk-LCYCKWAT.js → chunk-YZRR2UO6.js} +86 -5
  9. package/dist/chunk-YZRR2UO6.js.map +1 -0
  10. package/dist/{chunk-HJDPM37T.js → chunk-ZZBA5VVL.js} +11 -7
  11. package/dist/{chunk-HJDPM37T.js.map → chunk-ZZBA5VVL.js.map} +1 -1
  12. package/dist/cli/rig.js +290 -523
  13. package/dist/cli/rig.js.map +1 -1
  14. package/dist/index.d.ts +3 -115
  15. package/dist/index.js +9 -121
  16. package/dist/index.js.map +1 -1
  17. package/dist/{publisher-BFcnZeJI.d.ts → publisher-CcL96ugq.d.ts} +58 -1
  18. package/dist/standalone/index.d.ts +241 -438
  19. package/dist/standalone/index.js +19 -11
  20. package/dist/standalone-mode-LEAC4A2L.js +390 -0
  21. package/dist/standalone-mode-LEAC4A2L.js.map +1 -0
  22. package/package.json +5 -4
  23. package/dist/chunk-B5ISARMU.js.map +0 -1
  24. package/dist/chunk-IZ6KLYQ6.js +0 -724
  25. package/dist/chunk-IZ6KLYQ6.js.map +0 -1
  26. package/dist/chunk-LCYCKWAT.js.map +0 -1
  27. package/dist/chunk-PWNZWQOH.js +0 -404
  28. package/dist/chunk-PWNZWQOH.js.map +0 -1
  29. package/dist/chunk-UZZ2RN7A.js +0 -469
  30. package/dist/chunk-UZZ2RN7A.js.map +0 -1
  31. package/dist/network-bootstrap-27FYE3N7.js +0 -40
  32. package/dist/network-bootstrap-27FYE3N7.js.map +0 -1
  33. package/dist/standalone-mode-5MNNJC2D.js +0 -928
  34. package/dist/standalone-mode-5MNNJC2D.js.map +0 -1
@@ -1,8 +1,178 @@
1
- import { ToonClientConfig, SignedBalanceProof, PublishEventResult } from '@toon-protocol/client';
2
- export { extractArweaveTxId } from '@toon-protocol/client';
3
- import { U as UnsignedEvent, P as Publisher, F as FeeRates, G as GitObjectUpload, a as UploadReceipt, B as BlobUpload, b as PublishReceipt } from '../publisher-BFcnZeJI.js';
1
+ import { P as Publisher, F as FeeRates, G as GitObjectUpload, U as UploadReceipt, B as BlobUpload, S as StoreJobRequest, a as StoreJobResponse, b as UnsignedEvent, c as PublishReceipt } from '../publisher-CcL96ugq.js';
4
2
  import '@toon-protocol/core/nip34';
5
3
 
4
+ /**
5
+ * The paid write path on `@toon-protocol/client` 2.x — a {@link Publisher}
6
+ * that pays a TOON connector per request.
7
+ *
8
+ * Everything a paid command needs comes from one connector URL: the client
9
+ * reads the node's own `GET /ilp` (addresses, prices, settlement chains,
10
+ * sealing key — connector ADR 0050), opens or adopts a payment channel, and
11
+ * pays each request with a signed claim. There is no relay to discover peers
12
+ * on (kind:10032 was removed by ADR 0046) and no topology to negotiate: a
13
+ * destination is a route the node prices, and `send()` pays what the route
14
+ * costs — including the per-kibibyte part of an ADR 0065 schedule, which the
15
+ * client works out from the sealed payload so this publisher never has to.
16
+ *
17
+ * Three request shapes, all ordinary paid POSTs:
18
+ *
19
+ * 1. `publishEvent` — a NIP-34 event signed with the OWNER's Nostr key,
20
+ * sent as `{ event }` to the publish route; the relay answers
21
+ * `{ eventId, storedAt, payment? }`.
22
+ * 2. `uploadGitObject` / `uploadBlob` — a kind:5094 store job carrying the
23
+ * bytes base64 in an `i` tag; the store answers the Arweave tx id.
24
+ * 3. `submitStoreJob` — a kind:5095 / 5096 job with `param` tags; the
25
+ * DVM's answer comes back as data, refusal included (ADR 0020: an answer
26
+ * is an answer, and the payer was charged for it).
27
+ *
28
+ * The store route may terminate on a different node than the one addressed
29
+ * (the edge forwards it). Then the payload must be sealed to THAT node's
30
+ * identity — `sealTo` — and, when the store node holds its own channel, paid
31
+ * through its own client. Both are options here; the common single-node case
32
+ * needs neither.
33
+ */
34
+
35
+ /** A paid request, as `ToonClient.send` takes it. */
36
+ interface PaidRequest {
37
+ method?: string;
38
+ target?: string;
39
+ headers?: Record<string, string> | [string, string][];
40
+ body?: string | Uint8Array | object;
41
+ }
42
+ interface PaidSendOptions {
43
+ /** The terminating connector's `GET /ilp` URL or raw key, on a forwarded route. */
44
+ sealTo?: string | Uint8Array;
45
+ timeoutMs?: number;
46
+ }
47
+ interface PaidFulfilled {
48
+ fulfilled: true;
49
+ status: number;
50
+ body: Uint8Array;
51
+ /** What this request paid; absent on a route priced at zero. */
52
+ claim?: {
53
+ amount: bigint;
54
+ };
55
+ }
56
+ interface PaidRefused {
57
+ fulfilled: false;
58
+ code: string;
59
+ message: string;
60
+ refusedBy?: string;
61
+ }
62
+ type PaidSendResult = PaidFulfilled | PaidRefused;
63
+ /** The route terms a connector publishes: a base price and an optional slope. */
64
+ interface RouteTerms {
65
+ price: bigint;
66
+ pricePerKib?: bigint;
67
+ }
68
+ /** The slice of `ToonClient` (2.x) this publisher needs. */
69
+ interface PaidClientLike {
70
+ send(destination: string, request: PaidRequest, options?: PaidSendOptions): Promise<PaidSendResult>;
71
+ /** The node's terms for a destination, or `null` when it prices no such route. */
72
+ routePrice(destination: string): Promise<RouteTerms | null>;
73
+ }
74
+ /** One leg of the write path: who pays, where to, and whom to seal to. */
75
+ interface PaidLeg {
76
+ client: PaidClientLike;
77
+ destination: string;
78
+ sealTo?: string | Uint8Array;
79
+ }
80
+ interface ConnectorPublisherOptions {
81
+ /** Pays the publish route and signs nothing — the Nostr key below signs. */
82
+ publish: PaidLeg;
83
+ /** Pays the store route (often the same client as `publish`). */
84
+ store: PaidLeg;
85
+ /** The owner's Nostr secret key: signs every event this publisher sends. */
86
+ nostrSecretKey: Uint8Array;
87
+ /**
88
+ * A configured per-event fee. Informational floor only: the connector's
89
+ * published price for the publish route wins when it is higher, and the
90
+ * client pays that price whatever this says.
91
+ */
92
+ eventFee?: bigint;
93
+ /** Per-packet timeout for uploads (the store's Arweave round trip). */
94
+ uploadTimeoutMs?: number;
95
+ warn?(line: string): void;
96
+ }
97
+ declare class ConnectorPublishError extends Error {
98
+ constructor(message: string);
99
+ }
100
+ /**
101
+ * Read the Arweave tx id out of a store's kind:5094 answer.
102
+ *
103
+ * The store answers one of two ways — `{ txId }` as JSON, or the bare id
104
+ * base64 in `data` — and refuses with `{ accept: false, error }`. A non-2xx
105
+ * status is a refusal too; both are thrown, because an upload without an id
106
+ * is an upload that did not happen (even though the packet was paid for).
107
+ */
108
+ declare function extractArweaveTxId(status: number, body: Uint8Array): string;
109
+ declare class ConnectorPublisher implements Publisher {
110
+ private readonly publish;
111
+ private readonly store;
112
+ private readonly nostrSecretKey;
113
+ private readonly configuredEventFee;
114
+ private readonly uploadTimeoutMs;
115
+ private readonly warn;
116
+ private rates;
117
+ constructor(options: ConnectorPublisherOptions);
118
+ /** Where events go, and where objects go. */
119
+ get destinations(): {
120
+ publish: string;
121
+ store: string;
122
+ };
123
+ /**
124
+ * The two routes' published terms, read once per publisher. A route the
125
+ * node does not price is a configuration error and says which knob to fix.
126
+ */
127
+ getFeeRates(): Promise<FeeRates>;
128
+ private readFeeRates;
129
+ private sign;
130
+ /** One paid kind:5094 write; returns the tx id and what the claim spent. */
131
+ private storeWrite;
132
+ uploadGitObject(upload: GitObjectUpload): Promise<UploadReceipt>;
133
+ uploadBlob(upload: BlobUpload): Promise<UploadReceipt>;
134
+ /**
135
+ * One NIP-90 job to the store node's DVM, over the paid path. A refusal is
136
+ * returned, not thrown: `accept: false` and a non-2xx status are the
137
+ * answer's content, and the payer was charged for the answer (ADR 0020).
138
+ */
139
+ submitStoreJob(request: StoreJobRequest): Promise<StoreJobResponse>;
140
+ /**
141
+ * Sign the event with the owner's key and pay to publish it. `relayUrls`
142
+ * is the seam's plural surface: this publisher routes to one configured
143
+ * destination, so more than one relay is refused rather than half-done.
144
+ */
145
+ publishEvent(event: UnsignedEvent, relayUrls: string[]): Promise<PublishReceipt>;
146
+ }
147
+
148
+ /**
149
+ * The Nostr signing key rig derives from a BIP-39 phrase.
150
+ *
151
+ * `@toon-protocol/client` 2.x is a pure payer and carries no Nostr identity
152
+ * (its own words), so the derivation that lived in the 0.x client moves here,
153
+ * unchanged: `m/44'/1237'/0'/0/<accountIndex>`. Unchanged matters — this is
154
+ * the key every existing rig repo is owned by (`toon.owner`, the kind:30617
155
+ * author), and it is also the secp256k1 key the client reads a phrase's EVM
156
+ * account from under `keyDerivation: 'legacy'`, so one phrase still yields one
157
+ * author AND the wallet its channels were funded at.
158
+ */
159
+ /** A BIP-32 non-hardened child index fits in 31 bits. */
160
+ declare const MAX_ACCOUNT_INDEX = 2147483647;
161
+ /** The path rig has always derived its Nostr key at. */
162
+ declare function nostrDerivationPath(accountIndex?: number): string;
163
+ interface NostrKey {
164
+ /** 32-byte secp256k1 secret. */
165
+ secretKey: Uint8Array;
166
+ /** 32-byte x-only public key, hex. */
167
+ pubkey: string;
168
+ }
169
+ /**
170
+ * Derive the Nostr key of a phrase at an account index.
171
+ *
172
+ * @throws when the index is out of range or the phrase yields no key.
173
+ */
174
+ declare function deriveNostrKeyFromMnemonic(mnemonic: string, accountIndex?: number): NostrKey;
175
+
6
176
  /**
7
177
  * Peer→channel map for the STANDALONE embedded publisher (#262).
8
178
  *
@@ -32,6 +202,15 @@ import '@toon-protocol/core/nip34';
32
202
  * whole lifetime — the same guard that serializes the claim watermark also
33
203
  * serializes this file for one identity. `rig channel list` reads only.
34
204
  *
205
+ * COUNTERPARTY: the key names a ROUTE, not a node — an ILP name can change
206
+ * hands (the devnet apex `g.toon` was retired and another node took over
207
+ * `g.toon.relay`). A record therefore also carries the counterparty
208
+ * settlement address it was opened against (`context.recipient`), which is
209
+ * re-checked against the destination's announced address before every resume
210
+ * ({@link counterpartyMatch}); a rotated counterparty
211
+ * {@link ChannelMapStore.supersede}s the record instead of signing claims a
212
+ * connector with no record of that channel refuses (`F01`).
213
+ *
35
214
  * CORRUPTION: an unreadable/invalid map file is a hard
36
215
  * {@link ChannelMapCorruptError} — surfaced BEFORE any on-chain open — never
37
216
  * an empty fallback. Falling back to "no channels" would silently open (and
@@ -87,6 +266,15 @@ interface ChannelMapRecord {
87
266
  /** ISO timestamps. */
88
267
  openedAt: string;
89
268
  lastUsedAt: string;
269
+ /**
270
+ * ISO timestamp this record was RETIRED from the resume path because the
271
+ * destination now announces a different settlement address than the channel
272
+ * was opened against (see {@link counterpartyMatch} /
273
+ * {@link ChannelMapStore.supersede}). A superseded record is never resumed
274
+ * again, but stays in {@link ChannelMapStore.list} so `rig channel
275
+ * list/close/settle` can still reclaim whatever it holds on-chain.
276
+ */
277
+ supersededAt?: string;
90
278
  }
91
279
  /** The composite key a record is stored under. */
92
280
  interface ChannelMapKey {
@@ -114,6 +302,35 @@ declare class ChannelMapCorruptError extends Error {
114
302
  readonly path: string;
115
303
  constructor(path: string, detail: string);
116
304
  }
305
+ /**
306
+ * Compare settlement addresses. EVM addresses travel in mixed checksum case
307
+ * (an announce may say `0xF29f…` where the record says `0xf29f…`) and are
308
+ * compared case-insensitively; anything else (base58 Solana, base58check
309
+ * Mina) is case-SIGNIFICANT and compared verbatim.
310
+ */
311
+ declare function sameSettlementAddress(a: string, b: string): boolean;
312
+ /**
313
+ * Does a recorded channel still belong to the counterparty the destination
314
+ * announces TODAY?
315
+ *
316
+ * The map key is `identity|destination|chain|tokenNetwork` — it carries no
317
+ * counterparty. When the node terminating an ILP name is REPLACED (the devnet
318
+ * apex `g.toon` was retired and another node took over `g.toon.relay`), all
319
+ * four key fields still match, so rig resumed a channel opened against the
320
+ * old node and signed claims against it. The new connector holds no record of
321
+ * that channel and refuses every packet: `F01 - claim rejected: names a
322
+ * channel this connector has no record of`. Every paid write failed until the
323
+ * cache entry was deleted by hand.
324
+ *
325
+ * - `'match'` — same counterparty, safe to resume.
326
+ * - `'mismatch'` — the counterparty rotated: the record must be superseded and
327
+ * the channel re-resolved.
328
+ * - `'unrecorded'` — nothing to compare (a record written before this field
329
+ * was validated, or a peer that announced no settlement address). Resume is
330
+ * allowed and the caller should enrich the record with what the announce
331
+ * says, so the NEXT run is verifiable.
332
+ */
333
+ declare function counterpartyMatch(record: Pick<ChannelMapRecord, 'context'>, announced: string | undefined): 'match' | 'mismatch' | 'unrecorded';
117
334
  /** The key fields of a full record. */
118
335
  declare function recordKey(record: ChannelMapRecord): ChannelMapKey;
119
336
  interface ChannelMapStoreOptions {
@@ -135,7 +352,9 @@ declare class ChannelMapStore {
135
352
  list(): ChannelMapRecord[];
136
353
  /**
137
354
  * Recorded channels for one (identity, destination) pair — the resume
138
- * candidates for a paid command. @throws {ChannelMapCorruptError}
355
+ * candidates for a paid command. Superseded records are EXCLUDED: they are
356
+ * retired from the resume path for good, and only `list()` (what `rig
357
+ * channel` enumerates) still shows them. @throws {ChannelMapCorruptError}
139
358
  */
140
359
  listFor(identity: string, destination: string): ChannelMapRecord[];
141
360
  /**
@@ -146,12 +365,27 @@ declare class ChannelMapStore {
146
365
  */
147
366
  record(record: Omit<ChannelMapRecord, 'openedAt' | 'lastUsedAt'> & Partial<Pick<ChannelMapRecord, 'openedAt' | 'lastUsedAt'>>): void;
148
367
  /**
149
- * Bump a record's `lastUsedAt` (and optionally its known on-chain deposit)
150
- * after resuming it. Unknown keys are a no-op.
368
+ * Bump a record's `lastUsedAt` (and optionally its known on-chain deposit,
369
+ * or the counterparty a pre-validation record was written without) after
370
+ * resuming it. Unknown keys are a no-op.
151
371
  */
152
372
  touch(key: ChannelMapKey, update?: {
153
373
  depositTotal?: string;
374
+ recipient?: string;
154
375
  }): void;
376
+ /**
377
+ * Retire a record from the resume path — the counterparty that terminates
378
+ * its destination has been REPLACED, so the channel it names is dead to the
379
+ * new node (`F01 - claim rejected`) even though every key field still
380
+ * matches (see {@link counterpartyMatch}).
381
+ *
382
+ * The record is MOVED to an archive key rather than deleted: it may still
383
+ * hold an on-chain deposit, and `rig channel list/close/settle` find
384
+ * channels by scanning `list()`, so deleting it would strand those funds
385
+ * behind hand-editing the JSON. Moving it also frees the live key for the
386
+ * re-resolved channel. Idempotent, and a no-op for an unknown record.
387
+ */
388
+ supersede(record: ChannelMapRecord): void;
155
389
  /**
156
390
  * Read one channel's nonce-watermark entry from the client's
157
391
  * `channels.json` (undefined when the file or entry is missing).
@@ -283,437 +517,6 @@ interface StandaloneMoneyOps {
283
517
  walletChainBalances(): Promise<WalletChainBalanceInfo[]>;
284
518
  }
285
519
 
286
- /**
287
- * StandalonePublisher — impl 2 of the {@link Publisher} seam (#228): an
288
- * EMBEDDED ToonClient constructed from the caller's config (mnemonic +
289
- * account index, the exact `packages/client/src/config.ts` shape) instead of
290
- * routing through a running toon-clientd. Made for CI jobs, servers, and
291
- * one-shot CLI runs where no daemon exists.
292
- *
293
- * Paid-write mechanics mirror the production daemon path
294
- * (`client-mcp/src/daemon/client-runner.ts`) and the proven seed pipeline
295
- * (`rig/tests/e2e/seed/lib/{publish,git-builder}.ts`):
296
- *
297
- * - one signed balance-proof CLAIM per write (`signBalanceProof(channelId,
298
- * fee)` — the ChannelManager accumulates the cumulative watermark),
299
- * - publishes route to the relay write destination with a flat per-event
300
- * fee (the daemon's `feePerEvent` convention),
301
- * - git objects upload as kind:5094 store writes tagged
302
- * Git-SHA/Git-Type/Repo, priced at bytes × per-byte rate (the seed
303
- * pipeline's bid), routed to the store destination via `proxyPath:
304
- * '/store'`, with the Arweave txId decoded from the FULFILL data.
305
- *
306
- * Because the embedded client signs claims on the SAME channels a daemon on
307
- * the same identity would, every paid operation is preceded by the nonce
308
- * guard (./nonce-guard.ts): refuse if a toon-clientd holds this identity,
309
- * and hold an exclusive per-pubkey lockfile against other standalone
310
- * processes for the lifetime of this publisher.
311
- *
312
- * Channel REUSE (#262): with a `channelMap` configured, start() resumes the
313
- * channel recorded for (identity, channel anchor) — `trackChannel` rehydrates
314
- * the cumulative-claim watermark from the client's channels.json — and
315
- * records any fresh lazy open, so sequential CLI invocations share ONE
316
- * on-chain channel instead of stranding a deposit per run (./channel-map.ts).
317
- */
318
-
319
- /** A fully-signed Nostr event (structural subset of nostr-tools' NostrEvent). */
320
- interface SignedNostrEvent extends UnsignedEvent {
321
- id: string;
322
- pubkey: string;
323
- sig: string;
324
- }
325
- /**
326
- * The slice of `ToonClient` the publisher drives. Kept structural so tests
327
- * inject a mock and alternative client builds stay compatible.
328
- */
329
- interface ToonClientLike {
330
- start(): Promise<unknown>;
331
- stop(): Promise<void>;
332
- isStarted?(): boolean;
333
- getPublicKey(): string;
334
- signEvent(template: UnsignedEvent): SignedNostrEvent;
335
- openChannel(destination?: string): Promise<string>;
336
- signBalanceProof(channelId: string, amount: bigint): Promise<SignedBalanceProof>;
337
- publishEvent(event: SignedNostrEvent, options?: {
338
- destination?: string;
339
- claim?: SignedBalanceProof;
340
- ilpAmount?: bigint;
341
- proxyPath?: string;
342
- }): Promise<PublishEventResult>;
343
- /** On-chain deposit total for a tracked channel (channel-map bookkeeping). */
344
- getChannelDepositTotal?(channelId: string): bigint;
345
- /** Re-read a RESUMED channel's on-chain deposit (persisted state omits it). */
346
- rehydrateChannelDeposit?(channelId: string, opts: {
347
- chain: string;
348
- tokenNetworkAddress: string;
349
- }): Promise<bigint | undefined>;
350
- /** Deposit extra collateral (base-unit delta) into an open channel. */
351
- depositToChannel?(channelId: string, amount: string | bigint): Promise<{
352
- channelId: string;
353
- txHash?: string;
354
- depositTotal: string;
355
- }>;
356
- /** Close a channel — starts the settlement challenge window (on-chain). */
357
- closeChannel?(channelId: string): Promise<{
358
- channelId: string;
359
- txHash?: string;
360
- /** Unix SECONDS, string-encoded. */
361
- closedAt: string;
362
- settleableAt: string;
363
- }>;
364
- /** Settle a closed channel after its window — releases funds (on-chain). */
365
- settleChannel?(channelId: string): Promise<{
366
- channelId: string;
367
- txHash?: string;
368
- }>;
369
- /**
370
- * Free FULL multi-chain wallet view (#299) — native coin + configured tokens
371
- * per chain — works on an UNSTARTED client (Solana/Mina addresses are derived
372
- * from the mnemonic on demand). `fallback` supplies Solana/Mina channel params
373
- * to read WHEN the client config has none, without touching settlement config.
374
- */
375
- getWalletBalances?(fallback?: WalletViewFallback): Promise<WalletChainBalanceInfo[]>;
376
- }
377
- /**
378
- * Wallet-view-only Solana/Mina channel defaults (#299): RPC/GraphQL + token used
379
- * to READ the balance of a chain the identity has no configured channel for, so
380
- * `rig balance` shows all three chains (0 for a not-yet-on-chain account). These
381
- * NEVER enter the client's settlement config — they cannot influence chain
382
- * negotiation — so a preset default here is safe even for a chain the wallet
383
- * cannot yet settle on.
384
- */
385
- interface WalletViewFallback {
386
- solanaChannel?: ToonClientConfig['solanaChannel'];
387
- minaChannel?: ToonClientConfig['minaChannel'];
388
- }
389
- interface StandalonePublisherOptions {
390
- /**
391
- * ToonClient config (mnemonic + mnemonicAccountIndex + proxy/BTP uplink +
392
- * settlement fields — see `packages/client/src/config.ts`). Exactly one of
393
- * `clientConfig` | `client` is required.
394
- */
395
- clientConfig?: ToonClientConfig;
396
- /** Pre-built client (tests / advanced callers). */
397
- client?: ToonClientLike;
398
- /**
399
- * ILP route for event publishes (relay `/write`). Default: derived from the
400
- * config's `destinationAddress` anchor (`<base>.relay.store` → `<base>.relay`,
401
- * matching the daemon's route derivation), else the anchor itself.
402
- */
403
- publishDestination?: string;
404
- /**
405
- * ILP route for git-object uploads (store `/store` → Arweave). Default:
406
- * derived like `publishDestination` (`<base>.relay.store` → `<base>.store`).
407
- */
408
- storeDestination?: string;
409
- /**
410
- * ILP destination the payment channel anchors to. Default: the client
411
- * config's `destinationAddress`.
412
- */
413
- channelDestination?: string;
414
- /**
415
- * Client config for a SECOND uplink dedicated to store writes, when the
416
- * store route terminates on a different node than publishes.
417
- *
418
- * A balance-proof claim is only verifiable by the connector holding that
419
- * channel — one with no record of it refuses the packet (`F01 - claim
420
- * rejected: names a channel this connector has no record of`). The store
421
- * node has its own settlement address and its own channel, and on the
422
- * two-box fleet the publish node has no route to it, so a store write
423
- * cannot ride the publish session. Given this, store writes go out on
424
- * their own client, against their own channel, recorded under their own
425
- * anchor so `rig channel` still sees it.
426
- *
427
- * A separate CLIENT rather than a second destination on the existing one
428
- * because `@toon-protocol/client` carries a single `btpUrl` per instance —
429
- * there is no per-destination uplink to configure. Omitted (the
430
- * single-node case) keeps the historical behaviour: one client, one
431
- * channel, store writes on the publish session.
432
- */
433
- storeClientConfig?: ToonClientConfig;
434
- /**
435
- * Pre-built store-leg client (tests / advanced callers) — the store-side
436
- * counterpart of `client`, and mutually exclusive with
437
- * `storeClientConfig` in the same way.
438
- */
439
- storeClient?: ToonClientLike;
440
- /** Flat fee per published event (daemon `feePerEvent` convention). Default 1n. */
441
- eventFee?: bigint;
442
- /**
443
- * FLAT per-packet route prices for the publish/store destinations, as the
444
- * payment peer announces them (kind:10032 `capabilities` — e.g.
445
- * `{capability:'os.publish', address:'g.proxy.relay', price:'1000'}`) or as
446
- * the terminating connector reports them from `GET /ilp/routes/price`.
447
- *
448
- * The connector gates every paid packet at the destination route's price: a
449
- * balance-proof claim advancing the channel by less is rejected (F06). Since
450
- * ADR 0020 (toon-client#452) a price is flat per handler, so `store` is the
451
- * WHOLE upload fee rather than a floor under a per-byte computation — an
452
- * upload of any size costs it. `publish` still floors {@link eventFee},
453
- * which remains a caller-chosen flat figure.
454
- *
455
- * A destination without a known price prices its packets at 0 here and lets
456
- * the connector be the one to refuse — this class never invents a rate.
457
- */
458
- routePrices?: {
459
- publish?: bigint;
460
- store?: bigint;
461
- };
462
- /** Daemon control API port probed by the nonce guard. */
463
- daemonPort?: number;
464
- /**
465
- * Skip Guard 1 (the same-identity daemon refusal, {@link checkDaemonIdentity})
466
- * — the force-standalone override ({@link standaloneForced} / RIG_STANDALONE
467
- * / `rig --standalone`). Guard 2, the exclusive per-identity {@link NonceLock},
468
- * ALWAYS runs regardless, so two standalone processes still can't race the
469
- * channel watermark. Default false (daemon-aware).
470
- */
471
- skipDaemonCheck?: boolean;
472
- /** Directory for the per-identity advisory lockfile. */
473
- lockDir?: string;
474
- /** Fetch impl for the daemon probe (tests). */
475
- fetchImpl?: typeof fetch;
476
- /**
477
- * Peer→channel map store (#262): start() resumes the channel recorded for
478
- * (identity, channel anchor) instead of opening a fresh one, and records
479
- * any fresh lazy open for the next invocation. Absent (embedded callers
480
- * managing their own channel lifecycle): the historical behaviour — open
481
- * lazily every run, record nothing.
482
- */
483
- channelMap?: ChannelMapStore;
484
- /**
485
- * Per-chain settlement parameters to BACK-FILL into the client's peer
486
- * negotiations after start (#264/#260): peers whose kind:10032 announce
487
- * carries no `tokenNetworks`/`preferredTokens` negotiate with those fields
488
- * empty, and the on-chain channel open then fails ("tokenNetwork address
489
- * is required"). Values here never override what a peer DID announce —
490
- * they only fill gaps, keyed by the negotiated chain id.
491
- */
492
- negotiationFallbacks?: {
493
- tokenNetworks?: Record<string, string>;
494
- preferredTokens?: Record<string, string>;
495
- };
496
- /** Sink for non-fatal channel-persistence warnings (default: stderr). */
497
- warn?: (line: string) => void;
498
- }
499
- /** A relay/store rejected a paid write (fee NOT spent iff the claim failed too). */
500
- declare class StandalonePublishError extends Error {
501
- constructor(message: string);
502
- }
503
- /**
504
- * Derive publish/store routes from the channel anchor. Behind the devnet
505
- * proxy the anchor is `<base>.relay.store` (e.g. `g.proxy.relay.store`):
506
- * publishes terminate at `<base>.relay`, uploads at `<base>.store`. Anchors
507
- * not matching the convention pass through unchanged.
508
- */
509
- declare function deriveRouteDestinations(anchor: string): {
510
- publish: string;
511
- store: string;
512
- };
513
-
514
- declare class StandalonePublisher implements Publisher {
515
- private readonly client;
516
- private readonly ownsClient;
517
- private readonly publishDestination;
518
- private readonly storeDestination;
519
- private readonly channelDestination;
520
- private readonly eventFee;
521
- private readonly routePrices;
522
- private readonly daemonPort;
523
- private readonly skipDaemonCheck;
524
- private readonly lockDir;
525
- private readonly fetchImpl;
526
- private readonly channelMap;
527
- /** ILP anchor the channel is keyed by in the map (peer/apex destination). */
528
- private readonly channelAnchor;
529
- private readonly negotiationFallbacks;
530
- private readonly warn;
531
- private readonly storeClientConfig;
532
- private readonly injectedStoreClient;
533
- /** Lazily built on the first store write — see {@link storeLeg}. */
534
- private storeClient;
535
- private storeChannelId;
536
- private storeLegPromise;
537
- private lock;
538
- private channelId;
539
- private readyPromise;
540
- /** Guard + client start only (no channel) — see {@link startClientOnly}. */
541
- private clientReadyPromise;
542
- /** True when the last start() RESUMED the recorded channel (#263). */
543
- private lastOpenResumed;
544
- constructor(options: StandalonePublisherOptions);
545
- /** Hex Nostr pubkey of the embedded identity (available before start). */
546
- getPublicKey(): string;
547
- /**
548
- * Run the nonce guard, start the embedded client, and open (or resume) the
549
- * payment channel. Called lazily by the first paid operation; safe to call
550
- * eagerly to fail fast. Idempotent.
551
- */
552
- start(): Promise<void>;
553
- /**
554
- * Run the nonce guard and start the embedded client WITHOUT touching the
555
- * payment channel (#263): `channel close`/`settle` operate on a RECORDED
556
- * channel and must never open a fresh one as a side effect of starting.
557
- * Idempotent; `start()` layers the channel open/resume on top of this.
558
- */
559
- startClientOnly(): Promise<void>;
560
- private doStartClient;
561
- /**
562
- * Back-fill negotiated peer metadata the announce did not carry
563
- * (#264/#260 root cause 3): after the client's bootstrap negotiation, any
564
- * peer negotiation missing `tokenNetwork`/`tokenAddress` gets the derived
565
- * per-chain fallback for its negotiated chain, BEFORE the lazy channel
566
- * open reads them. Announced values are never overridden.
567
- */
568
- private applyNegotiationFallbacks;
569
- /**
570
- * Open the payment channel — or, when the channel map has a record for
571
- * (identity, anchor), RESUME the recorded on-chain channel (#262).
572
- *
573
- * Resume seeds the client's ChannelManager (`trackChannel` rehydrates the
574
- * cumulative-claim watermark from channels.json; `peerChannels` makes the
575
- * subsequent `openChannel` return the same id instead of opening on-chain).
576
- * A fresh open is RECORDED so the next invocation resumes it. A corrupt map
577
- * file throws BEFORE anything is opened — never a silent duplicate open.
578
- */
579
- private openOrResumeChannel;
580
- /**
581
- * {@link openOrResumeChannel} for an arbitrary client + destination, so
582
- * the store leg gets the same resume-before-open guarantee (and the same
583
- * corruption check) as the publish leg rather than a second, weaker path.
584
- *
585
- * `anchor` defaults to the destination: a leg other than the publish one
586
- * is keyed in the channel map by the route it pays for.
587
- * `trackResumeState` is publish-only — {@link lastOpenResumed} feeds the
588
- * `resumed` field of this publisher's channel status, which describes the
589
- * publish channel and would be wrong to overwrite from a store open.
590
- */
591
- private openOrResumeChannelOn;
592
- /**
593
- * Try to resume one recorded channel: the first candidate whose peer is
594
- * still negotiated on the SAME chain + tokenNetwork and whose watermark
595
- * does not show it closed/settled. Returns the resumed record, if any.
596
- */
597
- private resumeRecordedChannel;
598
- /** Record a freshly opened channel (+ seed its claim watermark at 0/0). */
599
- private recordOpenedChannel;
600
- /** Release the identity lock and stop the embedded client (if we own it). */
601
- stop(): Promise<void>;
602
- /**
603
- * Explicit `rig channel open`: the SAME resume-or-open path the lazy paid
604
- * writes use (guard → start → resume the recorded channel or open + record
605
- * a fresh one), surfaced with a receipt — plus an optional extra collateral
606
- * deposit on top of the open/resume.
607
- */
608
- openChannelExplicit(opts?: {
609
- /** Extra collateral to deposit AFTER the open/resume (base units). */
610
- deposit?: bigint;
611
- }): Promise<ChannelOpenOutcome>;
612
- /**
613
- * Adopt a RECORDED channel into the running client so on-chain close/
614
- * settle/deposit can act on it: `trackChannel` rehydrates the claim
615
- * watermark + withdraw timers from channels.json, `peerChannels` binds the
616
- * peer, and the on-chain client's context cache is re-seeded (it only
617
- * learns context at open time — a resumed channel would otherwise throw
618
- * "No on-chain context").
619
- */
620
- private adoptRecordedChannel;
621
- /**
622
- * Close a recorded channel: starts the on-chain settlement challenge
623
- * window. The client persists `closedAt`/`settleableAt` into the claim
624
- * watermark store, which is exactly where `rig channel list`/`balance`
625
- * derive the closing/settleable/settled status from. Guard + client start,
626
- * but NEVER a channel open ({@link startClientOnly}).
627
- */
628
- closeRecordedChannel(record: ChannelMapRecord): Promise<ChannelCloseOutcome>;
629
- /**
630
- * Settle a recorded channel after its challenge window — releases the
631
- * remaining collateral. The client enforces the `now >= settleableAt` time
632
- * guard BEFORE spending gas (a too-early call throws its retryable
633
- * SettleTooEarlyError) and persists `settledAt` into the watermark store.
634
- */
635
- settleRecordedChannel(record: ChannelMapRecord): Promise<ChannelSettleOutcome>;
636
- /**
637
- * The full multi-chain wallet view (#299) for the embedded identity — native
638
- * coin + configured tokens (USDC) per chain — a FREE read on the UNSTARTED
639
- * client (no nonce guard, no uplink, no channel). The client derives the
640
- * Solana/Mina addresses from the mnemonic on demand, so ALL configured chains
641
- * appear even before a start. Best-effort per chain (an unreachable RPC yields
642
- * an `unreadable` chain, not a failure). `fallback` supplies Solana/Mina RPC
643
- * defaults (e.g. the network preset) for chains the config has no channel for.
644
- */
645
- readWalletChainBalances(fallback?: WalletViewFallback): Promise<WalletChainBalanceInfo[]>;
646
- /**
647
- * Fee rates for `planPush` estimation, both flat per packet (ADR 0020),
648
- * with the announced route prices folded in so the ESTIMATE always equals
649
- * the CLAIMS: `eventFee` is returned pre-floored at the publish route price,
650
- * and `uploadFee` IS the store route price.
651
- */
652
- getFeeRates(): Promise<FeeRates>;
653
- /**
654
- * The flat per-event claim: `eventFee`, floored at the publish route's
655
- * announced price (the connector rejects a claim below it — F06).
656
- */
657
- private effectiveEventFee;
658
- /**
659
- * The per-upload claim: the store route's flat price, whatever the object's
660
- * size. Byte-proportional pricing has no successor (ADR 0020) — the route
661
- * table is the price list — and the connector charges this figure whether
662
- * the body is 100 bytes or 100 KB.
663
- */
664
- private uploadFee;
665
- /**
666
- * Upload one git object as a kind:5094 store write (Git-SHA/Git-Type/Repo
667
- * tagged — the proven seed-pipeline shape), signing one balance-proof claim
668
- * for the store route's flat price
669
- * ({@link StandalonePublisherOptions.routePrices}) — ADR 0020 prices a
670
- * packet per handler, not per byte.
671
- *
672
- * #368: a blob's `Content-Type` is derived from its path extension (else
673
- * octet-stream) and sent in the `output` tag, which the store forwards onto
674
- * the Arweave upload's Content-Type — so a gateway serves `index.html` as
675
- * `text/html`, not `application/octet-stream`. Non-blob objects (trees,
676
- * commits, tags) are never served as files, so they stay octet-stream.
677
- */
678
- uploadGitObject(upload: GitObjectUpload): Promise<UploadReceipt>;
679
- /**
680
- * Upload one raw blob (no git envelope) with an explicit `Content-Type`
681
- * (#368) — a kind:5094 store write carrying just the `i`/`bid`/`output`
682
- * tags (and an optional `Repo` provenance tag), NOT the Git-SHA/Git-Type
683
- * tags. Without those the store keeps the bytes verbatim instead of
684
- * re-deriving a git envelope, which is exactly what the ar.io path manifest
685
- * needs. One balance-proof claim for the store route's flat price.
686
- */
687
- uploadBlob(upload: BlobUpload): Promise<UploadReceipt>;
688
- /**
689
- * Sign the event with the embedded identity and pay-to-publish it through
690
- * the relay write route, one claim for the flat per-event fee.
691
- *
692
- * `relayUrls` is the interface's plural forward-compat surface (parked
693
- * #84): the standalone impl routes over ILP to its single configured
694
- * publish destination, so more than one relay is refused rather than
695
- * silently half-published.
696
- */
697
- publishEvent(event: UnsignedEvent, relayUrls: string[]): Promise<PublishReceipt>;
698
- private requireChannel;
699
- /**
700
- * The client + channel a STORE write must be paid on.
701
- *
702
- * With no `storeClientConfig` this is the publish leg unchanged — the
703
- * single-node case, where the store terminates on the connector that
704
- * already holds the channel. With one, the store node is a different
705
- * connector holding a different channel, and paying it from the publish
706
- * channel is precisely the `F01 - claim rejected: names a channel this
707
- * connector has no record of` failure this exists to prevent.
708
- *
709
- * Built on FIRST STORE WRITE, not in `start()`: `rig push` with nothing to
710
- * upload, and every read-only command, should not open a second on-chain
711
- * channel (gas) for a leg they never use. Memoised on the promise so
712
- * concurrent uploads share one open rather than racing two.
713
- */
714
- private storeLeg;
715
- }
716
-
717
520
  /**
718
521
  * Nonce-ownership guard for the STANDALONE embedded Publisher (#228).
719
522
  *
@@ -825,4 +628,4 @@ declare class NonceLock {
825
628
  release(): void;
826
629
  }
827
630
 
828
- export { type AcquireLockOptions, type ChannelCloseOutcome, ChannelMapCorruptError, type ChannelMapKey, type ChannelMapRecord, ChannelMapStore, type ChannelMapStoreOptions, type ChannelOpenOutcome, type ChannelSettleOutcome, type CheckDaemonOptions, DEFAULT_DAEMON_PORT, DaemonIdentityConflictError, NonceLock, type PersistedChannelContext, RIG_CHANNEL_MAP_FILENAME, type SignedNostrEvent, StandaloneLockError, type StandaloneMoneyOps, StandalonePublishError, StandalonePublisher, type StandalonePublisherOptions, type ToonClientLike, type WalletChainBalanceInfo, type WalletTokenAmountInfo, type WatermarkEntry, channelStatus, checkDaemonIdentity, defaultDaemonPort, defaultLockDir, deriveRouteDestinations, recordKey, resolveChannelPaths };
631
+ export { type AcquireLockOptions, type ChannelCloseOutcome, ChannelMapCorruptError, type ChannelMapKey, type ChannelMapRecord, ChannelMapStore, type ChannelMapStoreOptions, type ChannelOpenOutcome, type ChannelSettleOutcome, type CheckDaemonOptions, ConnectorPublishError, ConnectorPublisher, type ConnectorPublisherOptions, DEFAULT_DAEMON_PORT, DaemonIdentityConflictError, MAX_ACCOUNT_INDEX, NonceLock, type NostrKey, type PaidClientLike, type PaidFulfilled, type PaidLeg, type PaidRefused, type PaidRequest, type PaidSendOptions, type PaidSendResult, type PersistedChannelContext, RIG_CHANNEL_MAP_FILENAME, type RouteTerms, StandaloneLockError, type StandaloneMoneyOps, type WalletChainBalanceInfo, type WalletTokenAmountInfo, type WatermarkEntry, channelStatus, checkDaemonIdentity, counterpartyMatch, defaultDaemonPort, defaultLockDir, deriveNostrKeyFromMnemonic, extractArweaveTxId, nostrDerivationPath, recordKey, resolveChannelPaths, sameSettlementAddress };