@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/README.md +126 -0
- package/dist/chunk-UWYR7JNE.js +212 -0
- package/dist/chunk-UWYR7JNE.js.map +1 -0
- package/dist/core-alpha.d.ts +711 -0
- package/dist/core-beta.d.ts +711 -0
- package/dist/core-public.d.ts +711 -0
- package/dist/core-unstripped.d.ts +711 -0
- package/dist/crypto-ITLMIMRJ.js +3 -0
- package/dist/crypto-ITLMIMRJ.js.map +1 -0
- package/dist/index.cjs +915 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +691 -0
- package/dist/index.d.ts +691 -0
- package/dist/index.js +629 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
export { checkOpenSSL, generateKeyPair, getDefaultPrivateKeyPath, getDefaultPublicKeyPath, setKeyPermissions, sign, verify } from './chunk-UWYR7JNE.js';
|
|
2
|
+
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';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import * as crypto from 'crypto';
|
|
10
|
+
import { glob, globSync } from 'tinyglobby';
|
|
11
|
+
import * as os from 'os';
|
|
12
|
+
import * as canonicalizeNamespace from 'canonicalize';
|
|
13
|
+
|
|
14
|
+
var settingsSchema = z.object({
|
|
15
|
+
maxAgeDays: z.number().int().positive().default(30),
|
|
16
|
+
publicKeyPath: z.string().default(".attest-it/pubkey.pem"),
|
|
17
|
+
attestationsPath: z.string().default(".attest-it/attestations.json"),
|
|
18
|
+
defaultCommand: z.string().optional(),
|
|
19
|
+
algorithm: z.enum(["ed25519", "rsa"]).default("ed25519")
|
|
20
|
+
}).strict();
|
|
21
|
+
var suiteSchema = z.object({
|
|
22
|
+
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"),
|
|
24
|
+
files: z.array(z.string().min(1, "File path cannot be empty")).optional(),
|
|
25
|
+
ignore: z.array(z.string().min(1, "Ignore pattern cannot be empty")).optional(),
|
|
26
|
+
command: z.string().optional(),
|
|
27
|
+
invalidates: z.array(z.string().min(1, "Invalidated suite name cannot be empty")).optional()
|
|
28
|
+
}).strict();
|
|
29
|
+
var configSchema = z.object({
|
|
30
|
+
version: z.literal(1),
|
|
31
|
+
settings: settingsSchema.default({}),
|
|
32
|
+
suites: z.record(z.string(), suiteSchema).refine((suites) => Object.keys(suites).length >= 1, {
|
|
33
|
+
message: "At least one suite must be defined"
|
|
34
|
+
})
|
|
35
|
+
}).strict();
|
|
36
|
+
var ConfigValidationError = class extends Error {
|
|
37
|
+
constructor(message, issues) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.issues = issues;
|
|
40
|
+
this.name = "ConfigValidationError";
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
var ConfigNotFoundError = class extends Error {
|
|
44
|
+
constructor(message) {
|
|
45
|
+
super(message);
|
|
46
|
+
this.name = "ConfigNotFoundError";
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
function parseConfigContent(content, format) {
|
|
50
|
+
let rawConfig;
|
|
51
|
+
try {
|
|
52
|
+
if (format === "yaml") {
|
|
53
|
+
rawConfig = parse(content);
|
|
54
|
+
} else {
|
|
55
|
+
rawConfig = JSON.parse(content);
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
throw new ConfigValidationError(
|
|
59
|
+
`Failed to parse ${format.toUpperCase()}: ${error instanceof Error ? error.message : String(error)}`,
|
|
60
|
+
[]
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
const result = configSchema.safeParse(rawConfig);
|
|
64
|
+
if (!result.success) {
|
|
65
|
+
throw new ConfigValidationError(
|
|
66
|
+
"Configuration validation failed:\n" + result.error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n"),
|
|
67
|
+
result.error.issues
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return result.data;
|
|
71
|
+
}
|
|
72
|
+
function getConfigFormat(filePath) {
|
|
73
|
+
const ext = filePath.toLowerCase();
|
|
74
|
+
if (ext.endsWith(".yaml") || ext.endsWith(".yml")) {
|
|
75
|
+
return "yaml";
|
|
76
|
+
}
|
|
77
|
+
if (ext.endsWith(".json")) {
|
|
78
|
+
return "json";
|
|
79
|
+
}
|
|
80
|
+
return "yaml";
|
|
81
|
+
}
|
|
82
|
+
function findConfigPath(startDir = process.cwd()) {
|
|
83
|
+
const configDir = join(startDir, ".attest-it");
|
|
84
|
+
const candidates = ["config.yaml", "config.yml", "config.json"];
|
|
85
|
+
for (const candidate of candidates) {
|
|
86
|
+
const configPath = join(configDir, candidate);
|
|
87
|
+
try {
|
|
88
|
+
readFileSync(configPath, "utf8");
|
|
89
|
+
return configPath;
|
|
90
|
+
} catch {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
async function loadConfig(configPath) {
|
|
97
|
+
const resolvedPath = configPath ?? findConfigPath();
|
|
98
|
+
if (!resolvedPath) {
|
|
99
|
+
throw new ConfigNotFoundError(
|
|
100
|
+
"Configuration file not found. Expected .attest-it/config.yaml, .attest-it/config.yml, or .attest-it/config.json"
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const content = await readFile(resolvedPath, "utf8");
|
|
105
|
+
const format = getConfigFormat(resolvedPath);
|
|
106
|
+
return parseConfigContent(content, format);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
if (error instanceof ConfigValidationError) {
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
throw new ConfigNotFoundError(
|
|
112
|
+
`Failed to read configuration file at ${resolvedPath}: ${String(error)}`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function loadConfigSync(configPath) {
|
|
117
|
+
const resolvedPath = configPath ?? findConfigPath();
|
|
118
|
+
if (!resolvedPath) {
|
|
119
|
+
throw new ConfigNotFoundError(
|
|
120
|
+
"Configuration file not found. Expected .attest-it/config.yaml, .attest-it/config.yml, or .attest-it/config.json"
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
const content = readFileSync(resolvedPath, "utf8");
|
|
125
|
+
const format = getConfigFormat(resolvedPath);
|
|
126
|
+
return parseConfigContent(content, format);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (error instanceof ConfigValidationError) {
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
throw new ConfigNotFoundError(
|
|
132
|
+
`Failed to read configuration file at ${resolvedPath}: ${String(error)}`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function resolveConfigPaths(config, repoRoot) {
|
|
137
|
+
return {
|
|
138
|
+
...config,
|
|
139
|
+
settings: {
|
|
140
|
+
...config.settings,
|
|
141
|
+
publicKeyPath: resolve(repoRoot, config.settings.publicKeyPath),
|
|
142
|
+
attestationsPath: resolve(repoRoot, config.settings.attestationsPath)
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function toAttestItConfig(config) {
|
|
147
|
+
return {
|
|
148
|
+
version: config.version,
|
|
149
|
+
settings: {
|
|
150
|
+
maxAgeDays: config.settings.maxAgeDays,
|
|
151
|
+
publicKeyPath: config.settings.publicKeyPath,
|
|
152
|
+
attestationsPath: config.settings.attestationsPath,
|
|
153
|
+
algorithm: config.settings.algorithm,
|
|
154
|
+
...config.settings.defaultCommand !== void 0 && {
|
|
155
|
+
defaultCommand: config.settings.defaultCommand
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
suites: Object.fromEntries(
|
|
159
|
+
Object.entries(config.suites).map(([name, suite]) => [
|
|
160
|
+
name,
|
|
161
|
+
{
|
|
162
|
+
packages: suite.packages,
|
|
163
|
+
...suite.description !== void 0 && { description: suite.description },
|
|
164
|
+
...suite.files !== void 0 && { files: suite.files },
|
|
165
|
+
...suite.ignore !== void 0 && { ignore: suite.ignore },
|
|
166
|
+
...suite.command !== void 0 && { command: suite.command },
|
|
167
|
+
...suite.invalidates !== void 0 && { invalidates: suite.invalidates }
|
|
168
|
+
}
|
|
169
|
+
])
|
|
170
|
+
)
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
var LARGE_FILE_THRESHOLD = 50 * 1024 * 1024;
|
|
174
|
+
function sortFiles(files) {
|
|
175
|
+
return [...files].sort((a, b) => {
|
|
176
|
+
if (a < b) return -1;
|
|
177
|
+
if (a > b) return 1;
|
|
178
|
+
return 0;
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
function normalizePath(filePath) {
|
|
182
|
+
return filePath.split(path.sep).join("/");
|
|
183
|
+
}
|
|
184
|
+
function computeFinalFingerprint(fileHashes) {
|
|
185
|
+
const sorted = [...fileHashes].sort((a, b) => {
|
|
186
|
+
if (a.relativePath < b.relativePath) return -1;
|
|
187
|
+
if (a.relativePath > b.relativePath) return 1;
|
|
188
|
+
return 0;
|
|
189
|
+
});
|
|
190
|
+
const hashes = sorted.map((input) => input.hash);
|
|
191
|
+
const concatenated = Buffer.concat(hashes);
|
|
192
|
+
const finalHash = crypto.createHash("sha256").update(concatenated).digest();
|
|
193
|
+
return `sha256:${finalHash.toString("hex")}`;
|
|
194
|
+
}
|
|
195
|
+
async function hashFileAsync(realPath, normalizedPath, stats) {
|
|
196
|
+
if (stats.size > LARGE_FILE_THRESHOLD) {
|
|
197
|
+
return new Promise((resolve3, reject) => {
|
|
198
|
+
const hash2 = crypto.createHash("sha256");
|
|
199
|
+
hash2.update(normalizedPath);
|
|
200
|
+
hash2.update("\0");
|
|
201
|
+
const stream = fs.createReadStream(realPath);
|
|
202
|
+
stream.on("data", (chunk) => {
|
|
203
|
+
hash2.update(chunk);
|
|
204
|
+
});
|
|
205
|
+
stream.on("end", () => {
|
|
206
|
+
resolve3(hash2.digest());
|
|
207
|
+
});
|
|
208
|
+
stream.on("error", reject);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
const content = await fs.promises.readFile(realPath);
|
|
212
|
+
const hash = crypto.createHash("sha256");
|
|
213
|
+
hash.update(normalizedPath);
|
|
214
|
+
hash.update("\0");
|
|
215
|
+
hash.update(content);
|
|
216
|
+
return hash.digest();
|
|
217
|
+
}
|
|
218
|
+
function hashFileSync(realPath, normalizedPath) {
|
|
219
|
+
const content = fs.readFileSync(realPath);
|
|
220
|
+
const hash = crypto.createHash("sha256");
|
|
221
|
+
hash.update(normalizedPath);
|
|
222
|
+
hash.update("\0");
|
|
223
|
+
hash.update(content);
|
|
224
|
+
return hash.digest();
|
|
225
|
+
}
|
|
226
|
+
function validateOptions(options) {
|
|
227
|
+
if (options.packages.length === 0) {
|
|
228
|
+
throw new Error("packages array must not be empty");
|
|
229
|
+
}
|
|
230
|
+
const baseDir = options.baseDir ?? process.cwd();
|
|
231
|
+
for (const pkg of options.packages) {
|
|
232
|
+
const pkgPath = path.resolve(baseDir, pkg);
|
|
233
|
+
if (!fs.existsSync(pkgPath)) {
|
|
234
|
+
throw new Error(`Package path does not exist: ${pkgPath}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return baseDir;
|
|
238
|
+
}
|
|
239
|
+
async function computeFingerprint(options) {
|
|
240
|
+
const baseDir = validateOptions(options);
|
|
241
|
+
const files = await listPackageFiles(options.packages, options.ignore, baseDir);
|
|
242
|
+
const sortedFiles = sortFiles(files);
|
|
243
|
+
const fileHashCache = /* @__PURE__ */ new Map();
|
|
244
|
+
const fileHashInputs = [];
|
|
245
|
+
for (const file of sortedFiles) {
|
|
246
|
+
const filePath = path.resolve(baseDir, file);
|
|
247
|
+
let realPath = filePath;
|
|
248
|
+
let stats = await fs.promises.lstat(filePath);
|
|
249
|
+
if (stats.isSymbolicLink()) {
|
|
250
|
+
try {
|
|
251
|
+
realPath = await fs.promises.realpath(filePath);
|
|
252
|
+
} catch {
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
stats = await fs.promises.stat(realPath);
|
|
257
|
+
} catch {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (!stats.isFile()) {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const normalizedPath = normalizePath(file);
|
|
265
|
+
let hash;
|
|
266
|
+
const cachedHash = fileHashCache.get(realPath);
|
|
267
|
+
if (cachedHash !== void 0) {
|
|
268
|
+
hash = cachedHash;
|
|
269
|
+
} else {
|
|
270
|
+
hash = await hashFileAsync(realPath, normalizedPath, stats);
|
|
271
|
+
fileHashCache.set(realPath, hash);
|
|
272
|
+
}
|
|
273
|
+
fileHashInputs.push({ relativePath: normalizedPath, hash });
|
|
274
|
+
}
|
|
275
|
+
const fingerprint = computeFinalFingerprint(fileHashInputs);
|
|
276
|
+
return {
|
|
277
|
+
fingerprint,
|
|
278
|
+
files: sortedFiles,
|
|
279
|
+
fileCount: sortedFiles.length
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
function computeFingerprintSync(options) {
|
|
283
|
+
const baseDir = validateOptions(options);
|
|
284
|
+
const files = listPackageFilesSync(options.packages, options.ignore, baseDir);
|
|
285
|
+
const sortedFiles = sortFiles(files);
|
|
286
|
+
const fileHashCache = /* @__PURE__ */ new Map();
|
|
287
|
+
const fileHashInputs = [];
|
|
288
|
+
for (const file of sortedFiles) {
|
|
289
|
+
const filePath = path.resolve(baseDir, file);
|
|
290
|
+
let realPath = filePath;
|
|
291
|
+
let stats = fs.lstatSync(filePath);
|
|
292
|
+
if (stats.isSymbolicLink()) {
|
|
293
|
+
try {
|
|
294
|
+
realPath = fs.realpathSync(filePath);
|
|
295
|
+
} catch {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
stats = fs.statSync(realPath);
|
|
300
|
+
} catch {
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (!stats.isFile()) {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
const normalizedPath = normalizePath(file);
|
|
308
|
+
let hash;
|
|
309
|
+
const cachedHash = fileHashCache.get(realPath);
|
|
310
|
+
if (cachedHash !== void 0) {
|
|
311
|
+
hash = cachedHash;
|
|
312
|
+
} else {
|
|
313
|
+
hash = hashFileSync(realPath, normalizedPath);
|
|
314
|
+
fileHashCache.set(realPath, hash);
|
|
315
|
+
}
|
|
316
|
+
fileHashInputs.push({ relativePath: normalizedPath, hash });
|
|
317
|
+
}
|
|
318
|
+
const fingerprint = computeFinalFingerprint(fileHashInputs);
|
|
319
|
+
return {
|
|
320
|
+
fingerprint,
|
|
321
|
+
files: sortedFiles,
|
|
322
|
+
fileCount: sortedFiles.length
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
async function listPackageFiles(packages, ignore = [], baseDir = process.cwd()) {
|
|
326
|
+
const allFiles = [];
|
|
327
|
+
for (const pkg of packages) {
|
|
328
|
+
const patterns = [`${pkg}/**/*`];
|
|
329
|
+
const files = await glob(patterns, {
|
|
330
|
+
cwd: baseDir,
|
|
331
|
+
ignore,
|
|
332
|
+
onlyFiles: true,
|
|
333
|
+
dot: true,
|
|
334
|
+
// Include dotfiles
|
|
335
|
+
absolute: false
|
|
336
|
+
// Return relative paths
|
|
337
|
+
});
|
|
338
|
+
allFiles.push(...files);
|
|
339
|
+
}
|
|
340
|
+
return allFiles;
|
|
341
|
+
}
|
|
342
|
+
function listPackageFilesSync(packages, ignore = [], baseDir = process.cwd()) {
|
|
343
|
+
const allFiles = [];
|
|
344
|
+
for (const pkg of packages) {
|
|
345
|
+
const patterns = [`${pkg}/**/*`];
|
|
346
|
+
const files = globSync(patterns, {
|
|
347
|
+
cwd: baseDir,
|
|
348
|
+
ignore,
|
|
349
|
+
onlyFiles: true,
|
|
350
|
+
dot: true,
|
|
351
|
+
// Include dotfiles
|
|
352
|
+
absolute: false
|
|
353
|
+
// Return relative paths
|
|
354
|
+
});
|
|
355
|
+
allFiles.push(...files);
|
|
356
|
+
}
|
|
357
|
+
return allFiles;
|
|
358
|
+
}
|
|
359
|
+
var canonicalize = canonicalizeNamespace;
|
|
360
|
+
var serialize = canonicalize.default;
|
|
361
|
+
var attestationSchema = z.object({
|
|
362
|
+
suite: z.string().min(1),
|
|
363
|
+
fingerprint: z.string().regex(/^sha256:[a-f0-9]{64}$/),
|
|
364
|
+
attestedAt: z.string().datetime(),
|
|
365
|
+
attestedBy: z.string().min(1),
|
|
366
|
+
command: z.string().min(1),
|
|
367
|
+
exitCode: z.literal(0)
|
|
368
|
+
});
|
|
369
|
+
var attestationsFileSchema = z.object({
|
|
370
|
+
schemaVersion: z.literal("1"),
|
|
371
|
+
attestations: z.array(attestationSchema),
|
|
372
|
+
signature: z.string()
|
|
373
|
+
// Will be validated by crypto module
|
|
374
|
+
});
|
|
375
|
+
function isNodeError(error) {
|
|
376
|
+
if (error === null || typeof error !== "object") {
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
if (!("code" in error)) {
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
const errorObj = error;
|
|
383
|
+
return typeof errorObj.code === "string";
|
|
384
|
+
}
|
|
385
|
+
async function readAttestations(filePath) {
|
|
386
|
+
try {
|
|
387
|
+
const content = await fs.promises.readFile(filePath, "utf-8");
|
|
388
|
+
const parsed = JSON.parse(content);
|
|
389
|
+
return attestationsFileSchema.parse(parsed);
|
|
390
|
+
} catch (error) {
|
|
391
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
throw error;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
function readAttestationsSync(filePath) {
|
|
398
|
+
try {
|
|
399
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
400
|
+
const parsed = JSON.parse(content);
|
|
401
|
+
return attestationsFileSchema.parse(parsed);
|
|
402
|
+
} catch (error) {
|
|
403
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
throw error;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
async function writeAttestations(filePath, attestations, signature) {
|
|
410
|
+
const fileContent = {
|
|
411
|
+
schemaVersion: "1",
|
|
412
|
+
attestations,
|
|
413
|
+
signature
|
|
414
|
+
};
|
|
415
|
+
attestationsFileSchema.parse(fileContent);
|
|
416
|
+
const dir = path.dirname(filePath);
|
|
417
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
418
|
+
const json = JSON.stringify(fileContent, null, 2);
|
|
419
|
+
await fs.promises.writeFile(filePath, json, "utf-8");
|
|
420
|
+
}
|
|
421
|
+
function writeAttestationsSync(filePath, attestations, signature) {
|
|
422
|
+
const fileContent = {
|
|
423
|
+
schemaVersion: "1",
|
|
424
|
+
attestations,
|
|
425
|
+
signature
|
|
426
|
+
};
|
|
427
|
+
attestationsFileSchema.parse(fileContent);
|
|
428
|
+
const dir = path.dirname(filePath);
|
|
429
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
430
|
+
const json = JSON.stringify(fileContent, null, 2);
|
|
431
|
+
fs.writeFileSync(filePath, json, "utf-8");
|
|
432
|
+
}
|
|
433
|
+
function findAttestation(attestations, suite) {
|
|
434
|
+
return attestations.attestations.find((a) => a.suite === suite);
|
|
435
|
+
}
|
|
436
|
+
function upsertAttestation(attestations, newAttestation) {
|
|
437
|
+
attestationSchema.parse(newAttestation);
|
|
438
|
+
const existingIndex = attestations.findIndex((a) => a.suite === newAttestation.suite);
|
|
439
|
+
if (existingIndex === -1) {
|
|
440
|
+
return [...attestations, newAttestation];
|
|
441
|
+
} else {
|
|
442
|
+
const updated = [...attestations];
|
|
443
|
+
updated[existingIndex] = newAttestation;
|
|
444
|
+
return updated;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
function removeAttestation(attestations, suite) {
|
|
448
|
+
return attestations.filter((a) => a.suite !== suite);
|
|
449
|
+
}
|
|
450
|
+
function canonicalizeAttestations(attestations) {
|
|
451
|
+
const canonical = serialize(attestations);
|
|
452
|
+
if (canonical === void 0) {
|
|
453
|
+
throw new Error("Failed to canonicalize attestations");
|
|
454
|
+
}
|
|
455
|
+
return canonical;
|
|
456
|
+
}
|
|
457
|
+
function createAttestation(params) {
|
|
458
|
+
const attestation = {
|
|
459
|
+
suite: params.suite,
|
|
460
|
+
fingerprint: params.fingerprint,
|
|
461
|
+
attestedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
462
|
+
attestedBy: params.attestedBy ?? os.userInfo().username,
|
|
463
|
+
command: params.command,
|
|
464
|
+
exitCode: 0
|
|
465
|
+
};
|
|
466
|
+
attestationSchema.parse(attestation);
|
|
467
|
+
return attestation;
|
|
468
|
+
}
|
|
469
|
+
async function writeSignedAttestations(options) {
|
|
470
|
+
const { sign: sign2 } = await import('./crypto-ITLMIMRJ.js');
|
|
471
|
+
const canonical = canonicalizeAttestations(options.attestations);
|
|
472
|
+
const signature = await sign2({
|
|
473
|
+
privateKeyPath: options.privateKeyPath,
|
|
474
|
+
data: canonical
|
|
475
|
+
});
|
|
476
|
+
await writeAttestations(options.filePath, options.attestations, signature);
|
|
477
|
+
}
|
|
478
|
+
async function readAndVerifyAttestations(options) {
|
|
479
|
+
const { verify: verify2 } = await import('./crypto-ITLMIMRJ.js');
|
|
480
|
+
const file = await readAttestations(options.filePath);
|
|
481
|
+
if (!file) {
|
|
482
|
+
throw new Error(`Attestations file not found: ${options.filePath}`);
|
|
483
|
+
}
|
|
484
|
+
const canonical = canonicalizeAttestations(file.attestations);
|
|
485
|
+
const isValid = await verify2({
|
|
486
|
+
publicKeyPath: options.publicKeyPath,
|
|
487
|
+
data: canonical,
|
|
488
|
+
signature: file.signature
|
|
489
|
+
});
|
|
490
|
+
if (!isValid) {
|
|
491
|
+
throw new SignatureInvalidError(options.filePath);
|
|
492
|
+
}
|
|
493
|
+
return file;
|
|
494
|
+
}
|
|
495
|
+
var SignatureInvalidError = class extends Error {
|
|
496
|
+
/**
|
|
497
|
+
* Create a new SignatureInvalidError.
|
|
498
|
+
* @param filePath - Path to the file that failed verification
|
|
499
|
+
*/
|
|
500
|
+
constructor(filePath) {
|
|
501
|
+
super(`Signature verification failed for: ${filePath}`);
|
|
502
|
+
this.name = "SignatureInvalidError";
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
async function verifyAttestations(options) {
|
|
506
|
+
const { config, repoRoot = process.cwd() } = options;
|
|
507
|
+
const errors = [];
|
|
508
|
+
const suiteResults = [];
|
|
509
|
+
let signatureValid = true;
|
|
510
|
+
let attestationsFile = null;
|
|
511
|
+
const attestationsPath = resolvePath(config.settings.attestationsPath, repoRoot);
|
|
512
|
+
const publicKeyPath = resolvePath(config.settings.publicKeyPath, repoRoot);
|
|
513
|
+
try {
|
|
514
|
+
if (!fs.existsSync(attestationsPath)) {
|
|
515
|
+
attestationsFile = null;
|
|
516
|
+
} else if (!fs.existsSync(publicKeyPath)) {
|
|
517
|
+
errors.push(`Public key not found: ${publicKeyPath}`);
|
|
518
|
+
signatureValid = false;
|
|
519
|
+
} else {
|
|
520
|
+
attestationsFile = await readAndVerifyAttestations({
|
|
521
|
+
filePath: attestationsPath,
|
|
522
|
+
publicKeyPath
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
} catch (err) {
|
|
526
|
+
if (err instanceof SignatureInvalidError) {
|
|
527
|
+
signatureValid = false;
|
|
528
|
+
errors.push(err.message);
|
|
529
|
+
} else if (err instanceof Error) {
|
|
530
|
+
errors.push(err.message);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
const attestations = attestationsFile?.attestations ?? [];
|
|
534
|
+
for (const [suiteName, suiteConfig] of Object.entries(config.suites)) {
|
|
535
|
+
const result = await verifySuite({
|
|
536
|
+
suiteName,
|
|
537
|
+
suiteConfig,
|
|
538
|
+
attestations,
|
|
539
|
+
maxAgeDays: config.settings.maxAgeDays,
|
|
540
|
+
repoRoot
|
|
541
|
+
});
|
|
542
|
+
suiteResults.push(result);
|
|
543
|
+
}
|
|
544
|
+
checkInvalidationChains(config, suiteResults);
|
|
545
|
+
const allValid = signatureValid && suiteResults.every((r) => r.status === "VALID") && errors.length === 0;
|
|
546
|
+
return {
|
|
547
|
+
success: allValid,
|
|
548
|
+
signatureValid,
|
|
549
|
+
suites: suiteResults,
|
|
550
|
+
errors
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
async function verifySuite(options) {
|
|
554
|
+
const { suiteName, suiteConfig, attestations, maxAgeDays, repoRoot } = options;
|
|
555
|
+
const fingerprintOptions = {
|
|
556
|
+
packages: suiteConfig.packages.map((p) => resolvePath(p, repoRoot)),
|
|
557
|
+
baseDir: repoRoot,
|
|
558
|
+
...suiteConfig.ignore && { ignore: suiteConfig.ignore }
|
|
559
|
+
};
|
|
560
|
+
const fingerprintResult = await computeFingerprint(fingerprintOptions);
|
|
561
|
+
const attestation = attestations.find((a) => a.suite === suiteName);
|
|
562
|
+
if (!attestation) {
|
|
563
|
+
return {
|
|
564
|
+
suite: suiteName,
|
|
565
|
+
status: "NEEDS_ATTESTATION",
|
|
566
|
+
fingerprint: fingerprintResult.fingerprint,
|
|
567
|
+
message: "No attestation found for this suite"
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
if (attestation.fingerprint !== fingerprintResult.fingerprint) {
|
|
571
|
+
return {
|
|
572
|
+
suite: suiteName,
|
|
573
|
+
status: "FINGERPRINT_CHANGED",
|
|
574
|
+
fingerprint: fingerprintResult.fingerprint,
|
|
575
|
+
attestation,
|
|
576
|
+
message: `Fingerprint changed from ${attestation.fingerprint.slice(0, 20)}... to ${fingerprintResult.fingerprint.slice(0, 20)}...`
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
const attestedAt = new Date(attestation.attestedAt);
|
|
580
|
+
const ageMs = Date.now() - attestedAt.getTime();
|
|
581
|
+
const ageDays = Math.floor(ageMs / (1e3 * 60 * 60 * 24));
|
|
582
|
+
if (ageDays > maxAgeDays) {
|
|
583
|
+
return {
|
|
584
|
+
suite: suiteName,
|
|
585
|
+
status: "EXPIRED",
|
|
586
|
+
fingerprint: fingerprintResult.fingerprint,
|
|
587
|
+
attestation,
|
|
588
|
+
age: ageDays,
|
|
589
|
+
message: `Attestation expired (${String(ageDays)} days old, max ${String(maxAgeDays)} days)`
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
return {
|
|
593
|
+
suite: suiteName,
|
|
594
|
+
status: "VALID",
|
|
595
|
+
fingerprint: fingerprintResult.fingerprint,
|
|
596
|
+
attestation,
|
|
597
|
+
age: ageDays
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
function checkInvalidationChains(config, results) {
|
|
601
|
+
for (const [parentName, parentConfig] of Object.entries(config.suites)) {
|
|
602
|
+
const invalidates = parentConfig.invalidates ?? [];
|
|
603
|
+
const parentResult = results.find((r) => r.suite === parentName);
|
|
604
|
+
if (!parentResult?.attestation) continue;
|
|
605
|
+
const parentTime = new Date(parentResult.attestation.attestedAt).getTime();
|
|
606
|
+
for (const childName of invalidates) {
|
|
607
|
+
const childResult = results.find((r) => r.suite === childName);
|
|
608
|
+
if (!childResult?.attestation) continue;
|
|
609
|
+
const childTime = new Date(childResult.attestation.attestedAt).getTime();
|
|
610
|
+
if (parentTime > childTime && childResult.status === "VALID") {
|
|
611
|
+
childResult.status = "INVALIDATED_BY_PARENT";
|
|
612
|
+
childResult.message = `Invalidated by ${parentName} (attested later)`;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
function resolvePath(relativePath, baseDir) {
|
|
618
|
+
if (path.isAbsolute(relativePath)) {
|
|
619
|
+
return relativePath;
|
|
620
|
+
}
|
|
621
|
+
return path.join(baseDir, relativePath);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// src/index.ts
|
|
625
|
+
var version = "0.0.0";
|
|
626
|
+
|
|
627
|
+
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 };
|
|
628
|
+
//# sourceMappingURL=index.js.map
|
|
629
|
+
//# sourceMappingURL=index.js.map
|