@r4security/cli 0.0.2 → 0.0.5
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 +58 -10
- package/lib/index.js +1616 -294
- package/lib/index.js.map +1 -1
- package/package.json +4 -3
package/lib/index.js
CHANGED
|
@@ -1,38 +1,433 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command18 } from "commander";
|
|
5
5
|
|
|
6
|
-
// src/commands/
|
|
7
|
-
import { Command } from "commander";
|
|
6
|
+
// src/commands/agent/init.ts
|
|
7
|
+
import { Command as Command2 } from "commander";
|
|
8
8
|
import readline from "node:readline";
|
|
9
|
+
import ora2 from "ora";
|
|
9
10
|
|
|
10
|
-
// src/
|
|
11
|
+
// src/commands/doctor.ts
|
|
12
|
+
import { Command } from "commander";
|
|
13
|
+
import chalk2 from "chalk";
|
|
14
|
+
import ora from "ora";
|
|
15
|
+
|
|
16
|
+
// src/lib/doctor.ts
|
|
17
|
+
import R4 from "@r4security/sdk";
|
|
18
|
+
|
|
19
|
+
// src/lib/client.ts
|
|
20
|
+
var CliClient = class {
|
|
21
|
+
apiKey;
|
|
22
|
+
baseUrl;
|
|
23
|
+
constructor(apiKey, baseUrl) {
|
|
24
|
+
this.apiKey = apiKey;
|
|
25
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Make an authenticated request to the machine API.
|
|
29
|
+
* Handles error responses consistently with the SDK pattern.
|
|
30
|
+
*/
|
|
31
|
+
async request(method, path5, body) {
|
|
32
|
+
const url = `${this.baseUrl}${path5}`;
|
|
33
|
+
const response = await fetch(url, {
|
|
34
|
+
method,
|
|
35
|
+
headers: {
|
|
36
|
+
"X-API-Key": this.apiKey,
|
|
37
|
+
"Content-Type": "application/json"
|
|
38
|
+
},
|
|
39
|
+
body: body ? JSON.stringify(body) : void 0
|
|
40
|
+
});
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
const errorBody = await response.json().catch(() => ({}));
|
|
43
|
+
const error = errorBody?.error;
|
|
44
|
+
const errorMessage = error?.message || `HTTP ${response.status}: ${response.statusText}`;
|
|
45
|
+
const errorCode = typeof error?.code === "string" ? ` [${error.code}]` : "";
|
|
46
|
+
throw new Error(`R4 API Error${errorCode}: ${errorMessage}`);
|
|
47
|
+
}
|
|
48
|
+
return response.json();
|
|
49
|
+
}
|
|
50
|
+
/** Register or re-confirm the local agent runtime public key. */
|
|
51
|
+
async registerAgentPublicKey(body) {
|
|
52
|
+
return this.request(
|
|
53
|
+
"POST",
|
|
54
|
+
"/api/v1/machine/vault/public-key",
|
|
55
|
+
body
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
/** List all visible vaults for the authenticated machine principal. */
|
|
59
|
+
async listVaults(projectId) {
|
|
60
|
+
const search = projectId ? `?projectId=${encodeURIComponent(projectId)}` : "";
|
|
61
|
+
return this.request("GET", `/api/v1/machine/vault${search}`);
|
|
62
|
+
}
|
|
63
|
+
/** Retrieve the active wrapped DEK for the current agent on a vault. */
|
|
64
|
+
async getAgentWrappedKey(vaultId) {
|
|
65
|
+
return this.request(
|
|
66
|
+
"GET",
|
|
67
|
+
`/api/v1/machine/vault/${encodeURIComponent(vaultId)}/wrapped-key`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
/** List lightweight metadata for all items in a vault. */
|
|
71
|
+
async listVaultItems(vaultId) {
|
|
72
|
+
return this.request(
|
|
73
|
+
"GET",
|
|
74
|
+
`/api/v1/machine/vault/${encodeURIComponent(vaultId)}/items`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
/** List all projects. GET /api/v1/machine/project */
|
|
78
|
+
async listProjects() {
|
|
79
|
+
return this.request("GET", "/api/v1/machine/project");
|
|
80
|
+
}
|
|
81
|
+
/** Get project details. GET /api/v1/machine/project/:id */
|
|
82
|
+
async getProject(id) {
|
|
83
|
+
return this.request("GET", `/api/v1/machine/project/${id}`);
|
|
84
|
+
}
|
|
85
|
+
/** Create a new project. POST /api/v1/machine/project */
|
|
86
|
+
async createProject(data) {
|
|
87
|
+
return this.request("POST", "/api/v1/machine/project", data);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// src/lib/private-key.ts
|
|
92
|
+
import crypto from "node:crypto";
|
|
11
93
|
import fs from "node:fs";
|
|
12
94
|
import os from "node:os";
|
|
13
95
|
import path from "node:path";
|
|
14
|
-
var
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
96
|
+
var DEFAULT_KEYS_DIR = path.join(os.homedir(), ".r4", "keys");
|
|
97
|
+
function sanitizeProfileName(profileName) {
|
|
98
|
+
const sanitized = profileName.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
99
|
+
return sanitized || "default";
|
|
100
|
+
}
|
|
101
|
+
function getDefaultPrivateKeyPath(profileName) {
|
|
102
|
+
return path.join(DEFAULT_KEYS_DIR, `${sanitizeProfileName(profileName)}.pem`);
|
|
103
|
+
}
|
|
104
|
+
function resolvePrivateKeyPath(params) {
|
|
105
|
+
if (params.cliPath) {
|
|
106
|
+
return { value: params.cliPath, source: "--private-key-path flag" };
|
|
107
|
+
}
|
|
108
|
+
if (params.envPath) {
|
|
109
|
+
return { value: params.envPath, source: "R4_PRIVATE_KEY_PATH env var" };
|
|
110
|
+
}
|
|
111
|
+
if (params.profilePath) {
|
|
112
|
+
return { value: params.profilePath, source: "profile config" };
|
|
113
|
+
}
|
|
114
|
+
const defaultPath = getDefaultPrivateKeyPath(params.profileName);
|
|
115
|
+
if (fs.existsSync(defaultPath) || params.allowDefaultIfMissing === true) {
|
|
116
|
+
return { value: defaultPath, source: "default profile key path" };
|
|
22
117
|
}
|
|
118
|
+
return { value: void 0, source: null };
|
|
23
119
|
}
|
|
24
|
-
function
|
|
25
|
-
fs.
|
|
26
|
-
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
120
|
+
function loadPrivateKey(privateKeyPath) {
|
|
121
|
+
return fs.readFileSync(path.resolve(privateKeyPath), "utf8").trim();
|
|
27
122
|
}
|
|
28
|
-
function
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
123
|
+
function derivePublicKey(privateKeyPem) {
|
|
124
|
+
return crypto.createPublicKey(privateKeyPem).export({
|
|
125
|
+
type: "spki",
|
|
126
|
+
format: "pem"
|
|
127
|
+
}).toString();
|
|
128
|
+
}
|
|
129
|
+
function ensurePrivateKey(privateKeyPath) {
|
|
130
|
+
const resolvedPath = path.resolve(privateKeyPath);
|
|
131
|
+
if (fs.existsSync(resolvedPath)) {
|
|
132
|
+
return {
|
|
133
|
+
privateKeyPem: loadPrivateKey(resolvedPath),
|
|
134
|
+
created: false
|
|
135
|
+
};
|
|
32
136
|
}
|
|
137
|
+
const keyPair = crypto.generateKeyPairSync("rsa", {
|
|
138
|
+
modulusLength: 2048,
|
|
139
|
+
publicKeyEncoding: {
|
|
140
|
+
type: "spki",
|
|
141
|
+
format: "pem"
|
|
142
|
+
},
|
|
143
|
+
privateKeyEncoding: {
|
|
144
|
+
type: "pkcs8",
|
|
145
|
+
format: "pem"
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
|
|
149
|
+
fs.writeFileSync(resolvedPath, keyPair.privateKey.trim() + "\n", {
|
|
150
|
+
encoding: "utf8",
|
|
151
|
+
mode: 384
|
|
152
|
+
});
|
|
153
|
+
return {
|
|
154
|
+
privateKeyPem: keyPair.privateKey.trim(),
|
|
155
|
+
created: true
|
|
156
|
+
};
|
|
33
157
|
}
|
|
34
|
-
|
|
35
|
-
|
|
158
|
+
|
|
159
|
+
// src/lib/doctor.ts
|
|
160
|
+
function getAgentIdFromRegistration(registration) {
|
|
161
|
+
const entryCount = registration?.transparency?.entries.length ?? 0;
|
|
162
|
+
if (!registration?.transparency || entryCount === 0) {
|
|
163
|
+
return void 0;
|
|
164
|
+
}
|
|
165
|
+
return registration.transparency.entries[entryCount - 1]?.agentId;
|
|
166
|
+
}
|
|
167
|
+
function toDetailList(vaults) {
|
|
168
|
+
if (vaults.length === 0) {
|
|
169
|
+
return "0 visible vaults.";
|
|
170
|
+
}
|
|
171
|
+
const visibleNames = vaults.slice(0, 5).map((vault) => vault.name);
|
|
172
|
+
const suffix = vaults.length > visibleNames.length ? ", ..." : "";
|
|
173
|
+
return `${vaults.length} visible vault${vaults.length === 1 ? "" : "s"}: ${visibleNames.join(", ")}${suffix}`;
|
|
174
|
+
}
|
|
175
|
+
function isWrappedKeyMissing(error) {
|
|
176
|
+
return error instanceof Error && (error.message.includes("[wrapped_key_not_found]") || error.message.includes("No wrapped key found for this agent and vault."));
|
|
177
|
+
}
|
|
178
|
+
function doctorHasFailures(report) {
|
|
179
|
+
return report.checks.some((check) => check.status === "fail");
|
|
180
|
+
}
|
|
181
|
+
async function runDoctorChecks(connection) {
|
|
182
|
+
const report = {
|
|
183
|
+
profileName: connection.profileName,
|
|
184
|
+
baseUrl: connection.baseUrl,
|
|
185
|
+
dev: connection.dev,
|
|
186
|
+
projectId: connection.projectId,
|
|
187
|
+
privateKeyPath: connection.privateKeyPath,
|
|
188
|
+
trustStorePath: connection.trustStorePath,
|
|
189
|
+
agentName: connection.profile.agentName,
|
|
190
|
+
checks: [],
|
|
191
|
+
vaults: []
|
|
192
|
+
};
|
|
193
|
+
report.checks.push({
|
|
194
|
+
id: "base-url",
|
|
195
|
+
label: "Base URL",
|
|
196
|
+
status: "pass",
|
|
197
|
+
detail: `${connection.baseUrl}${connection.dev ? " (dev mode)" : ""}`
|
|
198
|
+
});
|
|
199
|
+
if (!connection.apiKey) {
|
|
200
|
+
report.checks.push({
|
|
201
|
+
id: "api-key",
|
|
202
|
+
label: "API Key",
|
|
203
|
+
status: "fail",
|
|
204
|
+
detail: "No API key is configured for the active profile."
|
|
205
|
+
});
|
|
206
|
+
report.checks.push({
|
|
207
|
+
id: "project-filter",
|
|
208
|
+
label: "Project Filter",
|
|
209
|
+
status: "pass",
|
|
210
|
+
detail: connection.projectId ? `Filtering to project ${connection.projectId}.` : "No project filter is set."
|
|
211
|
+
});
|
|
212
|
+
return report;
|
|
213
|
+
}
|
|
214
|
+
report.checks.push({
|
|
215
|
+
id: "api-key",
|
|
216
|
+
label: "API Key",
|
|
217
|
+
status: "pass",
|
|
218
|
+
detail: `Loaded from ${connection.apiKeySource}.`
|
|
219
|
+
});
|
|
220
|
+
report.checks.push({
|
|
221
|
+
id: "project-filter",
|
|
222
|
+
label: "Project Filter",
|
|
223
|
+
status: "pass",
|
|
224
|
+
detail: connection.projectId ? `Filtering vault reads to project ${connection.projectId}.` : "No project filter is set."
|
|
225
|
+
});
|
|
226
|
+
const client = new CliClient(connection.apiKey, connection.baseUrl);
|
|
227
|
+
let registration;
|
|
228
|
+
if (!connection.privateKeyPath) {
|
|
229
|
+
report.checks.push({
|
|
230
|
+
id: "private-key",
|
|
231
|
+
label: "Private Key",
|
|
232
|
+
status: "fail",
|
|
233
|
+
detail: "No local private-key path is configured, so zero-trust checks cannot run."
|
|
234
|
+
});
|
|
235
|
+
report.checks.push({
|
|
236
|
+
id: "public-key",
|
|
237
|
+
label: "Public Key Registration",
|
|
238
|
+
status: "skip",
|
|
239
|
+
detail: "Skipped because no local private key is configured."
|
|
240
|
+
});
|
|
241
|
+
report.checks.push({
|
|
242
|
+
id: "agent-identity",
|
|
243
|
+
label: "Agent Identity",
|
|
244
|
+
status: "skip",
|
|
245
|
+
detail: "Skipped because public-key registration could not run."
|
|
246
|
+
});
|
|
247
|
+
} else {
|
|
248
|
+
try {
|
|
249
|
+
const privateKeyPem = loadPrivateKey(connection.privateKeyPath);
|
|
250
|
+
const publicKeyPem = derivePublicKey(privateKeyPem);
|
|
251
|
+
registration = await client.registerAgentPublicKey({ publicKey: publicKeyPem });
|
|
252
|
+
report.agentId = getAgentIdFromRegistration(registration) ?? connection.profile.agentId;
|
|
253
|
+
report.registration = {
|
|
254
|
+
encryptionKeyId: registration.encryptionKeyId,
|
|
255
|
+
fingerprint: registration.fingerprint
|
|
256
|
+
};
|
|
257
|
+
report.checks.push({
|
|
258
|
+
id: "private-key",
|
|
259
|
+
label: "Private Key",
|
|
260
|
+
status: "pass",
|
|
261
|
+
detail: `Loaded ${connection.privateKeyPath}.`
|
|
262
|
+
});
|
|
263
|
+
report.checks.push({
|
|
264
|
+
id: "public-key",
|
|
265
|
+
label: "Public Key Registration",
|
|
266
|
+
status: "pass",
|
|
267
|
+
detail: `Registered encryption key ${registration.encryptionKeyId}.`
|
|
268
|
+
});
|
|
269
|
+
report.checks.push({
|
|
270
|
+
id: "agent-identity",
|
|
271
|
+
label: "Agent Identity",
|
|
272
|
+
status: report.agentId ? "pass" : "warn",
|
|
273
|
+
detail: report.agentId ? `Agent ${report.agentId}${report.agentName ? ` (${report.agentName})` : ""}.` : "The API returned no agent ID in the current registration proof."
|
|
274
|
+
});
|
|
275
|
+
} catch (error) {
|
|
276
|
+
report.checks.push({
|
|
277
|
+
id: "private-key",
|
|
278
|
+
label: "Private Key",
|
|
279
|
+
status: "pass",
|
|
280
|
+
detail: `Configured at ${connection.privateKeyPath}.`
|
|
281
|
+
});
|
|
282
|
+
report.checks.push({
|
|
283
|
+
id: "public-key",
|
|
284
|
+
label: "Public Key Registration",
|
|
285
|
+
status: "fail",
|
|
286
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
287
|
+
});
|
|
288
|
+
report.checks.push({
|
|
289
|
+
id: "agent-identity",
|
|
290
|
+
label: "Agent Identity",
|
|
291
|
+
status: "skip",
|
|
292
|
+
detail: "Skipped because public-key registration did not succeed."
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
let vaults = [];
|
|
297
|
+
try {
|
|
298
|
+
const response = await client.listVaults(connection.projectId);
|
|
299
|
+
vaults = response.vaults;
|
|
300
|
+
report.checks.push({
|
|
301
|
+
id: "visible-vaults",
|
|
302
|
+
label: "Visible Vaults",
|
|
303
|
+
status: "pass",
|
|
304
|
+
detail: toDetailList(vaults)
|
|
305
|
+
});
|
|
306
|
+
} catch (error) {
|
|
307
|
+
report.checks.push({
|
|
308
|
+
id: "visible-vaults",
|
|
309
|
+
label: "Visible Vaults",
|
|
310
|
+
status: "fail",
|
|
311
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
312
|
+
});
|
|
313
|
+
report.checks.push({
|
|
314
|
+
id: "wrapped-keys",
|
|
315
|
+
label: "Wrapped Keys",
|
|
316
|
+
status: "skip",
|
|
317
|
+
detail: "Skipped because visible vaults could not be listed."
|
|
318
|
+
});
|
|
319
|
+
report.checks.push({
|
|
320
|
+
id: "zero-trust",
|
|
321
|
+
label: "Trust & Decryption",
|
|
322
|
+
status: "skip",
|
|
323
|
+
detail: "Skipped because visible vaults could not be listed."
|
|
324
|
+
});
|
|
325
|
+
return report;
|
|
326
|
+
}
|
|
327
|
+
if (vaults.length === 0) {
|
|
328
|
+
report.checks.push({
|
|
329
|
+
id: "wrapped-keys",
|
|
330
|
+
label: "Wrapped Keys",
|
|
331
|
+
status: "skip",
|
|
332
|
+
detail: "No visible vaults means there were no wrapped keys to verify."
|
|
333
|
+
});
|
|
334
|
+
report.checks.push({
|
|
335
|
+
id: "zero-trust",
|
|
336
|
+
label: "Trust & Decryption",
|
|
337
|
+
status: "skip",
|
|
338
|
+
detail: "No visible vaults were returned, so no vault trust proof was exercised."
|
|
339
|
+
});
|
|
340
|
+
return report;
|
|
341
|
+
}
|
|
342
|
+
let missingWrappedKeys = 0;
|
|
343
|
+
let wrappedKeyErrors = 0;
|
|
344
|
+
for (const vault of vaults) {
|
|
345
|
+
try {
|
|
346
|
+
await client.getAgentWrappedKey(vault.id);
|
|
347
|
+
report.vaults.push({
|
|
348
|
+
id: vault.id,
|
|
349
|
+
name: vault.name,
|
|
350
|
+
itemCount: vault.itemCount,
|
|
351
|
+
wrappedKeyStatus: "present"
|
|
352
|
+
});
|
|
353
|
+
} catch (error) {
|
|
354
|
+
if (isWrappedKeyMissing(error)) {
|
|
355
|
+
missingWrappedKeys += 1;
|
|
356
|
+
report.vaults.push({
|
|
357
|
+
id: vault.id,
|
|
358
|
+
name: vault.name,
|
|
359
|
+
itemCount: vault.itemCount,
|
|
360
|
+
wrappedKeyStatus: "missing",
|
|
361
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
362
|
+
});
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
wrappedKeyErrors += 1;
|
|
366
|
+
report.vaults.push({
|
|
367
|
+
id: vault.id,
|
|
368
|
+
name: vault.name,
|
|
369
|
+
itemCount: vault.itemCount,
|
|
370
|
+
wrappedKeyStatus: "error",
|
|
371
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (wrappedKeyErrors > 0) {
|
|
376
|
+
report.checks.push({
|
|
377
|
+
id: "wrapped-keys",
|
|
378
|
+
label: "Wrapped Keys",
|
|
379
|
+
status: "fail",
|
|
380
|
+
detail: `${wrappedKeyErrors} vault read${wrappedKeyErrors === 1 ? "" : "s"} hit an unexpected wrapped-key error.`
|
|
381
|
+
});
|
|
382
|
+
} else if (missingWrappedKeys > 0) {
|
|
383
|
+
report.checks.push({
|
|
384
|
+
id: "wrapped-keys",
|
|
385
|
+
label: "Wrapped Keys",
|
|
386
|
+
status: "warn",
|
|
387
|
+
detail: `${missingWrappedKeys} of ${vaults.length} visible vault${vaults.length === 1 ? "" : "s"} do not have wrapped keys for this agent.`
|
|
388
|
+
});
|
|
389
|
+
} else {
|
|
390
|
+
report.checks.push({
|
|
391
|
+
id: "wrapped-keys",
|
|
392
|
+
label: "Wrapped Keys",
|
|
393
|
+
status: "pass",
|
|
394
|
+
detail: `Wrapped keys are present for all ${vaults.length} visible vault${vaults.length === 1 ? "" : "s"}.`
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
if (!connection.privateKeyPath) {
|
|
398
|
+
report.checks.push({
|
|
399
|
+
id: "zero-trust",
|
|
400
|
+
label: "Trust & Decryption",
|
|
401
|
+
status: "fail",
|
|
402
|
+
detail: "A local private key is required for end-to-end zero-trust verification."
|
|
403
|
+
});
|
|
404
|
+
return report;
|
|
405
|
+
}
|
|
406
|
+
try {
|
|
407
|
+
const r4 = await R4.create({
|
|
408
|
+
apiKey: connection.apiKey,
|
|
409
|
+
baseUrl: connection.baseUrl,
|
|
410
|
+
dev: connection.dev,
|
|
411
|
+
projectId: connection.projectId,
|
|
412
|
+
privateKeyPath: connection.privateKeyPath,
|
|
413
|
+
trustStorePath: connection.trustStorePath
|
|
414
|
+
});
|
|
415
|
+
report.envKeyCount = Object.keys(r4.env).length;
|
|
416
|
+
report.checks.push({
|
|
417
|
+
id: "zero-trust",
|
|
418
|
+
label: "Trust & Decryption",
|
|
419
|
+
status: "pass",
|
|
420
|
+
detail: `Verified trust/transparency checks and decrypted ${report.envKeyCount} env key${report.envKeyCount === 1 ? "" : "s"} locally.`
|
|
421
|
+
});
|
|
422
|
+
} catch (error) {
|
|
423
|
+
report.checks.push({
|
|
424
|
+
id: "zero-trust",
|
|
425
|
+
label: "Trust & Decryption",
|
|
426
|
+
status: "fail",
|
|
427
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
return report;
|
|
36
431
|
}
|
|
37
432
|
|
|
38
433
|
// src/lib/output.ts
|
|
@@ -75,13 +470,32 @@ function printError(message) {
|
|
|
75
470
|
}
|
|
76
471
|
|
|
77
472
|
// src/lib/errors.ts
|
|
473
|
+
function formatErrorMessage(message) {
|
|
474
|
+
if (message.includes("[wrapped_key_not_found]") || message.includes("No wrapped key found for this agent and vault.")) {
|
|
475
|
+
return `${message}
|
|
476
|
+
|
|
477
|
+
Remediation:
|
|
478
|
+
- Make sure the vault or vault item is shared with this agent, one of its security groups, or one of its projects.
|
|
479
|
+
- If you just created or rotated the local runtime key, re-run \`r4 agent init\` and then re-wrap access for the agent.
|
|
480
|
+
- Run \`r4 doctor\` to see which visible vaults are missing wrapped keys.`;
|
|
481
|
+
}
|
|
482
|
+
if (message.includes("failed to register the local agent public key")) {
|
|
483
|
+
return `${message}
|
|
484
|
+
|
|
485
|
+
Remediation:
|
|
486
|
+
- Use an AGENT-scoped API key.
|
|
487
|
+
- Point \`--private-key-path\` at the matching local PEM file, or run \`r4 agent init\` to generate and register one automatically.
|
|
488
|
+
- If you intentionally target a non-production environment, include \`--dev\` or set \`R4_DEV=1\`.`;
|
|
489
|
+
}
|
|
490
|
+
return message;
|
|
491
|
+
}
|
|
78
492
|
function withErrorHandler(fn) {
|
|
79
493
|
return async (...args) => {
|
|
80
494
|
try {
|
|
81
495
|
await fn(...args);
|
|
82
496
|
} catch (err) {
|
|
83
497
|
if (err instanceof Error) {
|
|
84
|
-
printError(err.message);
|
|
498
|
+
printError(formatErrorMessage(err.message));
|
|
85
499
|
} else {
|
|
86
500
|
printError("An unexpected error occurred");
|
|
87
501
|
}
|
|
@@ -90,6 +504,150 @@ function withErrorHandler(fn) {
|
|
|
90
504
|
};
|
|
91
505
|
}
|
|
92
506
|
|
|
507
|
+
// src/lib/resolve-auth.ts
|
|
508
|
+
import path3 from "node:path";
|
|
509
|
+
|
|
510
|
+
// src/lib/config.ts
|
|
511
|
+
import fs2 from "node:fs";
|
|
512
|
+
import os2 from "node:os";
|
|
513
|
+
import path2 from "node:path";
|
|
514
|
+
var CONFIG_DIR = path2.join(os2.homedir(), ".r4");
|
|
515
|
+
var CONFIG_PATH = path2.join(CONFIG_DIR, "config.json");
|
|
516
|
+
var DEFAULT_PROFILE_NAME = "default";
|
|
517
|
+
function emptyConfig() {
|
|
518
|
+
return {
|
|
519
|
+
version: 2,
|
|
520
|
+
currentProfile: DEFAULT_PROFILE_NAME,
|
|
521
|
+
profiles: {
|
|
522
|
+
[DEFAULT_PROFILE_NAME]: {}
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
function sanitizeProfileConfig(profile) {
|
|
527
|
+
if (!profile || typeof profile !== "object") {
|
|
528
|
+
return {};
|
|
529
|
+
}
|
|
530
|
+
const value = profile;
|
|
531
|
+
const nextProfile = {};
|
|
532
|
+
if (typeof value.apiKey === "string" && value.apiKey.trim()) {
|
|
533
|
+
nextProfile.apiKey = value.apiKey.trim();
|
|
534
|
+
}
|
|
535
|
+
if (typeof value.baseUrl === "string" && value.baseUrl.trim()) {
|
|
536
|
+
nextProfile.baseUrl = value.baseUrl.trim();
|
|
537
|
+
}
|
|
538
|
+
if (typeof value.dev === "boolean") {
|
|
539
|
+
nextProfile.dev = value.dev;
|
|
540
|
+
}
|
|
541
|
+
if (typeof value.projectId === "string" && value.projectId.trim()) {
|
|
542
|
+
nextProfile.projectId = value.projectId.trim();
|
|
543
|
+
}
|
|
544
|
+
if (typeof value.privateKeyPath === "string" && value.privateKeyPath.trim()) {
|
|
545
|
+
nextProfile.privateKeyPath = value.privateKeyPath.trim();
|
|
546
|
+
}
|
|
547
|
+
if (typeof value.trustStorePath === "string" && value.trustStorePath.trim()) {
|
|
548
|
+
nextProfile.trustStorePath = value.trustStorePath.trim();
|
|
549
|
+
}
|
|
550
|
+
if (typeof value.agentId === "string" && value.agentId.trim()) {
|
|
551
|
+
nextProfile.agentId = value.agentId.trim();
|
|
552
|
+
}
|
|
553
|
+
if (typeof value.agentName === "string" && value.agentName.trim()) {
|
|
554
|
+
nextProfile.agentName = value.agentName.trim();
|
|
555
|
+
}
|
|
556
|
+
return nextProfile;
|
|
557
|
+
}
|
|
558
|
+
function hasLegacyTopLevelProfileFields(config) {
|
|
559
|
+
return Boolean(
|
|
560
|
+
config.apiKey || config.baseUrl || config.dev !== void 0 || config.projectId || config.privateKeyPath || config.trustStorePath || config.agentId || config.agentName
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
function normalizeConfig(raw) {
|
|
564
|
+
if (!raw || typeof raw !== "object") {
|
|
565
|
+
return emptyConfig();
|
|
566
|
+
}
|
|
567
|
+
const parsed = raw;
|
|
568
|
+
const nextConfig = emptyConfig();
|
|
569
|
+
if (parsed.profiles && typeof parsed.profiles === "object") {
|
|
570
|
+
const entries = Object.entries(parsed.profiles).filter(([name]) => typeof name === "string" && name.trim().length > 0).map(([name, profile]) => [name.trim(), sanitizeProfileConfig(profile)]);
|
|
571
|
+
if (entries.length > 0) {
|
|
572
|
+
nextConfig.profiles = Object.fromEntries(entries);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
if (hasLegacyTopLevelProfileFields(parsed)) {
|
|
576
|
+
nextConfig.profiles[DEFAULT_PROFILE_NAME] = {
|
|
577
|
+
...nextConfig.profiles[DEFAULT_PROFILE_NAME],
|
|
578
|
+
...sanitizeProfileConfig(parsed)
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
const configuredCurrentProfile = typeof parsed.currentProfile === "string" && parsed.currentProfile.trim() ? parsed.currentProfile.trim() : DEFAULT_PROFILE_NAME;
|
|
582
|
+
nextConfig.currentProfile = nextConfig.profiles[configuredCurrentProfile] !== void 0 ? configuredCurrentProfile : Object.keys(nextConfig.profiles)[0] ?? DEFAULT_PROFILE_NAME;
|
|
583
|
+
if (Object.keys(nextConfig.profiles).length === 0) {
|
|
584
|
+
return emptyConfig();
|
|
585
|
+
}
|
|
586
|
+
return nextConfig;
|
|
587
|
+
}
|
|
588
|
+
function loadConfig() {
|
|
589
|
+
try {
|
|
590
|
+
const raw = fs2.readFileSync(CONFIG_PATH, "utf8");
|
|
591
|
+
return normalizeConfig(JSON.parse(raw));
|
|
592
|
+
} catch {
|
|
593
|
+
return emptyConfig();
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
function saveConfig(config) {
|
|
597
|
+
fs2.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
598
|
+
fs2.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
599
|
+
}
|
|
600
|
+
function clearConfig() {
|
|
601
|
+
try {
|
|
602
|
+
fs2.unlinkSync(CONFIG_PATH);
|
|
603
|
+
} catch {
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
function getConfigPath() {
|
|
607
|
+
return CONFIG_PATH;
|
|
608
|
+
}
|
|
609
|
+
function getProfileNames(config) {
|
|
610
|
+
return Object.keys(config.profiles).sort();
|
|
611
|
+
}
|
|
612
|
+
function getCurrentProfileName(config) {
|
|
613
|
+
return config.currentProfile;
|
|
614
|
+
}
|
|
615
|
+
function getSelectedProfileName(config, profileOverride, envProfile) {
|
|
616
|
+
const selectedProfile = profileOverride || envProfile || config.currentProfile;
|
|
617
|
+
return selectedProfile?.trim() || DEFAULT_PROFILE_NAME;
|
|
618
|
+
}
|
|
619
|
+
function getProfileConfig(config, profileName) {
|
|
620
|
+
return config.profiles[profileName] ?? {};
|
|
621
|
+
}
|
|
622
|
+
function updateProfile(config, profileName, profile) {
|
|
623
|
+
return {
|
|
624
|
+
...config,
|
|
625
|
+
profiles: {
|
|
626
|
+
...config.profiles,
|
|
627
|
+
[profileName]: profile
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
function setCurrentProfile(config, profileName) {
|
|
632
|
+
return {
|
|
633
|
+
...config,
|
|
634
|
+
currentProfile: profileName
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
function removeProfile(config, profileName) {
|
|
638
|
+
const remainingProfiles = Object.fromEntries(
|
|
639
|
+
Object.entries(config.profiles).filter(([name]) => name !== profileName)
|
|
640
|
+
);
|
|
641
|
+
if (Object.keys(remainingProfiles).length === 0) {
|
|
642
|
+
return emptyConfig();
|
|
643
|
+
}
|
|
644
|
+
return {
|
|
645
|
+
version: 2,
|
|
646
|
+
currentProfile: config.currentProfile === profileName ? Object.keys(remainingProfiles).sort()[0] ?? DEFAULT_PROFILE_NAME : config.currentProfile,
|
|
647
|
+
profiles: remainingProfiles
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
93
651
|
// src/lib/runtime-config.ts
|
|
94
652
|
var R4_DEFAULT_API_BASE_URL = "https://r4.dev";
|
|
95
653
|
var R4_DEV_API_BASE_URL = "https://dev.r4.dev";
|
|
@@ -126,28 +684,470 @@ function resolveRuntimeModeFromCli(opts, config) {
|
|
|
126
684
|
configDev: config.dev
|
|
127
685
|
});
|
|
128
686
|
}
|
|
129
|
-
function
|
|
130
|
-
const
|
|
687
|
+
function applyGlobalRuntimeOptionsToProfile(profile, opts) {
|
|
688
|
+
const nextProfile = { ...profile };
|
|
131
689
|
if (opts.baseUrl) {
|
|
132
|
-
|
|
133
|
-
delete
|
|
690
|
+
nextProfile.baseUrl = opts.baseUrl;
|
|
691
|
+
delete nextProfile.dev;
|
|
134
692
|
} else if (opts.dev === true) {
|
|
135
|
-
|
|
136
|
-
delete
|
|
693
|
+
nextProfile.dev = true;
|
|
694
|
+
delete nextProfile.baseUrl;
|
|
137
695
|
}
|
|
138
696
|
if (opts.projectId) {
|
|
139
|
-
|
|
697
|
+
nextProfile.projectId = opts.projectId;
|
|
140
698
|
}
|
|
141
699
|
if (opts.privateKeyPath) {
|
|
142
|
-
|
|
700
|
+
nextProfile.privateKeyPath = opts.privateKeyPath;
|
|
143
701
|
}
|
|
144
702
|
if (opts.trustStorePath) {
|
|
145
|
-
|
|
703
|
+
nextProfile.trustStorePath = opts.trustStorePath;
|
|
146
704
|
}
|
|
147
|
-
return
|
|
705
|
+
return nextProfile;
|
|
148
706
|
}
|
|
149
707
|
|
|
150
|
-
// src/
|
|
708
|
+
// src/lib/resolve-auth.ts
|
|
709
|
+
function buildApiKeyFromParts(accessKey, secretKey) {
|
|
710
|
+
if (!accessKey || !secretKey) {
|
|
711
|
+
return void 0;
|
|
712
|
+
}
|
|
713
|
+
return `${accessKey}.${secretKey}`;
|
|
714
|
+
}
|
|
715
|
+
function resolveApiKey(opts, profile, profileName) {
|
|
716
|
+
if (opts.apiKey) {
|
|
717
|
+
return {
|
|
718
|
+
value: opts.apiKey,
|
|
719
|
+
source: "--api-key flag",
|
|
720
|
+
incompleteSplitEnv: false
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
if (process.env.R4_API_KEY) {
|
|
724
|
+
return {
|
|
725
|
+
value: process.env.R4_API_KEY,
|
|
726
|
+
source: "R4_API_KEY env var",
|
|
727
|
+
incompleteSplitEnv: false
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
const splitEnvApiKey = buildApiKeyFromParts(
|
|
731
|
+
process.env.R4_ACCESS_KEY,
|
|
732
|
+
process.env.R4_SECRET_KEY
|
|
733
|
+
);
|
|
734
|
+
if (splitEnvApiKey) {
|
|
735
|
+
return {
|
|
736
|
+
value: splitEnvApiKey,
|
|
737
|
+
source: "R4_ACCESS_KEY + R4_SECRET_KEY env vars",
|
|
738
|
+
incompleteSplitEnv: false
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
if (profile.apiKey) {
|
|
742
|
+
return {
|
|
743
|
+
value: profile.apiKey,
|
|
744
|
+
source: `profile "${profileName}"`,
|
|
745
|
+
incompleteSplitEnv: false
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
return {
|
|
749
|
+
value: void 0,
|
|
750
|
+
source: "none",
|
|
751
|
+
incompleteSplitEnv: Boolean(process.env.R4_ACCESS_KEY || process.env.R4_SECRET_KEY)
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
function resolveProjectId(opts, profile) {
|
|
755
|
+
return opts.projectId || process.env.R4_PROJECT_ID || profile.projectId;
|
|
756
|
+
}
|
|
757
|
+
function resolveTrustStorePath(opts, profile, privateKeyPath) {
|
|
758
|
+
if (opts.trustStorePath) {
|
|
759
|
+
return { value: opts.trustStorePath, source: "--trust-store-path flag" };
|
|
760
|
+
}
|
|
761
|
+
if (process.env.R4_TRUST_STORE_PATH) {
|
|
762
|
+
return { value: process.env.R4_TRUST_STORE_PATH, source: "R4_TRUST_STORE_PATH env var" };
|
|
763
|
+
}
|
|
764
|
+
if (profile.trustStorePath) {
|
|
765
|
+
return { value: profile.trustStorePath, source: "profile config" };
|
|
766
|
+
}
|
|
767
|
+
if (privateKeyPath) {
|
|
768
|
+
return {
|
|
769
|
+
value: `${path3.resolve(privateKeyPath)}.trust.json`,
|
|
770
|
+
source: "default beside private key"
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
return { value: void 0, source: null };
|
|
774
|
+
}
|
|
775
|
+
function resolveConnection(opts, params) {
|
|
776
|
+
const configFile = loadConfig();
|
|
777
|
+
const profileName = getSelectedProfileName(
|
|
778
|
+
configFile,
|
|
779
|
+
opts.profile,
|
|
780
|
+
process.env.R4_PROFILE
|
|
781
|
+
);
|
|
782
|
+
const profile = getProfileConfig(configFile, profileName);
|
|
783
|
+
const runtimeMode = resolveRuntimeModeFromCli(opts, profile);
|
|
784
|
+
const apiKey = resolveApiKey(opts, profile, profileName);
|
|
785
|
+
const privateKey = resolvePrivateKeyPath({
|
|
786
|
+
cliPath: opts.privateKeyPath,
|
|
787
|
+
envPath: process.env.R4_PRIVATE_KEY_PATH,
|
|
788
|
+
profilePath: profile.privateKeyPath,
|
|
789
|
+
profileName
|
|
790
|
+
});
|
|
791
|
+
const trustStorePath = resolveTrustStorePath(opts, profile, privateKey.value);
|
|
792
|
+
if (params?.requireApiKey && !apiKey.value) {
|
|
793
|
+
throw new Error(
|
|
794
|
+
"No API key found. Provide one via:\n --api-key <key> CLI flag\n R4_API_KEY environment variable\n R4_ACCESS_KEY + R4_SECRET_KEY environment variables\n r4 auth login save it to a profile\n r4 agent init bootstrap the full first-run flow" + (apiKey.incompleteSplitEnv ? "\n\nBoth R4_ACCESS_KEY and R4_SECRET_KEY must be set together." : "")
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
if (params?.requirePrivateKey && !privateKey.value) {
|
|
798
|
+
throw new Error(
|
|
799
|
+
`No private key path found. Provide one via:
|
|
800
|
+
--private-key-path <path> CLI flag
|
|
801
|
+
R4_PRIVATE_KEY_PATH environment variable
|
|
802
|
+
profile config saved privateKeyPath
|
|
803
|
+
r4 agent init generate a key at ${getDefaultPrivateKeyPath(profileName)}`
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
return {
|
|
807
|
+
apiKey: apiKey.value,
|
|
808
|
+
apiKeySource: apiKey.source,
|
|
809
|
+
baseUrl: runtimeMode.baseUrl,
|
|
810
|
+
dev: runtimeMode.devMode,
|
|
811
|
+
projectId: resolveProjectId(opts, profile),
|
|
812
|
+
privateKeyPath: privateKey.value,
|
|
813
|
+
privateKeySource: privateKey.source,
|
|
814
|
+
trustStorePath: trustStorePath.value,
|
|
815
|
+
trustStoreSource: trustStorePath.source,
|
|
816
|
+
profileName,
|
|
817
|
+
profile,
|
|
818
|
+
configFile
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
function resolveAuth(opts) {
|
|
822
|
+
const connection = resolveConnection(opts, {
|
|
823
|
+
requireApiKey: true,
|
|
824
|
+
requirePrivateKey: true
|
|
825
|
+
});
|
|
826
|
+
return {
|
|
827
|
+
apiKey: connection.apiKey,
|
|
828
|
+
projectId: connection.projectId,
|
|
829
|
+
baseUrl: connection.baseUrl,
|
|
830
|
+
dev: connection.dev,
|
|
831
|
+
privateKeyPath: connection.privateKeyPath,
|
|
832
|
+
trustStorePath: connection.trustStorePath
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// src/commands/doctor.ts
|
|
837
|
+
function renderStatus(status) {
|
|
838
|
+
switch (status) {
|
|
839
|
+
case "pass":
|
|
840
|
+
return chalk2.green("PASS");
|
|
841
|
+
case "warn":
|
|
842
|
+
return chalk2.yellow("WARN");
|
|
843
|
+
case "fail":
|
|
844
|
+
return chalk2.red("FAIL");
|
|
845
|
+
default:
|
|
846
|
+
return chalk2.gray("SKIP");
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
function printDoctorReport(report) {
|
|
850
|
+
console.log();
|
|
851
|
+
console.log(chalk2.bold(" R4 Doctor"));
|
|
852
|
+
console.log();
|
|
853
|
+
printDetail(
|
|
854
|
+
[
|
|
855
|
+
["Profile", report.profileName],
|
|
856
|
+
["Base URL", report.baseUrl],
|
|
857
|
+
["Mode", report.dev ? "dev" : "prod"],
|
|
858
|
+
["Project ID", report.projectId || chalk2.dim("(not set)")],
|
|
859
|
+
["Private Key", report.privateKeyPath || chalk2.dim("(not set)")],
|
|
860
|
+
["Trust Store", report.trustStorePath || chalk2.dim("(not set)")],
|
|
861
|
+
["Agent ID", report.agentId || report.agentName || chalk2.dim("(unknown)")]
|
|
862
|
+
],
|
|
863
|
+
false
|
|
864
|
+
);
|
|
865
|
+
console.log();
|
|
866
|
+
printTable(
|
|
867
|
+
["Check", "Status", "Detail"],
|
|
868
|
+
report.checks.map((check) => [
|
|
869
|
+
check.label,
|
|
870
|
+
renderStatus(check.status),
|
|
871
|
+
check.detail
|
|
872
|
+
]),
|
|
873
|
+
false
|
|
874
|
+
);
|
|
875
|
+
if (report.vaults.length > 0) {
|
|
876
|
+
console.log();
|
|
877
|
+
console.log(chalk2.bold(" Vaults"));
|
|
878
|
+
console.log();
|
|
879
|
+
printTable(
|
|
880
|
+
["Vault", "Items", "Wrapped Key", "Detail"],
|
|
881
|
+
report.vaults.map((vault) => [
|
|
882
|
+
vault.name,
|
|
883
|
+
String(vault.itemCount),
|
|
884
|
+
vault.wrappedKeyStatus,
|
|
885
|
+
vault.detail ?? "-"
|
|
886
|
+
]),
|
|
887
|
+
false
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
console.log();
|
|
891
|
+
}
|
|
892
|
+
function doctorCommand(commandName = "doctor", description = "Verify CLI auth, runtime key registration, vault access, and zero-trust health") {
|
|
893
|
+
return new Command(commandName).description(description).action(
|
|
894
|
+
withErrorHandler(async (_opts, cmd) => {
|
|
895
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
896
|
+
const connection = resolveConnection(globalOpts);
|
|
897
|
+
const spinner = ora("Running CLI health checks...").start();
|
|
898
|
+
const report = await runDoctorChecks(connection);
|
|
899
|
+
spinner.stop();
|
|
900
|
+
if (globalOpts.json) {
|
|
901
|
+
console.log(JSON.stringify(report, null, 2));
|
|
902
|
+
if (doctorHasFailures(report)) {
|
|
903
|
+
process.exitCode = 1;
|
|
904
|
+
}
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
printDoctorReport(report);
|
|
908
|
+
if (doctorHasFailures(report)) {
|
|
909
|
+
process.exitCode = 1;
|
|
910
|
+
}
|
|
911
|
+
})
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// src/lib/credentials-file.ts
|
|
916
|
+
import fs3 from "node:fs";
|
|
917
|
+
import path4 from "node:path";
|
|
918
|
+
function normalizeFieldName(fieldName) {
|
|
919
|
+
return fieldName.trim().toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
920
|
+
}
|
|
921
|
+
function stripWrappingQuotes(value) {
|
|
922
|
+
const trimmed = value.trim();
|
|
923
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
924
|
+
return trimmed.slice(1, -1).trim();
|
|
925
|
+
}
|
|
926
|
+
return trimmed;
|
|
927
|
+
}
|
|
928
|
+
function parseBooleanLike(value) {
|
|
929
|
+
const normalized = value.trim().toLowerCase();
|
|
930
|
+
if (["1", "true", "yes", "on", "dev", "development"].includes(normalized)) {
|
|
931
|
+
return true;
|
|
932
|
+
}
|
|
933
|
+
if (["0", "false", "no", "off", "prod", "production"].includes(normalized)) {
|
|
934
|
+
return false;
|
|
935
|
+
}
|
|
936
|
+
return void 0;
|
|
937
|
+
}
|
|
938
|
+
function applyField(bundle, fieldName, rawValue) {
|
|
939
|
+
const value = stripWrappingQuotes(rawValue);
|
|
940
|
+
if (!value) {
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
const normalized = normalizeFieldName(fieldName);
|
|
944
|
+
if (["apikey", "r4apikey", "machineapikey"].includes(normalized)) {
|
|
945
|
+
bundle.apiKey = value;
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
if (["accesskey", "r4accesskey", "accesskeyid", "access", "keyid"].includes(normalized)) {
|
|
949
|
+
bundle.accessKey = value;
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
if (["secretkey", "r4secretkey", "secret", "accesssecret", "secretaccesskey"].includes(normalized)) {
|
|
953
|
+
bundle.secretKey = value;
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
if (["baseurl", "r4baseurl", "apiurl", "apibaseurl", "targetbaseurl", "url"].includes(normalized)) {
|
|
957
|
+
bundle.baseUrl = value;
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
if (["environment", "env", "targetenvironment"].includes(normalized)) {
|
|
961
|
+
const dev = parseBooleanLike(value);
|
|
962
|
+
if (dev !== void 0) {
|
|
963
|
+
bundle.dev = dev;
|
|
964
|
+
}
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
if (["projectid", "r4projectid", "project", "projectscope", "scopeprojectid"].includes(normalized)) {
|
|
968
|
+
bundle.projectId = value;
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
if (["agentid", "r4agentid", "machineid"].includes(normalized)) {
|
|
972
|
+
bundle.agentId = value;
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
if (["agentname", "r4agentname", "machinename"].includes(normalized)) {
|
|
976
|
+
bundle.agentName = value;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
function finalizeBundle(bundle) {
|
|
980
|
+
if (!bundle.apiKey && bundle.accessKey && bundle.secretKey) {
|
|
981
|
+
bundle.apiKey = `${bundle.accessKey}.${bundle.secretKey}`;
|
|
982
|
+
}
|
|
983
|
+
return bundle;
|
|
984
|
+
}
|
|
985
|
+
function parseJsonContent(content) {
|
|
986
|
+
const parsed = JSON.parse(content);
|
|
987
|
+
const bundle = {};
|
|
988
|
+
const sources = [parsed];
|
|
989
|
+
for (const key of ["credentials", "profile"]) {
|
|
990
|
+
const nested = parsed[key];
|
|
991
|
+
if (nested && typeof nested === "object") {
|
|
992
|
+
sources.push(nested);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
for (const source of sources) {
|
|
996
|
+
for (const [key, value] of Object.entries(source)) {
|
|
997
|
+
if (typeof value === "string") {
|
|
998
|
+
applyField(bundle, key, value);
|
|
999
|
+
} else if (typeof value === "boolean") {
|
|
1000
|
+
applyField(bundle, key, String(value));
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
return finalizeBundle(bundle);
|
|
1005
|
+
}
|
|
1006
|
+
function parseEnvContent(content) {
|
|
1007
|
+
const bundle = {};
|
|
1008
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
1009
|
+
const line = rawLine.trim();
|
|
1010
|
+
if (!line || line.startsWith("#")) {
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
1013
|
+
const separatorIndex = line.indexOf("=");
|
|
1014
|
+
if (separatorIndex === -1) {
|
|
1015
|
+
continue;
|
|
1016
|
+
}
|
|
1017
|
+
const key = line.slice(0, separatorIndex).trim();
|
|
1018
|
+
const value = line.slice(separatorIndex + 1).trim();
|
|
1019
|
+
applyField(bundle, key, value);
|
|
1020
|
+
}
|
|
1021
|
+
return finalizeBundle(bundle);
|
|
1022
|
+
}
|
|
1023
|
+
function splitCsvLine(line) {
|
|
1024
|
+
const cells = [];
|
|
1025
|
+
let current = "";
|
|
1026
|
+
let inQuotes = false;
|
|
1027
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
1028
|
+
const character = line[index];
|
|
1029
|
+
if (character === '"') {
|
|
1030
|
+
const nextCharacter = line[index + 1];
|
|
1031
|
+
if (inQuotes && nextCharacter === '"') {
|
|
1032
|
+
current += '"';
|
|
1033
|
+
index += 1;
|
|
1034
|
+
} else {
|
|
1035
|
+
inQuotes = !inQuotes;
|
|
1036
|
+
}
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
if (character === "," && !inQuotes) {
|
|
1040
|
+
cells.push(current.trim());
|
|
1041
|
+
current = "";
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1044
|
+
current += character;
|
|
1045
|
+
}
|
|
1046
|
+
cells.push(current.trim());
|
|
1047
|
+
return cells;
|
|
1048
|
+
}
|
|
1049
|
+
function parseHeaderlessCsvRow(values) {
|
|
1050
|
+
const bundle = {};
|
|
1051
|
+
if (values.length >= 1) {
|
|
1052
|
+
bundle.accessKey = stripWrappingQuotes(values[0] ?? "");
|
|
1053
|
+
}
|
|
1054
|
+
if (values.length >= 2) {
|
|
1055
|
+
bundle.secretKey = stripWrappingQuotes(values[1] ?? "");
|
|
1056
|
+
}
|
|
1057
|
+
if (values.length >= 3) {
|
|
1058
|
+
const thirdValue = stripWrappingQuotes(values[2] ?? "");
|
|
1059
|
+
if (thirdValue.startsWith("http://") || thirdValue.startsWith("https://")) {
|
|
1060
|
+
bundle.baseUrl = thirdValue;
|
|
1061
|
+
} else if (thirdValue) {
|
|
1062
|
+
bundle.projectId = thirdValue;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
if (values.length >= 4) {
|
|
1066
|
+
const fourthValue = stripWrappingQuotes(values[3] ?? "");
|
|
1067
|
+
if (!bundle.projectId) {
|
|
1068
|
+
bundle.projectId = fourthValue;
|
|
1069
|
+
} else if (!bundle.baseUrl) {
|
|
1070
|
+
bundle.baseUrl = fourthValue;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
return finalizeBundle(bundle);
|
|
1074
|
+
}
|
|
1075
|
+
function parseCsvContent(content) {
|
|
1076
|
+
const lines = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
1077
|
+
if (lines.length === 0) {
|
|
1078
|
+
return {};
|
|
1079
|
+
}
|
|
1080
|
+
const firstRow = splitCsvLine(lines[0] ?? "");
|
|
1081
|
+
const firstHeaders = firstRow.map(normalizeFieldName);
|
|
1082
|
+
const looksLikeHeader = firstHeaders.some(
|
|
1083
|
+
(header) => [
|
|
1084
|
+
"apikey",
|
|
1085
|
+
"accesskey",
|
|
1086
|
+
"secretkey",
|
|
1087
|
+
"baseurl",
|
|
1088
|
+
"environment",
|
|
1089
|
+
"projectid",
|
|
1090
|
+
"agentid",
|
|
1091
|
+
"agentname"
|
|
1092
|
+
].includes(header)
|
|
1093
|
+
);
|
|
1094
|
+
if (!looksLikeHeader) {
|
|
1095
|
+
return parseHeaderlessCsvRow(firstRow);
|
|
1096
|
+
}
|
|
1097
|
+
const dataRow = lines.slice(1).find((line) => line.trim().length > 0);
|
|
1098
|
+
if (!dataRow) {
|
|
1099
|
+
return {};
|
|
1100
|
+
}
|
|
1101
|
+
const values = splitCsvLine(dataRow);
|
|
1102
|
+
const bundle = {};
|
|
1103
|
+
for (const [index, header] of firstRow.entries()) {
|
|
1104
|
+
applyField(bundle, header, values[index] ?? "");
|
|
1105
|
+
}
|
|
1106
|
+
return finalizeBundle(bundle);
|
|
1107
|
+
}
|
|
1108
|
+
function parsePlainTextContent(content) {
|
|
1109
|
+
const trimmed = content.trim();
|
|
1110
|
+
if (!trimmed) {
|
|
1111
|
+
return {};
|
|
1112
|
+
}
|
|
1113
|
+
if (trimmed.includes(".") && !/\s/.test(trimmed)) {
|
|
1114
|
+
return { apiKey: trimmed };
|
|
1115
|
+
}
|
|
1116
|
+
const commaValues = splitCsvLine(trimmed);
|
|
1117
|
+
if (commaValues.length >= 2) {
|
|
1118
|
+
return parseHeaderlessCsvRow(commaValues);
|
|
1119
|
+
}
|
|
1120
|
+
return {};
|
|
1121
|
+
}
|
|
1122
|
+
function parseCredentialsFile(credentialsFilePath) {
|
|
1123
|
+
const resolvedPath = path4.resolve(credentialsFilePath);
|
|
1124
|
+
const content = fs3.readFileSync(resolvedPath, "utf8");
|
|
1125
|
+
const extension = path4.extname(resolvedPath).toLowerCase();
|
|
1126
|
+
if (extension === ".json") {
|
|
1127
|
+
return parseJsonContent(content);
|
|
1128
|
+
}
|
|
1129
|
+
if (extension === ".env") {
|
|
1130
|
+
return parseEnvContent(content);
|
|
1131
|
+
}
|
|
1132
|
+
if (extension === ".csv") {
|
|
1133
|
+
return parseCsvContent(content);
|
|
1134
|
+
}
|
|
1135
|
+
if (content.includes("=") && !content.trim().startsWith("{")) {
|
|
1136
|
+
return parseEnvContent(content);
|
|
1137
|
+
}
|
|
1138
|
+
if (content.includes(",") || content.includes("\n")) {
|
|
1139
|
+
const csvBundle = parseCsvContent(content);
|
|
1140
|
+
if (csvBundle.apiKey || csvBundle.accessKey || csvBundle.secretKey) {
|
|
1141
|
+
return csvBundle;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
if (content.trim().startsWith("{")) {
|
|
1145
|
+
return parseJsonContent(content);
|
|
1146
|
+
}
|
|
1147
|
+
return parsePlainTextContent(content);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// src/commands/agent/init.ts
|
|
151
1151
|
function prompt(question) {
|
|
152
1152
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
153
1153
|
return new Promise((resolve) => {
|
|
@@ -157,82 +1157,309 @@ function prompt(question) {
|
|
|
157
1157
|
});
|
|
158
1158
|
});
|
|
159
1159
|
}
|
|
160
|
-
function
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
1160
|
+
function getAgentId(bundle) {
|
|
1161
|
+
const entryCount = bundle.transparency?.entries.length ?? 0;
|
|
1162
|
+
if (!bundle.transparency || entryCount === 0) {
|
|
1163
|
+
return void 0;
|
|
1164
|
+
}
|
|
1165
|
+
return bundle.transparency.entries[entryCount - 1]?.agentId;
|
|
1166
|
+
}
|
|
1167
|
+
function applyCredentialBundleToProfile(profile, bundle, globalOpts, privateKeyPath) {
|
|
1168
|
+
let nextProfile = { ...profile };
|
|
1169
|
+
if (!globalOpts.baseUrl && globalOpts.dev !== true) {
|
|
1170
|
+
if (bundle.baseUrl) {
|
|
1171
|
+
nextProfile.baseUrl = bundle.baseUrl;
|
|
1172
|
+
delete nextProfile.dev;
|
|
1173
|
+
} else if (bundle.dev === true) {
|
|
1174
|
+
nextProfile.dev = true;
|
|
1175
|
+
delete nextProfile.baseUrl;
|
|
1176
|
+
} else if (bundle.dev === false) {
|
|
1177
|
+
delete nextProfile.dev;
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
if (!globalOpts.projectId && bundle.projectId) {
|
|
1181
|
+
nextProfile.projectId = bundle.projectId;
|
|
1182
|
+
}
|
|
1183
|
+
if (bundle.agentId) {
|
|
1184
|
+
nextProfile.agentId = bundle.agentId;
|
|
1185
|
+
}
|
|
1186
|
+
if (bundle.agentName) {
|
|
1187
|
+
nextProfile.agentName = bundle.agentName;
|
|
1188
|
+
}
|
|
1189
|
+
nextProfile.privateKeyPath = privateKeyPath;
|
|
1190
|
+
nextProfile = applyGlobalRuntimeOptionsToProfile(nextProfile, globalOpts);
|
|
1191
|
+
return nextProfile;
|
|
1192
|
+
}
|
|
1193
|
+
function initCommand() {
|
|
1194
|
+
return new Command2("init").description("Bootstrap local agent auth, key generation, public-key registration, and health checks").option(
|
|
1195
|
+
"--credentials-file <path>",
|
|
1196
|
+
"Read credentials from a CSV, .env, JSON, or plain-text handoff file"
|
|
1197
|
+
).action(
|
|
1198
|
+
withErrorHandler(
|
|
1199
|
+
async (opts, cmd) => {
|
|
1200
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1201
|
+
const config = loadConfig();
|
|
1202
|
+
const profileName = getSelectedProfileName(
|
|
1203
|
+
config,
|
|
1204
|
+
globalOpts.profile,
|
|
1205
|
+
process.env.R4_PROFILE
|
|
1206
|
+
);
|
|
1207
|
+
const existingProfile = getProfileConfig(config, profileName);
|
|
1208
|
+
const bundle = opts.credentialsFile ? parseCredentialsFile(opts.credentialsFile) : {};
|
|
1209
|
+
let apiKey = globalOpts.apiKey || process.env.R4_API_KEY || buildApiKeyFromParts(
|
|
1210
|
+
process.env.R4_ACCESS_KEY,
|
|
1211
|
+
process.env.R4_SECRET_KEY
|
|
1212
|
+
) || bundle.apiKey || existingProfile.apiKey;
|
|
1213
|
+
if (!apiKey) {
|
|
1214
|
+
apiKey = await prompt("Enter your R4 API key: ");
|
|
1215
|
+
}
|
|
1216
|
+
if (!apiKey) {
|
|
1217
|
+
throw new Error("No API key provided.");
|
|
1218
|
+
}
|
|
1219
|
+
if (!apiKey.includes(".")) {
|
|
1220
|
+
warn("API key format is usually {accessKey}.{secret}");
|
|
1221
|
+
}
|
|
1222
|
+
if (globalOpts.baseUrl && globalOpts.dev) {
|
|
1223
|
+
warn("--base-url takes precedence over --dev and will be saved as the active runtime URL.");
|
|
1224
|
+
}
|
|
1225
|
+
const privateKeyPath = resolvePrivateKeyPath({
|
|
1226
|
+
cliPath: globalOpts.privateKeyPath,
|
|
1227
|
+
envPath: process.env.R4_PRIVATE_KEY_PATH,
|
|
1228
|
+
profilePath: existingProfile.privateKeyPath,
|
|
1229
|
+
profileName,
|
|
1230
|
+
allowDefaultIfMissing: true
|
|
1231
|
+
}).value;
|
|
1232
|
+
if (!privateKeyPath) {
|
|
1233
|
+
throw new Error("Unable to resolve a local private-key path for agent bootstrap.");
|
|
1234
|
+
}
|
|
1235
|
+
const keySpinner = ora2("Ensuring a local RSA private key exists...").start();
|
|
1236
|
+
const { privateKeyPem, created } = ensurePrivateKey(privateKeyPath);
|
|
1237
|
+
keySpinner.stop();
|
|
1238
|
+
const publicKeyPem = derivePublicKey(privateKeyPem);
|
|
1239
|
+
const nextProfile = applyCredentialBundleToProfile(
|
|
1240
|
+
existingProfile,
|
|
1241
|
+
bundle,
|
|
1242
|
+
globalOpts,
|
|
1243
|
+
privateKeyPath
|
|
1244
|
+
);
|
|
1245
|
+
const runtimeMode = resolveRuntimeModeFromCli(globalOpts, nextProfile);
|
|
1246
|
+
const client = new CliClient(apiKey, runtimeMode.baseUrl);
|
|
1247
|
+
const registerSpinner = ora2("Registering the local public key...").start();
|
|
1248
|
+
const registration = await client.registerAgentPublicKey({
|
|
1249
|
+
publicKey: publicKeyPem
|
|
1250
|
+
});
|
|
1251
|
+
registerSpinner.stop();
|
|
1252
|
+
const savedProfile = {
|
|
1253
|
+
...nextProfile,
|
|
1254
|
+
apiKey,
|
|
1255
|
+
agentId: getAgentId(registration) ?? nextProfile.agentId
|
|
1256
|
+
};
|
|
1257
|
+
const savedConfig = setCurrentProfile(
|
|
1258
|
+
updateProfile(config, profileName, savedProfile),
|
|
1259
|
+
profileName
|
|
1260
|
+
);
|
|
1261
|
+
saveConfig(savedConfig);
|
|
1262
|
+
success(
|
|
1263
|
+
created ? `Generated a private key at ${privateKeyPath}` : `Reused the existing private key at ${privateKeyPath}`
|
|
1264
|
+
);
|
|
1265
|
+
success(`Saved profile "${profileName}" to the local CLI config`);
|
|
1266
|
+
const doctorConnection = resolveConnection(
|
|
1267
|
+
{ ...globalOpts, profile: profileName },
|
|
1268
|
+
{ requireApiKey: true, requirePrivateKey: true }
|
|
1269
|
+
);
|
|
1270
|
+
const doctorSpinner = ora2("Running health checks...").start();
|
|
1271
|
+
const report = await runDoctorChecks(doctorConnection);
|
|
1272
|
+
doctorSpinner.stop();
|
|
1273
|
+
printDoctorReport(report);
|
|
1274
|
+
if (doctorHasFailures(report)) {
|
|
1275
|
+
throw new Error(
|
|
1276
|
+
"Agent init saved your local profile, but the health check still has failures."
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1279
|
+
success(
|
|
1280
|
+
`Agent profile "${profileName}" is ready for ${runtimeMode.devMode ? "dev" : "prod"} use.`
|
|
1281
|
+
);
|
|
173
1282
|
}
|
|
174
|
-
|
|
175
|
-
|
|
1283
|
+
)
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// src/commands/agent/index.ts
|
|
1288
|
+
function registerAgentCommands(program2) {
|
|
1289
|
+
const agent = program2.command("agent").description("Bootstrap and manage local agent runtime setup");
|
|
1290
|
+
agent.addCommand(initCommand());
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// src/commands/auth/diagnose.ts
|
|
1294
|
+
function diagnoseCommand() {
|
|
1295
|
+
return doctorCommand(
|
|
1296
|
+
"diagnose",
|
|
1297
|
+
"Verify auth, public-key registration, vault access, and zero-trust health"
|
|
1298
|
+
);
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// src/commands/auth/login.ts
|
|
1302
|
+
import { Command as Command3 } from "commander";
|
|
1303
|
+
import readline2 from "node:readline";
|
|
1304
|
+
function prompt2(question) {
|
|
1305
|
+
const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
|
|
1306
|
+
return new Promise((resolve) => {
|
|
1307
|
+
rl.question(question, (answer) => {
|
|
1308
|
+
rl.close();
|
|
1309
|
+
resolve(answer.trim());
|
|
1310
|
+
});
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
function applyCredentialBundleToProfile2(profile, bundle, globalOpts) {
|
|
1314
|
+
let nextProfile = { ...profile };
|
|
1315
|
+
if (!globalOpts.baseUrl && globalOpts.dev !== true) {
|
|
1316
|
+
if (bundle.baseUrl) {
|
|
1317
|
+
nextProfile.baseUrl = bundle.baseUrl;
|
|
1318
|
+
delete nextProfile.dev;
|
|
1319
|
+
} else if (bundle.dev === true) {
|
|
1320
|
+
nextProfile.dev = true;
|
|
1321
|
+
delete nextProfile.baseUrl;
|
|
1322
|
+
} else if (bundle.dev === false) {
|
|
1323
|
+
delete nextProfile.dev;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
if (!globalOpts.projectId && bundle.projectId) {
|
|
1327
|
+
nextProfile.projectId = bundle.projectId;
|
|
1328
|
+
}
|
|
1329
|
+
if (bundle.agentId) {
|
|
1330
|
+
nextProfile.agentId = bundle.agentId;
|
|
1331
|
+
}
|
|
1332
|
+
if (bundle.agentName) {
|
|
1333
|
+
nextProfile.agentName = bundle.agentName;
|
|
1334
|
+
}
|
|
1335
|
+
return applyGlobalRuntimeOptionsToProfile(nextProfile, globalOpts);
|
|
1336
|
+
}
|
|
1337
|
+
function loginCommand() {
|
|
1338
|
+
return new Command3("login").description("Save your API key and runtime settings to the active CLI profile").option(
|
|
1339
|
+
"--credentials-file <path>",
|
|
1340
|
+
"Read credentials from a CSV, .env, JSON, or plain-text handoff file"
|
|
1341
|
+
).addHelpText(
|
|
1342
|
+
"after",
|
|
1343
|
+
`
|
|
1344
|
+
First run:
|
|
1345
|
+
r4 agent init --credentials-file ./agent-creds.csv --dev
|
|
1346
|
+
|
|
1347
|
+
That bootstrap flow can read the credentials bundle, generate a local RSA key if
|
|
1348
|
+
needed, register the public key with the machine API, save the profile, and run
|
|
1349
|
+
\`r4 doctor\`.
|
|
1350
|
+
|
|
1351
|
+
Manual save-only flow:
|
|
1352
|
+
r4 auth login --profile loom-dev --credentials-file ./agent-creds.csv --dev
|
|
1353
|
+
r4 doctor --profile loom-dev
|
|
1354
|
+
`
|
|
1355
|
+
).action(
|
|
1356
|
+
withErrorHandler(
|
|
1357
|
+
async (opts, cmd) => {
|
|
1358
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1359
|
+
const config = loadConfig();
|
|
1360
|
+
const profileName = getSelectedProfileName(
|
|
1361
|
+
config,
|
|
1362
|
+
globalOpts.profile,
|
|
1363
|
+
process.env.R4_PROFILE
|
|
1364
|
+
);
|
|
1365
|
+
const existingProfile = getProfileConfig(config, profileName);
|
|
1366
|
+
const bundle = opts.credentialsFile ? parseCredentialsFile(opts.credentialsFile) : {};
|
|
1367
|
+
let apiKey = globalOpts.apiKey || process.env.R4_API_KEY || buildApiKeyFromParts(
|
|
1368
|
+
process.env.R4_ACCESS_KEY,
|
|
1369
|
+
process.env.R4_SECRET_KEY
|
|
1370
|
+
) || bundle.apiKey || existingProfile.apiKey;
|
|
1371
|
+
if (!apiKey) {
|
|
1372
|
+
apiKey = await prompt2("Enter your R4 API key: ");
|
|
1373
|
+
}
|
|
1374
|
+
if (!apiKey) {
|
|
1375
|
+
throw new Error("No API key provided.");
|
|
1376
|
+
}
|
|
1377
|
+
if (!apiKey.includes(".")) {
|
|
1378
|
+
warn("API key format is usually {accessKey}.{secret}");
|
|
1379
|
+
}
|
|
1380
|
+
if (globalOpts.baseUrl && globalOpts.dev) {
|
|
1381
|
+
warn("--base-url takes precedence over --dev and will be saved as the active runtime URL.");
|
|
1382
|
+
}
|
|
1383
|
+
const nextProfile = applyCredentialBundleToProfile2(
|
|
1384
|
+
existingProfile,
|
|
1385
|
+
bundle,
|
|
1386
|
+
globalOpts
|
|
1387
|
+
);
|
|
1388
|
+
const savedConfig = setCurrentProfile(
|
|
1389
|
+
updateProfile(config, profileName, {
|
|
1390
|
+
...nextProfile,
|
|
1391
|
+
apiKey
|
|
1392
|
+
}),
|
|
1393
|
+
profileName
|
|
1394
|
+
);
|
|
1395
|
+
saveConfig(savedConfig);
|
|
1396
|
+
success(`Saved profile "${profileName}" to ${getConfigPath()}`);
|
|
176
1397
|
}
|
|
177
|
-
|
|
178
|
-
config.apiKey = apiKey;
|
|
179
|
-
saveConfig(config);
|
|
180
|
-
success(`Runtime settings saved to ${getConfigPath()}`);
|
|
181
|
-
})
|
|
1398
|
+
)
|
|
182
1399
|
);
|
|
183
1400
|
}
|
|
184
1401
|
|
|
185
1402
|
// src/commands/auth/logout.ts
|
|
186
|
-
import { Command as
|
|
1403
|
+
import { Command as Command4 } from "commander";
|
|
187
1404
|
function logoutCommand() {
|
|
188
|
-
return new
|
|
189
|
-
withErrorHandler(async () => {
|
|
190
|
-
|
|
191
|
-
|
|
1405
|
+
return new Command4("logout").description("Remove saved credentials from the active profile").option("--all", "Remove every saved profile and delete the config file").action(
|
|
1406
|
+
withErrorHandler(async (opts, cmd) => {
|
|
1407
|
+
if (opts.all) {
|
|
1408
|
+
clearConfig();
|
|
1409
|
+
success(`Removed all saved profiles from ${getConfigPath()}`);
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1413
|
+
const config = loadConfig();
|
|
1414
|
+
const profileName = getSelectedProfileName(
|
|
1415
|
+
config,
|
|
1416
|
+
globalOpts.profile,
|
|
1417
|
+
process.env.R4_PROFILE
|
|
1418
|
+
);
|
|
1419
|
+
if (!config.profiles[profileName]) {
|
|
1420
|
+
throw new Error(`Profile "${profileName}" does not exist.`);
|
|
1421
|
+
}
|
|
1422
|
+
if (Object.keys(config.profiles).length === 1) {
|
|
1423
|
+
clearConfig();
|
|
1424
|
+
} else {
|
|
1425
|
+
const nextConfig = removeProfile(config, profileName);
|
|
1426
|
+
saveConfig(nextConfig);
|
|
1427
|
+
}
|
|
1428
|
+
success(`Removed profile "${profileName}" from ${getConfigPath()}`);
|
|
192
1429
|
})
|
|
193
1430
|
);
|
|
194
1431
|
}
|
|
195
1432
|
|
|
196
1433
|
// src/commands/auth/status.ts
|
|
197
|
-
import { Command as
|
|
198
|
-
import
|
|
1434
|
+
import { Command as Command5 } from "commander";
|
|
1435
|
+
import chalk3 from "chalk";
|
|
199
1436
|
function maskKey(key) {
|
|
200
|
-
if (key.length <= 8)
|
|
201
|
-
|
|
1437
|
+
if (key.length <= 8) {
|
|
1438
|
+
return key;
|
|
1439
|
+
}
|
|
1440
|
+
return `${key.substring(0, 8)}...`;
|
|
202
1441
|
}
|
|
203
1442
|
function statusCommand() {
|
|
204
|
-
return new
|
|
1443
|
+
return new Command5("status").description("Show current authentication and profile status").action(
|
|
205
1444
|
withErrorHandler(async (_opts, cmd) => {
|
|
206
1445
|
const globalOpts = cmd.optsWithGlobals();
|
|
207
|
-
const
|
|
208
|
-
const runtimeMode = resolveRuntimeModeFromCli(globalOpts, config);
|
|
209
|
-
let source;
|
|
210
|
-
let apiKey;
|
|
211
|
-
const privateKeyPath = globalOpts.privateKeyPath || process.env.R4_PRIVATE_KEY_PATH || config.privateKeyPath;
|
|
212
|
-
const trustStorePath = globalOpts.trustStorePath || process.env.R4_TRUST_STORE_PATH || config.trustStorePath;
|
|
213
|
-
if (globalOpts.apiKey) {
|
|
214
|
-
source = "--api-key flag";
|
|
215
|
-
apiKey = globalOpts.apiKey;
|
|
216
|
-
} else if (process.env.R4_API_KEY) {
|
|
217
|
-
source = "R4_API_KEY env var";
|
|
218
|
-
apiKey = process.env.R4_API_KEY;
|
|
219
|
-
} else if (config.apiKey) {
|
|
220
|
-
source = `config file (${getConfigPath()})`;
|
|
221
|
-
apiKey = config.apiKey;
|
|
222
|
-
} else {
|
|
223
|
-
source = "none";
|
|
224
|
-
}
|
|
1446
|
+
const connection = resolveConnection(globalOpts);
|
|
225
1447
|
if (globalOpts.json) {
|
|
226
1448
|
console.log(
|
|
227
1449
|
JSON.stringify(
|
|
228
1450
|
{
|
|
229
|
-
authenticated:
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
1451
|
+
authenticated: Boolean(connection.apiKey),
|
|
1452
|
+
profile: connection.profileName,
|
|
1453
|
+
source: connection.apiKeySource,
|
|
1454
|
+
apiKey: connection.apiKey ? maskKey(connection.apiKey) : null,
|
|
1455
|
+
agentId: connection.profile.agentId || null,
|
|
1456
|
+
agentName: connection.profile.agentName || null,
|
|
1457
|
+
baseUrl: connection.baseUrl,
|
|
1458
|
+
devMode: connection.dev,
|
|
1459
|
+
projectId: connection.projectId || null,
|
|
1460
|
+
privateKeyPath: connection.privateKeyPath || null,
|
|
1461
|
+
trustStorePath: connection.trustStorePath || null,
|
|
1462
|
+
configPath: getConfigPath()
|
|
236
1463
|
},
|
|
237
1464
|
null,
|
|
238
1465
|
2
|
|
@@ -241,58 +1468,60 @@ function statusCommand() {
|
|
|
241
1468
|
return;
|
|
242
1469
|
}
|
|
243
1470
|
console.log();
|
|
244
|
-
if (apiKey) {
|
|
245
|
-
console.log(
|
|
1471
|
+
if (!connection.apiKey) {
|
|
1472
|
+
console.log(chalk3.bold(" Not authenticated"));
|
|
246
1473
|
console.log();
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
["Source", source],
|
|
250
|
-
["API Key", maskKey(apiKey)],
|
|
251
|
-
["Mode", runtimeMode.devMode ? "dev" : "prod"],
|
|
252
|
-
["Base URL", runtimeMode.baseUrl],
|
|
253
|
-
["Private Key", privateKeyPath || chalk2.dim("(not set)")],
|
|
254
|
-
["Trust Store", trustStorePath || chalk2.dim("(default beside key file)")]
|
|
255
|
-
],
|
|
256
|
-
false
|
|
257
|
-
);
|
|
258
|
-
} else {
|
|
259
|
-
console.log(chalk2.bold(" Not authenticated"));
|
|
1474
|
+
console.log(` Active profile: ${chalk3.cyan(connection.profileName)}`);
|
|
1475
|
+
console.log(" Run " + chalk3.cyan("r4 auth login") + " or " + chalk3.cyan("r4 agent init") + " to save credentials.");
|
|
260
1476
|
console.log();
|
|
261
|
-
|
|
1477
|
+
return;
|
|
262
1478
|
}
|
|
1479
|
+
console.log(chalk3.bold(" Authenticated"));
|
|
1480
|
+
console.log();
|
|
1481
|
+
printDetail(
|
|
1482
|
+
[
|
|
1483
|
+
["Profile", connection.profileName],
|
|
1484
|
+
["Source", connection.apiKeySource],
|
|
1485
|
+
["API Key", maskKey(connection.apiKey)],
|
|
1486
|
+
["Agent", connection.profile.agentName || connection.profile.agentId || chalk3.dim("(unknown)")],
|
|
1487
|
+
["Mode", connection.dev ? "dev" : "prod"],
|
|
1488
|
+
["Base URL", connection.baseUrl],
|
|
1489
|
+
["Project ID", connection.projectId || chalk3.dim("(not set)")],
|
|
1490
|
+
["Private Key", connection.privateKeyPath || chalk3.dim("(not set)")],
|
|
1491
|
+
["Trust Store", connection.trustStorePath || chalk3.dim("(not set)")],
|
|
1492
|
+
["Config File", getConfigPath()]
|
|
1493
|
+
],
|
|
1494
|
+
false
|
|
1495
|
+
);
|
|
263
1496
|
console.log();
|
|
264
1497
|
})
|
|
265
1498
|
);
|
|
266
1499
|
}
|
|
267
1500
|
|
|
268
1501
|
// src/commands/auth/whoami.ts
|
|
269
|
-
import { Command as
|
|
270
|
-
import
|
|
271
|
-
function maskKey2(key) {
|
|
272
|
-
if (key.length <= 8) return key;
|
|
273
|
-
return key.substring(0, 8) + "...";
|
|
274
|
-
}
|
|
1502
|
+
import { Command as Command6 } from "commander";
|
|
1503
|
+
import chalk4 from "chalk";
|
|
275
1504
|
function whoamiCommand() {
|
|
276
|
-
return new
|
|
1505
|
+
return new Command6("whoami").description("Show the current profile, agent identity, and runtime target").action(
|
|
277
1506
|
withErrorHandler(async (_opts, cmd) => {
|
|
278
1507
|
const globalOpts = cmd.optsWithGlobals();
|
|
279
|
-
const
|
|
280
|
-
const
|
|
281
|
-
const apiKey = globalOpts.apiKey || process.env.R4_API_KEY || config.apiKey;
|
|
282
|
-
const projectId = globalOpts.projectId || process.env.R4_PROJECT_ID || config.projectId;
|
|
283
|
-
const privateKeyPath = globalOpts.privateKeyPath || process.env.R4_PRIVATE_KEY_PATH || config.privateKeyPath;
|
|
284
|
-
const trustStorePath = globalOpts.trustStorePath || process.env.R4_TRUST_STORE_PATH || config.trustStorePath;
|
|
1508
|
+
const connection = resolveConnection(globalOpts);
|
|
1509
|
+
const agentIdentity = connection.profile.agentName || connection.profile.agentId || null;
|
|
285
1510
|
if (globalOpts.json) {
|
|
286
1511
|
console.log(
|
|
287
1512
|
JSON.stringify(
|
|
288
1513
|
{
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
1514
|
+
profile: connection.profileName,
|
|
1515
|
+
authenticated: Boolean(connection.apiKey),
|
|
1516
|
+
agent: {
|
|
1517
|
+
id: connection.profile.agentId || null,
|
|
1518
|
+
name: connection.profile.agentName || null
|
|
1519
|
+
},
|
|
1520
|
+
baseUrl: connection.baseUrl,
|
|
1521
|
+
devMode: connection.dev,
|
|
1522
|
+
projectId: connection.projectId || null,
|
|
1523
|
+
privateKeyPath: connection.privateKeyPath || null,
|
|
1524
|
+
trustStorePath: connection.trustStorePath || null,
|
|
296
1525
|
configPath: getConfigPath()
|
|
297
1526
|
},
|
|
298
1527
|
null,
|
|
@@ -302,23 +1531,18 @@ function whoamiCommand() {
|
|
|
302
1531
|
return;
|
|
303
1532
|
}
|
|
304
1533
|
console.log();
|
|
305
|
-
|
|
306
|
-
console.log(chalk3.bold(" Not authenticated"));
|
|
307
|
-
console.log();
|
|
308
|
-
console.log(" Run " + chalk3.cyan("r4 auth login") + " to save your API key.");
|
|
309
|
-
console.log();
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
console.log(chalk3.bold(" Authenticated"));
|
|
1534
|
+
console.log(chalk4.bold(connection.apiKey ? " Current Identity" : " Current Profile"));
|
|
313
1535
|
console.log();
|
|
314
1536
|
printDetail(
|
|
315
1537
|
[
|
|
316
|
-
["
|
|
317
|
-
["
|
|
318
|
-
["
|
|
319
|
-
["Base URL",
|
|
320
|
-
["
|
|
321
|
-
["
|
|
1538
|
+
["Profile", connection.profileName],
|
|
1539
|
+
["Authenticated", connection.apiKey ? "yes" : "no"],
|
|
1540
|
+
["Agent", agentIdentity || chalk4.dim("(unknown)")],
|
|
1541
|
+
["Base URL", connection.baseUrl],
|
|
1542
|
+
["Mode", connection.dev ? "dev" : "prod"],
|
|
1543
|
+
["Project ID", connection.projectId || chalk4.dim("(not set)")],
|
|
1544
|
+
["Private Key", connection.privateKeyPath || chalk4.dim("(not set)")],
|
|
1545
|
+
["Trust Store", connection.trustStorePath || chalk4.dim("(not set)")],
|
|
322
1546
|
["Config File", getConfigPath()]
|
|
323
1547
|
],
|
|
324
1548
|
false
|
|
@@ -335,49 +1559,79 @@ function registerAuthCommands(program2) {
|
|
|
335
1559
|
auth.addCommand(logoutCommand());
|
|
336
1560
|
auth.addCommand(statusCommand());
|
|
337
1561
|
auth.addCommand(whoamiCommand());
|
|
1562
|
+
auth.addCommand(diagnoseCommand());
|
|
338
1563
|
}
|
|
339
1564
|
|
|
340
|
-
// src/commands/
|
|
341
|
-
import { Command as
|
|
342
|
-
|
|
343
|
-
|
|
1565
|
+
// src/commands/profile/list.ts
|
|
1566
|
+
import { Command as Command7 } from "commander";
|
|
1567
|
+
function listCommand() {
|
|
1568
|
+
return new Command7("list").description("List saved CLI profiles").action(
|
|
1569
|
+
withErrorHandler(async (_opts, cmd) => {
|
|
1570
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1571
|
+
const config = loadConfig();
|
|
1572
|
+
const currentProfile = getCurrentProfileName(config);
|
|
1573
|
+
const rows = getProfileNames(config).map((profileName) => {
|
|
1574
|
+
const profile = config.profiles[profileName] ?? {};
|
|
1575
|
+
return [
|
|
1576
|
+
profileName,
|
|
1577
|
+
profileName === currentProfile ? "yes" : "",
|
|
1578
|
+
profile.baseUrl || (profile.dev ? "https://dev.r4.dev" : "(default)"),
|
|
1579
|
+
profile.projectId || "-",
|
|
1580
|
+
profile.agentId || profile.agentName || "-"
|
|
1581
|
+
];
|
|
1582
|
+
});
|
|
1583
|
+
console.log();
|
|
1584
|
+
printTable(
|
|
1585
|
+
["Name", "Current", "Base URL", "Project ID", "Agent"],
|
|
1586
|
+
rows,
|
|
1587
|
+
!!globalOpts.json,
|
|
1588
|
+
rows.map(([name, current, baseUrl, projectId, agent]) => ({
|
|
1589
|
+
name,
|
|
1590
|
+
current: current === "yes",
|
|
1591
|
+
baseUrl,
|
|
1592
|
+
projectId: projectId === "-" ? null : projectId,
|
|
1593
|
+
agent: agent === "-" ? null : agent
|
|
1594
|
+
}))
|
|
1595
|
+
);
|
|
1596
|
+
console.log();
|
|
1597
|
+
})
|
|
1598
|
+
);
|
|
1599
|
+
}
|
|
344
1600
|
|
|
345
|
-
// src/
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
baseUrl: runtimeMode.baseUrl,
|
|
367
|
-
dev: runtimeMode.devMode,
|
|
368
|
-
privateKeyPath,
|
|
369
|
-
trustStorePath
|
|
370
|
-
};
|
|
1601
|
+
// src/commands/profile/use.ts
|
|
1602
|
+
import { Command as Command8 } from "commander";
|
|
1603
|
+
function useCommand() {
|
|
1604
|
+
return new Command8("use").description("Switch the active CLI profile").argument("<name>", "Profile name").action(
|
|
1605
|
+
withErrorHandler(async (profileName) => {
|
|
1606
|
+
const config = loadConfig();
|
|
1607
|
+
const profile = getProfileConfig(config, profileName);
|
|
1608
|
+
if (Object.keys(profile).length === 0 && !config.profiles[profileName]) {
|
|
1609
|
+
throw new Error(`Profile "${profileName}" does not exist.`);
|
|
1610
|
+
}
|
|
1611
|
+
saveConfig(setCurrentProfile(config, profileName));
|
|
1612
|
+
success(`Now using profile "${profileName}"`);
|
|
1613
|
+
})
|
|
1614
|
+
);
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
// src/commands/profile/index.ts
|
|
1618
|
+
function registerProfileCommands(program2) {
|
|
1619
|
+
const profile = program2.command("profile").description("Manage saved CLI profiles");
|
|
1620
|
+
profile.addCommand(listCommand());
|
|
1621
|
+
profile.addCommand(useCommand());
|
|
371
1622
|
}
|
|
372
1623
|
|
|
373
1624
|
// src/commands/vault/list.ts
|
|
374
|
-
|
|
375
|
-
|
|
1625
|
+
import { Command as Command9 } from "commander";
|
|
1626
|
+
import ora3 from "ora";
|
|
1627
|
+
import R42 from "@r4security/sdk";
|
|
1628
|
+
function listCommand2() {
|
|
1629
|
+
return new Command9("list").description("List all locally decrypted environment variables").action(
|
|
376
1630
|
withErrorHandler(async (_opts, cmd) => {
|
|
377
1631
|
const globalOpts = cmd.optsWithGlobals();
|
|
378
1632
|
const config = resolveAuth(globalOpts);
|
|
379
|
-
const spinner =
|
|
380
|
-
const r4 = await
|
|
1633
|
+
const spinner = ora3("Fetching environment variables...").start();
|
|
1634
|
+
const r4 = await R42.create(config);
|
|
381
1635
|
spinner.stop();
|
|
382
1636
|
const env = r4.env;
|
|
383
1637
|
const keys = Object.keys(env).sort();
|
|
@@ -397,39 +1651,12 @@ function listCommand() {
|
|
|
397
1651
|
);
|
|
398
1652
|
}
|
|
399
1653
|
|
|
400
|
-
// src/commands/vault/
|
|
401
|
-
import { Command as
|
|
402
|
-
import ora2 from "ora";
|
|
403
|
-
import R42 from "@r4security/sdk";
|
|
404
|
-
function getCommand() {
|
|
405
|
-
return new Command6("get").description("Get a specific locally decrypted environment variable value").argument("<key>", "Environment variable key (SCREAMING_SNAKE_CASE)").action(
|
|
406
|
-
withErrorHandler(
|
|
407
|
-
async (keyArg, _opts, cmd) => {
|
|
408
|
-
const globalOpts = cmd.optsWithGlobals();
|
|
409
|
-
const config = resolveAuth(globalOpts);
|
|
410
|
-
const spinner = ora2("Fetching environment variables...").start();
|
|
411
|
-
const r4 = await R42.create(config);
|
|
412
|
-
spinner.stop();
|
|
413
|
-
const env = r4.env;
|
|
414
|
-
const key = keyArg.toUpperCase();
|
|
415
|
-
const value = env[key];
|
|
416
|
-
if (value === void 0) {
|
|
417
|
-
const available = Object.keys(env).sort().join(", ") || "(none)";
|
|
418
|
-
throw new Error(`Key "${key}" not found. Available keys: ${available}`);
|
|
419
|
-
}
|
|
420
|
-
if (globalOpts.json) {
|
|
421
|
-
console.log(JSON.stringify({ key, value }, null, 2));
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
process.stdout.write(value);
|
|
425
|
-
}
|
|
426
|
-
)
|
|
427
|
-
);
|
|
428
|
-
}
|
|
1654
|
+
// src/commands/vault/list-items.ts
|
|
1655
|
+
import { Command as Command11 } from "commander";
|
|
429
1656
|
|
|
430
1657
|
// src/commands/vault/items.ts
|
|
431
|
-
import { Command as
|
|
432
|
-
import
|
|
1658
|
+
import { Command as Command10 } from "commander";
|
|
1659
|
+
import ora4 from "ora";
|
|
433
1660
|
import R43 from "@r4security/sdk";
|
|
434
1661
|
function deriveItems(env) {
|
|
435
1662
|
const keys = Object.keys(env).sort();
|
|
@@ -438,12 +1665,71 @@ function deriveItems(env) {
|
|
|
438
1665
|
fields: [{ name: key, key }]
|
|
439
1666
|
}));
|
|
440
1667
|
}
|
|
1668
|
+
async function loadVaultMetadataRows(globalOpts) {
|
|
1669
|
+
const connection = resolveConnection(globalOpts, { requireApiKey: true });
|
|
1670
|
+
const client = new CliClient(connection.apiKey, connection.baseUrl);
|
|
1671
|
+
const { vaults } = await client.listVaults(connection.projectId);
|
|
1672
|
+
const rows = [];
|
|
1673
|
+
for (const vault of vaults) {
|
|
1674
|
+
const response = await client.listVaultItems(vault.id);
|
|
1675
|
+
const groupNames = Object.fromEntries(
|
|
1676
|
+
response.vaultItemGroups.map((group) => [group.id, group.name])
|
|
1677
|
+
);
|
|
1678
|
+
for (const item of response.items) {
|
|
1679
|
+
rows.push({
|
|
1680
|
+
vaultId: vault.id,
|
|
1681
|
+
vaultName: vault.name,
|
|
1682
|
+
itemId: item.id,
|
|
1683
|
+
itemName: item.name,
|
|
1684
|
+
type: item.type,
|
|
1685
|
+
fieldCount: item.fieldCount,
|
|
1686
|
+
groupName: item.groupId ? groupNames[item.groupId] ?? item.groupId : null,
|
|
1687
|
+
websites: item.websites
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
return rows.sort((left, right) => {
|
|
1692
|
+
const vaultCompare = left.vaultName.localeCompare(right.vaultName);
|
|
1693
|
+
return vaultCompare !== 0 ? vaultCompare : left.itemName.localeCompare(right.itemName);
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
async function renderVaultMetadataRows(globalOpts) {
|
|
1697
|
+
const spinner = ora4("Fetching vault item metadata...").start();
|
|
1698
|
+
const rows = await loadVaultMetadataRows(globalOpts);
|
|
1699
|
+
spinner.stop();
|
|
1700
|
+
if (globalOpts.json) {
|
|
1701
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
if (rows.length === 0) {
|
|
1705
|
+
console.log("\n No vault items found.\n");
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1708
|
+
console.log();
|
|
1709
|
+
printTable(
|
|
1710
|
+
["Vault", "Item", "Type", "Fields", "Group", "Websites"],
|
|
1711
|
+
rows.map((row) => [
|
|
1712
|
+
row.vaultName,
|
|
1713
|
+
row.itemName,
|
|
1714
|
+
row.type || "-",
|
|
1715
|
+
String(row.fieldCount),
|
|
1716
|
+
row.groupName || "-",
|
|
1717
|
+
row.websites.join(", ") || "-"
|
|
1718
|
+
]),
|
|
1719
|
+
false
|
|
1720
|
+
);
|
|
1721
|
+
console.log();
|
|
1722
|
+
}
|
|
441
1723
|
function itemsCommand() {
|
|
442
|
-
return new
|
|
443
|
-
withErrorHandler(async (
|
|
1724
|
+
return new Command10("items").description("List vault items from the decrypted env map, or use --metadata-only for raw machine metadata").option("--metadata-only", "List item metadata without local decryption").action(
|
|
1725
|
+
withErrorHandler(async (opts, cmd) => {
|
|
444
1726
|
const globalOpts = cmd.optsWithGlobals();
|
|
1727
|
+
if (opts.metadataOnly) {
|
|
1728
|
+
await renderVaultMetadataRows(globalOpts);
|
|
1729
|
+
return;
|
|
1730
|
+
}
|
|
445
1731
|
const config = resolveAuth(globalOpts);
|
|
446
|
-
const spinner =
|
|
1732
|
+
const spinner = ora4("Fetching vault items...").start();
|
|
447
1733
|
const r4 = await R43.create(config);
|
|
448
1734
|
spinner.stop();
|
|
449
1735
|
const env = r4.env;
|
|
@@ -468,18 +1754,95 @@ function itemsCommand() {
|
|
|
468
1754
|
);
|
|
469
1755
|
}
|
|
470
1756
|
|
|
471
|
-
// src/commands/vault/
|
|
472
|
-
|
|
473
|
-
|
|
1757
|
+
// src/commands/vault/list-items.ts
|
|
1758
|
+
function listItemsCommand() {
|
|
1759
|
+
return new Command11("list-items").description("List vault item metadata only, without requiring local decryption").action(
|
|
1760
|
+
withErrorHandler(async (_opts, cmd) => {
|
|
1761
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1762
|
+
await renderVaultMetadataRows(globalOpts);
|
|
1763
|
+
})
|
|
1764
|
+
);
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
// src/commands/vault/list-vaults.ts
|
|
1768
|
+
import { Command as Command12 } from "commander";
|
|
1769
|
+
import ora5 from "ora";
|
|
1770
|
+
function listVaultsCommand() {
|
|
1771
|
+
return new Command12("list-vaults").description("List visible vaults without requiring local decryption").action(
|
|
1772
|
+
withErrorHandler(async (_opts, cmd) => {
|
|
1773
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1774
|
+
const connection = resolveConnection(globalOpts, { requireApiKey: true });
|
|
1775
|
+
const client = new CliClient(connection.apiKey, connection.baseUrl);
|
|
1776
|
+
const spinner = ora5("Fetching visible vaults...").start();
|
|
1777
|
+
const response = await client.listVaults(connection.projectId);
|
|
1778
|
+
spinner.stop();
|
|
1779
|
+
if (globalOpts.json) {
|
|
1780
|
+
console.log(JSON.stringify(response.vaults, null, 2));
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1783
|
+
if (response.vaults.length === 0) {
|
|
1784
|
+
console.log("\n No visible vaults found.\n");
|
|
1785
|
+
return;
|
|
1786
|
+
}
|
|
1787
|
+
console.log();
|
|
1788
|
+
printTable(
|
|
1789
|
+
["ID", "Name", "Classification", "Items", "Created At"],
|
|
1790
|
+
response.vaults.map((vault) => [
|
|
1791
|
+
vault.id,
|
|
1792
|
+
vault.name,
|
|
1793
|
+
vault.dataClassification || "-",
|
|
1794
|
+
String(vault.itemCount),
|
|
1795
|
+
vault.createdAt
|
|
1796
|
+
]),
|
|
1797
|
+
false
|
|
1798
|
+
);
|
|
1799
|
+
console.log();
|
|
1800
|
+
})
|
|
1801
|
+
);
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
// src/commands/vault/get.ts
|
|
1805
|
+
import { Command as Command13 } from "commander";
|
|
1806
|
+
import ora6 from "ora";
|
|
474
1807
|
import R44 from "@r4security/sdk";
|
|
1808
|
+
function getCommand() {
|
|
1809
|
+
return new Command13("get").description("Get a specific locally decrypted environment variable value").argument("<key>", "Environment variable key (SCREAMING_SNAKE_CASE)").action(
|
|
1810
|
+
withErrorHandler(
|
|
1811
|
+
async (keyArg, _opts, cmd) => {
|
|
1812
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1813
|
+
const config = resolveAuth(globalOpts);
|
|
1814
|
+
const spinner = ora6("Fetching environment variables...").start();
|
|
1815
|
+
const r4 = await R44.create(config);
|
|
1816
|
+
spinner.stop();
|
|
1817
|
+
const env = r4.env;
|
|
1818
|
+
const key = keyArg.toUpperCase();
|
|
1819
|
+
const value = env[key];
|
|
1820
|
+
if (value === void 0) {
|
|
1821
|
+
const available = Object.keys(env).sort().join(", ") || "(none)";
|
|
1822
|
+
throw new Error(`Key "${key}" not found. Available keys: ${available}`);
|
|
1823
|
+
}
|
|
1824
|
+
if (globalOpts.json) {
|
|
1825
|
+
console.log(JSON.stringify({ key, value }, null, 2));
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
process.stdout.write(value);
|
|
1829
|
+
}
|
|
1830
|
+
)
|
|
1831
|
+
);
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
// src/commands/vault/search.ts
|
|
1835
|
+
import { Command as Command14 } from "commander";
|
|
1836
|
+
import ora7 from "ora";
|
|
1837
|
+
import R45 from "@r4security/sdk";
|
|
475
1838
|
function searchCommand() {
|
|
476
|
-
return new
|
|
1839
|
+
return new Command14("search").description("Search vault items by name").argument("<query>", "Search query (case-insensitive match against key names)").action(
|
|
477
1840
|
withErrorHandler(
|
|
478
1841
|
async (query, _opts, cmd) => {
|
|
479
1842
|
const globalOpts = cmd.optsWithGlobals();
|
|
480
1843
|
const config = resolveAuth(globalOpts);
|
|
481
|
-
const spinner =
|
|
482
|
-
const r4 = await
|
|
1844
|
+
const spinner = ora7("Searching vault items...").start();
|
|
1845
|
+
const r4 = await R45.create(config);
|
|
483
1846
|
spinner.stop();
|
|
484
1847
|
const env = r4.env;
|
|
485
1848
|
const lowerQuery = query.toLowerCase();
|
|
@@ -507,68 +1870,24 @@ function searchCommand() {
|
|
|
507
1870
|
// src/commands/vault/index.ts
|
|
508
1871
|
function registerVaultCommands(program2) {
|
|
509
1872
|
const vault = program2.command("vault").description("Manage vault secrets");
|
|
510
|
-
vault.addCommand(
|
|
1873
|
+
vault.addCommand(listCommand2());
|
|
1874
|
+
vault.addCommand(listVaultsCommand());
|
|
1875
|
+
vault.addCommand(listItemsCommand());
|
|
511
1876
|
vault.addCommand(getCommand());
|
|
512
1877
|
vault.addCommand(itemsCommand());
|
|
513
1878
|
vault.addCommand(searchCommand());
|
|
514
1879
|
}
|
|
515
1880
|
|
|
516
1881
|
// src/commands/project/list.ts
|
|
517
|
-
import { Command as
|
|
518
|
-
import
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
var CliClient = class {
|
|
522
|
-
apiKey;
|
|
523
|
-
baseUrl;
|
|
524
|
-
constructor(apiKey, baseUrl) {
|
|
525
|
-
this.apiKey = apiKey;
|
|
526
|
-
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
527
|
-
}
|
|
528
|
-
/**
|
|
529
|
-
* Make an authenticated request to the machine API.
|
|
530
|
-
* Handles error responses consistently with the SDK pattern.
|
|
531
|
-
*/
|
|
532
|
-
async request(method, path2, body) {
|
|
533
|
-
const url = `${this.baseUrl}${path2}`;
|
|
534
|
-
const response = await fetch(url, {
|
|
535
|
-
method,
|
|
536
|
-
headers: {
|
|
537
|
-
"X-API-Key": this.apiKey,
|
|
538
|
-
"Content-Type": "application/json"
|
|
539
|
-
},
|
|
540
|
-
body: body ? JSON.stringify(body) : void 0
|
|
541
|
-
});
|
|
542
|
-
if (!response.ok) {
|
|
543
|
-
const errorBody = await response.json().catch(() => ({}));
|
|
544
|
-
const error = errorBody?.error;
|
|
545
|
-
const errorMessage = error?.message || `HTTP ${response.status}: ${response.statusText}`;
|
|
546
|
-
throw new Error(`R4 API Error: ${errorMessage}`);
|
|
547
|
-
}
|
|
548
|
-
return response.json();
|
|
549
|
-
}
|
|
550
|
-
/** List all projects. GET /api/v1/machine/project */
|
|
551
|
-
async listProjects() {
|
|
552
|
-
return this.request("GET", "/api/v1/machine/project");
|
|
553
|
-
}
|
|
554
|
-
/** Get project details. GET /api/v1/machine/project/:id */
|
|
555
|
-
async getProject(id) {
|
|
556
|
-
return this.request("GET", `/api/v1/machine/project/${id}`);
|
|
557
|
-
}
|
|
558
|
-
/** Create a new project. POST /api/v1/machine/project */
|
|
559
|
-
async createProject(data) {
|
|
560
|
-
return this.request("POST", "/api/v1/machine/project", data);
|
|
561
|
-
}
|
|
562
|
-
};
|
|
563
|
-
|
|
564
|
-
// src/commands/project/list.ts
|
|
565
|
-
function listCommand2() {
|
|
566
|
-
return new Command9("list").description("List all projects").action(
|
|
1882
|
+
import { Command as Command15 } from "commander";
|
|
1883
|
+
import ora8 from "ora";
|
|
1884
|
+
function listCommand3() {
|
|
1885
|
+
return new Command15("list").description("List all projects").action(
|
|
567
1886
|
withErrorHandler(async (_opts, cmd) => {
|
|
568
1887
|
const globalOpts = cmd.optsWithGlobals();
|
|
569
|
-
const
|
|
570
|
-
const client = new CliClient(
|
|
571
|
-
const spinner =
|
|
1888
|
+
const connection = resolveConnection(globalOpts, { requireApiKey: true });
|
|
1889
|
+
const client = new CliClient(connection.apiKey, connection.baseUrl);
|
|
1890
|
+
const spinner = ora8("Fetching projects...").start();
|
|
572
1891
|
const response = await client.listProjects();
|
|
573
1892
|
spinner.stop();
|
|
574
1893
|
const rows = response.projects.map((p) => [
|
|
@@ -592,16 +1911,16 @@ function listCommand2() {
|
|
|
592
1911
|
}
|
|
593
1912
|
|
|
594
1913
|
// src/commands/project/get.ts
|
|
595
|
-
import { Command as
|
|
596
|
-
import
|
|
597
|
-
import
|
|
1914
|
+
import { Command as Command16 } from "commander";
|
|
1915
|
+
import chalk5 from "chalk";
|
|
1916
|
+
import ora9 from "ora";
|
|
598
1917
|
function getCommand2() {
|
|
599
|
-
return new
|
|
1918
|
+
return new Command16("get").description("Get project details").argument("<id>", "Project ID").action(
|
|
600
1919
|
withErrorHandler(async (id, _opts, cmd) => {
|
|
601
1920
|
const globalOpts = cmd.optsWithGlobals();
|
|
602
|
-
const
|
|
603
|
-
const client = new CliClient(
|
|
604
|
-
const spinner =
|
|
1921
|
+
const connection = resolveConnection(globalOpts, { requireApiKey: true });
|
|
1922
|
+
const client = new CliClient(connection.apiKey, connection.baseUrl);
|
|
1923
|
+
const spinner = ora9("Fetching project...").start();
|
|
605
1924
|
const project = await client.getProject(id);
|
|
606
1925
|
spinner.stop();
|
|
607
1926
|
if (globalOpts.json) {
|
|
@@ -609,7 +1928,7 @@ function getCommand2() {
|
|
|
609
1928
|
return;
|
|
610
1929
|
}
|
|
611
1930
|
console.log();
|
|
612
|
-
console.log(
|
|
1931
|
+
console.log(chalk5.bold(` ${project.name}`));
|
|
613
1932
|
console.log();
|
|
614
1933
|
printDetail(
|
|
615
1934
|
[
|
|
@@ -625,7 +1944,7 @@ function getCommand2() {
|
|
|
625
1944
|
);
|
|
626
1945
|
if (project.vaults.length > 0) {
|
|
627
1946
|
console.log();
|
|
628
|
-
console.log(
|
|
1947
|
+
console.log(chalk5.bold(" Vaults"));
|
|
629
1948
|
console.log();
|
|
630
1949
|
printTable(
|
|
631
1950
|
["ID", "Name", "Encrypted"],
|
|
@@ -635,7 +1954,7 @@ function getCommand2() {
|
|
|
635
1954
|
}
|
|
636
1955
|
if (project.licenses.length > 0) {
|
|
637
1956
|
console.log();
|
|
638
|
-
console.log(
|
|
1957
|
+
console.log(chalk5.bold(" Licenses"));
|
|
639
1958
|
console.log();
|
|
640
1959
|
printTable(
|
|
641
1960
|
["ID", "Name", "Type"],
|
|
@@ -645,7 +1964,7 @@ function getCommand2() {
|
|
|
645
1964
|
}
|
|
646
1965
|
if (project.licenseGroups.length > 0) {
|
|
647
1966
|
console.log();
|
|
648
|
-
console.log(
|
|
1967
|
+
console.log(chalk5.bold(" License Groups"));
|
|
649
1968
|
console.log();
|
|
650
1969
|
printTable(
|
|
651
1970
|
["ID", "Name"],
|
|
@@ -659,11 +1978,11 @@ function getCommand2() {
|
|
|
659
1978
|
}
|
|
660
1979
|
|
|
661
1980
|
// src/commands/project/create.ts
|
|
662
|
-
import { Command as
|
|
663
|
-
import
|
|
664
|
-
import
|
|
665
|
-
function
|
|
666
|
-
const rl =
|
|
1981
|
+
import { Command as Command17 } from "commander";
|
|
1982
|
+
import readline3 from "node:readline";
|
|
1983
|
+
import ora10 from "ora";
|
|
1984
|
+
function prompt3(question) {
|
|
1985
|
+
const rl = readline3.createInterface({ input: process.stdin, output: process.stdout });
|
|
667
1986
|
return new Promise((resolve) => {
|
|
668
1987
|
rl.question(question, (answer) => {
|
|
669
1988
|
rl.close();
|
|
@@ -672,19 +1991,19 @@ function prompt2(question) {
|
|
|
672
1991
|
});
|
|
673
1992
|
}
|
|
674
1993
|
function createCommand() {
|
|
675
|
-
return new
|
|
1994
|
+
return new Command17("create").description("Create a new project").option("--name <name>", "Project name").option("--description <description>", "Project description").option("--external-id <externalId>", "External identifier").action(
|
|
676
1995
|
withErrorHandler(async (opts, cmd) => {
|
|
677
1996
|
const globalOpts = cmd.optsWithGlobals();
|
|
678
|
-
const
|
|
679
|
-
const client = new CliClient(
|
|
1997
|
+
const connection = resolveConnection(globalOpts, { requireApiKey: true });
|
|
1998
|
+
const client = new CliClient(connection.apiKey, connection.baseUrl);
|
|
680
1999
|
let name = opts.name;
|
|
681
2000
|
if (!name) {
|
|
682
|
-
name = await
|
|
2001
|
+
name = await prompt3("Project name: ");
|
|
683
2002
|
}
|
|
684
2003
|
if (!name) {
|
|
685
2004
|
throw new Error("Project name is required.");
|
|
686
2005
|
}
|
|
687
|
-
const spinner =
|
|
2006
|
+
const spinner = ora10("Creating project...").start();
|
|
688
2007
|
const response = await client.createProject({
|
|
689
2008
|
name,
|
|
690
2009
|
description: opts.description,
|
|
@@ -703,23 +2022,23 @@ function createCommand() {
|
|
|
703
2022
|
// src/commands/project/index.ts
|
|
704
2023
|
function registerProjectCommands(program2) {
|
|
705
2024
|
const project = program2.command("project").description("Manage projects");
|
|
706
|
-
project.addCommand(
|
|
2025
|
+
project.addCommand(listCommand3());
|
|
707
2026
|
project.addCommand(getCommand2());
|
|
708
2027
|
project.addCommand(createCommand());
|
|
709
2028
|
}
|
|
710
2029
|
|
|
711
2030
|
// src/commands/run/index.ts
|
|
712
2031
|
import { spawn } from "node:child_process";
|
|
713
|
-
import
|
|
714
|
-
import
|
|
2032
|
+
import ora11 from "ora";
|
|
2033
|
+
import R46 from "@r4security/sdk";
|
|
715
2034
|
function registerRunCommand(program2) {
|
|
716
2035
|
program2.command("run").description("Run a command with vault secrets injected as environment variables").argument("<command...>", "Command and arguments to execute").option("--prefix <prefix>", "Add prefix to all injected env var names").action(
|
|
717
2036
|
withErrorHandler(
|
|
718
2037
|
async (commandParts, opts, cmd) => {
|
|
719
2038
|
const globalOpts = cmd.optsWithGlobals();
|
|
720
2039
|
const config = resolveAuth(globalOpts);
|
|
721
|
-
const spinner =
|
|
722
|
-
const r4 = await
|
|
2040
|
+
const spinner = ora11("Loading vault secrets...").start();
|
|
2041
|
+
const r4 = await R46.create(config);
|
|
723
2042
|
spinner.stop();
|
|
724
2043
|
const env = r4.env;
|
|
725
2044
|
const secretEnv = {};
|
|
@@ -745,11 +2064,14 @@ function registerRunCommand(program2) {
|
|
|
745
2064
|
}
|
|
746
2065
|
|
|
747
2066
|
// src/index.ts
|
|
748
|
-
var program = new
|
|
749
|
-
program.name("r4").description("R4 CLI \u2014 manage vaults, projects, and secrets from the terminal").version("0.0.
|
|
2067
|
+
var program = new Command18();
|
|
2068
|
+
program.name("r4").description("R4 CLI \u2014 manage vaults, projects, and secrets from the terminal").version("0.0.5").option("--api-key <key>", "API key (overrides R4_API_KEY env var and config file)").option("--profile <name>", "CLI profile name (overrides R4_PROFILE and the saved current profile)").option("--project-id <id>", "Optional project ID filter (overrides R4_PROJECT_ID env var and config file)").option("--dev", "Use https://dev.r4.dev unless an explicit base URL override is set").option("--base-url <url>", "API base URL (default: https://r4.dev)").option("--private-key-path <path>", "Path to the agent private key PEM (overrides R4_PRIVATE_KEY_PATH env var and config file)").option("--trust-store-path <path>", "Path to the local signer trust-store JSON (overrides R4_TRUST_STORE_PATH env var and config file)").option("--json", "Output as JSON for scripting and piping", false);
|
|
2069
|
+
registerAgentCommands(program);
|
|
750
2070
|
registerAuthCommands(program);
|
|
2071
|
+
registerProfileCommands(program);
|
|
751
2072
|
registerVaultCommands(program);
|
|
752
2073
|
registerProjectCommands(program);
|
|
753
2074
|
registerRunCommand(program);
|
|
2075
|
+
program.addCommand(doctorCommand());
|
|
754
2076
|
program.parse();
|
|
755
2077
|
//# sourceMappingURL=index.js.map
|