@attest-it/core 0.4.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.cjs CHANGED
@@ -1,14 +1,14 @@
1
1
  'use strict';
2
2
 
3
3
  var child_process = require('child_process');
4
- var fs2 = require('fs/promises');
4
+ var fs8 = require('fs/promises');
5
5
  var path2 = require('path');
6
6
  var os = require('os');
7
7
  var fs = require('fs');
8
8
  var ms = require('ms');
9
9
  var yaml = require('yaml');
10
10
  var zod = require('zod');
11
- var crypto2 = require('crypto');
11
+ var crypto3 = require('crypto');
12
12
  var tinyglobby = require('tinyglobby');
13
13
  var canonicalizeNamespace = require('canonicalize');
14
14
 
@@ -32,12 +32,12 @@ function _interopNamespace(e) {
32
32
  return Object.freeze(n);
33
33
  }
34
34
 
35
- var fs2__namespace = /*#__PURE__*/_interopNamespace(fs2);
35
+ var fs8__namespace = /*#__PURE__*/_interopNamespace(fs8);
36
36
  var path2__namespace = /*#__PURE__*/_interopNamespace(path2);
37
37
  var os__namespace = /*#__PURE__*/_interopNamespace(os);
38
38
  var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
39
39
  var ms__default = /*#__PURE__*/_interopDefault(ms);
40
- var crypto2__namespace = /*#__PURE__*/_interopNamespace(crypto2);
40
+ var crypto3__namespace = /*#__PURE__*/_interopNamespace(crypto3);
41
41
  var canonicalizeNamespace__namespace = /*#__PURE__*/_interopNamespace(canonicalizeNamespace);
42
42
 
43
43
  var __defProp = Object.defineProperty;
@@ -62,7 +62,7 @@ __export(crypto_exports, {
62
62
  verify: () => verify
63
63
  });
64
64
  async function runOpenSSL(args, stdin) {
65
- return new Promise((resolve3, reject) => {
65
+ return new Promise((resolve4, reject) => {
66
66
  const child = child_process.spawn("openssl", args, {
67
67
  stdio: ["pipe", "pipe", "pipe"]
68
68
  });
@@ -78,7 +78,7 @@ async function runOpenSSL(args, stdin) {
78
78
  reject(new Error(`Failed to spawn OpenSSL: ${err.message}`));
79
79
  });
80
80
  child.on("close", (code) => {
81
- resolve3({
81
+ resolve4({
82
82
  exitCode: code ?? 1,
83
83
  stdout: Buffer.concat(stdoutChunks),
84
84
  stderr
@@ -120,7 +120,7 @@ function getDefaultPublicKeyPath() {
120
120
  }
121
121
  async function ensureDir(dirPath) {
122
122
  try {
123
- await fs2__namespace.mkdir(dirPath, { recursive: true });
123
+ await fs8__namespace.mkdir(dirPath, { recursive: true });
124
124
  } catch (err) {
125
125
  if (err instanceof Error && "code" in err && err.code !== "EEXIST") {
126
126
  throw err;
@@ -129,7 +129,7 @@ async function ensureDir(dirPath) {
129
129
  }
130
130
  async function fileExists(filePath) {
131
131
  try {
132
- await fs2__namespace.access(filePath);
132
+ await fs8__namespace.access(filePath);
133
133
  return true;
134
134
  } catch {
135
135
  return false;
@@ -138,7 +138,7 @@ async function fileExists(filePath) {
138
138
  async function cleanupFiles(...paths) {
139
139
  for (const filePath of paths) {
140
140
  try {
141
- await fs2__namespace.unlink(filePath);
141
+ await fs8__namespace.unlink(filePath);
142
142
  } catch {
143
143
  }
144
144
  }
@@ -212,21 +212,21 @@ async function sign(options) {
212
212
  }
213
213
  const dataBuffer = typeof data === "string" ? Buffer.from(data, "utf8") : data;
214
214
  const processBuffer = dataBuffer.length === 0 ? Buffer.from([0]) : dataBuffer;
215
- const tmpDir = await fs2__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-"));
215
+ const tmpDir = await fs8__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-"));
216
216
  const dataFile = path2__namespace.join(tmpDir, "data.bin");
217
217
  const sigFile = path2__namespace.join(tmpDir, "sig.bin");
218
218
  try {
219
- await fs2__namespace.writeFile(dataFile, processBuffer);
219
+ await fs8__namespace.writeFile(dataFile, processBuffer);
220
220
  const signArgs = ["dgst", "-sha256", "-sign", effectiveKeyPath, "-out", sigFile, dataFile];
221
221
  const result = await runOpenSSL(signArgs);
222
222
  if (result.exitCode !== 0) {
223
223
  throw new Error(`Failed to sign data: ${result.stderr}`);
224
224
  }
225
- const sigBuffer = await fs2__namespace.readFile(sigFile);
225
+ const sigBuffer = await fs8__namespace.readFile(sigFile);
226
226
  return sigBuffer.toString("base64");
227
227
  } finally {
228
228
  try {
229
- await fs2__namespace.rm(tmpDir, { recursive: true, force: true });
229
+ await fs8__namespace.rm(tmpDir, { recursive: true, force: true });
230
230
  } catch {
231
231
  }
232
232
  }
@@ -245,12 +245,12 @@ async function verify(options) {
245
245
  const dataBuffer = typeof data === "string" ? Buffer.from(data, "utf8") : data;
246
246
  const processBuffer = dataBuffer.length === 0 ? Buffer.from([0]) : dataBuffer;
247
247
  const sigBuffer = Buffer.from(signature, "base64");
248
- const tmpDir = await fs2__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-"));
248
+ const tmpDir = await fs8__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-"));
249
249
  const dataFile = path2__namespace.join(tmpDir, "data.bin");
250
250
  const sigFile = path2__namespace.join(tmpDir, "sig.bin");
251
251
  try {
252
- await fs2__namespace.writeFile(dataFile, processBuffer);
253
- await fs2__namespace.writeFile(sigFile, sigBuffer);
252
+ await fs8__namespace.writeFile(dataFile, processBuffer);
253
+ await fs8__namespace.writeFile(sigFile, sigBuffer);
254
254
  const verifyArgs = [
255
255
  "dgst",
256
256
  "-sha256",
@@ -264,16 +264,16 @@ async function verify(options) {
264
264
  return result.exitCode === 0 && result.stdout.toString().includes("Verified OK");
265
265
  } finally {
266
266
  try {
267
- await fs2__namespace.rm(tmpDir, { recursive: true, force: true });
267
+ await fs8__namespace.rm(tmpDir, { recursive: true, force: true });
268
268
  } catch {
269
269
  }
270
270
  }
271
271
  }
272
272
  async function setKeyPermissions(keyPath) {
273
273
  if (process.platform === "win32") {
274
- await fs2__namespace.chmod(keyPath, 384);
274
+ await fs8__namespace.chmod(keyPath, 384);
275
275
  } else {
276
- await fs2__namespace.chmod(keyPath, 384);
276
+ await fs8__namespace.chmod(keyPath, 384);
277
277
  }
278
278
  }
279
279
  var openSSLChecked;
@@ -431,7 +431,7 @@ async function loadConfig(configPath) {
431
431
  );
432
432
  }
433
433
  try {
434
- const content = await fs2.readFile(resolvedPath, "utf8");
434
+ const content = await fs8.readFile(resolvedPath, "utf8");
435
435
  const format = getConfigFormat(resolvedPath);
436
436
  return parseConfigContent(content, format);
437
437
  } catch (error) {
@@ -521,6 +521,299 @@ function toAttestItConfig(config) {
521
521
  );
522
522
  return result;
523
523
  }
524
+ var teamMemberSchema2 = zod.z.object({
525
+ name: zod.z.string().min(1, "Team member name cannot be empty"),
526
+ email: zod.z.string().email().optional(),
527
+ github: zod.z.string().min(1).optional(),
528
+ publicKey: zod.z.string().min(1, "Public key is required")
529
+ }).strict();
530
+ var fingerprintConfigSchema2 = zod.z.object({
531
+ paths: zod.z.array(zod.z.string().min(1, "Path cannot be empty")).min(1, "At least one path is required"),
532
+ exclude: zod.z.array(zod.z.string().min(1, "Exclude pattern cannot be empty")).optional()
533
+ }).strict();
534
+ var durationSchema2 = zod.z.string().refine(
535
+ (val) => {
536
+ try {
537
+ const parsed = ms__default.default(val);
538
+ return typeof parsed === "number" && parsed > 0;
539
+ } catch {
540
+ return false;
541
+ }
542
+ },
543
+ {
544
+ message: 'Duration must be a valid duration string (e.g., "30d", "7d", "24h")'
545
+ }
546
+ );
547
+ var gateSchema2 = zod.z.object({
548
+ name: zod.z.string().min(1, "Gate name cannot be empty"),
549
+ description: zod.z.string().min(1, "Gate description cannot be empty"),
550
+ authorizedSigners: zod.z.array(zod.z.string().min(1, "Authorized signer slug cannot be empty")).min(1, "At least one authorized signer is required"),
551
+ fingerprint: fingerprintConfigSchema2,
552
+ maxAge: durationSchema2
553
+ }).strict();
554
+ var keyProviderOptionsSchema2 = zod.z.object({
555
+ privateKeyPath: zod.z.string().optional(),
556
+ account: zod.z.string().optional(),
557
+ vault: zod.z.string().optional(),
558
+ itemName: zod.z.string().optional()
559
+ }).passthrough();
560
+ var keyProviderSchema2 = zod.z.object({
561
+ type: zod.z.enum(["filesystem", "1password"]).or(zod.z.string()),
562
+ options: keyProviderOptionsSchema2.optional()
563
+ }).strict();
564
+
565
+ // src/config/policy-schema.ts
566
+ var policySettingsSchema = zod.z.object({
567
+ maxAgeDays: zod.z.number().int().positive().default(30),
568
+ publicKeyPath: zod.z.string().default(".attest-it/pubkey.pem"),
569
+ attestationsPath: zod.z.string().default(".attest-it/attestations.json")
570
+ }).strict();
571
+ var policySchema = zod.z.object({
572
+ version: zod.z.literal(1),
573
+ settings: policySettingsSchema.default({}),
574
+ team: zod.z.record(zod.z.string(), teamMemberSchema2).optional(),
575
+ gates: zod.z.record(zod.z.string(), gateSchema2).optional()
576
+ }).strict();
577
+ var PolicyValidationError = class extends Error {
578
+ constructor(message, issues) {
579
+ super(message);
580
+ this.issues = issues;
581
+ this.name = "PolicyValidationError";
582
+ }
583
+ };
584
+ function parsePolicyContent(content, format) {
585
+ let rawConfig;
586
+ try {
587
+ if (format === "yaml") {
588
+ rawConfig = yaml.parse(content);
589
+ } else {
590
+ rawConfig = JSON.parse(content);
591
+ }
592
+ } catch (error) {
593
+ throw new PolicyValidationError(
594
+ `Failed to parse ${format.toUpperCase()}: ${error instanceof Error ? error.message : String(error)}`,
595
+ []
596
+ );
597
+ }
598
+ const result = policySchema.safeParse(rawConfig);
599
+ if (!result.success) {
600
+ throw new PolicyValidationError(
601
+ "Policy validation failed:\n" + result.error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n"),
602
+ result.error.issues
603
+ );
604
+ }
605
+ return result.data;
606
+ }
607
+ var operationalSettingsSchema = zod.z.object({
608
+ defaultCommand: zod.z.string().optional(),
609
+ keyProvider: keyProviderSchema2.optional()
610
+ }).strict();
611
+ var suiteSchema2 = zod.z.object({
612
+ // Gate fields (if present, this suite references a gate)
613
+ gate: zod.z.string().optional(),
614
+ // Legacy fingerprint definition (for backward compatibility)
615
+ description: zod.z.string().optional(),
616
+ packages: zod.z.array(zod.z.string().min(1, "Package path cannot be empty")).optional(),
617
+ files: zod.z.array(zod.z.string().min(1, "File path cannot be empty")).optional(),
618
+ ignore: zod.z.array(zod.z.string().min(1, "Ignore pattern cannot be empty")).optional(),
619
+ // CLI-specific fields
620
+ command: zod.z.string().optional(),
621
+ timeout: zod.z.string().optional(),
622
+ interactive: zod.z.boolean().optional(),
623
+ // Relationship fields
624
+ invalidates: zod.z.array(zod.z.string().min(1, "Invalidated suite name cannot be empty")).optional(),
625
+ depends_on: zod.z.array(zod.z.string().min(1, "Dependency suite name cannot be empty")).optional()
626
+ }).strict().refine(
627
+ (suite) => {
628
+ return suite.gate !== void 0 || suite.packages !== void 0 && suite.packages.length > 0;
629
+ },
630
+ {
631
+ message: "Suite must either reference a gate or define packages for fingerprinting"
632
+ }
633
+ );
634
+ var operationalSchema = zod.z.object({
635
+ version: zod.z.literal(1),
636
+ settings: operationalSettingsSchema.default({}),
637
+ suites: zod.z.record(zod.z.string(), suiteSchema2).refine((suites) => Object.keys(suites).length >= 1, {
638
+ message: "At least one suite must be defined"
639
+ }),
640
+ groups: zod.z.record(zod.z.string(), zod.z.array(zod.z.string().min(1, "Suite name in group cannot be empty"))).optional()
641
+ }).strict();
642
+ var OperationalValidationError = class extends Error {
643
+ constructor(message, issues) {
644
+ super(message);
645
+ this.issues = issues;
646
+ this.name = "OperationalValidationError";
647
+ }
648
+ };
649
+ function parseOperationalContent(content, format) {
650
+ let rawConfig;
651
+ try {
652
+ if (format === "yaml") {
653
+ rawConfig = yaml.parse(content);
654
+ } else {
655
+ rawConfig = JSON.parse(content);
656
+ }
657
+ } catch (error) {
658
+ throw new OperationalValidationError(
659
+ `Failed to parse ${format.toUpperCase()}: ${error instanceof Error ? error.message : String(error)}`,
660
+ []
661
+ );
662
+ }
663
+ const result = operationalSchema.safeParse(rawConfig);
664
+ if (!result.success) {
665
+ throw new OperationalValidationError(
666
+ "Operational configuration validation failed:\n" + result.error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n"),
667
+ result.error.issues
668
+ );
669
+ }
670
+ return result.data;
671
+ }
672
+
673
+ // src/config/merge.ts
674
+ function toSuiteConfig(suite) {
675
+ const result = {};
676
+ if (suite.gate !== void 0) result.gate = suite.gate;
677
+ if (suite.description !== void 0) result.description = suite.description;
678
+ if (suite.packages !== void 0) result.packages = suite.packages;
679
+ if (suite.files !== void 0) result.files = suite.files;
680
+ if (suite.ignore !== void 0) result.ignore = suite.ignore;
681
+ if (suite.command !== void 0) result.command = suite.command;
682
+ if (suite.timeout !== void 0) result.timeout = suite.timeout;
683
+ if (suite.interactive !== void 0) result.interactive = suite.interactive;
684
+ if (suite.invalidates !== void 0) result.invalidates = suite.invalidates;
685
+ if (suite.depends_on !== void 0) result.depends_on = suite.depends_on;
686
+ return result;
687
+ }
688
+ function toTeamMember(member) {
689
+ const result = {
690
+ name: member.name,
691
+ publicKey: member.publicKey
692
+ };
693
+ if (member.email !== void 0) result.email = member.email;
694
+ if (member.github !== void 0) result.github = member.github;
695
+ return result;
696
+ }
697
+ function toGateConfig(gate) {
698
+ const fingerprint = {
699
+ paths: gate.fingerprint.paths
700
+ };
701
+ if (gate.fingerprint.exclude !== void 0) {
702
+ fingerprint.exclude = gate.fingerprint.exclude;
703
+ }
704
+ return {
705
+ name: gate.name,
706
+ description: gate.description,
707
+ authorizedSigners: gate.authorizedSigners,
708
+ fingerprint,
709
+ maxAge: gate.maxAge
710
+ };
711
+ }
712
+ function toKeyProvider(provider) {
713
+ const result = {
714
+ type: provider.type
715
+ };
716
+ if (provider.options !== void 0) {
717
+ const options = {};
718
+ let hasOptions = false;
719
+ if (provider.options.privateKeyPath !== void 0) {
720
+ options.privateKeyPath = provider.options.privateKeyPath;
721
+ hasOptions = true;
722
+ }
723
+ if (provider.options.account !== void 0) {
724
+ options.account = provider.options.account;
725
+ hasOptions = true;
726
+ }
727
+ if (provider.options.vault !== void 0) {
728
+ options.vault = provider.options.vault;
729
+ hasOptions = true;
730
+ }
731
+ if (provider.options.itemName !== void 0) {
732
+ options.itemName = provider.options.itemName;
733
+ hasOptions = true;
734
+ }
735
+ if (hasOptions) {
736
+ result.options = options;
737
+ }
738
+ }
739
+ return result;
740
+ }
741
+ function mergeConfigs(policy, operational) {
742
+ const settings = {
743
+ // Security settings from policy (these are trust-critical)
744
+ maxAgeDays: policy.settings.maxAgeDays,
745
+ publicKeyPath: policy.settings.publicKeyPath,
746
+ attestationsPath: policy.settings.attestationsPath
747
+ };
748
+ if (operational.settings.defaultCommand !== void 0) {
749
+ settings.defaultCommand = operational.settings.defaultCommand;
750
+ }
751
+ if (operational.settings.keyProvider !== void 0) {
752
+ settings.keyProvider = toKeyProvider(operational.settings.keyProvider);
753
+ }
754
+ const suites = {};
755
+ for (const [name, suite] of Object.entries(operational.suites)) {
756
+ suites[name] = toSuiteConfig(suite);
757
+ }
758
+ const config = {
759
+ version: 1,
760
+ settings,
761
+ suites
762
+ };
763
+ if (policy.team !== void 0) {
764
+ const team = {};
765
+ for (const [slug, member] of Object.entries(policy.team)) {
766
+ team[slug] = toTeamMember(member);
767
+ }
768
+ config.team = team;
769
+ }
770
+ if (policy.gates !== void 0) {
771
+ const gates = {};
772
+ for (const [slug, gate] of Object.entries(policy.gates)) {
773
+ gates[slug] = toGateConfig(gate);
774
+ }
775
+ config.gates = gates;
776
+ }
777
+ if (operational.groups !== void 0) {
778
+ config.groups = operational.groups;
779
+ }
780
+ return config;
781
+ }
782
+
783
+ // src/config/validation.ts
784
+ function validateSuiteGateReferences(policy, operational) {
785
+ const errors = [];
786
+ const gates = policy.gates ?? {};
787
+ const team = policy.team ?? {};
788
+ for (const [suiteName, suiteConfig] of Object.entries(operational.suites)) {
789
+ const gateName = suiteConfig.gate;
790
+ if (gateName === void 0) {
791
+ continue;
792
+ }
793
+ const gate = gates[gateName];
794
+ if (gate === void 0) {
795
+ errors.push({
796
+ type: "UNKNOWN_GATE",
797
+ suite: suiteName,
798
+ gate: gateName,
799
+ message: `Suite "${suiteName}" references unknown gate "${gateName}". The gate must be defined in policy.yaml.`
800
+ });
801
+ continue;
802
+ }
803
+ for (const signerSlug of gate.authorizedSigners) {
804
+ if (team[signerSlug] === void 0) {
805
+ errors.push({
806
+ type: "MISSING_TEAM_MEMBER",
807
+ suite: suiteName,
808
+ gate: gateName,
809
+ signer: signerSlug,
810
+ message: `Gate "${gateName}" (referenced by suite "${suiteName}") authorizes signer "${signerSlug}", but this team member is not defined in policy.yaml.`
811
+ });
812
+ }
813
+ }
814
+ }
815
+ return errors;
816
+ }
524
817
  var LARGE_FILE_THRESHOLD = 50 * 1024 * 1024;
525
818
  function sortFiles(files) {
526
819
  return [...files].sort((a, b) => {
@@ -540,13 +833,13 @@ function computeFinalFingerprint(fileHashes) {
540
833
  });
541
834
  const hashes = sorted.map((input) => input.hash);
542
835
  const concatenated = Buffer.concat(hashes);
543
- const finalHash = crypto2__namespace.createHash("sha256").update(concatenated).digest();
836
+ const finalHash = crypto3__namespace.createHash("sha256").update(concatenated).digest();
544
837
  return `sha256:${finalHash.toString("hex")}`;
545
838
  }
546
839
  async function hashFileAsync(realPath, normalizedPath, stats) {
547
840
  if (stats.size > LARGE_FILE_THRESHOLD) {
548
- return new Promise((resolve3, reject) => {
549
- const hash2 = crypto2__namespace.createHash("sha256");
841
+ return new Promise((resolve4, reject) => {
842
+ const hash2 = crypto3__namespace.createHash("sha256");
550
843
  hash2.update(normalizedPath);
551
844
  hash2.update(":");
552
845
  const stream = fs__namespace.createReadStream(realPath);
@@ -554,13 +847,13 @@ async function hashFileAsync(realPath, normalizedPath, stats) {
554
847
  hash2.update(chunk);
555
848
  });
556
849
  stream.on("end", () => {
557
- resolve3(hash2.digest());
850
+ resolve4(hash2.digest());
558
851
  });
559
852
  stream.on("error", reject);
560
853
  });
561
854
  }
562
855
  const content = await fs__namespace.promises.readFile(realPath);
563
- const hash = crypto2__namespace.createHash("sha256");
856
+ const hash = crypto3__namespace.createHash("sha256");
564
857
  hash.update(normalizedPath);
565
858
  hash.update(":");
566
859
  hash.update(content);
@@ -568,7 +861,7 @@ async function hashFileAsync(realPath, normalizedPath, stats) {
568
861
  }
569
862
  function hashFileSync(realPath, normalizedPath) {
570
863
  const content = fs__namespace.readFileSync(realPath);
571
- const hash = crypto2__namespace.createHash("sha256");
864
+ const hash = crypto3__namespace.createHash("sha256");
572
865
  hash.update(normalizedPath);
573
866
  hash.update(":");
574
867
  hash.update(content);
@@ -876,7 +1169,7 @@ function isBuffer(value) {
876
1169
  }
877
1170
  function generateKeyPair2() {
878
1171
  try {
879
- const keyPair = crypto2__namespace.generateKeyPairSync("ed25519", {
1172
+ const keyPair = crypto3__namespace.generateKeyPairSync("ed25519", {
880
1173
  publicKeyEncoding: {
881
1174
  type: "spki",
882
1175
  format: "pem"
@@ -890,7 +1183,7 @@ function generateKeyPair2() {
890
1183
  if (typeof publicKey !== "string" || typeof privateKey !== "string") {
891
1184
  throw new Error("Expected keypair to have string keys");
892
1185
  }
893
- const publicKeyObj = crypto2__namespace.createPublicKey(publicKey);
1186
+ const publicKeyObj = crypto3__namespace.createPublicKey(publicKey);
894
1187
  const publicKeyExport = publicKeyObj.export({
895
1188
  type: "spki",
896
1189
  format: "der"
@@ -913,8 +1206,8 @@ function generateKeyPair2() {
913
1206
  function sign3(data, privateKeyPem) {
914
1207
  try {
915
1208
  const dataBuffer = typeof data === "string" ? Buffer.from(data, "utf8") : data;
916
- const privateKeyObj = crypto2__namespace.createPrivateKey(privateKeyPem);
917
- const signatureResult = crypto2__namespace.sign(null, dataBuffer, privateKeyObj);
1209
+ const privateKeyObj = crypto3__namespace.createPrivateKey(privateKeyPem);
1210
+ const signatureResult = crypto3__namespace.sign(null, dataBuffer, privateKeyObj);
918
1211
  if (!isBuffer(signatureResult)) {
919
1212
  throw new Error("Expected signature to be a Buffer");
920
1213
  }
@@ -954,12 +1247,12 @@ function verify3(data, signature, publicKeyBase64) {
954
1247
  // BIT STRING, 33 bytes (32 key + 1 padding)
955
1248
  ]);
956
1249
  const spkiBuffer = Buffer.concat([spkiHeader, rawPublicKey]);
957
- const publicKeyObj = crypto2__namespace.createPublicKey({
1250
+ const publicKeyObj = crypto3__namespace.createPublicKey({
958
1251
  key: spkiBuffer,
959
1252
  format: "der",
960
1253
  type: "spki"
961
1254
  });
962
- return crypto2__namespace.verify(null, dataBuffer, publicKeyObj, signatureBuffer);
1255
+ return crypto3__namespace.verify(null, dataBuffer, publicKeyObj, signatureBuffer);
963
1256
  } catch (err) {
964
1257
  if (err instanceof Error && err.message.includes("verification failed")) {
965
1258
  return false;
@@ -971,8 +1264,8 @@ function verify3(data, signature, publicKeyBase64) {
971
1264
  }
972
1265
  function getPublicKeyFromPrivate(privateKeyPem) {
973
1266
  try {
974
- const privateKeyObj = crypto2__namespace.createPrivateKey(privateKeyPem);
975
- const publicKeyObj = crypto2__namespace.createPublicKey(privateKeyObj);
1267
+ const privateKeyObj = crypto3__namespace.createPrivateKey(privateKeyPem);
1268
+ const publicKeyObj = crypto3__namespace.createPublicKey(privateKeyObj);
976
1269
  const publicKeyExport = publicKeyObj.export({
977
1270
  type: "spki",
978
1271
  format: "der"
@@ -1141,7 +1434,7 @@ var FilesystemKeyProvider = class {
1141
1434
  */
1142
1435
  async keyExists(keyRef) {
1143
1436
  try {
1144
- await fs2__namespace.access(keyRef);
1437
+ await fs8__namespace.access(keyRef);
1145
1438
  return true;
1146
1439
  } catch {
1147
1440
  return false;
@@ -1302,7 +1595,7 @@ var OnePasswordKeyProvider = class _OnePasswordKeyProvider {
1302
1595
  `Key not found in 1Password: "${keyRef}" (vault: ${this.vault})` + (this.account ? ` (account: ${this.account})` : "")
1303
1596
  );
1304
1597
  }
1305
- const tempDir = await fs2__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-"));
1598
+ const tempDir = await fs8__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-"));
1306
1599
  const tempKeyPath = path2__namespace.join(tempDir, "private.pem");
1307
1600
  try {
1308
1601
  const args = ["document", "get", keyRef, "--vault", this.vault, "--out-file", tempKeyPath];
@@ -1315,8 +1608,8 @@ var OnePasswordKeyProvider = class _OnePasswordKeyProvider {
1315
1608
  keyPath: tempKeyPath,
1316
1609
  cleanup: async () => {
1317
1610
  try {
1318
- await fs2__namespace.unlink(tempKeyPath);
1319
- await fs2__namespace.rmdir(tempDir);
1611
+ await fs8__namespace.unlink(tempKeyPath);
1612
+ await fs8__namespace.rmdir(tempDir);
1320
1613
  } catch (cleanupError) {
1321
1614
  console.warn(
1322
1615
  `Warning: Failed to clean up temporary key file at ${tempKeyPath}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
@@ -1326,7 +1619,7 @@ var OnePasswordKeyProvider = class _OnePasswordKeyProvider {
1326
1619
  };
1327
1620
  } catch (error) {
1328
1621
  try {
1329
- await fs2__namespace.rm(tempDir, { recursive: true, force: true });
1622
+ await fs8__namespace.rm(tempDir, { recursive: true, force: true });
1330
1623
  } catch (cleanupError) {
1331
1624
  console.warn(
1332
1625
  `Warning: Failed to clean up temporary key directory at ${tempDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
@@ -1342,7 +1635,7 @@ var OnePasswordKeyProvider = class _OnePasswordKeyProvider {
1342
1635
  */
1343
1636
  async generateKeyPair(options) {
1344
1637
  const { publicKeyPath, force = false } = options;
1345
- const tempDir = await fs2__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-keygen-"));
1638
+ const tempDir = await fs8__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-keygen-"));
1346
1639
  const tempPrivateKeyPath = path2__namespace.join(tempDir, "private.pem");
1347
1640
  try {
1348
1641
  await generateKeyPair({
@@ -1363,8 +1656,8 @@ var OnePasswordKeyProvider = class _OnePasswordKeyProvider {
1363
1656
  args.push("--account", this.account);
1364
1657
  }
1365
1658
  await execCommand("op", args);
1366
- await fs2__namespace.unlink(tempPrivateKeyPath);
1367
- await fs2__namespace.rmdir(tempDir);
1659
+ await fs8__namespace.unlink(tempPrivateKeyPath);
1660
+ await fs8__namespace.rmdir(tempDir);
1368
1661
  return {
1369
1662
  privateKeyRef: this.itemName,
1370
1663
  publicKeyPath,
@@ -1372,7 +1665,7 @@ var OnePasswordKeyProvider = class _OnePasswordKeyProvider {
1372
1665
  };
1373
1666
  } catch (error) {
1374
1667
  try {
1375
- await fs2__namespace.rm(tempDir, { recursive: true, force: true });
1668
+ await fs8__namespace.rm(tempDir, { recursive: true, force: true });
1376
1669
  } catch (cleanupError) {
1377
1670
  console.warn(
1378
1671
  `Warning: Failed to clean up temporary key directory at ${tempDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
@@ -1396,7 +1689,7 @@ var OnePasswordKeyProvider = class _OnePasswordKeyProvider {
1396
1689
  }
1397
1690
  };
1398
1691
  async function execCommand(command, args) {
1399
- return new Promise((resolve3, reject) => {
1692
+ return new Promise((resolve4, reject) => {
1400
1693
  const proc = child_process.spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
1401
1694
  let stdout = "";
1402
1695
  let stderr = "";
@@ -1408,7 +1701,7 @@ async function execCommand(command, args) {
1408
1701
  });
1409
1702
  proc.on("close", (code) => {
1410
1703
  if (code === 0) {
1411
- resolve3(stdout.trim());
1704
+ resolve4(stdout.trim());
1412
1705
  } else {
1413
1706
  reject(new Error(`Command failed with exit code ${String(code)}: ${stderr}`));
1414
1707
  }
@@ -1425,6 +1718,7 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
1425
1718
  type = "macos-keychain";
1426
1719
  displayName = "macOS Keychain";
1427
1720
  itemName;
1721
+ keychain;
1428
1722
  static ACCOUNT = "attest-it";
1429
1723
  /**
1430
1724
  * Create a new MacOSKeychainKeyProvider.
@@ -1432,6 +1726,9 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
1432
1726
  */
1433
1727
  constructor(options) {
1434
1728
  this.itemName = options.itemName;
1729
+ if (options.keychain !== void 0) {
1730
+ this.keychain = options.keychain;
1731
+ }
1435
1732
  }
1436
1733
  /**
1437
1734
  * Check if this provider is available.
@@ -1440,6 +1737,32 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
1440
1737
  static isAvailable() {
1441
1738
  return process.platform === "darwin";
1442
1739
  }
1740
+ /**
1741
+ * List available keychains on the system.
1742
+ * @returns Array of keychain information
1743
+ */
1744
+ static async listKeychains() {
1745
+ if (!_MacOSKeychainKeyProvider.isAvailable()) {
1746
+ return [];
1747
+ }
1748
+ try {
1749
+ const output = await execCommand2("security", ["list-keychains"]);
1750
+ const keychains = [];
1751
+ const lines = output.split("\n");
1752
+ for (const line of lines) {
1753
+ const match = /"(.+)"/.exec(line.trim());
1754
+ if (match?.[1]) {
1755
+ const fullPath = match[1];
1756
+ const filename = fullPath.split("/").pop() ?? fullPath;
1757
+ const name = filename.replace(/\.keychain(-db)?$/, "");
1758
+ keychains.push({ path: fullPath, name });
1759
+ }
1760
+ }
1761
+ return keychains;
1762
+ } catch {
1763
+ return [];
1764
+ }
1765
+ }
1443
1766
  /**
1444
1767
  * Check if this provider is available on the current system.
1445
1768
  */
@@ -1452,13 +1775,11 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
1452
1775
  */
1453
1776
  async keyExists(keyRef) {
1454
1777
  try {
1455
- await execCommand2("security", [
1456
- "find-generic-password",
1457
- "-a",
1458
- _MacOSKeychainKeyProvider.ACCOUNT,
1459
- "-s",
1460
- keyRef
1461
- ]);
1778
+ const args = ["find-generic-password", "-a", _MacOSKeychainKeyProvider.ACCOUNT, "-s", keyRef];
1779
+ if (this.keychain) {
1780
+ args.push(this.keychain);
1781
+ }
1782
+ await execCommand2("security", args);
1462
1783
  return true;
1463
1784
  } catch {
1464
1785
  return false;
@@ -1476,26 +1797,30 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
1476
1797
  `Key not found in macOS Keychain: "${keyRef}" (account: ${_MacOSKeychainKeyProvider.ACCOUNT})`
1477
1798
  );
1478
1799
  }
1479
- const tempDir = await fs2__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-"));
1800
+ const tempDir = await fs8__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-"));
1480
1801
  const tempKeyPath = path2__namespace.join(tempDir, "private.pem");
1481
1802
  try {
1482
- const base64Key = await execCommand2("security", [
1803
+ const findArgs = [
1483
1804
  "find-generic-password",
1484
1805
  "-a",
1485
1806
  _MacOSKeychainKeyProvider.ACCOUNT,
1486
1807
  "-s",
1487
1808
  keyRef,
1488
1809
  "-w"
1489
- ]);
1810
+ ];
1811
+ if (this.keychain) {
1812
+ findArgs.push(this.keychain);
1813
+ }
1814
+ const base64Key = await execCommand2("security", findArgs);
1490
1815
  const keyContent = Buffer.from(base64Key, "base64").toString("utf8");
1491
- await fs2__namespace.writeFile(tempKeyPath, keyContent, { mode: 384 });
1816
+ await fs8__namespace.writeFile(tempKeyPath, keyContent, { mode: 384 });
1492
1817
  await setKeyPermissions(tempKeyPath);
1493
1818
  return {
1494
1819
  keyPath: tempKeyPath,
1495
1820
  cleanup: async () => {
1496
1821
  try {
1497
- await fs2__namespace.unlink(tempKeyPath);
1498
- await fs2__namespace.rmdir(tempDir);
1822
+ await fs8__namespace.unlink(tempKeyPath);
1823
+ await fs8__namespace.rmdir(tempDir);
1499
1824
  } catch (cleanupError) {
1500
1825
  console.warn(
1501
1826
  `Warning: Failed to clean up temporary key file at ${tempKeyPath}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
@@ -1505,7 +1830,7 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
1505
1830
  };
1506
1831
  } catch (error) {
1507
1832
  try {
1508
- await fs2__namespace.rm(tempDir, { recursive: true, force: true });
1833
+ await fs8__namespace.rm(tempDir, { recursive: true, force: true });
1509
1834
  } catch (cleanupError) {
1510
1835
  console.warn(
1511
1836
  `Warning: Failed to clean up temporary key directory at ${tempDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
@@ -1521,7 +1846,7 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
1521
1846
  */
1522
1847
  async generateKeyPair(options) {
1523
1848
  const { publicKeyPath, force = false } = options;
1524
- const tempDir = await fs2__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-keygen-"));
1849
+ const tempDir = await fs8__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-keygen-"));
1525
1850
  const tempPrivateKeyPath = path2__namespace.join(tempDir, "private.pem");
1526
1851
  try {
1527
1852
  await generateKeyPair({
@@ -1529,9 +1854,9 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
1529
1854
  publicPath: publicKeyPath,
1530
1855
  force
1531
1856
  });
1532
- const privateKeyContent = await fs2__namespace.readFile(tempPrivateKeyPath, "utf8");
1857
+ const privateKeyContent = await fs8__namespace.readFile(tempPrivateKeyPath, "utf8");
1533
1858
  const base64Key = Buffer.from(privateKeyContent, "utf8").toString("base64");
1534
- await execCommand2("security", [
1859
+ const addArgs = [
1535
1860
  "add-generic-password",
1536
1861
  "-a",
1537
1862
  _MacOSKeychainKeyProvider.ACCOUNT,
@@ -1542,9 +1867,13 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
1542
1867
  "-T",
1543
1868
  "",
1544
1869
  "-U"
1545
- ]);
1546
- await fs2__namespace.unlink(tempPrivateKeyPath);
1547
- await fs2__namespace.rmdir(tempDir);
1870
+ ];
1871
+ if (this.keychain) {
1872
+ addArgs.push(this.keychain);
1873
+ }
1874
+ await execCommand2("security", addArgs);
1875
+ await fs8__namespace.unlink(tempPrivateKeyPath);
1876
+ await fs8__namespace.rmdir(tempDir);
1548
1877
  return {
1549
1878
  privateKeyRef: this.itemName,
1550
1879
  publicKeyPath,
@@ -1552,7 +1881,7 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
1552
1881
  };
1553
1882
  } catch (error) {
1554
1883
  try {
1555
- await fs2__namespace.rm(tempDir, { recursive: true, force: true });
1884
+ await fs8__namespace.rm(tempDir, { recursive: true, force: true });
1556
1885
  } catch (cleanupError) {
1557
1886
  console.warn(
1558
1887
  `Warning: Failed to clean up temporary key directory at ${tempDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
@@ -1574,7 +1903,7 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
1574
1903
  }
1575
1904
  };
1576
1905
  async function execCommand2(command, args) {
1577
- return new Promise((resolve3, reject) => {
1906
+ return new Promise((resolve4, reject) => {
1578
1907
  const proc = child_process.spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
1579
1908
  let stdout = "";
1580
1909
  let stderr = "";
@@ -1586,7 +1915,7 @@ async function execCommand2(command, args) {
1586
1915
  });
1587
1916
  proc.on("close", (code) => {
1588
1917
  if (code === 0) {
1589
- resolve3(stdout.trim());
1918
+ resolve4(stdout.trim());
1590
1919
  } else {
1591
1920
  reject(new Error(`Command failed with exit code ${String(code)}: ${stderr}`));
1592
1921
  }
@@ -1597,68 +1926,15 @@ async function execCommand2(command, args) {
1597
1926
  });
1598
1927
  }
1599
1928
 
1600
- // src/key-provider/registry.ts
1601
- var KeyProviderRegistry = class {
1602
- static providers = /* @__PURE__ */ new Map();
1603
- /**
1604
- * Register a key provider factory.
1605
- * @param type - Provider type identifier
1606
- * @param factory - Factory function to create provider instances
1607
- */
1608
- static register(type, factory) {
1609
- this.providers.set(type, factory);
1610
- }
1611
- /**
1612
- * Create a key provider from configuration.
1613
- * @param config - Provider configuration
1614
- * @returns A key provider instance
1615
- * @throws Error if the provider type is not registered
1616
- */
1617
- static create(config) {
1618
- const factory = this.providers.get(config.type);
1619
- if (!factory) {
1620
- throw new Error(
1621
- `Unknown key provider type: ${config.type}. Available types: ${Array.from(this.providers.keys()).join(", ")}`
1622
- );
1623
- }
1624
- return factory(config);
1625
- }
1626
- /**
1627
- * Get all registered provider types.
1628
- * @returns Array of provider type identifiers
1629
- */
1630
- static getProviderTypes() {
1631
- return Array.from(this.providers.keys());
1632
- }
1633
- };
1634
- KeyProviderRegistry.register("filesystem", (config) => {
1635
- const privateKeyPath = typeof config.options.privateKeyPath === "string" ? config.options.privateKeyPath : void 0;
1636
- if (privateKeyPath !== void 0) {
1637
- return new FilesystemKeyProvider({ privateKeyPath });
1638
- }
1639
- return new FilesystemKeyProvider();
1640
- });
1641
- KeyProviderRegistry.register("1password", (config) => {
1642
- const { options } = config;
1643
- const account = typeof options.account === "string" ? options.account : void 0;
1644
- const vault = typeof options.vault === "string" ? options.vault : "";
1645
- const itemName = typeof options.itemName === "string" ? options.itemName : "";
1646
- if (!vault || !itemName) {
1647
- throw new Error("1Password provider requires vault and itemName options");
1648
- }
1649
- if (account !== void 0) {
1650
- return new OnePasswordKeyProvider({ account, vault, itemName });
1651
- }
1652
- return new OnePasswordKeyProvider({ vault, itemName });
1653
- });
1654
- KeyProviderRegistry.register("macos-keychain", (config) => {
1655
- const { options } = config;
1656
- const itemName = typeof options.itemName === "string" ? options.itemName : "";
1657
- if (!itemName) {
1658
- throw new Error("macOS Keychain provider requires itemName option");
1659
- }
1660
- return new MacOSKeychainKeyProvider({ itemName });
1661
- });
1929
+ // src/key-provider/yubikey-provider.ts
1930
+ init_crypto();
1931
+ var homeDirOverride = null;
1932
+ function setAttestItHomeDir(dir) {
1933
+ homeDirOverride = dir;
1934
+ }
1935
+ function getAttestItHomeDir() {
1936
+ return homeDirOverride;
1937
+ }
1662
1938
  var privateKeyRefSchema = zod.z.discriminatedUnion("type", [
1663
1939
  zod.z.object({
1664
1940
  type: zod.z.literal("file"),
@@ -1667,7 +1943,8 @@ var privateKeyRefSchema = zod.z.discriminatedUnion("type", [
1667
1943
  zod.z.object({
1668
1944
  type: zod.z.literal("keychain"),
1669
1945
  service: zod.z.string().min(1, "Service name cannot be empty"),
1670
- account: zod.z.string().min(1, "Account name cannot be empty")
1946
+ account: zod.z.string().min(1, "Account name cannot be empty"),
1947
+ keychain: zod.z.string().optional()
1671
1948
  }),
1672
1949
  zod.z.object({
1673
1950
  type: zod.z.literal("1password"),
@@ -1698,9 +1975,18 @@ var LocalConfigValidationError = class extends Error {
1698
1975
  }
1699
1976
  };
1700
1977
  function getLocalConfigPath() {
1978
+ if (homeDirOverride) {
1979
+ return path2.join(homeDirOverride, "config.yaml");
1980
+ }
1701
1981
  const home = os.homedir();
1702
1982
  return path2.join(home, ".config", "attest-it", "config.yaml");
1703
1983
  }
1984
+ function getAttestItConfigDir() {
1985
+ if (homeDirOverride) {
1986
+ return homeDirOverride;
1987
+ }
1988
+ return path2.join(os.homedir(), ".config", "attest-it");
1989
+ }
1704
1990
  function parseLocalConfigContent(content) {
1705
1991
  let rawConfig;
1706
1992
  try {
@@ -1731,6 +2017,15 @@ function parseLocalConfigContent(content) {
1731
2017
  },
1732
2018
  ...identity.privateKey.field !== void 0 && { field: identity.privateKey.field }
1733
2019
  };
2020
+ } else if (identity.privateKey.type === "keychain") {
2021
+ privateKey = {
2022
+ type: "keychain",
2023
+ service: identity.privateKey.service,
2024
+ account: identity.privateKey.account,
2025
+ ...identity.privateKey.keychain !== void 0 && {
2026
+ keychain: identity.privateKey.keychain
2027
+ }
2028
+ };
1734
2029
  } else {
1735
2030
  privateKey = identity.privateKey;
1736
2031
  }
@@ -1754,7 +2049,7 @@ function parseLocalConfigContent(content) {
1754
2049
  async function loadLocalConfig(configPath) {
1755
2050
  const resolvedPath = configPath ?? getLocalConfigPath();
1756
2051
  try {
1757
- const content = await fs2.readFile(resolvedPath, "utf8");
2052
+ const content = await fs8.readFile(resolvedPath, "utf8");
1758
2053
  return parseLocalConfigContent(content);
1759
2054
  } catch (error) {
1760
2055
  if (error instanceof LocalConfigValidationError) {
@@ -1785,8 +2080,8 @@ async function saveLocalConfig(config, configPath) {
1785
2080
  const resolvedPath = configPath ?? getLocalConfigPath();
1786
2081
  const content = yaml.stringify(config);
1787
2082
  const dir = path2.dirname(resolvedPath);
1788
- await fs2.mkdir(dir, { recursive: true });
1789
- await fs2.writeFile(resolvedPath, content, "utf8");
2083
+ await fs8.mkdir(dir, { recursive: true });
2084
+ await fs8.writeFile(resolvedPath, content, "utf8");
1790
2085
  }
1791
2086
  function saveLocalConfigSync(config, configPath) {
1792
2087
  const resolvedPath = configPath ?? getLocalConfigPath();
@@ -1798,6 +2093,617 @@ function saveLocalConfigSync(config, configPath) {
1798
2093
  function getActiveIdentity(config) {
1799
2094
  return config.identities[config.activeIdentity];
1800
2095
  }
2096
+
2097
+ // src/key-provider/yubikey-provider.ts
2098
+ var EncryptedKeyFileSchema = zod.z.object({
2099
+ version: zod.z.literal(1),
2100
+ iv: zod.z.string().min(1),
2101
+ authTag: zod.z.string().min(1),
2102
+ salt: zod.z.string().min(1),
2103
+ challenge: zod.z.string().min(1),
2104
+ ciphertext: zod.z.string().min(1),
2105
+ slot: zod.z.union([zod.z.literal(1), zod.z.literal(2)]),
2106
+ serial: zod.z.string().optional(),
2107
+ aad: zod.z.string().optional()
2108
+ });
2109
+ var activeCleanupHandlers = /* @__PURE__ */ new Set();
2110
+ var processHandlersInstalled = false;
2111
+ function installProcessHandlers() {
2112
+ if (processHandlersInstalled) return;
2113
+ processHandlersInstalled = true;
2114
+ const runCleanup = async () => {
2115
+ const handlers = Array.from(activeCleanupHandlers);
2116
+ await Promise.allSettled(handlers.map((h) => h()));
2117
+ };
2118
+ process.once("beforeExit", () => {
2119
+ void runCleanup();
2120
+ });
2121
+ process.once("SIGINT", () => {
2122
+ void runCleanup().finally(() => process.exit(130));
2123
+ });
2124
+ process.once("SIGTERM", () => {
2125
+ void runCleanup().finally(() => process.exit(143));
2126
+ });
2127
+ }
2128
+ function validateEncryptedKeyFile(data) {
2129
+ const parsed = EncryptedKeyFileSchema.parse(data);
2130
+ const iv = Buffer.from(parsed.iv, "base64");
2131
+ if (iv.length !== 12) {
2132
+ throw new Error(`Invalid IV size: expected 12 bytes, got ${String(iv.length)}`);
2133
+ }
2134
+ const authTag = Buffer.from(parsed.authTag, "base64");
2135
+ if (authTag.length !== 16) {
2136
+ throw new Error(`Invalid auth tag size: expected 16 bytes, got ${String(authTag.length)}`);
2137
+ }
2138
+ const salt = Buffer.from(parsed.salt, "base64");
2139
+ if (salt.length !== 32) {
2140
+ throw new Error(`Invalid salt size: expected 32 bytes, got ${String(salt.length)}`);
2141
+ }
2142
+ const challenge = Buffer.from(parsed.challenge, "base64");
2143
+ if (challenge.length !== 32) {
2144
+ throw new Error(`Invalid challenge size: expected 32 bytes, got ${String(challenge.length)}`);
2145
+ }
2146
+ return parsed;
2147
+ }
2148
+ function constructAAD(version2, slot, serial) {
2149
+ const aadObject = {
2150
+ version: version2,
2151
+ slot,
2152
+ serial: serial ?? "unspecified"
2153
+ };
2154
+ return Buffer.from(JSON.stringify(aadObject), "utf8");
2155
+ }
2156
+ var YubiKeyProvider = class _YubiKeyProvider {
2157
+ type = "yubikey";
2158
+ displayName = "YubiKey";
2159
+ encryptedKeyPath;
2160
+ slot;
2161
+ serial;
2162
+ /**
2163
+ * Create a new YubiKeyProvider.
2164
+ * @param options - Provider options
2165
+ * @throws Error if encryptedKeyPath is outside the attest-it config directory
2166
+ */
2167
+ constructor(options) {
2168
+ const resolvedPath = path2__namespace.resolve(options.encryptedKeyPath);
2169
+ const configDir = getAttestItConfigDir();
2170
+ if (!resolvedPath.startsWith(configDir)) {
2171
+ throw new Error(
2172
+ `Encrypted key path must be within attest-it config directory (${configDir}). Got: ${resolvedPath}`
2173
+ );
2174
+ }
2175
+ this.encryptedKeyPath = resolvedPath;
2176
+ this.slot = options.slot ?? 2;
2177
+ if (options.serial !== void 0) {
2178
+ this.serial = options.serial;
2179
+ }
2180
+ }
2181
+ /**
2182
+ * Check if ykman CLI is installed and available.
2183
+ * @returns true if ykman is available
2184
+ */
2185
+ static async isInstalled() {
2186
+ try {
2187
+ await execCommand3("ykman", ["--version"]);
2188
+ return true;
2189
+ } catch {
2190
+ return false;
2191
+ }
2192
+ }
2193
+ /**
2194
+ * Check if any YubiKey is connected.
2195
+ * @returns true if at least one YubiKey is connected
2196
+ */
2197
+ static async isConnected() {
2198
+ try {
2199
+ const output = await execCommand3("ykman", ["list", "--serials"]);
2200
+ return output.trim().length > 0;
2201
+ } catch {
2202
+ return false;
2203
+ }
2204
+ }
2205
+ /**
2206
+ * Check if HMAC challenge-response is configured on a slot.
2207
+ * @param slot - Slot number (1 or 2)
2208
+ * @param serial - Optional YubiKey serial number
2209
+ * @returns true if challenge-response is configured
2210
+ */
2211
+ static async isChallengeResponseConfigured(slot = 2, serial) {
2212
+ try {
2213
+ const args = ["otp", "info"];
2214
+ if (serial) {
2215
+ args.unshift("--device", serial);
2216
+ }
2217
+ const output = await execCommand3("ykman", args);
2218
+ const slotPattern = new RegExp(`Slot ${String(slot)}:\\s+programmed.*challenge-response`, "i");
2219
+ return slotPattern.test(output);
2220
+ } catch {
2221
+ return false;
2222
+ }
2223
+ }
2224
+ /**
2225
+ * List connected YubiKeys.
2226
+ * @returns Array of YubiKey information
2227
+ */
2228
+ static async listDevices() {
2229
+ if (!await _YubiKeyProvider.isInstalled()) {
2230
+ return [];
2231
+ }
2232
+ try {
2233
+ const output = await execCommand3("ykman", ["list", "--serials"]);
2234
+ const serials = output.trim().split("\n").filter((s) => s.length > 0);
2235
+ const devices = [];
2236
+ for (const serial of serials) {
2237
+ try {
2238
+ const infoOutput = await execCommand3("ykman", ["--device", serial, "info"]);
2239
+ const typeMatch = /Device type:\s+(.+)/i.exec(infoOutput);
2240
+ const fwMatch = /Firmware version:\s+(.+)/i.exec(infoOutput);
2241
+ devices.push({
2242
+ serial,
2243
+ type: typeMatch?.[1]?.trim() ?? "YubiKey",
2244
+ firmware: fwMatch?.[1]?.trim() ?? "Unknown"
2245
+ });
2246
+ } catch {
2247
+ devices.push({
2248
+ serial,
2249
+ type: "YubiKey",
2250
+ firmware: "Unknown"
2251
+ });
2252
+ }
2253
+ }
2254
+ return devices;
2255
+ } catch {
2256
+ return [];
2257
+ }
2258
+ }
2259
+ /**
2260
+ * Check if this provider is available on the current system.
2261
+ * Requires ykman to be installed.
2262
+ */
2263
+ async isAvailable() {
2264
+ return _YubiKeyProvider.isInstalled();
2265
+ }
2266
+ /**
2267
+ * Check if an encrypted key file exists.
2268
+ * @param keyRef - Path to encrypted key file
2269
+ */
2270
+ async keyExists(keyRef) {
2271
+ try {
2272
+ await fs8__namespace.access(keyRef);
2273
+ return true;
2274
+ } catch {
2275
+ return false;
2276
+ }
2277
+ }
2278
+ /**
2279
+ * Get the private key by decrypting with YubiKey.
2280
+ * Downloads to a temporary file and returns a cleanup function.
2281
+ *
2282
+ * **Important**: Always call the cleanup function when done to securely delete
2283
+ * the temporary key file. The cleanup is also registered for process exit handlers.
2284
+ *
2285
+ * @param keyRef - Path to encrypted key file
2286
+ * @throws Error if the key cannot be decrypted
2287
+ */
2288
+ async getPrivateKey(keyRef) {
2289
+ installProcessHandlers();
2290
+ if (!await this.keyExists(keyRef)) {
2291
+ throw new Error(`Encrypted key file not found: ${keyRef}`);
2292
+ }
2293
+ const encryptedData = await fs8__namespace.readFile(keyRef, "utf8");
2294
+ let keyFile;
2295
+ try {
2296
+ const parsed = JSON.parse(encryptedData);
2297
+ keyFile = validateEncryptedKeyFile(parsed);
2298
+ } catch (err) {
2299
+ if (err instanceof zod.z.ZodError) {
2300
+ throw new Error(
2301
+ `Invalid encrypted key file format: ${err.errors.map((e) => e.message).join(", ")}`
2302
+ );
2303
+ }
2304
+ throw new Error(`Invalid encrypted key file: malformed JSON or structure`);
2305
+ }
2306
+ const expectedSerial = this.serial ?? keyFile.serial;
2307
+ if (!expectedSerial) {
2308
+ console.warn(
2309
+ "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."
2310
+ );
2311
+ }
2312
+ if (expectedSerial) {
2313
+ const devices = await _YubiKeyProvider.listDevices();
2314
+ const matchingDevice = devices.find((d) => d.serial === expectedSerial);
2315
+ if (!matchingDevice) {
2316
+ throw new Error(
2317
+ `Required YubiKey not found. Expected serial: ${expectedSerial}. Connected devices: ${devices.map((d) => d.serial).join(", ") || "none"}`
2318
+ );
2319
+ }
2320
+ }
2321
+ const challenge = Buffer.from(keyFile.challenge, "base64");
2322
+ const response = await performChallengeResponse(challenge, keyFile.slot, expectedSerial);
2323
+ const salt = Buffer.from(keyFile.salt, "base64");
2324
+ const aesKey = deriveKey(response, salt);
2325
+ const iv = Buffer.from(keyFile.iv, "base64");
2326
+ const authTag = Buffer.from(keyFile.authTag, "base64");
2327
+ const ciphertext = Buffer.from(keyFile.ciphertext, "base64");
2328
+ let privateKeyContent;
2329
+ try {
2330
+ const decipher = crypto3__namespace.createDecipheriv("aes-256-gcm", aesKey, iv);
2331
+ if (keyFile.aad) {
2332
+ decipher.setAAD(Buffer.from(keyFile.aad, "base64"));
2333
+ }
2334
+ decipher.setAuthTag(authTag);
2335
+ const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
2336
+ privateKeyContent = decrypted.toString("utf8");
2337
+ } catch {
2338
+ throw new Error(
2339
+ "Failed to decrypt private key. Verify you are using the correct YubiKey and the encrypted key file has not been corrupted or tampered with."
2340
+ );
2341
+ }
2342
+ const tempDir = await fs8__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-"));
2343
+ const tempKeyPath = path2__namespace.join(tempDir, "private.pem");
2344
+ const cleanup = async () => {
2345
+ activeCleanupHandlers.delete(cleanup);
2346
+ try {
2347
+ const keySize = Buffer.byteLength(privateKeyContent);
2348
+ await fs8__namespace.writeFile(tempKeyPath, crypto3__namespace.randomBytes(keySize));
2349
+ await fs8__namespace.unlink(tempKeyPath);
2350
+ await fs8__namespace.rmdir(tempDir);
2351
+ } catch {
2352
+ }
2353
+ };
2354
+ try {
2355
+ await fs8__namespace.writeFile(tempKeyPath, privateKeyContent, { mode: 384 });
2356
+ await setKeyPermissions(tempKeyPath);
2357
+ activeCleanupHandlers.add(cleanup);
2358
+ return {
2359
+ keyPath: tempKeyPath,
2360
+ cleanup
2361
+ };
2362
+ } catch (error) {
2363
+ await cleanup();
2364
+ throw error;
2365
+ }
2366
+ }
2367
+ /**
2368
+ * Generate a new keypair and store encrypted with YubiKey.
2369
+ * Public key is written to filesystem for repository commit.
2370
+ *
2371
+ * **Security Note**: Always specify a serial number to bind the key to a specific YubiKey.
2372
+ *
2373
+ * @param options - Key generation options
2374
+ */
2375
+ async generateKeyPair(options) {
2376
+ const { publicKeyPath, force = false } = options;
2377
+ if (!await _YubiKeyProvider.isChallengeResponseConfigured(this.slot, this.serial)) {
2378
+ throw new Error(
2379
+ `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.`
2380
+ );
2381
+ }
2382
+ if (!force && await this.keyExists(this.encryptedKeyPath)) {
2383
+ throw new Error(
2384
+ `Encrypted key file already exists: ${this.encryptedKeyPath}. Use force: true to overwrite.`
2385
+ );
2386
+ }
2387
+ let serial;
2388
+ if (this.serial) {
2389
+ serial = this.serial;
2390
+ } else {
2391
+ const devices = await _YubiKeyProvider.listDevices();
2392
+ if (devices.length === 1 && devices[0]) {
2393
+ serial = devices[0].serial;
2394
+ } else if (devices.length > 1) {
2395
+ console.warn(
2396
+ "WARNING: Multiple YubiKeys detected but no serial specified. Key will not be bound to a specific device. For better security, specify a serial number."
2397
+ );
2398
+ }
2399
+ }
2400
+ const tempDir = await fs8__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-keygen-"));
2401
+ const tempPrivateKeyPath = path2__namespace.join(tempDir, "private.pem");
2402
+ try {
2403
+ await generateKeyPair({
2404
+ privatePath: tempPrivateKeyPath,
2405
+ publicPath: publicKeyPath,
2406
+ force
2407
+ });
2408
+ const privateKeyContent = await fs8__namespace.readFile(tempPrivateKeyPath, "utf8");
2409
+ const challenge = crypto3__namespace.randomBytes(32);
2410
+ const salt = crypto3__namespace.randomBytes(32);
2411
+ const iv = crypto3__namespace.randomBytes(12);
2412
+ const response = await performChallengeResponse(challenge, this.slot, this.serial);
2413
+ const aesKey = deriveKey(response, salt);
2414
+ const aad = constructAAD(1, this.slot, serial);
2415
+ const cipher = crypto3__namespace.createCipheriv("aes-256-gcm", aesKey, iv);
2416
+ cipher.setAAD(aad);
2417
+ const ciphertext = Buffer.concat([
2418
+ cipher.update(Buffer.from(privateKeyContent, "utf8")),
2419
+ cipher.final()
2420
+ ]);
2421
+ const authTag = cipher.getAuthTag();
2422
+ const keyFile = {
2423
+ version: 1,
2424
+ iv: iv.toString("base64"),
2425
+ authTag: authTag.toString("base64"),
2426
+ salt: salt.toString("base64"),
2427
+ challenge: challenge.toString("base64"),
2428
+ ciphertext: ciphertext.toString("base64"),
2429
+ slot: this.slot,
2430
+ aad: aad.toString("base64"),
2431
+ ...serial && { serial }
2432
+ };
2433
+ await fs8__namespace.mkdir(path2__namespace.dirname(this.encryptedKeyPath), { recursive: true });
2434
+ await fs8__namespace.writeFile(this.encryptedKeyPath, JSON.stringify(keyFile, null, 2), { mode: 384 });
2435
+ await setKeyPermissions(this.encryptedKeyPath);
2436
+ const keySize = Buffer.byteLength(privateKeyContent);
2437
+ await fs8__namespace.writeFile(tempPrivateKeyPath, crypto3__namespace.randomBytes(keySize));
2438
+ await fs8__namespace.unlink(tempPrivateKeyPath);
2439
+ await fs8__namespace.rmdir(tempDir);
2440
+ return {
2441
+ privateKeyRef: this.encryptedKeyPath,
2442
+ publicKeyPath,
2443
+ storageDescription: `YubiKey-encrypted: ${this.encryptedKeyPath}`
2444
+ };
2445
+ } catch (error) {
2446
+ try {
2447
+ await fs8__namespace.rm(tempDir, { recursive: true, force: true });
2448
+ } catch {
2449
+ }
2450
+ throw error;
2451
+ }
2452
+ }
2453
+ /**
2454
+ * Encrypt an existing private key with YubiKey challenge-response.
2455
+ *
2456
+ * @remarks
2457
+ * This static method allows encrypting a private key that was generated
2458
+ * elsewhere (e.g., by the CLI) without having to create a provider instance first.
2459
+ *
2460
+ * **Security Note**: Always specify a serial number to bind the key to a specific YubiKey.
2461
+ * The serial provides defense-in-depth by ensuring only the intended YubiKey can decrypt.
2462
+ *
2463
+ * @param options - Encryption options
2464
+ * @returns Path to the encrypted key file and storage description
2465
+ * @public
2466
+ */
2467
+ static async encryptPrivateKey(options) {
2468
+ const { privateKey, encryptedKeyPath, slot = 2, serial } = options;
2469
+ const resolvedPath = path2__namespace.resolve(encryptedKeyPath);
2470
+ const configDir = getAttestItConfigDir();
2471
+ if (!resolvedPath.startsWith(configDir)) {
2472
+ throw new Error(
2473
+ `Encrypted key path must be within attest-it config directory (${configDir}). Got: ${resolvedPath}`
2474
+ );
2475
+ }
2476
+ if (!serial) {
2477
+ console.warn(
2478
+ "WARNING: No YubiKey serial number specified. Key will not be bound to a specific device. For better security, specify a serial number."
2479
+ );
2480
+ }
2481
+ if (!await _YubiKeyProvider.isChallengeResponseConfigured(slot, serial)) {
2482
+ throw new Error(
2483
+ `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.`
2484
+ );
2485
+ }
2486
+ const challenge = crypto3__namespace.randomBytes(32);
2487
+ const salt = crypto3__namespace.randomBytes(32);
2488
+ const iv = crypto3__namespace.randomBytes(12);
2489
+ const response = await performChallengeResponse(challenge, slot, serial);
2490
+ const aesKey = deriveKey(response, salt);
2491
+ const aad = constructAAD(1, slot, serial);
2492
+ const cipher = crypto3__namespace.createCipheriv("aes-256-gcm", aesKey, iv);
2493
+ cipher.setAAD(aad);
2494
+ const ciphertext = Buffer.concat([
2495
+ cipher.update(Buffer.from(privateKey, "utf8")),
2496
+ cipher.final()
2497
+ ]);
2498
+ const authTag = cipher.getAuthTag();
2499
+ const keyFile = {
2500
+ version: 1,
2501
+ iv: iv.toString("base64"),
2502
+ authTag: authTag.toString("base64"),
2503
+ salt: salt.toString("base64"),
2504
+ challenge: challenge.toString("base64"),
2505
+ ciphertext: ciphertext.toString("base64"),
2506
+ slot,
2507
+ aad: aad.toString("base64"),
2508
+ ...serial && { serial }
2509
+ };
2510
+ await fs8__namespace.mkdir(path2__namespace.dirname(resolvedPath), { recursive: true });
2511
+ await fs8__namespace.writeFile(resolvedPath, JSON.stringify(keyFile, null, 2), { mode: 384 });
2512
+ await setKeyPermissions(resolvedPath);
2513
+ return {
2514
+ encryptedKeyPath: resolvedPath,
2515
+ storageDescription: `YubiKey-encrypted: ${resolvedPath}`
2516
+ };
2517
+ }
2518
+ /**
2519
+ * Get the configuration for this provider.
2520
+ */
2521
+ getConfig() {
2522
+ return {
2523
+ type: this.type,
2524
+ options: {
2525
+ encryptedKeyPath: this.encryptedKeyPath,
2526
+ slot: this.slot,
2527
+ ...this.serial && { serial: this.serial }
2528
+ }
2529
+ };
2530
+ }
2531
+ };
2532
+ async function execCommand3(command, args) {
2533
+ return new Promise((resolve4, reject) => {
2534
+ const proc = child_process.spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
2535
+ let stdout = "";
2536
+ let stderr = "";
2537
+ proc.stdout.on("data", (data) => {
2538
+ stdout += data.toString();
2539
+ });
2540
+ proc.stderr.on("data", (data) => {
2541
+ stderr += data.toString();
2542
+ });
2543
+ proc.on("close", (code) => {
2544
+ if (code === 0) {
2545
+ resolve4(stdout.trim());
2546
+ } else {
2547
+ reject(new Error(`Command failed with exit code ${String(code)}: ${stderr}`));
2548
+ }
2549
+ });
2550
+ proc.on("error", (error) => {
2551
+ reject(error);
2552
+ });
2553
+ });
2554
+ }
2555
+ async function performChallengeResponse(challenge, slot, serial) {
2556
+ const args = ["otp", "chalresp", "--slot", String(slot)];
2557
+ if (serial) {
2558
+ args.unshift("--device", serial);
2559
+ }
2560
+ args.push(challenge.toString("hex"));
2561
+ try {
2562
+ const output = await execCommand3("ykman", args);
2563
+ return Buffer.from(output.trim(), "hex");
2564
+ } catch {
2565
+ throw new Error(
2566
+ "YubiKey challenge-response failed. Verify your YubiKey is inserted and the slot is configured for challenge-response."
2567
+ );
2568
+ }
2569
+ }
2570
+ function deriveKey(response, salt) {
2571
+ const derived = crypto3__namespace.hkdfSync("sha256", response, salt, "attest-it-yubikey-v1", 32);
2572
+ return Buffer.from(derived);
2573
+ }
2574
+
2575
+ // src/key-provider/registry.ts
2576
+ var KeyProviderRegistry = class {
2577
+ static providers = /* @__PURE__ */ new Map();
2578
+ /**
2579
+ * Register a key provider factory.
2580
+ * @param type - Provider type identifier
2581
+ * @param factory - Factory function to create provider instances
2582
+ */
2583
+ static register(type, factory) {
2584
+ this.providers.set(type, factory);
2585
+ }
2586
+ /**
2587
+ * Create a key provider from configuration.
2588
+ * @param config - Provider configuration
2589
+ * @returns A key provider instance
2590
+ * @throws Error if the provider type is not registered
2591
+ */
2592
+ static create(config) {
2593
+ const factory = this.providers.get(config.type);
2594
+ if (!factory) {
2595
+ throw new Error(
2596
+ `Unknown key provider type: ${config.type}. Available types: ${Array.from(this.providers.keys()).join(", ")}`
2597
+ );
2598
+ }
2599
+ return factory(config);
2600
+ }
2601
+ /**
2602
+ * Get all registered provider types.
2603
+ * @returns Array of provider type identifiers
2604
+ */
2605
+ static getProviderTypes() {
2606
+ return Array.from(this.providers.keys());
2607
+ }
2608
+ };
2609
+ KeyProviderRegistry.register("filesystem", (config) => {
2610
+ const privateKeyPath = typeof config.options.privateKeyPath === "string" ? config.options.privateKeyPath : void 0;
2611
+ if (privateKeyPath !== void 0) {
2612
+ return new FilesystemKeyProvider({ privateKeyPath });
2613
+ }
2614
+ return new FilesystemKeyProvider();
2615
+ });
2616
+ KeyProviderRegistry.register("1password", (config) => {
2617
+ const { options } = config;
2618
+ const account = typeof options.account === "string" ? options.account : void 0;
2619
+ const vault = typeof options.vault === "string" ? options.vault : "";
2620
+ const itemName = typeof options.itemName === "string" ? options.itemName : "";
2621
+ if (!vault || !itemName) {
2622
+ throw new Error("1Password provider requires vault and itemName options");
2623
+ }
2624
+ if (account !== void 0) {
2625
+ return new OnePasswordKeyProvider({ account, vault, itemName });
2626
+ }
2627
+ return new OnePasswordKeyProvider({ vault, itemName });
2628
+ });
2629
+ KeyProviderRegistry.register("macos-keychain", (config) => {
2630
+ const { options } = config;
2631
+ const itemName = typeof options.itemName === "string" ? options.itemName : "";
2632
+ if (!itemName) {
2633
+ throw new Error("macOS Keychain provider requires itemName option");
2634
+ }
2635
+ return new MacOSKeychainKeyProvider({ itemName });
2636
+ });
2637
+ KeyProviderRegistry.register("yubikey", (config) => {
2638
+ const { options } = config;
2639
+ const encryptedKeyPath = typeof options.encryptedKeyPath === "string" ? options.encryptedKeyPath : "";
2640
+ if (!encryptedKeyPath) {
2641
+ throw new Error("YubiKey provider requires encryptedKeyPath option");
2642
+ }
2643
+ const slot = typeof options.slot === "number" && (options.slot === 1 || options.slot === 2) ? options.slot : void 0;
2644
+ const serial = typeof options.serial === "string" ? options.serial : void 0;
2645
+ const providerOptions = {
2646
+ encryptedKeyPath
2647
+ };
2648
+ if (slot !== void 0) {
2649
+ providerOptions.slot = slot;
2650
+ }
2651
+ if (serial !== void 0) {
2652
+ providerOptions.serial = serial;
2653
+ }
2654
+ return new YubiKeyProvider(providerOptions);
2655
+ });
2656
+ var cliExperienceSchema = zod.z.object({
2657
+ declinedCompletionInstall: zod.z.boolean().optional()
2658
+ }).strict();
2659
+ var userPreferencesSchema = zod.z.object({
2660
+ cliExperience: cliExperienceSchema.optional()
2661
+ }).strict();
2662
+ function getPreferencesPath() {
2663
+ return path2.join(getAttestItConfigDir(), "preferences.yaml");
2664
+ }
2665
+ async function loadPreferences() {
2666
+ const prefsPath = getPreferencesPath();
2667
+ try {
2668
+ const content = await fs8.readFile(prefsPath, "utf8");
2669
+ const parsed = yaml.parse(content);
2670
+ const result = userPreferencesSchema.safeParse(parsed);
2671
+ if (result.success) {
2672
+ const prefs = {};
2673
+ if (result.data.cliExperience) {
2674
+ prefs.cliExperience = {
2675
+ ...result.data.cliExperience.declinedCompletionInstall !== void 0 && {
2676
+ declinedCompletionInstall: result.data.cliExperience.declinedCompletionInstall
2677
+ }
2678
+ };
2679
+ }
2680
+ return prefs;
2681
+ }
2682
+ console.warn("Invalid preferences file, using defaults:", result.error.message);
2683
+ return {};
2684
+ } catch (error) {
2685
+ if (error && typeof error === "object" && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
2686
+ return {};
2687
+ }
2688
+ throw error;
2689
+ }
2690
+ }
2691
+ async function savePreferences(preferences) {
2692
+ const prefsPath = getPreferencesPath();
2693
+ const content = yaml.stringify(preferences);
2694
+ const dir = path2.dirname(prefsPath);
2695
+ await fs8.mkdir(dir, { recursive: true });
2696
+ await fs8.writeFile(prefsPath, content, "utf8");
2697
+ }
2698
+ async function setPreference(key, value) {
2699
+ const prefs = await loadPreferences();
2700
+ prefs[key] = value;
2701
+ await savePreferences(prefs);
2702
+ }
2703
+ async function getPreference(key) {
2704
+ const prefs = await loadPreferences();
2705
+ return prefs[key];
2706
+ }
1801
2707
  function isAuthorizedSigner(config, gateId, publicKey) {
1802
2708
  const gate = config.gates?.[gateId];
1803
2709
  if (!gate) {
@@ -2121,7 +3027,10 @@ exports.KeyProviderRegistry = KeyProviderRegistry;
2121
3027
  exports.LocalConfigValidationError = LocalConfigValidationError;
2122
3028
  exports.MacOSKeychainKeyProvider = MacOSKeychainKeyProvider;
2123
3029
  exports.OnePasswordKeyProvider = OnePasswordKeyProvider;
3030
+ exports.OperationalValidationError = OperationalValidationError;
3031
+ exports.PolicyValidationError = PolicyValidationError;
2124
3032
  exports.SignatureInvalidError = SignatureInvalidError;
3033
+ exports.YubiKeyProvider = YubiKeyProvider;
2125
3034
  exports.canonicalizeAttestations = canonicalizeAttestations;
2126
3035
  exports.checkOpenSSL = checkOpenSSL;
2127
3036
  exports.computeFingerprint = computeFingerprint;
@@ -2134,11 +3043,15 @@ exports.findTeamMemberByPublicKey = findTeamMemberByPublicKey;
2134
3043
  exports.generateEd25519KeyPair = generateKeyPair2;
2135
3044
  exports.generateKeyPair = generateKeyPair;
2136
3045
  exports.getActiveIdentity = getActiveIdentity;
3046
+ exports.getAttestItConfigDir = getAttestItConfigDir;
3047
+ exports.getAttestItHomeDir = getAttestItHomeDir;
2137
3048
  exports.getAuthorizedSignersForGate = getAuthorizedSignersForGate;
2138
3049
  exports.getDefaultPrivateKeyPath = getDefaultPrivateKeyPath;
2139
3050
  exports.getDefaultPublicKeyPath = getDefaultPublicKeyPath;
2140
3051
  exports.getGate = getGate;
2141
3052
  exports.getLocalConfigPath = getLocalConfigPath;
3053
+ exports.getPreference = getPreference;
3054
+ exports.getPreferencesPath = getPreferencesPath;
2142
3055
  exports.getPublicKeyFromPrivate = getPublicKeyFromPrivate;
2143
3056
  exports.isAuthorizedSigner = isAuthorizedSigner;
2144
3057
  exports.listPackageFiles = listPackageFiles;
@@ -2146,7 +3059,13 @@ exports.loadConfig = loadConfig;
2146
3059
  exports.loadConfigSync = loadConfigSync;
2147
3060
  exports.loadLocalConfig = loadLocalConfig;
2148
3061
  exports.loadLocalConfigSync = loadLocalConfigSync;
3062
+ exports.loadPreferences = loadPreferences;
3063
+ exports.mergeConfigs = mergeConfigs;
3064
+ exports.operationalSchema = operationalSchema;
2149
3065
  exports.parseDuration = parseDuration;
3066
+ exports.parseOperationalContent = parseOperationalContent;
3067
+ exports.parsePolicyContent = parsePolicyContent;
3068
+ exports.policySchema = policySchema;
2150
3069
  exports.readAndVerifyAttestations = readAndVerifyAttestations;
2151
3070
  exports.readAttestations = readAttestations;
2152
3071
  exports.readAttestationsSync = readAttestationsSync;
@@ -2156,11 +3075,15 @@ exports.removeAttestation = removeAttestation;
2156
3075
  exports.resolveConfigPaths = resolveConfigPaths;
2157
3076
  exports.saveLocalConfig = saveLocalConfig;
2158
3077
  exports.saveLocalConfigSync = saveLocalConfigSync;
3078
+ exports.savePreferences = savePreferences;
3079
+ exports.setAttestItHomeDir = setAttestItHomeDir;
2159
3080
  exports.setKeyPermissions = setKeyPermissions;
3081
+ exports.setPreference = setPreference;
2160
3082
  exports.sign = sign;
2161
3083
  exports.signEd25519 = sign3;
2162
3084
  exports.toAttestItConfig = toAttestItConfig;
2163
3085
  exports.upsertAttestation = upsertAttestation;
3086
+ exports.validateSuiteGateReferences = validateSuiteGateReferences;
2164
3087
  exports.verify = verify;
2165
3088
  exports.verifyAllSeals = verifyAllSeals;
2166
3089
  exports.verifyAttestations = verifyAttestations;