@attest-it/cli 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -4
- package/dist/bin/attest-it.js +2251 -249
- package/dist/bin/attest-it.js.map +1 -1
- package/dist/index.cjs +2246 -246
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2251 -249
- package/dist/index.js.map +1 -1
- package/package.json +9 -3
package/dist/bin/attest-it.js
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import * as fs from 'fs';
|
|
4
|
+
import { readFileSync } from 'fs';
|
|
4
5
|
import * as path from 'path';
|
|
5
6
|
import { join, dirname } from 'path';
|
|
6
7
|
import { detectTheme } from 'chromaterm';
|
|
7
|
-
import { confirm } from '@inquirer/prompts';
|
|
8
|
-
import { loadConfig,
|
|
8
|
+
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, checkOpenSSL, getDefaultPublicKeyPath, OnePasswordKeyProvider, MacOSKeychainKeyProvider, generateKeyPair, setKeyPermissions, getGate, loadLocalConfig, generateEd25519KeyPair, saveLocalConfig, findConfigPath, findAttestation } from '@attest-it/core';
|
|
9
10
|
import { spawn } from 'child_process';
|
|
10
11
|
import * as os from 'os';
|
|
12
|
+
import { homedir } from 'os';
|
|
11
13
|
import { parse } from 'shell-quote';
|
|
12
14
|
import * as React7 from 'react';
|
|
15
|
+
import { useState, useEffect } from 'react';
|
|
13
16
|
import { render, useApp, Box, Text, useInput } from 'ink';
|
|
14
17
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
15
|
-
import {
|
|
18
|
+
import { mkdir, writeFile, unlink, readFile } from 'fs/promises';
|
|
19
|
+
import { Spinner, Select, TextInput } from '@inkjs/ui';
|
|
20
|
+
import { stringify } from 'yaml';
|
|
21
|
+
import { fileURLToPath } from 'url';
|
|
16
22
|
|
|
17
23
|
var globalOptions = {};
|
|
18
24
|
var theme;
|
|
@@ -90,30 +96,63 @@ function formatTable(rows) {
|
|
|
90
96
|
}
|
|
91
97
|
return lines.join("\n");
|
|
92
98
|
}
|
|
93
|
-
function colorizeStatus(status) {
|
|
94
|
-
const t = getTheme();
|
|
95
|
-
switch (status) {
|
|
96
|
-
case "VALID":
|
|
97
|
-
return t.green(status);
|
|
98
|
-
case "NEEDS_ATTESTATION":
|
|
99
|
-
case "FINGERPRINT_CHANGED":
|
|
100
|
-
return t.yellow(status);
|
|
101
|
-
case "EXPIRED":
|
|
102
|
-
case "INVALIDATED_BY_PARENT":
|
|
103
|
-
return t.red(status);
|
|
104
|
-
case "SIGNATURE_INVALID":
|
|
105
|
-
return t.red.bold()(status);
|
|
106
|
-
default:
|
|
107
|
-
return status;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
99
|
function outputJson(data) {
|
|
111
100
|
console.log(JSON.stringify(data, null, 2));
|
|
112
101
|
}
|
|
102
|
+
var BOX_CHARS = {
|
|
103
|
+
topLeft: "\u250C",
|
|
104
|
+
topRight: "\u2510",
|
|
105
|
+
bottomLeft: "\u2514",
|
|
106
|
+
bottomRight: "\u2518",
|
|
107
|
+
horizontal: "\u2500",
|
|
108
|
+
vertical: "\u2502"};
|
|
109
|
+
var theme2;
|
|
110
|
+
function getTheme2() {
|
|
111
|
+
if (!theme2) {
|
|
112
|
+
const noopFn = (str) => str;
|
|
113
|
+
const chainable = () => noopFn;
|
|
114
|
+
theme2 = {
|
|
115
|
+
red: Object.assign(noopFn, { bold: chainable, dim: chainable }),
|
|
116
|
+
green: Object.assign(noopFn, { bold: chainable, dim: chainable }),
|
|
117
|
+
yellow: Object.assign(noopFn, { bold: chainable, dim: chainable }),
|
|
118
|
+
blue: Object.assign(noopFn, { bold: chainable, dim: chainable }),
|
|
119
|
+
success: noopFn,
|
|
120
|
+
error: noopFn,
|
|
121
|
+
warning: noopFn,
|
|
122
|
+
info: noopFn,
|
|
123
|
+
muted: noopFn
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return theme2;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// src/utils/prompts.ts
|
|
113
130
|
async function confirmAction(options) {
|
|
131
|
+
const theme3 = getTheme2();
|
|
132
|
+
const defaultIndicator = options.default ? "(Y/n)" : "(y/N)";
|
|
133
|
+
const message = `${options.message}? ${defaultIndicator}`;
|
|
134
|
+
const boxWidth = Math.max(message.length + 2, 40);
|
|
135
|
+
const contentPadding = " ".repeat(boxWidth - message.length - 1);
|
|
136
|
+
const topBorder = theme3.yellow(
|
|
137
|
+
`${BOX_CHARS.topLeft}${BOX_CHARS.horizontal.repeat(boxWidth)}${BOX_CHARS.topRight}`
|
|
138
|
+
);
|
|
139
|
+
const bottomBorder = theme3.yellow(
|
|
140
|
+
`${BOX_CHARS.bottomLeft}${BOX_CHARS.horizontal.repeat(boxWidth)}${BOX_CHARS.bottomRight}`
|
|
141
|
+
);
|
|
142
|
+
const contentLine = theme3.yellow(BOX_CHARS.vertical) + ` ${message}${contentPadding}` + theme3.yellow(BOX_CHARS.vertical);
|
|
143
|
+
console.log("");
|
|
144
|
+
console.log(topBorder);
|
|
145
|
+
console.log(contentLine);
|
|
146
|
+
console.log(bottomBorder);
|
|
147
|
+
console.log("");
|
|
114
148
|
return confirm({
|
|
115
|
-
message:
|
|
116
|
-
|
|
149
|
+
message: "",
|
|
150
|
+
// Empty message since we displayed it above
|
|
151
|
+
default: options.default ?? false,
|
|
152
|
+
theme: {
|
|
153
|
+
prefix: ""
|
|
154
|
+
// Remove default prefix
|
|
155
|
+
}
|
|
117
156
|
});
|
|
118
157
|
}
|
|
119
158
|
|
|
@@ -204,71 +243,68 @@ async function runInit(options) {
|
|
|
204
243
|
process.exit(ExitCode.CONFIG_ERROR);
|
|
205
244
|
}
|
|
206
245
|
}
|
|
207
|
-
var statusCommand = new Command("status").description("Show
|
|
208
|
-
await runStatus(options);
|
|
246
|
+
var statusCommand = new Command("status").description("Show seal status for all gates").argument("[gates...]", "Show status for specific gates only").option("--json", "Output JSON for machine parsing").action(async (gates, options) => {
|
|
247
|
+
await runStatus(gates, options);
|
|
209
248
|
});
|
|
210
|
-
async function runStatus(options) {
|
|
249
|
+
async function runStatus(gates, options) {
|
|
211
250
|
try {
|
|
212
251
|
const config = await loadConfig();
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
attestationsFile = await readAttestations(attestationsPath);
|
|
217
|
-
} catch (err) {
|
|
218
|
-
if (err instanceof Error && !err.message.includes("ENOENT")) {
|
|
219
|
-
throw err;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
const attestations = attestationsFile?.attestations ?? [];
|
|
223
|
-
const suiteNames = options.suite ? [options.suite] : Object.keys(config.suites);
|
|
224
|
-
if (options.suite && !config.suites[options.suite]) {
|
|
225
|
-
error(`Suite "${options.suite}" not found in config`);
|
|
252
|
+
const attestItConfig = toAttestItConfig(config);
|
|
253
|
+
if (!attestItConfig.gates || Object.keys(attestItConfig.gates).length === 0) {
|
|
254
|
+
error("No gates defined in configuration");
|
|
226
255
|
process.exit(ExitCode.CONFIG_ERROR);
|
|
227
256
|
}
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (!
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
);
|
|
245
|
-
const status = determineStatus(
|
|
246
|
-
attestation ?? null,
|
|
247
|
-
fingerprintResult.fingerprint,
|
|
248
|
-
config.settings.maxAgeDays
|
|
249
|
-
);
|
|
250
|
-
let age;
|
|
251
|
-
if (attestation) {
|
|
252
|
-
const attestedAt = new Date(attestation.attestedAt);
|
|
253
|
-
age = Math.floor((Date.now() - attestedAt.getTime()) / (1e3 * 60 * 60 * 24));
|
|
254
|
-
}
|
|
255
|
-
if (status !== "VALID") {
|
|
256
|
-
hasInvalid = true;
|
|
257
|
-
}
|
|
258
|
-
results.push({
|
|
259
|
-
name: suiteName,
|
|
260
|
-
status,
|
|
261
|
-
currentFingerprint: fingerprintResult.fingerprint,
|
|
262
|
-
attestedFingerprint: attestation?.fingerprint,
|
|
263
|
-
attestedAt: attestation?.attestedAt,
|
|
264
|
-
age
|
|
257
|
+
const projectRoot = process.cwd();
|
|
258
|
+
const sealsFile = readSealsSync(projectRoot);
|
|
259
|
+
const gatesToCheck = gates.length > 0 ? gates : Object.keys(attestItConfig.gates);
|
|
260
|
+
for (const gateId of gatesToCheck) {
|
|
261
|
+
if (!attestItConfig.gates[gateId]) {
|
|
262
|
+
error(`Gate '${gateId}' not found in configuration`);
|
|
263
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const fingerprints = {};
|
|
267
|
+
for (const gateId of gatesToCheck) {
|
|
268
|
+
const gate = attestItConfig.gates[gateId];
|
|
269
|
+
if (!gate) continue;
|
|
270
|
+
const result = computeFingerprintSync({
|
|
271
|
+
packages: gate.fingerprint.paths,
|
|
272
|
+
...gate.fingerprint.exclude && { ignore: gate.fingerprint.exclude }
|
|
265
273
|
});
|
|
274
|
+
fingerprints[gateId] = result.fingerprint;
|
|
266
275
|
}
|
|
276
|
+
const verificationResults = gates.length > 0 ? gatesToCheck.map(
|
|
277
|
+
(gateId) => (
|
|
278
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
279
|
+
verifyGateSeal(attestItConfig, gateId, sealsFile, fingerprints[gateId] ?? "")
|
|
280
|
+
)
|
|
281
|
+
) : verifyAllSeals(attestItConfig, sealsFile, fingerprints);
|
|
282
|
+
const results = verificationResults.map((result) => {
|
|
283
|
+
const status = {
|
|
284
|
+
gateId: result.gateId,
|
|
285
|
+
state: result.state,
|
|
286
|
+
currentFingerprint: fingerprints[result.gateId] ?? "",
|
|
287
|
+
message: result.message
|
|
288
|
+
};
|
|
289
|
+
if (result.seal) {
|
|
290
|
+
status.sealedFingerprint = result.seal.fingerprint;
|
|
291
|
+
status.sealedBy = result.seal.sealedBy;
|
|
292
|
+
status.sealedAt = result.seal.timestamp;
|
|
293
|
+
const timestamp = new Date(result.seal.timestamp);
|
|
294
|
+
const now = Date.now();
|
|
295
|
+
const ageMs = now - timestamp.getTime();
|
|
296
|
+
status.age = Math.floor(ageMs / (1e3 * 60 * 60 * 24));
|
|
297
|
+
}
|
|
298
|
+
return status;
|
|
299
|
+
});
|
|
267
300
|
if (options.json) {
|
|
268
301
|
outputJson(results);
|
|
269
302
|
} else {
|
|
270
|
-
displayStatusTable(results
|
|
303
|
+
displayStatusTable(results);
|
|
271
304
|
}
|
|
305
|
+
const hasInvalid = results.some(
|
|
306
|
+
(r) => r.state === "MISSING" || r.state === "FINGERPRINT_MISMATCH" || r.state === "INVALID_SIGNATURE" || r.state === "UNKNOWN_SIGNER" || r.state === "STALE"
|
|
307
|
+
);
|
|
272
308
|
process.exit(hasInvalid ? ExitCode.FAILURE : ExitCode.SUCCESS);
|
|
273
309
|
} catch (err) {
|
|
274
310
|
if (err instanceof Error) {
|
|
@@ -279,50 +315,73 @@ async function runStatus(options) {
|
|
|
279
315
|
process.exit(ExitCode.CONFIG_ERROR);
|
|
280
316
|
}
|
|
281
317
|
}
|
|
282
|
-
function
|
|
283
|
-
if (!attestation) {
|
|
284
|
-
return "NEEDS_ATTESTATION";
|
|
285
|
-
}
|
|
286
|
-
if (attestation.fingerprint !== currentFingerprint) {
|
|
287
|
-
return "FINGERPRINT_CHANGED";
|
|
288
|
-
}
|
|
289
|
-
const attestedAt = new Date(attestation.attestedAt);
|
|
290
|
-
const ageInDays = Math.floor((Date.now() - attestedAt.getTime()) / (1e3 * 60 * 60 * 24));
|
|
291
|
-
if (ageInDays > maxAgeDays) {
|
|
292
|
-
return "EXPIRED";
|
|
293
|
-
}
|
|
294
|
-
return "VALID";
|
|
295
|
-
}
|
|
296
|
-
function displayStatusTable(results, hasInvalid) {
|
|
318
|
+
function displayStatusTable(results) {
|
|
297
319
|
const tableRows = results.map((r) => ({
|
|
298
|
-
suite: r.
|
|
299
|
-
status:
|
|
320
|
+
suite: r.gateId,
|
|
321
|
+
status: colorizeState(r.state),
|
|
300
322
|
fingerprint: r.currentFingerprint.slice(0, 16) + "...",
|
|
301
323
|
age: formatAge(r)
|
|
302
324
|
}));
|
|
303
325
|
log("");
|
|
304
326
|
log(formatTable(tableRows));
|
|
305
327
|
log("");
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
328
|
+
const sealed = results.filter((r) => r.sealedBy && r.sealedAt);
|
|
329
|
+
if (sealed.length > 0) {
|
|
330
|
+
log("Seal metadata:");
|
|
331
|
+
for (const result of sealed) {
|
|
332
|
+
log(` ${result.gateId}:`);
|
|
333
|
+
log(` Sealed by: ${result.sealedBy ?? "unknown"}`);
|
|
334
|
+
if (result.sealedAt) {
|
|
335
|
+
const date = new Date(result.sealedAt);
|
|
336
|
+
log(` Sealed at: ${date.toLocaleString()}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
log("");
|
|
310
340
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
341
|
+
const withIssues = results.filter((r) => r.state !== "VALID" && r.message);
|
|
342
|
+
if (withIssues.length > 0) {
|
|
343
|
+
log("Issues:");
|
|
344
|
+
for (const result of withIssues) {
|
|
345
|
+
log(` ${result.gateId}: ${result.message ?? "Unknown issue"}`);
|
|
346
|
+
}
|
|
347
|
+
log("");
|
|
315
348
|
}
|
|
316
|
-
|
|
317
|
-
|
|
349
|
+
const validCount = results.filter((r) => r.state === "VALID").length;
|
|
350
|
+
const invalidCount = results.length - validCount;
|
|
351
|
+
if (invalidCount === 0) {
|
|
352
|
+
success("All gate seals valid");
|
|
353
|
+
} else {
|
|
354
|
+
log(`Run 'attest-it seal' to create or update seals`);
|
|
318
355
|
}
|
|
319
|
-
|
|
320
|
-
|
|
356
|
+
}
|
|
357
|
+
function colorizeState(state) {
|
|
358
|
+
const theme3 = getTheme();
|
|
359
|
+
switch (state) {
|
|
360
|
+
case "VALID":
|
|
361
|
+
return theme3.green(state);
|
|
362
|
+
case "MISSING":
|
|
363
|
+
case "STALE":
|
|
364
|
+
return theme3.yellow(state);
|
|
365
|
+
case "FINGERPRINT_MISMATCH":
|
|
366
|
+
case "INVALID_SIGNATURE":
|
|
367
|
+
case "UNKNOWN_SIGNER":
|
|
368
|
+
return theme3.red(state);
|
|
369
|
+
default:
|
|
370
|
+
return state;
|
|
321
371
|
}
|
|
322
|
-
|
|
323
|
-
|
|
372
|
+
}
|
|
373
|
+
function formatAge(result) {
|
|
374
|
+
if (result.state === "VALID" || result.state === "STALE") {
|
|
375
|
+
return `${String(result.age ?? 0)} days${result.state === "STALE" ? " (stale)" : ""}`;
|
|
376
|
+
}
|
|
377
|
+
switch (result.state) {
|
|
378
|
+
case "MISSING":
|
|
379
|
+
return "(none)";
|
|
380
|
+
case "FINGERPRINT_MISMATCH":
|
|
381
|
+
return "(changed)";
|
|
382
|
+
default:
|
|
383
|
+
return "-";
|
|
324
384
|
}
|
|
325
|
-
return "-";
|
|
326
385
|
}
|
|
327
386
|
function Header({ pendingCount }) {
|
|
328
387
|
const message = `${pendingCount.toString()} suite${pendingCount === 1 ? "" : "s"} need${pendingCount === 1 ? "s" : ""} attestation`;
|
|
@@ -405,14 +464,14 @@ function SelectionPrompt({
|
|
|
405
464
|
onSelect,
|
|
406
465
|
groups
|
|
407
466
|
}) {
|
|
408
|
-
useInput((
|
|
409
|
-
const matchedOption = options.find((opt) => opt.hint ===
|
|
467
|
+
useInput((input5) => {
|
|
468
|
+
const matchedOption = options.find((opt) => opt.hint === input5);
|
|
410
469
|
if (matchedOption) {
|
|
411
470
|
onSelect(matchedOption.value);
|
|
412
471
|
return;
|
|
413
472
|
}
|
|
414
473
|
if (groups) {
|
|
415
|
-
const matchedGroup = groups.find((group) => group.name ===
|
|
474
|
+
const matchedGroup = groups.find((group) => group.name === input5);
|
|
416
475
|
if (matchedGroup) {
|
|
417
476
|
onSelect(matchedGroup.name);
|
|
418
477
|
}
|
|
@@ -462,17 +521,17 @@ function SuiteSelector({
|
|
|
462
521
|
return next;
|
|
463
522
|
});
|
|
464
523
|
}, []);
|
|
465
|
-
useInput((
|
|
466
|
-
if (
|
|
524
|
+
useInput((input5, key) => {
|
|
525
|
+
if (input5 === "a") {
|
|
467
526
|
setSelectedSuites(new Set(pendingSuites.map((s) => s.name)));
|
|
468
527
|
return;
|
|
469
528
|
}
|
|
470
|
-
if (
|
|
529
|
+
if (input5 === "n") {
|
|
471
530
|
onExit();
|
|
472
531
|
return;
|
|
473
532
|
}
|
|
474
|
-
if (/^[1-9]$/.test(
|
|
475
|
-
const idx = parseInt(
|
|
533
|
+
if (/^[1-9]$/.test(input5)) {
|
|
534
|
+
const idx = parseInt(input5, 10) - 1;
|
|
476
535
|
if (idx < pendingSuites.length) {
|
|
477
536
|
const suite = pendingSuites[idx];
|
|
478
537
|
if (suite) {
|
|
@@ -481,8 +540,8 @@ function SuiteSelector({
|
|
|
481
540
|
}
|
|
482
541
|
return;
|
|
483
542
|
}
|
|
484
|
-
if (
|
|
485
|
-
const groupIdx = parseInt(
|
|
543
|
+
if (input5.startsWith("g") && groups) {
|
|
544
|
+
const groupIdx = parseInt(input5.slice(1), 10) - 1;
|
|
486
545
|
const groupNames = Object.keys(groups);
|
|
487
546
|
if (groupIdx >= 0 && groupIdx < groupNames.length) {
|
|
488
547
|
const groupName = groupNames[groupIdx];
|
|
@@ -499,7 +558,7 @@ function SuiteSelector({
|
|
|
499
558
|
onSelect(Array.from(selectedSuites));
|
|
500
559
|
return;
|
|
501
560
|
}
|
|
502
|
-
if (
|
|
561
|
+
if (input5 === " ") {
|
|
503
562
|
const currentSuite = pendingSuites[cursorIndex];
|
|
504
563
|
if (currentSuite) {
|
|
505
564
|
toggleSuite(currentSuite.name);
|
|
@@ -632,11 +691,11 @@ function TestRunner({
|
|
|
632
691
|
};
|
|
633
692
|
}, [currentIndex, phase, suites, executeTest, onComplete]);
|
|
634
693
|
useInput(
|
|
635
|
-
(
|
|
694
|
+
(input5, key) => {
|
|
636
695
|
if (phase !== "confirming") return;
|
|
637
696
|
const currentSuite2 = suites[currentIndex];
|
|
638
697
|
if (!currentSuite2) return;
|
|
639
|
-
if (
|
|
698
|
+
if (input5.toLowerCase() === "y" || key.return) {
|
|
640
699
|
createAttestation3(currentSuite2).then(() => {
|
|
641
700
|
setResults((prev) => ({
|
|
642
701
|
...prev,
|
|
@@ -653,7 +712,7 @@ function TestRunner({
|
|
|
653
712
|
setPhase("running");
|
|
654
713
|
});
|
|
655
714
|
}
|
|
656
|
-
if (
|
|
715
|
+
if (input5.toLowerCase() === "n") {
|
|
657
716
|
setResults((prev) => ({
|
|
658
717
|
...prev,
|
|
659
718
|
skipped: [...prev.skipped, currentSuite2]
|
|
@@ -822,7 +881,7 @@ function InteractiveRun({
|
|
|
822
881
|
] })
|
|
823
882
|
] });
|
|
824
883
|
}
|
|
825
|
-
function
|
|
884
|
+
function determineStatus(attestation, currentFingerprint, maxAgeDays) {
|
|
826
885
|
if (!attestation) {
|
|
827
886
|
return "NEEDS_ATTESTATION";
|
|
828
887
|
}
|
|
@@ -848,6 +907,9 @@ async function getAllSuiteStatuses(config) {
|
|
|
848
907
|
const attestations = attestationsFile?.attestations ?? [];
|
|
849
908
|
const results = [];
|
|
850
909
|
for (const [suiteName, suiteConfig] of Object.entries(config.suites)) {
|
|
910
|
+
if (!suiteConfig.packages) {
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
851
913
|
const fingerprintResult = await computeFingerprint({
|
|
852
914
|
packages: suiteConfig.packages,
|
|
853
915
|
...suiteConfig.ignore && { ignore: suiteConfig.ignore }
|
|
@@ -856,7 +918,7 @@ async function getAllSuiteStatuses(config) {
|
|
|
856
918
|
{ schemaVersion: "1", attestations, signature: "" },
|
|
857
919
|
suiteName
|
|
858
920
|
);
|
|
859
|
-
const status =
|
|
921
|
+
const status = determineStatus(
|
|
860
922
|
attestation,
|
|
861
923
|
fingerprintResult.fingerprint,
|
|
862
924
|
config.settings.maxAgeDays
|
|
@@ -1013,6 +1075,9 @@ function createAttestationCreator(config) {
|
|
|
1013
1075
|
if (!suiteConfig) {
|
|
1014
1076
|
throw new Error(`Suite "${suiteName}" not found`);
|
|
1015
1077
|
}
|
|
1078
|
+
if (!suiteConfig.packages) {
|
|
1079
|
+
throw new Error(`Suite "${suiteName}" has no packages defined`);
|
|
1080
|
+
}
|
|
1016
1081
|
const fingerprintResult = await computeFingerprint({
|
|
1017
1082
|
packages: suiteConfig.packages,
|
|
1018
1083
|
...suiteConfig.ignore && { ignore: suiteConfig.ignore }
|
|
@@ -1027,14 +1092,34 @@ function createAttestationCreator(config) {
|
|
|
1027
1092
|
const existingFile = await readAttestations(attestationsPath).catch(() => null);
|
|
1028
1093
|
const existingAttestations = existingFile?.attestations ?? [];
|
|
1029
1094
|
const newAttestations = upsertAttestation(existingAttestations, attestation);
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1095
|
+
let keyProvider;
|
|
1096
|
+
let keyRef;
|
|
1097
|
+
if (config.settings.keyProvider) {
|
|
1098
|
+
keyProvider = KeyProviderRegistry.create({
|
|
1099
|
+
type: config.settings.keyProvider.type,
|
|
1100
|
+
options: config.settings.keyProvider.options ?? {}
|
|
1101
|
+
});
|
|
1102
|
+
if (config.settings.keyProvider.type === "filesystem") {
|
|
1103
|
+
keyRef = config.settings.keyProvider.options?.privateKeyPath ?? getDefaultPrivateKeyPath();
|
|
1104
|
+
} else if (config.settings.keyProvider.type === "1password") {
|
|
1105
|
+
keyRef = config.settings.keyProvider.options?.itemName ?? "attest-it-private-key";
|
|
1106
|
+
} else {
|
|
1107
|
+
throw new Error(`Unsupported key provider type: ${config.settings.keyProvider.type}`);
|
|
1108
|
+
}
|
|
1109
|
+
} else {
|
|
1110
|
+
keyProvider = new FilesystemKeyProvider();
|
|
1111
|
+
keyRef = getDefaultPrivateKeyPath();
|
|
1112
|
+
}
|
|
1113
|
+
if (!await keyProvider.keyExists(keyRef)) {
|
|
1114
|
+
const providerName = keyProvider.displayName;
|
|
1115
|
+
const keygenMessage = keyProvider.type === "filesystem" ? 'Run "attest-it keygen" first to generate a keypair.' : 'Run "attest-it keygen" to generate and store a key.';
|
|
1116
|
+
throw new Error(`Private key not found in ${providerName}. ${keygenMessage}`);
|
|
1033
1117
|
}
|
|
1034
1118
|
await writeSignedAttestations({
|
|
1035
1119
|
filePath: attestationsPath,
|
|
1036
1120
|
attestations: newAttestations,
|
|
1037
|
-
|
|
1121
|
+
keyProvider,
|
|
1122
|
+
keyRef
|
|
1038
1123
|
});
|
|
1039
1124
|
log(`\u2713 Attestation created for ${suiteName}`);
|
|
1040
1125
|
};
|
|
@@ -1097,7 +1182,7 @@ async function checkDirtyWorkingTree() {
|
|
|
1097
1182
|
}
|
|
1098
1183
|
|
|
1099
1184
|
// src/commands/run.ts
|
|
1100
|
-
var runCommand = new Command("run").description("Execute tests and create attestation").option("-s, --suite <name>", "Run specific suite (required unless --all or interactive mode)").option("-a, --all", "Run all suites needing attestation").option("--no-attest", "Run tests without creating attestation").option("
|
|
1185
|
+
var runCommand = new Command("run").description("Execute tests and create attestation").option("-s, --suite <name>", "Run specific suite (required unless --all or interactive mode)").option("-a, --all", "Run all suites needing attestation").option("--no-attest", "Run tests without creating attestation").option("--dry-run", "Show what would run without executing").option("-c, --continue", "Resume interrupted session").option("--filter <pattern>", "Filter suites by pattern (glob-style)").action(async (options) => {
|
|
1101
1186
|
await runTests(options);
|
|
1102
1187
|
});
|
|
1103
1188
|
async function runTests(options) {
|
|
@@ -1262,6 +1347,10 @@ async function runSingleSuite(suiteName, config, options) {
|
|
|
1262
1347
|
error(`Suite "${suiteName}" not found in config`);
|
|
1263
1348
|
process.exit(ExitCode.CONFIG_ERROR);
|
|
1264
1349
|
}
|
|
1350
|
+
if (!suiteConfig.packages) {
|
|
1351
|
+
error(`Suite "${suiteName}" has no packages defined`);
|
|
1352
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
1353
|
+
}
|
|
1265
1354
|
log(`
|
|
1266
1355
|
=== Running suite: ${suiteName} ===
|
|
1267
1356
|
`);
|
|
@@ -1285,9 +1374,9 @@ async function runSingleSuite(suiteName, config, options) {
|
|
|
1285
1374
|
log("Skipping attestation (--no-attest)");
|
|
1286
1375
|
return;
|
|
1287
1376
|
}
|
|
1288
|
-
const shouldAttest =
|
|
1289
|
-
message: "Create attestation
|
|
1290
|
-
default:
|
|
1377
|
+
const shouldAttest = await confirmAction({
|
|
1378
|
+
message: "Create attestation",
|
|
1379
|
+
default: false
|
|
1291
1380
|
});
|
|
1292
1381
|
if (!shouldAttest) {
|
|
1293
1382
|
warn("Attestation cancelled");
|
|
@@ -1303,32 +1392,546 @@ async function runSingleSuite(suiteName, config, options) {
|
|
|
1303
1392
|
const existingFile = await readAttestations(attestationsPath);
|
|
1304
1393
|
const existingAttestations = existingFile?.attestations ?? [];
|
|
1305
1394
|
const newAttestations = upsertAttestation(existingAttestations, attestation);
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1395
|
+
let keyProvider;
|
|
1396
|
+
let keyRef;
|
|
1397
|
+
if (config.settings.keyProvider) {
|
|
1398
|
+
keyProvider = KeyProviderRegistry.create({
|
|
1399
|
+
type: config.settings.keyProvider.type,
|
|
1400
|
+
options: config.settings.keyProvider.options ?? {}
|
|
1401
|
+
});
|
|
1402
|
+
if (config.settings.keyProvider.type === "filesystem") {
|
|
1403
|
+
keyRef = config.settings.keyProvider.options?.privateKeyPath ?? getDefaultPrivateKeyPath();
|
|
1404
|
+
} else if (config.settings.keyProvider.type === "1password") {
|
|
1405
|
+
keyRef = config.settings.keyProvider.options?.itemName ?? "attest-it-private-key";
|
|
1406
|
+
} else {
|
|
1407
|
+
throw new Error(`Unsupported key provider type: ${config.settings.keyProvider.type}`);
|
|
1408
|
+
}
|
|
1409
|
+
} else {
|
|
1410
|
+
keyProvider = new FilesystemKeyProvider();
|
|
1411
|
+
keyRef = getDefaultPrivateKeyPath();
|
|
1412
|
+
}
|
|
1413
|
+
if (!await keyProvider.keyExists(keyRef)) {
|
|
1414
|
+
error(`Private key not found in ${keyProvider.displayName}`);
|
|
1415
|
+
if (keyProvider.type === "filesystem") {
|
|
1416
|
+
error('Run "attest-it keygen" first to generate a keypair.');
|
|
1417
|
+
} else {
|
|
1418
|
+
error('Run "attest-it keygen" to generate and store a key.');
|
|
1419
|
+
}
|
|
1310
1420
|
process.exit(ExitCode.MISSING_KEY);
|
|
1311
1421
|
}
|
|
1312
1422
|
await writeSignedAttestations({
|
|
1313
1423
|
filePath: attestationsPath,
|
|
1314
1424
|
attestations: newAttestations,
|
|
1315
|
-
|
|
1425
|
+
keyProvider,
|
|
1426
|
+
keyRef
|
|
1316
1427
|
});
|
|
1317
1428
|
success(`Attestation created for ${suiteName}`);
|
|
1318
1429
|
log(` Fingerprint: ${fingerprintResult.fingerprint}`);
|
|
1319
1430
|
log(` Attested by: ${attestation.attestedBy}`);
|
|
1320
1431
|
log(` Attested at: ${attestation.attestedAt}`);
|
|
1432
|
+
if (suiteConfig.gate) {
|
|
1433
|
+
await promptForSeal(suiteName, suiteConfig.gate, config);
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
async function promptForSeal(suiteName, gateId, config) {
|
|
1437
|
+
log("");
|
|
1438
|
+
log(`Suite '${suiteName}' is linked to gate '${gateId}'`);
|
|
1439
|
+
const localConfig = loadLocalConfigSync();
|
|
1440
|
+
if (!localConfig) {
|
|
1441
|
+
warn("No local identity configuration found - cannot create seal");
|
|
1442
|
+
warn('Run "attest-it keygen" to set up your identity');
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
const identity = getActiveIdentity(localConfig);
|
|
1446
|
+
if (!identity) {
|
|
1447
|
+
warn(`Active identity '${localConfig.activeIdentity}' not found in local config`);
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
const attestItConfig = toAttestItConfig(config);
|
|
1451
|
+
const authorized = isAuthorizedSigner(attestItConfig, gateId, identity.publicKey);
|
|
1452
|
+
if (!authorized) {
|
|
1453
|
+
warn(`You are not authorized to seal gate '${gateId}'`);
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
const shouldSeal = await confirmAction({
|
|
1457
|
+
message: `Create seal for gate '${gateId}'`,
|
|
1458
|
+
default: true
|
|
1459
|
+
});
|
|
1460
|
+
if (!shouldSeal) {
|
|
1461
|
+
log("Seal creation skipped");
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
try {
|
|
1465
|
+
if (!attestItConfig.gates?.[gateId]) {
|
|
1466
|
+
error(`Gate '${gateId}' not found in configuration`);
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
const gate = attestItConfig.gates[gateId];
|
|
1470
|
+
const gateFingerprint = computeFingerprintSync({
|
|
1471
|
+
packages: gate.fingerprint.paths,
|
|
1472
|
+
...gate.fingerprint.exclude && { ignore: gate.fingerprint.exclude }
|
|
1473
|
+
});
|
|
1474
|
+
const keyProvider = createKeyProviderFromIdentity(identity);
|
|
1475
|
+
const keyRef = getKeyRefFromIdentity(identity);
|
|
1476
|
+
const keyResult = await keyProvider.getPrivateKey(keyRef);
|
|
1477
|
+
const fs4 = await import('fs/promises');
|
|
1478
|
+
const privateKeyPem = await fs4.readFile(keyResult.keyPath, "utf8");
|
|
1479
|
+
await keyResult.cleanup();
|
|
1480
|
+
const seal = createSeal({
|
|
1481
|
+
gateId,
|
|
1482
|
+
fingerprint: gateFingerprint.fingerprint,
|
|
1483
|
+
sealedBy: identity.name,
|
|
1484
|
+
privateKey: privateKeyPem
|
|
1485
|
+
});
|
|
1486
|
+
const projectRoot = process.cwd();
|
|
1487
|
+
const sealsFile = readSealsSync(projectRoot);
|
|
1488
|
+
sealsFile.seals[gateId] = seal;
|
|
1489
|
+
writeSealsSync(projectRoot, sealsFile);
|
|
1490
|
+
success(`Seal created for gate '${gateId}'`);
|
|
1491
|
+
log(` Sealed by: ${identity.name}`);
|
|
1492
|
+
log(` Timestamp: ${seal.timestamp}`);
|
|
1493
|
+
} catch (err) {
|
|
1494
|
+
if (err instanceof Error) {
|
|
1495
|
+
error(`Failed to create seal: ${err.message}`);
|
|
1496
|
+
} else {
|
|
1497
|
+
error("Failed to create seal: Unknown error");
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
function createKeyProviderFromIdentity(identity) {
|
|
1502
|
+
const { privateKey } = identity;
|
|
1503
|
+
switch (privateKey.type) {
|
|
1504
|
+
case "file":
|
|
1505
|
+
return KeyProviderRegistry.create({
|
|
1506
|
+
type: "filesystem",
|
|
1507
|
+
options: { privateKeyPath: privateKey.path }
|
|
1508
|
+
});
|
|
1509
|
+
case "keychain":
|
|
1510
|
+
return KeyProviderRegistry.create({
|
|
1511
|
+
type: "macos-keychain",
|
|
1512
|
+
options: {
|
|
1513
|
+
service: privateKey.service,
|
|
1514
|
+
account: privateKey.account
|
|
1515
|
+
}
|
|
1516
|
+
});
|
|
1517
|
+
case "1password":
|
|
1518
|
+
return KeyProviderRegistry.create({
|
|
1519
|
+
type: "1password",
|
|
1520
|
+
options: {
|
|
1521
|
+
account: privateKey.account,
|
|
1522
|
+
vault: privateKey.vault,
|
|
1523
|
+
itemName: privateKey.item,
|
|
1524
|
+
field: privateKey.field
|
|
1525
|
+
}
|
|
1526
|
+
});
|
|
1527
|
+
default: {
|
|
1528
|
+
const _exhaustiveCheck = privateKey;
|
|
1529
|
+
throw new Error(`Unsupported private key type: ${String(_exhaustiveCheck)}`);
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1321
1532
|
}
|
|
1322
|
-
|
|
1533
|
+
function getKeyRefFromIdentity(identity) {
|
|
1534
|
+
const { privateKey } = identity;
|
|
1535
|
+
switch (privateKey.type) {
|
|
1536
|
+
case "file":
|
|
1537
|
+
return privateKey.path;
|
|
1538
|
+
case "keychain":
|
|
1539
|
+
return `${privateKey.service}:${privateKey.account}`;
|
|
1540
|
+
case "1password":
|
|
1541
|
+
return privateKey.item;
|
|
1542
|
+
default: {
|
|
1543
|
+
const _exhaustiveCheck = privateKey;
|
|
1544
|
+
throw new Error(`Unsupported private key type: ${String(_exhaustiveCheck)}`);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
function KeygenInteractive(props) {
|
|
1549
|
+
const { onComplete, onError } = props;
|
|
1550
|
+
const [step, setStep] = useState("checking-providers");
|
|
1551
|
+
const [opAvailable, setOpAvailable] = useState(false);
|
|
1552
|
+
const [keychainAvailable, setKeychainAvailable] = useState(false);
|
|
1553
|
+
const [accounts, setAccounts] = useState([]);
|
|
1554
|
+
const [vaults, setVaults] = useState([]);
|
|
1555
|
+
const [_selectedProvider, setSelectedProvider] = useState();
|
|
1556
|
+
const [selectedAccount, setSelectedAccount] = useState();
|
|
1557
|
+
const [selectedVault, setSelectedVault] = useState();
|
|
1558
|
+
const [itemName, setItemName] = useState("attest-it-private-key");
|
|
1559
|
+
const [keychainItemName, setKeychainItemName] = useState("attest-it-private-key");
|
|
1560
|
+
useEffect(() => {
|
|
1561
|
+
const checkProviders = async () => {
|
|
1562
|
+
try {
|
|
1563
|
+
const isInstalled = await OnePasswordKeyProvider.isInstalled();
|
|
1564
|
+
setOpAvailable(isInstalled);
|
|
1565
|
+
if (isInstalled) {
|
|
1566
|
+
const accountList = await OnePasswordKeyProvider.listAccounts();
|
|
1567
|
+
setAccounts(accountList);
|
|
1568
|
+
}
|
|
1569
|
+
} catch {
|
|
1570
|
+
setOpAvailable(false);
|
|
1571
|
+
}
|
|
1572
|
+
const isKeychainAvailable = MacOSKeychainKeyProvider.isAvailable();
|
|
1573
|
+
setKeychainAvailable(isKeychainAvailable);
|
|
1574
|
+
setStep("select-provider");
|
|
1575
|
+
};
|
|
1576
|
+
void checkProviders();
|
|
1577
|
+
}, []);
|
|
1578
|
+
useEffect(() => {
|
|
1579
|
+
if (step === "select-vault" && selectedAccount) {
|
|
1580
|
+
const fetchVaults = async () => {
|
|
1581
|
+
try {
|
|
1582
|
+
const vaultList = await OnePasswordKeyProvider.listVaults(selectedAccount);
|
|
1583
|
+
setVaults(vaultList);
|
|
1584
|
+
} catch (err) {
|
|
1585
|
+
onError(err instanceof Error ? err : new Error("Failed to fetch vaults"));
|
|
1586
|
+
}
|
|
1587
|
+
};
|
|
1588
|
+
void fetchVaults();
|
|
1589
|
+
}
|
|
1590
|
+
}, [step, selectedAccount, onError]);
|
|
1591
|
+
const handleProviderSelect = (value) => {
|
|
1592
|
+
if (value === "filesystem") {
|
|
1593
|
+
setSelectedProvider("filesystem");
|
|
1594
|
+
void generateKeys("filesystem");
|
|
1595
|
+
} else if (value === "1password") {
|
|
1596
|
+
setSelectedProvider("1password");
|
|
1597
|
+
if (accounts.length === 1 && accounts[0]) {
|
|
1598
|
+
setSelectedAccount(accounts[0].email);
|
|
1599
|
+
setStep("select-vault");
|
|
1600
|
+
} else {
|
|
1601
|
+
setStep("select-account");
|
|
1602
|
+
}
|
|
1603
|
+
} else if (value === "macos-keychain") {
|
|
1604
|
+
setSelectedProvider("macos-keychain");
|
|
1605
|
+
setStep("enter-keychain-item-name");
|
|
1606
|
+
}
|
|
1607
|
+
};
|
|
1608
|
+
const handleAccountSelect = (value) => {
|
|
1609
|
+
setSelectedAccount(value);
|
|
1610
|
+
setStep("select-vault");
|
|
1611
|
+
};
|
|
1612
|
+
const handleVaultSelect = (value) => {
|
|
1613
|
+
setSelectedVault(value);
|
|
1614
|
+
setStep("enter-item-name");
|
|
1615
|
+
};
|
|
1616
|
+
const handleItemNameSubmit = (value) => {
|
|
1617
|
+
setItemName(value);
|
|
1618
|
+
void generateKeys("1password");
|
|
1619
|
+
};
|
|
1620
|
+
const handleKeychainItemNameSubmit = (value) => {
|
|
1621
|
+
setKeychainItemName(value);
|
|
1622
|
+
void generateKeys("macos-keychain");
|
|
1623
|
+
};
|
|
1624
|
+
const generateKeys = async (provider) => {
|
|
1625
|
+
setStep("generating");
|
|
1626
|
+
try {
|
|
1627
|
+
const publicKeyPath = props.publicKeyPath ?? getDefaultPublicKeyPath();
|
|
1628
|
+
if (provider === "filesystem") {
|
|
1629
|
+
const fsProvider = new FilesystemKeyProvider();
|
|
1630
|
+
const genOptions = { publicKeyPath };
|
|
1631
|
+
if (props.force !== void 0) {
|
|
1632
|
+
genOptions.force = props.force;
|
|
1633
|
+
}
|
|
1634
|
+
const result = await fsProvider.generateKeyPair(genOptions);
|
|
1635
|
+
onComplete({
|
|
1636
|
+
provider: "filesystem",
|
|
1637
|
+
publicKeyPath: result.publicKeyPath,
|
|
1638
|
+
privateKeyRef: result.privateKeyRef,
|
|
1639
|
+
storageDescription: result.storageDescription
|
|
1640
|
+
});
|
|
1641
|
+
} else if (provider === "1password") {
|
|
1642
|
+
if (!selectedVault || !itemName) {
|
|
1643
|
+
throw new Error("Vault and item name are required for 1Password");
|
|
1644
|
+
}
|
|
1645
|
+
const providerOptions = {
|
|
1646
|
+
vault: selectedVault,
|
|
1647
|
+
itemName
|
|
1648
|
+
};
|
|
1649
|
+
if (selectedAccount !== void 0) {
|
|
1650
|
+
providerOptions.account = selectedAccount;
|
|
1651
|
+
}
|
|
1652
|
+
const opProvider = new OnePasswordKeyProvider(providerOptions);
|
|
1653
|
+
const genOptions = { publicKeyPath };
|
|
1654
|
+
if (props.force !== void 0) {
|
|
1655
|
+
genOptions.force = props.force;
|
|
1656
|
+
}
|
|
1657
|
+
const result = await opProvider.generateKeyPair(genOptions);
|
|
1658
|
+
const completionResult = {
|
|
1659
|
+
provider: "1password",
|
|
1660
|
+
publicKeyPath: result.publicKeyPath,
|
|
1661
|
+
privateKeyRef: result.privateKeyRef,
|
|
1662
|
+
storageDescription: result.storageDescription,
|
|
1663
|
+
vault: selectedVault,
|
|
1664
|
+
itemName
|
|
1665
|
+
};
|
|
1666
|
+
if (selectedAccount !== void 0) {
|
|
1667
|
+
completionResult.account = selectedAccount;
|
|
1668
|
+
}
|
|
1669
|
+
onComplete(completionResult);
|
|
1670
|
+
} else {
|
|
1671
|
+
if (!keychainItemName) {
|
|
1672
|
+
throw new Error("Item name is required for macOS Keychain");
|
|
1673
|
+
}
|
|
1674
|
+
const keychainProvider = new MacOSKeychainKeyProvider({
|
|
1675
|
+
itemName: keychainItemName
|
|
1676
|
+
});
|
|
1677
|
+
const genOptions = { publicKeyPath };
|
|
1678
|
+
if (props.force !== void 0) {
|
|
1679
|
+
genOptions.force = props.force;
|
|
1680
|
+
}
|
|
1681
|
+
const result = await keychainProvider.generateKeyPair(genOptions);
|
|
1682
|
+
onComplete({
|
|
1683
|
+
provider: "macos-keychain",
|
|
1684
|
+
publicKeyPath: result.publicKeyPath,
|
|
1685
|
+
privateKeyRef: result.privateKeyRef,
|
|
1686
|
+
storageDescription: result.storageDescription,
|
|
1687
|
+
itemName: keychainItemName
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
setStep("done");
|
|
1691
|
+
} catch (err) {
|
|
1692
|
+
onError(err instanceof Error ? err : new Error("Key generation failed"));
|
|
1693
|
+
}
|
|
1694
|
+
};
|
|
1695
|
+
if (step === "checking-providers") {
|
|
1696
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
|
|
1697
|
+
/* @__PURE__ */ jsx(Spinner, {}),
|
|
1698
|
+
/* @__PURE__ */ jsx(Text, { children: "Checking available key storage providers..." })
|
|
1699
|
+
] }) });
|
|
1700
|
+
}
|
|
1701
|
+
if (step === "select-provider") {
|
|
1702
|
+
const options = [
|
|
1703
|
+
{
|
|
1704
|
+
label: `Local Filesystem (${getDefaultPrivateKeyPath()})`,
|
|
1705
|
+
value: "filesystem"
|
|
1706
|
+
}
|
|
1707
|
+
];
|
|
1708
|
+
if (keychainAvailable) {
|
|
1709
|
+
options.push({
|
|
1710
|
+
label: "macOS Keychain",
|
|
1711
|
+
value: "macos-keychain"
|
|
1712
|
+
});
|
|
1713
|
+
}
|
|
1714
|
+
if (opAvailable) {
|
|
1715
|
+
options.push({
|
|
1716
|
+
label: "1Password (requires op CLI)",
|
|
1717
|
+
value: "1password"
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
1721
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: "Where would you like to store your private key?" }),
|
|
1722
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "" }),
|
|
1723
|
+
/* @__PURE__ */ jsx(Select, { options, onChange: handleProviderSelect })
|
|
1724
|
+
] });
|
|
1725
|
+
}
|
|
1726
|
+
if (step === "select-account") {
|
|
1727
|
+
const options = accounts.map((account) => ({
|
|
1728
|
+
label: account.email,
|
|
1729
|
+
value: account.email
|
|
1730
|
+
}));
|
|
1731
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
1732
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: "Select 1Password account:" }),
|
|
1733
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "" }),
|
|
1734
|
+
/* @__PURE__ */ jsx(Select, { options, onChange: handleAccountSelect })
|
|
1735
|
+
] });
|
|
1736
|
+
}
|
|
1737
|
+
if (step === "select-vault") {
|
|
1738
|
+
if (vaults.length === 0) {
|
|
1739
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
|
|
1740
|
+
/* @__PURE__ */ jsx(Spinner, {}),
|
|
1741
|
+
/* @__PURE__ */ jsx(Text, { children: "Loading vaults..." })
|
|
1742
|
+
] }) });
|
|
1743
|
+
}
|
|
1744
|
+
const options = vaults.map((vault) => ({
|
|
1745
|
+
label: vault.name,
|
|
1746
|
+
value: vault.name
|
|
1747
|
+
}));
|
|
1748
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
1749
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: "Select vault for private key storage:" }),
|
|
1750
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "" }),
|
|
1751
|
+
/* @__PURE__ */ jsx(Select, { options, onChange: handleVaultSelect })
|
|
1752
|
+
] });
|
|
1753
|
+
}
|
|
1754
|
+
if (step === "enter-item-name") {
|
|
1755
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
1756
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: "Enter name for the key item:" }),
|
|
1757
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "(This will be visible in your 1Password vault)" }),
|
|
1758
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "" }),
|
|
1759
|
+
/* @__PURE__ */ jsx(TextInput, { defaultValue: itemName, onSubmit: handleItemNameSubmit })
|
|
1760
|
+
] });
|
|
1761
|
+
}
|
|
1762
|
+
if (step === "enter-keychain-item-name") {
|
|
1763
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
1764
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: "Enter name for the keychain item:" }),
|
|
1765
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "(This will be the service name in your macOS Keychain)" }),
|
|
1766
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "" }),
|
|
1767
|
+
/* @__PURE__ */ jsx(TextInput, { defaultValue: keychainItemName, onSubmit: handleKeychainItemNameSubmit })
|
|
1768
|
+
] });
|
|
1769
|
+
}
|
|
1770
|
+
if (step === "generating") {
|
|
1771
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
|
|
1772
|
+
/* @__PURE__ */ jsx(Spinner, {}),
|
|
1773
|
+
/* @__PURE__ */ jsx(Text, { children: "Generating RSA-2048 keypair..." })
|
|
1774
|
+
] }) });
|
|
1775
|
+
}
|
|
1776
|
+
return /* @__PURE__ */ jsx(Box, {});
|
|
1777
|
+
}
|
|
1778
|
+
async function runKeygenInteractive(options) {
|
|
1779
|
+
return new Promise((resolve2, reject) => {
|
|
1780
|
+
const props = {
|
|
1781
|
+
onComplete: (result) => {
|
|
1782
|
+
unmount();
|
|
1783
|
+
resolve2(result);
|
|
1784
|
+
},
|
|
1785
|
+
onCancel: () => {
|
|
1786
|
+
unmount();
|
|
1787
|
+
reject(new Error("Keygen cancelled"));
|
|
1788
|
+
},
|
|
1789
|
+
onError: (error2) => {
|
|
1790
|
+
unmount();
|
|
1791
|
+
reject(error2);
|
|
1792
|
+
}
|
|
1793
|
+
};
|
|
1794
|
+
if (options.publicKeyPath !== void 0) {
|
|
1795
|
+
props.publicKeyPath = options.publicKeyPath;
|
|
1796
|
+
}
|
|
1797
|
+
if (options.force !== void 0) {
|
|
1798
|
+
props.force = options.force;
|
|
1799
|
+
}
|
|
1800
|
+
const { unmount } = render(/* @__PURE__ */ jsx(KeygenInteractive, { ...props }));
|
|
1801
|
+
});
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
// src/commands/keygen.ts
|
|
1805
|
+
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(
|
|
1806
|
+
"--provider <type>",
|
|
1807
|
+
"Key provider: filesystem, 1password, or macos-keychain (skips interactive)"
|
|
1808
|
+
).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) => {
|
|
1323
1809
|
await runKeygen(options);
|
|
1324
1810
|
});
|
|
1325
1811
|
async function runKeygen(options) {
|
|
1326
1812
|
try {
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1813
|
+
const useInteractive = options.interactive !== false && !options.provider;
|
|
1814
|
+
if (useInteractive) {
|
|
1815
|
+
const interactiveOptions = {};
|
|
1816
|
+
if (options.output !== void 0) {
|
|
1817
|
+
interactiveOptions.publicKeyPath = options.output;
|
|
1818
|
+
}
|
|
1819
|
+
if (options.force !== void 0) {
|
|
1820
|
+
interactiveOptions.force = options.force;
|
|
1821
|
+
}
|
|
1822
|
+
const result = await runKeygenInteractive(interactiveOptions);
|
|
1823
|
+
success("Keypair generated successfully!");
|
|
1824
|
+
log("");
|
|
1825
|
+
log("Private key stored in:");
|
|
1826
|
+
log(` ${result.storageDescription}`);
|
|
1827
|
+
log("");
|
|
1828
|
+
log("Public key (commit to repo):");
|
|
1829
|
+
log(` ${result.publicKeyPath}`);
|
|
1830
|
+
log("");
|
|
1831
|
+
if (result.provider === "1password") {
|
|
1832
|
+
log("Add to your .attest-it/config.yaml:");
|
|
1833
|
+
log("");
|
|
1834
|
+
log("settings:");
|
|
1835
|
+
log(` publicKeyPath: ${result.publicKeyPath}`);
|
|
1836
|
+
log(" keyProvider:");
|
|
1837
|
+
log(" type: 1password");
|
|
1838
|
+
log(" options:");
|
|
1839
|
+
if (result.account) {
|
|
1840
|
+
log(` account: ${result.account}`);
|
|
1841
|
+
}
|
|
1842
|
+
log(` vault: ${result.vault ?? ""}`);
|
|
1843
|
+
log(` itemName: ${result.itemName ?? ""}`);
|
|
1844
|
+
log("");
|
|
1845
|
+
} else if (result.provider === "macos-keychain") {
|
|
1846
|
+
log("Add to your .attest-it/config.yaml:");
|
|
1847
|
+
log("");
|
|
1848
|
+
log("settings:");
|
|
1849
|
+
log(` publicKeyPath: ${result.publicKeyPath}`);
|
|
1850
|
+
log(" keyProvider:");
|
|
1851
|
+
log(" type: macos-keychain");
|
|
1852
|
+
log(" options:");
|
|
1853
|
+
log(` itemName: ${result.itemName ?? ""}`);
|
|
1854
|
+
log("");
|
|
1855
|
+
}
|
|
1856
|
+
log("Next steps:");
|
|
1857
|
+
log(` 1. git add ${result.publicKeyPath}`);
|
|
1858
|
+
if (result.provider === "1password" || result.provider === "macos-keychain") {
|
|
1859
|
+
log(" 2. Update .attest-it/config.yaml with keyProvider settings");
|
|
1860
|
+
} else {
|
|
1861
|
+
log(" 2. Update .attest-it/config.yaml publicKeyPath if needed");
|
|
1862
|
+
}
|
|
1863
|
+
log(" 3. attest-it run --suite <suite-name>");
|
|
1864
|
+
} else {
|
|
1865
|
+
await runNonInteractiveKeygen(options);
|
|
1866
|
+
}
|
|
1867
|
+
} catch (err) {
|
|
1868
|
+
if (err instanceof Error) {
|
|
1869
|
+
error(err.message);
|
|
1870
|
+
} else {
|
|
1871
|
+
error("Unknown error occurred");
|
|
1872
|
+
}
|
|
1873
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
async function runNonInteractiveKeygen(options) {
|
|
1877
|
+
log("Checking OpenSSL...");
|
|
1878
|
+
const version = await checkOpenSSL();
|
|
1879
|
+
info(`OpenSSL: ${version}`);
|
|
1880
|
+
const publicPath = options.output ?? getDefaultPublicKeyPath();
|
|
1881
|
+
if (options.provider === "1password") {
|
|
1882
|
+
if (!options.vault || !options.itemName) {
|
|
1883
|
+
throw new Error("--vault and --item-name are required for 1password provider");
|
|
1884
|
+
}
|
|
1885
|
+
const providerOptions = {
|
|
1886
|
+
vault: options.vault,
|
|
1887
|
+
itemName: options.itemName
|
|
1888
|
+
};
|
|
1889
|
+
if (options.account !== void 0) {
|
|
1890
|
+
providerOptions.account = options.account;
|
|
1891
|
+
}
|
|
1892
|
+
const provider = new OnePasswordKeyProvider(providerOptions);
|
|
1893
|
+
log(`Generating keypair with 1Password storage...`);
|
|
1894
|
+
log(`Vault: ${options.vault}`);
|
|
1895
|
+
log(`Item: ${options.itemName}`);
|
|
1896
|
+
const genOptions = { publicKeyPath: publicPath };
|
|
1897
|
+
if (options.force !== void 0) {
|
|
1898
|
+
genOptions.force = options.force;
|
|
1899
|
+
}
|
|
1900
|
+
const result = await provider.generateKeyPair(genOptions);
|
|
1901
|
+
success("Keypair generated successfully!");
|
|
1902
|
+
log("");
|
|
1903
|
+
log("Private key stored in:");
|
|
1904
|
+
log(` ${result.storageDescription}`);
|
|
1905
|
+
log("");
|
|
1906
|
+
log("Public key (commit to repo):");
|
|
1907
|
+
log(` ${result.publicKeyPath}`);
|
|
1908
|
+
} else if (options.provider === "macos-keychain") {
|
|
1909
|
+
if (!options.itemName) {
|
|
1910
|
+
throw new Error("--item-name is required for macos-keychain provider");
|
|
1911
|
+
}
|
|
1912
|
+
const isAvailable = MacOSKeychainKeyProvider.isAvailable();
|
|
1913
|
+
if (!isAvailable) {
|
|
1914
|
+
throw new Error("macOS Keychain is not available on this platform");
|
|
1915
|
+
}
|
|
1916
|
+
const provider = new MacOSKeychainKeyProvider({
|
|
1917
|
+
itemName: options.itemName
|
|
1918
|
+
});
|
|
1919
|
+
log(`Generating keypair with macOS Keychain storage...`);
|
|
1920
|
+
log(`Item: ${options.itemName}`);
|
|
1921
|
+
const genOptions = { publicKeyPath: publicPath };
|
|
1922
|
+
if (options.force !== void 0) {
|
|
1923
|
+
genOptions.force = options.force;
|
|
1924
|
+
}
|
|
1925
|
+
const result = await provider.generateKeyPair(genOptions);
|
|
1926
|
+
success("Keypair generated successfully!");
|
|
1927
|
+
log("");
|
|
1928
|
+
log("Private key stored in:");
|
|
1929
|
+
log(` ${result.storageDescription}`);
|
|
1930
|
+
log("");
|
|
1931
|
+
log("Public key (commit to repo):");
|
|
1932
|
+
log(` ${result.publicKeyPath}`);
|
|
1933
|
+
} else {
|
|
1934
|
+
const privatePath = options.private ?? getDefaultPrivateKeyPath();
|
|
1332
1935
|
log(`Private key: ${privatePath}`);
|
|
1333
1936
|
log(`Public key: ${publicPath}`);
|
|
1334
1937
|
const privateExists = fs.existsSync(privatePath);
|
|
@@ -1363,21 +1966,14 @@ async function runKeygen(options) {
|
|
|
1363
1966
|
log("");
|
|
1364
1967
|
log("Public key (commit to repo):");
|
|
1365
1968
|
log(` ${result.publicPath}`);
|
|
1366
|
-
log("");
|
|
1367
|
-
info("Important: Back up your private key securely!");
|
|
1368
|
-
log("");
|
|
1369
|
-
log("Next steps:");
|
|
1370
|
-
log(` 1. git add ${result.publicPath}`);
|
|
1371
|
-
log(" 2. Update .attest-it/config.yaml publicKeyPath if needed");
|
|
1372
|
-
log(" 3. attest-it run --suite <suite-name>");
|
|
1373
|
-
} catch (err) {
|
|
1374
|
-
if (err instanceof Error) {
|
|
1375
|
-
error(err.message);
|
|
1376
|
-
} else {
|
|
1377
|
-
error("Unknown error occurred");
|
|
1378
|
-
}
|
|
1379
|
-
process.exit(ExitCode.CONFIG_ERROR);
|
|
1380
1969
|
}
|
|
1970
|
+
log("");
|
|
1971
|
+
info("Important: Back up your private key securely!");
|
|
1972
|
+
log("");
|
|
1973
|
+
log("Next steps:");
|
|
1974
|
+
log(` 1. git add ${publicPath}`);
|
|
1975
|
+
log(" 2. Update .attest-it/config.yaml publicKeyPath if needed");
|
|
1976
|
+
log(" 3. attest-it run --suite <suite-name>");
|
|
1381
1977
|
}
|
|
1382
1978
|
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) => {
|
|
1383
1979
|
await runPrune(options);
|
|
@@ -1410,7 +2006,7 @@ async function runPrune(options) {
|
|
|
1410
2006
|
let fingerprintMatches = false;
|
|
1411
2007
|
if (suiteExists) {
|
|
1412
2008
|
const suiteConfig = config.suites[attestation.suite];
|
|
1413
|
-
if (suiteConfig) {
|
|
2009
|
+
if (suiteConfig?.packages) {
|
|
1414
2010
|
const fingerprintOptions = {
|
|
1415
2011
|
packages: suiteConfig.packages,
|
|
1416
2012
|
...suiteConfig.ignore && { ignore: suiteConfig.ignore }
|
|
@@ -1471,57 +2067,211 @@ async function runPrune(options) {
|
|
|
1471
2067
|
return;
|
|
1472
2068
|
}
|
|
1473
2069
|
}
|
|
1474
|
-
var verifyCommand = new Command("verify").description("Verify all
|
|
1475
|
-
await runVerify(options);
|
|
2070
|
+
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) => {
|
|
2071
|
+
await runVerify(gates, options);
|
|
1476
2072
|
});
|
|
1477
|
-
async function runVerify(options) {
|
|
2073
|
+
async function runVerify(gates, options) {
|
|
1478
2074
|
try {
|
|
1479
2075
|
const config = await loadConfig();
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
2076
|
+
const attestItConfig = toAttestItConfig(config);
|
|
2077
|
+
if (!attestItConfig.gates || Object.keys(attestItConfig.gates).length === 0) {
|
|
2078
|
+
error("No gates defined in configuration");
|
|
2079
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
2080
|
+
}
|
|
2081
|
+
const projectRoot = process.cwd();
|
|
2082
|
+
const sealsFile = readSealsSync(projectRoot);
|
|
2083
|
+
const gatesToVerify = gates.length > 0 ? gates : Object.keys(attestItConfig.gates);
|
|
2084
|
+
for (const gateId of gatesToVerify) {
|
|
2085
|
+
if (!attestItConfig.gates[gateId]) {
|
|
2086
|
+
error(`Gate '${gateId}' not found in configuration`);
|
|
1488
2087
|
process.exit(ExitCode.CONFIG_ERROR);
|
|
1489
2088
|
}
|
|
1490
|
-
const filteredConfig = {
|
|
1491
|
-
version: config.version,
|
|
1492
|
-
settings: config.settings,
|
|
1493
|
-
suites: { [options.suite]: filteredSuiteEntry }
|
|
1494
|
-
};
|
|
1495
|
-
const result2 = await verifyAttestations({ config: toAttestItConfig(filteredConfig) });
|
|
1496
|
-
if (options.json) {
|
|
1497
|
-
outputJson(result2);
|
|
1498
|
-
} else {
|
|
1499
|
-
displayResults(result2, filteredConfig.settings.maxAgeDays, options.strict);
|
|
1500
|
-
}
|
|
1501
|
-
if (!result2.success) {
|
|
1502
|
-
process.exit(ExitCode.FAILURE);
|
|
1503
|
-
return;
|
|
1504
|
-
}
|
|
1505
|
-
if (options.strict && hasWarnings(result2, filteredConfig.settings.maxAgeDays)) {
|
|
1506
|
-
process.exit(ExitCode.FAILURE);
|
|
1507
|
-
return;
|
|
1508
|
-
}
|
|
1509
|
-
process.exit(ExitCode.SUCCESS);
|
|
1510
|
-
return;
|
|
1511
2089
|
}
|
|
1512
|
-
const
|
|
2090
|
+
const fingerprints = {};
|
|
2091
|
+
for (const gateId of gatesToVerify) {
|
|
2092
|
+
const gate = attestItConfig.gates[gateId];
|
|
2093
|
+
if (!gate) continue;
|
|
2094
|
+
const result = computeFingerprintSync({
|
|
2095
|
+
packages: gate.fingerprint.paths,
|
|
2096
|
+
...gate.fingerprint.exclude && { ignore: gate.fingerprint.exclude }
|
|
2097
|
+
});
|
|
2098
|
+
fingerprints[gateId] = result.fingerprint;
|
|
2099
|
+
}
|
|
2100
|
+
const results = gates.length > 0 ? gatesToVerify.map(
|
|
2101
|
+
(gateId) => (
|
|
2102
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
2103
|
+
verifyGateSeal(attestItConfig, gateId, sealsFile, fingerprints[gateId] ?? "")
|
|
2104
|
+
)
|
|
2105
|
+
) : verifyAllSeals(attestItConfig, sealsFile, fingerprints);
|
|
1513
2106
|
if (options.json) {
|
|
1514
|
-
outputJson(
|
|
2107
|
+
outputJson(results);
|
|
1515
2108
|
} else {
|
|
1516
|
-
displayResults(
|
|
2109
|
+
displayResults(results);
|
|
1517
2110
|
}
|
|
1518
|
-
|
|
2111
|
+
const hasInvalid = results.some(
|
|
2112
|
+
(r) => r.state === "MISSING" || r.state === "FINGERPRINT_MISMATCH" || r.state === "INVALID_SIGNATURE" || r.state === "UNKNOWN_SIGNER"
|
|
2113
|
+
);
|
|
2114
|
+
const hasStale = results.some((r) => r.state === "STALE");
|
|
2115
|
+
if (hasInvalid) {
|
|
1519
2116
|
process.exit(ExitCode.FAILURE);
|
|
2117
|
+
} else if (hasStale) {
|
|
2118
|
+
process.exit(ExitCode.SUCCESS);
|
|
2119
|
+
} else {
|
|
2120
|
+
process.exit(ExitCode.SUCCESS);
|
|
2121
|
+
}
|
|
2122
|
+
} catch (err) {
|
|
2123
|
+
if (err instanceof Error) {
|
|
2124
|
+
error(err.message);
|
|
2125
|
+
} else {
|
|
2126
|
+
error("Unknown error occurred");
|
|
1520
2127
|
}
|
|
1521
|
-
|
|
2128
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
function displayResults(results) {
|
|
2132
|
+
log("");
|
|
2133
|
+
const tableRows = results.map((r) => ({
|
|
2134
|
+
suite: r.gateId,
|
|
2135
|
+
status: colorizeState2(r.state),
|
|
2136
|
+
fingerprint: formatFingerprint(r),
|
|
2137
|
+
age: formatAge2(r)
|
|
2138
|
+
}));
|
|
2139
|
+
log(formatTable(tableRows));
|
|
2140
|
+
log("");
|
|
2141
|
+
const withIssues = results.filter(
|
|
2142
|
+
(r) => r.state !== "VALID" && r.state !== "STALE" && // STALE gets its own warning below
|
|
2143
|
+
r.message
|
|
2144
|
+
);
|
|
2145
|
+
if (withIssues.length > 0) {
|
|
2146
|
+
for (const result of withIssues) {
|
|
2147
|
+
if (result.message) {
|
|
2148
|
+
log(`${result.gateId}: ${result.message}`);
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
log("");
|
|
2152
|
+
}
|
|
2153
|
+
const validCount = results.filter((r) => r.state === "VALID").length;
|
|
2154
|
+
const staleCount = results.filter((r) => r.state === "STALE").length;
|
|
2155
|
+
const invalidCount = results.length - validCount - staleCount;
|
|
2156
|
+
if (invalidCount === 0 && staleCount === 0) {
|
|
2157
|
+
success("All gate seals valid");
|
|
2158
|
+
} else {
|
|
2159
|
+
if (invalidCount > 0) {
|
|
2160
|
+
error(`${String(invalidCount)} gate(s) have invalid or missing seals`);
|
|
2161
|
+
log("Run `attest-it seal` to create seals for these gates");
|
|
2162
|
+
}
|
|
2163
|
+
if (staleCount > 0) {
|
|
2164
|
+
warn(`${String(staleCount)} gate(s) have stale seals (exceeds maxAge)`);
|
|
2165
|
+
log("Run `attest-it seal --force <gate>` to update stale seals");
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
function colorizeState2(state) {
|
|
2170
|
+
const theme3 = getTheme();
|
|
2171
|
+
switch (state) {
|
|
2172
|
+
case "VALID":
|
|
2173
|
+
return theme3.green(state);
|
|
2174
|
+
case "MISSING":
|
|
2175
|
+
case "STALE":
|
|
2176
|
+
return theme3.yellow(state);
|
|
2177
|
+
case "FINGERPRINT_MISMATCH":
|
|
2178
|
+
case "INVALID_SIGNATURE":
|
|
2179
|
+
case "UNKNOWN_SIGNER":
|
|
2180
|
+
return theme3.red(state);
|
|
2181
|
+
default:
|
|
2182
|
+
return state;
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
function formatFingerprint(result) {
|
|
2186
|
+
if (result.seal?.fingerprint) {
|
|
2187
|
+
const fp = result.seal.fingerprint;
|
|
2188
|
+
if (fp.length > 16) {
|
|
2189
|
+
return fp.slice(0, 16) + "...";
|
|
2190
|
+
}
|
|
2191
|
+
return fp;
|
|
2192
|
+
}
|
|
2193
|
+
return result.state === "MISSING" ? "(none)" : "-";
|
|
2194
|
+
}
|
|
2195
|
+
function formatAge2(result) {
|
|
2196
|
+
if (result.seal?.timestamp) {
|
|
2197
|
+
const timestamp = new Date(result.seal.timestamp);
|
|
2198
|
+
const now = Date.now();
|
|
2199
|
+
const ageMs = now - timestamp.getTime();
|
|
2200
|
+
const ageDays = Math.floor(ageMs / (1e3 * 60 * 60 * 24));
|
|
2201
|
+
if (result.state === "STALE") {
|
|
2202
|
+
return `${String(ageDays)} days (stale)`;
|
|
2203
|
+
}
|
|
2204
|
+
return `${String(ageDays)} days`;
|
|
2205
|
+
}
|
|
2206
|
+
switch (result.state) {
|
|
2207
|
+
case "MISSING":
|
|
2208
|
+
return "(none)";
|
|
2209
|
+
case "FINGERPRINT_MISMATCH":
|
|
2210
|
+
return "(changed)";
|
|
2211
|
+
default:
|
|
2212
|
+
return "-";
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
var sealCommand = new Command("seal").description("Create seals for gates").argument("[gates...]", "Gate IDs to seal (defaults to all gates without valid seals)").option("--force", "Force seal creation even if gate already has a valid seal").option("--dry-run", "Show what would be sealed without creating seals").action(async (gates, options) => {
|
|
2216
|
+
await runSeal(gates, options);
|
|
2217
|
+
});
|
|
2218
|
+
async function runSeal(gates, options) {
|
|
2219
|
+
try {
|
|
2220
|
+
const config = await loadConfig();
|
|
2221
|
+
const attestItConfig = toAttestItConfig(config);
|
|
2222
|
+
if (!attestItConfig.gates || Object.keys(attestItConfig.gates).length === 0) {
|
|
2223
|
+
error("No gates defined in configuration");
|
|
2224
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
2225
|
+
}
|
|
2226
|
+
const localConfig = loadLocalConfigSync();
|
|
2227
|
+
if (!localConfig) {
|
|
2228
|
+
error("No local identity configuration found");
|
|
2229
|
+
error('Run "attest-it keygen" first to set up your identity');
|
|
2230
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
2231
|
+
}
|
|
2232
|
+
const identity = getActiveIdentity(localConfig);
|
|
2233
|
+
if (!identity) {
|
|
2234
|
+
error(`Active identity '${localConfig.activeIdentity}' not found in local config`);
|
|
2235
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
2236
|
+
}
|
|
2237
|
+
const projectRoot = process.cwd();
|
|
2238
|
+
const sealsFile = readSealsSync(projectRoot);
|
|
2239
|
+
const gatesToSeal = gates.length > 0 ? gates : getAllGateIds(attestItConfig);
|
|
2240
|
+
for (const gateId of gatesToSeal) {
|
|
2241
|
+
if (!attestItConfig.gates[gateId]) {
|
|
2242
|
+
error(`Gate '${gateId}' not found in configuration`);
|
|
2243
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
const summary = {
|
|
2247
|
+
sealed: [],
|
|
2248
|
+
skipped: [],
|
|
2249
|
+
failed: []
|
|
2250
|
+
};
|
|
2251
|
+
for (const gateId of gatesToSeal) {
|
|
2252
|
+
try {
|
|
2253
|
+
const result = await processSingleGate(gateId, attestItConfig, identity, sealsFile, options);
|
|
2254
|
+
if (result.sealed) {
|
|
2255
|
+
summary.sealed.push(gateId);
|
|
2256
|
+
} else if (result.skipped) {
|
|
2257
|
+
summary.skipped.push({ gate: gateId, reason: result.reason ?? "Unknown" });
|
|
2258
|
+
}
|
|
2259
|
+
} catch (err) {
|
|
2260
|
+
const errorMsg = err instanceof Error ? err.message : "Unknown error";
|
|
2261
|
+
summary.failed.push({ gate: gateId, error: errorMsg });
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
if (!options.dryRun && summary.sealed.length > 0) {
|
|
2265
|
+
writeSealsSync(projectRoot, sealsFile);
|
|
2266
|
+
}
|
|
2267
|
+
displaySummary(summary, options.dryRun);
|
|
2268
|
+
if (summary.failed.length > 0) {
|
|
1522
2269
|
process.exit(ExitCode.FAILURE);
|
|
2270
|
+
} else if (summary.sealed.length === 0 && summary.skipped.length === 0) {
|
|
2271
|
+
process.exit(ExitCode.NO_WORK);
|
|
2272
|
+
} else {
|
|
2273
|
+
process.exit(ExitCode.SUCCESS);
|
|
1523
2274
|
}
|
|
1524
|
-
process.exit(ExitCode.SUCCESS);
|
|
1525
2275
|
} catch (err) {
|
|
1526
2276
|
if (err instanceof Error) {
|
|
1527
2277
|
error(err.message);
|
|
@@ -1531,90 +2281,1342 @@ async function runVerify(options) {
|
|
|
1531
2281
|
process.exit(ExitCode.CONFIG_ERROR);
|
|
1532
2282
|
}
|
|
1533
2283
|
}
|
|
1534
|
-
function
|
|
2284
|
+
async function processSingleGate(gateId, config, identity, sealsFile, options) {
|
|
2285
|
+
verbose(`Processing gate: ${gateId}`);
|
|
2286
|
+
const gate = getGate(config, gateId);
|
|
2287
|
+
if (!gate) {
|
|
2288
|
+
return { sealed: false, skipped: true, reason: "Gate not found in configuration" };
|
|
2289
|
+
}
|
|
2290
|
+
const existingSeal = sealsFile.seals[gateId];
|
|
2291
|
+
if (existingSeal && !options.force) {
|
|
2292
|
+
return {
|
|
2293
|
+
sealed: false,
|
|
2294
|
+
skipped: true,
|
|
2295
|
+
reason: "Gate already has a seal (use --force to override)"
|
|
2296
|
+
};
|
|
2297
|
+
}
|
|
2298
|
+
const fingerprintResult = computeFingerprintSync({
|
|
2299
|
+
packages: gate.fingerprint.paths,
|
|
2300
|
+
...gate.fingerprint.exclude && { ignore: gate.fingerprint.exclude }
|
|
2301
|
+
});
|
|
2302
|
+
verbose(` Fingerprint: ${fingerprintResult.fingerprint}`);
|
|
2303
|
+
const authorized = isAuthorizedSigner(config, gateId, identity.publicKey);
|
|
2304
|
+
if (!authorized) {
|
|
2305
|
+
return {
|
|
2306
|
+
sealed: false,
|
|
2307
|
+
skipped: true,
|
|
2308
|
+
reason: `Not authorized to seal this gate (authorized signers: ${gate.authorizedSigners.join(", ")})`
|
|
2309
|
+
};
|
|
2310
|
+
}
|
|
2311
|
+
if (options.dryRun) {
|
|
2312
|
+
log(` Would seal gate: ${gateId}`);
|
|
2313
|
+
return { sealed: true, skipped: false };
|
|
2314
|
+
}
|
|
2315
|
+
const keyProvider = createKeyProviderFromIdentity2(identity);
|
|
2316
|
+
const keyRef = getKeyRefFromIdentity2(identity);
|
|
2317
|
+
const keyResult = await keyProvider.getPrivateKey(keyRef);
|
|
2318
|
+
const fs4 = await import('fs/promises');
|
|
2319
|
+
const privateKeyPem = await fs4.readFile(keyResult.keyPath, "utf8");
|
|
2320
|
+
await keyResult.cleanup();
|
|
2321
|
+
const seal = createSeal({
|
|
2322
|
+
gateId,
|
|
2323
|
+
fingerprint: fingerprintResult.fingerprint,
|
|
2324
|
+
sealedBy: identity.name,
|
|
2325
|
+
privateKey: privateKeyPem
|
|
2326
|
+
});
|
|
2327
|
+
sealsFile.seals[gateId] = seal;
|
|
2328
|
+
log(` Sealed gate: ${gateId}`);
|
|
2329
|
+
verbose(` Sealed by: ${identity.name}`);
|
|
2330
|
+
verbose(` Timestamp: ${seal.timestamp}`);
|
|
2331
|
+
return { sealed: true, skipped: false };
|
|
2332
|
+
}
|
|
2333
|
+
function getAllGateIds(config) {
|
|
2334
|
+
return Object.keys(config.gates ?? {});
|
|
2335
|
+
}
|
|
2336
|
+
function displaySummary(summary, dryRun) {
|
|
1535
2337
|
log("");
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
2338
|
+
const prefix = dryRun ? "Would seal" : "Sealed";
|
|
2339
|
+
if (summary.sealed.length > 0) {
|
|
2340
|
+
success(`${prefix} ${String(summary.sealed.length)} gate(s): ${summary.sealed.join(", ")}`);
|
|
2341
|
+
}
|
|
2342
|
+
if (summary.skipped.length > 0) {
|
|
1539
2343
|
log("");
|
|
2344
|
+
warn(`Skipped ${String(summary.skipped.length)} gate(s):`);
|
|
2345
|
+
for (const skip of summary.skipped) {
|
|
2346
|
+
log(` ${skip.gate}: ${skip.reason}`);
|
|
2347
|
+
}
|
|
1540
2348
|
}
|
|
1541
|
-
|
|
1542
|
-
|
|
2349
|
+
if (summary.failed.length > 0) {
|
|
2350
|
+
log("");
|
|
2351
|
+
error(`Failed to seal ${String(summary.failed.length)} gate(s):`);
|
|
2352
|
+
for (const fail of summary.failed) {
|
|
2353
|
+
log(` ${fail.gate}: ${fail.error}`);
|
|
2354
|
+
}
|
|
1543
2355
|
}
|
|
1544
|
-
if (
|
|
2356
|
+
if (summary.sealed.length === 0 && summary.skipped.length === 0 && summary.failed.length === 0) {
|
|
2357
|
+
log("No gates to seal");
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
function createKeyProviderFromIdentity2(identity) {
|
|
2361
|
+
const { privateKey } = identity;
|
|
2362
|
+
switch (privateKey.type) {
|
|
2363
|
+
case "file":
|
|
2364
|
+
return KeyProviderRegistry.create({
|
|
2365
|
+
type: "filesystem",
|
|
2366
|
+
options: { privateKeyPath: privateKey.path }
|
|
2367
|
+
});
|
|
2368
|
+
case "keychain":
|
|
2369
|
+
return KeyProviderRegistry.create({
|
|
2370
|
+
type: "macos-keychain",
|
|
2371
|
+
options: {
|
|
2372
|
+
itemName: privateKey.service
|
|
2373
|
+
}
|
|
2374
|
+
});
|
|
2375
|
+
case "1password":
|
|
2376
|
+
return KeyProviderRegistry.create({
|
|
2377
|
+
type: "1password",
|
|
2378
|
+
options: {
|
|
2379
|
+
account: privateKey.account,
|
|
2380
|
+
vault: privateKey.vault,
|
|
2381
|
+
itemName: privateKey.item,
|
|
2382
|
+
field: privateKey.field
|
|
2383
|
+
}
|
|
2384
|
+
});
|
|
2385
|
+
default: {
|
|
2386
|
+
const _exhaustiveCheck = privateKey;
|
|
2387
|
+
throw new Error(`Unsupported private key type: ${String(_exhaustiveCheck)}`);
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
function getKeyRefFromIdentity2(identity) {
|
|
2392
|
+
const { privateKey } = identity;
|
|
2393
|
+
switch (privateKey.type) {
|
|
2394
|
+
case "file":
|
|
2395
|
+
return privateKey.path;
|
|
2396
|
+
case "keychain":
|
|
2397
|
+
return privateKey.service;
|
|
2398
|
+
case "1password":
|
|
2399
|
+
return privateKey.item;
|
|
2400
|
+
default: {
|
|
2401
|
+
const _exhaustiveCheck = privateKey;
|
|
2402
|
+
throw new Error(`Unsupported private key type: ${String(_exhaustiveCheck)}`);
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
var listCommand = new Command("list").description("List all local identities").action(async () => {
|
|
2407
|
+
await runList();
|
|
2408
|
+
});
|
|
2409
|
+
async function runList() {
|
|
2410
|
+
try {
|
|
2411
|
+
const config = await loadLocalConfig();
|
|
2412
|
+
if (!config) {
|
|
2413
|
+
error("No identities configured");
|
|
2414
|
+
log("");
|
|
2415
|
+
log("Run: attest-it identity create");
|
|
2416
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
2417
|
+
}
|
|
2418
|
+
const theme3 = getTheme2();
|
|
2419
|
+
const identities = Object.entries(config.identities);
|
|
1545
2420
|
log("");
|
|
2421
|
+
log(theme3.blue.bold()("Local Identities:"));
|
|
2422
|
+
log("");
|
|
2423
|
+
for (const [slug, identity] of identities) {
|
|
2424
|
+
const isActive = slug === config.activeIdentity;
|
|
2425
|
+
const marker = isActive ? theme3.green("\u2605") : " ";
|
|
2426
|
+
const nameDisplay = isActive ? theme3.green.bold()(identity.name) : identity.name;
|
|
2427
|
+
const keyPreview = identity.publicKey.slice(0, 12) + "...";
|
|
2428
|
+
let keyType;
|
|
2429
|
+
switch (identity.privateKey.type) {
|
|
2430
|
+
case "file":
|
|
2431
|
+
keyType = "file";
|
|
2432
|
+
break;
|
|
2433
|
+
case "keychain":
|
|
2434
|
+
keyType = "keychain";
|
|
2435
|
+
break;
|
|
2436
|
+
case "1password":
|
|
2437
|
+
keyType = "1password";
|
|
2438
|
+
break;
|
|
2439
|
+
}
|
|
2440
|
+
log(`${marker} ${theme3.blue(slug)}`);
|
|
2441
|
+
log(` Name: ${nameDisplay}`);
|
|
2442
|
+
if (identity.email) {
|
|
2443
|
+
log(` Email: ${identity.email}`);
|
|
2444
|
+
}
|
|
2445
|
+
if (identity.github) {
|
|
2446
|
+
log(` GitHub: ${identity.github}`);
|
|
2447
|
+
}
|
|
2448
|
+
log(` Public Key: ${keyPreview}`);
|
|
2449
|
+
log(` Key Type: ${keyType}`);
|
|
2450
|
+
log("");
|
|
2451
|
+
}
|
|
2452
|
+
if (identities.length === 1) {
|
|
2453
|
+
log(`1 identity configured`);
|
|
2454
|
+
} else {
|
|
2455
|
+
log(`${identities.length.toString()} identities configured`);
|
|
2456
|
+
}
|
|
2457
|
+
log("");
|
|
2458
|
+
} catch (err) {
|
|
2459
|
+
if (err instanceof Error) {
|
|
2460
|
+
error(err.message);
|
|
2461
|
+
} else {
|
|
2462
|
+
error("Unknown error occurred");
|
|
2463
|
+
}
|
|
2464
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
1546
2465
|
}
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
const
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
2466
|
+
}
|
|
2467
|
+
var createCommand = new Command("create").description("Create a new identity with Ed25519 keypair").action(async () => {
|
|
2468
|
+
await runCreate();
|
|
2469
|
+
});
|
|
2470
|
+
async function runCreate() {
|
|
2471
|
+
try {
|
|
2472
|
+
const theme3 = getTheme2();
|
|
2473
|
+
log("");
|
|
2474
|
+
log(theme3.blue.bold()("Create New Identity"));
|
|
2475
|
+
log("");
|
|
2476
|
+
const existingConfig = await loadLocalConfig();
|
|
2477
|
+
const slug = await input({
|
|
2478
|
+
message: "Identity slug (unique identifier):",
|
|
2479
|
+
validate: (value) => {
|
|
2480
|
+
if (!value || value.trim().length === 0) {
|
|
2481
|
+
return "Slug cannot be empty";
|
|
2482
|
+
}
|
|
2483
|
+
if (!/^[a-z0-9-]+$/.test(value)) {
|
|
2484
|
+
return "Slug must contain only lowercase letters, numbers, and hyphens";
|
|
2485
|
+
}
|
|
2486
|
+
if (existingConfig?.identities[value]) {
|
|
2487
|
+
return `Identity "${value}" already exists`;
|
|
1565
2488
|
}
|
|
2489
|
+
return true;
|
|
1566
2490
|
}
|
|
2491
|
+
});
|
|
2492
|
+
const name = await input({
|
|
2493
|
+
message: "Display name:",
|
|
2494
|
+
validate: (value) => {
|
|
2495
|
+
if (!value || value.trim().length === 0) {
|
|
2496
|
+
return "Name cannot be empty";
|
|
2497
|
+
}
|
|
2498
|
+
return true;
|
|
2499
|
+
}
|
|
2500
|
+
});
|
|
2501
|
+
const email = await input({
|
|
2502
|
+
message: "Email (optional):",
|
|
2503
|
+
default: ""
|
|
2504
|
+
});
|
|
2505
|
+
const github = await input({
|
|
2506
|
+
message: "GitHub username (optional):",
|
|
2507
|
+
default: ""
|
|
2508
|
+
});
|
|
2509
|
+
const keyStorageType = await select({
|
|
2510
|
+
message: "Where should the private key be stored?",
|
|
2511
|
+
choices: [
|
|
2512
|
+
{ name: "File system (~/.config/attest-it/keys/)", value: "file" },
|
|
2513
|
+
{ name: "macOS Keychain", value: "keychain" },
|
|
2514
|
+
{ name: "1Password", value: "1password" }
|
|
2515
|
+
]
|
|
2516
|
+
});
|
|
2517
|
+
log("");
|
|
2518
|
+
log("Generating Ed25519 keypair...");
|
|
2519
|
+
const keyPair = generateEd25519KeyPair();
|
|
2520
|
+
let privateKeyRef;
|
|
2521
|
+
let keyStorageDescription;
|
|
2522
|
+
switch (keyStorageType) {
|
|
2523
|
+
case "file": {
|
|
2524
|
+
const keysDir = join(homedir(), ".config", "attest-it", "keys");
|
|
2525
|
+
await mkdir(keysDir, { recursive: true });
|
|
2526
|
+
const keyPath = join(keysDir, `${slug}.pem`);
|
|
2527
|
+
await writeFile(keyPath, keyPair.privateKey, { mode: 384 });
|
|
2528
|
+
privateKeyRef = { type: "file", path: keyPath };
|
|
2529
|
+
keyStorageDescription = keyPath;
|
|
2530
|
+
break;
|
|
2531
|
+
}
|
|
2532
|
+
case "keychain": {
|
|
2533
|
+
const { MacOSKeychainKeyProvider: MacOSKeychainKeyProvider3 } = await import('@attest-it/core');
|
|
2534
|
+
if (!MacOSKeychainKeyProvider3.isAvailable()) {
|
|
2535
|
+
error("macOS Keychain is not available on this system");
|
|
2536
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
2537
|
+
}
|
|
2538
|
+
const { execFile } = await import('child_process');
|
|
2539
|
+
const { promisify } = await import('util');
|
|
2540
|
+
const execFileAsync = promisify(execFile);
|
|
2541
|
+
const encodedKey = Buffer.from(keyPair.privateKey).toString("base64");
|
|
2542
|
+
try {
|
|
2543
|
+
await execFileAsync("security", [
|
|
2544
|
+
"add-generic-password",
|
|
2545
|
+
"-a",
|
|
2546
|
+
"attest-it",
|
|
2547
|
+
"-s",
|
|
2548
|
+
slug,
|
|
2549
|
+
"-w",
|
|
2550
|
+
encodedKey,
|
|
2551
|
+
"-U"
|
|
2552
|
+
]);
|
|
2553
|
+
} catch (err) {
|
|
2554
|
+
throw new Error(
|
|
2555
|
+
`Failed to store key in macOS Keychain: ${err instanceof Error ? err.message : String(err)}`
|
|
2556
|
+
);
|
|
2557
|
+
}
|
|
2558
|
+
privateKeyRef = { type: "keychain", service: slug, account: "attest-it" };
|
|
2559
|
+
keyStorageDescription = "macOS Keychain (" + slug + "/attest-it)";
|
|
2560
|
+
break;
|
|
2561
|
+
}
|
|
2562
|
+
case "1password": {
|
|
2563
|
+
const vault = await input({
|
|
2564
|
+
message: "1Password vault name:",
|
|
2565
|
+
validate: (value) => {
|
|
2566
|
+
if (!value || value.trim().length === 0) {
|
|
2567
|
+
return "Vault name cannot be empty";
|
|
2568
|
+
}
|
|
2569
|
+
return true;
|
|
2570
|
+
}
|
|
2571
|
+
});
|
|
2572
|
+
const item = await input({
|
|
2573
|
+
message: "1Password item name:",
|
|
2574
|
+
default: `attest-it-${slug}`,
|
|
2575
|
+
validate: (value) => {
|
|
2576
|
+
if (!value || value.trim().length === 0) {
|
|
2577
|
+
return "Item name cannot be empty";
|
|
2578
|
+
}
|
|
2579
|
+
return true;
|
|
2580
|
+
}
|
|
2581
|
+
});
|
|
2582
|
+
const { execFile } = await import('child_process');
|
|
2583
|
+
const { promisify } = await import('util');
|
|
2584
|
+
const execFileAsync = promisify(execFile);
|
|
2585
|
+
try {
|
|
2586
|
+
await execFileAsync("op", [
|
|
2587
|
+
"item",
|
|
2588
|
+
"create",
|
|
2589
|
+
"--category=SecureNote",
|
|
2590
|
+
"--vault",
|
|
2591
|
+
vault,
|
|
2592
|
+
`--title=${item}`,
|
|
2593
|
+
`privateKey[password]=${keyPair.privateKey}`
|
|
2594
|
+
]);
|
|
2595
|
+
} catch (err) {
|
|
2596
|
+
throw new Error(
|
|
2597
|
+
`Failed to store key in 1Password: ${err instanceof Error ? err.message : String(err)}`
|
|
2598
|
+
);
|
|
2599
|
+
}
|
|
2600
|
+
privateKeyRef = { type: "1password", vault, item, field: "privateKey" };
|
|
2601
|
+
keyStorageDescription = `1Password (${vault}/${item})`;
|
|
2602
|
+
break;
|
|
2603
|
+
}
|
|
2604
|
+
default:
|
|
2605
|
+
throw new Error(`Unknown key storage type: ${keyStorageType}`);
|
|
1567
2606
|
}
|
|
2607
|
+
const identity = {
|
|
2608
|
+
name,
|
|
2609
|
+
publicKey: keyPair.publicKey,
|
|
2610
|
+
privateKey: privateKeyRef,
|
|
2611
|
+
...email && { email },
|
|
2612
|
+
...github && { github }
|
|
2613
|
+
};
|
|
2614
|
+
let newConfig;
|
|
2615
|
+
if (existingConfig) {
|
|
2616
|
+
newConfig = {
|
|
2617
|
+
...existingConfig,
|
|
2618
|
+
identities: {
|
|
2619
|
+
...existingConfig.identities,
|
|
2620
|
+
[slug]: identity
|
|
2621
|
+
}
|
|
2622
|
+
};
|
|
2623
|
+
} else {
|
|
2624
|
+
newConfig = {
|
|
2625
|
+
activeIdentity: slug,
|
|
2626
|
+
identities: {
|
|
2627
|
+
[slug]: identity
|
|
2628
|
+
}
|
|
2629
|
+
};
|
|
2630
|
+
}
|
|
2631
|
+
await saveLocalConfig(newConfig);
|
|
2632
|
+
log("");
|
|
2633
|
+
success("Identity created successfully");
|
|
2634
|
+
log("");
|
|
2635
|
+
log(` Slug: ${slug}`);
|
|
2636
|
+
log(` Name: ${name}`);
|
|
2637
|
+
if (email) {
|
|
2638
|
+
log(` Email: ${email}`);
|
|
2639
|
+
}
|
|
2640
|
+
if (github) {
|
|
2641
|
+
log(` GitHub: ${github}`);
|
|
2642
|
+
}
|
|
2643
|
+
log(` Public Key: ${keyPair.publicKey.slice(0, 32)}...`);
|
|
2644
|
+
log(` Private Key: ${keyStorageDescription}`);
|
|
2645
|
+
log("");
|
|
2646
|
+
if (!existingConfig) {
|
|
2647
|
+
success(`Set as active identity`);
|
|
2648
|
+
log("");
|
|
2649
|
+
} else {
|
|
2650
|
+
log(`To use this identity, run: attest-it identity use ${slug}`);
|
|
2651
|
+
log("");
|
|
2652
|
+
}
|
|
2653
|
+
} catch (err) {
|
|
2654
|
+
if (err instanceof Error) {
|
|
2655
|
+
error(err.message);
|
|
2656
|
+
} else {
|
|
2657
|
+
error("Unknown error occurred");
|
|
2658
|
+
}
|
|
2659
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
1568
2660
|
}
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
2661
|
+
}
|
|
2662
|
+
var useCommand = new Command("use").description("Set the active identity").argument("<slug>", "Identity slug to activate").action(async (slug) => {
|
|
2663
|
+
await runUse(slug);
|
|
2664
|
+
});
|
|
2665
|
+
async function runUse(slug) {
|
|
2666
|
+
try {
|
|
2667
|
+
const config = await loadLocalConfig();
|
|
2668
|
+
if (!config) {
|
|
2669
|
+
error("No identities configured");
|
|
2670
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
2671
|
+
}
|
|
2672
|
+
const identity = config.identities[slug];
|
|
2673
|
+
if (!identity) {
|
|
2674
|
+
error(`Identity "${slug}" not found`);
|
|
2675
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
2676
|
+
}
|
|
2677
|
+
const newConfig = {
|
|
2678
|
+
...config,
|
|
2679
|
+
activeIdentity: slug
|
|
2680
|
+
};
|
|
2681
|
+
await saveLocalConfig(newConfig);
|
|
2682
|
+
success(`Active identity set to: ${identity.name} (${slug})`);
|
|
2683
|
+
} catch (err) {
|
|
2684
|
+
if (err instanceof Error) {
|
|
2685
|
+
error(err.message);
|
|
2686
|
+
} else {
|
|
2687
|
+
error("Unknown error occurred");
|
|
2688
|
+
}
|
|
2689
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
var showCommand = new Command("show").description("Show identity details").argument("[slug]", "Identity slug (defaults to active identity)").action(async (slug) => {
|
|
2693
|
+
await runShow(slug);
|
|
2694
|
+
});
|
|
2695
|
+
async function runShow(slug) {
|
|
2696
|
+
try {
|
|
2697
|
+
const config = await loadLocalConfig();
|
|
2698
|
+
if (!config) {
|
|
2699
|
+
error("No identities configured");
|
|
2700
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
2701
|
+
}
|
|
2702
|
+
const theme3 = getTheme2();
|
|
2703
|
+
let targetSlug;
|
|
2704
|
+
let isActive;
|
|
2705
|
+
if (slug) {
|
|
2706
|
+
targetSlug = slug;
|
|
2707
|
+
isActive = slug === config.activeIdentity;
|
|
2708
|
+
} else {
|
|
2709
|
+
targetSlug = config.activeIdentity;
|
|
2710
|
+
isActive = true;
|
|
2711
|
+
}
|
|
2712
|
+
const identity = config.identities[targetSlug];
|
|
2713
|
+
if (!identity) {
|
|
2714
|
+
error(`Identity "${targetSlug}" not found`);
|
|
2715
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
2716
|
+
}
|
|
2717
|
+
log("");
|
|
2718
|
+
log(theme3.blue.bold()("Identity Details:"));
|
|
2719
|
+
log("");
|
|
2720
|
+
log(` Slug: ${theme3.blue(targetSlug)}${isActive ? theme3.green(" (active)") : ""}`);
|
|
2721
|
+
log(` Name: ${identity.name}`);
|
|
2722
|
+
if (identity.email) {
|
|
2723
|
+
log(` Email: ${identity.email}`);
|
|
2724
|
+
}
|
|
2725
|
+
if (identity.github) {
|
|
2726
|
+
log(` GitHub: ${identity.github}`);
|
|
2727
|
+
}
|
|
1574
2728
|
log("");
|
|
1575
|
-
|
|
1576
|
-
|
|
2729
|
+
log(` Public Key: ${identity.publicKey}`);
|
|
2730
|
+
log("");
|
|
2731
|
+
log(" Private Key Storage:");
|
|
2732
|
+
switch (identity.privateKey.type) {
|
|
2733
|
+
case "file":
|
|
2734
|
+
log(` Type: File`);
|
|
2735
|
+
log(` Path: ${identity.privateKey.path}`);
|
|
2736
|
+
break;
|
|
2737
|
+
case "keychain":
|
|
2738
|
+
log(` Type: macOS Keychain`);
|
|
2739
|
+
log(` Service: ${identity.privateKey.service}`);
|
|
2740
|
+
log(` Account: ${identity.privateKey.account}`);
|
|
2741
|
+
break;
|
|
2742
|
+
case "1password":
|
|
2743
|
+
log(` Type: 1Password`);
|
|
2744
|
+
if (identity.privateKey.account) {
|
|
2745
|
+
log(` Account: ${identity.privateKey.account}`);
|
|
2746
|
+
}
|
|
2747
|
+
log(` Vault: ${identity.privateKey.vault}`);
|
|
2748
|
+
log(` Item: ${identity.privateKey.item}`);
|
|
2749
|
+
if (identity.privateKey.field) {
|
|
2750
|
+
log(` Field: ${identity.privateKey.field}`);
|
|
2751
|
+
}
|
|
2752
|
+
break;
|
|
1577
2753
|
}
|
|
1578
|
-
|
|
1579
|
-
|
|
2754
|
+
log("");
|
|
2755
|
+
} catch (err) {
|
|
2756
|
+
if (err instanceof Error) {
|
|
2757
|
+
error(err.message);
|
|
2758
|
+
} else {
|
|
2759
|
+
error("Unknown error occurred");
|
|
1580
2760
|
}
|
|
2761
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
1581
2762
|
}
|
|
1582
2763
|
}
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
2764
|
+
var editCommand = new Command("edit").description("Edit identity or rotate keypair").argument("<slug>", "Identity slug to edit").action(async (slug) => {
|
|
2765
|
+
await runEdit(slug);
|
|
2766
|
+
});
|
|
2767
|
+
async function runEdit(slug) {
|
|
2768
|
+
try {
|
|
2769
|
+
const config = await loadLocalConfig();
|
|
2770
|
+
if (!config) {
|
|
2771
|
+
error("No identities configured");
|
|
2772
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
2773
|
+
}
|
|
2774
|
+
const identity = config.identities[slug];
|
|
2775
|
+
if (!identity) {
|
|
2776
|
+
error(`Identity "${slug}" not found`);
|
|
2777
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
2778
|
+
}
|
|
2779
|
+
const theme3 = getTheme2();
|
|
2780
|
+
log("");
|
|
2781
|
+
log(theme3.blue.bold()(`Edit Identity: ${slug}`));
|
|
2782
|
+
log("");
|
|
2783
|
+
const name = await input({
|
|
2784
|
+
message: "Display name:",
|
|
2785
|
+
default: identity.name,
|
|
2786
|
+
validate: (value) => {
|
|
2787
|
+
if (!value || value.trim().length === 0) {
|
|
2788
|
+
return "Name cannot be empty";
|
|
2789
|
+
}
|
|
2790
|
+
return true;
|
|
2791
|
+
}
|
|
2792
|
+
});
|
|
2793
|
+
const email = await input({
|
|
2794
|
+
message: "Email (optional):",
|
|
2795
|
+
default: identity.email ?? ""
|
|
2796
|
+
});
|
|
2797
|
+
const github = await input({
|
|
2798
|
+
message: "GitHub username (optional):",
|
|
2799
|
+
default: identity.github ?? ""
|
|
2800
|
+
});
|
|
2801
|
+
const rotateKey = await confirm({
|
|
2802
|
+
message: "Rotate keypair (generate new keys)?",
|
|
2803
|
+
default: false
|
|
2804
|
+
});
|
|
2805
|
+
let publicKey = identity.publicKey;
|
|
2806
|
+
const privateKeyRef = identity.privateKey;
|
|
2807
|
+
if (rotateKey) {
|
|
2808
|
+
log("");
|
|
2809
|
+
log("Generating new Ed25519 keypair...");
|
|
2810
|
+
const keyPair = generateEd25519KeyPair();
|
|
2811
|
+
publicKey = keyPair.publicKey;
|
|
2812
|
+
switch (identity.privateKey.type) {
|
|
2813
|
+
case "file": {
|
|
2814
|
+
await writeFile(identity.privateKey.path, keyPair.privateKey, { mode: 384 });
|
|
2815
|
+
log(` Updated private key at: ${identity.privateKey.path}`);
|
|
2816
|
+
break;
|
|
2817
|
+
}
|
|
2818
|
+
case "keychain": {
|
|
2819
|
+
const { execFile } = await import('child_process');
|
|
2820
|
+
const { promisify } = await import('util');
|
|
2821
|
+
const execFileAsync = promisify(execFile);
|
|
2822
|
+
const encodedKey = Buffer.from(keyPair.privateKey).toString("base64");
|
|
2823
|
+
try {
|
|
2824
|
+
await execFileAsync("security", [
|
|
2825
|
+
"delete-generic-password",
|
|
2826
|
+
"-s",
|
|
2827
|
+
identity.privateKey.service,
|
|
2828
|
+
"-a",
|
|
2829
|
+
identity.privateKey.account
|
|
2830
|
+
]);
|
|
2831
|
+
await execFileAsync("security", [
|
|
2832
|
+
"add-generic-password",
|
|
2833
|
+
"-s",
|
|
2834
|
+
identity.privateKey.service,
|
|
2835
|
+
"-a",
|
|
2836
|
+
identity.privateKey.account,
|
|
2837
|
+
"-w",
|
|
2838
|
+
encodedKey,
|
|
2839
|
+
"-U"
|
|
2840
|
+
]);
|
|
2841
|
+
log(` Updated private key in macOS Keychain`);
|
|
2842
|
+
} catch (err) {
|
|
2843
|
+
throw new Error(
|
|
2844
|
+
`Failed to update key in macOS Keychain: ${err instanceof Error ? err.message : String(err)}`
|
|
2845
|
+
);
|
|
2846
|
+
}
|
|
2847
|
+
break;
|
|
2848
|
+
}
|
|
2849
|
+
case "1password": {
|
|
2850
|
+
const { execFile } = await import('child_process');
|
|
2851
|
+
const { promisify } = await import('util');
|
|
2852
|
+
const execFileAsync = promisify(execFile);
|
|
2853
|
+
try {
|
|
2854
|
+
const opArgs = [
|
|
2855
|
+
"item",
|
|
2856
|
+
"edit",
|
|
2857
|
+
identity.privateKey.item,
|
|
2858
|
+
"--vault",
|
|
2859
|
+
identity.privateKey.vault,
|
|
2860
|
+
`privateKey[password]=${keyPair.privateKey}`
|
|
2861
|
+
];
|
|
2862
|
+
if (identity.privateKey.account) {
|
|
2863
|
+
opArgs.push("--account", identity.privateKey.account);
|
|
2864
|
+
}
|
|
2865
|
+
await execFileAsync("op", opArgs);
|
|
2866
|
+
log(` Updated private key in 1Password`);
|
|
2867
|
+
} catch (err) {
|
|
2868
|
+
throw new Error(
|
|
2869
|
+
`Failed to update key in 1Password: ${err instanceof Error ? err.message : String(err)}`
|
|
2870
|
+
);
|
|
2871
|
+
}
|
|
2872
|
+
break;
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
const updatedIdentity = {
|
|
2877
|
+
name,
|
|
2878
|
+
publicKey,
|
|
2879
|
+
privateKey: privateKeyRef,
|
|
2880
|
+
...email && { email },
|
|
2881
|
+
...github && { github }
|
|
2882
|
+
};
|
|
2883
|
+
const newConfig = {
|
|
2884
|
+
...config,
|
|
2885
|
+
identities: {
|
|
2886
|
+
...config.identities,
|
|
2887
|
+
[slug]: updatedIdentity
|
|
2888
|
+
}
|
|
2889
|
+
};
|
|
2890
|
+
await saveLocalConfig(newConfig);
|
|
2891
|
+
log("");
|
|
2892
|
+
success("Identity updated successfully");
|
|
2893
|
+
log("");
|
|
2894
|
+
if (rotateKey) {
|
|
2895
|
+
log(" New Public Key: " + publicKey.slice(0, 32) + "...");
|
|
2896
|
+
log("");
|
|
2897
|
+
log(
|
|
2898
|
+
theme3.yellow(
|
|
2899
|
+
" Warning: If this identity is used in team configurations,\n you must update those configurations with the new public key."
|
|
2900
|
+
)
|
|
2901
|
+
);
|
|
2902
|
+
log("");
|
|
2903
|
+
}
|
|
2904
|
+
} catch (err) {
|
|
2905
|
+
if (err instanceof Error) {
|
|
2906
|
+
error(err.message);
|
|
2907
|
+
} else {
|
|
2908
|
+
error("Unknown error occurred");
|
|
2909
|
+
}
|
|
2910
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
1586
2911
|
}
|
|
1587
|
-
|
|
1588
|
-
|
|
2912
|
+
}
|
|
2913
|
+
var removeCommand = new Command("remove").description("Delete identity and optionally delete private key").argument("<slug>", "Identity slug to remove").action(async (slug) => {
|
|
2914
|
+
await runRemove(slug);
|
|
2915
|
+
});
|
|
2916
|
+
async function runRemove(slug) {
|
|
2917
|
+
try {
|
|
2918
|
+
const config = await loadLocalConfig();
|
|
2919
|
+
if (!config) {
|
|
2920
|
+
error("No identities configured");
|
|
2921
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
2922
|
+
}
|
|
2923
|
+
const identity = config.identities[slug];
|
|
2924
|
+
if (!identity) {
|
|
2925
|
+
error(`Identity "${slug}" not found`);
|
|
2926
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
2927
|
+
}
|
|
2928
|
+
const theme3 = getTheme2();
|
|
2929
|
+
log("");
|
|
2930
|
+
log(theme3.blue.bold()(`Remove Identity: ${slug}`));
|
|
2931
|
+
log("");
|
|
2932
|
+
log(` Name: ${identity.name}`);
|
|
2933
|
+
if (identity.email) {
|
|
2934
|
+
log(` Email: ${identity.email}`);
|
|
2935
|
+
}
|
|
2936
|
+
log("");
|
|
2937
|
+
const confirmDelete = await confirm({
|
|
2938
|
+
message: "Are you sure you want to delete this identity?",
|
|
2939
|
+
default: false
|
|
2940
|
+
});
|
|
2941
|
+
if (!confirmDelete) {
|
|
2942
|
+
log("Cancelled");
|
|
2943
|
+
process.exit(ExitCode.CANCELLED);
|
|
2944
|
+
}
|
|
2945
|
+
const deletePrivateKey = await confirm({
|
|
2946
|
+
message: "Also delete the private key from storage?",
|
|
2947
|
+
default: false
|
|
2948
|
+
});
|
|
2949
|
+
if (deletePrivateKey) {
|
|
2950
|
+
switch (identity.privateKey.type) {
|
|
2951
|
+
case "file": {
|
|
2952
|
+
try {
|
|
2953
|
+
await unlink(identity.privateKey.path);
|
|
2954
|
+
log(` Deleted private key file: ${identity.privateKey.path}`);
|
|
2955
|
+
} catch (err) {
|
|
2956
|
+
if (err && typeof err === "object" && "code" in err && err.code !== "ENOENT") {
|
|
2957
|
+
throw err;
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
break;
|
|
2961
|
+
}
|
|
2962
|
+
case "keychain": {
|
|
2963
|
+
const { execFile } = await import('child_process');
|
|
2964
|
+
const { promisify } = await import('util');
|
|
2965
|
+
const execFileAsync = promisify(execFile);
|
|
2966
|
+
try {
|
|
2967
|
+
await execFileAsync("security", [
|
|
2968
|
+
"delete-generic-password",
|
|
2969
|
+
"-s",
|
|
2970
|
+
identity.privateKey.service,
|
|
2971
|
+
"-a",
|
|
2972
|
+
identity.privateKey.account
|
|
2973
|
+
]);
|
|
2974
|
+
log(` Deleted private key from macOS Keychain`);
|
|
2975
|
+
} catch (err) {
|
|
2976
|
+
if (err instanceof Error && !err.message.includes("could not be found") && !err.message.includes("does not exist")) {
|
|
2977
|
+
throw err;
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
break;
|
|
2981
|
+
}
|
|
2982
|
+
case "1password": {
|
|
2983
|
+
const { execFile } = await import('child_process');
|
|
2984
|
+
const { promisify } = await import('util');
|
|
2985
|
+
const execFileAsync = promisify(execFile);
|
|
2986
|
+
try {
|
|
2987
|
+
const opArgs = [
|
|
2988
|
+
"item",
|
|
2989
|
+
"delete",
|
|
2990
|
+
identity.privateKey.item,
|
|
2991
|
+
"--vault",
|
|
2992
|
+
identity.privateKey.vault
|
|
2993
|
+
];
|
|
2994
|
+
if (identity.privateKey.account) {
|
|
2995
|
+
opArgs.push("--account", identity.privateKey.account);
|
|
2996
|
+
}
|
|
2997
|
+
await execFileAsync("op", opArgs);
|
|
2998
|
+
log(` Deleted private key from 1Password`);
|
|
2999
|
+
} catch (err) {
|
|
3000
|
+
if (err instanceof Error && !err.message.includes("not found") && !err.message.includes("doesn't exist")) {
|
|
3001
|
+
throw err;
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
break;
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
const { [slug]: _removed, ...remainingIdentities } = config.identities;
|
|
3009
|
+
if (Object.keys(remainingIdentities).length === 0) {
|
|
3010
|
+
error("Cannot remove last identity");
|
|
3011
|
+
log("");
|
|
3012
|
+
log("At least one identity must exist");
|
|
3013
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
3014
|
+
}
|
|
3015
|
+
let newActiveIdentity = config.activeIdentity;
|
|
3016
|
+
if (slug === config.activeIdentity) {
|
|
3017
|
+
const firstKey = Object.keys(remainingIdentities)[0];
|
|
3018
|
+
if (!firstKey) {
|
|
3019
|
+
throw new Error("No remaining identities after removal");
|
|
3020
|
+
}
|
|
3021
|
+
newActiveIdentity = firstKey;
|
|
3022
|
+
log("");
|
|
3023
|
+
log(theme3.yellow(` Removed active identity. New active identity: ${newActiveIdentity}`));
|
|
3024
|
+
}
|
|
3025
|
+
const newConfig = {
|
|
3026
|
+
activeIdentity: newActiveIdentity,
|
|
3027
|
+
identities: remainingIdentities
|
|
3028
|
+
};
|
|
3029
|
+
await saveLocalConfig(newConfig);
|
|
3030
|
+
log("");
|
|
3031
|
+
success(`Identity "${slug}" removed`);
|
|
3032
|
+
log("");
|
|
3033
|
+
} catch (err) {
|
|
3034
|
+
if (err instanceof Error) {
|
|
3035
|
+
error(err.message);
|
|
3036
|
+
} else {
|
|
3037
|
+
error("Unknown error occurred");
|
|
3038
|
+
}
|
|
3039
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
1589
3040
|
}
|
|
1590
|
-
|
|
1591
|
-
|
|
3041
|
+
}
|
|
3042
|
+
var exportCommand = new Command("export").description("Export identity for team onboarding (YAML snippet)").argument("[slug]", "Identity slug to export (defaults to active identity)").action(async (slug) => {
|
|
3043
|
+
await runExport(slug);
|
|
3044
|
+
});
|
|
3045
|
+
async function runExport(slug) {
|
|
3046
|
+
try {
|
|
3047
|
+
const config = await loadLocalConfig();
|
|
3048
|
+
if (!config) {
|
|
3049
|
+
error("No identities configured");
|
|
3050
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
3051
|
+
}
|
|
3052
|
+
const theme3 = getTheme2();
|
|
3053
|
+
const targetSlug = slug ?? config.activeIdentity;
|
|
3054
|
+
const identity = config.identities[targetSlug];
|
|
3055
|
+
if (!identity) {
|
|
3056
|
+
error(`Identity "${targetSlug}" not found`);
|
|
3057
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
3058
|
+
}
|
|
3059
|
+
log("");
|
|
3060
|
+
log(theme3.blue.bold()("Team Configuration YAML:"));
|
|
3061
|
+
log("");
|
|
3062
|
+
log(theme3.muted("# Add this to your team config file (.attest-it/team-config.yaml)"));
|
|
3063
|
+
log("");
|
|
3064
|
+
const exportData = {
|
|
3065
|
+
name: identity.name,
|
|
3066
|
+
publicKey: identity.publicKey
|
|
3067
|
+
};
|
|
3068
|
+
if (identity.email) {
|
|
3069
|
+
exportData.email = identity.email;
|
|
3070
|
+
}
|
|
3071
|
+
if (identity.github) {
|
|
3072
|
+
exportData.github = identity.github;
|
|
3073
|
+
}
|
|
3074
|
+
const yamlData = {
|
|
3075
|
+
[targetSlug]: exportData
|
|
3076
|
+
};
|
|
3077
|
+
const yamlString = stringify(yamlData);
|
|
3078
|
+
log(yamlString);
|
|
3079
|
+
log("");
|
|
3080
|
+
log(theme3.muted('# The team owner can add this to the "members:" section'));
|
|
3081
|
+
log("");
|
|
3082
|
+
} catch (err) {
|
|
3083
|
+
if (err instanceof Error) {
|
|
3084
|
+
error(err.message);
|
|
3085
|
+
} else {
|
|
3086
|
+
error("Unknown error occurred");
|
|
3087
|
+
}
|
|
3088
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
1592
3089
|
}
|
|
1593
|
-
|
|
1594
|
-
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3092
|
+
// src/commands/identity/index.ts
|
|
3093
|
+
var identityCommand = new Command("identity").description("Manage local identities and keypairs").addCommand(listCommand).addCommand(createCommand).addCommand(useCommand).addCommand(showCommand).addCommand(editCommand).addCommand(removeCommand).addCommand(exportCommand);
|
|
3094
|
+
var whoamiCommand = new Command("whoami").description("Show the current active identity").action(async () => {
|
|
3095
|
+
await runWhoami();
|
|
3096
|
+
});
|
|
3097
|
+
async function runWhoami() {
|
|
3098
|
+
try {
|
|
3099
|
+
const config = await loadLocalConfig();
|
|
3100
|
+
if (!config) {
|
|
3101
|
+
error("No identities configured");
|
|
3102
|
+
log("");
|
|
3103
|
+
log("Run: attest-it identity create");
|
|
3104
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
3105
|
+
}
|
|
3106
|
+
const identity = getActiveIdentity(config);
|
|
3107
|
+
if (!identity) {
|
|
3108
|
+
error("Active identity not found");
|
|
3109
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
3110
|
+
}
|
|
3111
|
+
const theme3 = getTheme2();
|
|
3112
|
+
log("");
|
|
3113
|
+
log(theme3.green.bold()(identity.name));
|
|
3114
|
+
if (identity.email) {
|
|
3115
|
+
log(theme3.muted(identity.email));
|
|
3116
|
+
}
|
|
3117
|
+
if (identity.github) {
|
|
3118
|
+
log(theme3.muted("@" + identity.github));
|
|
3119
|
+
}
|
|
3120
|
+
log("");
|
|
3121
|
+
log(`Identity: ${theme3.blue(config.activeIdentity)}`);
|
|
3122
|
+
log("");
|
|
3123
|
+
} catch (err) {
|
|
3124
|
+
if (err instanceof Error) {
|
|
3125
|
+
error(err.message);
|
|
3126
|
+
} else {
|
|
3127
|
+
error("Unknown error occurred");
|
|
3128
|
+
}
|
|
3129
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
1595
3130
|
}
|
|
1596
|
-
|
|
1597
|
-
|
|
3131
|
+
}
|
|
3132
|
+
var listCommand2 = new Command("list").description("List team members and their authorizations").action(async () => {
|
|
3133
|
+
await runList2();
|
|
3134
|
+
});
|
|
3135
|
+
async function runList2() {
|
|
3136
|
+
try {
|
|
3137
|
+
const config = await loadConfig();
|
|
3138
|
+
const attestItConfig = toAttestItConfig(config);
|
|
3139
|
+
if (!attestItConfig.team || Object.keys(attestItConfig.team).length === 0) {
|
|
3140
|
+
error("No team members configured");
|
|
3141
|
+
log("");
|
|
3142
|
+
log("Run: attest-it team add");
|
|
3143
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
3144
|
+
}
|
|
3145
|
+
const theme3 = getTheme2();
|
|
3146
|
+
const teamMembers = Object.entries(attestItConfig.team);
|
|
3147
|
+
log("");
|
|
3148
|
+
log(theme3.blue.bold()("Team Members:"));
|
|
3149
|
+
log("");
|
|
3150
|
+
for (const [slug, member] of teamMembers) {
|
|
3151
|
+
const keyPreview = member.publicKey.slice(0, 12) + "...";
|
|
3152
|
+
log(theme3.blue(slug));
|
|
3153
|
+
log(` Name: ${member.name}`);
|
|
3154
|
+
if (member.email) {
|
|
3155
|
+
log(` Email: ${member.email}`);
|
|
3156
|
+
}
|
|
3157
|
+
if (member.github) {
|
|
3158
|
+
log(` GitHub: ${member.github}`);
|
|
3159
|
+
}
|
|
3160
|
+
log(` Public Key: ${keyPreview}`);
|
|
3161
|
+
const authorizedGates = [];
|
|
3162
|
+
if (attestItConfig.gates) {
|
|
3163
|
+
for (const [gateId, gate] of Object.entries(attestItConfig.gates)) {
|
|
3164
|
+
if (gate.authorizedSigners.includes(slug)) {
|
|
3165
|
+
authorizedGates.push(gateId);
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
if (authorizedGates.length > 0) {
|
|
3170
|
+
log(` Gates: ${authorizedGates.join(", ")}`);
|
|
3171
|
+
} else {
|
|
3172
|
+
log(` Gates: ${theme3.muted("(none)")}`);
|
|
3173
|
+
}
|
|
3174
|
+
log("");
|
|
3175
|
+
}
|
|
3176
|
+
if (teamMembers.length === 1) {
|
|
3177
|
+
log(`1 team member configured`);
|
|
3178
|
+
} else {
|
|
3179
|
+
log(`${teamMembers.length.toString()} team members configured`);
|
|
3180
|
+
}
|
|
3181
|
+
log("");
|
|
3182
|
+
} catch (err) {
|
|
3183
|
+
if (err instanceof Error) {
|
|
3184
|
+
error(err.message);
|
|
3185
|
+
} else {
|
|
3186
|
+
error("Unknown error occurred");
|
|
3187
|
+
}
|
|
3188
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
1598
3189
|
}
|
|
1599
|
-
return "-";
|
|
1600
3190
|
}
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
)
|
|
3191
|
+
var addCommand = new Command("add").description("Add a new team member").action(async () => {
|
|
3192
|
+
await runAdd();
|
|
3193
|
+
});
|
|
3194
|
+
function validatePublicKey(value) {
|
|
3195
|
+
if (!value || value.trim().length === 0) {
|
|
3196
|
+
return "Public key cannot be empty";
|
|
3197
|
+
}
|
|
3198
|
+
const base64Regex = /^[A-Za-z0-9+/]+=*$/;
|
|
3199
|
+
if (!base64Regex.test(value)) {
|
|
3200
|
+
return "Public key must be valid Base64";
|
|
3201
|
+
}
|
|
3202
|
+
if (value.length !== 44) {
|
|
3203
|
+
return "Public key must be 44 characters (32 bytes in Base64)";
|
|
3204
|
+
}
|
|
3205
|
+
try {
|
|
3206
|
+
const decoded = Buffer.from(value, "base64");
|
|
3207
|
+
if (decoded.length !== 32) {
|
|
3208
|
+
return "Public key must decode to 32 bytes";
|
|
3209
|
+
}
|
|
3210
|
+
} catch {
|
|
3211
|
+
return "Invalid Base64 encoding";
|
|
3212
|
+
}
|
|
3213
|
+
return true;
|
|
3214
|
+
}
|
|
3215
|
+
async function runAdd() {
|
|
3216
|
+
try {
|
|
3217
|
+
const theme3 = getTheme2();
|
|
3218
|
+
log("");
|
|
3219
|
+
log(theme3.blue.bold()("Add Team Member"));
|
|
3220
|
+
log("");
|
|
3221
|
+
const config = await loadConfig();
|
|
3222
|
+
const attestItConfig = toAttestItConfig(config);
|
|
3223
|
+
const existingTeam = attestItConfig.team ?? {};
|
|
3224
|
+
const slug = await input({
|
|
3225
|
+
message: "Member slug (unique identifier):",
|
|
3226
|
+
validate: (value) => {
|
|
3227
|
+
if (!value || value.trim().length === 0) {
|
|
3228
|
+
return "Slug cannot be empty";
|
|
3229
|
+
}
|
|
3230
|
+
if (!/^[a-z0-9-]+$/.test(value)) {
|
|
3231
|
+
return "Slug must contain only lowercase letters, numbers, and hyphens";
|
|
3232
|
+
}
|
|
3233
|
+
if (existingTeam[value]) {
|
|
3234
|
+
return `Team member "${value}" already exists`;
|
|
3235
|
+
}
|
|
3236
|
+
return true;
|
|
3237
|
+
}
|
|
3238
|
+
});
|
|
3239
|
+
const name = await input({
|
|
3240
|
+
message: "Display name:",
|
|
3241
|
+
validate: (value) => {
|
|
3242
|
+
if (!value || value.trim().length === 0) {
|
|
3243
|
+
return "Name cannot be empty";
|
|
3244
|
+
}
|
|
3245
|
+
return true;
|
|
3246
|
+
}
|
|
3247
|
+
});
|
|
3248
|
+
const email = await input({
|
|
3249
|
+
message: "Email (optional):",
|
|
3250
|
+
default: ""
|
|
3251
|
+
});
|
|
3252
|
+
const github = await input({
|
|
3253
|
+
message: "GitHub username (optional):",
|
|
3254
|
+
default: ""
|
|
3255
|
+
});
|
|
3256
|
+
log("");
|
|
3257
|
+
log('Paste the public key (from "attest-it identity export"):');
|
|
3258
|
+
const publicKey = await input({
|
|
3259
|
+
message: "Public key:",
|
|
3260
|
+
validate: validatePublicKey
|
|
3261
|
+
});
|
|
3262
|
+
let authorizedGates = [];
|
|
3263
|
+
if (attestItConfig.gates && Object.keys(attestItConfig.gates).length > 0) {
|
|
3264
|
+
log("");
|
|
3265
|
+
const gateChoices = Object.entries(attestItConfig.gates).map(([gateId, gate]) => ({
|
|
3266
|
+
name: `${gateId} - ${gate.name}`,
|
|
3267
|
+
value: gateId
|
|
3268
|
+
}));
|
|
3269
|
+
authorizedGates = await checkbox({
|
|
3270
|
+
message: "Select gates to authorize (use space to select):",
|
|
3271
|
+
choices: gateChoices
|
|
3272
|
+
});
|
|
3273
|
+
}
|
|
3274
|
+
const teamMember = {
|
|
3275
|
+
name,
|
|
3276
|
+
publicKey: publicKey.trim()
|
|
3277
|
+
};
|
|
3278
|
+
if (email && email.trim().length > 0) {
|
|
3279
|
+
teamMember.email = email.trim();
|
|
3280
|
+
}
|
|
3281
|
+
if (github && github.trim().length > 0) {
|
|
3282
|
+
teamMember.github = github.trim();
|
|
3283
|
+
}
|
|
3284
|
+
const updatedConfig = {
|
|
3285
|
+
...config,
|
|
3286
|
+
team: {
|
|
3287
|
+
...existingTeam,
|
|
3288
|
+
[slug]: teamMember
|
|
3289
|
+
}
|
|
3290
|
+
};
|
|
3291
|
+
if (authorizedGates.length > 0 && updatedConfig.gates) {
|
|
3292
|
+
for (const gateId of authorizedGates) {
|
|
3293
|
+
const gate = updatedConfig.gates[gateId];
|
|
3294
|
+
if (gate) {
|
|
3295
|
+
if (!gate.authorizedSigners.includes(slug)) {
|
|
3296
|
+
gate.authorizedSigners.push(slug);
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
const configPath = findConfigPath();
|
|
3302
|
+
if (!configPath) {
|
|
3303
|
+
error("Configuration file not found");
|
|
3304
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
3305
|
+
}
|
|
3306
|
+
const yamlContent = stringify(updatedConfig);
|
|
3307
|
+
await writeFile(configPath, yamlContent, "utf8");
|
|
3308
|
+
log("");
|
|
3309
|
+
success(`Team member "${slug}" added successfully`);
|
|
3310
|
+
if (authorizedGates.length > 0) {
|
|
3311
|
+
log(`Authorized for gates: ${authorizedGates.join(", ")}`);
|
|
3312
|
+
}
|
|
3313
|
+
log("");
|
|
3314
|
+
} catch (err) {
|
|
3315
|
+
if (err instanceof Error) {
|
|
3316
|
+
error(err.message);
|
|
3317
|
+
} else {
|
|
3318
|
+
error("Unknown error occurred");
|
|
3319
|
+
}
|
|
3320
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
var editCommand2 = new Command("edit").description("Edit a team member").argument("<slug>", "Team member slug to edit").action(async (slug) => {
|
|
3324
|
+
await runEdit2(slug);
|
|
3325
|
+
});
|
|
3326
|
+
function validatePublicKey2(value) {
|
|
3327
|
+
if (!value || value.trim().length === 0) {
|
|
3328
|
+
return "Public key cannot be empty";
|
|
3329
|
+
}
|
|
3330
|
+
const base64Regex = /^[A-Za-z0-9+/]+=*$/;
|
|
3331
|
+
if (!base64Regex.test(value)) {
|
|
3332
|
+
return "Public key must be valid Base64";
|
|
3333
|
+
}
|
|
3334
|
+
if (value.length !== 44) {
|
|
3335
|
+
return "Public key must be 44 characters (32 bytes in Base64)";
|
|
3336
|
+
}
|
|
3337
|
+
try {
|
|
3338
|
+
const decoded = Buffer.from(value, "base64");
|
|
3339
|
+
if (decoded.length !== 32) {
|
|
3340
|
+
return "Public key must decode to 32 bytes";
|
|
3341
|
+
}
|
|
3342
|
+
} catch {
|
|
3343
|
+
return "Invalid Base64 encoding";
|
|
3344
|
+
}
|
|
3345
|
+
return true;
|
|
3346
|
+
}
|
|
3347
|
+
async function runEdit2(slug) {
|
|
3348
|
+
try {
|
|
3349
|
+
const theme3 = getTheme2();
|
|
3350
|
+
const config = await loadConfig();
|
|
3351
|
+
const attestItConfig = toAttestItConfig(config);
|
|
3352
|
+
const existingMember = attestItConfig.team?.[slug];
|
|
3353
|
+
if (!existingMember) {
|
|
3354
|
+
error(`Team member "${slug}" not found`);
|
|
3355
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
3356
|
+
}
|
|
3357
|
+
log("");
|
|
3358
|
+
log(theme3.blue.bold()(`Edit Team Member: ${slug}`));
|
|
3359
|
+
log("");
|
|
3360
|
+
log(theme3.muted("Leave blank to keep current value"));
|
|
3361
|
+
log("");
|
|
3362
|
+
const name = await input({
|
|
3363
|
+
message: "Display name:",
|
|
3364
|
+
default: existingMember.name,
|
|
3365
|
+
validate: (value) => {
|
|
3366
|
+
if (!value || value.trim().length === 0) {
|
|
3367
|
+
return "Name cannot be empty";
|
|
3368
|
+
}
|
|
3369
|
+
return true;
|
|
3370
|
+
}
|
|
3371
|
+
});
|
|
3372
|
+
const email = await input({
|
|
3373
|
+
message: "Email (optional):",
|
|
3374
|
+
default: existingMember.email ?? ""
|
|
3375
|
+
});
|
|
3376
|
+
const github = await input({
|
|
3377
|
+
message: "GitHub username (optional):",
|
|
3378
|
+
default: existingMember.github ?? ""
|
|
3379
|
+
});
|
|
3380
|
+
const updateKey = await confirm({
|
|
3381
|
+
message: "Update public key?",
|
|
3382
|
+
default: false
|
|
3383
|
+
});
|
|
3384
|
+
let publicKey = existingMember.publicKey;
|
|
3385
|
+
if (updateKey) {
|
|
3386
|
+
log("");
|
|
3387
|
+
log("Paste the new public key:");
|
|
3388
|
+
publicKey = await input({
|
|
3389
|
+
message: "Public key:",
|
|
3390
|
+
default: existingMember.publicKey,
|
|
3391
|
+
validate: validatePublicKey2
|
|
3392
|
+
});
|
|
3393
|
+
}
|
|
3394
|
+
const currentGates = [];
|
|
3395
|
+
if (attestItConfig.gates) {
|
|
3396
|
+
for (const [gateId, gate] of Object.entries(attestItConfig.gates)) {
|
|
3397
|
+
if (gate.authorizedSigners.includes(slug)) {
|
|
3398
|
+
currentGates.push(gateId);
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
let selectedGates = currentGates;
|
|
3403
|
+
if (attestItConfig.gates && Object.keys(attestItConfig.gates).length > 0) {
|
|
3404
|
+
log("");
|
|
3405
|
+
const gateChoices = Object.entries(attestItConfig.gates).map(([gateId, gate]) => ({
|
|
3406
|
+
name: `${gateId} - ${gate.name}`,
|
|
3407
|
+
value: gateId,
|
|
3408
|
+
checked: currentGates.includes(gateId)
|
|
3409
|
+
}));
|
|
3410
|
+
selectedGates = await checkbox({
|
|
3411
|
+
message: "Select gates to authorize (use space to select):",
|
|
3412
|
+
choices: gateChoices
|
|
3413
|
+
});
|
|
3414
|
+
}
|
|
3415
|
+
const updatedMember = {
|
|
3416
|
+
name: name.trim(),
|
|
3417
|
+
publicKey: publicKey.trim()
|
|
3418
|
+
};
|
|
3419
|
+
if (email && email.trim().length > 0) {
|
|
3420
|
+
updatedMember.email = email.trim();
|
|
3421
|
+
}
|
|
3422
|
+
if (github && github.trim().length > 0) {
|
|
3423
|
+
updatedMember.github = github.trim();
|
|
3424
|
+
}
|
|
3425
|
+
const updatedConfig = {
|
|
3426
|
+
...config,
|
|
3427
|
+
team: {
|
|
3428
|
+
...attestItConfig.team,
|
|
3429
|
+
[slug]: updatedMember
|
|
3430
|
+
}
|
|
3431
|
+
};
|
|
3432
|
+
if (updatedConfig.gates) {
|
|
3433
|
+
for (const [gateId, gate] of Object.entries(updatedConfig.gates)) {
|
|
3434
|
+
if (currentGates.includes(gateId) && !selectedGates.includes(gateId)) {
|
|
3435
|
+
gate.authorizedSigners = gate.authorizedSigners.filter((s) => s !== slug);
|
|
3436
|
+
}
|
|
3437
|
+
if (!currentGates.includes(gateId) && selectedGates.includes(gateId)) {
|
|
3438
|
+
if (!gate.authorizedSigners.includes(slug)) {
|
|
3439
|
+
gate.authorizedSigners.push(slug);
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
const configPath = findConfigPath();
|
|
3445
|
+
if (!configPath) {
|
|
3446
|
+
error("Configuration file not found");
|
|
3447
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
3448
|
+
}
|
|
3449
|
+
const yamlContent = stringify(updatedConfig);
|
|
3450
|
+
await writeFile(configPath, yamlContent, "utf8");
|
|
3451
|
+
log("");
|
|
3452
|
+
success(`Team member "${slug}" updated successfully`);
|
|
3453
|
+
if (selectedGates.length > 0) {
|
|
3454
|
+
log(`Authorized for gates: ${selectedGates.join(", ")}`);
|
|
3455
|
+
} else {
|
|
3456
|
+
log("Not authorized for any gates");
|
|
3457
|
+
}
|
|
3458
|
+
log("");
|
|
3459
|
+
} catch (err) {
|
|
3460
|
+
if (err instanceof Error) {
|
|
3461
|
+
error(err.message);
|
|
3462
|
+
} else {
|
|
3463
|
+
error("Unknown error occurred");
|
|
3464
|
+
}
|
|
3465
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
3466
|
+
}
|
|
3467
|
+
}
|
|
3468
|
+
var removeCommand2 = new Command("remove").description("Remove a team member").argument("<slug>", "Team member slug to remove").option("-f, --force", "Skip confirmation prompt").action(async (slug, options) => {
|
|
3469
|
+
await runRemove2(slug, options);
|
|
3470
|
+
});
|
|
3471
|
+
async function runRemove2(slug, options) {
|
|
3472
|
+
try {
|
|
3473
|
+
const theme3 = getTheme2();
|
|
3474
|
+
const config = await loadConfig();
|
|
3475
|
+
const attestItConfig = toAttestItConfig(config);
|
|
3476
|
+
const existingMember = attestItConfig.team?.[slug];
|
|
3477
|
+
if (!existingMember) {
|
|
3478
|
+
error(`Team member "${slug}" not found`);
|
|
3479
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
3480
|
+
}
|
|
3481
|
+
log("");
|
|
3482
|
+
log(theme3.blue.bold()(`Remove Team Member: ${slug}`));
|
|
3483
|
+
log("");
|
|
3484
|
+
log(`Name: ${existingMember.name}`);
|
|
3485
|
+
if (existingMember.email) {
|
|
3486
|
+
log(`Email: ${existingMember.email}`);
|
|
3487
|
+
}
|
|
3488
|
+
if (existingMember.github) {
|
|
3489
|
+
log(`GitHub: ${existingMember.github}`);
|
|
3490
|
+
}
|
|
3491
|
+
log("");
|
|
3492
|
+
const projectRoot = process.cwd();
|
|
3493
|
+
let sealsFile;
|
|
3494
|
+
try {
|
|
3495
|
+
sealsFile = readSealsSync(projectRoot);
|
|
3496
|
+
} catch {
|
|
3497
|
+
sealsFile = { version: 1, seals: {} };
|
|
3498
|
+
}
|
|
3499
|
+
const sealsCreatedByMember = [];
|
|
3500
|
+
for (const [gateId, seal] of Object.entries(sealsFile.seals)) {
|
|
3501
|
+
if (seal.sealedBy === slug) {
|
|
3502
|
+
sealsCreatedByMember.push(gateId);
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
if (sealsCreatedByMember.length > 0) {
|
|
3506
|
+
warn("This member has created seals for the following gates:");
|
|
3507
|
+
for (const gateId of sealsCreatedByMember) {
|
|
3508
|
+
warn(` - ${gateId}`);
|
|
3509
|
+
}
|
|
3510
|
+
log("");
|
|
3511
|
+
warn("These seals will still be valid but attributed to a removed member.");
|
|
3512
|
+
log("");
|
|
3513
|
+
}
|
|
3514
|
+
const authorizedGates = [];
|
|
3515
|
+
if (attestItConfig.gates) {
|
|
3516
|
+
for (const [gateId, gate] of Object.entries(attestItConfig.gates)) {
|
|
3517
|
+
if (gate.authorizedSigners.includes(slug)) {
|
|
3518
|
+
authorizedGates.push(gateId);
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
if (authorizedGates.length > 0) {
|
|
3523
|
+
log("This member is authorized for the following gates:");
|
|
3524
|
+
for (const gateId of authorizedGates) {
|
|
3525
|
+
log(` - ${gateId}`);
|
|
3526
|
+
}
|
|
3527
|
+
log("");
|
|
3528
|
+
}
|
|
3529
|
+
if (!options.force) {
|
|
3530
|
+
const confirmed = await confirm({
|
|
3531
|
+
message: `Are you sure you want to remove "${slug}"?`,
|
|
3532
|
+
default: false
|
|
3533
|
+
});
|
|
3534
|
+
if (!confirmed) {
|
|
3535
|
+
error("Removal cancelled");
|
|
3536
|
+
process.exit(ExitCode.CANCELLED);
|
|
3537
|
+
}
|
|
3538
|
+
}
|
|
3539
|
+
const updatedTeam = { ...attestItConfig.team };
|
|
3540
|
+
delete updatedTeam[slug];
|
|
3541
|
+
const updatedConfig = {
|
|
3542
|
+
...config,
|
|
3543
|
+
team: updatedTeam
|
|
3544
|
+
};
|
|
3545
|
+
if (updatedConfig.gates) {
|
|
3546
|
+
for (const gate of Object.values(updatedConfig.gates)) {
|
|
3547
|
+
gate.authorizedSigners = gate.authorizedSigners.filter((s) => s !== slug);
|
|
3548
|
+
}
|
|
3549
|
+
}
|
|
3550
|
+
const configPath = findConfigPath();
|
|
3551
|
+
if (!configPath) {
|
|
3552
|
+
error("Configuration file not found");
|
|
3553
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
3554
|
+
}
|
|
3555
|
+
const yamlContent = stringify(updatedConfig);
|
|
3556
|
+
await writeFile(configPath, yamlContent, "utf8");
|
|
3557
|
+
log("");
|
|
3558
|
+
success(`Team member "${slug}" removed successfully`);
|
|
3559
|
+
log("");
|
|
3560
|
+
} catch (err) {
|
|
3561
|
+
if (err instanceof Error) {
|
|
3562
|
+
error(err.message);
|
|
3563
|
+
} else {
|
|
3564
|
+
error("Unknown error occurred");
|
|
3565
|
+
}
|
|
3566
|
+
process.exit(ExitCode.CONFIG_ERROR);
|
|
3567
|
+
}
|
|
1606
3568
|
}
|
|
1607
3569
|
|
|
1608
|
-
// src/index.ts
|
|
3570
|
+
// src/commands/team/index.ts
|
|
3571
|
+
var teamCommand = new Command("team").description("Manage team members and authorizations").addCommand(listCommand2).addCommand(addCommand).addCommand(editCommand2).addCommand(removeCommand2);
|
|
3572
|
+
function hasVersion(data) {
|
|
3573
|
+
return typeof data === "object" && data !== null && "version" in data && // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
3574
|
+
typeof data.version === "string";
|
|
3575
|
+
}
|
|
3576
|
+
var cachedVersion;
|
|
3577
|
+
function getPackageVersion() {
|
|
3578
|
+
if (cachedVersion !== void 0) {
|
|
3579
|
+
return cachedVersion;
|
|
3580
|
+
}
|
|
3581
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
3582
|
+
const __dirname = dirname(__filename);
|
|
3583
|
+
const possiblePaths = [join(__dirname, "../package.json"), join(__dirname, "../../package.json")];
|
|
3584
|
+
for (const packageJsonPath of possiblePaths) {
|
|
3585
|
+
try {
|
|
3586
|
+
const content = readFileSync(packageJsonPath, "utf-8");
|
|
3587
|
+
const packageJsonData = JSON.parse(content);
|
|
3588
|
+
if (!hasVersion(packageJsonData)) {
|
|
3589
|
+
throw new Error(`Invalid package.json at ${packageJsonPath}: missing version field`);
|
|
3590
|
+
}
|
|
3591
|
+
cachedVersion = packageJsonData.version;
|
|
3592
|
+
return cachedVersion;
|
|
3593
|
+
} catch (error2) {
|
|
3594
|
+
if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") {
|
|
3595
|
+
continue;
|
|
3596
|
+
}
|
|
3597
|
+
throw error2;
|
|
3598
|
+
}
|
|
3599
|
+
}
|
|
3600
|
+
throw new Error("Could not find package.json");
|
|
3601
|
+
}
|
|
1609
3602
|
var program = new Command();
|
|
1610
|
-
program.name("attest-it").description("Human-gated test attestation system").
|
|
3603
|
+
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");
|
|
3604
|
+
program.option("-V, --version", "output the version number");
|
|
1611
3605
|
program.addCommand(initCommand);
|
|
1612
3606
|
program.addCommand(statusCommand);
|
|
1613
3607
|
program.addCommand(runCommand);
|
|
1614
3608
|
program.addCommand(keygenCommand);
|
|
1615
3609
|
program.addCommand(pruneCommand);
|
|
1616
3610
|
program.addCommand(verifyCommand);
|
|
3611
|
+
program.addCommand(sealCommand);
|
|
3612
|
+
program.addCommand(identityCommand);
|
|
3613
|
+
program.addCommand(teamCommand);
|
|
3614
|
+
program.addCommand(whoamiCommand);
|
|
1617
3615
|
async function run() {
|
|
3616
|
+
if (process.argv.includes("--version") || process.argv.includes("-V")) {
|
|
3617
|
+
console.log(getPackageVersion());
|
|
3618
|
+
process.exit(0);
|
|
3619
|
+
}
|
|
1618
3620
|
await initTheme();
|
|
1619
3621
|
program.parse();
|
|
1620
3622
|
const options = program.opts();
|