@attest-it/cli 0.3.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/dist/index.cjs CHANGED
@@ -13,6 +13,8 @@ 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');
16
18
  var url = require('url');
17
19
 
18
20
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
@@ -116,30 +118,63 @@ function formatTable(rows) {
116
118
  }
117
119
  return lines.join("\n");
118
120
  }
119
- function colorizeStatus(status) {
120
- const t = getTheme();
121
- switch (status) {
122
- case "VALID":
123
- return t.green(status);
124
- case "NEEDS_ATTESTATION":
125
- case "FINGERPRINT_CHANGED":
126
- return t.yellow(status);
127
- case "EXPIRED":
128
- case "INVALIDATED_BY_PARENT":
129
- return t.red(status);
130
- case "SIGNATURE_INVALID":
131
- return t.red.bold()(status);
132
- default:
133
- return status;
134
- }
135
- }
136
121
  function outputJson(data) {
137
122
  console.log(JSON.stringify(data, null, 2));
138
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
139
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("");
140
170
  return prompts.confirm({
141
- message: options.message,
142
- default: options.default ?? false
171
+ message: "",
172
+ // Empty message since we displayed it above
173
+ default: options.default ?? false,
174
+ theme: {
175
+ prefix: ""
176
+ // Remove default prefix
177
+ }
143
178
  });
144
179
  }
145
180
 
@@ -230,71 +265,68 @@ async function runInit(options) {
230
265
  process.exit(ExitCode.CONFIG_ERROR);
231
266
  }
232
267
  }
233
- var statusCommand = new commander.Command("status").description("Show attestation status for all suites").option("-s, --suite <name>", "Show status for specific suite only").option("--json", "Output JSON for machine parsing").action(async (options) => {
234
- 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);
235
270
  });
236
- async function runStatus(options) {
271
+ async function runStatus(gates, options) {
237
272
  try {
238
273
  const config = await core.loadConfig();
239
- const attestationsPath = config.settings.attestationsPath;
240
- let attestationsFile = null;
241
- try {
242
- attestationsFile = await core.readAttestations(attestationsPath);
243
- } catch (err) {
244
- if (err instanceof Error && !err.message.includes("ENOENT")) {
245
- throw err;
246
- }
247
- }
248
- const attestations = attestationsFile?.attestations ?? [];
249
- const suiteNames = options.suite ? [options.suite] : Object.keys(config.suites);
250
- if (options.suite && !config.suites[options.suite]) {
251
- error(`Suite "${options.suite}" not found in config`);
274
+ const attestItConfig = core.toAttestItConfig(config);
275
+ if (!attestItConfig.gates || Object.keys(attestItConfig.gates).length === 0) {
276
+ error("No gates defined in configuration");
252
277
  process.exit(ExitCode.CONFIG_ERROR);
253
278
  }
254
- const results = [];
255
- let hasInvalid = false;
256
- for (const suiteName of suiteNames) {
257
- const suiteConfig = config.suites[suiteName];
258
- if (!suiteConfig) continue;
259
- const fingerprintResult = await core.computeFingerprint({
260
- packages: suiteConfig.packages,
261
- ...suiteConfig.ignore && { ignore: suiteConfig.ignore }
262
- });
263
- const attestation = core.findAttestation(
264
- {
265
- schemaVersion: "1",
266
- attestations,
267
- signature: ""
268
- },
269
- suiteName
270
- );
271
- const status = determineStatus(
272
- attestation ?? null,
273
- fingerprintResult.fingerprint,
274
- config.settings.maxAgeDays
275
- );
276
- let age;
277
- if (attestation) {
278
- const attestedAt = new Date(attestation.attestedAt);
279
- age = Math.floor((Date.now() - attestedAt.getTime()) / (1e3 * 60 * 60 * 24));
280
- }
281
- if (status !== "VALID") {
282
- hasInvalid = true;
283
- }
284
- results.push({
285
- name: suiteName,
286
- status,
287
- currentFingerprint: fingerprintResult.fingerprint,
288
- attestedFingerprint: attestation?.fingerprint,
289
- attestedAt: attestation?.attestedAt,
290
- age
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 }
291
295
  });
296
+ fingerprints[gateId] = result.fingerprint;
292
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
+ });
293
322
  if (options.json) {
294
323
  outputJson(results);
295
324
  } else {
296
- displayStatusTable(results, hasInvalid);
325
+ displayStatusTable(results);
297
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
+ );
298
330
  process.exit(hasInvalid ? ExitCode.FAILURE : ExitCode.SUCCESS);
299
331
  } catch (err) {
300
332
  if (err instanceof Error) {
@@ -305,50 +337,73 @@ async function runStatus(options) {
305
337
  process.exit(ExitCode.CONFIG_ERROR);
306
338
  }
307
339
  }
308
- function determineStatus(attestation, currentFingerprint, maxAgeDays) {
309
- if (!attestation) {
310
- return "NEEDS_ATTESTATION";
311
- }
312
- if (attestation.fingerprint !== currentFingerprint) {
313
- return "FINGERPRINT_CHANGED";
314
- }
315
- const attestedAt = new Date(attestation.attestedAt);
316
- const ageInDays = Math.floor((Date.now() - attestedAt.getTime()) / (1e3 * 60 * 60 * 24));
317
- if (ageInDays > maxAgeDays) {
318
- return "EXPIRED";
319
- }
320
- return "VALID";
321
- }
322
- function displayStatusTable(results, hasInvalid) {
340
+ function displayStatusTable(results) {
323
341
  const tableRows = results.map((r) => ({
324
- suite: r.name,
325
- status: colorizeStatus(r.status),
342
+ suite: r.gateId,
343
+ status: colorizeState(r.state),
326
344
  fingerprint: r.currentFingerprint.slice(0, 16) + "...",
327
345
  age: formatAge(r)
328
346
  }));
329
347
  log("");
330
348
  log(formatTable(tableRows));
331
349
  log("");
332
- if (hasInvalid) {
333
- log("Run `attest-it run --suite <name>` to update attestations");
334
- } else {
335
- success("All attestations valid");
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("");
336
362
  }
337
- }
338
- function formatAge(result) {
339
- if (result.status === "VALID") {
340
- return `${String(result.age ?? 0)} days`;
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("");
341
370
  }
342
- if (result.status === "FINGERPRINT_CHANGED") {
343
- return "(changed)";
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`);
344
377
  }
345
- if (result.status === "NEEDS_ATTESTATION") {
346
- return "(none)";
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;
347
393
  }
348
- if (result.status === "EXPIRED") {
349
- return `${String(result.age ?? 0)} days (expired)`;
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 "-";
350
406
  }
351
- return "-";
352
407
  }
353
408
  function Header({ pendingCount }) {
354
409
  const message = `${pendingCount.toString()} suite${pendingCount === 1 ? "" : "s"} need${pendingCount === 1 ? "s" : ""} attestation`;
@@ -431,14 +486,14 @@ function SelectionPrompt({
431
486
  onSelect,
432
487
  groups
433
488
  }) {
434
- ink.useInput((input) => {
435
- const matchedOption = options.find((opt) => opt.hint === input);
489
+ ink.useInput((input5) => {
490
+ const matchedOption = options.find((opt) => opt.hint === input5);
436
491
  if (matchedOption) {
437
492
  onSelect(matchedOption.value);
438
493
  return;
439
494
  }
440
495
  if (groups) {
441
- const matchedGroup = groups.find((group) => group.name === input);
496
+ const matchedGroup = groups.find((group) => group.name === input5);
442
497
  if (matchedGroup) {
443
498
  onSelect(matchedGroup.name);
444
499
  }
@@ -488,17 +543,17 @@ function SuiteSelector({
488
543
  return next;
489
544
  });
490
545
  }, []);
491
- ink.useInput((input, key) => {
492
- if (input === "a") {
546
+ ink.useInput((input5, key) => {
547
+ if (input5 === "a") {
493
548
  setSelectedSuites(new Set(pendingSuites.map((s) => s.name)));
494
549
  return;
495
550
  }
496
- if (input === "n") {
551
+ if (input5 === "n") {
497
552
  onExit();
498
553
  return;
499
554
  }
500
- if (/^[1-9]$/.test(input)) {
501
- const idx = parseInt(input, 10) - 1;
555
+ if (/^[1-9]$/.test(input5)) {
556
+ const idx = parseInt(input5, 10) - 1;
502
557
  if (idx < pendingSuites.length) {
503
558
  const suite = pendingSuites[idx];
504
559
  if (suite) {
@@ -507,8 +562,8 @@ function SuiteSelector({
507
562
  }
508
563
  return;
509
564
  }
510
- if (input.startsWith("g") && groups) {
511
- const groupIdx = parseInt(input.slice(1), 10) - 1;
565
+ if (input5.startsWith("g") && groups) {
566
+ const groupIdx = parseInt(input5.slice(1), 10) - 1;
512
567
  const groupNames = Object.keys(groups);
513
568
  if (groupIdx >= 0 && groupIdx < groupNames.length) {
514
569
  const groupName = groupNames[groupIdx];
@@ -525,7 +580,7 @@ function SuiteSelector({
525
580
  onSelect(Array.from(selectedSuites));
526
581
  return;
527
582
  }
528
- if (input === " ") {
583
+ if (input5 === " ") {
529
584
  const currentSuite = pendingSuites[cursorIndex];
530
585
  if (currentSuite) {
531
586
  toggleSuite(currentSuite.name);
@@ -658,11 +713,11 @@ function TestRunner({
658
713
  };
659
714
  }, [currentIndex, phase, suites, executeTest, onComplete]);
660
715
  ink.useInput(
661
- (input, key) => {
716
+ (input5, key) => {
662
717
  if (phase !== "confirming") return;
663
718
  const currentSuite2 = suites[currentIndex];
664
719
  if (!currentSuite2) return;
665
- if (input.toLowerCase() === "y" || key.return) {
720
+ if (input5.toLowerCase() === "y" || key.return) {
666
721
  createAttestation3(currentSuite2).then(() => {
667
722
  setResults((prev) => ({
668
723
  ...prev,
@@ -679,7 +734,7 @@ function TestRunner({
679
734
  setPhase("running");
680
735
  });
681
736
  }
682
- if (input.toLowerCase() === "n") {
737
+ if (input5.toLowerCase() === "n") {
683
738
  setResults((prev) => ({
684
739
  ...prev,
685
740
  skipped: [...prev.skipped, currentSuite2]
@@ -848,7 +903,7 @@ function InteractiveRun({
848
903
  ] })
849
904
  ] });
850
905
  }
851
- function determineStatus2(attestation, currentFingerprint, maxAgeDays) {
906
+ function determineStatus(attestation, currentFingerprint, maxAgeDays) {
852
907
  if (!attestation) {
853
908
  return "NEEDS_ATTESTATION";
854
909
  }
@@ -874,6 +929,9 @@ async function getAllSuiteStatuses(config) {
874
929
  const attestations = attestationsFile?.attestations ?? [];
875
930
  const results = [];
876
931
  for (const [suiteName, suiteConfig] of Object.entries(config.suites)) {
932
+ if (!suiteConfig.packages) {
933
+ continue;
934
+ }
877
935
  const fingerprintResult = await core.computeFingerprint({
878
936
  packages: suiteConfig.packages,
879
937
  ...suiteConfig.ignore && { ignore: suiteConfig.ignore }
@@ -882,7 +940,7 @@ async function getAllSuiteStatuses(config) {
882
940
  { schemaVersion: "1", attestations, signature: "" },
883
941
  suiteName
884
942
  );
885
- const status = determineStatus2(
943
+ const status = determineStatus(
886
944
  attestation,
887
945
  fingerprintResult.fingerprint,
888
946
  config.settings.maxAgeDays
@@ -1039,6 +1097,9 @@ function createAttestationCreator(config) {
1039
1097
  if (!suiteConfig) {
1040
1098
  throw new Error(`Suite "${suiteName}" not found`);
1041
1099
  }
1100
+ if (!suiteConfig.packages) {
1101
+ throw new Error(`Suite "${suiteName}" has no packages defined`);
1102
+ }
1042
1103
  const fingerprintResult = await core.computeFingerprint({
1043
1104
  packages: suiteConfig.packages,
1044
1105
  ...suiteConfig.ignore && { ignore: suiteConfig.ignore }
@@ -1053,14 +1114,34 @@ function createAttestationCreator(config) {
1053
1114
  const existingFile = await core.readAttestations(attestationsPath).catch(() => null);
1054
1115
  const existingAttestations = existingFile?.attestations ?? [];
1055
1116
  const newAttestations = core.upsertAttestation(existingAttestations, attestation);
1056
- const privateKeyPath = core.getDefaultPrivateKeyPath();
1057
- if (!fs__namespace.existsSync(privateKeyPath)) {
1058
- throw new Error(`Private key not found: ${privateKeyPath}. Run "attest-it keygen" first.`);
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}`);
1059
1139
  }
1060
1140
  await core.writeSignedAttestations({
1061
1141
  filePath: attestationsPath,
1062
1142
  attestations: newAttestations,
1063
- privateKeyPath
1143
+ keyProvider,
1144
+ keyRef
1064
1145
  });
1065
1146
  log(`\u2713 Attestation created for ${suiteName}`);
1066
1147
  };
@@ -1123,7 +1204,7 @@ async function checkDirtyWorkingTree() {
1123
1204
  }
1124
1205
 
1125
1206
  // src/commands/run.ts
1126
- var runCommand = new commander.Command("run").description("Execute tests and create attestation").option("-s, --suite <name>", "Run specific suite (required unless --all or interactive mode)").option("-a, --all", "Run all suites needing attestation").option("--no-attest", "Run tests without creating attestation").option("-y, --yes", "Skip confirmation prompt").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) => {
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) => {
1127
1208
  await runTests(options);
1128
1209
  });
1129
1210
  async function runTests(options) {
@@ -1288,6 +1369,10 @@ async function runSingleSuite(suiteName, config, options) {
1288
1369
  error(`Suite "${suiteName}" not found in config`);
1289
1370
  process.exit(ExitCode.CONFIG_ERROR);
1290
1371
  }
1372
+ if (!suiteConfig.packages) {
1373
+ error(`Suite "${suiteName}" has no packages defined`);
1374
+ process.exit(ExitCode.CONFIG_ERROR);
1375
+ }
1291
1376
  log(`
1292
1377
  === Running suite: ${suiteName} ===
1293
1378
  `);
@@ -1311,9 +1396,9 @@ async function runSingleSuite(suiteName, config, options) {
1311
1396
  log("Skipping attestation (--no-attest)");
1312
1397
  return;
1313
1398
  }
1314
- const shouldAttest = options.yes ?? await confirmAction({
1315
- message: "Create attestation?",
1316
- default: true
1399
+ const shouldAttest = await confirmAction({
1400
+ message: "Create attestation",
1401
+ default: false
1317
1402
  });
1318
1403
  if (!shouldAttest) {
1319
1404
  warn("Attestation cancelled");
@@ -1329,32 +1414,546 @@ async function runSingleSuite(suiteName, config, options) {
1329
1414
  const existingFile = await core.readAttestations(attestationsPath);
1330
1415
  const existingAttestations = existingFile?.attestations ?? [];
1331
1416
  const newAttestations = core.upsertAttestation(existingAttestations, attestation);
1332
- const privateKeyPath = core.getDefaultPrivateKeyPath();
1333
- if (!fs__namespace.existsSync(privateKeyPath)) {
1334
- error(`Private key not found: ${privateKeyPath}`);
1335
- error('Run "attest-it keygen" first to generate a keypair.');
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
+ }
1336
1442
  process.exit(ExitCode.MISSING_KEY);
1337
1443
  }
1338
1444
  await core.writeSignedAttestations({
1339
1445
  filePath: attestationsPath,
1340
1446
  attestations: newAttestations,
1341
- privateKeyPath
1447
+ keyProvider,
1448
+ keyRef
1342
1449
  });
1343
1450
  success(`Attestation created for ${suiteName}`);
1344
1451
  log(` Fingerprint: ${fingerprintResult.fingerprint}`);
1345
1452
  log(` Attested by: ${attestation.attestedBy}`);
1346
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
+ }
1347
1554
  }
1348
- var keygenCommand = new commander.Command("keygen").description("Generate a new RSA keypair for signing attestations").option("-o, --output <path>", "Private key output path").option("-p, --public <path>", "Public key output path").option("-f, --force", "Overwrite existing keys").action(async (options) => {
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) => {
1349
1831
  await runKeygen(options);
1350
1832
  });
1351
1833
  async function runKeygen(options) {
1352
1834
  try {
1353
- log("Checking OpenSSL...");
1354
- const version = await core.checkOpenSSL();
1355
- info(`OpenSSL: ${version}`);
1356
- const privatePath = options.output ?? core.getDefaultPrivateKeyPath();
1357
- const publicPath = options.public ?? core.getDefaultPublicKeyPath();
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();
1358
1957
  log(`Private key: ${privatePath}`);
1359
1958
  log(`Public key: ${publicPath}`);
1360
1959
  const privateExists = fs__namespace.existsSync(privatePath);
@@ -1389,21 +1988,14 @@ async function runKeygen(options) {
1389
1988
  log("");
1390
1989
  log("Public key (commit to repo):");
1391
1990
  log(` ${result.publicPath}`);
1392
- log("");
1393
- info("Important: Back up your private key securely!");
1394
- log("");
1395
- log("Next steps:");
1396
- log(` 1. git add ${result.publicPath}`);
1397
- log(" 2. Update .attest-it/config.yaml publicKeyPath if needed");
1398
- log(" 3. attest-it run --suite <suite-name>");
1399
- } catch (err) {
1400
- if (err instanceof Error) {
1401
- error(err.message);
1402
- } else {
1403
- error("Unknown error occurred");
1404
- }
1405
- process.exit(ExitCode.CONFIG_ERROR);
1406
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>");
1407
1999
  }
1408
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) => {
1409
2001
  await runPrune(options);
@@ -1436,7 +2028,7 @@ async function runPrune(options) {
1436
2028
  let fingerprintMatches = false;
1437
2029
  if (suiteExists) {
1438
2030
  const suiteConfig = config.suites[attestation.suite];
1439
- if (suiteConfig) {
2031
+ if (suiteConfig?.packages) {
1440
2032
  const fingerprintOptions = {
1441
2033
  packages: suiteConfig.packages,
1442
2034
  ...suiteConfig.ignore && { ignore: suiteConfig.ignore }
@@ -1497,57 +2089,211 @@ async function runPrune(options) {
1497
2089
  return;
1498
2090
  }
1499
2091
  }
1500
- var verifyCommand = new commander.Command("verify").description("Verify all attestations (for CI)").option("-s, --suite <name>", "Verify specific suite only").option("--json", "Output JSON for machine parsing").option("--strict", "Fail on warnings (approaching expiry)").action(async (options) => {
1501
- 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);
1502
2094
  });
1503
- async function runVerify(options) {
2095
+ async function runVerify(gates, options) {
1504
2096
  try {
1505
2097
  const config = await core.loadConfig();
1506
- if (options.suite) {
1507
- if (!config.suites[options.suite]) {
1508
- error(`Suite "${options.suite}" not found in config`);
1509
- process.exit(ExitCode.CONFIG_ERROR);
1510
- }
1511
- const filteredSuiteEntry = config.suites[options.suite];
1512
- if (!filteredSuiteEntry) {
1513
- error(`Suite "${options.suite}" not found in config`);
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`);
1514
2109
  process.exit(ExitCode.CONFIG_ERROR);
1515
2110
  }
1516
- const filteredConfig = {
1517
- version: config.version,
1518
- settings: config.settings,
1519
- suites: { [options.suite]: filteredSuiteEntry }
1520
- };
1521
- const result2 = await core.verifyAttestations({ config: core.toAttestItConfig(filteredConfig) });
1522
- if (options.json) {
1523
- outputJson(result2);
1524
- } else {
1525
- displayResults(result2, filteredConfig.settings.maxAgeDays, options.strict);
1526
- }
1527
- if (!result2.success) {
1528
- process.exit(ExitCode.FAILURE);
1529
- return;
1530
- }
1531
- if (options.strict && hasWarnings(result2, filteredConfig.settings.maxAgeDays)) {
1532
- process.exit(ExitCode.FAILURE);
1533
- return;
1534
- }
1535
- process.exit(ExitCode.SUCCESS);
1536
- return;
1537
2111
  }
1538
- const result = await core.verifyAttestations({ config: core.toAttestItConfig(config) });
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);
1539
2128
  if (options.json) {
1540
- outputJson(result);
2129
+ outputJson(results);
1541
2130
  } else {
1542
- displayResults(result, config.settings.maxAgeDays, options.strict);
2131
+ displayResults(results);
1543
2132
  }
1544
- if (!result.success) {
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) {
1545
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");
2149
+ }
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);
1546
2253
  }
1547
- if (options.strict && hasWarnings(result, config.settings.maxAgeDays)) {
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) {
1548
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);
1549
2296
  }
1550
- process.exit(ExitCode.SUCCESS);
1551
2297
  } catch (err) {
1552
2298
  if (err instanceof Error) {
1553
2299
  error(err.message);
@@ -1557,79 +2303,1294 @@ async function runVerify(options) {
1557
2303
  process.exit(ExitCode.CONFIG_ERROR);
1558
2304
  }
1559
2305
  }
1560
- function displayResults(result, maxAgeDays, strict) {
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) {
1561
2359
  log("");
1562
- if (!result.signatureValid) {
1563
- error("Signature verification FAILED");
1564
- log("The attestations file may have been tampered with.");
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) {
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
+ }
2370
+ }
2371
+ if (summary.failed.length > 0) {
1565
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
+ }
2377
+ }
2378
+ if (summary.sealed.length === 0 && summary.skipped.length === 0 && summary.failed.length === 0) {
2379
+ log("No gates to seal");
1566
2380
  }
1567
- for (const errorMsg of result.errors) {
1568
- error(errorMsg);
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
+ }
1569
2426
  }
1570
- if (result.errors.length > 0) {
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);
1571
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);
1572
2487
  }
1573
- const tableRows = result.suites.map((s) => ({
1574
- suite: s.suite,
1575
- status: colorizeStatus(s.status),
1576
- fingerprint: s.fingerprint.slice(0, 16) + "...",
1577
- age: formatAgeColumn(s)
1578
- }));
1579
- log(formatTable(tableRows));
1580
- log("");
1581
- if (result.success) {
1582
- success("All attestations valid");
1583
- } else {
1584
- const needsAttestation = result.suites.filter((s) => s.status !== "VALID");
1585
- if (needsAttestation.length > 0) {
1586
- log("Remediation:");
1587
- for (const suite of needsAttestation) {
1588
- log(` attest-it run --suite ${suite.suite}`);
1589
- if (suite.message) {
1590
- log(` ${suite.message}`);
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";
1591
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`;
2510
+ }
2511
+ return true;
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;
1592
2625
  }
2626
+ default:
2627
+ throw new Error(`Unknown key storage type: ${keyStorageType}`);
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");
1593
2680
  }
2681
+ process.exit(ExitCode.CONFIG_ERROR);
1594
2682
  }
1595
- const warningThreshold = 7;
1596
- const nearExpiry = result.suites.filter(
1597
- (s) => s.status === "VALID" && (s.age ?? 0) > maxAgeDays - warningThreshold
1598
- );
1599
- if (nearExpiry.length > 0) {
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
+ }
2958
+ log("");
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();
1600
3134
  log("");
1601
- for (const suite of nearExpiry) {
1602
- warn(`${suite.suite} attestation approaching expiry (${String(suite.age)} days old)`);
3135
+ log(theme3.green.bold()(identity.name));
3136
+ if (identity.email) {
3137
+ log(theme3.muted(identity.email));
1603
3138
  }
1604
- if (strict) {
1605
- log("(--strict mode: warnings are treated as errors)");
3139
+ if (identity.github) {
3140
+ log(theme3.muted("@" + identity.github));
1606
3141
  }
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");
3150
+ }
3151
+ process.exit(ExitCode.CONFIG_ERROR);
1607
3152
  }
1608
3153
  }
1609
- function formatAgeColumn(s) {
1610
- if (s.status === "VALID") {
1611
- return `${String(s.age ?? 0)} days`;
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);
1612
3211
  }
1613
- if (s.status === "NEEDS_ATTESTATION") {
1614
- return "(none)";
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";
1615
3219
  }
1616
- if (s.status === "EXPIRED") {
1617
- return `${String(s.age ?? 0)} days (expired)`;
3220
+ const base64Regex = /^[A-Za-z0-9+/]+=*$/;
3221
+ if (!base64Regex.test(value)) {
3222
+ return "Public key must be valid Base64";
1618
3223
  }
1619
- if (s.status === "FINGERPRINT_CHANGED") {
1620
- return "(changed)";
3224
+ if (value.length !== 44) {
3225
+ return "Public key must be 44 characters (32 bytes in Base64)";
1621
3226
  }
1622
- if (s.status === "INVALIDATED_BY_PARENT") {
1623
- return "(invalidated)";
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";
1624
3234
  }
1625
- return "-";
3235
+ return true;
1626
3236
  }
1627
- function hasWarnings(result, maxAgeDays) {
1628
- const warningThreshold = 7;
1629
- return result.suites.some(
1630
- (s) => s.status === "VALID" && (s.age ?? 0) > maxAgeDays - warningThreshold
1631
- );
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
+ }
1632
3590
  }
3591
+
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);
1633
3594
  function hasVersion(data) {
1634
3595
  return typeof data === "object" && data !== null && "version" in data && // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
1635
3596
  typeof data.version === "string";
@@ -1669,6 +3630,10 @@ program.addCommand(runCommand);
1669
3630
  program.addCommand(keygenCommand);
1670
3631
  program.addCommand(pruneCommand);
1671
3632
  program.addCommand(verifyCommand);
3633
+ program.addCommand(sealCommand);
3634
+ program.addCommand(identityCommand);
3635
+ program.addCommand(teamCommand);
3636
+ program.addCommand(whoamiCommand);
1672
3637
  async function run() {
1673
3638
  if (process.argv.includes("--version") || process.argv.includes("-V")) {
1674
3639
  console.log(getPackageVersion());