@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
@@ -1,5 +1,5 @@
1
1
  import { Result } from '@fgv/ts-utils';
2
- import { ICryptoProvider, IEncryptionResult } from './model';
2
+ import { ICryptoProvider, IEncryptionResult, IWrapBytesOptions, IWrappedBytes, KeyPairAlgorithm } from './model';
3
3
  /**
4
4
  * Node.js implementation of {@link CryptoUtils.ICryptoProvider} using the built-in crypto module.
5
5
  * Uses AES-256-GCM for authenticated encryption.
@@ -59,6 +59,50 @@ export declare class NodeCryptoProvider implements ICryptoProvider {
59
59
  * @returns Success with decoded bytes, or Failure if invalid base64
60
60
  */
61
61
  fromBase64(base64: string): Result<Uint8Array>;
62
+ /**
63
+ * Generates a new asymmetric keypair using Node's WebCrypto.
64
+ * @param algorithm - The {@link CryptoUtils.KeyPairAlgorithm | algorithm} to use.
65
+ * @param extractable - Whether the resulting keys may be exported.
66
+ * @returns `Success` with the generated `CryptoKeyPair`, or `Failure` with an error.
67
+ */
68
+ generateKeyPair(algorithm: KeyPairAlgorithm, extractable: boolean): Promise<Result<CryptoKeyPair>>;
69
+ /**
70
+ * Exports a public `CryptoKey` as a JSON Web Key.
71
+ * @remarks
72
+ * Rejects non-public keys at runtime. WebCrypto's `exportKey('jwk', ...)`
73
+ * does not enforce public-vs-private; without this guard a caller that
74
+ * passed an extractable private key would receive its private fields
75
+ * (`d`, `p`, `q`, ...) as JWK, defeating the method's name.
76
+ * @param publicKey - Extractable public key to export.
77
+ * @returns `Success` with the JWK, or `Failure` if not a public key or if export fails.
78
+ */
79
+ exportPublicKeyJwk(publicKey: CryptoKey): Promise<Result<JsonWebKey>>;
80
+ /**
81
+ * Imports a public-key JWK as a `CryptoKey` for the requested algorithm.
82
+ * @param jwk - The JSON Web Key produced by a prior export.
83
+ * @param algorithm - The algorithm the key was generated for.
84
+ * @returns `Success` with the imported public `CryptoKey`, or `Failure` with an error.
85
+ */
86
+ importPublicKeyJwk(jwk: JsonWebKey, algorithm: KeyPairAlgorithm): Promise<Result<CryptoKey>>;
87
+ /**
88
+ * Wraps `plaintext` for the holder of `recipientPublicKey` using
89
+ * ECIES (ECDH P-256 + HKDF-SHA256 + AES-GCM-256). See
90
+ * {@link CryptoUtils.ICryptoProvider.wrapBytes | ICryptoProvider.wrapBytes}.
91
+ * @param plaintext - The bytes to wrap.
92
+ * @param recipientPublicKey - The recipient's ECDH P-256 public `CryptoKey`.
93
+ * @param options - HKDF salt and info; see {@link CryptoUtils.IWrapBytesOptions | IWrapBytesOptions}.
94
+ * @returns `Success` with the wrapped payload, or `Failure` with an error.
95
+ */
96
+ wrapBytes(plaintext: Uint8Array, recipientPublicKey: CryptoKey, options: IWrapBytesOptions): Promise<Result<IWrappedBytes>>;
97
+ /**
98
+ * Unwraps a payload produced by `wrapBytes` using the recipient's private
99
+ * key. See {@link CryptoUtils.ICryptoProvider.unwrapBytes | ICryptoProvider.unwrapBytes}.
100
+ * @param wrapped - The wrapped payload.
101
+ * @param recipientPrivateKey - The recipient's ECDH P-256 private `CryptoKey`.
102
+ * @param options - HKDF salt and info matching the wrap call.
103
+ * @returns `Success` with the original `plaintext`, or `Failure` with an error.
104
+ */
105
+ unwrapBytes(wrapped: IWrappedBytes, recipientPrivateKey: CryptoKey, options: IWrapBytesOptions): Promise<Result<Uint8Array>>;
62
106
  }
63
107
  /**
64
108
  * Singleton instance of {@link CryptoUtils.NodeCryptoProvider}.
@@ -56,6 +56,7 @@ exports.nodeCryptoProvider = exports.NodeCryptoProvider = void 0;
56
56
  const crypto = __importStar(require("crypto"));
57
57
  const ts_utils_1 = require("@fgv/ts-utils");
58
58
  const Constants = __importStar(require("./constants"));
59
+ const keyPairAlgorithmParams_1 = require("./keyPairAlgorithmParams");
59
60
  /**
60
61
  * Node.js implementation of {@link CryptoUtils.ICryptoProvider} using the built-in crypto module.
61
62
  * Uses AES-256-GCM for authenticated encryption.
@@ -198,8 +199,146 @@ class NodeCryptoProvider {
198
199
  }
199
200
  return ts_utils_1.Success.with(new Uint8Array(Buffer.from(base64, 'base64')));
200
201
  }
202
+ // ============================================================================
203
+ // Asymmetric Key Operations
204
+ // ============================================================================
205
+ /**
206
+ * Generates a new asymmetric keypair using Node's WebCrypto.
207
+ * @param algorithm - The {@link CryptoUtils.KeyPairAlgorithm | algorithm} to use.
208
+ * @param extractable - Whether the resulting keys may be exported.
209
+ * @returns `Success` with the generated `CryptoKeyPair`, or `Failure` with an error.
210
+ */
211
+ async generateKeyPair(algorithm, extractable) {
212
+ const params = keyPairAlgorithmParams_1.keyPairAlgorithmParams[algorithm];
213
+ const result = await (0, ts_utils_1.captureAsyncResult)(() => crypto.webcrypto.subtle.generateKey(params.generateKey, extractable, params.keyPairUsages));
214
+ return result.withErrorFormat((e) => `Failed to generate ${algorithm} keypair: ${e}`);
215
+ }
216
+ /**
217
+ * Exports a public `CryptoKey` as a JSON Web Key.
218
+ * @remarks
219
+ * Rejects non-public keys at runtime. WebCrypto's `exportKey('jwk', ...)`
220
+ * does not enforce public-vs-private; without this guard a caller that
221
+ * passed an extractable private key would receive its private fields
222
+ * (`d`, `p`, `q`, ...) as JWK, defeating the method's name.
223
+ * @param publicKey - Extractable public key to export.
224
+ * @returns `Success` with the JWK, or `Failure` if not a public key or if export fails.
225
+ */
226
+ async exportPublicKeyJwk(publicKey) {
227
+ if (publicKey.type !== 'public') {
228
+ return (0, ts_utils_1.fail)(`exportPublicKeyJwk requires a public CryptoKey, got '${publicKey.type}'`);
229
+ }
230
+ const result = await (0, ts_utils_1.captureAsyncResult)(() => crypto.webcrypto.subtle.exportKey('jwk', publicKey));
231
+ return result.withErrorFormat((e) => `Failed to export public key as JWK: ${e}`);
232
+ }
233
+ /**
234
+ * Imports a public-key JWK as a `CryptoKey` for the requested algorithm.
235
+ * @param jwk - The JSON Web Key produced by a prior export.
236
+ * @param algorithm - The algorithm the key was generated for.
237
+ * @returns `Success` with the imported public `CryptoKey`, or `Failure` with an error.
238
+ */
239
+ async importPublicKeyJwk(jwk, algorithm) {
240
+ const params = keyPairAlgorithmParams_1.keyPairAlgorithmParams[algorithm];
241
+ const result = await (0, ts_utils_1.captureAsyncResult)(() => crypto.webcrypto.subtle.importKey('jwk', jwk, params.importPublicKey, true, params.publicKeyUsages));
242
+ return result.withErrorFormat((e) => `Failed to import ${algorithm} public key from JWK: ${e}`);
243
+ }
244
+ /**
245
+ * Wraps `plaintext` for the holder of `recipientPublicKey` using
246
+ * ECIES (ECDH P-256 + HKDF-SHA256 + AES-GCM-256). See
247
+ * {@link CryptoUtils.ICryptoProvider.wrapBytes | ICryptoProvider.wrapBytes}.
248
+ * @param plaintext - The bytes to wrap.
249
+ * @param recipientPublicKey - The recipient's ECDH P-256 public `CryptoKey`.
250
+ * @param options - HKDF salt and info; see {@link CryptoUtils.IWrapBytesOptions | IWrapBytesOptions}.
251
+ * @returns `Success` with the wrapped payload, or `Failure` with an error.
252
+ */
253
+ async wrapBytes(plaintext, recipientPublicKey, options) {
254
+ const recipientCheck = checkEcdhP256(recipientPublicKey, 'public', 'recipient public key');
255
+ if (recipientCheck.isFailure()) {
256
+ return (0, ts_utils_1.fail)(`wrapBytes failed: ${recipientCheck.message}`);
257
+ }
258
+ const subtle = crypto.webcrypto.subtle;
259
+ const result = await (0, ts_utils_1.captureAsyncResult)(async () => {
260
+ const ephemeral = (await subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, [
261
+ 'deriveKey'
262
+ ]));
263
+ const hkdfBase = await subtle.deriveKey({ name: 'ECDH', public: recipientPublicKey }, ephemeral.privateKey, { name: 'HKDF' }, false, ['deriveKey']);
264
+ const wrapKey = await subtle.deriveKey({ name: 'HKDF', salt: options.salt, info: options.info, hash: 'SHA-256' }, hkdfBase, { name: 'AES-GCM', length: 256 }, false, ['encrypt']);
265
+ const nonce = crypto.randomBytes(Constants.GCM_IV_SIZE);
266
+ const ctBuf = await subtle.encrypt({ name: 'AES-GCM', iv: nonce }, wrapKey, plaintext);
267
+ const ephemeralPublicKey = await subtle.exportKey('jwk', ephemeral.publicKey);
268
+ return {
269
+ ephemeralPublicKey,
270
+ nonce: this.toBase64(nonce),
271
+ ciphertext: this.toBase64(new Uint8Array(ctBuf))
272
+ };
273
+ });
274
+ return result.withErrorFormat((e) => `wrapBytes failed: ${e}`);
275
+ }
276
+ /**
277
+ * Unwraps a payload produced by `wrapBytes` using the recipient's private
278
+ * key. See {@link CryptoUtils.ICryptoProvider.unwrapBytes | ICryptoProvider.unwrapBytes}.
279
+ * @param wrapped - The wrapped payload.
280
+ * @param recipientPrivateKey - The recipient's ECDH P-256 private `CryptoKey`.
281
+ * @param options - HKDF salt and info matching the wrap call.
282
+ * @returns `Success` with the original `plaintext`, or `Failure` with an error.
283
+ */
284
+ async unwrapBytes(wrapped, recipientPrivateKey, options) {
285
+ const recipientCheck = checkEcdhP256(recipientPrivateKey, 'private', 'recipient private key');
286
+ if (recipientCheck.isFailure()) {
287
+ return (0, ts_utils_1.fail)(`unwrapBytes failed: ${recipientCheck.message}`);
288
+ }
289
+ const nonceResult = this.fromBase64(wrapped.nonce);
290
+ if (nonceResult.isFailure()) {
291
+ return (0, ts_utils_1.fail)(`unwrapBytes failed: nonce: ${nonceResult.message}`);
292
+ }
293
+ if (nonceResult.value.length !== Constants.GCM_IV_SIZE) {
294
+ return (0, ts_utils_1.fail)(`unwrapBytes failed: nonce must be ${Constants.GCM_IV_SIZE} bytes (got ${nonceResult.value.length})`);
295
+ }
296
+ const ciphertextResult = this.fromBase64(wrapped.ciphertext);
297
+ if (ciphertextResult.isFailure()) {
298
+ return (0, ts_utils_1.fail)(`unwrapBytes failed: ciphertext: ${ciphertextResult.message}`);
299
+ }
300
+ if (ciphertextResult.value.length < Constants.GCM_AUTH_TAG_SIZE) {
301
+ return (0, ts_utils_1.fail)(`unwrapBytes failed: ciphertext must be at least ${Constants.GCM_AUTH_TAG_SIZE} bytes (got ${ciphertextResult.value.length})`);
302
+ }
303
+ const subtle = crypto.webcrypto.subtle;
304
+ const result = await (0, ts_utils_1.captureAsyncResult)(async () => {
305
+ const ephemeralPub = await subtle.importKey('jwk', wrapped.ephemeralPublicKey, { name: 'ECDH', namedCurve: 'P-256' }, false, []);
306
+ const hkdfBase = await subtle.deriveKey({ name: 'ECDH', public: ephemeralPub }, recipientPrivateKey, { name: 'HKDF' }, false, ['deriveKey']);
307
+ const wrapKey = await subtle.deriveKey({ name: 'HKDF', salt: options.salt, info: options.info, hash: 'SHA-256' }, hkdfBase, { name: 'AES-GCM', length: 256 }, false, ['decrypt']);
308
+ const ptBuf = await subtle.decrypt({ name: 'AES-GCM', iv: nonceResult.value }, wrapKey, ciphertextResult.value);
309
+ return new Uint8Array(ptBuf);
310
+ });
311
+ return result.withErrorFormat((e) => `unwrapBytes failed: ${e}`);
312
+ }
201
313
  }
202
314
  exports.NodeCryptoProvider = NodeCryptoProvider;
315
+ /**
316
+ * Verifies that `key` is an ECDH P-256 `CryptoKey` of the expected `keyType`
317
+ * (public or private). Used by the wrap/unwrap methods to surface a clean
318
+ * `Failure` instead of letting the WebCrypto deriveKey call throw a less
319
+ * informative error later in the pipeline. Key usages are intentionally not
320
+ * checked here: WebCrypto already produces a specific error if `deriveKey` is
321
+ * not in `usages`, and `deriveBits` is an equally valid alternative usage that
322
+ * an explicit check would have to track.
323
+ * @param key - The CryptoKey to validate.
324
+ * @param keyType - The required `key.type` ('public' for wrap, 'private' for unwrap).
325
+ * @param label - Human-readable role label included in the failure message.
326
+ * @returns `Success` with the key (unchanged) when the algorithm, curve, and
327
+ * type all match; otherwise `Failure` with `<label> must be ECDH P-256 (...)`.
328
+ */
329
+ function checkEcdhP256(key, keyType, label) {
330
+ if (key.algorithm.name !== 'ECDH') {
331
+ return (0, ts_utils_1.fail)(`${label} must be ECDH P-256 (got algorithm '${key.algorithm.name}')`);
332
+ }
333
+ const namedCurve = key.algorithm.namedCurve;
334
+ if (namedCurve !== 'P-256') {
335
+ return (0, ts_utils_1.fail)(`${label} must be ECDH P-256 (got curve '${namedCurve}')`);
336
+ }
337
+ if (key.type !== keyType) {
338
+ return (0, ts_utils_1.fail)(`${label} must be a ${keyType} CryptoKey (got '${key.type}')`);
339
+ }
340
+ return (0, ts_utils_1.succeed)(key);
341
+ }
203
342
  /**
204
343
  * Singleton instance of {@link CryptoUtils.NodeCryptoProvider}.
205
344
  * @public
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fgv/ts-extras",
3
- "version": "5.1.0-17",
3
+ "version": "5.1.0-19",
4
4
  "description": "Assorted Typescript Utilities",
5
5
  "main": "lib/index.js",
6
6
  "types": "dist/ts-extras.d.ts",
@@ -86,10 +86,10 @@
86
86
  "@types/js-yaml": "~4.0.9",
87
87
  "typedoc": "~0.28.16",
88
88
  "typedoc-plugin-markdown": "~4.9.0",
89
- "@fgv/ts-utils-jest": "5.1.0-17",
90
- "@fgv/typedoc-compact-theme": "5.1.0-17",
91
- "@fgv/ts-utils": "5.1.0-17",
92
- "@fgv/heft-dual-rig": "5.1.0-17"
89
+ "@fgv/heft-dual-rig": "5.1.0-19",
90
+ "@fgv/typedoc-compact-theme": "5.1.0-19",
91
+ "@fgv/ts-utils": "5.1.0-19",
92
+ "@fgv/ts-utils-jest": "5.1.0-19"
93
93
  },
94
94
  "dependencies": {
95
95
  "luxon": "^3.7.2",
@@ -97,10 +97,10 @@
97
97
  "papaparse": "^5.4.1",
98
98
  "fflate": "~0.8.2",
99
99
  "js-yaml": "~4.1.1",
100
- "@fgv/ts-json-base": "5.1.0-17"
100
+ "@fgv/ts-json-base": "5.1.0-19"
101
101
  },
102
102
  "peerDependencies": {
103
- "@fgv/ts-utils": "5.1.0-17"
103
+ "@fgv/ts-utils": "5.1.0-19"
104
104
  },
105
105
  "repository": {
106
106
  "type": "git",