@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.
@@ -0,0 +1 @@
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 * Channel REUSE (#262): with a `channelMap` configured, start() resumes the\n * channel recorded for (identity, channel anchor) — `trackChannel` rehydrates\n * the cumulative-claim watermark from the client's channels.json — and\n * records any fresh lazy open, so sequential CLI invocations share ONE\n * on-chain channel instead of stranding a deposit per run (./channel-map.ts).\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 {\n recordKey,\n type ChannelMapRecord,\n type ChannelMapStore,\n type PersistedChannelContext,\n} from './channel-map.js';\nimport type {\n ChannelCloseOutcome,\n ChannelOpenOutcome,\n ChannelSettleOutcome,\n WalletBalanceInfo,\n} from './money.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 /** On-chain deposit total for a tracked channel (channel-map bookkeeping). */\n getChannelDepositTotal?(channelId: string): bigint;\n /** Re-read a RESUMED channel's on-chain deposit (persisted state omits it). */\n rehydrateChannelDeposit?(\n channelId: string,\n opts: { chain: string; tokenNetworkAddress: string }\n ): Promise<bigint | undefined>;\n // ── money lifecycle (#263) — optional, matches ToonClient's surface ──────\n /** Deposit extra collateral (base-unit delta) into an open channel. */\n depositToChannel?(\n channelId: string,\n amount: string | bigint\n ): Promise<{ channelId: string; txHash?: string; depositTotal: string }>;\n /** Close a channel — starts the settlement challenge window (on-chain). */\n closeChannel?(channelId: string): Promise<{\n channelId: string;\n txHash?: string;\n /** Unix SECONDS, string-encoded. */\n closedAt: string;\n settleableAt: string;\n }>;\n /** Settle a closed channel after its window — releases funds (on-chain). */\n settleChannel?(\n channelId: string\n ): Promise<{ channelId: string; txHash?: string }>;\n /** Free on-chain wallet-balance read (works on an UNSTARTED client). */\n getBalances?(): Promise<WalletBalanceInfo[]>;\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 * Peer→channel map store (#262): start() resumes the channel recorded for\n * (identity, channel anchor) instead of opening a fresh one, and records\n * any fresh lazy open for the next invocation. Absent (embedded callers\n * managing their own channel lifecycle): the historical behaviour — open\n * lazily every run, record nothing.\n */\n channelMap?: ChannelMapStore;\n /**\n * Per-chain settlement parameters to BACK-FILL into the client's peer\n * negotiations after start (#264/#260): peers whose kind:10032 announce\n * carries no `tokenNetworks`/`preferredTokens` negotiate with those fields\n * empty, and the on-chain channel open then fails (\"tokenNetwork address\n * is required\"). Values here never override what a peer DID announce —\n * they only fill gaps, keyed by the negotiated chain id.\n */\n negotiationFallbacks?: {\n tokenNetworks?: Record<string, string>;\n preferredTokens?: Record<string, string>;\n };\n /** Sink for non-fatal channel-persistence warnings (default: stderr). */\n warn?: (line: string) => void;\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// Channel-resume introspection (#262)\n//\n// The peer negotiation table and the ChannelManager's peer→channel map are\n// PRIVATE on ToonClient, so resuming a channel reaches into them through a\n// structural runtime cast — the exact pattern the daemon uses\n// (client-mcp/src/daemon/client-runner.ts, openOrResumeApexChannel /\n// routeChildPeersThroughApexChannel). Keep the shapes in sync with\n// @toon-protocol/client's ToonClient/ChannelManager.\n// ---------------------------------------------------------------------------\n\n/** The slice of a `PeerNegotiation` the channel map records. */\ninterface NegotiationLike {\n chain: string;\n chainType: string;\n chainId: number | string;\n settlementAddress: string;\n tokenAddress?: string;\n tokenNetwork?: string;\n}\n\ninterface ChannelInternals {\n peerNegotiations?: Map<string, NegotiationLike>;\n channelManager?: {\n trackChannel?: (\n channelId: string,\n context: PersistedChannelContext\n ) => void;\n peerChannels?: Map<string, string>;\n };\n /**\n * The client's on-chain channel client caches per-channel chain context at\n * OPEN time only; close/settle/deposit on a channel resumed from the map\n * would throw \"No on-chain context\" without re-seeding it (#263). Same\n * structural-cast contract as the rest of this block — keep in sync with\n * `@toon-protocol/client`'s OnChainChannelClient.\n */\n onChainChannelClient?: {\n channelContext?: Map<\n string,\n { chain: string; tokenNetworkAddress: string; tokenAddress?: string }\n >;\n };\n}\n\n/** Best-effort access to the client's private negotiation/channel state. */\nfunction channelInternals(client: ToonClientLike): ChannelInternals {\n const c = client as unknown as ChannelInternals;\n const negotiations =\n c.peerNegotiations instanceof Map ? c.peerNegotiations : undefined;\n const cm =\n c.channelManager &&\n typeof c.channelManager.trackChannel === 'function' &&\n c.channelManager.peerChannels instanceof Map\n ? c.channelManager\n : undefined;\n const onChain =\n c.onChainChannelClient &&\n c.onChainChannelClient.channelContext instanceof Map\n ? c.onChainChannelClient\n : undefined;\n return {\n ...(negotiations ? { peerNegotiations: negotiations } : {}),\n ...(cm ? { channelManager: cm } : {}),\n ...(onChain ? { onChainChannelClient: onChain } : {}),\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 private readonly channelMap: ChannelMapStore | undefined;\n /** ILP anchor the channel is keyed by in the map (peer/apex destination). */\n private readonly channelAnchor: string | undefined;\n private readonly negotiationFallbacks:\n | StandalonePublisherOptions['negotiationFallbacks']\n | undefined;\n private readonly warn: (line: string) => void;\n\n private lock: NonceLock | undefined;\n private channelId: string | undefined;\n private readyPromise: Promise<void> | undefined;\n /** Guard + client start only (no channel) — see {@link startClientOnly}. */\n private clientReadyPromise: Promise<void> | undefined;\n /** True when the last start() RESUMED the recorded channel (#263). */\n private lastOpenResumed = false;\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 this.channelMap = options.channelMap;\n this.channelAnchor = anchor;\n this.negotiationFallbacks = options.negotiationFallbacks;\n this.warn =\n options.warn ?? ((line) => process.stderr.write(`${line}\\n`));\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 ??= (async () => {\n await this.startClientOnly();\n try {\n this.channelId = await this.openOrResumeChannel();\n } catch (err) {\n // Release the identity lock on a failed channel open (pre-#263\n // behaviour): nothing holds claims yet, so another process may go.\n this.lock?.release();\n this.lock = undefined;\n this.clientReadyPromise = undefined;\n throw err;\n }\n })().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 /**\n * Run the nonce guard and start the embedded client WITHOUT touching the\n * payment channel (#263): `channel close`/`settle` operate on a RECORDED\n * channel and must never open a fresh one as a side effect of starting.\n * Idempotent; `start()` layers the channel open/resume on top of this.\n */\n startClientOnly(): Promise<void> {\n this.clientReadyPromise ??= this.doStartClient().catch((err: unknown) => {\n this.clientReadyPromise = undefined;\n throw err;\n });\n return this.clientReadyPromise;\n }\n\n private async doStartClient(): 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 // #264: back-fill negotiation gaps as soon as the client's bootstrap\n // negotiation exists — before any channel open (start()) or recorded-\n // channel operation reads them.\n this.applyNegotiationFallbacks();\n } catch (err) {\n this.lock.release();\n this.lock = undefined;\n throw err;\n }\n }\n\n /**\n * Back-fill negotiated peer metadata the announce did not carry\n * (#264/#260 root cause 3): after the client's bootstrap negotiation, any\n * peer negotiation missing `tokenNetwork`/`tokenAddress` gets the derived\n * per-chain fallback for its negotiated chain, BEFORE the lazy channel\n * open reads them. Announced values are never overridden.\n */\n private applyNegotiationFallbacks(): void {\n const fallbacks = this.negotiationFallbacks;\n if (!fallbacks) return;\n const negotiations = channelInternals(this.client).peerNegotiations;\n if (!negotiations) {\n this.warn(\n 'rig: settlement fallbacks configured but the client does not ' +\n 'expose negotiation internals — on-chain channel opening may fail ' +\n 'if the peer announced no TokenNetwork'\n );\n return;\n }\n for (const negotiation of negotiations.values()) {\n if (!negotiation.tokenNetwork) {\n const tokenNetwork = fallbacks.tokenNetworks?.[negotiation.chain];\n if (tokenNetwork) negotiation.tokenNetwork = tokenNetwork;\n }\n if (!negotiation.tokenAddress) {\n const tokenAddress = fallbacks.preferredTokens?.[negotiation.chain];\n if (tokenAddress) negotiation.tokenAddress = tokenAddress;\n }\n }\n }\n\n /**\n * Open the payment channel — or, when the channel map has a record for\n * (identity, anchor), RESUME the recorded on-chain channel (#262).\n *\n * Resume seeds the client's ChannelManager (`trackChannel` rehydrates the\n * cumulative-claim watermark from channels.json; `peerChannels` makes the\n * subsequent `openChannel` return the same id instead of opening on-chain).\n * A fresh open is RECORDED so the next invocation resumes it. A corrupt map\n * file throws BEFORE anything is opened — never a silent duplicate open.\n */\n private async openOrResumeChannel(): Promise<string> {\n const map = this.channelMap;\n if (!map) {\n // No persistence configured: historical lazy open, nothing recorded.\n return this.client.openChannel(this.channelDestination);\n }\n if (!this.channelAnchor) {\n this.warn(\n 'rig: no channel anchor destination configured — the peer→channel ' +\n 'mapping cannot be persisted, so this run may open a fresh channel'\n );\n return this.client.openChannel(this.channelDestination);\n }\n\n const anchor = this.channelAnchor;\n const identity = this.client.getPublicKey();\n // Corruption check happens HERE, before any on-chain open (throws).\n const candidates = map.listFor(identity, anchor);\n const internals = channelInternals(this.client);\n const resumed = await this.resumeRecordedChannel(map, candidates, internals);\n\n // Idempotent — returns the (resumed or existing) channel for the peer if\n // one is tracked, else opens lazily on-chain.\n const channelId = await this.client.openChannel(this.channelDestination);\n\n if (resumed && channelId === resumed.channelId) {\n this.lastOpenResumed = true;\n map.touch(recordKey(resumed));\n } else {\n this.lastOpenResumed = false;\n this.recordOpenedChannel(map, internals, identity, anchor, channelId);\n }\n return channelId;\n }\n\n /**\n * Try to resume one recorded channel: the first candidate whose peer is\n * still negotiated on the SAME chain + tokenNetwork and whose watermark\n * does not show it closed/settled. Returns the resumed record, if any.\n */\n private async resumeRecordedChannel(\n map: ChannelMapStore,\n candidates: ChannelMapRecord[],\n internals: ChannelInternals\n ): Promise<ChannelMapRecord | undefined> {\n if (candidates.length === 0) return undefined;\n const cm = internals.channelManager;\n if (!cm?.trackChannel || !cm.peerChannels) {\n this.warn(\n 'rig: a recorded channel exists but the client does not expose ' +\n 'channel internals to resume it — a fresh channel may be opened'\n );\n return undefined;\n }\n\n for (const record of candidates) {\n // The peer must still be negotiated on the recorded chain/tokenNetwork;\n // a rotated peer identity or re-negotiated settlement gets a fresh\n // channel (recorded under its own key) instead of stale claims.\n const negotiation = internals.peerNegotiations?.get(record.peerId);\n if (\n !negotiation ||\n negotiation.chain !== record.chain ||\n (negotiation.tokenNetwork ?? '') !== record.tokenNetwork\n ) {\n continue;\n }\n\n // Never resume a channel the withdraw flow already closed/settled.\n const watermark = map.readWatermark(record.channelId);\n if (\n watermark?.closedAt !== undefined ||\n watermark?.settledAt !== undefined\n ) {\n continue;\n }\n if (!watermark) {\n // Fresh channels are seeded at record time, so a missing entry means\n // the watermark store was lost. Resuming from nonce 0 fails SAFE: a\n // regressed cumulative claim is rejected by the peer (no double\n // spend), but warn so the failure is diagnosable.\n this.warn(\n `rig: resuming channel ${record.channelId} with no local claim ` +\n `watermark (${map.watermarkPath}) — if this channel was claimed ` +\n 'against before, the peer will reject the stale claims; remove ' +\n `its entry from ${map.mapPath} to open a fresh channel instead`\n );\n }\n\n // trackChannel rehydrates nonce/cumulative from the watermark store;\n // seeding peerChannels makes ensureChannel/openChannel reuse the id.\n cm.trackChannel(record.channelId, record.context);\n cm.peerChannels.set(record.peerId, record.channelId);\n\n // Persisted channel state omits the on-chain deposit — re-read it so\n // fee/balance accounting is right (EVM only; mirrors the daemon).\n if (\n record.context.chainType === 'evm' &&\n this.client.rehydrateChannelDeposit\n ) {\n try {\n const deposit = await this.client.rehydrateChannelDeposit(\n record.channelId,\n {\n chain: record.chain,\n tokenNetworkAddress: record.context.tokenNetworkAddress,\n }\n );\n if (deposit !== undefined) {\n map.touch(recordKey(record), {\n depositTotal: deposit.toString(),\n });\n }\n } catch (err) {\n this.warn(\n `rig: deposit re-read for resumed channel ${record.channelId} ` +\n `failed: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n return record;\n }\n return undefined;\n }\n\n /** Record a freshly opened channel (+ seed its claim watermark at 0/0). */\n private recordOpenedChannel(\n map: ChannelMapStore,\n internals: ChannelInternals,\n identity: string,\n destination: string,\n channelId: string\n ): void {\n let peerId: string | undefined;\n for (const [peer, channel] of internals.channelManager?.peerChannels ??\n []) {\n if (channel === channelId) {\n peerId = peer;\n break;\n }\n }\n const negotiation =\n peerId !== undefined\n ? internals.peerNegotiations?.get(peerId)\n : undefined;\n if (peerId === undefined || !negotiation) {\n this.warn(\n `rig: opened channel ${channelId} but could not record the ` +\n 'peer→channel mapping (client does not expose negotiation ' +\n 'internals) — the NEXT invocation may open another channel'\n );\n return;\n }\n\n let depositTotal: bigint | undefined;\n try {\n depositTotal = this.client.getChannelDepositTotal?.(channelId);\n } catch {\n // deposit unknown — recorded without it; a later resume re-reads it\n }\n\n map.record({\n channelId,\n peerId,\n identity,\n destination,\n chain: negotiation.chain,\n tokenNetwork: negotiation.tokenNetwork ?? '',\n // Context mirrors the daemon's persistApexChannel shape — exactly what\n // trackChannel needs on resume.\n context: {\n chainType: negotiation.chainType,\n chainId:\n typeof negotiation.chainId === 'number' ? negotiation.chainId : 0,\n tokenNetworkAddress: negotiation.tokenNetwork ?? '',\n ...(negotiation.tokenAddress\n ? { tokenAddress: negotiation.tokenAddress }\n : {}),\n recipient: negotiation.settlementAddress,\n },\n ...(depositTotal !== undefined && depositTotal > 0n\n ? { depositTotal: depositTotal.toString() }\n : {}),\n });\n // Seed nonce 0 / cumulative 0 so a later resume can tell \"never claimed\n // against\" apart from \"watermark lost\" (which only fails claim-rejected).\n map.seedWatermark(channelId);\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.clientReadyPromise = undefined;\n this.channelId = undefined;\n // Never stop a client that was never started (`ToonClient.stop()` throws\n // INVALID_STATE) — e.g. after a free `rig balance` read (#263).\n if (this.ownsClient && this.client.isStarted?.() !== false) {\n await this.client.stop();\n }\n }\n\n // ── Money lifecycle (#263) ──────────────────────────────────────────────────\n\n /**\n * Explicit `rig channel open`: the SAME resume-or-open path the lazy paid\n * writes use (guard → start → resume the recorded channel or open + record\n * a fresh one), surfaced with a receipt — plus an optional extra collateral\n * deposit on top of the open/resume.\n */\n async openChannelExplicit(opts?: {\n /** Extra collateral to deposit AFTER the open/resume (base units). */\n deposit?: bigint;\n }): Promise<ChannelOpenOutcome> {\n await this.start();\n const channelId = this.requireChannel();\n const identity = this.client.getPublicKey();\n const destination = this.channelAnchor ?? '';\n const record =\n this.channelMap && this.channelAnchor\n ? this.channelMap\n .listFor(identity, this.channelAnchor)\n .find((r) => r.channelId === channelId)\n : undefined;\n\n const outcome: ChannelOpenOutcome = {\n channelId,\n resumed: this.lastOpenResumed,\n destination,\n ...(record ? { chain: record.chain, peerId: record.peerId } : {}),\n ...(record?.depositTotal !== undefined\n ? { depositTotal: record.depositTotal }\n : {}),\n };\n\n if (opts?.deposit !== undefined && opts.deposit > 0n) {\n if (!this.client.depositToChannel) {\n throw new StandalonePublishError(\n 'this client build does not support channel deposits ' +\n '(depositToChannel is unavailable)'\n );\n }\n const deposited = await this.client.depositToChannel(\n channelId,\n opts.deposit\n );\n outcome.depositAdded = opts.deposit.toString();\n outcome.depositTotal = deposited.depositTotal;\n if (deposited.txHash) outcome.depositTxHash = deposited.txHash;\n if (record) {\n this.channelMap?.touch(recordKey(record), {\n depositTotal: deposited.depositTotal,\n });\n }\n }\n return outcome;\n }\n\n /**\n * Adopt a RECORDED channel into the running client so on-chain close/\n * settle/deposit can act on it: `trackChannel` rehydrates the claim\n * watermark + withdraw timers from channels.json, `peerChannels` binds the\n * peer, and the on-chain client's context cache is re-seeded (it only\n * learns context at open time — a resumed channel would otherwise throw\n * \"No on-chain context\").\n */\n private adoptRecordedChannel(record: ChannelMapRecord): void {\n const internals = channelInternals(this.client);\n const cm = internals.channelManager;\n if (cm?.trackChannel && cm.peerChannels) {\n cm.trackChannel(record.channelId, record.context);\n cm.peerChannels.set(record.peerId, record.channelId);\n }\n const contextCache = internals.onChainChannelClient?.channelContext;\n if (contextCache && !contextCache.has(record.channelId)) {\n contextCache.set(record.channelId, {\n chain: record.chain,\n tokenNetworkAddress: record.context.tokenNetworkAddress,\n ...(record.context.tokenAddress\n ? { tokenAddress: record.context.tokenAddress }\n : {}),\n });\n }\n }\n\n /**\n * Close a recorded channel: starts the on-chain settlement challenge\n * window. The client persists `closedAt`/`settleableAt` into the claim\n * watermark store, which is exactly where `rig channel list`/`balance`\n * derive the closing/settleable/settled status from. Guard + client start,\n * but NEVER a channel open ({@link startClientOnly}).\n */\n async closeRecordedChannel(\n record: ChannelMapRecord\n ): Promise<ChannelCloseOutcome> {\n await this.startClientOnly();\n if (!this.client.closeChannel) {\n throw new StandalonePublishError(\n 'this client build does not support closing channels ' +\n '(closeChannel is unavailable)'\n );\n }\n this.adoptRecordedChannel(record);\n const result = await this.client.closeChannel(record.channelId);\n this.channelMap?.touch(recordKey(record));\n return result;\n }\n\n /**\n * Settle a recorded channel after its challenge window — releases the\n * remaining collateral. The client enforces the `now >= settleableAt` time\n * guard BEFORE spending gas (a too-early call throws its retryable\n * SettleTooEarlyError) and persists `settledAt` into the watermark store.\n */\n async settleRecordedChannel(\n record: ChannelMapRecord\n ): Promise<ChannelSettleOutcome> {\n await this.startClientOnly();\n if (!this.client.settleChannel) {\n throw new StandalonePublishError(\n 'this client build does not support settling channels ' +\n '(settleChannel is unavailable)'\n );\n }\n this.adoptRecordedChannel(record);\n const result = await this.client.settleChannel(record.channelId);\n this.channelMap?.touch(recordKey(record));\n return result;\n }\n\n /**\n * On-chain wallet balances for the embedded identity — a FREE read on the\n * UNSTARTED client (no nonce guard, no uplink, no channel): the client\n * reads the settlement chain its channels actually use (its EVM key is\n * derived at construction; Solana/Mina keys only exist after a start, so\n * those chains appear once a start-requiring command ran — same\n * best-effort contract as the client's own getBalances).\n */\n async readWalletBalances(): Promise<WalletBalanceInfo[]> {\n if (!this.client.getBalances) return [];\n return await this.client.getBalances();\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":[]}
@@ -102,13 +102,7 @@ function resolveMnemonicSource(options, file, configPath) {
102
102
  throw new MissingIdentityError(configPath);
103
103
  }
104
104
  async function loadClientKeys() {
105
- try {
106
- return await import("@toon-protocol/client");
107
- } catch (err) {
108
- throw new Error(
109
- `identity derivation needs the optional peer dependency @toon-protocol/client \u2014 install it (\`npm i @toon-protocol/client\`) and re-run (${err instanceof Error ? err.message : String(err)})`
110
- );
111
- }
105
+ return await import("@toon-protocol/client");
112
106
  }
113
107
  async function resolveIdentity(options) {
114
108
  const configPath = clientConfigPath(options.env);
@@ -140,4 +134,4 @@ export {
140
134
  MissingIdentityError,
141
135
  resolveIdentity
142
136
  };
143
- //# sourceMappingURL=chunk-QD437XAW.js.map
137
+ //# sourceMappingURL=chunk-CW4HJNMU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli/identity.ts"],"sourcesContent":["/**\n * Identity resolution for the standalone-only `rig` CLI (#248).\n *\n * One precedence chain, used by every command that signs or pays:\n *\n * 1. `RIG_MNEMONIC` environment variable\n * 2. `TOON_CLIENT_MNEMONIC` environment variable — DEPRECATED alias of the\n * env tier (a one-line stderr warning fires when it is the source)\n * 3. project-local `.env` — walked up from the working directory (through\n * the repo root to the filesystem root); ONLY the `RIG_MNEMONIC` line is\n * parsed out of it (never loaded into the process env, never required\n * to exist)\n * 4. the shared `~/.toon-client` state dir (`TOON_CLIENT_HOME` override):\n * encrypted keystore (`keystorePath` + `TOON_CLIENT_KEYSTORE_PASSWORD`,\n * auto-password fallback for daemon-provisioned keystores), then the\n * plain `mnemonic` config field — the pre-#248 standalone-context logic\n *\n * The BIP-44 `mnemonicAccountIndex` from the shared config file applies to\n * every source (same convention as the daemon), so the derived pubkey never\n * depends on WHERE the phrase came from.\n *\n * The phrase itself is never written anywhere by rig (not to git config, not\n * to any repo file) and never printed — callers report only the SOURCE and\n * the derived pubkey.\n *\n * Key derivation and keystore decryption live in the `@toon-protocol/client`\n * dependency, loaded via dynamic import so this module can be statically\n * imported by every command without dragging the client into runs that fail\n * earlier (usage errors, missing git repo, …).\n */\n\nimport { existsSync, readFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { dirname, join } from 'node:path';\n\n/** Where the mnemonic came from (reported in output; the phrase never is). */\nexport type IdentitySourceKind =\n /** `RIG_MNEMONIC` environment variable. */\n | 'env'\n /** `TOON_CLIENT_MNEMONIC` environment variable (deprecated alias). */\n | 'env-alias'\n /** `RIG_MNEMONIC` parsed out of a project-local `.env` file. */\n | 'dotenv'\n /** Encrypted keystore referenced by the shared client config. */\n | 'keystore'\n /** Plain `mnemonic` field of the shared client config file. */\n | 'config';\n\n/** A resolved signing identity: source + derived pubkey (never the phrase). */\nexport interface ResolvedIdentity {\n /** The BIP-39 phrase. Handle with care; never log or persist it. */\n mnemonic: string;\n /** BIP-44 account index used for derivation (shared config, default 0). */\n accountIndex: number;\n source: IdentitySourceKind;\n /** Human-facing source label, e.g. `RIG_MNEMONIC env` or a file path. */\n sourceLabel: string;\n /** Derived Nostr pubkey (hex) — the repo owner / `toon.owner` value. */\n pubkey: string;\n}\n\n/** No mnemonic could be resolved anywhere on the chain. */\nexport class MissingIdentityError extends Error {\n constructor(configPath: string) {\n super(\n 'no identity found — rig needs a BIP-39 seed phrase to sign and pay. ' +\n 'Provide one of (highest precedence first):\\n' +\n ' • RIG_MNEMONIC environment variable\\n' +\n ' • RIG_MNEMONIC=<phrase> in a project-local .env file (gitignore it!)\\n' +\n ` • the shared client config at ${configPath} (mnemonic or ` +\n 'keystorePath + TOON_CLIENT_KEYSTORE_PASSWORD)'\n );\n this.name = 'MissingIdentityError';\n }\n}\n\n// ---------------------------------------------------------------------------\n// Shared client config conventions (duplicated from client-mcp — the same\n// note as standalone/nonce-guard.ts: no @toon-protocol/client-mcp import)\n// ---------------------------------------------------------------------------\n\n/** Duplicated daemon convention: auto-keystore password. */\nconst DEFAULT_KEYSTORE_PASSWORD = 'toon-client-default';\n\n/** Shared client state dir: `TOON_CLIENT_HOME`, else `~/.toon-client`. */\nexport function clientConfigPath(env: NodeJS.ProcessEnv): string {\n const dir = env['TOON_CLIENT_HOME'] ?? join(homedir(), '.toon-client');\n return join(dir, 'config.json');\n}\n\n/** The subset of the shared client config file identity resolution reads. */\ninterface ClientConfigIdentityFields {\n mnemonic?: unknown;\n mnemonicAccountIndex?: unknown;\n keystorePath?: unknown;\n keystoreAutoPassword?: unknown;\n}\n\nfunction readClientConfigFile(path: string): ClientConfigIdentityFields {\n try {\n return JSON.parse(readFileSync(path, 'utf8')) as ClientConfigIdentityFields;\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// .env parsing (RIG_MNEMONIC only — never arbitrary env loading)\n// ---------------------------------------------------------------------------\n\nconst DOTENV_LINE_RE = /^\\s*(?:export\\s+)?RIG_MNEMONIC\\s*=\\s*(.*)\\s*$/;\n\n/**\n * Extract ONLY the `RIG_MNEMONIC` value from `.env` file content. Supports\n * `export ` prefixes, single/double quotes, full-line `#` comments, and\n * inline ` #` comments on unquoted values. Multiple assignments: the last\n * one wins (shell-sourcing semantics). An empty value counts as unset.\n * Nothing is ever loaded into `process.env`.\n */\nexport function parseDotenvMnemonic(content: string): string | undefined {\n let found: string | undefined;\n for (const line of content.split(/\\r?\\n/)) {\n const match = DOTENV_LINE_RE.exec(line);\n if (!match) continue;\n let value = (match[1] ?? '').trim();\n const quote = value[0];\n if ((quote === '\"' || quote === \"'\") && value.length >= 2 && value.endsWith(quote)) {\n value = value.slice(1, -1);\n } else {\n // Unquoted: an inline comment starts at the first whitespace-preceded #.\n const hash = value.search(/\\s#/);\n if (hash !== -1) value = value.slice(0, hash);\n value = value.trim();\n }\n if (value !== '') found = value;\n }\n return found;\n}\n\n/**\n * Walk up from `startDir` (the working directory — the walk passes through\n * the repo root) to the filesystem root, returning the first `.env` file\n * that defines `RIG_MNEMONIC`. `stopDir` bounds the walk (tests).\n */\nexport function findDotenvMnemonic(\n startDir: string,\n stopDir?: string\n): { path: string; mnemonic: string } | undefined {\n let dir = startDir;\n for (;;) {\n const candidate = join(dir, '.env');\n if (existsSync(candidate)) {\n try {\n const mnemonic = parseDotenvMnemonic(readFileSync(candidate, 'utf8'));\n if (mnemonic !== undefined) return { path: candidate, mnemonic };\n } catch {\n // Unreadable .env → keep walking; the file is never required.\n }\n }\n if (stopDir !== undefined && dir === stopDir) return undefined;\n const parent = dirname(dir);\n if (parent === dir) return undefined;\n dir = parent;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Resolution\n// ---------------------------------------------------------------------------\n\nexport interface ResolveIdentityOptions {\n env: NodeJS.ProcessEnv;\n /** Working directory the `.env` walk starts from. */\n cwd: string;\n /** Stderr line sink for the deprecated-alias warning. */\n warn(line: string): void;\n /** Bound the `.env` walk-up (tests only). */\n dotenvStopDir?: string;\n}\n\n/** The resolved source before any derivation (dependency-free). */\ninterface MnemonicSource {\n mnemonic: string;\n source: IdentitySourceKind;\n sourceLabel: string;\n}\n\nfunction resolveMnemonicSource(\n options: ResolveIdentityOptions,\n file: ClientConfigIdentityFields,\n configPath: string\n): MnemonicSource | { keystorePath: string } {\n const { env } = options;\n\n const rigEnv = env['RIG_MNEMONIC']?.trim();\n if (rigEnv) {\n return { mnemonic: rigEnv, source: 'env', sourceLabel: 'RIG_MNEMONIC env' };\n }\n\n const aliasEnv = env['TOON_CLIENT_MNEMONIC']?.trim();\n if (aliasEnv) {\n options.warn(\n 'rig: TOON_CLIENT_MNEMONIC is deprecated — rename the variable to RIG_MNEMONIC'\n );\n return {\n mnemonic: aliasEnv,\n source: 'env-alias',\n sourceLabel: 'TOON_CLIENT_MNEMONIC env (deprecated alias)',\n };\n }\n\n const dotenv = findDotenvMnemonic(options.cwd, options.dotenvStopDir);\n if (dotenv) {\n return {\n mnemonic: dotenv.mnemonic,\n source: 'dotenv',\n sourceLabel: dotenv.path,\n };\n }\n\n if (typeof file.keystorePath === 'string' && file.keystorePath !== '') {\n return { keystorePath: file.keystorePath };\n }\n if (typeof file.mnemonic === 'string' && file.mnemonic.trim() !== '') {\n return {\n mnemonic: file.mnemonic.trim(),\n source: 'config',\n sourceLabel: configPath,\n };\n }\n throw new MissingIdentityError(configPath);\n}\n\n/**\n * Load the client key helpers. `@toon-protocol/client` is a regular runtime\n * dependency (#259); the import stays dynamic purely so commands that fail\n * earlier never pay its startup cost.\n */\nasync function loadClientKeys(): Promise<{\n loadKeystore(path: string, password: string): string;\n deriveNostrKeyFromMnemonic(\n mnemonic: string,\n accountIndex?: number\n ): { secretKey: Uint8Array; pubkey: string };\n}> {\n return await import('@toon-protocol/client');\n}\n\n/**\n * Resolve the CLI signing identity along the precedence chain and derive its\n * Nostr pubkey. Throws {@link MissingIdentityError} (with the full\n * three-option remediation) when nothing on the chain yields a phrase.\n */\nexport async function resolveIdentity(\n options: ResolveIdentityOptions\n): Promise<ResolvedIdentity> {\n const configPath = clientConfigPath(options.env);\n const file = readClientConfigFile(configPath);\n const picked = resolveMnemonicSource(options, file, configPath);\n\n const keys = await loadClientKeys();\n let source: MnemonicSource;\n if ('keystorePath' in picked) {\n const password =\n options.env['TOON_CLIENT_KEYSTORE_PASSWORD'] ??\n (file.keystoreAutoPassword === true ? DEFAULT_KEYSTORE_PASSWORD : undefined);\n if (!password) {\n throw new Error(\n `keystorePath is set in ${configPath} but TOON_CLIENT_KEYSTORE_PASSWORD is not provided`\n );\n }\n source = {\n mnemonic: keys.loadKeystore(picked.keystorePath, password),\n source: 'keystore',\n sourceLabel: picked.keystorePath,\n };\n } else {\n source = picked;\n }\n\n const accountIndex =\n typeof file.mnemonicAccountIndex === 'number' ? file.mnemonicAccountIndex : 0;\n const { pubkey } = keys.deriveNostrKeyFromMnemonic(source.mnemonic, accountIndex);\n return { ...source, accountIndex, pubkey };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,217 @@
1
+ // src/standalone/channel-map.ts
2
+ import { mkdirSync, readFileSync, writeFileSync } from "fs";
3
+ import { homedir } from "os";
4
+ import { dirname, join } from "path";
5
+ var RIG_CHANNEL_MAP_FILENAME = "rig-channels.json";
6
+ function resolveChannelPaths(env) {
7
+ const dir = env["TOON_CLIENT_HOME"] ?? join(homedir(), ".toon-client");
8
+ let configured;
9
+ try {
10
+ const raw = readFileSync(join(dir, "config.json"), "utf8");
11
+ const parsed = JSON.parse(raw);
12
+ if (typeof parsed.channelStorePath === "string") {
13
+ configured = parsed.channelStorePath;
14
+ }
15
+ } catch (err) {
16
+ if (err.code !== "ENOENT") {
17
+ throw new Error(
18
+ `failed to read client config at ${join(dir, "config.json")}: ${err instanceof Error ? err.message : String(err)}`
19
+ );
20
+ }
21
+ }
22
+ return {
23
+ mapPath: join(dir, RIG_CHANNEL_MAP_FILENAME),
24
+ watermarkPath: configured ?? join(dir, "channels.json")
25
+ };
26
+ }
27
+ var ChannelMapCorruptError = class extends Error {
28
+ constructor(path, detail) {
29
+ super(
30
+ `channel state file ${path} is corrupt (${detail}) \u2014 refusing to continue: proceeding would silently open (and fund) a duplicate on-chain channel. Fix or remove the file; removing it makes rig forget which channels it holds (existing deposits stay locked on-chain until settled).`
31
+ );
32
+ this.path = path;
33
+ this.name = "ChannelMapCorruptError";
34
+ }
35
+ path;
36
+ };
37
+ function keyOf(key) {
38
+ return `${key.identity}|${key.destination}|${key.chain}|${key.tokenNetwork}`;
39
+ }
40
+ function recordKey(record) {
41
+ return {
42
+ identity: record.identity,
43
+ destination: record.destination,
44
+ chain: record.chain,
45
+ tokenNetwork: record.tokenNetwork
46
+ };
47
+ }
48
+ function isContext(v) {
49
+ if (typeof v !== "object" || v === null) return false;
50
+ const c = v;
51
+ return typeof c["chainType"] === "string" && typeof c["chainId"] === "number" && typeof c["tokenNetworkAddress"] === "string";
52
+ }
53
+ function isRecord(v) {
54
+ if (typeof v !== "object" || v === null) return false;
55
+ const r = v;
56
+ return typeof r["channelId"] === "string" && typeof r["peerId"] === "string" && typeof r["identity"] === "string" && typeof r["destination"] === "string" && typeof r["chain"] === "string" && typeof r["tokenNetwork"] === "string" && isContext(r["context"]);
57
+ }
58
+ var ChannelMapStore = class {
59
+ mapPath;
60
+ watermarkPath;
61
+ constructor(options) {
62
+ this.mapPath = options.mapPath;
63
+ this.watermarkPath = options.watermarkPath;
64
+ }
65
+ /** All recorded channels. @throws {ChannelMapCorruptError} */
66
+ list() {
67
+ return Object.values(this.readMap().channels);
68
+ }
69
+ /**
70
+ * Recorded channels for one (identity, destination) pair — the resume
71
+ * candidates for a paid command. @throws {ChannelMapCorruptError}
72
+ */
73
+ listFor(identity, destination) {
74
+ return this.list().filter(
75
+ (r) => r.identity === identity && r.destination === destination
76
+ );
77
+ }
78
+ /**
79
+ * Record a (freshly opened) channel. Overwrites any previous record under
80
+ * the same (identity, destination, chain, tokenNetwork) key — the old
81
+ * channel is closed/stale by then; its claim watermark stays in the
82
+ * watermark store.
83
+ */
84
+ record(record) {
85
+ const now = (/* @__PURE__ */ new Date()).toISOString();
86
+ const full = {
87
+ ...record,
88
+ openedAt: record.openedAt ?? now,
89
+ lastUsedAt: record.lastUsedAt ?? now
90
+ };
91
+ const data = this.readMap();
92
+ data.channels[keyOf(recordKey(full))] = full;
93
+ this.writeMap(data);
94
+ }
95
+ /**
96
+ * Bump a record's `lastUsedAt` (and optionally its known on-chain deposit)
97
+ * after resuming it. Unknown keys are a no-op.
98
+ */
99
+ touch(key, update) {
100
+ const data = this.readMap();
101
+ const existing = data.channels[keyOf(key)];
102
+ if (!existing) return;
103
+ existing.lastUsedAt = (/* @__PURE__ */ new Date()).toISOString();
104
+ if (update?.depositTotal !== void 0) {
105
+ existing.depositTotal = update.depositTotal;
106
+ }
107
+ this.writeMap(data);
108
+ }
109
+ /**
110
+ * Read one channel's nonce-watermark entry from the client's
111
+ * `channels.json` (undefined when the file or entry is missing).
112
+ * @throws {ChannelMapCorruptError} when the watermark file is unreadable.
113
+ */
114
+ readWatermark(channelId) {
115
+ return this.readWatermarkFile()[channelId];
116
+ }
117
+ /**
118
+ * Seed a fresh channel's watermark entry (`nonce 0, cumulative 0`) so a
119
+ * later resume can tell "never claimed against" apart from "watermark
120
+ * lost". Never overwrites an existing entry.
121
+ */
122
+ seedWatermark(channelId) {
123
+ const data = this.readWatermarkFile();
124
+ if (data[channelId]) return;
125
+ data[channelId] = { nonce: 0, cumulativeAmount: "0" };
126
+ mkdirSync(dirname(this.watermarkPath), { recursive: true });
127
+ writeFileSync(
128
+ this.watermarkPath,
129
+ JSON.stringify(data, null, 2),
130
+ "utf-8"
131
+ );
132
+ }
133
+ // ── file I/O ───────────────────────────────────────────────────────────────
134
+ readMap() {
135
+ let raw;
136
+ try {
137
+ raw = readFileSync(this.mapPath, "utf8");
138
+ } catch (err) {
139
+ if (err.code === "ENOENT") {
140
+ return { version: 1, channels: {} };
141
+ }
142
+ throw new ChannelMapCorruptError(
143
+ this.mapPath,
144
+ err instanceof Error ? err.message : String(err)
145
+ );
146
+ }
147
+ let parsed;
148
+ try {
149
+ parsed = JSON.parse(raw);
150
+ } catch (err) {
151
+ throw new ChannelMapCorruptError(
152
+ this.mapPath,
153
+ `invalid JSON: ${err instanceof Error ? err.message : String(err)}`
154
+ );
155
+ }
156
+ if (typeof parsed !== "object" || parsed === null || parsed.version !== 1 || typeof parsed.channels !== "object" || parsed.channels === null) {
157
+ throw new ChannelMapCorruptError(
158
+ this.mapPath,
159
+ 'expected { "version": 1, "channels": { \u2026 } }'
160
+ );
161
+ }
162
+ const channels = parsed.channels;
163
+ for (const [key, value] of Object.entries(channels)) {
164
+ if (!isRecord(value)) {
165
+ throw new ChannelMapCorruptError(
166
+ this.mapPath,
167
+ `entry ${JSON.stringify(key)} is missing required fields`
168
+ );
169
+ }
170
+ }
171
+ return parsed;
172
+ }
173
+ writeMap(data) {
174
+ mkdirSync(dirname(this.mapPath), { recursive: true });
175
+ writeFileSync(this.mapPath, JSON.stringify(data, null, 2), {
176
+ mode: 384
177
+ });
178
+ }
179
+ readWatermarkFile() {
180
+ let raw;
181
+ try {
182
+ raw = readFileSync(this.watermarkPath, "utf8");
183
+ } catch (err) {
184
+ if (err.code === "ENOENT") return {};
185
+ throw new ChannelMapCorruptError(
186
+ this.watermarkPath,
187
+ err instanceof Error ? err.message : String(err)
188
+ );
189
+ }
190
+ try {
191
+ return JSON.parse(raw);
192
+ } catch (err) {
193
+ throw new ChannelMapCorruptError(
194
+ this.watermarkPath,
195
+ `invalid JSON: ${err instanceof Error ? err.message : String(err)}`
196
+ );
197
+ }
198
+ }
199
+ };
200
+ function channelStatus(entry, nowSec = Math.floor(Date.now() / 1e3)) {
201
+ if (!entry || entry.closedAt === void 0) return "open";
202
+ if (entry.settledAt !== void 0) return "settled";
203
+ if (entry.settleableAt !== void 0 && BigInt(nowSec) >= BigInt(entry.settleableAt)) {
204
+ return "settleable";
205
+ }
206
+ return "closing";
207
+ }
208
+
209
+ export {
210
+ RIG_CHANNEL_MAP_FILENAME,
211
+ resolveChannelPaths,
212
+ ChannelMapCorruptError,
213
+ recordKey,
214
+ ChannelMapStore,
215
+ channelStatus
216
+ };
217
+ //# sourceMappingURL=chunk-O6TXHKWG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/standalone/channel-map.ts"],"sourcesContent":["/**\n * Peer→channel map for the STANDALONE embedded publisher (#262).\n *\n * Why this exists: `@toon-protocol/client`'s `ChannelManager` persists the\n * off-chain nonce/cumulative-claim watermark (its `ChannelStore`,\n * `channels.json`, keyed by channelId) but keeps the peer→channelId mapping\n * ONLY in memory. So every standalone CLI invocation used to open (and fund)\n * a FRESH on-chain channel — the #260 fresh-outsider e2e stranded five\n * deposits across five commands. This store remembers WHICH channel the\n * identity holds with each peer, keyed by\n * `identity pubkey | ILP anchor (peer/apex destination) | chain | tokenNetwork`,\n * so the next invocation resumes it via `ChannelManager.trackChannel` (which\n * rehydrates the nonce watermark from `channels.json`) with zero on-chain\n * writes.\n *\n * It is the standalone twin of the daemon's\n * `packages/client-mcp/src/daemon/apex-channel-store.ts` — same record shape\n * (channelId + the chain context `trackChannel` needs), extended with the\n * identity pubkey (rig identities come from an env/`.env` precedence chain,\n * so one state dir can serve several identities) and the tokenNetwork.\n * `@toon-protocol/rig` must not import `@toon-protocol/client-mcp` (that\n * package depends on this one — circular), hence the twin; keep the\n * semantics in sync.\n *\n * CONCURRENCY: writes happen only from paid commands, which already hold the\n * per-identity advisory lockfile (./nonce-guard.ts `NonceLock`) for their\n * whole lifetime — the same guard that serializes the claim watermark also\n * serializes this file for one identity. `rig channel list` reads only.\n *\n * CORRUPTION: an unreadable/invalid map file is a hard\n * {@link ChannelMapCorruptError} — surfaced BEFORE any on-chain open — never\n * an empty fallback. Falling back to \"no channels\" would silently open (and\n * fund) a duplicate channel, which is exactly the #262 bug.\n *\n * This module is dependency-light on purpose (node:fs only): the free\n * `rig channel list` command reads it without the optional\n * `@toon-protocol/client` peer dependency installed.\n */\n\nimport { mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { dirname, join } from 'node:path';\n\n// ---------------------------------------------------------------------------\n// Paths (TOON_CLIENT_HOME conventions — see nonce-guard.ts module doc)\n// ---------------------------------------------------------------------------\n\n/** Map filename under the shared client state dir. */\nexport const RIG_CHANNEL_MAP_FILENAME = 'rig-channels.json';\n\n/**\n * Resolve the two channel-state files under `TOON_CLIENT_HOME` (default\n * `~/.toon-client`): the rig peer→channel map, and the client's nonce\n * watermark store (`config.json`'s `channelStorePath`, default\n * `<dir>/channels.json` — the same resolution `cli/standalone-mode.ts` feeds\n * the embedded ToonClient).\n */\nexport function resolveChannelPaths(env: NodeJS.ProcessEnv): {\n mapPath: string;\n watermarkPath: string;\n} {\n const dir = env['TOON_CLIENT_HOME'] ?? join(homedir(), '.toon-client');\n let configured: string | undefined;\n try {\n const raw = readFileSync(join(dir, 'config.json'), 'utf8');\n const parsed = JSON.parse(raw) as { channelStorePath?: unknown };\n if (typeof parsed.channelStorePath === 'string') {\n configured = parsed.channelStorePath;\n }\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {\n throw new Error(\n `failed to read client config at ${join(dir, 'config.json')}: ` +\n `${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n return {\n mapPath: join(dir, RIG_CHANNEL_MAP_FILENAME),\n watermarkPath: configured ?? join(dir, 'channels.json'),\n };\n}\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Chain context `ChannelManager.trackChannel` needs to resume a channel\n * (same shape the daemon's apex-channel-store persists).\n */\nexport interface PersistedChannelContext {\n chainType: string;\n chainId: number;\n tokenNetworkAddress: string;\n tokenAddress?: string;\n /** Counterparty settlement address (required for Solana/Mina proofs). */\n recipient?: string;\n}\n\n/** One persisted peer→channel binding. */\nexport interface ChannelMapRecord {\n /** On-chain payment channel id. */\n channelId: string;\n /** Registered peer id the negotiation was keyed by (`peerNegotiations`). */\n peerId: string;\n /** Hex Nostr pubkey of the identity that opened the channel. */\n identity: string;\n /** ILP anchor destination the channel was opened against (peer/apex). */\n destination: string;\n /** Negotiated settlement chain, e.g. `evm:31337`. */\n chain: string;\n /** TokenNetwork contract address ('' when the peer announced none). */\n tokenNetwork: string;\n /** Context for `trackChannel` on resume. */\n context: PersistedChannelContext;\n /** On-chain deposit total (base units, string), when known. */\n depositTotal?: string;\n /** ISO timestamps. */\n openedAt: string;\n lastUsedAt: string;\n}\n\n/** The composite key a record is stored under. */\nexport interface ChannelMapKey {\n identity: string;\n destination: string;\n chain: string;\n tokenNetwork: string;\n}\n\n/**\n * One entry of the client's nonce-watermark store (`channels.json`) —\n * format DUPLICATED from `@toon-protocol/client`'s `JsonFileChannelStore`\n * (`packages/client/src/channel/ChannelStore.ts`); keep in sync.\n */\nexport interface WatermarkEntry {\n nonce: number;\n /** Cumulative claimed amount, base units (string-encoded bigint). */\n cumulativeAmount: string;\n /** Withdraw-flow timers, string-encoded unix SECONDS. */\n closedAt?: string;\n settleableAt?: string;\n settledAt?: string;\n}\n\n/** The peer→channel map file is unreadable or malformed. */\nexport class ChannelMapCorruptError extends Error {\n constructor(\n public readonly path: string,\n detail: string\n ) {\n super(\n `channel state file ${path} is corrupt (${detail}) — refusing to ` +\n 'continue: proceeding would silently open (and fund) a duplicate ' +\n 'on-chain channel. Fix or remove the file; removing it makes rig ' +\n 'forget which channels it holds (existing deposits stay locked ' +\n 'on-chain until settled).'\n );\n this.name = 'ChannelMapCorruptError';\n }\n}\n\n// ---------------------------------------------------------------------------\n// Store\n// ---------------------------------------------------------------------------\n\ninterface MapFile {\n version: 1;\n channels: Record<string, ChannelMapRecord>;\n}\n\nfunction keyOf(key: ChannelMapKey): string {\n return `${key.identity}|${key.destination}|${key.chain}|${key.tokenNetwork}`;\n}\n\n/** The key fields of a full record. */\nexport function recordKey(record: ChannelMapRecord): ChannelMapKey {\n return {\n identity: record.identity,\n destination: record.destination,\n chain: record.chain,\n tokenNetwork: record.tokenNetwork,\n };\n}\n\nfunction isContext(v: unknown): v is PersistedChannelContext {\n if (typeof v !== 'object' || v === null) return false;\n const c = v as Record<string, unknown>;\n return (\n typeof c['chainType'] === 'string' &&\n typeof c['chainId'] === 'number' &&\n typeof c['tokenNetworkAddress'] === 'string'\n );\n}\n\nfunction isRecord(v: unknown): v is ChannelMapRecord {\n if (typeof v !== 'object' || v === null) return false;\n const r = v as Record<string, unknown>;\n return (\n typeof r['channelId'] === 'string' &&\n typeof r['peerId'] === 'string' &&\n typeof r['identity'] === 'string' &&\n typeof r['destination'] === 'string' &&\n typeof r['chain'] === 'string' &&\n typeof r['tokenNetwork'] === 'string' &&\n isContext(r['context'])\n );\n}\n\nexport interface ChannelMapStoreOptions {\n /** The rig peer→channel map file (`rig-channels.json`). */\n mapPath: string;\n /** The client's nonce-watermark store (`channels.json`). */\n watermarkPath: string;\n}\n\n/**\n * File-backed peer→channel map + read/seed access to the client's nonce\n * watermark store. Synchronous I/O (matches the client's `ChannelStore`\n * surface); see the module doc for the locking and corruption contracts.\n */\nexport class ChannelMapStore {\n readonly mapPath: string;\n readonly watermarkPath: string;\n\n constructor(options: ChannelMapStoreOptions) {\n this.mapPath = options.mapPath;\n this.watermarkPath = options.watermarkPath;\n }\n\n /** All recorded channels. @throws {ChannelMapCorruptError} */\n list(): ChannelMapRecord[] {\n return Object.values(this.readMap().channels);\n }\n\n /**\n * Recorded channels for one (identity, destination) pair — the resume\n * candidates for a paid command. @throws {ChannelMapCorruptError}\n */\n listFor(identity: string, destination: string): ChannelMapRecord[] {\n return this.list().filter(\n (r) => r.identity === identity && r.destination === destination\n );\n }\n\n /**\n * Record a (freshly opened) channel. Overwrites any previous record under\n * the same (identity, destination, chain, tokenNetwork) key — the old\n * channel is closed/stale by then; its claim watermark stays in the\n * watermark store.\n */\n record(\n record: Omit<ChannelMapRecord, 'openedAt' | 'lastUsedAt'> &\n Partial<Pick<ChannelMapRecord, 'openedAt' | 'lastUsedAt'>>\n ): void {\n const now = new Date().toISOString();\n const full: ChannelMapRecord = {\n ...record,\n openedAt: record.openedAt ?? now,\n lastUsedAt: record.lastUsedAt ?? now,\n };\n const data = this.readMap();\n data.channels[keyOf(recordKey(full))] = full;\n this.writeMap(data);\n }\n\n /**\n * Bump a record's `lastUsedAt` (and optionally its known on-chain deposit)\n * after resuming it. Unknown keys are a no-op.\n */\n touch(key: ChannelMapKey, update?: { depositTotal?: string }): void {\n const data = this.readMap();\n const existing = data.channels[keyOf(key)];\n if (!existing) return;\n existing.lastUsedAt = new Date().toISOString();\n if (update?.depositTotal !== undefined) {\n existing.depositTotal = update.depositTotal;\n }\n this.writeMap(data);\n }\n\n /**\n * Read one channel's nonce-watermark entry from the client's\n * `channels.json` (undefined when the file or entry is missing).\n * @throws {ChannelMapCorruptError} when the watermark file is unreadable.\n */\n readWatermark(channelId: string): WatermarkEntry | undefined {\n return this.readWatermarkFile()[channelId];\n }\n\n /**\n * Seed a fresh channel's watermark entry (`nonce 0, cumulative 0`) so a\n * later resume can tell \"never claimed against\" apart from \"watermark\n * lost\". Never overwrites an existing entry.\n */\n seedWatermark(channelId: string): void {\n const data = this.readWatermarkFile();\n if (data[channelId]) return;\n data[channelId] = { nonce: 0, cumulativeAmount: '0' };\n mkdirSync(dirname(this.watermarkPath), { recursive: true });\n writeFileSync(\n this.watermarkPath,\n JSON.stringify(data, null, 2),\n 'utf-8'\n );\n }\n\n // ── file I/O ───────────────────────────────────────────────────────────────\n\n private readMap(): MapFile {\n let raw: string;\n try {\n raw = readFileSync(this.mapPath, 'utf8');\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n return { version: 1, channels: {} };\n }\n throw new ChannelMapCorruptError(\n this.mapPath,\n err instanceof Error ? err.message : String(err)\n );\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n throw new ChannelMapCorruptError(\n this.mapPath,\n `invalid JSON: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n if (\n typeof parsed !== 'object' ||\n parsed === null ||\n (parsed as { version?: unknown }).version !== 1 ||\n typeof (parsed as { channels?: unknown }).channels !== 'object' ||\n (parsed as { channels?: unknown }).channels === null\n ) {\n throw new ChannelMapCorruptError(\n this.mapPath,\n 'expected { \"version\": 1, \"channels\": { … } }'\n );\n }\n const channels = (parsed as { channels: Record<string, unknown> })\n .channels;\n for (const [key, value] of Object.entries(channels)) {\n if (!isRecord(value)) {\n throw new ChannelMapCorruptError(\n this.mapPath,\n `entry ${JSON.stringify(key)} is missing required fields`\n );\n }\n }\n return parsed as MapFile;\n }\n\n private writeMap(data: MapFile): void {\n mkdirSync(dirname(this.mapPath), { recursive: true });\n writeFileSync(this.mapPath, JSON.stringify(data, null, 2), {\n mode: 0o600,\n });\n }\n\n private readWatermarkFile(): Record<string, WatermarkEntry> {\n let raw: string;\n try {\n raw = readFileSync(this.watermarkPath, 'utf8');\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return {};\n throw new ChannelMapCorruptError(\n this.watermarkPath,\n err instanceof Error ? err.message : String(err)\n );\n }\n try {\n return JSON.parse(raw) as Record<string, WatermarkEntry>;\n } catch (err) {\n throw new ChannelMapCorruptError(\n this.watermarkPath,\n `invalid JSON: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Status derivation\n// ---------------------------------------------------------------------------\n\n/**\n * Where a channel sits in the withdraw journey, from its watermark timers —\n * mirrors `ChannelManager.getChannelCloseState`. A missing entry reads as\n * `open` (recorded channels are seeded at open time; a lost watermark file\n * surfaces separately as unknown claim state).\n */\nexport function channelStatus(\n entry: WatermarkEntry | undefined,\n nowSec: number = Math.floor(Date.now() / 1000)\n): 'open' | 'closing' | 'settleable' | 'settled' {\n if (!entry || entry.closedAt === undefined) return 'open';\n if (entry.settledAt !== undefined) return 'settled';\n if (\n entry.settleableAt !== undefined &&\n BigInt(nowSec) >= BigInt(entry.settleableAt)\n ) {\n return 'settleable';\n }\n return 'closing';\n}\n"],"mappings":"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","names":[]}
@@ -340,6 +340,7 @@ async function fetchRemoteState(options) {
340
340
  }
341
341
 
342
342
  export {
343
+ queryRelay,
343
344
  fetchRemoteState
344
345
  };
345
- //# sourceMappingURL=chunk-G4W4MH6G.js.map
346
+ //# sourceMappingURL=chunk-PLKZAUTG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/remote-state.ts","../../arweave/src/gateways.ts","../../arweave/src/git-sha.ts"],"sourcesContent":["/**\n * Remote repository state reader — the \"what does the remote have?\" half of\n * `rig push` (epic #222, ticket #225).\n *\n * Queries relay(s) over NIP-01 WebSocket for the repository's NIP-34 state:\n * - kind:30618 (repository state): `r` tags → ref map, `HEAD` symref,\n * `arweave` tags → git SHA → Arweave txId hints.\n * - kind:30617 (repository announcement): presence = the repo exists on\n * TOON (first-push detection) + name/description/relays metadata.\n *\n * Both kinds are NIP-33 parameterized-replaceable: per relay only the latest\n * event per (kind, author, d) survives, but we still pick the newest across\n * relays (highest created_at, ties broken by lowest id per NIP-01).\n *\n * SHAs missing from the `arweave` tag map can be resolved via the shared\n * Arweave GraphQL Git-SHA resolver (@toon-protocol/arweave — the same module\n * the rig SPA uses) through {@link RemoteState.resolveMissing}.\n *\n * Relay payload encodings (mirrors rig's `web/relay-client.ts` decode logic):\n * EVENT payloads arrive as an inline JSON object (standard NIP-01), as a\n * double-JSON-encoded string (devnet relay quirk: a JSON string containing\n * the event JSON), or as a TOON-encoded string. All three are tolerated.\n */\n\nimport { decode as decodeToon } from '@toon-format/toon';\nimport {\n resolveGitSha,\n seedShaCache,\n shaCacheKey,\n} from '@toon-protocol/arweave';\nimport { REPOSITORY_ANNOUNCEMENT_KIND } from '@toon-protocol/core/nip34';\n\nimport { REPOSITORY_STATE_KIND } from './nip34-events.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** A signed Nostr event as seen on a relay (read side). */\nexport interface NostrEvent {\n id: string;\n pubkey: string;\n created_at: number;\n kind: number;\n tags: string[][];\n content: string;\n sig: string;\n}\n\n/** NIP-01 subscription filter (only the fields this module sends). */\nexport interface NostrFilter {\n kinds?: number[];\n authors?: string[];\n '#d'?: string[];\n limit?: number;\n}\n\n/**\n * Minimal structural WebSocket type — satisfied by the WHATWG WebSocket\n * global (Node >= 22 / undici, browsers) and by the `ws` package.\n */\nexport interface WebSocketLike {\n readyState: number;\n send(data: string): void;\n close(): void;\n addEventListener(type: string, listener: (event: never) => void): void;\n}\n\n/** Factory for WebSocket connections (injectable for tests / `ws` fallback). */\nexport type WebSocketFactory = (url: string) => WebSocketLike;\n\nexport interface FetchRemoteStateOptions {\n /**\n * Relay WebSocket URLs to query. Plural from day one (forward-compat with\n * multi-relay routing); size 1 is typical today. Results are merged and the\n * newest replaceable event across all relays wins.\n */\n relayUrls: string[];\n /** Repository owner's pubkey (hex) — the author of 30617/30618. */\n ownerPubkey: string;\n /** Repository identifier (NIP-34 `d` tag). */\n repoId: string;\n /** Per-relay timeout in milliseconds (default 10000). On timeout the relay contributes whatever it sent so far. */\n timeoutMs?: number;\n /**\n * Git-SHA → Arweave txId resolver used by {@link RemoteState.resolveMissing}.\n * Defaults to the shared GraphQL resolver from @toon-protocol/arweave;\n * injectable for tests.\n */\n resolveSha?: (sha: string, repo: string) => Promise<string | null>;\n /** WebSocket constructor override (defaults to the global WebSocket). */\n webSocketFactory?: WebSocketFactory;\n}\n\n/** Remote repository state assembled from relay events. */\nexport interface RemoteState {\n /** True when a kind:30617 announcement exists (false ⇒ first push). */\n announced: boolean;\n /** Ref map from the latest kind:30618: refname → commit SHA. */\n refs: Map<string, string>;\n /** HEAD symref target (e.g. `refs/heads/main`), or null if unset. */\n headSymref: string | null;\n /** Git SHA → Arweave txId hints from the latest kind:30618 `arweave` tags. */\n shaToTxId: Map<string, string>;\n /** The latest kind:30618 event, or null if the repo has no state yet. */\n refsEvent: NostrEvent | null;\n /** The latest kind:30617 event, or null if the repo is unannounced. */\n announceEvent: NostrEvent | null;\n /** Repository name from the announcement `name` tag. */\n name: string | null;\n /** Announcement `description` tag (falls back to event content). */\n description: string | null;\n /** Relay URLs advertised in the announcement `relays` tag(s). */\n relays: string[];\n /**\n * Resolve SHAs to Arweave txIds: served from the `arweave` tag map when\n * present, otherwise via the GraphQL Git-SHA resolver. SHAs that resolve\n * nowhere are omitted from the returned map.\n */\n resolveMissing(shas: string[]): Promise<Map<string, string>>;\n}\n\n// ---------------------------------------------------------------------------\n// Relay query (NIP-01 REQ → EVENT* → EOSE)\n// ---------------------------------------------------------------------------\n\n/** Validate that a relay URL uses a WebSocket scheme (mirrors rig's url-utils). */\nfunction isValidRelayUrl(url: string): boolean {\n return /^wss?:\\/\\//i.test(url);\n}\n\n/** WHATWG WebSocket OPEN ready state. */\nconst WS_OPEN = 1;\n\nfunction defaultWebSocketFactory(url: string): WebSocketLike {\n const ctor = (\n globalThis as { WebSocket?: new (url: string) => WebSocketLike }\n ).WebSocket;\n if (!ctor) {\n throw new Error(\n 'No global WebSocket constructor (Node >= 22 required) — pass webSocketFactory'\n );\n }\n return new ctor(url);\n}\n\n/**\n * Decode a relay EVENT payload, tolerating every encoding seen in the wild:\n * inline object (standard NIP-01), double-JSON-encoded string (devnet relay\n * serves the event as a JSON string containing the event JSON), or a\n * TOON-encoded string (rig's `decodeToonMessage` path).\n */\nfunction decodeEventPayload(payload: unknown): NostrEvent | null {\n if (payload !== null && typeof payload === 'object') {\n return payload as NostrEvent;\n }\n if (typeof payload !== 'string') {\n return null;\n }\n try {\n const parsed: unknown = JSON.parse(payload);\n if (parsed !== null && typeof parsed === 'object') {\n return parsed as NostrEvent;\n }\n } catch {\n // Not JSON — fall through to TOON\n }\n try {\n return decodeToon(payload) as unknown as NostrEvent;\n } catch {\n return null;\n }\n}\n\n/**\n * Query one relay: send a REQ, collect EVENTs until EOSE, then CLOSE.\n * Mirrors rig's `queryRelay` (partial results on timeout / early close).\n * Exported for reuse by the network-bootstrap discovery (kind:10032).\n */\nexport function queryRelay(\n relayUrl: string,\n filter: NostrFilter,\n timeoutMs: number,\n webSocketFactory: WebSocketFactory\n): Promise<NostrEvent[]> {\n return new Promise((resolve, reject) => {\n const events: NostrEvent[] = [];\n const subId = `git-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;\n let ws: WebSocketLike;\n // eslint-disable-next-line prefer-const -- assigned after `settle` is defined\n let timeoutHandle: ReturnType<typeof setTimeout>;\n let settled = false;\n\n const settle = (outcome: 'resolve' | 'reject', error?: Error) => {\n if (settled) return;\n settled = true;\n clearTimeout(timeoutHandle);\n try {\n if (ws.readyState === WS_OPEN) {\n ws.send(JSON.stringify(['CLOSE', subId]));\n }\n ws.close();\n } catch {\n // Ignore close errors\n }\n if (outcome === 'reject') {\n reject(error);\n } else {\n resolve(events);\n }\n };\n\n if (!isValidRelayUrl(relayUrl)) {\n reject(\n new Error(\n `Invalid relay URL protocol (must be ws:// or wss://): ${relayUrl}`\n )\n );\n return;\n }\n\n try {\n ws = webSocketFactory(relayUrl);\n } catch (err) {\n reject(new Error(`Failed to connect to relay ${relayUrl}: ${String(err)}`));\n return;\n }\n\n timeoutHandle = setTimeout(() => {\n // Resolve with whatever we collected so far (partial results)\n settle('resolve');\n }, timeoutMs);\n\n ws.addEventListener('open', () => {\n ws.send(JSON.stringify(['REQ', subId, filter]));\n });\n\n ws.addEventListener('message', (msgEvent: { data?: unknown }) => {\n try {\n const msg = JSON.parse(String(msgEvent.data)) as unknown[];\n if (!Array.isArray(msg) || msg.length < 2) return;\n\n const msgType = msg[0];\n if (msgType === 'EVENT' && msg[1] === subId && msg[2] !== undefined) {\n const event = decodeEventPayload(msg[2]);\n if (event) events.push(event);\n } else if (msgType === 'EOSE' && msg[1] === subId) {\n settle('resolve');\n }\n } catch {\n // Ignore parse errors for individual messages\n }\n });\n\n ws.addEventListener('error', (event: { message?: unknown }) => {\n const detail =\n typeof event === 'object' && event !== null && 'message' in event\n ? String(event.message)\n : 'unknown';\n settle(\n 'reject',\n new Error(`WebSocket error connecting to ${relayUrl}: ${detail}`)\n );\n });\n\n ws.addEventListener('close', () => {\n // If we haven't settled yet, resolve with what we have\n settle('resolve');\n });\n });\n}\n\n// ---------------------------------------------------------------------------\n// NIP-33 replaceable selection + tag parsing\n// ---------------------------------------------------------------------------\n\n/**\n * Pick the winning replaceable event: highest created_at, ties broken by\n * lowest id (NIP-01 replaceable-event convention).\n */\nfunction latestReplaceable(events: NostrEvent[]): NostrEvent | null {\n let winner: NostrEvent | null = null;\n for (const event of events) {\n if (\n winner === null ||\n event.created_at > winner.created_at ||\n (event.created_at === winner.created_at && event.id < winner.id)\n ) {\n winner = event;\n }\n }\n return winner;\n}\n\n/** Get the first value for a tag name. */\nfunction getTagValue(tags: string[][], name: string): string | undefined {\n const tag = tags.find((t) => t[0] === name);\n return tag?.[1];\n}\n\n/** Maximum number of refs parsed from a single kind:30618 event (mirrors views). */\nconst MAX_REFS_PER_EVENT = 1000;\n\n/** Symref prefix used in `HEAD` tags: `[\"HEAD\", \"ref: refs/heads/main\"]`. */\nconst SYMREF_PREFIX = 'ref: ';\n\ninterface ParsedRefs {\n refs: Map<string, string>;\n headSymref: string | null;\n shaToTxId: Map<string, string>;\n}\n\n/** Parse a kind:30618 event's `r` / `HEAD` / `arweave` tags. */\nfunction parseRefsEvent(event: NostrEvent): ParsedRefs {\n const refs = new Map<string, string>();\n const shaToTxId = new Map<string, string>();\n let headSymref: string | null = null;\n\n for (const tag of event.tags) {\n const [tagName, v1, v2] = tag;\n if (tagName === 'r' && v1 && v2) {\n if (v1 === 'HEAD' && v2.startsWith(SYMREF_PREFIX)) {\n // Alternate symref spelling: [\"r\", \"HEAD\", \"ref: refs/heads/main\"]\n headSymref = v2.slice(SYMREF_PREFIX.length);\n continue;\n }\n if (refs.size >= MAX_REFS_PER_EVENT) continue;\n refs.set(v1, v2);\n } else if (tagName === 'HEAD' && v1?.startsWith(SYMREF_PREFIX)) {\n // NIP-34 symref tag: [\"HEAD\", \"ref: refs/heads/main\"]\n headSymref = v1.slice(SYMREF_PREFIX.length);\n } else if (tagName === 'arweave' && v1 && v2) {\n shaToTxId.set(v1, v2);\n }\n }\n\n return { refs, headSymref, shaToTxId };\n}\n\n// ---------------------------------------------------------------------------\n// fetchRemoteState\n// ---------------------------------------------------------------------------\n\n/**\n * Fetch the remote repository state from the relay(s).\n *\n * Sends one REQ per relay for kind:30617 + kind:30618 with\n * `authors=[ownerPubkey]`, `#d=[repoId]`, collects until EOSE (or timeout),\n * and reduces to the latest replaceable event per kind.\n *\n * Resolves as long as at least one relay answers; throws only when every\n * relay fails. Events from other authors / repos are ignored (defense\n * against misbehaving relays that over-return).\n */\nexport async function fetchRemoteState(\n options: FetchRemoteStateOptions\n): Promise<RemoteState> {\n const {\n relayUrls,\n ownerPubkey,\n repoId,\n timeoutMs = 10000,\n resolveSha = resolveGitSha,\n webSocketFactory = defaultWebSocketFactory,\n } = options;\n\n if (relayUrls.length === 0) {\n throw new Error('fetchRemoteState: relayUrls must not be empty');\n }\n if (!ownerPubkey) {\n throw new Error('fetchRemoteState: ownerPubkey is required');\n }\n if (!repoId) {\n throw new Error('fetchRemoteState: repoId is required');\n }\n\n const filter: NostrFilter = {\n kinds: [REPOSITORY_ANNOUNCEMENT_KIND, REPOSITORY_STATE_KIND],\n authors: [ownerPubkey],\n '#d': [repoId],\n };\n\n const results = await Promise.allSettled(\n relayUrls.map((url) => queryRelay(url, filter, timeoutMs, webSocketFactory))\n );\n\n const failures: string[] = [];\n const byId = new Map<string, NostrEvent>();\n for (const result of results) {\n if (result.status === 'rejected') {\n failures.push(String((result.reason as Error)?.message ?? result.reason));\n continue;\n }\n for (const event of result.value) {\n // Only trust events from the repo owner for this repo — relays are\n // untrusted and may over-return.\n if (event.pubkey !== ownerPubkey) continue;\n if (getTagValue(event.tags, 'd') !== repoId) continue;\n if (typeof event.id === 'string' && !byId.has(event.id)) {\n byId.set(event.id, event);\n }\n }\n }\n\n if (failures.length === relayUrls.length) {\n throw new Error(\n `fetchRemoteState: all ${relayUrls.length} relay(s) failed: ${failures.join('; ')}`\n );\n }\n\n const events = [...byId.values()];\n const refsEvent = latestReplaceable(\n events.filter((e) => e.kind === REPOSITORY_STATE_KIND)\n );\n const announceEvent = latestReplaceable(\n events.filter((e) => e.kind === REPOSITORY_ANNOUNCEMENT_KIND)\n );\n\n const { refs, headSymref, shaToTxId } = refsEvent\n ? parseRefsEvent(refsEvent)\n : {\n refs: new Map<string, string>(),\n headSymref: null,\n shaToTxId: new Map<string, string>(),\n };\n\n // Seed the shared resolver cache so later resolveGitSha calls (here or in\n // any other consumer of @toon-protocol/arweave) skip GraphQL for known SHAs.\n if (shaToTxId.size > 0) {\n seedShaCache(\n [...shaToTxId].map(\n ([sha, txId]) => [shaCacheKey(sha, repoId), txId] as [string, string]\n )\n );\n }\n\n // Announcement metadata (mirrors views' parseRepoAnnouncement defaults).\n const name = announceEvent\n ? (getTagValue(announceEvent.tags, 'name') ?? null)\n : null;\n const description = announceEvent\n ? (getTagValue(announceEvent.tags, 'description') ?? announceEvent.content)\n : null;\n // NIP-34 announcement relays tag is multi-valued: [\"relays\", url1, url2, …]\n const relays: string[] = [];\n if (announceEvent) {\n for (const tag of announceEvent.tags) {\n if (tag[0] === 'relays') {\n relays.push(...tag.slice(1).filter((url) => url.length > 0));\n }\n }\n }\n\n const resolveMissing = async (\n shas: string[]\n ): Promise<Map<string, string>> => {\n const resolved = new Map<string, string>();\n const missing: string[] = [];\n for (const sha of new Set(shas)) {\n const known = shaToTxId.get(sha);\n if (known !== undefined) {\n resolved.set(sha, known);\n } else {\n missing.push(sha);\n }\n }\n const lookups = await Promise.all(\n missing.map(\n async (sha) => [sha, await resolveSha(sha, repoId)] as const\n )\n );\n for (const [sha, txId] of lookups) {\n if (txId) resolved.set(sha, txId);\n }\n return resolved;\n };\n\n return {\n announced: announceEvent !== null,\n refs,\n headSymref,\n shaToTxId,\n refsEvent,\n announceEvent,\n name,\n description,\n relays,\n resolveMissing,\n };\n}\n","/**\n * Arweave gateway redundancy — single source of truth.\n *\n * Media bytes are content-addressed by Arweave tx id, so every gateway serves\n * the same bytes. This module owns the ordered gateway preference list and the\n * URL helpers used on BOTH sides of the wire:\n * - upload (client-mcp daemon): stamp a primary `url` + `fallback` mirrors.\n * - render (views/rig browser): re-point imeta URLs + fail over on error.\n *\n * Previously hand-duplicated in `views`, `rig`, and `client-mcp`; those now all\n * import from here. The default list can be overridden per call (e.g. from a\n * daemon env var) — pass `gateways` to the helpers.\n */\n\n/** Ordered Arweave gateways to try (primary first, then fallbacks). */\nexport const ARWEAVE_GATEWAYS = [\n 'https://ar-io.dev',\n 'https://arweave.net',\n 'https://permagate.io',\n] as const;\n\n/** Timeout for individual Arweave fetch requests in milliseconds. */\nexport const ARWEAVE_FETCH_TIMEOUT_MS = 15000;\n\n/** Arweave transaction IDs are 43-character base64url strings. */\nconst TX_ID_RE = /^[a-zA-Z0-9_-]{43}$/;\n\n/** Hosts recognized as Arweave gateways (path-addressed). */\nconst ARWEAVE_HOST_RE =\n /(^|\\.)(arweave\\.net|ar-io\\.dev|permagate\\.io|g8way\\.io|ar\\.io)$/i;\n\n/**\n * Extract an Arweave tx id from a media URL, or null if it is not\n * Arweave-addressable. Handles `ar://<txid>` and path-style\n * `https://<gateway>/<txid>`.\n *\n * Sandbox-subdomain URLs (`https://<txid>.<gateway>`) are deliberately NOT\n * decoded: tx ids are case-sensitive base64url, but `URL` (and DNS) lower-case\n * the hostname, which would corrupt the id. Real gateway sandboxing uses a\n * base32 label, not the raw id — the canonical id always travels in the path.\n */\nexport function arweaveTxId(rawUrl: string): string | null {\n const ar = /^ar:\\/\\/([a-zA-Z0-9_-]{43})(?:[/?#]|$)/.exec(rawUrl);\n if (ar?.[1]) return ar[1];\n\n let u: URL;\n try {\n u = new URL(rawUrl);\n } catch {\n return null;\n }\n // Only re-point hosts we actually recognize as Arweave gateways, so a stray\n // 43-char path segment on some other CDN is never misread as a tx id.\n if (!ARWEAVE_HOST_RE.test(u.hostname)) return null;\n\n // Path style: https://arweave.net/<txid>\n const seg = u.pathname.split('/').find(Boolean);\n if (seg && TX_ID_RE.test(seg)) return seg;\n\n return null;\n}\n\n/**\n * Primary URL + fallback mirror URLs for an Arweave tx id, one per gateway in\n * preference order. Used by the upload path to stamp `imeta` `url` + `fallback`.\n */\nexport function arweaveUrls(\n txId: string,\n gateways: readonly string[] = ARWEAVE_GATEWAYS\n): { url: string; fallbacks: string[] } {\n const all = (gateways.length ? gateways : ARWEAVE_GATEWAYS).map(\n (g) => `${g}/${txId}`\n );\n const [url, ...fallbacks] = all;\n return { url: url ?? `${ARWEAVE_GATEWAYS[0]}/${txId}`, fallbacks };\n}\n\n/**\n * Ordered candidate URLs for a media URL. Arweave-addressable URLs expand to the\n * full gateway-preference list (primary first); anything else is returned\n * unchanged. `extraFallbacks` (e.g. publisher-supplied `imeta` mirrors) are\n * appended last, de-duplicated. Used by the render path to fail over on error.\n */\nexport function arweaveGatewayCandidates(\n rawUrl: string,\n extraFallbacks: string[] = [],\n gateways: readonly string[] = ARWEAVE_GATEWAYS\n): string[] {\n const txId = arweaveTxId(rawUrl);\n const candidates = txId\n ? (gateways.length ? gateways : ARWEAVE_GATEWAYS).map((g) => `${g}/${txId}`)\n : [rawUrl];\n const seen = new Set(candidates);\n for (const f of extraFallbacks) {\n if (f && !seen.has(f)) {\n seen.add(f);\n candidates.push(f);\n }\n }\n return candidates;\n}\n","/**\n * Git-SHA → Arweave transaction ID resolution.\n *\n * Git objects uploaded to Arweave are tagged with `Git-SHA` (the object's\n * SHA-1) and `Repo` (the repository identifier, matching the NIP-34 `d` tag).\n * This module resolves a git SHA to its Arweave tx id via the Arweave GraphQL\n * gateway, with a bounded in-memory cache that can be pre-seeded from relay\n * state (kind:30618 `arweave` tags) to skip the GraphQL indexing delay.\n *\n * Extracted verbatim from rig's `web/arweave-client.ts` (#225) so the browser\n * SPA (rig-web) and the Node write path (@toon-protocol/rig) share ONE resolver.\n * Uses only WHATWG fetch + AbortSignal.timeout — browser and Node compatible.\n */\n\nimport { ARWEAVE_FETCH_TIMEOUT_MS } from './gateways.js';\n\n/** Arweave GraphQL endpoint used for Git-SHA tag lookups. */\nconst ARWEAVE_GRAPHQL_URL = 'https://arweave.net/graphql';\n\n/** Maximum number of entries in the SHA-to-txId cache to prevent unbounded memory growth. */\nconst SHA_CACHE_MAX_SIZE = 10000;\n\n/** In-memory cache for SHA-to-txId resolution. Bounded to prevent memory leaks. */\nconst shaToTxIdCache = new Map<string, string>();\n\n/**\n * Validate a git SHA-1 hash format (40-character hex string).\n */\nfunction isValidGitSha(sha: string): boolean {\n return /^[0-9a-f]{40}$/i.test(sha);\n}\n\n/**\n * Sanitize a string for safe inclusion in a GraphQL query.\n * Removes characters that could break out of a GraphQL string literal,\n * including backticks which some GraphQL parsers may interpret.\n */\nfunction sanitizeGraphQLValue(value: string): string {\n // eslint-disable-next-line no-control-regex -- intentional: strip control chars for GraphQL safety\n return value.replace(/[\"\\\\\\n\\r\\u0000-\\u001f`]/g, '');\n}\n\n/**\n * Build the cache key used by {@link resolveGitSha} / {@link seedShaCache}.\n *\n * The cache is keyed on `\"sha:repo\"` so the same SHA in different repos\n * resolves independently (uploads are tagged per-repo).\n */\nexport function shaCacheKey(sha: string, repo: string): string {\n return `${sha}:${repo}`;\n}\n\n/**\n * Clear the SHA-to-txId cache. Used for test isolation.\n */\nexport function clearShaCache(): void {\n shaToTxIdCache.clear();\n}\n\n/**\n * Pre-seed the SHA-to-txId cache with known mappings.\n *\n * Used when txId mappings are available from relay events (e.g., kind:30618\n * `arweave` tags) to avoid the GraphQL indexing delay after Turbo/Irys uploads.\n *\n * @param mappings - Map of \"sha:repo\" cache keys to Arweave transaction IDs\n */\nexport function seedShaCache(\n mappings: Map<string, string> | [string, string][]\n): void {\n const entries = mappings instanceof Map ? mappings.entries() : mappings;\n for (const [key, txId] of entries) {\n if (shaToTxIdCache.size >= SHA_CACHE_MAX_SIZE) {\n const firstKey = shaToTxIdCache.keys().next().value;\n if (firstKey !== undefined) {\n shaToTxIdCache.delete(firstKey);\n }\n }\n shaToTxIdCache.set(key, txId);\n }\n}\n\n/** Arweave transaction IDs are 43-character base64url strings. */\nconst ARWEAVE_TX_ID_RE = /^[a-zA-Z0-9_-]{43}$/;\n\n/**\n * Validate an Arweave transaction ID format.\n * Arweave tx IDs are 43-character base64url-encoded strings.\n */\nexport function isValidArweaveTxId(txId: string): boolean {\n return ARWEAVE_TX_ID_RE.test(txId);\n}\n\n/**\n * Resolve a git SHA to an Arweave transaction ID via GraphQL.\n *\n * Queries the Arweave GraphQL endpoint for transactions tagged with\n * the given Git-SHA and Repo values. Results are cached in-memory.\n *\n * @param sha - Git object SHA-1 hash (hex)\n * @param repo - Repository identifier (matches d tag)\n * @returns Arweave transaction ID, or null if not found\n */\nexport async function resolveGitSha(\n sha: string,\n repo: string\n): Promise<string | null> {\n // Validate SHA format to prevent injection of arbitrary strings into GraphQL\n if (!isValidGitSha(sha)) {\n return null;\n }\n\n const cacheKey = shaCacheKey(sha, repo);\n const cached = shaToTxIdCache.get(cacheKey);\n if (cached !== undefined) {\n return cached;\n }\n\n const safeSha = sanitizeGraphQLValue(sha);\n const safeRepo = sanitizeGraphQLValue(repo);\n const query = `query {\n transactions(tags: [\n { name: \"Git-SHA\", values: [\"${safeSha}\"] },\n { name: \"Repo\", values: [\"${safeRepo}\"] }\n ]) {\n edges { node { id } }\n }\n}`;\n\n try {\n const response = await fetch(ARWEAVE_GRAPHQL_URL, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ query }),\n signal: AbortSignal.timeout(ARWEAVE_FETCH_TIMEOUT_MS),\n });\n\n if (!response.ok) {\n return null;\n }\n\n const json = (await response.json()) as {\n data?: {\n transactions?: {\n edges?: { node?: { id?: string } }[];\n };\n };\n };\n\n const edges = json.data?.transactions?.edges;\n if (!edges || edges.length === 0) {\n return null;\n }\n\n const txId = edges[0]?.node?.id;\n if (!txId || !isValidArweaveTxId(txId)) {\n return null;\n }\n\n // Evict oldest entries if cache exceeds max size\n if (shaToTxIdCache.size >= SHA_CACHE_MAX_SIZE) {\n const firstKey = shaToTxIdCache.keys().next().value;\n if (firstKey !== undefined) {\n shaToTxIdCache.delete(firstKey);\n }\n }\n shaToTxIdCache.set(cacheKey, txId);\n return txId;\n } catch {\n return null;\n }\n}\n"],"mappings":"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","names":[]}