@rine-network/cli 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/main.js +1571 -741
- package/package.json +17 -5
package/dist/main.js
CHANGED
|
@@ -2,8 +2,25 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import { ed25519, x25519 } from "@noble/curves/ed25519.js";
|
|
5
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";
|
|
6
11
|
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
|
|
7
24
|
//#region src/errors.ts
|
|
8
25
|
var RineApiError = class extends Error {
|
|
9
26
|
constructor(status, detail, raw) {
|
|
@@ -67,6 +84,14 @@ function saveTokenCache(cache) {
|
|
|
67
84
|
ensureDir(dir);
|
|
68
85
|
writeAtomic(join(dir, "token_cache.json"), JSON.stringify(cache, null, 2));
|
|
69
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
|
+
}
|
|
70
95
|
/**
|
|
71
96
|
* Resolves credentials for a profile. Priority:
|
|
72
97
|
* 1. RINE_CLIENT_ID + RINE_CLIENT_SECRET env vars (both required)
|
|
@@ -96,10 +121,12 @@ var HttpClient = class {
|
|
|
96
121
|
baseUrl;
|
|
97
122
|
tokenFn;
|
|
98
123
|
canRefresh;
|
|
124
|
+
defaultHeaders;
|
|
99
125
|
constructor(opts) {
|
|
100
|
-
this.baseUrl =
|
|
126
|
+
this.baseUrl = opts.apiUrl ?? getApiUrl();
|
|
101
127
|
this.tokenFn = opts.tokenFn;
|
|
102
128
|
this.canRefresh = opts.canRefresh ?? true;
|
|
129
|
+
this.defaultHeaders = opts.defaultHeaders ?? {};
|
|
103
130
|
}
|
|
104
131
|
async request(method, path, body, params, extraHeaders) {
|
|
105
132
|
let url = this.baseUrl + path;
|
|
@@ -110,6 +137,7 @@ var HttpClient = class {
|
|
|
110
137
|
const doFetch = async (force) => {
|
|
111
138
|
const headers = {
|
|
112
139
|
Authorization: `Bearer ${await this.tokenFn(force)}`,
|
|
140
|
+
...this.defaultHeaders,
|
|
113
141
|
...extraHeaders
|
|
114
142
|
};
|
|
115
143
|
if (body !== void 0) headers["Content-Type"] = "application/json";
|
|
@@ -129,20 +157,32 @@ var HttpClient = class {
|
|
|
129
157
|
if (res.status === 204) return void 0;
|
|
130
158
|
return res.json();
|
|
131
159
|
}
|
|
132
|
-
get(path, params) {
|
|
133
|
-
return this.request("GET", path, void 0, params);
|
|
160
|
+
get(path, params, extraHeaders) {
|
|
161
|
+
return this.request("GET", path, void 0, params, extraHeaders);
|
|
134
162
|
}
|
|
135
163
|
post(path, body, extraHeaders) {
|
|
136
164
|
return this.request("POST", path, body, void 0, extraHeaders);
|
|
137
165
|
}
|
|
138
|
-
put(path, body) {
|
|
139
|
-
return this.request("PUT", path, body);
|
|
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);
|
|
140
171
|
}
|
|
141
|
-
|
|
142
|
-
return this.request("
|
|
172
|
+
delete(path, extraHeaders) {
|
|
173
|
+
return this.request("DELETE", path, void 0, void 0, extraHeaders);
|
|
143
174
|
}
|
|
144
|
-
|
|
145
|
-
|
|
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();
|
|
146
186
|
}
|
|
147
187
|
};
|
|
148
188
|
/**
|
|
@@ -151,6 +191,27 @@ var HttpClient = class {
|
|
|
151
191
|
* 2. Token cache — returned if not expired (60s margin)
|
|
152
192
|
* 3. POST /oauth/token — fetches fresh token and caches it
|
|
153
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
|
+
}
|
|
154
215
|
async function getOrRefreshToken(entry, profileName, force = false) {
|
|
155
216
|
const envToken = process.env.RINE_TOKEN;
|
|
156
217
|
if (envToken) return envToken;
|
|
@@ -160,28 +221,11 @@ async function getOrRefreshToken(entry, profileName, force = false) {
|
|
|
160
221
|
if (cached !== void 0 && cached.expires_at - now > 60) return cached.access_token;
|
|
161
222
|
}
|
|
162
223
|
if (!entry) throw new RineApiError(0, "No credentials found. Run `rine login` first.");
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
method: "POST",
|
|
166
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
167
|
-
body: new URLSearchParams({
|
|
168
|
-
grant_type: "client_credentials",
|
|
169
|
-
client_id: entry.client_id,
|
|
170
|
-
client_secret: entry.client_secret
|
|
171
|
-
}).toString()
|
|
172
|
-
});
|
|
173
|
-
if (!res.ok) throw new RineApiError(res.status, "Token refresh failed");
|
|
174
|
-
const data = await res.json();
|
|
175
|
-
const freshNow = Date.now() / 1e3;
|
|
176
|
-
const cache = loadTokenCache();
|
|
177
|
-
cache[profileName] = {
|
|
178
|
-
access_token: data.access_token,
|
|
179
|
-
expires_at: freshNow + data.expires_in
|
|
180
|
-
};
|
|
181
|
-
saveTokenCache(cache);
|
|
224
|
+
const data = await fetchOAuthToken(entry.client_id, entry.client_secret);
|
|
225
|
+
cacheToken(profileName, data);
|
|
182
226
|
return data.access_token;
|
|
183
227
|
}
|
|
184
|
-
async function createClient(profileFlag) {
|
|
228
|
+
async function createClient(profileFlag, defaultHeaders) {
|
|
185
229
|
const profileName = profileFlag ?? "default";
|
|
186
230
|
const entry = getCredentialEntry(profileName);
|
|
187
231
|
const canRefresh = !process.env.RINE_TOKEN && entry !== void 0;
|
|
@@ -189,7 +233,8 @@ async function createClient(profileFlag) {
|
|
|
189
233
|
return {
|
|
190
234
|
client: new HttpClient({
|
|
191
235
|
tokenFn,
|
|
192
|
-
canRefresh
|
|
236
|
+
canRefresh,
|
|
237
|
+
defaultHeaders
|
|
193
238
|
}),
|
|
194
239
|
profileName,
|
|
195
240
|
entry
|
|
@@ -234,31 +279,45 @@ function printText(msg) {
|
|
|
234
279
|
function printError(msg) {
|
|
235
280
|
console.error(msg);
|
|
236
281
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
282
|
+
//#endregion
|
|
283
|
+
//#region src/action.ts
|
|
284
|
+
/**
|
|
285
|
+
* Wraps a command action with automatic client creation and error handling.
|
|
286
|
+
* Commander calls action(fn) with positional args first, then the options object.
|
|
287
|
+
* This wrapper forwards all arguments to the inner function.
|
|
288
|
+
*/
|
|
289
|
+
function withClient(program, fn) {
|
|
290
|
+
return async (...args) => {
|
|
291
|
+
try {
|
|
292
|
+
const gOpts = program.opts();
|
|
293
|
+
const defaultHeaders = {};
|
|
294
|
+
if (gOpts.as) defaultHeaders["X-Rine-Agent"] = gOpts.as;
|
|
295
|
+
const { client, profileName } = await createClient(gOpts.profile, defaultHeaders);
|
|
296
|
+
await fn({
|
|
297
|
+
client,
|
|
298
|
+
gOpts,
|
|
299
|
+
profileName,
|
|
300
|
+
extraHeaders: defaultHeaders
|
|
301
|
+
}, ...args);
|
|
302
|
+
} catch (err) {
|
|
303
|
+
printError(formatError(err));
|
|
304
|
+
process.exitCode = 1;
|
|
305
|
+
}
|
|
306
|
+
};
|
|
251
307
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
308
|
+
/**
|
|
309
|
+
* Wraps a command action with error handling only (no client creation).
|
|
310
|
+
* For commands like logout that don't need authentication.
|
|
311
|
+
*/
|
|
312
|
+
function withErrorHandler(program, fn) {
|
|
313
|
+
return async (...args) => {
|
|
314
|
+
try {
|
|
315
|
+
await fn(program.opts(), ...args);
|
|
316
|
+
} catch (err) {
|
|
317
|
+
printError(formatError(err));
|
|
318
|
+
process.exitCode = 1;
|
|
319
|
+
}
|
|
320
|
+
};
|
|
262
321
|
}
|
|
263
322
|
//#endregion
|
|
264
323
|
//#region src/commands/agent-profile.ts
|
|
@@ -307,6 +366,11 @@ function toRow(card) {
|
|
|
307
366
|
Updated: String(card.updated_at ?? "—")
|
|
308
367
|
};
|
|
309
368
|
}
|
|
369
|
+
async function updateCard(client, agentId, modify) {
|
|
370
|
+
const card = await client.get(`/agents/${agentId}/card`);
|
|
371
|
+
const updated = modify(card) ?? card;
|
|
372
|
+
return await client.put(`/agents/${agentId}/card`, stripReadOnly(updated));
|
|
373
|
+
}
|
|
310
374
|
function getAgentCmd(program) {
|
|
311
375
|
const cmd = program.commands.find((c) => c.name() === "agent");
|
|
312
376
|
if (!cmd) throw new Error("registerAgent must be called before registerAgentProfile");
|
|
@@ -314,36 +378,20 @@ function getAgentCmd(program) {
|
|
|
314
378
|
}
|
|
315
379
|
function registerAgentProfile(program) {
|
|
316
380
|
const agent = getAgentCmd(program);
|
|
317
|
-
agent.command("profile").description("Get agent card").argument("<agent-id>", "Agent ID").action(async (agentId) => {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
} catch (err) {
|
|
325
|
-
printError(formatError(err));
|
|
326
|
-
process.exitCode = 1;
|
|
327
|
-
}
|
|
328
|
-
});
|
|
329
|
-
agent.command("describe").description("Update agent description").argument("<agent-id>", "Agent ID").requiredOption("--description <desc>", "Agent description").action(async (agentId, opts) => {
|
|
330
|
-
try {
|
|
331
|
-
const gOpts = program.opts();
|
|
332
|
-
const { client } = await createClient(gOpts.profile);
|
|
333
|
-
const card = await client.get(`/agents/${agentId}/card`);
|
|
381
|
+
agent.command("profile").description("Get agent card").argument("<agent-id>", "Agent ID").action(withClient(program, async ({ client, gOpts }, agentId) => {
|
|
382
|
+
const card = await client.get(`/agents/${agentId}/card`);
|
|
383
|
+
if (gOpts.json) printJson(card);
|
|
384
|
+
else printTable([toRow(card)]);
|
|
385
|
+
}));
|
|
386
|
+
agent.command("describe").description("Update agent description").argument("<agent-id>", "Agent ID").requiredOption("--description <desc>", "Agent description").action(withClient(program, async ({ client, gOpts }, agentId, opts) => {
|
|
387
|
+
const result = await updateCard(client, agentId, (card) => {
|
|
334
388
|
card.description = opts.description;
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
});
|
|
342
|
-
agent.command("add-skill").description("Add a skill to the agent card").argument("<agent-id>", "Agent ID").requiredOption("--skill-id <id>", "Skill ID").requiredOption("--skill-name <name>", "Skill name").option("--skill-description <desc>", "Skill description").option("--tags <tags>", "Comma-separated tags").option("--examples <examples>", "Comma-separated examples").option("--input-modes <modes>", "Comma-separated input modes").option("--output-modes <modes>", "Comma-separated output modes").action(async (agentId, opts) => {
|
|
343
|
-
try {
|
|
344
|
-
const gOpts = program.opts();
|
|
345
|
-
const { client } = await createClient(gOpts.profile);
|
|
346
|
-
const card = await client.get(`/agents/${agentId}/card`);
|
|
389
|
+
});
|
|
390
|
+
if (gOpts.json) printJson(result);
|
|
391
|
+
else printSuccess("Description updated");
|
|
392
|
+
}));
|
|
393
|
+
agent.command("add-skill").description("Add a skill to the agent card").argument("<agent-id>", "Agent ID").requiredOption("--skill-id <id>", "Skill ID").requiredOption("--skill-name <name>", "Skill name").option("--skill-description <desc>", "Skill description").option("--tags <tags>", "Comma-separated tags").option("--examples <examples>", "Comma-separated examples").option("--input-modes <modes>", "Comma-separated input modes").option("--output-modes <modes>", "Comma-separated output modes").action(withClient(program, async ({ client, gOpts }, agentId, opts) => {
|
|
394
|
+
const result = await updateCard(client, agentId, (card) => {
|
|
347
395
|
const skill = {
|
|
348
396
|
id: opts.skillId,
|
|
349
397
|
name: opts.skillName
|
|
@@ -354,69 +402,153 @@ function registerAgentProfile(program) {
|
|
|
354
402
|
if (opts.inputModes) skill.inputModes = opts.inputModes.split(",").map((m) => m.trim());
|
|
355
403
|
if (opts.outputModes) skill.outputModes = opts.outputModes.split(",").map((m) => m.trim());
|
|
356
404
|
card.skills = [...Array.isArray(card.skills) ? card.skills : [], skill];
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
try {
|
|
379
|
-
const gOpts = program.opts();
|
|
380
|
-
const { client } = await createClient(gOpts.profile);
|
|
381
|
-
const card = await client.get(`/agents/${agentId}/card`);
|
|
382
|
-
const langs = opts.languages.split(",").map((l) => l.trim());
|
|
383
|
-
await client.put(`/agents/${agentId}/card`, stripReadOnly(setRineField(card, "languages", langs)));
|
|
384
|
-
printMutationOk("Languages updated", gOpts.json);
|
|
385
|
-
} catch (err) {
|
|
386
|
-
printError(formatError(err));
|
|
387
|
-
process.exitCode = 1;
|
|
388
|
-
}
|
|
389
|
-
});
|
|
390
|
-
agent.command("set-pricing").description("Set agent pricing model").argument("<agent-id>", "Agent ID").requiredOption("--model <model>", `Pricing model (${VALID_PRICING.join("|")})`).action(async (agentId, opts) => {
|
|
391
|
-
try {
|
|
392
|
-
const gOpts = program.opts();
|
|
393
|
-
if (!VALID_PRICING.includes(opts.model)) {
|
|
394
|
-
printError(`Invalid pricing model '${opts.model}'. Valid: ${VALID_PRICING.join(", ")}`);
|
|
395
|
-
process.exitCode = 2;
|
|
396
|
-
return;
|
|
397
|
-
}
|
|
398
|
-
const { client } = await createClient(gOpts.profile);
|
|
399
|
-
const card = await client.get(`/agents/${agentId}/card`);
|
|
400
|
-
await client.put(`/agents/${agentId}/card`, stripReadOnly(setRineField(card, "pricing_model", opts.model)));
|
|
401
|
-
printMutationOk("Pricing updated", gOpts.json);
|
|
402
|
-
} catch (err) {
|
|
403
|
-
printError(formatError(err));
|
|
404
|
-
process.exitCode = 1;
|
|
405
|
+
});
|
|
406
|
+
if (gOpts.json) printJson(result);
|
|
407
|
+
else printSuccess("Skill added");
|
|
408
|
+
}));
|
|
409
|
+
agent.command("set-categories").description("Set agent categories").argument("<agent-id>", "Agent ID").requiredOption("--categories <cats>", "Comma-separated category names").action(withClient(program, async ({ client, gOpts }, agentId, opts) => {
|
|
410
|
+
const cats = opts.categories.split(",").map((c) => c.trim());
|
|
411
|
+
const result = await updateCard(client, agentId, (card) => setRineField(card, "categories", cats));
|
|
412
|
+
if (gOpts.json) printJson(result);
|
|
413
|
+
else printSuccess("Categories updated");
|
|
414
|
+
}));
|
|
415
|
+
agent.command("set-languages").description("Set agent supported languages").argument("<agent-id>", "Agent ID").requiredOption("--languages <langs>", "Comma-separated language codes").action(withClient(program, async ({ client, gOpts }, agentId, opts) => {
|
|
416
|
+
const langs = opts.languages.split(",").map((l) => l.trim());
|
|
417
|
+
const result = await updateCard(client, agentId, (card) => setRineField(card, "languages", langs));
|
|
418
|
+
if (gOpts.json) printJson(result);
|
|
419
|
+
else printSuccess("Languages updated");
|
|
420
|
+
}));
|
|
421
|
+
agent.command("set-pricing").description("Set agent pricing model").argument("<agent-id>", "Agent ID").requiredOption("--model <model>", `Pricing model (${VALID_PRICING.join("|")})`).action(withClient(program, async ({ client, gOpts }, agentId, opts) => {
|
|
422
|
+
if (!VALID_PRICING.includes(opts.model)) {
|
|
423
|
+
printError(`Invalid pricing model '${opts.model}'. Valid: ${VALID_PRICING.join(", ")}`);
|
|
424
|
+
process.exitCode = 2;
|
|
425
|
+
return;
|
|
405
426
|
}
|
|
427
|
+
const result = await updateCard(client, agentId, (card) => setRineField(card, "pricing_model", opts.model));
|
|
428
|
+
if (gOpts.json) printJson(result);
|
|
429
|
+
else printSuccess("Pricing updated");
|
|
430
|
+
}));
|
|
431
|
+
agent.command("accept-types").description("Set accepted message types").argument("<agent-id>", "Agent ID").requiredOption("--types <types>", "Comma-separated message types").action(withClient(program, async ({ client, gOpts }, agentId, opts) => {
|
|
432
|
+
const types = opts.types.split(",").map((t) => t.trim());
|
|
433
|
+
const result = await updateCard(client, agentId, (card) => setRineField(card, "message_types_accepted", types));
|
|
434
|
+
if (gOpts.json) printJson(result);
|
|
435
|
+
else printSuccess("Accept types updated");
|
|
436
|
+
}));
|
|
437
|
+
}
|
|
438
|
+
//#endregion
|
|
439
|
+
//#region src/crypto/keys.ts
|
|
440
|
+
var keys_exports = /* @__PURE__ */ __exportAll({
|
|
441
|
+
agentIdFromKid: () => agentIdFromKid,
|
|
442
|
+
agentKeysExist: () => agentKeysExist,
|
|
443
|
+
encryptionPublicKeyToJWK: () => encryptionPublicKeyToJWK,
|
|
444
|
+
fromBase64Url: () => fromBase64Url,
|
|
445
|
+
generateAgentKeys: () => generateAgentKeys,
|
|
446
|
+
generateEncryptionKeyPair: () => generateEncryptionKeyPair,
|
|
447
|
+
generateSigningKeyPair: () => generateSigningKeyPair,
|
|
448
|
+
jwkToPublicKey: () => jwkToPublicKey,
|
|
449
|
+
loadAgentKeys: () => loadAgentKeys,
|
|
450
|
+
saveAgentKeys: () => saveAgentKeys,
|
|
451
|
+
signingPublicKeyToJWK: () => signingPublicKeyToJWK,
|
|
452
|
+
toBase64Url: () => toBase64Url,
|
|
453
|
+
validatePathId: () => validatePathId
|
|
454
|
+
});
|
|
455
|
+
function toBase64Url(bytes) {
|
|
456
|
+
return Buffer.from(bytes).toString("base64url");
|
|
457
|
+
}
|
|
458
|
+
function fromBase64Url(s) {
|
|
459
|
+
return new Uint8Array(Buffer.from(s, "base64url"));
|
|
460
|
+
}
|
|
461
|
+
function generateSigningKeyPair() {
|
|
462
|
+
const privateKey = ed25519.utils.randomSecretKey();
|
|
463
|
+
return {
|
|
464
|
+
privateKey,
|
|
465
|
+
publicKey: ed25519.getPublicKey(privateKey)
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
function generateEncryptionKeyPair() {
|
|
469
|
+
const privateKey = x25519.utils.randomSecretKey();
|
|
470
|
+
return {
|
|
471
|
+
privateKey,
|
|
472
|
+
publicKey: x25519.getPublicKey(privateKey)
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
function generateAgentKeys() {
|
|
476
|
+
return {
|
|
477
|
+
signing: generateSigningKeyPair(),
|
|
478
|
+
encryption: generateEncryptionKeyPair()
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
function signingPublicKeyToJWK(publicKey) {
|
|
482
|
+
return {
|
|
483
|
+
kty: "OKP",
|
|
484
|
+
crv: "Ed25519",
|
|
485
|
+
x: toBase64Url(publicKey)
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
function encryptionPublicKeyToJWK(publicKey) {
|
|
489
|
+
return {
|
|
490
|
+
kty: "OKP",
|
|
491
|
+
crv: "X25519",
|
|
492
|
+
x: toBase64Url(publicKey)
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
function jwkToPublicKey(jwk) {
|
|
496
|
+
const key = fromBase64Url(jwk.x);
|
|
497
|
+
if (key.length !== 32) throw new Error(`Invalid public key: expected 32 bytes, got ${key.length}`);
|
|
498
|
+
return key;
|
|
499
|
+
}
|
|
500
|
+
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;
|
|
501
|
+
function agentIdFromKid(kid) {
|
|
502
|
+
const match = KID_RE.exec(kid);
|
|
503
|
+
if (!match) throw new Error(`Invalid sender KID format: ${kid}`);
|
|
504
|
+
return match[1];
|
|
505
|
+
}
|
|
506
|
+
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;
|
|
507
|
+
function validatePathId(id, label) {
|
|
508
|
+
if (!PATH_SAFE_RE.test(id)) throw new Error(`Invalid ${label}: must be a UUID, got '${id}'`);
|
|
509
|
+
}
|
|
510
|
+
function getKeysDir$1() {
|
|
511
|
+
return join(getConfigDir(), "keys");
|
|
512
|
+
}
|
|
513
|
+
function agentKeysDir(agentId) {
|
|
514
|
+
validatePathId(agentId, "agent ID");
|
|
515
|
+
return join(getKeysDir$1(), agentId);
|
|
516
|
+
}
|
|
517
|
+
function saveAgentKeys(agentId, keys) {
|
|
518
|
+
const dir = agentKeysDir(agentId);
|
|
519
|
+
fs.mkdirSync(dir, {
|
|
520
|
+
recursive: true,
|
|
521
|
+
mode: 448
|
|
406
522
|
});
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
523
|
+
const sigPath = join(dir, "signing.key");
|
|
524
|
+
fs.writeFileSync(sigPath, toBase64Url(keys.signing.privateKey), "utf-8");
|
|
525
|
+
fs.chmodSync(sigPath, 384);
|
|
526
|
+
const encPath = join(dir, "encryption.key");
|
|
527
|
+
fs.writeFileSync(encPath, toBase64Url(keys.encryption.privateKey), "utf-8");
|
|
528
|
+
fs.chmodSync(encPath, 384);
|
|
529
|
+
}
|
|
530
|
+
function loadAgentKeys(agentId) {
|
|
531
|
+
const dir = agentKeysDir(agentId);
|
|
532
|
+
const sigPriv = fromBase64Url(fs.readFileSync(join(dir, "signing.key"), "utf-8").trim());
|
|
533
|
+
if (sigPriv.length !== 32) throw new Error(`Corrupt signing key: expected 32 bytes, got ${sigPriv.length}`);
|
|
534
|
+
const sigPub = ed25519.getPublicKey(sigPriv);
|
|
535
|
+
const encPriv = fromBase64Url(fs.readFileSync(join(dir, "encryption.key"), "utf-8").trim());
|
|
536
|
+
if (encPriv.length !== 32) throw new Error(`Corrupt encryption key: expected 32 bytes, got ${encPriv.length}`);
|
|
537
|
+
const encPub = x25519.getPublicKey(encPriv);
|
|
538
|
+
return {
|
|
539
|
+
signing: {
|
|
540
|
+
privateKey: sigPriv,
|
|
541
|
+
publicKey: sigPub
|
|
542
|
+
},
|
|
543
|
+
encryption: {
|
|
544
|
+
privateKey: encPriv,
|
|
545
|
+
publicKey: encPub
|
|
418
546
|
}
|
|
419
|
-
}
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
function agentKeysExist(agentId) {
|
|
550
|
+
const dir = agentKeysDir(agentId);
|
|
551
|
+
return fs.existsSync(join(dir, "signing.key")) && fs.existsSync(join(dir, "encryption.key"));
|
|
420
552
|
}
|
|
421
553
|
//#endregion
|
|
422
554
|
//#region src/prompt.ts
|
|
@@ -466,112 +598,96 @@ async function promptConfirm(question) {
|
|
|
466
598
|
//#region src/commands/agent.ts
|
|
467
599
|
function toAgentRow(a, opts = {}) {
|
|
468
600
|
const row = {
|
|
469
|
-
ID:
|
|
470
|
-
Name:
|
|
471
|
-
Handle:
|
|
472
|
-
"Human Oversight": String(a.human_oversight
|
|
473
|
-
Created:
|
|
601
|
+
ID: a.id,
|
|
602
|
+
Name: a.name,
|
|
603
|
+
Handle: a.handle,
|
|
604
|
+
"Human Oversight": String(a.human_oversight),
|
|
605
|
+
Created: a.created_at
|
|
474
606
|
};
|
|
475
|
-
if (opts.includeVerificationWords && a.verification_words) row["Verification Words"] =
|
|
607
|
+
if (opts.includeVerificationWords && a.verification_words) row["Verification Words"] = a.verification_words;
|
|
476
608
|
if (opts.includeCard) row.Card = a.unlisted ? "unlisted" : "public";
|
|
477
609
|
if (opts.includeRevoked) row.Revoked = a.revoked_at ? String(a.revoked_at) : "—";
|
|
478
610
|
return row;
|
|
479
611
|
}
|
|
480
612
|
function registerAgent(program) {
|
|
481
613
|
const agent = program.command("agent").description("Agent management");
|
|
482
|
-
agent.command("create").description("Create a new agent").option("--name <name>", "Agent name").option("--human-oversight
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
614
|
+
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) => {
|
|
615
|
+
let name = opts.name;
|
|
616
|
+
if (!name) {
|
|
617
|
+
name = await promptText("Agent name: ");
|
|
486
618
|
if (!name) {
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
process.exitCode = 2;
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
const body = { name };
|
|
495
|
-
if (opts.humanOversight !== void 0) body.human_oversight = opts.humanOversight === "true";
|
|
496
|
-
if (opts.unlisted) body.unlisted = true;
|
|
497
|
-
const { client } = await createClient(gOpts.profile);
|
|
498
|
-
const data = await client.post("/agents", body);
|
|
499
|
-
if (gOpts.json) printJson(data);
|
|
500
|
-
else {
|
|
501
|
-
printTable([toAgentRow(data, {
|
|
502
|
-
includeCard: true,
|
|
503
|
-
includeVerificationWords: true
|
|
504
|
-
})]);
|
|
505
|
-
const handle = String(data.handle ?? "");
|
|
506
|
-
const name = String(data.name ?? "");
|
|
507
|
-
console.log(`
|
|
508
|
-
Agent '${name}' created — reachable at ${handle}`);
|
|
509
|
-
if (data.verification_words) console.log(`Verification words: ${data.verification_words}`);
|
|
619
|
+
printError("Agent name is required");
|
|
620
|
+
process.exitCode = 2;
|
|
621
|
+
return;
|
|
510
622
|
}
|
|
511
|
-
} catch (err) {
|
|
512
|
-
printError(formatError(err));
|
|
513
|
-
process.exitCode = 1;
|
|
514
623
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
624
|
+
const agentKeys = generateAgentKeys();
|
|
625
|
+
const body = {
|
|
626
|
+
name,
|
|
627
|
+
signing_public_key: signingPublicKeyToJWK(agentKeys.signing.publicKey),
|
|
628
|
+
encryption_public_key: encryptionPublicKeyToJWK(agentKeys.encryption.publicKey)
|
|
629
|
+
};
|
|
630
|
+
if (opts.humanOversight !== void 0) body.human_oversight = opts.humanOversight;
|
|
631
|
+
if (opts.unlisted) body.unlisted = true;
|
|
632
|
+
const data = await client.post("/agents", body);
|
|
633
|
+
saveAgentKeys(data.id, agentKeys);
|
|
634
|
+
if (gOpts.json) printJson(data);
|
|
635
|
+
else {
|
|
636
|
+
printTable([toAgentRow(data, {
|
|
637
|
+
includeCard: true,
|
|
638
|
+
includeVerificationWords: true
|
|
639
|
+
})]);
|
|
640
|
+
console.log(`\nAgent '${data.name}' created \u2014 reachable at ${data.handle}`);
|
|
641
|
+
console.log("E2EE keys generated and stored locally.");
|
|
642
|
+
if (data.verification_words) console.log(`Verification words: ${data.verification_words}`);
|
|
527
643
|
}
|
|
528
|
-
});
|
|
529
|
-
agent.command("
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
644
|
+
}));
|
|
645
|
+
agent.command("list").description("List all agents").option("--include-revoked", "Include revoked agents").action(withClient(program, async ({ client, gOpts }, opts) => {
|
|
646
|
+
const params = opts.includeRevoked ? { include_revoked: true } : void 0;
|
|
647
|
+
const data = await client.get("/agents", params);
|
|
648
|
+
if (gOpts.json) printJson(data);
|
|
649
|
+
else printTable(data.items.map((a) => toAgentRow(a, { includeRevoked: opts.includeRevoked })));
|
|
650
|
+
}));
|
|
651
|
+
agent.command("get").description("Get agent details").argument("<agent-id>", "Agent ID").action(withClient(program, async ({ client, gOpts }, agentId) => {
|
|
652
|
+
const data = await client.get(`/agents/${agentId}`);
|
|
653
|
+
if (gOpts.json) printJson(data);
|
|
654
|
+
else printTable([{
|
|
655
|
+
...toAgentRow(data, { includeRevoked: true }),
|
|
656
|
+
"Incoming Policy": data.incoming_policy ?? "accept_all",
|
|
657
|
+
"Outgoing Policy": data.outgoing_policy ?? "send_all"
|
|
658
|
+
}]);
|
|
659
|
+
}));
|
|
660
|
+
agent.command("update").description("Update agent details").argument("<agent-id>", "Agent ID").option("--name <name>", "Agent name").option("--[no-]human-oversight", "Require human oversight").option("--incoming-policy <policy>", "Incoming message policy (accept_all|groups_only)").option("--outgoing-policy <policy>", "Outgoing message policy (send_all|groups_only)").action(withClient(program, async ({ client, gOpts }, agentId, opts) => {
|
|
661
|
+
const body = {};
|
|
662
|
+
if (opts.name) body.name = opts.name;
|
|
663
|
+
if (opts.humanOversight !== void 0) body.human_oversight = opts.humanOversight;
|
|
664
|
+
if (opts.incomingPolicy) body.incoming_policy = opts.incomingPolicy;
|
|
665
|
+
if (opts.outgoingPolicy) body.outgoing_policy = opts.outgoingPolicy;
|
|
666
|
+
if (Object.keys(body).length === 0) {
|
|
667
|
+
printError("At least one of --name, --human-oversight, --incoming-policy, --outgoing-policy is required");
|
|
668
|
+
process.exitCode = 2;
|
|
669
|
+
return;
|
|
539
670
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
const gOpts = program.opts();
|
|
544
|
-
const body = {};
|
|
545
|
-
if (opts.name) body.name = opts.name;
|
|
546
|
-
if (opts.humanOversight !== void 0) body.human_oversight = opts.humanOversight === "true";
|
|
547
|
-
if (Object.keys(body).length === 0) {
|
|
548
|
-
printError("At least one of --name, --human-oversight is required");
|
|
549
|
-
process.exitCode = 2;
|
|
550
|
-
return;
|
|
551
|
-
}
|
|
552
|
-
const { client } = await createClient(gOpts.profile);
|
|
553
|
-
await client.patch(`/agents/${agentId}`, body);
|
|
671
|
+
const data = await client.patch(`/agents/${agentId}`, body);
|
|
672
|
+
if (gOpts.json) printJson(data);
|
|
673
|
+
else {
|
|
554
674
|
printMutationOk("Agent updated", gOpts.json);
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
675
|
+
if (data.warnings && data.warnings.length > 0) for (const w of data.warnings) process.stderr.write(`Warning: ${w}\n`);
|
|
676
|
+
}
|
|
677
|
+
}));
|
|
678
|
+
agent.command("revoke").description("Revoke an agent").argument("<agent-id>", "Agent ID").option("--yes", "Skip confirmation prompt").action(withClient(program, async ({ client, gOpts }, agentId, opts) => {
|
|
679
|
+
if (!opts.yes && !gOpts.json) {
|
|
680
|
+
if (!await promptConfirm(`Revoke agent ${agentId}?`)) return;
|
|
558
681
|
}
|
|
559
|
-
});
|
|
560
|
-
agent.command("revoke").description("Revoke an agent").argument("<agent-id>", "Agent ID").option("--yes", "Skip confirmation prompt").action(async (agentId, opts) => {
|
|
561
682
|
try {
|
|
562
|
-
const gOpts = program.opts();
|
|
563
|
-
if (!opts.yes && !gOpts.json) {
|
|
564
|
-
if (!await promptConfirm(`Revoke agent ${agentId}?`)) return;
|
|
565
|
-
}
|
|
566
|
-
const { client } = await createClient(gOpts.profile);
|
|
567
683
|
await client.delete(`/agents/${agentId}`);
|
|
568
684
|
printMutationOk("Agent revoked", gOpts.json);
|
|
569
685
|
} catch (err) {
|
|
570
686
|
if (err instanceof RineApiError && err.status === 409) printError("Agent already revoked");
|
|
571
|
-
else
|
|
687
|
+
else throw err;
|
|
572
688
|
process.exitCode = 1;
|
|
573
689
|
}
|
|
574
|
-
});
|
|
690
|
+
}));
|
|
575
691
|
}
|
|
576
692
|
//#endregion
|
|
577
693
|
//#region src/commands/auth.ts
|
|
@@ -581,126 +697,136 @@ function registerAuth(program) {
|
|
|
581
697
|
const profile = program.opts().profile ?? "default";
|
|
582
698
|
const clientId = opts.clientId ?? await promptText("Client ID: ");
|
|
583
699
|
const clientSecret = opts.clientSecret ?? await promptPassword("Client secret: ");
|
|
584
|
-
const
|
|
585
|
-
const res = await fetch(`${apiUrl}/oauth/token`, {
|
|
586
|
-
method: "POST",
|
|
587
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
588
|
-
body: new URLSearchParams({
|
|
589
|
-
grant_type: "client_credentials",
|
|
590
|
-
client_id: clientId,
|
|
591
|
-
client_secret: clientSecret
|
|
592
|
-
}).toString()
|
|
593
|
-
});
|
|
594
|
-
if (!res.ok) {
|
|
595
|
-
printError(`Login failed: ${(await res.json().catch(() => ({}))).detail ?? res.statusText}`);
|
|
596
|
-
process.exitCode = 1;
|
|
597
|
-
return;
|
|
598
|
-
}
|
|
599
|
-
const tokenData = await res.json();
|
|
700
|
+
const tokenData = await fetchOAuthToken(clientId, clientSecret);
|
|
600
701
|
const creds = loadCredentials();
|
|
601
702
|
creds[profile] = {
|
|
602
703
|
client_id: clientId,
|
|
603
704
|
client_secret: clientSecret
|
|
604
705
|
};
|
|
605
706
|
saveCredentials(creds);
|
|
606
|
-
|
|
607
|
-
cache[profile] = {
|
|
608
|
-
access_token: tokenData.access_token,
|
|
609
|
-
expires_at: Date.now() / 1e3 + tokenData.expires_in
|
|
610
|
-
};
|
|
611
|
-
saveTokenCache(cache);
|
|
707
|
+
cacheToken(profile, tokenData);
|
|
612
708
|
printSuccess(`Logged in as ${clientId}`);
|
|
613
709
|
} catch (err) {
|
|
614
710
|
printError(formatError(err));
|
|
615
711
|
process.exitCode = 1;
|
|
616
712
|
}
|
|
617
713
|
});
|
|
618
|
-
program.command("logout").description("Clear token
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
}
|
|
629
|
-
});
|
|
630
|
-
program.command("status").description("Show org status").action(async () => {
|
|
631
|
-
await showOrgStatus(program);
|
|
632
|
-
});
|
|
714
|
+
program.command("logout").description("Clear cached token. Credentials are preserved.").action(withErrorHandler(program, (gOpts) => {
|
|
715
|
+
const profile = gOpts.profile ?? "default";
|
|
716
|
+
const cache = loadTokenCache();
|
|
717
|
+
delete cache[profile];
|
|
718
|
+
saveTokenCache(cache);
|
|
719
|
+
printSuccess(`Logged out — token cache cleared for profile '${profile}'. Credentials preserved.`);
|
|
720
|
+
}));
|
|
721
|
+
program.command("status").description("Show org status").action(withClient(program, async ({ client, gOpts }) => {
|
|
722
|
+
await showOrgStatus(client, gOpts);
|
|
723
|
+
}));
|
|
633
724
|
const authGroup = program.command("auth").description("Auth commands");
|
|
634
|
-
authGroup.command("token").description("Get or refresh access token").option("--force", "Force token refresh (bypass cache)").action(async (opts) => {
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
725
|
+
authGroup.command("token").description("Get or refresh access token").option("--force", "Force token refresh (bypass cache)").action(withErrorHandler(program, async (gOpts, opts) => {
|
|
726
|
+
const profile = gOpts.profile ?? "default";
|
|
727
|
+
const force = opts.force === true;
|
|
728
|
+
const now = Date.now() / 1e3;
|
|
729
|
+
const envToken = process.env.RINE_TOKEN;
|
|
730
|
+
const cached = loadTokenCache()[profile];
|
|
731
|
+
const source = envToken !== void 0 || !force && cached !== void 0 && cached.expires_at - now > 60 ? "cache" : "server";
|
|
732
|
+
const token = await getOrRefreshToken(getCredentialEntry(profile), profile, force);
|
|
733
|
+
if (gOpts.json) printJson({
|
|
734
|
+
access_token: token,
|
|
735
|
+
source
|
|
736
|
+
});
|
|
737
|
+
else console.log(token);
|
|
738
|
+
}));
|
|
739
|
+
authGroup.command("status").description("Show org status").action(withClient(program, async ({ client, gOpts }) => {
|
|
740
|
+
await showOrgStatus(client, gOpts);
|
|
741
|
+
}));
|
|
742
|
+
}
|
|
743
|
+
async function showOrgStatus(client, gOpts) {
|
|
744
|
+
const data = await client.get("/org");
|
|
745
|
+
if (gOpts.json) {
|
|
746
|
+
printJson(data);
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
printTable([
|
|
750
|
+
{
|
|
751
|
+
Field: "Name",
|
|
752
|
+
Value: data.name
|
|
753
|
+
},
|
|
754
|
+
{
|
|
755
|
+
Field: "Slug",
|
|
756
|
+
Value: data.slug
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
Field: "Trust Tier",
|
|
760
|
+
Value: String(data.trust_tier)
|
|
761
|
+
},
|
|
762
|
+
{
|
|
763
|
+
Field: "Agent Count",
|
|
764
|
+
Value: String(data.agent_count)
|
|
652
765
|
}
|
|
653
|
-
|
|
654
|
-
authGroup.command("status").description("Show org status").action(async () => {
|
|
655
|
-
await showOrgStatus(program);
|
|
656
|
-
});
|
|
766
|
+
]);
|
|
657
767
|
}
|
|
658
|
-
|
|
768
|
+
//#endregion
|
|
769
|
+
//#region src/resolve-handle.ts
|
|
770
|
+
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;
|
|
771
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
772
|
+
/**
|
|
773
|
+
* Resolves a handle (e.g. "agent@org.rine.network") to a UUID
|
|
774
|
+
* via WebFinger (RFC 7033).
|
|
775
|
+
*
|
|
776
|
+
* Returns the UUID string on success, or the original input unchanged
|
|
777
|
+
* on failure (caller validates UUID format and reports the error).
|
|
778
|
+
*/
|
|
779
|
+
async function resolveHandleViaWebFinger(handle) {
|
|
659
780
|
try {
|
|
660
|
-
const
|
|
661
|
-
const
|
|
662
|
-
const data
|
|
781
|
+
const params = new URLSearchParams({ resource: `acct:${handle}` });
|
|
782
|
+
const data = await HttpClient.publicGet("/.well-known/webfinger", params);
|
|
783
|
+
for (const alias of data.aliases ?? []) {
|
|
784
|
+
const match = UUID_ALIAS_RE.exec(alias);
|
|
785
|
+
if (match?.[1]) return match[1];
|
|
786
|
+
}
|
|
787
|
+
} catch {}
|
|
788
|
+
return handle;
|
|
789
|
+
}
|
|
790
|
+
//#endregion
|
|
791
|
+
//#region src/commands/discover-groups.ts
|
|
792
|
+
function registerDiscoverGroups(discover, program) {
|
|
793
|
+
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) => {
|
|
794
|
+
const params = new URLSearchParams();
|
|
795
|
+
if (opts.query) params.append("q", opts.query);
|
|
796
|
+
if (opts.limit) params.append("limit", opts.limit);
|
|
797
|
+
if (opts.cursor) params.append("cursor", opts.cursor);
|
|
798
|
+
const data = await HttpClient.publicGet("/directory/groups", params.toString() ? params : void 0);
|
|
663
799
|
if (gOpts.json) {
|
|
664
|
-
|
|
800
|
+
printJson(data);
|
|
665
801
|
return;
|
|
666
802
|
}
|
|
667
|
-
printTable([
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
803
|
+
printTable((data.items ?? []).map((g) => ({
|
|
804
|
+
Name: g.name,
|
|
805
|
+
Handle: g.handle,
|
|
806
|
+
Description: g.description ?? "",
|
|
807
|
+
Enrollment: g.enrollment_policy,
|
|
808
|
+
Members: g.member_count
|
|
809
|
+
})));
|
|
810
|
+
})).command("inspect").description("Inspect a public group").argument("<group-id>", "Group ID").action(withErrorHandler(program, async (gOpts, groupId) => {
|
|
811
|
+
const data = await HttpClient.publicGet(`/directory/groups/${groupId}`);
|
|
812
|
+
if (gOpts.json) {
|
|
813
|
+
printJson(data);
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
printTable([{
|
|
817
|
+
ID: data.id,
|
|
818
|
+
Name: data.name,
|
|
819
|
+
Handle: data.handle,
|
|
820
|
+
Description: data.description ?? "",
|
|
821
|
+
"Org ID": data.org_id,
|
|
822
|
+
Enrollment: data.enrollment_policy,
|
|
823
|
+
Members: data.member_count,
|
|
824
|
+
Created: data.created_at
|
|
825
|
+
}]);
|
|
826
|
+
}));
|
|
689
827
|
}
|
|
690
828
|
//#endregion
|
|
691
829
|
//#region src/commands/discover.ts
|
|
692
|
-
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
693
|
-
async function directoryFetch(path, params) {
|
|
694
|
-
const base = getApiUrl();
|
|
695
|
-
const qs = params?.toString();
|
|
696
|
-
const url = qs ? `${base}${path}?${qs}` : `${base}${path}`;
|
|
697
|
-
const res = await fetch(url, { headers: { Accept: "application/json" } });
|
|
698
|
-
if (!res.ok) {
|
|
699
|
-
const body = await res.text().catch(() => res.statusText);
|
|
700
|
-
throw new RineApiError(res.status, body, res);
|
|
701
|
-
}
|
|
702
|
-
return res.json();
|
|
703
|
-
}
|
|
704
830
|
function trunc(s, len) {
|
|
705
831
|
const str = String(s ?? "");
|
|
706
832
|
return str.length > len ? `${str.slice(0, len - 1)}\u2026` : str;
|
|
@@ -720,7 +846,7 @@ async function doAgentSearch(opts, gOpts) {
|
|
|
720
846
|
if (opts.limit) params.append("limit", opts.limit);
|
|
721
847
|
if (opts.cursor) params.append("cursor", opts.cursor);
|
|
722
848
|
if (opts.sort) params.append("sort", opts.sort);
|
|
723
|
-
const data = await
|
|
849
|
+
const data = await HttpClient.publicGet("/directory/agents", params);
|
|
724
850
|
if (gOpts.json) {
|
|
725
851
|
printJson(data);
|
|
726
852
|
return;
|
|
@@ -736,132 +862,851 @@ async function doAgentSearch(opts, gOpts) {
|
|
|
736
862
|
if (result.next_cursor) printText(`Next cursor: ${result.next_cursor}`);
|
|
737
863
|
}
|
|
738
864
|
const collect = (v, prev) => [...prev, v];
|
|
739
|
-
function
|
|
865
|
+
function addAgentSearchCommand(parent, name, description, program, requireQuery) {
|
|
866
|
+
const cmd = parent.command(name).description(description);
|
|
740
867
|
if (requireQuery) cmd.requiredOption("-q, --query <query>", "Search query");
|
|
741
868
|
else cmd.option("-q, --query <query>", "Search query");
|
|
742
|
-
|
|
869
|
+
cmd.option("--category <cat>", "Category filter (repeatable)", collect, []).option("--tag <tag>", "Tag filter (repeatable)", collect, []).option("--jurisdiction <j>", "Jurisdiction filter").option("--language <lang>", "Language filter (repeatable)", collect, []).option("--verified", "Only verified agents").option("--no-verified", "Only unverified agents").option("--pricing-model <model>", "Pricing model filter").option("--limit <n>", "Max results").option("--cursor <cursor>", "Pagination cursor").option("--sort <sort>", "Sort order (relevance|name|created_at)").action(withErrorHandler(program, async (gOpts, opts) => {
|
|
870
|
+
await doAgentSearch(opts, gOpts);
|
|
871
|
+
}));
|
|
743
872
|
}
|
|
744
873
|
function registerDiscover(program) {
|
|
745
|
-
const discover = program.command("discover").description("Discover agents in the directory");
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
874
|
+
const discover = program.command("discover").description("Discover agents and groups in the directory");
|
|
875
|
+
addAgentSearchCommand(discover, "agents", "List agents in the directory", program, false);
|
|
876
|
+
addAgentSearchCommand(discover, "search", "Search agents (--query required)", program, true);
|
|
877
|
+
discover.command("categories").description("List directory categories with counts").action(withErrorHandler(program, async (gOpts) => {
|
|
878
|
+
const data = await HttpClient.publicGet("/directory/categories");
|
|
879
|
+
if (gOpts.json) {
|
|
880
|
+
printJson(data);
|
|
881
|
+
return;
|
|
753
882
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
883
|
+
printTable((data.items ?? []).sort((a, b) => a.name.localeCompare(b.name)).map((c) => ({
|
|
884
|
+
Category: c.name,
|
|
885
|
+
Count: c.count
|
|
886
|
+
})));
|
|
887
|
+
}));
|
|
888
|
+
discover.command("inspect").description("Inspect a specific agent in the directory").argument("<agent-id>", "Agent UUID or handle").action(withErrorHandler(program, async (gOpts, agentIdArg) => {
|
|
889
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
890
|
+
let resolvedId = agentIdArg;
|
|
891
|
+
if (agentIdArg.includes("@")) resolvedId = await resolveHandleViaWebFinger(agentIdArg);
|
|
892
|
+
if (!UUID_RE.test(resolvedId)) {
|
|
893
|
+
printError(`Could not resolve '${agentIdArg}' to an agent UUID. Provide a UUID or a valid handle (e.g. agent@org.rine.network).`);
|
|
761
894
|
process.exitCode = 1;
|
|
895
|
+
return;
|
|
762
896
|
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
const data = await directoryFetch("/directory/categories");
|
|
768
|
-
if (gOpts.json) {
|
|
769
|
-
printJson(data);
|
|
770
|
-
return;
|
|
771
|
-
}
|
|
772
|
-
printTable((data.items ?? []).sort((a, b) => a.name.localeCompare(b.name)).map((c) => ({
|
|
773
|
-
Category: c.name,
|
|
774
|
-
Count: c.count
|
|
775
|
-
})));
|
|
776
|
-
} catch (err) {
|
|
777
|
-
printError(formatError(err));
|
|
778
|
-
process.exitCode = 1;
|
|
897
|
+
const data = await HttpClient.publicGet(`/directory/agents/${resolvedId}`);
|
|
898
|
+
if (gOpts.json) {
|
|
899
|
+
printJson(data);
|
|
900
|
+
return;
|
|
779
901
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
const
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
902
|
+
const result = data;
|
|
903
|
+
const card = result.card ?? data;
|
|
904
|
+
const meta = result.directory_metadata ?? {};
|
|
905
|
+
printTable([{
|
|
906
|
+
Name: String(card.name ?? ""),
|
|
907
|
+
Description: trunc(card.description, 120),
|
|
908
|
+
Categories: fmtList(card.rine?.categories ?? card.categories),
|
|
909
|
+
Tags: fmtList(card.rine?.tags ?? card.tags),
|
|
910
|
+
Pricing: String(card.rine?.pricing_model ?? card.pricing_model ?? ""),
|
|
911
|
+
"Registered At": String(meta.registered_at ?? ""),
|
|
912
|
+
"Messages (30d)": String(meta.messages_processed_30d ?? ""),
|
|
913
|
+
"Avg Response (ms)": String(meta.avg_response_time_ms ?? "")
|
|
914
|
+
}]);
|
|
915
|
+
}));
|
|
916
|
+
registerDiscoverGroups(discover, program);
|
|
917
|
+
}
|
|
918
|
+
//#endregion
|
|
919
|
+
//#region src/resolve-agent.ts
|
|
920
|
+
let cached;
|
|
921
|
+
/**
|
|
922
|
+
* Resolve which agent ID to use.
|
|
923
|
+
* Priority: explicit --agent flag > --as global flag > auto-resolve (single-agent shortcut).
|
|
924
|
+
*/
|
|
925
|
+
async function resolveAgent(client, explicit, asFlag) {
|
|
926
|
+
if (explicit) return explicit;
|
|
927
|
+
if (asFlag) return asFlag;
|
|
928
|
+
if (cached === void 0) cached = (await client.get("/agents")).items;
|
|
929
|
+
if (cached.length === 1) {
|
|
930
|
+
const agent = cached[0];
|
|
931
|
+
if (agent) return agent.id;
|
|
932
|
+
}
|
|
933
|
+
if (cached.length === 0) throw new Error("No active agents. Create one with 'rine agent create --name <name>'");
|
|
934
|
+
const lines = cached.map((a) => ` ${a.id} ${a.handle}`).join("\n");
|
|
935
|
+
throw new Error(`Multiple agents found. Specify with --agent <uuid>:\n${lines}`);
|
|
936
|
+
}
|
|
937
|
+
//#endregion
|
|
938
|
+
//#region src/commands/group.ts
|
|
939
|
+
function groupRow(g) {
|
|
940
|
+
return {
|
|
941
|
+
ID: g.id,
|
|
942
|
+
Name: g.name,
|
|
943
|
+
Handle: g.handle,
|
|
944
|
+
Enrollment: g.enrollment_policy,
|
|
945
|
+
Visibility: g.visibility,
|
|
946
|
+
Members: g.member_count,
|
|
947
|
+
Created: g.created_at
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
function groupDetailRow(g) {
|
|
951
|
+
return {
|
|
952
|
+
ID: g.id,
|
|
953
|
+
Name: g.name,
|
|
954
|
+
Handle: g.handle,
|
|
955
|
+
Description: g.description ?? "",
|
|
956
|
+
Enrollment: g.enrollment_policy,
|
|
957
|
+
Visibility: g.visibility,
|
|
958
|
+
Isolated: g.isolated,
|
|
959
|
+
"Vote Duration": g.vote_duration_hours,
|
|
960
|
+
Members: g.member_count,
|
|
961
|
+
Created: g.created_at
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
function memberRow(m) {
|
|
965
|
+
return {
|
|
966
|
+
"Agent ID": m.agent_id,
|
|
967
|
+
Handle: m.agent_handle ?? "",
|
|
968
|
+
Role: m.role,
|
|
969
|
+
Joined: m.joined_at
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
function requestRow(r) {
|
|
973
|
+
return {
|
|
974
|
+
"Request ID": r.id,
|
|
975
|
+
"Agent ID": r.agent_id,
|
|
976
|
+
Message: r.message ?? "",
|
|
977
|
+
"Your Vote": r.your_vote ?? "",
|
|
978
|
+
Expires: r.expires_at ?? "",
|
|
979
|
+
Created: r.created_at
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
function registerGroup(program) {
|
|
983
|
+
const group = program.command("group").description("Group management");
|
|
984
|
+
group.command("create").description("Create a new group").requiredOption("--name <name>", "Group name (DNS-safe slug)").option("--enrollment <policy>", "Enrollment policy (open|closed|majority|unanimity)").option("--visibility <vis>", "Visibility (public|private)").option("--isolated", "Isolate group communication").option("--vote-duration <hours>", "Vote duration in hours (1-72)").action(withClient(program, async ({ client, gOpts }, opts) => {
|
|
985
|
+
const body = { name: opts.name };
|
|
986
|
+
if (opts.enrollment) body.enrollment_policy = opts.enrollment;
|
|
987
|
+
if (opts.visibility) body.visibility = opts.visibility;
|
|
988
|
+
else {
|
|
989
|
+
body.visibility = opts.enrollment === "open" ? "public" : "private";
|
|
990
|
+
if (!gOpts.json) printText(`Visibility defaulted to "${body.visibility}" (enrollment: ${opts.enrollment ?? "closed"})`);
|
|
794
991
|
}
|
|
795
|
-
if (
|
|
796
|
-
|
|
992
|
+
if (opts.isolated) body.isolated = true;
|
|
993
|
+
if (opts.voteDuration) body.vote_duration_hours = Number(opts.voteDuration);
|
|
994
|
+
const data = await client.post("/groups", body);
|
|
995
|
+
if (gOpts.json) printJson(data);
|
|
996
|
+
else {
|
|
997
|
+
printTable([groupDetailRow(data)]);
|
|
998
|
+
printText(`\nGroup created — handle: ${data.handle}`);
|
|
999
|
+
}
|
|
1000
|
+
}));
|
|
1001
|
+
group.command("list").description("List groups").action(withClient(program, async ({ client, gOpts }) => {
|
|
1002
|
+
const data = await client.get("/groups");
|
|
1003
|
+
if (gOpts.json) printJson(data);
|
|
1004
|
+
else printTable(data.items.map(groupRow));
|
|
1005
|
+
}));
|
|
1006
|
+
group.command("get").description("Get group details").argument("<group-id>", "Group ID").action(withClient(program, async ({ client, gOpts }, groupId) => {
|
|
1007
|
+
const data = await client.get(`/groups/${groupId}`);
|
|
1008
|
+
if (gOpts.json) printJson(data);
|
|
1009
|
+
else printTable([groupDetailRow(data)]);
|
|
1010
|
+
}));
|
|
1011
|
+
group.command("update").description("Update group settings").argument("<group-id>", "Group ID").option("--description <desc>", "Group description").option("--enrollment <policy>", "Enrollment policy (open|closed|majority|unanimity)").option("--visibility <vis>", "Visibility (public|private)").option("--vote-duration <hours>", "Vote duration in hours (1-72)").action(withClient(program, async ({ client, gOpts }, groupId, opts) => {
|
|
1012
|
+
const body = {};
|
|
1013
|
+
if (opts.description !== void 0) body.description = opts.description;
|
|
1014
|
+
if (opts.enrollment) body.enrollment_policy = opts.enrollment;
|
|
1015
|
+
if (opts.visibility) body.visibility = opts.visibility;
|
|
1016
|
+
if (opts.voteDuration) body.vote_duration_hours = Number(opts.voteDuration);
|
|
1017
|
+
if (Object.keys(body).length === 0) {
|
|
1018
|
+
printError("At least one of --description, --enrollment, --visibility, --vote-duration is required");
|
|
797
1019
|
process.exitCode = 2;
|
|
798
1020
|
return;
|
|
799
1021
|
}
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
}
|
|
806
|
-
const result = data;
|
|
807
|
-
const card = result.card ?? data;
|
|
808
|
-
const meta = result.directory_metadata ?? {};
|
|
809
|
-
printTable([{
|
|
810
|
-
Name: String(card.name ?? ""),
|
|
811
|
-
Description: trunc(card.description, 120),
|
|
812
|
-
Categories: fmtList(card.rine?.categories ?? card.categories),
|
|
813
|
-
Tags: fmtList(card.rine?.tags ?? card.tags),
|
|
814
|
-
Pricing: String(card.rine?.pricing_model ?? card.pricing_model ?? ""),
|
|
815
|
-
"Registered At": String(meta.registered_at ?? ""),
|
|
816
|
-
"Messages (30d)": String(meta.messages_processed_30d ?? ""),
|
|
817
|
-
"Avg Response (ms)": String(meta.avg_response_time_ms ?? "")
|
|
818
|
-
}]);
|
|
819
|
-
} catch (err) {
|
|
820
|
-
printError(formatError(err));
|
|
821
|
-
process.exitCode = 1;
|
|
1022
|
+
const data = await client.patch(`/groups/${groupId}`, body);
|
|
1023
|
+
if (gOpts.json) printJson(data);
|
|
1024
|
+
else {
|
|
1025
|
+
printTable([groupDetailRow(data)]);
|
|
1026
|
+
printText("Group updated");
|
|
822
1027
|
}
|
|
1028
|
+
}));
|
|
1029
|
+
group.command("delete").description("Delete a group").argument("<group-id>", "Group ID").option("--yes", "Skip confirmation prompt").action(withClient(program, async ({ client, gOpts }, groupId, opts) => {
|
|
1030
|
+
if (!opts.yes && !gOpts.json) {
|
|
1031
|
+
if (!await promptConfirm(`Delete group ${groupId}?`)) return;
|
|
1032
|
+
}
|
|
1033
|
+
await client.delete(`/groups/${groupId}`);
|
|
1034
|
+
printMutationOk("Group deleted", gOpts.json);
|
|
1035
|
+
}));
|
|
1036
|
+
group.command("members").description("List group members").argument("<group-id>", "Group ID").action(withClient(program, async ({ client, gOpts }, groupId) => {
|
|
1037
|
+
const data = await client.get(`/groups/${groupId}/members`);
|
|
1038
|
+
if (gOpts.json) printJson(data);
|
|
1039
|
+
else printTable(data.items.map(memberRow));
|
|
1040
|
+
}));
|
|
1041
|
+
group.command("join").description("Join a group").argument("<group-id>", "Group ID").option("--message <msg>", "Join request message").action(withClient(program, async ({ client, gOpts }, groupId, opts) => {
|
|
1042
|
+
const body = {};
|
|
1043
|
+
if (opts.message) body.message = opts.message;
|
|
1044
|
+
const data = await client.post(`/groups/${groupId}/join`, body);
|
|
1045
|
+
if (gOpts.json) printJson(data);
|
|
1046
|
+
else if ("role" in data) {
|
|
1047
|
+
printTable([memberRow(data)]);
|
|
1048
|
+
printText("Joined group (member)");
|
|
1049
|
+
} else {
|
|
1050
|
+
printTable([requestRow(data)]);
|
|
1051
|
+
printText("Join request submitted (pending approval)");
|
|
1052
|
+
}
|
|
1053
|
+
}));
|
|
1054
|
+
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) => {
|
|
1055
|
+
const agentId = await resolveAgent(client, opts.agent, gOpts.as);
|
|
1056
|
+
await client.delete(`/groups/${groupId}/members/${agentId}`);
|
|
1057
|
+
printMutationOk("Left group", gOpts.json);
|
|
1058
|
+
}));
|
|
1059
|
+
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) => {
|
|
1060
|
+
await client.delete(`/groups/${groupId}/members/${agentId}`);
|
|
1061
|
+
printMutationOk("Member removed", gOpts.json);
|
|
1062
|
+
}));
|
|
1063
|
+
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) => {
|
|
1064
|
+
const body = { agent_id: opts.agent };
|
|
1065
|
+
if (opts.message) body.message = opts.message;
|
|
1066
|
+
const data = await client.post(`/groups/${groupId}/invite`, body);
|
|
1067
|
+
if (gOpts.json) printJson(data);
|
|
1068
|
+
else {
|
|
1069
|
+
printText("Invitation sent");
|
|
1070
|
+
if (data.id) printTable([requestRow(data)]);
|
|
1071
|
+
}
|
|
1072
|
+
}));
|
|
1073
|
+
group.command("requests").description("List pending join requests").argument("<group-id>", "Group ID").action(withClient(program, async ({ client, gOpts }, groupId) => {
|
|
1074
|
+
const data = await client.get(`/groups/${groupId}/requests`);
|
|
1075
|
+
if (gOpts.json) printJson(data);
|
|
1076
|
+
else printTable(data.items.map(requestRow));
|
|
1077
|
+
}));
|
|
1078
|
+
group.command("vote").description("Vote on a join request").argument("<group-id>", "Group ID").argument("<request-id>", "Join request ID").requiredOption("--vote <vote>", "Vote: approve or deny").action(withClient(program, async ({ client, gOpts }, groupId, requestId, opts) => {
|
|
1079
|
+
if (opts.vote !== "approve" && opts.vote !== "deny") {
|
|
1080
|
+
printError("--vote must be 'approve' or 'deny'");
|
|
1081
|
+
process.exitCode = 2;
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
const data = await client.post(`/groups/${groupId}/requests/${requestId}/vote`, { vote: opts.vote });
|
|
1085
|
+
if (gOpts.json) printJson(data);
|
|
1086
|
+
else printText(`Vote recorded. Request status: ${data.status}`);
|
|
1087
|
+
}));
|
|
1088
|
+
}
|
|
1089
|
+
//#endregion
|
|
1090
|
+
//#region src/crypto/envelope.ts
|
|
1091
|
+
const SIGNATURE_SIZE = 64;
|
|
1092
|
+
const MAX_KID_LENGTH = 255;
|
|
1093
|
+
function encodeEnvelope(kid, signature, payload) {
|
|
1094
|
+
const kidBytes = new TextEncoder().encode(kid);
|
|
1095
|
+
if (kidBytes.length > MAX_KID_LENGTH) throw new Error(`kid too long: ${kidBytes.length} bytes (max ${MAX_KID_LENGTH})`);
|
|
1096
|
+
if (signature.length !== SIGNATURE_SIZE) throw new Error(`signature must be ${SIGNATURE_SIZE} bytes, got ${signature.length}`);
|
|
1097
|
+
const buf = new Uint8Array(1 + kidBytes.length + SIGNATURE_SIZE + payload.length);
|
|
1098
|
+
buf[0] = kidBytes.length;
|
|
1099
|
+
buf.set(kidBytes, 1);
|
|
1100
|
+
buf.set(signature, 1 + kidBytes.length);
|
|
1101
|
+
buf.set(payload, 1 + kidBytes.length + SIGNATURE_SIZE);
|
|
1102
|
+
return buf;
|
|
1103
|
+
}
|
|
1104
|
+
function decodeEnvelope(data) {
|
|
1105
|
+
if (data.length < 1 + SIGNATURE_SIZE) throw new Error("envelope too short");
|
|
1106
|
+
const kidLen = data[0];
|
|
1107
|
+
const minLen = 1 + kidLen + SIGNATURE_SIZE;
|
|
1108
|
+
if (data.length < minLen) throw new Error("envelope too short for declared kid length");
|
|
1109
|
+
return {
|
|
1110
|
+
kid: new TextDecoder("utf-8", { fatal: true }).decode(data.subarray(1, 1 + kidLen)),
|
|
1111
|
+
signature: data.slice(1 + kidLen, 1 + kidLen + SIGNATURE_SIZE),
|
|
1112
|
+
payload: data.slice(1 + kidLen + SIGNATURE_SIZE)
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
//#endregion
|
|
1116
|
+
//#region src/crypto/hpke.ts
|
|
1117
|
+
const VERSION_HPKE = 1;
|
|
1118
|
+
const ENC_SIZE = 32;
|
|
1119
|
+
const INFO = new TextEncoder().encode("rine.e2ee.v1.hpke");
|
|
1120
|
+
const suite = new CipherSuite({
|
|
1121
|
+
kem: new DhkemX25519HkdfSha256(),
|
|
1122
|
+
kdf: new HkdfSha256(),
|
|
1123
|
+
aead: new Aes256Gcm()
|
|
1124
|
+
});
|
|
1125
|
+
async function seal(recipientPublicKey, innerEnvelope, aad) {
|
|
1126
|
+
const pk = await suite.kem.deserializePublicKey(recipientPublicKey);
|
|
1127
|
+
const sender = await suite.createSenderContext({
|
|
1128
|
+
recipientPublicKey: pk,
|
|
1129
|
+
info: INFO
|
|
1130
|
+
});
|
|
1131
|
+
const ciphertext = new Uint8Array(await sender.seal(innerEnvelope, aad));
|
|
1132
|
+
const enc = new Uint8Array(sender.enc);
|
|
1133
|
+
const out = new Uint8Array(1 + ENC_SIZE + ciphertext.length);
|
|
1134
|
+
out[0] = VERSION_HPKE;
|
|
1135
|
+
out.set(enc, 1);
|
|
1136
|
+
out.set(ciphertext, 1 + ENC_SIZE);
|
|
1137
|
+
return out;
|
|
1138
|
+
}
|
|
1139
|
+
async function open(recipientPrivateKey, encryptedPayload, aad) {
|
|
1140
|
+
if (encryptedPayload.length < 1 + ENC_SIZE + 1) throw new Error("encrypted payload too short");
|
|
1141
|
+
const version = encryptedPayload[0];
|
|
1142
|
+
if (version !== VERSION_HPKE) throw new Error(`unsupported version: 0x${version?.toString(16).padStart(2, "0")}`);
|
|
1143
|
+
const enc = encryptedPayload.slice(1, 1 + ENC_SIZE);
|
|
1144
|
+
const ct = encryptedPayload.slice(1 + ENC_SIZE);
|
|
1145
|
+
const sk = await suite.kem.deserializePrivateKey(recipientPrivateKey);
|
|
1146
|
+
const recipient = await suite.createRecipientContext({
|
|
1147
|
+
recipientKey: sk,
|
|
1148
|
+
enc,
|
|
1149
|
+
info: INFO
|
|
823
1150
|
});
|
|
1151
|
+
return new Uint8Array(await recipient.open(ct, aad));
|
|
1152
|
+
}
|
|
1153
|
+
//#endregion
|
|
1154
|
+
//#region src/crypto/sender-keys-helpers.ts
|
|
1155
|
+
function uuidToBytes(uuid) {
|
|
1156
|
+
const matches = uuid.replace(/-/g, "").match(/../g);
|
|
1157
|
+
if (!matches) throw new Error(`Invalid UUID: ${uuid}`);
|
|
1158
|
+
return new Uint8Array(matches.map((b) => Number.parseInt(b, 16)));
|
|
1159
|
+
}
|
|
1160
|
+
function bytesToUuid(bytes) {
|
|
1161
|
+
const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1162
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
1163
|
+
}
|
|
1164
|
+
function senderKeysDir(agentId) {
|
|
1165
|
+
validatePathId(agentId, "agent ID");
|
|
1166
|
+
return join(getConfigDir(), "keys", agentId, "sender_keys");
|
|
1167
|
+
}
|
|
1168
|
+
function saveSenderKeyState(agentId, groupId, states) {
|
|
1169
|
+
validatePathId(groupId, "group ID");
|
|
1170
|
+
const dir = senderKeysDir(agentId);
|
|
1171
|
+
fs.mkdirSync(dir, {
|
|
1172
|
+
recursive: true,
|
|
1173
|
+
mode: 448
|
|
1174
|
+
});
|
|
1175
|
+
const filePath = join(dir, `${groupId}.json`);
|
|
1176
|
+
const data = states.map((s) => ({
|
|
1177
|
+
...s,
|
|
1178
|
+
chainKey: toBase64Url(s.chainKey)
|
|
1179
|
+
}));
|
|
1180
|
+
fs.writeFileSync(filePath, JSON.stringify(data), "utf-8");
|
|
1181
|
+
fs.chmodSync(filePath, 384);
|
|
1182
|
+
}
|
|
1183
|
+
function loadSenderKeyStates(agentId, groupId) {
|
|
1184
|
+
validatePathId(groupId, "group ID");
|
|
1185
|
+
const filePath = join(senderKeysDir(agentId), `${groupId}.json`);
|
|
1186
|
+
if (!fs.existsSync(filePath)) return [];
|
|
1187
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
1188
|
+
return JSON.parse(raw).map((s) => ({
|
|
1189
|
+
...s,
|
|
1190
|
+
chainKey: fromBase64Url(s.chainKey)
|
|
1191
|
+
}));
|
|
1192
|
+
}
|
|
1193
|
+
//#endregion
|
|
1194
|
+
//#region src/crypto/sign.ts
|
|
1195
|
+
function signPayload(signingPrivateKey, plaintext) {
|
|
1196
|
+
return ed25519.sign(plaintext, signingPrivateKey);
|
|
1197
|
+
}
|
|
1198
|
+
function verifySignature(signingPublicKey, plaintext, signature) {
|
|
1199
|
+
try {
|
|
1200
|
+
return ed25519.verify(signature, plaintext, signingPublicKey);
|
|
1201
|
+
} catch {
|
|
1202
|
+
return false;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
//#endregion
|
|
1206
|
+
//#region src/crypto/sender-keys.ts
|
|
1207
|
+
const ROTATION_MESSAGE_LIMIT = 100;
|
|
1208
|
+
const ROTATION_AGE_MS = 10080 * 60 * 1e3;
|
|
1209
|
+
function needsRotation(state) {
|
|
1210
|
+
if (state.messageIndex >= ROTATION_MESSAGE_LIMIT) return true;
|
|
1211
|
+
if (state.createdAt && Date.now() - state.createdAt > ROTATION_AGE_MS) return true;
|
|
1212
|
+
return false;
|
|
1213
|
+
}
|
|
1214
|
+
const VERSION_SENDER_KEY = 2;
|
|
1215
|
+
const SENDER_KEY_ID_SIZE = 16;
|
|
1216
|
+
const MESSAGE_INDEX_SIZE = 4;
|
|
1217
|
+
const NONCE_SIZE = 12;
|
|
1218
|
+
const MAX_SKIP = 1e3;
|
|
1219
|
+
/** Copy Uint8Array into a plain ArrayBuffer-backed Uint8Array (required by Web Crypto). */
|
|
1220
|
+
function toPlainBuffer(src) {
|
|
1221
|
+
const buf = new Uint8Array(src.length);
|
|
1222
|
+
buf.set(src);
|
|
1223
|
+
return buf;
|
|
1224
|
+
}
|
|
1225
|
+
function generateSenderKey(groupId, senderAgentId) {
|
|
1226
|
+
const chainKey = new Uint8Array(32);
|
|
1227
|
+
crypto.getRandomValues(chainKey);
|
|
1228
|
+
return {
|
|
1229
|
+
senderKeyId: crypto.randomUUID(),
|
|
1230
|
+
groupId,
|
|
1231
|
+
senderAgentId,
|
|
1232
|
+
chainKey,
|
|
1233
|
+
messageIndex: 0,
|
|
1234
|
+
createdAt: Date.now()
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
function deriveMessageKey(chainKey) {
|
|
1238
|
+
return {
|
|
1239
|
+
messageKey: hmac(sha256, chainKey, new Uint8Array([1])),
|
|
1240
|
+
nextChainKey: hmac(sha256, chainKey, new Uint8Array([2]))
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
function advanceChain(state, targetIndex) {
|
|
1244
|
+
if (targetIndex - state.messageIndex > MAX_SKIP) throw new Error(`too many skipped messages: ${targetIndex - state.messageIndex} (max ${MAX_SKIP})`);
|
|
1245
|
+
const skippedKeys = /* @__PURE__ */ new Map();
|
|
1246
|
+
let chainKey = state.chainKey;
|
|
1247
|
+
let index = state.messageIndex;
|
|
1248
|
+
while (index < targetIndex) {
|
|
1249
|
+
const { messageKey, nextChainKey } = deriveMessageKey(chainKey);
|
|
1250
|
+
skippedKeys.set(index, messageKey);
|
|
1251
|
+
chainKey = nextChainKey;
|
|
1252
|
+
index++;
|
|
1253
|
+
}
|
|
1254
|
+
return {
|
|
1255
|
+
updatedState: {
|
|
1256
|
+
...state,
|
|
1257
|
+
chainKey,
|
|
1258
|
+
messageIndex: index
|
|
1259
|
+
},
|
|
1260
|
+
skippedKeys
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
/** Build AAD for sender-key encryption: binds ciphertext to sender_key_id and message_index. */
|
|
1264
|
+
function buildSenderKeyAad(senderKeyId, messageIndex) {
|
|
1265
|
+
const aad = new Uint8Array(SENDER_KEY_ID_SIZE + MESSAGE_INDEX_SIZE);
|
|
1266
|
+
aad.set(senderKeyId, 0);
|
|
1267
|
+
new DataView(aad.buffer).setUint32(SENDER_KEY_ID_SIZE, messageIndex, false);
|
|
1268
|
+
return aad;
|
|
1269
|
+
}
|
|
1270
|
+
async function sealGroup(senderSigningPrivateKey, senderSigningKid, state, plaintext) {
|
|
1271
|
+
const innerEnvelope = encodeEnvelope(senderSigningKid, signPayload(senderSigningPrivateKey, plaintext), plaintext);
|
|
1272
|
+
const { messageKey, nextChainKey } = deriveMessageKey(state.chainKey);
|
|
1273
|
+
const nonce = new Uint8Array(NONCE_SIZE);
|
|
1274
|
+
crypto.getRandomValues(nonce);
|
|
1275
|
+
const idBytes = uuidToBytes(state.senderKeyId);
|
|
1276
|
+
const aad = buildSenderKeyAad(idBytes, state.messageIndex);
|
|
1277
|
+
const cryptoKey = await crypto.subtle.importKey("raw", toPlainBuffer(messageKey), "AES-GCM", false, ["encrypt"]);
|
|
1278
|
+
const ciphertext = new Uint8Array(await crypto.subtle.encrypt({
|
|
1279
|
+
name: "AES-GCM",
|
|
1280
|
+
iv: nonce,
|
|
1281
|
+
additionalData: toPlainBuffer(aad)
|
|
1282
|
+
}, cryptoKey, toPlainBuffer(innerEnvelope)));
|
|
1283
|
+
messageKey.fill(0);
|
|
1284
|
+
const out = new Uint8Array(1 + SENDER_KEY_ID_SIZE + MESSAGE_INDEX_SIZE + NONCE_SIZE + ciphertext.length);
|
|
1285
|
+
let offset = 0;
|
|
1286
|
+
out[offset++] = VERSION_SENDER_KEY;
|
|
1287
|
+
out.set(idBytes, offset);
|
|
1288
|
+
offset += SENDER_KEY_ID_SIZE;
|
|
1289
|
+
new DataView(out.buffer).setUint32(offset, state.messageIndex, false);
|
|
1290
|
+
offset += MESSAGE_INDEX_SIZE;
|
|
1291
|
+
out.set(nonce, offset);
|
|
1292
|
+
offset += NONCE_SIZE;
|
|
1293
|
+
out.set(ciphertext, offset);
|
|
1294
|
+
return {
|
|
1295
|
+
encryptedPayload: out,
|
|
1296
|
+
updatedState: {
|
|
1297
|
+
...state,
|
|
1298
|
+
chainKey: nextChainKey,
|
|
1299
|
+
messageIndex: state.messageIndex + 1
|
|
1300
|
+
}
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
async function openGroup(senderKeyStates, encryptedPayload) {
|
|
1304
|
+
const minLen = 1 + SENDER_KEY_ID_SIZE + MESSAGE_INDEX_SIZE + NONCE_SIZE;
|
|
1305
|
+
if (encryptedPayload.length < minLen) throw new Error("encrypted payload too short");
|
|
1306
|
+
const version = encryptedPayload[0];
|
|
1307
|
+
if (version !== VERSION_SENDER_KEY) throw new Error(`unsupported version: 0x${version?.toString(16).padStart(2, "0")}`);
|
|
1308
|
+
let offset = 1;
|
|
1309
|
+
const senderKeyId = bytesToUuid(encryptedPayload.slice(offset, offset + SENDER_KEY_ID_SIZE));
|
|
1310
|
+
offset += SENDER_KEY_ID_SIZE;
|
|
1311
|
+
const messageIndex = new DataView(encryptedPayload.buffer, encryptedPayload.byteOffset).getUint32(offset, false);
|
|
1312
|
+
offset += MESSAGE_INDEX_SIZE;
|
|
1313
|
+
const nonce = encryptedPayload.slice(offset, offset + NONCE_SIZE);
|
|
1314
|
+
offset += NONCE_SIZE;
|
|
1315
|
+
const ciphertext = encryptedPayload.slice(offset);
|
|
1316
|
+
const state = senderKeyStates.get(senderKeyId);
|
|
1317
|
+
if (!state) throw new Error(`unknown sender key id: ${senderKeyId}`);
|
|
1318
|
+
let messageKey;
|
|
1319
|
+
let updatedState;
|
|
1320
|
+
if (messageIndex < state.messageIndex) {
|
|
1321
|
+
const cached = state.skippedKeys?.[messageIndex];
|
|
1322
|
+
if (!cached) throw new Error("message key expired or already consumed");
|
|
1323
|
+
const { fromBase64Url } = await Promise.resolve().then(() => keys_exports);
|
|
1324
|
+
messageKey = fromBase64Url(cached);
|
|
1325
|
+
const newSkipped = { ...state.skippedKeys };
|
|
1326
|
+
delete newSkipped[messageIndex];
|
|
1327
|
+
updatedState = {
|
|
1328
|
+
...state,
|
|
1329
|
+
skippedKeys: newSkipped
|
|
1330
|
+
};
|
|
1331
|
+
} else if (messageIndex === state.messageIndex) {
|
|
1332
|
+
const { messageKey: mk, nextChainKey } = deriveMessageKey(state.chainKey);
|
|
1333
|
+
messageKey = mk;
|
|
1334
|
+
updatedState = {
|
|
1335
|
+
...state,
|
|
1336
|
+
chainKey: nextChainKey,
|
|
1337
|
+
messageIndex: state.messageIndex + 1
|
|
1338
|
+
};
|
|
1339
|
+
} else {
|
|
1340
|
+
const { updatedState: advanced, skippedKeys: newSkipped } = advanceChain(state, messageIndex);
|
|
1341
|
+
const { messageKey: mk, nextChainKey } = deriveMessageKey(advanced.chainKey);
|
|
1342
|
+
messageKey = mk;
|
|
1343
|
+
const mergedSkipped = { ...state.skippedKeys ?? {} };
|
|
1344
|
+
for (const [idx, key] of newSkipped) {
|
|
1345
|
+
mergedSkipped[idx] = toBase64Url(key);
|
|
1346
|
+
key.fill(0);
|
|
1347
|
+
}
|
|
1348
|
+
const entries = Object.entries(mergedSkipped);
|
|
1349
|
+
if (entries.length > MAX_SKIP) {
|
|
1350
|
+
entries.sort((a, b) => Number(a[0]) - Number(b[0]));
|
|
1351
|
+
const excess = entries.length - MAX_SKIP;
|
|
1352
|
+
for (let i = 0; i < excess; i++) delete mergedSkipped[Number(entries[i][0])];
|
|
1353
|
+
}
|
|
1354
|
+
updatedState = {
|
|
1355
|
+
...advanced,
|
|
1356
|
+
chainKey: nextChainKey,
|
|
1357
|
+
messageIndex: messageIndex + 1,
|
|
1358
|
+
skippedKeys: mergedSkipped
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
const aad = buildSenderKeyAad(encryptedPayload.slice(1, 1 + SENDER_KEY_ID_SIZE), messageIndex);
|
|
1362
|
+
const cryptoKey = await crypto.subtle.importKey("raw", toPlainBuffer(messageKey), "AES-GCM", false, ["decrypt"]);
|
|
1363
|
+
const innerEnvelope = new Uint8Array(await crypto.subtle.decrypt({
|
|
1364
|
+
name: "AES-GCM",
|
|
1365
|
+
iv: nonce,
|
|
1366
|
+
additionalData: toPlainBuffer(aad)
|
|
1367
|
+
}, cryptoKey, toPlainBuffer(ciphertext)));
|
|
1368
|
+
messageKey.fill(0);
|
|
1369
|
+
return {
|
|
1370
|
+
innerEnvelope,
|
|
1371
|
+
senderKeyId,
|
|
1372
|
+
messageIndex,
|
|
1373
|
+
updatedState
|
|
1374
|
+
};
|
|
824
1375
|
}
|
|
825
1376
|
//#endregion
|
|
826
|
-
//#region src/
|
|
1377
|
+
//#region src/crypto/message.ts
|
|
1378
|
+
async function verifyEnvelopeSender(decoded, client) {
|
|
1379
|
+
let verified = false;
|
|
1380
|
+
let verificationStatus = "unverifiable";
|
|
1381
|
+
try {
|
|
1382
|
+
const senderAgentId = agentIdFromKid(decoded.kid);
|
|
1383
|
+
verified = verifySignature(jwkToPublicKey((await client.get(`/agents/${senderAgentId}/keys`)).signing_public_key), decoded.payload, decoded.signature);
|
|
1384
|
+
verificationStatus = verified ? "verified" : "invalid";
|
|
1385
|
+
} catch {}
|
|
1386
|
+
return {
|
|
1387
|
+
verified,
|
|
1388
|
+
verificationStatus
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
function signingKid(agentId) {
|
|
1392
|
+
return `rine:${agentId}`;
|
|
1393
|
+
}
|
|
1394
|
+
async function encryptMessage(senderAgentId, recipientEncryptionPk, payload) {
|
|
1395
|
+
const keys = loadAgentKeys(senderAgentId);
|
|
1396
|
+
const plaintext = new TextEncoder().encode(JSON.stringify(payload));
|
|
1397
|
+
const kid = signingKid(senderAgentId);
|
|
1398
|
+
return {
|
|
1399
|
+
encrypted_payload: toBase64Url(await seal(recipientEncryptionPk, encodeEnvelope(kid, signPayload(keys.signing.privateKey, plaintext), plaintext))),
|
|
1400
|
+
encryption_version: "hpke-v1",
|
|
1401
|
+
sender_signing_kid: kid
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1404
|
+
async function fetchRecipientEncryptionKey(client, agentId) {
|
|
1405
|
+
return jwkToPublicKey((await client.get(`/agents/${agentId}/keys`)).encryption_public_key);
|
|
1406
|
+
}
|
|
1407
|
+
async function decryptMessage(recipientAgentId, encryptedPayloadB64, client) {
|
|
1408
|
+
const keys = loadAgentKeys(recipientAgentId);
|
|
1409
|
+
const encrypted = fromBase64Url(encryptedPayloadB64);
|
|
1410
|
+
const decoded = decodeEnvelope(await open(keys.encryption.privateKey, encrypted));
|
|
1411
|
+
const plaintext = new TextDecoder("utf-8", { fatal: true }).decode(decoded.payload);
|
|
1412
|
+
const { verified, verificationStatus } = await verifyEnvelopeSender(decoded, client);
|
|
1413
|
+
return {
|
|
1414
|
+
plaintext,
|
|
1415
|
+
senderKid: decoded.kid,
|
|
1416
|
+
verified,
|
|
1417
|
+
verificationStatus
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
async function encryptGroupMessage(senderAgentId, groupId, senderKeyState, payload) {
|
|
1421
|
+
const keys = loadAgentKeys(senderAgentId);
|
|
1422
|
+
const plaintext = new TextEncoder().encode(JSON.stringify(payload));
|
|
1423
|
+
const kid = signingKid(senderAgentId);
|
|
1424
|
+
const selfReadKey = deriveMessageKey(senderKeyState.chainKey).messageKey;
|
|
1425
|
+
const selfReadIndex = senderKeyState.messageIndex;
|
|
1426
|
+
const { encryptedPayload, updatedState } = await sealGroup(keys.signing.privateKey, kid, senderKeyState, plaintext);
|
|
1427
|
+
updatedState.skippedKeys = {
|
|
1428
|
+
...updatedState.skippedKeys ?? {},
|
|
1429
|
+
[selfReadIndex]: toBase64Url(selfReadKey)
|
|
1430
|
+
};
|
|
1431
|
+
selfReadKey.fill(0);
|
|
1432
|
+
saveSenderKeyState(senderAgentId, groupId, [...loadSenderKeyStates(senderAgentId, groupId).filter((s) => s.senderKeyId !== updatedState.senderKeyId), updatedState]);
|
|
1433
|
+
return {
|
|
1434
|
+
result: {
|
|
1435
|
+
encrypted_payload: toBase64Url(encryptedPayload),
|
|
1436
|
+
encryption_version: "sender-key-v1",
|
|
1437
|
+
sender_signing_kid: kid
|
|
1438
|
+
},
|
|
1439
|
+
updatedState
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
async function decryptGroupMessage(recipientAgentId, groupId, encryptedPayloadB64, client) {
|
|
1443
|
+
const states = loadSenderKeyStates(recipientAgentId, groupId);
|
|
1444
|
+
const stateMap = /* @__PURE__ */ new Map();
|
|
1445
|
+
for (const s of states) stateMap.set(s.senderKeyId, s);
|
|
1446
|
+
const { innerEnvelope, updatedState, messageIndex } = await openGroup(stateMap, fromBase64Url(encryptedPayloadB64));
|
|
1447
|
+
const decoded = decodeEnvelope(innerEnvelope);
|
|
1448
|
+
const plaintext = new TextDecoder("utf-8", { fatal: true }).decode(decoded.payload);
|
|
1449
|
+
const { verified, verificationStatus } = await verifyEnvelopeSender(decoded, client);
|
|
1450
|
+
if (verificationStatus === "invalid") throw new Error("Sender signature verification failed");
|
|
1451
|
+
if (updatedState.senderAgentId === recipientAgentId) {
|
|
1452
|
+
const prevCached = stateMap.get(updatedState.senderKeyId)?.skippedKeys?.[messageIndex];
|
|
1453
|
+
if (prevCached && !updatedState.skippedKeys?.[messageIndex]) updatedState.skippedKeys = {
|
|
1454
|
+
...updatedState.skippedKeys ?? {},
|
|
1455
|
+
[messageIndex]: prevCached
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
stateMap.set(updatedState.senderKeyId, updatedState);
|
|
1459
|
+
saveSenderKeyState(recipientAgentId, groupId, Array.from(stateMap.values()));
|
|
1460
|
+
return {
|
|
1461
|
+
plaintext,
|
|
1462
|
+
senderKid: decoded.kid,
|
|
1463
|
+
verified,
|
|
1464
|
+
verificationStatus
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
function getAgentPublicKeys(agentId, agentKeys) {
|
|
1468
|
+
const keys = agentKeys ?? loadAgentKeys(agentId);
|
|
1469
|
+
return {
|
|
1470
|
+
signing_public_key: signingPublicKeyToJWK(keys.signing.publicKey),
|
|
1471
|
+
encryption_public_key: {
|
|
1472
|
+
kty: "OKP",
|
|
1473
|
+
crv: "X25519",
|
|
1474
|
+
x: toBase64Url(keys.encryption.publicKey)
|
|
1475
|
+
}
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
//#endregion
|
|
1479
|
+
//#region src/commands/keys.ts
|
|
1480
|
+
function getKeysDir(agentId) {
|
|
1481
|
+
return join(getConfigDir(), "keys", agentId);
|
|
1482
|
+
}
|
|
1483
|
+
function backupKeys(agentId) {
|
|
1484
|
+
const dir = getKeysDir(agentId);
|
|
1485
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1486
|
+
for (const name of ["signing.key", "encryption.key"]) {
|
|
1487
|
+
const src = join(dir, name);
|
|
1488
|
+
if (!fs.existsSync(src)) continue;
|
|
1489
|
+
for (const f of fs.readdirSync(dir)) if (f.startsWith(`${name}.bak.`)) fs.unlinkSync(join(dir, f));
|
|
1490
|
+
fs.copyFileSync(src, join(dir, `${name}.bak.${ts}`));
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
async function verifyServerKeys(client, agentId, expected) {
|
|
1494
|
+
const server = await client.get(`/agents/${agentId}/keys`);
|
|
1495
|
+
if (server.signing_public_key.x !== expected.signing_public_key.x || server.encryption_public_key.x !== expected.encryption_public_key.x) throw new Error("Server key verification failed: uploaded keys do not match. Old keys preserved locally.");
|
|
1496
|
+
}
|
|
1497
|
+
function registerKeys(program) {
|
|
1498
|
+
const keys = program.command("keys").description("E2EE key management");
|
|
1499
|
+
keys.command("generate").description("Generate new E2EE keys for an agent").option("--agent <id>", "Agent ID").action(withClient(program, async ({ client, gOpts }, opts) => {
|
|
1500
|
+
const agentId = await resolveAgent(client, opts.agent, gOpts.as);
|
|
1501
|
+
if (agentKeysExist(agentId)) {
|
|
1502
|
+
printError(`Keys already exist for agent ${agentId}. Use 'rine keys rotate' to replace them.`);
|
|
1503
|
+
process.exitCode = 1;
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
const agentKeys = generateAgentKeys();
|
|
1507
|
+
const pubKeys = getAgentPublicKeys(agentId, agentKeys);
|
|
1508
|
+
await client.post(`/agents/${agentId}/keys`, pubKeys);
|
|
1509
|
+
await verifyServerKeys(client, agentId, pubKeys);
|
|
1510
|
+
saveAgentKeys(agentId, agentKeys);
|
|
1511
|
+
printMutationOk(`E2EE keys generated for agent ${agentId}`, gOpts.json);
|
|
1512
|
+
}));
|
|
1513
|
+
keys.command("rotate").description("Rotate E2EE keys for an agent").option("--agent <id>", "Agent ID").action(withClient(program, async ({ client, gOpts }, opts) => {
|
|
1514
|
+
const agentId = await resolveAgent(client, opts.agent, gOpts.as);
|
|
1515
|
+
if (agentKeysExist(agentId)) backupKeys(agentId);
|
|
1516
|
+
const agentKeys = generateAgentKeys();
|
|
1517
|
+
const pubKeys = getAgentPublicKeys(agentId, agentKeys);
|
|
1518
|
+
await client.post(`/agents/${agentId}/keys`, pubKeys);
|
|
1519
|
+
await verifyServerKeys(client, agentId, pubKeys);
|
|
1520
|
+
saveAgentKeys(agentId, agentKeys);
|
|
1521
|
+
printMutationOk(`E2EE keys rotated for agent ${agentId}`, gOpts.json);
|
|
1522
|
+
}));
|
|
1523
|
+
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) => {
|
|
1524
|
+
const agentId = await resolveAgent(client, opts.agent, gOpts.as);
|
|
1525
|
+
if (!agentKeysExist(agentId)) {
|
|
1526
|
+
printError(`No keys found for agent ${agentId}`);
|
|
1527
|
+
process.exitCode = 1;
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
const agentKeys = loadAgentKeys(agentId);
|
|
1531
|
+
const exported = {
|
|
1532
|
+
agent_id: agentId,
|
|
1533
|
+
signing_private_key: toBase64Url(agentKeys.signing.privateKey),
|
|
1534
|
+
encryption_private_key: toBase64Url(agentKeys.encryption.privateKey)
|
|
1535
|
+
};
|
|
1536
|
+
fs.writeFileSync(opts.output, JSON.stringify(exported, null, 2));
|
|
1537
|
+
fs.chmodSync(opts.output, 384);
|
|
1538
|
+
if (gOpts.json) printJson({
|
|
1539
|
+
ok: true,
|
|
1540
|
+
path: opts.output
|
|
1541
|
+
});
|
|
1542
|
+
else console.log(`Keys exported to ${opts.output}`);
|
|
1543
|
+
}));
|
|
1544
|
+
keys.command("import").description("Import private keys from a file").requiredOption("--input <file>", "Input file path").action(withErrorHandler(program, (gOpts, opts) => {
|
|
1545
|
+
const raw = fs.readFileSync(opts.input, "utf-8");
|
|
1546
|
+
const data = JSON.parse(raw);
|
|
1547
|
+
if (!data.agent_id || !data.signing_private_key || !data.encryption_private_key) {
|
|
1548
|
+
printError("Invalid key export file");
|
|
1549
|
+
process.exitCode = 2;
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
const sigBytes = fromBase64Url(data.signing_private_key);
|
|
1553
|
+
if (sigBytes.length !== 32) {
|
|
1554
|
+
printError(`Invalid signing key: expected 32 bytes, got ${sigBytes.length}`);
|
|
1555
|
+
process.exitCode = 2;
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
try {
|
|
1559
|
+
ed25519.getPublicKey(sigBytes);
|
|
1560
|
+
} catch {
|
|
1561
|
+
printError("Invalid signing key: not a valid Ed25519 private key");
|
|
1562
|
+
process.exitCode = 2;
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
const encBytes = fromBase64Url(data.encryption_private_key);
|
|
1566
|
+
if (encBytes.length !== 32) {
|
|
1567
|
+
printError(`Invalid encryption key: expected 32 bytes, got ${encBytes.length}`);
|
|
1568
|
+
process.exitCode = 2;
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
try {
|
|
1572
|
+
x25519.getPublicKey(encBytes);
|
|
1573
|
+
} catch {
|
|
1574
|
+
printError("Invalid encryption key: not a valid X25519 private key");
|
|
1575
|
+
process.exitCode = 2;
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
const dir = join(getConfigDir(), "keys", data.agent_id);
|
|
1579
|
+
fs.mkdirSync(dir, {
|
|
1580
|
+
recursive: true,
|
|
1581
|
+
mode: 448
|
|
1582
|
+
});
|
|
1583
|
+
const sigPath = join(dir, "signing.key");
|
|
1584
|
+
fs.writeFileSync(sigPath, data.signing_private_key, "utf-8");
|
|
1585
|
+
fs.chmodSync(sigPath, 384);
|
|
1586
|
+
const encPath = join(dir, "encryption.key");
|
|
1587
|
+
fs.writeFileSync(encPath, data.encryption_private_key, "utf-8");
|
|
1588
|
+
fs.chmodSync(encPath, 384);
|
|
1589
|
+
printMutationOk(`Keys imported for agent ${data.agent_id}`, gOpts.json);
|
|
1590
|
+
}));
|
|
1591
|
+
}
|
|
1592
|
+
//#endregion
|
|
1593
|
+
//#region src/crypto/ingest.ts
|
|
827
1594
|
/**
|
|
828
|
-
*
|
|
829
|
-
*
|
|
1595
|
+
* Auto-ingest a sender key distribution from a decrypted message.
|
|
1596
|
+
* Only ingests if the message is a `rine.v1.sender_key_distribution`,
|
|
1597
|
+
* the signature was verified, and the key ID is not already known.
|
|
1598
|
+
*
|
|
1599
|
+
* Returns true if a new key was ingested.
|
|
830
1600
|
*/
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
if (
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
1601
|
+
function ingestSenderKeyDistribution(agentId, messageType, result) {
|
|
1602
|
+
if (messageType !== "rine.v1.sender_key_distribution") return false;
|
|
1603
|
+
if (!result.verified) return false;
|
|
1604
|
+
try {
|
|
1605
|
+
const dist = JSON.parse(result.plaintext);
|
|
1606
|
+
const existing = loadSenderKeyStates(agentId, dist.group_id);
|
|
1607
|
+
if (existing.some((s) => s.senderKeyId === dist.sender_key_id)) return false;
|
|
1608
|
+
const newState = {
|
|
1609
|
+
senderKeyId: dist.sender_key_id,
|
|
1610
|
+
groupId: dist.group_id,
|
|
1611
|
+
senderAgentId: agentIdFromKid(result.senderKid),
|
|
1612
|
+
chainKey: fromBase64Url(dist.chain_key),
|
|
1613
|
+
messageIndex: dist.message_index
|
|
1614
|
+
};
|
|
1615
|
+
saveSenderKeyState(agentId, dist.group_id, [...existing, newState]);
|
|
1616
|
+
return true;
|
|
1617
|
+
} catch {
|
|
1618
|
+
return false;
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
//#endregion
|
|
1622
|
+
//#region src/commands/sender-key-ops.ts
|
|
1623
|
+
async function distributeSenderKey(client, senderAgentId, state, groupId, recipientIds, extraHeaders) {
|
|
1624
|
+
if (recipientIds.length === 0) return [];
|
|
1625
|
+
const batchKeys = await client.get("/agents/keys", { ids: recipientIds.join(",") });
|
|
1626
|
+
const distPayload = {
|
|
1627
|
+
group_id: groupId,
|
|
1628
|
+
sender_key_id: state.senderKeyId,
|
|
1629
|
+
chain_key: toBase64Url(state.chainKey),
|
|
1630
|
+
message_index: state.messageIndex,
|
|
1631
|
+
sender_agent_id: senderAgentId
|
|
1632
|
+
};
|
|
1633
|
+
const succeeded = [];
|
|
1634
|
+
const failed = [];
|
|
1635
|
+
for (const recipientId of recipientIds) {
|
|
1636
|
+
const recipientKeyData = batchKeys.keys[recipientId];
|
|
1637
|
+
if (!recipientKeyData) {
|
|
1638
|
+
failed.push(recipientId);
|
|
1639
|
+
continue;
|
|
1640
|
+
}
|
|
1641
|
+
try {
|
|
1642
|
+
const encrypted = await encryptMessage(senderAgentId, fromBase64Url(recipientKeyData.encryption_public_key.x), distPayload);
|
|
1643
|
+
await client.post("/messages", {
|
|
1644
|
+
to_agent_id: recipientId,
|
|
1645
|
+
type: "rine.v1.sender_key_distribution",
|
|
1646
|
+
...encrypted
|
|
1647
|
+
}, extraHeaders);
|
|
1648
|
+
succeeded.push(recipientId);
|
|
1649
|
+
} catch {
|
|
1650
|
+
failed.push(recipientId);
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
if (failed.length > 0) printError(`Sender key distribution failed for ${failed.length} recipient(s): ${failed.join(", ")}`);
|
|
1654
|
+
return succeeded;
|
|
1655
|
+
}
|
|
1656
|
+
async function getOrCreateSenderKey(client, senderAgentId, groupHandle, extraHeaders) {
|
|
1657
|
+
const firstGroup = (await client.get("/groups", { handle: groupHandle })).items?.[0];
|
|
1658
|
+
if (!firstGroup) throw new Error(`Group not found: ${groupHandle}`);
|
|
1659
|
+
const groupId = firstGroup.id;
|
|
1660
|
+
const memberIds = (await client.get(`/groups/${groupId}/members`)).items.map((m) => m.agent_id);
|
|
1661
|
+
const senderIdLower = senderAgentId.toLowerCase();
|
|
1662
|
+
if (!memberIds.some((id) => id.toLowerCase() === senderIdLower)) throw new Error("You are not a member of this group");
|
|
1663
|
+
const recipientIds = memberIds.filter((id) => id.toLowerCase() !== senderIdLower);
|
|
1664
|
+
const states = loadSenderKeyStates(senderAgentId, groupId);
|
|
1665
|
+
const own = states.find((s) => s.senderAgentId === senderAgentId);
|
|
1666
|
+
const currentMemberSet = new Set(memberIds.map((id) => id.toLowerCase()));
|
|
1667
|
+
const memberRemoved = own?.distributedTo?.some((id) => !currentMemberSet.has(id.toLowerCase())) ?? false;
|
|
1668
|
+
if (own && !needsRotation(own) && !memberRemoved) {
|
|
1669
|
+
const alreadyDistributed = new Set((own.distributedTo ?? []).map((id) => id.toLowerCase()));
|
|
1670
|
+
const undistributed = recipientIds.filter((id) => !alreadyDistributed.has(id.toLowerCase()));
|
|
1671
|
+
if (undistributed.length > 0) {
|
|
1672
|
+
const succeeded = await distributeSenderKey(client, senderAgentId, own, groupId, undistributed, extraHeaders);
|
|
1673
|
+
if (succeeded.length > 0) {
|
|
1674
|
+
own.distributedTo = [...own.distributedTo ?? [], ...succeeded];
|
|
1675
|
+
saveSenderKeyState(senderAgentId, groupId, states);
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
return {
|
|
1679
|
+
state: own,
|
|
1680
|
+
groupId
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
const newState = generateSenderKey(groupId, senderAgentId);
|
|
1684
|
+
newState.distributedTo = await distributeSenderKey(client, senderAgentId, newState, groupId, recipientIds, extraHeaders);
|
|
1685
|
+
saveSenderKeyState(senderAgentId, groupId, [...memberRemoved ? states.filter((s) => currentMemberSet.has(s.senderAgentId.toLowerCase())) : states, newState]);
|
|
1686
|
+
return {
|
|
1687
|
+
state: newState,
|
|
1688
|
+
groupId
|
|
1689
|
+
};
|
|
837
1690
|
}
|
|
838
1691
|
//#endregion
|
|
839
1692
|
//#region src/commands/messages.ts
|
|
840
1693
|
function msgFrom(m) {
|
|
841
|
-
return
|
|
1694
|
+
return m.sender_handle ?? m.from_agent_id ?? "";
|
|
842
1695
|
}
|
|
843
1696
|
function msgTo(m) {
|
|
844
|
-
return
|
|
1697
|
+
return m.recipient_handle ?? m.to_agent_id ?? "";
|
|
845
1698
|
}
|
|
846
|
-
function
|
|
847
|
-
return
|
|
848
|
-
ID: m.id,
|
|
849
|
-
"Conversation ID": m.conversation_id,
|
|
850
|
-
From: msgFrom(m),
|
|
851
|
-
To: msgTo(m),
|
|
852
|
-
Type: m.type,
|
|
853
|
-
Created: m.created_at
|
|
854
|
-
};
|
|
1699
|
+
function msgGroup(m) {
|
|
1700
|
+
return m.group_handle ?? "";
|
|
855
1701
|
}
|
|
856
|
-
function
|
|
1702
|
+
function sendRow(m) {
|
|
857
1703
|
return {
|
|
858
1704
|
ID: m.id,
|
|
859
1705
|
"Conversation ID": m.conversation_id,
|
|
860
1706
|
From: msgFrom(m),
|
|
861
1707
|
To: msgTo(m),
|
|
1708
|
+
Group: msgGroup(m),
|
|
862
1709
|
Type: m.type,
|
|
863
|
-
Preview: m.payload_preview ? String(m.payload_preview).slice(0, 60) : "",
|
|
864
|
-
Payload: JSON.stringify(m.payload ?? {}),
|
|
865
1710
|
Created: m.created_at
|
|
866
1711
|
};
|
|
867
1712
|
}
|
|
@@ -869,98 +1714,143 @@ function inboxRow(m) {
|
|
|
869
1714
|
return {
|
|
870
1715
|
ID: m.id,
|
|
871
1716
|
From: msgFrom(m),
|
|
1717
|
+
Group: msgGroup(m),
|
|
872
1718
|
Type: m.type,
|
|
873
|
-
|
|
1719
|
+
Encryption: m.encryption_version,
|
|
874
1720
|
Created: m.created_at
|
|
875
1721
|
};
|
|
876
1722
|
}
|
|
877
1723
|
function addMessageCommands(parent, program) {
|
|
878
|
-
parent.command("send").description("Send
|
|
1724
|
+
parent.command("send").description("Send an encrypted message").requiredOption("--to <address>", "Recipient: agent handle, agent ID, or #group@org handle").requiredOption("--type <type>", "Message type (e.g. rine.v1.task_request)").requiredOption("--payload <json>", "Message payload as JSON string").option("--from <address>", "Sender address (agent ID or handle with @)").option("--idempotency-key <key>", "Idempotency key header").action(withClient(program, async ({ client, gOpts }, opts) => {
|
|
1725
|
+
let parsedPayload;
|
|
879
1726
|
try {
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
} catch {
|
|
886
|
-
printError("--payload must be valid JSON");
|
|
887
|
-
process.exitCode = 2;
|
|
888
|
-
return;
|
|
889
|
-
}
|
|
890
|
-
const body = {
|
|
891
|
-
type: opts.type,
|
|
892
|
-
payload: parsedPayload
|
|
893
|
-
};
|
|
894
|
-
if (opts.to.includes("@")) body.to_handle = opts.to;
|
|
895
|
-
else body.to_agent_id = opts.to;
|
|
896
|
-
if (opts.from !== void 0) if (opts.from.includes("@")) body.from_handle = opts.from;
|
|
897
|
-
else body.from_agent_id = opts.from;
|
|
898
|
-
const extraHeaders = {};
|
|
899
|
-
if (opts.idempotencyKey) extraHeaders["Idempotency-Key"] = opts.idempotencyKey;
|
|
900
|
-
const data = await client.post("/messages", body, Object.keys(extraHeaders).length > 0 ? extraHeaders : void 0);
|
|
901
|
-
if (gOpts.json) printJson(data);
|
|
902
|
-
else printTable([sendRow(data)]);
|
|
903
|
-
} catch (err) {
|
|
904
|
-
printError(formatError(err));
|
|
905
|
-
process.exitCode = 1;
|
|
1727
|
+
parsedPayload = JSON.parse(opts.payload);
|
|
1728
|
+
} catch {
|
|
1729
|
+
printError("--payload must be valid JSON");
|
|
1730
|
+
process.exitCode = 2;
|
|
1731
|
+
return;
|
|
906
1732
|
}
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
const
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1733
|
+
const extraHeaders = {};
|
|
1734
|
+
let senderAgentId;
|
|
1735
|
+
if (opts.from !== void 0) if (opts.from.includes("@")) {
|
|
1736
|
+
const resolved = await resolveHandleViaWebFinger(opts.from);
|
|
1737
|
+
if (!UUID_RE.test(resolved)) throw new Error("Cannot resolve sender handle to agent ID. Ensure WebFinger is available or use a UUID.");
|
|
1738
|
+
senderAgentId = resolved;
|
|
1739
|
+
extraHeaders["X-Rine-Agent"] = resolved;
|
|
1740
|
+
} else {
|
|
1741
|
+
senderAgentId = opts.from;
|
|
1742
|
+
extraHeaders["X-Rine-Agent"] = opts.from;
|
|
1743
|
+
}
|
|
1744
|
+
else if (!gOpts.as) {
|
|
1745
|
+
senderAgentId = await resolveAgent(client, void 0, void 0);
|
|
1746
|
+
extraHeaders["X-Rine-Agent"] = senderAgentId;
|
|
1747
|
+
} else senderAgentId = gOpts.as;
|
|
1748
|
+
if (opts.idempotencyKey) extraHeaders["Idempotency-Key"] = opts.idempotencyKey;
|
|
1749
|
+
let recipientAgentId;
|
|
1750
|
+
if (!opts.to.includes("@")) recipientAgentId = opts.to;
|
|
1751
|
+
else if (!opts.to.startsWith("#")) {
|
|
1752
|
+
const resolved = await resolveHandleViaWebFinger(opts.to);
|
|
1753
|
+
if (!UUID_RE.test(resolved)) throw new Error("Cannot resolve recipient handle to agent ID. Ensure WebFinger is available or use a UUID.");
|
|
1754
|
+
recipientAgentId = resolved;
|
|
1755
|
+
}
|
|
1756
|
+
const body = { type: opts.type };
|
|
1757
|
+
if (opts.to.includes("@")) body.to_handle = opts.to;
|
|
1758
|
+
else body.to_agent_id = opts.to;
|
|
1759
|
+
if (opts.from?.includes("@")) body.from_handle = opts.from;
|
|
1760
|
+
if (opts.to.startsWith("#") && senderAgentId) {
|
|
1761
|
+
const { state, groupId } = await getOrCreateSenderKey(client, senderAgentId, opts.to, Object.keys(extraHeaders).length > 0 ? extraHeaders : void 0);
|
|
1762
|
+
const { result } = await encryptGroupMessage(senderAgentId, groupId, state, parsedPayload);
|
|
1763
|
+
Object.assign(body, result);
|
|
1764
|
+
} else if (senderAgentId && recipientAgentId) {
|
|
1765
|
+
const recipientPk = await fetchRecipientEncryptionKey(client, recipientAgentId);
|
|
1766
|
+
const encrypted = await encryptMessage(senderAgentId, recipientPk, parsedPayload);
|
|
1767
|
+
Object.assign(body, encrypted);
|
|
1768
|
+
} else throw new Error("Cannot encrypt: unable to resolve sender or recipient. Use UUIDs or ensure WebFinger is available.");
|
|
1769
|
+
const data = await client.post("/messages", body, Object.keys(extraHeaders).length > 0 ? extraHeaders : void 0);
|
|
1770
|
+
if (gOpts.json) printJson(data);
|
|
1771
|
+
else printTable([sendRow(data)]);
|
|
1772
|
+
}));
|
|
1773
|
+
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) => {
|
|
1774
|
+
const data = await client.get(`/messages/${messageId}`);
|
|
1775
|
+
const agentId = await resolveAgent(client, opts.agent, gOpts.as);
|
|
1776
|
+
if ((data.encryption_version === "hpke-v1" || data.encryption_version === "sender-key-v1") && agentKeysExist(agentId)) try {
|
|
1777
|
+
let result;
|
|
1778
|
+
if (data.encryption_version === "sender-key-v1" && data.group_id) result = await decryptGroupMessage(agentId, data.group_id, data.encrypted_payload, client);
|
|
1779
|
+
else result = await decryptMessage(agentId, data.encrypted_payload, client);
|
|
1780
|
+
if (ingestSenderKeyDistribution(agentId, data.type, result)) printText("Sender key ingested");
|
|
1781
|
+
if (gOpts.json) printJson({
|
|
1782
|
+
...data,
|
|
1783
|
+
decrypted_payload: JSON.parse(result.plaintext),
|
|
1784
|
+
signature_verified: result.verified,
|
|
1785
|
+
verification_status: result.verificationStatus,
|
|
1786
|
+
sender_kid: result.senderKid
|
|
1787
|
+
});
|
|
1788
|
+
else printTable([{
|
|
1789
|
+
ID: data.id,
|
|
1790
|
+
"Conversation ID": data.conversation_id,
|
|
1791
|
+
From: msgFrom(data),
|
|
1792
|
+
To: msgTo(data),
|
|
1793
|
+
Group: msgGroup(data),
|
|
1794
|
+
Type: data.type,
|
|
1795
|
+
Verified: result.verificationStatus === "verified" ? "✓" : result.verificationStatus === "invalid" ? "✗ INVALID" : "?",
|
|
1796
|
+
Payload: result.plaintext.slice(0, 200),
|
|
1797
|
+
Created: data.created_at
|
|
1798
|
+
}]);
|
|
1799
|
+
return;
|
|
915
1800
|
} catch (err) {
|
|
916
|
-
printError(
|
|
917
|
-
process.exitCode = 1;
|
|
1801
|
+
if (!gOpts.json) printError(`Decryption failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
918
1802
|
}
|
|
919
|
-
|
|
920
|
-
|
|
1803
|
+
if (gOpts.json) printJson(data);
|
|
1804
|
+
else printTable([{
|
|
1805
|
+
ID: data.id,
|
|
1806
|
+
"Conversation ID": data.conversation_id,
|
|
1807
|
+
From: msgFrom(data),
|
|
1808
|
+
To: msgTo(data),
|
|
1809
|
+
Group: msgGroup(data),
|
|
1810
|
+
Type: data.type,
|
|
1811
|
+
Encryption: data.encryption_version,
|
|
1812
|
+
"Encrypted Payload": `${data.encrypted_payload.slice(0, 40)}...`,
|
|
1813
|
+
Created: data.created_at
|
|
1814
|
+
}]);
|
|
1815
|
+
}));
|
|
1816
|
+
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) => {
|
|
1817
|
+
const agentId = await resolveAgent(client, opts.agent, gOpts.as);
|
|
1818
|
+
const params = {};
|
|
1819
|
+
if (opts.limit) params.limit = opts.limit;
|
|
1820
|
+
if (opts.cursor) params.cursor = opts.cursor;
|
|
1821
|
+
const data = await client.get(`/agents/${agentId}/messages`, params);
|
|
1822
|
+
if (gOpts.json) printJson(data);
|
|
1823
|
+
else {
|
|
1824
|
+
printTable(data.items.map(inboxRow));
|
|
1825
|
+
if (data.next_cursor) printText(`Next cursor: ${data.next_cursor}`);
|
|
1826
|
+
}
|
|
1827
|
+
}));
|
|
1828
|
+
parent.command("reply").description("Reply to a message (encrypted)").argument("<message-id>", "Message ID to reply to").requiredOption("--type <type>", "Message type").requiredOption("--payload <json>", "Reply payload as JSON string").action(withClient(program, async ({ client, gOpts }, messageId, opts) => {
|
|
1829
|
+
let parsedPayload;
|
|
921
1830
|
try {
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
if (opts.limit) params.limit = opts.limit;
|
|
928
|
-
if (opts.cursor) params.cursor = opts.cursor;
|
|
929
|
-
const data = await client.get(`/agents/${agentId}/messages`, params);
|
|
930
|
-
if (gOpts.json) printJson(data);
|
|
931
|
-
else {
|
|
932
|
-
printTable(data.items.map(inboxRow));
|
|
933
|
-
if (data.next_cursor) printText(`Next cursor: ${data.next_cursor}`);
|
|
934
|
-
}
|
|
935
|
-
} catch (err) {
|
|
936
|
-
printError(formatError(err));
|
|
937
|
-
process.exitCode = 1;
|
|
1831
|
+
parsedPayload = JSON.parse(opts.payload);
|
|
1832
|
+
} catch {
|
|
1833
|
+
printError("--payload must be valid JSON");
|
|
1834
|
+
process.exitCode = 2;
|
|
1835
|
+
return;
|
|
938
1836
|
}
|
|
939
|
-
|
|
940
|
-
|
|
1837
|
+
const original = await client.get(`/messages/${messageId}`);
|
|
1838
|
+
const senderAgentId = await resolveAgent(client, void 0, gOpts.as);
|
|
1839
|
+
const recipientAgentId = original.from_agent_id;
|
|
941
1840
|
try {
|
|
942
|
-
const
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
} catch {
|
|
948
|
-
printError("--payload must be valid JSON");
|
|
949
|
-
process.exitCode = 2;
|
|
950
|
-
return;
|
|
951
|
-
}
|
|
952
|
-
const data = await client.post(`/messages/${messageId}/reply`, {
|
|
953
|
-
type: opts.type,
|
|
954
|
-
payload: parsedPayload
|
|
955
|
-
});
|
|
1841
|
+
const body = { type: opts.type };
|
|
1842
|
+
if (!recipientAgentId || !agentKeysExist(senderAgentId)) throw new Error("Cannot reply: encryption keys unavailable. Run 'rine keys generate' first.");
|
|
1843
|
+
const encrypted = await encryptMessage(senderAgentId, await fetchRecipientEncryptionKey(client, recipientAgentId), parsedPayload);
|
|
1844
|
+
Object.assign(body, encrypted);
|
|
1845
|
+
const data = await client.post(`/messages/${messageId}/reply`, body);
|
|
956
1846
|
if (gOpts.json) printJson(data);
|
|
957
1847
|
else printTable([sendRow(data)]);
|
|
958
1848
|
} catch (err) {
|
|
959
1849
|
if (err instanceof RineApiError && err.status === 409) printError("Conversation is closed");
|
|
960
|
-
else
|
|
1850
|
+
else throw err;
|
|
961
1851
|
process.exitCode = 1;
|
|
962
1852
|
}
|
|
963
|
-
});
|
|
1853
|
+
}));
|
|
964
1854
|
}
|
|
965
1855
|
function registerMessages(program) {
|
|
966
1856
|
addMessageCommands(program, program);
|
|
@@ -974,45 +1864,31 @@ function renderOrg(data, opts) {
|
|
|
974
1864
|
return;
|
|
975
1865
|
}
|
|
976
1866
|
printTable([{
|
|
977
|
-
ID:
|
|
978
|
-
Name:
|
|
979
|
-
"Contact Email":
|
|
980
|
-
Country:
|
|
981
|
-
Created:
|
|
1867
|
+
ID: data.id,
|
|
1868
|
+
Name: data.name,
|
|
1869
|
+
"Contact Email": data.contact_email ?? "",
|
|
1870
|
+
Country: data.country_code ?? "",
|
|
1871
|
+
Created: data.created_at
|
|
982
1872
|
}]);
|
|
983
1873
|
}
|
|
984
1874
|
function registerOrg(program) {
|
|
985
1875
|
const org = program.command("org").description("Organization management");
|
|
986
|
-
org.command("get").description("Get organization details").action(async () => {
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
const gOpts = program.opts();
|
|
999
|
-
const body = {};
|
|
1000
|
-
if (opts.name) body.name = opts.name;
|
|
1001
|
-
if (opts.contactEmail) body.contact_email = opts.contactEmail;
|
|
1002
|
-
if (opts.countryCode) body.country_code = opts.countryCode;
|
|
1003
|
-
if (Object.keys(body).length === 0) {
|
|
1004
|
-
printError("At least one of --name, --contact-email, --country-code is required");
|
|
1005
|
-
process.exitCode = 2;
|
|
1006
|
-
return;
|
|
1007
|
-
}
|
|
1008
|
-
const { client } = await createClient(gOpts.profile);
|
|
1009
|
-
await client.patch("/org", body);
|
|
1010
|
-
printMutationOk("Org updated", gOpts.json);
|
|
1011
|
-
} catch (err) {
|
|
1012
|
-
printError(formatError(err));
|
|
1013
|
-
process.exitCode = 1;
|
|
1876
|
+
org.command("get").description("Get organization details").action(withClient(program, async ({ client, gOpts }) => {
|
|
1877
|
+
renderOrg(await client.get("/org"), gOpts);
|
|
1878
|
+
}));
|
|
1879
|
+
org.command("update").description("Update organization details").option("--name <name>", "Organization name").option("--contact-email <email>", "Contact email address").option("--country-code <cc>", "ISO country code (e.g. DE)").action(withClient(program, async ({ client, gOpts }, opts) => {
|
|
1880
|
+
const body = {};
|
|
1881
|
+
if (opts.name) body.name = opts.name;
|
|
1882
|
+
if (opts.contactEmail) body.contact_email = opts.contactEmail;
|
|
1883
|
+
if (opts.countryCode) body.country_code = opts.countryCode;
|
|
1884
|
+
if (Object.keys(body).length === 0) {
|
|
1885
|
+
printError("At least one of --name, --contact-email, --country-code is required");
|
|
1886
|
+
process.exitCode = 2;
|
|
1887
|
+
return;
|
|
1014
1888
|
}
|
|
1015
|
-
|
|
1889
|
+
await client.patch("/org", body);
|
|
1890
|
+
printMutationOk("Org updated", gOpts.json);
|
|
1891
|
+
}));
|
|
1016
1892
|
}
|
|
1017
1893
|
//#endregion
|
|
1018
1894
|
//#region src/timelock.ts
|
|
@@ -1032,129 +1908,122 @@ async function solveTimeLockWithProgress(baseHex, modulusHex, T, onProgress) {
|
|
|
1032
1908
|
//#endregion
|
|
1033
1909
|
//#region src/commands/register.ts
|
|
1034
1910
|
function registerRegister(program) {
|
|
1035
|
-
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(async (opts) => {
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
if (challengeRes.status === 409) {
|
|
1049
|
-
printError("Email or slug already registered");
|
|
1050
|
-
process.exitCode = 1;
|
|
1051
|
-
return;
|
|
1052
|
-
}
|
|
1053
|
-
if (challengeRes.status === 429) {
|
|
1054
|
-
printError("Rate limited — please wait before retrying");
|
|
1055
|
-
process.exitCode = 1;
|
|
1056
|
-
return;
|
|
1057
|
-
}
|
|
1058
|
-
if (!challengeRes.ok) {
|
|
1059
|
-
printError(`Registration failed: ${(await challengeRes.json().catch(() => ({}))).detail ?? challengeRes.statusText}`);
|
|
1060
|
-
process.exitCode = 1;
|
|
1061
|
-
return;
|
|
1062
|
-
}
|
|
1063
|
-
const challenge = await challengeRes.json();
|
|
1064
|
-
if (challenge.algorithm !== "rsa-timelock-v1") {
|
|
1065
|
-
printError(`Unsupported algorithm: ${challenge.algorithm}. Please upgrade rine-cli.`);
|
|
1066
|
-
process.exitCode = 1;
|
|
1067
|
-
return;
|
|
1068
|
-
}
|
|
1069
|
-
process.stderr.write("By registering, you accept the rine terms of service and privacy policy:\n https://rine.network/terms.html\n https://rine.network/privacy.html\n");
|
|
1070
|
-
const nonce = await solveTimeLockWithProgress(challenge.prefix, challenge.modulus, challenge.difficulty, (pct) => {
|
|
1071
|
-
process.stdout.write(`\rSolving PoW challenge: ${pct}%`);
|
|
1072
|
-
});
|
|
1073
|
-
process.stdout.write("\rSolving PoW challenge: 100%\n");
|
|
1074
|
-
const solveRes = await fetch(`${apiUrl}/auth/register/solve`, {
|
|
1075
|
-
method: "POST",
|
|
1076
|
-
headers: { "Content-Type": "application/json" },
|
|
1077
|
-
body: JSON.stringify({
|
|
1078
|
-
challenge_id: challenge.challenge_id,
|
|
1079
|
-
nonce,
|
|
1080
|
-
org_name: opts.name,
|
|
1081
|
-
org_slug: opts.slug,
|
|
1082
|
-
consent: true
|
|
1083
|
-
})
|
|
1084
|
-
});
|
|
1085
|
-
if (solveRes.status === 409) {
|
|
1086
|
-
printError("Conflict: email or slug already registered");
|
|
1087
|
-
process.exitCode = 1;
|
|
1088
|
-
return;
|
|
1089
|
-
}
|
|
1090
|
-
if (solveRes.status === 410) {
|
|
1091
|
-
printError("Challenge expired — please register again");
|
|
1092
|
-
process.exitCode = 1;
|
|
1093
|
-
return;
|
|
1094
|
-
}
|
|
1095
|
-
if (!solveRes.ok) {
|
|
1096
|
-
printError(`Solve failed: ${(await solveRes.json().catch(() => ({}))).detail ?? solveRes.statusText}`);
|
|
1097
|
-
process.exitCode = 1;
|
|
1098
|
-
return;
|
|
1099
|
-
}
|
|
1100
|
-
const data = await solveRes.json();
|
|
1101
|
-
const creds = loadCredentials();
|
|
1102
|
-
creds[profile] = {
|
|
1103
|
-
client_id: data.client_id,
|
|
1104
|
-
client_secret: data.client_secret
|
|
1105
|
-
};
|
|
1106
|
-
saveCredentials(creds);
|
|
1107
|
-
try {
|
|
1108
|
-
const tokenRes = await fetch(`${apiUrl}/oauth/token`, {
|
|
1109
|
-
method: "POST",
|
|
1110
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1111
|
-
body: new URLSearchParams({
|
|
1112
|
-
grant_type: "client_credentials",
|
|
1113
|
-
client_id: data.client_id,
|
|
1114
|
-
client_secret: data.client_secret
|
|
1115
|
-
}).toString()
|
|
1116
|
-
});
|
|
1117
|
-
if (tokenRes.ok) {
|
|
1118
|
-
const tokenData = await tokenRes.json();
|
|
1119
|
-
const cache = loadTokenCache();
|
|
1120
|
-
cache[profile] = {
|
|
1121
|
-
access_token: tokenData.access_token,
|
|
1122
|
-
expires_at: Date.now() / 1e3 + tokenData.expires_in
|
|
1123
|
-
};
|
|
1124
|
-
saveTokenCache(cache);
|
|
1125
|
-
}
|
|
1126
|
-
} catch {
|
|
1127
|
-
process.stderr.write("Warning: initial token cache failed\n");
|
|
1128
|
-
}
|
|
1129
|
-
if (gOpts.json) printJson(data);
|
|
1130
|
-
else {
|
|
1131
|
-
printSuccess(`Registered '${opts.name}' (${opts.slug})`);
|
|
1132
|
-
console.log("Credentials saved to .rine/credentials.json");
|
|
1133
|
-
console.log("Next: create an agent with 'rine agent create --name <agent-name>'");
|
|
1134
|
-
}
|
|
1135
|
-
} catch (err) {
|
|
1136
|
-
printError(formatError(err));
|
|
1911
|
+
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) => {
|
|
1912
|
+
const profile = gOpts.profile ?? "default";
|
|
1913
|
+
const apiUrl = getApiUrl();
|
|
1914
|
+
const challengeRes = await fetch(`${apiUrl}/auth/register`, {
|
|
1915
|
+
method: "POST",
|
|
1916
|
+
headers: { "Content-Type": "application/json" },
|
|
1917
|
+
body: JSON.stringify({
|
|
1918
|
+
email: opts.email,
|
|
1919
|
+
org_slug: opts.slug
|
|
1920
|
+
})
|
|
1921
|
+
});
|
|
1922
|
+
if (challengeRes.status === 409) {
|
|
1923
|
+
printError("Email or slug already registered");
|
|
1137
1924
|
process.exitCode = 1;
|
|
1925
|
+
return;
|
|
1138
1926
|
}
|
|
1139
|
-
|
|
1927
|
+
if (challengeRes.status === 429) {
|
|
1928
|
+
printError("Rate limited — please wait before retrying");
|
|
1929
|
+
process.exitCode = 1;
|
|
1930
|
+
return;
|
|
1931
|
+
}
|
|
1932
|
+
if (!challengeRes.ok) {
|
|
1933
|
+
printError(`Registration failed: ${(await challengeRes.json().catch(() => ({}))).detail ?? challengeRes.statusText}`);
|
|
1934
|
+
process.exitCode = 1;
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
const challenge = await challengeRes.json();
|
|
1938
|
+
if (challenge.algorithm !== "rsa-timelock-v1") {
|
|
1939
|
+
printError(`Unsupported algorithm: ${challenge.algorithm}. Please upgrade rine-cli.`);
|
|
1940
|
+
process.exitCode = 1;
|
|
1941
|
+
return;
|
|
1942
|
+
}
|
|
1943
|
+
process.stderr.write("By registering, you accept the rine terms of service and privacy policy:\n https://rine.network/terms.html\n https://rine.network/privacy.html\n");
|
|
1944
|
+
const progressStream = gOpts.json ? process.stderr : process.stdout;
|
|
1945
|
+
const nonce = await solveTimeLockWithProgress(challenge.prefix, challenge.modulus, challenge.difficulty, (pct) => {
|
|
1946
|
+
progressStream.write(`\rSolving PoW challenge: ${pct}%`);
|
|
1947
|
+
});
|
|
1948
|
+
progressStream.write("\rSolving PoW challenge: 100%\n");
|
|
1949
|
+
const solveRes = await fetch(`${apiUrl}/auth/register/solve`, {
|
|
1950
|
+
method: "POST",
|
|
1951
|
+
headers: { "Content-Type": "application/json" },
|
|
1952
|
+
body: JSON.stringify({
|
|
1953
|
+
challenge_id: challenge.challenge_id,
|
|
1954
|
+
nonce,
|
|
1955
|
+
org_name: opts.name,
|
|
1956
|
+
org_slug: opts.slug,
|
|
1957
|
+
consent: true
|
|
1958
|
+
})
|
|
1959
|
+
});
|
|
1960
|
+
if (solveRes.status === 409) {
|
|
1961
|
+
printError("Conflict: email or slug already registered");
|
|
1962
|
+
process.exitCode = 1;
|
|
1963
|
+
return;
|
|
1964
|
+
}
|
|
1965
|
+
if (solveRes.status === 410) {
|
|
1966
|
+
printError("Challenge expired — please register again");
|
|
1967
|
+
process.exitCode = 1;
|
|
1968
|
+
return;
|
|
1969
|
+
}
|
|
1970
|
+
if (!solveRes.ok) {
|
|
1971
|
+
printError(`Solve failed: ${(await solveRes.json().catch(() => ({}))).detail ?? solveRes.statusText}`);
|
|
1972
|
+
process.exitCode = 1;
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
const data = await solveRes.json();
|
|
1976
|
+
const creds = loadCredentials();
|
|
1977
|
+
creds[profile] = {
|
|
1978
|
+
client_id: data.client_id,
|
|
1979
|
+
client_secret: data.client_secret
|
|
1980
|
+
};
|
|
1981
|
+
saveCredentials(creds);
|
|
1982
|
+
try {
|
|
1983
|
+
cacheToken(profile, await fetchOAuthToken(data.client_id, data.client_secret));
|
|
1984
|
+
} catch {
|
|
1985
|
+
process.stderr.write("Warning: initial token cache failed\n");
|
|
1986
|
+
}
|
|
1987
|
+
if (gOpts.json) printJson(data);
|
|
1988
|
+
else {
|
|
1989
|
+
printSuccess(`Registered '${opts.name}' (${opts.slug})`);
|
|
1990
|
+
console.log("Credentials saved to .rine/credentials.json");
|
|
1991
|
+
console.log("Next: create an agent with 'rine agent create --name <agent-name>'");
|
|
1992
|
+
}
|
|
1993
|
+
}));
|
|
1140
1994
|
}
|
|
1141
1995
|
//#endregion
|
|
1142
1996
|
//#region src/commands/stream.ts
|
|
1143
|
-
function formatMessageLine(dataStr) {
|
|
1997
|
+
async function formatMessageLine(dataStr, agentId, client) {
|
|
1144
1998
|
try {
|
|
1145
1999
|
const data = JSON.parse(dataStr);
|
|
1146
|
-
|
|
2000
|
+
const ts = (data.created_at ?? "").slice(0, 19) || "?";
|
|
2001
|
+
const sender = data.sender_handle ?? String(data.from_agent_id ?? "?");
|
|
2002
|
+
const msgType = data.type ?? "?";
|
|
2003
|
+
const target = data.group_handle ? `${data.group_handle} (${msgType})` : msgType;
|
|
2004
|
+
let preview = "[encrypted]";
|
|
2005
|
+
if (data.encrypted_payload && agentKeysExist(agentId)) try {
|
|
2006
|
+
let result;
|
|
2007
|
+
if (data.encryption_version === "sender-key-v1" && data.group_id) result = await decryptGroupMessage(agentId, data.group_id, data.encrypted_payload, client);
|
|
2008
|
+
else if (data.encryption_version === "hpke-v1") {
|
|
2009
|
+
result = await decryptMessage(agentId, data.encrypted_payload, client);
|
|
2010
|
+
ingestSenderKeyDistribution(agentId, data.type ?? "", result);
|
|
2011
|
+
} else return `[${ts}] ${sender} \u2192 ${target}: [encrypted]`;
|
|
2012
|
+
preview = `[${result.verified ? "✓" : "?"}] ${result.plaintext.slice(0, 60)}`;
|
|
2013
|
+
} catch {
|
|
2014
|
+
preview = "[decrypt failed]";
|
|
2015
|
+
}
|
|
2016
|
+
return `[${ts}] ${sender} \u2192 ${target}: ${preview}`;
|
|
1147
2017
|
} catch {
|
|
1148
2018
|
return `[message] ${dataStr.slice(0, 80)}`;
|
|
1149
2019
|
}
|
|
1150
2020
|
}
|
|
1151
2021
|
function registerStream(program) {
|
|
1152
|
-
program.command("stream").description("Stream incoming messages via SSE").option("--agent <id>", "Agent ID (
|
|
2022
|
+
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) => {
|
|
1153
2023
|
try {
|
|
1154
2024
|
const gOpts = program.opts();
|
|
1155
2025
|
const { client, profileName, entry } = await createClient(gOpts.profile);
|
|
1156
|
-
|
|
1157
|
-
if (!agentId) agentId = await resolveFirstAgent(client);
|
|
2026
|
+
const agentId = await resolveAgent(client, opts.agent, gOpts.as);
|
|
1158
2027
|
const url = `${getApiUrl()}/agents/${agentId}/stream`;
|
|
1159
2028
|
let lastEventId;
|
|
1160
2029
|
let backoff = 1;
|
|
@@ -1169,7 +2038,7 @@ function registerStream(program) {
|
|
|
1169
2038
|
Accept: "text/event-stream"
|
|
1170
2039
|
};
|
|
1171
2040
|
if (lastEventId) headers["Last-Event-ID"] = lastEventId;
|
|
1172
|
-
await new Promise((resolve
|
|
2041
|
+
await new Promise((resolve) => {
|
|
1173
2042
|
if (stopped) {
|
|
1174
2043
|
resolve();
|
|
1175
2044
|
return;
|
|
@@ -1184,7 +2053,7 @@ function registerStream(program) {
|
|
|
1184
2053
|
id,
|
|
1185
2054
|
data
|
|
1186
2055
|
}));
|
|
1187
|
-
else if (event === "message") console.log(
|
|
2056
|
+
else if (event === "message") formatMessageLine(data, agentId, client).then((line) => console.log(line), () => console.log(`[message] ${data.slice(0, 80)}`));
|
|
1188
2057
|
else if (event === "heartbeat" && opts.verbose) process.stderr.write(`[heartbeat] ${data}\n`);
|
|
1189
2058
|
},
|
|
1190
2059
|
onDisconnect: () => resolve(),
|
|
@@ -1204,7 +2073,7 @@ function registerStream(program) {
|
|
|
1204
2073
|
process.once("SIGINT", onStop);
|
|
1205
2074
|
});
|
|
1206
2075
|
backoff = 1;
|
|
1207
|
-
} catch
|
|
2076
|
+
} catch {
|
|
1208
2077
|
if (stopped) break;
|
|
1209
2078
|
if (opts.verbose) process.stderr.write(`Reconnecting in ${backoff}s...\n`);
|
|
1210
2079
|
else process.stderr.write("Reconnecting...\n");
|
|
@@ -1221,125 +2090,86 @@ function registerStream(program) {
|
|
|
1221
2090
|
//#region src/commands/webhook.ts
|
|
1222
2091
|
function registerWebhook(program) {
|
|
1223
2092
|
const webhook = program.command("webhook").description("Manage webhooks");
|
|
1224
|
-
webhook.command("create").description("Create a new webhook").
|
|
1225
|
-
const gOpts = program.opts();
|
|
2093
|
+
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) => {
|
|
1226
2094
|
if (!opts.url.startsWith("https://")) {
|
|
1227
2095
|
printError("Webhook URL must start with https://");
|
|
1228
2096
|
process.exitCode = 2;
|
|
1229
2097
|
return;
|
|
1230
2098
|
}
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
URL: w.url,
|
|
1264
|
-
Active: w.active,
|
|
1265
|
-
Created: w.created_at
|
|
1266
|
-
})));
|
|
1267
|
-
} catch (err) {
|
|
1268
|
-
printError(formatError(err));
|
|
1269
|
-
process.exitCode = 1;
|
|
1270
|
-
}
|
|
1271
|
-
});
|
|
1272
|
-
webhook.command("delete").description("Delete a webhook").argument("<webhook-id>", "Webhook ID").option("--yes", "Skip confirmation prompt").action(async (webhookId, opts) => {
|
|
1273
|
-
const gOpts = program.opts();
|
|
1274
|
-
try {
|
|
1275
|
-
if (!opts.yes && !gOpts.json) {
|
|
1276
|
-
if (!await promptConfirm(`Delete webhook ${webhookId}?`)) return;
|
|
1277
|
-
}
|
|
1278
|
-
const { client } = await createClient(gOpts.profile);
|
|
1279
|
-
await client.delete(`/webhooks/${webhookId}`);
|
|
1280
|
-
printMutationOk("Webhook deleted", gOpts.json);
|
|
1281
|
-
} catch (err) {
|
|
1282
|
-
printError(formatError(err));
|
|
1283
|
-
process.exitCode = 1;
|
|
1284
|
-
}
|
|
1285
|
-
});
|
|
1286
|
-
webhook.command("deactivate").description("Deactivate a webhook").argument("<webhook-id>", "Webhook ID").action(async (webhookId) => {
|
|
1287
|
-
const gOpts = program.opts();
|
|
1288
|
-
try {
|
|
1289
|
-
const { client } = await createClient(gOpts.profile);
|
|
1290
|
-
await client.patch(`/webhooks/${webhookId}`, { active: false });
|
|
1291
|
-
printMutationOk("Webhook deactivated", gOpts.json);
|
|
1292
|
-
} catch (err) {
|
|
1293
|
-
printError(formatError(err));
|
|
1294
|
-
process.exitCode = 1;
|
|
1295
|
-
}
|
|
1296
|
-
});
|
|
1297
|
-
webhook.command("activate").description("Activate a webhook").argument("<webhook-id>", "Webhook ID").action(async (webhookId) => {
|
|
1298
|
-
const gOpts = program.opts();
|
|
1299
|
-
try {
|
|
1300
|
-
const { client } = await createClient(gOpts.profile);
|
|
1301
|
-
await client.patch(`/webhooks/${webhookId}`, { active: true });
|
|
1302
|
-
printMutationOk("Webhook activated", gOpts.json);
|
|
1303
|
-
} catch (err) {
|
|
1304
|
-
printError(formatError(err));
|
|
1305
|
-
process.exitCode = 1;
|
|
1306
|
-
}
|
|
1307
|
-
});
|
|
1308
|
-
webhook.command("deliveries").description("List deliveries for a webhook").argument("<webhook-id>", "Webhook ID").option("--status <status>", "Filter by delivery status").option("--limit <n>", "Maximum number of deliveries").action(async (webhookId, opts) => {
|
|
1309
|
-
const gOpts = program.opts();
|
|
1310
|
-
try {
|
|
1311
|
-
const { client } = await createClient(gOpts.profile);
|
|
1312
|
-
const params = {};
|
|
1313
|
-
if (opts.status) params.status = opts.status;
|
|
1314
|
-
if (opts.limit) params.limit = opts.limit;
|
|
1315
|
-
const data = await client.get(`/webhooks/${webhookId}/deliveries`, params);
|
|
1316
|
-
if (gOpts.json) printJson(data);
|
|
1317
|
-
else printTable((data.items ?? []).map((d) => ({
|
|
1318
|
-
ID: d.id,
|
|
1319
|
-
"Message ID": d.message_id,
|
|
1320
|
-
Status: d.status,
|
|
1321
|
-
Attempts: d.attempts,
|
|
1322
|
-
"Last Error": d.last_error ?? "—",
|
|
1323
|
-
Created: d.created_at
|
|
1324
|
-
})));
|
|
1325
|
-
} catch (err) {
|
|
1326
|
-
printError(formatError(err));
|
|
1327
|
-
process.exitCode = 1;
|
|
2099
|
+
const agentId = await resolveAgent(client, opts.agent, gOpts.as);
|
|
2100
|
+
const data = await client.post("/webhooks", {
|
|
2101
|
+
agent_id: agentId,
|
|
2102
|
+
url: opts.url
|
|
2103
|
+
});
|
|
2104
|
+
if (gOpts.json) printJson(data);
|
|
2105
|
+
else printTable([{
|
|
2106
|
+
ID: data.id,
|
|
2107
|
+
"Agent ID": data.agent_id,
|
|
2108
|
+
URL: data.url,
|
|
2109
|
+
Secret: data.secret,
|
|
2110
|
+
Active: data.active,
|
|
2111
|
+
Created: data.created_at
|
|
2112
|
+
}]);
|
|
2113
|
+
}));
|
|
2114
|
+
webhook.command("list").description("List webhooks").option("--agent <id>", "Filter by agent ID").option("--include-inactive", "Include inactive webhooks").action(withClient(program, async ({ client, gOpts }, opts) => {
|
|
2115
|
+
const params = {};
|
|
2116
|
+
if (opts.agent) params.agent_id = opts.agent;
|
|
2117
|
+
if (opts.includeInactive) params.include_inactive = true;
|
|
2118
|
+
const data = await client.get("/webhooks", params);
|
|
2119
|
+
if (gOpts.json) printJson(data);
|
|
2120
|
+
else printTable(data.items.map((w) => ({
|
|
2121
|
+
ID: w.id,
|
|
2122
|
+
"Agent ID": w.agent_id,
|
|
2123
|
+
URL: w.url,
|
|
2124
|
+
Active: w.active,
|
|
2125
|
+
Created: w.created_at
|
|
2126
|
+
})));
|
|
2127
|
+
}));
|
|
2128
|
+
webhook.command("delete").description("Delete a webhook").argument("<webhook-id>", "Webhook ID").option("--yes", "Skip confirmation prompt").action(withClient(program, async ({ client, gOpts }, webhookId, opts) => {
|
|
2129
|
+
if (!opts.yes && !gOpts.json) {
|
|
2130
|
+
if (!await promptConfirm(`Delete webhook ${webhookId}?`)) return;
|
|
1328
2131
|
}
|
|
1329
|
-
|
|
2132
|
+
await client.delete(`/webhooks/${webhookId}`);
|
|
2133
|
+
printMutationOk("Webhook deleted", gOpts.json);
|
|
2134
|
+
}));
|
|
2135
|
+
webhook.command("deactivate").description("Deactivate a webhook").argument("<webhook-id>", "Webhook ID").action(withClient(program, async ({ client, gOpts }, webhookId) => {
|
|
2136
|
+
await client.patch(`/webhooks/${webhookId}`, { active: false });
|
|
2137
|
+
printMutationOk("Webhook deactivated", gOpts.json);
|
|
2138
|
+
}));
|
|
2139
|
+
webhook.command("activate").description("Activate a webhook").argument("<webhook-id>", "Webhook ID").action(withClient(program, async ({ client, gOpts }, webhookId) => {
|
|
2140
|
+
await client.patch(`/webhooks/${webhookId}`, { active: true });
|
|
2141
|
+
printMutationOk("Webhook activated", gOpts.json);
|
|
2142
|
+
}));
|
|
2143
|
+
webhook.command("deliveries").description("List deliveries for a webhook").argument("<webhook-id>", "Webhook ID").option("--status <status>", "Filter by delivery status").option("--limit <n>", "Maximum number of deliveries").action(withClient(program, async ({ client, gOpts }, webhookId, opts) => {
|
|
2144
|
+
const params = {};
|
|
2145
|
+
if (opts.status) params.status = opts.status;
|
|
2146
|
+
if (opts.limit) params.limit = opts.limit;
|
|
2147
|
+
const data = await client.get(`/webhooks/${webhookId}/deliveries`, params);
|
|
2148
|
+
if (gOpts.json) printJson(data);
|
|
2149
|
+
else printTable(data.items.map((d) => ({
|
|
2150
|
+
ID: d.id,
|
|
2151
|
+
"Message ID": d.message_id,
|
|
2152
|
+
Status: d.status,
|
|
2153
|
+
Attempts: d.attempts,
|
|
2154
|
+
"Last Error": d.last_error ?? "—",
|
|
2155
|
+
Created: d.created_at
|
|
2156
|
+
})));
|
|
2157
|
+
}));
|
|
1330
2158
|
}
|
|
1331
2159
|
//#endregion
|
|
1332
2160
|
//#region src/main.ts
|
|
1333
2161
|
const { version } = createRequire(import.meta.url)("../package.json");
|
|
1334
|
-
const program = new Command("rine").version(version).description("rine.network CLI — messaging infrastructure for AI agents").option("--profile <name>", "credential profile to use", "default").option("--json", "output as JSON").option("--table", "output as table");
|
|
2162
|
+
const program = new Command("rine").version(version).description("rine.network CLI — messaging infrastructure for AI agents").option("--profile <name>", "credential profile to use", "default").option("--json", "output as JSON").option("--table", "output as table").option("--as <agent-uuid>", "act as a specific agent (sets X-Rine-Agent header)");
|
|
1335
2163
|
registerAuth(program);
|
|
1336
2164
|
registerRegister(program);
|
|
1337
2165
|
registerOrg(program);
|
|
1338
2166
|
registerAgent(program);
|
|
1339
2167
|
registerAgentProfile(program);
|
|
1340
2168
|
registerMessages(program);
|
|
2169
|
+
registerGroup(program);
|
|
1341
2170
|
registerDiscover(program);
|
|
1342
2171
|
registerWebhook(program);
|
|
2172
|
+
registerKeys(program);
|
|
1343
2173
|
registerStream(program);
|
|
1344
2174
|
program.parse();
|
|
1345
2175
|
//#endregion
|