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