@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/bin/attest-it.js
CHANGED
|
@@ -1,25 +1,85 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { Command } from 'commander';
|
|
3
2
|
import * as fs from 'fs';
|
|
4
|
-
import { readFileSync } from 'fs';
|
|
3
|
+
import { readFileSync, existsSync, realpathSync } from 'fs';
|
|
5
4
|
import * as path from 'path';
|
|
6
5
|
import { join, dirname } from 'path';
|
|
6
|
+
import { spawn, spawnSync } from 'child_process';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { Command } from 'commander';
|
|
7
9
|
import { detectTheme } from 'chromaterm';
|
|
8
10
|
import { input, select, confirm, checkbox } from '@inquirer/prompts';
|
|
9
|
-
import { loadConfig, toAttestItConfig, readSealsSync, computeFingerprintSync, verifyGateSeal, verifyAllSeals, computeFingerprint, createAttestation, readAttestations, upsertAttestation, KeyProviderRegistry, getDefaultPrivateKeyPath, FilesystemKeyProvider, writeSignedAttestations, loadLocalConfigSync, getActiveIdentity, isAuthorizedSigner, createSeal, writeSealsSync,
|
|
11
|
+
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';
|
|
10
12
|
import tabtab2 from '@pnpm/tabtab';
|
|
11
|
-
import { spawn } from 'child_process';
|
|
12
13
|
import * as os from 'os';
|
|
13
14
|
import { parse } from 'shell-quote';
|
|
14
15
|
import * as React7 from 'react';
|
|
15
|
-
import { useState, useEffect } from 'react';
|
|
16
16
|
import { render, useApp, Box, Text, useInput } from 'ink';
|
|
17
17
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
18
18
|
import { mkdir, writeFile, unlink, readFile } from 'fs/promises';
|
|
19
|
-
import { Spinner, Select, PasswordInput, TextInput } from '@inkjs/ui';
|
|
20
19
|
import { stringify } from 'yaml';
|
|
21
|
-
import { fileURLToPath } from 'url';
|
|
22
20
|
|
|
21
|
+
var __filename$1 = fileURLToPath(import.meta.url);
|
|
22
|
+
function getExecutableExtensions() {
|
|
23
|
+
if (process.platform === "win32") {
|
|
24
|
+
return [".cmd", ".ps1", ""];
|
|
25
|
+
}
|
|
26
|
+
return [""];
|
|
27
|
+
}
|
|
28
|
+
function findLocalCli() {
|
|
29
|
+
let dir = process.cwd();
|
|
30
|
+
const extensions = getExecutableExtensions();
|
|
31
|
+
while (dir !== dirname(dir)) {
|
|
32
|
+
const binDir = join(dir, "node_modules", ".bin");
|
|
33
|
+
for (const ext of extensions) {
|
|
34
|
+
const localBin = join(binDir, `attest-it${ext}`);
|
|
35
|
+
if (existsSync(localBin)) {
|
|
36
|
+
return localBin;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
dir = dirname(dir);
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
function getPackageRoot(filePath) {
|
|
44
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
45
|
+
const regex = /(.*node_modules\/@attest-it\/cli)/;
|
|
46
|
+
const match = regex.exec(normalizedPath);
|
|
47
|
+
if (!match?.[1]) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
return filePath.slice(0, match[1].length);
|
|
51
|
+
}
|
|
52
|
+
function isSameAsCurrentCli(localCliPath) {
|
|
53
|
+
try {
|
|
54
|
+
const localReal = realpathSync(localCliPath);
|
|
55
|
+
const currentReal = realpathSync(__filename$1);
|
|
56
|
+
if (localReal === currentReal) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
const localPkgRoot = getPackageRoot(localReal);
|
|
60
|
+
const currentPkgRoot = getPackageRoot(currentReal);
|
|
61
|
+
return localPkgRoot !== null && localPkgRoot === currentPkgRoot;
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.warn(
|
|
64
|
+
`Warning: Could not resolve CLI paths for comparison: ${err instanceof Error ? err.message : String(err)}`
|
|
65
|
+
);
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function tryDelegateToLocal() {
|
|
70
|
+
if (process.env.ATTEST_IT_SKIP_LOCAL_RESOLUTION === "1") {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
const localCli = findLocalCli();
|
|
74
|
+
if (!localCli || isSameAsCurrentCli(localCli)) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
const result = spawnSync(localCli, process.argv.slice(2), {
|
|
78
|
+
stdio: "inherit",
|
|
79
|
+
env: { ...process.env, ATTEST_IT_SKIP_LOCAL_RESOLUTION: "1" }
|
|
80
|
+
});
|
|
81
|
+
process.exit(result.status ?? 1);
|
|
82
|
+
}
|
|
23
83
|
var globalOptions = {};
|
|
24
84
|
var theme;
|
|
25
85
|
async function initTheme() {
|
|
@@ -252,47 +312,93 @@ async function offerCompletionInstall() {
|
|
|
252
312
|
return false;
|
|
253
313
|
}
|
|
254
314
|
}
|
|
315
|
+
function hasVersion(data) {
|
|
316
|
+
return typeof data === "object" && data !== null && "version" in data && // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
317
|
+
typeof data.version === "string";
|
|
318
|
+
}
|
|
319
|
+
var cachedVersion;
|
|
320
|
+
function getPackageVersion() {
|
|
321
|
+
if (cachedVersion !== void 0) {
|
|
322
|
+
return cachedVersion;
|
|
323
|
+
}
|
|
324
|
+
const __filename2 = fileURLToPath(import.meta.url);
|
|
325
|
+
const __dirname = dirname(__filename2);
|
|
326
|
+
const possiblePaths = [join(__dirname, "../package.json"), join(__dirname, "../../package.json")];
|
|
327
|
+
for (const packageJsonPath of possiblePaths) {
|
|
328
|
+
try {
|
|
329
|
+
const content = readFileSync(packageJsonPath, "utf-8");
|
|
330
|
+
const packageJsonData = JSON.parse(content);
|
|
331
|
+
if (!hasVersion(packageJsonData)) {
|
|
332
|
+
throw new Error(`Invalid package.json at ${packageJsonPath}: missing version field`);
|
|
333
|
+
}
|
|
334
|
+
cachedVersion = packageJsonData.version;
|
|
335
|
+
return cachedVersion;
|
|
336
|
+
} catch (error2) {
|
|
337
|
+
if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") {
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
throw error2;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
throw new Error("Could not find package.json");
|
|
344
|
+
}
|
|
255
345
|
|
|
256
346
|
// src/commands/init.ts
|
|
257
347
|
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
348
|
await runInit(options);
|
|
259
349
|
});
|
|
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
|
-
|
|
350
|
+
function loadConfigTemplate() {
|
|
351
|
+
const __filename2 = fileURLToPath(import.meta.url);
|
|
352
|
+
const __dirname = dirname(__filename2);
|
|
353
|
+
const possiblePaths = [
|
|
354
|
+
join(__dirname, "../../templates/config.yaml"),
|
|
355
|
+
join(__dirname, "../templates/config.yaml")
|
|
356
|
+
];
|
|
357
|
+
for (const templatePath of possiblePaths) {
|
|
358
|
+
try {
|
|
359
|
+
return fs.readFileSync(templatePath, "utf-8");
|
|
360
|
+
} catch (error2) {
|
|
361
|
+
if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") {
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
throw error2;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
throw new Error("Could not find config.yaml template");
|
|
368
|
+
}
|
|
369
|
+
function isPackageJson(data) {
|
|
370
|
+
return typeof data === "object" && data !== null && "name" in data && "version" in data && // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
371
|
+
typeof data.name === "string" && // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
372
|
+
typeof data.version === "string";
|
|
373
|
+
}
|
|
374
|
+
function detectPackageManager() {
|
|
375
|
+
if (fs.existsSync("pnpm-lock.yaml")) return "pnpm";
|
|
376
|
+
if (fs.existsSync("yarn.lock")) return "yarn";
|
|
377
|
+
if (fs.existsSync("bun.lockb")) return "bun";
|
|
378
|
+
return "npm";
|
|
379
|
+
}
|
|
380
|
+
async function ensureDevDependency() {
|
|
381
|
+
const packageJsonPath = "package.json";
|
|
382
|
+
const packageManager = detectPackageManager();
|
|
383
|
+
let created = false;
|
|
384
|
+
let packageJson;
|
|
385
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
386
|
+
const content = await fs.promises.readFile(packageJsonPath, "utf8");
|
|
387
|
+
const parsed = JSON.parse(content);
|
|
388
|
+
if (!isPackageJson(parsed)) {
|
|
389
|
+
throw new Error("Invalid package.json: missing required name or version field");
|
|
390
|
+
}
|
|
391
|
+
packageJson = parsed;
|
|
392
|
+
} else {
|
|
393
|
+
packageJson = { name: path.basename(process.cwd()), version: "1.0.0" };
|
|
394
|
+
created = true;
|
|
395
|
+
}
|
|
396
|
+
const devDeps = packageJson.devDependencies ?? {};
|
|
397
|
+
devDeps["attest-it"] = "^" + getPackageVersion();
|
|
398
|
+
packageJson.devDependencies = devDeps;
|
|
399
|
+
await fs.promises.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n");
|
|
400
|
+
return { packageManager, created };
|
|
401
|
+
}
|
|
296
402
|
async function runInit(options) {
|
|
297
403
|
try {
|
|
298
404
|
const configPath = path.resolve(options.path);
|
|
@@ -307,14 +413,22 @@ async function runInit(options) {
|
|
|
307
413
|
process.exit(ExitCode.CANCELLED);
|
|
308
414
|
}
|
|
309
415
|
}
|
|
416
|
+
const { packageManager, created } = await ensureDevDependency();
|
|
417
|
+
if (created) {
|
|
418
|
+
success("Created package.json");
|
|
419
|
+
} else {
|
|
420
|
+
success("Updated package.json with attest-it devDependency");
|
|
421
|
+
}
|
|
310
422
|
await fs.promises.mkdir(configDir, { recursive: true });
|
|
311
|
-
|
|
423
|
+
const configTemplate = loadConfigTemplate();
|
|
424
|
+
await fs.promises.writeFile(configPath, configTemplate, "utf-8");
|
|
312
425
|
success(`Configuration created at ${configPath}`);
|
|
313
426
|
log("");
|
|
314
427
|
log("Next steps:");
|
|
315
|
-
log(` 1.
|
|
316
|
-
log(" 2. Run: attest-it
|
|
317
|
-
log(" 3. Run: attest-it
|
|
428
|
+
log(` 1. Run: ${packageManager} install`);
|
|
429
|
+
log(" 2. Run: attest-it identity create (if you haven't already)");
|
|
430
|
+
log(" 3. Run: attest-it team join");
|
|
431
|
+
log(" 4. Edit .attest-it/config.yaml to define your gates and suites");
|
|
318
432
|
await offerCompletionInstall();
|
|
319
433
|
} catch (err) {
|
|
320
434
|
if (err instanceof Error) {
|
|
@@ -546,14 +660,14 @@ function SelectionPrompt({
|
|
|
546
660
|
onSelect,
|
|
547
661
|
groups
|
|
548
662
|
}) {
|
|
549
|
-
useInput((
|
|
550
|
-
const matchedOption = options.find((opt) => opt.hint ===
|
|
663
|
+
useInput((input4) => {
|
|
664
|
+
const matchedOption = options.find((opt) => opt.hint === input4);
|
|
551
665
|
if (matchedOption) {
|
|
552
666
|
onSelect(matchedOption.value);
|
|
553
667
|
return;
|
|
554
668
|
}
|
|
555
669
|
if (groups) {
|
|
556
|
-
const matchedGroup = groups.find((group) => group.name ===
|
|
670
|
+
const matchedGroup = groups.find((group) => group.name === input4);
|
|
557
671
|
if (matchedGroup) {
|
|
558
672
|
onSelect(matchedGroup.name);
|
|
559
673
|
}
|
|
@@ -603,17 +717,17 @@ function SuiteSelector({
|
|
|
603
717
|
return next;
|
|
604
718
|
});
|
|
605
719
|
}, []);
|
|
606
|
-
useInput((
|
|
607
|
-
if (
|
|
720
|
+
useInput((input4, key) => {
|
|
721
|
+
if (input4 === "a") {
|
|
608
722
|
setSelectedSuites(new Set(pendingSuites.map((s) => s.name)));
|
|
609
723
|
return;
|
|
610
724
|
}
|
|
611
|
-
if (
|
|
725
|
+
if (input4 === "n") {
|
|
612
726
|
onExit();
|
|
613
727
|
return;
|
|
614
728
|
}
|
|
615
|
-
if (/^[1-9]$/.test(
|
|
616
|
-
const idx = parseInt(
|
|
729
|
+
if (/^[1-9]$/.test(input4)) {
|
|
730
|
+
const idx = parseInt(input4, 10) - 1;
|
|
617
731
|
if (idx < pendingSuites.length) {
|
|
618
732
|
const suite = pendingSuites[idx];
|
|
619
733
|
if (suite) {
|
|
@@ -622,8 +736,8 @@ function SuiteSelector({
|
|
|
622
736
|
}
|
|
623
737
|
return;
|
|
624
738
|
}
|
|
625
|
-
if (
|
|
626
|
-
const groupIdx = parseInt(
|
|
739
|
+
if (input4.startsWith("g") && groups) {
|
|
740
|
+
const groupIdx = parseInt(input4.slice(1), 10) - 1;
|
|
627
741
|
const groupNames = Object.keys(groups);
|
|
628
742
|
if (groupIdx >= 0 && groupIdx < groupNames.length) {
|
|
629
743
|
const groupName = groupNames[groupIdx];
|
|
@@ -640,7 +754,7 @@ function SuiteSelector({
|
|
|
640
754
|
onSelect(Array.from(selectedSuites));
|
|
641
755
|
return;
|
|
642
756
|
}
|
|
643
|
-
if (
|
|
757
|
+
if (input4 === " ") {
|
|
644
758
|
const currentSuite = pendingSuites[cursorIndex];
|
|
645
759
|
if (currentSuite) {
|
|
646
760
|
toggleSuite(currentSuite.name);
|
|
@@ -773,11 +887,11 @@ function TestRunner({
|
|
|
773
887
|
};
|
|
774
888
|
}, [currentIndex, phase, suites, executeTest, onComplete]);
|
|
775
889
|
useInput(
|
|
776
|
-
(
|
|
890
|
+
(input4, key) => {
|
|
777
891
|
if (phase !== "confirming") return;
|
|
778
892
|
const currentSuite2 = suites[currentIndex];
|
|
779
893
|
if (!currentSuite2) return;
|
|
780
|
-
if (
|
|
894
|
+
if (input4.toLowerCase() === "y" || key.return) {
|
|
781
895
|
createAttestation3(currentSuite2).then(() => {
|
|
782
896
|
setResults((prev) => ({
|
|
783
897
|
...prev,
|
|
@@ -794,7 +908,7 @@ function TestRunner({
|
|
|
794
908
|
setPhase("running");
|
|
795
909
|
});
|
|
796
910
|
}
|
|
797
|
-
if (
|
|
911
|
+
if (input4.toLowerCase() === "n") {
|
|
798
912
|
setResults((prev) => ({
|
|
799
913
|
...prev,
|
|
800
914
|
skipped: [...prev.skipped, currentSuite2]
|
|
@@ -1507,9 +1621,9 @@ async function runSingleSuite(suiteName, config, options) {
|
|
|
1507
1621
|
if (!await keyProvider.keyExists(keyRef)) {
|
|
1508
1622
|
error(`Private key not found in ${keyProvider.displayName}`);
|
|
1509
1623
|
if (keyProvider.type === "filesystem") {
|
|
1510
|
-
error('Run "attest-it
|
|
1624
|
+
error('Run "attest-it identity create" first to generate a keypair.');
|
|
1511
1625
|
} else {
|
|
1512
|
-
error('Run "attest-it
|
|
1626
|
+
error('Run "attest-it identity create" to generate and store a key.');
|
|
1513
1627
|
}
|
|
1514
1628
|
process.exit(ExitCode.MISSING_KEY);
|
|
1515
1629
|
}
|
|
@@ -1533,7 +1647,7 @@ async function promptForSeal(suiteName, gateId, config) {
|
|
|
1533
1647
|
const localConfig = loadLocalConfigSync();
|
|
1534
1648
|
if (!localConfig) {
|
|
1535
1649
|
warn("No local identity configuration found - cannot create seal");
|
|
1536
|
-
warn('Run "attest-it
|
|
1650
|
+
warn('Run "attest-it identity create" to set up your identity');
|
|
1537
1651
|
return;
|
|
1538
1652
|
}
|
|
1539
1653
|
const identity = getActiveIdentity(localConfig);
|
|
@@ -1568,8 +1682,8 @@ async function promptForSeal(suiteName, gateId, config) {
|
|
|
1568
1682
|
const keyProvider = createKeyProviderFromIdentity(identity);
|
|
1569
1683
|
const keyRef = getKeyRefFromIdentity(identity);
|
|
1570
1684
|
const keyResult = await keyProvider.getPrivateKey(keyRef);
|
|
1571
|
-
const
|
|
1572
|
-
const privateKeyPem = await
|
|
1685
|
+
const fs3 = await import('fs/promises');
|
|
1686
|
+
const privateKeyPem = await fs3.readFile(keyResult.keyPath, "utf8");
|
|
1573
1687
|
await keyResult.cleanup();
|
|
1574
1688
|
const identitySlug = localConfig.activeIdentity;
|
|
1575
1689
|
const seal = createSeal({
|
|
@@ -1651,818 +1765,127 @@ function getKeyRefFromIdentity(identity) {
|
|
|
1651
1765
|
}
|
|
1652
1766
|
}
|
|
1653
1767
|
}
|
|
1654
|
-
var
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
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();
|
|
1768
|
+
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) => {
|
|
1769
|
+
await runPrune(options);
|
|
1770
|
+
});
|
|
1771
|
+
async function runPrune(options) {
|
|
1772
|
+
try {
|
|
1773
|
+
const keepDays = parseInt(options.keepDays, 10);
|
|
1774
|
+
if (isNaN(keepDays) || keepDays < 1) {
|
|
1775
|
+
error("--keep-days must be a positive integer");
|
|
1776
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
1777
|
+
return;
|
|
1677
1778
|
}
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
const
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
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();
|
|
1779
|
+
const config = await loadConfig();
|
|
1780
|
+
const attestationsPath = config.settings.attestationsPath;
|
|
1781
|
+
const file = await readAttestations(attestationsPath);
|
|
1782
|
+
if (!file || file.attestations.length === 0) {
|
|
1783
|
+
info("No attestations to prune");
|
|
1784
|
+
process.exit(ExitCode.SUCCESS);
|
|
1785
|
+
return;
|
|
1721
1786
|
}
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1787
|
+
const now = Date.now();
|
|
1788
|
+
const keepMs = keepDays * 24 * 60 * 60 * 1e3;
|
|
1789
|
+
const stale = [];
|
|
1790
|
+
const keep = [];
|
|
1791
|
+
for (const attestation of file.attestations) {
|
|
1792
|
+
const attestedAt = new Date(attestation.attestedAt).getTime();
|
|
1793
|
+
const ageMs = now - attestedAt;
|
|
1794
|
+
const ageDays = Math.floor(ageMs / (1e3 * 60 * 60 * 24));
|
|
1795
|
+
const suiteExists = attestation.suite in config.suites;
|
|
1796
|
+
let fingerprintMatches = false;
|
|
1797
|
+
if (suiteExists) {
|
|
1798
|
+
const suiteConfig = config.suites[attestation.suite];
|
|
1799
|
+
if (suiteConfig?.packages) {
|
|
1800
|
+
const fingerprintOptions = {
|
|
1801
|
+
packages: suiteConfig.packages,
|
|
1802
|
+
...suiteConfig.ignore && { ignore: suiteConfig.ignore }
|
|
1803
|
+
};
|
|
1804
|
+
const result = await computeFingerprint(fingerprintOptions);
|
|
1805
|
+
fingerprintMatches = result.fingerprint === attestation.fingerprint;
|
|
1806
|
+
}
|
|
1739
1807
|
}
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
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");
|
|
1808
|
+
const isStale = !fingerprintMatches && ageMs > keepMs;
|
|
1809
|
+
const orphaned = !suiteExists;
|
|
1810
|
+
if (isStale || orphaned) {
|
|
1811
|
+
stale.push(attestation);
|
|
1812
|
+
const reason = orphaned ? "suite removed" : !fingerprintMatches ? "fingerprint changed" : "expired";
|
|
1813
|
+
verbose(`Stale: ${attestation.suite} (${reason}, ${String(ageDays)} days old)`);
|
|
1753
1814
|
} else {
|
|
1754
|
-
|
|
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);
|
|
1815
|
+
keep.push(attestation);
|
|
1766
1816
|
}
|
|
1767
1817
|
}
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
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"));
|
|
1818
|
+
if (stale.length === 0) {
|
|
1819
|
+
success("No stale attestations found");
|
|
1820
|
+
process.exit(ExitCode.SUCCESS);
|
|
1821
|
+
return;
|
|
1799
1822
|
}
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
void generateKeys("filesystem");
|
|
1823
|
+
log(`Found ${String(stale.length)} stale attestation(s):`);
|
|
1824
|
+
for (const attestation of stale) {
|
|
1825
|
+
const ageDays = Math.floor(
|
|
1826
|
+
(now - new Date(attestation.attestedAt).getTime()) / (1e3 * 60 * 60 * 24)
|
|
1827
|
+
);
|
|
1828
|
+
log(` - ${attestation.suite} (${String(ageDays)} days old)`);
|
|
1807
1829
|
}
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
onError(new Error(`Passphrase must be at least ${String(MIN_PASSPHRASE_LENGTH)} characters`));
|
|
1830
|
+
if (options.dryRun) {
|
|
1831
|
+
info("Dry run - no changes made");
|
|
1832
|
+
process.exit(ExitCode.SUCCESS);
|
|
1812
1833
|
return;
|
|
1813
1834
|
}
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
onError(new Error("Passphrases do not match. Please try again."));
|
|
1820
|
-
setEncryptionPassphrase(void 0);
|
|
1821
|
-
setStep("enter-encryption-passphrase");
|
|
1835
|
+
const privateKeyPath = getDefaultPrivateKeyPath();
|
|
1836
|
+
if (!fs.existsSync(privateKeyPath)) {
|
|
1837
|
+
error(`Private key not found: ${privateKeyPath}`);
|
|
1838
|
+
error("Cannot re-sign attestations file.");
|
|
1839
|
+
process.exit(ExitCode.MISSING_KEY);
|
|
1822
1840
|
return;
|
|
1823
1841
|
}
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
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"));
|
|
1842
|
+
await writeSignedAttestations({
|
|
1843
|
+
filePath: attestationsPath,
|
|
1844
|
+
attestations: keep,
|
|
1845
|
+
privateKeyPath
|
|
1846
|
+
});
|
|
1847
|
+
success(`Pruned ${String(stale.length)} stale attestation(s)`);
|
|
1848
|
+
log(`Remaining: ${String(keep.length)} attestation(s)`);
|
|
1849
|
+
process.exit(ExitCode.SUCCESS);
|
|
1850
|
+
} catch (err) {
|
|
1851
|
+
if (err instanceof Error) {
|
|
1852
|
+
error(err.message);
|
|
1853
|
+
} else {
|
|
1854
|
+
error("Unknown error occurred");
|
|
1849
1855
|
}
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
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);
|
|
1856
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
var verifyCommand = new Command("verify").description("Verify all gate seals (for CI)").argument("[gates...]", "Verify specific gates only").option("--json", "Output JSON for machine parsing").action(async (gates, options) => {
|
|
1861
|
+
await runVerify(gates, options);
|
|
1862
|
+
});
|
|
1863
|
+
async function runVerify(gates, options) {
|
|
1864
|
+
try {
|
|
1865
|
+
const config = await loadConfig();
|
|
1866
|
+
const attestItConfig = toAttestItConfig(config);
|
|
1867
|
+
if (!attestItConfig.gates || Object.keys(attestItConfig.gates).length === 0) {
|
|
1868
|
+
error("No gates defined in configuration");
|
|
1869
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
1962
1870
|
}
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
if (step === "select-provider") {
|
|
1971
|
-
const options = [
|
|
1972
|
-
{
|
|
1973
|
-
label: `Local Filesystem (${getDefaultPrivateKeyPath()})`,
|
|
1974
|
-
value: "filesystem"
|
|
1871
|
+
const projectRoot = process.cwd();
|
|
1872
|
+
const sealsFile = readSealsSync(projectRoot, attestItConfig.settings.sealsPath);
|
|
1873
|
+
const gatesToVerify = gates.length > 0 ? gates : Object.keys(attestItConfig.gates);
|
|
1874
|
+
for (const gateId of gatesToVerify) {
|
|
1875
|
+
if (!attestItConfig.gates[gateId]) {
|
|
1876
|
+
error(`Gate '${gateId}' not found in configuration`);
|
|
1877
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
1975
1878
|
}
|
|
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
1879
|
}
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1880
|
+
const fingerprints = {};
|
|
1881
|
+
for (const gateId of gatesToVerify) {
|
|
1882
|
+
const gate = attestItConfig.gates[gateId];
|
|
1883
|
+
if (!gate) continue;
|
|
1884
|
+
const result = computeFingerprintSync({
|
|
1885
|
+
packages: gate.fingerprint.paths,
|
|
1886
|
+
...gate.fingerprint.exclude && { ignore: gate.fingerprint.exclude }
|
|
1993
1887
|
});
|
|
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
|
-
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
|
-
await runPrune(options);
|
|
2347
|
-
});
|
|
2348
|
-
async function runPrune(options) {
|
|
2349
|
-
try {
|
|
2350
|
-
const keepDays = parseInt(options.keepDays, 10);
|
|
2351
|
-
if (isNaN(keepDays) || keepDays < 1) {
|
|
2352
|
-
error("--keep-days must be a positive integer");
|
|
2353
|
-
process.exit(ExitCode.CONFIG_ERROR);
|
|
2354
|
-
return;
|
|
2355
|
-
}
|
|
2356
|
-
const config = await loadConfig();
|
|
2357
|
-
const attestationsPath = config.settings.attestationsPath;
|
|
2358
|
-
const file = await readAttestations(attestationsPath);
|
|
2359
|
-
if (!file || file.attestations.length === 0) {
|
|
2360
|
-
info("No attestations to prune");
|
|
2361
|
-
process.exit(ExitCode.SUCCESS);
|
|
2362
|
-
return;
|
|
2363
|
-
}
|
|
2364
|
-
const now = Date.now();
|
|
2365
|
-
const keepMs = keepDays * 24 * 60 * 60 * 1e3;
|
|
2366
|
-
const stale = [];
|
|
2367
|
-
const keep = [];
|
|
2368
|
-
for (const attestation of file.attestations) {
|
|
2369
|
-
const attestedAt = new Date(attestation.attestedAt).getTime();
|
|
2370
|
-
const ageMs = now - attestedAt;
|
|
2371
|
-
const ageDays = Math.floor(ageMs / (1e3 * 60 * 60 * 24));
|
|
2372
|
-
const suiteExists = attestation.suite in config.suites;
|
|
2373
|
-
let fingerprintMatches = false;
|
|
2374
|
-
if (suiteExists) {
|
|
2375
|
-
const suiteConfig = config.suites[attestation.suite];
|
|
2376
|
-
if (suiteConfig?.packages) {
|
|
2377
|
-
const fingerprintOptions = {
|
|
2378
|
-
packages: suiteConfig.packages,
|
|
2379
|
-
...suiteConfig.ignore && { ignore: suiteConfig.ignore }
|
|
2380
|
-
};
|
|
2381
|
-
const result = await computeFingerprint(fingerprintOptions);
|
|
2382
|
-
fingerprintMatches = result.fingerprint === attestation.fingerprint;
|
|
2383
|
-
}
|
|
2384
|
-
}
|
|
2385
|
-
const isStale = !fingerprintMatches && ageMs > keepMs;
|
|
2386
|
-
const orphaned = !suiteExists;
|
|
2387
|
-
if (isStale || orphaned) {
|
|
2388
|
-
stale.push(attestation);
|
|
2389
|
-
const reason = orphaned ? "suite removed" : !fingerprintMatches ? "fingerprint changed" : "expired";
|
|
2390
|
-
verbose(`Stale: ${attestation.suite} (${reason}, ${String(ageDays)} days old)`);
|
|
2391
|
-
} else {
|
|
2392
|
-
keep.push(attestation);
|
|
2393
|
-
}
|
|
2394
|
-
}
|
|
2395
|
-
if (stale.length === 0) {
|
|
2396
|
-
success("No stale attestations found");
|
|
2397
|
-
process.exit(ExitCode.SUCCESS);
|
|
2398
|
-
return;
|
|
2399
|
-
}
|
|
2400
|
-
log(`Found ${String(stale.length)} stale attestation(s):`);
|
|
2401
|
-
for (const attestation of stale) {
|
|
2402
|
-
const ageDays = Math.floor(
|
|
2403
|
-
(now - new Date(attestation.attestedAt).getTime()) / (1e3 * 60 * 60 * 24)
|
|
2404
|
-
);
|
|
2405
|
-
log(` - ${attestation.suite} (${String(ageDays)} days old)`);
|
|
2406
|
-
}
|
|
2407
|
-
if (options.dryRun) {
|
|
2408
|
-
info("Dry run - no changes made");
|
|
2409
|
-
process.exit(ExitCode.SUCCESS);
|
|
2410
|
-
return;
|
|
2411
|
-
}
|
|
2412
|
-
const privateKeyPath = getDefaultPrivateKeyPath();
|
|
2413
|
-
if (!fs.existsSync(privateKeyPath)) {
|
|
2414
|
-
error(`Private key not found: ${privateKeyPath}`);
|
|
2415
|
-
error("Cannot re-sign attestations file.");
|
|
2416
|
-
process.exit(ExitCode.MISSING_KEY);
|
|
2417
|
-
return;
|
|
2418
|
-
}
|
|
2419
|
-
await writeSignedAttestations({
|
|
2420
|
-
filePath: attestationsPath,
|
|
2421
|
-
attestations: keep,
|
|
2422
|
-
privateKeyPath
|
|
2423
|
-
});
|
|
2424
|
-
success(`Pruned ${String(stale.length)} stale attestation(s)`);
|
|
2425
|
-
log(`Remaining: ${String(keep.length)} attestation(s)`);
|
|
2426
|
-
process.exit(ExitCode.SUCCESS);
|
|
2427
|
-
} catch (err) {
|
|
2428
|
-
if (err instanceof Error) {
|
|
2429
|
-
error(err.message);
|
|
2430
|
-
} else {
|
|
2431
|
-
error("Unknown error occurred");
|
|
2432
|
-
}
|
|
2433
|
-
process.exit(ExitCode.CONFIG_ERROR);
|
|
2434
|
-
return;
|
|
2435
|
-
}
|
|
2436
|
-
}
|
|
2437
|
-
var verifyCommand = new Command("verify").description("Verify all gate seals (for CI)").argument("[gates...]", "Verify specific gates only").option("--json", "Output JSON for machine parsing").action(async (gates, options) => {
|
|
2438
|
-
await runVerify(gates, options);
|
|
2439
|
-
});
|
|
2440
|
-
async function runVerify(gates, options) {
|
|
2441
|
-
try {
|
|
2442
|
-
const config = await loadConfig();
|
|
2443
|
-
const attestItConfig = toAttestItConfig(config);
|
|
2444
|
-
if (!attestItConfig.gates || Object.keys(attestItConfig.gates).length === 0) {
|
|
2445
|
-
error("No gates defined in configuration");
|
|
2446
|
-
process.exit(ExitCode.CONFIG_ERROR);
|
|
2447
|
-
}
|
|
2448
|
-
const projectRoot = process.cwd();
|
|
2449
|
-
const sealsFile = readSealsSync(projectRoot, attestItConfig.settings.sealsPath);
|
|
2450
|
-
const gatesToVerify = gates.length > 0 ? gates : Object.keys(attestItConfig.gates);
|
|
2451
|
-
for (const gateId of gatesToVerify) {
|
|
2452
|
-
if (!attestItConfig.gates[gateId]) {
|
|
2453
|
-
error(`Gate '${gateId}' not found in configuration`);
|
|
2454
|
-
process.exit(ExitCode.CONFIG_ERROR);
|
|
2455
|
-
}
|
|
2456
|
-
}
|
|
2457
|
-
const fingerprints = {};
|
|
2458
|
-
for (const gateId of gatesToVerify) {
|
|
2459
|
-
const gate = attestItConfig.gates[gateId];
|
|
2460
|
-
if (!gate) continue;
|
|
2461
|
-
const result = computeFingerprintSync({
|
|
2462
|
-
packages: gate.fingerprint.paths,
|
|
2463
|
-
...gate.fingerprint.exclude && { ignore: gate.fingerprint.exclude }
|
|
2464
|
-
});
|
|
2465
|
-
fingerprints[gateId] = result.fingerprint;
|
|
1888
|
+
fingerprints[gateId] = result.fingerprint;
|
|
2466
1889
|
}
|
|
2467
1890
|
const results = gates.length > 0 ? gatesToVerify.map(
|
|
2468
1891
|
(gateId) => (
|
|
@@ -2593,7 +2016,7 @@ async function runSeal(gates, options) {
|
|
|
2593
2016
|
const localConfig = loadLocalConfigSync();
|
|
2594
2017
|
if (!localConfig) {
|
|
2595
2018
|
error("No local identity configuration found");
|
|
2596
|
-
error('Run "attest-it
|
|
2019
|
+
error('Run "attest-it identity create" first to set up your identity');
|
|
2597
2020
|
process.exit(ExitCode.CONFIG_ERROR);
|
|
2598
2021
|
}
|
|
2599
2022
|
const identity = getActiveIdentity(localConfig);
|
|
@@ -2690,8 +2113,8 @@ async function processSingleGate(gateId, config, identity, identitySlug, sealsFi
|
|
|
2690
2113
|
const keyProvider = createKeyProviderFromIdentity2(identity);
|
|
2691
2114
|
const keyRef = getKeyRefFromIdentity2(identity);
|
|
2692
2115
|
const keyResult = await keyProvider.getPrivateKey(keyRef);
|
|
2693
|
-
const
|
|
2694
|
-
const privateKeyPem = await
|
|
2116
|
+
const fs3 = await import('fs/promises');
|
|
2117
|
+
const privateKeyPem = await fs3.readFile(keyResult.keyPath, "utf8");
|
|
2695
2118
|
await keyResult.cleanup();
|
|
2696
2119
|
const seal = createSeal({
|
|
2697
2120
|
gateId,
|
|
@@ -2912,10 +2335,19 @@ async function runCreate() {
|
|
|
2912
2335
|
default: ""
|
|
2913
2336
|
});
|
|
2914
2337
|
info("Checking available key storage providers...");
|
|
2338
|
+
info(
|
|
2339
|
+
"You may see authentication prompts from 1Password, macOS Keychain, or other security tools."
|
|
2340
|
+
);
|
|
2915
2341
|
const opAvailable = await OnePasswordKeyProvider.isInstalled();
|
|
2342
|
+
verbose(` 1Password CLI (op): ${opAvailable ? "found" : "not found"}`);
|
|
2916
2343
|
const keychainAvailable = MacOSKeychainKeyProvider.isAvailable();
|
|
2344
|
+
verbose(` macOS Keychain: ${keychainAvailable ? "available" : "not available (not macOS)"}`);
|
|
2917
2345
|
const yubikeyInstalled = await YubiKeyProvider.isInstalled();
|
|
2346
|
+
verbose(` YubiKey CLI (ykman): ${yubikeyInstalled ? "found" : "not found"}`);
|
|
2918
2347
|
const yubikeyConnected = yubikeyInstalled ? await YubiKeyProvider.isConnected() : false;
|
|
2348
|
+
if (yubikeyInstalled) {
|
|
2349
|
+
verbose(` YubiKey device: ${yubikeyConnected ? "connected" : "not connected"}`);
|
|
2350
|
+
}
|
|
2919
2351
|
const configDir = getAttestItConfigDir();
|
|
2920
2352
|
const storageChoices = [
|
|
2921
2353
|
{ name: `File system (${join(configDir, "keys")})`, value: "file" }
|
|
@@ -2929,24 +2361,24 @@ async function runCreate() {
|
|
|
2929
2361
|
if (yubikeyInstalled) {
|
|
2930
2362
|
const yubikeyLabel = yubikeyConnected ? "YubiKey (encrypted with challenge-response)" : "YubiKey (not connected - insert YubiKey first)";
|
|
2931
2363
|
storageChoices.push({ name: yubikeyLabel, value: "yubikey" });
|
|
2364
|
+
} else {
|
|
2365
|
+
storageChoices.push({
|
|
2366
|
+
name: theme3.muted("YubiKey (install ykman CLI to enable)"),
|
|
2367
|
+
value: "yubikey-disabled",
|
|
2368
|
+
// @ts-expect-error -- @inquirer/prompts supports disabled property but types may not reflect it
|
|
2369
|
+
disabled: true
|
|
2370
|
+
});
|
|
2932
2371
|
}
|
|
2933
2372
|
const keyStorageType = await select({
|
|
2934
2373
|
message: "Where should the private key be stored?",
|
|
2935
2374
|
choices: storageChoices
|
|
2936
2375
|
});
|
|
2937
|
-
|
|
2938
|
-
log("Generating Ed25519 keypair...");
|
|
2939
|
-
const keyPair = generateEd25519KeyPair();
|
|
2940
|
-
let privateKeyRef;
|
|
2941
|
-
let keyStorageDescription;
|
|
2376
|
+
let storageConfig;
|
|
2942
2377
|
switch (keyStorageType) {
|
|
2943
2378
|
case "file": {
|
|
2944
2379
|
const keysDir = join(getAttestItConfigDir(), "keys");
|
|
2945
|
-
await mkdir(keysDir, { recursive: true });
|
|
2946
2380
|
const keyPath = join(keysDir, `${slug}.pem`);
|
|
2947
|
-
|
|
2948
|
-
privateKeyRef = { type: "file", path: keyPath };
|
|
2949
|
-
keyStorageDescription = keyPath;
|
|
2381
|
+
storageConfig = { type: "file", keyPath };
|
|
2950
2382
|
break;
|
|
2951
2383
|
}
|
|
2952
2384
|
case "keychain": {
|
|
@@ -2954,6 +2386,9 @@ async function runCreate() {
|
|
|
2954
2386
|
error("macOS Keychain is not available on this system");
|
|
2955
2387
|
process.exit(ExitCode.CONFIG_ERROR);
|
|
2956
2388
|
}
|
|
2389
|
+
log("");
|
|
2390
|
+
info("Accessing macOS Keychain to list available keychains...");
|
|
2391
|
+
info("You may be prompted to allow access or enter your password.");
|
|
2957
2392
|
const keychains = await MacOSKeychainKeyProvider.listKeychains();
|
|
2958
2393
|
if (keychains.length === 0) {
|
|
2959
2394
|
throw new Error("No keychains found on this system");
|
|
@@ -2989,38 +2424,15 @@ async function runCreate() {
|
|
|
2989
2424
|
return true;
|
|
2990
2425
|
}
|
|
2991
2426
|
});
|
|
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}`;
|
|
2427
|
+
storageConfig = { type: "keychain", selectedKeychain, keychainItemName };
|
|
3021
2428
|
break;
|
|
3022
2429
|
}
|
|
3023
2430
|
case "1password": {
|
|
2431
|
+
log("");
|
|
2432
|
+
info("Accessing 1Password to list your accounts and vaults...");
|
|
2433
|
+
info(
|
|
2434
|
+
"You may see biometric prompts or be asked to unlock 1Password for each configured account."
|
|
2435
|
+
);
|
|
3024
2436
|
const accounts = await OnePasswordKeyProvider.listAccounts();
|
|
3025
2437
|
if (accounts.length === 0) {
|
|
3026
2438
|
throw new Error(
|
|
@@ -3041,7 +2453,7 @@ async function runCreate() {
|
|
|
3041
2453
|
"--format=json"
|
|
3042
2454
|
]);
|
|
3043
2455
|
const details = JSON.parse(stdout);
|
|
3044
|
-
const name2 = details !== null && typeof details === "object" && "name" in details && typeof details.name === "string" ? details.name :
|
|
2456
|
+
const name2 = details !== null && typeof details === "object" && "name" in details && typeof details.name === "string" ? details.name : "[Could not read account name]";
|
|
3045
2457
|
return {
|
|
3046
2458
|
url: acc.url,
|
|
3047
2459
|
email: acc.email,
|
|
@@ -3051,7 +2463,7 @@ async function runCreate() {
|
|
|
3051
2463
|
return {
|
|
3052
2464
|
url: acc.url,
|
|
3053
2465
|
email: acc.email,
|
|
3054
|
-
name:
|
|
2466
|
+
name: "[Could not read account name]"
|
|
3055
2467
|
};
|
|
3056
2468
|
}
|
|
3057
2469
|
})
|
|
@@ -3093,43 +2505,21 @@ async function runCreate() {
|
|
|
3093
2505
|
return true;
|
|
3094
2506
|
}
|
|
3095
2507
|
});
|
|
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 = {
|
|
2508
|
+
const selectedAccountDetails = accountDetails.find((acc) => acc.url === selectedAccount);
|
|
2509
|
+
const accountDisplayName = selectedAccountDetails?.name ?? selectedAccount;
|
|
2510
|
+
storageConfig = {
|
|
3124
2511
|
type: "1password",
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
2512
|
+
selectedAccount,
|
|
2513
|
+
accountDisplayName,
|
|
2514
|
+
selectedVault,
|
|
2515
|
+
item
|
|
3128
2516
|
};
|
|
3129
|
-
keyStorageDescription = `1Password (${selectedVault}/${item})`;
|
|
3130
2517
|
break;
|
|
3131
2518
|
}
|
|
3132
2519
|
case "yubikey": {
|
|
2520
|
+
log("");
|
|
2521
|
+
info("Accessing YubiKey to detect connected devices...");
|
|
2522
|
+
info("Your private key will be encrypted using HMAC challenge-response from the YubiKey.");
|
|
3133
2523
|
if (!await YubiKeyProvider.isConnected()) {
|
|
3134
2524
|
error("No YubiKey detected. Please insert your YubiKey and try again.");
|
|
3135
2525
|
process.exit(ExitCode.CONFIG_ERROR);
|
|
@@ -3180,26 +2570,116 @@ async function runCreate() {
|
|
|
3180
2570
|
return true;
|
|
3181
2571
|
}
|
|
3182
2572
|
});
|
|
2573
|
+
const keysDir = join(getAttestItConfigDir(), "keys");
|
|
2574
|
+
const encryptedKeyPath = join(keysDir, encryptedKeyName);
|
|
2575
|
+
storageConfig = { type: "yubikey", selectedSerial, slot, encryptedKeyPath };
|
|
2576
|
+
break;
|
|
2577
|
+
}
|
|
2578
|
+
default:
|
|
2579
|
+
throw new Error(`Unknown key storage type: ${keyStorageType}`);
|
|
2580
|
+
}
|
|
2581
|
+
log("");
|
|
2582
|
+
log("Generating Ed25519 keypair...");
|
|
2583
|
+
const keyPair = generateEd25519KeyPair();
|
|
2584
|
+
let privateKeyRef;
|
|
2585
|
+
let keyStorageDescription;
|
|
2586
|
+
switch (storageConfig.type) {
|
|
2587
|
+
case "file": {
|
|
2588
|
+
log("");
|
|
2589
|
+
info("Creating private key file on disk...");
|
|
2590
|
+
const keysDir = join(getAttestItConfigDir(), "keys");
|
|
2591
|
+
await mkdir(keysDir, { recursive: true });
|
|
2592
|
+
await writeFile(storageConfig.keyPath, keyPair.privateKey, { mode: 384 });
|
|
2593
|
+
privateKeyRef = { type: "file", path: storageConfig.keyPath };
|
|
2594
|
+
keyStorageDescription = storageConfig.keyPath;
|
|
2595
|
+
break;
|
|
2596
|
+
}
|
|
2597
|
+
case "keychain": {
|
|
2598
|
+
const { execFile } = await import('child_process');
|
|
2599
|
+
const { promisify } = await import('util');
|
|
2600
|
+
const execFileAsync = promisify(execFile);
|
|
2601
|
+
const encodedKey = Buffer.from(keyPair.privateKey).toString("base64");
|
|
2602
|
+
try {
|
|
2603
|
+
const addArgs = [
|
|
2604
|
+
"add-generic-password",
|
|
2605
|
+
"-a",
|
|
2606
|
+
"attest-it",
|
|
2607
|
+
"-s",
|
|
2608
|
+
storageConfig.keychainItemName,
|
|
2609
|
+
"-w",
|
|
2610
|
+
encodedKey,
|
|
2611
|
+
"-U",
|
|
2612
|
+
storageConfig.selectedKeychain.path
|
|
2613
|
+
];
|
|
2614
|
+
await execFileAsync("security", addArgs);
|
|
2615
|
+
} catch (err) {
|
|
2616
|
+
throw new Error(
|
|
2617
|
+
`Failed to store key in macOS Keychain: ${err instanceof Error ? err.message : String(err)}`
|
|
2618
|
+
);
|
|
2619
|
+
}
|
|
2620
|
+
privateKeyRef = {
|
|
2621
|
+
type: "keychain",
|
|
2622
|
+
service: storageConfig.keychainItemName,
|
|
2623
|
+
account: "attest-it",
|
|
2624
|
+
keychain: storageConfig.selectedKeychain.path
|
|
2625
|
+
};
|
|
2626
|
+
keyStorageDescription = `macOS Keychain: ${storageConfig.selectedKeychain.name}/${storageConfig.keychainItemName}`;
|
|
2627
|
+
break;
|
|
2628
|
+
}
|
|
2629
|
+
case "1password": {
|
|
2630
|
+
const { tmpdir } = await import('os');
|
|
2631
|
+
const tempDir = join(tmpdir(), `attest-it-${String(Date.now())}`);
|
|
2632
|
+
await mkdir(tempDir, { recursive: true });
|
|
2633
|
+
const tempPrivatePath = join(tempDir, "private.pem");
|
|
2634
|
+
try {
|
|
2635
|
+
await writeFile(tempPrivatePath, keyPair.privateKey, { mode: 384 });
|
|
2636
|
+
const { execFile } = await import('child_process');
|
|
2637
|
+
const { promisify } = await import('util');
|
|
2638
|
+
const execFileAsync = promisify(execFile);
|
|
2639
|
+
const opArgs = [
|
|
2640
|
+
"document",
|
|
2641
|
+
"create",
|
|
2642
|
+
tempPrivatePath,
|
|
2643
|
+
"--title",
|
|
2644
|
+
storageConfig.item,
|
|
2645
|
+
"--vault",
|
|
2646
|
+
storageConfig.selectedVault,
|
|
2647
|
+
"--account",
|
|
2648
|
+
storageConfig.selectedAccount
|
|
2649
|
+
];
|
|
2650
|
+
await execFileAsync("op", opArgs);
|
|
2651
|
+
} finally {
|
|
2652
|
+
const { rm } = await import('fs/promises');
|
|
2653
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => {
|
|
2654
|
+
});
|
|
2655
|
+
}
|
|
2656
|
+
privateKeyRef = {
|
|
2657
|
+
type: "1password",
|
|
2658
|
+
vault: storageConfig.selectedVault,
|
|
2659
|
+
item: storageConfig.item,
|
|
2660
|
+
account: storageConfig.selectedAccount
|
|
2661
|
+
};
|
|
2662
|
+
keyStorageDescription = `1Password (${storageConfig.accountDisplayName}/${storageConfig.selectedVault}/${storageConfig.item})`;
|
|
2663
|
+
break;
|
|
2664
|
+
}
|
|
2665
|
+
case "yubikey": {
|
|
3183
2666
|
const keysDir = join(getAttestItConfigDir(), "keys");
|
|
3184
2667
|
await mkdir(keysDir, { recursive: true });
|
|
3185
|
-
const encryptedKeyPath = join(keysDir, encryptedKeyName);
|
|
3186
2668
|
const result = await YubiKeyProvider.encryptPrivateKey({
|
|
3187
2669
|
privateKey: keyPair.privateKey,
|
|
3188
|
-
encryptedKeyPath,
|
|
3189
|
-
slot,
|
|
3190
|
-
serial: selectedSerial
|
|
2670
|
+
encryptedKeyPath: storageConfig.encryptedKeyPath,
|
|
2671
|
+
slot: storageConfig.slot,
|
|
2672
|
+
serial: storageConfig.selectedSerial
|
|
3191
2673
|
});
|
|
3192
2674
|
privateKeyRef = {
|
|
3193
2675
|
type: "yubikey",
|
|
3194
2676
|
encryptedKeyPath: result.encryptedKeyPath,
|
|
3195
|
-
slot,
|
|
3196
|
-
serial: selectedSerial
|
|
2677
|
+
slot: storageConfig.slot,
|
|
2678
|
+
serial: storageConfig.selectedSerial
|
|
3197
2679
|
};
|
|
3198
2680
|
keyStorageDescription = result.storageDescription;
|
|
3199
2681
|
break;
|
|
3200
2682
|
}
|
|
3201
|
-
default:
|
|
3202
|
-
throw new Error(`Unknown key storage type: ${keyStorageType}`);
|
|
3203
2683
|
}
|
|
3204
2684
|
const identity = {
|
|
3205
2685
|
name,
|
|
@@ -3243,9 +2723,9 @@ async function runCreate() {
|
|
|
3243
2723
|
log("");
|
|
3244
2724
|
log(theme3.blue.bold()("Public key saved to:"));
|
|
3245
2725
|
log(` ${publicKeyResult.homePath}`);
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
2726
|
+
log("");
|
|
2727
|
+
log("To add yourself to a project, run:");
|
|
2728
|
+
log(theme3.blue(" attest-it team join"));
|
|
3249
2729
|
log("");
|
|
3250
2730
|
if (!existingConfig) {
|
|
3251
2731
|
success(`Set as active identity`);
|
|
@@ -3366,155 +2846,6 @@ async function runShow(slug) {
|
|
|
3366
2846
|
process.exit(ExitCode.CONFIG_ERROR);
|
|
3367
2847
|
}
|
|
3368
2848
|
}
|
|
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
2849
|
|
|
3519
2850
|
// src/utils/format-key-location.ts
|
|
3520
2851
|
function formatKeyLocation(privateKey) {
|
|
@@ -3733,7 +3064,7 @@ async function runExport(slug) {
|
|
|
3733
3064
|
}
|
|
3734
3065
|
|
|
3735
3066
|
// 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(
|
|
3067
|
+
var identityCommand = new Command("identity").description("Manage local identities and keypairs").addCommand(listCommand).addCommand(createCommand).addCommand(useCommand).addCommand(showCommand).addCommand(removeCommand).addCommand(exportCommand);
|
|
3737
3068
|
var whoamiCommand = new Command("whoami").description("Show the current active identity").action(async () => {
|
|
3738
3069
|
await runWhoami();
|
|
3739
3070
|
});
|
|
@@ -3834,6 +3165,49 @@ async function runList2() {
|
|
|
3834
3165
|
process.exit(ExitCode.CONFIG_ERROR);
|
|
3835
3166
|
}
|
|
3836
3167
|
}
|
|
3168
|
+
async function promptForGateAuthorization(gates) {
|
|
3169
|
+
if (!gates || Object.keys(gates).length === 0) {
|
|
3170
|
+
return [];
|
|
3171
|
+
}
|
|
3172
|
+
const gateChoices = Object.entries(gates).map(([gateId, gate]) => ({
|
|
3173
|
+
name: `${gateId} - ${gate.name}`,
|
|
3174
|
+
value: gateId
|
|
3175
|
+
}));
|
|
3176
|
+
const authorizedGates = await checkbox({
|
|
3177
|
+
message: "Select gates to authorize (use space to select):",
|
|
3178
|
+
choices: gateChoices
|
|
3179
|
+
});
|
|
3180
|
+
return authorizedGates;
|
|
3181
|
+
}
|
|
3182
|
+
function addTeamMemberToConfig(config, memberSlug, memberData, authorizedGates) {
|
|
3183
|
+
const existingTeam = config.team ?? {};
|
|
3184
|
+
const updatedConfig = {
|
|
3185
|
+
...config,
|
|
3186
|
+
team: {
|
|
3187
|
+
...existingTeam,
|
|
3188
|
+
[memberSlug]: {
|
|
3189
|
+
name: memberData.name,
|
|
3190
|
+
publicKey: memberData.publicKey,
|
|
3191
|
+
publicKeyAlgorithm: memberData.publicKeyAlgorithm ?? "ed25519",
|
|
3192
|
+
...memberData.email ? { email: memberData.email } : {},
|
|
3193
|
+
...memberData.github ? { github: memberData.github } : {}
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
};
|
|
3197
|
+
if (authorizedGates.length > 0 && updatedConfig.gates) {
|
|
3198
|
+
for (const gateId of authorizedGates) {
|
|
3199
|
+
const gate = updatedConfig.gates[gateId];
|
|
3200
|
+
if (gate) {
|
|
3201
|
+
if (!gate.authorizedSigners.includes(memberSlug)) {
|
|
3202
|
+
gate.authorizedSigners.push(memberSlug);
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
return updatedConfig;
|
|
3208
|
+
}
|
|
3209
|
+
|
|
3210
|
+
// src/commands/team/add.ts
|
|
3837
3211
|
var addCommand = new Command("add").description("Add a new team member").action(async () => {
|
|
3838
3212
|
await runAdd();
|
|
3839
3213
|
});
|
|
@@ -3905,45 +3279,22 @@ async function runAdd() {
|
|
|
3905
3279
|
message: "Public key:",
|
|
3906
3280
|
validate: validatePublicKey
|
|
3907
3281
|
});
|
|
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 = {
|
|
3282
|
+
log("");
|
|
3283
|
+
const authorizedGates = await promptForGateAuthorization(attestItConfig.gates);
|
|
3284
|
+
const memberData = {
|
|
3921
3285
|
name,
|
|
3922
|
-
publicKey: publicKey.trim()
|
|
3286
|
+
publicKey: publicKey.trim(),
|
|
3287
|
+
publicKeyAlgorithm: "ed25519"
|
|
3923
3288
|
};
|
|
3924
|
-
|
|
3925
|
-
|
|
3289
|
+
const trimmedEmail = email.trim();
|
|
3290
|
+
const trimmedGithub = github.trim();
|
|
3291
|
+
if (trimmedEmail && trimmedEmail.length > 0) {
|
|
3292
|
+
memberData.email = trimmedEmail;
|
|
3926
3293
|
}
|
|
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
|
-
}
|
|
3294
|
+
if (trimmedGithub && trimmedGithub.length > 0) {
|
|
3295
|
+
memberData.github = trimmedGithub;
|
|
3946
3296
|
}
|
|
3297
|
+
const updatedConfig = addTeamMemberToConfig(config, slug, memberData, authorizedGates);
|
|
3947
3298
|
const configPath = findConfigPath();
|
|
3948
3299
|
if (!configPath) {
|
|
3949
3300
|
error("Configuration file not found");
|
|
@@ -3966,127 +3317,73 @@ async function runAdd() {
|
|
|
3966
3317
|
process.exit(ExitCode.CONFIG_ERROR);
|
|
3967
3318
|
}
|
|
3968
3319
|
}
|
|
3969
|
-
var
|
|
3970
|
-
await
|
|
3320
|
+
var joinCommand = new Command("join").description("Add yourself to the project team using your active identity").action(async () => {
|
|
3321
|
+
await runJoin();
|
|
3971
3322
|
});
|
|
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) {
|
|
3323
|
+
async function runJoin() {
|
|
3994
3324
|
try {
|
|
3995
3325
|
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
3326
|
log("");
|
|
4004
|
-
log(theme3.blue.bold()(
|
|
3327
|
+
log(theme3.blue.bold()("Join Project Team"));
|
|
4005
3328
|
log("");
|
|
4006
|
-
|
|
3329
|
+
const localConfig = await loadLocalConfig();
|
|
3330
|
+
if (!localConfig) {
|
|
3331
|
+
error('No identity found. Run "attest-it identity create" first.');
|
|
3332
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
3333
|
+
}
|
|
3334
|
+
const activeIdentity = getActiveIdentity(localConfig);
|
|
3335
|
+
if (!activeIdentity) {
|
|
3336
|
+
error('No active identity. Run "attest-it identity use <slug>" to select one.');
|
|
3337
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
3338
|
+
}
|
|
3339
|
+
const activeSlug = localConfig.activeIdentity;
|
|
3340
|
+
info(`Using identity: ${activeSlug}`);
|
|
3341
|
+
log(` Name: ${activeIdentity.name}`);
|
|
3342
|
+
log(` Public Key: ${activeIdentity.publicKey.slice(0, 32)}...`);
|
|
4007
3343
|
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
|
-
});
|
|
3344
|
+
const config = await loadConfig();
|
|
3345
|
+
const attestItConfig = toAttestItConfig(config);
|
|
3346
|
+
const existingTeam = attestItConfig.team ?? {};
|
|
3347
|
+
const existingMemberWithKey = Object.entries(existingTeam).find(
|
|
3348
|
+
([, member]) => member.publicKey === activeIdentity.publicKey
|
|
3349
|
+
);
|
|
3350
|
+
if (existingMemberWithKey) {
|
|
3351
|
+
error(`You're already a team member as "${existingMemberWithKey[0]}"`);
|
|
3352
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
4039
3353
|
}
|
|
4040
|
-
|
|
4041
|
-
if (
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
3354
|
+
let slug = activeSlug;
|
|
3355
|
+
if (existingTeam[slug]) {
|
|
3356
|
+
log(`Slug "${slug}" is already taken by another team member.`);
|
|
3357
|
+
slug = await input({
|
|
3358
|
+
message: "Choose a different slug:",
|
|
3359
|
+
validate: (value) => {
|
|
3360
|
+
if (!value || value.trim().length === 0) {
|
|
3361
|
+
return "Slug cannot be empty";
|
|
3362
|
+
}
|
|
3363
|
+
if (!/^[a-z0-9-]+$/.test(value)) {
|
|
3364
|
+
return "Slug must contain only lowercase letters, numbers, and hyphens";
|
|
3365
|
+
}
|
|
3366
|
+
if (existingTeam[value]) {
|
|
3367
|
+
return `Slug "${value}" is already taken`;
|
|
3368
|
+
}
|
|
3369
|
+
return true;
|
|
4045
3370
|
}
|
|
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
3371
|
});
|
|
4060
3372
|
}
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
3373
|
+
log("");
|
|
3374
|
+
const authorizedGates = await promptForGateAuthorization(attestItConfig.gates);
|
|
3375
|
+
const memberData = {
|
|
3376
|
+
name: activeIdentity.name,
|
|
3377
|
+
publicKey: activeIdentity.publicKey,
|
|
3378
|
+
publicKeyAlgorithm: "ed25519"
|
|
4064
3379
|
};
|
|
4065
|
-
if (email
|
|
4066
|
-
|
|
3380
|
+
if (activeIdentity.email) {
|
|
3381
|
+
memberData.email = activeIdentity.email;
|
|
4067
3382
|
}
|
|
4068
|
-
if (github
|
|
4069
|
-
|
|
4070
|
-
}
|
|
4071
|
-
const updatedConfig = {
|
|
4072
|
-
...config,
|
|
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
|
-
}
|
|
3383
|
+
if (activeIdentity.github) {
|
|
3384
|
+
memberData.github = activeIdentity.github;
|
|
4089
3385
|
}
|
|
3386
|
+
const updatedConfig = addTeamMemberToConfig(config, slug, memberData, authorizedGates);
|
|
4090
3387
|
const configPath = findConfigPath();
|
|
4091
3388
|
if (!configPath) {
|
|
4092
3389
|
error("Configuration file not found");
|
|
@@ -4095,11 +3392,9 @@ async function runEdit2(slug) {
|
|
|
4095
3392
|
const yamlContent = stringify(updatedConfig);
|
|
4096
3393
|
await writeFile(configPath, yamlContent, "utf8");
|
|
4097
3394
|
log("");
|
|
4098
|
-
success(`Team member "${slug}"
|
|
4099
|
-
if (
|
|
4100
|
-
log(`Authorized for gates: ${
|
|
4101
|
-
} else {
|
|
4102
|
-
log("Not authorized for any gates");
|
|
3395
|
+
success(`Team member "${slug}" added successfully`);
|
|
3396
|
+
if (authorizedGates.length > 0) {
|
|
3397
|
+
log(`Authorized for gates: ${authorizedGates.join(", ")}`);
|
|
4103
3398
|
}
|
|
4104
3399
|
log("");
|
|
4105
3400
|
} catch (err) {
|
|
@@ -4214,7 +3509,7 @@ async function runRemove2(slug, options) {
|
|
|
4214
3509
|
}
|
|
4215
3510
|
|
|
4216
3511
|
// src/commands/team/index.ts
|
|
4217
|
-
var teamCommand = new Command("team").description("Manage team members and authorizations").addCommand(listCommand2).addCommand(addCommand).addCommand(
|
|
3512
|
+
var teamCommand = new Command("team").description("Manage team members and authorizations").addCommand(listCommand2).addCommand(addCommand).addCommand(joinCommand).addCommand(removeCommand2);
|
|
4218
3513
|
var PROGRAM_NAME2 = "attest-it";
|
|
4219
3514
|
var PROGRAM_ALIAS2 = "attest";
|
|
4220
3515
|
var PROGRAM_NAMES = [PROGRAM_NAME2, PROGRAM_ALIAS2];
|
|
@@ -4235,7 +3530,6 @@ async function getCompletions(env) {
|
|
|
4235
3530
|
{ name: "run", description: "Run test suites interactively" },
|
|
4236
3531
|
{ name: "verify", description: "Verify all seals are valid" },
|
|
4237
3532
|
{ name: "seal", description: "Create a seal for a gate" },
|
|
4238
|
-
{ name: "keygen", description: "Generate a new keypair" },
|
|
4239
3533
|
{ name: "prune", description: "Remove stale attestations" },
|
|
4240
3534
|
{ name: "identity", description: "Manage identities" },
|
|
4241
3535
|
{ name: "team", description: "Manage team members" },
|
|
@@ -4340,7 +3634,6 @@ async function getCompletions(env) {
|
|
|
4340
3634
|
"run",
|
|
4341
3635
|
"verify",
|
|
4342
3636
|
"seal",
|
|
4343
|
-
"keygen",
|
|
4344
3637
|
"prune",
|
|
4345
3638
|
"identity",
|
|
4346
3639
|
"team",
|
|
@@ -4475,43 +3768,12 @@ function createCompletionServerCommand() {
|
|
|
4475
3768
|
}
|
|
4476
3769
|
});
|
|
4477
3770
|
}
|
|
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
3771
|
var program = new Command();
|
|
4509
3772
|
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
3773
|
program.option("-V, --version", "output the version number");
|
|
4511
3774
|
program.addCommand(initCommand);
|
|
4512
3775
|
program.addCommand(statusCommand);
|
|
4513
3776
|
program.addCommand(runCommand);
|
|
4514
|
-
program.addCommand(keygenCommand);
|
|
4515
3777
|
program.addCommand(pruneCommand);
|
|
4516
3778
|
program.addCommand(verifyCommand);
|
|
4517
3779
|
program.addCommand(sealCommand);
|
|
@@ -4553,6 +3815,7 @@ async function run() {
|
|
|
4553
3815
|
}
|
|
4554
3816
|
|
|
4555
3817
|
// bin/attest-it.ts
|
|
3818
|
+
tryDelegateToLocal();
|
|
4556
3819
|
void run();
|
|
4557
3820
|
//# sourceMappingURL=attest-it.js.map
|
|
4558
3821
|
//# sourceMappingURL=attest-it.js.map
|