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