@glyphteck/veyl 0.1.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/README.md +98 -0
- package/package.json +31 -0
- package/src/cli.js +280 -0
- package/src/client.js +499 -0
- package/src/cloud.js +52 -0
- package/src/index.js +11 -0
- package/src/kdf.js +12 -0
- package/src/listen.js +120 -0
- package/src/machine.js +38 -0
- package/src/mcp.js +96 -0
- package/src/passkey.js +182 -0
- package/src/storage.js +113 -0
package/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Veyl Headless
|
|
2
|
+
|
|
3
|
+
`apps/headless` is the programmable Bun client for terminal users, scripts, automations, and MCP servers.
|
|
4
|
+
|
|
5
|
+
It is a client, not a privileged backend. Headless accounts own their local machine credential, vault secret, wallet seed, and chat key material. The server should only create public account records, verify login challenges, and store encrypted vault material it cannot decrypt.
|
|
6
|
+
|
|
7
|
+
The package also supports real passkey accounts from a terminal. Passkey create/login opens the configured Veyl web origin for the WebAuthn ceremony, receives a Firebase token on a localhost callback, then pairs a local machine credential to that signed-in account for future CLI sessions. True machine-created headless accounts remain separate from passkey-created accounts.
|
|
8
|
+
|
|
9
|
+
## Interface Boundary
|
|
10
|
+
|
|
11
|
+
`headless` is the client boundary. CLI, MCP, scripts, and future channel adapters are interfaces on top of the same client, not separate clients with their own account, vault, chat, or money logic.
|
|
12
|
+
|
|
13
|
+
- `src/client.js`: shared programmable API.
|
|
14
|
+
- `src/cli.js`: terminal command wrapper.
|
|
15
|
+
- `src/mcp.js`: MCP wrapper.
|
|
16
|
+
- `src/passkey.js`: browser-assisted passkey bridge for terminal flows.
|
|
17
|
+
|
|
18
|
+
## API Shape
|
|
19
|
+
|
|
20
|
+
The public surface should stay simple enough to infer from the command names:
|
|
21
|
+
|
|
22
|
+
```js
|
|
23
|
+
import { open } from '@glyphteck/veyl';
|
|
24
|
+
|
|
25
|
+
const veyl = await open();
|
|
26
|
+
|
|
27
|
+
const account = await veyl.account.create({ username: 'runner' });
|
|
28
|
+
const vault = await veyl.vault.create();
|
|
29
|
+
await veyl.vault.unlock();
|
|
30
|
+
await veyl.chat.send('@alice', 'hello');
|
|
31
|
+
const messages = await veyl.chat.read('@alice');
|
|
32
|
+
|
|
33
|
+
await veyl.money.send('@alice', 1000);
|
|
34
|
+
await veyl.money.request('@alice', 1000);
|
|
35
|
+
const transactions = await veyl.money.transactions();
|
|
36
|
+
|
|
37
|
+
console.log(account.uid, vault.vaultSecret, messages, transactions);
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
CLI names should mirror the JS API:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
veyl create --passkey @runner
|
|
44
|
+
veyl send @alice 1000
|
|
45
|
+
veyl request @alice 1000
|
|
46
|
+
veyl balance
|
|
47
|
+
veyl read @alice
|
|
48
|
+
veyl say @alice "hello"
|
|
49
|
+
veyl listen
|
|
50
|
+
|
|
51
|
+
veyl account create @runner
|
|
52
|
+
veyl account create --passkey @runner
|
|
53
|
+
veyl account login @runner
|
|
54
|
+
veyl account login --passkey @runner
|
|
55
|
+
veyl vault create
|
|
56
|
+
veyl vault unlock
|
|
57
|
+
veyl vault lock
|
|
58
|
+
veyl chat read @alice
|
|
59
|
+
veyl chat send @alice "hello"
|
|
60
|
+
veyl money balance
|
|
61
|
+
veyl money send @alice 1000
|
|
62
|
+
veyl money request @alice 1000
|
|
63
|
+
veyl transactions
|
|
64
|
+
veyl mcp serve
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The same commands are exposed as MCP tools through `veyl mcp serve`.
|
|
68
|
+
|
|
69
|
+
`veyl listen` keeps one auth and vault session alive in the current process, then emits newline-delimited JSON events for new messages and wallet transfers. It is the terminal event-stream path for agents that do not want to speak MCP.
|
|
70
|
+
|
|
71
|
+
Passkey browser auth defaults to `https://veyl.glyphteck.com`. Use `--web-url` or `VEYL_WEB_URL` to point the CLI at another allowed passkey origin, such as a local dev host or a future dedicated auth host.
|
|
72
|
+
|
|
73
|
+
From this monorepo, use the root dev helper instead of the package filter form:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
bun dev headless regtest create --passkey @runner
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Local Secrets
|
|
80
|
+
|
|
81
|
+
The package creates a local profile under `~/.veyl` by default. It stores the machine credential and, unless `saveSecret: false` or `--no-save-secret` is used, the generated vault secret. Passkey flows pair and store a local machine credential by default so later CLI commands do not need a browser prompt; use `--no-save-credential` or `saveCredential: false` to return the credential material without saving the private key locally. Callers that already have a secret manager can pass `VEYL_HOME`, `VEYL_PROFILE`, `VEYL_VAULT_SECRET`, or explicit SDK options and store those secrets elsewhere.
|
|
82
|
+
|
|
83
|
+
Glyphteck-owned Google Secret Manager bot seeds are not used by this public path.
|
|
84
|
+
|
|
85
|
+
## Backend Contract
|
|
86
|
+
|
|
87
|
+
The backend only verifies machine-key challenges and issues Firebase custom tokens:
|
|
88
|
+
|
|
89
|
+
- `POST /headless/account/create/start`
|
|
90
|
+
- `POST /headless/account/create/finish`
|
|
91
|
+
- `POST /headless/account/login/start`
|
|
92
|
+
- `POST /headless/account/login/finish`
|
|
93
|
+
- `POST /headless/account/credential/add/start`
|
|
94
|
+
- `POST /headless/account/credential/add/finish`
|
|
95
|
+
|
|
96
|
+
Vault secrets, wallet entropy, chat seeds, Spark wallet boot, and message/payment signing all run locally in this package through shared Veyl primitives.
|
|
97
|
+
|
|
98
|
+
Generic unlocked-account actions live in `@veyl/shared/account/actions`; this package only wraps them with headless account auth, local profile storage, CLI commands, and MCP serving.
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@glyphteck/veyl",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Programmable Bun client for Veyl accounts, vaults, chat, and wallet payments.",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"veyl": "src/cli.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"src",
|
|
15
|
+
"README.md",
|
|
16
|
+
"package.json"
|
|
17
|
+
],
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"start": "bun src/cli.js",
|
|
23
|
+
"lint": "eslint . --quiet"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@buildonspark/spark-sdk": "^0.8.8",
|
|
27
|
+
"@noble/hashes": "^2.2.0",
|
|
28
|
+
"@veyl/shared": "^0.1.0",
|
|
29
|
+
"firebase": "^12.15.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { ClientNotReadyError, createClient } from './index.js';
|
|
3
|
+
|
|
4
|
+
const HELP = `Usage:
|
|
5
|
+
veyl create [--passkey] @name
|
|
6
|
+
veyl login [--passkey] [@name]
|
|
7
|
+
veyl unlock
|
|
8
|
+
veyl send @name 1000
|
|
9
|
+
veyl request @name 1000
|
|
10
|
+
veyl balance
|
|
11
|
+
veyl read @name
|
|
12
|
+
veyl say @name "message"
|
|
13
|
+
veyl listen
|
|
14
|
+
|
|
15
|
+
Structured:
|
|
16
|
+
veyl [--profile name] [--network REGTEST|MAINNET] account create @name
|
|
17
|
+
veyl account create --passkey @name
|
|
18
|
+
veyl account login @name
|
|
19
|
+
veyl account login --passkey [@name]
|
|
20
|
+
veyl account me
|
|
21
|
+
veyl vault create
|
|
22
|
+
veyl vault unlock [--secret value]
|
|
23
|
+
veyl vault lock
|
|
24
|
+
veyl chats
|
|
25
|
+
veyl chat read @name
|
|
26
|
+
veyl chat send @name "message"
|
|
27
|
+
veyl money balance
|
|
28
|
+
veyl money send @name 1000
|
|
29
|
+
veyl money request @name 1000
|
|
30
|
+
veyl money pay <request-id>
|
|
31
|
+
veyl transactions
|
|
32
|
+
veyl mcp serve
|
|
33
|
+
|
|
34
|
+
Listen:
|
|
35
|
+
--interval ms poll interval, minimum 1000
|
|
36
|
+
--replay emit current messages and transactions on startup
|
|
37
|
+
--no-chats skip chat message events
|
|
38
|
+
--no-transactions skip transaction events
|
|
39
|
+
|
|
40
|
+
Env:
|
|
41
|
+
VEYL_HOME local profile directory
|
|
42
|
+
VEYL_PROFILE default profile
|
|
43
|
+
VEYL_NETWORK wallet network
|
|
44
|
+
VEYL_VAULT_SECRET vault secret for unlock/create
|
|
45
|
+
VEYL_WEB_URL passkey browser origin`;
|
|
46
|
+
|
|
47
|
+
function print(value) {
|
|
48
|
+
if (value === undefined) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (value === null) {
|
|
52
|
+
console.log('null');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (typeof value === 'string') {
|
|
56
|
+
console.log(value);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
console.log(JSON.stringify(value, null, 2));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function usageError(message) {
|
|
63
|
+
const error = new Error(`${message}\n\n${HELP}`);
|
|
64
|
+
error.code = 'usage';
|
|
65
|
+
return error;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function parseArgs(argv) {
|
|
69
|
+
const options = {};
|
|
70
|
+
const args = [];
|
|
71
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
72
|
+
const item = argv[index];
|
|
73
|
+
if (item === '--profile') {
|
|
74
|
+
options.profile = argv[++index];
|
|
75
|
+
} else if (item === '--network') {
|
|
76
|
+
options.network = argv[++index];
|
|
77
|
+
} else if (item === '--secret') {
|
|
78
|
+
options.secret = argv[++index];
|
|
79
|
+
} else if (item === '--no-save-secret') {
|
|
80
|
+
options.saveSecret = false;
|
|
81
|
+
} else if (item === '--passkey') {
|
|
82
|
+
options.passkey = true;
|
|
83
|
+
} else if (item === '--web-url') {
|
|
84
|
+
options.webUrl = argv[++index];
|
|
85
|
+
} else if (item === '--no-save-credential') {
|
|
86
|
+
options.saveCredential = false;
|
|
87
|
+
} else if (item === '--interval') {
|
|
88
|
+
options.interval = argv[++index];
|
|
89
|
+
} else if (item === '--chat-count') {
|
|
90
|
+
options.chatCount = argv[++index];
|
|
91
|
+
} else if (item === '--message-count') {
|
|
92
|
+
options.messageCount = argv[++index];
|
|
93
|
+
} else if (item === '--tx-count') {
|
|
94
|
+
options.txCount = argv[++index];
|
|
95
|
+
} else if (item === '--replay') {
|
|
96
|
+
options.replay = true;
|
|
97
|
+
} else if (item === '--no-chats') {
|
|
98
|
+
options.chats = false;
|
|
99
|
+
} else if (item === '--no-transactions') {
|
|
100
|
+
options.transactions = false;
|
|
101
|
+
} else {
|
|
102
|
+
args.push(item);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return { args, options };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function runCommand(argv) {
|
|
109
|
+
const parsed = parseArgs(argv);
|
|
110
|
+
if (parsed.options.secret) {
|
|
111
|
+
process.env.VEYL_VAULT_SECRET = parsed.options.secret;
|
|
112
|
+
}
|
|
113
|
+
const [area, command, ...args] = parsed.args;
|
|
114
|
+
const veyl = createClient({
|
|
115
|
+
profile: parsed.options.profile,
|
|
116
|
+
network: parsed.options.network,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (!area || area === 'help' || area === '--help' || area === '-h') {
|
|
120
|
+
return HELP;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (area === 'chats') {
|
|
124
|
+
return veyl.chat.list();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (area === 'transactions') {
|
|
128
|
+
return veyl.money.transactions();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (area === 'create') {
|
|
132
|
+
return parsed.options.passkey
|
|
133
|
+
? veyl.account.createPasskey({
|
|
134
|
+
username: args[0],
|
|
135
|
+
network: parsed.options.network,
|
|
136
|
+
webUrl: parsed.options.webUrl,
|
|
137
|
+
saveCredential: parsed.options.saveCredential,
|
|
138
|
+
onPasskeyUrl: (url) => console.error(`open ${url}`),
|
|
139
|
+
})
|
|
140
|
+
: veyl.account.create({ username: args[0], network: parsed.options.network, saveCredential: parsed.options.saveCredential });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (area === 'login') {
|
|
144
|
+
return parsed.options.passkey
|
|
145
|
+
? veyl.account.loginPasskey({
|
|
146
|
+
username: args[0],
|
|
147
|
+
network: parsed.options.network,
|
|
148
|
+
webUrl: parsed.options.webUrl,
|
|
149
|
+
saveCredential: parsed.options.saveCredential,
|
|
150
|
+
onPasskeyUrl: (url) => console.error(`open ${url}`),
|
|
151
|
+
})
|
|
152
|
+
: veyl.account.login({ username: args[0] });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (area === 'me') {
|
|
156
|
+
return veyl.account.me();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (area === 'unlock') {
|
|
160
|
+
return veyl.vault.unlock({ secret: parsed.options.secret, saveSecret: parsed.options.saveSecret });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (area === 'lock') {
|
|
164
|
+
return veyl.vault.lock();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (area === 'balance') {
|
|
168
|
+
return veyl.money.balance();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (area === 'send') {
|
|
172
|
+
return veyl.money.send(args[0], args[1]);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (area === 'request') {
|
|
176
|
+
return veyl.money.request(args[0], args[1]);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (area === 'pay') {
|
|
180
|
+
return veyl.money.pay(args[0]);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (area === 'txs') {
|
|
184
|
+
return veyl.money.transactions();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (area === 'read') {
|
|
188
|
+
return veyl.chat.read(args[0]);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (area === 'say' || area === 'msg') {
|
|
192
|
+
return veyl.chat.send(args[0], args.slice(1).join(' '));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (area === 'listen') {
|
|
196
|
+
const controller = new AbortController();
|
|
197
|
+
process.once('SIGINT', () => controller.abort());
|
|
198
|
+
process.once('SIGTERM', () => controller.abort());
|
|
199
|
+
await veyl.listen({
|
|
200
|
+
interval: parsed.options.interval,
|
|
201
|
+
chatCount: parsed.options.chatCount,
|
|
202
|
+
messageCount: parsed.options.messageCount,
|
|
203
|
+
txCount: parsed.options.txCount,
|
|
204
|
+
replay: parsed.options.replay,
|
|
205
|
+
chats: parsed.options.chats,
|
|
206
|
+
transactions: parsed.options.transactions,
|
|
207
|
+
signal: controller.signal,
|
|
208
|
+
onEvent: (event) => console.log(JSON.stringify(event)),
|
|
209
|
+
});
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (area === 'account') {
|
|
214
|
+
const passkeyOptions = {
|
|
215
|
+
username: args[0],
|
|
216
|
+
network: parsed.options.network,
|
|
217
|
+
webUrl: parsed.options.webUrl,
|
|
218
|
+
saveCredential: parsed.options.saveCredential,
|
|
219
|
+
onPasskeyUrl: (url) => console.error(`open ${url}`),
|
|
220
|
+
};
|
|
221
|
+
if (command === 'create') {
|
|
222
|
+
return parsed.options.passkey
|
|
223
|
+
? veyl.account.createPasskey(passkeyOptions)
|
|
224
|
+
: veyl.account.create({ username: args[0], network: parsed.options.network, saveCredential: parsed.options.saveCredential });
|
|
225
|
+
}
|
|
226
|
+
if (command === 'login') {
|
|
227
|
+
return parsed.options.passkey
|
|
228
|
+
? veyl.account.loginPasskey(passkeyOptions)
|
|
229
|
+
: veyl.account.login({ username: args[0] });
|
|
230
|
+
}
|
|
231
|
+
if (command === 'me') return veyl.account.me();
|
|
232
|
+
throw usageError('unknown account command');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (area === 'vault') {
|
|
236
|
+
if (command === 'create') return veyl.vault.create({ secret: parsed.options.secret, saveSecret: parsed.options.saveSecret });
|
|
237
|
+
if (command === 'unlock') return veyl.vault.unlock({ secret: parsed.options.secret, saveSecret: parsed.options.saveSecret });
|
|
238
|
+
if (command === 'lock') return veyl.vault.lock();
|
|
239
|
+
throw usageError('unknown vault command');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (area === 'chat') {
|
|
243
|
+
if (command === 'list') return veyl.chat.list();
|
|
244
|
+
if (command === 'read') return veyl.chat.read(args[0]);
|
|
245
|
+
if (command === 'send') return veyl.chat.send(args[0], args.slice(1).join(' '));
|
|
246
|
+
throw usageError('unknown chat command');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (area === 'money') {
|
|
250
|
+
if (command === 'balance') return veyl.money.balance();
|
|
251
|
+
if (command === 'send') return veyl.money.send(args[0], args[1]);
|
|
252
|
+
if (command === 'request') return veyl.money.request(args[0], args[1]);
|
|
253
|
+
if (command === 'pay') return veyl.money.pay(args[0]);
|
|
254
|
+
if (command === 'transactions') return veyl.money.transactions();
|
|
255
|
+
throw usageError('unknown money command');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (area === 'mcp') {
|
|
259
|
+
if (command === 'serve') return veyl.mcp.serve();
|
|
260
|
+
throw usageError('unknown mcp command');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
throw usageError('unknown command');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function main() {
|
|
267
|
+
try {
|
|
268
|
+
print(await runCommand(process.argv.slice(2)));
|
|
269
|
+
} catch (error) {
|
|
270
|
+
if (error instanceof ClientNotReadyError) {
|
|
271
|
+
console.error(error.message);
|
|
272
|
+
process.exitCode = 2;
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
console.error(error?.message || String(error));
|
|
276
|
+
process.exitCode = error?.code === 'usage' ? 1 : 2;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
main();
|