# Plan: nostr_core_lib client updates for new n_signer features

Status: design / ready for review.

Related:
- [`../nostr_core_lib/nostr_core/nsigner_transport.h`](../../nostr_core_lib/nostr_core/nsigner_transport.h) — transport vtable
- [`../nostr_core_lib/nostr_core/nsigner_client.h`](../../nostr_core_lib/nostr_core/nsigner_client.h) — low-level client
- [`../nostr_core_lib/nostr_core/nostr_signer.h`](../../nostr_core_lib/nostr_core/nostr_signer.h) — high-level signer
- [`../nostr_core_lib/nostr_core/nostr_signer.c`](../../nostr_core_lib/nostr_core/nostr_signer.c) — `signer_remote_params_with_selector`
- [`plans/qrexec_persistent_bridge.md`](qrexec_persistent_bridge.md) — qrexec bridge design
- [`plans/interactive_transport_selection.md`](interactive_transport_selection.md) — index whitelist

---

## 1. Gaps identified

The `nostr_core_lib` client stack was built before the recent n_signer features. Three gaps need filling:

### 1.1 No `nostr_index` selector in the high-level API

`signer_remote_params_with_selector()` ([`nostr_signer.c:287`](../../nostr_core_lib/nostr_core/nostr_signer.c:287)) only emits `{"role":"..."}`. It does not support `{"nostr_index":N}`, which is n_signer's primary key-selection mechanism for Nostr identities (NIP-06 `m/44'/1237'/N'/0/0`).

The `nostr_signer_nsigner_*` constructors take a `const char* role` parameter. There's no way to say "use nostr_index 3" through the high-level API. Callers who want a specific Nostr identity by index must drop down to the low-level `nsigner_client_call` and build params manually — which is exactly what [`examples/get_pubkey_tcp.c`](../examples/get_pubkey_tcp.c) and [`examples/n_signer_qube_example_fips.js`](../examples/n_signer_qube_example_fips.js) do.

### 1.2 No qrexec transport

`nsigner_transport_open_*` supports unix, tcp, serial, fds — but not qrexec. On Qubes OS, the primary inter-qube path is `qrexec-client-vm <target> qubes.NsignerRpc`, which pipes framed I/O through stdin/stdout of a subprocess. There's no transport that wraps this.

### 1.3 No error code mapping for `index_not_allowed` (2002)

n_signer now returns `{"error":{"code":2002,"message":"index_not_allowed"}}` when the index whitelist denies a request. The client should surface this distinctly from `policy_denied` (2001) so callers can tell "this index isn't whitelisted" vs "this caller isn't approved".

---

## 2. Proposed changes

### 2.1 Add `nostr_index` selector to the high-level API

**Option A (minimal):** Add a new constructor variant that takes `nostr_index` instead of `role`:

```c
nostr_signer_t* nostr_signer_nsigner_unix_index(const char* socket_name, int nostr_index, int timeout_ms);
nostr_signer_t* nostr_signer_nsigner_tcp_index(const char* host, int port, int nostr_index, int timeout_ms);
/* etc. */
```

**Option B (cleaner):** Replace the `role` string parameter with a selector struct:

```c
typedef struct {
    const char* role;       /* NULL = use default */
    int nostr_index;        /* -1 = not set */
    const char* role_path;  /* NULL = not set */
} nsigner_selector_t;

nostr_signer_t* nostr_signer_nsigner_unix(const char* socket_name, const nsigner_selector_t* selector, int timeout_ms);
```

**Option C (additive, chosen):** Keep existing constructors, add a setter for nostr_index:

```c
int nostr_signer_nsigner_set_nostr_index(nostr_signer_t* signer, int nostr_index);
```

This sets the index on the remote backend, and `signer_remote_params_with_selector` emits `{"nostr_index":N}` instead of `{"role":"..."}` when set. Existing callers using `role` are unaffected.

**Decision: Option C** — additive, no API break, smallest change.

### 2.2 Add qrexec transport

New transport constructor:

```c
nsigner_transport_t* nsigner_transport_open_qrexec(const char* target_qube, const char* service_name, int timeout_ms);
```

Implementation: `fork()` + `execvp("qrexec-client-vm", [target_qube, service_name])` with `stdin`/`stdout` pipes. The `send_framed`/`recv_framed` vtable methods write/read through the pipes. `reconnect` re-spawns the subprocess (qrexec handles one request per invocation, so reconnect is mandatory before each call — same as the existing per-request reconnect pattern).

The transport is stateless per call: each `reconnect` spawns a fresh `qrexec-client-vm` process, sends one framed request, reads one framed response, and the process exits. This matches the qrexec execution model.

### 2.3 Add `nostr_signer_nsigner_qrexec` high-level constructor

```c
nostr_signer_t* nostr_signer_nsigner_qrexec(const char* target_qube, const char* service_name, const char* role, int timeout_ms);
```

Wraps `nsigner_transport_open_qrexec` + `nostr_signer_nsigner_from_transport`. No auth envelope needed — qrexec identity comes from `QREXEC_REMOTE_DOMAIN` on the server side.

### 2.4 Surface `index_not_allowed` error distinctly

Add to `nostr_common.h`:

```c
#define NOSTR_ERROR_NSIGNER_INDEX_NOT_ALLOWED -2002
```

In `nsigner_client_call` (or `nostr_signer` remote backend), when the response error code is 2002, map it to `NOSTR_ERROR_NSIGNER_INDEX_NOT_ALLOWED` so callers can distinguish it from generic policy denial.

---

## 3. Implementation checklist

In `nostr_core_lib`:

- [ ] `nsigner_transport.h` / `nsigner_transport.c`: add `nsigner_transport_open_qrexec(target_qube, service_name, timeout_ms)` — fork+exec `qrexec-client-vm`, pipe-based framed I/O, reconnect re-spawns.
- [ ] `nostr_signer.h` / `nostr_signer.c`: add `nostr_signer_nsigner_qrexec(target_qube, service_name, role, timeout_ms)`.
- [ ] `nostr_signer.c`: add `nostr_signer_nsigner_set_nostr_index(signer, nostr_index)`; update `signer_remote_params_with_selector` to emit `{"nostr_index":N}` when index is set.
- [ ] `nostr_common.h`: add `NOSTR_ERROR_NSIGNER_INDEX_NOT_ALLOWED` error code.
- [ ] `nsigner_client.c`: map error code 2002 to the new constant.
- [ ] `NSIGNER_INTEGRATION.md`: document the qrexec transport, nostr_index selector, and index_not_allowed error.

In `n_signer` (this repo):

- [ ] Update [`examples/get_pubkey_tcp.c`](../examples/get_pubkey_tcp.c) to optionally use the high-level `nostr_signer` API with `nostr_index` selector (demonstrates the new client API).
- [ ] Add a C example for the qrexec path using the new `nostr_signer_nsigner_qrexec` constructor.

---

## 4. Open questions

1. **Should the qrexec transport live in `nostr_core_lib` or in a Qubes-specific addon?** The transport is only useful on Qubes OS, but `nostr_core_lib` is meant to be cross-platform. **Decision: put it in `nostr_core_lib` behind `NOSTR_ENABLE_NSIGNER_CLIENT` — it just won't work on non-Qubes hosts (execvp fails). No new feature flag needed.**

2. **Should `nostr_signer_nsigner_set_nostr_index` override `role` or coexist?** n_signer rejects ambiguous selectors (both `role` and `nostr_index` set = `ambiguous_role_selector`). **Decision: setting nostr_index clears role, and vice versa. The setter is exclusive.**
