@arkstack/common 0.17.26 → 0.18.1

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/README.md CHANGED
@@ -515,9 +515,9 @@ Clears all registered hooks.
515
515
 
516
516
  **`src/utils/encryption.ts`**
517
517
 
518
- AES-256-GCM symmetric encryption for sensitive values (e.g. two-factor authentication secrets). Requires the `TWO_FACTOR_ENCRYPTION_KEY` environment variable.
518
+ A thin wrapper around [`@arkstack/encryption`](https://www.npmjs.com/package/@arkstack/encryption), bound to the application key. AES-256-GCM for sensitive values (e.g. two-factor authentication secrets); the package underneath runs on the Web Crypto API, so a value encrypted on the server can be decrypted in the browser and vice versa.
519
519
 
520
- #### `Encryption.encrypt(value)`
520
+ #### `Encryption.encrypt(value, key?)`
521
521
 
522
522
  Encrypts a string. Returns a colon-delimited base64url string: `<iv>:<authTag>:<ciphertext>`.
523
523
 
@@ -528,7 +528,9 @@ const token = Encryption.encrypt('my-secret-value');
528
528
  // "abc123:def456:ghi789"
529
529
  ```
530
530
 
531
- #### `Encryption.decrypt(payload)`
531
+ ---
532
+
533
+ #### `Encryption.decrypt(payload, key?)`
532
534
 
533
535
  Decrypts a payload produced by `encrypt`. Throws if the format is invalid or the key is wrong.
534
536
 
@@ -537,11 +539,54 @@ const original = Encryption.decrypt(token);
537
539
  // "my-secret-value"
538
540
  ```
539
541
 
542
+ ---
543
+
544
+ #### `Encryption.encryptAsync(value, key?, options?)` / `Encryption.decryptAsync(payload, key?, options?)`
545
+
546
+ The same operations on the Web Crypto path, in the same payload format, so the two can be mixed freely. Use these when the calling code is (or may become) shared with the browser. `options.aad` binds additional authenticated data to the ciphertext.
547
+
548
+ ---
549
+
550
+ #### `Encryption.cipher(key?)`
551
+
552
+ A `Cipher` bound to the application key, for encrypting many values without re-deriving the key, and for raw bytes via `encryptBytes` / `decryptBytes`.
553
+
554
+ ---
555
+
556
+ #### Key utilities
557
+
558
+ ```ts
559
+ Encryption.generateKey(); // random base64url key
560
+ await Encryption.generateKeyPair(); // { publicKey, privateKey } ECDH identity
561
+ await Encryption.deriveKey(password); // PBKDF2 → { key, salt, iterations }
562
+ await Encryption.compareKeys(left, right); // constant time
563
+ await Encryption.fingerprint(); // displayable digest of the app key
564
+ ```
565
+
566
+ ---
567
+
568
+ #### End-to-end encryption
569
+
570
+ ```ts
571
+ const channel = await Encryption.channel(myPrivateKey, peerPublicKey);
572
+
573
+ await channel.decrypt(await channel.encrypt('hey'));
574
+
575
+ await Encryption.seal('anonymous tip', peerPublicKey);
576
+ await Encryption.open(payload, myPrivateKey);
577
+
578
+ await Encryption.safetyNumber(myPublicKey, peerPublicKey);
579
+ ```
580
+
581
+ The full `@arkstack/encryption` surface — `Cipher`, `Codec`, `EncryptionKey`, `KeyPair`, `Keys`, `SealedBox`, `SecureChannel`, `NodeCipher` — is re-exported from this package.
582
+
540
583
  **Environment variable:**
541
584
 
542
- | Variable | Required | Description |
543
- | --------------------------- | -------- | ---------------------------------------------------------- |
544
- | `TWO_FACTOR_ENCRYPTION_KEY` | Yes | Raw secret; hashed to a 256-bit key internally via SHA-256 |
585
+ | Variable | Required | Description |
586
+ | --------- | -------- | ---------------------------------------------------------- |
587
+ | `APP_KEY` | Yes | Raw secret; hashed to a 256-bit key internally via SHA-256 |
588
+
589
+ Generate one with `ark key:generate`. The legacy `TWO_FACTOR_ENCRYPTION_KEY` is still honored when `APP_KEY` is not set.
545
590
 
546
591
  ---
547
592
 
@@ -1,7 +1,9 @@
1
+ import { ChannelOptions, Cipher, CipherOptions, Codec, DeriveOptions, DerivedKey, EncryptionKey, FingerprintOptions, KeyInput, KeyPair, Keys, SealedBox, SecureChannel, SerializedKeyPair } from "@arkstack/encryption";
2
+ import { NodeCipher } from "@arkstack/encryption/node";
1
3
  import { JitiOptions, JitiResolveOptions } from "jiti";
2
4
  import { TOTP } from "otpauth";
5
+ import { Model, ModelStatic, RegisteredModelClass, RegisteredModelName, RelatedModelClass } from "arkormx";
3
6
  import { ChalkInstance } from "chalk";
4
- import { Model, ModelStatic } from "arkormx";
5
7
  //#region src/Logger.d.ts
6
8
  declare class Console {
7
9
  static log: (...args: any[]) => string | void;
@@ -388,11 +390,160 @@ interface PublishConfirmation {
388
390
  }
389
391
  //#endregion
390
392
  //#region src/utils/encryption.d.ts
393
+ /**
394
+ * Application facing encryption, bound to the app key.
395
+ *
396
+ * This is a thin wrapper over `@arkstack/encryption`. `encrypt()` and
397
+ * `decrypt()` keep the synchronous signatures and the exact payload format
398
+ * they have always had — `<iv>:<authTag>:<ciphertext>`, AES-256-GCM under
399
+ * SHA-256 of `APP_KEY` — so existing ciphertexts and call sites are unaffected.
400
+ *
401
+ * Everything else is new surface: the asynchronous methods run on the Web
402
+ * Crypto API, which means a browser holding the same key (or the same key pair
403
+ * peer) can decrypt what the server wrote, and the server can decrypt what the
404
+ * browser wrote.
405
+ */
391
406
  declare class Encryption {
392
- private static readonly algorithm;
393
- private static getKey;
394
- static encrypt(value: string): string;
395
- static decrypt(payload: string): string;
407
+ /**
408
+ * Encrypt a string with the application key.
409
+ *
410
+ * @param value
411
+ * @param key Override the application key for this call.
412
+ * @returns
413
+ */
414
+ static encrypt(value: string, key?: KeyInput): string;
415
+ /**
416
+ * Decrypt a payload produced by {@link encrypt}.
417
+ *
418
+ * @param payload
419
+ * @param key Override the application key for this call.
420
+ * @returns
421
+ */
422
+ static decrypt(payload: string, key?: KeyInput): string;
423
+ /**
424
+ * Encrypt through the isomorphic Web Crypto implementation.
425
+ *
426
+ * Produces the same payload format as {@link encrypt}; use it when the
427
+ * calling code is (or may become) shared with the browser.
428
+ *
429
+ * @param value
430
+ * @param key
431
+ * @param options
432
+ * @returns
433
+ */
434
+ static encryptAsync(value: string, key?: KeyInput, options?: CipherOptions): Promise<string>;
435
+ /**
436
+ * Decrypt through the isomorphic Web Crypto implementation.
437
+ *
438
+ * @param payload
439
+ * @param key
440
+ * @param options
441
+ * @returns
442
+ */
443
+ static decryptAsync(payload: string, key?: KeyInput, options?: CipherOptions): Promise<string>;
444
+ /**
445
+ * A cipher bound to the application key, for encrypting many values or raw
446
+ * bytes without re-deriving the key each time.
447
+ *
448
+ * @param key
449
+ * @returns
450
+ */
451
+ static cipher(key?: KeyInput): Promise<Cipher>;
452
+ /**
453
+ * The application key as it appears in the environment.
454
+ *
455
+ * Reads `APP_KEY`, falling back to the legacy `TWO_FACTOR_ENCRYPTION_KEY`
456
+ * variable. Override this in a subclass to source the key elsewhere.
457
+ *
458
+ * @returns
459
+ */
460
+ protected static secret(): string;
461
+ /**
462
+ * The 32 bytes of key material actually handed to the cipher: SHA-256 of
463
+ * the application key, or of an explicit override.
464
+ *
465
+ * The same bytes are reachable in the browser with
466
+ * `EncryptionKey.fromSecret(secret)`.
467
+ *
468
+ * @param key
469
+ * @returns
470
+ */
471
+ static material(key?: KeyInput): Uint8Array;
472
+ /**
473
+ * Generate a random base64url encryption key.
474
+ *
475
+ * @param length Key length in bytes, defaults to 32.
476
+ * @returns
477
+ */
478
+ static generateKey(length?: number): string;
479
+ /**
480
+ * Generate an end-to-end encryption identity. The public key is published,
481
+ * the private key stays with its owner.
482
+ *
483
+ * @returns
484
+ */
485
+ static generateKeyPair(): Promise<SerializedKeyPair>;
486
+ /**
487
+ * Stretch a user supplied password into a key with PBKDF2-HMAC-SHA256.
488
+ *
489
+ * @param password
490
+ * @param options
491
+ * @returns
492
+ */
493
+ static deriveKey(password: string, options?: DeriveOptions): Promise<DerivedKey>;
494
+ /**
495
+ * Constant time comparison of two keys.
496
+ *
497
+ * @param left
498
+ * @param right
499
+ * @returns
500
+ */
501
+ static compareKeys(left: KeyInput, right: KeyInput): Promise<boolean>;
502
+ /**
503
+ * A displayable digest of a key, safe to show to users or write to logs.
504
+ *
505
+ * @param key Defaults to the application key.
506
+ * @param options
507
+ * @returns
508
+ */
509
+ static fingerprint(key?: KeyInput, options?: FingerprintOptions): Promise<string>;
510
+ /**
511
+ * Open an end-to-end encrypted channel between a local private key and a
512
+ * peer's public key. Neither key, nor the secret they agree on, ever
513
+ * crosses the wire.
514
+ *
515
+ * @param privateKey
516
+ * @param peerPublicKey
517
+ * @param options
518
+ * @returns
519
+ */
520
+ static channel(privateKey: string | KeyPair, peerPublicKey: string | KeyPair, options?: ChannelOptions): Promise<SecureChannel>;
521
+ /**
522
+ * Encrypt a message to a public key without needing a sender identity.
523
+ *
524
+ * @param message
525
+ * @param recipientPublicKey
526
+ * @returns
527
+ */
528
+ static seal(message: string, recipientPublicKey: string | KeyPair): Promise<string>;
529
+ /**
530
+ * Open a payload produced by {@link seal}.
531
+ *
532
+ * @param payload
533
+ * @param recipientPrivateKey
534
+ * @returns
535
+ */
536
+ static open(payload: string, recipientPrivateKey: string | KeyPair): Promise<string>;
537
+ /**
538
+ * The safety number for a conversation between two public keys — show it to
539
+ * both participants so they can verify nobody swapped a key in transit.
540
+ *
541
+ * @param first
542
+ * @param second
543
+ * @param groups
544
+ * @returns
545
+ */
546
+ static safetyNumber(first: string, second: string, groups?: number): Promise<string>;
396
547
  }
397
548
  //#endregion
398
549
  //#region src/utils/hash.d.ts
@@ -428,7 +579,6 @@ declare class Hash {
428
579
  type AbstractModelConstructor<TModel = unknown> = abstract new (attributes?: Record<string, unknown>) => TModel;
429
580
  type ModelConstructor<TModel extends Model = Model> = AbstractModelConstructor<TModel> & Pick<ModelStatic<TModel>, keyof ModelStatic<TModel>>;
430
581
  interface ModelRegistry {}
431
- type ModelName = Extract<keyof ModelRegistry, string>;
432
582
  /**
433
583
  * Checks and asserts if target is a class
434
584
  *
@@ -474,15 +624,18 @@ declare const resolvePagination: (query: {
474
624
  maxPerPage?: number;
475
625
  }) => PaginationOptions;
476
626
  /**
477
- * Import an application model by name.
627
+ * Synchronously resolve an application model by name.
478
628
  *
479
- * Apps can augment `ModelRegistry` to make `getModel('User')` return `typeof User`.
480
- * Without a registry entry, pass the class type explicitly: `getModel<typeof User>('User')`.
629
+ * Registered models are returned first. If a model has not been registered yet,
630
+ * ArkORM loads it from the configured models paths, registers it, and returns
631
+ * the matching constructor.
481
632
  *
482
633
  * @param modelName
634
+ * @alias {@link getArkormxModel}
635
+ * @returns
483
636
  */
484
- declare function getModel<TName extends ModelName>(modelName: TName): Promise<ModelRegistry[TName]>;
485
- declare function getModel<TModel extends AbstractModelConstructor = ModelConstructor>(modelName: string): Promise<TModel>;
637
+ declare function getModel$1<TName extends RegisteredModelName>(modelName: TName): RegisteredModelClass<TName>;
638
+ declare function getModel$1<TModel extends RelatedModelClass = RelatedModelClass>(modelName: string): TModel;
486
639
  /**
487
640
  * Synchronously import an application model by name.
488
641
  *
@@ -490,9 +643,11 @@ declare function getModel<TModel extends AbstractModelConstructor = ModelConstru
490
643
  * Without a registry entry, pass the class type explicitly: `getModel<typeof User>('User')`.
491
644
  *
492
645
  * @param modelName
646
+ * @alias {@link getArkormxModel}
647
+ * @deprecated 0.17.27 - Use {@link getModel} or {@link getArkormxModel}
493
648
  */
494
- declare function getModelSync<TName extends ModelName>(modelName: TName): ModelRegistry[TName];
495
- declare function getModelSync<TModel extends AbstractModelConstructor = ModelConstructor>(modelName: string): TModel;
649
+ declare function getModelSync<TName extends RegisteredModelName>(modelName: TName): RegisteredModelClass<TName>;
650
+ declare function getModelSync<TModel extends RelatedModelClass = RelatedModelClass>(modelName: string): TModel;
496
651
  declare const initializeGlobalContext: ({ Request, Response, Session }?: {
497
652
  Request?: any;
498
653
  Response?: any;
@@ -525,4 +680,4 @@ declare const abortIf: <T>(boolean: T, message?: string, code?: number) => asser
525
680
  */
526
681
  declare const assertFound: <T>(value: T | null | undefined, message: string, code?: number) => asserts value is T;
527
682
  //#endregion
528
- export { GlobalConfig as A, LoggerLog as B, DotPath as C, EnvRegistry as D, EnvLookup as E, HookPos as F, PublishConfirmation as G, MergedConfig as H, HookPositions as I, PublishGroup as J, PublishEntry as K, HookRegistry as L, HookArgs as M, HookFor as N, EnvReturn as O, HookName as P, IHook as R, ConfigShape as S, EnvKey as T, PaginationOptions as U, LoggerParseSignature as V, Primitive as W, Logger as X, UnionToIntersection as Y, ArkstackErrorPayload as _, abortIf as a, Choices as b, getModelSync as c, normalizePositiveInteger as d, perPage as f, AppConfig as g, Encryption as h, abort as i, GlobalEnv as j, FileImporter as k, initializeGlobalContext as l, Hash as m, ModelConstructor as n, assertFound as o, resolvePagination as p, PublishFilter as q, ModelRegistry as r, getModel as s, AbstractModelConstructor as t, isClass as u, ArkstackErrorShape as v, DotPathValue as w, ConfigRegistry as x, Choice as y, LoggerChalk as z };
683
+ export { IHook as $, SerializedKeyPair as A, EnvKey as B, FingerprintOptions as C, NodeCipher as D, Keys as E, Choices as F, GlobalConfig as G, EnvRegistry as H, ConfigRegistry as I, HookFor as J, GlobalEnv as K, ConfigShape as L, ArkstackErrorPayload as M, ArkstackErrorShape as N, SealedBox as O, Choice as P, HookRegistry as Q, DotPath as R, EncryptionKey as S, KeyPair as T, EnvReturn as U, EnvLookup as V, FileImporter as W, HookPos as X, HookName as Y, HookPositions as Z, CipherOptions as _, abortIf as a, Primitive as at, DerivedKey as b, getModelSync as c, PublishFilter as ct, normalizePositiveInteger as d, Logger as dt, LoggerChalk as et, perPage as f, Cipher as g, ChannelOptions as h, abort as i, PaginationOptions as it, AppConfig as j, SecureChannel as k, initializeGlobalContext as l, PublishGroup as lt, Hash as m, ModelConstructor as n, LoggerParseSignature as nt, assertFound as o, PublishConfirmation as ot, resolvePagination as p, HookArgs as q, ModelRegistry as r, MergedConfig as rt, getModel$1 as s, PublishEntry as st, AbstractModelConstructor as t, LoggerLog as tt, isClass as u, UnionToIntersection as ut, Codec as v, KeyInput as w, Encryption as x, DeriveOptions as y, DotPathValue as z };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /// <reference path="./app.d.ts" />
2
- import { A as GlobalConfig, B as LoggerLog, C as DotPath, D as EnvRegistry, E as EnvLookup, F as HookPos, G as PublishConfirmation, H as MergedConfig, I as HookPositions, J as PublishGroup, K as PublishEntry, L as HookRegistry, M as HookArgs, N as HookFor, O as EnvReturn, P as HookName, R as IHook, S as ConfigShape, T as EnvKey, U as PaginationOptions, V as LoggerParseSignature, W as Primitive, X as Logger, Y as UnionToIntersection, _ as ArkstackErrorPayload, a as abortIf, b as Choices, c as getModelSync, d as normalizePositiveInteger, f as perPage, g as AppConfig, h as Encryption, i as abort, j as GlobalEnv, k as FileImporter, l as initializeGlobalContext, m as Hash, n as ModelConstructor, o as assertFound, p as resolvePagination, q as PublishFilter, r as ModelRegistry, s as getModel, t as AbstractModelConstructor, u as isClass, v as ArkstackErrorShape, w as DotPathValue, x as ConfigRegistry, y as Choice, z as LoggerChalk } from "./helpers-E_O2UDRn.js";
2
+ import { $ as IHook, A as SerializedKeyPair, B as EnvKey, C as FingerprintOptions, D as NodeCipher, E as Keys, F as Choices, G as GlobalConfig, H as EnvRegistry, I as ConfigRegistry, J as HookFor, K as GlobalEnv, L as ConfigShape, M as ArkstackErrorPayload, N as ArkstackErrorShape, O as SealedBox, P as Choice, Q as HookRegistry, R as DotPath, S as EncryptionKey, T as KeyPair, U as EnvReturn, V as EnvLookup, W as FileImporter, X as HookPos, Y as HookName, Z as HookPositions, _ as CipherOptions, a as abortIf, at as Primitive, b as DerivedKey, c as getModelSync, ct as PublishFilter, d as normalizePositiveInteger, dt as Logger, et as LoggerChalk, f as perPage, g as Cipher, h as ChannelOptions, i as abort, it as PaginationOptions, j as AppConfig, k as SecureChannel, l as initializeGlobalContext, lt as PublishGroup, m as Hash, n as ModelConstructor, nt as LoggerParseSignature, o as assertFound, ot as PublishConfirmation, p as resolvePagination, q as HookArgs, r as ModelRegistry, rt as MergedConfig, s as getModel, st as PublishEntry, t as AbstractModelConstructor, tt as LoggerLog, u as isClass, ut as UnionToIntersection, v as Codec, w as KeyInput, x as Encryption, y as DeriveOptions, z as DotPathValue } from "./helpers-Bn-0uv3R.js";
3
3
  import { Arkstack } from "@arkstack/contract";
4
4
  import { ForkOptions } from "node:child_process";
5
5
  import pino from "pino";
@@ -514,4 +514,4 @@ declare const devServer: (options: DevServerRunnerOptions) => {
514
514
  closeWatcher(): Promise<void>;
515
515
  };
516
516
  //#endregion
517
- export { AbstractModelConstructor, AppConfig, AppException, ArkstackErrorPayload, ArkstackErrorShape, CONFIG_KEY, Choice, Choices, ConfigLoader, ConfigRegistry, ConfigShape, DevServerRunnerOptions, DotPath, DotPathValue, Encryption, EnvKey, EnvLoader, EnvLookup, EnvRegistry, EnvReturn, ErrorHandler, Exception, FileImporter, GlobalConfig, GlobalEnv, Hash, Hook, HookArgs, HookFor, HookName, HookPos, HookPositions, HookRegistry, IHook, Logger, LoggerChalk, LoggerLog, LoggerParseSignature, MergedConfig, ModelConstructor, ModelRegistry, PaginationOptions, Primitive, PublishConfirmation, PublishEntry, PublishFilter, PublishGroup, Publisher, RequestException, TlsCredentials, UnionToIntersection, abort, abortIf, appKey, appUrl, assertFound, bindGracefulShutdown, bootWithDetectedPort, config, configLoader, createErrorPayload, devServer, devTlsCredentials, discoverCommands, env, envLoader, getErrorLogger, getModel, getModelSync, getPrimaryError, getValidationErrors, importFile, initializeGlobalContext, interopDefault, isClass, isModelNotFoundError, isValidationError, loadPrototypes, localNetworkAddress, logUnhandledError, nodeEnv, normalizePositiveInteger, normalizeStatusCode, outputDir, perPage, rebuildOutput, renderError, resolvePagination, resolveRuntimeDir, resolveRuntimeModule, serializeError, shouldHideStack, shouldLogError, toErrorShape, toOutputPath };
517
+ export { AbstractModelConstructor, AppConfig, AppException, ArkstackErrorPayload, ArkstackErrorShape, CONFIG_KEY, type ChannelOptions, Choice, Choices, Cipher, type CipherOptions, Codec, ConfigLoader, ConfigRegistry, ConfigShape, type DeriveOptions, type DerivedKey, DevServerRunnerOptions, DotPath, DotPathValue, Encryption, EncryptionKey, EnvKey, EnvLoader, EnvLookup, EnvRegistry, EnvReturn, ErrorHandler, Exception, FileImporter, type FingerprintOptions, GlobalConfig, GlobalEnv, Hash, Hook, HookArgs, HookFor, HookName, HookPos, HookPositions, HookRegistry, IHook, type KeyInput, KeyPair, Keys, Logger, LoggerChalk, LoggerLog, LoggerParseSignature, MergedConfig, ModelConstructor, ModelRegistry, NodeCipher, PaginationOptions, Primitive, PublishConfirmation, PublishEntry, PublishFilter, PublishGroup, Publisher, RequestException, SealedBox, SecureChannel, type SerializedKeyPair, TlsCredentials, UnionToIntersection, abort, abortIf, appKey, appUrl, assertFound, bindGracefulShutdown, bootWithDetectedPort, config, configLoader, createErrorPayload, devServer, devTlsCredentials, discoverCommands, env, envLoader, getErrorLogger, getModel, getModelSync, getPrimaryError, getValidationErrors, importFile, initializeGlobalContext, interopDefault, isClass, isModelNotFoundError, isValidationError, loadPrototypes, localNetworkAddress, logUnhandledError, nodeEnv, normalizePositiveInteger, normalizeStatusCode, outputDir, perPage, rebuildOutput, renderError, resolvePagination, resolveRuntimeDir, resolveRuntimeModule, serializeError, shouldHideStack, shouldLogError, toErrorShape, toOutputPath };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { _ as EnvLoader, a as env, c as nodeEnv, d as resolveRuntimeDir, f as resolveRuntimeModule, g as configLoader, h as ConfigLoader, i as discoverCommands, l as outputDir, m as CONFIG_KEY, n as appUrl, o as importFile, p as toOutputPath, r as config, s as interopDefault, t as appKey, u as rebuildOutput, v as envLoader } from "./system-Die_Dv5y.js";
2
- import { _ as RequestException, b as Hash, c as abortIf, d as getModelSync, f as initializeGlobalContext, g as resolvePagination, h as perPage, l as assertFound, m as normalizePositiveInteger, p as isClass, s as abort, u as getModel, v as AppException, x as Encryption, y as Exception } from "./utils-mymPEYd4.js";
2
+ import { C as Encryption, D as NodeCipher, E as Keys, O as SealedBox, S as Codec, T as KeyPair, _ as RequestException, b as Hash, c as abortIf, d as getModelSync, f as initializeGlobalContext, g as resolvePagination, h as perPage, k as SecureChannel, l as assertFound, m as normalizePositiveInteger, p as isClass, s as abort, u as getModel, v as AppException, w as EncryptionKey, x as Cipher, y as Exception } from "./utils-gKiLoXG9.js";
3
3
  import { Hook as Hook$1 } from "@arkstack/foundry";
4
4
  import { Arkstack } from "@arkstack/contract";
5
5
  import { str } from "@h3ravel/support";
@@ -660,4 +660,4 @@ const devServer = (options) => {
660
660
  };
661
661
  };
662
662
  //#endregion
663
- export { AppException, CONFIG_KEY, ConfigLoader, Encryption, EnvLoader, ErrorHandler, Exception, Hash, Hook, Logger, Publisher, RequestException, abort, abortIf, appKey, appUrl, assertFound, bindGracefulShutdown, bootWithDetectedPort, config, configLoader, createErrorPayload, devServer, devTlsCredentials, discoverCommands, env, envLoader, getErrorLogger, getModel, getModelSync, getPrimaryError, getValidationErrors, importFile, initializeGlobalContext, interopDefault, isClass, isModelNotFoundError, isValidationError, loadPrototypes, localNetworkAddress, logUnhandledError, nodeEnv, normalizePositiveInteger, normalizeStatusCode, outputDir, perPage, rebuildOutput, renderError, resolvePagination, resolveRuntimeDir, resolveRuntimeModule, serializeError, shouldHideStack, shouldLogError, toErrorShape, toOutputPath };
663
+ export { AppException, CONFIG_KEY, Cipher, Codec, ConfigLoader, Encryption, EncryptionKey, EnvLoader, ErrorHandler, Exception, Hash, Hook, KeyPair, Keys, Logger, NodeCipher, Publisher, RequestException, SealedBox, SecureChannel, abort, abortIf, appKey, appUrl, assertFound, bindGracefulShutdown, bootWithDetectedPort, config, configLoader, createErrorPayload, devServer, devTlsCredentials, discoverCommands, env, envLoader, getErrorLogger, getModel, getModelSync, getPrimaryError, getValidationErrors, importFile, initializeGlobalContext, interopDefault, isClass, isModelNotFoundError, isValidationError, loadPrototypes, localNetworkAddress, logUnhandledError, nodeEnv, normalizePositiveInteger, normalizeStatusCode, outputDir, perPage, rebuildOutput, renderError, resolvePagination, resolveRuntimeDir, resolveRuntimeModule, serializeError, shouldHideStack, shouldLogError, toErrorShape, toOutputPath };
@@ -1,4 +1,4 @@
1
- import { a as abortIf, c as getModelSync, d as normalizePositiveInteger, f as perPage, h as Encryption, i as abort, l as initializeGlobalContext, m as Hash, n as ModelConstructor, o as assertFound, p as resolvePagination, r as ModelRegistry, s as getModel, t as AbstractModelConstructor, u as isClass } from "../helpers-E_O2UDRn.js";
1
+ import { A as SerializedKeyPair, C as FingerprintOptions, D as NodeCipher, E as Keys, O as SealedBox, S as EncryptionKey, T as KeyPair, _ as CipherOptions, a as abortIf, b as DerivedKey, c as getModelSync, d as normalizePositiveInteger, f as perPage, g as Cipher, h as ChannelOptions, i as abort, k as SecureChannel, l as initializeGlobalContext, m as Hash, n as ModelConstructor, o as assertFound, p as resolvePagination, r as ModelRegistry, s as getModel, t as AbstractModelConstructor, u as isClass, v as Codec, w as KeyInput, x as Encryption, y as DeriveOptions } from "../helpers-Bn-0uv3R.js";
2
2
  import { Model } from "arkormx";
3
3
  //#region src/utils/traits.d.ts
4
4
  declare const crc32: (str: string) => number;
@@ -184,4 +184,4 @@ type Derived<T extends (Trait | TypeFactory<Trait> | Cons)> = T extends TypeFact
184
184
  */
185
185
  declare function uses<T extends (Trait | TypeFactory<Trait> | Cons)>(instance: unknown, trait: T): instance is Derived<T>;
186
186
  //#endregion
187
- export { AbstractModelConstructor, Derived, Encryption, Hash, ModelConstructor, ModelRegistry, Trait, abort, abortIf, assertFound, callTraitMethods, crc32, getModel, getModelSync, getTraitMethods, initializeGlobalContext, isClass, normalizePositiveInteger, perPage, resolvePagination, trait, use, uses };
187
+ export { AbstractModelConstructor, type ChannelOptions, Cipher, type CipherOptions, Codec, type DeriveOptions, Derived, type DerivedKey, Encryption, EncryptionKey, type FingerprintOptions, Hash, type KeyInput, KeyPair, Keys, ModelConstructor, ModelRegistry, NodeCipher, SealedBox, SecureChannel, type SerializedKeyPair, Trait, abort, abortIf, assertFound, callTraitMethods, crc32, getModel, getModelSync, getTraitMethods, initializeGlobalContext, isClass, normalizePositiveInteger, perPage, resolvePagination, trait, use, uses };
@@ -1,2 +1,2 @@
1
- import { a as use, b as Hash, c as abortIf, d as getModelSync, f as initializeGlobalContext, g as resolvePagination, h as perPage, i as trait, l as assertFound, m as normalizePositiveInteger, n as crc32, o as uses, p as isClass, r as getTraitMethods, s as abort, t as callTraitMethods, u as getModel, x as Encryption } from "../utils-mymPEYd4.js";
2
- export { Encryption, Hash, abort, abortIf, assertFound, callTraitMethods, crc32, getModel, getModelSync, getTraitMethods, initializeGlobalContext, isClass, normalizePositiveInteger, perPage, resolvePagination, trait, use, uses };
1
+ import { C as Encryption, D as NodeCipher, E as Keys, O as SealedBox, S as Codec, T as KeyPair, a as use, b as Hash, c as abortIf, d as getModelSync, f as initializeGlobalContext, g as resolvePagination, h as perPage, i as trait, k as SecureChannel, l as assertFound, m as normalizePositiveInteger, n as crc32, o as uses, p as isClass, r as getTraitMethods, s as abort, t as callTraitMethods, u as getModel, w as EncryptionKey, x as Cipher } from "../utils-gKiLoXG9.js";
2
+ export { Cipher, Codec, Encryption, EncryptionKey, Hash, KeyPair, Keys, NodeCipher, SealedBox, SecureChannel, abort, abortIf, assertFound, callTraitMethods, crc32, getModel, getModelSync, getTraitMethods, initializeGlobalContext, isClass, normalizePositiveInteger, perPage, resolvePagination, trait, use, uses };
@@ -1,34 +1,201 @@
1
- import { a as env, f as resolveRuntimeModule, o as importFile, t as appKey } from "./system-Die_Dv5y.js";
2
- import { createRequire } from "node:module";
3
- import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto";
4
- import { Arkstack } from "@arkstack/contract";
5
- import path from "node:path";
1
+ import { a as env, t as appKey } from "./system-Die_Dv5y.js";
2
+ import { Cipher, Codec, EncryptionKey, KeyPair, Keys, SealedBox, SecureChannel } from "@arkstack/encryption";
3
+ import { NodeCipher } from "@arkstack/encryption/node";
6
4
  import { Secret, TOTP } from "otpauth";
7
5
  import { compare, genSalt, hash } from "bcryptjs";
6
+ import { getModel } from "arkormx";
8
7
  //#region src/utils/encryption.ts
8
+ /**
9
+ * Application facing encryption, bound to the app key.
10
+ *
11
+ * This is a thin wrapper over `@arkstack/encryption`. `encrypt()` and
12
+ * `decrypt()` keep the synchronous signatures and the exact payload format
13
+ * they have always had — `<iv>:<authTag>:<ciphertext>`, AES-256-GCM under
14
+ * SHA-256 of `APP_KEY` — so existing ciphertexts and call sites are unaffected.
15
+ *
16
+ * Everything else is new surface: the asynchronous methods run on the Web
17
+ * Crypto API, which means a browser holding the same key (or the same key pair
18
+ * peer) can decrypt what the server wrote, and the server can decrypt what the
19
+ * browser wrote.
20
+ */
9
21
  var Encryption = class {
10
- static algorithm = "aes-256-gcm";
11
- static getKey() {
22
+ /**
23
+ * Encrypt a string with the application key.
24
+ *
25
+ * @param value
26
+ * @param key Override the application key for this call.
27
+ * @returns
28
+ */
29
+ static encrypt(value, key) {
30
+ return NodeCipher.encrypt(value, this.material(key));
31
+ }
32
+ /**
33
+ * Decrypt a payload produced by {@link encrypt}.
34
+ *
35
+ * @param payload
36
+ * @param key Override the application key for this call.
37
+ * @returns
38
+ */
39
+ static decrypt(payload, key) {
40
+ return NodeCipher.decrypt(payload, this.material(key));
41
+ }
42
+ /**
43
+ * Encrypt through the isomorphic Web Crypto implementation.
44
+ *
45
+ * Produces the same payload format as {@link encrypt}; use it when the
46
+ * calling code is (or may become) shared with the browser.
47
+ *
48
+ * @param value
49
+ * @param key
50
+ * @param options
51
+ * @returns
52
+ */
53
+ static async encryptAsync(value, key, options = {}) {
54
+ return await (await this.cipher(key)).encrypt(value, options);
55
+ }
56
+ /**
57
+ * Decrypt through the isomorphic Web Crypto implementation.
58
+ *
59
+ * @param payload
60
+ * @param key
61
+ * @param options
62
+ * @returns
63
+ */
64
+ static async decryptAsync(payload, key, options = {}) {
65
+ return await (await this.cipher(key)).decrypt(payload, options);
66
+ }
67
+ /**
68
+ * A cipher bound to the application key, for encrypting many values or raw
69
+ * bytes without re-deriving the key each time.
70
+ *
71
+ * @param key
72
+ * @returns
73
+ */
74
+ static async cipher(key) {
75
+ return key === void 0 ? new Cipher(new EncryptionKey(this.material())) : await Cipher.from(key);
76
+ }
77
+ /**
78
+ * The application key as it appears in the environment.
79
+ *
80
+ * Reads `APP_KEY`, falling back to the legacy `TWO_FACTOR_ENCRYPTION_KEY`
81
+ * variable. Override this in a subclass to source the key elsewhere.
82
+ *
83
+ * @returns
84
+ */
85
+ static secret() {
12
86
  const secret = appKey("TWO_FACTOR_ENCRYPTION_KEY");
13
- if (!secret) throw new Error("APP_KEY is required to use two-factor authentication. Run `ark key:generate`.");
14
- return createHash("sha256").update(secret).digest();
15
- }
16
- static encrypt(value) {
17
- const iv = randomBytes(12);
18
- const cipher = createCipheriv(this.algorithm, this.getKey(), iv);
19
- const ciphertext = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
20
- return [
21
- iv,
22
- cipher.getAuthTag(),
23
- ciphertext
24
- ].map((part) => part.toString("base64url")).join(":");
25
- }
26
- static decrypt(payload) {
27
- const [iv, authTag, ciphertext] = payload.split(":");
28
- if (!iv || !authTag || !ciphertext) throw new Error("Invalid encrypted payload format");
29
- const decipher = createDecipheriv(this.algorithm, this.getKey(), Buffer.from(iv, "base64url"));
30
- decipher.setAuthTag(Buffer.from(authTag, "base64url"));
31
- return Buffer.concat([decipher.update(Buffer.from(ciphertext, "base64url")), decipher.final()]).toString("utf8");
87
+ if (!secret) throw new Error("APP_KEY is required to use Encryption. Run `ark key:generate`.");
88
+ return secret;
89
+ }
90
+ /**
91
+ * The 32 bytes of key material actually handed to the cipher: SHA-256 of
92
+ * the application key, or of an explicit override.
93
+ *
94
+ * The same bytes are reachable in the browser with
95
+ * `EncryptionKey.fromSecret(secret)`.
96
+ *
97
+ * @param key
98
+ * @returns
99
+ */
100
+ static material(key) {
101
+ if (key === void 0) return NodeCipher.fromSecret(this.secret());
102
+ if (key instanceof EncryptionKey) return key.bytes;
103
+ if (key instanceof Uint8Array) return key;
104
+ if (typeof key === "string") return NodeCipher.resolve(key);
105
+ throw new TypeError("A CryptoKey cannot be used with the synchronous cipher; pass raw key material instead");
106
+ }
107
+ /**
108
+ * Generate a random base64url encryption key.
109
+ *
110
+ * @param length Key length in bytes, defaults to 32.
111
+ * @returns
112
+ */
113
+ static generateKey(length = 32) {
114
+ return Keys.generateString(length);
115
+ }
116
+ /**
117
+ * Generate an end-to-end encryption identity. The public key is published,
118
+ * the private key stays with its owner.
119
+ *
120
+ * @returns
121
+ */
122
+ static async generateKeyPair() {
123
+ return await Keys.generateSerializedPair();
124
+ }
125
+ /**
126
+ * Stretch a user supplied password into a key with PBKDF2-HMAC-SHA256.
127
+ *
128
+ * @param password
129
+ * @param options
130
+ * @returns
131
+ */
132
+ static async deriveKey(password, options = {}) {
133
+ return await Keys.derive(password, options);
134
+ }
135
+ /**
136
+ * Constant time comparison of two keys.
137
+ *
138
+ * @param left
139
+ * @param right
140
+ * @returns
141
+ */
142
+ static compareKeys(left, right) {
143
+ return Keys.matches(left, right);
144
+ }
145
+ /**
146
+ * A displayable digest of a key, safe to show to users or write to logs.
147
+ *
148
+ * @param key Defaults to the application key.
149
+ * @param options
150
+ * @returns
151
+ */
152
+ static async fingerprint(key, options = {}) {
153
+ return await Keys.fingerprint(key ?? this.material(), options);
154
+ }
155
+ /**
156
+ * Open an end-to-end encrypted channel between a local private key and a
157
+ * peer's public key. Neither key, nor the secret they agree on, ever
158
+ * crosses the wire.
159
+ *
160
+ * @param privateKey
161
+ * @param peerPublicKey
162
+ * @param options
163
+ * @returns
164
+ */
165
+ static async channel(privateKey, peerPublicKey, options = {}) {
166
+ return await SecureChannel.between(privateKey, peerPublicKey, options);
167
+ }
168
+ /**
169
+ * Encrypt a message to a public key without needing a sender identity.
170
+ *
171
+ * @param message
172
+ * @param recipientPublicKey
173
+ * @returns
174
+ */
175
+ static async seal(message, recipientPublicKey) {
176
+ return await SealedBox.seal(message, recipientPublicKey);
177
+ }
178
+ /**
179
+ * Open a payload produced by {@link seal}.
180
+ *
181
+ * @param payload
182
+ * @param recipientPrivateKey
183
+ * @returns
184
+ */
185
+ static async open(payload, recipientPrivateKey) {
186
+ return await SealedBox.open(payload, recipientPrivateKey);
187
+ }
188
+ /**
189
+ * The safety number for a conversation between two public keys — show it to
190
+ * both participants so they can verify nobody swapped a key in transit.
191
+ *
192
+ * @param first
193
+ * @param second
194
+ * @param groups
195
+ * @returns
196
+ */
197
+ static async safetyNumber(first, second, groups = 12) {
198
+ return await Keys.safetyNumber(first, second, groups);
32
199
  }
33
200
  };
34
201
  //#endregion
@@ -202,37 +369,11 @@ const resolvePagination = (query, defaults) => {
202
369
  perPage: perPage(query, defaults)
203
370
  };
204
371
  };
205
- async function getModel(modelName) {
206
- const resolveModelExport = (module, modelName) => {
207
- if (!isModelModule(module)) return module;
208
- return module.default ?? module[modelName] ?? module;
209
- };
210
- const isModelModule = (value) => typeof value === "object" && value !== null;
211
- const { getUserConfig } = await import("arkormx");
212
- const modelPath = getUserConfig().paths?.models || "./src/models";
213
- const sourcePath = path.join(path.isAbsolute(modelPath) ? modelPath : path.join(Arkstack.rootDir(), modelPath), modelName);
214
- const modulePath = resolveRuntimeModule(sourcePath);
215
- const model = resolveModelExport(await importFile(modulePath), path.basename(modelName, path.extname(modelName)));
216
- if (typeof model !== "function") throw new Error(`Model "${modelName}" not found`);
217
- return model;
372
+ function getModel$1(modelName) {
373
+ return getModel(modelName);
218
374
  }
219
- const isModelModule = (value) => typeof value === "object" && value !== null;
220
- const resolveModelExport = (module, modelName) => {
221
- if (!isModelModule(module)) return module;
222
- return module.default ?? module[modelName] ?? module;
223
- };
224
375
  function getModelSync(modelName) {
225
- const require = createRequire(import.meta.url);
226
- const { Arkorm, getUserConfig } = require("arkormx");
227
- const exportName = path.basename(modelName, path.extname(modelName));
228
- const registeredModel = Arkorm.getRegisteredModels().find((model) => model.name === exportName);
229
- if (registeredModel) return registeredModel;
230
- const modelPath = getUserConfig().paths?.models || "./src/models";
231
- const sourcePath = path.join(path.isAbsolute(modelPath) ? modelPath : path.join(Arkstack.rootDir(), modelPath), modelName);
232
- const module = require(resolveRuntimeModule(sourcePath));
233
- const model = resolveModelExport(module, exportName);
234
- if (typeof model !== "function") throw new Error(`Model "${modelName}" not found`);
235
- return model;
376
+ return getModel(modelName);
236
377
  }
237
378
  const initializeGlobalContext = async ({ Request, Response, Session } = {}) => {
238
379
  try {
@@ -492,4 +633,4 @@ function uses(instance, trait) {
492
633
  return false;
493
634
  }
494
635
  //#endregion
495
- export { RequestException as _, use as a, Hash as b, abortIf as c, getModelSync as d, initializeGlobalContext as f, resolvePagination as g, perPage as h, trait as i, assertFound as l, normalizePositiveInteger as m, crc32 as n, uses as o, isClass as p, getTraitMethods as r, abort as s, callTraitMethods as t, getModel as u, AppException as v, Encryption as x, Exception as y };
636
+ export { Encryption as C, NodeCipher as D, Keys as E, SealedBox as O, Codec as S, KeyPair as T, RequestException as _, use as a, Hash as b, abortIf as c, getModelSync as d, initializeGlobalContext as f, resolvePagination as g, perPage as h, trait as i, SecureChannel as k, assertFound as l, normalizePositiveInteger as m, crc32 as n, uses as o, isClass as p, getTraitMethods as r, abort as s, callTraitMethods as t, getModel$1 as u, AppException as v, EncryptionKey as w, Cipher as x, Exception as y };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arkstack/common",
3
- "version": "0.17.26",
3
+ "version": "0.18.1",
4
4
  "type": "module",
5
5
  "description": "Core utilities, primitives, and shared infrastructure for the Arkstack ecosystem.",
6
6
  "homepage": "https://arkstack.toneflix.net",
@@ -42,13 +42,14 @@
42
42
  "jiti": "^2.7.0",
43
43
  "otpauth": "^9.5.1",
44
44
  "pino": "^10.3.1",
45
- "selfsigned": "^2.4.1"
45
+ "selfsigned": "^2.4.1",
46
+ "@arkstack/encryption": "^0.18.1"
46
47
  },
47
48
  "peerDependencies": {
48
49
  "@h3ravel/support": "^2.2.7",
49
50
  "arkormx": "^2.12.8",
50
- "@arkstack/contract": "^0.17.26",
51
- "@arkstack/foundry": "^0.17.26"
51
+ "@arkstack/contract": "^0.18.1",
52
+ "@arkstack/foundry": "^0.18.1"
52
53
  },
53
54
  "optionalDependencies": {
54
55
  "@faker-js/faker": "^10.4.0"