@toon-protocol/rig 4.6.0 → 5.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 +118 -25
- package/dist/{chunk-X232W4TG.js → chunk-2TG4YHEC.js} +89 -59
- package/dist/chunk-2TG4YHEC.js.map +1 -0
- package/dist/{chunk-DCGV6ALH.js → chunk-4WVH2ITA.js} +158 -9
- package/dist/chunk-4WVH2ITA.js.map +1 -0
- package/dist/{chunk-U5A7MHBP.js → chunk-7VIVQHRG.js} +1 -1
- package/dist/chunk-7VIVQHRG.js.map +1 -0
- package/dist/chunk-CHVOXCDV.js +0 -0
- package/dist/chunk-CHVOXCDV.js.map +1 -0
- package/dist/{chunk-XCLUB2CY.js → chunk-LMZSAFAB.js} +44 -15
- package/dist/chunk-LMZSAFAB.js.map +1 -0
- package/dist/{chunk-JBFBD7HV.js → chunk-QKL53YZS.js} +7 -3
- package/dist/chunk-QKL53YZS.js.map +1 -0
- package/dist/{chunk-UE5TAFEU.js → chunk-ZQGXFFBP.js} +4 -4
- package/dist/chunk-ZQGXFFBP.js.map +1 -0
- package/dist/{ci-serve-AYFZOY4O.js → ci-serve-BU7NDT7O.js} +152 -26
- package/dist/ci-serve-BU7NDT7O.js.map +1 -0
- package/dist/cli/rig.js +6 -5
- package/dist/cli/rig.js.map +1 -1
- package/dist/index.d.ts +117 -4
- package/dist/index.js +40 -7
- package/dist/index.js.map +1 -1
- package/dist/standalone/index.d.ts +109 -74
- package/dist/standalone/index.js +13 -3
- package/dist/{standalone-mode-UQSP4677.js → standalone-mode-AT4S5EZG.js} +148 -50
- package/dist/standalone-mode-AT4S5EZG.js.map +1 -0
- package/package.json +2 -2
- package/dist/chunk-DCGV6ALH.js.map +0 -1
- package/dist/chunk-JBFBD7HV.js.map +0 -1
- package/dist/chunk-U5A7MHBP.js.map +0 -1
- package/dist/chunk-UE5TAFEU.js.map +0 -1
- package/dist/chunk-X232W4TG.js.map +0 -1
- package/dist/chunk-XCLUB2CY.js.map +0 -1
- package/dist/ci-serve-AYFZOY4O.js.map +0 -1
- package/dist/standalone-mode-UQSP4677.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
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: every x402 voucher names the channel's CUMULATIVE total\n * (client 4.x, connector ADR 0075; the nonce of the 3.x toon-channel claims\n * is gone), and the connector accepts one only if it exceeds the last by at\n * least the charge. Two writers signing vouchers on the same channel from\n * separate processes (a running `toon-clientd` daemon plus a standalone\n * embedded client, or two standalone processes) each keep their own running\n * total, so their vouchers race: the connector refuses the one that does not\n * advance, and a re-signed total can double-charge (the hazard documented in\n * 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 and later 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":";AAqNO,IAAM,iCAAiC;AAMvC,SAAS,0BAA0B,WAA2B;AACnE,SAAO,KAAK,KAAK,YAAY,CAAC,IAAI,IAAI;AACxC;AAQO,SAAS,gBAAgB,OAAiB,WAA2B;AAC1E,QAAM,SAAS,MAAM,gBAAgB;AACrC,MAAI,WAAW,GAAI,QAAO,MAAM;AAChC,QAAM,SAAS,0BAA0B,SAAS;AAClD,SAAO,MAAM,YAAY,SAAS,OAAO,KAAK,MAAM,SAAS,IAAI,IAAI,CAAC;AACxE;;;ACnMA,SAAS,WAAW,cAAc,YAAY,qBAAqB;AACnE,SAAS,eAAe;AACxB,SAAS,YAAY;AAOd,IAAM,sBAAsB;AAG5B,SAAS,oBAA4B;AAC1C,QAAM,MAAM,QAAQ,IAAI,uBAAuB;AAC/C,QAAM,SAAS,MAAM,OAAO,GAAG,IAAI;AACnC,SAAO,OAAO,SAAS,MAAM,KAAK,SAAS,IAAI,SAAS;AAC1D;AAOO,SAAS,iBAAyB;AACvC,SAAO,QAAQ,IAAI,kBAAkB,KAAK,KAAK,QAAQ,GAAG,cAAc;AAC1E;AAsBO,IAAM,qBAAqB;AAO3B,SAAS,iBAAiB,KAAiC;AAChE,QAAM,MAAM,IAAI,kBAAkB;AAClC,MAAI,QAAQ,OAAW,QAAO;AAC9B,QAAM,IAAI,IAAI,KAAK,EAAE,YAAY;AACjC,SAAO,MAAM,OAAO,MAAM,UAAU,MAAM,SAAS,MAAM;AAC3D;AAOO,IAAM,8BAAN,cAA0C,MAAM;AAAA,EACrD,YAEkB,QAEA,WAChB;AACA;AAAA,MACE,+CAA+C,OAAO,MAAM,GAAG,CAAC,CAAC,cAC5D,SAAS;AAAA,IAKhB;AAXgB;AAEA;AAUhB,SAAK,OAAO;AAAA,EACd;AAAA,EAbkB;AAAA,EAEA;AAYpB;AAGO,IAAM,sBAAN,cAAkC,MAAM;AAAA,EAC7C,YACkB,QACA,UACA,WAChB;AACA;AAAA,MACE,mCAAmC,SAAS,yDACL,OAAO,MAAM,GAAG,CAAC,CAAC,WACnD,QAAQ;AAAA,IAEhB;AATgB;AACA;AACA;AAQhB,SAAK,OAAO;AAAA,EACd;AAAA,EAXkB;AAAA,EACA;AAAA,EACA;AAUpB;AAyBA,eAAsB,oBACpB,QACA,UAA8B,CAAC,GAChB;AACf,QAAM,OAAO,QAAQ,QAAQ,kBAAkB;AAC/C,QAAM,YAAY,QAAQ,aAAa;AACvC,QAAM,MAAM,oBAAoB,IAAI;AAEpC,MAAI;AACJ,MAAI;AACF,UAAM,MAAM,MAAM,UAAU,KAAK;AAAA,MAC/B,QAAQ,YAAY,QAAQ,QAAQ,aAAa,IAAI;AAAA,IACvD,CAAC;AACD,QAAI,CAAC,IAAI,GAAI;AACb,UAAM,OAAQ,MAAM,IAAI,KAAK;AAG7B,UAAM,YAAY,MAAM,UAAU;AAClC,QAAI,OAAO,cAAc,SAAU,gBAAe;AAAA,EACpD,QAAQ;AAEN;AAAA,EACF;AAEA,MAAI,iBAAiB,UAAa,iBAAiB,QAAQ;AACzD,UAAM,IAAI,4BAA4B,QAAQ,GAAG;AAAA,EACnD;AACF;AAoBA,SAAS,SAAS,KAAsB;AACtC,MAAI;AACF,YAAQ,KAAK,KAAK,CAAC;AACnB,WAAO;AAAA,EACT,SAAS,KAAK;AAEZ,WAAQ,IAA8B,SAAS;AAAA,EACjD;AACF;AAWO,IAAM,YAAN,MAAM,WAAU;AAAA,EAIb,YACU,QACA,UAChB;AAFgB;AACA;AAEhB,SAAK,cAAc,MAAM;AACvB,UAAI;AACF,mBAAW,KAAK,QAAQ;AAAA,MAC1B,QAAQ;AAAA,MAER;AAAA,IACF;AACA,YAAQ,KAAK,QAAQ,KAAK,WAAW;AAAA,EACvC;AAAA,EAXkB;AAAA,EACA;AAAA,EALV,WAAW;AAAA,EACF;AAAA,EAgBjB,aAAa,QACX,QACA,UAA8B,CAAC,GACX;AACpB,UAAM,MAAM,QAAQ,OAAO,eAAe;AAC1C,UAAM,MAAM,QAAQ,OAAO,QAAQ;AACnC,UAAM,WAAW,KAAK,KAAK,cAAc,MAAM,OAAO;AACtD,cAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAElC,UAAM,UAAU,KAAK;AAAA,MACnB;AAAA,QACE;AAAA,QACA;AAAA,QACA,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,MACpC;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAIA,aAAS,UAAU,GAAG,UAAU,GAAG,WAAW;AAC5C,UAAI;AACF,sBAAc,UAAU,SAAS,EAAE,MAAM,KAAK,CAAC;AAC/C,eAAO,IAAI,WAAU,QAAQ,QAAQ;AAAA,MACvC,SAAS,KAAK;AACZ,YAAK,IAA8B,SAAS,SAAU,OAAM;AAE5D,YAAI;AACJ,YAAI;AACF,gBAAM,SAAS,KAAK;AAAA,YAClB,aAAa,UAAU,MAAM;AAAA,UAC/B;AACA,cAAI,OAAO,OAAO,QAAQ,SAAU,aAAY,OAAO;AAAA,QACzD,QAAQ;AAAA,QAER;AAIA,YACE,cAAc,UACd,cAAc,OACd,SAAS,SAAS,GAClB;AACA,gBAAM,IAAI,oBAAoB,QAAQ,UAAU,SAAS;AAAA,QAC3D;AAGA,YAAI;AACF,qBAAW,QAAQ;AAAA,QACrB,QAAQ;AAAA,QAGR;AAAA,MACF;AAAA,IACF;AAEA,UAAM,IAAI,oBAAoB,QAAQ,UAAU,EAAE;AAAA,EACpD;AAAA;AAAA,EAGA,UAAgB;AACd,QAAI,KAAK,SAAU;AACnB,SAAK,WAAW;AAChB,YAAQ,eAAe,QAAQ,KAAK,WAAW;AAC/C,QAAI;AACF,iBAAW,KAAK,QAAQ;AAAA,IAC1B,QAAQ;AAAA,IAER;AAAA,EACF;AACF;;;ACpTA,SAAS,aAAa;AACtB,SAAS,0BAA0B;AACnC,SAAS,oBAAoB;AAGtB,IAAM,oBAAoB;AAG1B,SAAS,oBAAoB,eAAe,GAAW;AAC5D,SAAO,oBAAoB,OAAO,YAAY,CAAC;AACjD;AAcO,SAAS,2BACd,UACA,eAAe,GACL;AACV,MACE,CAAC,OAAO,UAAU,YAAY,KAC9B,eAAe,KACf,eAAe,mBACf;AACA,UAAM,IAAI;AAAA,MACR,2CAA2C,OAAO,iBAAiB,CAAC,UAAU,OAAO,YAAY,CAAC;AAAA,IACpG;AAAA,EACF;AACA,QAAM,OAAO,mBAAmB,SAAS,KAAK,CAAC;AAC/C,MAAI;AACF,UAAM,QAAQ,MAAM,eAAe,IAAI,EAAE;AAAA,MACvC,oBAAoB,YAAY;AAAA,IAClC;AACA,QAAI,CAAC,MAAM,YAAY;AACrB,YAAM,IAAI,MAAM,oDAAoD;AAAA,IACtE;AACA,UAAM,YAAY,IAAI,WAAW,MAAM,UAAU;AACjD,WAAO,EAAE,WAAW,QAAQ,aAAa,SAAS,EAAE;AAAA,EACtD,UAAE;AACA,SAAK,KAAK,CAAC;AAAA,EACb;AACF;","names":[]}
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/gateway-preference.ts","../src/git-sha-resolver.ts"],"sourcesContent":["/**\n * Which Arweave gateway rig PRINTS — and which one it READS through (#176).\n *\n * Fetches try every gateway in `@toon-protocol/arweave`'s shared list, so the\n * order there only matters for redundancy. A printed URL is different: it is\n * the one link a reader clicks. `ar-io.dev` heads the shared list but is\n * ar.io's TESTNET gateway — its ArNS resolver runs against the Solana devnet\n * contracts and it serves testnet-bundler uploads that never reach Arweave —\n * so a mainnet upload printed there is a guaranteed 404 (toon-client#577\n * reorders the shared list). Until then rig prefers the first MAINNET gateway\n * of that list and never prints the testnet one as the primary.\n */\n\nimport { ARWEAVE_GATEWAYS } from '@toon-protocol/arweave';\n\nconst TESTNET_GATEWAY = /(^|\\.)ar-io\\.dev$/i;\n\n/** Every gateway that is not ar.io's testnet. Malformed URLs are not gateways. */\nexport function isMainnetGateway(url: string): boolean {\n try {\n return !TESTNET_GATEWAY.test(new URL(url).hostname);\n } catch {\n return false;\n }\n}\n\n/** The gateway a printed URL defaults to: the shared list's first mainnet one. */\nexport const PREFERRED_GATEWAY: string =\n ARWEAVE_GATEWAYS.find(isMainnetGateway) ?? 'https://arweave.net';\n\n/** The shared list minus one gateway, for \"also at\" mirrors under a primary. */\nexport function mirrorGatewaysFor(primary: string): string[] {\n const p = primary.replace(/\\/+$/, '');\n return ARWEAVE_GATEWAYS.filter((g) => g.replace(/\\/+$/, '') !== p);\n}\n\n// ---------------------------------------------------------------------------\n// The gateway override on the READ path (#176)\n// ---------------------------------------------------------------------------\n\n/** Environment variable naming the gateway rig writes through and reads from. */\nexport const ARWEAVE_GATEWAY_ENV = 'RIG_ARWEAVE_GATEWAY';\n\n/** Trailing slashes off a gateway base URL, so `${g}/raw` never doubles up. */\nexport function normalizeGatewayUrl(url: string): string {\n return url.replace(/\\/+$/, '');\n}\n\n/**\n * The gateway this invocation was configured with: `--gateway`, else\n * `RIG_ARWEAVE_GATEWAY`, else null — nothing configured, blank counting as\n * unset. Normalized, so a caller can append a route to it.\n */\nexport function configuredGateway(\n flag: string | undefined,\n env: NodeJS.ProcessEnv\n): string | null {\n const raw = (flag ?? env[ARWEAVE_GATEWAY_ENV] ?? '').trim();\n return raw === '' ? null : normalizeGatewayUrl(raw);\n}\n\n/**\n * Where object bytes are read from once a gateway IS configured: its raw-bytes\n * route FIRST, then the shared public list as the fallback.\n *\n * `<gateway>/raw/<txId>`, not `<gateway>/<txId>`: an ar.io node serves raw\n * transaction bytes there whether or not it does sandboxed-subdomain\n * redirects, and the TOON store's own gateway (the dev sandbox's) answers on\n * that route ALONE. A public gateway serves the same bytes on both.\n */\nexport function readGatewaysFor(gateway: string): string[] {\n return [`${normalizeGatewayUrl(gateway)}/raw`, ...ARWEAVE_GATEWAYS];\n}\n\n/**\n * The read path's gateway list for a command that takes `--gateway`: the\n * configured gateway first ({@link readGatewaysFor}), or — nothing configured\n * — the shared public list exactly as it stands today.\n *\n * This is what gives `rig clone` / `rig fetch` the reach `rig push` has: a\n * repository whose objects live only on a private, local or air-gapped\n * gateway becomes readable by naming it, with the public gateways still\n * serving everyone else.\n */\nexport function readGateways(\n flag: string | undefined,\n env: NodeJS.ProcessEnv\n): readonly string[] {\n const gateway = configuredGateway(flag, env);\n return gateway === null ? ARWEAVE_GATEWAYS : readGatewaysFor(gateway);\n}\n","/**\n * The Git-SHA → Arweave txId fallback, pointed at the CONFIGURED gateway\n * (#183, follow-up to #176).\n *\n * A kind:30618 object map is a HINT map, not an index: it is capped (#162),\n * so a SHA it does not carry is resolved by asking a gateway's GraphQL\n * endpoint which transaction carries the `Git-SHA` + `Repo` tag pair the\n * store stamps on every object upload. `@toon-protocol/arweave`'s shared\n * `resolveGitSha` does exactly that — but against ONE baked-in endpoint,\n * `https://arweave.net/graphql`. After #176 gave `rig clone` / `rig fetch` /\n * `rig ci serve` a gateway override for object bytes, that fallback was the\n * one read left crossing to the public permaweb: on a self-hosted, local or\n * air-gapped stack it either fails or — worse — answers about a DIFFERENT\n * network, and a wrong txId is served as fact.\n *\n * So when (and only when) a gateway is configured, rig asks that gateway\n * instead, with the same query. Nothing configured and\n * {@link shaResolverFor} hands back the shared resolver itself: the default\n * path stays byte-for-byte what it was, one implementation, one cache.\n *\n * ## Why there is no public fallback behind the configured gateway\n *\n * Object BYTES keep the public list behind the configured gateway (#176):\n * they are content-addressed and SHA-verified on arrival, so any mirror may\n * serve them and a wrong answer cannot survive. A tag query is the opposite\n * kind of fact — an unverifiable statement about which transaction holds an\n * object ON ONE NETWORK — so it goes to the network the user named and\n * nowhere else. A gateway that serves no GraphQL resolves nothing, which\n * surfaces as the honest \"missing objects\" report the read pipeline already\n * prints, not as a silent cross-network read.\n *\n * ## The cache is keyed per endpoint\n *\n * The shared resolver's cache is keyed `sha:repo` alone, so two endpoints\n * answering about the same repository would overwrite each other's answers in\n * one process — a sandbox txId served to a mainnet query, or the reverse.\n * This module therefore keeps its OWN cache, keyed by endpoint as well, and\n * neither reads nor writes the shared one. (Hints seeded from a 30618 are\n * unaffected: `RemoteState.resolveMissing` serves those from the event's own\n * map before any resolver is called.)\n */\n\nimport {\n ARWEAVE_FETCH_TIMEOUT_MS,\n isValidArweaveTxId,\n resolveGitSha,\n shaCacheKey,\n} from '@toon-protocol/arweave';\n\nimport {\n configuredGateway,\n normalizeGatewayUrl,\n} from './gateway-preference.js';\n\n/** Git SHA → Arweave txId, the shape `fetchRemoteState` takes as `resolveSha`. */\nexport type ShaResolver = (\n sha: string,\n repo: string\n) => Promise<string | null>;\n\n/** WHATWG-fetch seam for the GraphQL POST (injectable for tests). */\nexport type GraphqlFetch = (\n url: string,\n init: {\n method: string;\n headers: Record<string, string>;\n body: string;\n signal?: AbortSignal;\n }\n) => Promise<{ ok: boolean; json(): Promise<unknown> }>;\n\n/** A git SHA-1 as a full 40-hex string — anything else is never queried. */\nconst FULL_SHA_RE = /^[0-9a-f]{40}$/i;\n\n/** Entries kept in the per-endpoint cache (mirrors the shared resolver's cap). */\nconst SHA_CACHE_MAX_SIZE = 10000;\n\n/** `endpoint\\nsha:repo` → txId. Endpoint-keyed: see the module header. */\nconst cache = new Map<string, string>();\n\n/** Drop every cached answer. Test isolation only. */\nexport function clearGatewayShaCache(): void {\n cache.clear();\n}\n\n/**\n * Where a gateway answers tag queries: `<gateway>/graphql`. Every ar.io node\n * and the public Arweave gateways serve GraphQL on that path, so the one\n * URL the user already gives rig is enough to reach it.\n */\nexport function graphqlEndpointFor(gateway: string): string {\n return `${normalizeGatewayUrl(gateway)}/graphql`;\n}\n\n/**\n * Strip what would break out of a GraphQL string literal. Same guard as the\n * shared resolver's — the repo id reaches the query from a relay event.\n */\nfunction sanitizeGraphQLValue(value: string): string {\n // eslint-disable-next-line no-control-regex -- intentional: strip control chars for GraphQL safety\n return value.replace(/[\"\\\\\\n\\r\u0000-\u001f`]/g, '');\n}\n\n/** The tag query, byte-identical to the shared resolver's. */\nfunction tagQuery(sha: string, repo: string): string {\n return `query {\n transactions(tags: [\n { name: \"Git-SHA\", values: [\"${sanitizeGraphQLValue(sha)}\"] },\n { name: \"Repo\", values: [\"${sanitizeGraphQLValue(repo)}\"] }\n ]) {\n edges { node { id } }\n }\n}`;\n}\n\ninterface TagQueryResponse {\n data?: { transactions?: { edges?: { node?: { id?: string } }[] } };\n}\n\n/**\n * A resolver bound to one gateway: it asks `<gateway>/graphql` and nothing\n * else. Unresolvable is `null`, never a throw — a SHA that resolves nowhere\n * is a missing object, which the read pipeline already reports honestly.\n */\nexport function gatewayShaResolver(\n gateway: string,\n fetchFn?: GraphqlFetch\n): ShaResolver {\n const endpoint = graphqlEndpointFor(gateway);\n const post: GraphqlFetch =\n fetchFn ?? ((url, init) => fetch(url, init) as ReturnType<GraphqlFetch>);\n\n return async (sha, repo) => {\n if (!FULL_SHA_RE.test(sha)) return null;\n\n const key = `${endpoint}\\n${shaCacheKey(sha, repo)}`;\n const cached = cache.get(key);\n if (cached !== undefined) return cached;\n\n let txId: string | undefined;\n try {\n const response = await post(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ query: tagQuery(sha, repo) }),\n signal: AbortSignal.timeout(ARWEAVE_FETCH_TIMEOUT_MS),\n });\n if (!response.ok) return null;\n const json = (await response.json()) as TagQueryResponse;\n txId = json.data?.transactions?.edges?.[0]?.node?.id;\n } catch {\n return null;\n }\n if (txId === undefined || !isValidArweaveTxId(txId)) return null;\n\n if (cache.size >= SHA_CACHE_MAX_SIZE) {\n const oldest = cache.keys().next().value;\n if (oldest !== undefined) cache.delete(oldest);\n }\n cache.set(key, txId);\n return txId;\n };\n}\n\n/**\n * The resolver a command hands `fetchRemoteState` as `resolveSha`: one bound\n * to the gateway this invocation was configured with (`--gateway`, else\n * `RIG_ARWEAVE_GATEWAY`), or — nothing configured — the shared\n * `resolveGitSha` itself, unchanged.\n *\n * This is the {@link configuredGateway} decision #176 introduced, applied to\n * the one read that was still leaving the configured permaweb.\n */\nexport function shaResolverFor(\n flag: string | undefined,\n env: NodeJS.ProcessEnv,\n fetchFn?: GraphqlFetch\n): ShaResolver {\n const gateway = configuredGateway(flag, env);\n return gateway === null\n ? resolveGitSha\n : gatewayShaResolver(gateway, fetchFn);\n}\n"],"mappings":";;;;;;;;;AAeA,IAAM,kBAAkB;AAGjB,SAAS,iBAAiB,KAAsB;AACrD,MAAI;AACF,WAAO,CAAC,gBAAgB,KAAK,IAAI,IAAI,GAAG,EAAE,QAAQ;AAAA,EACpD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGO,IAAM,oBACX,iBAAiB,KAAK,gBAAgB,KAAK;AAGtC,SAAS,kBAAkB,SAA2B;AAC3D,QAAM,IAAI,QAAQ,QAAQ,QAAQ,EAAE;AACpC,SAAO,iBAAiB,OAAO,CAAC,MAAM,EAAE,QAAQ,QAAQ,EAAE,MAAM,CAAC;AACnE;AAOO,IAAM,sBAAsB;AAG5B,SAAS,oBAAoB,KAAqB;AACvD,SAAO,IAAI,QAAQ,QAAQ,EAAE;AAC/B;AAOO,SAAS,kBACd,MACA,KACe;AACf,QAAM,OAAO,QAAQ,IAAI,mBAAmB,KAAK,IAAI,KAAK;AAC1D,SAAO,QAAQ,KAAK,OAAO,oBAAoB,GAAG;AACpD;AAWO,SAAS,gBAAgB,SAA2B;AACzD,SAAO,CAAC,GAAG,oBAAoB,OAAO,CAAC,QAAQ,GAAG,gBAAgB;AACpE;AAYO,SAAS,aACd,MACA,KACmB;AACnB,QAAM,UAAU,kBAAkB,MAAM,GAAG;AAC3C,SAAO,YAAY,OAAO,mBAAmB,gBAAgB,OAAO;AACtE;;;AClBA,IAAM,cAAc;AAGpB,IAAM,qBAAqB;AAG3B,IAAM,QAAQ,oBAAI,IAAoB;AAY/B,SAAS,mBAAmB,SAAyB;AAC1D,SAAO,GAAG,oBAAoB,OAAO,CAAC;AACxC;AAMA,SAAS,qBAAqB,OAAuB;AAEnD,SAAO,MAAM,QAAQ,kBAAkB,EAAE;AAC3C;AAGA,SAAS,SAAS,KAAa,MAAsB;AACnD,SAAO;AAAA;AAAA,mCAE0B,qBAAqB,GAAG,CAAC;AAAA,gCAC5B,qBAAqB,IAAI,CAAC;AAAA;AAAA;AAAA;AAAA;AAK1D;AAWO,SAAS,mBACd,SACA,SACa;AACb,QAAM,WAAW,mBAAmB,OAAO;AAC3C,QAAM,OACJ,YAAY,CAAC,KAAK,SAAS,MAAM,KAAK,IAAI;AAE5C,SAAO,OAAO,KAAK,SAAS;AAC1B,QAAI,CAAC,YAAY,KAAK,GAAG,EAAG,QAAO;AAEnC,UAAM,MAAM,GAAG,QAAQ;AAAA,EAAK,YAAY,KAAK,IAAI,CAAC;AAClD,UAAM,SAAS,MAAM,IAAI,GAAG;AAC5B,QAAI,WAAW,OAAW,QAAO;AAEjC,QAAI;AACJ,QAAI;AACF,YAAM,WAAW,MAAM,KAAK,UAAU;AAAA,QACpC,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,KAAK,UAAU,EAAE,OAAO,SAAS,KAAK,IAAI,EAAE,CAAC;AAAA,QACnD,QAAQ,YAAY,QAAQ,wBAAwB;AAAA,MACtD,CAAC;AACD,UAAI,CAAC,SAAS,GAAI,QAAO;AACzB,YAAM,OAAQ,MAAM,SAAS,KAAK;AAClC,aAAO,KAAK,MAAM,cAAc,QAAQ,CAAC,GAAG,MAAM;AAAA,IACpD,QAAQ;AACN,aAAO;AAAA,IACT;AACA,QAAI,SAAS,UAAa,CAAC,mBAAmB,IAAI,EAAG,QAAO;AAE5D,QAAI,MAAM,QAAQ,oBAAoB;AACpC,YAAM,SAAS,MAAM,KAAK,EAAE,KAAK,EAAE;AACnC,UAAI,WAAW,OAAW,OAAM,OAAO,MAAM;AAAA,IAC/C;AACA,UAAM,IAAI,KAAK,IAAI;AACnB,WAAO;AAAA,EACT;AACF;AAWO,SAAS,eACd,MACA,KACA,SACa;AACb,QAAM,UAAU,kBAAkB,MAAM,GAAG;AAC3C,SAAO,YAAY,OACf,gBACA,mBAAmB,SAAS,OAAO;AACzC;","names":[]}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
// src/standalone/channel-map.ts
|
|
2
|
-
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
3
3
|
import { homedir } from "os";
|
|
4
4
|
import { dirname, join } from "path";
|
|
5
|
-
var RIG_CHANNEL_MAP_FILENAME = "rig-channels.json";
|
|
5
|
+
var RIG_CHANNEL_MAP_FILENAME = "rig-channels-x402.json";
|
|
6
|
+
var CHANNEL_STORE_FILENAME = "channels-x402.json";
|
|
7
|
+
var LEGACY_RIG_CHANNEL_MAP_FILENAME = "rig-channels.json";
|
|
8
|
+
var LEGACY_CHANNEL_STORE_FILENAME = "channels.json";
|
|
6
9
|
function resolveChannelPaths(env) {
|
|
7
10
|
const dir = env["TOON_CLIENT_HOME"] ?? join(homedir(), ".toon-client");
|
|
8
11
|
let configured;
|
|
@@ -21,9 +24,34 @@ function resolveChannelPaths(env) {
|
|
|
21
24
|
}
|
|
22
25
|
return {
|
|
23
26
|
mapPath: join(dir, RIG_CHANNEL_MAP_FILENAME),
|
|
24
|
-
watermarkPath: configured ?? join(dir,
|
|
27
|
+
watermarkPath: configured ?? join(dir, CHANNEL_STORE_FILENAME)
|
|
25
28
|
};
|
|
26
29
|
}
|
|
30
|
+
function legacyChannelFiles(dir) {
|
|
31
|
+
return [LEGACY_RIG_CHANNEL_MAP_FILENAME, LEGACY_CHANNEL_STORE_FILENAME].map((name) => join(dir, name)).filter((path) => existsSync(path));
|
|
32
|
+
}
|
|
33
|
+
function isToonChannelStore(path) {
|
|
34
|
+
const read = (p) => {
|
|
35
|
+
try {
|
|
36
|
+
const parsed = JSON.parse(readFileSync(p, "utf8"));
|
|
37
|
+
return typeof parsed === "object" && parsed !== null ? parsed : void 0;
|
|
38
|
+
} catch {
|
|
39
|
+
return void 0;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const entries = Object.values(read(path) ?? {});
|
|
43
|
+
if (entries.some(
|
|
44
|
+
(e) => typeof e === "object" && e !== null && typeof e.nonce === "number" && e.nonce > 0
|
|
45
|
+
)) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
const bindings = Object.values(
|
|
49
|
+
read(path.replace(/(\.json)?$/, ".peers.json")) ?? {}
|
|
50
|
+
);
|
|
51
|
+
return bindings.some(
|
|
52
|
+
(b) => typeof b === "object" && b !== null && b.batchSettlement === void 0
|
|
53
|
+
);
|
|
54
|
+
}
|
|
27
55
|
var ChannelMapCorruptError = class extends Error {
|
|
28
56
|
constructor(path, detail) {
|
|
29
57
|
super(
|
|
@@ -91,9 +119,9 @@ var ChannelMapStore = class {
|
|
|
91
119
|
}
|
|
92
120
|
/**
|
|
93
121
|
* Record a (freshly opened) channel. Overwrites any previous record under
|
|
94
|
-
* the same (identity, destination, chain, tokenNetwork) key
|
|
95
|
-
*
|
|
96
|
-
* watermark store.
|
|
122
|
+
* the same (identity, destination, chain, tokenNetwork) key; a caller that
|
|
123
|
+
* replaces a channel still holding a deposit {@link supersede}s the old
|
|
124
|
+
* record first. Its watermark stays in the watermark store.
|
|
97
125
|
*/
|
|
98
126
|
record(record) {
|
|
99
127
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -154,15 +182,15 @@ var ChannelMapStore = class {
|
|
|
154
182
|
this.writeMap({ ...data, channels });
|
|
155
183
|
}
|
|
156
184
|
/**
|
|
157
|
-
* Read one channel's
|
|
158
|
-
*
|
|
185
|
+
* Read one channel's watermark entry from the client's voucher store
|
|
186
|
+
* (undefined when the file or entry is missing).
|
|
159
187
|
* @throws {ChannelMapCorruptError} when the watermark file is unreadable.
|
|
160
188
|
*/
|
|
161
189
|
readWatermark(channelId) {
|
|
162
190
|
return this.readWatermarkFile()[channelId];
|
|
163
191
|
}
|
|
164
192
|
/**
|
|
165
|
-
* Seed a fresh channel's watermark entry (
|
|
193
|
+
* Seed a fresh channel's watermark entry (cumulative 0) so a
|
|
166
194
|
* later resume can tell "never claimed against" apart from "watermark
|
|
167
195
|
* lost". Never overwrites an existing entry.
|
|
168
196
|
*/
|
|
@@ -171,11 +199,7 @@ var ChannelMapStore = class {
|
|
|
171
199
|
if (data[channelId]) return;
|
|
172
200
|
data[channelId] = { nonce: 0, cumulativeAmount: "0" };
|
|
173
201
|
mkdirSync(dirname(this.watermarkPath), { recursive: true });
|
|
174
|
-
writeFileSync(
|
|
175
|
-
this.watermarkPath,
|
|
176
|
-
JSON.stringify(data, null, 2),
|
|
177
|
-
"utf-8"
|
|
178
|
-
);
|
|
202
|
+
writeFileSync(this.watermarkPath, JSON.stringify(data, null, 2), "utf-8");
|
|
179
203
|
}
|
|
180
204
|
// ── file I/O ───────────────────────────────────────────────────────────────
|
|
181
205
|
readMap() {
|
|
@@ -318,7 +342,12 @@ function resolveConflictingPath(paths) {
|
|
|
318
342
|
|
|
319
343
|
export {
|
|
320
344
|
RIG_CHANNEL_MAP_FILENAME,
|
|
345
|
+
CHANNEL_STORE_FILENAME,
|
|
346
|
+
LEGACY_RIG_CHANNEL_MAP_FILENAME,
|
|
347
|
+
LEGACY_CHANNEL_STORE_FILENAME,
|
|
321
348
|
resolveChannelPaths,
|
|
349
|
+
legacyChannelFiles,
|
|
350
|
+
isToonChannelStore,
|
|
322
351
|
ChannelMapCorruptError,
|
|
323
352
|
sameSettlementAddress,
|
|
324
353
|
counterpartyMatch,
|
|
@@ -329,4 +358,4 @@ export {
|
|
|
329
358
|
contentTypeForPath,
|
|
330
359
|
resolveConflictingPath
|
|
331
360
|
};
|
|
332
|
-
//# sourceMappingURL=chunk-
|
|
361
|
+
//# sourceMappingURL=chunk-LMZSAFAB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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: rig used to open (and fund) a FRESH on-chain channel per\n * invocation, because the client of the day kept the peer→channelId mapping\n * only in memory (the #260 fresh-outsider e2e stranded five deposits across\n * five commands). Since client 4.x the client persists its own channel config\n * beside the watermark (`JsonFileChannelStore`: `channels-x402.json` plus its\n * `.peers.json` sibling) and resumes from it by itself. This map remains the\n * record `rig channel list/close/settle`, `rig balance` and `rig ci serve`\n * read: WHICH channel the identity holds with each peer, keyed by\n * `identity pubkey | ILP anchor (peer/apex destination) | chain | tokenNetwork`.\n *\n * VOUCHERS (client 4.x, connector ADR 0075): every paid packet carries an x402\n * `batch-settlement` voucher naming the running total. There is no nonce; the\n * watermark is the cumulative amount alone. The toon-channel claims client\n * 3.x signed are refused by a current connector, so a 3.x rig's files\n * (`rig-channels.json`, `channels.json`) are never read by this module and\n * never written: they are only named, so their deposits can still be taken\n * back with the rig that opened them ({@link legacyChannelFiles}).\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), extended with the identity pubkey (rig\n * identities come from an env/`.env` precedence chain, so one state dir can\n * 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 voucher 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 can be\n * re-checked against the destination's announced address ({@link\n * counterpartyMatch}); a rotated counterparty {@link ChannelMapStore.supersede}s\n * the record rather than paying a connector with no record of that channel.\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 { existsSync, 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 (x402 voucher channels). */\nexport const RIG_CHANNEL_MAP_FILENAME = 'rig-channels-x402.json';\n\n/**\n * The client's x402 channel store under the shared client state dir: the\n * voucher watermark, with every channel's config in the `.peers.json` sibling.\n * A new name for client 4.x so a 3.x store is never read as one.\n */\nexport const CHANNEL_STORE_FILENAME = 'channels-x402.json';\n\n/** What a client 3.x rig kept its toon-channel map in. Never read or written. */\nexport const LEGACY_RIG_CHANNEL_MAP_FILENAME = 'rig-channels.json';\n\n/** What a client 3.x rig kept its toon-channel watermark in. Never read or written. */\nexport const LEGACY_CHANNEL_STORE_FILENAME = '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 x402 voucher\n * store (`config.json`'s `channelStorePath`, default\n * `<dir>/channels-x402.json`: the same resolution `cli/standalone-mode.ts`\n * feeds 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, CHANNEL_STORE_FILENAME),\n };\n}\n\n/**\n * The client 3.x channel files present in a state dir: a toon-channel map and\n * watermark a current connector no longer honours. Named so a caller can say\n * they were skipped; the deposits they record are left for the 3.x rig that\n * opened them (`npm i -g @toon-protocol/rig@4.7.0`, then `rig channel close`).\n */\nexport function legacyChannelFiles(dir: string): string[] {\n return [LEGACY_RIG_CHANNEL_MAP_FILENAME, LEGACY_CHANNEL_STORE_FILENAME]\n .map((name) => join(dir, name))\n .filter((path) => existsSync(path));\n}\n\n/**\n * Does `path` hold a client 3.x toon-channel store? Its watermark entries\n * carry a nonce that moved (a 4.x entry keeps `nonce: 0` for the file's\n * sake), and its `.peers.json` bindings carry no `batchSettlement` config.\n * An unreadable file answers false: the client reports it on its own read.\n */\nexport function isToonChannelStore(path: string): boolean {\n const read = (p: string): Record<string, unknown> | undefined => {\n try {\n const parsed: unknown = JSON.parse(readFileSync(p, 'utf8'));\n return typeof parsed === 'object' && parsed !== null\n ? (parsed as Record<string, unknown>)\n : undefined;\n } catch {\n return undefined;\n }\n };\n const entries = Object.values(read(path) ?? {});\n if (\n entries.some(\n (e) =>\n typeof e === 'object' &&\n e !== null &&\n typeof (e as { nonce?: unknown }).nonce === 'number' &&\n (e as { nonce: number }).nonce > 0\n )\n ) {\n return true;\n }\n const bindings = Object.values(\n read(path.replace(/(\\.json)?$/, '.peers.json')) ?? {}\n );\n return bindings.some(\n (b) =>\n typeof b === 'object' &&\n b !== null &&\n (b as { batchSettlement?: unknown }).batchSettlement === undefined\n );\n}\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * A channel's chain context (same shape the daemon's apex-channel-store\n * persists, and the client's own binding carries).\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 /** The node's sealing key id (`GET /ilp` `edgeIdentity.keyId`), else its URL. */\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 /** The x402 network the channel lives on, e.g. `eip155:8453`, `solana:5eykt…`. */\n chain: string;\n /** The batch-settlement contract (EVM) or program (Solana) holding it. */\n tokenNetwork: string;\n /** Chain context: chain type, token, counterparty. */\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 voucher watermark store (`channels-x402.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 /** Always 0 for an x402 channel: vouchers carry no nonce. Kept for the file's shape. */\n nonce?: number;\n /** Cumulative amount signed so far, base units (string-encoded bigint). */\n cumulativeAmount: string;\n /** Exit 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 voucher watermark store (`channels-x402.json`). */\n watermarkPath: string;\n}\n\n/**\n * File-backed peer→channel map + read/seed access to the client's voucher\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; a caller that\n * replaces a channel still holding a deposit {@link supersede}s the old\n * record first. Its watermark stays in the 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 watermark entry from the client's voucher store\n * (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 (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(this.watermarkPath, JSON.stringify(data, null, 2), 'utf-8');\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> }).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 exit journey, from its watermark timers:\n * mirrors what the client's `channel.channels()` reports. 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,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
deriveNostrKeyFromMnemonic
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-7VIVQHRG.js";
|
|
4
4
|
|
|
5
5
|
// src/nip34-events.ts
|
|
6
6
|
import { getAddress, isAddress } from "viem";
|
|
@@ -177,6 +177,9 @@ function buildStatus(repoOwnerPubkey, repoId, targetEventId, statusKind, targetP
|
|
|
177
177
|
};
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
// src/remote-state.ts
|
|
181
|
+
import { decode as decodeToon } from "@toon-format/toon";
|
|
182
|
+
|
|
180
183
|
// ../../node_modules/.pnpm/@toon-protocol+arweave@0.2.0/node_modules/@toon-protocol/arweave/dist/gateways.js
|
|
181
184
|
var ARWEAVE_GATEWAYS = [
|
|
182
185
|
"https://ar-io.dev",
|
|
@@ -266,7 +269,6 @@ async function resolveGitSha(sha, repo) {
|
|
|
266
269
|
}
|
|
267
270
|
|
|
268
271
|
// src/remote-state.ts
|
|
269
|
-
import { decode as decodeToon } from "@toon-format/toon";
|
|
270
272
|
import { REPOSITORY_ANNOUNCEMENT_KIND as REPOSITORY_ANNOUNCEMENT_KIND2 } from "@toon-protocol/core/nip34";
|
|
271
273
|
|
|
272
274
|
// src/nip34-refs.ts
|
|
@@ -755,7 +757,9 @@ export {
|
|
|
755
757
|
buildStatus,
|
|
756
758
|
ARWEAVE_GATEWAYS,
|
|
757
759
|
ARWEAVE_FETCH_TIMEOUT_MS,
|
|
760
|
+
shaCacheKey,
|
|
758
761
|
isValidArweaveTxId,
|
|
762
|
+
resolveGitSha,
|
|
759
763
|
parseStateRefTags,
|
|
760
764
|
defaultWebSocketFactory,
|
|
761
765
|
queryRelay,
|
|
@@ -769,4 +773,4 @@ export {
|
|
|
769
773
|
resolveAccountIndex,
|
|
770
774
|
resolveIdentity
|
|
771
775
|
};
|
|
772
|
-
//# sourceMappingURL=chunk-
|
|
776
|
+
//# sourceMappingURL=chunk-QKL53YZS.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/nip34-events.ts","../src/remote-state.ts","../../../node_modules/.pnpm/@toon-protocol+arweave@0.2.0/node_modules/@toon-protocol/arweave/src/gateways.ts","../../../node_modules/.pnpm/@toon-protocol+arweave@0.2.0/node_modules/@toon-protocol/arweave/src/git-sha.ts","../src/nip34-refs.ts","../src/cli/identity.ts"],"sourcesContent":["/**\n * Pure NIP-34 event builders for the Git-to-TOON write path.\n *\n * Promoted from `packages/rig-web/tests/e2e/seed/lib/event-builders.ts` (#223).\n * All builders return UnsignedEvent — the caller signs with their keypair\n * via finalizeEvent() and publishes through a Publisher (#226). Tag\n * structures follow the NIP-34 spec and `@toon-protocol/core/nip34`.\n *\n * The wire contract — every tag each builder writes, the NIP-34 clause it\n * follows, and any legacy shape still read (with its end condition) — is\n * `docs/nip34-wire-shapes.md`. Read that before changing a tag shape here;\n * update it after. Where the two disagree, this file's actual builders are\n * the ground truth and the document is the bug.\n */\n\nimport { getAddress, isAddress } from 'viem';\nimport {\n ISSUE_KIND,\n PATCH_KIND,\n REPOSITORY_ANNOUNCEMENT_KIND,\n} from '@toon-protocol/core/nip34';\nimport type {\n STATUS_APPLIED_KIND,\n STATUS_CLOSED_KIND,\n STATUS_DRAFT_KIND,\n STATUS_OPEN_KIND,\n} from '@toon-protocol/core/nip34';\n\n// Kinds not (yet) exported by @toon-protocol/core/nip34:\n/** Repository State (refs) — replaceable, pairs with kind:30617 via `d` tag. */\nexport const REPOSITORY_STATE_KIND = 30618;\n/**\n * Comment on an issue or patch — NIP-22 `kind:1111`, which NIP-34's \"Replies\"\n * clause mandates (\"Replies … should follow NIP-22 comment\"). This is the kind\n * rig WRITES (rig#159).\n */\nexport const COMMENT_KIND = 1111;\n/**\n * rig's pre-#159 private comment dialect. READ-ONLY: never written again, kept\n * so threads published before that release keep rendering. Every reader merges\n * it with {@link COMMENT_KIND} by `created_at`.\n */\nexport const LEGACY_COMMENT_KIND = 1622;\n\n// ---------------------------------------------------------------------------\n// UnsignedEvent type (subset of nostr-tools — no id, sig, or pubkey)\n// ---------------------------------------------------------------------------\n\nexport interface UnsignedEvent {\n kind: number;\n content: string;\n tags: string[][];\n created_at: number;\n}\n\n// ---------------------------------------------------------------------------\n// kind:30617 — Repository Announcement (+ maintainer authority, #287)\n// ---------------------------------------------------------------------------\n\n/**\n * NIP-34 tag naming the repo's declared maintainers: one multi-valued tag\n * `[\"maintainers\", \"<hex-pubkey>\", \"<hex-pubkey>\", …]` on the kind:30617\n * announcement (mirrors the spec's multi-valued `relays` tag). The repo\n * OWNER — the announcement event's own pubkey — is ALWAYS an implicit\n * maintainer and need not be listed. Consumers derive an issue/PR's status\n * ONLY from kind:1630-1633 events signed by owner ∪ maintainers (#287): the\n * relay is permissionless, so this is the CONSUMER-side authority filter.\n */\nexport const MAINTAINERS_TAG = 'maintainers';\n\nconst HEX64 = /^[0-9a-f]{64}$/;\n\n/**\n * First value of the named tag, or `undefined`. Tag names are matched\n * CASE-SENSITIVELY, exactly as NIP-01 defines them: `E` (a NIP-22 root scope)\n * is a different tag from `e` (the parent item).\n */\nexport function firstTagValue(\n tags: string[][],\n name: string\n): string | undefined {\n return tags.find((t) => t[0] === name)?.[1];\n}\n\n/**\n * Collect the declared maintainer pubkeys (lowercased hex) from a kind:30617\n * event's tags. Tolerant of repeated `maintainers` tags and non-hex noise —\n * only 64-char hex values survive. Does NOT include the owner (implicit).\n */\nexport function parseMaintainers(tags: string[][]): string[] {\n const out: string[] = [];\n const seen = new Set<string>();\n for (const tag of tags) {\n if (tag[0] !== MAINTAINERS_TAG) continue;\n for (const value of tag.slice(1)) {\n const hex = value.toLowerCase();\n if (HEX64.test(hex) && !seen.has(hex)) {\n seen.add(hex);\n out.push(hex);\n }\n }\n }\n return out;\n}\n\n/**\n * The set of pubkeys whose kind:1630-1633 status events are authoritative for\n * a repo: the owner (always) ∪ the declared maintainers (from the 30617's\n * `maintainers` tag). All values are lowercased hex.\n */\nexport function authorizedStatusAuthors(\n ownerPubkey: string,\n repoAnnouncementTags: string[][]\n): Set<string> {\n return new Set([\n ownerPubkey.toLowerCase(),\n ...parseMaintainers(repoAnnouncementTags),\n ]);\n}\n\n// ---------------------------------------------------------------------------\n// Payout pointer (rig#92, part of the payout epic toon-protocol/toon-meta#391)\n// ---------------------------------------------------------------------------\n\n/**\n * NIP-34 tag naming the repo's declared payout pointer: a single\n * `[\"payout\", \"<chain>\", \"<address>\"]` tag on the kind:30617 announcement.\n * No pointer → no split, the serving node keeps 100% of repo-scoped write\n * fees (toon-meta#391 decision 2). The tag carries the chain label so the\n * shape survives future chains, but v1 accepts exactly one: `evm` — the\n * payout accrual ledger is EVM-only today\n * (`crates/connector-client-edge/src/btp.rs:270-272`).\n */\nexport const PAYOUT_TAG = 'payout';\n\n/** Chains the payout pointer can target. v1: `evm` only (toon-meta#391). */\nexport type PayoutChain = 'evm';\n\nconst SUPPORTED_PAYOUT_CHAINS: ReadonlySet<string> = new Set<PayoutChain>([\n 'evm',\n]);\n\n/** True when `chain` is a chain the payout pointer supports today. */\nfunction isSupportedPayoutChain(chain: string): chain is PayoutChain {\n return SUPPORTED_PAYOUT_CHAINS.has(chain);\n}\n\n/** A repo's declared payout pointer (parsed from / built into the `payout` tag). */\nexport interface PayoutPointer {\n chain: PayoutChain;\n /** EIP-55 checksummed address (never a raw lowercase/mixed-case echo). */\n address: string;\n}\n\n/**\n * True when `address` is a structurally valid, correctly-checksummed EVM\n * address: `0x` + 40 hex chars, and — when mixed-case — a valid EIP-55\n * checksum (an all-lowercase address is accepted as \"unchecksummed\").\n */\nexport function isValidEvmPayoutAddress(address: string): boolean {\n return isAddress(address);\n}\n\n/**\n * Collect the repo's declared payout pointer from a kind:30617 event's tags.\n * Only `[\"payout\", \"evm\", <address>]` with a shape- and checksum-valid\n * address is accepted; the address is normalized to its EIP-55 checksummed\n * form. Tolerant of relay noise: an unsupported chain or malformed address\n * is skipped (not thrown). The relay is permissionless and 30617 is\n * replaceable, so a legacy/hand-crafted event could carry more than one\n * `payout` tag — the first valid one wins and the rest are ignored (a\n * `console.warn` notes the drop so a stray extra tag isn't silently\n * mysterious).\n */\nexport function parsePayout(tags: string[][]): PayoutPointer | null {\n let result: PayoutPointer | null = null;\n let ignored = 0;\n for (const tag of tags) {\n if (tag[0] !== PAYOUT_TAG) continue;\n const [, chain, address] = tag;\n if (\n result === null &&\n chain !== undefined &&\n isSupportedPayoutChain(chain) &&\n address !== undefined &&\n isValidEvmPayoutAddress(address)\n ) {\n result = { chain, address: getAddress(address) };\n } else {\n ignored++;\n }\n }\n if (ignored > 0) {\n console.warn(\n `rig: ignoring ${ignored} extra/invalid \"${PAYOUT_TAG}\" tag(s) on kind:30617` +\n (result ? ` — using ${result.chain} ${result.address}` : '')\n );\n }\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// kind:30618 — Repository Refs/State\n// ---------------------------------------------------------------------------\n\n/**\n * Maximum number of `arweave` (git SHA → Arweave txId) tags on ONE kind:30618\n * event, enforced on write (here and in `executePush`) and on read\n * (`parseRefsEvent`, rig-web's `parseRepoRefs`). Before this cap the map was\n * merged cumulatively on every push and bounded by nothing, so a large repo\n * eventually produced an event relays reject (#162).\n *\n * WHY 2000 — the arithmetic, in the event's JSON serialization:\n *\n * one arweave tag `[\"arweave\",\"<40-hex sha>\",\"<43-char txId>\"]`\n * = 1 + 9 + 1 + 42 + 1 + 45 + 1 = 100 bytes, +1 comma = 101\n * 2000 of them = 202,000 bytes ≈ 197 KiB ← the map's whole budget\n *\n * Which limit is that 197 KiB measured against? rig's own relay\n * (`relay-ws.devnet.toonprotocol.dev`) advertises no `limitation` in its\n * NIP-11 document — probed 2026-09-17, the HTTPS origin answers `Upgrade\n * Required` rather than a relay-information document — so there is no\n * self-declared number to size against. The smallest limit any relay surveyed\n * for #153 declares is `relay.ngit.dev`'s 5 MiB message cap (probed the same\n * day), and that is the figure used here: the map is capped at under 4% of\n * it. That leaves the rest of the budget to the refs, which are the other\n * term in the same event:\n *\n * `[\"r\",\"<refname>\",\"<sha>\"]`, and `isSafeRefname` admits refnames up to\n * 1024 bytes → ≤ 1076 bytes each. Every rig reader ingests at most\n * MAX_REFS_PER_EVENT = 1000 of them, and 1000 at that adversarial width is\n * ≈ 1.03 MiB, so a readable event tops out near 1.23 MiB — a quarter of the\n * 5 MiB limit. With realistic refnames (≤ 64 bytes) a full-cap event is\n * ≈ 260 KiB.\n *\n * NOTE the asymmetry: the 1000-ref cap is a READ-side constant. `buildRepoRefs`\n * still writes every ref it is given, so a repo with more than 1000 refs can\n * publish an event past the figures above — that is the ref tags' problem, not\n * the map's, and it is the ref shape work's to fix (#153 §Repository state).\n * This cap's job is to stop the OBJECT MAP being the term that grows without\n * limit, which it was: it grew with the object count, not the ref count.\n *\n * Dropping an entry loses nothing: the object is still on Arweave under its\n * `Git-SHA` / `Repo` tags and resolves through the GraphQL resolver, which is\n * already the documented fallback (`RemoteState.resolveMissing`). The map is\n * a cache, not the index — so push planning must treat \"absent from the map\"\n * as \"ask the resolver\", NEVER as \"needs upload\".\n */\nexport const MAX_ARWEAVE_TAGS_PER_EVENT = 2000;\n\n/**\n * Build a kind:30618 repository refs/state event.\n *\n * Writes each ref in BOTH shapes, with identical SHAs (rig#157, dual-write\n * window): the NIP-34 shape `[<refPath>, <sha>]`, where the ref path IS the\n * tag name — what ngit, gitworkshop.dev and every other conformant client\n * read — alongside rig's legacy `[\"r\", <refPath>, <sha>]`, so rig installs\n * older than this release keep fetching. Both shapes are read by\n * {@link parseStateRefTags} in `./nip34-refs.ts` and by any legacy-only\n * reader that only knows the `r` shape. `HEAD` and `arweave` tags are\n * unaffected by this dual-write. The legacy write is removed in a later,\n * separately ticketed change once older rig versions are unsupported.\n *\n * At most {@link MAX_ARWEAVE_TAGS_PER_EVENT} `arweave` tags are emitted; a\n * larger `arweaveMap` is truncated to its first entries in iteration order,\n * so the caller decides priority (see `executePush`) and a repo under the cap\n * publishes byte-identically to before the cap existed.\n *\n * @param repoId - Repository identifier (d tag, matches kind:30617)\n * @param refs - Map of ref paths to commit SHAs (e.g., { 'refs/heads/main': 'abc123' })\n * @param arweaveMap - Map of git SHAs to Arweave transaction IDs\n */\nexport function buildRepoRefs(\n repoId: string,\n refs: Record<string, string>,\n arweaveMap: Record<string, string> = {}\n): UnsignedEvent {\n const tags: string[][] = [['d', repoId]];\n\n // Add ref tags, dual-written in both shapes (rig#157).\n for (const [refPath, commitSha] of Object.entries(refs)) {\n tags.push([refPath, commitSha]); // NIP-34 shape: ref path is the tag name\n tags.push(['r', refPath, commitSha]); // legacy shape, dual-write window\n }\n\n // Default HEAD to first ref (typically refs/heads/main)\n const firstRef = Object.keys(refs)[0];\n if (firstRef) {\n tags.push(['HEAD', `ref: ${firstRef}`]);\n }\n\n // Add arweave SHA-to-txId mapping tags, bounded (#162).\n const mapped = Object.entries(arweaveMap).slice(\n 0,\n MAX_ARWEAVE_TAGS_PER_EVENT\n );\n for (const [sha, txId] of mapped) {\n tags.push(['arweave', sha, txId]);\n }\n\n return {\n kind: REPOSITORY_STATE_KIND,\n content: '',\n tags,\n created_at: Math.floor(Date.now() / 1000),\n };\n}\n\n// ---------------------------------------------------------------------------\n// kind:1621 — Issue\n// ---------------------------------------------------------------------------\n\n/**\n * Build a kind:1621 issue event.\n *\n * @param repoOwnerPubkey - Pubkey of the repository owner\n * @param repoId - Repository identifier\n * @param title - Issue title (subject tag)\n * @param body - Issue body (Markdown content)\n * @param labels - Optional labels (t tags)\n */\nexport function buildIssue(\n repoOwnerPubkey: string,\n repoId: string,\n title: string,\n body: string,\n labels: string[] = []\n): UnsignedEvent {\n const tags: string[][] = [\n ['a', `${REPOSITORY_ANNOUNCEMENT_KIND}:${repoOwnerPubkey}:${repoId}`],\n ['p', repoOwnerPubkey],\n ['subject', title],\n ...labels.map((label) => ['t', label]),\n ];\n\n return {\n kind: ISSUE_KIND,\n content: body,\n tags,\n created_at: Math.floor(Date.now() / 1000),\n };\n}\n\n// ---------------------------------------------------------------------------\n// kind:1111 — NIP-22 comment (on an issue or patch)\n// ---------------------------------------------------------------------------\n\n/**\n * The event a comment thread hangs off: the kind:1621 issue or kind:1617\n * patch being discussed — NEVER the repository. Becomes the comment's\n * uppercase NIP-22 root scope (`E`/`K`/`P`).\n */\nexport interface CommentRoot {\n /** Event id of the issue/patch (uppercase `E`). */\n eventId: string;\n /** Kind of that event, e.g. 1621 or 1617 (uppercase `K`). */\n kind: number;\n /** Pubkey of that event's author (uppercase `P`). */\n authorPubkey: string;\n}\n\n/**\n * The kind:1111 comment being replied to, when the new comment is a nested\n * reply rather than a top-level comment. Becomes the lowercase parent\n * (`e`/`k`/`p`), with `k` always {@link COMMENT_KIND}.\n */\nexport interface CommentParent {\n /** Event id of the comment replied to (lowercase `e`). */\n eventId: string;\n /** Pubkey of that comment's author (lowercase `p`). */\n authorPubkey: string;\n}\n\n/**\n * Build a NIP-22 kind:1111 comment on a NIP-34 issue or patch (rig#159).\n *\n * NIP-22: \"Comments MUST point to the root scope using uppercase tag names\n * (e.g. `K`, `E`, `A` or `I`)\" and \"MUST point to the parent item with\n * lowercase ones (e.g. `k`, `e`, `a` or `i`)\", with \"`P` for the root scope\n * and `p` for the author of the parent item\". So:\n *\n * - top-level comment → parent === root: `e`/`k`/`p` repeat `E`/`K`/`P`;\n * - reply → `e` is the parent comment, `k` is `1111`, `p` its author, while\n * `E`/`K`/`P` still name the issue/patch at the top of the thread.\n *\n * The repo coordinate rides along as an ordinary lowercase `a` tag so a\n * subscription can scope a whole repo's comments with one `#a` filter (spec\n * rig#153). It is deliberately NOT the NIP-22 parent pointer — the parent is\n * the `e` tag — so it is emitted last, after the six threading tags.\n *\n * @param repoOwnerPubkey - Pubkey of the repository owner\n * @param repoId - Repository identifier (NIP-34 `d` tag)\n * @param root - The issue/patch the thread hangs off\n * @param body - Comment body (Markdown content)\n * @param parent - The kind:1111 comment being replied to; omit for a\n * top-level comment, whose parent is the root itself\n */\nexport function buildComment(\n repoOwnerPubkey: string,\n repoId: string,\n root: CommentRoot,\n body: string,\n parent?: CommentParent\n): UnsignedEvent {\n const parentTags: string[][] =\n parent === undefined\n ? [\n ['e', root.eventId, '', root.authorPubkey],\n ['k', String(root.kind)],\n ['p', root.authorPubkey],\n ]\n : [\n ['e', parent.eventId, '', parent.authorPubkey],\n ['k', String(COMMENT_KIND)],\n ['p', parent.authorPubkey],\n ];\n\n return {\n kind: COMMENT_KIND,\n content: body,\n tags: [\n ['E', root.eventId, '', root.authorPubkey],\n ['K', String(root.kind)],\n ['P', root.authorPubkey],\n ...parentTags,\n ['a', `${REPOSITORY_ANNOUNCEMENT_KIND}:${repoOwnerPubkey}:${repoId}`],\n ],\n created_at: Math.floor(Date.now() / 1000),\n };\n}\n\n/**\n * Does `event` belong to the comment thread rooted at `rootEventId`?\n *\n * kind:1111 membership is decided by the **uppercase** `E` tag, matched\n * case-sensitively — the same rule rig already applies to kind:1619 PR\n * updates. A kind:1111 whose only match is a lowercase `e` is a reply to\n * something else and is NOT part of this thread. Legacy kind:1622 has no\n * uppercase form: its `e` tag is the thread root.\n */\nexport function commentBelongsToThread(\n event: { kind: number; tags: string[][] },\n rootEventId: string\n): boolean {\n if (event.kind === COMMENT_KIND) {\n return event.tags.some((t) => t[0] === 'E' && t[1] === rootEventId);\n }\n if (event.kind === LEGACY_COMMENT_KIND) {\n return event.tags.some((t) => t[0] === 'e' && t[1] === rootEventId);\n }\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// kind:1617 — Patch / PR\n// ---------------------------------------------------------------------------\n\n/**\n * Build a kind:1617 patch event.\n *\n * The PR body/description travels in a dedicated `description` tag, NEVER in\n * `content` (#280): `content` is real `git format-patch` output that readers\n * pipe straight into `git am`, and git's patch-format detection hard-fails on\n * any leading prose (verified: \"Patch format detection failed.\"). The tag\n * route keeps `git am` consumption intact while `rig pr show` and the\n * rig-web/views `parsePR` renderers surface the description.\n *\n * @param repoOwnerPubkey - Pubkey of the repository owner\n * @param repoId - Repository identifier\n * @param title - Patch/PR title (subject tag)\n * @param commits - Array of { sha, parentSha } for commit and parent-commit tags\n * @param branchTag - Branch name, written as the `branch-name` tag (#161) —\n * the wider NIP-34 ecosystem's spelling (used verbatim by\n * kind:1618 pull requests; confirmed against the NIP-34\n * source at implementation time). Never written to `t`:\n * that tag is reserved for real labels.\n * @param content - Real `git format-patch` text (NIP-34 patch body); defaults\n * to '' for callers that only reference commits by tag\n * @param description - PR body/cover text (`description` tag) — kept out of\n * `content` so `git am` still applies it\n */\nexport function buildPatch(\n repoOwnerPubkey: string,\n repoId: string,\n title: string,\n commits: { sha: string; parentSha: string }[],\n branchTag?: string,\n content = '',\n description?: string\n): UnsignedEvent {\n const tags: string[][] = [\n ['a', `${REPOSITORY_ANNOUNCEMENT_KIND}:${repoOwnerPubkey}:${repoId}`],\n ['p', repoOwnerPubkey],\n ['subject', title],\n ];\n\n if (description !== undefined && description !== '') {\n tags.push(['description', description]);\n }\n\n for (const commit of commits) {\n tags.push(['commit', commit.sha]);\n tags.push(['parent-commit', commit.parentSha]);\n }\n\n if (branchTag) {\n tags.push(['branch-name', branchTag]);\n }\n\n return {\n kind: PATCH_KIND,\n content,\n tags,\n created_at: Math.floor(Date.now() / 1000),\n };\n}\n\n// ---------------------------------------------------------------------------\n// kind:1630-1633 — Status\n// ---------------------------------------------------------------------------\n\n/** Status kinds: 1630 open, 1631 applied/merged, 1632 closed, 1633 draft. */\nexport type StatusKind =\n | typeof STATUS_OPEN_KIND\n | typeof STATUS_APPLIED_KIND\n | typeof STATUS_CLOSED_KIND\n | typeof STATUS_DRAFT_KIND;\n\n/**\n * Build a status event (kind 1630-1633).\n *\n * rig#153/#160: the `e` tag carries the NIP-10 `root` marker\n * (`[\"e\", <target>, \"\", \"root\"]`) rather than the old bare `[\"e\", <id>]`, so\n * clients that resolve a status's root via the marker (ngit's\n * `get_event_root`, among others) honor rig statuses. The repo `a` tag rides\n * along so a status stream can be scoped to the repository without first\n * resolving the target. Readers (this package's tracker and rig-web's\n * parsers) accept BOTH this marker form and the legacy bare form — a status\n * is matched by `tags.find(t => t[0] === 'e')?.[1] === targetEventId`\n * regardless of any trailing marker elements.\n *\n * @param repoOwnerPubkey - Pubkey of the repository owner\n * @param repoId - Repository identifier\n * @param targetEventId - Event ID of the patch, PR, or issue being updated\n * @param statusKind - One of 1630 (open), 1631 (applied), 1632 (closed), 1633 (draft)\n * @param targetPubkey - Optional pubkey of the target event author (p tag per NIP-34 StatusEvent), when known\n */\nexport function buildStatus(\n repoOwnerPubkey: string,\n repoId: string,\n targetEventId: string,\n statusKind: StatusKind,\n targetPubkey?: string\n): UnsignedEvent {\n const tags: string[][] = [\n ['e', targetEventId, '', 'root'],\n ['a', `${REPOSITORY_ANNOUNCEMENT_KIND}:${repoOwnerPubkey}:${repoId}`],\n ];\n if (targetPubkey) {\n tags.push(['p', targetPubkey]);\n }\n return {\n kind: statusKind,\n content: '',\n tags,\n created_at: Math.floor(Date.now() / 1000),\n };\n}\n","/**\n * Remote repository state reader — the \"what does the remote have?\" half of\n * `rig push` (epic #222, ticket #225).\n *\n * Queries relay(s) over NIP-01 WebSocket for the repository's NIP-34 state:\n * - kind:30618 (repository state): ref tags → ref map (both the NIP-34\n * shape and rig's legacy `r` shape — see `./nip34-refs.ts`), `HEAD`\n * symref, `arweave` tags → git SHA → Arweave txId hints.\n * - kind:30617 (repository announcement): presence = the repo exists on\n * TOON (first-push detection) + name/description/relays metadata.\n *\n * Both kinds are NIP-33 parameterized-replaceable: per relay only the latest\n * event per (kind, author, d) survives, but we still pick the newest across\n * relays (highest created_at, ties broken by lowest id per NIP-01).\n *\n * SHAs missing from the `arweave` tag map can be resolved via the shared\n * Arweave GraphQL Git-SHA resolver (@toon-protocol/arweave — the same module\n * the rig SPA uses) through {@link RemoteState.resolveMissing}.\n *\n * Relay payload encodings (mirrors rig's `web/relay-client.ts` decode logic):\n * EVENT payloads arrive as an inline JSON object (standard NIP-01), as a\n * double-JSON-encoded string (devnet relay quirk: a JSON string containing\n * the event JSON), or as a TOON-encoded string. All three are tolerated.\n */\n\nimport { decode as decodeToon } from '@toon-format/toon';\nimport {\n resolveGitSha,\n seedShaCache,\n shaCacheKey,\n} from '@toon-protocol/arweave';\nimport { REPOSITORY_ANNOUNCEMENT_KIND } from '@toon-protocol/core/nip34';\n\nimport {\n MAX_ARWEAVE_TAGS_PER_EVENT,\n REPOSITORY_STATE_KIND,\n parseMaintainers,\n parsePayout,\n type PayoutPointer,\n} from './nip34-events.js';\nimport { parseStateRefTags, type ParsedStateRefs } from './nip34-refs.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** A signed Nostr event as seen on a relay (read side). */\nexport interface NostrEvent {\n id: string;\n pubkey: string;\n created_at: number;\n kind: number;\n tags: string[][];\n content: string;\n sig: string;\n}\n\n/** NIP-01 subscription filter (only the fields rig's readers send). */\nexport interface NostrFilter {\n ids?: string[];\n kinds?: number[];\n authors?: string[];\n '#d'?: string[];\n /** Repo address tag filter, e.g. `30617:<owner>:<repoId>` (#278 tracker). */\n '#a'?: string[];\n /** Event-reference tag filter (#278 tracker: statuses + comments). */\n '#e'?: string[];\n /**\n * NIP-22 UPPERCASE root-scope reference — distinct from `#e` and matched\n * case-sensitively (#159: kind:1111 comments hang off their thread root's\n * `E` tag, while `e` names the immediate parent).\n */\n '#E'?: string[];\n /** Commit tag filter (#125 CI: results/progress/jobs for one commit). */\n '#c'?: string[];\n /** Addressee tag filter (#125 CI: a coordinator's inbox — 9843/9844/9840/29846). */\n '#p'?: string[];\n /** Only events at or after this unix time (#125 coordinator cursor resume). */\n since?: number;\n limit?: number;\n}\n\n/**\n * Minimal structural WebSocket type — satisfied by the WHATWG WebSocket\n * global (Node >= 22 / undici, browsers) and by the `ws` package.\n */\nexport interface WebSocketLike {\n readyState: number;\n send(data: string): void;\n close(): void;\n addEventListener(type: string, listener: (event: never) => void): void;\n}\n\n/** Factory for WebSocket connections (injectable for tests / `ws` fallback). */\nexport type WebSocketFactory = (url: string) => WebSocketLike;\n\nexport interface FetchRemoteStateOptions {\n /**\n * Relay WebSocket URLs to query. Plural from day one (forward-compat with\n * multi-relay routing); size 1 is typical today. Results are merged and the\n * newest replaceable event across all relays wins.\n */\n relayUrls: string[];\n /** Repository owner's pubkey (hex) — the author of 30617/30618. */\n ownerPubkey: string;\n /** Repository identifier (NIP-34 `d` tag). */\n repoId: string;\n /** Per-relay timeout in milliseconds (default 10000). On timeout the relay contributes whatever it sent so far. */\n timeoutMs?: number;\n /**\n * Git-SHA → Arweave txId resolver used by {@link RemoteState.resolveMissing}.\n * Defaults to the shared GraphQL resolver from @toon-protocol/arweave,\n * which asks `arweave.net` — so every command that takes a gateway passes\n * one bound to it instead (`../git-sha-resolver.ts`, #183). Injectable for\n * tests.\n */\n resolveSha?: (sha: string, repo: string) => Promise<string | null>;\n /** WebSocket constructor override (defaults to the global WebSocket). */\n webSocketFactory?: WebSocketFactory;\n}\n\n/** Remote repository state assembled from relay events. */\nexport interface RemoteState {\n /** True when a kind:30617 announcement exists (false ⇒ first push). */\n announced: boolean;\n /** Ref map from the latest kind:30618: refname → commit SHA. */\n refs: Map<string, string>;\n /** HEAD symref target (e.g. `refs/heads/main`), or null if unset. */\n headSymref: string | null;\n /**\n * Git SHA → Arweave txId hints from the latest kind:30618 `arweave` tags,\n * capped at `MAX_ARWEAVE_TAGS_PER_EVENT` (#162). A HINT MAP, not an index:\n * a SHA missing from it is resolved via {@link RemoteState.resolveMissing},\n * never assumed absent from Arweave.\n */\n shaToTxId: Map<string, string>;\n /** The latest kind:30618 event, or null if the repo has no state yet. */\n refsEvent: NostrEvent | null;\n /** The latest kind:30617 event, or null if the repo is unannounced. */\n announceEvent: NostrEvent | null;\n /** Repository name from the announcement `name` tag. */\n name: string | null;\n /** Announcement `description` tag (falls back to event content). */\n description: string | null;\n /** Relay URLs advertised in the announcement `relays` tag(s). */\n relays: string[];\n /**\n * Declared maintainer pubkeys (hex) from the announcement `maintainers`\n * tag (#287). Does NOT include the owner (an implicit maintainer). Empty\n * when unannounced or owner-only.\n */\n maintainers: string[];\n /**\n * Declared payout pointer (rig#92) from the announcement `payout` tag, or\n * `null` when unannounced or no pointer is declared — no pointer means the\n * serving node keeps 100% of repo-scoped write fees (toon-meta#391).\n */\n payout: PayoutPointer | null;\n /**\n * Resolve SHAs to Arweave txIds: served from the `arweave` tag map when\n * present, otherwise via the GraphQL Git-SHA resolver. SHAs that resolve\n * nowhere are omitted from the returned map.\n */\n resolveMissing(shas: string[]): Promise<Map<string, string>>;\n}\n\n// ---------------------------------------------------------------------------\n// Relay query (NIP-01 REQ → EVENT* → EOSE)\n// ---------------------------------------------------------------------------\n\n/** Validate that a relay URL uses a WebSocket scheme (mirrors rig's url-utils). */\nfunction isValidRelayUrl(url: string): boolean {\n return /^wss?:\\/\\//i.test(url);\n}\n\n/** WHATWG WebSocket OPEN ready state. */\nconst WS_OPEN = 1;\n\n/**\n * The production {@link WebSocketFactory}: the global WHATWG WebSocket (Node\n * >= 22). Shared by every relay reader so the \"no global WebSocket\" failure\n * reads the same everywhere.\n */\nexport function defaultWebSocketFactory(url: string): WebSocketLike {\n const ctor = (\n globalThis as { WebSocket?: new (url: string) => WebSocketLike }\n ).WebSocket;\n if (!ctor) {\n throw new Error(\n 'No global WebSocket constructor (Node >= 22 required) — pass webSocketFactory'\n );\n }\n return new ctor(url);\n}\n\n/**\n * Decode a relay EVENT payload, tolerating every encoding seen in the wild:\n * inline object (standard NIP-01), double-JSON-encoded string (devnet relay\n * serves the event as a JSON string containing the event JSON), or a\n * TOON-encoded string (rig's `decodeToonMessage` path).\n */\nfunction decodeEventPayload(payload: unknown): NostrEvent | null {\n if (payload !== null && typeof payload === 'object') {\n return payload as NostrEvent;\n }\n if (typeof payload !== 'string') {\n return null;\n }\n try {\n const parsed: unknown = JSON.parse(payload);\n if (parsed !== null && typeof parsed === 'object') {\n return parsed as NostrEvent;\n }\n } catch {\n // Not JSON — fall through to TOON\n }\n try {\n return decodeToon(payload) as unknown as NostrEvent;\n } catch {\n return null;\n }\n}\n\n/**\n * Query one relay: send a REQ, collect EVENTs until EOSE, then CLOSE.\n * Mirrors rig's `queryRelay` (partial results on timeout / early close).\n * Exported for reuse by the network-bootstrap discovery (kind:10032).\n */\nexport function queryRelay(\n relayUrl: string,\n filter: NostrFilter,\n timeoutMs: number,\n webSocketFactory: WebSocketFactory\n): Promise<NostrEvent[]> {\n return new Promise((resolve, reject) => {\n const events: NostrEvent[] = [];\n const subId = `git-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;\n let ws: WebSocketLike;\n // eslint-disable-next-line prefer-const -- assigned after `settle` is defined\n let timeoutHandle: ReturnType<typeof setTimeout>;\n let settled = false;\n\n const settle = (outcome: 'resolve' | 'reject', error?: Error) => {\n if (settled) return;\n settled = true;\n clearTimeout(timeoutHandle);\n try {\n if (ws.readyState === WS_OPEN) {\n ws.send(JSON.stringify(['CLOSE', subId]));\n }\n ws.close();\n } catch {\n // Ignore close errors\n }\n if (outcome === 'reject') {\n reject(error);\n } else {\n resolve(events);\n }\n };\n\n if (!isValidRelayUrl(relayUrl)) {\n reject(\n new Error(\n `Invalid relay URL protocol (must be ws:// or wss://): ${relayUrl}`\n )\n );\n return;\n }\n\n try {\n ws = webSocketFactory(relayUrl);\n } catch (err) {\n reject(\n new Error(`Failed to connect to relay ${relayUrl}: ${String(err)}`)\n );\n return;\n }\n\n timeoutHandle = setTimeout(() => {\n // Resolve with whatever we collected so far (partial results)\n settle('resolve');\n }, timeoutMs);\n\n ws.addEventListener('open', () => {\n ws.send(JSON.stringify(['REQ', subId, filter]));\n });\n\n ws.addEventListener('message', (msgEvent: { data?: unknown }) => {\n try {\n const msg = JSON.parse(String(msgEvent.data)) as unknown[];\n if (!Array.isArray(msg) || msg.length < 2) return;\n\n const msgType = msg[0];\n if (msgType === 'EVENT' && msg[1] === subId && msg[2] !== undefined) {\n const event = decodeEventPayload(msg[2]);\n if (event) events.push(event);\n } else if (msgType === 'EOSE' && msg[1] === subId) {\n settle('resolve');\n }\n } catch {\n // Ignore parse errors for individual messages\n }\n });\n\n ws.addEventListener('error', (event: { message?: unknown }) => {\n const detail =\n typeof event === 'object' && event !== null && 'message' in event\n ? String(event.message)\n : 'unknown';\n settle(\n 'reject',\n new Error(`WebSocket error connecting to ${relayUrl}: ${detail}`)\n );\n });\n\n ws.addEventListener('close', () => {\n // If we haven't settled yet, resolve with what we have\n settle('resolve');\n });\n });\n}\n\n// ---------------------------------------------------------------------------\n// NIP-33 replaceable selection + tag parsing\n// ---------------------------------------------------------------------------\n\n/**\n * Pick the winning replaceable event: highest created_at, ties broken by\n * lowest id (NIP-01 replaceable-event convention).\n */\nfunction latestReplaceable(events: NostrEvent[]): NostrEvent | null {\n let winner: NostrEvent | null = null;\n for (const event of events) {\n if (\n winner === null ||\n event.created_at > winner.created_at ||\n (event.created_at === winner.created_at && event.id < winner.id)\n ) {\n winner = event;\n }\n }\n return winner;\n}\n\n/** Get the first value for a tag name. */\nfunction getTagValue(tags: string[][], name: string): string | undefined {\n const tag = tags.find((t) => t[0] === name);\n return tag?.[1];\n}\n\ninterface ParsedRefs extends ParsedStateRefs {\n shaToTxId: Map<string, string>;\n}\n\n/**\n * Parse a kind:30618 event's ref / `HEAD` / `arweave` tags.\n *\n * Ref and `HEAD` tags go through the shared dual-shape parser\n * (`./nip34-refs.ts`), so a state event written by ngit (or any other\n * conformant NIP-34 client) reads exactly like one written by rig — see that\n * module for the conflict, peeled-tag and cap rules (rig#156).\n *\n * Both tag lists are capped — refs at {@link MAX_REFS_PER_EVENT} inside that\n * parser, the object map at {@link MAX_ARWEAVE_TAGS_PER_EVENT} here — so a\n * hostile relay cannot exhaust memory with one giant state event (#162).\n * Truncating the map is safe: a SHA it does not cover is resolved through the\n * GraphQL `Git-SHA` resolver by {@link RemoteState.resolveMissing}.\n */\nfunction parseRefsEvent(event: NostrEvent): ParsedRefs {\n const { refs, headSymref } = parseStateRefTags(event.tags);\n const shaToTxId = new Map<string, string>();\n\n for (const tag of event.tags) {\n if (tag[0] === 'arweave' && tag[1] && tag[2]) {\n // Hostile-relay guard (#162): a relay may serve a state event with any\n // number of `arweave` tags. Ingest no more than rig itself writes.\n if (shaToTxId.size >= MAX_ARWEAVE_TAGS_PER_EVENT) continue;\n shaToTxId.set(tag[1], tag[2]);\n }\n }\n\n return { refs, headSymref, shaToTxId };\n}\n\n// ---------------------------------------------------------------------------\n// fetchRemoteState\n// ---------------------------------------------------------------------------\n\n/**\n * Fetch the remote repository state from the relay(s).\n *\n * Sends one REQ per relay for kind:30617 + kind:30618 with\n * `authors=[ownerPubkey]`, `#d=[repoId]`, collects until EOSE (or timeout),\n * and reduces to the latest replaceable event per kind.\n *\n * Resolves as long as at least one relay answers; throws only when every\n * relay fails. Events from other authors / repos are ignored (defense\n * against misbehaving relays that over-return).\n */\nexport async function fetchRemoteState(\n options: FetchRemoteStateOptions\n): Promise<RemoteState> {\n const {\n relayUrls,\n ownerPubkey,\n repoId,\n timeoutMs = 10000,\n resolveSha = resolveGitSha,\n webSocketFactory = defaultWebSocketFactory,\n } = options;\n\n if (relayUrls.length === 0) {\n throw new Error('fetchRemoteState: relayUrls must not be empty');\n }\n if (!ownerPubkey) {\n throw new Error('fetchRemoteState: ownerPubkey is required');\n }\n if (!repoId) {\n throw new Error('fetchRemoteState: repoId is required');\n }\n\n const filter: NostrFilter = {\n kinds: [REPOSITORY_ANNOUNCEMENT_KIND, REPOSITORY_STATE_KIND],\n authors: [ownerPubkey],\n '#d': [repoId],\n };\n\n const results = await Promise.allSettled(\n relayUrls.map((url) => queryRelay(url, filter, timeoutMs, webSocketFactory))\n );\n\n const failures: string[] = [];\n const byId = new Map<string, NostrEvent>();\n for (const result of results) {\n if (result.status === 'rejected') {\n failures.push(String((result.reason as Error)?.message ?? result.reason));\n continue;\n }\n for (const event of result.value) {\n // Only trust events from the repo owner for this repo — relays are\n // untrusted and may over-return.\n if (event.pubkey !== ownerPubkey) continue;\n if (getTagValue(event.tags, 'd') !== repoId) continue;\n if (typeof event.id === 'string' && !byId.has(event.id)) {\n byId.set(event.id, event);\n }\n }\n }\n\n if (failures.length === relayUrls.length) {\n throw new Error(\n `fetchRemoteState: all ${relayUrls.length} relay(s) failed: ${failures.join('; ')}`\n );\n }\n\n const events = [...byId.values()];\n const refsEvent = latestReplaceable(\n events.filter((e) => e.kind === REPOSITORY_STATE_KIND)\n );\n const announceEvent = latestReplaceable(\n events.filter((e) => e.kind === REPOSITORY_ANNOUNCEMENT_KIND)\n );\n\n const { refs, headSymref, shaToTxId } = refsEvent\n ? parseRefsEvent(refsEvent)\n : {\n refs: new Map<string, string>(),\n headSymref: null,\n shaToTxId: new Map<string, string>(),\n };\n\n // Seed the shared resolver cache so later resolveGitSha calls (here or in\n // any other consumer of @toon-protocol/arweave) skip GraphQL for known SHAs.\n if (shaToTxId.size > 0) {\n seedShaCache(\n [...shaToTxId].map(\n ([sha, txId]) => [shaCacheKey(sha, repoId), txId] as [string, string]\n )\n );\n }\n\n // Announcement metadata (mirrors views' parseRepoAnnouncement defaults).\n const name = announceEvent\n ? (getTagValue(announceEvent.tags, 'name') ?? null)\n : null;\n const description = announceEvent\n ? (getTagValue(announceEvent.tags, 'description') ?? announceEvent.content)\n : null;\n // NIP-34 announcement relays tag is multi-valued: [\"relays\", url1, url2, …]\n const relays: string[] = [];\n if (announceEvent) {\n for (const tag of announceEvent.tags) {\n if (tag[0] === 'relays') {\n relays.push(...tag.slice(1).filter((url) => url.length > 0));\n }\n }\n }\n // Declared maintainers (#287): the `maintainers` tag on the 30617. Owner is\n // an implicit maintainer and is NOT listed here.\n const maintainers = announceEvent ? parseMaintainers(announceEvent.tags) : [];\n // Declared payout pointer (rig#92): the `payout` tag on the 30617.\n const payout = announceEvent ? parsePayout(announceEvent.tags) : null;\n\n const resolveMissing = async (\n shas: string[]\n ): Promise<Map<string, string>> => {\n const resolved = new Map<string, string>();\n const missing: string[] = [];\n for (const sha of new Set(shas)) {\n const known = shaToTxId.get(sha);\n if (known !== undefined) {\n resolved.set(sha, known);\n } else {\n missing.push(sha);\n }\n }\n const lookups = await Promise.all(\n missing.map(async (sha) => [sha, await resolveSha(sha, repoId)] as const)\n );\n for (const [sha, txId] of lookups) {\n if (txId) resolved.set(sha, txId);\n }\n return resolved;\n };\n\n return {\n announced: announceEvent !== null,\n refs,\n headSymref,\n shaToTxId,\n refsEvent,\n announceEvent,\n name,\n description,\n relays,\n maintainers,\n payout,\n resolveMissing,\n };\n}\n",null,null,"/**\n * Repository-state ref parsing for kind:30618 (spec rig#153, ticket rig#156).\n *\n * ONE parser shared by every rig-side reader of a repository state event —\n * the CLI's remote-state reader (`./remote-state.ts`, behind `rig clone` and\n * `rig fetch`) and the CI Coordinator's push watcher\n * (`./ci/coordinator.ts`), whose commit materialization reads the refs it\n * produces. `@toon-protocol/rig-web` cannot import this module (it is only a\n * DEVELOPMENT dependency of that package, resolved through a `dist/` that\n * does not exist when the repo's typecheck gate runs), so it keeps a\n * documented COPY of this logic in `nip34-parsers.ts` — the same\n * \"copy rather than depend, and say so\" pattern as `gateway-preference.ts`\n * and the ngit wire fixtures. Both are tested against the same captured ngit\n * events (`./nip34-fixtures/`).\n *\n * TWO ref tag shapes are accepted:\n *\n * - **NIP-34 shape** — `[\"refs/heads/main\", \"<sha>\"]`. The ref path IS the\n * tag name. This is what NIP-34 specifies and what ngit, gitworkshop.dev\n * and the rest of the ecosystem write.\n * - **Legacy rig shape** — `[\"r\", \"refs/heads/main\", \"<sha>\"]`. rig's own\n * spelling, still written during the dual-write window (rig#157) and read\n * forever after, so every repo pushed before rig#153 stays clonable.\n *\n * The rules, all from rig#153's Implementation Decisions:\n *\n * - A tag is a NIP-shape ref when its name starts with `refs/heads/` or\n * `refs/tags/`. Nothing else in the NIP shape is read as a ref, so a\n * hostile relay cannot smuggle a bare `[\"--upload-pack=…\", …]` in as one.\n * - A NIP-shape name ending in `^{}` is a PEELED annotated tag (ngit emits\n * these for `git fetch --prune`). It is not a ref and is not listed.\n * - When both shapes name the same ref with DIFFERENT shas, **the NIP shape\n * wins**, whichever came first in tag order — so two rig versions reading\n * the same event can never silently disagree.\n * - {@link MAX_REFS_PER_EVENT} caps DISTINCT refs across both shapes\n * combined, so a dual-written event cannot double the limit. The NIP shape\n * also gets FIRST CLAIM on those slots: each shape is collected on its own\n * and the two are merged NIP-first, so which refs survive a >1000-ref\n * event never depends on the order a writer (or a hostile relay) happened\n * to interleave the two shapes in.\n *\n * What this parser deliberately does NOT do is validate. Refname safety\n * (`isSafeRefname`, ./materialize.ts) and full-SHA checks live at the\n * git-invoking boundary, where they apply to every ref regardless of which\n * tag shape carried it — so the NIP shape is not a hostile-relay bypass, and\n * stays byte-for-byte as strict as the `r` shape it joins.\n *\n * See `docs/nip34-wire-shapes.md` for the full kind:30618 wire contract,\n * including the dual-write end condition and the object-map (`arweave` tag)\n * cap. Where the two disagree, this file's actual code is the ground\n * truth.\n */\n\n/**\n * Maximum number of DISTINCT refs parsed from a single kind:30618 event,\n * counted across both tag shapes combined.\n */\nexport const MAX_REFS_PER_EVENT = 1000;\n\n/** Symref prefix used in `HEAD` tags: `[\"HEAD\", \"ref: refs/heads/main\"]`. */\nconst SYMREF_PREFIX = 'ref: ';\n\n/** Tag-name prefixes that make a NIP-34-shaped tag a ref. */\nconst NIP_REF_PREFIXES = ['refs/heads/', 'refs/tags/'] as const;\n\n/** Suffix marking a peeled annotated-tag entry: `refs/tags/v1.0.0^{}`. */\nconst PEELED_SUFFIX = '^{}';\n\n/**\n * Is this tag NAME a NIP-34-shaped ref path? True for `refs/heads/…` and\n * `refs/tags/…`, including the peeled `^{}` spellings — {@link\n * parseStateRefTags} drops those separately.\n */\nfunction isNipRefTagName(name: string): boolean {\n return NIP_REF_PREFIXES.some((prefix) => name.startsWith(prefix));\n}\n\n/**\n * Merge the two shapes' refs into one map: NIP shape first, so it both wins\n * every same-ref disagreement and takes first claim on the cap's slots;\n * legacy refs fill whatever remains.\n */\nfunction mergeShapes(\n nip: Map<string, string>,\n legacy: Map<string, string>\n): Map<string, string> {\n const refs = new Map([...nip].slice(0, MAX_REFS_PER_EVENT));\n for (const [refname, sha] of legacy) {\n if (refs.size >= MAX_REFS_PER_EVENT) break;\n if (nip.has(refname)) continue;\n refs.set(refname, sha);\n }\n return refs;\n}\n\n/** Refs and the HEAD symref read out of one kind:30618 event's tags. */\nexport interface ParsedStateRefs {\n /** Refname → commit sha, capped at {@link MAX_REFS_PER_EVENT} distinct refs. */\n refs: Map<string, string>;\n /** HEAD symref target (e.g. `refs/heads/main`), or null when unset. */\n headSymref: string | null;\n}\n\n/**\n * Parse the ref and `HEAD` tags of a kind:30618 repository state event,\n * accepting the NIP-34 shape and rig's legacy `r` shape together. See the\n * module header for the conflict, peeled-tag and cap rules.\n *\n * `arweave` tags are NOT read here — they are an object-location map, not\n * ref state, and each caller handles them itself.\n */\nexport function parseStateRefTags(tags: string[][]): ParsedStateRefs {\n // Each shape is staged on its own, then merged NIP-first (see mergeShapes).\n // Both staging maps are themselves bounded, so a relay that serves a\n // million ref tags costs bounded memory, not a million entries.\n const nip = new Map<string, string>();\n const legacy = new Map<string, string>();\n let headSymref: string | null = null;\n\n for (const tag of tags) {\n const [name, v1, v2] = tag;\n if (name === undefined) continue;\n\n if (name === 'r' && v1 && v2) {\n if (v1 === 'HEAD' && v2.startsWith(SYMREF_PREFIX)) {\n // Alternate symref spelling: [\"r\", \"HEAD\", \"ref: refs/heads/main\"]\n headSymref = v2.slice(SYMREF_PREFIX.length);\n continue;\n }\n if (legacy.size < MAX_REFS_PER_EVENT || legacy.has(v1)) {\n legacy.set(v1, v2);\n }\n continue;\n }\n\n if (name === 'HEAD' && v1?.startsWith(SYMREF_PREFIX)) {\n // NIP-34 symref tag: [\"HEAD\", \"ref: refs/heads/main\"]\n headSymref = v1.slice(SYMREF_PREFIX.length);\n continue;\n }\n\n if (v1 && isNipRefTagName(name) && !name.endsWith(PEELED_SUFFIX)) {\n // NIP-34 ref tag: [\"refs/heads/main\", \"<sha>\"]\n if (nip.size < MAX_REFS_PER_EVENT || nip.has(name)) nip.set(name, v1);\n }\n }\n\n return { refs: mergeShapes(nip, legacy), headSymref };\n}\n","/**\n * Identity resolution for the standalone-only `rig` CLI (#248).\n *\n * One precedence chain, used by every command that signs or pays:\n *\n * 1. `RIG_MNEMONIC` environment variable\n * 2. `TOON_CLIENT_MNEMONIC` environment variable — DEPRECATED alias of the\n * env tier (a one-line stderr warning fires when it is the source)\n * 3. project-local `.env` — walked up from the working directory (through\n * the repo root to the filesystem root); ONLY the `RIG_MNEMONIC` line is\n * parsed out of it (never loaded into the process env, never required\n * to exist)\n * 4. the shared `~/.toon-client` state dir (`TOON_CLIENT_HOME` override):\n * encrypted keystore (`keystorePath` + `TOON_CLIENT_KEYSTORE_PASSWORD`,\n * auto-password fallback for daemon-provisioned keystores), then the\n * plain `mnemonic` config field — the pre-#248 standalone-context logic\n *\n * The BIP-44 account index follows the same env-over-config precedence:\n *\n * 1. `RIG_ACCOUNT_INDEX` environment variable (applies to every source)\n * 2. `mnemonicAccountIndex` from the shared config file — but ONLY when the\n * phrase itself came from that shared state dir (keystore/config tiers).\n * An env- or `.env`-sourced phrase derives at index 0 unless\n * `RIG_ACCOUNT_INDEX` says otherwise, so an explicit `RIG_MNEMONIC`\n * pins the same identity no matter which `TOON_CLIENT_HOME` is active\n * (#384 — a per-home config must never silently shift an explicitly\n * provided identity).\n * 3. account 0\n *\n * The phrase itself is never written anywhere by rig (not to git config, not\n * to any repo file) and never printed — callers report only the SOURCE and\n * the derived pubkey.\n *\n * Key derivation and keystore decryption live in the `@toon-protocol/client`\n * dependency, loaded via dynamic import so this module can be statically\n * imported by every command without dragging the client into runs that fail\n * earlier (usage errors, missing git repo, …).\n */\n\nimport { deriveNostrKeyFromMnemonic } from '../standalone/nostr-identity.js';\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { dirname, join } from 'node:path';\n\n/** Where the mnemonic came from (reported in output; the phrase never is). */\nexport type IdentitySourceKind =\n /** `RIG_MNEMONIC` environment variable. */\n | 'env'\n /** `TOON_CLIENT_MNEMONIC` environment variable (deprecated alias). */\n | 'env-alias'\n /** `RIG_MNEMONIC` parsed out of a project-local `.env` file. */\n | 'dotenv'\n /** Encrypted keystore referenced by the shared client config. */\n | 'keystore'\n /** Plain `mnemonic` field of the shared client config file. */\n | 'config';\n\n/** A resolved signing identity: source + derived pubkey (never the phrase). */\nexport interface ResolvedIdentity {\n /** The BIP-39 phrase. Handle with care; never log or persist it. */\n mnemonic: string;\n /** BIP-44 account index used for derivation — `RIG_ACCOUNT_INDEX` env,\n * else `mnemonicAccountIndex` from the shared config (home-sourced phrases\n * only), else 0. */\n accountIndex: number;\n source: IdentitySourceKind;\n /** Human-facing source label, e.g. `RIG_MNEMONIC env` or a file path. */\n sourceLabel: string;\n /** Derived Nostr pubkey (hex) — the repo owner / `toon.owner` value. */\n pubkey: string;\n}\n\n/** No mnemonic could be resolved anywhere on the chain. */\nexport class MissingIdentityError extends Error {\n constructor(configPath: string) {\n super(\n 'no identity found — rig needs a BIP-39 seed phrase to sign and pay.\\n' +\n 'Generate one now (no BIP-39 tooling needed):\\n' +\n ' • `rig identity create` — mint a fresh identity into the encrypted ' +\n 'keystore (shows the phrase once to back up)\\n' +\n 'Or point rig at an existing phrase (highest precedence first):\\n' +\n ' • RIG_MNEMONIC environment variable\\n' +\n ' • RIG_MNEMONIC=<phrase> in a project-local .env file (gitignore it!)\\n' +\n ` • the shared client config at ${configPath} (mnemonic or ` +\n 'keystorePath + TOON_CLIENT_KEYSTORE_PASSWORD)'\n );\n this.name = 'MissingIdentityError';\n }\n}\n\n// ---------------------------------------------------------------------------\n// Shared client config conventions (duplicated from client-mcp — the same\n// note as standalone/nonce-guard.ts: no @toon-protocol/client-mcp import)\n// ---------------------------------------------------------------------------\n\n/**\n * Duplicated daemon convention (client-mcp `daemon/config.ts`): the password\n * used to encrypt a keystore rig auto-provisions (`rig identity create`\n * with no `TOON_CLIENT_KEYSTORE_PASSWORD` set) so the identity reloads across\n * runs with no env var. At-rest obfuscation only.\n */\nexport const DEFAULT_KEYSTORE_PASSWORD = 'toon-client-default';\n\n/** Shared client state dir: `TOON_CLIENT_HOME`, else `~/.toon-client`. */\nexport function clientStateDir(env: NodeJS.ProcessEnv): string {\n return env['TOON_CLIENT_HOME'] ?? join(homedir(), '.toon-client');\n}\n\n/** Shared client config file path (`<state-dir>/config.json`). */\nexport function clientConfigPath(env: NodeJS.ProcessEnv): string {\n return join(clientStateDir(env), 'config.json');\n}\n\n/** Default encrypted-keystore path (`<state-dir>/keystore.json`) — the same\n * convention the daemon's first-run provisioning uses. */\nexport function clientKeystorePath(env: NodeJS.ProcessEnv): string {\n return join(clientStateDir(env), 'keystore.json');\n}\n\n/** The subset of the shared client config file identity resolution reads. */\nexport interface ClientConfigIdentityFields {\n mnemonic?: unknown;\n mnemonicAccountIndex?: unknown;\n keystorePath?: unknown;\n keystoreAutoPassword?: unknown;\n}\n\nexport function readClientConfigFile(path: string): ClientConfigIdentityFields {\n try {\n return JSON.parse(readFileSync(path, 'utf8')) as ClientConfigIdentityFields;\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return {};\n throw new Error(\n `failed to read client config at ${path}: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n}\n\n/**\n * Link a freshly written keystore into the shared client config so the\n * keystore tier of {@link resolveIdentity} (and the daemon) picks it up on the\n * next run — merging into any existing config, preserving every other field,\n * and writing owner-only (mode 0o600). `keystoreAutoPassword` records whether\n * rig chose the default password (so a user-set password still requires the\n * env var to decrypt, exactly like the daemon's first-run convention).\n */\nexport function linkKeystoreInClientConfig(\n env: NodeJS.ProcessEnv,\n keystorePath: string,\n autoPassword: boolean\n): string {\n const configPath = clientConfigPath(env);\n mkdirSync(dirname(configPath), { recursive: true });\n const existing = readClientConfigFile(configPath) as Record<string, unknown>;\n const merged: Record<string, unknown> = {\n ...existing,\n keystorePath,\n ...(autoPassword\n ? { keystoreAutoPassword: true }\n : // A user-set password: drop any stale auto flag so decryption\n // requires TOON_CLIENT_KEYSTORE_PASSWORD.\n { keystoreAutoPassword: false }),\n };\n writeFileSync(configPath, JSON.stringify(merged, null, 2) + '\\n', {\n encoding: 'utf8',\n mode: 0o600,\n });\n return configPath;\n}\n\n// ---------------------------------------------------------------------------\n// .env parsing (RIG_MNEMONIC only — never arbitrary env loading)\n// ---------------------------------------------------------------------------\n\nconst DOTENV_LINE_RE = /^\\s*(?:export\\s+)?RIG_MNEMONIC\\s*=\\s*(.*)\\s*$/;\n\n/**\n * Extract ONLY the `RIG_MNEMONIC` value from `.env` file content. Supports\n * `export ` prefixes, single/double quotes, full-line `#` comments, and\n * inline ` #` comments on unquoted values. Multiple assignments: the last\n * one wins (shell-sourcing semantics). An empty value counts as unset.\n * Nothing is ever loaded into `process.env`.\n */\nexport function parseDotenvMnemonic(content: string): string | undefined {\n let found: string | undefined;\n for (const line of content.split(/\\r?\\n/)) {\n const match = DOTENV_LINE_RE.exec(line);\n if (!match) continue;\n let value = (match[1] ?? '').trim();\n const quote = value[0];\n if ((quote === '\"' || quote === \"'\") && value.length >= 2 && value.endsWith(quote)) {\n value = value.slice(1, -1);\n } else {\n // Unquoted: an inline comment starts at the first whitespace-preceded #.\n const hash = value.search(/\\s#/);\n if (hash !== -1) value = value.slice(0, hash);\n value = value.trim();\n }\n if (value !== '') found = value;\n }\n return found;\n}\n\n/**\n * Walk up from `startDir` (the working directory — the walk passes through\n * the repo root) to the filesystem root, returning the first `.env` file\n * that defines `RIG_MNEMONIC`. `stopDir` bounds the walk (tests).\n */\nexport function findDotenvMnemonic(\n startDir: string,\n stopDir?: string\n): { path: string; mnemonic: string } | undefined {\n let dir = startDir;\n for (;;) {\n const candidate = join(dir, '.env');\n if (existsSync(candidate)) {\n try {\n const mnemonic = parseDotenvMnemonic(readFileSync(candidate, 'utf8'));\n if (mnemonic !== undefined) return { path: candidate, mnemonic };\n } catch {\n // Unreadable .env → keep walking; the file is never required.\n }\n }\n if (stopDir !== undefined && dir === stopDir) return undefined;\n const parent = dirname(dir);\n if (parent === dir) return undefined;\n dir = parent;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Account index (env > per-home config for home-sourced phrases > 0)\n// ---------------------------------------------------------------------------\n\n/** Sources whose phrase lives OUTSIDE the shared state dir. For these the\n * per-home `mnemonicAccountIndex` must not apply — an explicit\n * `RIG_MNEMONIC` derives the same identity across every\n * `TOON_CLIENT_HOME` (#384). */\nconst HOME_INDEPENDENT_SOURCES: ReadonlySet<IdentitySourceKind> = new Set([\n 'env',\n 'env-alias',\n 'dotenv',\n]);\n\n/**\n * Resolve the BIP-44 account index for a phrase from `source`:\n * `RIG_ACCOUNT_INDEX` env always wins; the shared config's\n * `mnemonicAccountIndex` applies only when the phrase itself came from the\n * shared state dir (keystore/config tiers); default 0. Throws on a\n * malformed `RIG_ACCOUNT_INDEX` — silently deriving a different identity\n * is exactly the surprise this precedence exists to prevent.\n */\nexport function resolveAccountIndex(\n env: NodeJS.ProcessEnv,\n source: IdentitySourceKind,\n file: ClientConfigIdentityFields\n): number {\n const raw = env['RIG_ACCOUNT_INDEX']?.trim();\n if (raw) {\n const parsed = Number(raw);\n if (!Number.isInteger(parsed) || parsed < 0) {\n throw new Error(\n `RIG_ACCOUNT_INDEX must be a non-negative integer, got ${JSON.stringify(raw)}`\n );\n }\n return parsed;\n }\n if (HOME_INDEPENDENT_SOURCES.has(source)) return 0;\n return typeof file.mnemonicAccountIndex === 'number' ? file.mnemonicAccountIndex : 0;\n}\n\n// ---------------------------------------------------------------------------\n// Resolution\n// ---------------------------------------------------------------------------\n\nexport interface ResolveIdentityOptions {\n env: NodeJS.ProcessEnv;\n /** Working directory the `.env` walk starts from. */\n cwd: string;\n /** Stderr line sink for the deprecated-alias warning. */\n warn(line: string): void;\n /** Bound the `.env` walk-up (tests only). */\n dotenvStopDir?: string;\n}\n\n/** The resolved source before any derivation (dependency-free). */\ninterface MnemonicSource {\n mnemonic: string;\n source: IdentitySourceKind;\n sourceLabel: string;\n}\n\nfunction resolveMnemonicSource(\n options: ResolveIdentityOptions,\n file: ClientConfigIdentityFields,\n configPath: string\n): MnemonicSource | { keystorePath: string } {\n const { env } = options;\n\n const rigEnv = env['RIG_MNEMONIC']?.trim();\n if (rigEnv) {\n return { mnemonic: rigEnv, source: 'env', sourceLabel: 'RIG_MNEMONIC env' };\n }\n\n const aliasEnv = env['TOON_CLIENT_MNEMONIC']?.trim();\n if (aliasEnv) {\n options.warn(\n 'rig: TOON_CLIENT_MNEMONIC is deprecated — rename the variable to RIG_MNEMONIC'\n );\n return {\n mnemonic: aliasEnv,\n source: 'env-alias',\n sourceLabel: 'TOON_CLIENT_MNEMONIC env (deprecated alias)',\n };\n }\n\n const dotenv = findDotenvMnemonic(options.cwd, options.dotenvStopDir);\n if (dotenv) {\n return {\n mnemonic: dotenv.mnemonic,\n source: 'dotenv',\n sourceLabel: dotenv.path,\n };\n }\n\n if (typeof file.keystorePath === 'string' && file.keystorePath !== '') {\n return { keystorePath: file.keystorePath };\n }\n if (typeof file.mnemonic === 'string' && file.mnemonic.trim() !== '') {\n return {\n mnemonic: file.mnemonic.trim(),\n source: 'config',\n sourceLabel: configPath,\n };\n }\n throw new MissingIdentityError(configPath);\n}\n\n/**\n * Load the client key helpers. `@toon-protocol/client` is a regular runtime\n * dependency (#259); the import stays dynamic purely so commands that fail\n * earlier never pay its startup cost.\n */\nasync function loadClientKeys(): Promise<{\n loadKeystore(path: string, password: string): string;\n deriveNostrKeyFromMnemonic(\n mnemonic: string,\n accountIndex?: number\n ): { secretKey: Uint8Array; pubkey: string };\n}> {\n const { loadKeystore } = await import('@toon-protocol/client');\n return { loadKeystore, deriveNostrKeyFromMnemonic };\n}\n\n/**\n * Resolve the CLI signing identity along the precedence chain and derive its\n * Nostr pubkey. Throws {@link MissingIdentityError} (with the full\n * three-option remediation) when nothing on the chain yields a phrase.\n */\nexport async function resolveIdentity(\n options: ResolveIdentityOptions\n): Promise<ResolvedIdentity> {\n const configPath = clientConfigPath(options.env);\n const file = readClientConfigFile(configPath);\n const picked = resolveMnemonicSource(options, file, configPath);\n\n const keys = await loadClientKeys();\n let source: MnemonicSource;\n if ('keystorePath' in picked) {\n const password =\n options.env['TOON_CLIENT_KEYSTORE_PASSWORD'] ??\n (file.keystoreAutoPassword === true ? DEFAULT_KEYSTORE_PASSWORD : undefined);\n if (!password) {\n throw new Error(\n `keystorePath is set in ${configPath} but TOON_CLIENT_KEYSTORE_PASSWORD is not provided`\n );\n }\n source = {\n mnemonic: keys.loadKeystore(picked.keystorePath, password),\n source: 'keystore',\n sourceLabel: picked.keystorePath,\n };\n } else {\n source = picked;\n }\n\n const accountIndex = resolveAccountIndex(options.env, source.source, file);\n const { pubkey } = keys.deriveNostrKeyFromMnemonic(source.mnemonic, accountIndex);\n return { ...source, accountIndex, pubkey };\n}\n"],"mappings":"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","names":["REPOSITORY_ANNOUNCEMENT_KIND","REPOSITORY_ANNOUNCEMENT_KIND"]}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DEFAULT_CONTENT_TYPE,
|
|
3
3
|
contentTypeForPath
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-LMZSAFAB.js";
|
|
5
5
|
import {
|
|
6
6
|
uploadChargeFor
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-7VIVQHRG.js";
|
|
8
8
|
|
|
9
9
|
// src/standalone/connector-publisher.ts
|
|
10
10
|
import { finalizeEvent } from "nostr-tools/pure";
|
|
@@ -129,7 +129,7 @@ var ConnectorPublisher = class {
|
|
|
129
129
|
this.nostrSecretKey
|
|
130
130
|
);
|
|
131
131
|
}
|
|
132
|
-
/** One paid kind:5094 write; returns the tx id and what the
|
|
132
|
+
/** One paid kind:5094 write; returns the tx id and what the voucher spent. */
|
|
133
133
|
async storeWrite(tags, bodyBytes, what) {
|
|
134
134
|
const rates = await this.getFeeRates();
|
|
135
135
|
const estimate = uploadChargeFor(rates, bodyBytes);
|
|
@@ -276,4 +276,4 @@ export {
|
|
|
276
276
|
extractArweaveTxId,
|
|
277
277
|
ConnectorPublisher
|
|
278
278
|
};
|
|
279
|
-
//# sourceMappingURL=chunk-
|
|
279
|
+
//# sourceMappingURL=chunk-ZQGXFFBP.js.map
|