@rine-network/cli 0.6.1 → 0.7.0

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