@toon-protocol/rig 3.6.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +112 -152
- package/dist/chunk-CDSFVZDV.js +290 -0
- package/dist/chunk-CDSFVZDV.js.map +1 -0
- package/dist/{chunk-B5ISARMU.js → chunk-GVXBUL3C.js} +15 -2
- package/dist/chunk-GVXBUL3C.js.map +1 -0
- package/dist/chunk-LTFMPOHU.js +194 -0
- package/dist/chunk-LTFMPOHU.js.map +1 -0
- package/dist/{chunk-LCYCKWAT.js → chunk-YZRR2UO6.js} +86 -5
- package/dist/chunk-YZRR2UO6.js.map +1 -0
- package/dist/{chunk-HJDPM37T.js → chunk-ZZBA5VVL.js} +11 -7
- package/dist/{chunk-HJDPM37T.js.map → chunk-ZZBA5VVL.js.map} +1 -1
- package/dist/cli/rig.js +290 -523
- package/dist/cli/rig.js.map +1 -1
- package/dist/index.d.ts +3 -115
- package/dist/index.js +9 -121
- package/dist/index.js.map +1 -1
- package/dist/{publisher-BFcnZeJI.d.ts → publisher-CcL96ugq.d.ts} +58 -1
- package/dist/standalone/index.d.ts +241 -438
- package/dist/standalone/index.js +19 -11
- package/dist/standalone-mode-LEAC4A2L.js +390 -0
- package/dist/standalone-mode-LEAC4A2L.js.map +1 -0
- package/package.json +5 -4
- package/dist/chunk-B5ISARMU.js.map +0 -1
- package/dist/chunk-IZ6KLYQ6.js +0 -724
- package/dist/chunk-IZ6KLYQ6.js.map +0 -1
- package/dist/chunk-LCYCKWAT.js.map +0 -1
- package/dist/chunk-PWNZWQOH.js +0 -404
- package/dist/chunk-PWNZWQOH.js.map +0 -1
- package/dist/chunk-UZZ2RN7A.js +0 -469
- package/dist/chunk-UZZ2RN7A.js.map +0 -1
- package/dist/network-bootstrap-27FYE3N7.js +0 -40
- package/dist/network-bootstrap-27FYE3N7.js.map +0 -1
- package/dist/standalone-mode-5MNNJC2D.js +0 -928
- package/dist/standalone-mode-5MNNJC2D.js.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/standalone/standalone-publisher.ts"],"sourcesContent":["/**\n * StandalonePublisher — impl 2 of the {@link Publisher} seam (#228): an\n * EMBEDDED ToonClient constructed from the caller's config (mnemonic +\n * account index, the exact `packages/client/src/config.ts` shape) instead of\n * routing through a running toon-clientd. Made for CI jobs, servers, and\n * one-shot CLI runs where no daemon exists.\n *\n * Paid-write mechanics mirror the production daemon path\n * (`client-mcp/src/daemon/client-runner.ts`) and the proven seed pipeline\n * (`rig/tests/e2e/seed/lib/{publish,git-builder}.ts`):\n *\n * - one signed balance-proof CLAIM per write (`signBalanceProof(channelId,\n * fee)` — the ChannelManager accumulates the cumulative watermark),\n * - publishes route to the relay write destination with a flat per-event\n * fee (the daemon's `feePerEvent` convention),\n * - git objects upload as kind:5094 store writes tagged\n * Git-SHA/Git-Type/Repo, priced at bytes × per-byte rate (the seed\n * pipeline's bid), routed to the store destination via `proxyPath:\n * '/store'`, with the Arweave txId decoded from the FULFILL data.\n *\n * Because the embedded client signs claims on the SAME channels a daemon on\n * the same identity would, every paid operation is preceded by the nonce\n * guard (./nonce-guard.ts): refuse if a toon-clientd holds this identity,\n * and hold an exclusive per-pubkey lockfile against other standalone\n * processes for the lifetime of this publisher.\n *\n * Channel REUSE (#262): with a `channelMap` configured, start() resumes the\n * channel recorded for (identity, channel anchor) — `trackChannel` rehydrates\n * the cumulative-claim watermark from the client's channels.json — and\n * records any fresh lazy open, so sequential CLI invocations share ONE\n * on-chain channel instead of stranding a deposit per run (./channel-map.ts).\n */\n\nimport type {\n PublishEventResult,\n SignedBalanceProof,\n ToonClientConfig,\n} from '@toon-protocol/client';\nimport { ToonClient, extractArweaveTxId } from '@toon-protocol/client';\nimport { MAX_OBJECT_SIZE } from '../objects.js';\nimport { contentTypeForPath, DEFAULT_CONTENT_TYPE } from '../mime.js';\nimport type { UnsignedEvent } from '../nip34-events.js';\nimport {\n type BlobUpload,\n type FeeRates,\n type GitObjectUpload,\n type PublishReceipt,\n type Publisher,\n type UploadReceipt,\n} from '../publisher.js';\nimport {\n recordKey,\n type ChannelMapRecord,\n type ChannelMapStore,\n type PersistedChannelContext,\n} from './channel-map.js';\nimport type {\n ChannelCloseOutcome,\n ChannelOpenOutcome,\n ChannelSettleOutcome,\n WalletChainBalanceInfo,\n} from './money.js';\nimport { checkDaemonIdentity, NonceLock } from './nonce-guard.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** A fully-signed Nostr event (structural subset of nostr-tools' NostrEvent). */\nexport interface SignedNostrEvent extends UnsignedEvent {\n id: string;\n pubkey: string;\n sig: string;\n}\n\n/**\n * The slice of `ToonClient` the publisher drives. Kept structural so tests\n * inject a mock and alternative client builds stay compatible.\n */\nexport interface ToonClientLike {\n start(): Promise<unknown>;\n stop(): Promise<void>;\n isStarted?(): boolean;\n getPublicKey(): string;\n signEvent(template: UnsignedEvent): SignedNostrEvent;\n openChannel(destination?: string): Promise<string>;\n signBalanceProof(\n channelId: string,\n amount: bigint\n ): Promise<SignedBalanceProof>;\n publishEvent(\n event: SignedNostrEvent,\n options?: {\n destination?: string;\n claim?: SignedBalanceProof;\n ilpAmount?: bigint;\n proxyPath?: string;\n }\n ): Promise<PublishEventResult>;\n /** On-chain deposit total for a tracked channel (channel-map bookkeeping). */\n getChannelDepositTotal?(channelId: string): bigint;\n /** Re-read a RESUMED channel's on-chain deposit (persisted state omits it). */\n rehydrateChannelDeposit?(\n channelId: string,\n opts: { chain: string; tokenNetworkAddress: string }\n ): Promise<bigint | undefined>;\n // ── money lifecycle (#263) — optional, matches ToonClient's surface ──────\n /** Deposit extra collateral (base-unit delta) into an open channel. */\n depositToChannel?(\n channelId: string,\n amount: string | bigint\n ): Promise<{ channelId: string; txHash?: string; depositTotal: string }>;\n /** Close a channel — starts the settlement challenge window (on-chain). */\n closeChannel?(channelId: string): Promise<{\n channelId: string;\n txHash?: string;\n /** Unix SECONDS, string-encoded. */\n closedAt: string;\n settleableAt: string;\n }>;\n /** Settle a closed channel after its window — releases funds (on-chain). */\n settleChannel?(\n channelId: string\n ): Promise<{ channelId: string; txHash?: string }>;\n /**\n * Free FULL multi-chain wallet view (#299) — native coin + configured tokens\n * per chain — works on an UNSTARTED client (Solana/Mina addresses are derived\n * from the mnemonic on demand). `fallback` supplies Solana/Mina channel params\n * to read WHEN the client config has none, without touching settlement config.\n */\n getWalletBalances?(\n fallback?: WalletViewFallback\n ): Promise<WalletChainBalanceInfo[]>;\n}\n\n/**\n * Wallet-view-only Solana/Mina channel defaults (#299): RPC/GraphQL + token used\n * to READ the balance of a chain the identity has no configured channel for, so\n * `rig balance` shows all three chains (0 for a not-yet-on-chain account). These\n * NEVER enter the client's settlement config — they cannot influence chain\n * negotiation — so a preset default here is safe even for a chain the wallet\n * cannot yet settle on.\n */\nexport interface WalletViewFallback {\n solanaChannel?: ToonClientConfig['solanaChannel'];\n minaChannel?: ToonClientConfig['minaChannel'];\n}\n\n/**\n * The client + channel one leg of the write path pays on. Two legs exist\n * only when the store route terminates on a different node than publishes;\n * otherwise both names point at the same client and channel.\n */\ninterface StoreLeg {\n client: ToonClientLike;\n channelId: string;\n}\n\nexport interface StandalonePublisherOptions {\n /**\n * ToonClient config (mnemonic + mnemonicAccountIndex + proxy/BTP uplink +\n * settlement fields — see `packages/client/src/config.ts`). Exactly one of\n * `clientConfig` | `client` is required.\n */\n clientConfig?: ToonClientConfig;\n /** Pre-built client (tests / advanced callers). */\n client?: ToonClientLike;\n /**\n * ILP route for event publishes (relay `/write`). Default: derived from the\n * config's `destinationAddress` anchor (`<base>.relay.store` → `<base>.relay`,\n * matching the daemon's route derivation), else the anchor itself.\n */\n publishDestination?: string;\n /**\n * ILP route for git-object uploads (store `/store` → Arweave). Default:\n * derived like `publishDestination` (`<base>.relay.store` → `<base>.store`).\n */\n storeDestination?: string;\n /**\n * ILP destination the payment channel anchors to. Default: the client\n * config's `destinationAddress`.\n */\n channelDestination?: string;\n /**\n * Client config for a SECOND uplink dedicated to store writes, when the\n * store route terminates on a different node than publishes.\n *\n * A balance-proof claim is only verifiable by the connector holding that\n * channel — one with no record of it refuses the packet (`F01 - claim\n * rejected: names a channel this connector has no record of`). The store\n * node has its own settlement address and its own channel, and on the\n * two-box fleet the publish node has no route to it, so a store write\n * cannot ride the publish session. Given this, store writes go out on\n * their own client, against their own channel, recorded under their own\n * anchor so `rig channel` still sees it.\n *\n * A separate CLIENT rather than a second destination on the existing one\n * because `@toon-protocol/client` carries a single `btpUrl` per instance —\n * there is no per-destination uplink to configure. Omitted (the\n * single-node case) keeps the historical behaviour: one client, one\n * channel, store writes on the publish session.\n */\n storeClientConfig?: ToonClientConfig;\n /**\n * Pre-built store-leg client (tests / advanced callers) — the store-side\n * counterpart of `client`, and mutually exclusive with\n * `storeClientConfig` in the same way.\n */\n storeClient?: ToonClientLike;\n /** Flat fee per published event (daemon `feePerEvent` convention). Default 1n. */\n eventFee?: bigint;\n /**\n * FLAT per-packet route prices for the publish/store destinations, as the\n * payment peer announces them (kind:10032 `capabilities` — e.g.\n * `{capability:'os.publish', address:'g.proxy.relay', price:'1000'}`) or as\n * the terminating connector reports them from `GET /ilp/routes/price`.\n *\n * The connector gates every paid packet at the destination route's price: a\n * balance-proof claim advancing the channel by less is rejected (F06). Since\n * ADR 0020 (toon-client#452) a price is flat per handler, so `store` is the\n * WHOLE upload fee rather than a floor under a per-byte computation — an\n * upload of any size costs it. `publish` still floors {@link eventFee},\n * which remains a caller-chosen flat figure.\n *\n * A destination without a known price prices its packets at 0 here and lets\n * the connector be the one to refuse — this class never invents a rate.\n */\n routePrices?: { publish?: bigint; store?: bigint };\n /** Daemon control API port probed by the nonce guard. */\n daemonPort?: number;\n /**\n * Skip Guard 1 (the same-identity daemon refusal, {@link checkDaemonIdentity})\n * — the force-standalone override ({@link standaloneForced} / RIG_STANDALONE\n * / `rig --standalone`). Guard 2, the exclusive per-identity {@link NonceLock},\n * ALWAYS runs regardless, so two standalone processes still can't race the\n * channel watermark. Default false (daemon-aware).\n */\n skipDaemonCheck?: boolean;\n /** Directory for the per-identity advisory lockfile. */\n lockDir?: string;\n /** Fetch impl for the daemon probe (tests). */\n fetchImpl?: typeof fetch;\n /**\n * Peer→channel map store (#262): start() resumes the channel recorded for\n * (identity, channel anchor) instead of opening a fresh one, and records\n * any fresh lazy open for the next invocation. Absent (embedded callers\n * managing their own channel lifecycle): the historical behaviour — open\n * lazily every run, record nothing.\n */\n channelMap?: ChannelMapStore;\n /**\n * Per-chain settlement parameters to BACK-FILL into the client's peer\n * negotiations after start (#264/#260): peers whose kind:10032 announce\n * carries no `tokenNetworks`/`preferredTokens` negotiate with those fields\n * empty, and the on-chain channel open then fails (\"tokenNetwork address\n * is required\"). Values here never override what a peer DID announce —\n * they only fill gaps, keyed by the negotiated chain id.\n */\n negotiationFallbacks?: {\n tokenNetworks?: Record<string, string>;\n preferredTokens?: Record<string, string>;\n };\n /** Sink for non-fatal channel-persistence warnings (default: stderr). */\n warn?: (line: string) => void;\n}\n\n/** A relay/store rejected a paid write (fee NOT spent iff the claim failed too). */\nexport class StandalonePublishError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'StandalonePublishError';\n }\n}\n\n// ---------------------------------------------------------------------------\n// Route derivation (duplicated from client-mcp daemon/config.ts — that\n// package depends on this one for #227, so importing it back would be\n// circular; keep in sync)\n// ---------------------------------------------------------------------------\n\n/**\n * Derive publish/store routes from the channel anchor. Behind the devnet\n * proxy the anchor is `<base>.relay.store` (e.g. `g.proxy.relay.store`):\n * publishes terminate at `<base>.relay`, uploads at `<base>.store`. Anchors\n * not matching the convention pass through unchanged.\n */\nexport function deriveRouteDestinations(anchor: string): {\n publish: string;\n store: string;\n} {\n const segs = anchor.split('.');\n if (segs.at(-1) === 'store' && segs.at(-2) === 'relay') {\n const base = segs.slice(0, -2).join('.');\n return { publish: `${base}.relay`, store: `${base}.store` };\n }\n return { publish: anchor, store: anchor };\n}\n\n// ---------------------------------------------------------------------------\n// FULFILL → Arweave txId\n// ---------------------------------------------------------------------------\n\n/**\n * Decode the Arweave txId from a store-write answer.\n *\n * Re-exported from `@toon-protocol/client` rather than reimplemented: this\n * file used to carry its own copy because the client's extractor was thought\n * not to be exported. It is (and always was), and now that the answer is a\n * sealed response envelope rather than HTTP text there is exactly one shape\n * to read — a second reader of it could only drift.\n */\nexport { extractArweaveTxId };\n\n// ---------------------------------------------------------------------------\n// Channel-resume introspection (#262)\n//\n// The peer negotiation table and the ChannelManager's peer→channel map are\n// PRIVATE on ToonClient, so resuming a channel reaches into them through a\n// structural runtime cast — the exact pattern the daemon uses\n// (client-mcp/src/daemon/client-runner.ts, openOrResumeApexChannel /\n// routeChildPeersThroughApexChannel). Keep the shapes in sync with\n// @toon-protocol/client's ToonClient/ChannelManager.\n// ---------------------------------------------------------------------------\n\n/** The slice of a `PeerNegotiation` the channel map records. */\ninterface NegotiationLike {\n chain: string;\n chainType: string;\n chainId: number | string;\n settlementAddress: string;\n tokenAddress?: string;\n tokenNetwork?: string;\n}\n\ninterface ChannelInternals {\n peerNegotiations?: Map<string, NegotiationLike>;\n channelManager?: {\n trackChannel?: (\n channelId: string,\n context: PersistedChannelContext\n ) => void;\n peerChannels?: Map<string, string>;\n };\n /**\n * The client's on-chain channel client caches per-channel chain context at\n * OPEN time only; close/settle/deposit on a channel resumed from the map\n * would throw \"No on-chain context\" without re-seeding it (#263). Same\n * structural-cast contract as the rest of this block — keep in sync with\n * `@toon-protocol/client`'s OnChainChannelClient.\n */\n onChainChannelClient?: {\n channelContext?: Map<\n string,\n { chain: string; tokenNetworkAddress: string; tokenAddress?: string }\n >;\n };\n}\n\n/**\n * The key `ChannelManager` binds a peer's channel under —\n * `<peerId>|<chain>|<tokenNetwork>` (toon-client#489: the same peer on a\n * second chain, or a redeployed token network, is a DIFFERENT channel).\n * `peerChannels` used to be keyed by the bare peer id, so rig both wrote\n * resumed channels under a key `ensureChannel` never looks up (a fresh\n * on-chain channel per invocation) and read the composite key back as if it\n * were a peer id (so `peerNegotiations.get(...)` missed and nothing was ever\n * recorded). Keep in sync with `@toon-protocol/client`'s\n * `ChannelManager.bindingKey`.\n */\nfunction channelBindingKey(\n peerId: string,\n chain: string,\n tokenNetwork: string\n): string {\n return `${peerId}|${chain}|${tokenNetwork}`;\n}\n\n/**\n * The peer id inside a {@link channelBindingKey}. A pre-#489 bare-peer-id key\n * has no separator and passes through unchanged, so this reads either shape.\n */\nfunction peerIdFromBindingKey(key: string): string {\n return key.split('|')[0] ?? key;\n}\n\n/** Best-effort access to the client's private negotiation/channel state. */\nfunction channelInternals(client: ToonClientLike): ChannelInternals {\n const c = client as unknown as ChannelInternals;\n const negotiations =\n c.peerNegotiations instanceof Map ? c.peerNegotiations : undefined;\n const cm =\n c.channelManager &&\n typeof c.channelManager.trackChannel === 'function' &&\n c.channelManager.peerChannels instanceof Map\n ? c.channelManager\n : undefined;\n const onChain =\n c.onChainChannelClient &&\n c.onChainChannelClient.channelContext instanceof Map\n ? c.onChainChannelClient\n : undefined;\n return {\n ...(negotiations ? { peerNegotiations: negotiations } : {}),\n ...(cm ? { channelManager: cm } : {}),\n ...(onChain ? { onChainChannelClient: onChain } : {}),\n };\n}\n\n// ---------------------------------------------------------------------------\n// StandalonePublisher\n// ---------------------------------------------------------------------------\n\nexport class StandalonePublisher implements Publisher {\n private readonly client: ToonClientLike;\n private readonly ownsClient: boolean;\n private readonly publishDestination: string | undefined;\n private readonly storeDestination: string | undefined;\n private readonly channelDestination: string | undefined;\n private readonly eventFee: bigint;\n private readonly routePrices:\n | StandalonePublisherOptions['routePrices']\n | undefined;\n private readonly daemonPort: number | undefined;\n private readonly skipDaemonCheck: boolean;\n private readonly lockDir: string | undefined;\n private readonly fetchImpl: typeof fetch | undefined;\n private readonly channelMap: ChannelMapStore | undefined;\n /** ILP anchor the channel is keyed by in the map (peer/apex destination). */\n private readonly channelAnchor: string | undefined;\n private readonly negotiationFallbacks:\n | StandalonePublisherOptions['negotiationFallbacks']\n | undefined;\n private readonly warn: (line: string) => void;\n\n private readonly storeClientConfig: ToonClientConfig | undefined;\n private readonly injectedStoreClient: ToonClientLike | undefined;\n /** Lazily built on the first store write — see {@link storeLeg}. */\n private storeClient: ToonClientLike | undefined;\n private storeChannelId: string | undefined;\n private storeLegPromise: Promise<StoreLeg> | undefined;\n\n private lock: NonceLock | undefined;\n private channelId: string | undefined;\n private readyPromise: Promise<void> | undefined;\n /** Guard + client start only (no channel) — see {@link startClientOnly}. */\n private clientReadyPromise: Promise<void> | undefined;\n /** True when the last start() RESUMED the recorded channel (#263). */\n private lastOpenResumed = false;\n\n constructor(options: StandalonePublisherOptions) {\n if (options.client && options.clientConfig) {\n throw new Error(\n 'StandalonePublisher: provide either `clientConfig` or `client`, not both'\n );\n }\n if (options.client) {\n this.client = options.client;\n this.ownsClient = false;\n } else if (options.clientConfig) {\n this.client = new ToonClient(options.clientConfig);\n this.ownsClient = true;\n } else {\n throw new Error(\n 'StandalonePublisher: one of `clientConfig` (mnemonic-based ToonClient config) or `client` is required'\n );\n }\n\n // Routes: explicit option → derived from the channel anchor (same\n // `<base>.relay.store` convention the daemon resolves).\n const anchor =\n options.channelDestination ?? options.clientConfig?.destinationAddress;\n const routes = anchor ? deriveRouteDestinations(anchor) : undefined;\n this.publishDestination = options.publishDestination ?? routes?.publish;\n this.storeDestination = options.storeDestination ?? routes?.store;\n this.channelDestination = options.channelDestination;\n\n this.eventFee = options.eventFee ?? 1n;\n this.routePrices = options.routePrices;\n this.daemonPort = options.daemonPort;\n this.skipDaemonCheck = options.skipDaemonCheck ?? false;\n this.lockDir = options.lockDir;\n this.fetchImpl = options.fetchImpl;\n this.channelMap = options.channelMap;\n this.channelAnchor = anchor;\n if (options.storeClient && options.storeClientConfig) {\n throw new Error(\n 'StandalonePublisher: provide either `storeClientConfig` or `storeClient`, not both'\n );\n }\n this.storeClientConfig = options.storeClientConfig;\n this.injectedStoreClient = options.storeClient;\n this.negotiationFallbacks = options.negotiationFallbacks;\n this.warn = options.warn ?? ((line) => process.stderr.write(`${line}\\n`));\n }\n\n /** Hex Nostr pubkey of the embedded identity (available before start). */\n getPublicKey(): string {\n return this.client.getPublicKey();\n }\n\n /**\n * Run the nonce guard, start the embedded client, and open (or resume) the\n * payment channel. Called lazily by the first paid operation; safe to call\n * eagerly to fail fast. Idempotent.\n */\n start(): Promise<void> {\n this.readyPromise ??= (async () => {\n await this.startClientOnly();\n try {\n this.channelId = await this.openOrResumeChannel();\n } catch (err) {\n // Release the identity lock on a failed channel open (pre-#263\n // behaviour): nothing holds claims yet, so another process may go.\n this.lock?.release();\n this.lock = undefined;\n this.clientReadyPromise = undefined;\n throw err;\n }\n })().catch((err: unknown) => {\n // Let a later call retry (e.g. after the conflicting daemon stops).\n this.readyPromise = undefined;\n throw err;\n });\n return this.readyPromise;\n }\n\n /**\n * Run the nonce guard and start the embedded client WITHOUT touching the\n * payment channel (#263): `channel close`/`settle` operate on a RECORDED\n * channel and must never open a fresh one as a side effect of starting.\n * Idempotent; `start()` layers the channel open/resume on top of this.\n */\n startClientOnly(): Promise<void> {\n this.clientReadyPromise ??= this.doStartClient().catch((err: unknown) => {\n this.clientReadyPromise = undefined;\n throw err;\n });\n return this.clientReadyPromise;\n }\n\n private async doStartClient(): Promise<void> {\n const pubkey = this.client.getPublicKey();\n\n // Guard 1: refuse while a toon-clientd holds this identity — UNLESS the\n // force-standalone override is set (RIG_STANDALONE / `rig --standalone`),\n // in which case the caller has explicitly chosen to sign for itself and\n // ignore the daemon. Guard 2 (the NonceLock below) still runs either way.\n if (!this.skipDaemonCheck) {\n await checkDaemonIdentity(pubkey, {\n ...(this.daemonPort !== undefined ? { port: this.daemonPort } : {}),\n ...(this.fetchImpl ? { fetchImpl: this.fetchImpl } : {}),\n });\n }\n\n // Guard 2: exclusive advisory lock against other standalone processes.\n this.lock = await NonceLock.acquire(pubkey, {\n ...(this.lockDir !== undefined ? { dir: this.lockDir } : {}),\n });\n\n try {\n if (this.client.isStarted?.() !== true) {\n await this.client.start();\n }\n // #264: back-fill negotiation gaps as soon as the client's bootstrap\n // negotiation exists — before any channel open (start()) or recorded-\n // channel operation reads them.\n this.applyNegotiationFallbacks();\n } catch (err) {\n this.lock.release();\n this.lock = undefined;\n throw err;\n }\n }\n\n /**\n * Back-fill negotiated peer metadata the announce did not carry\n * (#264/#260 root cause 3): after the client's bootstrap negotiation, any\n * peer negotiation missing `tokenNetwork`/`tokenAddress` gets the derived\n * per-chain fallback for its negotiated chain, BEFORE the lazy channel\n * open reads them. Announced values are never overridden.\n */\n private applyNegotiationFallbacks(): void {\n const fallbacks = this.negotiationFallbacks;\n if (!fallbacks) return;\n const negotiations = channelInternals(this.client).peerNegotiations;\n if (!negotiations) {\n this.warn(\n 'rig: settlement fallbacks configured but the client does not ' +\n 'expose negotiation internals — on-chain channel opening may fail ' +\n 'if the peer announced no TokenNetwork'\n );\n return;\n }\n for (const negotiation of negotiations.values()) {\n if (!negotiation.tokenNetwork) {\n const tokenNetwork = fallbacks.tokenNetworks?.[negotiation.chain];\n if (tokenNetwork) negotiation.tokenNetwork = tokenNetwork;\n }\n if (!negotiation.tokenAddress) {\n const tokenAddress = fallbacks.preferredTokens?.[negotiation.chain];\n if (tokenAddress) negotiation.tokenAddress = tokenAddress;\n }\n }\n }\n\n /**\n * Open the payment channel — or, when the channel map has a record for\n * (identity, anchor), RESUME the recorded on-chain channel (#262).\n *\n * Resume seeds the client's ChannelManager (`trackChannel` rehydrates the\n * cumulative-claim watermark from channels.json; `peerChannels` makes the\n * subsequent `openChannel` return the same id instead of opening on-chain).\n * A fresh open is RECORDED so the next invocation resumes it. A corrupt map\n * file throws BEFORE anything is opened — never a silent duplicate open.\n */\n private async openOrResumeChannel(): Promise<string> {\n return this.openOrResumeChannelOn(this.client, this.channelDestination, {\n anchor: this.channelAnchor,\n trackResumeState: true,\n });\n }\n\n /**\n * {@link openOrResumeChannel} for an arbitrary client + destination, so\n * the store leg gets the same resume-before-open guarantee (and the same\n * corruption check) as the publish leg rather than a second, weaker path.\n *\n * `anchor` defaults to the destination: a leg other than the publish one\n * is keyed in the channel map by the route it pays for.\n * `trackResumeState` is publish-only — {@link lastOpenResumed} feeds the\n * `resumed` field of this publisher's channel status, which describes the\n * publish channel and would be wrong to overwrite from a store open.\n */\n private async openOrResumeChannelOn(\n client: ToonClientLike,\n destination: string | undefined,\n opts: { anchor?: string; trackResumeState?: boolean } = {}\n ): Promise<string> {\n const map = this.channelMap;\n const trackResumeState = opts.trackResumeState ?? false;\n if (!map) {\n // No persistence configured: historical lazy open, nothing recorded.\n return client.openChannel(destination);\n }\n const anchorFor = opts.anchor ?? destination;\n if (!anchorFor) {\n this.warn(\n 'rig: no channel anchor destination configured — the peer→channel ' +\n 'mapping cannot be persisted, so this run may open a fresh channel'\n );\n return client.openChannel(destination);\n }\n\n const anchor = anchorFor;\n const identity = client.getPublicKey();\n // Corruption check happens HERE, before any on-chain open (throws).\n const candidates = map.listFor(identity, anchor);\n const internals = channelInternals(client);\n const resumed = await this.resumeRecordedChannel(\n map,\n candidates,\n internals,\n client\n );\n\n // Idempotent — returns the (resumed or existing) channel for the peer if\n // one is tracked, else opens lazily on-chain.\n const channelId = await client.openChannel(destination);\n\n if (resumed && channelId === resumed.channelId) {\n if (trackResumeState) this.lastOpenResumed = true;\n map.touch(recordKey(resumed));\n } else {\n if (trackResumeState) this.lastOpenResumed = false;\n this.recordOpenedChannel(\n map,\n internals,\n identity,\n anchor,\n channelId,\n client\n );\n }\n return channelId;\n }\n\n /**\n * Try to resume one recorded channel: the first candidate whose peer is\n * still negotiated on the SAME chain + tokenNetwork and whose watermark\n * does not show it closed/settled. Returns the resumed record, if any.\n */\n private async resumeRecordedChannel(\n map: ChannelMapStore,\n candidates: ChannelMapRecord[],\n internals: ChannelInternals,\n // The client this leg pays on. Deposit bookkeeping must read from the\n // SAME client that tracks the channel — reading the publish client for a\n // store channel reports \"is not being tracked\" and records a deposit of\n // undefined against a real, funded channel.\n client: ToonClientLike\n ): Promise<ChannelMapRecord | undefined> {\n if (candidates.length === 0) return undefined;\n const cm = internals.channelManager;\n if (!cm?.trackChannel || !cm.peerChannels) {\n this.warn(\n 'rig: a recorded channel exists but the client does not expose ' +\n 'channel internals to resume it — a fresh channel may be opened'\n );\n return undefined;\n }\n\n for (const record of candidates) {\n // The peer must still be negotiated on the recorded chain/tokenNetwork;\n // a rotated peer identity or re-negotiated settlement gets a fresh\n // channel (recorded under its own key) instead of stale claims.\n const negotiation = internals.peerNegotiations?.get(record.peerId);\n if (\n !negotiation ||\n negotiation.chain !== record.chain ||\n (negotiation.tokenNetwork ?? '') !== record.tokenNetwork\n ) {\n continue;\n }\n\n // Never resume a channel the withdraw flow already closed/settled.\n const watermark = map.readWatermark(record.channelId);\n if (\n watermark?.closedAt !== undefined ||\n watermark?.settledAt !== undefined\n ) {\n continue;\n }\n if (!watermark) {\n // Fresh channels are seeded at record time, so a missing entry means\n // the watermark store was lost. Resuming from nonce 0 fails SAFE: a\n // regressed cumulative claim is rejected by the peer (no double\n // spend), but warn so the failure is diagnosable.\n this.warn(\n `rig: resuming channel ${record.channelId} with no local claim ` +\n `watermark (${map.watermarkPath}) — if this channel was claimed ` +\n 'against before, the peer will reject the stale claims; remove ' +\n `its entry from ${map.mapPath} to open a fresh channel instead`\n );\n }\n\n // trackChannel rehydrates nonce/cumulative from the watermark store;\n // seeding peerChannels makes ensureChannel/openChannel reuse the id —\n // but ONLY under the composite binding key it looks up.\n cm.trackChannel(record.channelId, record.context);\n cm.peerChannels.set(\n channelBindingKey(record.peerId, record.chain, record.tokenNetwork),\n record.channelId\n );\n\n // Persisted channel state omits the on-chain deposit — re-read it so\n // fee/balance accounting is right (EVM only; mirrors the daemon).\n // #279 latency trim: skipped when the record already carries a\n // depositTotal — deposits only change through rig's own flows (open\n // --deposit / a later resume), which update the record; this is\n // display/accounting state, not the claim watermark, so a stale value\n // can never double-spend (the peer enforces the cumulative claim).\n if (\n record.context.chainType === 'evm' &&\n record.depositTotal === undefined &&\n client.rehydrateChannelDeposit\n ) {\n try {\n const deposit = await client.rehydrateChannelDeposit(\n record.channelId,\n {\n chain: record.chain,\n tokenNetworkAddress: record.context.tokenNetworkAddress,\n }\n );\n if (deposit !== undefined) {\n map.touch(recordKey(record), {\n depositTotal: deposit.toString(),\n });\n }\n } catch (err) {\n this.warn(\n `rig: deposit re-read for resumed channel ${record.channelId} ` +\n `failed: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n return record;\n }\n return undefined;\n }\n\n /** Record a freshly opened channel (+ seed its claim watermark at 0/0). */\n private recordOpenedChannel(\n map: ChannelMapStore,\n internals: ChannelInternals,\n identity: string,\n destination: string,\n channelId: string,\n // The client this leg pays on — the deposit must be read from the client\n // that actually tracks the channel, not from whichever one happens to be\n // the publish client.\n client: ToonClientLike\n ): void {\n // `peerChannels` is keyed by the COMPOSITE binding key, not the peer id\n // (toon-client#489) — read the peer id back out of it.\n let peerId: string | undefined;\n for (const [key, channel] of internals.channelManager?.peerChannels ??\n []) {\n if (channel === channelId) {\n peerId = peerIdFromBindingKey(key);\n break;\n }\n }\n const negotiation =\n peerId !== undefined\n ? internals.peerNegotiations?.get(peerId)\n : undefined;\n if (peerId === undefined || !negotiation) {\n this.warn(\n `rig: opened channel ${channelId} but could not record the ` +\n 'peer→channel mapping (client does not expose negotiation ' +\n 'internals) — the NEXT invocation may open another channel'\n );\n return;\n }\n\n let depositTotal: bigint | undefined;\n try {\n depositTotal = client.getChannelDepositTotal?.(channelId);\n } catch {\n // deposit unknown — recorded without it; a later resume re-reads it\n }\n\n map.record({\n channelId,\n peerId,\n identity,\n destination,\n chain: negotiation.chain,\n tokenNetwork: negotiation.tokenNetwork ?? '',\n // Context mirrors the daemon's persistApexChannel shape — exactly what\n // trackChannel needs on resume.\n context: {\n chainType: negotiation.chainType,\n chainId:\n typeof negotiation.chainId === 'number' ? negotiation.chainId : 0,\n tokenNetworkAddress: negotiation.tokenNetwork ?? '',\n ...(negotiation.tokenAddress\n ? { tokenAddress: negotiation.tokenAddress }\n : {}),\n recipient: negotiation.settlementAddress,\n },\n ...(depositTotal !== undefined && depositTotal > 0n\n ? { depositTotal: depositTotal.toString() }\n : {}),\n });\n // Seed nonce 0 / cumulative 0 so a later resume can tell \"never claimed\n // against\" apart from \"watermark lost\" (which only fails claim-rejected).\n map.seedWatermark(channelId);\n }\n\n /** Release the identity lock and stop the embedded client (if we own it). */\n async stop(): Promise<void> {\n this.lock?.release();\n this.lock = undefined;\n this.readyPromise = undefined;\n this.clientReadyPromise = undefined;\n this.channelId = undefined;\n // Never stop a client that was never started (`ToonClient.stop()` throws\n // INVALID_STATE) — e.g. after a free `rig balance` read (#263).\n // The store leg owns its client outright (it is only ever built here),\n // so it is stopped regardless of `ownsClient` — that flag describes the\n // caller-injectable publish client.\n if (this.storeClient) {\n const storeClient = this.storeClient;\n this.storeClient = undefined;\n this.storeLegPromise = undefined;\n this.storeChannelId = undefined;\n await storeClient.stop();\n }\n if (this.ownsClient && this.client.isStarted?.() !== false) {\n await this.client.stop();\n }\n }\n\n // ── Money lifecycle (#263) ──────────────────────────────────────────────────\n\n /**\n * Explicit `rig channel open`: the SAME resume-or-open path the lazy paid\n * writes use (guard → start → resume the recorded channel or open + record\n * a fresh one), surfaced with a receipt — plus an optional extra collateral\n * deposit on top of the open/resume.\n */\n async openChannelExplicit(opts?: {\n /** Extra collateral to deposit AFTER the open/resume (base units). */\n deposit?: bigint;\n }): Promise<ChannelOpenOutcome> {\n await this.start();\n const channelId = this.requireChannel();\n const identity = this.client.getPublicKey();\n const destination = this.channelAnchor ?? '';\n const record =\n this.channelMap && this.channelAnchor\n ? this.channelMap\n .listFor(identity, this.channelAnchor)\n .find((r) => r.channelId === channelId)\n : undefined;\n\n const outcome: ChannelOpenOutcome = {\n channelId,\n resumed: this.lastOpenResumed,\n destination,\n ...(record ? { chain: record.chain, peerId: record.peerId } : {}),\n ...(record?.depositTotal !== undefined\n ? { depositTotal: record.depositTotal }\n : {}),\n };\n\n if (opts?.deposit !== undefined && opts.deposit > 0n) {\n if (!this.client.depositToChannel) {\n throw new StandalonePublishError(\n 'this client build does not support channel deposits ' +\n '(depositToChannel is unavailable)'\n );\n }\n const deposited = await this.client.depositToChannel(\n channelId,\n opts.deposit\n );\n outcome.depositAdded = opts.deposit.toString();\n outcome.depositTotal = deposited.depositTotal;\n if (deposited.txHash) outcome.depositTxHash = deposited.txHash;\n if (record) {\n this.channelMap?.touch(recordKey(record), {\n depositTotal: deposited.depositTotal,\n });\n }\n }\n return outcome;\n }\n\n /**\n * Adopt a RECORDED channel into the running client so on-chain close/\n * settle/deposit can act on it: `trackChannel` rehydrates the claim\n * watermark + withdraw timers from channels.json, `peerChannels` binds the\n * peer, and the on-chain client's context cache is re-seeded (it only\n * learns context at open time — a resumed channel would otherwise throw\n * \"No on-chain context\").\n */\n private adoptRecordedChannel(record: ChannelMapRecord): void {\n const internals = channelInternals(this.client);\n const cm = internals.channelManager;\n if (cm?.trackChannel && cm.peerChannels) {\n cm.trackChannel(record.channelId, record.context);\n cm.peerChannels.set(\n channelBindingKey(record.peerId, record.chain, record.tokenNetwork),\n record.channelId\n );\n }\n const contextCache = internals.onChainChannelClient?.channelContext;\n if (contextCache && !contextCache.has(record.channelId)) {\n contextCache.set(record.channelId, {\n chain: record.chain,\n tokenNetworkAddress: record.context.tokenNetworkAddress,\n ...(record.context.tokenAddress\n ? { tokenAddress: record.context.tokenAddress }\n : {}),\n });\n }\n }\n\n /**\n * Close a recorded channel: starts the on-chain settlement challenge\n * window. The client persists `closedAt`/`settleableAt` into the claim\n * watermark store, which is exactly where `rig channel list`/`balance`\n * derive the closing/settleable/settled status from. Guard + client start,\n * but NEVER a channel open ({@link startClientOnly}).\n */\n async closeRecordedChannel(\n record: ChannelMapRecord\n ): Promise<ChannelCloseOutcome> {\n await this.startClientOnly();\n if (!this.client.closeChannel) {\n throw new StandalonePublishError(\n 'this client build does not support closing channels ' +\n '(closeChannel is unavailable)'\n );\n }\n this.adoptRecordedChannel(record);\n const result = await this.client.closeChannel(record.channelId);\n this.channelMap?.touch(recordKey(record));\n return result;\n }\n\n /**\n * Settle a recorded channel after its challenge window — releases the\n * remaining collateral. The client enforces the `now >= settleableAt` time\n * guard BEFORE spending gas (a too-early call throws its retryable\n * SettleTooEarlyError) and persists `settledAt` into the watermark store.\n */\n async settleRecordedChannel(\n record: ChannelMapRecord\n ): Promise<ChannelSettleOutcome> {\n await this.startClientOnly();\n if (!this.client.settleChannel) {\n throw new StandalonePublishError(\n 'this client build does not support settling channels ' +\n '(settleChannel is unavailable)'\n );\n }\n this.adoptRecordedChannel(record);\n const result = await this.client.settleChannel(record.channelId);\n this.channelMap?.touch(recordKey(record));\n return result;\n }\n\n /**\n * The full multi-chain wallet view (#299) for the embedded identity — native\n * coin + configured tokens (USDC) per chain — a FREE read on the UNSTARTED\n * client (no nonce guard, no uplink, no channel). The client derives the\n * Solana/Mina addresses from the mnemonic on demand, so ALL configured chains\n * appear even before a start. Best-effort per chain (an unreachable RPC yields\n * an `unreadable` chain, not a failure). `fallback` supplies Solana/Mina RPC\n * defaults (e.g. the network preset) for chains the config has no channel for.\n */\n async readWalletChainBalances(\n fallback?: WalletViewFallback\n ): Promise<WalletChainBalanceInfo[]> {\n if (!this.client.getWalletBalances) return [];\n return await this.client.getWalletBalances(fallback);\n }\n\n // ── Publisher ─────────────────────────────────────────────────────────────\n\n /**\n * Fee rates for `planPush` estimation, both flat per packet (ADR 0020),\n * with the announced route prices folded in so the ESTIMATE always equals\n * the CLAIMS: `eventFee` is returned pre-floored at the publish route price,\n * and `uploadFee` IS the store route price.\n */\n getFeeRates(): Promise<FeeRates> {\n return Promise.resolve({\n uploadFee: this.uploadFee(),\n eventFee: this.effectiveEventFee(),\n });\n }\n\n /**\n * The flat per-event claim: `eventFee`, floored at the publish route's\n * announced price (the connector rejects a claim below it — F06).\n */\n private effectiveEventFee(): bigint {\n const min = this.routePrices?.publish ?? 0n;\n return this.eventFee > min ? this.eventFee : min;\n }\n\n /**\n * The per-upload claim: the store route's flat price, whatever the object's\n * size. Byte-proportional pricing has no successor (ADR 0020) — the route\n * table is the price list — and the connector charges this figure whether\n * the body is 100 bytes or 100 KB.\n */\n private uploadFee(): bigint {\n return this.routePrices?.store ?? 0n;\n }\n\n /**\n * Upload one git object as a kind:5094 store write (Git-SHA/Git-Type/Repo\n * tagged — the proven seed-pipeline shape), signing one balance-proof claim\n * for the store route's flat price\n * ({@link StandalonePublisherOptions.routePrices}) — ADR 0020 prices a\n * packet per handler, not per byte.\n *\n * #368: a blob's `Content-Type` is derived from its path extension (else\n * octet-stream) and sent in the `output` tag, which the store forwards onto\n * the Arweave upload's Content-Type — so a gateway serves `index.html` as\n * `text/html`, not `application/octet-stream`. Non-blob objects (trees,\n * commits, tags) are never served as files, so they stay octet-stream.\n */\n async uploadGitObject(upload: GitObjectUpload): Promise<UploadReceipt> {\n if (upload.body.length > MAX_OBJECT_SIZE) {\n throw new StandalonePublishError(\n `git object ${upload.sha} exceeds the ${MAX_OBJECT_SIZE}-byte limit: ${upload.body.length} bytes`\n );\n }\n await this.start();\n const leg = await this.storeLeg();\n\n const contentType =\n upload.type === 'blob'\n ? contentTypeForPath(upload.path)\n : DEFAULT_CONTENT_TYPE;\n\n const fee = this.uploadFee();\n const event = leg.client.signEvent({\n kind: 5094,\n content: '',\n created_at: nowSeconds(),\n tags: [\n ['i', upload.body.toString('base64'), 'blob'],\n ['bid', fee.toString(), 'usdc'],\n ['output', contentType],\n ['Git-SHA', upload.sha],\n ['Git-Type', upload.type],\n ['Repo', upload.repoId],\n ],\n });\n\n const claim = await leg.client.signBalanceProof(leg.channelId, fee);\n const result = await leg.client.publishEvent(event, {\n ...(this.storeDestination ? { destination: this.storeDestination } : {}),\n claim,\n ilpAmount: fee,\n // The store backend serves POST /store (not the relay's /write).\n proxyPath: '/store',\n });\n if (!result.success) {\n throw new StandalonePublishError(\n `git-object upload rejected (${upload.sha}): ${result.error ?? 'store rejected the write'}`\n );\n }\n if (!result.response) {\n throw new StandalonePublishError(\n `git-object upload FULFILL carried no sealed response (${upload.sha}); expected the Arweave tx ID`\n );\n }\n return { txId: extractArweaveTxId(result.response), feePaid: fee };\n }\n\n /**\n * Upload one raw blob (no git envelope) with an explicit `Content-Type`\n * (#368) — a kind:5094 store write carrying just the `i`/`bid`/`output`\n * tags (and an optional `Repo` provenance tag), NOT the Git-SHA/Git-Type\n * tags. Without those the store keeps the bytes verbatim instead of\n * re-deriving a git envelope, which is exactly what the ar.io path manifest\n * needs. One balance-proof claim for the store route's flat price.\n */\n async uploadBlob(upload: BlobUpload): Promise<UploadReceipt> {\n if (upload.body.length > MAX_OBJECT_SIZE) {\n throw new StandalonePublishError(\n `blob exceeds the ${MAX_OBJECT_SIZE}-byte limit: ${upload.body.length} bytes`\n );\n }\n await this.start();\n const leg = await this.storeLeg();\n\n const fee = this.uploadFee();\n const event = leg.client.signEvent({\n kind: 5094,\n content: '',\n created_at: nowSeconds(),\n tags: [\n ['i', upload.body.toString('base64'), 'blob'],\n ['bid', fee.toString(), 'usdc'],\n ['output', upload.contentType],\n ...(upload.repoId ? [['Repo', upload.repoId]] : []),\n ],\n });\n\n const claim = await leg.client.signBalanceProof(leg.channelId, fee);\n const result = await leg.client.publishEvent(event, {\n ...(this.storeDestination ? { destination: this.storeDestination } : {}),\n claim,\n ilpAmount: fee,\n proxyPath: '/store',\n });\n if (!result.success) {\n throw new StandalonePublishError(\n `blob upload rejected: ${result.error ?? 'store rejected the write'}`\n );\n }\n if (!result.response) {\n throw new StandalonePublishError(\n 'blob upload FULFILL carried no sealed response; expected the Arweave tx ID'\n );\n }\n return { txId: extractArweaveTxId(result.response), feePaid: fee };\n }\n\n /**\n * Sign the event with the embedded identity and pay-to-publish it through\n * the relay write route, one claim for the flat per-event fee.\n *\n * `relayUrls` is the interface's plural forward-compat surface (parked\n * #84): the standalone impl routes over ILP to its single configured\n * publish destination, so more than one relay is refused rather than\n * silently half-published.\n */\n async publishEvent(\n event: UnsignedEvent,\n relayUrls: string[]\n ): Promise<PublishReceipt> {\n if (relayUrls.length > 1) {\n throw new StandalonePublishError(\n `multi-relay publish is not supported yet (got ${relayUrls.length} relays) — the standalone publisher routes to a single relay destination (#84 parked)`\n );\n }\n await this.start();\n const channelId = this.requireChannel();\n\n const signed = this.client.signEvent(event);\n const fee = this.effectiveEventFee();\n const claim = await this.client.signBalanceProof(channelId, fee);\n const result = await this.client.publishEvent(signed, {\n ...(this.publishDestination\n ? { destination: this.publishDestination }\n : {}),\n claim,\n ilpAmount: fee,\n });\n if (!result.success) {\n throw new StandalonePublishError(\n `publish rejected (kind ${event.kind}): ${result.error ?? 'relay rejected the event'}`\n );\n }\n return { eventId: result.eventId ?? signed.id, feePaid: fee };\n }\n\n private requireChannel(): string {\n if (!this.channelId) {\n throw new StandalonePublishError(\n 'no payment channel open — start() did not complete'\n );\n }\n return this.channelId;\n }\n\n /**\n * The client + channel a STORE write must be paid on.\n *\n * With no `storeClientConfig` this is the publish leg unchanged — the\n * single-node case, where the store terminates on the connector that\n * already holds the channel. With one, the store node is a different\n * connector holding a different channel, and paying it from the publish\n * channel is precisely the `F01 - claim rejected: names a channel this\n * connector has no record of` failure this exists to prevent.\n *\n * Built on FIRST STORE WRITE, not in `start()`: `rig push` with nothing to\n * upload, and every read-only command, should not open a second on-chain\n * channel (gas) for a leg they never use. Memoised on the promise so\n * concurrent uploads share one open rather than racing two.\n */\n private async storeLeg(): Promise<StoreLeg> {\n const injected = this.injectedStoreClient;\n const config = this.storeClientConfig;\n if (!config && !injected) {\n return { client: this.client, channelId: this.requireChannel() };\n }\n this.storeLegPromise ??= (async (): Promise<StoreLeg> => {\n const client = injected ?? new ToonClient(config as ToonClientConfig);\n this.storeClient = client;\n await client.start();\n // The store node is a distinct peer: its channel is recorded under the\n // store destination as its own anchor, so `rig channel list/close/\n // settle` enumerate it alongside the publish channel instead of it\n // becoming invisible collateral.\n const channelId = await this.openOrResumeChannelOn(\n client,\n this.storeDestination\n );\n this.storeChannelId = channelId;\n return { client, channelId };\n })();\n return this.storeLegPromise;\n }\n}\n\nfunction nowSeconds(): number {\n return Math.floor(Date.now() / 1000);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/standalone/channel-map.ts","../src/standalone/nonce-guard.ts","../src/mime.ts"],"sourcesContent":["/**\n * Peer→channel map for the STANDALONE embedded publisher (#262).\n *\n * Why this exists: `@toon-protocol/client`'s `ChannelManager` persists the\n * off-chain nonce/cumulative-claim watermark (its `ChannelStore`,\n * `channels.json`, keyed by channelId) but keeps the peer→channelId mapping\n * ONLY in memory. So every standalone CLI invocation used to open (and fund)\n * a FRESH on-chain channel — the #260 fresh-outsider e2e stranded five\n * deposits across five commands. This store remembers WHICH channel the\n * identity holds with each peer, keyed by\n * `identity pubkey | ILP anchor (peer/apex destination) | chain | tokenNetwork`,\n * so the next invocation resumes it via `ChannelManager.trackChannel` (which\n * rehydrates the nonce watermark from `channels.json`) with zero on-chain\n * writes.\n *\n * It is the standalone twin of the daemon's\n * `packages/client-mcp/src/daemon/apex-channel-store.ts` — same record shape\n * (channelId + the chain context `trackChannel` needs), extended with the\n * identity pubkey (rig identities come from an env/`.env` precedence chain,\n * so one state dir can serve several identities) and the tokenNetwork.\n * `@toon-protocol/rig` must not import `@toon-protocol/client-mcp` (that\n * package depends on this one — circular), hence the twin; keep the\n * semantics in sync.\n *\n * CONCURRENCY: writes happen only from paid commands, which already hold the\n * per-identity advisory lockfile (./nonce-guard.ts `NonceLock`) for their\n * whole lifetime — the same guard that serializes the claim watermark also\n * serializes this file for one identity. `rig channel list` reads only.\n *\n * CORRUPTION: an unreadable/invalid map file is a hard\n * {@link ChannelMapCorruptError} — surfaced BEFORE any on-chain open — never\n * an empty fallback. Falling back to \"no channels\" would silently open (and\n * fund) a duplicate channel, which is exactly the #262 bug.\n *\n * This module is dependency-light on purpose (node:fs only): the free\n * `rig channel list` command reads it without the optional\n * `@toon-protocol/client` peer dependency installed.\n */\n\nimport { mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { dirname, join } from 'node:path';\n\n// ---------------------------------------------------------------------------\n// Paths (TOON_CLIENT_HOME conventions — see nonce-guard.ts module doc)\n// ---------------------------------------------------------------------------\n\n/** Map filename under the shared client state dir. */\nexport const RIG_CHANNEL_MAP_FILENAME = 'rig-channels.json';\n\n/**\n * Resolve the two channel-state files under `TOON_CLIENT_HOME` (default\n * `~/.toon-client`): the rig peer→channel map, and the client's nonce\n * watermark store (`config.json`'s `channelStorePath`, default\n * `<dir>/channels.json` — the same resolution `cli/standalone-mode.ts` feeds\n * the embedded ToonClient).\n */\nexport function resolveChannelPaths(env: NodeJS.ProcessEnv): {\n mapPath: string;\n watermarkPath: string;\n} {\n const dir = env['TOON_CLIENT_HOME'] ?? join(homedir(), '.toon-client');\n let configured: string | undefined;\n try {\n const raw = readFileSync(join(dir, 'config.json'), 'utf8');\n const parsed = JSON.parse(raw) as { channelStorePath?: unknown };\n if (typeof parsed.channelStorePath === 'string') {\n configured = parsed.channelStorePath;\n }\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {\n throw new Error(\n `failed to read client config at ${join(dir, 'config.json')}: ` +\n `${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n return {\n mapPath: join(dir, RIG_CHANNEL_MAP_FILENAME),\n watermarkPath: configured ?? join(dir, 'channels.json'),\n };\n}\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Chain context `ChannelManager.trackChannel` needs to resume a channel\n * (same shape the daemon's apex-channel-store persists).\n */\nexport interface PersistedChannelContext {\n chainType: string;\n chainId: number;\n tokenNetworkAddress: string;\n tokenAddress?: string;\n /** Counterparty settlement address (required for Solana/Mina proofs). */\n recipient?: string;\n}\n\n/** One persisted peer→channel binding. */\nexport interface ChannelMapRecord {\n /** On-chain payment channel id. */\n channelId: string;\n /** Registered peer id the negotiation was keyed by (`peerNegotiations`). */\n peerId: string;\n /** Hex Nostr pubkey of the identity that opened the channel. */\n identity: string;\n /** ILP anchor destination the channel was opened against (peer/apex). */\n destination: string;\n /** Negotiated settlement chain, e.g. `evm:31337`. */\n chain: string;\n /** TokenNetwork contract address ('' when the peer announced none). */\n tokenNetwork: string;\n /** Context for `trackChannel` on resume. */\n context: PersistedChannelContext;\n /** On-chain deposit total (base units, string), when known. */\n depositTotal?: string;\n /** ISO timestamps. */\n openedAt: string;\n lastUsedAt: string;\n}\n\n/** The composite key a record is stored under. */\nexport interface ChannelMapKey {\n identity: string;\n destination: string;\n chain: string;\n tokenNetwork: string;\n}\n\n/**\n * One entry of the client's nonce-watermark store (`channels.json`) —\n * format DUPLICATED from `@toon-protocol/client`'s `JsonFileChannelStore`\n * (`packages/client/src/channel/ChannelStore.ts`); keep in sync.\n */\nexport interface WatermarkEntry {\n nonce: number;\n /** Cumulative claimed amount, base units (string-encoded bigint). */\n cumulativeAmount: string;\n /** Withdraw-flow timers, string-encoded unix SECONDS. */\n closedAt?: string;\n settleableAt?: string;\n settledAt?: string;\n}\n\n/** The peer→channel map file is unreadable or malformed. */\nexport class ChannelMapCorruptError extends Error {\n constructor(\n public readonly path: string,\n detail: string\n ) {\n super(\n `channel state file ${path} is corrupt (${detail}) — refusing to ` +\n 'continue: proceeding would silently open (and fund) a duplicate ' +\n 'on-chain channel. Fix or remove the file; removing it makes rig ' +\n 'forget which channels it holds (existing deposits stay locked ' +\n 'on-chain until settled).'\n );\n this.name = 'ChannelMapCorruptError';\n }\n}\n\n// ---------------------------------------------------------------------------\n// Store\n// ---------------------------------------------------------------------------\n\ninterface MapFile {\n version: 1;\n channels: Record<string, ChannelMapRecord>;\n}\n\nfunction keyOf(key: ChannelMapKey): string {\n return `${key.identity}|${key.destination}|${key.chain}|${key.tokenNetwork}`;\n}\n\n/** The key fields of a full record. */\nexport function recordKey(record: ChannelMapRecord): ChannelMapKey {\n return {\n identity: record.identity,\n destination: record.destination,\n chain: record.chain,\n tokenNetwork: record.tokenNetwork,\n };\n}\n\nfunction isContext(v: unknown): v is PersistedChannelContext {\n if (typeof v !== 'object' || v === null) return false;\n const c = v as Record<string, unknown>;\n return (\n typeof c['chainType'] === 'string' &&\n typeof c['chainId'] === 'number' &&\n typeof c['tokenNetworkAddress'] === 'string'\n );\n}\n\nfunction isRecord(v: unknown): v is ChannelMapRecord {\n if (typeof v !== 'object' || v === null) return false;\n const r = v as Record<string, unknown>;\n return (\n typeof r['channelId'] === 'string' &&\n typeof r['peerId'] === 'string' &&\n typeof r['identity'] === 'string' &&\n typeof r['destination'] === 'string' &&\n typeof r['chain'] === 'string' &&\n typeof r['tokenNetwork'] === 'string' &&\n isContext(r['context'])\n );\n}\n\nexport interface ChannelMapStoreOptions {\n /** The rig peer→channel map file (`rig-channels.json`). */\n mapPath: string;\n /** The client's nonce-watermark store (`channels.json`). */\n watermarkPath: string;\n}\n\n/**\n * File-backed peer→channel map + read/seed access to the client's nonce\n * watermark store. Synchronous I/O (matches the client's `ChannelStore`\n * surface); see the module doc for the locking and corruption contracts.\n */\nexport class ChannelMapStore {\n readonly mapPath: string;\n readonly watermarkPath: string;\n\n constructor(options: ChannelMapStoreOptions) {\n this.mapPath = options.mapPath;\n this.watermarkPath = options.watermarkPath;\n }\n\n /** All recorded channels. @throws {ChannelMapCorruptError} */\n list(): ChannelMapRecord[] {\n return Object.values(this.readMap().channels);\n }\n\n /**\n * Recorded channels for one (identity, destination) pair — the resume\n * candidates for a paid command. @throws {ChannelMapCorruptError}\n */\n listFor(identity: string, destination: string): ChannelMapRecord[] {\n return this.list().filter(\n (r) => r.identity === identity && r.destination === destination\n );\n }\n\n /**\n * Record a (freshly opened) channel. Overwrites any previous record under\n * the same (identity, destination, chain, tokenNetwork) key — the old\n * channel is closed/stale by then; its claim watermark stays in the\n * watermark store.\n */\n record(\n record: Omit<ChannelMapRecord, 'openedAt' | 'lastUsedAt'> &\n Partial<Pick<ChannelMapRecord, 'openedAt' | 'lastUsedAt'>>\n ): void {\n const now = new Date().toISOString();\n const full: ChannelMapRecord = {\n ...record,\n openedAt: record.openedAt ?? now,\n lastUsedAt: record.lastUsedAt ?? now,\n };\n const data = this.readMap();\n data.channels[keyOf(recordKey(full))] = full;\n this.writeMap(data);\n }\n\n /**\n * Bump a record's `lastUsedAt` (and optionally its known on-chain deposit)\n * after resuming it. Unknown keys are a no-op.\n */\n touch(key: ChannelMapKey, update?: { depositTotal?: string }): void {\n const data = this.readMap();\n const existing = data.channels[keyOf(key)];\n if (!existing) return;\n existing.lastUsedAt = new Date().toISOString();\n if (update?.depositTotal !== undefined) {\n existing.depositTotal = update.depositTotal;\n }\n this.writeMap(data);\n }\n\n /**\n * Read one channel's nonce-watermark entry from the client's\n * `channels.json` (undefined when the file or entry is missing).\n * @throws {ChannelMapCorruptError} when the watermark file is unreadable.\n */\n readWatermark(channelId: string): WatermarkEntry | undefined {\n return this.readWatermarkFile()[channelId];\n }\n\n /**\n * Seed a fresh channel's watermark entry (`nonce 0, cumulative 0`) so a\n * later resume can tell \"never claimed against\" apart from \"watermark\n * lost\". Never overwrites an existing entry.\n */\n seedWatermark(channelId: string): void {\n const data = this.readWatermarkFile();\n if (data[channelId]) return;\n data[channelId] = { nonce: 0, cumulativeAmount: '0' };\n mkdirSync(dirname(this.watermarkPath), { recursive: true });\n writeFileSync(\n this.watermarkPath,\n JSON.stringify(data, null, 2),\n 'utf-8'\n );\n }\n\n // ── file I/O ───────────────────────────────────────────────────────────────\n\n private readMap(): MapFile {\n let raw: string;\n try {\n raw = readFileSync(this.mapPath, 'utf8');\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n return { version: 1, channels: {} };\n }\n throw new ChannelMapCorruptError(\n this.mapPath,\n err instanceof Error ? err.message : String(err)\n );\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n throw new ChannelMapCorruptError(\n this.mapPath,\n `invalid JSON: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n if (\n typeof parsed !== 'object' ||\n parsed === null ||\n (parsed as { version?: unknown }).version !== 1 ||\n typeof (parsed as { channels?: unknown }).channels !== 'object' ||\n (parsed as { channels?: unknown }).channels === null\n ) {\n throw new ChannelMapCorruptError(\n this.mapPath,\n 'expected { \"version\": 1, \"channels\": { … } }'\n );\n }\n const channels = (parsed as { channels: Record<string, unknown> })\n .channels;\n for (const [key, value] of Object.entries(channels)) {\n if (!isRecord(value)) {\n throw new ChannelMapCorruptError(\n this.mapPath,\n `entry ${JSON.stringify(key)} is missing required fields`\n );\n }\n }\n return parsed as MapFile;\n }\n\n private writeMap(data: MapFile): void {\n mkdirSync(dirname(this.mapPath), { recursive: true });\n writeFileSync(this.mapPath, JSON.stringify(data, null, 2), {\n mode: 0o600,\n });\n }\n\n private readWatermarkFile(): Record<string, WatermarkEntry> {\n let raw: string;\n try {\n raw = readFileSync(this.watermarkPath, 'utf8');\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return {};\n throw new ChannelMapCorruptError(\n this.watermarkPath,\n err instanceof Error ? err.message : String(err)\n );\n }\n try {\n return JSON.parse(raw) as Record<string, WatermarkEntry>;\n } catch (err) {\n throw new ChannelMapCorruptError(\n this.watermarkPath,\n `invalid JSON: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Status derivation\n// ---------------------------------------------------------------------------\n\n/**\n * Where a channel sits in the withdraw journey, from its watermark timers —\n * mirrors `ChannelManager.getChannelCloseState`. A missing entry reads as\n * `open` (recorded channels are seeded at open time; a lost watermark file\n * surfaces separately as unknown claim state).\n */\nexport function channelStatus(\n entry: WatermarkEntry | undefined,\n nowSec: number = Math.floor(Date.now() / 1000)\n): 'open' | 'closing' | 'settleable' | 'settled' {\n if (!entry || entry.closedAt === undefined) return 'open';\n if (entry.settledAt !== undefined) return 'settled';\n if (\n entry.settleableAt !== undefined &&\n BigInt(nowSec) >= BigInt(entry.settleableAt)\n ) {\n return 'settleable';\n }\n return 'closing';\n}\n","/**\n * Nonce-ownership guard for the STANDALONE embedded Publisher (#228).\n *\n * Why this exists: a payment channel's balance proof is a CUMULATIVE\n * watermark — the ChannelManager auto-increments the nonce and cumulative\n * amount on every `signBalanceProof`. Two writers signing claims on the same\n * channel from separate processes (a running `toon-clientd` daemon plus a\n * standalone embedded client, or two standalone processes) each keep their\n * own cumulative counter, so their claims race: the connector sees\n * non-monotonic watermarks and a re-signed claim can double-charge (the\n * hazard documented in packages/rig-web/tests/e2e/seed/lib/publish.ts).\n *\n * Two independent defenses, both keyed by the Nostr pubkey (one identity =\n * one channel set):\n *\n * 1. Daemon detection — probe the toon-clientd loopback control API\n * (`GET /status`) and REFUSE when it reports the SAME identity. A daemon\n * on a different identity holds different channels and is harmless.\n * Since #279 the CLI's paid WRITE commands never get here with a\n * same-identity daemon up — they delegate to its `/git/*` routes first\n * (`cli/daemon-session.ts`), which achieves the same one-writer goal by\n * handing the watermark to the daemon. This guard remains the backstop\n * for the probe→publish race window and for operations with no daemon\n * route (channel close/settle, explicit open).\n * 2. Advisory lockfile — an exclusive per-pubkey lockfile under the shared\n * `~/.toon-client` state dir so two STANDALONE processes can't race each\n * other (the daemon check only covers the daemon). Stale locks (dead pid)\n * are reclaimed.\n *\n * The daemon port and state-dir conventions are DUPLICATED from\n * `packages/client-mcp/src/daemon/config.ts` (default port 8787 /\n * `TOON_CLIENT_HTTP_PORT`; `~/.toon-client` / `TOON_CLIENT_HOME`).\n * `@toon-protocol/rig` must not depend on `@toon-protocol/client-mcp`\n * (the daemon package depends on this one for the #227 Publisher — the\n * import would be circular), so the constants live here with this note.\n * Keep them in sync.\n */\n\nimport { mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\n\n// ---------------------------------------------------------------------------\n// Shared conventions (duplicated from client-mcp — see module doc)\n// ---------------------------------------------------------------------------\n\n/** Default toon-clientd loopback control API port (client-mcp `httpPort`). */\nexport const DEFAULT_DAEMON_PORT = 8787;\n\n/** Daemon control API port: `TOON_CLIENT_HTTP_PORT` env, else 8787. */\nexport function defaultDaemonPort(): number {\n const env = process.env['TOON_CLIENT_HTTP_PORT'];\n const parsed = env ? Number(env) : NaN;\n return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_DAEMON_PORT;\n}\n\n/**\n * Shared client state dir: `TOON_CLIENT_HOME` env, else `~/.toon-client` —\n * the same dir the daemon keeps its config/channel stores in, so daemon and\n * standalone processes agree on where the advisory locks live.\n */\nexport function defaultLockDir(): string {\n return process.env['TOON_CLIENT_HOME'] ?? join(homedir(), '.toon-client');\n}\n\n// ---------------------------------------------------------------------------\n// Force-standalone override\n// ---------------------------------------------------------------------------\n\n/**\n * Env var (and `--standalone` / `--no-daemon` flag) that makes rig ALWAYS run\n * embedded, bypassing a running `toon-clientd` entirely — both the #279\n * daemon-delegation fast path ({@link ../cli/daemon-session.ts}) and the\n * same-identity refusal ({@link checkDaemonIdentity}, Guard 1 below).\n *\n * SAFETY: this only disables the DAEMON detection. The exclusive per-pubkey\n * advisory lockfile (Guard 2, {@link NonceLock}) still runs, so two forced\n * STANDALONE rig processes cannot race the channel's cumulative-claim\n * watermark. What it CANNOT protect against is a running daemon on the SAME\n * identity that is ALSO signing claims concurrently — the daemon does not\n * take the lockfile. So the durable, race-free way to \"always standalone\" is\n * to STOP the daemon (its pid is in `~/.toon-client/daemon.pid`); this\n * override is for the common case where the daemon is idle/in-the-way and you\n * just want rig to sign for itself.\n */\nexport const RIG_STANDALONE_ENV = 'RIG_STANDALONE';\n\n/**\n * True when {@link RIG_STANDALONE_ENV} is set to a truthy value\n * (`1`/`true`/`yes`/`on`, case-insensitive). Any other value — including an\n * explicit `0`/`false`/empty — leaves the default daemon-aware behaviour.\n */\nexport function standaloneForced(env: NodeJS.ProcessEnv): boolean {\n const raw = env[RIG_STANDALONE_ENV];\n if (raw === undefined) return false;\n const v = raw.trim().toLowerCase();\n return v === '1' || v === 'true' || v === 'yes' || v === 'on';\n}\n\n// ---------------------------------------------------------------------------\n// Errors\n// ---------------------------------------------------------------------------\n\n/** A running toon-clientd holds the same identity (channel watermark owner). */\nexport class DaemonIdentityConflictError extends Error {\n constructor(\n /** The shared Nostr pubkey (hex). */\n public readonly pubkey: string,\n /** The daemon control API URL that answered. */\n public readonly daemonUrl: string\n ) {\n super(\n `toon-clientd is running with this identity (${pubkey.slice(0, 8)}…) at ` +\n `${daemonUrl} — paid rig writes delegate to it automatically, ` +\n `but this operation has no daemon route (or the daemon appeared ` +\n `mid-run): stop the daemon and re-run. Two writers on one identity ` +\n `would race the payment channel's cumulative-claim watermark ` +\n `(double-charge hazard).`\n );\n this.name = 'DaemonIdentityConflictError';\n }\n}\n\n/** Another standalone process already holds the per-identity lock. */\nexport class StandaloneLockError extends Error {\n constructor(\n public readonly pubkey: string,\n public readonly lockPath: string,\n public readonly holderPid: number\n ) {\n super(\n `another standalone process (pid ${holderPid}) already holds the ` +\n `payment-channel lock for identity ${pubkey.slice(0, 8)}… ` +\n `(${lockPath}) — wait for it to finish or stop it. Two writers on one ` +\n `identity would race the cumulative-claim watermark.`\n );\n this.name = 'StandaloneLockError';\n }\n}\n\n// ---------------------------------------------------------------------------\n// Daemon detection\n// ---------------------------------------------------------------------------\n\nexport interface CheckDaemonOptions {\n /** Control API port (default: `TOON_CLIENT_HTTP_PORT` env, else 8787). */\n port?: number;\n /** Probe timeout, ms (default 1500 — loopback, so fast). */\n timeoutMs?: number;\n /** Inject a fetch implementation (tests). Defaults to global `fetch`. */\n fetchImpl?: typeof fetch;\n}\n\n/**\n * Probe the toon-clientd loopback control API and throw\n * {@link DaemonIdentityConflictError} when a daemon responds on `/status`\n * with `identity.nostrPubkey === pubkey`.\n *\n * Anything short of a positive identity match lets the caller proceed: no\n * listener, a timeout, a non-JSON response (some other local service on the\n * port), or a daemon on a DIFFERENT identity (its channels are keyed to its\n * own pubkey — no shared watermark).\n */\nexport async function checkDaemonIdentity(\n pubkey: string,\n options: CheckDaemonOptions = {}\n): Promise<void> {\n const port = options.port ?? defaultDaemonPort();\n const fetchImpl = options.fetchImpl ?? fetch;\n const url = `http://127.0.0.1:${port}/status`;\n\n let daemonPubkey: string | undefined;\n try {\n const res = await fetchImpl(url, {\n signal: AbortSignal.timeout(options.timeoutMs ?? 1500),\n });\n if (!res.ok) return; // listening, but not a healthy daemon status\n const body = (await res.json()) as {\n identity?: { nostrPubkey?: unknown };\n };\n const candidate = body?.identity?.nostrPubkey;\n if (typeof candidate === 'string') daemonPubkey = candidate;\n } catch {\n // Unreachable / timed out / not JSON → no same-identity daemon detected.\n return;\n }\n\n if (daemonPubkey !== undefined && daemonPubkey === pubkey) {\n throw new DaemonIdentityConflictError(pubkey, url);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Advisory per-identity lockfile\n// ---------------------------------------------------------------------------\n\ninterface LockFileContents {\n pid: number;\n pubkey: string;\n createdAt: string;\n}\n\nexport interface AcquireLockOptions {\n /** Directory the lockfile lives in (default: {@link defaultLockDir}). */\n dir?: string;\n /** Override the recorded pid (tests). Defaults to `process.pid`. */\n pid?: number;\n}\n\n/** True when `pid` refers to a live process we can see. */\nfunction pidAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (err) {\n // EPERM = alive but not ours; ESRCH (and anything else) = not running.\n return (err as NodeJS.ErrnoException).code === 'EPERM';\n }\n}\n\n/**\n * Exclusive advisory lock for one identity's payment-channel watermark.\n *\n * Acquired with an atomic `wx` create of `standalone-<pubkey>.lock` (JSON:\n * pid + pubkey + timestamp) under the shared state dir. A pre-existing lock\n * whose pid is dead (or whose contents are unreadable) is STALE and gets\n * reclaimed; a live holder throws {@link StandaloneLockError}. Released\n * explicitly via {@link release} and best-effort on process exit.\n */\nexport class NonceLock {\n private released = false;\n private readonly exitHandler: () => void;\n\n private constructor(\n public readonly pubkey: string,\n public readonly lockPath: string\n ) {\n this.exitHandler = () => {\n try {\n unlinkSync(this.lockPath);\n } catch {\n // best-effort — the pid check makes a leftover lock reclaimable anyway\n }\n };\n process.once('exit', this.exitHandler);\n }\n\n static async acquire(\n pubkey: string,\n options: AcquireLockOptions = {}\n ): Promise<NonceLock> {\n const dir = options.dir ?? defaultLockDir();\n const pid = options.pid ?? process.pid;\n const lockPath = join(dir, `standalone-${pubkey}.lock`);\n mkdirSync(dir, { recursive: true });\n\n const payload = JSON.stringify(\n {\n pid,\n pubkey,\n createdAt: new Date().toISOString(),\n } satisfies LockFileContents,\n null,\n 2\n );\n\n // Two attempts: initial exclusive create, then one retry after reclaiming\n // a stale lock. A live holder on either attempt is a hard refusal.\n for (let attempt = 0; attempt < 2; attempt++) {\n try {\n writeFileSync(lockPath, payload, { flag: 'wx' });\n return new NonceLock(pubkey, lockPath);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== 'EEXIST') throw err;\n\n let holderPid: number | undefined;\n try {\n const parsed = JSON.parse(\n readFileSync(lockPath, 'utf8')\n ) as Partial<LockFileContents>;\n if (typeof parsed.pid === 'number') holderPid = parsed.pid;\n } catch {\n // Unreadable/corrupt lock → treat as stale.\n }\n\n // Same process re-acquiring (e.g. a retried push in one CLI run) is\n // not a race — the ChannelManager watermark is shared in-process.\n if (\n holderPid !== undefined &&\n holderPid !== pid &&\n pidAlive(holderPid)\n ) {\n throw new StandaloneLockError(pubkey, lockPath, holderPid);\n }\n\n // Stale (dead pid / corrupt / our own pid): reclaim and retry.\n try {\n unlinkSync(lockPath);\n } catch {\n // Lost a reclaim race with another process — the retry's exclusive\n // create settles the winner.\n }\n }\n }\n // Both attempts hit EEXIST → another process is actively (re)creating it.\n throw new StandaloneLockError(pubkey, lockPath, -1);\n }\n\n /** Remove the lockfile and detach the exit hook. Idempotent. */\n release(): void {\n if (this.released) return;\n this.released = true;\n process.removeListener('exit', this.exitHandler);\n try {\n unlinkSync(this.lockPath);\n } catch {\n // already gone — fine\n }\n }\n}\n","/**\n * File-extension → MIME type derivation for permaweb site deploys (#368).\n *\n * Every `rig push` already stores a repo's raw blob bytes on Arweave; tagging\n * each blob upload with a `Content-Type` derived from its path turns a pushed\n * repo into a servable static site (a gateway returns `text/html` for\n * `index.html` instead of `application/octet-stream`). The store forwards the\n * kind:5094 `output` tag to the Turbo upload's `Content-Type` — verified\n * against `@toon-protocol/core`'s `buildBlobStorageRequest` (sets\n * `[\"output\", contentType]`) and its inverse parser (reads the `output` tag\n * back into `contentType`, defaulting to `application/octet-stream`).\n *\n * The map is deliberately small — the common static-site asset extensions —\n * and any unknown or extensionless path falls back to the octet-stream\n * default (the same default the store applies for an absent `output`), so\n * this never guesses wildly.\n */\n\n/** The universal fallback: the store's own default for an absent `output`. */\nexport const DEFAULT_CONTENT_TYPE = 'application/octet-stream';\n\n/**\n * Lowercased extension (no dot) → MIME type. Curated for static sites; extend\n * as real deploys need more. Anything absent falls back to octet-stream.\n */\nconst EXTENSION_MIME: ReadonlyMap<string, string> = new Map([\n // Markup / documents\n ['html', 'text/html'],\n ['htm', 'text/html'],\n ['xml', 'application/xml'],\n ['txt', 'text/plain'],\n ['md', 'text/markdown'],\n ['csv', 'text/csv'],\n ['pdf', 'application/pdf'],\n // Styles / scripts\n ['css', 'text/css'],\n ['js', 'text/javascript'],\n ['mjs', 'text/javascript'],\n ['cjs', 'text/javascript'],\n ['map', 'application/json'],\n ['json', 'application/json'],\n ['wasm', 'application/wasm'],\n // Fonts\n ['woff', 'font/woff'],\n ['woff2', 'font/woff2'],\n ['ttf', 'font/ttf'],\n ['otf', 'font/otf'],\n ['eot', 'application/vnd.ms-fontobject'],\n // Images\n ['png', 'image/png'],\n ['jpg', 'image/jpeg'],\n ['jpeg', 'image/jpeg'],\n ['gif', 'image/gif'],\n ['svg', 'image/svg+xml'],\n ['webp', 'image/webp'],\n ['avif', 'image/avif'],\n ['ico', 'image/x-icon'],\n ['bmp', 'image/bmp'],\n // Media\n ['mp3', 'audio/mpeg'],\n ['mp4', 'video/mp4'],\n ['webm', 'video/webm'],\n ['ogg', 'audio/ogg'],\n ['wav', 'audio/wav'],\n]);\n\n/** The lowercased extension of a path (no leading dot), or `undefined`. */\nfunction extensionOf(path: string): string | undefined {\n const slash = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\\\'));\n const base = slash >= 0 ? path.slice(slash + 1) : path;\n const dot = base.lastIndexOf('.');\n // A leading dot (dotfile like `.gitignore`) or no dot → no usable extension.\n if (dot <= 0) return undefined;\n return base.slice(dot + 1).toLowerCase();\n}\n\n/**\n * Derive a `Content-Type` from a file path's extension. Unknown/extensionless\n * paths (and `undefined`) fall back to {@link DEFAULT_CONTENT_TYPE}.\n */\nexport function contentTypeForPath(path: string | undefined): string {\n if (!path) return DEFAULT_CONTENT_TYPE;\n const ext = extensionOf(path);\n if (ext === undefined) return DEFAULT_CONTENT_TYPE;\n return EXTENSION_MIME.get(ext) ?? DEFAULT_CONTENT_TYPE;\n}\n\n/**\n * Pick a single deterministic upload path for a blob reachable by MULTIPLE\n * paths (the same content committed under different names). The store keys a\n * blob by its content, so it carries ONE `Content-Type`:\n *\n * - all paths agree on a content type → the lexicographically-first path\n * (deterministic across runs);\n * - the paths DISAGREE (e.g. `a.js` and `a.txt` share bytes) → `undefined`,\n * so the caller uploads it as {@link DEFAULT_CONTENT_TYPE} rather than\n * silently favoring one extension over another.\n *\n * An empty list yields `undefined` (octet-stream).\n */\nexport function resolveConflictingPath(paths: string[]): string | undefined {\n if (paths.length === 0) return undefined;\n const sorted = [...paths].sort();\n const types = new Set(sorted.map((p) => contentTypeForPath(p)));\n if (types.size === 1) return sorted[0];\n return undefined;\n}\n"],"mappings":"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","names":["mkdirSync","readFileSync","writeFileSync","homedir","join"]}
|