@attest-it/cli 0.8.0 → 0.9.1
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/README.md +0 -10
- package/dist/bin/attest-it.js +542 -1279
- package/dist/bin/attest-it.js.map +1 -1
- package/dist/index.cjs +371 -1170
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +372 -1172
- package/dist/index.js.map +1 -1
- package/dist/templates/config.yaml +55 -0
- package/package.json +2 -3
package/dist/index.js
CHANGED
|
@@ -3,21 +3,19 @@ import * as fs from 'fs';
|
|
|
3
3
|
import { readFileSync } from 'fs';
|
|
4
4
|
import * as path from 'path';
|
|
5
5
|
import { join, dirname } from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
6
7
|
import { detectTheme } from 'chromaterm';
|
|
7
8
|
import { input, select, confirm, checkbox } from '@inquirer/prompts';
|
|
8
|
-
import { loadConfig, toAttestItConfig, readSealsSync, computeFingerprintSync, verifyGateSeal, verifyAllSeals, computeFingerprint, createAttestation, readAttestations, upsertAttestation, KeyProviderRegistry, getDefaultPrivateKeyPath, FilesystemKeyProvider, writeSignedAttestations, loadLocalConfigSync, getActiveIdentity, isAuthorizedSigner, createSeal, writeSealsSync,
|
|
9
|
+
import { loadConfig, toAttestItConfig, readSealsSync, computeFingerprintSync, verifyGateSeal, verifyAllSeals, computeFingerprint, createAttestation, readAttestations, upsertAttestation, KeyProviderRegistry, getDefaultPrivateKeyPath, FilesystemKeyProvider, writeSignedAttestations, loadLocalConfigSync, getActiveIdentity, isAuthorizedSigner, createSeal, writeSealsSync, getGate, loadLocalConfig, OnePasswordKeyProvider, MacOSKeychainKeyProvider, YubiKeyProvider, getAttestItConfigDir, generateEd25519KeyPair, saveLocalConfig, savePublicKey, findConfigPath, loadPreferences, savePreferences, findAttestation, setAttestItHomeDir } from '@attest-it/core';
|
|
9
10
|
import tabtab2 from '@pnpm/tabtab';
|
|
10
11
|
import { spawn } from 'child_process';
|
|
11
12
|
import * as os from 'os';
|
|
12
13
|
import { parse } from 'shell-quote';
|
|
13
14
|
import * as React7 from 'react';
|
|
14
|
-
import { useState, useEffect } from 'react';
|
|
15
15
|
import { render, useApp, Box, Text, useInput } from 'ink';
|
|
16
16
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
17
17
|
import { mkdir, writeFile, unlink, readFile } from 'fs/promises';
|
|
18
|
-
import { Spinner, Select, PasswordInput, TextInput } from '@inkjs/ui';
|
|
19
18
|
import { stringify } from 'yaml';
|
|
20
|
-
import { fileURLToPath } from 'url';
|
|
21
19
|
|
|
22
20
|
// src/index.ts
|
|
23
21
|
var globalOptions = {};
|
|
@@ -252,47 +250,93 @@ async function offerCompletionInstall() {
|
|
|
252
250
|
return false;
|
|
253
251
|
}
|
|
254
252
|
}
|
|
253
|
+
function hasVersion(data) {
|
|
254
|
+
return typeof data === "object" && data !== null && "version" in data && // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
255
|
+
typeof data.version === "string";
|
|
256
|
+
}
|
|
257
|
+
var cachedVersion;
|
|
258
|
+
function getPackageVersion() {
|
|
259
|
+
if (cachedVersion !== void 0) {
|
|
260
|
+
return cachedVersion;
|
|
261
|
+
}
|
|
262
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
263
|
+
const __dirname = dirname(__filename);
|
|
264
|
+
const possiblePaths = [join(__dirname, "../package.json"), join(__dirname, "../../package.json")];
|
|
265
|
+
for (const packageJsonPath of possiblePaths) {
|
|
266
|
+
try {
|
|
267
|
+
const content = readFileSync(packageJsonPath, "utf-8");
|
|
268
|
+
const packageJsonData = JSON.parse(content);
|
|
269
|
+
if (!hasVersion(packageJsonData)) {
|
|
270
|
+
throw new Error(`Invalid package.json at ${packageJsonPath}: missing version field`);
|
|
271
|
+
}
|
|
272
|
+
cachedVersion = packageJsonData.version;
|
|
273
|
+
return cachedVersion;
|
|
274
|
+
} catch (error2) {
|
|
275
|
+
if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
throw error2;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
throw new Error("Could not find package.json");
|
|
282
|
+
}
|
|
255
283
|
|
|
256
284
|
// src/commands/init.ts
|
|
257
285
|
var initCommand = new 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) => {
|
|
258
286
|
await runInit(options);
|
|
259
287
|
});
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
288
|
+
function loadConfigTemplate() {
|
|
289
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
290
|
+
const __dirname = dirname(__filename);
|
|
291
|
+
const possiblePaths = [
|
|
292
|
+
join(__dirname, "../../templates/config.yaml"),
|
|
293
|
+
join(__dirname, "../templates/config.yaml")
|
|
294
|
+
];
|
|
295
|
+
for (const templatePath of possiblePaths) {
|
|
296
|
+
try {
|
|
297
|
+
return fs.readFileSync(templatePath, "utf-8");
|
|
298
|
+
} catch (error2) {
|
|
299
|
+
if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
throw error2;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
throw new Error("Could not find config.yaml template");
|
|
306
|
+
}
|
|
307
|
+
function isPackageJson(data) {
|
|
308
|
+
return typeof data === "object" && data !== null && "name" in data && "version" in data && // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
309
|
+
typeof data.name === "string" && // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
310
|
+
typeof data.version === "string";
|
|
311
|
+
}
|
|
312
|
+
function detectPackageManager() {
|
|
313
|
+
if (fs.existsSync("pnpm-lock.yaml")) return "pnpm";
|
|
314
|
+
if (fs.existsSync("yarn.lock")) return "yarn";
|
|
315
|
+
if (fs.existsSync("bun.lockb")) return "bun";
|
|
316
|
+
return "npm";
|
|
317
|
+
}
|
|
318
|
+
async function ensureDevDependency() {
|
|
319
|
+
const packageJsonPath = "package.json";
|
|
320
|
+
const packageManager = detectPackageManager();
|
|
321
|
+
let created = false;
|
|
322
|
+
let packageJson;
|
|
323
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
324
|
+
const content = await fs.promises.readFile(packageJsonPath, "utf8");
|
|
325
|
+
const parsed = JSON.parse(content);
|
|
326
|
+
if (!isPackageJson(parsed)) {
|
|
327
|
+
throw new Error("Invalid package.json: missing required name or version field");
|
|
328
|
+
}
|
|
329
|
+
packageJson = parsed;
|
|
330
|
+
} else {
|
|
331
|
+
packageJson = { name: path.basename(process.cwd()), version: "1.0.0" };
|
|
332
|
+
created = true;
|
|
333
|
+
}
|
|
334
|
+
const devDeps = packageJson.devDependencies ?? {};
|
|
335
|
+
devDeps["attest-it"] = "^" + getPackageVersion();
|
|
336
|
+
packageJson.devDependencies = devDeps;
|
|
337
|
+
await fs.promises.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n");
|
|
338
|
+
return { packageManager, created };
|
|
339
|
+
}
|
|
296
340
|
async function runInit(options) {
|
|
297
341
|
try {
|
|
298
342
|
const configPath = path.resolve(options.path);
|
|
@@ -307,14 +351,22 @@ async function runInit(options) {
|
|
|
307
351
|
process.exit(ExitCode.CANCELLED);
|
|
308
352
|
}
|
|
309
353
|
}
|
|
354
|
+
const { packageManager, created } = await ensureDevDependency();
|
|
355
|
+
if (created) {
|
|
356
|
+
success("Created package.json");
|
|
357
|
+
} else {
|
|
358
|
+
success("Updated package.json with attest-it devDependency");
|
|
359
|
+
}
|
|
310
360
|
await fs.promises.mkdir(configDir, { recursive: true });
|
|
311
|
-
|
|
361
|
+
const configTemplate = loadConfigTemplate();
|
|
362
|
+
await fs.promises.writeFile(configPath, configTemplate, "utf-8");
|
|
312
363
|
success(`Configuration created at ${configPath}`);
|
|
313
364
|
log("");
|
|
314
365
|
log("Next steps:");
|
|
315
|
-
log(` 1.
|
|
316
|
-
log(" 2. Run: attest-it
|
|
317
|
-
log(" 3. Run: attest-it
|
|
366
|
+
log(` 1. Run: ${packageManager} install`);
|
|
367
|
+
log(" 2. Run: attest-it identity create (if you haven't already)");
|
|
368
|
+
log(" 3. Run: attest-it team join");
|
|
369
|
+
log(" 4. Edit .attest-it/config.yaml to define your gates and suites");
|
|
318
370
|
await offerCompletionInstall();
|
|
319
371
|
} catch (err) {
|
|
320
372
|
if (err instanceof Error) {
|
|
@@ -546,14 +598,14 @@ function SelectionPrompt({
|
|
|
546
598
|
onSelect,
|
|
547
599
|
groups
|
|
548
600
|
}) {
|
|
549
|
-
useInput((
|
|
550
|
-
const matchedOption = options.find((opt) => opt.hint ===
|
|
601
|
+
useInput((input4) => {
|
|
602
|
+
const matchedOption = options.find((opt) => opt.hint === input4);
|
|
551
603
|
if (matchedOption) {
|
|
552
604
|
onSelect(matchedOption.value);
|
|
553
605
|
return;
|
|
554
606
|
}
|
|
555
607
|
if (groups) {
|
|
556
|
-
const matchedGroup = groups.find((group) => group.name ===
|
|
608
|
+
const matchedGroup = groups.find((group) => group.name === input4);
|
|
557
609
|
if (matchedGroup) {
|
|
558
610
|
onSelect(matchedGroup.name);
|
|
559
611
|
}
|
|
@@ -603,17 +655,17 @@ function SuiteSelector({
|
|
|
603
655
|
return next;
|
|
604
656
|
});
|
|
605
657
|
}, []);
|
|
606
|
-
useInput((
|
|
607
|
-
if (
|
|
658
|
+
useInput((input4, key) => {
|
|
659
|
+
if (input4 === "a") {
|
|
608
660
|
setSelectedSuites(new Set(pendingSuites.map((s) => s.name)));
|
|
609
661
|
return;
|
|
610
662
|
}
|
|
611
|
-
if (
|
|
663
|
+
if (input4 === "n") {
|
|
612
664
|
onExit();
|
|
613
665
|
return;
|
|
614
666
|
}
|
|
615
|
-
if (/^[1-9]$/.test(
|
|
616
|
-
const idx = parseInt(
|
|
667
|
+
if (/^[1-9]$/.test(input4)) {
|
|
668
|
+
const idx = parseInt(input4, 10) - 1;
|
|
617
669
|
if (idx < pendingSuites.length) {
|
|
618
670
|
const suite = pendingSuites[idx];
|
|
619
671
|
if (suite) {
|
|
@@ -622,8 +674,8 @@ function SuiteSelector({
|
|
|
622
674
|
}
|
|
623
675
|
return;
|
|
624
676
|
}
|
|
625
|
-
if (
|
|
626
|
-
const groupIdx = parseInt(
|
|
677
|
+
if (input4.startsWith("g") && groups) {
|
|
678
|
+
const groupIdx = parseInt(input4.slice(1), 10) - 1;
|
|
627
679
|
const groupNames = Object.keys(groups);
|
|
628
680
|
if (groupIdx >= 0 && groupIdx < groupNames.length) {
|
|
629
681
|
const groupName = groupNames[groupIdx];
|
|
@@ -640,7 +692,7 @@ function SuiteSelector({
|
|
|
640
692
|
onSelect(Array.from(selectedSuites));
|
|
641
693
|
return;
|
|
642
694
|
}
|
|
643
|
-
if (
|
|
695
|
+
if (input4 === " ") {
|
|
644
696
|
const currentSuite = pendingSuites[cursorIndex];
|
|
645
697
|
if (currentSuite) {
|
|
646
698
|
toggleSuite(currentSuite.name);
|
|
@@ -773,11 +825,11 @@ function TestRunner({
|
|
|
773
825
|
};
|
|
774
826
|
}, [currentIndex, phase, suites, executeTest, onComplete]);
|
|
775
827
|
useInput(
|
|
776
|
-
(
|
|
828
|
+
(input4, key) => {
|
|
777
829
|
if (phase !== "confirming") return;
|
|
778
830
|
const currentSuite2 = suites[currentIndex];
|
|
779
831
|
if (!currentSuite2) return;
|
|
780
|
-
if (
|
|
832
|
+
if (input4.toLowerCase() === "y" || key.return) {
|
|
781
833
|
createAttestation3(currentSuite2).then(() => {
|
|
782
834
|
setResults((prev) => ({
|
|
783
835
|
...prev,
|
|
@@ -794,7 +846,7 @@ function TestRunner({
|
|
|
794
846
|
setPhase("running");
|
|
795
847
|
});
|
|
796
848
|
}
|
|
797
|
-
if (
|
|
849
|
+
if (input4.toLowerCase() === "n") {
|
|
798
850
|
setResults((prev) => ({
|
|
799
851
|
...prev,
|
|
800
852
|
skipped: [...prev.skipped, currentSuite2]
|
|
@@ -1507,9 +1559,9 @@ async function runSingleSuite(suiteName, config, options) {
|
|
|
1507
1559
|
if (!await keyProvider.keyExists(keyRef)) {
|
|
1508
1560
|
error(`Private key not found in ${keyProvider.displayName}`);
|
|
1509
1561
|
if (keyProvider.type === "filesystem") {
|
|
1510
|
-
error('Run "attest-it
|
|
1562
|
+
error('Run "attest-it identity create" first to generate a keypair.');
|
|
1511
1563
|
} else {
|
|
1512
|
-
error('Run "attest-it
|
|
1564
|
+
error('Run "attest-it identity create" to generate and store a key.');
|
|
1513
1565
|
}
|
|
1514
1566
|
process.exit(ExitCode.MISSING_KEY);
|
|
1515
1567
|
}
|
|
@@ -1533,7 +1585,7 @@ async function promptForSeal(suiteName, gateId, config) {
|
|
|
1533
1585
|
const localConfig = loadLocalConfigSync();
|
|
1534
1586
|
if (!localConfig) {
|
|
1535
1587
|
warn("No local identity configuration found - cannot create seal");
|
|
1536
|
-
warn('Run "attest-it
|
|
1588
|
+
warn('Run "attest-it identity create" to set up your identity');
|
|
1537
1589
|
return;
|
|
1538
1590
|
}
|
|
1539
1591
|
const identity = getActiveIdentity(localConfig);
|
|
@@ -1568,8 +1620,8 @@ async function promptForSeal(suiteName, gateId, config) {
|
|
|
1568
1620
|
const keyProvider = createKeyProviderFromIdentity(identity);
|
|
1569
1621
|
const keyRef = getKeyRefFromIdentity(identity);
|
|
1570
1622
|
const keyResult = await keyProvider.getPrivateKey(keyRef);
|
|
1571
|
-
const
|
|
1572
|
-
const privateKeyPem = await
|
|
1623
|
+
const fs3 = await import('fs/promises');
|
|
1624
|
+
const privateKeyPem = await fs3.readFile(keyResult.keyPath, "utf8");
|
|
1573
1625
|
await keyResult.cleanup();
|
|
1574
1626
|
const identitySlug = localConfig.activeIdentity;
|
|
1575
1627
|
const seal = createSeal({
|
|
@@ -1651,697 +1703,6 @@ function getKeyRefFromIdentity(identity) {
|
|
|
1651
1703
|
}
|
|
1652
1704
|
}
|
|
1653
1705
|
}
|
|
1654
|
-
var MIN_PASSPHRASE_LENGTH = 8;
|
|
1655
|
-
function KeygenInteractive(props) {
|
|
1656
|
-
const { onComplete, onCancel, onError } = props;
|
|
1657
|
-
const [step, setStep] = useState("checking-providers");
|
|
1658
|
-
const [opAvailable, setOpAvailable] = useState(false);
|
|
1659
|
-
const [keychainAvailable, setKeychainAvailable] = useState(false);
|
|
1660
|
-
const [yubiKeyAvailable, setYubiKeyAvailable] = useState(false);
|
|
1661
|
-
const [accounts, setAccounts] = useState([]);
|
|
1662
|
-
const [vaults, setVaults] = useState([]);
|
|
1663
|
-
const [yubiKeyDevices, setYubiKeyDevices] = useState([]);
|
|
1664
|
-
const [_selectedProvider, setSelectedProvider] = useState();
|
|
1665
|
-
const [selectedAccount, setSelectedAccount] = useState();
|
|
1666
|
-
const [selectedVault, setSelectedVault] = useState();
|
|
1667
|
-
const [itemName, setItemName] = useState("attest-it-private-key");
|
|
1668
|
-
const [keychainItemName, setKeychainItemName] = useState("attest-it-private-key");
|
|
1669
|
-
const [selectedYubiKeySerial, setSelectedYubiKeySerial] = useState();
|
|
1670
|
-
const [selectedYubiKeySlot, setSelectedYubiKeySlot] = useState(2);
|
|
1671
|
-
const [slot1Configured, setSlot1Configured] = useState(false);
|
|
1672
|
-
const [slot2Configured, setSlot2Configured] = useState(false);
|
|
1673
|
-
const [encryptionPassphrase, setEncryptionPassphrase] = useState();
|
|
1674
|
-
useInput((_input, key) => {
|
|
1675
|
-
if (key.escape) {
|
|
1676
|
-
onCancel();
|
|
1677
|
-
}
|
|
1678
|
-
});
|
|
1679
|
-
useEffect(() => {
|
|
1680
|
-
const checkProviders = async () => {
|
|
1681
|
-
try {
|
|
1682
|
-
const isInstalled = await OnePasswordKeyProvider.isInstalled();
|
|
1683
|
-
setOpAvailable(isInstalled);
|
|
1684
|
-
if (isInstalled) {
|
|
1685
|
-
const accountList = await OnePasswordKeyProvider.listAccounts();
|
|
1686
|
-
setAccounts(accountList);
|
|
1687
|
-
}
|
|
1688
|
-
} catch {
|
|
1689
|
-
setOpAvailable(false);
|
|
1690
|
-
}
|
|
1691
|
-
const isKeychainAvailable = MacOSKeychainKeyProvider.isAvailable();
|
|
1692
|
-
setKeychainAvailable(isKeychainAvailable);
|
|
1693
|
-
try {
|
|
1694
|
-
const isInstalled = await YubiKeyProvider.isInstalled();
|
|
1695
|
-
if (isInstalled) {
|
|
1696
|
-
const isConnected = await YubiKeyProvider.isConnected();
|
|
1697
|
-
if (isConnected) {
|
|
1698
|
-
const devices = await YubiKeyProvider.listDevices();
|
|
1699
|
-
setYubiKeyDevices(devices);
|
|
1700
|
-
setYubiKeyAvailable(devices.length > 0);
|
|
1701
|
-
}
|
|
1702
|
-
}
|
|
1703
|
-
} catch {
|
|
1704
|
-
setYubiKeyAvailable(false);
|
|
1705
|
-
}
|
|
1706
|
-
setStep("select-provider");
|
|
1707
|
-
};
|
|
1708
|
-
void checkProviders();
|
|
1709
|
-
}, []);
|
|
1710
|
-
useEffect(() => {
|
|
1711
|
-
if (step === "select-vault" && selectedAccount) {
|
|
1712
|
-
const fetchVaults = async () => {
|
|
1713
|
-
try {
|
|
1714
|
-
const vaultList = await OnePasswordKeyProvider.listVaults(selectedAccount);
|
|
1715
|
-
setVaults(vaultList);
|
|
1716
|
-
} catch (err) {
|
|
1717
|
-
onError(err instanceof Error ? err : new Error("Failed to fetch vaults"));
|
|
1718
|
-
}
|
|
1719
|
-
};
|
|
1720
|
-
void fetchVaults();
|
|
1721
|
-
}
|
|
1722
|
-
}, [step, selectedAccount, onError]);
|
|
1723
|
-
const checkYubiKeySlots = async (serial) => {
|
|
1724
|
-
try {
|
|
1725
|
-
const slot1 = await YubiKeyProvider.isChallengeResponseConfigured(1, serial);
|
|
1726
|
-
const slot2 = await YubiKeyProvider.isChallengeResponseConfigured(2, serial);
|
|
1727
|
-
setSlot1Configured(slot1);
|
|
1728
|
-
setSlot2Configured(slot2);
|
|
1729
|
-
if (slot1 && slot2) {
|
|
1730
|
-
setStep("select-yubikey-slot");
|
|
1731
|
-
} else if (slot2) {
|
|
1732
|
-
setSelectedYubiKeySlot(2);
|
|
1733
|
-
void generateKeys("yubikey");
|
|
1734
|
-
} else if (slot1) {
|
|
1735
|
-
setSelectedYubiKeySlot(1);
|
|
1736
|
-
void generateKeys("yubikey");
|
|
1737
|
-
} else {
|
|
1738
|
-
setStep("yubikey-offer-setup");
|
|
1739
|
-
}
|
|
1740
|
-
} catch (err) {
|
|
1741
|
-
onError(err instanceof Error ? err : new Error("Failed to check YubiKey slots"));
|
|
1742
|
-
}
|
|
1743
|
-
};
|
|
1744
|
-
const handleProviderSelect = (value) => {
|
|
1745
|
-
if (value === "filesystem") {
|
|
1746
|
-
setSelectedProvider("filesystem");
|
|
1747
|
-
setStep("select-filesystem-encryption");
|
|
1748
|
-
} else if (value === "1password") {
|
|
1749
|
-
setSelectedProvider("1password");
|
|
1750
|
-
if (accounts.length === 1 && accounts[0]) {
|
|
1751
|
-
setSelectedAccount(accounts[0].user_uuid);
|
|
1752
|
-
setStep("select-vault");
|
|
1753
|
-
} else {
|
|
1754
|
-
setStep("select-account");
|
|
1755
|
-
}
|
|
1756
|
-
} else if (value === "macos-keychain") {
|
|
1757
|
-
setSelectedProvider("macos-keychain");
|
|
1758
|
-
setStep("enter-keychain-item-name");
|
|
1759
|
-
} else if (value === "yubikey") {
|
|
1760
|
-
setSelectedProvider("yubikey");
|
|
1761
|
-
if (yubiKeyDevices.length > 1) {
|
|
1762
|
-
setStep("select-yubikey-device");
|
|
1763
|
-
} else if (yubiKeyDevices.length === 1 && yubiKeyDevices[0]) {
|
|
1764
|
-
setSelectedYubiKeySerial(yubiKeyDevices[0].serial);
|
|
1765
|
-
void checkYubiKeySlots(yubiKeyDevices[0].serial);
|
|
1766
|
-
}
|
|
1767
|
-
}
|
|
1768
|
-
};
|
|
1769
|
-
const handleAccountSelect = (value) => {
|
|
1770
|
-
setSelectedAccount(value);
|
|
1771
|
-
setStep("select-vault");
|
|
1772
|
-
};
|
|
1773
|
-
const handleVaultSelect = (value) => {
|
|
1774
|
-
setSelectedVault(value);
|
|
1775
|
-
setStep("enter-item-name");
|
|
1776
|
-
};
|
|
1777
|
-
const handleItemNameSubmit = (value) => {
|
|
1778
|
-
setItemName(value);
|
|
1779
|
-
void generateKeys("1password");
|
|
1780
|
-
};
|
|
1781
|
-
const handleKeychainItemNameSubmit = (value) => {
|
|
1782
|
-
setKeychainItemName(value);
|
|
1783
|
-
void generateKeys("macos-keychain");
|
|
1784
|
-
};
|
|
1785
|
-
const handleYubiKeyDeviceSelect = (value) => {
|
|
1786
|
-
setSelectedYubiKeySerial(value);
|
|
1787
|
-
void checkYubiKeySlots(value);
|
|
1788
|
-
};
|
|
1789
|
-
const handleYubiKeySlotSelect = (value) => {
|
|
1790
|
-
const slot = value === "1" ? 1 : 2;
|
|
1791
|
-
setSelectedYubiKeySlot(slot);
|
|
1792
|
-
void generateKeys("yubikey");
|
|
1793
|
-
};
|
|
1794
|
-
const handleYubiKeySetupConfirm = (value) => {
|
|
1795
|
-
if (value === "yes") {
|
|
1796
|
-
void setupYubiKeySlot();
|
|
1797
|
-
} else {
|
|
1798
|
-
onError(new Error("YubiKey setup cancelled"));
|
|
1799
|
-
}
|
|
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
|
-
};
|
|
1826
|
-
const setupYubiKeySlot = async () => {
|
|
1827
|
-
setStep("yubikey-configuring");
|
|
1828
|
-
try {
|
|
1829
|
-
const { spawn: spawn3 } = await import('child_process');
|
|
1830
|
-
const args = ["otp", "chalresp", "--touch", "--generate", "2"];
|
|
1831
|
-
if (selectedYubiKeySerial) {
|
|
1832
|
-
args.unshift("--device", selectedYubiKeySerial);
|
|
1833
|
-
}
|
|
1834
|
-
await new Promise((resolve2, reject) => {
|
|
1835
|
-
const proc = spawn3("ykman", args, { stdio: "inherit" });
|
|
1836
|
-
proc.on("close", (code) => {
|
|
1837
|
-
if (code === 0) {
|
|
1838
|
-
resolve2();
|
|
1839
|
-
} else {
|
|
1840
|
-
reject(new Error(`ykman exited with code ${String(code)}`));
|
|
1841
|
-
}
|
|
1842
|
-
});
|
|
1843
|
-
proc.on("error", reject);
|
|
1844
|
-
});
|
|
1845
|
-
setSelectedYubiKeySlot(2);
|
|
1846
|
-
void generateKeys("yubikey");
|
|
1847
|
-
} catch (err) {
|
|
1848
|
-
onError(err instanceof Error ? err : new Error("Failed to configure YubiKey"));
|
|
1849
|
-
}
|
|
1850
|
-
};
|
|
1851
|
-
const generateKeys = async (provider) => {
|
|
1852
|
-
setStep("generating");
|
|
1853
|
-
try {
|
|
1854
|
-
const publicKeyPath = props.publicKeyPath ?? getDefaultPublicKeyPath();
|
|
1855
|
-
if (provider === "filesystem") {
|
|
1856
|
-
const fsProvider = new FilesystemKeyProvider();
|
|
1857
|
-
const genOptions = {
|
|
1858
|
-
publicKeyPath
|
|
1859
|
-
};
|
|
1860
|
-
if (props.force !== void 0) {
|
|
1861
|
-
genOptions.force = props.force;
|
|
1862
|
-
}
|
|
1863
|
-
if (encryptionPassphrase !== void 0) {
|
|
1864
|
-
genOptions.passphrase = encryptionPassphrase;
|
|
1865
|
-
}
|
|
1866
|
-
const result = await fsProvider.generateKeyPair(genOptions);
|
|
1867
|
-
const completionResult = {
|
|
1868
|
-
provider: "filesystem",
|
|
1869
|
-
publicKeyPath: result.publicKeyPath,
|
|
1870
|
-
privateKeyRef: result.privateKeyRef,
|
|
1871
|
-
storageDescription: result.storageDescription
|
|
1872
|
-
};
|
|
1873
|
-
if (result.encrypted) {
|
|
1874
|
-
completionResult.encrypted = result.encrypted;
|
|
1875
|
-
}
|
|
1876
|
-
onComplete(completionResult);
|
|
1877
|
-
} else if (provider === "1password") {
|
|
1878
|
-
if (!selectedVault || !itemName) {
|
|
1879
|
-
throw new Error("Vault and item name are required for 1Password");
|
|
1880
|
-
}
|
|
1881
|
-
const vault = vaults.find((v) => v.id === selectedVault);
|
|
1882
|
-
if (!vault) {
|
|
1883
|
-
throw new Error("Selected vault not found");
|
|
1884
|
-
}
|
|
1885
|
-
const providerOptions = {
|
|
1886
|
-
vault: vault.name,
|
|
1887
|
-
itemName
|
|
1888
|
-
};
|
|
1889
|
-
if (selectedAccount !== void 0) {
|
|
1890
|
-
providerOptions.account = selectedAccount;
|
|
1891
|
-
}
|
|
1892
|
-
const opProvider = new OnePasswordKeyProvider(providerOptions);
|
|
1893
|
-
const genOptions = { publicKeyPath };
|
|
1894
|
-
if (props.force !== void 0) {
|
|
1895
|
-
genOptions.force = props.force;
|
|
1896
|
-
}
|
|
1897
|
-
const result = await opProvider.generateKeyPair(genOptions);
|
|
1898
|
-
const completionResult = {
|
|
1899
|
-
provider: "1password",
|
|
1900
|
-
publicKeyPath: result.publicKeyPath,
|
|
1901
|
-
privateKeyRef: result.privateKeyRef,
|
|
1902
|
-
storageDescription: result.storageDescription,
|
|
1903
|
-
vault: vault.name,
|
|
1904
|
-
itemName
|
|
1905
|
-
};
|
|
1906
|
-
if (selectedAccount !== void 0) {
|
|
1907
|
-
completionResult.account = selectedAccount;
|
|
1908
|
-
}
|
|
1909
|
-
onComplete(completionResult);
|
|
1910
|
-
} else if (provider === "macos-keychain") {
|
|
1911
|
-
if (!keychainItemName) {
|
|
1912
|
-
throw new Error("Item name is required for macOS Keychain");
|
|
1913
|
-
}
|
|
1914
|
-
const keychainProvider = new MacOSKeychainKeyProvider({
|
|
1915
|
-
itemName: keychainItemName
|
|
1916
|
-
});
|
|
1917
|
-
const genOptions = { publicKeyPath };
|
|
1918
|
-
if (props.force !== void 0) {
|
|
1919
|
-
genOptions.force = props.force;
|
|
1920
|
-
}
|
|
1921
|
-
const result = await keychainProvider.generateKeyPair(genOptions);
|
|
1922
|
-
onComplete({
|
|
1923
|
-
provider: "macos-keychain",
|
|
1924
|
-
publicKeyPath: result.publicKeyPath,
|
|
1925
|
-
privateKeyRef: result.privateKeyRef,
|
|
1926
|
-
storageDescription: result.storageDescription,
|
|
1927
|
-
itemName: keychainItemName
|
|
1928
|
-
});
|
|
1929
|
-
} else {
|
|
1930
|
-
const encryptedKeyPath = getDefaultYubiKeyEncryptedKeyPath();
|
|
1931
|
-
const providerOptions = {
|
|
1932
|
-
encryptedKeyPath,
|
|
1933
|
-
slot: selectedYubiKeySlot
|
|
1934
|
-
};
|
|
1935
|
-
if (selectedYubiKeySerial !== void 0) {
|
|
1936
|
-
providerOptions.serial = selectedYubiKeySerial;
|
|
1937
|
-
}
|
|
1938
|
-
const ykProvider = new YubiKeyProvider(providerOptions);
|
|
1939
|
-
const genOptions = { publicKeyPath };
|
|
1940
|
-
if (props.force !== void 0) {
|
|
1941
|
-
genOptions.force = props.force;
|
|
1942
|
-
}
|
|
1943
|
-
const result = await ykProvider.generateKeyPair(genOptions);
|
|
1944
|
-
const completionResult = {
|
|
1945
|
-
provider: "yubikey",
|
|
1946
|
-
publicKeyPath: result.publicKeyPath,
|
|
1947
|
-
privateKeyRef: result.privateKeyRef,
|
|
1948
|
-
storageDescription: result.storageDescription,
|
|
1949
|
-
slot: selectedYubiKeySlot,
|
|
1950
|
-
encryptedKeyPath
|
|
1951
|
-
};
|
|
1952
|
-
if (selectedYubiKeySerial !== void 0) {
|
|
1953
|
-
completionResult.serial = selectedYubiKeySerial;
|
|
1954
|
-
}
|
|
1955
|
-
onComplete(completionResult);
|
|
1956
|
-
}
|
|
1957
|
-
setStep("done");
|
|
1958
|
-
} catch (err) {
|
|
1959
|
-
onError(err instanceof Error ? err : new Error("Key generation failed"));
|
|
1960
|
-
} finally {
|
|
1961
|
-
setEncryptionPassphrase(void 0);
|
|
1962
|
-
}
|
|
1963
|
-
};
|
|
1964
|
-
if (step === "checking-providers") {
|
|
1965
|
-
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
|
|
1966
|
-
/* @__PURE__ */ jsx(Spinner, {}),
|
|
1967
|
-
/* @__PURE__ */ jsx(Text, { children: "Checking available key storage providers..." })
|
|
1968
|
-
] }) });
|
|
1969
|
-
}
|
|
1970
|
-
if (step === "select-provider") {
|
|
1971
|
-
const options = [
|
|
1972
|
-
{
|
|
1973
|
-
label: `Local Filesystem (${getDefaultPrivateKeyPath()})`,
|
|
1974
|
-
value: "filesystem"
|
|
1975
|
-
}
|
|
1976
|
-
];
|
|
1977
|
-
if (keychainAvailable) {
|
|
1978
|
-
options.push({
|
|
1979
|
-
label: "macOS Keychain",
|
|
1980
|
-
value: "macos-keychain"
|
|
1981
|
-
});
|
|
1982
|
-
}
|
|
1983
|
-
if (yubiKeyAvailable) {
|
|
1984
|
-
options.push({
|
|
1985
|
-
label: "YubiKey (hardware security key)",
|
|
1986
|
-
value: "yubikey"
|
|
1987
|
-
});
|
|
1988
|
-
}
|
|
1989
|
-
if (opAvailable) {
|
|
1990
|
-
options.push({
|
|
1991
|
-
label: "1Password (requires op CLI)",
|
|
1992
|
-
value: "1password"
|
|
1993
|
-
});
|
|
1994
|
-
}
|
|
1995
|
-
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
1996
|
-
/* @__PURE__ */ jsx(Text, { bold: true, children: "Where would you like to store your private key?" }),
|
|
1997
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "" }),
|
|
1998
|
-
/* @__PURE__ */ jsx(Select, { options, onChange: handleProviderSelect })
|
|
1999
|
-
] });
|
|
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
|
-
}
|
|
2034
|
-
if (step === "select-account") {
|
|
2035
|
-
const options = accounts.map((account) => ({
|
|
2036
|
-
label: account.name ? `${account.name} (${account.email})` : account.email,
|
|
2037
|
-
value: account.user_uuid
|
|
2038
|
-
}));
|
|
2039
|
-
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
2040
|
-
/* @__PURE__ */ jsx(Text, { bold: true, children: "Select 1Password account:" }),
|
|
2041
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "" }),
|
|
2042
|
-
/* @__PURE__ */ jsx(Select, { options, onChange: handleAccountSelect })
|
|
2043
|
-
] });
|
|
2044
|
-
}
|
|
2045
|
-
if (step === "select-vault") {
|
|
2046
|
-
if (vaults.length === 0) {
|
|
2047
|
-
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
|
|
2048
|
-
/* @__PURE__ */ jsx(Spinner, {}),
|
|
2049
|
-
/* @__PURE__ */ jsx(Text, { children: "Loading vaults..." })
|
|
2050
|
-
] }) });
|
|
2051
|
-
}
|
|
2052
|
-
const options = vaults.map((vault) => ({
|
|
2053
|
-
label: vault.name,
|
|
2054
|
-
value: vault.id
|
|
2055
|
-
}));
|
|
2056
|
-
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
2057
|
-
/* @__PURE__ */ jsx(Text, { bold: true, children: "Select vault for private key storage:" }),
|
|
2058
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "" }),
|
|
2059
|
-
/* @__PURE__ */ jsx(Select, { options, onChange: handleVaultSelect })
|
|
2060
|
-
] });
|
|
2061
|
-
}
|
|
2062
|
-
if (step === "enter-item-name") {
|
|
2063
|
-
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
2064
|
-
/* @__PURE__ */ jsx(Text, { bold: true, children: "Enter name for the key item:" }),
|
|
2065
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "(This will be visible in your 1Password vault)" }),
|
|
2066
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "" }),
|
|
2067
|
-
/* @__PURE__ */ jsx(TextInput, { defaultValue: itemName, onSubmit: handleItemNameSubmit })
|
|
2068
|
-
] });
|
|
2069
|
-
}
|
|
2070
|
-
if (step === "enter-keychain-item-name") {
|
|
2071
|
-
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
2072
|
-
/* @__PURE__ */ jsx(Text, { bold: true, children: "Enter name for the keychain item:" }),
|
|
2073
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "(This will be the service name in your macOS Keychain)" }),
|
|
2074
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "" }),
|
|
2075
|
-
/* @__PURE__ */ jsx(TextInput, { defaultValue: keychainItemName, onSubmit: handleKeychainItemNameSubmit })
|
|
2076
|
-
] });
|
|
2077
|
-
}
|
|
2078
|
-
if (step === "select-yubikey-device") {
|
|
2079
|
-
const options = yubiKeyDevices.map((device) => ({
|
|
2080
|
-
label: `${device.type} (Serial: ${device.serial})`,
|
|
2081
|
-
value: device.serial
|
|
2082
|
-
}));
|
|
2083
|
-
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
2084
|
-
/* @__PURE__ */ jsx(Text, { bold: true, children: "Select YubiKey device:" }),
|
|
2085
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "" }),
|
|
2086
|
-
/* @__PURE__ */ jsx(Select, { options, onChange: handleYubiKeyDeviceSelect })
|
|
2087
|
-
] });
|
|
2088
|
-
}
|
|
2089
|
-
if (step === "select-yubikey-slot") {
|
|
2090
|
-
const options = [];
|
|
2091
|
-
if (slot2Configured) {
|
|
2092
|
-
options.push({
|
|
2093
|
-
label: "Slot 2 (recommended)",
|
|
2094
|
-
value: "2"
|
|
2095
|
-
});
|
|
2096
|
-
}
|
|
2097
|
-
if (slot1Configured) {
|
|
2098
|
-
options.push({
|
|
2099
|
-
label: "Slot 1",
|
|
2100
|
-
value: "1"
|
|
2101
|
-
});
|
|
2102
|
-
}
|
|
2103
|
-
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
2104
|
-
/* @__PURE__ */ jsx(Text, { bold: true, children: "Select YubiKey slot for challenge-response:" }),
|
|
2105
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "" }),
|
|
2106
|
-
/* @__PURE__ */ jsx(Select, { options, onChange: handleYubiKeySlotSelect })
|
|
2107
|
-
] });
|
|
2108
|
-
}
|
|
2109
|
-
if (step === "yubikey-offer-setup") {
|
|
2110
|
-
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
2111
|
-
/* @__PURE__ */ jsx(Text, { bold: true, children: "Your YubiKey is not configured for challenge-response." }),
|
|
2112
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "" }),
|
|
2113
|
-
/* @__PURE__ */ jsx(Text, { children: "Would you like to configure slot 2 for challenge-response now?" }),
|
|
2114
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "This will enable touch-to-sign functionality." }),
|
|
2115
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "" }),
|
|
2116
|
-
/* @__PURE__ */ jsx(
|
|
2117
|
-
Select,
|
|
2118
|
-
{
|
|
2119
|
-
options: [
|
|
2120
|
-
{ label: "Yes, configure my YubiKey", value: "yes" },
|
|
2121
|
-
{ label: "No, cancel", value: "no" }
|
|
2122
|
-
],
|
|
2123
|
-
onChange: handleYubiKeySetupConfirm
|
|
2124
|
-
}
|
|
2125
|
-
)
|
|
2126
|
-
] });
|
|
2127
|
-
}
|
|
2128
|
-
if (step === "yubikey-configuring") {
|
|
2129
|
-
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
2130
|
-
/* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
|
|
2131
|
-
/* @__PURE__ */ jsx(Spinner, {}),
|
|
2132
|
-
/* @__PURE__ */ jsx(Text, { children: "Configuring YubiKey slot 2 for challenge-response..." })
|
|
2133
|
-
] }),
|
|
2134
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "Touch your YubiKey when it flashes." })
|
|
2135
|
-
] });
|
|
2136
|
-
}
|
|
2137
|
-
if (step === "generating") {
|
|
2138
|
-
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
|
|
2139
|
-
/* @__PURE__ */ jsx(Spinner, {}),
|
|
2140
|
-
/* @__PURE__ */ jsx(Text, { children: "Generating RSA-2048 keypair..." })
|
|
2141
|
-
] }) });
|
|
2142
|
-
}
|
|
2143
|
-
return /* @__PURE__ */ jsx(Box, {});
|
|
2144
|
-
}
|
|
2145
|
-
async function runKeygenInteractive(options) {
|
|
2146
|
-
return new Promise((resolve2, reject) => {
|
|
2147
|
-
const props = {
|
|
2148
|
-
onComplete: (result) => {
|
|
2149
|
-
unmount();
|
|
2150
|
-
resolve2(result);
|
|
2151
|
-
},
|
|
2152
|
-
onCancel: () => {
|
|
2153
|
-
unmount();
|
|
2154
|
-
reject(new Error("Keygen cancelled"));
|
|
2155
|
-
},
|
|
2156
|
-
onError: (error2) => {
|
|
2157
|
-
unmount();
|
|
2158
|
-
reject(error2);
|
|
2159
|
-
}
|
|
2160
|
-
};
|
|
2161
|
-
if (options.publicKeyPath !== void 0) {
|
|
2162
|
-
props.publicKeyPath = options.publicKeyPath;
|
|
2163
|
-
}
|
|
2164
|
-
if (options.force !== void 0) {
|
|
2165
|
-
props.force = options.force;
|
|
2166
|
-
}
|
|
2167
|
-
const { unmount } = render(/* @__PURE__ */ jsx(KeygenInteractive, { ...props }));
|
|
2168
|
-
});
|
|
2169
|
-
}
|
|
2170
|
-
|
|
2171
|
-
// src/commands/keygen.ts
|
|
2172
|
-
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(
|
|
2173
|
-
"--provider <type>",
|
|
2174
|
-
"Key provider: filesystem, 1password, or macos-keychain (skips interactive)"
|
|
2175
|
-
).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) => {
|
|
2176
|
-
await runKeygen(options);
|
|
2177
|
-
});
|
|
2178
|
-
async function runKeygen(options) {
|
|
2179
|
-
try {
|
|
2180
|
-
const useInteractive = options.interactive !== false && !options.provider;
|
|
2181
|
-
if (useInteractive) {
|
|
2182
|
-
const interactiveOptions = {};
|
|
2183
|
-
if (options.output !== void 0) {
|
|
2184
|
-
interactiveOptions.publicKeyPath = options.output;
|
|
2185
|
-
}
|
|
2186
|
-
if (options.force !== void 0) {
|
|
2187
|
-
interactiveOptions.force = options.force;
|
|
2188
|
-
}
|
|
2189
|
-
const result = await runKeygenInteractive(interactiveOptions);
|
|
2190
|
-
success("Keypair generated successfully!");
|
|
2191
|
-
log("");
|
|
2192
|
-
log("Private key stored in:");
|
|
2193
|
-
log(` ${result.storageDescription}`);
|
|
2194
|
-
log("");
|
|
2195
|
-
log("Public key (commit to repo):");
|
|
2196
|
-
log(` ${result.publicKeyPath}`);
|
|
2197
|
-
log("");
|
|
2198
|
-
if (result.provider === "1password") {
|
|
2199
|
-
log("Add to your .attest-it/config.yaml:");
|
|
2200
|
-
log("");
|
|
2201
|
-
log("settings:");
|
|
2202
|
-
log(` publicKeyPath: ${result.publicKeyPath}`);
|
|
2203
|
-
log(" keyProvider:");
|
|
2204
|
-
log(" type: 1password");
|
|
2205
|
-
log(" options:");
|
|
2206
|
-
if (result.account) {
|
|
2207
|
-
log(` account: ${result.account}`);
|
|
2208
|
-
}
|
|
2209
|
-
log(` vault: ${result.vault ?? ""}`);
|
|
2210
|
-
log(` itemName: ${result.itemName ?? ""}`);
|
|
2211
|
-
log("");
|
|
2212
|
-
} else if (result.provider === "macos-keychain") {
|
|
2213
|
-
log("Add to your .attest-it/config.yaml:");
|
|
2214
|
-
log("");
|
|
2215
|
-
log("settings:");
|
|
2216
|
-
log(` publicKeyPath: ${result.publicKeyPath}`);
|
|
2217
|
-
log(" keyProvider:");
|
|
2218
|
-
log(" type: macos-keychain");
|
|
2219
|
-
log(" options:");
|
|
2220
|
-
log(` itemName: ${result.itemName ?? ""}`);
|
|
2221
|
-
log("");
|
|
2222
|
-
}
|
|
2223
|
-
log("Next steps:");
|
|
2224
|
-
log(` 1. git add ${result.publicKeyPath}`);
|
|
2225
|
-
if (result.provider === "1password" || result.provider === "macos-keychain") {
|
|
2226
|
-
log(" 2. Update .attest-it/config.yaml with keyProvider settings");
|
|
2227
|
-
} else {
|
|
2228
|
-
log(" 2. Update .attest-it/config.yaml publicKeyPath if needed");
|
|
2229
|
-
}
|
|
2230
|
-
log(" 3. attest-it run --suite <suite-name>");
|
|
2231
|
-
} else {
|
|
2232
|
-
await runNonInteractiveKeygen(options);
|
|
2233
|
-
}
|
|
2234
|
-
} catch (err) {
|
|
2235
|
-
if (err instanceof Error) {
|
|
2236
|
-
error(err.message);
|
|
2237
|
-
} else {
|
|
2238
|
-
error("Unknown error occurred");
|
|
2239
|
-
}
|
|
2240
|
-
process.exit(ExitCode.CONFIG_ERROR);
|
|
2241
|
-
}
|
|
2242
|
-
}
|
|
2243
|
-
async function runNonInteractiveKeygen(options) {
|
|
2244
|
-
log("Checking OpenSSL...");
|
|
2245
|
-
const version = await checkOpenSSL();
|
|
2246
|
-
info(`OpenSSL: ${version}`);
|
|
2247
|
-
const publicPath = options.output ?? getDefaultPublicKeyPath();
|
|
2248
|
-
if (options.provider === "1password") {
|
|
2249
|
-
if (!options.vault || !options.itemName) {
|
|
2250
|
-
throw new Error("--vault and --item-name are required for 1password provider");
|
|
2251
|
-
}
|
|
2252
|
-
const providerOptions = {
|
|
2253
|
-
vault: options.vault,
|
|
2254
|
-
itemName: options.itemName
|
|
2255
|
-
};
|
|
2256
|
-
if (options.account !== void 0) {
|
|
2257
|
-
providerOptions.account = options.account;
|
|
2258
|
-
}
|
|
2259
|
-
const provider = new OnePasswordKeyProvider(providerOptions);
|
|
2260
|
-
log(`Generating keypair with 1Password storage...`);
|
|
2261
|
-
log(`Vault: ${options.vault}`);
|
|
2262
|
-
log(`Item: ${options.itemName}`);
|
|
2263
|
-
const genOptions = { publicKeyPath: publicPath };
|
|
2264
|
-
if (options.force !== void 0) {
|
|
2265
|
-
genOptions.force = options.force;
|
|
2266
|
-
}
|
|
2267
|
-
const result = await provider.generateKeyPair(genOptions);
|
|
2268
|
-
success("Keypair generated successfully!");
|
|
2269
|
-
log("");
|
|
2270
|
-
log("Private key stored in:");
|
|
2271
|
-
log(` ${result.storageDescription}`);
|
|
2272
|
-
log("");
|
|
2273
|
-
log("Public key (commit to repo):");
|
|
2274
|
-
log(` ${result.publicKeyPath}`);
|
|
2275
|
-
} else if (options.provider === "macos-keychain") {
|
|
2276
|
-
if (!options.itemName) {
|
|
2277
|
-
throw new Error("--item-name is required for macos-keychain provider");
|
|
2278
|
-
}
|
|
2279
|
-
const isAvailable = MacOSKeychainKeyProvider.isAvailable();
|
|
2280
|
-
if (!isAvailable) {
|
|
2281
|
-
throw new Error("macOS Keychain is not available on this platform");
|
|
2282
|
-
}
|
|
2283
|
-
const provider = new MacOSKeychainKeyProvider({
|
|
2284
|
-
itemName: options.itemName
|
|
2285
|
-
});
|
|
2286
|
-
log(`Generating keypair with macOS Keychain storage...`);
|
|
2287
|
-
log(`Item: ${options.itemName}`);
|
|
2288
|
-
const genOptions = { publicKeyPath: publicPath };
|
|
2289
|
-
if (options.force !== void 0) {
|
|
2290
|
-
genOptions.force = options.force;
|
|
2291
|
-
}
|
|
2292
|
-
const result = await provider.generateKeyPair(genOptions);
|
|
2293
|
-
success("Keypair generated successfully!");
|
|
2294
|
-
log("");
|
|
2295
|
-
log("Private key stored in:");
|
|
2296
|
-
log(` ${result.storageDescription}`);
|
|
2297
|
-
log("");
|
|
2298
|
-
log("Public key (commit to repo):");
|
|
2299
|
-
log(` ${result.publicKeyPath}`);
|
|
2300
|
-
} else {
|
|
2301
|
-
const privatePath = options.private ?? getDefaultPrivateKeyPath();
|
|
2302
|
-
log(`Private key: ${privatePath}`);
|
|
2303
|
-
log(`Public key: ${publicPath}`);
|
|
2304
|
-
const privateExists = fs.existsSync(privatePath);
|
|
2305
|
-
const publicExists = fs.existsSync(publicPath);
|
|
2306
|
-
if ((privateExists || publicExists) && !options.force) {
|
|
2307
|
-
if (privateExists) {
|
|
2308
|
-
warn(`Private key already exists: ${privatePath}`);
|
|
2309
|
-
}
|
|
2310
|
-
if (publicExists) {
|
|
2311
|
-
warn(`Public key already exists: ${publicPath}`);
|
|
2312
|
-
}
|
|
2313
|
-
const shouldOverwrite = await confirmAction({
|
|
2314
|
-
message: "Overwrite existing keys?",
|
|
2315
|
-
default: false
|
|
2316
|
-
});
|
|
2317
|
-
if (!shouldOverwrite) {
|
|
2318
|
-
error("Keygen cancelled");
|
|
2319
|
-
process.exit(ExitCode.CANCELLED);
|
|
2320
|
-
}
|
|
2321
|
-
}
|
|
2322
|
-
log("\nGenerating RSA-2048 keypair...");
|
|
2323
|
-
const result = await generateKeyPair({
|
|
2324
|
-
privatePath,
|
|
2325
|
-
publicPath,
|
|
2326
|
-
force: true
|
|
2327
|
-
});
|
|
2328
|
-
await setKeyPermissions(result.privatePath);
|
|
2329
|
-
success("Keypair generated successfully!");
|
|
2330
|
-
log("");
|
|
2331
|
-
log("Private key (KEEP SECRET):");
|
|
2332
|
-
log(` ${result.privatePath}`);
|
|
2333
|
-
log("");
|
|
2334
|
-
log("Public key (commit to repo):");
|
|
2335
|
-
log(` ${result.publicPath}`);
|
|
2336
|
-
}
|
|
2337
|
-
log("");
|
|
2338
|
-
info("Important: Back up your private key securely!");
|
|
2339
|
-
log("");
|
|
2340
|
-
log("Next steps:");
|
|
2341
|
-
log(` 1. git add ${publicPath}`);
|
|
2342
|
-
log(" 2. Update .attest-it/config.yaml publicKeyPath if needed");
|
|
2343
|
-
log(" 3. attest-it run --suite <suite-name>");
|
|
2344
|
-
}
|
|
2345
1706
|
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) => {
|
|
2346
1707
|
await runPrune(options);
|
|
2347
1708
|
});
|
|
@@ -2593,7 +1954,7 @@ async function runSeal(gates, options) {
|
|
|
2593
1954
|
const localConfig = loadLocalConfigSync();
|
|
2594
1955
|
if (!localConfig) {
|
|
2595
1956
|
error("No local identity configuration found");
|
|
2596
|
-
error('Run "attest-it
|
|
1957
|
+
error('Run "attest-it identity create" first to set up your identity');
|
|
2597
1958
|
process.exit(ExitCode.CONFIG_ERROR);
|
|
2598
1959
|
}
|
|
2599
1960
|
const identity = getActiveIdentity(localConfig);
|
|
@@ -2690,8 +2051,8 @@ async function processSingleGate(gateId, config, identity, identitySlug, sealsFi
|
|
|
2690
2051
|
const keyProvider = createKeyProviderFromIdentity2(identity);
|
|
2691
2052
|
const keyRef = getKeyRefFromIdentity2(identity);
|
|
2692
2053
|
const keyResult = await keyProvider.getPrivateKey(keyRef);
|
|
2693
|
-
const
|
|
2694
|
-
const privateKeyPem = await
|
|
2054
|
+
const fs3 = await import('fs/promises');
|
|
2055
|
+
const privateKeyPem = await fs3.readFile(keyResult.keyPath, "utf8");
|
|
2695
2056
|
await keyResult.cleanup();
|
|
2696
2057
|
const seal = createSeal({
|
|
2697
2058
|
gateId,
|
|
@@ -2912,10 +2273,19 @@ async function runCreate() {
|
|
|
2912
2273
|
default: ""
|
|
2913
2274
|
});
|
|
2914
2275
|
info("Checking available key storage providers...");
|
|
2276
|
+
info(
|
|
2277
|
+
"You may see authentication prompts from 1Password, macOS Keychain, or other security tools."
|
|
2278
|
+
);
|
|
2915
2279
|
const opAvailable = await OnePasswordKeyProvider.isInstalled();
|
|
2280
|
+
verbose(` 1Password CLI (op): ${opAvailable ? "found" : "not found"}`);
|
|
2916
2281
|
const keychainAvailable = MacOSKeychainKeyProvider.isAvailable();
|
|
2282
|
+
verbose(` macOS Keychain: ${keychainAvailable ? "available" : "not available (not macOS)"}`);
|
|
2917
2283
|
const yubikeyInstalled = await YubiKeyProvider.isInstalled();
|
|
2284
|
+
verbose(` YubiKey CLI (ykman): ${yubikeyInstalled ? "found" : "not found"}`);
|
|
2918
2285
|
const yubikeyConnected = yubikeyInstalled ? await YubiKeyProvider.isConnected() : false;
|
|
2286
|
+
if (yubikeyInstalled) {
|
|
2287
|
+
verbose(` YubiKey device: ${yubikeyConnected ? "connected" : "not connected"}`);
|
|
2288
|
+
}
|
|
2919
2289
|
const configDir = getAttestItConfigDir();
|
|
2920
2290
|
const storageChoices = [
|
|
2921
2291
|
{ name: `File system (${join(configDir, "keys")})`, value: "file" }
|
|
@@ -2929,24 +2299,24 @@ async function runCreate() {
|
|
|
2929
2299
|
if (yubikeyInstalled) {
|
|
2930
2300
|
const yubikeyLabel = yubikeyConnected ? "YubiKey (encrypted with challenge-response)" : "YubiKey (not connected - insert YubiKey first)";
|
|
2931
2301
|
storageChoices.push({ name: yubikeyLabel, value: "yubikey" });
|
|
2302
|
+
} else {
|
|
2303
|
+
storageChoices.push({
|
|
2304
|
+
name: theme3.muted("YubiKey (install ykman CLI to enable)"),
|
|
2305
|
+
value: "yubikey-disabled",
|
|
2306
|
+
// @ts-expect-error -- @inquirer/prompts supports disabled property but types may not reflect it
|
|
2307
|
+
disabled: true
|
|
2308
|
+
});
|
|
2932
2309
|
}
|
|
2933
2310
|
const keyStorageType = await select({
|
|
2934
2311
|
message: "Where should the private key be stored?",
|
|
2935
2312
|
choices: storageChoices
|
|
2936
2313
|
});
|
|
2937
|
-
|
|
2938
|
-
log("Generating Ed25519 keypair...");
|
|
2939
|
-
const keyPair = generateEd25519KeyPair();
|
|
2940
|
-
let privateKeyRef;
|
|
2941
|
-
let keyStorageDescription;
|
|
2314
|
+
let storageConfig;
|
|
2942
2315
|
switch (keyStorageType) {
|
|
2943
2316
|
case "file": {
|
|
2944
2317
|
const keysDir = join(getAttestItConfigDir(), "keys");
|
|
2945
|
-
await mkdir(keysDir, { recursive: true });
|
|
2946
2318
|
const keyPath = join(keysDir, `${slug}.pem`);
|
|
2947
|
-
|
|
2948
|
-
privateKeyRef = { type: "file", path: keyPath };
|
|
2949
|
-
keyStorageDescription = keyPath;
|
|
2319
|
+
storageConfig = { type: "file", keyPath };
|
|
2950
2320
|
break;
|
|
2951
2321
|
}
|
|
2952
2322
|
case "keychain": {
|
|
@@ -2954,6 +2324,9 @@ async function runCreate() {
|
|
|
2954
2324
|
error("macOS Keychain is not available on this system");
|
|
2955
2325
|
process.exit(ExitCode.CONFIG_ERROR);
|
|
2956
2326
|
}
|
|
2327
|
+
log("");
|
|
2328
|
+
info("Accessing macOS Keychain to list available keychains...");
|
|
2329
|
+
info("You may be prompted to allow access or enter your password.");
|
|
2957
2330
|
const keychains = await MacOSKeychainKeyProvider.listKeychains();
|
|
2958
2331
|
if (keychains.length === 0) {
|
|
2959
2332
|
throw new Error("No keychains found on this system");
|
|
@@ -2989,38 +2362,15 @@ async function runCreate() {
|
|
|
2989
2362
|
return true;
|
|
2990
2363
|
}
|
|
2991
2364
|
});
|
|
2992
|
-
|
|
2993
|
-
const { promisify } = await import('util');
|
|
2994
|
-
const execFileAsync = promisify(execFile);
|
|
2995
|
-
const encodedKey = Buffer.from(keyPair.privateKey).toString("base64");
|
|
2996
|
-
try {
|
|
2997
|
-
const addArgs = [
|
|
2998
|
-
"add-generic-password",
|
|
2999
|
-
"-a",
|
|
3000
|
-
"attest-it",
|
|
3001
|
-
"-s",
|
|
3002
|
-
keychainItemName,
|
|
3003
|
-
"-w",
|
|
3004
|
-
encodedKey,
|
|
3005
|
-
"-U",
|
|
3006
|
-
selectedKeychain.path
|
|
3007
|
-
];
|
|
3008
|
-
await execFileAsync("security", addArgs);
|
|
3009
|
-
} catch (err) {
|
|
3010
|
-
throw new Error(
|
|
3011
|
-
`Failed to store key in macOS Keychain: ${err instanceof Error ? err.message : String(err)}`
|
|
3012
|
-
);
|
|
3013
|
-
}
|
|
3014
|
-
privateKeyRef = {
|
|
3015
|
-
type: "keychain",
|
|
3016
|
-
service: keychainItemName,
|
|
3017
|
-
account: "attest-it",
|
|
3018
|
-
keychain: selectedKeychain.path
|
|
3019
|
-
};
|
|
3020
|
-
keyStorageDescription = `macOS Keychain: ${selectedKeychain.name}/${keychainItemName}`;
|
|
2365
|
+
storageConfig = { type: "keychain", selectedKeychain, keychainItemName };
|
|
3021
2366
|
break;
|
|
3022
2367
|
}
|
|
3023
2368
|
case "1password": {
|
|
2369
|
+
log("");
|
|
2370
|
+
info("Accessing 1Password to list your accounts and vaults...");
|
|
2371
|
+
info(
|
|
2372
|
+
"You may see biometric prompts or be asked to unlock 1Password for each configured account."
|
|
2373
|
+
);
|
|
3024
2374
|
const accounts = await OnePasswordKeyProvider.listAccounts();
|
|
3025
2375
|
if (accounts.length === 0) {
|
|
3026
2376
|
throw new Error(
|
|
@@ -3041,7 +2391,7 @@ async function runCreate() {
|
|
|
3041
2391
|
"--format=json"
|
|
3042
2392
|
]);
|
|
3043
2393
|
const details = JSON.parse(stdout);
|
|
3044
|
-
const name2 = details !== null && typeof details === "object" && "name" in details && typeof details.name === "string" ? details.name :
|
|
2394
|
+
const name2 = details !== null && typeof details === "object" && "name" in details && typeof details.name === "string" ? details.name : "[Could not read account name]";
|
|
3045
2395
|
return {
|
|
3046
2396
|
url: acc.url,
|
|
3047
2397
|
email: acc.email,
|
|
@@ -3051,7 +2401,7 @@ async function runCreate() {
|
|
|
3051
2401
|
return {
|
|
3052
2402
|
url: acc.url,
|
|
3053
2403
|
email: acc.email,
|
|
3054
|
-
name:
|
|
2404
|
+
name: "[Could not read account name]"
|
|
3055
2405
|
};
|
|
3056
2406
|
}
|
|
3057
2407
|
})
|
|
@@ -3093,43 +2443,21 @@ async function runCreate() {
|
|
|
3093
2443
|
return true;
|
|
3094
2444
|
}
|
|
3095
2445
|
});
|
|
3096
|
-
const
|
|
3097
|
-
const
|
|
3098
|
-
|
|
3099
|
-
const tempPrivatePath = join(tempDir, "private.pem");
|
|
3100
|
-
try {
|
|
3101
|
-
await writeFile(tempPrivatePath, keyPair.privateKey, { mode: 384 });
|
|
3102
|
-
const { execFile: execFile2 } = await import('child_process');
|
|
3103
|
-
const { promisify: promisify2 } = await import('util');
|
|
3104
|
-
const execFileAsync2 = promisify2(execFile2);
|
|
3105
|
-
const opArgs = [
|
|
3106
|
-
"document",
|
|
3107
|
-
"create",
|
|
3108
|
-
tempPrivatePath,
|
|
3109
|
-
"--title",
|
|
3110
|
-
item,
|
|
3111
|
-
"--vault",
|
|
3112
|
-
selectedVault
|
|
3113
|
-
];
|
|
3114
|
-
if (selectedAccount) {
|
|
3115
|
-
opArgs.push("--account", selectedAccount);
|
|
3116
|
-
}
|
|
3117
|
-
await execFileAsync2("op", opArgs);
|
|
3118
|
-
} finally {
|
|
3119
|
-
const { rm } = await import('fs/promises');
|
|
3120
|
-
await rm(tempDir, { recursive: true, force: true }).catch(() => {
|
|
3121
|
-
});
|
|
3122
|
-
}
|
|
3123
|
-
privateKeyRef = {
|
|
2446
|
+
const selectedAccountDetails = accountDetails.find((acc) => acc.url === selectedAccount);
|
|
2447
|
+
const accountDisplayName = selectedAccountDetails?.name ?? selectedAccount;
|
|
2448
|
+
storageConfig = {
|
|
3124
2449
|
type: "1password",
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
2450
|
+
selectedAccount,
|
|
2451
|
+
accountDisplayName,
|
|
2452
|
+
selectedVault,
|
|
2453
|
+
item
|
|
3128
2454
|
};
|
|
3129
|
-
keyStorageDescription = `1Password (${selectedVault}/${item})`;
|
|
3130
2455
|
break;
|
|
3131
2456
|
}
|
|
3132
2457
|
case "yubikey": {
|
|
2458
|
+
log("");
|
|
2459
|
+
info("Accessing YubiKey to detect connected devices...");
|
|
2460
|
+
info("Your private key will be encrypted using HMAC challenge-response from the YubiKey.");
|
|
3133
2461
|
if (!await YubiKeyProvider.isConnected()) {
|
|
3134
2462
|
error("No YubiKey detected. Please insert your YubiKey and try again.");
|
|
3135
2463
|
process.exit(ExitCode.CONFIG_ERROR);
|
|
@@ -3181,25 +2509,115 @@ async function runCreate() {
|
|
|
3181
2509
|
}
|
|
3182
2510
|
});
|
|
3183
2511
|
const keysDir = join(getAttestItConfigDir(), "keys");
|
|
3184
|
-
await mkdir(keysDir, { recursive: true });
|
|
3185
2512
|
const encryptedKeyPath = join(keysDir, encryptedKeyName);
|
|
2513
|
+
storageConfig = { type: "yubikey", selectedSerial, slot, encryptedKeyPath };
|
|
2514
|
+
break;
|
|
2515
|
+
}
|
|
2516
|
+
default:
|
|
2517
|
+
throw new Error(`Unknown key storage type: ${keyStorageType}`);
|
|
2518
|
+
}
|
|
2519
|
+
log("");
|
|
2520
|
+
log("Generating Ed25519 keypair...");
|
|
2521
|
+
const keyPair = generateEd25519KeyPair();
|
|
2522
|
+
let privateKeyRef;
|
|
2523
|
+
let keyStorageDescription;
|
|
2524
|
+
switch (storageConfig.type) {
|
|
2525
|
+
case "file": {
|
|
2526
|
+
log("");
|
|
2527
|
+
info("Creating private key file on disk...");
|
|
2528
|
+
const keysDir = join(getAttestItConfigDir(), "keys");
|
|
2529
|
+
await mkdir(keysDir, { recursive: true });
|
|
2530
|
+
await writeFile(storageConfig.keyPath, keyPair.privateKey, { mode: 384 });
|
|
2531
|
+
privateKeyRef = { type: "file", path: storageConfig.keyPath };
|
|
2532
|
+
keyStorageDescription = storageConfig.keyPath;
|
|
2533
|
+
break;
|
|
2534
|
+
}
|
|
2535
|
+
case "keychain": {
|
|
2536
|
+
const { execFile } = await import('child_process');
|
|
2537
|
+
const { promisify } = await import('util');
|
|
2538
|
+
const execFileAsync = promisify(execFile);
|
|
2539
|
+
const encodedKey = Buffer.from(keyPair.privateKey).toString("base64");
|
|
2540
|
+
try {
|
|
2541
|
+
const addArgs = [
|
|
2542
|
+
"add-generic-password",
|
|
2543
|
+
"-a",
|
|
2544
|
+
"attest-it",
|
|
2545
|
+
"-s",
|
|
2546
|
+
storageConfig.keychainItemName,
|
|
2547
|
+
"-w",
|
|
2548
|
+
encodedKey,
|
|
2549
|
+
"-U",
|
|
2550
|
+
storageConfig.selectedKeychain.path
|
|
2551
|
+
];
|
|
2552
|
+
await execFileAsync("security", addArgs);
|
|
2553
|
+
} catch (err) {
|
|
2554
|
+
throw new Error(
|
|
2555
|
+
`Failed to store key in macOS Keychain: ${err instanceof Error ? err.message : String(err)}`
|
|
2556
|
+
);
|
|
2557
|
+
}
|
|
2558
|
+
privateKeyRef = {
|
|
2559
|
+
type: "keychain",
|
|
2560
|
+
service: storageConfig.keychainItemName,
|
|
2561
|
+
account: "attest-it",
|
|
2562
|
+
keychain: storageConfig.selectedKeychain.path
|
|
2563
|
+
};
|
|
2564
|
+
keyStorageDescription = `macOS Keychain: ${storageConfig.selectedKeychain.name}/${storageConfig.keychainItemName}`;
|
|
2565
|
+
break;
|
|
2566
|
+
}
|
|
2567
|
+
case "1password": {
|
|
2568
|
+
const { tmpdir } = await import('os');
|
|
2569
|
+
const tempDir = join(tmpdir(), `attest-it-${String(Date.now())}`);
|
|
2570
|
+
await mkdir(tempDir, { recursive: true });
|
|
2571
|
+
const tempPrivatePath = join(tempDir, "private.pem");
|
|
2572
|
+
try {
|
|
2573
|
+
await writeFile(tempPrivatePath, keyPair.privateKey, { mode: 384 });
|
|
2574
|
+
const { execFile } = await import('child_process');
|
|
2575
|
+
const { promisify } = await import('util');
|
|
2576
|
+
const execFileAsync = promisify(execFile);
|
|
2577
|
+
const opArgs = [
|
|
2578
|
+
"document",
|
|
2579
|
+
"create",
|
|
2580
|
+
tempPrivatePath,
|
|
2581
|
+
"--title",
|
|
2582
|
+
storageConfig.item,
|
|
2583
|
+
"--vault",
|
|
2584
|
+
storageConfig.selectedVault,
|
|
2585
|
+
"--account",
|
|
2586
|
+
storageConfig.selectedAccount
|
|
2587
|
+
];
|
|
2588
|
+
await execFileAsync("op", opArgs);
|
|
2589
|
+
} finally {
|
|
2590
|
+
const { rm } = await import('fs/promises');
|
|
2591
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => {
|
|
2592
|
+
});
|
|
2593
|
+
}
|
|
2594
|
+
privateKeyRef = {
|
|
2595
|
+
type: "1password",
|
|
2596
|
+
vault: storageConfig.selectedVault,
|
|
2597
|
+
item: storageConfig.item,
|
|
2598
|
+
account: storageConfig.selectedAccount
|
|
2599
|
+
};
|
|
2600
|
+
keyStorageDescription = `1Password (${storageConfig.accountDisplayName}/${storageConfig.selectedVault}/${storageConfig.item})`;
|
|
2601
|
+
break;
|
|
2602
|
+
}
|
|
2603
|
+
case "yubikey": {
|
|
2604
|
+
const keysDir = join(getAttestItConfigDir(), "keys");
|
|
2605
|
+
await mkdir(keysDir, { recursive: true });
|
|
3186
2606
|
const result = await YubiKeyProvider.encryptPrivateKey({
|
|
3187
2607
|
privateKey: keyPair.privateKey,
|
|
3188
|
-
encryptedKeyPath,
|
|
3189
|
-
slot,
|
|
3190
|
-
serial: selectedSerial
|
|
2608
|
+
encryptedKeyPath: storageConfig.encryptedKeyPath,
|
|
2609
|
+
slot: storageConfig.slot,
|
|
2610
|
+
serial: storageConfig.selectedSerial
|
|
3191
2611
|
});
|
|
3192
2612
|
privateKeyRef = {
|
|
3193
2613
|
type: "yubikey",
|
|
3194
2614
|
encryptedKeyPath: result.encryptedKeyPath,
|
|
3195
|
-
slot,
|
|
3196
|
-
serial: selectedSerial
|
|
2615
|
+
slot: storageConfig.slot,
|
|
2616
|
+
serial: storageConfig.selectedSerial
|
|
3197
2617
|
};
|
|
3198
2618
|
keyStorageDescription = result.storageDescription;
|
|
3199
2619
|
break;
|
|
3200
2620
|
}
|
|
3201
|
-
default:
|
|
3202
|
-
throw new Error(`Unknown key storage type: ${keyStorageType}`);
|
|
3203
2621
|
}
|
|
3204
2622
|
const identity = {
|
|
3205
2623
|
name,
|
|
@@ -3243,9 +2661,9 @@ async function runCreate() {
|
|
|
3243
2661
|
log("");
|
|
3244
2662
|
log(theme3.blue.bold()("Public key saved to:"));
|
|
3245
2663
|
log(` ${publicKeyResult.homePath}`);
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
2664
|
+
log("");
|
|
2665
|
+
log("To add yourself to a project, run:");
|
|
2666
|
+
log(theme3.blue(" attest-it team join"));
|
|
3249
2667
|
log("");
|
|
3250
2668
|
if (!existingConfig) {
|
|
3251
2669
|
success(`Set as active identity`);
|
|
@@ -3366,155 +2784,6 @@ async function runShow(slug) {
|
|
|
3366
2784
|
process.exit(ExitCode.CONFIG_ERROR);
|
|
3367
2785
|
}
|
|
3368
2786
|
}
|
|
3369
|
-
var editCommand = new Command("edit").description("Edit identity or rotate keypair").argument("<slug>", "Identity slug to edit").action(async (slug) => {
|
|
3370
|
-
await runEdit(slug);
|
|
3371
|
-
});
|
|
3372
|
-
async function runEdit(slug) {
|
|
3373
|
-
try {
|
|
3374
|
-
const config = await loadLocalConfig();
|
|
3375
|
-
if (!config) {
|
|
3376
|
-
error("No identities configured");
|
|
3377
|
-
process.exit(ExitCode.CONFIG_ERROR);
|
|
3378
|
-
}
|
|
3379
|
-
const identity = config.identities[slug];
|
|
3380
|
-
if (!identity) {
|
|
3381
|
-
error(`Identity "${slug}" not found`);
|
|
3382
|
-
process.exit(ExitCode.CONFIG_ERROR);
|
|
3383
|
-
}
|
|
3384
|
-
const theme3 = getTheme2();
|
|
3385
|
-
log("");
|
|
3386
|
-
log(theme3.blue.bold()(`Edit Identity: ${slug}`));
|
|
3387
|
-
log("");
|
|
3388
|
-
const name = await input({
|
|
3389
|
-
message: "Display name:",
|
|
3390
|
-
default: identity.name,
|
|
3391
|
-
validate: (value) => {
|
|
3392
|
-
if (!value || value.trim().length === 0) {
|
|
3393
|
-
return "Name cannot be empty";
|
|
3394
|
-
}
|
|
3395
|
-
return true;
|
|
3396
|
-
}
|
|
3397
|
-
});
|
|
3398
|
-
const email = await input({
|
|
3399
|
-
message: "Email (optional):",
|
|
3400
|
-
default: identity.email ?? ""
|
|
3401
|
-
});
|
|
3402
|
-
const github = await input({
|
|
3403
|
-
message: "GitHub username (optional):",
|
|
3404
|
-
default: identity.github ?? ""
|
|
3405
|
-
});
|
|
3406
|
-
const rotateKey = await confirm({
|
|
3407
|
-
message: "Rotate keypair (generate new keys)?",
|
|
3408
|
-
default: false
|
|
3409
|
-
});
|
|
3410
|
-
let publicKey = identity.publicKey;
|
|
3411
|
-
const privateKeyRef = identity.privateKey;
|
|
3412
|
-
if (rotateKey) {
|
|
3413
|
-
log("");
|
|
3414
|
-
log("Generating new Ed25519 keypair...");
|
|
3415
|
-
const keyPair = generateEd25519KeyPair();
|
|
3416
|
-
publicKey = keyPair.publicKey;
|
|
3417
|
-
switch (identity.privateKey.type) {
|
|
3418
|
-
case "file": {
|
|
3419
|
-
await writeFile(identity.privateKey.path, keyPair.privateKey, { mode: 384 });
|
|
3420
|
-
log(` Updated private key at: ${identity.privateKey.path}`);
|
|
3421
|
-
break;
|
|
3422
|
-
}
|
|
3423
|
-
case "keychain": {
|
|
3424
|
-
const { execFile } = await import('child_process');
|
|
3425
|
-
const { promisify } = await import('util');
|
|
3426
|
-
const execFileAsync = promisify(execFile);
|
|
3427
|
-
const encodedKey = Buffer.from(keyPair.privateKey).toString("base64");
|
|
3428
|
-
try {
|
|
3429
|
-
await execFileAsync("security", [
|
|
3430
|
-
"delete-generic-password",
|
|
3431
|
-
"-s",
|
|
3432
|
-
identity.privateKey.service,
|
|
3433
|
-
"-a",
|
|
3434
|
-
identity.privateKey.account
|
|
3435
|
-
]);
|
|
3436
|
-
await execFileAsync("security", [
|
|
3437
|
-
"add-generic-password",
|
|
3438
|
-
"-s",
|
|
3439
|
-
identity.privateKey.service,
|
|
3440
|
-
"-a",
|
|
3441
|
-
identity.privateKey.account,
|
|
3442
|
-
"-w",
|
|
3443
|
-
encodedKey,
|
|
3444
|
-
"-U"
|
|
3445
|
-
]);
|
|
3446
|
-
log(` Updated private key in macOS Keychain`);
|
|
3447
|
-
} catch (err) {
|
|
3448
|
-
throw new Error(
|
|
3449
|
-
`Failed to update key in macOS Keychain: ${err instanceof Error ? err.message : String(err)}`
|
|
3450
|
-
);
|
|
3451
|
-
}
|
|
3452
|
-
break;
|
|
3453
|
-
}
|
|
3454
|
-
case "1password": {
|
|
3455
|
-
const { execFile } = await import('child_process');
|
|
3456
|
-
const { promisify } = await import('util');
|
|
3457
|
-
const execFileAsync = promisify(execFile);
|
|
3458
|
-
try {
|
|
3459
|
-
const opArgs = [
|
|
3460
|
-
"item",
|
|
3461
|
-
"edit",
|
|
3462
|
-
identity.privateKey.item,
|
|
3463
|
-
"--vault",
|
|
3464
|
-
identity.privateKey.vault,
|
|
3465
|
-
`privateKey[password]=${keyPair.privateKey}`
|
|
3466
|
-
];
|
|
3467
|
-
if (identity.privateKey.account) {
|
|
3468
|
-
opArgs.push("--account", identity.privateKey.account);
|
|
3469
|
-
}
|
|
3470
|
-
await execFileAsync("op", opArgs);
|
|
3471
|
-
log(` Updated private key in 1Password`);
|
|
3472
|
-
} catch (err) {
|
|
3473
|
-
throw new Error(
|
|
3474
|
-
`Failed to update key in 1Password: ${err instanceof Error ? err.message : String(err)}`
|
|
3475
|
-
);
|
|
3476
|
-
}
|
|
3477
|
-
break;
|
|
3478
|
-
}
|
|
3479
|
-
}
|
|
3480
|
-
}
|
|
3481
|
-
const updatedIdentity = {
|
|
3482
|
-
name,
|
|
3483
|
-
publicKey,
|
|
3484
|
-
privateKey: privateKeyRef,
|
|
3485
|
-
...email && { email },
|
|
3486
|
-
...github && { github }
|
|
3487
|
-
};
|
|
3488
|
-
const newConfig = {
|
|
3489
|
-
...config,
|
|
3490
|
-
identities: {
|
|
3491
|
-
...config.identities,
|
|
3492
|
-
[slug]: updatedIdentity
|
|
3493
|
-
}
|
|
3494
|
-
};
|
|
3495
|
-
await saveLocalConfig(newConfig);
|
|
3496
|
-
log("");
|
|
3497
|
-
success("Identity updated successfully");
|
|
3498
|
-
log("");
|
|
3499
|
-
if (rotateKey) {
|
|
3500
|
-
log(" New Public Key: " + publicKey.slice(0, 32) + "...");
|
|
3501
|
-
log("");
|
|
3502
|
-
log(
|
|
3503
|
-
theme3.yellow(
|
|
3504
|
-
" Warning: If this identity is used in team configurations,\n you must update those configurations with the new public key."
|
|
3505
|
-
)
|
|
3506
|
-
);
|
|
3507
|
-
log("");
|
|
3508
|
-
}
|
|
3509
|
-
} catch (err) {
|
|
3510
|
-
if (err instanceof Error) {
|
|
3511
|
-
error(err.message);
|
|
3512
|
-
} else {
|
|
3513
|
-
error("Unknown error occurred");
|
|
3514
|
-
}
|
|
3515
|
-
process.exit(ExitCode.CONFIG_ERROR);
|
|
3516
|
-
}
|
|
3517
|
-
}
|
|
3518
2787
|
|
|
3519
2788
|
// src/utils/format-key-location.ts
|
|
3520
2789
|
function formatKeyLocation(privateKey) {
|
|
@@ -3733,7 +3002,7 @@ async function runExport(slug) {
|
|
|
3733
3002
|
}
|
|
3734
3003
|
|
|
3735
3004
|
// src/commands/identity/index.ts
|
|
3736
|
-
var identityCommand = new Command("identity").description("Manage local identities and keypairs").addCommand(listCommand).addCommand(createCommand).addCommand(useCommand).addCommand(showCommand).addCommand(
|
|
3005
|
+
var identityCommand = new Command("identity").description("Manage local identities and keypairs").addCommand(listCommand).addCommand(createCommand).addCommand(useCommand).addCommand(showCommand).addCommand(removeCommand).addCommand(exportCommand);
|
|
3737
3006
|
var whoamiCommand = new Command("whoami").description("Show the current active identity").action(async () => {
|
|
3738
3007
|
await runWhoami();
|
|
3739
3008
|
});
|
|
@@ -3834,6 +3103,49 @@ async function runList2() {
|
|
|
3834
3103
|
process.exit(ExitCode.CONFIG_ERROR);
|
|
3835
3104
|
}
|
|
3836
3105
|
}
|
|
3106
|
+
async function promptForGateAuthorization(gates) {
|
|
3107
|
+
if (!gates || Object.keys(gates).length === 0) {
|
|
3108
|
+
return [];
|
|
3109
|
+
}
|
|
3110
|
+
const gateChoices = Object.entries(gates).map(([gateId, gate]) => ({
|
|
3111
|
+
name: `${gateId} - ${gate.name}`,
|
|
3112
|
+
value: gateId
|
|
3113
|
+
}));
|
|
3114
|
+
const authorizedGates = await checkbox({
|
|
3115
|
+
message: "Select gates to authorize (use space to select):",
|
|
3116
|
+
choices: gateChoices
|
|
3117
|
+
});
|
|
3118
|
+
return authorizedGates;
|
|
3119
|
+
}
|
|
3120
|
+
function addTeamMemberToConfig(config, memberSlug, memberData, authorizedGates) {
|
|
3121
|
+
const existingTeam = config.team ?? {};
|
|
3122
|
+
const updatedConfig = {
|
|
3123
|
+
...config,
|
|
3124
|
+
team: {
|
|
3125
|
+
...existingTeam,
|
|
3126
|
+
[memberSlug]: {
|
|
3127
|
+
name: memberData.name,
|
|
3128
|
+
publicKey: memberData.publicKey,
|
|
3129
|
+
publicKeyAlgorithm: memberData.publicKeyAlgorithm ?? "ed25519",
|
|
3130
|
+
...memberData.email ? { email: memberData.email } : {},
|
|
3131
|
+
...memberData.github ? { github: memberData.github } : {}
|
|
3132
|
+
}
|
|
3133
|
+
}
|
|
3134
|
+
};
|
|
3135
|
+
if (authorizedGates.length > 0 && updatedConfig.gates) {
|
|
3136
|
+
for (const gateId of authorizedGates) {
|
|
3137
|
+
const gate = updatedConfig.gates[gateId];
|
|
3138
|
+
if (gate) {
|
|
3139
|
+
if (!gate.authorizedSigners.includes(memberSlug)) {
|
|
3140
|
+
gate.authorizedSigners.push(memberSlug);
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
return updatedConfig;
|
|
3146
|
+
}
|
|
3147
|
+
|
|
3148
|
+
// src/commands/team/add.ts
|
|
3837
3149
|
var addCommand = new Command("add").description("Add a new team member").action(async () => {
|
|
3838
3150
|
await runAdd();
|
|
3839
3151
|
});
|
|
@@ -3905,45 +3217,22 @@ async function runAdd() {
|
|
|
3905
3217
|
message: "Public key:",
|
|
3906
3218
|
validate: validatePublicKey
|
|
3907
3219
|
});
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
const gateChoices = Object.entries(attestItConfig.gates).map(([gateId, gate]) => ({
|
|
3912
|
-
name: `${gateId} - ${gate.name}`,
|
|
3913
|
-
value: gateId
|
|
3914
|
-
}));
|
|
3915
|
-
authorizedGates = await checkbox({
|
|
3916
|
-
message: "Select gates to authorize (use space to select):",
|
|
3917
|
-
choices: gateChoices
|
|
3918
|
-
});
|
|
3919
|
-
}
|
|
3920
|
-
const teamMember = {
|
|
3220
|
+
log("");
|
|
3221
|
+
const authorizedGates = await promptForGateAuthorization(attestItConfig.gates);
|
|
3222
|
+
const memberData = {
|
|
3921
3223
|
name,
|
|
3922
|
-
publicKey: publicKey.trim()
|
|
3224
|
+
publicKey: publicKey.trim(),
|
|
3225
|
+
publicKeyAlgorithm: "ed25519"
|
|
3923
3226
|
};
|
|
3924
|
-
|
|
3925
|
-
|
|
3227
|
+
const trimmedEmail = email.trim();
|
|
3228
|
+
const trimmedGithub = github.trim();
|
|
3229
|
+
if (trimmedEmail && trimmedEmail.length > 0) {
|
|
3230
|
+
memberData.email = trimmedEmail;
|
|
3926
3231
|
}
|
|
3927
|
-
if (
|
|
3928
|
-
|
|
3929
|
-
}
|
|
3930
|
-
const updatedConfig = {
|
|
3931
|
-
...config,
|
|
3932
|
-
team: {
|
|
3933
|
-
...existingTeam,
|
|
3934
|
-
[slug]: teamMember
|
|
3935
|
-
}
|
|
3936
|
-
};
|
|
3937
|
-
if (authorizedGates.length > 0 && updatedConfig.gates) {
|
|
3938
|
-
for (const gateId of authorizedGates) {
|
|
3939
|
-
const gate = updatedConfig.gates[gateId];
|
|
3940
|
-
if (gate) {
|
|
3941
|
-
if (!gate.authorizedSigners.includes(slug)) {
|
|
3942
|
-
gate.authorizedSigners.push(slug);
|
|
3943
|
-
}
|
|
3944
|
-
}
|
|
3945
|
-
}
|
|
3232
|
+
if (trimmedGithub && trimmedGithub.length > 0) {
|
|
3233
|
+
memberData.github = trimmedGithub;
|
|
3946
3234
|
}
|
|
3235
|
+
const updatedConfig = addTeamMemberToConfig(config, slug, memberData, authorizedGates);
|
|
3947
3236
|
const configPath = findConfigPath();
|
|
3948
3237
|
if (!configPath) {
|
|
3949
3238
|
error("Configuration file not found");
|
|
@@ -3966,127 +3255,73 @@ async function runAdd() {
|
|
|
3966
3255
|
process.exit(ExitCode.CONFIG_ERROR);
|
|
3967
3256
|
}
|
|
3968
3257
|
}
|
|
3969
|
-
var
|
|
3970
|
-
await
|
|
3258
|
+
var joinCommand = new Command("join").description("Add yourself to the project team using your active identity").action(async () => {
|
|
3259
|
+
await runJoin();
|
|
3971
3260
|
});
|
|
3972
|
-
function
|
|
3973
|
-
if (!value || value.trim().length === 0) {
|
|
3974
|
-
return "Public key cannot be empty";
|
|
3975
|
-
}
|
|
3976
|
-
const base64Regex = /^[A-Za-z0-9+/]+=*$/;
|
|
3977
|
-
if (!base64Regex.test(value)) {
|
|
3978
|
-
return "Public key must be valid Base64";
|
|
3979
|
-
}
|
|
3980
|
-
if (value.length !== 44) {
|
|
3981
|
-
return "Public key must be 44 characters (32 bytes in Base64)";
|
|
3982
|
-
}
|
|
3983
|
-
try {
|
|
3984
|
-
const decoded = Buffer.from(value, "base64");
|
|
3985
|
-
if (decoded.length !== 32) {
|
|
3986
|
-
return "Public key must decode to 32 bytes";
|
|
3987
|
-
}
|
|
3988
|
-
} catch {
|
|
3989
|
-
return "Invalid Base64 encoding";
|
|
3990
|
-
}
|
|
3991
|
-
return true;
|
|
3992
|
-
}
|
|
3993
|
-
async function runEdit2(slug) {
|
|
3261
|
+
async function runJoin() {
|
|
3994
3262
|
try {
|
|
3995
3263
|
const theme3 = getTheme2();
|
|
3996
|
-
const config = await loadConfig();
|
|
3997
|
-
const attestItConfig = toAttestItConfig(config);
|
|
3998
|
-
const existingMember = attestItConfig.team?.[slug];
|
|
3999
|
-
if (!existingMember) {
|
|
4000
|
-
error(`Team member "${slug}" not found`);
|
|
4001
|
-
process.exit(ExitCode.CONFIG_ERROR);
|
|
4002
|
-
}
|
|
4003
3264
|
log("");
|
|
4004
|
-
log(theme3.blue.bold()(
|
|
3265
|
+
log(theme3.blue.bold()("Join Project Team"));
|
|
4005
3266
|
log("");
|
|
4006
|
-
|
|
3267
|
+
const localConfig = await loadLocalConfig();
|
|
3268
|
+
if (!localConfig) {
|
|
3269
|
+
error('No identity found. Run "attest-it identity create" first.');
|
|
3270
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
3271
|
+
}
|
|
3272
|
+
const activeIdentity = getActiveIdentity(localConfig);
|
|
3273
|
+
if (!activeIdentity) {
|
|
3274
|
+
error('No active identity. Run "attest-it identity use <slug>" to select one.');
|
|
3275
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
3276
|
+
}
|
|
3277
|
+
const activeSlug = localConfig.activeIdentity;
|
|
3278
|
+
info(`Using identity: ${activeSlug}`);
|
|
3279
|
+
log(` Name: ${activeIdentity.name}`);
|
|
3280
|
+
log(` Public Key: ${activeIdentity.publicKey.slice(0, 32)}...`);
|
|
4007
3281
|
log("");
|
|
4008
|
-
const
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
});
|
|
4018
|
-
const email = await input({
|
|
4019
|
-
message: "Email (optional):",
|
|
4020
|
-
default: existingMember.email ?? ""
|
|
4021
|
-
});
|
|
4022
|
-
const github = await input({
|
|
4023
|
-
message: "GitHub username (optional):",
|
|
4024
|
-
default: existingMember.github ?? ""
|
|
4025
|
-
});
|
|
4026
|
-
const updateKey = await confirm({
|
|
4027
|
-
message: "Update public key?",
|
|
4028
|
-
default: false
|
|
4029
|
-
});
|
|
4030
|
-
let publicKey = existingMember.publicKey;
|
|
4031
|
-
if (updateKey) {
|
|
4032
|
-
log("");
|
|
4033
|
-
log("Paste the new public key:");
|
|
4034
|
-
publicKey = await input({
|
|
4035
|
-
message: "Public key:",
|
|
4036
|
-
default: existingMember.publicKey,
|
|
4037
|
-
validate: validatePublicKey2
|
|
4038
|
-
});
|
|
3282
|
+
const config = await loadConfig();
|
|
3283
|
+
const attestItConfig = toAttestItConfig(config);
|
|
3284
|
+
const existingTeam = attestItConfig.team ?? {};
|
|
3285
|
+
const existingMemberWithKey = Object.entries(existingTeam).find(
|
|
3286
|
+
([, member]) => member.publicKey === activeIdentity.publicKey
|
|
3287
|
+
);
|
|
3288
|
+
if (existingMemberWithKey) {
|
|
3289
|
+
error(`You're already a team member as "${existingMemberWithKey[0]}"`);
|
|
3290
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
4039
3291
|
}
|
|
4040
|
-
|
|
4041
|
-
if (
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
3292
|
+
let slug = activeSlug;
|
|
3293
|
+
if (existingTeam[slug]) {
|
|
3294
|
+
log(`Slug "${slug}" is already taken by another team member.`);
|
|
3295
|
+
slug = await input({
|
|
3296
|
+
message: "Choose a different slug:",
|
|
3297
|
+
validate: (value) => {
|
|
3298
|
+
if (!value || value.trim().length === 0) {
|
|
3299
|
+
return "Slug cannot be empty";
|
|
3300
|
+
}
|
|
3301
|
+
if (!/^[a-z0-9-]+$/.test(value)) {
|
|
3302
|
+
return "Slug must contain only lowercase letters, numbers, and hyphens";
|
|
3303
|
+
}
|
|
3304
|
+
if (existingTeam[value]) {
|
|
3305
|
+
return `Slug "${value}" is already taken`;
|
|
3306
|
+
}
|
|
3307
|
+
return true;
|
|
4045
3308
|
}
|
|
4046
|
-
}
|
|
4047
|
-
}
|
|
4048
|
-
let selectedGates = currentGates;
|
|
4049
|
-
if (attestItConfig.gates && Object.keys(attestItConfig.gates).length > 0) {
|
|
4050
|
-
log("");
|
|
4051
|
-
const gateChoices = Object.entries(attestItConfig.gates).map(([gateId, gate]) => ({
|
|
4052
|
-
name: `${gateId} - ${gate.name}`,
|
|
4053
|
-
value: gateId,
|
|
4054
|
-
checked: currentGates.includes(gateId)
|
|
4055
|
-
}));
|
|
4056
|
-
selectedGates = await checkbox({
|
|
4057
|
-
message: "Select gates to authorize (use space to select):",
|
|
4058
|
-
choices: gateChoices
|
|
4059
3309
|
});
|
|
4060
3310
|
}
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
3311
|
+
log("");
|
|
3312
|
+
const authorizedGates = await promptForGateAuthorization(attestItConfig.gates);
|
|
3313
|
+
const memberData = {
|
|
3314
|
+
name: activeIdentity.name,
|
|
3315
|
+
publicKey: activeIdentity.publicKey,
|
|
3316
|
+
publicKeyAlgorithm: "ed25519"
|
|
4064
3317
|
};
|
|
4065
|
-
if (email
|
|
4066
|
-
|
|
4067
|
-
}
|
|
4068
|
-
if (github && github.trim().length > 0) {
|
|
4069
|
-
updatedMember.github = github.trim();
|
|
3318
|
+
if (activeIdentity.email) {
|
|
3319
|
+
memberData.email = activeIdentity.email;
|
|
4070
3320
|
}
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
team: {
|
|
4074
|
-
...attestItConfig.team,
|
|
4075
|
-
[slug]: updatedMember
|
|
4076
|
-
}
|
|
4077
|
-
};
|
|
4078
|
-
if (updatedConfig.gates) {
|
|
4079
|
-
for (const [gateId, gate] of Object.entries(updatedConfig.gates)) {
|
|
4080
|
-
if (currentGates.includes(gateId) && !selectedGates.includes(gateId)) {
|
|
4081
|
-
gate.authorizedSigners = gate.authorizedSigners.filter((s) => s !== slug);
|
|
4082
|
-
}
|
|
4083
|
-
if (!currentGates.includes(gateId) && selectedGates.includes(gateId)) {
|
|
4084
|
-
if (!gate.authorizedSigners.includes(slug)) {
|
|
4085
|
-
gate.authorizedSigners.push(slug);
|
|
4086
|
-
}
|
|
4087
|
-
}
|
|
4088
|
-
}
|
|
3321
|
+
if (activeIdentity.github) {
|
|
3322
|
+
memberData.github = activeIdentity.github;
|
|
4089
3323
|
}
|
|
3324
|
+
const updatedConfig = addTeamMemberToConfig(config, slug, memberData, authorizedGates);
|
|
4090
3325
|
const configPath = findConfigPath();
|
|
4091
3326
|
if (!configPath) {
|
|
4092
3327
|
error("Configuration file not found");
|
|
@@ -4095,11 +3330,9 @@ async function runEdit2(slug) {
|
|
|
4095
3330
|
const yamlContent = stringify(updatedConfig);
|
|
4096
3331
|
await writeFile(configPath, yamlContent, "utf8");
|
|
4097
3332
|
log("");
|
|
4098
|
-
success(`Team member "${slug}"
|
|
4099
|
-
if (
|
|
4100
|
-
log(`Authorized for gates: ${
|
|
4101
|
-
} else {
|
|
4102
|
-
log("Not authorized for any gates");
|
|
3333
|
+
success(`Team member "${slug}" added successfully`);
|
|
3334
|
+
if (authorizedGates.length > 0) {
|
|
3335
|
+
log(`Authorized for gates: ${authorizedGates.join(", ")}`);
|
|
4103
3336
|
}
|
|
4104
3337
|
log("");
|
|
4105
3338
|
} catch (err) {
|
|
@@ -4214,7 +3447,7 @@ async function runRemove2(slug, options) {
|
|
|
4214
3447
|
}
|
|
4215
3448
|
|
|
4216
3449
|
// src/commands/team/index.ts
|
|
4217
|
-
var teamCommand = new Command("team").description("Manage team members and authorizations").addCommand(listCommand2).addCommand(addCommand).addCommand(
|
|
3450
|
+
var teamCommand = new Command("team").description("Manage team members and authorizations").addCommand(listCommand2).addCommand(addCommand).addCommand(joinCommand).addCommand(removeCommand2);
|
|
4218
3451
|
var PROGRAM_NAME2 = "attest-it";
|
|
4219
3452
|
var PROGRAM_ALIAS2 = "attest";
|
|
4220
3453
|
var PROGRAM_NAMES = [PROGRAM_NAME2, PROGRAM_ALIAS2];
|
|
@@ -4235,7 +3468,6 @@ async function getCompletions(env) {
|
|
|
4235
3468
|
{ name: "run", description: "Run test suites interactively" },
|
|
4236
3469
|
{ name: "verify", description: "Verify all seals are valid" },
|
|
4237
3470
|
{ name: "seal", description: "Create a seal for a gate" },
|
|
4238
|
-
{ name: "keygen", description: "Generate a new keypair" },
|
|
4239
3471
|
{ name: "prune", description: "Remove stale attestations" },
|
|
4240
3472
|
{ name: "identity", description: "Manage identities" },
|
|
4241
3473
|
{ name: "team", description: "Manage team members" },
|
|
@@ -4340,7 +3572,6 @@ async function getCompletions(env) {
|
|
|
4340
3572
|
"run",
|
|
4341
3573
|
"verify",
|
|
4342
3574
|
"seal",
|
|
4343
|
-
"keygen",
|
|
4344
3575
|
"prune",
|
|
4345
3576
|
"identity",
|
|
4346
3577
|
"team",
|
|
@@ -4475,43 +3706,12 @@ function createCompletionServerCommand() {
|
|
|
4475
3706
|
}
|
|
4476
3707
|
});
|
|
4477
3708
|
}
|
|
4478
|
-
function hasVersion(data) {
|
|
4479
|
-
return typeof data === "object" && data !== null && "version" in data && // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
4480
|
-
typeof data.version === "string";
|
|
4481
|
-
}
|
|
4482
|
-
var cachedVersion;
|
|
4483
|
-
function getPackageVersion() {
|
|
4484
|
-
if (cachedVersion !== void 0) {
|
|
4485
|
-
return cachedVersion;
|
|
4486
|
-
}
|
|
4487
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
4488
|
-
const __dirname = dirname(__filename);
|
|
4489
|
-
const possiblePaths = [join(__dirname, "../package.json"), join(__dirname, "../../package.json")];
|
|
4490
|
-
for (const packageJsonPath of possiblePaths) {
|
|
4491
|
-
try {
|
|
4492
|
-
const content = readFileSync(packageJsonPath, "utf-8");
|
|
4493
|
-
const packageJsonData = JSON.parse(content);
|
|
4494
|
-
if (!hasVersion(packageJsonData)) {
|
|
4495
|
-
throw new Error(`Invalid package.json at ${packageJsonPath}: missing version field`);
|
|
4496
|
-
}
|
|
4497
|
-
cachedVersion = packageJsonData.version;
|
|
4498
|
-
return cachedVersion;
|
|
4499
|
-
} catch (error2) {
|
|
4500
|
-
if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") {
|
|
4501
|
-
continue;
|
|
4502
|
-
}
|
|
4503
|
-
throw error2;
|
|
4504
|
-
}
|
|
4505
|
-
}
|
|
4506
|
-
throw new Error("Could not find package.json");
|
|
4507
|
-
}
|
|
4508
3709
|
var program = new Command();
|
|
4509
3710
|
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");
|
|
4510
3711
|
program.option("-V, --version", "output the version number");
|
|
4511
3712
|
program.addCommand(initCommand);
|
|
4512
3713
|
program.addCommand(statusCommand);
|
|
4513
3714
|
program.addCommand(runCommand);
|
|
4514
|
-
program.addCommand(keygenCommand);
|
|
4515
3715
|
program.addCommand(pruneCommand);
|
|
4516
3716
|
program.addCommand(verifyCommand);
|
|
4517
3717
|
program.addCommand(sealCommand);
|