@attest-it/core 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,14 +2,14 @@ import { getDefaultPrivateKeyPath, generateKeyPair, setKeyPermissions } from './
2
2
  export { checkOpenSSL, generateKeyPair, getDefaultPrivateKeyPath, getDefaultPublicKeyPath, setKeyPermissions, sign, verify } from './chunk-VC3BBBBO.js';
3
3
  import * as fs from 'fs';
4
4
  import { readFileSync, mkdirSync, writeFileSync } from 'fs';
5
- import * as fs6 from 'fs/promises';
5
+ import * as fs7 from 'fs/promises';
6
6
  import { readFile, mkdir, writeFile } from 'fs/promises';
7
7
  import * as path6 from 'path';
8
8
  import { join, resolve, dirname } from 'path';
9
9
  import ms from 'ms';
10
- import { stringify, parse } from 'yaml';
10
+ import { parse, stringify } from 'yaml';
11
11
  import { z } from 'zod';
12
- import * as crypto2 from 'crypto';
12
+ import * as crypto3 from 'crypto';
13
13
  import { glob, globSync } from 'tinyglobby';
14
14
  import * as os2 from 'os';
15
15
  import { homedir } from 'os';
@@ -255,6 +255,299 @@ function toAttestItConfig(config) {
255
255
  );
256
256
  return result;
257
257
  }
258
+ var teamMemberSchema2 = z.object({
259
+ name: z.string().min(1, "Team member name cannot be empty"),
260
+ email: z.string().email().optional(),
261
+ github: z.string().min(1).optional(),
262
+ publicKey: z.string().min(1, "Public key is required")
263
+ }).strict();
264
+ var fingerprintConfigSchema2 = z.object({
265
+ paths: z.array(z.string().min(1, "Path cannot be empty")).min(1, "At least one path is required"),
266
+ exclude: z.array(z.string().min(1, "Exclude pattern cannot be empty")).optional()
267
+ }).strict();
268
+ var durationSchema2 = z.string().refine(
269
+ (val) => {
270
+ try {
271
+ const parsed = ms(val);
272
+ return typeof parsed === "number" && parsed > 0;
273
+ } catch {
274
+ return false;
275
+ }
276
+ },
277
+ {
278
+ message: 'Duration must be a valid duration string (e.g., "30d", "7d", "24h")'
279
+ }
280
+ );
281
+ var gateSchema2 = z.object({
282
+ name: z.string().min(1, "Gate name cannot be empty"),
283
+ description: z.string().min(1, "Gate description cannot be empty"),
284
+ authorizedSigners: z.array(z.string().min(1, "Authorized signer slug cannot be empty")).min(1, "At least one authorized signer is required"),
285
+ fingerprint: fingerprintConfigSchema2,
286
+ maxAge: durationSchema2
287
+ }).strict();
288
+ var keyProviderOptionsSchema2 = z.object({
289
+ privateKeyPath: z.string().optional(),
290
+ account: z.string().optional(),
291
+ vault: z.string().optional(),
292
+ itemName: z.string().optional()
293
+ }).passthrough();
294
+ var keyProviderSchema2 = z.object({
295
+ type: z.enum(["filesystem", "1password"]).or(z.string()),
296
+ options: keyProviderOptionsSchema2.optional()
297
+ }).strict();
298
+
299
+ // src/config/policy-schema.ts
300
+ var policySettingsSchema = z.object({
301
+ maxAgeDays: z.number().int().positive().default(30),
302
+ publicKeyPath: z.string().default(".attest-it/pubkey.pem"),
303
+ attestationsPath: z.string().default(".attest-it/attestations.json")
304
+ }).strict();
305
+ var policySchema = z.object({
306
+ version: z.literal(1),
307
+ settings: policySettingsSchema.default({}),
308
+ team: z.record(z.string(), teamMemberSchema2).optional(),
309
+ gates: z.record(z.string(), gateSchema2).optional()
310
+ }).strict();
311
+ var PolicyValidationError = class extends Error {
312
+ constructor(message, issues) {
313
+ super(message);
314
+ this.issues = issues;
315
+ this.name = "PolicyValidationError";
316
+ }
317
+ };
318
+ function parsePolicyContent(content, format) {
319
+ let rawConfig;
320
+ try {
321
+ if (format === "yaml") {
322
+ rawConfig = parse(content);
323
+ } else {
324
+ rawConfig = JSON.parse(content);
325
+ }
326
+ } catch (error) {
327
+ throw new PolicyValidationError(
328
+ `Failed to parse ${format.toUpperCase()}: ${error instanceof Error ? error.message : String(error)}`,
329
+ []
330
+ );
331
+ }
332
+ const result = policySchema.safeParse(rawConfig);
333
+ if (!result.success) {
334
+ throw new PolicyValidationError(
335
+ "Policy validation failed:\n" + result.error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n"),
336
+ result.error.issues
337
+ );
338
+ }
339
+ return result.data;
340
+ }
341
+ var operationalSettingsSchema = z.object({
342
+ defaultCommand: z.string().optional(),
343
+ keyProvider: keyProviderSchema2.optional()
344
+ }).strict();
345
+ var suiteSchema2 = z.object({
346
+ // Gate fields (if present, this suite references a gate)
347
+ gate: z.string().optional(),
348
+ // Legacy fingerprint definition (for backward compatibility)
349
+ description: z.string().optional(),
350
+ packages: z.array(z.string().min(1, "Package path cannot be empty")).optional(),
351
+ files: z.array(z.string().min(1, "File path cannot be empty")).optional(),
352
+ ignore: z.array(z.string().min(1, "Ignore pattern cannot be empty")).optional(),
353
+ // CLI-specific fields
354
+ command: z.string().optional(),
355
+ timeout: z.string().optional(),
356
+ interactive: z.boolean().optional(),
357
+ // Relationship fields
358
+ invalidates: z.array(z.string().min(1, "Invalidated suite name cannot be empty")).optional(),
359
+ depends_on: z.array(z.string().min(1, "Dependency suite name cannot be empty")).optional()
360
+ }).strict().refine(
361
+ (suite) => {
362
+ return suite.gate !== void 0 || suite.packages !== void 0 && suite.packages.length > 0;
363
+ },
364
+ {
365
+ message: "Suite must either reference a gate or define packages for fingerprinting"
366
+ }
367
+ );
368
+ var operationalSchema = z.object({
369
+ version: z.literal(1),
370
+ settings: operationalSettingsSchema.default({}),
371
+ suites: z.record(z.string(), suiteSchema2).refine((suites) => Object.keys(suites).length >= 1, {
372
+ message: "At least one suite must be defined"
373
+ }),
374
+ groups: z.record(z.string(), z.array(z.string().min(1, "Suite name in group cannot be empty"))).optional()
375
+ }).strict();
376
+ var OperationalValidationError = class extends Error {
377
+ constructor(message, issues) {
378
+ super(message);
379
+ this.issues = issues;
380
+ this.name = "OperationalValidationError";
381
+ }
382
+ };
383
+ function parseOperationalContent(content, format) {
384
+ let rawConfig;
385
+ try {
386
+ if (format === "yaml") {
387
+ rawConfig = parse(content);
388
+ } else {
389
+ rawConfig = JSON.parse(content);
390
+ }
391
+ } catch (error) {
392
+ throw new OperationalValidationError(
393
+ `Failed to parse ${format.toUpperCase()}: ${error instanceof Error ? error.message : String(error)}`,
394
+ []
395
+ );
396
+ }
397
+ const result = operationalSchema.safeParse(rawConfig);
398
+ if (!result.success) {
399
+ throw new OperationalValidationError(
400
+ "Operational configuration validation failed:\n" + result.error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n"),
401
+ result.error.issues
402
+ );
403
+ }
404
+ return result.data;
405
+ }
406
+
407
+ // src/config/merge.ts
408
+ function toSuiteConfig(suite) {
409
+ const result = {};
410
+ if (suite.gate !== void 0) result.gate = suite.gate;
411
+ if (suite.description !== void 0) result.description = suite.description;
412
+ if (suite.packages !== void 0) result.packages = suite.packages;
413
+ if (suite.files !== void 0) result.files = suite.files;
414
+ if (suite.ignore !== void 0) result.ignore = suite.ignore;
415
+ if (suite.command !== void 0) result.command = suite.command;
416
+ if (suite.timeout !== void 0) result.timeout = suite.timeout;
417
+ if (suite.interactive !== void 0) result.interactive = suite.interactive;
418
+ if (suite.invalidates !== void 0) result.invalidates = suite.invalidates;
419
+ if (suite.depends_on !== void 0) result.depends_on = suite.depends_on;
420
+ return result;
421
+ }
422
+ function toTeamMember(member) {
423
+ const result = {
424
+ name: member.name,
425
+ publicKey: member.publicKey
426
+ };
427
+ if (member.email !== void 0) result.email = member.email;
428
+ if (member.github !== void 0) result.github = member.github;
429
+ return result;
430
+ }
431
+ function toGateConfig(gate) {
432
+ const fingerprint = {
433
+ paths: gate.fingerprint.paths
434
+ };
435
+ if (gate.fingerprint.exclude !== void 0) {
436
+ fingerprint.exclude = gate.fingerprint.exclude;
437
+ }
438
+ return {
439
+ name: gate.name,
440
+ description: gate.description,
441
+ authorizedSigners: gate.authorizedSigners,
442
+ fingerprint,
443
+ maxAge: gate.maxAge
444
+ };
445
+ }
446
+ function toKeyProvider(provider) {
447
+ const result = {
448
+ type: provider.type
449
+ };
450
+ if (provider.options !== void 0) {
451
+ const options = {};
452
+ let hasOptions = false;
453
+ if (provider.options.privateKeyPath !== void 0) {
454
+ options.privateKeyPath = provider.options.privateKeyPath;
455
+ hasOptions = true;
456
+ }
457
+ if (provider.options.account !== void 0) {
458
+ options.account = provider.options.account;
459
+ hasOptions = true;
460
+ }
461
+ if (provider.options.vault !== void 0) {
462
+ options.vault = provider.options.vault;
463
+ hasOptions = true;
464
+ }
465
+ if (provider.options.itemName !== void 0) {
466
+ options.itemName = provider.options.itemName;
467
+ hasOptions = true;
468
+ }
469
+ if (hasOptions) {
470
+ result.options = options;
471
+ }
472
+ }
473
+ return result;
474
+ }
475
+ function mergeConfigs(policy, operational) {
476
+ const settings = {
477
+ // Security settings from policy (these are trust-critical)
478
+ maxAgeDays: policy.settings.maxAgeDays,
479
+ publicKeyPath: policy.settings.publicKeyPath,
480
+ attestationsPath: policy.settings.attestationsPath
481
+ };
482
+ if (operational.settings.defaultCommand !== void 0) {
483
+ settings.defaultCommand = operational.settings.defaultCommand;
484
+ }
485
+ if (operational.settings.keyProvider !== void 0) {
486
+ settings.keyProvider = toKeyProvider(operational.settings.keyProvider);
487
+ }
488
+ const suites = {};
489
+ for (const [name, suite] of Object.entries(operational.suites)) {
490
+ suites[name] = toSuiteConfig(suite);
491
+ }
492
+ const config = {
493
+ version: 1,
494
+ settings,
495
+ suites
496
+ };
497
+ if (policy.team !== void 0) {
498
+ const team = {};
499
+ for (const [slug, member] of Object.entries(policy.team)) {
500
+ team[slug] = toTeamMember(member);
501
+ }
502
+ config.team = team;
503
+ }
504
+ if (policy.gates !== void 0) {
505
+ const gates = {};
506
+ for (const [slug, gate] of Object.entries(policy.gates)) {
507
+ gates[slug] = toGateConfig(gate);
508
+ }
509
+ config.gates = gates;
510
+ }
511
+ if (operational.groups !== void 0) {
512
+ config.groups = operational.groups;
513
+ }
514
+ return config;
515
+ }
516
+
517
+ // src/config/validation.ts
518
+ function validateSuiteGateReferences(policy, operational) {
519
+ const errors = [];
520
+ const gates = policy.gates ?? {};
521
+ const team = policy.team ?? {};
522
+ for (const [suiteName, suiteConfig] of Object.entries(operational.suites)) {
523
+ const gateName = suiteConfig.gate;
524
+ if (gateName === void 0) {
525
+ continue;
526
+ }
527
+ const gate = gates[gateName];
528
+ if (gate === void 0) {
529
+ errors.push({
530
+ type: "UNKNOWN_GATE",
531
+ suite: suiteName,
532
+ gate: gateName,
533
+ message: `Suite "${suiteName}" references unknown gate "${gateName}". The gate must be defined in policy.yaml.`
534
+ });
535
+ continue;
536
+ }
537
+ for (const signerSlug of gate.authorizedSigners) {
538
+ if (team[signerSlug] === void 0) {
539
+ errors.push({
540
+ type: "MISSING_TEAM_MEMBER",
541
+ suite: suiteName,
542
+ gate: gateName,
543
+ signer: signerSlug,
544
+ message: `Gate "${gateName}" (referenced by suite "${suiteName}") authorizes signer "${signerSlug}", but this team member is not defined in policy.yaml.`
545
+ });
546
+ }
547
+ }
548
+ }
549
+ return errors;
550
+ }
258
551
  var LARGE_FILE_THRESHOLD = 50 * 1024 * 1024;
259
552
  function sortFiles(files) {
260
553
  return [...files].sort((a, b) => {
@@ -274,13 +567,13 @@ function computeFinalFingerprint(fileHashes) {
274
567
  });
275
568
  const hashes = sorted.map((input) => input.hash);
276
569
  const concatenated = Buffer.concat(hashes);
277
- const finalHash = crypto2.createHash("sha256").update(concatenated).digest();
570
+ const finalHash = crypto3.createHash("sha256").update(concatenated).digest();
278
571
  return `sha256:${finalHash.toString("hex")}`;
279
572
  }
280
573
  async function hashFileAsync(realPath, normalizedPath, stats) {
281
574
  if (stats.size > LARGE_FILE_THRESHOLD) {
282
- return new Promise((resolve3, reject) => {
283
- const hash2 = crypto2.createHash("sha256");
575
+ return new Promise((resolve4, reject) => {
576
+ const hash2 = crypto3.createHash("sha256");
284
577
  hash2.update(normalizedPath);
285
578
  hash2.update(":");
286
579
  const stream = fs.createReadStream(realPath);
@@ -288,13 +581,13 @@ async function hashFileAsync(realPath, normalizedPath, stats) {
288
581
  hash2.update(chunk);
289
582
  });
290
583
  stream.on("end", () => {
291
- resolve3(hash2.digest());
584
+ resolve4(hash2.digest());
292
585
  });
293
586
  stream.on("error", reject);
294
587
  });
295
588
  }
296
589
  const content = await fs.promises.readFile(realPath);
297
- const hash = crypto2.createHash("sha256");
590
+ const hash = crypto3.createHash("sha256");
298
591
  hash.update(normalizedPath);
299
592
  hash.update(":");
300
593
  hash.update(content);
@@ -302,7 +595,7 @@ async function hashFileAsync(realPath, normalizedPath, stats) {
302
595
  }
303
596
  function hashFileSync(realPath, normalizedPath) {
304
597
  const content = fs.readFileSync(realPath);
305
- const hash = crypto2.createHash("sha256");
598
+ const hash = crypto3.createHash("sha256");
306
599
  hash.update(normalizedPath);
307
600
  hash.update(":");
308
601
  hash.update(content);
@@ -607,7 +900,7 @@ function isBuffer(value) {
607
900
  }
608
901
  function generateKeyPair2() {
609
902
  try {
610
- const keyPair = crypto2.generateKeyPairSync("ed25519", {
903
+ const keyPair = crypto3.generateKeyPairSync("ed25519", {
611
904
  publicKeyEncoding: {
612
905
  type: "spki",
613
906
  format: "pem"
@@ -621,7 +914,7 @@ function generateKeyPair2() {
621
914
  if (typeof publicKey !== "string" || typeof privateKey !== "string") {
622
915
  throw new Error("Expected keypair to have string keys");
623
916
  }
624
- const publicKeyObj = crypto2.createPublicKey(publicKey);
917
+ const publicKeyObj = crypto3.createPublicKey(publicKey);
625
918
  const publicKeyExport = publicKeyObj.export({
626
919
  type: "spki",
627
920
  format: "der"
@@ -644,8 +937,8 @@ function generateKeyPair2() {
644
937
  function sign3(data, privateKeyPem) {
645
938
  try {
646
939
  const dataBuffer = typeof data === "string" ? Buffer.from(data, "utf8") : data;
647
- const privateKeyObj = crypto2.createPrivateKey(privateKeyPem);
648
- const signatureResult = crypto2.sign(null, dataBuffer, privateKeyObj);
940
+ const privateKeyObj = crypto3.createPrivateKey(privateKeyPem);
941
+ const signatureResult = crypto3.sign(null, dataBuffer, privateKeyObj);
649
942
  if (!isBuffer(signatureResult)) {
650
943
  throw new Error("Expected signature to be a Buffer");
651
944
  }
@@ -685,12 +978,12 @@ function verify3(data, signature, publicKeyBase64) {
685
978
  // BIT STRING, 33 bytes (32 key + 1 padding)
686
979
  ]);
687
980
  const spkiBuffer = Buffer.concat([spkiHeader, rawPublicKey]);
688
- const publicKeyObj = crypto2.createPublicKey({
981
+ const publicKeyObj = crypto3.createPublicKey({
689
982
  key: spkiBuffer,
690
983
  format: "der",
691
984
  type: "spki"
692
985
  });
693
- return crypto2.verify(null, dataBuffer, publicKeyObj, signatureBuffer);
986
+ return crypto3.verify(null, dataBuffer, publicKeyObj, signatureBuffer);
694
987
  } catch (err) {
695
988
  if (err instanceof Error && err.message.includes("verification failed")) {
696
989
  return false;
@@ -702,8 +995,8 @@ function verify3(data, signature, publicKeyBase64) {
702
995
  }
703
996
  function getPublicKeyFromPrivate(privateKeyPem) {
704
997
  try {
705
- const privateKeyObj = crypto2.createPrivateKey(privateKeyPem);
706
- const publicKeyObj = crypto2.createPublicKey(privateKeyObj);
998
+ const privateKeyObj = crypto3.createPrivateKey(privateKeyPem);
999
+ const publicKeyObj = crypto3.createPublicKey(privateKeyObj);
707
1000
  const publicKeyExport = publicKeyObj.export({
708
1001
  type: "spki",
709
1002
  format: "der"
@@ -869,7 +1162,7 @@ var FilesystemKeyProvider = class {
869
1162
  */
870
1163
  async keyExists(keyRef) {
871
1164
  try {
872
- await fs6.access(keyRef);
1165
+ await fs7.access(keyRef);
873
1166
  return true;
874
1167
  } catch {
875
1168
  return false;
@@ -1027,7 +1320,7 @@ var OnePasswordKeyProvider = class _OnePasswordKeyProvider {
1027
1320
  `Key not found in 1Password: "${keyRef}" (vault: ${this.vault})` + (this.account ? ` (account: ${this.account})` : "")
1028
1321
  );
1029
1322
  }
1030
- const tempDir = await fs6.mkdtemp(path6.join(os2.tmpdir(), "attest-it-"));
1323
+ const tempDir = await fs7.mkdtemp(path6.join(os2.tmpdir(), "attest-it-"));
1031
1324
  const tempKeyPath = path6.join(tempDir, "private.pem");
1032
1325
  try {
1033
1326
  const args = ["document", "get", keyRef, "--vault", this.vault, "--out-file", tempKeyPath];
@@ -1040,8 +1333,8 @@ var OnePasswordKeyProvider = class _OnePasswordKeyProvider {
1040
1333
  keyPath: tempKeyPath,
1041
1334
  cleanup: async () => {
1042
1335
  try {
1043
- await fs6.unlink(tempKeyPath);
1044
- await fs6.rmdir(tempDir);
1336
+ await fs7.unlink(tempKeyPath);
1337
+ await fs7.rmdir(tempDir);
1045
1338
  } catch (cleanupError) {
1046
1339
  console.warn(
1047
1340
  `Warning: Failed to clean up temporary key file at ${tempKeyPath}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
@@ -1051,7 +1344,7 @@ var OnePasswordKeyProvider = class _OnePasswordKeyProvider {
1051
1344
  };
1052
1345
  } catch (error) {
1053
1346
  try {
1054
- await fs6.rm(tempDir, { recursive: true, force: true });
1347
+ await fs7.rm(tempDir, { recursive: true, force: true });
1055
1348
  } catch (cleanupError) {
1056
1349
  console.warn(
1057
1350
  `Warning: Failed to clean up temporary key directory at ${tempDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
@@ -1067,7 +1360,7 @@ var OnePasswordKeyProvider = class _OnePasswordKeyProvider {
1067
1360
  */
1068
1361
  async generateKeyPair(options) {
1069
1362
  const { publicKeyPath, force = false } = options;
1070
- const tempDir = await fs6.mkdtemp(path6.join(os2.tmpdir(), "attest-it-keygen-"));
1363
+ const tempDir = await fs7.mkdtemp(path6.join(os2.tmpdir(), "attest-it-keygen-"));
1071
1364
  const tempPrivateKeyPath = path6.join(tempDir, "private.pem");
1072
1365
  try {
1073
1366
  await generateKeyPair({
@@ -1088,8 +1381,8 @@ var OnePasswordKeyProvider = class _OnePasswordKeyProvider {
1088
1381
  args.push("--account", this.account);
1089
1382
  }
1090
1383
  await execCommand("op", args);
1091
- await fs6.unlink(tempPrivateKeyPath);
1092
- await fs6.rmdir(tempDir);
1384
+ await fs7.unlink(tempPrivateKeyPath);
1385
+ await fs7.rmdir(tempDir);
1093
1386
  return {
1094
1387
  privateKeyRef: this.itemName,
1095
1388
  publicKeyPath,
@@ -1097,7 +1390,7 @@ var OnePasswordKeyProvider = class _OnePasswordKeyProvider {
1097
1390
  };
1098
1391
  } catch (error) {
1099
1392
  try {
1100
- await fs6.rm(tempDir, { recursive: true, force: true });
1393
+ await fs7.rm(tempDir, { recursive: true, force: true });
1101
1394
  } catch (cleanupError) {
1102
1395
  console.warn(
1103
1396
  `Warning: Failed to clean up temporary key directory at ${tempDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
@@ -1121,7 +1414,7 @@ var OnePasswordKeyProvider = class _OnePasswordKeyProvider {
1121
1414
  }
1122
1415
  };
1123
1416
  async function execCommand(command, args) {
1124
- return new Promise((resolve3, reject) => {
1417
+ return new Promise((resolve4, reject) => {
1125
1418
  const proc = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
1126
1419
  let stdout = "";
1127
1420
  let stderr = "";
@@ -1133,7 +1426,7 @@ async function execCommand(command, args) {
1133
1426
  });
1134
1427
  proc.on("close", (code) => {
1135
1428
  if (code === 0) {
1136
- resolve3(stdout.trim());
1429
+ resolve4(stdout.trim());
1137
1430
  } else {
1138
1431
  reject(new Error(`Command failed with exit code ${String(code)}: ${stderr}`));
1139
1432
  }
@@ -1226,7 +1519,7 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
1226
1519
  `Key not found in macOS Keychain: "${keyRef}" (account: ${_MacOSKeychainKeyProvider.ACCOUNT})`
1227
1520
  );
1228
1521
  }
1229
- const tempDir = await fs6.mkdtemp(path6.join(os2.tmpdir(), "attest-it-"));
1522
+ const tempDir = await fs7.mkdtemp(path6.join(os2.tmpdir(), "attest-it-"));
1230
1523
  const tempKeyPath = path6.join(tempDir, "private.pem");
1231
1524
  try {
1232
1525
  const findArgs = [
@@ -1242,14 +1535,14 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
1242
1535
  }
1243
1536
  const base64Key = await execCommand2("security", findArgs);
1244
1537
  const keyContent = Buffer.from(base64Key, "base64").toString("utf8");
1245
- await fs6.writeFile(tempKeyPath, keyContent, { mode: 384 });
1538
+ await fs7.writeFile(tempKeyPath, keyContent, { mode: 384 });
1246
1539
  await setKeyPermissions(tempKeyPath);
1247
1540
  return {
1248
1541
  keyPath: tempKeyPath,
1249
1542
  cleanup: async () => {
1250
1543
  try {
1251
- await fs6.unlink(tempKeyPath);
1252
- await fs6.rmdir(tempDir);
1544
+ await fs7.unlink(tempKeyPath);
1545
+ await fs7.rmdir(tempDir);
1253
1546
  } catch (cleanupError) {
1254
1547
  console.warn(
1255
1548
  `Warning: Failed to clean up temporary key file at ${tempKeyPath}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
@@ -1259,7 +1552,7 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
1259
1552
  };
1260
1553
  } catch (error) {
1261
1554
  try {
1262
- await fs6.rm(tempDir, { recursive: true, force: true });
1555
+ await fs7.rm(tempDir, { recursive: true, force: true });
1263
1556
  } catch (cleanupError) {
1264
1557
  console.warn(
1265
1558
  `Warning: Failed to clean up temporary key directory at ${tempDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
@@ -1275,7 +1568,7 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
1275
1568
  */
1276
1569
  async generateKeyPair(options) {
1277
1570
  const { publicKeyPath, force = false } = options;
1278
- const tempDir = await fs6.mkdtemp(path6.join(os2.tmpdir(), "attest-it-keygen-"));
1571
+ const tempDir = await fs7.mkdtemp(path6.join(os2.tmpdir(), "attest-it-keygen-"));
1279
1572
  const tempPrivateKeyPath = path6.join(tempDir, "private.pem");
1280
1573
  try {
1281
1574
  await generateKeyPair({
@@ -1283,7 +1576,7 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
1283
1576
  publicPath: publicKeyPath,
1284
1577
  force
1285
1578
  });
1286
- const privateKeyContent = await fs6.readFile(tempPrivateKeyPath, "utf8");
1579
+ const privateKeyContent = await fs7.readFile(tempPrivateKeyPath, "utf8");
1287
1580
  const base64Key = Buffer.from(privateKeyContent, "utf8").toString("base64");
1288
1581
  const addArgs = [
1289
1582
  "add-generic-password",
@@ -1301,8 +1594,8 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
1301
1594
  addArgs.push(this.keychain);
1302
1595
  }
1303
1596
  await execCommand2("security", addArgs);
1304
- await fs6.unlink(tempPrivateKeyPath);
1305
- await fs6.rmdir(tempDir);
1597
+ await fs7.unlink(tempPrivateKeyPath);
1598
+ await fs7.rmdir(tempDir);
1306
1599
  return {
1307
1600
  privateKeyRef: this.itemName,
1308
1601
  publicKeyPath,
@@ -1310,7 +1603,7 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
1310
1603
  };
1311
1604
  } catch (error) {
1312
1605
  try {
1313
- await fs6.rm(tempDir, { recursive: true, force: true });
1606
+ await fs7.rm(tempDir, { recursive: true, force: true });
1314
1607
  } catch (cleanupError) {
1315
1608
  console.warn(
1316
1609
  `Warning: Failed to clean up temporary key directory at ${tempDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
@@ -1332,7 +1625,7 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
1332
1625
  }
1333
1626
  };
1334
1627
  async function execCommand2(command, args) {
1335
- return new Promise((resolve3, reject) => {
1628
+ return new Promise((resolve4, reject) => {
1336
1629
  const proc = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
1337
1630
  let stdout = "";
1338
1631
  let stderr = "";
@@ -1344,7 +1637,7 @@ async function execCommand2(command, args) {
1344
1637
  });
1345
1638
  proc.on("close", (code) => {
1346
1639
  if (code === 0) {
1347
- resolve3(stdout.trim());
1640
+ resolve4(stdout.trim());
1348
1641
  } else {
1349
1642
  reject(new Error(`Command failed with exit code ${String(code)}: ${stderr}`));
1350
1643
  }
@@ -1354,69 +1647,6 @@ async function execCommand2(command, args) {
1354
1647
  });
1355
1648
  });
1356
1649
  }
1357
-
1358
- // src/key-provider/registry.ts
1359
- var KeyProviderRegistry = class {
1360
- static providers = /* @__PURE__ */ new Map();
1361
- /**
1362
- * Register a key provider factory.
1363
- * @param type - Provider type identifier
1364
- * @param factory - Factory function to create provider instances
1365
- */
1366
- static register(type, factory) {
1367
- this.providers.set(type, factory);
1368
- }
1369
- /**
1370
- * Create a key provider from configuration.
1371
- * @param config - Provider configuration
1372
- * @returns A key provider instance
1373
- * @throws Error if the provider type is not registered
1374
- */
1375
- static create(config) {
1376
- const factory = this.providers.get(config.type);
1377
- if (!factory) {
1378
- throw new Error(
1379
- `Unknown key provider type: ${config.type}. Available types: ${Array.from(this.providers.keys()).join(", ")}`
1380
- );
1381
- }
1382
- return factory(config);
1383
- }
1384
- /**
1385
- * Get all registered provider types.
1386
- * @returns Array of provider type identifiers
1387
- */
1388
- static getProviderTypes() {
1389
- return Array.from(this.providers.keys());
1390
- }
1391
- };
1392
- KeyProviderRegistry.register("filesystem", (config) => {
1393
- const privateKeyPath = typeof config.options.privateKeyPath === "string" ? config.options.privateKeyPath : void 0;
1394
- if (privateKeyPath !== void 0) {
1395
- return new FilesystemKeyProvider({ privateKeyPath });
1396
- }
1397
- return new FilesystemKeyProvider();
1398
- });
1399
- KeyProviderRegistry.register("1password", (config) => {
1400
- const { options } = config;
1401
- const account = typeof options.account === "string" ? options.account : void 0;
1402
- const vault = typeof options.vault === "string" ? options.vault : "";
1403
- const itemName = typeof options.itemName === "string" ? options.itemName : "";
1404
- if (!vault || !itemName) {
1405
- throw new Error("1Password provider requires vault and itemName options");
1406
- }
1407
- if (account !== void 0) {
1408
- return new OnePasswordKeyProvider({ account, vault, itemName });
1409
- }
1410
- return new OnePasswordKeyProvider({ vault, itemName });
1411
- });
1412
- KeyProviderRegistry.register("macos-keychain", (config) => {
1413
- const { options } = config;
1414
- const itemName = typeof options.itemName === "string" ? options.itemName : "";
1415
- if (!itemName) {
1416
- throw new Error("macOS Keychain provider requires itemName option");
1417
- }
1418
- return new MacOSKeychainKeyProvider({ itemName });
1419
- });
1420
1650
  var homeDirOverride = null;
1421
1651
  function setAttestItHomeDir(dir) {
1422
1652
  homeDirOverride = dir;
@@ -1582,6 +1812,617 @@ function saveLocalConfigSync(config, configPath) {
1582
1812
  function getActiveIdentity(config) {
1583
1813
  return config.identities[config.activeIdentity];
1584
1814
  }
1815
+
1816
+ // src/key-provider/yubikey-provider.ts
1817
+ var EncryptedKeyFileSchema = z.object({
1818
+ version: z.literal(1),
1819
+ iv: z.string().min(1),
1820
+ authTag: z.string().min(1),
1821
+ salt: z.string().min(1),
1822
+ challenge: z.string().min(1),
1823
+ ciphertext: z.string().min(1),
1824
+ slot: z.union([z.literal(1), z.literal(2)]),
1825
+ serial: z.string().optional(),
1826
+ aad: z.string().optional()
1827
+ });
1828
+ var activeCleanupHandlers = /* @__PURE__ */ new Set();
1829
+ var processHandlersInstalled = false;
1830
+ function installProcessHandlers() {
1831
+ if (processHandlersInstalled) return;
1832
+ processHandlersInstalled = true;
1833
+ const runCleanup = async () => {
1834
+ const handlers = Array.from(activeCleanupHandlers);
1835
+ await Promise.allSettled(handlers.map((h) => h()));
1836
+ };
1837
+ process.once("beforeExit", () => {
1838
+ void runCleanup();
1839
+ });
1840
+ process.once("SIGINT", () => {
1841
+ void runCleanup().finally(() => process.exit(130));
1842
+ });
1843
+ process.once("SIGTERM", () => {
1844
+ void runCleanup().finally(() => process.exit(143));
1845
+ });
1846
+ }
1847
+ function validateEncryptedKeyFile(data) {
1848
+ const parsed = EncryptedKeyFileSchema.parse(data);
1849
+ const iv = Buffer.from(parsed.iv, "base64");
1850
+ if (iv.length !== 12) {
1851
+ throw new Error(`Invalid IV size: expected 12 bytes, got ${String(iv.length)}`);
1852
+ }
1853
+ const authTag = Buffer.from(parsed.authTag, "base64");
1854
+ if (authTag.length !== 16) {
1855
+ throw new Error(`Invalid auth tag size: expected 16 bytes, got ${String(authTag.length)}`);
1856
+ }
1857
+ const salt = Buffer.from(parsed.salt, "base64");
1858
+ if (salt.length !== 32) {
1859
+ throw new Error(`Invalid salt size: expected 32 bytes, got ${String(salt.length)}`);
1860
+ }
1861
+ const challenge = Buffer.from(parsed.challenge, "base64");
1862
+ if (challenge.length !== 32) {
1863
+ throw new Error(`Invalid challenge size: expected 32 bytes, got ${String(challenge.length)}`);
1864
+ }
1865
+ return parsed;
1866
+ }
1867
+ function constructAAD(version2, slot, serial) {
1868
+ const aadObject = {
1869
+ version: version2,
1870
+ slot,
1871
+ serial: serial ?? "unspecified"
1872
+ };
1873
+ return Buffer.from(JSON.stringify(aadObject), "utf8");
1874
+ }
1875
+ var YubiKeyProvider = class _YubiKeyProvider {
1876
+ type = "yubikey";
1877
+ displayName = "YubiKey";
1878
+ encryptedKeyPath;
1879
+ slot;
1880
+ serial;
1881
+ /**
1882
+ * Create a new YubiKeyProvider.
1883
+ * @param options - Provider options
1884
+ * @throws Error if encryptedKeyPath is outside the attest-it config directory
1885
+ */
1886
+ constructor(options) {
1887
+ const resolvedPath = path6.resolve(options.encryptedKeyPath);
1888
+ const configDir = getAttestItConfigDir();
1889
+ if (!resolvedPath.startsWith(configDir)) {
1890
+ throw new Error(
1891
+ `Encrypted key path must be within attest-it config directory (${configDir}). Got: ${resolvedPath}`
1892
+ );
1893
+ }
1894
+ this.encryptedKeyPath = resolvedPath;
1895
+ this.slot = options.slot ?? 2;
1896
+ if (options.serial !== void 0) {
1897
+ this.serial = options.serial;
1898
+ }
1899
+ }
1900
+ /**
1901
+ * Check if ykman CLI is installed and available.
1902
+ * @returns true if ykman is available
1903
+ */
1904
+ static async isInstalled() {
1905
+ try {
1906
+ await execCommand3("ykman", ["--version"]);
1907
+ return true;
1908
+ } catch {
1909
+ return false;
1910
+ }
1911
+ }
1912
+ /**
1913
+ * Check if any YubiKey is connected.
1914
+ * @returns true if at least one YubiKey is connected
1915
+ */
1916
+ static async isConnected() {
1917
+ try {
1918
+ const output = await execCommand3("ykman", ["list", "--serials"]);
1919
+ return output.trim().length > 0;
1920
+ } catch {
1921
+ return false;
1922
+ }
1923
+ }
1924
+ /**
1925
+ * Check if HMAC challenge-response is configured on a slot.
1926
+ * @param slot - Slot number (1 or 2)
1927
+ * @param serial - Optional YubiKey serial number
1928
+ * @returns true if challenge-response is configured
1929
+ */
1930
+ static async isChallengeResponseConfigured(slot = 2, serial) {
1931
+ try {
1932
+ const args = ["otp", "info"];
1933
+ if (serial) {
1934
+ args.unshift("--device", serial);
1935
+ }
1936
+ const output = await execCommand3("ykman", args);
1937
+ const slotPattern = new RegExp(`Slot ${String(slot)}:\\s+programmed.*challenge-response`, "i");
1938
+ return slotPattern.test(output);
1939
+ } catch {
1940
+ return false;
1941
+ }
1942
+ }
1943
+ /**
1944
+ * List connected YubiKeys.
1945
+ * @returns Array of YubiKey information
1946
+ */
1947
+ static async listDevices() {
1948
+ if (!await _YubiKeyProvider.isInstalled()) {
1949
+ return [];
1950
+ }
1951
+ try {
1952
+ const output = await execCommand3("ykman", ["list", "--serials"]);
1953
+ const serials = output.trim().split("\n").filter((s) => s.length > 0);
1954
+ const devices = [];
1955
+ for (const serial of serials) {
1956
+ try {
1957
+ const infoOutput = await execCommand3("ykman", ["--device", serial, "info"]);
1958
+ const typeMatch = /Device type:\s+(.+)/i.exec(infoOutput);
1959
+ const fwMatch = /Firmware version:\s+(.+)/i.exec(infoOutput);
1960
+ devices.push({
1961
+ serial,
1962
+ type: typeMatch?.[1]?.trim() ?? "YubiKey",
1963
+ firmware: fwMatch?.[1]?.trim() ?? "Unknown"
1964
+ });
1965
+ } catch {
1966
+ devices.push({
1967
+ serial,
1968
+ type: "YubiKey",
1969
+ firmware: "Unknown"
1970
+ });
1971
+ }
1972
+ }
1973
+ return devices;
1974
+ } catch {
1975
+ return [];
1976
+ }
1977
+ }
1978
+ /**
1979
+ * Check if this provider is available on the current system.
1980
+ * Requires ykman to be installed.
1981
+ */
1982
+ async isAvailable() {
1983
+ return _YubiKeyProvider.isInstalled();
1984
+ }
1985
+ /**
1986
+ * Check if an encrypted key file exists.
1987
+ * @param keyRef - Path to encrypted key file
1988
+ */
1989
+ async keyExists(keyRef) {
1990
+ try {
1991
+ await fs7.access(keyRef);
1992
+ return true;
1993
+ } catch {
1994
+ return false;
1995
+ }
1996
+ }
1997
+ /**
1998
+ * Get the private key by decrypting with YubiKey.
1999
+ * Downloads to a temporary file and returns a cleanup function.
2000
+ *
2001
+ * **Important**: Always call the cleanup function when done to securely delete
2002
+ * the temporary key file. The cleanup is also registered for process exit handlers.
2003
+ *
2004
+ * @param keyRef - Path to encrypted key file
2005
+ * @throws Error if the key cannot be decrypted
2006
+ */
2007
+ async getPrivateKey(keyRef) {
2008
+ installProcessHandlers();
2009
+ if (!await this.keyExists(keyRef)) {
2010
+ throw new Error(`Encrypted key file not found: ${keyRef}`);
2011
+ }
2012
+ const encryptedData = await fs7.readFile(keyRef, "utf8");
2013
+ let keyFile;
2014
+ try {
2015
+ const parsed = JSON.parse(encryptedData);
2016
+ keyFile = validateEncryptedKeyFile(parsed);
2017
+ } catch (err) {
2018
+ if (err instanceof z.ZodError) {
2019
+ throw new Error(
2020
+ `Invalid encrypted key file format: ${err.errors.map((e) => e.message).join(", ")}`
2021
+ );
2022
+ }
2023
+ throw new Error(`Invalid encrypted key file: malformed JSON or structure`);
2024
+ }
2025
+ const expectedSerial = this.serial ?? keyFile.serial;
2026
+ if (!expectedSerial) {
2027
+ console.warn(
2028
+ "WARNING: No YubiKey serial number specified for key verification. Any YubiKey with the correct HMAC secret could decrypt this key. For better security, re-encrypt the key with a serial number specified."
2029
+ );
2030
+ }
2031
+ if (expectedSerial) {
2032
+ const devices = await _YubiKeyProvider.listDevices();
2033
+ const matchingDevice = devices.find((d) => d.serial === expectedSerial);
2034
+ if (!matchingDevice) {
2035
+ throw new Error(
2036
+ `Required YubiKey not found. Expected serial: ${expectedSerial}. Connected devices: ${devices.map((d) => d.serial).join(", ") || "none"}`
2037
+ );
2038
+ }
2039
+ }
2040
+ const challenge = Buffer.from(keyFile.challenge, "base64");
2041
+ const response = await performChallengeResponse(challenge, keyFile.slot, expectedSerial);
2042
+ const salt = Buffer.from(keyFile.salt, "base64");
2043
+ const aesKey = deriveKey(response, salt);
2044
+ const iv = Buffer.from(keyFile.iv, "base64");
2045
+ const authTag = Buffer.from(keyFile.authTag, "base64");
2046
+ const ciphertext = Buffer.from(keyFile.ciphertext, "base64");
2047
+ let privateKeyContent;
2048
+ try {
2049
+ const decipher = crypto3.createDecipheriv("aes-256-gcm", aesKey, iv);
2050
+ if (keyFile.aad) {
2051
+ decipher.setAAD(Buffer.from(keyFile.aad, "base64"));
2052
+ }
2053
+ decipher.setAuthTag(authTag);
2054
+ const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
2055
+ privateKeyContent = decrypted.toString("utf8");
2056
+ } catch {
2057
+ throw new Error(
2058
+ "Failed to decrypt private key. Verify you are using the correct YubiKey and the encrypted key file has not been corrupted or tampered with."
2059
+ );
2060
+ }
2061
+ const tempDir = await fs7.mkdtemp(path6.join(os2.tmpdir(), "attest-it-"));
2062
+ const tempKeyPath = path6.join(tempDir, "private.pem");
2063
+ const cleanup = async () => {
2064
+ activeCleanupHandlers.delete(cleanup);
2065
+ try {
2066
+ const keySize = Buffer.byteLength(privateKeyContent);
2067
+ await fs7.writeFile(tempKeyPath, crypto3.randomBytes(keySize));
2068
+ await fs7.unlink(tempKeyPath);
2069
+ await fs7.rmdir(tempDir);
2070
+ } catch {
2071
+ }
2072
+ };
2073
+ try {
2074
+ await fs7.writeFile(tempKeyPath, privateKeyContent, { mode: 384 });
2075
+ await setKeyPermissions(tempKeyPath);
2076
+ activeCleanupHandlers.add(cleanup);
2077
+ return {
2078
+ keyPath: tempKeyPath,
2079
+ cleanup
2080
+ };
2081
+ } catch (error) {
2082
+ await cleanup();
2083
+ throw error;
2084
+ }
2085
+ }
2086
+ /**
2087
+ * Generate a new keypair and store encrypted with YubiKey.
2088
+ * Public key is written to filesystem for repository commit.
2089
+ *
2090
+ * **Security Note**: Always specify a serial number to bind the key to a specific YubiKey.
2091
+ *
2092
+ * @param options - Key generation options
2093
+ */
2094
+ async generateKeyPair(options) {
2095
+ const { publicKeyPath, force = false } = options;
2096
+ if (!await _YubiKeyProvider.isChallengeResponseConfigured(this.slot, this.serial)) {
2097
+ throw new Error(
2098
+ `YubiKey slot ${String(this.slot)} is not configured for HMAC challenge-response. Ensure your YubiKey is connected and use "ykman otp chalresp --generate 2" to configure it.`
2099
+ );
2100
+ }
2101
+ if (!force && await this.keyExists(this.encryptedKeyPath)) {
2102
+ throw new Error(
2103
+ `Encrypted key file already exists: ${this.encryptedKeyPath}. Use force: true to overwrite.`
2104
+ );
2105
+ }
2106
+ let serial;
2107
+ if (this.serial) {
2108
+ serial = this.serial;
2109
+ } else {
2110
+ const devices = await _YubiKeyProvider.listDevices();
2111
+ if (devices.length === 1 && devices[0]) {
2112
+ serial = devices[0].serial;
2113
+ } else if (devices.length > 1) {
2114
+ console.warn(
2115
+ "WARNING: Multiple YubiKeys detected but no serial specified. Key will not be bound to a specific device. For better security, specify a serial number."
2116
+ );
2117
+ }
2118
+ }
2119
+ const tempDir = await fs7.mkdtemp(path6.join(os2.tmpdir(), "attest-it-keygen-"));
2120
+ const tempPrivateKeyPath = path6.join(tempDir, "private.pem");
2121
+ try {
2122
+ await generateKeyPair({
2123
+ privatePath: tempPrivateKeyPath,
2124
+ publicPath: publicKeyPath,
2125
+ force
2126
+ });
2127
+ const privateKeyContent = await fs7.readFile(tempPrivateKeyPath, "utf8");
2128
+ const challenge = crypto3.randomBytes(32);
2129
+ const salt = crypto3.randomBytes(32);
2130
+ const iv = crypto3.randomBytes(12);
2131
+ const response = await performChallengeResponse(challenge, this.slot, this.serial);
2132
+ const aesKey = deriveKey(response, salt);
2133
+ const aad = constructAAD(1, this.slot, serial);
2134
+ const cipher = crypto3.createCipheriv("aes-256-gcm", aesKey, iv);
2135
+ cipher.setAAD(aad);
2136
+ const ciphertext = Buffer.concat([
2137
+ cipher.update(Buffer.from(privateKeyContent, "utf8")),
2138
+ cipher.final()
2139
+ ]);
2140
+ const authTag = cipher.getAuthTag();
2141
+ const keyFile = {
2142
+ version: 1,
2143
+ iv: iv.toString("base64"),
2144
+ authTag: authTag.toString("base64"),
2145
+ salt: salt.toString("base64"),
2146
+ challenge: challenge.toString("base64"),
2147
+ ciphertext: ciphertext.toString("base64"),
2148
+ slot: this.slot,
2149
+ aad: aad.toString("base64"),
2150
+ ...serial && { serial }
2151
+ };
2152
+ await fs7.mkdir(path6.dirname(this.encryptedKeyPath), { recursive: true });
2153
+ await fs7.writeFile(this.encryptedKeyPath, JSON.stringify(keyFile, null, 2), { mode: 384 });
2154
+ await setKeyPermissions(this.encryptedKeyPath);
2155
+ const keySize = Buffer.byteLength(privateKeyContent);
2156
+ await fs7.writeFile(tempPrivateKeyPath, crypto3.randomBytes(keySize));
2157
+ await fs7.unlink(tempPrivateKeyPath);
2158
+ await fs7.rmdir(tempDir);
2159
+ return {
2160
+ privateKeyRef: this.encryptedKeyPath,
2161
+ publicKeyPath,
2162
+ storageDescription: `YubiKey-encrypted: ${this.encryptedKeyPath}`
2163
+ };
2164
+ } catch (error) {
2165
+ try {
2166
+ await fs7.rm(tempDir, { recursive: true, force: true });
2167
+ } catch {
2168
+ }
2169
+ throw error;
2170
+ }
2171
+ }
2172
+ /**
2173
+ * Encrypt an existing private key with YubiKey challenge-response.
2174
+ *
2175
+ * @remarks
2176
+ * This static method allows encrypting a private key that was generated
2177
+ * elsewhere (e.g., by the CLI) without having to create a provider instance first.
2178
+ *
2179
+ * **Security Note**: Always specify a serial number to bind the key to a specific YubiKey.
2180
+ * The serial provides defense-in-depth by ensuring only the intended YubiKey can decrypt.
2181
+ *
2182
+ * @param options - Encryption options
2183
+ * @returns Path to the encrypted key file and storage description
2184
+ * @public
2185
+ */
2186
+ static async encryptPrivateKey(options) {
2187
+ const { privateKey, encryptedKeyPath, slot = 2, serial } = options;
2188
+ const resolvedPath = path6.resolve(encryptedKeyPath);
2189
+ const configDir = getAttestItConfigDir();
2190
+ if (!resolvedPath.startsWith(configDir)) {
2191
+ throw new Error(
2192
+ `Encrypted key path must be within attest-it config directory (${configDir}). Got: ${resolvedPath}`
2193
+ );
2194
+ }
2195
+ if (!serial) {
2196
+ console.warn(
2197
+ "WARNING: No YubiKey serial number specified. Key will not be bound to a specific device. For better security, specify a serial number."
2198
+ );
2199
+ }
2200
+ if (!await _YubiKeyProvider.isChallengeResponseConfigured(slot, serial)) {
2201
+ throw new Error(
2202
+ `YubiKey slot ${String(slot)} is not configured for HMAC challenge-response. Ensure your YubiKey is connected and use "ykman otp chalresp --generate 2" to configure it.`
2203
+ );
2204
+ }
2205
+ const challenge = crypto3.randomBytes(32);
2206
+ const salt = crypto3.randomBytes(32);
2207
+ const iv = crypto3.randomBytes(12);
2208
+ const response = await performChallengeResponse(challenge, slot, serial);
2209
+ const aesKey = deriveKey(response, salt);
2210
+ const aad = constructAAD(1, slot, serial);
2211
+ const cipher = crypto3.createCipheriv("aes-256-gcm", aesKey, iv);
2212
+ cipher.setAAD(aad);
2213
+ const ciphertext = Buffer.concat([
2214
+ cipher.update(Buffer.from(privateKey, "utf8")),
2215
+ cipher.final()
2216
+ ]);
2217
+ const authTag = cipher.getAuthTag();
2218
+ const keyFile = {
2219
+ version: 1,
2220
+ iv: iv.toString("base64"),
2221
+ authTag: authTag.toString("base64"),
2222
+ salt: salt.toString("base64"),
2223
+ challenge: challenge.toString("base64"),
2224
+ ciphertext: ciphertext.toString("base64"),
2225
+ slot,
2226
+ aad: aad.toString("base64"),
2227
+ ...serial && { serial }
2228
+ };
2229
+ await fs7.mkdir(path6.dirname(resolvedPath), { recursive: true });
2230
+ await fs7.writeFile(resolvedPath, JSON.stringify(keyFile, null, 2), { mode: 384 });
2231
+ await setKeyPermissions(resolvedPath);
2232
+ return {
2233
+ encryptedKeyPath: resolvedPath,
2234
+ storageDescription: `YubiKey-encrypted: ${resolvedPath}`
2235
+ };
2236
+ }
2237
+ /**
2238
+ * Get the configuration for this provider.
2239
+ */
2240
+ getConfig() {
2241
+ return {
2242
+ type: this.type,
2243
+ options: {
2244
+ encryptedKeyPath: this.encryptedKeyPath,
2245
+ slot: this.slot,
2246
+ ...this.serial && { serial: this.serial }
2247
+ }
2248
+ };
2249
+ }
2250
+ };
2251
+ async function execCommand3(command, args) {
2252
+ return new Promise((resolve4, reject) => {
2253
+ const proc = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
2254
+ let stdout = "";
2255
+ let stderr = "";
2256
+ proc.stdout.on("data", (data) => {
2257
+ stdout += data.toString();
2258
+ });
2259
+ proc.stderr.on("data", (data) => {
2260
+ stderr += data.toString();
2261
+ });
2262
+ proc.on("close", (code) => {
2263
+ if (code === 0) {
2264
+ resolve4(stdout.trim());
2265
+ } else {
2266
+ reject(new Error(`Command failed with exit code ${String(code)}: ${stderr}`));
2267
+ }
2268
+ });
2269
+ proc.on("error", (error) => {
2270
+ reject(error);
2271
+ });
2272
+ });
2273
+ }
2274
+ async function performChallengeResponse(challenge, slot, serial) {
2275
+ const args = ["otp", "chalresp", "--slot", String(slot)];
2276
+ if (serial) {
2277
+ args.unshift("--device", serial);
2278
+ }
2279
+ args.push(challenge.toString("hex"));
2280
+ try {
2281
+ const output = await execCommand3("ykman", args);
2282
+ return Buffer.from(output.trim(), "hex");
2283
+ } catch {
2284
+ throw new Error(
2285
+ "YubiKey challenge-response failed. Verify your YubiKey is inserted and the slot is configured for challenge-response."
2286
+ );
2287
+ }
2288
+ }
2289
+ function deriveKey(response, salt) {
2290
+ const derived = crypto3.hkdfSync("sha256", response, salt, "attest-it-yubikey-v1", 32);
2291
+ return Buffer.from(derived);
2292
+ }
2293
+
2294
+ // src/key-provider/registry.ts
2295
+ var KeyProviderRegistry = class {
2296
+ static providers = /* @__PURE__ */ new Map();
2297
+ /**
2298
+ * Register a key provider factory.
2299
+ * @param type - Provider type identifier
2300
+ * @param factory - Factory function to create provider instances
2301
+ */
2302
+ static register(type, factory) {
2303
+ this.providers.set(type, factory);
2304
+ }
2305
+ /**
2306
+ * Create a key provider from configuration.
2307
+ * @param config - Provider configuration
2308
+ * @returns A key provider instance
2309
+ * @throws Error if the provider type is not registered
2310
+ */
2311
+ static create(config) {
2312
+ const factory = this.providers.get(config.type);
2313
+ if (!factory) {
2314
+ throw new Error(
2315
+ `Unknown key provider type: ${config.type}. Available types: ${Array.from(this.providers.keys()).join(", ")}`
2316
+ );
2317
+ }
2318
+ return factory(config);
2319
+ }
2320
+ /**
2321
+ * Get all registered provider types.
2322
+ * @returns Array of provider type identifiers
2323
+ */
2324
+ static getProviderTypes() {
2325
+ return Array.from(this.providers.keys());
2326
+ }
2327
+ };
2328
+ KeyProviderRegistry.register("filesystem", (config) => {
2329
+ const privateKeyPath = typeof config.options.privateKeyPath === "string" ? config.options.privateKeyPath : void 0;
2330
+ if (privateKeyPath !== void 0) {
2331
+ return new FilesystemKeyProvider({ privateKeyPath });
2332
+ }
2333
+ return new FilesystemKeyProvider();
2334
+ });
2335
+ KeyProviderRegistry.register("1password", (config) => {
2336
+ const { options } = config;
2337
+ const account = typeof options.account === "string" ? options.account : void 0;
2338
+ const vault = typeof options.vault === "string" ? options.vault : "";
2339
+ const itemName = typeof options.itemName === "string" ? options.itemName : "";
2340
+ if (!vault || !itemName) {
2341
+ throw new Error("1Password provider requires vault and itemName options");
2342
+ }
2343
+ if (account !== void 0) {
2344
+ return new OnePasswordKeyProvider({ account, vault, itemName });
2345
+ }
2346
+ return new OnePasswordKeyProvider({ vault, itemName });
2347
+ });
2348
+ KeyProviderRegistry.register("macos-keychain", (config) => {
2349
+ const { options } = config;
2350
+ const itemName = typeof options.itemName === "string" ? options.itemName : "";
2351
+ if (!itemName) {
2352
+ throw new Error("macOS Keychain provider requires itemName option");
2353
+ }
2354
+ return new MacOSKeychainKeyProvider({ itemName });
2355
+ });
2356
+ KeyProviderRegistry.register("yubikey", (config) => {
2357
+ const { options } = config;
2358
+ const encryptedKeyPath = typeof options.encryptedKeyPath === "string" ? options.encryptedKeyPath : "";
2359
+ if (!encryptedKeyPath) {
2360
+ throw new Error("YubiKey provider requires encryptedKeyPath option");
2361
+ }
2362
+ const slot = typeof options.slot === "number" && (options.slot === 1 || options.slot === 2) ? options.slot : void 0;
2363
+ const serial = typeof options.serial === "string" ? options.serial : void 0;
2364
+ const providerOptions = {
2365
+ encryptedKeyPath
2366
+ };
2367
+ if (slot !== void 0) {
2368
+ providerOptions.slot = slot;
2369
+ }
2370
+ if (serial !== void 0) {
2371
+ providerOptions.serial = serial;
2372
+ }
2373
+ return new YubiKeyProvider(providerOptions);
2374
+ });
2375
+ var cliExperienceSchema = z.object({
2376
+ declinedCompletionInstall: z.boolean().optional()
2377
+ }).strict();
2378
+ var userPreferencesSchema = z.object({
2379
+ cliExperience: cliExperienceSchema.optional()
2380
+ }).strict();
2381
+ function getPreferencesPath() {
2382
+ return join(getAttestItConfigDir(), "preferences.yaml");
2383
+ }
2384
+ async function loadPreferences() {
2385
+ const prefsPath = getPreferencesPath();
2386
+ try {
2387
+ const content = await readFile(prefsPath, "utf8");
2388
+ const parsed = parse(content);
2389
+ const result = userPreferencesSchema.safeParse(parsed);
2390
+ if (result.success) {
2391
+ const prefs = {};
2392
+ if (result.data.cliExperience) {
2393
+ prefs.cliExperience = {
2394
+ ...result.data.cliExperience.declinedCompletionInstall !== void 0 && {
2395
+ declinedCompletionInstall: result.data.cliExperience.declinedCompletionInstall
2396
+ }
2397
+ };
2398
+ }
2399
+ return prefs;
2400
+ }
2401
+ console.warn("Invalid preferences file, using defaults:", result.error.message);
2402
+ return {};
2403
+ } catch (error) {
2404
+ if (error && typeof error === "object" && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
2405
+ return {};
2406
+ }
2407
+ throw error;
2408
+ }
2409
+ }
2410
+ async function savePreferences(preferences) {
2411
+ const prefsPath = getPreferencesPath();
2412
+ const content = stringify(preferences);
2413
+ const dir = dirname(prefsPath);
2414
+ await mkdir(dir, { recursive: true });
2415
+ await writeFile(prefsPath, content, "utf8");
2416
+ }
2417
+ async function setPreference(key, value) {
2418
+ const prefs = await loadPreferences();
2419
+ prefs[key] = value;
2420
+ await savePreferences(prefs);
2421
+ }
2422
+ async function getPreference(key) {
2423
+ const prefs = await loadPreferences();
2424
+ return prefs[key];
2425
+ }
1585
2426
  function isAuthorizedSigner(config, gateId, publicKey) {
1586
2427
  const gate = config.gates?.[gateId];
1587
2428
  if (!gate) {
@@ -1898,6 +2739,6 @@ function verifyAllSeals(config, seals, fingerprints) {
1898
2739
  // src/index.ts
1899
2740
  var version = "0.0.0";
1900
2741
 
1901
- export { ConfigNotFoundError, ConfigValidationError, FilesystemKeyProvider, KeyProviderRegistry, LocalConfigValidationError, MacOSKeychainKeyProvider, OnePasswordKeyProvider, SignatureInvalidError, canonicalizeAttestations, computeFingerprint, computeFingerprintSync, createAttestation, createSeal, findAttestation, findConfigPath, findTeamMemberByPublicKey, generateKeyPair2 as generateEd25519KeyPair, getActiveIdentity, getAttestItConfigDir, getAttestItHomeDir, getAuthorizedSignersForGate, getGate, getLocalConfigPath, getPublicKeyFromPrivate, isAuthorizedSigner, listPackageFiles, loadConfig, loadConfigSync, loadLocalConfig, loadLocalConfigSync, parseDuration, readAndVerifyAttestations, readAttestations, readAttestationsSync, readSeals, readSealsSync, removeAttestation, resolveConfigPaths, saveLocalConfig, saveLocalConfigSync, setAttestItHomeDir, sign3 as signEd25519, toAttestItConfig, upsertAttestation, verifyAllSeals, verifyAttestations, verify3 as verifyEd25519, verifyGateSeal, verifySeal, version, writeAttestations, writeAttestationsSync, writeSeals, writeSealsSync, writeSignedAttestations };
2742
+ export { ConfigNotFoundError, ConfigValidationError, FilesystemKeyProvider, KeyProviderRegistry, LocalConfigValidationError, MacOSKeychainKeyProvider, OnePasswordKeyProvider, OperationalValidationError, PolicyValidationError, SignatureInvalidError, YubiKeyProvider, canonicalizeAttestations, computeFingerprint, computeFingerprintSync, createAttestation, createSeal, findAttestation, findConfigPath, findTeamMemberByPublicKey, generateKeyPair2 as generateEd25519KeyPair, getActiveIdentity, getAttestItConfigDir, getAttestItHomeDir, getAuthorizedSignersForGate, getGate, getLocalConfigPath, getPreference, getPreferencesPath, getPublicKeyFromPrivate, isAuthorizedSigner, listPackageFiles, loadConfig, loadConfigSync, loadLocalConfig, loadLocalConfigSync, loadPreferences, mergeConfigs, operationalSchema, parseDuration, parseOperationalContent, parsePolicyContent, policySchema, readAndVerifyAttestations, readAttestations, readAttestationsSync, readSeals, readSealsSync, removeAttestation, resolveConfigPaths, saveLocalConfig, saveLocalConfigSync, savePreferences, setAttestItHomeDir, setPreference, sign3 as signEd25519, toAttestItConfig, upsertAttestation, validateSuiteGateReferences, verifyAllSeals, verifyAttestations, verify3 as verifyEd25519, verifyGateSeal, verifySeal, version, writeAttestations, writeAttestationsSync, writeSeals, writeSealsSync, writeSignedAttestations };
1902
2743
  //# sourceMappingURL=index.js.map
1903
2744
  //# sourceMappingURL=index.js.map