@fgv/ts-extras 5.1.0-17 → 5.1.0-19

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.
Files changed (43) hide show
  1. package/dist/packlets/ai-assist/apiClient.js +247 -24
  2. package/dist/packlets/ai-assist/index.js +1 -1
  3. package/dist/packlets/ai-assist/registry.js +49 -4
  4. package/dist/packlets/crypto-utils/index.browser.js +2 -0
  5. package/dist/packlets/crypto-utils/index.js +2 -0
  6. package/dist/packlets/crypto-utils/keyPairAlgorithmParams.js +47 -0
  7. package/dist/packlets/crypto-utils/keystore/converters.js +101 -9
  8. package/dist/packlets/crypto-utils/keystore/index.js +1 -0
  9. package/dist/packlets/crypto-utils/keystore/keyStore.js +271 -46
  10. package/dist/packlets/crypto-utils/keystore/model.js +22 -1
  11. package/dist/packlets/crypto-utils/keystore/privateKeyStorage.js +21 -0
  12. package/dist/packlets/crypto-utils/model.js +5 -0
  13. package/dist/packlets/crypto-utils/nodeCryptoProvider.js +140 -1
  14. package/dist/test/unit/crypto/keystore/inMemoryPrivateKeyStorage.js +78 -0
  15. package/dist/ts-extras.d.ts +799 -40
  16. package/lib/packlets/ai-assist/apiClient.d.ts +11 -3
  17. package/lib/packlets/ai-assist/apiClient.js +245 -22
  18. package/lib/packlets/ai-assist/index.d.ts +2 -2
  19. package/lib/packlets/ai-assist/index.js +3 -1
  20. package/lib/packlets/ai-assist/model.d.ts +66 -5
  21. package/lib/packlets/ai-assist/registry.d.ts +25 -1
  22. package/lib/packlets/ai-assist/registry.js +51 -4
  23. package/lib/packlets/crypto-utils/index.browser.d.ts +1 -0
  24. package/lib/packlets/crypto-utils/index.browser.js +4 -1
  25. package/lib/packlets/crypto-utils/index.d.ts +1 -0
  26. package/lib/packlets/crypto-utils/index.js +4 -1
  27. package/lib/packlets/crypto-utils/keyPairAlgorithmParams.d.ts +39 -0
  28. package/lib/packlets/crypto-utils/keyPairAlgorithmParams.js +50 -0
  29. package/lib/packlets/crypto-utils/keystore/converters.d.ts +68 -6
  30. package/lib/packlets/crypto-utils/keystore/converters.js +100 -8
  31. package/lib/packlets/crypto-utils/keystore/index.d.ts +1 -0
  32. package/lib/packlets/crypto-utils/keystore/index.js +1 -0
  33. package/lib/packlets/crypto-utils/keystore/keyStore.d.ts +77 -9
  34. package/lib/packlets/crypto-utils/keystore/keyStore.js +271 -46
  35. package/lib/packlets/crypto-utils/keystore/model.d.ts +238 -19
  36. package/lib/packlets/crypto-utils/keystore/model.js +24 -2
  37. package/lib/packlets/crypto-utils/keystore/privateKeyStorage.d.ts +50 -0
  38. package/lib/packlets/crypto-utils/keystore/privateKeyStorage.js +22 -0
  39. package/lib/packlets/crypto-utils/model.d.ts +130 -0
  40. package/lib/packlets/crypto-utils/model.js +6 -1
  41. package/lib/packlets/crypto-utils/nodeCryptoProvider.d.ts +45 -1
  42. package/lib/packlets/crypto-utils/nodeCryptoProvider.js +139 -0
  43. package/package.json +7 -7
@@ -99,8 +99,9 @@ function getCurrentTimestamp() {
99
99
  * @public
100
100
  */
101
101
  class KeyStore {
102
- constructor(cryptoProvider, iterations, keystoreFile, isNew = true) {
102
+ constructor(cryptoProvider, iterations, keystoreFile, isNew, privateKeyStorage) {
103
103
  this._cryptoProvider = cryptoProvider;
104
+ this._privateKeyStorage = privateKeyStorage;
104
105
  this._iterations = iterations;
105
106
  this._keystoreFile = keystoreFile;
106
107
  this._state = 'locked';
@@ -123,7 +124,7 @@ class KeyStore {
123
124
  if (iterations < 1) {
124
125
  return (0, ts_utils_1.fail)('Iterations must be at least 1');
125
126
  }
126
- return (0, ts_utils_1.succeed)(new KeyStore(params.cryptoProvider, iterations, undefined, true));
127
+ return (0, ts_utils_1.succeed)(new KeyStore(params.cryptoProvider, iterations, undefined, true, params.privateKeyStorage));
127
128
  }
128
129
  /**
129
130
  * Opens an existing encrypted key store.
@@ -139,7 +140,7 @@ class KeyStore {
139
140
  return (0, ts_utils_1.fail)(`Invalid key store file: ${fileResult.message}`);
140
141
  }
141
142
  const iterations = fileResult.value.keyDerivation.iterations;
142
- return (0, ts_utils_1.succeed)(new KeyStore(params.cryptoProvider, iterations, fileResult.value, false));
143
+ return (0, ts_utils_1.succeed)(new KeyStore(params.cryptoProvider, iterations, fileResult.value, false, params.privateKeyStorage));
143
144
  }
144
145
  // ============================================================================
145
146
  // Lifecycle Methods
@@ -253,7 +254,9 @@ class KeyStore {
253
254
  // Clear secrets from memory (overwrite for security)
254
255
  if (this._secrets) {
255
256
  for (const entry of this._secrets.values()) {
256
- entry.key.fill(0);
257
+ if (entry.type !== 'asymmetric-keypair') {
258
+ entry.key.fill(0);
259
+ }
257
260
  }
258
261
  this._secrets.clear();
259
262
  this._secrets = undefined;
@@ -315,7 +318,9 @@ class KeyStore {
315
318
  return (0, ts_utils_1.succeed)(Array.from(this._secrets.keys()));
316
319
  }
317
320
  /**
318
- * Gets a secret by name.
321
+ * Gets a secret by name. Returns the {@link CryptoUtils.KeyStore.IKeyStoreEntry | discriminated union}
322
+ * — callers must check `entry.type` before accessing `key`/`id` since asymmetric
323
+ * entries carry no raw key material.
319
324
  * @param name - Name of the secret
320
325
  * @returns Success with secret entry, Failure if not found or locked
321
326
  * @public
@@ -330,6 +335,27 @@ class KeyStore {
330
335
  }
331
336
  return (0, ts_utils_1.succeed)(entry);
332
337
  }
338
+ /**
339
+ * Returns the public-key JWK for an asymmetric-keypair entry.
340
+ * Available without {@link CryptoUtils.KeyStore.IPrivateKeyStorage} since the
341
+ * public key lives in the vault metadata directly.
342
+ * @param name - Name of the entry
343
+ * @returns Success with the JWK, Failure if not found, locked, or wrong type
344
+ * @public
345
+ */
346
+ getPublicKeyJwk(name) {
347
+ if (!this._secrets) {
348
+ return (0, ts_utils_1.fail)('Key store is locked');
349
+ }
350
+ const entry = this._secrets.get(name);
351
+ if (!entry) {
352
+ return (0, ts_utils_1.fail)(`Secret '${name}' not found`);
353
+ }
354
+ if (entry.type !== 'asymmetric-keypair') {
355
+ return (0, ts_utils_1.fail)(`Secret '${name}' is not an asymmetric keypair (type: ${entry.type})`);
356
+ }
357
+ return (0, ts_utils_1.succeed)(entry.publicKeyJwk);
358
+ }
333
359
  /**
334
360
  * Checks if a secret exists.
335
361
  * @param name - Name of the secret
@@ -356,7 +382,6 @@ class KeyStore {
356
382
  if (!name || name.length === 0) {
357
383
  return (0, ts_utils_1.fail)('Secret name cannot be empty');
358
384
  }
359
- const replaced = this._secrets.has(name);
360
385
  // Generate a new random key
361
386
  const keyResult = await this._cryptoProvider.generateKey();
362
387
  /* c8 ignore next 3 - crypto provider errors tested but coverage intermittently missed */
@@ -370,9 +395,11 @@ class KeyStore {
370
395
  description: options === null || options === void 0 ? void 0 : options.description,
371
396
  createdAt: getCurrentTimestamp()
372
397
  };
398
+ const existing = this._secrets.get(name);
399
+ const warning = existing ? await this._releaseEntryResources(existing) : undefined;
373
400
  this._secrets.set(name, entry);
374
401
  this._dirty = true;
375
- return (0, ts_utils_1.succeed)({ entry, replaced });
402
+ return (0, ts_utils_1.succeed)({ entry, replaced: existing !== undefined, warning });
376
403
  }
377
404
  /**
378
405
  * Imports raw 32-byte key material into the vault.
@@ -388,7 +415,7 @@ class KeyStore {
388
415
  * @returns Success with entry, Failure if locked, key invalid, or exists and !replace
389
416
  * @public
390
417
  */
391
- importSecret(name, key, options) {
418
+ async importSecret(name, key, options) {
392
419
  var _a;
393
420
  if (!this._secrets) {
394
421
  return (0, ts_utils_1.fail)('Key store is locked');
@@ -399,8 +426,8 @@ class KeyStore {
399
426
  if (key.length !== Constants.AES_256_KEY_SIZE) {
400
427
  return (0, ts_utils_1.fail)(`Key must be ${Constants.AES_256_KEY_SIZE} bytes, got ${key.length}`);
401
428
  }
402
- const exists = this._secrets.has(name);
403
- if (exists && !(options === null || options === void 0 ? void 0 : options.replace)) {
429
+ const existing = this._secrets.get(name);
430
+ if (existing && !(options === null || options === void 0 ? void 0 : options.replace)) {
404
431
  return (0, ts_utils_1.fail)(`Secret '${name}' already exists - use replace=true to overwrite`);
405
432
  }
406
433
  const entry = {
@@ -410,9 +437,10 @@ class KeyStore {
410
437
  description: options === null || options === void 0 ? void 0 : options.description,
411
438
  createdAt: getCurrentTimestamp()
412
439
  };
440
+ const warning = existing ? await this._releaseEntryResources(existing) : undefined;
413
441
  this._secrets.set(name, entry);
414
442
  this._dirty = true;
415
- return (0, ts_utils_1.succeed)({ entry, replaced: exists });
443
+ return (0, ts_utils_1.succeed)({ entry, replaced: existing !== undefined, warning });
416
444
  }
417
445
  /**
418
446
  * Adds a secret derived from a password using PBKDF2.
@@ -439,8 +467,8 @@ class KeyStore {
439
467
  if (!password || password.length === 0) {
440
468
  return (0, ts_utils_1.fail)('Password cannot be empty');
441
469
  }
442
- const exists = this._secrets.has(name);
443
- if (exists && !(options === null || options === void 0 ? void 0 : options.replace)) {
470
+ const existing = this._secrets.get(name);
471
+ if (existing && !(options === null || options === void 0 ? void 0 : options.replace)) {
444
472
  return (0, ts_utils_1.fail)(`Secret '${name}' already exists - use replace=true to overwrite`);
445
473
  }
446
474
  const iterations = (_a = options === null || options === void 0 ? void 0 : options.iterations) !== null && _a !== void 0 ? _a : model_1.DEFAULT_SECRET_ITERATIONS;
@@ -463,11 +491,13 @@ class KeyStore {
463
491
  description: options === null || options === void 0 ? void 0 : options.description,
464
492
  createdAt: getCurrentTimestamp()
465
493
  };
494
+ const warning = existing ? await this._releaseEntryResources(existing) : undefined;
466
495
  this._secrets.set(name, entry);
467
496
  this._dirty = true;
468
497
  return (0, ts_utils_1.succeed)({
469
498
  entry,
470
- replaced: exists,
499
+ replaced: existing !== undefined,
500
+ warning,
471
501
  keyDerivation: {
472
502
  kdf: 'pbkdf2',
473
503
  salt: this._cryptoProvider.toBase64(saltResult.value),
@@ -476,12 +506,16 @@ class KeyStore {
476
506
  });
477
507
  }
478
508
  /**
479
- * Removes a secret by name.
509
+ * Removes a secret by name. Vault-first: the in-memory vault entry is dropped
510
+ * before any storage cleanup runs. For asymmetric-keypair entries, best-effort
511
+ * calls {@link CryptoUtils.KeyStore.IPrivateKeyStorage}.delete on the entry's
512
+ * `id`; a failure is reported via `warning` on the result but does not roll
513
+ * back the vault removal.
480
514
  * @param name - Name of the secret to remove
481
- * @returns Success with removed entry, Failure if not found or locked
515
+ * @returns Success with removed entry (and optional warning), Failure if not found or locked
482
516
  * @public
483
517
  */
484
- removeSecret(name) {
518
+ async removeSecret(name) {
485
519
  if (!this._secrets) {
486
520
  return (0, ts_utils_1.fail)('Key store is locked');
487
521
  }
@@ -489,11 +523,12 @@ class KeyStore {
489
523
  if (!entry) {
490
524
  return (0, ts_utils_1.fail)(`Secret '${name}' not found`);
491
525
  }
492
- // Clear the key before removing (security)
493
- entry.key.fill(0);
526
+ // Vault-first: drop the in-memory entry before touching storage so a
527
+ // storage failure cannot block removal.
494
528
  this._secrets.delete(name);
495
529
  this._dirty = true;
496
- return (0, ts_utils_1.succeed)(entry);
530
+ const warning = await this._releaseEntryResources(entry);
531
+ return (0, ts_utils_1.succeed)({ entry, warning });
497
532
  }
498
533
  /**
499
534
  * Imports an API key string into the vault.
@@ -504,7 +539,7 @@ class KeyStore {
504
539
  * @returns Success with entry, Failure if locked, empty, or exists and !replace
505
540
  * @public
506
541
  */
507
- importApiKey(name, apiKey, options) {
542
+ async importApiKey(name, apiKey, options) {
508
543
  if (!this._secrets) {
509
544
  return (0, ts_utils_1.fail)('Key store is locked');
510
545
  }
@@ -514,8 +549,8 @@ class KeyStore {
514
549
  if (!apiKey || apiKey.length === 0) {
515
550
  return (0, ts_utils_1.fail)('API key cannot be empty');
516
551
  }
517
- const exists = this._secrets.has(name);
518
- if (exists && !(options === null || options === void 0 ? void 0 : options.replace)) {
552
+ const existing = this._secrets.get(name);
553
+ if (existing && !(options === null || options === void 0 ? void 0 : options.replace)) {
519
554
  return (0, ts_utils_1.fail)(`Secret '${name}' already exists - use replace=true to overwrite`);
520
555
  }
521
556
  const encoder = new TextEncoder();
@@ -526,9 +561,10 @@ class KeyStore {
526
561
  description: options === null || options === void 0 ? void 0 : options.description,
527
562
  createdAt: getCurrentTimestamp()
528
563
  };
564
+ const warning = existing ? await this._releaseEntryResources(existing) : undefined;
529
565
  this._secrets.set(name, entry);
530
566
  this._dirty = true;
531
- return (0, ts_utils_1.succeed)({ entry, replaced: exists });
567
+ return (0, ts_utils_1.succeed)({ entry, replaced: existing !== undefined, warning });
532
568
  }
533
569
  /**
534
570
  * Retrieves an API key string by name.
@@ -551,6 +587,118 @@ class KeyStore {
551
587
  const decoder = new TextDecoder();
552
588
  return (0, ts_utils_1.succeed)(decoder.decode(entry.key));
553
589
  }
590
+ // ============================================================================
591
+ // Asymmetric Keypair Management
592
+ // ============================================================================
593
+ /**
594
+ * Adds a new asymmetric keypair to the vault. Storage-first: the private key
595
+ * is stored under a freshly-minted `id` before the public-key vault entry is
596
+ * committed. If the storage call fails, no vault entry is written and the
597
+ * operation returns Failure.
598
+ *
599
+ * When `replace: true` displaces an existing entry (asymmetric or symmetric),
600
+ * a fresh `id` is minted; the displaced entry's resources are released
601
+ * best-effort. Failure of the storage delete is reported via `warning` on the
602
+ * result but does not roll back the replacement.
603
+ *
604
+ * Requires a {@link CryptoUtils.KeyStore.IPrivateKeyStorage} backend
605
+ * supplied at construction.
606
+ *
607
+ * @param name - Unique name for the entry
608
+ * @param options - Algorithm, optional description, replace flag
609
+ * @returns Success with the new entry, Failure if locked, no provider, or storage write failed
610
+ * @public
611
+ */
612
+ async addKeyPair(name, options) {
613
+ if (!this._secrets) {
614
+ return (0, ts_utils_1.fail)('Key store is locked');
615
+ }
616
+ if (!name || name.length === 0) {
617
+ return (0, ts_utils_1.fail)('Entry name cannot be empty');
618
+ }
619
+ if (!this._privateKeyStorage) {
620
+ return (0, ts_utils_1.fail)('No private key storage configured');
621
+ }
622
+ const existing = this._secrets.get(name);
623
+ if (existing && !options.replace) {
624
+ return (0, ts_utils_1.fail)(`Secret '${name}' already exists - use replace=true to overwrite`);
625
+ }
626
+ // Generate the keypair before touching storage. extractable=true on backends
627
+ // that round-trip via JWK; extractable=false on backends that hold CryptoKey
628
+ // refs directly.
629
+ const extractable = !this._privateKeyStorage.supportsNonExtractable;
630
+ const keyPairResult = await this._cryptoProvider.generateKeyPair(options.algorithm, extractable);
631
+ /* c8 ignore next 3 - crypto provider errors covered in nodeCryptoProvider tests; cannot be triggered here without mocking */
632
+ if (keyPairResult.isFailure()) {
633
+ return (0, ts_utils_1.fail)(`Failed to generate keypair for '${name}': ${keyPairResult.message}`);
634
+ }
635
+ const { publicKey, privateKey } = keyPairResult.value;
636
+ const jwkResult = await this._cryptoProvider.exportPublicKeyJwk(publicKey);
637
+ /* c8 ignore next 3 - export of an extractable freshly-generated public key is hard to fail */
638
+ if (jwkResult.isFailure()) {
639
+ return (0, ts_utils_1.fail)(`Failed to export public key for '${name}': ${jwkResult.message}`);
640
+ }
641
+ const idResult = this._generateId();
642
+ /* c8 ignore next 3 - random-bytes failure is hard to trigger with a healthy provider */
643
+ if (idResult.isFailure()) {
644
+ return (0, ts_utils_1.fail)(`Failed to mint storage id for '${name}': ${idResult.message}`);
645
+ }
646
+ const id = idResult.value;
647
+ // Storage-first: write the private key before committing the vault entry.
648
+ const storeResult = await this._privateKeyStorage.store(id, privateKey);
649
+ if (storeResult.isFailure()) {
650
+ return (0, ts_utils_1.fail)(`Failed to persist private key for '${name}': ${storeResult.message}`);
651
+ }
652
+ const entry = {
653
+ name,
654
+ type: 'asymmetric-keypair',
655
+ id,
656
+ algorithm: options.algorithm,
657
+ publicKeyJwk: jwkResult.value,
658
+ description: options.description,
659
+ createdAt: getCurrentTimestamp()
660
+ };
661
+ const warning = existing ? await this._releaseEntryResources(existing) : undefined;
662
+ this._secrets.set(name, entry);
663
+ this._dirty = true;
664
+ return (0, ts_utils_1.succeed)({ entry, replaced: existing !== undefined, warning });
665
+ }
666
+ /**
667
+ * Retrieves the keypair for an asymmetric-keypair entry. The private key is
668
+ * loaded from {@link CryptoUtils.KeyStore.IPrivateKeyStorage} on every call —
669
+ * the keystore never caches private `CryptoKey` references between calls.
670
+ * The public key is re-imported from the vault's JWK so callers always
671
+ * receive a `CryptoKey` rather than the JWK form.
672
+ * @param name - Name of the entry
673
+ * @returns Success with `{ publicKey, privateKey }`, Failure if not found,
674
+ * locked, wrong type, no provider, or storage load failed.
675
+ * @public
676
+ */
677
+ async getKeyPair(name) {
678
+ if (!this._secrets) {
679
+ return (0, ts_utils_1.fail)('Key store is locked');
680
+ }
681
+ const entry = this._secrets.get(name);
682
+ if (!entry) {
683
+ return (0, ts_utils_1.fail)(`Secret '${name}' not found`);
684
+ }
685
+ if (entry.type !== 'asymmetric-keypair') {
686
+ return (0, ts_utils_1.fail)(`Secret '${name}' is not an asymmetric keypair (type: ${entry.type})`);
687
+ }
688
+ if (!this._privateKeyStorage) {
689
+ return (0, ts_utils_1.fail)('No private key storage configured');
690
+ }
691
+ const privateResult = await this._privateKeyStorage.load(entry.id);
692
+ if (privateResult.isFailure()) {
693
+ return (0, ts_utils_1.fail)(`Failed to load private key for '${name}': ${privateResult.message}`);
694
+ }
695
+ const publicResult = await this._cryptoProvider.importPublicKeyJwk(entry.publicKeyJwk, entry.algorithm);
696
+ /* c8 ignore next 3 - vault JWKs that previously exported cleanly are extremely unlikely to fail re-import */
697
+ if (publicResult.isFailure()) {
698
+ return (0, ts_utils_1.fail)(`Failed to re-import public key for '${name}': ${publicResult.message}`);
699
+ }
700
+ return (0, ts_utils_1.succeed)({ publicKey: publicResult.value, privateKey: privateResult.value });
701
+ }
554
702
  /**
555
703
  * Lists secret names filtered by type.
556
704
  * @param type - The secret type to filter by
@@ -590,7 +738,8 @@ class KeyStore {
590
738
  if (oldName !== newName && this._secrets.has(newName)) {
591
739
  return (0, ts_utils_1.fail)(`Secret '${newName}' already exists`);
592
740
  }
593
- // Create new entry with new name (preserve type)
741
+ // Create new entry with new name. For asymmetric entries the spread
742
+ // preserves `id` so the storage handle survives the rename.
594
743
  const newEntry = Object.assign(Object.assign({}, entry), { name: newName });
595
744
  this._secrets.delete(oldName);
596
745
  this._secrets.set(newName, newEntry);
@@ -710,6 +859,9 @@ class KeyStore {
710
859
  if (secretResult.isFailure()) {
711
860
  return (0, ts_utils_1.fail)(`encryptByName: ${secretResult.message}`);
712
861
  }
862
+ if (secretResult.value.type === 'asymmetric-keypair') {
863
+ return (0, ts_utils_1.fail)(`encryptByName: secret '${secretName}' is an asymmetric keypair, not symmetric key material`);
864
+ }
713
865
  return (0, encryptedFile_1.createEncryptedFile)({
714
866
  content,
715
867
  secretName,
@@ -737,6 +889,9 @@ class KeyStore {
737
889
  if (!entry) {
738
890
  return (0, ts_utils_1.fail)(`Secret '${secretName}' not found in key store`);
739
891
  }
892
+ if (entry.type === 'asymmetric-keypair') {
893
+ return (0, ts_utils_1.fail)(`Secret '${secretName}' is an asymmetric keypair, not symmetric key material`);
894
+ }
740
895
  return (0, ts_utils_1.succeed)(entry.key);
741
896
  };
742
897
  return (0, ts_utils_1.succeed)(provider);
@@ -770,13 +925,26 @@ class KeyStore {
770
925
  // Build vault contents
771
926
  const secretEntries = {};
772
927
  for (const [name, entry] of secrets) {
773
- secretEntries[name] = {
774
- name: entry.name,
775
- type: entry.type,
776
- key: this._cryptoProvider.toBase64(entry.key),
777
- description: entry.description,
778
- createdAt: entry.createdAt
779
- };
928
+ if (entry.type === 'asymmetric-keypair') {
929
+ secretEntries[name] = {
930
+ name: entry.name,
931
+ type: entry.type,
932
+ id: entry.id,
933
+ algorithm: entry.algorithm,
934
+ publicKeyJwk: entry.publicKeyJwk,
935
+ description: entry.description,
936
+ createdAt: entry.createdAt
937
+ };
938
+ }
939
+ else {
940
+ secretEntries[name] = {
941
+ name: entry.name,
942
+ type: entry.type,
943
+ key: this._cryptoProvider.toBase64(entry.key),
944
+ description: entry.description,
945
+ createdAt: entry.createdAt
946
+ };
947
+ }
780
948
  }
781
949
  const vaultContents = {
782
950
  version: model_1.KEYSTORE_FORMAT,
@@ -816,7 +984,6 @@ class KeyStore {
816
984
  * Shared by `unlock()` and `unlockWithKey()`.
817
985
  */
818
986
  async _decryptVault(derivedKey) {
819
- var _a;
820
987
  const keystoreFile = this._keystoreFile;
821
988
  if (keystoreFile === undefined) {
822
989
  return (0, ts_utils_1.fail)('No key store file loaded');
@@ -856,20 +1023,33 @@ class KeyStore {
856
1023
  }
857
1024
  const secrets = new Map();
858
1025
  for (const [name, jsonEntry] of Object.entries(vaultResult.value.secrets)) {
859
- const keyBytesResult = this._cryptoProvider.fromBase64(jsonEntry.key);
860
- /* c8 ignore next 3 - error path tested but coverage intermittently missed */
861
- if (keyBytesResult.isFailure()) {
862
- return (0, ts_utils_1.fail)(`Invalid key for secret '${name}': ${keyBytesResult.message}`);
1026
+ if (jsonEntry.type === 'asymmetric-keypair') {
1027
+ const entry = {
1028
+ name,
1029
+ type: jsonEntry.type,
1030
+ id: jsonEntry.id,
1031
+ algorithm: jsonEntry.algorithm,
1032
+ publicKeyJwk: jsonEntry.publicKeyJwk,
1033
+ description: jsonEntry.description,
1034
+ createdAt: jsonEntry.createdAt
1035
+ };
1036
+ secrets.set(name, entry);
1037
+ }
1038
+ else {
1039
+ const keyBytesResult = this._cryptoProvider.fromBase64(jsonEntry.key);
1040
+ /* c8 ignore next 3 - error path tested but coverage intermittently missed */
1041
+ if (keyBytesResult.isFailure()) {
1042
+ return (0, ts_utils_1.fail)(`Invalid key for secret '${name}': ${keyBytesResult.message}`);
1043
+ }
1044
+ const entry = {
1045
+ name,
1046
+ type: jsonEntry.type,
1047
+ key: keyBytesResult.value,
1048
+ description: jsonEntry.description,
1049
+ createdAt: jsonEntry.createdAt
1050
+ };
1051
+ secrets.set(name, entry);
863
1052
  }
864
- const entry = {
865
- name,
866
- /* c8 ignore next 1 - backwards compatibility: old vaults may lack type field */
867
- type: (_a = jsonEntry.type) !== null && _a !== void 0 ? _a : 'encryption-key',
868
- key: keyBytesResult.value,
869
- description: jsonEntry.description,
870
- createdAt: jsonEntry.createdAt
871
- };
872
- secrets.set(name, entry);
873
1053
  }
874
1054
  // All validation passed — commit state atomically
875
1055
  this._salt = saltResult.value;
@@ -878,6 +1058,51 @@ class KeyStore {
878
1058
  this._dirty = false;
879
1059
  return (0, ts_utils_1.succeed)(this);
880
1060
  }
1061
+ // ============================================================================
1062
+ // Private: Helpers for asymmetric flows
1063
+ // ============================================================================
1064
+ /**
1065
+ * Releases the resources held by an entry being displaced from the vault.
1066
+ * Symmetric entries get their key buffer zeroed in place. Asymmetric entries
1067
+ * have their private-key blob best-effort deleted from
1068
+ * {@link CryptoUtils.KeyStore.IPrivateKeyStorage}; if the storage call fails,
1069
+ * a warning string is returned but the displacement still proceeds — the
1070
+ * orphaned blob is left for consumer-side GC. Without a configured provider,
1071
+ * asymmetric cleanup is silently skipped.
1072
+ * @returns A warning string if storage cleanup failed, otherwise undefined.
1073
+ */
1074
+ async _releaseEntryResources(entry) {
1075
+ if (entry.type === 'asymmetric-keypair') {
1076
+ if (!this._privateKeyStorage) {
1077
+ return undefined;
1078
+ }
1079
+ const deleteResult = await this._privateKeyStorage.delete(entry.id);
1080
+ if (deleteResult.isFailure()) {
1081
+ return `Failed to delete prior storage blob for '${entry.name}' (id ${entry.id}): ${deleteResult.message}`;
1082
+ }
1083
+ return undefined;
1084
+ }
1085
+ entry.key.fill(0);
1086
+ return undefined;
1087
+ }
1088
+ /**
1089
+ * Mints a fresh UUID v4 storage handle using the crypto provider's
1090
+ * {@link CryptoUtils.ICryptoProvider.generateRandomBytes | generateRandomBytes}.
1091
+ * Random-bytes failures propagate as Failure.
1092
+ */
1093
+ _generateId() {
1094
+ return this._cryptoProvider.generateRandomBytes(16).onSuccess((bytes) => {
1095
+ // Per RFC 4122 §4.4: set version (4) and variant (10xx) bits.
1096
+ // eslint-disable-next-line no-bitwise
1097
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
1098
+ // eslint-disable-next-line no-bitwise
1099
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
1100
+ const hex = Array.from(bytes)
1101
+ .map((b) => b.toString(16).padStart(2, '0'))
1102
+ .join('');
1103
+ return (0, ts_utils_1.succeed)(`${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`);
1104
+ });
1105
+ }
881
1106
  }
882
1107
  exports.KeyStore = KeyStore;
883
1108
  //# sourceMappingURL=keyStore.js.map