# Migrate `client/` onto `nostr_core_lib`'s nsigner client

> Status: Planning. This plan supersedes the hand-rolled `client/nsigner_client.{c,h}` with the shared, transport-pluggable client now living in `nostr_core_lib` (commit `e842de3`).

## 1. Goal

Replace n_signer's own [`client/nsigner_client.{c,h}`](../client/nsigner_client.h) (stack-allocated, unix-only, raw-JSON-string responses, depends on `src/auth_envelope.c`) with the shared client stack now in [`resources/nostr_core_lib`](../resources/nostr_core_lib):

- [`nostr_core/nsigner_transport.{h,c}`](../resources/nostr_core_lib/nostr_core/nsigner_transport.h) — pluggable transport (unix / tcp / serial / fds) + discovery
- [`nostr_core/nsigner_client.{h,c}`](../resources/nostr_core_lib/nostr_core/nsigner_client.h) — heap-allocated client, framed I/O, kind-27235 auth envelope (self-contained), cJSON result parsing, RPC error mapping
- [`nostr_core/nostr_signer.{h,c}`](../resources/nostr_core_lib/nostr_core/nostr_signer.h) — high-level `nostr_signer_t` abstraction (local + remote backends, 6 verbs)

After migration there is **one** wire-contract implementation (in `nostr_core_lib`), and n_signer's `client/` directory either shrinks to a thin compatibility shim or is removed entirely.

## 2. Current state — what exists today

### 2.1 Old client (`client/`)

[`client/nsigner_client.h`](../client/nsigner_client.h) exposes a **stack-allocated** struct and these verbs:

```c
typedef struct { int fd; int timeout_ms; unsigned char auth_privkey[32]; int auth_enabled; char auth_label[64]; char last_error[128]; } nsigner_client_t;
void nsigner_client_init(nsigner_client_t *client);
void nsigner_client_close(nsigner_client_t *client);
int  nsigner_client_connect_unix(nsigner_client_t *client, const char *socket_name, int timeout_ms);
int  nsigner_client_set_auth(nsigner_client_t *client, const unsigned char privkey[32], const char *label);
const char *nsigner_client_last_error(const nsigner_client_t *client);
int  nsigner_client_request_raw(nsigner_client_t *client, const char *request_json, char **out_response_json);
int  nsigner_client_request(nsigner_client_t *client, const char *id, const char *method, const cJSON *params, char **out_response_json);
int  nsigner_client_get_public_key(nsigner_client_t *client, const char *id, const char *selector, char **out_response_json);
int  nsigner_client_sign_event(nsigner_client_t *client, const char *id, const char *event_json, const char *role, char **out_response_json);
```

Characteristics:
- Unix-abstract only; no TCP/serial/fds.
- Returns **raw JSON response strings** (caller parses).
- Auth envelope built via n_signer's own [`src/auth_envelope.c`](../src/auth_envelope.c) (`auth_envelope_build_for_request`).
- Framing code is duplicated (same 4-byte BE length prefix as the new lib client).

### 2.2 Consumers of the old client

| Consumer | File | What it uses |
|---|---|---|
| Example: get_public_key | [`examples/get_public_key_client.c`](../examples/get_public_key_client.c) | `init` / `connect_unix` / `get_public_key` / `close` |
| Example: sign_event | [`examples/sign_event_client.c`](../examples/sign_event_client.c) | `init` / `connect_unix` / `sign_event` / `close` |
| Integration test | [`tests/test_integration.c`](../tests/test_integration.c) | `init` / `connect_unix` / `get_public_key` / `sign_event` / `close` (embeds headerless decls, lines 3–451) |
| Build | [`Makefile`](../Makefile) lines 141–163 | Compiles `client/nsigner_client.c` + `src/auth_envelope.c` into 3 targets |

### 2.3 NOT a consumer

- The **`nsigner ... client '<json>'`** subcommand in [`src/main.c:906`](../src/main.c:906) (`client_main`) does **not** use `client/nsigner_client.c`. It has its own `connect_abstract_socket` + `transport_send_framed/recv_framed` (from `src/transport_frame.c`). This is a raw pass-through and is unaffected by this migration.

### 2.4 New client in `nostr_core_lib` (already linked)

The static lib `resources/nostr_core_lib/libnostr_core_x64.a` already exports the new symbols (`nsigner_client_new`, `nsigner_transport_open_unix`, `nostr_signer_nsigner_unix`, etc.) and was built with `-DNOSTR_ENABLE_NSIGNER_CLIENT=1`. n_signer's [`Makefile`](../Makefile) already links this `.a`. So the new client is **available for use right now** — no lib rebuild needed for the migration.

## 3. API mapping (old → new)

| Old (`client/`) | New (`nostr_core_lib`) | Notes |
|---|---|---|
| `nsigner_client_t` (stack) | `nsigner_client_t*` (heap) or `nostr_signer_t*` (heap) | New is opaque + heap-allocated |
| `nsigner_client_init(&c)` | `nsigner_client_new(transport)` or `nostr_signer_nsigner_unix(name, role, timeout_ms)` | New takes a transport or factory args |
| `nsigner_client_connect_unix(&c, name, ms)` | `nsigner_transport_open_unix(name, ms)` then `nsigner_client_new(transport)` | Or use `nostr_signer_nsigner_unix` one-shot |
| `nsigner_client_close(&c)` | `nsigner_client_free(client)` or `nostr_signer_free(signer)` | New frees the transport too |
| `nsigner_client_set_auth(&c, key, label)` | `nsigner_client_set_auth(client, key, label)` or `nostr_signer_nsigner_set_auth(signer, key, label)` | Same semantics |
| `nsigner_client_request_raw(&c, json, &resp)` | `transport->send_framed` + `transport->recv_framed` (low-level) | No direct equivalent at client level; use `nsigner_client_call` instead |
| `nsigner_client_request(&c, id, method, params, &resp_json)` | `nsigner_client_call(client, method, params, &out_result_cJSON)` | New returns parsed cJSON `result`, auto-generates id, handles error mapping. No raw JSON string out. |
| `nsigner_client_get_public_key(&c, id, selector, &resp)` | `nostr_signer_get_public_key(signer, out_hex)` **or** `nsigner_client_call(client, "get_public_key", params, &result)` | High-level returns hex string directly |
| `nsigner_client_sign_event(&c, id, event_json, role, &resp)` | `nostr_signer_sign_event(signer, unsigned_event_cJSON, &signed_out)` **or** `nsigner_client_call(client, "sign_event", params, &result)` | High-level takes cJSON event, returns cJSON signed event |
| `nsigner_client_last_error(&c)` | `nsigner_client_last_error(client)` | Same; buffer is 256 vs 128 |

### Key behavioral differences
1. **Heap vs stack**: new client is `malloc`'d; must pair `*_new` with `*_free`.
2. **cJSON results vs raw strings**: `nsigner_client_call` returns a parsed cJSON `result` object (caller `cJSON_Delete`s it). The old `nsigner_client_request` returned a raw JSON string. Callers that did `strstr(resp, "\"id\":\"1\"")` must switch to cJSON parsing.
3. **Auto-generated ids**: new client generates its own numeric ids; old client took caller-supplied ids. The id is still echoed in responses but callers can't control it.
4. **Auth envelope self-contained**: new client builds the kind-27235 envelope using `nostr_create_and_sign_event` from the lib (not n_signer's `src/auth_envelope.c`). This is the single-source-of-truth win.
5. **Transport pluggability**: new client works over unix/tcp/serial/fds; old was unix-only.

## 4. Design decision: two migration levels

### Level A — Thin shim (minimal disruption)
Keep `client/nsigner_client.h` API **byte-for-byte**, reimplement `client/nsigner_client.c` as a wrapper over `nsigner_transport_open_unix` + `nsigner_client_new` + `nsigner_client_call`. Internally convert cJSON results back to raw JSON strings for the `*_request` / `*_get_public_key` / `*_sign_event` verbs. Consumers (examples, tests, Makefile) stay unchanged.

- **Pro**: zero changes to examples/tests/Makefile; lowest risk.
- **Con**: keeps a redundant wrapper layer; doesn't demonstrate the new high-level `nostr_signer_t` API; raw-string conversion is wasteful.

### Level B — Full migration (recommended)
Delete `client/nsigner_client.{c,h}`. Rewrite the 3 consumers to use the new API directly:
- Examples → use `nostr_signer_nsigner_unix` + `nostr_signer_get_public_key` / `nostr_signer_sign_event` (cleanest, shows the abstraction).
- `test_integration.c` → use `nsigner_transport_open_unix` + `nsigner_client_new` + `nsigner_client_call` (needs cJSON result parsing instead of `strstr`).
- `Makefile` → drop `client/nsigner_client.c` and `src/auth_envelope.c` from the 3 targets; they now get the client from the static lib.
- Update `client/README.md` to point at `nostr_core_lib`'s client, or remove the dir.

- **Pro**: single source of truth; removes ~370 lines of duplicated framing/auth; unlocks tcp/serial/fds for n_signer's own examples; demonstrates the intended API.
- **Con**: touches 4 files + Makefile; `test_integration.c` needs cJSON-based assertions.

**Recommendation: Level B**, because the whole point of the `nostr_core_lib` integration (per [`plans/nsigner_integration_plan.md`](../resources/nostr_core_lib/plans/nsigner_integration_plan.md) Phase 7) is to retire the per-project hand-rolled clients. Level A just defers the cleanup.

## 5. Step-by-step plan (Level B)

### Step 1 — Rewrite `examples/get_public_key_client.c`
Switch from `client/nsigner_client.h` to `nostr_core/nostr_signer.h` + `nostr_core/nsigner_transport.h`. Use the high-level `nostr_signer_t`:
```c
#include "nostr_signer.h"
nostr_signer_t *signer = nostr_signer_nsigner_unix(socket_name, NULL, 5000);
char pubkey_hex[65];
nostr_signer_get_public_key(signer, pubkey_hex);
printf("%s\n", pubkey_hex);  /* or build a JSON response string */
nostr_signer_free(signer);
```
Decision needed: keep the example printing a raw JSON-RPC response string (for backward-compatible CLI output) or print just the pubkey? The old example printed the full `{"id":...,"result":"..."}`. To preserve CLI compatibility, either (a) build the JSON string manually, or (b) use `nsigner_client_call` and re-serialize the cJSON result. **Suggest (b)** so the example shows the low-level client too.

### Step 2 — Rewrite `examples/sign_event_client.c`
Same pattern. Use `nostr_signer_sign_event(signer, unsigned_event_cJSON, &signed_out)` and print `cJSON_PrintUnformatted(signed_out)`. Or use `nsigner_client_call` with `method="sign_event"` and params `[event_json_str, {"role":"main"}]`, then print the cJSON result.

### Step 3 — Rewrite `tests/test_integration.c` client section (lines ~711–745)
Replace the `nsigner_client_t` stack usage with:
```c
nsigner_transport_t *t = nsigner_transport_open_unix(SOCKET_NAME_A, 5000);
nsigner_client_t *c = nsigner_client_new(t);
cJSON *result = NULL;
if (nsigner_client_call(c, "get_public_key", params, &result) == NOSTR_SUCCESS) { ... }
nsigner_client_free(c);
```
Update the `strstr(resp, "\"id\":\"1\"")` assertions to cJSON-based checks on `result`. Remove the headerless decls for the old client (lines 3–451 embed many n_signer internal headers — only the client decls need removal; the server-side decls stay because the test still spawns the server).

### Step 4 — Update `Makefile`
- Remove `$(CLIENT_DIR)/nsigner_client.c` and `$(SRC_DIR)/auth_envelope.c` from the 3 targets (`test_integration`, `example_get_public_key_client`, `example_sign_event_client`).
- Remove `-I$(CLIENT_DIR)` from those targets' compile lines (the new headers come from `-Iresources/nostr_core_lib/nostr_core` already in `CFLAGS`).
- The new client symbols resolve from `resources/nostr_core_lib/libnostr_core_x64.a` (already in `LDFLAGS`).
- `auth_envelope.c` stays in the main `nsigner` binary build (server side still uses it for verification) — just not in the client targets.

### Step 5 — Delete `client/nsigner_client.{c,h}` and update `client/README.md`
Either remove the files or leave `client/README.md` as a pointer to `resources/nostr_core_lib/nostr_core/nsigner_client.h`. The `client/` dir can be removed entirely if nothing else references it.

### Step 6 — Verify build + run
- `make clean && make dev` — confirm the main binary still builds.
- `make examples` — confirm the 2 example binaries build against the new API.
- `make test-integration` — confirm the integration test builds and passes (needs a running n_signer).
- Run the examples against a live n_signer (`@nsigner-test`) to confirm end-to-end.

### Step 7 — Wire-contract parity check (optional but recommended)
Compare `nsigner_client_compute_body_hash_hex` in the new client against n_signer's `src/auth_envelope.c` body-hash logic to confirm they agree on JCS canonicalization and `SHA-256("null")` for null params. This is risk item #1 from the integration plan. If they diverge, TCP-mode auth will fail.

## 6. Files touched

| File | Change |
|---|---|
| [`examples/get_public_key_client.c`](../examples/get_public_key_client.c) | Rewrite to use new client/signer API |
| [`examples/sign_event_client.c`](../examples/sign_event_client.c) | Rewrite to use new client/signer API |
| [`tests/test_integration.c`](../tests/test_integration.c) | Replace old client usage (lines ~711–745) with new client; update assertions; remove old client headerless decls |
| [`Makefile`](../Makefile) | Drop `client/nsigner_client.c` + `src/auth_envelope.c` from 3 targets; drop `-I$(CLIENT_DIR)` |
| [`client/nsigner_client.c`](../client/nsigner_client.c) | Delete |
| [`client/nsigner_client.h`](../client/nsigner_client.h) | Delete |
| [`client/README.md`](../client/README.md) | Rewrite as pointer to `nostr_core_lib` client, or delete |

## 7. Risks / open items

1. **CLI output format change**: old examples printed full JSON-RPC response strings. If any script/CI parses that exact format, switching to the new API may change the output shape (e.g., no `"id"` field if using `nostr_signer_get_public_key` directly). Mitigation: use `nsigner_client_call` and re-serialize, or document the change.
2. **test_integration.c headerless decls**: the test embeds ~450 lines of n_signer internal declarations (lines 3–451) to compile as a single TU. Only the client-related decls need removal; the server/dispatcher/policy decls must stay. Careful editing required.
3. **Auth envelope divergence**: if n_signer's `src/auth_envelope.c` and the new `nsigner_client.c` compute body hashes differently, TCP auth breaks. Needs a parity test (Step 7).
4. **`NOSTR_ENABLE_NSIGNER_CLIENT` already set**: the static lib is built with it ON, so the new client symbols are present. No build-flag changes needed in n_signer. But if someone rebuilds the lib without the flag, the client targets will fail to link. Worth a Makefile guard.
5. **`nostr_signer_nsigner_unix` auto-discovery**: if `socket_name` is NULL/empty, the new factory auto-discovers a single `@nsigner*` socket. The old client required an explicit name. Examples that default to `"nsigner"` should pass it explicitly to preserve behavior.

## 8. Mermaid — migration flow

```mermaid
flowchart LR
    subgraph BEFORE[Before]
        OLD[client/nsigner_client.c]
        AE[src/auth_envelope.c]
        EX1[examples/get_public_key_client.c]
        EX2[examples/sign_event_client.c]
        TI[tests/test_integration.c]
        OLD --> EX1
        OLD --> EX2
        OLD --> TI
        AE --> OLD
    end
    subgraph LIB[nostr_core_lib static lib]
        NT[nsigner_transport.c]
        NC[nsigner_client.c]
        NS[nostr_signer.c]
    end
    subgraph AFTER[After]
        EX1N[examples/get_public_key_client.c]
        EX2N[examples/sign_event_client.c]
        TIN[tests/test_integration.c]
        EX1N --> NS
        EX2N --> NS
        TIN --> NC
        NS --> NC
        NC --> NT
    end
    BEFORE -.replaced by.-> AFTER
    LIB -.linked via.-> AFTER
```
