@attest-it/cli 0.3.0 → 0.5.0

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