@attest-it/core 0.0.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 ADDED
@@ -0,0 +1,915 @@
1
+ 'use strict';
2
+
3
+ var child_process = require('child_process');
4
+ var fs2 = require('fs/promises');
5
+ var path2 = require('path');
6
+ var os = require('os');
7
+ var fs = require('fs');
8
+ var yaml = require('yaml');
9
+ var zod = require('zod');
10
+ var crypto = require('crypto');
11
+ var tinyglobby = require('tinyglobby');
12
+ var canonicalizeNamespace = require('canonicalize');
13
+
14
+ function _interopNamespace(e) {
15
+ if (e && e.__esModule) return e;
16
+ var n = Object.create(null);
17
+ if (e) {
18
+ Object.keys(e).forEach(function (k) {
19
+ if (k !== 'default') {
20
+ var d = Object.getOwnPropertyDescriptor(e, k);
21
+ Object.defineProperty(n, k, d.get ? d : {
22
+ enumerable: true,
23
+ get: function () { return e[k]; }
24
+ });
25
+ }
26
+ });
27
+ }
28
+ n.default = e;
29
+ return Object.freeze(n);
30
+ }
31
+
32
+ var fs2__namespace = /*#__PURE__*/_interopNamespace(fs2);
33
+ var path2__namespace = /*#__PURE__*/_interopNamespace(path2);
34
+ var os__namespace = /*#__PURE__*/_interopNamespace(os);
35
+ var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
36
+ var crypto__namespace = /*#__PURE__*/_interopNamespace(crypto);
37
+ var canonicalizeNamespace__namespace = /*#__PURE__*/_interopNamespace(canonicalizeNamespace);
38
+
39
+ var __defProp = Object.defineProperty;
40
+ var __getOwnPropNames = Object.getOwnPropertyNames;
41
+ var __esm = (fn, res) => function __init() {
42
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
43
+ };
44
+ var __export = (target, all) => {
45
+ for (var name in all)
46
+ __defProp(target, name, { get: all[name], enumerable: true });
47
+ };
48
+
49
+ // src/crypto.ts
50
+ var crypto_exports = {};
51
+ __export(crypto_exports, {
52
+ checkOpenSSL: () => checkOpenSSL,
53
+ generateKeyPair: () => generateKeyPair,
54
+ getDefaultPrivateKeyPath: () => getDefaultPrivateKeyPath,
55
+ getDefaultPublicKeyPath: () => getDefaultPublicKeyPath,
56
+ setKeyPermissions: () => setKeyPermissions,
57
+ sign: () => sign,
58
+ verify: () => verify
59
+ });
60
+ async function runOpenSSL(args, stdin) {
61
+ return new Promise((resolve3, reject) => {
62
+ const child = child_process.spawn("openssl", args, {
63
+ stdio: ["pipe", "pipe", "pipe"]
64
+ });
65
+ const stdoutChunks = [];
66
+ let stderr = "";
67
+ child.stdout.on("data", (chunk) => {
68
+ stdoutChunks.push(chunk);
69
+ });
70
+ child.stderr.on("data", (chunk) => {
71
+ stderr += chunk.toString();
72
+ });
73
+ child.on("error", (err) => {
74
+ reject(new Error(`Failed to spawn OpenSSL: ${err.message}`));
75
+ });
76
+ child.on("close", (code) => {
77
+ resolve3({
78
+ exitCode: code ?? 1,
79
+ stdout: Buffer.concat(stdoutChunks),
80
+ stderr
81
+ });
82
+ });
83
+ child.stdin.end();
84
+ });
85
+ }
86
+ async function checkOpenSSL() {
87
+ const result = await runOpenSSL(["version"]);
88
+ if (result.exitCode !== 0) {
89
+ throw new Error(`OpenSSL check failed: ${result.stderr}`);
90
+ }
91
+ return result.stdout.toString().trim();
92
+ }
93
+ async function ensureOpenSSLAvailable() {
94
+ if (openSSLChecked) {
95
+ return;
96
+ }
97
+ try {
98
+ await checkOpenSSL();
99
+ openSSLChecked = true;
100
+ } catch {
101
+ throw new Error(
102
+ "OpenSSL is not installed or not in PATH. Please install OpenSSL to use attest-it. On macOS: brew install openssl. On Ubuntu: apt-get install openssl"
103
+ );
104
+ }
105
+ }
106
+ function getDefaultPrivateKeyPath() {
107
+ const homeDir = os__namespace.homedir();
108
+ if (process.platform === "win32") {
109
+ const appData = process.env.APPDATA ?? path2__namespace.join(homeDir, "AppData", "Roaming");
110
+ return path2__namespace.join(appData, "attest-it", "private.pem");
111
+ }
112
+ return path2__namespace.join(homeDir, ".config", "attest-it", "private.pem");
113
+ }
114
+ function getDefaultPublicKeyPath() {
115
+ return path2__namespace.join(process.cwd(), "attest-it-public.pem");
116
+ }
117
+ async function ensureDir(dirPath) {
118
+ try {
119
+ await fs2__namespace.mkdir(dirPath, { recursive: true });
120
+ } catch (err) {
121
+ if (err instanceof Error && "code" in err && err.code !== "EEXIST") {
122
+ throw err;
123
+ }
124
+ }
125
+ }
126
+ async function fileExists(filePath) {
127
+ try {
128
+ await fs2__namespace.access(filePath);
129
+ return true;
130
+ } catch {
131
+ return false;
132
+ }
133
+ }
134
+ async function cleanupFiles(...paths) {
135
+ for (const filePath of paths) {
136
+ try {
137
+ await fs2__namespace.unlink(filePath);
138
+ } catch {
139
+ }
140
+ }
141
+ }
142
+ async function generateKeyPair(options = {}) {
143
+ await ensureOpenSSLAvailable();
144
+ const {
145
+ algorithm = "ed25519",
146
+ privatePath = getDefaultPrivateKeyPath(),
147
+ publicPath = getDefaultPublicKeyPath(),
148
+ force = false
149
+ } = options;
150
+ const privateExists = await fileExists(privatePath);
151
+ const publicExists = await fileExists(publicPath);
152
+ if ((privateExists || publicExists) && !force) {
153
+ const existing = [privateExists ? privatePath : null, publicExists ? publicPath : null].filter(
154
+ Boolean
155
+ );
156
+ throw new Error(
157
+ `Key files already exist: ${existing.join(", ")}. Use force: true to overwrite.`
158
+ );
159
+ }
160
+ await ensureDir(path2__namespace.dirname(privatePath));
161
+ await ensureDir(path2__namespace.dirname(publicPath));
162
+ try {
163
+ const genArgs = algorithm === "ed25519" ? ["genpkey", "-algorithm", "Ed25519", "-out", privatePath] : ["genpkey", "-algorithm", "RSA", "-pkeyopt", "rsa_keygen_bits:2048", "-out", privatePath];
164
+ const genResult = await runOpenSSL(genArgs);
165
+ if (genResult.exitCode !== 0) {
166
+ throw new Error(`Failed to generate private key: ${genResult.stderr}`);
167
+ }
168
+ await setKeyPermissions(privatePath);
169
+ const pubResult = await runOpenSSL(["pkey", "-in", privatePath, "-pubout", "-out", publicPath]);
170
+ if (pubResult.exitCode !== 0) {
171
+ throw new Error(`Failed to extract public key: ${pubResult.stderr}`);
172
+ }
173
+ return {
174
+ privatePath,
175
+ publicPath
176
+ };
177
+ } catch (err) {
178
+ await cleanupFiles(privatePath, publicPath);
179
+ throw err;
180
+ }
181
+ }
182
+ async function sign(options) {
183
+ await ensureOpenSSLAvailable();
184
+ const { privateKeyPath, data } = options;
185
+ if (!await fileExists(privateKeyPath)) {
186
+ throw new Error(`Private key not found: ${privateKeyPath}`);
187
+ }
188
+ const dataBuffer = typeof data === "string" ? Buffer.from(data, "utf8") : data;
189
+ const processBuffer = dataBuffer.length === 0 ? Buffer.from([0]) : dataBuffer;
190
+ const tmpDir = await fs2__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-"));
191
+ const dataFile = path2__namespace.join(tmpDir, "data.bin");
192
+ const sigFile = path2__namespace.join(tmpDir, "sig.bin");
193
+ try {
194
+ await fs2__namespace.writeFile(dataFile, processBuffer);
195
+ const result = await runOpenSSL([
196
+ "pkeyutl",
197
+ "-sign",
198
+ "-inkey",
199
+ privateKeyPath,
200
+ "-in",
201
+ dataFile,
202
+ "-out",
203
+ sigFile
204
+ ]);
205
+ if (result.exitCode !== 0) {
206
+ throw new Error(`Failed to sign data: ${result.stderr}`);
207
+ }
208
+ const sigBuffer = await fs2__namespace.readFile(sigFile);
209
+ return sigBuffer.toString("base64");
210
+ } finally {
211
+ try {
212
+ await fs2__namespace.rm(tmpDir, { recursive: true, force: true });
213
+ } catch {
214
+ }
215
+ }
216
+ }
217
+ async function verify(options) {
218
+ await ensureOpenSSLAvailable();
219
+ const { publicKeyPath, data, signature } = options;
220
+ if (!await fileExists(publicKeyPath)) {
221
+ throw new Error(`Public key not found: ${publicKeyPath}`);
222
+ }
223
+ const dataBuffer = typeof data === "string" ? Buffer.from(data, "utf8") : data;
224
+ const processBuffer = dataBuffer.length === 0 ? Buffer.from([0]) : dataBuffer;
225
+ const sigBuffer = Buffer.from(signature, "base64");
226
+ const tmpDir = await fs2__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-"));
227
+ const dataFile = path2__namespace.join(tmpDir, "data.bin");
228
+ const sigFile = path2__namespace.join(tmpDir, "sig.bin");
229
+ try {
230
+ await fs2__namespace.writeFile(dataFile, processBuffer);
231
+ await fs2__namespace.writeFile(sigFile, sigBuffer);
232
+ const result = await runOpenSSL([
233
+ "pkeyutl",
234
+ "-verify",
235
+ "-pubin",
236
+ "-inkey",
237
+ publicKeyPath,
238
+ "-sigfile",
239
+ sigFile,
240
+ "-in",
241
+ dataFile
242
+ ]);
243
+ if (result.exitCode !== 0 && result.exitCode !== 1) {
244
+ throw new Error(`Verification error: ${result.stderr}`);
245
+ }
246
+ return result.exitCode === 0;
247
+ } finally {
248
+ try {
249
+ await fs2__namespace.rm(tmpDir, { recursive: true, force: true });
250
+ } catch {
251
+ }
252
+ }
253
+ }
254
+ async function setKeyPermissions(keyPath) {
255
+ if (process.platform === "win32") {
256
+ await fs2__namespace.chmod(keyPath, 384);
257
+ } else {
258
+ await fs2__namespace.chmod(keyPath, 384);
259
+ }
260
+ }
261
+ var openSSLChecked;
262
+ var init_crypto = __esm({
263
+ "src/crypto.ts"() {
264
+ openSSLChecked = false;
265
+ }
266
+ });
267
+ var settingsSchema = zod.z.object({
268
+ maxAgeDays: zod.z.number().int().positive().default(30),
269
+ publicKeyPath: zod.z.string().default(".attest-it/pubkey.pem"),
270
+ attestationsPath: zod.z.string().default(".attest-it/attestations.json"),
271
+ defaultCommand: zod.z.string().optional(),
272
+ algorithm: zod.z.enum(["ed25519", "rsa"]).default("ed25519")
273
+ }).strict();
274
+ var suiteSchema = zod.z.object({
275
+ description: zod.z.string().optional(),
276
+ packages: zod.z.array(zod.z.string().min(1, "Package path cannot be empty")).min(1, "At least one package pattern is required"),
277
+ files: zod.z.array(zod.z.string().min(1, "File path cannot be empty")).optional(),
278
+ ignore: zod.z.array(zod.z.string().min(1, "Ignore pattern cannot be empty")).optional(),
279
+ command: zod.z.string().optional(),
280
+ invalidates: zod.z.array(zod.z.string().min(1, "Invalidated suite name cannot be empty")).optional()
281
+ }).strict();
282
+ var configSchema = zod.z.object({
283
+ version: zod.z.literal(1),
284
+ settings: settingsSchema.default({}),
285
+ suites: zod.z.record(zod.z.string(), suiteSchema).refine((suites) => Object.keys(suites).length >= 1, {
286
+ message: "At least one suite must be defined"
287
+ })
288
+ }).strict();
289
+ var ConfigValidationError = class extends Error {
290
+ constructor(message, issues) {
291
+ super(message);
292
+ this.issues = issues;
293
+ this.name = "ConfigValidationError";
294
+ }
295
+ };
296
+ var ConfigNotFoundError = class extends Error {
297
+ constructor(message) {
298
+ super(message);
299
+ this.name = "ConfigNotFoundError";
300
+ }
301
+ };
302
+ function parseConfigContent(content, format) {
303
+ let rawConfig;
304
+ try {
305
+ if (format === "yaml") {
306
+ rawConfig = yaml.parse(content);
307
+ } else {
308
+ rawConfig = JSON.parse(content);
309
+ }
310
+ } catch (error) {
311
+ throw new ConfigValidationError(
312
+ `Failed to parse ${format.toUpperCase()}: ${error instanceof Error ? error.message : String(error)}`,
313
+ []
314
+ );
315
+ }
316
+ const result = configSchema.safeParse(rawConfig);
317
+ if (!result.success) {
318
+ throw new ConfigValidationError(
319
+ "Configuration validation failed:\n" + result.error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n"),
320
+ result.error.issues
321
+ );
322
+ }
323
+ return result.data;
324
+ }
325
+ function getConfigFormat(filePath) {
326
+ const ext = filePath.toLowerCase();
327
+ if (ext.endsWith(".yaml") || ext.endsWith(".yml")) {
328
+ return "yaml";
329
+ }
330
+ if (ext.endsWith(".json")) {
331
+ return "json";
332
+ }
333
+ return "yaml";
334
+ }
335
+ function findConfigPath(startDir = process.cwd()) {
336
+ const configDir = path2.join(startDir, ".attest-it");
337
+ const candidates = ["config.yaml", "config.yml", "config.json"];
338
+ for (const candidate of candidates) {
339
+ const configPath = path2.join(configDir, candidate);
340
+ try {
341
+ fs.readFileSync(configPath, "utf8");
342
+ return configPath;
343
+ } catch {
344
+ continue;
345
+ }
346
+ }
347
+ return null;
348
+ }
349
+ async function loadConfig(configPath) {
350
+ const resolvedPath = configPath ?? findConfigPath();
351
+ if (!resolvedPath) {
352
+ throw new ConfigNotFoundError(
353
+ "Configuration file not found. Expected .attest-it/config.yaml, .attest-it/config.yml, or .attest-it/config.json"
354
+ );
355
+ }
356
+ try {
357
+ const content = await fs2.readFile(resolvedPath, "utf8");
358
+ const format = getConfigFormat(resolvedPath);
359
+ return parseConfigContent(content, format);
360
+ } catch (error) {
361
+ if (error instanceof ConfigValidationError) {
362
+ throw error;
363
+ }
364
+ throw new ConfigNotFoundError(
365
+ `Failed to read configuration file at ${resolvedPath}: ${String(error)}`
366
+ );
367
+ }
368
+ }
369
+ function loadConfigSync(configPath) {
370
+ const resolvedPath = configPath ?? findConfigPath();
371
+ if (!resolvedPath) {
372
+ throw new ConfigNotFoundError(
373
+ "Configuration file not found. Expected .attest-it/config.yaml, .attest-it/config.yml, or .attest-it/config.json"
374
+ );
375
+ }
376
+ try {
377
+ const content = fs.readFileSync(resolvedPath, "utf8");
378
+ const format = getConfigFormat(resolvedPath);
379
+ return parseConfigContent(content, format);
380
+ } catch (error) {
381
+ if (error instanceof ConfigValidationError) {
382
+ throw error;
383
+ }
384
+ throw new ConfigNotFoundError(
385
+ `Failed to read configuration file at ${resolvedPath}: ${String(error)}`
386
+ );
387
+ }
388
+ }
389
+ function resolveConfigPaths(config, repoRoot) {
390
+ return {
391
+ ...config,
392
+ settings: {
393
+ ...config.settings,
394
+ publicKeyPath: path2.resolve(repoRoot, config.settings.publicKeyPath),
395
+ attestationsPath: path2.resolve(repoRoot, config.settings.attestationsPath)
396
+ }
397
+ };
398
+ }
399
+ function toAttestItConfig(config) {
400
+ return {
401
+ version: config.version,
402
+ settings: {
403
+ maxAgeDays: config.settings.maxAgeDays,
404
+ publicKeyPath: config.settings.publicKeyPath,
405
+ attestationsPath: config.settings.attestationsPath,
406
+ algorithm: config.settings.algorithm,
407
+ ...config.settings.defaultCommand !== void 0 && {
408
+ defaultCommand: config.settings.defaultCommand
409
+ }
410
+ },
411
+ suites: Object.fromEntries(
412
+ Object.entries(config.suites).map(([name, suite]) => [
413
+ name,
414
+ {
415
+ packages: suite.packages,
416
+ ...suite.description !== void 0 && { description: suite.description },
417
+ ...suite.files !== void 0 && { files: suite.files },
418
+ ...suite.ignore !== void 0 && { ignore: suite.ignore },
419
+ ...suite.command !== void 0 && { command: suite.command },
420
+ ...suite.invalidates !== void 0 && { invalidates: suite.invalidates }
421
+ }
422
+ ])
423
+ )
424
+ };
425
+ }
426
+ var LARGE_FILE_THRESHOLD = 50 * 1024 * 1024;
427
+ function sortFiles(files) {
428
+ return [...files].sort((a, b) => {
429
+ if (a < b) return -1;
430
+ if (a > b) return 1;
431
+ return 0;
432
+ });
433
+ }
434
+ function normalizePath(filePath) {
435
+ return filePath.split(path2__namespace.sep).join("/");
436
+ }
437
+ function computeFinalFingerprint(fileHashes) {
438
+ const sorted = [...fileHashes].sort((a, b) => {
439
+ if (a.relativePath < b.relativePath) return -1;
440
+ if (a.relativePath > b.relativePath) return 1;
441
+ return 0;
442
+ });
443
+ const hashes = sorted.map((input) => input.hash);
444
+ const concatenated = Buffer.concat(hashes);
445
+ const finalHash = crypto__namespace.createHash("sha256").update(concatenated).digest();
446
+ return `sha256:${finalHash.toString("hex")}`;
447
+ }
448
+ async function hashFileAsync(realPath, normalizedPath, stats) {
449
+ if (stats.size > LARGE_FILE_THRESHOLD) {
450
+ return new Promise((resolve3, reject) => {
451
+ const hash2 = crypto__namespace.createHash("sha256");
452
+ hash2.update(normalizedPath);
453
+ hash2.update("\0");
454
+ const stream = fs__namespace.createReadStream(realPath);
455
+ stream.on("data", (chunk) => {
456
+ hash2.update(chunk);
457
+ });
458
+ stream.on("end", () => {
459
+ resolve3(hash2.digest());
460
+ });
461
+ stream.on("error", reject);
462
+ });
463
+ }
464
+ const content = await fs__namespace.promises.readFile(realPath);
465
+ const hash = crypto__namespace.createHash("sha256");
466
+ hash.update(normalizedPath);
467
+ hash.update("\0");
468
+ hash.update(content);
469
+ return hash.digest();
470
+ }
471
+ function hashFileSync(realPath, normalizedPath) {
472
+ const content = fs__namespace.readFileSync(realPath);
473
+ const hash = crypto__namespace.createHash("sha256");
474
+ hash.update(normalizedPath);
475
+ hash.update("\0");
476
+ hash.update(content);
477
+ return hash.digest();
478
+ }
479
+ function validateOptions(options) {
480
+ if (options.packages.length === 0) {
481
+ throw new Error("packages array must not be empty");
482
+ }
483
+ const baseDir = options.baseDir ?? process.cwd();
484
+ for (const pkg of options.packages) {
485
+ const pkgPath = path2__namespace.resolve(baseDir, pkg);
486
+ if (!fs__namespace.existsSync(pkgPath)) {
487
+ throw new Error(`Package path does not exist: ${pkgPath}`);
488
+ }
489
+ }
490
+ return baseDir;
491
+ }
492
+ async function computeFingerprint(options) {
493
+ const baseDir = validateOptions(options);
494
+ const files = await listPackageFiles(options.packages, options.ignore, baseDir);
495
+ const sortedFiles = sortFiles(files);
496
+ const fileHashCache = /* @__PURE__ */ new Map();
497
+ const fileHashInputs = [];
498
+ for (const file of sortedFiles) {
499
+ const filePath = path2__namespace.resolve(baseDir, file);
500
+ let realPath = filePath;
501
+ let stats = await fs__namespace.promises.lstat(filePath);
502
+ if (stats.isSymbolicLink()) {
503
+ try {
504
+ realPath = await fs__namespace.promises.realpath(filePath);
505
+ } catch {
506
+ continue;
507
+ }
508
+ try {
509
+ stats = await fs__namespace.promises.stat(realPath);
510
+ } catch {
511
+ continue;
512
+ }
513
+ }
514
+ if (!stats.isFile()) {
515
+ continue;
516
+ }
517
+ const normalizedPath = normalizePath(file);
518
+ let hash;
519
+ const cachedHash = fileHashCache.get(realPath);
520
+ if (cachedHash !== void 0) {
521
+ hash = cachedHash;
522
+ } else {
523
+ hash = await hashFileAsync(realPath, normalizedPath, stats);
524
+ fileHashCache.set(realPath, hash);
525
+ }
526
+ fileHashInputs.push({ relativePath: normalizedPath, hash });
527
+ }
528
+ const fingerprint = computeFinalFingerprint(fileHashInputs);
529
+ return {
530
+ fingerprint,
531
+ files: sortedFiles,
532
+ fileCount: sortedFiles.length
533
+ };
534
+ }
535
+ function computeFingerprintSync(options) {
536
+ const baseDir = validateOptions(options);
537
+ const files = listPackageFilesSync(options.packages, options.ignore, baseDir);
538
+ const sortedFiles = sortFiles(files);
539
+ const fileHashCache = /* @__PURE__ */ new Map();
540
+ const fileHashInputs = [];
541
+ for (const file of sortedFiles) {
542
+ const filePath = path2__namespace.resolve(baseDir, file);
543
+ let realPath = filePath;
544
+ let stats = fs__namespace.lstatSync(filePath);
545
+ if (stats.isSymbolicLink()) {
546
+ try {
547
+ realPath = fs__namespace.realpathSync(filePath);
548
+ } catch {
549
+ continue;
550
+ }
551
+ try {
552
+ stats = fs__namespace.statSync(realPath);
553
+ } catch {
554
+ continue;
555
+ }
556
+ }
557
+ if (!stats.isFile()) {
558
+ continue;
559
+ }
560
+ const normalizedPath = normalizePath(file);
561
+ let hash;
562
+ const cachedHash = fileHashCache.get(realPath);
563
+ if (cachedHash !== void 0) {
564
+ hash = cachedHash;
565
+ } else {
566
+ hash = hashFileSync(realPath, normalizedPath);
567
+ fileHashCache.set(realPath, hash);
568
+ }
569
+ fileHashInputs.push({ relativePath: normalizedPath, hash });
570
+ }
571
+ const fingerprint = computeFinalFingerprint(fileHashInputs);
572
+ return {
573
+ fingerprint,
574
+ files: sortedFiles,
575
+ fileCount: sortedFiles.length
576
+ };
577
+ }
578
+ async function listPackageFiles(packages, ignore = [], baseDir = process.cwd()) {
579
+ const allFiles = [];
580
+ for (const pkg of packages) {
581
+ const patterns = [`${pkg}/**/*`];
582
+ const files = await tinyglobby.glob(patterns, {
583
+ cwd: baseDir,
584
+ ignore,
585
+ onlyFiles: true,
586
+ dot: true,
587
+ // Include dotfiles
588
+ absolute: false
589
+ // Return relative paths
590
+ });
591
+ allFiles.push(...files);
592
+ }
593
+ return allFiles;
594
+ }
595
+ function listPackageFilesSync(packages, ignore = [], baseDir = process.cwd()) {
596
+ const allFiles = [];
597
+ for (const pkg of packages) {
598
+ const patterns = [`${pkg}/**/*`];
599
+ const files = tinyglobby.globSync(patterns, {
600
+ cwd: baseDir,
601
+ ignore,
602
+ onlyFiles: true,
603
+ dot: true,
604
+ // Include dotfiles
605
+ absolute: false
606
+ // Return relative paths
607
+ });
608
+ allFiles.push(...files);
609
+ }
610
+ return allFiles;
611
+ }
612
+ var canonicalize = canonicalizeNamespace__namespace;
613
+ var serialize = canonicalize.default;
614
+ var attestationSchema = zod.z.object({
615
+ suite: zod.z.string().min(1),
616
+ fingerprint: zod.z.string().regex(/^sha256:[a-f0-9]{64}$/),
617
+ attestedAt: zod.z.string().datetime(),
618
+ attestedBy: zod.z.string().min(1),
619
+ command: zod.z.string().min(1),
620
+ exitCode: zod.z.literal(0)
621
+ });
622
+ var attestationsFileSchema = zod.z.object({
623
+ schemaVersion: zod.z.literal("1"),
624
+ attestations: zod.z.array(attestationSchema),
625
+ signature: zod.z.string()
626
+ // Will be validated by crypto module
627
+ });
628
+ function isNodeError(error) {
629
+ if (error === null || typeof error !== "object") {
630
+ return false;
631
+ }
632
+ if (!("code" in error)) {
633
+ return false;
634
+ }
635
+ const errorObj = error;
636
+ return typeof errorObj.code === "string";
637
+ }
638
+ async function readAttestations(filePath) {
639
+ try {
640
+ const content = await fs__namespace.promises.readFile(filePath, "utf-8");
641
+ const parsed = JSON.parse(content);
642
+ return attestationsFileSchema.parse(parsed);
643
+ } catch (error) {
644
+ if (isNodeError(error) && error.code === "ENOENT") {
645
+ return null;
646
+ }
647
+ throw error;
648
+ }
649
+ }
650
+ function readAttestationsSync(filePath) {
651
+ try {
652
+ const content = fs__namespace.readFileSync(filePath, "utf-8");
653
+ const parsed = JSON.parse(content);
654
+ return attestationsFileSchema.parse(parsed);
655
+ } catch (error) {
656
+ if (isNodeError(error) && error.code === "ENOENT") {
657
+ return null;
658
+ }
659
+ throw error;
660
+ }
661
+ }
662
+ async function writeAttestations(filePath, attestations, signature) {
663
+ const fileContent = {
664
+ schemaVersion: "1",
665
+ attestations,
666
+ signature
667
+ };
668
+ attestationsFileSchema.parse(fileContent);
669
+ const dir = path2__namespace.dirname(filePath);
670
+ await fs__namespace.promises.mkdir(dir, { recursive: true });
671
+ const json = JSON.stringify(fileContent, null, 2);
672
+ await fs__namespace.promises.writeFile(filePath, json, "utf-8");
673
+ }
674
+ function writeAttestationsSync(filePath, attestations, signature) {
675
+ const fileContent = {
676
+ schemaVersion: "1",
677
+ attestations,
678
+ signature
679
+ };
680
+ attestationsFileSchema.parse(fileContent);
681
+ const dir = path2__namespace.dirname(filePath);
682
+ fs__namespace.mkdirSync(dir, { recursive: true });
683
+ const json = JSON.stringify(fileContent, null, 2);
684
+ fs__namespace.writeFileSync(filePath, json, "utf-8");
685
+ }
686
+ function findAttestation(attestations, suite) {
687
+ return attestations.attestations.find((a) => a.suite === suite);
688
+ }
689
+ function upsertAttestation(attestations, newAttestation) {
690
+ attestationSchema.parse(newAttestation);
691
+ const existingIndex = attestations.findIndex((a) => a.suite === newAttestation.suite);
692
+ if (existingIndex === -1) {
693
+ return [...attestations, newAttestation];
694
+ } else {
695
+ const updated = [...attestations];
696
+ updated[existingIndex] = newAttestation;
697
+ return updated;
698
+ }
699
+ }
700
+ function removeAttestation(attestations, suite) {
701
+ return attestations.filter((a) => a.suite !== suite);
702
+ }
703
+ function canonicalizeAttestations(attestations) {
704
+ const canonical = serialize(attestations);
705
+ if (canonical === void 0) {
706
+ throw new Error("Failed to canonicalize attestations");
707
+ }
708
+ return canonical;
709
+ }
710
+ function createAttestation(params) {
711
+ const attestation = {
712
+ suite: params.suite,
713
+ fingerprint: params.fingerprint,
714
+ attestedAt: (/* @__PURE__ */ new Date()).toISOString(),
715
+ attestedBy: params.attestedBy ?? os__namespace.userInfo().username,
716
+ command: params.command,
717
+ exitCode: 0
718
+ };
719
+ attestationSchema.parse(attestation);
720
+ return attestation;
721
+ }
722
+ async function writeSignedAttestations(options) {
723
+ const { sign: sign2 } = await Promise.resolve().then(() => (init_crypto(), crypto_exports));
724
+ const canonical = canonicalizeAttestations(options.attestations);
725
+ const signature = await sign2({
726
+ privateKeyPath: options.privateKeyPath,
727
+ data: canonical
728
+ });
729
+ await writeAttestations(options.filePath, options.attestations, signature);
730
+ }
731
+ async function readAndVerifyAttestations(options) {
732
+ const { verify: verify2 } = await Promise.resolve().then(() => (init_crypto(), crypto_exports));
733
+ const file = await readAttestations(options.filePath);
734
+ if (!file) {
735
+ throw new Error(`Attestations file not found: ${options.filePath}`);
736
+ }
737
+ const canonical = canonicalizeAttestations(file.attestations);
738
+ const isValid = await verify2({
739
+ publicKeyPath: options.publicKeyPath,
740
+ data: canonical,
741
+ signature: file.signature
742
+ });
743
+ if (!isValid) {
744
+ throw new SignatureInvalidError(options.filePath);
745
+ }
746
+ return file;
747
+ }
748
+ var SignatureInvalidError = class extends Error {
749
+ /**
750
+ * Create a new SignatureInvalidError.
751
+ * @param filePath - Path to the file that failed verification
752
+ */
753
+ constructor(filePath) {
754
+ super(`Signature verification failed for: ${filePath}`);
755
+ this.name = "SignatureInvalidError";
756
+ }
757
+ };
758
+
759
+ // src/index.ts
760
+ init_crypto();
761
+ async function verifyAttestations(options) {
762
+ const { config, repoRoot = process.cwd() } = options;
763
+ const errors = [];
764
+ const suiteResults = [];
765
+ let signatureValid = true;
766
+ let attestationsFile = null;
767
+ const attestationsPath = resolvePath(config.settings.attestationsPath, repoRoot);
768
+ const publicKeyPath = resolvePath(config.settings.publicKeyPath, repoRoot);
769
+ try {
770
+ if (!fs__namespace.existsSync(attestationsPath)) {
771
+ attestationsFile = null;
772
+ } else if (!fs__namespace.existsSync(publicKeyPath)) {
773
+ errors.push(`Public key not found: ${publicKeyPath}`);
774
+ signatureValid = false;
775
+ } else {
776
+ attestationsFile = await readAndVerifyAttestations({
777
+ filePath: attestationsPath,
778
+ publicKeyPath
779
+ });
780
+ }
781
+ } catch (err) {
782
+ if (err instanceof SignatureInvalidError) {
783
+ signatureValid = false;
784
+ errors.push(err.message);
785
+ } else if (err instanceof Error) {
786
+ errors.push(err.message);
787
+ }
788
+ }
789
+ const attestations = attestationsFile?.attestations ?? [];
790
+ for (const [suiteName, suiteConfig] of Object.entries(config.suites)) {
791
+ const result = await verifySuite({
792
+ suiteName,
793
+ suiteConfig,
794
+ attestations,
795
+ maxAgeDays: config.settings.maxAgeDays,
796
+ repoRoot
797
+ });
798
+ suiteResults.push(result);
799
+ }
800
+ checkInvalidationChains(config, suiteResults);
801
+ const allValid = signatureValid && suiteResults.every((r) => r.status === "VALID") && errors.length === 0;
802
+ return {
803
+ success: allValid,
804
+ signatureValid,
805
+ suites: suiteResults,
806
+ errors
807
+ };
808
+ }
809
+ async function verifySuite(options) {
810
+ const { suiteName, suiteConfig, attestations, maxAgeDays, repoRoot } = options;
811
+ const fingerprintOptions = {
812
+ packages: suiteConfig.packages.map((p) => resolvePath(p, repoRoot)),
813
+ baseDir: repoRoot,
814
+ ...suiteConfig.ignore && { ignore: suiteConfig.ignore }
815
+ };
816
+ const fingerprintResult = await computeFingerprint(fingerprintOptions);
817
+ const attestation = attestations.find((a) => a.suite === suiteName);
818
+ if (!attestation) {
819
+ return {
820
+ suite: suiteName,
821
+ status: "NEEDS_ATTESTATION",
822
+ fingerprint: fingerprintResult.fingerprint,
823
+ message: "No attestation found for this suite"
824
+ };
825
+ }
826
+ if (attestation.fingerprint !== fingerprintResult.fingerprint) {
827
+ return {
828
+ suite: suiteName,
829
+ status: "FINGERPRINT_CHANGED",
830
+ fingerprint: fingerprintResult.fingerprint,
831
+ attestation,
832
+ message: `Fingerprint changed from ${attestation.fingerprint.slice(0, 20)}... to ${fingerprintResult.fingerprint.slice(0, 20)}...`
833
+ };
834
+ }
835
+ const attestedAt = new Date(attestation.attestedAt);
836
+ const ageMs = Date.now() - attestedAt.getTime();
837
+ const ageDays = Math.floor(ageMs / (1e3 * 60 * 60 * 24));
838
+ if (ageDays > maxAgeDays) {
839
+ return {
840
+ suite: suiteName,
841
+ status: "EXPIRED",
842
+ fingerprint: fingerprintResult.fingerprint,
843
+ attestation,
844
+ age: ageDays,
845
+ message: `Attestation expired (${String(ageDays)} days old, max ${String(maxAgeDays)} days)`
846
+ };
847
+ }
848
+ return {
849
+ suite: suiteName,
850
+ status: "VALID",
851
+ fingerprint: fingerprintResult.fingerprint,
852
+ attestation,
853
+ age: ageDays
854
+ };
855
+ }
856
+ function checkInvalidationChains(config, results) {
857
+ for (const [parentName, parentConfig] of Object.entries(config.suites)) {
858
+ const invalidates = parentConfig.invalidates ?? [];
859
+ const parentResult = results.find((r) => r.suite === parentName);
860
+ if (!parentResult?.attestation) continue;
861
+ const parentTime = new Date(parentResult.attestation.attestedAt).getTime();
862
+ for (const childName of invalidates) {
863
+ const childResult = results.find((r) => r.suite === childName);
864
+ if (!childResult?.attestation) continue;
865
+ const childTime = new Date(childResult.attestation.attestedAt).getTime();
866
+ if (parentTime > childTime && childResult.status === "VALID") {
867
+ childResult.status = "INVALIDATED_BY_PARENT";
868
+ childResult.message = `Invalidated by ${parentName} (attested later)`;
869
+ }
870
+ }
871
+ }
872
+ }
873
+ function resolvePath(relativePath, baseDir) {
874
+ if (path2__namespace.isAbsolute(relativePath)) {
875
+ return relativePath;
876
+ }
877
+ return path2__namespace.join(baseDir, relativePath);
878
+ }
879
+
880
+ // src/index.ts
881
+ var version = "0.0.0";
882
+
883
+ exports.ConfigNotFoundError = ConfigNotFoundError;
884
+ exports.ConfigValidationError = ConfigValidationError;
885
+ exports.SignatureInvalidError = SignatureInvalidError;
886
+ exports.canonicalizeAttestations = canonicalizeAttestations;
887
+ exports.checkOpenSSL = checkOpenSSL;
888
+ exports.computeFingerprint = computeFingerprint;
889
+ exports.computeFingerprintSync = computeFingerprintSync;
890
+ exports.createAttestation = createAttestation;
891
+ exports.findAttestation = findAttestation;
892
+ exports.findConfigPath = findConfigPath;
893
+ exports.generateKeyPair = generateKeyPair;
894
+ exports.getDefaultPrivateKeyPath = getDefaultPrivateKeyPath;
895
+ exports.getDefaultPublicKeyPath = getDefaultPublicKeyPath;
896
+ exports.listPackageFiles = listPackageFiles;
897
+ exports.loadConfig = loadConfig;
898
+ exports.loadConfigSync = loadConfigSync;
899
+ exports.readAndVerifyAttestations = readAndVerifyAttestations;
900
+ exports.readAttestations = readAttestations;
901
+ exports.readAttestationsSync = readAttestationsSync;
902
+ exports.removeAttestation = removeAttestation;
903
+ exports.resolveConfigPaths = resolveConfigPaths;
904
+ exports.setKeyPermissions = setKeyPermissions;
905
+ exports.sign = sign;
906
+ exports.toAttestItConfig = toAttestItConfig;
907
+ exports.upsertAttestation = upsertAttestation;
908
+ exports.verify = verify;
909
+ exports.verifyAttestations = verifyAttestations;
910
+ exports.version = version;
911
+ exports.writeAttestations = writeAttestations;
912
+ exports.writeAttestationsSync = writeAttestationsSync;
913
+ exports.writeSignedAttestations = writeSignedAttestations;
914
+ //# sourceMappingURL=index.cjs.map
915
+ //# sourceMappingURL=index.cjs.map