@rine-network/cli 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.js +170 -1060
- package/package.json +2 -4
package/dist/main.js
CHANGED
|
@@ -1,246 +1,11 @@
|
|
|
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 } 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
7
|
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
8
|
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
9
|
//#region src/output.ts
|
|
245
10
|
function printSuccess(msg) {
|
|
246
11
|
console.log(`✓ ${msg}`);
|
|
@@ -280,103 +45,43 @@ function printError(msg) {
|
|
|
280
45
|
console.error(msg);
|
|
281
46
|
}
|
|
282
47
|
//#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
48
|
//#region src/action.ts
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
49
|
+
async function createClient(configDir, apiUrl, profileFlag, defaultHeaders) {
|
|
50
|
+
const profileName = profileFlag ?? "default";
|
|
51
|
+
const entry = getCredentialEntry(configDir, profileName);
|
|
52
|
+
const envToken = process.env.RINE_TOKEN;
|
|
53
|
+
const canRefresh = !envToken && entry !== void 0;
|
|
54
|
+
const tokenFn = (force = false) => getOrRefreshToken(configDir, apiUrl, entry, profileName, {
|
|
55
|
+
force,
|
|
56
|
+
envToken
|
|
57
|
+
});
|
|
58
|
+
return {
|
|
59
|
+
client: new HttpClient({
|
|
60
|
+
tokenFn,
|
|
61
|
+
apiUrl,
|
|
62
|
+
canRefresh,
|
|
63
|
+
defaultHeaders
|
|
64
|
+
}),
|
|
65
|
+
profileName,
|
|
66
|
+
entry
|
|
67
|
+
};
|
|
68
|
+
}
|
|
368
69
|
function withClient(program, fn) {
|
|
369
70
|
return async (...args) => {
|
|
370
71
|
try {
|
|
371
72
|
const gOpts = program.opts();
|
|
73
|
+
const configDir = resolveConfigDir();
|
|
74
|
+
const apiUrl = resolveApiUrl();
|
|
372
75
|
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);
|
|
76
|
+
if (gOpts.as && !isBareAgentName(gOpts.as)) defaultHeaders["X-Rine-Agent"] = await resolveToUuid(apiUrl, gOpts.as);
|
|
77
|
+
const { client, profileName } = await createClient(configDir, apiUrl, gOpts.profile, defaultHeaders);
|
|
78
|
+
if (gOpts.as && isBareAgentName(gOpts.as)) defaultHeaders["X-Rine-Agent"] = await resolveAgent(apiUrl, await fetchAgents(client), void 0, gOpts.as);
|
|
376
79
|
await fn({
|
|
377
80
|
client,
|
|
378
81
|
gOpts,
|
|
379
82
|
profileName,
|
|
83
|
+
configDir,
|
|
84
|
+
apiUrl,
|
|
380
85
|
extraHeaders: defaultHeaders
|
|
381
86
|
}, ...args);
|
|
382
87
|
} catch (err) {
|
|
@@ -385,10 +90,6 @@ function withClient(program, fn) {
|
|
|
385
90
|
}
|
|
386
91
|
};
|
|
387
92
|
}
|
|
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
93
|
function withErrorHandler(program, fn) {
|
|
393
94
|
return async (...args) => {
|
|
394
95
|
try {
|
|
@@ -516,121 +217,6 @@ function registerAgentProfile(program) {
|
|
|
516
217
|
}));
|
|
517
218
|
}
|
|
518
219
|
//#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
220
|
//#region src/prompt.ts
|
|
635
221
|
/** Prompt for a plain-text value on stdout/stdin. */
|
|
636
222
|
async function promptText(question) {
|
|
@@ -691,7 +277,7 @@ function toAgentRow(a, opts = {}) {
|
|
|
691
277
|
}
|
|
692
278
|
function registerAgent(program) {
|
|
693
279
|
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) => {
|
|
280
|
+
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
281
|
let name = opts.name;
|
|
696
282
|
if (!name) {
|
|
697
283
|
name = await promptText("Agent name: ");
|
|
@@ -710,13 +296,13 @@ function registerAgent(program) {
|
|
|
710
296
|
if (opts.humanOversight !== void 0) body.human_oversight = opts.humanOversight;
|
|
711
297
|
if (opts.unlisted) body.unlisted = true;
|
|
712
298
|
const data = await client.post("/agents", body);
|
|
713
|
-
saveAgentKeys(data.id, agentKeys);
|
|
299
|
+
saveAgentKeys(configDir, data.id, agentKeys);
|
|
714
300
|
if (data.poll_url) {
|
|
715
301
|
const profile = gOpts.profile ?? "default";
|
|
716
|
-
const creds = loadCredentials();
|
|
302
|
+
const creds = loadCredentials(configDir);
|
|
717
303
|
if (creds[profile]) {
|
|
718
304
|
creds[profile].poll_url = data.poll_url;
|
|
719
|
-
saveCredentials(creds);
|
|
305
|
+
saveCredentials(configDir, creds);
|
|
720
306
|
}
|
|
721
307
|
}
|
|
722
308
|
if (gOpts.json) printJson(data);
|
|
@@ -784,16 +370,18 @@ function registerAuth(program) {
|
|
|
784
370
|
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
371
|
try {
|
|
786
372
|
const profile = program.opts().profile ?? "default";
|
|
373
|
+
const configDir = resolveConfigDir();
|
|
374
|
+
const apiUrl = resolveApiUrl();
|
|
787
375
|
const clientId = opts.clientId ?? await promptText("Client ID: ");
|
|
788
376
|
const clientSecret = opts.clientSecret ?? await promptPassword("Client secret: ");
|
|
789
|
-
const tokenData = await fetchOAuthToken(clientId, clientSecret);
|
|
790
|
-
const creds = loadCredentials();
|
|
377
|
+
const tokenData = await fetchOAuthToken(apiUrl, clientId, clientSecret);
|
|
378
|
+
const creds = loadCredentials(configDir);
|
|
791
379
|
creds[profile] = {
|
|
792
380
|
client_id: clientId,
|
|
793
381
|
client_secret: clientSecret
|
|
794
382
|
};
|
|
795
|
-
saveCredentials(creds);
|
|
796
|
-
cacheToken(profile, tokenData);
|
|
383
|
+
saveCredentials(configDir, creds);
|
|
384
|
+
cacheToken(configDir, profile, tokenData);
|
|
797
385
|
printSuccess(`Logged in as ${clientId}`);
|
|
798
386
|
} catch (err) {
|
|
799
387
|
printError(formatError(err));
|
|
@@ -802,9 +390,10 @@ function registerAuth(program) {
|
|
|
802
390
|
});
|
|
803
391
|
program.command("logout").description("Clear cached token. Credentials are preserved.").action(withErrorHandler(program, (gOpts) => {
|
|
804
392
|
const profile = gOpts.profile ?? "default";
|
|
805
|
-
const
|
|
393
|
+
const configDir = resolveConfigDir();
|
|
394
|
+
const cache = loadTokenCache(configDir);
|
|
806
395
|
delete cache[profile];
|
|
807
|
-
saveTokenCache(cache);
|
|
396
|
+
saveTokenCache(configDir, cache);
|
|
808
397
|
printSuccess(`Logged out — token cache cleared for profile '${profile}'. Credentials preserved.`);
|
|
809
398
|
}));
|
|
810
399
|
program.command("status").description("Show org status").action(withClient(program, async ({ client, gOpts }) => {
|
|
@@ -813,7 +402,9 @@ function registerAuth(program) {
|
|
|
813
402
|
program.command("whoami").description("Show current identity and status").action(async () => {
|
|
814
403
|
const gOpts = program.opts();
|
|
815
404
|
const profile = gOpts.profile ?? "default";
|
|
816
|
-
const
|
|
405
|
+
const configDir = resolveConfigDir();
|
|
406
|
+
const apiUrl = resolveApiUrl();
|
|
407
|
+
const entry = getCredentialEntry(configDir, profile);
|
|
817
408
|
if (!entry) {
|
|
818
409
|
if (gOpts.json) printJson({ logged_in: false });
|
|
819
410
|
else printTable([{
|
|
@@ -823,9 +414,10 @@ function registerAuth(program) {
|
|
|
823
414
|
return;
|
|
824
415
|
}
|
|
825
416
|
try {
|
|
826
|
-
const token = await getOrRefreshToken(entry, profile);
|
|
417
|
+
const token = await getOrRefreshToken(configDir, apiUrl, entry, profile);
|
|
827
418
|
const client = new HttpClient({
|
|
828
419
|
tokenFn: () => Promise.resolve(token),
|
|
420
|
+
apiUrl,
|
|
829
421
|
canRefresh: false,
|
|
830
422
|
defaultHeaders: {}
|
|
831
423
|
});
|
|
@@ -834,9 +426,10 @@ function registerAuth(program) {
|
|
|
834
426
|
let agentId;
|
|
835
427
|
let keysLabel;
|
|
836
428
|
try {
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
429
|
+
const agents = await fetchAgents(client);
|
|
430
|
+
agentId = await resolveAgent(apiUrl, agents, void 0, gOpts.as);
|
|
431
|
+
agentHandle = agents.find((a) => a.id === agentId)?.handle;
|
|
432
|
+
const hasLocal = agentKeysExist(configDir, agentId);
|
|
840
433
|
let hasServer = false;
|
|
841
434
|
try {
|
|
842
435
|
await client.get(`/agents/${agentId}/keys`);
|
|
@@ -846,7 +439,7 @@ function registerAuth(program) {
|
|
|
846
439
|
} catch {}
|
|
847
440
|
if (gOpts.json) printJson({
|
|
848
441
|
profile,
|
|
849
|
-
api:
|
|
442
|
+
api: apiUrl,
|
|
850
443
|
org: {
|
|
851
444
|
name: org.name,
|
|
852
445
|
slug: org.slug
|
|
@@ -866,7 +459,7 @@ function registerAuth(program) {
|
|
|
866
459
|
},
|
|
867
460
|
{
|
|
868
461
|
Field: "API",
|
|
869
|
-
Value:
|
|
462
|
+
Value: apiUrl
|
|
870
463
|
},
|
|
871
464
|
{
|
|
872
465
|
Field: "Org",
|
|
@@ -902,11 +495,16 @@ function registerAuth(program) {
|
|
|
902
495
|
authGroup.command("token").description("Get or refresh access token").option("--force", "Force token refresh (bypass cache)").action(withErrorHandler(program, async (gOpts, opts) => {
|
|
903
496
|
const profile = gOpts.profile ?? "default";
|
|
904
497
|
const force = opts.force === true;
|
|
498
|
+
const configDir = resolveConfigDir();
|
|
499
|
+
const apiUrl = resolveApiUrl();
|
|
905
500
|
const now = Date.now() / 1e3;
|
|
906
501
|
const envToken = process.env.RINE_TOKEN;
|
|
907
|
-
const cached = loadTokenCache()[profile];
|
|
502
|
+
const cached = loadTokenCache(configDir)[profile];
|
|
908
503
|
const source = envToken !== void 0 || !force && cached !== void 0 && cached.expires_at - now > 60 ? "cache" : "server";
|
|
909
|
-
const token = await getOrRefreshToken(getCredentialEntry(profile), profile,
|
|
504
|
+
const token = await getOrRefreshToken(configDir, apiUrl, getCredentialEntry(configDir, profile), profile, {
|
|
505
|
+
force,
|
|
506
|
+
envToken
|
|
507
|
+
});
|
|
910
508
|
if (gOpts.json) printJson({
|
|
911
509
|
access_token: token,
|
|
912
510
|
source
|
|
@@ -946,11 +544,12 @@ async function showOrgStatus(client, gOpts) {
|
|
|
946
544
|
//#region src/commands/discover-groups.ts
|
|
947
545
|
function registerDiscoverGroups(discover, program) {
|
|
948
546
|
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) => {
|
|
547
|
+
const apiUrl = resolveApiUrl();
|
|
949
548
|
const params = new URLSearchParams();
|
|
950
549
|
if (opts.query) params.append("q", opts.query);
|
|
951
550
|
if (opts.limit) params.append("limit", opts.limit);
|
|
952
551
|
if (opts.cursor) params.append("cursor", opts.cursor);
|
|
953
|
-
const data = await HttpClient.publicGet("/directory/groups", params.toString() ? params : void 0);
|
|
552
|
+
const data = await HttpClient.publicGet(apiUrl, "/directory/groups", params.toString() ? params : void 0);
|
|
954
553
|
if (gOpts.json) {
|
|
955
554
|
printJson(data);
|
|
956
555
|
return;
|
|
@@ -963,7 +562,8 @@ function registerDiscoverGroups(discover, program) {
|
|
|
963
562
|
Members: g.member_count
|
|
964
563
|
})));
|
|
965
564
|
})).command("inspect").description("Inspect a public group").argument("<group-id>", "Group ID").action(withErrorHandler(program, async (gOpts, groupId) => {
|
|
966
|
-
const
|
|
565
|
+
const apiUrl = resolveApiUrl();
|
|
566
|
+
const data = await HttpClient.publicGet(apiUrl, `/directory/groups/${groupId}`);
|
|
967
567
|
if (gOpts.json) {
|
|
968
568
|
printJson(data);
|
|
969
569
|
return;
|
|
@@ -990,6 +590,7 @@ function fmtList(v) {
|
|
|
990
590
|
return Array.isArray(v) ? v.join(", ") : String(v ?? "");
|
|
991
591
|
}
|
|
992
592
|
async function doAgentSearch(opts, gOpts) {
|
|
593
|
+
const apiUrl = resolveApiUrl();
|
|
993
594
|
const params = new URLSearchParams();
|
|
994
595
|
if (opts.query) params.append("q", opts.query);
|
|
995
596
|
for (const c of opts.category) params.append("category", c);
|
|
@@ -1001,7 +602,7 @@ async function doAgentSearch(opts, gOpts) {
|
|
|
1001
602
|
if (opts.limit) params.append("limit", opts.limit);
|
|
1002
603
|
if (opts.cursor) params.append("cursor", opts.cursor);
|
|
1003
604
|
if (opts.sort) params.append("sort", opts.sort);
|
|
1004
|
-
const data = await HttpClient.publicGet("/directory/agents", params);
|
|
605
|
+
const data = await HttpClient.publicGet(apiUrl, "/directory/agents", params);
|
|
1005
606
|
if (gOpts.json) {
|
|
1006
607
|
printJson(data);
|
|
1007
608
|
return;
|
|
@@ -1030,7 +631,8 @@ function registerDiscover(program) {
|
|
|
1030
631
|
addAgentSearchCommand(discover, "agents", "List agents in the directory", program, false);
|
|
1031
632
|
addAgentSearchCommand(discover, "search", "Search agents (--query required)", program, true);
|
|
1032
633
|
discover.command("categories").description("List directory categories with counts").action(withErrorHandler(program, async (gOpts) => {
|
|
1033
|
-
const
|
|
634
|
+
const apiUrl = resolveApiUrl();
|
|
635
|
+
const data = await HttpClient.publicGet(apiUrl, "/directory/categories");
|
|
1034
636
|
if (gOpts.json) {
|
|
1035
637
|
printJson(data);
|
|
1036
638
|
return;
|
|
@@ -1041,15 +643,15 @@ function registerDiscover(program) {
|
|
|
1041
643
|
})));
|
|
1042
644
|
}));
|
|
1043
645
|
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
|
|
646
|
+
const apiUrl = resolveApiUrl();
|
|
1045
647
|
let resolvedId = agentIdArg;
|
|
1046
|
-
if (agentIdArg.includes("@")) resolvedId = await resolveHandleViaWebFinger(agentIdArg);
|
|
648
|
+
if (agentIdArg.includes("@")) resolvedId = await resolveHandleViaWebFinger(apiUrl, agentIdArg);
|
|
1047
649
|
if (!UUID_RE.test(resolvedId)) {
|
|
1048
650
|
printError(`Could not resolve '${agentIdArg}' to an agent UUID. Provide a UUID or a valid handle (e.g. agent@org.rine.network).`);
|
|
1049
651
|
process.exitCode = 1;
|
|
1050
652
|
return;
|
|
1051
653
|
}
|
|
1052
|
-
const data = await HttpClient.publicGet(`/directory/agents/${resolvedId}`);
|
|
654
|
+
const data = await HttpClient.publicGet(apiUrl, `/directory/agents/${resolvedId}`);
|
|
1053
655
|
if (gOpts.json) {
|
|
1054
656
|
printJson(data);
|
|
1055
657
|
return;
|
|
@@ -1187,17 +789,18 @@ function registerGroup(program) {
|
|
|
1187
789
|
printText("Join request submitted (pending approval)");
|
|
1188
790
|
}
|
|
1189
791
|
}));
|
|
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);
|
|
792
|
+
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) => {
|
|
793
|
+
const agentId = await resolveAgent(apiUrl, await fetchAgents(client), opts.agent, gOpts.as);
|
|
1192
794
|
await client.delete(`/groups/${groupId}/members/${agentId}`);
|
|
1193
795
|
printMutationOk("Left group", gOpts.json);
|
|
1194
796
|
}));
|
|
1195
|
-
group.command("kick").description("Remove a member from the group (admin)").argument("<group-id>", "Group ID").argument("<agent-id>", "Agent ID to remove").action(withClient(program, async ({ client, gOpts }, groupId, agentId) => {
|
|
1196
|
-
await
|
|
797
|
+
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) => {
|
|
798
|
+
const resolved = await resolveToUuid(apiUrl, agentId);
|
|
799
|
+
await client.delete(`/groups/${groupId}/members/${resolved}`);
|
|
1197
800
|
printMutationOk("Member removed", gOpts.json);
|
|
1198
801
|
}));
|
|
1199
|
-
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) => {
|
|
1200
|
-
const body = { agent_id: opts.agent };
|
|
802
|
+
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) => {
|
|
803
|
+
const body = { agent_id: await resolveToUuid(apiUrl, opts.agent) };
|
|
1201
804
|
if (opts.message) body.message = opts.message;
|
|
1202
805
|
const data = await client.post(`/groups/${groupId}/invite`, body);
|
|
1203
806
|
if (gOpts.json) printJson(data);
|
|
@@ -1223,401 +826,12 @@ function registerGroup(program) {
|
|
|
1223
826
|
}));
|
|
1224
827
|
}
|
|
1225
828
|
//#endregion
|
|
1226
|
-
//#region src/crypto/envelope.ts
|
|
1227
|
-
const SIGNATURE_SIZE = 64;
|
|
1228
|
-
const MAX_KID_LENGTH = 255;
|
|
1229
|
-
function encodeEnvelope(kid, signature, payload) {
|
|
1230
|
-
const kidBytes = new TextEncoder().encode(kid);
|
|
1231
|
-
if (kidBytes.length > MAX_KID_LENGTH) throw new Error(`kid too long: ${kidBytes.length} bytes (max ${MAX_KID_LENGTH})`);
|
|
1232
|
-
if (signature.length !== SIGNATURE_SIZE) throw new Error(`signature must be ${SIGNATURE_SIZE} bytes, got ${signature.length}`);
|
|
1233
|
-
const buf = new Uint8Array(1 + kidBytes.length + SIGNATURE_SIZE + payload.length);
|
|
1234
|
-
buf[0] = kidBytes.length;
|
|
1235
|
-
buf.set(kidBytes, 1);
|
|
1236
|
-
buf.set(signature, 1 + kidBytes.length);
|
|
1237
|
-
buf.set(payload, 1 + kidBytes.length + SIGNATURE_SIZE);
|
|
1238
|
-
return buf;
|
|
1239
|
-
}
|
|
1240
|
-
function decodeEnvelope(data) {
|
|
1241
|
-
if (data.length < 1 + SIGNATURE_SIZE) throw new Error("envelope too short");
|
|
1242
|
-
const kidLen = data[0];
|
|
1243
|
-
const minLen = 1 + kidLen + SIGNATURE_SIZE;
|
|
1244
|
-
if (data.length < minLen) throw new Error("envelope too short for declared kid length");
|
|
1245
|
-
return {
|
|
1246
|
-
kid: new TextDecoder("utf-8", { fatal: true }).decode(data.subarray(1, 1 + kidLen)),
|
|
1247
|
-
signature: data.slice(1 + kidLen, 1 + kidLen + SIGNATURE_SIZE),
|
|
1248
|
-
payload: data.slice(1 + kidLen + SIGNATURE_SIZE)
|
|
1249
|
-
};
|
|
1250
|
-
}
|
|
1251
|
-
//#endregion
|
|
1252
|
-
//#region src/crypto/hpke.ts
|
|
1253
|
-
const VERSION_HPKE = 1;
|
|
1254
|
-
const ENC_SIZE = 32;
|
|
1255
|
-
const INFO = new TextEncoder().encode("rine.e2ee.v1.hpke");
|
|
1256
|
-
const suite = new CipherSuite({
|
|
1257
|
-
kem: new DhkemX25519HkdfSha256(),
|
|
1258
|
-
kdf: new HkdfSha256(),
|
|
1259
|
-
aead: new Aes256Gcm()
|
|
1260
|
-
});
|
|
1261
|
-
async function seal(recipientPublicKey, innerEnvelope, aad) {
|
|
1262
|
-
const pk = await suite.kem.deserializePublicKey(recipientPublicKey);
|
|
1263
|
-
const sender = await suite.createSenderContext({
|
|
1264
|
-
recipientPublicKey: pk,
|
|
1265
|
-
info: INFO
|
|
1266
|
-
});
|
|
1267
|
-
const ciphertext = new Uint8Array(await sender.seal(innerEnvelope, aad));
|
|
1268
|
-
const enc = new Uint8Array(sender.enc);
|
|
1269
|
-
const out = new Uint8Array(1 + ENC_SIZE + ciphertext.length);
|
|
1270
|
-
out[0] = VERSION_HPKE;
|
|
1271
|
-
out.set(enc, 1);
|
|
1272
|
-
out.set(ciphertext, 1 + ENC_SIZE);
|
|
1273
|
-
return out;
|
|
1274
|
-
}
|
|
1275
|
-
async function open(recipientPrivateKey, encryptedPayload, aad) {
|
|
1276
|
-
if (encryptedPayload.length < 1 + ENC_SIZE + 1) throw new Error("encrypted payload too short");
|
|
1277
|
-
const version = encryptedPayload[0];
|
|
1278
|
-
if (version !== VERSION_HPKE) throw new Error(`unsupported version: 0x${version?.toString(16).padStart(2, "0")}`);
|
|
1279
|
-
const enc = encryptedPayload.slice(1, 1 + ENC_SIZE);
|
|
1280
|
-
const ct = encryptedPayload.slice(1 + ENC_SIZE);
|
|
1281
|
-
const sk = await suite.kem.deserializePrivateKey(recipientPrivateKey);
|
|
1282
|
-
const recipient = await suite.createRecipientContext({
|
|
1283
|
-
recipientKey: sk,
|
|
1284
|
-
enc,
|
|
1285
|
-
info: INFO
|
|
1286
|
-
});
|
|
1287
|
-
return new Uint8Array(await recipient.open(ct, aad));
|
|
1288
|
-
}
|
|
1289
|
-
//#endregion
|
|
1290
|
-
//#region src/crypto/sender-keys-helpers.ts
|
|
1291
|
-
function uuidToBytes(uuid) {
|
|
1292
|
-
const matches = uuid.replace(/-/g, "").match(/../g);
|
|
1293
|
-
if (!matches) throw new Error(`Invalid UUID: ${uuid}`);
|
|
1294
|
-
return new Uint8Array(matches.map((b) => Number.parseInt(b, 16)));
|
|
1295
|
-
}
|
|
1296
|
-
function bytesToUuid(bytes) {
|
|
1297
|
-
const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1298
|
-
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
1299
|
-
}
|
|
1300
|
-
function senderKeysDir(agentId) {
|
|
1301
|
-
validatePathId(agentId, "agent ID");
|
|
1302
|
-
return join(getConfigDir(), "keys", agentId, "sender_keys");
|
|
1303
|
-
}
|
|
1304
|
-
function saveSenderKeyState(agentId, groupId, states) {
|
|
1305
|
-
validatePathId(groupId, "group ID");
|
|
1306
|
-
const dir = senderKeysDir(agentId);
|
|
1307
|
-
fs.mkdirSync(dir, {
|
|
1308
|
-
recursive: true,
|
|
1309
|
-
mode: 448
|
|
1310
|
-
});
|
|
1311
|
-
const filePath = join(dir, `${groupId}.json`);
|
|
1312
|
-
const data = states.map((s) => ({
|
|
1313
|
-
...s,
|
|
1314
|
-
chainKey: toBase64Url(s.chainKey)
|
|
1315
|
-
}));
|
|
1316
|
-
fs.writeFileSync(filePath, JSON.stringify(data), "utf-8");
|
|
1317
|
-
fs.chmodSync(filePath, 384);
|
|
1318
|
-
}
|
|
1319
|
-
function loadSenderKeyStates(agentId, groupId) {
|
|
1320
|
-
validatePathId(groupId, "group ID");
|
|
1321
|
-
const filePath = join(senderKeysDir(agentId), `${groupId}.json`);
|
|
1322
|
-
if (!fs.existsSync(filePath)) return [];
|
|
1323
|
-
const raw = fs.readFileSync(filePath, "utf-8");
|
|
1324
|
-
return JSON.parse(raw).map((s) => ({
|
|
1325
|
-
...s,
|
|
1326
|
-
chainKey: fromBase64Url(s.chainKey)
|
|
1327
|
-
}));
|
|
1328
|
-
}
|
|
1329
|
-
//#endregion
|
|
1330
|
-
//#region src/crypto/sign.ts
|
|
1331
|
-
function signPayload(signingPrivateKey, plaintext) {
|
|
1332
|
-
return ed25519.sign(plaintext, signingPrivateKey);
|
|
1333
|
-
}
|
|
1334
|
-
function verifySignature(signingPublicKey, plaintext, signature) {
|
|
1335
|
-
try {
|
|
1336
|
-
return ed25519.verify(signature, plaintext, signingPublicKey);
|
|
1337
|
-
} catch {
|
|
1338
|
-
return false;
|
|
1339
|
-
}
|
|
1340
|
-
}
|
|
1341
|
-
//#endregion
|
|
1342
|
-
//#region src/crypto/sender-keys.ts
|
|
1343
|
-
const ROTATION_MESSAGE_LIMIT = 100;
|
|
1344
|
-
const ROTATION_AGE_MS = 10080 * 60 * 1e3;
|
|
1345
|
-
function needsRotation(state) {
|
|
1346
|
-
if (state.messageIndex >= ROTATION_MESSAGE_LIMIT) return true;
|
|
1347
|
-
if (state.createdAt && Date.now() - state.createdAt > ROTATION_AGE_MS) return true;
|
|
1348
|
-
return false;
|
|
1349
|
-
}
|
|
1350
|
-
const VERSION_SENDER_KEY = 2;
|
|
1351
|
-
const SENDER_KEY_ID_SIZE = 16;
|
|
1352
|
-
const MESSAGE_INDEX_SIZE = 4;
|
|
1353
|
-
const NONCE_SIZE = 12;
|
|
1354
|
-
const MAX_SKIP = 1e3;
|
|
1355
|
-
/** Copy Uint8Array into a plain ArrayBuffer-backed Uint8Array (required by Web Crypto). */
|
|
1356
|
-
function toPlainBuffer(src) {
|
|
1357
|
-
const buf = new Uint8Array(src.length);
|
|
1358
|
-
buf.set(src);
|
|
1359
|
-
return buf;
|
|
1360
|
-
}
|
|
1361
|
-
function generateSenderKey(groupId, senderAgentId) {
|
|
1362
|
-
const chainKey = new Uint8Array(32);
|
|
1363
|
-
crypto.getRandomValues(chainKey);
|
|
1364
|
-
return {
|
|
1365
|
-
senderKeyId: crypto.randomUUID(),
|
|
1366
|
-
groupId,
|
|
1367
|
-
senderAgentId,
|
|
1368
|
-
chainKey,
|
|
1369
|
-
messageIndex: 0,
|
|
1370
|
-
createdAt: Date.now()
|
|
1371
|
-
};
|
|
1372
|
-
}
|
|
1373
|
-
function deriveMessageKey(chainKey) {
|
|
1374
|
-
return {
|
|
1375
|
-
messageKey: hmac(sha256, chainKey, new Uint8Array([1])),
|
|
1376
|
-
nextChainKey: hmac(sha256, chainKey, new Uint8Array([2]))
|
|
1377
|
-
};
|
|
1378
|
-
}
|
|
1379
|
-
function advanceChain(state, targetIndex) {
|
|
1380
|
-
if (targetIndex - state.messageIndex > MAX_SKIP) throw new Error(`too many skipped messages: ${targetIndex - state.messageIndex} (max ${MAX_SKIP})`);
|
|
1381
|
-
const skippedKeys = /* @__PURE__ */ new Map();
|
|
1382
|
-
let chainKey = state.chainKey;
|
|
1383
|
-
let index = state.messageIndex;
|
|
1384
|
-
while (index < targetIndex) {
|
|
1385
|
-
const { messageKey, nextChainKey } = deriveMessageKey(chainKey);
|
|
1386
|
-
skippedKeys.set(index, messageKey);
|
|
1387
|
-
chainKey = nextChainKey;
|
|
1388
|
-
index++;
|
|
1389
|
-
}
|
|
1390
|
-
return {
|
|
1391
|
-
updatedState: {
|
|
1392
|
-
...state,
|
|
1393
|
-
chainKey,
|
|
1394
|
-
messageIndex: index
|
|
1395
|
-
},
|
|
1396
|
-
skippedKeys
|
|
1397
|
-
};
|
|
1398
|
-
}
|
|
1399
|
-
/** Build AAD for sender-key encryption: binds ciphertext to sender_key_id and message_index. */
|
|
1400
|
-
function buildSenderKeyAad(senderKeyId, messageIndex) {
|
|
1401
|
-
const aad = new Uint8Array(SENDER_KEY_ID_SIZE + MESSAGE_INDEX_SIZE);
|
|
1402
|
-
aad.set(senderKeyId, 0);
|
|
1403
|
-
new DataView(aad.buffer).setUint32(SENDER_KEY_ID_SIZE, messageIndex, false);
|
|
1404
|
-
return aad;
|
|
1405
|
-
}
|
|
1406
|
-
async function sealGroup(senderSigningPrivateKey, senderSigningKid, state, plaintext) {
|
|
1407
|
-
const innerEnvelope = encodeEnvelope(senderSigningKid, signPayload(senderSigningPrivateKey, plaintext), plaintext);
|
|
1408
|
-
const { messageKey, nextChainKey } = deriveMessageKey(state.chainKey);
|
|
1409
|
-
const nonce = new Uint8Array(NONCE_SIZE);
|
|
1410
|
-
crypto.getRandomValues(nonce);
|
|
1411
|
-
const idBytes = uuidToBytes(state.senderKeyId);
|
|
1412
|
-
const aad = buildSenderKeyAad(idBytes, state.messageIndex);
|
|
1413
|
-
const cryptoKey = await crypto.subtle.importKey("raw", toPlainBuffer(messageKey), "AES-GCM", false, ["encrypt"]);
|
|
1414
|
-
const ciphertext = new Uint8Array(await crypto.subtle.encrypt({
|
|
1415
|
-
name: "AES-GCM",
|
|
1416
|
-
iv: nonce,
|
|
1417
|
-
additionalData: toPlainBuffer(aad)
|
|
1418
|
-
}, cryptoKey, toPlainBuffer(innerEnvelope)));
|
|
1419
|
-
messageKey.fill(0);
|
|
1420
|
-
const out = new Uint8Array(1 + SENDER_KEY_ID_SIZE + MESSAGE_INDEX_SIZE + NONCE_SIZE + ciphertext.length);
|
|
1421
|
-
let offset = 0;
|
|
1422
|
-
out[offset++] = VERSION_SENDER_KEY;
|
|
1423
|
-
out.set(idBytes, offset);
|
|
1424
|
-
offset += SENDER_KEY_ID_SIZE;
|
|
1425
|
-
new DataView(out.buffer).setUint32(offset, state.messageIndex, false);
|
|
1426
|
-
offset += MESSAGE_INDEX_SIZE;
|
|
1427
|
-
out.set(nonce, offset);
|
|
1428
|
-
offset += NONCE_SIZE;
|
|
1429
|
-
out.set(ciphertext, offset);
|
|
1430
|
-
return {
|
|
1431
|
-
encryptedPayload: out,
|
|
1432
|
-
updatedState: {
|
|
1433
|
-
...state,
|
|
1434
|
-
chainKey: nextChainKey,
|
|
1435
|
-
messageIndex: state.messageIndex + 1
|
|
1436
|
-
}
|
|
1437
|
-
};
|
|
1438
|
-
}
|
|
1439
|
-
async function openGroup(senderKeyStates, encryptedPayload) {
|
|
1440
|
-
const minLen = 1 + SENDER_KEY_ID_SIZE + MESSAGE_INDEX_SIZE + NONCE_SIZE;
|
|
1441
|
-
if (encryptedPayload.length < minLen) throw new Error("encrypted payload too short");
|
|
1442
|
-
const version = encryptedPayload[0];
|
|
1443
|
-
if (version !== VERSION_SENDER_KEY) throw new Error(`unsupported version: 0x${version?.toString(16).padStart(2, "0")}`);
|
|
1444
|
-
let offset = 1;
|
|
1445
|
-
const senderKeyId = bytesToUuid(encryptedPayload.slice(offset, offset + SENDER_KEY_ID_SIZE));
|
|
1446
|
-
offset += SENDER_KEY_ID_SIZE;
|
|
1447
|
-
const messageIndex = new DataView(encryptedPayload.buffer, encryptedPayload.byteOffset).getUint32(offset, false);
|
|
1448
|
-
offset += MESSAGE_INDEX_SIZE;
|
|
1449
|
-
const nonce = encryptedPayload.slice(offset, offset + NONCE_SIZE);
|
|
1450
|
-
offset += NONCE_SIZE;
|
|
1451
|
-
const ciphertext = encryptedPayload.slice(offset);
|
|
1452
|
-
const state = senderKeyStates.get(senderKeyId);
|
|
1453
|
-
if (!state) throw new Error(`unknown sender key id: ${senderKeyId}`);
|
|
1454
|
-
let messageKey;
|
|
1455
|
-
let updatedState;
|
|
1456
|
-
if (messageIndex < state.messageIndex) {
|
|
1457
|
-
const cached = state.skippedKeys?.[messageIndex];
|
|
1458
|
-
if (!cached) throw new Error("message key expired or already consumed");
|
|
1459
|
-
const { fromBase64Url } = await Promise.resolve().then(() => keys_exports);
|
|
1460
|
-
messageKey = fromBase64Url(cached);
|
|
1461
|
-
const newSkipped = { ...state.skippedKeys };
|
|
1462
|
-
delete newSkipped[messageIndex];
|
|
1463
|
-
updatedState = {
|
|
1464
|
-
...state,
|
|
1465
|
-
skippedKeys: newSkipped
|
|
1466
|
-
};
|
|
1467
|
-
} else if (messageIndex === state.messageIndex) {
|
|
1468
|
-
const { messageKey: mk, nextChainKey } = deriveMessageKey(state.chainKey);
|
|
1469
|
-
messageKey = mk;
|
|
1470
|
-
updatedState = {
|
|
1471
|
-
...state,
|
|
1472
|
-
chainKey: nextChainKey,
|
|
1473
|
-
messageIndex: state.messageIndex + 1
|
|
1474
|
-
};
|
|
1475
|
-
} else {
|
|
1476
|
-
const { updatedState: advanced, skippedKeys: newSkipped } = advanceChain(state, messageIndex);
|
|
1477
|
-
const { messageKey: mk, nextChainKey } = deriveMessageKey(advanced.chainKey);
|
|
1478
|
-
messageKey = mk;
|
|
1479
|
-
const mergedSkipped = { ...state.skippedKeys ?? {} };
|
|
1480
|
-
for (const [idx, key] of newSkipped) {
|
|
1481
|
-
mergedSkipped[idx] = toBase64Url(key);
|
|
1482
|
-
key.fill(0);
|
|
1483
|
-
}
|
|
1484
|
-
const entries = Object.entries(mergedSkipped);
|
|
1485
|
-
if (entries.length > MAX_SKIP) {
|
|
1486
|
-
entries.sort((a, b) => Number(a[0]) - Number(b[0]));
|
|
1487
|
-
const excess = entries.length - MAX_SKIP;
|
|
1488
|
-
for (let i = 0; i < excess; i++) delete mergedSkipped[Number(entries[i][0])];
|
|
1489
|
-
}
|
|
1490
|
-
updatedState = {
|
|
1491
|
-
...advanced,
|
|
1492
|
-
chainKey: nextChainKey,
|
|
1493
|
-
messageIndex: messageIndex + 1,
|
|
1494
|
-
skippedKeys: mergedSkipped
|
|
1495
|
-
};
|
|
1496
|
-
}
|
|
1497
|
-
const aad = buildSenderKeyAad(encryptedPayload.slice(1, 1 + SENDER_KEY_ID_SIZE), messageIndex);
|
|
1498
|
-
const cryptoKey = await crypto.subtle.importKey("raw", toPlainBuffer(messageKey), "AES-GCM", false, ["decrypt"]);
|
|
1499
|
-
const innerEnvelope = new Uint8Array(await crypto.subtle.decrypt({
|
|
1500
|
-
name: "AES-GCM",
|
|
1501
|
-
iv: nonce,
|
|
1502
|
-
additionalData: toPlainBuffer(aad)
|
|
1503
|
-
}, cryptoKey, toPlainBuffer(ciphertext)));
|
|
1504
|
-
messageKey.fill(0);
|
|
1505
|
-
return {
|
|
1506
|
-
innerEnvelope,
|
|
1507
|
-
senderKeyId,
|
|
1508
|
-
messageIndex,
|
|
1509
|
-
updatedState
|
|
1510
|
-
};
|
|
1511
|
-
}
|
|
1512
|
-
//#endregion
|
|
1513
|
-
//#region src/crypto/message.ts
|
|
1514
|
-
async function verifyEnvelopeSender(decoded, client) {
|
|
1515
|
-
let verified = false;
|
|
1516
|
-
let verificationStatus = "unverifiable";
|
|
1517
|
-
try {
|
|
1518
|
-
const senderAgentId = agentIdFromKid(decoded.kid);
|
|
1519
|
-
verified = verifySignature(jwkToPublicKey((await client.get(`/agents/${senderAgentId}/keys`)).signing_public_key), decoded.payload, decoded.signature);
|
|
1520
|
-
verificationStatus = verified ? "verified" : "invalid";
|
|
1521
|
-
} catch {}
|
|
1522
|
-
return {
|
|
1523
|
-
verified,
|
|
1524
|
-
verificationStatus
|
|
1525
|
-
};
|
|
1526
|
-
}
|
|
1527
|
-
function signingKid(agentId) {
|
|
1528
|
-
return `rine:${agentId}`;
|
|
1529
|
-
}
|
|
1530
|
-
async function encryptMessage(senderAgentId, recipientEncryptionPk, payload) {
|
|
1531
|
-
const keys = loadAgentKeys(senderAgentId);
|
|
1532
|
-
const plaintext = new TextEncoder().encode(JSON.stringify(payload));
|
|
1533
|
-
const kid = signingKid(senderAgentId);
|
|
1534
|
-
return {
|
|
1535
|
-
encrypted_payload: toBase64Url(await seal(recipientEncryptionPk, encodeEnvelope(kid, signPayload(keys.signing.privateKey, plaintext), plaintext))),
|
|
1536
|
-
encryption_version: "hpke-v1",
|
|
1537
|
-
sender_signing_kid: kid
|
|
1538
|
-
};
|
|
1539
|
-
}
|
|
1540
|
-
async function fetchRecipientEncryptionKey(client, agentId) {
|
|
1541
|
-
return jwkToPublicKey((await client.get(`/agents/${agentId}/keys`)).encryption_public_key);
|
|
1542
|
-
}
|
|
1543
|
-
async function decryptMessage(recipientAgentId, encryptedPayloadB64, client) {
|
|
1544
|
-
const keys = loadAgentKeys(recipientAgentId);
|
|
1545
|
-
const encrypted = fromBase64Url(encryptedPayloadB64);
|
|
1546
|
-
const decoded = decodeEnvelope(await open(keys.encryption.privateKey, encrypted));
|
|
1547
|
-
const plaintext = new TextDecoder("utf-8", { fatal: true }).decode(decoded.payload);
|
|
1548
|
-
const { verified, verificationStatus } = await verifyEnvelopeSender(decoded, client);
|
|
1549
|
-
return {
|
|
1550
|
-
plaintext,
|
|
1551
|
-
senderKid: decoded.kid,
|
|
1552
|
-
verified,
|
|
1553
|
-
verificationStatus
|
|
1554
|
-
};
|
|
1555
|
-
}
|
|
1556
|
-
async function encryptGroupMessage(senderAgentId, groupId, senderKeyState, payload) {
|
|
1557
|
-
const keys = loadAgentKeys(senderAgentId);
|
|
1558
|
-
const plaintext = new TextEncoder().encode(JSON.stringify(payload));
|
|
1559
|
-
const kid = signingKid(senderAgentId);
|
|
1560
|
-
const selfReadKey = deriveMessageKey(senderKeyState.chainKey).messageKey;
|
|
1561
|
-
const selfReadIndex = senderKeyState.messageIndex;
|
|
1562
|
-
const { encryptedPayload, updatedState } = await sealGroup(keys.signing.privateKey, kid, senderKeyState, plaintext);
|
|
1563
|
-
updatedState.skippedKeys = {
|
|
1564
|
-
...updatedState.skippedKeys ?? {},
|
|
1565
|
-
[selfReadIndex]: toBase64Url(selfReadKey)
|
|
1566
|
-
};
|
|
1567
|
-
selfReadKey.fill(0);
|
|
1568
|
-
saveSenderKeyState(senderAgentId, groupId, [...loadSenderKeyStates(senderAgentId, groupId).filter((s) => s.senderKeyId !== updatedState.senderKeyId), updatedState]);
|
|
1569
|
-
return {
|
|
1570
|
-
result: {
|
|
1571
|
-
encrypted_payload: toBase64Url(encryptedPayload),
|
|
1572
|
-
encryption_version: "sender-key-v1",
|
|
1573
|
-
sender_signing_kid: kid
|
|
1574
|
-
},
|
|
1575
|
-
updatedState
|
|
1576
|
-
};
|
|
1577
|
-
}
|
|
1578
|
-
async function decryptGroupMessage(recipientAgentId, groupId, encryptedPayloadB64, client) {
|
|
1579
|
-
const states = loadSenderKeyStates(recipientAgentId, groupId);
|
|
1580
|
-
const stateMap = /* @__PURE__ */ new Map();
|
|
1581
|
-
for (const s of states) stateMap.set(s.senderKeyId, s);
|
|
1582
|
-
const { innerEnvelope, updatedState, messageIndex } = await openGroup(stateMap, fromBase64Url(encryptedPayloadB64));
|
|
1583
|
-
const decoded = decodeEnvelope(innerEnvelope);
|
|
1584
|
-
const plaintext = new TextDecoder("utf-8", { fatal: true }).decode(decoded.payload);
|
|
1585
|
-
const { verified, verificationStatus } = await verifyEnvelopeSender(decoded, client);
|
|
1586
|
-
if (verificationStatus === "invalid") throw new Error("Sender signature verification failed");
|
|
1587
|
-
if (updatedState.senderAgentId === recipientAgentId) {
|
|
1588
|
-
const prevCached = stateMap.get(updatedState.senderKeyId)?.skippedKeys?.[messageIndex];
|
|
1589
|
-
if (prevCached && !updatedState.skippedKeys?.[messageIndex]) updatedState.skippedKeys = {
|
|
1590
|
-
...updatedState.skippedKeys ?? {},
|
|
1591
|
-
[messageIndex]: prevCached
|
|
1592
|
-
};
|
|
1593
|
-
}
|
|
1594
|
-
stateMap.set(updatedState.senderKeyId, updatedState);
|
|
1595
|
-
saveSenderKeyState(recipientAgentId, groupId, Array.from(stateMap.values()));
|
|
1596
|
-
return {
|
|
1597
|
-
plaintext,
|
|
1598
|
-
senderKid: decoded.kid,
|
|
1599
|
-
verified,
|
|
1600
|
-
verificationStatus
|
|
1601
|
-
};
|
|
1602
|
-
}
|
|
1603
|
-
function getAgentPublicKeys(agentId, agentKeys) {
|
|
1604
|
-
const keys = agentKeys ?? loadAgentKeys(agentId);
|
|
1605
|
-
return {
|
|
1606
|
-
signing_public_key: signingPublicKeyToJWK(keys.signing.publicKey),
|
|
1607
|
-
encryption_public_key: {
|
|
1608
|
-
kty: "OKP",
|
|
1609
|
-
crv: "X25519",
|
|
1610
|
-
x: toBase64Url(keys.encryption.publicKey)
|
|
1611
|
-
}
|
|
1612
|
-
};
|
|
1613
|
-
}
|
|
1614
|
-
//#endregion
|
|
1615
829
|
//#region src/commands/keys.ts
|
|
1616
|
-
function getKeysDir(agentId) {
|
|
1617
|
-
return join(
|
|
830
|
+
function getKeysDir(configDir, agentId) {
|
|
831
|
+
return join(configDir, "keys", agentId);
|
|
1618
832
|
}
|
|
1619
|
-
function backupKeys(agentId) {
|
|
1620
|
-
const dir = getKeysDir(agentId);
|
|
833
|
+
function backupKeys(configDir, agentId) {
|
|
834
|
+
const dir = getKeysDir(configDir, agentId);
|
|
1621
835
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1622
836
|
for (const name of ["signing.key", "encryption.key"]) {
|
|
1623
837
|
const src = join(dir, name);
|
|
@@ -1632,10 +846,11 @@ async function verifyServerKeys(client, agentId, expected) {
|
|
|
1632
846
|
}
|
|
1633
847
|
function registerKeys(program) {
|
|
1634
848
|
const keys = program.command("keys").description("E2EE key management");
|
|
1635
|
-
keys.command("status").description("Show E2EE key status for an agent").option("--agent <id>", "Agent ID").action(withClient(program, async ({ client, gOpts }, opts) => {
|
|
1636
|
-
const
|
|
1637
|
-
const
|
|
1638
|
-
const
|
|
849
|
+
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) => {
|
|
850
|
+
const agents = await fetchAgents(client);
|
|
851
|
+
const agentId = await resolveAgent(apiUrl, agents, opts.agent, gOpts.as);
|
|
852
|
+
const handle = agents.find((a) => a.id === agentId)?.handle ?? "unknown";
|
|
853
|
+
const hasLocal = agentKeysExist(configDir, agentId);
|
|
1639
854
|
let hasServer = false;
|
|
1640
855
|
let serverKeys;
|
|
1641
856
|
try {
|
|
@@ -1647,9 +862,9 @@ function registerKeys(program) {
|
|
|
1647
862
|
let sigFingerprint = "-";
|
|
1648
863
|
let encFingerprint = "-";
|
|
1649
864
|
if (hasLocal) {
|
|
1650
|
-
const
|
|
1651
|
-
sigFingerprint = toBase64Url(
|
|
1652
|
-
encFingerprint = toBase64Url(
|
|
865
|
+
const agentKeys = loadAgentKeys(configDir, agentId);
|
|
866
|
+
sigFingerprint = toBase64Url(agentKeys.signing.publicKey).slice(0, 8);
|
|
867
|
+
encFingerprint = toBase64Url(agentKeys.encryption.publicKey).slice(0, 8);
|
|
1653
868
|
} else if (serverKeys) {
|
|
1654
869
|
sigFingerprint = serverKeys.signing_public_key.x.slice(0, 8);
|
|
1655
870
|
encFingerprint = serverKeys.encryption_public_key.x.slice(0, 8);
|
|
@@ -1694,38 +909,38 @@ function registerKeys(program) {
|
|
|
1694
909
|
}
|
|
1695
910
|
]);
|
|
1696
911
|
}));
|
|
1697
|
-
keys.command("generate").description("Generate new E2EE keys for an agent").option("--agent <id>", "Agent ID").action(withClient(program, async ({ client, gOpts }, opts) => {
|
|
1698
|
-
const agentId = await resolveAgent(client, opts.agent, gOpts.as);
|
|
1699
|
-
if (agentKeysExist(agentId)) {
|
|
912
|
+
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) => {
|
|
913
|
+
const agentId = await resolveAgent(apiUrl, await fetchAgents(client), opts.agent, gOpts.as);
|
|
914
|
+
if (agentKeysExist(configDir, agentId)) {
|
|
1700
915
|
printError(`Keys already exist for agent ${agentId}. Use 'rine keys rotate' to replace them.`);
|
|
1701
916
|
process.exitCode = 1;
|
|
1702
917
|
return;
|
|
1703
918
|
}
|
|
1704
919
|
const agentKeys = generateAgentKeys();
|
|
1705
|
-
const pubKeys = getAgentPublicKeys(agentId, agentKeys);
|
|
920
|
+
const pubKeys = getAgentPublicKeys(configDir, agentId, agentKeys);
|
|
1706
921
|
await client.post(`/agents/${agentId}/keys`, pubKeys);
|
|
1707
922
|
await verifyServerKeys(client, agentId, pubKeys);
|
|
1708
|
-
saveAgentKeys(agentId, agentKeys);
|
|
923
|
+
saveAgentKeys(configDir, agentId, agentKeys);
|
|
1709
924
|
printMutationOk(`E2EE keys generated for agent ${agentId}`, gOpts.json);
|
|
1710
925
|
}));
|
|
1711
|
-
keys.command("rotate").description("Rotate E2EE keys for an agent").option("--agent <id>", "Agent ID").action(withClient(program, async ({ client, gOpts }, opts) => {
|
|
1712
|
-
const agentId = await resolveAgent(client, opts.agent, gOpts.as);
|
|
1713
|
-
if (agentKeysExist(agentId)) backupKeys(agentId);
|
|
926
|
+
keys.command("rotate").description("Rotate E2EE keys for an agent").option("--agent <id>", "Agent ID").action(withClient(program, async ({ client, gOpts, configDir, apiUrl }, opts) => {
|
|
927
|
+
const agentId = await resolveAgent(apiUrl, await fetchAgents(client), opts.agent, gOpts.as);
|
|
928
|
+
if (agentKeysExist(configDir, agentId)) backupKeys(configDir, agentId);
|
|
1714
929
|
const agentKeys = generateAgentKeys();
|
|
1715
|
-
const pubKeys = getAgentPublicKeys(agentId, agentKeys);
|
|
930
|
+
const pubKeys = getAgentPublicKeys(configDir, agentId, agentKeys);
|
|
1716
931
|
await client.post(`/agents/${agentId}/keys`, pubKeys);
|
|
1717
932
|
await verifyServerKeys(client, agentId, pubKeys);
|
|
1718
|
-
saveAgentKeys(agentId, agentKeys);
|
|
933
|
+
saveAgentKeys(configDir, agentId, agentKeys);
|
|
1719
934
|
printMutationOk(`E2EE keys rotated for agent ${agentId}`, gOpts.json);
|
|
1720
935
|
}));
|
|
1721
|
-
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) => {
|
|
1722
|
-
const agentId = await resolveAgent(client, opts.agent, gOpts.as);
|
|
1723
|
-
if (!agentKeysExist(agentId)) {
|
|
936
|
+
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) => {
|
|
937
|
+
const agentId = await resolveAgent(apiUrl, await fetchAgents(client), opts.agent, gOpts.as);
|
|
938
|
+
if (!agentKeysExist(configDir, agentId)) {
|
|
1724
939
|
printError(`No keys found for agent ${agentId}`);
|
|
1725
940
|
process.exitCode = 1;
|
|
1726
941
|
return;
|
|
1727
942
|
}
|
|
1728
|
-
const agentKeys = loadAgentKeys(agentId);
|
|
943
|
+
const agentKeys = loadAgentKeys(configDir, agentId);
|
|
1729
944
|
const exported = {
|
|
1730
945
|
agent_id: agentId,
|
|
1731
946
|
signing_private_key: toBase64Url(agentKeys.signing.privateKey),
|
|
@@ -1773,7 +988,7 @@ function registerKeys(program) {
|
|
|
1773
988
|
process.exitCode = 2;
|
|
1774
989
|
return;
|
|
1775
990
|
}
|
|
1776
|
-
const dir = join(
|
|
991
|
+
const dir = join(resolveConfigDir(), "keys", data.agent_id);
|
|
1777
992
|
fs.mkdirSync(dir, {
|
|
1778
993
|
recursive: true,
|
|
1779
994
|
mode: 448
|
|
@@ -1788,104 +1003,7 @@ function registerKeys(program) {
|
|
|
1788
1003
|
}));
|
|
1789
1004
|
}
|
|
1790
1005
|
//#endregion
|
|
1791
|
-
//#region src/crypto/ingest.ts
|
|
1792
|
-
/**
|
|
1793
|
-
* Auto-ingest a sender key distribution from a decrypted message.
|
|
1794
|
-
* Only ingests if the message is a `rine.v1.sender_key_distribution`,
|
|
1795
|
-
* the signature was verified, and the key ID is not already known.
|
|
1796
|
-
*
|
|
1797
|
-
* Returns true if a new key was ingested.
|
|
1798
|
-
*/
|
|
1799
|
-
function ingestSenderKeyDistribution(agentId, messageType, result) {
|
|
1800
|
-
if (messageType !== "rine.v1.sender_key_distribution") return false;
|
|
1801
|
-
if (!result.verified) return false;
|
|
1802
|
-
try {
|
|
1803
|
-
const dist = JSON.parse(result.plaintext);
|
|
1804
|
-
const existing = loadSenderKeyStates(agentId, dist.group_id);
|
|
1805
|
-
if (existing.some((s) => s.senderKeyId === dist.sender_key_id)) return false;
|
|
1806
|
-
const newState = {
|
|
1807
|
-
senderKeyId: dist.sender_key_id,
|
|
1808
|
-
groupId: dist.group_id,
|
|
1809
|
-
senderAgentId: agentIdFromKid(result.senderKid),
|
|
1810
|
-
chainKey: fromBase64Url(dist.chain_key),
|
|
1811
|
-
messageIndex: dist.message_index
|
|
1812
|
-
};
|
|
1813
|
-
saveSenderKeyState(agentId, dist.group_id, [...existing, newState]);
|
|
1814
|
-
return true;
|
|
1815
|
-
} catch {
|
|
1816
|
-
return false;
|
|
1817
|
-
}
|
|
1818
|
-
}
|
|
1819
|
-
//#endregion
|
|
1820
1006
|
//#region src/commands/sender-key-ops.ts
|
|
1821
|
-
async function distributeSenderKey(client, senderAgentId, state, groupId, recipientIds, extraHeaders) {
|
|
1822
|
-
if (recipientIds.length === 0) return [];
|
|
1823
|
-
const batchKeys = await client.get("/agents/keys", { ids: recipientIds.join(",") });
|
|
1824
|
-
const distPayload = {
|
|
1825
|
-
group_id: groupId,
|
|
1826
|
-
sender_key_id: state.senderKeyId,
|
|
1827
|
-
chain_key: toBase64Url(state.chainKey),
|
|
1828
|
-
message_index: state.messageIndex,
|
|
1829
|
-
sender_agent_id: senderAgentId
|
|
1830
|
-
};
|
|
1831
|
-
const succeeded = [];
|
|
1832
|
-
const failed = [];
|
|
1833
|
-
for (const recipientId of recipientIds) {
|
|
1834
|
-
const recipientKeyData = batchKeys.keys[recipientId];
|
|
1835
|
-
if (!recipientKeyData) {
|
|
1836
|
-
failed.push(recipientId);
|
|
1837
|
-
continue;
|
|
1838
|
-
}
|
|
1839
|
-
try {
|
|
1840
|
-
const encrypted = await encryptMessage(senderAgentId, fromBase64Url(recipientKeyData.encryption_public_key.x), distPayload);
|
|
1841
|
-
await client.post("/messages", {
|
|
1842
|
-
to_agent_id: recipientId,
|
|
1843
|
-
type: "rine.v1.sender_key_distribution",
|
|
1844
|
-
...encrypted
|
|
1845
|
-
}, extraHeaders);
|
|
1846
|
-
succeeded.push(recipientId);
|
|
1847
|
-
} catch {
|
|
1848
|
-
failed.push(recipientId);
|
|
1849
|
-
}
|
|
1850
|
-
}
|
|
1851
|
-
if (failed.length > 0) printError(`Sender key distribution failed for ${failed.length} recipient(s): ${failed.join(", ")}`);
|
|
1852
|
-
return succeeded;
|
|
1853
|
-
}
|
|
1854
|
-
async function getOrCreateSenderKey(client, senderAgentId, groupHandle, extraHeaders) {
|
|
1855
|
-
const firstGroup = (await client.get("/groups", { handle: groupHandle })).items?.[0];
|
|
1856
|
-
if (!firstGroup) throw new Error(`Group not found: ${groupHandle}`);
|
|
1857
|
-
const groupId = firstGroup.id;
|
|
1858
|
-
const memberIds = (await client.get(`/groups/${groupId}/members`)).items.map((m) => m.agent_id);
|
|
1859
|
-
const senderIdLower = senderAgentId.toLowerCase();
|
|
1860
|
-
if (!memberIds.some((id) => id.toLowerCase() === senderIdLower)) throw new Error("You are not a member of this group");
|
|
1861
|
-
const recipientIds = memberIds.filter((id) => id.toLowerCase() !== senderIdLower);
|
|
1862
|
-
const states = loadSenderKeyStates(senderAgentId, groupId);
|
|
1863
|
-
const own = states.find((s) => s.senderAgentId === senderAgentId);
|
|
1864
|
-
const currentMemberSet = new Set(memberIds.map((id) => id.toLowerCase()));
|
|
1865
|
-
const memberRemoved = own?.distributedTo?.some((id) => !currentMemberSet.has(id.toLowerCase())) ?? false;
|
|
1866
|
-
if (own && !needsRotation(own) && !memberRemoved) {
|
|
1867
|
-
const alreadyDistributed = new Set((own.distributedTo ?? []).map((id) => id.toLowerCase()));
|
|
1868
|
-
const undistributed = recipientIds.filter((id) => !alreadyDistributed.has(id.toLowerCase()));
|
|
1869
|
-
if (undistributed.length > 0) {
|
|
1870
|
-
const succeeded = await distributeSenderKey(client, senderAgentId, own, groupId, undistributed, extraHeaders);
|
|
1871
|
-
if (succeeded.length > 0) {
|
|
1872
|
-
own.distributedTo = [...own.distributedTo ?? [], ...succeeded];
|
|
1873
|
-
saveSenderKeyState(senderAgentId, groupId, states);
|
|
1874
|
-
}
|
|
1875
|
-
}
|
|
1876
|
-
return {
|
|
1877
|
-
state: own,
|
|
1878
|
-
groupId
|
|
1879
|
-
};
|
|
1880
|
-
}
|
|
1881
|
-
const newState = generateSenderKey(groupId, senderAgentId);
|
|
1882
|
-
newState.distributedTo = await distributeSenderKey(client, senderAgentId, newState, groupId, recipientIds, extraHeaders);
|
|
1883
|
-
saveSenderKeyState(senderAgentId, groupId, [...memberRemoved ? states.filter((s) => currentMemberSet.has(s.senderAgentId.toLowerCase())) : states, newState]);
|
|
1884
|
-
return {
|
|
1885
|
-
state: newState,
|
|
1886
|
-
groupId
|
|
1887
|
-
};
|
|
1888
|
-
}
|
|
1889
1007
|
/**
|
|
1890
1008
|
* Fetch pending sender key distributions from the inbox and ingest them.
|
|
1891
1009
|
* Used by `read` and `stream` to auto-recover when a sender key is missing.
|
|
@@ -1893,7 +1011,7 @@ async function getOrCreateSenderKey(client, senderAgentId, groupHandle, extraHea
|
|
|
1893
1011
|
* Optionally short-circuits once `targetSenderKeyId` is ingested.
|
|
1894
1012
|
* Returns the number of newly ingested keys.
|
|
1895
1013
|
*/
|
|
1896
|
-
async function fetchAndIngestPendingSKDistributions(client, agentId, targetSenderKeyId) {
|
|
1014
|
+
async function fetchAndIngestPendingSKDistributions(client, configDir, agentId, targetSenderKeyId) {
|
|
1897
1015
|
const inbox = await client.get(`/agents/${agentId}/messages`, {
|
|
1898
1016
|
type: "rine.v1.sender_key_distribution",
|
|
1899
1017
|
limit: 100
|
|
@@ -1901,8 +1019,8 @@ async function fetchAndIngestPendingSKDistributions(client, agentId, targetSende
|
|
|
1901
1019
|
let ingested = 0;
|
|
1902
1020
|
for (const msg of inbox.items) try {
|
|
1903
1021
|
const full = await client.get(`/messages/${msg.id}`);
|
|
1904
|
-
const result = await decryptMessage(agentId, full.encrypted_payload, client);
|
|
1905
|
-
if (ingestSenderKeyDistribution(agentId, full.type, result)) {
|
|
1022
|
+
const result = await decryptMessage(configDir, agentId, full.encrypted_payload, client);
|
|
1023
|
+
if (ingestSenderKeyDistribution(configDir, agentId, full.type, result)) {
|
|
1906
1024
|
ingested++;
|
|
1907
1025
|
if (targetSenderKeyId) {
|
|
1908
1026
|
if (JSON.parse(result.plaintext).sender_key_id === targetSenderKeyId) break;
|
|
@@ -1960,7 +1078,7 @@ function resolvePayload(payload, payloadFile) {
|
|
|
1960
1078
|
}
|
|
1961
1079
|
}
|
|
1962
1080
|
function addMessageCommands(parent, program) {
|
|
1963
|
-
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) => {
|
|
1081
|
+
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) => {
|
|
1964
1082
|
const result = resolvePayload(opts.payload, opts.payloadFile);
|
|
1965
1083
|
if ("error" in result) {
|
|
1966
1084
|
printError(result.error);
|
|
@@ -1971,45 +1089,45 @@ function addMessageCommands(parent, program) {
|
|
|
1971
1089
|
const extraHeaders = {};
|
|
1972
1090
|
let senderAgentId;
|
|
1973
1091
|
if (opts.from !== void 0) {
|
|
1974
|
-
senderAgentId = await resolveToUuid(opts.from);
|
|
1092
|
+
senderAgentId = await resolveToUuid(apiUrl, opts.from);
|
|
1975
1093
|
extraHeaders["X-Rine-Agent"] = senderAgentId;
|
|
1976
1094
|
} else if (!gOpts.as) {
|
|
1977
|
-
senderAgentId = await resolveAgent(client, void 0, void 0);
|
|
1095
|
+
senderAgentId = await resolveAgent(apiUrl, await fetchAgents(client), void 0, void 0);
|
|
1978
1096
|
extraHeaders["X-Rine-Agent"] = senderAgentId;
|
|
1979
|
-
} else senderAgentId = await resolveAgent(client, void 0, gOpts.as);
|
|
1097
|
+
} else senderAgentId = await resolveAgent(apiUrl, await fetchAgents(client), void 0, gOpts.as);
|
|
1980
1098
|
if (opts.idempotencyKey) extraHeaders["Idempotency-Key"] = opts.idempotencyKey;
|
|
1981
1099
|
let recipientAgentId;
|
|
1982
1100
|
if (!opts.to.includes("@")) recipientAgentId = opts.to;
|
|
1983
|
-
else if (!opts.to.startsWith("#")) recipientAgentId = await resolveToUuid(opts.to);
|
|
1101
|
+
else if (!opts.to.startsWith("#")) recipientAgentId = await resolveToUuid(apiUrl, opts.to);
|
|
1984
1102
|
const body = { type: opts.type ?? "rine.v1.dm" };
|
|
1985
1103
|
if (opts.to.includes("@")) body.to_handle = opts.to;
|
|
1986
1104
|
else body.to_agent_id = opts.to;
|
|
1987
1105
|
if (opts.from?.includes("@")) body.from_handle = opts.from;
|
|
1988
1106
|
if (opts.to.startsWith("#") && senderAgentId) {
|
|
1989
|
-
const { state, groupId } = await getOrCreateSenderKey(client, senderAgentId, opts.to, Object.keys(extraHeaders).length > 0 ? extraHeaders : void 0);
|
|
1990
|
-
const { result } = await encryptGroupMessage(senderAgentId, groupId, state, parsedPayload);
|
|
1991
|
-
Object.assign(body,
|
|
1107
|
+
const { state, groupId } = await getOrCreateSenderKey(client, configDir, senderAgentId, opts.to, Object.keys(extraHeaders).length > 0 ? extraHeaders : void 0);
|
|
1108
|
+
const { result: encResult } = await encryptGroupMessage(configDir, senderAgentId, groupId, state, parsedPayload);
|
|
1109
|
+
Object.assign(body, encResult);
|
|
1992
1110
|
} else if (senderAgentId && recipientAgentId) {
|
|
1993
1111
|
const recipientPk = await fetchRecipientEncryptionKey(client, recipientAgentId);
|
|
1994
|
-
const encrypted = await encryptMessage(senderAgentId, recipientPk, parsedPayload);
|
|
1112
|
+
const encrypted = await encryptMessage(configDir, senderAgentId, recipientPk, parsedPayload);
|
|
1995
1113
|
Object.assign(body, encrypted);
|
|
1996
1114
|
} else throw new Error("Cannot encrypt: unable to resolve sender or recipient. Use UUIDs or ensure WebFinger is available.");
|
|
1997
1115
|
const data = await client.post("/messages", body, Object.keys(extraHeaders).length > 0 ? extraHeaders : void 0);
|
|
1998
1116
|
if (gOpts.json) printJson(data);
|
|
1999
1117
|
else printTable([sendRow(data)]);
|
|
2000
1118
|
}));
|
|
2001
|
-
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) => {
|
|
1119
|
+
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) => {
|
|
2002
1120
|
const data = await client.get(`/messages/${messageId}`);
|
|
2003
|
-
const agentId = await resolveAgent(client, opts.agent, gOpts.as);
|
|
2004
|
-
if ((data.encryption_version === "hpke-v1" || data.encryption_version === "sender-key-v1") && agentKeysExist(agentId)) {
|
|
2005
|
-
let
|
|
1121
|
+
const agentId = await resolveAgent(apiUrl, await fetchAgents(client), opts.agent, gOpts.as);
|
|
1122
|
+
if ((data.encryption_version === "hpke-v1" || data.encryption_version === "sender-key-v1") && agentKeysExist(configDir, agentId)) {
|
|
1123
|
+
let decResult;
|
|
2006
1124
|
if (data.encryption_version === "sender-key-v1" && data.group_id) try {
|
|
2007
|
-
|
|
1125
|
+
decResult = await decryptGroupMessage(configDir, agentId, data.group_id, data.encrypted_payload, client);
|
|
2008
1126
|
} catch (err) {
|
|
2009
1127
|
const match = err instanceof Error && err.message.match(/^unknown sender key id: (.+)$/);
|
|
2010
1128
|
if (match) {
|
|
2011
|
-
if (await fetchAndIngestPendingSKDistributions(client, agentId, match[1]) > 0) try {
|
|
2012
|
-
|
|
1129
|
+
if (await fetchAndIngestPendingSKDistributions(client, configDir, agentId, match[1]) > 0) try {
|
|
1130
|
+
decResult = await decryptGroupMessage(configDir, agentId, data.group_id, data.encrypted_payload, client);
|
|
2013
1131
|
} catch (retryErr) {
|
|
2014
1132
|
if (!gOpts.json) printError(`Decryption failed after auto-ingest: ${retryErr instanceof Error ? retryErr.message : String(retryErr)}`);
|
|
2015
1133
|
}
|
|
@@ -2017,18 +1135,18 @@ function addMessageCommands(parent, program) {
|
|
|
2017
1135
|
} else if (!gOpts.json) printError(`Decryption failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2018
1136
|
}
|
|
2019
1137
|
else try {
|
|
2020
|
-
|
|
1138
|
+
decResult = await decryptMessage(configDir, agentId, data.encrypted_payload, client);
|
|
2021
1139
|
} catch (err) {
|
|
2022
1140
|
if (!gOpts.json) printError(`Decryption failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2023
1141
|
}
|
|
2024
|
-
if (
|
|
2025
|
-
if (ingestSenderKeyDistribution(agentId, data.type,
|
|
1142
|
+
if (decResult) {
|
|
1143
|
+
if (ingestSenderKeyDistribution(configDir, agentId, data.type, decResult)) printText("Sender key ingested");
|
|
2026
1144
|
if (gOpts.json) printJson({
|
|
2027
1145
|
...data,
|
|
2028
|
-
decrypted_payload: JSON.parse(
|
|
2029
|
-
signature_verified:
|
|
2030
|
-
verification_status:
|
|
2031
|
-
sender_kid:
|
|
1146
|
+
decrypted_payload: JSON.parse(decResult.plaintext),
|
|
1147
|
+
signature_verified: decResult.verified,
|
|
1148
|
+
verification_status: decResult.verificationStatus,
|
|
1149
|
+
sender_kid: decResult.senderKid
|
|
2032
1150
|
});
|
|
2033
1151
|
else printTable([{
|
|
2034
1152
|
ID: data.id,
|
|
@@ -2037,8 +1155,8 @@ function addMessageCommands(parent, program) {
|
|
|
2037
1155
|
To: msgTo(data),
|
|
2038
1156
|
Group: msgGroup(data),
|
|
2039
1157
|
Type: data.type,
|
|
2040
|
-
Verified:
|
|
2041
|
-
Payload:
|
|
1158
|
+
Verified: decResult.verificationStatus === "verified" ? "✓" : decResult.verificationStatus === "invalid" ? "✗ INVALID" : "?",
|
|
1159
|
+
Payload: decResult.plaintext.slice(0, 200),
|
|
2042
1160
|
Created: data.created_at
|
|
2043
1161
|
}]);
|
|
2044
1162
|
return;
|
|
@@ -2057,8 +1175,8 @@ function addMessageCommands(parent, program) {
|
|
|
2057
1175
|
Created: data.created_at
|
|
2058
1176
|
}]);
|
|
2059
1177
|
}));
|
|
2060
|
-
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) => {
|
|
2061
|
-
const agentId = await resolveAgent(client, opts.agent, gOpts.as);
|
|
1178
|
+
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) => {
|
|
1179
|
+
const agentId = await resolveAgent(apiUrl, await fetchAgents(client), opts.agent, gOpts.as);
|
|
2062
1180
|
const params = {};
|
|
2063
1181
|
if (opts.limit) params.limit = opts.limit;
|
|
2064
1182
|
if (opts.cursor) params.cursor = opts.cursor;
|
|
@@ -2069,7 +1187,7 @@ function addMessageCommands(parent, program) {
|
|
|
2069
1187
|
if (data.next_cursor) printText(`Next cursor: ${data.next_cursor}`);
|
|
2070
1188
|
}
|
|
2071
1189
|
}));
|
|
2072
|
-
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) => {
|
|
1190
|
+
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) => {
|
|
2073
1191
|
const result = resolvePayload(opts.payload, opts.payloadFile);
|
|
2074
1192
|
if ("error" in result) {
|
|
2075
1193
|
printError(result.error);
|
|
@@ -2078,13 +1196,13 @@ function addMessageCommands(parent, program) {
|
|
|
2078
1196
|
}
|
|
2079
1197
|
const parsedPayload = result.parsed;
|
|
2080
1198
|
const original = await client.get(`/messages/${messageId}`);
|
|
2081
|
-
const senderAgentId = await resolveAgent(client, void 0, gOpts.as);
|
|
1199
|
+
const senderAgentId = await resolveAgent(apiUrl, await fetchAgents(client), void 0, gOpts.as);
|
|
2082
1200
|
const recipientAgentId = original.from_agent_id;
|
|
2083
1201
|
const resolvedType = opts.type ?? original.type;
|
|
2084
1202
|
try {
|
|
2085
1203
|
const body = { type: resolvedType };
|
|
2086
|
-
if (!recipientAgentId || !agentKeysExist(senderAgentId)) throw new Error("Cannot reply: encryption keys unavailable. Run 'rine keys generate' first.");
|
|
2087
|
-
const encrypted = await encryptMessage(senderAgentId, await fetchRecipientEncryptionKey(client, recipientAgentId), parsedPayload);
|
|
1204
|
+
if (!recipientAgentId || !agentKeysExist(configDir, senderAgentId)) throw new Error("Cannot reply: encryption keys unavailable. Run 'rine keys generate' first.");
|
|
1205
|
+
const encrypted = await encryptMessage(configDir, senderAgentId, await fetchRecipientEncryptionKey(client, recipientAgentId), parsedPayload);
|
|
2088
1206
|
Object.assign(body, encrypted);
|
|
2089
1207
|
const data = await client.post(`/messages/${messageId}/reply`, body);
|
|
2090
1208
|
if (gOpts.json) printJson(data);
|
|
@@ -2103,23 +1221,23 @@ function registerMessages(program) {
|
|
|
2103
1221
|
//#endregion
|
|
2104
1222
|
//#region src/commands/poll-token.ts
|
|
2105
1223
|
function registerPollToken(program) {
|
|
2106
|
-
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) => {
|
|
2107
|
-
const agentId = await resolveAgent(client, opts.agent, gOpts.as);
|
|
1224
|
+
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) => {
|
|
1225
|
+
const agentId = await resolveAgent(apiUrl, await fetchAgents(client), opts.agent, gOpts.as);
|
|
2108
1226
|
if (opts.revoke) {
|
|
2109
1227
|
await client.delete(`/agents/${agentId}/poll-token`);
|
|
2110
|
-
const creds = loadCredentials();
|
|
1228
|
+
const creds = loadCredentials(configDir);
|
|
2111
1229
|
if (creds[profileName]) {
|
|
2112
1230
|
delete creds[profileName].poll_url;
|
|
2113
|
-
saveCredentials(creds);
|
|
1231
|
+
saveCredentials(configDir, creds);
|
|
2114
1232
|
}
|
|
2115
1233
|
printMutationOk("Poll token revoked", gOpts.json);
|
|
2116
1234
|
return;
|
|
2117
1235
|
}
|
|
2118
1236
|
const data = await client.post(`/agents/${agentId}/poll-token`, {});
|
|
2119
|
-
const creds = loadCredentials();
|
|
1237
|
+
const creds = loadCredentials(configDir);
|
|
2120
1238
|
if (creds[profileName]) {
|
|
2121
1239
|
creds[profileName].poll_url = data.poll_url;
|
|
2122
|
-
saveCredentials(creds);
|
|
1240
|
+
saveCredentials(configDir, creds);
|
|
2123
1241
|
}
|
|
2124
1242
|
if (gOpts.json) printJson(data);
|
|
2125
1243
|
else {
|
|
@@ -2163,26 +1281,12 @@ function registerOrg(program) {
|
|
|
2163
1281
|
}));
|
|
2164
1282
|
}
|
|
2165
1283
|
//#endregion
|
|
2166
|
-
//#region src/timelock.ts
|
|
2167
|
-
async function solveTimeLockWithProgress(baseHex, modulusHex, T, onProgress) {
|
|
2168
|
-
const N = BigInt(`0x${modulusHex}`);
|
|
2169
|
-
let x = BigInt(`0x${baseHex}`) % N;
|
|
2170
|
-
const step = Math.max(1, Math.floor(T / 100));
|
|
2171
|
-
for (let i = 0; i < T; i++) {
|
|
2172
|
-
x = x * x % N;
|
|
2173
|
-
if (i % step === 0) {
|
|
2174
|
-
onProgress?.(Math.floor(i / T * 100));
|
|
2175
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
2176
|
-
}
|
|
2177
|
-
}
|
|
2178
|
-
return x.toString(16);
|
|
2179
|
-
}
|
|
2180
|
-
//#endregion
|
|
2181
1284
|
//#region src/commands/register.ts
|
|
2182
1285
|
function registerRegister(program) {
|
|
2183
1286
|
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) => {
|
|
2184
1287
|
const profile = gOpts.profile ?? "default";
|
|
2185
|
-
const
|
|
1288
|
+
const configDir = resolveConfigDir();
|
|
1289
|
+
const apiUrl = resolveApiUrl();
|
|
2186
1290
|
const challengeRes = await fetch(`${apiUrl}/auth/register`, {
|
|
2187
1291
|
method: "POST",
|
|
2188
1292
|
headers: { "Content-Type": "application/json" },
|
|
@@ -2245,14 +1349,14 @@ function registerRegister(program) {
|
|
|
2245
1349
|
return;
|
|
2246
1350
|
}
|
|
2247
1351
|
const data = await solveRes.json();
|
|
2248
|
-
const creds = loadCredentials();
|
|
1352
|
+
const creds = loadCredentials(configDir);
|
|
2249
1353
|
creds[profile] = {
|
|
2250
1354
|
client_id: data.client_id,
|
|
2251
1355
|
client_secret: data.client_secret
|
|
2252
1356
|
};
|
|
2253
|
-
saveCredentials(creds);
|
|
1357
|
+
saveCredentials(configDir, creds);
|
|
2254
1358
|
try {
|
|
2255
|
-
cacheToken(profile, await fetchOAuthToken(data.client_id, data.client_secret));
|
|
1359
|
+
cacheToken(configDir, profile, await fetchOAuthToken(apiUrl, data.client_id, data.client_secret));
|
|
2256
1360
|
} catch {
|
|
2257
1361
|
process.stderr.write("Warning: initial token cache failed\n");
|
|
2258
1362
|
}
|
|
@@ -2266,7 +1370,7 @@ function registerRegister(program) {
|
|
|
2266
1370
|
}
|
|
2267
1371
|
//#endregion
|
|
2268
1372
|
//#region src/commands/stream.ts
|
|
2269
|
-
async function formatMessageLine(dataStr, agentId, client) {
|
|
1373
|
+
async function formatMessageLine(dataStr, agentId, configDir, client) {
|
|
2270
1374
|
try {
|
|
2271
1375
|
const data = JSON.parse(dataStr);
|
|
2272
1376
|
const ts = (data.created_at ?? "").slice(0, 19) || "?";
|
|
@@ -2274,23 +1378,23 @@ async function formatMessageLine(dataStr, agentId, client) {
|
|
|
2274
1378
|
const msgType = data.type ?? "?";
|
|
2275
1379
|
const target = data.group_handle ? `${data.group_handle} (${msgType})` : msgType;
|
|
2276
1380
|
let preview = "[encrypted]";
|
|
2277
|
-
if (data.encrypted_payload && agentKeysExist(agentId)) {
|
|
1381
|
+
if (data.encrypted_payload && agentKeysExist(configDir, agentId)) {
|
|
2278
1382
|
let result;
|
|
2279
1383
|
if (data.encryption_version === "sender-key-v1" && data.group_id) try {
|
|
2280
|
-
result = await decryptGroupMessage(agentId, data.group_id, data.encrypted_payload, client);
|
|
1384
|
+
result = await decryptGroupMessage(configDir, agentId, data.group_id, data.encrypted_payload, client);
|
|
2281
1385
|
} catch (err) {
|
|
2282
1386
|
const match = err instanceof Error && err.message.match(/^unknown sender key id: (.+)$/);
|
|
2283
1387
|
if (match) {
|
|
2284
|
-
if (await fetchAndIngestPendingSKDistributions(client, agentId, match[1]) > 0) try {
|
|
2285
|
-
result = await decryptGroupMessage(agentId, data.group_id, data.encrypted_payload, client);
|
|
1388
|
+
if (await fetchAndIngestPendingSKDistributions(client, configDir, agentId, match[1]) > 0) try {
|
|
1389
|
+
result = await decryptGroupMessage(configDir, agentId, data.group_id, data.encrypted_payload, client);
|
|
2286
1390
|
} catch {}
|
|
2287
1391
|
}
|
|
2288
1392
|
}
|
|
2289
1393
|
else if (data.encryption_version === "hpke-v1") try {
|
|
2290
|
-
result = await decryptMessage(agentId, data.encrypted_payload, client);
|
|
1394
|
+
result = await decryptMessage(configDir, agentId, data.encrypted_payload, client);
|
|
2291
1395
|
} catch {}
|
|
2292
1396
|
if (result) {
|
|
2293
|
-
ingestSenderKeyDistribution(agentId, data.type ?? "", result);
|
|
1397
|
+
ingestSenderKeyDistribution(configDir, agentId, data.type ?? "", result);
|
|
2294
1398
|
preview = `[${result.verified ? "✓" : "?"}] ${result.plaintext.slice(0, 60)}`;
|
|
2295
1399
|
} else if (data.encryption_version === "hpke-v1" || data.encryption_version === "sender-key-v1") preview = "[decrypt failed]";
|
|
2296
1400
|
}
|
|
@@ -2303,9 +1407,11 @@ function registerStream(program) {
|
|
|
2303
1407
|
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) => {
|
|
2304
1408
|
try {
|
|
2305
1409
|
const gOpts = program.opts();
|
|
2306
|
-
const
|
|
2307
|
-
const
|
|
2308
|
-
const
|
|
1410
|
+
const configDir = resolveConfigDir();
|
|
1411
|
+
const apiUrl = resolveApiUrl();
|
|
1412
|
+
const { client, profileName, entry } = await createClient(configDir, apiUrl, gOpts.profile);
|
|
1413
|
+
const agentId = await resolveAgent(apiUrl, await fetchAgents(client), opts.agent, gOpts.as);
|
|
1414
|
+
const url = `${apiUrl}/agents/${agentId}/stream`;
|
|
2309
1415
|
let lastEventId;
|
|
2310
1416
|
let backoff = 1;
|
|
2311
1417
|
let stopped = false;
|
|
@@ -2314,8 +1420,12 @@ function registerStream(program) {
|
|
|
2314
1420
|
process.exitCode = 0;
|
|
2315
1421
|
});
|
|
2316
1422
|
while (!stopped) try {
|
|
1423
|
+
const envToken = process.env.RINE_TOKEN;
|
|
2317
1424
|
const headers = {
|
|
2318
|
-
Authorization: `Bearer ${await getOrRefreshToken(entry, profileName,
|
|
1425
|
+
Authorization: `Bearer ${await getOrRefreshToken(configDir, apiUrl, entry, profileName, {
|
|
1426
|
+
force: false,
|
|
1427
|
+
envToken
|
|
1428
|
+
})}`,
|
|
2319
1429
|
Accept: "text/event-stream"
|
|
2320
1430
|
};
|
|
2321
1431
|
if (lastEventId) headers["Last-Event-ID"] = lastEventId;
|
|
@@ -2334,7 +1444,7 @@ function registerStream(program) {
|
|
|
2334
1444
|
id,
|
|
2335
1445
|
data
|
|
2336
1446
|
}));
|
|
2337
|
-
else if (event === "message") formatMessageLine(data, agentId, client).then((line) => console.log(line), () => console.log(`[message] ${data.slice(0, 80)}`));
|
|
1447
|
+
else if (event === "message") formatMessageLine(data, agentId, configDir, client).then((line) => console.log(line), () => console.log(`[message] ${data.slice(0, 80)}`));
|
|
2338
1448
|
else if (event === "heartbeat" && opts.verbose) process.stderr.write(`[heartbeat] ${data}\n`);
|
|
2339
1449
|
},
|
|
2340
1450
|
onDisconnect: () => resolve(),
|
|
@@ -2371,13 +1481,13 @@ function registerStream(program) {
|
|
|
2371
1481
|
//#region src/commands/webhook.ts
|
|
2372
1482
|
function registerWebhook(program) {
|
|
2373
1483
|
const webhook = program.command("webhook").description("Manage webhooks");
|
|
2374
|
-
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) => {
|
|
1484
|
+
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) => {
|
|
2375
1485
|
if (!opts.url.startsWith("https://")) {
|
|
2376
1486
|
printError("Webhook URL must start with https://");
|
|
2377
1487
|
process.exitCode = 2;
|
|
2378
1488
|
return;
|
|
2379
1489
|
}
|
|
2380
|
-
const agentId = await resolveAgent(client, opts.agent, gOpts.as);
|
|
1490
|
+
const agentId = await resolveAgent(apiUrl, await fetchAgents(client), opts.agent, gOpts.as);
|
|
2381
1491
|
const data = await client.post("/webhooks", {
|
|
2382
1492
|
agent_id: agentId,
|
|
2383
1493
|
url: opts.url
|