@attest-it/cli 0.7.0 → 0.8.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.
@@ -16,7 +16,7 @@ import { useState, useEffect } from 'react';
16
16
  import { render, useApp, Box, Text, useInput } from 'ink';
17
17
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
18
18
  import { mkdir, writeFile, unlink, readFile } from 'fs/promises';
19
- import { Spinner, Select, TextInput } from '@inkjs/ui';
19
+ import { Spinner, Select, PasswordInput, TextInput } from '@inkjs/ui';
20
20
  import { stringify } from 'yaml';
21
21
  import { fileURLToPath } from 'url';
22
22
 
@@ -337,7 +337,7 @@ async function runStatus(gates, options) {
337
337
  process.exit(ExitCode.CONFIG_ERROR);
338
338
  }
339
339
  const projectRoot = process.cwd();
340
- const sealsFile = readSealsSync(projectRoot);
340
+ const sealsFile = readSealsSync(projectRoot, attestItConfig.settings.sealsPath);
341
341
  const gatesToCheck = gates.length > 0 ? gates : Object.keys(attestItConfig.gates);
342
342
  for (const gateId of gatesToCheck) {
343
343
  if (!attestItConfig.gates[gateId]) {
@@ -989,12 +989,24 @@ async function getAllSuiteStatuses(config) {
989
989
  const attestations = attestationsFile?.attestations ?? [];
990
990
  const results = [];
991
991
  for (const [suiteName, suiteConfig] of Object.entries(config.suites)) {
992
- if (!suiteConfig.packages) {
992
+ let packages;
993
+ let ignore;
994
+ if (suiteConfig.gate && config.gates) {
995
+ const gateConfig = config.gates[suiteConfig.gate];
996
+ if (gateConfig) {
997
+ packages = gateConfig.fingerprint.paths;
998
+ ignore = gateConfig.fingerprint.exclude;
999
+ }
1000
+ } else if (suiteConfig.packages) {
1001
+ packages = suiteConfig.packages;
1002
+ ignore = suiteConfig.ignore;
1003
+ }
1004
+ if (!packages || packages.length === 0) {
993
1005
  continue;
994
1006
  }
995
1007
  const fingerprintResult = await computeFingerprint({
996
- packages: suiteConfig.packages,
997
- ...suiteConfig.ignore && { ignore: suiteConfig.ignore }
1008
+ packages,
1009
+ ...ignore && { ignore }
998
1010
  });
999
1011
  const attestation = findAttestation(
1000
1012
  { schemaVersion: "1", attestations, signature: "" },
@@ -1559,18 +1571,19 @@ async function promptForSeal(suiteName, gateId, config) {
1559
1571
  const fs4 = await import('fs/promises');
1560
1572
  const privateKeyPem = await fs4.readFile(keyResult.keyPath, "utf8");
1561
1573
  await keyResult.cleanup();
1574
+ const identitySlug = localConfig.activeIdentity;
1562
1575
  const seal = createSeal({
1563
1576
  gateId,
1564
1577
  fingerprint: gateFingerprint.fingerprint,
1565
- sealedBy: identity.name,
1578
+ sealedBy: identitySlug,
1566
1579
  privateKey: privateKeyPem
1567
1580
  });
1568
1581
  const projectRoot = process.cwd();
1569
- const sealsFile = readSealsSync(projectRoot);
1582
+ const sealsFile = readSealsSync(projectRoot, attestItConfig.settings.sealsPath);
1570
1583
  sealsFile.seals[gateId] = seal;
1571
- writeSealsSync(projectRoot, sealsFile);
1584
+ writeSealsSync(projectRoot, sealsFile, attestItConfig.settings.sealsPath);
1572
1585
  success(`Seal created for gate '${gateId}'`);
1573
- log(` Sealed by: ${identity.name}`);
1586
+ log(` Sealed by: ${identitySlug} (${identity.name})`);
1574
1587
  log(` Timestamp: ${seal.timestamp}`);
1575
1588
  } catch (err) {
1576
1589
  if (err instanceof Error) {
@@ -1638,6 +1651,7 @@ function getKeyRefFromIdentity(identity) {
1638
1651
  }
1639
1652
  }
1640
1653
  }
1654
+ var MIN_PASSPHRASE_LENGTH = 8;
1641
1655
  function KeygenInteractive(props) {
1642
1656
  const { onComplete, onCancel, onError } = props;
1643
1657
  const [step, setStep] = useState("checking-providers");
@@ -1656,6 +1670,7 @@ function KeygenInteractive(props) {
1656
1670
  const [selectedYubiKeySlot, setSelectedYubiKeySlot] = useState(2);
1657
1671
  const [slot1Configured, setSlot1Configured] = useState(false);
1658
1672
  const [slot2Configured, setSlot2Configured] = useState(false);
1673
+ const [encryptionPassphrase, setEncryptionPassphrase] = useState();
1659
1674
  useInput((_input, key) => {
1660
1675
  if (key.escape) {
1661
1676
  onCancel();
@@ -1729,7 +1744,7 @@ function KeygenInteractive(props) {
1729
1744
  const handleProviderSelect = (value) => {
1730
1745
  if (value === "filesystem") {
1731
1746
  setSelectedProvider("filesystem");
1732
- void generateKeys("filesystem");
1747
+ setStep("select-filesystem-encryption");
1733
1748
  } else if (value === "1password") {
1734
1749
  setSelectedProvider("1password");
1735
1750
  if (accounts.length === 1 && accounts[0]) {
@@ -1783,6 +1798,31 @@ function KeygenInteractive(props) {
1783
1798
  onError(new Error("YubiKey setup cancelled"));
1784
1799
  }
1785
1800
  };
1801
+ const handleEncryptionMethodSelect = (value) => {
1802
+ if (value === "passphrase") {
1803
+ setStep("enter-encryption-passphrase");
1804
+ } else {
1805
+ setEncryptionPassphrase(void 0);
1806
+ void generateKeys("filesystem");
1807
+ }
1808
+ };
1809
+ const handleEncryptionPassphrase = (value) => {
1810
+ if (value.length < MIN_PASSPHRASE_LENGTH) {
1811
+ onError(new Error(`Passphrase must be at least ${String(MIN_PASSPHRASE_LENGTH)} characters`));
1812
+ return;
1813
+ }
1814
+ setEncryptionPassphrase(value);
1815
+ setStep("confirm-encryption-passphrase");
1816
+ };
1817
+ const handleConfirmPassphrase = (value) => {
1818
+ if (value !== encryptionPassphrase) {
1819
+ onError(new Error("Passphrases do not match. Please try again."));
1820
+ setEncryptionPassphrase(void 0);
1821
+ setStep("enter-encryption-passphrase");
1822
+ return;
1823
+ }
1824
+ void generateKeys("filesystem");
1825
+ };
1786
1826
  const setupYubiKeySlot = async () => {
1787
1827
  setStep("yubikey-configuring");
1788
1828
  try {
@@ -1814,17 +1854,26 @@ function KeygenInteractive(props) {
1814
1854
  const publicKeyPath = props.publicKeyPath ?? getDefaultPublicKeyPath();
1815
1855
  if (provider === "filesystem") {
1816
1856
  const fsProvider = new FilesystemKeyProvider();
1817
- const genOptions = { publicKeyPath };
1857
+ const genOptions = {
1858
+ publicKeyPath
1859
+ };
1818
1860
  if (props.force !== void 0) {
1819
1861
  genOptions.force = props.force;
1820
1862
  }
1863
+ if (encryptionPassphrase !== void 0) {
1864
+ genOptions.passphrase = encryptionPassphrase;
1865
+ }
1821
1866
  const result = await fsProvider.generateKeyPair(genOptions);
1822
- onComplete({
1867
+ const completionResult = {
1823
1868
  provider: "filesystem",
1824
1869
  publicKeyPath: result.publicKeyPath,
1825
1870
  privateKeyRef: result.privateKeyRef,
1826
1871
  storageDescription: result.storageDescription
1827
- });
1872
+ };
1873
+ if (result.encrypted) {
1874
+ completionResult.encrypted = result.encrypted;
1875
+ }
1876
+ onComplete(completionResult);
1828
1877
  } else if (provider === "1password") {
1829
1878
  if (!selectedVault || !itemName) {
1830
1879
  throw new Error("Vault and item name are required for 1Password");
@@ -1908,6 +1957,8 @@ function KeygenInteractive(props) {
1908
1957
  setStep("done");
1909
1958
  } catch (err) {
1910
1959
  onError(err instanceof Error ? err : new Error("Key generation failed"));
1960
+ } finally {
1961
+ setEncryptionPassphrase(void 0);
1911
1962
  }
1912
1963
  };
1913
1964
  if (step === "checking-providers") {
@@ -1947,6 +1998,39 @@ function KeygenInteractive(props) {
1947
1998
  /* @__PURE__ */ jsx(Select, { options, onChange: handleProviderSelect })
1948
1999
  ] });
1949
2000
  }
2001
+ if (step === "select-filesystem-encryption") {
2002
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
2003
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Would you like to encrypt your private key with a passphrase?" }),
2004
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "A passphrase adds extra security but must be entered each time you sign." }),
2005
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "" }),
2006
+ /* @__PURE__ */ jsx(
2007
+ Select,
2008
+ {
2009
+ options: [
2010
+ { label: "No encryption (key protected by file permissions only)", value: "none" },
2011
+ { label: "Passphrase protection (AES-256 encryption)", value: "passphrase" }
2012
+ ],
2013
+ onChange: handleEncryptionMethodSelect
2014
+ }
2015
+ )
2016
+ ] });
2017
+ }
2018
+ if (step === "enter-encryption-passphrase") {
2019
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
2020
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Enter a passphrase to encrypt your private key:" }),
2021
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: `(Minimum ${String(MIN_PASSPHRASE_LENGTH)} characters. You will need this passphrase each time you sign.)` }),
2022
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "" }),
2023
+ /* @__PURE__ */ jsx(PasswordInput, { onSubmit: handleEncryptionPassphrase })
2024
+ ] });
2025
+ }
2026
+ if (step === "confirm-encryption-passphrase") {
2027
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
2028
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Confirm your passphrase:" }),
2029
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "(Enter the same passphrase again to confirm.)" }),
2030
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "" }),
2031
+ /* @__PURE__ */ jsx(PasswordInput, { onSubmit: handleConfirmPassphrase })
2032
+ ] });
2033
+ }
1950
2034
  if (step === "select-account") {
1951
2035
  const options = accounts.map((account) => ({
1952
2036
  label: account.name ? `${account.name} (${account.email})` : account.email,
@@ -2362,7 +2446,7 @@ async function runVerify(gates, options) {
2362
2446
  process.exit(ExitCode.CONFIG_ERROR);
2363
2447
  }
2364
2448
  const projectRoot = process.cwd();
2365
- const sealsFile = readSealsSync(projectRoot);
2449
+ const sealsFile = readSealsSync(projectRoot, attestItConfig.settings.sealsPath);
2366
2450
  const gatesToVerify = gates.length > 0 ? gates : Object.keys(attestItConfig.gates);
2367
2451
  for (const gateId of gatesToVerify) {
2368
2452
  if (!attestItConfig.gates[gateId]) {
@@ -2518,7 +2602,7 @@ async function runSeal(gates, options) {
2518
2602
  process.exit(ExitCode.CONFIG_ERROR);
2519
2603
  }
2520
2604
  const projectRoot = process.cwd();
2521
- const sealsFile = readSealsSync(projectRoot);
2605
+ const sealsFile = readSealsSync(projectRoot, attestItConfig.settings.sealsPath);
2522
2606
  const gatesToSeal = gates.length > 0 ? gates : getAllGateIds(attestItConfig);
2523
2607
  for (const gateId of gatesToSeal) {
2524
2608
  if (!attestItConfig.gates[gateId]) {
@@ -2531,9 +2615,17 @@ async function runSeal(gates, options) {
2531
2615
  skipped: [],
2532
2616
  failed: []
2533
2617
  };
2618
+ const identitySlug = localConfig.activeIdentity;
2534
2619
  for (const gateId of gatesToSeal) {
2535
2620
  try {
2536
- const result = await processSingleGate(gateId, attestItConfig, identity, sealsFile, options);
2621
+ const result = await processSingleGate(
2622
+ gateId,
2623
+ attestItConfig,
2624
+ identity,
2625
+ identitySlug,
2626
+ sealsFile,
2627
+ options
2628
+ );
2537
2629
  if (result.sealed) {
2538
2630
  summary.sealed.push(gateId);
2539
2631
  } else if (result.skipped) {
@@ -2545,7 +2637,7 @@ async function runSeal(gates, options) {
2545
2637
  }
2546
2638
  }
2547
2639
  if (!options.dryRun && summary.sealed.length > 0) {
2548
- writeSealsSync(projectRoot, sealsFile);
2640
+ writeSealsSync(projectRoot, sealsFile, attestItConfig.settings.sealsPath);
2549
2641
  }
2550
2642
  displaySummary(summary, options.dryRun);
2551
2643
  if (summary.failed.length > 0) {
@@ -2564,7 +2656,7 @@ async function runSeal(gates, options) {
2564
2656
  process.exit(ExitCode.CONFIG_ERROR);
2565
2657
  }
2566
2658
  }
2567
- async function processSingleGate(gateId, config, identity, sealsFile, options) {
2659
+ async function processSingleGate(gateId, config, identity, identitySlug, sealsFile, options) {
2568
2660
  verbose(`Processing gate: ${gateId}`);
2569
2661
  const gate = getGate(config, gateId);
2570
2662
  if (!gate) {
@@ -2604,12 +2696,12 @@ async function processSingleGate(gateId, config, identity, sealsFile, options) {
2604
2696
  const seal = createSeal({
2605
2697
  gateId,
2606
2698
  fingerprint: fingerprintResult.fingerprint,
2607
- sealedBy: identity.name,
2699
+ sealedBy: identitySlug,
2608
2700
  privateKey: privateKeyPem
2609
2701
  });
2610
2702
  sealsFile.seals[gateId] = seal;
2611
2703
  log(` Sealed gate: ${gateId}`);
2612
- verbose(` Sealed by: ${identity.name}`);
2704
+ verbose(` Sealed by: ${identitySlug} (${identity.name})`);
2613
2705
  verbose(` Timestamp: ${seal.timestamp}`);
2614
2706
  return { sealed: true, skipped: false };
2615
2707
  }
@@ -4046,7 +4138,7 @@ async function runRemove2(slug, options) {
4046
4138
  const projectRoot = process.cwd();
4047
4139
  let sealsFile;
4048
4140
  try {
4049
- sealsFile = readSealsSync(projectRoot);
4141
+ sealsFile = readSealsSync(projectRoot, attestItConfig.settings.sealsPath);
4050
4142
  } catch {
4051
4143
  sealsFile = { version: 1, seals: {} };
4052
4144
  }