@toon-protocol/rig 1.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,40 +1,120 @@
1
1
  # @toon-protocol/rig
2
2
 
3
- Git-to-TOON write path core — build git objects and NIP-34 events for the Rig control plane. Ships the **`rig`** CLI.
3
+ Git-to-TOON write path core — build git objects and NIP-34 events for the Rig control plane. Ships the **`rig`** CLI: a 1:1 git experience with a TOON remote. rig owns a handful of TOON verbs (table below); **every other command is passed through to system `git` verbatim** — `rig status` runs `git status`, `rig add -p`, `rig commit`, `rig rebase -i`, … all behave exactly like git (same output, same prompts, same exit code).
4
+
5
+ | command | owner | what it does |
6
+ | --- | --- | --- |
7
+ | `rig init` | rig (free) | one-shot repo setup: identity + `toon.*` git config |
8
+ | `rig remote add/remove/list` | rig (free) | relays as REAL git remotes (`origin` = default publish target) |
9
+ | `rig push [remote] [refspecs...]` | rig (paid) | the TOON push: Arweave upload + NIP-34 refs publish. Shadows `git push` — plain-git pushes stay available by running `git push` directly |
10
+ | `rig issue create` | rig (paid) | file an issue (kind:1621) |
11
+ | `rig comment <root-event-id>` | rig (paid) | comment (kind:1622) on an issue/patch |
12
+ | `rig pr create` | rig (paid) | publish a patch (kind:1617) from real `git format-patch` |
13
+ | `rig pr status <event-id> <state>` | rig (paid) | set issue/patch status (kind:1630–1633). **Was `rig status` before v2** |
14
+ | `rig help` / `rig --version` | rig | usage / version |
15
+ | everything else | **git** | executed as `git <args...>` with rig's stdio and git's exit code |
4
16
 
5
17
  ## `rig` quickstart
6
18
 
7
19
  ```sh
8
- # inside any git repository
20
+ npm install -g @toon-protocol/rig
21
+
22
+ # 1. identity — a BIP-39 seed phrase, either in your environment…
23
+ export RIG_MNEMONIC="abandon abandon … about"
24
+ # …or in a project-local .env (gitignore it!):
25
+ echo 'RIG_MNEMONIC="abandon abandon … about"' >> .env
26
+
27
+ # 2. one-shot repo setup (free): writes toon.repoid + toon.owner to the
28
+ # repo's local git config and reports which identity source is active
29
+ rig init # default repo id = directory name
30
+ rig init --repo-id my-repo # or pick one
31
+
32
+ # 3. add your relay as an origin (free) — a REAL git remote, so
33
+ # `git remote -v` shows it and git tooling round-trips it
34
+ rig remote add origin wss://relay.example
35
+ rig remote list # names + URLs; --json for machines
36
+ rig remote remove origin
37
+
38
+ # 4. work exactly like git — unowned commands pass through to system git:
39
+ rig status # IS `git status`
40
+ rig add -p && rig commit -m "fix"
41
+ rig log --oneline # pagers, colors, prompts all behave like git
42
+ rig rebase -i HEAD~3 # interactive works (stdio is inherited)
43
+
44
+ # 5. push (paid) — defaults to the "origin" remote, exactly like git
9
45
  rig push # plan + price the current branch, confirm, push
10
46
  rig push main v1.0.0 --yes # specific refs, skip the confirm prompt
47
+ rig push staging main # push via another configured remote
11
48
  rig push --all --tags --json # machine-readable plan/receipts (agents)
12
49
 
13
- # after the first push, the repo address (30617:<owner>:<repoId>) lives in
14
- # git config — issues, comments, patches, and statuses work with no flags:
50
+ # issues, comments, patches, and statuses use the same rig init config:
15
51
  rig issue create --title "Fix the flux" --body "It broke." # kind:1621
16
52
  echo "longer body" | rig issue create --title t --yes # body via stdin
17
53
  rig comment <root-event-id> --body "Nice catch." # kind:1622
18
54
  rig pr create --title "Add feature" --range main..feature # kind:1617 with
19
55
  # REAL format-patch text
20
- rig status <event-id> applied # kind:1631
56
+ rig pr status <event-id> applied # kind:1631
57
+ # (bare `rig status` is git's)
21
58
  ```
22
59
 
23
- `rig push` uploads the object delta to Arweave (paid, content-addressed — a re-push never re-pays for known objects) and publishes the NIP-34 refs event (kind:30618; plus the kind:30617 announcement on first push). It renders the fee table (refs with classification, objects, bytes, itemized + total fee) and asks for confirmation before spending — writes are permanent and non-refundable. `--yes` skips the prompt (and is required when stdin is not a TTY); `--json` without `--yes` is a pure estimate (nothing executed). `--force` allows non-fast-forward updates; `--relay <url>` (repeatable) and `--repo-id <id>` override the defaults.
60
+ ### Git passthrough
61
+
62
+ Any subcommand rig does not own is executed as `git <args...>` verbatim: the exact argv tail is handed to the system git with `stdio: 'inherit'` (interactive commands, pagers, colors, and prompts work), and git's exit code is rig's exit code (a child killed by a signal maps to the shell convention 128+N). rig-owned verbs always take precedence — in particular `rig push` is the TOON transport and shadows `git push`; plain-git pushes remain available by calling `git push` directly. If no system git is installed, passthrough fails with a clear error (exit 127).
63
+
64
+ ### Identity
65
+
66
+ The CLI is **standalone-only**: it embeds its own payment client built from
67
+ your seed phrase (`@toon-protocol/client` is a regular dependency, installed
68
+ automatically with the package). The mnemonic is resolved along one
69
+ precedence chain — highest first:
70
+
71
+ 1. `RIG_MNEMONIC` environment variable
72
+ 2. `TOON_CLIENT_MNEMONIC` environment variable — deprecated alias, warns on
73
+ stderr; rename it to `RIG_MNEMONIC`
74
+ 3. project-local `.env` — found by walking up from the working directory
75
+ (through the repo root); ONLY the `RIG_MNEMONIC` line is parsed out of it
76
+ (rig never loads arbitrary env from the file, and never requires it).
77
+ **Gitignore it** — the phrase must never be committed.
78
+ 4. the shared `~/.toon-client` state dir (`TOON_CLIENT_HOME` override):
79
+ encrypted keystore (`keystorePath` + `TOON_CLIENT_KEYSTORE_PASSWORD`),
80
+ then the `mnemonic` config field
81
+
82
+ Every paid command reports which source is active and the derived pubkey
83
+ (`Identity: <pubkey> (from …)`, and an `identity` object in `--json`
84
+ output) — the phrase itself is never printed and never written to git config
85
+ or any repo file.
86
+
87
+ A running `toon-clientd` daemon on the **same identity** is still refused
88
+ (the nonce guard): two writers would race the payment channel's
89
+ cumulative-claim watermark. Stop the daemon or publish through its
90
+ `toon_git_*` MCP tools instead — the daemon's `/git/*` routes are the MCP
91
+ host path and are unaffected by the CLI.
92
+
93
+ ### Pushing
94
+
95
+ `rig push [remote] [refspecs...]` uploads the object delta to Arweave (paid, content-addressed — a re-push never re-pays for known objects) and publishes the NIP-34 refs event (kind:30618; plus the kind:30617 announcement on first push). It renders the fee table (refs with classification, objects, bytes, itemized + total fee) and asks for confirmation before spending — writes are permanent and non-refundable. `--yes` skips the prompt (and is required when stdin is not a TTY); `--json` without `--yes` is a pure estimate (nothing executed). `--force` allows non-fast-forward updates; `--repo-id <id>` overrides the configured repo id.
96
+
97
+ Repo addressing (`30617:<owner>:<repoId>`) comes from the `toon.repoid`/`toon.owner` git config keys `rig init` writes — an unconfigured repo is a clear "run `rig init`" error, and pushing never mutates git config. Objects over 95KB are a hard error in v1 (large-object support: toon-client#235).
98
+
99
+ ### Relays are origins
24
100
 
25
- Two ways to pay, picked automatically (`--daemon` / `--standalone` force one):
101
+ Relays are configured as **real git remotes** (`rig remote add` is `git remote add` underneath — `git remote -v` shows them, and remotes added with plain git work too, as long as the URL is `ws://`/`wss://`/`http://`/`https://`):
26
102
 
27
- - **daemon** a running `toon-clientd` (from `@toon-protocol/client-mcp`) on loopback; its identity is the repo owner. Selected when `GET /status` answers with an identity.
28
- - **standalone** an embedded client built from your own `TOON_CLIENT_MNEMONIC` (or `~/.toon-client/config.json`), guarded against racing a daemon on the same identity (cumulative-claim watermark protection).
103
+ - `rig remote add origin <relay-url>` / `rig remote remove <name>` / `rig remote list` (`--json` supported). Junk URLs are rejected at add time; an existing name is refused with a `git remote set-url` hint.
104
+ - `rig push` publishes via `origin`; `rig push <remote> [refspecs...]` via a named remote. Git-like resolution: when the first positional matches a configured remote name it is the remote, otherwise it is a refspec and the remote defaults to `origin`. No usable remote → a clear ``no origin configured — run `rig remote add origin <relay-url>` `` error.
105
+ - The event commands take `--remote <name>` (default `origin`).
106
+ - `--relay <url>` stays as an **ad-hoc override** on every paid command: it bypasses the configured remotes entirely (for push, every positional is then a refspec).
107
+ - One relay URL per remote: a remote with multiple URLs (`git remote set-url --add`) is refused **before anything is uploaded, published, or paid** — rig publishes to exactly one relay per paid command.
108
+ - Migration from v0.1: a configured `git config toon.relay` still works as a fallback when no relay `origin` exists (paid commands print a one-line deprecation nudge), and `rig init` migrates it to a real `origin` remote automatically. The `toon.relay` key is removed in v0.3.
29
109
 
30
- After the first successful push, `rig` persists `toon.repoid`, `toon.owner`, and `toon.relay` into the repo's git config the `a`-tag addressing (`30617:<owner>:<repoId>`) the single-event subcommands read (`--repo-id`/`--owner` override it; use `--owner` for repos you don't own). Objects over 95KB are a hard error in v1 (large-object support: toon-client#235).
110
+ The single-event subcommands follow the same paid-write discipline as push the per-event fee is quoted and confirmed before publishing; `--yes` skips, `--json` without `--yes` is a free estimate:
31
111
 
32
- The single-event subcommands follow the same paid-write discipline as push — the per-event fee is quoted (daemon `/status` `feePerEvent`, or the standalone fee rates) and confirmed before publishing; `--yes` skips, `--json` without `--yes` is a free estimate:
112
+ - `rig issue create --title <t> [--body <b> | --body-file <f> | stdin] [--label <l>]…` kind:1621.
113
+ - `rig comment <root-event-id> --body <b> [--parent-author <pubkey>] [--marker root|reply]` — kind:1622.
114
+ - `rig pr create --title <t> (--range <A..B> | --patch-file <f>) [--branch <name>]` — kind:1617; `--range` runs real `git format-patch --stdout` locally and derives the `commit`/`parent-commit` tags. A multi-commit range publishes ONE event carrying the whole series (cover-letter threading is out of scope in v1).
115
+ - `rig pr status <target-event-id> <open|applied|closed|draft>` — kind:1630–1633, with the repo `a` tag attached. (This was top-level `rig status` before v2; bare `rig status` now passes through to `git status`.)
33
116
 
34
- - `rig issue create --title <t> [--body <b> | --body-file <f> | stdin] [--label <l>]…` — kind:1621 via `POST /git/issue` (or standalone).
35
- - `rig comment <root-event-id> --body <b> [--parent-author <pubkey>] [--marker root|reply]` — kind:1622 via `POST /git/comment`.
36
- - `rig pr create --title <t> (--range <A..B> | --patch-file <f>) [--branch <name>]` — kind:1617 via `POST /git/patch`; `--range` runs real `git format-patch --stdout` locally and derives the `commit`/`parent-commit` tags. A multi-commit range publishes ONE event carrying the whole series (cover-letter threading is out of scope in v1).
37
- - `rig status <target-event-id> <open|applied|closed|draft>` — kind:1630–1633 via `POST /git/status`, with the repo `a` tag attached.
117
+ `--repo-id`/`--owner` override the git config address (use `--owner` for repos you don't own).
38
118
 
39
119
  ## Library
40
120
 
@@ -46,7 +126,7 @@ No signing or payment code lives in the core — that stays behind the `Publishe
46
126
  - `remote-state.ts` — `fetchRemoteState`, the "what does the remote have?" reader: kind:30617/30618 relay fetch (NIP-33 latest-wins across a plural relay list) + `resolveMissing` Arweave GraphQL Git-SHA fallback.
47
127
  - `publisher.ts` — the `Publisher` interface (paid transport seam): `getFeeRates`, `uploadGitObject`, `publishEvent`. Implemented by the daemon (#227) and the standalone embedded client (#228).
48
128
  - `push.ts` — `planPush` (ref classification, object delta minus known sha→txId hints, oversize hard error, fee estimate) and `executePush` (uploads ref tips last, then ONE cumulative kind:30618 merging the full arweave map, kind:30617 first on first push; crash-resume safe via content-addressed skip).
49
- - `routes.ts` — the JSON wire shapes of the daemon's `/git/*` control routes (bigints as decimal strings, Maps as records) + the matching `serializePushPlan`/`serializePushResult` helpers, shared by the CLI and adoptable by `@toon-protocol/client-mcp`.
50
- - `cli/` — the `rig` bin: `push` (#229) and the single-event `issue`/`comment`/`pr`/`status` subcommands (#231).
129
+ - `routes.ts` — the JSON wire shapes of the daemon's `/git/*` control routes (bigints as decimal strings, Maps as records) + the matching `serializePushPlan`/`serializePushResult` helpers, shared with `@toon-protocol/client-mcp` (the daemon keeps these routes; only the CLI stopped using them).
130
+ - `cli/` — the `rig` bin: `init` (#248), `remote` (#249, relays as real git remotes + the shared relay resolution), `push` (#229), the single-event `issue`/`comment`/`pr create`/`pr status` subcommands (#231, nested under `pr` since #250), and the git passthrough (#250, `dispatch.ts` + `git-passthrough.ts`), all standalone-only with the `identity.ts` resolution chain.
51
131
 
52
- Pure builders promoted from the proven Rig E2E seed pipeline (`packages/rig-web/tests/e2e/seed/lib`). Part of [epic #222](https://github.com/toon-protocol/toon-client/issues/222).
132
+ Pure builders promoted from the proven Rig E2E seed pipeline (`packages/rig-web/tests/e2e/seed/lib`). Part of [epic #222](https://github.com/toon-protocol/toon-client/issues/222) and [epic #246](https://github.com/toon-protocol/toon-client/issues/246).
@@ -0,0 +1,137 @@
1
+ // src/cli/identity.ts
2
+ import { existsSync, readFileSync } from "fs";
3
+ import { homedir } from "os";
4
+ import { dirname, join } from "path";
5
+ var MissingIdentityError = class extends Error {
6
+ constructor(configPath) {
7
+ super(
8
+ `no identity found \u2014 rig needs a BIP-39 seed phrase to sign and pay. Provide one of (highest precedence first):
9
+ \u2022 RIG_MNEMONIC environment variable
10
+ \u2022 RIG_MNEMONIC=<phrase> in a project-local .env file (gitignore it!)
11
+ \u2022 the shared client config at ${configPath} (mnemonic or keystorePath + TOON_CLIENT_KEYSTORE_PASSWORD)`
12
+ );
13
+ this.name = "MissingIdentityError";
14
+ }
15
+ };
16
+ var DEFAULT_KEYSTORE_PASSWORD = "toon-client-default";
17
+ function clientConfigPath(env) {
18
+ const dir = env["TOON_CLIENT_HOME"] ?? join(homedir(), ".toon-client");
19
+ return join(dir, "config.json");
20
+ }
21
+ function readClientConfigFile(path) {
22
+ try {
23
+ return JSON.parse(readFileSync(path, "utf8"));
24
+ } catch (err) {
25
+ if (err.code === "ENOENT") return {};
26
+ throw new Error(
27
+ `failed to read client config at ${path}: ${err instanceof Error ? err.message : String(err)}`
28
+ );
29
+ }
30
+ }
31
+ var DOTENV_LINE_RE = /^\s*(?:export\s+)?RIG_MNEMONIC\s*=\s*(.*)\s*$/;
32
+ function parseDotenvMnemonic(content) {
33
+ let found;
34
+ for (const line of content.split(/\r?\n/)) {
35
+ const match = DOTENV_LINE_RE.exec(line);
36
+ if (!match) continue;
37
+ let value = (match[1] ?? "").trim();
38
+ const quote = value[0];
39
+ if ((quote === '"' || quote === "'") && value.length >= 2 && value.endsWith(quote)) {
40
+ value = value.slice(1, -1);
41
+ } else {
42
+ const hash = value.search(/\s#/);
43
+ if (hash !== -1) value = value.slice(0, hash);
44
+ value = value.trim();
45
+ }
46
+ if (value !== "") found = value;
47
+ }
48
+ return found;
49
+ }
50
+ function findDotenvMnemonic(startDir, stopDir) {
51
+ let dir = startDir;
52
+ for (; ; ) {
53
+ const candidate = join(dir, ".env");
54
+ if (existsSync(candidate)) {
55
+ try {
56
+ const mnemonic = parseDotenvMnemonic(readFileSync(candidate, "utf8"));
57
+ if (mnemonic !== void 0) return { path: candidate, mnemonic };
58
+ } catch {
59
+ }
60
+ }
61
+ if (stopDir !== void 0 && dir === stopDir) return void 0;
62
+ const parent = dirname(dir);
63
+ if (parent === dir) return void 0;
64
+ dir = parent;
65
+ }
66
+ }
67
+ function resolveMnemonicSource(options, file, configPath) {
68
+ const { env } = options;
69
+ const rigEnv = env["RIG_MNEMONIC"]?.trim();
70
+ if (rigEnv) {
71
+ return { mnemonic: rigEnv, source: "env", sourceLabel: "RIG_MNEMONIC env" };
72
+ }
73
+ const aliasEnv = env["TOON_CLIENT_MNEMONIC"]?.trim();
74
+ if (aliasEnv) {
75
+ options.warn(
76
+ "rig: TOON_CLIENT_MNEMONIC is deprecated \u2014 rename the variable to RIG_MNEMONIC"
77
+ );
78
+ return {
79
+ mnemonic: aliasEnv,
80
+ source: "env-alias",
81
+ sourceLabel: "TOON_CLIENT_MNEMONIC env (deprecated alias)"
82
+ };
83
+ }
84
+ const dotenv = findDotenvMnemonic(options.cwd, options.dotenvStopDir);
85
+ if (dotenv) {
86
+ return {
87
+ mnemonic: dotenv.mnemonic,
88
+ source: "dotenv",
89
+ sourceLabel: dotenv.path
90
+ };
91
+ }
92
+ if (typeof file.keystorePath === "string" && file.keystorePath !== "") {
93
+ return { keystorePath: file.keystorePath };
94
+ }
95
+ if (typeof file.mnemonic === "string" && file.mnemonic.trim() !== "") {
96
+ return {
97
+ mnemonic: file.mnemonic.trim(),
98
+ source: "config",
99
+ sourceLabel: configPath
100
+ };
101
+ }
102
+ throw new MissingIdentityError(configPath);
103
+ }
104
+ async function loadClientKeys() {
105
+ return await import("@toon-protocol/client");
106
+ }
107
+ async function resolveIdentity(options) {
108
+ const configPath = clientConfigPath(options.env);
109
+ const file = readClientConfigFile(configPath);
110
+ const picked = resolveMnemonicSource(options, file, configPath);
111
+ const keys = await loadClientKeys();
112
+ let source;
113
+ if ("keystorePath" in picked) {
114
+ const password = options.env["TOON_CLIENT_KEYSTORE_PASSWORD"] ?? (file.keystoreAutoPassword === true ? DEFAULT_KEYSTORE_PASSWORD : void 0);
115
+ if (!password) {
116
+ throw new Error(
117
+ `keystorePath is set in ${configPath} but TOON_CLIENT_KEYSTORE_PASSWORD is not provided`
118
+ );
119
+ }
120
+ source = {
121
+ mnemonic: keys.loadKeystore(picked.keystorePath, password),
122
+ source: "keystore",
123
+ sourceLabel: picked.keystorePath
124
+ };
125
+ } else {
126
+ source = picked;
127
+ }
128
+ const accountIndex = typeof file.mnemonicAccountIndex === "number" ? file.mnemonicAccountIndex : 0;
129
+ const { pubkey } = keys.deriveNostrKeyFromMnemonic(source.mnemonic, accountIndex);
130
+ return { ...source, accountIndex, pubkey };
131
+ }
132
+
133
+ export {
134
+ MissingIdentityError,
135
+ resolveIdentity
136
+ };
137
+ //# sourceMappingURL=chunk-CW4HJNMU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli/identity.ts"],"sourcesContent":["/**\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 `mnemonicAccountIndex` from the shared config file applies to\n * every source (same convention as the daemon), so the derived pubkey never\n * depends on WHERE the phrase came from.\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 { existsSync, readFileSync } 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 (shared config, default 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 'Provide one of (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/** Duplicated daemon convention: auto-keystore password. */\nconst DEFAULT_KEYSTORE_PASSWORD = 'toon-client-default';\n\n/** Shared client state dir: `TOON_CLIENT_HOME`, else `~/.toon-client`. */\nexport function clientConfigPath(env: NodeJS.ProcessEnv): string {\n const dir = env['TOON_CLIENT_HOME'] ?? join(homedir(), '.toon-client');\n return join(dir, 'config.json');\n}\n\n/** The subset of the shared client config file identity resolution reads. */\ninterface ClientConfigIdentityFields {\n mnemonic?: unknown;\n mnemonicAccountIndex?: unknown;\n keystorePath?: unknown;\n keystoreAutoPassword?: unknown;\n}\n\nfunction 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// .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// 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 return await import('@toon-protocol/client');\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 =\n typeof file.mnemonicAccountIndex === 'number' ? file.mnemonicAccountIndex : 0;\n const { pubkey } = keys.deriveNostrKeyFromMnemonic(source.mnemonic, accountIndex);\n return { ...source, accountIndex, pubkey };\n}\n"],"mappings":"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","names":[]}
@@ -1,11 +1,141 @@
1
- import {
2
- NonceLock,
3
- checkDaemonIdentity
4
- } from "./chunk-D3HGS44Y.js";
5
1
  import {
6
2
  MAX_OBJECT_SIZE
7
3
  } from "./chunk-X2CZPPDM.js";
8
4
 
5
+ // src/standalone/nonce-guard.ts
6
+ import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
7
+ import { homedir } from "os";
8
+ import { join } from "path";
9
+ var DEFAULT_DAEMON_PORT = 8787;
10
+ function defaultDaemonPort() {
11
+ const env = process.env["TOON_CLIENT_HTTP_PORT"];
12
+ const parsed = env ? Number(env) : NaN;
13
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_DAEMON_PORT;
14
+ }
15
+ function defaultLockDir() {
16
+ return process.env["TOON_CLIENT_HOME"] ?? join(homedir(), ".toon-client");
17
+ }
18
+ var DaemonIdentityConflictError = class extends Error {
19
+ constructor(pubkey, daemonUrl) {
20
+ super(
21
+ `toon-clientd is running with this identity (${pubkey.slice(0, 8)}\u2026) at ${daemonUrl} \u2014 use daemon mode or stop the daemon. Two writers on one identity would race the payment channel's cumulative-claim watermark (double-charge hazard).`
22
+ );
23
+ this.pubkey = pubkey;
24
+ this.daemonUrl = daemonUrl;
25
+ this.name = "DaemonIdentityConflictError";
26
+ }
27
+ pubkey;
28
+ daemonUrl;
29
+ };
30
+ var StandaloneLockError = class extends Error {
31
+ constructor(pubkey, lockPath, holderPid) {
32
+ super(
33
+ `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.`
34
+ );
35
+ this.pubkey = pubkey;
36
+ this.lockPath = lockPath;
37
+ this.holderPid = holderPid;
38
+ this.name = "StandaloneLockError";
39
+ }
40
+ pubkey;
41
+ lockPath;
42
+ holderPid;
43
+ };
44
+ async function checkDaemonIdentity(pubkey, options = {}) {
45
+ const port = options.port ?? defaultDaemonPort();
46
+ const fetchImpl = options.fetchImpl ?? fetch;
47
+ const url = `http://127.0.0.1:${port}/status`;
48
+ let daemonPubkey;
49
+ try {
50
+ const res = await fetchImpl(url, {
51
+ signal: AbortSignal.timeout(options.timeoutMs ?? 1500)
52
+ });
53
+ if (!res.ok) return;
54
+ const body = await res.json();
55
+ const candidate = body?.identity?.nostrPubkey;
56
+ if (typeof candidate === "string") daemonPubkey = candidate;
57
+ } catch {
58
+ return;
59
+ }
60
+ if (daemonPubkey !== void 0 && daemonPubkey === pubkey) {
61
+ throw new DaemonIdentityConflictError(pubkey, url);
62
+ }
63
+ }
64
+ function pidAlive(pid) {
65
+ try {
66
+ process.kill(pid, 0);
67
+ return true;
68
+ } catch (err) {
69
+ return err.code === "EPERM";
70
+ }
71
+ }
72
+ var NonceLock = class _NonceLock {
73
+ constructor(pubkey, lockPath) {
74
+ this.pubkey = pubkey;
75
+ this.lockPath = lockPath;
76
+ this.exitHandler = () => {
77
+ try {
78
+ unlinkSync(this.lockPath);
79
+ } catch {
80
+ }
81
+ };
82
+ process.once("exit", this.exitHandler);
83
+ }
84
+ pubkey;
85
+ lockPath;
86
+ released = false;
87
+ exitHandler;
88
+ static async acquire(pubkey, options = {}) {
89
+ const dir = options.dir ?? defaultLockDir();
90
+ const pid = options.pid ?? process.pid;
91
+ const lockPath = join(dir, `standalone-${pubkey}.lock`);
92
+ mkdirSync(dir, { recursive: true });
93
+ const payload = JSON.stringify(
94
+ {
95
+ pid,
96
+ pubkey,
97
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
98
+ },
99
+ null,
100
+ 2
101
+ );
102
+ for (let attempt = 0; attempt < 2; attempt++) {
103
+ try {
104
+ writeFileSync(lockPath, payload, { flag: "wx" });
105
+ return new _NonceLock(pubkey, lockPath);
106
+ } catch (err) {
107
+ if (err.code !== "EEXIST") throw err;
108
+ let holderPid;
109
+ try {
110
+ const parsed = JSON.parse(
111
+ readFileSync(lockPath, "utf8")
112
+ );
113
+ if (typeof parsed.pid === "number") holderPid = parsed.pid;
114
+ } catch {
115
+ }
116
+ if (holderPid !== void 0 && holderPid !== pid && pidAlive(holderPid)) {
117
+ throw new StandaloneLockError(pubkey, lockPath, holderPid);
118
+ }
119
+ try {
120
+ unlinkSync(lockPath);
121
+ } catch {
122
+ }
123
+ }
124
+ }
125
+ throw new StandaloneLockError(pubkey, lockPath, -1);
126
+ }
127
+ /** Remove the lockfile and detach the exit hook. Idempotent. */
128
+ release() {
129
+ if (this.released) return;
130
+ this.released = true;
131
+ process.removeListener("exit", this.exitHandler);
132
+ try {
133
+ unlinkSync(this.lockPath);
134
+ } catch {
135
+ }
136
+ }
137
+ };
138
+
9
139
  // src/standalone/standalone-publisher.ts
10
140
  import { ToonClient, parseFulfillHttp } from "@toon-protocol/client";
11
141
  var StandalonePublishError = class extends Error {
@@ -257,9 +387,16 @@ function nowSeconds() {
257
387
  }
258
388
 
259
389
  export {
390
+ DEFAULT_DAEMON_PORT,
391
+ defaultDaemonPort,
392
+ defaultLockDir,
393
+ DaemonIdentityConflictError,
394
+ StandaloneLockError,
395
+ checkDaemonIdentity,
396
+ NonceLock,
260
397
  StandalonePublishError,
261
398
  deriveRouteDestinations,
262
399
  extractArweaveTxId,
263
400
  StandalonePublisher
264
401
  };
265
- //# sourceMappingURL=chunk-5EXGKIPH.js.map
402
+ //# sourceMappingURL=chunk-XRRU6YBQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/standalone/nonce-guard.ts","../src/standalone/standalone-publisher.ts"],"sourcesContent":["/**\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 * 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} — use daemon mode or stop the daemon. Two writers on one ` +\n `identity 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 * StandalonePublisher — impl 2 of the {@link Publisher} seam (#228): an\n * EMBEDDED ToonClient constructed from the caller's config (mnemonic +\n * account index, the exact `packages/client/src/config.ts` shape) instead of\n * routing through a running toon-clientd. Made for CI jobs, servers, and\n * one-shot CLI runs where no daemon exists.\n *\n * Paid-write mechanics mirror the production daemon path\n * (`client-mcp/src/daemon/client-runner.ts`) and the proven seed pipeline\n * (`rig/tests/e2e/seed/lib/{publish,git-builder}.ts`):\n *\n * - one signed balance-proof CLAIM per write (`signBalanceProof(channelId,\n * fee)` — the ChannelManager accumulates the cumulative watermark),\n * - publishes route to the relay write destination with a flat per-event\n * fee (the daemon's `feePerEvent` convention),\n * - git objects upload as kind:5094 store writes tagged\n * Git-SHA/Git-Type/Repo, priced at bytes × per-byte rate (the seed\n * pipeline's bid), routed to the store destination via `proxyPath:\n * '/store'`, with the Arweave txId decoded from the FULFILL data.\n *\n * Because the embedded client signs claims on the SAME channels a daemon on\n * the same identity would, every paid operation is preceded by the nonce\n * guard (./nonce-guard.ts): refuse if a toon-clientd holds this identity,\n * and hold an exclusive per-pubkey lockfile against other standalone\n * processes for the lifetime of this publisher.\n */\n\nimport type {\n PublishEventResult,\n SignedBalanceProof,\n ToonClientConfig,\n} from '@toon-protocol/client';\nimport { ToonClient, parseFulfillHttp } from '@toon-protocol/client';\nimport { MAX_OBJECT_SIZE } from '../objects.js';\nimport type { UnsignedEvent } from '../nip34-events.js';\nimport type {\n FeeRates,\n GitObjectUpload,\n PublishReceipt,\n Publisher,\n UploadReceipt,\n} from '../publisher.js';\nimport { checkDaemonIdentity, NonceLock } from './nonce-guard.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** A fully-signed Nostr event (structural subset of nostr-tools' NostrEvent). */\nexport interface SignedNostrEvent extends UnsignedEvent {\n id: string;\n pubkey: string;\n sig: string;\n}\n\n/**\n * The slice of `ToonClient` the publisher drives. Kept structural so tests\n * inject a mock and alternative client builds stay compatible.\n */\nexport interface ToonClientLike {\n start(): Promise<unknown>;\n stop(): Promise<void>;\n isStarted?(): boolean;\n getPublicKey(): string;\n signEvent(template: UnsignedEvent): SignedNostrEvent;\n openChannel(destination?: string): Promise<string>;\n signBalanceProof(\n channelId: string,\n amount: bigint\n ): Promise<SignedBalanceProof>;\n publishEvent(\n event: SignedNostrEvent,\n options?: {\n destination?: string;\n claim?: SignedBalanceProof;\n ilpAmount?: bigint;\n proxyPath?: string;\n }\n ): Promise<PublishEventResult>;\n}\n\nexport interface StandalonePublisherOptions {\n /**\n * ToonClient config (mnemonic + mnemonicAccountIndex + proxy/BTP uplink +\n * settlement fields — see `packages/client/src/config.ts`). Exactly one of\n * `clientConfig` | `client` is required.\n */\n clientConfig?: ToonClientConfig;\n /** Pre-built client (tests / advanced callers). */\n client?: ToonClientLike;\n /**\n * ILP route for event publishes (relay `/write`). Default: derived from the\n * config's `destinationAddress` anchor (`<base>.relay.store` → `<base>.relay`,\n * matching the daemon's route derivation), else the anchor itself.\n */\n publishDestination?: string;\n /**\n * ILP route for git-object uploads (store `/store` → Arweave). Default:\n * derived like `publishDestination` (`<base>.relay.store` → `<base>.store`).\n */\n storeDestination?: string;\n /**\n * ILP destination the payment channel anchors to. Default: the client\n * config's `destinationAddress`.\n */\n channelDestination?: string;\n /** Flat fee per published event (daemon `feePerEvent` convention). Default 1n. */\n eventFee?: bigint;\n /** Upload fee per object body byte (seed pipeline's bid rate). Default 10n. */\n uploadFeePerByte?: bigint;\n /** Daemon control API port probed by the nonce guard. */\n daemonPort?: number;\n /** Directory for the per-identity advisory lockfile. */\n lockDir?: string;\n /** Fetch impl for the daemon probe (tests). */\n fetchImpl?: typeof fetch;\n}\n\n/** A relay/store rejected a paid write (fee NOT spent iff the claim failed too). */\nexport class StandalonePublishError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'StandalonePublishError';\n }\n}\n\n// ---------------------------------------------------------------------------\n// Route derivation (duplicated from client-mcp daemon/config.ts — that\n// package depends on this one for #227, so importing it back would be\n// circular; keep in sync)\n// ---------------------------------------------------------------------------\n\n/**\n * Derive publish/store routes from the channel anchor. Behind the devnet\n * proxy the anchor is `<base>.relay.store` (e.g. `g.proxy.relay.store`):\n * publishes terminate at `<base>.relay`, uploads at `<base>.store`. Anchors\n * not matching the convention pass through unchanged.\n */\nexport function deriveRouteDestinations(anchor: string): {\n publish: string;\n store: string;\n} {\n const segs = anchor.split('.');\n if (segs.at(-1) === 'store' && segs.at(-2) === 'relay') {\n const base = segs.slice(0, -2).join('.');\n return { publish: `${base}.relay`, store: `${base}.store` };\n }\n return { publish: anchor, store: anchor };\n}\n\n// ---------------------------------------------------------------------------\n// FULFILL → Arweave txId (mirrors @toon-protocol/client blob-storage.ts,\n// whose extractor is not exported; uses the exported parseFulfillHttp)\n// ---------------------------------------------------------------------------\n\n/** Arweave tx IDs are base64url-encoded 32-byte values (43 chars). */\nconst ARWEAVE_TX_ID_REGEX = /^[A-Za-z0-9_-]{43}$/;\n\n/**\n * Decode the Arweave txId from a store-write FULFILL. The deployed payment\n * proxy returns the store's verbatim HTTP/1.1 response\n * (`{\"accept\":true,\"txId\":…}` body); legacy non-proxy providers return bare\n * `base64(utf8(txId))`.\n *\n * @throws {StandalonePublishError} when no valid txId can be extracted.\n */\nexport function extractArweaveTxId(base64Data: string): string {\n const http = parseFulfillHttp(base64Data);\n\n if (!http.isHttp) {\n const legacy = Buffer.from(base64Data, 'base64').toString('utf8');\n if (!ARWEAVE_TX_ID_REGEX.test(legacy)) {\n throw new StandalonePublishError(\n `FULFILL data is not a valid Arweave tx ID: \"${legacy}\"`\n );\n }\n return legacy;\n }\n\n if (http.status < 200 || http.status >= 300) {\n throw new StandalonePublishError(\n `git-object upload failed: store returned HTTP ${http.status}` +\n (http.body ? ` - ${http.body}` : '')\n );\n }\n\n let parsed: { accept?: boolean; txId?: unknown; data?: unknown; error?: unknown };\n try {\n parsed = JSON.parse(http.body) as typeof parsed;\n } catch {\n throw new StandalonePublishError(\n `git-object upload response body was not valid JSON: \"${http.body}\"`\n );\n }\n\n if (parsed.accept === false) {\n const reason = typeof parsed.error === 'string' ? `: ${parsed.error}` : '';\n throw new StandalonePublishError(\n `git-object upload rejected by store (accept:false)${reason}`\n );\n }\n\n if (typeof parsed.txId === 'string' && ARWEAVE_TX_ID_REGEX.test(parsed.txId)) {\n return parsed.txId;\n }\n if (typeof parsed.data === 'string' && parsed.data.length > 0) {\n const decoded = Buffer.from(parsed.data, 'base64').toString('utf8');\n if (ARWEAVE_TX_ID_REGEX.test(decoded)) return decoded;\n }\n\n throw new StandalonePublishError(\n `git-object upload response did not contain a valid Arweave tx ID: \"${http.body}\"`\n );\n}\n\n// ---------------------------------------------------------------------------\n// StandalonePublisher\n// ---------------------------------------------------------------------------\n\nexport class StandalonePublisher implements Publisher {\n private readonly client: ToonClientLike;\n private readonly ownsClient: boolean;\n private readonly publishDestination: string | undefined;\n private readonly storeDestination: string | undefined;\n private readonly channelDestination: string | undefined;\n private readonly eventFee: bigint;\n private readonly uploadFeePerByte: bigint;\n private readonly daemonPort: number | undefined;\n private readonly lockDir: string | undefined;\n private readonly fetchImpl: typeof fetch | undefined;\n\n private lock: NonceLock | undefined;\n private channelId: string | undefined;\n private readyPromise: Promise<void> | undefined;\n\n constructor(options: StandalonePublisherOptions) {\n if (options.client && options.clientConfig) {\n throw new Error(\n 'StandalonePublisher: provide either `clientConfig` or `client`, not both'\n );\n }\n if (options.client) {\n this.client = options.client;\n this.ownsClient = false;\n } else if (options.clientConfig) {\n this.client = new ToonClient(options.clientConfig);\n this.ownsClient = true;\n } else {\n throw new Error(\n 'StandalonePublisher: one of `clientConfig` (mnemonic-based ToonClient config) or `client` is required'\n );\n }\n\n // Routes: explicit option → derived from the channel anchor (same\n // `<base>.relay.store` convention the daemon resolves).\n const anchor =\n options.channelDestination ?? options.clientConfig?.destinationAddress;\n const routes = anchor ? deriveRouteDestinations(anchor) : undefined;\n this.publishDestination = options.publishDestination ?? routes?.publish;\n this.storeDestination = options.storeDestination ?? routes?.store;\n this.channelDestination = options.channelDestination;\n\n this.eventFee = options.eventFee ?? 1n;\n this.uploadFeePerByte = options.uploadFeePerByte ?? 10n;\n this.daemonPort = options.daemonPort;\n this.lockDir = options.lockDir;\n this.fetchImpl = options.fetchImpl;\n }\n\n /** Hex Nostr pubkey of the embedded identity (available before start). */\n getPublicKey(): string {\n return this.client.getPublicKey();\n }\n\n /**\n * Run the nonce guard, start the embedded client, and open (or resume) the\n * payment channel. Called lazily by the first paid operation; safe to call\n * eagerly to fail fast. Idempotent.\n */\n start(): Promise<void> {\n this.readyPromise ??= this.doStart().catch((err: unknown) => {\n // Let a later call retry (e.g. after the conflicting daemon stops).\n this.readyPromise = undefined;\n throw err;\n });\n return this.readyPromise;\n }\n\n private async doStart(): Promise<void> {\n const pubkey = this.client.getPublicKey();\n\n // Guard 1: refuse while a toon-clientd holds this identity.\n await checkDaemonIdentity(pubkey, {\n ...(this.daemonPort !== undefined ? { port: this.daemonPort } : {}),\n ...(this.fetchImpl ? { fetchImpl: this.fetchImpl } : {}),\n });\n\n // Guard 2: exclusive advisory lock against other standalone processes.\n this.lock = await NonceLock.acquire(pubkey, {\n ...(this.lockDir !== undefined ? { dir: this.lockDir } : {}),\n });\n\n try {\n if (this.client.isStarted?.() !== true) {\n await this.client.start();\n }\n // Idempotent — returns the existing channel for the peer if one is open.\n this.channelId = await this.client.openChannel(this.channelDestination);\n } catch (err) {\n this.lock.release();\n this.lock = undefined;\n throw err;\n }\n }\n\n /** Release the identity lock and stop the embedded client (if we own it). */\n async stop(): Promise<void> {\n this.lock?.release();\n this.lock = undefined;\n this.readyPromise = undefined;\n this.channelId = undefined;\n if (this.ownsClient) {\n await this.client.stop();\n }\n }\n\n // ── Publisher ─────────────────────────────────────────────────────────────\n\n /**\n * Fee rates for `planPush` estimation: the flat per-event fee and the\n * per-byte upload rate this publisher pays (daemon `feePerEvent` and seed\n * bid-rate conventions; override via options).\n */\n getFeeRates(): Promise<FeeRates> {\n return Promise.resolve({\n uploadFeePerByte: this.uploadFeePerByte,\n eventFee: this.eventFee,\n });\n }\n\n /**\n * Upload one git object as a kind:5094 store write (Git-SHA/Git-Type/Repo\n * tagged — the proven seed-pipeline shape), signing one balance-proof claim\n * for `body.length × uploadFeePerByte`.\n */\n async uploadGitObject(upload: GitObjectUpload): Promise<UploadReceipt> {\n if (upload.body.length > MAX_OBJECT_SIZE) {\n throw new StandalonePublishError(\n `git object ${upload.sha} exceeds the ${MAX_OBJECT_SIZE}-byte limit: ${upload.body.length} bytes`\n );\n }\n await this.start();\n const channelId = this.requireChannel();\n\n const fee = BigInt(upload.body.length) * this.uploadFeePerByte;\n const event = this.client.signEvent({\n kind: 5094,\n content: '',\n created_at: nowSeconds(),\n tags: [\n ['i', upload.body.toString('base64'), 'blob'],\n ['bid', fee.toString(), 'usdc'],\n ['output', 'application/octet-stream'],\n ['Git-SHA', upload.sha],\n ['Git-Type', upload.type],\n ['Repo', upload.repoId],\n ],\n });\n\n const claim = await this.client.signBalanceProof(channelId, fee);\n const result = await this.client.publishEvent(event, {\n ...(this.storeDestination ? { destination: this.storeDestination } : {}),\n claim,\n ilpAmount: fee,\n // The store backend serves POST /store (not the relay's /write).\n proxyPath: '/store',\n });\n if (!result.success) {\n throw new StandalonePublishError(\n `git-object upload rejected (${upload.sha}): ${result.error ?? 'store rejected the write'}`\n );\n }\n if (!result.data) {\n throw new StandalonePublishError(\n `git-object upload FULFILL carried no data (${upload.sha}); expected the Arweave tx ID`\n );\n }\n return { txId: extractArweaveTxId(result.data), feePaid: fee };\n }\n\n /**\n * Sign the event with the embedded identity and pay-to-publish it through\n * the relay write route, one claim for the flat per-event fee.\n *\n * `relayUrls` is the interface's plural forward-compat surface (parked\n * #84): the standalone impl routes over ILP to its single configured\n * publish destination, so more than one relay is refused rather than\n * silently half-published.\n */\n async publishEvent(\n event: UnsignedEvent,\n relayUrls: string[]\n ): Promise<PublishReceipt> {\n if (relayUrls.length > 1) {\n throw new StandalonePublishError(\n `multi-relay publish is not supported yet (got ${relayUrls.length} relays) — the standalone publisher routes to a single relay destination (#84 parked)`\n );\n }\n await this.start();\n const channelId = this.requireChannel();\n\n const signed = this.client.signEvent(event);\n const fee = this.eventFee;\n const claim = await this.client.signBalanceProof(channelId, fee);\n const result = await this.client.publishEvent(signed, {\n ...(this.publishDestination\n ? { destination: this.publishDestination }\n : {}),\n claim,\n ilpAmount: fee,\n });\n if (!result.success) {\n throw new StandalonePublishError(\n `publish rejected (kind ${event.kind}): ${result.error ?? 'relay rejected the event'}`\n );\n }\n return { eventId: result.eventId ?? signed.id, feePaid: fee };\n }\n\n private requireChannel(): string {\n if (!this.channelId) {\n throw new StandalonePublishError(\n 'no payment channel open — start() did not complete'\n );\n }\n return this.channelId;\n }\n}\n\nfunction nowSeconds(): number {\n return Math.floor(Date.now() / 1000);\n}\n"],"mappings":"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","names":[]}