@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.
package/dist/index.cjs CHANGED
@@ -363,7 +363,7 @@ async function runStatus(gates, options) {
363
363
  process.exit(ExitCode.CONFIG_ERROR);
364
364
  }
365
365
  const projectRoot = process.cwd();
366
- const sealsFile = core.readSealsSync(projectRoot);
366
+ const sealsFile = core.readSealsSync(projectRoot, attestItConfig.settings.sealsPath);
367
367
  const gatesToCheck = gates.length > 0 ? gates : Object.keys(attestItConfig.gates);
368
368
  for (const gateId of gatesToCheck) {
369
369
  if (!attestItConfig.gates[gateId]) {
@@ -1015,12 +1015,24 @@ async function getAllSuiteStatuses(config) {
1015
1015
  const attestations = attestationsFile?.attestations ?? [];
1016
1016
  const results = [];
1017
1017
  for (const [suiteName, suiteConfig] of Object.entries(config.suites)) {
1018
- if (!suiteConfig.packages) {
1018
+ let packages;
1019
+ let ignore;
1020
+ if (suiteConfig.gate && config.gates) {
1021
+ const gateConfig = config.gates[suiteConfig.gate];
1022
+ if (gateConfig) {
1023
+ packages = gateConfig.fingerprint.paths;
1024
+ ignore = gateConfig.fingerprint.exclude;
1025
+ }
1026
+ } else if (suiteConfig.packages) {
1027
+ packages = suiteConfig.packages;
1028
+ ignore = suiteConfig.ignore;
1029
+ }
1030
+ if (!packages || packages.length === 0) {
1019
1031
  continue;
1020
1032
  }
1021
1033
  const fingerprintResult = await core.computeFingerprint({
1022
- packages: suiteConfig.packages,
1023
- ...suiteConfig.ignore && { ignore: suiteConfig.ignore }
1034
+ packages,
1035
+ ...ignore && { ignore }
1024
1036
  });
1025
1037
  const attestation = core.findAttestation(
1026
1038
  { schemaVersion: "1", attestations, signature: "" },
@@ -1585,18 +1597,19 @@ async function promptForSeal(suiteName, gateId, config) {
1585
1597
  const fs4 = await import('fs/promises');
1586
1598
  const privateKeyPem = await fs4.readFile(keyResult.keyPath, "utf8");
1587
1599
  await keyResult.cleanup();
1600
+ const identitySlug = localConfig.activeIdentity;
1588
1601
  const seal = core.createSeal({
1589
1602
  gateId,
1590
1603
  fingerprint: gateFingerprint.fingerprint,
1591
- sealedBy: identity.name,
1604
+ sealedBy: identitySlug,
1592
1605
  privateKey: privateKeyPem
1593
1606
  });
1594
1607
  const projectRoot = process.cwd();
1595
- const sealsFile = core.readSealsSync(projectRoot);
1608
+ const sealsFile = core.readSealsSync(projectRoot, attestItConfig.settings.sealsPath);
1596
1609
  sealsFile.seals[gateId] = seal;
1597
- core.writeSealsSync(projectRoot, sealsFile);
1610
+ core.writeSealsSync(projectRoot, sealsFile, attestItConfig.settings.sealsPath);
1598
1611
  success(`Seal created for gate '${gateId}'`);
1599
- log(` Sealed by: ${identity.name}`);
1612
+ log(` Sealed by: ${identitySlug} (${identity.name})`);
1600
1613
  log(` Timestamp: ${seal.timestamp}`);
1601
1614
  } catch (err) {
1602
1615
  if (err instanceof Error) {
@@ -1664,6 +1677,7 @@ function getKeyRefFromIdentity(identity) {
1664
1677
  }
1665
1678
  }
1666
1679
  }
1680
+ var MIN_PASSPHRASE_LENGTH = 8;
1667
1681
  function KeygenInteractive(props) {
1668
1682
  const { onComplete, onCancel, onError } = props;
1669
1683
  const [step, setStep] = React7.useState("checking-providers");
@@ -1682,6 +1696,7 @@ function KeygenInteractive(props) {
1682
1696
  const [selectedYubiKeySlot, setSelectedYubiKeySlot] = React7.useState(2);
1683
1697
  const [slot1Configured, setSlot1Configured] = React7.useState(false);
1684
1698
  const [slot2Configured, setSlot2Configured] = React7.useState(false);
1699
+ const [encryptionPassphrase, setEncryptionPassphrase] = React7.useState();
1685
1700
  ink.useInput((_input, key) => {
1686
1701
  if (key.escape) {
1687
1702
  onCancel();
@@ -1755,7 +1770,7 @@ function KeygenInteractive(props) {
1755
1770
  const handleProviderSelect = (value) => {
1756
1771
  if (value === "filesystem") {
1757
1772
  setSelectedProvider("filesystem");
1758
- void generateKeys("filesystem");
1773
+ setStep("select-filesystem-encryption");
1759
1774
  } else if (value === "1password") {
1760
1775
  setSelectedProvider("1password");
1761
1776
  if (accounts.length === 1 && accounts[0]) {
@@ -1809,6 +1824,31 @@ function KeygenInteractive(props) {
1809
1824
  onError(new Error("YubiKey setup cancelled"));
1810
1825
  }
1811
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
+ };
1812
1852
  const setupYubiKeySlot = async () => {
1813
1853
  setStep("yubikey-configuring");
1814
1854
  try {
@@ -1840,17 +1880,26 @@ function KeygenInteractive(props) {
1840
1880
  const publicKeyPath = props.publicKeyPath ?? core.getDefaultPublicKeyPath();
1841
1881
  if (provider === "filesystem") {
1842
1882
  const fsProvider = new core.FilesystemKeyProvider();
1843
- const genOptions = { publicKeyPath };
1883
+ const genOptions = {
1884
+ publicKeyPath
1885
+ };
1844
1886
  if (props.force !== void 0) {
1845
1887
  genOptions.force = props.force;
1846
1888
  }
1889
+ if (encryptionPassphrase !== void 0) {
1890
+ genOptions.passphrase = encryptionPassphrase;
1891
+ }
1847
1892
  const result = await fsProvider.generateKeyPair(genOptions);
1848
- onComplete({
1893
+ const completionResult = {
1849
1894
  provider: "filesystem",
1850
1895
  publicKeyPath: result.publicKeyPath,
1851
1896
  privateKeyRef: result.privateKeyRef,
1852
1897
  storageDescription: result.storageDescription
1853
- });
1898
+ };
1899
+ if (result.encrypted) {
1900
+ completionResult.encrypted = result.encrypted;
1901
+ }
1902
+ onComplete(completionResult);
1854
1903
  } else if (provider === "1password") {
1855
1904
  if (!selectedVault || !itemName) {
1856
1905
  throw new Error("Vault and item name are required for 1Password");
@@ -1934,6 +1983,8 @@ function KeygenInteractive(props) {
1934
1983
  setStep("done");
1935
1984
  } catch (err) {
1936
1985
  onError(err instanceof Error ? err : new Error("Key generation failed"));
1986
+ } finally {
1987
+ setEncryptionPassphrase(void 0);
1937
1988
  }
1938
1989
  };
1939
1990
  if (step === "checking-providers") {
@@ -1973,6 +2024,39 @@ function KeygenInteractive(props) {
1973
2024
  /* @__PURE__ */ jsxRuntime.jsx(ui.Select, { options, onChange: handleProviderSelect })
1974
2025
  ] });
1975
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
+ }
1976
2060
  if (step === "select-account") {
1977
2061
  const options = accounts.map((account) => ({
1978
2062
  label: account.name ? `${account.name} (${account.email})` : account.email,
@@ -2388,7 +2472,7 @@ async function runVerify(gates, options) {
2388
2472
  process.exit(ExitCode.CONFIG_ERROR);
2389
2473
  }
2390
2474
  const projectRoot = process.cwd();
2391
- const sealsFile = core.readSealsSync(projectRoot);
2475
+ const sealsFile = core.readSealsSync(projectRoot, attestItConfig.settings.sealsPath);
2392
2476
  const gatesToVerify = gates.length > 0 ? gates : Object.keys(attestItConfig.gates);
2393
2477
  for (const gateId of gatesToVerify) {
2394
2478
  if (!attestItConfig.gates[gateId]) {
@@ -2544,7 +2628,7 @@ async function runSeal(gates, options) {
2544
2628
  process.exit(ExitCode.CONFIG_ERROR);
2545
2629
  }
2546
2630
  const projectRoot = process.cwd();
2547
- const sealsFile = core.readSealsSync(projectRoot);
2631
+ const sealsFile = core.readSealsSync(projectRoot, attestItConfig.settings.sealsPath);
2548
2632
  const gatesToSeal = gates.length > 0 ? gates : getAllGateIds(attestItConfig);
2549
2633
  for (const gateId of gatesToSeal) {
2550
2634
  if (!attestItConfig.gates[gateId]) {
@@ -2557,9 +2641,17 @@ async function runSeal(gates, options) {
2557
2641
  skipped: [],
2558
2642
  failed: []
2559
2643
  };
2644
+ const identitySlug = localConfig.activeIdentity;
2560
2645
  for (const gateId of gatesToSeal) {
2561
2646
  try {
2562
- const result = await processSingleGate(gateId, attestItConfig, identity, sealsFile, options);
2647
+ const result = await processSingleGate(
2648
+ gateId,
2649
+ attestItConfig,
2650
+ identity,
2651
+ identitySlug,
2652
+ sealsFile,
2653
+ options
2654
+ );
2563
2655
  if (result.sealed) {
2564
2656
  summary.sealed.push(gateId);
2565
2657
  } else if (result.skipped) {
@@ -2571,7 +2663,7 @@ async function runSeal(gates, options) {
2571
2663
  }
2572
2664
  }
2573
2665
  if (!options.dryRun && summary.sealed.length > 0) {
2574
- core.writeSealsSync(projectRoot, sealsFile);
2666
+ core.writeSealsSync(projectRoot, sealsFile, attestItConfig.settings.sealsPath);
2575
2667
  }
2576
2668
  displaySummary(summary, options.dryRun);
2577
2669
  if (summary.failed.length > 0) {
@@ -2590,7 +2682,7 @@ async function runSeal(gates, options) {
2590
2682
  process.exit(ExitCode.CONFIG_ERROR);
2591
2683
  }
2592
2684
  }
2593
- async function processSingleGate(gateId, config, identity, sealsFile, options) {
2685
+ async function processSingleGate(gateId, config, identity, identitySlug, sealsFile, options) {
2594
2686
  verbose(`Processing gate: ${gateId}`);
2595
2687
  const gate = core.getGate(config, gateId);
2596
2688
  if (!gate) {
@@ -2630,12 +2722,12 @@ async function processSingleGate(gateId, config, identity, sealsFile, options) {
2630
2722
  const seal = core.createSeal({
2631
2723
  gateId,
2632
2724
  fingerprint: fingerprintResult.fingerprint,
2633
- sealedBy: identity.name,
2725
+ sealedBy: identitySlug,
2634
2726
  privateKey: privateKeyPem
2635
2727
  });
2636
2728
  sealsFile.seals[gateId] = seal;
2637
2729
  log(` Sealed gate: ${gateId}`);
2638
- verbose(` Sealed by: ${identity.name}`);
2730
+ verbose(` Sealed by: ${identitySlug} (${identity.name})`);
2639
2731
  verbose(` Timestamp: ${seal.timestamp}`);
2640
2732
  return { sealed: true, skipped: false };
2641
2733
  }
@@ -4072,7 +4164,7 @@ async function runRemove2(slug, options) {
4072
4164
  const projectRoot = process.cwd();
4073
4165
  let sealsFile;
4074
4166
  try {
4075
- sealsFile = core.readSealsSync(projectRoot);
4167
+ sealsFile = core.readSealsSync(projectRoot, attestItConfig.settings.sealsPath);
4076
4168
  } catch {
4077
4169
  sealsFile = { version: 1, seals: {} };
4078
4170
  }