@toon-protocol/client-mcp 0.14.1 → 0.15.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/daemon/first-run.ts","../src/relay-subscription.ts","../src/daemon/client-runner.ts","../../../node_modules/.pnpm/@toon-protocol+core@1.4.2_@toon-protocol+connector@3.13.0_typescript@5.9.3/node_modules/@toon-protocol/core/src/nip34/constants.ts","../../../node_modules/.pnpm/@toon-protocol+core@1.4.2_@toon-protocol+connector@3.13.0_typescript@5.9.3/node_modules/@toon-protocol/core/src/nip34/types.ts","../../../node_modules/.pnpm/@toon-protocol+core@1.4.2_@toon-protocol+connector@3.13.0_typescript@5.9.3/node_modules/@toon-protocol/core/src/nip34/ForgejoClient.ts","../../../node_modules/.pnpm/@toon-protocol+core@1.4.2_@toon-protocol+connector@3.13.0_typescript@5.9.3/node_modules/@toon-protocol/core/src/nip34/NIP34Handler.ts","../../rig/src/objects.ts","../../../node_modules/.pnpm/@toon-protocol+core@2.0.1_typescript@5.9.3/node_modules/@toon-protocol/core/src/nip34/constants.ts","../../../node_modules/.pnpm/@toon-protocol+core@2.0.1_typescript@5.9.3/node_modules/@toon-protocol/core/src/nip34/types.ts","../../../node_modules/.pnpm/@toon-protocol+core@2.0.1_typescript@5.9.3/node_modules/@toon-protocol/core/src/nip34/ForgejoClient.ts","../../../node_modules/.pnpm/@toon-protocol+core@2.0.1_typescript@5.9.3/node_modules/@toon-protocol/core/src/nip34/NIP34Handler.ts","../../rig/src/nip34-events.ts","../../rig/src/remote-state.ts","../../arweave/src/gateways.ts","../../arweave/src/git-sha.ts","../../rig/src/repo-reader.ts","../../rig/src/object-fetch.ts","../../rig/src/read-pipeline.ts","../../rig/src/materialize.ts","../../rig/src/npub.ts","../../rig/src/routes.ts","../../rig/src/push.ts","../../../node_modules/.pnpm/@toon-protocol+sdk@0.5.1_@toon-protocol+connector@3.13.0_mina-signer@3.1.0_typescript@5.9.3/node_modules/@toon-protocol/sdk/dist/chunk-UP2VWCW5.js","../../../node_modules/.pnpm/@toon-protocol+sdk@0.5.1_@toon-protocol+connector@3.13.0_mina-signer@3.1.0_typescript@5.9.3/node_modules/@toon-protocol/sdk/src/errors.ts","../../../node_modules/.pnpm/@toon-protocol+sdk@0.5.1_@toon-protocol+connector@3.13.0_mina-signer@3.1.0_typescript@5.9.3/node_modules/@toon-protocol/sdk/src/identity.ts","../../../node_modules/.pnpm/@toon-protocol+sdk@0.5.1_@toon-protocol+connector@3.13.0_mina-signer@3.1.0_typescript@5.9.3/node_modules/@toon-protocol/sdk/src/gift-wrap.ts","../../../node_modules/.pnpm/@toon-protocol+sdk@0.5.1_@toon-protocol+connector@3.13.0_mina-signer@3.1.0_typescript@5.9.3/node_modules/@toon-protocol/sdk/src/swap-handler.ts","../../../node_modules/.pnpm/@toon-protocol+sdk@0.5.1_@toon-protocol+connector@3.13.0_mina-signer@3.1.0_typescript@5.9.3/node_modules/@toon-protocol/sdk/src/stream-swap.ts","../src/daemon/apex-channel-store.ts","../src/daemon/targets-store.ts","../src/daemon/apex-discovery.ts","../src/daemon/routes.ts"],"sourcesContent":["/**\n * First-run onboarding for `toon-clientd`.\n *\n * A brand-new user (`npx`/plugin install) has no identity and no config file.\n * Before the daemon resolves its config it calls {@link scaffoldFirstRun},\n * which makes a fresh install start with zero manual setup:\n *\n * 1. **Identity** — if no mnemonic source is configured (no\n * `TOON_CLIENT_MNEMONIC`, no `keystorePath`, no `mnemonic`), generate a\n * fresh BIP-39 mnemonic, encrypt it to `~/.toon-client/keystore.json`, and\n * record `keystorePath` (+ `keystoreAutoPassword`) in `config.json`. The\n * keystore is encrypted with `TOON_CLIENT_KEYSTORE_PASSWORD` when set, else\n * a default password so the identity survives restarts with no env var.\n * The mnemonic + derived addresses are printed ONCE for backup.\n * 2. **Transport scaffolding** — ensure `config.json` carries the transport\n * knobs (`btpUrl`/`relayUrl`) plus a `_help` block documenting them, so the\n * user can see what to point at.\n *\n * This is all idempotent: on later runs an identity already exists, so nothing\n * is regenerated and the config is left untouched.\n */\n\nimport { existsSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { deriveFullIdentity, generateKeystore } from '@toon-protocol/client';\nimport {\n configDir,\n defaultConfigPath,\n readConfigFile,\n DEFAULT_KEYSTORE_PASSWORD,\n type DaemonConfigFile,\n} from './config.js';\n\n/** Default keystore path: `~/.toon-client/keystore.json`. */\nexport function defaultKeystorePath(): string {\n return join(configDir(), 'keystore.json');\n}\n\n/**\n * True when SOME mnemonic source is already configured: the env var, a keystore\n * path, or an inline mnemonic. When false the daemon would otherwise hard-fail\n * with \"No mnemonic configured\".\n */\nexport function hasConfiguredIdentity(file: DaemonConfigFile): boolean {\n return Boolean(\n process.env['TOON_CLIENT_MNEMONIC']?.trim() ||\n file.keystorePath ||\n file.mnemonic\n );\n}\n\n/** The `_help` block written into a scaffolded config to document transport. */\nconst CONFIG_HELP = {\n transport:\n 'Configure ONE uplink for paid writes: either `proxyUrl` (connector ' +\n 'payment-proxy over ILP-over-HTTP, e.g. https://proxy.devnet.toonprotocol.dev) ' +\n 'OR `btpUrl` (BTP WebSocket, e.g. ws://<host>:3000/btp). Reads are always ' +\n 'free over relayUrl.',\n proxyUrl:\n 'Connector-proxy base URL (deployed devnet/testnet edge). When set, paid ' +\n 'writes route through `POST /ilp` and no BTP socket is needed. Set ' +\n '`destination` to the apex ILP address (e.g. g.proxy for devnet).',\n faucetUrl:\n 'Devnet faucet base URL (e.g. https://faucet.devnet.toonprotocol.dev) used ' +\n 'to drip test funds before publishing.',\n btpUrl:\n 'BTP WebSocket URL of the apex/connector for paid writes. Optional when ' +\n '`proxyUrl` is set.',\n relayUrl:\n 'Relay WebSocket URL for FREE reads. Default ws://localhost:7100.',\n destination:\n 'Default ILP publish destination (apex address). Default g.proxy.',\n keystorePath:\n 'Auto-generated encrypted identity. Do not hand-edit; back up your seed phrase.',\n};\n\ninterface ScaffoldOptions {\n /** Config file path (defaults to `TOON_CLIENT_CONFIG` / `~/.toon-client/config.json`). */\n configPath?: string;\n /** Log sink (defaults to stderr, since stdout may carry MCP/JSON). */\n log?: (msg: string) => void;\n}\n\n/**\n * Generate + persist an identity and/or scaffold transport config on first run.\n * Safe to call on every startup — it only acts when something is missing.\n */\nexport async function scaffoldFirstRun(\n opts: ScaffoldOptions = {}\n): Promise<void> {\n const log = opts.log ?? ((m: string): void => console.error(m));\n const configPath =\n opts.configPath ?? process.env['TOON_CLIENT_CONFIG'] ?? defaultConfigPath();\n\n // Ensure the config directory exists before writing the keystore/config.\n mkdirSync(dirname(configPath), { recursive: true });\n\n const file = readConfigFile(configPath);\n let updated: DaemonConfigFile = { ...file };\n let changed = false;\n\n // --- 1. Identity ----------------------------------------------------------\n if (!hasConfiguredIdentity(file)) {\n const keystorePath = defaultKeystorePath();\n const envPassword = process.env['TOON_CLIENT_KEYSTORE_PASSWORD'];\n const password = envPassword ?? DEFAULT_KEYSTORE_PASSWORD;\n\n // Reuse an existing keystore file if one is already on disk (e.g. config\n // was deleted but the keystore survived); otherwise mint a new mnemonic.\n let mnemonic: string;\n if (existsSync(keystorePath)) {\n // Leave the existing keystore untouched; just relink it in config.\n mnemonic = '';\n log(\n `[toon-clientd] first run: relinking existing keystore at ${keystorePath}`\n );\n } else {\n const generated = generateKeystore(keystorePath, password);\n mnemonic = generated.mnemonic;\n }\n\n updated = {\n ...updated,\n keystorePath,\n // Only flag auto-password when WE chose it, so a user-imported keystore\n // (custom password) still requires the env var.\n ...(envPassword ? {} : { keystoreAutoPassword: true }),\n };\n changed = true;\n\n if (mnemonic) {\n await printNewIdentity(mnemonic, keystorePath, Boolean(envPassword), log);\n }\n }\n\n // --- 2. Transport scaffolding --------------------------------------------\n if (!existsSync(configPath)) {\n // Fresh install: surface the transport knobs with guidance.\n updated = {\n _help: CONFIG_HELP,\n proxyUrl: '',\n btpUrl: '',\n relayUrl: 'ws://localhost:7100',\n ...updated,\n } as DaemonConfigFile;\n changed = true;\n log(\n `[toon-clientd] wrote starter config at ${configPath} — set \"proxyUrl\" (connector proxy, ILP-over-HTTP) or \"btpUrl\" (BTP) to your apex before publishing.`\n );\n }\n\n if (changed) writeConfigFile(configPath, updated);\n}\n\n/** Derive + print the new identity's addresses and a one-time backup notice. */\nasync function printNewIdentity(\n mnemonic: string,\n keystorePath: string,\n hasEnvPassword: boolean,\n log: (msg: string) => void\n): Promise<void> {\n const id = await deriveFullIdentity(mnemonic);\n const lines = [\n '',\n '════════════════════════════════════════════════════════════════',\n ' TOON client: generated a new identity (first run)',\n '════════════════════════════════════════════════════════════════',\n ` Nostr pubkey : ${id.nostr.pubkey}`,\n ` EVM address : ${id.evm.address}`,\n ...(id.solana.publicKey ? [` Solana : ${id.solana.publicKey}`] : []),\n ...(id.mina.publicKey ? [` Mina : ${id.mina.publicKey}`] : []),\n '',\n ' Seed phrase (BACK THIS UP — shown only once):',\n ` ${mnemonic}`,\n '',\n ` Encrypted keystore: ${keystorePath}`,\n hasEnvPassword\n ? ' Encrypted with TOON_CLIENT_KEYSTORE_PASSWORD.'\n : ' Encrypted with the default password (set TOON_CLIENT_KEYSTORE_PASSWORD\\n' +\n ' + re-import to use your own). Identity reloads automatically on restart.',\n '════════════════════════════════════════════════════════════════',\n '',\n ];\n log(lines.join('\\n'));\n}\n\n/** Write the config file as pretty JSON with mode 0o600. */\nfunction writeConfigFile(path: string, file: DaemonConfigFile): void {\n writeFileSync(path, JSON.stringify(file, null, 2) + '\\n', {\n encoding: 'utf8',\n mode: 0o600,\n });\n}\n","/**\n * Persistent relay Nostr-WS subscription — the read half of the TOON\n * client, which `@toon-protocol/client` does not provide (its bootstrap only\n * issues one-shot WS queries and `DiscoveryTracker` is passive).\n *\n * Reads are FREE: this opens a long-lived NIP-01 connection to the relay,\n * keeps a bounded ring buffer of received events (de-duplicated by `event.id`),\n * and lets callers drain new events via a monotonic cursor. It auto-reconnects\n * with exponential backoff and re-issues every active REQ on reconnect.\n *\n * The WebSocket is injectable (`wsFactory`) so unit tests can drive the wire\n * protocol without a real relay; the default factory uses the `ws` package.\n */\n\nimport { createRequire } from 'node:module';\nimport type { NostrEvent } from 'nostr-tools/pure';\nimport type { NostrFilter } from './control-api.js';\n\n// ESM has no `require`; synchronously load the node-only `ws` dep for the default\n// WebSocket factory without forcing every caller (e.g. the browser-style\n// injected-factory path in tests) to pull it in.\nconst nodeRequire = createRequire(import.meta.url);\n\n/** Minimal WebSocket surface this module depends on (subset of `ws`). */\nexport interface MinimalWebSocket {\n send(data: string): void;\n close(): void;\n on(event: 'open' | 'close', cb: () => void): void;\n on(event: 'message', cb: (data: unknown) => void): void;\n on(event: 'error', cb: (err: unknown) => void): void;\n}\n\nexport type WebSocketFactory = (url: string) => MinimalWebSocket;\n\nexport interface RelaySubscriptionOptions {\n /** Relay WS URL, e.g. `ws://localhost:7100`. */\n relayUrl: string;\n /** Max events retained in the ring buffer (oldest evicted). Default 5000. */\n bufferSize?: number;\n /** Base reconnect delay, ms. Default 1000. */\n reconnectBaseMs?: number;\n /** Max reconnect delay, ms. Default 30000. */\n reconnectMaxMs?: number;\n /** Inject a WebSocket factory (tests / proxy customisation). */\n wsFactory?: WebSocketFactory;\n /**\n * Decode an `EVENT` payload that arrived as a string. The TOON relay sends\n * events TOON-encoded (key/value text) rather than as a JSON object, so the\n * daemon injects a TOON decoder here. When the payload is already a JSON\n * object it is used directly and this is not called.\n */\n decodeEvent?: (raw: string) => NostrEvent;\n /**\n * Invoked once per newly-buffered (de-duplicated) event. The daemon uses this\n * to feed a runner-level MERGED buffer across many relays — so a fan-out read\n * (`toon_read` with no relayUrl) draws from one ordered stream with a single\n * scalar cursor. The relay still keeps its own buffer for `bufferedCount` /\n * single-relay drains.\n */\n onEvent?: (subId: string, event: NostrEvent) => void;\n /** Optional logger. */\n logger?: (msg: string) => void;\n}\n\ninterface BufferedEvent {\n seq: number;\n subId: string;\n event: NostrEvent;\n}\n\n/** Result of {@link RelaySubscription.getEvents}. */\nexport interface DrainResult {\n events: NostrEvent[];\n cursor: number;\n hasMore: boolean;\n}\n\nconst DEFAULT_BUFFER = 5000;\nconst DEFAULT_BASE_MS = 1000;\nconst DEFAULT_MAX_MS = 30_000;\n\n/** Shared no-op used as the default logger. */\nconst noop = (): void => undefined;\n\nexport class RelaySubscription {\n private readonly relayUrl: string;\n private readonly bufferSize: number;\n private readonly reconnectBaseMs: number;\n private readonly reconnectMaxMs: number;\n private readonly log: (msg: string) => void;\n private readonly wsFactory: WebSocketFactory;\n private readonly decodeEvent?: (raw: string) => NostrEvent;\n private readonly onEvent?: (subId: string, event: NostrEvent) => void;\n\n /** Active subscriptions: subId -> filters (re-sent on every (re)connect). */\n private readonly subscriptions = new Map<string, NostrFilter[]>();\n\n /** Ring buffer of received events, ordered by ascending `seq`. */\n private buffer: BufferedEvent[] = [];\n /** De-dup index: event.id -> seq (kept in lockstep with the buffer). */\n private readonly seen = new Set<string>();\n private seqCounter = 0;\n private subIdCounter = 0;\n\n private ws: MinimalWebSocket | null = null;\n private connected = false;\n private closing = false;\n private reconnectAttempts = 0;\n private reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n\n constructor(opts: RelaySubscriptionOptions) {\n this.relayUrl = opts.relayUrl;\n this.bufferSize = opts.bufferSize ?? DEFAULT_BUFFER;\n this.reconnectBaseMs = opts.reconnectBaseMs ?? DEFAULT_BASE_MS;\n this.reconnectMaxMs = opts.reconnectMaxMs ?? DEFAULT_MAX_MS;\n this.log = opts.logger ?? noop;\n this.wsFactory = opts.wsFactory ?? defaultWebSocketFactory();\n this.decodeEvent = opts.decodeEvent;\n this.onEvent = opts.onEvent;\n }\n\n /** Whether the underlying socket is currently open. */\n isConnected(): boolean {\n return this.connected;\n }\n\n /** Number of events currently held in the buffer. */\n bufferedCount(): number {\n return this.buffer.length;\n }\n\n /** Active subscription ids. */\n activeSubscriptions(): string[] {\n return [...this.subscriptions.keys()];\n }\n\n /** Open the connection (idempotent). */\n start(): void {\n this.closing = false;\n if (this.ws) return;\n this.open();\n }\n\n /**\n * Register a persistent subscription and (if connected) send the REQ.\n * Returns the subscription id (caller-supplied or generated).\n */\n subscribe(filters: NostrFilter | NostrFilter[], subId?: string): string {\n const id = subId ?? `sub-${++this.subIdCounter}`;\n const list = Array.isArray(filters) ? filters : [filters];\n this.subscriptions.set(id, list);\n if (this.connected) this.sendReq(id, list);\n return id;\n }\n\n /** Cancel a subscription and send CLOSE if connected. */\n unsubscribe(subId: string): void {\n if (!this.subscriptions.delete(subId)) return;\n if (this.connected) this.sendRaw(['CLOSE', subId]);\n }\n\n /**\n * Drain events newer than `cursor`. The cursor is the highest `seq` returned;\n * pass it back to fetch only events received since. Filtering by `subId`\n * restricts to one subscription.\n */\n getEvents(\n opts: { subId?: string; cursor?: number; limit?: number } = {}\n ): DrainResult {\n const after = opts.cursor ?? 0;\n const limit = opts.limit ?? 200;\n const matches = this.buffer.filter(\n (b) =>\n b.seq > after && (opts.subId === undefined || b.subId === opts.subId)\n );\n const page = matches.slice(0, limit);\n const hasMore = matches.length > page.length;\n const last = page.at(-1);\n const cursor = last ? last.seq : after;\n return { events: page.map((b) => b.event), cursor, hasMore };\n }\n\n /** Close the connection permanently and stop reconnecting. */\n close(): void {\n this.closing = true;\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n if (this.ws) {\n try {\n this.ws.close();\n } catch {\n /* ignore */\n }\n this.ws = null;\n }\n this.connected = false;\n }\n\n // ── internals ────────────────────────────────────────────────────────────\n\n private open(): void {\n let ws: MinimalWebSocket;\n try {\n ws = this.wsFactory(this.relayUrl);\n } catch (err) {\n this.log(`[relay] connect failed: ${errMsg(err)}`);\n this.scheduleReconnect();\n return;\n }\n this.ws = ws;\n\n ws.on('open', () => {\n this.connected = true;\n this.reconnectAttempts = 0;\n this.log(`[relay] connected to ${this.relayUrl}`);\n // Re-issue every active subscription.\n for (const [id, filters] of this.subscriptions) this.sendReq(id, filters);\n });\n\n ws.on('message', (data: unknown) => this.handleMessage(data));\n\n ws.on('error', (err: unknown) => {\n this.log(`[relay] socket error: ${errMsg(err)}`);\n });\n\n ws.on('close', () => {\n this.connected = false;\n this.ws = null;\n if (!this.closing) {\n this.log('[relay] disconnected; scheduling reconnect');\n this.scheduleReconnect();\n }\n });\n }\n\n private scheduleReconnect(): void {\n if (this.closing || this.reconnectTimer) return;\n const delay = Math.min(\n this.reconnectMaxMs,\n this.reconnectBaseMs * 2 ** this.reconnectAttempts\n );\n this.reconnectAttempts += 1;\n this.reconnectTimer = setTimeout(() => {\n this.reconnectTimer = null;\n if (!this.closing) this.open();\n }, delay);\n // Don't keep the event loop alive solely for a reconnect timer.\n (this.reconnectTimer as { unref?: () => void }).unref?.();\n }\n\n private sendReq(subId: string, filters: NostrFilter[]): void {\n this.sendRaw(['REQ', subId, ...filters]);\n }\n\n private sendRaw(message: unknown[]): void {\n if (!this.ws || !this.connected) return;\n try {\n this.ws.send(JSON.stringify(message));\n } catch (err) {\n this.log(`[relay] send failed: ${errMsg(err)}`);\n }\n }\n\n private handleMessage(data: unknown): void {\n let parsed: unknown;\n try {\n parsed = JSON.parse(toText(data));\n } catch {\n return;\n }\n if (!Array.isArray(parsed) || parsed.length === 0) return;\n const type = parsed[0];\n switch (type) {\n case 'EVENT': {\n const subId = typeof parsed[1] === 'string' ? parsed[1] : '';\n const event = this.parseEventPayload(parsed[2]);\n if (event && typeof event.id === 'string')\n this.bufferEvent(subId, event);\n break;\n }\n case 'EOSE':\n // End of stored events — nothing to do; we keep streaming live events.\n break;\n case 'CLOSED':\n this.log(\n `[relay] subscription closed by relay: ${String(parsed[2] ?? '')}`\n );\n break;\n case 'NOTICE':\n this.log(`[relay] NOTICE: ${String(parsed[1] ?? '')}`);\n break;\n default:\n break;\n }\n }\n\n /**\n * Normalise an `EVENT` payload to a NostrEvent. Standard relays send a JSON\n * object; the TOON relay sends a TOON-encoded string, decoded via the injected\n * `decodeEvent`. Returns undefined when it can't be parsed.\n */\n private parseEventPayload(raw: unknown): NostrEvent | undefined {\n if (\n raw &&\n typeof raw === 'object' &&\n typeof (raw as NostrEvent).id === 'string'\n ) {\n return raw as NostrEvent;\n }\n if (typeof raw === 'string' && this.decodeEvent) {\n try {\n return this.decodeEvent(raw);\n } catch (err) {\n this.log(`[relay] event decode failed: ${errMsg(err)}`);\n return undefined;\n }\n }\n return undefined;\n }\n\n private bufferEvent(subId: string, event: NostrEvent): void {\n if (this.seen.has(event.id)) return; // de-dup by event.id\n this.seen.add(event.id);\n this.buffer.push({ seq: ++this.seqCounter, subId, event });\n if (this.buffer.length > this.bufferSize) {\n const evicted = this.buffer.shift();\n if (evicted) this.seen.delete(evicted.event.id);\n }\n // Mirror into the runner-level merged buffer (cross-relay fan-out reads).\n this.onEvent?.(subId, event);\n }\n}\n\nfunction toText(data: unknown): string {\n if (typeof data === 'string') return data;\n if (data instanceof Uint8Array) return Buffer.from(data).toString('utf8');\n if (Buffer.isBuffer(data)) return data.toString('utf8');\n return String(data);\n}\n\nfunction errMsg(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n/**\n * Default factory backed by the `ws` package.\n */\nfunction defaultWebSocketFactory(): WebSocketFactory {\n return (url: string): MinimalWebSocket => {\n const WebSocketImpl = nodeRequire('ws') as new (\n address: string\n ) => MinimalWebSocket;\n return new WebSocketImpl(url);\n };\n}\n","/**\n * ClientRunner — the daemon's connection owner. The TOON client is 1-to-MANY:\n * it can write through several apexes (each a `ToonClient` + BTP session +\n * payment channel) and read from several relays (each a `RelaySubscription`).\n *\n * • Writes go through BTP, never to a relay directly — `publish`/`swap` select\n * an apex (default: the config-seeded one).\n * • Reads FAN OUT — `subscribe`/`getEvents` apply across every relay and merge\n * into one ordered stream with a single scalar cursor (the runner owns the\n * merged buffer; each `RelaySubscription` mirrors new events into it).\n *\n * Targets are added at runtime (`addRelay`/`addApex`), persisted to\n * `targets.json`, replayed on the next boot, and removable. The config-seeded\n * relay + apex are the permanent DEFAULT targets and cannot be removed.\n *\n * Each apex bootstraps asynchronously and non-blocking: the connection comes up\n * in the background. Until ready, writes against it report `bootstrapping` so\n * tools surface \"retry\".\n */\n\nimport { readFile, stat } from 'node:fs/promises';\nimport { resolve, sep } from 'node:path';\nimport type { NostrEvent, EventTemplate } from 'nostr-tools/pure';\nimport { generateSecretKey } from 'nostr-tools/pure';\nimport { decodeEventFromToon } from '@toon-protocol/core';\nimport {\n STATUS_APPLIED_KIND,\n STATUS_CLOSED_KIND,\n STATUS_DRAFT_KIND,\n STATUS_OPEN_KIND,\n REPOSITORY_ANNOUNCEMENT_KIND,\n} from '@toon-protocol/core/nip34';\nimport { arweaveUrls } from '@toon-protocol/arweave';\nimport type { ToonClientConfig } from '@toon-protocol/client';\nimport {\n extractArweaveTxId,\n fundWallet as faucetFund,\n type FaucetChain,\n} from '@toon-protocol/client';\nimport {\n GitRepoReader,\n buildComment,\n buildIssue,\n buildPatch,\n buildStatus,\n executePush,\n fetchRemoteState,\n planPush,\n type Publisher,\n type PublishReceipt,\n type PushPlan,\n type PushResult,\n type RemoteState,\n type StatusKind,\n type UnsignedEvent,\n type UploadReceipt,\n type GitObjectUpload,\n} from '@toon-protocol/rig';\nimport { streamSwap } from '@toon-protocol/sdk/swap';\nimport { RelaySubscription } from '../relay-subscription.js';\nimport type {\n AddApexRequest,\n AddApexResponse,\n ApexTargetStatus,\n BalanceInfo,\n BalancesResponse,\n ChannelDepositRequest,\n ChannelDepositResponse,\n CloseChannelRequest,\n CloseChannelResponse,\n SettleChannelRequest,\n SettleChannelResponse,\n ChannelsResponse,\n ChainStatus,\n EventsResponse,\n FundStatusResponse,\n FundWalletRequest,\n FundWalletResponse,\n GitCommentRequest,\n GitEstimateRequest,\n GitEstimateResponse,\n GitEventResponse,\n GitFeeEstimate,\n GitIssueRequest,\n GitPatchRequest,\n GitPushRequest,\n GitPushResponse,\n GitRepoAddr,\n GitStatusRequest,\n HttpFetchPaidRequest,\n HttpFetchPaidResponse,\n NostrFilter,\n PublishResponse,\n PublishUnsignedRequest,\n RelayTargetStatus,\n SettlementChain,\n StatusResponse,\n SubscribeRequest,\n SubscribeResponse,\n SwapResponse,\n TargetsResponse,\n UploadMediaRequest,\n UploadMediaResponse,\n} from '../control-api.js';\nimport type {\n EventsQuery,\n PublishRequest,\n SwapRequest,\n} from '../control-api.js';\nimport {\n configDir,\n type ApexNegotiationConfig,\n type ResolvedDaemonConfig,\n} from './config.js';\nimport {\n loadApexChannel,\n saveApexChannel,\n type PersistedChannelContext,\n} from './apex-channel-store.js';\nimport {\n loadTargets,\n removeApexTarget,\n removeRelayTarget,\n saveApexTarget,\n saveRelayTarget,\n type PersistedApexTarget,\n} from './targets-store.js';\nimport { discoverApex } from './apex-discovery.js';\n\n/** The subset of `ToonClient` the runner depends on. */\nexport interface ToonClientLike {\n start(): Promise<{ peersDiscovered: number; mode: string }>;\n stop(): Promise<void>;\n getPublicKey(): string;\n getEvmAddress(): string | undefined;\n getSolanaAddress(): string | undefined;\n getMinaAddress(): string | undefined;\n getNetworkStatus(): { evm: string; solana: string; mina: string } | undefined;\n publishEvent(\n event: NostrEvent,\n options?: {\n destination?: string;\n claim?: unknown;\n ilpAmount?: bigint;\n /** HTTP request-target the payment-proxy replays (default '/write';\n * '/store' routes to the Arweave store/DVM backend). */\n proxyPath?: string;\n }\n ): Promise<{\n success: boolean;\n eventId?: string;\n data?: string;\n error?: string;\n }>;\n signBalanceProof(channelId: string, amount: bigint): Promise<unknown>;\n /**\n * Sign an unsigned event template with the daemon-held Nostr key (the key\n * never leaves the daemon). Backs the `publish-unsigned` / `upload-media`\n * paths so a UI/agent supplies only the event shell.\n */\n signEvent(template: EventTemplate): NostrEvent | Promise<NostrEvent>;\n /**\n * Upload bytes to Arweave via the kind:5094 blob-storage DVM (single-packet),\n * returning the Arweave tx id. Reuses the client's claim/channel plumbing.\n */\n uploadBlob(params: {\n blobData: Uint8Array;\n contentType?: string;\n bid?: string;\n destination?: string;\n ilpAmount?: bigint;\n }): Promise<{ success: boolean; txId?: string; eventId?: string; error?: string }>;\n openChannel(destination?: string): Promise<string>;\n getTrackedChannels(): string[];\n getChannelNonce(channelId: string): number;\n getChannelCumulativeAmount(channelId: string): bigint;\n getChannelDepositTotal(channelId: string): bigint;\n getBalances(): Promise<BalanceInfo[]>;\n depositToChannel(\n channelId: string,\n amount: string\n ): Promise<{ channelId: string; txHash?: string; depositTotal: string }>;\n closeChannel(\n channelId: string\n ): Promise<{ channelId: string; txHash?: string; closedAt: string; settleableAt: string }>;\n settleChannel(channelId: string): Promise<{ channelId: string; txHash?: string }>;\n getChannelCloseState(channelId: string): 'open' | 'closing' | 'settleable' | 'settled';\n getSettleableAt(channelId: string): bigint | undefined;\n /**\n * Re-read a resumed channel's on-chain deposit (persisted state omits it).\n * Optional so lightweight fakes need not implement it; the real ToonClient\n * does. Best-effort — callers await + catch.\n */\n rehydrateChannelDeposit?(\n channelId: string,\n opts: { chain: string; tokenNetworkAddress: string }\n ): Promise<bigint | undefined>;\n sendSwapPacket(params: {\n destination: string;\n amount: bigint;\n toonData: Uint8Array;\n claim?: unknown;\n }): Promise<{\n accepted: boolean;\n data?: string;\n code?: string;\n message?: string;\n }>;\n /**\n * Payment-aware HTTP fetch: issue the request and, on a `402 Payment\n * Required`, transparently pay over TOON and retry, returning the settled Web\n * `Response`. Pinned to the `ToonClient.h402Fetch` shape (issue #50).\n */\n h402Fetch(\n url: string,\n opts?: {\n method?: string;\n headers?: Record<string, string>;\n body?: string | Uint8Array;\n timeout?: number;\n destination?: string;\n }\n ): Promise<Response>;\n}\n\n/** A started managed proxy: just the teardown handle the runner needs. */\n/** Builds a `ToonClient` (or a fake) for a given resolved client config. */\nexport type CreateClient = (config: ToonClientConfig) => ToonClientLike;\n\n/** Builds a `RelaySubscription` for a given relay URL. */\nexport type CreateRelay = (opts: {\n relayUrl: string;\n onEvent: (subId: string, event: NostrEvent) => void;\n logger?: (msg: string) => void;\n}) => RelaySubscription;\n\nexport interface ClientRunnerDeps {\n config: ResolvedDaemonConfig;\n /** Factory producing the (real or fake) ToonClient for a client config. */\n createClient: CreateClient;\n /** Factory producing a relay subscription (defaults to the real one). */\n createRelay?: CreateRelay;\n logger?: (msg: string) => void;\n /** Path to the dynamic-targets store (tests override). */\n targetsPath?: string;\n /**\n * Test seams for the `/git/*` pipeline (default: the real\n * @toon-protocol/rig implementations). `fetchRemoteState` opens relay\n * WebSockets, so tests inject a canned reader instead of hitting the network.\n */\n gitDeps?: {\n fetchRemoteState?: typeof fetchRemoteState;\n createRepoReader?: (repoPath: string) => GitRepoReader;\n };\n}\n\n/** One apex write target: a BTP session + its payment channel + settlement. */\ninterface ApexConnection {\n btpUrl: string;\n client: ToonClientLike;\n negotiation?: ApexNegotiationConfig;\n childPeers: string[];\n destination: string;\n chain: SettlementChain;\n /** Per-apex channel-store path (distinct so parallel apexes don't race it). */\n channelStorePath: string;\n feePerEvent: bigint;\n apexChannelId?: string;\n ready: boolean;\n bootstrapping: boolean;\n /** In-flight bootstrap, so concurrent callers await the same work (not re-run). */\n bootstrapPromise?: Promise<void>;\n lastError?: string;\n isDefault: boolean;\n}\n\n/** A runner-level merged read-buffer entry, tagged with its source relay. */\ninterface MergedEvent {\n seq: number;\n relayUrl: string;\n subId: string;\n event: NostrEvent;\n}\n\nconst MERGED_BUFFER = 5000;\n\n/**\n * Per-attempt bound for an on-chain balance read, kept WELL under the control\n * client's `/balances` timeout (12s) so a stalled provider fast-fails inside the\n * daemon instead of letting the whole control request hang to the wire timeout\n * (#199). With {@link BALANCES_READ_ATTEMPTS} the worst case stays under 12s.\n */\nconst BALANCES_READ_TIMEOUT_MS = 5_000;\n/** Bounded retry for a transient provider stall on a balance read (#199). */\nconst BALANCES_READ_ATTEMPTS = 2;\n\n/**\n * In-memory record of one background faucet drip. Structurally identical to the\n * wire {@link FundWalletResponse} snapshot the daemon returns, so a job can be\n * handed back verbatim.\n */\ntype FundJob = FundWalletResponse;\n\nexport class ClientRunner {\n private readonly config: ResolvedDaemonConfig;\n private readonly createClient: CreateClient;\n private readonly createRelay: CreateRelay;\n private readonly log: (msg: string) => void;\n private readonly targetsPath?: string;\n\n /** Remote-state reader for `/git/*` (injectable — opens relay sockets). */\n private readonly fetchGitRemoteState: typeof fetchRemoteState;\n /** Local-repo reader factory for `/git/*` (injectable for tests). */\n private readonly createRepoReader: (repoPath: string) => GitRepoReader;\n\n /**\n * Identity-level chain-read client. Reading your OWN on-chain wallet balance is\n * a pure (wallet keys + chain RPC) operation that has nothing to do with the\n * ILP/payment peer, so it lives at the daemon level rather than inside an apex.\n * Built once from the daemon's own `toonClientConfig` (the same keys + chain\n * RPC config every apex shares) and REUSED as the default apex's client, so a\n * funded apex's `start()` (which derives Solana/Mina keys) also benefits this\n * reader. `getBalances` uses it directly, so balances work even with zero\n * apexes registered (follow-up to #199/#200).\n */\n private readonly identityClient: ToonClientLike;\n\n private readonly startedAt = Date.now();\n\n /** Apex write targets, keyed by btpUrl. */\n private readonly apexes = new Map<string, ApexConnection>();\n /** Relay read targets, keyed by relayUrl. */\n private readonly relays = new Map<string, RelaySubscription>();\n\n /**\n * Async faucet drip jobs, keyed by chain. A drip is launched in the background\n * (the Mina faucet legitimately takes ~75s — longer than the MCP host's ~60s\n * tool-call budget) and its terminal state is observed via {@link getFundStatus}\n * / re-reading balances rather than by blocking the caller.\n */\n private readonly fundJobs = new Map<FaucetChain, FundJob>();\n\n /** Runner-level merged read buffer across all relays (de-duped by event.id). */\n private merged: MergedEvent[] = [];\n private readonly mergedSeen = new Set<string>();\n private mergedSeq = 0;\n\n /**\n * Fan-out subscriptions (no relayUrl restriction): replayed onto relays added\n * later so a new relay immediately participates in existing reads.\n */\n private readonly fanoutSubs = new Map<string, NostrFilter[]>();\n private subIdCounter = 0;\n\n private readonly defaultBtpUrl: string;\n private readonly defaultRelayUrl: string;\n\n private stopped = false;\n private started = false;\n\n constructor(deps: ClientRunnerDeps) {\n this.config = deps.config;\n this.createClient = deps.createClient;\n this.log = deps.logger ?? ((): void => undefined);\n if (deps.targetsPath !== undefined) this.targetsPath = deps.targetsPath;\n this.fetchGitRemoteState = deps.gitDeps?.fetchRemoteState ?? fetchRemoteState;\n this.createRepoReader =\n deps.gitDeps?.createRepoReader ?? ((repoPath) => new GitRepoReader(repoPath));\n this.defaultBtpUrl = deps.config.toonClientConfig.btpUrl ?? '';\n this.defaultRelayUrl = deps.config.relayUrl;\n\n this.createRelay =\n deps.createRelay ??\n ((opts) =>\n new RelaySubscription({\n relayUrl: opts.relayUrl,\n ...(opts.logger ? { logger: opts.logger } : {}),\n onEvent: opts.onEvent,\n // The TOON relay sends events TOON-encoded (text) on reads, not JSON.\n decodeEvent: (raw) =>\n decodeEventFromToon(new TextEncoder().encode(raw)),\n }));\n\n // Build the permanent config-seeded default relay + apex up front (not yet\n // started/bootstrapped) so `bootstrap()` works standalone (the daemon and\n // tests both rely on constructing then awaiting bootstrap()).\n this.registerRelay(this.defaultRelayUrl);\n // Build the identity-level read client ONCE and reuse it as the default\n // apex's client (same keys + chain RPC config), so on-chain balance reads\n // never depend on an apex existing.\n this.identityClient = this.createClient(this.config.toonClientConfig);\n const defaultApex = this.makeApex({\n btpUrl: this.defaultBtpUrl,\n client: this.identityClient,\n ...(this.config.apex ? { negotiation: this.config.apex } : {}),\n childPeers: this.config.apexChildPeers ?? [],\n destination: this.config.destination,\n chain: this.config.chain,\n channelStorePath:\n this.config.toonClientConfig.channelStorePath ??\n this.apexChannelStorePathFor(this.defaultBtpUrl),\n feePerEvent: this.config.feePerEvent,\n isDefault: true,\n });\n this.apexes.set(defaultApex.btpUrl, defaultApex);\n }\n\n /**\n * Start the live connections: the shared read proxy, every relay socket, the\n * default apex bootstrap (non-blocking), then replay persisted dynamic\n * targets. Returns immediately; apexes become ready asynchronously.\n */\n start(): void {\n if (this.started) return;\n this.started = true;\n for (const relay of this.relays.values()) relay.start();\n void this.bootstrap();\n this.replayPersistedTargets();\n }\n\n /** Await the default apex's bootstrap (kicking it off if not already running). */\n bootstrap(): Promise<void> {\n // Read-only daemon (no proxy/BTP uplink): never bootstrap an apex — there is\n // no write transport and FREE reads run off the relay subscription (#69).\n if (!this.config.hasUplink) return Promise.resolve();\n const apex = this.apexes.get(this.defaultBtpUrl);\n if (!apex) return Promise.resolve();\n return this.bootstrapApex(apex);\n }\n\n // ── Relays (reads) ─────────────────────────────────────────────────────────\n\n /**\n * Build + register a relay (idempotent by URL), wiring its events into the\n * merged buffer and replaying active fan-out subscriptions. Does NOT start the\n * socket — callers start it (so construction stays side-effect-free for tests).\n */\n private registerRelay(relayUrl: string): RelaySubscription {\n const existing = this.relays.get(relayUrl);\n if (existing) return existing;\n const relay = this.createRelay({\n relayUrl,\n logger: this.log,\n onEvent: (subId, event) => this.pushMerged(relayUrl, subId, event),\n });\n this.relays.set(relayUrl, relay);\n // A new relay joins every active fan-out subscription.\n for (const [subId, filters] of this.fanoutSubs)\n relay.subscribe(filters, subId);\n return relay;\n }\n\n /**\n * Add a relay read target at runtime. Persisted unless `persist` is false.\n */\n async addRelay(relayUrl: string, persist = true): Promise<void> {\n if (this.relays.has(relayUrl)) return;\n const relay = this.registerRelay(relayUrl);\n relay.start();\n if (persist) saveRelayTarget(relayUrl, this.targetsPath);\n }\n\n /** Remove a relay read target. The config-seeded default cannot be removed. */\n removeRelay(relayUrl: string): void {\n if (relayUrl === this.defaultRelayUrl) {\n throw new TargetError('Cannot remove the default (config-seeded) relay.');\n }\n const relay = this.relays.get(relayUrl);\n if (!relay) throw new TargetError(`No such relay: ${relayUrl}`);\n relay.close();\n this.relays.delete(relayUrl);\n // Drop its events from the merged buffer (and dedup index).\n this.merged = this.merged.filter((m) => {\n if (m.relayUrl === relayUrl) {\n this.mergedSeen.delete(m.event.id);\n return false;\n }\n return true;\n });\n removeRelayTarget(relayUrl, this.targetsPath);\n }\n\n /** Mirror a newly-buffered relay event into the merged cross-relay buffer. */\n private pushMerged(relayUrl: string, subId: string, event: NostrEvent): void {\n if (this.mergedSeen.has(event.id)) return;\n this.mergedSeen.add(event.id);\n this.merged.push({ seq: ++this.mergedSeq, relayUrl, subId, event });\n if (this.merged.length > MERGED_BUFFER) {\n const evicted = this.merged.shift();\n if (evicted) this.mergedSeen.delete(evicted.event.id);\n }\n }\n\n /**\n * Register a free-read subscription. With no `relayUrl` it FANS OUT across\n * every relay (and onto relays added later); with one it targets that relay.\n */\n subscribe(req: SubscribeRequest): SubscribeResponse {\n const subId = req.subId ?? `sub-${++this.subIdCounter}`;\n const filters = Array.isArray(req.filters) ? req.filters : [req.filters];\n const targets = req.relayUrl ? [req.relayUrl] : [...this.relays.keys()];\n if (req.relayUrl && !this.relays.has(req.relayUrl)) {\n throw new TargetError(`No such relay: ${req.relayUrl}`);\n }\n if (!req.relayUrl) this.fanoutSubs.set(subId, filters);\n for (const url of targets) this.relays.get(url)?.subscribe(filters, subId);\n return { subId, relays: targets };\n }\n\n /**\n * One-shot free read: subscribe the given filter(s) across all relays, wait a\n * bounded window for the relay(s) to deliver, then return every buffered event\n * matching the filter (matched by content, not subId — so events already\n * buffered by other subscriptions are included despite the global dedup).\n *\n * Backs the apps `toon_query` tool the generative-UI runtime calls to resolve\n * a ViewSpec node's data bind.\n */\n async query(\n filters: NostrFilter | NostrFilter[],\n timeoutMs = 1200\n ): Promise<NostrEvent[]> {\n const list = Array.isArray(filters) ? filters : [filters];\n const subId = `q-${++this.subIdCounter}`;\n const targets = [...this.relays.keys()];\n for (const url of targets) this.relays.get(url)?.subscribe(list, subId);\n await delay(timeoutMs);\n for (const url of targets) this.relays.get(url)?.unsubscribe(subId);\n return this.merged\n .map((m) => m.event)\n .filter((event) => list.some((f) => matchesFilter(event, f)));\n }\n\n /** Drain merged events newer than the cursor (free read), optionally scoped. */\n getEvents(query: EventsQuery): EventsResponse {\n const after = query.cursor ?? 0;\n const limit = query.limit ?? 200;\n const matches = this.merged.filter(\n (m) =>\n m.seq > after &&\n (query.subId === undefined || m.subId === query.subId) &&\n (query.relayUrl === undefined || m.relayUrl === query.relayUrl)\n );\n const page = matches.slice(0, limit);\n const hasMore = matches.length > page.length;\n const last = page.at(-1);\n return {\n events: page.map((m) => m.event),\n cursor: last ? last.seq : after,\n hasMore,\n };\n }\n\n // ── Apexes (writes) ──────────────────────────────────────────────────────\n\n private makeApex(init: {\n btpUrl: string;\n client: ToonClientLike;\n negotiation?: ApexNegotiationConfig;\n childPeers: string[];\n destination: string;\n chain: SettlementChain;\n channelStorePath: string;\n feePerEvent: bigint;\n isDefault: boolean;\n }): ApexConnection {\n return {\n ...init,\n ready: false,\n bootstrapping: false,\n };\n }\n\n /**\n * Bootstrap one apex (memoized): start, inject negotiation, open/resume the\n * channel, route child peers. Concurrent callers await the same in-flight\n * work rather than re-running it.\n */\n private bootstrapApex(apex: ApexConnection): Promise<void> {\n if (apex.ready) return Promise.resolve();\n if (!apex.bootstrapPromise) {\n apex.bootstrapPromise = this.doBootstrapApex(apex);\n }\n return apex.bootstrapPromise;\n }\n\n private async doBootstrapApex(apex: ApexConnection): Promise<void> {\n apex.bootstrapping = true;\n try {\n // PROXY mode (no BTP discovery): if no negotiation was supplied via config,\n // discover the apex's settlement params from its kind:10032 on the default\n // relay before opening the channel (#69). Config-supplied negotiation wins.\n // In BTP mode the legacy bootstrap path handles discovery, so skip this.\n if (!apex.negotiation && this.config.proxyUrl) {\n await this.discoverApexNegotiation(apex);\n }\n await apex.client.start();\n this.injectApexNegotiation(apex);\n // PROXY mode: resume a previously-opened channel up front, else DEFER the\n // on-chain open to the first write / `POST /channels` so the wallet can be\n // funded AFTER the daemon starts (the fund→open→publish demo flow, #69).\n // The apex is \"ready\" once negotiation is in place — `openChannel` /\n // `publish` open lazily and idempotently via the ChannelManager.\n // BTP mode keeps the historical eager open at bootstrap.\n const deferOpen = Boolean(this.config.proxyUrl);\n apex.apexChannelId = await this.openOrResumeApexChannel(apex, {\n resumeOnly: deferOpen,\n });\n this.routeChildPeersThroughApexChannel(apex);\n apex.ready = true;\n apex.lastError = undefined;\n this.log(\n `[runner] apex ${apex.btpUrl || apex.destination} ready; channel ${\n apex.apexChannelId ?? '(deferred — open on first write)'\n }`\n );\n } catch (err) {\n apex.lastError = err instanceof Error ? err.message : String(err);\n this.log(\n `[runner] apex ${apex.btpUrl} bootstrap failed: ${apex.lastError}`\n );\n } finally {\n apex.bootstrapping = false;\n }\n }\n\n /**\n * Add an apex write target. Settlement params are discovered by reading the\n * apex's kind:10032 off the given relay (added first if unknown). Persisted.\n */\n async addApex(req: AddApexRequest): Promise<AddApexResponse> {\n await this.addRelay(req.relayUrl); // ensure + persist the discovery relay\n const relay = this.relays.get(req.relayUrl);\n if (!relay) throw new TargetError(`Relay unavailable: ${req.relayUrl}`);\n\n const discovered = await discoverApex({\n relay,\n ilpAddress: req.ilpAddress,\n ...(req.pubkey ? { pubkey: req.pubkey } : {}),\n ...(req.chain ? { chain: req.chain } : {}),\n ...(req.childPeers ? { childPeers: req.childPeers } : {}),\n });\n\n const feePerEvent =\n req.feePerEvent !== undefined\n ? BigInt(req.feePerEvent)\n : this.config.feePerEvent;\n\n await this.instantiateApex(\n {\n btpUrl: discovered.btpUrl,\n negotiation: discovered.negotiation,\n ...(discovered.apexChildPeers\n ? { apexChildPeers: discovered.apexChildPeers }\n : {}),\n feePerEvent: req.feePerEvent ?? feePerEvent.toString(),\n discoveredFrom: req.relayUrl,\n },\n true\n );\n\n const apex = this.apexes.get(discovered.btpUrl);\n if (!apex) {\n throw new TargetError(\n `Apex ${discovered.btpUrl} failed to register after discovery.`\n );\n }\n return {\n btpUrl: apex.btpUrl,\n destination: apex.destination,\n chain: apex.chain,\n ready: apex.ready,\n };\n }\n\n /** Build + register + bootstrap an apex from a (persisted) target record. */\n private async instantiateApex(\n target: PersistedApexTarget,\n persist: boolean\n ): Promise<void> {\n if (this.apexes.has(target.btpUrl)) return;\n const clientConfig = this.deriveApexClientConfig(\n target.btpUrl,\n target.negotiation.destination\n );\n const apex = this.makeApex({\n btpUrl: target.btpUrl,\n client: this.createClient(clientConfig),\n negotiation: target.negotiation,\n childPeers: target.apexChildPeers ?? [],\n destination: target.negotiation.destination,\n chain: target.negotiation.chain,\n channelStorePath: this.apexChannelStorePathFor(target.btpUrl),\n feePerEvent: BigInt(target.feePerEvent ?? this.config.feePerEvent),\n isDefault: false,\n });\n this.apexes.set(apex.btpUrl, apex);\n if (persist) saveApexTarget(target, this.targetsPath);\n await this.bootstrapApex(apex);\n }\n\n /** Remove an apex write target. The config-seeded default cannot be removed. */\n async removeApex(btpUrl: string): Promise<void> {\n if (btpUrl === this.defaultBtpUrl) {\n throw new TargetError('Cannot remove the default (config-seeded) apex.');\n }\n const apex = this.apexes.get(btpUrl);\n if (!apex) throw new TargetError(`No such apex: ${btpUrl}`);\n try {\n await apex.client.stop();\n } catch (err) {\n this.log(\n `[runner] apex ${btpUrl} stop error: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n this.apexes.delete(btpUrl);\n removeApexTarget(btpUrl, this.targetsPath);\n }\n\n /** Derive a per-apex ToonClientConfig from the default (shared identity/transport). */\n private deriveApexClientConfig(\n btpUrl: string,\n destination: string\n ): ToonClientConfig {\n const base = this.config.toonClientConfig;\n // A DISCOVERED apex (e.g. the store DVM at `wss://proxy.store…:443`) lives on\n // a different connector than the default `proxyUrl`. In direct/HTTP transport\n // every paid packet POSTs to `proxyUrl`, so without a per-apex override the\n // discovered apex's packets go to the DEFAULT connector — which has no route\n // to its ILP prefix (F02 \"No route to destination\"). Derive the apex's own\n // HTTP `/ilp` base from its BTP url so its packets reach the right connector.\n const derivedProxyUrl = btpUrl\n .replace(/^wss:\\/\\//, 'https://')\n .replace(/^ws:\\/\\//, 'http://')\n .replace(/:443(\\/|$)/, '$1')\n .replace(/\\/btp\\/?$/, '')\n .replace(/\\/$/, '');\n return {\n ...base,\n ...(derivedProxyUrl ? { proxyUrl: derivedProxyUrl } : {}),\n btpUrl,\n destinationAddress: destination,\n // Distinct nonce-watermark store per apex so parallel ChannelManagers in\n // this process never race a shared channels.json.\n channelStorePath: this.apexChannelStorePathFor(btpUrl),\n ilpInfo: { ...base.ilpInfo, btpEndpoint: btpUrl },\n };\n }\n\n private apexChannelStorePathFor(btpUrl: string): string {\n return `${configDir()}/channels-${sanitize(btpUrl)}.json`;\n }\n\n // ── Persisted-target replay ────────────────────────────────────────────────\n\n private replayPersistedTargets(): void {\n let store;\n try {\n store = loadTargets(this.targetsPath);\n } catch (err) {\n this.log(\n `[runner] failed to load targets store: ${err instanceof Error ? err.message : String(err)}`\n );\n return;\n }\n for (const r of store.relays) {\n if (r.relayUrl === this.defaultRelayUrl) continue;\n void this.addRelay(r.relayUrl, false).catch((err) =>\n this.log(`[runner] replay relay ${r.relayUrl} failed: ${errMsg(err)}`)\n );\n }\n for (const a of store.apexes) {\n if (a.btpUrl === this.defaultBtpUrl) continue;\n void this.instantiateApex(a, false).catch((err) =>\n this.log(`[runner] replay apex ${a.btpUrl} failed: ${errMsg(err)}`)\n );\n }\n }\n\n // ── Channel / negotiation helpers (per-apex) ───────────────────────────────\n\n /**\n * Open the apex channel — or, on a restart, RESUME the existing one.\n *\n * With `resumeOnly`, only a persisted channel is resumed (no on-chain open);\n * returns undefined when none exists so the caller can defer the open to the\n * first write (funded-after-start demo flow, #69).\n */\n private async openOrResumeApexChannel(\n apex: ApexConnection,\n opts: { resumeOnly?: boolean } = {}\n ): Promise<string | undefined> {\n const { destination, chain } = apex;\n const { apexChannelStorePath } = this.config;\n const saved = loadApexChannel(apexChannelStorePath, destination, chain);\n const cm = (\n apex.client as unknown as {\n channelManager?: {\n trackChannel?: (id: string, ctx: PersistedChannelContext) => void;\n };\n }\n ).channelManager;\n\n if (saved && cm && typeof cm.trackChannel === 'function') {\n cm.trackChannel(saved.channelId, saved.context);\n // Persisted channel state omits the on-chain deposit, so re-read it from\n // chain — otherwise the wallet shows 0 spendable on a funded channel.\n if (saved.context.chainType === 'evm') {\n await apex.client\n .rehydrateChannelDeposit?.(saved.channelId, {\n chain: `evm:${saved.context.chainId}`,\n tokenNetworkAddress: saved.context.tokenNetworkAddress,\n })\n .catch((err) =>\n this.log(\n `[runner] deposit re-hydrate for ${saved.channelId} failed: ${errMsg(err)}`\n )\n );\n }\n this.log(`[runner] resumed apex channel ${saved.channelId} (deposit re-read)`);\n return saved.channelId;\n }\n\n if (opts.resumeOnly) return undefined;\n\n const channelId = await apex.client.openChannel(destination);\n this.persistApexChannel(apex, channelId);\n return channelId;\n }\n\n /**\n * Persist a (lazily- or eagerly-) opened apex channel so a restart RESUMES it\n * (tracked, no re-deposit) rather than opening a second on-chain channel.\n * No-op when the apex carries no negotiation (nothing to key the store on).\n */\n private persistApexChannel(apex: ApexConnection, channelId: string): void {\n const a = apex.negotiation;\n if (!a) return;\n saveApexChannel(\n this.config.apexChannelStorePath,\n apex.destination,\n apex.chain,\n {\n channelId,\n context: {\n chainType: a.chain,\n chainId: a.chainId,\n tokenNetworkAddress: a.tokenNetwork ?? '',\n ...(a.tokenAddress ? { tokenAddress: a.tokenAddress } : {}),\n recipient: a.settlementAddress,\n },\n }\n );\n }\n\n /**\n * Discover the apex's settlement negotiation from its kind:10032 on the\n * default relay and attach it to the apex (proxy-mode fallback when no config\n * negotiation was supplied, #69). Throws ApexDiscoveryError on timeout/missing\n * settlement params so the apex's `lastError` reports exactly what is missing.\n */\n private async discoverApexNegotiation(apex: ApexConnection): Promise<void> {\n const relay = this.relays.get(this.defaultRelayUrl);\n if (!relay) {\n throw new TargetError(\n `Cannot discover apex \"${apex.destination}\": default relay ` +\n `${this.defaultRelayUrl} is not registered.`\n );\n }\n relay.start();\n const discovered = await discoverApex({\n relay,\n ilpAddress: apex.destination,\n chain: apex.chain,\n ...(apex.childPeers.length > 0 ? { childPeers: apex.childPeers } : {}),\n });\n apex.negotiation = discovered.negotiation;\n if (discovered.apexChildPeers) apex.childPeers = discovered.apexChildPeers;\n this.log(\n `[runner] discovered apex negotiation for \"${apex.destination}\" ` +\n `(chain ${discovered.negotiation.chainKey}, settle ` +\n `${discovered.negotiation.settlementAddress})`\n );\n }\n\n /** Inject the apex settlement negotiation directly into its ToonClient. */\n private injectApexNegotiation(apex: ApexConnection): void {\n const a = apex.negotiation;\n if (!a) return;\n const negotiations = (\n apex.client as unknown as { peerNegotiations?: Map<string, unknown> }\n ).peerNegotiations;\n if (!(negotiations instanceof Map)) {\n throw new Error(\n 'ToonClient.peerNegotiations layout changed — cannot inject apex negotiation'\n );\n }\n negotiations.set(a.peerId, {\n chain: a.chainKey,\n chainType: a.chain,\n chainId: a.chainId,\n settlementAddress: a.settlementAddress,\n tokenAddress: a.tokenAddress,\n tokenNetwork: a.tokenNetwork,\n });\n this.log(`[runner] injected apex negotiation for peer \"${a.peerId}\"`);\n }\n\n /** Route apex CHILD peers (store/swap) through the SAME apex payment channel. */\n private routeChildPeersThroughApexChannel(apex: ApexConnection): void {\n const a = apex.negotiation;\n if (!a || !apex.apexChannelId || apex.childPeers.length === 0) return;\n const client = apex.client as unknown as {\n peerNegotiations?: Map<string, unknown>;\n channelManager?: { peerChannels?: Map<string, string> };\n };\n const negotiations = client.peerNegotiations;\n const peerChannels = client.channelManager?.peerChannels;\n if (!(negotiations instanceof Map) || !(peerChannels instanceof Map)) {\n this.log(\n '[runner] cannot route child peers — ToonClient internals layout changed'\n );\n return;\n }\n for (const peer of apex.childPeers) {\n if (peer === a.peerId) continue;\n negotiations.set(peer, {\n chain: a.chainKey,\n chainType: a.chain,\n chainId: a.chainId,\n settlementAddress: a.settlementAddress,\n tokenAddress: a.tokenAddress,\n tokenNetwork: a.tokenNetwork,\n });\n peerChannels.set(peer, apex.apexChannelId);\n this.log(\n `[runner] routed child peer \"${peer}\" through apex channel ${apex.apexChannelId}`\n );\n }\n }\n\n // ── Status ─────────────────────────────────────────────────────────────────\n\n private defaultApex(): ApexConnection | undefined {\n return this.apexes.get(this.defaultBtpUrl);\n }\n\n /** Whether any apex has finished bootstrapping. */\n isReady(): boolean {\n return [...this.apexes.values()].some((a) => a.ready);\n }\n\n isBootstrapping(): boolean {\n return [...this.apexes.values()].some((a) => a.bootstrapping);\n }\n\n getStatus(): StatusResponse {\n const apex = this.defaultApex();\n const client = apex?.client;\n const net = client?.getNetworkStatus();\n const network: ChainStatus[] | undefined = net\n ? (['evm', 'solana', 'mina'] as const).map((c) => ({\n chain: c,\n ready: net[c] === 'configured',\n detail: net[c],\n }))\n : undefined;\n const relay = this.relays.get(this.defaultRelayUrl);\n return {\n uptimeMs: Date.now() - this.startedAt,\n bootstrapping: apex?.bootstrapping ?? false,\n ready: apex?.ready ?? false,\n settlementChain: this.config.chain,\n feePerEvent: (apex?.feePerEvent ?? this.config.feePerEvent).toString(),\n identity: {\n nostrPubkey: safe(() => client?.getPublicKey()) ?? '',\n evmAddress: safe(() => client?.getEvmAddress()),\n solanaAddress: safe(() => client?.getSolanaAddress()),\n minaAddress: safe(() => client?.getMinaAddress()),\n },\n transport: {\n type: 'direct',\n ...(apex ? { btpUrl: apex.btpUrl } : {}),\n },\n relay: {\n url: this.defaultRelayUrl,\n connected: relay?.isConnected() ?? false,\n buffered: relay?.bufferedCount() ?? 0,\n subscriptions: relay?.activeSubscriptions() ?? [],\n },\n ...(network ? { network } : {}),\n ...(apex?.lastError ? { lastError: apex.lastError } : {}),\n // Advertise the optional-route surface this daemon build serves so a\n // version-skewed rig CLI can capability-gate the `/git/*` write path\n // BEFORE delegating (an old daemon lacking these routes 404s otherwise —\n // #306). Static: these routes are always registered by this build.\n capabilities: ['git'],\n };\n }\n\n /**\n * Drip devnet test funds to a wallet from the configured faucet. Defaults the\n * chain to the active settlement chain and the address to this client's own\n * address on that chain, so a no-arg call funds the caller's own wallet\n * (the typical \"fund me before I open a channel\" flow). The daemon holds the\n * faucet URL + the keys, so the MCP caller never needs either.\n */\n fundWallet(req: FundWalletRequest = {}): FundWalletResponse {\n const faucetUrl = this.config.faucetUrl;\n if (!faucetUrl) {\n throw new InvalidPayloadError(\n 'no faucet configured — set faucetUrl in the daemon config (or the ' +\n 'TOON_CLIENT_FAUCET_URL env var) to fund wallets.'\n );\n }\n const chain: FaucetChain = req.chain ?? this.config.chain;\n const client = this.defaultApex()?.client;\n const address =\n req.address ??\n safe(() =>\n chain === 'evm'\n ? client?.getEvmAddress()\n : chain === 'solana'\n ? client?.getSolanaAddress()\n : client?.getMinaAddress()\n );\n if (!address) {\n throw new InvalidPayloadError(\n `no ${chain} address available to fund — pass an explicit address ` +\n `(this client has no ${chain} key configured).`\n );\n }\n\n // Idempotent: a drip already in flight for this chain returns its snapshot\n // rather than launching a second faucet call (a re-click / poll mustn't\n // double-drip).\n const existing = this.fundJobs.get(chain);\n if (existing && existing.status === 'pending') {\n return { ...existing };\n }\n\n // The drip is ASYNC: launch the faucet call in the background and return a\n // 'pending' snapshot immediately. The Mina faucet mints native MINA + USDC\n // on a slow-settling chain and legitimately takes ~75s — longer than the MCP\n // host's ~60s tool-call budget and the control client's wire timeout — so a\n // blocking call surfaces a working drip as a misleading relay/apex timeout\n // (#199-class). The daemon happily waits the full chain-aware faucet budget\n // in the background; the caller observes the result via getFundStatus /\n // re-reading balances.\n const job: FundJob = {\n chain,\n address,\n faucetUrl,\n status: 'pending',\n startedAt: Date.now(),\n };\n this.fundJobs.set(chain, job);\n\n // The drip runs in the BACKGROUND, so there is no caller to protect from a\n // slow faucet — use a GENEROUS timeout. The faucet client default (30s for\n // evm/solana) is tuned for a synchronous call and falsely aborts a drip that\n // succeeds server-side a bit later: e.g. a loaded EVM faucet answers >30s but\n // the tx still lands, so the job would report `error` while the balance\n // actually went up — causing a misleading failure + double-fund risk. Await\n // the real outcome instead (config `faucetTimeoutMs` still overrides).\n const faucetTimeout =\n this.config.faucetTimeoutMs ?? (chain === 'mina' ? 130_000 : 90_000);\n void faucetFund(faucetUrl, address, chain, { timeout: faucetTimeout })\n .then(({ response }) => {\n job.status = 'success';\n job.response = response;\n job.finishedAt = Date.now();\n this.log(`[runner] faucet drip succeeded: ${chain} → ${address}`);\n })\n .catch((err: unknown) => {\n // The background promise must never become an unhandled rejection.\n try {\n const msg = errMsg(err);\n // A timeout is NOT a definitive failure — the on-chain drip may still\n // settle after the client gives up (observed on EVM). Mark it as a\n // distinct, non-terminal-sounding state and advise re-checking\n // balances before re-funding, rather than asserting it failed.\n const timedOut = /timed out|timeout|aborted/i.test(msg);\n job.status = timedOut ? 'timeout' : 'error';\n job.error = timedOut\n ? `${msg} — the on-chain drip may still have settled; re-check balances before re-funding.`\n : msg;\n job.finishedAt = Date.now();\n this.log(\n `[runner] faucet drip ${timedOut ? 'timed out' : 'failed'}: ${chain} → ${address}: ${msg}`\n );\n } catch {\n // Swallow — recording the failure must not itself reject.\n }\n });\n\n return { ...job };\n }\n\n /**\n * Snapshots of tracked faucet drip jobs — all of them, or just the one for\n * `chain`. Lets a caller poll for the terminal state of an async drip without\n * re-dripping.\n */\n getFundStatus(chain?: FaucetChain): FundStatusResponse {\n const jobs = chain\n ? this.fundJobs.has(chain)\n ? [{ ...this.fundJobs.get(chain)! }]\n : []\n : [...this.fundJobs.values()].map((j) => ({ ...j }));\n return { jobs };\n }\n\n /** Full registry of relay + apex targets with per-target status. */\n getTargets(): TargetsResponse {\n const relays: RelayTargetStatus[] = [...this.relays.entries()].map(\n ([relayUrl, r]) => ({\n relayUrl,\n connected: r.isConnected(),\n buffered: r.bufferedCount(),\n subscriptions: r.activeSubscriptions(),\n isDefault: relayUrl === this.defaultRelayUrl,\n })\n );\n const apexes: ApexTargetStatus[] = [...this.apexes.values()].map((a) => ({\n btpUrl: a.btpUrl,\n destination: a.destination,\n chain: a.chain,\n ready: a.ready,\n bootstrapping: a.bootstrapping,\n ...(a.apexChannelId ? { channelId: a.apexChannelId } : {}),\n ...(a.lastError ? { lastError: a.lastError } : {}),\n isDefault: a.isDefault,\n }));\n return { relays, apexes };\n }\n\n // ── Paid operations ──────────────────────────────────────────────────────\n\n /**\n * Lazily open the apex channel on first paid write (deferred at bootstrap so\n * the wallet can be funded after start, #69) and persist it for resume.\n */\n private async ensureApexChannel(\n apex: ApexConnection,\n destination?: string\n ): Promise<string> {\n let channelId = apex.apexChannelId;\n if (!channelId) {\n channelId = await apex.client.openChannel(destination);\n if (!destination || destination === apex.destination) {\n apex.apexChannelId = channelId;\n this.persistApexChannel(apex, channelId);\n }\n }\n return channelId;\n }\n\n /** Pay-to-write a single event through the selected (or default) apex. */\n async publish(req: PublishRequest): Promise<PublishResponse> {\n const apex = this.selectApex(req.btpUrl);\n this.assertApexReady(apex);\n const channelId = await this.ensureApexChannel(apex, req.destination);\n const fee = req.fee !== undefined ? BigInt(req.fee) : apex.feePerEvent;\n const claim = await apex.client.signBalanceProof(channelId, fee);\n // Relay writes default to the configured publish destination (e.g.\n // g.proxy.relay) — NOT the apex anchor, which on the devnet proxy is\n // g.proxy.relay.store and would forward a /write to the store (→ 404). An\n // explicit per-call destination still wins. The claim is pre-signed on the\n // apex channel, so the destination is pure routing (settlement is unaffected).\n const result = await apex.client.publishEvent(req.event, {\n destination: req.destination ?? this.config.publishDestination,\n claim,\n ilpAmount: fee,\n });\n if (!result.success) {\n throw new PublishRejectedError(result.error ?? 'relay rejected event');\n }\n return {\n eventId: result.eventId ?? req.event.id,\n ...(result.data !== undefined ? { data: result.data } : {}),\n channelId,\n nonce: apex.client.getChannelNonce(channelId),\n feePaid: fee.toString(),\n channelBalanceAfter: this.channelAvailable(apex, channelId),\n };\n }\n\n /**\n * Available (spendable) balance for a channel after a write — locked collateral\n * minus cumulative spent, clamped at 0. Same math as {@link getChannels}; used\n * to report a truthful post-write balance in publish/upload receipts. Returns\n * undefined if the channel isn't tracked on this apex (balance unknown).\n */\n private channelAvailable(apex: ApexConnection, channelId: string): string | undefined {\n if (!apex.client.getTrackedChannels().includes(channelId)) return undefined;\n const cumulative = apex.client.getChannelCumulativeAmount(channelId);\n const depositTotal = apex.client.getChannelDepositTotal(channelId);\n const available = depositTotal > cumulative ? depositTotal - cumulative : 0n;\n return available.toString();\n }\n\n /**\n * Build, sign (with the daemon-held key), and pay-to-write an event. The\n * caller supplies only the event shell; the private key never leaves the\n * daemon. Payloads are MODEL-AUTHORED → validated server-side here (the model\n * is not a security boundary). Replaceable kinds (0/3) merge the latest known\n * event's tags before signing.\n */\n async publishUnsigned(req: PublishUnsignedRequest): Promise<PublishResponse> {\n const apex = this.selectApex(req.btpUrl);\n this.assertApexReady(apex);\n const template = this.buildTemplate(apex, req);\n const signed = await apex.client.signEvent(template);\n return this.publish({\n event: signed,\n ...(req.destination ? { destination: req.destination } : {}),\n ...(req.fee ? { fee: req.fee } : {}),\n ...(req.btpUrl ? { btpUrl: req.btpUrl } : {}),\n });\n }\n\n /**\n * Upload media to Arweave (kind:5094 blob DVM, single-packet) then sign+publish\n * a media event referencing the resulting URL. One spendy operation, two steps,\n * entirely server-side.\n */\n async uploadMedia(req: UploadMediaRequest): Promise<UploadMediaResponse> {\n const apex = this.selectApex(req.btpUrl);\n this.assertApexReady(apex);\n // Source the bytes from EXACTLY ONE of inline base64 or an on-disk path.\n // `filePath` lets agent callers skip materializing the whole payload as a\n // tool argument (it never touches the model context); `dataBase64` stays for\n // back-compat. Both-or-neither is a payload error.\n const hasData = typeof req.dataBase64 === 'string' && req.dataBase64 !== '';\n const hasPath = typeof req.filePath === 'string' && req.filePath !== '';\n if (hasData === hasPath) {\n throw new InvalidPayloadError(\n 'exactly one of dataBase64 (base64 media bytes) | filePath (absolute path) is required.'\n );\n }\n const blobData = hasPath\n ? await this.readUploadFile(req.filePath as string)\n : new Uint8Array(Buffer.from(req.dataBase64 as string, 'base64'));\n const fee = req.fee !== undefined ? BigInt(req.fee) : apex.feePerEvent;\n // ── Leg 1: Arweave blob upload ──────────────────────────────────────────\n // Blob storage terminates at the store/DVM backend (POST /store → Arweave),\n // so it routes to the configured store destination (e.g. g.proxy.store,\n // derived from the `….relay.store` anchor by #143). This makes uploads work\n // via the default apex without the caller hand-passing a store `btpUrl`. A\n // failure here is distinct from the kind:1-equivalent publish below; label\n // it so the UI/agent can tell the upload leg apart from the publish leg.\n const upload = await apex.client.uploadBlob({\n blobData,\n destination: this.config.storeDestination,\n ...(req.mime ? { contentType: req.mime } : {}),\n ilpAmount: fee,\n });\n if (!upload.success || !upload.txId) {\n throw new PublishRejectedError(\n `Arweave upload leg failed (store ${this.config.storeDestination}): ${upload.error ?? 'blob upload rejected'}`\n );\n }\n const { url, fallbacks } = arweaveUrls(upload.txId, this.config.arweaveGateways);\n const kind = req.kind ?? 1063;\n const signed = await apex.client.signEvent({\n kind,\n created_at: nowSeconds(),\n tags: this.buildMediaTags(kind, url, fallbacks, req),\n content: req.caption ?? '',\n });\n // ── Leg 2: publish the NIP-94/NIP-68 reference event ────────────────────\n // The reference event is a normal Nostr write, so it must publish through a\n // RELAY apex, not the store/DVM. `this.publish` routes it to the configured\n // publish destination (e.g. g.proxy.relay) — the exact path #143 made kind:1\n // work. Omit `btpUrl` so it uses the default (relay) apex. Label any failure\n // here as the post-upload publish leg (the blob already stored OK).\n let pub: PublishResponse;\n try {\n pub = await this.publish({\n event: signed,\n ...(req.fee ? { fee: req.fee } : {}),\n });\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n throw new PublishRejectedError(\n `kind:${kind} publish leg failed after upload (blob stored at ${url}): ${detail}`\n );\n }\n // An upload pays TWICE — the blob (leg 1) and the reference event (leg 2) —\n // so the truthful total is the sum, not just the publish leg's fee. The\n // post-write balance from the (last) publish leg is already current.\n const feePaid = (fee + BigInt(pub.feePaid)).toString();\n return { ...pub, feePaid, url, txId: upload.txId };\n }\n\n /**\n * Read media bytes off disk for an upload `filePath`. The path is resolved\n * and, when an `uploadAllowedRoot` is configured, must resolve inside it —\n * bounding which filesystem locations the daemon reads on an agent's behalf.\n * A missing/unreadable file (or an out-of-bounds path) surfaces as an\n * `InvalidPayloadError` (HTTP 400), not an unhandled crash.\n */\n private async readUploadFile(filePath: string): Promise<Uint8Array> {\n const resolved = resolve(filePath);\n const root = this.config.uploadAllowedRoot;\n if (root && resolved !== root && !resolved.startsWith(root + sep)) {\n throw new InvalidPayloadError(\n `filePath must resolve inside the configured upload root (${root}).`\n );\n }\n try {\n const buf = await readFile(resolved);\n return new Uint8Array(buf);\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n throw new InvalidPayloadError(`failed to read filePath ${resolved}: ${detail}`);\n }\n }\n\n /** Validate + assemble a signable event template (with replaceable merge). */\n private buildTemplate(\n apex: ApexConnection,\n req: PublishUnsignedRequest\n ): EventTemplate {\n if (!Number.isInteger(req.kind) || req.kind < 0 || req.kind > 65535) {\n throw new InvalidPayloadError('kind must be an integer in [0, 65535].');\n }\n if (req.content !== undefined && typeof req.content !== 'string') {\n throw new InvalidPayloadError('content must be a string.');\n }\n const tags = normalizeTags(req.tags);\n const content = req.content ?? '';\n\n // Replaceable kinds: merge the latest known self-authored event so a single\n // \"follow X\" / profile edit doesn't clobber prior tags. Best-effort from the\n // read buffer (v1 — concurrent edits can still race; see plan risk #6).\n if (req.kind === 0 || req.kind === 3) {\n const prior = this.latestSelfReplaceable(apex, req.kind);\n if (prior) {\n return {\n kind: req.kind,\n created_at: nowSeconds(),\n tags: mergeTags(prior.tags, tags),\n content: content !== '' ? content : prior.content,\n };\n }\n }\n return { kind: req.kind, created_at: nowSeconds(), tags, content };\n }\n\n /** Latest self-authored event of `kind` currently in the merged read buffer. */\n private latestSelfReplaceable(\n apex: ApexConnection,\n kind: number\n ): NostrEvent | undefined {\n const pubkey = safe(() => apex.client.getPublicKey());\n if (!pubkey) return undefined;\n let latest: NostrEvent | undefined;\n for (const m of this.merged) {\n if (m.event.kind !== kind || m.event.pubkey !== pubkey) continue;\n if (!latest || m.event.created_at > latest.created_at) latest = m.event;\n }\n return latest;\n }\n\n /**\n * Tags for a published media event referencing an Arweave URL. `url` is the\n * primary gateway; `fallbacks` are mirror URLs for the same tx id on other\n * gateways, emitted so readers can fail over if the primary is unreachable.\n */\n private buildMediaTags(\n kind: number,\n url: string,\n fallbacks: string[],\n req: UploadMediaRequest\n ): string[][] {\n const mime = req.mime ?? 'application/octet-stream';\n const extra = normalizeTags(req.tags);\n if (kind === 1063) {\n // NIP-94 file metadata: separate url/m tags, mirrors as `fallback` tags.\n return [\n ['url', url],\n ['m', mime],\n ...fallbacks.map((f) => ['fallback', f]),\n ...extra,\n ];\n }\n // NIP-68/71 picture/video + NIP-92 inline note: a single `imeta` tag with\n // the primary `url` first and the remaining gateways as `fallback` mirrors.\n return [\n ['imeta', `url ${url}`, `m ${mime}`, ...fallbacks.map((f) => `fallback ${f}`)],\n ...extra,\n ];\n }\n\n /** Open (or return) a payment channel on the selected (or default) apex. */\n async openChannel(\n destination?: string,\n btpUrl?: string\n ): Promise<{ channelId: string }> {\n const apex = this.selectApex(btpUrl);\n this.assertApexReady(apex);\n const channelId = await apex.client.openChannel(\n destination ?? apex.destination\n );\n if (!destination || destination === apex.destination) {\n const firstOpen = apex.apexChannelId !== channelId;\n apex.apexChannelId = channelId;\n // Persist the (possibly lazily-opened) apex channel for restart-resume.\n if (firstOpen) this.persistApexChannel(apex, channelId);\n }\n return { channelId };\n }\n\n /** List tracked channels across ALL apexes with nonce + cumulative amount. */\n getChannels(): ChannelsResponse {\n const seen = new Set<string>();\n const channels: ChannelsResponse['channels'] = [];\n for (const apex of this.apexes.values()) {\n for (const channelId of apex.client.getTrackedChannels()) {\n if (seen.has(channelId)) continue;\n seen.add(channelId);\n const cumulative = apex.client.getChannelCumulativeAmount(channelId);\n const depositTotal = apex.client.getChannelDepositTotal(channelId);\n // Available (spendable) balance = locked collateral − cumulative spent.\n // Clamp at 0 so an over-spend estimate never surfaces as negative.\n const available = depositTotal > cumulative ? depositTotal - cumulative : 0n;\n const settleableAt = apex.client.getSettleableAt(channelId);\n channels.push({\n channelId,\n nonce: apex.client.getChannelNonce(channelId),\n cumulativeAmount: cumulative.toString(),\n depositTotal: depositTotal.toString(),\n availableBalance: available.toString(),\n closeState: apex.client.getChannelCloseState(channelId),\n ...(settleableAt !== undefined ? { settleableAt: settleableAt.toString() } : {}),\n });\n }\n }\n return { channels };\n }\n\n /**\n * On-chain wallet balances. The wallet is identity-level (same keys across\n * apexes), so this reads from the daemon's {@link identityClient} — NOT an apex\n * — and therefore works even with zero apexes / no payment peer configured\n * (reading your own balance is a pure wallet-keys + chain-RPC operation).\n * Per-chain reads are best-effort inside the client (a failing chain is simply\n * omitted).\n *\n * Each underlying read hits per-chain RPC providers that can stall\n * indefinitely on devnet (a provider being `detail: \"configured\"` in\n * toon_status means it is WIRED, not that its RPC is live). A stall here used\n * to block the whole control request until the client aborted, surfacing as a\n * misleading \"relay/apex unreachable\" timeout (#199). Bound each attempt well\n * under the control API timeout and retry once so a single transient\n * provider stall FAST-FAILS with an honest \"balances handler / provider\n * stalled\" error instead of hanging.\n */\n async getBalances(): Promise<BalancesResponse> {\n let lastErr: unknown;\n for (let attempt = 1; attempt <= BALANCES_READ_ATTEMPTS; attempt++) {\n try {\n const balances = (await withTimeout(\n this.identityClient.getBalances(),\n BALANCES_READ_TIMEOUT_MS,\n `chain balance read timed out after ${BALANCES_READ_TIMEOUT_MS}ms`\n )) as BalanceInfo[];\n return { balances };\n } catch (err) {\n lastErr = err;\n }\n }\n throw new BalancesUnavailableError(\n `the balances control handler's chain RPC/provider read did not return ` +\n `(${BALANCES_READ_ATTEMPTS} attempts, ${BALANCES_READ_TIMEOUT_MS}ms each) — ` +\n `the on-chain provider stalled, not the relay or apex. Retry shortly.`,\n lastErr instanceof Error ? lastErr.message : undefined\n );\n }\n\n /**\n * Deposit additional collateral into an open channel. Routes to the apex whose\n * client tracks the channel (each apex client opens/tracks its own channels);\n * the client signs its own on-chain tx.\n */\n async depositToChannel(req: ChannelDepositRequest): Promise<ChannelDepositResponse> {\n return this.withTrackingApex(req.channelId, (client) =>\n client.depositToChannel(req.channelId, req.amount)\n );\n }\n\n /** Close a channel to begin the settlement grace period (withdraw, step 1). */\n async closeChannel(req: CloseChannelRequest): Promise<CloseChannelResponse> {\n return this.withTrackingApex(req.channelId, (client) => client.closeChannel(req.channelId));\n }\n\n /**\n * Settle a closed channel to release collateral (withdraw, step 2). The client\n * enforces the `now >= settleableAt` guard and throws a retryable error if\n * called early; `mapError` maps that to HTTP 425.\n */\n async settleChannel(req: SettleChannelRequest): Promise<SettleChannelResponse> {\n return this.withTrackingApex(req.channelId, (client) => client.settleChannel(req.channelId));\n }\n\n /** Run `fn` against the apex client that tracks `channelId`, else throw. */\n private async withTrackingApex<T>(\n channelId: string,\n fn: (client: ApexConnection['client']) => Promise<T>\n ): Promise<T> {\n for (const apex of this.apexes.values()) {\n if (apex.client.getTrackedChannels().includes(channelId)) {\n return fn(apex.client);\n }\n }\n throw new Error(`Channel \"${channelId}\" is not tracked by any apex.`);\n }\n\n /** Swap source→target asset against a swap peer via the selected apex. */\n async swap(req: SwapRequest): Promise<SwapResponse> {\n const apex = this.selectApex(req.btpUrl);\n this.assertApexReady(apex);\n const senderSecretKey = generateSecretKey();\n const result = await streamSwap({\n client: apex.client as unknown as Parameters<\n typeof streamSwap\n >[0]['client'],\n millPubkey: req.swapPubkey,\n millIlpAddress: req.destination,\n pair: req.pair,\n senderSecretKey,\n chainRecipient: req.chainRecipient,\n totalAmount: BigInt(req.amount),\n packetCount: req.packetCount ?? 1,\n });\n const firstReject = result.rejections[0];\n return {\n accepted: result.claims.length > 0,\n packetsAccepted: result.claims.length,\n claims: result.claims.map((c) => ({\n sourceAmount: c.sourceAmount.toString(),\n targetAmount: c.targetAmount.toString(),\n claim: Buffer.from(c.claimBytes).toString('base64'),\n ...(c.channelId ? { channelId: c.channelId } : {}),\n ...(c.recipient ? { recipient: c.recipient } : {}),\n ...(c.millSignerAddress\n ? { swapSignerAddress: c.millSignerAddress }\n : {}),\n ...(c.claimId ? { claimId: c.claimId } : {}),\n ...(c.nonce ? { nonce: c.nonce } : {}),\n ...(c.cumulativeAmount ? { cumulativeAmount: c.cumulativeAmount } : {}),\n })),\n cumulativeSource: result.cumulativeSource.toString(),\n cumulativeTarget: result.cumulativeTarget.toString(),\n state: result.state,\n ...(firstReject\n ? { code: firstReject.code, message: firstReject.message }\n : {}),\n };\n }\n\n /**\n * Payment-aware HTTP fetch through an apex's client. The client issues the\n * request and, on `402 Payment Required`, pays over TOON and retries; we\n * translate the resulting Web `Response` into the wire envelope.\n */\n async httpFetchPaid(\n req: HttpFetchPaidRequest\n ): Promise<HttpFetchPaidResponse> {\n const apex = this.selectApex();\n this.assertApexReady(apex);\n const res = await apex.client.h402Fetch(req.url, {\n ...(req.method ? { method: req.method } : {}),\n ...(req.headers ? { headers: req.headers } : {}),\n ...(req.body !== undefined ? { body: req.body } : {}),\n ...(req.timeout !== undefined ? { timeout: req.timeout } : {}),\n });\n return {\n status: res.status,\n headers: Object.fromEntries(res.headers.entries()),\n body: await res.text(),\n };\n }\n\n // ── Git write path (/git/*, epic #222 ticket #227) ────────────────────────\n\n /**\n * The daemon `Publisher` implementation (see @toon-protocol/rig) for one\n * apex. Maps the interface onto the runner's production paid-write\n * machinery:\n *\n * - `getFeeRates`: flat `apex.feePerEvent` per publish + the network\n * per-byte upload rate.\n * - `uploadGitObject`: kind:5094 store write with Git-SHA/Git-Type/Repo\n * tags (the proven seed-pipeline shape), signed with the daemon key,\n * paid via signBalanceProof on the apex channel, routed to the store\n * destination (`POST /store`); the Arweave txId is decoded from the\n * FULFILL HTTP envelope.\n * - `publishEvent`: sign with the daemon key + the standard paid publish\n * path (signBalanceProof → publishEvent → feePaid). The daemon owns its\n * write routing (config-seeded relay via the apex), so the advisory\n * `relayUrls` list is not consulted here — remote-state reads DO use it.\n */\n private gitPublisher(apex: ApexConnection): Publisher {\n return {\n getFeeRates: async () => ({\n uploadFeePerByte: UPLOAD_FEE_PER_BYTE,\n eventFee: apex.feePerEvent,\n }),\n uploadGitObject: (upload) => this.gitUploadObject(apex, upload),\n publishEvent: (event) => this.gitPublishEvent(apex, event),\n };\n }\n\n /** Upload one git object body as a paid kind:5094 store write. */\n private async gitUploadObject(\n apex: ApexConnection,\n upload: GitObjectUpload\n ): Promise<UploadReceipt> {\n const channelId = await this.ensureApexChannel(apex);\n const fee = BigInt(upload.body.length) * UPLOAD_FEE_PER_BYTE;\n const claim = await apex.client.signBalanceProof(channelId, fee);\n const signed = await apex.client.signEvent({\n kind: 5094,\n content: '',\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 created_at: nowSeconds(),\n });\n const result = await apex.client.publishEvent(signed, {\n destination: this.config.storeDestination,\n claim,\n ilpAmount: fee,\n // The store/DVM backend serves POST /store (not the relay's /write).\n proxyPath: '/store',\n });\n if (!result.success) {\n throw new PublishRejectedError(\n `git object ${upload.sha} upload failed (store ` +\n `${this.config.storeDestination}): ${result.error ?? 'store rejected the write'}`\n );\n }\n if (!result.data) {\n throw new PublishRejectedError(\n `git object ${upload.sha} upload FULFILL carried no data — expected the Arweave tx ID`\n );\n }\n let txId: string;\n try {\n txId = extractArweaveTxId(result.data);\n } catch (err) {\n throw new PublishRejectedError(\n `git object ${upload.sha} upload: ${errMsg(err)}`\n );\n }\n return { txId, feePaid: fee };\n }\n\n /** Sign (daemon key) + pay-to-publish one NIP-34 event via the apex. */\n private async gitPublishEvent(\n apex: ApexConnection,\n event: UnsignedEvent\n ): Promise<PublishReceipt> {\n const signed = await apex.client.signEvent(event);\n const pub = await this.publish({\n event: signed,\n ...(apex.btpUrl ? { btpUrl: apex.btpUrl } : {}),\n });\n return { eventId: pub.eventId, feePaid: BigInt(pub.feePaid) };\n }\n\n /**\n * Plan a push: read the local repo + the remote NIP-34 state, classify ref\n * updates, compute the object delta, and price it. Shared by\n * estimate (returns the plan) and push (executes it).\n */\n private async planGitPush(\n apex: ApexConnection,\n req: GitEstimateRequest\n ): Promise<{\n plan: PushPlan;\n remoteState: RemoteState;\n repoReader: GitRepoReader;\n relayUrls: string[];\n publisher: Publisher;\n }> {\n await assertRepoPath(req.repoPath);\n if (typeof req.repoId !== 'string' || req.repoId === '') {\n throw new InvalidPayloadError('repoId is required.');\n }\n const relayUrls =\n req.relayUrls && req.relayUrls.length > 0\n ? req.relayUrls\n : [this.defaultRelayUrl];\n // Pushes publish kind:30617/30618 signed by the daemon key, so the daemon\n // identity IS the repo owner whose remote state we read.\n const ownerPubkey = apex.client.getPublicKey();\n const repoReader = this.createRepoReader(req.repoPath);\n const remoteState = await this.fetchGitRemoteState({\n relayUrls,\n ownerPubkey,\n repoId: req.repoId,\n });\n const publisher = this.gitPublisher(apex);\n const feeRates = await publisher.getFeeRates();\n const plan = await planPush({\n repoReader,\n remoteState,\n feeRates,\n repoId: req.repoId,\n ...(req.refspecs !== undefined ? { refs: req.refspecs } : {}),\n ...(req.force !== undefined ? { force: req.force } : {}),\n ...(req.announcement !== undefined\n ? { announcement: req.announcement }\n : {}),\n });\n return { plan, remoteState, repoReader, relayUrls, publisher };\n }\n\n /** Plan + price a push WITHOUT paying (backs `POST /git/estimate`). */\n async gitEstimate(req: GitEstimateRequest): Promise<GitEstimateResponse> {\n const apex = this.selectApex();\n this.assertApexReady(apex);\n const { plan } = await this.planGitPush(apex, req);\n return serializePushPlan(plan);\n }\n\n /** Plan + EXECUTE a push: paid uploads + paid publishes (`POST /git/push`). */\n async gitPush(req: GitPushRequest): Promise<GitPushResponse> {\n if (req.confirm !== true) {\n throw new InvalidPayloadError(\n 'a push uploads objects to Arweave and publishes events — permanent ' +\n 'and paid. Run /git/estimate first, then set confirm: true to proceed.'\n );\n }\n const apex = this.selectApex();\n this.assertApexReady(apex);\n const { plan, remoteState, repoReader, relayUrls, publisher } =\n await this.planGitPush(apex, req);\n const result = await executePush({\n plan,\n publisher,\n remoteState,\n repoReader,\n relayUrls,\n });\n return serializePushResult(plan, result);\n }\n\n /** Build, sign, and pay-to-publish a kind:1621 issue. */\n async gitIssue(req: GitIssueRequest): Promise<GitEventResponse> {\n const addr = validateRepoAddr(req.repoAddr);\n assertNonEmptyString(req.title, 'title');\n assertNonEmptyString(req.body, 'body');\n const event = buildIssue(\n addr.ownerPubkey,\n addr.repoId,\n req.title,\n req.body,\n req.labels ?? []\n );\n return this.gitPublishSigned(event);\n }\n\n /** Build, sign, and pay-to-publish a kind:1622 comment on an issue/patch. */\n async gitComment(req: GitCommentRequest): Promise<GitEventResponse> {\n const addr = validateRepoAddr(req.repoAddr);\n assertNonEmptyString(req.rootEventId, 'rootEventId');\n assertNonEmptyString(req.body, 'body');\n const event = buildComment(\n addr.ownerPubkey,\n addr.repoId,\n req.rootEventId,\n req.parentAuthorPubkey ?? addr.ownerPubkey,\n req.body,\n req.marker ?? 'root'\n );\n return this.gitPublishSigned(event);\n }\n\n /**\n * Build, sign, and pay-to-publish a kind:1617 patch. Content is either the\n * supplied `patchText` or real `git format-patch --stdout <range>` output\n * from a local repository — exactly one source must be given.\n */\n async gitPatch(req: GitPatchRequest): Promise<GitEventResponse> {\n const addr = validateRepoAddr(req.repoAddr);\n assertNonEmptyString(req.title, 'title');\n const hasText = typeof req.patchText === 'string' && req.patchText !== '';\n const hasRange =\n typeof req.repoPath === 'string' &&\n req.repoPath !== '' &&\n typeof req.range === 'string' &&\n req.range !== '';\n if (hasText === hasRange) {\n throw new InvalidPayloadError(\n 'exactly one of patchText | repoPath+range is required.'\n );\n }\n let content: string;\n if (hasRange) {\n await assertRepoPath(req.repoPath as string);\n content = await this.createRepoReader(req.repoPath as string).formatPatch(\n req.range as string\n );\n if (content === '') {\n throw new InvalidPayloadError(\n `range ${JSON.stringify(req.range)} selects no commits — nothing to publish.`\n );\n }\n } else {\n content = req.patchText as string;\n }\n const event = buildPatch(\n addr.ownerPubkey,\n addr.repoId,\n req.title,\n req.commits ?? [],\n req.branch,\n content,\n // PR body → `description` tag; never the content (git am safety, #280).\n typeof req.description === 'string' && req.description !== ''\n ? req.description\n : undefined\n );\n return this.gitPublishSigned(event);\n }\n\n /** Build, sign, and pay-to-publish a kind:1630-1633 status event. */\n async gitStatus(req: GitStatusRequest): Promise<GitEventResponse> {\n const addr = validateRepoAddr(req.repoAddr);\n assertNonEmptyString(req.targetEventId, 'targetEventId');\n const kind = STATUS_KIND_BY_VALUE[req.status];\n if (kind === undefined) {\n throw new InvalidPayloadError(\n 'status must be one of open | applied | closed | draft.'\n );\n }\n const event = buildStatus(req.targetEventId, kind, req.targetPubkey);\n // NIP-34 status events also carry the repo `a` tag so readers can scope\n // a status stream to the repository without resolving the target first.\n event.tags.push([\n 'a',\n `${REPOSITORY_ANNOUNCEMENT_KIND}:${addr.ownerPubkey}:${addr.repoId}`,\n ]);\n return this.gitPublishSigned(event);\n }\n\n /** Sign a built NIP-34 event with the daemon key and pay-to-publish it. */\n private async gitPublishSigned(event: UnsignedEvent): Promise<GitEventResponse> {\n const apex = this.selectApex();\n this.assertApexReady(apex);\n const signed = await apex.client.signEvent(event);\n const pub = await this.publish({ event: signed });\n return { ...pub, kind: event.kind };\n }\n\n /** Graceful teardown: close every relay + stop every apex client. */\n async stop(): Promise<void> {\n if (this.stopped) return;\n this.stopped = true;\n for (const relay of this.relays.values()) relay.close();\n for (const apex of this.apexes.values()) {\n try {\n await apex.client.stop();\n } catch (err) {\n this.log(`[runner] client stop error (${apex.btpUrl}): ${errMsg(err)}`);\n }\n }\n }\n\n // ── internals ────────────────────────────────────────────────────────────\n\n private selectApex(btpUrl?: string): ApexConnection {\n if (btpUrl) {\n const apex = this.apexes.get(btpUrl);\n if (!apex) throw new TargetError(`No such apex: ${btpUrl}`);\n return apex;\n }\n const def = this.defaultApex();\n if (!def) throw new NotReadyError('No apex configured.');\n return def;\n }\n\n private assertApexReady(apex: ApexConnection): void {\n // FREE reads need no uplink; a write does. Reject early with an actionable\n // message rather than letting the apex sit forever un-bootstrapped (#69).\n if (!this.config.hasUplink) {\n throw new TargetError(\n 'No write uplink configured — this daemon is read-only. Set ' +\n 'TOON_CLIENT_PROXY_URL (connector proxy) or TOON_CLIENT_BTP_URL to ' +\n 'enable paid writes.'\n );\n }\n if (!apex.ready) {\n throw new NotReadyError(\n apex.bootstrapping\n ? 'Apex is still bootstrapping (transport/channel coming up) — retry shortly.'\n : (apex.lastError ?? 'Apex is not ready.')\n );\n }\n }\n}\n\n/** Thrown by paid-write operations while the target apex is not yet ready. */\nexport class NotReadyError extends Error {\n readonly retryable = true;\n constructor(message: string) {\n super(message);\n this.name = 'NotReadyError';\n }\n}\n\n/** Thrown when the relay/connector rejects a paid write. */\nexport class PublishRejectedError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'PublishRejectedError';\n }\n}\n\n/** Thrown for invalid target add/remove/select operations (maps to HTTP 400/404). */\nexport class TargetError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'TargetError';\n }\n}\n\n/** Thrown when a model-authored publish/upload payload fails validation (HTTP 400). */\nexport class InvalidPayloadError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'InvalidPayloadError';\n }\n}\n\n/**\n * Thrown when the on-chain balance read stalls past its per-call provider\n * timeout (after the bounded retry). Retryable, and explicitly attributed to the\n * balances handler / chain provider — NOT the relay/apex — so the user-facing\n * message names the real failing subsystem (#199). Maps to HTTP 504.\n */\nexport class BalancesUnavailableError extends Error {\n readonly retryable = true;\n /** The underlying provider error message, when one was captured. */\n readonly providerError?: string;\n constructor(message: string, providerError?: string) {\n super(message);\n this.name = 'BalancesUnavailableError';\n if (providerError !== undefined) this.providerError = providerError;\n }\n}\n\n/**\n * Upload price per git-object body byte, micro-USDC. Matches the network\n * default `basePricePerByte` (10) the ToonClient prices writes with and the\n * seed pipeline bids (`bytes × 10`); the bid tag, the signed claim, and the\n * ILP amount all use this same figure so the pre-push estimate is exactly\n * what a push pays.\n */\nconst UPLOAD_FEE_PER_BYTE = 10n;\n\n/** NIP-34 status kinds by wire value (`GitStatusRequest.status`). */\nconst STATUS_KIND_BY_VALUE: Record<string, StatusKind> = {\n open: STATUS_OPEN_KIND,\n applied: STATUS_APPLIED_KIND,\n closed: STATUS_CLOSED_KIND,\n draft: STATUS_DRAFT_KIND,\n};\n\n/** Validate that `repoPath` names an existing directory (a git repo check\n * proper happens on first plumbing call — a non-repo dir surfaces as a\n * GitError the routes map to 400). */\nasync function assertRepoPath(repoPath: unknown): Promise<void> {\n if (typeof repoPath !== 'string' || repoPath === '') {\n throw new InvalidPayloadError('repoPath is required.');\n }\n let stats;\n try {\n stats = await stat(resolve(repoPath));\n } catch {\n throw new InvalidPayloadError(`repoPath does not exist: ${repoPath}`);\n }\n if (!stats.isDirectory()) {\n throw new InvalidPayloadError(`repoPath is not a directory: ${repoPath}`);\n }\n}\n\nfunction assertNonEmptyString(value: unknown, what: string): void {\n if (typeof value !== 'string' || value === '') {\n throw new InvalidPayloadError(`${what} is required.`);\n }\n}\n\n/** Validate a NIP-34 repo address (owner pubkey + repo id). */\nfunction validateRepoAddr(addr: GitRepoAddr | undefined): GitRepoAddr {\n if (\n !addr ||\n typeof addr.ownerPubkey !== 'string' ||\n !/^[0-9a-f]{64}$/.test(addr.ownerPubkey)\n ) {\n throw new InvalidPayloadError(\n 'repoAddr.ownerPubkey must be a 64-char lowercase hex Nostr pubkey.'\n );\n }\n if (typeof addr.repoId !== 'string' || addr.repoId === '') {\n throw new InvalidPayloadError('repoAddr.repoId is required.');\n }\n return addr;\n}\n\n/** Serialize a PushPlan onto the wire (bigints → strings, Maps → records). */\nfunction serializePushPlan(plan: PushPlan): GitEstimateResponse {\n return {\n repoId: plan.repoId,\n refUpdates: plan.refUpdates,\n newRefs: plan.newRefs,\n headSymref: plan.headSymref,\n objects: plan.objects,\n knownShaToTxId: Object.fromEntries(plan.knownShaToTxId),\n announceNeeded: plan.announceNeeded,\n announcement: plan.announcement,\n estimate: serializeFeeEstimate(plan),\n };\n}\n\nfunction serializeFeeEstimate(plan: PushPlan): GitFeeEstimate {\n return {\n objectCount: plan.estimate.objectCount,\n totalObjectBytes: plan.estimate.totalObjectBytes,\n uploadFee: plan.estimate.uploadFee.toString(),\n eventCount: plan.estimate.eventCount,\n eventFees: plan.estimate.eventFees.toString(),\n totalFee: plan.estimate.totalFee.toString(),\n };\n}\n\n/** Serialize a PushResult onto the wire (bigints → strings, Maps → records). */\nfunction serializePushResult(\n plan: PushPlan,\n result: PushResult\n): GitPushResponse {\n return {\n repoId: plan.repoId,\n refUpdates: plan.refUpdates,\n uploads: result.uploads.map((u) => ({\n sha: u.sha,\n txId: u.txId,\n feePaid: u.feePaid.toString(),\n skipped: u.skipped,\n })),\n announceReceipt: result.announceReceipt\n ? {\n eventId: result.announceReceipt.eventId,\n feePaid: result.announceReceipt.feePaid.toString(),\n }\n : null,\n refsReceipt: {\n eventId: result.refsReceipt.eventId,\n feePaid: result.refsReceipt.feePaid.toString(),\n },\n arweaveMap: Object.fromEntries(result.arweaveMap),\n totalFeePaid: result.totalFeePaid.toString(),\n estimate: serializeFeeEstimate(plan),\n };\n}\n\n/** Current time in whole seconds (Nostr `created_at` unit). */\nfunction nowSeconds(): number {\n return Math.floor(Date.now() / 1000);\n}\n\n/** Resolve after `ms` (bounded wait for relay delivery in `query`). */\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Race a promise against a timeout, rejecting with `message` if it does not\n * settle in `ms`. The underlying work is NOT cancelled (it may complete in the\n * background) — this just bounds how long the caller waits, so a stalled chain\n * RPC fast-fails instead of blocking the control request (#199).\n */\nfunction withTimeout<T>(promise: Promise<T>, ms: number, message: string): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n const timer = setTimeout(() => reject(new Error(message)), ms);\n promise.then(\n (value) => {\n clearTimeout(timer);\n resolve(value);\n },\n (err) => {\n clearTimeout(timer);\n reject(err);\n }\n );\n });\n}\n\n/** NIP-01 filter match (kinds/authors/ids/since/until + `#<letter>` tag filters). */\nfunction matchesFilter(event: NostrEvent, filter: NostrFilter): boolean {\n if (filter.ids && !filter.ids.includes(event.id)) return false;\n if (filter.kinds && !filter.kinds.includes(event.kind)) return false;\n if (filter.authors && !filter.authors.includes(event.pubkey)) return false;\n if (filter.since !== undefined && event.created_at < filter.since) return false;\n if (filter.until !== undefined && event.created_at > filter.until) return false;\n for (const [key, values] of Object.entries(filter)) {\n if (!key.startsWith('#') || !Array.isArray(values)) continue;\n const letter = key.slice(1);\n const hit = event.tags.some(\n (t) => t[0] === letter && t[1] !== undefined && values.includes(t[1])\n );\n if (!hit) return false;\n }\n return true;\n}\n\n/** Validate that `raw` is an array of string arrays, returning it typed. */\nfunction normalizeTags(raw: unknown): string[][] {\n if (raw === undefined) return [];\n if (!Array.isArray(raw)) throw new InvalidPayloadError('tags must be an array.');\n return raw.map((tag, i) => {\n if (!Array.isArray(tag) || !tag.every((x) => typeof x === 'string')) {\n throw new InvalidPayloadError(`tags[${i}] must be an array of strings.`);\n }\n return tag as string[];\n });\n}\n\n/** Append `additions` to `base`, de-duping whole tags (for replaceable merges). */\nfunction mergeTags(base: string[][], additions: string[][]): string[][] {\n const seen = new Set(base.map((t) => JSON.stringify(t)));\n const out = [...base];\n for (const tag of additions) {\n const key = JSON.stringify(tag);\n if (!seen.has(key)) {\n seen.add(key);\n out.push(tag);\n }\n }\n return out;\n}\n\nfunction safe<T>(fn: () => T): T | undefined {\n try {\n return fn();\n } catch {\n return undefined;\n }\n}\n\nfunction errMsg(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n/** Filesystem-safe slug for a per-apex channel-store filename. */\nfunction sanitize(s: string): string {\n return s\n .replace(/[^a-zA-Z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 80);\n}\n","/**\n * NIP-34: Git Stuff\n * https://github.com/nostr-protocol/nips/blob/master/34.md\n *\n * Event kinds for decentralized code collaboration via Nostr.\n */\n\n/**\n * Repository Announcement (kind 30617)\n * Replaceable event announcing a Git repository's existence.\n * Contains clone URLs, maintainers, and metadata.\n */\nexport const REPOSITORY_ANNOUNCEMENT_KIND = 30617;\n\n/**\n * Patch (kind 1617)\n * Direct patch submission for files under 60KB.\n * Contains git format-patch output and commit metadata.\n */\nexport const PATCH_KIND = 1617;\n\n/**\n * Pull Request (kind 1618)\n * Larger submissions or branch-based changes.\n * References remote repository and commit range.\n */\nexport const PULL_REQUEST_KIND = 1618;\n\n/**\n * Pull Request Status Update (kind 1619)\n * Updates to existing pull requests.\n */\nexport const PR_STATUS_UPDATE_KIND = 1619;\n\n/**\n * Issue (kind 1621)\n * Bug reports and feature requests in Markdown format.\n */\nexport const ISSUE_KIND = 1621;\n\n/**\n * Status: Open (kind 1630)\n */\nexport const STATUS_OPEN_KIND = 1630;\n\n/**\n * Status: Applied/Merged (kind 1631)\n */\nexport const STATUS_APPLIED_KIND = 1631;\n\n/**\n * Status: Closed (kind 1632)\n */\nexport const STATUS_CLOSED_KIND = 1632;\n\n/**\n * Status: Draft (kind 1633)\n */\nexport const STATUS_DRAFT_KIND = 1633;\n\n/**\n * All NIP-34 event kinds\n */\nexport const NIP34_EVENT_KINDS = [\n REPOSITORY_ANNOUNCEMENT_KIND,\n PATCH_KIND,\n PULL_REQUEST_KIND,\n PR_STATUS_UPDATE_KIND,\n ISSUE_KIND,\n STATUS_OPEN_KIND,\n STATUS_APPLIED_KIND,\n STATUS_CLOSED_KIND,\n STATUS_DRAFT_KIND,\n] as const;\n\n/**\n * Check if an event kind is a NIP-34 event\n */\nexport function isNIP34Event(kind: number): boolean {\n return (NIP34_EVENT_KINDS as readonly number[]).includes(kind);\n}\n","import type { Event as NostrEvent } from 'nostr-tools/pure';\n\n/**\n * Helper to get tag value by name\n */\nexport function getTag(event: NostrEvent, tagName: string): string | undefined {\n return event.tags.find((t) => t[0] === tagName)?.[1];\n}\n\n/**\n * Helper to get all tag values by name\n */\nexport function getTags(event: NostrEvent, tagName: string): string[] {\n return event.tags\n .filter((t) => t[0] === tagName)\n .map((t) => t[1])\n .filter((v): v is string => v !== undefined);\n}\n\n/**\n * Repository Announcement Event (Kind 30617)\n */\nexport interface RepositoryAnnouncement extends NostrEvent {\n kind: 30617;\n tags: (\n | ['d', string] // repository identifier\n | ['name', string] // human-readable name\n | ['description', string] // repository description\n | ['web', string] // browsing URL\n | ['clone', string] // clone URL\n | ['relays', ...string[]] // preferred relays\n | ['r', string, 'euc'] // earliest unique commit\n | ['maintainers', ...string[]]\n )[];\n}\n\n/**\n * Patch Event (Kind 1617)\n */\nexport interface PatchEvent extends NostrEvent {\n kind: 1617;\n content: string; // git format-patch output\n tags: (\n | ['a', string] // repository reference (30617:pubkey:repo-id)\n | ['r', string] // earliest unique commit\n | ['p', string] // repository owner pubkey\n | ['commit', string] // current commit SHA\n | ['parent-commit', string] // parent commit SHA\n | ['commit-pgp-sig', string] // optional PGP signature\n | ['committer', string] // optional committer info\n | ['t', 'root' | 'reply']\n )[];\n}\n\n/**\n * Pull Request Event (Kind 1618)\n */\nexport interface PullRequestEvent extends NostrEvent {\n kind: 1618;\n tags: (\n | ['a', string] // repository reference (30617:pubkey:repo-id)\n | ['r', string] // earliest unique commit\n | ['p', string] // repository owner pubkey\n | ['clone', string] // contributor's clone URL\n | ['c', string] // commit tip SHA\n | ['merge-base', string] // merge base commit\n | ['subject', string] // PR title\n | ['t', 'root' | 'reply']\n )[];\n}\n\n/**\n * Issue Event (Kind 1621)\n */\nexport interface IssueEvent extends NostrEvent {\n kind: 1621;\n content: string; // Markdown issue body\n tags: (\n | ['a', string] // repository reference (30617:pubkey:repo-id)\n | ['p', string] // repository owner pubkey\n | ['subject', string] // issue title\n | ['t', string]\n )[];\n}\n\n/**\n * Status Event (Kinds 1630-1633)\n */\nexport interface StatusEvent extends NostrEvent {\n kind: 1630 | 1631 | 1632 | 1633; // open, applied, closed, draft\n tags: (\n | ['e', string] // event being updated (patch, PR, issue)\n | ['p', string]\n )[];\n}\n\n/**\n * Union type of all NIP-34 events\n */\nexport type NIP34Event =\n | RepositoryAnnouncement\n | PatchEvent\n | PullRequestEvent\n | IssueEvent\n | StatusEvent;\n\n/**\n * Parse repository reference from 'a' tag\n * Format: \"30617:pubkey:repo-id\"\n */\nexport interface RepositoryReference {\n kind: 30617;\n pubkey: string;\n repoId: string;\n}\n\nexport function parseRepositoryReference(aTag: string): RepositoryReference {\n const parts = aTag.split(':');\n if (parts.length !== 3 || parts[0] !== '30617' || !parts[1] || !parts[2]) {\n throw new Error(`Invalid repository reference format: ${aTag}`);\n }\n return {\n kind: 30617,\n pubkey: parts[1],\n repoId: parts[2],\n };\n}\n\n/**\n * Extract commit message from git format-patch content\n */\nexport function extractCommitMessage(patchContent: string): string {\n const lines = patchContent.split('\\n');\n const subjectLine = lines.find((line) => line.startsWith('Subject:'));\n if (!subjectLine) {\n return 'Applied patch from Nostr';\n }\n // Remove \"Subject: [PATCH]\" prefix\n return subjectLine\n .replace(/^Subject:\\s*\\[PATCH\\]\\s*/, '')\n .replace(/^Subject:\\s*/, '');\n}\n","/**\n * Forgejo API Client\n *\n * Wrapper around Forgejo REST API for repository and issue management.\n * Uses fetch API for HTTP requests (compatible with gitea-js SDK structure).\n */\n\nexport interface ForgejoConfig {\n /** Base URL of Forgejo instance (e.g., \"http://forgejo:3000\") */\n baseUrl: string;\n /** API token for authentication */\n token: string;\n /** Default owner/organization for repositories */\n defaultOwner?: string;\n}\n\nexport interface CreateRepositoryOptions {\n name: string;\n description?: string;\n private?: boolean;\n auto_init?: boolean;\n default_branch?: string;\n}\n\nexport interface CreatePullRequestOptions {\n owner: string;\n repo: string;\n title: string;\n head: string; // source branch\n base: string; // target branch\n body?: string;\n}\n\nexport interface CreateIssueOptions {\n owner: string;\n repo: string;\n title: string;\n body?: string;\n labels?: number[]; // label IDs\n}\n\nexport interface ForgejoRepository {\n id: number;\n name: string;\n full_name: string;\n description: string;\n html_url: string;\n clone_url: string;\n ssh_url: string;\n}\n\nexport interface ForgejoPullRequest {\n id: number;\n number: number;\n title: string;\n html_url: string;\n state: 'open' | 'closed';\n}\n\nexport interface ForgejoIssue {\n id: number;\n number: number;\n title: string;\n html_url: string;\n state: 'open' | 'closed';\n}\n\nexport interface CreateOrUpdateFileOptions {\n owner: string;\n repo: string;\n filepath: string;\n content: string; // base64 encoded\n message: string;\n branch?: string;\n sha?: string; // required for updates\n}\n\nexport interface ForgejoFileResponse {\n content: {\n name: string;\n path: string;\n sha: string;\n };\n commit: {\n sha: string;\n };\n}\n\n/**\n * Forgejo API Client\n */\nexport class ForgejoClient {\n private baseUrl: string;\n private token: string;\n private defaultOwner?: string;\n\n constructor(config: ForgejoConfig) {\n this.baseUrl = config.baseUrl.replace(/\\/$/, ''); // Remove trailing slash\n this.token = config.token;\n this.defaultOwner = config.defaultOwner;\n }\n\n /**\n * Make an authenticated API request\n */\n private async request<T>(\n method: string,\n path: string,\n body?: unknown\n ): Promise<T> {\n const url = `${this.baseUrl}/api/v1${path}`;\n const headers: Record<string, string> = {\n Authorization: `token ${this.token}`,\n 'Content-Type': 'application/json',\n };\n\n const response = await fetch(url, {\n method,\n headers,\n body: body ? JSON.stringify(body) : undefined,\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Forgejo API error (${response.status}): ${errorText}`);\n }\n\n return response.json() as Promise<T>;\n }\n\n /**\n * Create a new repository\n */\n async createRepository(\n options: CreateRepositoryOptions\n ): Promise<ForgejoRepository> {\n const owner = this.defaultOwner;\n if (!owner) {\n throw new Error('No default owner configured for repository creation');\n }\n\n return this.request<ForgejoRepository>('POST', '/user/repos', {\n name: options.name,\n description: options.description || '',\n private: options.private ?? false,\n auto_init: options.auto_init ?? true,\n default_branch: options.default_branch || 'main',\n });\n }\n\n /**\n * Create a pull request\n */\n async createPullRequest(\n options: CreatePullRequestOptions\n ): Promise<ForgejoPullRequest> {\n return this.request<ForgejoPullRequest>(\n 'POST',\n `/repos/${options.owner}/${options.repo}/pulls`,\n {\n title: options.title,\n head: options.head,\n base: options.base,\n body: options.body || '',\n }\n );\n }\n\n /**\n * Create an issue\n */\n async createIssue(options: CreateIssueOptions): Promise<ForgejoIssue> {\n return this.request<ForgejoIssue>(\n 'POST',\n `/repos/${options.owner}/${options.repo}/issues`,\n {\n title: options.title,\n body: options.body || '',\n labels: options.labels,\n }\n );\n }\n\n /**\n * Get repository information\n */\n async getRepository(owner: string, repo: string): Promise<ForgejoRepository> {\n return this.request<ForgejoRepository>('GET', `/repos/${owner}/${repo}`);\n }\n\n /**\n * Check if repository exists\n */\n async repositoryExists(owner: string, repo: string): Promise<boolean> {\n try {\n await this.getRepository(owner, repo);\n return true;\n } catch {\n return false;\n }\n }\n\n /**\n * Get clone URL for internal use (within Docker network)\n */\n getInternalCloneUrl(owner: string, repo: string): string {\n // Use internal Docker network URL\n return `${this.baseUrl}/${owner}/${repo}.git`;\n }\n\n /**\n * Get clone URL for external use\n */\n getExternalCloneUrl(repo: ForgejoRepository): string {\n return repo.clone_url;\n }\n\n /**\n * Create or update a file in a repository\n */\n async createOrUpdateFile(\n options: CreateOrUpdateFileOptions\n ): Promise<ForgejoFileResponse> {\n const path = `/repos/${options.owner}/${options.repo}/contents/${options.filepath}`;\n const body: Record<string, unknown> = {\n content: options.content,\n message: options.message,\n };\n\n if (options.branch) {\n body['branch'] = options.branch;\n }\n if (options.sha) {\n body['sha'] = options.sha;\n }\n\n // Use PUT for updates (when SHA provided), POST for creates\n const method = options.sha ? 'PUT' : 'POST';\n return this.request<ForgejoFileResponse>(method, path, body);\n }\n\n /**\n * Create a new branch\n */\n async createBranch(\n owner: string,\n repo: string,\n branchName: string,\n fromBranch = 'main'\n ): Promise<void> {\n // Create the new branch from the source branch\n // Forgejo API: POST /repos/{owner}/{repo}/branches\n await this.request('POST', `/repos/${owner}/${repo}/branches`, {\n new_branch_name: branchName,\n old_branch_name: fromBranch,\n });\n }\n\n /**\n * Get file content from repository\n */\n async getFileContent(\n owner: string,\n repo: string,\n filepath: string,\n branch?: string\n ): Promise<{ content: string; sha: string } | null> {\n try {\n const path = `/repos/${owner}/${repo}/contents/${filepath}${branch ? `?ref=${branch}` : ''}`;\n const response = await this.request<{\n content: string;\n sha: string;\n }>('GET', path);\n return response;\n } catch {\n return null;\n }\n }\n}\n","/**\n * NIP-34 Handler\n *\n * Processes NIP-34 events (Git stuff on Nostr) and executes corresponding\n * Git operations on a Forgejo instance.\n *\n * Flow:\n * 1. TOON receives NIP-34 event via ILP payment\n * 2. BLS validates payment and stores event\n * 3. BLS calls NIP34Handler.handleEvent()\n * 4. Handler maps event to Git operation\n * 5. Operation executes on Forgejo\n */\n\nimport type { Event as NostrEvent } from 'nostr-tools/pure';\nimport {\n ForgejoClient,\n type CreateRepositoryOptions,\n} from './ForgejoClient.js';\nimport {\n isNIP34Event,\n REPOSITORY_ANNOUNCEMENT_KIND,\n PATCH_KIND,\n PULL_REQUEST_KIND,\n ISSUE_KIND,\n} from './constants.js';\nimport {\n getTag,\n parseRepositoryReference,\n extractCommitMessage,\n type NIP34Event,\n} from './types.js';\n\nexport interface NIP34Config {\n /** Forgejo base URL (e.g., \"http://forgejo:3000\") */\n forgejoUrl: string;\n /** Forgejo API token */\n forgejoToken: string;\n /** Default owner/org for repositories */\n defaultOwner: string;\n /** Git commit identity configuration */\n gitConfig?: {\n userName: string;\n userEmail: string;\n };\n /** Enable verbose logging */\n verbose?: boolean;\n}\n\nexport interface HandleEventResult {\n success: boolean;\n operation:\n | 'repository'\n | 'patch'\n | 'pull_request'\n | 'issue'\n | 'status'\n | 'unsupported';\n message: string;\n metadata?: Record<string, unknown>;\n}\n\n/**\n * NIP-34 Event Handler\n *\n * Maps Nostr events to Git operations on Forgejo.\n */\nexport class NIP34Handler {\n private forgejo: ForgejoClient;\n private verbose: boolean;\n private defaultOwner: string;\n\n constructor(config: NIP34Config) {\n this.forgejo = new ForgejoClient({\n baseUrl: config.forgejoUrl,\n token: config.forgejoToken,\n defaultOwner: config.defaultOwner,\n });\n\n this.defaultOwner = config.defaultOwner;\n this.verbose = config.verbose ?? false;\n }\n\n /**\n * Handle a NIP-34 event\n *\n * This is the main entry point called by the BLS after storing an event.\n */\n async handleEvent(event: NostrEvent): Promise<HandleEventResult> {\n // Check if this is a NIP-34 event\n if (!isNIP34Event(event.kind)) {\n return {\n success: false,\n operation: 'unsupported',\n message: `Not a NIP-34 event (kind ${event.kind})`,\n };\n }\n\n this.log(\n `Handling NIP-34 event: kind=${event.kind} id=${event.id.substring(0, 8)}`\n );\n\n try {\n switch (event.kind) {\n case REPOSITORY_ANNOUNCEMENT_KIND:\n return await this.handleRepositoryAnnouncement(event as NIP34Event);\n\n case PATCH_KIND:\n return await this.handlePatch(event as NIP34Event);\n\n case PULL_REQUEST_KIND:\n return await this.handlePullRequest(event as NIP34Event);\n\n case ISSUE_KIND:\n return await this.handleIssue(event as NIP34Event);\n\n default:\n // Status events (1630-1633) - not yet implemented\n return {\n success: true,\n operation: 'status',\n message: `Status event kind ${event.kind} received (not yet implemented)`,\n };\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error';\n this.log(`Error handling event: ${message}`);\n return {\n success: false,\n operation: this.getOperationType(event.kind),\n message: `Failed to process event: ${message}`,\n };\n }\n }\n\n /**\n * Handle Repository Announcement (kind 30617)\n *\n * Creates a new repository in Forgejo.\n */\n private async handleRepositoryAnnouncement(\n event: NIP34Event\n ): Promise<HandleEventResult> {\n const repoId = getTag(event, 'd');\n const name = getTag(event, 'name') || repoId;\n const description = getTag(event, 'description');\n\n if (!repoId) {\n return {\n success: false,\n operation: 'repository',\n message: 'Missing required \"d\" tag (repository identifier)',\n };\n }\n\n // Extract just the repository name (remove owner prefix if present)\n // e.g., \"admin/repo-name\" -> \"repo-name\"\n const repoName = repoId.includes('/')\n ? (repoId.split('/').pop() ?? repoId)\n : repoId;\n\n this.log(`Creating repository: ${repoName}`);\n\n const options: CreateRepositoryOptions = {\n name: repoName,\n description: description || name,\n private: false,\n auto_init: true,\n };\n\n const repo = await this.forgejo.createRepository(options);\n\n this.log(`Repository created: ${repo.html_url}`);\n\n return {\n success: true,\n operation: 'repository',\n message: `Repository \"${repoName}\" created`,\n metadata: {\n repoId: repoName,\n htmlUrl: repo.html_url,\n cloneUrl: repo.clone_url,\n },\n };\n }\n\n /**\n * Handle Patch (kind 1617)\n *\n * Applies a patch to a repository via Forgejo API and creates a pull request.\n */\n private async handlePatch(event: NIP34Event): Promise<HandleEventResult> {\n const aTag = getTag(event, 'a');\n const patchContent = event.content;\n\n if (!aTag) {\n return {\n success: false,\n operation: 'patch',\n message: 'Missing required \"a\" tag (repository reference)',\n };\n }\n\n const repoRef = parseRepositoryReference(aTag);\n const owner = this.defaultOwner;\n // Extract just the repository name (remove owner prefix if present)\n const repoName = repoRef.repoId.includes('/')\n ? (repoRef.repoId.split('/').pop() ?? repoRef.repoId)\n : repoRef.repoId;\n\n // Check if repository exists\n const exists = await this.forgejo.repositoryExists(owner, repoName);\n if (!exists) {\n return {\n success: false,\n operation: 'patch',\n message: `Repository ${owner}/${repoName} does not exist`,\n };\n }\n\n this.log(`Applying patch to ${owner}/${repoName}`);\n\n // Parse patch to extract file changes\n const patchInfo = this.parsePatch(patchContent);\n if (!patchInfo) {\n return {\n success: false,\n operation: 'patch',\n message: 'Failed to parse patch content',\n };\n }\n\n const patchBranch = `patch-${event.id.substring(0, 8)}`;\n const commitMessage = extractCommitMessage(patchContent);\n\n try {\n // Create a new branch via API\n await this.forgejo.createBranch(owner, repoName, patchBranch, 'main');\n\n // Apply each file change via API\n for (const file of patchInfo.files) {\n const content = Buffer.from(file.content).toString('base64');\n\n // Check if file exists on the branch to get its SHA for updates\n const existingFile = await this.forgejo.getFileContent(\n owner,\n repoName,\n file.path,\n patchBranch\n );\n\n await this.forgejo.createOrUpdateFile({\n owner,\n repo: repoName,\n filepath: file.path,\n content,\n message: commitMessage,\n branch: patchBranch,\n sha: existingFile?.sha, // Include SHA if file exists (for update)\n });\n }\n\n // Create pull request via API\n const pr = await this.forgejo.createPullRequest({\n owner,\n repo: repoName,\n title: commitMessage,\n head: patchBranch,\n base: 'main',\n body: `Patch from Nostr event: ${event.id}\\n\\nAuthor: ${event.pubkey}`,\n });\n\n this.log(`Pull request created: ${pr.html_url}`);\n\n return {\n success: true,\n operation: 'patch',\n message: `Patch applied and PR #${pr.number} created`,\n metadata: {\n branch: patchBranch,\n prNumber: pr.number,\n prUrl: pr.html_url,\n },\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error';\n return {\n success: false,\n operation: 'patch',\n message: `Failed to apply patch: ${message}`,\n };\n }\n }\n\n /**\n * Handle Pull Request (kind 1618)\n *\n * Creates an issue documenting the pull request (simplified approach without git).\n */\n private async handlePullRequest(\n event: NIP34Event\n ): Promise<HandleEventResult> {\n const aTag = getTag(event, 'a');\n const cloneUrl = getTag(event, 'clone');\n const commitTip = getTag(event, 'c');\n const subject = getTag(event, 'subject');\n\n if (!aTag || !cloneUrl || !commitTip) {\n return {\n success: false,\n operation: 'pull_request',\n message: 'Missing required tags: a, clone, c',\n };\n }\n\n const repoRef = parseRepositoryReference(aTag);\n const owner = this.defaultOwner;\n // Extract just the repository name (remove owner prefix if present)\n const repoName = repoRef.repoId.includes('/')\n ? (repoRef.repoId.split('/').pop() ?? repoRef.repoId)\n : repoRef.repoId;\n\n this.log(`Creating PR issue for ${owner}/${repoName} from ${cloneUrl}`);\n\n // Create an issue documenting the pull request\n const issueBody = `\n**Pull Request from Nostr**\n\nA pull request was submitted via NIP-34 event.\n\n**Details:**\n- Source Repository: ${cloneUrl}\n- Commit: ${commitTip}\n- Nostr Event: ${event.id}\n- Author: ${event.pubkey}\n\n**To apply this pull request:**\n1. Clone the source repository: \\`git clone ${cloneUrl}\\`\n2. Fetch the specific commit: \\`git fetch origin ${commitTip}\\`\n3. Create a branch: \\`git checkout -b pr-${event.id.substring(0, 8)} ${commitTip}\\`\n4. Push to this repository and create a PR manually\n\n${event.content}\n`;\n\n const issue = await this.forgejo.createIssue({\n owner,\n repo: repoName,\n title: subject || `Pull Request from ${event.pubkey.substring(0, 8)}`,\n body: issueBody,\n });\n\n this.log(`PR issue created: ${issue.html_url}`);\n\n return {\n success: true,\n operation: 'pull_request',\n message: `PR documented as issue #${issue.number}`,\n metadata: {\n issueNumber: issue.number,\n issueUrl: issue.html_url,\n },\n };\n }\n\n /**\n * Handle Issue (kind 1621)\n *\n * Creates an issue in Forgejo.\n */\n private async handleIssue(event: NIP34Event): Promise<HandleEventResult> {\n const aTag = getTag(event, 'a');\n const subject = getTag(event, 'subject');\n const body = event.content;\n\n if (!aTag || !subject) {\n return {\n success: false,\n operation: 'issue',\n message: 'Missing required tags: a, subject',\n };\n }\n\n const repoRef = parseRepositoryReference(aTag);\n const owner = this.defaultOwner;\n // Extract just the repository name (remove owner prefix if present)\n const repoName = repoRef.repoId.includes('/')\n ? (repoRef.repoId.split('/').pop() ?? repoRef.repoId)\n : repoRef.repoId;\n\n this.log(`Creating issue in ${owner}/${repoName}`);\n\n const issue = await this.forgejo.createIssue({\n owner,\n repo: repoName,\n title: subject,\n body: `${body}\\n\\n---\\nSubmitted via Nostr event: ${event.id}\\nAuthor: ${event.pubkey}`,\n });\n\n this.log(`Issue created: ${issue.html_url}`);\n\n return {\n success: true,\n operation: 'issue',\n message: `Issue #${issue.number} created`,\n metadata: {\n issueNumber: issue.number,\n issueUrl: issue.html_url,\n },\n };\n }\n\n /**\n * Get operation type from event kind\n */\n private getOperationType(\n kind: number\n ):\n | 'repository'\n | 'patch'\n | 'pull_request'\n | 'issue'\n | 'status'\n | 'unsupported' {\n switch (kind) {\n case REPOSITORY_ANNOUNCEMENT_KIND:\n return 'repository';\n case PATCH_KIND:\n return 'patch';\n case PULL_REQUEST_KIND:\n return 'pull_request';\n case ISSUE_KIND:\n return 'issue';\n case 1630:\n case 1631:\n case 1632:\n case 1633:\n return 'status';\n default:\n return 'unsupported';\n }\n }\n\n /**\n * Parse git format-patch output to extract file changes\n */\n private parsePatch(\n patchContent: string\n ): { files: { path: string; content: string }[] } | null {\n try {\n const files: { path: string; content: string }[] = [];\n\n // Simple parser for git format-patch\n // Look for diff --git lines to identify files\n const diffPattern = /diff --git a\\/(.*?) b\\/(.*?)$/gm;\n const matches = [...patchContent.matchAll(diffPattern)];\n\n if (matches.length === 0) {\n return null;\n }\n\n for (const match of matches) {\n const filePath = match[2]; // Use the \"b/\" path (new file)\n if (!filePath) continue; // Skip if no path found\n\n // Extract the patch content for this file\n // For simplicity, we'll extract everything after the diff header\n // In a full implementation, we'd properly parse hunks and apply them\n const fileStart = match.index ?? 0;\n const nextMatch = matches[matches.indexOf(match) + 1];\n const fileEnd = nextMatch?.index ?? patchContent.length;\n const filePatch = patchContent.substring(fileStart, fileEnd);\n\n // For now, extract content lines (lines starting with +)\n // This is a simplified approach - a full parser would handle hunks properly\n const contentLines = filePatch\n .split('\\n')\n .filter((line) => line.startsWith('+') && !line.startsWith('+++'))\n .map((line) => line.substring(1));\n\n if (contentLines.length > 0) {\n files.push({\n path: filePath,\n content: contentLines.join('\\n'),\n });\n }\n }\n\n return files.length > 0 ? { files } : null;\n } catch (error) {\n this.log(\n `Error parsing patch: ${error instanceof Error ? error.message : 'Unknown'}`\n );\n return null;\n }\n }\n\n /**\n * Log message if verbose mode is enabled\n */\n private log(message: string): void {\n if (this.verbose) {\n console.log(`[NIP34] ${message}`);\n }\n }\n}\n","/**\n * Pure git object construction and SHA-1 envelope hashing.\n *\n * Promoted from `packages/rig-web/tests/e2e/seed/lib/git-builder.ts` (#223) —\n * the proven seed pipeline builders, now the core of the Git-to-TOON write\n * path. Everything here is pure: no network, no signing, no payments.\n * Upload/publish lives with the Publisher (#226).\n *\n * Git object format: `<type> <size>\\0<content>`. The SHA-1 is computed over\n * the full envelope (header + NUL + content); the `body` (content only) is\n * what gets uploaded to Arweave.\n */\n\nimport { createHash } from 'node:crypto';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** All git object types TOON can carry. */\nexport type GitObjectType = 'blob' | 'tree' | 'commit' | 'tag';\n\nexport interface GitObject {\n /** SHA-1 hex digest computed over full git envelope */\n sha: string;\n /** Full git object (header + null + content) */\n buffer: Buffer;\n /** Body only (content after the null byte) — this is what gets uploaded */\n body: Buffer;\n}\n\n/**\n * Maximum uploadable git object body size: 95KB safety margin under the\n * 100KB free tier (R10-005). Larger objects are a hard error in v1.\n */\nexport const MAX_OBJECT_SIZE = 95 * 1024;\n\n/**\n * Git's universal empty-blob SHA-1: the object for a zero-byte file.\n *\n * It is the ONLY zero-byte object git can produce (trees, commits, and tags\n * are never empty) and it always has this exact constant value. The store\n * rejects a zero-byte kind:5094 blob upload as malformed (F00), so `rig` never\n * uploads this object — it is skipped on push and synthesized locally on\n * clone/fetch (both keyed off an ACTUAL zero-length body, never a heuristic).\n */\nexport const EMPTY_BLOB_SHA = 'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391';\n\n// ---------------------------------------------------------------------------\n// Envelope hashing\n// ---------------------------------------------------------------------------\n\n/**\n * Wrap a raw object body in the git envelope (`<type> <size>\\0`) and compute\n * its SHA-1. This is exactly what `git hash-object -t <type>` does.\n */\nexport function hashGitObject(type: GitObjectType, body: Buffer): GitObject {\n const header = Buffer.from(`${type} ${body.length}\\0`);\n const fullObject = Buffer.concat([header, body]);\n const sha = createHash('sha1').update(fullObject).digest('hex');\n return { sha, buffer: fullObject, body };\n}\n\n// ---------------------------------------------------------------------------\n// Git object construction\n// ---------------------------------------------------------------------------\n\n/**\n * Construct a git blob object and compute its SHA-1.\n *\n * Format: blob <size>\\0<content>\n * SHA is over the full envelope; body is content only (for upload).\n */\nexport function createGitBlob(content: string): GitObject {\n return hashGitObject('blob', Buffer.from(content, 'utf-8'));\n}\n\n/**\n * Construct a git tree object from sorted entries.\n *\n * Format: tree <size>\\0<entries>\n * Each entry: <mode> <name>\\0<20-byte-raw-sha1>\n * Entries MUST be sorted by name (byte-wise).\n */\nexport function createGitTree(\n entries: { mode: string; name: string; sha: string }[]\n): GitObject {\n // Git sorts tree entries by raw byte order (NOT locale-aware)\n const sorted = [...entries].sort((a, b) =>\n a.name < b.name ? -1 : a.name > b.name ? 1 : 0\n );\n\n const entryBuffers: Buffer[] = [];\n for (const entry of sorted) {\n const modeAndName = Buffer.from(`${entry.mode} ${entry.name}\\0`);\n // Raw 20-byte SHA-1 (NOT hex)\n const rawSha = Buffer.from(entry.sha, 'hex');\n entryBuffers.push(Buffer.concat([modeAndName, rawSha]));\n }\n\n return hashGitObject('tree', Buffer.concat(entryBuffers));\n}\n\n/**\n * Construct a git commit object.\n *\n * Format: commit <size>\\0tree <tree-sha>\\n[parent ...]\\nauthor ...\\ncommitter ...\\n\\n<message>\n * Tree/parent SHAs are hex-encoded (40 chars) in commits, unlike tree entries.\n */\nexport function createGitCommit(opts: {\n treeSha: string;\n parentSha?: string;\n authorName: string;\n authorPubkey: string;\n message: string;\n timestamp: number;\n}): GitObject {\n const lines = [\n `tree ${opts.treeSha}`,\n ...(opts.parentSha ? [`parent ${opts.parentSha}`] : []),\n `author ${opts.authorName} <${opts.authorPubkey}@nostr> ${opts.timestamp} +0000`,\n `committer ${opts.authorName} <${opts.authorPubkey}@nostr> ${opts.timestamp} +0000`,\n '',\n opts.message,\n ];\n return hashGitObject('commit', Buffer.from(lines.join('\\n'), 'utf-8'));\n}\n\n/**\n * Construct an annotated git tag object.\n *\n * Format: tag <size>\\0object <sha>\\ntype <type>\\ntag <name>\\ntagger ...\\n\\n<message>\n * The tagged object is usually a commit, but git allows tagging any object\n * type (including another tag).\n */\nexport function createGitTag(opts: {\n /** SHA-1 of the object being tagged (hex, 40 chars) */\n objectSha: string;\n /** Type of the tagged object (usually 'commit') */\n objectType: GitObjectType;\n /** Tag name, e.g. 'v1.0.0' */\n tagName: string;\n taggerName: string;\n taggerPubkey: string;\n message: string;\n timestamp: number;\n}): GitObject {\n const lines = [\n `object ${opts.objectSha}`,\n `type ${opts.objectType}`,\n `tag ${opts.tagName}`,\n `tagger ${opts.taggerName} <${opts.taggerPubkey}@nostr> ${opts.timestamp} +0000`,\n '',\n opts.message,\n ];\n return hashGitObject('tag', Buffer.from(lines.join('\\n'), 'utf-8'));\n}\n","/**\n * NIP-34: Git Stuff\n * https://github.com/nostr-protocol/nips/blob/master/34.md\n *\n * Event kinds for decentralized code collaboration via Nostr.\n */\n\n/**\n * Repository Announcement (kind 30617)\n * Replaceable event announcing a Git repository's existence.\n * Contains clone URLs, maintainers, and metadata.\n */\nexport const REPOSITORY_ANNOUNCEMENT_KIND = 30617;\n\n/**\n * Patch (kind 1617)\n * Direct patch submission for files under 60KB.\n * Contains git format-patch output and commit metadata.\n */\nexport const PATCH_KIND = 1617;\n\n/**\n * Pull Request (kind 1618)\n * Larger submissions or branch-based changes.\n * References remote repository and commit range.\n */\nexport const PULL_REQUEST_KIND = 1618;\n\n/**\n * Pull Request Status Update (kind 1619)\n * Updates to existing pull requests.\n */\nexport const PR_STATUS_UPDATE_KIND = 1619;\n\n/**\n * Issue (kind 1621)\n * Bug reports and feature requests in Markdown format.\n */\nexport const ISSUE_KIND = 1621;\n\n/**\n * Status: Open (kind 1630)\n */\nexport const STATUS_OPEN_KIND = 1630;\n\n/**\n * Status: Applied/Merged (kind 1631)\n */\nexport const STATUS_APPLIED_KIND = 1631;\n\n/**\n * Status: Closed (kind 1632)\n */\nexport const STATUS_CLOSED_KIND = 1632;\n\n/**\n * Status: Draft (kind 1633)\n */\nexport const STATUS_DRAFT_KIND = 1633;\n\n/**\n * All NIP-34 event kinds\n */\nexport const NIP34_EVENT_KINDS = [\n REPOSITORY_ANNOUNCEMENT_KIND,\n PATCH_KIND,\n PULL_REQUEST_KIND,\n PR_STATUS_UPDATE_KIND,\n ISSUE_KIND,\n STATUS_OPEN_KIND,\n STATUS_APPLIED_KIND,\n STATUS_CLOSED_KIND,\n STATUS_DRAFT_KIND,\n] as const;\n\n/**\n * Check if an event kind is a NIP-34 event\n */\nexport function isNIP34Event(kind: number): boolean {\n return (NIP34_EVENT_KINDS as readonly number[]).includes(kind);\n}\n","import type { Event as NostrEvent } from 'nostr-tools/pure';\n\n/**\n * Helper to get tag value by name\n */\nexport function getTag(event: NostrEvent, tagName: string): string | undefined {\n return event.tags.find((t) => t[0] === tagName)?.[1];\n}\n\n/**\n * Helper to get all tag values by name\n */\nexport function getTags(event: NostrEvent, tagName: string): string[] {\n return event.tags\n .filter((t) => t[0] === tagName)\n .map((t) => t[1])\n .filter((v): v is string => v !== undefined);\n}\n\n/**\n * Repository Announcement Event (Kind 30617)\n */\nexport interface RepositoryAnnouncement extends NostrEvent {\n kind: 30617;\n tags: (\n | ['d', string] // repository identifier\n | ['name', string] // human-readable name\n | ['description', string] // repository description\n | ['web', string] // browsing URL\n | ['clone', string] // clone URL\n | ['relays', ...string[]] // preferred relays\n | ['r', string, 'euc'] // earliest unique commit\n | ['maintainers', ...string[]]\n )[];\n}\n\n/**\n * Patch Event (Kind 1617)\n */\nexport interface PatchEvent extends NostrEvent {\n kind: 1617;\n content: string; // git format-patch output\n tags: (\n | ['a', string] // repository reference (30617:pubkey:repo-id)\n | ['r', string] // earliest unique commit\n | ['p', string] // repository owner pubkey\n | ['commit', string] // current commit SHA\n | ['parent-commit', string] // parent commit SHA\n | ['commit-pgp-sig', string] // optional PGP signature\n | ['committer', string] // optional committer info\n | ['t', 'root' | 'reply']\n )[];\n}\n\n/**\n * Pull Request Event (Kind 1618)\n */\nexport interface PullRequestEvent extends NostrEvent {\n kind: 1618;\n tags: (\n | ['a', string] // repository reference (30617:pubkey:repo-id)\n | ['r', string] // earliest unique commit\n | ['p', string] // repository owner pubkey\n | ['clone', string] // contributor's clone URL\n | ['c', string] // commit tip SHA\n | ['merge-base', string] // merge base commit\n | ['subject', string] // PR title\n | ['t', 'root' | 'reply']\n )[];\n}\n\n/**\n * Issue Event (Kind 1621)\n */\nexport interface IssueEvent extends NostrEvent {\n kind: 1621;\n content: string; // Markdown issue body\n tags: (\n | ['a', string] // repository reference (30617:pubkey:repo-id)\n | ['p', string] // repository owner pubkey\n | ['subject', string] // issue title\n | ['t', string]\n )[];\n}\n\n/**\n * Status Event (Kinds 1630-1633)\n */\nexport interface StatusEvent extends NostrEvent {\n kind: 1630 | 1631 | 1632 | 1633; // open, applied, closed, draft\n tags: (\n | ['e', string] // event being updated (patch, PR, issue)\n | ['p', string]\n )[];\n}\n\n/**\n * Union type of all NIP-34 events\n */\nexport type NIP34Event =\n | RepositoryAnnouncement\n | PatchEvent\n | PullRequestEvent\n | IssueEvent\n | StatusEvent;\n\n/**\n * Parse repository reference from 'a' tag\n * Format: \"30617:pubkey:repo-id\"\n */\nexport interface RepositoryReference {\n kind: 30617;\n pubkey: string;\n repoId: string;\n}\n\nexport function parseRepositoryReference(aTag: string): RepositoryReference {\n const parts = aTag.split(':');\n if (parts.length !== 3 || parts[0] !== '30617' || !parts[1] || !parts[2]) {\n throw new Error(`Invalid repository reference format: ${aTag}`);\n }\n return {\n kind: 30617,\n pubkey: parts[1],\n repoId: parts[2],\n };\n}\n\n/**\n * Extract commit message from git format-patch content\n */\nexport function extractCommitMessage(patchContent: string): string {\n const lines = patchContent.split('\\n');\n const subjectLine = lines.find((line) => line.startsWith('Subject:'));\n if (!subjectLine) {\n return 'Applied patch from Nostr';\n }\n // Remove \"Subject: [PATCH]\" prefix\n return subjectLine\n .replace(/^Subject:\\s*\\[PATCH\\]\\s*/, '')\n .replace(/^Subject:\\s*/, '');\n}\n","/**\n * Forgejo API Client\n *\n * Wrapper around Forgejo REST API for repository and issue management.\n * Uses fetch API for HTTP requests (compatible with gitea-js SDK structure).\n */\n\nexport interface ForgejoConfig {\n /** Base URL of Forgejo instance (e.g., \"http://forgejo:3000\") */\n baseUrl: string;\n /** API token for authentication */\n token: string;\n /** Default owner/organization for repositories */\n defaultOwner?: string;\n}\n\nexport interface CreateRepositoryOptions {\n name: string;\n description?: string;\n private?: boolean;\n auto_init?: boolean;\n default_branch?: string;\n}\n\nexport interface CreatePullRequestOptions {\n owner: string;\n repo: string;\n title: string;\n head: string; // source branch\n base: string; // target branch\n body?: string;\n}\n\nexport interface CreateIssueOptions {\n owner: string;\n repo: string;\n title: string;\n body?: string;\n labels?: number[]; // label IDs\n}\n\nexport interface ForgejoRepository {\n id: number;\n name: string;\n full_name: string;\n description: string;\n html_url: string;\n clone_url: string;\n ssh_url: string;\n}\n\nexport interface ForgejoPullRequest {\n id: number;\n number: number;\n title: string;\n html_url: string;\n state: 'open' | 'closed';\n}\n\nexport interface ForgejoIssue {\n id: number;\n number: number;\n title: string;\n html_url: string;\n state: 'open' | 'closed';\n}\n\nexport interface CreateOrUpdateFileOptions {\n owner: string;\n repo: string;\n filepath: string;\n content: string; // base64 encoded\n message: string;\n branch?: string;\n sha?: string; // required for updates\n}\n\nexport interface ForgejoFileResponse {\n content: {\n name: string;\n path: string;\n sha: string;\n };\n commit: {\n sha: string;\n };\n}\n\n/**\n * Forgejo API Client\n */\nexport class ForgejoClient {\n private baseUrl: string;\n private token: string;\n private defaultOwner?: string;\n\n constructor(config: ForgejoConfig) {\n this.baseUrl = config.baseUrl.replace(/\\/$/, ''); // Remove trailing slash\n this.token = config.token;\n this.defaultOwner = config.defaultOwner;\n }\n\n /**\n * Make an authenticated API request\n */\n private async request<T>(\n method: string,\n path: string,\n body?: unknown\n ): Promise<T> {\n const url = `${this.baseUrl}/api/v1${path}`;\n const headers: Record<string, string> = {\n Authorization: `token ${this.token}`,\n 'Content-Type': 'application/json',\n };\n\n const response = await fetch(url, {\n method,\n headers,\n body: body ? JSON.stringify(body) : undefined,\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Forgejo API error (${response.status}): ${errorText}`);\n }\n\n return response.json() as Promise<T>;\n }\n\n /**\n * Create a new repository\n */\n async createRepository(\n options: CreateRepositoryOptions\n ): Promise<ForgejoRepository> {\n const owner = this.defaultOwner;\n if (!owner) {\n throw new Error('No default owner configured for repository creation');\n }\n\n return this.request<ForgejoRepository>('POST', '/user/repos', {\n name: options.name,\n description: options.description || '',\n private: options.private ?? false,\n auto_init: options.auto_init ?? true,\n default_branch: options.default_branch || 'main',\n });\n }\n\n /**\n * Create a pull request\n */\n async createPullRequest(\n options: CreatePullRequestOptions\n ): Promise<ForgejoPullRequest> {\n return this.request<ForgejoPullRequest>(\n 'POST',\n `/repos/${options.owner}/${options.repo}/pulls`,\n {\n title: options.title,\n head: options.head,\n base: options.base,\n body: options.body || '',\n }\n );\n }\n\n /**\n * Create an issue\n */\n async createIssue(options: CreateIssueOptions): Promise<ForgejoIssue> {\n return this.request<ForgejoIssue>(\n 'POST',\n `/repos/${options.owner}/${options.repo}/issues`,\n {\n title: options.title,\n body: options.body || '',\n labels: options.labels,\n }\n );\n }\n\n /**\n * Get repository information\n */\n async getRepository(owner: string, repo: string): Promise<ForgejoRepository> {\n return this.request<ForgejoRepository>('GET', `/repos/${owner}/${repo}`);\n }\n\n /**\n * Check if repository exists\n */\n async repositoryExists(owner: string, repo: string): Promise<boolean> {\n try {\n await this.getRepository(owner, repo);\n return true;\n } catch {\n return false;\n }\n }\n\n /**\n * Get clone URL for internal use (within Docker network)\n */\n getInternalCloneUrl(owner: string, repo: string): string {\n // Use internal Docker network URL\n return `${this.baseUrl}/${owner}/${repo}.git`;\n }\n\n /**\n * Get clone URL for external use\n */\n getExternalCloneUrl(repo: ForgejoRepository): string {\n return repo.clone_url;\n }\n\n /**\n * Create or update a file in a repository\n */\n async createOrUpdateFile(\n options: CreateOrUpdateFileOptions\n ): Promise<ForgejoFileResponse> {\n const path = `/repos/${options.owner}/${options.repo}/contents/${options.filepath}`;\n const body: Record<string, unknown> = {\n content: options.content,\n message: options.message,\n };\n\n if (options.branch) {\n body['branch'] = options.branch;\n }\n if (options.sha) {\n body['sha'] = options.sha;\n }\n\n // Use PUT for updates (when SHA provided), POST for creates\n const method = options.sha ? 'PUT' : 'POST';\n return this.request<ForgejoFileResponse>(method, path, body);\n }\n\n /**\n * Create a new branch\n */\n async createBranch(\n owner: string,\n repo: string,\n branchName: string,\n fromBranch = 'main'\n ): Promise<void> {\n // Create the new branch from the source branch\n // Forgejo API: POST /repos/{owner}/{repo}/branches\n await this.request('POST', `/repos/${owner}/${repo}/branches`, {\n new_branch_name: branchName,\n old_branch_name: fromBranch,\n });\n }\n\n /**\n * Get file content from repository\n */\n async getFileContent(\n owner: string,\n repo: string,\n filepath: string,\n branch?: string\n ): Promise<{ content: string; sha: string } | null> {\n try {\n const path = `/repos/${owner}/${repo}/contents/${filepath}${branch ? `?ref=${branch}` : ''}`;\n const response = await this.request<{\n content: string;\n sha: string;\n }>('GET', path);\n return response;\n } catch {\n return null;\n }\n }\n}\n","/**\n * NIP-34 Handler\n *\n * Processes NIP-34 events (Git stuff on Nostr) and executes corresponding\n * Git operations on a Forgejo instance.\n *\n * Flow:\n * 1. TOON receives NIP-34 event via ILP payment\n * 2. BLS validates payment and stores event\n * 3. BLS calls NIP34Handler.handleEvent()\n * 4. Handler maps event to Git operation\n * 5. Operation executes on Forgejo\n */\n\nimport type { Event as NostrEvent } from 'nostr-tools/pure';\nimport {\n ForgejoClient,\n type CreateRepositoryOptions,\n} from './ForgejoClient.js';\nimport {\n isNIP34Event,\n REPOSITORY_ANNOUNCEMENT_KIND,\n PATCH_KIND,\n PULL_REQUEST_KIND,\n ISSUE_KIND,\n} from './constants.js';\nimport {\n getTag,\n parseRepositoryReference,\n extractCommitMessage,\n type NIP34Event,\n} from './types.js';\n\nexport interface NIP34Config {\n /** Forgejo base URL (e.g., \"http://forgejo:3000\") */\n forgejoUrl: string;\n /** Forgejo API token */\n forgejoToken: string;\n /** Default owner/org for repositories */\n defaultOwner: string;\n /** Git commit identity configuration */\n gitConfig?: {\n userName: string;\n userEmail: string;\n };\n /** Enable verbose logging */\n verbose?: boolean;\n}\n\nexport interface HandleEventResult {\n success: boolean;\n operation:\n | 'repository'\n | 'patch'\n | 'pull_request'\n | 'issue'\n | 'status'\n | 'unsupported';\n message: string;\n metadata?: Record<string, unknown>;\n}\n\n/**\n * NIP-34 Event Handler\n *\n * Maps Nostr events to Git operations on Forgejo.\n */\nexport class NIP34Handler {\n private forgejo: ForgejoClient;\n private verbose: boolean;\n private defaultOwner: string;\n\n constructor(config: NIP34Config) {\n this.forgejo = new ForgejoClient({\n baseUrl: config.forgejoUrl,\n token: config.forgejoToken,\n defaultOwner: config.defaultOwner,\n });\n\n this.defaultOwner = config.defaultOwner;\n this.verbose = config.verbose ?? false;\n }\n\n /**\n * Handle a NIP-34 event\n *\n * This is the main entry point called by the BLS after storing an event.\n */\n async handleEvent(event: NostrEvent): Promise<HandleEventResult> {\n // Check if this is a NIP-34 event\n if (!isNIP34Event(event.kind)) {\n return {\n success: false,\n operation: 'unsupported',\n message: `Not a NIP-34 event (kind ${event.kind})`,\n };\n }\n\n this.log(\n `Handling NIP-34 event: kind=${event.kind} id=${event.id.substring(0, 8)}`\n );\n\n try {\n switch (event.kind) {\n case REPOSITORY_ANNOUNCEMENT_KIND:\n return await this.handleRepositoryAnnouncement(event as NIP34Event);\n\n case PATCH_KIND:\n return await this.handlePatch(event as NIP34Event);\n\n case PULL_REQUEST_KIND:\n return await this.handlePullRequest(event as NIP34Event);\n\n case ISSUE_KIND:\n return await this.handleIssue(event as NIP34Event);\n\n default:\n // Status events (1630-1633) - not yet implemented\n return {\n success: true,\n operation: 'status',\n message: `Status event kind ${event.kind} received (not yet implemented)`,\n };\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error';\n this.log(`Error handling event: ${message}`);\n return {\n success: false,\n operation: this.getOperationType(event.kind),\n message: `Failed to process event: ${message}`,\n };\n }\n }\n\n /**\n * Handle Repository Announcement (kind 30617)\n *\n * Creates a new repository in Forgejo.\n */\n private async handleRepositoryAnnouncement(\n event: NIP34Event\n ): Promise<HandleEventResult> {\n const repoId = getTag(event, 'd');\n const name = getTag(event, 'name') || repoId;\n const description = getTag(event, 'description');\n\n if (!repoId) {\n return {\n success: false,\n operation: 'repository',\n message: 'Missing required \"d\" tag (repository identifier)',\n };\n }\n\n // Extract just the repository name (remove owner prefix if present)\n // e.g., \"admin/repo-name\" -> \"repo-name\"\n const repoName = repoId.includes('/')\n ? (repoId.split('/').pop() ?? repoId)\n : repoId;\n\n this.log(`Creating repository: ${repoName}`);\n\n const options: CreateRepositoryOptions = {\n name: repoName,\n description: description || name,\n private: false,\n auto_init: true,\n };\n\n const repo = await this.forgejo.createRepository(options);\n\n this.log(`Repository created: ${repo.html_url}`);\n\n return {\n success: true,\n operation: 'repository',\n message: `Repository \"${repoName}\" created`,\n metadata: {\n repoId: repoName,\n htmlUrl: repo.html_url,\n cloneUrl: repo.clone_url,\n },\n };\n }\n\n /**\n * Handle Patch (kind 1617)\n *\n * Applies a patch to a repository via Forgejo API and creates a pull request.\n */\n private async handlePatch(event: NIP34Event): Promise<HandleEventResult> {\n const aTag = getTag(event, 'a');\n const patchContent = event.content;\n\n if (!aTag) {\n return {\n success: false,\n operation: 'patch',\n message: 'Missing required \"a\" tag (repository reference)',\n };\n }\n\n const repoRef = parseRepositoryReference(aTag);\n const owner = this.defaultOwner;\n // Extract just the repository name (remove owner prefix if present)\n const repoName = repoRef.repoId.includes('/')\n ? (repoRef.repoId.split('/').pop() ?? repoRef.repoId)\n : repoRef.repoId;\n\n // Check if repository exists\n const exists = await this.forgejo.repositoryExists(owner, repoName);\n if (!exists) {\n return {\n success: false,\n operation: 'patch',\n message: `Repository ${owner}/${repoName} does not exist`,\n };\n }\n\n this.log(`Applying patch to ${owner}/${repoName}`);\n\n // Parse patch to extract file changes\n const patchInfo = this.parsePatch(patchContent);\n if (!patchInfo) {\n return {\n success: false,\n operation: 'patch',\n message: 'Failed to parse patch content',\n };\n }\n\n const patchBranch = `patch-${event.id.substring(0, 8)}`;\n const commitMessage = extractCommitMessage(patchContent);\n\n try {\n // Create a new branch via API\n await this.forgejo.createBranch(owner, repoName, patchBranch, 'main');\n\n // Apply each file change via API\n for (const file of patchInfo.files) {\n const content = Buffer.from(file.content).toString('base64');\n\n // Check if file exists on the branch to get its SHA for updates\n const existingFile = await this.forgejo.getFileContent(\n owner,\n repoName,\n file.path,\n patchBranch\n );\n\n await this.forgejo.createOrUpdateFile({\n owner,\n repo: repoName,\n filepath: file.path,\n content,\n message: commitMessage,\n branch: patchBranch,\n sha: existingFile?.sha, // Include SHA if file exists (for update)\n });\n }\n\n // Create pull request via API\n const pr = await this.forgejo.createPullRequest({\n owner,\n repo: repoName,\n title: commitMessage,\n head: patchBranch,\n base: 'main',\n body: `Patch from Nostr event: ${event.id}\\n\\nAuthor: ${event.pubkey}`,\n });\n\n this.log(`Pull request created: ${pr.html_url}`);\n\n return {\n success: true,\n operation: 'patch',\n message: `Patch applied and PR #${pr.number} created`,\n metadata: {\n branch: patchBranch,\n prNumber: pr.number,\n prUrl: pr.html_url,\n },\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error';\n return {\n success: false,\n operation: 'patch',\n message: `Failed to apply patch: ${message}`,\n };\n }\n }\n\n /**\n * Handle Pull Request (kind 1618)\n *\n * Creates an issue documenting the pull request (simplified approach without git).\n */\n private async handlePullRequest(\n event: NIP34Event\n ): Promise<HandleEventResult> {\n const aTag = getTag(event, 'a');\n const cloneUrl = getTag(event, 'clone');\n const commitTip = getTag(event, 'c');\n const subject = getTag(event, 'subject');\n\n if (!aTag || !cloneUrl || !commitTip) {\n return {\n success: false,\n operation: 'pull_request',\n message: 'Missing required tags: a, clone, c',\n };\n }\n\n const repoRef = parseRepositoryReference(aTag);\n const owner = this.defaultOwner;\n // Extract just the repository name (remove owner prefix if present)\n const repoName = repoRef.repoId.includes('/')\n ? (repoRef.repoId.split('/').pop() ?? repoRef.repoId)\n : repoRef.repoId;\n\n this.log(`Creating PR issue for ${owner}/${repoName} from ${cloneUrl}`);\n\n // Create an issue documenting the pull request\n const issueBody = `\n**Pull Request from Nostr**\n\nA pull request was submitted via NIP-34 event.\n\n**Details:**\n- Source Repository: ${cloneUrl}\n- Commit: ${commitTip}\n- Nostr Event: ${event.id}\n- Author: ${event.pubkey}\n\n**To apply this pull request:**\n1. Clone the source repository: \\`git clone ${cloneUrl}\\`\n2. Fetch the specific commit: \\`git fetch origin ${commitTip}\\`\n3. Create a branch: \\`git checkout -b pr-${event.id.substring(0, 8)} ${commitTip}\\`\n4. Push to this repository and create a PR manually\n\n${event.content}\n`;\n\n const issue = await this.forgejo.createIssue({\n owner,\n repo: repoName,\n title: subject || `Pull Request from ${event.pubkey.substring(0, 8)}`,\n body: issueBody,\n });\n\n this.log(`PR issue created: ${issue.html_url}`);\n\n return {\n success: true,\n operation: 'pull_request',\n message: `PR documented as issue #${issue.number}`,\n metadata: {\n issueNumber: issue.number,\n issueUrl: issue.html_url,\n },\n };\n }\n\n /**\n * Handle Issue (kind 1621)\n *\n * Creates an issue in Forgejo.\n */\n private async handleIssue(event: NIP34Event): Promise<HandleEventResult> {\n const aTag = getTag(event, 'a');\n const subject = getTag(event, 'subject');\n const body = event.content;\n\n if (!aTag || !subject) {\n return {\n success: false,\n operation: 'issue',\n message: 'Missing required tags: a, subject',\n };\n }\n\n const repoRef = parseRepositoryReference(aTag);\n const owner = this.defaultOwner;\n // Extract just the repository name (remove owner prefix if present)\n const repoName = repoRef.repoId.includes('/')\n ? (repoRef.repoId.split('/').pop() ?? repoRef.repoId)\n : repoRef.repoId;\n\n this.log(`Creating issue in ${owner}/${repoName}`);\n\n const issue = await this.forgejo.createIssue({\n owner,\n repo: repoName,\n title: subject,\n body: `${body}\\n\\n---\\nSubmitted via Nostr event: ${event.id}\\nAuthor: ${event.pubkey}`,\n });\n\n this.log(`Issue created: ${issue.html_url}`);\n\n return {\n success: true,\n operation: 'issue',\n message: `Issue #${issue.number} created`,\n metadata: {\n issueNumber: issue.number,\n issueUrl: issue.html_url,\n },\n };\n }\n\n /**\n * Get operation type from event kind\n */\n private getOperationType(\n kind: number\n ):\n | 'repository'\n | 'patch'\n | 'pull_request'\n | 'issue'\n | 'status'\n | 'unsupported' {\n switch (kind) {\n case REPOSITORY_ANNOUNCEMENT_KIND:\n return 'repository';\n case PATCH_KIND:\n return 'patch';\n case PULL_REQUEST_KIND:\n return 'pull_request';\n case ISSUE_KIND:\n return 'issue';\n case 1630:\n case 1631:\n case 1632:\n case 1633:\n return 'status';\n default:\n return 'unsupported';\n }\n }\n\n /**\n * Parse git format-patch output to extract file changes\n */\n private parsePatch(\n patchContent: string\n ): { files: { path: string; content: string }[] } | null {\n try {\n const files: { path: string; content: string }[] = [];\n\n // Simple parser for git format-patch\n // Look for diff --git lines to identify files\n const diffPattern = /diff --git a\\/(.*?) b\\/(.*?)$/gm;\n const matches = [...patchContent.matchAll(diffPattern)];\n\n if (matches.length === 0) {\n return null;\n }\n\n for (const match of matches) {\n const filePath = match[2]; // Use the \"b/\" path (new file)\n if (!filePath) continue; // Skip if no path found\n\n // Extract the patch content for this file\n // For simplicity, we'll extract everything after the diff header\n // In a full implementation, we'd properly parse hunks and apply them\n const fileStart = match.index ?? 0;\n const nextMatch = matches[matches.indexOf(match) + 1];\n const fileEnd = nextMatch?.index ?? patchContent.length;\n const filePatch = patchContent.substring(fileStart, fileEnd);\n\n // For now, extract content lines (lines starting with +)\n // This is a simplified approach - a full parser would handle hunks properly\n const contentLines = filePatch\n .split('\\n')\n .filter((line) => line.startsWith('+') && !line.startsWith('+++'))\n .map((line) => line.substring(1));\n\n if (contentLines.length > 0) {\n files.push({\n path: filePath,\n content: contentLines.join('\\n'),\n });\n }\n }\n\n return files.length > 0 ? { files } : null;\n } catch (error) {\n this.log(\n `Error parsing patch: ${error instanceof Error ? error.message : 'Unknown'}`\n );\n return null;\n }\n }\n\n /**\n * Log message if verbose mode is enabled\n */\n private log(message: string): void {\n if (this.verbose) {\n console.log(`[NIP34] ${message}`);\n }\n }\n}\n","/**\n * Pure NIP-34 event builders for the Git-to-TOON write path.\n *\n * Promoted from `packages/rig-web/tests/e2e/seed/lib/event-builders.ts` (#223).\n * All builders return UnsignedEvent — the caller signs with their keypair\n * via finalizeEvent() and publishes through a Publisher (#226). Tag\n * structures follow the NIP-34 spec and `@toon-protocol/core/nip34`.\n */\n\nimport {\n ISSUE_KIND,\n PATCH_KIND,\n REPOSITORY_ANNOUNCEMENT_KIND,\n} from '@toon-protocol/core/nip34';\nimport type {\n STATUS_APPLIED_KIND,\n STATUS_CLOSED_KIND,\n STATUS_DRAFT_KIND,\n STATUS_OPEN_KIND,\n} from '@toon-protocol/core/nip34';\n\n// Kinds not (yet) exported by @toon-protocol/core/nip34:\n/** Repository State (refs) — replaceable, pairs with kind:30617 via `d` tag. */\nexport const REPOSITORY_STATE_KIND = 30618;\n/** Comment on an issue or patch (NIP-22 style threading within NIP-34). */\nexport const COMMENT_KIND = 1622;\n\n// ---------------------------------------------------------------------------\n// UnsignedEvent type (subset of nostr-tools — no id, sig, or pubkey)\n// ---------------------------------------------------------------------------\n\nexport interface UnsignedEvent {\n kind: number;\n content: string;\n tags: string[][];\n created_at: number;\n}\n\n// ---------------------------------------------------------------------------\n// kind:30617 — Repository Announcement (+ maintainer authority, #287)\n// ---------------------------------------------------------------------------\n\n/**\n * NIP-34 tag naming the repo's declared maintainers: one multi-valued tag\n * `[\"maintainers\", \"<hex-pubkey>\", \"<hex-pubkey>\", …]` on the kind:30617\n * announcement (mirrors the spec's multi-valued `relays` tag). The repo\n * OWNER — the announcement event's own pubkey — is ALWAYS an implicit\n * maintainer and need not be listed. Consumers derive an issue/PR's status\n * ONLY from kind:1630-1633 events signed by owner ∪ maintainers (#287): the\n * relay is permissionless, so this is the CONSUMER-side authority filter.\n */\nexport const MAINTAINERS_TAG = 'maintainers';\n\nconst HEX64 = /^[0-9a-f]{64}$/;\n\n/**\n * Collect the declared maintainer pubkeys (lowercased hex) from a kind:30617\n * event's tags. Tolerant of repeated `maintainers` tags and non-hex noise —\n * only 64-char hex values survive. Does NOT include the owner (implicit).\n */\nexport function parseMaintainers(tags: string[][]): string[] {\n const out: string[] = [];\n const seen = new Set<string>();\n for (const tag of tags) {\n if (tag[0] !== MAINTAINERS_TAG) continue;\n for (const value of tag.slice(1)) {\n const hex = value.toLowerCase();\n if (HEX64.test(hex) && !seen.has(hex)) {\n seen.add(hex);\n out.push(hex);\n }\n }\n }\n return out;\n}\n\n/**\n * The set of pubkeys whose kind:1630-1633 status events are authoritative for\n * a repo: the owner (always) ∪ the declared maintainers (from the 30617's\n * `maintainers` tag). All values are lowercased hex.\n */\nexport function authorizedStatusAuthors(\n ownerPubkey: string,\n repoAnnouncementTags: string[][]\n): Set<string> {\n return new Set([\n ownerPubkey.toLowerCase(),\n ...parseMaintainers(repoAnnouncementTags),\n ]);\n}\n\n/**\n * Build a kind:30617 repository announcement event.\n *\n * @param repoId - Repository identifier (d tag)\n * @param name - Human-readable repository name\n * @param description - Repository description\n * @param maintainers - Optional declared maintainer pubkeys (hex). Emitted as\n * a single `[\"maintainers\", …]` tag when non-empty; duplicate and non-64-hex\n * values are dropped. The owner (the signer) is an implicit maintainer and\n * need not be listed — if passed it is emitted, which is harmless since the\n * owner is authorized regardless. See {@link MAINTAINERS_TAG}.\n */\nexport function buildRepoAnnouncement(\n repoId: string,\n name: string,\n description: string,\n maintainers: string[] = []\n): UnsignedEvent {\n const tags: string[][] = [\n ['d', repoId],\n ['name', name],\n ['description', description],\n ];\n const declared: string[] = [];\n const seen = new Set<string>();\n for (const value of maintainers) {\n const hex = value.toLowerCase();\n if (HEX64.test(hex) && !seen.has(hex)) {\n seen.add(hex);\n declared.push(hex);\n }\n }\n if (declared.length > 0) {\n tags.push([MAINTAINERS_TAG, ...declared]);\n }\n return {\n kind: REPOSITORY_ANNOUNCEMENT_KIND,\n content: '',\n tags,\n created_at: Math.floor(Date.now() / 1000),\n };\n}\n\n// ---------------------------------------------------------------------------\n// kind:30618 — Repository Refs/State\n// ---------------------------------------------------------------------------\n\n/**\n * Build a kind:30618 repository refs/state event.\n *\n * @param repoId - Repository identifier (d tag, matches kind:30617)\n * @param refs - Map of ref paths to commit SHAs (e.g., { 'refs/heads/main': 'abc123' })\n * @param arweaveMap - Map of git SHAs to Arweave transaction IDs\n */\nexport function buildRepoRefs(\n repoId: string,\n refs: Record<string, string>,\n arweaveMap: Record<string, string> = {}\n): UnsignedEvent {\n const tags: string[][] = [['d', repoId]];\n\n // Add ref tags\n for (const [refPath, commitSha] of Object.entries(refs)) {\n tags.push(['r', refPath, commitSha]);\n }\n\n // Default HEAD to first ref (typically refs/heads/main)\n const firstRef = Object.keys(refs)[0];\n if (firstRef) {\n tags.push(['HEAD', `ref: ${firstRef}`]);\n }\n\n // Add arweave SHA-to-txId mapping tags\n for (const [sha, txId] of Object.entries(arweaveMap)) {\n tags.push(['arweave', sha, txId]);\n }\n\n return {\n kind: REPOSITORY_STATE_KIND,\n content: '',\n tags,\n created_at: Math.floor(Date.now() / 1000),\n };\n}\n\n// ---------------------------------------------------------------------------\n// kind:1621 — Issue\n// ---------------------------------------------------------------------------\n\n/**\n * Build a kind:1621 issue event.\n *\n * @param repoOwnerPubkey - Pubkey of the repository owner\n * @param repoId - Repository identifier\n * @param title - Issue title (subject tag)\n * @param body - Issue body (Markdown content)\n * @param labels - Optional labels (t tags)\n */\nexport function buildIssue(\n repoOwnerPubkey: string,\n repoId: string,\n title: string,\n body: string,\n labels: string[] = []\n): UnsignedEvent {\n const tags: string[][] = [\n ['a', `${REPOSITORY_ANNOUNCEMENT_KIND}:${repoOwnerPubkey}:${repoId}`],\n ['p', repoOwnerPubkey],\n ['subject', title],\n ...labels.map((label) => ['t', label]),\n ];\n\n return {\n kind: ISSUE_KIND,\n content: body,\n tags,\n created_at: Math.floor(Date.now() / 1000),\n };\n}\n\n// ---------------------------------------------------------------------------\n// kind:1622 — Comment (on issue or PR)\n// ---------------------------------------------------------------------------\n\n/**\n * Build a kind:1622 comment event.\n *\n * @param repoOwnerPubkey - Pubkey of the repository owner\n * @param repoId - Repository identifier\n * @param issueOrPrEventId - Event ID of the issue or PR being commented on\n * @param authorPubkey - Pubkey of the issue/PR author (NIP-34 `p` tag for threading), NOT the comment author\n * @param body - Comment body (Markdown content)\n * @param marker - Event reference marker: 'root' or 'reply' (default: 'reply')\n */\nexport function buildComment(\n repoOwnerPubkey: string,\n repoId: string,\n issueOrPrEventId: string,\n authorPubkey: string,\n body: string,\n marker: 'root' | 'reply' = 'reply'\n): UnsignedEvent {\n return {\n kind: COMMENT_KIND,\n content: body,\n tags: [\n ['a', `${REPOSITORY_ANNOUNCEMENT_KIND}:${repoOwnerPubkey}:${repoId}`],\n ['e', issueOrPrEventId, '', marker],\n ['p', authorPubkey],\n ],\n created_at: Math.floor(Date.now() / 1000),\n };\n}\n\n// ---------------------------------------------------------------------------\n// kind:1617 — Patch / PR\n// ---------------------------------------------------------------------------\n\n/**\n * Build a kind:1617 patch event.\n *\n * The PR body/description travels in a dedicated `description` tag, NEVER in\n * `content` (#280): `content` is real `git format-patch` output that readers\n * pipe straight into `git am`, and git's patch-format detection hard-fails on\n * any leading prose (verified: \"Patch format detection failed.\"). The tag\n * route keeps `git am` consumption intact while `rig pr show` and the\n * rig-web/views `parsePR` renderers surface the description.\n *\n * @param repoOwnerPubkey - Pubkey of the repository owner\n * @param repoId - Repository identifier\n * @param title - Patch/PR title (subject tag)\n * @param commits - Array of { sha, parentSha } for commit and parent-commit tags\n * @param branchTag - Branch name for the t tag\n * @param content - Real `git format-patch` text (NIP-34 patch body); defaults\n * to '' for callers that only reference commits by tag\n * @param description - PR body/cover text (`description` tag) — kept out of\n * `content` so `git am` still applies it\n */\nexport function buildPatch(\n repoOwnerPubkey: string,\n repoId: string,\n title: string,\n commits: { sha: string; parentSha: string }[],\n branchTag?: string,\n content = '',\n description?: string\n): UnsignedEvent {\n const tags: string[][] = [\n ['a', `${REPOSITORY_ANNOUNCEMENT_KIND}:${repoOwnerPubkey}:${repoId}`],\n ['p', repoOwnerPubkey],\n ['subject', title],\n ];\n\n if (description !== undefined && description !== '') {\n tags.push(['description', description]);\n }\n\n for (const commit of commits) {\n tags.push(['commit', commit.sha]);\n tags.push(['parent-commit', commit.parentSha]);\n }\n\n if (branchTag) {\n tags.push(['t', branchTag]);\n }\n\n return {\n kind: PATCH_KIND,\n content,\n tags,\n created_at: Math.floor(Date.now() / 1000),\n };\n}\n\n// ---------------------------------------------------------------------------\n// kind:1630-1633 — Status\n// ---------------------------------------------------------------------------\n\n/** Status kinds: 1630 open, 1631 applied/merged, 1632 closed, 1633 draft. */\nexport type StatusKind =\n | typeof STATUS_OPEN_KIND\n | typeof STATUS_APPLIED_KIND\n | typeof STATUS_CLOSED_KIND\n | typeof STATUS_DRAFT_KIND;\n\n/**\n * Build a status event (kind 1630-1633).\n *\n * @param targetEventId - Event ID of the patch, PR, or issue being updated\n * @param statusKind - One of 1630 (open), 1631 (applied), 1632 (closed), 1633 (draft)\n * @param targetPubkey - Optional pubkey of the target event author (p tag per NIP-34 StatusEvent)\n */\nexport function buildStatus(\n targetEventId: string,\n statusKind: StatusKind,\n targetPubkey?: string\n): UnsignedEvent {\n const tags: string[][] = [['e', targetEventId]];\n if (targetPubkey) {\n tags.push(['p', targetPubkey]);\n }\n return {\n kind: statusKind,\n content: '',\n tags,\n created_at: Math.floor(Date.now() / 1000),\n };\n}\n","/**\n * Remote repository state reader — the \"what does the remote have?\" half of\n * `rig push` (epic #222, ticket #225).\n *\n * Queries relay(s) over NIP-01 WebSocket for the repository's NIP-34 state:\n * - kind:30618 (repository state): `r` tags → ref map, `HEAD` symref,\n * `arweave` tags → git SHA → Arweave txId hints.\n * - kind:30617 (repository announcement): presence = the repo exists on\n * TOON (first-push detection) + name/description/relays metadata.\n *\n * Both kinds are NIP-33 parameterized-replaceable: per relay only the latest\n * event per (kind, author, d) survives, but we still pick the newest across\n * relays (highest created_at, ties broken by lowest id per NIP-01).\n *\n * SHAs missing from the `arweave` tag map can be resolved via the shared\n * Arweave GraphQL Git-SHA resolver (@toon-protocol/arweave — the same module\n * the rig SPA uses) through {@link RemoteState.resolveMissing}.\n *\n * Relay payload encodings (mirrors rig's `web/relay-client.ts` decode logic):\n * EVENT payloads arrive as an inline JSON object (standard NIP-01), as a\n * double-JSON-encoded string (devnet relay quirk: a JSON string containing\n * the event JSON), or as a TOON-encoded string. All three are tolerated.\n */\n\nimport { decode as decodeToon } from '@toon-format/toon';\nimport {\n resolveGitSha,\n seedShaCache,\n shaCacheKey,\n} from '@toon-protocol/arweave';\nimport { REPOSITORY_ANNOUNCEMENT_KIND } from '@toon-protocol/core/nip34';\n\nimport { REPOSITORY_STATE_KIND, parseMaintainers } from './nip34-events.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** A signed Nostr event as seen on a relay (read side). */\nexport interface NostrEvent {\n id: string;\n pubkey: string;\n created_at: number;\n kind: number;\n tags: string[][];\n content: string;\n sig: string;\n}\n\n/** NIP-01 subscription filter (only the fields rig's readers send). */\nexport interface NostrFilter {\n ids?: string[];\n kinds?: number[];\n authors?: string[];\n '#d'?: string[];\n /** Repo address tag filter, e.g. `30617:<owner>:<repoId>` (#278 tracker). */\n '#a'?: string[];\n /** Event-reference tag filter (#278 tracker: statuses + comments). */\n '#e'?: string[];\n limit?: number;\n}\n\n/**\n * Minimal structural WebSocket type — satisfied by the WHATWG WebSocket\n * global (Node >= 22 / undici, browsers) and by the `ws` package.\n */\nexport interface WebSocketLike {\n readyState: number;\n send(data: string): void;\n close(): void;\n addEventListener(type: string, listener: (event: never) => void): void;\n}\n\n/** Factory for WebSocket connections (injectable for tests / `ws` fallback). */\nexport type WebSocketFactory = (url: string) => WebSocketLike;\n\nexport interface FetchRemoteStateOptions {\n /**\n * Relay WebSocket URLs to query. Plural from day one (forward-compat with\n * multi-relay routing); size 1 is typical today. Results are merged and the\n * newest replaceable event across all relays wins.\n */\n relayUrls: string[];\n /** Repository owner's pubkey (hex) — the author of 30617/30618. */\n ownerPubkey: string;\n /** Repository identifier (NIP-34 `d` tag). */\n repoId: string;\n /** Per-relay timeout in milliseconds (default 10000). On timeout the relay contributes whatever it sent so far. */\n timeoutMs?: number;\n /**\n * Git-SHA → Arweave txId resolver used by {@link RemoteState.resolveMissing}.\n * Defaults to the shared GraphQL resolver from @toon-protocol/arweave;\n * injectable for tests.\n */\n resolveSha?: (sha: string, repo: string) => Promise<string | null>;\n /** WebSocket constructor override (defaults to the global WebSocket). */\n webSocketFactory?: WebSocketFactory;\n}\n\n/** Remote repository state assembled from relay events. */\nexport interface RemoteState {\n /** True when a kind:30617 announcement exists (false ⇒ first push). */\n announced: boolean;\n /** Ref map from the latest kind:30618: refname → commit SHA. */\n refs: Map<string, string>;\n /** HEAD symref target (e.g. `refs/heads/main`), or null if unset. */\n headSymref: string | null;\n /** Git SHA → Arweave txId hints from the latest kind:30618 `arweave` tags. */\n shaToTxId: Map<string, string>;\n /** The latest kind:30618 event, or null if the repo has no state yet. */\n refsEvent: NostrEvent | null;\n /** The latest kind:30617 event, or null if the repo is unannounced. */\n announceEvent: NostrEvent | null;\n /** Repository name from the announcement `name` tag. */\n name: string | null;\n /** Announcement `description` tag (falls back to event content). */\n description: string | null;\n /** Relay URLs advertised in the announcement `relays` tag(s). */\n relays: string[];\n /**\n * Declared maintainer pubkeys (hex) from the announcement `maintainers`\n * tag (#287). Does NOT include the owner (an implicit maintainer). Empty\n * when unannounced or owner-only.\n */\n maintainers: 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 // Declared maintainers (#287): the `maintainers` tag on the 30617. Owner is\n // an implicit maintainer and is NOT listed here.\n const maintainers = announceEvent\n ? parseMaintainers(announceEvent.tags)\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 maintainers,\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","/**\n * GitRepoReader — read a local repository via `execFile` git plumbing.\n *\n * Real git gives perfect fidelity (packfiles, delta chains, exotic history)\n * with zero new deps — no isomorphic-git. Everything here is read-only and\n * injection-safe:\n *\n * - child processes are spawned with `execFile`/`spawn` and argument\n * ARRAYS — never a shell, never string interpolation;\n * - every caller-supplied revision/range is validated against strict\n * regexes that (among other things) reject a leading `-`, so a value\n * like `--upload-pack=…` can never be parsed as an option;\n * - `--` terminators are appended where git supports them so nothing\n * user-supplied can be re-interpreted as a pathspec.\n *\n * Push planning/publishing live in follow-up tickets of epic\n * toon-client#222 — this module only reads.\n */\n\nimport { execFile, spawn } from 'node:child_process';\nimport { promisify } from 'node:util';\nimport type { GitObjectType } from './objects.js';\n\nconst execFileAsync = promisify(execFile);\n\n/** Generous cap for plumbing stdout (rev-list on big repos, format-patch). */\nconst MAX_BUFFER = 256 * 1024 * 1024;\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** A single ref from `git for-each-ref` (branches + tags). */\nexport interface GitRef {\n /** Full refname, e.g. `refs/heads/main` or `refs/tags/v1.0.0`. */\n refname: string;\n /**\n * SHA the ref points at. For annotated tags this is the TAG object's SHA\n * (the peeled commit is in {@link peeledSha}); for branches and\n * lightweight tags it is the commit SHA.\n */\n sha: string;\n /** Type of the referenced object: `commit`, or `tag` for annotated tags. */\n type: GitObjectType;\n /** For annotated tags: the peeled (target) object SHA. */\n peeledSha?: string;\n}\n\n/** Result of {@link GitRepoReader.listRefs}. */\nexport interface RepoRefs {\n /**\n * Full refname HEAD points at (e.g. `refs/heads/main`), or `undefined`\n * when HEAD is detached.\n */\n head?: string;\n refs: GitRef[];\n}\n\n/** One object streamed out of `git cat-file --batch`. */\nexport interface ReadGitObject {\n /** Full 40-hex SHA-1. */\n sha: string;\n type: GitObjectType;\n /** Raw object body (content only, no envelope header). May be binary. */\n body: Buffer;\n}\n\n/** Result of {@link GitRepoReader.readObjects}. */\nexport interface ReadObjectsResult {\n /** Objects found, in input order (minus missing ones). */\n objects: ReadGitObject[];\n /** Requested SHAs not present in the repository. */\n missing: string[];\n}\n\n/** One object from `rev-list --objects`: SHA plus the path it was reached by. */\nexport interface ObjectWithPath {\n /** Full 40-hex SHA-1. */\n sha: string;\n /**\n * Path the object was first reached by (blobs and non-root trees);\n * `undefined` for commits, root trees, and tag objects.\n */\n path?: string;\n}\n\n/** One object's metadata from `cat-file --batch-check`. */\nexport interface ObjectStat {\n sha: string;\n type: GitObjectType;\n /** Object body size in bytes (content only, no envelope header). */\n size: number;\n}\n\n/** Result of {@link GitRepoReader.statObjects}. */\nexport interface StatObjectsResult {\n /** Stats found, in input order (minus missing ones). */\n objects: ObjectStat[];\n /** Requested SHAs not present in the repository. */\n missing: string[];\n}\n\n/** Error from a git child process, carrying exit code and stderr. */\nexport class GitError extends Error {\n constructor(\n message: string,\n /** Process exit code (undefined when the process failed to spawn). */\n public readonly exitCode: number | undefined,\n /** Captured stderr, trimmed. */\n public readonly stderr: string\n ) {\n super(message);\n this.name = 'GitError';\n }\n}\n\n// ---------------------------------------------------------------------------\n// Argument validation (injection defense)\n// ---------------------------------------------------------------------------\n\n/** Full 40-hex SHA-1. */\nconst FULL_SHA_RE = /^[0-9a-f]{40}$/;\n\n/**\n * One revision token: a SHA prefix (4–40 hex) or a refname-ish word with an\n * optional `^`/`~<n>` ancestry suffix. Must start with an alphanumeric, so a\n * leading `-` (option injection) is impossible; `@{…}`, whitespace, and other\n * revspec exotica are deliberately rejected.\n */\nconst REV_TOKEN_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]*(?:[~^][0-9]*)*$/;\n\nfunction isValidRevision(rev: string): boolean {\n if (rev.length === 0 || rev.length > 1024) return false;\n if (!REV_TOKEN_RE.test(rev)) return false;\n // Refname rules git enforces that our charset alone doesn't:\n if (rev.includes('..')) return false; // range separator / invalid in refnames\n if (rev.endsWith('.lock') || rev.endsWith('/') || rev.endsWith('.')) return false;\n return true;\n}\n\nfunction assertRevision(rev: string, what: string): void {\n if (!isValidRevision(rev)) {\n throw new Error(\n `${what} is not a valid git revision (got ${JSON.stringify(rev)}); ` +\n 'expected a SHA or simple refname — options/ranges are rejected'\n );\n }\n}\n\nfunction assertFullSha(sha: string, what: string): void {\n if (!FULL_SHA_RE.test(sha)) {\n throw new Error(\n `${what} is not a full 40-hex SHA-1 (got ${JSON.stringify(sha)})`\n );\n }\n}\n\n/**\n * A revision range for format-patch: `<rev>`, `<rev>..<rev>`, or\n * `<rev>...<rev>` where each side passes {@link isValidRevision}.\n */\nfunction assertRange(range: string, what: string): void {\n const parts = range.split(/\\.{2,3}/);\n const separators = range.match(/\\.{2,3}/g) ?? [];\n const ok =\n parts.length <= 2 &&\n separators.length === parts.length - 1 &&\n parts.every((p) => isValidRevision(p));\n if (!ok) {\n throw new Error(\n `${what} is not a valid revision range (got ${JSON.stringify(range)}); ` +\n 'expected <rev>, <rev>..<rev>, or <rev>...<rev>'\n );\n }\n}\n\n// ---------------------------------------------------------------------------\n// cat-file --batch incremental parser\n// ---------------------------------------------------------------------------\n\nconst OBJECT_TYPES: ReadonlySet<string> = new Set(['blob', 'tree', 'commit', 'tag']);\n\n/**\n * Incremental parser for `git cat-file --batch` output:\n * `<sha> <type> <size>\\n<body>\\n` per found object, `<name> missing\\n` for\n * absent ones. Bodies are raw bytes (possibly binary) and may be split\n * across arbitrary chunk boundaries, so parsing is strictly size-driven.\n */\nclass BatchParser {\n private buf: Buffer = Buffer.alloc(0);\n private pending: { sha: string; type: GitObjectType; size: number } | null = null;\n\n readonly objects: ReadGitObject[] = [];\n readonly missing: string[] = [];\n\n push(chunk: Buffer): void {\n this.buf = this.buf.length === 0 ? chunk : Buffer.concat([this.buf, chunk]);\n this.drain();\n }\n\n /** True when no partially-parsed record remains. */\n isComplete(): boolean {\n return this.pending === null && this.buf.length === 0;\n }\n\n private drain(): void {\n for (;;) {\n if (this.pending) {\n // Need body + trailing LF before the record is complete.\n const needed = this.pending.size + 1;\n if (this.buf.length < needed) return;\n const body = Buffer.from(this.buf.subarray(0, this.pending.size));\n this.objects.push({ sha: this.pending.sha, type: this.pending.type, body });\n this.buf = this.buf.subarray(needed);\n this.pending = null;\n continue;\n }\n\n const nl = this.buf.indexOf(0x0a);\n if (nl === -1) return;\n const header = this.buf.subarray(0, nl).toString('utf-8');\n this.buf = this.buf.subarray(nl + 1);\n\n const [name, second, third] = header.split(' ');\n if (name && second === 'missing' && third === undefined) {\n this.missing.push(name);\n continue;\n }\n if (name && second && third !== undefined && OBJECT_TYPES.has(second)) {\n const size = Number.parseInt(third, 10);\n if (Number.isSafeInteger(size) && size >= 0) {\n this.pending = { sha: name, type: second as GitObjectType, size };\n continue;\n }\n }\n throw new GitError(\n `unexpected cat-file --batch header: ${JSON.stringify(header)}`,\n undefined,\n ''\n );\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// GitRepoReader\n// ---------------------------------------------------------------------------\n\n/**\n * Read-only view of a local git repository via git plumbing commands.\n *\n * All methods throw {@link GitError} when the underlying git process fails\n * unexpectedly, and plain `Error` when a caller-supplied argument fails\n * validation (before any process is spawned).\n */\nexport class GitRepoReader {\n constructor(\n /** Absolute or relative path to the repository worktree (or .git dir). */\n public readonly repoPath: string\n ) {}\n\n /** Run git with argument-array safety; resolves stdout as UTF-8. */\n private async git(\n args: string[],\n opts: { allowExitCodes?: number[] } = {}\n ): Promise<{ stdout: string; exitCode: number }> {\n try {\n const { stdout } = await execFileAsync('git', args, {\n cwd: this.repoPath,\n maxBuffer: MAX_BUFFER,\n encoding: 'utf-8',\n });\n return { stdout, exitCode: 0 };\n } catch (err) {\n const e = err as NodeJS.ErrnoException & {\n code?: number | string;\n stdout?: string;\n stderr?: string;\n };\n const exitCode = typeof e.code === 'number' ? e.code : undefined;\n if (exitCode !== undefined && opts.allowExitCodes?.includes(exitCode)) {\n return { stdout: e.stdout ?? '', exitCode };\n }\n throw new GitError(\n `git ${args[0]} failed${exitCode !== undefined ? ` (exit ${exitCode})` : ''}: ` +\n `${(e.stderr ?? e.message ?? '').trim()}`,\n exitCode,\n (e.stderr ?? '').trim()\n );\n }\n }\n\n /**\n * List all branches and tags plus the symbolic HEAD.\n *\n * Annotated tags report the tag object's SHA/type with the peeled target\n * in `peeledSha`. A detached HEAD is tolerated (`head` is `undefined`).\n */\n async listRefs(): Promise<RepoRefs> {\n const format = '%(refname)%00%(objectname)%00%(objecttype)%00%(*objectname)';\n const [refsRes, headRes] = await Promise.all([\n this.git(['for-each-ref', `--format=${format}`, 'refs/heads', 'refs/tags']),\n // Exit 1 = detached HEAD (or unborn branch pointer oddities) — tolerated.\n this.git(['symbolic-ref', '--quiet', 'HEAD'], { allowExitCodes: [1] }),\n ]);\n\n const refs: GitRef[] = [];\n for (const line of refsRes.stdout.split('\\n')) {\n if (!line) continue;\n const [refname, sha, objecttype, peeled] = line.split('\\0');\n if (!refname || !sha || !objecttype || !OBJECT_TYPES.has(objecttype)) {\n throw new GitError(`unexpected for-each-ref line: ${JSON.stringify(line)}`, undefined, '');\n }\n refs.push({\n refname,\n sha,\n type: objecttype as GitObjectType,\n ...(peeled ? { peeledSha: peeled } : {}),\n });\n }\n\n const head = headRes.exitCode === 0 ? headRes.stdout.trim() || undefined : undefined;\n return { head, refs };\n }\n\n /**\n * SHAs of every object reachable from `want` but not from `have`\n * (`git rev-list --objects <want…> --not <have…>`), i.e. the push delta.\n *\n * Haves that don't exist locally (e.g. remote tips we never fetched) are\n * filtered out first via one `cat-file --batch-check` pass — rev-list\n * would otherwise die on them.\n */\n async objectsBetween(want: string[], have: string[]): Promise<string[]> {\n const objects = await this.objectsBetweenWithPaths(want, have);\n return objects.map((o) => o.sha);\n }\n\n /**\n * Like {@link objectsBetween} but keeps the path each object was reached\n * by (`rev-list --objects` emits `<sha> <path>` for blobs and non-root\n * trees) — used by push planning to report actionable oversize errors.\n */\n async objectsBetweenWithPaths(\n want: string[],\n have: string[]\n ): Promise<ObjectWithPath[]> {\n for (const w of want) assertRevision(w, 'want');\n for (const h of have) assertRevision(h, 'have');\n if (want.length === 0) return [];\n\n const knownHaves = await this.filterExisting(have);\n\n const args = ['rev-list', '--objects', ...want];\n if (knownHaves.length > 0) args.push('--not', ...knownHaves);\n args.push('--'); // nothing user-supplied can become a pathspec\n const { stdout } = await this.git(args);\n\n const objects: ObjectWithPath[] = [];\n for (const line of stdout.split('\\n')) {\n if (!line) continue;\n // `--objects` lines are `<sha>` or `<sha> <path>`.\n const spaceIdx = line.indexOf(' ');\n if (spaceIdx === -1) {\n objects.push({ sha: line });\n } else {\n const path = line.slice(spaceIdx + 1);\n objects.push({\n sha: line.slice(0, spaceIdx),\n ...(path ? { path } : {}),\n });\n }\n }\n return objects;\n }\n\n /** Run git feeding `input` on stdin; resolves collected stdout bytes. */\n private runWithStdin(args: string[], input: string): Promise<Buffer> {\n return new Promise<Buffer>((resolve, reject) => {\n const child = spawn('git', args, {\n cwd: this.repoPath,\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n const out: Buffer[] = [];\n let stderr = '';\n child.stdout.on('data', (chunk: Buffer) => out.push(chunk));\n child.stderr.on('data', (chunk: Buffer) => {\n stderr += chunk.toString('utf-8');\n });\n child.on('error', (err) => {\n reject(new GitError(`failed to spawn git ${args[0]}: ${err.message}`, undefined, ''));\n });\n child.on('close', (code) => {\n if (code !== 0) {\n return reject(\n new GitError(`git ${args[0]} failed (exit ${code}): ${stderr.trim()}`, code ?? undefined, stderr.trim())\n );\n }\n resolve(Buffer.concat(out));\n });\n child.stdin.on('error', () => {\n // Child died before consuming stdin; 'close' surfaces the failure.\n });\n child.stdin.write(input);\n child.stdin.end();\n });\n }\n\n /** Of the given revisions, keep only those resolvable locally. */\n private async filterExisting(revs: string[]): Promise<string[]> {\n if (revs.length === 0) return [];\n const { missing } = await this.batchCheck(revs);\n const missingSet = new Set(missing);\n return revs.filter((r) => !missingSet.has(r));\n }\n\n /** One `cat-file --batch-check` pass; returns names reported missing. */\n private async batchCheck(names: string[]): Promise<{ missing: string[] }> {\n const stdout = await this.runWithStdin(\n ['cat-file', '--batch-check'],\n names.join('\\n') + '\\n'\n );\n const missing: string[] = [];\n for (const line of stdout.toString('utf-8').split('\\n')) {\n if (line.endsWith(' missing')) missing.push(line.slice(0, -' missing'.length));\n }\n return { missing };\n }\n\n /**\n * Object metadata (type + body size) for a batch of SHAs via one\n * `cat-file --batch-check` pass — no bodies are read. Missing objects are\n * reported, not thrown.\n */\n async statObjects(shas: string[]): Promise<StatObjectsResult> {\n for (const sha of shas) assertFullSha(sha, 'sha');\n if (shas.length === 0) return { objects: [], missing: [] };\n\n const stdout = await this.runWithStdin(\n ['cat-file', '--batch-check'],\n shas.join('\\n') + '\\n'\n );\n\n const objects: ObjectStat[] = [];\n const missing: string[] = [];\n for (const line of stdout.toString('utf-8').split('\\n')) {\n if (!line) continue;\n if (line.endsWith(' missing')) {\n missing.push(line.slice(0, -' missing'.length));\n continue;\n }\n const [sha, type, sizeStr] = line.split(' ');\n const size = Number.parseInt(sizeStr ?? '', 10);\n if (\n !sha ||\n !type ||\n !OBJECT_TYPES.has(type) ||\n !Number.isSafeInteger(size) ||\n size < 0\n ) {\n throw new GitError(\n `unexpected cat-file --batch-check line: ${JSON.stringify(line)}`,\n undefined,\n ''\n );\n }\n objects.push({ sha, type: type as GitObjectType, size });\n }\n return { objects, missing };\n }\n\n /**\n * Read raw object bodies via a single streaming `git cat-file --batch`\n * child process. Bodies may be binary and are parsed size-driven across\n * chunk boundaries. Missing objects are reported, not thrown.\n */\n async readObjects(shas: string[]): Promise<ReadObjectsResult> {\n for (const sha of shas) assertFullSha(sha, 'sha');\n if (shas.length === 0) return { objects: [], missing: [] };\n\n const parser = new BatchParser();\n await new Promise<void>((resolve, reject) => {\n const child = spawn('git', ['cat-file', '--batch'], {\n cwd: this.repoPath,\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n\n let stderr = '';\n let parseError: Error | null = null;\n\n child.stderr.on('data', (chunk: Buffer) => {\n stderr += chunk.toString('utf-8');\n });\n child.stdout.on('data', (chunk: Buffer) => {\n if (parseError) return;\n try {\n parser.push(chunk);\n } catch (err) {\n parseError = err as Error;\n child.kill();\n }\n });\n child.on('error', (err) => {\n reject(new GitError(`failed to spawn git cat-file: ${err.message}`, undefined, ''));\n });\n child.on('close', (code) => {\n if (parseError) return reject(parseError);\n if (code !== 0) {\n return reject(\n new GitError(`git cat-file --batch failed (exit ${code}): ${stderr.trim()}`, code ?? undefined, stderr.trim())\n );\n }\n if (!parser.isComplete()) {\n return reject(\n new GitError('git cat-file --batch output ended mid-record', code ?? undefined, stderr.trim())\n );\n }\n resolve();\n });\n\n child.stdin.on('error', () => {\n // Child died before consuming stdin; 'close' will surface the error.\n });\n child.stdin.write(shas.join('\\n') + '\\n');\n child.stdin.end();\n });\n\n return { objects: parser.objects, missing: parser.missing };\n }\n\n /**\n * `git merge-base --is-ancestor <a> <b>` — true when `a` is an ancestor\n * of `b` (fast-forward check / force detection). Exit codes other than\n * 0/1 (e.g. unknown revisions) throw.\n */\n async isAncestor(a: string, b: string): Promise<boolean> {\n assertRevision(a, 'ancestor candidate');\n assertRevision(b, 'descendant candidate');\n const { exitCode } = await this.git(\n ['merge-base', '--is-ancestor', a, b],\n { allowExitCodes: [1] }\n );\n return exitCode === 0;\n }\n\n /**\n * `git format-patch --stdout <range>` — the full mbox-formatted patch\n * series text (empty string when the range selects no commits).\n */\n async formatPatch(range: string): Promise<string> {\n assertRange(range, 'range');\n const { stdout } = await this.git(['format-patch', '--stdout', range, '--']);\n return stdout;\n }\n\n /**\n * Parent SHAs for a batch of commit SHAs via one\n * `git rev-list --no-walk=unsorted --parents` pass. Root commits map to an\n * empty array. Used to derive the kind:1617 `commit`/`parent-commit` tag\n * pairs for exactly the commits a format-patch series carries.\n */\n async commitParents(shas: string[]): Promise<Map<string, string[]>> {\n for (const sha of shas) assertFullSha(sha, 'sha');\n if (shas.length === 0) return new Map();\n const { stdout } = await this.git([\n 'rev-list',\n '--no-walk=unsorted',\n '--parents',\n ...shas,\n '--',\n ]);\n const parents = new Map<string, string[]>();\n for (const line of stdout.split('\\n')) {\n if (!line) continue;\n const [sha, ...rest] = line.split(' ');\n if (!sha || !FULL_SHA_RE.test(sha) || rest.some((p) => !FULL_SHA_RE.test(p))) {\n throw new GitError(\n `unexpected rev-list --parents line: ${JSON.stringify(line)}`,\n undefined,\n ''\n );\n }\n parents.set(sha, rest);\n }\n return parents;\n }\n\n /**\n * Resolve a ref/revision to a full SHA via `git rev-parse --verify`.\n * Throws {@link GitError} when the name doesn't resolve.\n */\n async resolveRef(name: string): Promise<string> {\n assertRevision(name, 'ref name');\n const { stdout } = await this.git(['rev-parse', '--verify', '--quiet', name]);\n const sha = stdout.trim();\n if (!FULL_SHA_RE.test(sha)) {\n throw new GitError(\n `rev-parse --verify returned unexpected output for ${JSON.stringify(name)}: ${JSON.stringify(sha)}`,\n undefined,\n ''\n );\n }\n return sha;\n }\n}\n","/**\n * The Git-from-TOON READ pipeline core (#278): download git object bodies\n * from Arweave gateways, verify them against their SHA-1, and walk the\n * object graph to prove a ref closure is complete.\n *\n * This is the CLI counterpart of rig-web's proven browser read path\n * (`web/arweave-client.ts` + `web/git-objects.ts` + the commit walker) — the\n * logic is mirrored, not imported, because the SPA package is not a library.\n *\n * Everything here is FREE (reads only: Arweave gateway GETs + the GraphQL\n * Git-SHA resolver) and pure of git — materializing objects into a real\n * repository lives in ./materialize.ts.\n *\n * INTEGRITY IS NON-NEGOTIABLE: an Arweave upload stores the object BODY\n * (content after the envelope NUL). Re-wrapping the body as each of the four\n * git object types and comparing the envelope SHA-1 against the expected SHA\n * both AUTHENTICATES the bytes and DISCOVERS the object's type in one step —\n * a body that matches under no type is rejected as corrupt/tampered, never\n * written.\n */\n\nimport {\n ARWEAVE_FETCH_TIMEOUT_MS,\n ARWEAVE_GATEWAYS,\n isValidArweaveTxId,\n} from '@toon-protocol/arweave';\nimport { hashGitObject, type GitObjectType } from './objects.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** A downloaded git object: SHA-verified body + the type that verified it. */\nexport interface FetchedObject {\n /** Full 40-hex SHA-1 (verified against the body). */\n sha: string;\n type: GitObjectType;\n /** Raw object body (content only, no envelope header). May be binary. */\n body: Buffer;\n}\n\n/** WHATWG-fetch seam (injectable for tests). */\nexport type FetchLike = (\n url: string,\n init?: { signal?: AbortSignal }\n) => Promise<{\n ok: boolean;\n arrayBuffer(): Promise<ArrayBuffer>;\n}>;\n\nexport interface GatewayFetchOptions {\n /** Ordered gateway base URLs (default: the shared preference list). */\n gateways?: readonly string[];\n /** fetch implementation (default: global fetch). */\n fetchFn?: FetchLike;\n /** Per-request timeout in milliseconds. */\n timeoutMs?: number;\n}\n\nexport interface DownloadOptions extends GatewayFetchOptions {\n /** Maximum concurrent gateway downloads (default {@link DEFAULT_CONCURRENCY}). */\n concurrency?: number;\n /** Progress callback, called once per finished object. */\n onObject?: (done: number, total: number) => void;\n}\n\n/** Result of {@link downloadGitObjects}. */\nexport interface DownloadResult {\n /** SHA → verified object, for every SHA that could be downloaded. */\n objects: Map<string, FetchedObject>;\n /** SHAs whose txId 404'd/errored on EVERY gateway (propagation lag). */\n unavailable: { sha: string; txId: string }[];\n}\n\n/** Default parallel-download cap. */\nexport const DEFAULT_CONCURRENCY = 8;\n\n/**\n * A downloaded body did not hash to its expected SHA under ANY git object\n * type — corrupt or tampered content. The clone/fetch pipelines treat this\n * as a hard failure: nothing is written.\n */\nexport class ObjectIntegrityError extends Error {\n constructor(\n /** The objects that failed verification. */\n public readonly objects: { sha: string; txId: string }[]\n ) {\n super(\n `${objects.length} downloaded object(s) failed SHA-1 verification — ` +\n 'the gateway content does not match the announced git SHA(s): ' +\n objects.map((o) => `${o.sha} (tx ${o.txId})`).join(', ') +\n '. Refusing to write corrupt/tampered objects.'\n );\n this.name = 'ObjectIntegrityError';\n }\n}\n\n// ---------------------------------------------------------------------------\n// Verification (SHA check == type discovery)\n// ---------------------------------------------------------------------------\n\nconst OBJECT_TYPES: readonly GitObjectType[] = [\n 'blob',\n 'tree',\n 'commit',\n 'tag',\n];\n\n/**\n * Verify a downloaded body against its expected SHA-1 by trying the four git\n * envelope types. Returns the verified object, or null when no type matches\n * (corrupt/tampered bytes).\n */\nexport function verifyObjectBody(\n expectedSha: string,\n bytes: Uint8Array\n): FetchedObject | null {\n const body = Buffer.from(bytes);\n for (const type of OBJECT_TYPES) {\n if (hashGitObject(type, body).sha === expectedSha) {\n return { sha: expectedSha, type, body };\n }\n }\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Gateway download (fallback chain, mirrors rig-web's fetchArweaveObject)\n// ---------------------------------------------------------------------------\n\n/**\n * Fetch raw bytes for an Arweave tx id, trying each gateway in preference\n * order. Returns null when every gateway fails (404 / error / timeout).\n */\nexport async function fetchTxBytes(\n txId: string,\n options: GatewayFetchOptions = {}\n): Promise<Uint8Array | null> {\n if (!isValidArweaveTxId(txId)) return null;\n const gateways = options.gateways ?? ARWEAVE_GATEWAYS;\n const fetchFn = options.fetchFn ?? (fetch as FetchLike);\n const timeoutMs = options.timeoutMs ?? ARWEAVE_FETCH_TIMEOUT_MS;\n\n for (const gateway of gateways) {\n try {\n const response = await fetchFn(`${gateway}/${txId}`, {\n signal: AbortSignal.timeout(timeoutMs),\n });\n if (!response.ok) continue;\n return new Uint8Array(await response.arrayBuffer());\n } catch {\n // Network error, timeout, or other failure — try the next gateway.\n }\n }\n return null;\n}\n\n/**\n * Download + verify a batch of git objects (sha → Arweave txId) with a\n * concurrency cap and per-gateway fallback.\n *\n * Objects that 404 on every gateway are reported in `unavailable` (Arweave\n * propagation lag — the caller decides whether that is fatal). Objects whose\n * bytes fail SHA-1 verification throw {@link ObjectIntegrityError}: corrupt\n * content is NEVER returned.\n */\nexport async function downloadGitObjects(\n entries: Iterable<[sha: string, txId: string]>,\n options: DownloadOptions = {}\n): Promise<DownloadResult> {\n const queue = [...entries];\n const total = queue.length;\n const concurrency = Math.max(1, options.concurrency ?? DEFAULT_CONCURRENCY);\n\n const objects = new Map<string, FetchedObject>();\n const unavailable: { sha: string; txId: string }[] = [];\n const corrupt: { sha: string; txId: string }[] = [];\n let done = 0;\n\n const worker = async (): Promise<void> => {\n for (;;) {\n const next = queue.shift();\n if (!next) return;\n const [sha, txId] = next;\n const bytes = await fetchTxBytes(txId, options);\n if (bytes === null) {\n unavailable.push({ sha, txId });\n } else {\n const verified = verifyObjectBody(sha, bytes);\n if (verified === null) {\n corrupt.push({ sha, txId });\n } else {\n objects.set(sha, verified);\n }\n }\n done += 1;\n options.onObject?.(done, total);\n }\n };\n\n await Promise.all(\n Array.from({ length: Math.min(concurrency, total) }, () => worker())\n );\n\n if (corrupt.length > 0) throw new ObjectIntegrityError(corrupt);\n return { objects, unavailable };\n}\n\n// ---------------------------------------------------------------------------\n// Object-graph references (mirrors rig-web's git-objects parsing)\n// ---------------------------------------------------------------------------\n\nconst FULL_SHA_RE = /^[0-9a-f]{40}$/;\n\n/** Submodule (gitlink) tree-entry mode: references a commit in ANOTHER repo. */\nconst GITLINK_MODE = '160000';\n\nfunction bytesToHex(bytes: Uint8Array): string {\n let hex = '';\n for (const byte of bytes) hex += byte.toString(16).padStart(2, '0');\n return hex;\n}\n\n/**\n * SHAs an object references inside the SAME repository:\n * - commit → its tree + parents\n * - tree → entry SHAs, EXCEPT gitlinks (mode 160000: submodule commits\n * live in another repository and are never present — git fsck\n * skips them too)\n * - tag → the tagged object\n * - blob → nothing\n */\nexport function referencedShas(object: FetchedObject): string[] {\n switch (object.type) {\n case 'blob':\n return [];\n case 'tree': {\n const refs: string[] = [];\n const data = object.body;\n let offset = 0;\n while (offset < data.length) {\n const spaceIdx = data.indexOf(0x20, offset);\n if (spaceIdx === -1) break;\n const mode = data.subarray(offset, spaceIdx).toString('utf-8');\n const nulIdx = data.indexOf(0x00, spaceIdx + 1);\n if (nulIdx === -1 || nulIdx + 21 > data.length) break;\n const sha = bytesToHex(data.subarray(nulIdx + 1, nulIdx + 21));\n if (mode !== GITLINK_MODE) refs.push(sha);\n offset = nulIdx + 21;\n }\n return refs;\n }\n case 'commit': {\n const refs: string[] = [];\n const text = object.body.toString('utf-8');\n const headerEnd = text.indexOf('\\n\\n');\n const header = headerEnd === -1 ? text : text.slice(0, headerEnd);\n for (const line of header.split('\\n')) {\n if (line.startsWith('tree ')) refs.push(line.slice(5).trim());\n else if (line.startsWith('parent ')) refs.push(line.slice(7).trim());\n }\n return refs.filter((sha) => FULL_SHA_RE.test(sha));\n }\n case 'tag': {\n const text = object.body.toString('utf-8');\n const match = /^object ([0-9a-f]{40})$/m.exec(text);\n return match?.[1] ? [match[1]] : [];\n }\n }\n}\n\n/** Result of {@link walkClosure}. */\nexport interface ClosureResult {\n /** Every SHA reachable from the tips that lives in `objects`. */\n reachable: Set<string>;\n /** Reachable SHAs found NEITHER in `objects` nor in `presentLocally`. */\n missing: string[];\n}\n\n/**\n * Walk the object graph from the ref tips over the downloaded object set and\n * report which reachable SHAs are missing. `presentLocally` marks SHAs that\n * already exist in the destination repository — the walk does not descend\n * into them (a consistent local repo carries its own closure; the same\n * assumption `git fetch` makes).\n */\nexport function walkClosure(\n tips: Iterable<string>,\n objects: ReadonlyMap<string, FetchedObject>,\n presentLocally: ReadonlySet<string> = new Set()\n): ClosureResult {\n const reachable = new Set<string>();\n const missing = new Set<string>();\n const stack = [...new Set(tips)];\n\n while (stack.length > 0) {\n const sha = stack.pop() as string;\n if (reachable.has(sha) || missing.has(sha)) continue;\n if (presentLocally.has(sha)) continue; // local closure assumed complete\n const object = objects.get(sha);\n if (!object) {\n missing.add(sha);\n continue;\n }\n reachable.add(sha);\n for (const ref of referencedShas(object)) {\n if (!reachable.has(ref) && !missing.has(ref)) stack.push(ref);\n }\n }\n\n return { reachable, missing: [...missing] };\n}\n","/**\n * The shared clone/fetch object-collection engine (#278).\n *\n * Given the remote's ref tips and its kind:30618 sha→Arweave-txId map, gather\n * every object the refs need:\n *\n * 1. download the mapped objects the destination repo doesn't already have\n * (parallel, gateway fallback chain, SHA-verified — ./object-fetch.ts);\n * 2. walk the object graph from the tips (./object-fetch.ts walkClosure) —\n * the local repository's objects count as present (git fetch's own\n * assumption: a consistent repo carries its own closure);\n * 3. SHAs the map doesn't cover are resolved through the Arweave GraphQL\n * Git-SHA resolver (RemoteState.resolveMissing) and downloaded, looping\n * until the closure is complete or no progress can be made.\n *\n * The result separates FATAL gaps (reachable objects that could not be\n * obtained — usually Arweave gateway propagation lag, 10–20 min for fresh\n * pushes) from harmless ones (mapped-but-unreachable objects, e.g. history\n * that was force-pushed away). Corrupt objects throw ObjectIntegrityError\n * from the download layer and never surface here.\n */\n\nimport {\n downloadGitObjects,\n walkClosure,\n type DownloadOptions,\n type FetchedObject,\n} from './object-fetch.js';\nimport { EMPTY_BLOB_SHA } from './objects.js';\n\n/** A reachable object that could not be obtained. */\nexport interface MissingObject {\n sha: string;\n /** The txId that failed on every gateway, or null when no txId resolved. */\n txId: string | null;\n}\n\nexport interface CollectRepoObjectsOptions extends DownloadOptions {\n /** Ref tip SHAs (commits or annotated tags) the closure must reach. */\n tips: string[];\n /** kind:30618 `arweave` tag map: git SHA → Arweave txId. */\n shaToTxId: ReadonlyMap<string, string>;\n /** GraphQL fallback for SHAs the map doesn't cover (RemoteState.resolveMissing). */\n resolveMissing: (shas: string[]) => Promise<Map<string, string>>;\n /** SHAs already present in the destination repository (fetch delta). */\n presentLocally?: ReadonlySet<string>;\n}\n\nexport interface CollectRepoObjectsResult {\n /** Verified objects to write, keyed by SHA. */\n objects: Map<string, FetchedObject>;\n /** Reachable SHAs that could not be obtained — FATAL for clone/fetch. */\n missing: MissingObject[];\n /** Mapped-but-unreachable SHAs that failed to download — warn only. */\n skippedUnavailable: { sha: string; txId: string }[];\n}\n\n/** Iteration cap: closure depth of NEW unmapped SHAs per pass; generous. */\nconst MAX_PASSES = 64;\n\n/** Collect (download + verify + close over) the objects the ref tips need. */\nexport async function collectRepoObjects(\n options: CollectRepoObjectsOptions\n): Promise<CollectRepoObjectsResult> {\n const { tips, shaToTxId, resolveMissing } = options;\n const present = options.presentLocally ?? new Set<string>();\n\n /** All txId knowledge: the 30618 map + GraphQL-resolved additions. */\n const txIds = new Map<string, string>(shaToTxId);\n /** SHAs we already asked the GraphQL resolver about (avoid re-queries). */\n const resolverAsked = new Set<string>();\n /** SHAs whose download failed on every gateway. */\n const undownloadable = new Map<string, string>();\n const objects = new Map<string, FetchedObject>();\n\n // Pass 0: bulk-download everything the map covers that isn't local yet.\n const initial: [string, string][] = [];\n for (const [sha, txId] of txIds) {\n if (!present.has(sha)) initial.push([sha, txId]);\n }\n const bulk = await downloadGitObjects(initial, options);\n for (const [sha, object] of bulk.objects) objects.set(sha, object);\n for (const { sha, txId } of bulk.unavailable) undownloadable.set(sha, txId);\n\n // Iterate: close over the tips; resolve + download whatever is still open.\n for (let pass = 0; pass < MAX_PASSES; pass++) {\n const closure = walkClosure(tips, objects, present);\n const open = closure.missing.filter((sha) => !undownloadable.has(sha));\n if (open.length === 0) break;\n\n // Resolve txIds for open SHAs we haven't asked the resolver about.\n const toResolve = open.filter(\n (sha) => !txIds.has(sha) && !resolverAsked.has(sha)\n );\n for (const sha of toResolve) resolverAsked.add(sha);\n if (toResolve.length > 0) {\n const resolved = await resolveMissing(toResolve);\n for (const [sha, txId] of resolved) txIds.set(sha, txId);\n }\n\n // Download open SHAs that now have a txId and no failed attempt yet.\n const batch: [string, string][] = [];\n for (const sha of open) {\n const txId = txIds.get(sha);\n if (txId !== undefined && !objects.has(sha)) batch.push([sha, txId]);\n }\n if (batch.length === 0) break; // no progress possible\n const result = await downloadGitObjects(batch, options);\n for (const [sha, object] of result.objects) objects.set(sha, object);\n for (const { sha, txId } of result.unavailable)\n undownloadable.set(sha, txId);\n if (result.objects.size === 0) break; // every attempt failed — stop\n }\n\n // The git empty blob is never uploaded (the store rejects zero-byte\n // content; `rig push` skips it), so a tree that references it reports it\n // \"missing\" here even though it is a git constant. Synthesize it locally —\n // a zero-byte blob body always hashes to EMPTY_BLOB_SHA — instead of\n // erroring, so an empty file reconstructs bit-identically (git fsck clean).\n // Keyed off the EXACT constant SHA: the honest lag-error still fires for any\n // genuinely-missing non-empty object.\n let finalClosure = walkClosure(tips, objects, present);\n if (\n finalClosure.missing.includes(EMPTY_BLOB_SHA) &&\n !present.has(EMPTY_BLOB_SHA)\n ) {\n objects.set(EMPTY_BLOB_SHA, {\n sha: EMPTY_BLOB_SHA,\n type: 'blob',\n body: Buffer.alloc(0),\n });\n finalClosure = walkClosure(tips, objects, present);\n }\n\n // Final accounting.\n const missing: MissingObject[] = finalClosure.missing.map((sha) => ({\n sha,\n txId: txIds.get(sha) ?? null,\n }));\n const reachable = finalClosure.reachable;\n const skippedUnavailable = [...undownloadable]\n .filter(\n ([sha]) => !reachable.has(sha) && !finalClosure.missing.includes(sha)\n )\n .map(([sha, txId]) => ({ sha, txId }));\n\n return { objects, missing, skippedUnavailable };\n}\n\n/**\n * The honest propagation-lag error text: which SHAs are unobtainable and why\n * retrying later is the expected remedy.\n */\nexport function missingObjectsMessage(\n missing: MissingObject[],\n context: string\n): string {\n const listed = missing\n .slice(0, 20)\n .map(\n (m) =>\n ` ${m.sha}${m.txId ? ` (tx ${m.txId})` : ' (no Arweave tx found)'}`\n )\n .join('\\n');\n const more =\n missing.length > 20 ? `\\n … and ${missing.length - 20} more` : '';\n return (\n `${context}: ${missing.length} required object(s) could not be downloaded:\\n` +\n `${listed}${more}\\n` +\n 'Recently pushed objects can take 10-20 minutes to become fetchable from ' +\n 'Arweave gateways — if this repo was just pushed, retry in a few minutes. ' +\n 'Nothing was written.'\n );\n}\n","/**\n * Materialize downloaded git objects into a REAL repository (#278).\n *\n * Objects are written through git's own plumbing — `git hash-object -w\n * --stdin -t <type>` with the raw body on stdin — so git computes, validates\n * (syntax checks for trees/commits/tags), stores (loose object + zlib), and\n * RETURNS the SHA. The returned SHA is compared against the expected one:\n * a second, independent integrity gate after ./object-fetch.ts's envelope\n * verification. Refs land via `git update-ref`, HEAD via `git symbolic-ref`.\n *\n * Same injection posture as GitRepoReader: child processes use\n * `execFile`/`spawn` with argument ARRAYS (never a shell), and refnames are\n * validated with `git check-ref-format` semantics before use.\n */\n\nimport { execFile, spawn } from 'node:child_process';\nimport { promisify } from 'node:util';\nimport type { FetchedObject } from './object-fetch.js';\n\nconst execFileAsync = promisify(execFile);\n\n/** A written object's SHA disagreed with what `git hash-object` computed. */\nexport class ObjectWriteMismatchError extends Error {\n constructor(\n public readonly expectedSha: string,\n public readonly writtenSha: string\n ) {\n super(\n `git hash-object wrote ${writtenSha} where ${expectedSha} was expected — ` +\n 'object content does not round-trip; aborting'\n );\n this.name = 'ObjectWriteMismatchError';\n }\n}\n\n/** Full 40-hex SHA-1. */\nconst FULL_SHA_RE = /^[0-9a-f]{40}$/;\n\n/**\n * Conservative refname validation (superset-safe subset of\n * `git check-ref-format`): must start `refs/`, no component may start with\n * `-` or `.`, no `..`, no control/space/git-special characters, no trailing\n * `/`, `.`, or `.lock`. Rejecting odd-but-legal names is fine — these\n * refnames come from relay events, and a hostile relay must not be able to\n * smuggle options or path traversal into git invocations.\n */\nexport function isSafeRefname(refname: string): boolean {\n if (!refname.startsWith('refs/') || refname.length > 1024) return false;\n // eslint-disable-next-line no-control-regex -- explicit control-char reject\n if (/[\\u0000-\\u0020~^:?*[\\\\\\u007f]/.test(refname)) return false;\n if (refname.includes('..') || refname.includes('@{')) return false;\n if (refname.endsWith('/') || refname.endsWith('.')) return false;\n for (const part of refname.split('/')) {\n if (part === '' || part.startsWith('.') || part.startsWith('-'))\n return false;\n if (part.endsWith('.lock')) return false;\n }\n return true;\n}\n\nfunction assertSafeRefname(refname: string): void {\n if (!isSafeRefname(refname)) {\n throw new Error(\n `unsafe ref name from remote state: ${JSON.stringify(refname)} — refusing`\n );\n }\n}\n\nfunction assertFullSha(sha: string): void {\n if (!FULL_SHA_RE.test(sha)) {\n throw new Error(`not a full 40-hex SHA-1: ${JSON.stringify(sha)}`);\n }\n}\n\n/** Run git with argument-array safety in `repoPath`. */\nexport async function runGit(\n repoPath: string,\n args: string[]\n): Promise<string> {\n try {\n const { stdout } = await execFileAsync('git', args, {\n cwd: repoPath,\n encoding: 'utf-8',\n maxBuffer: 64 * 1024 * 1024,\n });\n return stdout;\n } catch (err) {\n const e = err as {\n code?: number | string;\n stderr?: string;\n message?: string;\n };\n throw new Error(\n `git ${args[0]} failed${typeof e.code === 'number' ? ` (exit ${e.code})` : ''}: ` +\n `${(e.stderr ?? e.message ?? '').trim()}`\n );\n }\n}\n\n/**\n * Write one object via `git hash-object -w --stdin -t <type>` (binary-safe\n * stdin) and verify the SHA git computed matches the expected one.\n */\nexport async function writeGitObject(\n repoPath: string,\n object: FetchedObject\n): Promise<void> {\n assertFullSha(object.sha);\n const written = await new Promise<string>((resolve, reject) => {\n const child = spawn(\n 'git',\n ['hash-object', '-w', '--stdin', '-t', object.type],\n { cwd: repoPath, stdio: ['pipe', 'pipe', 'pipe'] }\n );\n let stdout = '';\n let stderr = '';\n child.stdout.on('data', (chunk: Buffer) => {\n stdout += chunk.toString('utf-8');\n });\n child.stderr.on('data', (chunk: Buffer) => {\n stderr += chunk.toString('utf-8');\n });\n child.on('error', (err) => {\n reject(new Error(`failed to spawn git hash-object: ${err.message}`));\n });\n child.on('close', (code) => {\n if (code !== 0) {\n return reject(\n new Error(`git hash-object failed (exit ${code}): ${stderr.trim()}`)\n );\n }\n resolve(stdout.trim());\n });\n child.stdin.on('error', () => {\n // Child died before consuming stdin; 'close' surfaces the failure.\n });\n child.stdin.write(object.body);\n child.stdin.end();\n });\n if (written !== object.sha) {\n throw new ObjectWriteMismatchError(object.sha, written);\n }\n}\n\n/** Write a batch of verified objects into the repository (sequential). */\nexport async function writeGitObjects(\n repoPath: string,\n objects: Iterable<FetchedObject>\n): Promise<number> {\n let count = 0;\n for (const object of objects) {\n await writeGitObject(repoPath, object);\n count += 1;\n }\n return count;\n}\n\n/** `git update-ref <refname> <sha>` with refname/SHA validation. */\nexport async function updateRef(\n repoPath: string,\n refname: string,\n sha: string\n): Promise<void> {\n assertSafeRefname(refname);\n assertFullSha(sha);\n await runGit(repoPath, ['update-ref', refname, sha]);\n}\n\n/** Point HEAD at a branch via `git symbolic-ref HEAD <refname>`. */\nexport async function setHeadSymref(\n repoPath: string,\n refname: string\n): Promise<void> {\n assertSafeRefname(refname);\n await runGit(repoPath, ['symbolic-ref', 'HEAD', refname]);\n}\n","/**\n * Minimal bech32 npub encoding/decoding for Nostr pubkeys (BIP-173 / NIP-19),\n * dependency-free. Mirrors rig-web's proven `web/npub.ts` — the CLI must not\n * import from the SPA package, so the ~100 lines live here too (#278: `rig\n * clone` accepts `<owner-npub-or-hex>/<repo-id>` addresses).\n */\n\nconst BECH32_CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';\n\nfunction bech32Polymod(values: number[]): number {\n const GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];\n let chk = 1;\n for (const v of values) {\n const b = chk >> 25;\n chk = ((chk & 0x1ffffff) << 5) ^ v;\n for (let i = 0; i < 5; i++) {\n chk ^= (b >> i) & 1 ? (GEN[i] as number) : 0;\n }\n }\n return chk;\n}\n\nfunction bech32HrpExpand(hrp: string): number[] {\n const ret: number[] = [];\n for (let i = 0; i < hrp.length; i++) {\n ret.push(hrp.charCodeAt(i) >> 5);\n }\n ret.push(0);\n for (let i = 0; i < hrp.length; i++) {\n ret.push(hrp.charCodeAt(i) & 31);\n }\n return ret;\n}\n\nfunction bech32CreateChecksum(hrp: string, data: number[]): number[] {\n const values = bech32HrpExpand(hrp).concat(data).concat([0, 0, 0, 0, 0, 0]);\n const polymod = bech32Polymod(values) ^ 1;\n const ret: number[] = [];\n for (let i = 0; i < 6; i++) {\n ret.push((polymod >> (5 * (5 - i))) & 31);\n }\n return ret;\n}\n\nfunction convertBits(\n data: number[],\n fromBits: number,\n toBits: number,\n pad: boolean\n): number[] {\n let acc = 0;\n let bits = 0;\n const ret: number[] = [];\n const maxv = (1 << toBits) - 1;\n for (const value of data) {\n acc = (acc << fromBits) | value;\n bits += fromBits;\n while (bits >= toBits) {\n bits -= toBits;\n ret.push((acc >> bits) & maxv);\n }\n }\n if (pad && bits > 0) {\n ret.push((acc << (toBits - bits)) & maxv);\n }\n return ret;\n}\n\nfunction hexToBytes(hex: string): number[] {\n const bytes: number[] = [];\n for (let i = 0; i < hex.length; i += 2) {\n bytes.push(parseInt(hex.slice(i, i + 2), 16));\n }\n return bytes;\n}\n\n/** Encode a 64-char hex pubkey as an `npub1…` bech32 string. */\nexport function hexToNpub(hexPubkey: string): string {\n const hrp = 'npub';\n const bytes = hexToBytes(hexPubkey);\n const words = convertBits(bytes, 8, 5, true);\n const checksum = bech32CreateChecksum(hrp, words);\n const combined = words.concat(checksum);\n return hrp + '1' + combined.map((d) => BECH32_CHARSET[d]).join('');\n}\n\n/**\n * Decode an `npub1…` bech32 string back to a 64-char hex pubkey.\n * Throws on malformed input (prefix, length, charset, checksum, padding).\n */\nexport function npubToHex(npub: string): string {\n const lower = npub.toLowerCase();\n if (lower !== npub && npub.toUpperCase() !== npub) {\n throw new Error('npub: mixed case');\n }\n if (!lower.startsWith('npub1')) {\n throw new Error('npub: invalid prefix');\n }\n if (lower.length !== 63) {\n throw new Error('npub: invalid length');\n }\n\n const data: number[] = [];\n for (let i = 5; i < lower.length; i++) {\n const idx = BECH32_CHARSET.indexOf(lower[i] as string);\n if (idx === -1) throw new Error('npub: invalid character');\n data.push(idx);\n }\n\n // Verify checksum\n const hrpExpanded = bech32HrpExpand('npub');\n if (bech32Polymod(hrpExpanded.concat(data)) !== 1) {\n throw new Error('npub: invalid checksum');\n }\n\n // Strip 6-word checksum, convert 5-bit words back to 8-bit bytes\n const words = data.slice(0, -6);\n const bytes = convertBits(words, 5, 8, false);\n\n if (bytes.length !== 32) {\n throw new Error('npub: invalid data length');\n }\n\n // Validate trailing bits are zero\n const totalBits = words.length * 5;\n const trailingBits = totalBits - bytes.length * 8;\n if (trailingBits > 0) {\n const lastWord = words[words.length - 1] as number;\n const mask = (1 << trailingBits) - 1;\n if ((lastWord & mask) !== 0) {\n throw new Error('npub: non-zero padding bits');\n }\n }\n\n return bytes.map((b) => b.toString(16).padStart(2, '0')).join('');\n}\n\nconst HEX64_RE = /^[0-9a-f]{64}$/;\n\n/**\n * Normalize a repo-owner reference to a 64-char hex pubkey: accepts lowercase\n * hex verbatim or an `npub1…` string. Throws a caller-facing error otherwise.\n */\nexport function ownerToHex(owner: string): string {\n if (HEX64_RE.test(owner)) return owner;\n if (owner.startsWith('npub1')) {\n try {\n return npubToHex(owner);\n } catch (err) {\n throw new Error(\n `invalid owner ${JSON.stringify(owner)}: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n throw new Error(\n `invalid owner ${JSON.stringify(owner)}: expected a 64-char lowercase hex pubkey or an npub1… string`\n );\n}\n","/**\n * Wire shapes of the toon-clientd `/git/*` control routes (epic #222).\n *\n * These are the JSON request/response types the daemon serves (bigints as\n * decimal strings, Maps as plain records) — defined HERE, in the dependency\n * root, so both sides of the route can share them: the `rig` CLI (#229)\n * consumes them as a plain-fetch client, and `@toon-protocol/client-mcp`\n * (which depends on this package for the planner — the reverse import would\n * be circular) can adopt them for its `control-api.ts` declarations. TYPES\n * ONLY plus two pure serializers; no transport code lives here.\n *\n * Keep in byte-for-byte sync with\n * `packages/client-mcp/src/control-api.ts` (`Git*` shapes) and\n * `packages/client-mcp/src/daemon/routes.ts` (error envelopes: 409\n * `non_fast_forward` carries `refs`, 413 `oversize_objects` carries\n * `objects`, 503 `bootstrapping` / 402 `insufficient_gas` are retryable).\n */\n\nimport type { PublishReceipt } from './publisher.js';\nimport type { PlannedObject, PushPlan, PushResult, RefUpdate } from './push.js';\n\n/** One planned ref update (JSON-safe as-is). */\nexport type GitRefUpdate = RefUpdate;\n\n/** One object scheduled for upload (JSON-safe as-is). */\nexport type GitPlannedObject = PlannedObject;\n\n/**\n * `POST /git/estimate` — plan a push (local git plumbing + remote-state read)\n * and price it WITHOUT paying anything. The same body (plus `confirm`) drives\n * `POST /git/push`.\n */\nexport interface GitEstimateRequest {\n /** Path to the local git repository (worktree or .git dir). Must exist. */\n repoPath: string;\n /** Repository identifier (NIP-34 `d` tag). The daemon identity is the owner. */\n repoId: string;\n /**\n * Full refnames to push (e.g. `[\"refs/heads/main\"]`). Default: every local\n * branch and tag.\n */\n refspecs?: string[];\n /** Allow non-fast-forward updates (default false → 409 `non_fast_forward`). */\n force?: boolean;\n /**\n * Relay URLs to read remote state from and publish to. Plural from day one\n * (forward-compat); defaults to the daemon's config-seeded relay.\n */\n relayUrls?: string[];\n /** Repo name/description for the first-push kind:30617 announcement. */\n announcement?: { name?: string; description?: string };\n}\n\n/** Pre-push fee table (all fees in base/micro units, decimal strings). */\nexport interface GitFeeEstimate {\n objectCount: number;\n totalObjectBytes: number;\n /** Σ size × uploadFeePerByte. */\n uploadFee: string;\n /** Events to publish (refs event + announcement on first push). */\n eventCount: number;\n /** eventCount × per-event fee. */\n eventFees: string;\n /** uploadFee + eventFees. */\n totalFee: string;\n /**\n * Zero-byte objects (the git empty blob) excluded from the upload — the\n * store rejects zero-byte content as malformed, so they are skipped on push\n * and reconstructed on clone/fetch. Optional for wire compatibility with\n * daemons predating the empty-blob handling. Default 0.\n */\n skippedEmptyCount?: number;\n}\n\n/** Serialized `PushPlan` — everything a confirm UI needs. */\nexport interface GitEstimateResponse {\n repoId: string;\n refUpdates: GitRefUpdate[];\n /** Full new ref state to publish (HEAD target first). */\n newRefs: Record<string, string>;\n headSymref: string | null;\n objects: GitPlannedObject[];\n /** sha→txId hints known WITHOUT uploading (remote tags + resolver finds). */\n knownShaToTxId: Record<string, string>;\n /** True when no kind:30617 exists yet — the push announces first. */\n announceNeeded: boolean;\n announcement: { name: string; description: string };\n estimate: GitFeeEstimate;\n}\n\n/**\n * `POST /git/push` — plan + execute: upload the delta to Arweave and publish\n * the cumulative kind:30618 (+ kind:30617 on first push). PERMANENT + PAID.\n */\nexport interface GitPushRequest extends GitEstimateRequest {\n /** Must be literally `true` — a push spends channel funds irreversibly. */\n confirm: boolean;\n}\n\n/** One object-upload step result. */\nexport interface GitUploadStep {\n sha: string;\n txId: string;\n /** '0' when skipped (already on Arweave — content-addressed resume). */\n feePaid: string;\n skipped: boolean;\n}\n\n/** Receipt for one published event. */\nexport interface GitPublishReceipt {\n eventId: string;\n feePaid: string;\n}\n\n/** Serialized `PushResult` — per-step receipts + total fees actually paid. */\nexport interface GitPushResponse {\n repoId: string;\n refUpdates: GitRefUpdate[];\n /** Per-object results, in plan order. */\n uploads: GitUploadStep[];\n /** kind:30617 receipt, or null when the repo was already announced. */\n announceReceipt: GitPublishReceipt | null;\n /** kind:30618 (cumulative refs + arweave map) receipt. */\n refsReceipt: GitPublishReceipt;\n /** Full sha→txId map published in the refs event. */\n arweaveMap: Record<string, string>;\n /** Total fees actually paid (uploads + events), base units, decimal. */\n totalFeePaid: string;\n /** The pre-push estimate the push ran under (compare against totalFeePaid). */\n estimate: GitFeeEstimate;\n}\n\n// ---------------------------------------------------------------------------\n// Single-event git publishes (`/git/issue|comment|patch|status`, #231)\n// ---------------------------------------------------------------------------\n\n/** NIP-34 repository address: the owner+id pair behind `a` tags. */\nexport interface GitRepoAddr {\n /** Repository owner's Nostr pubkey (64-char hex) — author of kind:30617/30618. */\n ownerPubkey: string;\n /** Repository identifier (NIP-34 `d` tag). */\n repoId: string;\n}\n\n/** `POST /git/issue` — publish a kind:1621 issue against a repo. PAID. */\nexport interface GitIssueRequest {\n repoAddr: GitRepoAddr;\n /** Issue title (`subject` tag). */\n title: string;\n /** Issue body (Markdown content). */\n body: string;\n /** Labels (`t` tags). */\n labels?: string[];\n}\n\n/** `POST /git/comment` — publish a kind:1622 comment on an issue/patch. PAID. */\nexport interface GitCommentRequest {\n repoAddr: GitRepoAddr;\n /** Event id of the issue or patch being commented on. */\n rootEventId: string;\n /** Comment body (Markdown content). */\n body: string;\n /**\n * Pubkey of the TARGET event's author (NIP-34 `p` threading tag — not the\n * comment author). Defaults to the repo owner.\n */\n parentAuthorPubkey?: string;\n /** `e`-tag marker (default 'root': commenting directly on the issue/patch). */\n marker?: 'root' | 'reply';\n}\n\n/**\n * `POST /git/patch` — publish a kind:1617 patch. Supply EXACTLY ONE of\n * `patchText` (literal `git format-patch` output) or `repoPath`+`range`\n * (the daemon runs `git format-patch --stdout <range>` locally). PAID.\n */\nexport interface GitPatchRequest {\n repoAddr: GitRepoAddr;\n /** Patch/PR title (`subject` tag). */\n title: string;\n /**\n * PR body/cover text (`description` tag). Kept OUT of the event content so\n * `git am` still consumes the patch text verbatim (#280).\n */\n description?: string;\n /** Literal patch text. Mutually exclusive with `repoPath`+`range`. */\n patchText?: string;\n /** Local repository to run format-patch in. Requires `range`. */\n repoPath?: string;\n /** Revision range for format-patch (`<rev>`, `<rev>..<rev>`, `<rev>...<rev>`). */\n range?: string;\n /** Commit/parent pairs for `commit`/`parent-commit` tags. */\n commits?: { sha: string; parentSha: string }[];\n /** Branch name for the `t` tag. */\n branch?: string;\n}\n\nexport type GitStatusValue = 'open' | 'applied' | 'closed' | 'draft';\n\n/** `POST /git/status` — publish a kind:1630-1633 status event. PAID. */\nexport interface GitStatusRequest {\n repoAddr: GitRepoAddr;\n /** Event id of the issue/patch whose status is being set. */\n targetEventId: string;\n /** open → 1630, applied → 1631, closed → 1632, draft → 1633. */\n status: GitStatusValue;\n /** Pubkey of the target event's author (`p` tag), when known. */\n targetPubkey?: string;\n}\n\n/**\n * Response of the single-event git publishes (issue/comment/patch/status):\n * a publish receipt plus the NIP-34 kind that was published. Daemon\n * responses extend the full `POST /publish` receipt, so the channel fields\n * (`channelId`/`nonce`/…) are present there; they are optional here because\n * the CLI's standalone path publishes through the embedded client and has\n * no channel wire shape to report.\n */\nexport interface GitEventResponse {\n /** Event ID as accepted by the relay. */\n eventId: string;\n /** Fee actually paid for this publish, base units, decimal string. */\n feePaid: string;\n /** The NIP-34 kind that was published. */\n kind: number;\n /** Channel the claim was signed against (daemon responses). */\n channelId?: string;\n /** Channel nonce after this publish (daemon responses). */\n nonce?: number;\n /** FULFILL response data (base64), when the backend returned any. */\n data?: string;\n /** Spendable channel balance after this write, when known. */\n channelBalanceAfter?: string;\n}\n\n/**\n * Uniform error envelope of non-2xx control-route responses. Structured\n * errors put extra fields at the top level: `non_fast_forward` (409) adds\n * `refs`, `oversize_objects` (413) adds `objects`.\n */\nexport interface GitErrorEnvelope {\n error: string;\n detail?: string;\n /** True when the caller should retry (e.g. daemon still bootstrapping). */\n retryable?: boolean;\n [extra: string]: unknown;\n}\n\n// ---------------------------------------------------------------------------\n// Serializers (pure) — the exact mapping the daemon routes apply; used by the\n// CLI's standalone mode so both surfaces emit identical wire JSON.\n// ---------------------------------------------------------------------------\n\n/** Serialize a plan's fee estimate onto the wire (bigints → strings). */\nexport function serializeFeeEstimate(plan: PushPlan): GitFeeEstimate {\n return {\n objectCount: plan.estimate.objectCount,\n totalObjectBytes: plan.estimate.totalObjectBytes,\n uploadFee: plan.estimate.uploadFee.toString(),\n eventCount: plan.estimate.eventCount,\n eventFees: plan.estimate.eventFees.toString(),\n totalFee: plan.estimate.totalFee.toString(),\n ...(plan.estimate.skippedEmptyCount > 0\n ? { skippedEmptyCount: plan.estimate.skippedEmptyCount }\n : {}),\n };\n}\n\n/** Serialize a PushPlan onto the wire (bigints → strings, Maps → records). */\nexport function serializePushPlan(plan: PushPlan): GitEstimateResponse {\n return {\n repoId: plan.repoId,\n refUpdates: plan.refUpdates,\n newRefs: plan.newRefs,\n headSymref: plan.headSymref,\n objects: plan.objects,\n knownShaToTxId: Object.fromEntries(plan.knownShaToTxId),\n announceNeeded: plan.announceNeeded,\n announcement: plan.announcement,\n estimate: serializeFeeEstimate(plan),\n };\n}\n\n/**\n * Serialize a standalone {@link PublishReceipt} into the wire shape the\n * daemon's single-event `/git/*` routes answer with, so `--json` consumers\n * see one `GitEventResponse` shape regardless of publisher mode.\n */\nexport function serializeEventReceipt(\n kind: number,\n receipt: PublishReceipt\n): GitEventResponse {\n return {\n eventId: receipt.eventId,\n feePaid: receipt.feePaid.toString(),\n kind,\n };\n}\n\n/** Serialize a PushResult onto the wire (bigints → strings, Maps → records). */\nexport function serializePushResult(\n plan: PushPlan,\n result: PushResult\n): GitPushResponse {\n return {\n repoId: plan.repoId,\n refUpdates: plan.refUpdates,\n uploads: result.uploads.map((u) => ({\n sha: u.sha,\n txId: u.txId,\n feePaid: u.feePaid.toString(),\n skipped: u.skipped,\n })),\n announceReceipt: result.announceReceipt\n ? {\n eventId: result.announceReceipt.eventId,\n feePaid: result.announceReceipt.feePaid.toString(),\n }\n : null,\n refsReceipt: {\n eventId: result.refsReceipt.eventId,\n feePaid: result.refsReceipt.feePaid.toString(),\n },\n arweaveMap: Object.fromEntries(result.arweaveMap),\n totalFeePaid: result.totalFeePaid.toString(),\n estimate: serializeFeeEstimate(plan),\n };\n}\n","/**\n * Push planner/executor — the core of `rig push` (epic #222, ticket #226).\n *\n * `planPush` is network-free (relay/Arweave-wise — it only runs local git\n * plumbing through GitRepoReader plus one injectable async resolver step):\n * it classifies every ref update, computes the object delta against what the\n * remote already stores, hard-errors on oversize objects, and prices the\n * push. The returned {@link PushPlan} carries everything a confirm UI needs.\n *\n * `executePush` spends money: it uploads the planned objects through a\n * {@link Publisher} (ref-tip objects last, so a crashed push never leads to\n * a state where a discoverable tip's history is missing), then publishes ONE\n * cumulative kind:30618 whose `arweave` tags are the MERGE of the remote's\n * existing sha→txId map and the new uploads — kind:30618 is NIP-33\n * replaceable, so dropping prior tags would orphan earlier hints — and whose\n * `r` tags are the full new ref state. On a first push it publishes the\n * kind:30617 announcement before the refs event.\n *\n * Resume safety: uploads are content-addressed (Git-SHA-tagged), so re-running\n * `executePush` after a crash is safe — it consults the merged\n * remote + planned sha→txId map before paying for any upload, and a re-plan\n * with fresh remote state (whose `resolveMissing` finds the already-uploaded\n * objects via GraphQL) skips them entirely.\n */\n\nimport { buildRepoAnnouncement, buildRepoRefs } from './nip34-events.js';\nimport {\n EMPTY_BLOB_SHA,\n MAX_OBJECT_SIZE,\n type GitObjectType,\n} from './objects.js';\nimport type {\n FeeRates,\n PublishReceipt,\n Publisher,\n} from './publisher.js';\nimport { GitError, type GitRef, type GitRepoReader } from './repo-reader.js';\nimport type { RemoteState } from './remote-state.js';\n\n// ---------------------------------------------------------------------------\n// Errors\n// ---------------------------------------------------------------------------\n\n/** A ref update rejected because it is not a fast-forward. */\nexport interface RejectedRefUpdate {\n refname: string;\n localSha: string;\n remoteSha: string;\n}\n\n/** Thrown by {@link planPush} when a non-fast-forward update lacks `force`. */\nexport class NonFastForwardError extends Error {\n constructor(\n /** The refs that would need `--force` to update. */\n public readonly refs: RejectedRefUpdate[]\n ) {\n super(\n `non-fast-forward update rejected for ${refs\n .map((r) => r.refname)\n .join(', ')} — re-run with force to overwrite the remote ref(s)`\n );\n this.name = 'NonFastForwardError';\n }\n}\n\n/** One object exceeding {@link MAX_OBJECT_SIZE}. */\nexport interface OversizeObject {\n sha: string;\n type: GitObjectType;\n /** Body size in bytes. */\n size: number;\n /** Path the object was reached by (blobs / non-root trees), if known. */\n path?: string;\n}\n\n/**\n * Thrown by {@link planPush} when any object in the delta exceeds the 95KB\n * upload limit (hard error in v1 — the paid blob path is a follow-up spike).\n */\nexport class OversizeObjectsError extends Error {\n constructor(\n /** The offending objects with paths and sizes. */\n public readonly objects: OversizeObject[]\n ) {\n super(\n `${objects.length} object(s) exceed the ${MAX_OBJECT_SIZE} byte upload limit: ` +\n objects\n .map((o) => `${o.path ?? o.sha} (${o.size} bytes)`)\n .join(', ')\n );\n this.name = 'OversizeObjectsError';\n }\n}\n\n// ---------------------------------------------------------------------------\n// Plan types\n// ---------------------------------------------------------------------------\n\n/** How a ref moves relative to the remote. */\nexport type RefUpdateKind =\n /** Ref does not exist on the remote yet. */\n | 'new'\n /** Remote tip is an ancestor of the local tip. */\n | 'fast-forward'\n /** Non-fast-forward, allowed because `force` was set. */\n | 'forced'\n /** Local and remote tips already match — nothing to push. */\n | 'up-to-date';\n\n/** One planned ref update (deletions are out of scope in v1). */\nexport interface RefUpdate {\n /** Full refname, e.g. `refs/heads/main`. */\n refname: string;\n /** Local tip SHA (tag object SHA for annotated tags). */\n localSha: string;\n /** Remote tip SHA, or null when the ref is new. */\n remoteSha: string | null;\n kind: RefUpdateKind;\n}\n\n/** One object scheduled for upload. */\nexport interface PlannedObject {\n sha: string;\n type: GitObjectType;\n /** Body size in bytes (what the upload fee is charged on). */\n size: number;\n /** Path the object was reached by, if any (blobs / non-root trees). */\n path?: string;\n /** True when this SHA is the tip of a planned ref update (uploaded last). */\n isRefTip: boolean;\n}\n\n/** Pre-push fee estimate — render this in the confirm table. */\nexport interface PushFeeEstimate {\n /** Number of objects to upload. */\n objectCount: number;\n /** Total bytes across all planned object bodies. */\n totalObjectBytes: number;\n /** Σ size × uploadFeePerByte (smallest asset unit). */\n uploadFee: bigint;\n /** Number of events to publish (refs event + announcement on first push). */\n eventCount: number;\n /** eventCount × eventFee (smallest asset unit). */\n eventFees: bigint;\n /** uploadFee + eventFees. */\n totalFee: bigint;\n /**\n * Objects excluded from the upload because their body is zero bytes — the\n * git empty blob, which the store rejects as malformed (F00). Reconstructed\n * locally on clone/fetch, so nothing is lost; surfaced here so the fee table\n * can report the skip honestly.\n */\n skippedEmptyCount: number;\n}\n\n/** Everything `executePush` (and a confirm UI) needs. */\nexport interface PushPlan {\n repoId: string;\n /** Every considered ref with its classification (incl. up-to-date). */\n refUpdates: RefUpdate[];\n /**\n * Full new ref state to publish as `r` tags: the remote's refs overlaid\n * with the planned updates (refs not being pushed are preserved — v1\n * never deletes). Ordered with the HEAD target first, which is what\n * `buildRepoRefs` derives the HEAD symref tag from.\n */\n newRefs: Record<string, string>;\n /** HEAD symref target for the new state (first key of {@link newRefs}). */\n headSymref: string | null;\n /**\n * Objects to upload, dependency-safe order: ref-tip objects last so a\n * crashed push never uploads a tip whose history is missing.\n */\n objects: PlannedObject[];\n /**\n * Zero-byte objects (the git empty blob) excluded from {@link objects}: the\n * store rejects a zero-byte kind:5094 upload as malformed (F00), so `rig`\n * never uploads it — the commit/tree still references it and clone/fetch\n * synthesizes it locally. Kept for honest receipts, never uploaded.\n */\n skippedEmptyObjects: PlannedObject[];\n /**\n * sha→txId hints known WITHOUT uploading: the remote's `arweave` tags\n * plus anything `resolveMissing` found. Merged into the published\n * kind:30618 so prior hints are never dropped.\n */\n knownShaToTxId: Map<string, string>;\n /** True when no kind:30617 exists yet — executePush announces first. */\n announceNeeded: boolean;\n /** Announcement metadata used when {@link announceNeeded}. */\n announcement: { name: string; description: string };\n estimate: PushFeeEstimate;\n}\n\nexport interface PlanPushOptions {\n repoReader: GitRepoReader;\n remoteState: RemoteState;\n /** Fee rates from `Publisher.getFeeRates()`. */\n feeRates: FeeRates;\n /** Repository identifier (NIP-34 `d` tag). */\n repoId: string;\n /**\n * Full refnames to push (e.g. `['refs/heads/main']`). Defaults to every\n * local branch and tag. Refs that don't exist locally are an error\n * (deletions are out of scope in v1).\n */\n refs?: string[];\n /** Allow non-fast-forward updates (default false → hard error). */\n force?: boolean;\n /** Repo name/description for the first-push announcement. */\n announcement?: { name?: string; description?: string };\n /**\n * Async resolver for SHAs the remote's `arweave` tags don't cover —\n * consulted before deciding to re-upload. Defaults to\n * `remoteState.resolveMissing` (GraphQL fallback); injectable so the\n * planner core stays testable without network.\n */\n resolveMissing?: (shas: string[]) => Promise<Map<string, string>>;\n}\n\n// ---------------------------------------------------------------------------\n// planPush\n// ---------------------------------------------------------------------------\n\n/**\n * Classify ref updates, compute the object delta, enforce the size limit,\n * and price the push. Throws {@link NonFastForwardError} /\n * {@link OversizeObjectsError} (both carry structured data for UIs).\n */\nexport async function planPush(options: PlanPushOptions): Promise<PushPlan> {\n const { repoReader, remoteState, feeRates, repoId, force = false } = options;\n const resolveMissing =\n options.resolveMissing ?? remoteState.resolveMissing.bind(remoteState);\n\n // 1. Select and classify refs. -------------------------------------------\n const { head, refs: localRefs } = await repoReader.listRefs();\n const localByName = new Map(localRefs.map((r) => [r.refname, r]));\n\n let selected: GitRef[];\n if (options.refs !== undefined) {\n selected = options.refs.map((name) => {\n const ref = localByName.get(name);\n if (!ref) {\n throw new Error(\n `ref ${JSON.stringify(name)} does not exist locally ` +\n '(ref deletion is out of scope in v1)'\n );\n }\n return ref;\n });\n } else {\n selected = localRefs;\n }\n\n const refUpdates: RefUpdate[] = [];\n const rejected: RejectedRefUpdate[] = [];\n for (const ref of selected) {\n const remoteSha = remoteState.refs.get(ref.refname) ?? null;\n if (remoteSha === null) {\n refUpdates.push({ refname: ref.refname, localSha: ref.sha, remoteSha, kind: 'new' });\n continue;\n }\n if (remoteSha === ref.sha) {\n refUpdates.push({ refname: ref.refname, localSha: ref.sha, remoteSha, kind: 'up-to-date' });\n continue;\n }\n let fastForward = false;\n try {\n fastForward = await repoReader.isAncestor(remoteSha, ref.sha);\n } catch (err) {\n // Remote tip unknown locally (never fetched) or not a commit-ish —\n // we can't prove ancestry, so treat it as non-fast-forward.\n if (!(err instanceof GitError)) throw err;\n fastForward = false;\n }\n if (fastForward) {\n refUpdates.push({ refname: ref.refname, localSha: ref.sha, remoteSha, kind: 'fast-forward' });\n } else if (force) {\n refUpdates.push({ refname: ref.refname, localSha: ref.sha, remoteSha, kind: 'forced' });\n } else {\n rejected.push({ refname: ref.refname, localSha: ref.sha, remoteSha });\n }\n }\n if (rejected.length > 0) throw new NonFastForwardError(rejected);\n\n const updates = refUpdates.filter((u) => u.kind !== 'up-to-date');\n\n // 2. Object delta: reachable from the new tips, minus what the remote has.\n const wantTips = [...new Set(updates.map((u) => u.localSha))];\n const haveTips = [...new Set(remoteState.refs.values())];\n const delta =\n wantTips.length > 0\n ? await repoReader.objectsBetweenWithPaths(wantTips, haveTips)\n : [];\n\n const knownShaToTxId = new Map(remoteState.shaToTxId);\n let candidates = delta.filter((o) => !knownShaToTxId.has(o.sha));\n if (candidates.length > 0) {\n // The remote's `arweave` tags may lag reality (e.g. a crashed push\n // uploaded objects but never published the refs event) — resolve the\n // gaps before paying to re-upload content-addressed data.\n const resolved = await resolveMissing(candidates.map((o) => o.sha));\n for (const [sha, txId] of resolved) knownShaToTxId.set(sha, txId);\n candidates = candidates.filter((o) => !knownShaToTxId.has(o.sha));\n }\n\n // 3. Sizes + oversize hard error. -----------------------------------------\n const pathBySha = new Map(candidates.map((c) => [c.sha, c.path]));\n const { objects: stats, missing } = await repoReader.statObjects(\n candidates.map((c) => c.sha)\n );\n if (missing.length > 0) {\n throw new Error(\n `objects vanished from the local repository during planning: ${missing.join(', ')}`\n );\n }\n\n const oversize: OversizeObject[] = [];\n for (const stat of stats) {\n if (stat.size > MAX_OBJECT_SIZE) {\n const path = pathBySha.get(stat.sha);\n oversize.push({ ...stat, ...(path ? { path } : {}) });\n }\n }\n if (oversize.length > 0) throw new OversizeObjectsError(oversize);\n\n // 4. Split off the empty blob, then order the rest ref-tips-last. ----------\n // The git empty blob uploads as an empty kind:5094 `i` value, which the\n // store rejects as malformed (F00). Skip it: it is reconstructed locally on\n // clone/fetch. Keyed off the EXACT empty-blob constant SHA — NOT a\n // `size === 0` heuristic, which would also match the (distinct, valid) empty\n // TREE object `4b825dc6…` and silently drop it (it is not synthesized on\n // read). An object whose SHA is EMPTY_BLOB_SHA is provably the empty blob.\n const tipShas = new Set(updates.map((u) => u.localSha));\n const planned: PlannedObject[] = [];\n const skippedEmptyObjects: PlannedObject[] = [];\n for (const stat of stats) {\n const path = pathBySha.get(stat.sha);\n const object: PlannedObject = {\n ...stat,\n ...(path ? { path } : {}),\n isRefTip: tipShas.has(stat.sha),\n };\n if (stat.sha === EMPTY_BLOB_SHA) skippedEmptyObjects.push(object);\n else planned.push(object);\n }\n const objects = [\n ...planned.filter((o) => !o.isRefTip),\n ...planned.filter((o) => o.isRefTip),\n ];\n\n // 5. Full new ref state (remote refs overlaid with updates), HEAD first. --\n const newRefsMap = new Map(remoteState.refs);\n for (const update of updates) newRefsMap.set(update.refname, update.localSha);\n\n const headSymref =\n head && newRefsMap.has(head)\n ? head\n : remoteState.headSymref && newRefsMap.has(remoteState.headSymref)\n ? remoteState.headSymref\n : ([...newRefsMap.keys()][0] ?? null);\n\n const newRefs: Record<string, string> = {};\n const headSha = headSymref ? newRefsMap.get(headSymref) : undefined;\n if (headSymref && headSha) newRefs[headSymref] = headSha;\n for (const [refname, sha] of newRefsMap) {\n if (refname !== headSymref) newRefs[refname] = sha;\n }\n\n // 6. Fee estimate. ---------------------------------------------------------\n const announceNeeded = !remoteState.announced;\n const totalObjectBytes = objects.reduce((sum, o) => sum + o.size, 0);\n const uploadFee = BigInt(totalObjectBytes) * feeRates.uploadFeePerByte;\n const eventCount = 1 + (announceNeeded ? 1 : 0);\n const eventFees = BigInt(eventCount) * feeRates.eventFee;\n\n return {\n repoId,\n refUpdates,\n newRefs,\n headSymref,\n objects,\n skippedEmptyObjects,\n knownShaToTxId,\n announceNeeded,\n announcement: {\n name: options.announcement?.name ?? repoId,\n description: options.announcement?.description ?? '',\n },\n estimate: {\n objectCount: objects.length,\n totalObjectBytes,\n uploadFee,\n eventCount,\n eventFees,\n totalFee: uploadFee + eventFees,\n skippedEmptyCount: skippedEmptyObjects.length,\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// executePush\n// ---------------------------------------------------------------------------\n\n/** Result of one object-upload step. */\nexport interface UploadStepResult {\n sha: string;\n txId: string;\n /** 0n when skipped (already uploaded — content-addressed resume). */\n feePaid: bigint;\n /** True when the object was already on Arweave and nothing was paid. */\n skipped: boolean;\n}\n\n/** Result of the whole push. */\nexport interface PushResult {\n /** Per-object results, in plan order. */\n uploads: UploadStepResult[];\n /** kind:30617 receipt, or null when the repo was already announced. */\n announceReceipt: PublishReceipt | null;\n /** kind:30618 (cumulative refs + arweave map) receipt. */\n refsReceipt: PublishReceipt;\n /**\n * The full sha→txId map published in the refs event: remote hints +\n * resolver finds + this push's uploads.\n */\n arweaveMap: Map<string, string>;\n /** Total fees actually paid (uploads + events), smallest asset unit. */\n totalFeePaid: bigint;\n}\n\nexport interface ExecutePushOptions {\n plan: PushPlan;\n publisher: Publisher;\n /**\n * Remote state — pass a FRESH fetch when resuming after a crash so its\n * `shaToTxId` (and `announced`) reflect what the previous attempt already\n * paid for.\n */\n remoteState: RemoteState;\n repoReader: GitRepoReader;\n /** Relay URLs to publish events to (plural from day one; size 1 today). */\n relayUrls: string[];\n}\n\n/** How many object bodies to hold in memory at once between read and upload. */\nconst READ_BATCH_SIZE = 100;\n\n/**\n * Execute a {@link PushPlan}: upload objects (ref tips last), then publish\n * the kind:30617 announcement (first push only) and ONE cumulative\n * kind:30618 whose `arweave` tags merge every known sha→txId hint with the\n * new uploads and whose `r` tags carry the full new ref state.\n *\n * Safe to re-run after a crash: SHAs already present in the merged\n * remote + plan map are skipped without paying.\n */\nexport async function executePush(\n options: ExecutePushOptions\n): Promise<PushResult> {\n const { plan, publisher, remoteState, repoReader, relayUrls } = options;\n\n // Merged sha→txId map: remote hints (fresh on resume) + plan-time finds.\n // Consulted before every upload — this is the resume-safety check.\n const merged = new Map([...remoteState.shaToTxId, ...plan.knownShaToTxId]);\n\n const resultBySha = new Map<string, UploadStepResult>();\n let totalFeePaid = 0n;\n\n const pending: PlannedObject[] = [];\n for (const object of plan.objects) {\n const knownTxId = merged.get(object.sha);\n if (knownTxId !== undefined) {\n resultBySha.set(object.sha, {\n sha: object.sha,\n txId: knownTxId,\n feePaid: 0n,\n skipped: true,\n });\n } else {\n pending.push(object);\n }\n }\n\n // Upload in plan order (ref tips are already last), reading bodies in\n // batches so memory stays bounded on large pushes.\n for (let i = 0; i < pending.length; i += READ_BATCH_SIZE) {\n const batch = pending.slice(i, i + READ_BATCH_SIZE);\n const { objects: read, missing } = await repoReader.readObjects(\n batch.map((o) => o.sha)\n );\n if (missing.length > 0) {\n throw new Error(\n `objects vanished from the local repository during push: ${missing.join(', ')}`\n );\n }\n const bodyBySha = new Map(read.map((r) => [r.sha, r.body]));\n for (const object of batch) {\n const body = bodyBySha.get(object.sha);\n if (!body) {\n throw new Error(\n `internal: cat-file returned no body for ${object.sha}`\n );\n }\n const receipt = await publisher.uploadGitObject({\n sha: object.sha,\n type: object.type,\n body,\n repoId: plan.repoId,\n });\n merged.set(object.sha, receipt.txId);\n totalFeePaid += receipt.feePaid;\n resultBySha.set(object.sha, {\n sha: object.sha,\n txId: receipt.txId,\n feePaid: receipt.feePaid,\n skipped: false,\n });\n }\n }\n\n // Announcement (first push only) goes before the refs event so a repo is\n // never referenced by an `a` tag before its kind:30617 exists. Re-check\n // the (fresh-on-resume) remote state so a crashed push doesn't announce\n // twice.\n let announceReceipt: PublishReceipt | null = null;\n if (plan.announceNeeded && !remoteState.announced) {\n const announceEvent = buildRepoAnnouncement(\n plan.repoId,\n plan.announcement.name,\n plan.announcement.description\n );\n announceReceipt = await publisher.publishEvent(announceEvent, relayUrls);\n totalFeePaid += announceReceipt.feePaid;\n }\n\n // ONE cumulative kind:30618: full ref state + MERGED arweave map (NIP-33\n // replaceable — dropping prior tags would orphan earlier sha→txId hints).\n const refsEvent = buildRepoRefs(\n plan.repoId,\n plan.newRefs,\n Object.fromEntries(merged)\n );\n const refsReceipt = await publisher.publishEvent(refsEvent, relayUrls);\n totalFeePaid += refsReceipt.feePaid;\n\n const uploads = plan.objects.map((o) => {\n const step = resultBySha.get(o.sha);\n if (!step) {\n throw new Error(`internal: no upload result recorded for ${o.sha}`);\n }\n return step;\n });\n\n return {\n uploads,\n announceReceipt,\n refsReceipt,\n arweaveMap: merged,\n totalFeePaid,\n };\n}\n","var __defProp = Object.defineProperty;\nvar __getOwnPropDesc = Object.getOwnPropertyDescriptor;\nvar __getOwnPropNames = Object.getOwnPropertyNames;\nvar __hasOwnProp = Object.prototype.hasOwnProperty;\nvar __require = /* @__PURE__ */ ((x) => typeof require !== \"undefined\" ? require : typeof Proxy !== \"undefined\" ? new Proxy(x, {\n get: (a, b) => (typeof require !== \"undefined\" ? require : a)[b]\n}) : x)(function(x) {\n if (typeof require !== \"undefined\") return require.apply(this, arguments);\n throw Error('Dynamic require of \"' + x + '\" is not supported');\n});\nvar __esm = (fn, res) => function __init() {\n return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;\n};\nvar __commonJS = (cb, mod) => function __require2() {\n return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;\n};\nvar __export = (target, all) => {\n for (var name in all)\n __defProp(target, name, { get: all[name], enumerable: true });\n};\nvar __copyProps = (to, from, except, desc) => {\n if (from && typeof from === \"object\" || typeof from === \"function\") {\n for (let key of __getOwnPropNames(from))\n if (!__hasOwnProp.call(to, key) && key !== except)\n __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });\n }\n return to;\n};\nvar __toCommonJS = (mod) => __copyProps(__defProp({}, \"__esModule\", { value: true }), mod);\n\nexport {\n __require,\n __esm,\n __commonJS,\n __export,\n __toCommonJS\n};\n//# sourceMappingURL=chunk-UP2VWCW5.js.map","/**\n * SDK-specific error classes for @toon-protocol/sdk.\n * All errors extend ToonError from @toon-protocol/core for a consistent error hierarchy.\n */\n\nimport { ToonError } from '@toon-protocol/core';\n\n/**\n * Error thrown when identity operations fail.\n * Used for invalid mnemonics, invalid secret keys, and key derivation failures.\n */\nexport class IdentityError extends ToonError {\n constructor(message: string, cause?: Error) {\n super(message, 'IDENTITY_ERROR', cause);\n this.name = 'IdentityError';\n }\n}\n\n/**\n * Error thrown when node lifecycle operations fail.\n * Used for start/stop failures, configuration errors, etc.\n */\nexport class NodeError extends ToonError {\n constructor(message: string, cause?: Error) {\n super(message, 'NODE_ERROR', cause);\n this.name = 'NodeError';\n }\n}\n\n/**\n * Error thrown when handler dispatch operations fail.\n * Used for handler registration conflicts, missing handlers, and dispatch errors.\n */\nexport class HandlerError extends ToonError {\n constructor(message: string, cause?: Error) {\n super(message, 'HANDLER_ERROR', cause);\n this.name = 'HandlerError';\n }\n}\n\n/**\n * Error thrown when Schnorr signature verification fails.\n * Used for invalid signatures, malformed events, and verification pipeline errors.\n */\nexport class VerificationError extends ToonError {\n constructor(message: string, cause?: Error) {\n super(message, 'VERIFICATION_ERROR', cause);\n this.name = 'VerificationError';\n }\n}\n\n/**\n * Error thrown when payment validation fails.\n * Used for pricing calculation errors, insufficient payment, and pricing policy violations.\n */\nexport class PricingError extends ToonError {\n constructor(message: string, cause?: Error) {\n super(message, 'PRICING_ERROR', cause);\n this.name = 'PricingError';\n }\n}\n\n/**\n * Error thrown when NIP-59 gift wrap or NIP-44 FULFILL encryption operations fail.\n * Used for wrap/unwrap failures, decryption errors, and malformed gift wrap events.\n */\nexport class GiftWrapError extends ToonError {\n constructor(message: string, cause?: Error) {\n super(message, 'GIFT_WRAP_ERROR', cause);\n this.name = 'GiftWrapError';\n }\n}\n\n/**\n * Error thrown when Mill swap handler orchestration fails.\n * Used for rate-conversion errors (invalid format, zero, overflow guards),\n * unsupported pair lookups, and issuer-boundary failures that are NOT\n * gift-wrap-specific. Gift-wrap failures continue to surface as `GiftWrapError`.\n */\nexport class SwapHandlerError extends ToonError {\n constructor(message: string, cause?: Error) {\n super(message, 'SWAP_HANDLER_ERROR', cause);\n this.name = 'SwapHandlerError';\n }\n}\n\n/**\n * Error thrown by the sender-side `streamSwap()` API (Story 12.5).\n *\n * All failures are categorized by a narrow `code` so callers can branch on\n * cause. `INVALID_*` codes are construction-time validation failures (thrown\n * synchronously before any packet fires). `FULFILL_DECODE_FAILED` surfaces\n * when the Mill returns `accepted: true` but the FULFILL data cannot be\n * decoded — this is a non-fatal per-packet error and is captured in\n * `StreamSwapResult.errors[]`.\n */\nexport class StreamSwapError extends Error {\n readonly code:\n | 'INVALID_AMOUNT'\n | 'INVALID_CHUNKING'\n | 'INVALID_PAIR'\n | 'INVALID_STATE'\n | 'INVALID_CHAIN_RECIPIENT'\n | 'FULFILL_DECODE_FAILED';\n // Not declared on Error in lib.es5; ES2022 adds it, but some tsconfigs\n // still target older libs. Declare explicitly for cross-version safety.\n declare readonly cause?: unknown;\n\n constructor(\n code: StreamSwapError['code'],\n message: string,\n options?: { cause?: unknown }\n ) {\n super(message);\n this.name = 'StreamSwapError';\n this.code = code;\n if (options && 'cause' in options) {\n Object.defineProperty(this, 'cause', {\n value: options.cause,\n enumerable: false,\n writable: true,\n configurable: true,\n });\n }\n }\n}\n\n/**\n * Error thrown by the sender-side `buildSettlementTx()` helper (Story 12.6).\n *\n * Settlement is a post-swap one-shot computation, so this error class is\n * THROWN synchronously (unlike `streamSwap` which routes per-packet failures\n * through `StreamSwapResult`). Callers are expected to wrap the call in\n * `try/catch`.\n *\n * Narrow `code` union lets callers branch on cause — see\n * `_bmad-output/implementation-artifacts/12-6-build-settlement-tx.md` AC-11\n * for the per-code semantics.\n *\n * @since 12.6\n * @stable — Epic 13 Chain Bridge DVM depends on this error shape.\n */\nexport class SettlementTxError extends Error {\n readonly code:\n | 'INVALID_INPUT'\n | 'MISSING_SETTLEMENT_METADATA'\n | 'UNSUPPORTED_CHAIN'\n | 'MISSING_RECIPIENT'\n | 'RECIPIENT_MISMATCH'\n | 'MILL_SIGNER_MISMATCH'\n | 'DUPLICATE_NONCE'\n | 'NON_MONOTONIC_CUMULATIVE'\n | 'INVALID_SIGNATURE_LENGTH'\n | 'INVALID_SIGNATURE_V'\n | 'ENCODING_FAILED';\n declare readonly cause?: unknown;\n\n constructor(\n code: SettlementTxError['code'],\n message: string,\n options?: { cause?: unknown }\n ) {\n super(message);\n this.name = 'SettlementTxError';\n this.code = code;\n if (options && 'cause' in options) {\n Object.defineProperty(this, 'cause', {\n value: options.cause,\n enumerable: false,\n writable: true,\n configurable: true,\n });\n }\n }\n}\n","/**\n * Unified identity module for @toon-protocol/sdk.\n *\n * Derives both a Nostr pubkey (x-only Schnorr, BIP-340) and an EVM address\n * (Keccak-256) from a single secp256k1 private key, following the NIP-06\n * derivation standard.\n *\n * Both Nostr and EVM use the secp256k1 elliptic curve, so a single 12-word\n * seed phrase can recover the complete identity across both layers.\n */\n\nimport {\n generateMnemonic as _generateMnemonic,\n validateMnemonic,\n mnemonicToSeedSync,\n} from '@scure/bip39';\nimport { wordlist } from '@scure/bip39/wordlists/english.js';\nimport { HDKey } from '@scure/bip32';\nimport { getPublicKey } from 'nostr-tools/pure';\nimport { secp256k1 } from '@noble/curves/secp256k1.js';\nimport { ed25519 } from '@noble/curves/ed25519.js';\nimport { keccak_256 } from '@noble/hashes/sha3.js';\nimport { hmac } from '@noble/hashes/hmac.js';\nimport { sha512 } from '@noble/hashes/sha2.js';\nimport { bytesToHex } from '@noble/hashes/utils.js';\nimport { hexToMinaBase58PrivateKey } from '@toon-protocol/core';\nimport { IdentityError } from './errors.js';\n\n/**\n * Represents a node's complete identity: secret key, Nostr pubkey, and EVM address.\n */\nexport interface NodeIdentity {\n /** The 32-byte secp256k1 secret key. */\n secretKey: Uint8Array;\n /** The x-only Schnorr public key (32 bytes, 64 lowercase hex characters). */\n pubkey: string;\n /** The EIP-55 checksummed EVM address (0x-prefixed, 42 characters). */\n evmAddress: string;\n}\n\n/**\n * Solana Ed25519 identity derived via SLIP-0010 from a BIP-39 mnemonic.\n */\nexport interface SolanaIdentity {\n /** 64-byte Ed25519 keypair (32-byte private key + 32-byte public key). */\n secretKey: Uint8Array;\n /** Base58-encoded Ed25519 public key (Solana address). */\n publicKey: string;\n}\n\n/**\n * Mina Pallas identity derived from a BIP-39 mnemonic via mina-signer.\n */\nexport interface MinaIdentity {\n /** Hex-encoded Pallas private key. */\n privateKey: string;\n /** Base58 Mina public key (B62 prefix). */\n publicKey: string;\n}\n\n/**\n * Full multi-chain identity derived from a single BIP-39 mnemonic.\n * Extends NodeIdentity (Nostr + EVM) with Solana and optionally Mina.\n */\nexport interface ToonIdentity extends NodeIdentity {\n /** Solana Ed25519 identity (always populated from mnemonic derivation). */\n solana: SolanaIdentity;\n /** Mina Pallas identity (undefined when mina-signer is not installed). */\n mina?: MinaIdentity;\n}\n\n/**\n * Options for mnemonic-based key derivation.\n */\nexport interface FromMnemonicOptions {\n /** Key index in the NIP-06 derivation path. Defaults to 0. */\n accountIndex?: number;\n}\n\n/**\n * Generates a valid 12-word BIP-39 mnemonic using 128-bit entropy.\n *\n * @returns A space-separated string of 12 BIP-39 English words.\n */\nexport function generateMnemonic(): string {\n return _generateMnemonic(wordlist, 128);\n}\n\n/**\n * Derives a complete NodeIdentity from a BIP-39 mnemonic phrase.\n *\n * Uses the NIP-06 derivation path: m/44'/1237'/0'/0/{accountIndex}\n *\n * @param mnemonic - A valid BIP-39 mnemonic (12 or 24 words).\n * @param options - Optional derivation options (accountIndex defaults to 0).\n * @returns The derived NodeIdentity with secretKey, pubkey, and evmAddress.\n * @throws {IdentityError} If the mnemonic is invalid.\n */\nexport function fromMnemonic(\n mnemonic: string,\n options?: FromMnemonicOptions\n): ToonIdentity {\n if (!validateMnemonic(mnemonic, wordlist)) {\n throw new IdentityError(\n `Invalid BIP-39 mnemonic: the provided words do not form a valid mnemonic phrase`\n );\n }\n\n const accountIndex = options?.accountIndex ?? 0;\n\n if (\n !Number.isInteger(accountIndex) ||\n accountIndex < 0 ||\n accountIndex > MAX_BIP32_INDEX\n ) {\n throw new IdentityError(\n `Invalid accountIndex: expected a non-negative integer (0 to ${MAX_BIP32_INDEX}), got ${String(accountIndex)}`\n );\n }\n\n const path = `m/44'/1237'/0'/0/${accountIndex}`;\n\n let seed: Uint8Array | undefined;\n try {\n seed = mnemonicToSeedSync(mnemonic);\n const hdKey = HDKey.fromMasterSeed(seed).derive(path);\n\n if (!hdKey.privateKey) {\n throw new IdentityError(`Failed to derive private key at path ${path}`);\n }\n\n const secretKey = hdKey.privateKey;\n const base = deriveIdentity(secretKey);\n\n // Derive Solana Ed25519 identity from the same seed via SLIP-0010,\n // varying by accountIndex so distinct indices yield distinct Solana keys.\n const solana = deriveSolanaIdentity(seed, accountIndex);\n\n return { ...base, solana };\n } catch (error: unknown) {\n if (error instanceof IdentityError) {\n throw error;\n }\n throw new IdentityError(\n `Key derivation failed at path ${path}: ${error instanceof Error ? error.message : String(error)}`,\n error instanceof Error ? error : undefined\n );\n } finally {\n // Best-effort zeroing of the intermediate seed to reduce the window\n // during which sensitive material remains in memory. This is not a\n // guarantee (JS has no secure-erase primitive), but it limits exposure.\n if (seed) {\n seed.fill(0);\n }\n }\n}\n\n/**\n * Derives a complete NodeIdentity from an existing 32-byte secret key.\n *\n * @param secretKey - A 32-byte secp256k1 secret key.\n * @returns The derived NodeIdentity with secretKey, pubkey, and evmAddress.\n * @throws {IdentityError} If the secret key is not exactly 32 bytes.\n */\nexport function fromSecretKey(secretKey: Uint8Array): NodeIdentity {\n if (!(secretKey instanceof Uint8Array)) {\n throw new IdentityError(\n `Invalid secret key: expected Uint8Array, got ${secretKey === null ? 'null' : typeof secretKey}`\n );\n }\n\n if (secretKey.length !== 32) {\n throw new IdentityError(\n `Invalid secret key: expected 32 bytes, got ${secretKey.length} bytes`\n );\n }\n\n try {\n return deriveIdentity(secretKey);\n } catch (error: unknown) {\n if (error instanceof IdentityError) {\n throw error;\n }\n throw new IdentityError(\n `Invalid secret key: ${error instanceof Error ? error.message : String(error)}`,\n error instanceof Error ? error : undefined\n );\n }\n}\n\n/**\n * Maximum valid BIP-32 non-hardened child index (2^31 - 1).\n * Values at or above 2^31 are reserved for hardened derivation.\n */\nconst MAX_BIP32_INDEX = 0x7fffffff;\n\n/**\n * Shared helper that computes pubkey and evmAddress from a 32-byte secret key.\n * Returns a defensive copy of the secret key to prevent external mutation.\n */\nfunction deriveIdentity(secretKey: Uint8Array): NodeIdentity {\n // Compute x-only Schnorr pubkey (32 bytes hex) via nostr-tools\n const pubkey = getPublicKey(secretKey);\n\n // Compute EVM address from the uncompressed secp256k1 public key\n const evmAddress = computeEvmAddress(secretKey);\n\n // Return a defensive copy of the secret key to prevent external mutation\n // from affecting the identity or vice versa\n return { secretKey: new Uint8Array(secretKey), pubkey, evmAddress };\n}\n\n/**\n * Computes an EIP-55 checksummed EVM address from a secp256k1 private key.\n *\n * Steps:\n * 1. Compute the full uncompressed public key (65 bytes: 0x04 + 64 bytes X,Y)\n * 2. Strip the 0x04 prefix to get 64 bytes\n * 3. Hash with Keccak-256\n * 4. Take the last 20 bytes\n * 5. Format as 0x-prefixed hex with EIP-55 checksum\n */\nfunction computeEvmAddress(secretKey: Uint8Array): string {\n // Get the uncompressed public key (65 bytes: 0x04 prefix + 64 bytes X,Y)\n const uncompressedPubkey = secp256k1.getPublicKey(secretKey, false);\n\n // Strip the 0x04 prefix (first byte), leaving 64 bytes of X,Y coordinates\n const pubkeyWithoutPrefix = uncompressedPubkey.slice(1);\n\n // Hash with Keccak-256\n const hash = keccak_256(pubkeyWithoutPrefix);\n\n // Take last 20 bytes\n const addressBytes = hash.slice(-20);\n const addressHex = bytesToHex(addressBytes);\n\n // Apply EIP-55 mixed-case checksum\n return toChecksumAddress(addressHex);\n}\n\n/**\n * Applies EIP-55 mixed-case checksum encoding to an Ethereum address.\n *\n * EIP-55 rules:\n * - Hash the lowercase hex address (without 0x) with Keccak-256\n * - For each hex character in the address:\n * - If the corresponding nibble in the hash is >= 8, uppercase it\n * - Otherwise, lowercase it\n *\n * @param addressHex - The 40-character lowercase hex address (without 0x prefix).\n * @returns The checksummed address with 0x prefix.\n */\nfunction toChecksumAddress(addressHex: string): string {\n const lower = addressHex.toLowerCase();\n const hash = bytesToHex(keccak_256(new TextEncoder().encode(lower)));\n\n let checksummed = '0x';\n for (let i = 0; i < 40; i++) {\n const char = lower[i];\n const hashChar = hash[i];\n if (char === undefined || hashChar === undefined) {\n throw new IdentityError(\n `Unexpected undefined at index ${i} during checksum computation`\n );\n }\n const hashNibble = parseInt(hashChar, 16);\n checksummed += hashNibble >= 8 ? char.toUpperCase() : char;\n }\n\n return checksummed;\n}\n\n// ---------------------------------------------------------------------------\n// SLIP-0010 Ed25519 HD Key Derivation\n// ---------------------------------------------------------------------------\n\n/**\n * SLIP-0010 master key and child key derivation for Ed25519.\n *\n * Ed25519 SLIP-0010 only supports hardened derivation. The path\n * m/44'/501'/0'/0' is standard for Solana wallets.\n *\n * @param seed - The BIP-39 seed (64 bytes).\n * @param path - Array of hardened indices (must have 0x80000000 bit set).\n * @returns The derived 32-byte Ed25519 private key.\n */\nfunction slip0010Derive(seed: Uint8Array, path: number[]): Uint8Array {\n const encoder = new TextEncoder();\n\n // Master key: HMAC-SHA512(\"ed25519 seed\", seed)\n let I = hmac(sha512, encoder.encode('ed25519 seed'), seed);\n let key = I.slice(0, 32);\n let chainCode = I.slice(32);\n\n // Child derivation (hardened only)\n for (const index of path) {\n const data = new Uint8Array(37);\n data[0] = 0x00;\n data.set(key, 1);\n // Write index as big-endian uint32\n data[33] = (index >>> 24) & 0xff;\n data[34] = (index >>> 16) & 0xff;\n data[35] = (index >>> 8) & 0xff;\n data[36] = index & 0xff;\n\n I = hmac(sha512, chainCode, data);\n key = I.slice(0, 32);\n chainCode = I.slice(32);\n }\n\n return key;\n}\n\n/**\n * SLIP-0010 path for Solana, varying the hardened account component by\n * `accountIndex`: m/44'/501'/{accountIndex}'/0' (all hardened). At\n * `accountIndex` 0 this is the canonical m/44'/501'/0'/0', so existing\n * (index-0) keys are unchanged.\n */\nfunction solanaPath(accountIndex: number): number[] {\n return [\n 0x8000002c, // 44'\n 0x800001f5, // 501'\n (0x80000000 + accountIndex) >>> 0, // {accountIndex}'\n 0x80000000, // 0'\n ];\n}\n\n/**\n * Derives a Solana Ed25519 identity from a BIP-39 seed using SLIP-0010.\n *\n * @param accountIndex - Hardened account index (default 0). Distinct indices\n * yield distinct keypairs from the same seed.\n */\nfunction deriveSolanaIdentity(\n seed: Uint8Array,\n accountIndex = 0\n): SolanaIdentity {\n const privateKey = slip0010Derive(seed, solanaPath(accountIndex));\n const publicKeyBytes = ed25519.getPublicKey(privateKey);\n\n // Solana keypair = 32-byte private key + 32-byte public key = 64 bytes\n const keypair = new Uint8Array(64);\n keypair.set(privateKey, 0);\n keypair.set(publicKeyBytes, 32);\n\n return { secretKey: keypair, publicKey: base58Encode(publicKeyBytes) };\n}\n\n// ---------------------------------------------------------------------------\n// Mina Pallas Key Derivation (optional, requires mina-signer)\n// ---------------------------------------------------------------------------\n\n/**\n * Derives a Mina Pallas identity from a BIP-39 seed.\n *\n * Uses BIP-32 secp256k1 derivation at path m/44'/12586'/{accountIndex}'/0/0 to\n * produce a 32-byte scalar, then converts to Mina key format via mina-signer.\n * At `accountIndex` 0 this is the canonical m/44'/12586'/0'/0/0, so existing\n * (index-0) keys are unchanged.\n *\n * @param seed - The BIP-39 seed (64 bytes).\n * @param accountIndex - Hardened account index (default 0). Distinct indices\n * yield distinct keys from the same seed.\n * @returns The Mina identity, or undefined if mina-signer is not installed.\n */\nasync function deriveMinaIdentity(\n seed: Uint8Array,\n accountIndex = 0\n): Promise<MinaIdentity | undefined> {\n const path = `m/44'/12586'/${accountIndex}'/0/0`;\n const hdKey = HDKey.fromMasterSeed(seed).derive(path);\n\n if (!hdKey.privateKey) {\n throw new IdentityError(\n `Failed to derive Mina private key at path ${path}`\n );\n }\n\n const keyBytes = new Uint8Array(hdKey.privateKey);\n // Clamp the top 2 bits so the big-endian scalar is within the Pallas\n // base-field order. A raw BIP-32 child scalar can exceed it (~75% of 256-bit\n // values do), and mina-signer then rejects it with\n // \"Scalar: inputs larger than … are not allowed\". Matches the client's\n // deriveMinaKey and mill's deriveMillKeys, so all three produce the SAME Mina\n // key for a given mnemonic + accountIndex.\n keyBytes[0] = (keyBytes[0] ?? 0) & 0x3f;\n const hexKey = bytesToHex(keyBytes);\n\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const MinaSignerLib: any = await import('mina-signer');\n const Client =\n 'default' in MinaSignerLib ? MinaSignerLib.default : MinaSignerLib;\n const client = new Client({ network: 'mainnet' });\n\n // mina-signer's derivePublicKey expects a Mina base58check (\"EK…\") private\n // key, NOT a raw hex scalar. Convert first — passing hex makes mina-signer\n // throw `fromBase58: invalid character`, which the old broad catch silently\n // swallowed (so Mina derivation always returned undefined). Keep the\n // exposed privateKey as hex for consumer compatibility (e.g. the client's\n // MinaSigner accepts hex and converts internally).\n const minaBase58PrivateKey = hexToMinaBase58PrivateKey(hexKey);\n const publicKey: string = client.derivePublicKey(minaBase58PrivateKey);\n return { privateKey: hexKey, publicKey };\n } catch (err: unknown) {\n // Degrade gracefully ONLY when mina-signer is genuinely absent (optional\n // dependency). Surface any other failure instead of masking it — the broad\n // catch previously hid the hex-vs-base58 key-format bug above.\n const code = (err as { code?: string } | undefined)?.code;\n if (code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND') {\n return undefined;\n }\n throw new IdentityError(\n `Mina identity derivation failed at path ${path}: ${\n err instanceof Error ? err.message : String(err)\n }`,\n err instanceof Error ? err : undefined\n );\n }\n}\n\n// ---------------------------------------------------------------------------\n// Public Multi-Chain Functions\n// ---------------------------------------------------------------------------\n\n/**\n * Derives a full multi-chain ToonIdentity from a BIP-39 mnemonic,\n * including async Mina derivation (requires mina-signer).\n *\n * Chains derived:\n * - Nostr (secp256k1): m/44'/1237'/0'/0/{accountIndex}\n * - EVM (secp256k1): same key as Nostr, Keccak-256 for address\n * - Solana (Ed25519): m/44'/501'/{accountIndex}'/0' (SLIP-0010)\n * - Mina (Pallas): m/44'/12586'/{accountIndex}'/0/0 (optional, requires mina-signer)\n *\n * All four chains vary by `accountIndex`, so distinct indices yield fully\n * distinct multi-chain identities from one seed. Index 0 is unchanged from the\n * historical fixed paths.\n *\n * @param mnemonic - A valid BIP-39 mnemonic (12 or 24 words).\n * @param options - Optional derivation options (accountIndex defaults to 0).\n * @returns The derived ToonIdentity with all chain identities populated.\n * @throws {IdentityError} If the mnemonic is invalid.\n */\nexport async function fromMnemonicFull(\n mnemonic: string,\n options?: FromMnemonicOptions\n): Promise<ToonIdentity> {\n // Derive Nostr + EVM + Solana synchronously\n const identity = fromMnemonic(mnemonic, options);\n const accountIndex = options?.accountIndex ?? 0;\n\n // Attempt async Mina derivation (varies by accountIndex, like the others)\n let seed: Uint8Array | undefined;\n try {\n seed = mnemonicToSeedSync(mnemonic);\n const mina = await deriveMinaIdentity(seed, accountIndex);\n if (mina) {\n return { ...identity, mina };\n }\n } finally {\n if (seed) {\n seed.fill(0);\n }\n }\n\n return identity;\n}\n\n/**\n * Generates a random Solana Ed25519 keypair (non-deterministic).\n *\n * For deterministic derivation from a mnemonic, use fromMnemonic() instead.\n *\n * @returns A SolanaIdentity with random keypair.\n */\nexport function generateSolanaKeypair(): SolanaIdentity {\n const privateKey = ed25519.utils.randomSecretKey();\n const publicKeyBytes = ed25519.getPublicKey(privateKey);\n\n const keypair = new Uint8Array(64);\n keypair.set(privateKey, 0);\n keypair.set(publicKeyBytes, 32);\n\n return { secretKey: keypair, publicKey: base58Encode(publicKeyBytes) };\n}\n\n// ---------------------------------------------------------------------------\n// Base58 Encoding/Decoding\n// ---------------------------------------------------------------------------\n\nconst BASE58_ALPHABET =\n '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';\n\n/**\n * Encodes a byte array to a Base58 string (Bitcoin/Solana alphabet).\n */\nexport function base58Encode(bytes: Uint8Array): string {\n // Count leading zeros\n let zeros = 0;\n for (let i = 0; i < bytes.length && bytes[i] === 0; i++) zeros++;\n\n let value = 0n;\n for (const byte of bytes) {\n value = value * 256n + BigInt(byte);\n }\n\n let result = '';\n while (value > 0n) {\n result = BASE58_ALPHABET[Number(value % 58n)] + result;\n value = value / 58n;\n }\n\n // Add leading '1's for leading zero bytes\n for (let i = 0; i < zeros; i++) {\n result = '1' + result;\n }\n\n return result || '1';\n}\n\n/**\n * Decodes a Base58 string to a byte array (Bitcoin/Solana alphabet).\n */\nexport function base58Decode(str: string): Uint8Array {\n // Count leading '1's (zero bytes)\n let zeros = 0;\n for (let i = 0; i < str.length && str[i] === '1'; i++) zeros++;\n\n let value = 0n;\n for (const ch of str) {\n const idx = BASE58_ALPHABET.indexOf(ch);\n if (idx === -1) throw new IdentityError(`Invalid base58 character: ${ch}`);\n value = value * 58n + BigInt(idx);\n }\n\n // Convert bigint to bytes\n const hex = value === 0n ? '' : value.toString(16);\n const hexPadded = hex.length % 2 ? '0' + hex : hex;\n const rawBytes: number[] = [];\n for (let i = 0; i < hexPadded.length; i += 2) {\n rawBytes.push(parseInt(hexPadded.slice(i, i + 2), 16));\n }\n\n const result = new Uint8Array(zeros + rawBytes.length);\n result.set(rawBytes, zeros);\n return result;\n}\n","/**\n * NIP-59 Gift Wrap Integration for ILP Packets (Story 12.2)\n *\n * Provides privacy-preserving encoding/decoding of swap packets using the\n * NIP-59 three-layer gift wrap construction (rumor -> seal -> gift wrap).\n * Intermediary peers routing ILP packets see only opaque TOON-encoded binary\n * in the data field -- they cannot determine the event kind, sender identity,\n * or swap intent. Only the destination Mill can unwrap and process.\n *\n * Also provides NIP-44 encryption/decryption for FULFILL claim return path\n * (D12-008), using ephemeral keys so intermediaries on the return path see\n * only opaque ciphertext.\n *\n * @module\n */\n\nimport { generateSecretKey, getPublicKey } from 'nostr-tools/pure';\nimport type { UnsignedEvent, NostrEvent } from 'nostr-tools/pure';\nimport { createRumor, createSeal, createWrap } from 'nostr-tools/nip59';\nimport {\n encrypt as nip44Encrypt,\n decrypt as nip44Decrypt,\n getConversationKey,\n} from 'nostr-tools/nip44';\nimport {\n encodeEventToToon,\n decodeEventFromToon,\n} from '@toon-protocol/core/toon';\nimport { buildIlpPrepare } from '@toon-protocol/core';\nimport type { IlpPreparePacket } from '@toon-protocol/core';\n\nimport { GiftWrapError } from './errors.js';\n\n// ---------------------------------------------------------------------------\n// Type definitions\n// ---------------------------------------------------------------------------\n\n/** Parameters for {@link wrapSwapPacket}. */\nexport interface WrapSwapPacketParams {\n /** Unsigned inner event containing swap metadata. */\n rumor: UnsignedEvent;\n /** Sender's secp256k1 secret key. */\n senderSecretKey: Uint8Array;\n /** Mill's compressed hex pubkey (64 chars). */\n recipientPubkey: string;\n}\n\n/** Result of {@link wrapSwapPacket}. */\nexport interface WrapSwapPacketResult {\n /** Fully-formed kind:1059 gift wrap event signed by a fresh ephemeral key. */\n giftWrap: NostrEvent;\n /** The ephemeral pubkey used for the outer gift wrap layer. */\n ephemeralPubkey: string;\n}\n\n/** Parameters for {@link unwrapSwapPacket}. */\nexport interface UnwrapSwapPacketParams {\n /** A kind:1059 gift wrap event. */\n giftWrap: NostrEvent;\n /** Recipient's (Mill's) secret key. */\n recipientSecretKey: Uint8Array;\n}\n\n/** Result of {@link unwrapSwapPacket}. */\nexport interface UnwrapSwapPacketResult {\n /** The decrypted inner rumor (unsigned). */\n rumor: UnsignedEvent;\n /** The sender's real pubkey (extracted from the seal layer). */\n senderPubkey: string;\n}\n\n/** Parameters for {@link wrapSwapPacketToToon}. */\nexport interface WrapSwapPacketToToonParams {\n /** Unsigned inner event containing swap metadata. */\n rumor: UnsignedEvent;\n /** Sender's secp256k1 secret key. */\n senderSecretKey: Uint8Array;\n /** Mill's compressed hex pubkey (64 chars). */\n recipientPubkey: string;\n /** ILP destination address. */\n destination: string;\n /** Payment amount in ILP units (bigint). */\n amount: bigint;\n /** Packet expiry. Default: 30s from now (inside buildIlpPrepare). */\n expiresAt?: Date;\n}\n\n/** Result of {@link wrapSwapPacketToToon}. */\nexport interface WrapSwapPacketToToonResult {\n /** Ready-to-send ILP PREPARE packet with TOON-encoded gift wrap as data. */\n ilpPrepare: IlpPreparePacket;\n /** The ephemeral pubkey used for the outer gift wrap layer. */\n ephemeralPubkey: string;\n}\n\n/** Parameters for {@link unwrapSwapPacketFromToon}. */\nexport interface UnwrapSwapPacketFromToonParams {\n /** The data field from an incoming ILP PREPARE (TOON-encoded gift wrap). */\n toonData: Uint8Array;\n /** Recipient's (Mill's) secret key. */\n recipientSecretKey: Uint8Array;\n}\n\n/** Parameters for {@link encryptFulfillClaim}. */\nexport interface EncryptFulfillClaimParams {\n /** The signed claim bytes to encrypt. */\n claimData: Uint8Array;\n /** The original sender's pubkey (recovered from unwrap). */\n senderPubkey: string;\n}\n\n/** Result of {@link encryptFulfillClaim}. */\nexport interface EncryptFulfillClaimResult {\n /** NIP-44 encrypted claim bytes. */\n ciphertext: Uint8Array;\n /** The Mill's ephemeral pubkey (included in FULFILL so sender can decrypt). */\n ephemeralPubkey: string;\n}\n\n/** Parameters for {@link decryptFulfillClaim}. */\nexport interface DecryptFulfillClaimParams {\n /** The encrypted claim bytes from the FULFILL data field. */\n ciphertext: Uint8Array;\n /** The Mill's ephemeral pubkey from the FULFILL data field. */\n ephemeralPubkey: string;\n /** The sender's (recipient of FULFILL) secret key. */\n recipientSecretKey: Uint8Array;\n}\n\n// ---------------------------------------------------------------------------\n// Input validation helpers\n// ---------------------------------------------------------------------------\n\n/** Validate a 32-byte secret key. */\nfunction validateSecretKey(key: Uint8Array, paramName: string): void {\n if (!(key instanceof Uint8Array) || key.length !== 32) {\n throw new GiftWrapError(\n `${paramName} must be a 32-byte Uint8Array, got ${key instanceof Uint8Array ? `${key.length} bytes` : typeof key}`\n );\n }\n}\n\n/** Validate a 64-char lowercase hex pubkey. */\nfunction validatePubkey(pubkey: string, paramName: string): void {\n if (typeof pubkey !== 'string' || !/^[0-9a-f]{64}$/.test(pubkey)) {\n throw new GiftWrapError(\n `${paramName} must be a 64-character lowercase hex string`\n );\n }\n}\n\n// ---------------------------------------------------------------------------\n// Core wrap/unwrap functions (AC-1, AC-2)\n// ---------------------------------------------------------------------------\n\n/**\n * NIP-59 three-layer gift wrap a swap packet.\n *\n * Constructs a kind:1059 gift wrap event using a fresh ephemeral keypair.\n * The rumor is sealed with the sender's real key, then wrapped with the\n * ephemeral key. Intermediaries see only the ephemeral pubkey.\n *\n * Each invocation generates a fresh ephemeral keypair for forward secrecy\n * and message unlinkability (risk R-006).\n *\n * @throws {GiftWrapError} If the wrap operation fails (invalid keys, crypto failure).\n */\nexport function wrapSwapPacket(\n params: WrapSwapPacketParams\n): WrapSwapPacketResult {\n const { rumor, senderSecretKey, recipientPubkey } = params;\n\n validateSecretKey(senderSecretKey, 'senderSecretKey');\n validatePubkey(recipientPubkey, 'recipientPubkey');\n\n try {\n // Use nostr-tools nip59 building blocks so we can capture the ephemeral pubkey.\n // Step 1: Create rumor (adds id + pubkey derived from senderSecretKey)\n const rumorEvent = createRumor(rumor, senderSecretKey);\n\n // Step 2: Create seal (kind:13, NIP-44 encrypted rumor, randomized timestamp)\n const seal = createSeal(rumorEvent, senderSecretKey, recipientPubkey);\n\n // Step 3: Create gift wrap (kind:1059, fresh ephemeral key, NIP-44 encrypted seal)\n const giftWrap = createWrap(seal, recipientPubkey);\n\n // The ephemeral pubkey is the pubkey on the gift wrap event\n const ephemeralPubkey = giftWrap.pubkey;\n\n return { giftWrap, ephemeralPubkey };\n } catch (error) {\n throw new GiftWrapError(\n `Failed to wrap swap packet: ${error instanceof Error ? error.message : String(error)}`,\n error instanceof Error ? error : undefined\n );\n }\n}\n\n/**\n * Unwrap a NIP-59 gift-wrapped swap packet, recovering the inner rumor\n * and the sender's real pubkey.\n *\n * Performs two-layer decryption: gift wrap -> seal -> rumor.\n * The sender pubkey is extracted from the seal's pubkey field.\n *\n * @throws {GiftWrapError} If kind is not 1059, decryption fails, or structure is malformed.\n */\nexport function unwrapSwapPacket(\n params: UnwrapSwapPacketParams\n): UnwrapSwapPacketResult {\n const { giftWrap, recipientSecretKey } = params;\n\n validateSecretKey(recipientSecretKey, 'recipientSecretKey');\n\n // Guard against null/undefined/non-object giftWrap before property access\n if (!giftWrap || typeof giftWrap !== 'object') {\n throw new GiftWrapError('giftWrap must be a non-null object');\n }\n\n // Validate kind before attempting decryption\n if (giftWrap.kind !== 1059) {\n throw new GiftWrapError('Expected kind:1059 gift wrap');\n }\n\n let conversationKey1: Uint8Array | null = null;\n let conversationKey2: Uint8Array | null = null;\n\n try {\n // Layer 1: Decrypt the gift wrap to recover the seal.\n // The gift wrap was encrypted with ECDH(ephemeralPrivkey, recipientPubkey),\n // so we decrypt with ECDH(recipientSecretKey, giftWrap.pubkey).\n conversationKey1 = getConversationKey(recipientSecretKey, giftWrap.pubkey);\n const sealJson = nip44Decrypt(giftWrap.content, conversationKey1);\n const seal = JSON.parse(sealJson) as NostrEvent;\n\n // Validate seal kind (must be kind:13 per NIP-59)\n if (seal.kind !== 13) {\n throw new GiftWrapError(\n `Expected kind:13 seal inside gift wrap, got kind:${seal.kind}`\n );\n }\n\n // Extract and validate the sender's real pubkey from the seal\n const senderPubkey = seal.pubkey;\n validatePubkey(senderPubkey, 'seal.pubkey (sender identity)');\n\n // Layer 2: Decrypt the seal to recover the rumor.\n // The seal was encrypted with ECDH(senderSecretKey, recipientPubkey),\n // so we decrypt with ECDH(recipientSecretKey, senderPubkey).\n conversationKey2 = getConversationKey(recipientSecretKey, senderPubkey);\n const rumorJson = nip44Decrypt(seal.content, conversationKey2);\n const rumor = JSON.parse(rumorJson) as UnsignedEvent & {\n id?: string;\n sig?: string;\n };\n\n // Strip sig if present (rumor should be unsigned)\n delete rumor.sig;\n\n return { rumor, senderPubkey };\n } catch (error) {\n if (error instanceof GiftWrapError) {\n throw error;\n }\n throw new GiftWrapError(\n `Failed to unwrap swap packet: ${error instanceof Error ? error.message : String(error)}`,\n error instanceof Error ? error : undefined\n );\n } finally {\n // Zero ECDH-derived conversation keys (defense-in-depth)\n if (conversationKey1) {\n conversationKey1.fill(0);\n conversationKey1 = null;\n }\n if (conversationKey2) {\n conversationKey2.fill(0);\n conversationKey2 = null;\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Convenience wrappers for TOON + ILP integration (AC-3, AC-4)\n// ---------------------------------------------------------------------------\n\n/**\n * Convenience: gift wrap a swap packet, encode to TOON binary, and build\n * an ILP PREPARE packet.\n *\n * This is the path that `streamSwap()` (Story 12.5) will use in its\n * per-packet loop.\n */\nexport function wrapSwapPacketToToon(\n params: WrapSwapPacketToToonParams\n): WrapSwapPacketToToonResult {\n const {\n rumor,\n senderSecretKey,\n recipientPubkey,\n destination,\n amount,\n expiresAt,\n } = params;\n\n // Step 1: Gift wrap\n const { giftWrap, ephemeralPubkey } = wrapSwapPacket({\n rumor,\n senderSecretKey,\n recipientPubkey,\n });\n\n // Step 2: Encode to TOON binary\n const toonBinary = encodeEventToToon(giftWrap);\n\n // Step 3: Build ILP PREPARE packet\n const ilpPrepare = buildIlpPrepare({\n destination,\n amount,\n data: toonBinary,\n expiresAt,\n });\n\n return { ilpPrepare, ephemeralPubkey };\n}\n\n/**\n * Convenience: decode TOON binary from an incoming ILP PREPARE data field\n * and unwrap the gift-wrapped swap packet.\n *\n * This is the path that the Mill handler (Story 12.3) will use to process\n * incoming swap packets.\n */\nexport function unwrapSwapPacketFromToon(\n params: UnwrapSwapPacketFromToonParams\n): UnwrapSwapPacketResult {\n const { toonData, recipientSecretKey } = params;\n\n if (!(toonData instanceof Uint8Array) || toonData.length === 0) {\n throw new GiftWrapError('toonData must be a non-empty Uint8Array');\n }\n\n try {\n // Step 1: Decode TOON binary to NostrEvent\n const giftWrap = decodeEventFromToon(toonData);\n\n // Step 2: Unwrap\n return unwrapSwapPacket({ giftWrap, recipientSecretKey });\n } catch (error) {\n if (error instanceof GiftWrapError) {\n throw error;\n }\n throw new GiftWrapError(\n `Failed to unwrap swap packet from TOON: ${error instanceof Error ? error.message : String(error)}`,\n error instanceof Error ? error : undefined\n );\n }\n}\n\n// ---------------------------------------------------------------------------\n// FULFILL encryption/decryption (AC-5, AC-6)\n// ---------------------------------------------------------------------------\n\n/**\n * Encrypt a FULFILL claim for the return path (D12-008).\n *\n * Generates a fresh ephemeral keypair and NIP-44 encrypts the claim data.\n * The ephemeral pubkey is included alongside the ciphertext so the sender\n * can decrypt. The ephemeral privkey is discarded after encryption.\n *\n * @throws {GiftWrapError} If claimData is empty or encryption fails.\n */\nexport function encryptFulfillClaim(\n params: EncryptFulfillClaimParams\n): EncryptFulfillClaimResult {\n const { claimData, senderPubkey } = params;\n\n validatePubkey(senderPubkey, 'senderPubkey');\n\n if (!(claimData instanceof Uint8Array)) {\n throw new GiftWrapError(\n `claimData must be a Uint8Array, got ${typeof claimData}`\n );\n }\n\n if (claimData.length === 0) {\n throw new GiftWrapError('claimData must not be empty');\n }\n\n // Generate fresh ephemeral keypair (let-scoped for GC after return)\n let ephemeralSecretKey: Uint8Array | null = generateSecretKey();\n const ephemeralPubkey = getPublicKey(ephemeralSecretKey);\n let conversationKey: Uint8Array | null = null;\n\n try {\n // NIP-44 encrypt: ECDH(ephemeralPrivkey, senderPubkey)\n conversationKey = getConversationKey(ephemeralSecretKey, senderPubkey);\n\n // NIP-44 encrypts strings, so base64-encode the claim bytes\n const claimString = Buffer.from(claimData).toString('base64');\n const ciphertextString = nip44Encrypt(claimString, conversationKey);\n\n // Convert ciphertext string to Uint8Array for transport\n const ciphertext = new TextEncoder().encode(ciphertextString);\n\n return { ciphertext, ephemeralPubkey };\n } catch (error) {\n if (error instanceof GiftWrapError) {\n throw error;\n }\n throw new GiftWrapError(\n `Failed to encrypt FULFILL claim: ${error instanceof Error ? error.message : String(error)}`,\n error instanceof Error ? error : undefined\n );\n } finally {\n // Zero sensitive key material before releasing for GC (defense-in-depth)\n if (ephemeralSecretKey) {\n ephemeralSecretKey.fill(0);\n ephemeralSecretKey = null;\n }\n if (conversationKey) {\n conversationKey.fill(0);\n conversationKey = null;\n }\n }\n}\n\n/**\n * Decrypt a FULFILL claim from the return path.\n *\n * Uses the sender's secret key and the Mill's ephemeral pubkey (from the\n * FULFILL data field) to NIP-44 decrypt the claim bytes.\n *\n * @throws {GiftWrapError} If decryption fails (wrong key, malformed ciphertext).\n */\nexport function decryptFulfillClaim(\n params: DecryptFulfillClaimParams\n): Uint8Array {\n const { ciphertext, ephemeralPubkey, recipientSecretKey } = params;\n\n validateSecretKey(recipientSecretKey, 'recipientSecretKey');\n validatePubkey(ephemeralPubkey, 'ephemeralPubkey');\n\n if (!(ciphertext instanceof Uint8Array) || ciphertext.length === 0) {\n throw new GiftWrapError('ciphertext must be a non-empty Uint8Array');\n }\n\n let conversationKey: Uint8Array | null = null;\n\n try {\n // NIP-44 decrypt: ECDH(recipientSecretKey, ephemeralPubkey)\n conversationKey = getConversationKey(recipientSecretKey, ephemeralPubkey);\n\n // Convert ciphertext Uint8Array back to string\n const ciphertextString = new TextDecoder().decode(ciphertext);\n\n // NIP-44 decrypt returns a string (base64-encoded claim bytes)\n const claimString = nip44Decrypt(ciphertextString, conversationKey);\n\n // Decode base64 back to Uint8Array\n return new Uint8Array(Buffer.from(claimString, 'base64'));\n } catch (error) {\n if (error instanceof GiftWrapError) {\n throw error;\n }\n throw new GiftWrapError(\n `Failed to decrypt FULFILL claim: ${error instanceof Error ? error.message : String(error)}`,\n error instanceof Error ? error : undefined\n );\n } finally {\n // Zero ECDH-derived conversation key (defense-in-depth)\n if (conversationKey) {\n conversationKey.fill(0);\n conversationKey = null;\n }\n }\n}\n","/**\n * Mill Swap Handler (Story 12.3)\n *\n * `createSwapHandler()` factory produces a kind:1059 `Handler` that:\n * 1. Unwraps an incoming NIP-59 gift-wrapped ILP swap packet (via Story 12.2).\n * 2. Identifies the requested `SwapPair` from inner-rumor `swap-from` / `swap-to` tags.\n * 3. Applies a per-packet exchange rate (pair.rate or live rateProvider hook).\n * 4. Delegates signed claim issuance to a pluggable `ClaimIssuer` (Story 12.4).\n * 5. NIP-44 encrypts the claim with an ephemeral key (Story 12.2) for return.\n *\n * The handler is a pure application-layer composition — no connector, routing,\n * or wallet code lives here. See `_bmad-output/epics/epic-12-token-swap-primitive.md`\n * for D12-001/D12-008/D12-009/D12-010 and the scope fence.\n *\n * Transport encoding: the `accept()` metadata emits `claim` as a base64-encoded\n * NIP-44 ciphertext, `ephemeralPubkey` as 64-char lowercase hex, and optional\n * `claimId`. The sender-side `streamSwap()` (Story 12.5) base64-decodes `claim`\n * before calling `decryptFulfillClaim`.\n *\n * @module\n */\n\nimport { createHash } from 'node:crypto';\nimport type { UnsignedEvent } from 'nostr-tools/pure';\nimport type { SwapPair } from '@toon-protocol/core';\n\nimport { GiftWrapError, SwapHandlerError } from './errors.js';\nimport { unwrapSwapPacketFromToon, encryptFulfillClaim } from './gift-wrap.js';\nimport type { Handler } from './handler-registry.js';\nimport { base58Decode } from './identity.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Parameters passed to a {@link ClaimIssuer.issueClaim} call. */\nexport interface IssueClaimParams {\n /** Source-asset amount received by the Mill (ILP packet amount, source micro-units). */\n sourceAmount: bigint;\n /** Target-asset amount owed to the sender (post-rate-conversion, target micro-units). */\n targetAmount: bigint;\n /** The `SwapPair` this packet is being priced against. */\n pair: SwapPair;\n /**\n * The sender's real Nostr pubkey (extracted from the unwrapped seal).\n *\n * Identity-layer key only: used by the Mill for inventory ledger keying and\n * the sender→channel sticky binding (`channelState.reserve()` /\n * `channelState.release()`). The Mill MUST NOT pass this to chain-layer\n * signers as the balance-proof `recipient` — use {@link chainRecipient}\n * for that (Story 12.9 D12-011).\n */\n senderPubkey: string;\n /**\n * The sender's chain-specific payout address for `pair.to.chain`\n * (Story 12.9 AC-10). Extracted and format-validated from the rumor's\n * `chain-recipient` tag by the swap handler. REQUIRED. This is the\n * address the Mill's `PaymentChannelSigner` MUST use as the balance-proof\n * `recipient` (e.g., 20-byte EVM address, 32-byte Solana Ed25519 pubkey).\n */\n chainRecipient: string;\n /** The inner rumor (for optional Mill-side context; may be ignored by the issuer). */\n rumor: UnsignedEvent;\n}\n\n/**\n * Result returned from {@link ClaimIssuer.issueClaim}.\n *\n * Story 12.6 extension: additive settlement-context fields let the sender\n * reconstruct the balance-proof message hash for signature verification and\n * on-chain settlement (see `buildSettlementTx()`).\n */\nexport interface IssueClaimResult {\n /** Signed claim bytes ready for NIP-44 encryption (chain-specific format). */\n claim: Uint8Array;\n /** Optional Mill-side claim ID for logging/tracing. */\n claimId?: string;\n // --- Story 12.6 settlement-context fields (additive, all optional for\n // one-story-cycle backward compat; the Mill SHOULD emit all of them) ---\n /** Channel identifier on the target chain. */\n channelId?: string;\n /** Balance-proof nonce (monotonically increasing per channel). */\n nonce?: bigint;\n /** Cumulative transferred amount on the channel (target micro-units). */\n cumulativeAmount?: bigint;\n /** Recipient address (the sender's target-asset address). */\n recipient?: string;\n /** Mill's on-chain signer address. */\n millSignerAddress?: string;\n}\n\n/**\n * Pluggable signed-claim issuer. Story 12.3 defines only the contract — the\n * concrete multi-chain implementation ships in Story 12.4.\n *\n * The issuer owns inventory accounting and signing-key material. The handler\n * relies on `issueClaim()` being atomic with inventory debit: if the call\n * resolves, the target-asset amount MUST be considered committed from the\n * Mill's reserves. If the call throws, no inventory change SHOULD have occurred.\n */\nexport interface ClaimIssuer {\n /**\n * Produce a signed off-chain payment-channel claim in the target asset.\n *\n * @throws Error (or subclass) on insufficient reserves, unsupported pair,\n * or signing failure. Errors with `code === 'INSUFFICIENT_INVENTORY'` or\n * messages matching `/insufficient/i` are surfaced as ILP T04; all other\n * errors as T00.\n */\n issueClaim(params: IssueClaimParams): Promise<IssueClaimResult>;\n}\n\n/** Parameters for {@link applyRate}. */\nexport interface ApplyRateParams {\n /** Source amount in source micro-units. */\n sourceAmount: bigint;\n /** `SwapPair.from.assetScale` (number of decimals on source side). */\n fromScale: number;\n /** `SwapPair.to.assetScale` (number of decimals on target side). */\n toScale: number;\n /** Decimal-string rate (target whole-units per source whole-unit). */\n rate: string;\n}\n\n/** Minimal pino-compatible logger interface. */\nexport interface SwapHandlerLogger {\n debug: (...args: unknown[]) => void;\n info: (...args: unknown[]) => void;\n warn: (...args: unknown[]) => void;\n error: (...args: unknown[]) => void;\n}\n\n/**\n * Minimal `Set`-like contract the handler requires from\n * `seenPacketIds`. Both the native `Set<string>` and\n * {@link BoundedSeenPacketIds} satisfy this; operators can swap in a\n * persistent/remote-backed replacement too.\n */\nexport interface SeenPacketIdsLike {\n has(value: string): boolean;\n add(value: string): unknown;\n delete(value: string): boolean;\n}\n\n/** Configuration for {@link createSwapHandler}. */\nexport interface CreateSwapHandlerConfig {\n /** Mill's secp256k1 secret key for unwrapping gift-wrapped packets (32 bytes). */\n recipientSecretKey: Uint8Array;\n /** Swap pairs this Mill currently supports. */\n swapPairs: SwapPair[];\n /** Claim issuer delegate (Story 12.4 plugs in the multi-chain implementation). */\n claimIssuer: ClaimIssuer;\n /**\n * Optional live-rate override hook. When provided, the handler calls this per\n * packet instead of reading `pair.rate`. MUST return a decimal string matching\n * `SwapPair.rate` format: /^(0|[1-9]\\d*)(\\.\\d+)?$/.\n */\n rateProvider?: (pair: SwapPair) => string | Promise<string>;\n /**\n * Optional replay-protection set. When provided, the handler uses it\n * VERBATIM (operator-owned — bounding/persistence is the operator's\n * responsibility). When OMITTED (Story 12.8 AC-14), the handler\n * defaults to a {@link BoundedSeenPacketIds} with cap\n * {@link DEFAULT_SEEN_PACKET_IDS_CAP}. Any `Set`-shaped object with\n * `has`/`add`/`delete` (and a `size` getter for test introspection)\n * satisfies the contract.\n */\n seenPacketIds?: SeenPacketIdsLike;\n /** Optional pino-compatible logger. Defaults to a no-op logger. */\n logger?: SwapHandlerLogger;\n}\n\n// ---------------------------------------------------------------------------\n// Story 12.8 AC-14 — bounded seenPacketIds default\n// ---------------------------------------------------------------------------\n\n/**\n * Default ceiling for the `seenPacketIds` replay-protection set when the\n * operator does not supply a custom `Set`. Rationale: 10_000 packet-ids at\n * ~64 bytes each yields a ~640KB memory ceiling — high enough to absorb\n * legitimate bursts, low enough to bound DoS via distinct-id flooding.\n *\n * When the default bounded set is in use, eviction is LAST-ACCESS order\n * (LRU), NOT insertion order: a replay attacker who retries the same\n * packet-id forever would, under insertion-order LRU, have their entry\n * evicted after 10_000 new ids pass through — re-opening the replay\n * window. Access-order keeps frequently-replayed ids pinned, so the\n * window stays closed.\n *\n * Operators may override by supplying a custom `Set<string>` (e.g. a\n * persistent backing store). `createSwapHandler` uses the supplied set\n * verbatim without size-capping.\n */\nexport const DEFAULT_SEEN_PACKET_IDS_CAP = 10_000;\n\n/**\n * ILP REJECT codes emitted by `createSwapHandler()`.\n *\n * Exported as named constants so tests (Story 12.8 AC-3 forbids hardcoded\n * error-code strings) and downstream integrators can assert against the\n * same symbolic source the handler uses. Values follow RFC 27 / ILPv4\n * reject-code conventions (F01 = malformed, F02 = unreachable, F04 =\n * duplicate, F06 = unsupported pair, T00 = transient internal, T04 =\n * insufficient liquidity).\n */\nexport const SWAP_HANDLER_REJECT_CODES = {\n /** Malformed / invalid PREPARE content — gift-wrap shape or amount invalid. */\n INVALID_GIFT_WRAP: 'F01',\n /** No route — the handler did not match a registered destination. */\n UNREACHABLE: 'F02',\n /** Duplicate packet — `seenPacketIds` replay hit. */\n DUPLICATE_PACKET: 'F04',\n /** Requested swap pair is not advertised by this Mill. */\n UNSUPPORTED_PAIR: 'F06',\n /** Transient internal failure — signing, rate provider, or unexpected. */\n INTERNAL: 'T00',\n /** Insufficient Mill inventory for the requested amount. */\n INSUFFICIENT_LIQUIDITY: 'T04',\n} as const;\n\n/**\n * Human-readable reject messages emitted by `createSwapHandler()`.\n *\n * Tests may assert against these verbatim rather than matching regexes\n * (Story 12.8 AC-3 guidance).\n */\nexport const SWAP_HANDLER_REJECT_MESSAGES = {\n INVALID_GIFT_WRAP: 'Invalid gift wrap',\n INVALID_AMOUNT: 'Invalid amount',\n UNREACHABLE: 'Unreachable',\n DUPLICATE_PACKET: 'Duplicate packet',\n UNSUPPORTED_PAIR: 'Unsupported swap pair',\n INTERNAL: 'Internal error',\n INSUFFICIENT_LIQUIDITY: 'Insufficient liquidity',\n RATE_PROVIDER: 'Rate provider error',\n RATE_CONVERSION: 'Rate conversion error',\n} as const;\n\n/**\n * LRU-ish `Set<string>`: re-adding an existing element promotes it to\n * \"most recently accessed\"; `has()` also promotes. Eviction occurs on\n * `add()` when size exceeds the cap — the least-recently-accessed entry\n * (Map insertion-order head) is removed.\n *\n * Exported as `@internal` for Story 12.8 AC-10 test introspection.\n *\n * @internal\n */\nexport class BoundedSeenPacketIds {\n readonly #map = new Map<string, true>();\n readonly #cap: number;\n readonly [Symbol.toStringTag] = 'Set';\n\n constructor(cap: number = DEFAULT_SEEN_PACKET_IDS_CAP) {\n if (!Number.isInteger(cap) || cap <= 0) {\n throw new Error(\n `BoundedSeenPacketIds cap must be a positive integer, got ${cap}`\n );\n }\n this.#cap = cap;\n }\n\n get size(): number {\n return this.#map.size;\n }\n\n /** Exposed for AC-10 test introspection. */\n get cap(): number {\n return this.#cap;\n }\n\n has(value: string): boolean {\n if (!this.#map.has(value)) return false;\n // Access-order promotion: re-insert so iteration order puts this at tail.\n this.#map.delete(value);\n this.#map.set(value, true);\n return true;\n }\n\n add(value: string): this {\n if (this.#map.has(value)) {\n // Promote to most-recently-accessed.\n this.#map.delete(value);\n this.#map.set(value, true);\n return this;\n }\n this.#map.set(value, true);\n while (this.#map.size > this.#cap) {\n // Evict least-recently-accessed (Map iteration is insertion order;\n // first key is the oldest that hasn't been re-added).\n const first = this.#map.keys().next();\n if (first.done) break;\n this.#map.delete(first.value);\n }\n return this;\n }\n\n delete(value: string): boolean {\n return this.#map.delete(value);\n }\n\n clear(): void {\n this.#map.clear();\n }\n\n forEach(\n callback: (value: string, value2: string, set: Set<string>) => void,\n thisArg?: unknown\n ): void {\n for (const k of this.#map.keys()) {\n callback.call(thisArg, k, k, this);\n }\n }\n\n *[Symbol.iterator](): IterableIterator<string> {\n yield* this.#map.keys();\n }\n\n *keys(): IterableIterator<string> {\n yield* this.#map.keys();\n }\n\n *values(): IterableIterator<string> {\n yield* this.#map.keys();\n }\n\n *entries(): IterableIterator<[string, string]> {\n for (const k of this.#map.keys()) yield [k, k];\n }\n}\n\n// ---------------------------------------------------------------------------\n// applyRate helper (AC-8)\n// ---------------------------------------------------------------------------\n\nconst RATE_REGEX = /^(0|[1-9]\\d*)(\\.\\d+)?$/;\n\n/**\n * Apply a decimal-string exchange rate to a source amount across asset scales.\n * Uses BigInt arithmetic throughout — never coerces to `Number` — to preserve\n * 18-decimal EVM precision (Epic 11 retro MAX_SAFE_INTEGER guard).\n *\n * Rounds toward zero (integer division), which economically favors the Mill\n * (standard market-maker convention).\n *\n * @throws {SwapHandlerError} If rate format is invalid, rate is zero, or\n * sourceAmount is not positive.\n */\nexport function applyRate(params: ApplyRateParams): bigint {\n const { sourceAmount, fromScale, toScale, rate } = params;\n\n if (!RATE_REGEX.test(rate)) {\n throw new SwapHandlerError(`Invalid rate format: ${rate}`);\n }\n // Reject any zero-valued rate regardless of decimal presentation.\n // RATE_REGEX matches `'0'`, `'0.0'`, `'0.00'`, etc. — all semantically\n // \"not quoting\". Previous check only caught the bare `'0'` form, letting\n // `'0.0'` slip through and produce a zero-valued targetAmount that the\n // sender's rate-deviation guard could not catch (expectedTargetAmount=0n\n // skips the deviation math). (Story 12.5 code-review pass #3.)\n if (/^0(\\.0+)?$/.test(rate)) {\n throw new SwapHandlerError('Rate is zero (pair not quoting)');\n }\n if (sourceAmount <= 0n) {\n throw new SwapHandlerError(\n `sourceAmount must be positive, got ${sourceAmount}`\n );\n }\n\n const dotIdx = rate.indexOf('.');\n const integerPart = dotIdx === -1 ? rate : rate.slice(0, dotIdx);\n const fractionalPart = dotIdx === -1 ? '' : rate.slice(dotIdx + 1);\n\n const rateNumerator = BigInt(integerPart + fractionalPart);\n const rateDenominator = 10n ** BigInt(fractionalPart.length);\n\n const scaleUp = 10n ** BigInt(toScale);\n const scaleDown = 10n ** BigInt(fromScale);\n\n return (\n (sourceAmount * rateNumerator * scaleUp) / (rateDenominator * scaleDown)\n );\n}\n\n// ---------------------------------------------------------------------------\n// findSwapPair helper (AC-7)\n// ---------------------------------------------------------------------------\n\n/**\n * Find the `SwapPair` identified by the rumor's `swap-from` / `swap-to` tags.\n *\n * Each tag value is parsed as `<assetCode>:<chain>`, split on the FIRST `:`\n * so multi-segment chain IDs like `evm:base:8453` remain intact as the chain\n * portion. Returns `null` for any malformed/missing tag — the handler\n * interprets `null` as \"unsupported pair\" and rejects via ILP F06.\n */\nexport function findSwapPair(\n rumor: UnsignedEvent,\n pairs: SwapPair[]\n): SwapPair | null {\n const fromTag = findTagValue(rumor, 'swap-from');\n const toTag = findTagValue(rumor, 'swap-to');\n\n if (!fromTag || !toTag) return null;\n\n const fromParts = splitAssetChain(fromTag);\n const toParts = splitAssetChain(toTag);\n if (!fromParts || !toParts) return null;\n\n for (const pair of pairs) {\n if (\n pair.from.assetCode === fromParts.assetCode &&\n pair.from.chain === fromParts.chain &&\n pair.to.assetCode === toParts.assetCode &&\n pair.to.chain === toParts.chain\n ) {\n return pair;\n }\n }\n return null;\n}\n\nfunction findTagValue(\n rumor: UnsignedEvent,\n tagName: string\n): string | undefined {\n if (!Array.isArray(rumor.tags)) return undefined;\n for (const t of rumor.tags) {\n if (Array.isArray(t) && t[0] === tagName && typeof t[1] === 'string') {\n return t[1];\n }\n }\n return undefined;\n}\n\nfunction splitAssetChain(\n raw: string\n): { assetCode: string; chain: string } | null {\n const idx = raw.indexOf(':');\n if (idx <= 0 || idx === raw.length - 1) return null;\n const assetCode = raw.slice(0, idx);\n const chain = raw.slice(idx + 1);\n if (!assetCode || !chain) return null;\n return { assetCode, chain };\n}\n\n// ---------------------------------------------------------------------------\n// Story 12.9 AC-2 / AC-8 — chain-recipient format validation\n// ---------------------------------------------------------------------------\n//\n// Duplicated (intentionally small) from `stream-swap.ts` to avoid a circular\n// module cycle (`stream-swap` imports `applyRate` from this file). Guardrail\n// 8.5 sanctions local duplication rather than introducing a shared helper\n// package. Rules MUST match the sender-side validator byte-for-byte.\n\nconst SWAP_HANDLER_EVM_ADDRESS_REGEX = /^0x[0-9a-f]{40}$/;\nconst SWAP_HANDLER_BASE58_REGEX = /^[1-9A-HJ-NP-Za-km-z]+$/;\n\n/**\n * Validate a chain-recipient address against `chain`. MUST remain in sync\n * with `validateChainAddress(value, chain, 'address')` in `stream-swap.ts`.\n */\nexport function validateChainRecipient(value: string, chain: string): boolean {\n if (typeof value !== 'string' || value.length === 0) return false;\n if (chain.startsWith('evm:')) {\n return SWAP_HANDLER_EVM_ADDRESS_REGEX.test(value);\n }\n if (chain.startsWith('solana:')) {\n if (!SWAP_HANDLER_BASE58_REGEX.test(value)) return false;\n if (value.length < 32 || value.length > 44) return false;\n try {\n return base58Decode(value).length === 32;\n } catch {\n return false;\n }\n }\n if (chain.startsWith('mina:')) {\n return SWAP_HANDLER_BASE58_REGEX.test(value) && value.length >= 32;\n }\n return value.length > 0;\n}\n\n/**\n * Extract the sender-supplied chain-recipient address from the rumor's\n * `chain-recipient` tag (Story 12.9 AC-8). Returns `null` if the tag is\n * missing or malformed for `chain`.\n */\nexport function findChainRecipient(\n rumor: UnsignedEvent,\n chain: string\n): string | null {\n const raw = findTagValue(rumor, 'chain-recipient');\n if (!raw) return null;\n if (!validateChainRecipient(raw, chain)) return null;\n return raw;\n}\n\n// ---------------------------------------------------------------------------\n// No-op logger\n// ---------------------------------------------------------------------------\n\nconst noop = (): void => undefined;\nconst NOOP_LOGGER: SwapHandlerLogger = {\n debug: noop,\n info: noop,\n warn: noop,\n error: noop,\n};\n\n// ---------------------------------------------------------------------------\n// createSwapHandler factory (AC-3..AC-12)\n// ---------------------------------------------------------------------------\n\n/**\n * Construct a kind:1059 Mill inbound-swap handler.\n *\n * The returned `Handler` is a pure closure over `config`; two calls with the\n * same config yield two independent-but-equivalent handlers. Register via\n * `node.handlers.on(1059, handler)` (Story 12.7).\n *\n * @throws {SwapHandlerError} At construction time if config is malformed.\n */\nexport function createSwapHandler(config: CreateSwapHandlerConfig): Handler {\n // Construction-time validation (pre-empt Story 12.2 retro finding #1).\n if (\n !(config.recipientSecretKey instanceof Uint8Array) ||\n config.recipientSecretKey.length !== 32\n ) {\n throw new SwapHandlerError(\n 'recipientSecretKey must be a 32-byte Uint8Array'\n );\n }\n if (!Array.isArray(config.swapPairs)) {\n throw new SwapHandlerError('swapPairs must be an array');\n }\n if (\n !config.claimIssuer ||\n typeof config.claimIssuer.issueClaim !== 'function'\n ) {\n throw new SwapHandlerError(\n 'claimIssuer must implement issueClaim(params): Promise<IssueClaimResult>'\n );\n }\n\n const logger = config.logger ?? NOOP_LOGGER;\n\n // Story 12.8 AC-14: when the operator does not supply a custom set,\n // default to a bounded access-order-LRU set. Operator-supplied sets are\n // used verbatim (we do not second-guess the operator's choice of\n // persistence / bounding).\n const seenPacketIds: SeenPacketIdsLike =\n config.seenPacketIds ?? new BoundedSeenPacketIds();\n\n return async (ctx) => {\n // AC-4: defensive kind guard. HandlerRegistry.dispatch already routes by\n // kind, but a mis-registered handler should fail loudly rather than\n // silently mutate unrelated traffic.\n if (ctx.kind !== 1059) {\n // Generic reject message -- do not leak handler role to the caller.\n // A swap handler registered for non-1059 traffic is a mis-configuration;\n // the caller doesn't need to know which handler fielded the packet.\n return ctx.reject('F02', 'Unreachable');\n }\n\n // Defense-in-depth: reject non-positive amounts eagerly with a dedicated\n // code so the sender gets an unambiguous error (otherwise applyRate would\n // throw and surface as the generic T00 \"Rate conversion error\"). ILP\n // connectors already enforce amount > 0, but we double-check at the\n // protocol boundary.\n if (typeof ctx.amount !== 'bigint' || ctx.amount <= 0n) {\n logger.warn({\n event: 'swap_handler.invalid_amount',\n destination: ctx.destination,\n });\n return ctx.reject('F01', 'Invalid amount');\n }\n\n // AC-4 / AC-5 / AC-6: decode and unwrap. `ctx.toon` is the base64 string\n // lifted verbatim from `ilpPrepare.data` (which `buildIlpPrepare` produces\n // by base64-encoding the raw TOON binary). Single decode -> TOON bytes.\n //\n // NOTE: `ctx.pubkey` is the OUTER ephemeral gift-wrap pubkey, NOT the real\n // sender. The real sender comes from the seal inside\n // unwrapSwapPacketFromToon. Do not use `ctx.pubkey` for sender identity.\n if (typeof ctx.toon !== 'string' || ctx.toon.length === 0) {\n logger.warn({\n event: 'swap_handler.invalid_toon',\n destination: ctx.destination,\n });\n return ctx.reject('F01', 'Invalid gift wrap');\n }\n let rumor: UnsignedEvent;\n let senderPubkey: string;\n try {\n const toonData = new Uint8Array(Buffer.from(ctx.toon, 'base64'));\n ({ rumor, senderPubkey } = unwrapSwapPacketFromToon({\n toonData,\n recipientSecretKey: config.recipientSecretKey,\n }));\n } catch (err) {\n if (err instanceof GiftWrapError) {\n logger.warn({\n event: 'swap_handler.unwrap_failed',\n destination: ctx.destination,\n error: err.message,\n });\n return ctx.reject('F01', 'Invalid gift wrap');\n }\n logger.error({\n event: 'swap_handler.unwrap_unexpected_error',\n destination: ctx.destination,\n error: err instanceof Error ? err.message : String(err),\n });\n return ctx.reject('F01', 'Invalid gift wrap');\n }\n\n // AC-7: pair lookup\n const pair = findSwapPair(rumor, config.swapPairs);\n if (!pair) {\n logger.debug({\n event: 'swap_handler.unsupported_pair',\n destination: ctx.destination,\n });\n return ctx.reject('F06', 'Unsupported swap pair');\n }\n\n // Story 12.9 AC-1 / AC-8: extract and validate the `chain-recipient`\n // tag from the inner rumor. Missing or malformed values are treated as\n // a malformed rumor (T00, consistent with other opaque-internal-error\n // paths) — the sender may retry with a corrected address.\n const chainRecipient = findChainRecipient(rumor, pair.to.chain);\n if (!chainRecipient) {\n logger.debug({\n event: 'swap_handler.malformed_rumor',\n destination: ctx.destination,\n reason: 'missing_or_malformed_chain_recipient',\n chain: pair.to.chain,\n });\n return ctx.reject('T00', 'Internal error');\n }\n\n // AC-11: replay protection check. We RESERVE the packetId synchronously\n // here (before the first `await`) so that two concurrent invocations with\n // an identical packet ID cannot both pass the `has()` gate. Because the\n // JS event loop is cooperative, the check-and-add pair is atomic relative\n // to other microtasks as long as it straddles no `await`. If issuance or\n // encryption later fails, we release the reservation so the sender can\n // legitimately retry (AC-11 requires retries of rejected packets).\n // Story 12.8 AC-14: replay check always runs (against the bounded default\n // when the operator did not supply a custom set).\n const packetId: string = computePacketId(senderPubkey, ctx.amount, rumor);\n if (seenPacketIds.has(packetId)) {\n logger.debug({\n event: 'swap_handler.duplicate_packet',\n packetId,\n });\n return ctx.reject('F04', 'Duplicate packet');\n }\n // Reserve eagerly to close the concurrent check-then-add race.\n seenPacketIds.add(packetId);\n\n // Helper: release the replay reservation on failure so the sender can retry.\n const releaseReservation = (): void => {\n seenPacketIds.delete(packetId);\n };\n\n // AC-9 rate resolution (optional live hook per D12-006).\n let rate: string;\n try {\n rate = config.rateProvider ? await config.rateProvider(pair) : pair.rate;\n } catch (err) {\n logger.error({\n event: 'swap_handler.rate_provider_failed',\n error: err instanceof Error ? err.message : String(err),\n });\n releaseReservation();\n return ctx.reject('T00', 'Rate provider error');\n }\n\n // AC-8: apply rate (BigInt throughout).\n let targetAmount: bigint;\n try {\n targetAmount = applyRate({\n sourceAmount: ctx.amount,\n fromScale: pair.from.assetScale,\n toScale: pair.to.assetScale,\n rate,\n });\n logger.debug({\n event: 'swap_handler.rate_applied',\n sourceAmount: ctx.amount.toString(),\n targetAmount: targetAmount.toString(),\n rate,\n });\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n logger.warn({\n event: 'swap_handler.rate_conversion_failed',\n error: msg,\n });\n // Do NOT surface the internal rate string / validation detail to the\n // sender -- return a generic privacy-preserving message.\n releaseReservation();\n return ctx.reject('T00', 'Rate conversion error');\n }\n\n // AC-9: delegate to claim issuer\n let claim: Uint8Array;\n let claimId: string | undefined;\n let settlementChannelId: string | undefined;\n let settlementNonce: bigint | undefined;\n let settlementCumulative: bigint | undefined;\n let settlementRecipient: string | undefined;\n let settlementMillSigner: string | undefined;\n try {\n const result = await config.claimIssuer.issueClaim({\n sourceAmount: ctx.amount,\n targetAmount,\n pair,\n senderPubkey,\n chainRecipient,\n rumor,\n });\n claim = result.claim;\n claimId = result.claimId;\n settlementChannelId = result.channelId;\n settlementNonce = result.nonce;\n settlementCumulative = result.cumulativeAmount;\n settlementRecipient = result.recipient;\n settlementMillSigner = result.millSignerAddress;\n } catch (err) {\n const code = (err as { code?: unknown })?.code;\n const message = err instanceof Error ? err.message : String(err);\n if (code === 'INSUFFICIENT_INVENTORY' || /insufficient/i.test(message)) {\n logger.warn({\n event: 'swap_handler.insufficient_inventory',\n error: message,\n });\n releaseReservation();\n return ctx.reject('T04', 'Insufficient liquidity');\n }\n logger.error({\n event: 'swap_handler.issuer_failed',\n error: message,\n });\n releaseReservation();\n return ctx.reject('T00', 'Internal error');\n }\n\n // AC-10: NIP-44 encrypt the claim (Story 12.2 handles ephemeral-key zeroing).\n let ciphertext: Uint8Array;\n let ephemeralPubkey: string;\n try {\n const enc = encryptFulfillClaim({ claimData: claim, senderPubkey });\n ciphertext = enc.ciphertext;\n ephemeralPubkey = enc.ephemeralPubkey;\n } catch (err) {\n logger.error({\n event: 'swap_handler.encrypt_failed',\n error: err instanceof Error ? err.message : String(err),\n });\n releaseReservation();\n return ctx.reject('T00', 'Internal error');\n }\n\n // AC-11: packetId was reserved pre-issuance to close the concurrent\n // check-then-add race; it remains committed on this success path.\n\n const claimBase64 = Buffer.from(ciphertext).toString('base64');\n logger.info({\n event: 'swap_handler.claim_issued',\n claimId,\n ephemeralPubkey,\n });\n\n // Story 12.5: emit the Mill-computed `targetAmount` (decimal string) so\n // the sender can run an end-to-end rate-deviation check without having to\n // parse the opaque chain-specific `claimBytes`. This is additive and\n // backward compatible — legacy senders that ignore the field get the\n // same {claim, ephemeralPubkey, claimId?} shape they always did.\n const metadata: Record<string, unknown> = {\n claim: claimBase64,\n ephemeralPubkey,\n targetAmount: targetAmount.toString(),\n };\n if (claimId !== undefined) metadata['claimId'] = claimId;\n // Story 12.6: thread settlement-context fields through when the claim\n // issuer emits them. All-or-nothing: a Mill that supplies settlement\n // fields MUST supply all five, since the sender's FULFILL decoder\n // enforces all-present-or-all-absent.\n if (\n settlementChannelId !== undefined &&\n settlementNonce !== undefined &&\n settlementCumulative !== undefined &&\n settlementRecipient !== undefined &&\n settlementMillSigner !== undefined\n ) {\n metadata['channelId'] = settlementChannelId;\n metadata['nonce'] = settlementNonce.toString();\n metadata['cumulativeAmount'] = settlementCumulative.toString();\n metadata['recipient'] = settlementRecipient;\n metadata['millSignerAddress'] = settlementMillSigner;\n }\n\n return ctx.accept(metadata);\n };\n}\n\n// ---------------------------------------------------------------------------\n// Replay packet ID hash\n// ---------------------------------------------------------------------------\n\n/**\n * Compute a deterministic packet ID for replay protection.\n *\n * Uses explicit length-prefix delimiters between the three inputs so that\n * `('ab','c12','3')` and `('abc','12','3')` produce distinct digests. Without\n * delimiters, variable-width string concatenation is ambiguous under hashing.\n */\nfunction computePacketId(\n senderPubkey: string,\n sourceAmount: bigint,\n rumor: UnsignedEvent\n): string {\n const rumorId = (rumor as UnsignedEvent & { id?: string }).id ?? '';\n const hash = createHash('sha256');\n const parts = [senderPubkey, sourceAmount.toString(), rumorId];\n for (const p of parts) {\n // 4-byte big-endian length prefix followed by UTF-8 bytes.\n const buf = Buffer.from(p, 'utf8');\n const lenBuf = Buffer.alloc(4);\n lenBuf.writeUInt32BE(buf.length, 0);\n hash.update(lenBuf);\n hash.update(buf);\n }\n return hash.digest('hex');\n}\n","/**\n * Sender-side `streamSwap()` API (Story 12.5).\n *\n * Drives the sender side of the Token Swap Primitive: chunks a total source\n * amount into N sender-chosen packets, wraps each with `wrapSwapPacketToToon()`\n * using a fresh ephemeral gift-wrap key per packet (D12-003 / risk R-006),\n * sends each via `ToonClient.sendSwapPacket(...)`, decrypts each FULFILL's\n * NIP-44-encrypted claim with `decryptFulfillClaim()`, accumulates the\n * decrypted claims into an ordered array, invokes a per-packet rate monitoring\n * callback, and supports pause / resume / stop / AbortSignal so the sender\n * can abort when rate drifts past tolerance.\n *\n * Composition story: consumes Stories 12.1 (SwapPair type), 12.2 (gift wrap\n * primitives), 12.3 (handler wire contract). Produces `AccumulatedClaim[]`\n * consumed by Story 12.6 (`buildSettlementTx()`).\n *\n * Design decisions:\n * - `streamSwap()` does NOT throw on mid-stream failure. Caller gets\n * `StreamSwapResult` with `state` and `abortReason`. Only construction-time\n * validation throws synchronously.\n * - `streamSwap()` does NOT retry individual packets. BTP-level connection\n * retries are handled inside `BtpRuntimeClient`; application-layer retry\n * (e.g., T04 insufficient inventory) is a future-story concern.\n * - Rate deviation math stays BigInt throughout. The `effectiveRate` on\n * `PacketProgress` is a display-only `number` (Epic 11 retro MAX_SAFE_INTEGER\n * guard).\n *\n * @module\n */\n\nimport { getPublicKey } from 'nostr-tools/pure';\nimport type { UnsignedEvent } from 'nostr-tools/pure';\nimport type { SwapPair } from '@toon-protocol/core';\n\nimport { StreamSwapError } from './errors.js';\nimport { wrapSwapPacketToToon, decryptFulfillClaim } from './gift-wrap.js';\nimport { base58Decode } from './identity.js';\nimport { applyRate } from './swap-handler.js';\n\n// ---------------------------------------------------------------------------\n// Minimal structural shapes (avoid cross-package type imports)\n// ---------------------------------------------------------------------------\n\n/** Minimal `IlpSendResult` shape — mirrors `packages/client/src/types.ts`. */\ninterface IlpSendResultLike {\n accepted: boolean;\n data?: string;\n code?: string;\n message?: string;\n}\n\n/**\n * Minimal `SignedBalanceProof` structural type.\n *\n * We avoid importing from `@toon-protocol/client` so the SDK package does not\n * gain a runtime dep on the client package. The sender layer only forwards\n * this opaquely to `ToonClient.sendSwapPacket`.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- opaque forwarded type\ntype SignedBalanceProofLike = any;\n\n/**\n * The subset of `ToonClient` that `streamSwap()` requires. This narrow shape\n * exists so that consumers can pass a real `ToonClient` OR a mock without\n * importing the whole class.\n */\nexport interface StreamSwapClient {\n sendSwapPacket(params: {\n destination: string;\n amount: bigint;\n toonData: Uint8Array;\n timeout?: number;\n claim?: SignedBalanceProofLike;\n }): Promise<IlpSendResultLike>;\n getPublicKey(): string;\n}\n\n// ---------------------------------------------------------------------------\n// Public interfaces (AC-2, AC-7, AC-8, AC-9, AC-10)\n// ---------------------------------------------------------------------------\n\n/**\n * Parameters for {@link streamSwap} / {@link streamSwapControlled}.\n *\n * @stable — downstream Stories 12.6 and 12.8 depend on this shape.\n */\nexport interface StreamSwapParams {\n /** SDK client with BTP wiring (see {@link StreamSwapClient}). */\n client: StreamSwapClient;\n /** Mill's 64-char lowercase hex pubkey (recipient of gift wrap). */\n millPubkey: string;\n /** Mill's ILP destination address (e.g., 'g.toon.mill1'). */\n millIlpAddress: string;\n /** The `SwapPair` being executed (from kind:10032 discovery, Story 12.1). */\n pair: SwapPair;\n /** Sender's 32-byte secp256k1 secret key. Used for seal signing AND FULFILL decryption. */\n senderSecretKey: Uint8Array;\n /**\n * Sender's chain-specific payout address for `pair.to.chain` (Story 12.9 AC-4).\n *\n * REQUIRED. The Nostr `senderPubkey` (identity layer) cannot be used as the\n * on-chain `recipient` in a balance-proof because they are cryptographically\n * independent keys (D12-011). For `evm:*` chains this must be a 20-byte\n * lowercased `0x`-prefixed hex address; for `solana:*` a base58 pubkey that\n * decodes to 32 bytes; for `mina:*` a base58 public-key string. The value\n * is validated at `streamSwapControlled()` entry via `validateChainAddress`\n * and echoed on every rumor as the `chain-recipient` tag (AC-6).\n */\n chainRecipient: string;\n /** Total source-asset amount to swap (source micro-units). */\n totalAmount: bigint;\n /** Even-split packet count. EXACTLY ONE of this or `packetAmounts` is required. */\n packetCount?: number;\n /** Explicit per-packet amounts. EXACTLY ONE of this or `packetCount` is required. */\n packetAmounts?: readonly bigint[];\n /** Source-asset balance proof claim. Required unless ChannelManager is wired. */\n claim?: SignedBalanceProofLike;\n /** Rate monitoring callback (fires after each accepted FULFILL). */\n onPacket?: RateMonitorCallback;\n /** Rate deviation threshold (decimal, e.g., 0.02 = 2%). */\n rateDeviationThreshold?: number;\n /** Per-packet timeout in ms. Default 30000. */\n packetTimeoutMs?: number;\n /** Optional AbortSignal. */\n signal?: AbortSignal;\n /**\n * Optional pino-compatible logger. Defaults to a no-op.\n *\n * Note: `streamSwap` calls each method with a single structured-event object\n * (e.g., `logger.warn({ event: 'stream_swap.packet_rejected', code, ... })`).\n * Pino accepts this form (the object is logged as top-level fields and the\n * message is left empty); other loggers that expect `(msg, meta)` should\n * wrap accordingly.\n */\n logger?: {\n debug: (...a: unknown[]) => void;\n info: (...a: unknown[]) => void;\n warn: (...a: unknown[]) => void;\n error: (...a: unknown[]) => void;\n };\n}\n\n/**\n * Rate monitoring callback. Fires after each successful FULFILL decryption,\n * before the next packet is sent. If the callback throws or rejects, the\n * stream is treated as stopped.\n */\nexport type RateMonitorCallback = (\n progress: PacketProgress\n) => void | Promise<void>;\n\n/**\n * Progress payload delivered to {@link RateMonitorCallback}. Frozen to\n * prevent callback mutation.\n */\nexport interface PacketProgress {\n /** 0-indexed packet number within this streamSwap invocation. */\n index: number;\n /** Total number of packets scheduled. */\n total: number;\n /** Source-asset amount sent for this packet (micro-units). */\n sourceAmount: bigint;\n /** Target-asset claim amount derived for this packet (micro-units). */\n targetAmount: bigint;\n /** Rate advertised on the `SwapPair` at swap start (decimal string). */\n advertisedRate: string;\n /** Effective rate for this packet as JS Number (target / source in whole units). Display-only. */\n effectiveRate: number;\n /** Absolute deviation from advertisedRate as a decimal (e.g., 0.0125 = 1.25%). */\n rateDeviation: number;\n /** Cumulative source sent across accepted packets so far (including this one). */\n cumulativeSource: bigint;\n /** Cumulative target received so far (including this one). */\n cumulativeTarget: bigint;\n /** Controller state at callback time. */\n state: 'running' | 'paused' | 'stopped';\n}\n\n/**\n * An accumulated claim successfully harvested from a single packet.\n *\n * @stable — Story 12.6 (`buildSettlementTx()`) depends on this shape.\n * Breaking changes require a coordinated migration.\n *\n * Story 12.6 ADDITIVE extension: the settlement-context fields\n * `channelId`, `nonce`, `cumulativeAmount`, `recipient`, and\n * `millSignerAddress` are marked optional (`?:`) for one story-cycle of\n * backward compat but are REQUIRED in practice: Story 12.6's\n * `buildSettlementTx()` throws `MISSING_SETTLEMENT_METADATA` when any of\n * these are absent.\n */\nexport interface AccumulatedClaim {\n /** 0-indexed position in the swap's packet stream. */\n packetIndex: number;\n /** Source-asset amount sent for this packet (micro-units). */\n sourceAmount: bigint;\n /**\n * Target-asset amount claimed (micro-units).\n *\n * **Source of truth caveat:** This is the expected target amount computed\n * by `applyRate(pair.rate)`. The actual signed-claim amount lives inside\n * `claimBytes`; Story 12.6 is responsible for parsing `claimBytes` per\n * chain and verifying the on-wire signed amount equals this expected amount.\n */\n targetAmount: bigint;\n /** Decrypted signed claim bytes. Chain-specific encoding per Story 12.4. */\n claimBytes: Uint8Array;\n /** Mill's ephemeral pubkey from the FULFILL (64-char lowercase hex). */\n millEphemeralPubkey: string;\n /** Optional Mill-side claim ID (passed through from handler metadata). */\n claimId?: string;\n /** Swap pair this claim was priced against (copy of `pair` for settlement-time routing). */\n pair: SwapPair;\n /** Unix ms timestamp when this claim was accepted. */\n receivedAt: number;\n // --- Story 12.6 settlement-context fields (additive) ---\n /** Channel identifier on the target chain (lowercase hex with 0x prefix for EVM; base58 for Solana). */\n channelId?: string;\n /** Balance-proof nonce (decimal string). Monotonically increasing within a channel. */\n nonce?: string;\n /** Cumulative transferred amount on the channel (target micro-units, decimal string). */\n cumulativeAmount?: string;\n /** Recipient address (the sender's target-asset address). */\n recipient?: string;\n /** Mill's on-chain signer address. */\n millSignerAddress?: string;\n}\n\n/**\n * Aggregate result of a `streamSwap()` invocation.\n *\n * @stable — Stories 12.6 and 12.8 depend on this shape.\n */\nexport interface StreamSwapResult {\n state: 'completed' | 'failed' | 'stopped';\n claims: AccumulatedClaim[];\n rejections: {\n packetIndex: number;\n sourceAmount: bigint;\n code: string;\n message: string;\n }[];\n errors: { packetIndex: number; cause: Error }[];\n abortReason:\n | 'complete'\n | 'aborted'\n | 'stopped'\n | 'callback-stop'\n | 'callback-throw'\n | 'rate-deviation'\n | 'all-rejected';\n cumulativeSource: bigint;\n cumulativeTarget: bigint;\n packetsSent: number;\n packetsScheduled: number;\n}\n\n/** External controller returned by {@link streamSwapControlled}. */\nexport interface StreamSwapController {\n pause(): void;\n resume(): void;\n stop(): void;\n readonly state: 'running' | 'paused' | 'stopped' | 'completed' | 'failed';\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nconst RATE_REGEX = /^(0|[1-9]\\d*)(\\.\\d+)?$/;\nconst HEX64_REGEX = /^[0-9a-f]{64}$/;\n// Strict base64: only `=` padding at the end, 0/1/2 pad chars. The character\n// class is intentionally restrictive — previous form `/^[A-Za-z0-9+/=]+$/`\n// accepted `=` anywhere and non-multiple-of-4 lengths, which could funnel\n// malformed payloads into `Buffer.from`/`JSON.parse` and surface confusing\n// errors. (Story 12.5 code-review pass #3.)\nconst BASE64_REGEX = /^[A-Za-z0-9+/]+={0,2}$/;\n\n/** True iff `s` is a non-empty, base64-shaped string of length multiple of 4. */\nfunction isBase64(s: string): boolean {\n if (s.length === 0 || s.length % 4 !== 0) return false;\n return BASE64_REGEX.test(s);\n}\n\n/** Length-split `total` into `count` bigints. Remainder absorbed on last element. */\nfunction chunkAmount(total: bigint, count: number): bigint[] {\n if (\n !Number.isInteger(count) ||\n count <= 0 ||\n count > Number.MAX_SAFE_INTEGER\n ) {\n throw new StreamSwapError(\n 'INVALID_CHUNKING',\n `packetCount must be a positive integer, got ${count}`\n );\n }\n if (total < BigInt(count)) {\n throw new StreamSwapError(\n 'INVALID_CHUNKING',\n `totalAmount (${total}) must be >= packetCount (${count}) so per-packet amount >= 1`\n );\n }\n const base = total / BigInt(count);\n const remainder = total - base * BigInt(count);\n const out: bigint[] = new Array(count);\n for (let i = 0; i < count; i++) out[i] = base;\n const last = out[count - 1] ?? 0n;\n out[count - 1] = last + remainder;\n return out;\n}\n\n/** Build the kind:20032 unsigned \"swap rumor\" event per AC-4.\n *\n * Story 12.9 AC-1/AC-6: the returned rumor MUST include a `chain-recipient`\n * tag carrying the sender-supplied chain-format payout address for\n * `pair.to.chain`. The value is echoed verbatim per packet — no case-folding\n * or transformation beyond what the sender-side `validateChainAddress`\n * accepts.\n */\nfunction buildSwapRumor(input: {\n senderPubkey: string;\n pair: SwapPair;\n sourceAmount: bigint;\n packetIndex: number;\n totalPackets: number;\n nonce: Uint8Array;\n createdAt: number;\n chainRecipient: string;\n}): UnsignedEvent {\n const {\n senderPubkey,\n pair,\n sourceAmount,\n packetIndex,\n totalPackets,\n nonce,\n createdAt,\n chainRecipient,\n } = input;\n return {\n kind: 20032,\n pubkey: senderPubkey,\n content: '',\n created_at: createdAt,\n tags: [\n ['swap-from', `${pair.from.assetCode}:${pair.from.chain}`],\n ['swap-to', `${pair.to.assetCode}:${pair.to.chain}`],\n ['amount', sourceAmount.toString()],\n ['seq', String(packetIndex), String(totalPackets)],\n ['nonce', Buffer.from(nonce).toString('hex')],\n ['chain-recipient', chainRecipient],\n ],\n };\n}\n\nconst EVM_CHANNEL_ID_REGEX = /^0x[0-9a-f]{64}$/;\nconst EVM_ADDRESS_REGEX = /^0x[0-9a-f]{40}$/;\nconst DECIMAL_UINT_REGEX = /^(0|[1-9]\\d*)$/;\n// Permissive base58 charset (Bitcoin alphabet — no 0, O, I, l).\nconst BASE58_REGEX = /^[1-9A-HJ-NP-Za-km-z]+$/;\n\n/**\n * Validate a chain-specific address or channelId string. `chain` is the\n * `pair.to.chain` string (e.g., `'evm:base:8453'`, `'solana:mainnet'`).\n *\n * Returns true for valid formats; false otherwise.\n */\nexport function validateChainAddress(\n value: string,\n chain: string,\n kind: 'channelId' | 'address'\n): boolean {\n if (chain.startsWith('evm:')) {\n // #153: viem / EIP-55 emits checksummed (mixed-case) addresses, and\n // callers commonly pass a checksummed `chainRecipient`. Lowercase-normalize\n // before the strict-lowercase-hex regex so a valid checksummed address (or\n // channelId) is accepted instead of being spuriously rejected with\n // INVALID_CHAIN_RECIPIENT / FULFILL_DECODE_FAILED.\n const normalized = value.toLowerCase();\n if (kind === 'channelId') return EVM_CHANNEL_ID_REGEX.test(normalized);\n return EVM_ADDRESS_REGEX.test(normalized);\n }\n if (chain.startsWith('solana:')) {\n // AC-3: base58 decode MUST succeed AND length MUST be 32 bytes. A pure\n // regex + char-length sanity check is too loose — a malformed \"32-char\"\n // base58 string may decode to 22–24 bytes and slip through.\n if (!BASE58_REGEX.test(value)) return false;\n if (value.length < 32 || value.length > 44) return false;\n try {\n return base58Decode(value).length === 32;\n } catch {\n return false;\n }\n }\n if (chain.startsWith('mina:')) {\n return BASE58_REGEX.test(value) && value.length >= 32;\n }\n // Unknown chain — permit; settlement layer will surface UNSUPPORTED_CHAIN.\n return value.length > 0;\n}\n\n/**\n * Decode the FULFILL response `data` (base64-encoded JSON metadata) into\n * the `{ claim, ephemeralPubkey, claimId?, targetAmount?, ...settlement }`\n * shape.\n *\n * Story 12.6 extension: also parses the settlement-context fields\n * (`channelId`, `nonce`, `cumulativeAmount`, `recipient`, `millSignerAddress`).\n * These are OPTIONAL and best-effort (#153): each is threaded through only\n * when it is a well-formed string for the target chain; an absent or malformed\n * settlement field is silently dropped rather than failing the whole decode,\n * so a fulfilled swap still surfaces its signed `claim` + `ephemeralPubkey`.\n * Only `claim` and `ephemeralPubkey` are strictly required.\n *\n * @param chain Optional `pair.to.chain` string for per-chain format validation\n * of channelId / recipient / millSignerAddress. When omitted, format checks\n * fall back to a length-only sanity check.\n */\nfunction decodeFulfillMetadata(\n data: string | undefined,\n chain?: string\n): {\n claim: string;\n ephemeralPubkey: string;\n claimId?: string;\n /** Optional Mill-reported actual target amount (decimal string). Used for rate deviation when present. */\n targetAmount?: string;\n channelId?: string;\n nonce?: string;\n cumulativeAmount?: string;\n recipient?: string;\n millSignerAddress?: string;\n} {\n if (data === undefined || data === null || data === '') {\n throw new StreamSwapError('FULFILL_DECODE_FAILED', 'FULFILL data missing');\n }\n // Quick-fail for obvious non-base64 input. Rejects '@' etc. and strings\n // that aren't a multiple-of-4 length.\n if (!isBase64(data)) {\n throw new StreamSwapError(\n 'FULFILL_DECODE_FAILED',\n 'FULFILL data is not valid base64'\n );\n }\n let jsonBytes: Buffer;\n try {\n jsonBytes = Buffer.from(data, 'base64');\n } catch (err) {\n throw new StreamSwapError(\n 'FULFILL_DECODE_FAILED',\n `FULFILL data base64 decode failed: ${err instanceof Error ? err.message : String(err)}`,\n { cause: err }\n );\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(jsonBytes.toString('utf8'));\n } catch (err) {\n throw new StreamSwapError(\n 'FULFILL_DECODE_FAILED',\n `FULFILL data JSON parse failed: ${err instanceof Error ? err.message : String(err)}`,\n { cause: err }\n );\n }\n if (!parsed || typeof parsed !== 'object') {\n throw new StreamSwapError(\n 'FULFILL_DECODE_FAILED',\n 'FULFILL metadata is not an object'\n );\n }\n const obj = parsed as Record<string, unknown>;\n const claim = obj['claim'];\n const ephemeralPubkey = obj['ephemeralPubkey'];\n if (typeof claim !== 'string' || !isBase64(claim)) {\n throw new StreamSwapError(\n 'FULFILL_DECODE_FAILED',\n 'FULFILL metadata.claim is missing or not base64 string'\n );\n }\n if (\n typeof ephemeralPubkey !== 'string' ||\n !HEX64_REGEX.test(ephemeralPubkey)\n ) {\n throw new StreamSwapError(\n 'FULFILL_DECODE_FAILED',\n 'FULFILL metadata.ephemeralPubkey is missing or not 64-char hex'\n );\n }\n const result: {\n claim: string;\n ephemeralPubkey: string;\n claimId?: string;\n targetAmount?: string;\n channelId?: string;\n nonce?: string;\n cumulativeAmount?: string;\n recipient?: string;\n millSignerAddress?: string;\n } = {\n claim,\n ephemeralPubkey,\n };\n if (typeof obj['claimId'] === 'string') {\n result.claimId = obj['claimId'] as string;\n }\n // Mill-reported target amount (optional). MUST be a non-negative integer\n // decimal string — reject signed / fractional / non-numeric values so a\n // malicious or buggy Mill cannot poison `cumulativeTarget`, the deviation\n // calc, or the `AccumulatedClaim.targetAmount` surface that Story 12.6\n // settles against. Presence of the field with a malformed value is a\n // FULFILL_DECODE_FAILED — the sender cannot safely consume the metadata.\n if (obj['targetAmount'] !== undefined) {\n const ta = obj['targetAmount'];\n if (typeof ta !== 'string' || !/^(0|[1-9]\\d*)$/.test(ta)) {\n throw new StreamSwapError(\n 'FULFILL_DECODE_FAILED',\n 'FULFILL metadata.targetAmount must be a non-negative integer decimal string'\n );\n }\n result.targetAmount = ta;\n }\n // Story 12.6 settlement-context fields — OPTIONAL, best-effort (#153).\n //\n // These five fields are only consumed downstream by `buildSettlementTx()`\n // (Story 12.6), which performs on-chain target redemption — a flow that is\n // itself #82-bounded (synthetic devnet channels) and independently validates\n // every field it consumes before signing. They are NOT required to surface\n // the mill's signed claim to the caller.\n //\n // The previous contract was all-or-nothing AND hard-failed the entire FULFILL\n // decode (`FULFILL_DECODE_FAILED`) on any partial/malformed settlement field.\n // That rejected otherwise-valid mill FULFILLs whenever the mill's real\n // envelope carried, e.g., a cross-chain channelId (an EVM-style hex channelId\n // echoed on a `solana:`/`mina:` target) or a checksummed address — so a\n // fulfilled swap reported `state: failed` with an empty `claims[]`.\n //\n // New contract: thread each settlement field through ONLY when it is a\n // well-formed string for the target chain; silently drop any field that is\n // absent or malformed. A swap whose mill omits/garbles settlement metadata\n // still completes with the signed claim; `buildSettlementTx()` will then\n // surface `MISSING_SETTLEMENT_METADATA` at settlement time if the caller\n // actually attempts on-chain redemption with an incomplete bundle.\n //\n // `recipient` is special-cased: it is the anti-substitution security check in\n // `runLoop` (the mill must echo the sender-supplied `chainRecipient`). We thread\n // it through whenever it is a non-empty string — even if it fails the chain\n // format check — so the runLoop equality check still fires. The equality\n // comparison there is the real boundary; a format-only mismatch must not be\n // silently dropped (which would skip the check entirely).\n const channelId = obj['channelId'];\n if (\n typeof channelId === 'string' &&\n (!chain || validateChainAddress(channelId, chain, 'channelId'))\n ) {\n result.channelId = channelId;\n }\n const nonce = obj['nonce'];\n if (typeof nonce === 'string' && DECIMAL_UINT_REGEX.test(nonce)) {\n result.nonce = nonce;\n }\n const cumulativeAmount = obj['cumulativeAmount'];\n if (\n typeof cumulativeAmount === 'string' &&\n DECIMAL_UINT_REGEX.test(cumulativeAmount)\n ) {\n result.cumulativeAmount = cumulativeAmount;\n }\n const recipient = obj['recipient'];\n if (typeof recipient === 'string' && recipient.length > 0) {\n // Always thread a present recipient so the runLoop anti-substitution\n // equality check (`metadata.recipient === params.chainRecipient`) runs.\n result.recipient = recipient;\n }\n const millSignerAddress = obj['millSignerAddress'];\n if (\n typeof millSignerAddress === 'string' &&\n (!chain || validateChainAddress(millSignerAddress, chain, 'address'))\n ) {\n result.millSignerAddress = millSignerAddress;\n }\n return result;\n}\n\n/** Simple Deferred for pause/resume gating. */\nclass Deferred<T> {\n readonly promise: Promise<T>;\n resolve!: (v: T) => void;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- mirror Promise constructor\n reject!: (e: any) => void;\n constructor() {\n this.promise = new Promise<T>((res, rej) => {\n this.resolve = res;\n this.reject = rej;\n });\n }\n}\n\nconst noop = (): void => undefined;\nconst NOOP_LOGGER: NonNullable<StreamSwapParams['logger']> = {\n debug: noop,\n info: noop,\n warn: noop,\n error: noop,\n};\n\n// ---------------------------------------------------------------------------\n// Validation (AC-2) — synchronous, fires before any packet\n// ---------------------------------------------------------------------------\n\nfunction validateParams(params: StreamSwapParams): void {\n if (typeof params.totalAmount !== 'bigint' || params.totalAmount <= 0n) {\n throw new StreamSwapError(\n 'INVALID_AMOUNT',\n `totalAmount must be a positive bigint, got ${String(params.totalAmount)}`\n );\n }\n\n const hasCount = params.packetCount !== undefined;\n const hasAmounts = params.packetAmounts !== undefined;\n if (hasCount === hasAmounts) {\n throw new StreamSwapError(\n 'INVALID_CHUNKING',\n 'Exactly one of packetCount or packetAmounts must be provided'\n );\n }\n\n if (hasCount) {\n const c = params.packetCount as number;\n if (!Number.isInteger(c) || c <= 0) {\n throw new StreamSwapError(\n 'INVALID_CHUNKING',\n `packetCount must be a positive integer, got ${c}`\n );\n }\n if (BigInt(c) > params.totalAmount) {\n throw new StreamSwapError(\n 'INVALID_CHUNKING',\n `packetCount (${c}) exceeds totalAmount (${params.totalAmount}); per-packet amount would be < 1 micro-unit`\n );\n }\n } else {\n const arr = params.packetAmounts as readonly bigint[];\n if (!Array.isArray(arr) || arr.length === 0) {\n throw new StreamSwapError(\n 'INVALID_CHUNKING',\n 'packetAmounts must be a non-empty array'\n );\n }\n let sum = 0n;\n for (const a of arr) {\n if (typeof a !== 'bigint' || a <= 0n) {\n throw new StreamSwapError(\n 'INVALID_CHUNKING',\n `packetAmounts entries must be positive bigint, got ${String(a)}`\n );\n }\n sum += a;\n }\n if (sum !== params.totalAmount) {\n throw new StreamSwapError(\n 'INVALID_CHUNKING',\n `sum(packetAmounts) (${sum}) !== totalAmount (${params.totalAmount})`\n );\n }\n }\n\n if (\n !(params.senderSecretKey instanceof Uint8Array) ||\n params.senderSecretKey.length !== 32\n ) {\n throw new StreamSwapError(\n 'INVALID_STATE',\n 'senderSecretKey must be a 32-byte Uint8Array'\n );\n }\n\n if (\n typeof params.millPubkey !== 'string' ||\n !HEX64_REGEX.test(params.millPubkey)\n ) {\n throw new StreamSwapError(\n 'INVALID_STATE',\n 'millPubkey must be a 64-char lowercase hex string'\n );\n }\n\n if (!params.pair || typeof params.pair !== 'object') {\n throw new StreamSwapError('INVALID_PAIR', 'pair is required');\n }\n // Guard the nested `from` / `to` shape before deep-reading `.assetScale` etc.\n // Otherwise malformed input produces a bare TypeError at `applyRate()` or\n // `buildSwapRumor()` instead of a categorized StreamSwapError.\n if (\n !params.pair.from ||\n typeof params.pair.from !== 'object' ||\n typeof params.pair.from.assetCode !== 'string' ||\n typeof params.pair.from.assetScale !== 'number' ||\n typeof params.pair.from.chain !== 'string'\n ) {\n throw new StreamSwapError(\n 'INVALID_PAIR',\n 'pair.from must have { assetCode: string, assetScale: number, chain: string }'\n );\n }\n if (\n !params.pair.to ||\n typeof params.pair.to !== 'object' ||\n typeof params.pair.to.assetCode !== 'string' ||\n typeof params.pair.to.assetScale !== 'number' ||\n typeof params.pair.to.chain !== 'string'\n ) {\n throw new StreamSwapError(\n 'INVALID_PAIR',\n 'pair.to must have { assetCode: string, assetScale: number, chain: string }'\n );\n }\n if (\n typeof params.pair.rate !== 'string' ||\n !RATE_REGEX.test(params.pair.rate)\n ) {\n throw new StreamSwapError(\n 'INVALID_PAIR',\n `pair.rate must match ${RATE_REGEX}, got ${params.pair.rate}`\n );\n }\n try {\n applyRate({\n sourceAmount: 1n,\n fromScale: params.pair.from.assetScale,\n toScale: params.pair.to.assetScale,\n rate: params.pair.rate,\n });\n } catch (err) {\n throw new StreamSwapError(\n 'INVALID_PAIR',\n `pair failed applyRate sanity check: ${err instanceof Error ? err.message : String(err)}`,\n { cause: err }\n );\n }\n\n if (\n params.rateDeviationThreshold !== undefined &&\n (typeof params.rateDeviationThreshold !== 'number' ||\n !Number.isFinite(params.rateDeviationThreshold) ||\n params.rateDeviationThreshold < 0)\n ) {\n throw new StreamSwapError(\n 'INVALID_STATE',\n `rateDeviationThreshold must be a non-negative finite number, got ${params.rateDeviationThreshold}`\n );\n }\n\n // Story 12.9 AC-4 / AC-5: `chainRecipient` is REQUIRED and MUST validate\n // against `pair.to.chain`. Defense-in-depth for JS callers who bypass the\n // TS interface (the field is declared non-optional on StreamSwapParams).\n if (\n typeof params.chainRecipient !== 'string' ||\n params.chainRecipient.length === 0\n ) {\n throw new StreamSwapError(\n 'INVALID_STATE',\n 'chainRecipient must be a non-empty string (sender payout address for pair.to.chain)'\n );\n }\n if (\n !validateChainAddress(\n params.chainRecipient,\n params.pair.to.chain,\n 'address'\n )\n ) {\n throw new StreamSwapError(\n 'INVALID_CHAIN_RECIPIENT',\n `chainRecipient ${params.chainRecipient} is malformed for chain ${params.pair.to.chain}`\n );\n }\n}\n\n// ---------------------------------------------------------------------------\n// Core: streamSwapControlled (AC-6, AC-7, AC-10)\n// ---------------------------------------------------------------------------\n\n/**\n * Drive a multi-packet swap against a Mill and return a `StreamSwapResult`.\n *\n * `streamSwap()` does NOT throw on mid-stream failure — inspect the result's\n * `state`, `abortReason`, `rejections[]`, and `errors[]` to diagnose.\n *\n * @example\n * ```ts\n * // Discover the SwapPair from the Mill's kind:10032 peer-info event (Story 12.1).\n * const result = await streamSwap({\n * client: toonClient,\n * millPubkey: mill.pubkey,\n * millIlpAddress: 'g.toon.mill1',\n * pair,\n * senderSecretKey,\n * totalAmount: 1_000_000n,\n * packetCount: 10,\n * onPacket: (p) => console.log('packet', p.index, 'rate', p.effectiveRate),\n * rateDeviationThreshold: 0.02,\n * });\n * // Feed result.claims into buildSettlementTx() from Story 12.6.\n * ```\n *\n * @throws {StreamSwapError} Only for construction-time validation failures\n * (INVALID_AMOUNT, INVALID_CHUNKING, INVALID_PAIR, INVALID_STATE).\n */\nexport async function streamSwap(\n params: StreamSwapParams\n): Promise<StreamSwapResult> {\n // Note: validation errors are thrown synchronously inside\n // `streamSwapControlled`. We wrap the call so construction-time throws\n // become Promise rejections for ergonomic `await` / `.rejects` handling.\n return streamSwapControlled(params).result;\n}\n\n/**\n * Two-form variant of {@link streamSwap} that additionally returns a\n * {@link StreamSwapController} with `pause()` / `resume()` / `stop()`.\n *\n * @throws {StreamSwapError} Synchronously for construction-time validation.\n */\nexport function streamSwapControlled(params: StreamSwapParams): {\n result: Promise<StreamSwapResult>;\n controller: StreamSwapController;\n} {\n // Construction-time validation — synchronous throw per AC-2 & AC-9.\n validateParams(params);\n\n const logger = params.logger ?? NOOP_LOGGER;\n\n // Derive schedule\n const schedule: bigint[] = params.packetAmounts\n ? [...params.packetAmounts]\n : chunkAmount(params.totalAmount, params.packetCount as number);\n\n // Freeze a defensive copy of `pair` so callers can't mutate the stored\n // reference on every AccumulatedClaim post-call. (Story 12.5 code-review\n // pass #3.) Shape matches SwapPair (Story 12.1 stable type).\n const frozenPair: SwapPair = Object.freeze({\n from: Object.freeze({ ...params.pair.from }),\n to: Object.freeze({ ...params.pair.to }),\n rate: params.pair.rate,\n }) as SwapPair;\n\n const senderPubkey = getPublicKey(params.senderSecretKey);\n\n // Controller state machine\n type State = 'running' | 'paused' | 'stopped' | 'completed' | 'failed';\n let streamState: State = 'running';\n let resumeDeferred: Deferred<'resume' | 'stop'> | null = null;\n\n const controller: StreamSwapController = {\n pause(): void {\n if (streamState === 'running') {\n streamState = 'paused';\n // resumeDeferred will be created on first await in loop\n }\n },\n resume(): void {\n if (streamState === 'paused') {\n streamState = 'running';\n if (resumeDeferred) {\n resumeDeferred.resolve('resume');\n resumeDeferred = null;\n }\n } else if (streamState === 'running') {\n // no-op\n } else {\n throw new StreamSwapError(\n 'INVALID_STATE',\n `Cannot resume from state \"${streamState}\"`\n );\n }\n },\n stop(): void {\n if (streamState === 'completed' || streamState === 'failed') return;\n const prev = streamState;\n streamState = 'stopped';\n if (prev === 'paused' && resumeDeferred) {\n resumeDeferred.resolve('stop');\n resumeDeferred = null;\n }\n },\n get state(): State {\n return streamState;\n },\n };\n\n const result = runLoop(\n params,\n frozenPair,\n schedule,\n senderPubkey,\n logger,\n () => streamState,\n (v: State) => {\n streamState = v;\n },\n () => {\n if (streamState !== 'paused') return Promise.resolve('resume');\n if (!resumeDeferred) resumeDeferred = new Deferred<'resume' | 'stop'>();\n return resumeDeferred.promise;\n }\n );\n\n return { result, controller };\n}\n\n// ---------------------------------------------------------------------------\n// Core loop\n// ---------------------------------------------------------------------------\n\nasync function runLoop(\n params: StreamSwapParams,\n pair: SwapPair,\n schedule: bigint[],\n senderPubkey: string,\n logger: NonNullable<StreamSwapParams['logger']>,\n getState: () => 'running' | 'paused' | 'stopped' | 'completed' | 'failed',\n setState: (\n v: 'running' | 'paused' | 'stopped' | 'completed' | 'failed'\n ) => void,\n waitForResumeOrStop: () => Promise<'resume' | 'stop'>\n): Promise<StreamSwapResult> {\n const claims: AccumulatedClaim[] = [];\n const rejections: StreamSwapResult['rejections'] = [];\n const errors: StreamSwapResult['errors'] = [];\n let cumulativeSource = 0n;\n let cumulativeTarget = 0n;\n let packetsSent = 0;\n let abortReason: StreamSwapResult['abortReason'] = 'complete';\n\n const totalPackets = schedule.length;\n\n // Snapshot the signal abort state at each check — we don't listen via\n // addEventListener because we check at loop boundaries (between packets).\n const isAborted = (): boolean => params.signal?.aborted === true;\n\n packetLoop: for (\n let packetIndex = 0;\n packetIndex < totalPackets;\n packetIndex++\n ) {\n // --- Abort/stop/pause checks at loop boundary ---\n if (isAborted()) {\n abortReason = 'aborted';\n break;\n }\n if (getState() === 'stopped') {\n abortReason = 'stopped';\n break;\n }\n if (getState() === 'paused') {\n const resumedBy = await waitForResumeOrStop();\n if (resumedBy === 'stop' || getState() === 'stopped') {\n abortReason = 'stopped';\n break;\n }\n // After resume, re-check abort signal\n if (isAborted()) {\n abortReason = 'aborted';\n break;\n }\n }\n\n // Bounds-checked at loop init (`packetIndex < totalPackets === schedule.length`).\n // Narrow out the `undefined` TS widening without silently masking with 0n,\n // which would hide a real bug if the schedule were ever mutated mid-loop.\n // (Story 12.5 code-review pass #3.)\n const sourceAmount = schedule[packetIndex];\n if (sourceAmount === undefined) {\n // Defensive: should be impossible given the bound check. Surface, don't mask.\n throw new StreamSwapError(\n 'INVALID_STATE',\n `schedule[${packetIndex}] is undefined; schedule was mutated mid-stream`\n );\n }\n\n // --- Build + wrap packet ---\n const nonce = new Uint8Array(16);\n getRandomValues(nonce);\n const rumor = buildSwapRumor({\n senderPubkey,\n pair,\n sourceAmount,\n packetIndex: packetIndex + 1,\n totalPackets,\n nonce,\n createdAt: Math.floor(Date.now() / 1000),\n chainRecipient: params.chainRecipient,\n });\n\n let toonData: Uint8Array;\n try {\n const wrapped = wrapSwapPacketToToon({\n rumor,\n senderSecretKey: params.senderSecretKey,\n recipientPubkey: params.millPubkey,\n destination: params.millIlpAddress,\n amount: sourceAmount,\n });\n // `wrapped.ilpPrepare.data` is base64 per buildIlpPrepare. Decode back\n // to raw bytes for the sender API (Uint8Array contract in AC-3).\n toonData = new Uint8Array(Buffer.from(wrapped.ilpPrepare.data, 'base64'));\n } catch (err) {\n logger.error({\n event: 'stream_swap.wrap_failed',\n packetIndex,\n error: err instanceof Error ? err.message : String(err),\n });\n errors.push({\n packetIndex,\n cause: err instanceof Error ? err : new Error(String(err)),\n });\n continue;\n }\n\n // --- Send packet via client ---\n let sendResult: IlpSendResultLike;\n try {\n sendResult = await params.client.sendSwapPacket({\n destination: params.millIlpAddress,\n amount: sourceAmount,\n toonData,\n timeout: params.packetTimeoutMs ?? 30000,\n claim: params.claim,\n });\n packetsSent += 1;\n } catch (err) {\n logger.error({\n event: 'stream_swap.send_failed',\n packetIndex,\n error: err instanceof Error ? err.message : String(err),\n });\n errors.push({\n packetIndex,\n cause: err instanceof Error ? err : new Error(String(err)),\n });\n continue;\n }\n\n // --- Rejection path ---\n if (!sendResult.accepted) {\n const code = sendResult.code ?? 'F00';\n const message = sendResult.message ?? 'rejected';\n logger.warn({\n event: 'stream_swap.packet_rejected',\n packetIndex,\n code,\n message,\n });\n rejections.push({ packetIndex, sourceAmount, code, message });\n continue;\n }\n\n // --- Decode FULFILL metadata ---\n let metadata: {\n claim: string;\n ephemeralPubkey: string;\n claimId?: string;\n targetAmount?: string;\n channelId?: string;\n nonce?: string;\n cumulativeAmount?: string;\n recipient?: string;\n millSignerAddress?: string;\n };\n try {\n metadata = decodeFulfillMetadata(sendResult.data, pair.to.chain);\n } catch (err) {\n logger.error({\n event: 'stream_swap.fulfill_decode_failed',\n packetIndex,\n error: err instanceof Error ? err.message : String(err),\n });\n errors.push({\n packetIndex,\n cause: err instanceof Error ? err : new Error(String(err)),\n });\n continue;\n }\n\n // Story 12.9 AC-7: when the Mill echoes a `recipient` in FULFILL metadata\n // (Story 12.6 settlement context), it MUST equal the sender-supplied\n // `chainRecipient`. A mismatch indicates the Mill is substituting its own\n // address — refuse to accumulate the claim and surface a per-packet\n // rejection with a clear reason code. Missing recipient = legacy\n // (pre-12.6) metadata, permitted.\n //\n // #153: EVM addresses are case-insensitive (EIP-55 checksum casing is\n // purely a typo-detection hint). The mill lowercases its echoed recipient\n // while the sender may pass a checksummed `chainRecipient` (or vice versa);\n // compare case-insensitively on EVM targets so a casing-only difference is\n // NOT flagged as a substitution attack. Non-EVM chains keep the exact\n // (base58 case-sensitive) comparison.\n const isEvmTarget = pair.to.chain.startsWith('evm:');\n const recipientMatches =\n metadata.recipient === undefined ||\n (isEvmTarget\n ? metadata.recipient.toLowerCase() ===\n params.chainRecipient.toLowerCase()\n : metadata.recipient === params.chainRecipient);\n if (!recipientMatches) {\n logger.warn({\n event: 'stream_swap.recipient_mismatch',\n packetIndex,\n expected: params.chainRecipient,\n actual: metadata.recipient,\n });\n rejections.push({\n packetIndex,\n sourceAmount,\n code: 'MILL_RECIPIENT_MISMATCH',\n message: `Mill echoed recipient ${metadata.recipient} but sender expected ${params.chainRecipient}`,\n });\n continue;\n }\n\n // --- Decrypt claim ---\n let claimBytes: Uint8Array;\n try {\n const ciphertext = new Uint8Array(Buffer.from(metadata.claim, 'base64'));\n claimBytes = decryptFulfillClaim({\n ciphertext,\n ephemeralPubkey: metadata.ephemeralPubkey,\n recipientSecretKey: params.senderSecretKey,\n });\n } catch (err) {\n logger.error({\n event: 'stream_swap.decrypt_failed',\n packetIndex,\n error: err instanceof Error ? err.message : String(err),\n });\n errors.push({\n packetIndex,\n cause: err instanceof Error ? err : new Error(String(err)),\n });\n continue;\n }\n\n if (claimBytes.length === 0) {\n logger.warn({\n event: 'stream_swap.empty_claim_bytes',\n packetIndex,\n });\n }\n\n // --- Compute expected + actual target + deviation ---\n const expectedTargetAmount = applyRate({\n sourceAmount,\n fromScale: pair.from.assetScale,\n toScale: pair.to.assetScale,\n rate: pair.rate,\n });\n\n // If the Mill includes a `targetAmount` in the FULFILL metadata, use it\n // (chain-specific parsing of `claimBytes` is Story 12.6's job). Otherwise\n // fall back to the advertised-rate expected amount so settlement-time\n // verification still has a baseline.\n // `metadata.targetAmount`, when present, is already validated as a\n // non-negative integer decimal string by `decodeFulfillMetadata`, so\n // `BigInt()` is safe here (no try/catch needed).\n const targetAmount: bigint =\n metadata.targetAmount !== undefined\n ? BigInt(metadata.targetAmount)\n : expectedTargetAmount;\n\n // BigInt-safe deviation: scale up by 1e6 before dividing so we don't lose\n // precision on 18-decimal assets. (Epic 11 retro MAX_SAFE_INTEGER guard.)\n let rateDeviation = 0;\n if (expectedTargetAmount > 0n) {\n const diff =\n targetAmount >= expectedTargetAmount\n ? targetAmount - expectedTargetAmount\n : expectedTargetAmount - targetAmount;\n const scaled = (diff * 1_000_000n) / expectedTargetAmount;\n rateDeviation = Number(scaled) / 1_000_000;\n }\n\n // Display-only effectiveRate for the callback payload. Guard against\n // non-finite results (e.g., advertisedRate=0 + any deviation -> 0;\n // parseFloat surprises) so callback consumers never observe NaN/Infinity.\n const advertisedRate = parseFloat(pair.rate);\n let effectiveRate: number;\n if (targetAmount === expectedTargetAmount) {\n effectiveRate = advertisedRate;\n } else {\n const signedDeviation =\n targetAmount >= expectedTargetAmount ? rateDeviation : -rateDeviation;\n effectiveRate = advertisedRate * (1 + signedDeviation);\n }\n if (!Number.isFinite(effectiveRate)) {\n effectiveRate = advertisedRate;\n }\n\n cumulativeSource += sourceAmount;\n cumulativeTarget += targetAmount;\n\n const accumulated: AccumulatedClaim = {\n packetIndex,\n sourceAmount,\n targetAmount,\n claimBytes,\n millEphemeralPubkey: metadata.ephemeralPubkey,\n pair,\n receivedAt: Date.now(),\n };\n if (metadata.claimId !== undefined) accumulated.claimId = metadata.claimId;\n // Story 12.6: thread settlement-context fields through when present.\n if (metadata.channelId !== undefined)\n accumulated.channelId = metadata.channelId;\n if (metadata.nonce !== undefined) accumulated.nonce = metadata.nonce;\n if (metadata.cumulativeAmount !== undefined)\n accumulated.cumulativeAmount = metadata.cumulativeAmount;\n if (metadata.recipient !== undefined)\n accumulated.recipient = metadata.recipient;\n if (metadata.millSignerAddress !== undefined)\n accumulated.millSignerAddress = metadata.millSignerAddress;\n claims.push(accumulated);\n\n logger.debug({\n event: 'stream_swap.packet_accepted',\n packetIndex,\n sourceAmount: sourceAmount.toString(),\n targetAmount: targetAmount.toString(),\n });\n\n // --- onPacket callback ---\n if (params.onPacket) {\n const progress: PacketProgress = Object.freeze({\n index: packetIndex,\n total: totalPackets,\n sourceAmount,\n targetAmount,\n advertisedRate: pair.rate,\n effectiveRate,\n rateDeviation,\n cumulativeSource,\n cumulativeTarget,\n state:\n getState() === 'paused'\n ? 'paused'\n : getState() === 'stopped'\n ? 'stopped'\n : 'running',\n });\n\n try {\n const maybePromise = params.onPacket(progress);\n if (\n maybePromise &&\n typeof (maybePromise as Promise<void>).then === 'function'\n ) {\n await maybePromise;\n }\n } catch (err) {\n logger.warn({\n event: 'stream_swap.callback_threw',\n packetIndex,\n error: err instanceof Error ? err.message : String(err),\n });\n errors.push({\n packetIndex,\n cause: err instanceof Error ? err : new Error(String(err)),\n });\n abortReason = 'callback-throw';\n break packetLoop;\n }\n }\n\n // --- Abort signal / stop check AFTER callback (so tests can abort\n // inside onPacket and we exit the loop on the next iteration's\n // boundary check — but honor stopped/aborted without running\n // additional packets). ---\n if (isAborted()) {\n abortReason = 'aborted';\n break;\n }\n if (getState() === 'stopped') {\n abortReason = 'stopped';\n break;\n }\n\n // --- Rate deviation check (after callback, after accumulation) ---\n if (\n params.rateDeviationThreshold !== undefined &&\n rateDeviation > params.rateDeviationThreshold\n ) {\n abortReason = 'rate-deviation';\n break;\n }\n }\n\n // --- Terminal state ---\n let finalState: 'completed' | 'failed' | 'stopped';\n if (abortReason === 'aborted' || abortReason === 'stopped') {\n finalState = 'stopped';\n } else if (\n claims.length === 0 &&\n (rejections.length > 0 || errors.length > 0)\n ) {\n finalState = 'failed';\n // If we drained the entire schedule without accepting any claim AND the\n // only failures were Mill rejections, surface that explicitly so callers\n // can distinguish \"all rejected\" from \"loop aborted early with no claims\".\n if (\n abortReason === 'complete' &&\n rejections.length > 0 &&\n errors.length === 0\n ) {\n abortReason = 'all-rejected';\n }\n } else {\n finalState = 'completed';\n }\n\n setState(finalState);\n\n return {\n state: finalState,\n claims,\n rejections,\n errors,\n abortReason,\n cumulativeSource,\n cumulativeTarget,\n packetsSent,\n packetsScheduled: totalPackets,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Crypto RNG — prefer globalThis.crypto; fall back to node:crypto.webcrypto.\n// ---------------------------------------------------------------------------\n\nfunction getRandomValues(buf: Uint8Array): Uint8Array {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- defensive env probe\n const g: any = globalThis as any;\n if (g.crypto && typeof g.crypto.getRandomValues === 'function') {\n g.crypto.getRandomValues(buf);\n return buf;\n }\n // Node 18/20 fallback\n // eslint-disable-next-line @typescript-eslint/no-require-imports -- fallback only\n const nodeCrypto = require('node:crypto') as {\n webcrypto?: { getRandomValues: (b: Uint8Array) => Uint8Array };\n randomFillSync?: (b: Uint8Array) => Uint8Array;\n };\n if (nodeCrypto.webcrypto?.getRandomValues) {\n nodeCrypto.webcrypto.getRandomValues(buf);\n return buf;\n }\n if (nodeCrypto.randomFillSync) {\n nodeCrypto.randomFillSync(buf);\n return buf;\n }\n throw new StreamSwapError(\n 'INVALID_STATE',\n 'No crypto.getRandomValues available in this environment'\n );\n}\n\n// Re-export `chunkAmount` / `decodeFulfillMetadata` / `buildSwapRumor` via a\n// single-purpose testing surface so unit tests can exercise helpers directly.\n// This surface is NOT part of the public SDK; it is intentionally excluded\n// from `packages/sdk/src/index.ts` per AC-1.\nexport const __testing = {\n chunkAmount,\n decodeFulfillMetadata,\n buildSwapRumor,\n};\n","/**\n * Persists the apex payment-channel id (+ its chain context) per\n * (destination, chain) so a daemon RESTART can resume the EXISTING on-chain\n * channel instead of opening a new one.\n *\n * Why this is needed: `ChannelManager` persists the off-chain nonce/cumulative\n * watermark (keyed by channelId) but NOT the peer→channelId mapping. So after a\n * restart `openChannel()` would open + re-deposit into a fresh channel, which\n * reverts on a chain where the deposit already exists. With the channelId saved\n * here, the runner instead calls `trackChannel(channelId, context)` — which\n * rehydrates the nonce from the channel store — and signs against the live\n * channel with zero on-chain writes.\n */\n\nimport { mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { dirname } from 'node:path';\n\n/** Chain context needed to re-track a channel (matches ChannelManager.trackChannel). */\nexport interface PersistedChannelContext {\n chainType: string;\n chainId: number;\n tokenNetworkAddress: string;\n tokenAddress?: string;\n recipient?: string;\n}\n\nexport interface PersistedApexChannel {\n channelId: string;\n context: PersistedChannelContext;\n}\n\ntype Store = Record<string, PersistedApexChannel>;\n\nfunction key(destination: string, chain: string): string {\n return `${destination}|${chain}`;\n}\n\nfunction readStore(path: string): Store {\n try {\n return JSON.parse(readFileSync(path, 'utf8')) as Store;\n } catch {\n return {};\n }\n}\n\n/** Load the saved apex channel for (destination, chain), or null. */\nexport function loadApexChannel(\n path: string,\n destination: string,\n chain: string\n): PersistedApexChannel | null {\n return readStore(path)[key(destination, chain)] ?? null;\n}\n\n/** Save the apex channel for (destination, chain) with mode 0o600. */\nexport function saveApexChannel(\n path: string,\n destination: string,\n chain: string,\n record: PersistedApexChannel\n): void {\n const store = readStore(path);\n store[key(destination, chain)] = record;\n mkdirSync(dirname(path), { recursive: true });\n writeFileSync(path, JSON.stringify(store, null, 2), { mode: 0o600 });\n}\n","/**\n * Persists DYNAMIC targets — relays added via `toon_add_relay` and apexes added\n * via `toon_add_apex` — to `~/.toon-client/targets.json` so they survive a\n * daemon restart. The config file's single `relayUrl`/`btpUrl` remain the\n * permanent \"default\" target and are NOT stored here; only runtime additions are.\n *\n * On boot the `ClientRunner` seeds the default from config, then replays this\n * store to re-instantiate every dynamically-added relay/apex. Add/remove tool\n * calls write straight back here (last-write-wins, mode 0o600), mirroring the\n * `apex-channel-store.ts` pattern.\n */\n\nimport { mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { configDir } from './config.js';\nimport type { ApexNegotiationConfig } from './config.js';\n\n/** A persisted relay read target. */\nexport interface PersistedRelayTarget {\n /** Relay WS URL (the map key). */\n relayUrl: string;\n}\n\n/** A persisted apex write target with its discovered settlement negotiation. */\nexport interface PersistedApexTarget {\n /** BTP WS endpoint of the apex (the map key). */\n btpUrl: string;\n /** Settlement negotiation, discovered from the apex's kind:10032 announcement. */\n negotiation: ApexNegotiationConfig;\n /** Child peers reached via this apex's channel (e.g. `[\"store\",\"swap\"]`). */\n apexChildPeers?: string[];\n /** Per-write fee override (base units). Falls back to the daemon default. */\n feePerEvent?: string;\n /** Relay the negotiation was discovered on (re-discovery / provenance). */\n discoveredFrom?: string;\n}\n\nexport interface TargetsFile {\n relays: PersistedRelayTarget[];\n apexes: PersistedApexTarget[];\n}\n\nconst EMPTY: TargetsFile = { relays: [], apexes: [] };\n\n/** Default targets-store path: `~/.toon-client/targets.json`. */\nexport function defaultTargetsPath(): string {\n return join(configDir(), 'targets.json');\n}\n\n/** Read + parse the targets store, returning empty arrays when absent/invalid. */\nexport function loadTargets(path = defaultTargetsPath()): TargetsFile {\n let parsed: Partial<TargetsFile>;\n try {\n parsed = JSON.parse(readFileSync(path, 'utf8')) as Partial<TargetsFile>;\n } catch {\n return { relays: [], apexes: [] };\n }\n return {\n relays: Array.isArray(parsed.relays) ? parsed.relays : [],\n apexes: Array.isArray(parsed.apexes) ? parsed.apexes : [],\n };\n}\n\nfunction write(path: string, data: TargetsFile): void {\n mkdirSync(dirname(path), { recursive: true });\n writeFileSync(path, JSON.stringify(data, null, 2), { mode: 0o600 });\n}\n\n/** Upsert a relay target (idempotent by `relayUrl`). */\nexport function saveRelayTarget(\n relayUrl: string,\n path = defaultTargetsPath()\n): void {\n const store = loadTargets(path);\n if (!store.relays.some((r) => r.relayUrl === relayUrl)) {\n store.relays.push({ relayUrl });\n write(path, store);\n }\n}\n\n/** Remove a relay target. Returns true if it was present. */\nexport function removeRelayTarget(\n relayUrl: string,\n path = defaultTargetsPath()\n): boolean {\n const store = loadTargets(path);\n const next = store.relays.filter((r) => r.relayUrl !== relayUrl);\n if (next.length === store.relays.length) return false;\n store.relays = next;\n write(path, store);\n return true;\n}\n\n/** Upsert an apex target (last-write-wins by `btpUrl`). */\nexport function saveApexTarget(\n target: PersistedApexTarget,\n path = defaultTargetsPath()\n): void {\n const store = loadTargets(path);\n store.apexes = store.apexes.filter((a) => a.btpUrl !== target.btpUrl);\n store.apexes.push(target);\n write(path, store);\n}\n\n/** Remove an apex target. Returns true if it was present. */\nexport function removeApexTarget(\n btpUrl: string,\n path = defaultTargetsPath()\n): boolean {\n const store = loadTargets(path);\n const next = store.apexes.filter((a) => a.btpUrl !== btpUrl);\n if (next.length === store.apexes.length) return false;\n store.apexes = next;\n write(path, store);\n return true;\n}\n\nexport { EMPTY as EMPTY_TARGETS };\n","/**\n * Discover an apex's settlement negotiation by reading its `kind:10032`\n * (`ILP_PEER_INFO_KIND`) announcement off a relay, rather than making the caller\n * hand-supply chain/settlement params. An apex (relay node) publishes its\n * `IlpPeerInfo` — `btpEndpoint`, `supportedChains`, `settlementAddresses`,\n * `preferredTokens`, `tokenNetworks` — to its relay; this module subscribes for\n * it, parses via core's `parseIlpPeerInfo`, and maps it onto the daemon's\n * `ApexNegotiationConfig` so the runner can stand up a `ToonClient` against it.\n *\n * The relay is injected as a `RelaySubscription` (already started) so this is\n * unit-testable with a fake WS and reuses a relay the runner already manages.\n */\n\nimport {\n ILP_PEER_INFO_KIND,\n parseIlpPeerInfo,\n isEventExpired,\n} from '@toon-protocol/core';\nimport type { NostrEvent } from 'nostr-tools/pure';\nimport type { RelaySubscription } from '../relay-subscription.js';\nimport type { ApexNegotiationConfig } from './config.js';\nimport type { SettlementChain } from '../control-api.js';\n\nexport interface DiscoverApexParams {\n /** A started relay subscription to query for the apex's kind:10032. */\n relay: RelaySubscription;\n /** ILP address of the apex to match (e.g. `g.proxy`). */\n ilpAddress: string;\n /** Optional apex Nostr pubkey to narrow the REQ filter (64-char hex). */\n pubkey?: string;\n /** Preferred settlement chain family; defaults to the first supported chain. */\n chain?: SettlementChain;\n /** Child peers reached via this apex's channel (e.g. `[\"store\",\"swap\"]`). */\n childPeers?: string[];\n /** Max time to wait for the announcement, ms. Default 15000. */\n timeoutMs?: number;\n /** Poll interval against the relay buffer, ms. Default 250. */\n pollMs?: number;\n}\n\nexport interface DiscoveredApex {\n btpUrl: string;\n negotiation: ApexNegotiationConfig;\n apexChildPeers?: string[];\n}\n\n/**\n * Thrown when discovery fails. `retryable` is true for a TIMEOUT (the apex may\n * just be slow/offline — the caller can retry once it's reachable) and false\n * for a malformed announcement (retrying won't help until the apex republishes).\n */\nexport class ApexDiscoveryError extends Error {\n constructor(\n message: string,\n readonly retryable = false\n ) {\n super(message);\n this.name = 'ApexDiscoveryError';\n }\n}\n\n/**\n * Subscribe for the apex's `kind:10032`, wait for a match, and map it to an\n * `ApexNegotiationConfig` + `btpUrl`. Rejects with {@link ApexDiscoveryError}\n * on timeout or when the announcement lacks settlement params for any chain.\n */\nexport async function discoverApex(\n params: DiscoverApexParams\n): Promise<DiscoveredApex> {\n const { relay, ilpAddress, pubkey, chain, childPeers } = params;\n const timeoutMs = params.timeoutMs ?? 15_000;\n const pollMs = params.pollMs ?? 250;\n\n const subId = relay.subscribe(\n [\n {\n kinds: [ILP_PEER_INFO_KIND],\n ...(pubkey ? { authors: [pubkey] } : {}),\n },\n ],\n `apex-discovery-${ilpAddress}`\n );\n\n try {\n const deadline = Date.now() + timeoutMs;\n let cursor = 0;\n while (Date.now() < deadline) {\n // Scan the WHOLE relay buffer (no subId filter), not just our discovery\n // subscription. `RelaySubscription` de-dups by event.id globally, so if a\n // pre-existing subscription already buffered the announcement, the relay's\n // replay to our fresh REQ is dropped and would never appear under our\n // subId — buffer-wide reads find it regardless of which sub received it.\n const { events, cursor: next } = relay.getEvents({ cursor });\n cursor = next;\n const match = events.find((e) => matchesApex(e, ilpAddress, pubkey));\n if (match) return mapAnnouncement(match, { chain, childPeers });\n await delay(pollMs);\n }\n throw new ApexDiscoveryError(\n `Timed out after ${timeoutMs}ms waiting for the apex kind:${ILP_PEER_INFO_KIND} ` +\n `announcement for \"${ilpAddress}\" on the relay. Is the relay reachable and the apex online?`,\n true // retryable: the apex may just be slow/offline\n );\n } finally {\n relay.unsubscribe(subId);\n }\n}\n\n/**\n * Whether a kind:10032 event announces the target apex's ILP address. When a\n * `pubkey` is given it must also match the event author — multiple nodes can\n * advertise the same ILP address (e.g. `g.proxy`), so the pubkey is how\n * the caller disambiguates which one to add. (Buffer-wide scanning means we can\n * no longer rely on the REQ's `authors` filter to do this for us.)\n */\nfunction matchesApex(\n event: NostrEvent,\n ilpAddress: string,\n pubkey?: string\n): boolean {\n if (event.kind !== ILP_PEER_INFO_KIND) return false;\n if (pubkey && event.pubkey !== pubkey) return false;\n // A NIP-40-expired announcement means the apex stopped re-publishing — it is\n // offline and its advertised BTP endpoint is unreachable, so don't match it\n // (issue #261). Discovery keeps waiting for a fresh, unexpired announcement.\n if (isEventExpired(event)) return false;\n try {\n const info = parseIlpPeerInfo(event);\n const addrs = info.ilpAddresses ?? [info.ilpAddress];\n return addrs.includes(ilpAddress) || info.ilpAddress === ilpAddress;\n } catch {\n return false;\n }\n}\n\n/** Map a parsed kind:10032 announcement onto the daemon's negotiation config. */\nfunction mapAnnouncement(\n event: NostrEvent,\n opts: { chain?: SettlementChain; childPeers?: string[] }\n): DiscoveredApex {\n const info = parseIlpPeerInfo(event);\n const chains = info.supportedChains ?? [];\n if (chains.length === 0) {\n throw new ApexDiscoveryError(\n `Apex \"${info.ilpAddress}\" announced no supportedChains — cannot settle.`\n );\n }\n\n // Pick the chainKey: prefer one whose family matches the requested chain,\n // else the first advertised chain.\n const chainKey =\n (opts.chain\n ? chains.find((c) => c.split(':')[0] === opts.chain)\n : undefined) ?? chains[0];\n if (!chainKey) {\n throw new ApexDiscoveryError(\n `Apex \"${info.ilpAddress}\" announced no usable settlement chain.`\n );\n }\n const family = chainKey.split(':')[0] as SettlementChain;\n const settlementAddress = info.settlementAddresses?.[chainKey];\n if (!settlementAddress) {\n throw new ApexDiscoveryError(\n `Apex \"${info.ilpAddress}\" announced no settlementAddress for chain \"${chainKey}\".`\n );\n }\n\n const btpUrl = info.btpEndpoint;\n if (!btpUrl) {\n throw new ApexDiscoveryError(\n `Apex \"${info.ilpAddress}\" announced an empty btpEndpoint — cannot open a BTP session.`\n );\n }\n\n const negotiation: ApexNegotiationConfig = {\n destination: info.ilpAddress,\n peerId: info.ilpAddress.split('.').at(-1) ?? info.ilpAddress,\n chain: family,\n chainKey,\n // EVM chainKeys are `evm:<network>:<chainId>`; non-EVM carry no numeric id.\n // Tolerate the 2-part `evm:<chainId>` form some connectors advertise.\n chainId:\n family === 'evm'\n ? Number(chainKey.split(':')[2] ?? chainKey.split(':')[1] ?? 0)\n : 0,\n settlementAddress,\n ...(info.preferredTokens?.[chainKey]\n ? { tokenAddress: info.preferredTokens[chainKey] }\n : {}),\n ...(info.tokenNetworks?.[chainKey]\n ? { tokenNetwork: info.tokenNetworks[chainKey] }\n : {}),\n };\n\n return {\n btpUrl,\n negotiation,\n ...(opts.childPeers && opts.childPeers.length > 0\n ? { apexChildPeers: opts.childPeers }\n : {}),\n };\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((r) => setTimeout(r, ms));\n}\n","/**\n * Fastify route registration for the `toon-clientd` control API. Each route\n * is a thin adapter: parse/validate the request, call the `ClientRunner`, map\n * errors to the uniform `ErrorResponse` envelope.\n *\n * Bound to loopback only by the daemon entry — there is no auth layer because\n * the surface never leaves `127.0.0.1`.\n */\n\nimport type { FastifyInstance, FastifyReply } from 'fastify';\nimport type { NostrEvent } from 'nostr-tools/pure';\nimport type { ClientRunner } from './client-runner.js';\nimport {\n BalancesUnavailableError,\n InvalidPayloadError,\n NotReadyError,\n PublishRejectedError,\n TargetError,\n} from './client-runner.js';\nimport { ApexDiscoveryError } from './apex-discovery.js';\nimport {\n GitError,\n NonFastForwardError,\n OversizeObjectsError,\n} from '@toon-protocol/rig';\nimport type {\n AddApexRequest,\n AddRelayRequest,\n EventsQuery,\n FundWalletRequest,\n GitCommentRequest,\n GitEstimateRequest,\n GitIssueRequest,\n GitPatchRequest,\n GitPushRequest,\n GitStatusRequest,\n HttpFetchPaidRequest,\n ChannelDepositRequest,\n CloseChannelRequest,\n SettleChannelRequest,\n OpenChannelRequest,\n PublishRequest,\n PublishUnsignedRequest,\n QueryRequest,\n RemoveApexRequest,\n RemoveRelayRequest,\n SettlementChain,\n SubscribeRequest,\n SwapRequest,\n UploadMediaRequest,\n} from '../control-api.js';\n\nexport function registerRoutes(\n app: FastifyInstance,\n runner: ClientRunner\n): void {\n app.get('/status', async () => runner.getStatus());\n\n app.post<{ Body: PublishRequest }>('/publish', async (req, reply) => {\n const body = req.body;\n if (!body || !isSignedEvent(body.event)) {\n return sendError(reply, 400, 'invalid_event', {\n detail: 'body.event must be a fully-signed Nostr event (id + sig).',\n });\n }\n try {\n return await runner.publish(body);\n } catch (err) {\n return mapError(reply, err);\n }\n });\n\n app.post<{ Body: PublishUnsignedRequest }>(\n '/publish-unsigned',\n async (req, reply) => {\n const body = req.body;\n if (!body || !Number.isInteger(body.kind)) {\n return sendError(reply, 400, 'invalid_event', {\n detail: 'body.kind (integer) is required; the daemon signs the event.',\n });\n }\n try {\n return await runner.publishUnsigned(body);\n } catch (err) {\n return mapError(reply, err);\n }\n }\n );\n\n app.post<{ Body: UploadMediaRequest }>('/upload-media', async (req, reply) => {\n const body = req.body;\n const hasData = typeof body?.dataBase64 === 'string' && body.dataBase64 !== '';\n const hasPath = typeof body?.filePath === 'string' && body.filePath !== '';\n if (!body || (!hasData && !hasPath)) {\n return sendError(reply, 400, 'invalid_media', {\n detail:\n 'body.dataBase64 (base64-encoded media bytes) or body.filePath (absolute path) is required.',\n });\n }\n try {\n return await runner.uploadMedia(body);\n } catch (err) {\n return mapError(reply, err);\n }\n });\n\n app.post<{ Body: QueryRequest }>('/query', async (req, reply) => {\n const body = req.body;\n if (!body || body.filters === undefined) {\n return sendError(reply, 400, 'invalid_filters', {\n detail: 'body.filters is required (a NIP-01 filter or array of filters).',\n });\n }\n try {\n const events = await runner.query(body.filters, body.timeoutMs);\n return { events };\n } catch (err) {\n return mapError(reply, err);\n }\n });\n\n app.post<{ Body: SubscribeRequest }>('/subscribe', async (req, reply) => {\n const body = req.body;\n if (!body || body.filters === undefined) {\n return sendError(reply, 400, 'invalid_filters', {\n detail:\n 'body.filters is required (a NIP-01 filter or array of filters).',\n });\n }\n try {\n return runner.subscribe(body);\n } catch (err) {\n return mapError(reply, err);\n }\n });\n\n app.get<{\n Querystring: {\n subId?: string;\n cursor?: string;\n limit?: string;\n relayUrl?: string;\n };\n }>('/events', async (req) => {\n const q = req.query;\n const query: EventsQuery = {};\n if (q.subId) query.subId = q.subId;\n if (q.cursor !== undefined) query.cursor = Number(q.cursor);\n if (q.limit !== undefined) query.limit = Number(q.limit);\n if (q.relayUrl) query.relayUrl = q.relayUrl;\n return runner.getEvents(query);\n });\n\n app.post<{ Body: OpenChannelRequest }>('/channels', async (req, reply) => {\n try {\n return await runner.openChannel(req.body?.destination);\n } catch (err) {\n return mapError(reply, err);\n }\n });\n\n app.get('/channels', async () => runner.getChannels());\n\n app.get('/balances', async (_req, reply) => {\n try {\n return await runner.getBalances();\n } catch (err) {\n return mapError(reply, err);\n }\n });\n\n app.post<{ Body: ChannelDepositRequest }>('/channels/deposit', async (req, reply) => {\n try {\n return await runner.depositToChannel(req.body);\n } catch (err) {\n return mapError(reply, err);\n }\n });\n\n app.post<{ Body: CloseChannelRequest }>('/channels/close', async (req, reply) => {\n try {\n return await runner.closeChannel(req.body);\n } catch (err) {\n return mapError(reply, err);\n }\n });\n\n app.post<{ Body: SettleChannelRequest }>('/channels/settle', async (req, reply) => {\n try {\n return await runner.settleChannel(req.body);\n } catch (err) {\n return mapError(reply, err);\n }\n });\n\n app.post<{ Body: SwapRequest }>('/swap', async (req, reply) => {\n const body = req.body;\n if (\n !body ||\n !body.destination ||\n body.amount === undefined ||\n !body.swapPubkey ||\n !body.pair ||\n !body.chainRecipient\n ) {\n return sendError(reply, 400, 'invalid_swap', {\n detail:\n 'body.destination, amount, swapPubkey, pair, and chainRecipient are required.',\n });\n }\n try {\n return await runner.swap(body);\n } catch (err) {\n return mapError(reply, err);\n }\n });\n\n app.post<{ Body: HttpFetchPaidRequest }>(\n '/http-fetch-paid',\n async (req, reply) => {\n const body = req.body;\n if (!body || typeof body.url !== 'string' || body.url === '') {\n return sendError(reply, 400, 'invalid_url', {\n detail: 'body.url (absolute resource URL) is required.',\n });\n }\n try {\n return await runner.httpFetchPaid(body);\n } catch (err) {\n return mapError(reply, err);\n }\n }\n );\n\n app.post<{ Body: FundWalletRequest }>('/fund-wallet', async (req, reply) => {\n try {\n // Returns immediately with a 'pending' snapshot — the drip runs async in\n // the daemon (the Mina faucet outlasts the host's tool-call timeout).\n return runner.fundWallet(req.body ?? {});\n } catch (err) {\n return mapError(reply, err);\n }\n });\n\n app.get<{ Querystring: { chain?: SettlementChain } }>(\n '/fund-wallet/status',\n async (req) => runner.getFundStatus(req.query?.chain)\n );\n\n app.get('/targets', async () => runner.getTargets());\n\n app.post<{ Body: AddRelayRequest }>('/relays', async (req, reply) => {\n const url = req.body?.relayUrl;\n if (!url) {\n return sendError(reply, 400, 'invalid_relay', {\n detail: 'body.relayUrl is required.',\n });\n }\n try {\n await runner.addRelay(url);\n return runner.getTargets();\n } catch (err) {\n return mapError(reply, err);\n }\n });\n\n app.delete<{ Body: RemoveRelayRequest }>('/relays', async (req, reply) => {\n const url = req.body?.relayUrl;\n if (!url) {\n return sendError(reply, 400, 'invalid_relay', {\n detail: 'body.relayUrl is required.',\n });\n }\n try {\n runner.removeRelay(url);\n return runner.getTargets();\n } catch (err) {\n return mapError(reply, err);\n }\n });\n\n app.post<{ Body: AddApexRequest }>('/apex', async (req, reply) => {\n const body = req.body;\n if (!body || !body.ilpAddress || !body.relayUrl) {\n return sendError(reply, 400, 'invalid_apex', {\n detail: 'body.ilpAddress and body.relayUrl are required.',\n });\n }\n try {\n return await runner.addApex(body);\n } catch (err) {\n return mapError(reply, err);\n }\n });\n\n app.delete<{ Body: RemoveApexRequest }>('/apex', async (req, reply) => {\n const url = req.body?.btpUrl;\n if (!url) {\n return sendError(reply, 400, 'invalid_apex', {\n detail: 'body.btpUrl is required.',\n });\n }\n try {\n await runner.removeApex(url);\n return runner.getTargets();\n } catch (err) {\n return mapError(reply, err);\n }\n });\n\n // ── Git write path (/git/*, epic #222 ticket #227) ─────────────────────────\n\n app.post<{ Body: GitEstimateRequest }>('/git/estimate', async (req, reply) => {\n const body = req.body;\n if (!body || !isNonEmptyString(body.repoPath) || !isNonEmptyString(body.repoId)) {\n return sendError(reply, 400, 'invalid_git_request', {\n detail:\n 'body.repoPath (local repository path) and body.repoId are required.',\n });\n }\n try {\n return await runner.gitEstimate(body);\n } catch (err) {\n return mapGitError(reply, err);\n }\n });\n\n app.post<{ Body: GitPushRequest }>('/git/push', async (req, reply) => {\n const body = req.body;\n if (!body || !isNonEmptyString(body.repoPath) || !isNonEmptyString(body.repoId)) {\n return sendError(reply, 400, 'invalid_git_request', {\n detail:\n 'body.repoPath (local repository path) and body.repoId are required.',\n });\n }\n try {\n return await runner.gitPush(body);\n } catch (err) {\n return mapGitError(reply, err);\n }\n });\n\n app.post<{ Body: GitIssueRequest }>('/git/issue', async (req, reply) => {\n const body = req.body;\n if (!body || !body.repoAddr || !isNonEmptyString(body.title) || !isNonEmptyString(body.body)) {\n return sendError(reply, 400, 'invalid_git_request', {\n detail:\n 'body.repoAddr ({ ownerPubkey, repoId }), body.title, and body.body are required.',\n });\n }\n try {\n return await runner.gitIssue(body);\n } catch (err) {\n return mapGitError(reply, err);\n }\n });\n\n app.post<{ Body: GitCommentRequest }>('/git/comment', async (req, reply) => {\n const body = req.body;\n if (!body || !body.repoAddr || !isNonEmptyString(body.rootEventId) || !isNonEmptyString(body.body)) {\n return sendError(reply, 400, 'invalid_git_request', {\n detail:\n 'body.repoAddr ({ ownerPubkey, repoId }), body.rootEventId, and body.body are required.',\n });\n }\n try {\n return await runner.gitComment(body);\n } catch (err) {\n return mapGitError(reply, err);\n }\n });\n\n app.post<{ Body: GitPatchRequest }>('/git/patch', async (req, reply) => {\n const body = req.body;\n if (!body || !body.repoAddr || !isNonEmptyString(body.title)) {\n return sendError(reply, 400, 'invalid_git_request', {\n detail:\n 'body.repoAddr ({ ownerPubkey, repoId }) and body.title are required ' +\n '(plus exactly one of patchText | repoPath+range).',\n });\n }\n try {\n return await runner.gitPatch(body);\n } catch (err) {\n return mapGitError(reply, err);\n }\n });\n\n app.post<{ Body: GitStatusRequest }>('/git/status', async (req, reply) => {\n const body = req.body;\n if (!body || !body.repoAddr || !isNonEmptyString(body.targetEventId) || !isNonEmptyString(body.status)) {\n return sendError(reply, 400, 'invalid_git_request', {\n detail:\n 'body.repoAddr ({ ownerPubkey, repoId }), body.targetEventId, and ' +\n 'body.status (open | applied | closed | draft) are required.',\n });\n }\n try {\n return await runner.gitStatus(body);\n } catch (err) {\n return mapGitError(reply, err);\n }\n });\n}\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === 'string' && value !== '';\n}\n\n/**\n * Map `/git/*` errors: planPush's structured errors surface as clean JSON\n * with their data attached (a confirm UI / CLI renders the refs or the\n * oversize table directly), a git plumbing failure (not a repo, bad range,\n * vanished objects) is caller-fixable 400, and everything else falls through\n * to the standard mapper (bootstrapping 503, funding 402, ...).\n */\nfunction mapGitError(reply: FastifyReply, err: unknown): FastifyReply {\n if (err instanceof NonFastForwardError) {\n // 409 Conflict: the remote moved — re-run with force or pull first.\n return reply.status(409).send({\n error: 'non_fast_forward',\n detail: err.message,\n refs: err.refs,\n });\n }\n if (err instanceof OversizeObjectsError) {\n // 413 Payload Too Large: objects over the 95KB v1 upload limit.\n return reply.status(413).send({\n error: 'oversize_objects',\n detail: err.message,\n objects: err.objects,\n });\n }\n if (err instanceof GitError) {\n return sendError(reply, 400, 'git_error', { detail: err.message });\n }\n return mapError(reply, err);\n}\n\nfunction isSignedEvent(event: unknown): event is NostrEvent {\n if (typeof event !== 'object' || event === null) return false;\n const e = event as Record<string, unknown>;\n return (\n typeof e['id'] === 'string' &&\n typeof e['sig'] === 'string' &&\n typeof e['pubkey'] === 'string' &&\n typeof e['kind'] === 'number'\n );\n}\n\nfunction mapError(reply: FastifyReply, err: unknown): FastifyReply {\n if (err instanceof NotReadyError) {\n return sendError(reply, 503, 'bootstrapping', {\n detail: err.message,\n retryable: true,\n });\n }\n if (err instanceof InvalidPayloadError) {\n return sendError(reply, 400, 'invalid_payload', { detail: err.message });\n }\n if (err instanceof PublishRejectedError) {\n return sendError(reply, 502, 'rejected', { detail: err.message });\n }\n if (err instanceof BalancesUnavailableError) {\n // The chain RPC/provider behind the balances handler stalled — retryable,\n // and attributed to the balances handler, NOT the relay/apex (#199).\n return sendError(reply, 504, 'balances_unavailable', {\n detail: err.message,\n retryable: true,\n });\n }\n if (err instanceof TargetError) {\n // 404 for \"no such target\", 400 otherwise — both are caller-fixable.\n const status = /no such/i.test(err.message) ? 404 : 400;\n return sendError(reply, status, 'invalid_target', { detail: err.message });\n }\n if (err instanceof ApexDiscoveryError) {\n // A timeout is a retryable 504 (apex may be slow/offline); a malformed\n // announcement is a non-retryable 502 (the apex must republish).\n return err.retryable\n ? sendError(reply, 504, 'discovery_timeout', {\n detail: err.message,\n retryable: true,\n })\n : sendError(reply, 502, 'discovery_failed', { detail: err.message });\n }\n // Settle called before the grace period elapsed — retryable (the UI polls\n // until `now >= settleableAt`). The client throws a tagged error; 425 Too Early.\n if (err instanceof Error && (err as { name?: string }).name === 'SettleTooEarlyError') {\n return sendError(reply, 425, 'settle_too_early', { detail: err.message, retryable: true });\n }\n // First-write channel OPEN reverted because the settlement wallet has no\n // native gas (toon-meta#65). The client throws a tagged `ChannelFundingError`\n // with an actionable \"fund the wallet\" message; on the upload path it is\n // wrapped in a `ToonClientError('Failed to publish event')`, so walk the\n // `cause` chain. 402 Payment Required — caller-fixable + retryable once funded.\n const funding = findChannelFundingError(err);\n if (funding) {\n return sendError(reply, 402, 'insufficient_gas', {\n detail: funding.message,\n retryable: true,\n });\n }\n return sendError(reply, 500, 'internal_error', {\n detail: err instanceof Error ? err.message : String(err),\n });\n}\n\n/**\n * Walk an error's `cause` chain and return the first `ChannelFundingError`\n * (matched by name to avoid a hard import of the client package). The client\n * tags the gas-revert error and `publishEvent` wraps it one level deep on the\n * upload path, so the actionable message can be nested.\n */\nfunction findChannelFundingError(err: unknown): Error | undefined {\n let cur: unknown = err;\n for (let i = 0; i < 10 && cur != null; i++) {\n if (cur instanceof Error && (cur as { name?: string }).name === 'ChannelFundingError') {\n return cur;\n }\n cur = cur instanceof Error ? (cur as { cause?: unknown }).cause : undefined;\n }\n return undefined;\n}\n\nfunction sendError(\n reply: FastifyReply,\n status: number,\n error: string,\n extra: { detail?: string; retryable?: boolean } = {}\n): FastifyReply {\n return reply.status(status).send({\n error,\n ...(extra.detail ? { detail: extra.detail } : {}),\n ...(extra.retryable ? { retryable: true } : {}),\n });\n}\n"],"mappings":"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","names":["delay","generateSecretKey","REPOSITORY_ANNOUNCEMENT_KIND","REPOSITORY_ANNOUNCEMENT_KIND","key","defaultWebSocketFactory","resolve","REPOSITORY_ANNOUNCEMENT_KIND","execFile","spawn","promisify","resolve","execFileAsync","promisify","execFile","stat","__require","getPublicKey","getPublicKey","key","RATE_REGEX","noop","NOOP_LOGGER","getPublicKey","__require","mkdirSync","writeFileSync","dirname","mkdirSync","readFileSync","writeFileSync","dirname","join","join","readFileSync","mkdirSync","dirname","writeFileSync","delay","errMsg","generateSecretKey","serializePushPlan","serializePushResult","serializeFeeEstimate","resolve","key"]}
|