@venizia/ignis-docs 0.2.1-0 → 0.2.1-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.
Files changed (145) hide show
  1. package/content/best-practices/architectural-patterns.md +3 -3
  2. package/content/best-practices/code-style-standards/naming-conventions.md +1 -1
  3. package/content/best-practices/contribution-workflow.md +2 -2
  4. package/content/best-practices/error-handling.md +94 -89
  5. package/content/best-practices/security-guidelines.md +5 -5
  6. package/content/extensions/components/api-reference.md +22 -21
  7. package/content/extensions/components/authentication/api.md +64 -28
  8. package/content/extensions/components/authentication/errors.md +19 -5
  9. package/content/extensions/components/authentication/index.md +19 -18
  10. package/content/extensions/components/authentication/usage.md +55 -30
  11. package/content/extensions/components/authorization/api.md +351 -84
  12. package/content/extensions/components/authorization/errors.md +51 -17
  13. package/content/extensions/components/authorization/getting-started.md +227 -0
  14. package/content/extensions/components/authorization/index.md +24 -16
  15. package/content/extensions/components/authorization/usage.md +45 -21
  16. package/content/extensions/components/health-check.md +15 -9
  17. package/content/extensions/components/index.md +24 -90
  18. package/content/extensions/components/mail/api.md +105 -54
  19. package/content/extensions/components/mail/errors.md +12 -10
  20. package/content/extensions/components/mail/index.md +32 -13
  21. package/content/extensions/components/mail/usage.md +26 -18
  22. package/content/extensions/components/request-tracker.md +18 -14
  23. package/content/extensions/components/socket-io/api.md +377 -882
  24. package/content/extensions/components/socket-io/errors.md +49 -51
  25. package/content/extensions/components/socket-io/index.md +72 -88
  26. package/content/extensions/components/socket-io/usage.md +107 -117
  27. package/content/extensions/components/static-asset/api.md +83 -31
  28. package/content/extensions/components/static-asset/errors.md +18 -7
  29. package/content/extensions/components/static-asset/index.md +18 -11
  30. package/content/extensions/components/static-asset/usage.md +11 -6
  31. package/content/extensions/components/template/index.md +3 -3
  32. package/content/extensions/components/websocket/api.md +58 -27
  33. package/content/extensions/components/websocket/errors.md +3 -3
  34. package/content/extensions/components/websocket/index.md +8 -7
  35. package/content/extensions/components/websocket/usage.md +21 -8
  36. package/content/extensions/helpers/cron/index.md +8 -7
  37. package/content/extensions/helpers/crypto/index.md +16 -8
  38. package/content/extensions/helpers/crypto/reference.md +96 -24
  39. package/content/extensions/helpers/env/index.md +14 -10
  40. package/content/extensions/helpers/error/index.md +99 -23
  41. package/content/extensions/helpers/index.md +61 -47
  42. package/content/extensions/helpers/inversion/index.md +23 -6
  43. package/content/extensions/helpers/inversion/reference.md +30 -22
  44. package/content/extensions/helpers/kafka/admin.md +3 -2
  45. package/content/extensions/helpers/kafka/compile-binary.md +69 -44
  46. package/content/extensions/helpers/kafka/consumer.md +24 -21
  47. package/content/extensions/helpers/kafka/examples.md +22 -234
  48. package/content/extensions/helpers/kafka/index.md +30 -62
  49. package/content/extensions/helpers/kafka/producer.md +31 -26
  50. package/content/extensions/helpers/kafka/schema-registry.md +19 -14
  51. package/content/extensions/helpers/logger/hf-logger.md +49 -22
  52. package/content/extensions/helpers/logger/index.md +37 -12
  53. package/content/extensions/helpers/logger/pino.md +31 -11
  54. package/content/extensions/helpers/logger/reference.md +243 -52
  55. package/content/extensions/helpers/network/api.md +65 -30
  56. package/content/extensions/helpers/network/index.md +32 -11
  57. package/content/extensions/helpers/queue/index.md +17 -6
  58. package/content/extensions/helpers/queue/reference.md +52 -25
  59. package/content/extensions/helpers/redis/index.md +29 -11
  60. package/content/extensions/helpers/redis/reference.md +85 -28
  61. package/content/extensions/helpers/secrets/index.md +82 -12
  62. package/content/extensions/helpers/socket-io/api.md +40 -21
  63. package/content/extensions/helpers/socket-io/index.md +26 -10
  64. package/content/extensions/helpers/storage/api.md +42 -24
  65. package/content/extensions/helpers/storage/index.md +10 -7
  66. package/content/extensions/helpers/types/index.md +20 -7
  67. package/content/extensions/helpers/types/reference.md +53 -14
  68. package/content/extensions/helpers/uid/index.md +166 -10
  69. package/content/extensions/helpers/websocket/api.md +52 -13
  70. package/content/extensions/helpers/websocket/index.md +7 -4
  71. package/content/extensions/helpers/worker-thread/index.md +9 -5
  72. package/content/extensions/helpers/worker-thread/reference.md +20 -20
  73. package/content/extensions/index.md +38 -39
  74. package/content/extensions/src-details/mcp-server.md +96 -548
  75. package/content/guides/core-concepts/persistent/datasources.md +2 -2
  76. package/content/guides/core-concepts/persistent/index.md +5 -1
  77. package/content/guides/core-concepts/persistent/pglite.md +218 -0
  78. package/content/guides/core-concepts/persistent/postgres-drivers.md +21 -15
  79. package/content/guides/core-concepts/persistent/search-meilisearch.md +22 -12
  80. package/content/guides/core-concepts/persistent/search-typesense.md +26 -14
  81. package/content/guides/core-concepts/persistent/sqlite.md +296 -0
  82. package/content/guides/core-concepts/persistent/transactions.md +12 -11
  83. package/content/guides/get-started/5-minute-quickstart.md +59 -206
  84. package/content/guides/get-started/philosophy.md +135 -670
  85. package/content/guides/get-started/setup.md +53 -74
  86. package/content/guides/migrations/redis-helpers-migration.md +4 -3
  87. package/content/guides/migrations/scoped-rbac-migration.md +5 -5
  88. package/content/guides/migrations/unified-connectors-migration.md +7 -6
  89. package/content/guides/tutorials/realtime-chat.md +1 -1
  90. package/content/references/base/application.md +63 -21
  91. package/content/references/base/components.md +3 -3
  92. package/content/references/base/connectors.md +14 -14
  93. package/content/references/base/controllers.md +12 -12
  94. package/content/references/base/datasources-reference.md +32 -31
  95. package/content/references/base/datasources.md +6 -6
  96. package/content/references/base/dependency-injection.md +12 -11
  97. package/content/references/base/filter-system/application-usage.md +54 -30
  98. package/content/references/base/filter-system/array-operators.md +24 -46
  99. package/content/references/base/filter-system/comparison-operators.md +47 -67
  100. package/content/references/base/filter-system/default-filter.md +59 -53
  101. package/content/references/base/filter-system/fields-order-pagination.md +92 -146
  102. package/content/references/base/filter-system/index.md +25 -13
  103. package/content/references/base/filter-system/json-filtering.md +45 -184
  104. package/content/references/base/filter-system/list-operators.md +23 -53
  105. package/content/references/base/filter-system/logical-operators.md +63 -121
  106. package/content/references/base/filter-system/null-operators.md +34 -104
  107. package/content/references/base/filter-system/pattern-matching.md +40 -55
  108. package/content/references/base/filter-system/quick-reference.md +86 -198
  109. package/content/references/base/filter-system/range-operators.md +18 -46
  110. package/content/references/base/filter-system/tips.md +6 -6
  111. package/content/references/base/filter-system/use-cases.md +33 -15
  112. package/content/references/base/grpc-controllers.md +53 -17
  113. package/content/references/base/index.md +5 -3
  114. package/content/references/base/middlewares.md +11 -10
  115. package/content/references/base/models-reference.md +17 -17
  116. package/content/references/base/models.md +4 -3
  117. package/content/references/base/providers.md +8 -8
  118. package/content/references/base/repositories/advanced.md +224 -326
  119. package/content/references/base/repositories/index.md +19 -6
  120. package/content/references/base/repositories/mixins.md +5 -5
  121. package/content/references/base/repositories/relations.md +160 -293
  122. package/content/references/base/repositories/soft-deletable.md +16 -6
  123. package/content/references/base/secrets.md +17 -13
  124. package/content/references/base/services.md +6 -4
  125. package/content/references/configuration/environment-variables.md +31 -23
  126. package/content/references/configuration/index.md +6 -4
  127. package/content/references/index.md +1 -1
  128. package/content/references/utilities/duration.md +85 -0
  129. package/content/references/utilities/index.md +5 -1
  130. package/content/references/utilities/jsx-reference.md +11 -11
  131. package/content/references/utilities/jsx.md +2 -2
  132. package/content/references/utilities/module.md +78 -25
  133. package/content/references/utilities/request.md +2 -1
  134. package/content/references/utilities/retry.md +139 -0
  135. package/content/references/utilities/schema.md +2 -2
  136. package/content/references/utilities/statuses-reference.md +4 -4
  137. package/content/references/utilities/statuses.md +5 -5
  138. package/dist/mcp-server/index.js +0 -0
  139. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
  140. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts.map +1 -1
  141. package/dist/mcp-server/tools/github/list-project-files.tool.js +3 -3
  142. package/dist/mcp-server/tools/github/search-code.tool.js +1 -1
  143. package/dist/mcp-server/tools/github/verify-dependencies.tool.js +1 -1
  144. package/dist/mcp-server/tools/github/view-source-file.tool.js +1 -1
  145. package/package.json +17 -16
@@ -42,11 +42,12 @@ export class Application extends BaseApplication {
42
42
 
43
43
  ## How it works
44
44
 
45
- - **Two-phase startup solves a timing problem.** `binding()` runs during `initialize()`, before any Bun server exists. It validates bindings and registers a post-start hook; the actual `WebSocketServerHelper` is only constructed once `executePostStartHooks()` runs, after `Bun.serve()` has produced a live server instance.
46
- - **Two bindings gate startup.** `REDIS_CONNECTION` (must be an `AbstractRedisHelper` instance) and `AUTHENTICATE_HANDLER` are required. `binding()` throws synchronously if either is missing or the wrong type - before the post-start hook is even registered.
45
+ - **Two-phase startup solves a timing problem.** `binding()` runs during `initialize()`, before any Bun server exists. It validates bindings and registers a post-start hook. The actual `WebSocketServerHelper` is only constructed once `executePostStartHooks()` runs, after `Bun.serve()` has produced a live server instance.
46
+ - **Two bindings gate startup.** `REDIS_CONNECTION` and `AUTHENTICATE_HANDLER` are both required. `REDIS_CONNECTION` must be an `AbstractRedisHelper` instance. `binding()` throws synchronously if either is missing or the wrong type - before the post-start hook is even registered.
47
47
  - **Runtime is checked first, fast.** `RuntimeModules.detect()` runs at the top of `binding()`. On Node.js it throws immediately, so a misconfigured app fails at startup, not on first connection.
48
48
  - **The instance appears only after start.** The post-start hook binds the configured helper to `WebSocketBindingKeys.WEBSOCKET_INSTANCE`. It does not exist during DI construction. Inject it lazily from a service or controller, never via `@inject` in a constructor.
49
- - **A custom `fetch` handler splits traffic.** After the post-start hook runs, `server.reload()` swaps in a handler that routes WebSocket upgrade requests (`GET <path>` with an `Upgrade: websocket` header) to Bun's native handler, and everything else to the existing Hono server unchanged.
49
+ - **A custom `fetch` handler splits traffic.** After the post-start hook runs, `server.reload()` swaps in a new handler. It routes WebSocket upgrade requests - `GET <path>` with an `Upgrade: websocket` header - to Bun's native handler.
50
+ - Everything else goes to the existing Hono server, unchanged.
50
51
 
51
52
  ## Common tasks
52
53
 
@@ -120,7 +121,7 @@ this.bind({ key: WebSocketBindingKeys.HANDSHAKE_HANDLER }).toValue(handshakeFn);
120
121
 
121
122
  **Files:**
122
123
 
123
- - [`packages/core/src/components/websocket/component.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/websocket/component.ts) - `WebSocketComponent`
124
- - [`packages/core/src/components/websocket/common/keys.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/websocket/common/keys.ts) - `WebSocketBindingKeys`
125
- - [`packages/core/src/components/websocket/common/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/websocket/common/types.ts) - `IServerOptions`, `DEFAULT_SERVER_OPTIONS`
126
- - [`packages/core/src/components/websocket/handlers/bun.handler.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/websocket/handlers/bun.handler.ts) - `createBunFetchHandler`
124
+ - [`packages/core-server/src/components/websocket/component.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/websocket/component.ts) - `WebSocketComponent`
125
+ - [`packages/core-server/src/components/websocket/common/keys.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/websocket/common/keys.ts) - `WebSocketBindingKeys`
126
+ - [`packages/core-server/src/components/websocket/common/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/websocket/common/types.ts) - `IServerOptions`, `DEFAULT_SERVER_OPTIONS`
127
+ - [`packages/core-server/src/components/websocket/handlers/bun.handler.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/websocket/handlers/bun.handler.ts) - `createBunFetchHandler`
@@ -61,11 +61,19 @@ export class NotificationService extends BaseService {
61
61
 
62
62
  - **Never `@inject` `WEBSOCKET_INSTANCE` in a constructor.** It is not bound yet at that point - the lazy getter is the only correct pattern.
63
63
  - **`sendToClient`/`sendToUser`/`sendToRoom`/`broadcast` are local-only.** They fan out to clients connected to this process. Cross-instance delivery goes through `send()` (Redis-backed) or `WebSocketEmitter` - see below.
64
- - **`send({ destination, payload })` resolves `destination` dynamically** against local clients, then local rooms, then falls back to publishing as a `ROOM` message on Redis. There is no `userId` destination in `send()` - use `sendToUser()` (local) or `WebSocketEmitter.toUser()` (cross-instance) to reach every session of a user.
64
+ - **`send({ destination, payload })` resolves `destination` dynamically**, in this order:
65
+
66
+ | Order | Checks |
67
+ |---|---|
68
+ | 1 | Local clients |
69
+ | 2 | Local rooms |
70
+ | 3 | Redis, publishing as a `ROOM` message (fallback) |
71
+
72
+ There is no `userId` destination in `send()`. To reach every session of a user, use `sendToUser()` (local) or `WebSocketEmitter.toUser()` (cross-instance).
65
73
 
66
74
  ## Send from a process with no WebSocket server
67
75
 
68
- `WebSocketEmitter` is a standalone, Redis-only publisher for background workers, cron jobs, other microservices, or CLI scripts - anything that needs to push a WebSocket message without running a server. It publishes the same `IRedisSocketMessage` envelope the server helper listens for, so every connected server instance delivers it to its local clients.
76
+ `WebSocketEmitter` is a standalone, Redis-only publisher. Use it from background workers, cron jobs, other microservices, or CLI scripts - anything that needs to push a WebSocket message without running a server. It publishes the same `IRedisSocketMessage` envelope the server helper listens for, so every connected server instance delivers it to its local clients.
69
77
 
70
78
  | Scenario | Use |
71
79
  |----------|-----|
@@ -90,9 +98,9 @@ await emitter.broadcast({ event: 'system:maintenance', data: { message: 'Schedul
90
98
  await emitter.shutdown(); // always release the Redis connection when done
91
99
  ```
92
100
 
93
- - **Fixed `serverId`.** The emitter always publishes with `serverId: 'emitter'`, which never matches a server's `crypto.randomUUID()` - so every server instance processes its messages, none self-dedup.
94
- - **One Redis client, not two.** The emitter only needs a pub client; the server helper needs pub + sub.
95
- - **`toUser()` is the recommended cross-instance path.** It publishes to `ws:user:{userId}`; every server subscribed via `psubscribe('ws:user:*')` receives it and calls `sendToUser()` locally, reaching every session of that user across all instances.
101
+ - **Fixed `serverId`.** The emitter always publishes with `serverId: 'emitter'`. That value never matches a server's `crypto.randomUUID()`, so every server instance processes its messages - none self-dedup.
102
+ - **One Redis client, not two.** The emitter only needs a pub client. The server helper needs both pub and sub.
103
+ - **`toUser()` is the recommended cross-instance path.** It publishes to `ws:user:{userId}`. Every server subscribed via `psubscribe('ws:user:*')` receives it and calls `sendToUser()` locally - reaching every session of that user across all instances.
96
104
 
97
105
  ## Read the wire protocol
98
106
 
@@ -216,8 +224,13 @@ Client Server
216
224
  | |-- clientConnectedFn()
217
225
  ```
218
226
 
219
- - **Two timeout phases, not one.** The initial `authTimeout` (5s default) starts on connect and closes with `4001` if no `authenticate` event arrives. Once `authenticate` is received, that timer is replaced with `authTimeout * 3` (15s default) to give the async `authenticateFn` (and `handshakeFn`) room to complete.
220
- - **A client's own ID becomes a room.** After authentication, `joinRoom({ clientId, room: clientId })` runs automatically - this is what lets `send({ destination: clientId })` or `sendToRoom({ room: clientId })` target one specific client.
227
+ - **Two timeout phases, not one:**
228
+
229
+ | Phase | Timeout | Starts when | On expiry |
230
+ |---|---|---|---|
231
+ | Initial | `authTimeout` (5s default) | Connection opens | Closes with `4001` if no `authenticate` event arrives |
232
+ | Post-authenticate | `authTimeout * 3` (15s default) | `authenticate` event is received | Gives the async `authenticateFn` (and `handshakeFn`, when encryption is required) room to complete |
233
+ - **A client's own ID becomes a room.** After authentication, `joinRoom({ clientId, room: clientId })` runs automatically. This is what lets `send({ destination: clientId })` or `sendToRoom({ room: clientId })` target one specific client.
221
234
  - **Encrypted clients skip Bun's native topics.** A client's own `clientId` topic is subscribed before auth (always). `BROADCAST_TOPIC` and rooms are subscribed after auth, but only when `!client.encrypted`. Encrypted clients rely entirely on the per-client `outboundTransformer` path.
222
235
 
223
236
  ## Understand the delivery strategy
@@ -232,7 +245,7 @@ The helper picks a delivery path per call, based on encryption and `exclude`:
232
245
  | `outboundTransformer` bound at all | **All** room/broadcast sends fall back to per-client iteration, even for non-encrypted clients in the same room - Bun's native pub/sub cannot selectively transform |
233
246
 
234
247
  > [!IMPORTANT]
235
- > Only bind `outboundTransformer` when you actually need per-client message transformation (e.g. per-client encryption). Binding it removes the fast path for every room/broadcast send, encrypted or not.
248
+ > Only bind `outboundTransformer` when you actually need per-client message transformation (for example per-client encryption). Binding it removes the fast path for every room/broadcast send, encrypted or not.
236
249
 
237
250
  ## See also
238
251
 
@@ -6,7 +6,7 @@ difficulty: beginner
6
6
 
7
7
  # Cron
8
8
 
9
- `CronHelper` wraps the `cron` package's `CronJob` with scoped logging and convenience methods for rescheduling and duplicating jobs.
9
+ `CronHelper` wraps the `cron` package's `CronJob`. It adds scoped logging, and convenience methods for rescheduling and duplicating jobs.
10
10
 
11
11
  ## In one example
12
12
 
@@ -27,11 +27,12 @@ const job = new CronHelper({
27
27
 
28
28
  ## How it works
29
29
 
30
- - **The constructor builds the job synchronously.** `buildInstance()` runs inside the constructor and creates a `CronJob` via `CronJob.from(...)`. An empty `cronTime` or a malformed cron expression throws immediately - `getError` never lets the object come back into your hands half-built.
31
- - **`start()` and `stop()` guard against a missing instance.** If `buildInstance()` never produced a `CronJob` (a prior `configure()` failure), `start()` logs `'Invalid cron instance to start cronjob!'` and returns without throwing. `stop()` is `async` because the underlying `CronJob.stop()` resolves only once an in-flight tick finishes - awaiting it prevents a replacement job from starting while the old handler is still running.
32
- - **`modifyCronTime()` reschedules in place.** It builds a new `CronTime`, calls `instance.setTime(...)`, and updates the stored `cronTime` - the same `CronJob` keeps running, it just fires on the new schedule.
33
- - **`duplicate()` clones configuration, not state.** The new instance shares `onTick`, `onCompleted`, `autoStart`, `tz`, and `errorHandler` with a different `cronTime`. It is fully independent - stopping or modifying one does not touch the other.
34
- - **The `instance` property is the raw `CronJob`.** Use it for anything the wrapper does not expose - `isActive`, `lastDate()`, `fireOnTick()` - from the [`cron`](https://github.com/kelektiv/node-cron) package (an optional peer dependency, `^4.3.3`).
30
+ - **The constructor builds the job right away.** `buildInstance()` runs inside the constructor and creates a `CronJob` via `CronJob.from(...)`. An empty `cronTime`, or a malformed cron expression, throws immediately - `getError` never lets you hold a half-built job.
31
+ - **`start()` checks for a built job first.** If a prior `configure()` call failed, `buildInstance()` never produced a `CronJob`. `start()` then logs a warning and returns - it does not throw.
32
+ - **`stop()` is `async` on purpose.** The underlying `CronJob.stop()` resolves only once an in-flight tick finishes. Awaiting it stops a replacement job from starting while the old handler still runs.
33
+ - **`modifyCronTime()` reschedules in place.** It builds a new `CronTime`, calls `instance.setTime(...)`, and updates the stored `cronTime`. The same `CronJob` keeps running - it just fires on the new schedule.
34
+ - **`duplicate()` clones configuration, not state.** The new instance shares `onTick`, `onCompleted`, `autoStart`, `tz`, and `errorHandler`, with a different `cronTime`. Stopping or modifying one instance never touches the other.
35
+ - **`instance` is the raw `CronJob`.** Reach it for anything the wrapper skips - `isActive`, `lastDate()`, `fireOnTick()`. It comes from the [`cron`](https://github.com/kelektiv/node-cron) package, an optional peer dependency (`^4.3.3`).
35
36
 
36
37
  **`ICronHelperOptions`**
37
38
 
@@ -61,7 +62,7 @@ job.start();
61
62
 
62
63
  ### Reschedule at runtime
63
64
 
64
- `modifyCronTime()` swaps the cron pattern without recreating the job. Set `shouldFireOnTick: true` to fire once immediately after the change (fire-and-forget - errors are logged, not thrown).
65
+ `modifyCronTime()` swaps the cron pattern without recreating the job. Set `shouldFireOnTick: true` to fire once immediately after the change. That fire is fire-and-forget - errors are logged, not thrown.
65
66
 
66
67
  ```typescript
67
68
  job.modifyCronTime({ cronTime: '0 */10 * * * *', shouldFireOnTick: true });
@@ -25,14 +25,20 @@ const decrypted = aes.decrypt({ message: encrypted, secret });
25
25
  // => 'This is a secret message.'
26
26
  ```
27
27
 
28
- `RSA` and `ECDH` follow the same `withAlgorithm()` factory + `encrypt`/`decrypt` shape - only the secret type and speed/message-size trade-offs differ.
28
+ `RSA` and `ECDH` follow the same `withAlgorithm()` factory + `encrypt`/`decrypt` shape. Only the secret type and the speed/message-size trade-offs differ.
29
29
 
30
30
  ## How it works
31
31
 
32
- - **One factory pattern.** Every algorithm class exposes a static `withAlgorithm()` that returns an instance; there is no public constructor to call directly.
33
- - **`AES` and `RSA` share `BaseCryptoAlgorithm`.** It adds `normalizeSecretKey()` (pads/truncates a string secret to the algorithm's key size) and `getAlgorithmKeySize()` (parses the bit size out of the algorithm name, e.g. `256` from `aes-256-gcm`).
34
- - **`ECDH` extends the neutral `AbstractCryptoAlgorithm` directly.** It uses `CryptoKey` objects from the Web Crypto API (`crypto.subtle`), not string secrets, so it skips the string-normalization helpers entirely.
35
- - **Options objects, throw-by-default.** Every `encrypt`/`decrypt` takes `{ message, secret, opts? }`. On internal error, each throws unless `opts.doThrow: false`, in which case the original input is returned unchanged.
32
+ - **One factory pattern.** Every algorithm class exposes a static `withAlgorithm()` that returns an instance. There is no public constructor to call directly.
33
+ - **`ECDH` extends the neutral `AbstractCryptoAlgorithm` directly.** It uses `CryptoKey` objects from the Web Crypto API (`crypto.subtle`), not string secrets. It skips the string-normalization helpers entirely.
34
+ - **Options objects, throw-by-default.** Every `encrypt`/`decrypt` takes `{ message, secret, opts? }`. On internal error, each throws by default. Pass `opts.doThrow: false` to get the original input back unchanged instead.
35
+
36
+ **`AES` and `RSA` share `BaseCryptoAlgorithm`**, which adds two helpers:
37
+
38
+ | Method | Does |
39
+ |---|---|
40
+ | `normalizeSecretKey()` | Pads or truncates a string secret to the algorithm's key size |
41
+ | `getAlgorithmKeySize()` | Parses the bit size out of the algorithm name - `256` from `aes-256-gcm` |
36
42
 
37
43
  **Class comparison**
38
44
 
@@ -42,7 +48,9 @@ const decrypted = aes.decrypt({ message: encrypted, secret });
42
48
  | `RSA` | `BaseCryptoAlgorithm` | `string` (base64 DER key) | No | Public-key encryption, small payloads |
43
49
  | `ECDH` | `AbstractCryptoAlgorithm` | `CryptoKey` | Yes | Session key exchange with forward secrecy |
44
50
 
45
- `AES` supports two modes selected at construction: `aes-256-cbc` (plain block cipher) and `aes-256-gcm` (authenticated - detects tampering). Everything on this page uses the default options; the [Full reference](/extensions/helpers/crypto/reference) covers every option, the ECDH key-exchange flow, `IECDHEncryptedPayload`, and the standalone `hash()` utility that lives alongside these classes in the same package.
51
+ `AES` supports two modes selected at construction: `aes-256-cbc` (plain block cipher) and `aes-256-gcm` (authenticated - detects tampering). Everything on this page uses the default options.
52
+
53
+ See the [Full reference](/extensions/helpers/crypto/reference) for every option, the ECDH key-exchange flow, `IECDHEncryptedPayload`, and the standalone `hash()` utility.
46
54
 
47
55
  ## Common tasks
48
56
 
@@ -66,7 +74,7 @@ const decrypted = aes.decryptFile({ absolutePath: '/path/to/config.json.enc', se
66
74
 
67
75
  ### Generate an RSA key pair and encrypt with it
68
76
 
69
- Keys are DER-encoded (`SPKI` public, `PKCS8` private); base64-encode them to pass as the `secret` string.
77
+ Keys are DER-encoded (`SPKI` public, `PKCS8` private). Base64-encode them to pass as the `secret` string.
70
78
 
71
79
  ```typescript
72
80
  import { RSA } from '@venizia/ignis-helpers';
@@ -81,7 +89,7 @@ const decrypted = rsa.decrypt({ message: encrypted, secret: privateKey.toString(
81
89
 
82
90
  ### Fail soft instead of throwing
83
91
 
84
- Pass `opts: { doThrow: false }` to get the original message back on error instead of an exception - useful when decryption failure should be a fallback path, not a crash.
92
+ Pass `opts: { doThrow: false }` to get the original message back on error instead of an exception. Use this when a decryption failure should be a fallback path, not a crash.
85
93
 
86
94
  ```typescript
87
95
  const result = rsa.encrypt({ message: 'test', secret: 'invalid-key', opts: { doThrow: false } });
@@ -80,16 +80,16 @@ interface ICryptoAlgorithm<
80
80
  }
81
81
  ```
82
82
 
83
- `AbstractCryptoAlgorithm` extends `BaseHelper` and declares `encrypt`/`decrypt` as abstract; it adds no behavior of its own. `BaseCryptoAlgorithm` is the concrete base for string-secret algorithms:
83
+ `AbstractCryptoAlgorithm` extends `BaseHelper` and declares `encrypt`/`decrypt` as abstract. It adds no behavior of its own. `BaseCryptoAlgorithm` is the concrete base for string-secret algorithms:
84
84
 
85
85
  | Member | Signature | Description |
86
86
  |--------|-----------|--------------|
87
87
  | constructor | `(opts: { scope: string; algorithm: AlgorithmType })` | Sets `this.algorithm`, calls `validateAlgorithmName` |
88
88
  | `validateAlgorithmName` | `(opts: { algorithm: AlgorithmType }) => void` | Throws if `algorithm` is empty/falsy |
89
- | `normalizeSecretKey` | `(opts: { secret: string; length: number; padEnd?: string }) => string` | Truncates to `length` or right-pads with `padEnd` (default `'0'` - the digit character, since `DEFAULT_PAD_END` is `(0x00).toString()`, not a null byte) |
89
+ | `normalizeSecretKey` | `(opts: { secret: string; length: number }) => Buffer` | Derives a `length`-byte key with PBKDF2-SHA256, 100,000 iterations. Results are memoised per secret |
90
90
  | `getAlgorithmKeySize` | `() => number` | Parses the bit size out of `this.algorithm` (e.g. `256` from `'aes-256-gcm'`), divides by 8 for byte length |
91
91
 
92
- `ECDH` extends `AbstractCryptoAlgorithm` directly - it does not inherit `normalizeSecretKey` or `getAlgorithmKeySize`, since its secrets are `CryptoKey` objects, not strings.
92
+ `ECDH` extends `AbstractCryptoAlgorithm` directly. It does not inherit `normalizeSecretKey` or `getAlgorithmKeySize` - its secrets are `CryptoKey` objects, not strings.
93
93
 
94
94
  ## AES
95
95
 
@@ -119,7 +119,16 @@ encrypt(opts: { message: string; secret: string; opts?: IAESExtraOptions }): str
119
119
  | `outputEncoding` | `crypto.Encoding` | `'base64'` | Encoding of the returned ciphertext |
120
120
  | `doThrow` | `boolean` | `true` | If `false`, returns the original `message` instead of throwing on error |
121
121
 
122
- The secret is normalized via `normalizeSecretKey` to the algorithm's key size (32 bytes for both modes) before being used as the cipher key. The output layout is `IV [+ GCM auth tag] + ciphertext`, concatenated and encoded with `outputEncoding`.
122
+ The secret is normalized via `normalizeSecretKey` to the algorithm's key size (32 bytes for both modes) before being used as the cipher key. The output is a self-describing envelope, concatenated and encoded with `outputEncoding`:
123
+
124
+ ```
125
+ [version(1)][idLen(1)][id(idLen)][iv(16)][authTag(16, gcm only)][ciphertext]
126
+ ```
127
+
128
+ The version byte is `0x01`. The key id is the entry `decrypt` looks up in a keyring - `'0'` when `secret` is a bare string.
129
+
130
+ > [!WARNING] This envelope is not the pre-PBKDF2 one
131
+ > Ciphertext written before this format started with the raw IV and derived its key by padding the secret. `decrypt` rejects it. Read that data with [`LegacyAES`](#legacyaes) instead.
123
132
 
124
133
  ```typescript
125
134
  import C from 'node:crypto';
@@ -139,20 +148,21 @@ const encrypted = aes.encrypt({
139
148
  ### `decrypt`
140
149
 
141
150
  ```typescript
142
- decrypt(opts: { message: string; secret: string; opts?: IAESExtraOptions }): string
151
+ decrypt(opts: { message: string; secret: TAESSecret; opts?: IAESDecryptOptions }): string
143
152
  ```
144
153
 
154
+ `decrypt` takes no `iv`. The envelope carries the one `encrypt` used, so passing another would be ignored - the option is absent from `IAESDecryptOptions` and supplying it is a compile error.
155
+
145
156
  | Option (`opts.opts`) | Type | Default | Description |
146
157
  |-----------------------|------|---------|-------------|
147
- | `iv` | `Buffer` | Extracted from the first 16 bytes of the decoded ciphertext | Initialization vector |
148
158
  | `inputEncoding` | `crypto.Encoding` | `'base64'` | Encoding of `message` |
149
159
  | `outputEncoding` | `crypto.Encoding` | `'utf-8'` | Encoding of the returned plaintext |
150
160
  | `doThrow` | `boolean` | `true` | If `false`, returns the original `message` instead of throwing on error |
151
161
 
152
- For `aes-256-gcm`, the next 16 bytes after the IV are read as the auth tag and passed to `setAuthTag` before the remaining bytes are treated as ciphertext.
162
+ For `aes-256-gcm`, the next 16 bytes after the IV are read as the auth tag. That tag is passed to `setAuthTag` before the remaining bytes are treated as ciphertext.
153
163
 
154
164
  > [!WARNING]
155
- > Decrypting `aes-256-gcm` ciphertext with an `aes-256-cbc` instance (or vice versa) throws `Unsupported state or unable to authenticate data` - the two modes produce incompatible byte layouts. Always encrypt and decrypt with the same algorithm mode.
165
+ > Decrypting `aes-256-gcm` ciphertext with an `aes-256-cbc` instance (or vice versa) throws `Unsupported state or unable to authenticate data`. The two modes produce incompatible byte layouts - always encrypt and decrypt with the same one.
156
166
 
157
167
  ### `encryptFile` / `decryptFile`
158
168
 
@@ -168,6 +178,66 @@ const encrypted = aes.encryptFile({ absolutePath: '/path/to/config.json', secret
168
178
  const decrypted = aes.decryptFile({ absolutePath: '/path/to/config.json.enc', secret: 'my-secret' });
169
179
  ```
170
180
 
181
+ ### Key rotation with a keyring
182
+
183
+ `secret` accepts a list as well as a string. Encryption always uses the first entry; decryption looks up the id stamped in the envelope.
184
+
185
+ ```typescript
186
+ const KEYRING = [
187
+ { id: '2', secret: process.env.APP_ENV_SECRET_V2! }, // current - everything new is written with this
188
+ { id: '1', secret: process.env.APP_ENV_SECRET_V1! }, // retired - still needed to read old rows
189
+ ];
190
+
191
+ const fresh = aes.encrypt({ message: 'payload', secret: KEYRING }); // tagged id '2'
192
+ const old = aes.decrypt({ message: rowFromLastYear, secret: KEYRING }); // resolved by its own id
193
+ ```
194
+
195
+ Rotating means prepending a new entry, not re-encrypting the estate. Drop an old entry only once nothing carries its id any more - `decrypt` throws `No key in keyring matches ciphertext key id` when it cannot resolve one, and a keyring entry with an empty `secret` is refused by name rather than failing later inside OpenSSL.
196
+
197
+ | `secret` shape | Encrypts with | Envelope key id |
198
+ |---|---|---|
199
+ | `'my-secret'` | that string | `'0'` |
200
+ | `[{ id, secret }, ...]` | the FIRST entry | that entry's `id` |
201
+
202
+ ### LegacyAES
203
+
204
+ `Source ->` [`packages/helpers/src/modules/crypto/algorithms/aes-legacy.algorithm.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/crypto/algorithms/aes-legacy.algorithm.ts)
205
+
206
+ `LegacyAES` reproduces the pre-PBKDF2 behaviour exactly: the key is the secret padded with `'0'` (or truncated) to the algorithm's key size, and the output is `IV [+ GCM auth tag] + ciphertext` with no version header.
207
+
208
+ Reach for it when you hold data written by an earlier IGNIS and do not want to re-encrypt it. The API mirrors `AES`, minus the keyring - `secret` is a plain string.
209
+
210
+ ```typescript
211
+ import { LegacyAES } from '@venizia/ignis-helpers';
212
+
213
+ const legacy = LegacyAES.withAlgorithm('aes-256-cbc');
214
+ const plaintext = legacy.decrypt({ message: rowWrittenBeforeTheUpgrade, secret: APPLICATION_SECRET });
215
+ ```
216
+
217
+ The two formats never cross-decrypt, by design. `AES` rejects a legacy envelope on its version byte; `LegacyAES` fails the auth tag on a new one. Nothing falls back silently in either direction.
218
+
219
+ ### IPayloadCipher - choosing the cipher a component uses
220
+
221
+ A component that encrypts on your behalf takes `IPayloadCipher`, the string-in/string-out slice both classes satisfy:
222
+
223
+ ```typescript
224
+ export interface IPayloadCipher {
225
+ encrypt(opts: { message: string; secret: string }): string;
226
+ decrypt(opts: { message: string; secret: string }): string;
227
+ }
228
+ ```
229
+
230
+ The bearer-token services accept it as `cipher`. An application holding tokens issued before the envelope change keeps them readable by handing over the legacy cipher instead of invalidating every session:
231
+
232
+ ```typescript
233
+ new JWSTokenService({
234
+ jwtSecret,
235
+ getTokenExpiresFn,
236
+ applicationSecret,
237
+ cipher: LegacyAES.withAlgorithm('aes-256-cbc'), // omit it and you get AES
238
+ });
239
+ ```
240
+
171
241
  ## RSA
172
242
 
173
243
  `Source ->` [`rsa.algorithm.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/crypto/algorithms/rsa.algorithm.ts)
@@ -188,7 +258,7 @@ generateDERKeyPair(opts?: { modulus: number }): { publicKey: Buffer; privateKey:
188
258
  |--------|------|---------|-------------|
189
259
  | `modulus` | `number` | `2048` | RSA modulus length in bits, passed to `crypto.generateKeyPairSync` |
190
260
 
191
- `publicKey` is exported as `{ type: 'spki', format: 'der' }`; `privateKey` as `{ type: 'pkcs8', format: 'der' }`. Both are raw `Buffer`s - base64-encode them (`.toString('base64')`) to pass as the `secret` string to `encrypt`/`decrypt`.
261
+ `publicKey` is exported as `{ type: 'spki', format: 'der' }`, `privateKey` as `{ type: 'pkcs8', format: 'der' }`. Both are raw `Buffer`s. Base64-encode them (`.toString('base64')`) to pass as the `secret` string to `encrypt`/`decrypt`.
192
262
 
193
263
  ### `encrypt`
194
264
 
@@ -245,13 +315,15 @@ const decrypted = rsa.decrypt({
245
315
 
246
316
  ```typescript
247
317
  const ecdh = ECDH.withAlgorithm(); // curve is always 'ecdh-p256'
248
- const ecdhCustom = ECDH.withAlgorithm({ hkdfInfo: 'my-app-session-keys' });
318
+ const ecdhCustom = ECDH.withAlgorithm({ algorithm: 'ecdh-p256', hkdfInfo: 'my-app-session-keys' });
249
319
  ```
250
320
 
251
- | Constructor option | Type | Default | Description |
252
- |---------------------|------|---------|-------------|
253
- | `algorithm` | `'ecdh-p256'` | `'ecdh-p256'` | Accepted but not branched on - the curve is always P-256 |
254
- | `hkdfInfo` | `string` | `'ignis-ecdh-p256-aes-256-gcm-v1'` | HKDF info string, UTF-8 encoded, used to isolate key derivation between application contexts |
321
+ The options argument is optional. Pass one and `algorithm` becomes required, even though the constructor ignores it and always sets `'ecdh-p256'`.
322
+
323
+ | Constructor option | Type | Required | Description |
324
+ |---------------------|------|----------|-------------|
325
+ | `algorithm` | `'ecdh-p256'` | Only when you pass an options object | Accepted but not branched on - the curve is always P-256 |
326
+ | `hkdfInfo` | `string` | No - defaults to `'ignis-ecdh-p256-aes-256-gcm-v1'` | HKDF info string, UTF-8 encoded, used to isolate key derivation between application contexts |
255
327
 
256
328
  Different `hkdfInfo` values produce **incompatible** derived keys from the same ECDH shared secret.
257
329
 
@@ -277,7 +349,7 @@ deriveAESKey(opts: {
277
349
  }): Promise<{ key: CryptoKey; salt: string }>
278
350
  ```
279
351
 
280
- Derives shared bits via ECDH (`deriveBits`, 256 bits), imports them as an HKDF key, then derives a non-extractable AES-256-GCM `CryptoKey` via HKDF-SHA256 using `salt` and the instance's `hkdfInfo`.
352
+ Derives shared bits via ECDH (`deriveBits`, 256 bits) and imports them as an HKDF key. It then derives a non-extractable AES-256-GCM `CryptoKey` via HKDF-SHA256, using `salt` and the instance's `hkdfInfo`.
281
353
 
282
354
  | Option | Type | Default | Description |
283
355
  |--------|------|---------|-------------|
@@ -286,7 +358,7 @@ Derives shared bits via ECDH (`deriveBits`, 256 bits), imports them as an HKDF k
286
358
  | `salt` | `string` | A random 32-byte salt is generated | Base64-encoded HKDF salt. Omit to generate a new random one |
287
359
 
288
360
  > [!IMPORTANT]
289
- > Both parties must use the **same salt** to derive matching keys. The initiator omits `salt` (a random one is generated and returned); the responder must pass that returned `salt` back into their own `deriveAESKey` call. If both sides generate their own salt, they derive different, non-matching keys.
361
+ > Both parties must use the **same salt** to derive matching keys. The initiator omits `salt` - a random one is generated and returned. The responder passes that returned `salt` back into their own `deriveAESKey` call. If both sides generate their own salt instead, the two keys never match.
290
362
 
291
363
  ### `encrypt` / `decrypt`
292
364
 
@@ -308,7 +380,7 @@ interface IECDHExtraOptions {
308
380
  }
309
381
  ```
310
382
 
311
- `opts.additionalData` (AAD) is authenticated but not encrypted - it binds the ciphertext to a context (channel ID, session ID) so the ciphertext cannot be replayed in a different context. Decrypt must supply the exact same `additionalData`; a mismatch (including an omitted value where one was used to encrypt) throws.
383
+ `opts.additionalData` (AAD) is authenticated but not encrypted. It binds the ciphertext to a context - a channel ID, a session ID - so it can't be replayed into a different one. Decrypt must supply the exact same `additionalData`. A mismatch throws, and so does omitting it when encrypt supplied one.
312
384
 
313
385
  ### Complete flow
314
386
 
@@ -356,7 +428,7 @@ const decrypted = await ecdh.decrypt({ message: encrypted, secret: bobKey });
356
428
 
357
429
  `Source ->` [`crypto.utility.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/utilities/crypto.utility.ts)
358
430
 
359
- `hash()` is a standalone function - not a class, not part of the `AES`/`RSA`/`ECDH` hierarchy - exported from `packages/helpers/src/utilities`, not `modules/crypto`. It is documented in full on the [Crypto Utility reference](/references/utilities/crypto).
431
+ `hash()` is a standalone function, not a class - it isn't part of the `AES`/`RSA`/`ECDH` hierarchy. It's exported from `packages/helpers/src/utilities`, not `modules/crypto`. It is documented in full on the [Crypto Utility reference](/references/utilities/crypto).
360
432
 
361
433
  ```typescript
362
434
  function hash(
@@ -398,7 +470,7 @@ const sha256Hmac = hash('some text', { algorithm: 'SHA256', secret: 'a-secret-ke
398
470
  | `generateDERKeyPair(opts?)` | `RSA` | `{ publicKey: Buffer; privateKey: Buffer }` | Generate a DER-format key pair |
399
471
  | `encrypt(opts)` | `RSA` | `string` | Encrypt with a public key |
400
472
  | `decrypt(opts)` | `RSA` | `string` | Decrypt with a private key |
401
- | `ECDH.withAlgorithm(opts?)` | `ECDH` | `ECDH` | Create an ECDH instance with optional `hkdfInfo` |
473
+ | `ECDH.withAlgorithm(opts?)` | `ECDH` | `ECDH` | Create an ECDH instance. Passing options requires `algorithm` alongside `hkdfInfo` |
402
474
  | `generateKeyPair()` | `ECDH` | `Promise<{ keyPair: CryptoKeyPair; publicKeyB64: string }>` | Generate a P-256 key pair |
403
475
  | `importPublicKey(opts)` | `ECDH` | `Promise<CryptoKey>` | Import a peer's base64 public key |
404
476
  | `deriveAESKey(opts)` | `ECDH` | `Promise<{ key: CryptoKey; salt: string }>` | Derive an AES-256-GCM key via HKDF |
@@ -422,13 +494,13 @@ const ecdh = ECDH.withAlgorithm(); // no parameter needed
422
494
 
423
495
  ### "[ECDH.fromBase64] Invalid base64 input"
424
496
 
425
- **Cause:** A value passed to an ECDH method (public key, salt, IV, or ciphertext) is not valid base64 - length not divisible by 4, or characters outside `A-Za-z0-9+/=`.
497
+ **Cause:** A value passed to an ECDH method - a public key, salt, IV, or ciphertext - is not valid base64. Its length isn't divisible by 4, or it has characters outside `A-Za-z0-9+/=`.
426
498
 
427
- **Fix:** Pass base64 strings through exactly as produced by the methods that generated them (`publicKeyB64`, `salt`, `iv`, `ct`) - do not trim, re-encode, or modify them.
499
+ **Fix:** Pass base64 strings through exactly as produced by the methods that generated them (`publicKeyB64`, `salt`, `iv`, `ct`). Do not trim, re-encode, or modify them.
428
500
 
429
501
  ### "Unsupported state or unable to authenticate data"
430
502
 
431
- **Cause:** The ciphertext or auth tag was modified in transit, or you decrypted `aes-256-gcm` ciphertext with a `aes-256-cbc` instance (or vice versa) - the two modes produce incompatible byte layouts.
503
+ **Cause:** Either the ciphertext or auth tag was modified in transit, or encrypt and decrypt used different algorithm modes. The two modes produce incompatible byte layouts.
432
504
 
433
505
  **Fix:** Use the same algorithm mode for both encrypt and decrypt.
434
506
 
@@ -436,11 +508,11 @@ const ecdh = ECDH.withAlgorithm(); // no parameter needed
436
508
 
437
509
  **Cause:** Each `deriveAESKey` call without a `salt` generates a new random 32-byte salt. If both sides generate their own, they derive different AES keys.
438
510
 
439
- **Fix:** The initiator calls `deriveAESKey` without `salt` and sends the returned `salt` to the responder; the responder passes that exact `salt` into their own `deriveAESKey` call.
511
+ **Fix:** The initiator calls `deriveAESKey` without `salt` and sends the returned `salt` to the responder. The responder passes that exact `salt` into their own `deriveAESKey` call.
440
512
 
441
513
  ### SHA256 hash returns the original text instead of a hash
442
514
 
443
- **Cause:** `hash()` with `algorithm: 'SHA256'` requires `secret`; when `secret` is `undefined` it short-circuits and returns `text` unchanged.
515
+ **Cause:** `hash()` with `algorithm: 'SHA256'` requires `secret`. When `secret` is `undefined`, it short-circuits and returns `text` unchanged.
444
516
 
445
517
  **Fix:**
446
518
 
@@ -6,7 +6,7 @@ difficulty: beginner
6
6
 
7
7
  # Environment
8
8
 
9
- `applicationEnvironment` is a singleton that filters `process.env` down to your app's prefix and gives typed access to it; `Environment` reads the current deployment stage from `NODE_ENV`.
9
+ `applicationEnvironment` is a singleton that filters `process.env` down to your app's prefix, and gives typed access to it. `Environment` reads the current deployment stage from `NODE_ENV`.
10
10
 
11
11
  ## In one example
12
12
 
@@ -17,14 +17,18 @@ const jwtSecret = applicationEnvironment.get<string>('APP_ENV_JWT_SECRET');
17
17
  const timeout = applicationEnvironment.get<number>('APP_ENV_TIMEOUT', { defaultValue: 5000 });
18
18
  ```
19
19
 
20
- The singleton is created once at module load, reading only keys that start with `APP_ENV` (the default prefix) from `process.env`. `Envs` is an exported alias for the same instance.
20
+ The singleton is created once at module load. It reads only keys that start with `APP_ENV` (the default prefix) from `process.env`. `Envs` is an exported alias for the same instance.
21
21
 
22
22
  ## How it works
23
23
 
24
- - **Construction filters by prefix.** `new ApplicationEnvironment({ prefix, envs })` copies only the keys of `envs` that start with `prefix` into an internal map - everything else is invisible to `get()`. The default singleton uses `process.env.APPLICATION_ENV_PREFIX ?? 'APP_ENV'` and `process.env`.
25
- - **`get()` takes an options object, not a positional default.** The signature is `get<ReturnType, BeforeTransformType = unknown>(key, opts?: { defaultValue?, transform? })`. Without `transform`, it returns the raw value (still a `string`) or `defaultValue` when the key is missing. With `transform`, it calls `transform(rawValue)` and falls back to `defaultValue` if that returns `undefined` or `null`.
26
- - **`get<T>()` is a type cast without `transform`, not a runtime conversion.** Every `process.env` value is a `string`; asking for `get<number>('APP_ENV_PORT')` still returns a string at runtime unless you pass `transform: Number`.
27
- - **Stage detection is separate from the singleton.** `Environment.current` reads `process.env.NODE_ENV` directly (falling back to `'development'` when unset); `Environment.is({ name })` compares against it. `ApplicationEnvironment.isDevelopment()` is narrower - it checks `NODE_ENV === 'development'` exactly, so the `'dev'` alias returns `false` there even though it counts as a development stage everywhere else.
24
+ - **Construction filters by prefix.** `new ApplicationEnvironment({ prefix, envs })` copies only the keys of `envs` that start with `prefix` into an internal map. Everything else stays invisible to `get()`. The default singleton uses `process.env.APPLICATION_ENV_PREFIX ?? 'APP_ENV'` and `process.env`.
25
+ - **`get()` takes an options object, not a positional default.** The signature is `get<ReturnType, BeforeTransformType = unknown>(key, opts?: { defaultValue?, transform? })`.
26
+ - **Without `transform`,** `get()` returns the raw value - still a `string` - or `defaultValue` when the key is missing.
27
+ - **With `transform`,** `get()` calls `transform(rawValue)`. It falls back to `defaultValue` only if that call returns `undefined` or `null`.
28
+ - **`get<T>()` is a type cast, not a runtime conversion, unless you pass `transform`.** Every `process.env` value is a `string`. Asking for `get<number>('APP_ENV_PORT')` still returns a string at runtime, unless you also pass `transform: Number`.
29
+ - **Stage detection is separate from the singleton.** `Environment.current` reads `process.env.NODE_ENV` directly. It falls back to `'development'` when `NODE_ENV` is unset.
30
+ - **`Environment.is({ name })` compares a name against `Environment.current`.**
31
+ - **`ApplicationEnvironment.isDevelopment()` is narrower.** It checks `NODE_ENV === 'development'` exactly. The `'dev'` alias fails that check, even though `dev` counts as a development stage everywhere else in IGNIS.
28
32
 
29
33
  **Deployment stages** (`Environment.*`)
30
34
 
@@ -41,7 +45,7 @@ The singleton is created once at module load, reading only keys that start with
41
45
  | `STAGING` | `'staging'` | no |
42
46
  | `PRODUCTION` | `'production'` | no |
43
47
 
44
- All ten stages are in `Environment.COMMON_ENVS`, which the Logger uses to decide whether `DEBUG=true` is honored. The five marked above are `Environment.DEVELOPMENT_ENVS` - the set IGNIS's error handler consults to decide whether a response may carry a stack trace or a raw driver message. The rule is fail-closed: `alpha`, `beta`, `uat`, `staging`, a typo'd name, and an unset `NODE_ENV` are all sanitized as production.
48
+ All ten stages are in `Environment.COMMON_ENVS`, which the Logger uses to decide whether `DEBUG=true` is honored. The five marked above are `Environment.DEVELOPMENT_ENVS`. IGNIS's error handler consults this set to decide whether a response may carry a stack trace or a raw driver message. The rule is fail-closed: `alpha`, `beta`, `uat`, `staging`, a typo'd name, and an unset `NODE_ENV` are all sanitized as production.
45
49
 
46
50
  ## Common tasks
47
51
 
@@ -66,7 +70,7 @@ const timeout = applicationEnvironment.get<number>('APP_ENV_TIMEOUT', {
66
70
 
67
71
  ### Set or merge variables at runtime
68
72
 
69
- `set()` writes a single key; `merge()` overwrites several at once - both bypass the prefix filter (they write directly, no `startsWith` check).
73
+ `set()` writes a single key. `merge()` overwrites several keys at once. Both bypass the prefix filter - they write directly, with no `startsWith` check.
70
74
 
71
75
  ```typescript
72
76
  applicationEnvironment.set('APP_ENV_FEATURE_FLAG', 'enabled');
@@ -85,7 +89,7 @@ if (Environment.is({ name: Environment.STAGING })) {
85
89
 
86
90
  ### Use a custom prefix
87
91
 
88
- Set `APPLICATION_ENV_PREFIX` before the first import of `@venizia/ignis-helpers` - the singleton is constructed at module load, so a later change has no effect on it.
92
+ Set `APPLICATION_ENV_PREFIX` before the first import of `@venizia/ignis-helpers`. The singleton is constructed at module load, so a later change has no effect on it.
89
93
 
90
94
  ```
91
95
  APPLICATION_ENV_PREFIX=MY_APP_ENV
@@ -100,7 +104,7 @@ const allKeys = applicationEnvironment.keys();
100
104
  ```
101
105
 
102
106
  > [!TIP]
103
- > `BaseApplication` validates every prefixed key at startup and throws on an empty value unless `ALLOW_EMPTY_ENV_VALUE` is truthy - see [Application](/guides/core-concepts/application/).
107
+ > `BaseApplication` validates every prefixed key at startup and throws on an empty value, unless `ALLOW_EMPTY_ENV_VALUE` is truthy - see [Application](/guides/core-concepts/application/).
104
108
 
105
109
  ## See also
106
110