@glyphteck/veyl 0.2.0 → 0.3.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/docs/api.md ADDED
@@ -0,0 +1,647 @@
1
+ # Veyl Headless API
2
+
3
+ This is the JavaScript API reference for `@glyphteck/veyl`. The CLI and MCP server are thin adapters over this API.
4
+
5
+ ## Model
6
+
7
+ Headless is a client. It uses the same client-owned cryptographic model as web and iOS:
8
+
9
+ - machine or passkey authentication signs into Firebase;
10
+ - vault secrets stay local;
11
+ - wallet and chat seeds are derived locally;
12
+ - Spark wallet boot and signing happen locally;
13
+ - messages are encrypted locally;
14
+ - Veyl backend services store public profile metadata, opaque encrypted chat records, and encrypted vault data.
15
+
16
+ Machine-created accounts are publicly marked as `bot`. Passkey-created CLI accounts are not marked as bots.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ bunx @glyphteck/veyl help
22
+ bun add @glyphteck/veyl
23
+ ```
24
+
25
+ ## Exports
26
+
27
+ ```js
28
+ import {
29
+ API_VERSION,
30
+ COMMANDS,
31
+ ClientNotReadyError,
32
+ VeylHeadlessClient,
33
+ createClient,
34
+ open,
35
+ } from '@glyphteck/veyl';
36
+ ```
37
+
38
+ - `open(options)`: returns a `VeylHeadlessClient`.
39
+ - `createClient(options)`: synchronous constructor helper.
40
+ - `COMMANDS`: command inventory generated from the canonical adapter descriptors in `src/commands.js`.
41
+ - `API_VERSION`: numeric API version.
42
+
43
+ ## Client Options
44
+
45
+ ```js
46
+ const veyl = await open({
47
+ profile: 'runner',
48
+ homeDir: '/secure/veyl-home',
49
+ network: 'REGTEST',
50
+ webUrl: 'https://veyl.glyphteck.com',
51
+ functionBaseUrl: 'https://us-central1-glyphteck.cloudfunctions.net/headless',
52
+ firebaseConfig,
53
+ region: 'us-central1',
54
+ appName: 'my-agent',
55
+ });
56
+ ```
57
+
58
+ - `profile`: local profile name. Defaults to `VEYL_PROFILE` or the active profile.
59
+ - `homeDir`: local profile directory. Defaults to `VEYL_HOME` or `~/.veyl`.
60
+ - `network`: `REGTEST` or `MAINNET`. Defaults to `VEYL_NETWORK`, `NETWORK`, or `REGTEST`.
61
+ - `webUrl`: passkey web origin. Defaults to `VEYL_WEB_URL` or production Veyl.
62
+ - `functionBaseUrl`: override for the headless account HTTP function.
63
+ - `firebaseConfig`, `region`, `appName`: Firebase app configuration overrides.
64
+ - `store`, `profileData`, `session`, `cloudContext`: advanced injection points for tests or custom runtimes.
65
+
66
+ ## Account API
67
+
68
+ ### `veyl.account.create(options)`
69
+
70
+ Creates a machine-operated account.
71
+
72
+ ```js
73
+ const account = await veyl.account.create({
74
+ username: 'runner',
75
+ network: 'REGTEST',
76
+ saveCredential: true,
77
+ });
78
+ ```
79
+
80
+ Returns an account summary plus `publicKeyPem` and `privateKeyPem`. If `saveCredential` is not `false`, the private key is stored in the local profile.
81
+
82
+ ### `veyl.account.login(options)`
83
+
84
+ Logs in with the saved or provided machine credential.
85
+
86
+ ```js
87
+ await veyl.account.login();
88
+ await veyl.account.login({ username: 'runner' });
89
+ await veyl.account.login({ credentialId, privateKeyPem });
90
+ ```
91
+
92
+ ### `veyl.account.createPasskey(options)`
93
+
94
+ Creates a real passkey account from the terminal.
95
+
96
+ ```js
97
+ await veyl.account.createPasskey({
98
+ username: 'zak',
99
+ webUrl: 'https://veyl.glyphteck.com',
100
+ saveCredential: true,
101
+ });
102
+ ```
103
+
104
+ The browser completes WebAuthn. The CLI receives a Firebase token through a localhost callback and adds a local machine credential so future CLI sessions can run without another browser prompt.
105
+
106
+ ### `veyl.account.loginPasskey(options)`
107
+
108
+ Logs into an existing passkey account through the browser-assisted flow.
109
+
110
+ ```js
111
+ await veyl.account.loginPasskey({
112
+ username: 'zak',
113
+ webUrl: 'https://veyl.glyphteck.com',
114
+ });
115
+ ```
116
+
117
+ ### `veyl.account.me()`
118
+
119
+ Reads the current local profile without logging in.
120
+
121
+ ```js
122
+ const account = await veyl.account.me();
123
+ ```
124
+
125
+ ### Account Summary
126
+
127
+ ```js
128
+ {
129
+ uid,
130
+ username,
131
+ profile,
132
+ credentialId,
133
+ network,
134
+ hasVault,
135
+ hasVaultSecret,
136
+ walletPK,
137
+ chatPK,
138
+ auth, // "machine" or "passkey"
139
+ bot, // "bot" for machine-created accounts, otherwise null
140
+ signedIn,
141
+ unlocked,
142
+ }
143
+ ```
144
+
145
+ ## Vault API
146
+
147
+ ### `veyl.vault.create(options)`
148
+
149
+ Creates wallet/chat key material locally, encrypts it with a vault secret, uploads the encrypted vault, and stores public wallet/chat keys on the profile.
150
+
151
+ ```js
152
+ const vault = await veyl.vault.create({
153
+ secret: process.env.VEYL_VAULT_SECRET,
154
+ saveSecret: true,
155
+ });
156
+ ```
157
+
158
+ If `secret` is omitted, the package uses `VEYL_VAULT_SECRET` or generates a random secret. If `saveSecret` is not `false`, the secret is saved in the local profile.
159
+
160
+ ### `veyl.vault.unlock(options)`
161
+
162
+ Decrypts the vault locally and boots the Spark wallet and chat session.
163
+
164
+ ```js
165
+ await veyl.vault.unlock();
166
+ await veyl.vault.unlock({ secret: process.env.VEYL_VAULT_SECRET });
167
+ ```
168
+
169
+ Most chat and money methods auto-unlock through the saved vault secret when possible.
170
+
171
+ ### `veyl.vault.lock()`
172
+
173
+ Clears the in-process session and closes wallet connections.
174
+
175
+ ```js
176
+ await veyl.vault.lock();
177
+ ```
178
+
179
+ ## Chat API
180
+
181
+ Peers can be usernames such as `@alice`, chat public keys, wallet public keys, or compatible stream event targets.
182
+
183
+ ### `veyl.chat.list(options)`
184
+
185
+ Lists recent chats.
186
+
187
+ ```js
188
+ const chats = await veyl.chat.list({ count: 20 });
189
+ ```
190
+
191
+ Chat summaries contain `id`, `linkId`, `peerChatPK`, `peerUid`, `username`, `unseen`, `ts`, and `preview`.
192
+
193
+ ### `veyl.chat.read(peer, options)`
194
+
195
+ Reads display messages from a chat.
196
+
197
+ ```js
198
+ const page = await veyl.chat.read('@alice', {
199
+ count: 30,
200
+ markRead: true,
201
+ raw: false,
202
+ });
203
+ ```
204
+
205
+ Returns:
206
+
207
+ ```js
208
+ {
209
+ chat,
210
+ messages,
211
+ hasMore,
212
+ nextOlderThan,
213
+ }
214
+ ```
215
+
216
+ Text messages:
217
+
218
+ ```js
219
+ {
220
+ id,
221
+ cid,
222
+ chatId,
223
+ from, // "peer" or "self"
224
+ peerChatPK,
225
+ type: 'txt',
226
+ text,
227
+ replyId,
228
+ ts,
229
+ }
230
+ ```
231
+
232
+ Payment requests:
233
+
234
+ ```js
235
+ {
236
+ type: 'req',
237
+ amountSats,
238
+ paid,
239
+ txId,
240
+ requestId,
241
+ replyId,
242
+ }
243
+ ```
244
+
245
+ Other message types return `{ type, payload }`. Use `raw: true` when a caller needs source payload detail.
246
+
247
+ ### `veyl.chat.send(peer, message, options)`
248
+
249
+ Sends a text message.
250
+
251
+ ```js
252
+ await veyl.chat.send('@alice', 'hello');
253
+ ```
254
+
255
+ ### `veyl.chat.reply(target, message, options)`
256
+
257
+ Replies to a message. The target can be `{ peer, messageId }` or a `message` event from `listen`.
258
+
259
+ ```js
260
+ await veyl.chat.reply({ peer: '@alice', messageId: 'abc123' }, 'got it');
261
+ await veyl.chat.reply(event, 'thanks');
262
+ ```
263
+
264
+ If no message id is present, this falls back to a normal text send to the resolved peer.
265
+
266
+ ### `veyl.chat.react(peer, messageId, emoji, options)`
267
+
268
+ Adds or changes your reaction to a displayed message.
269
+
270
+ ```js
271
+ await veyl.chat.react('@alice', 'abc123', '+1');
272
+ ```
273
+
274
+ ### `veyl.chat.reactTo(target, emoji, options)`
275
+
276
+ Reacts to `{ peer, messageId }` or a stream event.
277
+
278
+ ```js
279
+ await veyl.chat.reactTo(event, '+1');
280
+ ```
281
+
282
+ ### `veyl.chat.unreact(peer, messageId, options)`
283
+
284
+ Removes your reaction from a message.
285
+
286
+ ```js
287
+ await veyl.chat.unreact('@alice', 'abc123');
288
+ ```
289
+
290
+ ### `veyl.chat.save(peer, messageId, options)`
291
+
292
+ Keeps a displayed message permanently.
293
+
294
+ ```js
295
+ await veyl.chat.save('@alice', 'abc123');
296
+ ```
297
+
298
+ ### `veyl.chat.unsave(peer, messageId, options)`
299
+
300
+ Returns a saved message to normal temporary retention.
301
+
302
+ ```js
303
+ await veyl.chat.unsave('@alice', 'abc123');
304
+ ```
305
+
306
+ ### `veyl.chat.delete(peer, messageId, options)`
307
+
308
+ Deletes one displayed message.
309
+
310
+ ```js
311
+ await veyl.chat.delete('@alice', 'abc123');
312
+ ```
313
+
314
+ ### `veyl.chat.deleteChat(peer, options)`
315
+
316
+ Deletes one chat locally and optionally requests cleanup.
317
+
318
+ ```js
319
+ await veyl.chat.deleteChat('@alice', { cleanup: true });
320
+ ```
321
+
322
+ ### `veyl.chat.retention(peer, value, options)`
323
+
324
+ Sets chat retention. Valid values are `seen` and `24h`.
325
+
326
+ ```js
327
+ await veyl.chat.retention('@alice', '24h');
328
+ ```
329
+
330
+ ## Money API
331
+
332
+ Money methods require an unlocked vault. Amounts are satoshis.
333
+
334
+ ### `veyl.money.balance(options)`
335
+
336
+ Reads the wallet balance.
337
+
338
+ ```js
339
+ const balance = await veyl.money.balance();
340
+ ```
341
+
342
+ Returns `{ availableSats }` plus `raw` when `raw: true`.
343
+
344
+ ### `veyl.money.receive(options)` / `veyl.money.address(options)`
345
+
346
+ Returns the static funding address for the active network.
347
+
348
+ ```js
349
+ const { address, network } = await veyl.money.receive();
350
+ ```
351
+
352
+ ### `veyl.money.claim(options)`
353
+
354
+ Claims static deposits and pending incoming transfers.
355
+
356
+ ```js
357
+ await veyl.money.claim({ count: 100 });
358
+ ```
359
+
360
+ ### `veyl.money.send(peer, sats, options)`
361
+
362
+ Sends sats to a Veyl peer.
363
+
364
+ ```js
365
+ await veyl.money.send('@alice', 1000);
366
+ ```
367
+
368
+ ### `veyl.money.request(peer, sats, options)`
369
+
370
+ Sends a chat payment request.
371
+
372
+ ```js
373
+ const request = await veyl.money.request('@alice', 1000);
374
+ ```
375
+
376
+ The request message contains `requestId`, which the payer can use with `money.pay`.
377
+
378
+ ### `veyl.money.pay(requestId, options)`
379
+
380
+ Pays a chat payment request.
381
+
382
+ ```js
383
+ await veyl.money.pay(request.message.requestId);
384
+ ```
385
+
386
+ ### `veyl.money.transactions(options)`
387
+
388
+ Lists visible wallet transfers.
389
+
390
+ ```js
391
+ const txs = await veyl.money.transactions({ count: 50, offset: 0 });
392
+ ```
393
+
394
+ Returns `{ transfers }`; each transfer contains `id`, `type`, `status`, `direction`, `amountSats`, `createdAt`, `updatedAt`, sender and receiver keys.
395
+
396
+ ### `veyl.money.invoice(sats, options)`
397
+
398
+ Creates a Lightning invoice.
399
+
400
+ ```js
401
+ const invoice = await veyl.money.invoice(1000, {
402
+ memo: 'coffee',
403
+ expirySeconds: 3600,
404
+ includeSparkAddress: false,
405
+ includeSparkInvoice: false,
406
+ });
407
+ ```
408
+
409
+ ### `veyl.money.payInvoice(invoice, options)`
410
+
411
+ Pays a Lightning or Spark invoice. QR payloads are accepted.
412
+
413
+ ```js
414
+ await veyl.money.payInvoice(encodedInvoice, {
415
+ type: 'lightning',
416
+ amountSats: 1000,
417
+ maxFeeSats: 20,
418
+ preferSpark: true,
419
+ });
420
+ ```
421
+
422
+ ### `veyl.money.lightningQuote(invoice, options)`
423
+
424
+ Quotes Lightning send fees.
425
+
426
+ ```js
427
+ await veyl.money.lightningQuote(invoice, { amountSats: 1000 });
428
+ ```
429
+
430
+ ### `veyl.money.lightningPay(invoice, options)`
431
+
432
+ Pays a Lightning invoice directly.
433
+
434
+ ```js
435
+ await veyl.money.lightningPay(invoice, {
436
+ amountSats: 1000,
437
+ maxFeeSats: 20,
438
+ preferSpark: true,
439
+ });
440
+ ```
441
+
442
+ ### `veyl.money.lightningReceive(id, options)`
443
+
444
+ Reads a Lightning receive request.
445
+
446
+ ```js
447
+ await veyl.money.lightningReceive(id);
448
+ ```
449
+
450
+ ### `veyl.money.lightningSend(id, options)`
451
+
452
+ Reads a Lightning send request.
453
+
454
+ ```js
455
+ await veyl.money.lightningSend(id);
456
+ ```
457
+
458
+ ### `veyl.money.withdrawQuote(address, sats, options)`
459
+
460
+ Quotes an on-chain withdrawal.
461
+
462
+ ```js
463
+ await veyl.money.withdrawQuote(bitcoinAddress, 10000);
464
+ ```
465
+
466
+ ### `veyl.money.withdrawPrepare(address, sats, options)`
467
+
468
+ Prepares an on-chain withdrawal review.
469
+
470
+ ```js
471
+ await veyl.money.withdrawPrepare(bitcoinAddress, 10000, {
472
+ speed: 'MEDIUM',
473
+ deductFeeFromWithdrawalAmount: true,
474
+ });
475
+ ```
476
+
477
+ ### `veyl.money.withdraw(address, sats, options)`
478
+
479
+ Sends an on-chain withdrawal.
480
+
481
+ ```js
482
+ await veyl.money.withdraw(bitcoinAddress, 10000, {
483
+ speed: 'MEDIUM',
484
+ deductFeeFromWithdrawalAmount: true,
485
+ });
486
+ ```
487
+
488
+ ## Listen API
489
+
490
+ `veyl.listen(options)` keeps one auth and vault session alive, polls chats and transfers, and emits events through `onEvent`.
491
+
492
+ ```js
493
+ const controller = new AbortController();
494
+
495
+ await veyl.listen({
496
+ agent: true,
497
+ interval: 3000,
498
+ chatCount: 20,
499
+ messageCount: 10,
500
+ txCount: 50,
501
+ replay: false,
502
+ chats: true,
503
+ transactions: true,
504
+ signal: controller.signal,
505
+ onEvent: async (event) => {
506
+ if (event.type !== 'message') return;
507
+ await veyl.chat.reactTo(event, '+1');
508
+ await veyl.chat.reply(event, 'thanks');
509
+ },
510
+ });
511
+ ```
512
+
513
+ Options:
514
+
515
+ - `agent: true`: shorthand for `compact: true` and incoming-only messages.
516
+ - `compact: true`: emits smaller account/chat/message payloads.
517
+ - `incomingOnly: true`: emits only peer-authored messages.
518
+ - `incomingOnly: false`: emits self-authored and peer-authored messages.
519
+ - `interval` / `intervalMs`: poll interval, minimum 1000 ms, default 3000 ms.
520
+ - `chatCount`: number of chats to scan, default 20, max 100.
521
+ - `messageCount`: messages per chat to scan, default 10, max 100.
522
+ - `txCount`: transfers to scan, default 50, max 100.
523
+ - `replay: true`: emits current matching messages/transfers on startup.
524
+ - `chats: false`: disables message events.
525
+ - `transactions: false`: disables transfer events.
526
+ - `signal`: `AbortSignal` to stop listening.
527
+ - `onEvent`: event callback.
528
+
529
+ When `replay` is false, `ready` means the initial baseline scan has completed. Messages that arrive after `ready` should emit as new events.
530
+
531
+ Compact `ready` event:
532
+
533
+ ```js
534
+ {
535
+ type: 'ready',
536
+ ts,
537
+ account: { uid, username, network, auth, bot, unlocked },
538
+ }
539
+ ```
540
+
541
+ Compact `message` event:
542
+
543
+ ```js
544
+ {
545
+ type: 'message',
546
+ ts,
547
+ peer: '@alice',
548
+ replyTo: { peer: '@alice', messageId: 'abc123' },
549
+ reactTo: { peer: '@alice', messageId: 'abc123' },
550
+ chat: {
551
+ id,
552
+ peer,
553
+ username,
554
+ peerChatPK,
555
+ peerUid,
556
+ unseen,
557
+ ts,
558
+ },
559
+ message: {
560
+ id,
561
+ cid,
562
+ chatId,
563
+ from,
564
+ type,
565
+ ts,
566
+ peerChatPK,
567
+ text,
568
+ amountSats,
569
+ paid,
570
+ requestId,
571
+ replyId,
572
+ },
573
+ }
574
+ ```
575
+
576
+ Transaction event:
577
+
578
+ ```js
579
+ {
580
+ type: 'transaction',
581
+ ts,
582
+ transaction,
583
+ }
584
+ ```
585
+
586
+ Error event:
587
+
588
+ ```js
589
+ {
590
+ type: 'error',
591
+ ts,
592
+ peer,
593
+ message,
594
+ }
595
+ ```
596
+
597
+ Per-chat read errors are isolated, so one unreadable stale chat does not stop valid incoming messages from other chats.
598
+
599
+ ## Local Profiles and Secrets
600
+
601
+ Profiles live under `~/.veyl` by default:
602
+
603
+ ```text
604
+ ~/.veyl/
605
+ active.json
606
+ profiles/<profile>.json
607
+ ```
608
+
609
+ Profile files are written with `0600` permissions and directories with `0700`.
610
+
611
+ Stored profile fields can include Firebase uid, username, network, machine credential id, public/private machine keys, vault metadata, wallet/chat public keys, and optionally the vault secret. If the caller has a dedicated secret manager, use `--no-save-secret`, `--no-save-credential`, `saveSecret: false`, or `saveCredential: false`, then store returned secrets yourself.
612
+
613
+ Glyphteck-owned Google Secret Manager bot seeds are not used by this public path.
614
+
615
+ ## Environment
616
+
617
+ ```bash
618
+ VEYL_HOME=/secure/veyl-home
619
+ VEYL_PROFILE=runner
620
+ VEYL_NETWORK=REGTEST
621
+ VEYL_VAULT_SECRET=...
622
+ VEYL_WEB_URL=https://veyl.glyphteck.com
623
+ ```
624
+
625
+ `VEYL_WEB_URL` must be `https://...` or `localhost` for passkey flows.
626
+
627
+ ## Backend Contract
628
+
629
+ The package calls the deployed headless HTTP function:
630
+
631
+ ```text
632
+ POST /headless/account/create/start
633
+ POST /headless/account/create/finish
634
+ POST /headless/account/login/start
635
+ POST /headless/account/login/finish
636
+ POST /headless/account/credential/add/start
637
+ POST /headless/account/credential/add/finish
638
+ ```
639
+
640
+ The backend verifies signatures over short-lived challenges and returns Firebase custom tokens. It does not receive the vault secret, wallet seed, chat seed, or Spark wallet entropy.
641
+
642
+ ## Current Limits
643
+
644
+ - `listen` is polling-based. There is no backend wake-ping contract yet.
645
+ - Headless chat currently sends and replies with text. Media read summaries are exposed, but media upload helpers are not part of this public client API yet.
646
+ - Machine-created headless accounts are intentionally separate from passkey-created human accounts.
647
+ - Passkey account creation and login require a browser-capable environment.