@glyphteck/veyl 0.1.0 → 0.1.1
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 +8 -0
- package/dist/cli.js +133402 -0
- package/dist/index.js +133168 -0
- package/package.json +10 -11
- package/src/cli.js +0 -280
- package/src/client.js +0 -499
- package/src/cloud.js +0 -52
- package/src/index.js +0 -11
- package/src/kdf.js +0 -12
- package/src/listen.js +0 -120
- package/src/machine.js +0 -38
- package/src/mcp.js +0 -96
- package/src/passkey.js +0 -182
- package/src/storage.js +0 -113
package/src/machine.js
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { Buffer } from 'node:buffer';
|
|
2
|
-
import { createHash, createPrivateKey, generateKeyPairSync, sign } from 'node:crypto';
|
|
3
|
-
|
|
4
|
-
const SIGNATURE_CONTEXT = 'veyl-machine-auth-v1\n';
|
|
5
|
-
|
|
6
|
-
function challengeMessage(challenge) {
|
|
7
|
-
const value = String(challenge ?? '').trim();
|
|
8
|
-
if (!value) {
|
|
9
|
-
throw new Error('challenge required');
|
|
10
|
-
}
|
|
11
|
-
return Buffer.from(`${SIGNATURE_CONTEXT}${value}`, 'utf8');
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function credentialIdForPublicKey(publicKeyPem) {
|
|
15
|
-
const key = String(publicKeyPem ?? '').trim();
|
|
16
|
-
if (!key) {
|
|
17
|
-
throw new Error('machine public key required');
|
|
18
|
-
}
|
|
19
|
-
return createHash('sha256').update(key).digest('base64url');
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function createMachineCredential() {
|
|
23
|
-
const { privateKey, publicKey } = generateKeyPairSync('ed25519');
|
|
24
|
-
const publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' });
|
|
25
|
-
const privateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' });
|
|
26
|
-
const credentialId = credentialIdForPublicKey(publicKeyPem);
|
|
27
|
-
return {
|
|
28
|
-
type: 'ed25519',
|
|
29
|
-
credentialId,
|
|
30
|
-
publicKeyPem,
|
|
31
|
-
privateKeyPem,
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function signMachineChallenge(privateKeyPem, challenge) {
|
|
36
|
-
const key = createPrivateKey(String(privateKeyPem ?? ''));
|
|
37
|
-
return sign(null, challengeMessage(challenge), key).toString('base64url');
|
|
38
|
-
}
|
package/src/mcp.js
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { createInterface } from 'node:readline';
|
|
2
|
-
import process from 'node:process';
|
|
3
|
-
|
|
4
|
-
const TOOLS = [
|
|
5
|
-
{ name: 'account_create', description: 'Create a Veyl account headlessly.', inputSchema: { type: 'object', properties: { username: { type: 'string' }, network: { type: 'string' } }, required: ['username'] } },
|
|
6
|
-
{ name: 'account_login', description: 'Log in with the local machine credential.', inputSchema: { type: 'object', properties: { username: { type: 'string' } } } },
|
|
7
|
-
{ name: 'account_me', description: 'Show the current local account.', inputSchema: { type: 'object', properties: {} } },
|
|
8
|
-
{ name: 'vault_create', description: 'Create and upload the local encrypted vault.', inputSchema: { type: 'object', properties: { secret: { type: 'string' }, saveSecret: { type: 'boolean' } } } },
|
|
9
|
-
{ name: 'vault_unlock', description: 'Unlock the local encrypted vault.', inputSchema: { type: 'object', properties: { secret: { type: 'string' } } } },
|
|
10
|
-
{ name: 'vault_lock', description: 'Lock the wallet and chat session.', inputSchema: { type: 'object', properties: {} } },
|
|
11
|
-
{ name: 'chat_list', description: 'List chats.', inputSchema: { type: 'object', properties: { count: { type: 'number' } } } },
|
|
12
|
-
{ name: 'chat_read', description: 'Read display messages from a chat.', inputSchema: { type: 'object', properties: { peer: { type: 'string' }, count: { type: 'number' } }, required: ['peer'] } },
|
|
13
|
-
{ name: 'chat_send', description: 'Send a text message.', inputSchema: { type: 'object', properties: { peer: { type: 'string' }, message: { type: 'string' } }, required: ['peer', 'message'] } },
|
|
14
|
-
{ name: 'money_balance', description: 'Read wallet balance.', inputSchema: { type: 'object', properties: {} } },
|
|
15
|
-
{ name: 'money_send', description: 'Send sats to a peer.', inputSchema: { type: 'object', properties: { peer: { type: 'string' }, sats: { type: 'number' } }, required: ['peer', 'sats'] } },
|
|
16
|
-
{ name: 'money_request', description: 'Request sats from a peer.', inputSchema: { type: 'object', properties: { peer: { type: 'string' }, sats: { type: 'number' } }, required: ['peer', 'sats'] } },
|
|
17
|
-
{ name: 'money_pay', description: 'Pay a chat payment request.', inputSchema: { type: 'object', properties: { requestId: { type: 'string' } }, required: ['requestId'] } },
|
|
18
|
-
{ name: 'transactions_list', description: 'List wallet transactions.', inputSchema: { type: 'object', properties: { count: { type: 'number' }, offset: { type: 'number' } } } },
|
|
19
|
-
];
|
|
20
|
-
|
|
21
|
-
function result(value) {
|
|
22
|
-
return {
|
|
23
|
-
content: [
|
|
24
|
-
{
|
|
25
|
-
type: 'text',
|
|
26
|
-
text: JSON.stringify(value ?? null, null, 2),
|
|
27
|
-
},
|
|
28
|
-
],
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async function callTool(client, name, args = {}) {
|
|
33
|
-
if (name === 'account_create') return client.account.create(args);
|
|
34
|
-
if (name === 'account_login') return client.account.login(args);
|
|
35
|
-
if (name === 'account_me') return client.account.me();
|
|
36
|
-
if (name === 'vault_create') return client.vault.create(args);
|
|
37
|
-
if (name === 'vault_unlock') return client.vault.unlock(args);
|
|
38
|
-
if (name === 'vault_lock') return client.vault.lock(args);
|
|
39
|
-
if (name === 'chat_list') return client.chat.list(args);
|
|
40
|
-
if (name === 'chat_read') return client.chat.read(args.peer, args);
|
|
41
|
-
if (name === 'chat_send') return client.chat.send(args.peer, args.message, args);
|
|
42
|
-
if (name === 'money_balance') return client.money.balance(args);
|
|
43
|
-
if (name === 'money_send') return client.money.send(args.peer, args.sats, args);
|
|
44
|
-
if (name === 'money_request') return client.money.request(args.peer, args.sats, args);
|
|
45
|
-
if (name === 'money_pay') return client.money.pay(args.requestId, args);
|
|
46
|
-
if (name === 'transactions_list') return client.money.transactions(args);
|
|
47
|
-
throw new Error(`unknown tool ${name}`);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function write(message) {
|
|
51
|
-
process.stdout.write(`${JSON.stringify(message)}\n`);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function writeError(id, error) {
|
|
55
|
-
write({
|
|
56
|
-
jsonrpc: '2.0',
|
|
57
|
-
id,
|
|
58
|
-
error: {
|
|
59
|
-
code: -32000,
|
|
60
|
-
message: error?.message || String(error),
|
|
61
|
-
},
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export async function serveMcp(client) {
|
|
66
|
-
const rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
67
|
-
for await (const line of rl) {
|
|
68
|
-
if (!line.trim()) {
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
let message = null;
|
|
72
|
-
try {
|
|
73
|
-
message = JSON.parse(line);
|
|
74
|
-
if (message.method === 'initialize') {
|
|
75
|
-
write({
|
|
76
|
-
jsonrpc: '2.0',
|
|
77
|
-
id: message.id,
|
|
78
|
-
result: {
|
|
79
|
-
protocolVersion: '2024-11-05',
|
|
80
|
-
capabilities: { tools: {} },
|
|
81
|
-
serverInfo: { name: 'veyl', version: '0.1.0' },
|
|
82
|
-
},
|
|
83
|
-
});
|
|
84
|
-
} else if (message.method === 'tools/list') {
|
|
85
|
-
write({ jsonrpc: '2.0', id: message.id, result: { tools: TOOLS } });
|
|
86
|
-
} else if (message.method === 'tools/call') {
|
|
87
|
-
const toolResult = await callTool(client, message.params?.name, message.params?.arguments || {});
|
|
88
|
-
write({ jsonrpc: '2.0', id: message.id, result: result(toolResult) });
|
|
89
|
-
} else if (message.id != null) {
|
|
90
|
-
write({ jsonrpc: '2.0', id: message.id, result: {} });
|
|
91
|
-
}
|
|
92
|
-
} catch (error) {
|
|
93
|
-
writeError(message?.id ?? null, error);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
package/src/passkey.js
DELETED
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import { randomBytes } from 'node:crypto';
|
|
3
|
-
import http from 'node:http';
|
|
4
|
-
import process from 'node:process';
|
|
5
|
-
import { links } from '@veyl/shared/links';
|
|
6
|
-
|
|
7
|
-
const CALLBACK_PATH = '/callback';
|
|
8
|
-
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
9
|
-
|
|
10
|
-
function cleanMode(value) {
|
|
11
|
-
const mode = String(value || '').trim().toLowerCase();
|
|
12
|
-
if (mode === 'create' || mode === 'login') {
|
|
13
|
-
return mode;
|
|
14
|
-
}
|
|
15
|
-
throw new Error('passkey mode must be create or login');
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function cleanWebUrl(value) {
|
|
19
|
-
const source = String(value || process.env.VEYL_WEB_URL || links.veyl).trim();
|
|
20
|
-
const url = new URL(source);
|
|
21
|
-
if (url.protocol !== 'https:' && url.hostname !== 'localhost') {
|
|
22
|
-
throw new Error('passkey web url must be https or localhost');
|
|
23
|
-
}
|
|
24
|
-
return url;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function appendSearch(url, params) {
|
|
28
|
-
Object.entries(params).forEach(([key, value]) => {
|
|
29
|
-
const text = String(value || '').trim();
|
|
30
|
-
if (text) {
|
|
31
|
-
url.searchParams.set(key, text);
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
return url;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function openBrowser(url) {
|
|
38
|
-
const target = String(url);
|
|
39
|
-
const command =
|
|
40
|
-
process.platform === 'darwin' ? 'open' :
|
|
41
|
-
process.platform === 'win32' ? 'cmd' :
|
|
42
|
-
'xdg-open';
|
|
43
|
-
const args =
|
|
44
|
-
process.platform === 'win32' ? ['/c', 'start', '', target] :
|
|
45
|
-
[target];
|
|
46
|
-
|
|
47
|
-
const child = spawn(command, args, {
|
|
48
|
-
detached: true,
|
|
49
|
-
stdio: 'ignore',
|
|
50
|
-
});
|
|
51
|
-
child.unref();
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function setCors(res) {
|
|
55
|
-
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
56
|
-
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
|
57
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
58
|
-
res.setHeader('Access-Control-Allow-Private-Network', 'true');
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function readBody(req) {
|
|
62
|
-
return new Promise((resolve, reject) => {
|
|
63
|
-
let body = '';
|
|
64
|
-
req.setEncoding('utf8');
|
|
65
|
-
req.on('data', (chunk) => {
|
|
66
|
-
body += chunk;
|
|
67
|
-
if (body.length > 32_768) {
|
|
68
|
-
reject(new Error('passkey callback body too large'));
|
|
69
|
-
req.destroy();
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
req.on('end', () => {
|
|
73
|
-
try {
|
|
74
|
-
resolve(body ? JSON.parse(body) : {});
|
|
75
|
-
} catch {
|
|
76
|
-
reject(new Error('passkey callback body must be json'));
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
req.on('error', reject);
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function listen(server) {
|
|
84
|
-
return new Promise((resolve, reject) => {
|
|
85
|
-
server.once('error', reject);
|
|
86
|
-
server.listen(0, '127.0.0.1', () => {
|
|
87
|
-
server.off('error', reject);
|
|
88
|
-
resolve(server.address());
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export async function runPasskeyBrowserFlow(options = {}) {
|
|
94
|
-
const mode = cleanMode(options.mode);
|
|
95
|
-
const state = randomBytes(24).toString('base64url');
|
|
96
|
-
const timeoutMs = Math.max(10_000, Number(options.timeoutMs || DEFAULT_TIMEOUT_MS));
|
|
97
|
-
|
|
98
|
-
let settled = false;
|
|
99
|
-
let timer = null;
|
|
100
|
-
let server = null;
|
|
101
|
-
|
|
102
|
-
function close() {
|
|
103
|
-
if (timer) {
|
|
104
|
-
clearTimeout(timer);
|
|
105
|
-
timer = null;
|
|
106
|
-
}
|
|
107
|
-
server?.close?.();
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const result = new Promise((resolve, reject) => {
|
|
111
|
-
server = http.createServer(async (req, res) => {
|
|
112
|
-
setCors(res);
|
|
113
|
-
if (req.method === 'OPTIONS') {
|
|
114
|
-
res.writeHead(204).end();
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
if (req.method !== 'POST' || new URL(req.url || '/', 'http://127.0.0.1').pathname !== CALLBACK_PATH) {
|
|
118
|
-
res.writeHead(404, { 'Content-Type': 'application/json' }).end(JSON.stringify({ ok: false }));
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
try {
|
|
122
|
-
const body = await readBody(req);
|
|
123
|
-
if (body.state !== state) {
|
|
124
|
-
res.writeHead(403, { 'Content-Type': 'application/json' }).end(JSON.stringify({ ok: false }));
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
if (body.ok === false) {
|
|
128
|
-
throw new Error(body.message || 'passkey flow failed');
|
|
129
|
-
}
|
|
130
|
-
if (!body.token) {
|
|
131
|
-
throw new Error('passkey token missing');
|
|
132
|
-
}
|
|
133
|
-
settled = true;
|
|
134
|
-
res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({ ok: true }));
|
|
135
|
-
close();
|
|
136
|
-
resolve({
|
|
137
|
-
mode,
|
|
138
|
-
token: body.token,
|
|
139
|
-
uid: body.uid || null,
|
|
140
|
-
});
|
|
141
|
-
} catch (error) {
|
|
142
|
-
settled = true;
|
|
143
|
-
res.writeHead(400, { 'Content-Type': 'application/json' }).end(JSON.stringify({ ok: false }));
|
|
144
|
-
close();
|
|
145
|
-
reject(error);
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
timer = setTimeout(() => {
|
|
150
|
-
if (settled) return;
|
|
151
|
-
settled = true;
|
|
152
|
-
close();
|
|
153
|
-
reject(new Error('passkey flow timed out'));
|
|
154
|
-
}, timeoutMs);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
let address;
|
|
158
|
-
try {
|
|
159
|
-
address = await listen(server);
|
|
160
|
-
} catch (error) {
|
|
161
|
-
close();
|
|
162
|
-
throw error;
|
|
163
|
-
}
|
|
164
|
-
const callback = `http://127.0.0.1:${address.port}${CALLBACK_PATH}`;
|
|
165
|
-
const target = appendSearch(new URL('/cli/passkey', cleanWebUrl(options.webUrl)), {
|
|
166
|
-
mode,
|
|
167
|
-
state,
|
|
168
|
-
callback,
|
|
169
|
-
label: options.label,
|
|
170
|
-
uid: options.uid,
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
options.onUrl?.(String(target));
|
|
174
|
-
try {
|
|
175
|
-
openBrowser(target);
|
|
176
|
-
} catch (error) {
|
|
177
|
-
close();
|
|
178
|
-
throw error;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return result;
|
|
182
|
-
}
|
package/src/storage.js
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import { access, chmod, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { dirname, join } from 'node:path';
|
|
4
|
-
import process from 'node:process';
|
|
5
|
-
|
|
6
|
-
const FILE_MODE = 0o600;
|
|
7
|
-
const DIR_MODE = 0o700;
|
|
8
|
-
|
|
9
|
-
function trimAt(value) {
|
|
10
|
-
return String(value ?? '').trim().replace(/^@+/, '');
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function cleanProfileName(value, fallback = '') {
|
|
14
|
-
const clean = trimAt(value)
|
|
15
|
-
.toLowerCase()
|
|
16
|
-
.replace(/[^a-z0-9._-]+/g, '-')
|
|
17
|
-
.replace(/^-+|-+$/g, '');
|
|
18
|
-
return clean || fallback;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function defaultHomeDir() {
|
|
22
|
-
return process.env.VEYL_HOME || join(homedir(), '.veyl');
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async function exists(path) {
|
|
26
|
-
try {
|
|
27
|
-
await access(path);
|
|
28
|
-
return true;
|
|
29
|
-
} catch {
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async function readJson(path, fallback = null) {
|
|
35
|
-
if (!(await exists(path))) {
|
|
36
|
-
return fallback;
|
|
37
|
-
}
|
|
38
|
-
return JSON.parse(await readFile(path, 'utf8'));
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async function writeJson(path, value) {
|
|
42
|
-
await mkdir(dirname(path), { recursive: true, mode: DIR_MODE });
|
|
43
|
-
const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
44
|
-
await writeFile(tmp, `${JSON.stringify(value, null, 2)}\n`, { mode: FILE_MODE });
|
|
45
|
-
await chmod(tmp, FILE_MODE).catch(() => {});
|
|
46
|
-
await rename(tmp, path);
|
|
47
|
-
await chmod(path, FILE_MODE).catch(() => {});
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export class ProfileStore {
|
|
51
|
-
constructor(options = {}) {
|
|
52
|
-
this.dir = options.dir || defaultHomeDir();
|
|
53
|
-
this.profile = cleanProfileName(options.profile || process.env.VEYL_PROFILE || '', '');
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
activePath() {
|
|
57
|
-
return join(this.dir, 'active.json');
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
profilePath(profileName) {
|
|
61
|
-
const profile = cleanProfileName(profileName, '');
|
|
62
|
-
if (!profile) {
|
|
63
|
-
throw new Error('profile required');
|
|
64
|
-
}
|
|
65
|
-
return join(this.dir, 'profiles', `${profile}.json`);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async readActiveProfile() {
|
|
69
|
-
const active = await readJson(this.activePath(), null);
|
|
70
|
-
return cleanProfileName(this.profile || active?.profile || '', '');
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async writeActiveProfile(profileName) {
|
|
74
|
-
const profile = cleanProfileName(profileName, '');
|
|
75
|
-
if (!profile) {
|
|
76
|
-
throw new Error('profile required');
|
|
77
|
-
}
|
|
78
|
-
await writeJson(this.activePath(), { profile, updatedAt: Date.now() });
|
|
79
|
-
return profile;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async load(profileName = '') {
|
|
83
|
-
const profile = cleanProfileName(profileName || this.profile || await this.readActiveProfile(), '');
|
|
84
|
-
if (!profile) {
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
const data = await readJson(this.profilePath(profile), null);
|
|
88
|
-
return data ? { ...data, profile } : null;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async require(profileName = '') {
|
|
92
|
-
const profile = await this.load(profileName);
|
|
93
|
-
if (!profile) {
|
|
94
|
-
throw new Error('account profile not found. run account.create or account.login first');
|
|
95
|
-
}
|
|
96
|
-
return profile;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
async save(profile) {
|
|
100
|
-
const profileName = cleanProfileName(profile?.profile || profile?.username, '');
|
|
101
|
-
if (!profileName) {
|
|
102
|
-
throw new Error('profile required');
|
|
103
|
-
}
|
|
104
|
-
const next = {
|
|
105
|
-
...profile,
|
|
106
|
-
profile: profileName,
|
|
107
|
-
updatedAt: Date.now(),
|
|
108
|
-
};
|
|
109
|
-
await writeJson(this.profilePath(profileName), next);
|
|
110
|
-
await this.writeActiveProfile(profileName);
|
|
111
|
-
return next;
|
|
112
|
-
}
|
|
113
|
-
}
|