@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
@@ -17,6 +17,9 @@
17
17
  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
18
  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
19
  // SOFTWARE.
20
+ // Re-export so consumers can continue to access the algorithm enum via the
21
+ // CryptoUtils.KeyStore namespace alongside the rest of the keystore types.
22
+ export { allKeyPairAlgorithms } from '../model';
20
23
  /**
21
24
  * Current format version constant.
22
25
  * @public
@@ -33,11 +36,29 @@ export const DEFAULT_KEYSTORE_ITERATIONS = 600000;
33
36
  * @public
34
37
  */
35
38
  export const MIN_SALT_LENGTH = 16;
39
+ /**
40
+ * All valid symmetric secret types.
41
+ * @public
42
+ */
43
+ export const allKeyStoreSymmetricSecretTypes = [
44
+ 'encryption-key',
45
+ 'api-key'
46
+ ];
47
+ /**
48
+ * All valid asymmetric secret types.
49
+ * @public
50
+ */
51
+ export const allKeyStoreAsymmetricSecretTypes = [
52
+ 'asymmetric-keypair'
53
+ ];
36
54
  /**
37
55
  * All valid key store secret types.
38
56
  * @public
39
57
  */
40
- export const allKeyStoreSecretTypes = ['encryption-key', 'api-key'];
58
+ export const allKeyStoreSecretTypes = [
59
+ ...allKeyStoreAsymmetricSecretTypes,
60
+ ...allKeyStoreSymmetricSecretTypes
61
+ ];
41
62
  /**
42
63
  * Default PBKDF2 iterations for secret-level key derivation.
43
64
  * Lower than keystore encryption since these are used more frequently.
@@ -0,0 +1,21 @@
1
+ // Copyright (c) 2026 Erik Fortune
2
+ //
3
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ // of this software and associated documentation files (the "Software"), to deal
5
+ // in the Software without restriction, including without limitation the rights
6
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ // copies of the Software, and to permit persons to whom the Software is
8
+ // furnished to do so, subject to the following conditions:
9
+ //
10
+ // The above copyright notice and this permission notice shall be included in all
11
+ // copies or substantial portions of the Software.
12
+ //
13
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ // SOFTWARE.
20
+ export {};
21
+ //# sourceMappingURL=privateKeyStorage.js.map
@@ -19,6 +19,11 @@
19
19
  // SOFTWARE.
20
20
  import * as Constants from './constants';
21
21
  export { Constants };
22
+ /**
23
+ * All valid key pair algorithms.
24
+ * @public
25
+ */
26
+ export const allKeyPairAlgorithms = ['ecdsa-p256', 'rsa-oaep-2048'];
22
27
  // ============================================================================
23
28
  // Detection Helper
24
29
  // ============================================================================
@@ -18,8 +18,9 @@
18
18
  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
19
  // SOFTWARE.
20
20
  import * as crypto from 'crypto';
21
- import { captureResult, fail, Failure, succeed, Success } from '@fgv/ts-utils';
21
+ import { captureAsyncResult, captureResult, fail, Failure, succeed, Success } from '@fgv/ts-utils';
22
22
  import * as Constants from './constants';
23
+ import { keyPairAlgorithmParams } from './keyPairAlgorithmParams';
23
24
  /**
24
25
  * Node.js implementation of {@link CryptoUtils.ICryptoProvider} using the built-in crypto module.
25
26
  * Uses AES-256-GCM for authenticated encryption.
@@ -162,6 +163,144 @@ export class NodeCryptoProvider {
162
163
  }
163
164
  return Success.with(new Uint8Array(Buffer.from(base64, 'base64')));
164
165
  }
166
+ // ============================================================================
167
+ // Asymmetric Key Operations
168
+ // ============================================================================
169
+ /**
170
+ * Generates a new asymmetric keypair using Node's WebCrypto.
171
+ * @param algorithm - The {@link CryptoUtils.KeyPairAlgorithm | algorithm} to use.
172
+ * @param extractable - Whether the resulting keys may be exported.
173
+ * @returns `Success` with the generated `CryptoKeyPair`, or `Failure` with an error.
174
+ */
175
+ async generateKeyPair(algorithm, extractable) {
176
+ const params = keyPairAlgorithmParams[algorithm];
177
+ const result = await captureAsyncResult(() => crypto.webcrypto.subtle.generateKey(params.generateKey, extractable, params.keyPairUsages));
178
+ return result.withErrorFormat((e) => `Failed to generate ${algorithm} keypair: ${e}`);
179
+ }
180
+ /**
181
+ * Exports a public `CryptoKey` as a JSON Web Key.
182
+ * @remarks
183
+ * Rejects non-public keys at runtime. WebCrypto's `exportKey('jwk', ...)`
184
+ * does not enforce public-vs-private; without this guard a caller that
185
+ * passed an extractable private key would receive its private fields
186
+ * (`d`, `p`, `q`, ...) as JWK, defeating the method's name.
187
+ * @param publicKey - Extractable public key to export.
188
+ * @returns `Success` with the JWK, or `Failure` if not a public key or if export fails.
189
+ */
190
+ async exportPublicKeyJwk(publicKey) {
191
+ if (publicKey.type !== 'public') {
192
+ return fail(`exportPublicKeyJwk requires a public CryptoKey, got '${publicKey.type}'`);
193
+ }
194
+ const result = await captureAsyncResult(() => crypto.webcrypto.subtle.exportKey('jwk', publicKey));
195
+ return result.withErrorFormat((e) => `Failed to export public key as JWK: ${e}`);
196
+ }
197
+ /**
198
+ * Imports a public-key JWK as a `CryptoKey` for the requested algorithm.
199
+ * @param jwk - The JSON Web Key produced by a prior export.
200
+ * @param algorithm - The algorithm the key was generated for.
201
+ * @returns `Success` with the imported public `CryptoKey`, or `Failure` with an error.
202
+ */
203
+ async importPublicKeyJwk(jwk, algorithm) {
204
+ const params = keyPairAlgorithmParams[algorithm];
205
+ const result = await captureAsyncResult(() => crypto.webcrypto.subtle.importKey('jwk', jwk, params.importPublicKey, true, params.publicKeyUsages));
206
+ return result.withErrorFormat((e) => `Failed to import ${algorithm} public key from JWK: ${e}`);
207
+ }
208
+ /**
209
+ * Wraps `plaintext` for the holder of `recipientPublicKey` using
210
+ * ECIES (ECDH P-256 + HKDF-SHA256 + AES-GCM-256). See
211
+ * {@link CryptoUtils.ICryptoProvider.wrapBytes | ICryptoProvider.wrapBytes}.
212
+ * @param plaintext - The bytes to wrap.
213
+ * @param recipientPublicKey - The recipient's ECDH P-256 public `CryptoKey`.
214
+ * @param options - HKDF salt and info; see {@link CryptoUtils.IWrapBytesOptions | IWrapBytesOptions}.
215
+ * @returns `Success` with the wrapped payload, or `Failure` with an error.
216
+ */
217
+ async wrapBytes(plaintext, recipientPublicKey, options) {
218
+ const recipientCheck = checkEcdhP256(recipientPublicKey, 'public', 'recipient public key');
219
+ if (recipientCheck.isFailure()) {
220
+ return fail(`wrapBytes failed: ${recipientCheck.message}`);
221
+ }
222
+ const subtle = crypto.webcrypto.subtle;
223
+ const result = await captureAsyncResult(async () => {
224
+ const ephemeral = (await subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, [
225
+ 'deriveKey'
226
+ ]));
227
+ const hkdfBase = await subtle.deriveKey({ name: 'ECDH', public: recipientPublicKey }, ephemeral.privateKey, { name: 'HKDF' }, false, ['deriveKey']);
228
+ const wrapKey = await subtle.deriveKey({ name: 'HKDF', salt: options.salt, info: options.info, hash: 'SHA-256' }, hkdfBase, { name: 'AES-GCM', length: 256 }, false, ['encrypt']);
229
+ const nonce = crypto.randomBytes(Constants.GCM_IV_SIZE);
230
+ const ctBuf = await subtle.encrypt({ name: 'AES-GCM', iv: nonce }, wrapKey, plaintext);
231
+ const ephemeralPublicKey = await subtle.exportKey('jwk', ephemeral.publicKey);
232
+ return {
233
+ ephemeralPublicKey,
234
+ nonce: this.toBase64(nonce),
235
+ ciphertext: this.toBase64(new Uint8Array(ctBuf))
236
+ };
237
+ });
238
+ return result.withErrorFormat((e) => `wrapBytes failed: ${e}`);
239
+ }
240
+ /**
241
+ * Unwraps a payload produced by `wrapBytes` using the recipient's private
242
+ * key. See {@link CryptoUtils.ICryptoProvider.unwrapBytes | ICryptoProvider.unwrapBytes}.
243
+ * @param wrapped - The wrapped payload.
244
+ * @param recipientPrivateKey - The recipient's ECDH P-256 private `CryptoKey`.
245
+ * @param options - HKDF salt and info matching the wrap call.
246
+ * @returns `Success` with the original `plaintext`, or `Failure` with an error.
247
+ */
248
+ async unwrapBytes(wrapped, recipientPrivateKey, options) {
249
+ const recipientCheck = checkEcdhP256(recipientPrivateKey, 'private', 'recipient private key');
250
+ if (recipientCheck.isFailure()) {
251
+ return fail(`unwrapBytes failed: ${recipientCheck.message}`);
252
+ }
253
+ const nonceResult = this.fromBase64(wrapped.nonce);
254
+ if (nonceResult.isFailure()) {
255
+ return fail(`unwrapBytes failed: nonce: ${nonceResult.message}`);
256
+ }
257
+ if (nonceResult.value.length !== Constants.GCM_IV_SIZE) {
258
+ return fail(`unwrapBytes failed: nonce must be ${Constants.GCM_IV_SIZE} bytes (got ${nonceResult.value.length})`);
259
+ }
260
+ const ciphertextResult = this.fromBase64(wrapped.ciphertext);
261
+ if (ciphertextResult.isFailure()) {
262
+ return fail(`unwrapBytes failed: ciphertext: ${ciphertextResult.message}`);
263
+ }
264
+ if (ciphertextResult.value.length < Constants.GCM_AUTH_TAG_SIZE) {
265
+ return fail(`unwrapBytes failed: ciphertext must be at least ${Constants.GCM_AUTH_TAG_SIZE} bytes (got ${ciphertextResult.value.length})`);
266
+ }
267
+ const subtle = crypto.webcrypto.subtle;
268
+ const result = await captureAsyncResult(async () => {
269
+ const ephemeralPub = await subtle.importKey('jwk', wrapped.ephemeralPublicKey, { name: 'ECDH', namedCurve: 'P-256' }, false, []);
270
+ const hkdfBase = await subtle.deriveKey({ name: 'ECDH', public: ephemeralPub }, recipientPrivateKey, { name: 'HKDF' }, false, ['deriveKey']);
271
+ const wrapKey = await subtle.deriveKey({ name: 'HKDF', salt: options.salt, info: options.info, hash: 'SHA-256' }, hkdfBase, { name: 'AES-GCM', length: 256 }, false, ['decrypt']);
272
+ const ptBuf = await subtle.decrypt({ name: 'AES-GCM', iv: nonceResult.value }, wrapKey, ciphertextResult.value);
273
+ return new Uint8Array(ptBuf);
274
+ });
275
+ return result.withErrorFormat((e) => `unwrapBytes failed: ${e}`);
276
+ }
277
+ }
278
+ /**
279
+ * Verifies that `key` is an ECDH P-256 `CryptoKey` of the expected `keyType`
280
+ * (public or private). Used by the wrap/unwrap methods to surface a clean
281
+ * `Failure` instead of letting the WebCrypto deriveKey call throw a less
282
+ * informative error later in the pipeline. Key usages are intentionally not
283
+ * checked here: WebCrypto already produces a specific error if `deriveKey` is
284
+ * not in `usages`, and `deriveBits` is an equally valid alternative usage that
285
+ * an explicit check would have to track.
286
+ * @param key - The CryptoKey to validate.
287
+ * @param keyType - The required `key.type` ('public' for wrap, 'private' for unwrap).
288
+ * @param label - Human-readable role label included in the failure message.
289
+ * @returns `Success` with the key (unchanged) when the algorithm, curve, and
290
+ * type all match; otherwise `Failure` with `<label> must be ECDH P-256 (...)`.
291
+ */
292
+ function checkEcdhP256(key, keyType, label) {
293
+ if (key.algorithm.name !== 'ECDH') {
294
+ return fail(`${label} must be ECDH P-256 (got algorithm '${key.algorithm.name}')`);
295
+ }
296
+ const namedCurve = key.algorithm.namedCurve;
297
+ if (namedCurve !== 'P-256') {
298
+ return fail(`${label} must be ECDH P-256 (got curve '${namedCurve}')`);
299
+ }
300
+ if (key.type !== keyType) {
301
+ return fail(`${label} must be a ${keyType} CryptoKey (got '${key.type}')`);
302
+ }
303
+ return succeed(key);
165
304
  }
166
305
  /**
167
306
  * Singleton instance of {@link CryptoUtils.NodeCryptoProvider}.
@@ -0,0 +1,78 @@
1
+ // Copyright (c) 2026 Erik Fortune
2
+ //
3
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ // of this software and associated documentation files (the "Software"), to deal
5
+ // in the Software without restriction, including without limitation the rights
6
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ // copies of the Software, and to permit persons to whom the Software is
8
+ // furnished to do so, subject to the following conditions:
9
+ //
10
+ // The above copyright notice and this permission notice shall be included in all
11
+ // copies or substantial portions of the Software.
12
+ //
13
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ // SOFTWARE.
20
+ import { fail, succeed } from '@fgv/ts-utils';
21
+ /**
22
+ * In-memory implementation of `IPrivateKeyStorage` for keystore tests.
23
+ *
24
+ * Not exported from `@fgv/ts-extras`. Lives in the test tree as a reference
25
+ * implementation downstream packages may copy if useful, but the public API
26
+ * delegates concrete backends to platform-specific packages
27
+ * (browser/IDB in `@fgv/ts-web-extras`, encrypted-file in `@fgv/ts-chocolate`).
28
+ *
29
+ * Holds `CryptoKey` objects directly in a `Map`, so it advertises
30
+ * `supportsNonExtractable: true` by default. The constructor accepts overrides
31
+ * useful for negative tests:
32
+ * - `supportsNonExtractable: false` exercises the extractable=true codepath.
33
+ * - `failOn: { store?, load?, delete?, list? }` makes the named operations
34
+ * return Failure with a stable message so tests can assert downstream
35
+ * warning/error propagation without resorting to mocks.
36
+ */
37
+ export class InMemoryPrivateKeyStorage {
38
+ constructor(options) {
39
+ var _a, _b;
40
+ this.supportsNonExtractable = (_a = options === null || options === void 0 ? void 0 : options.supportsNonExtractable) !== null && _a !== void 0 ? _a : true;
41
+ this.entries = new Map();
42
+ this._failOn = (_b = options === null || options === void 0 ? void 0 : options.failOn) !== null && _b !== void 0 ? _b : {};
43
+ }
44
+ async store(id, key) {
45
+ if (this._failOn.store) {
46
+ return fail(this._failOn.store);
47
+ }
48
+ this.entries.set(id, key);
49
+ return succeed(id);
50
+ }
51
+ async load(id) {
52
+ if (this._failOn.load) {
53
+ return fail(this._failOn.load);
54
+ }
55
+ const key = this.entries.get(id);
56
+ if (!key) {
57
+ return fail(`No private key for id '${id}'`);
58
+ }
59
+ return succeed(key);
60
+ }
61
+ async delete(id) {
62
+ if (this._failOn.delete) {
63
+ return fail(this._failOn.delete);
64
+ }
65
+ if (!this.entries.has(id)) {
66
+ return fail(`No private key for id '${id}'`);
67
+ }
68
+ this.entries.delete(id);
69
+ return succeed(id);
70
+ }
71
+ async list() {
72
+ if (this._failOn.list) {
73
+ return fail(this._failOn.list);
74
+ }
75
+ return succeed(Array.from(this.entries.keys()));
76
+ }
77
+ }
78
+ //# sourceMappingURL=inMemoryPrivateKeyStorage.js.map