@attest-it/cli 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/attest-it.js +2600 -251
- package/dist/bin/attest-it.js.map +1 -1
- package/dist/index.cjs +2599 -248
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2599 -250
- package/dist/index.js.map +1 -1
- package/package.json +9 -3
package/dist/index.cjs
CHANGED
|
@@ -13,9 +13,14 @@ var React7 = require('react');
|
|
|
13
13
|
var ink = require('ink');
|
|
14
14
|
var jsxRuntime = require('react/jsx-runtime');
|
|
15
15
|
var promises = require('fs/promises');
|
|
16
|
+
var ui = require('@inkjs/ui');
|
|
17
|
+
var yaml = require('yaml');
|
|
18
|
+
var tabtab = require('@pnpm/tabtab');
|
|
16
19
|
var url = require('url');
|
|
17
20
|
|
|
18
21
|
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
22
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
23
|
+
|
|
19
24
|
function _interopNamespace(e) {
|
|
20
25
|
if (e && e.__esModule) return e;
|
|
21
26
|
var n = Object.create(null);
|
|
@@ -38,6 +43,7 @@ var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
|
|
|
38
43
|
var path__namespace = /*#__PURE__*/_interopNamespace(path);
|
|
39
44
|
var os__namespace = /*#__PURE__*/_interopNamespace(os);
|
|
40
45
|
var React7__namespace = /*#__PURE__*/_interopNamespace(React7);
|
|
46
|
+
var tabtab__default = /*#__PURE__*/_interopDefault(tabtab);
|
|
41
47
|
|
|
42
48
|
// src/index.ts
|
|
43
49
|
var globalOptions = {};
|
|
@@ -116,30 +122,63 @@ function formatTable(rows) {
|
|
|
116
122
|
}
|
|
117
123
|
return lines.join("\n");
|
|
118
124
|
}
|
|
119
|
-
function colorizeStatus(status) {
|
|
120
|
-
const t = getTheme();
|
|
121
|
-
switch (status) {
|
|
122
|
-
case "VALID":
|
|
123
|
-
return t.green(status);
|
|
124
|
-
case "NEEDS_ATTESTATION":
|
|
125
|
-
case "FINGERPRINT_CHANGED":
|
|
126
|
-
return t.yellow(status);
|
|
127
|
-
case "EXPIRED":
|
|
128
|
-
case "INVALIDATED_BY_PARENT":
|
|
129
|
-
return t.red(status);
|
|
130
|
-
case "SIGNATURE_INVALID":
|
|
131
|
-
return t.red.bold()(status);
|
|
132
|
-
default:
|
|
133
|
-
return status;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
125
|
function outputJson(data) {
|
|
137
126
|
console.log(JSON.stringify(data, null, 2));
|
|
138
127
|
}
|
|
128
|
+
var BOX_CHARS = {
|
|
129
|
+
topLeft: "\u250C",
|
|
130
|
+
topRight: "\u2510",
|
|
131
|
+
bottomLeft: "\u2514",
|
|
132
|
+
bottomRight: "\u2518",
|
|
133
|
+
horizontal: "\u2500",
|
|
134
|
+
vertical: "\u2502"};
|
|
135
|
+
var theme2;
|
|
136
|
+
function getTheme2() {
|
|
137
|
+
if (!theme2) {
|
|
138
|
+
const noopFn = (str) => str;
|
|
139
|
+
const chainable = () => noopFn;
|
|
140
|
+
theme2 = {
|
|
141
|
+
red: Object.assign(noopFn, { bold: chainable, dim: chainable }),
|
|
142
|
+
green: Object.assign(noopFn, { bold: chainable, dim: chainable }),
|
|
143
|
+
yellow: Object.assign(noopFn, { bold: chainable, dim: chainable }),
|
|
144
|
+
blue: Object.assign(noopFn, { bold: chainable, dim: chainable }),
|
|
145
|
+
success: noopFn,
|
|
146
|
+
error: noopFn,
|
|
147
|
+
warning: noopFn,
|
|
148
|
+
info: noopFn,
|
|
149
|
+
muted: noopFn
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
return theme2;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/utils/prompts.ts
|
|
139
156
|
async function confirmAction(options) {
|
|
157
|
+
const theme3 = getTheme2();
|
|
158
|
+
const defaultIndicator = options.default ? "(Y/n)" : "(y/N)";
|
|
159
|
+
const message = `${options.message}? ${defaultIndicator}`;
|
|
160
|
+
const boxWidth = Math.max(message.length + 2, 40);
|
|
161
|
+
const contentPadding = " ".repeat(boxWidth - message.length - 1);
|
|
162
|
+
const topBorder = theme3.yellow(
|
|
163
|
+
`${BOX_CHARS.topLeft}${BOX_CHARS.horizontal.repeat(boxWidth)}${BOX_CHARS.topRight}`
|
|
164
|
+
);
|
|
165
|
+
const bottomBorder = theme3.yellow(
|
|
166
|
+
`${BOX_CHARS.bottomLeft}${BOX_CHARS.horizontal.repeat(boxWidth)}${BOX_CHARS.bottomRight}`
|
|
167
|
+
);
|
|
168
|
+
const contentLine = theme3.yellow(BOX_CHARS.vertical) + ` ${message}${contentPadding}` + theme3.yellow(BOX_CHARS.vertical);
|
|
169
|
+
console.log("");
|
|
170
|
+
console.log(topBorder);
|
|
171
|
+
console.log(contentLine);
|
|
172
|
+
console.log(bottomBorder);
|
|
173
|
+
console.log("");
|
|
140
174
|
return prompts.confirm({
|
|
141
|
-
message:
|
|
142
|
-
|
|
175
|
+
message: "",
|
|
176
|
+
// Empty message since we displayed it above
|
|
177
|
+
default: options.default ?? false,
|
|
178
|
+
theme: {
|
|
179
|
+
prefix: ""
|
|
180
|
+
// Remove default prefix
|
|
181
|
+
}
|
|
143
182
|
});
|
|
144
183
|
}
|
|
145
184
|
|
|
@@ -230,71 +269,68 @@ async function runInit(options) {
|
|
|
230
269
|
process.exit(ExitCode.CONFIG_ERROR);
|
|
231
270
|
}
|
|
232
271
|
}
|
|
233
|
-
var statusCommand = new commander.Command("status").description("Show
|
|
234
|
-
await runStatus(options);
|
|
272
|
+
var statusCommand = new commander.Command("status").description("Show seal status for all gates").argument("[gates...]", "Show status for specific gates only").option("--json", "Output JSON for machine parsing").action(async (gates, options) => {
|
|
273
|
+
await runStatus(gates, options);
|
|
235
274
|
});
|
|
236
|
-
async function runStatus(options) {
|
|
275
|
+
async function runStatus(gates, options) {
|
|
237
276
|
try {
|
|
238
277
|
const config = await core.loadConfig();
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
attestationsFile = await core.readAttestations(attestationsPath);
|
|
243
|
-
} catch (err) {
|
|
244
|
-
if (err instanceof Error && !err.message.includes("ENOENT")) {
|
|
245
|
-
throw err;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
const attestations = attestationsFile?.attestations ?? [];
|
|
249
|
-
const suiteNames = options.suite ? [options.suite] : Object.keys(config.suites);
|
|
250
|
-
if (options.suite && !config.suites[options.suite]) {
|
|
251
|
-
error(`Suite "${options.suite}" not found in config`);
|
|
278
|
+
const attestItConfig = core.toAttestItConfig(config);
|
|
279
|
+
if (!attestItConfig.gates || Object.keys(attestItConfig.gates).length === 0) {
|
|
280
|
+
error("No gates defined in configuration");
|
|
252
281
|
process.exit(ExitCode.CONFIG_ERROR);
|
|
253
282
|
}
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
if (!
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
);
|
|
271
|
-
const status = determineStatus(
|
|
272
|
-
attestation ?? null,
|
|
273
|
-
fingerprintResult.fingerprint,
|
|
274
|
-
config.settings.maxAgeDays
|
|
275
|
-
);
|
|
276
|
-
let age;
|
|
277
|
-
if (attestation) {
|
|
278
|
-
const attestedAt = new Date(attestation.attestedAt);
|
|
279
|
-
age = Math.floor((Date.now() - attestedAt.getTime()) / (1e3 * 60 * 60 * 24));
|
|
280
|
-
}
|
|
281
|
-
if (status !== "VALID") {
|
|
282
|
-
hasInvalid = true;
|
|
283
|
-
}
|
|
284
|
-
results.push({
|
|
285
|
-
name: suiteName,
|
|
286
|
-
status,
|
|
287
|
-
currentFingerprint: fingerprintResult.fingerprint,
|
|
288
|
-
attestedFingerprint: attestation?.fingerprint,
|
|
289
|
-
attestedAt: attestation?.attestedAt,
|
|
290
|
-
age
|
|
283
|
+
const projectRoot = process.cwd();
|
|
284
|
+
const sealsFile = core.readSealsSync(projectRoot);
|
|
285
|
+
const gatesToCheck = gates.length > 0 ? gates : Object.keys(attestItConfig.gates);
|
|
286
|
+
for (const gateId of gatesToCheck) {
|
|
287
|
+
if (!attestItConfig.gates[gateId]) {
|
|
288
|
+
error(`Gate '${gateId}' not found in configuration`);
|
|
289
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const fingerprints = {};
|
|
293
|
+
for (const gateId of gatesToCheck) {
|
|
294
|
+
const gate = attestItConfig.gates[gateId];
|
|
295
|
+
if (!gate) continue;
|
|
296
|
+
const result = core.computeFingerprintSync({
|
|
297
|
+
packages: gate.fingerprint.paths,
|
|
298
|
+
...gate.fingerprint.exclude && { ignore: gate.fingerprint.exclude }
|
|
291
299
|
});
|
|
300
|
+
fingerprints[gateId] = result.fingerprint;
|
|
292
301
|
}
|
|
302
|
+
const verificationResults = gates.length > 0 ? gatesToCheck.map(
|
|
303
|
+
(gateId) => (
|
|
304
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
305
|
+
core.verifyGateSeal(attestItConfig, gateId, sealsFile, fingerprints[gateId] ?? "")
|
|
306
|
+
)
|
|
307
|
+
) : core.verifyAllSeals(attestItConfig, sealsFile, fingerprints);
|
|
308
|
+
const results = verificationResults.map((result) => {
|
|
309
|
+
const status = {
|
|
310
|
+
gateId: result.gateId,
|
|
311
|
+
state: result.state,
|
|
312
|
+
currentFingerprint: fingerprints[result.gateId] ?? "",
|
|
313
|
+
message: result.message
|
|
314
|
+
};
|
|
315
|
+
if (result.seal) {
|
|
316
|
+
status.sealedFingerprint = result.seal.fingerprint;
|
|
317
|
+
status.sealedBy = result.seal.sealedBy;
|
|
318
|
+
status.sealedAt = result.seal.timestamp;
|
|
319
|
+
const timestamp = new Date(result.seal.timestamp);
|
|
320
|
+
const now = Date.now();
|
|
321
|
+
const ageMs = now - timestamp.getTime();
|
|
322
|
+
status.age = Math.floor(ageMs / (1e3 * 60 * 60 * 24));
|
|
323
|
+
}
|
|
324
|
+
return status;
|
|
325
|
+
});
|
|
293
326
|
if (options.json) {
|
|
294
327
|
outputJson(results);
|
|
295
328
|
} else {
|
|
296
|
-
displayStatusTable(results
|
|
329
|
+
displayStatusTable(results);
|
|
297
330
|
}
|
|
331
|
+
const hasInvalid = results.some(
|
|
332
|
+
(r) => r.state === "MISSING" || r.state === "FINGERPRINT_MISMATCH" || r.state === "INVALID_SIGNATURE" || r.state === "UNKNOWN_SIGNER" || r.state === "STALE"
|
|
333
|
+
);
|
|
298
334
|
process.exit(hasInvalid ? ExitCode.FAILURE : ExitCode.SUCCESS);
|
|
299
335
|
} catch (err) {
|
|
300
336
|
if (err instanceof Error) {
|
|
@@ -305,50 +341,73 @@ async function runStatus(options) {
|
|
|
305
341
|
process.exit(ExitCode.CONFIG_ERROR);
|
|
306
342
|
}
|
|
307
343
|
}
|
|
308
|
-
function
|
|
309
|
-
if (!attestation) {
|
|
310
|
-
return "NEEDS_ATTESTATION";
|
|
311
|
-
}
|
|
312
|
-
if (attestation.fingerprint !== currentFingerprint) {
|
|
313
|
-
return "FINGERPRINT_CHANGED";
|
|
314
|
-
}
|
|
315
|
-
const attestedAt = new Date(attestation.attestedAt);
|
|
316
|
-
const ageInDays = Math.floor((Date.now() - attestedAt.getTime()) / (1e3 * 60 * 60 * 24));
|
|
317
|
-
if (ageInDays > maxAgeDays) {
|
|
318
|
-
return "EXPIRED";
|
|
319
|
-
}
|
|
320
|
-
return "VALID";
|
|
321
|
-
}
|
|
322
|
-
function displayStatusTable(results, hasInvalid) {
|
|
344
|
+
function displayStatusTable(results) {
|
|
323
345
|
const tableRows = results.map((r) => ({
|
|
324
|
-
suite: r.
|
|
325
|
-
status:
|
|
346
|
+
suite: r.gateId,
|
|
347
|
+
status: colorizeState(r.state),
|
|
326
348
|
fingerprint: r.currentFingerprint.slice(0, 16) + "...",
|
|
327
349
|
age: formatAge(r)
|
|
328
350
|
}));
|
|
329
351
|
log("");
|
|
330
352
|
log(formatTable(tableRows));
|
|
331
353
|
log("");
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
354
|
+
const sealed = results.filter((r) => r.sealedBy && r.sealedAt);
|
|
355
|
+
if (sealed.length > 0) {
|
|
356
|
+
log("Seal metadata:");
|
|
357
|
+
for (const result of sealed) {
|
|
358
|
+
log(` ${result.gateId}:`);
|
|
359
|
+
log(` Sealed by: ${result.sealedBy ?? "unknown"}`);
|
|
360
|
+
if (result.sealedAt) {
|
|
361
|
+
const date = new Date(result.sealedAt);
|
|
362
|
+
log(` Sealed at: ${date.toLocaleString()}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
log("");
|
|
336
366
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
367
|
+
const withIssues = results.filter((r) => r.state !== "VALID" && r.message);
|
|
368
|
+
if (withIssues.length > 0) {
|
|
369
|
+
log("Issues:");
|
|
370
|
+
for (const result of withIssues) {
|
|
371
|
+
log(` ${result.gateId}: ${result.message ?? "Unknown issue"}`);
|
|
372
|
+
}
|
|
373
|
+
log("");
|
|
341
374
|
}
|
|
342
|
-
|
|
343
|
-
|
|
375
|
+
const validCount = results.filter((r) => r.state === "VALID").length;
|
|
376
|
+
const invalidCount = results.length - validCount;
|
|
377
|
+
if (invalidCount === 0) {
|
|
378
|
+
success("All gate seals valid");
|
|
379
|
+
} else {
|
|
380
|
+
log(`Run 'attest-it seal' to create or update seals`);
|
|
344
381
|
}
|
|
345
|
-
|
|
346
|
-
|
|
382
|
+
}
|
|
383
|
+
function colorizeState(state) {
|
|
384
|
+
const theme3 = getTheme();
|
|
385
|
+
switch (state) {
|
|
386
|
+
case "VALID":
|
|
387
|
+
return theme3.green(state);
|
|
388
|
+
case "MISSING":
|
|
389
|
+
case "STALE":
|
|
390
|
+
return theme3.yellow(state);
|
|
391
|
+
case "FINGERPRINT_MISMATCH":
|
|
392
|
+
case "INVALID_SIGNATURE":
|
|
393
|
+
case "UNKNOWN_SIGNER":
|
|
394
|
+
return theme3.red(state);
|
|
395
|
+
default:
|
|
396
|
+
return state;
|
|
347
397
|
}
|
|
348
|
-
|
|
349
|
-
|
|
398
|
+
}
|
|
399
|
+
function formatAge(result) {
|
|
400
|
+
if (result.state === "VALID" || result.state === "STALE") {
|
|
401
|
+
return `${String(result.age ?? 0)} days${result.state === "STALE" ? " (stale)" : ""}`;
|
|
402
|
+
}
|
|
403
|
+
switch (result.state) {
|
|
404
|
+
case "MISSING":
|
|
405
|
+
return "(none)";
|
|
406
|
+
case "FINGERPRINT_MISMATCH":
|
|
407
|
+
return "(changed)";
|
|
408
|
+
default:
|
|
409
|
+
return "-";
|
|
350
410
|
}
|
|
351
|
-
return "-";
|
|
352
411
|
}
|
|
353
412
|
function Header({ pendingCount }) {
|
|
354
413
|
const message = `${pendingCount.toString()} suite${pendingCount === 1 ? "" : "s"} need${pendingCount === 1 ? "s" : ""} attestation`;
|
|
@@ -431,14 +490,14 @@ function SelectionPrompt({
|
|
|
431
490
|
onSelect,
|
|
432
491
|
groups
|
|
433
492
|
}) {
|
|
434
|
-
ink.useInput((
|
|
435
|
-
const matchedOption = options.find((opt) => opt.hint ===
|
|
493
|
+
ink.useInput((input5) => {
|
|
494
|
+
const matchedOption = options.find((opt) => opt.hint === input5);
|
|
436
495
|
if (matchedOption) {
|
|
437
496
|
onSelect(matchedOption.value);
|
|
438
497
|
return;
|
|
439
498
|
}
|
|
440
499
|
if (groups) {
|
|
441
|
-
const matchedGroup = groups.find((group) => group.name ===
|
|
500
|
+
const matchedGroup = groups.find((group) => group.name === input5);
|
|
442
501
|
if (matchedGroup) {
|
|
443
502
|
onSelect(matchedGroup.name);
|
|
444
503
|
}
|
|
@@ -488,17 +547,17 @@ function SuiteSelector({
|
|
|
488
547
|
return next;
|
|
489
548
|
});
|
|
490
549
|
}, []);
|
|
491
|
-
ink.useInput((
|
|
492
|
-
if (
|
|
550
|
+
ink.useInput((input5, key) => {
|
|
551
|
+
if (input5 === "a") {
|
|
493
552
|
setSelectedSuites(new Set(pendingSuites.map((s) => s.name)));
|
|
494
553
|
return;
|
|
495
554
|
}
|
|
496
|
-
if (
|
|
555
|
+
if (input5 === "n") {
|
|
497
556
|
onExit();
|
|
498
557
|
return;
|
|
499
558
|
}
|
|
500
|
-
if (/^[1-9]$/.test(
|
|
501
|
-
const idx = parseInt(
|
|
559
|
+
if (/^[1-9]$/.test(input5)) {
|
|
560
|
+
const idx = parseInt(input5, 10) - 1;
|
|
502
561
|
if (idx < pendingSuites.length) {
|
|
503
562
|
const suite = pendingSuites[idx];
|
|
504
563
|
if (suite) {
|
|
@@ -507,8 +566,8 @@ function SuiteSelector({
|
|
|
507
566
|
}
|
|
508
567
|
return;
|
|
509
568
|
}
|
|
510
|
-
if (
|
|
511
|
-
const groupIdx = parseInt(
|
|
569
|
+
if (input5.startsWith("g") && groups) {
|
|
570
|
+
const groupIdx = parseInt(input5.slice(1), 10) - 1;
|
|
512
571
|
const groupNames = Object.keys(groups);
|
|
513
572
|
if (groupIdx >= 0 && groupIdx < groupNames.length) {
|
|
514
573
|
const groupName = groupNames[groupIdx];
|
|
@@ -525,7 +584,7 @@ function SuiteSelector({
|
|
|
525
584
|
onSelect(Array.from(selectedSuites));
|
|
526
585
|
return;
|
|
527
586
|
}
|
|
528
|
-
if (
|
|
587
|
+
if (input5 === " ") {
|
|
529
588
|
const currentSuite = pendingSuites[cursorIndex];
|
|
530
589
|
if (currentSuite) {
|
|
531
590
|
toggleSuite(currentSuite.name);
|
|
@@ -658,11 +717,11 @@ function TestRunner({
|
|
|
658
717
|
};
|
|
659
718
|
}, [currentIndex, phase, suites, executeTest, onComplete]);
|
|
660
719
|
ink.useInput(
|
|
661
|
-
(
|
|
720
|
+
(input5, key) => {
|
|
662
721
|
if (phase !== "confirming") return;
|
|
663
722
|
const currentSuite2 = suites[currentIndex];
|
|
664
723
|
if (!currentSuite2) return;
|
|
665
|
-
if (
|
|
724
|
+
if (input5.toLowerCase() === "y" || key.return) {
|
|
666
725
|
createAttestation3(currentSuite2).then(() => {
|
|
667
726
|
setResults((prev) => ({
|
|
668
727
|
...prev,
|
|
@@ -679,7 +738,7 @@ function TestRunner({
|
|
|
679
738
|
setPhase("running");
|
|
680
739
|
});
|
|
681
740
|
}
|
|
682
|
-
if (
|
|
741
|
+
if (input5.toLowerCase() === "n") {
|
|
683
742
|
setResults((prev) => ({
|
|
684
743
|
...prev,
|
|
685
744
|
skipped: [...prev.skipped, currentSuite2]
|
|
@@ -848,7 +907,7 @@ function InteractiveRun({
|
|
|
848
907
|
] })
|
|
849
908
|
] });
|
|
850
909
|
}
|
|
851
|
-
function
|
|
910
|
+
function determineStatus(attestation, currentFingerprint, maxAgeDays) {
|
|
852
911
|
if (!attestation) {
|
|
853
912
|
return "NEEDS_ATTESTATION";
|
|
854
913
|
}
|
|
@@ -874,6 +933,9 @@ async function getAllSuiteStatuses(config) {
|
|
|
874
933
|
const attestations = attestationsFile?.attestations ?? [];
|
|
875
934
|
const results = [];
|
|
876
935
|
for (const [suiteName, suiteConfig] of Object.entries(config.suites)) {
|
|
936
|
+
if (!suiteConfig.packages) {
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
877
939
|
const fingerprintResult = await core.computeFingerprint({
|
|
878
940
|
packages: suiteConfig.packages,
|
|
879
941
|
...suiteConfig.ignore && { ignore: suiteConfig.ignore }
|
|
@@ -882,7 +944,7 @@ async function getAllSuiteStatuses(config) {
|
|
|
882
944
|
{ schemaVersion: "1", attestations, signature: "" },
|
|
883
945
|
suiteName
|
|
884
946
|
);
|
|
885
|
-
const status =
|
|
947
|
+
const status = determineStatus(
|
|
886
948
|
attestation,
|
|
887
949
|
fingerprintResult.fingerprint,
|
|
888
950
|
config.settings.maxAgeDays
|
|
@@ -1039,6 +1101,9 @@ function createAttestationCreator(config) {
|
|
|
1039
1101
|
if (!suiteConfig) {
|
|
1040
1102
|
throw new Error(`Suite "${suiteName}" not found`);
|
|
1041
1103
|
}
|
|
1104
|
+
if (!suiteConfig.packages) {
|
|
1105
|
+
throw new Error(`Suite "${suiteName}" has no packages defined`);
|
|
1106
|
+
}
|
|
1042
1107
|
const fingerprintResult = await core.computeFingerprint({
|
|
1043
1108
|
packages: suiteConfig.packages,
|
|
1044
1109
|
...suiteConfig.ignore && { ignore: suiteConfig.ignore }
|
|
@@ -1053,14 +1118,34 @@ function createAttestationCreator(config) {
|
|
|
1053
1118
|
const existingFile = await core.readAttestations(attestationsPath).catch(() => null);
|
|
1054
1119
|
const existingAttestations = existingFile?.attestations ?? [];
|
|
1055
1120
|
const newAttestations = core.upsertAttestation(existingAttestations, attestation);
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1121
|
+
let keyProvider;
|
|
1122
|
+
let keyRef;
|
|
1123
|
+
if (config.settings.keyProvider) {
|
|
1124
|
+
keyProvider = core.KeyProviderRegistry.create({
|
|
1125
|
+
type: config.settings.keyProvider.type,
|
|
1126
|
+
options: config.settings.keyProvider.options ?? {}
|
|
1127
|
+
});
|
|
1128
|
+
if (config.settings.keyProvider.type === "filesystem") {
|
|
1129
|
+
keyRef = config.settings.keyProvider.options?.privateKeyPath ?? core.getDefaultPrivateKeyPath();
|
|
1130
|
+
} else if (config.settings.keyProvider.type === "1password") {
|
|
1131
|
+
keyRef = config.settings.keyProvider.options?.itemName ?? "attest-it-private-key";
|
|
1132
|
+
} else {
|
|
1133
|
+
throw new Error(`Unsupported key provider type: ${config.settings.keyProvider.type}`);
|
|
1134
|
+
}
|
|
1135
|
+
} else {
|
|
1136
|
+
keyProvider = new core.FilesystemKeyProvider();
|
|
1137
|
+
keyRef = core.getDefaultPrivateKeyPath();
|
|
1138
|
+
}
|
|
1139
|
+
if (!await keyProvider.keyExists(keyRef)) {
|
|
1140
|
+
const providerName = keyProvider.displayName;
|
|
1141
|
+
const keygenMessage = keyProvider.type === "filesystem" ? 'Run "attest-it keygen" first to generate a keypair.' : 'Run "attest-it keygen" to generate and store a key.';
|
|
1142
|
+
throw new Error(`Private key not found in ${providerName}. ${keygenMessage}`);
|
|
1059
1143
|
}
|
|
1060
1144
|
await core.writeSignedAttestations({
|
|
1061
1145
|
filePath: attestationsPath,
|
|
1062
1146
|
attestations: newAttestations,
|
|
1063
|
-
|
|
1147
|
+
keyProvider,
|
|
1148
|
+
keyRef
|
|
1064
1149
|
});
|
|
1065
1150
|
log(`\u2713 Attestation created for ${suiteName}`);
|
|
1066
1151
|
};
|
|
@@ -1123,7 +1208,7 @@ async function checkDirtyWorkingTree() {
|
|
|
1123
1208
|
}
|
|
1124
1209
|
|
|
1125
1210
|
// src/commands/run.ts
|
|
1126
|
-
var runCommand = new commander.Command("run").description("Execute tests and create attestation").option("-s, --suite <name>", "Run specific suite (required unless --all or interactive mode)").option("-a, --all", "Run all suites needing attestation").option("--no-attest", "Run tests without creating attestation").option("
|
|
1211
|
+
var runCommand = new commander.Command("run").description("Execute tests and create attestation").option("-s, --suite <name>", "Run specific suite (required unless --all or interactive mode)").option("-a, --all", "Run all suites needing attestation").option("--no-attest", "Run tests without creating attestation").option("--dry-run", "Show what would run without executing").option("-c, --continue", "Resume interrupted session").option("--filter <pattern>", "Filter suites by pattern (glob-style)").action(async (options) => {
|
|
1127
1212
|
await runTests(options);
|
|
1128
1213
|
});
|
|
1129
1214
|
async function runTests(options) {
|
|
@@ -1288,6 +1373,10 @@ async function runSingleSuite(suiteName, config, options) {
|
|
|
1288
1373
|
error(`Suite "${suiteName}" not found in config`);
|
|
1289
1374
|
process.exit(ExitCode.CONFIG_ERROR);
|
|
1290
1375
|
}
|
|
1376
|
+
if (!suiteConfig.packages) {
|
|
1377
|
+
error(`Suite "${suiteName}" has no packages defined`);
|
|
1378
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
1379
|
+
}
|
|
1291
1380
|
log(`
|
|
1292
1381
|
=== Running suite: ${suiteName} ===
|
|
1293
1382
|
`);
|
|
@@ -1311,9 +1400,9 @@ async function runSingleSuite(suiteName, config, options) {
|
|
|
1311
1400
|
log("Skipping attestation (--no-attest)");
|
|
1312
1401
|
return;
|
|
1313
1402
|
}
|
|
1314
|
-
const shouldAttest =
|
|
1315
|
-
message: "Create attestation
|
|
1316
|
-
default:
|
|
1403
|
+
const shouldAttest = await confirmAction({
|
|
1404
|
+
message: "Create attestation",
|
|
1405
|
+
default: false
|
|
1317
1406
|
});
|
|
1318
1407
|
if (!shouldAttest) {
|
|
1319
1408
|
warn("Attestation cancelled");
|
|
@@ -1329,32 +1418,546 @@ async function runSingleSuite(suiteName, config, options) {
|
|
|
1329
1418
|
const existingFile = await core.readAttestations(attestationsPath);
|
|
1330
1419
|
const existingAttestations = existingFile?.attestations ?? [];
|
|
1331
1420
|
const newAttestations = core.upsertAttestation(existingAttestations, attestation);
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1421
|
+
let keyProvider;
|
|
1422
|
+
let keyRef;
|
|
1423
|
+
if (config.settings.keyProvider) {
|
|
1424
|
+
keyProvider = core.KeyProviderRegistry.create({
|
|
1425
|
+
type: config.settings.keyProvider.type,
|
|
1426
|
+
options: config.settings.keyProvider.options ?? {}
|
|
1427
|
+
});
|
|
1428
|
+
if (config.settings.keyProvider.type === "filesystem") {
|
|
1429
|
+
keyRef = config.settings.keyProvider.options?.privateKeyPath ?? core.getDefaultPrivateKeyPath();
|
|
1430
|
+
} else if (config.settings.keyProvider.type === "1password") {
|
|
1431
|
+
keyRef = config.settings.keyProvider.options?.itemName ?? "attest-it-private-key";
|
|
1432
|
+
} else {
|
|
1433
|
+
throw new Error(`Unsupported key provider type: ${config.settings.keyProvider.type}`);
|
|
1434
|
+
}
|
|
1435
|
+
} else {
|
|
1436
|
+
keyProvider = new core.FilesystemKeyProvider();
|
|
1437
|
+
keyRef = core.getDefaultPrivateKeyPath();
|
|
1438
|
+
}
|
|
1439
|
+
if (!await keyProvider.keyExists(keyRef)) {
|
|
1440
|
+
error(`Private key not found in ${keyProvider.displayName}`);
|
|
1441
|
+
if (keyProvider.type === "filesystem") {
|
|
1442
|
+
error('Run "attest-it keygen" first to generate a keypair.');
|
|
1443
|
+
} else {
|
|
1444
|
+
error('Run "attest-it keygen" to generate and store a key.');
|
|
1445
|
+
}
|
|
1336
1446
|
process.exit(ExitCode.MISSING_KEY);
|
|
1337
1447
|
}
|
|
1338
1448
|
await core.writeSignedAttestations({
|
|
1339
1449
|
filePath: attestationsPath,
|
|
1340
1450
|
attestations: newAttestations,
|
|
1341
|
-
|
|
1451
|
+
keyProvider,
|
|
1452
|
+
keyRef
|
|
1342
1453
|
});
|
|
1343
1454
|
success(`Attestation created for ${suiteName}`);
|
|
1344
1455
|
log(` Fingerprint: ${fingerprintResult.fingerprint}`);
|
|
1345
1456
|
log(` Attested by: ${attestation.attestedBy}`);
|
|
1346
1457
|
log(` Attested at: ${attestation.attestedAt}`);
|
|
1458
|
+
if (suiteConfig.gate) {
|
|
1459
|
+
await promptForSeal(suiteName, suiteConfig.gate, config);
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
async function promptForSeal(suiteName, gateId, config) {
|
|
1463
|
+
log("");
|
|
1464
|
+
log(`Suite '${suiteName}' is linked to gate '${gateId}'`);
|
|
1465
|
+
const localConfig = core.loadLocalConfigSync();
|
|
1466
|
+
if (!localConfig) {
|
|
1467
|
+
warn("No local identity configuration found - cannot create seal");
|
|
1468
|
+
warn('Run "attest-it keygen" to set up your identity');
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
const identity = core.getActiveIdentity(localConfig);
|
|
1472
|
+
if (!identity) {
|
|
1473
|
+
warn(`Active identity '${localConfig.activeIdentity}' not found in local config`);
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
const attestItConfig = core.toAttestItConfig(config);
|
|
1477
|
+
const authorized = core.isAuthorizedSigner(attestItConfig, gateId, identity.publicKey);
|
|
1478
|
+
if (!authorized) {
|
|
1479
|
+
warn(`You are not authorized to seal gate '${gateId}'`);
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
const shouldSeal = await confirmAction({
|
|
1483
|
+
message: `Create seal for gate '${gateId}'`,
|
|
1484
|
+
default: true
|
|
1485
|
+
});
|
|
1486
|
+
if (!shouldSeal) {
|
|
1487
|
+
log("Seal creation skipped");
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
try {
|
|
1491
|
+
if (!attestItConfig.gates?.[gateId]) {
|
|
1492
|
+
error(`Gate '${gateId}' not found in configuration`);
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
const gate = attestItConfig.gates[gateId];
|
|
1496
|
+
const gateFingerprint = core.computeFingerprintSync({
|
|
1497
|
+
packages: gate.fingerprint.paths,
|
|
1498
|
+
...gate.fingerprint.exclude && { ignore: gate.fingerprint.exclude }
|
|
1499
|
+
});
|
|
1500
|
+
const keyProvider = createKeyProviderFromIdentity(identity);
|
|
1501
|
+
const keyRef = getKeyRefFromIdentity(identity);
|
|
1502
|
+
const keyResult = await keyProvider.getPrivateKey(keyRef);
|
|
1503
|
+
const fs4 = await import('fs/promises');
|
|
1504
|
+
const privateKeyPem = await fs4.readFile(keyResult.keyPath, "utf8");
|
|
1505
|
+
await keyResult.cleanup();
|
|
1506
|
+
const seal = core.createSeal({
|
|
1507
|
+
gateId,
|
|
1508
|
+
fingerprint: gateFingerprint.fingerprint,
|
|
1509
|
+
sealedBy: identity.name,
|
|
1510
|
+
privateKey: privateKeyPem
|
|
1511
|
+
});
|
|
1512
|
+
const projectRoot = process.cwd();
|
|
1513
|
+
const sealsFile = core.readSealsSync(projectRoot);
|
|
1514
|
+
sealsFile.seals[gateId] = seal;
|
|
1515
|
+
core.writeSealsSync(projectRoot, sealsFile);
|
|
1516
|
+
success(`Seal created for gate '${gateId}'`);
|
|
1517
|
+
log(` Sealed by: ${identity.name}`);
|
|
1518
|
+
log(` Timestamp: ${seal.timestamp}`);
|
|
1519
|
+
} catch (err) {
|
|
1520
|
+
if (err instanceof Error) {
|
|
1521
|
+
error(`Failed to create seal: ${err.message}`);
|
|
1522
|
+
} else {
|
|
1523
|
+
error("Failed to create seal: Unknown error");
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
function createKeyProviderFromIdentity(identity) {
|
|
1528
|
+
const { privateKey } = identity;
|
|
1529
|
+
switch (privateKey.type) {
|
|
1530
|
+
case "file":
|
|
1531
|
+
return core.KeyProviderRegistry.create({
|
|
1532
|
+
type: "filesystem",
|
|
1533
|
+
options: { privateKeyPath: privateKey.path }
|
|
1534
|
+
});
|
|
1535
|
+
case "keychain":
|
|
1536
|
+
return core.KeyProviderRegistry.create({
|
|
1537
|
+
type: "macos-keychain",
|
|
1538
|
+
options: {
|
|
1539
|
+
service: privateKey.service,
|
|
1540
|
+
account: privateKey.account
|
|
1541
|
+
}
|
|
1542
|
+
});
|
|
1543
|
+
case "1password":
|
|
1544
|
+
return core.KeyProviderRegistry.create({
|
|
1545
|
+
type: "1password",
|
|
1546
|
+
options: {
|
|
1547
|
+
account: privateKey.account,
|
|
1548
|
+
vault: privateKey.vault,
|
|
1549
|
+
itemName: privateKey.item,
|
|
1550
|
+
field: privateKey.field
|
|
1551
|
+
}
|
|
1552
|
+
});
|
|
1553
|
+
default: {
|
|
1554
|
+
const _exhaustiveCheck = privateKey;
|
|
1555
|
+
throw new Error(`Unsupported private key type: ${String(_exhaustiveCheck)}`);
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
function getKeyRefFromIdentity(identity) {
|
|
1560
|
+
const { privateKey } = identity;
|
|
1561
|
+
switch (privateKey.type) {
|
|
1562
|
+
case "file":
|
|
1563
|
+
return privateKey.path;
|
|
1564
|
+
case "keychain":
|
|
1565
|
+
return `${privateKey.service}:${privateKey.account}`;
|
|
1566
|
+
case "1password":
|
|
1567
|
+
return privateKey.item;
|
|
1568
|
+
default: {
|
|
1569
|
+
const _exhaustiveCheck = privateKey;
|
|
1570
|
+
throw new Error(`Unsupported private key type: ${String(_exhaustiveCheck)}`);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
function KeygenInteractive(props) {
|
|
1575
|
+
const { onComplete, onError } = props;
|
|
1576
|
+
const [step, setStep] = React7.useState("checking-providers");
|
|
1577
|
+
const [opAvailable, setOpAvailable] = React7.useState(false);
|
|
1578
|
+
const [keychainAvailable, setKeychainAvailable] = React7.useState(false);
|
|
1579
|
+
const [accounts, setAccounts] = React7.useState([]);
|
|
1580
|
+
const [vaults, setVaults] = React7.useState([]);
|
|
1581
|
+
const [_selectedProvider, setSelectedProvider] = React7.useState();
|
|
1582
|
+
const [selectedAccount, setSelectedAccount] = React7.useState();
|
|
1583
|
+
const [selectedVault, setSelectedVault] = React7.useState();
|
|
1584
|
+
const [itemName, setItemName] = React7.useState("attest-it-private-key");
|
|
1585
|
+
const [keychainItemName, setKeychainItemName] = React7.useState("attest-it-private-key");
|
|
1586
|
+
React7.useEffect(() => {
|
|
1587
|
+
const checkProviders = async () => {
|
|
1588
|
+
try {
|
|
1589
|
+
const isInstalled = await core.OnePasswordKeyProvider.isInstalled();
|
|
1590
|
+
setOpAvailable(isInstalled);
|
|
1591
|
+
if (isInstalled) {
|
|
1592
|
+
const accountList = await core.OnePasswordKeyProvider.listAccounts();
|
|
1593
|
+
setAccounts(accountList);
|
|
1594
|
+
}
|
|
1595
|
+
} catch {
|
|
1596
|
+
setOpAvailable(false);
|
|
1597
|
+
}
|
|
1598
|
+
const isKeychainAvailable = core.MacOSKeychainKeyProvider.isAvailable();
|
|
1599
|
+
setKeychainAvailable(isKeychainAvailable);
|
|
1600
|
+
setStep("select-provider");
|
|
1601
|
+
};
|
|
1602
|
+
void checkProviders();
|
|
1603
|
+
}, []);
|
|
1604
|
+
React7.useEffect(() => {
|
|
1605
|
+
if (step === "select-vault" && selectedAccount) {
|
|
1606
|
+
const fetchVaults = async () => {
|
|
1607
|
+
try {
|
|
1608
|
+
const vaultList = await core.OnePasswordKeyProvider.listVaults(selectedAccount);
|
|
1609
|
+
setVaults(vaultList);
|
|
1610
|
+
} catch (err) {
|
|
1611
|
+
onError(err instanceof Error ? err : new Error("Failed to fetch vaults"));
|
|
1612
|
+
}
|
|
1613
|
+
};
|
|
1614
|
+
void fetchVaults();
|
|
1615
|
+
}
|
|
1616
|
+
}, [step, selectedAccount, onError]);
|
|
1617
|
+
const handleProviderSelect = (value) => {
|
|
1618
|
+
if (value === "filesystem") {
|
|
1619
|
+
setSelectedProvider("filesystem");
|
|
1620
|
+
void generateKeys("filesystem");
|
|
1621
|
+
} else if (value === "1password") {
|
|
1622
|
+
setSelectedProvider("1password");
|
|
1623
|
+
if (accounts.length === 1 && accounts[0]) {
|
|
1624
|
+
setSelectedAccount(accounts[0].email);
|
|
1625
|
+
setStep("select-vault");
|
|
1626
|
+
} else {
|
|
1627
|
+
setStep("select-account");
|
|
1628
|
+
}
|
|
1629
|
+
} else if (value === "macos-keychain") {
|
|
1630
|
+
setSelectedProvider("macos-keychain");
|
|
1631
|
+
setStep("enter-keychain-item-name");
|
|
1632
|
+
}
|
|
1633
|
+
};
|
|
1634
|
+
const handleAccountSelect = (value) => {
|
|
1635
|
+
setSelectedAccount(value);
|
|
1636
|
+
setStep("select-vault");
|
|
1637
|
+
};
|
|
1638
|
+
const handleVaultSelect = (value) => {
|
|
1639
|
+
setSelectedVault(value);
|
|
1640
|
+
setStep("enter-item-name");
|
|
1641
|
+
};
|
|
1642
|
+
const handleItemNameSubmit = (value) => {
|
|
1643
|
+
setItemName(value);
|
|
1644
|
+
void generateKeys("1password");
|
|
1645
|
+
};
|
|
1646
|
+
const handleKeychainItemNameSubmit = (value) => {
|
|
1647
|
+
setKeychainItemName(value);
|
|
1648
|
+
void generateKeys("macos-keychain");
|
|
1649
|
+
};
|
|
1650
|
+
const generateKeys = async (provider) => {
|
|
1651
|
+
setStep("generating");
|
|
1652
|
+
try {
|
|
1653
|
+
const publicKeyPath = props.publicKeyPath ?? core.getDefaultPublicKeyPath();
|
|
1654
|
+
if (provider === "filesystem") {
|
|
1655
|
+
const fsProvider = new core.FilesystemKeyProvider();
|
|
1656
|
+
const genOptions = { publicKeyPath };
|
|
1657
|
+
if (props.force !== void 0) {
|
|
1658
|
+
genOptions.force = props.force;
|
|
1659
|
+
}
|
|
1660
|
+
const result = await fsProvider.generateKeyPair(genOptions);
|
|
1661
|
+
onComplete({
|
|
1662
|
+
provider: "filesystem",
|
|
1663
|
+
publicKeyPath: result.publicKeyPath,
|
|
1664
|
+
privateKeyRef: result.privateKeyRef,
|
|
1665
|
+
storageDescription: result.storageDescription
|
|
1666
|
+
});
|
|
1667
|
+
} else if (provider === "1password") {
|
|
1668
|
+
if (!selectedVault || !itemName) {
|
|
1669
|
+
throw new Error("Vault and item name are required for 1Password");
|
|
1670
|
+
}
|
|
1671
|
+
const providerOptions = {
|
|
1672
|
+
vault: selectedVault,
|
|
1673
|
+
itemName
|
|
1674
|
+
};
|
|
1675
|
+
if (selectedAccount !== void 0) {
|
|
1676
|
+
providerOptions.account = selectedAccount;
|
|
1677
|
+
}
|
|
1678
|
+
const opProvider = new core.OnePasswordKeyProvider(providerOptions);
|
|
1679
|
+
const genOptions = { publicKeyPath };
|
|
1680
|
+
if (props.force !== void 0) {
|
|
1681
|
+
genOptions.force = props.force;
|
|
1682
|
+
}
|
|
1683
|
+
const result = await opProvider.generateKeyPair(genOptions);
|
|
1684
|
+
const completionResult = {
|
|
1685
|
+
provider: "1password",
|
|
1686
|
+
publicKeyPath: result.publicKeyPath,
|
|
1687
|
+
privateKeyRef: result.privateKeyRef,
|
|
1688
|
+
storageDescription: result.storageDescription,
|
|
1689
|
+
vault: selectedVault,
|
|
1690
|
+
itemName
|
|
1691
|
+
};
|
|
1692
|
+
if (selectedAccount !== void 0) {
|
|
1693
|
+
completionResult.account = selectedAccount;
|
|
1694
|
+
}
|
|
1695
|
+
onComplete(completionResult);
|
|
1696
|
+
} else {
|
|
1697
|
+
if (!keychainItemName) {
|
|
1698
|
+
throw new Error("Item name is required for macOS Keychain");
|
|
1699
|
+
}
|
|
1700
|
+
const keychainProvider = new core.MacOSKeychainKeyProvider({
|
|
1701
|
+
itemName: keychainItemName
|
|
1702
|
+
});
|
|
1703
|
+
const genOptions = { publicKeyPath };
|
|
1704
|
+
if (props.force !== void 0) {
|
|
1705
|
+
genOptions.force = props.force;
|
|
1706
|
+
}
|
|
1707
|
+
const result = await keychainProvider.generateKeyPair(genOptions);
|
|
1708
|
+
onComplete({
|
|
1709
|
+
provider: "macos-keychain",
|
|
1710
|
+
publicKeyPath: result.publicKeyPath,
|
|
1711
|
+
privateKeyRef: result.privateKeyRef,
|
|
1712
|
+
storageDescription: result.storageDescription,
|
|
1713
|
+
itemName: keychainItemName
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
setStep("done");
|
|
1717
|
+
} catch (err) {
|
|
1718
|
+
onError(err instanceof Error ? err : new Error("Key generation failed"));
|
|
1719
|
+
}
|
|
1720
|
+
};
|
|
1721
|
+
if (step === "checking-providers") {
|
|
1722
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { flexDirection: "column", children: /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", gap: 1, children: [
|
|
1723
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Spinner, {}),
|
|
1724
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: "Checking available key storage providers..." })
|
|
1725
|
+
] }) });
|
|
1726
|
+
}
|
|
1727
|
+
if (step === "select-provider") {
|
|
1728
|
+
const options = [
|
|
1729
|
+
{
|
|
1730
|
+
label: `Local Filesystem (${core.getDefaultPrivateKeyPath()})`,
|
|
1731
|
+
value: "filesystem"
|
|
1732
|
+
}
|
|
1733
|
+
];
|
|
1734
|
+
if (keychainAvailable) {
|
|
1735
|
+
options.push({
|
|
1736
|
+
label: "macOS Keychain",
|
|
1737
|
+
value: "macos-keychain"
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
if (opAvailable) {
|
|
1741
|
+
options.push({
|
|
1742
|
+
label: "1Password (requires op CLI)",
|
|
1743
|
+
value: "1password"
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
|
|
1747
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Where would you like to store your private key?" }),
|
|
1748
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "" }),
|
|
1749
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Select, { options, onChange: handleProviderSelect })
|
|
1750
|
+
] });
|
|
1751
|
+
}
|
|
1752
|
+
if (step === "select-account") {
|
|
1753
|
+
const options = accounts.map((account) => ({
|
|
1754
|
+
label: account.email,
|
|
1755
|
+
value: account.email
|
|
1756
|
+
}));
|
|
1757
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
|
|
1758
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Select 1Password account:" }),
|
|
1759
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "" }),
|
|
1760
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Select, { options, onChange: handleAccountSelect })
|
|
1761
|
+
] });
|
|
1762
|
+
}
|
|
1763
|
+
if (step === "select-vault") {
|
|
1764
|
+
if (vaults.length === 0) {
|
|
1765
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { flexDirection: "column", children: /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", gap: 1, children: [
|
|
1766
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Spinner, {}),
|
|
1767
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: "Loading vaults..." })
|
|
1768
|
+
] }) });
|
|
1769
|
+
}
|
|
1770
|
+
const options = vaults.map((vault) => ({
|
|
1771
|
+
label: vault.name,
|
|
1772
|
+
value: vault.name
|
|
1773
|
+
}));
|
|
1774
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
|
|
1775
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Select vault for private key storage:" }),
|
|
1776
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "" }),
|
|
1777
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Select, { options, onChange: handleVaultSelect })
|
|
1778
|
+
] });
|
|
1779
|
+
}
|
|
1780
|
+
if (step === "enter-item-name") {
|
|
1781
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
|
|
1782
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Enter name for the key item:" }),
|
|
1783
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "(This will be visible in your 1Password vault)" }),
|
|
1784
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "" }),
|
|
1785
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.TextInput, { defaultValue: itemName, onSubmit: handleItemNameSubmit })
|
|
1786
|
+
] });
|
|
1787
|
+
}
|
|
1788
|
+
if (step === "enter-keychain-item-name") {
|
|
1789
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
|
|
1790
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Enter name for the keychain item:" }),
|
|
1791
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "(This will be the service name in your macOS Keychain)" }),
|
|
1792
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "" }),
|
|
1793
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.TextInput, { defaultValue: keychainItemName, onSubmit: handleKeychainItemNameSubmit })
|
|
1794
|
+
] });
|
|
1795
|
+
}
|
|
1796
|
+
if (step === "generating") {
|
|
1797
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { flexDirection: "column", children: /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", gap: 1, children: [
|
|
1798
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Spinner, {}),
|
|
1799
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: "Generating RSA-2048 keypair..." })
|
|
1800
|
+
] }) });
|
|
1801
|
+
}
|
|
1802
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ink.Box, {});
|
|
1803
|
+
}
|
|
1804
|
+
async function runKeygenInteractive(options) {
|
|
1805
|
+
return new Promise((resolve2, reject) => {
|
|
1806
|
+
const props = {
|
|
1807
|
+
onComplete: (result) => {
|
|
1808
|
+
unmount();
|
|
1809
|
+
resolve2(result);
|
|
1810
|
+
},
|
|
1811
|
+
onCancel: () => {
|
|
1812
|
+
unmount();
|
|
1813
|
+
reject(new Error("Keygen cancelled"));
|
|
1814
|
+
},
|
|
1815
|
+
onError: (error2) => {
|
|
1816
|
+
unmount();
|
|
1817
|
+
reject(error2);
|
|
1818
|
+
}
|
|
1819
|
+
};
|
|
1820
|
+
if (options.publicKeyPath !== void 0) {
|
|
1821
|
+
props.publicKeyPath = options.publicKeyPath;
|
|
1822
|
+
}
|
|
1823
|
+
if (options.force !== void 0) {
|
|
1824
|
+
props.force = options.force;
|
|
1825
|
+
}
|
|
1826
|
+
const { unmount } = ink.render(/* @__PURE__ */ jsxRuntime.jsx(KeygenInteractive, { ...props }));
|
|
1827
|
+
});
|
|
1347
1828
|
}
|
|
1348
|
-
|
|
1829
|
+
|
|
1830
|
+
// src/commands/keygen.ts
|
|
1831
|
+
var keygenCommand = new commander.Command("keygen").description("Generate a new RSA keypair for signing attestations").option("-o, --output <path>", "Public key output path").option("-p, --private <path>", "Private key output path (filesystem only)").option(
|
|
1832
|
+
"--provider <type>",
|
|
1833
|
+
"Key provider: filesystem, 1password, or macos-keychain (skips interactive)"
|
|
1834
|
+
).option("--vault <name>", "1Password vault name").option("--item-name <name>", "1Password/macOS Keychain item name").option("--account <email>", "1Password account").option("-f, --force", "Overwrite existing keys").option("--no-interactive", "Disable interactive mode").action(async (options) => {
|
|
1349
1835
|
await runKeygen(options);
|
|
1350
1836
|
});
|
|
1351
1837
|
async function runKeygen(options) {
|
|
1352
1838
|
try {
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1839
|
+
const useInteractive = options.interactive !== false && !options.provider;
|
|
1840
|
+
if (useInteractive) {
|
|
1841
|
+
const interactiveOptions = {};
|
|
1842
|
+
if (options.output !== void 0) {
|
|
1843
|
+
interactiveOptions.publicKeyPath = options.output;
|
|
1844
|
+
}
|
|
1845
|
+
if (options.force !== void 0) {
|
|
1846
|
+
interactiveOptions.force = options.force;
|
|
1847
|
+
}
|
|
1848
|
+
const result = await runKeygenInteractive(interactiveOptions);
|
|
1849
|
+
success("Keypair generated successfully!");
|
|
1850
|
+
log("");
|
|
1851
|
+
log("Private key stored in:");
|
|
1852
|
+
log(` ${result.storageDescription}`);
|
|
1853
|
+
log("");
|
|
1854
|
+
log("Public key (commit to repo):");
|
|
1855
|
+
log(` ${result.publicKeyPath}`);
|
|
1856
|
+
log("");
|
|
1857
|
+
if (result.provider === "1password") {
|
|
1858
|
+
log("Add to your .attest-it/config.yaml:");
|
|
1859
|
+
log("");
|
|
1860
|
+
log("settings:");
|
|
1861
|
+
log(` publicKeyPath: ${result.publicKeyPath}`);
|
|
1862
|
+
log(" keyProvider:");
|
|
1863
|
+
log(" type: 1password");
|
|
1864
|
+
log(" options:");
|
|
1865
|
+
if (result.account) {
|
|
1866
|
+
log(` account: ${result.account}`);
|
|
1867
|
+
}
|
|
1868
|
+
log(` vault: ${result.vault ?? ""}`);
|
|
1869
|
+
log(` itemName: ${result.itemName ?? ""}`);
|
|
1870
|
+
log("");
|
|
1871
|
+
} else if (result.provider === "macos-keychain") {
|
|
1872
|
+
log("Add to your .attest-it/config.yaml:");
|
|
1873
|
+
log("");
|
|
1874
|
+
log("settings:");
|
|
1875
|
+
log(` publicKeyPath: ${result.publicKeyPath}`);
|
|
1876
|
+
log(" keyProvider:");
|
|
1877
|
+
log(" type: macos-keychain");
|
|
1878
|
+
log(" options:");
|
|
1879
|
+
log(` itemName: ${result.itemName ?? ""}`);
|
|
1880
|
+
log("");
|
|
1881
|
+
}
|
|
1882
|
+
log("Next steps:");
|
|
1883
|
+
log(` 1. git add ${result.publicKeyPath}`);
|
|
1884
|
+
if (result.provider === "1password" || result.provider === "macos-keychain") {
|
|
1885
|
+
log(" 2. Update .attest-it/config.yaml with keyProvider settings");
|
|
1886
|
+
} else {
|
|
1887
|
+
log(" 2. Update .attest-it/config.yaml publicKeyPath if needed");
|
|
1888
|
+
}
|
|
1889
|
+
log(" 3. attest-it run --suite <suite-name>");
|
|
1890
|
+
} else {
|
|
1891
|
+
await runNonInteractiveKeygen(options);
|
|
1892
|
+
}
|
|
1893
|
+
} catch (err) {
|
|
1894
|
+
if (err instanceof Error) {
|
|
1895
|
+
error(err.message);
|
|
1896
|
+
} else {
|
|
1897
|
+
error("Unknown error occurred");
|
|
1898
|
+
}
|
|
1899
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
async function runNonInteractiveKeygen(options) {
|
|
1903
|
+
log("Checking OpenSSL...");
|
|
1904
|
+
const version = await core.checkOpenSSL();
|
|
1905
|
+
info(`OpenSSL: ${version}`);
|
|
1906
|
+
const publicPath = options.output ?? core.getDefaultPublicKeyPath();
|
|
1907
|
+
if (options.provider === "1password") {
|
|
1908
|
+
if (!options.vault || !options.itemName) {
|
|
1909
|
+
throw new Error("--vault and --item-name are required for 1password provider");
|
|
1910
|
+
}
|
|
1911
|
+
const providerOptions = {
|
|
1912
|
+
vault: options.vault,
|
|
1913
|
+
itemName: options.itemName
|
|
1914
|
+
};
|
|
1915
|
+
if (options.account !== void 0) {
|
|
1916
|
+
providerOptions.account = options.account;
|
|
1917
|
+
}
|
|
1918
|
+
const provider = new core.OnePasswordKeyProvider(providerOptions);
|
|
1919
|
+
log(`Generating keypair with 1Password storage...`);
|
|
1920
|
+
log(`Vault: ${options.vault}`);
|
|
1921
|
+
log(`Item: ${options.itemName}`);
|
|
1922
|
+
const genOptions = { publicKeyPath: publicPath };
|
|
1923
|
+
if (options.force !== void 0) {
|
|
1924
|
+
genOptions.force = options.force;
|
|
1925
|
+
}
|
|
1926
|
+
const result = await provider.generateKeyPair(genOptions);
|
|
1927
|
+
success("Keypair generated successfully!");
|
|
1928
|
+
log("");
|
|
1929
|
+
log("Private key stored in:");
|
|
1930
|
+
log(` ${result.storageDescription}`);
|
|
1931
|
+
log("");
|
|
1932
|
+
log("Public key (commit to repo):");
|
|
1933
|
+
log(` ${result.publicKeyPath}`);
|
|
1934
|
+
} else if (options.provider === "macos-keychain") {
|
|
1935
|
+
if (!options.itemName) {
|
|
1936
|
+
throw new Error("--item-name is required for macos-keychain provider");
|
|
1937
|
+
}
|
|
1938
|
+
const isAvailable = core.MacOSKeychainKeyProvider.isAvailable();
|
|
1939
|
+
if (!isAvailable) {
|
|
1940
|
+
throw new Error("macOS Keychain is not available on this platform");
|
|
1941
|
+
}
|
|
1942
|
+
const provider = new core.MacOSKeychainKeyProvider({
|
|
1943
|
+
itemName: options.itemName
|
|
1944
|
+
});
|
|
1945
|
+
log(`Generating keypair with macOS Keychain storage...`);
|
|
1946
|
+
log(`Item: ${options.itemName}`);
|
|
1947
|
+
const genOptions = { publicKeyPath: publicPath };
|
|
1948
|
+
if (options.force !== void 0) {
|
|
1949
|
+
genOptions.force = options.force;
|
|
1950
|
+
}
|
|
1951
|
+
const result = await provider.generateKeyPair(genOptions);
|
|
1952
|
+
success("Keypair generated successfully!");
|
|
1953
|
+
log("");
|
|
1954
|
+
log("Private key stored in:");
|
|
1955
|
+
log(` ${result.storageDescription}`);
|
|
1956
|
+
log("");
|
|
1957
|
+
log("Public key (commit to repo):");
|
|
1958
|
+
log(` ${result.publicKeyPath}`);
|
|
1959
|
+
} else {
|
|
1960
|
+
const privatePath = options.private ?? core.getDefaultPrivateKeyPath();
|
|
1358
1961
|
log(`Private key: ${privatePath}`);
|
|
1359
1962
|
log(`Public key: ${publicPath}`);
|
|
1360
1963
|
const privateExists = fs__namespace.existsSync(privatePath);
|
|
@@ -1389,21 +1992,14 @@ async function runKeygen(options) {
|
|
|
1389
1992
|
log("");
|
|
1390
1993
|
log("Public key (commit to repo):");
|
|
1391
1994
|
log(` ${result.publicPath}`);
|
|
1392
|
-
log("");
|
|
1393
|
-
info("Important: Back up your private key securely!");
|
|
1394
|
-
log("");
|
|
1395
|
-
log("Next steps:");
|
|
1396
|
-
log(` 1. git add ${result.publicPath}`);
|
|
1397
|
-
log(" 2. Update .attest-it/config.yaml publicKeyPath if needed");
|
|
1398
|
-
log(" 3. attest-it run --suite <suite-name>");
|
|
1399
|
-
} catch (err) {
|
|
1400
|
-
if (err instanceof Error) {
|
|
1401
|
-
error(err.message);
|
|
1402
|
-
} else {
|
|
1403
|
-
error("Unknown error occurred");
|
|
1404
|
-
}
|
|
1405
|
-
process.exit(ExitCode.CONFIG_ERROR);
|
|
1406
1995
|
}
|
|
1996
|
+
log("");
|
|
1997
|
+
info("Important: Back up your private key securely!");
|
|
1998
|
+
log("");
|
|
1999
|
+
log("Next steps:");
|
|
2000
|
+
log(` 1. git add ${publicPath}`);
|
|
2001
|
+
log(" 2. Update .attest-it/config.yaml publicKeyPath if needed");
|
|
2002
|
+
log(" 3. attest-it run --suite <suite-name>");
|
|
1407
2003
|
}
|
|
1408
2004
|
var pruneCommand = new commander.Command("prune").description("Remove stale attestations").option("-n, --dry-run", "Show what would be removed without removing").option("-k, --keep-days <n>", "Keep attestations newer than n days", "30").action(async (options) => {
|
|
1409
2005
|
await runPrune(options);
|
|
@@ -1436,7 +2032,7 @@ async function runPrune(options) {
|
|
|
1436
2032
|
let fingerprintMatches = false;
|
|
1437
2033
|
if (suiteExists) {
|
|
1438
2034
|
const suiteConfig = config.suites[attestation.suite];
|
|
1439
|
-
if (suiteConfig) {
|
|
2035
|
+
if (suiteConfig?.packages) {
|
|
1440
2036
|
const fingerprintOptions = {
|
|
1441
2037
|
packages: suiteConfig.packages,
|
|
1442
2038
|
...suiteConfig.ignore && { ignore: suiteConfig.ignore }
|
|
@@ -1497,57 +2093,1228 @@ async function runPrune(options) {
|
|
|
1497
2093
|
return;
|
|
1498
2094
|
}
|
|
1499
2095
|
}
|
|
1500
|
-
var verifyCommand = new commander.Command("verify").description("Verify all
|
|
1501
|
-
await runVerify(options);
|
|
2096
|
+
var verifyCommand = new commander.Command("verify").description("Verify all gate seals (for CI)").argument("[gates...]", "Verify specific gates only").option("--json", "Output JSON for machine parsing").action(async (gates, options) => {
|
|
2097
|
+
await runVerify(gates, options);
|
|
1502
2098
|
});
|
|
1503
|
-
async function runVerify(options) {
|
|
2099
|
+
async function runVerify(gates, options) {
|
|
1504
2100
|
try {
|
|
1505
2101
|
const config = await core.loadConfig();
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
2102
|
+
const attestItConfig = core.toAttestItConfig(config);
|
|
2103
|
+
if (!attestItConfig.gates || Object.keys(attestItConfig.gates).length === 0) {
|
|
2104
|
+
error("No gates defined in configuration");
|
|
2105
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
2106
|
+
}
|
|
2107
|
+
const projectRoot = process.cwd();
|
|
2108
|
+
const sealsFile = core.readSealsSync(projectRoot);
|
|
2109
|
+
const gatesToVerify = gates.length > 0 ? gates : Object.keys(attestItConfig.gates);
|
|
2110
|
+
for (const gateId of gatesToVerify) {
|
|
2111
|
+
if (!attestItConfig.gates[gateId]) {
|
|
2112
|
+
error(`Gate '${gateId}' not found in configuration`);
|
|
1514
2113
|
process.exit(ExitCode.CONFIG_ERROR);
|
|
1515
2114
|
}
|
|
1516
|
-
const filteredConfig = {
|
|
1517
|
-
version: config.version,
|
|
1518
|
-
settings: config.settings,
|
|
1519
|
-
suites: { [options.suite]: filteredSuiteEntry }
|
|
1520
|
-
};
|
|
1521
|
-
const result2 = await core.verifyAttestations({ config: core.toAttestItConfig(filteredConfig) });
|
|
1522
|
-
if (options.json) {
|
|
1523
|
-
outputJson(result2);
|
|
1524
|
-
} else {
|
|
1525
|
-
displayResults(result2, filteredConfig.settings.maxAgeDays, options.strict);
|
|
1526
|
-
}
|
|
1527
|
-
if (!result2.success) {
|
|
1528
|
-
process.exit(ExitCode.FAILURE);
|
|
1529
|
-
return;
|
|
1530
|
-
}
|
|
1531
|
-
if (options.strict && hasWarnings(result2, filteredConfig.settings.maxAgeDays)) {
|
|
1532
|
-
process.exit(ExitCode.FAILURE);
|
|
1533
|
-
return;
|
|
1534
|
-
}
|
|
1535
|
-
process.exit(ExitCode.SUCCESS);
|
|
1536
|
-
return;
|
|
1537
2115
|
}
|
|
1538
|
-
const
|
|
1539
|
-
|
|
1540
|
-
|
|
2116
|
+
const fingerprints = {};
|
|
2117
|
+
for (const gateId of gatesToVerify) {
|
|
2118
|
+
const gate = attestItConfig.gates[gateId];
|
|
2119
|
+
if (!gate) continue;
|
|
2120
|
+
const result = core.computeFingerprintSync({
|
|
2121
|
+
packages: gate.fingerprint.paths,
|
|
2122
|
+
...gate.fingerprint.exclude && { ignore: gate.fingerprint.exclude }
|
|
2123
|
+
});
|
|
2124
|
+
fingerprints[gateId] = result.fingerprint;
|
|
2125
|
+
}
|
|
2126
|
+
const results = gates.length > 0 ? gatesToVerify.map(
|
|
2127
|
+
(gateId) => (
|
|
2128
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
2129
|
+
core.verifyGateSeal(attestItConfig, gateId, sealsFile, fingerprints[gateId] ?? "")
|
|
2130
|
+
)
|
|
2131
|
+
) : core.verifyAllSeals(attestItConfig, sealsFile, fingerprints);
|
|
2132
|
+
if (options.json) {
|
|
2133
|
+
outputJson(results);
|
|
2134
|
+
} else {
|
|
2135
|
+
displayResults(results);
|
|
2136
|
+
}
|
|
2137
|
+
const hasInvalid = results.some(
|
|
2138
|
+
(r) => r.state === "MISSING" || r.state === "FINGERPRINT_MISMATCH" || r.state === "INVALID_SIGNATURE" || r.state === "UNKNOWN_SIGNER"
|
|
2139
|
+
);
|
|
2140
|
+
const hasStale = results.some((r) => r.state === "STALE");
|
|
2141
|
+
if (hasInvalid) {
|
|
2142
|
+
process.exit(ExitCode.FAILURE);
|
|
2143
|
+
} else if (hasStale) {
|
|
2144
|
+
process.exit(ExitCode.SUCCESS);
|
|
2145
|
+
} else {
|
|
2146
|
+
process.exit(ExitCode.SUCCESS);
|
|
2147
|
+
}
|
|
2148
|
+
} catch (err) {
|
|
2149
|
+
if (err instanceof Error) {
|
|
2150
|
+
error(err.message);
|
|
2151
|
+
} else {
|
|
2152
|
+
error("Unknown error occurred");
|
|
2153
|
+
}
|
|
2154
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
function displayResults(results) {
|
|
2158
|
+
log("");
|
|
2159
|
+
const tableRows = results.map((r) => ({
|
|
2160
|
+
suite: r.gateId,
|
|
2161
|
+
status: colorizeState2(r.state),
|
|
2162
|
+
fingerprint: formatFingerprint(r),
|
|
2163
|
+
age: formatAge2(r)
|
|
2164
|
+
}));
|
|
2165
|
+
log(formatTable(tableRows));
|
|
2166
|
+
log("");
|
|
2167
|
+
const withIssues = results.filter(
|
|
2168
|
+
(r) => r.state !== "VALID" && r.state !== "STALE" && // STALE gets its own warning below
|
|
2169
|
+
r.message
|
|
2170
|
+
);
|
|
2171
|
+
if (withIssues.length > 0) {
|
|
2172
|
+
for (const result of withIssues) {
|
|
2173
|
+
if (result.message) {
|
|
2174
|
+
log(`${result.gateId}: ${result.message}`);
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
log("");
|
|
2178
|
+
}
|
|
2179
|
+
const validCount = results.filter((r) => r.state === "VALID").length;
|
|
2180
|
+
const staleCount = results.filter((r) => r.state === "STALE").length;
|
|
2181
|
+
const invalidCount = results.length - validCount - staleCount;
|
|
2182
|
+
if (invalidCount === 0 && staleCount === 0) {
|
|
2183
|
+
success("All gate seals valid");
|
|
2184
|
+
} else {
|
|
2185
|
+
if (invalidCount > 0) {
|
|
2186
|
+
error(`${String(invalidCount)} gate(s) have invalid or missing seals`);
|
|
2187
|
+
log("Run `attest-it seal` to create seals for these gates");
|
|
2188
|
+
}
|
|
2189
|
+
if (staleCount > 0) {
|
|
2190
|
+
warn(`${String(staleCount)} gate(s) have stale seals (exceeds maxAge)`);
|
|
2191
|
+
log("Run `attest-it seal --force <gate>` to update stale seals");
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
function colorizeState2(state) {
|
|
2196
|
+
const theme3 = getTheme();
|
|
2197
|
+
switch (state) {
|
|
2198
|
+
case "VALID":
|
|
2199
|
+
return theme3.green(state);
|
|
2200
|
+
case "MISSING":
|
|
2201
|
+
case "STALE":
|
|
2202
|
+
return theme3.yellow(state);
|
|
2203
|
+
case "FINGERPRINT_MISMATCH":
|
|
2204
|
+
case "INVALID_SIGNATURE":
|
|
2205
|
+
case "UNKNOWN_SIGNER":
|
|
2206
|
+
return theme3.red(state);
|
|
2207
|
+
default:
|
|
2208
|
+
return state;
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
function formatFingerprint(result) {
|
|
2212
|
+
if (result.seal?.fingerprint) {
|
|
2213
|
+
const fp = result.seal.fingerprint;
|
|
2214
|
+
if (fp.length > 16) {
|
|
2215
|
+
return fp.slice(0, 16) + "...";
|
|
2216
|
+
}
|
|
2217
|
+
return fp;
|
|
2218
|
+
}
|
|
2219
|
+
return result.state === "MISSING" ? "(none)" : "-";
|
|
2220
|
+
}
|
|
2221
|
+
function formatAge2(result) {
|
|
2222
|
+
if (result.seal?.timestamp) {
|
|
2223
|
+
const timestamp = new Date(result.seal.timestamp);
|
|
2224
|
+
const now = Date.now();
|
|
2225
|
+
const ageMs = now - timestamp.getTime();
|
|
2226
|
+
const ageDays = Math.floor(ageMs / (1e3 * 60 * 60 * 24));
|
|
2227
|
+
if (result.state === "STALE") {
|
|
2228
|
+
return `${String(ageDays)} days (stale)`;
|
|
2229
|
+
}
|
|
2230
|
+
return `${String(ageDays)} days`;
|
|
2231
|
+
}
|
|
2232
|
+
switch (result.state) {
|
|
2233
|
+
case "MISSING":
|
|
2234
|
+
return "(none)";
|
|
2235
|
+
case "FINGERPRINT_MISMATCH":
|
|
2236
|
+
return "(changed)";
|
|
2237
|
+
default:
|
|
2238
|
+
return "-";
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
var sealCommand = new commander.Command("seal").description("Create seals for gates").argument("[gates...]", "Gate IDs to seal (defaults to all gates without valid seals)").option("--force", "Force seal creation even if gate already has a valid seal").option("--dry-run", "Show what would be sealed without creating seals").action(async (gates, options) => {
|
|
2242
|
+
await runSeal(gates, options);
|
|
2243
|
+
});
|
|
2244
|
+
async function runSeal(gates, options) {
|
|
2245
|
+
try {
|
|
2246
|
+
const config = await core.loadConfig();
|
|
2247
|
+
const attestItConfig = core.toAttestItConfig(config);
|
|
2248
|
+
if (!attestItConfig.gates || Object.keys(attestItConfig.gates).length === 0) {
|
|
2249
|
+
error("No gates defined in configuration");
|
|
2250
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
2251
|
+
}
|
|
2252
|
+
const localConfig = core.loadLocalConfigSync();
|
|
2253
|
+
if (!localConfig) {
|
|
2254
|
+
error("No local identity configuration found");
|
|
2255
|
+
error('Run "attest-it keygen" first to set up your identity');
|
|
2256
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
2257
|
+
}
|
|
2258
|
+
const identity = core.getActiveIdentity(localConfig);
|
|
2259
|
+
if (!identity) {
|
|
2260
|
+
error(`Active identity '${localConfig.activeIdentity}' not found in local config`);
|
|
2261
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
2262
|
+
}
|
|
2263
|
+
const projectRoot = process.cwd();
|
|
2264
|
+
const sealsFile = core.readSealsSync(projectRoot);
|
|
2265
|
+
const gatesToSeal = gates.length > 0 ? gates : getAllGateIds(attestItConfig);
|
|
2266
|
+
for (const gateId of gatesToSeal) {
|
|
2267
|
+
if (!attestItConfig.gates[gateId]) {
|
|
2268
|
+
error(`Gate '${gateId}' not found in configuration`);
|
|
2269
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
const summary = {
|
|
2273
|
+
sealed: [],
|
|
2274
|
+
skipped: [],
|
|
2275
|
+
failed: []
|
|
2276
|
+
};
|
|
2277
|
+
for (const gateId of gatesToSeal) {
|
|
2278
|
+
try {
|
|
2279
|
+
const result = await processSingleGate(gateId, attestItConfig, identity, sealsFile, options);
|
|
2280
|
+
if (result.sealed) {
|
|
2281
|
+
summary.sealed.push(gateId);
|
|
2282
|
+
} else if (result.skipped) {
|
|
2283
|
+
summary.skipped.push({ gate: gateId, reason: result.reason ?? "Unknown" });
|
|
2284
|
+
}
|
|
2285
|
+
} catch (err) {
|
|
2286
|
+
const errorMsg = err instanceof Error ? err.message : "Unknown error";
|
|
2287
|
+
summary.failed.push({ gate: gateId, error: errorMsg });
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
if (!options.dryRun && summary.sealed.length > 0) {
|
|
2291
|
+
core.writeSealsSync(projectRoot, sealsFile);
|
|
2292
|
+
}
|
|
2293
|
+
displaySummary(summary, options.dryRun);
|
|
2294
|
+
if (summary.failed.length > 0) {
|
|
2295
|
+
process.exit(ExitCode.FAILURE);
|
|
2296
|
+
} else if (summary.sealed.length === 0 && summary.skipped.length === 0) {
|
|
2297
|
+
process.exit(ExitCode.NO_WORK);
|
|
2298
|
+
} else {
|
|
2299
|
+
process.exit(ExitCode.SUCCESS);
|
|
2300
|
+
}
|
|
2301
|
+
} catch (err) {
|
|
2302
|
+
if (err instanceof Error) {
|
|
2303
|
+
error(err.message);
|
|
2304
|
+
} else {
|
|
2305
|
+
error("Unknown error occurred");
|
|
2306
|
+
}
|
|
2307
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
async function processSingleGate(gateId, config, identity, sealsFile, options) {
|
|
2311
|
+
verbose(`Processing gate: ${gateId}`);
|
|
2312
|
+
const gate = core.getGate(config, gateId);
|
|
2313
|
+
if (!gate) {
|
|
2314
|
+
return { sealed: false, skipped: true, reason: "Gate not found in configuration" };
|
|
2315
|
+
}
|
|
2316
|
+
const existingSeal = sealsFile.seals[gateId];
|
|
2317
|
+
if (existingSeal && !options.force) {
|
|
2318
|
+
return {
|
|
2319
|
+
sealed: false,
|
|
2320
|
+
skipped: true,
|
|
2321
|
+
reason: "Gate already has a seal (use --force to override)"
|
|
2322
|
+
};
|
|
2323
|
+
}
|
|
2324
|
+
const fingerprintResult = core.computeFingerprintSync({
|
|
2325
|
+
packages: gate.fingerprint.paths,
|
|
2326
|
+
...gate.fingerprint.exclude && { ignore: gate.fingerprint.exclude }
|
|
2327
|
+
});
|
|
2328
|
+
verbose(` Fingerprint: ${fingerprintResult.fingerprint}`);
|
|
2329
|
+
const authorized = core.isAuthorizedSigner(config, gateId, identity.publicKey);
|
|
2330
|
+
if (!authorized) {
|
|
2331
|
+
return {
|
|
2332
|
+
sealed: false,
|
|
2333
|
+
skipped: true,
|
|
2334
|
+
reason: `Not authorized to seal this gate (authorized signers: ${gate.authorizedSigners.join(", ")})`
|
|
2335
|
+
};
|
|
2336
|
+
}
|
|
2337
|
+
if (options.dryRun) {
|
|
2338
|
+
log(` Would seal gate: ${gateId}`);
|
|
2339
|
+
return { sealed: true, skipped: false };
|
|
2340
|
+
}
|
|
2341
|
+
const keyProvider = createKeyProviderFromIdentity2(identity);
|
|
2342
|
+
const keyRef = getKeyRefFromIdentity2(identity);
|
|
2343
|
+
const keyResult = await keyProvider.getPrivateKey(keyRef);
|
|
2344
|
+
const fs4 = await import('fs/promises');
|
|
2345
|
+
const privateKeyPem = await fs4.readFile(keyResult.keyPath, "utf8");
|
|
2346
|
+
await keyResult.cleanup();
|
|
2347
|
+
const seal = core.createSeal({
|
|
2348
|
+
gateId,
|
|
2349
|
+
fingerprint: fingerprintResult.fingerprint,
|
|
2350
|
+
sealedBy: identity.name,
|
|
2351
|
+
privateKey: privateKeyPem
|
|
2352
|
+
});
|
|
2353
|
+
sealsFile.seals[gateId] = seal;
|
|
2354
|
+
log(` Sealed gate: ${gateId}`);
|
|
2355
|
+
verbose(` Sealed by: ${identity.name}`);
|
|
2356
|
+
verbose(` Timestamp: ${seal.timestamp}`);
|
|
2357
|
+
return { sealed: true, skipped: false };
|
|
2358
|
+
}
|
|
2359
|
+
function getAllGateIds(config) {
|
|
2360
|
+
return Object.keys(config.gates ?? {});
|
|
2361
|
+
}
|
|
2362
|
+
function displaySummary(summary, dryRun) {
|
|
2363
|
+
log("");
|
|
2364
|
+
const prefix = dryRun ? "Would seal" : "Sealed";
|
|
2365
|
+
if (summary.sealed.length > 0) {
|
|
2366
|
+
success(`${prefix} ${String(summary.sealed.length)} gate(s): ${summary.sealed.join(", ")}`);
|
|
2367
|
+
}
|
|
2368
|
+
if (summary.skipped.length > 0) {
|
|
2369
|
+
log("");
|
|
2370
|
+
warn(`Skipped ${String(summary.skipped.length)} gate(s):`);
|
|
2371
|
+
for (const skip of summary.skipped) {
|
|
2372
|
+
log(` ${skip.gate}: ${skip.reason}`);
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
if (summary.failed.length > 0) {
|
|
2376
|
+
log("");
|
|
2377
|
+
error(`Failed to seal ${String(summary.failed.length)} gate(s):`);
|
|
2378
|
+
for (const fail of summary.failed) {
|
|
2379
|
+
log(` ${fail.gate}: ${fail.error}`);
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
if (summary.sealed.length === 0 && summary.skipped.length === 0 && summary.failed.length === 0) {
|
|
2383
|
+
log("No gates to seal");
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
function createKeyProviderFromIdentity2(identity) {
|
|
2387
|
+
const { privateKey } = identity;
|
|
2388
|
+
switch (privateKey.type) {
|
|
2389
|
+
case "file":
|
|
2390
|
+
return core.KeyProviderRegistry.create({
|
|
2391
|
+
type: "filesystem",
|
|
2392
|
+
options: { privateKeyPath: privateKey.path }
|
|
2393
|
+
});
|
|
2394
|
+
case "keychain":
|
|
2395
|
+
return core.KeyProviderRegistry.create({
|
|
2396
|
+
type: "macos-keychain",
|
|
2397
|
+
options: {
|
|
2398
|
+
itemName: privateKey.service
|
|
2399
|
+
}
|
|
2400
|
+
});
|
|
2401
|
+
case "1password":
|
|
2402
|
+
return core.KeyProviderRegistry.create({
|
|
2403
|
+
type: "1password",
|
|
2404
|
+
options: {
|
|
2405
|
+
account: privateKey.account,
|
|
2406
|
+
vault: privateKey.vault,
|
|
2407
|
+
itemName: privateKey.item,
|
|
2408
|
+
field: privateKey.field
|
|
2409
|
+
}
|
|
2410
|
+
});
|
|
2411
|
+
default: {
|
|
2412
|
+
const _exhaustiveCheck = privateKey;
|
|
2413
|
+
throw new Error(`Unsupported private key type: ${String(_exhaustiveCheck)}`);
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
function getKeyRefFromIdentity2(identity) {
|
|
2418
|
+
const { privateKey } = identity;
|
|
2419
|
+
switch (privateKey.type) {
|
|
2420
|
+
case "file":
|
|
2421
|
+
return privateKey.path;
|
|
2422
|
+
case "keychain":
|
|
2423
|
+
return privateKey.service;
|
|
2424
|
+
case "1password":
|
|
2425
|
+
return privateKey.item;
|
|
2426
|
+
default: {
|
|
2427
|
+
const _exhaustiveCheck = privateKey;
|
|
2428
|
+
throw new Error(`Unsupported private key type: ${String(_exhaustiveCheck)}`);
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
var listCommand = new commander.Command("list").description("List all local identities").action(async () => {
|
|
2433
|
+
await runList();
|
|
2434
|
+
});
|
|
2435
|
+
async function runList() {
|
|
2436
|
+
try {
|
|
2437
|
+
const config = await core.loadLocalConfig();
|
|
2438
|
+
if (!config) {
|
|
2439
|
+
error("No identities configured");
|
|
2440
|
+
log("");
|
|
2441
|
+
log("Run: attest-it identity create");
|
|
2442
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
2443
|
+
}
|
|
2444
|
+
const theme3 = getTheme2();
|
|
2445
|
+
const identities = Object.entries(config.identities);
|
|
2446
|
+
log("");
|
|
2447
|
+
log(theme3.blue.bold()("Local Identities:"));
|
|
2448
|
+
log("");
|
|
2449
|
+
for (const [slug, identity] of identities) {
|
|
2450
|
+
const isActive = slug === config.activeIdentity;
|
|
2451
|
+
const marker = isActive ? theme3.green("\u2605") : " ";
|
|
2452
|
+
const nameDisplay = isActive ? theme3.green.bold()(identity.name) : identity.name;
|
|
2453
|
+
const keyPreview = identity.publicKey.slice(0, 12) + "...";
|
|
2454
|
+
let keyType;
|
|
2455
|
+
switch (identity.privateKey.type) {
|
|
2456
|
+
case "file":
|
|
2457
|
+
keyType = "file";
|
|
2458
|
+
break;
|
|
2459
|
+
case "keychain":
|
|
2460
|
+
keyType = "keychain";
|
|
2461
|
+
break;
|
|
2462
|
+
case "1password":
|
|
2463
|
+
keyType = "1password";
|
|
2464
|
+
break;
|
|
2465
|
+
}
|
|
2466
|
+
log(`${marker} ${theme3.blue(slug)}`);
|
|
2467
|
+
log(` Name: ${nameDisplay}`);
|
|
2468
|
+
if (identity.email) {
|
|
2469
|
+
log(` Email: ${identity.email}`);
|
|
2470
|
+
}
|
|
2471
|
+
if (identity.github) {
|
|
2472
|
+
log(` GitHub: ${identity.github}`);
|
|
2473
|
+
}
|
|
2474
|
+
log(` Public Key: ${keyPreview}`);
|
|
2475
|
+
log(` Key Type: ${keyType}`);
|
|
2476
|
+
log("");
|
|
2477
|
+
}
|
|
2478
|
+
if (identities.length === 1) {
|
|
2479
|
+
log(`1 identity configured`);
|
|
2480
|
+
} else {
|
|
2481
|
+
log(`${identities.length.toString()} identities configured`);
|
|
2482
|
+
}
|
|
2483
|
+
log("");
|
|
2484
|
+
} catch (err) {
|
|
2485
|
+
if (err instanceof Error) {
|
|
2486
|
+
error(err.message);
|
|
2487
|
+
} else {
|
|
2488
|
+
error("Unknown error occurred");
|
|
2489
|
+
}
|
|
2490
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
// src/commands/identity/validation.ts
|
|
2495
|
+
function validateSlug(value, existingIdentities) {
|
|
2496
|
+
const trimmed = value.trim();
|
|
2497
|
+
if (!trimmed) {
|
|
2498
|
+
return "Slug cannot be empty";
|
|
2499
|
+
}
|
|
2500
|
+
if (!/^[a-z0-9-]+$/.test(trimmed)) {
|
|
2501
|
+
return "Slug must contain only lowercase letters, numbers, and hyphens";
|
|
2502
|
+
}
|
|
2503
|
+
if (existingIdentities?.[trimmed]) {
|
|
2504
|
+
return `Identity "${trimmed}" already exists`;
|
|
2505
|
+
}
|
|
2506
|
+
return true;
|
|
2507
|
+
}
|
|
2508
|
+
var EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
2509
|
+
function validateEmail(value, required = false) {
|
|
2510
|
+
const trimmed = value.trim();
|
|
2511
|
+
if (!trimmed) {
|
|
2512
|
+
return required ? "Email cannot be empty" : true;
|
|
2513
|
+
}
|
|
2514
|
+
if (!EMAIL_REGEX.test(trimmed)) {
|
|
2515
|
+
return "Please enter a valid email address";
|
|
2516
|
+
}
|
|
2517
|
+
return true;
|
|
2518
|
+
}
|
|
2519
|
+
var createCommand = new commander.Command("create").description("Create a new identity with Ed25519 keypair").action(async () => {
|
|
2520
|
+
await runCreate();
|
|
2521
|
+
});
|
|
2522
|
+
async function runCreate() {
|
|
2523
|
+
try {
|
|
2524
|
+
const theme3 = getTheme();
|
|
2525
|
+
log("");
|
|
2526
|
+
log(theme3.blue.bold()("Create New Identity"));
|
|
2527
|
+
log("");
|
|
2528
|
+
const existingConfig = await core.loadLocalConfig();
|
|
2529
|
+
const slug = (await prompts.input({
|
|
2530
|
+
message: "Identity slug (unique identifier):",
|
|
2531
|
+
validate: (value) => validateSlug(value, existingConfig?.identities)
|
|
2532
|
+
})).trim();
|
|
2533
|
+
const name = await prompts.input({
|
|
2534
|
+
message: "Display name:",
|
|
2535
|
+
validate: (value) => {
|
|
2536
|
+
if (!value || value.trim().length === 0) {
|
|
2537
|
+
return "Name cannot be empty";
|
|
2538
|
+
}
|
|
2539
|
+
return true;
|
|
2540
|
+
}
|
|
2541
|
+
});
|
|
2542
|
+
const email = (await prompts.input({
|
|
2543
|
+
message: "Email (optional):",
|
|
2544
|
+
default: "",
|
|
2545
|
+
validate: validateEmail
|
|
2546
|
+
})).trim();
|
|
2547
|
+
const github = await prompts.input({
|
|
2548
|
+
message: "GitHub username (optional):",
|
|
2549
|
+
default: ""
|
|
2550
|
+
});
|
|
2551
|
+
info("Checking available key storage providers...");
|
|
2552
|
+
const opAvailable = await core.OnePasswordKeyProvider.isInstalled();
|
|
2553
|
+
const keychainAvailable = core.MacOSKeychainKeyProvider.isAvailable();
|
|
2554
|
+
const configDir = core.getAttestItConfigDir();
|
|
2555
|
+
const storageChoices = [
|
|
2556
|
+
{ name: `File system (${path.join(configDir, "keys")})`, value: "file" }
|
|
2557
|
+
];
|
|
2558
|
+
if (keychainAvailable) {
|
|
2559
|
+
storageChoices.push({ name: "macOS Keychain", value: "keychain" });
|
|
2560
|
+
}
|
|
2561
|
+
if (opAvailable) {
|
|
2562
|
+
storageChoices.push({ name: "1Password", value: "1password" });
|
|
2563
|
+
}
|
|
2564
|
+
const keyStorageType = await prompts.select({
|
|
2565
|
+
message: "Where should the private key be stored?",
|
|
2566
|
+
choices: storageChoices
|
|
2567
|
+
});
|
|
2568
|
+
log("");
|
|
2569
|
+
log("Generating Ed25519 keypair...");
|
|
2570
|
+
const keyPair = core.generateEd25519KeyPair();
|
|
2571
|
+
let privateKeyRef;
|
|
2572
|
+
let keyStorageDescription;
|
|
2573
|
+
switch (keyStorageType) {
|
|
2574
|
+
case "file": {
|
|
2575
|
+
const keysDir = path.join(core.getAttestItConfigDir(), "keys");
|
|
2576
|
+
await promises.mkdir(keysDir, { recursive: true });
|
|
2577
|
+
const keyPath = path.join(keysDir, `${slug}.pem`);
|
|
2578
|
+
await promises.writeFile(keyPath, keyPair.privateKey, { mode: 384 });
|
|
2579
|
+
privateKeyRef = { type: "file", path: keyPath };
|
|
2580
|
+
keyStorageDescription = keyPath;
|
|
2581
|
+
break;
|
|
2582
|
+
}
|
|
2583
|
+
case "keychain": {
|
|
2584
|
+
if (!core.MacOSKeychainKeyProvider.isAvailable()) {
|
|
2585
|
+
error("macOS Keychain is not available on this system");
|
|
2586
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
2587
|
+
}
|
|
2588
|
+
const keychains = await core.MacOSKeychainKeyProvider.listKeychains();
|
|
2589
|
+
if (keychains.length === 0) {
|
|
2590
|
+
throw new Error("No keychains found on this system");
|
|
2591
|
+
}
|
|
2592
|
+
const formatKeychainChoice = (kc) => {
|
|
2593
|
+
return `${theme3.blue.bold()(kc.name)} ${theme3.muted(`(${kc.path})`)}`;
|
|
2594
|
+
};
|
|
2595
|
+
let selectedKeychain;
|
|
2596
|
+
if (keychains.length === 1 && keychains[0]) {
|
|
2597
|
+
selectedKeychain = keychains[0];
|
|
2598
|
+
info(`Using keychain: ${formatKeychainChoice(selectedKeychain)}`);
|
|
2599
|
+
} else {
|
|
2600
|
+
const selectedPath = await prompts.select({
|
|
2601
|
+
message: "Select keychain:",
|
|
2602
|
+
choices: keychains.map((kc) => ({
|
|
2603
|
+
name: formatKeychainChoice(kc),
|
|
2604
|
+
value: kc.path
|
|
2605
|
+
}))
|
|
2606
|
+
});
|
|
2607
|
+
const foundKeychain = keychains.find((kc) => kc.path === selectedPath);
|
|
2608
|
+
if (!foundKeychain) {
|
|
2609
|
+
throw new Error("Selected keychain not found");
|
|
2610
|
+
}
|
|
2611
|
+
selectedKeychain = foundKeychain;
|
|
2612
|
+
}
|
|
2613
|
+
const keychainItemName = await prompts.input({
|
|
2614
|
+
message: "Keychain item name:",
|
|
2615
|
+
default: `attest-it-${slug}`,
|
|
2616
|
+
validate: (value) => {
|
|
2617
|
+
if (!value || value.trim().length === 0) {
|
|
2618
|
+
return "Item name cannot be empty";
|
|
2619
|
+
}
|
|
2620
|
+
return true;
|
|
2621
|
+
}
|
|
2622
|
+
});
|
|
2623
|
+
const { execFile } = await import('child_process');
|
|
2624
|
+
const { promisify } = await import('util');
|
|
2625
|
+
const execFileAsync = promisify(execFile);
|
|
2626
|
+
const encodedKey = Buffer.from(keyPair.privateKey).toString("base64");
|
|
2627
|
+
try {
|
|
2628
|
+
const addArgs = [
|
|
2629
|
+
"add-generic-password",
|
|
2630
|
+
"-a",
|
|
2631
|
+
"attest-it",
|
|
2632
|
+
"-s",
|
|
2633
|
+
keychainItemName,
|
|
2634
|
+
"-w",
|
|
2635
|
+
encodedKey,
|
|
2636
|
+
"-U",
|
|
2637
|
+
selectedKeychain.path
|
|
2638
|
+
];
|
|
2639
|
+
await execFileAsync("security", addArgs);
|
|
2640
|
+
} catch (err) {
|
|
2641
|
+
throw new Error(
|
|
2642
|
+
`Failed to store key in macOS Keychain: ${err instanceof Error ? err.message : String(err)}`
|
|
2643
|
+
);
|
|
2644
|
+
}
|
|
2645
|
+
privateKeyRef = {
|
|
2646
|
+
type: "keychain",
|
|
2647
|
+
service: keychainItemName,
|
|
2648
|
+
account: "attest-it",
|
|
2649
|
+
keychain: selectedKeychain.path
|
|
2650
|
+
};
|
|
2651
|
+
keyStorageDescription = `macOS Keychain: ${selectedKeychain.name}/${keychainItemName}`;
|
|
2652
|
+
break;
|
|
2653
|
+
}
|
|
2654
|
+
case "1password": {
|
|
2655
|
+
const accounts = await core.OnePasswordKeyProvider.listAccounts();
|
|
2656
|
+
if (accounts.length === 0) {
|
|
2657
|
+
throw new Error(
|
|
2658
|
+
'1Password CLI is installed but no accounts are signed in. Run "op signin" first.'
|
|
2659
|
+
);
|
|
2660
|
+
}
|
|
2661
|
+
const { execFile } = await import('child_process');
|
|
2662
|
+
const { promisify } = await import('util');
|
|
2663
|
+
const execFileAsync = promisify(execFile);
|
|
2664
|
+
const accountDetails = await Promise.all(
|
|
2665
|
+
accounts.map(async (acc) => {
|
|
2666
|
+
try {
|
|
2667
|
+
const { stdout } = await execFileAsync("op", [
|
|
2668
|
+
"account",
|
|
2669
|
+
"get",
|
|
2670
|
+
"--account",
|
|
2671
|
+
acc.user_uuid,
|
|
2672
|
+
"--format=json"
|
|
2673
|
+
]);
|
|
2674
|
+
const details = JSON.parse(stdout);
|
|
2675
|
+
const name2 = details !== null && typeof details === "object" && "name" in details && typeof details.name === "string" ? details.name : acc.url;
|
|
2676
|
+
return {
|
|
2677
|
+
url: acc.url,
|
|
2678
|
+
email: acc.email,
|
|
2679
|
+
name: name2
|
|
2680
|
+
};
|
|
2681
|
+
} catch {
|
|
2682
|
+
return {
|
|
2683
|
+
url: acc.url,
|
|
2684
|
+
email: acc.email,
|
|
2685
|
+
name: acc.url
|
|
2686
|
+
};
|
|
2687
|
+
}
|
|
2688
|
+
})
|
|
2689
|
+
);
|
|
2690
|
+
const formatAccountChoice = (acc) => {
|
|
2691
|
+
return `${theme3.blue.bold()(acc.name)} ${theme3.muted(`(${acc.url})`)}`;
|
|
2692
|
+
};
|
|
2693
|
+
let selectedAccount;
|
|
2694
|
+
if (accountDetails.length === 1 && accountDetails[0]) {
|
|
2695
|
+
selectedAccount = accountDetails[0].url;
|
|
2696
|
+
info(`Using 1Password account: ${formatAccountChoice(accountDetails[0])}`);
|
|
2697
|
+
} else {
|
|
2698
|
+
selectedAccount = await prompts.select({
|
|
2699
|
+
message: "Select 1Password account:",
|
|
2700
|
+
choices: accountDetails.map((acc) => ({
|
|
2701
|
+
name: formatAccountChoice(acc),
|
|
2702
|
+
value: acc.url
|
|
2703
|
+
}))
|
|
2704
|
+
});
|
|
2705
|
+
}
|
|
2706
|
+
const vaults = await core.OnePasswordKeyProvider.listVaults(selectedAccount);
|
|
2707
|
+
if (vaults.length === 0) {
|
|
2708
|
+
throw new Error(`No vaults found in 1Password account: ${selectedAccount}`);
|
|
2709
|
+
}
|
|
2710
|
+
const selectedVault = await prompts.select({
|
|
2711
|
+
message: "Select vault for private key storage:",
|
|
2712
|
+
choices: vaults.map((v) => ({
|
|
2713
|
+
name: v.name,
|
|
2714
|
+
value: v.name
|
|
2715
|
+
}))
|
|
2716
|
+
});
|
|
2717
|
+
const item = await prompts.input({
|
|
2718
|
+
message: "1Password item name:",
|
|
2719
|
+
default: `attest-it-${slug}`,
|
|
2720
|
+
validate: (value) => {
|
|
2721
|
+
if (!value || value.trim().length === 0) {
|
|
2722
|
+
return "Item name cannot be empty";
|
|
2723
|
+
}
|
|
2724
|
+
return true;
|
|
2725
|
+
}
|
|
2726
|
+
});
|
|
2727
|
+
const { tmpdir } = await import('os');
|
|
2728
|
+
const tempDir = path.join(tmpdir(), `attest-it-${String(Date.now())}`);
|
|
2729
|
+
await promises.mkdir(tempDir, { recursive: true });
|
|
2730
|
+
const tempPrivatePath = path.join(tempDir, "private.pem");
|
|
2731
|
+
try {
|
|
2732
|
+
await promises.writeFile(tempPrivatePath, keyPair.privateKey, { mode: 384 });
|
|
2733
|
+
const { execFile: execFile2 } = await import('child_process');
|
|
2734
|
+
const { promisify: promisify2 } = await import('util');
|
|
2735
|
+
const execFileAsync2 = promisify2(execFile2);
|
|
2736
|
+
const opArgs = [
|
|
2737
|
+
"document",
|
|
2738
|
+
"create",
|
|
2739
|
+
tempPrivatePath,
|
|
2740
|
+
"--title",
|
|
2741
|
+
item,
|
|
2742
|
+
"--vault",
|
|
2743
|
+
selectedVault
|
|
2744
|
+
];
|
|
2745
|
+
if (selectedAccount) {
|
|
2746
|
+
opArgs.push("--account", selectedAccount);
|
|
2747
|
+
}
|
|
2748
|
+
await execFileAsync2("op", opArgs);
|
|
2749
|
+
} finally {
|
|
2750
|
+
const { rm } = await import('fs/promises');
|
|
2751
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => {
|
|
2752
|
+
});
|
|
2753
|
+
}
|
|
2754
|
+
privateKeyRef = {
|
|
2755
|
+
type: "1password",
|
|
2756
|
+
vault: selectedVault,
|
|
2757
|
+
item,
|
|
2758
|
+
...selectedAccount && { account: selectedAccount }
|
|
2759
|
+
};
|
|
2760
|
+
keyStorageDescription = `1Password (${selectedVault}/${item})`;
|
|
2761
|
+
break;
|
|
2762
|
+
}
|
|
2763
|
+
default:
|
|
2764
|
+
throw new Error(`Unknown key storage type: ${keyStorageType}`);
|
|
2765
|
+
}
|
|
2766
|
+
const identity = {
|
|
2767
|
+
name,
|
|
2768
|
+
publicKey: keyPair.publicKey,
|
|
2769
|
+
privateKey: privateKeyRef,
|
|
2770
|
+
...email && { email },
|
|
2771
|
+
...github && { github }
|
|
2772
|
+
};
|
|
2773
|
+
let newConfig;
|
|
2774
|
+
if (existingConfig) {
|
|
2775
|
+
newConfig = {
|
|
2776
|
+
...existingConfig,
|
|
2777
|
+
identities: {
|
|
2778
|
+
...existingConfig.identities,
|
|
2779
|
+
[slug]: identity
|
|
2780
|
+
}
|
|
2781
|
+
};
|
|
2782
|
+
} else {
|
|
2783
|
+
newConfig = {
|
|
2784
|
+
activeIdentity: slug,
|
|
2785
|
+
identities: {
|
|
2786
|
+
[slug]: identity
|
|
2787
|
+
}
|
|
2788
|
+
};
|
|
2789
|
+
}
|
|
2790
|
+
await core.saveLocalConfig(newConfig);
|
|
2791
|
+
log("");
|
|
2792
|
+
success("Identity created successfully");
|
|
2793
|
+
log("");
|
|
2794
|
+
log(` Slug: ${slug}`);
|
|
2795
|
+
log(` Name: ${name}`);
|
|
2796
|
+
if (email) {
|
|
2797
|
+
log(` Email: ${email}`);
|
|
2798
|
+
}
|
|
2799
|
+
if (github) {
|
|
2800
|
+
log(` GitHub: ${github}`);
|
|
2801
|
+
}
|
|
2802
|
+
log(` Public Key: ${keyPair.publicKey.slice(0, 32)}...`);
|
|
2803
|
+
log(` Private Key: ${keyStorageDescription}`);
|
|
2804
|
+
log("");
|
|
2805
|
+
if (!existingConfig) {
|
|
2806
|
+
success(`Set as active identity`);
|
|
2807
|
+
log("");
|
|
2808
|
+
} else {
|
|
2809
|
+
log(`To use this identity, run: attest-it identity use ${slug}`);
|
|
2810
|
+
log("");
|
|
2811
|
+
}
|
|
2812
|
+
} catch (err) {
|
|
2813
|
+
if (err instanceof Error) {
|
|
2814
|
+
error(err.message);
|
|
2815
|
+
} else {
|
|
2816
|
+
error("Unknown error occurred");
|
|
2817
|
+
}
|
|
2818
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
var useCommand = new commander.Command("use").description("Set the active identity").argument("<slug>", "Identity slug to activate").action(async (slug) => {
|
|
2822
|
+
await runUse(slug);
|
|
2823
|
+
});
|
|
2824
|
+
async function runUse(slug) {
|
|
2825
|
+
try {
|
|
2826
|
+
const config = await core.loadLocalConfig();
|
|
2827
|
+
if (!config) {
|
|
2828
|
+
error("No identities configured");
|
|
2829
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
2830
|
+
}
|
|
2831
|
+
const identity = config.identities[slug];
|
|
2832
|
+
if (!identity) {
|
|
2833
|
+
error(`Identity "${slug}" not found`);
|
|
2834
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
2835
|
+
}
|
|
2836
|
+
const newConfig = {
|
|
2837
|
+
...config,
|
|
2838
|
+
activeIdentity: slug
|
|
2839
|
+
};
|
|
2840
|
+
await core.saveLocalConfig(newConfig);
|
|
2841
|
+
success(`Active identity set to: ${identity.name} (${slug})`);
|
|
2842
|
+
} catch (err) {
|
|
2843
|
+
if (err instanceof Error) {
|
|
2844
|
+
error(err.message);
|
|
2845
|
+
} else {
|
|
2846
|
+
error("Unknown error occurred");
|
|
2847
|
+
}
|
|
2848
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
var showCommand = new commander.Command("show").description("Show identity details").argument("[slug]", "Identity slug (defaults to active identity)").action(async (slug) => {
|
|
2852
|
+
await runShow(slug);
|
|
2853
|
+
});
|
|
2854
|
+
async function runShow(slug) {
|
|
2855
|
+
try {
|
|
2856
|
+
const config = await core.loadLocalConfig();
|
|
2857
|
+
if (!config) {
|
|
2858
|
+
error("No identities configured");
|
|
2859
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
2860
|
+
}
|
|
2861
|
+
const theme3 = getTheme2();
|
|
2862
|
+
let targetSlug;
|
|
2863
|
+
let isActive;
|
|
2864
|
+
if (slug) {
|
|
2865
|
+
targetSlug = slug;
|
|
2866
|
+
isActive = slug === config.activeIdentity;
|
|
2867
|
+
} else {
|
|
2868
|
+
targetSlug = config.activeIdentity;
|
|
2869
|
+
isActive = true;
|
|
2870
|
+
}
|
|
2871
|
+
const identity = config.identities[targetSlug];
|
|
2872
|
+
if (!identity) {
|
|
2873
|
+
error(`Identity "${targetSlug}" not found`);
|
|
2874
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
2875
|
+
}
|
|
2876
|
+
log("");
|
|
2877
|
+
log(theme3.blue.bold()("Identity Details:"));
|
|
2878
|
+
log("");
|
|
2879
|
+
log(` Slug: ${theme3.blue(targetSlug)}${isActive ? theme3.green(" (active)") : ""}`);
|
|
2880
|
+
log(` Name: ${identity.name}`);
|
|
2881
|
+
if (identity.email) {
|
|
2882
|
+
log(` Email: ${identity.email}`);
|
|
2883
|
+
}
|
|
2884
|
+
if (identity.github) {
|
|
2885
|
+
log(` GitHub: ${identity.github}`);
|
|
2886
|
+
}
|
|
2887
|
+
log("");
|
|
2888
|
+
log(` Public Key: ${identity.publicKey}`);
|
|
2889
|
+
log("");
|
|
2890
|
+
log(" Private Key Storage:");
|
|
2891
|
+
switch (identity.privateKey.type) {
|
|
2892
|
+
case "file":
|
|
2893
|
+
log(` Type: File`);
|
|
2894
|
+
log(` Path: ${identity.privateKey.path}`);
|
|
2895
|
+
break;
|
|
2896
|
+
case "keychain":
|
|
2897
|
+
log(` Type: macOS Keychain`);
|
|
2898
|
+
log(` Service: ${identity.privateKey.service}`);
|
|
2899
|
+
log(` Account: ${identity.privateKey.account}`);
|
|
2900
|
+
break;
|
|
2901
|
+
case "1password":
|
|
2902
|
+
log(` Type: 1Password`);
|
|
2903
|
+
if (identity.privateKey.account) {
|
|
2904
|
+
log(` Account: ${identity.privateKey.account}`);
|
|
2905
|
+
}
|
|
2906
|
+
log(` Vault: ${identity.privateKey.vault}`);
|
|
2907
|
+
log(` Item: ${identity.privateKey.item}`);
|
|
2908
|
+
if (identity.privateKey.field) {
|
|
2909
|
+
log(` Field: ${identity.privateKey.field}`);
|
|
2910
|
+
}
|
|
2911
|
+
break;
|
|
2912
|
+
}
|
|
2913
|
+
log("");
|
|
2914
|
+
} catch (err) {
|
|
2915
|
+
if (err instanceof Error) {
|
|
2916
|
+
error(err.message);
|
|
2917
|
+
} else {
|
|
2918
|
+
error("Unknown error occurred");
|
|
2919
|
+
}
|
|
2920
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
var editCommand = new commander.Command("edit").description("Edit identity or rotate keypair").argument("<slug>", "Identity slug to edit").action(async (slug) => {
|
|
2924
|
+
await runEdit(slug);
|
|
2925
|
+
});
|
|
2926
|
+
async function runEdit(slug) {
|
|
2927
|
+
try {
|
|
2928
|
+
const config = await core.loadLocalConfig();
|
|
2929
|
+
if (!config) {
|
|
2930
|
+
error("No identities configured");
|
|
2931
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
2932
|
+
}
|
|
2933
|
+
const identity = config.identities[slug];
|
|
2934
|
+
if (!identity) {
|
|
2935
|
+
error(`Identity "${slug}" not found`);
|
|
2936
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
2937
|
+
}
|
|
2938
|
+
const theme3 = getTheme2();
|
|
2939
|
+
log("");
|
|
2940
|
+
log(theme3.blue.bold()(`Edit Identity: ${slug}`));
|
|
2941
|
+
log("");
|
|
2942
|
+
const name = await prompts.input({
|
|
2943
|
+
message: "Display name:",
|
|
2944
|
+
default: identity.name,
|
|
2945
|
+
validate: (value) => {
|
|
2946
|
+
if (!value || value.trim().length === 0) {
|
|
2947
|
+
return "Name cannot be empty";
|
|
2948
|
+
}
|
|
2949
|
+
return true;
|
|
2950
|
+
}
|
|
2951
|
+
});
|
|
2952
|
+
const email = await prompts.input({
|
|
2953
|
+
message: "Email (optional):",
|
|
2954
|
+
default: identity.email ?? ""
|
|
2955
|
+
});
|
|
2956
|
+
const github = await prompts.input({
|
|
2957
|
+
message: "GitHub username (optional):",
|
|
2958
|
+
default: identity.github ?? ""
|
|
2959
|
+
});
|
|
2960
|
+
const rotateKey = await prompts.confirm({
|
|
2961
|
+
message: "Rotate keypair (generate new keys)?",
|
|
2962
|
+
default: false
|
|
2963
|
+
});
|
|
2964
|
+
let publicKey = identity.publicKey;
|
|
2965
|
+
const privateKeyRef = identity.privateKey;
|
|
2966
|
+
if (rotateKey) {
|
|
2967
|
+
log("");
|
|
2968
|
+
log("Generating new Ed25519 keypair...");
|
|
2969
|
+
const keyPair = core.generateEd25519KeyPair();
|
|
2970
|
+
publicKey = keyPair.publicKey;
|
|
2971
|
+
switch (identity.privateKey.type) {
|
|
2972
|
+
case "file": {
|
|
2973
|
+
await promises.writeFile(identity.privateKey.path, keyPair.privateKey, { mode: 384 });
|
|
2974
|
+
log(` Updated private key at: ${identity.privateKey.path}`);
|
|
2975
|
+
break;
|
|
2976
|
+
}
|
|
2977
|
+
case "keychain": {
|
|
2978
|
+
const { execFile } = await import('child_process');
|
|
2979
|
+
const { promisify } = await import('util');
|
|
2980
|
+
const execFileAsync = promisify(execFile);
|
|
2981
|
+
const encodedKey = Buffer.from(keyPair.privateKey).toString("base64");
|
|
2982
|
+
try {
|
|
2983
|
+
await execFileAsync("security", [
|
|
2984
|
+
"delete-generic-password",
|
|
2985
|
+
"-s",
|
|
2986
|
+
identity.privateKey.service,
|
|
2987
|
+
"-a",
|
|
2988
|
+
identity.privateKey.account
|
|
2989
|
+
]);
|
|
2990
|
+
await execFileAsync("security", [
|
|
2991
|
+
"add-generic-password",
|
|
2992
|
+
"-s",
|
|
2993
|
+
identity.privateKey.service,
|
|
2994
|
+
"-a",
|
|
2995
|
+
identity.privateKey.account,
|
|
2996
|
+
"-w",
|
|
2997
|
+
encodedKey,
|
|
2998
|
+
"-U"
|
|
2999
|
+
]);
|
|
3000
|
+
log(` Updated private key in macOS Keychain`);
|
|
3001
|
+
} catch (err) {
|
|
3002
|
+
throw new Error(
|
|
3003
|
+
`Failed to update key in macOS Keychain: ${err instanceof Error ? err.message : String(err)}`
|
|
3004
|
+
);
|
|
3005
|
+
}
|
|
3006
|
+
break;
|
|
3007
|
+
}
|
|
3008
|
+
case "1password": {
|
|
3009
|
+
const { execFile } = await import('child_process');
|
|
3010
|
+
const { promisify } = await import('util');
|
|
3011
|
+
const execFileAsync = promisify(execFile);
|
|
3012
|
+
try {
|
|
3013
|
+
const opArgs = [
|
|
3014
|
+
"item",
|
|
3015
|
+
"edit",
|
|
3016
|
+
identity.privateKey.item,
|
|
3017
|
+
"--vault",
|
|
3018
|
+
identity.privateKey.vault,
|
|
3019
|
+
`privateKey[password]=${keyPair.privateKey}`
|
|
3020
|
+
];
|
|
3021
|
+
if (identity.privateKey.account) {
|
|
3022
|
+
opArgs.push("--account", identity.privateKey.account);
|
|
3023
|
+
}
|
|
3024
|
+
await execFileAsync("op", opArgs);
|
|
3025
|
+
log(` Updated private key in 1Password`);
|
|
3026
|
+
} catch (err) {
|
|
3027
|
+
throw new Error(
|
|
3028
|
+
`Failed to update key in 1Password: ${err instanceof Error ? err.message : String(err)}`
|
|
3029
|
+
);
|
|
3030
|
+
}
|
|
3031
|
+
break;
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
const updatedIdentity = {
|
|
3036
|
+
name,
|
|
3037
|
+
publicKey,
|
|
3038
|
+
privateKey: privateKeyRef,
|
|
3039
|
+
...email && { email },
|
|
3040
|
+
...github && { github }
|
|
3041
|
+
};
|
|
3042
|
+
const newConfig = {
|
|
3043
|
+
...config,
|
|
3044
|
+
identities: {
|
|
3045
|
+
...config.identities,
|
|
3046
|
+
[slug]: updatedIdentity
|
|
3047
|
+
}
|
|
3048
|
+
};
|
|
3049
|
+
await core.saveLocalConfig(newConfig);
|
|
3050
|
+
log("");
|
|
3051
|
+
success("Identity updated successfully");
|
|
3052
|
+
log("");
|
|
3053
|
+
if (rotateKey) {
|
|
3054
|
+
log(" New Public Key: " + publicKey.slice(0, 32) + "...");
|
|
3055
|
+
log("");
|
|
3056
|
+
log(
|
|
3057
|
+
theme3.yellow(
|
|
3058
|
+
" Warning: If this identity is used in team configurations,\n you must update those configurations with the new public key."
|
|
3059
|
+
)
|
|
3060
|
+
);
|
|
3061
|
+
log("");
|
|
3062
|
+
}
|
|
3063
|
+
} catch (err) {
|
|
3064
|
+
if (err instanceof Error) {
|
|
3065
|
+
error(err.message);
|
|
3066
|
+
} else {
|
|
3067
|
+
error("Unknown error occurred");
|
|
3068
|
+
}
|
|
3069
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
// src/utils/format-key-location.ts
|
|
3074
|
+
function formatKeyLocation(privateKey) {
|
|
3075
|
+
const theme3 = getTheme();
|
|
3076
|
+
switch (privateKey.type) {
|
|
3077
|
+
case "file":
|
|
3078
|
+
return `${theme3.blue.bold()("File")}: ${theme3.muted(privateKey.path)}`;
|
|
3079
|
+
case "keychain": {
|
|
3080
|
+
let keychainName = "default";
|
|
3081
|
+
if (privateKey.keychain) {
|
|
3082
|
+
const filename = privateKey.keychain.split("/").pop() ?? privateKey.keychain;
|
|
3083
|
+
keychainName = filename.replace(/\.keychain(-db)?$/, "");
|
|
3084
|
+
}
|
|
3085
|
+
return `${theme3.blue.bold()("macOS Keychain")}: ${theme3.muted(`${keychainName}/${privateKey.service}`)}`;
|
|
3086
|
+
}
|
|
3087
|
+
case "1password": {
|
|
3088
|
+
const parts = [privateKey.vault, privateKey.item];
|
|
3089
|
+
if (privateKey.account) {
|
|
3090
|
+
parts.unshift(privateKey.account);
|
|
3091
|
+
}
|
|
3092
|
+
return `${theme3.blue.bold()("1Password")}: ${theme3.muted(parts.join("/"))}`;
|
|
3093
|
+
}
|
|
3094
|
+
default:
|
|
3095
|
+
return "Unknown storage";
|
|
3096
|
+
}
|
|
3097
|
+
}
|
|
3098
|
+
var removeCommand = new commander.Command("remove").description("Delete identity and optionally delete private key").argument("<slug>", "Identity slug to remove").action(async (slug) => {
|
|
3099
|
+
await runRemove(slug);
|
|
3100
|
+
});
|
|
3101
|
+
async function runRemove(slug) {
|
|
3102
|
+
try {
|
|
3103
|
+
const config = await core.loadLocalConfig();
|
|
3104
|
+
if (!config) {
|
|
3105
|
+
error("No identities configured");
|
|
3106
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
3107
|
+
}
|
|
3108
|
+
const identity = config.identities[slug];
|
|
3109
|
+
if (!identity) {
|
|
3110
|
+
error(`Identity "${slug}" not found`);
|
|
3111
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
3112
|
+
}
|
|
3113
|
+
const theme3 = getTheme();
|
|
3114
|
+
log("");
|
|
3115
|
+
log(theme3.blue.bold()(`Remove Identity: ${slug}`));
|
|
3116
|
+
log("");
|
|
3117
|
+
log(` Name: ${identity.name}`);
|
|
3118
|
+
if (identity.email) {
|
|
3119
|
+
log(` Email: ${identity.email}`);
|
|
3120
|
+
}
|
|
3121
|
+
log("");
|
|
3122
|
+
const confirmDelete = await prompts.confirm({
|
|
3123
|
+
message: "Are you sure you want to delete this identity?",
|
|
3124
|
+
default: false
|
|
3125
|
+
});
|
|
3126
|
+
if (!confirmDelete) {
|
|
3127
|
+
log("Cancelled");
|
|
3128
|
+
process.exit(ExitCode.CANCELLED);
|
|
3129
|
+
}
|
|
3130
|
+
const keyLocation = formatKeyLocation(identity.privateKey);
|
|
3131
|
+
log(` Private key: ${keyLocation}`);
|
|
3132
|
+
log("");
|
|
3133
|
+
const deletePrivateKey = await prompts.confirm({
|
|
3134
|
+
message: "Also delete the private key from storage?",
|
|
3135
|
+
default: false
|
|
3136
|
+
});
|
|
3137
|
+
if (deletePrivateKey) {
|
|
3138
|
+
switch (identity.privateKey.type) {
|
|
3139
|
+
case "file": {
|
|
3140
|
+
try {
|
|
3141
|
+
await promises.unlink(identity.privateKey.path);
|
|
3142
|
+
log(` Deleted private key file: ${identity.privateKey.path}`);
|
|
3143
|
+
} catch (err) {
|
|
3144
|
+
if (err && typeof err === "object" && "code" in err && err.code !== "ENOENT") {
|
|
3145
|
+
throw err;
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
break;
|
|
3149
|
+
}
|
|
3150
|
+
case "keychain": {
|
|
3151
|
+
const { execFile } = await import('child_process');
|
|
3152
|
+
const { promisify } = await import('util');
|
|
3153
|
+
const execFileAsync = promisify(execFile);
|
|
3154
|
+
try {
|
|
3155
|
+
const deleteArgs = [
|
|
3156
|
+
"delete-generic-password",
|
|
3157
|
+
"-s",
|
|
3158
|
+
identity.privateKey.service,
|
|
3159
|
+
"-a",
|
|
3160
|
+
identity.privateKey.account
|
|
3161
|
+
];
|
|
3162
|
+
if (identity.privateKey.keychain) {
|
|
3163
|
+
deleteArgs.push(identity.privateKey.keychain);
|
|
3164
|
+
}
|
|
3165
|
+
await execFileAsync("security", deleteArgs);
|
|
3166
|
+
log(` Deleted private key from macOS Keychain`);
|
|
3167
|
+
} catch (err) {
|
|
3168
|
+
if (err instanceof Error && !err.message.includes("could not be found") && !err.message.includes("does not exist")) {
|
|
3169
|
+
throw err;
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
break;
|
|
3173
|
+
}
|
|
3174
|
+
case "1password": {
|
|
3175
|
+
const { execFile } = await import('child_process');
|
|
3176
|
+
const { promisify } = await import('util');
|
|
3177
|
+
const execFileAsync = promisify(execFile);
|
|
3178
|
+
try {
|
|
3179
|
+
const opArgs = [
|
|
3180
|
+
"item",
|
|
3181
|
+
"delete",
|
|
3182
|
+
identity.privateKey.item,
|
|
3183
|
+
"--vault",
|
|
3184
|
+
identity.privateKey.vault
|
|
3185
|
+
];
|
|
3186
|
+
if (identity.privateKey.account) {
|
|
3187
|
+
opArgs.push("--account", identity.privateKey.account);
|
|
3188
|
+
}
|
|
3189
|
+
await execFileAsync("op", opArgs);
|
|
3190
|
+
log(` Deleted private key from 1Password`);
|
|
3191
|
+
} catch (err) {
|
|
3192
|
+
if (err instanceof Error && !err.message.includes("not found") && !err.message.includes("doesn't exist")) {
|
|
3193
|
+
throw err;
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
break;
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
3200
|
+
const { [slug]: _removed, ...remainingIdentities } = config.identities;
|
|
3201
|
+
if (Object.keys(remainingIdentities).length === 0) {
|
|
3202
|
+
error("Cannot remove last identity");
|
|
3203
|
+
log("");
|
|
3204
|
+
log("At least one identity must exist");
|
|
3205
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
3206
|
+
}
|
|
3207
|
+
let newActiveIdentity = config.activeIdentity;
|
|
3208
|
+
if (slug === config.activeIdentity) {
|
|
3209
|
+
const firstKey = Object.keys(remainingIdentities)[0];
|
|
3210
|
+
if (!firstKey) {
|
|
3211
|
+
throw new Error("No remaining identities after removal");
|
|
3212
|
+
}
|
|
3213
|
+
newActiveIdentity = firstKey;
|
|
3214
|
+
log("");
|
|
3215
|
+
log(theme3.yellow(` Removed active identity. New active identity: ${newActiveIdentity}`));
|
|
3216
|
+
}
|
|
3217
|
+
const newConfig = {
|
|
3218
|
+
activeIdentity: newActiveIdentity,
|
|
3219
|
+
identities: remainingIdentities
|
|
3220
|
+
};
|
|
3221
|
+
await core.saveLocalConfig(newConfig);
|
|
3222
|
+
log("");
|
|
3223
|
+
success(`Identity "${slug}" removed`);
|
|
3224
|
+
log("");
|
|
3225
|
+
} catch (err) {
|
|
3226
|
+
if (err instanceof Error) {
|
|
3227
|
+
error(err.message);
|
|
3228
|
+
} else {
|
|
3229
|
+
error("Unknown error occurred");
|
|
3230
|
+
}
|
|
3231
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
var exportCommand = new commander.Command("export").description("Export identity for team onboarding (YAML snippet)").argument("[slug]", "Identity slug to export (defaults to active identity)").action(async (slug) => {
|
|
3235
|
+
await runExport(slug);
|
|
3236
|
+
});
|
|
3237
|
+
async function runExport(slug) {
|
|
3238
|
+
try {
|
|
3239
|
+
const config = await core.loadLocalConfig();
|
|
3240
|
+
if (!config) {
|
|
3241
|
+
error("No identities configured");
|
|
3242
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
3243
|
+
}
|
|
3244
|
+
const theme3 = getTheme2();
|
|
3245
|
+
const targetSlug = slug ?? config.activeIdentity;
|
|
3246
|
+
const identity = config.identities[targetSlug];
|
|
3247
|
+
if (!identity) {
|
|
3248
|
+
error(`Identity "${targetSlug}" not found`);
|
|
3249
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
3250
|
+
}
|
|
3251
|
+
log("");
|
|
3252
|
+
log(theme3.blue.bold()("Team Configuration YAML:"));
|
|
3253
|
+
log("");
|
|
3254
|
+
log(theme3.muted("# Add this to your team config file (.attest-it/team-config.yaml)"));
|
|
3255
|
+
log("");
|
|
3256
|
+
const exportData = {
|
|
3257
|
+
name: identity.name,
|
|
3258
|
+
publicKey: identity.publicKey
|
|
3259
|
+
};
|
|
3260
|
+
if (identity.email) {
|
|
3261
|
+
exportData.email = identity.email;
|
|
3262
|
+
}
|
|
3263
|
+
if (identity.github) {
|
|
3264
|
+
exportData.github = identity.github;
|
|
3265
|
+
}
|
|
3266
|
+
const yamlData = {
|
|
3267
|
+
[targetSlug]: exportData
|
|
3268
|
+
};
|
|
3269
|
+
const yamlString = yaml.stringify(yamlData);
|
|
3270
|
+
log(yamlString);
|
|
3271
|
+
log("");
|
|
3272
|
+
log(theme3.muted('# The team owner can add this to the "members:" section'));
|
|
3273
|
+
log("");
|
|
3274
|
+
} catch (err) {
|
|
3275
|
+
if (err instanceof Error) {
|
|
3276
|
+
error(err.message);
|
|
1541
3277
|
} else {
|
|
1542
|
-
|
|
3278
|
+
error("Unknown error occurred");
|
|
3279
|
+
}
|
|
3280
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
|
|
3284
|
+
// src/commands/identity/index.ts
|
|
3285
|
+
var identityCommand = new commander.Command("identity").description("Manage local identities and keypairs").addCommand(listCommand).addCommand(createCommand).addCommand(useCommand).addCommand(showCommand).addCommand(editCommand).addCommand(removeCommand).addCommand(exportCommand);
|
|
3286
|
+
var whoamiCommand = new commander.Command("whoami").description("Show the current active identity").action(async () => {
|
|
3287
|
+
await runWhoami();
|
|
3288
|
+
});
|
|
3289
|
+
async function runWhoami() {
|
|
3290
|
+
try {
|
|
3291
|
+
const config = await core.loadLocalConfig();
|
|
3292
|
+
if (!config) {
|
|
3293
|
+
error("No identities configured");
|
|
3294
|
+
log("");
|
|
3295
|
+
log("Run: attest-it identity create");
|
|
3296
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
1543
3297
|
}
|
|
1544
|
-
|
|
1545
|
-
|
|
3298
|
+
const identity = core.getActiveIdentity(config);
|
|
3299
|
+
if (!identity) {
|
|
3300
|
+
error("Active identity not found");
|
|
3301
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
1546
3302
|
}
|
|
1547
|
-
|
|
1548
|
-
|
|
3303
|
+
const theme3 = getTheme();
|
|
3304
|
+
log("");
|
|
3305
|
+
log(theme3.blue.bold()("Active Identity"));
|
|
3306
|
+
log("");
|
|
3307
|
+
log(` Slug: ${theme3.green.bold()(config.activeIdentity)}`);
|
|
3308
|
+
log(` Name: ${identity.name}`);
|
|
3309
|
+
if (identity.email) {
|
|
3310
|
+
log(` Email: ${theme3.muted(identity.email)}`);
|
|
1549
3311
|
}
|
|
1550
|
-
|
|
3312
|
+
if (identity.github) {
|
|
3313
|
+
log(` GitHub: ${theme3.muted("@" + identity.github)}`);
|
|
3314
|
+
}
|
|
3315
|
+
log(` Public Key: ${theme3.muted(identity.publicKey.slice(0, 24) + "...")}`);
|
|
3316
|
+
log(` Key Store: ${formatKeyLocation(identity.privateKey)}`);
|
|
3317
|
+
log("");
|
|
1551
3318
|
} catch (err) {
|
|
1552
3319
|
if (err instanceof Error) {
|
|
1553
3320
|
error(err.message);
|
|
@@ -1557,79 +3324,647 @@ async function runVerify(options) {
|
|
|
1557
3324
|
process.exit(ExitCode.CONFIG_ERROR);
|
|
1558
3325
|
}
|
|
1559
3326
|
}
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
3327
|
+
var listCommand2 = new commander.Command("list").description("List team members and their authorizations").action(async () => {
|
|
3328
|
+
await runList2();
|
|
3329
|
+
});
|
|
3330
|
+
async function runList2() {
|
|
3331
|
+
try {
|
|
3332
|
+
const config = await core.loadConfig();
|
|
3333
|
+
const attestItConfig = core.toAttestItConfig(config);
|
|
3334
|
+
if (!attestItConfig.team || Object.keys(attestItConfig.team).length === 0) {
|
|
3335
|
+
error("No team members configured");
|
|
3336
|
+
log("");
|
|
3337
|
+
log("Run: attest-it team add");
|
|
3338
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
3339
|
+
}
|
|
3340
|
+
const theme3 = getTheme2();
|
|
3341
|
+
const teamMembers = Object.entries(attestItConfig.team);
|
|
3342
|
+
log("");
|
|
3343
|
+
log(theme3.blue.bold()("Team Members:"));
|
|
3344
|
+
log("");
|
|
3345
|
+
for (const [slug, member] of teamMembers) {
|
|
3346
|
+
const keyPreview = member.publicKey.slice(0, 12) + "...";
|
|
3347
|
+
log(theme3.blue(slug));
|
|
3348
|
+
log(` Name: ${member.name}`);
|
|
3349
|
+
if (member.email) {
|
|
3350
|
+
log(` Email: ${member.email}`);
|
|
3351
|
+
}
|
|
3352
|
+
if (member.github) {
|
|
3353
|
+
log(` GitHub: ${member.github}`);
|
|
3354
|
+
}
|
|
3355
|
+
log(` Public Key: ${keyPreview}`);
|
|
3356
|
+
const authorizedGates = [];
|
|
3357
|
+
if (attestItConfig.gates) {
|
|
3358
|
+
for (const [gateId, gate] of Object.entries(attestItConfig.gates)) {
|
|
3359
|
+
if (gate.authorizedSigners.includes(slug)) {
|
|
3360
|
+
authorizedGates.push(gateId);
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
if (authorizedGates.length > 0) {
|
|
3365
|
+
log(` Gates: ${authorizedGates.join(", ")}`);
|
|
3366
|
+
} else {
|
|
3367
|
+
log(` Gates: ${theme3.muted("(none)")}`);
|
|
3368
|
+
}
|
|
3369
|
+
log("");
|
|
3370
|
+
}
|
|
3371
|
+
if (teamMembers.length === 1) {
|
|
3372
|
+
log(`1 team member configured`);
|
|
3373
|
+
} else {
|
|
3374
|
+
log(`${teamMembers.length.toString()} team members configured`);
|
|
3375
|
+
}
|
|
1565
3376
|
log("");
|
|
3377
|
+
} catch (err) {
|
|
3378
|
+
if (err instanceof Error) {
|
|
3379
|
+
error(err.message);
|
|
3380
|
+
} else {
|
|
3381
|
+
error("Unknown error occurred");
|
|
3382
|
+
}
|
|
3383
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
var addCommand = new commander.Command("add").description("Add a new team member").action(async () => {
|
|
3387
|
+
await runAdd();
|
|
3388
|
+
});
|
|
3389
|
+
function validatePublicKey(value) {
|
|
3390
|
+
if (!value || value.trim().length === 0) {
|
|
3391
|
+
return "Public key cannot be empty";
|
|
3392
|
+
}
|
|
3393
|
+
const base64Regex = /^[A-Za-z0-9+/]+=*$/;
|
|
3394
|
+
if (!base64Regex.test(value)) {
|
|
3395
|
+
return "Public key must be valid Base64";
|
|
3396
|
+
}
|
|
3397
|
+
if (value.length !== 44) {
|
|
3398
|
+
return "Public key must be 44 characters (32 bytes in Base64)";
|
|
1566
3399
|
}
|
|
1567
|
-
|
|
1568
|
-
|
|
3400
|
+
try {
|
|
3401
|
+
const decoded = Buffer.from(value, "base64");
|
|
3402
|
+
if (decoded.length !== 32) {
|
|
3403
|
+
return "Public key must decode to 32 bytes";
|
|
3404
|
+
}
|
|
3405
|
+
} catch {
|
|
3406
|
+
return "Invalid Base64 encoding";
|
|
1569
3407
|
}
|
|
1570
|
-
|
|
3408
|
+
return true;
|
|
3409
|
+
}
|
|
3410
|
+
async function runAdd() {
|
|
3411
|
+
try {
|
|
3412
|
+
const theme3 = getTheme2();
|
|
3413
|
+
log("");
|
|
3414
|
+
log(theme3.blue.bold()("Add Team Member"));
|
|
3415
|
+
log("");
|
|
3416
|
+
const config = await core.loadConfig();
|
|
3417
|
+
const attestItConfig = core.toAttestItConfig(config);
|
|
3418
|
+
const existingTeam = attestItConfig.team ?? {};
|
|
3419
|
+
const slug = await prompts.input({
|
|
3420
|
+
message: "Member slug (unique identifier):",
|
|
3421
|
+
validate: (value) => {
|
|
3422
|
+
if (!value || value.trim().length === 0) {
|
|
3423
|
+
return "Slug cannot be empty";
|
|
3424
|
+
}
|
|
3425
|
+
if (!/^[a-z0-9-]+$/.test(value)) {
|
|
3426
|
+
return "Slug must contain only lowercase letters, numbers, and hyphens";
|
|
3427
|
+
}
|
|
3428
|
+
if (existingTeam[value]) {
|
|
3429
|
+
return `Team member "${value}" already exists`;
|
|
3430
|
+
}
|
|
3431
|
+
return true;
|
|
3432
|
+
}
|
|
3433
|
+
});
|
|
3434
|
+
const name = await prompts.input({
|
|
3435
|
+
message: "Display name:",
|
|
3436
|
+
validate: (value) => {
|
|
3437
|
+
if (!value || value.trim().length === 0) {
|
|
3438
|
+
return "Name cannot be empty";
|
|
3439
|
+
}
|
|
3440
|
+
return true;
|
|
3441
|
+
}
|
|
3442
|
+
});
|
|
3443
|
+
const email = await prompts.input({
|
|
3444
|
+
message: "Email (optional):",
|
|
3445
|
+
default: ""
|
|
3446
|
+
});
|
|
3447
|
+
const github = await prompts.input({
|
|
3448
|
+
message: "GitHub username (optional):",
|
|
3449
|
+
default: ""
|
|
3450
|
+
});
|
|
3451
|
+
log("");
|
|
3452
|
+
log('Paste the public key (from "attest-it identity export"):');
|
|
3453
|
+
const publicKey = await prompts.input({
|
|
3454
|
+
message: "Public key:",
|
|
3455
|
+
validate: validatePublicKey
|
|
3456
|
+
});
|
|
3457
|
+
let authorizedGates = [];
|
|
3458
|
+
if (attestItConfig.gates && Object.keys(attestItConfig.gates).length > 0) {
|
|
3459
|
+
log("");
|
|
3460
|
+
const gateChoices = Object.entries(attestItConfig.gates).map(([gateId, gate]) => ({
|
|
3461
|
+
name: `${gateId} - ${gate.name}`,
|
|
3462
|
+
value: gateId
|
|
3463
|
+
}));
|
|
3464
|
+
authorizedGates = await prompts.checkbox({
|
|
3465
|
+
message: "Select gates to authorize (use space to select):",
|
|
3466
|
+
choices: gateChoices
|
|
3467
|
+
});
|
|
3468
|
+
}
|
|
3469
|
+
const teamMember = {
|
|
3470
|
+
name,
|
|
3471
|
+
publicKey: publicKey.trim()
|
|
3472
|
+
};
|
|
3473
|
+
if (email && email.trim().length > 0) {
|
|
3474
|
+
teamMember.email = email.trim();
|
|
3475
|
+
}
|
|
3476
|
+
if (github && github.trim().length > 0) {
|
|
3477
|
+
teamMember.github = github.trim();
|
|
3478
|
+
}
|
|
3479
|
+
const updatedConfig = {
|
|
3480
|
+
...config,
|
|
3481
|
+
team: {
|
|
3482
|
+
...existingTeam,
|
|
3483
|
+
[slug]: teamMember
|
|
3484
|
+
}
|
|
3485
|
+
};
|
|
3486
|
+
if (authorizedGates.length > 0 && updatedConfig.gates) {
|
|
3487
|
+
for (const gateId of authorizedGates) {
|
|
3488
|
+
const gate = updatedConfig.gates[gateId];
|
|
3489
|
+
if (gate) {
|
|
3490
|
+
if (!gate.authorizedSigners.includes(slug)) {
|
|
3491
|
+
gate.authorizedSigners.push(slug);
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3496
|
+
const configPath = core.findConfigPath();
|
|
3497
|
+
if (!configPath) {
|
|
3498
|
+
error("Configuration file not found");
|
|
3499
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
3500
|
+
}
|
|
3501
|
+
const yamlContent = yaml.stringify(updatedConfig);
|
|
3502
|
+
await promises.writeFile(configPath, yamlContent, "utf8");
|
|
3503
|
+
log("");
|
|
3504
|
+
success(`Team member "${slug}" added successfully`);
|
|
3505
|
+
if (authorizedGates.length > 0) {
|
|
3506
|
+
log(`Authorized for gates: ${authorizedGates.join(", ")}`);
|
|
3507
|
+
}
|
|
1571
3508
|
log("");
|
|
3509
|
+
} catch (err) {
|
|
3510
|
+
if (err instanceof Error) {
|
|
3511
|
+
error(err.message);
|
|
3512
|
+
} else {
|
|
3513
|
+
error("Unknown error occurred");
|
|
3514
|
+
}
|
|
3515
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
1572
3516
|
}
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
3517
|
+
}
|
|
3518
|
+
var editCommand2 = new commander.Command("edit").description("Edit a team member").argument("<slug>", "Team member slug to edit").action(async (slug) => {
|
|
3519
|
+
await runEdit2(slug);
|
|
3520
|
+
});
|
|
3521
|
+
function validatePublicKey2(value) {
|
|
3522
|
+
if (!value || value.trim().length === 0) {
|
|
3523
|
+
return "Public key cannot be empty";
|
|
3524
|
+
}
|
|
3525
|
+
const base64Regex = /^[A-Za-z0-9+/]+=*$/;
|
|
3526
|
+
if (!base64Regex.test(value)) {
|
|
3527
|
+
return "Public key must be valid Base64";
|
|
3528
|
+
}
|
|
3529
|
+
if (value.length !== 44) {
|
|
3530
|
+
return "Public key must be 44 characters (32 bytes in Base64)";
|
|
3531
|
+
}
|
|
3532
|
+
try {
|
|
3533
|
+
const decoded = Buffer.from(value, "base64");
|
|
3534
|
+
if (decoded.length !== 32) {
|
|
3535
|
+
return "Public key must decode to 32 bytes";
|
|
3536
|
+
}
|
|
3537
|
+
} catch {
|
|
3538
|
+
return "Invalid Base64 encoding";
|
|
3539
|
+
}
|
|
3540
|
+
return true;
|
|
3541
|
+
}
|
|
3542
|
+
async function runEdit2(slug) {
|
|
3543
|
+
try {
|
|
3544
|
+
const theme3 = getTheme2();
|
|
3545
|
+
const config = await core.loadConfig();
|
|
3546
|
+
const attestItConfig = core.toAttestItConfig(config);
|
|
3547
|
+
const existingMember = attestItConfig.team?.[slug];
|
|
3548
|
+
if (!existingMember) {
|
|
3549
|
+
error(`Team member "${slug}" not found`);
|
|
3550
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
3551
|
+
}
|
|
3552
|
+
log("");
|
|
3553
|
+
log(theme3.blue.bold()(`Edit Team Member: ${slug}`));
|
|
3554
|
+
log("");
|
|
3555
|
+
log(theme3.muted("Leave blank to keep current value"));
|
|
3556
|
+
log("");
|
|
3557
|
+
const name = await prompts.input({
|
|
3558
|
+
message: "Display name:",
|
|
3559
|
+
default: existingMember.name,
|
|
3560
|
+
validate: (value) => {
|
|
3561
|
+
if (!value || value.trim().length === 0) {
|
|
3562
|
+
return "Name cannot be empty";
|
|
3563
|
+
}
|
|
3564
|
+
return true;
|
|
3565
|
+
}
|
|
3566
|
+
});
|
|
3567
|
+
const email = await prompts.input({
|
|
3568
|
+
message: "Email (optional):",
|
|
3569
|
+
default: existingMember.email ?? ""
|
|
3570
|
+
});
|
|
3571
|
+
const github = await prompts.input({
|
|
3572
|
+
message: "GitHub username (optional):",
|
|
3573
|
+
default: existingMember.github ?? ""
|
|
3574
|
+
});
|
|
3575
|
+
const updateKey = await prompts.confirm({
|
|
3576
|
+
message: "Update public key?",
|
|
3577
|
+
default: false
|
|
3578
|
+
});
|
|
3579
|
+
let publicKey = existingMember.publicKey;
|
|
3580
|
+
if (updateKey) {
|
|
3581
|
+
log("");
|
|
3582
|
+
log("Paste the new public key:");
|
|
3583
|
+
publicKey = await prompts.input({
|
|
3584
|
+
message: "Public key:",
|
|
3585
|
+
default: existingMember.publicKey,
|
|
3586
|
+
validate: validatePublicKey2
|
|
3587
|
+
});
|
|
3588
|
+
}
|
|
3589
|
+
const currentGates = [];
|
|
3590
|
+
if (attestItConfig.gates) {
|
|
3591
|
+
for (const [gateId, gate] of Object.entries(attestItConfig.gates)) {
|
|
3592
|
+
if (gate.authorizedSigners.includes(slug)) {
|
|
3593
|
+
currentGates.push(gateId);
|
|
3594
|
+
}
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
let selectedGates = currentGates;
|
|
3598
|
+
if (attestItConfig.gates && Object.keys(attestItConfig.gates).length > 0) {
|
|
3599
|
+
log("");
|
|
3600
|
+
const gateChoices = Object.entries(attestItConfig.gates).map(([gateId, gate]) => ({
|
|
3601
|
+
name: `${gateId} - ${gate.name}`,
|
|
3602
|
+
value: gateId,
|
|
3603
|
+
checked: currentGates.includes(gateId)
|
|
3604
|
+
}));
|
|
3605
|
+
selectedGates = await prompts.checkbox({
|
|
3606
|
+
message: "Select gates to authorize (use space to select):",
|
|
3607
|
+
choices: gateChoices
|
|
3608
|
+
});
|
|
3609
|
+
}
|
|
3610
|
+
const updatedMember = {
|
|
3611
|
+
name: name.trim(),
|
|
3612
|
+
publicKey: publicKey.trim()
|
|
3613
|
+
};
|
|
3614
|
+
if (email && email.trim().length > 0) {
|
|
3615
|
+
updatedMember.email = email.trim();
|
|
3616
|
+
}
|
|
3617
|
+
if (github && github.trim().length > 0) {
|
|
3618
|
+
updatedMember.github = github.trim();
|
|
3619
|
+
}
|
|
3620
|
+
const updatedConfig = {
|
|
3621
|
+
...config,
|
|
3622
|
+
team: {
|
|
3623
|
+
...attestItConfig.team,
|
|
3624
|
+
[slug]: updatedMember
|
|
3625
|
+
}
|
|
3626
|
+
};
|
|
3627
|
+
if (updatedConfig.gates) {
|
|
3628
|
+
for (const [gateId, gate] of Object.entries(updatedConfig.gates)) {
|
|
3629
|
+
if (currentGates.includes(gateId) && !selectedGates.includes(gateId)) {
|
|
3630
|
+
gate.authorizedSigners = gate.authorizedSigners.filter((s) => s !== slug);
|
|
3631
|
+
}
|
|
3632
|
+
if (!currentGates.includes(gateId) && selectedGates.includes(gateId)) {
|
|
3633
|
+
if (!gate.authorizedSigners.includes(slug)) {
|
|
3634
|
+
gate.authorizedSigners.push(slug);
|
|
3635
|
+
}
|
|
1591
3636
|
}
|
|
1592
3637
|
}
|
|
1593
3638
|
}
|
|
3639
|
+
const configPath = core.findConfigPath();
|
|
3640
|
+
if (!configPath) {
|
|
3641
|
+
error("Configuration file not found");
|
|
3642
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
3643
|
+
}
|
|
3644
|
+
const yamlContent = yaml.stringify(updatedConfig);
|
|
3645
|
+
await promises.writeFile(configPath, yamlContent, "utf8");
|
|
3646
|
+
log("");
|
|
3647
|
+
success(`Team member "${slug}" updated successfully`);
|
|
3648
|
+
if (selectedGates.length > 0) {
|
|
3649
|
+
log(`Authorized for gates: ${selectedGates.join(", ")}`);
|
|
3650
|
+
} else {
|
|
3651
|
+
log("Not authorized for any gates");
|
|
3652
|
+
}
|
|
3653
|
+
log("");
|
|
3654
|
+
} catch (err) {
|
|
3655
|
+
if (err instanceof Error) {
|
|
3656
|
+
error(err.message);
|
|
3657
|
+
} else {
|
|
3658
|
+
error("Unknown error occurred");
|
|
3659
|
+
}
|
|
3660
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
1594
3661
|
}
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
3662
|
+
}
|
|
3663
|
+
var removeCommand2 = new commander.Command("remove").description("Remove a team member").argument("<slug>", "Team member slug to remove").option("-f, --force", "Skip confirmation prompt").action(async (slug, options) => {
|
|
3664
|
+
await runRemove2(slug, options);
|
|
3665
|
+
});
|
|
3666
|
+
async function runRemove2(slug, options) {
|
|
3667
|
+
try {
|
|
3668
|
+
const theme3 = getTheme2();
|
|
3669
|
+
const config = await core.loadConfig();
|
|
3670
|
+
const attestItConfig = core.toAttestItConfig(config);
|
|
3671
|
+
const existingMember = attestItConfig.team?.[slug];
|
|
3672
|
+
if (!existingMember) {
|
|
3673
|
+
error(`Team member "${slug}" not found`);
|
|
3674
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
3675
|
+
}
|
|
3676
|
+
log("");
|
|
3677
|
+
log(theme3.blue.bold()(`Remove Team Member: ${slug}`));
|
|
1600
3678
|
log("");
|
|
1601
|
-
|
|
1602
|
-
|
|
3679
|
+
log(`Name: ${existingMember.name}`);
|
|
3680
|
+
if (existingMember.email) {
|
|
3681
|
+
log(`Email: ${existingMember.email}`);
|
|
3682
|
+
}
|
|
3683
|
+
if (existingMember.github) {
|
|
3684
|
+
log(`GitHub: ${existingMember.github}`);
|
|
3685
|
+
}
|
|
3686
|
+
log("");
|
|
3687
|
+
const projectRoot = process.cwd();
|
|
3688
|
+
let sealsFile;
|
|
3689
|
+
try {
|
|
3690
|
+
sealsFile = core.readSealsSync(projectRoot);
|
|
3691
|
+
} catch {
|
|
3692
|
+
sealsFile = { version: 1, seals: {} };
|
|
3693
|
+
}
|
|
3694
|
+
const sealsCreatedByMember = [];
|
|
3695
|
+
for (const [gateId, seal] of Object.entries(sealsFile.seals)) {
|
|
3696
|
+
if (seal.sealedBy === slug) {
|
|
3697
|
+
sealsCreatedByMember.push(gateId);
|
|
3698
|
+
}
|
|
3699
|
+
}
|
|
3700
|
+
if (sealsCreatedByMember.length > 0) {
|
|
3701
|
+
warn("This member has created seals for the following gates:");
|
|
3702
|
+
for (const gateId of sealsCreatedByMember) {
|
|
3703
|
+
warn(` - ${gateId}`);
|
|
3704
|
+
}
|
|
3705
|
+
log("");
|
|
3706
|
+
warn("These seals will still be valid but attributed to a removed member.");
|
|
3707
|
+
log("");
|
|
3708
|
+
}
|
|
3709
|
+
const authorizedGates = [];
|
|
3710
|
+
if (attestItConfig.gates) {
|
|
3711
|
+
for (const [gateId, gate] of Object.entries(attestItConfig.gates)) {
|
|
3712
|
+
if (gate.authorizedSigners.includes(slug)) {
|
|
3713
|
+
authorizedGates.push(gateId);
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
}
|
|
3717
|
+
if (authorizedGates.length > 0) {
|
|
3718
|
+
log("This member is authorized for the following gates:");
|
|
3719
|
+
for (const gateId of authorizedGates) {
|
|
3720
|
+
log(` - ${gateId}`);
|
|
3721
|
+
}
|
|
3722
|
+
log("");
|
|
3723
|
+
}
|
|
3724
|
+
if (!options.force) {
|
|
3725
|
+
const confirmed = await prompts.confirm({
|
|
3726
|
+
message: `Are you sure you want to remove "${slug}"?`,
|
|
3727
|
+
default: false
|
|
3728
|
+
});
|
|
3729
|
+
if (!confirmed) {
|
|
3730
|
+
error("Removal cancelled");
|
|
3731
|
+
process.exit(ExitCode.CANCELLED);
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3734
|
+
const updatedTeam = { ...attestItConfig.team };
|
|
3735
|
+
delete updatedTeam[slug];
|
|
3736
|
+
const updatedConfig = {
|
|
3737
|
+
...config,
|
|
3738
|
+
team: updatedTeam
|
|
3739
|
+
};
|
|
3740
|
+
if (updatedConfig.gates) {
|
|
3741
|
+
for (const gate of Object.values(updatedConfig.gates)) {
|
|
3742
|
+
gate.authorizedSigners = gate.authorizedSigners.filter((s) => s !== slug);
|
|
3743
|
+
}
|
|
3744
|
+
}
|
|
3745
|
+
const configPath = core.findConfigPath();
|
|
3746
|
+
if (!configPath) {
|
|
3747
|
+
error("Configuration file not found");
|
|
3748
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
1603
3749
|
}
|
|
1604
|
-
|
|
1605
|
-
|
|
3750
|
+
const yamlContent = yaml.stringify(updatedConfig);
|
|
3751
|
+
await promises.writeFile(configPath, yamlContent, "utf8");
|
|
3752
|
+
log("");
|
|
3753
|
+
success(`Team member "${slug}" removed successfully`);
|
|
3754
|
+
log("");
|
|
3755
|
+
} catch (err) {
|
|
3756
|
+
if (err instanceof Error) {
|
|
3757
|
+
error(err.message);
|
|
3758
|
+
} else {
|
|
3759
|
+
error("Unknown error occurred");
|
|
1606
3760
|
}
|
|
3761
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
1607
3762
|
}
|
|
1608
3763
|
}
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
3764
|
+
|
|
3765
|
+
// src/commands/team/index.ts
|
|
3766
|
+
var teamCommand = new commander.Command("team").description("Manage team members and authorizations").addCommand(listCommand2).addCommand(addCommand).addCommand(editCommand2).addCommand(removeCommand2);
|
|
3767
|
+
var PROGRAM_NAME = "attest-it";
|
|
3768
|
+
async function getCompletions(env) {
|
|
3769
|
+
let shell;
|
|
3770
|
+
try {
|
|
3771
|
+
const detectedShell = tabtab__default.default.getShellFromEnv(process.env);
|
|
3772
|
+
shell = detectedShell === "pwsh" ? "bash" : detectedShell;
|
|
3773
|
+
} catch {
|
|
3774
|
+
shell = "bash";
|
|
3775
|
+
}
|
|
3776
|
+
const commands = [
|
|
3777
|
+
{ name: "init", description: "Initialize a new config file" },
|
|
3778
|
+
{ name: "status", description: "Show status of all gates" },
|
|
3779
|
+
{ name: "run", description: "Run test suites interactively" },
|
|
3780
|
+
{ name: "verify", description: "Verify all seals are valid" },
|
|
3781
|
+
{ name: "seal", description: "Create a seal for a gate" },
|
|
3782
|
+
{ name: "keygen", description: "Generate a new keypair" },
|
|
3783
|
+
{ name: "prune", description: "Remove stale attestations" },
|
|
3784
|
+
{ name: "identity", description: "Manage identities" },
|
|
3785
|
+
{ name: "team", description: "Manage team members" },
|
|
3786
|
+
{ name: "whoami", description: "Show active identity" },
|
|
3787
|
+
{ name: "completion", description: "Shell completion commands" }
|
|
3788
|
+
];
|
|
3789
|
+
const globalOptions2 = [
|
|
3790
|
+
{ name: "--help", description: "Show help" },
|
|
3791
|
+
{ name: "--version", description: "Show version" },
|
|
3792
|
+
{ name: "--verbose", description: "Verbose output" },
|
|
3793
|
+
{ name: "--quiet", description: "Minimal output" },
|
|
3794
|
+
{ name: "--config", description: "Path to config file" }
|
|
3795
|
+
];
|
|
3796
|
+
const identitySubcommands = [
|
|
3797
|
+
{ name: "create", description: "Create a new identity" },
|
|
3798
|
+
{ name: "list", description: "List all identities" },
|
|
3799
|
+
{ name: "use", description: "Switch active identity" },
|
|
3800
|
+
{ name: "remove", description: "Remove an identity" }
|
|
3801
|
+
];
|
|
3802
|
+
const teamSubcommands = [
|
|
3803
|
+
{ name: "add", description: "Add yourself to the team" },
|
|
3804
|
+
{ name: "list", description: "List team members" },
|
|
3805
|
+
{ name: "remove", description: "Remove a team member" }
|
|
3806
|
+
];
|
|
3807
|
+
const completionSubcommands = [
|
|
3808
|
+
{ name: "install", description: "Install shell completion" },
|
|
3809
|
+
{ name: "uninstall", description: "Uninstall shell completion" }
|
|
3810
|
+
];
|
|
3811
|
+
const words = env.line.split(/\s+/).filter(Boolean);
|
|
3812
|
+
const lastWord = env.last;
|
|
3813
|
+
const prevWord = env.prev;
|
|
3814
|
+
if (prevWord === "--config" || prevWord === "-c") {
|
|
3815
|
+
tabtab__default.default.logFiles();
|
|
3816
|
+
return;
|
|
3817
|
+
}
|
|
3818
|
+
if (lastWord.startsWith("-")) {
|
|
3819
|
+
tabtab__default.default.log(globalOptions2, shell, console.log);
|
|
3820
|
+
return;
|
|
3821
|
+
}
|
|
3822
|
+
const commandIndex = words.findIndex(
|
|
3823
|
+
(w) => !w.startsWith("-") && w !== PROGRAM_NAME && w !== "npx"
|
|
3824
|
+
);
|
|
3825
|
+
const currentCommand = commandIndex >= 0 ? words[commandIndex] ?? null : null;
|
|
3826
|
+
if (currentCommand === "identity") {
|
|
3827
|
+
const subcommandIndex = words.findIndex(
|
|
3828
|
+
(w, i) => i > commandIndex && !w.startsWith("-")
|
|
3829
|
+
);
|
|
3830
|
+
const subcommand = subcommandIndex >= 0 ? words[subcommandIndex] ?? null : null;
|
|
3831
|
+
if (subcommand === "use" || subcommand === "remove") {
|
|
3832
|
+
const identities = await getIdentitySlugs();
|
|
3833
|
+
if (identities.length > 0) {
|
|
3834
|
+
tabtab__default.default.log(identities, shell, console.log);
|
|
3835
|
+
return;
|
|
3836
|
+
}
|
|
3837
|
+
}
|
|
3838
|
+
if (!subcommand || subcommandIndex < 0) {
|
|
3839
|
+
tabtab__default.default.log(identitySubcommands, shell, console.log);
|
|
3840
|
+
return;
|
|
3841
|
+
}
|
|
3842
|
+
}
|
|
3843
|
+
if (currentCommand === "team") {
|
|
3844
|
+
const subcommandIndex = words.findIndex(
|
|
3845
|
+
(w, i) => i > commandIndex && !w.startsWith("-")
|
|
3846
|
+
);
|
|
3847
|
+
const subcommand = subcommandIndex >= 0 ? words[subcommandIndex] ?? null : null;
|
|
3848
|
+
if (!subcommand || subcommandIndex < 0) {
|
|
3849
|
+
tabtab__default.default.log(teamSubcommands, shell, console.log);
|
|
3850
|
+
return;
|
|
3851
|
+
}
|
|
3852
|
+
}
|
|
3853
|
+
if (currentCommand === "completion") {
|
|
3854
|
+
const subcommandIndex = words.findIndex(
|
|
3855
|
+
(w, i) => i > commandIndex && !w.startsWith("-")
|
|
3856
|
+
);
|
|
3857
|
+
const subcommand = subcommandIndex >= 0 ? words[subcommandIndex] ?? null : null;
|
|
3858
|
+
if (subcommand === "install") {
|
|
3859
|
+
tabtab__default.default.log(["bash", "zsh", "fish"], shell, console.log);
|
|
3860
|
+
return;
|
|
3861
|
+
}
|
|
3862
|
+
if (!subcommand || subcommandIndex < 0) {
|
|
3863
|
+
tabtab__default.default.log(completionSubcommands, shell, console.log);
|
|
3864
|
+
return;
|
|
3865
|
+
}
|
|
1612
3866
|
}
|
|
1613
|
-
if (
|
|
1614
|
-
|
|
3867
|
+
if (currentCommand === "status" || currentCommand === "verify" || currentCommand === "seal") {
|
|
3868
|
+
const gates = await getGateNames();
|
|
3869
|
+
if (gates.length > 0) {
|
|
3870
|
+
tabtab__default.default.log(gates, shell, console.log);
|
|
3871
|
+
return;
|
|
3872
|
+
}
|
|
3873
|
+
}
|
|
3874
|
+
if (currentCommand === "run") {
|
|
3875
|
+
const suites = await getSuiteNames();
|
|
3876
|
+
if (suites.length > 0) {
|
|
3877
|
+
tabtab__default.default.log(suites, shell, console.log);
|
|
3878
|
+
return;
|
|
3879
|
+
}
|
|
1615
3880
|
}
|
|
1616
|
-
if (
|
|
1617
|
-
|
|
3881
|
+
if (!currentCommand) {
|
|
3882
|
+
tabtab__default.default.log([...commands, ...globalOptions2], shell, console.log);
|
|
1618
3883
|
}
|
|
1619
|
-
|
|
1620
|
-
|
|
3884
|
+
}
|
|
3885
|
+
async function getIdentitySlugs() {
|
|
3886
|
+
try {
|
|
3887
|
+
const config = await core.loadLocalConfig();
|
|
3888
|
+
if (config?.identities) {
|
|
3889
|
+
return Object.keys(config.identities);
|
|
3890
|
+
}
|
|
3891
|
+
} catch {
|
|
1621
3892
|
}
|
|
1622
|
-
|
|
1623
|
-
|
|
3893
|
+
return [];
|
|
3894
|
+
}
|
|
3895
|
+
async function getGateNames() {
|
|
3896
|
+
try {
|
|
3897
|
+
const config = await core.loadConfig();
|
|
3898
|
+
if (config.gates) {
|
|
3899
|
+
return Object.keys(config.gates);
|
|
3900
|
+
}
|
|
3901
|
+
} catch {
|
|
1624
3902
|
}
|
|
1625
|
-
return
|
|
3903
|
+
return [];
|
|
1626
3904
|
}
|
|
1627
|
-
function
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
3905
|
+
async function getSuiteNames() {
|
|
3906
|
+
try {
|
|
3907
|
+
const config = await core.loadConfig();
|
|
3908
|
+
return Object.keys(config.suites);
|
|
3909
|
+
} catch {
|
|
3910
|
+
}
|
|
3911
|
+
return [];
|
|
1632
3912
|
}
|
|
3913
|
+
var completionCommand = new commander.Command("completion").description("Shell completion commands");
|
|
3914
|
+
completionCommand.command("install [shell]").description("Install shell completion (bash, zsh, or fish)").action(async (shellArg) => {
|
|
3915
|
+
try {
|
|
3916
|
+
let shell;
|
|
3917
|
+
if (shellArg !== void 0) {
|
|
3918
|
+
if (tabtab__default.default.isShellSupported(shellArg)) {
|
|
3919
|
+
shell = shellArg;
|
|
3920
|
+
} else {
|
|
3921
|
+
error(`Shell "${shellArg}" is not supported. Use bash, zsh, or fish.`);
|
|
3922
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
3923
|
+
}
|
|
3924
|
+
}
|
|
3925
|
+
await tabtab__default.default.install({
|
|
3926
|
+
name: PROGRAM_NAME,
|
|
3927
|
+
completer: PROGRAM_NAME,
|
|
3928
|
+
shell
|
|
3929
|
+
});
|
|
3930
|
+
log("");
|
|
3931
|
+
success("Shell completion installed!");
|
|
3932
|
+
log("");
|
|
3933
|
+
info("Restart your shell or run:");
|
|
3934
|
+
if (shell === "bash" || !shell) {
|
|
3935
|
+
log(" source ~/.bashrc");
|
|
3936
|
+
}
|
|
3937
|
+
if (shell === "zsh" || !shell) {
|
|
3938
|
+
log(" source ~/.zshrc");
|
|
3939
|
+
}
|
|
3940
|
+
if (shell === "fish" || !shell) {
|
|
3941
|
+
log(" source ~/.config/fish/config.fish");
|
|
3942
|
+
}
|
|
3943
|
+
log("");
|
|
3944
|
+
} catch (err) {
|
|
3945
|
+
error(`Failed to install completion: ${err instanceof Error ? err.message : String(err)}`);
|
|
3946
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
3947
|
+
}
|
|
3948
|
+
});
|
|
3949
|
+
completionCommand.command("uninstall").description("Uninstall shell completion").action(async () => {
|
|
3950
|
+
try {
|
|
3951
|
+
await tabtab__default.default.uninstall({
|
|
3952
|
+
name: PROGRAM_NAME
|
|
3953
|
+
});
|
|
3954
|
+
log("");
|
|
3955
|
+
success("Shell completion uninstalled!");
|
|
3956
|
+
log("");
|
|
3957
|
+
} catch (err) {
|
|
3958
|
+
error(`Failed to uninstall completion: ${err instanceof Error ? err.message : String(err)}`);
|
|
3959
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
3960
|
+
}
|
|
3961
|
+
});
|
|
3962
|
+
completionCommand.command("server", { hidden: true }).description("Completion server (internal)").action(async () => {
|
|
3963
|
+
const env = tabtab__default.default.parseEnv(process.env);
|
|
3964
|
+
if (env.complete) {
|
|
3965
|
+
await getCompletions(env);
|
|
3966
|
+
}
|
|
3967
|
+
});
|
|
1633
3968
|
function hasVersion(data) {
|
|
1634
3969
|
return typeof data === "object" && data !== null && "version" in data && // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
1635
3970
|
typeof data.version === "string";
|
|
@@ -1669,7 +4004,23 @@ program.addCommand(runCommand);
|
|
|
1669
4004
|
program.addCommand(keygenCommand);
|
|
1670
4005
|
program.addCommand(pruneCommand);
|
|
1671
4006
|
program.addCommand(verifyCommand);
|
|
4007
|
+
program.addCommand(sealCommand);
|
|
4008
|
+
program.addCommand(identityCommand);
|
|
4009
|
+
program.addCommand(teamCommand);
|
|
4010
|
+
program.addCommand(whoamiCommand);
|
|
4011
|
+
program.addCommand(completionCommand);
|
|
4012
|
+
function processHomeDirOption() {
|
|
4013
|
+
const homeDirIndex = process.argv.indexOf("--home-dir");
|
|
4014
|
+
if (homeDirIndex !== -1 && homeDirIndex + 1 < process.argv.length) {
|
|
4015
|
+
const homeDir = process.argv[homeDirIndex + 1];
|
|
4016
|
+
if (homeDir && !homeDir.startsWith("-")) {
|
|
4017
|
+
core.setAttestItHomeDir(homeDir);
|
|
4018
|
+
process.argv.splice(homeDirIndex, 2);
|
|
4019
|
+
}
|
|
4020
|
+
}
|
|
4021
|
+
}
|
|
1672
4022
|
async function run() {
|
|
4023
|
+
processHomeDirOption();
|
|
1673
4024
|
if (process.argv.includes("--version") || process.argv.includes("-V")) {
|
|
1674
4025
|
console.log(getPackageVersion());
|
|
1675
4026
|
process.exit(0);
|