@toon-protocol/rig 4.2.0 → 4.3.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.
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  DEFAULT_CONTENT_TYPE,
3
3
  contentTypeForPath
4
- } from "./chunk-YZRR2UO6.js";
4
+ } from "./chunk-XCLUB2CY.js";
5
5
  import {
6
6
  uploadChargeFor
7
- } from "./chunk-CXREFXB6.js";
7
+ } from "./chunk-U5A7MHBP.js";
8
8
 
9
9
  // src/standalone/connector-publisher.ts
10
10
  import { finalizeEvent } from "nostr-tools/pure";
@@ -276,4 +276,4 @@ export {
276
276
  extractArweaveTxId,
277
277
  ConnectorPublisher
278
278
  };
279
- //# sourceMappingURL=chunk-FT2ZEKPX.js.map
279
+ //# sourceMappingURL=chunk-UE5TAFEU.js.map
@@ -253,176 +253,6 @@ function channelStatus(entry, nowSec = Math.floor(Date.now() / 1e3)) {
253
253
  return "closing";
254
254
  }
255
255
 
256
- // src/standalone/nonce-guard.ts
257
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
258
- import { homedir as homedir2 } from "os";
259
- import { join as join2 } from "path";
260
- var DEFAULT_DAEMON_PORT = 8787;
261
- function defaultDaemonPort() {
262
- const env = process.env["TOON_CLIENT_HTTP_PORT"];
263
- const parsed = env ? Number(env) : NaN;
264
- return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_DAEMON_PORT;
265
- }
266
- function defaultLockDir() {
267
- return process.env["TOON_CLIENT_HOME"] ?? join2(homedir2(), ".toon-client");
268
- }
269
- var RIG_STANDALONE_ENV = "RIG_STANDALONE";
270
- function standaloneForced(env) {
271
- const raw = env[RIG_STANDALONE_ENV];
272
- if (raw === void 0) return false;
273
- const v = raw.trim().toLowerCase();
274
- return v === "1" || v === "true" || v === "yes" || v === "on";
275
- }
276
- var DaemonIdentityConflictError = class extends Error {
277
- constructor(pubkey, daemonUrl) {
278
- super(
279
- `toon-clientd is running with this identity (${pubkey.slice(0, 8)}\u2026) at ${daemonUrl} \u2014 paid rig writes delegate to it automatically, but this operation has no daemon route (or the daemon appeared mid-run): stop the daemon and re-run. Two writers on one identity would race the payment channel's cumulative-claim watermark (double-charge hazard).`
280
- );
281
- this.pubkey = pubkey;
282
- this.daemonUrl = daemonUrl;
283
- this.name = "DaemonIdentityConflictError";
284
- }
285
- pubkey;
286
- daemonUrl;
287
- };
288
- var StandaloneLockError = class extends Error {
289
- constructor(pubkey, lockPath, holderPid) {
290
- super(
291
- `another standalone process (pid ${holderPid}) already holds the payment-channel lock for identity ${pubkey.slice(0, 8)}\u2026 (${lockPath}) \u2014 wait for it to finish or stop it. Two writers on one identity would race the cumulative-claim watermark.`
292
- );
293
- this.pubkey = pubkey;
294
- this.lockPath = lockPath;
295
- this.holderPid = holderPid;
296
- this.name = "StandaloneLockError";
297
- }
298
- pubkey;
299
- lockPath;
300
- holderPid;
301
- };
302
- async function checkDaemonIdentity(pubkey, options = {}) {
303
- const port = options.port ?? defaultDaemonPort();
304
- const fetchImpl = options.fetchImpl ?? fetch;
305
- const url = `http://127.0.0.1:${port}/status`;
306
- let daemonPubkey;
307
- try {
308
- const res = await fetchImpl(url, {
309
- signal: AbortSignal.timeout(options.timeoutMs ?? 1500)
310
- });
311
- if (!res.ok) return;
312
- const body = await res.json();
313
- const candidate = body?.identity?.nostrPubkey;
314
- if (typeof candidate === "string") daemonPubkey = candidate;
315
- } catch {
316
- return;
317
- }
318
- if (daemonPubkey !== void 0 && daemonPubkey === pubkey) {
319
- throw new DaemonIdentityConflictError(pubkey, url);
320
- }
321
- }
322
- function pidAlive(pid) {
323
- try {
324
- process.kill(pid, 0);
325
- return true;
326
- } catch (err) {
327
- return err.code === "EPERM";
328
- }
329
- }
330
- var NonceLock = class _NonceLock {
331
- constructor(pubkey, lockPath) {
332
- this.pubkey = pubkey;
333
- this.lockPath = lockPath;
334
- this.exitHandler = () => {
335
- try {
336
- unlinkSync(this.lockPath);
337
- } catch {
338
- }
339
- };
340
- process.once("exit", this.exitHandler);
341
- }
342
- pubkey;
343
- lockPath;
344
- released = false;
345
- exitHandler;
346
- static async acquire(pubkey, options = {}) {
347
- const dir = options.dir ?? defaultLockDir();
348
- const pid = options.pid ?? process.pid;
349
- const lockPath = join2(dir, `standalone-${pubkey}.lock`);
350
- mkdirSync2(dir, { recursive: true });
351
- const payload = JSON.stringify(
352
- {
353
- pid,
354
- pubkey,
355
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
356
- },
357
- null,
358
- 2
359
- );
360
- for (let attempt = 0; attempt < 2; attempt++) {
361
- try {
362
- writeFileSync2(lockPath, payload, { flag: "wx" });
363
- return new _NonceLock(pubkey, lockPath);
364
- } catch (err) {
365
- if (err.code !== "EEXIST") throw err;
366
- let holderPid;
367
- try {
368
- const parsed = JSON.parse(
369
- readFileSync2(lockPath, "utf8")
370
- );
371
- if (typeof parsed.pid === "number") holderPid = parsed.pid;
372
- } catch {
373
- }
374
- if (holderPid !== void 0 && holderPid !== pid && pidAlive(holderPid)) {
375
- throw new StandaloneLockError(pubkey, lockPath, holderPid);
376
- }
377
- try {
378
- unlinkSync(lockPath);
379
- } catch {
380
- }
381
- }
382
- }
383
- throw new StandaloneLockError(pubkey, lockPath, -1);
384
- }
385
- /** Remove the lockfile and detach the exit hook. Idempotent. */
386
- release() {
387
- if (this.released) return;
388
- this.released = true;
389
- process.removeListener("exit", this.exitHandler);
390
- try {
391
- unlinkSync(this.lockPath);
392
- } catch {
393
- }
394
- }
395
- };
396
-
397
- // src/standalone/nostr-identity.ts
398
- import { HDKey } from "@scure/bip32";
399
- import { mnemonicToSeedSync } from "@scure/bip39";
400
- import { getPublicKey } from "nostr-tools/pure";
401
- var MAX_ACCOUNT_INDEX = 2147483647;
402
- function nostrDerivationPath(accountIndex = 0) {
403
- return `m/44'/1237'/0'/0/${String(accountIndex)}`;
404
- }
405
- function deriveNostrKeyFromMnemonic(mnemonic, accountIndex = 0) {
406
- if (!Number.isInteger(accountIndex) || accountIndex < 0 || accountIndex > MAX_ACCOUNT_INDEX) {
407
- throw new Error(
408
- `account index must be an integer in [0, ${String(MAX_ACCOUNT_INDEX)}]; got ${String(accountIndex)}`
409
- );
410
- }
411
- const seed = mnemonicToSeedSync(mnemonic.trim());
412
- try {
413
- const child = HDKey.fromMasterSeed(seed).derive(
414
- nostrDerivationPath(accountIndex)
415
- );
416
- if (!child.privateKey) {
417
- throw new Error("failed to derive a Nostr private key from the seed");
418
- }
419
- const secretKey = new Uint8Array(child.privateKey);
420
- return { secretKey, pubkey: getPublicKey(secretKey) };
421
- } finally {
422
- seed.fill(0);
423
- }
424
- }
425
-
426
256
  // src/mime.ts
427
257
  var DEFAULT_CONTENT_TYPE = "application/octet-stream";
428
258
  var EXTENSION_MIME = /* @__PURE__ */ new Map([
@@ -495,20 +325,8 @@ export {
495
325
  recordKey,
496
326
  ChannelMapStore,
497
327
  channelStatus,
498
- DEFAULT_DAEMON_PORT,
499
- defaultDaemonPort,
500
- defaultLockDir,
501
- RIG_STANDALONE_ENV,
502
- standaloneForced,
503
- DaemonIdentityConflictError,
504
- StandaloneLockError,
505
- checkDaemonIdentity,
506
- NonceLock,
507
- MAX_ACCOUNT_INDEX,
508
- nostrDerivationPath,
509
- deriveNostrKeyFromMnemonic,
510
328
  DEFAULT_CONTENT_TYPE,
511
329
  contentTypeForPath,
512
330
  resolveConflictingPath
513
331
  };
514
- //# sourceMappingURL=chunk-YZRR2UO6.js.map
332
+ //# sourceMappingURL=chunk-XCLUB2CY.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: `@toon-protocol/client`'s `ChannelManager` persists the\n * off-chain nonce/cumulative-claim watermark (its `ChannelStore`,\n * `channels.json`, keyed by channelId) but keeps the peer→channelId mapping\n * ONLY in memory. So every standalone CLI invocation used to open (and fund)\n * a FRESH on-chain channel — the #260 fresh-outsider e2e stranded five\n * deposits across five commands. This store remembers WHICH channel the\n * identity holds with each peer, keyed by\n * `identity pubkey | ILP anchor (peer/apex destination) | chain | tokenNetwork`,\n * so the next invocation resumes it via `ChannelManager.trackChannel` (which\n * rehydrates the nonce watermark from `channels.json`) with zero on-chain\n * writes.\n *\n * It is the standalone twin of the daemon's\n * `packages/client-mcp/src/daemon/apex-channel-store.ts` — same record shape\n * (channelId + the chain context `trackChannel` needs), extended with the\n * identity pubkey (rig identities come from an env/`.env` precedence chain,\n * so one state dir can serve several identities) and the tokenNetwork.\n * `@toon-protocol/rig` must not import `@toon-protocol/client-mcp` (that\n * package depends on this one — circular), hence the twin; keep the\n * semantics in sync.\n *\n * CONCURRENCY: writes happen only from paid commands, which already hold the\n * per-identity advisory lockfile (./nonce-guard.ts `NonceLock`) for their\n * whole lifetime — the same guard that serializes the claim watermark also\n * serializes this file for one identity. `rig channel list` reads only.\n *\n * COUNTERPARTY: the key names a ROUTE, not a node — an ILP name can change\n * hands (the devnet apex `g.toon` was retired and another node took over\n * `g.toon.relay`). A record therefore also carries the counterparty\n * settlement address it was opened against (`context.recipient`), which is\n * re-checked against the destination's announced address before every resume\n * ({@link counterpartyMatch}); a rotated counterparty\n * {@link ChannelMapStore.supersede}s the record instead of signing claims a\n * connector with no record of that channel refuses (`F01`).\n *\n * CORRUPTION: an unreadable/invalid map file is a hard\n * {@link ChannelMapCorruptError} — surfaced BEFORE any on-chain open — never\n * an empty fallback. Falling back to \"no channels\" would silently open (and\n * fund) a duplicate channel, which is exactly the #262 bug.\n *\n * This module is dependency-light on purpose (node:fs only): the free\n * `rig channel list` command reads it without the optional\n * `@toon-protocol/client` peer dependency installed.\n */\n\nimport { mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { dirname, join } from 'node:path';\n\n// ---------------------------------------------------------------------------\n// Paths (TOON_CLIENT_HOME conventions — see nonce-guard.ts module doc)\n// ---------------------------------------------------------------------------\n\n/** Map filename under the shared client state dir. */\nexport const RIG_CHANNEL_MAP_FILENAME = 'rig-channels.json';\n\n/**\n * Resolve the two channel-state files under `TOON_CLIENT_HOME` (default\n * `~/.toon-client`): the rig peer→channel map, and the client's nonce\n * watermark store (`config.json`'s `channelStorePath`, default\n * `<dir>/channels.json` — the same resolution `cli/standalone-mode.ts` feeds\n * the embedded ToonClient).\n */\nexport function resolveChannelPaths(env: NodeJS.ProcessEnv): {\n mapPath: string;\n watermarkPath: string;\n} {\n const dir = env['TOON_CLIENT_HOME'] ?? join(homedir(), '.toon-client');\n let configured: string | undefined;\n try {\n const raw = readFileSync(join(dir, 'config.json'), 'utf8');\n const parsed = JSON.parse(raw) as { channelStorePath?: unknown };\n if (typeof parsed.channelStorePath === 'string') {\n configured = parsed.channelStorePath;\n }\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {\n throw new Error(\n `failed to read client config at ${join(dir, 'config.json')}: ` +\n `${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n return {\n mapPath: join(dir, RIG_CHANNEL_MAP_FILENAME),\n watermarkPath: configured ?? join(dir, 'channels.json'),\n };\n}\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Chain context `ChannelManager.trackChannel` needs to resume a channel\n * (same shape the daemon's apex-channel-store persists).\n */\nexport interface PersistedChannelContext {\n chainType: string;\n chainId: number;\n tokenNetworkAddress: string;\n tokenAddress?: string;\n /** Counterparty settlement address (required for Solana/Mina proofs). */\n recipient?: string;\n}\n\n/** One persisted peer→channel binding. */\nexport interface ChannelMapRecord {\n /** On-chain payment channel id. */\n channelId: string;\n /** Registered peer id the negotiation was keyed by (`peerNegotiations`). */\n peerId: string;\n /** Hex Nostr pubkey of the identity that opened the channel. */\n identity: string;\n /** ILP anchor destination the channel was opened against (peer/apex). */\n destination: string;\n /** Negotiated settlement chain, e.g. `evm:31337`. */\n chain: string;\n /** TokenNetwork contract address ('' when the peer announced none). */\n tokenNetwork: string;\n /** Context for `trackChannel` on resume. */\n context: PersistedChannelContext;\n /** On-chain deposit total (base units, string), when known. */\n depositTotal?: string;\n /** ISO timestamps. */\n openedAt: string;\n lastUsedAt: string;\n /**\n * ISO timestamp this record was RETIRED from the resume path because the\n * destination now announces a different settlement address than the channel\n * was opened against (see {@link counterpartyMatch} /\n * {@link ChannelMapStore.supersede}). A superseded record is never resumed\n * again, but stays in {@link ChannelMapStore.list} so `rig channel\n * list/close/settle` can still reclaim whatever it holds on-chain.\n */\n supersededAt?: string;\n}\n\n/** The composite key a record is stored under. */\nexport interface ChannelMapKey {\n identity: string;\n destination: string;\n chain: string;\n tokenNetwork: string;\n}\n\n/**\n * One entry of the client's nonce-watermark store (`channels.json`) —\n * format DUPLICATED from `@toon-protocol/client`'s `JsonFileChannelStore`\n * (`packages/client/src/channel/ChannelStore.ts`); keep in sync.\n */\nexport interface WatermarkEntry {\n nonce: number;\n /** Cumulative claimed amount, base units (string-encoded bigint). */\n cumulativeAmount: string;\n /** Withdraw-flow timers, string-encoded unix SECONDS. */\n closedAt?: string;\n settleableAt?: string;\n settledAt?: string;\n}\n\n/** The peer→channel map file is unreadable or malformed. */\nexport class ChannelMapCorruptError extends Error {\n constructor(\n public readonly path: string,\n detail: string\n ) {\n super(\n `channel state file ${path} is corrupt (${detail}) — refusing to ` +\n 'continue: proceeding would silently open (and fund) a duplicate ' +\n 'on-chain channel. Fix or remove the file; removing it makes rig ' +\n 'forget which channels it holds (existing deposits stay locked ' +\n 'on-chain until settled).'\n );\n this.name = 'ChannelMapCorruptError';\n }\n}\n\n// ---------------------------------------------------------------------------\n// Store\n// ---------------------------------------------------------------------------\n\ninterface MapFile {\n version: 1;\n channels: Record<string, ChannelMapRecord>;\n}\n\nfunction keyOf(key: ChannelMapKey): string {\n return `${key.identity}|${key.destination}|${key.chain}|${key.tokenNetwork}`;\n}\n\n/**\n * Archive key a {@link ChannelMapStore.supersede}d record moves to: the live\n * key plus the channel id, so the retired record survives the fresh channel\n * being recorded under the live key (deposits it still holds stay reachable\n * from `rig channel list/close/settle`) and several supersessions of the same\n * route never collide.\n */\nfunction supersededKeyOf(record: ChannelMapRecord): string {\n return `${keyOf(recordKey(record))}|superseded:${record.channelId}`;\n}\n\n/**\n * Compare settlement addresses. EVM addresses travel in mixed checksum case\n * (an announce may say `0xF29f…` where the record says `0xf29f…`) and are\n * compared case-insensitively; anything else (base58 Solana, base58check\n * Mina) is case-SIGNIFICANT and compared verbatim.\n */\nexport function sameSettlementAddress(a: string, b: string): boolean {\n const normalize = (v: string): string =>\n /^0x[0-9a-fA-F]+$/.test(v) ? v.toLowerCase() : v;\n return normalize(a) === normalize(b);\n}\n\n/**\n * Does a recorded channel still belong to the counterparty the destination\n * announces TODAY?\n *\n * The map key is `identity|destination|chain|tokenNetwork` — it carries no\n * counterparty. When the node terminating an ILP name is REPLACED (the devnet\n * apex `g.toon` was retired and another node took over `g.toon.relay`), all\n * four key fields still match, so rig resumed a channel opened against the\n * old node and signed claims against it. The new connector holds no record of\n * that channel and refuses every packet: `F01 - claim rejected: names a\n * channel this connector has no record of`. Every paid write failed until the\n * cache entry was deleted by hand.\n *\n * - `'match'` — same counterparty, safe to resume.\n * - `'mismatch'` — the counterparty rotated: the record must be superseded and\n * the channel re-resolved.\n * - `'unrecorded'` — nothing to compare (a record written before this field\n * was validated, or a peer that announced no settlement address). Resume is\n * allowed and the caller should enrich the record with what the announce\n * says, so the NEXT run is verifiable.\n */\nexport function counterpartyMatch(\n record: Pick<ChannelMapRecord, 'context'>,\n announced: string | undefined\n): 'match' | 'mismatch' | 'unrecorded' {\n const recorded = record.context.recipient;\n if (!recorded || !announced) return 'unrecorded';\n return sameSettlementAddress(recorded, announced) ? 'match' : 'mismatch';\n}\n\n/** The key fields of a full record. */\nexport function recordKey(record: ChannelMapRecord): ChannelMapKey {\n return {\n identity: record.identity,\n destination: record.destination,\n chain: record.chain,\n tokenNetwork: record.tokenNetwork,\n };\n}\n\nfunction isContext(v: unknown): v is PersistedChannelContext {\n if (typeof v !== 'object' || v === null) return false;\n const c = v as Record<string, unknown>;\n return (\n typeof c['chainType'] === 'string' &&\n typeof c['chainId'] === 'number' &&\n typeof c['tokenNetworkAddress'] === 'string'\n );\n}\n\nfunction isRecord(v: unknown): v is ChannelMapRecord {\n if (typeof v !== 'object' || v === null) return false;\n const r = v as Record<string, unknown>;\n return (\n typeof r['channelId'] === 'string' &&\n typeof r['peerId'] === 'string' &&\n typeof r['identity'] === 'string' &&\n typeof r['destination'] === 'string' &&\n typeof r['chain'] === 'string' &&\n typeof r['tokenNetwork'] === 'string' &&\n isContext(r['context'])\n );\n}\n\nexport interface ChannelMapStoreOptions {\n /** The rig peer→channel map file (`rig-channels.json`). */\n mapPath: string;\n /** The client's nonce-watermark store (`channels.json`). */\n watermarkPath: string;\n}\n\n/**\n * File-backed peer→channel map + read/seed access to the client's nonce\n * watermark store. Synchronous I/O (matches the client's `ChannelStore`\n * surface); see the module doc for the locking and corruption contracts.\n */\nexport class ChannelMapStore {\n readonly mapPath: string;\n readonly watermarkPath: string;\n\n constructor(options: ChannelMapStoreOptions) {\n this.mapPath = options.mapPath;\n this.watermarkPath = options.watermarkPath;\n }\n\n /** All recorded channels. @throws {ChannelMapCorruptError} */\n list(): ChannelMapRecord[] {\n return Object.values(this.readMap().channels);\n }\n\n /**\n * Recorded channels for one (identity, destination) pair — the resume\n * candidates for a paid command. Superseded records are EXCLUDED: they are\n * retired from the resume path for good, and only `list()` (what `rig\n * channel` enumerates) still shows them. @throws {ChannelMapCorruptError}\n */\n listFor(identity: string, destination: string): ChannelMapRecord[] {\n return this.list().filter(\n (r) =>\n r.identity === identity &&\n r.destination === destination &&\n r.supersededAt === undefined\n );\n }\n\n /**\n * Record a (freshly opened) channel. Overwrites any previous record under\n * the same (identity, destination, chain, tokenNetwork) key — the old\n * channel is closed/stale by then; its claim watermark stays in the\n * watermark store.\n */\n record(\n record: Omit<ChannelMapRecord, 'openedAt' | 'lastUsedAt'> &\n Partial<Pick<ChannelMapRecord, 'openedAt' | 'lastUsedAt'>>\n ): void {\n const now = new Date().toISOString();\n const full: ChannelMapRecord = {\n ...record,\n openedAt: record.openedAt ?? now,\n lastUsedAt: record.lastUsedAt ?? now,\n };\n const data = this.readMap();\n data.channels[keyOf(recordKey(full))] = full;\n this.writeMap(data);\n }\n\n /**\n * Bump a record's `lastUsedAt` (and optionally its known on-chain deposit,\n * or the counterparty a pre-validation record was written without) after\n * resuming it. Unknown keys are a no-op.\n */\n touch(\n key: ChannelMapKey,\n update?: { depositTotal?: string; recipient?: string }\n ): void {\n const data = this.readMap();\n const existing = data.channels[keyOf(key)];\n if (!existing) return;\n existing.lastUsedAt = new Date().toISOString();\n if (update?.depositTotal !== undefined) {\n existing.depositTotal = update.depositTotal;\n }\n if (update?.recipient !== undefined) {\n existing.context.recipient = update.recipient;\n }\n this.writeMap(data);\n }\n\n /**\n * Retire a record from the resume path — the counterparty that terminates\n * its destination has been REPLACED, so the channel it names is dead to the\n * new node (`F01 - claim rejected`) even though every key field still\n * matches (see {@link counterpartyMatch}).\n *\n * The record is MOVED to an archive key rather than deleted: it may still\n * hold an on-chain deposit, and `rig channel list/close/settle` find\n * channels by scanning `list()`, so deleting it would strand those funds\n * behind hand-editing the JSON. Moving it also frees the live key for the\n * re-resolved channel. Idempotent, and a no-op for an unknown record.\n */\n supersede(record: ChannelMapRecord): void {\n const data = this.readMap();\n const liveKey = keyOf(recordKey(record));\n const existing = data.channels[liveKey];\n const isLive =\n existing !== undefined && existing.channelId === record.channelId;\n const retired: ChannelMapRecord = {\n ...(isLive && existing ? existing : record),\n supersededAt: record.supersededAt ?? new Date().toISOString(),\n };\n const channels: Record<string, ChannelMapRecord> = {};\n for (const [key, value] of Object.entries(data.channels)) {\n // The live key is FREED (not just re-pointed): the re-resolved channel\n // is recorded under it moments later.\n if (isLive && key === liveKey) continue;\n channels[key] = value;\n }\n channels[supersededKeyOf(retired)] = retired;\n this.writeMap({ ...data, channels });\n }\n\n /**\n * Read one channel's nonce-watermark entry from the client's\n * `channels.json` (undefined when the file or entry is missing).\n * @throws {ChannelMapCorruptError} when the watermark file is unreadable.\n */\n readWatermark(channelId: string): WatermarkEntry | undefined {\n return this.readWatermarkFile()[channelId];\n }\n\n /**\n * Seed a fresh channel's watermark entry (`nonce 0, cumulative 0`) so a\n * later resume can tell \"never claimed against\" apart from \"watermark\n * lost\". Never overwrites an existing entry.\n */\n seedWatermark(channelId: string): void {\n const data = this.readWatermarkFile();\n if (data[channelId]) return;\n data[channelId] = { nonce: 0, cumulativeAmount: '0' };\n mkdirSync(dirname(this.watermarkPath), { recursive: true });\n writeFileSync(\n this.watermarkPath,\n JSON.stringify(data, null, 2),\n 'utf-8'\n );\n }\n\n // ── file I/O ───────────────────────────────────────────────────────────────\n\n private readMap(): MapFile {\n let raw: string;\n try {\n raw = readFileSync(this.mapPath, 'utf8');\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n return { version: 1, channels: {} };\n }\n throw new ChannelMapCorruptError(\n this.mapPath,\n err instanceof Error ? err.message : String(err)\n );\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n throw new ChannelMapCorruptError(\n this.mapPath,\n `invalid JSON: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n if (\n typeof parsed !== 'object' ||\n parsed === null ||\n (parsed as { version?: unknown }).version !== 1 ||\n typeof (parsed as { channels?: unknown }).channels !== 'object' ||\n (parsed as { channels?: unknown }).channels === null\n ) {\n throw new ChannelMapCorruptError(\n this.mapPath,\n 'expected { \"version\": 1, \"channels\": { … } }'\n );\n }\n const channels = (parsed as { channels: Record<string, unknown> })\n .channels;\n for (const [key, value] of Object.entries(channels)) {\n if (!isRecord(value)) {\n throw new ChannelMapCorruptError(\n this.mapPath,\n `entry ${JSON.stringify(key)} is missing required fields`\n );\n }\n }\n return parsed as MapFile;\n }\n\n private writeMap(data: MapFile): void {\n mkdirSync(dirname(this.mapPath), { recursive: true });\n writeFileSync(this.mapPath, JSON.stringify(data, null, 2), {\n mode: 0o600,\n });\n }\n\n private readWatermarkFile(): Record<string, WatermarkEntry> {\n let raw: string;\n try {\n raw = readFileSync(this.watermarkPath, 'utf8');\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return {};\n throw new ChannelMapCorruptError(\n this.watermarkPath,\n err instanceof Error ? err.message : String(err)\n );\n }\n try {\n return JSON.parse(raw) as Record<string, WatermarkEntry>;\n } catch (err) {\n throw new ChannelMapCorruptError(\n this.watermarkPath,\n `invalid JSON: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Status derivation\n// ---------------------------------------------------------------------------\n\n/**\n * Where a channel sits in the withdraw journey, from its watermark timers —\n * mirrors `ChannelManager.getChannelCloseState`. A missing entry reads as\n * `open` (recorded channels are seeded at open time; a lost watermark file\n * surfaces separately as unknown claim state).\n */\nexport function channelStatus(\n entry: WatermarkEntry | undefined,\n nowSec: number = Math.floor(Date.now() / 1000)\n): 'open' | 'closing' | 'settleable' | 'settled' {\n if (!entry || entry.closedAt === undefined) return 'open';\n if (entry.settledAt !== undefined) return 'settled';\n if (\n entry.settleableAt !== undefined &&\n BigInt(nowSec) >= BigInt(entry.settleableAt)\n ) {\n return 'settleable';\n }\n return 'closing';\n}\n","/**\n * File-extension → MIME type derivation for permaweb site deploys (#368).\n *\n * Every `rig push` already stores a repo's raw blob bytes on Arweave; tagging\n * each blob upload with a `Content-Type` derived from its path turns a pushed\n * repo into a servable static site (a gateway returns `text/html` for\n * `index.html` instead of `application/octet-stream`). The store forwards the\n * kind:5094 `output` tag to the Turbo upload's `Content-Type` — verified\n * against `@toon-protocol/core`'s `buildBlobStorageRequest` (sets\n * `[\"output\", contentType]`) and its inverse parser (reads the `output` tag\n * back into `contentType`, defaulting to `application/octet-stream`).\n *\n * The map is deliberately small — the common static-site asset extensions —\n * and any unknown or extensionless path falls back to the octet-stream\n * default (the same default the store applies for an absent `output`), so\n * this never guesses wildly.\n */\n\n/** The universal fallback: the store's own default for an absent `output`. */\nexport const DEFAULT_CONTENT_TYPE = 'application/octet-stream';\n\n/**\n * Lowercased extension (no dot) → MIME type. Curated for static sites; extend\n * as real deploys need more. Anything absent falls back to octet-stream.\n */\nconst EXTENSION_MIME: ReadonlyMap<string, string> = new Map([\n // Markup / documents\n ['html', 'text/html'],\n ['htm', 'text/html'],\n ['xml', 'application/xml'],\n ['txt', 'text/plain'],\n ['md', 'text/markdown'],\n ['csv', 'text/csv'],\n ['pdf', 'application/pdf'],\n // Styles / scripts\n ['css', 'text/css'],\n ['js', 'text/javascript'],\n ['mjs', 'text/javascript'],\n ['cjs', 'text/javascript'],\n ['map', 'application/json'],\n ['json', 'application/json'],\n ['wasm', 'application/wasm'],\n // Fonts\n ['woff', 'font/woff'],\n ['woff2', 'font/woff2'],\n ['ttf', 'font/ttf'],\n ['otf', 'font/otf'],\n ['eot', 'application/vnd.ms-fontobject'],\n // Images\n ['png', 'image/png'],\n ['jpg', 'image/jpeg'],\n ['jpeg', 'image/jpeg'],\n ['gif', 'image/gif'],\n ['svg', 'image/svg+xml'],\n ['webp', 'image/webp'],\n ['avif', 'image/avif'],\n ['ico', 'image/x-icon'],\n ['bmp', 'image/bmp'],\n // Media\n ['mp3', 'audio/mpeg'],\n ['mp4', 'video/mp4'],\n ['webm', 'video/webm'],\n ['ogg', 'audio/ogg'],\n ['wav', 'audio/wav'],\n]);\n\n/** The lowercased extension of a path (no leading dot), or `undefined`. */\nfunction extensionOf(path: string): string | undefined {\n const slash = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\\\'));\n const base = slash >= 0 ? path.slice(slash + 1) : path;\n const dot = base.lastIndexOf('.');\n // A leading dot (dotfile like `.gitignore`) or no dot → no usable extension.\n if (dot <= 0) return undefined;\n return base.slice(dot + 1).toLowerCase();\n}\n\n/**\n * Derive a `Content-Type` from a file path's extension. Unknown/extensionless\n * paths (and `undefined`) fall back to {@link DEFAULT_CONTENT_TYPE}.\n */\nexport function contentTypeForPath(path: string | undefined): string {\n if (!path) return DEFAULT_CONTENT_TYPE;\n const ext = extensionOf(path);\n if (ext === undefined) return DEFAULT_CONTENT_TYPE;\n return EXTENSION_MIME.get(ext) ?? DEFAULT_CONTENT_TYPE;\n}\n\n/**\n * Pick a single deterministic upload path for a blob reachable by MULTIPLE\n * paths (the same content committed under different names). The store keys a\n * blob by its content, so it carries ONE `Content-Type`:\n *\n * - all paths agree on a content type → the lexicographically-first path\n * (deterministic across runs);\n * - the paths DISAGREE (e.g. `a.js` and `a.txt` share bytes) → `undefined`,\n * so the caller uploads it as {@link DEFAULT_CONTENT_TYPE} rather than\n * silently favoring one extension over another.\n *\n * An empty list yields `undefined` (octet-stream).\n */\nexport function resolveConflictingPath(paths: string[]): string | undefined {\n if (paths.length === 0) return undefined;\n const sorted = [...paths].sort();\n const types = new Set(sorted.map((p) => contentTypeForPath(p)));\n if (types.size === 1) return sorted[0];\n return undefined;\n}\n"],"mappings":"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","names":[]}