@toon-protocol/rig 2.0.1 → 2.2.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/cli/standalone-mode.ts","../src/standalone/topology-cache.ts","../src/standalone/network-bootstrap.ts"],"sourcesContent":["/**\n * The embedded-client (standalone) publisher backing every paid `rig`\n * command: build a nonce-guarded {@link StandalonePublisher} from the\n * caller's own identity and config.\n *\n * Identity comes from the #248 precedence chain (`./identity.ts`:\n * RIG_MNEMONIC env → TOON_CLIENT_MNEMONIC env alias → project `.env` →\n * `~/.toon-client` keystore/config). The remaining config resolution\n * DUPLICATES the toon-clientd conventions\n * (`packages/client-mcp/src/daemon/config.ts`) the same way\n * `../standalone/nonce-guard.ts` does — this package must not import\n * `@toon-protocol/client-mcp` (circular; see that module's doc). Keep in sync:\n *\n * - state dir: `TOON_CLIENT_HOME`, else `~/.toon-client`; config `config.json`\n * - env overrides: `TOON_CLIENT_PROXY_URL`, `TOON_CLIENT_BTP_URL`,\n * `TOON_CLIENT_RELAY_URL`, `TOON_CLIENT_DESTINATION`,\n * `TOON_CLIENT_PUBLISH_DESTINATION`, `TOON_CLIENT_STORE_DESTINATION`,\n * `TOON_CLIENT_CHAIN`\n *\n * NETWORK BOOTSTRAP (#264): what explicit config does not pin is resolved\n * from the network itself — the payment peer's live kind:10032 announce on\n * the relay-origin, falling back to `@toon-protocol/core`'s committed\n * genesis peer seed. Uplink, channel anchor, publish/store routes, the\n * settlement chain and its TokenNetwork/token/RPC parameters all follow the\n * `explicit config > live announce > genesis seed` order documented in\n * `../standalone/network-bootstrap.ts`. The pure resolution lives in\n * {@link resolveNetworkTopology} (unit-testable without any network).\n *\n * TOPOLOGY CACHE (#279): the resolved topology is persisted under\n * `TOON_CLIENT_HOME` (`../standalone/topology-cache.ts`) keyed by\n * relay-origin + identity + an explicit-config fingerprint, with a 15-min\n * TTL (`RIG_TOPOLOGY_TTL_MS` overrides; `0` disables). A cache hit skips\n * announce discovery and the funded-chain probes entirely; a cached\n * topology that then fails to BOOTSTRAP is invalidated and re-resolved\n * live in-process ({@link TopologyRecoveringPublisher}), so staleness costs\n * one failed attempt, never a broken run. Only paid-path resolutions\n * (`requireUplink !== false`) are written — a free-read topology may lack\n * an uplink and must not shadow the paid path's MissingUplinkError.\n *\n * CORE COEXISTENCE NOTE: rig performs discovery with ITS OWN\n * `@toon-protocol/core` (^2.0.x — live genesis seed), while the embedded\n * `@toon-protocol/client` keeps its internal core (^1.6.x) for its own\n * bootstrap/negotiation. The two never exchange class instances — rig feeds\n * the client plain config (`knownPeers`, settlement maps), so the version\n * split is safe by construction.\n *\n * This module statically imports `@toon-protocol/client` (heavy: viem,\n * noble, nostr-tools), so it must only ever be reached through the dynamic\n * import in `push.ts` (see `./standalone-context.ts`).\n */\n\nimport { readFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport type { ToonClientConfig } from '@toon-protocol/client';\nimport { EvmSigner, deriveNostrKeyFromMnemonic } from '@toon-protocol/client';\nimport {\n decodeEventFromToon,\n encodeEventToToon,\n} from '@toon-protocol/core';\nimport type {\n FeeRates,\n GitObjectUpload,\n Publisher,\n PublishReceipt,\n UploadReceipt,\n} from '../publisher.js';\nimport type { UnsignedEvent } from '../nip34-events.js';\nimport {\n ChannelMapStore,\n RIG_CHANNEL_MAP_FILENAME,\n type ChannelMapRecord,\n} from '../standalone/channel-map.js';\nimport {\n TOPOLOGY_CACHE_FILENAME,\n TOPOLOGY_TTL_ENV,\n TopologyCache,\n explicitConfigFingerprint,\n topologyCacheKey,\n topologyCacheTtlMs,\n} from '../standalone/topology-cache.js';\nimport {\n DISCOVERY_TIMEOUT_MS,\n TokenNetworkUnderivableError,\n discoverAnnouncedPeers,\n evmTokenBalance,\n genesisSeedPubkeys,\n loadGenesisSeed,\n pickPaymentPeer,\n resolveChainSettlement,\n selectSettlementChain,\n type AnnouncedPeer,\n type ChainSelection,\n type ChannelRecordLike,\n type EvmBalanceProbe,\n type ExplicitChainConfig,\n} from '../standalone/network-bootstrap.js';\nimport { StandalonePublisher } from '../standalone/standalone-publisher.js';\nimport { fetchRemoteState } from '../remote-state.js';\nimport { resolveIdentity } from './identity.js';\nimport type {\n StandaloneContext,\n StandaloneLoadOptions,\n} from './standalone-context.js';\n\n/** The subset of the shared client config file standalone mode consumes. */\nexport interface ClientConfigFile {\n network?: 'mainnet' | 'testnet' | 'devnet' | 'custom';\n mnemonicAccountIndex?: number;\n btpUrl?: string;\n proxyUrl?: string;\n relayUrl?: string;\n destination?: string;\n publishDestination?: string;\n storeDestination?: string;\n feePerEvent?: string;\n channelStorePath?: string;\n /** Settlement chain: family (`evm`) or full id (`evm:31337`). */\n chain?: string;\n supportedChains?: string[];\n settlementAddresses?: Record<string, string>;\n preferredTokens?: Record<string, string>;\n tokenNetworks?: Record<string, string>;\n chainRpcUrls?: Record<string, string>;\n solanaChannel?: ToonClientConfig['solanaChannel'];\n minaChannel?: ToonClientConfig['minaChannel'];\n}\n\n/** An identity was resolved, but there is no way to send paid writes. */\nexport class MissingUplinkError extends Error {\n constructor(configPath: string, relayUrl: string | undefined) {\n const discovered = relayUrl\n ? `no announce with a btp/http endpoint was found on ${relayUrl} and ` +\n 'the genesis seed has none; '\n : '';\n super(\n `no write uplink configured: ${discovered}set TOON_CLIENT_PROXY_URL ` +\n '(connector payment proxy) or TOON_CLIENT_BTP_URL, or add ' +\n `proxyUrl/btpUrl to ${configPath}`\n );\n this.name = 'MissingUplinkError';\n }\n}\n\nfunction configDir(env: NodeJS.ProcessEnv): string {\n return env['TOON_CLIENT_HOME'] ?? join(homedir(), '.toon-client');\n}\n\nfunction readClientConfig(path: string): ClientConfigFile {\n try {\n return JSON.parse(readFileSync(path, 'utf8')) as ClientConfigFile;\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return {};\n throw new Error(\n `failed to read client config at ${path}: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n}\n\n/** `https://host/ilp` → `https://host` (the client re-derives `/ilp`). */\nfunction proxyBaseOf(httpEndpoint: string): string {\n return httpEndpoint.replace(/\\/+$/, '').replace(/\\/ilp$/i, '');\n}\n\n// ---------------------------------------------------------------------------\n// Pure topology resolution (#264) — exported for tests\n// ---------------------------------------------------------------------------\n\n/** A genesis-seed peer as this module consumes it (rig's core 2.x shape). */\nexport interface GenesisSeedLike {\n pubkey: string;\n relayUrl: string;\n ilpAddress: string;\n btpEndpoint: string;\n}\n\n/** Inputs to {@link resolveNetworkTopology} (side-effect free). */\nexport interface NetworkTopologyInputs {\n env: NodeJS.ProcessEnv;\n file: ClientConfigFile;\n /** For error messages (\"add X to <configPath>\"). */\n configPath: string;\n /** Resolved relay-origin (already precedence-resolved). */\n relayUrl: string;\n /** The discovered payment-peer announce, if any. */\n announce: AnnouncedPeer | undefined;\n /** The committed genesis seed entry, if any. */\n genesisSeed: GenesisSeedLike | undefined;\n identity: { mnemonic: string; accountIndex: number; pubkey: string };\n /**\n * #262 channel-map records for this identity (chain selection input).\n * A thunk so the (possibly corrupt-throwing) map read only happens when\n * chain selection actually needs it.\n */\n channelRecords: () => ChannelRecordLike[];\n /** Balance probe override (tests); default: raw `eth_call`. */\n probeBalance?: EvmBalanceProbe;\n /**\n * When false (#263 free reads, e.g. `rig balance`), a missing uplink is\n * tolerated instead of throwing {@link MissingUplinkError}.\n */\n requireUplink?: boolean;\n warn: (line: string) => void;\n}\n\n/** The resolved payment topology feeding the embedded client config. */\nexport interface NetworkTopology {\n proxyUrl?: string;\n btpUrl?: string;\n /** Channel anchor / default ILP destination. */\n destination: string;\n publishDestination?: string;\n storeDestination?: string;\n /** The peer the embedded client bootstraps + negotiates with. */\n knownPeers: { pubkey: string; relayUrl: string; btpEndpoint: string }[];\n /** The selected settlement chain + rationale (absent: nothing known). */\n selection?: ChainSelection;\n supportedChains?: string[];\n preferredTokens?: Record<string, string>;\n tokenNetworks?: Record<string, string>;\n chainRpcUrls?: Record<string, string>;\n}\n\n/**\n * Resolve the payment topology per the #264 precedence order —\n * `explicit config > live announce > genesis seed` for every field, plus the\n * documented settlement-chain selection rule (see\n * `../standalone/network-bootstrap.ts`).\n *\n * @throws {MissingUplinkError} when no source yields an uplink.\n * @throws {TokenNetworkUnderivableError} when the selected EVM chain's\n * TokenNetwork cannot be derived from config, announce, or chain preset.\n */\nexport async function resolveNetworkTopology(\n inputs: NetworkTopologyInputs\n): Promise<NetworkTopology> {\n const { env, file, configPath, relayUrl, announce, genesisSeed, warn } =\n inputs;\n\n // ── Explicit config (always wins, per field) ─────────────────────────────\n const explicitProxyUrl = env['TOON_CLIENT_PROXY_URL'] ?? file.proxyUrl;\n const explicitBtpUrl = env['TOON_CLIENT_BTP_URL'] ?? file.btpUrl;\n const explicitDestination =\n env['TOON_CLIENT_DESTINATION'] ?? file.destination;\n const explicitPublish =\n env['TOON_CLIENT_PUBLISH_DESTINATION'] ?? file.publishDestination;\n const explicitStore =\n env['TOON_CLIENT_STORE_DESTINATION'] ?? file.storeDestination;\n const explicitChain = env['TOON_CLIENT_CHAIN'] ?? file.chain;\n const explicitMaps: ExplicitChainConfig = {\n ...(file.chainRpcUrls ? { chainRpcUrls: file.chainRpcUrls } : {}),\n ...(file.preferredTokens ? { preferredTokens: file.preferredTokens } : {}),\n ...(file.tokenNetworks ? { tokenNetworks: file.tokenNetworks } : {}),\n };\n\n // ── Uplink: explicit > announce (http > btp) > genesis seed ──────────────\n let proxyUrl = explicitProxyUrl;\n let btpUrl = explicitBtpUrl;\n if (!proxyUrl && !btpUrl) {\n if (announce?.info.httpEndpoint) {\n proxyUrl = proxyBaseOf(announce.info.httpEndpoint);\n } else if (announce?.info.btpEndpoint) {\n btpUrl = announce.info.btpEndpoint;\n } else if (genesisSeed?.btpEndpoint) {\n btpUrl = genesisSeed.btpEndpoint;\n } else if (inputs.requireUplink !== false) {\n // Free reads (`rig balance`, #263) tolerate a missing uplink; paid\n // commands fail here, after every source has been tried.\n throw new MissingUplinkError(configPath, relayUrl);\n }\n }\n\n // ── Destination anchor + publish/store routes ────────────────────────────\n // The channel anchors at the peer's announced ilpAddress; publish/store\n // routes come from the announce's `routes` map. Explicit values always\n // win; with neither, the publisher's `<base>.relay.store` anchor-derivation\n // convention remains as the last-resort fallback (explicit anchors only).\n const destination =\n explicitDestination ??\n announce?.info.ilpAddress ??\n genesisSeed?.ilpAddress ??\n 'g.proxy';\n const publishDestination = explicitPublish ?? announce?.routes?.publish;\n const storeDestination = explicitStore ?? announce?.routes?.store;\n\n // ── Known peer for the embedded client's own bootstrap ───────────────────\n // The client re-queries the peer's announce itself (its internal core) and\n // negotiates the settlement chain; rig just tells it WHO to bootstrap with.\n const knownPeers = announce\n ? [\n {\n pubkey: announce.pubkey,\n relayUrl,\n btpEndpoint: announce.info.btpEndpoint ?? '',\n },\n ]\n : genesisSeed\n ? [\n {\n pubkey: genesisSeed.pubkey,\n relayUrl: genesisSeed.relayUrl,\n btpEndpoint: genesisSeed.btpEndpoint,\n },\n ]\n : [];\n\n // ── Settlement chain + per-chain parameters ──────────────────────────────\n // NOTE: the `network` preset field is deliberately NOT forwarded to the\n // embedded client. `applyNetworkPresets` puts preset chains FIRST in\n // `supportedChains`, which is what steered devnet negotiation to the\n // unfunded public Solana preset (#260 root cause 4). The announce + the\n // rule below define the chain; presets only serve as per-chain parameter\n // fallbacks inside `resolveChainSettlement`.\n const announcedChains = announce?.info.supportedChains ?? [];\n const resolveSettlement = (chain: string) =>\n resolveChainSettlement(chain, explicitMaps, announce);\n\n let selection: ChainSelection | undefined;\n let supportedChains: string[] | undefined;\n let preferredTokens: Record<string, string> | undefined;\n let tokenNetworks: Record<string, string> | undefined;\n let chainRpcUrls: Record<string, string> | undefined;\n\n if (file.supportedChains?.length) {\n // Explicit chain list: pass through as-is (its order IS the negotiation\n // preference), filling per-chain parameter gaps from announce/presets.\n supportedChains = file.supportedChains;\n preferredTokens = { ...file.preferredTokens };\n tokenNetworks = { ...file.tokenNetworks };\n chainRpcUrls = { ...file.chainRpcUrls };\n for (const chain of supportedChains) {\n const s = resolveSettlement(chain);\n if (s.tokenAddress && !preferredTokens[chain]) {\n preferredTokens[chain] = s.tokenAddress;\n }\n if (s.tokenNetwork && !tokenNetworks[chain]) {\n tokenNetworks[chain] = s.tokenNetwork;\n }\n if (s.rpcUrl && !chainRpcUrls[chain]) {\n chainRpcUrls[chain] = s.rpcUrl;\n }\n // Same fail-fast guarantee as the announce/selection path below: an\n // explicitly configured EVM chain whose TokenNetwork/RPC cannot be\n // derived must fail HERE with an actionable error, not later as the\n // embedded client's generic \"tokenNetwork address is required\".\n if (s.family === 'evm') {\n if (!tokenNetworks[chain]) {\n throw new TokenNetworkUnderivableError(chain, announce, relayUrl);\n }\n if (!chainRpcUrls[chain]) {\n throw new Error(\n `no RPC URL is derivable for settlement chain \"${chain}\"` +\n ` — add chainRpcUrls[\"${chain}\"] to ${configPath}`\n );\n }\n }\n }\n selection = {\n chain: supportedChains[0] as string,\n reason: 'explicit',\n detail: 'supportedChains set by config',\n };\n } else if (explicitChain || announcedChains.length > 0) {\n // Selection rule: explicit > persisted channel > funded > first EVM.\n const { secretKey } = deriveNostrKeyFromMnemonic(\n inputs.identity.mnemonic,\n inputs.identity.accountIndex\n );\n const evmAddress = new EvmSigner(secretKey).address;\n selection = await selectSettlementChain({\n ...(explicitChain ? { explicitChain } : {}),\n announcedChains,\n records: inputs.channelRecords(),\n evmAddress,\n resolveSettlement,\n probeBalance: inputs.probeBalance ?? evmTokenBalance,\n });\n const settlement = resolveSettlement(selection.chain);\n if (settlement.family === 'evm') {\n if (!settlement.tokenNetwork) {\n throw new TokenNetworkUnderivableError(\n selection.chain,\n announce,\n relayUrl\n );\n }\n if (!settlement.rpcUrl) {\n throw new Error(\n `no RPC URL is derivable for settlement chain \"${selection.chain}\"` +\n ` — add chainRpcUrls[\"${selection.chain}\"] to ${configPath}`\n );\n }\n }\n supportedChains = [selection.chain];\n preferredTokens = settlement.tokenAddress\n ? { [selection.chain]: settlement.tokenAddress }\n : undefined;\n tokenNetworks = settlement.tokenNetwork\n ? { [selection.chain]: settlement.tokenNetwork }\n : undefined;\n chainRpcUrls = settlement.rpcUrl\n ? { [selection.chain]: settlement.rpcUrl }\n : undefined;\n if (selection.reason !== 'explicit') {\n warn(\n `rig: settlement chain ${selection.chain} selected — ` +\n `${selection.detail}; set TOON_CLIENT_CHAIN (or supportedChains ` +\n 'in the client config) to override'\n );\n }\n } else {\n warn(\n 'rig: no settlement chains are configured or announced — paid writes ' +\n 'will fail until a chain is configured (supportedChains) or the ' +\n `payment peer announces its chains on ${relayUrl}`\n );\n }\n\n const network = env['TOON_CLIENT_NETWORK'] ?? file.network;\n if (network && network !== 'custom' && !file.supportedChains?.length) {\n warn(\n `rig: ignoring the \"${network}\" network preset for settlement — the ` +\n \"settlement chain comes from the payment peer's announce and your \" +\n 'config, because preset chains can point at networks your wallet ' +\n 'has no funds on; set supportedChains explicitly to use preset chains'\n );\n }\n\n return {\n ...(proxyUrl ? { proxyUrl } : {}),\n ...(btpUrl ? { btpUrl } : {}),\n destination,\n ...(publishDestination ? { publishDestination } : {}),\n ...(storeDestination ? { storeDestination } : {}),\n knownPeers,\n ...(selection ? { selection } : {}),\n ...(supportedChains ? { supportedChains } : {}),\n ...(preferredTokens && Object.keys(preferredTokens).length > 0\n ? { preferredTokens }\n : {}),\n ...(tokenNetworks && Object.keys(tokenNetworks).length > 0\n ? { tokenNetworks }\n : {}),\n ...(chainRpcUrls && Object.keys(chainRpcUrls).length > 0\n ? { chainRpcUrls }\n : {}),\n };\n}\n\n// ---------------------------------------------------------------------------\n// Cached-topology recovery (#279)\n// ---------------------------------------------------------------------------\n\n/** Structural check for a cached {@link NetworkTopology} document. */\nfunction isNetworkTopology(value: unknown): value is NetworkTopology {\n if (typeof value !== 'object' || value === null) return false;\n const t = value as Record<string, unknown>;\n return typeof t['destination'] === 'string' && Array.isArray(t['knownPeers']);\n}\n\n/**\n * Start failures that must NEVER trigger a cache-invalidation retry: they\n * are concurrency/state guards, not topology staleness — retrying would\n * bypass exactly what they protect.\n */\nconst NON_RECOVERABLE_START_ERRORS: ReadonlySet<string> = new Set([\n 'DaemonIdentityConflictError',\n 'StandaloneLockError',\n 'ChannelMapCorruptError',\n]);\n\n/**\n * Publisher wrapper implementing the #279 cache-invalidation contract: when\n * the inner publisher was built from a CACHED topology and its bootstrap\n * (`start`/`startClientOnly`) fails, the cache entry is invalidated, the\n * topology is re-resolved LIVE, a fresh publisher replaces the inner one,\n * and the operation proceeds — one retry, never more. Bootstrap failures\n * are pre-payment by construction (start() completes before any claim is\n * signed), so the retry can never double-pay. Publishers built from a live\n * resolution get no recovery hook and fail through unchanged.\n */\nclass TopologyRecoveringPublisher implements Publisher {\n private inner: StandalonePublisher;\n private rebuild: (() => Promise<StandalonePublisher>) | undefined;\n private readonly warn: (line: string) => void;\n\n constructor(\n inner: StandalonePublisher,\n rebuild: (() => Promise<StandalonePublisher>) | undefined,\n warn: (line: string) => void\n ) {\n this.inner = inner;\n this.rebuild = rebuild;\n this.warn = warn;\n }\n\n getPublicKey(): string {\n return this.inner.getPublicKey();\n }\n\n getFeeRates(): Promise<FeeRates> {\n return this.inner.getFeeRates();\n }\n\n /**\n * Run the bootstrap step, recovering ONCE from a stale cached topology.\n * A rebuild failure surfaces the LIVE resolution's error — that is the\n * network's real state, strictly more actionable than the cached failure.\n */\n private async ensure(\n start: (p: StandalonePublisher) => Promise<void>\n ): Promise<StandalonePublisher> {\n try {\n await start(this.inner);\n return this.inner;\n } catch (err) {\n const rebuild = this.rebuild;\n this.rebuild = undefined;\n const name = err instanceof Error ? err.name : '';\n if (!rebuild || NON_RECOVERABLE_START_ERRORS.has(name)) throw err;\n this.warn(\n 'rig: bootstrap with the cached network topology failed ' +\n `(${err instanceof Error ? err.message : String(err)}) — ` +\n 'invalidating the cache and re-resolving live'\n );\n const fresh = await rebuild();\n try {\n // Failed starts release their own lock/state; stop() is idempotent.\n await this.inner.stop();\n } catch {\n // best-effort teardown of the abandoned publisher\n }\n this.inner = fresh;\n await start(this.inner);\n return this.inner;\n }\n }\n\n async publishEvent(\n event: UnsignedEvent,\n relayUrls: string[]\n ): Promise<PublishReceipt> {\n const p = await this.ensure((x) => x.start());\n return p.publishEvent(event, relayUrls);\n }\n\n async uploadGitObject(upload: GitObjectUpload): Promise<UploadReceipt> {\n const p = await this.ensure((x) => x.start());\n return p.uploadGitObject(upload);\n }\n\n // ── money lifecycle passthroughs (#263) — same recovery contract ─────────\n\n async openChannelExplicit(\n opts?: Parameters<StandalonePublisher['openChannelExplicit']>[0]\n ): ReturnType<StandalonePublisher['openChannelExplicit']> {\n const p = await this.ensure((x) => x.start());\n return p.openChannelExplicit(opts);\n }\n\n async closeRecordedChannel(\n record: ChannelMapRecord\n ): ReturnType<StandalonePublisher['closeRecordedChannel']> {\n const p = await this.ensure((x) => x.startClientOnly());\n return p.closeRecordedChannel(record);\n }\n\n async settleRecordedChannel(\n record: ChannelMapRecord\n ): ReturnType<StandalonePublisher['settleRecordedChannel']> {\n const p = await this.ensure((x) => x.startClientOnly());\n return p.settleRecordedChannel(record);\n }\n\n /** Free read on the unstarted client — no bootstrap, no recovery needed. */\n readWalletBalances(): ReturnType<StandalonePublisher['readWalletBalances']> {\n return this.inner.readWalletBalances();\n }\n\n async stop(): Promise<void> {\n await this.inner.stop();\n }\n}\n\n// ---------------------------------------------------------------------------\n// Context factory\n// ---------------------------------------------------------------------------\n\n/**\n * #262 channel-map records for this identity, reduced to the slice chain\n * selection consumes (`closed` folds in the claim-watermark timers).\n */\nfunction chainRecordsFor(\n map: ChannelMapStore,\n identity: string\n): ChannelRecordLike[] {\n return map\n .list()\n .filter((r) => r.identity === identity)\n .map((r) => {\n const watermark = map.readWatermark(r.channelId);\n return {\n chain: r.chain,\n lastUsedAt: r.lastUsedAt,\n closed:\n watermark?.closedAt !== undefined ||\n watermark?.settledAt !== undefined,\n };\n });\n}\n\n/**\n * Assemble an embedded-client standalone context: resolved identity + config\n * → network bootstrap (announce discovery / genesis seed) → ToonClientConfig\n * → nonce-guarded StandalonePublisher (guard + client start + channel open\n * happen lazily on the first paid call, or eagerly via the publisher's own\n * `start`).\n */\nexport async function createStandaloneContext(\n options: StandaloneLoadOptions\n): Promise<StandaloneContext> {\n const { env } = options;\n const warn = (line: string) => options.warn(line);\n const dir = configDir(env);\n const configPath = join(dir, 'config.json');\n const file = readClientConfig(configPath);\n const identity = await resolveIdentity(options);\n\n const genesisSeed = loadGenesisSeed();\n\n // ── Relay-origin ──────────────────────────────────────────────────────────\n // The relay the paid command resolved via `rig remote` (passed by the\n // command) is the user's clearest network statement; env/file follow, and\n // the genesis seed's relay is the out-of-the-box fallback.\n const relayUrl =\n options.relayUrl ??\n env['TOON_CLIENT_RELAY_URL'] ??\n file.relayUrl ??\n genesisSeed?.relayUrl ??\n 'ws://localhost:7100';\n\n // ── Peer→channel persistence (#262) ──────────────────────────────────────\n const channelStorePath = file.channelStorePath ?? join(dir, 'channels.json');\n const channelMap = new ChannelMapStore({\n mapPath: join(dir, RIG_CHANNEL_MAP_FILENAME),\n watermarkPath: channelStorePath,\n });\n\n // ── Topology cache (#279): keyed by relay-origin + identity + explicit\n // config; a hit skips discovery AND the pure-but-probing resolution below.\n const cache = new TopologyCache<NetworkTopology>({\n path: join(dir, TOPOLOGY_CACHE_FILENAME),\n ttlMs: topologyCacheTtlMs(env),\n validate: isNetworkTopology,\n });\n const cacheKey = topologyCacheKey({\n relayUrl,\n identity: identity.pubkey,\n fingerprint: explicitConfigFingerprint(\n env,\n file as Record<string, unknown>\n ),\n });\n\n const resolveLiveTopology = async (): Promise<NetworkTopology> => {\n // ── Live announce discovery ────────────────────────────────────────────\n // Skipped when explicit config already pins the whole payment topology\n // (fully-configured setups keep their zero-roundtrip start and their\n // exact pre-#264 behavior). Discovery failure is non-fatal: warn +\n // genesis seed.\n const fullyExplicit =\n Boolean(\n (env['TOON_CLIENT_PROXY_URL'] ?? file.proxyUrl) ||\n (env['TOON_CLIENT_BTP_URL'] ?? file.btpUrl)\n ) &&\n Boolean(env['TOON_CLIENT_DESTINATION'] ?? file.destination) &&\n Boolean(file.supportedChains?.length);\n let announce: AnnouncedPeer | undefined;\n if (!fullyExplicit) {\n try {\n const peers = await discoverAnnouncedPeers(relayUrl, {\n timeoutMs: DISCOVERY_TIMEOUT_MS,\n });\n announce = pickPaymentPeer(peers, genesisSeedPubkeys());\n if (!announce) {\n warn(\n `rig: no payment-peer announce (kind:10032) found on ${relayUrl} — ` +\n 'falling back to the genesis peer seed'\n );\n }\n } catch (err) {\n warn(\n `rig: announce discovery on ${relayUrl} failed ` +\n `(${err instanceof Error ? err.message : String(err)}) — falling ` +\n 'back to the genesis peer seed'\n );\n }\n }\n\n // ── Topology resolution (pure; explicit > announce > genesis) ──────────\n const resolved = await resolveNetworkTopology({\n env,\n file,\n configPath,\n relayUrl,\n announce,\n genesisSeed,\n identity: {\n mnemonic: identity.mnemonic,\n accountIndex: identity.accountIndex,\n pubkey: identity.pubkey,\n },\n channelRecords: () => chainRecordsFor(channelMap, identity.pubkey),\n ...(options.requireUplink !== undefined\n ? { requireUplink: options.requireUplink }\n : {}),\n warn,\n });\n // Cache only paid-path resolutions: a `requireUplink: false` free read\n // may resolve WITHOUT an uplink, and caching that would let a later paid\n // command skip past MissingUplinkError with a broken topology.\n if (options.requireUplink !== false) cache.write(cacheKey, resolved);\n return resolved;\n };\n\n const cached = cache.read(cacheKey);\n if (cached) {\n warn(\n `rig: network topology from cache (${Math.round(cached.ageMs / 1000)}s ` +\n `old; ${TOPOLOGY_TTL_ENV}=0 disables) — skipping announce discovery`\n );\n }\n const topology = cached?.topology ?? (await resolveLiveTopology());\n\n const eventFee = BigInt(file.feePerEvent ?? '1');\n\n const buildPublisher = (topo: NetworkTopology): StandalonePublisher => {\n const clientConfig: ToonClientConfig = {\n // validateConfig requires connectorUrl OR proxyUrl; with BTP-only\n // config a dummy connectorUrl satisfies it (unused at runtime — same\n // convention as the daemon).\n ...(topo.proxyUrl\n ? { proxyUrl: topo.proxyUrl }\n : { connectorUrl: 'http://127.0.0.1:1' }),\n mnemonic: identity.mnemonic,\n mnemonicAccountIndex: identity.accountIndex,\n ilpInfo: {\n pubkey: '00'.repeat(32),\n ilpAddress: 'g.toon.client',\n btpEndpoint: topo.btpUrl ?? '',\n assetCode: 'USD',\n assetScale: 6,\n },\n toonEncoder: encodeEventToToon,\n toonDecoder: decodeEventFromToon,\n ...(topo.btpUrl ? { btpUrl: topo.btpUrl, btpAuthToken: '' } : {}),\n destinationAddress: topo.destination,\n // The embedded client bootstraps against the known peer above; its\n // `relayUrl` config only seeds ArDrive-merged peers, so it stays unset.\n relayUrl: '',\n knownPeers: topo.knownPeers,\n channelStorePath,\n ...(topo.supportedChains\n ? { supportedChains: topo.supportedChains }\n : {}),\n ...(file.settlementAddresses\n ? { settlementAddresses: file.settlementAddresses }\n : {}),\n ...(topo.preferredTokens ? { preferredTokens: topo.preferredTokens } : {}),\n ...(topo.tokenNetworks ? { tokenNetworks: topo.tokenNetworks } : {}),\n ...(topo.chainRpcUrls ? { chainRpcUrls: topo.chainRpcUrls } : {}),\n ...(file.solanaChannel ? { solanaChannel: file.solanaChannel } : {}),\n ...(file.minaChannel ? { minaChannel: file.minaChannel } : {}),\n };\n\n return new StandalonePublisher({\n clientConfig,\n eventFee,\n channelMap,\n warn,\n ...(topo.publishDestination\n ? { publishDestination: topo.publishDestination }\n : {}),\n ...(topo.storeDestination\n ? { storeDestination: topo.storeDestination }\n : {}),\n // `rig channel open --peer` (#263): anchor the channel (and its map\n // key) to an explicit peer destination instead of the configured\n // default.\n ...(options.channelDestination\n ? { channelDestination: options.channelDestination }\n : {}),\n // The peer's announce does not carry TokenNetwork/token parameters, so\n // the client's negotiation leaves them empty (#260 root cause 3) — the\n // publisher back-fills them from the derived per-chain maps before the\n // channel opens.\n ...(topo.tokenNetworks || topo.preferredTokens\n ? {\n negotiationFallbacks: {\n ...(topo.tokenNetworks\n ? { tokenNetworks: topo.tokenNetworks }\n : {}),\n ...(topo.preferredTokens\n ? { preferredTokens: topo.preferredTokens }\n : {}),\n },\n }\n : {}),\n });\n };\n\n // Cache-sourced publishers get the #279 recovery hook: a failed bootstrap\n // invalidates the entry, re-resolves live (which re-writes the cache), and\n // retries once. Live-resolved publishers fail through unchanged.\n const publisher = new TopologyRecoveringPublisher(\n buildPublisher(topology),\n cached\n ? async () => {\n cache.invalidate(cacheKey);\n return buildPublisher(await resolveLiveTopology());\n }\n : undefined,\n warn\n );\n\n return {\n ownerPubkey: publisher.getPublicKey(),\n identitySource: identity.source,\n identitySourceLabel: identity.sourceLabel,\n publisher,\n defaultRelayUrls: [relayUrl],\n fetchRemote: (args) => fetchRemoteState(args),\n // Money lifecycle (#263): same guard/start/channel-map machinery as the\n // paid-write path, surfaced for fund/balance/channel open|close|settle.\n money: {\n openChannel: (opts) => publisher.openChannelExplicit(opts),\n closeChannel: (record) => publisher.closeRecordedChannel(record),\n settleChannel: (record) => publisher.settleRecordedChannel(record),\n walletBalances: () => publisher.readWalletBalances(),\n },\n stop: () => publisher.stop(),\n };\n}\n","/**\n * Standalone network-topology cache (#279): persist the #264 bootstrap\n * result — the announce-derived uplink, ILP routes, settlement chain and its\n * TokenNetwork/token/RPC parameters (or the genesis-seed fallback) — so\n * back-to-back CLI invocations skip the kind:10032 relay query, the\n * payment-peer pick, and the funded-chain `eth_call` probes.\n *\n * SAFETY MODEL — the cache holds only DISCOVERY results, never money state:\n *\n * - The claim watermark (nonce/cumulative) lives in the client's\n * `channels.json` and the #262 channel map, untouched by this cache;\n * every paid write still resumes from the persisted cumulative.\n * - Entries are keyed by relay-origin + identity pubkey + a fingerprint of\n * every explicit env/config field that feeds topology resolution, so a\n * changed relay, identity, or override can never hit a stale entry.\n * - Entries expire after a TTL (default 15 min;\n * `RIG_TOPOLOGY_TTL_MS` overrides, `0` disables the cache entirely).\n * - A cached topology that fails to bootstrap is EXPLICITLY invalidated\n * and re-resolved live (see `cli/standalone-mode.ts`), so a rotated peer\n * endpoint costs one failed attempt, not a broken 15 minutes.\n * - A corrupt/unreadable cache file is a MISS (discovery re-runs), never\n * an error: unlike the #262 channel map, nothing here guards money.\n *\n * Dependency-light on purpose (node:crypto + node:fs): the cached value is a\n * generic JSON document; `cli/standalone-mode.ts` instantiates the class\n * with its `NetworkTopology` shape and a structural validator.\n */\n\nimport { createHash } from 'node:crypto';\nimport { mkdirSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs';\nimport { dirname } from 'node:path';\n\n/** Cache filename under the shared client state dir (`TOON_CLIENT_HOME`). */\nexport const TOPOLOGY_CACHE_FILENAME = 'rig-topology-cache.json';\n\n/** Default entry TTL: 15 minutes. */\nexport const DEFAULT_TOPOLOGY_TTL_MS = 15 * 60 * 1000;\n\n/** TTL env override (milliseconds; `0` disables caching entirely). */\nexport const TOPOLOGY_TTL_ENV = 'RIG_TOPOLOGY_TTL_MS';\n\n/** Resolve the entry TTL: `RIG_TOPOLOGY_TTL_MS` env (>= 0), else 15 min. */\nexport function topologyCacheTtlMs(env: NodeJS.ProcessEnv): number {\n const raw = env[TOPOLOGY_TTL_ENV];\n if (raw === undefined || raw === '') return DEFAULT_TOPOLOGY_TTL_MS;\n const parsed = Number(raw);\n return Number.isFinite(parsed) && parsed >= 0\n ? parsed\n : DEFAULT_TOPOLOGY_TTL_MS;\n}\n\n/**\n * Stable cache key: relay-origin + identity pubkey + the explicit-config\n * fingerprint. Hashed so the key is filename/JSON-safe and the mnemonic-side\n * inputs never appear in the cache file verbatim.\n */\nexport function topologyCacheKey(args: {\n relayUrl: string;\n identity: string;\n fingerprint: string;\n}): string {\n return createHash('sha256')\n .update(`${args.relayUrl}\\n${args.identity}\\n${args.fingerprint}`)\n .digest('hex');\n}\n\n/**\n * Canonical fingerprint of the explicit inputs that steer topology\n * resolution (see `resolveNetworkTopology`'s precedence order): the\n * TOON_CLIENT_* env overrides plus the shared-config fields. Any change\n * produces a different cache key — explicit config can never be shadowed by\n * a cached resolution made under different settings.\n */\nexport function explicitConfigFingerprint(\n env: NodeJS.ProcessEnv,\n file: Record<string, unknown>\n): string {\n const envKeys = [\n 'TOON_CLIENT_PROXY_URL',\n 'TOON_CLIENT_BTP_URL',\n 'TOON_CLIENT_DESTINATION',\n 'TOON_CLIENT_PUBLISH_DESTINATION',\n 'TOON_CLIENT_STORE_DESTINATION',\n 'TOON_CLIENT_CHAIN',\n 'TOON_CLIENT_NETWORK',\n ] as const;\n const fileKeys = [\n 'network',\n 'btpUrl',\n 'proxyUrl',\n 'destination',\n 'publishDestination',\n 'storeDestination',\n 'chain',\n 'supportedChains',\n 'settlementAddresses',\n 'preferredTokens',\n 'tokenNetworks',\n 'chainRpcUrls',\n ] as const;\n const picked: Record<string, unknown> = {};\n for (const key of envKeys) {\n if (env[key] !== undefined) picked[`env:${key}`] = env[key];\n }\n for (const key of fileKeys) {\n if (file[key] !== undefined) picked[`file:${key}`] = file[key];\n }\n // Keys are inserted in fixed order above → deterministic serialization.\n return JSON.stringify(picked);\n}\n\ninterface CacheEntry {\n /** ISO timestamp the entry was written. */\n cachedAt: string;\n /** The cached (JSON-safe) topology document. */\n topology: unknown;\n}\n\ninterface CacheFile {\n version: 1;\n entries: Record<string, CacheEntry>;\n}\n\nexport interface TopologyCacheOptions<T> {\n /** Cache file path (under `TOON_CLIENT_HOME`). */\n path: string;\n /** Entry TTL in ms; `0` (or negative) disables reads AND writes. */\n ttlMs: number;\n /** Structural validator for cached values (a failed check is a miss). */\n validate: (value: unknown) => value is T;\n /** Clock override (tests). */\n now?: () => number;\n}\n\n/**\n * File-backed, TTL'd topology cache. All operations are best-effort and\n * synchronous; failures degrade to cache misses / no-ops (discovery is the\n * always-correct fallback).\n */\nexport class TopologyCache<T> {\n readonly path: string;\n readonly ttlMs: number;\n private readonly validate: (value: unknown) => value is T;\n private readonly now: () => number;\n\n constructor(options: TopologyCacheOptions<T>) {\n this.path = options.path;\n this.ttlMs = options.ttlMs;\n this.validate = options.validate;\n this.now = options.now ?? Date.now;\n }\n\n /** True when the cache is disabled (`RIG_TOPOLOGY_TTL_MS=0`). */\n get disabled(): boolean {\n return this.ttlMs <= 0;\n }\n\n /**\n * A fresh, structurally-valid entry for `key` — or undefined (expired,\n * missing, invalid, corrupt file, or disabled). Also reports the entry age\n * for the \"topology from cache\" stderr line.\n */\n read(key: string): { topology: T; ageMs: number } | undefined {\n if (this.disabled) return undefined;\n const entry = this.readFile().entries[key];\n if (!entry) return undefined;\n const cachedAt = Date.parse(entry.cachedAt);\n if (!Number.isFinite(cachedAt)) return undefined;\n const ageMs = this.now() - cachedAt;\n if (ageMs < 0 || ageMs > this.ttlMs) return undefined;\n if (!this.validate(entry.topology)) return undefined;\n return { topology: entry.topology, ageMs };\n }\n\n /** Persist `topology` under `key` (prunes expired entries; best-effort). */\n write(key: string, topology: T): void {\n if (this.disabled) return;\n try {\n const file = this.readFile();\n const nowMs = this.now();\n const fresh = Object.fromEntries(\n Object.entries(file.entries).filter(([, entry]) => {\n const at = Date.parse(entry.cachedAt);\n return Number.isFinite(at) && nowMs - at <= this.ttlMs;\n })\n );\n fresh[key] = {\n cachedAt: new Date(nowMs).toISOString(),\n topology,\n };\n mkdirSync(dirname(this.path), { recursive: true });\n writeFileSync(\n this.path,\n JSON.stringify(\n { version: 1, entries: fresh } satisfies CacheFile,\n null,\n 2\n ),\n { mode: 0o600 }\n );\n } catch {\n // Best-effort — a failed write just means the next run discovers live.\n }\n }\n\n /** Drop `key` (a cached topology failed to bootstrap). Best-effort. */\n invalidate(key: string): void {\n try {\n const file = this.readFile();\n if (!(key in file.entries)) return;\n const remaining = Object.fromEntries(\n Object.entries(file.entries).filter(([k]) => k !== key)\n );\n if (Object.keys(remaining).length === 0) {\n unlinkSync(this.path);\n return;\n }\n writeFileSync(\n this.path,\n JSON.stringify(\n { version: 1, entries: remaining } satisfies CacheFile,\n null,\n 2\n ),\n { mode: 0o600 }\n );\n } catch {\n // Best-effort — worst case the entry expires by TTL.\n }\n }\n\n private readFile(): CacheFile {\n const empty: CacheFile = { version: 1, entries: {} };\n let raw: string;\n try {\n raw = readFileSync(this.path, 'utf8');\n } catch {\n return empty;\n }\n try {\n const parsed = JSON.parse(raw) as CacheFile;\n if (\n typeof parsed !== 'object' ||\n parsed === null ||\n parsed.version !== 1 ||\n typeof parsed.entries !== 'object' ||\n parsed.entries === null\n ) {\n return empty;\n }\n return parsed;\n } catch {\n return empty; // corrupt cache = miss, never an error (see module doc)\n }\n }\n}\n","/**\n * Network bootstrap for the standalone (embedded-client) rig path (#264):\n * resolve the payment topology — uplink, ILP destinations/routes, settlement\n * chain and its on-chain parameters — from the network itself instead of\n * hand-fed constants.\n *\n * Sources, in strict precedence order (first hit wins, per field):\n *\n * 1. EXPLICIT USER CONFIG — env vars / shared client-config file fields.\n * 2. LIVE kind:10032 ANNOUNCE — the payment peer's `IlpPeerInfo` event\n * discovered on the relay-origin (the relay the paid command resolved\n * via `rig remote`, i.e. what the user pointed rig at). The announce\n * carries `btpEndpoint`/`httpEndpoint` (uplink), `ilpAddress` (channel\n * anchor), `supportedChains` + `settlementAddresses` (settlement), and\n * the out-of-band `routes` map (`{publish, store}` ILP destinations).\n * 3. GENESIS SEED — `@toon-protocol/core`'s committed genesis peer seed\n * (core >= 2.0.1 ships the live devnet apex), the offline fallback when\n * the relay is unreachable or serves no valid announce.\n *\n * Chain-level parameters the announce does NOT carry (EVM TokenNetwork\n * contract, token address, RPC URL) are derived per selected chain:\n * explicit config > announce (`tokenNetworks`/`preferredTokens`, when a peer\n * announces them) > the deployed-devnet endpoint table (canonical\n * `*.devnet.toonprotocol.dev` hosts, keyed off the announce's own hostnames)\n * > core's deterministic chain presets (`CHAIN_PRESETS`, matched by chain\n * id — e.g. `evm:31337` is the Foundry/anvil deploy whose TOON contract\n * addresses are deterministic).\n *\n * Settlement-chain selection (#260 root cause 4) — simple and predictable:\n *\n * 1. EXPLICIT — `TOON_CLIENT_CHAIN` env / `chain` config field (family or\n * full chain id), or the first entry of an explicit `supportedChains`.\n * 2. PERSISTED CHANNEL — the chain of the most recently used live channel\n * recorded for this identity in the #262 channel map (a channel there\n * means collateral is already locked on that chain).\n * 3. FUNDED — the first announced EVM chain where the identity's token\n * balance is > 0 (one `eth_call` per candidate).\n * 4. DEFAULT — the first EVM chain the peer announces (else the first\n * announced chain), with a printed rationale.\n *\n * This module is pure Node + `@toon-protocol/core` (rig's own core 2.x —\n * distinct from the embedded client's internal core; see\n * `../cli/standalone-mode.ts` for the coexistence note).\n */\n\nimport {\n CHAIN_PRESETS,\n GenesisPeerLoader,\n isEventExpired,\n parseIlpPeerInfo,\n type GenesisPeer,\n type IlpPeerInfo,\n} from '@toon-protocol/core';\nimport {\n queryRelay,\n type NostrEvent,\n type WebSocketFactory,\n} from '../remote-state.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** kind:10032 — ILP peer info announcement (mirrors core's constant). */\nexport const ILP_PEER_INFO_KIND = 10032;\n\n/** Publish/store ILP route hints riding out-of-band in announce content. */\nexport interface AnnouncedRoutes {\n publish?: string;\n store?: string;\n}\n\n/** One schema-valid, unexpired kind:10032 announce seen on the relay. */\nexport interface AnnouncedPeer {\n /** Announcing identity (event author, hex). */\n pubkey: string;\n /** Parsed + validated `IlpPeerInfo` (rig's core 2.x parser). */\n info: IlpPeerInfo;\n /** Out-of-band `routes` content field, when present and well-formed. */\n routes?: AnnouncedRoutes;\n /** Announce timestamp (freshness tiebreaker). */\n createdAt: number;\n}\n\n/** Per-chain settlement parameters resolved for the embedded client. */\nexport interface ChainSettlement {\n /** Chain id as announced/configured, e.g. `evm:31337`. */\n chain: string;\n /** `evm` | `solana` | `mina` | … (first chain-id segment). */\n family: string;\n /** JSON-RPC / GraphQL endpoint, when derivable. */\n rpcUrl?: string;\n /** Preferred token (USDC) address, when derivable. */\n tokenAddress?: string;\n /** EVM TokenNetwork contract, when derivable. */\n tokenNetwork?: string;\n}\n\n/** Why a settlement chain was selected (documented rule, in order). */\nexport type ChainSelectionReason =\n | 'explicit'\n | 'persisted-channel'\n | 'funded'\n | 'default';\n\n/** The selected settlement chain plus its rationale. */\nexport interface ChainSelection {\n chain: string;\n reason: ChainSelectionReason;\n /** One human-readable line explaining the pick. */\n detail: string;\n}\n\n// ---------------------------------------------------------------------------\n// Announce discovery\n// ---------------------------------------------------------------------------\n\n/** Default bounded wait for the kind:10032 relay query. */\nexport const DISCOVERY_TIMEOUT_MS = 5000;\n\nfunction parseRoutes(content: string): AnnouncedRoutes | undefined {\n try {\n const parsed = JSON.parse(content) as { routes?: unknown };\n const routes = parsed.routes;\n if (typeof routes !== 'object' || routes === null) return undefined;\n const { publish, store } = routes as Record<string, unknown>;\n const out: AnnouncedRoutes = {\n ...(typeof publish === 'string' && publish.length > 0 ? { publish } : {}),\n ...(typeof store === 'string' && store.length > 0 ? { store } : {}),\n };\n return out.publish || out.store ? out : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Query `relayUrl` for kind:10032 announces and return the latest\n * schema-valid, unexpired announce per author. Invalid/expired events are\n * skipped silently (the relay serves plenty of non-peer 10032 experiments).\n */\nexport async function discoverAnnouncedPeers(\n relayUrl: string,\n options: {\n timeoutMs?: number;\n webSocketFactory?: WebSocketFactory;\n } = {}\n): Promise<AnnouncedPeer[]> {\n const factory =\n options.webSocketFactory ?? defaultDiscoveryWebSocketFactory();\n const events = await queryRelay(\n relayUrl,\n { kinds: [ILP_PEER_INFO_KIND], limit: 100 },\n options.timeoutMs ?? DISCOVERY_TIMEOUT_MS,\n factory\n );\n\n const latestByAuthor = new Map<string, NostrEvent>();\n for (const event of events) {\n if (event.kind !== ILP_PEER_INFO_KIND) continue;\n const prev = latestByAuthor.get(event.pubkey);\n if (!prev || event.created_at > prev.created_at) {\n latestByAuthor.set(event.pubkey, event);\n }\n }\n\n const peers: AnnouncedPeer[] = [];\n for (const event of latestByAuthor.values()) {\n if (isEventExpired(event as Parameters<typeof isEventExpired>[0])) {\n continue;\n }\n let info: IlpPeerInfo;\n try {\n info = parseIlpPeerInfo(event as Parameters<typeof parseIlpPeerInfo>[0]);\n } catch {\n continue; // not a schema-valid IlpPeerInfo announce\n }\n const routes = parseRoutes(event.content);\n peers.push({\n pubkey: event.pubkey,\n info,\n ...(routes ? { routes } : {}),\n createdAt: event.created_at,\n });\n }\n return peers;\n}\n\nfunction defaultDiscoveryWebSocketFactory(): WebSocketFactory {\n return (url) => {\n const ctor = (\n globalThis as {\n WebSocket?: new (url: string) => ReturnType<WebSocketFactory>;\n }\n ).WebSocket;\n if (!ctor) {\n throw new Error(\n 'No global WebSocket constructor (Node >= 22 required) for announce discovery'\n );\n }\n return new ctor(url);\n };\n}\n\n/**\n * Pick THE payment peer among discovered announces:\n *\n * 1. the announce authored by a genesis-seed pubkey (the committed apex\n * identity — the strongest signal),\n * 2. else an announce that can actually take paid writes: has an uplink\n * endpoint AND `settlementAddresses`, preferring one whose own\n * `ilpAddress` is its `routes.publish` (the publish edge — that is\n * where rig pays first),\n * 3. freshest `created_at` breaks remaining ties.\n */\nexport function pickPaymentPeer(\n peers: AnnouncedPeer[],\n seedPubkeys: readonly string[]\n): AnnouncedPeer | undefined {\n const seeded = peers.filter((p) => seedPubkeys.includes(p.pubkey));\n if (seeded.length > 0) {\n return seeded.sort((a, b) => b.createdAt - a.createdAt)[0];\n }\n\n const payable = peers.filter(\n (p) =>\n (p.info.httpEndpoint || p.info.btpEndpoint) &&\n p.info.settlementAddresses &&\n Object.keys(p.info.settlementAddresses).length > 0\n );\n if (payable.length === 0) return undefined;\n const publishEdges = payable.filter(\n (p) => p.routes?.publish !== undefined && p.routes.publish === p.info.ilpAddress\n );\n const pool = publishEdges.length > 0 ? publishEdges : payable;\n return pool.sort((a, b) => b.createdAt - a.createdAt)[0];\n}\n\n// ---------------------------------------------------------------------------\n// Per-chain settlement derivation\n// ---------------------------------------------------------------------------\n\n/**\n * The one deployed-network endpoint table rig ships: the canonical TOON\n * devnet (`*.devnet.toonprotocol.dev`, see toon-meta docs/deployment.md).\n * Chain RPC endpoints are deployment infrastructure the kind:10032 announce\n * does not carry (yet), so when the discovered peer's own endpoints live\n * under this zone, its self-hosted chains resolve to the canonical hosts.\n * Same status as `DEVNET_FAUCET_URL` in `../cli/fund.ts`. Everything here\n * is overridable by explicit `chainRpcUrls` config.\n */\nexport const DEVNET_ZONE = 'devnet.toonprotocol.dev';\n\n/** Canonical devnet chain RPC endpoints (self-hosted chains only). */\nexport const DEVNET_CHAIN_RPC_URLS: Readonly<Record<string, string>> = {\n 'evm:31337': 'https://evm-rpc.devnet.toonprotocol.dev',\n 'solana:devnet': 'https://solana-rpc.devnet.toonprotocol.dev',\n};\n\n/** Hostname of a ws(s)/http(s) URL, or undefined when unparsable. */\nfunction hostOf(url: string | undefined): string | undefined {\n if (!url) return undefined;\n try {\n return new URL(url).hostname;\n } catch {\n return undefined;\n }\n}\n\n/** True when the announce's own endpoints live under the TOON devnet zone. */\nexport function isDevnetZonePeer(\n peer: Pick<AnnouncedPeer, 'info'> | undefined\n): boolean {\n if (!peer) return false;\n return [\n hostOf(peer.info.httpEndpoint),\n hostOf(peer.info.btpEndpoint),\n hostOf(peer.info.relayUrl),\n ].some((h) => h !== undefined && (h === DEVNET_ZONE || h.endsWith(`.${DEVNET_ZONE}`)));\n}\n\n/** Numeric chain id of an `evm:<id>` / `evm:<name>:<id>` chain key. */\nfunction evmChainIdOf(chain: string): number | undefined {\n const parts = chain.split(':');\n if (parts[0] !== 'evm') return undefined;\n const raw = parts.length >= 3 ? parts[2] : parts[1];\n const id = Number.parseInt(raw ?? '', 10);\n return Number.isNaN(id) ? undefined : id;\n}\n\n/**\n * Core chain preset matching an EVM chain key by numeric chain id. Presets\n * carry the DETERMINISTIC TOON contract addresses per chain (e.g. the\n * `anvil` 31337 Foundry deploy), which is what makes `tokenNetwork`\n * derivable without the announce carrying it.\n */\nexport function evmPresetForChain(chain: string):\n | { rpcUrl: string; usdcAddress: string; tokenNetworkAddress: string }\n | undefined {\n const id = evmChainIdOf(chain);\n if (id === undefined) return undefined;\n for (const preset of Object.values(CHAIN_PRESETS)) {\n if (preset.chainId === id) {\n return {\n rpcUrl: preset.rpcUrl,\n usdcAddress: preset.usdcAddress,\n tokenNetworkAddress: preset.tokenNetworkAddress,\n };\n }\n }\n return undefined;\n}\n\n/** Explicit per-chain maps from the shared client-config file. */\nexport interface ExplicitChainConfig {\n chainRpcUrls?: Record<string, string>;\n preferredTokens?: Record<string, string>;\n tokenNetworks?: Record<string, string>;\n}\n\n/**\n * Resolve one chain's settlement parameters: explicit config > announce >\n * devnet endpoint table (RPC only, devnet-zone peers) > core chain preset.\n * Fields stay undefined when no source covers them — callers decide whether\n * that is fatal (see {@link requireTokenNetwork}).\n */\nexport function resolveChainSettlement(\n chain: string,\n explicit: ExplicitChainConfig,\n announce?: AnnouncedPeer\n): ChainSettlement {\n const family = chain.split(':')[0] ?? chain;\n const preset = evmPresetForChain(chain);\n const devnetRpc = isDevnetZonePeer(announce)\n ? DEVNET_CHAIN_RPC_URLS[chain]\n : undefined;\n\n const rpcUrl =\n explicit.chainRpcUrls?.[chain] ?? devnetRpc ?? preset?.rpcUrl;\n const tokenAddress =\n explicit.preferredTokens?.[chain] ??\n announce?.info.preferredTokens?.[chain] ??\n (preset?.usdcAddress || undefined);\n const tokenNetwork =\n explicit.tokenNetworks?.[chain] ??\n announce?.info.tokenNetworks?.[chain] ??\n (preset?.tokenNetworkAddress || undefined);\n\n return {\n chain,\n family,\n ...(rpcUrl ? { rpcUrl } : {}),\n ...(tokenAddress ? { tokenAddress } : {}),\n ...(tokenNetwork ? { tokenNetwork } : {}),\n };\n}\n\n/** An EVM chain was selected but its TokenNetwork cannot be derived. */\nexport class TokenNetworkUnderivableError extends Error {\n constructor(chain: string, announce: AnnouncedPeer | undefined, relayUrl: string) {\n const announceRef = announce\n ? `the kind:10032 announce from ${announce.pubkey.slice(0, 16)}… on ${relayUrl}`\n : `no kind:10032 announce was found on ${relayUrl}`;\n super(\n `cannot derive the TokenNetwork contract for settlement chain ` +\n `\"${chain}\": ${announceRef} carries no tokenNetworks[\"${chain}\"], ` +\n `and no built-in chain preset matches its chain id — add ` +\n `tokenNetworks[\"${chain}\"] to the client config (or pick another ` +\n `chain via TOON_CLIENT_CHAIN / the chain config field)`\n );\n this.name = 'TokenNetworkUnderivableError';\n }\n}\n\n// ---------------------------------------------------------------------------\n// Settlement-chain selection (#260 root cause 4)\n// ---------------------------------------------------------------------------\n\n/** The slice of a #262 channel-map record chain selection consumes. */\nexport interface ChannelRecordLike {\n chain: string;\n lastUsedAt: string;\n /** True when the withdraw flow closed/settled the channel. */\n closed: boolean;\n}\n\n/** Async token-balance probe (injectable; default is a raw `eth_call`). */\nexport type EvmBalanceProbe = (args: {\n rpcUrl: string;\n tokenAddress: string;\n owner: string;\n}) => Promise<bigint>;\n\nexport interface SelectChainOptions {\n /**\n * Explicit chain choice: full chain id (`evm:31337`), a family\n * (`evm` | `solana` | `mina`) matched against announced chains, or the\n * first entry of an explicit `supportedChains` config.\n */\n explicitChain?: string;\n /** Chains the payment peer announces (announce order preserved). */\n announcedChains: readonly string[];\n /** #262 channel-map records for this identity (any anchor). */\n records?: readonly ChannelRecordLike[];\n /** Identity's EVM address (funded-chain probe). */\n evmAddress?: string;\n /** Per-chain settlement resolver (for RPC/token of probe candidates). */\n resolveSettlement: (chain: string) => ChainSettlement;\n /** Balance probe; probe errors just skip the candidate. */\n probeBalance?: EvmBalanceProbe;\n}\n\n/**\n * Select the settlement chain per the documented rule: explicit >\n * persisted channel > funded EVM chain > first announced EVM chain.\n */\nexport async function selectSettlementChain(\n options: SelectChainOptions\n): Promise<ChainSelection> {\n const { explicitChain, announcedChains } = options;\n\n // 1 — explicit config always wins (even when the peer does not announce\n // it: the user may know better than a stale announce).\n if (explicitChain) {\n if (explicitChain.includes(':')) {\n return {\n chain: explicitChain,\n reason: 'explicit',\n detail: `chain ${explicitChain} set by config`,\n };\n }\n const familyMatch = announcedChains.find(\n (c) => (c.split(':')[0] ?? c) === explicitChain\n );\n if (!familyMatch) {\n throw new Error(\n `configured chain family \"${explicitChain}\" is not announced by the ` +\n `payment peer (announced: ${announcedChains.join(', ') || 'none'}) — ` +\n 'set a full chain id (e.g. \"evm:31337\") to force it'\n );\n }\n return {\n chain: familyMatch,\n reason: 'explicit',\n detail: `chain family \"${explicitChain}\" set by config → ${familyMatch}`,\n };\n }\n\n // 2 — a live persisted channel means collateral is already locked there.\n const live = (options.records ?? [])\n .filter((r) => !r.closed && announcedChains.includes(r.chain))\n .sort((a, b) => b.lastUsedAt.localeCompare(a.lastUsedAt));\n const persisted = live[0];\n if (persisted) {\n return {\n chain: persisted.chain,\n reason: 'persisted-channel',\n detail: `existing payment channel on ${persisted.chain} (rig channel map)`,\n };\n }\n\n // 3 — first announced EVM chain where the identity holds tokens.\n const evmChains = announcedChains.filter((c) => c.startsWith('evm:'));\n if (options.evmAddress && options.probeBalance) {\n for (const chain of evmChains) {\n const settlement = options.resolveSettlement(chain);\n if (!settlement.rpcUrl || !settlement.tokenAddress) continue;\n try {\n const balance = await options.probeBalance({\n rpcUrl: settlement.rpcUrl,\n tokenAddress: settlement.tokenAddress,\n owner: options.evmAddress,\n });\n if (balance > 0n) {\n return {\n chain,\n reason: 'funded',\n detail: `wallet holds ${balance} token base units on ${chain}`,\n };\n }\n } catch {\n // Unreachable RPC / bad token — not a candidate; fall through.\n }\n }\n }\n\n // 4 — predictable default: the first EVM chain the peer announces.\n const fallback = evmChains[0] ?? announcedChains[0];\n if (!fallback) {\n throw new Error(\n 'the payment peer announces no settlement chains — cannot select a ' +\n 'chain for paid writes (set supportedChains/chain in the client config)'\n );\n }\n return {\n chain: fallback,\n reason: 'default',\n detail: evmChains[0]\n ? `first EVM chain announced by the payment peer`\n : `first chain announced by the payment peer (no EVM chain announced)`,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Default EVM balance probe (raw JSON-RPC eth_call, no extra deps)\n// ---------------------------------------------------------------------------\n\n/** ERC-20 `balanceOf(address)` selector. */\nconst BALANCE_OF_SELECTOR = '0x70a08231';\n\n/**\n * Read an ERC-20 balance with one raw `eth_call` (keeps the probe free of\n * viem — the embedded client is not started yet when selection runs).\n */\nexport async function evmTokenBalance(args: {\n rpcUrl: string;\n tokenAddress: string;\n owner: string;\n fetchImpl?: typeof fetch;\n timeoutMs?: number;\n}): Promise<bigint> {\n const fetchImpl = args.fetchImpl ?? fetch;\n const owner = args.owner.replace(/^0x/, '').toLowerCase().padStart(64, '0');\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), args.timeoutMs ?? 5000);\n try {\n const res = await fetchImpl(args.rpcUrl, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n jsonrpc: '2.0',\n id: 1,\n method: 'eth_call',\n params: [\n { to: args.tokenAddress, data: `${BALANCE_OF_SELECTOR}${owner}` },\n 'latest',\n ],\n }),\n signal: controller.signal,\n });\n if (!res.ok) {\n throw new Error(`eth_call failed: HTTP ${res.status}`);\n }\n const body = (await res.json()) as { result?: unknown; error?: { message?: string } };\n if (typeof body.result !== 'string') {\n throw new Error(`eth_call failed: ${body.error?.message ?? 'no result'}`);\n }\n return BigInt(body.result === '0x' ? '0x0' : body.result);\n } finally {\n clearTimeout(timer);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Genesis seed access (rig's own core 2.x — live devnet apex since 2.0.1)\n// ---------------------------------------------------------------------------\n\n/** The committed genesis peer seed (first entry), if any. */\nexport function loadGenesisSeed(): GenesisPeer | undefined {\n return GenesisPeerLoader.loadGenesisPeers()[0];\n}\n\n/** All committed genesis-seed pubkeys (announce-selection preference). */\nexport function genesisSeedPubkeys(): string[] {\n return GenesisPeerLoader.loadGenesisPeers().map((p) => p.pubkey);\n}\n"],"mappings":"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","names":["readFileSync","readFileSync"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toon-protocol/rig",
3
- "version": "2.0.1",
3
+ "version": "2.2.0",
4
4
  "description": "Git-to-TOON write path core — build git objects and NIP-34 events for the Rig control plane",
5
5
  "license": "MIT",
6
6
  "author": "Jonathan Green",
@@ -43,7 +43,7 @@
43
43
  ],
44
44
  "dependencies": {
45
45
  "@toon-format/toon": "^1.0.0",
46
- "@toon-protocol/core": "^1.6.0",
46
+ "@toon-protocol/core": "^2.0.1",
47
47
  "@toon-protocol/client": "^0.15.0"
48
48
  },
49
49
  "devDependencies": {
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/remote-state.ts","../../arweave/src/gateways.ts","../../arweave/src/git-sha.ts"],"sourcesContent":["/**\n * Remote repository state reader — the \"what does the remote have?\" half of\n * `rig push` (epic #222, ticket #225).\n *\n * Queries relay(s) over NIP-01 WebSocket for the repository's NIP-34 state:\n * - kind:30618 (repository state): `r` tags → ref map, `HEAD` symref,\n * `arweave` tags → git SHA → Arweave txId hints.\n * - kind:30617 (repository announcement): presence = the repo exists on\n * TOON (first-push detection) + name/description/relays metadata.\n *\n * Both kinds are NIP-33 parameterized-replaceable: per relay only the latest\n * event per (kind, author, d) survives, but we still pick the newest across\n * relays (highest created_at, ties broken by lowest id per NIP-01).\n *\n * SHAs missing from the `arweave` tag map can be resolved via the shared\n * Arweave GraphQL Git-SHA resolver (@toon-protocol/arweave — the same module\n * the rig SPA uses) through {@link RemoteState.resolveMissing}.\n *\n * Relay payload encodings (mirrors rig's `web/relay-client.ts` decode logic):\n * EVENT payloads arrive as an inline JSON object (standard NIP-01), as a\n * double-JSON-encoded string (devnet relay quirk: a JSON string containing\n * the event JSON), or as a TOON-encoded string. All three are tolerated.\n */\n\nimport { decode as decodeToon } from '@toon-format/toon';\nimport {\n resolveGitSha,\n seedShaCache,\n shaCacheKey,\n} from '@toon-protocol/arweave';\nimport { REPOSITORY_ANNOUNCEMENT_KIND } from '@toon-protocol/core/nip34';\n\nimport { REPOSITORY_STATE_KIND } from './nip34-events.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** A signed Nostr event as seen on a relay (read side). */\nexport interface NostrEvent {\n id: string;\n pubkey: string;\n created_at: number;\n kind: number;\n tags: string[][];\n content: string;\n sig: string;\n}\n\n/** NIP-01 subscription filter (only the fields this module sends). */\ninterface NostrFilter {\n kinds?: number[];\n authors?: string[];\n '#d'?: string[];\n}\n\n/**\n * Minimal structural WebSocket type — satisfied by the WHATWG WebSocket\n * global (Node >= 22 / undici, browsers) and by the `ws` package.\n */\nexport interface WebSocketLike {\n readyState: number;\n send(data: string): void;\n close(): void;\n addEventListener(type: string, listener: (event: never) => void): void;\n}\n\n/** Factory for WebSocket connections (injectable for tests / `ws` fallback). */\nexport type WebSocketFactory = (url: string) => WebSocketLike;\n\nexport interface FetchRemoteStateOptions {\n /**\n * Relay WebSocket URLs to query. Plural from day one (forward-compat with\n * multi-relay routing); size 1 is typical today. Results are merged and the\n * newest replaceable event across all relays wins.\n */\n relayUrls: string[];\n /** Repository owner's pubkey (hex) — the author of 30617/30618. */\n ownerPubkey: string;\n /** Repository identifier (NIP-34 `d` tag). */\n repoId: string;\n /** Per-relay timeout in milliseconds (default 10000). On timeout the relay contributes whatever it sent so far. */\n timeoutMs?: number;\n /**\n * Git-SHA → Arweave txId resolver used by {@link RemoteState.resolveMissing}.\n * Defaults to the shared GraphQL resolver from @toon-protocol/arweave;\n * injectable for tests.\n */\n resolveSha?: (sha: string, repo: string) => Promise<string | null>;\n /** WebSocket constructor override (defaults to the global WebSocket). */\n webSocketFactory?: WebSocketFactory;\n}\n\n/** Remote repository state assembled from relay events. */\nexport interface RemoteState {\n /** True when a kind:30617 announcement exists (false ⇒ first push). */\n announced: boolean;\n /** Ref map from the latest kind:30618: refname → commit SHA. */\n refs: Map<string, string>;\n /** HEAD symref target (e.g. `refs/heads/main`), or null if unset. */\n headSymref: string | null;\n /** Git SHA → Arweave txId hints from the latest kind:30618 `arweave` tags. */\n shaToTxId: Map<string, string>;\n /** The latest kind:30618 event, or null if the repo has no state yet. */\n refsEvent: NostrEvent | null;\n /** The latest kind:30617 event, or null if the repo is unannounced. */\n announceEvent: NostrEvent | null;\n /** Repository name from the announcement `name` tag. */\n name: string | null;\n /** Announcement `description` tag (falls back to event content). */\n description: string | null;\n /** Relay URLs advertised in the announcement `relays` tag(s). */\n relays: string[];\n /**\n * Resolve SHAs to Arweave txIds: served from the `arweave` tag map when\n * present, otherwise via the GraphQL Git-SHA resolver. SHAs that resolve\n * nowhere are omitted from the returned map.\n */\n resolveMissing(shas: string[]): Promise<Map<string, string>>;\n}\n\n// ---------------------------------------------------------------------------\n// Relay query (NIP-01 REQ → EVENT* → EOSE)\n// ---------------------------------------------------------------------------\n\n/** Validate that a relay URL uses a WebSocket scheme (mirrors rig's url-utils). */\nfunction isValidRelayUrl(url: string): boolean {\n return /^wss?:\\/\\//i.test(url);\n}\n\n/** WHATWG WebSocket OPEN ready state. */\nconst WS_OPEN = 1;\n\nfunction defaultWebSocketFactory(url: string): WebSocketLike {\n const ctor = (\n globalThis as { WebSocket?: new (url: string) => WebSocketLike }\n ).WebSocket;\n if (!ctor) {\n throw new Error(\n 'No global WebSocket constructor (Node >= 22 required) — pass webSocketFactory'\n );\n }\n return new ctor(url);\n}\n\n/**\n * Decode a relay EVENT payload, tolerating every encoding seen in the wild:\n * inline object (standard NIP-01), double-JSON-encoded string (devnet relay\n * serves the event as a JSON string containing the event JSON), or a\n * TOON-encoded string (rig's `decodeToonMessage` path).\n */\nfunction decodeEventPayload(payload: unknown): NostrEvent | null {\n if (payload !== null && typeof payload === 'object') {\n return payload as NostrEvent;\n }\n if (typeof payload !== 'string') {\n return null;\n }\n try {\n const parsed: unknown = JSON.parse(payload);\n if (parsed !== null && typeof parsed === 'object') {\n return parsed as NostrEvent;\n }\n } catch {\n // Not JSON — fall through to TOON\n }\n try {\n return decodeToon(payload) as unknown as NostrEvent;\n } catch {\n return null;\n }\n}\n\n/**\n * Query one relay: send a REQ, collect EVENTs until EOSE, then CLOSE.\n * Mirrors rig's `queryRelay` (partial results on timeout / early close).\n */\nfunction queryRelay(\n relayUrl: string,\n filter: NostrFilter,\n timeoutMs: number,\n webSocketFactory: WebSocketFactory\n): Promise<NostrEvent[]> {\n return new Promise((resolve, reject) => {\n const events: NostrEvent[] = [];\n const subId = `git-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;\n let ws: WebSocketLike;\n // eslint-disable-next-line prefer-const -- assigned after `settle` is defined\n let timeoutHandle: ReturnType<typeof setTimeout>;\n let settled = false;\n\n const settle = (outcome: 'resolve' | 'reject', error?: Error) => {\n if (settled) return;\n settled = true;\n clearTimeout(timeoutHandle);\n try {\n if (ws.readyState === WS_OPEN) {\n ws.send(JSON.stringify(['CLOSE', subId]));\n }\n ws.close();\n } catch {\n // Ignore close errors\n }\n if (outcome === 'reject') {\n reject(error);\n } else {\n resolve(events);\n }\n };\n\n if (!isValidRelayUrl(relayUrl)) {\n reject(\n new Error(\n `Invalid relay URL protocol (must be ws:// or wss://): ${relayUrl}`\n )\n );\n return;\n }\n\n try {\n ws = webSocketFactory(relayUrl);\n } catch (err) {\n reject(new Error(`Failed to connect to relay ${relayUrl}: ${String(err)}`));\n return;\n }\n\n timeoutHandle = setTimeout(() => {\n // Resolve with whatever we collected so far (partial results)\n settle('resolve');\n }, timeoutMs);\n\n ws.addEventListener('open', () => {\n ws.send(JSON.stringify(['REQ', subId, filter]));\n });\n\n ws.addEventListener('message', (msgEvent: { data?: unknown }) => {\n try {\n const msg = JSON.parse(String(msgEvent.data)) as unknown[];\n if (!Array.isArray(msg) || msg.length < 2) return;\n\n const msgType = msg[0];\n if (msgType === 'EVENT' && msg[1] === subId && msg[2] !== undefined) {\n const event = decodeEventPayload(msg[2]);\n if (event) events.push(event);\n } else if (msgType === 'EOSE' && msg[1] === subId) {\n settle('resolve');\n }\n } catch {\n // Ignore parse errors for individual messages\n }\n });\n\n ws.addEventListener('error', (event: { message?: unknown }) => {\n const detail =\n typeof event === 'object' && event !== null && 'message' in event\n ? String(event.message)\n : 'unknown';\n settle(\n 'reject',\n new Error(`WebSocket error connecting to ${relayUrl}: ${detail}`)\n );\n });\n\n ws.addEventListener('close', () => {\n // If we haven't settled yet, resolve with what we have\n settle('resolve');\n });\n });\n}\n\n// ---------------------------------------------------------------------------\n// NIP-33 replaceable selection + tag parsing\n// ---------------------------------------------------------------------------\n\n/**\n * Pick the winning replaceable event: highest created_at, ties broken by\n * lowest id (NIP-01 replaceable-event convention).\n */\nfunction latestReplaceable(events: NostrEvent[]): NostrEvent | null {\n let winner: NostrEvent | null = null;\n for (const event of events) {\n if (\n winner === null ||\n event.created_at > winner.created_at ||\n (event.created_at === winner.created_at && event.id < winner.id)\n ) {\n winner = event;\n }\n }\n return winner;\n}\n\n/** Get the first value for a tag name. */\nfunction getTagValue(tags: string[][], name: string): string | undefined {\n const tag = tags.find((t) => t[0] === name);\n return tag?.[1];\n}\n\n/** Maximum number of refs parsed from a single kind:30618 event (mirrors views). */\nconst MAX_REFS_PER_EVENT = 1000;\n\n/** Symref prefix used in `HEAD` tags: `[\"HEAD\", \"ref: refs/heads/main\"]`. */\nconst SYMREF_PREFIX = 'ref: ';\n\ninterface ParsedRefs {\n refs: Map<string, string>;\n headSymref: string | null;\n shaToTxId: Map<string, string>;\n}\n\n/** Parse a kind:30618 event's `r` / `HEAD` / `arweave` tags. */\nfunction parseRefsEvent(event: NostrEvent): ParsedRefs {\n const refs = new Map<string, string>();\n const shaToTxId = new Map<string, string>();\n let headSymref: string | null = null;\n\n for (const tag of event.tags) {\n const [tagName, v1, v2] = tag;\n if (tagName === 'r' && v1 && v2) {\n if (v1 === 'HEAD' && v2.startsWith(SYMREF_PREFIX)) {\n // Alternate symref spelling: [\"r\", \"HEAD\", \"ref: refs/heads/main\"]\n headSymref = v2.slice(SYMREF_PREFIX.length);\n continue;\n }\n if (refs.size >= MAX_REFS_PER_EVENT) continue;\n refs.set(v1, v2);\n } else if (tagName === 'HEAD' && v1?.startsWith(SYMREF_PREFIX)) {\n // NIP-34 symref tag: [\"HEAD\", \"ref: refs/heads/main\"]\n headSymref = v1.slice(SYMREF_PREFIX.length);\n } else if (tagName === 'arweave' && v1 && v2) {\n shaToTxId.set(v1, v2);\n }\n }\n\n return { refs, headSymref, shaToTxId };\n}\n\n// ---------------------------------------------------------------------------\n// fetchRemoteState\n// ---------------------------------------------------------------------------\n\n/**\n * Fetch the remote repository state from the relay(s).\n *\n * Sends one REQ per relay for kind:30617 + kind:30618 with\n * `authors=[ownerPubkey]`, `#d=[repoId]`, collects until EOSE (or timeout),\n * and reduces to the latest replaceable event per kind.\n *\n * Resolves as long as at least one relay answers; throws only when every\n * relay fails. Events from other authors / repos are ignored (defense\n * against misbehaving relays that over-return).\n */\nexport async function fetchRemoteState(\n options: FetchRemoteStateOptions\n): Promise<RemoteState> {\n const {\n relayUrls,\n ownerPubkey,\n repoId,\n timeoutMs = 10000,\n resolveSha = resolveGitSha,\n webSocketFactory = defaultWebSocketFactory,\n } = options;\n\n if (relayUrls.length === 0) {\n throw new Error('fetchRemoteState: relayUrls must not be empty');\n }\n if (!ownerPubkey) {\n throw new Error('fetchRemoteState: ownerPubkey is required');\n }\n if (!repoId) {\n throw new Error('fetchRemoteState: repoId is required');\n }\n\n const filter: NostrFilter = {\n kinds: [REPOSITORY_ANNOUNCEMENT_KIND, REPOSITORY_STATE_KIND],\n authors: [ownerPubkey],\n '#d': [repoId],\n };\n\n const results = await Promise.allSettled(\n relayUrls.map((url) => queryRelay(url, filter, timeoutMs, webSocketFactory))\n );\n\n const failures: string[] = [];\n const byId = new Map<string, NostrEvent>();\n for (const result of results) {\n if (result.status === 'rejected') {\n failures.push(String((result.reason as Error)?.message ?? result.reason));\n continue;\n }\n for (const event of result.value) {\n // Only trust events from the repo owner for this repo — relays are\n // untrusted and may over-return.\n if (event.pubkey !== ownerPubkey) continue;\n if (getTagValue(event.tags, 'd') !== repoId) continue;\n if (typeof event.id === 'string' && !byId.has(event.id)) {\n byId.set(event.id, event);\n }\n }\n }\n\n if (failures.length === relayUrls.length) {\n throw new Error(\n `fetchRemoteState: all ${relayUrls.length} relay(s) failed: ${failures.join('; ')}`\n );\n }\n\n const events = [...byId.values()];\n const refsEvent = latestReplaceable(\n events.filter((e) => e.kind === REPOSITORY_STATE_KIND)\n );\n const announceEvent = latestReplaceable(\n events.filter((e) => e.kind === REPOSITORY_ANNOUNCEMENT_KIND)\n );\n\n const { refs, headSymref, shaToTxId } = refsEvent\n ? parseRefsEvent(refsEvent)\n : {\n refs: new Map<string, string>(),\n headSymref: null,\n shaToTxId: new Map<string, string>(),\n };\n\n // Seed the shared resolver cache so later resolveGitSha calls (here or in\n // any other consumer of @toon-protocol/arweave) skip GraphQL for known SHAs.\n if (shaToTxId.size > 0) {\n seedShaCache(\n [...shaToTxId].map(\n ([sha, txId]) => [shaCacheKey(sha, repoId), txId] as [string, string]\n )\n );\n }\n\n // Announcement metadata (mirrors views' parseRepoAnnouncement defaults).\n const name = announceEvent\n ? (getTagValue(announceEvent.tags, 'name') ?? null)\n : null;\n const description = announceEvent\n ? (getTagValue(announceEvent.tags, 'description') ?? announceEvent.content)\n : null;\n // NIP-34 announcement relays tag is multi-valued: [\"relays\", url1, url2, …]\n const relays: string[] = [];\n if (announceEvent) {\n for (const tag of announceEvent.tags) {\n if (tag[0] === 'relays') {\n relays.push(...tag.slice(1).filter((url) => url.length > 0));\n }\n }\n }\n\n const resolveMissing = async (\n shas: string[]\n ): Promise<Map<string, string>> => {\n const resolved = new Map<string, string>();\n const missing: string[] = [];\n for (const sha of new Set(shas)) {\n const known = shaToTxId.get(sha);\n if (known !== undefined) {\n resolved.set(sha, known);\n } else {\n missing.push(sha);\n }\n }\n const lookups = await Promise.all(\n missing.map(\n async (sha) => [sha, await resolveSha(sha, repoId)] as const\n )\n );\n for (const [sha, txId] of lookups) {\n if (txId) resolved.set(sha, txId);\n }\n return resolved;\n };\n\n return {\n announced: announceEvent !== null,\n refs,\n headSymref,\n shaToTxId,\n refsEvent,\n announceEvent,\n name,\n description,\n relays,\n resolveMissing,\n };\n}\n","/**\n * Arweave gateway redundancy — single source of truth.\n *\n * Media bytes are content-addressed by Arweave tx id, so every gateway serves\n * the same bytes. This module owns the ordered gateway preference list and the\n * URL helpers used on BOTH sides of the wire:\n * - upload (client-mcp daemon): stamp a primary `url` + `fallback` mirrors.\n * - render (views/rig browser): re-point imeta URLs + fail over on error.\n *\n * Previously hand-duplicated in `views`, `rig`, and `client-mcp`; those now all\n * import from here. The default list can be overridden per call (e.g. from a\n * daemon env var) — pass `gateways` to the helpers.\n */\n\n/** Ordered Arweave gateways to try (primary first, then fallbacks). */\nexport const ARWEAVE_GATEWAYS = [\n 'https://ar-io.dev',\n 'https://arweave.net',\n 'https://permagate.io',\n] as const;\n\n/** Timeout for individual Arweave fetch requests in milliseconds. */\nexport const ARWEAVE_FETCH_TIMEOUT_MS = 15000;\n\n/** Arweave transaction IDs are 43-character base64url strings. */\nconst TX_ID_RE = /^[a-zA-Z0-9_-]{43}$/;\n\n/** Hosts recognized as Arweave gateways (path-addressed). */\nconst ARWEAVE_HOST_RE =\n /(^|\\.)(arweave\\.net|ar-io\\.dev|permagate\\.io|g8way\\.io|ar\\.io)$/i;\n\n/**\n * Extract an Arweave tx id from a media URL, or null if it is not\n * Arweave-addressable. Handles `ar://<txid>` and path-style\n * `https://<gateway>/<txid>`.\n *\n * Sandbox-subdomain URLs (`https://<txid>.<gateway>`) are deliberately NOT\n * decoded: tx ids are case-sensitive base64url, but `URL` (and DNS) lower-case\n * the hostname, which would corrupt the id. Real gateway sandboxing uses a\n * base32 label, not the raw id — the canonical id always travels in the path.\n */\nexport function arweaveTxId(rawUrl: string): string | null {\n const ar = /^ar:\\/\\/([a-zA-Z0-9_-]{43})(?:[/?#]|$)/.exec(rawUrl);\n if (ar?.[1]) return ar[1];\n\n let u: URL;\n try {\n u = new URL(rawUrl);\n } catch {\n return null;\n }\n // Only re-point hosts we actually recognize as Arweave gateways, so a stray\n // 43-char path segment on some other CDN is never misread as a tx id.\n if (!ARWEAVE_HOST_RE.test(u.hostname)) return null;\n\n // Path style: https://arweave.net/<txid>\n const seg = u.pathname.split('/').find(Boolean);\n if (seg && TX_ID_RE.test(seg)) return seg;\n\n return null;\n}\n\n/**\n * Primary URL + fallback mirror URLs for an Arweave tx id, one per gateway in\n * preference order. Used by the upload path to stamp `imeta` `url` + `fallback`.\n */\nexport function arweaveUrls(\n txId: string,\n gateways: readonly string[] = ARWEAVE_GATEWAYS\n): { url: string; fallbacks: string[] } {\n const all = (gateways.length ? gateways : ARWEAVE_GATEWAYS).map(\n (g) => `${g}/${txId}`\n );\n const [url, ...fallbacks] = all;\n return { url: url ?? `${ARWEAVE_GATEWAYS[0]}/${txId}`, fallbacks };\n}\n\n/**\n * Ordered candidate URLs for a media URL. Arweave-addressable URLs expand to the\n * full gateway-preference list (primary first); anything else is returned\n * unchanged. `extraFallbacks` (e.g. publisher-supplied `imeta` mirrors) are\n * appended last, de-duplicated. Used by the render path to fail over on error.\n */\nexport function arweaveGatewayCandidates(\n rawUrl: string,\n extraFallbacks: string[] = [],\n gateways: readonly string[] = ARWEAVE_GATEWAYS\n): string[] {\n const txId = arweaveTxId(rawUrl);\n const candidates = txId\n ? (gateways.length ? gateways : ARWEAVE_GATEWAYS).map((g) => `${g}/${txId}`)\n : [rawUrl];\n const seen = new Set(candidates);\n for (const f of extraFallbacks) {\n if (f && !seen.has(f)) {\n seen.add(f);\n candidates.push(f);\n }\n }\n return candidates;\n}\n","/**\n * Git-SHA → Arweave transaction ID resolution.\n *\n * Git objects uploaded to Arweave are tagged with `Git-SHA` (the object's\n * SHA-1) and `Repo` (the repository identifier, matching the NIP-34 `d` tag).\n * This module resolves a git SHA to its Arweave tx id via the Arweave GraphQL\n * gateway, with a bounded in-memory cache that can be pre-seeded from relay\n * state (kind:30618 `arweave` tags) to skip the GraphQL indexing delay.\n *\n * Extracted verbatim from rig's `web/arweave-client.ts` (#225) so the browser\n * SPA (rig-web) and the Node write path (@toon-protocol/rig) share ONE resolver.\n * Uses only WHATWG fetch + AbortSignal.timeout — browser and Node compatible.\n */\n\nimport { ARWEAVE_FETCH_TIMEOUT_MS } from './gateways.js';\n\n/** Arweave GraphQL endpoint used for Git-SHA tag lookups. */\nconst ARWEAVE_GRAPHQL_URL = 'https://arweave.net/graphql';\n\n/** Maximum number of entries in the SHA-to-txId cache to prevent unbounded memory growth. */\nconst SHA_CACHE_MAX_SIZE = 10000;\n\n/** In-memory cache for SHA-to-txId resolution. Bounded to prevent memory leaks. */\nconst shaToTxIdCache = new Map<string, string>();\n\n/**\n * Validate a git SHA-1 hash format (40-character hex string).\n */\nfunction isValidGitSha(sha: string): boolean {\n return /^[0-9a-f]{40}$/i.test(sha);\n}\n\n/**\n * Sanitize a string for safe inclusion in a GraphQL query.\n * Removes characters that could break out of a GraphQL string literal,\n * including backticks which some GraphQL parsers may interpret.\n */\nfunction sanitizeGraphQLValue(value: string): string {\n // eslint-disable-next-line no-control-regex -- intentional: strip control chars for GraphQL safety\n return value.replace(/[\"\\\\\\n\\r\\u0000-\\u001f`]/g, '');\n}\n\n/**\n * Build the cache key used by {@link resolveGitSha} / {@link seedShaCache}.\n *\n * The cache is keyed on `\"sha:repo\"` so the same SHA in different repos\n * resolves independently (uploads are tagged per-repo).\n */\nexport function shaCacheKey(sha: string, repo: string): string {\n return `${sha}:${repo}`;\n}\n\n/**\n * Clear the SHA-to-txId cache. Used for test isolation.\n */\nexport function clearShaCache(): void {\n shaToTxIdCache.clear();\n}\n\n/**\n * Pre-seed the SHA-to-txId cache with known mappings.\n *\n * Used when txId mappings are available from relay events (e.g., kind:30618\n * `arweave` tags) to avoid the GraphQL indexing delay after Turbo/Irys uploads.\n *\n * @param mappings - Map of \"sha:repo\" cache keys to Arweave transaction IDs\n */\nexport function seedShaCache(\n mappings: Map<string, string> | [string, string][]\n): void {\n const entries = mappings instanceof Map ? mappings.entries() : mappings;\n for (const [key, txId] of entries) {\n if (shaToTxIdCache.size >= SHA_CACHE_MAX_SIZE) {\n const firstKey = shaToTxIdCache.keys().next().value;\n if (firstKey !== undefined) {\n shaToTxIdCache.delete(firstKey);\n }\n }\n shaToTxIdCache.set(key, txId);\n }\n}\n\n/** Arweave transaction IDs are 43-character base64url strings. */\nconst ARWEAVE_TX_ID_RE = /^[a-zA-Z0-9_-]{43}$/;\n\n/**\n * Validate an Arweave transaction ID format.\n * Arweave tx IDs are 43-character base64url-encoded strings.\n */\nexport function isValidArweaveTxId(txId: string): boolean {\n return ARWEAVE_TX_ID_RE.test(txId);\n}\n\n/**\n * Resolve a git SHA to an Arweave transaction ID via GraphQL.\n *\n * Queries the Arweave GraphQL endpoint for transactions tagged with\n * the given Git-SHA and Repo values. Results are cached in-memory.\n *\n * @param sha - Git object SHA-1 hash (hex)\n * @param repo - Repository identifier (matches d tag)\n * @returns Arweave transaction ID, or null if not found\n */\nexport async function resolveGitSha(\n sha: string,\n repo: string\n): Promise<string | null> {\n // Validate SHA format to prevent injection of arbitrary strings into GraphQL\n if (!isValidGitSha(sha)) {\n return null;\n }\n\n const cacheKey = shaCacheKey(sha, repo);\n const cached = shaToTxIdCache.get(cacheKey);\n if (cached !== undefined) {\n return cached;\n }\n\n const safeSha = sanitizeGraphQLValue(sha);\n const safeRepo = sanitizeGraphQLValue(repo);\n const query = `query {\n transactions(tags: [\n { name: \"Git-SHA\", values: [\"${safeSha}\"] },\n { name: \"Repo\", values: [\"${safeRepo}\"] }\n ]) {\n edges { node { id } }\n }\n}`;\n\n try {\n const response = await fetch(ARWEAVE_GRAPHQL_URL, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ query }),\n signal: AbortSignal.timeout(ARWEAVE_FETCH_TIMEOUT_MS),\n });\n\n if (!response.ok) {\n return null;\n }\n\n const json = (await response.json()) as {\n data?: {\n transactions?: {\n edges?: { node?: { id?: string } }[];\n };\n };\n };\n\n const edges = json.data?.transactions?.edges;\n if (!edges || edges.length === 0) {\n return null;\n }\n\n const txId = edges[0]?.node?.id;\n if (!txId || !isValidArweaveTxId(txId)) {\n return null;\n }\n\n // Evict oldest entries if cache exceeds max size\n if (shaToTxIdCache.size >= SHA_CACHE_MAX_SIZE) {\n const firstKey = shaToTxIdCache.keys().next().value;\n if (firstKey !== undefined) {\n shaToTxIdCache.delete(firstKey);\n }\n }\n shaToTxIdCache.set(cacheKey, txId);\n return txId;\n } catch {\n return null;\n }\n}\n"],"mappings":"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","names":[]}
@@ -1,109 +0,0 @@
1
- // src/nip34-events.ts
2
- import {
3
- ISSUE_KIND,
4
- PATCH_KIND,
5
- REPOSITORY_ANNOUNCEMENT_KIND
6
- } from "@toon-protocol/core/nip34";
7
- var REPOSITORY_STATE_KIND = 30618;
8
- var COMMENT_KIND = 1622;
9
- function buildRepoAnnouncement(repoId, name, description) {
10
- return {
11
- kind: REPOSITORY_ANNOUNCEMENT_KIND,
12
- content: "",
13
- tags: [
14
- ["d", repoId],
15
- ["name", name],
16
- ["description", description]
17
- ],
18
- created_at: Math.floor(Date.now() / 1e3)
19
- };
20
- }
21
- function buildRepoRefs(repoId, refs, arweaveMap = {}) {
22
- const tags = [["d", repoId]];
23
- for (const [refPath, commitSha] of Object.entries(refs)) {
24
- tags.push(["r", refPath, commitSha]);
25
- }
26
- const firstRef = Object.keys(refs)[0];
27
- if (firstRef) {
28
- tags.push(["HEAD", `ref: ${firstRef}`]);
29
- }
30
- for (const [sha, txId] of Object.entries(arweaveMap)) {
31
- tags.push(["arweave", sha, txId]);
32
- }
33
- return {
34
- kind: REPOSITORY_STATE_KIND,
35
- content: "",
36
- tags,
37
- created_at: Math.floor(Date.now() / 1e3)
38
- };
39
- }
40
- function buildIssue(repoOwnerPubkey, repoId, title, body, labels = []) {
41
- const tags = [
42
- ["a", `${REPOSITORY_ANNOUNCEMENT_KIND}:${repoOwnerPubkey}:${repoId}`],
43
- ["p", repoOwnerPubkey],
44
- ["subject", title],
45
- ...labels.map((label) => ["t", label])
46
- ];
47
- return {
48
- kind: ISSUE_KIND,
49
- content: body,
50
- tags,
51
- created_at: Math.floor(Date.now() / 1e3)
52
- };
53
- }
54
- function buildComment(repoOwnerPubkey, repoId, issueOrPrEventId, authorPubkey, body, marker = "reply") {
55
- return {
56
- kind: COMMENT_KIND,
57
- content: body,
58
- tags: [
59
- ["a", `${REPOSITORY_ANNOUNCEMENT_KIND}:${repoOwnerPubkey}:${repoId}`],
60
- ["e", issueOrPrEventId, "", marker],
61
- ["p", authorPubkey]
62
- ],
63
- created_at: Math.floor(Date.now() / 1e3)
64
- };
65
- }
66
- function buildPatch(repoOwnerPubkey, repoId, title, commits, branchTag, content = "") {
67
- const tags = [
68
- ["a", `${REPOSITORY_ANNOUNCEMENT_KIND}:${repoOwnerPubkey}:${repoId}`],
69
- ["p", repoOwnerPubkey],
70
- ["subject", title]
71
- ];
72
- for (const commit of commits) {
73
- tags.push(["commit", commit.sha]);
74
- tags.push(["parent-commit", commit.parentSha]);
75
- }
76
- if (branchTag) {
77
- tags.push(["t", branchTag]);
78
- }
79
- return {
80
- kind: PATCH_KIND,
81
- content,
82
- tags,
83
- created_at: Math.floor(Date.now() / 1e3)
84
- };
85
- }
86
- function buildStatus(targetEventId, statusKind, targetPubkey) {
87
- const tags = [["e", targetEventId]];
88
- if (targetPubkey) {
89
- tags.push(["p", targetPubkey]);
90
- }
91
- return {
92
- kind: statusKind,
93
- content: "",
94
- tags,
95
- created_at: Math.floor(Date.now() / 1e3)
96
- };
97
- }
98
-
99
- export {
100
- REPOSITORY_STATE_KIND,
101
- COMMENT_KIND,
102
- buildRepoAnnouncement,
103
- buildRepoRefs,
104
- buildIssue,
105
- buildComment,
106
- buildPatch,
107
- buildStatus
108
- };
109
- //# sourceMappingURL=chunk-HPSOQP7Q.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/nip34-events.ts"],"sourcesContent":["/**\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\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 */\nexport function buildRepoAnnouncement(\n repoId: string,\n name: string,\n description: string\n): UnsignedEvent {\n return {\n kind: REPOSITORY_ANNOUNCEMENT_KIND,\n content: '',\n tags: [\n ['d', repoId],\n ['name', name],\n ['description', description],\n ],\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 * @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 */\nexport function buildPatch(\n repoOwnerPubkey: string,\n repoId: string,\n title: string,\n commits: { sha: string; parentSha: string }[],\n branchTag?: string,\n content = ''\n): UnsignedEvent {\n const tags: string[][] = [\n ['a', `${REPOSITORY_ANNOUNCEMENT_KIND}:${repoOwnerPubkey}:${repoId}`],\n ['p', repoOwnerPubkey],\n ['subject', title],\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"],"mappings":"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","names":[]}