v0.1.13 - Change default index to 0 in wizard (when 0 is in range/set)

This commit is contained in:
Laan Tungir
2026-08-04 21:29:58 -04:00
parent d7fb3787e6
commit 7cbefe13ec
2 changed files with 200 additions and 7 deletions
+179
View File
@@ -0,0 +1,179 @@
# Plan: `n_signer_client` — Linux CLI for n_signer
## Goal
A standalone Linux command-line client `n_signer_client` that connects to a
running `n_signer` process over its abstract UNIX socket (and optionally the
other framed transports) and exposes the full verb surface over stdin/stdout so
that signed events can be piped directly into `nak publish`.
## Deliverable
- New file: [`client/n_signer_client.c`](../client/n_signer_client.c) — single-file C99 program.
- New Makefile target producing `build/n_signer_client`.
- Updated [`client/README.md`](../client/README.md) with usage and the pipe-to-nak recipe.
The binary links `nostr_core_lib` exactly like the existing examples
[`examples/sign_event_client.c`](../examples/sign_event_client.c) and
[`examples/get_public_key_client.c`](../examples/get_public_key_client.c). It
uses:
- [`nsigner_transport_open_unix`](../resources/nostr_core_lib/nostr_core/nsigner_transport.h) (and optionally `_tcp`, `_serial`, `_qrexec`)
- [`nsigner_client_new`](../resources/nostr_core_lib/nostr_core/nsigner_client.h) / [`nsigner_client_free`](../resources/nostr_core_lib/nostr_core/nsigner_client.h)
- [`nsigner_client_call`](../resources/nostr_core_lib/nostr_core/nsigner_client.h) (takes ownership of `params`)
- [`nsigner_client_set_auth`](../resources/nostr_core_lib/nostr_core/nsigner_client.h) for TCP mode
## CLI shape
```
n_signer_client [global options] <verb> [verb args...]
```
Global options:
| Flag | Default | Meaning |
|---|---|---|
| `--socket-name`, `-n <name>` | auto-discover | Abstract socket name without `@` |
| `--role <name>` | none | Selector `{"role":"<name>"}` (Nostr verbs) |
| `--nostr-index <N>` | none | Selector `{"nostr_index":N}` (mutually exclusive with `--role`) |
| `--algorithm <alg>` | none | Algorithm-based verbs: `secp256k1`/`ed25519`/`x25519`/`ml-dsa-65`/`slh-dsa-128s`/`ml-kem-768`/`otp` |
| `--index <N>` | `0` | Algorithm key index |
| `--scheme <schnorr\|ecdsa>` | `schnorr` | secp256k1 sign/verify scheme |
| `--format <plain\|structured>` | `plain` | `get-public-key` output shape |
| `--timeout <ms>` | `5000` | Transport timeout |
| `--tcp <host:port>` | none | Use TCP transport (requires `--auth-privkey`) |
| `--serial <device>` | none | Use USB CDC-ACM serial transport |
| `--qrexec <qube:service>` | none | Use Qubes qrexec transport |
| `--auth-privkey <32-byte hex>` | none | Auth envelope privkey for TCP |
| `--auth-label <text>` | none | Auth envelope label |
Auto-discovery: when no `--socket-name` and no explicit transport is given,
enumerate via `nsigner_transport_list_unix` and proceed only if exactly one
`nsigner*` socket exists (mirror `discover_single_socket_name` in
[`src/main.c`](../src/main.c)).
## Verb surface (full)
### Nostr verbs (role-based; selector from `--role` / `--nostr-index`)
| Verb | RPC method | stdin/argv | stdout |
|---|---|---|---|
| `get-public-key` | `nostr_get_public_key` | none | pubkey hex (or structured JSON with `--format structured`) |
| `sign-event` | `nostr_sign_event` | event JSON from argv or one stdin line | signed event JSON, one line |
| `mine-event` | `nostr_mine_event` | event JSON from argv or stdin; `--difficulty`, `--threads`, `--timeout-sec` | signed mined event JSON |
| `nip04-encrypt <peer-pubkey>` | `nostr_nip04_encrypt` | plaintext from argv or stdin | ciphertext |
| `nip04-decrypt <peer-pubkey>` | `nostr_nip04_decrypt` | ciphertext from argv or stdin | plaintext |
| `nip44-encrypt <peer-pubkey>` | `nostr_nip44_encrypt` | plaintext from argv or stdin | ciphertext |
| `nip44-decrypt <peer-pubkey>` | `nostr_nip44_decrypt` | ciphertext from argv or stdin | plaintext |
### Algorithm-based verbs (use `--algorithm` and `--index`)
| Verb | RPC method | argv | stdout |
|---|---|---|---|
| `get-public-key` | `get_public_key` | none | structured JSON `{"algorithm":...,"public_key":...,"key_id":...}` |
| `sign <msg-hex>` | `sign` | hex bytes | signature hex |
| `verify <msg-hex> <sig-hex>` | `verify` | hex bytes | `valid` / `invalid` (exit 0/1) |
| `derive <data>` | `derive` | UTF-8 data (argv or stdin) | `{"algorithm":...,"key_id":...,"digest":...}` |
| `encapsulate <peer-pubkey-hex>` | `encapsulate` | hex | `{"ciphertext":...,"shared_secret":...}` |
| `decapsulate <ciphertext-hex>` | `decapsulate` | hex | `{"shared_secret":...}` |
| `derive-shared-secret <peer-pubkey-hex>` | `derive_shared_secret` | hex | shared secret hex |
### Generic escape hatch
| Verb | RPC method | input | stdout |
|---|---|---|---|
| `call <method>` | `<method>` | JSON `params` array from stdin (one line) or argv | raw `result` JSON |
This keeps the client future-proof for any new server verb without a CLI rewrite.
## stdin/stdout contract (pipe-friendly)
- All payload output goes to stdout as a single line, newline-terminated.
- All diagnostics go to stderr.
- Exit code: `0` on success, non-zero on transport/RPC error (use
`nsigner_client_last_error` for the message).
- `sign-event` reads event JSON from argv if present, else reads exactly one
line from stdin. This is the pipe-to-nak path:
```bash
echo '{"kind":1,"content":"hello","tags":[],"created_at":1700000000}' \
| n_signer_client sign-event \
| nak publish
```
- `nip04-encrypt` / `nip44-encrypt` read plaintext from argv or stdin.
- `nip04-decrypt` / `nip44-decrypt` read ciphertext from argv or stdin.
- `sign` / `verify` / `encapsulate` / `decapsulate` / `derive-shared-secret`
take hex from argv (binary payloads, not pipe-friendly text).
- `derive` takes UTF-8 data from argv or stdin.
- `call` reads a JSON `params` array from stdin (one line) or argv.
## Selector handling
- `--role <name>``{"role":"<name>"}` in the options object.
- `--nostr-index <N>``{"nostr_index":N}` (mutually exclusive with `--role`).
- Default: no selector (server picks default role `main`).
- For algorithm verbs, `--algorithm` and `--index` populate the options object
instead; `--scheme` adds `"scheme"` for secp256k1 sign/verify.
## Transport
- Default: UNIX abstract socket via `nsigner_transport_open_unix(name, timeout_ms)`.
- `--tcp host:port``nsigner_transport_open_tcp` (requires `--auth-privkey`
32-byte hex; calls `nsigner_client_set_auth` with `--auth-label`).
- `--serial /dev/ttyACM0``nsigner_transport_open_serial`.
- `--qrexec qube:service``nsigner_transport_open_qrexec`.
- The vtable is uniform so all four transports share the same call path after
construction.
## Build
Add to [`Makefile`](../Makefile):
```make
N_SIGNER_CLIENT_TARGET := $(BUILD_DIR)/n_signer_client
clients: $(N_SIGNER_CLIENT_TARGET)
$(N_SIGNER_CLIENT_TARGET): $(CLIENT_DIR)/n_signer_client.c
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(CLIENT_DIR)/n_signer_client.c -o $(N_SIGNER_CLIENT_TARGET) $(LDFLAGS)
```
Add `clients` to the `all` aggregate and to the `test-client` target so it is
built alongside the examples.
## Testing
1. Manual smoke test against a running `nsigner`:
- `n_signer_client get-public-key` → 64-hex pubkey.
- `echo '{"kind":1,"content":"hello","tags":[],"created_at":1}' | n_signer_client sign-event` → signed event with `id`, `pubkey`, `sig`.
- Pipe to `nak event` / `nak publish` to verify the signed event is well-formed.
- `n_signer_client --algorithm ed25519 sign 68656c6c6f` → 64-byte sig hex.
2. Optional bash script `tests/test_n_signer_client.sh` that:
- Spawns `nsigner --socket-name nsigner_test --listen unix --mnemonic-stdin` with a fixed test mnemonic.
- Runs each verb and asserts on stdout shape.
- Tears down the server.
## Mermaid flow
```mermaid
flowchart LR
A[stdin or argv event JSON] --> B[n_signer_client sign-event]
B --> C[nsigner_transport_open_unix]
C --> D[nsigner_client_call nostr_sign_event]
D --> E[nsigner @nsigner socket]
E --> F[signed event JSON result]
F --> G[stdout one line]
G --> H[nak publish]
```
## Out of scope
- No TUI, no approval UI — the human attendant lives in the running `nsigner`
process; the client is just a thin wire caller.
- No key storage, no mnemonic handling.
- No HTTP listener client (the `http_listener` is server-side; the client uses
the framed transports).
- No NIP-46 bunker mode (covered separately by
[`plans/nip46_bunker_mode.md`](nip46_bunker_mode.md)).
+21 -7
View File
@@ -800,8 +800,8 @@ int socket_name_random(char *out, size_t out_len);
/* Version information (auto-updated by build/version tooling) */
#define NSIGNER_VERSION_MAJOR 0
#define NSIGNER_VERSION_MINOR 1
#define NSIGNER_VERSION_PATCH 12
#define NSIGNER_VERSION "v0.1.12"
#define NSIGNER_VERSION_PATCH 13
#define NSIGNER_VERSION "v0.1.13"
/* NSIGNER_HEADERLESS_DECLS_END */
@@ -2336,9 +2336,23 @@ static void prompt_named_path_roles(role_table_t *role_table) {
int is_fixed = (range_lo < 0); /* fixed path, no variable segment */
if (!is_fixed) {
/* Default index — only for templated paths */
/* Default index — only for templated paths.
* Default to 0 if it's in the allowed range/set, otherwise range_lo. */
int suggested_default;
if (allowed_count > 0) {
/* Set form: check if 0 is in the set */
int j, has_zero = 0;
for (j = 0; j < allowed_count; j++) {
if (allowed_indices[j] == 0) { has_zero = 1; break; }
}
suggested_default = has_zero ? 0 : range_lo;
} else {
/* Range form: 0 is valid if range_lo <= 0 <= range_hi */
suggested_default = (range_lo <= 0 && range_hi >= 0) ? 0 : range_lo;
}
char default_idx_str[16];
printf(" Default index [%d]: ", range_lo);
printf(" Default index [%d]: ", suggested_default);
fflush(stdout);
if (read_line_stdin(default_idx_str, sizeof(default_idx_str)) != 0) return;
{
@@ -2349,14 +2363,14 @@ static void prompt_named_path_roles(role_table_t *role_table) {
}
}
if (default_idx_str[0] == '\0') {
default_index = range_lo;
default_index = suggested_default;
} else {
char *endp = NULL;
long di = strtol(default_idx_str, &endp, 10);
if (*endp != '\0' || di < range_lo || di > range_hi) {
printf(" Default index out of range [%d-%d], using %d.\n",
range_lo, range_hi, range_lo);
default_index = range_lo;
range_lo, range_hi, suggested_default);
default_index = suggested_default;
} else {
default_index = (int)di;
}