@attest-it/core 0.2.0 → 0.5.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
@@ -1,35 +1,97 @@
1
- export { checkOpenSSL, generateKeyPair, getDefaultPrivateKeyPath, getDefaultPublicKeyPath, setKeyPermissions, sign, verify } from './chunk-CEE7ONNG.js';
1
+ import { getDefaultPrivateKeyPath, generateKeyPair, setKeyPermissions } from './chunk-VC3BBBBO.js';
2
+ export { checkOpenSSL, generateKeyPair, getDefaultPrivateKeyPath, getDefaultPublicKeyPath, setKeyPermissions, sign, verify } from './chunk-VC3BBBBO.js';
2
3
  import * as fs from 'fs';
3
- import { readFileSync } from 'fs';
4
- import { readFile } from 'fs/promises';
5
- import * as path from 'path';
6
- import { join, resolve } from 'path';
7
- import { parse } from 'yaml';
4
+ import { readFileSync, mkdirSync, writeFileSync } from 'fs';
5
+ import * as fs6 from 'fs/promises';
6
+ import { readFile, mkdir, writeFile } from 'fs/promises';
7
+ import * as path6 from 'path';
8
+ import { join, resolve, dirname } from 'path';
9
+ import ms from 'ms';
10
+ import { stringify, parse } from 'yaml';
8
11
  import { z } from 'zod';
9
- import * as crypto from 'crypto';
12
+ import * as crypto2 from 'crypto';
10
13
  import { glob, globSync } from 'tinyglobby';
11
- import * as os from 'os';
14
+ import * as os2 from 'os';
15
+ import { homedir } from 'os';
12
16
  import * as canonicalizeNamespace from 'canonicalize';
17
+ import { spawn } from 'child_process';
13
18
 
19
+ var keyProviderOptionsSchema = z.object({
20
+ privateKeyPath: z.string().optional(),
21
+ account: z.string().optional(),
22
+ vault: z.string().optional(),
23
+ itemName: z.string().optional()
24
+ }).strict();
25
+ var keyProviderSchema = z.object({
26
+ type: z.enum(["filesystem", "1password"]).or(z.string()),
27
+ options: keyProviderOptionsSchema.optional()
28
+ }).strict();
29
+ var teamMemberSchema = z.object({
30
+ name: z.string().min(1, "Team member name cannot be empty"),
31
+ email: z.string().email().optional(),
32
+ github: z.string().min(1).optional(),
33
+ publicKey: z.string().min(1, "Public key is required")
34
+ }).strict();
35
+ var fingerprintConfigSchema = z.object({
36
+ paths: z.array(z.string().min(1, "Path cannot be empty")).min(1, "At least one path is required"),
37
+ exclude: z.array(z.string().min(1, "Exclude pattern cannot be empty")).optional()
38
+ }).strict();
39
+ var durationSchema = z.string().refine(
40
+ (val) => {
41
+ try {
42
+ const parsed = ms(val);
43
+ return typeof parsed === "number" && parsed > 0;
44
+ } catch {
45
+ return false;
46
+ }
47
+ },
48
+ {
49
+ message: 'Duration must be a valid duration string (e.g., "30d", "7d", "24h")'
50
+ }
51
+ );
52
+ var gateSchema = z.object({
53
+ name: z.string().min(1, "Gate name cannot be empty"),
54
+ description: z.string().min(1, "Gate description cannot be empty"),
55
+ authorizedSigners: z.array(z.string().min(1, "Authorized signer slug cannot be empty")).min(1, "At least one authorized signer is required"),
56
+ fingerprint: fingerprintConfigSchema,
57
+ maxAge: durationSchema
58
+ }).strict();
14
59
  var settingsSchema = z.object({
15
60
  maxAgeDays: z.number().int().positive().default(30),
16
61
  publicKeyPath: z.string().default(".attest-it/pubkey.pem"),
17
62
  attestationsPath: z.string().default(".attest-it/attestations.json"),
18
- defaultCommand: z.string().optional()
63
+ defaultCommand: z.string().optional(),
64
+ keyProvider: keyProviderSchema.optional()
19
65
  // Note: algorithm field was removed - RSA is the only supported algorithm
20
66
  }).passthrough();
21
67
  var suiteSchema = z.object({
68
+ // Gate fields (if present, this suite references a gate)
69
+ gate: z.string().optional(),
70
+ // Legacy fingerprint definition (for backward compatibility)
22
71
  description: z.string().optional(),
23
- packages: z.array(z.string().min(1, "Package path cannot be empty")).min(1, "At least one package pattern is required"),
72
+ packages: z.array(z.string().min(1, "Package path cannot be empty")).optional(),
24
73
  files: z.array(z.string().min(1, "File path cannot be empty")).optional(),
25
74
  ignore: z.array(z.string().min(1, "Ignore pattern cannot be empty")).optional(),
75
+ // CLI-specific fields
26
76
  command: z.string().optional(),
77
+ timeout: z.string().optional(),
78
+ interactive: z.boolean().optional(),
79
+ // Relationship fields
27
80
  invalidates: z.array(z.string().min(1, "Invalidated suite name cannot be empty")).optional(),
28
81
  depends_on: z.array(z.string().min(1, "Dependency suite name cannot be empty")).optional()
29
- }).strict();
82
+ }).strict().refine(
83
+ (suite) => {
84
+ return suite.gate !== void 0 || suite.packages !== void 0 && suite.packages.length > 0;
85
+ },
86
+ {
87
+ message: "Suite must either reference a gate or define packages for fingerprinting"
88
+ }
89
+ );
30
90
  var configSchema = z.object({
31
91
  version: z.literal(1),
32
92
  settings: settingsSchema.default({}),
93
+ team: z.record(z.string(), teamMemberSchema).optional(),
94
+ gates: z.record(z.string(), gateSchema).optional(),
33
95
  suites: z.record(z.string(), suiteSchema).refine((suites) => Object.keys(suites).length >= 1, {
34
96
  message: "At least one suite must be defined"
35
97
  }),
@@ -146,32 +208,52 @@ function resolveConfigPaths(config, repoRoot) {
146
208
  };
147
209
  }
148
210
  function toAttestItConfig(config) {
149
- return {
211
+ const result = {
150
212
  version: config.version,
151
213
  settings: {
152
214
  maxAgeDays: config.settings.maxAgeDays,
153
215
  publicKeyPath: config.settings.publicKeyPath,
154
- attestationsPath: config.settings.attestationsPath,
155
- ...config.settings.defaultCommand !== void 0 && {
156
- defaultCommand: config.settings.defaultCommand
157
- }
216
+ attestationsPath: config.settings.attestationsPath
158
217
  },
159
- suites: Object.fromEntries(
160
- Object.entries(config.suites).map(([name, suite]) => [
161
- name,
162
- {
163
- packages: suite.packages,
164
- ...suite.description !== void 0 && { description: suite.description },
165
- ...suite.files !== void 0 && { files: suite.files },
166
- ...suite.ignore !== void 0 && { ignore: suite.ignore },
167
- ...suite.command !== void 0 && { command: suite.command },
168
- ...suite.invalidates !== void 0 && { invalidates: suite.invalidates },
169
- ...suite.depends_on !== void 0 && { depends_on: suite.depends_on }
170
- }
171
- ])
172
- ),
173
- ...config.groups !== void 0 && { groups: config.groups }
218
+ suites: {}
174
219
  };
220
+ if (config.settings.defaultCommand !== void 0) {
221
+ result.settings.defaultCommand = config.settings.defaultCommand;
222
+ }
223
+ if (config.settings.keyProvider !== void 0) {
224
+ result.settings.keyProvider = {
225
+ type: config.settings.keyProvider.type,
226
+ ...config.settings.keyProvider.options !== void 0 && {
227
+ options: config.settings.keyProvider.options
228
+ }
229
+ };
230
+ }
231
+ if (config.team !== void 0) {
232
+ result.team = config.team;
233
+ }
234
+ if (config.gates !== void 0) {
235
+ result.gates = config.gates;
236
+ }
237
+ if (config.groups !== void 0) {
238
+ result.groups = config.groups;
239
+ }
240
+ result.suites = Object.fromEntries(
241
+ Object.entries(config.suites).map(([name, suite]) => {
242
+ const mappedSuite = {};
243
+ if (suite.gate !== void 0) mappedSuite.gate = suite.gate;
244
+ if (suite.packages !== void 0) mappedSuite.packages = suite.packages;
245
+ if (suite.description !== void 0) mappedSuite.description = suite.description;
246
+ if (suite.files !== void 0) mappedSuite.files = suite.files;
247
+ if (suite.ignore !== void 0) mappedSuite.ignore = suite.ignore;
248
+ if (suite.command !== void 0) mappedSuite.command = suite.command;
249
+ if (suite.timeout !== void 0) mappedSuite.timeout = suite.timeout;
250
+ if (suite.interactive !== void 0) mappedSuite.interactive = suite.interactive;
251
+ if (suite.invalidates !== void 0) mappedSuite.invalidates = suite.invalidates;
252
+ if (suite.depends_on !== void 0) mappedSuite.depends_on = suite.depends_on;
253
+ return [name, mappedSuite];
254
+ })
255
+ );
256
+ return result;
175
257
  }
176
258
  var LARGE_FILE_THRESHOLD = 50 * 1024 * 1024;
177
259
  function sortFiles(files) {
@@ -182,7 +264,7 @@ function sortFiles(files) {
182
264
  });
183
265
  }
184
266
  function normalizePath(filePath) {
185
- return filePath.split(path.sep).join("/");
267
+ return filePath.split(path6.sep).join("/");
186
268
  }
187
269
  function computeFinalFingerprint(fileHashes) {
188
270
  const sorted = [...fileHashes].sort((a, b) => {
@@ -192,15 +274,15 @@ function computeFinalFingerprint(fileHashes) {
192
274
  });
193
275
  const hashes = sorted.map((input) => input.hash);
194
276
  const concatenated = Buffer.concat(hashes);
195
- const finalHash = crypto.createHash("sha256").update(concatenated).digest();
277
+ const finalHash = crypto2.createHash("sha256").update(concatenated).digest();
196
278
  return `sha256:${finalHash.toString("hex")}`;
197
279
  }
198
280
  async function hashFileAsync(realPath, normalizedPath, stats) {
199
281
  if (stats.size > LARGE_FILE_THRESHOLD) {
200
282
  return new Promise((resolve3, reject) => {
201
- const hash2 = crypto.createHash("sha256");
283
+ const hash2 = crypto2.createHash("sha256");
202
284
  hash2.update(normalizedPath);
203
- hash2.update("\0");
285
+ hash2.update(":");
204
286
  const stream = fs.createReadStream(realPath);
205
287
  stream.on("data", (chunk) => {
206
288
  hash2.update(chunk);
@@ -212,17 +294,17 @@ async function hashFileAsync(realPath, normalizedPath, stats) {
212
294
  });
213
295
  }
214
296
  const content = await fs.promises.readFile(realPath);
215
- const hash = crypto.createHash("sha256");
297
+ const hash = crypto2.createHash("sha256");
216
298
  hash.update(normalizedPath);
217
- hash.update("\0");
299
+ hash.update(":");
218
300
  hash.update(content);
219
301
  return hash.digest();
220
302
  }
221
303
  function hashFileSync(realPath, normalizedPath) {
222
304
  const content = fs.readFileSync(realPath);
223
- const hash = crypto.createHash("sha256");
305
+ const hash = crypto2.createHash("sha256");
224
306
  hash.update(normalizedPath);
225
- hash.update("\0");
307
+ hash.update(":");
226
308
  hash.update(content);
227
309
  return hash.digest();
228
310
  }
@@ -232,7 +314,7 @@ function validateOptions(options) {
232
314
  }
233
315
  const baseDir = options.baseDir ?? process.cwd();
234
316
  for (const pkg of options.packages) {
235
- const pkgPath = path.resolve(baseDir, pkg);
317
+ const pkgPath = path6.resolve(baseDir, pkg);
236
318
  if (!fs.existsSync(pkgPath)) {
237
319
  throw new Error(`Package path does not exist: ${pkgPath}`);
238
320
  }
@@ -246,7 +328,7 @@ async function computeFingerprint(options) {
246
328
  const fileHashCache = /* @__PURE__ */ new Map();
247
329
  const fileHashInputs = [];
248
330
  for (const file of sortedFiles) {
249
- const filePath = path.resolve(baseDir, file);
331
+ const filePath = path6.resolve(baseDir, file);
250
332
  let realPath = filePath;
251
333
  let stats = await fs.promises.lstat(filePath);
252
334
  if (stats.isSymbolicLink()) {
@@ -289,7 +371,7 @@ function computeFingerprintSync(options) {
289
371
  const fileHashCache = /* @__PURE__ */ new Map();
290
372
  const fileHashInputs = [];
291
373
  for (const file of sortedFiles) {
292
- const filePath = path.resolve(baseDir, file);
374
+ const filePath = path6.resolve(baseDir, file);
293
375
  let realPath = filePath;
294
376
  let stats = fs.lstatSync(filePath);
295
377
  if (stats.isSymbolicLink()) {
@@ -416,7 +498,7 @@ async function writeAttestations(filePath, attestations, signature) {
416
498
  signature
417
499
  };
418
500
  attestationsFileSchema.parse(fileContent);
419
- const dir = path.dirname(filePath);
501
+ const dir = path6.dirname(filePath);
420
502
  await fs.promises.mkdir(dir, { recursive: true });
421
503
  const json = JSON.stringify(fileContent, null, 2);
422
504
  await fs.promises.writeFile(filePath, json, "utf-8");
@@ -428,7 +510,7 @@ function writeAttestationsSync(filePath, attestations, signature) {
428
510
  signature
429
511
  };
430
512
  attestationsFileSchema.parse(fileContent);
431
- const dir = path.dirname(filePath);
513
+ const dir = path6.dirname(filePath);
432
514
  fs.mkdirSync(dir, { recursive: true });
433
515
  const json = JSON.stringify(fileContent, null, 2);
434
516
  fs.writeFileSync(filePath, json, "utf-8");
@@ -462,7 +544,7 @@ function createAttestation(params) {
462
544
  suite: params.suite,
463
545
  fingerprint: params.fingerprint,
464
546
  attestedAt: (/* @__PURE__ */ new Date()).toISOString(),
465
- attestedBy: params.attestedBy ?? os.userInfo().username,
547
+ attestedBy: params.attestedBy ?? os2.userInfo().username,
466
548
  command: params.command,
467
549
  exitCode: 0
468
550
  };
@@ -470,22 +552,37 @@ function createAttestation(params) {
470
552
  return attestation;
471
553
  }
472
554
  async function writeSignedAttestations(options) {
473
- const { sign: sign2 } = await import('./crypto-VAXWUGKL.js');
555
+ const { sign: sign4 } = await import('./crypto-CE2YISRD.js');
556
+ const { privateKeyPath, keyProvider, keyRef } = options;
557
+ if (!privateKeyPath && (!keyProvider || !keyRef)) {
558
+ throw new Error(
559
+ "Either privateKeyPath or both keyProvider and keyRef must be provided for signing"
560
+ );
561
+ }
474
562
  const canonical = canonicalizeAttestations(options.attestations);
475
- const signature = await sign2({
476
- privateKeyPath: options.privateKeyPath,
563
+ const signOptions = {
477
564
  data: canonical
478
- });
565
+ };
566
+ if (privateKeyPath !== void 0) {
567
+ signOptions.privateKeyPath = privateKeyPath;
568
+ }
569
+ if (keyProvider !== void 0) {
570
+ signOptions.keyProvider = keyProvider;
571
+ }
572
+ if (keyRef !== void 0) {
573
+ signOptions.keyRef = keyRef;
574
+ }
575
+ const signature = await sign4(signOptions);
479
576
  await writeAttestations(options.filePath, options.attestations, signature);
480
577
  }
481
578
  async function readAndVerifyAttestations(options) {
482
- const { verify: verify2 } = await import('./crypto-VAXWUGKL.js');
579
+ const { verify: verify4 } = await import('./crypto-CE2YISRD.js');
483
580
  const file = await readAttestations(options.filePath);
484
581
  if (!file) {
485
582
  throw new Error(`Attestations file not found: ${options.filePath}`);
486
583
  }
487
584
  const canonical = canonicalizeAttestations(file.attestations);
488
- const isValid = await verify2({
585
+ const isValid = await verify4({
489
586
  publicKeyPath: options.publicKeyPath,
490
587
  data: canonical,
491
588
  signature: file.signature
@@ -505,6 +602,123 @@ var SignatureInvalidError = class extends Error {
505
602
  this.name = "SignatureInvalidError";
506
603
  }
507
604
  };
605
+ function isBuffer(value) {
606
+ return Buffer.isBuffer(value);
607
+ }
608
+ function generateKeyPair2() {
609
+ try {
610
+ const keyPair = crypto2.generateKeyPairSync("ed25519", {
611
+ publicKeyEncoding: {
612
+ type: "spki",
613
+ format: "pem"
614
+ },
615
+ privateKeyEncoding: {
616
+ type: "pkcs8",
617
+ format: "pem"
618
+ }
619
+ });
620
+ const { publicKey, privateKey } = keyPair;
621
+ if (typeof publicKey !== "string" || typeof privateKey !== "string") {
622
+ throw new Error("Expected keypair to have string keys");
623
+ }
624
+ const publicKeyObj = crypto2.createPublicKey(publicKey);
625
+ const publicKeyExport = publicKeyObj.export({
626
+ type: "spki",
627
+ format: "der"
628
+ });
629
+ if (!isBuffer(publicKeyExport)) {
630
+ throw new Error("Expected public key export to be a Buffer");
631
+ }
632
+ const rawPublicKey = publicKeyExport.subarray(12);
633
+ const publicKeyBase64 = rawPublicKey.toString("base64");
634
+ return {
635
+ publicKey: publicKeyBase64,
636
+ privateKey
637
+ };
638
+ } catch (err) {
639
+ throw new Error(
640
+ `Failed to generate Ed25519 keypair: ${err instanceof Error ? err.message : String(err)}`
641
+ );
642
+ }
643
+ }
644
+ function sign3(data, privateKeyPem) {
645
+ try {
646
+ const dataBuffer = typeof data === "string" ? Buffer.from(data, "utf8") : data;
647
+ const privateKeyObj = crypto2.createPrivateKey(privateKeyPem);
648
+ const signatureResult = crypto2.sign(null, dataBuffer, privateKeyObj);
649
+ if (!isBuffer(signatureResult)) {
650
+ throw new Error("Expected signature to be a Buffer");
651
+ }
652
+ return signatureResult.toString("base64");
653
+ } catch (err) {
654
+ throw new Error(
655
+ `Failed to sign data with Ed25519: ${err instanceof Error ? err.message : String(err)}`
656
+ );
657
+ }
658
+ }
659
+ function verify3(data, signature, publicKeyBase64) {
660
+ try {
661
+ const dataBuffer = typeof data === "string" ? Buffer.from(data, "utf8") : data;
662
+ const signatureBuffer = Buffer.from(signature, "base64");
663
+ const rawPublicKey = Buffer.from(publicKeyBase64, "base64");
664
+ if (rawPublicKey.length !== 32) {
665
+ throw new Error(
666
+ `Invalid Ed25519 public key length: expected 32 bytes, got ${rawPublicKey.length.toString()}`
667
+ );
668
+ }
669
+ const spkiHeader = Buffer.from([
670
+ 48,
671
+ 42,
672
+ // SEQUENCE, 42 bytes
673
+ 48,
674
+ 5,
675
+ // SEQUENCE, 5 bytes
676
+ 6,
677
+ 3,
678
+ 43,
679
+ 101,
680
+ 112,
681
+ // OID 1.3.101.112 (Ed25519)
682
+ 3,
683
+ 33,
684
+ 0
685
+ // BIT STRING, 33 bytes (32 key + 1 padding)
686
+ ]);
687
+ const spkiBuffer = Buffer.concat([spkiHeader, rawPublicKey]);
688
+ const publicKeyObj = crypto2.createPublicKey({
689
+ key: spkiBuffer,
690
+ format: "der",
691
+ type: "spki"
692
+ });
693
+ return crypto2.verify(null, dataBuffer, publicKeyObj, signatureBuffer);
694
+ } catch (err) {
695
+ if (err instanceof Error && err.message.includes("verification failed")) {
696
+ return false;
697
+ }
698
+ throw new Error(
699
+ `Failed to verify Ed25519 signature: ${err instanceof Error ? err.message : String(err)}`
700
+ );
701
+ }
702
+ }
703
+ function getPublicKeyFromPrivate(privateKeyPem) {
704
+ try {
705
+ const privateKeyObj = crypto2.createPrivateKey(privateKeyPem);
706
+ const publicKeyObj = crypto2.createPublicKey(privateKeyObj);
707
+ const publicKeyExport = publicKeyObj.export({
708
+ type: "spki",
709
+ format: "der"
710
+ });
711
+ if (!isBuffer(publicKeyExport)) {
712
+ throw new Error("Expected public key export to be a Buffer");
713
+ }
714
+ const rawPublicKey = publicKeyExport.subarray(12);
715
+ return rawPublicKey.toString("base64");
716
+ } catch (err) {
717
+ throw new Error(
718
+ `Failed to extract public key from Ed25519 private key: ${err instanceof Error ? err.message : String(err)}`
719
+ );
720
+ }
721
+ }
508
722
  async function verifyAttestations(options) {
509
723
  const { config, repoRoot = process.cwd() } = options;
510
724
  const errors = [];
@@ -555,6 +769,14 @@ async function verifyAttestations(options) {
555
769
  }
556
770
  async function verifySuite(options) {
557
771
  const { suiteName, suiteConfig, attestations, maxAgeDays, repoRoot } = options;
772
+ if (!suiteConfig.packages || suiteConfig.packages.length === 0) {
773
+ return {
774
+ suite: suiteName,
775
+ status: "NEEDS_ATTESTATION",
776
+ fingerprint: "",
777
+ message: "Suite configuration missing packages field"
778
+ };
779
+ }
558
780
  const fingerprintOptions = {
559
781
  packages: suiteConfig.packages.map((p) => resolvePath(p, repoRoot)),
560
782
  baseDir: repoRoot,
@@ -618,15 +840,1064 @@ function checkInvalidationChains(config, results) {
618
840
  }
619
841
  }
620
842
  function resolvePath(relativePath, baseDir) {
621
- if (path.isAbsolute(relativePath)) {
843
+ if (path6.isAbsolute(relativePath)) {
622
844
  return relativePath;
623
845
  }
624
- return path.join(baseDir, relativePath);
846
+ return path6.join(baseDir, relativePath);
847
+ }
848
+ var FilesystemKeyProvider = class {
849
+ type = "filesystem";
850
+ displayName = "Filesystem";
851
+ privateKeyPath;
852
+ /**
853
+ * Create a new FilesystemKeyProvider.
854
+ * @param options - Provider options
855
+ */
856
+ constructor(options = {}) {
857
+ this.privateKeyPath = options.privateKeyPath ?? getDefaultPrivateKeyPath();
858
+ }
859
+ /**
860
+ * Check if this provider is available.
861
+ * Filesystem provider is always available.
862
+ */
863
+ async isAvailable() {
864
+ return Promise.resolve(true);
865
+ }
866
+ /**
867
+ * Check if a key exists at the given path.
868
+ * @param keyRef - Path to the private key file
869
+ */
870
+ async keyExists(keyRef) {
871
+ try {
872
+ await fs6.access(keyRef);
873
+ return true;
874
+ } catch {
875
+ return false;
876
+ }
877
+ }
878
+ /**
879
+ * Get the private key path for signing.
880
+ * Returns the path directly with a no-op cleanup function.
881
+ * @param keyRef - Path to the private key file
882
+ */
883
+ async getPrivateKey(keyRef) {
884
+ if (!await this.keyExists(keyRef)) {
885
+ throw new Error(`Private key not found: ${keyRef}`);
886
+ }
887
+ return {
888
+ keyPath: keyRef,
889
+ // No-op cleanup for filesystem provider
890
+ cleanup: async () => {
891
+ }
892
+ };
893
+ }
894
+ /**
895
+ * Generate a new keypair and store on filesystem.
896
+ * @param options - Key generation options
897
+ */
898
+ async generateKeyPair(options) {
899
+ const { publicKeyPath, force = false } = options;
900
+ const result = await generateKeyPair({
901
+ privatePath: this.privateKeyPath,
902
+ publicPath: publicKeyPath,
903
+ force
904
+ });
905
+ return {
906
+ privateKeyRef: result.privatePath,
907
+ publicKeyPath: result.publicPath,
908
+ storageDescription: `Filesystem: ${result.privatePath}`
909
+ };
910
+ }
911
+ /**
912
+ * Get the configuration for this provider.
913
+ */
914
+ getConfig() {
915
+ return {
916
+ type: this.type,
917
+ options: {
918
+ privateKeyPath: this.privateKeyPath
919
+ }
920
+ };
921
+ }
922
+ };
923
+ var OnePasswordKeyProvider = class _OnePasswordKeyProvider {
924
+ type = "1password";
925
+ displayName = "1Password";
926
+ account;
927
+ vault;
928
+ itemName;
929
+ /**
930
+ * Create a new OnePasswordKeyProvider.
931
+ * @param options - Provider options
932
+ */
933
+ constructor(options) {
934
+ if (options.account !== void 0) {
935
+ this.account = options.account;
936
+ }
937
+ this.vault = options.vault;
938
+ this.itemName = options.itemName;
939
+ }
940
+ /**
941
+ * Check if the 1Password CLI is installed.
942
+ * @returns True if `op` command is available
943
+ */
944
+ static async isInstalled() {
945
+ try {
946
+ await execCommand("op", ["--version"]);
947
+ return true;
948
+ } catch {
949
+ return false;
950
+ }
951
+ }
952
+ /**
953
+ * List all 1Password accounts.
954
+ * @returns Array of account information
955
+ */
956
+ static async listAccounts() {
957
+ try {
958
+ const output = await execCommand("op", ["account", "list", "--format=json"]);
959
+ const parsed = JSON.parse(output);
960
+ if (!Array.isArray(parsed)) {
961
+ return [];
962
+ }
963
+ return parsed;
964
+ } catch (error) {
965
+ if (process.env.NODE_ENV !== "production") {
966
+ console.error("Failed to list 1Password accounts:", error);
967
+ }
968
+ return [];
969
+ }
970
+ }
971
+ /**
972
+ * List vaults in a specific account.
973
+ * @param account - Account email (optional if only one account)
974
+ * @returns Array of vault information
975
+ */
976
+ static async listVaults(account) {
977
+ try {
978
+ const args = ["vault", "list", "--format=json"];
979
+ if (account) {
980
+ args.push("--account", account);
981
+ }
982
+ const output = await execCommand("op", args);
983
+ const parsed = JSON.parse(output);
984
+ if (!Array.isArray(parsed)) {
985
+ return [];
986
+ }
987
+ return parsed;
988
+ } catch (error) {
989
+ if (process.env.NODE_ENV !== "production") {
990
+ console.error("Failed to list 1Password vaults:", error);
991
+ }
992
+ return [];
993
+ }
994
+ }
995
+ /**
996
+ * Check if this provider is available.
997
+ * Requires `op` CLI to be installed and authenticated.
998
+ */
999
+ async isAvailable() {
1000
+ return _OnePasswordKeyProvider.isInstalled();
1001
+ }
1002
+ /**
1003
+ * Check if a key exists in 1Password.
1004
+ * @param keyRef - Item name in 1Password
1005
+ */
1006
+ async keyExists(keyRef) {
1007
+ try {
1008
+ const args = ["item", "get", keyRef, "--vault", this.vault, "--format=json"];
1009
+ if (this.account) {
1010
+ args.push("--account", this.account);
1011
+ }
1012
+ await execCommand("op", args);
1013
+ return true;
1014
+ } catch {
1015
+ return false;
1016
+ }
1017
+ }
1018
+ /**
1019
+ * Get the private key from 1Password for signing.
1020
+ * Downloads to a temporary file and returns a cleanup function.
1021
+ * @param keyRef - Item name in 1Password
1022
+ * @throws Error if the key does not exist in 1Password
1023
+ */
1024
+ async getPrivateKey(keyRef) {
1025
+ if (!await this.keyExists(keyRef)) {
1026
+ throw new Error(
1027
+ `Key not found in 1Password: "${keyRef}" (vault: ${this.vault})` + (this.account ? ` (account: ${this.account})` : "")
1028
+ );
1029
+ }
1030
+ const tempDir = await fs6.mkdtemp(path6.join(os2.tmpdir(), "attest-it-"));
1031
+ const tempKeyPath = path6.join(tempDir, "private.pem");
1032
+ try {
1033
+ const args = ["document", "get", keyRef, "--vault", this.vault, "--out-file", tempKeyPath];
1034
+ if (this.account) {
1035
+ args.push("--account", this.account);
1036
+ }
1037
+ await execCommand("op", args);
1038
+ await setKeyPermissions(tempKeyPath);
1039
+ return {
1040
+ keyPath: tempKeyPath,
1041
+ cleanup: async () => {
1042
+ try {
1043
+ await fs6.unlink(tempKeyPath);
1044
+ await fs6.rmdir(tempDir);
1045
+ } catch (cleanupError) {
1046
+ console.warn(
1047
+ `Warning: Failed to clean up temporary key file at ${tempKeyPath}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
1048
+ );
1049
+ }
1050
+ }
1051
+ };
1052
+ } catch (error) {
1053
+ try {
1054
+ await fs6.rm(tempDir, { recursive: true, force: true });
1055
+ } catch (cleanupError) {
1056
+ console.warn(
1057
+ `Warning: Failed to clean up temporary key directory at ${tempDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
1058
+ );
1059
+ }
1060
+ throw error;
1061
+ }
1062
+ }
1063
+ /**
1064
+ * Generate a new keypair and store private key in 1Password.
1065
+ * Public key is written to filesystem for repository commit.
1066
+ * @param options - Key generation options
1067
+ */
1068
+ async generateKeyPair(options) {
1069
+ const { publicKeyPath, force = false } = options;
1070
+ const tempDir = await fs6.mkdtemp(path6.join(os2.tmpdir(), "attest-it-keygen-"));
1071
+ const tempPrivateKeyPath = path6.join(tempDir, "private.pem");
1072
+ try {
1073
+ await generateKeyPair({
1074
+ privatePath: tempPrivateKeyPath,
1075
+ publicPath: publicKeyPath,
1076
+ force
1077
+ });
1078
+ const args = [
1079
+ "document",
1080
+ "create",
1081
+ tempPrivateKeyPath,
1082
+ "--title",
1083
+ this.itemName,
1084
+ "--vault",
1085
+ this.vault
1086
+ ];
1087
+ if (this.account) {
1088
+ args.push("--account", this.account);
1089
+ }
1090
+ await execCommand("op", args);
1091
+ await fs6.unlink(tempPrivateKeyPath);
1092
+ await fs6.rmdir(tempDir);
1093
+ return {
1094
+ privateKeyRef: this.itemName,
1095
+ publicKeyPath,
1096
+ storageDescription: `1Password: ${this.vault}/${this.itemName}`
1097
+ };
1098
+ } catch (error) {
1099
+ try {
1100
+ await fs6.rm(tempDir, { recursive: true, force: true });
1101
+ } catch (cleanupError) {
1102
+ console.warn(
1103
+ `Warning: Failed to clean up temporary key directory at ${tempDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
1104
+ );
1105
+ }
1106
+ throw error;
1107
+ }
1108
+ }
1109
+ /**
1110
+ * Get the configuration for this provider.
1111
+ */
1112
+ getConfig() {
1113
+ return {
1114
+ type: this.type,
1115
+ options: {
1116
+ ...this.account && { account: this.account },
1117
+ vault: this.vault,
1118
+ itemName: this.itemName
1119
+ }
1120
+ };
1121
+ }
1122
+ };
1123
+ async function execCommand(command, args) {
1124
+ return new Promise((resolve3, reject) => {
1125
+ const proc = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
1126
+ let stdout = "";
1127
+ let stderr = "";
1128
+ proc.stdout.on("data", (data) => {
1129
+ stdout += data.toString();
1130
+ });
1131
+ proc.stderr.on("data", (data) => {
1132
+ stderr += data.toString();
1133
+ });
1134
+ proc.on("close", (code) => {
1135
+ if (code === 0) {
1136
+ resolve3(stdout.trim());
1137
+ } else {
1138
+ reject(new Error(`Command failed with exit code ${String(code)}: ${stderr}`));
1139
+ }
1140
+ });
1141
+ proc.on("error", (error) => {
1142
+ reject(error);
1143
+ });
1144
+ });
1145
+ }
1146
+ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
1147
+ type = "macos-keychain";
1148
+ displayName = "macOS Keychain";
1149
+ itemName;
1150
+ keychain;
1151
+ static ACCOUNT = "attest-it";
1152
+ /**
1153
+ * Create a new MacOSKeychainKeyProvider.
1154
+ * @param options - Provider options
1155
+ */
1156
+ constructor(options) {
1157
+ this.itemName = options.itemName;
1158
+ if (options.keychain !== void 0) {
1159
+ this.keychain = options.keychain;
1160
+ }
1161
+ }
1162
+ /**
1163
+ * Check if this provider is available.
1164
+ * Only available on macOS platforms.
1165
+ */
1166
+ static isAvailable() {
1167
+ return process.platform === "darwin";
1168
+ }
1169
+ /**
1170
+ * List available keychains on the system.
1171
+ * @returns Array of keychain information
1172
+ */
1173
+ static async listKeychains() {
1174
+ if (!_MacOSKeychainKeyProvider.isAvailable()) {
1175
+ return [];
1176
+ }
1177
+ try {
1178
+ const output = await execCommand2("security", ["list-keychains"]);
1179
+ const keychains = [];
1180
+ const lines = output.split("\n");
1181
+ for (const line of lines) {
1182
+ const match = /"(.+)"/.exec(line.trim());
1183
+ if (match?.[1]) {
1184
+ const fullPath = match[1];
1185
+ const filename = fullPath.split("/").pop() ?? fullPath;
1186
+ const name = filename.replace(/\.keychain(-db)?$/, "");
1187
+ keychains.push({ path: fullPath, name });
1188
+ }
1189
+ }
1190
+ return keychains;
1191
+ } catch {
1192
+ return [];
1193
+ }
1194
+ }
1195
+ /**
1196
+ * Check if this provider is available on the current system.
1197
+ */
1198
+ isAvailable() {
1199
+ return Promise.resolve(_MacOSKeychainKeyProvider.isAvailable());
1200
+ }
1201
+ /**
1202
+ * Check if a key exists in the keychain.
1203
+ * @param keyRef - Item name in keychain
1204
+ */
1205
+ async keyExists(keyRef) {
1206
+ try {
1207
+ const args = ["find-generic-password", "-a", _MacOSKeychainKeyProvider.ACCOUNT, "-s", keyRef];
1208
+ if (this.keychain) {
1209
+ args.push(this.keychain);
1210
+ }
1211
+ await execCommand2("security", args);
1212
+ return true;
1213
+ } catch {
1214
+ return false;
1215
+ }
1216
+ }
1217
+ /**
1218
+ * Get the private key from keychain for signing.
1219
+ * Downloads to a temporary file and returns a cleanup function.
1220
+ * @param keyRef - Item name in keychain
1221
+ * @throws Error if the key does not exist in keychain
1222
+ */
1223
+ async getPrivateKey(keyRef) {
1224
+ if (!await this.keyExists(keyRef)) {
1225
+ throw new Error(
1226
+ `Key not found in macOS Keychain: "${keyRef}" (account: ${_MacOSKeychainKeyProvider.ACCOUNT})`
1227
+ );
1228
+ }
1229
+ const tempDir = await fs6.mkdtemp(path6.join(os2.tmpdir(), "attest-it-"));
1230
+ const tempKeyPath = path6.join(tempDir, "private.pem");
1231
+ try {
1232
+ const findArgs = [
1233
+ "find-generic-password",
1234
+ "-a",
1235
+ _MacOSKeychainKeyProvider.ACCOUNT,
1236
+ "-s",
1237
+ keyRef,
1238
+ "-w"
1239
+ ];
1240
+ if (this.keychain) {
1241
+ findArgs.push(this.keychain);
1242
+ }
1243
+ const base64Key = await execCommand2("security", findArgs);
1244
+ const keyContent = Buffer.from(base64Key, "base64").toString("utf8");
1245
+ await fs6.writeFile(tempKeyPath, keyContent, { mode: 384 });
1246
+ await setKeyPermissions(tempKeyPath);
1247
+ return {
1248
+ keyPath: tempKeyPath,
1249
+ cleanup: async () => {
1250
+ try {
1251
+ await fs6.unlink(tempKeyPath);
1252
+ await fs6.rmdir(tempDir);
1253
+ } catch (cleanupError) {
1254
+ console.warn(
1255
+ `Warning: Failed to clean up temporary key file at ${tempKeyPath}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
1256
+ );
1257
+ }
1258
+ }
1259
+ };
1260
+ } catch (error) {
1261
+ try {
1262
+ await fs6.rm(tempDir, { recursive: true, force: true });
1263
+ } catch (cleanupError) {
1264
+ console.warn(
1265
+ `Warning: Failed to clean up temporary key directory at ${tempDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
1266
+ );
1267
+ }
1268
+ throw error;
1269
+ }
1270
+ }
1271
+ /**
1272
+ * Generate a new keypair and store private key in keychain.
1273
+ * Public key is written to filesystem for repository commit.
1274
+ * @param options - Key generation options
1275
+ */
1276
+ async generateKeyPair(options) {
1277
+ const { publicKeyPath, force = false } = options;
1278
+ const tempDir = await fs6.mkdtemp(path6.join(os2.tmpdir(), "attest-it-keygen-"));
1279
+ const tempPrivateKeyPath = path6.join(tempDir, "private.pem");
1280
+ try {
1281
+ await generateKeyPair({
1282
+ privatePath: tempPrivateKeyPath,
1283
+ publicPath: publicKeyPath,
1284
+ force
1285
+ });
1286
+ const privateKeyContent = await fs6.readFile(tempPrivateKeyPath, "utf8");
1287
+ const base64Key = Buffer.from(privateKeyContent, "utf8").toString("base64");
1288
+ const addArgs = [
1289
+ "add-generic-password",
1290
+ "-a",
1291
+ _MacOSKeychainKeyProvider.ACCOUNT,
1292
+ "-s",
1293
+ this.itemName,
1294
+ "-w",
1295
+ base64Key,
1296
+ "-T",
1297
+ "",
1298
+ "-U"
1299
+ ];
1300
+ if (this.keychain) {
1301
+ addArgs.push(this.keychain);
1302
+ }
1303
+ await execCommand2("security", addArgs);
1304
+ await fs6.unlink(tempPrivateKeyPath);
1305
+ await fs6.rmdir(tempDir);
1306
+ return {
1307
+ privateKeyRef: this.itemName,
1308
+ publicKeyPath,
1309
+ storageDescription: `macOS Keychain: ${this.itemName}`
1310
+ };
1311
+ } catch (error) {
1312
+ try {
1313
+ await fs6.rm(tempDir, { recursive: true, force: true });
1314
+ } catch (cleanupError) {
1315
+ console.warn(
1316
+ `Warning: Failed to clean up temporary key directory at ${tempDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
1317
+ );
1318
+ }
1319
+ throw error;
1320
+ }
1321
+ }
1322
+ /**
1323
+ * Get the configuration for this provider.
1324
+ */
1325
+ getConfig() {
1326
+ return {
1327
+ type: this.type,
1328
+ options: {
1329
+ itemName: this.itemName
1330
+ }
1331
+ };
1332
+ }
1333
+ };
1334
+ async function execCommand2(command, args) {
1335
+ return new Promise((resolve3, reject) => {
1336
+ const proc = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
1337
+ let stdout = "";
1338
+ let stderr = "";
1339
+ proc.stdout.on("data", (data) => {
1340
+ stdout += data.toString();
1341
+ });
1342
+ proc.stderr.on("data", (data) => {
1343
+ stderr += data.toString();
1344
+ });
1345
+ proc.on("close", (code) => {
1346
+ if (code === 0) {
1347
+ resolve3(stdout.trim());
1348
+ } else {
1349
+ reject(new Error(`Command failed with exit code ${String(code)}: ${stderr}`));
1350
+ }
1351
+ });
1352
+ proc.on("error", (error) => {
1353
+ reject(error);
1354
+ });
1355
+ });
1356
+ }
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
+ var homeDirOverride = null;
1421
+ function setAttestItHomeDir(dir) {
1422
+ homeDirOverride = dir;
1423
+ }
1424
+ function getAttestItHomeDir() {
1425
+ return homeDirOverride;
1426
+ }
1427
+ var privateKeyRefSchema = z.discriminatedUnion("type", [
1428
+ z.object({
1429
+ type: z.literal("file"),
1430
+ path: z.string().min(1, "File path cannot be empty")
1431
+ }),
1432
+ z.object({
1433
+ type: z.literal("keychain"),
1434
+ service: z.string().min(1, "Service name cannot be empty"),
1435
+ account: z.string().min(1, "Account name cannot be empty"),
1436
+ keychain: z.string().optional()
1437
+ }),
1438
+ z.object({
1439
+ type: z.literal("1password"),
1440
+ account: z.string().optional(),
1441
+ vault: z.string().min(1, "Vault name cannot be empty"),
1442
+ item: z.string().min(1, "Item name cannot be empty"),
1443
+ field: z.string().optional()
1444
+ })
1445
+ ]);
1446
+ var identitySchema = z.object({
1447
+ name: z.string().min(1, "Identity name cannot be empty"),
1448
+ email: z.string().optional(),
1449
+ github: z.string().optional(),
1450
+ publicKey: z.string().min(1, "Public key cannot be empty"),
1451
+ privateKey: privateKeyRefSchema
1452
+ }).strict();
1453
+ var localConfigSchema = z.object({
1454
+ activeIdentity: z.string().min(1, "Active identity name cannot be empty"),
1455
+ identities: z.record(z.string(), identitySchema).refine((identities) => Object.keys(identities).length >= 1, {
1456
+ message: "At least one identity must be defined"
1457
+ })
1458
+ }).strict();
1459
+ var LocalConfigValidationError = class extends Error {
1460
+ constructor(message, issues) {
1461
+ super(message);
1462
+ this.issues = issues;
1463
+ this.name = "LocalConfigValidationError";
1464
+ }
1465
+ };
1466
+ function getLocalConfigPath() {
1467
+ if (homeDirOverride) {
1468
+ return join(homeDirOverride, "config.yaml");
1469
+ }
1470
+ const home = homedir();
1471
+ return join(home, ".config", "attest-it", "config.yaml");
1472
+ }
1473
+ function getAttestItConfigDir() {
1474
+ if (homeDirOverride) {
1475
+ return homeDirOverride;
1476
+ }
1477
+ return join(homedir(), ".config", "attest-it");
1478
+ }
1479
+ function parseLocalConfigContent(content) {
1480
+ let rawConfig;
1481
+ try {
1482
+ rawConfig = parse(content);
1483
+ } catch (error) {
1484
+ throw new LocalConfigValidationError(
1485
+ `Failed to parse YAML: ${error instanceof Error ? error.message : String(error)}`,
1486
+ []
1487
+ );
1488
+ }
1489
+ const result = localConfigSchema.safeParse(rawConfig);
1490
+ if (!result.success) {
1491
+ throw new LocalConfigValidationError(
1492
+ "Local configuration validation failed:\n" + result.error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n"),
1493
+ result.error.issues
1494
+ );
1495
+ }
1496
+ const identities = Object.fromEntries(
1497
+ Object.entries(result.data.identities).map(([key, identity]) => {
1498
+ let privateKey;
1499
+ if (identity.privateKey.type === "1password") {
1500
+ privateKey = {
1501
+ type: "1password",
1502
+ vault: identity.privateKey.vault,
1503
+ item: identity.privateKey.item,
1504
+ ...identity.privateKey.account !== void 0 && {
1505
+ account: identity.privateKey.account
1506
+ },
1507
+ ...identity.privateKey.field !== void 0 && { field: identity.privateKey.field }
1508
+ };
1509
+ } else if (identity.privateKey.type === "keychain") {
1510
+ privateKey = {
1511
+ type: "keychain",
1512
+ service: identity.privateKey.service,
1513
+ account: identity.privateKey.account,
1514
+ ...identity.privateKey.keychain !== void 0 && {
1515
+ keychain: identity.privateKey.keychain
1516
+ }
1517
+ };
1518
+ } else {
1519
+ privateKey = identity.privateKey;
1520
+ }
1521
+ return [
1522
+ key,
1523
+ {
1524
+ name: identity.name,
1525
+ publicKey: identity.publicKey,
1526
+ privateKey,
1527
+ ...identity.email !== void 0 && { email: identity.email },
1528
+ ...identity.github !== void 0 && { github: identity.github }
1529
+ }
1530
+ ];
1531
+ })
1532
+ );
1533
+ return {
1534
+ activeIdentity: result.data.activeIdentity,
1535
+ identities
1536
+ };
1537
+ }
1538
+ async function loadLocalConfig(configPath) {
1539
+ const resolvedPath = configPath ?? getLocalConfigPath();
1540
+ try {
1541
+ const content = await readFile(resolvedPath, "utf8");
1542
+ return parseLocalConfigContent(content);
1543
+ } catch (error) {
1544
+ if (error instanceof LocalConfigValidationError) {
1545
+ throw error;
1546
+ }
1547
+ if (error && typeof error === "object" && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
1548
+ return null;
1549
+ }
1550
+ throw error;
1551
+ }
1552
+ }
1553
+ function loadLocalConfigSync(configPath) {
1554
+ const resolvedPath = configPath ?? getLocalConfigPath();
1555
+ try {
1556
+ const content = readFileSync(resolvedPath, "utf8");
1557
+ return parseLocalConfigContent(content);
1558
+ } catch (error) {
1559
+ if (error instanceof LocalConfigValidationError) {
1560
+ throw error;
1561
+ }
1562
+ if (error && typeof error === "object" && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
1563
+ return null;
1564
+ }
1565
+ throw error;
1566
+ }
1567
+ }
1568
+ async function saveLocalConfig(config, configPath) {
1569
+ const resolvedPath = configPath ?? getLocalConfigPath();
1570
+ const content = stringify(config);
1571
+ const dir = dirname(resolvedPath);
1572
+ await mkdir(dir, { recursive: true });
1573
+ await writeFile(resolvedPath, content, "utf8");
1574
+ }
1575
+ function saveLocalConfigSync(config, configPath) {
1576
+ const resolvedPath = configPath ?? getLocalConfigPath();
1577
+ const content = stringify(config);
1578
+ const dir = dirname(resolvedPath);
1579
+ mkdirSync(dir, { recursive: true });
1580
+ writeFileSync(resolvedPath, content, "utf8");
1581
+ }
1582
+ function getActiveIdentity(config) {
1583
+ return config.identities[config.activeIdentity];
1584
+ }
1585
+ function isAuthorizedSigner(config, gateId, publicKey) {
1586
+ const gate = config.gates?.[gateId];
1587
+ if (!gate) {
1588
+ return false;
1589
+ }
1590
+ const teamMember = findTeamMemberByPublicKey(config, publicKey);
1591
+ if (!teamMember) {
1592
+ return false;
1593
+ }
1594
+ const teamMemberSlug = findTeamMemberSlug(config, teamMember);
1595
+ if (!teamMemberSlug) {
1596
+ return false;
1597
+ }
1598
+ return gate.authorizedSigners.includes(teamMemberSlug);
1599
+ }
1600
+ function getAuthorizedSignersForGate(config, gateId) {
1601
+ const gate = config.gates?.[gateId];
1602
+ if (!gate || !config.team) {
1603
+ return [];
1604
+ }
1605
+ const authorizedMembers = [];
1606
+ for (const signerSlug of gate.authorizedSigners) {
1607
+ const member = config.team[signerSlug];
1608
+ if (member) {
1609
+ authorizedMembers.push(member);
1610
+ }
1611
+ }
1612
+ return authorizedMembers;
1613
+ }
1614
+ function findTeamMemberByPublicKey(config, publicKey) {
1615
+ if (!config.team) {
1616
+ return void 0;
1617
+ }
1618
+ for (const member of Object.values(config.team)) {
1619
+ if (member.publicKey === publicKey) {
1620
+ return member;
1621
+ }
1622
+ }
1623
+ return void 0;
1624
+ }
1625
+ function findTeamMemberSlug(config, teamMember) {
1626
+ if (!config.team) {
1627
+ return void 0;
1628
+ }
1629
+ for (const [slug, member] of Object.entries(config.team)) {
1630
+ if (member === teamMember || member.publicKey === teamMember.publicKey) {
1631
+ return slug;
1632
+ }
1633
+ }
1634
+ return void 0;
1635
+ }
1636
+ function getGate(config, gateId) {
1637
+ return config.gates?.[gateId];
1638
+ }
1639
+ var DURATION_PATTERN = /^\d+(\.\d+)?\s*(ms|s|m|h|d|w|y)$/i;
1640
+ function isValidDurationFormat(value) {
1641
+ return DURATION_PATTERN.test(value.trim());
1642
+ }
1643
+ function parseDuration(duration) {
1644
+ if (!isValidDurationFormat(duration)) {
1645
+ throw new Error(`Invalid duration string: ${duration}`);
1646
+ }
1647
+ const result = ms(duration);
1648
+ if (typeof result !== "number" || result <= 0) {
1649
+ throw new Error(`Invalid duration string: ${duration}`);
1650
+ }
1651
+ return result;
1652
+ }
1653
+ var sealSchema = z.object({
1654
+ gateId: z.string().min(1, "Gate ID cannot be empty"),
1655
+ // Fingerprint format: sha256:<hex> where hex is at least 1 character
1656
+ // Full fingerprints are 64 hex chars, but tests may use shorter values
1657
+ fingerprint: z.string().regex(/^sha256:[a-f0-9]+$/i, "Invalid fingerprint format (expected sha256:<hex>)"),
1658
+ timestamp: z.string().datetime({ message: "Invalid ISO 8601 timestamp" }),
1659
+ sealedBy: z.string().min(1, "Signer slug cannot be empty"),
1660
+ signature: z.string().min(1, "Signature cannot be empty")
1661
+ });
1662
+ var sealsFileSchema = z.object({
1663
+ version: z.literal(1, { errorMap: () => ({ message: "Unsupported seals file version" }) }),
1664
+ seals: z.record(z.string(), sealSchema)
1665
+ });
1666
+ function createSeal(options) {
1667
+ const { gateId, fingerprint, sealedBy, privateKey } = options;
1668
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1669
+ const canonicalString = `${gateId}:${fingerprint}:${timestamp}`;
1670
+ const signature = sign3(canonicalString, privateKey);
1671
+ return {
1672
+ gateId,
1673
+ fingerprint,
1674
+ timestamp,
1675
+ sealedBy,
1676
+ signature
1677
+ };
1678
+ }
1679
+ function verifySeal(seal, config) {
1680
+ const { gateId, fingerprint, timestamp, sealedBy, signature } = seal;
1681
+ if (!config.team) {
1682
+ return {
1683
+ valid: false,
1684
+ error: `No team configuration found`
1685
+ };
1686
+ }
1687
+ const teamMember = config.team[sealedBy];
1688
+ if (!teamMember) {
1689
+ return {
1690
+ valid: false,
1691
+ error: `Team member '${sealedBy}' not found in configuration`
1692
+ };
1693
+ }
1694
+ const canonicalString = `${gateId}:${fingerprint}:${timestamp}`;
1695
+ try {
1696
+ const isValid = verify3(canonicalString, signature, teamMember.publicKey);
1697
+ if (!isValid) {
1698
+ return {
1699
+ valid: false,
1700
+ error: "Signature verification failed"
1701
+ };
1702
+ }
1703
+ return { valid: true };
1704
+ } catch (error) {
1705
+ return {
1706
+ valid: false,
1707
+ error: `Signature verification error: ${error instanceof Error ? error.message : String(error)}`
1708
+ };
1709
+ }
1710
+ }
1711
+ function parseSealsContent(content) {
1712
+ let rawData;
1713
+ try {
1714
+ rawData = JSON.parse(content);
1715
+ } catch (error) {
1716
+ throw new Error(`Invalid JSON: ${error instanceof Error ? error.message : String(error)}`);
1717
+ }
1718
+ const result = sealsFileSchema.safeParse(rawData);
1719
+ if (!result.success) {
1720
+ const issues = result.error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n");
1721
+ throw new Error(`Invalid seals file:
1722
+ ${issues}`);
1723
+ }
1724
+ return result.data;
1725
+ }
1726
+ async function readSeals(dir) {
1727
+ const sealsPath = path6.join(dir, ".attest-it", "seals.json");
1728
+ try {
1729
+ const content = await fs.promises.readFile(sealsPath, "utf8");
1730
+ return parseSealsContent(content);
1731
+ } catch (error) {
1732
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
1733
+ return {
1734
+ version: 1,
1735
+ seals: {}
1736
+ };
1737
+ }
1738
+ throw new Error(
1739
+ `Failed to read seals file: ${error instanceof Error ? error.message : String(error)}`
1740
+ );
1741
+ }
1742
+ }
1743
+ function readSealsSync(dir) {
1744
+ const sealsPath = path6.join(dir, ".attest-it", "seals.json");
1745
+ try {
1746
+ const content = fs.readFileSync(sealsPath, "utf8");
1747
+ return parseSealsContent(content);
1748
+ } catch (error) {
1749
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
1750
+ return {
1751
+ version: 1,
1752
+ seals: {}
1753
+ };
1754
+ }
1755
+ throw new Error(
1756
+ `Failed to read seals file: ${error instanceof Error ? error.message : String(error)}`
1757
+ );
1758
+ }
1759
+ }
1760
+ async function writeSeals(dir, sealsFile) {
1761
+ const attestItDir = path6.join(dir, ".attest-it");
1762
+ const sealsPath = path6.join(attestItDir, "seals.json");
1763
+ try {
1764
+ await fs.promises.mkdir(attestItDir, { recursive: true });
1765
+ const content = JSON.stringify(sealsFile, null, 2) + "\n";
1766
+ await fs.promises.writeFile(sealsPath, content, "utf8");
1767
+ } catch (error) {
1768
+ throw new Error(
1769
+ `Failed to write seals file: ${error instanceof Error ? error.message : String(error)}`
1770
+ );
1771
+ }
1772
+ }
1773
+ function writeSealsSync(dir, sealsFile) {
1774
+ const attestItDir = path6.join(dir, ".attest-it");
1775
+ const sealsPath = path6.join(attestItDir, "seals.json");
1776
+ try {
1777
+ fs.mkdirSync(attestItDir, { recursive: true });
1778
+ const content = JSON.stringify(sealsFile, null, 2) + "\n";
1779
+ fs.writeFileSync(sealsPath, content, "utf8");
1780
+ } catch (error) {
1781
+ throw new Error(
1782
+ `Failed to write seals file: ${error instanceof Error ? error.message : String(error)}`
1783
+ );
1784
+ }
1785
+ }
1786
+
1787
+ // src/seal/verification.ts
1788
+ function verifyGateSeal(config, gateId, seals, currentFingerprint) {
1789
+ const gate = getGate(config, gateId);
1790
+ if (!gate) {
1791
+ return {
1792
+ gateId,
1793
+ state: "MISSING",
1794
+ message: `Gate '${gateId}' not found in configuration`
1795
+ };
1796
+ }
1797
+ const seal = seals.seals[gateId];
1798
+ if (!seal) {
1799
+ return {
1800
+ gateId,
1801
+ state: "MISSING",
1802
+ message: `No seal found for gate '${gateId}'`
1803
+ };
1804
+ }
1805
+ if (seal.fingerprint !== currentFingerprint) {
1806
+ return {
1807
+ gateId,
1808
+ state: "FINGERPRINT_MISMATCH",
1809
+ seal,
1810
+ message: `Fingerprint changed since seal was created`
1811
+ };
1812
+ }
1813
+ if (!config.team) {
1814
+ return {
1815
+ gateId,
1816
+ state: "UNKNOWN_SIGNER",
1817
+ seal,
1818
+ message: `No team configuration found`
1819
+ };
1820
+ }
1821
+ const teamMember = config.team[seal.sealedBy];
1822
+ if (!teamMember) {
1823
+ return {
1824
+ gateId,
1825
+ state: "UNKNOWN_SIGNER",
1826
+ seal,
1827
+ message: `Signer '${seal.sealedBy}' not found in team`
1828
+ };
1829
+ }
1830
+ const authorized = isAuthorizedSigner(config, gateId, teamMember.publicKey);
1831
+ if (!authorized) {
1832
+ return {
1833
+ gateId,
1834
+ state: "UNKNOWN_SIGNER",
1835
+ seal,
1836
+ message: `Signer '${seal.sealedBy}' is not authorized for gate '${gateId}'`
1837
+ };
1838
+ }
1839
+ const verificationResult = verifySeal(seal, config);
1840
+ if (!verificationResult.valid) {
1841
+ return {
1842
+ gateId,
1843
+ state: "INVALID_SIGNATURE",
1844
+ seal,
1845
+ message: verificationResult.error ?? "Signature verification failed"
1846
+ };
1847
+ }
1848
+ try {
1849
+ const maxAgeMs = parseDuration(gate.maxAge);
1850
+ const sealTimestamp = new Date(seal.timestamp).getTime();
1851
+ const now = Date.now();
1852
+ const ageMs = now - sealTimestamp;
1853
+ if (ageMs > maxAgeMs) {
1854
+ const ageDays = Math.floor(ageMs / (1e3 * 60 * 60 * 24));
1855
+ const maxAgeDays = Math.floor(maxAgeMs / (1e3 * 60 * 60 * 24));
1856
+ return {
1857
+ gateId,
1858
+ state: "STALE",
1859
+ seal,
1860
+ message: `Seal is ${ageDays.toString()} days old, exceeds maxAge of ${maxAgeDays.toString()} days`
1861
+ };
1862
+ }
1863
+ } catch (error) {
1864
+ return {
1865
+ gateId,
1866
+ state: "STALE",
1867
+ seal,
1868
+ message: `Cannot verify freshness: invalid maxAge format: ${error instanceof Error ? error.message : String(error)}`
1869
+ };
1870
+ }
1871
+ return {
1872
+ gateId,
1873
+ state: "VALID",
1874
+ seal
1875
+ };
1876
+ }
1877
+ function verifyAllSeals(config, seals, fingerprints) {
1878
+ if (!config.gates) {
1879
+ return [];
1880
+ }
1881
+ const results = [];
1882
+ for (const gateId of Object.keys(config.gates)) {
1883
+ const fingerprint = fingerprints[gateId];
1884
+ if (!fingerprint) {
1885
+ results.push({
1886
+ gateId,
1887
+ state: "MISSING",
1888
+ message: `No fingerprint computed for gate '${gateId}'`
1889
+ });
1890
+ continue;
1891
+ }
1892
+ const result = verifyGateSeal(config, gateId, seals, fingerprint);
1893
+ results.push(result);
1894
+ }
1895
+ return results;
625
1896
  }
626
1897
 
627
1898
  // src/index.ts
628
1899
  var version = "0.0.0";
629
1900
 
630
- export { ConfigNotFoundError, ConfigValidationError, SignatureInvalidError, canonicalizeAttestations, computeFingerprint, computeFingerprintSync, createAttestation, findAttestation, findConfigPath, listPackageFiles, loadConfig, loadConfigSync, readAndVerifyAttestations, readAttestations, readAttestationsSync, removeAttestation, resolveConfigPaths, toAttestItConfig, upsertAttestation, verifyAttestations, version, writeAttestations, writeAttestationsSync, writeSignedAttestations };
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 };
631
1902
  //# sourceMappingURL=index.js.map
632
1903
  //# sourceMappingURL=index.js.map