@attest-it/core 0.0.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,37 +1,101 @@
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(),
27
- invalidates: z.array(z.string().min(1, "Invalidated suite name cannot be empty")).optional()
28
- }).strict();
77
+ timeout: z.string().optional(),
78
+ interactive: z.boolean().optional(),
79
+ // Relationship fields
80
+ invalidates: z.array(z.string().min(1, "Invalidated suite name cannot be empty")).optional(),
81
+ depends_on: z.array(z.string().min(1, "Dependency suite name cannot be empty")).optional()
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
+ );
29
90
  var configSchema = z.object({
30
91
  version: z.literal(1),
31
92
  settings: settingsSchema.default({}),
93
+ team: z.record(z.string(), teamMemberSchema).optional(),
94
+ gates: z.record(z.string(), gateSchema).optional(),
32
95
  suites: z.record(z.string(), suiteSchema).refine((suites) => Object.keys(suites).length >= 1, {
33
96
  message: "At least one suite must be defined"
34
- })
97
+ }),
98
+ groups: z.record(z.string(), z.array(z.string().min(1, "Suite name in group cannot be empty"))).optional()
35
99
  }).strict();
36
100
  var ConfigValidationError = class extends Error {
37
101
  constructor(message, issues) {
@@ -144,30 +208,52 @@ function resolveConfigPaths(config, repoRoot) {
144
208
  };
145
209
  }
146
210
  function toAttestItConfig(config) {
147
- return {
211
+ const result = {
148
212
  version: config.version,
149
213
  settings: {
150
214
  maxAgeDays: config.settings.maxAgeDays,
151
215
  publicKeyPath: config.settings.publicKeyPath,
152
- attestationsPath: config.settings.attestationsPath,
153
- ...config.settings.defaultCommand !== void 0 && {
154
- defaultCommand: config.settings.defaultCommand
155
- }
216
+ attestationsPath: config.settings.attestationsPath
156
217
  },
157
- suites: Object.fromEntries(
158
- Object.entries(config.suites).map(([name, suite]) => [
159
- name,
160
- {
161
- packages: suite.packages,
162
- ...suite.description !== void 0 && { description: suite.description },
163
- ...suite.files !== void 0 && { files: suite.files },
164
- ...suite.ignore !== void 0 && { ignore: suite.ignore },
165
- ...suite.command !== void 0 && { command: suite.command },
166
- ...suite.invalidates !== void 0 && { invalidates: suite.invalidates }
167
- }
168
- ])
169
- )
218
+ suites: {}
170
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;
171
257
  }
172
258
  var LARGE_FILE_THRESHOLD = 50 * 1024 * 1024;
173
259
  function sortFiles(files) {
@@ -178,7 +264,7 @@ function sortFiles(files) {
178
264
  });
179
265
  }
180
266
  function normalizePath(filePath) {
181
- return filePath.split(path.sep).join("/");
267
+ return filePath.split(path6.sep).join("/");
182
268
  }
183
269
  function computeFinalFingerprint(fileHashes) {
184
270
  const sorted = [...fileHashes].sort((a, b) => {
@@ -188,15 +274,15 @@ function computeFinalFingerprint(fileHashes) {
188
274
  });
189
275
  const hashes = sorted.map((input) => input.hash);
190
276
  const concatenated = Buffer.concat(hashes);
191
- const finalHash = crypto.createHash("sha256").update(concatenated).digest();
277
+ const finalHash = crypto2.createHash("sha256").update(concatenated).digest();
192
278
  return `sha256:${finalHash.toString("hex")}`;
193
279
  }
194
280
  async function hashFileAsync(realPath, normalizedPath, stats) {
195
281
  if (stats.size > LARGE_FILE_THRESHOLD) {
196
282
  return new Promise((resolve3, reject) => {
197
- const hash2 = crypto.createHash("sha256");
283
+ const hash2 = crypto2.createHash("sha256");
198
284
  hash2.update(normalizedPath);
199
- hash2.update("\0");
285
+ hash2.update(":");
200
286
  const stream = fs.createReadStream(realPath);
201
287
  stream.on("data", (chunk) => {
202
288
  hash2.update(chunk);
@@ -208,17 +294,17 @@ async function hashFileAsync(realPath, normalizedPath, stats) {
208
294
  });
209
295
  }
210
296
  const content = await fs.promises.readFile(realPath);
211
- const hash = crypto.createHash("sha256");
297
+ const hash = crypto2.createHash("sha256");
212
298
  hash.update(normalizedPath);
213
- hash.update("\0");
299
+ hash.update(":");
214
300
  hash.update(content);
215
301
  return hash.digest();
216
302
  }
217
303
  function hashFileSync(realPath, normalizedPath) {
218
304
  const content = fs.readFileSync(realPath);
219
- const hash = crypto.createHash("sha256");
305
+ const hash = crypto2.createHash("sha256");
220
306
  hash.update(normalizedPath);
221
- hash.update("\0");
307
+ hash.update(":");
222
308
  hash.update(content);
223
309
  return hash.digest();
224
310
  }
@@ -228,7 +314,7 @@ function validateOptions(options) {
228
314
  }
229
315
  const baseDir = options.baseDir ?? process.cwd();
230
316
  for (const pkg of options.packages) {
231
- const pkgPath = path.resolve(baseDir, pkg);
317
+ const pkgPath = path6.resolve(baseDir, pkg);
232
318
  if (!fs.existsSync(pkgPath)) {
233
319
  throw new Error(`Package path does not exist: ${pkgPath}`);
234
320
  }
@@ -242,7 +328,7 @@ async function computeFingerprint(options) {
242
328
  const fileHashCache = /* @__PURE__ */ new Map();
243
329
  const fileHashInputs = [];
244
330
  for (const file of sortedFiles) {
245
- const filePath = path.resolve(baseDir, file);
331
+ const filePath = path6.resolve(baseDir, file);
246
332
  let realPath = filePath;
247
333
  let stats = await fs.promises.lstat(filePath);
248
334
  if (stats.isSymbolicLink()) {
@@ -285,7 +371,7 @@ function computeFingerprintSync(options) {
285
371
  const fileHashCache = /* @__PURE__ */ new Map();
286
372
  const fileHashInputs = [];
287
373
  for (const file of sortedFiles) {
288
- const filePath = path.resolve(baseDir, file);
374
+ const filePath = path6.resolve(baseDir, file);
289
375
  let realPath = filePath;
290
376
  let stats = fs.lstatSync(filePath);
291
377
  if (stats.isSymbolicLink()) {
@@ -412,7 +498,7 @@ async function writeAttestations(filePath, attestations, signature) {
412
498
  signature
413
499
  };
414
500
  attestationsFileSchema.parse(fileContent);
415
- const dir = path.dirname(filePath);
501
+ const dir = path6.dirname(filePath);
416
502
  await fs.promises.mkdir(dir, { recursive: true });
417
503
  const json = JSON.stringify(fileContent, null, 2);
418
504
  await fs.promises.writeFile(filePath, json, "utf-8");
@@ -424,7 +510,7 @@ function writeAttestationsSync(filePath, attestations, signature) {
424
510
  signature
425
511
  };
426
512
  attestationsFileSchema.parse(fileContent);
427
- const dir = path.dirname(filePath);
513
+ const dir = path6.dirname(filePath);
428
514
  fs.mkdirSync(dir, { recursive: true });
429
515
  const json = JSON.stringify(fileContent, null, 2);
430
516
  fs.writeFileSync(filePath, json, "utf-8");
@@ -458,7 +544,7 @@ function createAttestation(params) {
458
544
  suite: params.suite,
459
545
  fingerprint: params.fingerprint,
460
546
  attestedAt: (/* @__PURE__ */ new Date()).toISOString(),
461
- attestedBy: params.attestedBy ?? os.userInfo().username,
547
+ attestedBy: params.attestedBy ?? os2.userInfo().username,
462
548
  command: params.command,
463
549
  exitCode: 0
464
550
  };
@@ -466,22 +552,37 @@ function createAttestation(params) {
466
552
  return attestation;
467
553
  }
468
554
  async function writeSignedAttestations(options) {
469
- 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
+ }
470
562
  const canonical = canonicalizeAttestations(options.attestations);
471
- const signature = await sign2({
472
- privateKeyPath: options.privateKeyPath,
563
+ const signOptions = {
473
564
  data: canonical
474
- });
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);
475
576
  await writeAttestations(options.filePath, options.attestations, signature);
476
577
  }
477
578
  async function readAndVerifyAttestations(options) {
478
- const { verify: verify2 } = await import('./crypto-VAXWUGKL.js');
579
+ const { verify: verify4 } = await import('./crypto-CE2YISRD.js');
479
580
  const file = await readAttestations(options.filePath);
480
581
  if (!file) {
481
582
  throw new Error(`Attestations file not found: ${options.filePath}`);
482
583
  }
483
584
  const canonical = canonicalizeAttestations(file.attestations);
484
- const isValid = await verify2({
585
+ const isValid = await verify4({
485
586
  publicKeyPath: options.publicKeyPath,
486
587
  data: canonical,
487
588
  signature: file.signature
@@ -501,6 +602,123 @@ var SignatureInvalidError = class extends Error {
501
602
  this.name = "SignatureInvalidError";
502
603
  }
503
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
+ }
504
722
  async function verifyAttestations(options) {
505
723
  const { config, repoRoot = process.cwd() } = options;
506
724
  const errors = [];
@@ -551,6 +769,14 @@ async function verifyAttestations(options) {
551
769
  }
552
770
  async function verifySuite(options) {
553
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
+ }
554
780
  const fingerprintOptions = {
555
781
  packages: suiteConfig.packages.map((p) => resolvePath(p, repoRoot)),
556
782
  baseDir: repoRoot,
@@ -614,15 +840,1002 @@ function checkInvalidationChains(config, results) {
614
840
  }
615
841
  }
616
842
  function resolvePath(relativePath, baseDir) {
617
- if (path.isAbsolute(relativePath)) {
843
+ if (path6.isAbsolute(relativePath)) {
618
844
  return relativePath;
619
845
  }
620
- 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
+ static ACCOUNT = "attest-it";
1151
+ /**
1152
+ * Create a new MacOSKeychainKeyProvider.
1153
+ * @param options - Provider options
1154
+ */
1155
+ constructor(options) {
1156
+ this.itemName = options.itemName;
1157
+ }
1158
+ /**
1159
+ * Check if this provider is available.
1160
+ * Only available on macOS platforms.
1161
+ */
1162
+ static isAvailable() {
1163
+ return process.platform === "darwin";
1164
+ }
1165
+ /**
1166
+ * Check if this provider is available on the current system.
1167
+ */
1168
+ isAvailable() {
1169
+ return Promise.resolve(_MacOSKeychainKeyProvider.isAvailable());
1170
+ }
1171
+ /**
1172
+ * Check if a key exists in the keychain.
1173
+ * @param keyRef - Item name in keychain
1174
+ */
1175
+ async keyExists(keyRef) {
1176
+ try {
1177
+ await execCommand2("security", [
1178
+ "find-generic-password",
1179
+ "-a",
1180
+ _MacOSKeychainKeyProvider.ACCOUNT,
1181
+ "-s",
1182
+ keyRef
1183
+ ]);
1184
+ return true;
1185
+ } catch {
1186
+ return false;
1187
+ }
1188
+ }
1189
+ /**
1190
+ * Get the private key from keychain for signing.
1191
+ * Downloads to a temporary file and returns a cleanup function.
1192
+ * @param keyRef - Item name in keychain
1193
+ * @throws Error if the key does not exist in keychain
1194
+ */
1195
+ async getPrivateKey(keyRef) {
1196
+ if (!await this.keyExists(keyRef)) {
1197
+ throw new Error(
1198
+ `Key not found in macOS Keychain: "${keyRef}" (account: ${_MacOSKeychainKeyProvider.ACCOUNT})`
1199
+ );
1200
+ }
1201
+ const tempDir = await fs6.mkdtemp(path6.join(os2.tmpdir(), "attest-it-"));
1202
+ const tempKeyPath = path6.join(tempDir, "private.pem");
1203
+ try {
1204
+ const base64Key = await execCommand2("security", [
1205
+ "find-generic-password",
1206
+ "-a",
1207
+ _MacOSKeychainKeyProvider.ACCOUNT,
1208
+ "-s",
1209
+ keyRef,
1210
+ "-w"
1211
+ ]);
1212
+ const keyContent = Buffer.from(base64Key, "base64").toString("utf8");
1213
+ await fs6.writeFile(tempKeyPath, keyContent, { mode: 384 });
1214
+ await setKeyPermissions(tempKeyPath);
1215
+ return {
1216
+ keyPath: tempKeyPath,
1217
+ cleanup: async () => {
1218
+ try {
1219
+ await fs6.unlink(tempKeyPath);
1220
+ await fs6.rmdir(tempDir);
1221
+ } catch (cleanupError) {
1222
+ console.warn(
1223
+ `Warning: Failed to clean up temporary key file at ${tempKeyPath}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
1224
+ );
1225
+ }
1226
+ }
1227
+ };
1228
+ } catch (error) {
1229
+ try {
1230
+ await fs6.rm(tempDir, { recursive: true, force: true });
1231
+ } catch (cleanupError) {
1232
+ console.warn(
1233
+ `Warning: Failed to clean up temporary key directory at ${tempDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
1234
+ );
1235
+ }
1236
+ throw error;
1237
+ }
1238
+ }
1239
+ /**
1240
+ * Generate a new keypair and store private key in keychain.
1241
+ * Public key is written to filesystem for repository commit.
1242
+ * @param options - Key generation options
1243
+ */
1244
+ async generateKeyPair(options) {
1245
+ const { publicKeyPath, force = false } = options;
1246
+ const tempDir = await fs6.mkdtemp(path6.join(os2.tmpdir(), "attest-it-keygen-"));
1247
+ const tempPrivateKeyPath = path6.join(tempDir, "private.pem");
1248
+ try {
1249
+ await generateKeyPair({
1250
+ privatePath: tempPrivateKeyPath,
1251
+ publicPath: publicKeyPath,
1252
+ force
1253
+ });
1254
+ const privateKeyContent = await fs6.readFile(tempPrivateKeyPath, "utf8");
1255
+ const base64Key = Buffer.from(privateKeyContent, "utf8").toString("base64");
1256
+ await execCommand2("security", [
1257
+ "add-generic-password",
1258
+ "-a",
1259
+ _MacOSKeychainKeyProvider.ACCOUNT,
1260
+ "-s",
1261
+ this.itemName,
1262
+ "-w",
1263
+ base64Key,
1264
+ "-T",
1265
+ "",
1266
+ "-U"
1267
+ ]);
1268
+ await fs6.unlink(tempPrivateKeyPath);
1269
+ await fs6.rmdir(tempDir);
1270
+ return {
1271
+ privateKeyRef: this.itemName,
1272
+ publicKeyPath,
1273
+ storageDescription: `macOS Keychain: ${this.itemName}`
1274
+ };
1275
+ } catch (error) {
1276
+ try {
1277
+ await fs6.rm(tempDir, { recursive: true, force: true });
1278
+ } catch (cleanupError) {
1279
+ console.warn(
1280
+ `Warning: Failed to clean up temporary key directory at ${tempDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
1281
+ );
1282
+ }
1283
+ throw error;
1284
+ }
1285
+ }
1286
+ /**
1287
+ * Get the configuration for this provider.
1288
+ */
1289
+ getConfig() {
1290
+ return {
1291
+ type: this.type,
1292
+ options: {
1293
+ itemName: this.itemName
1294
+ }
1295
+ };
1296
+ }
1297
+ };
1298
+ async function execCommand2(command, args) {
1299
+ return new Promise((resolve3, reject) => {
1300
+ const proc = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
1301
+ let stdout = "";
1302
+ let stderr = "";
1303
+ proc.stdout.on("data", (data) => {
1304
+ stdout += data.toString();
1305
+ });
1306
+ proc.stderr.on("data", (data) => {
1307
+ stderr += data.toString();
1308
+ });
1309
+ proc.on("close", (code) => {
1310
+ if (code === 0) {
1311
+ resolve3(stdout.trim());
1312
+ } else {
1313
+ reject(new Error(`Command failed with exit code ${String(code)}: ${stderr}`));
1314
+ }
1315
+ });
1316
+ proc.on("error", (error) => {
1317
+ reject(error);
1318
+ });
1319
+ });
1320
+ }
1321
+
1322
+ // src/key-provider/registry.ts
1323
+ var KeyProviderRegistry = class {
1324
+ static providers = /* @__PURE__ */ new Map();
1325
+ /**
1326
+ * Register a key provider factory.
1327
+ * @param type - Provider type identifier
1328
+ * @param factory - Factory function to create provider instances
1329
+ */
1330
+ static register(type, factory) {
1331
+ this.providers.set(type, factory);
1332
+ }
1333
+ /**
1334
+ * Create a key provider from configuration.
1335
+ * @param config - Provider configuration
1336
+ * @returns A key provider instance
1337
+ * @throws Error if the provider type is not registered
1338
+ */
1339
+ static create(config) {
1340
+ const factory = this.providers.get(config.type);
1341
+ if (!factory) {
1342
+ throw new Error(
1343
+ `Unknown key provider type: ${config.type}. Available types: ${Array.from(this.providers.keys()).join(", ")}`
1344
+ );
1345
+ }
1346
+ return factory(config);
1347
+ }
1348
+ /**
1349
+ * Get all registered provider types.
1350
+ * @returns Array of provider type identifiers
1351
+ */
1352
+ static getProviderTypes() {
1353
+ return Array.from(this.providers.keys());
1354
+ }
1355
+ };
1356
+ KeyProviderRegistry.register("filesystem", (config) => {
1357
+ const privateKeyPath = typeof config.options.privateKeyPath === "string" ? config.options.privateKeyPath : void 0;
1358
+ if (privateKeyPath !== void 0) {
1359
+ return new FilesystemKeyProvider({ privateKeyPath });
1360
+ }
1361
+ return new FilesystemKeyProvider();
1362
+ });
1363
+ KeyProviderRegistry.register("1password", (config) => {
1364
+ const { options } = config;
1365
+ const account = typeof options.account === "string" ? options.account : void 0;
1366
+ const vault = typeof options.vault === "string" ? options.vault : "";
1367
+ const itemName = typeof options.itemName === "string" ? options.itemName : "";
1368
+ if (!vault || !itemName) {
1369
+ throw new Error("1Password provider requires vault and itemName options");
1370
+ }
1371
+ if (account !== void 0) {
1372
+ return new OnePasswordKeyProvider({ account, vault, itemName });
1373
+ }
1374
+ return new OnePasswordKeyProvider({ vault, itemName });
1375
+ });
1376
+ KeyProviderRegistry.register("macos-keychain", (config) => {
1377
+ const { options } = config;
1378
+ const itemName = typeof options.itemName === "string" ? options.itemName : "";
1379
+ if (!itemName) {
1380
+ throw new Error("macOS Keychain provider requires itemName option");
1381
+ }
1382
+ return new MacOSKeychainKeyProvider({ itemName });
1383
+ });
1384
+ var privateKeyRefSchema = z.discriminatedUnion("type", [
1385
+ z.object({
1386
+ type: z.literal("file"),
1387
+ path: z.string().min(1, "File path cannot be empty")
1388
+ }),
1389
+ z.object({
1390
+ type: z.literal("keychain"),
1391
+ service: z.string().min(1, "Service name cannot be empty"),
1392
+ account: z.string().min(1, "Account name cannot be empty")
1393
+ }),
1394
+ z.object({
1395
+ type: z.literal("1password"),
1396
+ account: z.string().optional(),
1397
+ vault: z.string().min(1, "Vault name cannot be empty"),
1398
+ item: z.string().min(1, "Item name cannot be empty"),
1399
+ field: z.string().optional()
1400
+ })
1401
+ ]);
1402
+ var identitySchema = z.object({
1403
+ name: z.string().min(1, "Identity name cannot be empty"),
1404
+ email: z.string().optional(),
1405
+ github: z.string().optional(),
1406
+ publicKey: z.string().min(1, "Public key cannot be empty"),
1407
+ privateKey: privateKeyRefSchema
1408
+ }).strict();
1409
+ var localConfigSchema = z.object({
1410
+ activeIdentity: z.string().min(1, "Active identity name cannot be empty"),
1411
+ identities: z.record(z.string(), identitySchema).refine((identities) => Object.keys(identities).length >= 1, {
1412
+ message: "At least one identity must be defined"
1413
+ })
1414
+ }).strict();
1415
+ var LocalConfigValidationError = class extends Error {
1416
+ constructor(message, issues) {
1417
+ super(message);
1418
+ this.issues = issues;
1419
+ this.name = "LocalConfigValidationError";
1420
+ }
1421
+ };
1422
+ function getLocalConfigPath() {
1423
+ const home = homedir();
1424
+ return join(home, ".config", "attest-it", "config.yaml");
1425
+ }
1426
+ function parseLocalConfigContent(content) {
1427
+ let rawConfig;
1428
+ try {
1429
+ rawConfig = parse(content);
1430
+ } catch (error) {
1431
+ throw new LocalConfigValidationError(
1432
+ `Failed to parse YAML: ${error instanceof Error ? error.message : String(error)}`,
1433
+ []
1434
+ );
1435
+ }
1436
+ const result = localConfigSchema.safeParse(rawConfig);
1437
+ if (!result.success) {
1438
+ throw new LocalConfigValidationError(
1439
+ "Local configuration validation failed:\n" + result.error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n"),
1440
+ result.error.issues
1441
+ );
1442
+ }
1443
+ const identities = Object.fromEntries(
1444
+ Object.entries(result.data.identities).map(([key, identity]) => {
1445
+ let privateKey;
1446
+ if (identity.privateKey.type === "1password") {
1447
+ privateKey = {
1448
+ type: "1password",
1449
+ vault: identity.privateKey.vault,
1450
+ item: identity.privateKey.item,
1451
+ ...identity.privateKey.account !== void 0 && {
1452
+ account: identity.privateKey.account
1453
+ },
1454
+ ...identity.privateKey.field !== void 0 && { field: identity.privateKey.field }
1455
+ };
1456
+ } else {
1457
+ privateKey = identity.privateKey;
1458
+ }
1459
+ return [
1460
+ key,
1461
+ {
1462
+ name: identity.name,
1463
+ publicKey: identity.publicKey,
1464
+ privateKey,
1465
+ ...identity.email !== void 0 && { email: identity.email },
1466
+ ...identity.github !== void 0 && { github: identity.github }
1467
+ }
1468
+ ];
1469
+ })
1470
+ );
1471
+ return {
1472
+ activeIdentity: result.data.activeIdentity,
1473
+ identities
1474
+ };
1475
+ }
1476
+ async function loadLocalConfig(configPath) {
1477
+ const resolvedPath = configPath ?? getLocalConfigPath();
1478
+ try {
1479
+ const content = await readFile(resolvedPath, "utf8");
1480
+ return parseLocalConfigContent(content);
1481
+ } catch (error) {
1482
+ if (error instanceof LocalConfigValidationError) {
1483
+ throw error;
1484
+ }
1485
+ if (error && typeof error === "object" && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
1486
+ return null;
1487
+ }
1488
+ throw error;
1489
+ }
1490
+ }
1491
+ function loadLocalConfigSync(configPath) {
1492
+ const resolvedPath = configPath ?? getLocalConfigPath();
1493
+ try {
1494
+ const content = readFileSync(resolvedPath, "utf8");
1495
+ return parseLocalConfigContent(content);
1496
+ } catch (error) {
1497
+ if (error instanceof LocalConfigValidationError) {
1498
+ throw error;
1499
+ }
1500
+ if (error && typeof error === "object" && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
1501
+ return null;
1502
+ }
1503
+ throw error;
1504
+ }
1505
+ }
1506
+ async function saveLocalConfig(config, configPath) {
1507
+ const resolvedPath = configPath ?? getLocalConfigPath();
1508
+ const content = stringify(config);
1509
+ const dir = dirname(resolvedPath);
1510
+ await mkdir(dir, { recursive: true });
1511
+ await writeFile(resolvedPath, content, "utf8");
1512
+ }
1513
+ function saveLocalConfigSync(config, configPath) {
1514
+ const resolvedPath = configPath ?? getLocalConfigPath();
1515
+ const content = stringify(config);
1516
+ const dir = dirname(resolvedPath);
1517
+ mkdirSync(dir, { recursive: true });
1518
+ writeFileSync(resolvedPath, content, "utf8");
1519
+ }
1520
+ function getActiveIdentity(config) {
1521
+ return config.identities[config.activeIdentity];
1522
+ }
1523
+ function isAuthorizedSigner(config, gateId, publicKey) {
1524
+ const gate = config.gates?.[gateId];
1525
+ if (!gate) {
1526
+ return false;
1527
+ }
1528
+ const teamMember = findTeamMemberByPublicKey(config, publicKey);
1529
+ if (!teamMember) {
1530
+ return false;
1531
+ }
1532
+ const teamMemberSlug = findTeamMemberSlug(config, teamMember);
1533
+ if (!teamMemberSlug) {
1534
+ return false;
1535
+ }
1536
+ return gate.authorizedSigners.includes(teamMemberSlug);
1537
+ }
1538
+ function getAuthorizedSignersForGate(config, gateId) {
1539
+ const gate = config.gates?.[gateId];
1540
+ if (!gate || !config.team) {
1541
+ return [];
1542
+ }
1543
+ const authorizedMembers = [];
1544
+ for (const signerSlug of gate.authorizedSigners) {
1545
+ const member = config.team[signerSlug];
1546
+ if (member) {
1547
+ authorizedMembers.push(member);
1548
+ }
1549
+ }
1550
+ return authorizedMembers;
1551
+ }
1552
+ function findTeamMemberByPublicKey(config, publicKey) {
1553
+ if (!config.team) {
1554
+ return void 0;
1555
+ }
1556
+ for (const member of Object.values(config.team)) {
1557
+ if (member.publicKey === publicKey) {
1558
+ return member;
1559
+ }
1560
+ }
1561
+ return void 0;
1562
+ }
1563
+ function findTeamMemberSlug(config, teamMember) {
1564
+ if (!config.team) {
1565
+ return void 0;
1566
+ }
1567
+ for (const [slug, member] of Object.entries(config.team)) {
1568
+ if (member === teamMember || member.publicKey === teamMember.publicKey) {
1569
+ return slug;
1570
+ }
1571
+ }
1572
+ return void 0;
1573
+ }
1574
+ function getGate(config, gateId) {
1575
+ return config.gates?.[gateId];
1576
+ }
1577
+ var DURATION_PATTERN = /^\d+(\.\d+)?\s*(ms|s|m|h|d|w|y)$/i;
1578
+ function isValidDurationFormat(value) {
1579
+ return DURATION_PATTERN.test(value.trim());
1580
+ }
1581
+ function parseDuration(duration) {
1582
+ if (!isValidDurationFormat(duration)) {
1583
+ throw new Error(`Invalid duration string: ${duration}`);
1584
+ }
1585
+ const result = ms(duration);
1586
+ if (typeof result !== "number" || result <= 0) {
1587
+ throw new Error(`Invalid duration string: ${duration}`);
1588
+ }
1589
+ return result;
1590
+ }
1591
+ var sealSchema = z.object({
1592
+ gateId: z.string().min(1, "Gate ID cannot be empty"),
1593
+ // Fingerprint format: sha256:<hex> where hex is at least 1 character
1594
+ // Full fingerprints are 64 hex chars, but tests may use shorter values
1595
+ fingerprint: z.string().regex(/^sha256:[a-f0-9]+$/i, "Invalid fingerprint format (expected sha256:<hex>)"),
1596
+ timestamp: z.string().datetime({ message: "Invalid ISO 8601 timestamp" }),
1597
+ sealedBy: z.string().min(1, "Signer slug cannot be empty"),
1598
+ signature: z.string().min(1, "Signature cannot be empty")
1599
+ });
1600
+ var sealsFileSchema = z.object({
1601
+ version: z.literal(1, { errorMap: () => ({ message: "Unsupported seals file version" }) }),
1602
+ seals: z.record(z.string(), sealSchema)
1603
+ });
1604
+ function createSeal(options) {
1605
+ const { gateId, fingerprint, sealedBy, privateKey } = options;
1606
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1607
+ const canonicalString = `${gateId}:${fingerprint}:${timestamp}`;
1608
+ const signature = sign3(canonicalString, privateKey);
1609
+ return {
1610
+ gateId,
1611
+ fingerprint,
1612
+ timestamp,
1613
+ sealedBy,
1614
+ signature
1615
+ };
1616
+ }
1617
+ function verifySeal(seal, config) {
1618
+ const { gateId, fingerprint, timestamp, sealedBy, signature } = seal;
1619
+ if (!config.team) {
1620
+ return {
1621
+ valid: false,
1622
+ error: `No team configuration found`
1623
+ };
1624
+ }
1625
+ const teamMember = config.team[sealedBy];
1626
+ if (!teamMember) {
1627
+ return {
1628
+ valid: false,
1629
+ error: `Team member '${sealedBy}' not found in configuration`
1630
+ };
1631
+ }
1632
+ const canonicalString = `${gateId}:${fingerprint}:${timestamp}`;
1633
+ try {
1634
+ const isValid = verify3(canonicalString, signature, teamMember.publicKey);
1635
+ if (!isValid) {
1636
+ return {
1637
+ valid: false,
1638
+ error: "Signature verification failed"
1639
+ };
1640
+ }
1641
+ return { valid: true };
1642
+ } catch (error) {
1643
+ return {
1644
+ valid: false,
1645
+ error: `Signature verification error: ${error instanceof Error ? error.message : String(error)}`
1646
+ };
1647
+ }
1648
+ }
1649
+ function parseSealsContent(content) {
1650
+ let rawData;
1651
+ try {
1652
+ rawData = JSON.parse(content);
1653
+ } catch (error) {
1654
+ throw new Error(`Invalid JSON: ${error instanceof Error ? error.message : String(error)}`);
1655
+ }
1656
+ const result = sealsFileSchema.safeParse(rawData);
1657
+ if (!result.success) {
1658
+ const issues = result.error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n");
1659
+ throw new Error(`Invalid seals file:
1660
+ ${issues}`);
1661
+ }
1662
+ return result.data;
1663
+ }
1664
+ async function readSeals(dir) {
1665
+ const sealsPath = path6.join(dir, ".attest-it", "seals.json");
1666
+ try {
1667
+ const content = await fs.promises.readFile(sealsPath, "utf8");
1668
+ return parseSealsContent(content);
1669
+ } catch (error) {
1670
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
1671
+ return {
1672
+ version: 1,
1673
+ seals: {}
1674
+ };
1675
+ }
1676
+ throw new Error(
1677
+ `Failed to read seals file: ${error instanceof Error ? error.message : String(error)}`
1678
+ );
1679
+ }
1680
+ }
1681
+ function readSealsSync(dir) {
1682
+ const sealsPath = path6.join(dir, ".attest-it", "seals.json");
1683
+ try {
1684
+ const content = fs.readFileSync(sealsPath, "utf8");
1685
+ return parseSealsContent(content);
1686
+ } catch (error) {
1687
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
1688
+ return {
1689
+ version: 1,
1690
+ seals: {}
1691
+ };
1692
+ }
1693
+ throw new Error(
1694
+ `Failed to read seals file: ${error instanceof Error ? error.message : String(error)}`
1695
+ );
1696
+ }
1697
+ }
1698
+ async function writeSeals(dir, sealsFile) {
1699
+ const attestItDir = path6.join(dir, ".attest-it");
1700
+ const sealsPath = path6.join(attestItDir, "seals.json");
1701
+ try {
1702
+ await fs.promises.mkdir(attestItDir, { recursive: true });
1703
+ const content = JSON.stringify(sealsFile, null, 2) + "\n";
1704
+ await fs.promises.writeFile(sealsPath, content, "utf8");
1705
+ } catch (error) {
1706
+ throw new Error(
1707
+ `Failed to write seals file: ${error instanceof Error ? error.message : String(error)}`
1708
+ );
1709
+ }
1710
+ }
1711
+ function writeSealsSync(dir, sealsFile) {
1712
+ const attestItDir = path6.join(dir, ".attest-it");
1713
+ const sealsPath = path6.join(attestItDir, "seals.json");
1714
+ try {
1715
+ fs.mkdirSync(attestItDir, { recursive: true });
1716
+ const content = JSON.stringify(sealsFile, null, 2) + "\n";
1717
+ fs.writeFileSync(sealsPath, content, "utf8");
1718
+ } catch (error) {
1719
+ throw new Error(
1720
+ `Failed to write seals file: ${error instanceof Error ? error.message : String(error)}`
1721
+ );
1722
+ }
1723
+ }
1724
+
1725
+ // src/seal/verification.ts
1726
+ function verifyGateSeal(config, gateId, seals, currentFingerprint) {
1727
+ const gate = getGate(config, gateId);
1728
+ if (!gate) {
1729
+ return {
1730
+ gateId,
1731
+ state: "MISSING",
1732
+ message: `Gate '${gateId}' not found in configuration`
1733
+ };
1734
+ }
1735
+ const seal = seals.seals[gateId];
1736
+ if (!seal) {
1737
+ return {
1738
+ gateId,
1739
+ state: "MISSING",
1740
+ message: `No seal found for gate '${gateId}'`
1741
+ };
1742
+ }
1743
+ if (seal.fingerprint !== currentFingerprint) {
1744
+ return {
1745
+ gateId,
1746
+ state: "FINGERPRINT_MISMATCH",
1747
+ seal,
1748
+ message: `Fingerprint changed since seal was created`
1749
+ };
1750
+ }
1751
+ if (!config.team) {
1752
+ return {
1753
+ gateId,
1754
+ state: "UNKNOWN_SIGNER",
1755
+ seal,
1756
+ message: `No team configuration found`
1757
+ };
1758
+ }
1759
+ const teamMember = config.team[seal.sealedBy];
1760
+ if (!teamMember) {
1761
+ return {
1762
+ gateId,
1763
+ state: "UNKNOWN_SIGNER",
1764
+ seal,
1765
+ message: `Signer '${seal.sealedBy}' not found in team`
1766
+ };
1767
+ }
1768
+ const authorized = isAuthorizedSigner(config, gateId, teamMember.publicKey);
1769
+ if (!authorized) {
1770
+ return {
1771
+ gateId,
1772
+ state: "UNKNOWN_SIGNER",
1773
+ seal,
1774
+ message: `Signer '${seal.sealedBy}' is not authorized for gate '${gateId}'`
1775
+ };
1776
+ }
1777
+ const verificationResult = verifySeal(seal, config);
1778
+ if (!verificationResult.valid) {
1779
+ return {
1780
+ gateId,
1781
+ state: "INVALID_SIGNATURE",
1782
+ seal,
1783
+ message: verificationResult.error ?? "Signature verification failed"
1784
+ };
1785
+ }
1786
+ try {
1787
+ const maxAgeMs = parseDuration(gate.maxAge);
1788
+ const sealTimestamp = new Date(seal.timestamp).getTime();
1789
+ const now = Date.now();
1790
+ const ageMs = now - sealTimestamp;
1791
+ if (ageMs > maxAgeMs) {
1792
+ const ageDays = Math.floor(ageMs / (1e3 * 60 * 60 * 24));
1793
+ const maxAgeDays = Math.floor(maxAgeMs / (1e3 * 60 * 60 * 24));
1794
+ return {
1795
+ gateId,
1796
+ state: "STALE",
1797
+ seal,
1798
+ message: `Seal is ${ageDays.toString()} days old, exceeds maxAge of ${maxAgeDays.toString()} days`
1799
+ };
1800
+ }
1801
+ } catch (error) {
1802
+ return {
1803
+ gateId,
1804
+ state: "STALE",
1805
+ seal,
1806
+ message: `Cannot verify freshness: invalid maxAge format: ${error instanceof Error ? error.message : String(error)}`
1807
+ };
1808
+ }
1809
+ return {
1810
+ gateId,
1811
+ state: "VALID",
1812
+ seal
1813
+ };
1814
+ }
1815
+ function verifyAllSeals(config, seals, fingerprints) {
1816
+ if (!config.gates) {
1817
+ return [];
1818
+ }
1819
+ const results = [];
1820
+ for (const gateId of Object.keys(config.gates)) {
1821
+ const fingerprint = fingerprints[gateId];
1822
+ if (!fingerprint) {
1823
+ results.push({
1824
+ gateId,
1825
+ state: "MISSING",
1826
+ message: `No fingerprint computed for gate '${gateId}'`
1827
+ });
1828
+ continue;
1829
+ }
1830
+ const result = verifyGateSeal(config, gateId, seals, fingerprint);
1831
+ results.push(result);
1832
+ }
1833
+ return results;
621
1834
  }
622
1835
 
623
1836
  // src/index.ts
624
1837
  var version = "0.0.0";
625
1838
 
626
- 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 };
1839
+ export { ConfigNotFoundError, ConfigValidationError, FilesystemKeyProvider, KeyProviderRegistry, LocalConfigValidationError, MacOSKeychainKeyProvider, OnePasswordKeyProvider, SignatureInvalidError, canonicalizeAttestations, computeFingerprint, computeFingerprintSync, createAttestation, createSeal, findAttestation, findConfigPath, findTeamMemberByPublicKey, generateKeyPair2 as generateEd25519KeyPair, getActiveIdentity, getAuthorizedSignersForGate, getGate, getLocalConfigPath, getPublicKeyFromPrivate, isAuthorizedSigner, listPackageFiles, loadConfig, loadConfigSync, loadLocalConfig, loadLocalConfigSync, parseDuration, readAndVerifyAttestations, readAttestations, readAttestationsSync, readSeals, readSealsSync, removeAttestation, resolveConfigPaths, saveLocalConfig, saveLocalConfigSync, sign3 as signEd25519, toAttestItConfig, upsertAttestation, verifyAllSeals, verifyAttestations, verify3 as verifyEd25519, verifyGateSeal, verifySeal, version, writeAttestations, writeAttestationsSync, writeSeals, writeSealsSync, writeSignedAttestations };
627
1840
  //# sourceMappingURL=index.js.map
628
1841
  //# sourceMappingURL=index.js.map