@attest-it/core 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -5,12 +5,15 @@ var fs2 = require('fs/promises');
5
5
  var path2 = require('path');
6
6
  var os = require('os');
7
7
  var fs = require('fs');
8
+ var ms = require('ms');
8
9
  var yaml = require('yaml');
9
10
  var zod = require('zod');
10
- var crypto = require('crypto');
11
+ var crypto2 = require('crypto');
11
12
  var tinyglobby = require('tinyglobby');
12
13
  var canonicalizeNamespace = require('canonicalize');
13
14
 
15
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
16
+
14
17
  function _interopNamespace(e) {
15
18
  if (e && e.__esModule) return e;
16
19
  var n = Object.create(null);
@@ -33,7 +36,8 @@ var fs2__namespace = /*#__PURE__*/_interopNamespace(fs2);
33
36
  var path2__namespace = /*#__PURE__*/_interopNamespace(path2);
34
37
  var os__namespace = /*#__PURE__*/_interopNamespace(os);
35
38
  var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
36
- var crypto__namespace = /*#__PURE__*/_interopNamespace(crypto);
39
+ var ms__default = /*#__PURE__*/_interopDefault(ms);
40
+ var crypto2__namespace = /*#__PURE__*/_interopNamespace(crypto2);
37
41
  var canonicalizeNamespace__namespace = /*#__PURE__*/_interopNamespace(canonicalizeNamespace);
38
42
 
39
43
  var __defProp = Object.defineProperty;
@@ -188,28 +192,47 @@ async function generateKeyPair(options = {}) {
188
192
  }
189
193
  async function sign(options) {
190
194
  await ensureOpenSSLAvailable();
191
- const { privateKeyPath, data } = options;
192
- if (!await fileExists(privateKeyPath)) {
193
- throw new Error(`Private key not found: ${privateKeyPath}`);
195
+ const { privateKeyPath, keyProvider, keyRef, data } = options;
196
+ let effectiveKeyPath;
197
+ let cleanup;
198
+ if (keyProvider && keyRef) {
199
+ const result = await keyProvider.getPrivateKey(keyRef);
200
+ effectiveKeyPath = result.keyPath;
201
+ cleanup = result.cleanup;
202
+ } else if (privateKeyPath) {
203
+ effectiveKeyPath = privateKeyPath;
204
+ } else {
205
+ throw new Error(
206
+ "Either privateKeyPath or both keyProvider and keyRef must be provided for signing"
207
+ );
194
208
  }
195
- const dataBuffer = typeof data === "string" ? Buffer.from(data, "utf8") : data;
196
- const processBuffer = dataBuffer.length === 0 ? Buffer.from([0]) : dataBuffer;
197
- const tmpDir = await fs2__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-"));
198
- const dataFile = path2__namespace.join(tmpDir, "data.bin");
199
- const sigFile = path2__namespace.join(tmpDir, "sig.bin");
200
209
  try {
201
- await fs2__namespace.writeFile(dataFile, processBuffer);
202
- const signArgs = ["dgst", "-sha256", "-sign", privateKeyPath, "-out", sigFile, dataFile];
203
- const result = await runOpenSSL(signArgs);
204
- if (result.exitCode !== 0) {
205
- throw new Error(`Failed to sign data: ${result.stderr}`);
210
+ if (!await fileExists(effectiveKeyPath)) {
211
+ throw new Error(`Private key not found: ${effectiveKeyPath}`);
206
212
  }
207
- const sigBuffer = await fs2__namespace.readFile(sigFile);
208
- return sigBuffer.toString("base64");
209
- } finally {
213
+ const dataBuffer = typeof data === "string" ? Buffer.from(data, "utf8") : data;
214
+ const processBuffer = dataBuffer.length === 0 ? Buffer.from([0]) : dataBuffer;
215
+ const tmpDir = await fs2__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-"));
216
+ const dataFile = path2__namespace.join(tmpDir, "data.bin");
217
+ const sigFile = path2__namespace.join(tmpDir, "sig.bin");
210
218
  try {
211
- await fs2__namespace.rm(tmpDir, { recursive: true, force: true });
212
- } catch {
219
+ await fs2__namespace.writeFile(dataFile, processBuffer);
220
+ const signArgs = ["dgst", "-sha256", "-sign", effectiveKeyPath, "-out", sigFile, dataFile];
221
+ const result = await runOpenSSL(signArgs);
222
+ if (result.exitCode !== 0) {
223
+ throw new Error(`Failed to sign data: ${result.stderr}`);
224
+ }
225
+ const sigBuffer = await fs2__namespace.readFile(sigFile);
226
+ return sigBuffer.toString("base64");
227
+ } finally {
228
+ try {
229
+ await fs2__namespace.rm(tmpDir, { recursive: true, force: true });
230
+ } catch {
231
+ }
232
+ }
233
+ } finally {
234
+ if (cleanup) {
235
+ await cleanup();
213
236
  }
214
237
  }
215
238
  }
@@ -259,25 +282,82 @@ var init_crypto = __esm({
259
282
  openSSLChecked = false;
260
283
  }
261
284
  });
285
+ var keyProviderOptionsSchema = zod.z.object({
286
+ privateKeyPath: zod.z.string().optional(),
287
+ account: zod.z.string().optional(),
288
+ vault: zod.z.string().optional(),
289
+ itemName: zod.z.string().optional()
290
+ }).strict();
291
+ var keyProviderSchema = zod.z.object({
292
+ type: zod.z.enum(["filesystem", "1password"]).or(zod.z.string()),
293
+ options: keyProviderOptionsSchema.optional()
294
+ }).strict();
295
+ var teamMemberSchema = zod.z.object({
296
+ name: zod.z.string().min(1, "Team member name cannot be empty"),
297
+ email: zod.z.string().email().optional(),
298
+ github: zod.z.string().min(1).optional(),
299
+ publicKey: zod.z.string().min(1, "Public key is required")
300
+ }).strict();
301
+ var fingerprintConfigSchema = zod.z.object({
302
+ paths: zod.z.array(zod.z.string().min(1, "Path cannot be empty")).min(1, "At least one path is required"),
303
+ exclude: zod.z.array(zod.z.string().min(1, "Exclude pattern cannot be empty")).optional()
304
+ }).strict();
305
+ var durationSchema = zod.z.string().refine(
306
+ (val) => {
307
+ try {
308
+ const parsed = ms__default.default(val);
309
+ return typeof parsed === "number" && parsed > 0;
310
+ } catch {
311
+ return false;
312
+ }
313
+ },
314
+ {
315
+ message: 'Duration must be a valid duration string (e.g., "30d", "7d", "24h")'
316
+ }
317
+ );
318
+ var gateSchema = zod.z.object({
319
+ name: zod.z.string().min(1, "Gate name cannot be empty"),
320
+ description: zod.z.string().min(1, "Gate description cannot be empty"),
321
+ authorizedSigners: zod.z.array(zod.z.string().min(1, "Authorized signer slug cannot be empty")).min(1, "At least one authorized signer is required"),
322
+ fingerprint: fingerprintConfigSchema,
323
+ maxAge: durationSchema
324
+ }).strict();
262
325
  var settingsSchema = zod.z.object({
263
326
  maxAgeDays: zod.z.number().int().positive().default(30),
264
327
  publicKeyPath: zod.z.string().default(".attest-it/pubkey.pem"),
265
328
  attestationsPath: zod.z.string().default(".attest-it/attestations.json"),
266
- defaultCommand: zod.z.string().optional()
329
+ defaultCommand: zod.z.string().optional(),
330
+ keyProvider: keyProviderSchema.optional()
267
331
  // Note: algorithm field was removed - RSA is the only supported algorithm
268
332
  }).passthrough();
269
333
  var suiteSchema = zod.z.object({
334
+ // Gate fields (if present, this suite references a gate)
335
+ gate: zod.z.string().optional(),
336
+ // Legacy fingerprint definition (for backward compatibility)
270
337
  description: zod.z.string().optional(),
271
- packages: zod.z.array(zod.z.string().min(1, "Package path cannot be empty")).min(1, "At least one package pattern is required"),
338
+ packages: zod.z.array(zod.z.string().min(1, "Package path cannot be empty")).optional(),
272
339
  files: zod.z.array(zod.z.string().min(1, "File path cannot be empty")).optional(),
273
340
  ignore: zod.z.array(zod.z.string().min(1, "Ignore pattern cannot be empty")).optional(),
341
+ // CLI-specific fields
274
342
  command: zod.z.string().optional(),
343
+ timeout: zod.z.string().optional(),
344
+ interactive: zod.z.boolean().optional(),
345
+ // Relationship fields
275
346
  invalidates: zod.z.array(zod.z.string().min(1, "Invalidated suite name cannot be empty")).optional(),
276
347
  depends_on: zod.z.array(zod.z.string().min(1, "Dependency suite name cannot be empty")).optional()
277
- }).strict();
348
+ }).strict().refine(
349
+ (suite) => {
350
+ return suite.gate !== void 0 || suite.packages !== void 0 && suite.packages.length > 0;
351
+ },
352
+ {
353
+ message: "Suite must either reference a gate or define packages for fingerprinting"
354
+ }
355
+ );
278
356
  var configSchema = zod.z.object({
279
357
  version: zod.z.literal(1),
280
358
  settings: settingsSchema.default({}),
359
+ team: zod.z.record(zod.z.string(), teamMemberSchema).optional(),
360
+ gates: zod.z.record(zod.z.string(), gateSchema).optional(),
281
361
  suites: zod.z.record(zod.z.string(), suiteSchema).refine((suites) => Object.keys(suites).length >= 1, {
282
362
  message: "At least one suite must be defined"
283
363
  }),
@@ -394,32 +474,52 @@ function resolveConfigPaths(config, repoRoot) {
394
474
  };
395
475
  }
396
476
  function toAttestItConfig(config) {
397
- return {
477
+ const result = {
398
478
  version: config.version,
399
479
  settings: {
400
480
  maxAgeDays: config.settings.maxAgeDays,
401
481
  publicKeyPath: config.settings.publicKeyPath,
402
- attestationsPath: config.settings.attestationsPath,
403
- ...config.settings.defaultCommand !== void 0 && {
404
- defaultCommand: config.settings.defaultCommand
405
- }
482
+ attestationsPath: config.settings.attestationsPath
406
483
  },
407
- suites: Object.fromEntries(
408
- Object.entries(config.suites).map(([name, suite]) => [
409
- name,
410
- {
411
- packages: suite.packages,
412
- ...suite.description !== void 0 && { description: suite.description },
413
- ...suite.files !== void 0 && { files: suite.files },
414
- ...suite.ignore !== void 0 && { ignore: suite.ignore },
415
- ...suite.command !== void 0 && { command: suite.command },
416
- ...suite.invalidates !== void 0 && { invalidates: suite.invalidates },
417
- ...suite.depends_on !== void 0 && { depends_on: suite.depends_on }
418
- }
419
- ])
420
- ),
421
- ...config.groups !== void 0 && { groups: config.groups }
484
+ suites: {}
422
485
  };
486
+ if (config.settings.defaultCommand !== void 0) {
487
+ result.settings.defaultCommand = config.settings.defaultCommand;
488
+ }
489
+ if (config.settings.keyProvider !== void 0) {
490
+ result.settings.keyProvider = {
491
+ type: config.settings.keyProvider.type,
492
+ ...config.settings.keyProvider.options !== void 0 && {
493
+ options: config.settings.keyProvider.options
494
+ }
495
+ };
496
+ }
497
+ if (config.team !== void 0) {
498
+ result.team = config.team;
499
+ }
500
+ if (config.gates !== void 0) {
501
+ result.gates = config.gates;
502
+ }
503
+ if (config.groups !== void 0) {
504
+ result.groups = config.groups;
505
+ }
506
+ result.suites = Object.fromEntries(
507
+ Object.entries(config.suites).map(([name, suite]) => {
508
+ const mappedSuite = {};
509
+ if (suite.gate !== void 0) mappedSuite.gate = suite.gate;
510
+ if (suite.packages !== void 0) mappedSuite.packages = suite.packages;
511
+ if (suite.description !== void 0) mappedSuite.description = suite.description;
512
+ if (suite.files !== void 0) mappedSuite.files = suite.files;
513
+ if (suite.ignore !== void 0) mappedSuite.ignore = suite.ignore;
514
+ if (suite.command !== void 0) mappedSuite.command = suite.command;
515
+ if (suite.timeout !== void 0) mappedSuite.timeout = suite.timeout;
516
+ if (suite.interactive !== void 0) mappedSuite.interactive = suite.interactive;
517
+ if (suite.invalidates !== void 0) mappedSuite.invalidates = suite.invalidates;
518
+ if (suite.depends_on !== void 0) mappedSuite.depends_on = suite.depends_on;
519
+ return [name, mappedSuite];
520
+ })
521
+ );
522
+ return result;
423
523
  }
424
524
  var LARGE_FILE_THRESHOLD = 50 * 1024 * 1024;
425
525
  function sortFiles(files) {
@@ -440,15 +540,15 @@ function computeFinalFingerprint(fileHashes) {
440
540
  });
441
541
  const hashes = sorted.map((input) => input.hash);
442
542
  const concatenated = Buffer.concat(hashes);
443
- const finalHash = crypto__namespace.createHash("sha256").update(concatenated).digest();
543
+ const finalHash = crypto2__namespace.createHash("sha256").update(concatenated).digest();
444
544
  return `sha256:${finalHash.toString("hex")}`;
445
545
  }
446
546
  async function hashFileAsync(realPath, normalizedPath, stats) {
447
547
  if (stats.size > LARGE_FILE_THRESHOLD) {
448
548
  return new Promise((resolve3, reject) => {
449
- const hash2 = crypto__namespace.createHash("sha256");
549
+ const hash2 = crypto2__namespace.createHash("sha256");
450
550
  hash2.update(normalizedPath);
451
- hash2.update("\0");
551
+ hash2.update(":");
452
552
  const stream = fs__namespace.createReadStream(realPath);
453
553
  stream.on("data", (chunk) => {
454
554
  hash2.update(chunk);
@@ -460,17 +560,17 @@ async function hashFileAsync(realPath, normalizedPath, stats) {
460
560
  });
461
561
  }
462
562
  const content = await fs__namespace.promises.readFile(realPath);
463
- const hash = crypto__namespace.createHash("sha256");
563
+ const hash = crypto2__namespace.createHash("sha256");
464
564
  hash.update(normalizedPath);
465
- hash.update("\0");
565
+ hash.update(":");
466
566
  hash.update(content);
467
567
  return hash.digest();
468
568
  }
469
569
  function hashFileSync(realPath, normalizedPath) {
470
570
  const content = fs__namespace.readFileSync(realPath);
471
- const hash = crypto__namespace.createHash("sha256");
571
+ const hash = crypto2__namespace.createHash("sha256");
472
572
  hash.update(normalizedPath);
473
- hash.update("\0");
573
+ hash.update(":");
474
574
  hash.update(content);
475
575
  return hash.digest();
476
576
  }
@@ -718,22 +818,37 @@ function createAttestation(params) {
718
818
  return attestation;
719
819
  }
720
820
  async function writeSignedAttestations(options) {
721
- const { sign: sign2 } = await Promise.resolve().then(() => (init_crypto(), crypto_exports));
821
+ const { sign: sign4 } = await Promise.resolve().then(() => (init_crypto(), crypto_exports));
822
+ const { privateKeyPath, keyProvider, keyRef } = options;
823
+ if (!privateKeyPath && (!keyProvider || !keyRef)) {
824
+ throw new Error(
825
+ "Either privateKeyPath or both keyProvider and keyRef must be provided for signing"
826
+ );
827
+ }
722
828
  const canonical = canonicalizeAttestations(options.attestations);
723
- const signature = await sign2({
724
- privateKeyPath: options.privateKeyPath,
829
+ const signOptions = {
725
830
  data: canonical
726
- });
831
+ };
832
+ if (privateKeyPath !== void 0) {
833
+ signOptions.privateKeyPath = privateKeyPath;
834
+ }
835
+ if (keyProvider !== void 0) {
836
+ signOptions.keyProvider = keyProvider;
837
+ }
838
+ if (keyRef !== void 0) {
839
+ signOptions.keyRef = keyRef;
840
+ }
841
+ const signature = await sign4(signOptions);
727
842
  await writeAttestations(options.filePath, options.attestations, signature);
728
843
  }
729
844
  async function readAndVerifyAttestations(options) {
730
- const { verify: verify2 } = await Promise.resolve().then(() => (init_crypto(), crypto_exports));
845
+ const { verify: verify4 } = await Promise.resolve().then(() => (init_crypto(), crypto_exports));
731
846
  const file = await readAttestations(options.filePath);
732
847
  if (!file) {
733
848
  throw new Error(`Attestations file not found: ${options.filePath}`);
734
849
  }
735
850
  const canonical = canonicalizeAttestations(file.attestations);
736
- const isValid = await verify2({
851
+ const isValid = await verify4({
737
852
  publicKeyPath: options.publicKeyPath,
738
853
  data: canonical,
739
854
  signature: file.signature
@@ -756,6 +871,123 @@ var SignatureInvalidError = class extends Error {
756
871
 
757
872
  // src/index.ts
758
873
  init_crypto();
874
+ function isBuffer(value) {
875
+ return Buffer.isBuffer(value);
876
+ }
877
+ function generateKeyPair2() {
878
+ try {
879
+ const keyPair = crypto2__namespace.generateKeyPairSync("ed25519", {
880
+ publicKeyEncoding: {
881
+ type: "spki",
882
+ format: "pem"
883
+ },
884
+ privateKeyEncoding: {
885
+ type: "pkcs8",
886
+ format: "pem"
887
+ }
888
+ });
889
+ const { publicKey, privateKey } = keyPair;
890
+ if (typeof publicKey !== "string" || typeof privateKey !== "string") {
891
+ throw new Error("Expected keypair to have string keys");
892
+ }
893
+ const publicKeyObj = crypto2__namespace.createPublicKey(publicKey);
894
+ const publicKeyExport = publicKeyObj.export({
895
+ type: "spki",
896
+ format: "der"
897
+ });
898
+ if (!isBuffer(publicKeyExport)) {
899
+ throw new Error("Expected public key export to be a Buffer");
900
+ }
901
+ const rawPublicKey = publicKeyExport.subarray(12);
902
+ const publicKeyBase64 = rawPublicKey.toString("base64");
903
+ return {
904
+ publicKey: publicKeyBase64,
905
+ privateKey
906
+ };
907
+ } catch (err) {
908
+ throw new Error(
909
+ `Failed to generate Ed25519 keypair: ${err instanceof Error ? err.message : String(err)}`
910
+ );
911
+ }
912
+ }
913
+ function sign3(data, privateKeyPem) {
914
+ try {
915
+ const dataBuffer = typeof data === "string" ? Buffer.from(data, "utf8") : data;
916
+ const privateKeyObj = crypto2__namespace.createPrivateKey(privateKeyPem);
917
+ const signatureResult = crypto2__namespace.sign(null, dataBuffer, privateKeyObj);
918
+ if (!isBuffer(signatureResult)) {
919
+ throw new Error("Expected signature to be a Buffer");
920
+ }
921
+ return signatureResult.toString("base64");
922
+ } catch (err) {
923
+ throw new Error(
924
+ `Failed to sign data with Ed25519: ${err instanceof Error ? err.message : String(err)}`
925
+ );
926
+ }
927
+ }
928
+ function verify3(data, signature, publicKeyBase64) {
929
+ try {
930
+ const dataBuffer = typeof data === "string" ? Buffer.from(data, "utf8") : data;
931
+ const signatureBuffer = Buffer.from(signature, "base64");
932
+ const rawPublicKey = Buffer.from(publicKeyBase64, "base64");
933
+ if (rawPublicKey.length !== 32) {
934
+ throw new Error(
935
+ `Invalid Ed25519 public key length: expected 32 bytes, got ${rawPublicKey.length.toString()}`
936
+ );
937
+ }
938
+ const spkiHeader = Buffer.from([
939
+ 48,
940
+ 42,
941
+ // SEQUENCE, 42 bytes
942
+ 48,
943
+ 5,
944
+ // SEQUENCE, 5 bytes
945
+ 6,
946
+ 3,
947
+ 43,
948
+ 101,
949
+ 112,
950
+ // OID 1.3.101.112 (Ed25519)
951
+ 3,
952
+ 33,
953
+ 0
954
+ // BIT STRING, 33 bytes (32 key + 1 padding)
955
+ ]);
956
+ const spkiBuffer = Buffer.concat([spkiHeader, rawPublicKey]);
957
+ const publicKeyObj = crypto2__namespace.createPublicKey({
958
+ key: spkiBuffer,
959
+ format: "der",
960
+ type: "spki"
961
+ });
962
+ return crypto2__namespace.verify(null, dataBuffer, publicKeyObj, signatureBuffer);
963
+ } catch (err) {
964
+ if (err instanceof Error && err.message.includes("verification failed")) {
965
+ return false;
966
+ }
967
+ throw new Error(
968
+ `Failed to verify Ed25519 signature: ${err instanceof Error ? err.message : String(err)}`
969
+ );
970
+ }
971
+ }
972
+ function getPublicKeyFromPrivate(privateKeyPem) {
973
+ try {
974
+ const privateKeyObj = crypto2__namespace.createPrivateKey(privateKeyPem);
975
+ const publicKeyObj = crypto2__namespace.createPublicKey(privateKeyObj);
976
+ const publicKeyExport = publicKeyObj.export({
977
+ type: "spki",
978
+ format: "der"
979
+ });
980
+ if (!isBuffer(publicKeyExport)) {
981
+ throw new Error("Expected public key export to be a Buffer");
982
+ }
983
+ const rawPublicKey = publicKeyExport.subarray(12);
984
+ return rawPublicKey.toString("base64");
985
+ } catch (err) {
986
+ throw new Error(
987
+ `Failed to extract public key from Ed25519 private key: ${err instanceof Error ? err.message : String(err)}`
988
+ );
989
+ }
990
+ }
759
991
  async function verifyAttestations(options) {
760
992
  const { config, repoRoot = process.cwd() } = options;
761
993
  const errors = [];
@@ -806,6 +1038,14 @@ async function verifyAttestations(options) {
806
1038
  }
807
1039
  async function verifySuite(options) {
808
1040
  const { suiteName, suiteConfig, attestations, maxAgeDays, repoRoot } = options;
1041
+ if (!suiteConfig.packages || suiteConfig.packages.length === 0) {
1042
+ return {
1043
+ suite: suiteName,
1044
+ status: "NEEDS_ATTESTATION",
1045
+ fingerprint: "",
1046
+ message: "Suite configuration missing packages field"
1047
+ };
1048
+ }
809
1049
  const fingerprintOptions = {
810
1050
  packages: suiteConfig.packages.map((p) => resolvePath(p, repoRoot)),
811
1051
  baseDir: repoRoot,
@@ -875,39 +1115,1063 @@ function resolvePath(relativePath, baseDir) {
875
1115
  return path2__namespace.join(baseDir, relativePath);
876
1116
  }
877
1117
 
1118
+ // src/key-provider/filesystem-provider.ts
1119
+ init_crypto();
1120
+ var FilesystemKeyProvider = class {
1121
+ type = "filesystem";
1122
+ displayName = "Filesystem";
1123
+ privateKeyPath;
1124
+ /**
1125
+ * Create a new FilesystemKeyProvider.
1126
+ * @param options - Provider options
1127
+ */
1128
+ constructor(options = {}) {
1129
+ this.privateKeyPath = options.privateKeyPath ?? getDefaultPrivateKeyPath();
1130
+ }
1131
+ /**
1132
+ * Check if this provider is available.
1133
+ * Filesystem provider is always available.
1134
+ */
1135
+ async isAvailable() {
1136
+ return Promise.resolve(true);
1137
+ }
1138
+ /**
1139
+ * Check if a key exists at the given path.
1140
+ * @param keyRef - Path to the private key file
1141
+ */
1142
+ async keyExists(keyRef) {
1143
+ try {
1144
+ await fs2__namespace.access(keyRef);
1145
+ return true;
1146
+ } catch {
1147
+ return false;
1148
+ }
1149
+ }
1150
+ /**
1151
+ * Get the private key path for signing.
1152
+ * Returns the path directly with a no-op cleanup function.
1153
+ * @param keyRef - Path to the private key file
1154
+ */
1155
+ async getPrivateKey(keyRef) {
1156
+ if (!await this.keyExists(keyRef)) {
1157
+ throw new Error(`Private key not found: ${keyRef}`);
1158
+ }
1159
+ return {
1160
+ keyPath: keyRef,
1161
+ // No-op cleanup for filesystem provider
1162
+ cleanup: async () => {
1163
+ }
1164
+ };
1165
+ }
1166
+ /**
1167
+ * Generate a new keypair and store on filesystem.
1168
+ * @param options - Key generation options
1169
+ */
1170
+ async generateKeyPair(options) {
1171
+ const { publicKeyPath, force = false } = options;
1172
+ const result = await generateKeyPair({
1173
+ privatePath: this.privateKeyPath,
1174
+ publicPath: publicKeyPath,
1175
+ force
1176
+ });
1177
+ return {
1178
+ privateKeyRef: result.privatePath,
1179
+ publicKeyPath: result.publicPath,
1180
+ storageDescription: `Filesystem: ${result.privatePath}`
1181
+ };
1182
+ }
1183
+ /**
1184
+ * Get the configuration for this provider.
1185
+ */
1186
+ getConfig() {
1187
+ return {
1188
+ type: this.type,
1189
+ options: {
1190
+ privateKeyPath: this.privateKeyPath
1191
+ }
1192
+ };
1193
+ }
1194
+ };
1195
+
1196
+ // src/key-provider/one-password-provider.ts
1197
+ init_crypto();
1198
+ var OnePasswordKeyProvider = class _OnePasswordKeyProvider {
1199
+ type = "1password";
1200
+ displayName = "1Password";
1201
+ account;
1202
+ vault;
1203
+ itemName;
1204
+ /**
1205
+ * Create a new OnePasswordKeyProvider.
1206
+ * @param options - Provider options
1207
+ */
1208
+ constructor(options) {
1209
+ if (options.account !== void 0) {
1210
+ this.account = options.account;
1211
+ }
1212
+ this.vault = options.vault;
1213
+ this.itemName = options.itemName;
1214
+ }
1215
+ /**
1216
+ * Check if the 1Password CLI is installed.
1217
+ * @returns True if `op` command is available
1218
+ */
1219
+ static async isInstalled() {
1220
+ try {
1221
+ await execCommand("op", ["--version"]);
1222
+ return true;
1223
+ } catch {
1224
+ return false;
1225
+ }
1226
+ }
1227
+ /**
1228
+ * List all 1Password accounts.
1229
+ * @returns Array of account information
1230
+ */
1231
+ static async listAccounts() {
1232
+ try {
1233
+ const output = await execCommand("op", ["account", "list", "--format=json"]);
1234
+ const parsed = JSON.parse(output);
1235
+ if (!Array.isArray(parsed)) {
1236
+ return [];
1237
+ }
1238
+ return parsed;
1239
+ } catch (error) {
1240
+ if (process.env.NODE_ENV !== "production") {
1241
+ console.error("Failed to list 1Password accounts:", error);
1242
+ }
1243
+ return [];
1244
+ }
1245
+ }
1246
+ /**
1247
+ * List vaults in a specific account.
1248
+ * @param account - Account email (optional if only one account)
1249
+ * @returns Array of vault information
1250
+ */
1251
+ static async listVaults(account) {
1252
+ try {
1253
+ const args = ["vault", "list", "--format=json"];
1254
+ if (account) {
1255
+ args.push("--account", account);
1256
+ }
1257
+ const output = await execCommand("op", args);
1258
+ const parsed = JSON.parse(output);
1259
+ if (!Array.isArray(parsed)) {
1260
+ return [];
1261
+ }
1262
+ return parsed;
1263
+ } catch (error) {
1264
+ if (process.env.NODE_ENV !== "production") {
1265
+ console.error("Failed to list 1Password vaults:", error);
1266
+ }
1267
+ return [];
1268
+ }
1269
+ }
1270
+ /**
1271
+ * Check if this provider is available.
1272
+ * Requires `op` CLI to be installed and authenticated.
1273
+ */
1274
+ async isAvailable() {
1275
+ return _OnePasswordKeyProvider.isInstalled();
1276
+ }
1277
+ /**
1278
+ * Check if a key exists in 1Password.
1279
+ * @param keyRef - Item name in 1Password
1280
+ */
1281
+ async keyExists(keyRef) {
1282
+ try {
1283
+ const args = ["item", "get", keyRef, "--vault", this.vault, "--format=json"];
1284
+ if (this.account) {
1285
+ args.push("--account", this.account);
1286
+ }
1287
+ await execCommand("op", args);
1288
+ return true;
1289
+ } catch {
1290
+ return false;
1291
+ }
1292
+ }
1293
+ /**
1294
+ * Get the private key from 1Password for signing.
1295
+ * Downloads to a temporary file and returns a cleanup function.
1296
+ * @param keyRef - Item name in 1Password
1297
+ * @throws Error if the key does not exist in 1Password
1298
+ */
1299
+ async getPrivateKey(keyRef) {
1300
+ if (!await this.keyExists(keyRef)) {
1301
+ throw new Error(
1302
+ `Key not found in 1Password: "${keyRef}" (vault: ${this.vault})` + (this.account ? ` (account: ${this.account})` : "")
1303
+ );
1304
+ }
1305
+ const tempDir = await fs2__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-"));
1306
+ const tempKeyPath = path2__namespace.join(tempDir, "private.pem");
1307
+ try {
1308
+ const args = ["document", "get", keyRef, "--vault", this.vault, "--out-file", tempKeyPath];
1309
+ if (this.account) {
1310
+ args.push("--account", this.account);
1311
+ }
1312
+ await execCommand("op", args);
1313
+ await setKeyPermissions(tempKeyPath);
1314
+ return {
1315
+ keyPath: tempKeyPath,
1316
+ cleanup: async () => {
1317
+ try {
1318
+ await fs2__namespace.unlink(tempKeyPath);
1319
+ await fs2__namespace.rmdir(tempDir);
1320
+ } catch (cleanupError) {
1321
+ console.warn(
1322
+ `Warning: Failed to clean up temporary key file at ${tempKeyPath}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
1323
+ );
1324
+ }
1325
+ }
1326
+ };
1327
+ } catch (error) {
1328
+ try {
1329
+ await fs2__namespace.rm(tempDir, { recursive: true, force: true });
1330
+ } catch (cleanupError) {
1331
+ console.warn(
1332
+ `Warning: Failed to clean up temporary key directory at ${tempDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
1333
+ );
1334
+ }
1335
+ throw error;
1336
+ }
1337
+ }
1338
+ /**
1339
+ * Generate a new keypair and store private key in 1Password.
1340
+ * Public key is written to filesystem for repository commit.
1341
+ * @param options - Key generation options
1342
+ */
1343
+ async generateKeyPair(options) {
1344
+ const { publicKeyPath, force = false } = options;
1345
+ const tempDir = await fs2__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-keygen-"));
1346
+ const tempPrivateKeyPath = path2__namespace.join(tempDir, "private.pem");
1347
+ try {
1348
+ await generateKeyPair({
1349
+ privatePath: tempPrivateKeyPath,
1350
+ publicPath: publicKeyPath,
1351
+ force
1352
+ });
1353
+ const args = [
1354
+ "document",
1355
+ "create",
1356
+ tempPrivateKeyPath,
1357
+ "--title",
1358
+ this.itemName,
1359
+ "--vault",
1360
+ this.vault
1361
+ ];
1362
+ if (this.account) {
1363
+ args.push("--account", this.account);
1364
+ }
1365
+ await execCommand("op", args);
1366
+ await fs2__namespace.unlink(tempPrivateKeyPath);
1367
+ await fs2__namespace.rmdir(tempDir);
1368
+ return {
1369
+ privateKeyRef: this.itemName,
1370
+ publicKeyPath,
1371
+ storageDescription: `1Password: ${this.vault}/${this.itemName}`
1372
+ };
1373
+ } catch (error) {
1374
+ try {
1375
+ await fs2__namespace.rm(tempDir, { recursive: true, force: true });
1376
+ } catch (cleanupError) {
1377
+ console.warn(
1378
+ `Warning: Failed to clean up temporary key directory at ${tempDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
1379
+ );
1380
+ }
1381
+ throw error;
1382
+ }
1383
+ }
1384
+ /**
1385
+ * Get the configuration for this provider.
1386
+ */
1387
+ getConfig() {
1388
+ return {
1389
+ type: this.type,
1390
+ options: {
1391
+ ...this.account && { account: this.account },
1392
+ vault: this.vault,
1393
+ itemName: this.itemName
1394
+ }
1395
+ };
1396
+ }
1397
+ };
1398
+ async function execCommand(command, args) {
1399
+ return new Promise((resolve3, reject) => {
1400
+ const proc = child_process.spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
1401
+ let stdout = "";
1402
+ let stderr = "";
1403
+ proc.stdout.on("data", (data) => {
1404
+ stdout += data.toString();
1405
+ });
1406
+ proc.stderr.on("data", (data) => {
1407
+ stderr += data.toString();
1408
+ });
1409
+ proc.on("close", (code) => {
1410
+ if (code === 0) {
1411
+ resolve3(stdout.trim());
1412
+ } else {
1413
+ reject(new Error(`Command failed with exit code ${String(code)}: ${stderr}`));
1414
+ }
1415
+ });
1416
+ proc.on("error", (error) => {
1417
+ reject(error);
1418
+ });
1419
+ });
1420
+ }
1421
+
1422
+ // src/key-provider/macos-keychain-provider.ts
1423
+ init_crypto();
1424
+ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
1425
+ type = "macos-keychain";
1426
+ displayName = "macOS Keychain";
1427
+ itemName;
1428
+ static ACCOUNT = "attest-it";
1429
+ /**
1430
+ * Create a new MacOSKeychainKeyProvider.
1431
+ * @param options - Provider options
1432
+ */
1433
+ constructor(options) {
1434
+ this.itemName = options.itemName;
1435
+ }
1436
+ /**
1437
+ * Check if this provider is available.
1438
+ * Only available on macOS platforms.
1439
+ */
1440
+ static isAvailable() {
1441
+ return process.platform === "darwin";
1442
+ }
1443
+ /**
1444
+ * Check if this provider is available on the current system.
1445
+ */
1446
+ isAvailable() {
1447
+ return Promise.resolve(_MacOSKeychainKeyProvider.isAvailable());
1448
+ }
1449
+ /**
1450
+ * Check if a key exists in the keychain.
1451
+ * @param keyRef - Item name in keychain
1452
+ */
1453
+ async keyExists(keyRef) {
1454
+ try {
1455
+ await execCommand2("security", [
1456
+ "find-generic-password",
1457
+ "-a",
1458
+ _MacOSKeychainKeyProvider.ACCOUNT,
1459
+ "-s",
1460
+ keyRef
1461
+ ]);
1462
+ return true;
1463
+ } catch {
1464
+ return false;
1465
+ }
1466
+ }
1467
+ /**
1468
+ * Get the private key from keychain for signing.
1469
+ * Downloads to a temporary file and returns a cleanup function.
1470
+ * @param keyRef - Item name in keychain
1471
+ * @throws Error if the key does not exist in keychain
1472
+ */
1473
+ async getPrivateKey(keyRef) {
1474
+ if (!await this.keyExists(keyRef)) {
1475
+ throw new Error(
1476
+ `Key not found in macOS Keychain: "${keyRef}" (account: ${_MacOSKeychainKeyProvider.ACCOUNT})`
1477
+ );
1478
+ }
1479
+ const tempDir = await fs2__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-"));
1480
+ const tempKeyPath = path2__namespace.join(tempDir, "private.pem");
1481
+ try {
1482
+ const base64Key = await execCommand2("security", [
1483
+ "find-generic-password",
1484
+ "-a",
1485
+ _MacOSKeychainKeyProvider.ACCOUNT,
1486
+ "-s",
1487
+ keyRef,
1488
+ "-w"
1489
+ ]);
1490
+ const keyContent = Buffer.from(base64Key, "base64").toString("utf8");
1491
+ await fs2__namespace.writeFile(tempKeyPath, keyContent, { mode: 384 });
1492
+ await setKeyPermissions(tempKeyPath);
1493
+ return {
1494
+ keyPath: tempKeyPath,
1495
+ cleanup: async () => {
1496
+ try {
1497
+ await fs2__namespace.unlink(tempKeyPath);
1498
+ await fs2__namespace.rmdir(tempDir);
1499
+ } catch (cleanupError) {
1500
+ console.warn(
1501
+ `Warning: Failed to clean up temporary key file at ${tempKeyPath}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
1502
+ );
1503
+ }
1504
+ }
1505
+ };
1506
+ } catch (error) {
1507
+ try {
1508
+ await fs2__namespace.rm(tempDir, { recursive: true, force: true });
1509
+ } catch (cleanupError) {
1510
+ console.warn(
1511
+ `Warning: Failed to clean up temporary key directory at ${tempDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
1512
+ );
1513
+ }
1514
+ throw error;
1515
+ }
1516
+ }
1517
+ /**
1518
+ * Generate a new keypair and store private key in keychain.
1519
+ * Public key is written to filesystem for repository commit.
1520
+ * @param options - Key generation options
1521
+ */
1522
+ async generateKeyPair(options) {
1523
+ const { publicKeyPath, force = false } = options;
1524
+ const tempDir = await fs2__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-keygen-"));
1525
+ const tempPrivateKeyPath = path2__namespace.join(tempDir, "private.pem");
1526
+ try {
1527
+ await generateKeyPair({
1528
+ privatePath: tempPrivateKeyPath,
1529
+ publicPath: publicKeyPath,
1530
+ force
1531
+ });
1532
+ const privateKeyContent = await fs2__namespace.readFile(tempPrivateKeyPath, "utf8");
1533
+ const base64Key = Buffer.from(privateKeyContent, "utf8").toString("base64");
1534
+ await execCommand2("security", [
1535
+ "add-generic-password",
1536
+ "-a",
1537
+ _MacOSKeychainKeyProvider.ACCOUNT,
1538
+ "-s",
1539
+ this.itemName,
1540
+ "-w",
1541
+ base64Key,
1542
+ "-T",
1543
+ "",
1544
+ "-U"
1545
+ ]);
1546
+ await fs2__namespace.unlink(tempPrivateKeyPath);
1547
+ await fs2__namespace.rmdir(tempDir);
1548
+ return {
1549
+ privateKeyRef: this.itemName,
1550
+ publicKeyPath,
1551
+ storageDescription: `macOS Keychain: ${this.itemName}`
1552
+ };
1553
+ } catch (error) {
1554
+ try {
1555
+ await fs2__namespace.rm(tempDir, { recursive: true, force: true });
1556
+ } catch (cleanupError) {
1557
+ console.warn(
1558
+ `Warning: Failed to clean up temporary key directory at ${tempDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
1559
+ );
1560
+ }
1561
+ throw error;
1562
+ }
1563
+ }
1564
+ /**
1565
+ * Get the configuration for this provider.
1566
+ */
1567
+ getConfig() {
1568
+ return {
1569
+ type: this.type,
1570
+ options: {
1571
+ itemName: this.itemName
1572
+ }
1573
+ };
1574
+ }
1575
+ };
1576
+ async function execCommand2(command, args) {
1577
+ return new Promise((resolve3, reject) => {
1578
+ const proc = child_process.spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
1579
+ let stdout = "";
1580
+ let stderr = "";
1581
+ proc.stdout.on("data", (data) => {
1582
+ stdout += data.toString();
1583
+ });
1584
+ proc.stderr.on("data", (data) => {
1585
+ stderr += data.toString();
1586
+ });
1587
+ proc.on("close", (code) => {
1588
+ if (code === 0) {
1589
+ resolve3(stdout.trim());
1590
+ } else {
1591
+ reject(new Error(`Command failed with exit code ${String(code)}: ${stderr}`));
1592
+ }
1593
+ });
1594
+ proc.on("error", (error) => {
1595
+ reject(error);
1596
+ });
1597
+ });
1598
+ }
1599
+
1600
+ // src/key-provider/registry.ts
1601
+ var KeyProviderRegistry = class {
1602
+ static providers = /* @__PURE__ */ new Map();
1603
+ /**
1604
+ * Register a key provider factory.
1605
+ * @param type - Provider type identifier
1606
+ * @param factory - Factory function to create provider instances
1607
+ */
1608
+ static register(type, factory) {
1609
+ this.providers.set(type, factory);
1610
+ }
1611
+ /**
1612
+ * Create a key provider from configuration.
1613
+ * @param config - Provider configuration
1614
+ * @returns A key provider instance
1615
+ * @throws Error if the provider type is not registered
1616
+ */
1617
+ static create(config) {
1618
+ const factory = this.providers.get(config.type);
1619
+ if (!factory) {
1620
+ throw new Error(
1621
+ `Unknown key provider type: ${config.type}. Available types: ${Array.from(this.providers.keys()).join(", ")}`
1622
+ );
1623
+ }
1624
+ return factory(config);
1625
+ }
1626
+ /**
1627
+ * Get all registered provider types.
1628
+ * @returns Array of provider type identifiers
1629
+ */
1630
+ static getProviderTypes() {
1631
+ return Array.from(this.providers.keys());
1632
+ }
1633
+ };
1634
+ KeyProviderRegistry.register("filesystem", (config) => {
1635
+ const privateKeyPath = typeof config.options.privateKeyPath === "string" ? config.options.privateKeyPath : void 0;
1636
+ if (privateKeyPath !== void 0) {
1637
+ return new FilesystemKeyProvider({ privateKeyPath });
1638
+ }
1639
+ return new FilesystemKeyProvider();
1640
+ });
1641
+ KeyProviderRegistry.register("1password", (config) => {
1642
+ const { options } = config;
1643
+ const account = typeof options.account === "string" ? options.account : void 0;
1644
+ const vault = typeof options.vault === "string" ? options.vault : "";
1645
+ const itemName = typeof options.itemName === "string" ? options.itemName : "";
1646
+ if (!vault || !itemName) {
1647
+ throw new Error("1Password provider requires vault and itemName options");
1648
+ }
1649
+ if (account !== void 0) {
1650
+ return new OnePasswordKeyProvider({ account, vault, itemName });
1651
+ }
1652
+ return new OnePasswordKeyProvider({ vault, itemName });
1653
+ });
1654
+ KeyProviderRegistry.register("macos-keychain", (config) => {
1655
+ const { options } = config;
1656
+ const itemName = typeof options.itemName === "string" ? options.itemName : "";
1657
+ if (!itemName) {
1658
+ throw new Error("macOS Keychain provider requires itemName option");
1659
+ }
1660
+ return new MacOSKeychainKeyProvider({ itemName });
1661
+ });
1662
+ var privateKeyRefSchema = zod.z.discriminatedUnion("type", [
1663
+ zod.z.object({
1664
+ type: zod.z.literal("file"),
1665
+ path: zod.z.string().min(1, "File path cannot be empty")
1666
+ }),
1667
+ zod.z.object({
1668
+ type: zod.z.literal("keychain"),
1669
+ service: zod.z.string().min(1, "Service name cannot be empty"),
1670
+ account: zod.z.string().min(1, "Account name cannot be empty")
1671
+ }),
1672
+ zod.z.object({
1673
+ type: zod.z.literal("1password"),
1674
+ account: zod.z.string().optional(),
1675
+ vault: zod.z.string().min(1, "Vault name cannot be empty"),
1676
+ item: zod.z.string().min(1, "Item name cannot be empty"),
1677
+ field: zod.z.string().optional()
1678
+ })
1679
+ ]);
1680
+ var identitySchema = zod.z.object({
1681
+ name: zod.z.string().min(1, "Identity name cannot be empty"),
1682
+ email: zod.z.string().optional(),
1683
+ github: zod.z.string().optional(),
1684
+ publicKey: zod.z.string().min(1, "Public key cannot be empty"),
1685
+ privateKey: privateKeyRefSchema
1686
+ }).strict();
1687
+ var localConfigSchema = zod.z.object({
1688
+ activeIdentity: zod.z.string().min(1, "Active identity name cannot be empty"),
1689
+ identities: zod.z.record(zod.z.string(), identitySchema).refine((identities) => Object.keys(identities).length >= 1, {
1690
+ message: "At least one identity must be defined"
1691
+ })
1692
+ }).strict();
1693
+ var LocalConfigValidationError = class extends Error {
1694
+ constructor(message, issues) {
1695
+ super(message);
1696
+ this.issues = issues;
1697
+ this.name = "LocalConfigValidationError";
1698
+ }
1699
+ };
1700
+ function getLocalConfigPath() {
1701
+ const home = os.homedir();
1702
+ return path2.join(home, ".config", "attest-it", "config.yaml");
1703
+ }
1704
+ function parseLocalConfigContent(content) {
1705
+ let rawConfig;
1706
+ try {
1707
+ rawConfig = yaml.parse(content);
1708
+ } catch (error) {
1709
+ throw new LocalConfigValidationError(
1710
+ `Failed to parse YAML: ${error instanceof Error ? error.message : String(error)}`,
1711
+ []
1712
+ );
1713
+ }
1714
+ const result = localConfigSchema.safeParse(rawConfig);
1715
+ if (!result.success) {
1716
+ throw new LocalConfigValidationError(
1717
+ "Local configuration validation failed:\n" + result.error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n"),
1718
+ result.error.issues
1719
+ );
1720
+ }
1721
+ const identities = Object.fromEntries(
1722
+ Object.entries(result.data.identities).map(([key, identity]) => {
1723
+ let privateKey;
1724
+ if (identity.privateKey.type === "1password") {
1725
+ privateKey = {
1726
+ type: "1password",
1727
+ vault: identity.privateKey.vault,
1728
+ item: identity.privateKey.item,
1729
+ ...identity.privateKey.account !== void 0 && {
1730
+ account: identity.privateKey.account
1731
+ },
1732
+ ...identity.privateKey.field !== void 0 && { field: identity.privateKey.field }
1733
+ };
1734
+ } else {
1735
+ privateKey = identity.privateKey;
1736
+ }
1737
+ return [
1738
+ key,
1739
+ {
1740
+ name: identity.name,
1741
+ publicKey: identity.publicKey,
1742
+ privateKey,
1743
+ ...identity.email !== void 0 && { email: identity.email },
1744
+ ...identity.github !== void 0 && { github: identity.github }
1745
+ }
1746
+ ];
1747
+ })
1748
+ );
1749
+ return {
1750
+ activeIdentity: result.data.activeIdentity,
1751
+ identities
1752
+ };
1753
+ }
1754
+ async function loadLocalConfig(configPath) {
1755
+ const resolvedPath = configPath ?? getLocalConfigPath();
1756
+ try {
1757
+ const content = await fs2.readFile(resolvedPath, "utf8");
1758
+ return parseLocalConfigContent(content);
1759
+ } catch (error) {
1760
+ if (error instanceof LocalConfigValidationError) {
1761
+ throw error;
1762
+ }
1763
+ if (error && typeof error === "object" && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
1764
+ return null;
1765
+ }
1766
+ throw error;
1767
+ }
1768
+ }
1769
+ function loadLocalConfigSync(configPath) {
1770
+ const resolvedPath = configPath ?? getLocalConfigPath();
1771
+ try {
1772
+ const content = fs.readFileSync(resolvedPath, "utf8");
1773
+ return parseLocalConfigContent(content);
1774
+ } catch (error) {
1775
+ if (error instanceof LocalConfigValidationError) {
1776
+ throw error;
1777
+ }
1778
+ if (error && typeof error === "object" && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
1779
+ return null;
1780
+ }
1781
+ throw error;
1782
+ }
1783
+ }
1784
+ async function saveLocalConfig(config, configPath) {
1785
+ const resolvedPath = configPath ?? getLocalConfigPath();
1786
+ const content = yaml.stringify(config);
1787
+ const dir = path2.dirname(resolvedPath);
1788
+ await fs2.mkdir(dir, { recursive: true });
1789
+ await fs2.writeFile(resolvedPath, content, "utf8");
1790
+ }
1791
+ function saveLocalConfigSync(config, configPath) {
1792
+ const resolvedPath = configPath ?? getLocalConfigPath();
1793
+ const content = yaml.stringify(config);
1794
+ const dir = path2.dirname(resolvedPath);
1795
+ fs.mkdirSync(dir, { recursive: true });
1796
+ fs.writeFileSync(resolvedPath, content, "utf8");
1797
+ }
1798
+ function getActiveIdentity(config) {
1799
+ return config.identities[config.activeIdentity];
1800
+ }
1801
+ function isAuthorizedSigner(config, gateId, publicKey) {
1802
+ const gate = config.gates?.[gateId];
1803
+ if (!gate) {
1804
+ return false;
1805
+ }
1806
+ const teamMember = findTeamMemberByPublicKey(config, publicKey);
1807
+ if (!teamMember) {
1808
+ return false;
1809
+ }
1810
+ const teamMemberSlug = findTeamMemberSlug(config, teamMember);
1811
+ if (!teamMemberSlug) {
1812
+ return false;
1813
+ }
1814
+ return gate.authorizedSigners.includes(teamMemberSlug);
1815
+ }
1816
+ function getAuthorizedSignersForGate(config, gateId) {
1817
+ const gate = config.gates?.[gateId];
1818
+ if (!gate || !config.team) {
1819
+ return [];
1820
+ }
1821
+ const authorizedMembers = [];
1822
+ for (const signerSlug of gate.authorizedSigners) {
1823
+ const member = config.team[signerSlug];
1824
+ if (member) {
1825
+ authorizedMembers.push(member);
1826
+ }
1827
+ }
1828
+ return authorizedMembers;
1829
+ }
1830
+ function findTeamMemberByPublicKey(config, publicKey) {
1831
+ if (!config.team) {
1832
+ return void 0;
1833
+ }
1834
+ for (const member of Object.values(config.team)) {
1835
+ if (member.publicKey === publicKey) {
1836
+ return member;
1837
+ }
1838
+ }
1839
+ return void 0;
1840
+ }
1841
+ function findTeamMemberSlug(config, teamMember) {
1842
+ if (!config.team) {
1843
+ return void 0;
1844
+ }
1845
+ for (const [slug, member] of Object.entries(config.team)) {
1846
+ if (member === teamMember || member.publicKey === teamMember.publicKey) {
1847
+ return slug;
1848
+ }
1849
+ }
1850
+ return void 0;
1851
+ }
1852
+ function getGate(config, gateId) {
1853
+ return config.gates?.[gateId];
1854
+ }
1855
+ var DURATION_PATTERN = /^\d+(\.\d+)?\s*(ms|s|m|h|d|w|y)$/i;
1856
+ function isValidDurationFormat(value) {
1857
+ return DURATION_PATTERN.test(value.trim());
1858
+ }
1859
+ function parseDuration(duration) {
1860
+ if (!isValidDurationFormat(duration)) {
1861
+ throw new Error(`Invalid duration string: ${duration}`);
1862
+ }
1863
+ const result = ms__default.default(duration);
1864
+ if (typeof result !== "number" || result <= 0) {
1865
+ throw new Error(`Invalid duration string: ${duration}`);
1866
+ }
1867
+ return result;
1868
+ }
1869
+ var sealSchema = zod.z.object({
1870
+ gateId: zod.z.string().min(1, "Gate ID cannot be empty"),
1871
+ // Fingerprint format: sha256:<hex> where hex is at least 1 character
1872
+ // Full fingerprints are 64 hex chars, but tests may use shorter values
1873
+ fingerprint: zod.z.string().regex(/^sha256:[a-f0-9]+$/i, "Invalid fingerprint format (expected sha256:<hex>)"),
1874
+ timestamp: zod.z.string().datetime({ message: "Invalid ISO 8601 timestamp" }),
1875
+ sealedBy: zod.z.string().min(1, "Signer slug cannot be empty"),
1876
+ signature: zod.z.string().min(1, "Signature cannot be empty")
1877
+ });
1878
+ var sealsFileSchema = zod.z.object({
1879
+ version: zod.z.literal(1, { errorMap: () => ({ message: "Unsupported seals file version" }) }),
1880
+ seals: zod.z.record(zod.z.string(), sealSchema)
1881
+ });
1882
+ function createSeal(options) {
1883
+ const { gateId, fingerprint, sealedBy, privateKey } = options;
1884
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1885
+ const canonicalString = `${gateId}:${fingerprint}:${timestamp}`;
1886
+ const signature = sign3(canonicalString, privateKey);
1887
+ return {
1888
+ gateId,
1889
+ fingerprint,
1890
+ timestamp,
1891
+ sealedBy,
1892
+ signature
1893
+ };
1894
+ }
1895
+ function verifySeal(seal, config) {
1896
+ const { gateId, fingerprint, timestamp, sealedBy, signature } = seal;
1897
+ if (!config.team) {
1898
+ return {
1899
+ valid: false,
1900
+ error: `No team configuration found`
1901
+ };
1902
+ }
1903
+ const teamMember = config.team[sealedBy];
1904
+ if (!teamMember) {
1905
+ return {
1906
+ valid: false,
1907
+ error: `Team member '${sealedBy}' not found in configuration`
1908
+ };
1909
+ }
1910
+ const canonicalString = `${gateId}:${fingerprint}:${timestamp}`;
1911
+ try {
1912
+ const isValid = verify3(canonicalString, signature, teamMember.publicKey);
1913
+ if (!isValid) {
1914
+ return {
1915
+ valid: false,
1916
+ error: "Signature verification failed"
1917
+ };
1918
+ }
1919
+ return { valid: true };
1920
+ } catch (error) {
1921
+ return {
1922
+ valid: false,
1923
+ error: `Signature verification error: ${error instanceof Error ? error.message : String(error)}`
1924
+ };
1925
+ }
1926
+ }
1927
+ function parseSealsContent(content) {
1928
+ let rawData;
1929
+ try {
1930
+ rawData = JSON.parse(content);
1931
+ } catch (error) {
1932
+ throw new Error(`Invalid JSON: ${error instanceof Error ? error.message : String(error)}`);
1933
+ }
1934
+ const result = sealsFileSchema.safeParse(rawData);
1935
+ if (!result.success) {
1936
+ const issues = result.error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n");
1937
+ throw new Error(`Invalid seals file:
1938
+ ${issues}`);
1939
+ }
1940
+ return result.data;
1941
+ }
1942
+ async function readSeals(dir) {
1943
+ const sealsPath = path2__namespace.join(dir, ".attest-it", "seals.json");
1944
+ try {
1945
+ const content = await fs__namespace.promises.readFile(sealsPath, "utf8");
1946
+ return parseSealsContent(content);
1947
+ } catch (error) {
1948
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
1949
+ return {
1950
+ version: 1,
1951
+ seals: {}
1952
+ };
1953
+ }
1954
+ throw new Error(
1955
+ `Failed to read seals file: ${error instanceof Error ? error.message : String(error)}`
1956
+ );
1957
+ }
1958
+ }
1959
+ function readSealsSync(dir) {
1960
+ const sealsPath = path2__namespace.join(dir, ".attest-it", "seals.json");
1961
+ try {
1962
+ const content = fs__namespace.readFileSync(sealsPath, "utf8");
1963
+ return parseSealsContent(content);
1964
+ } catch (error) {
1965
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
1966
+ return {
1967
+ version: 1,
1968
+ seals: {}
1969
+ };
1970
+ }
1971
+ throw new Error(
1972
+ `Failed to read seals file: ${error instanceof Error ? error.message : String(error)}`
1973
+ );
1974
+ }
1975
+ }
1976
+ async function writeSeals(dir, sealsFile) {
1977
+ const attestItDir = path2__namespace.join(dir, ".attest-it");
1978
+ const sealsPath = path2__namespace.join(attestItDir, "seals.json");
1979
+ try {
1980
+ await fs__namespace.promises.mkdir(attestItDir, { recursive: true });
1981
+ const content = JSON.stringify(sealsFile, null, 2) + "\n";
1982
+ await fs__namespace.promises.writeFile(sealsPath, content, "utf8");
1983
+ } catch (error) {
1984
+ throw new Error(
1985
+ `Failed to write seals file: ${error instanceof Error ? error.message : String(error)}`
1986
+ );
1987
+ }
1988
+ }
1989
+ function writeSealsSync(dir, sealsFile) {
1990
+ const attestItDir = path2__namespace.join(dir, ".attest-it");
1991
+ const sealsPath = path2__namespace.join(attestItDir, "seals.json");
1992
+ try {
1993
+ fs__namespace.mkdirSync(attestItDir, { recursive: true });
1994
+ const content = JSON.stringify(sealsFile, null, 2) + "\n";
1995
+ fs__namespace.writeFileSync(sealsPath, content, "utf8");
1996
+ } catch (error) {
1997
+ throw new Error(
1998
+ `Failed to write seals file: ${error instanceof Error ? error.message : String(error)}`
1999
+ );
2000
+ }
2001
+ }
2002
+
2003
+ // src/seal/verification.ts
2004
+ function verifyGateSeal(config, gateId, seals, currentFingerprint) {
2005
+ const gate = getGate(config, gateId);
2006
+ if (!gate) {
2007
+ return {
2008
+ gateId,
2009
+ state: "MISSING",
2010
+ message: `Gate '${gateId}' not found in configuration`
2011
+ };
2012
+ }
2013
+ const seal = seals.seals[gateId];
2014
+ if (!seal) {
2015
+ return {
2016
+ gateId,
2017
+ state: "MISSING",
2018
+ message: `No seal found for gate '${gateId}'`
2019
+ };
2020
+ }
2021
+ if (seal.fingerprint !== currentFingerprint) {
2022
+ return {
2023
+ gateId,
2024
+ state: "FINGERPRINT_MISMATCH",
2025
+ seal,
2026
+ message: `Fingerprint changed since seal was created`
2027
+ };
2028
+ }
2029
+ if (!config.team) {
2030
+ return {
2031
+ gateId,
2032
+ state: "UNKNOWN_SIGNER",
2033
+ seal,
2034
+ message: `No team configuration found`
2035
+ };
2036
+ }
2037
+ const teamMember = config.team[seal.sealedBy];
2038
+ if (!teamMember) {
2039
+ return {
2040
+ gateId,
2041
+ state: "UNKNOWN_SIGNER",
2042
+ seal,
2043
+ message: `Signer '${seal.sealedBy}' not found in team`
2044
+ };
2045
+ }
2046
+ const authorized = isAuthorizedSigner(config, gateId, teamMember.publicKey);
2047
+ if (!authorized) {
2048
+ return {
2049
+ gateId,
2050
+ state: "UNKNOWN_SIGNER",
2051
+ seal,
2052
+ message: `Signer '${seal.sealedBy}' is not authorized for gate '${gateId}'`
2053
+ };
2054
+ }
2055
+ const verificationResult = verifySeal(seal, config);
2056
+ if (!verificationResult.valid) {
2057
+ return {
2058
+ gateId,
2059
+ state: "INVALID_SIGNATURE",
2060
+ seal,
2061
+ message: verificationResult.error ?? "Signature verification failed"
2062
+ };
2063
+ }
2064
+ try {
2065
+ const maxAgeMs = parseDuration(gate.maxAge);
2066
+ const sealTimestamp = new Date(seal.timestamp).getTime();
2067
+ const now = Date.now();
2068
+ const ageMs = now - sealTimestamp;
2069
+ if (ageMs > maxAgeMs) {
2070
+ const ageDays = Math.floor(ageMs / (1e3 * 60 * 60 * 24));
2071
+ const maxAgeDays = Math.floor(maxAgeMs / (1e3 * 60 * 60 * 24));
2072
+ return {
2073
+ gateId,
2074
+ state: "STALE",
2075
+ seal,
2076
+ message: `Seal is ${ageDays.toString()} days old, exceeds maxAge of ${maxAgeDays.toString()} days`
2077
+ };
2078
+ }
2079
+ } catch (error) {
2080
+ return {
2081
+ gateId,
2082
+ state: "STALE",
2083
+ seal,
2084
+ message: `Cannot verify freshness: invalid maxAge format: ${error instanceof Error ? error.message : String(error)}`
2085
+ };
2086
+ }
2087
+ return {
2088
+ gateId,
2089
+ state: "VALID",
2090
+ seal
2091
+ };
2092
+ }
2093
+ function verifyAllSeals(config, seals, fingerprints) {
2094
+ if (!config.gates) {
2095
+ return [];
2096
+ }
2097
+ const results = [];
2098
+ for (const gateId of Object.keys(config.gates)) {
2099
+ const fingerprint = fingerprints[gateId];
2100
+ if (!fingerprint) {
2101
+ results.push({
2102
+ gateId,
2103
+ state: "MISSING",
2104
+ message: `No fingerprint computed for gate '${gateId}'`
2105
+ });
2106
+ continue;
2107
+ }
2108
+ const result = verifyGateSeal(config, gateId, seals, fingerprint);
2109
+ results.push(result);
2110
+ }
2111
+ return results;
2112
+ }
2113
+
878
2114
  // src/index.ts
879
2115
  var version = "0.0.0";
880
2116
 
881
2117
  exports.ConfigNotFoundError = ConfigNotFoundError;
882
2118
  exports.ConfigValidationError = ConfigValidationError;
2119
+ exports.FilesystemKeyProvider = FilesystemKeyProvider;
2120
+ exports.KeyProviderRegistry = KeyProviderRegistry;
2121
+ exports.LocalConfigValidationError = LocalConfigValidationError;
2122
+ exports.MacOSKeychainKeyProvider = MacOSKeychainKeyProvider;
2123
+ exports.OnePasswordKeyProvider = OnePasswordKeyProvider;
883
2124
  exports.SignatureInvalidError = SignatureInvalidError;
884
2125
  exports.canonicalizeAttestations = canonicalizeAttestations;
885
2126
  exports.checkOpenSSL = checkOpenSSL;
886
2127
  exports.computeFingerprint = computeFingerprint;
887
2128
  exports.computeFingerprintSync = computeFingerprintSync;
888
2129
  exports.createAttestation = createAttestation;
2130
+ exports.createSeal = createSeal;
889
2131
  exports.findAttestation = findAttestation;
890
2132
  exports.findConfigPath = findConfigPath;
2133
+ exports.findTeamMemberByPublicKey = findTeamMemberByPublicKey;
2134
+ exports.generateEd25519KeyPair = generateKeyPair2;
891
2135
  exports.generateKeyPair = generateKeyPair;
2136
+ exports.getActiveIdentity = getActiveIdentity;
2137
+ exports.getAuthorizedSignersForGate = getAuthorizedSignersForGate;
892
2138
  exports.getDefaultPrivateKeyPath = getDefaultPrivateKeyPath;
893
2139
  exports.getDefaultPublicKeyPath = getDefaultPublicKeyPath;
2140
+ exports.getGate = getGate;
2141
+ exports.getLocalConfigPath = getLocalConfigPath;
2142
+ exports.getPublicKeyFromPrivate = getPublicKeyFromPrivate;
2143
+ exports.isAuthorizedSigner = isAuthorizedSigner;
894
2144
  exports.listPackageFiles = listPackageFiles;
895
2145
  exports.loadConfig = loadConfig;
896
2146
  exports.loadConfigSync = loadConfigSync;
2147
+ exports.loadLocalConfig = loadLocalConfig;
2148
+ exports.loadLocalConfigSync = loadLocalConfigSync;
2149
+ exports.parseDuration = parseDuration;
897
2150
  exports.readAndVerifyAttestations = readAndVerifyAttestations;
898
2151
  exports.readAttestations = readAttestations;
899
2152
  exports.readAttestationsSync = readAttestationsSync;
2153
+ exports.readSeals = readSeals;
2154
+ exports.readSealsSync = readSealsSync;
900
2155
  exports.removeAttestation = removeAttestation;
901
2156
  exports.resolveConfigPaths = resolveConfigPaths;
2157
+ exports.saveLocalConfig = saveLocalConfig;
2158
+ exports.saveLocalConfigSync = saveLocalConfigSync;
902
2159
  exports.setKeyPermissions = setKeyPermissions;
903
2160
  exports.sign = sign;
2161
+ exports.signEd25519 = sign3;
904
2162
  exports.toAttestItConfig = toAttestItConfig;
905
2163
  exports.upsertAttestation = upsertAttestation;
906
2164
  exports.verify = verify;
2165
+ exports.verifyAllSeals = verifyAllSeals;
907
2166
  exports.verifyAttestations = verifyAttestations;
2167
+ exports.verifyEd25519 = verify3;
2168
+ exports.verifyGateSeal = verifyGateSeal;
2169
+ exports.verifySeal = verifySeal;
908
2170
  exports.version = version;
909
2171
  exports.writeAttestations = writeAttestations;
910
2172
  exports.writeAttestationsSync = writeAttestationsSync;
2173
+ exports.writeSeals = writeSeals;
2174
+ exports.writeSealsSync = writeSealsSync;
911
2175
  exports.writeSignedAttestations = writeSignedAttestations;
912
2176
  //# sourceMappingURL=index.cjs.map
913
2177
  //# sourceMappingURL=index.cjs.map