@toon-protocol/rig 4.7.0 → 5.0.1
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 +31 -25
- package/dist/{chunk-J6Y3U645.js → chunk-2TG4YHEC.js} +59 -41
- package/dist/chunk-2TG4YHEC.js.map +1 -0
- package/dist/{chunk-3IVO2MSZ.js → chunk-4WVH2ITA.js} +3 -3
- package/dist/{chunk-U5A7MHBP.js → chunk-7VIVQHRG.js} +1 -1
- package/dist/chunk-7VIVQHRG.js.map +1 -0
- package/dist/{chunk-XQ2AWNHQ.js → chunk-CHVOXCDV.js} +0 -0
- package/dist/{chunk-XCLUB2CY.js → chunk-LMZSAFAB.js} +44 -15
- package/dist/chunk-LMZSAFAB.js.map +1 -0
- package/dist/{chunk-S3KKQ4IC.js → chunk-QKL53YZS.js} +2 -2
- package/dist/{chunk-UE5TAFEU.js → chunk-ZQGXFFBP.js} +4 -4
- package/dist/chunk-ZQGXFFBP.js.map +1 -0
- package/dist/{ci-serve-T33FCQVT.js → ci-serve-BU7NDT7O.js} +7 -7
- package/dist/cli/rig.js +6 -6
- package/dist/index.js +3 -3
- package/dist/standalone/index.d.ts +109 -74
- package/dist/standalone/index.js +13 -3
- package/dist/{standalone-mode-FY3XVQQK.js → standalone-mode-AT4S5EZG.js} +135 -49
- package/dist/standalone-mode-AT4S5EZG.js.map +1 -0
- package/package.json +2 -2
- package/dist/chunk-J6Y3U645.js.map +0 -1
- package/dist/chunk-U5A7MHBP.js.map +0 -1
- package/dist/chunk-UE5TAFEU.js.map +0 -1
- package/dist/chunk-XCLUB2CY.js.map +0 -1
- package/dist/standalone-mode-FY3XVQQK.js.map +0 -1
- /package/dist/{chunk-3IVO2MSZ.js.map → chunk-4WVH2ITA.js.map} +0 -0
- /package/dist/{chunk-XQ2AWNHQ.js.map → chunk-CHVOXCDV.js.map} +0 -0
- /package/dist/{chunk-S3KKQ4IC.js.map → chunk-QKL53YZS.js.map} +0 -0
- /package/dist/{ci-serve-T33FCQVT.js.map → ci-serve-BU7NDT7O.js.map} +0 -0
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/publisher.ts","../src/standalone/nonce-guard.ts","../src/standalone/nostr-identity.ts"],"sourcesContent":["/**\n * Publisher — the paid-write seam between push planning (this package) and\n * the two transport implementations that follow it in epic #222:\n *\n * 1. daemon (#227): client-mcp ControlClient → toon-clientd loopback\n * /git/* routes, backed by `ClientRunner.publish`/`uploadBlob` (the\n * production paid-publish path — apex channel, signBalanceProof, flat\n * per-event fee).\n * 2. standalone (#228): an embedded ToonClient constructed from a\n * mnemonic, uploading git objects as kind:5094 store writes with\n * Git-SHA/Git-Type/Repo tags (the proven seed-pipeline shape) and\n * publishing NIP-34 events through the relay.\n *\n * The interface is deliberately minimal so both fit:\n *\n * - `uploadGitObject` takes the raw object body (content only — no git\n * envelope header; the store re-derives the SHA envelope from the\n * Git-Type/Git-SHA tags) and returns the Arweave txId plus the fee paid.\n * - `publishEvent` takes an UNSIGNED event template — signing stays with\n * the implementation (the daemon-held key never leaves the daemon; the\n * standalone impl signs with its own keypair). Relay URLs are plural\n * from day one (forward-compat for parked #84/#92; size 1 today).\n * - `getFeeRates` exposes what `planPush` needs for the pre-push estimate:\n * a per-byte upload rate (seed pipeline bids bytes × rate) and a flat\n * per-event publish fee (the daemon charges `apex.feePerEvent` regardless\n * of event size).\n *\n * All fees are bigint in the smallest asset unit (matches `@toon-protocol/client`\n * channel math); HTTP transports serialize them as strings and convert at\n * the boundary.\n */\n\nimport type { UnsignedEvent } from './nip34-events.js';\nimport type { GitObjectType } from './objects.js';\n\n/** A git object queued for upload to Arweave via the paid store path. */\nexport interface GitObjectUpload {\n /** Full 40-hex SHA-1 (over the git envelope — see `hashGitObject`). */\n sha: string;\n type: GitObjectType;\n /** Raw object body (content only, no `<type> <size>\\0` header). */\n body: Buffer;\n /** Repository identifier — becomes the `Repo` tag on the store write. */\n repoId: string;\n /**\n * Path the blob was reached by in the tree (#368), if known. Its extension\n * derives the `Content-Type` sent in the store write's `output` tag so a\n * gateway serves the blob as its real media type instead of\n * `application/octet-stream`. Absent (or a non-blob object) → octet-stream.\n */\n path?: string;\n}\n\n/**\n * A NON-git blob queued for upload to Arweave with an explicit `Content-Type`\n * (#368): the ar.io path manifest that turns a pushed repo into a permaweb\n * site. Unlike {@link GitObjectUpload} it carries no `Git-SHA`/`Git-Type`\n * tags, so the store stores the raw bytes verbatim (no git-envelope\n * re-derivation) — exactly what a manifest needs.\n */\nexport interface BlobUpload {\n /** Raw bytes to store. */\n body: Buffer;\n /** MIME type sent in the store write's `output` tag. */\n contentType: string;\n /** Optional repository identifier for provenance (`Repo` tag). */\n repoId?: string;\n}\n\n/** Receipt for one uploaded git object. */\nexport interface UploadReceipt {\n /** Arweave transaction ID the object is retrievable under. */\n txId: string;\n /** Fee paid for this upload, in the smallest asset unit. */\n feePaid: bigint;\n}\n\n/** Receipt for one published Nostr event. */\nexport interface PublishReceipt {\n /** Event ID as accepted by the relay(s). */\n eventId: string;\n /** Fee paid for this publish, in the smallest asset unit. */\n feePaid: bigint;\n}\n\n/**\n * One NIP-90 job for a store node's DVM, carried on the PAID path (#101).\n *\n * The store sits behind the connector's payment termination, so there is no\n * public HTTP endpoint to POST these to — a reachable one would be an\n * anonymous free gateway to a paid handler, which is the failure ADR 0020\n * exists to close. The job therefore travels exactly as a kind:5094 store\n * write does: inside a paid ILP packet, with `/store` as the envelope target\n * BENEATH the route's handler path rather than as a URL path.\n */\nexport interface StoreJobRequest {\n /** NIP-90 job kind — 5095 brokered ArNS buy, 5096 gas station. */\n kind: number;\n /** Job arguments, sent as `[\"param\", key, value]` tags. */\n params: [string, string][];\n}\n\n/**\n * A store DVM's answer, opened from the sealed FULFILL.\n *\n * ⚠️ A refusal is a RESULT, not an error. Under ADR 0020 an HTTP status is\n * envelope content and never a packet outcome, so `accept: false` arrives on a\n * FULFILL and the payer was charged for it either way. Implementations must\n * return it rather than throw: the zero-ARIO rehearsal (submit a buy with no\n * `processId` and watch the handler refuse by name before it quotes or touches\n * the registry) is the cheapest proof the whole paid path works, and it only\n * works if the refusal comes back as data.\n */\nexport interface StoreJobResponse {\n /** HTTP status the app answered with, inside the envelope. */\n status: number;\n /** The DVM's own accept flag. `false` is a refusal, not a transport fault. */\n accept: boolean;\n /** Machine-readable refusal reason, when the DVM sent one. */\n code?: string;\n /** Human-readable detail, when the DVM sent one. */\n message?: string;\n /** The job's output, shape depending on `kind`. */\n result?: Record<string, unknown>;\n /** Fee paid to carry this job, in the smallest asset unit. */\n feePaid: bigint;\n}\n\n/**\n * Fee rates used by `planPush` for the pre-push estimate.\n *\n * Both figures are FLAT per packet. ADR 0020 (toon-client#452) removed\n * byte-proportional pricing from the protocol: one handler, one price, and an\n * app that wants to charge differently exposes more handlers. A 100-byte and\n * a 100 KB upload to the same store route now cost the same, and the\n * connector charges accordingly regardless of what a client computes — so\n * there is no `uploadFeePerByte` to multiply and no floor to apply, because\n * the route's price is the whole fee rather than a lower bound on one.\n */\nexport interface FeeRates {\n /**\n * Flat cost per git-object/blob upload (smallest asset unit): the store\n * destination's route price, as reported by the terminating connector's\n * `GET /ilp/routes/price`. The connector gates every paid packet at that\n * price, so an estimate built from it is exactly what a push pays.\n */\n uploadFee: bigint;\n /**\n * The store route's per-kibibyte rate, when it meters by size (ADR 0065: a\n * price is a schedule `{base, per_kib}`; a flat route has no slope). Absent\n * or zero means every upload costs exactly `uploadFee`.\n */\n uploadPerKib?: bigint;\n /**\n * Flat cost per published event (smallest asset unit). Implementations\n * already fold any per-packet route-price floor into this flat value, so\n * estimates using it match the claims actually signed.\n */\n eventFee: bigint;\n}\n\n/**\n * Paid transport for `executePush` — implemented by the daemon route\n * handlers (#227) and the standalone embedded client (#228).\n *\n * Implementations must be safe to call sequentially (uploads are issued one\n * at a time in dependency-safe order) and should throw on any failure —\n * `executePush` does not retry; a crashed push is resumed by re-planning\n * (content-addressed uploads make the retry idempotent).\n */\nexport interface Publisher {\n /** Current fee rates for estimation (may be queried before every plan). */\n getFeeRates(): Promise<FeeRates>;\n /** Upload one git object body to Arweave; paid. */\n uploadGitObject(upload: GitObjectUpload): Promise<UploadReceipt>;\n /**\n * Upload one raw blob (no git envelope) with an explicit `Content-Type`;\n * paid. Used by `rig site` (#368) for the ar.io path manifest. Optional so\n * transports that only move git objects (and pre-#368 test fakes) need not\n * implement it — `rig site` requires it and errors clearly when absent.\n */\n uploadBlob?(upload: BlobUpload): Promise<UploadReceipt>;\n /**\n * Submit one NIP-90 job to the store node's DVM over the paid path and\n * return its answer; paid. Used by `rig name buy/set --via` (#101) for the\n * brokered kind:5095 buy and the kind:5096 gas station.\n *\n * Optional for the same reason as {@link uploadBlob}: transports that only\n * move git objects (and pre-#101 test fakes) need not implement it, and\n * `rig name --via` errors clearly when it is absent.\n */\n submitStoreJob?(request: StoreJobRequest): Promise<StoreJobResponse>;\n /**\n * Sign (implementation-held key) and pay-to-publish one event to the\n * given relay(s).\n */\n publishEvent(\n event: UnsignedEvent,\n relayUrls: string[]\n ): Promise<PublishReceipt>;\n}\n\n// ---------------------------------------------------------------------------\n// Per-upload charge on a metered route\n// ---------------------------------------------------------------------------\n\n/**\n * Bytes the connector meters that are not the caller's object: the kind:5094\n * event around it (id, pubkey, sig, kind, created_at, tag names), the `{event}`\n * body wrapper, the request envelope and the gift wrap. Measured against the\n * live edge and rounded up; a KiB boundary crossed by this margin costs one\n * `uploadPerKib` more than the store will actually charge, never less.\n */\nexport const UPLOAD_ENVELOPE_OVERHEAD_BYTES = 704;\n\n/**\n * The bytes a connector meters for an upload of `bodyBytes`: the object is\n * base64 in an `i` tag (4/3 expansion), plus {@link UPLOAD_ENVELOPE_OVERHEAD_BYTES}.\n */\nexport function estimateSealedUploadBytes(bodyBytes: number): number {\n return Math.ceil(bodyBytes / 3) * 4 + UPLOAD_ENVELOPE_OVERHEAD_BYTES;\n}\n\n/**\n * What one upload of `bodyBytes` costs under `rates`: the flat `uploadFee`,\n * plus — on a metered route — `uploadPerKib` per started kibibyte of the\n * sealed payload, counted from one (the connector's own rule:\n * `price + pricePerKib × (⌊bytes / 1024⌋ + 1)`).\n */\nexport function uploadChargeFor(rates: FeeRates, bodyBytes: number): bigint {\n const perKib = rates.uploadPerKib ?? 0n;\n if (perKib === 0n) return rates.uploadFee;\n const sealed = estimateSealedUploadBytes(bodyBytes);\n return rates.uploadFee + perKib * BigInt(Math.floor(sealed / 1024) + 1);\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 * The Nostr signing key rig derives from a BIP-39 phrase.\n *\n * `@toon-protocol/client` 2.x is a pure payer and carries no Nostr identity\n * (its own words), so the derivation that lived in the 0.x client moves here,\n * unchanged: `m/44'/1237'/0'/0/<accountIndex>`. Unchanged matters — this is\n * the key every existing rig repo is owned by (`toon.owner`, the kind:30617\n * author), and it is also the secp256k1 key the client reads a phrase's EVM\n * account from under `keyDerivation: 'legacy'`, so one phrase still yields one\n * author AND the wallet its channels were funded at.\n */\n\nimport { HDKey } from '@scure/bip32';\nimport { mnemonicToSeedSync } from '@scure/bip39';\nimport { getPublicKey } from 'nostr-tools/pure';\n\n/** A BIP-32 non-hardened child index fits in 31 bits. */\nexport const MAX_ACCOUNT_INDEX = 0x7fffffff;\n\n/** The path rig has always derived its Nostr key at. */\nexport function nostrDerivationPath(accountIndex = 0): string {\n return `m/44'/1237'/0'/0/${String(accountIndex)}`;\n}\n\nexport interface NostrKey {\n /** 32-byte secp256k1 secret. */\n secretKey: Uint8Array;\n /** 32-byte x-only public key, hex. */\n pubkey: string;\n}\n\n/**\n * Derive the Nostr key of a phrase at an account index.\n *\n * @throws when the index is out of range or the phrase yields no key.\n */\nexport function deriveNostrKeyFromMnemonic(\n mnemonic: string,\n accountIndex = 0\n): NostrKey {\n if (\n !Number.isInteger(accountIndex) ||\n accountIndex < 0 ||\n accountIndex > MAX_ACCOUNT_INDEX\n ) {\n throw new Error(\n `account index must be an integer in [0, ${String(MAX_ACCOUNT_INDEX)}]; got ${String(accountIndex)}`\n );\n }\n const seed = mnemonicToSeedSync(mnemonic.trim());\n try {\n const child = HDKey.fromMasterSeed(seed).derive(\n nostrDerivationPath(accountIndex)\n );\n if (!child.privateKey) {\n throw new Error('failed to derive a Nostr private key from the seed');\n }\n const secretKey = new Uint8Array(child.privateKey);\n return { secretKey, pubkey: getPublicKey(secretKey) };\n } finally {\n seed.fill(0);\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/standalone/connector-publisher.ts"],"sourcesContent":["/**\n * The paid write path on `@toon-protocol/client` 2.x — a {@link Publisher}\n * that pays a TOON connector per request.\n *\n * Everything a paid command needs comes from one connector URL: the client\n * reads the node's own `GET /ilp` (addresses, prices, settlement chains,\n * sealing key — connector ADR 0050), opens or adopts a payment channel, and\n * pays each request with a signed claim. There is no relay to discover peers\n * on (kind:10032 was removed by ADR 0046) and no topology to negotiate: a\n * destination is a route the node prices, and `send()` pays what the route\n * costs — including the per-kibibyte part of an ADR 0065 schedule, which the\n * client works out from the sealed payload so this publisher never has to.\n *\n * Three request shapes, all ordinary paid POSTs:\n *\n * 1. `publishEvent` — a NIP-34 event signed with the OWNER's Nostr key,\n * sent as `{ event }` to the publish route; the relay answers\n * `{ eventId, storedAt, payment? }`.\n * 2. `uploadGitObject` / `uploadBlob` — a kind:5094 store job carrying the\n * bytes base64 in an `i` tag; the store answers the Arweave tx id.\n * 3. `submitStoreJob` — a kind:5095 / 5096 job with `param` tags; the\n * DVM's answer comes back as data, refusal included (ADR 0020: an answer\n * is an answer, and the payer was charged for it).\n *\n * The store route may terminate on a different node than the one addressed\n * (the edge forwards it). Then the payload must be sealed to THAT node's\n * identity — `sealTo` — and, when the store node holds its own channel, paid\n * through its own client. Both are options here; the common single-node case\n * needs neither.\n */\n\nimport { finalizeEvent } from 'nostr-tools/pure';\nimport { contentTypeForPath, DEFAULT_CONTENT_TYPE } from '../mime.js';\nimport type { UnsignedEvent } from '../nip34-events.js';\nimport {\n uploadChargeFor,\n type BlobUpload,\n type FeeRates,\n type GitObjectUpload,\n type PublishReceipt,\n type Publisher,\n type StoreJobRequest,\n type StoreJobResponse,\n type UploadReceipt,\n} from '../publisher.js';\n\n// ---------------------------------------------------------------------------\n// The slice of ToonClient this publisher drives (structural, so tests mock it)\n// ---------------------------------------------------------------------------\n\n/** A paid request, as `ToonClient.send` takes it. */\nexport interface PaidRequest {\n method?: string;\n target?: string;\n headers?: Record<string, string> | [string, string][];\n body?: string | Uint8Array | object;\n}\n\nexport interface PaidSendOptions {\n /** The terminating connector's `GET /ilp` URL or raw key, on a forwarded route. */\n sealTo?: string | Uint8Array;\n timeoutMs?: number;\n}\n\nexport interface PaidFulfilled {\n fulfilled: true;\n status: number;\n body: Uint8Array;\n /** What this request paid; absent on a route priced at zero. */\n claim?: { amount: bigint };\n}\n\nexport interface PaidRefused {\n fulfilled: false;\n code: string;\n message: string;\n refusedBy?: string;\n}\n\nexport type PaidSendResult = PaidFulfilled | PaidRefused;\n\n/** The route terms a connector publishes: a base price and an optional slope. */\nexport interface RouteTerms {\n price: bigint;\n pricePerKib?: bigint;\n}\n\n/** The slice of `ToonClient` (2.x) this publisher needs. */\nexport interface PaidClientLike {\n send(\n destination: string,\n request: PaidRequest,\n options?: PaidSendOptions\n ): Promise<PaidSendResult>;\n /** The node's terms for a destination, or `null` when it prices no such route. */\n routePrice(destination: string): Promise<RouteTerms | null>;\n}\n\n/** One leg of the write path: who pays, where to, and whom to seal to. */\nexport interface PaidLeg {\n client: PaidClientLike;\n destination: string;\n sealTo?: string | Uint8Array;\n}\n\nexport interface ConnectorPublisherOptions {\n /** Pays the publish route and signs nothing — the Nostr key below signs. */\n publish: PaidLeg;\n /** Pays the store route (often the same client as `publish`). */\n store: PaidLeg;\n /** The owner's Nostr secret key: signs every event this publisher sends. */\n nostrSecretKey: Uint8Array;\n /**\n * A configured per-event fee. Informational floor only: the connector's\n * published price for the publish route wins when it is higher, and the\n * client pays that price whatever this says.\n */\n eventFee?: bigint;\n /** Per-packet timeout for uploads (the store's Arweave round trip). */\n uploadTimeoutMs?: number;\n warn?(line: string): void;\n}\n\nexport class ConnectorPublishError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ConnectorPublishError';\n }\n}\n\nconst ARWEAVE_TX_ID_REGEX = /^[A-Za-z0-9_-]{43}$/;\n\nfunction decodeUtf8(bytes: Uint8Array): string {\n return new TextDecoder().decode(bytes);\n}\n\nfunction nowSeconds(): number {\n return Math.floor(Date.now() / 1000);\n}\n\nfunction describeRefusal(result: PaidRefused): string {\n const by = result.refusedBy ? ` by the ${result.refusedBy}` : '';\n return `${result.code}${by}: ${result.message}`;\n}\n\n/**\n * Read the Arweave tx id out of a store's kind:5094 answer.\n *\n * The store answers one of two ways — `{ txId }` as JSON, or the bare id\n * base64 in `data` — and refuses with `{ accept: false, error }`. A non-2xx\n * status is a refusal too; both are thrown, because an upload without an id\n * is an upload that did not happen (even though the packet was paid for).\n */\nexport function extractArweaveTxId(status: number, body: Uint8Array): string {\n const text = decodeUtf8(body);\n if (status < 200 || status >= 300) {\n throw new ConnectorPublishError(\n `the store answered HTTP ${String(status)}${text ? ` - ${text}` : ''}`\n );\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n if (ARWEAVE_TX_ID_REGEX.test(text.trim())) return text.trim();\n throw new ConnectorPublishError(\n `the store's answer was not valid JSON: ${JSON.stringify(text.slice(0, 200))}`\n );\n }\n const answer = (parsed ?? {}) as {\n accept?: unknown;\n error?: unknown;\n txId?: unknown;\n data?: unknown;\n result?: unknown;\n };\n if (answer.accept === false) {\n const reason = typeof answer.error === 'string' ? `: ${answer.error}` : '';\n throw new ConnectorPublishError(\n `upload refused by the store (accept:false)${reason}`\n );\n }\n if (\n typeof answer.txId === 'string' &&\n ARWEAVE_TX_ID_REGEX.test(answer.txId)\n ) {\n return answer.txId;\n }\n const nested = answer.result as { txId?: unknown } | undefined;\n if (\n nested &&\n typeof nested.txId === 'string' &&\n ARWEAVE_TX_ID_REGEX.test(nested.txId)\n ) {\n return nested.txId;\n }\n if (typeof answer.data === 'string' && answer.data.length > 0) {\n const decoded = Buffer.from(answer.data, 'base64').toString('utf8');\n if (ARWEAVE_TX_ID_REGEX.test(decoded)) return decoded;\n }\n throw new ConnectorPublishError(\n `the store's answer carried no Arweave tx id: ${JSON.stringify(text.slice(0, 200))}`\n );\n}\n\n// ---------------------------------------------------------------------------\n// The publisher\n// ---------------------------------------------------------------------------\n\nexport class ConnectorPublisher implements Publisher {\n private readonly publish: PaidLeg;\n private readonly store: PaidLeg;\n private readonly nostrSecretKey: Uint8Array;\n private readonly configuredEventFee: bigint;\n private readonly uploadTimeoutMs: number | undefined;\n private readonly warn: (line: string) => void;\n private rates: Promise<FeeRates> | undefined;\n\n constructor(options: ConnectorPublisherOptions) {\n this.publish = options.publish;\n this.store = options.store;\n this.nostrSecretKey = options.nostrSecretKey;\n this.configuredEventFee = options.eventFee ?? 0n;\n this.uploadTimeoutMs = options.uploadTimeoutMs;\n this.warn = options.warn ?? (() => undefined);\n }\n\n /** Where events go, and where objects go. */\n get destinations(): { publish: string; store: string } {\n return { publish: this.publish.destination, store: this.store.destination };\n }\n\n /**\n * The two routes' published terms, read once per publisher. A route the\n * node does not price is a configuration error and says which knob to fix.\n */\n getFeeRates(): Promise<FeeRates> {\n this.rates ??= this.readFeeRates();\n return this.rates;\n }\n\n private async readFeeRates(): Promise<FeeRates> {\n const [publishTerms, storeTerms] = await Promise.all([\n this.publish.client.routePrice(this.publish.destination),\n this.store.client.routePrice(this.store.destination),\n ]);\n if (!publishTerms) {\n throw new ConnectorPublishError(\n `the connector prices no route for the publish destination ${this.publish.destination} ` +\n '(set publishDestination / TOON_CLIENT_PUBLISH_DESTINATION to one of its GET /ilp routes)'\n );\n }\n if (!storeTerms) {\n throw new ConnectorPublishError(\n `the connector prices no route for the store destination ${this.store.destination} ` +\n '(set storeDestination / TOON_CLIENT_STORE_DESTINATION to one of its GET /ilp routes)'\n );\n }\n if (\n publishTerms.pricePerKib !== undefined &&\n publishTerms.pricePerKib > 0n\n ) {\n this.warn(\n `rig: the publish route ${this.publish.destination} meters by size ` +\n `(${String(publishTerms.pricePerKib)}/KiB); the per-event estimate shows its base price only`\n );\n }\n const eventFee =\n this.configuredEventFee > publishTerms.price\n ? this.configuredEventFee\n : publishTerms.price;\n return {\n uploadFee: storeTerms.price,\n ...(storeTerms.pricePerKib !== undefined && storeTerms.pricePerKib > 0n\n ? { uploadPerKib: storeTerms.pricePerKib }\n : {}),\n eventFee,\n };\n }\n\n private sign(template: UnsignedEvent) {\n return finalizeEvent(\n {\n kind: template.kind,\n content: template.content,\n tags: template.tags,\n created_at: template.created_at,\n },\n this.nostrSecretKey\n );\n }\n\n /** One paid kind:5094 write; returns the tx id and what the claim spent. */\n private async storeWrite(\n tags: string[][],\n bodyBytes: number,\n what: string\n ): Promise<UploadReceipt> {\n const rates = await this.getFeeRates();\n const estimate = uploadChargeFor(rates, bodyBytes);\n const event = this.sign({\n kind: 5094,\n content: '',\n created_at: nowSeconds(),\n tags: [...tags, ['bid', estimate.toString(), 'usdc']],\n });\n const result = await this.store.client.send(\n this.store.destination,\n { body: { event } },\n {\n ...(this.store.sealTo !== undefined\n ? { sealTo: this.store.sealTo }\n : {}),\n ...(this.uploadTimeoutMs !== undefined\n ? { timeoutMs: this.uploadTimeoutMs }\n : {}),\n }\n );\n if (!result.fulfilled) {\n throw new ConnectorPublishError(\n `${what} rejected — ${describeRefusal(result)}`\n );\n }\n let txId: string;\n try {\n txId = extractArweaveTxId(result.status, result.body);\n } catch (err) {\n throw new ConnectorPublishError(\n `${what}: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n return { txId, feePaid: result.claim?.amount ?? estimate };\n }\n\n async uploadGitObject(upload: GitObjectUpload): Promise<UploadReceipt> {\n // No size gate (#102): the store route prices per KiB, so a body above\n // FREE_TIER_MAX_ITEM_BYTES is a paid upload rather than a refusal.\n const contentType =\n upload.type === 'blob'\n ? contentTypeForPath(upload.path)\n : DEFAULT_CONTENT_TYPE;\n return this.storeWrite(\n [\n ['i', upload.body.toString('base64'), 'blob'],\n ['output', contentType],\n ['Git-SHA', upload.sha],\n ['Git-Type', upload.type],\n ['Repo', upload.repoId],\n ],\n upload.body.length,\n `git-object upload (${upload.sha})`\n );\n }\n\n async uploadBlob(upload: BlobUpload): Promise<UploadReceipt> {\n // No size gate (#102), matching uploadGitObject: the store route prices\n // per KiB, so a blob above FREE_TIER_MAX_ITEM_BYTES costs more rather than\n // being refused.\n return this.storeWrite(\n [\n ['i', upload.body.toString('base64'), 'blob'],\n ['output', upload.contentType],\n ...(upload.repoId ? [['Repo', upload.repoId]] : []),\n ],\n upload.body.length,\n 'blob upload'\n );\n }\n\n /**\n * One NIP-90 job to the store node's DVM, over the paid path. A refusal is\n * returned, not thrown: `accept: false` and a non-2xx status are the\n * answer's content, and the payer was charged for the answer (ADR 0020).\n */\n async submitStoreJob(request: StoreJobRequest): Promise<StoreJobResponse> {\n const rates = await this.getFeeRates();\n const fee = rates.uploadFee;\n const event = this.sign({\n kind: request.kind,\n content: '',\n created_at: nowSeconds(),\n tags: [\n ...request.params.map(([key, value]) => ['param', key, value]),\n ['bid', fee.toString(), 'usdc'],\n ],\n });\n const result = await this.store.client.send(\n this.store.destination,\n { body: { event } },\n this.store.sealTo !== undefined ? { sealTo: this.store.sealTo } : {}\n );\n if (!result.fulfilled) {\n throw new ConnectorPublishError(\n `kind:${String(request.kind)} job rejected — ${describeRefusal(result)}`\n );\n }\n const text = decodeUtf8(result.body);\n let body: {\n accept?: unknown;\n code?: unknown;\n message?: unknown;\n result?: unknown;\n };\n try {\n body = JSON.parse(text) as typeof body;\n } catch {\n throw new ConnectorPublishError(\n `kind:${String(request.kind)} job answer was not valid JSON ` +\n `(HTTP ${String(result.status)}): ${JSON.stringify(text.slice(0, 200))}`\n );\n }\n return {\n status: result.status,\n accept: body.accept === true,\n ...(typeof body.code === 'string' ? { code: body.code } : {}),\n ...(typeof body.message === 'string' ? { message: body.message } : {}),\n ...(body.result !== null && typeof body.result === 'object'\n ? { result: body.result as Record<string, unknown> }\n : {}),\n feePaid: result.claim?.amount ?? fee,\n };\n }\n\n /**\n * Sign the event with the owner's key and pay to publish it. `relayUrls`\n * is the seam's plural surface: this publisher routes to one configured\n * destination, so more than one relay is refused rather than half-done.\n */\n async publishEvent(\n event: UnsignedEvent,\n relayUrls: string[]\n ): Promise<PublishReceipt> {\n if (relayUrls.length > 1) {\n throw new ConnectorPublishError(\n `multi-relay publish is not supported (got ${String(relayUrls.length)} relays) — ` +\n 'the connector publisher routes to a single publish destination'\n );\n }\n const rates = await this.getFeeRates();\n const signed = this.sign(event);\n const result = await this.publish.client.send(\n this.publish.destination,\n { body: { event: signed } },\n this.publish.sealTo !== undefined ? { sealTo: this.publish.sealTo } : {}\n );\n if (!result.fulfilled) {\n throw new ConnectorPublishError(\n `publish rejected (kind ${String(event.kind)}) — ${describeRefusal(result)}`\n );\n }\n if (result.status < 200 || result.status >= 300) {\n throw new ConnectorPublishError(\n `publish (kind ${String(event.kind)}): the relay answered HTTP ${String(result.status)} - ${decodeUtf8(result.body)}`\n );\n }\n let eventId = signed.id;\n try {\n const answer = JSON.parse(decodeUtf8(result.body)) as {\n eventId?: unknown;\n };\n if (typeof answer.eventId === 'string' && answer.eventId)\n eventId = answer.eventId;\n } catch {\n // A 2xx without a JSON body still stored the event we signed.\n }\n return { eventId, feePaid: result.claim?.amount ?? rates.eventFee };\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/standalone/channel-map.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 * COUNTERPARTY: the key names a ROUTE, not a node — an ILP name can change\n * hands (the devnet apex `g.toon` was retired and another node took over\n * `g.toon.relay`). A record therefore also carries the counterparty\n * settlement address it was opened against (`context.recipient`), which is\n * re-checked against the destination's announced address before every resume\n * ({@link counterpartyMatch}); a rotated counterparty\n * {@link ChannelMapStore.supersede}s the record instead of signing claims a\n * connector with no record of that channel refuses (`F01`).\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 * ISO timestamp this record was RETIRED from the resume path because the\n * destination now announces a different settlement address than the channel\n * was opened against (see {@link counterpartyMatch} /\n * {@link ChannelMapStore.supersede}). A superseded record is never resumed\n * again, but stays in {@link ChannelMapStore.list} so `rig channel\n * list/close/settle` can still reclaim whatever it holds on-chain.\n */\n supersededAt?: 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/**\n * Archive key a {@link ChannelMapStore.supersede}d record moves to: the live\n * key plus the channel id, so the retired record survives the fresh channel\n * being recorded under the live key (deposits it still holds stay reachable\n * from `rig channel list/close/settle`) and several supersessions of the same\n * route never collide.\n */\nfunction supersededKeyOf(record: ChannelMapRecord): string {\n return `${keyOf(recordKey(record))}|superseded:${record.channelId}`;\n}\n\n/**\n * Compare settlement addresses. EVM addresses travel in mixed checksum case\n * (an announce may say `0xF29f…` where the record says `0xf29f…`) and are\n * compared case-insensitively; anything else (base58 Solana, base58check\n * Mina) is case-SIGNIFICANT and compared verbatim.\n */\nexport function sameSettlementAddress(a: string, b: string): boolean {\n const normalize = (v: string): string =>\n /^0x[0-9a-fA-F]+$/.test(v) ? v.toLowerCase() : v;\n return normalize(a) === normalize(b);\n}\n\n/**\n * Does a recorded channel still belong to the counterparty the destination\n * announces TODAY?\n *\n * The map key is `identity|destination|chain|tokenNetwork` — it carries no\n * counterparty. When the node terminating an ILP name is REPLACED (the devnet\n * apex `g.toon` was retired and another node took over `g.toon.relay`), all\n * four key fields still match, so rig resumed a channel opened against the\n * old node and signed claims against it. The new connector holds no record of\n * that channel and refuses every packet: `F01 - claim rejected: names a\n * channel this connector has no record of`. Every paid write failed until the\n * cache entry was deleted by hand.\n *\n * - `'match'` — same counterparty, safe to resume.\n * - `'mismatch'` — the counterparty rotated: the record must be superseded and\n * the channel re-resolved.\n * - `'unrecorded'` — nothing to compare (a record written before this field\n * was validated, or a peer that announced no settlement address). Resume is\n * allowed and the caller should enrich the record with what the announce\n * says, so the NEXT run is verifiable.\n */\nexport function counterpartyMatch(\n record: Pick<ChannelMapRecord, 'context'>,\n announced: string | undefined\n): 'match' | 'mismatch' | 'unrecorded' {\n const recorded = record.context.recipient;\n if (!recorded || !announced) return 'unrecorded';\n return sameSettlementAddress(recorded, announced) ? 'match' : 'mismatch';\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. Superseded records are EXCLUDED: they are\n * retired from the resume path for good, and only `list()` (what `rig\n * channel` enumerates) still shows them. @throws {ChannelMapCorruptError}\n */\n listFor(identity: string, destination: string): ChannelMapRecord[] {\n return this.list().filter(\n (r) =>\n r.identity === identity &&\n r.destination === destination &&\n r.supersededAt === undefined\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 * or the counterparty a pre-validation record was written without) after\n * resuming it. Unknown keys are a no-op.\n */\n touch(\n key: ChannelMapKey,\n update?: { depositTotal?: string; recipient?: string }\n ): 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 if (update?.recipient !== undefined) {\n existing.context.recipient = update.recipient;\n }\n this.writeMap(data);\n }\n\n /**\n * Retire a record from the resume path — the counterparty that terminates\n * its destination has been REPLACED, so the channel it names is dead to the\n * new node (`F01 - claim rejected`) even though every key field still\n * matches (see {@link counterpartyMatch}).\n *\n * The record is MOVED to an archive key rather than deleted: it may still\n * hold an on-chain deposit, and `rig channel list/close/settle` find\n * channels by scanning `list()`, so deleting it would strand those funds\n * behind hand-editing the JSON. Moving it also frees the live key for the\n * re-resolved channel. Idempotent, and a no-op for an unknown record.\n */\n supersede(record: ChannelMapRecord): void {\n const data = this.readMap();\n const liveKey = keyOf(recordKey(record));\n const existing = data.channels[liveKey];\n const isLive =\n existing !== undefined && existing.channelId === record.channelId;\n const retired: ChannelMapRecord = {\n ...(isLive && existing ? existing : record),\n supersededAt: record.supersededAt ?? new Date().toISOString(),\n };\n const channels: Record<string, ChannelMapRecord> = {};\n for (const [key, value] of Object.entries(data.channels)) {\n // The live key is FREED (not just re-pointed): the re-resolved channel\n // is recorded under it moments later.\n if (isLive && key === liveKey) continue;\n channels[key] = value;\n }\n channels[supersededKeyOf(retired)] = retired;\n this.writeMap({ ...data, channels });\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 * 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":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli/standalone-mode.ts"],"sourcesContent":["/**\n * The standalone (embedded-client) session behind every paid command.\n *\n * One URL is the whole configuration. A TOON connector describes itself on\n * `GET /ilp` (connector ADR 0050): its ILP addresses, the routes it prices,\n * the chains it settles on and the key a payload is sealed to. The client\n * (`@toon-protocol/client` 3.x) reads that once, opens or adopts a payment\n * channel against the node, and pays each request with a signed claim. There\n * is nothing to discover and nothing to negotiate — kind:10032 announces were\n * removed by ADR 0046, and a node's word about itself replaced them.\n *\n * What this module resolves, in precedence order (env > config file):\n *\n * - `TOON_CONNECTOR` / `connectorUrl` — the node's client-edge URL. The\n * legacy `TOON_CLIENT_PROXY_URL` / `proxyUrl` (…`/ilp`) is still read; the\n * client normalizes a trailing `/ilp` away. `btpUrl` is ignored (the client\n * reads the BTP endpoint from `GET /ilp`).\n * - `TOON_CLIENT_PUBLISH_DESTINATION` / `publishDestination` — the route\n * events go to. Default: the first address the node publishes for itself\n * that it also prices (`defaultDestinationFor`).\n * - `TOON_CLIENT_STORE_DESTINATION` / `storeDestination` — the route git\n * objects go to. Default: the node's first priced route named `*.store`,\n * else `*.ario`. A `--via` override (`rig name`) outranks both.\n * - `TOON_CLIENT_STORE_CONNECTOR_URL` / `storeConnectorUrl` — when the store\n * route terminates on ANOTHER node that holds its own channel: that node's\n * URL. Uploads then ride a second client with its own watermark file.\n * `storeSealTo` alone (same channel, different terminating identity) makes\n * the edge's client seal to that node instead.\n * - `TOON_CLIENT_CHAIN` / `chain` — `evm` or `solana` (a full id such as\n * `evm:8453` is read by its family). Default: the first chain in the node's\n * `settlements[]` this identity holds a key for.\n * - `TOON_CLIENT_RPC_URL` / `rpcUrl` / `chainRpcUrls[<chain>]`.\n * - `RIG_SOLANA_KEY_FILE` / `solanaKeyFile`, `RIG_EVM_PRIVATE_KEY` /\n * `evmPrivateKey` — pay with THIS key instead of the phrase's derived one.\n * The author stays the phrase's Nostr key; who pays and who signs the\n * event are independent facts (the connector attributes payment from the\n * claim, never from the event).\n *\n * Key derivation: a rig phrase yields its Nostr key at `m/44'/1237'/0'/0/i`,\n * and its EVM account has always been that same secp256k1 key — which the\n * client calls `keyDerivation: 'legacy'`. That is the default here, so every\n * channel an existing identity funded is still its own. `keyDerivation:\n * 'standard'` / `TOON_CLIENT_KEY_DERIVATION=standard` opts into the\n * MetaMask-importable path for a fresh identity.\n */\n\nimport { readFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport {\n ToonClient,\n defaultDestinationFor,\n type ChannelState,\n type NodeSelfDescription,\n type ToonClientConfig,\n} from '@toon-protocol/client';\nimport { shaResolverFor } from '../git-sha-resolver.js';\nimport { fetchRemoteState } from '../remote-state.js';\nimport {\n ChannelMapStore,\n RIG_CHANNEL_MAP_FILENAME,\n type ChannelMapRecord,\n} from '../standalone/channel-map.js';\nimport { ConnectorPublisher } from '../standalone/connector-publisher.js';\nimport type {\n ChannelCloseOutcome,\n ChannelOpenOutcome,\n ChannelSettleOutcome,\n StandaloneMoneyOps,\n WalletChainBalanceInfo,\n} from '../standalone/money.js';\nimport {\n NonceLock,\n checkDaemonIdentity,\n standaloneForced,\n} from '../standalone/nonce-guard.js';\nimport { deriveNostrKeyFromMnemonic } from '../standalone/nostr-identity.js';\nimport { resolveIdentity } from './identity.js';\nimport type {\n StandaloneContext,\n StandaloneLoadOptions,\n} from './standalone-context.js';\n\n// ---------------------------------------------------------------------------\n// Config file\n// ---------------------------------------------------------------------------\n\n/** The shared `~/.toon-client/config.json` fields rig reads. */\nexport interface ClientConfigFile {\n /** The connector's client-edge URL (`GET /ilp` lives here). */\n connectorUrl?: string;\n /** @deprecated the pre-2.0 spelling: the connector's `…/ilp` ingress URL. */\n proxyUrl?: string;\n /** @deprecated ignored — the client reads the BTP endpoint from `GET /ilp`. */\n btpUrl?: string;\n /** Free-read relay (WebSocket) for the identity's kind:0 and NIP-34 reads. */\n relayUrl?: string;\n /** @deprecated alias of `publishDestination`. */\n destination?: string;\n publishDestination?: string;\n storeDestination?: string;\n storeConnectorUrl?: string;\n storeSealTo?: string;\n feePerEvent?: string;\n channelStorePath?: string;\n /** `evm` | `solana`, or a full id (`evm:8453`) read by its family. */\n chain?: string;\n /** @deprecated pre-2.0 chain list; the first entry is read as `chain`. */\n supportedChains?: string[];\n rpcUrl?: string;\n chainRpcUrls?: Record<string, string>;\n transport?: 'auto' | 'http' | 'btp';\n keyDerivation?: 'standard' | 'legacy';\n mnemonicAccountIndex?: number;\n /** Collateral for a first channel open, base units. */\n deposit?: string;\n /** Path to a Solana keypair JSON (64-byte array) that pays instead of the phrase's key. */\n solanaKeyFile?: string;\n /** Hex EVM private key that pays instead of the phrase's key. */\n evmPrivateKey?: string;\n /** Per-packet timeout for uploads, ms. */\n uploadTimeoutMs?: number;\n [key: string]: unknown;\n}\n\nfunction configDir(env: NodeJS.ProcessEnv): string {\n return env['TOON_CLIENT_HOME'] ?? join(homedir(), '.toon-client');\n}\n\nexport function 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/** An identity was resolved, but there is no connector to pay. */\nexport class MissingUplinkError extends Error {\n constructor(configPath: string) {\n super(\n \"no connector configured: set TOON_CONNECTOR to the node's URL \" +\n '(the one whose GET /ilp describes it), run `rig entry <url>`, or add ' +\n `connectorUrl to ${configPath}`\n );\n this.name = 'MissingUplinkError';\n }\n}\n\n// ---------------------------------------------------------------------------\n// Settings (pure: env + file → what the client is built from)\n// ---------------------------------------------------------------------------\n\nexport type ChainFamily = 'evm' | 'solana';\n\nexport interface ConnectorSettings {\n connectorUrl?: string;\n connectorSource: 'env' | 'config' | 'legacy-env' | 'legacy-config' | null;\n publishDestination?: string;\n storeDestination?: string;\n storeConnectorUrl?: string;\n storeSealTo?: string;\n relayUrl?: string;\n chain?: ChainFamily;\n /** The full chain id the user named, when they named one (`evm:8453`). */\n chainKey?: string;\n rpcUrl?: string;\n transport: 'auto' | 'http' | 'btp';\n keyDerivation: 'standard' | 'legacy';\n channelStorePath: string;\n deposit?: bigint;\n eventFee: bigint;\n solanaKeyFile?: string;\n evmPrivateKey?: string;\n uploadTimeoutMs?: number;\n /** Notes for the caller's stderr: deprecated spellings met on the way. */\n warnings: string[];\n}\n\n/** `evm:8453` → `evm`; `solana` → `solana`; anything else → undefined. */\nexport function chainFamilyOf(\n chain: string | undefined\n): ChainFamily | undefined {\n if (!chain) return undefined;\n const family = chain.split(':')[0];\n return family === 'evm' || family === 'solana' ? family : undefined;\n}\n\nexport function resolveConnectorSettings(args: {\n env: NodeJS.ProcessEnv;\n file: ClientConfigFile;\n configDir: string;\n storeDestinationOverride?: string;\n}): ConnectorSettings {\n const { env, file } = args;\n const warnings: string[] = [];\n\n let connectorUrl: string | undefined;\n let connectorSource: ConnectorSettings['connectorSource'] = null;\n if (env['TOON_CONNECTOR']) {\n connectorUrl = env['TOON_CONNECTOR'];\n connectorSource = 'env';\n } else if (env['TOON_CLIENT_PROXY_URL']) {\n connectorUrl = env['TOON_CLIENT_PROXY_URL'];\n connectorSource = 'legacy-env';\n warnings.push(\n 'rig: TOON_CLIENT_PROXY_URL is the pre-2.0 spelling — set TOON_CONNECTOR to the node URL instead'\n );\n } else if (file.connectorUrl) {\n connectorUrl = file.connectorUrl;\n connectorSource = 'config';\n } else if (file.proxyUrl) {\n connectorUrl = file.proxyUrl;\n connectorSource = 'legacy-config';\n warnings.push(\n 'rig: config `proxyUrl` is the pre-2.0 spelling — `rig entry <url>` records `connectorUrl`'\n );\n }\n if (env['TOON_CLIENT_BTP_URL'] || file.btpUrl) {\n warnings.push(\n \"rig: btpUrl is ignored since 4.0 — the client reads the BTP endpoint from the node's GET /ilp\"\n );\n }\n\n const chainKey =\n env['TOON_CLIENT_CHAIN'] ?? file.chain ?? file.supportedChains?.[0];\n const chain = chainFamilyOf(chainKey);\n if (chainKey && !chain) {\n warnings.push(\n `rig: chain ${JSON.stringify(chainKey)} is not a settlement chain the connector supports (evm | solana) — ignoring it`\n );\n }\n const rpcUrl =\n env['TOON_CLIENT_RPC_URL'] ??\n file.rpcUrl ??\n (chainKey ? file.chainRpcUrls?.[chainKey] : undefined) ??\n (chain ? file.chainRpcUrls?.[chain] : undefined);\n\n const transportRaw = env['TOON_CLIENT_TRANSPORT'] ?? file.transport;\n const transport: ConnectorSettings['transport'] =\n transportRaw === 'http' || transportRaw === 'btp' ? transportRaw : 'auto';\n\n const derivationRaw = env['TOON_CLIENT_KEY_DERIVATION'] ?? file.keyDerivation;\n const keyDerivation: ConnectorSettings['keyDerivation'] =\n derivationRaw === 'standard' ? 'standard' : 'legacy';\n\n const depositRaw = env['TOON_CLIENT_DEPOSIT'] ?? file.deposit;\n const publishDestination =\n env['TOON_CLIENT_PUBLISH_DESTINATION'] ??\n env['TOON_CLIENT_DESTINATION'] ??\n file.publishDestination ??\n file.destination;\n const storeDestination =\n args.storeDestinationOverride ??\n env['TOON_CLIENT_STORE_DESTINATION'] ??\n file.storeDestination;\n const relayUrl = env['TOON_CLIENT_RELAY_URL'] ?? file.relayUrl;\n\n return {\n ...(connectorUrl ? { connectorUrl } : {}),\n connectorSource,\n ...(publishDestination ? { publishDestination } : {}),\n ...(storeDestination ? { storeDestination } : {}),\n ...((env['TOON_CLIENT_STORE_CONNECTOR_URL'] ?? file.storeConnectorUrl)\n ? {\n storeConnectorUrl:\n env['TOON_CLIENT_STORE_CONNECTOR_URL'] ?? file.storeConnectorUrl,\n }\n : {}),\n ...((env['TOON_CLIENT_STORE_SEAL_TO'] ?? file.storeSealTo)\n ? { storeSealTo: env['TOON_CLIENT_STORE_SEAL_TO'] ?? file.storeSealTo }\n : {}),\n ...(relayUrl ? { relayUrl } : {}),\n ...(chain ? { chain } : {}),\n ...(chainKey ? { chainKey } : {}),\n ...(rpcUrl ? { rpcUrl } : {}),\n transport,\n keyDerivation,\n channelStorePath:\n env['TOON_CLIENT_CHANNEL_STORE'] ??\n file.channelStorePath ??\n join(args.configDir, 'channels.json'),\n ...(depositRaw !== undefined ? { deposit: BigInt(depositRaw) } : {}),\n eventFee: BigInt(\n env['TOON_CLIENT_FEE_PER_EVENT'] ?? file.feePerEvent ?? '0'\n ),\n ...((env['RIG_SOLANA_KEY_FILE'] ?? file.solanaKeyFile)\n ? { solanaKeyFile: env['RIG_SOLANA_KEY_FILE'] ?? file.solanaKeyFile }\n : {}),\n ...((env['RIG_EVM_PRIVATE_KEY'] ?? file.evmPrivateKey)\n ? { evmPrivateKey: env['RIG_EVM_PRIVATE_KEY'] ?? file.evmPrivateKey }\n : {}),\n ...(file.uploadTimeoutMs !== undefined\n ? { uploadTimeoutMs: file.uploadTimeoutMs }\n : {}),\n warnings,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Destinations (pure: settings + the node's self-description)\n// ---------------------------------------------------------------------------\n\n/**\n * The store route a node offers, when the caller named none: its first priced\n * route spelled `*.store`, else `*.ario` (the AR.IO-backed store this\n * repository has always uploaded to), else nothing.\n */\nexport function defaultStoreDestinationFor(\n desc: NodeSelfDescription\n): string | undefined {\n const prefixes = desc.routes.map((r) => r.prefix);\n return (\n prefixes.find((p) => p === 'store' || p.endsWith('.store')) ??\n prefixes.find((p) => p === 'ario' || p.endsWith('.ario')) ??\n prefixes.find((p) => /store/i.test(p))\n );\n}\n\nexport function resolveDestinations(\n desc: NodeSelfDescription,\n settings: Pick<ConnectorSettings, 'publishDestination' | 'storeDestination'>,\n connectorUrl: string\n): { publish: string; store: string } {\n const publish = settings.publishDestination ?? defaultDestinationFor(desc);\n if (!publish) {\n throw new Error(\n `the connector at ${connectorUrl} publishes no priced address of its own — ` +\n 'set publishDestination / TOON_CLIENT_PUBLISH_DESTINATION to one of its GET /ilp routes'\n );\n }\n const store = settings.storeDestination ?? defaultStoreDestinationFor(desc);\n if (!store) {\n throw new Error(\n `the connector at ${connectorUrl} prices no store route (none named *.store or *.ario) — ` +\n 'set storeDestination / TOON_CLIENT_STORE_DESTINATION to the route its store answers on'\n );\n }\n return { publish, store };\n}\n\n// ---------------------------------------------------------------------------\n// Key files\n// ---------------------------------------------------------------------------\n\n/** A Solana keypair file: the 64-byte JSON array `solana-keygen` writes. */\nexport function readSolanaKeyFile(path: string): Uint8Array {\n let parsed: unknown;\n try {\n parsed = JSON.parse(readFileSync(path, 'utf8'));\n } catch (err) {\n throw new Error(\n `failed to read the Solana key file ${path}: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n if (\n !Array.isArray(parsed) ||\n (parsed.length !== 64 && parsed.length !== 32) ||\n !parsed.every((n) => Number.isInteger(n) && n >= 0 && n <= 255)\n ) {\n throw new Error(\n `${path} is not a Solana keypair file (expected a JSON array of 64 bytes)`\n );\n }\n return Uint8Array.from(parsed as number[]);\n}\n\n// ---------------------------------------------------------------------------\n// Money ops on the client's channel and wallet facades\n// ---------------------------------------------------------------------------\n\nfunction recordFor(\n state: ChannelState,\n args: { identity: string; destination: string; peerId: string }\n): Omit<ChannelMapRecord, 'openedAt' | 'lastUsedAt'> {\n const tokenNetwork =\n state.domain.tokenNetwork ?? state.domain.programId ?? '';\n return {\n channelId: state.channelId,\n peerId: args.peerId,\n identity: args.identity,\n destination: args.destination,\n chain: state.domain.chain,\n tokenNetwork,\n context: {\n chainType: state.chain,\n chainId: state.domain.chainId ?? 0,\n tokenNetworkAddress: tokenNetwork,\n tokenAddress: state.domain.token,\n recipient: state.domain.counterparty,\n },\n depositTotal: state.depositTotal.toString(),\n };\n}\n\nfunction buildMoneyOps(args: {\n client: ToonClient;\n channelMap: ChannelMapStore;\n identity: string;\n destination: string;\n peerId: string;\n}): StandaloneMoneyOps {\n const { client, channelMap } = args;\n const known = (channelId: string) =>\n channelMap.list().some((r) => r.channelId === channelId);\n const requireSameChannel = async (record: ChannelMapRecord) => {\n const state = await client.channel.state();\n if (state.channelId !== record.channelId) {\n throw new Error(\n `this identity's channel with ${args.destination} is ${state.channelId}, ` +\n `not the recorded ${record.channelId} — the record is stale or belongs to another node`\n );\n }\n };\n return {\n async openChannel(opts): Promise<ChannelOpenOutcome> {\n const before = await client.channel.state().catch(() => undefined);\n const resumed = before !== undefined && known(before.channelId);\n const state =\n opts?.deposit !== undefined && before?.status === 'open'\n ? await client.channel.deposit(opts.deposit)\n : await client.channel.open(\n opts?.deposit !== undefined ? { deposit: opts.deposit } : {}\n );\n channelMap.record(recordFor(state, args));\n return {\n channelId: state.channelId,\n resumed,\n destination: args.destination,\n chain: state.domain.chain,\n peerId: args.peerId,\n depositTotal: state.depositTotal.toString(),\n ...(opts?.deposit !== undefined\n ? { depositAdded: opts.deposit.toString() }\n : {}),\n };\n },\n async closeChannel(record): Promise<ChannelCloseOutcome> {\n await requireSameChannel(record);\n const result = await client.channel.close();\n const closedAt = result.closedAt ?? BigInt(Math.floor(Date.now() / 1000));\n return {\n channelId: record.channelId,\n ...(result.txHash ? { txHash: result.txHash } : {}),\n closedAt: closedAt.toString(),\n settleableAt: (result.settleableAt ?? closedAt).toString(),\n };\n },\n async settleChannel(record): Promise<ChannelSettleOutcome> {\n await requireSameChannel(record);\n const result = await client.channel.settle();\n channelMap.supersede(record);\n return {\n channelId: record.channelId,\n ...(result.txHash ? { txHash: result.txHash } : {}),\n };\n },\n async walletChainBalances(): Promise<WalletChainBalanceInfo[]> {\n return (await client.wallet.balances()).map((b) => ({\n chain: b.chain,\n chainKey: b.chainKey,\n address: b.address,\n ...(b.native ? { native: b.native } : {}),\n tokens: b.tokens,\n ...(b.unreadable !== undefined ? { unreadable: b.unreadable } : {}),\n ...(b.error !== undefined ? { error: b.error } : {}),\n }));\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// The context factory\n// ---------------------------------------------------------------------------\n\n/** What `rig balance` gets when nothing is configured to pay with. */\nclass UnconfiguredPublisher {\n constructor(private readonly error: MissingUplinkError) {}\n getFeeRates(): Promise<never> {\n return Promise.reject(this.error);\n }\n uploadGitObject(): Promise<never> {\n return Promise.reject(this.error);\n }\n publishEvent(): Promise<never> {\n return Promise.reject(this.error);\n }\n}\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 const nostr = deriveNostrKeyFromMnemonic(\n identity.mnemonic,\n identity.accountIndex\n );\n\n const settings = resolveConnectorSettings({\n env,\n file,\n configDir: dir,\n ...(options.storeDestination\n ? { storeDestinationOverride: options.storeDestination }\n : {}),\n });\n for (const line of settings.warnings) warn(line);\n const defaultRelayUrls = settings.relayUrl ? [settings.relayUrl] : [];\n\n if (!settings.connectorUrl) {\n const missing = new MissingUplinkError(configPath);\n if (options.requireUplink === false) {\n return {\n ownerPubkey: nostr.pubkey,\n identitySource: identity.source,\n identitySourceLabel: identity.sourceLabel,\n publisher: new UnconfiguredPublisher(missing),\n defaultRelayUrls,\n // SHAs the 30618 object map does not cover are resolved against\n // the configured gateway's GraphQL endpoint, not arweave.net (#183).\n fetchRemote: (args) =>\n fetchRemoteState({\n ...args,\n resolveSha: shaResolverFor(undefined, env),\n }),\n stop: () => Promise.resolve(),\n };\n }\n throw missing;\n }\n const connectorUrl = settings.connectorUrl;\n\n // One writer per identity per machine: two rig processes signing claims on\n // one channel race the nonce watermark, and the connector refuses the loser.\n if (!standaloneForced(env)) await checkDaemonIdentity(nostr.pubkey);\n const lock = await NonceLock.acquire(nostr.pubkey);\n\n const clients: ToonClient[] = [];\n const stop = async () => {\n for (const c of clients) await c.close();\n lock.release();\n };\n\n try {\n const payerKeys: Pick<\n ToonClientConfig,\n | 'mnemonic'\n | 'accountIndex'\n | 'keyDerivation'\n | 'solanaSecretKey'\n | 'evmPrivateKey'\n > = {\n mnemonic: identity.mnemonic,\n accountIndex: identity.accountIndex,\n keyDerivation: settings.keyDerivation,\n ...(settings.solanaKeyFile\n ? { solanaSecretKey: readSolanaKeyFile(settings.solanaKeyFile) }\n : {}),\n ...(settings.evmPrivateKey\n ? { evmPrivateKey: settings.evmPrivateKey }\n : {}),\n };\n const shared: Omit<ToonClientConfig, 'connector' | 'channelStore'> = {\n ...payerKeys,\n ...(settings.chain ? { chain: settings.chain } : {}),\n ...(settings.rpcUrl ? { rpcUrl: settings.rpcUrl } : {}),\n transport: settings.transport,\n autoOpenChannel: true,\n ...(settings.deposit !== undefined ? { deposit: settings.deposit } : {}),\n };\n if (settings.chain && !settings.rpcUrl) {\n warn(\n `rig: no rpcUrl configured for ${settings.chain} — the client falls back to its devnet preset; ` +\n 'set TOON_CLIENT_RPC_URL (or config rpcUrl) for a mainnet node'\n );\n }\n\n const client = await ToonClient.create({\n ...shared,\n connector: connectorUrl,\n channelStore: settings.channelStorePath,\n });\n clients.push(client);\n const desc = await client.describe();\n const destinations = resolveDestinations(desc, settings, connectorUrl);\n\n // The store leg: the same client unless the store terminates on another\n // node that holds its own channel (then its own client + watermark file).\n let storeClient = client;\n if (\n settings.storeConnectorUrl &&\n normalizeUrl(settings.storeConnectorUrl) !== normalizeUrl(connectorUrl)\n ) {\n storeClient = await ToonClient.create({\n ...shared,\n connector: settings.storeConnectorUrl,\n channelStore: settings.channelStorePath.replace(\n /(\\.json)?$/,\n '.store.json'\n ),\n });\n clients.push(storeClient);\n }\n\n const publisher = new ConnectorPublisher({\n publish: { client, destination: destinations.publish },\n store: {\n client: storeClient,\n destination: destinations.store,\n ...(storeClient === client && settings.storeSealTo\n ? { sealTo: settings.storeSealTo }\n : {}),\n },\n nostrSecretKey: nostr.secretKey,\n eventFee: settings.eventFee,\n ...(settings.uploadTimeoutMs !== undefined\n ? { uploadTimeoutMs: settings.uploadTimeoutMs }\n : {}),\n warn,\n });\n\n const channelMap = new ChannelMapStore({\n mapPath: join(dir, RIG_CHANNEL_MAP_FILENAME),\n watermarkPath: settings.channelStorePath,\n });\n const money = buildMoneyOps({\n client,\n channelMap,\n identity: nostr.pubkey,\n destination: options.channelDestination ?? destinations.publish,\n peerId: desc.edgeIdentity?.keyId ?? connectorUrl,\n });\n\n return {\n ownerPubkey: nostr.pubkey,\n identitySource: identity.source,\n identitySourceLabel: identity.sourceLabel,\n publisher,\n defaultRelayUrls,\n // As above (#183): the GraphQL fallback follows RIG_ARWEAVE_GATEWAY.\n fetchRemote: (args) =>\n fetchRemoteState({\n ...args,\n resolveSha: shaResolverFor(undefined, env),\n }),\n money,\n stop,\n };\n } catch (err) {\n await stop().catch(() => undefined);\n throw err;\n }\n}\n\nfunction normalizeUrl(url: string): string {\n return url.replace(/\\/+$/, '').replace(/\\/ilp$/, '');\n}\n"],"mappings":"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","names":[]}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|