@attest-it/core 0.5.0 → 0.6.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/core-alpha.d.ts +1198 -531
- package/dist/core-beta.d.ts +1198 -531
- package/dist/core-public.d.ts +1198 -531
- package/dist/core-unstripped.d.ts +1198 -531
- package/dist/index.cjs +980 -122
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +743 -1
- package/dist/index.d.ts +739 -1
- package/dist/index.js +945 -104
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -2,14 +2,14 @@ import { getDefaultPrivateKeyPath, generateKeyPair, setKeyPermissions } from './
|
|
|
2
2
|
export { checkOpenSSL, generateKeyPair, getDefaultPrivateKeyPath, getDefaultPublicKeyPath, setKeyPermissions, sign, verify } from './chunk-VC3BBBBO.js';
|
|
3
3
|
import * as fs from 'fs';
|
|
4
4
|
import { readFileSync, mkdirSync, writeFileSync } from 'fs';
|
|
5
|
-
import * as
|
|
5
|
+
import * as fs7 from 'fs/promises';
|
|
6
6
|
import { readFile, mkdir, writeFile } from 'fs/promises';
|
|
7
7
|
import * as path6 from 'path';
|
|
8
8
|
import { join, resolve, dirname } from 'path';
|
|
9
9
|
import ms from 'ms';
|
|
10
|
-
import {
|
|
10
|
+
import { parse, stringify } from 'yaml';
|
|
11
11
|
import { z } from 'zod';
|
|
12
|
-
import * as
|
|
12
|
+
import * as crypto3 from 'crypto';
|
|
13
13
|
import { glob, globSync } from 'tinyglobby';
|
|
14
14
|
import * as os2 from 'os';
|
|
15
15
|
import { homedir } from 'os';
|
|
@@ -255,6 +255,299 @@ function toAttestItConfig(config) {
|
|
|
255
255
|
);
|
|
256
256
|
return result;
|
|
257
257
|
}
|
|
258
|
+
var teamMemberSchema2 = z.object({
|
|
259
|
+
name: z.string().min(1, "Team member name cannot be empty"),
|
|
260
|
+
email: z.string().email().optional(),
|
|
261
|
+
github: z.string().min(1).optional(),
|
|
262
|
+
publicKey: z.string().min(1, "Public key is required")
|
|
263
|
+
}).strict();
|
|
264
|
+
var fingerprintConfigSchema2 = z.object({
|
|
265
|
+
paths: z.array(z.string().min(1, "Path cannot be empty")).min(1, "At least one path is required"),
|
|
266
|
+
exclude: z.array(z.string().min(1, "Exclude pattern cannot be empty")).optional()
|
|
267
|
+
}).strict();
|
|
268
|
+
var durationSchema2 = z.string().refine(
|
|
269
|
+
(val) => {
|
|
270
|
+
try {
|
|
271
|
+
const parsed = ms(val);
|
|
272
|
+
return typeof parsed === "number" && parsed > 0;
|
|
273
|
+
} catch {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
message: 'Duration must be a valid duration string (e.g., "30d", "7d", "24h")'
|
|
279
|
+
}
|
|
280
|
+
);
|
|
281
|
+
var gateSchema2 = z.object({
|
|
282
|
+
name: z.string().min(1, "Gate name cannot be empty"),
|
|
283
|
+
description: z.string().min(1, "Gate description cannot be empty"),
|
|
284
|
+
authorizedSigners: z.array(z.string().min(1, "Authorized signer slug cannot be empty")).min(1, "At least one authorized signer is required"),
|
|
285
|
+
fingerprint: fingerprintConfigSchema2,
|
|
286
|
+
maxAge: durationSchema2
|
|
287
|
+
}).strict();
|
|
288
|
+
var keyProviderOptionsSchema2 = z.object({
|
|
289
|
+
privateKeyPath: z.string().optional(),
|
|
290
|
+
account: z.string().optional(),
|
|
291
|
+
vault: z.string().optional(),
|
|
292
|
+
itemName: z.string().optional()
|
|
293
|
+
}).passthrough();
|
|
294
|
+
var keyProviderSchema2 = z.object({
|
|
295
|
+
type: z.enum(["filesystem", "1password"]).or(z.string()),
|
|
296
|
+
options: keyProviderOptionsSchema2.optional()
|
|
297
|
+
}).strict();
|
|
298
|
+
|
|
299
|
+
// src/config/policy-schema.ts
|
|
300
|
+
var policySettingsSchema = z.object({
|
|
301
|
+
maxAgeDays: z.number().int().positive().default(30),
|
|
302
|
+
publicKeyPath: z.string().default(".attest-it/pubkey.pem"),
|
|
303
|
+
attestationsPath: z.string().default(".attest-it/attestations.json")
|
|
304
|
+
}).strict();
|
|
305
|
+
var policySchema = z.object({
|
|
306
|
+
version: z.literal(1),
|
|
307
|
+
settings: policySettingsSchema.default({}),
|
|
308
|
+
team: z.record(z.string(), teamMemberSchema2).optional(),
|
|
309
|
+
gates: z.record(z.string(), gateSchema2).optional()
|
|
310
|
+
}).strict();
|
|
311
|
+
var PolicyValidationError = class extends Error {
|
|
312
|
+
constructor(message, issues) {
|
|
313
|
+
super(message);
|
|
314
|
+
this.issues = issues;
|
|
315
|
+
this.name = "PolicyValidationError";
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
function parsePolicyContent(content, format) {
|
|
319
|
+
let rawConfig;
|
|
320
|
+
try {
|
|
321
|
+
if (format === "yaml") {
|
|
322
|
+
rawConfig = parse(content);
|
|
323
|
+
} else {
|
|
324
|
+
rawConfig = JSON.parse(content);
|
|
325
|
+
}
|
|
326
|
+
} catch (error) {
|
|
327
|
+
throw new PolicyValidationError(
|
|
328
|
+
`Failed to parse ${format.toUpperCase()}: ${error instanceof Error ? error.message : String(error)}`,
|
|
329
|
+
[]
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
const result = policySchema.safeParse(rawConfig);
|
|
333
|
+
if (!result.success) {
|
|
334
|
+
throw new PolicyValidationError(
|
|
335
|
+
"Policy validation failed:\n" + result.error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n"),
|
|
336
|
+
result.error.issues
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
return result.data;
|
|
340
|
+
}
|
|
341
|
+
var operationalSettingsSchema = z.object({
|
|
342
|
+
defaultCommand: z.string().optional(),
|
|
343
|
+
keyProvider: keyProviderSchema2.optional()
|
|
344
|
+
}).strict();
|
|
345
|
+
var suiteSchema2 = z.object({
|
|
346
|
+
// Gate fields (if present, this suite references a gate)
|
|
347
|
+
gate: z.string().optional(),
|
|
348
|
+
// Legacy fingerprint definition (for backward compatibility)
|
|
349
|
+
description: z.string().optional(),
|
|
350
|
+
packages: z.array(z.string().min(1, "Package path cannot be empty")).optional(),
|
|
351
|
+
files: z.array(z.string().min(1, "File path cannot be empty")).optional(),
|
|
352
|
+
ignore: z.array(z.string().min(1, "Ignore pattern cannot be empty")).optional(),
|
|
353
|
+
// CLI-specific fields
|
|
354
|
+
command: z.string().optional(),
|
|
355
|
+
timeout: z.string().optional(),
|
|
356
|
+
interactive: z.boolean().optional(),
|
|
357
|
+
// Relationship fields
|
|
358
|
+
invalidates: z.array(z.string().min(1, "Invalidated suite name cannot be empty")).optional(),
|
|
359
|
+
depends_on: z.array(z.string().min(1, "Dependency suite name cannot be empty")).optional()
|
|
360
|
+
}).strict().refine(
|
|
361
|
+
(suite) => {
|
|
362
|
+
return suite.gate !== void 0 || suite.packages !== void 0 && suite.packages.length > 0;
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
message: "Suite must either reference a gate or define packages for fingerprinting"
|
|
366
|
+
}
|
|
367
|
+
);
|
|
368
|
+
var operationalSchema = z.object({
|
|
369
|
+
version: z.literal(1),
|
|
370
|
+
settings: operationalSettingsSchema.default({}),
|
|
371
|
+
suites: z.record(z.string(), suiteSchema2).refine((suites) => Object.keys(suites).length >= 1, {
|
|
372
|
+
message: "At least one suite must be defined"
|
|
373
|
+
}),
|
|
374
|
+
groups: z.record(z.string(), z.array(z.string().min(1, "Suite name in group cannot be empty"))).optional()
|
|
375
|
+
}).strict();
|
|
376
|
+
var OperationalValidationError = class extends Error {
|
|
377
|
+
constructor(message, issues) {
|
|
378
|
+
super(message);
|
|
379
|
+
this.issues = issues;
|
|
380
|
+
this.name = "OperationalValidationError";
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
function parseOperationalContent(content, format) {
|
|
384
|
+
let rawConfig;
|
|
385
|
+
try {
|
|
386
|
+
if (format === "yaml") {
|
|
387
|
+
rawConfig = parse(content);
|
|
388
|
+
} else {
|
|
389
|
+
rawConfig = JSON.parse(content);
|
|
390
|
+
}
|
|
391
|
+
} catch (error) {
|
|
392
|
+
throw new OperationalValidationError(
|
|
393
|
+
`Failed to parse ${format.toUpperCase()}: ${error instanceof Error ? error.message : String(error)}`,
|
|
394
|
+
[]
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
const result = operationalSchema.safeParse(rawConfig);
|
|
398
|
+
if (!result.success) {
|
|
399
|
+
throw new OperationalValidationError(
|
|
400
|
+
"Operational configuration validation failed:\n" + result.error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n"),
|
|
401
|
+
result.error.issues
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
return result.data;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// src/config/merge.ts
|
|
408
|
+
function toSuiteConfig(suite) {
|
|
409
|
+
const result = {};
|
|
410
|
+
if (suite.gate !== void 0) result.gate = suite.gate;
|
|
411
|
+
if (suite.description !== void 0) result.description = suite.description;
|
|
412
|
+
if (suite.packages !== void 0) result.packages = suite.packages;
|
|
413
|
+
if (suite.files !== void 0) result.files = suite.files;
|
|
414
|
+
if (suite.ignore !== void 0) result.ignore = suite.ignore;
|
|
415
|
+
if (suite.command !== void 0) result.command = suite.command;
|
|
416
|
+
if (suite.timeout !== void 0) result.timeout = suite.timeout;
|
|
417
|
+
if (suite.interactive !== void 0) result.interactive = suite.interactive;
|
|
418
|
+
if (suite.invalidates !== void 0) result.invalidates = suite.invalidates;
|
|
419
|
+
if (suite.depends_on !== void 0) result.depends_on = suite.depends_on;
|
|
420
|
+
return result;
|
|
421
|
+
}
|
|
422
|
+
function toTeamMember(member) {
|
|
423
|
+
const result = {
|
|
424
|
+
name: member.name,
|
|
425
|
+
publicKey: member.publicKey
|
|
426
|
+
};
|
|
427
|
+
if (member.email !== void 0) result.email = member.email;
|
|
428
|
+
if (member.github !== void 0) result.github = member.github;
|
|
429
|
+
return result;
|
|
430
|
+
}
|
|
431
|
+
function toGateConfig(gate) {
|
|
432
|
+
const fingerprint = {
|
|
433
|
+
paths: gate.fingerprint.paths
|
|
434
|
+
};
|
|
435
|
+
if (gate.fingerprint.exclude !== void 0) {
|
|
436
|
+
fingerprint.exclude = gate.fingerprint.exclude;
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
name: gate.name,
|
|
440
|
+
description: gate.description,
|
|
441
|
+
authorizedSigners: gate.authorizedSigners,
|
|
442
|
+
fingerprint,
|
|
443
|
+
maxAge: gate.maxAge
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
function toKeyProvider(provider) {
|
|
447
|
+
const result = {
|
|
448
|
+
type: provider.type
|
|
449
|
+
};
|
|
450
|
+
if (provider.options !== void 0) {
|
|
451
|
+
const options = {};
|
|
452
|
+
let hasOptions = false;
|
|
453
|
+
if (provider.options.privateKeyPath !== void 0) {
|
|
454
|
+
options.privateKeyPath = provider.options.privateKeyPath;
|
|
455
|
+
hasOptions = true;
|
|
456
|
+
}
|
|
457
|
+
if (provider.options.account !== void 0) {
|
|
458
|
+
options.account = provider.options.account;
|
|
459
|
+
hasOptions = true;
|
|
460
|
+
}
|
|
461
|
+
if (provider.options.vault !== void 0) {
|
|
462
|
+
options.vault = provider.options.vault;
|
|
463
|
+
hasOptions = true;
|
|
464
|
+
}
|
|
465
|
+
if (provider.options.itemName !== void 0) {
|
|
466
|
+
options.itemName = provider.options.itemName;
|
|
467
|
+
hasOptions = true;
|
|
468
|
+
}
|
|
469
|
+
if (hasOptions) {
|
|
470
|
+
result.options = options;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return result;
|
|
474
|
+
}
|
|
475
|
+
function mergeConfigs(policy, operational) {
|
|
476
|
+
const settings = {
|
|
477
|
+
// Security settings from policy (these are trust-critical)
|
|
478
|
+
maxAgeDays: policy.settings.maxAgeDays,
|
|
479
|
+
publicKeyPath: policy.settings.publicKeyPath,
|
|
480
|
+
attestationsPath: policy.settings.attestationsPath
|
|
481
|
+
};
|
|
482
|
+
if (operational.settings.defaultCommand !== void 0) {
|
|
483
|
+
settings.defaultCommand = operational.settings.defaultCommand;
|
|
484
|
+
}
|
|
485
|
+
if (operational.settings.keyProvider !== void 0) {
|
|
486
|
+
settings.keyProvider = toKeyProvider(operational.settings.keyProvider);
|
|
487
|
+
}
|
|
488
|
+
const suites = {};
|
|
489
|
+
for (const [name, suite] of Object.entries(operational.suites)) {
|
|
490
|
+
suites[name] = toSuiteConfig(suite);
|
|
491
|
+
}
|
|
492
|
+
const config = {
|
|
493
|
+
version: 1,
|
|
494
|
+
settings,
|
|
495
|
+
suites
|
|
496
|
+
};
|
|
497
|
+
if (policy.team !== void 0) {
|
|
498
|
+
const team = {};
|
|
499
|
+
for (const [slug, member] of Object.entries(policy.team)) {
|
|
500
|
+
team[slug] = toTeamMember(member);
|
|
501
|
+
}
|
|
502
|
+
config.team = team;
|
|
503
|
+
}
|
|
504
|
+
if (policy.gates !== void 0) {
|
|
505
|
+
const gates = {};
|
|
506
|
+
for (const [slug, gate] of Object.entries(policy.gates)) {
|
|
507
|
+
gates[slug] = toGateConfig(gate);
|
|
508
|
+
}
|
|
509
|
+
config.gates = gates;
|
|
510
|
+
}
|
|
511
|
+
if (operational.groups !== void 0) {
|
|
512
|
+
config.groups = operational.groups;
|
|
513
|
+
}
|
|
514
|
+
return config;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// src/config/validation.ts
|
|
518
|
+
function validateSuiteGateReferences(policy, operational) {
|
|
519
|
+
const errors = [];
|
|
520
|
+
const gates = policy.gates ?? {};
|
|
521
|
+
const team = policy.team ?? {};
|
|
522
|
+
for (const [suiteName, suiteConfig] of Object.entries(operational.suites)) {
|
|
523
|
+
const gateName = suiteConfig.gate;
|
|
524
|
+
if (gateName === void 0) {
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
const gate = gates[gateName];
|
|
528
|
+
if (gate === void 0) {
|
|
529
|
+
errors.push({
|
|
530
|
+
type: "UNKNOWN_GATE",
|
|
531
|
+
suite: suiteName,
|
|
532
|
+
gate: gateName,
|
|
533
|
+
message: `Suite "${suiteName}" references unknown gate "${gateName}". The gate must be defined in policy.yaml.`
|
|
534
|
+
});
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
for (const signerSlug of gate.authorizedSigners) {
|
|
538
|
+
if (team[signerSlug] === void 0) {
|
|
539
|
+
errors.push({
|
|
540
|
+
type: "MISSING_TEAM_MEMBER",
|
|
541
|
+
suite: suiteName,
|
|
542
|
+
gate: gateName,
|
|
543
|
+
signer: signerSlug,
|
|
544
|
+
message: `Gate "${gateName}" (referenced by suite "${suiteName}") authorizes signer "${signerSlug}", but this team member is not defined in policy.yaml.`
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return errors;
|
|
550
|
+
}
|
|
258
551
|
var LARGE_FILE_THRESHOLD = 50 * 1024 * 1024;
|
|
259
552
|
function sortFiles(files) {
|
|
260
553
|
return [...files].sort((a, b) => {
|
|
@@ -274,13 +567,13 @@ function computeFinalFingerprint(fileHashes) {
|
|
|
274
567
|
});
|
|
275
568
|
const hashes = sorted.map((input) => input.hash);
|
|
276
569
|
const concatenated = Buffer.concat(hashes);
|
|
277
|
-
const finalHash =
|
|
570
|
+
const finalHash = crypto3.createHash("sha256").update(concatenated).digest();
|
|
278
571
|
return `sha256:${finalHash.toString("hex")}`;
|
|
279
572
|
}
|
|
280
573
|
async function hashFileAsync(realPath, normalizedPath, stats) {
|
|
281
574
|
if (stats.size > LARGE_FILE_THRESHOLD) {
|
|
282
|
-
return new Promise((
|
|
283
|
-
const hash2 =
|
|
575
|
+
return new Promise((resolve4, reject) => {
|
|
576
|
+
const hash2 = crypto3.createHash("sha256");
|
|
284
577
|
hash2.update(normalizedPath);
|
|
285
578
|
hash2.update(":");
|
|
286
579
|
const stream = fs.createReadStream(realPath);
|
|
@@ -288,13 +581,13 @@ async function hashFileAsync(realPath, normalizedPath, stats) {
|
|
|
288
581
|
hash2.update(chunk);
|
|
289
582
|
});
|
|
290
583
|
stream.on("end", () => {
|
|
291
|
-
|
|
584
|
+
resolve4(hash2.digest());
|
|
292
585
|
});
|
|
293
586
|
stream.on("error", reject);
|
|
294
587
|
});
|
|
295
588
|
}
|
|
296
589
|
const content = await fs.promises.readFile(realPath);
|
|
297
|
-
const hash =
|
|
590
|
+
const hash = crypto3.createHash("sha256");
|
|
298
591
|
hash.update(normalizedPath);
|
|
299
592
|
hash.update(":");
|
|
300
593
|
hash.update(content);
|
|
@@ -302,7 +595,7 @@ async function hashFileAsync(realPath, normalizedPath, stats) {
|
|
|
302
595
|
}
|
|
303
596
|
function hashFileSync(realPath, normalizedPath) {
|
|
304
597
|
const content = fs.readFileSync(realPath);
|
|
305
|
-
const hash =
|
|
598
|
+
const hash = crypto3.createHash("sha256");
|
|
306
599
|
hash.update(normalizedPath);
|
|
307
600
|
hash.update(":");
|
|
308
601
|
hash.update(content);
|
|
@@ -607,7 +900,7 @@ function isBuffer(value) {
|
|
|
607
900
|
}
|
|
608
901
|
function generateKeyPair2() {
|
|
609
902
|
try {
|
|
610
|
-
const keyPair =
|
|
903
|
+
const keyPair = crypto3.generateKeyPairSync("ed25519", {
|
|
611
904
|
publicKeyEncoding: {
|
|
612
905
|
type: "spki",
|
|
613
906
|
format: "pem"
|
|
@@ -621,7 +914,7 @@ function generateKeyPair2() {
|
|
|
621
914
|
if (typeof publicKey !== "string" || typeof privateKey !== "string") {
|
|
622
915
|
throw new Error("Expected keypair to have string keys");
|
|
623
916
|
}
|
|
624
|
-
const publicKeyObj =
|
|
917
|
+
const publicKeyObj = crypto3.createPublicKey(publicKey);
|
|
625
918
|
const publicKeyExport = publicKeyObj.export({
|
|
626
919
|
type: "spki",
|
|
627
920
|
format: "der"
|
|
@@ -644,8 +937,8 @@ function generateKeyPair2() {
|
|
|
644
937
|
function sign3(data, privateKeyPem) {
|
|
645
938
|
try {
|
|
646
939
|
const dataBuffer = typeof data === "string" ? Buffer.from(data, "utf8") : data;
|
|
647
|
-
const privateKeyObj =
|
|
648
|
-
const signatureResult =
|
|
940
|
+
const privateKeyObj = crypto3.createPrivateKey(privateKeyPem);
|
|
941
|
+
const signatureResult = crypto3.sign(null, dataBuffer, privateKeyObj);
|
|
649
942
|
if (!isBuffer(signatureResult)) {
|
|
650
943
|
throw new Error("Expected signature to be a Buffer");
|
|
651
944
|
}
|
|
@@ -685,12 +978,12 @@ function verify3(data, signature, publicKeyBase64) {
|
|
|
685
978
|
// BIT STRING, 33 bytes (32 key + 1 padding)
|
|
686
979
|
]);
|
|
687
980
|
const spkiBuffer = Buffer.concat([spkiHeader, rawPublicKey]);
|
|
688
|
-
const publicKeyObj =
|
|
981
|
+
const publicKeyObj = crypto3.createPublicKey({
|
|
689
982
|
key: spkiBuffer,
|
|
690
983
|
format: "der",
|
|
691
984
|
type: "spki"
|
|
692
985
|
});
|
|
693
|
-
return
|
|
986
|
+
return crypto3.verify(null, dataBuffer, publicKeyObj, signatureBuffer);
|
|
694
987
|
} catch (err) {
|
|
695
988
|
if (err instanceof Error && err.message.includes("verification failed")) {
|
|
696
989
|
return false;
|
|
@@ -702,8 +995,8 @@ function verify3(data, signature, publicKeyBase64) {
|
|
|
702
995
|
}
|
|
703
996
|
function getPublicKeyFromPrivate(privateKeyPem) {
|
|
704
997
|
try {
|
|
705
|
-
const privateKeyObj =
|
|
706
|
-
const publicKeyObj =
|
|
998
|
+
const privateKeyObj = crypto3.createPrivateKey(privateKeyPem);
|
|
999
|
+
const publicKeyObj = crypto3.createPublicKey(privateKeyObj);
|
|
707
1000
|
const publicKeyExport = publicKeyObj.export({
|
|
708
1001
|
type: "spki",
|
|
709
1002
|
format: "der"
|
|
@@ -869,7 +1162,7 @@ var FilesystemKeyProvider = class {
|
|
|
869
1162
|
*/
|
|
870
1163
|
async keyExists(keyRef) {
|
|
871
1164
|
try {
|
|
872
|
-
await
|
|
1165
|
+
await fs7.access(keyRef);
|
|
873
1166
|
return true;
|
|
874
1167
|
} catch {
|
|
875
1168
|
return false;
|
|
@@ -1027,7 +1320,7 @@ var OnePasswordKeyProvider = class _OnePasswordKeyProvider {
|
|
|
1027
1320
|
`Key not found in 1Password: "${keyRef}" (vault: ${this.vault})` + (this.account ? ` (account: ${this.account})` : "")
|
|
1028
1321
|
);
|
|
1029
1322
|
}
|
|
1030
|
-
const tempDir = await
|
|
1323
|
+
const tempDir = await fs7.mkdtemp(path6.join(os2.tmpdir(), "attest-it-"));
|
|
1031
1324
|
const tempKeyPath = path6.join(tempDir, "private.pem");
|
|
1032
1325
|
try {
|
|
1033
1326
|
const args = ["document", "get", keyRef, "--vault", this.vault, "--out-file", tempKeyPath];
|
|
@@ -1040,8 +1333,8 @@ var OnePasswordKeyProvider = class _OnePasswordKeyProvider {
|
|
|
1040
1333
|
keyPath: tempKeyPath,
|
|
1041
1334
|
cleanup: async () => {
|
|
1042
1335
|
try {
|
|
1043
|
-
await
|
|
1044
|
-
await
|
|
1336
|
+
await fs7.unlink(tempKeyPath);
|
|
1337
|
+
await fs7.rmdir(tempDir);
|
|
1045
1338
|
} catch (cleanupError) {
|
|
1046
1339
|
console.warn(
|
|
1047
1340
|
`Warning: Failed to clean up temporary key file at ${tempKeyPath}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
|
|
@@ -1051,7 +1344,7 @@ var OnePasswordKeyProvider = class _OnePasswordKeyProvider {
|
|
|
1051
1344
|
};
|
|
1052
1345
|
} catch (error) {
|
|
1053
1346
|
try {
|
|
1054
|
-
await
|
|
1347
|
+
await fs7.rm(tempDir, { recursive: true, force: true });
|
|
1055
1348
|
} catch (cleanupError) {
|
|
1056
1349
|
console.warn(
|
|
1057
1350
|
`Warning: Failed to clean up temporary key directory at ${tempDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
|
|
@@ -1067,7 +1360,7 @@ var OnePasswordKeyProvider = class _OnePasswordKeyProvider {
|
|
|
1067
1360
|
*/
|
|
1068
1361
|
async generateKeyPair(options) {
|
|
1069
1362
|
const { publicKeyPath, force = false } = options;
|
|
1070
|
-
const tempDir = await
|
|
1363
|
+
const tempDir = await fs7.mkdtemp(path6.join(os2.tmpdir(), "attest-it-keygen-"));
|
|
1071
1364
|
const tempPrivateKeyPath = path6.join(tempDir, "private.pem");
|
|
1072
1365
|
try {
|
|
1073
1366
|
await generateKeyPair({
|
|
@@ -1088,8 +1381,8 @@ var OnePasswordKeyProvider = class _OnePasswordKeyProvider {
|
|
|
1088
1381
|
args.push("--account", this.account);
|
|
1089
1382
|
}
|
|
1090
1383
|
await execCommand("op", args);
|
|
1091
|
-
await
|
|
1092
|
-
await
|
|
1384
|
+
await fs7.unlink(tempPrivateKeyPath);
|
|
1385
|
+
await fs7.rmdir(tempDir);
|
|
1093
1386
|
return {
|
|
1094
1387
|
privateKeyRef: this.itemName,
|
|
1095
1388
|
publicKeyPath,
|
|
@@ -1097,7 +1390,7 @@ var OnePasswordKeyProvider = class _OnePasswordKeyProvider {
|
|
|
1097
1390
|
};
|
|
1098
1391
|
} catch (error) {
|
|
1099
1392
|
try {
|
|
1100
|
-
await
|
|
1393
|
+
await fs7.rm(tempDir, { recursive: true, force: true });
|
|
1101
1394
|
} catch (cleanupError) {
|
|
1102
1395
|
console.warn(
|
|
1103
1396
|
`Warning: Failed to clean up temporary key directory at ${tempDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
|
|
@@ -1121,7 +1414,7 @@ var OnePasswordKeyProvider = class _OnePasswordKeyProvider {
|
|
|
1121
1414
|
}
|
|
1122
1415
|
};
|
|
1123
1416
|
async function execCommand(command, args) {
|
|
1124
|
-
return new Promise((
|
|
1417
|
+
return new Promise((resolve4, reject) => {
|
|
1125
1418
|
const proc = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1126
1419
|
let stdout = "";
|
|
1127
1420
|
let stderr = "";
|
|
@@ -1133,7 +1426,7 @@ async function execCommand(command, args) {
|
|
|
1133
1426
|
});
|
|
1134
1427
|
proc.on("close", (code) => {
|
|
1135
1428
|
if (code === 0) {
|
|
1136
|
-
|
|
1429
|
+
resolve4(stdout.trim());
|
|
1137
1430
|
} else {
|
|
1138
1431
|
reject(new Error(`Command failed with exit code ${String(code)}: ${stderr}`));
|
|
1139
1432
|
}
|
|
@@ -1226,7 +1519,7 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
|
|
|
1226
1519
|
`Key not found in macOS Keychain: "${keyRef}" (account: ${_MacOSKeychainKeyProvider.ACCOUNT})`
|
|
1227
1520
|
);
|
|
1228
1521
|
}
|
|
1229
|
-
const tempDir = await
|
|
1522
|
+
const tempDir = await fs7.mkdtemp(path6.join(os2.tmpdir(), "attest-it-"));
|
|
1230
1523
|
const tempKeyPath = path6.join(tempDir, "private.pem");
|
|
1231
1524
|
try {
|
|
1232
1525
|
const findArgs = [
|
|
@@ -1242,14 +1535,14 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
|
|
|
1242
1535
|
}
|
|
1243
1536
|
const base64Key = await execCommand2("security", findArgs);
|
|
1244
1537
|
const keyContent = Buffer.from(base64Key, "base64").toString("utf8");
|
|
1245
|
-
await
|
|
1538
|
+
await fs7.writeFile(tempKeyPath, keyContent, { mode: 384 });
|
|
1246
1539
|
await setKeyPermissions(tempKeyPath);
|
|
1247
1540
|
return {
|
|
1248
1541
|
keyPath: tempKeyPath,
|
|
1249
1542
|
cleanup: async () => {
|
|
1250
1543
|
try {
|
|
1251
|
-
await
|
|
1252
|
-
await
|
|
1544
|
+
await fs7.unlink(tempKeyPath);
|
|
1545
|
+
await fs7.rmdir(tempDir);
|
|
1253
1546
|
} catch (cleanupError) {
|
|
1254
1547
|
console.warn(
|
|
1255
1548
|
`Warning: Failed to clean up temporary key file at ${tempKeyPath}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
|
|
@@ -1259,7 +1552,7 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
|
|
|
1259
1552
|
};
|
|
1260
1553
|
} catch (error) {
|
|
1261
1554
|
try {
|
|
1262
|
-
await
|
|
1555
|
+
await fs7.rm(tempDir, { recursive: true, force: true });
|
|
1263
1556
|
} catch (cleanupError) {
|
|
1264
1557
|
console.warn(
|
|
1265
1558
|
`Warning: Failed to clean up temporary key directory at ${tempDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
|
|
@@ -1275,7 +1568,7 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
|
|
|
1275
1568
|
*/
|
|
1276
1569
|
async generateKeyPair(options) {
|
|
1277
1570
|
const { publicKeyPath, force = false } = options;
|
|
1278
|
-
const tempDir = await
|
|
1571
|
+
const tempDir = await fs7.mkdtemp(path6.join(os2.tmpdir(), "attest-it-keygen-"));
|
|
1279
1572
|
const tempPrivateKeyPath = path6.join(tempDir, "private.pem");
|
|
1280
1573
|
try {
|
|
1281
1574
|
await generateKeyPair({
|
|
@@ -1283,7 +1576,7 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
|
|
|
1283
1576
|
publicPath: publicKeyPath,
|
|
1284
1577
|
force
|
|
1285
1578
|
});
|
|
1286
|
-
const privateKeyContent = await
|
|
1579
|
+
const privateKeyContent = await fs7.readFile(tempPrivateKeyPath, "utf8");
|
|
1287
1580
|
const base64Key = Buffer.from(privateKeyContent, "utf8").toString("base64");
|
|
1288
1581
|
const addArgs = [
|
|
1289
1582
|
"add-generic-password",
|
|
@@ -1301,8 +1594,8 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
|
|
|
1301
1594
|
addArgs.push(this.keychain);
|
|
1302
1595
|
}
|
|
1303
1596
|
await execCommand2("security", addArgs);
|
|
1304
|
-
await
|
|
1305
|
-
await
|
|
1597
|
+
await fs7.unlink(tempPrivateKeyPath);
|
|
1598
|
+
await fs7.rmdir(tempDir);
|
|
1306
1599
|
return {
|
|
1307
1600
|
privateKeyRef: this.itemName,
|
|
1308
1601
|
publicKeyPath,
|
|
@@ -1310,7 +1603,7 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
|
|
|
1310
1603
|
};
|
|
1311
1604
|
} catch (error) {
|
|
1312
1605
|
try {
|
|
1313
|
-
await
|
|
1606
|
+
await fs7.rm(tempDir, { recursive: true, force: true });
|
|
1314
1607
|
} catch (cleanupError) {
|
|
1315
1608
|
console.warn(
|
|
1316
1609
|
`Warning: Failed to clean up temporary key directory at ${tempDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
|
|
@@ -1332,7 +1625,7 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
|
|
|
1332
1625
|
}
|
|
1333
1626
|
};
|
|
1334
1627
|
async function execCommand2(command, args) {
|
|
1335
|
-
return new Promise((
|
|
1628
|
+
return new Promise((resolve4, reject) => {
|
|
1336
1629
|
const proc = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1337
1630
|
let stdout = "";
|
|
1338
1631
|
let stderr = "";
|
|
@@ -1344,7 +1637,7 @@ async function execCommand2(command, args) {
|
|
|
1344
1637
|
});
|
|
1345
1638
|
proc.on("close", (code) => {
|
|
1346
1639
|
if (code === 0) {
|
|
1347
|
-
|
|
1640
|
+
resolve4(stdout.trim());
|
|
1348
1641
|
} else {
|
|
1349
1642
|
reject(new Error(`Command failed with exit code ${String(code)}: ${stderr}`));
|
|
1350
1643
|
}
|
|
@@ -1354,69 +1647,6 @@ async function execCommand2(command, args) {
|
|
|
1354
1647
|
});
|
|
1355
1648
|
});
|
|
1356
1649
|
}
|
|
1357
|
-
|
|
1358
|
-
// src/key-provider/registry.ts
|
|
1359
|
-
var KeyProviderRegistry = class {
|
|
1360
|
-
static providers = /* @__PURE__ */ new Map();
|
|
1361
|
-
/**
|
|
1362
|
-
* Register a key provider factory.
|
|
1363
|
-
* @param type - Provider type identifier
|
|
1364
|
-
* @param factory - Factory function to create provider instances
|
|
1365
|
-
*/
|
|
1366
|
-
static register(type, factory) {
|
|
1367
|
-
this.providers.set(type, factory);
|
|
1368
|
-
}
|
|
1369
|
-
/**
|
|
1370
|
-
* Create a key provider from configuration.
|
|
1371
|
-
* @param config - Provider configuration
|
|
1372
|
-
* @returns A key provider instance
|
|
1373
|
-
* @throws Error if the provider type is not registered
|
|
1374
|
-
*/
|
|
1375
|
-
static create(config) {
|
|
1376
|
-
const factory = this.providers.get(config.type);
|
|
1377
|
-
if (!factory) {
|
|
1378
|
-
throw new Error(
|
|
1379
|
-
`Unknown key provider type: ${config.type}. Available types: ${Array.from(this.providers.keys()).join(", ")}`
|
|
1380
|
-
);
|
|
1381
|
-
}
|
|
1382
|
-
return factory(config);
|
|
1383
|
-
}
|
|
1384
|
-
/**
|
|
1385
|
-
* Get all registered provider types.
|
|
1386
|
-
* @returns Array of provider type identifiers
|
|
1387
|
-
*/
|
|
1388
|
-
static getProviderTypes() {
|
|
1389
|
-
return Array.from(this.providers.keys());
|
|
1390
|
-
}
|
|
1391
|
-
};
|
|
1392
|
-
KeyProviderRegistry.register("filesystem", (config) => {
|
|
1393
|
-
const privateKeyPath = typeof config.options.privateKeyPath === "string" ? config.options.privateKeyPath : void 0;
|
|
1394
|
-
if (privateKeyPath !== void 0) {
|
|
1395
|
-
return new FilesystemKeyProvider({ privateKeyPath });
|
|
1396
|
-
}
|
|
1397
|
-
return new FilesystemKeyProvider();
|
|
1398
|
-
});
|
|
1399
|
-
KeyProviderRegistry.register("1password", (config) => {
|
|
1400
|
-
const { options } = config;
|
|
1401
|
-
const account = typeof options.account === "string" ? options.account : void 0;
|
|
1402
|
-
const vault = typeof options.vault === "string" ? options.vault : "";
|
|
1403
|
-
const itemName = typeof options.itemName === "string" ? options.itemName : "";
|
|
1404
|
-
if (!vault || !itemName) {
|
|
1405
|
-
throw new Error("1Password provider requires vault and itemName options");
|
|
1406
|
-
}
|
|
1407
|
-
if (account !== void 0) {
|
|
1408
|
-
return new OnePasswordKeyProvider({ account, vault, itemName });
|
|
1409
|
-
}
|
|
1410
|
-
return new OnePasswordKeyProvider({ vault, itemName });
|
|
1411
|
-
});
|
|
1412
|
-
KeyProviderRegistry.register("macos-keychain", (config) => {
|
|
1413
|
-
const { options } = config;
|
|
1414
|
-
const itemName = typeof options.itemName === "string" ? options.itemName : "";
|
|
1415
|
-
if (!itemName) {
|
|
1416
|
-
throw new Error("macOS Keychain provider requires itemName option");
|
|
1417
|
-
}
|
|
1418
|
-
return new MacOSKeychainKeyProvider({ itemName });
|
|
1419
|
-
});
|
|
1420
1650
|
var homeDirOverride = null;
|
|
1421
1651
|
function setAttestItHomeDir(dir) {
|
|
1422
1652
|
homeDirOverride = dir;
|
|
@@ -1582,6 +1812,617 @@ function saveLocalConfigSync(config, configPath) {
|
|
|
1582
1812
|
function getActiveIdentity(config) {
|
|
1583
1813
|
return config.identities[config.activeIdentity];
|
|
1584
1814
|
}
|
|
1815
|
+
|
|
1816
|
+
// src/key-provider/yubikey-provider.ts
|
|
1817
|
+
var EncryptedKeyFileSchema = z.object({
|
|
1818
|
+
version: z.literal(1),
|
|
1819
|
+
iv: z.string().min(1),
|
|
1820
|
+
authTag: z.string().min(1),
|
|
1821
|
+
salt: z.string().min(1),
|
|
1822
|
+
challenge: z.string().min(1),
|
|
1823
|
+
ciphertext: z.string().min(1),
|
|
1824
|
+
slot: z.union([z.literal(1), z.literal(2)]),
|
|
1825
|
+
serial: z.string().optional(),
|
|
1826
|
+
aad: z.string().optional()
|
|
1827
|
+
});
|
|
1828
|
+
var activeCleanupHandlers = /* @__PURE__ */ new Set();
|
|
1829
|
+
var processHandlersInstalled = false;
|
|
1830
|
+
function installProcessHandlers() {
|
|
1831
|
+
if (processHandlersInstalled) return;
|
|
1832
|
+
processHandlersInstalled = true;
|
|
1833
|
+
const runCleanup = async () => {
|
|
1834
|
+
const handlers = Array.from(activeCleanupHandlers);
|
|
1835
|
+
await Promise.allSettled(handlers.map((h) => h()));
|
|
1836
|
+
};
|
|
1837
|
+
process.once("beforeExit", () => {
|
|
1838
|
+
void runCleanup();
|
|
1839
|
+
});
|
|
1840
|
+
process.once("SIGINT", () => {
|
|
1841
|
+
void runCleanup().finally(() => process.exit(130));
|
|
1842
|
+
});
|
|
1843
|
+
process.once("SIGTERM", () => {
|
|
1844
|
+
void runCleanup().finally(() => process.exit(143));
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1847
|
+
function validateEncryptedKeyFile(data) {
|
|
1848
|
+
const parsed = EncryptedKeyFileSchema.parse(data);
|
|
1849
|
+
const iv = Buffer.from(parsed.iv, "base64");
|
|
1850
|
+
if (iv.length !== 12) {
|
|
1851
|
+
throw new Error(`Invalid IV size: expected 12 bytes, got ${String(iv.length)}`);
|
|
1852
|
+
}
|
|
1853
|
+
const authTag = Buffer.from(parsed.authTag, "base64");
|
|
1854
|
+
if (authTag.length !== 16) {
|
|
1855
|
+
throw new Error(`Invalid auth tag size: expected 16 bytes, got ${String(authTag.length)}`);
|
|
1856
|
+
}
|
|
1857
|
+
const salt = Buffer.from(parsed.salt, "base64");
|
|
1858
|
+
if (salt.length !== 32) {
|
|
1859
|
+
throw new Error(`Invalid salt size: expected 32 bytes, got ${String(salt.length)}`);
|
|
1860
|
+
}
|
|
1861
|
+
const challenge = Buffer.from(parsed.challenge, "base64");
|
|
1862
|
+
if (challenge.length !== 32) {
|
|
1863
|
+
throw new Error(`Invalid challenge size: expected 32 bytes, got ${String(challenge.length)}`);
|
|
1864
|
+
}
|
|
1865
|
+
return parsed;
|
|
1866
|
+
}
|
|
1867
|
+
function constructAAD(version2, slot, serial) {
|
|
1868
|
+
const aadObject = {
|
|
1869
|
+
version: version2,
|
|
1870
|
+
slot,
|
|
1871
|
+
serial: serial ?? "unspecified"
|
|
1872
|
+
};
|
|
1873
|
+
return Buffer.from(JSON.stringify(aadObject), "utf8");
|
|
1874
|
+
}
|
|
1875
|
+
var YubiKeyProvider = class _YubiKeyProvider {
|
|
1876
|
+
type = "yubikey";
|
|
1877
|
+
displayName = "YubiKey";
|
|
1878
|
+
encryptedKeyPath;
|
|
1879
|
+
slot;
|
|
1880
|
+
serial;
|
|
1881
|
+
/**
|
|
1882
|
+
* Create a new YubiKeyProvider.
|
|
1883
|
+
* @param options - Provider options
|
|
1884
|
+
* @throws Error if encryptedKeyPath is outside the attest-it config directory
|
|
1885
|
+
*/
|
|
1886
|
+
constructor(options) {
|
|
1887
|
+
const resolvedPath = path6.resolve(options.encryptedKeyPath);
|
|
1888
|
+
const configDir = getAttestItConfigDir();
|
|
1889
|
+
if (!resolvedPath.startsWith(configDir)) {
|
|
1890
|
+
throw new Error(
|
|
1891
|
+
`Encrypted key path must be within attest-it config directory (${configDir}). Got: ${resolvedPath}`
|
|
1892
|
+
);
|
|
1893
|
+
}
|
|
1894
|
+
this.encryptedKeyPath = resolvedPath;
|
|
1895
|
+
this.slot = options.slot ?? 2;
|
|
1896
|
+
if (options.serial !== void 0) {
|
|
1897
|
+
this.serial = options.serial;
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
/**
|
|
1901
|
+
* Check if ykman CLI is installed and available.
|
|
1902
|
+
* @returns true if ykman is available
|
|
1903
|
+
*/
|
|
1904
|
+
static async isInstalled() {
|
|
1905
|
+
try {
|
|
1906
|
+
await execCommand3("ykman", ["--version"]);
|
|
1907
|
+
return true;
|
|
1908
|
+
} catch {
|
|
1909
|
+
return false;
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
/**
|
|
1913
|
+
* Check if any YubiKey is connected.
|
|
1914
|
+
* @returns true if at least one YubiKey is connected
|
|
1915
|
+
*/
|
|
1916
|
+
static async isConnected() {
|
|
1917
|
+
try {
|
|
1918
|
+
const output = await execCommand3("ykman", ["list", "--serials"]);
|
|
1919
|
+
return output.trim().length > 0;
|
|
1920
|
+
} catch {
|
|
1921
|
+
return false;
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
/**
|
|
1925
|
+
* Check if HMAC challenge-response is configured on a slot.
|
|
1926
|
+
* @param slot - Slot number (1 or 2)
|
|
1927
|
+
* @param serial - Optional YubiKey serial number
|
|
1928
|
+
* @returns true if challenge-response is configured
|
|
1929
|
+
*/
|
|
1930
|
+
static async isChallengeResponseConfigured(slot = 2, serial) {
|
|
1931
|
+
try {
|
|
1932
|
+
const args = ["otp", "info"];
|
|
1933
|
+
if (serial) {
|
|
1934
|
+
args.unshift("--device", serial);
|
|
1935
|
+
}
|
|
1936
|
+
const output = await execCommand3("ykman", args);
|
|
1937
|
+
const slotPattern = new RegExp(`Slot ${String(slot)}:\\s+programmed.*challenge-response`, "i");
|
|
1938
|
+
return slotPattern.test(output);
|
|
1939
|
+
} catch {
|
|
1940
|
+
return false;
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
/**
|
|
1944
|
+
* List connected YubiKeys.
|
|
1945
|
+
* @returns Array of YubiKey information
|
|
1946
|
+
*/
|
|
1947
|
+
static async listDevices() {
|
|
1948
|
+
if (!await _YubiKeyProvider.isInstalled()) {
|
|
1949
|
+
return [];
|
|
1950
|
+
}
|
|
1951
|
+
try {
|
|
1952
|
+
const output = await execCommand3("ykman", ["list", "--serials"]);
|
|
1953
|
+
const serials = output.trim().split("\n").filter((s) => s.length > 0);
|
|
1954
|
+
const devices = [];
|
|
1955
|
+
for (const serial of serials) {
|
|
1956
|
+
try {
|
|
1957
|
+
const infoOutput = await execCommand3("ykman", ["--device", serial, "info"]);
|
|
1958
|
+
const typeMatch = /Device type:\s+(.+)/i.exec(infoOutput);
|
|
1959
|
+
const fwMatch = /Firmware version:\s+(.+)/i.exec(infoOutput);
|
|
1960
|
+
devices.push({
|
|
1961
|
+
serial,
|
|
1962
|
+
type: typeMatch?.[1]?.trim() ?? "YubiKey",
|
|
1963
|
+
firmware: fwMatch?.[1]?.trim() ?? "Unknown"
|
|
1964
|
+
});
|
|
1965
|
+
} catch {
|
|
1966
|
+
devices.push({
|
|
1967
|
+
serial,
|
|
1968
|
+
type: "YubiKey",
|
|
1969
|
+
firmware: "Unknown"
|
|
1970
|
+
});
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
return devices;
|
|
1974
|
+
} catch {
|
|
1975
|
+
return [];
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
/**
|
|
1979
|
+
* Check if this provider is available on the current system.
|
|
1980
|
+
* Requires ykman to be installed.
|
|
1981
|
+
*/
|
|
1982
|
+
async isAvailable() {
|
|
1983
|
+
return _YubiKeyProvider.isInstalled();
|
|
1984
|
+
}
|
|
1985
|
+
/**
|
|
1986
|
+
* Check if an encrypted key file exists.
|
|
1987
|
+
* @param keyRef - Path to encrypted key file
|
|
1988
|
+
*/
|
|
1989
|
+
async keyExists(keyRef) {
|
|
1990
|
+
try {
|
|
1991
|
+
await fs7.access(keyRef);
|
|
1992
|
+
return true;
|
|
1993
|
+
} catch {
|
|
1994
|
+
return false;
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
/**
|
|
1998
|
+
* Get the private key by decrypting with YubiKey.
|
|
1999
|
+
* Downloads to a temporary file and returns a cleanup function.
|
|
2000
|
+
*
|
|
2001
|
+
* **Important**: Always call the cleanup function when done to securely delete
|
|
2002
|
+
* the temporary key file. The cleanup is also registered for process exit handlers.
|
|
2003
|
+
*
|
|
2004
|
+
* @param keyRef - Path to encrypted key file
|
|
2005
|
+
* @throws Error if the key cannot be decrypted
|
|
2006
|
+
*/
|
|
2007
|
+
async getPrivateKey(keyRef) {
|
|
2008
|
+
installProcessHandlers();
|
|
2009
|
+
if (!await this.keyExists(keyRef)) {
|
|
2010
|
+
throw new Error(`Encrypted key file not found: ${keyRef}`);
|
|
2011
|
+
}
|
|
2012
|
+
const encryptedData = await fs7.readFile(keyRef, "utf8");
|
|
2013
|
+
let keyFile;
|
|
2014
|
+
try {
|
|
2015
|
+
const parsed = JSON.parse(encryptedData);
|
|
2016
|
+
keyFile = validateEncryptedKeyFile(parsed);
|
|
2017
|
+
} catch (err) {
|
|
2018
|
+
if (err instanceof z.ZodError) {
|
|
2019
|
+
throw new Error(
|
|
2020
|
+
`Invalid encrypted key file format: ${err.errors.map((e) => e.message).join(", ")}`
|
|
2021
|
+
);
|
|
2022
|
+
}
|
|
2023
|
+
throw new Error(`Invalid encrypted key file: malformed JSON or structure`);
|
|
2024
|
+
}
|
|
2025
|
+
const expectedSerial = this.serial ?? keyFile.serial;
|
|
2026
|
+
if (!expectedSerial) {
|
|
2027
|
+
console.warn(
|
|
2028
|
+
"WARNING: No YubiKey serial number specified for key verification. Any YubiKey with the correct HMAC secret could decrypt this key. For better security, re-encrypt the key with a serial number specified."
|
|
2029
|
+
);
|
|
2030
|
+
}
|
|
2031
|
+
if (expectedSerial) {
|
|
2032
|
+
const devices = await _YubiKeyProvider.listDevices();
|
|
2033
|
+
const matchingDevice = devices.find((d) => d.serial === expectedSerial);
|
|
2034
|
+
if (!matchingDevice) {
|
|
2035
|
+
throw new Error(
|
|
2036
|
+
`Required YubiKey not found. Expected serial: ${expectedSerial}. Connected devices: ${devices.map((d) => d.serial).join(", ") || "none"}`
|
|
2037
|
+
);
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
const challenge = Buffer.from(keyFile.challenge, "base64");
|
|
2041
|
+
const response = await performChallengeResponse(challenge, keyFile.slot, expectedSerial);
|
|
2042
|
+
const salt = Buffer.from(keyFile.salt, "base64");
|
|
2043
|
+
const aesKey = deriveKey(response, salt);
|
|
2044
|
+
const iv = Buffer.from(keyFile.iv, "base64");
|
|
2045
|
+
const authTag = Buffer.from(keyFile.authTag, "base64");
|
|
2046
|
+
const ciphertext = Buffer.from(keyFile.ciphertext, "base64");
|
|
2047
|
+
let privateKeyContent;
|
|
2048
|
+
try {
|
|
2049
|
+
const decipher = crypto3.createDecipheriv("aes-256-gcm", aesKey, iv);
|
|
2050
|
+
if (keyFile.aad) {
|
|
2051
|
+
decipher.setAAD(Buffer.from(keyFile.aad, "base64"));
|
|
2052
|
+
}
|
|
2053
|
+
decipher.setAuthTag(authTag);
|
|
2054
|
+
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
2055
|
+
privateKeyContent = decrypted.toString("utf8");
|
|
2056
|
+
} catch {
|
|
2057
|
+
throw new Error(
|
|
2058
|
+
"Failed to decrypt private key. Verify you are using the correct YubiKey and the encrypted key file has not been corrupted or tampered with."
|
|
2059
|
+
);
|
|
2060
|
+
}
|
|
2061
|
+
const tempDir = await fs7.mkdtemp(path6.join(os2.tmpdir(), "attest-it-"));
|
|
2062
|
+
const tempKeyPath = path6.join(tempDir, "private.pem");
|
|
2063
|
+
const cleanup = async () => {
|
|
2064
|
+
activeCleanupHandlers.delete(cleanup);
|
|
2065
|
+
try {
|
|
2066
|
+
const keySize = Buffer.byteLength(privateKeyContent);
|
|
2067
|
+
await fs7.writeFile(tempKeyPath, crypto3.randomBytes(keySize));
|
|
2068
|
+
await fs7.unlink(tempKeyPath);
|
|
2069
|
+
await fs7.rmdir(tempDir);
|
|
2070
|
+
} catch {
|
|
2071
|
+
}
|
|
2072
|
+
};
|
|
2073
|
+
try {
|
|
2074
|
+
await fs7.writeFile(tempKeyPath, privateKeyContent, { mode: 384 });
|
|
2075
|
+
await setKeyPermissions(tempKeyPath);
|
|
2076
|
+
activeCleanupHandlers.add(cleanup);
|
|
2077
|
+
return {
|
|
2078
|
+
keyPath: tempKeyPath,
|
|
2079
|
+
cleanup
|
|
2080
|
+
};
|
|
2081
|
+
} catch (error) {
|
|
2082
|
+
await cleanup();
|
|
2083
|
+
throw error;
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
/**
|
|
2087
|
+
* Generate a new keypair and store encrypted with YubiKey.
|
|
2088
|
+
* Public key is written to filesystem for repository commit.
|
|
2089
|
+
*
|
|
2090
|
+
* **Security Note**: Always specify a serial number to bind the key to a specific YubiKey.
|
|
2091
|
+
*
|
|
2092
|
+
* @param options - Key generation options
|
|
2093
|
+
*/
|
|
2094
|
+
async generateKeyPair(options) {
|
|
2095
|
+
const { publicKeyPath, force = false } = options;
|
|
2096
|
+
if (!await _YubiKeyProvider.isChallengeResponseConfigured(this.slot, this.serial)) {
|
|
2097
|
+
throw new Error(
|
|
2098
|
+
`YubiKey slot ${String(this.slot)} is not configured for HMAC challenge-response. Ensure your YubiKey is connected and use "ykman otp chalresp --generate 2" to configure it.`
|
|
2099
|
+
);
|
|
2100
|
+
}
|
|
2101
|
+
if (!force && await this.keyExists(this.encryptedKeyPath)) {
|
|
2102
|
+
throw new Error(
|
|
2103
|
+
`Encrypted key file already exists: ${this.encryptedKeyPath}. Use force: true to overwrite.`
|
|
2104
|
+
);
|
|
2105
|
+
}
|
|
2106
|
+
let serial;
|
|
2107
|
+
if (this.serial) {
|
|
2108
|
+
serial = this.serial;
|
|
2109
|
+
} else {
|
|
2110
|
+
const devices = await _YubiKeyProvider.listDevices();
|
|
2111
|
+
if (devices.length === 1 && devices[0]) {
|
|
2112
|
+
serial = devices[0].serial;
|
|
2113
|
+
} else if (devices.length > 1) {
|
|
2114
|
+
console.warn(
|
|
2115
|
+
"WARNING: Multiple YubiKeys detected but no serial specified. Key will not be bound to a specific device. For better security, specify a serial number."
|
|
2116
|
+
);
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
const tempDir = await fs7.mkdtemp(path6.join(os2.tmpdir(), "attest-it-keygen-"));
|
|
2120
|
+
const tempPrivateKeyPath = path6.join(tempDir, "private.pem");
|
|
2121
|
+
try {
|
|
2122
|
+
await generateKeyPair({
|
|
2123
|
+
privatePath: tempPrivateKeyPath,
|
|
2124
|
+
publicPath: publicKeyPath,
|
|
2125
|
+
force
|
|
2126
|
+
});
|
|
2127
|
+
const privateKeyContent = await fs7.readFile(tempPrivateKeyPath, "utf8");
|
|
2128
|
+
const challenge = crypto3.randomBytes(32);
|
|
2129
|
+
const salt = crypto3.randomBytes(32);
|
|
2130
|
+
const iv = crypto3.randomBytes(12);
|
|
2131
|
+
const response = await performChallengeResponse(challenge, this.slot, this.serial);
|
|
2132
|
+
const aesKey = deriveKey(response, salt);
|
|
2133
|
+
const aad = constructAAD(1, this.slot, serial);
|
|
2134
|
+
const cipher = crypto3.createCipheriv("aes-256-gcm", aesKey, iv);
|
|
2135
|
+
cipher.setAAD(aad);
|
|
2136
|
+
const ciphertext = Buffer.concat([
|
|
2137
|
+
cipher.update(Buffer.from(privateKeyContent, "utf8")),
|
|
2138
|
+
cipher.final()
|
|
2139
|
+
]);
|
|
2140
|
+
const authTag = cipher.getAuthTag();
|
|
2141
|
+
const keyFile = {
|
|
2142
|
+
version: 1,
|
|
2143
|
+
iv: iv.toString("base64"),
|
|
2144
|
+
authTag: authTag.toString("base64"),
|
|
2145
|
+
salt: salt.toString("base64"),
|
|
2146
|
+
challenge: challenge.toString("base64"),
|
|
2147
|
+
ciphertext: ciphertext.toString("base64"),
|
|
2148
|
+
slot: this.slot,
|
|
2149
|
+
aad: aad.toString("base64"),
|
|
2150
|
+
...serial && { serial }
|
|
2151
|
+
};
|
|
2152
|
+
await fs7.mkdir(path6.dirname(this.encryptedKeyPath), { recursive: true });
|
|
2153
|
+
await fs7.writeFile(this.encryptedKeyPath, JSON.stringify(keyFile, null, 2), { mode: 384 });
|
|
2154
|
+
await setKeyPermissions(this.encryptedKeyPath);
|
|
2155
|
+
const keySize = Buffer.byteLength(privateKeyContent);
|
|
2156
|
+
await fs7.writeFile(tempPrivateKeyPath, crypto3.randomBytes(keySize));
|
|
2157
|
+
await fs7.unlink(tempPrivateKeyPath);
|
|
2158
|
+
await fs7.rmdir(tempDir);
|
|
2159
|
+
return {
|
|
2160
|
+
privateKeyRef: this.encryptedKeyPath,
|
|
2161
|
+
publicKeyPath,
|
|
2162
|
+
storageDescription: `YubiKey-encrypted: ${this.encryptedKeyPath}`
|
|
2163
|
+
};
|
|
2164
|
+
} catch (error) {
|
|
2165
|
+
try {
|
|
2166
|
+
await fs7.rm(tempDir, { recursive: true, force: true });
|
|
2167
|
+
} catch {
|
|
2168
|
+
}
|
|
2169
|
+
throw error;
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
/**
|
|
2173
|
+
* Encrypt an existing private key with YubiKey challenge-response.
|
|
2174
|
+
*
|
|
2175
|
+
* @remarks
|
|
2176
|
+
* This static method allows encrypting a private key that was generated
|
|
2177
|
+
* elsewhere (e.g., by the CLI) without having to create a provider instance first.
|
|
2178
|
+
*
|
|
2179
|
+
* **Security Note**: Always specify a serial number to bind the key to a specific YubiKey.
|
|
2180
|
+
* The serial provides defense-in-depth by ensuring only the intended YubiKey can decrypt.
|
|
2181
|
+
*
|
|
2182
|
+
* @param options - Encryption options
|
|
2183
|
+
* @returns Path to the encrypted key file and storage description
|
|
2184
|
+
* @public
|
|
2185
|
+
*/
|
|
2186
|
+
static async encryptPrivateKey(options) {
|
|
2187
|
+
const { privateKey, encryptedKeyPath, slot = 2, serial } = options;
|
|
2188
|
+
const resolvedPath = path6.resolve(encryptedKeyPath);
|
|
2189
|
+
const configDir = getAttestItConfigDir();
|
|
2190
|
+
if (!resolvedPath.startsWith(configDir)) {
|
|
2191
|
+
throw new Error(
|
|
2192
|
+
`Encrypted key path must be within attest-it config directory (${configDir}). Got: ${resolvedPath}`
|
|
2193
|
+
);
|
|
2194
|
+
}
|
|
2195
|
+
if (!serial) {
|
|
2196
|
+
console.warn(
|
|
2197
|
+
"WARNING: No YubiKey serial number specified. Key will not be bound to a specific device. For better security, specify a serial number."
|
|
2198
|
+
);
|
|
2199
|
+
}
|
|
2200
|
+
if (!await _YubiKeyProvider.isChallengeResponseConfigured(slot, serial)) {
|
|
2201
|
+
throw new Error(
|
|
2202
|
+
`YubiKey slot ${String(slot)} is not configured for HMAC challenge-response. Ensure your YubiKey is connected and use "ykman otp chalresp --generate 2" to configure it.`
|
|
2203
|
+
);
|
|
2204
|
+
}
|
|
2205
|
+
const challenge = crypto3.randomBytes(32);
|
|
2206
|
+
const salt = crypto3.randomBytes(32);
|
|
2207
|
+
const iv = crypto3.randomBytes(12);
|
|
2208
|
+
const response = await performChallengeResponse(challenge, slot, serial);
|
|
2209
|
+
const aesKey = deriveKey(response, salt);
|
|
2210
|
+
const aad = constructAAD(1, slot, serial);
|
|
2211
|
+
const cipher = crypto3.createCipheriv("aes-256-gcm", aesKey, iv);
|
|
2212
|
+
cipher.setAAD(aad);
|
|
2213
|
+
const ciphertext = Buffer.concat([
|
|
2214
|
+
cipher.update(Buffer.from(privateKey, "utf8")),
|
|
2215
|
+
cipher.final()
|
|
2216
|
+
]);
|
|
2217
|
+
const authTag = cipher.getAuthTag();
|
|
2218
|
+
const keyFile = {
|
|
2219
|
+
version: 1,
|
|
2220
|
+
iv: iv.toString("base64"),
|
|
2221
|
+
authTag: authTag.toString("base64"),
|
|
2222
|
+
salt: salt.toString("base64"),
|
|
2223
|
+
challenge: challenge.toString("base64"),
|
|
2224
|
+
ciphertext: ciphertext.toString("base64"),
|
|
2225
|
+
slot,
|
|
2226
|
+
aad: aad.toString("base64"),
|
|
2227
|
+
...serial && { serial }
|
|
2228
|
+
};
|
|
2229
|
+
await fs7.mkdir(path6.dirname(resolvedPath), { recursive: true });
|
|
2230
|
+
await fs7.writeFile(resolvedPath, JSON.stringify(keyFile, null, 2), { mode: 384 });
|
|
2231
|
+
await setKeyPermissions(resolvedPath);
|
|
2232
|
+
return {
|
|
2233
|
+
encryptedKeyPath: resolvedPath,
|
|
2234
|
+
storageDescription: `YubiKey-encrypted: ${resolvedPath}`
|
|
2235
|
+
};
|
|
2236
|
+
}
|
|
2237
|
+
/**
|
|
2238
|
+
* Get the configuration for this provider.
|
|
2239
|
+
*/
|
|
2240
|
+
getConfig() {
|
|
2241
|
+
return {
|
|
2242
|
+
type: this.type,
|
|
2243
|
+
options: {
|
|
2244
|
+
encryptedKeyPath: this.encryptedKeyPath,
|
|
2245
|
+
slot: this.slot,
|
|
2246
|
+
...this.serial && { serial: this.serial }
|
|
2247
|
+
}
|
|
2248
|
+
};
|
|
2249
|
+
}
|
|
2250
|
+
};
|
|
2251
|
+
async function execCommand3(command, args) {
|
|
2252
|
+
return new Promise((resolve4, reject) => {
|
|
2253
|
+
const proc = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
2254
|
+
let stdout = "";
|
|
2255
|
+
let stderr = "";
|
|
2256
|
+
proc.stdout.on("data", (data) => {
|
|
2257
|
+
stdout += data.toString();
|
|
2258
|
+
});
|
|
2259
|
+
proc.stderr.on("data", (data) => {
|
|
2260
|
+
stderr += data.toString();
|
|
2261
|
+
});
|
|
2262
|
+
proc.on("close", (code) => {
|
|
2263
|
+
if (code === 0) {
|
|
2264
|
+
resolve4(stdout.trim());
|
|
2265
|
+
} else {
|
|
2266
|
+
reject(new Error(`Command failed with exit code ${String(code)}: ${stderr}`));
|
|
2267
|
+
}
|
|
2268
|
+
});
|
|
2269
|
+
proc.on("error", (error) => {
|
|
2270
|
+
reject(error);
|
|
2271
|
+
});
|
|
2272
|
+
});
|
|
2273
|
+
}
|
|
2274
|
+
async function performChallengeResponse(challenge, slot, serial) {
|
|
2275
|
+
const args = ["otp", "chalresp", "--slot", String(slot)];
|
|
2276
|
+
if (serial) {
|
|
2277
|
+
args.unshift("--device", serial);
|
|
2278
|
+
}
|
|
2279
|
+
args.push(challenge.toString("hex"));
|
|
2280
|
+
try {
|
|
2281
|
+
const output = await execCommand3("ykman", args);
|
|
2282
|
+
return Buffer.from(output.trim(), "hex");
|
|
2283
|
+
} catch {
|
|
2284
|
+
throw new Error(
|
|
2285
|
+
"YubiKey challenge-response failed. Verify your YubiKey is inserted and the slot is configured for challenge-response."
|
|
2286
|
+
);
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
function deriveKey(response, salt) {
|
|
2290
|
+
const derived = crypto3.hkdfSync("sha256", response, salt, "attest-it-yubikey-v1", 32);
|
|
2291
|
+
return Buffer.from(derived);
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
// src/key-provider/registry.ts
|
|
2295
|
+
var KeyProviderRegistry = class {
|
|
2296
|
+
static providers = /* @__PURE__ */ new Map();
|
|
2297
|
+
/**
|
|
2298
|
+
* Register a key provider factory.
|
|
2299
|
+
* @param type - Provider type identifier
|
|
2300
|
+
* @param factory - Factory function to create provider instances
|
|
2301
|
+
*/
|
|
2302
|
+
static register(type, factory) {
|
|
2303
|
+
this.providers.set(type, factory);
|
|
2304
|
+
}
|
|
2305
|
+
/**
|
|
2306
|
+
* Create a key provider from configuration.
|
|
2307
|
+
* @param config - Provider configuration
|
|
2308
|
+
* @returns A key provider instance
|
|
2309
|
+
* @throws Error if the provider type is not registered
|
|
2310
|
+
*/
|
|
2311
|
+
static create(config) {
|
|
2312
|
+
const factory = this.providers.get(config.type);
|
|
2313
|
+
if (!factory) {
|
|
2314
|
+
throw new Error(
|
|
2315
|
+
`Unknown key provider type: ${config.type}. Available types: ${Array.from(this.providers.keys()).join(", ")}`
|
|
2316
|
+
);
|
|
2317
|
+
}
|
|
2318
|
+
return factory(config);
|
|
2319
|
+
}
|
|
2320
|
+
/**
|
|
2321
|
+
* Get all registered provider types.
|
|
2322
|
+
* @returns Array of provider type identifiers
|
|
2323
|
+
*/
|
|
2324
|
+
static getProviderTypes() {
|
|
2325
|
+
return Array.from(this.providers.keys());
|
|
2326
|
+
}
|
|
2327
|
+
};
|
|
2328
|
+
KeyProviderRegistry.register("filesystem", (config) => {
|
|
2329
|
+
const privateKeyPath = typeof config.options.privateKeyPath === "string" ? config.options.privateKeyPath : void 0;
|
|
2330
|
+
if (privateKeyPath !== void 0) {
|
|
2331
|
+
return new FilesystemKeyProvider({ privateKeyPath });
|
|
2332
|
+
}
|
|
2333
|
+
return new FilesystemKeyProvider();
|
|
2334
|
+
});
|
|
2335
|
+
KeyProviderRegistry.register("1password", (config) => {
|
|
2336
|
+
const { options } = config;
|
|
2337
|
+
const account = typeof options.account === "string" ? options.account : void 0;
|
|
2338
|
+
const vault = typeof options.vault === "string" ? options.vault : "";
|
|
2339
|
+
const itemName = typeof options.itemName === "string" ? options.itemName : "";
|
|
2340
|
+
if (!vault || !itemName) {
|
|
2341
|
+
throw new Error("1Password provider requires vault and itemName options");
|
|
2342
|
+
}
|
|
2343
|
+
if (account !== void 0) {
|
|
2344
|
+
return new OnePasswordKeyProvider({ account, vault, itemName });
|
|
2345
|
+
}
|
|
2346
|
+
return new OnePasswordKeyProvider({ vault, itemName });
|
|
2347
|
+
});
|
|
2348
|
+
KeyProviderRegistry.register("macos-keychain", (config) => {
|
|
2349
|
+
const { options } = config;
|
|
2350
|
+
const itemName = typeof options.itemName === "string" ? options.itemName : "";
|
|
2351
|
+
if (!itemName) {
|
|
2352
|
+
throw new Error("macOS Keychain provider requires itemName option");
|
|
2353
|
+
}
|
|
2354
|
+
return new MacOSKeychainKeyProvider({ itemName });
|
|
2355
|
+
});
|
|
2356
|
+
KeyProviderRegistry.register("yubikey", (config) => {
|
|
2357
|
+
const { options } = config;
|
|
2358
|
+
const encryptedKeyPath = typeof options.encryptedKeyPath === "string" ? options.encryptedKeyPath : "";
|
|
2359
|
+
if (!encryptedKeyPath) {
|
|
2360
|
+
throw new Error("YubiKey provider requires encryptedKeyPath option");
|
|
2361
|
+
}
|
|
2362
|
+
const slot = typeof options.slot === "number" && (options.slot === 1 || options.slot === 2) ? options.slot : void 0;
|
|
2363
|
+
const serial = typeof options.serial === "string" ? options.serial : void 0;
|
|
2364
|
+
const providerOptions = {
|
|
2365
|
+
encryptedKeyPath
|
|
2366
|
+
};
|
|
2367
|
+
if (slot !== void 0) {
|
|
2368
|
+
providerOptions.slot = slot;
|
|
2369
|
+
}
|
|
2370
|
+
if (serial !== void 0) {
|
|
2371
|
+
providerOptions.serial = serial;
|
|
2372
|
+
}
|
|
2373
|
+
return new YubiKeyProvider(providerOptions);
|
|
2374
|
+
});
|
|
2375
|
+
var cliExperienceSchema = z.object({
|
|
2376
|
+
declinedCompletionInstall: z.boolean().optional()
|
|
2377
|
+
}).strict();
|
|
2378
|
+
var userPreferencesSchema = z.object({
|
|
2379
|
+
cliExperience: cliExperienceSchema.optional()
|
|
2380
|
+
}).strict();
|
|
2381
|
+
function getPreferencesPath() {
|
|
2382
|
+
return join(getAttestItConfigDir(), "preferences.yaml");
|
|
2383
|
+
}
|
|
2384
|
+
async function loadPreferences() {
|
|
2385
|
+
const prefsPath = getPreferencesPath();
|
|
2386
|
+
try {
|
|
2387
|
+
const content = await readFile(prefsPath, "utf8");
|
|
2388
|
+
const parsed = parse(content);
|
|
2389
|
+
const result = userPreferencesSchema.safeParse(parsed);
|
|
2390
|
+
if (result.success) {
|
|
2391
|
+
const prefs = {};
|
|
2392
|
+
if (result.data.cliExperience) {
|
|
2393
|
+
prefs.cliExperience = {
|
|
2394
|
+
...result.data.cliExperience.declinedCompletionInstall !== void 0 && {
|
|
2395
|
+
declinedCompletionInstall: result.data.cliExperience.declinedCompletionInstall
|
|
2396
|
+
}
|
|
2397
|
+
};
|
|
2398
|
+
}
|
|
2399
|
+
return prefs;
|
|
2400
|
+
}
|
|
2401
|
+
console.warn("Invalid preferences file, using defaults:", result.error.message);
|
|
2402
|
+
return {};
|
|
2403
|
+
} catch (error) {
|
|
2404
|
+
if (error && typeof error === "object" && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
|
|
2405
|
+
return {};
|
|
2406
|
+
}
|
|
2407
|
+
throw error;
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
async function savePreferences(preferences) {
|
|
2411
|
+
const prefsPath = getPreferencesPath();
|
|
2412
|
+
const content = stringify(preferences);
|
|
2413
|
+
const dir = dirname(prefsPath);
|
|
2414
|
+
await mkdir(dir, { recursive: true });
|
|
2415
|
+
await writeFile(prefsPath, content, "utf8");
|
|
2416
|
+
}
|
|
2417
|
+
async function setPreference(key, value) {
|
|
2418
|
+
const prefs = await loadPreferences();
|
|
2419
|
+
prefs[key] = value;
|
|
2420
|
+
await savePreferences(prefs);
|
|
2421
|
+
}
|
|
2422
|
+
async function getPreference(key) {
|
|
2423
|
+
const prefs = await loadPreferences();
|
|
2424
|
+
return prefs[key];
|
|
2425
|
+
}
|
|
1585
2426
|
function isAuthorizedSigner(config, gateId, publicKey) {
|
|
1586
2427
|
const gate = config.gates?.[gateId];
|
|
1587
2428
|
if (!gate) {
|
|
@@ -1898,6 +2739,6 @@ function verifyAllSeals(config, seals, fingerprints) {
|
|
|
1898
2739
|
// src/index.ts
|
|
1899
2740
|
var version = "0.0.0";
|
|
1900
2741
|
|
|
1901
|
-
export { ConfigNotFoundError, ConfigValidationError, FilesystemKeyProvider, KeyProviderRegistry, LocalConfigValidationError, MacOSKeychainKeyProvider, OnePasswordKeyProvider, SignatureInvalidError, canonicalizeAttestations, computeFingerprint, computeFingerprintSync, createAttestation, createSeal, findAttestation, findConfigPath, findTeamMemberByPublicKey, generateKeyPair2 as generateEd25519KeyPair, getActiveIdentity, getAttestItConfigDir, getAttestItHomeDir, getAuthorizedSignersForGate, getGate, getLocalConfigPath, getPublicKeyFromPrivate, isAuthorizedSigner, listPackageFiles, loadConfig, loadConfigSync, loadLocalConfig, loadLocalConfigSync, parseDuration, readAndVerifyAttestations, readAttestations, readAttestationsSync, readSeals, readSealsSync, removeAttestation, resolveConfigPaths, saveLocalConfig, saveLocalConfigSync, setAttestItHomeDir, sign3 as signEd25519, toAttestItConfig, upsertAttestation, verifyAllSeals, verifyAttestations, verify3 as verifyEd25519, verifyGateSeal, verifySeal, version, writeAttestations, writeAttestationsSync, writeSeals, writeSealsSync, writeSignedAttestations };
|
|
2742
|
+
export { ConfigNotFoundError, ConfigValidationError, FilesystemKeyProvider, KeyProviderRegistry, LocalConfigValidationError, MacOSKeychainKeyProvider, OnePasswordKeyProvider, OperationalValidationError, PolicyValidationError, SignatureInvalidError, YubiKeyProvider, canonicalizeAttestations, computeFingerprint, computeFingerprintSync, createAttestation, createSeal, findAttestation, findConfigPath, findTeamMemberByPublicKey, generateKeyPair2 as generateEd25519KeyPair, getActiveIdentity, getAttestItConfigDir, getAttestItHomeDir, getAuthorizedSignersForGate, getGate, getLocalConfigPath, getPreference, getPreferencesPath, getPublicKeyFromPrivate, isAuthorizedSigner, listPackageFiles, loadConfig, loadConfigSync, loadLocalConfig, loadLocalConfigSync, loadPreferences, mergeConfigs, operationalSchema, parseDuration, parseOperationalContent, parsePolicyContent, policySchema, readAndVerifyAttestations, readAttestations, readAttestationsSync, readSeals, readSealsSync, removeAttestation, resolveConfigPaths, saveLocalConfig, saveLocalConfigSync, savePreferences, setAttestItHomeDir, setPreference, sign3 as signEd25519, toAttestItConfig, upsertAttestation, validateSuiteGateReferences, verifyAllSeals, verifyAttestations, verify3 as verifyEd25519, verifyGateSeal, verifySeal, version, writeAttestations, writeAttestationsSync, writeSeals, writeSealsSync, writeSignedAttestations };
|
|
1902
2743
|
//# sourceMappingURL=index.js.map
|
|
1903
2744
|
//# sourceMappingURL=index.js.map
|