@toon-protocol/rig 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -18
- package/dist/chunk-QD437XAW.js +143 -0
- package/dist/chunk-QD437XAW.js.map +1 -0
- package/dist/{chunk-5EXGKIPH.js → chunk-XRRU6YBQ.js} +142 -5
- package/dist/chunk-XRRU6YBQ.js.map +1 -0
- package/dist/cli/rig.js +820 -500
- package/dist/cli/rig.js.map +1 -1
- package/dist/standalone/index.js +6 -8
- package/dist/{standalone-mode-IK2RXILS.js → standalone-mode-CAYWOURK.js} +13 -34
- package/dist/standalone-mode-CAYWOURK.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-5EXGKIPH.js.map +0 -1
- package/dist/chunk-D3HGS44Y.js +0 -144
- package/dist/chunk-D3HGS44Y.js.map +0 -1
- package/dist/standalone-mode-IK2RXILS.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,40 +1,119 @@
|
|
|
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
|
-
|
|
20
|
+
npm install -g @toon-protocol/rig @toon-protocol/client
|
|
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
|
-
#
|
|
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
|
|
56
|
+
rig pr status <event-id> applied # kind:1631
|
|
57
|
+
# (bare `rig status` is git's)
|
|
21
58
|
```
|
|
22
59
|
|
|
23
|
-
|
|
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 (install the optional `@toon-protocol/client` peer to use
|
|
68
|
+
it). The mnemonic is resolved along one precedence chain — highest first:
|
|
69
|
+
|
|
70
|
+
1. `RIG_MNEMONIC` environment variable
|
|
71
|
+
2. `TOON_CLIENT_MNEMONIC` environment variable — deprecated alias, warns on
|
|
72
|
+
stderr; rename it to `RIG_MNEMONIC`
|
|
73
|
+
3. project-local `.env` — found by walking up from the working directory
|
|
74
|
+
(through the repo root); ONLY the `RIG_MNEMONIC` line is parsed out of it
|
|
75
|
+
(rig never loads arbitrary env from the file, and never requires it).
|
|
76
|
+
**Gitignore it** — the phrase must never be committed.
|
|
77
|
+
4. the shared `~/.toon-client` state dir (`TOON_CLIENT_HOME` override):
|
|
78
|
+
encrypted keystore (`keystorePath` + `TOON_CLIENT_KEYSTORE_PASSWORD`),
|
|
79
|
+
then the `mnemonic` config field
|
|
80
|
+
|
|
81
|
+
Every paid command reports which source is active and the derived pubkey
|
|
82
|
+
(`Identity: <pubkey> (from …)`, and an `identity` object in `--json`
|
|
83
|
+
output) — the phrase itself is never printed and never written to git config
|
|
84
|
+
or any repo file.
|
|
85
|
+
|
|
86
|
+
A running `toon-clientd` daemon on the **same identity** is still refused
|
|
87
|
+
(the nonce guard): two writers would race the payment channel's
|
|
88
|
+
cumulative-claim watermark. Stop the daemon or publish through its
|
|
89
|
+
`toon_git_*` MCP tools instead — the daemon's `/git/*` routes are the MCP
|
|
90
|
+
host path and are unaffected by the CLI.
|
|
91
|
+
|
|
92
|
+
### Pushing
|
|
93
|
+
|
|
94
|
+
`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.
|
|
95
|
+
|
|
96
|
+
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).
|
|
97
|
+
|
|
98
|
+
### Relays are origins
|
|
24
99
|
|
|
25
|
-
|
|
100
|
+
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
101
|
|
|
27
|
-
-
|
|
28
|
-
-
|
|
102
|
+
- `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.
|
|
103
|
+
- `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.
|
|
104
|
+
- The event commands take `--remote <name>` (default `origin`).
|
|
105
|
+
- `--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).
|
|
106
|
+
- 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.
|
|
107
|
+
- 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
108
|
|
|
30
|
-
|
|
109
|
+
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
110
|
|
|
32
|
-
|
|
111
|
+
- `rig issue create --title <t> [--body <b> | --body-file <f> | stdin] [--label <l>]…` — kind:1621.
|
|
112
|
+
- `rig comment <root-event-id> --body <b> [--parent-author <pubkey>] [--marker root|reply]` — kind:1622.
|
|
113
|
+
- `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).
|
|
114
|
+
- `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
115
|
|
|
34
|
-
-
|
|
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.
|
|
116
|
+
`--repo-id`/`--owner` override the git config address (use `--owner` for repos you don't own).
|
|
38
117
|
|
|
39
118
|
## Library
|
|
40
119
|
|
|
@@ -46,7 +125,7 @@ No signing or payment code lives in the core — that stays behind the `Publishe
|
|
|
46
125
|
- `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
126
|
- `publisher.ts` — the `Publisher` interface (paid transport seam): `getFeeRates`, `uploadGitObject`, `publishEvent`. Implemented by the daemon (#227) and the standalone embedded client (#228).
|
|
48
127
|
- `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
|
|
50
|
-
- `cli/` — the `rig` bin: `push` (#229)
|
|
128
|
+
- `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).
|
|
129
|
+
- `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
130
|
|
|
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).
|
|
131
|
+
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,143 @@
|
|
|
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
|
+
try {
|
|
106
|
+
return await import("@toon-protocol/client");
|
|
107
|
+
} catch (err) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`identity derivation needs the optional peer dependency @toon-protocol/client \u2014 install it (\`npm i @toon-protocol/client\`) and re-run (${err instanceof Error ? err.message : String(err)})`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async function resolveIdentity(options) {
|
|
114
|
+
const configPath = clientConfigPath(options.env);
|
|
115
|
+
const file = readClientConfigFile(configPath);
|
|
116
|
+
const picked = resolveMnemonicSource(options, file, configPath);
|
|
117
|
+
const keys = await loadClientKeys();
|
|
118
|
+
let source;
|
|
119
|
+
if ("keystorePath" in picked) {
|
|
120
|
+
const password = options.env["TOON_CLIENT_KEYSTORE_PASSWORD"] ?? (file.keystoreAutoPassword === true ? DEFAULT_KEYSTORE_PASSWORD : void 0);
|
|
121
|
+
if (!password) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`keystorePath is set in ${configPath} but TOON_CLIENT_KEYSTORE_PASSWORD is not provided`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
source = {
|
|
127
|
+
mnemonic: keys.loadKeystore(picked.keystorePath, password),
|
|
128
|
+
source: "keystore",
|
|
129
|
+
sourceLabel: picked.keystorePath
|
|
130
|
+
};
|
|
131
|
+
} else {
|
|
132
|
+
source = picked;
|
|
133
|
+
}
|
|
134
|
+
const accountIndex = typeof file.mnemonicAccountIndex === "number" ? file.mnemonicAccountIndex : 0;
|
|
135
|
+
const { pubkey } = keys.deriveNostrKeyFromMnemonic(source.mnemonic, accountIndex);
|
|
136
|
+
return { ...source, accountIndex, pubkey };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export {
|
|
140
|
+
MissingIdentityError,
|
|
141
|
+
resolveIdentity
|
|
142
|
+
};
|
|
143
|
+
//# sourceMappingURL=chunk-QD437XAW.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 OPTIONAL\n * `@toon-protocol/client` peer dependency, loaded via dynamic import so this\n * module can be statically imported by every command without dragging the\n * client into runs that fail 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/** Load the optional client peer dependency, with a clear error when absent. */\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 try {\n return await import('@toon-protocol/client');\n } catch (err) {\n throw new Error(\n 'identity derivation needs the optional peer dependency ' +\n '@toon-protocol/client — install it (`npm i @toon-protocol/client`) ' +\n `and re-run (${err instanceof Error ? err.message : String(err)})`\n );\n }\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-
|
|
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":[]}
|