@fgv/ts-extras 5.1.0-16 → 5.1.0-18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.browser.js +2 -1
- package/dist/packlets/ai-assist/apiClient.js +570 -58
- package/dist/packlets/ai-assist/chatRequestBuilders.js +180 -0
- package/dist/packlets/ai-assist/index.js +4 -3
- package/dist/packlets/ai-assist/model.js +20 -3
- package/dist/packlets/ai-assist/registry.js +66 -10
- package/dist/packlets/ai-assist/sseParser.js +122 -0
- package/dist/packlets/ai-assist/streamingAdapters/anthropic.js +192 -0
- package/dist/packlets/ai-assist/streamingAdapters/common.js +77 -0
- package/dist/packlets/ai-assist/streamingAdapters/gemini.js +160 -0
- package/dist/packlets/ai-assist/streamingAdapters/openaiChat.js +149 -0
- package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js +163 -0
- package/dist/packlets/ai-assist/streamingAdapters/proxy.js +157 -0
- package/dist/packlets/ai-assist/streamingClient.js +88 -0
- package/dist/packlets/conversion/converters.js +1 -1
- package/dist/packlets/crypto-utils/index.browser.js +2 -0
- package/dist/packlets/crypto-utils/index.js +2 -0
- package/dist/packlets/crypto-utils/keyPairAlgorithmParams.js +47 -0
- package/dist/packlets/crypto-utils/keystore/converters.js +101 -9
- package/dist/packlets/crypto-utils/keystore/index.js +1 -0
- package/dist/packlets/crypto-utils/keystore/keyStore.js +271 -46
- package/dist/packlets/crypto-utils/keystore/model.js +22 -1
- package/dist/packlets/crypto-utils/keystore/privateKeyStorage.js +21 -0
- package/dist/packlets/crypto-utils/model.js +5 -0
- package/dist/packlets/crypto-utils/nodeCryptoProvider.js +44 -1
- package/dist/packlets/zip-file-tree/zipFileTreeAccessors.js +2 -2
- package/dist/test/unit/crypto/keystore/inMemoryPrivateKeyStorage.js +78 -0
- package/dist/ts-extras.d.ts +1089 -37
- package/lib/index.browser.d.ts +2 -1
- package/lib/index.browser.js +3 -1
- package/lib/packlets/ai-assist/apiClient.d.ts +103 -1
- package/lib/packlets/ai-assist/apiClient.js +574 -58
- package/lib/packlets/ai-assist/chatRequestBuilders.d.ts +89 -0
- package/lib/packlets/ai-assist/chatRequestBuilders.js +189 -0
- package/lib/packlets/ai-assist/index.d.ts +4 -3
- package/lib/packlets/ai-assist/index.js +10 -1
- package/lib/packlets/ai-assist/model.d.ts +271 -2
- package/lib/packlets/ai-assist/model.js +21 -3
- package/lib/packlets/ai-assist/registry.d.ts +10 -1
- package/lib/packlets/ai-assist/registry.js +67 -11
- package/lib/packlets/ai-assist/sseParser.d.ts +45 -0
- package/lib/packlets/ai-assist/sseParser.js +127 -0
- package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts +18 -0
- package/lib/packlets/ai-assist/streamingAdapters/anthropic.js +195 -0
- package/lib/packlets/ai-assist/streamingAdapters/common.d.ts +71 -0
- package/lib/packlets/ai-assist/streamingAdapters/common.js +81 -0
- package/lib/packlets/ai-assist/streamingAdapters/gemini.d.ts +19 -0
- package/lib/packlets/ai-assist/streamingAdapters/gemini.js +163 -0
- package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts +18 -0
- package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js +152 -0
- package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts +19 -0
- package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js +166 -0
- package/lib/packlets/ai-assist/streamingAdapters/proxy.d.ts +34 -0
- package/lib/packlets/ai-assist/streamingAdapters/proxy.js +160 -0
- package/lib/packlets/ai-assist/streamingClient.d.ts +33 -0
- package/lib/packlets/ai-assist/streamingClient.js +93 -0
- package/lib/packlets/conversion/converters.d.ts +1 -1
- package/lib/packlets/conversion/converters.js +1 -1
- package/lib/packlets/crypto-utils/index.browser.d.ts +1 -0
- package/lib/packlets/crypto-utils/index.browser.js +4 -1
- package/lib/packlets/crypto-utils/index.d.ts +1 -0
- package/lib/packlets/crypto-utils/index.js +4 -1
- package/lib/packlets/crypto-utils/keyPairAlgorithmParams.d.ts +39 -0
- package/lib/packlets/crypto-utils/keyPairAlgorithmParams.js +50 -0
- package/lib/packlets/crypto-utils/keystore/converters.d.ts +68 -6
- package/lib/packlets/crypto-utils/keystore/converters.js +100 -8
- package/lib/packlets/crypto-utils/keystore/index.d.ts +1 -0
- package/lib/packlets/crypto-utils/keystore/index.js +1 -0
- package/lib/packlets/crypto-utils/keystore/keyStore.d.ts +77 -9
- package/lib/packlets/crypto-utils/keystore/keyStore.js +271 -46
- package/lib/packlets/crypto-utils/keystore/model.d.ts +238 -19
- package/lib/packlets/crypto-utils/keystore/model.js +24 -2
- package/lib/packlets/crypto-utils/keystore/privateKeyStorage.d.ts +50 -0
- package/lib/packlets/crypto-utils/keystore/privateKeyStorage.js +22 -0
- package/lib/packlets/crypto-utils/model.d.ts +38 -0
- package/lib/packlets/crypto-utils/model.js +6 -1
- package/lib/packlets/crypto-utils/nodeCryptoProvider.d.ts +26 -1
- package/lib/packlets/crypto-utils/nodeCryptoProvider.js +43 -0
- package/lib/packlets/zip-file-tree/zipFileTreeAccessors.d.ts +2 -2
- package/lib/packlets/zip-file-tree/zipFileTreeAccessors.js +2 -2
- 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
|
|
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.
|
|
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
|
|
403
|
-
if (
|
|
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:
|
|
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
|
|
443
|
-
if (
|
|
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:
|
|
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
|
-
//
|
|
493
|
-
|
|
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
|
-
|
|
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
|
|
518
|
-
if (
|
|
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:
|
|
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
|
|
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
|
-
|
|
774
|
-
name
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
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
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
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
|