@rine-network/cli 0.6.1 → 0.7.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 +1 -1
- package/bin/rine.js +0 -0
- package/dist/main.js +175 -1079
- package/package.json +59 -62
package/dist/main.js
CHANGED
|
@@ -1,246 +1,10 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { Command } from "commander";
|
|
3
|
+
import { HttpClient, RineApiError, UUID_RE, agentKeysExist, cacheToken, decryptGroupMessage, decryptMessage, encryptGroupMessage, encryptMessage, encryptionPublicKeyToJWK, fetchAgents, fetchOAuthToken, fetchRecipientEncryptionKey, formatError, fromBase64Url, generateAgentKeys, getAgentPublicKeys, getCredentialEntry, getOrCreateSenderKey, getOrRefreshToken, ingestSenderKeyDistribution, isBareAgentName, loadAgentKeys, loadCredentials, loadTokenCache, resolveAgent, resolveApiUrl, resolveConfigDir, resolveHandleViaWebFinger, resolveToUuid, saveAgentKeys, saveCredentials, saveTokenCache, signingPublicKeyToJWK, solveTimeLockWithProgress, toBase64Url, validateEncryptionKey, validateSigningKey } from "@rine-network/core";
|
|
4
|
+
import readline from "node:readline";
|
|
3
5
|
import * as fs from "node:fs";
|
|
4
6
|
import { join } from "node:path";
|
|
5
|
-
import { ed25519, x25519 } from "@noble/curves/ed25519.js";
|
|
6
|
-
import readline from "node:readline";
|
|
7
|
-
import { Aes256Gcm, CipherSuite, HkdfSha256 } from "@hpke/core";
|
|
8
|
-
import { DhkemX25519HkdfSha256 } from "@hpke/dhkem-x25519";
|
|
9
|
-
import { hmac } from "@noble/hashes/hmac.js";
|
|
10
|
-
import { sha256 } from "@noble/hashes/sha2.js";
|
|
11
7
|
import { createEventSource } from "eventsource-client";
|
|
12
|
-
//#region \0rolldown/runtime.js
|
|
13
|
-
var __defProp = Object.defineProperty;
|
|
14
|
-
var __exportAll = (all, no_symbols) => {
|
|
15
|
-
let target = {};
|
|
16
|
-
for (var name in all) __defProp(target, name, {
|
|
17
|
-
get: all[name],
|
|
18
|
-
enumerable: true
|
|
19
|
-
});
|
|
20
|
-
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
21
|
-
return target;
|
|
22
|
-
};
|
|
23
|
-
//#endregion
|
|
24
|
-
//#region src/errors.ts
|
|
25
|
-
var RineApiError = class extends Error {
|
|
26
|
-
constructor(status, detail, raw) {
|
|
27
|
-
super(`${status}: ${detail}`);
|
|
28
|
-
this.status = status;
|
|
29
|
-
this.detail = detail;
|
|
30
|
-
this.raw = raw;
|
|
31
|
-
this.name = "RineApiError";
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
function formatError(err) {
|
|
35
|
-
if (err instanceof RineApiError) return err.detail;
|
|
36
|
-
if (err instanceof Error) return err.message;
|
|
37
|
-
return String(err);
|
|
38
|
-
}
|
|
39
|
-
//#endregion
|
|
40
|
-
//#region src/config.ts
|
|
41
|
-
function getConfigDir() {
|
|
42
|
-
return process.env.RINE_CONFIG_DIR ?? join(process.cwd(), ".rine");
|
|
43
|
-
}
|
|
44
|
-
function getApiUrl() {
|
|
45
|
-
return (process.env.RINE_API_URL ?? "https://rine.network").replace(/\/$/, "");
|
|
46
|
-
}
|
|
47
|
-
function ensureDir(dir) {
|
|
48
|
-
fs.mkdirSync(dir, {
|
|
49
|
-
recursive: true,
|
|
50
|
-
mode: 448
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
function writeAtomic(path, content) {
|
|
54
|
-
const tmpPath = `${path}.tmp`;
|
|
55
|
-
fs.writeFileSync(tmpPath, content, "utf-8");
|
|
56
|
-
fs.renameSync(tmpPath, path);
|
|
57
|
-
fs.chmodSync(path, 384);
|
|
58
|
-
}
|
|
59
|
-
function loadCredentials() {
|
|
60
|
-
const path = join(getConfigDir(), "credentials.json");
|
|
61
|
-
try {
|
|
62
|
-
const raw = fs.readFileSync(path, "utf-8");
|
|
63
|
-
return JSON.parse(raw);
|
|
64
|
-
} catch {
|
|
65
|
-
return {};
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
function saveCredentials(creds) {
|
|
69
|
-
const dir = getConfigDir();
|
|
70
|
-
ensureDir(dir);
|
|
71
|
-
writeAtomic(join(dir, "credentials.json"), JSON.stringify(creds, null, 2));
|
|
72
|
-
}
|
|
73
|
-
function loadTokenCache() {
|
|
74
|
-
const path = join(getConfigDir(), "token_cache.json");
|
|
75
|
-
try {
|
|
76
|
-
const raw = fs.readFileSync(path, "utf-8");
|
|
77
|
-
return JSON.parse(raw);
|
|
78
|
-
} catch {
|
|
79
|
-
return {};
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
function saveTokenCache(cache) {
|
|
83
|
-
const dir = getConfigDir();
|
|
84
|
-
ensureDir(dir);
|
|
85
|
-
writeAtomic(join(dir, "token_cache.json"), JSON.stringify(cache, null, 2));
|
|
86
|
-
}
|
|
87
|
-
function cacheToken(profile, token) {
|
|
88
|
-
const cache = loadTokenCache();
|
|
89
|
-
cache[profile] = {
|
|
90
|
-
access_token: token.access_token,
|
|
91
|
-
expires_at: Date.now() / 1e3 + token.expires_in
|
|
92
|
-
};
|
|
93
|
-
saveTokenCache(cache);
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Resolves credentials for a profile. Priority:
|
|
97
|
-
* 1. RINE_CLIENT_ID + RINE_CLIENT_SECRET env vars (both required)
|
|
98
|
-
* 2. credentials.json for the given profile
|
|
99
|
-
* Returns undefined if no credentials found.
|
|
100
|
-
*/
|
|
101
|
-
function getCredentialEntry(profile = "default") {
|
|
102
|
-
const envId = process.env.RINE_CLIENT_ID;
|
|
103
|
-
const envSecret = process.env.RINE_CLIENT_SECRET;
|
|
104
|
-
if (envId && envSecret) return {
|
|
105
|
-
client_id: envId,
|
|
106
|
-
client_secret: envSecret
|
|
107
|
-
};
|
|
108
|
-
return loadCredentials()[profile];
|
|
109
|
-
}
|
|
110
|
-
//#endregion
|
|
111
|
-
//#region src/http.ts
|
|
112
|
-
async function parseErrorDetail(res) {
|
|
113
|
-
try {
|
|
114
|
-
const body = await res.json();
|
|
115
|
-
if (typeof body.detail === "string") return body.detail;
|
|
116
|
-
if (Array.isArray(body.detail)) return body.detail.map((e) => e.msg).join("; ");
|
|
117
|
-
} catch {}
|
|
118
|
-
return res.statusText;
|
|
119
|
-
}
|
|
120
|
-
var HttpClient = class {
|
|
121
|
-
baseUrl;
|
|
122
|
-
tokenFn;
|
|
123
|
-
canRefresh;
|
|
124
|
-
defaultHeaders;
|
|
125
|
-
constructor(opts) {
|
|
126
|
-
this.baseUrl = opts.apiUrl ?? getApiUrl();
|
|
127
|
-
this.tokenFn = opts.tokenFn;
|
|
128
|
-
this.canRefresh = opts.canRefresh ?? true;
|
|
129
|
-
this.defaultHeaders = opts.defaultHeaders ?? {};
|
|
130
|
-
}
|
|
131
|
-
async request(method, path, body, params, extraHeaders) {
|
|
132
|
-
let url = this.baseUrl + path;
|
|
133
|
-
if (params) {
|
|
134
|
-
const qs = new URLSearchParams(Object.entries(params).filter(([, v]) => v !== void 0 && v !== null).map(([k, v]) => [k, String(v)])).toString();
|
|
135
|
-
if (qs) url += `?${qs}`;
|
|
136
|
-
}
|
|
137
|
-
const doFetch = async (force) => {
|
|
138
|
-
const headers = {
|
|
139
|
-
Authorization: `Bearer ${await this.tokenFn(force)}`,
|
|
140
|
-
...this.defaultHeaders,
|
|
141
|
-
...extraHeaders
|
|
142
|
-
};
|
|
143
|
-
if (body !== void 0) headers["Content-Type"] = "application/json";
|
|
144
|
-
const init = {
|
|
145
|
-
method,
|
|
146
|
-
headers
|
|
147
|
-
};
|
|
148
|
-
if (body !== void 0) init.body = JSON.stringify(body);
|
|
149
|
-
return fetch(url, init);
|
|
150
|
-
};
|
|
151
|
-
let res = await doFetch();
|
|
152
|
-
if (res.status === 401 && this.canRefresh) res = await doFetch(true);
|
|
153
|
-
if (!res.ok) {
|
|
154
|
-
const detail = await parseErrorDetail(res);
|
|
155
|
-
throw new RineApiError(res.status, detail, res);
|
|
156
|
-
}
|
|
157
|
-
if (res.status === 204) return void 0;
|
|
158
|
-
return res.json();
|
|
159
|
-
}
|
|
160
|
-
get(path, params, extraHeaders) {
|
|
161
|
-
return this.request("GET", path, void 0, params, extraHeaders);
|
|
162
|
-
}
|
|
163
|
-
post(path, body, extraHeaders) {
|
|
164
|
-
return this.request("POST", path, body, void 0, extraHeaders);
|
|
165
|
-
}
|
|
166
|
-
put(path, body, extraHeaders) {
|
|
167
|
-
return this.request("PUT", path, body, void 0, extraHeaders);
|
|
168
|
-
}
|
|
169
|
-
patch(path, body, extraHeaders) {
|
|
170
|
-
return this.request("PATCH", path, body, void 0, extraHeaders);
|
|
171
|
-
}
|
|
172
|
-
delete(path, extraHeaders) {
|
|
173
|
-
return this.request("DELETE", path, void 0, void 0, extraHeaders);
|
|
174
|
-
}
|
|
175
|
-
/** Unauthenticated GET for public endpoints (e.g. /directory/*). */
|
|
176
|
-
static async publicGet(path, params) {
|
|
177
|
-
const base = getApiUrl();
|
|
178
|
-
const qs = params?.toString();
|
|
179
|
-
const url = qs ? `${base}${path}?${qs}` : `${base}${path}`;
|
|
180
|
-
const res = await fetch(url, { headers: { Accept: "application/json" } });
|
|
181
|
-
if (!res.ok) {
|
|
182
|
-
const detail = await parseErrorDetail(res);
|
|
183
|
-
throw new RineApiError(res.status, detail, res);
|
|
184
|
-
}
|
|
185
|
-
return res.json();
|
|
186
|
-
}
|
|
187
|
-
};
|
|
188
|
-
/**
|
|
189
|
-
* Resolves a valid access token. Priority:
|
|
190
|
-
* 1. RINE_TOKEN env var — used directly, no cache/refresh
|
|
191
|
-
* 2. Token cache — returned if not expired (60s margin)
|
|
192
|
-
* 3. POST /oauth/token — fetches fresh token and caches it
|
|
193
|
-
*/
|
|
194
|
-
/**
|
|
195
|
-
* Fetches an OAuth token from POST /oauth/token.
|
|
196
|
-
* Shared by getOrRefreshToken, login, and register.
|
|
197
|
-
*/
|
|
198
|
-
async function fetchOAuthToken(clientId, clientSecret) {
|
|
199
|
-
const apiUrl = getApiUrl();
|
|
200
|
-
const res = await fetch(`${apiUrl}/oauth/token`, {
|
|
201
|
-
method: "POST",
|
|
202
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
203
|
-
body: new URLSearchParams({
|
|
204
|
-
grant_type: "client_credentials",
|
|
205
|
-
client_id: clientId,
|
|
206
|
-
client_secret: clientSecret
|
|
207
|
-
}).toString()
|
|
208
|
-
});
|
|
209
|
-
if (!res.ok) {
|
|
210
|
-
const body = await res.json().catch(() => ({}));
|
|
211
|
-
throw new RineApiError(res.status, body.detail ?? "Token request failed");
|
|
212
|
-
}
|
|
213
|
-
return res.json();
|
|
214
|
-
}
|
|
215
|
-
async function getOrRefreshToken(entry, profileName, force = false) {
|
|
216
|
-
const envToken = process.env.RINE_TOKEN;
|
|
217
|
-
if (envToken) return envToken;
|
|
218
|
-
const now = Date.now() / 1e3;
|
|
219
|
-
if (!force) {
|
|
220
|
-
const cached = loadTokenCache()[profileName];
|
|
221
|
-
if (cached !== void 0 && cached.expires_at - now > 60) return cached.access_token;
|
|
222
|
-
}
|
|
223
|
-
if (!entry) throw new RineApiError(0, "No credentials found. Run `rine login` first.");
|
|
224
|
-
const data = await fetchOAuthToken(entry.client_id, entry.client_secret);
|
|
225
|
-
cacheToken(profileName, data);
|
|
226
|
-
return data.access_token;
|
|
227
|
-
}
|
|
228
|
-
async function createClient(profileFlag, defaultHeaders) {
|
|
229
|
-
const profileName = profileFlag ?? "default";
|
|
230
|
-
const entry = getCredentialEntry(profileName);
|
|
231
|
-
const canRefresh = !process.env.RINE_TOKEN && entry !== void 0;
|
|
232
|
-
const tokenFn = (force = false) => getOrRefreshToken(entry, profileName, force);
|
|
233
|
-
return {
|
|
234
|
-
client: new HttpClient({
|
|
235
|
-
tokenFn,
|
|
236
|
-
canRefresh,
|
|
237
|
-
defaultHeaders
|
|
238
|
-
}),
|
|
239
|
-
profileName,
|
|
240
|
-
entry
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
//#endregion
|
|
244
8
|
//#region src/output.ts
|
|
245
9
|
function printSuccess(msg) {
|
|
246
10
|
console.log(`✓ ${msg}`);
|
|
@@ -280,103 +44,43 @@ function printError(msg) {
|
|
|
280
44
|
console.error(msg);
|
|
281
45
|
}
|
|
282
46
|
//#endregion
|
|
283
|
-
//#region src/resolve-handle.ts
|
|
284
|
-
const UUID_ALIAS_RE = /\/agents\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;
|
|
285
|
-
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
286
|
-
/**
|
|
287
|
-
* Resolves a handle (e.g. "agent@org.rine.network") to a UUID
|
|
288
|
-
* via WebFinger (RFC 7033).
|
|
289
|
-
*
|
|
290
|
-
* Returns the UUID string on success, or the original input unchanged
|
|
291
|
-
* on failure (caller validates UUID format and reports the error).
|
|
292
|
-
*/
|
|
293
|
-
async function resolveHandleViaWebFinger(handle) {
|
|
294
|
-
try {
|
|
295
|
-
const params = new URLSearchParams({ resource: `acct:${handle}` });
|
|
296
|
-
const data = await HttpClient.publicGet("/.well-known/webfinger", params);
|
|
297
|
-
for (const alias of data.aliases ?? []) {
|
|
298
|
-
const match = UUID_ALIAS_RE.exec(alias);
|
|
299
|
-
if (match?.[1]) return match[1];
|
|
300
|
-
}
|
|
301
|
-
} catch {}
|
|
302
|
-
return handle;
|
|
303
|
-
}
|
|
304
|
-
//#endregion
|
|
305
|
-
//#region src/resolve-agent.ts
|
|
306
|
-
let cached;
|
|
307
|
-
/**
|
|
308
|
-
* Resolve a value that may be a UUID or a handle (containing @) to a UUID.
|
|
309
|
-
* Handles are resolved via WebFinger. Required because local key storage
|
|
310
|
-
* is indexed by UUID.
|
|
311
|
-
*/
|
|
312
|
-
async function resolveToUuid(value) {
|
|
313
|
-
if (!value.includes("@")) {
|
|
314
|
-
if (UUID_RE.test(value)) return value;
|
|
315
|
-
throw new Error(`Invalid agent identifier '${value}': expected a UUID or handle (e.g. agent@org.rine.network).`);
|
|
316
|
-
}
|
|
317
|
-
const resolved = await resolveHandleViaWebFinger(value);
|
|
318
|
-
if (!UUID_RE.test(resolved)) throw new Error(`Cannot resolve handle "${value}" to agent ID. Ensure WebFinger is available or use a UUID.`);
|
|
319
|
-
return resolved;
|
|
320
|
-
}
|
|
321
|
-
/** Check if a value is a bare agent name (not a UUID, not a handle). */
|
|
322
|
-
function isBareAgentName(value) {
|
|
323
|
-
return !value.includes("@") && !UUID_RE.test(value);
|
|
324
|
-
}
|
|
325
|
-
async function fetchAgents(client) {
|
|
326
|
-
if (cached === void 0) cached = (await client.get("/agents")).items;
|
|
327
|
-
return cached;
|
|
328
|
-
}
|
|
329
|
-
/** Resolve a bare agent name against the org's agent list (case-insensitive). */
|
|
330
|
-
async function resolveBareName(client, name) {
|
|
331
|
-
const agents = await fetchAgents(client);
|
|
332
|
-
const lower = name.toLowerCase();
|
|
333
|
-
const match = agents.find((a) => a.name.toLowerCase() === lower);
|
|
334
|
-
if (match) return match.id;
|
|
335
|
-
const lines = agents.map((a) => ` ${a.name} ${a.handle}`).join("\n");
|
|
336
|
-
throw new Error(agents.length > 0 ? `No agent named '${name}'. Available agents:\n${lines}` : `No agent named '${name}'. No active agents found.`);
|
|
337
|
-
}
|
|
338
|
-
/**
|
|
339
|
-
* Resolve which agent ID to use.
|
|
340
|
-
* Priority: explicit --agent flag > --as global flag > auto-resolve (single-agent shortcut).
|
|
341
|
-
* Accepts UUIDs, handles (e.g. "bot@org.rine.network"), or bare names (e.g. "kofi").
|
|
342
|
-
*/
|
|
343
|
-
async function resolveAgent(client, explicit, asFlag) {
|
|
344
|
-
if (explicit) {
|
|
345
|
-
if (isBareAgentName(explicit)) return resolveBareName(client, explicit);
|
|
346
|
-
return resolveToUuid(explicit);
|
|
347
|
-
}
|
|
348
|
-
if (asFlag) {
|
|
349
|
-
if (isBareAgentName(asFlag)) return resolveBareName(client, asFlag);
|
|
350
|
-
return resolveToUuid(asFlag);
|
|
351
|
-
}
|
|
352
|
-
const agents = await fetchAgents(client);
|
|
353
|
-
if (agents.length === 1) {
|
|
354
|
-
const agent = agents[0];
|
|
355
|
-
if (agent) return agent.id;
|
|
356
|
-
}
|
|
357
|
-
if (agents.length === 0) throw new Error("No active agents. Create one with 'rine agent create --name <name>'");
|
|
358
|
-
const lines = agents.map((a) => ` ${a.id} ${a.handle}`).join("\n");
|
|
359
|
-
throw new Error(`Multiple agents found. Specify with --agent <uuid>:\n${lines}`);
|
|
360
|
-
}
|
|
361
|
-
//#endregion
|
|
362
47
|
//#region src/action.ts
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
48
|
+
async function createClient(configDir, apiUrl, profileFlag, defaultHeaders) {
|
|
49
|
+
const profileName = profileFlag ?? "default";
|
|
50
|
+
const entry = getCredentialEntry(configDir, profileName);
|
|
51
|
+
const envToken = process.env.RINE_TOKEN;
|
|
52
|
+
const canRefresh = !envToken && entry !== void 0;
|
|
53
|
+
const tokenFn = (force = false) => getOrRefreshToken(configDir, apiUrl, entry, profileName, {
|
|
54
|
+
force,
|
|
55
|
+
envToken
|
|
56
|
+
});
|
|
57
|
+
return {
|
|
58
|
+
client: new HttpClient({
|
|
59
|
+
tokenFn,
|
|
60
|
+
apiUrl,
|
|
61
|
+
canRefresh,
|
|
62
|
+
defaultHeaders
|
|
63
|
+
}),
|
|
64
|
+
profileName,
|
|
65
|
+
entry
|
|
66
|
+
};
|
|
67
|
+
}
|
|
368
68
|
function withClient(program, fn) {
|
|
369
69
|
return async (...args) => {
|
|
370
70
|
try {
|
|
371
71
|
const gOpts = program.opts();
|
|
72
|
+
const configDir = resolveConfigDir();
|
|
73
|
+
const apiUrl = resolveApiUrl();
|
|
372
74
|
const defaultHeaders = {};
|
|
373
|
-
if (gOpts.as && !isBareAgentName(gOpts.as)) defaultHeaders["X-Rine-Agent"] = await resolveToUuid(gOpts.as);
|
|
374
|
-
const { client, profileName } = await createClient(gOpts.profile, defaultHeaders);
|
|
375
|
-
if (gOpts.as && isBareAgentName(gOpts.as)) defaultHeaders["X-Rine-Agent"] = await resolveAgent(client, void 0, gOpts.as);
|
|
75
|
+
if (gOpts.as && !isBareAgentName(gOpts.as)) defaultHeaders["X-Rine-Agent"] = await resolveToUuid(apiUrl, gOpts.as);
|
|
76
|
+
const { client, profileName } = await createClient(configDir, apiUrl, gOpts.profile, defaultHeaders);
|
|
77
|
+
if (gOpts.as && isBareAgentName(gOpts.as)) defaultHeaders["X-Rine-Agent"] = await resolveAgent(apiUrl, await fetchAgents(client), void 0, gOpts.as);
|
|
376
78
|
await fn({
|
|
377
79
|
client,
|
|
378
80
|
gOpts,
|
|
379
81
|
profileName,
|
|
82
|
+
configDir,
|
|
83
|
+
apiUrl,
|
|
380
84
|
extraHeaders: defaultHeaders
|
|
381
85
|
}, ...args);
|
|
382
86
|
} catch (err) {
|
|
@@ -385,10 +89,6 @@ function withClient(program, fn) {
|
|
|
385
89
|
}
|
|
386
90
|
};
|
|
387
91
|
}
|
|
388
|
-
/**
|
|
389
|
-
* Wraps a command action with error handling only (no client creation).
|
|
390
|
-
* For commands like logout that don't need authentication.
|
|
391
|
-
*/
|
|
392
92
|
function withErrorHandler(program, fn) {
|
|
393
93
|
return async (...args) => {
|
|
394
94
|
try {
|
|
@@ -516,121 +216,6 @@ function registerAgentProfile(program) {
|
|
|
516
216
|
}));
|
|
517
217
|
}
|
|
518
218
|
//#endregion
|
|
519
|
-
//#region src/crypto/keys.ts
|
|
520
|
-
var keys_exports = /* @__PURE__ */ __exportAll({
|
|
521
|
-
agentIdFromKid: () => agentIdFromKid,
|
|
522
|
-
agentKeysExist: () => agentKeysExist,
|
|
523
|
-
encryptionPublicKeyToJWK: () => encryptionPublicKeyToJWK,
|
|
524
|
-
fromBase64Url: () => fromBase64Url,
|
|
525
|
-
generateAgentKeys: () => generateAgentKeys,
|
|
526
|
-
generateEncryptionKeyPair: () => generateEncryptionKeyPair,
|
|
527
|
-
generateSigningKeyPair: () => generateSigningKeyPair,
|
|
528
|
-
jwkToPublicKey: () => jwkToPublicKey,
|
|
529
|
-
loadAgentKeys: () => loadAgentKeys,
|
|
530
|
-
saveAgentKeys: () => saveAgentKeys,
|
|
531
|
-
signingPublicKeyToJWK: () => signingPublicKeyToJWK,
|
|
532
|
-
toBase64Url: () => toBase64Url,
|
|
533
|
-
validatePathId: () => validatePathId
|
|
534
|
-
});
|
|
535
|
-
function toBase64Url(bytes) {
|
|
536
|
-
return Buffer.from(bytes).toString("base64url");
|
|
537
|
-
}
|
|
538
|
-
function fromBase64Url(s) {
|
|
539
|
-
return new Uint8Array(Buffer.from(s, "base64url"));
|
|
540
|
-
}
|
|
541
|
-
function generateSigningKeyPair() {
|
|
542
|
-
const privateKey = ed25519.utils.randomSecretKey();
|
|
543
|
-
return {
|
|
544
|
-
privateKey,
|
|
545
|
-
publicKey: ed25519.getPublicKey(privateKey)
|
|
546
|
-
};
|
|
547
|
-
}
|
|
548
|
-
function generateEncryptionKeyPair() {
|
|
549
|
-
const privateKey = x25519.utils.randomSecretKey();
|
|
550
|
-
return {
|
|
551
|
-
privateKey,
|
|
552
|
-
publicKey: x25519.getPublicKey(privateKey)
|
|
553
|
-
};
|
|
554
|
-
}
|
|
555
|
-
function generateAgentKeys() {
|
|
556
|
-
return {
|
|
557
|
-
signing: generateSigningKeyPair(),
|
|
558
|
-
encryption: generateEncryptionKeyPair()
|
|
559
|
-
};
|
|
560
|
-
}
|
|
561
|
-
function signingPublicKeyToJWK(publicKey) {
|
|
562
|
-
return {
|
|
563
|
-
kty: "OKP",
|
|
564
|
-
crv: "Ed25519",
|
|
565
|
-
x: toBase64Url(publicKey)
|
|
566
|
-
};
|
|
567
|
-
}
|
|
568
|
-
function encryptionPublicKeyToJWK(publicKey) {
|
|
569
|
-
return {
|
|
570
|
-
kty: "OKP",
|
|
571
|
-
crv: "X25519",
|
|
572
|
-
x: toBase64Url(publicKey)
|
|
573
|
-
};
|
|
574
|
-
}
|
|
575
|
-
function jwkToPublicKey(jwk) {
|
|
576
|
-
const key = fromBase64Url(jwk.x);
|
|
577
|
-
if (key.length !== 32) throw new Error(`Invalid public key: expected 32 bytes, got ${key.length}`);
|
|
578
|
-
return key;
|
|
579
|
-
}
|
|
580
|
-
const KID_RE = /^rine:([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;
|
|
581
|
-
function agentIdFromKid(kid) {
|
|
582
|
-
const match = KID_RE.exec(kid);
|
|
583
|
-
if (!match) throw new Error(`Invalid sender KID format: ${kid}`);
|
|
584
|
-
return match[1];
|
|
585
|
-
}
|
|
586
|
-
const PATH_SAFE_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
587
|
-
function validatePathId(id, label) {
|
|
588
|
-
if (!PATH_SAFE_RE.test(id)) throw new Error(`Invalid ${label}: must be a UUID, got '${id}'`);
|
|
589
|
-
}
|
|
590
|
-
function getKeysDir$1() {
|
|
591
|
-
return join(getConfigDir(), "keys");
|
|
592
|
-
}
|
|
593
|
-
function agentKeysDir(agentId) {
|
|
594
|
-
validatePathId(agentId, "agent ID");
|
|
595
|
-
return join(getKeysDir$1(), agentId);
|
|
596
|
-
}
|
|
597
|
-
function saveAgentKeys(agentId, keys) {
|
|
598
|
-
const dir = agentKeysDir(agentId);
|
|
599
|
-
fs.mkdirSync(dir, {
|
|
600
|
-
recursive: true,
|
|
601
|
-
mode: 448
|
|
602
|
-
});
|
|
603
|
-
const sigPath = join(dir, "signing.key");
|
|
604
|
-
fs.writeFileSync(sigPath, toBase64Url(keys.signing.privateKey), "utf-8");
|
|
605
|
-
fs.chmodSync(sigPath, 384);
|
|
606
|
-
const encPath = join(dir, "encryption.key");
|
|
607
|
-
fs.writeFileSync(encPath, toBase64Url(keys.encryption.privateKey), "utf-8");
|
|
608
|
-
fs.chmodSync(encPath, 384);
|
|
609
|
-
}
|
|
610
|
-
function loadAgentKeys(agentId) {
|
|
611
|
-
const dir = agentKeysDir(agentId);
|
|
612
|
-
const sigPriv = fromBase64Url(fs.readFileSync(join(dir, "signing.key"), "utf-8").trim());
|
|
613
|
-
if (sigPriv.length !== 32) throw new Error(`Corrupt signing key: expected 32 bytes, got ${sigPriv.length}`);
|
|
614
|
-
const sigPub = ed25519.getPublicKey(sigPriv);
|
|
615
|
-
const encPriv = fromBase64Url(fs.readFileSync(join(dir, "encryption.key"), "utf-8").trim());
|
|
616
|
-
if (encPriv.length !== 32) throw new Error(`Corrupt encryption key: expected 32 bytes, got ${encPriv.length}`);
|
|
617
|
-
const encPub = x25519.getPublicKey(encPriv);
|
|
618
|
-
return {
|
|
619
|
-
signing: {
|
|
620
|
-
privateKey: sigPriv,
|
|
621
|
-
publicKey: sigPub
|
|
622
|
-
},
|
|
623
|
-
encryption: {
|
|
624
|
-
privateKey: encPriv,
|
|
625
|
-
publicKey: encPub
|
|
626
|
-
}
|
|
627
|
-
};
|
|
628
|
-
}
|
|
629
|
-
function agentKeysExist(agentId) {
|
|
630
|
-
const dir = agentKeysDir(agentId);
|
|
631
|
-
return fs.existsSync(join(dir, "signing.key")) && fs.existsSync(join(dir, "encryption.key"));
|
|
632
|
-
}
|
|
633
|
-
//#endregion
|
|
634
219
|
//#region src/prompt.ts
|
|
635
220
|
/** Prompt for a plain-text value on stdout/stdin. */
|
|
636
221
|
async function promptText(question) {
|
|
@@ -691,7 +276,7 @@ function toAgentRow(a, opts = {}) {
|
|
|
691
276
|
}
|
|
692
277
|
function registerAgent(program) {
|
|
693
278
|
const agent = program.command("agent").description("Agent management");
|
|
694
|
-
agent.command("create").description("Create a new agent").option("--name <name>", "Agent name").option("--[no-]human-oversight", "Require human oversight (default: true)").option("--unlisted", "Mark agent as unlisted (not in public directory)").action(withClient(program, async ({ client, gOpts }, opts) => {
|
|
279
|
+
agent.command("create").description("Create a new agent").option("--name <name>", "Agent name").option("--[no-]human-oversight", "Require human oversight (default: true)").option("--unlisted", "Mark agent as unlisted (not in public directory)").action(withClient(program, async ({ client, gOpts, configDir }, opts) => {
|
|
695
280
|
let name = opts.name;
|
|
696
281
|
if (!name) {
|
|
697
282
|
name = await promptText("Agent name: ");
|
|
@@ -710,13 +295,13 @@ function registerAgent(program) {
|
|
|
710
295
|
if (opts.humanOversight !== void 0) body.human_oversight = opts.humanOversight;
|
|
711
296
|
if (opts.unlisted) body.unlisted = true;
|
|
712
297
|
const data = await client.post("/agents", body);
|
|
713
|
-
saveAgentKeys(data.id, agentKeys);
|
|
298
|
+
saveAgentKeys(configDir, data.id, agentKeys);
|
|
714
299
|
if (data.poll_url) {
|
|
715
300
|
const profile = gOpts.profile ?? "default";
|
|
716
|
-
const creds = loadCredentials();
|
|
301
|
+
const creds = loadCredentials(configDir);
|
|
717
302
|
if (creds[profile]) {
|
|
718
303
|
creds[profile].poll_url = data.poll_url;
|
|
719
|
-
saveCredentials(creds);
|
|
304
|
+
saveCredentials(configDir, creds);
|
|
720
305
|
}
|
|
721
306
|
}
|
|
722
307
|
if (gOpts.json) printJson(data);
|
|
@@ -784,16 +369,18 @@ function registerAuth(program) {
|
|
|
784
369
|
program.command("login").description("Log in with OAuth2 client credentials").option("--client-id <id>", "Client ID").option("--client-secret <secret>", "Client secret").action(async (opts) => {
|
|
785
370
|
try {
|
|
786
371
|
const profile = program.opts().profile ?? "default";
|
|
372
|
+
const configDir = resolveConfigDir();
|
|
373
|
+
const apiUrl = resolveApiUrl();
|
|
787
374
|
const clientId = opts.clientId ?? await promptText("Client ID: ");
|
|
788
375
|
const clientSecret = opts.clientSecret ?? await promptPassword("Client secret: ");
|
|
789
|
-
const tokenData = await fetchOAuthToken(clientId, clientSecret);
|
|
790
|
-
const creds = loadCredentials();
|
|
376
|
+
const tokenData = await fetchOAuthToken(apiUrl, clientId, clientSecret);
|
|
377
|
+
const creds = loadCredentials(configDir);
|
|
791
378
|
creds[profile] = {
|
|
792
379
|
client_id: clientId,
|
|
793
380
|
client_secret: clientSecret
|
|
794
381
|
};
|
|
795
|
-
saveCredentials(creds);
|
|
796
|
-
cacheToken(profile, tokenData);
|
|
382
|
+
saveCredentials(configDir, creds);
|
|
383
|
+
cacheToken(configDir, profile, tokenData);
|
|
797
384
|
printSuccess(`Logged in as ${clientId}`);
|
|
798
385
|
} catch (err) {
|
|
799
386
|
printError(formatError(err));
|
|
@@ -802,9 +389,10 @@ function registerAuth(program) {
|
|
|
802
389
|
});
|
|
803
390
|
program.command("logout").description("Clear cached token. Credentials are preserved.").action(withErrorHandler(program, (gOpts) => {
|
|
804
391
|
const profile = gOpts.profile ?? "default";
|
|
805
|
-
const
|
|
392
|
+
const configDir = resolveConfigDir();
|
|
393
|
+
const cache = loadTokenCache(configDir);
|
|
806
394
|
delete cache[profile];
|
|
807
|
-
saveTokenCache(cache);
|
|
395
|
+
saveTokenCache(configDir, cache);
|
|
808
396
|
printSuccess(`Logged out — token cache cleared for profile '${profile}'. Credentials preserved.`);
|
|
809
397
|
}));
|
|
810
398
|
program.command("status").description("Show org status").action(withClient(program, async ({ client, gOpts }) => {
|
|
@@ -813,7 +401,9 @@ function registerAuth(program) {
|
|
|
813
401
|
program.command("whoami").description("Show current identity and status").action(async () => {
|
|
814
402
|
const gOpts = program.opts();
|
|
815
403
|
const profile = gOpts.profile ?? "default";
|
|
816
|
-
const
|
|
404
|
+
const configDir = resolveConfigDir();
|
|
405
|
+
const apiUrl = resolveApiUrl();
|
|
406
|
+
const entry = getCredentialEntry(configDir, profile);
|
|
817
407
|
if (!entry) {
|
|
818
408
|
if (gOpts.json) printJson({ logged_in: false });
|
|
819
409
|
else printTable([{
|
|
@@ -823,9 +413,10 @@ function registerAuth(program) {
|
|
|
823
413
|
return;
|
|
824
414
|
}
|
|
825
415
|
try {
|
|
826
|
-
const token = await getOrRefreshToken(entry, profile);
|
|
416
|
+
const token = await getOrRefreshToken(configDir, apiUrl, entry, profile);
|
|
827
417
|
const client = new HttpClient({
|
|
828
418
|
tokenFn: () => Promise.resolve(token),
|
|
419
|
+
apiUrl,
|
|
829
420
|
canRefresh: false,
|
|
830
421
|
defaultHeaders: {}
|
|
831
422
|
});
|
|
@@ -834,9 +425,10 @@ function registerAuth(program) {
|
|
|
834
425
|
let agentId;
|
|
835
426
|
let keysLabel;
|
|
836
427
|
try {
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
428
|
+
const agents = await fetchAgents(client);
|
|
429
|
+
agentId = await resolveAgent(apiUrl, agents, void 0, gOpts.as);
|
|
430
|
+
agentHandle = agents.find((a) => a.id === agentId)?.handle;
|
|
431
|
+
const hasLocal = agentKeysExist(configDir, agentId);
|
|
840
432
|
let hasServer = false;
|
|
841
433
|
try {
|
|
842
434
|
await client.get(`/agents/${agentId}/keys`);
|
|
@@ -846,7 +438,7 @@ function registerAuth(program) {
|
|
|
846
438
|
} catch {}
|
|
847
439
|
if (gOpts.json) printJson({
|
|
848
440
|
profile,
|
|
849
|
-
api:
|
|
441
|
+
api: apiUrl,
|
|
850
442
|
org: {
|
|
851
443
|
name: org.name,
|
|
852
444
|
slug: org.slug
|
|
@@ -866,7 +458,7 @@ function registerAuth(program) {
|
|
|
866
458
|
},
|
|
867
459
|
{
|
|
868
460
|
Field: "API",
|
|
869
|
-
Value:
|
|
461
|
+
Value: apiUrl
|
|
870
462
|
},
|
|
871
463
|
{
|
|
872
464
|
Field: "Org",
|
|
@@ -902,11 +494,16 @@ function registerAuth(program) {
|
|
|
902
494
|
authGroup.command("token").description("Get or refresh access token").option("--force", "Force token refresh (bypass cache)").action(withErrorHandler(program, async (gOpts, opts) => {
|
|
903
495
|
const profile = gOpts.profile ?? "default";
|
|
904
496
|
const force = opts.force === true;
|
|
497
|
+
const configDir = resolveConfigDir();
|
|
498
|
+
const apiUrl = resolveApiUrl();
|
|
905
499
|
const now = Date.now() / 1e3;
|
|
906
500
|
const envToken = process.env.RINE_TOKEN;
|
|
907
|
-
const cached = loadTokenCache()[profile];
|
|
501
|
+
const cached = loadTokenCache(configDir)[profile];
|
|
908
502
|
const source = envToken !== void 0 || !force && cached !== void 0 && cached.expires_at - now > 60 ? "cache" : "server";
|
|
909
|
-
const token = await getOrRefreshToken(getCredentialEntry(profile), profile,
|
|
503
|
+
const token = await getOrRefreshToken(configDir, apiUrl, getCredentialEntry(configDir, profile), profile, {
|
|
504
|
+
force,
|
|
505
|
+
envToken
|
|
506
|
+
});
|
|
910
507
|
if (gOpts.json) printJson({
|
|
911
508
|
access_token: token,
|
|
912
509
|
source
|
|
@@ -946,11 +543,12 @@ async function showOrgStatus(client, gOpts) {
|
|
|
946
543
|
//#region src/commands/discover-groups.ts
|
|
947
544
|
function registerDiscoverGroups(discover, program) {
|
|
948
545
|
discover.command("groups").description("List public groups in the directory").option("-q, --query <query>", "Search query").option("--limit <n>", "Max results").option("--cursor <cursor>", "Pagination cursor").action(withErrorHandler(program, async (gOpts, opts) => {
|
|
546
|
+
const apiUrl = resolveApiUrl();
|
|
949
547
|
const params = new URLSearchParams();
|
|
950
548
|
if (opts.query) params.append("q", opts.query);
|
|
951
549
|
if (opts.limit) params.append("limit", opts.limit);
|
|
952
550
|
if (opts.cursor) params.append("cursor", opts.cursor);
|
|
953
|
-
const data = await HttpClient.publicGet("/directory/groups", params.toString() ? params : void 0);
|
|
551
|
+
const data = await HttpClient.publicGet(apiUrl, "/directory/groups", params.toString() ? params : void 0);
|
|
954
552
|
if (gOpts.json) {
|
|
955
553
|
printJson(data);
|
|
956
554
|
return;
|
|
@@ -963,7 +561,8 @@ function registerDiscoverGroups(discover, program) {
|
|
|
963
561
|
Members: g.member_count
|
|
964
562
|
})));
|
|
965
563
|
})).command("inspect").description("Inspect a public group").argument("<group-id>", "Group ID").action(withErrorHandler(program, async (gOpts, groupId) => {
|
|
966
|
-
const
|
|
564
|
+
const apiUrl = resolveApiUrl();
|
|
565
|
+
const data = await HttpClient.publicGet(apiUrl, `/directory/groups/${groupId}`);
|
|
967
566
|
if (gOpts.json) {
|
|
968
567
|
printJson(data);
|
|
969
568
|
return;
|
|
@@ -990,6 +589,7 @@ function fmtList(v) {
|
|
|
990
589
|
return Array.isArray(v) ? v.join(", ") : String(v ?? "");
|
|
991
590
|
}
|
|
992
591
|
async function doAgentSearch(opts, gOpts) {
|
|
592
|
+
const apiUrl = resolveApiUrl();
|
|
993
593
|
const params = new URLSearchParams();
|
|
994
594
|
if (opts.query) params.append("q", opts.query);
|
|
995
595
|
for (const c of opts.category) params.append("category", c);
|
|
@@ -1001,7 +601,7 @@ async function doAgentSearch(opts, gOpts) {
|
|
|
1001
601
|
if (opts.limit) params.append("limit", opts.limit);
|
|
1002
602
|
if (opts.cursor) params.append("cursor", opts.cursor);
|
|
1003
603
|
if (opts.sort) params.append("sort", opts.sort);
|
|
1004
|
-
const data = await HttpClient.publicGet("/directory/agents", params);
|
|
604
|
+
const data = await HttpClient.publicGet(apiUrl, "/directory/agents", params);
|
|
1005
605
|
if (gOpts.json) {
|
|
1006
606
|
printJson(data);
|
|
1007
607
|
return;
|
|
@@ -1030,7 +630,8 @@ function registerDiscover(program) {
|
|
|
1030
630
|
addAgentSearchCommand(discover, "agents", "List agents in the directory", program, false);
|
|
1031
631
|
addAgentSearchCommand(discover, "search", "Search agents (--query required)", program, true);
|
|
1032
632
|
discover.command("categories").description("List directory categories with counts").action(withErrorHandler(program, async (gOpts) => {
|
|
1033
|
-
const
|
|
633
|
+
const apiUrl = resolveApiUrl();
|
|
634
|
+
const data = await HttpClient.publicGet(apiUrl, "/directory/categories");
|
|
1034
635
|
if (gOpts.json) {
|
|
1035
636
|
printJson(data);
|
|
1036
637
|
return;
|
|
@@ -1041,15 +642,15 @@ function registerDiscover(program) {
|
|
|
1041
642
|
})));
|
|
1042
643
|
}));
|
|
1043
644
|
discover.command("inspect").description("Inspect a specific agent in the directory").argument("<agent-id>", "Agent UUID or handle").action(withErrorHandler(program, async (gOpts, agentIdArg) => {
|
|
1044
|
-
const
|
|
645
|
+
const apiUrl = resolveApiUrl();
|
|
1045
646
|
let resolvedId = agentIdArg;
|
|
1046
|
-
if (agentIdArg.includes("@")) resolvedId = await resolveHandleViaWebFinger(agentIdArg);
|
|
647
|
+
if (agentIdArg.includes("@")) resolvedId = await resolveHandleViaWebFinger(apiUrl, agentIdArg);
|
|
1047
648
|
if (!UUID_RE.test(resolvedId)) {
|
|
1048
649
|
printError(`Could not resolve '${agentIdArg}' to an agent UUID. Provide a UUID or a valid handle (e.g. agent@org.rine.network).`);
|
|
1049
650
|
process.exitCode = 1;
|
|
1050
651
|
return;
|
|
1051
652
|
}
|
|
1052
|
-
const data = await HttpClient.publicGet(`/directory/agents/${resolvedId}`);
|
|
653
|
+
const data = await HttpClient.publicGet(apiUrl, `/directory/agents/${resolvedId}`);
|
|
1053
654
|
if (gOpts.json) {
|
|
1054
655
|
printJson(data);
|
|
1055
656
|
return;
|
|
@@ -1187,18 +788,18 @@ function registerGroup(program) {
|
|
|
1187
788
|
printText("Join request submitted (pending approval)");
|
|
1188
789
|
}
|
|
1189
790
|
}));
|
|
1190
|
-
group.command("leave").description("Leave a group").argument("<group-id>", "Group ID").option("--agent <id>", "Agent ID (defaults to your agent)").action(withClient(program, async ({ client, gOpts }, groupId, opts) => {
|
|
1191
|
-
const agentId = await resolveAgent(client, opts.agent, gOpts.as);
|
|
791
|
+
group.command("leave").description("Leave a group").argument("<group-id>", "Group ID").option("--agent <id>", "Agent ID (defaults to your agent)").action(withClient(program, async ({ client, gOpts, apiUrl }, groupId, opts) => {
|
|
792
|
+
const agentId = await resolveAgent(apiUrl, await fetchAgents(client), opts.agent, gOpts.as);
|
|
1192
793
|
await client.delete(`/groups/${groupId}/members/${agentId}`);
|
|
1193
794
|
printMutationOk("Left group", gOpts.json);
|
|
1194
795
|
}));
|
|
1195
|
-
group.command("kick").description("Remove a member from the group (admin)").argument("<group-id>", "Group ID").argument("<agent-id>", "Agent ID or handle to remove").action(withClient(program, async ({ client, gOpts }, groupId, agentId) => {
|
|
1196
|
-
const resolved = await resolveToUuid(agentId);
|
|
796
|
+
group.command("kick").description("Remove a member from the group (admin)").argument("<group-id>", "Group ID").argument("<agent-id>", "Agent ID or handle to remove").action(withClient(program, async ({ client, gOpts, apiUrl }, groupId, agentId) => {
|
|
797
|
+
const resolved = await resolveToUuid(apiUrl, agentId);
|
|
1197
798
|
await client.delete(`/groups/${groupId}/members/${resolved}`);
|
|
1198
799
|
printMutationOk("Member removed", gOpts.json);
|
|
1199
800
|
}));
|
|
1200
|
-
group.command("invite").description("Invite an agent to the group").argument("<group-id>", "Group ID").requiredOption("--agent <id>", "Agent ID to invite").option("--message <msg>", "Invitation message").action(withClient(program, async ({ client, gOpts }, groupId, opts) => {
|
|
1201
|
-
const body = { agent_id: await resolveToUuid(opts.agent) };
|
|
801
|
+
group.command("invite").description("Invite an agent to the group").argument("<group-id>", "Group ID").requiredOption("--agent <id>", "Agent ID to invite").option("--message <msg>", "Invitation message").action(withClient(program, async ({ client, gOpts, apiUrl }, groupId, opts) => {
|
|
802
|
+
const body = { agent_id: await resolveToUuid(apiUrl, opts.agent) };
|
|
1202
803
|
if (opts.message) body.message = opts.message;
|
|
1203
804
|
const data = await client.post(`/groups/${groupId}/invite`, body);
|
|
1204
805
|
if (gOpts.json) printJson(data);
|
|
@@ -1224,401 +825,12 @@ function registerGroup(program) {
|
|
|
1224
825
|
}));
|
|
1225
826
|
}
|
|
1226
827
|
//#endregion
|
|
1227
|
-
//#region src/crypto/envelope.ts
|
|
1228
|
-
const SIGNATURE_SIZE = 64;
|
|
1229
|
-
const MAX_KID_LENGTH = 255;
|
|
1230
|
-
function encodeEnvelope(kid, signature, payload) {
|
|
1231
|
-
const kidBytes = new TextEncoder().encode(kid);
|
|
1232
|
-
if (kidBytes.length > MAX_KID_LENGTH) throw new Error(`kid too long: ${kidBytes.length} bytes (max ${MAX_KID_LENGTH})`);
|
|
1233
|
-
if (signature.length !== SIGNATURE_SIZE) throw new Error(`signature must be ${SIGNATURE_SIZE} bytes, got ${signature.length}`);
|
|
1234
|
-
const buf = new Uint8Array(1 + kidBytes.length + SIGNATURE_SIZE + payload.length);
|
|
1235
|
-
buf[0] = kidBytes.length;
|
|
1236
|
-
buf.set(kidBytes, 1);
|
|
1237
|
-
buf.set(signature, 1 + kidBytes.length);
|
|
1238
|
-
buf.set(payload, 1 + kidBytes.length + SIGNATURE_SIZE);
|
|
1239
|
-
return buf;
|
|
1240
|
-
}
|
|
1241
|
-
function decodeEnvelope(data) {
|
|
1242
|
-
if (data.length < 1 + SIGNATURE_SIZE) throw new Error("envelope too short");
|
|
1243
|
-
const kidLen = data[0];
|
|
1244
|
-
const minLen = 1 + kidLen + SIGNATURE_SIZE;
|
|
1245
|
-
if (data.length < minLen) throw new Error("envelope too short for declared kid length");
|
|
1246
|
-
return {
|
|
1247
|
-
kid: new TextDecoder("utf-8", { fatal: true }).decode(data.subarray(1, 1 + kidLen)),
|
|
1248
|
-
signature: data.slice(1 + kidLen, 1 + kidLen + SIGNATURE_SIZE),
|
|
1249
|
-
payload: data.slice(1 + kidLen + SIGNATURE_SIZE)
|
|
1250
|
-
};
|
|
1251
|
-
}
|
|
1252
|
-
//#endregion
|
|
1253
|
-
//#region src/crypto/hpke.ts
|
|
1254
|
-
const VERSION_HPKE = 1;
|
|
1255
|
-
const ENC_SIZE = 32;
|
|
1256
|
-
const INFO = new TextEncoder().encode("rine.e2ee.v1.hpke");
|
|
1257
|
-
const suite = new CipherSuite({
|
|
1258
|
-
kem: new DhkemX25519HkdfSha256(),
|
|
1259
|
-
kdf: new HkdfSha256(),
|
|
1260
|
-
aead: new Aes256Gcm()
|
|
1261
|
-
});
|
|
1262
|
-
async function seal(recipientPublicKey, innerEnvelope, aad) {
|
|
1263
|
-
const pk = await suite.kem.deserializePublicKey(recipientPublicKey);
|
|
1264
|
-
const sender = await suite.createSenderContext({
|
|
1265
|
-
recipientPublicKey: pk,
|
|
1266
|
-
info: INFO
|
|
1267
|
-
});
|
|
1268
|
-
const ciphertext = new Uint8Array(await sender.seal(innerEnvelope, aad));
|
|
1269
|
-
const enc = new Uint8Array(sender.enc);
|
|
1270
|
-
const out = new Uint8Array(1 + ENC_SIZE + ciphertext.length);
|
|
1271
|
-
out[0] = VERSION_HPKE;
|
|
1272
|
-
out.set(enc, 1);
|
|
1273
|
-
out.set(ciphertext, 1 + ENC_SIZE);
|
|
1274
|
-
return out;
|
|
1275
|
-
}
|
|
1276
|
-
async function open(recipientPrivateKey, encryptedPayload, aad) {
|
|
1277
|
-
if (encryptedPayload.length < 1 + ENC_SIZE + 1) throw new Error("encrypted payload too short");
|
|
1278
|
-
const version = encryptedPayload[0];
|
|
1279
|
-
if (version !== VERSION_HPKE) throw new Error(`unsupported version: 0x${version?.toString(16).padStart(2, "0")}`);
|
|
1280
|
-
const enc = encryptedPayload.slice(1, 1 + ENC_SIZE);
|
|
1281
|
-
const ct = encryptedPayload.slice(1 + ENC_SIZE);
|
|
1282
|
-
const sk = await suite.kem.deserializePrivateKey(recipientPrivateKey);
|
|
1283
|
-
const recipient = await suite.createRecipientContext({
|
|
1284
|
-
recipientKey: sk,
|
|
1285
|
-
enc,
|
|
1286
|
-
info: INFO
|
|
1287
|
-
});
|
|
1288
|
-
return new Uint8Array(await recipient.open(ct, aad));
|
|
1289
|
-
}
|
|
1290
|
-
//#endregion
|
|
1291
|
-
//#region src/crypto/sender-keys-helpers.ts
|
|
1292
|
-
function uuidToBytes(uuid) {
|
|
1293
|
-
const matches = uuid.replace(/-/g, "").match(/../g);
|
|
1294
|
-
if (!matches) throw new Error(`Invalid UUID: ${uuid}`);
|
|
1295
|
-
return new Uint8Array(matches.map((b) => Number.parseInt(b, 16)));
|
|
1296
|
-
}
|
|
1297
|
-
function bytesToUuid(bytes) {
|
|
1298
|
-
const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1299
|
-
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
1300
|
-
}
|
|
1301
|
-
function senderKeysDir(agentId) {
|
|
1302
|
-
validatePathId(agentId, "agent ID");
|
|
1303
|
-
return join(getConfigDir(), "keys", agentId, "sender_keys");
|
|
1304
|
-
}
|
|
1305
|
-
function saveSenderKeyState(agentId, groupId, states) {
|
|
1306
|
-
validatePathId(groupId, "group ID");
|
|
1307
|
-
const dir = senderKeysDir(agentId);
|
|
1308
|
-
fs.mkdirSync(dir, {
|
|
1309
|
-
recursive: true,
|
|
1310
|
-
mode: 448
|
|
1311
|
-
});
|
|
1312
|
-
const filePath = join(dir, `${groupId}.json`);
|
|
1313
|
-
const data = states.map((s) => ({
|
|
1314
|
-
...s,
|
|
1315
|
-
chainKey: toBase64Url(s.chainKey)
|
|
1316
|
-
}));
|
|
1317
|
-
fs.writeFileSync(filePath, JSON.stringify(data), "utf-8");
|
|
1318
|
-
fs.chmodSync(filePath, 384);
|
|
1319
|
-
}
|
|
1320
|
-
function loadSenderKeyStates(agentId, groupId) {
|
|
1321
|
-
validatePathId(groupId, "group ID");
|
|
1322
|
-
const filePath = join(senderKeysDir(agentId), `${groupId}.json`);
|
|
1323
|
-
if (!fs.existsSync(filePath)) return [];
|
|
1324
|
-
const raw = fs.readFileSync(filePath, "utf-8");
|
|
1325
|
-
return JSON.parse(raw).map((s) => ({
|
|
1326
|
-
...s,
|
|
1327
|
-
chainKey: fromBase64Url(s.chainKey)
|
|
1328
|
-
}));
|
|
1329
|
-
}
|
|
1330
|
-
//#endregion
|
|
1331
|
-
//#region src/crypto/sign.ts
|
|
1332
|
-
function signPayload(signingPrivateKey, plaintext) {
|
|
1333
|
-
return ed25519.sign(plaintext, signingPrivateKey);
|
|
1334
|
-
}
|
|
1335
|
-
function verifySignature(signingPublicKey, plaintext, signature) {
|
|
1336
|
-
try {
|
|
1337
|
-
return ed25519.verify(signature, plaintext, signingPublicKey);
|
|
1338
|
-
} catch {
|
|
1339
|
-
return false;
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1342
|
-
//#endregion
|
|
1343
|
-
//#region src/crypto/sender-keys.ts
|
|
1344
|
-
const ROTATION_MESSAGE_LIMIT = 100;
|
|
1345
|
-
const ROTATION_AGE_MS = 10080 * 60 * 1e3;
|
|
1346
|
-
function needsRotation(state) {
|
|
1347
|
-
if (state.messageIndex >= ROTATION_MESSAGE_LIMIT) return true;
|
|
1348
|
-
if (state.createdAt && Date.now() - state.createdAt > ROTATION_AGE_MS) return true;
|
|
1349
|
-
return false;
|
|
1350
|
-
}
|
|
1351
|
-
const VERSION_SENDER_KEY = 2;
|
|
1352
|
-
const SENDER_KEY_ID_SIZE = 16;
|
|
1353
|
-
const MESSAGE_INDEX_SIZE = 4;
|
|
1354
|
-
const NONCE_SIZE = 12;
|
|
1355
|
-
const MAX_SKIP = 1e3;
|
|
1356
|
-
/** Copy Uint8Array into a plain ArrayBuffer-backed Uint8Array (required by Web Crypto). */
|
|
1357
|
-
function toPlainBuffer(src) {
|
|
1358
|
-
const buf = new Uint8Array(src.length);
|
|
1359
|
-
buf.set(src);
|
|
1360
|
-
return buf;
|
|
1361
|
-
}
|
|
1362
|
-
function generateSenderKey(groupId, senderAgentId) {
|
|
1363
|
-
const chainKey = new Uint8Array(32);
|
|
1364
|
-
crypto.getRandomValues(chainKey);
|
|
1365
|
-
return {
|
|
1366
|
-
senderKeyId: crypto.randomUUID(),
|
|
1367
|
-
groupId,
|
|
1368
|
-
senderAgentId,
|
|
1369
|
-
chainKey,
|
|
1370
|
-
messageIndex: 0,
|
|
1371
|
-
createdAt: Date.now()
|
|
1372
|
-
};
|
|
1373
|
-
}
|
|
1374
|
-
function deriveMessageKey(chainKey) {
|
|
1375
|
-
return {
|
|
1376
|
-
messageKey: hmac(sha256, chainKey, new Uint8Array([1])),
|
|
1377
|
-
nextChainKey: hmac(sha256, chainKey, new Uint8Array([2]))
|
|
1378
|
-
};
|
|
1379
|
-
}
|
|
1380
|
-
function advanceChain(state, targetIndex) {
|
|
1381
|
-
if (targetIndex - state.messageIndex > MAX_SKIP) throw new Error(`too many skipped messages: ${targetIndex - state.messageIndex} (max ${MAX_SKIP})`);
|
|
1382
|
-
const skippedKeys = /* @__PURE__ */ new Map();
|
|
1383
|
-
let chainKey = state.chainKey;
|
|
1384
|
-
let index = state.messageIndex;
|
|
1385
|
-
while (index < targetIndex) {
|
|
1386
|
-
const { messageKey, nextChainKey } = deriveMessageKey(chainKey);
|
|
1387
|
-
skippedKeys.set(index, messageKey);
|
|
1388
|
-
chainKey = nextChainKey;
|
|
1389
|
-
index++;
|
|
1390
|
-
}
|
|
1391
|
-
return {
|
|
1392
|
-
updatedState: {
|
|
1393
|
-
...state,
|
|
1394
|
-
chainKey,
|
|
1395
|
-
messageIndex: index
|
|
1396
|
-
},
|
|
1397
|
-
skippedKeys
|
|
1398
|
-
};
|
|
1399
|
-
}
|
|
1400
|
-
/** Build AAD for sender-key encryption: binds ciphertext to sender_key_id and message_index. */
|
|
1401
|
-
function buildSenderKeyAad(senderKeyId, messageIndex) {
|
|
1402
|
-
const aad = new Uint8Array(SENDER_KEY_ID_SIZE + MESSAGE_INDEX_SIZE);
|
|
1403
|
-
aad.set(senderKeyId, 0);
|
|
1404
|
-
new DataView(aad.buffer).setUint32(SENDER_KEY_ID_SIZE, messageIndex, false);
|
|
1405
|
-
return aad;
|
|
1406
|
-
}
|
|
1407
|
-
async function sealGroup(senderSigningPrivateKey, senderSigningKid, state, plaintext) {
|
|
1408
|
-
const innerEnvelope = encodeEnvelope(senderSigningKid, signPayload(senderSigningPrivateKey, plaintext), plaintext);
|
|
1409
|
-
const { messageKey, nextChainKey } = deriveMessageKey(state.chainKey);
|
|
1410
|
-
const nonce = new Uint8Array(NONCE_SIZE);
|
|
1411
|
-
crypto.getRandomValues(nonce);
|
|
1412
|
-
const idBytes = uuidToBytes(state.senderKeyId);
|
|
1413
|
-
const aad = buildSenderKeyAad(idBytes, state.messageIndex);
|
|
1414
|
-
const cryptoKey = await crypto.subtle.importKey("raw", toPlainBuffer(messageKey), "AES-GCM", false, ["encrypt"]);
|
|
1415
|
-
const ciphertext = new Uint8Array(await crypto.subtle.encrypt({
|
|
1416
|
-
name: "AES-GCM",
|
|
1417
|
-
iv: nonce,
|
|
1418
|
-
additionalData: toPlainBuffer(aad)
|
|
1419
|
-
}, cryptoKey, toPlainBuffer(innerEnvelope)));
|
|
1420
|
-
messageKey.fill(0);
|
|
1421
|
-
const out = new Uint8Array(1 + SENDER_KEY_ID_SIZE + MESSAGE_INDEX_SIZE + NONCE_SIZE + ciphertext.length);
|
|
1422
|
-
let offset = 0;
|
|
1423
|
-
out[offset++] = VERSION_SENDER_KEY;
|
|
1424
|
-
out.set(idBytes, offset);
|
|
1425
|
-
offset += SENDER_KEY_ID_SIZE;
|
|
1426
|
-
new DataView(out.buffer).setUint32(offset, state.messageIndex, false);
|
|
1427
|
-
offset += MESSAGE_INDEX_SIZE;
|
|
1428
|
-
out.set(nonce, offset);
|
|
1429
|
-
offset += NONCE_SIZE;
|
|
1430
|
-
out.set(ciphertext, offset);
|
|
1431
|
-
return {
|
|
1432
|
-
encryptedPayload: out,
|
|
1433
|
-
updatedState: {
|
|
1434
|
-
...state,
|
|
1435
|
-
chainKey: nextChainKey,
|
|
1436
|
-
messageIndex: state.messageIndex + 1
|
|
1437
|
-
}
|
|
1438
|
-
};
|
|
1439
|
-
}
|
|
1440
|
-
async function openGroup(senderKeyStates, encryptedPayload) {
|
|
1441
|
-
const minLen = 1 + SENDER_KEY_ID_SIZE + MESSAGE_INDEX_SIZE + NONCE_SIZE;
|
|
1442
|
-
if (encryptedPayload.length < minLen) throw new Error("encrypted payload too short");
|
|
1443
|
-
const version = encryptedPayload[0];
|
|
1444
|
-
if (version !== VERSION_SENDER_KEY) throw new Error(`unsupported version: 0x${version?.toString(16).padStart(2, "0")}`);
|
|
1445
|
-
let offset = 1;
|
|
1446
|
-
const senderKeyId = bytesToUuid(encryptedPayload.slice(offset, offset + SENDER_KEY_ID_SIZE));
|
|
1447
|
-
offset += SENDER_KEY_ID_SIZE;
|
|
1448
|
-
const messageIndex = new DataView(encryptedPayload.buffer, encryptedPayload.byteOffset).getUint32(offset, false);
|
|
1449
|
-
offset += MESSAGE_INDEX_SIZE;
|
|
1450
|
-
const nonce = encryptedPayload.slice(offset, offset + NONCE_SIZE);
|
|
1451
|
-
offset += NONCE_SIZE;
|
|
1452
|
-
const ciphertext = encryptedPayload.slice(offset);
|
|
1453
|
-
const state = senderKeyStates.get(senderKeyId);
|
|
1454
|
-
if (!state) throw new Error(`unknown sender key id: ${senderKeyId}`);
|
|
1455
|
-
let messageKey;
|
|
1456
|
-
let updatedState;
|
|
1457
|
-
if (messageIndex < state.messageIndex) {
|
|
1458
|
-
const cached = state.skippedKeys?.[messageIndex];
|
|
1459
|
-
if (!cached) throw new Error("message key expired or already consumed");
|
|
1460
|
-
const { fromBase64Url } = await Promise.resolve().then(() => keys_exports);
|
|
1461
|
-
messageKey = fromBase64Url(cached);
|
|
1462
|
-
const newSkipped = { ...state.skippedKeys };
|
|
1463
|
-
delete newSkipped[messageIndex];
|
|
1464
|
-
updatedState = {
|
|
1465
|
-
...state,
|
|
1466
|
-
skippedKeys: newSkipped
|
|
1467
|
-
};
|
|
1468
|
-
} else if (messageIndex === state.messageIndex) {
|
|
1469
|
-
const { messageKey: mk, nextChainKey } = deriveMessageKey(state.chainKey);
|
|
1470
|
-
messageKey = mk;
|
|
1471
|
-
updatedState = {
|
|
1472
|
-
...state,
|
|
1473
|
-
chainKey: nextChainKey,
|
|
1474
|
-
messageIndex: state.messageIndex + 1
|
|
1475
|
-
};
|
|
1476
|
-
} else {
|
|
1477
|
-
const { updatedState: advanced, skippedKeys: newSkipped } = advanceChain(state, messageIndex);
|
|
1478
|
-
const { messageKey: mk, nextChainKey } = deriveMessageKey(advanced.chainKey);
|
|
1479
|
-
messageKey = mk;
|
|
1480
|
-
const mergedSkipped = { ...state.skippedKeys ?? {} };
|
|
1481
|
-
for (const [idx, key] of newSkipped) {
|
|
1482
|
-
mergedSkipped[idx] = toBase64Url(key);
|
|
1483
|
-
key.fill(0);
|
|
1484
|
-
}
|
|
1485
|
-
const entries = Object.entries(mergedSkipped);
|
|
1486
|
-
if (entries.length > MAX_SKIP) {
|
|
1487
|
-
entries.sort((a, b) => Number(a[0]) - Number(b[0]));
|
|
1488
|
-
const excess = entries.length - MAX_SKIP;
|
|
1489
|
-
for (let i = 0; i < excess; i++) delete mergedSkipped[Number(entries[i][0])];
|
|
1490
|
-
}
|
|
1491
|
-
updatedState = {
|
|
1492
|
-
...advanced,
|
|
1493
|
-
chainKey: nextChainKey,
|
|
1494
|
-
messageIndex: messageIndex + 1,
|
|
1495
|
-
skippedKeys: mergedSkipped
|
|
1496
|
-
};
|
|
1497
|
-
}
|
|
1498
|
-
const aad = buildSenderKeyAad(encryptedPayload.slice(1, 1 + SENDER_KEY_ID_SIZE), messageIndex);
|
|
1499
|
-
const cryptoKey = await crypto.subtle.importKey("raw", toPlainBuffer(messageKey), "AES-GCM", false, ["decrypt"]);
|
|
1500
|
-
const innerEnvelope = new Uint8Array(await crypto.subtle.decrypt({
|
|
1501
|
-
name: "AES-GCM",
|
|
1502
|
-
iv: nonce,
|
|
1503
|
-
additionalData: toPlainBuffer(aad)
|
|
1504
|
-
}, cryptoKey, toPlainBuffer(ciphertext)));
|
|
1505
|
-
messageKey.fill(0);
|
|
1506
|
-
return {
|
|
1507
|
-
innerEnvelope,
|
|
1508
|
-
senderKeyId,
|
|
1509
|
-
messageIndex,
|
|
1510
|
-
updatedState
|
|
1511
|
-
};
|
|
1512
|
-
}
|
|
1513
|
-
//#endregion
|
|
1514
|
-
//#region src/crypto/message.ts
|
|
1515
|
-
async function verifyEnvelopeSender(decoded, client) {
|
|
1516
|
-
let verified = false;
|
|
1517
|
-
let verificationStatus = "unverifiable";
|
|
1518
|
-
try {
|
|
1519
|
-
const senderAgentId = agentIdFromKid(decoded.kid);
|
|
1520
|
-
verified = verifySignature(jwkToPublicKey((await client.get(`/agents/${senderAgentId}/keys`)).signing_public_key), decoded.payload, decoded.signature);
|
|
1521
|
-
verificationStatus = verified ? "verified" : "invalid";
|
|
1522
|
-
} catch {}
|
|
1523
|
-
return {
|
|
1524
|
-
verified,
|
|
1525
|
-
verificationStatus
|
|
1526
|
-
};
|
|
1527
|
-
}
|
|
1528
|
-
function signingKid(agentId) {
|
|
1529
|
-
return `rine:${agentId}`;
|
|
1530
|
-
}
|
|
1531
|
-
async function encryptMessage(senderAgentId, recipientEncryptionPk, payload) {
|
|
1532
|
-
const keys = loadAgentKeys(senderAgentId);
|
|
1533
|
-
const plaintext = new TextEncoder().encode(JSON.stringify(payload));
|
|
1534
|
-
const kid = signingKid(senderAgentId);
|
|
1535
|
-
return {
|
|
1536
|
-
encrypted_payload: toBase64Url(await seal(recipientEncryptionPk, encodeEnvelope(kid, signPayload(keys.signing.privateKey, plaintext), plaintext))),
|
|
1537
|
-
encryption_version: "hpke-v1",
|
|
1538
|
-
sender_signing_kid: kid
|
|
1539
|
-
};
|
|
1540
|
-
}
|
|
1541
|
-
async function fetchRecipientEncryptionKey(client, agentId) {
|
|
1542
|
-
return jwkToPublicKey((await client.get(`/agents/${agentId}/keys`)).encryption_public_key);
|
|
1543
|
-
}
|
|
1544
|
-
async function decryptMessage(recipientAgentId, encryptedPayloadB64, client) {
|
|
1545
|
-
const keys = loadAgentKeys(recipientAgentId);
|
|
1546
|
-
const encrypted = fromBase64Url(encryptedPayloadB64);
|
|
1547
|
-
const decoded = decodeEnvelope(await open(keys.encryption.privateKey, encrypted));
|
|
1548
|
-
const plaintext = new TextDecoder("utf-8", { fatal: true }).decode(decoded.payload);
|
|
1549
|
-
const { verified, verificationStatus } = await verifyEnvelopeSender(decoded, client);
|
|
1550
|
-
return {
|
|
1551
|
-
plaintext,
|
|
1552
|
-
senderKid: decoded.kid,
|
|
1553
|
-
verified,
|
|
1554
|
-
verificationStatus
|
|
1555
|
-
};
|
|
1556
|
-
}
|
|
1557
|
-
async function encryptGroupMessage(senderAgentId, groupId, senderKeyState, payload) {
|
|
1558
|
-
const keys = loadAgentKeys(senderAgentId);
|
|
1559
|
-
const plaintext = new TextEncoder().encode(JSON.stringify(payload));
|
|
1560
|
-
const kid = signingKid(senderAgentId);
|
|
1561
|
-
const selfReadKey = deriveMessageKey(senderKeyState.chainKey).messageKey;
|
|
1562
|
-
const selfReadIndex = senderKeyState.messageIndex;
|
|
1563
|
-
const { encryptedPayload, updatedState } = await sealGroup(keys.signing.privateKey, kid, senderKeyState, plaintext);
|
|
1564
|
-
updatedState.skippedKeys = {
|
|
1565
|
-
...updatedState.skippedKeys ?? {},
|
|
1566
|
-
[selfReadIndex]: toBase64Url(selfReadKey)
|
|
1567
|
-
};
|
|
1568
|
-
selfReadKey.fill(0);
|
|
1569
|
-
saveSenderKeyState(senderAgentId, groupId, [...loadSenderKeyStates(senderAgentId, groupId).filter((s) => s.senderKeyId !== updatedState.senderKeyId), updatedState]);
|
|
1570
|
-
return {
|
|
1571
|
-
result: {
|
|
1572
|
-
encrypted_payload: toBase64Url(encryptedPayload),
|
|
1573
|
-
encryption_version: "sender-key-v1",
|
|
1574
|
-
sender_signing_kid: kid
|
|
1575
|
-
},
|
|
1576
|
-
updatedState
|
|
1577
|
-
};
|
|
1578
|
-
}
|
|
1579
|
-
async function decryptGroupMessage(recipientAgentId, groupId, encryptedPayloadB64, client) {
|
|
1580
|
-
const states = loadSenderKeyStates(recipientAgentId, groupId);
|
|
1581
|
-
const stateMap = /* @__PURE__ */ new Map();
|
|
1582
|
-
for (const s of states) stateMap.set(s.senderKeyId, s);
|
|
1583
|
-
const { innerEnvelope, updatedState, messageIndex } = await openGroup(stateMap, fromBase64Url(encryptedPayloadB64));
|
|
1584
|
-
const decoded = decodeEnvelope(innerEnvelope);
|
|
1585
|
-
const plaintext = new TextDecoder("utf-8", { fatal: true }).decode(decoded.payload);
|
|
1586
|
-
const { verified, verificationStatus } = await verifyEnvelopeSender(decoded, client);
|
|
1587
|
-
if (verificationStatus === "invalid") throw new Error("Sender signature verification failed");
|
|
1588
|
-
if (updatedState.senderAgentId === recipientAgentId) {
|
|
1589
|
-
const prevCached = stateMap.get(updatedState.senderKeyId)?.skippedKeys?.[messageIndex];
|
|
1590
|
-
if (prevCached && !updatedState.skippedKeys?.[messageIndex]) updatedState.skippedKeys = {
|
|
1591
|
-
...updatedState.skippedKeys ?? {},
|
|
1592
|
-
[messageIndex]: prevCached
|
|
1593
|
-
};
|
|
1594
|
-
}
|
|
1595
|
-
stateMap.set(updatedState.senderKeyId, updatedState);
|
|
1596
|
-
saveSenderKeyState(recipientAgentId, groupId, Array.from(stateMap.values()));
|
|
1597
|
-
return {
|
|
1598
|
-
plaintext,
|
|
1599
|
-
senderKid: decoded.kid,
|
|
1600
|
-
verified,
|
|
1601
|
-
verificationStatus
|
|
1602
|
-
};
|
|
1603
|
-
}
|
|
1604
|
-
function getAgentPublicKeys(agentId, agentKeys) {
|
|
1605
|
-
const keys = agentKeys ?? loadAgentKeys(agentId);
|
|
1606
|
-
return {
|
|
1607
|
-
signing_public_key: signingPublicKeyToJWK(keys.signing.publicKey),
|
|
1608
|
-
encryption_public_key: {
|
|
1609
|
-
kty: "OKP",
|
|
1610
|
-
crv: "X25519",
|
|
1611
|
-
x: toBase64Url(keys.encryption.publicKey)
|
|
1612
|
-
}
|
|
1613
|
-
};
|
|
1614
|
-
}
|
|
1615
|
-
//#endregion
|
|
1616
828
|
//#region src/commands/keys.ts
|
|
1617
|
-
function getKeysDir(agentId) {
|
|
1618
|
-
return join(
|
|
829
|
+
function getKeysDir(configDir, agentId) {
|
|
830
|
+
return join(configDir, "keys", agentId);
|
|
1619
831
|
}
|
|
1620
|
-
function backupKeys(agentId) {
|
|
1621
|
-
const dir = getKeysDir(agentId);
|
|
832
|
+
function backupKeys(configDir, agentId) {
|
|
833
|
+
const dir = getKeysDir(configDir, agentId);
|
|
1622
834
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1623
835
|
for (const name of ["signing.key", "encryption.key"]) {
|
|
1624
836
|
const src = join(dir, name);
|
|
@@ -1633,10 +845,11 @@ async function verifyServerKeys(client, agentId, expected) {
|
|
|
1633
845
|
}
|
|
1634
846
|
function registerKeys(program) {
|
|
1635
847
|
const keys = program.command("keys").description("E2EE key management");
|
|
1636
|
-
keys.command("status").description("Show E2EE key status for an agent").option("--agent <id>", "Agent ID").action(withClient(program, async ({ client, gOpts }, opts) => {
|
|
1637
|
-
const
|
|
1638
|
-
const
|
|
1639
|
-
const
|
|
848
|
+
keys.command("status").description("Show E2EE key status for an agent").option("--agent <id>", "Agent ID").action(withClient(program, async ({ client, gOpts, configDir, apiUrl }, opts) => {
|
|
849
|
+
const agents = await fetchAgents(client);
|
|
850
|
+
const agentId = await resolveAgent(apiUrl, agents, opts.agent, gOpts.as);
|
|
851
|
+
const handle = agents.find((a) => a.id === agentId)?.handle ?? "unknown";
|
|
852
|
+
const hasLocal = agentKeysExist(configDir, agentId);
|
|
1640
853
|
let hasServer = false;
|
|
1641
854
|
let serverKeys;
|
|
1642
855
|
try {
|
|
@@ -1648,9 +861,9 @@ function registerKeys(program) {
|
|
|
1648
861
|
let sigFingerprint = "-";
|
|
1649
862
|
let encFingerprint = "-";
|
|
1650
863
|
if (hasLocal) {
|
|
1651
|
-
const
|
|
1652
|
-
sigFingerprint = toBase64Url(
|
|
1653
|
-
encFingerprint = toBase64Url(
|
|
864
|
+
const agentKeys = loadAgentKeys(configDir, agentId);
|
|
865
|
+
sigFingerprint = toBase64Url(agentKeys.signing.publicKey).slice(0, 8);
|
|
866
|
+
encFingerprint = toBase64Url(agentKeys.encryption.publicKey).slice(0, 8);
|
|
1654
867
|
} else if (serverKeys) {
|
|
1655
868
|
sigFingerprint = serverKeys.signing_public_key.x.slice(0, 8);
|
|
1656
869
|
encFingerprint = serverKeys.encryption_public_key.x.slice(0, 8);
|
|
@@ -1695,38 +908,38 @@ function registerKeys(program) {
|
|
|
1695
908
|
}
|
|
1696
909
|
]);
|
|
1697
910
|
}));
|
|
1698
|
-
keys.command("generate").description("Generate new E2EE keys for an agent").option("--agent <id>", "Agent ID").action(withClient(program, async ({ client, gOpts }, opts) => {
|
|
1699
|
-
const agentId = await resolveAgent(client, opts.agent, gOpts.as);
|
|
1700
|
-
if (agentKeysExist(agentId)) {
|
|
911
|
+
keys.command("generate").description("Generate new E2EE keys for an agent").option("--agent <id>", "Agent ID").action(withClient(program, async ({ client, gOpts, configDir, apiUrl }, opts) => {
|
|
912
|
+
const agentId = await resolveAgent(apiUrl, await fetchAgents(client), opts.agent, gOpts.as);
|
|
913
|
+
if (agentKeysExist(configDir, agentId)) {
|
|
1701
914
|
printError(`Keys already exist for agent ${agentId}. Use 'rine keys rotate' to replace them.`);
|
|
1702
915
|
process.exitCode = 1;
|
|
1703
916
|
return;
|
|
1704
917
|
}
|
|
1705
918
|
const agentKeys = generateAgentKeys();
|
|
1706
|
-
const pubKeys = getAgentPublicKeys(agentId, agentKeys);
|
|
919
|
+
const pubKeys = getAgentPublicKeys(configDir, agentId, agentKeys);
|
|
1707
920
|
await client.post(`/agents/${agentId}/keys`, pubKeys);
|
|
1708
921
|
await verifyServerKeys(client, agentId, pubKeys);
|
|
1709
|
-
saveAgentKeys(agentId, agentKeys);
|
|
922
|
+
saveAgentKeys(configDir, agentId, agentKeys);
|
|
1710
923
|
printMutationOk(`E2EE keys generated for agent ${agentId}`, gOpts.json);
|
|
1711
924
|
}));
|
|
1712
|
-
keys.command("rotate").description("Rotate E2EE keys for an agent").option("--agent <id>", "Agent ID").action(withClient(program, async ({ client, gOpts }, opts) => {
|
|
1713
|
-
const agentId = await resolveAgent(client, opts.agent, gOpts.as);
|
|
1714
|
-
if (agentKeysExist(agentId)) backupKeys(agentId);
|
|
925
|
+
keys.command("rotate").description("Rotate E2EE keys for an agent").option("--agent <id>", "Agent ID").action(withClient(program, async ({ client, gOpts, configDir, apiUrl }, opts) => {
|
|
926
|
+
const agentId = await resolveAgent(apiUrl, await fetchAgents(client), opts.agent, gOpts.as);
|
|
927
|
+
if (agentKeysExist(configDir, agentId)) backupKeys(configDir, agentId);
|
|
1715
928
|
const agentKeys = generateAgentKeys();
|
|
1716
|
-
const pubKeys = getAgentPublicKeys(agentId, agentKeys);
|
|
929
|
+
const pubKeys = getAgentPublicKeys(configDir, agentId, agentKeys);
|
|
1717
930
|
await client.post(`/agents/${agentId}/keys`, pubKeys);
|
|
1718
931
|
await verifyServerKeys(client, agentId, pubKeys);
|
|
1719
|
-
saveAgentKeys(agentId, agentKeys);
|
|
932
|
+
saveAgentKeys(configDir, agentId, agentKeys);
|
|
1720
933
|
printMutationOk(`E2EE keys rotated for agent ${agentId}`, gOpts.json);
|
|
1721
934
|
}));
|
|
1722
|
-
keys.command("export").description("Export private keys to a file").option("--agent <id>", "Agent ID").requiredOption("--output <file>", "Output file path").action(withClient(program, async ({ client, gOpts }, opts) => {
|
|
1723
|
-
const agentId = await resolveAgent(client, opts.agent, gOpts.as);
|
|
1724
|
-
if (!agentKeysExist(agentId)) {
|
|
935
|
+
keys.command("export").description("Export private keys to a file").option("--agent <id>", "Agent ID").requiredOption("--output <file>", "Output file path").action(withClient(program, async ({ client, gOpts, configDir, apiUrl }, opts) => {
|
|
936
|
+
const agentId = await resolveAgent(apiUrl, await fetchAgents(client), opts.agent, gOpts.as);
|
|
937
|
+
if (!agentKeysExist(configDir, agentId)) {
|
|
1725
938
|
printError(`No keys found for agent ${agentId}`);
|
|
1726
939
|
process.exitCode = 1;
|
|
1727
940
|
return;
|
|
1728
941
|
}
|
|
1729
|
-
const agentKeys = loadAgentKeys(agentId);
|
|
942
|
+
const agentKeys = loadAgentKeys(configDir, agentId);
|
|
1730
943
|
const exported = {
|
|
1731
944
|
agent_id: agentId,
|
|
1732
945
|
signing_private_key: toBase64Url(agentKeys.signing.privateKey),
|
|
@@ -1748,33 +961,21 @@ function registerKeys(program) {
|
|
|
1748
961
|
process.exitCode = 2;
|
|
1749
962
|
return;
|
|
1750
963
|
}
|
|
1751
|
-
const sigBytes = fromBase64Url(data.signing_private_key);
|
|
1752
|
-
if (sigBytes.length !== 32) {
|
|
1753
|
-
printError(`Invalid signing key: expected 32 bytes, got ${sigBytes.length}`);
|
|
1754
|
-
process.exitCode = 2;
|
|
1755
|
-
return;
|
|
1756
|
-
}
|
|
1757
964
|
try {
|
|
1758
|
-
|
|
1759
|
-
} catch {
|
|
1760
|
-
printError(
|
|
1761
|
-
process.exitCode = 2;
|
|
1762
|
-
return;
|
|
1763
|
-
}
|
|
1764
|
-
const encBytes = fromBase64Url(data.encryption_private_key);
|
|
1765
|
-
if (encBytes.length !== 32) {
|
|
1766
|
-
printError(`Invalid encryption key: expected 32 bytes, got ${encBytes.length}`);
|
|
965
|
+
validateSigningKey(fromBase64Url(data.signing_private_key));
|
|
966
|
+
} catch (err) {
|
|
967
|
+
printError(`Invalid signing key: ${err.message}`);
|
|
1767
968
|
process.exitCode = 2;
|
|
1768
969
|
return;
|
|
1769
970
|
}
|
|
1770
971
|
try {
|
|
1771
|
-
|
|
1772
|
-
} catch {
|
|
1773
|
-
printError(
|
|
972
|
+
validateEncryptionKey(fromBase64Url(data.encryption_private_key));
|
|
973
|
+
} catch (err) {
|
|
974
|
+
printError(`Invalid encryption key: ${err.message}`);
|
|
1774
975
|
process.exitCode = 2;
|
|
1775
976
|
return;
|
|
1776
977
|
}
|
|
1777
|
-
const dir = join(
|
|
978
|
+
const dir = join(resolveConfigDir(), "keys", data.agent_id);
|
|
1778
979
|
fs.mkdirSync(dir, {
|
|
1779
980
|
recursive: true,
|
|
1780
981
|
mode: 448
|
|
@@ -1789,104 +990,7 @@ function registerKeys(program) {
|
|
|
1789
990
|
}));
|
|
1790
991
|
}
|
|
1791
992
|
//#endregion
|
|
1792
|
-
//#region src/crypto/ingest.ts
|
|
1793
|
-
/**
|
|
1794
|
-
* Auto-ingest a sender key distribution from a decrypted message.
|
|
1795
|
-
* Only ingests if the message is a `rine.v1.sender_key_distribution`,
|
|
1796
|
-
* the signature was verified, and the key ID is not already known.
|
|
1797
|
-
*
|
|
1798
|
-
* Returns true if a new key was ingested.
|
|
1799
|
-
*/
|
|
1800
|
-
function ingestSenderKeyDistribution(agentId, messageType, result) {
|
|
1801
|
-
if (messageType !== "rine.v1.sender_key_distribution") return false;
|
|
1802
|
-
if (!result.verified) return false;
|
|
1803
|
-
try {
|
|
1804
|
-
const dist = JSON.parse(result.plaintext);
|
|
1805
|
-
const existing = loadSenderKeyStates(agentId, dist.group_id);
|
|
1806
|
-
if (existing.some((s) => s.senderKeyId === dist.sender_key_id)) return false;
|
|
1807
|
-
const newState = {
|
|
1808
|
-
senderKeyId: dist.sender_key_id,
|
|
1809
|
-
groupId: dist.group_id,
|
|
1810
|
-
senderAgentId: agentIdFromKid(result.senderKid),
|
|
1811
|
-
chainKey: fromBase64Url(dist.chain_key),
|
|
1812
|
-
messageIndex: dist.message_index
|
|
1813
|
-
};
|
|
1814
|
-
saveSenderKeyState(agentId, dist.group_id, [...existing, newState]);
|
|
1815
|
-
return true;
|
|
1816
|
-
} catch {
|
|
1817
|
-
return false;
|
|
1818
|
-
}
|
|
1819
|
-
}
|
|
1820
|
-
//#endregion
|
|
1821
993
|
//#region src/commands/sender-key-ops.ts
|
|
1822
|
-
async function distributeSenderKey(client, senderAgentId, state, groupId, recipientIds, extraHeaders) {
|
|
1823
|
-
if (recipientIds.length === 0) return [];
|
|
1824
|
-
const batchKeys = await client.get("/agents/keys", { ids: recipientIds.join(",") });
|
|
1825
|
-
const distPayload = {
|
|
1826
|
-
group_id: groupId,
|
|
1827
|
-
sender_key_id: state.senderKeyId,
|
|
1828
|
-
chain_key: toBase64Url(state.chainKey),
|
|
1829
|
-
message_index: state.messageIndex,
|
|
1830
|
-
sender_agent_id: senderAgentId
|
|
1831
|
-
};
|
|
1832
|
-
const succeeded = [];
|
|
1833
|
-
const failed = [];
|
|
1834
|
-
for (const recipientId of recipientIds) {
|
|
1835
|
-
const recipientKeyData = batchKeys.keys[recipientId];
|
|
1836
|
-
if (!recipientKeyData) {
|
|
1837
|
-
failed.push(recipientId);
|
|
1838
|
-
continue;
|
|
1839
|
-
}
|
|
1840
|
-
try {
|
|
1841
|
-
const encrypted = await encryptMessage(senderAgentId, fromBase64Url(recipientKeyData.encryption_public_key.x), distPayload);
|
|
1842
|
-
await client.post("/messages", {
|
|
1843
|
-
to_agent_id: recipientId,
|
|
1844
|
-
type: "rine.v1.sender_key_distribution",
|
|
1845
|
-
...encrypted
|
|
1846
|
-
}, extraHeaders);
|
|
1847
|
-
succeeded.push(recipientId);
|
|
1848
|
-
} catch {
|
|
1849
|
-
failed.push(recipientId);
|
|
1850
|
-
}
|
|
1851
|
-
}
|
|
1852
|
-
if (failed.length > 0) printError(`Sender key distribution failed for ${failed.length} recipient(s): ${failed.join(", ")}`);
|
|
1853
|
-
return succeeded;
|
|
1854
|
-
}
|
|
1855
|
-
async function getOrCreateSenderKey(client, senderAgentId, groupHandle, extraHeaders) {
|
|
1856
|
-
const firstGroup = (await client.get("/groups", { handle: groupHandle })).items?.[0];
|
|
1857
|
-
if (!firstGroup) throw new Error(`Group not found: ${groupHandle}`);
|
|
1858
|
-
const groupId = firstGroup.id;
|
|
1859
|
-
const memberIds = (await client.get(`/groups/${groupId}/members`)).items.map((m) => m.agent_id);
|
|
1860
|
-
const senderIdLower = senderAgentId.toLowerCase();
|
|
1861
|
-
if (!memberIds.some((id) => id.toLowerCase() === senderIdLower)) throw new Error("You are not a member of this group");
|
|
1862
|
-
const recipientIds = memberIds.filter((id) => id.toLowerCase() !== senderIdLower);
|
|
1863
|
-
const states = loadSenderKeyStates(senderAgentId, groupId);
|
|
1864
|
-
const own = states.find((s) => s.senderAgentId === senderAgentId);
|
|
1865
|
-
const currentMemberSet = new Set(memberIds.map((id) => id.toLowerCase()));
|
|
1866
|
-
const memberRemoved = own?.distributedTo?.some((id) => !currentMemberSet.has(id.toLowerCase())) ?? false;
|
|
1867
|
-
if (own && !needsRotation(own) && !memberRemoved) {
|
|
1868
|
-
const alreadyDistributed = new Set((own.distributedTo ?? []).map((id) => id.toLowerCase()));
|
|
1869
|
-
const undistributed = recipientIds.filter((id) => !alreadyDistributed.has(id.toLowerCase()));
|
|
1870
|
-
if (undistributed.length > 0) {
|
|
1871
|
-
const succeeded = await distributeSenderKey(client, senderAgentId, own, groupId, undistributed, extraHeaders);
|
|
1872
|
-
if (succeeded.length > 0) {
|
|
1873
|
-
own.distributedTo = [...own.distributedTo ?? [], ...succeeded];
|
|
1874
|
-
saveSenderKeyState(senderAgentId, groupId, states);
|
|
1875
|
-
}
|
|
1876
|
-
}
|
|
1877
|
-
return {
|
|
1878
|
-
state: own,
|
|
1879
|
-
groupId
|
|
1880
|
-
};
|
|
1881
|
-
}
|
|
1882
|
-
const newState = generateSenderKey(groupId, senderAgentId);
|
|
1883
|
-
newState.distributedTo = await distributeSenderKey(client, senderAgentId, newState, groupId, recipientIds, extraHeaders);
|
|
1884
|
-
saveSenderKeyState(senderAgentId, groupId, [...memberRemoved ? states.filter((s) => currentMemberSet.has(s.senderAgentId.toLowerCase())) : states, newState]);
|
|
1885
|
-
return {
|
|
1886
|
-
state: newState,
|
|
1887
|
-
groupId
|
|
1888
|
-
};
|
|
1889
|
-
}
|
|
1890
994
|
/**
|
|
1891
995
|
* Fetch pending sender key distributions from the inbox and ingest them.
|
|
1892
996
|
* Used by `read` and `stream` to auto-recover when a sender key is missing.
|
|
@@ -1894,7 +998,7 @@ async function getOrCreateSenderKey(client, senderAgentId, groupHandle, extraHea
|
|
|
1894
998
|
* Optionally short-circuits once `targetSenderKeyId` is ingested.
|
|
1895
999
|
* Returns the number of newly ingested keys.
|
|
1896
1000
|
*/
|
|
1897
|
-
async function fetchAndIngestPendingSKDistributions(client, agentId, targetSenderKeyId) {
|
|
1001
|
+
async function fetchAndIngestPendingSKDistributions(client, configDir, agentId, targetSenderKeyId) {
|
|
1898
1002
|
const inbox = await client.get(`/agents/${agentId}/messages`, {
|
|
1899
1003
|
type: "rine.v1.sender_key_distribution",
|
|
1900
1004
|
limit: 100
|
|
@@ -1902,8 +1006,8 @@ async function fetchAndIngestPendingSKDistributions(client, agentId, targetSende
|
|
|
1902
1006
|
let ingested = 0;
|
|
1903
1007
|
for (const msg of inbox.items) try {
|
|
1904
1008
|
const full = await client.get(`/messages/${msg.id}`);
|
|
1905
|
-
const result = await decryptMessage(agentId, full.encrypted_payload, client);
|
|
1906
|
-
if (ingestSenderKeyDistribution(agentId, full.type, result)) {
|
|
1009
|
+
const result = await decryptMessage(configDir, agentId, full.encrypted_payload, client);
|
|
1010
|
+
if (ingestSenderKeyDistribution(configDir, agentId, full.type, result)) {
|
|
1907
1011
|
ingested++;
|
|
1908
1012
|
if (targetSenderKeyId) {
|
|
1909
1013
|
if (JSON.parse(result.plaintext).sender_key_id === targetSenderKeyId) break;
|
|
@@ -1961,7 +1065,7 @@ function resolvePayload(payload, payloadFile) {
|
|
|
1961
1065
|
}
|
|
1962
1066
|
}
|
|
1963
1067
|
function addMessageCommands(parent, program) {
|
|
1964
|
-
parent.command("send").description("Send an encrypted message").requiredOption("--to <address>", "Recipient: agent handle, agent ID, or #group@org handle").option("--type <type>", "Message type (default: rine.v1.dm)").option("--payload <json>", "Message payload as JSON string").option("--payload-file <path>", "Read payload JSON from file (use - for stdin)").option("--from <address>", "Sender address (agent ID or handle with @)").option("--idempotency-key <key>", "Idempotency key header").action(withClient(program, async ({ client, gOpts }, opts) => {
|
|
1068
|
+
parent.command("send").description("Send an encrypted message").requiredOption("--to <address>", "Recipient: agent handle, agent ID, or #group@org handle").option("--type <type>", "Message type (default: rine.v1.dm)").option("--payload <json>", "Message payload as JSON string").option("--payload-file <path>", "Read payload JSON from file (use - for stdin)").option("--from <address>", "Sender address (agent ID or handle with @)").option("--idempotency-key <key>", "Idempotency key header").action(withClient(program, async ({ client, gOpts, configDir, apiUrl }, opts) => {
|
|
1965
1069
|
const result = resolvePayload(opts.payload, opts.payloadFile);
|
|
1966
1070
|
if ("error" in result) {
|
|
1967
1071
|
printError(result.error);
|
|
@@ -1972,45 +1076,45 @@ function addMessageCommands(parent, program) {
|
|
|
1972
1076
|
const extraHeaders = {};
|
|
1973
1077
|
let senderAgentId;
|
|
1974
1078
|
if (opts.from !== void 0) {
|
|
1975
|
-
senderAgentId = await resolveToUuid(opts.from);
|
|
1079
|
+
senderAgentId = await resolveToUuid(apiUrl, opts.from);
|
|
1976
1080
|
extraHeaders["X-Rine-Agent"] = senderAgentId;
|
|
1977
1081
|
} else if (!gOpts.as) {
|
|
1978
|
-
senderAgentId = await resolveAgent(client, void 0, void 0);
|
|
1082
|
+
senderAgentId = await resolveAgent(apiUrl, await fetchAgents(client), void 0, void 0);
|
|
1979
1083
|
extraHeaders["X-Rine-Agent"] = senderAgentId;
|
|
1980
|
-
} else senderAgentId = await resolveAgent(client, void 0, gOpts.as);
|
|
1084
|
+
} else senderAgentId = await resolveAgent(apiUrl, await fetchAgents(client), void 0, gOpts.as);
|
|
1981
1085
|
if (opts.idempotencyKey) extraHeaders["Idempotency-Key"] = opts.idempotencyKey;
|
|
1982
1086
|
let recipientAgentId;
|
|
1983
1087
|
if (!opts.to.includes("@")) recipientAgentId = opts.to;
|
|
1984
|
-
else if (!opts.to.startsWith("#")) recipientAgentId = await resolveToUuid(opts.to);
|
|
1088
|
+
else if (!opts.to.startsWith("#")) recipientAgentId = await resolveToUuid(apiUrl, opts.to);
|
|
1985
1089
|
const body = { type: opts.type ?? "rine.v1.dm" };
|
|
1986
1090
|
if (opts.to.includes("@")) body.to_handle = opts.to;
|
|
1987
1091
|
else body.to_agent_id = opts.to;
|
|
1988
1092
|
if (opts.from?.includes("@")) body.from_handle = opts.from;
|
|
1989
1093
|
if (opts.to.startsWith("#") && senderAgentId) {
|
|
1990
|
-
const { state, groupId } = await getOrCreateSenderKey(client, senderAgentId, opts.to, Object.keys(extraHeaders).length > 0 ? extraHeaders : void 0);
|
|
1991
|
-
const { result } = await encryptGroupMessage(senderAgentId, groupId, state, parsedPayload);
|
|
1992
|
-
Object.assign(body,
|
|
1094
|
+
const { state, groupId } = await getOrCreateSenderKey(client, configDir, senderAgentId, opts.to, Object.keys(extraHeaders).length > 0 ? extraHeaders : void 0);
|
|
1095
|
+
const { result: encResult } = await encryptGroupMessage(configDir, senderAgentId, groupId, state, parsedPayload);
|
|
1096
|
+
Object.assign(body, encResult);
|
|
1993
1097
|
} else if (senderAgentId && recipientAgentId) {
|
|
1994
1098
|
const recipientPk = await fetchRecipientEncryptionKey(client, recipientAgentId);
|
|
1995
|
-
const encrypted = await encryptMessage(senderAgentId, recipientPk, parsedPayload);
|
|
1099
|
+
const encrypted = await encryptMessage(configDir, senderAgentId, recipientPk, parsedPayload);
|
|
1996
1100
|
Object.assign(body, encrypted);
|
|
1997
1101
|
} else throw new Error("Cannot encrypt: unable to resolve sender or recipient. Use UUIDs or ensure WebFinger is available.");
|
|
1998
1102
|
const data = await client.post("/messages", body, Object.keys(extraHeaders).length > 0 ? extraHeaders : void 0);
|
|
1999
1103
|
if (gOpts.json) printJson(data);
|
|
2000
1104
|
else printTable([sendRow(data)]);
|
|
2001
1105
|
}));
|
|
2002
|
-
parent.command("read").description("Read and decrypt a message by ID").argument("<message-id>", "Message ID").option("--agent <id>", "Agent ID (for decryption key)").action(withClient(program, async ({ client, gOpts }, messageId, opts) => {
|
|
1106
|
+
parent.command("read").description("Read and decrypt a message by ID").argument("<message-id>", "Message ID").option("--agent <id>", "Agent ID (for decryption key)").action(withClient(program, async ({ client, gOpts, configDir, apiUrl }, messageId, opts) => {
|
|
2003
1107
|
const data = await client.get(`/messages/${messageId}`);
|
|
2004
|
-
const agentId = await resolveAgent(client, opts.agent, gOpts.as);
|
|
2005
|
-
if ((data.encryption_version === "hpke-v1" || data.encryption_version === "sender-key-v1") && agentKeysExist(agentId)) {
|
|
2006
|
-
let
|
|
1108
|
+
const agentId = await resolveAgent(apiUrl, await fetchAgents(client), opts.agent, gOpts.as);
|
|
1109
|
+
if ((data.encryption_version === "hpke-v1" || data.encryption_version === "sender-key-v1") && agentKeysExist(configDir, agentId)) {
|
|
1110
|
+
let decResult;
|
|
2007
1111
|
if (data.encryption_version === "sender-key-v1" && data.group_id) try {
|
|
2008
|
-
|
|
1112
|
+
decResult = await decryptGroupMessage(configDir, agentId, data.group_id, data.encrypted_payload, client);
|
|
2009
1113
|
} catch (err) {
|
|
2010
1114
|
const match = err instanceof Error && err.message.match(/^unknown sender key id: (.+)$/);
|
|
2011
1115
|
if (match) {
|
|
2012
|
-
if (await fetchAndIngestPendingSKDistributions(client, agentId, match[1]) > 0) try {
|
|
2013
|
-
|
|
1116
|
+
if (await fetchAndIngestPendingSKDistributions(client, configDir, agentId, match[1]) > 0) try {
|
|
1117
|
+
decResult = await decryptGroupMessage(configDir, agentId, data.group_id, data.encrypted_payload, client);
|
|
2014
1118
|
} catch (retryErr) {
|
|
2015
1119
|
if (!gOpts.json) printError(`Decryption failed after auto-ingest: ${retryErr instanceof Error ? retryErr.message : String(retryErr)}`);
|
|
2016
1120
|
}
|
|
@@ -2018,18 +1122,18 @@ function addMessageCommands(parent, program) {
|
|
|
2018
1122
|
} else if (!gOpts.json) printError(`Decryption failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2019
1123
|
}
|
|
2020
1124
|
else try {
|
|
2021
|
-
|
|
1125
|
+
decResult = await decryptMessage(configDir, agentId, data.encrypted_payload, client);
|
|
2022
1126
|
} catch (err) {
|
|
2023
1127
|
if (!gOpts.json) printError(`Decryption failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2024
1128
|
}
|
|
2025
|
-
if (
|
|
2026
|
-
if (ingestSenderKeyDistribution(agentId, data.type,
|
|
1129
|
+
if (decResult) {
|
|
1130
|
+
if (ingestSenderKeyDistribution(configDir, agentId, data.type, decResult)) printText("Sender key ingested");
|
|
2027
1131
|
if (gOpts.json) printJson({
|
|
2028
1132
|
...data,
|
|
2029
|
-
decrypted_payload: JSON.parse(
|
|
2030
|
-
signature_verified:
|
|
2031
|
-
verification_status:
|
|
2032
|
-
sender_kid:
|
|
1133
|
+
decrypted_payload: JSON.parse(decResult.plaintext),
|
|
1134
|
+
signature_verified: decResult.verified,
|
|
1135
|
+
verification_status: decResult.verificationStatus,
|
|
1136
|
+
sender_kid: decResult.senderKid
|
|
2033
1137
|
});
|
|
2034
1138
|
else printTable([{
|
|
2035
1139
|
ID: data.id,
|
|
@@ -2038,8 +1142,8 @@ function addMessageCommands(parent, program) {
|
|
|
2038
1142
|
To: msgTo(data),
|
|
2039
1143
|
Group: msgGroup(data),
|
|
2040
1144
|
Type: data.type,
|
|
2041
|
-
Verified:
|
|
2042
|
-
Payload:
|
|
1145
|
+
Verified: decResult.verificationStatus === "verified" ? "✓" : decResult.verificationStatus === "invalid" ? "✗ INVALID" : "?",
|
|
1146
|
+
Payload: decResult.plaintext.slice(0, 200),
|
|
2043
1147
|
Created: data.created_at
|
|
2044
1148
|
}]);
|
|
2045
1149
|
return;
|
|
@@ -2058,8 +1162,8 @@ function addMessageCommands(parent, program) {
|
|
|
2058
1162
|
Created: data.created_at
|
|
2059
1163
|
}]);
|
|
2060
1164
|
}));
|
|
2061
|
-
parent.command("inbox").description("List inbox messages").option("--agent <id>", "Agent ID (defaults to your agent)").option("--limit <n>", "Maximum number of messages to return").option("--cursor <cursor>", "Pagination cursor").action(withClient(program, async ({ client, gOpts }, opts) => {
|
|
2062
|
-
const agentId = await resolveAgent(client, opts.agent, gOpts.as);
|
|
1165
|
+
parent.command("inbox").description("List inbox messages").option("--agent <id>", "Agent ID (defaults to your agent)").option("--limit <n>", "Maximum number of messages to return").option("--cursor <cursor>", "Pagination cursor").action(withClient(program, async ({ client, gOpts, apiUrl }, opts) => {
|
|
1166
|
+
const agentId = await resolveAgent(apiUrl, await fetchAgents(client), opts.agent, gOpts.as);
|
|
2063
1167
|
const params = {};
|
|
2064
1168
|
if (opts.limit) params.limit = opts.limit;
|
|
2065
1169
|
if (opts.cursor) params.cursor = opts.cursor;
|
|
@@ -2070,7 +1174,7 @@ function addMessageCommands(parent, program) {
|
|
|
2070
1174
|
if (data.next_cursor) printText(`Next cursor: ${data.next_cursor}`);
|
|
2071
1175
|
}
|
|
2072
1176
|
}));
|
|
2073
|
-
parent.command("reply").description("Reply to a message (encrypted)").argument("<message-id>", "Message ID to reply to").option("--type <type>", "Message type (defaults to original message type)").option("--payload <json>", "Reply payload as JSON string").option("--payload-file <path>", "Read payload JSON from file (use - for stdin)").action(withClient(program, async ({ client, gOpts }, messageId, opts) => {
|
|
1177
|
+
parent.command("reply").description("Reply to a message (encrypted)").argument("<message-id>", "Message ID to reply to").option("--type <type>", "Message type (defaults to original message type)").option("--payload <json>", "Reply payload as JSON string").option("--payload-file <path>", "Read payload JSON from file (use - for stdin)").action(withClient(program, async ({ client, gOpts, configDir, apiUrl }, messageId, opts) => {
|
|
2074
1178
|
const result = resolvePayload(opts.payload, opts.payloadFile);
|
|
2075
1179
|
if ("error" in result) {
|
|
2076
1180
|
printError(result.error);
|
|
@@ -2079,13 +1183,13 @@ function addMessageCommands(parent, program) {
|
|
|
2079
1183
|
}
|
|
2080
1184
|
const parsedPayload = result.parsed;
|
|
2081
1185
|
const original = await client.get(`/messages/${messageId}`);
|
|
2082
|
-
const senderAgentId = await resolveAgent(client, void 0, gOpts.as);
|
|
1186
|
+
const senderAgentId = await resolveAgent(apiUrl, await fetchAgents(client), void 0, gOpts.as);
|
|
2083
1187
|
const recipientAgentId = original.from_agent_id;
|
|
2084
1188
|
const resolvedType = opts.type ?? original.type;
|
|
2085
1189
|
try {
|
|
2086
1190
|
const body = { type: resolvedType };
|
|
2087
|
-
if (!recipientAgentId || !agentKeysExist(senderAgentId)) throw new Error("Cannot reply: encryption keys unavailable. Run 'rine keys generate' first.");
|
|
2088
|
-
const encrypted = await encryptMessage(senderAgentId, await fetchRecipientEncryptionKey(client, recipientAgentId), parsedPayload);
|
|
1191
|
+
if (!recipientAgentId || !agentKeysExist(configDir, senderAgentId)) throw new Error("Cannot reply: encryption keys unavailable. Run 'rine keys generate' first.");
|
|
1192
|
+
const encrypted = await encryptMessage(configDir, senderAgentId, await fetchRecipientEncryptionKey(client, recipientAgentId), parsedPayload);
|
|
2089
1193
|
Object.assign(body, encrypted);
|
|
2090
1194
|
const data = await client.post(`/messages/${messageId}/reply`, body);
|
|
2091
1195
|
if (gOpts.json) printJson(data);
|
|
@@ -2104,23 +1208,23 @@ function registerMessages(program) {
|
|
|
2104
1208
|
//#endregion
|
|
2105
1209
|
//#region src/commands/poll-token.ts
|
|
2106
1210
|
function registerPollToken(program) {
|
|
2107
|
-
program.command("poll-token").description("Generate or revoke a poll token for inbox monitoring").option("--agent <id>", "Agent ID (auto-resolved for single-agent orgs)").option("--revoke", "Revoke the poll token").action(withClient(program, async ({ client, gOpts, profileName }, opts) => {
|
|
2108
|
-
const agentId = await resolveAgent(client, opts.agent, gOpts.as);
|
|
1211
|
+
program.command("poll-token").description("Generate or revoke a poll token for inbox monitoring").option("--agent <id>", "Agent ID (auto-resolved for single-agent orgs)").option("--revoke", "Revoke the poll token").action(withClient(program, async ({ client, gOpts, profileName, configDir, apiUrl }, opts) => {
|
|
1212
|
+
const agentId = await resolveAgent(apiUrl, await fetchAgents(client), opts.agent, gOpts.as);
|
|
2109
1213
|
if (opts.revoke) {
|
|
2110
1214
|
await client.delete(`/agents/${agentId}/poll-token`);
|
|
2111
|
-
const creds = loadCredentials();
|
|
1215
|
+
const creds = loadCredentials(configDir);
|
|
2112
1216
|
if (creds[profileName]) {
|
|
2113
1217
|
delete creds[profileName].poll_url;
|
|
2114
|
-
saveCredentials(creds);
|
|
1218
|
+
saveCredentials(configDir, creds);
|
|
2115
1219
|
}
|
|
2116
1220
|
printMutationOk("Poll token revoked", gOpts.json);
|
|
2117
1221
|
return;
|
|
2118
1222
|
}
|
|
2119
1223
|
const data = await client.post(`/agents/${agentId}/poll-token`, {});
|
|
2120
|
-
const creds = loadCredentials();
|
|
1224
|
+
const creds = loadCredentials(configDir);
|
|
2121
1225
|
if (creds[profileName]) {
|
|
2122
1226
|
creds[profileName].poll_url = data.poll_url;
|
|
2123
|
-
saveCredentials(creds);
|
|
1227
|
+
saveCredentials(configDir, creds);
|
|
2124
1228
|
}
|
|
2125
1229
|
if (gOpts.json) printJson(data);
|
|
2126
1230
|
else {
|
|
@@ -2164,26 +1268,12 @@ function registerOrg(program) {
|
|
|
2164
1268
|
}));
|
|
2165
1269
|
}
|
|
2166
1270
|
//#endregion
|
|
2167
|
-
//#region src/timelock.ts
|
|
2168
|
-
async function solveTimeLockWithProgress(baseHex, modulusHex, T, onProgress) {
|
|
2169
|
-
const N = BigInt(`0x${modulusHex}`);
|
|
2170
|
-
let x = BigInt(`0x${baseHex}`) % N;
|
|
2171
|
-
const step = Math.max(1, Math.floor(T / 100));
|
|
2172
|
-
for (let i = 0; i < T; i++) {
|
|
2173
|
-
x = x * x % N;
|
|
2174
|
-
if (i % step === 0) {
|
|
2175
|
-
onProgress?.(Math.floor(i / T * 100));
|
|
2176
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
2177
|
-
}
|
|
2178
|
-
}
|
|
2179
|
-
return x.toString(16);
|
|
2180
|
-
}
|
|
2181
|
-
//#endregion
|
|
2182
1271
|
//#region src/commands/register.ts
|
|
2183
1272
|
function registerRegister(program) {
|
|
2184
1273
|
program.command("register").description("Register a new organization (two-step PoW flow)").requiredOption("--email <email>", "Email address").requiredOption("--name <name>", "Organization name").requiredOption("--slug <slug>", "Organization slug").action(withErrorHandler(program, async (gOpts, opts) => {
|
|
2185
1274
|
const profile = gOpts.profile ?? "default";
|
|
2186
|
-
const
|
|
1275
|
+
const configDir = resolveConfigDir();
|
|
1276
|
+
const apiUrl = resolveApiUrl();
|
|
2187
1277
|
const challengeRes = await fetch(`${apiUrl}/auth/register`, {
|
|
2188
1278
|
method: "POST",
|
|
2189
1279
|
headers: { "Content-Type": "application/json" },
|
|
@@ -2246,14 +1336,14 @@ function registerRegister(program) {
|
|
|
2246
1336
|
return;
|
|
2247
1337
|
}
|
|
2248
1338
|
const data = await solveRes.json();
|
|
2249
|
-
const creds = loadCredentials();
|
|
1339
|
+
const creds = loadCredentials(configDir);
|
|
2250
1340
|
creds[profile] = {
|
|
2251
1341
|
client_id: data.client_id,
|
|
2252
1342
|
client_secret: data.client_secret
|
|
2253
1343
|
};
|
|
2254
|
-
saveCredentials(creds);
|
|
1344
|
+
saveCredentials(configDir, creds);
|
|
2255
1345
|
try {
|
|
2256
|
-
cacheToken(profile, await fetchOAuthToken(data.client_id, data.client_secret));
|
|
1346
|
+
cacheToken(configDir, profile, await fetchOAuthToken(apiUrl, data.client_id, data.client_secret));
|
|
2257
1347
|
} catch {
|
|
2258
1348
|
process.stderr.write("Warning: initial token cache failed\n");
|
|
2259
1349
|
}
|
|
@@ -2267,7 +1357,7 @@ function registerRegister(program) {
|
|
|
2267
1357
|
}
|
|
2268
1358
|
//#endregion
|
|
2269
1359
|
//#region src/commands/stream.ts
|
|
2270
|
-
async function formatMessageLine(dataStr, agentId, client) {
|
|
1360
|
+
async function formatMessageLine(dataStr, agentId, configDir, client) {
|
|
2271
1361
|
try {
|
|
2272
1362
|
const data = JSON.parse(dataStr);
|
|
2273
1363
|
const ts = (data.created_at ?? "").slice(0, 19) || "?";
|
|
@@ -2275,23 +1365,23 @@ async function formatMessageLine(dataStr, agentId, client) {
|
|
|
2275
1365
|
const msgType = data.type ?? "?";
|
|
2276
1366
|
const target = data.group_handle ? `${data.group_handle} (${msgType})` : msgType;
|
|
2277
1367
|
let preview = "[encrypted]";
|
|
2278
|
-
if (data.encrypted_payload && agentKeysExist(agentId)) {
|
|
1368
|
+
if (data.encrypted_payload && agentKeysExist(configDir, agentId)) {
|
|
2279
1369
|
let result;
|
|
2280
1370
|
if (data.encryption_version === "sender-key-v1" && data.group_id) try {
|
|
2281
|
-
result = await decryptGroupMessage(agentId, data.group_id, data.encrypted_payload, client);
|
|
1371
|
+
result = await decryptGroupMessage(configDir, agentId, data.group_id, data.encrypted_payload, client);
|
|
2282
1372
|
} catch (err) {
|
|
2283
1373
|
const match = err instanceof Error && err.message.match(/^unknown sender key id: (.+)$/);
|
|
2284
1374
|
if (match) {
|
|
2285
|
-
if (await fetchAndIngestPendingSKDistributions(client, agentId, match[1]) > 0) try {
|
|
2286
|
-
result = await decryptGroupMessage(agentId, data.group_id, data.encrypted_payload, client);
|
|
1375
|
+
if (await fetchAndIngestPendingSKDistributions(client, configDir, agentId, match[1]) > 0) try {
|
|
1376
|
+
result = await decryptGroupMessage(configDir, agentId, data.group_id, data.encrypted_payload, client);
|
|
2287
1377
|
} catch {}
|
|
2288
1378
|
}
|
|
2289
1379
|
}
|
|
2290
1380
|
else if (data.encryption_version === "hpke-v1") try {
|
|
2291
|
-
result = await decryptMessage(agentId, data.encrypted_payload, client);
|
|
1381
|
+
result = await decryptMessage(configDir, agentId, data.encrypted_payload, client);
|
|
2292
1382
|
} catch {}
|
|
2293
1383
|
if (result) {
|
|
2294
|
-
ingestSenderKeyDistribution(agentId, data.type ?? "", result);
|
|
1384
|
+
ingestSenderKeyDistribution(configDir, agentId, data.type ?? "", result);
|
|
2295
1385
|
preview = `[${result.verified ? "✓" : "?"}] ${result.plaintext.slice(0, 60)}`;
|
|
2296
1386
|
} else if (data.encryption_version === "hpke-v1" || data.encryption_version === "sender-key-v1") preview = "[decrypt failed]";
|
|
2297
1387
|
}
|
|
@@ -2304,9 +1394,11 @@ function registerStream(program) {
|
|
|
2304
1394
|
program.command("stream").description("Stream incoming messages via SSE").option("--agent <id>", "Agent ID to stream (defaults to your agent)").option("--verbose", "Show heartbeats and reconnect details").action(async (opts) => {
|
|
2305
1395
|
try {
|
|
2306
1396
|
const gOpts = program.opts();
|
|
2307
|
-
const
|
|
2308
|
-
const
|
|
2309
|
-
const
|
|
1397
|
+
const configDir = resolveConfigDir();
|
|
1398
|
+
const apiUrl = resolveApiUrl();
|
|
1399
|
+
const { client, profileName, entry } = await createClient(configDir, apiUrl, gOpts.profile);
|
|
1400
|
+
const agentId = await resolveAgent(apiUrl, await fetchAgents(client), opts.agent, gOpts.as);
|
|
1401
|
+
const url = `${apiUrl}/agents/${agentId}/stream`;
|
|
2310
1402
|
let lastEventId;
|
|
2311
1403
|
let backoff = 1;
|
|
2312
1404
|
let stopped = false;
|
|
@@ -2315,8 +1407,12 @@ function registerStream(program) {
|
|
|
2315
1407
|
process.exitCode = 0;
|
|
2316
1408
|
});
|
|
2317
1409
|
while (!stopped) try {
|
|
1410
|
+
const envToken = process.env.RINE_TOKEN;
|
|
2318
1411
|
const headers = {
|
|
2319
|
-
Authorization: `Bearer ${await getOrRefreshToken(entry, profileName,
|
|
1412
|
+
Authorization: `Bearer ${await getOrRefreshToken(configDir, apiUrl, entry, profileName, {
|
|
1413
|
+
force: false,
|
|
1414
|
+
envToken
|
|
1415
|
+
})}`,
|
|
2320
1416
|
Accept: "text/event-stream"
|
|
2321
1417
|
};
|
|
2322
1418
|
if (lastEventId) headers["Last-Event-ID"] = lastEventId;
|
|
@@ -2335,7 +1431,7 @@ function registerStream(program) {
|
|
|
2335
1431
|
id,
|
|
2336
1432
|
data
|
|
2337
1433
|
}));
|
|
2338
|
-
else if (event === "message") formatMessageLine(data, agentId, client).then((line) => console.log(line), () => console.log(`[message] ${data.slice(0, 80)}`));
|
|
1434
|
+
else if (event === "message") formatMessageLine(data, agentId, configDir, client).then((line) => console.log(line), () => console.log(`[message] ${data.slice(0, 80)}`));
|
|
2339
1435
|
else if (event === "heartbeat" && opts.verbose) process.stderr.write(`[heartbeat] ${data}\n`);
|
|
2340
1436
|
},
|
|
2341
1437
|
onDisconnect: () => resolve(),
|
|
@@ -2372,13 +1468,13 @@ function registerStream(program) {
|
|
|
2372
1468
|
//#region src/commands/webhook.ts
|
|
2373
1469
|
function registerWebhook(program) {
|
|
2374
1470
|
const webhook = program.command("webhook").description("Manage webhooks");
|
|
2375
|
-
webhook.command("create").description("Create a new webhook").option("--agent <id>", "Agent ID (defaults to your agent)").requiredOption("--url <url>", "Webhook target URL (must be https://)").action(withClient(program, async ({ client, gOpts }, opts) => {
|
|
1471
|
+
webhook.command("create").description("Create a new webhook").option("--agent <id>", "Agent ID (defaults to your agent)").requiredOption("--url <url>", "Webhook target URL (must be https://)").action(withClient(program, async ({ client, gOpts, apiUrl }, opts) => {
|
|
2376
1472
|
if (!opts.url.startsWith("https://")) {
|
|
2377
1473
|
printError("Webhook URL must start with https://");
|
|
2378
1474
|
process.exitCode = 2;
|
|
2379
1475
|
return;
|
|
2380
1476
|
}
|
|
2381
|
-
const agentId = await resolveAgent(client, opts.agent, gOpts.as);
|
|
1477
|
+
const agentId = await resolveAgent(apiUrl, await fetchAgents(client), opts.agent, gOpts.as);
|
|
2382
1478
|
const data = await client.post("/webhooks", {
|
|
2383
1479
|
agent_id: agentId,
|
|
2384
1480
|
url: opts.url
|