@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.
- package/content/best-practices/architectural-patterns.md +3 -3
- package/content/best-practices/code-style-standards/naming-conventions.md +1 -1
- package/content/best-practices/contribution-workflow.md +2 -2
- package/content/best-practices/error-handling.md +94 -89
- package/content/best-practices/security-guidelines.md +5 -5
- package/content/extensions/components/api-reference.md +22 -21
- package/content/extensions/components/authentication/api.md +64 -28
- package/content/extensions/components/authentication/errors.md +19 -5
- package/content/extensions/components/authentication/index.md +19 -18
- package/content/extensions/components/authentication/usage.md +55 -30
- package/content/extensions/components/authorization/api.md +351 -84
- package/content/extensions/components/authorization/errors.md +51 -17
- package/content/extensions/components/authorization/getting-started.md +227 -0
- package/content/extensions/components/authorization/index.md +24 -16
- package/content/extensions/components/authorization/usage.md +45 -21
- package/content/extensions/components/health-check.md +15 -9
- package/content/extensions/components/index.md +24 -90
- package/content/extensions/components/mail/api.md +105 -54
- package/content/extensions/components/mail/errors.md +12 -10
- package/content/extensions/components/mail/index.md +32 -13
- package/content/extensions/components/mail/usage.md +26 -18
- package/content/extensions/components/request-tracker.md +18 -14
- package/content/extensions/components/socket-io/api.md +377 -882
- package/content/extensions/components/socket-io/errors.md +49 -51
- package/content/extensions/components/socket-io/index.md +72 -88
- package/content/extensions/components/socket-io/usage.md +107 -117
- package/content/extensions/components/static-asset/api.md +83 -31
- package/content/extensions/components/static-asset/errors.md +18 -7
- package/content/extensions/components/static-asset/index.md +18 -11
- package/content/extensions/components/static-asset/usage.md +11 -6
- package/content/extensions/components/template/index.md +3 -3
- package/content/extensions/components/websocket/api.md +58 -27
- package/content/extensions/components/websocket/errors.md +3 -3
- package/content/extensions/components/websocket/index.md +8 -7
- package/content/extensions/components/websocket/usage.md +21 -8
- package/content/extensions/helpers/cron/index.md +8 -7
- package/content/extensions/helpers/crypto/index.md +16 -8
- package/content/extensions/helpers/crypto/reference.md +96 -24
- package/content/extensions/helpers/env/index.md +14 -10
- package/content/extensions/helpers/error/index.md +99 -23
- package/content/extensions/helpers/index.md +61 -47
- package/content/extensions/helpers/inversion/index.md +23 -6
- package/content/extensions/helpers/inversion/reference.md +30 -22
- package/content/extensions/helpers/kafka/admin.md +3 -2
- package/content/extensions/helpers/kafka/compile-binary.md +69 -44
- package/content/extensions/helpers/kafka/consumer.md +24 -21
- package/content/extensions/helpers/kafka/examples.md +22 -234
- package/content/extensions/helpers/kafka/index.md +30 -62
- package/content/extensions/helpers/kafka/producer.md +31 -26
- package/content/extensions/helpers/kafka/schema-registry.md +19 -14
- package/content/extensions/helpers/logger/hf-logger.md +49 -22
- package/content/extensions/helpers/logger/index.md +37 -12
- package/content/extensions/helpers/logger/pino.md +31 -11
- package/content/extensions/helpers/logger/reference.md +243 -52
- package/content/extensions/helpers/network/api.md +65 -30
- package/content/extensions/helpers/network/index.md +32 -11
- package/content/extensions/helpers/queue/index.md +17 -6
- package/content/extensions/helpers/queue/reference.md +52 -25
- package/content/extensions/helpers/redis/index.md +29 -11
- package/content/extensions/helpers/redis/reference.md +85 -28
- package/content/extensions/helpers/secrets/index.md +82 -12
- package/content/extensions/helpers/socket-io/api.md +40 -21
- package/content/extensions/helpers/socket-io/index.md +26 -10
- package/content/extensions/helpers/storage/api.md +42 -24
- package/content/extensions/helpers/storage/index.md +10 -7
- package/content/extensions/helpers/types/index.md +20 -7
- package/content/extensions/helpers/types/reference.md +53 -14
- package/content/extensions/helpers/uid/index.md +166 -10
- package/content/extensions/helpers/websocket/api.md +52 -13
- package/content/extensions/helpers/websocket/index.md +7 -4
- package/content/extensions/helpers/worker-thread/index.md +9 -5
- package/content/extensions/helpers/worker-thread/reference.md +20 -20
- package/content/extensions/index.md +38 -39
- package/content/extensions/src-details/mcp-server.md +96 -548
- package/content/guides/core-concepts/persistent/datasources.md +2 -2
- package/content/guides/core-concepts/persistent/index.md +5 -1
- package/content/guides/core-concepts/persistent/pglite.md +218 -0
- package/content/guides/core-concepts/persistent/postgres-drivers.md +21 -15
- package/content/guides/core-concepts/persistent/search-meilisearch.md +22 -12
- package/content/guides/core-concepts/persistent/search-typesense.md +26 -14
- package/content/guides/core-concepts/persistent/sqlite.md +296 -0
- package/content/guides/core-concepts/persistent/transactions.md +12 -11
- package/content/guides/get-started/5-minute-quickstart.md +59 -206
- package/content/guides/get-started/philosophy.md +135 -670
- package/content/guides/get-started/setup.md +53 -74
- package/content/guides/migrations/redis-helpers-migration.md +4 -3
- package/content/guides/migrations/scoped-rbac-migration.md +5 -5
- package/content/guides/migrations/unified-connectors-migration.md +7 -6
- package/content/guides/tutorials/realtime-chat.md +1 -1
- package/content/references/base/application.md +63 -21
- package/content/references/base/components.md +3 -3
- package/content/references/base/connectors.md +14 -14
- package/content/references/base/controllers.md +12 -12
- package/content/references/base/datasources-reference.md +32 -31
- package/content/references/base/datasources.md +6 -6
- package/content/references/base/dependency-injection.md +12 -11
- package/content/references/base/filter-system/application-usage.md +54 -30
- package/content/references/base/filter-system/array-operators.md +24 -46
- package/content/references/base/filter-system/comparison-operators.md +47 -67
- package/content/references/base/filter-system/default-filter.md +59 -53
- package/content/references/base/filter-system/fields-order-pagination.md +92 -146
- package/content/references/base/filter-system/index.md +25 -13
- package/content/references/base/filter-system/json-filtering.md +45 -184
- package/content/references/base/filter-system/list-operators.md +23 -53
- package/content/references/base/filter-system/logical-operators.md +63 -121
- package/content/references/base/filter-system/null-operators.md +34 -104
- package/content/references/base/filter-system/pattern-matching.md +40 -55
- package/content/references/base/filter-system/quick-reference.md +86 -198
- package/content/references/base/filter-system/range-operators.md +18 -46
- package/content/references/base/filter-system/tips.md +6 -6
- package/content/references/base/filter-system/use-cases.md +33 -15
- package/content/references/base/grpc-controllers.md +53 -17
- package/content/references/base/index.md +5 -3
- package/content/references/base/middlewares.md +11 -10
- package/content/references/base/models-reference.md +17 -17
- package/content/references/base/models.md +4 -3
- package/content/references/base/providers.md +8 -8
- package/content/references/base/repositories/advanced.md +224 -326
- package/content/references/base/repositories/index.md +19 -6
- package/content/references/base/repositories/mixins.md +5 -5
- package/content/references/base/repositories/relations.md +160 -293
- package/content/references/base/repositories/soft-deletable.md +16 -6
- package/content/references/base/secrets.md +17 -13
- package/content/references/base/services.md +6 -4
- package/content/references/configuration/environment-variables.md +31 -23
- package/content/references/configuration/index.md +6 -4
- package/content/references/index.md +1 -1
- package/content/references/utilities/duration.md +85 -0
- package/content/references/utilities/index.md +5 -1
- package/content/references/utilities/jsx-reference.md +11 -11
- package/content/references/utilities/jsx.md +2 -2
- package/content/references/utilities/module.md +78 -25
- package/content/references/utilities/request.md +2 -1
- package/content/references/utilities/retry.md +139 -0
- package/content/references/utilities/schema.md +2 -2
- package/content/references/utilities/statuses-reference.md +4 -4
- package/content/references/utilities/statuses.md +5 -5
- package/dist/mcp-server/index.js +0 -0
- package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
- package/dist/mcp-server/tools/github/list-project-files.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/github/list-project-files.tool.js +3 -3
- package/dist/mcp-server/tools/github/search-code.tool.js +1 -1
- package/dist/mcp-server/tools/github/verify-dependencies.tool.js +1 -1
- package/dist/mcp-server/tools/github/view-source-file.tool.js +1 -1
- 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
|
|
46
|
-
- **Two bindings gate startup.** `REDIS_CONNECTION`
|
|
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
|
|
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
|
|
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
|
|
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'
|
|
94
|
-
- **One Redis client, not two.** The emitter only needs a pub client
|
|
95
|
-
- **`toUser()` is the recommended cross-instance path.** It publishes to `ws:user:{userId}
|
|
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
|
|
220
|
-
|
|
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 (
|
|
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
|
|
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
|
|
31
|
-
- **`start()`
|
|
32
|
-
- **`
|
|
33
|
-
- **`
|
|
34
|
-
-
|
|
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
|
|
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
|
|
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
|
|
33
|
-
- **`
|
|
34
|
-
-
|
|
35
|
-
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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' }
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
|
254
|
-
|
|
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)
|
|
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`
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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`)
|
|
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:**
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
25
|
-
- **`get()` takes an options object, not a positional default.** The signature is `get<ReturnType, BeforeTransformType = unknown>(key, opts?: { defaultValue?, transform? })`.
|
|
26
|
-
-
|
|
27
|
-
- **
|
|
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
|
|
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
|
|
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
|
|
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
|
|