@attest-it/cli 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -3,6 +3,7 @@
3
3
  var commander = require('commander');
4
4
  var fs = require('fs');
5
5
  var path = require('path');
6
+ var url = require('url');
6
7
  var chromaterm = require('chromaterm');
7
8
  var prompts = require('@inquirer/prompts');
8
9
  var core = require('@attest-it/core');
@@ -14,9 +15,7 @@ var React7 = require('react');
14
15
  var ink = require('ink');
15
16
  var jsxRuntime = require('react/jsx-runtime');
16
17
  var promises = require('fs/promises');
17
- var ui = require('@inkjs/ui');
18
18
  var yaml = require('yaml');
19
- var url = require('url');
20
19
 
21
20
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
22
21
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
@@ -278,47 +277,93 @@ async function offerCompletionInstall() {
278
277
  return false;
279
278
  }
280
279
  }
280
+ function hasVersion(data) {
281
+ return typeof data === "object" && data !== null && "version" in data && // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
282
+ typeof data.version === "string";
283
+ }
284
+ var cachedVersion;
285
+ function getPackageVersion() {
286
+ if (cachedVersion !== void 0) {
287
+ return cachedVersion;
288
+ }
289
+ const __filename = url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
290
+ const __dirname = path.dirname(__filename);
291
+ const possiblePaths = [path.join(__dirname, "../package.json"), path.join(__dirname, "../../package.json")];
292
+ for (const packageJsonPath of possiblePaths) {
293
+ try {
294
+ const content = fs.readFileSync(packageJsonPath, "utf-8");
295
+ const packageJsonData = JSON.parse(content);
296
+ if (!hasVersion(packageJsonData)) {
297
+ throw new Error(`Invalid package.json at ${packageJsonPath}: missing version field`);
298
+ }
299
+ cachedVersion = packageJsonData.version;
300
+ return cachedVersion;
301
+ } catch (error2) {
302
+ if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") {
303
+ continue;
304
+ }
305
+ throw error2;
306
+ }
307
+ }
308
+ throw new Error("Could not find package.json");
309
+ }
281
310
 
282
311
  // src/commands/init.ts
283
312
  var initCommand = new commander.Command("init").description("Initialize attest-it configuration").option("-p, --path <path>", "Config file path", ".attest-it/config.yaml").option("-f, --force", "Overwrite existing config").action(async (options) => {
284
313
  await runInit(options);
285
314
  });
286
- var CONFIG_TEMPLATE = `# attest-it configuration
287
- # See https://github.com/attest-it/attest-it for documentation
288
-
289
- version: 1
290
-
291
- settings:
292
- # How long attestations remain valid (in days)
293
- maxAgeDays: 30
294
- # Path to the public key used for signature verification
295
- publicKeyPath: .attest-it/pubkey.pem
296
- # Path to the attestations file
297
- attestationsPath: .attest-it/attestations.json
298
- # Signing algorithm
299
- algorithm: rsa
300
-
301
- # Define your test suites below. Each suite groups tests that require
302
- # human verification before their attestations are accepted.
303
- #
304
- # Example:
305
- #
306
- # suites:
307
- # visual-tests:
308
- # description: Visual regression tests requiring human review
309
- # packages:
310
- # - packages/ui
311
- # - packages/components
312
- # command: pnpm vitest packages/ui packages/components
313
- #
314
- # integration:
315
- # description: Integration tests with external services
316
- # packages:
317
- # - packages/api
318
- # command: pnpm vitest packages/api --project=integration
319
-
320
- suites: {}
321
- `;
315
+ function loadConfigTemplate() {
316
+ const __filename = url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
317
+ const __dirname = path.dirname(__filename);
318
+ const possiblePaths = [
319
+ path.join(__dirname, "../../templates/config.yaml"),
320
+ path.join(__dirname, "../templates/config.yaml")
321
+ ];
322
+ for (const templatePath of possiblePaths) {
323
+ try {
324
+ return fs__namespace.readFileSync(templatePath, "utf-8");
325
+ } catch (error2) {
326
+ if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") {
327
+ continue;
328
+ }
329
+ throw error2;
330
+ }
331
+ }
332
+ throw new Error("Could not find config.yaml template");
333
+ }
334
+ function isPackageJson(data) {
335
+ return typeof data === "object" && data !== null && "name" in data && "version" in data && // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
336
+ typeof data.name === "string" && // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
337
+ typeof data.version === "string";
338
+ }
339
+ function detectPackageManager() {
340
+ if (fs__namespace.existsSync("pnpm-lock.yaml")) return "pnpm";
341
+ if (fs__namespace.existsSync("yarn.lock")) return "yarn";
342
+ if (fs__namespace.existsSync("bun.lockb")) return "bun";
343
+ return "npm";
344
+ }
345
+ async function ensureDevDependency() {
346
+ const packageJsonPath = "package.json";
347
+ const packageManager = detectPackageManager();
348
+ let created = false;
349
+ let packageJson;
350
+ if (fs__namespace.existsSync(packageJsonPath)) {
351
+ const content = await fs__namespace.promises.readFile(packageJsonPath, "utf8");
352
+ const parsed = JSON.parse(content);
353
+ if (!isPackageJson(parsed)) {
354
+ throw new Error("Invalid package.json: missing required name or version field");
355
+ }
356
+ packageJson = parsed;
357
+ } else {
358
+ packageJson = { name: path__namespace.basename(process.cwd()), version: "1.0.0" };
359
+ created = true;
360
+ }
361
+ const devDeps = packageJson.devDependencies ?? {};
362
+ devDeps["attest-it"] = "^" + getPackageVersion();
363
+ packageJson.devDependencies = devDeps;
364
+ await fs__namespace.promises.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n");
365
+ return { packageManager, created };
366
+ }
322
367
  async function runInit(options) {
323
368
  try {
324
369
  const configPath = path__namespace.resolve(options.path);
@@ -333,14 +378,22 @@ async function runInit(options) {
333
378
  process.exit(ExitCode.CANCELLED);
334
379
  }
335
380
  }
381
+ const { packageManager, created } = await ensureDevDependency();
382
+ if (created) {
383
+ success("Created package.json");
384
+ } else {
385
+ success("Updated package.json with attest-it devDependency");
386
+ }
336
387
  await fs__namespace.promises.mkdir(configDir, { recursive: true });
337
- await fs__namespace.promises.writeFile(configPath, CONFIG_TEMPLATE, "utf-8");
388
+ const configTemplate = loadConfigTemplate();
389
+ await fs__namespace.promises.writeFile(configPath, configTemplate, "utf-8");
338
390
  success(`Configuration created at ${configPath}`);
339
391
  log("");
340
392
  log("Next steps:");
341
- log(` 1. Edit ${options.path} to define your test suites`);
342
- log(" 2. Run: attest-it keygen");
343
- log(" 3. Run: attest-it status");
393
+ log(` 1. Run: ${packageManager} install`);
394
+ log(" 2. Run: attest-it identity create (if you haven't already)");
395
+ log(" 3. Run: attest-it team join");
396
+ log(" 4. Edit .attest-it/config.yaml to define your gates and suites");
344
397
  await offerCompletionInstall();
345
398
  } catch (err) {
346
399
  if (err instanceof Error) {
@@ -572,14 +625,14 @@ function SelectionPrompt({
572
625
  onSelect,
573
626
  groups
574
627
  }) {
575
- ink.useInput((input5) => {
576
- const matchedOption = options.find((opt) => opt.hint === input5);
628
+ ink.useInput((input4) => {
629
+ const matchedOption = options.find((opt) => opt.hint === input4);
577
630
  if (matchedOption) {
578
631
  onSelect(matchedOption.value);
579
632
  return;
580
633
  }
581
634
  if (groups) {
582
- const matchedGroup = groups.find((group) => group.name === input5);
635
+ const matchedGroup = groups.find((group) => group.name === input4);
583
636
  if (matchedGroup) {
584
637
  onSelect(matchedGroup.name);
585
638
  }
@@ -629,17 +682,17 @@ function SuiteSelector({
629
682
  return next;
630
683
  });
631
684
  }, []);
632
- ink.useInput((input5, key) => {
633
- if (input5 === "a") {
685
+ ink.useInput((input4, key) => {
686
+ if (input4 === "a") {
634
687
  setSelectedSuites(new Set(pendingSuites.map((s) => s.name)));
635
688
  return;
636
689
  }
637
- if (input5 === "n") {
690
+ if (input4 === "n") {
638
691
  onExit();
639
692
  return;
640
693
  }
641
- if (/^[1-9]$/.test(input5)) {
642
- const idx = parseInt(input5, 10) - 1;
694
+ if (/^[1-9]$/.test(input4)) {
695
+ const idx = parseInt(input4, 10) - 1;
643
696
  if (idx < pendingSuites.length) {
644
697
  const suite = pendingSuites[idx];
645
698
  if (suite) {
@@ -648,8 +701,8 @@ function SuiteSelector({
648
701
  }
649
702
  return;
650
703
  }
651
- if (input5.startsWith("g") && groups) {
652
- const groupIdx = parseInt(input5.slice(1), 10) - 1;
704
+ if (input4.startsWith("g") && groups) {
705
+ const groupIdx = parseInt(input4.slice(1), 10) - 1;
653
706
  const groupNames = Object.keys(groups);
654
707
  if (groupIdx >= 0 && groupIdx < groupNames.length) {
655
708
  const groupName = groupNames[groupIdx];
@@ -666,7 +719,7 @@ function SuiteSelector({
666
719
  onSelect(Array.from(selectedSuites));
667
720
  return;
668
721
  }
669
- if (input5 === " ") {
722
+ if (input4 === " ") {
670
723
  const currentSuite = pendingSuites[cursorIndex];
671
724
  if (currentSuite) {
672
725
  toggleSuite(currentSuite.name);
@@ -799,11 +852,11 @@ function TestRunner({
799
852
  };
800
853
  }, [currentIndex, phase, suites, executeTest, onComplete]);
801
854
  ink.useInput(
802
- (input5, key) => {
855
+ (input4, key) => {
803
856
  if (phase !== "confirming") return;
804
857
  const currentSuite2 = suites[currentIndex];
805
858
  if (!currentSuite2) return;
806
- if (input5.toLowerCase() === "y" || key.return) {
859
+ if (input4.toLowerCase() === "y" || key.return) {
807
860
  createAttestation3(currentSuite2).then(() => {
808
861
  setResults((prev) => ({
809
862
  ...prev,
@@ -820,7 +873,7 @@ function TestRunner({
820
873
  setPhase("running");
821
874
  });
822
875
  }
823
- if (input5.toLowerCase() === "n") {
876
+ if (input4.toLowerCase() === "n") {
824
877
  setResults((prev) => ({
825
878
  ...prev,
826
879
  skipped: [...prev.skipped, currentSuite2]
@@ -1533,9 +1586,9 @@ async function runSingleSuite(suiteName, config, options) {
1533
1586
  if (!await keyProvider.keyExists(keyRef)) {
1534
1587
  error(`Private key not found in ${keyProvider.displayName}`);
1535
1588
  if (keyProvider.type === "filesystem") {
1536
- error('Run "attest-it keygen" first to generate a keypair.');
1589
+ error('Run "attest-it identity create" first to generate a keypair.');
1537
1590
  } else {
1538
- error('Run "attest-it keygen" to generate and store a key.');
1591
+ error('Run "attest-it identity create" to generate and store a key.');
1539
1592
  }
1540
1593
  process.exit(ExitCode.MISSING_KEY);
1541
1594
  }
@@ -1559,7 +1612,7 @@ async function promptForSeal(suiteName, gateId, config) {
1559
1612
  const localConfig = core.loadLocalConfigSync();
1560
1613
  if (!localConfig) {
1561
1614
  warn("No local identity configuration found - cannot create seal");
1562
- warn('Run "attest-it keygen" to set up your identity');
1615
+ warn('Run "attest-it identity create" to set up your identity');
1563
1616
  return;
1564
1617
  }
1565
1618
  const identity = core.getActiveIdentity(localConfig);
@@ -1594,8 +1647,8 @@ async function promptForSeal(suiteName, gateId, config) {
1594
1647
  const keyProvider = createKeyProviderFromIdentity(identity);
1595
1648
  const keyRef = getKeyRefFromIdentity(identity);
1596
1649
  const keyResult = await keyProvider.getPrivateKey(keyRef);
1597
- const fs4 = await import('fs/promises');
1598
- const privateKeyPem = await fs4.readFile(keyResult.keyPath, "utf8");
1650
+ const fs3 = await import('fs/promises');
1651
+ const privateKeyPem = await fs3.readFile(keyResult.keyPath, "utf8");
1599
1652
  await keyResult.cleanup();
1600
1653
  const identitySlug = localConfig.activeIdentity;
1601
1654
  const seal = core.createSeal({
@@ -1677,697 +1730,6 @@ function getKeyRefFromIdentity(identity) {
1677
1730
  }
1678
1731
  }
1679
1732
  }
1680
- var MIN_PASSPHRASE_LENGTH = 8;
1681
- function KeygenInteractive(props) {
1682
- const { onComplete, onCancel, onError } = props;
1683
- const [step, setStep] = React7.useState("checking-providers");
1684
- const [opAvailable, setOpAvailable] = React7.useState(false);
1685
- const [keychainAvailable, setKeychainAvailable] = React7.useState(false);
1686
- const [yubiKeyAvailable, setYubiKeyAvailable] = React7.useState(false);
1687
- const [accounts, setAccounts] = React7.useState([]);
1688
- const [vaults, setVaults] = React7.useState([]);
1689
- const [yubiKeyDevices, setYubiKeyDevices] = React7.useState([]);
1690
- const [_selectedProvider, setSelectedProvider] = React7.useState();
1691
- const [selectedAccount, setSelectedAccount] = React7.useState();
1692
- const [selectedVault, setSelectedVault] = React7.useState();
1693
- const [itemName, setItemName] = React7.useState("attest-it-private-key");
1694
- const [keychainItemName, setKeychainItemName] = React7.useState("attest-it-private-key");
1695
- const [selectedYubiKeySerial, setSelectedYubiKeySerial] = React7.useState();
1696
- const [selectedYubiKeySlot, setSelectedYubiKeySlot] = React7.useState(2);
1697
- const [slot1Configured, setSlot1Configured] = React7.useState(false);
1698
- const [slot2Configured, setSlot2Configured] = React7.useState(false);
1699
- const [encryptionPassphrase, setEncryptionPassphrase] = React7.useState();
1700
- ink.useInput((_input, key) => {
1701
- if (key.escape) {
1702
- onCancel();
1703
- }
1704
- });
1705
- React7.useEffect(() => {
1706
- const checkProviders = async () => {
1707
- try {
1708
- const isInstalled = await core.OnePasswordKeyProvider.isInstalled();
1709
- setOpAvailable(isInstalled);
1710
- if (isInstalled) {
1711
- const accountList = await core.OnePasswordKeyProvider.listAccounts();
1712
- setAccounts(accountList);
1713
- }
1714
- } catch {
1715
- setOpAvailable(false);
1716
- }
1717
- const isKeychainAvailable = core.MacOSKeychainKeyProvider.isAvailable();
1718
- setKeychainAvailable(isKeychainAvailable);
1719
- try {
1720
- const isInstalled = await core.YubiKeyProvider.isInstalled();
1721
- if (isInstalled) {
1722
- const isConnected = await core.YubiKeyProvider.isConnected();
1723
- if (isConnected) {
1724
- const devices = await core.YubiKeyProvider.listDevices();
1725
- setYubiKeyDevices(devices);
1726
- setYubiKeyAvailable(devices.length > 0);
1727
- }
1728
- }
1729
- } catch {
1730
- setYubiKeyAvailable(false);
1731
- }
1732
- setStep("select-provider");
1733
- };
1734
- void checkProviders();
1735
- }, []);
1736
- React7.useEffect(() => {
1737
- if (step === "select-vault" && selectedAccount) {
1738
- const fetchVaults = async () => {
1739
- try {
1740
- const vaultList = await core.OnePasswordKeyProvider.listVaults(selectedAccount);
1741
- setVaults(vaultList);
1742
- } catch (err) {
1743
- onError(err instanceof Error ? err : new Error("Failed to fetch vaults"));
1744
- }
1745
- };
1746
- void fetchVaults();
1747
- }
1748
- }, [step, selectedAccount, onError]);
1749
- const checkYubiKeySlots = async (serial) => {
1750
- try {
1751
- const slot1 = await core.YubiKeyProvider.isChallengeResponseConfigured(1, serial);
1752
- const slot2 = await core.YubiKeyProvider.isChallengeResponseConfigured(2, serial);
1753
- setSlot1Configured(slot1);
1754
- setSlot2Configured(slot2);
1755
- if (slot1 && slot2) {
1756
- setStep("select-yubikey-slot");
1757
- } else if (slot2) {
1758
- setSelectedYubiKeySlot(2);
1759
- void generateKeys("yubikey");
1760
- } else if (slot1) {
1761
- setSelectedYubiKeySlot(1);
1762
- void generateKeys("yubikey");
1763
- } else {
1764
- setStep("yubikey-offer-setup");
1765
- }
1766
- } catch (err) {
1767
- onError(err instanceof Error ? err : new Error("Failed to check YubiKey slots"));
1768
- }
1769
- };
1770
- const handleProviderSelect = (value) => {
1771
- if (value === "filesystem") {
1772
- setSelectedProvider("filesystem");
1773
- setStep("select-filesystem-encryption");
1774
- } else if (value === "1password") {
1775
- setSelectedProvider("1password");
1776
- if (accounts.length === 1 && accounts[0]) {
1777
- setSelectedAccount(accounts[0].user_uuid);
1778
- setStep("select-vault");
1779
- } else {
1780
- setStep("select-account");
1781
- }
1782
- } else if (value === "macos-keychain") {
1783
- setSelectedProvider("macos-keychain");
1784
- setStep("enter-keychain-item-name");
1785
- } else if (value === "yubikey") {
1786
- setSelectedProvider("yubikey");
1787
- if (yubiKeyDevices.length > 1) {
1788
- setStep("select-yubikey-device");
1789
- } else if (yubiKeyDevices.length === 1 && yubiKeyDevices[0]) {
1790
- setSelectedYubiKeySerial(yubiKeyDevices[0].serial);
1791
- void checkYubiKeySlots(yubiKeyDevices[0].serial);
1792
- }
1793
- }
1794
- };
1795
- const handleAccountSelect = (value) => {
1796
- setSelectedAccount(value);
1797
- setStep("select-vault");
1798
- };
1799
- const handleVaultSelect = (value) => {
1800
- setSelectedVault(value);
1801
- setStep("enter-item-name");
1802
- };
1803
- const handleItemNameSubmit = (value) => {
1804
- setItemName(value);
1805
- void generateKeys("1password");
1806
- };
1807
- const handleKeychainItemNameSubmit = (value) => {
1808
- setKeychainItemName(value);
1809
- void generateKeys("macos-keychain");
1810
- };
1811
- const handleYubiKeyDeviceSelect = (value) => {
1812
- setSelectedYubiKeySerial(value);
1813
- void checkYubiKeySlots(value);
1814
- };
1815
- const handleYubiKeySlotSelect = (value) => {
1816
- const slot = value === "1" ? 1 : 2;
1817
- setSelectedYubiKeySlot(slot);
1818
- void generateKeys("yubikey");
1819
- };
1820
- const handleYubiKeySetupConfirm = (value) => {
1821
- if (value === "yes") {
1822
- void setupYubiKeySlot();
1823
- } else {
1824
- onError(new Error("YubiKey setup cancelled"));
1825
- }
1826
- };
1827
- const handleEncryptionMethodSelect = (value) => {
1828
- if (value === "passphrase") {
1829
- setStep("enter-encryption-passphrase");
1830
- } else {
1831
- setEncryptionPassphrase(void 0);
1832
- void generateKeys("filesystem");
1833
- }
1834
- };
1835
- const handleEncryptionPassphrase = (value) => {
1836
- if (value.length < MIN_PASSPHRASE_LENGTH) {
1837
- onError(new Error(`Passphrase must be at least ${String(MIN_PASSPHRASE_LENGTH)} characters`));
1838
- return;
1839
- }
1840
- setEncryptionPassphrase(value);
1841
- setStep("confirm-encryption-passphrase");
1842
- };
1843
- const handleConfirmPassphrase = (value) => {
1844
- if (value !== encryptionPassphrase) {
1845
- onError(new Error("Passphrases do not match. Please try again."));
1846
- setEncryptionPassphrase(void 0);
1847
- setStep("enter-encryption-passphrase");
1848
- return;
1849
- }
1850
- void generateKeys("filesystem");
1851
- };
1852
- const setupYubiKeySlot = async () => {
1853
- setStep("yubikey-configuring");
1854
- try {
1855
- const { spawn: spawn3 } = await import('child_process');
1856
- const args = ["otp", "chalresp", "--touch", "--generate", "2"];
1857
- if (selectedYubiKeySerial) {
1858
- args.unshift("--device", selectedYubiKeySerial);
1859
- }
1860
- await new Promise((resolve2, reject) => {
1861
- const proc = spawn3("ykman", args, { stdio: "inherit" });
1862
- proc.on("close", (code) => {
1863
- if (code === 0) {
1864
- resolve2();
1865
- } else {
1866
- reject(new Error(`ykman exited with code ${String(code)}`));
1867
- }
1868
- });
1869
- proc.on("error", reject);
1870
- });
1871
- setSelectedYubiKeySlot(2);
1872
- void generateKeys("yubikey");
1873
- } catch (err) {
1874
- onError(err instanceof Error ? err : new Error("Failed to configure YubiKey"));
1875
- }
1876
- };
1877
- const generateKeys = async (provider) => {
1878
- setStep("generating");
1879
- try {
1880
- const publicKeyPath = props.publicKeyPath ?? core.getDefaultPublicKeyPath();
1881
- if (provider === "filesystem") {
1882
- const fsProvider = new core.FilesystemKeyProvider();
1883
- const genOptions = {
1884
- publicKeyPath
1885
- };
1886
- if (props.force !== void 0) {
1887
- genOptions.force = props.force;
1888
- }
1889
- if (encryptionPassphrase !== void 0) {
1890
- genOptions.passphrase = encryptionPassphrase;
1891
- }
1892
- const result = await fsProvider.generateKeyPair(genOptions);
1893
- const completionResult = {
1894
- provider: "filesystem",
1895
- publicKeyPath: result.publicKeyPath,
1896
- privateKeyRef: result.privateKeyRef,
1897
- storageDescription: result.storageDescription
1898
- };
1899
- if (result.encrypted) {
1900
- completionResult.encrypted = result.encrypted;
1901
- }
1902
- onComplete(completionResult);
1903
- } else if (provider === "1password") {
1904
- if (!selectedVault || !itemName) {
1905
- throw new Error("Vault and item name are required for 1Password");
1906
- }
1907
- const vault = vaults.find((v) => v.id === selectedVault);
1908
- if (!vault) {
1909
- throw new Error("Selected vault not found");
1910
- }
1911
- const providerOptions = {
1912
- vault: vault.name,
1913
- itemName
1914
- };
1915
- if (selectedAccount !== void 0) {
1916
- providerOptions.account = selectedAccount;
1917
- }
1918
- const opProvider = new core.OnePasswordKeyProvider(providerOptions);
1919
- const genOptions = { publicKeyPath };
1920
- if (props.force !== void 0) {
1921
- genOptions.force = props.force;
1922
- }
1923
- const result = await opProvider.generateKeyPair(genOptions);
1924
- const completionResult = {
1925
- provider: "1password",
1926
- publicKeyPath: result.publicKeyPath,
1927
- privateKeyRef: result.privateKeyRef,
1928
- storageDescription: result.storageDescription,
1929
- vault: vault.name,
1930
- itemName
1931
- };
1932
- if (selectedAccount !== void 0) {
1933
- completionResult.account = selectedAccount;
1934
- }
1935
- onComplete(completionResult);
1936
- } else if (provider === "macos-keychain") {
1937
- if (!keychainItemName) {
1938
- throw new Error("Item name is required for macOS Keychain");
1939
- }
1940
- const keychainProvider = new core.MacOSKeychainKeyProvider({
1941
- itemName: keychainItemName
1942
- });
1943
- const genOptions = { publicKeyPath };
1944
- if (props.force !== void 0) {
1945
- genOptions.force = props.force;
1946
- }
1947
- const result = await keychainProvider.generateKeyPair(genOptions);
1948
- onComplete({
1949
- provider: "macos-keychain",
1950
- publicKeyPath: result.publicKeyPath,
1951
- privateKeyRef: result.privateKeyRef,
1952
- storageDescription: result.storageDescription,
1953
- itemName: keychainItemName
1954
- });
1955
- } else {
1956
- const encryptedKeyPath = core.getDefaultYubiKeyEncryptedKeyPath();
1957
- const providerOptions = {
1958
- encryptedKeyPath,
1959
- slot: selectedYubiKeySlot
1960
- };
1961
- if (selectedYubiKeySerial !== void 0) {
1962
- providerOptions.serial = selectedYubiKeySerial;
1963
- }
1964
- const ykProvider = new core.YubiKeyProvider(providerOptions);
1965
- const genOptions = { publicKeyPath };
1966
- if (props.force !== void 0) {
1967
- genOptions.force = props.force;
1968
- }
1969
- const result = await ykProvider.generateKeyPair(genOptions);
1970
- const completionResult = {
1971
- provider: "yubikey",
1972
- publicKeyPath: result.publicKeyPath,
1973
- privateKeyRef: result.privateKeyRef,
1974
- storageDescription: result.storageDescription,
1975
- slot: selectedYubiKeySlot,
1976
- encryptedKeyPath
1977
- };
1978
- if (selectedYubiKeySerial !== void 0) {
1979
- completionResult.serial = selectedYubiKeySerial;
1980
- }
1981
- onComplete(completionResult);
1982
- }
1983
- setStep("done");
1984
- } catch (err) {
1985
- onError(err instanceof Error ? err : new Error("Key generation failed"));
1986
- } finally {
1987
- setEncryptionPassphrase(void 0);
1988
- }
1989
- };
1990
- if (step === "checking-providers") {
1991
- return /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { flexDirection: "column", children: /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", gap: 1, children: [
1992
- /* @__PURE__ */ jsxRuntime.jsx(ui.Spinner, {}),
1993
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: "Checking available key storage providers..." })
1994
- ] }) });
1995
- }
1996
- if (step === "select-provider") {
1997
- const options = [
1998
- {
1999
- label: `Local Filesystem (${core.getDefaultPrivateKeyPath()})`,
2000
- value: "filesystem"
2001
- }
2002
- ];
2003
- if (keychainAvailable) {
2004
- options.push({
2005
- label: "macOS Keychain",
2006
- value: "macos-keychain"
2007
- });
2008
- }
2009
- if (yubiKeyAvailable) {
2010
- options.push({
2011
- label: "YubiKey (hardware security key)",
2012
- value: "yubikey"
2013
- });
2014
- }
2015
- if (opAvailable) {
2016
- options.push({
2017
- label: "1Password (requires op CLI)",
2018
- value: "1password"
2019
- });
2020
- }
2021
- return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
2022
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Where would you like to store your private key?" }),
2023
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "" }),
2024
- /* @__PURE__ */ jsxRuntime.jsx(ui.Select, { options, onChange: handleProviderSelect })
2025
- ] });
2026
- }
2027
- if (step === "select-filesystem-encryption") {
2028
- return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
2029
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Would you like to encrypt your private key with a passphrase?" }),
2030
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "A passphrase adds extra security but must be entered each time you sign." }),
2031
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "" }),
2032
- /* @__PURE__ */ jsxRuntime.jsx(
2033
- ui.Select,
2034
- {
2035
- options: [
2036
- { label: "No encryption (key protected by file permissions only)", value: "none" },
2037
- { label: "Passphrase protection (AES-256 encryption)", value: "passphrase" }
2038
- ],
2039
- onChange: handleEncryptionMethodSelect
2040
- }
2041
- )
2042
- ] });
2043
- }
2044
- if (step === "enter-encryption-passphrase") {
2045
- return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
2046
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Enter a passphrase to encrypt your private key:" }),
2047
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: `(Minimum ${String(MIN_PASSPHRASE_LENGTH)} characters. You will need this passphrase each time you sign.)` }),
2048
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "" }),
2049
- /* @__PURE__ */ jsxRuntime.jsx(ui.PasswordInput, { onSubmit: handleEncryptionPassphrase })
2050
- ] });
2051
- }
2052
- if (step === "confirm-encryption-passphrase") {
2053
- return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
2054
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Confirm your passphrase:" }),
2055
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "(Enter the same passphrase again to confirm.)" }),
2056
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "" }),
2057
- /* @__PURE__ */ jsxRuntime.jsx(ui.PasswordInput, { onSubmit: handleConfirmPassphrase })
2058
- ] });
2059
- }
2060
- if (step === "select-account") {
2061
- const options = accounts.map((account) => ({
2062
- label: account.name ? `${account.name} (${account.email})` : account.email,
2063
- value: account.user_uuid
2064
- }));
2065
- return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
2066
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Select 1Password account:" }),
2067
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "" }),
2068
- /* @__PURE__ */ jsxRuntime.jsx(ui.Select, { options, onChange: handleAccountSelect })
2069
- ] });
2070
- }
2071
- if (step === "select-vault") {
2072
- if (vaults.length === 0) {
2073
- return /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { flexDirection: "column", children: /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", gap: 1, children: [
2074
- /* @__PURE__ */ jsxRuntime.jsx(ui.Spinner, {}),
2075
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: "Loading vaults..." })
2076
- ] }) });
2077
- }
2078
- const options = vaults.map((vault) => ({
2079
- label: vault.name,
2080
- value: vault.id
2081
- }));
2082
- return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
2083
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Select vault for private key storage:" }),
2084
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "" }),
2085
- /* @__PURE__ */ jsxRuntime.jsx(ui.Select, { options, onChange: handleVaultSelect })
2086
- ] });
2087
- }
2088
- if (step === "enter-item-name") {
2089
- return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
2090
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Enter name for the key item:" }),
2091
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "(This will be visible in your 1Password vault)" }),
2092
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "" }),
2093
- /* @__PURE__ */ jsxRuntime.jsx(ui.TextInput, { defaultValue: itemName, onSubmit: handleItemNameSubmit })
2094
- ] });
2095
- }
2096
- if (step === "enter-keychain-item-name") {
2097
- return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
2098
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Enter name for the keychain item:" }),
2099
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "(This will be the service name in your macOS Keychain)" }),
2100
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "" }),
2101
- /* @__PURE__ */ jsxRuntime.jsx(ui.TextInput, { defaultValue: keychainItemName, onSubmit: handleKeychainItemNameSubmit })
2102
- ] });
2103
- }
2104
- if (step === "select-yubikey-device") {
2105
- const options = yubiKeyDevices.map((device) => ({
2106
- label: `${device.type} (Serial: ${device.serial})`,
2107
- value: device.serial
2108
- }));
2109
- return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
2110
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Select YubiKey device:" }),
2111
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "" }),
2112
- /* @__PURE__ */ jsxRuntime.jsx(ui.Select, { options, onChange: handleYubiKeyDeviceSelect })
2113
- ] });
2114
- }
2115
- if (step === "select-yubikey-slot") {
2116
- const options = [];
2117
- if (slot2Configured) {
2118
- options.push({
2119
- label: "Slot 2 (recommended)",
2120
- value: "2"
2121
- });
2122
- }
2123
- if (slot1Configured) {
2124
- options.push({
2125
- label: "Slot 1",
2126
- value: "1"
2127
- });
2128
- }
2129
- return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
2130
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Select YubiKey slot for challenge-response:" }),
2131
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "" }),
2132
- /* @__PURE__ */ jsxRuntime.jsx(ui.Select, { options, onChange: handleYubiKeySlotSelect })
2133
- ] });
2134
- }
2135
- if (step === "yubikey-offer-setup") {
2136
- return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
2137
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Your YubiKey is not configured for challenge-response." }),
2138
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "" }),
2139
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: "Would you like to configure slot 2 for challenge-response now?" }),
2140
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "This will enable touch-to-sign functionality." }),
2141
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "" }),
2142
- /* @__PURE__ */ jsxRuntime.jsx(
2143
- ui.Select,
2144
- {
2145
- options: [
2146
- { label: "Yes, configure my YubiKey", value: "yes" },
2147
- { label: "No, cancel", value: "no" }
2148
- ],
2149
- onChange: handleYubiKeySetupConfirm
2150
- }
2151
- )
2152
- ] });
2153
- }
2154
- if (step === "yubikey-configuring") {
2155
- return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
2156
- /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", gap: 1, children: [
2157
- /* @__PURE__ */ jsxRuntime.jsx(ui.Spinner, {}),
2158
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: "Configuring YubiKey slot 2 for challenge-response..." })
2159
- ] }),
2160
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Touch your YubiKey when it flashes." })
2161
- ] });
2162
- }
2163
- if (step === "generating") {
2164
- return /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { flexDirection: "column", children: /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", gap: 1, children: [
2165
- /* @__PURE__ */ jsxRuntime.jsx(ui.Spinner, {}),
2166
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: "Generating RSA-2048 keypair..." })
2167
- ] }) });
2168
- }
2169
- return /* @__PURE__ */ jsxRuntime.jsx(ink.Box, {});
2170
- }
2171
- async function runKeygenInteractive(options) {
2172
- return new Promise((resolve2, reject) => {
2173
- const props = {
2174
- onComplete: (result) => {
2175
- unmount();
2176
- resolve2(result);
2177
- },
2178
- onCancel: () => {
2179
- unmount();
2180
- reject(new Error("Keygen cancelled"));
2181
- },
2182
- onError: (error2) => {
2183
- unmount();
2184
- reject(error2);
2185
- }
2186
- };
2187
- if (options.publicKeyPath !== void 0) {
2188
- props.publicKeyPath = options.publicKeyPath;
2189
- }
2190
- if (options.force !== void 0) {
2191
- props.force = options.force;
2192
- }
2193
- const { unmount } = ink.render(/* @__PURE__ */ jsxRuntime.jsx(KeygenInteractive, { ...props }));
2194
- });
2195
- }
2196
-
2197
- // src/commands/keygen.ts
2198
- var keygenCommand = new commander.Command("keygen").description("Generate a new RSA keypair for signing attestations").option("-o, --output <path>", "Public key output path").option("-p, --private <path>", "Private key output path (filesystem only)").option(
2199
- "--provider <type>",
2200
- "Key provider: filesystem, 1password, or macos-keychain (skips interactive)"
2201
- ).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) => {
2202
- await runKeygen(options);
2203
- });
2204
- async function runKeygen(options) {
2205
- try {
2206
- const useInteractive = options.interactive !== false && !options.provider;
2207
- if (useInteractive) {
2208
- const interactiveOptions = {};
2209
- if (options.output !== void 0) {
2210
- interactiveOptions.publicKeyPath = options.output;
2211
- }
2212
- if (options.force !== void 0) {
2213
- interactiveOptions.force = options.force;
2214
- }
2215
- const result = await runKeygenInteractive(interactiveOptions);
2216
- success("Keypair generated successfully!");
2217
- log("");
2218
- log("Private key stored in:");
2219
- log(` ${result.storageDescription}`);
2220
- log("");
2221
- log("Public key (commit to repo):");
2222
- log(` ${result.publicKeyPath}`);
2223
- log("");
2224
- if (result.provider === "1password") {
2225
- log("Add to your .attest-it/config.yaml:");
2226
- log("");
2227
- log("settings:");
2228
- log(` publicKeyPath: ${result.publicKeyPath}`);
2229
- log(" keyProvider:");
2230
- log(" type: 1password");
2231
- log(" options:");
2232
- if (result.account) {
2233
- log(` account: ${result.account}`);
2234
- }
2235
- log(` vault: ${result.vault ?? ""}`);
2236
- log(` itemName: ${result.itemName ?? ""}`);
2237
- log("");
2238
- } else if (result.provider === "macos-keychain") {
2239
- log("Add to your .attest-it/config.yaml:");
2240
- log("");
2241
- log("settings:");
2242
- log(` publicKeyPath: ${result.publicKeyPath}`);
2243
- log(" keyProvider:");
2244
- log(" type: macos-keychain");
2245
- log(" options:");
2246
- log(` itemName: ${result.itemName ?? ""}`);
2247
- log("");
2248
- }
2249
- log("Next steps:");
2250
- log(` 1. git add ${result.publicKeyPath}`);
2251
- if (result.provider === "1password" || result.provider === "macos-keychain") {
2252
- log(" 2. Update .attest-it/config.yaml with keyProvider settings");
2253
- } else {
2254
- log(" 2. Update .attest-it/config.yaml publicKeyPath if needed");
2255
- }
2256
- log(" 3. attest-it run --suite <suite-name>");
2257
- } else {
2258
- await runNonInteractiveKeygen(options);
2259
- }
2260
- } catch (err) {
2261
- if (err instanceof Error) {
2262
- error(err.message);
2263
- } else {
2264
- error("Unknown error occurred");
2265
- }
2266
- process.exit(ExitCode.CONFIG_ERROR);
2267
- }
2268
- }
2269
- async function runNonInteractiveKeygen(options) {
2270
- log("Checking OpenSSL...");
2271
- const version = await core.checkOpenSSL();
2272
- info(`OpenSSL: ${version}`);
2273
- const publicPath = options.output ?? core.getDefaultPublicKeyPath();
2274
- if (options.provider === "1password") {
2275
- if (!options.vault || !options.itemName) {
2276
- throw new Error("--vault and --item-name are required for 1password provider");
2277
- }
2278
- const providerOptions = {
2279
- vault: options.vault,
2280
- itemName: options.itemName
2281
- };
2282
- if (options.account !== void 0) {
2283
- providerOptions.account = options.account;
2284
- }
2285
- const provider = new core.OnePasswordKeyProvider(providerOptions);
2286
- log(`Generating keypair with 1Password storage...`);
2287
- log(`Vault: ${options.vault}`);
2288
- log(`Item: ${options.itemName}`);
2289
- const genOptions = { publicKeyPath: publicPath };
2290
- if (options.force !== void 0) {
2291
- genOptions.force = options.force;
2292
- }
2293
- const result = await provider.generateKeyPair(genOptions);
2294
- success("Keypair generated successfully!");
2295
- log("");
2296
- log("Private key stored in:");
2297
- log(` ${result.storageDescription}`);
2298
- log("");
2299
- log("Public key (commit to repo):");
2300
- log(` ${result.publicKeyPath}`);
2301
- } else if (options.provider === "macos-keychain") {
2302
- if (!options.itemName) {
2303
- throw new Error("--item-name is required for macos-keychain provider");
2304
- }
2305
- const isAvailable = core.MacOSKeychainKeyProvider.isAvailable();
2306
- if (!isAvailable) {
2307
- throw new Error("macOS Keychain is not available on this platform");
2308
- }
2309
- const provider = new core.MacOSKeychainKeyProvider({
2310
- itemName: options.itemName
2311
- });
2312
- log(`Generating keypair with macOS Keychain storage...`);
2313
- log(`Item: ${options.itemName}`);
2314
- const genOptions = { publicKeyPath: publicPath };
2315
- if (options.force !== void 0) {
2316
- genOptions.force = options.force;
2317
- }
2318
- const result = await provider.generateKeyPair(genOptions);
2319
- success("Keypair generated successfully!");
2320
- log("");
2321
- log("Private key stored in:");
2322
- log(` ${result.storageDescription}`);
2323
- log("");
2324
- log("Public key (commit to repo):");
2325
- log(` ${result.publicKeyPath}`);
2326
- } else {
2327
- const privatePath = options.private ?? core.getDefaultPrivateKeyPath();
2328
- log(`Private key: ${privatePath}`);
2329
- log(`Public key: ${publicPath}`);
2330
- const privateExists = fs__namespace.existsSync(privatePath);
2331
- const publicExists = fs__namespace.existsSync(publicPath);
2332
- if ((privateExists || publicExists) && !options.force) {
2333
- if (privateExists) {
2334
- warn(`Private key already exists: ${privatePath}`);
2335
- }
2336
- if (publicExists) {
2337
- warn(`Public key already exists: ${publicPath}`);
2338
- }
2339
- const shouldOverwrite = await confirmAction({
2340
- message: "Overwrite existing keys?",
2341
- default: false
2342
- });
2343
- if (!shouldOverwrite) {
2344
- error("Keygen cancelled");
2345
- process.exit(ExitCode.CANCELLED);
2346
- }
2347
- }
2348
- log("\nGenerating RSA-2048 keypair...");
2349
- const result = await core.generateKeyPair({
2350
- privatePath,
2351
- publicPath,
2352
- force: true
2353
- });
2354
- await core.setKeyPermissions(result.privatePath);
2355
- success("Keypair generated successfully!");
2356
- log("");
2357
- log("Private key (KEEP SECRET):");
2358
- log(` ${result.privatePath}`);
2359
- log("");
2360
- log("Public key (commit to repo):");
2361
- log(` ${result.publicPath}`);
2362
- }
2363
- log("");
2364
- info("Important: Back up your private key securely!");
2365
- log("");
2366
- log("Next steps:");
2367
- log(` 1. git add ${publicPath}`);
2368
- log(" 2. Update .attest-it/config.yaml publicKeyPath if needed");
2369
- log(" 3. attest-it run --suite <suite-name>");
2370
- }
2371
1733
  var pruneCommand = new commander.Command("prune").description("Remove stale attestations").option("-n, --dry-run", "Show what would be removed without removing").option("-k, --keep-days <n>", "Keep attestations newer than n days", "30").action(async (options) => {
2372
1734
  await runPrune(options);
2373
1735
  });
@@ -2619,7 +1981,7 @@ async function runSeal(gates, options) {
2619
1981
  const localConfig = core.loadLocalConfigSync();
2620
1982
  if (!localConfig) {
2621
1983
  error("No local identity configuration found");
2622
- error('Run "attest-it keygen" first to set up your identity');
1984
+ error('Run "attest-it identity create" first to set up your identity');
2623
1985
  process.exit(ExitCode.CONFIG_ERROR);
2624
1986
  }
2625
1987
  const identity = core.getActiveIdentity(localConfig);
@@ -2716,8 +2078,8 @@ async function processSingleGate(gateId, config, identity, identitySlug, sealsFi
2716
2078
  const keyProvider = createKeyProviderFromIdentity2(identity);
2717
2079
  const keyRef = getKeyRefFromIdentity2(identity);
2718
2080
  const keyResult = await keyProvider.getPrivateKey(keyRef);
2719
- const fs4 = await import('fs/promises');
2720
- const privateKeyPem = await fs4.readFile(keyResult.keyPath, "utf8");
2081
+ const fs3 = await import('fs/promises');
2082
+ const privateKeyPem = await fs3.readFile(keyResult.keyPath, "utf8");
2721
2083
  await keyResult.cleanup();
2722
2084
  const seal = core.createSeal({
2723
2085
  gateId,
@@ -3269,9 +2631,9 @@ async function runCreate() {
3269
2631
  log("");
3270
2632
  log(theme3.blue.bold()("Public key saved to:"));
3271
2633
  log(` ${publicKeyResult.homePath}`);
3272
- if (publicKeyResult.projectPath) {
3273
- log(` ${publicKeyResult.projectPath}`);
3274
- }
2634
+ log("");
2635
+ log("To add yourself to a project, run:");
2636
+ log(theme3.blue(" attest-it team join"));
3275
2637
  log("");
3276
2638
  if (!existingConfig) {
3277
2639
  success(`Set as active identity`);
@@ -3392,155 +2754,6 @@ async function runShow(slug) {
3392
2754
  process.exit(ExitCode.CONFIG_ERROR);
3393
2755
  }
3394
2756
  }
3395
- var editCommand = new commander.Command("edit").description("Edit identity or rotate keypair").argument("<slug>", "Identity slug to edit").action(async (slug) => {
3396
- await runEdit(slug);
3397
- });
3398
- async function runEdit(slug) {
3399
- try {
3400
- const config = await core.loadLocalConfig();
3401
- if (!config) {
3402
- error("No identities configured");
3403
- process.exit(ExitCode.CONFIG_ERROR);
3404
- }
3405
- const identity = config.identities[slug];
3406
- if (!identity) {
3407
- error(`Identity "${slug}" not found`);
3408
- process.exit(ExitCode.CONFIG_ERROR);
3409
- }
3410
- const theme3 = getTheme2();
3411
- log("");
3412
- log(theme3.blue.bold()(`Edit Identity: ${slug}`));
3413
- log("");
3414
- const name = await prompts.input({
3415
- message: "Display name:",
3416
- default: identity.name,
3417
- validate: (value) => {
3418
- if (!value || value.trim().length === 0) {
3419
- return "Name cannot be empty";
3420
- }
3421
- return true;
3422
- }
3423
- });
3424
- const email = await prompts.input({
3425
- message: "Email (optional):",
3426
- default: identity.email ?? ""
3427
- });
3428
- const github = await prompts.input({
3429
- message: "GitHub username (optional):",
3430
- default: identity.github ?? ""
3431
- });
3432
- const rotateKey = await prompts.confirm({
3433
- message: "Rotate keypair (generate new keys)?",
3434
- default: false
3435
- });
3436
- let publicKey = identity.publicKey;
3437
- const privateKeyRef = identity.privateKey;
3438
- if (rotateKey) {
3439
- log("");
3440
- log("Generating new Ed25519 keypair...");
3441
- const keyPair = core.generateEd25519KeyPair();
3442
- publicKey = keyPair.publicKey;
3443
- switch (identity.privateKey.type) {
3444
- case "file": {
3445
- await promises.writeFile(identity.privateKey.path, keyPair.privateKey, { mode: 384 });
3446
- log(` Updated private key at: ${identity.privateKey.path}`);
3447
- break;
3448
- }
3449
- case "keychain": {
3450
- const { execFile } = await import('child_process');
3451
- const { promisify } = await import('util');
3452
- const execFileAsync = promisify(execFile);
3453
- const encodedKey = Buffer.from(keyPair.privateKey).toString("base64");
3454
- try {
3455
- await execFileAsync("security", [
3456
- "delete-generic-password",
3457
- "-s",
3458
- identity.privateKey.service,
3459
- "-a",
3460
- identity.privateKey.account
3461
- ]);
3462
- await execFileAsync("security", [
3463
- "add-generic-password",
3464
- "-s",
3465
- identity.privateKey.service,
3466
- "-a",
3467
- identity.privateKey.account,
3468
- "-w",
3469
- encodedKey,
3470
- "-U"
3471
- ]);
3472
- log(` Updated private key in macOS Keychain`);
3473
- } catch (err) {
3474
- throw new Error(
3475
- `Failed to update key in macOS Keychain: ${err instanceof Error ? err.message : String(err)}`
3476
- );
3477
- }
3478
- break;
3479
- }
3480
- case "1password": {
3481
- const { execFile } = await import('child_process');
3482
- const { promisify } = await import('util');
3483
- const execFileAsync = promisify(execFile);
3484
- try {
3485
- const opArgs = [
3486
- "item",
3487
- "edit",
3488
- identity.privateKey.item,
3489
- "--vault",
3490
- identity.privateKey.vault,
3491
- `privateKey[password]=${keyPair.privateKey}`
3492
- ];
3493
- if (identity.privateKey.account) {
3494
- opArgs.push("--account", identity.privateKey.account);
3495
- }
3496
- await execFileAsync("op", opArgs);
3497
- log(` Updated private key in 1Password`);
3498
- } catch (err) {
3499
- throw new Error(
3500
- `Failed to update key in 1Password: ${err instanceof Error ? err.message : String(err)}`
3501
- );
3502
- }
3503
- break;
3504
- }
3505
- }
3506
- }
3507
- const updatedIdentity = {
3508
- name,
3509
- publicKey,
3510
- privateKey: privateKeyRef,
3511
- ...email && { email },
3512
- ...github && { github }
3513
- };
3514
- const newConfig = {
3515
- ...config,
3516
- identities: {
3517
- ...config.identities,
3518
- [slug]: updatedIdentity
3519
- }
3520
- };
3521
- await core.saveLocalConfig(newConfig);
3522
- log("");
3523
- success("Identity updated successfully");
3524
- log("");
3525
- if (rotateKey) {
3526
- log(" New Public Key: " + publicKey.slice(0, 32) + "...");
3527
- log("");
3528
- log(
3529
- theme3.yellow(
3530
- " Warning: If this identity is used in team configurations,\n you must update those configurations with the new public key."
3531
- )
3532
- );
3533
- log("");
3534
- }
3535
- } catch (err) {
3536
- if (err instanceof Error) {
3537
- error(err.message);
3538
- } else {
3539
- error("Unknown error occurred");
3540
- }
3541
- process.exit(ExitCode.CONFIG_ERROR);
3542
- }
3543
- }
3544
2757
 
3545
2758
  // src/utils/format-key-location.ts
3546
2759
  function formatKeyLocation(privateKey) {
@@ -3759,7 +2972,7 @@ async function runExport(slug) {
3759
2972
  }
3760
2973
 
3761
2974
  // src/commands/identity/index.ts
3762
- var identityCommand = new commander.Command("identity").description("Manage local identities and keypairs").addCommand(listCommand).addCommand(createCommand).addCommand(useCommand).addCommand(showCommand).addCommand(editCommand).addCommand(removeCommand).addCommand(exportCommand);
2975
+ var identityCommand = new commander.Command("identity").description("Manage local identities and keypairs").addCommand(listCommand).addCommand(createCommand).addCommand(useCommand).addCommand(showCommand).addCommand(removeCommand).addCommand(exportCommand);
3763
2976
  var whoamiCommand = new commander.Command("whoami").description("Show the current active identity").action(async () => {
3764
2977
  await runWhoami();
3765
2978
  });
@@ -3860,6 +3073,49 @@ async function runList2() {
3860
3073
  process.exit(ExitCode.CONFIG_ERROR);
3861
3074
  }
3862
3075
  }
3076
+ async function promptForGateAuthorization(gates) {
3077
+ if (!gates || Object.keys(gates).length === 0) {
3078
+ return [];
3079
+ }
3080
+ const gateChoices = Object.entries(gates).map(([gateId, gate]) => ({
3081
+ name: `${gateId} - ${gate.name}`,
3082
+ value: gateId
3083
+ }));
3084
+ const authorizedGates = await prompts.checkbox({
3085
+ message: "Select gates to authorize (use space to select):",
3086
+ choices: gateChoices
3087
+ });
3088
+ return authorizedGates;
3089
+ }
3090
+ function addTeamMemberToConfig(config, memberSlug, memberData, authorizedGates) {
3091
+ const existingTeam = config.team ?? {};
3092
+ const updatedConfig = {
3093
+ ...config,
3094
+ team: {
3095
+ ...existingTeam,
3096
+ [memberSlug]: {
3097
+ name: memberData.name,
3098
+ publicKey: memberData.publicKey,
3099
+ publicKeyAlgorithm: memberData.publicKeyAlgorithm ?? "ed25519",
3100
+ ...memberData.email ? { email: memberData.email } : {},
3101
+ ...memberData.github ? { github: memberData.github } : {}
3102
+ }
3103
+ }
3104
+ };
3105
+ if (authorizedGates.length > 0 && updatedConfig.gates) {
3106
+ for (const gateId of authorizedGates) {
3107
+ const gate = updatedConfig.gates[gateId];
3108
+ if (gate) {
3109
+ if (!gate.authorizedSigners.includes(memberSlug)) {
3110
+ gate.authorizedSigners.push(memberSlug);
3111
+ }
3112
+ }
3113
+ }
3114
+ }
3115
+ return updatedConfig;
3116
+ }
3117
+
3118
+ // src/commands/team/add.ts
3863
3119
  var addCommand = new commander.Command("add").description("Add a new team member").action(async () => {
3864
3120
  await runAdd();
3865
3121
  });
@@ -3931,45 +3187,22 @@ async function runAdd() {
3931
3187
  message: "Public key:",
3932
3188
  validate: validatePublicKey
3933
3189
  });
3934
- let authorizedGates = [];
3935
- if (attestItConfig.gates && Object.keys(attestItConfig.gates).length > 0) {
3936
- log("");
3937
- const gateChoices = Object.entries(attestItConfig.gates).map(([gateId, gate]) => ({
3938
- name: `${gateId} - ${gate.name}`,
3939
- value: gateId
3940
- }));
3941
- authorizedGates = await prompts.checkbox({
3942
- message: "Select gates to authorize (use space to select):",
3943
- choices: gateChoices
3944
- });
3945
- }
3946
- const teamMember = {
3190
+ log("");
3191
+ const authorizedGates = await promptForGateAuthorization(attestItConfig.gates);
3192
+ const memberData = {
3947
3193
  name,
3948
- publicKey: publicKey.trim()
3194
+ publicKey: publicKey.trim(),
3195
+ publicKeyAlgorithm: "ed25519"
3949
3196
  };
3950
- if (email && email.trim().length > 0) {
3951
- teamMember.email = email.trim();
3952
- }
3953
- if (github && github.trim().length > 0) {
3954
- teamMember.github = github.trim();
3197
+ const trimmedEmail = email.trim();
3198
+ const trimmedGithub = github.trim();
3199
+ if (trimmedEmail && trimmedEmail.length > 0) {
3200
+ memberData.email = trimmedEmail;
3955
3201
  }
3956
- const updatedConfig = {
3957
- ...config,
3958
- team: {
3959
- ...existingTeam,
3960
- [slug]: teamMember
3961
- }
3962
- };
3963
- if (authorizedGates.length > 0 && updatedConfig.gates) {
3964
- for (const gateId of authorizedGates) {
3965
- const gate = updatedConfig.gates[gateId];
3966
- if (gate) {
3967
- if (!gate.authorizedSigners.includes(slug)) {
3968
- gate.authorizedSigners.push(slug);
3969
- }
3970
- }
3971
- }
3202
+ if (trimmedGithub && trimmedGithub.length > 0) {
3203
+ memberData.github = trimmedGithub;
3972
3204
  }
3205
+ const updatedConfig = addTeamMemberToConfig(config, slug, memberData, authorizedGates);
3973
3206
  const configPath = core.findConfigPath();
3974
3207
  if (!configPath) {
3975
3208
  error("Configuration file not found");
@@ -3992,127 +3225,73 @@ async function runAdd() {
3992
3225
  process.exit(ExitCode.CONFIG_ERROR);
3993
3226
  }
3994
3227
  }
3995
- var editCommand2 = new commander.Command("edit").description("Edit a team member").argument("<slug>", "Team member slug to edit").action(async (slug) => {
3996
- await runEdit2(slug);
3228
+ var joinCommand = new commander.Command("join").description("Add yourself to the project team using your active identity").action(async () => {
3229
+ await runJoin();
3997
3230
  });
3998
- function validatePublicKey2(value) {
3999
- if (!value || value.trim().length === 0) {
4000
- return "Public key cannot be empty";
4001
- }
4002
- const base64Regex = /^[A-Za-z0-9+/]+=*$/;
4003
- if (!base64Regex.test(value)) {
4004
- return "Public key must be valid Base64";
4005
- }
4006
- if (value.length !== 44) {
4007
- return "Public key must be 44 characters (32 bytes in Base64)";
4008
- }
4009
- try {
4010
- const decoded = Buffer.from(value, "base64");
4011
- if (decoded.length !== 32) {
4012
- return "Public key must decode to 32 bytes";
4013
- }
4014
- } catch {
4015
- return "Invalid Base64 encoding";
4016
- }
4017
- return true;
4018
- }
4019
- async function runEdit2(slug) {
3231
+ async function runJoin() {
4020
3232
  try {
4021
3233
  const theme3 = getTheme2();
4022
- const config = await core.loadConfig();
4023
- const attestItConfig = core.toAttestItConfig(config);
4024
- const existingMember = attestItConfig.team?.[slug];
4025
- if (!existingMember) {
4026
- error(`Team member "${slug}" not found`);
4027
- process.exit(ExitCode.CONFIG_ERROR);
4028
- }
4029
3234
  log("");
4030
- log(theme3.blue.bold()(`Edit Team Member: ${slug}`));
3235
+ log(theme3.blue.bold()("Join Project Team"));
4031
3236
  log("");
4032
- log(theme3.muted("Leave blank to keep current value"));
3237
+ const localConfig = await core.loadLocalConfig();
3238
+ if (!localConfig) {
3239
+ error('No identity found. Run "attest-it identity create" first.');
3240
+ process.exit(ExitCode.CONFIG_ERROR);
3241
+ }
3242
+ const activeIdentity = core.getActiveIdentity(localConfig);
3243
+ if (!activeIdentity) {
3244
+ error('No active identity. Run "attest-it identity use <slug>" to select one.');
3245
+ process.exit(ExitCode.CONFIG_ERROR);
3246
+ }
3247
+ const activeSlug = localConfig.activeIdentity;
3248
+ info(`Using identity: ${activeSlug}`);
3249
+ log(` Name: ${activeIdentity.name}`);
3250
+ log(` Public Key: ${activeIdentity.publicKey.slice(0, 32)}...`);
4033
3251
  log("");
4034
- const name = await prompts.input({
4035
- message: "Display name:",
4036
- default: existingMember.name,
4037
- validate: (value) => {
4038
- if (!value || value.trim().length === 0) {
4039
- return "Name cannot be empty";
4040
- }
4041
- return true;
4042
- }
4043
- });
4044
- const email = await prompts.input({
4045
- message: "Email (optional):",
4046
- default: existingMember.email ?? ""
4047
- });
4048
- const github = await prompts.input({
4049
- message: "GitHub username (optional):",
4050
- default: existingMember.github ?? ""
4051
- });
4052
- const updateKey = await prompts.confirm({
4053
- message: "Update public key?",
4054
- default: false
4055
- });
4056
- let publicKey = existingMember.publicKey;
4057
- if (updateKey) {
4058
- log("");
4059
- log("Paste the new public key:");
4060
- publicKey = await prompts.input({
4061
- message: "Public key:",
4062
- default: existingMember.publicKey,
4063
- validate: validatePublicKey2
4064
- });
3252
+ const config = await core.loadConfig();
3253
+ const attestItConfig = core.toAttestItConfig(config);
3254
+ const existingTeam = attestItConfig.team ?? {};
3255
+ const existingMemberWithKey = Object.entries(existingTeam).find(
3256
+ ([, member]) => member.publicKey === activeIdentity.publicKey
3257
+ );
3258
+ if (existingMemberWithKey) {
3259
+ error(`You're already a team member as "${existingMemberWithKey[0]}"`);
3260
+ process.exit(ExitCode.CONFIG_ERROR);
4065
3261
  }
4066
- const currentGates = [];
4067
- if (attestItConfig.gates) {
4068
- for (const [gateId, gate] of Object.entries(attestItConfig.gates)) {
4069
- if (gate.authorizedSigners.includes(slug)) {
4070
- currentGates.push(gateId);
3262
+ let slug = activeSlug;
3263
+ if (existingTeam[slug]) {
3264
+ log(`Slug "${slug}" is already taken by another team member.`);
3265
+ slug = await prompts.input({
3266
+ message: "Choose a different slug:",
3267
+ validate: (value) => {
3268
+ if (!value || value.trim().length === 0) {
3269
+ return "Slug cannot be empty";
3270
+ }
3271
+ if (!/^[a-z0-9-]+$/.test(value)) {
3272
+ return "Slug must contain only lowercase letters, numbers, and hyphens";
3273
+ }
3274
+ if (existingTeam[value]) {
3275
+ return `Slug "${value}" is already taken`;
3276
+ }
3277
+ return true;
4071
3278
  }
4072
- }
4073
- }
4074
- let selectedGates = currentGates;
4075
- if (attestItConfig.gates && Object.keys(attestItConfig.gates).length > 0) {
4076
- log("");
4077
- const gateChoices = Object.entries(attestItConfig.gates).map(([gateId, gate]) => ({
4078
- name: `${gateId} - ${gate.name}`,
4079
- value: gateId,
4080
- checked: currentGates.includes(gateId)
4081
- }));
4082
- selectedGates = await prompts.checkbox({
4083
- message: "Select gates to authorize (use space to select):",
4084
- choices: gateChoices
4085
3279
  });
4086
3280
  }
4087
- const updatedMember = {
4088
- name: name.trim(),
4089
- publicKey: publicKey.trim()
3281
+ log("");
3282
+ const authorizedGates = await promptForGateAuthorization(attestItConfig.gates);
3283
+ const memberData = {
3284
+ name: activeIdentity.name,
3285
+ publicKey: activeIdentity.publicKey,
3286
+ publicKeyAlgorithm: "ed25519"
4090
3287
  };
4091
- if (email && email.trim().length > 0) {
4092
- updatedMember.email = email.trim();
3288
+ if (activeIdentity.email) {
3289
+ memberData.email = activeIdentity.email;
4093
3290
  }
4094
- if (github && github.trim().length > 0) {
4095
- updatedMember.github = github.trim();
4096
- }
4097
- const updatedConfig = {
4098
- ...config,
4099
- team: {
4100
- ...attestItConfig.team,
4101
- [slug]: updatedMember
4102
- }
4103
- };
4104
- if (updatedConfig.gates) {
4105
- for (const [gateId, gate] of Object.entries(updatedConfig.gates)) {
4106
- if (currentGates.includes(gateId) && !selectedGates.includes(gateId)) {
4107
- gate.authorizedSigners = gate.authorizedSigners.filter((s) => s !== slug);
4108
- }
4109
- if (!currentGates.includes(gateId) && selectedGates.includes(gateId)) {
4110
- if (!gate.authorizedSigners.includes(slug)) {
4111
- gate.authorizedSigners.push(slug);
4112
- }
4113
- }
4114
- }
3291
+ if (activeIdentity.github) {
3292
+ memberData.github = activeIdentity.github;
4115
3293
  }
3294
+ const updatedConfig = addTeamMemberToConfig(config, slug, memberData, authorizedGates);
4116
3295
  const configPath = core.findConfigPath();
4117
3296
  if (!configPath) {
4118
3297
  error("Configuration file not found");
@@ -4121,11 +3300,9 @@ async function runEdit2(slug) {
4121
3300
  const yamlContent = yaml.stringify(updatedConfig);
4122
3301
  await promises.writeFile(configPath, yamlContent, "utf8");
4123
3302
  log("");
4124
- success(`Team member "${slug}" updated successfully`);
4125
- if (selectedGates.length > 0) {
4126
- log(`Authorized for gates: ${selectedGates.join(", ")}`);
4127
- } else {
4128
- log("Not authorized for any gates");
3303
+ success(`Team member "${slug}" added successfully`);
3304
+ if (authorizedGates.length > 0) {
3305
+ log(`Authorized for gates: ${authorizedGates.join(", ")}`);
4129
3306
  }
4130
3307
  log("");
4131
3308
  } catch (err) {
@@ -4240,7 +3417,7 @@ async function runRemove2(slug, options) {
4240
3417
  }
4241
3418
 
4242
3419
  // src/commands/team/index.ts
4243
- var teamCommand = new commander.Command("team").description("Manage team members and authorizations").addCommand(listCommand2).addCommand(addCommand).addCommand(editCommand2).addCommand(removeCommand2);
3420
+ var teamCommand = new commander.Command("team").description("Manage team members and authorizations").addCommand(listCommand2).addCommand(addCommand).addCommand(joinCommand).addCommand(removeCommand2);
4244
3421
  var PROGRAM_NAME2 = "attest-it";
4245
3422
  var PROGRAM_ALIAS2 = "attest";
4246
3423
  var PROGRAM_NAMES = [PROGRAM_NAME2, PROGRAM_ALIAS2];
@@ -4261,7 +3438,6 @@ async function getCompletions(env) {
4261
3438
  { name: "run", description: "Run test suites interactively" },
4262
3439
  { name: "verify", description: "Verify all seals are valid" },
4263
3440
  { name: "seal", description: "Create a seal for a gate" },
4264
- { name: "keygen", description: "Generate a new keypair" },
4265
3441
  { name: "prune", description: "Remove stale attestations" },
4266
3442
  { name: "identity", description: "Manage identities" },
4267
3443
  { name: "team", description: "Manage team members" },
@@ -4366,7 +3542,6 @@ async function getCompletions(env) {
4366
3542
  "run",
4367
3543
  "verify",
4368
3544
  "seal",
4369
- "keygen",
4370
3545
  "prune",
4371
3546
  "identity",
4372
3547
  "team",
@@ -4501,43 +3676,12 @@ function createCompletionServerCommand() {
4501
3676
  }
4502
3677
  });
4503
3678
  }
4504
- function hasVersion(data) {
4505
- return typeof data === "object" && data !== null && "version" in data && // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
4506
- typeof data.version === "string";
4507
- }
4508
- var cachedVersion;
4509
- function getPackageVersion() {
4510
- if (cachedVersion !== void 0) {
4511
- return cachedVersion;
4512
- }
4513
- const __filename = url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
4514
- const __dirname = path.dirname(__filename);
4515
- const possiblePaths = [path.join(__dirname, "../package.json"), path.join(__dirname, "../../package.json")];
4516
- for (const packageJsonPath of possiblePaths) {
4517
- try {
4518
- const content = fs.readFileSync(packageJsonPath, "utf-8");
4519
- const packageJsonData = JSON.parse(content);
4520
- if (!hasVersion(packageJsonData)) {
4521
- throw new Error(`Invalid package.json at ${packageJsonPath}: missing version field`);
4522
- }
4523
- cachedVersion = packageJsonData.version;
4524
- return cachedVersion;
4525
- } catch (error2) {
4526
- if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") {
4527
- continue;
4528
- }
4529
- throw error2;
4530
- }
4531
- }
4532
- throw new Error("Could not find package.json");
4533
- }
4534
3679
  var program = new commander.Command();
4535
3680
  program.name("attest-it").description("Human-gated test attestation system").option("-c, --config <path>", "Path to config file").option("-v, --verbose", "Verbose output").option("-q, --quiet", "Minimal output");
4536
3681
  program.option("-V, --version", "output the version number");
4537
3682
  program.addCommand(initCommand);
4538
3683
  program.addCommand(statusCommand);
4539
3684
  program.addCommand(runCommand);
4540
- program.addCommand(keygenCommand);
4541
3685
  program.addCommand(pruneCommand);
4542
3686
  program.addCommand(verifyCommand);
4543
3687
  program.addCommand(sealCommand);