This document describes the workings of the index.js test client, which provides a CLI for interacting with the Nostr API layer (see doc/apiLayer.md). It covers flows for identity selection, profile updates, posting, viewing posts, publishing encrypted actions, real-time streaming, and error handling.
- Node.js (v14+)
- Dependencies:
- axios
- readline
- eventsource
- nostr-tools
- Install dependencies:
npm install axios readline eventsource nostr-toolsAPI_URL(optional): Base URL for the API server (default:http://localhost:3000)POW_BITS(optional): Default proof-of-work difficulty (default:20)TIMEOUT_MS(optional): Default timeout for publishing events in milliseconds (default:10000)IGNORE_OLD(optional): Milliseconds threshold for ignoring old messages (default:no limit)
node index.jsUpon execution, the client performs:
- Identity Selection
- Main Menu Loop
- Retrieves existing keypairs via
getAllKeys(). - If keys exist:
- Lists keys and prompts:
- Select by number (
1,2, …) - Or type
nto create a new user.
- Select by number (
- Lists keys and prompts:
- If no keys found:
- Prompts for a new user name and calls
generateKeyPair(name).
- Prompts for a new user name and calls
- Returns a
sessionKeyobject:{ name, // user-provided name privkey, // hex private key pubkey, // hex public key nsec, // nsec format npub // npub format }
Displays:
Hello <name>, what would you like to do?
a) Update profile
b) Create a post
c) View last 10 posts
d) Publish encrypted action
f) Sign event remotely
5) Subscribe for Data Input
e) Exit
Prompts: Enter a, b, c, d, f, 5 or e:
- Prompts for:
NameAboutPicture URL(optional)
- Calls
POST /profile/updatewith:{ npub, name, about, picture? } - Logs:
Updated profile: <response data> - Fetches latest posts:
GET /post/view10?npub=<npub> - Prints the latest 10 events.
- Prompts:
Post content - Connects to Nostr via
connect(sessionKey) - Determines:
powBits(fromprocess.env.POW_BITSor default20)timeoutMs(fromprocess.env.TIMEOUT_MSor default10000)
- Calls
POST /post/notewith:{ npub, content, powBits, timeoutMs } - Logs:
Created note: <response data> - Retrieves and prints latest 10 events again:
GET /post/view10?npub=<npub>
- Prompts:
Kind filter (default 1) - Parses integer
kindor defaults to1. - Calls:
GET /post/view10?kind=<kind>&npub=<npub> - Prints each event:
[<created_at>] <content> (id: <id>) - Action Trigger
If an event haskind === 30078:const payload = JSON.parse(event.content); axios.post('/action/take', payload);
- Prompts:
Call NPub (target):Response NPub (default <sessionKey.npub>):Enter JSON payload or leave blank for default
- Default payload:
{ "cmd": "pay", "target": "<callNpub>", "amount": "21000" } - Parses input JSON, aborts on invalid JSON.
- Calls:
with body:
POST /action/encrypted{ "senderNpub": "<sessionKey.npub>", "callNpub": "<callNpub>", "responseNpub": "<responseNpub>", "payload": { ... }, "powBits": <powBits>, "timeoutMs": <timeoutMs> } - Logs:
Encrypted action published: <response data>
- Prompts:
Call NPub (target, default npub1z54lfwx2v7vek7z79mkurm8nyrgjpmeanngx9m2fnc7qf53kv3sqjw8ex5):Response NPub (default <sessionKey.npub>):Signer NPub (default npub1py2a9kmpqjj45wapuw4gpwjjkt83ymr05grjh0xuwkgdtyrjzxdq8lpcdp):Enter note content
Client 2 signs the note using the private key corresponding to the specified signerNpub
- Calls:
with body:
POST /post/note_remote{ "senderNpub": "<sessionKey.npub>", "callNpub": "<callNpub>", "responseNpub": "<responseNpub>", "signerNpub": "<signerNpub>", "noteContent": "<noteContent>" } - Logs:
Remote sign request sent: <response data>
- Retrieves all
npubkeys. - Calls
POST /stream/startwithnpubsarray (backend filters and decrypts for these npubs). - Receives
sessionId. - Opens SSE:
GET /stream/events/<sessionId> - On each
message:- Parses the JSON message.
- If
msg.type === 'decryptedAction':🆕 Decrypted payload from <senderNpub>: <payload> (Respond to: <responseNpub>) - Else logs raw message:
🆕 Raw message: <msg>
- Waits for keypress:
qorQ: stop and return to menu.Ctrl+C/Ctrl+D: stop and exit.
- Cleanup:
- Close SSE connection
DELETE /stream/stop/<sessionId>- Restore terminal state
- Calls
process.exit(0)
- API calls wrapped in
try/catch. - On error:
- If
err.response: logserr.response.data - Else logs
err.message
- If
- Stream errors:
EventSource.onerror- Input errors on
stdin - Cleanup failures log and exit.
prompt(question): wrapsreadlinefor async user input.chooseKey(): user/key selection logic.tailEvents(): SSE-based event subscription and keypress handling.