@toon-protocol/rig 2.0.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -13
- package/dist/{chunk-G4W4MH6G.js → chunk-JBB7HBQC.js} +120 -6
- package/dist/chunk-JBB7HBQC.js.map +1 -0
- package/dist/chunk-OIJNRACA.js +588 -0
- package/dist/chunk-OIJNRACA.js.map +1 -0
- package/dist/{chunk-LFGDLD6J.js → chunk-PS5QOT62.js} +464 -4
- package/dist/chunk-PS5QOT62.js.map +1 -0
- package/dist/chunk-SW7ZHMGS.js +358 -0
- package/dist/chunk-SW7ZHMGS.js.map +1 -0
- package/dist/cli/rig.js +2664 -410
- package/dist/cli/rig.js.map +1 -1
- package/dist/index.d.ts +271 -3
- package/dist/index.js +44 -8
- package/dist/{publisher-CClD9OIZ.d.ts → publisher-CrwaNQrK.d.ts} +10 -1
- package/dist/standalone/index.d.ts +410 -3
- package/dist/standalone/index.js +20 -6
- package/dist/standalone-mode-64CKUVU2.js +865 -0
- package/dist/standalone-mode-64CKUVU2.js.map +1 -0
- package/package.json +2 -2
- package/dist/chunk-G4W4MH6G.js.map +0 -1
- package/dist/chunk-HPSOQP7Q.js +0 -109
- package/dist/chunk-HPSOQP7Q.js.map +0 -1
- package/dist/chunk-LFGDLD6J.js.map +0 -1
- package/dist/chunk-XRRU6YBQ.js +0 -402
- package/dist/chunk-XRRU6YBQ.js.map +0 -1
- package/dist/standalone-mode-Q3FFZUXE.js +0 -111
- package/dist/standalone-mode-Q3FFZUXE.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/standalone/channel-map.ts","../src/standalone/nonce-guard.ts"],"sourcesContent":["/**\n * Peer→channel map for the STANDALONE embedded publisher (#262).\n *\n * Why this exists: `@toon-protocol/client`'s `ChannelManager` persists the\n * off-chain nonce/cumulative-claim watermark (its `ChannelStore`,\n * `channels.json`, keyed by channelId) but keeps the peer→channelId mapping\n * ONLY in memory. So every standalone CLI invocation used to open (and fund)\n * a FRESH on-chain channel — the #260 fresh-outsider e2e stranded five\n * deposits across five commands. This store remembers WHICH channel the\n * identity holds with each peer, keyed by\n * `identity pubkey | ILP anchor (peer/apex destination) | chain | tokenNetwork`,\n * so the next invocation resumes it via `ChannelManager.trackChannel` (which\n * rehydrates the nonce watermark from `channels.json`) with zero on-chain\n * writes.\n *\n * It is the standalone twin of the daemon's\n * `packages/client-mcp/src/daemon/apex-channel-store.ts` — same record shape\n * (channelId + the chain context `trackChannel` needs), extended with the\n * identity pubkey (rig identities come from an env/`.env` precedence chain,\n * so one state dir can serve several identities) and the tokenNetwork.\n * `@toon-protocol/rig` must not import `@toon-protocol/client-mcp` (that\n * package depends on this one — circular), hence the twin; keep the\n * semantics in sync.\n *\n * CONCURRENCY: writes happen only from paid commands, which already hold the\n * per-identity advisory lockfile (./nonce-guard.ts `NonceLock`) for their\n * whole lifetime — the same guard that serializes the claim watermark also\n * serializes this file for one identity. `rig channel list` reads only.\n *\n * CORRUPTION: an unreadable/invalid map file is a hard\n * {@link ChannelMapCorruptError} — surfaced BEFORE any on-chain open — never\n * an empty fallback. Falling back to \"no channels\" would silently open (and\n * fund) a duplicate channel, which is exactly the #262 bug.\n *\n * This module is dependency-light on purpose (node:fs only): the free\n * `rig channel list` command reads it without the optional\n * `@toon-protocol/client` peer dependency installed.\n */\n\nimport { mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { dirname, join } from 'node:path';\n\n// ---------------------------------------------------------------------------\n// Paths (TOON_CLIENT_HOME conventions — see nonce-guard.ts module doc)\n// ---------------------------------------------------------------------------\n\n/** Map filename under the shared client state dir. */\nexport const RIG_CHANNEL_MAP_FILENAME = 'rig-channels.json';\n\n/**\n * Resolve the two channel-state files under `TOON_CLIENT_HOME` (default\n * `~/.toon-client`): the rig peer→channel map, and the client's nonce\n * watermark store (`config.json`'s `channelStorePath`, default\n * `<dir>/channels.json` — the same resolution `cli/standalone-mode.ts` feeds\n * the embedded ToonClient).\n */\nexport function resolveChannelPaths(env: NodeJS.ProcessEnv): {\n mapPath: string;\n watermarkPath: string;\n} {\n const dir = env['TOON_CLIENT_HOME'] ?? join(homedir(), '.toon-client');\n let configured: string | undefined;\n try {\n const raw = readFileSync(join(dir, 'config.json'), 'utf8');\n const parsed = JSON.parse(raw) as { channelStorePath?: unknown };\n if (typeof parsed.channelStorePath === 'string') {\n configured = parsed.channelStorePath;\n }\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {\n throw new Error(\n `failed to read client config at ${join(dir, 'config.json')}: ` +\n `${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n return {\n mapPath: join(dir, RIG_CHANNEL_MAP_FILENAME),\n watermarkPath: configured ?? join(dir, 'channels.json'),\n };\n}\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Chain context `ChannelManager.trackChannel` needs to resume a channel\n * (same shape the daemon's apex-channel-store persists).\n */\nexport interface PersistedChannelContext {\n chainType: string;\n chainId: number;\n tokenNetworkAddress: string;\n tokenAddress?: string;\n /** Counterparty settlement address (required for Solana/Mina proofs). */\n recipient?: string;\n}\n\n/** One persisted peer→channel binding. */\nexport interface ChannelMapRecord {\n /** On-chain payment channel id. */\n channelId: string;\n /** Registered peer id the negotiation was keyed by (`peerNegotiations`). */\n peerId: string;\n /** Hex Nostr pubkey of the identity that opened the channel. */\n identity: string;\n /** ILP anchor destination the channel was opened against (peer/apex). */\n destination: string;\n /** Negotiated settlement chain, e.g. `evm:31337`. */\n chain: string;\n /** TokenNetwork contract address ('' when the peer announced none). */\n tokenNetwork: string;\n /** Context for `trackChannel` on resume. */\n context: PersistedChannelContext;\n /** On-chain deposit total (base units, string), when known. */\n depositTotal?: string;\n /** ISO timestamps. */\n openedAt: string;\n lastUsedAt: string;\n}\n\n/** The composite key a record is stored under. */\nexport interface ChannelMapKey {\n identity: string;\n destination: string;\n chain: string;\n tokenNetwork: string;\n}\n\n/**\n * One entry of the client's nonce-watermark store (`channels.json`) —\n * format DUPLICATED from `@toon-protocol/client`'s `JsonFileChannelStore`\n * (`packages/client/src/channel/ChannelStore.ts`); keep in sync.\n */\nexport interface WatermarkEntry {\n nonce: number;\n /** Cumulative claimed amount, base units (string-encoded bigint). */\n cumulativeAmount: string;\n /** Withdraw-flow timers, string-encoded unix SECONDS. */\n closedAt?: string;\n settleableAt?: string;\n settledAt?: string;\n}\n\n/** The peer→channel map file is unreadable or malformed. */\nexport class ChannelMapCorruptError extends Error {\n constructor(\n public readonly path: string,\n detail: string\n ) {\n super(\n `channel state file ${path} is corrupt (${detail}) — refusing to ` +\n 'continue: proceeding would silently open (and fund) a duplicate ' +\n 'on-chain channel. Fix or remove the file; removing it makes rig ' +\n 'forget which channels it holds (existing deposits stay locked ' +\n 'on-chain until settled).'\n );\n this.name = 'ChannelMapCorruptError';\n }\n}\n\n// ---------------------------------------------------------------------------\n// Store\n// ---------------------------------------------------------------------------\n\ninterface MapFile {\n version: 1;\n channels: Record<string, ChannelMapRecord>;\n}\n\nfunction keyOf(key: ChannelMapKey): string {\n return `${key.identity}|${key.destination}|${key.chain}|${key.tokenNetwork}`;\n}\n\n/** The key fields of a full record. */\nexport function recordKey(record: ChannelMapRecord): ChannelMapKey {\n return {\n identity: record.identity,\n destination: record.destination,\n chain: record.chain,\n tokenNetwork: record.tokenNetwork,\n };\n}\n\nfunction isContext(v: unknown): v is PersistedChannelContext {\n if (typeof v !== 'object' || v === null) return false;\n const c = v as Record<string, unknown>;\n return (\n typeof c['chainType'] === 'string' &&\n typeof c['chainId'] === 'number' &&\n typeof c['tokenNetworkAddress'] === 'string'\n );\n}\n\nfunction isRecord(v: unknown): v is ChannelMapRecord {\n if (typeof v !== 'object' || v === null) return false;\n const r = v as Record<string, unknown>;\n return (\n typeof r['channelId'] === 'string' &&\n typeof r['peerId'] === 'string' &&\n typeof r['identity'] === 'string' &&\n typeof r['destination'] === 'string' &&\n typeof r['chain'] === 'string' &&\n typeof r['tokenNetwork'] === 'string' &&\n isContext(r['context'])\n );\n}\n\nexport interface ChannelMapStoreOptions {\n /** The rig peer→channel map file (`rig-channels.json`). */\n mapPath: string;\n /** The client's nonce-watermark store (`channels.json`). */\n watermarkPath: string;\n}\n\n/**\n * File-backed peer→channel map + read/seed access to the client's nonce\n * watermark store. Synchronous I/O (matches the client's `ChannelStore`\n * surface); see the module doc for the locking and corruption contracts.\n */\nexport class ChannelMapStore {\n readonly mapPath: string;\n readonly watermarkPath: string;\n\n constructor(options: ChannelMapStoreOptions) {\n this.mapPath = options.mapPath;\n this.watermarkPath = options.watermarkPath;\n }\n\n /** All recorded channels. @throws {ChannelMapCorruptError} */\n list(): ChannelMapRecord[] {\n return Object.values(this.readMap().channels);\n }\n\n /**\n * Recorded channels for one (identity, destination) pair — the resume\n * candidates for a paid command. @throws {ChannelMapCorruptError}\n */\n listFor(identity: string, destination: string): ChannelMapRecord[] {\n return this.list().filter(\n (r) => r.identity === identity && r.destination === destination\n );\n }\n\n /**\n * Record a (freshly opened) channel. Overwrites any previous record under\n * the same (identity, destination, chain, tokenNetwork) key — the old\n * channel is closed/stale by then; its claim watermark stays in the\n * watermark store.\n */\n record(\n record: Omit<ChannelMapRecord, 'openedAt' | 'lastUsedAt'> &\n Partial<Pick<ChannelMapRecord, 'openedAt' | 'lastUsedAt'>>\n ): void {\n const now = new Date().toISOString();\n const full: ChannelMapRecord = {\n ...record,\n openedAt: record.openedAt ?? now,\n lastUsedAt: record.lastUsedAt ?? now,\n };\n const data = this.readMap();\n data.channels[keyOf(recordKey(full))] = full;\n this.writeMap(data);\n }\n\n /**\n * Bump a record's `lastUsedAt` (and optionally its known on-chain deposit)\n * after resuming it. Unknown keys are a no-op.\n */\n touch(key: ChannelMapKey, update?: { depositTotal?: string }): void {\n const data = this.readMap();\n const existing = data.channels[keyOf(key)];\n if (!existing) return;\n existing.lastUsedAt = new Date().toISOString();\n if (update?.depositTotal !== undefined) {\n existing.depositTotal = update.depositTotal;\n }\n this.writeMap(data);\n }\n\n /**\n * Read one channel's nonce-watermark entry from the client's\n * `channels.json` (undefined when the file or entry is missing).\n * @throws {ChannelMapCorruptError} when the watermark file is unreadable.\n */\n readWatermark(channelId: string): WatermarkEntry | undefined {\n return this.readWatermarkFile()[channelId];\n }\n\n /**\n * Seed a fresh channel's watermark entry (`nonce 0, cumulative 0`) so a\n * later resume can tell \"never claimed against\" apart from \"watermark\n * lost\". Never overwrites an existing entry.\n */\n seedWatermark(channelId: string): void {\n const data = this.readWatermarkFile();\n if (data[channelId]) return;\n data[channelId] = { nonce: 0, cumulativeAmount: '0' };\n mkdirSync(dirname(this.watermarkPath), { recursive: true });\n writeFileSync(\n this.watermarkPath,\n JSON.stringify(data, null, 2),\n 'utf-8'\n );\n }\n\n // ── file I/O ───────────────────────────────────────────────────────────────\n\n private readMap(): MapFile {\n let raw: string;\n try {\n raw = readFileSync(this.mapPath, 'utf8');\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n return { version: 1, channels: {} };\n }\n throw new ChannelMapCorruptError(\n this.mapPath,\n err instanceof Error ? err.message : String(err)\n );\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n throw new ChannelMapCorruptError(\n this.mapPath,\n `invalid JSON: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n if (\n typeof parsed !== 'object' ||\n parsed === null ||\n (parsed as { version?: unknown }).version !== 1 ||\n typeof (parsed as { channels?: unknown }).channels !== 'object' ||\n (parsed as { channels?: unknown }).channels === null\n ) {\n throw new ChannelMapCorruptError(\n this.mapPath,\n 'expected { \"version\": 1, \"channels\": { … } }'\n );\n }\n const channels = (parsed as { channels: Record<string, unknown> })\n .channels;\n for (const [key, value] of Object.entries(channels)) {\n if (!isRecord(value)) {\n throw new ChannelMapCorruptError(\n this.mapPath,\n `entry ${JSON.stringify(key)} is missing required fields`\n );\n }\n }\n return parsed as MapFile;\n }\n\n private writeMap(data: MapFile): void {\n mkdirSync(dirname(this.mapPath), { recursive: true });\n writeFileSync(this.mapPath, JSON.stringify(data, null, 2), {\n mode: 0o600,\n });\n }\n\n private readWatermarkFile(): Record<string, WatermarkEntry> {\n let raw: string;\n try {\n raw = readFileSync(this.watermarkPath, 'utf8');\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return {};\n throw new ChannelMapCorruptError(\n this.watermarkPath,\n err instanceof Error ? err.message : String(err)\n );\n }\n try {\n return JSON.parse(raw) as Record<string, WatermarkEntry>;\n } catch (err) {\n throw new ChannelMapCorruptError(\n this.watermarkPath,\n `invalid JSON: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Status derivation\n// ---------------------------------------------------------------------------\n\n/**\n * Where a channel sits in the withdraw journey, from its watermark timers —\n * mirrors `ChannelManager.getChannelCloseState`. A missing entry reads as\n * `open` (recorded channels are seeded at open time; a lost watermark file\n * surfaces separately as unknown claim state).\n */\nexport function channelStatus(\n entry: WatermarkEntry | undefined,\n nowSec: number = Math.floor(Date.now() / 1000)\n): 'open' | 'closing' | 'settleable' | 'settled' {\n if (!entry || entry.closedAt === undefined) return 'open';\n if (entry.settledAt !== undefined) return 'settled';\n if (\n entry.settleableAt !== undefined &&\n BigInt(nowSec) >= BigInt(entry.settleableAt)\n ) {\n return 'settleable';\n }\n return 'closing';\n}\n","/**\n * Nonce-ownership guard for the STANDALONE embedded Publisher (#228).\n *\n * Why this exists: a payment channel's balance proof is a CUMULATIVE\n * watermark — the ChannelManager auto-increments the nonce and cumulative\n * amount on every `signBalanceProof`. Two writers signing claims on the same\n * channel from separate processes (a running `toon-clientd` daemon plus a\n * standalone embedded client, or two standalone processes) each keep their\n * own cumulative counter, so their claims race: the connector sees\n * non-monotonic watermarks and a re-signed claim can double-charge (the\n * hazard documented in packages/rig-web/tests/e2e/seed/lib/publish.ts).\n *\n * Two independent defenses, both keyed by the Nostr pubkey (one identity =\n * one channel set):\n *\n * 1. Daemon detection — probe the toon-clientd loopback control API\n * (`GET /status`) and REFUSE when it reports the SAME identity. A daemon\n * on a different identity holds different channels and is harmless.\n * Since #279 the CLI's paid WRITE commands never get here with a\n * same-identity daemon up — they delegate to its `/git/*` routes first\n * (`cli/daemon-session.ts`), which achieves the same one-writer goal by\n * handing the watermark to the daemon. This guard remains the backstop\n * for the probe→publish race window and for operations with no daemon\n * route (channel close/settle, explicit open).\n * 2. Advisory lockfile — an exclusive per-pubkey lockfile under the shared\n * `~/.toon-client` state dir so two STANDALONE processes can't race each\n * other (the daemon check only covers the daemon). Stale locks (dead pid)\n * are reclaimed.\n *\n * The daemon port and state-dir conventions are DUPLICATED from\n * `packages/client-mcp/src/daemon/config.ts` (default port 8787 /\n * `TOON_CLIENT_HTTP_PORT`; `~/.toon-client` / `TOON_CLIENT_HOME`).\n * `@toon-protocol/rig` must not depend on `@toon-protocol/client-mcp`\n * (the daemon package depends on this one for the #227 Publisher — the\n * import would be circular), so the constants live here with this note.\n * Keep them in sync.\n */\n\nimport { mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\n\n// ---------------------------------------------------------------------------\n// Shared conventions (duplicated from client-mcp — see module doc)\n// ---------------------------------------------------------------------------\n\n/** Default toon-clientd loopback control API port (client-mcp `httpPort`). */\nexport const DEFAULT_DAEMON_PORT = 8787;\n\n/** Daemon control API port: `TOON_CLIENT_HTTP_PORT` env, else 8787. */\nexport function defaultDaemonPort(): number {\n const env = process.env['TOON_CLIENT_HTTP_PORT'];\n const parsed = env ? Number(env) : NaN;\n return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_DAEMON_PORT;\n}\n\n/**\n * Shared client state dir: `TOON_CLIENT_HOME` env, else `~/.toon-client` —\n * the same dir the daemon keeps its config/channel stores in, so daemon and\n * standalone processes agree on where the advisory locks live.\n */\nexport function defaultLockDir(): string {\n return process.env['TOON_CLIENT_HOME'] ?? join(homedir(), '.toon-client');\n}\n\n// ---------------------------------------------------------------------------\n// 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"],"mappings":"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","names":["mkdirSync","readFileSync","writeFileSync","homedir","join"]}
|