@venizia/ignis-docs 0.0.8 → 0.2.0

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 (180) hide show
  1. package/README.md +7 -7
  2. package/content/best-practices/api-usage-examples.md +15 -12
  3. package/content/best-practices/architectural-patterns.md +70 -78
  4. package/content/best-practices/architecture-decisions.md +91 -60
  5. package/content/best-practices/code-style-standards/advanced-patterns.md +56 -44
  6. package/content/best-practices/code-style-standards/constants-configuration.md +11 -11
  7. package/content/best-practices/code-style-standards/control-flow.md +5 -2
  8. package/content/best-practices/code-style-standards/documentation.md +13 -13
  9. package/content/best-practices/code-style-standards/function-patterns.md +9 -10
  10. package/content/best-practices/code-style-standards/index.md +1 -1
  11. package/content/best-practices/code-style-standards/naming-conventions.md +10 -8
  12. package/content/best-practices/code-style-standards/route-definitions.md +30 -12
  13. package/content/best-practices/code-style-standards/tooling.md +8 -5
  14. package/content/best-practices/code-style-standards/type-safety.md +13 -12
  15. package/content/best-practices/common-pitfalls.md +56 -37
  16. package/content/best-practices/contribution-workflow.md +13 -14
  17. package/content/best-practices/data-modeling.md +46 -22
  18. package/content/best-practices/deployment-strategies.md +28 -27
  19. package/content/best-practices/error-handling.md +48 -24
  20. package/content/best-practices/index.md +5 -5
  21. package/content/best-practices/performance-optimization.md +40 -31
  22. package/content/best-practices/security-guidelines.md +52 -23
  23. package/content/best-practices/testing-strategies.md +65 -51
  24. package/content/best-practices/troubleshooting-tips.md +24 -24
  25. package/content/extensions/components/{swagger.md → api-reference.md} +40 -31
  26. package/content/extensions/components/authentication/api.md +19 -19
  27. package/content/extensions/components/authentication/errors.md +7 -7
  28. package/content/extensions/components/authentication/index.md +10 -8
  29. package/content/extensions/components/authentication/usage.md +101 -6
  30. package/content/extensions/components/authorization/api.md +45 -25
  31. package/content/extensions/components/authorization/errors.md +6 -6
  32. package/content/extensions/components/authorization/index.md +11 -10
  33. package/content/extensions/components/authorization/usage.md +21 -21
  34. package/content/extensions/components/health-check.md +1 -1
  35. package/content/extensions/components/index.md +5 -5
  36. package/content/extensions/components/mail/errors.md +15 -15
  37. package/content/extensions/components/mail/index.md +1 -2
  38. package/content/extensions/components/mail/usage.md +1 -1
  39. package/content/extensions/components/request-tracker.md +1 -1
  40. package/content/extensions/components/socket-io/api.md +9 -9
  41. package/content/extensions/components/socket-io/errors.md +5 -5
  42. package/content/extensions/components/socket-io/index.md +8 -8
  43. package/content/extensions/components/socket-io/usage.md +1 -1
  44. package/content/extensions/components/static-asset/api.md +17 -4
  45. package/content/extensions/components/static-asset/errors.md +4 -4
  46. package/content/extensions/components/static-asset/index.md +26 -28
  47. package/content/extensions/components/static-asset/usage.md +13 -12
  48. package/content/extensions/components/template/index.md +2 -2
  49. package/content/extensions/components/template/setup-page.md +1 -1
  50. package/content/extensions/components/websocket/api.md +3 -3
  51. package/content/extensions/components/websocket/errors.md +5 -5
  52. package/content/extensions/components/websocket/index.md +5 -5
  53. package/content/extensions/components/websocket/usage.md +3 -3
  54. package/content/extensions/helpers/cron/index.md +2 -2
  55. package/content/extensions/helpers/crypto/index.md +1 -1
  56. package/content/extensions/helpers/env/index.md +27 -12
  57. package/content/extensions/helpers/error/index.md +81 -25
  58. package/content/extensions/helpers/index.md +2 -3
  59. package/content/extensions/helpers/inversion/index.md +15 -7
  60. package/content/extensions/helpers/kafka/compile-binary.md +92 -0
  61. package/content/extensions/helpers/kafka/examples.md +1 -1
  62. package/content/extensions/helpers/kafka/index.md +3 -0
  63. package/content/extensions/helpers/logger/index.md +32 -2
  64. package/content/extensions/helpers/network/index.md +6 -0
  65. package/content/extensions/helpers/queue/index.md +14 -17
  66. package/content/extensions/helpers/redis/index.md +548 -323
  67. package/content/extensions/helpers/socket-io/index.md +14 -10
  68. package/content/extensions/helpers/storage/api.md +44 -8
  69. package/content/extensions/helpers/storage/index.md +43 -7
  70. package/content/extensions/helpers/template/index.md +6 -3
  71. package/content/extensions/helpers/types/index.md +11 -8
  72. package/content/extensions/helpers/websocket/api.md +9 -9
  73. package/content/extensions/helpers/websocket/index.md +7 -7
  74. package/content/extensions/helpers/worker-thread/index.md +2 -2
  75. package/content/extensions/index.md +3 -4
  76. package/content/extensions/src-details/mcp-server.md +18 -24
  77. package/content/guides/core-concepts/application/bootstrapping.md +11 -14
  78. package/content/guides/core-concepts/application/index.md +3 -3
  79. package/content/guides/core-concepts/components.md +19 -10
  80. package/content/guides/core-concepts/dependency-injection.md +6 -3
  81. package/content/guides/core-concepts/grpc-controllers.md +6 -5
  82. package/content/guides/core-concepts/persistent/datasources.md +42 -43
  83. package/content/guides/core-concepts/persistent/index.md +16 -7
  84. package/content/guides/core-concepts/persistent/models.md +24 -20
  85. package/content/guides/core-concepts/persistent/postgres-drivers.md +201 -0
  86. package/content/guides/core-concepts/persistent/repositories.md +40 -23
  87. package/content/guides/core-concepts/persistent/search-meilisearch.md +185 -0
  88. package/content/guides/core-concepts/persistent/search-typesense.md +431 -0
  89. package/content/guides/core-concepts/persistent/transactions.md +61 -25
  90. package/content/guides/core-concepts/rest-controllers.md +12 -9
  91. package/content/guides/core-concepts/services.md +330 -60
  92. package/content/guides/get-started/5-minute-quickstart.md +15 -15
  93. package/content/guides/get-started/philosophy.md +36 -36
  94. package/content/guides/get-started/setup.md +3 -3
  95. package/content/guides/index.md +3 -3
  96. package/content/guides/migrations/redis-helpers-migration.md +177 -0
  97. package/content/guides/migrations/scoped-rbac-migration.md +17 -17
  98. package/content/guides/migrations/unified-connectors-migration.md +113 -0
  99. package/content/guides/reference/glossary.md +19 -12
  100. package/content/guides/reference/mcp-docs-server.md +22 -18
  101. package/content/guides/tutorials/building-a-crud-api.md +37 -44
  102. package/content/guides/tutorials/complete-installation.md +17 -17
  103. package/content/guides/tutorials/ecommerce-api.md +163 -124
  104. package/content/guides/tutorials/realtime-chat.md +181 -135
  105. package/content/guides/tutorials/testing.md +65 -523
  106. package/content/index.md +2 -180
  107. package/content/public/apple-touch-icon.png +0 -0
  108. package/content/public/og-image.png +0 -0
  109. package/content/public/site.webmanifest +11 -0
  110. package/content/references/base/application.md +4 -5
  111. package/content/references/base/bootstrapping.md +18 -5
  112. package/content/references/base/components.md +149 -120
  113. package/content/references/base/connectors.md +178 -0
  114. package/content/references/base/controllers.md +41 -30
  115. package/content/references/base/datasources.md +163 -92
  116. package/content/references/base/dependency-injection.md +34 -22
  117. package/content/references/base/filter-system/application-usage.md +17 -14
  118. package/content/references/base/filter-system/array-operators.md +7 -2
  119. package/content/references/base/filter-system/comparison-operators.md +3 -0
  120. package/content/references/base/filter-system/default-filter.md +89 -71
  121. package/content/references/base/filter-system/fields-order-pagination.md +22 -22
  122. package/content/references/base/filter-system/index.md +6 -3
  123. package/content/references/base/filter-system/json-filtering.md +20 -1
  124. package/content/references/base/filter-system/list-operators.md +1 -1
  125. package/content/references/base/filter-system/logical-operators.md +33 -1
  126. package/content/references/base/filter-system/null-operators.md +30 -1
  127. package/content/references/base/filter-system/quick-reference.md +23 -4
  128. package/content/references/base/filter-system/tips.md +5 -5
  129. package/content/references/base/filter-system/use-cases.md +12 -12
  130. package/content/references/base/grpc-controllers.md +13 -13
  131. package/content/references/base/index.md +24 -12
  132. package/content/references/base/middlewares.md +265 -327
  133. package/content/references/base/models.md +63 -49
  134. package/content/references/base/providers.md +136 -130
  135. package/content/references/base/repositories/advanced.md +59 -58
  136. package/content/references/base/repositories/index.md +115 -91
  137. package/content/references/base/repositories/mixins.md +55 -291
  138. package/content/references/base/repositories/relations.md +54 -64
  139. package/content/references/base/repositories/soft-deletable.md +31 -30
  140. package/content/references/base/services.md +296 -93
  141. package/content/references/configuration/environment-variables.md +49 -31
  142. package/content/references/configuration/index.md +6 -6
  143. package/content/references/index.md +17 -12
  144. package/content/references/quick-reference.md +65 -106
  145. package/content/references/utilities/crypto.md +65 -23
  146. package/content/references/utilities/index.md +3 -3
  147. package/content/references/utilities/jsx.md +6 -4
  148. package/content/references/utilities/module.md +68 -20
  149. package/content/references/utilities/parse.md +4 -14
  150. package/content/references/utilities/promise.md +9 -7
  151. package/content/references/utilities/schema.md +5 -3
  152. package/dist/mcp-server/common/guards.d.ts +8 -0
  153. package/dist/mcp-server/common/guards.d.ts.map +1 -0
  154. package/dist/mcp-server/common/guards.js +14 -0
  155. package/dist/mcp-server/common/guards.js.map +1 -0
  156. package/dist/mcp-server/common/index.d.ts +1 -0
  157. package/dist/mcp-server/common/index.d.ts.map +1 -1
  158. package/dist/mcp-server/common/index.js +1 -0
  159. package/dist/mcp-server/common/index.js.map +1 -1
  160. package/dist/mcp-server/helpers/docs.helper.d.ts.map +1 -1
  161. package/dist/mcp-server/helpers/docs.helper.js +4 -2
  162. package/dist/mcp-server/helpers/docs.helper.js.map +1 -1
  163. package/dist/mcp-server/helpers/github.helper.js +1 -1
  164. package/dist/mcp-server/index.js +7 -2
  165. package/dist/mcp-server/index.js.map +1 -1
  166. package/dist/mcp-server/tools/base.tool.d.ts +6 -2
  167. package/dist/mcp-server/tools/base.tool.d.ts.map +1 -1
  168. package/dist/mcp-server/tools/base.tool.js.map +1 -1
  169. package/dist/mcp-server/tools/docs/search-documents.tool.d.ts +1 -1
  170. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
  171. package/dist/mcp-server/tools/github/search-code.tool.d.ts +1 -1
  172. package/dist/mcp-server/tools/github/search-code.tool.d.ts.map +1 -1
  173. package/dist/mcp-server/tools/github/search-code.tool.js +4 -1
  174. package/dist/mcp-server/tools/github/search-code.tool.js.map +1 -1
  175. package/dist/mcp-server/tools/github/verify-dependencies.tool.d.ts.map +1 -1
  176. package/dist/mcp-server/tools/github/verify-dependencies.tool.js +3 -1
  177. package/dist/mcp-server/tools/github/verify-dependencies.tool.js.map +1 -1
  178. package/package.json +9 -9
  179. package/content/extensions/helpers/testing/index.md +0 -510
  180. package/content/references/base/middleware.md +0 -347
@@ -129,16 +129,16 @@ export class Application extends BaseApplication {
129
129
  The component auto-registers REST endpoints for each configured backend. No injection needed in downstream code.
130
130
 
131
131
  ```
132
- GET /assets/buckets List all buckets
133
- GET /assets/buckets/:bucketName Get bucket details (or null)
134
- POST /assets/buckets/:bucketName Create a bucket
135
- DELETE /assets/buckets/:bucketName Delete a bucket
136
- POST /assets/buckets/:bucketName/upload Upload files
137
- GET /assets/buckets/:bucketName/objects List objects in bucket
138
- GET /assets/buckets/:bucketName/objects/:obj Stream file inline
139
- GET /assets/buckets/:bucketName/objects/:obj/download Download file (attachment)
140
- DELETE /assets/buckets/:bucketName/objects/:obj Delete file
141
- PUT /assets/buckets/:bucketName/objects/:obj/meta-links Sync MetaLink (MetaLink only)
132
+ GET /assets/buckets - List all buckets
133
+ GET /assets/buckets/:bucketName - Get bucket details (or null)
134
+ POST /assets/buckets/:bucketName - Create a bucket
135
+ DELETE /assets/buckets/:bucketName - Delete a bucket
136
+ POST /assets/buckets/:bucketName/upload - Upload files
137
+ GET /assets/buckets/:bucketName/objects - List objects in bucket
138
+ GET /assets/buckets/:bucketName/objects/:obj - Stream file inline
139
+ GET /assets/buckets/:bucketName/download/:obj - Download file (attachment)
140
+ DELETE /assets/buckets/:bucketName/objects/:obj - Delete file
141
+ PUT /assets/buckets/:bucketName/meta-links/:obj - Sync MetaLink (MetaLink only)
142
142
  ```
143
143
 
144
144
  Each storage backend gets its own base path (`/assets`, `/resources`, etc.) with the same endpoint structure.
@@ -237,12 +237,12 @@ Each route can be individually configured with authentication, middleware, and p
237
237
  name: 'AssetController',
238
238
  basePath: '/assets',
239
239
  routes: {
240
- getBuckets: { authenticate: { strategies: ['jwt'], mode: 'required' } },
241
- upload: { authenticate: { strategies: ['jwt'], mode: 'required' }, middleware: [rateLimitMw] },
240
+ getBuckets: { authenticate: { strategies: ['jwt'], mode: 'any' } },
241
+ upload: { authenticate: { strategies: ['jwt'], mode: 'any' }, middleware: [rateLimitMw] },
242
242
  getObjectByName: { /* public -- no authenticate */ },
243
243
  downloadObjectByName: { /* public */ },
244
- deleteObject: { authenticate: { strategies: ['jwt'], mode: 'required' } },
245
- deleteBucket: { authenticate: { strategies: ['jwt'], mode: 'required' } },
244
+ deleteObject: { authenticate: { strategies: ['jwt'], mode: 'any' } },
245
+ deleteBucket: { authenticate: { strategies: ['jwt'], mode: 'any' } },
246
246
  },
247
247
  },
248
248
  // ...
@@ -288,11 +288,13 @@ type TStaticAssetExtraOptions = {
288
288
  };
289
289
  normalizeNameFn?: (opts: { originalName: string }) => string;
290
290
  normalizeLinkFn?: (opts: { bucketName: string; normalizeName: string }) => string;
291
+ /** Maximum folder nesting depth allowed in object paths. Default: 2 */
292
+ maxFolderDepth?: number;
291
293
  [key: string]: any;
292
294
  };
293
295
 
294
296
  type TMetaLinkConfig<Schema extends TMetaLinkSchema = TMetaLinkSchema> = {
295
- model: typeof BaseEntity<Schema>;
297
+ model: typeof BasePostgresEntity<Schema>;
296
298
  repository: DefaultCRUDRepository<Schema>;
297
299
  createMetaLink?: (opts: {
298
300
  uploadResult: IUploadResult;
@@ -393,7 +395,8 @@ MetaLink is an optional feature that tracks uploaded files in a database, storin
393
395
  **1. Create Model:**
394
396
 
395
397
  ```typescript
396
- import { BaseMetaLinkModel, model } from '@venizia/ignis/static-asset';
398
+ import { BaseMetaLinkModel } from '@venizia/ignis/static-asset';
399
+ import { model } from '@venizia/ignis';
397
400
 
398
401
  @model({ type: 'entity' })
399
402
  export class FileMetaLinkModel extends BaseMetaLinkModel {
@@ -405,18 +408,13 @@ export class FileMetaLinkModel extends BaseMetaLinkModel {
405
408
 
406
409
  ```typescript
407
410
  import { BaseMetaLinkRepository } from '@venizia/ignis/static-asset';
408
- import { repository, inject } from '@venizia/ignis';
409
- import type { IDataSource } from '@venizia/ignis';
411
+ import { repository } from '@venizia/ignis';
412
+ import { FileMetaLinkModel } from './file-meta-link.model';
413
+ import { PostgresDataSource } from '../datasources/postgres.datasource';
410
414
 
411
- @repository({})
415
+ @repository({ model: FileMetaLinkModel, dataSource: PostgresDataSource })
412
416
  export class FileMetaLinkRepository extends BaseMetaLinkRepository {
413
- constructor(@inject({ key: 'datasources.postgres' }) dataSource: IDataSource) {
414
- super({
415
- entityClass: FileMetaLinkModel,
416
- relations: {},
417
- dataSource,
418
- });
419
- }
417
+ // No constructor needed - dataSource auto-injected from @repository metadata
420
418
  }
421
419
  ```
422
420
 
@@ -427,8 +425,8 @@ The model has `skipMigrate: true`, so create the table manually:
427
425
  ```sql
428
426
  CREATE TABLE "MetaLink" (
429
427
  id TEXT PRIMARY KEY,
430
- created_at TIMESTAMP NOT NULL DEFAULT NOW(),
431
- modified_at TIMESTAMP NOT NULL DEFAULT NOW(),
428
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
429
+ modified_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
432
430
  bucket_name TEXT NOT NULL,
433
431
  object_name TEXT NOT NULL,
434
432
  link TEXT NOT NULL,
@@ -15,9 +15,9 @@ The component dynamically generates REST endpoints for each configured storage b
15
15
  | `POST` | <code v-pre>/{basePath}/buckets/:bucketName/upload</code> | Upload files |
16
16
  | `GET` | <code v-pre>/{basePath}/buckets/:bucketName/objects</code> | List objects |
17
17
  | `GET` | <code v-pre>/{basePath}/buckets/:bucketName/objects/:objectName</code> | Stream file |
18
- | `GET` | <code v-pre>/{basePath}/buckets/:bucketName/objects/:objectName/download</code> | Download file |
18
+ | `GET` | <code v-pre>/{basePath}/buckets/:bucketName/download/:objectName</code> | Download file |
19
19
  | `DELETE` | <code v-pre>/{basePath}/buckets/:bucketName/objects/:objectName</code> | Delete object |
20
- | `PUT` | <code v-pre>/{basePath}/buckets/:bucketName/objects/:objectName/meta-links</code> | Sync MetaLink (MetaLink only) |
20
+ | `PUT` | <code v-pre>/{basePath}/buckets/:bucketName/meta-links/:objectName</code> | Sync MetaLink (MetaLink only) |
21
21
 
22
22
  #### GET <code v-pre>/{basePath}/buckets</code>
23
23
  **Response `200`:**
@@ -74,6 +74,7 @@ The `isDeleted` field is a boolean indicating whether the bucket was successfull
74
74
  - `principalType` (optional, string): Type of the principal to associate with the uploaded files (e.g., `"user"`, `"service"`)
75
75
  - `principalId` (optional, string or number): ID of the principal. Always coerced to a string via `String()` before storage regardless of input type
76
76
  - `variant` (optional, string): Variant tag for the upload (e.g., `"thumbnail"`, `"original"`)
77
+ - `folderPath` (optional, string): Target folder path for uploaded files (e.g., `"photos/2024"`). Each segment is validated with `isValidName()` and the segment count must not exceed the configured `maxFolderDepth` (default 2); returns 400 on violation
77
78
 
78
79
  **Validation:** Bucket name validated with `isValidName()`. Returns 400 `"Invalid bucket name"` if invalid.
79
80
 
@@ -187,7 +188,7 @@ All fields in the `IObjectInfo` response are optional. The `prefix` field is pre
187
188
  - `bucketName` (path): Bucket name
188
189
  - `objectName` (path): Object name (URL-encoded)
189
190
 
190
- **Validation:** Both bucket and object names validated with `isValidName()`. Returns 400 `"Invalid bucket name"` or `"Invalid object name"` respectively if either is invalid.
191
+ **Validation:** Bucket name validated with `isValidName()`; object name validated with `isValidPath()`. Returns 400 `"Invalid bucket name"` or `"Invalid object name or path"` respectively if either is invalid.
191
192
 
192
193
  **Response:**
193
194
  - Streams file content with appropriate headers
@@ -196,12 +197,12 @@ All fields in the `IObjectInfo` response are optional. The `prefix` field is pre
196
197
  - `X-Content-Type-Options`: `nosniff`
197
198
  - Additional whitelisted headers forwarded from storage metadata (see [Header Sanitization](./api#header-sanitization))
198
199
 
199
- #### GET <code v-pre>/{basePath}/buckets/:bucketName/objects/:objectName/download</code>
200
+ #### GET <code v-pre>/{basePath}/buckets/:bucketName/download/:objectName</code>
200
201
  **Parameters:**
201
202
  - `bucketName` (path): Bucket name
202
203
  - `objectName` (path): Object name (URL-encoded)
203
204
 
204
- **Validation:** Both bucket and object names validated with `isValidName()`. Returns 400 `"Invalid bucket name"` or `"Invalid object name"` respectively if either is invalid.
205
+ **Validation:** Bucket name validated with `isValidName()`; object name validated with `isValidPath()`. Returns 400 `"Invalid bucket name"` or `"Invalid object name or path"` respectively if either is invalid.
205
206
 
206
207
  **Response:**
207
208
  - Streams file with download headers
@@ -214,7 +215,7 @@ All fields in the `IObjectInfo` response are optional. The `prefix` field is pre
214
215
 
215
216
  **Example:**
216
217
  ```typescript
217
- const downloadUrl = `/assets/buckets/uploads/objects/${encodeURIComponent('document.pdf')}/download`;
218
+ const downloadUrl = `/assets/buckets/uploads/download/${encodeURIComponent('document.pdf')}`;
218
219
  window.open(downloadUrl, '_blank');
219
220
  ```
220
221
 
@@ -223,7 +224,7 @@ window.open(downloadUrl, '_blank');
223
224
  - `bucketName` (path): Bucket name
224
225
  - `objectName` (path): Object to delete (URL-encoded)
225
226
 
226
- **Validation:** Both bucket and object names validated with `isValidName()`. Returns 400 `"Invalid bucket name"` or `"Invalid object name"` respectively if either is invalid.
227
+ **Validation:** Bucket name validated with `isValidName()`; object name validated with `isValidPath()`. Returns 400 `"Invalid bucket name"` or `"Invalid object name or path"` respectively if either is invalid.
227
228
 
228
229
  **Behavior:**
229
230
  - Deletes file from storage
@@ -248,14 +249,14 @@ await fetch(`/assets/buckets/${bucketName}/objects/${encodeURIComponent(objectNa
248
249
  // MetaLink record deletion initiated (if enabled) but may complete after response
249
250
  ```
250
251
 
251
- #### PUT <code v-pre>/{basePath}/buckets/:bucketName/objects/:objectName/meta-links</code>
252
+ #### PUT <code v-pre>/{basePath}/buckets/:bucketName/meta-links/:objectName</code>
252
253
  **Availability:** Only registered when `useMetaLink: true`.
253
254
 
254
255
  **Parameters:**
255
256
  - `bucketName` (path): Bucket name
256
257
  - `objectName` (path): Object name (URL-encoded)
257
258
 
258
- **Validation:** Both bucket and object names validated with `isValidName()`. Returns 400 `"Invalid bucket name"` or `"Invalid object name"` respectively if either is invalid.
259
+ **Validation:** Bucket name validated with `isValidName()`; object name validated with `isValidPath()`. Returns 400 `"Invalid bucket name"` or `"Invalid object name or path"` respectively if either is invalid.
259
260
 
260
261
  **Behavior:**
261
262
  - Fetches current file metadata from storage via `helper.getStat()`
@@ -302,7 +303,7 @@ const bucketName = 'user-uploads';
302
303
  const objectName = 'document.pdf';
303
304
 
304
305
  const response = await fetch(
305
- `/assets/buckets/${bucketName}/objects/${encodeURIComponent(objectName)}/meta-links`,
306
+ `/assets/buckets/${bucketName}/meta-links/${encodeURIComponent(objectName)}`,
306
307
  { method: 'PUT' }
307
308
  );
308
309
 
@@ -315,7 +316,7 @@ const objects = await fetch(`/assets/buckets/${bucketName}/objects`).then(r => r
315
316
 
316
317
  for (const obj of objects) {
317
318
  await fetch(
318
- `/assets/buckets/${bucketName}/objects/${encodeURIComponent(obj.name)}/meta-links`,
319
+ `/assets/buckets/${bucketName}/meta-links/${encodeURIComponent(obj.name)}`,
319
320
  { method: 'PUT' }
320
321
  );
321
322
  }
@@ -345,7 +346,7 @@ async function uploadFile(file: File, principalType?: string, principalId?: stri
345
346
 
346
347
  // Download file
347
348
  function downloadFile(bucketName: string, objectName: string) {
348
- const url = `/assets/buckets/${bucketName}/objects/${encodeURIComponent(objectName)}/download`;
349
+ const url = `/assets/buckets/${bucketName}/download/${encodeURIComponent(objectName)}`;
349
350
  window.open(url, '_blank');
350
351
  }
351
352
 
@@ -1,6 +1,6 @@
1
1
  # Component Documentation Template
2
2
 
3
- Guide for writing consistent, professional component reference docs for Ignis.
3
+ Guide for writing consistent, professional component reference docs for IGNIS.
4
4
 
5
5
  ## Principles
6
6
 
@@ -14,7 +14,7 @@ Guide for writing consistent, professional component reference docs for Ignis.
14
14
  | Tier | Structure | When to Use | Examples |
15
15
  |------|-----------|-------------|----------|
16
16
  | **Tier 1** | [Single page](./single-page) | 5 or fewer config options, straightforward behavior | Health Check, Request Tracker, Swagger |
17
- | **Tier 2** | 4 pages | 6+ config options, multiple strategies/providers, architectural depth | Authentication, Mail, Socket.IO, WebSocket, Static Asset |
17
+ | **Tier 2** | 4 pages | 6+ config options, multiple strategies/providers, architectural depth | Authentication, Authorization, Mail, Socket.IO, WebSocket, Static Asset |
18
18
 
19
19
  ### Tier 1 -- Single Page
20
20
 
@@ -13,7 +13,7 @@ Paired with [Usage](./usage-page), [API Reference](./api-page), and [Error Refer
13
13
  ## Page Structure
14
14
 
15
15
  ```markdown
16
- # {Component Name}
16
+ # {Component Name} -- Setup & Configuration
17
17
 
18
18
  {One-line description of what this component does.}
19
19
 
@@ -111,7 +111,7 @@ upgrade() fetch(req, server)
111
111
  ```typescript
112
112
  interface IWebSocketEmitterOptions {
113
113
  identifier?: string; // Default: 'WebSocketEmitter' (used as logger scope)
114
- redisConnection: DefaultRedisHelper; // Required -- same Redis as the server(s)
114
+ redisConnection: IRedisHelper; // Required -- same Redis as the server(s)
115
115
  }
116
116
  ```
117
117
 
@@ -127,7 +127,7 @@ const emitter = new WebSocketEmitter({
127
127
  The constructor:
128
128
  1. Calls `super({ scope })` with `identifier` (or `'WebSocketEmitter'` if not provided)
129
129
  2. Validates `redisConnection` is truthy (throws `"Invalid redis connection!"` if not)
130
- 3. Calls `redisConnection.getClient().duplicate()` to create an isolated pub client
130
+ 3. Calls `redisConnection.duplicateClient()` to create an isolated pub client
131
131
 
132
132
  ### `EMITTER_SERVER_ID`
133
133
 
@@ -217,7 +217,7 @@ Reads all binding keys from the DI container and validates required ones:
217
217
  | Binding | Validation | Error on Failure |
218
218
  |---------|-----------|------------------|
219
219
  | `SERVER_OPTIONS` | Optional, merged with `DEFAULT_SERVER_OPTIONS` via `Object.assign()` | -- |
220
- | `REDIS_CONNECTION` | Must be `instanceof DefaultRedisHelper` | `"Invalid instance of redisConnection"` |
220
+ | `REDIS_CONNECTION` | Must be `instanceof AbstractRedisHelper` | `"Invalid instance of redisConnection"` |
221
221
  | `AUTHENTICATE_HANDLER` | Must be truthy (non-null) | `"Invalid authenticateFn to setup WebSocket server!"` |
222
222
  | `VALIDATE_ROOM_HANDLER` | Optional, coerced `null` to `undefined` | -- |
223
223
  | `CLIENT_CONNECTED_HANDLER` | Optional, coerced `null` to `undefined` | -- |
@@ -28,7 +28,7 @@ The server can send `error` events or close the connection under the following c
28
28
  |--------|-----------|---------------|
29
29
  | `binding()` | `application` is falsy | `"[binding] Invalid application to bind WebSocketComponent"` |
30
30
  | `binding()` | Node.js runtime detected | `"[WebSocketComponent] Node.js runtime is not supported yet. Please use Bun runtime."` |
31
- | `resolveBindings()` | `REDIS_CONNECTION` not instanceof `DefaultRedisHelper` | `"[WebSocketComponent][resolveBindings] Invalid instance of redisConnection | Please init connection with RedisHelper for single redis connection or RedisClusterHelper for redis cluster mode!"` |
31
+ | `resolveBindings()` | `REDIS_CONNECTION` not instanceof `AbstractRedisHelper` | `"[WebSocketComponent][resolveBindings] Invalid instance of redisConnection | Please init connection with RedisSingleHelper (single), RedisClusterHelper (cluster), or RedisSentinelHelper (sentinel)"` |
32
32
  | `resolveBindings()` | `AUTHENTICATE_HANDLER` is falsy | `"[WebSocketComponent] Invalid authenticateFn to setup WebSocket server!"` |
33
33
  | `registerBunHook()` | Bun server instance not available | `"[WebSocketComponent] Bun server instance not available!"` |
34
34
 
@@ -42,20 +42,20 @@ The server can send `error` events or close the connection under the following c
42
42
 
43
43
  ### "Invalid instance of redisConnection"
44
44
 
45
- **Cause**: The value bound to `REDIS_CONNECTION` is not an instance of `DefaultRedisHelper` (or its subclasses `RedisHelper` / `RedisClusterHelper`).
45
+ **Cause**: The value bound to `REDIS_CONNECTION` is not an instance of `AbstractRedisHelper` (i.e. not a `RedisSingleHelper`, `RedisClusterHelper`, or `RedisSentinelHelper`).
46
46
 
47
- **Fix**: Use `RedisHelper` (single instance) or `RedisClusterHelper` (cluster mode):
47
+ **Fix**: Use one of the concrete topology helpers:
48
48
 
49
49
  ```typescript
50
50
  import { WebSocketBindingKeys } from '@venizia/ignis/websocket';
51
51
 
52
52
  // Correct
53
53
  this.bind({ key: WebSocketBindingKeys.REDIS_CONNECTION })
54
- .toValue(new RedisHelper({ name: 'websocket', host, port, password }));
54
+ .toValue(new RedisSingleHelper({ name: 'websocket', host, port, password }));
55
55
 
56
56
  // Wrong -- raw ioredis client
57
57
  this.bind({ key: WebSocketBindingKeys.REDIS_CONNECTION })
58
- .toValue(new Redis(6379)); // This is NOT a DefaultRedisHelper!
58
+ .toValue(new Redis(6379)); // This is NOT an AbstractRedisHelper!
59
59
  ```
60
60
 
61
61
  ### "Invalid authenticateFn to setup WebSocket server!"
@@ -92,7 +92,7 @@ import {
92
92
  WebSocketBindingKeys,
93
93
  } from '@venizia/ignis/websocket';
94
94
  import {
95
- RedisHelper,
95
+ RedisSingleHelper,
96
96
  } from '@venizia/ignis-helpers';
97
97
  import type {
98
98
  TWebSocketAuthenticateFn,
@@ -107,7 +107,7 @@ import type {
107
107
  } from '@venizia/ignis-helpers';
108
108
 
109
109
  export class Application extends BaseApplication {
110
- private redisHelper: RedisHelper;
110
+ private redisHelper: RedisSingleHelper;
111
111
 
112
112
  preConfigure(): ValueOrPromise<void> {
113
113
  this.setupWebSocket();
@@ -116,7 +116,7 @@ export class Application extends BaseApplication {
116
116
 
117
117
  setupWebSocket() {
118
118
  // 1. Redis connection (required for cross-instance messaging)
119
- this.redisHelper = new RedisHelper({
119
+ this.redisHelper = new RedisSingleHelper({
120
120
  name: 'websocket-redis',
121
121
  host: process.env.REDIS_HOST ?? 'localhost',
122
122
  port: +(process.env.REDIS_PORT ?? 6379),
@@ -124,7 +124,7 @@ export class Application extends BaseApplication {
124
124
  autoConnect: false,
125
125
  });
126
126
 
127
- this.bind<RedisHelper>({
127
+ this.bind<RedisSingleHelper>({
128
128
  key: WebSocketBindingKeys.REDIS_CONNECTION,
129
129
  }).toValue(this.redisHelper);
130
130
 
@@ -331,7 +331,7 @@ interface IServerOptions {
331
331
  | Binding Key | Constant | Type | Required | Default |
332
332
  |------------|----------|------|----------|---------|
333
333
  | `@app/websocket/server-options` | `WebSocketBindingKeys.SERVER_OPTIONS` | `Partial<IServerOptions>` | No | See [Configuration](#configuration) |
334
- | `@app/websocket/redis-connection` | `WebSocketBindingKeys.REDIS_CONNECTION` | `DefaultRedisHelper` | **Yes** | `null` |
334
+ | `@app/websocket/redis-connection` | `WebSocketBindingKeys.REDIS_CONNECTION` | `IRedisHelper` | **Yes** | `null` |
335
335
  | `@app/websocket/authenticate-handler` | `WebSocketBindingKeys.AUTHENTICATE_HANDLER` | `TWebSocketAuthenticateFn` | **Yes** | `null` |
336
336
  | `@app/websocket/validate-room-handler` | `WebSocketBindingKeys.VALIDATE_ROOM_HANDLER` | `TWebSocketValidateRoomFn` | No | `null` |
337
337
  | `@app/websocket/client-connected-handler` | `WebSocketBindingKeys.CLIENT_CONNECTED_HANDLER` | `TWebSocketClientConnectedFn` | No | `null` |
@@ -109,10 +109,10 @@ It connects to Redis and publishes messages using the same `IRedisSocketMessage`
109
109
  #### Emitter Setup
110
110
 
111
111
  ```typescript
112
- import { WebSocketEmitter, RedisHelper } from '@venizia/ignis-helpers';
112
+ import { WebSocketEmitter, RedisSingleHelper } from '@venizia/ignis-helpers';
113
113
 
114
114
  // 1. Create a Redis connection (same Redis instance as the WebSocket server)
115
- const redisHelper = new RedisHelper({
115
+ const redisHelper = new RedisSingleHelper({
116
116
  name: 'emitter-redis',
117
117
  host: process.env.REDIS_HOST ?? 'localhost',
118
118
  port: +(process.env.REDIS_PORT ?? 6379),
@@ -343,7 +343,7 @@ Both `WebSocketServerHelper` and `WebSocketEmitter` support Redis single instanc
343
343
  type TRedisClient = Redis | Cluster;
344
344
  ```
345
345
 
346
- The Redis client is obtained via `redisConnection.getClient().duplicate()`. The `duplicate()` call creates a fresh connection that inherits the parent's configuration (including cluster mode). This ensures WebSocket pub/sub traffic does not interfere with application Redis usage.
346
+ The Redis client is obtained via `redisConnection.duplicateClient()`. This creates a fresh connection that inherits the parent's configuration (including cluster mode). This ensures WebSocket pub/sub traffic does not interfere with application Redis usage.
347
347
 
348
348
  ### Subscription Setup
349
349
 
@@ -147,7 +147,7 @@ hourlyJob.start();
147
147
 
148
148
  ### Accessing the Underlying CronJob
149
149
 
150
- The `instance` property exposes the underlying `CronJob` from the `cron` package, giving access to the full API (e.g., `stop()`, `running`, `lastDate()`).
150
+ The `instance` property exposes the underlying `CronJob` from the `cron` package, giving access to the full API (e.g., `stop()`, `isActive`, `lastDate()`).
151
151
 
152
152
  ```typescript
153
153
  const job = new CronHelper({
@@ -158,7 +158,7 @@ const job = new CronHelper({
158
158
  job.start();
159
159
 
160
160
  // Access the underlying CronJob directly
161
- console.log(job.instance.running); // true
161
+ console.log(job.instance.isActive); // true
162
162
  job.instance.stop();
163
163
  ```
164
164
 
@@ -18,7 +18,7 @@ Cryptographic utilities for AES symmetric encryption, RSA asymmetric encryption,
18
18
  | Type | Symmetric | Asymmetric | Asymmetric + Symmetric |
19
19
  | Key exchange | Shared secret | Public/private | Diffie-Hellman |
20
20
  | Speed | Fast | Slow (large keys) | Fast (small keys) |
21
- | Max message | Unlimited | ~190 bytes (2048-bit) | Unlimited |
21
+ | Max message | Unlimited | ~214 bytes (2048-bit, OAEP) | Unlimited |
22
22
  | Async | No | No | Yes (Web Crypto) |
23
23
  | Runtime | Node.js `crypto` | Node.js `crypto` | `crypto.subtle` (Bun/Browser) |
24
24
 
@@ -8,7 +8,7 @@ Structured access to application environment variables with prefix filtering, ty
8
8
  |------|-------|
9
9
  | **Package** | `@venizia/ignis-helpers` |
10
10
  | **Classes** | `ApplicationEnvironment`, `Environment` |
11
- | **Extends** | `IApplicationEnvironment` (interface) |
11
+ | **Implements** | `IApplicationEnvironment` (interface) |
12
12
  | **Singleton** | `applicationEnvironment` (alias `Envs`) -- auto-initialized at module load |
13
13
  | **Runtimes** | Both |
14
14
 
@@ -129,17 +129,32 @@ if (Environment.is({ name: 'staging' })) {
129
129
 
130
130
  #### Available Stages
131
131
 
132
- | Constant | Value |
133
- |----------|-------|
134
- | `Environment.LOCAL` | `'local'` |
135
- | `Environment.DEBUG` | `'debug'` |
136
- | `Environment.DEVELOPMENT` | `'development'` |
137
- | `Environment.ALPHA` | `'alpha'` |
138
- | `Environment.BETA` | `'beta'` |
139
- | `Environment.STAGING` | `'staging'` |
140
- | `Environment.PRODUCTION` | `'production'` |
141
-
142
- All stages are collected in `Environment.COMMON_ENVS` (a `Set<string>`), which is used internally by the Logger to determine whether debug logging should be active.
132
+ | Constant | Value | Development stage |
133
+ |----------|-------|-------------------|
134
+ | `Environment.LOCAL` | `'local'` | yes |
135
+ | `Environment.DEBUG` | `'debug'` | yes |
136
+ | `Environment.DEVELOPMENT` | `'development'` | yes |
137
+ | `Environment.DEV` | `'dev'` | yes - the short spelling of `development` |
138
+ | `Environment.SIT` | `'sit'` | yes |
139
+ | `Environment.UAT` | `'uat'` | no |
140
+ | `Environment.ALPHA` | `'alpha'` | no |
141
+ | `Environment.BETA` | `'beta'` | no |
142
+ | `Environment.STAGING` | `'staging'` | no |
143
+ | `Environment.PRODUCTION` | `'production'` | no |
144
+
145
+ All stages are collected in `Environment.COMMON_ENVS` (a `Set<string>`), which is used internally by the Logger to determine whether debug logging should be active. A `NODE_ENV` outside this set silences `DEBUG=true` entirely.
146
+
147
+ #### `Environment.DEVELOPMENT_ENVS` - the error-detail boundary
148
+
149
+ The stages marked "development stage" above form `Environment.DEVELOPMENT_ENVS`. IGNIS's error handler consults this set to decide whether an error response may carry internal detail - a stack trace, a SQL constraint name, a raw driver message.
150
+
151
+ The rule is fail-closed. A leak is opt-in by an explicit development name, so **anything else is sanitized as production**, including:
152
+
153
+ - `alpha`, `beta`, `uat`, `staging` - real users reach these
154
+ - an unrecognized name (a typo, a stage nobody added to the set)
155
+ - `NODE_ENV` left unset
156
+
157
+ Running a local service under `NODE_ENV=alpha` therefore gives you the same stripped-down error responses your users see. If you want the details while developing, set `NODE_ENV` to `development`, `dev`, or `local`.
143
158
 
144
159
  ### Configuring the Prefix
145
160
 
@@ -14,62 +14,108 @@ Standardized error class and factory for throwing HTTP-aware errors with machine
14
14
  #### Import Paths
15
15
 
16
16
  ```typescript
17
- import { ApplicationError, getError } from '@venizia/ignis-helpers';
17
+ import { ApplicationError, getError, isApplicationError } from '@venizia/ignis-helpers';
18
18
  import { ErrorSchema } from '@venizia/ignis-helpers';
19
19
  import type { TError } from '@venizia/ignis-helpers';
20
20
  ```
21
21
 
22
22
  ## Creating an Instance
23
23
 
24
- `ApplicationError` extends the native `Error` class with an HTTP `statusCode` and an optional `messageCode` for machine-readable error identification.
24
+ The canonical way to raise an error in IGNIS is the standalone `getError()` factory - use it everywhere (house rule: `getError`, never `new Error`). `ApplicationError` extends the native `Error` class with an HTTP `statusCode` and an optional `messageCode` for machine-readable error identification; `getError()` constructs one for you.
25
25
 
26
26
  ```typescript
27
- import { ApplicationError, HTTP } from '@venizia/ignis-helpers';
27
+ import { getError, HTTP } from '@venizia/ignis-helpers';
28
28
 
29
- const error = new ApplicationError({
29
+ throw getError({
30
30
  message: 'User not found',
31
31
  statusCode: HTTP.ResultCodes.RS_4.NotFound,
32
- messageCode: 'USER_NOT_FOUND',
32
+ messageCode: 'core.user.not_found',
33
33
  });
34
34
  ```
35
35
 
36
- #### Constructor Options (`TError`)
36
+ #### Options (`TError`)
37
+
38
+ The same options apply to `getError()`, the `ApplicationError` constructor, and the static factory:
37
39
 
38
40
  | Option | Type | Default | Description |
39
41
  |--------|------|---------|-------------|
40
42
  | `message` | `string` | -- (required) | Human-readable error message |
41
43
  | `statusCode` | `number` | `400` | HTTP status code |
42
- | `messageCode` | `string` | `undefined` | Machine-readable error code for client-side handling |
43
- | `name` | `string` | `undefined` | Error name |
44
+ | `messageCode` | `string` | `MessageCode.DEFAULT` (`'core.system_error'`) | Machine-readable error code for client-side handling. Always resolved through `MessageCode.resolve()` -- never left `undefined`, and lower-cased regardless of what was passed in |
45
+ | `name` | `string` | `undefined` | Accepted by the schema but discarded by the constructor (the native `Error` name is kept) |
44
46
 
45
47
  > [!TIP]
46
- > The `TError` type is derived from `ErrorSchema` (a Zod schema) and uses `.catchall(z.any())`, so you can pass additional arbitrary properties beyond the four listed above.
48
+ > The `TError` type is derived from `ErrorSchema` (a Zod schema) and uses `.catchall(z.any())`, so you can pass additional arbitrary properties beyond the four listed above. Extra properties are collected into the `extra` field on the resulting `ApplicationError` instance.
47
49
 
48
- #### `getError()` Factory Function
50
+ #### `ApplicationError` Constructor and Static Factory
49
51
 
50
- For convenience, use the standalone `getError()` function instead of calling `new ApplicationError()` directly. This is the preferred pattern throughout the Ignis codebase.
52
+ `getError()` is the canonical form. The class constructor and the static `ApplicationError.getError()` are legal equivalents - use them only when a direct class reference reads better:
51
53
 
52
54
  ```typescript
53
- import { getError, HTTP } from '@venizia/ignis-helpers';
55
+ import { ApplicationError, HTTP } from '@venizia/ignis-helpers';
54
56
 
55
- throw getError({
57
+ // Class constructor (equivalent to getError)
58
+ const error = new ApplicationError({
59
+ message: 'Configuration missing',
60
+ statusCode: HTTP.ResultCodes.RS_5.InternalServerError,
61
+ });
62
+
63
+ // Static factory (equivalent to getError)
64
+ throw ApplicationError.getError({
56
65
  message: 'Invalid credentials',
57
66
  statusCode: HTTP.ResultCodes.RS_4.Unauthorized,
58
- messageCode: 'INVALID_CREDENTIALS',
67
+ messageCode: 'core.auth.invalid_credentials',
59
68
  });
60
69
  ```
61
70
 
62
- #### `ApplicationError.getError()` Static Method
71
+ ### MessageCode
63
72
 
64
- An equivalent static factory method on the class itself:
73
+ `MessageCode` builds and normalizes the machine-readable codes carried on `ApplicationError.messageCode`. Every code an application throws should be constructed with `MessageCode.build()` rather than typed as a raw string literal -- a malformed code fails at module load (import time) instead of shipping dead into production.
65
74
 
66
75
  ```typescript
67
- throw ApplicationError.getError({
68
- message: 'Configuration missing',
69
- statusCode: HTTP.ResultCodes.RS_5.InternalServerError,
70
- });
76
+ import { MessageCode } from '@venizia/ignis-helpers';
77
+
78
+ export class UserErrorCodes {
79
+ static readonly NOT_FOUND = MessageCode.build({ parts: ['core', 'user', 'not_found'] });
80
+ static readonly DUPLICATE_EMAIL = MessageCode.build({ parts: ['core', 'user', 'duplicate_email'] });
81
+ }
82
+ ```
83
+
84
+ | Member | Type | Value / Signature | Description |
85
+ |--------|------|--------------------|-------------|
86
+ | `DEFAULT` | `string` | `'core.system_error'` | The code `ApplicationError` falls back to when no `messageCode` is supplied |
87
+ | `SEPARATOR` | `string` | `'.'` | Joins segments into a dotted code (e.g. `core.mail.send_failed`) |
88
+ | `SEGMENT_PATTERN` | `RegExp` | `/^[a-z0-9]+(_[a-z0-9]+)*$/` | Each segment must be lower snake_case -- `a-z`, `0-9`, `_` |
89
+ | `MIN_SEGMENTS` | `number` | `2` | A code needs at least a namespace and a reason (e.g. `core.not_found` is valid, `not_found` alone is not) |
90
+ | `build(opts: { parts: Array<string> })` | `string` | -- | Joins `parts` with `SEPARATOR` and lower-cases the result. Throws (via `getError`) if `parts.length < MIN_SEGMENTS`, or if any segment fails `SEGMENT_PATTERN` |
91
+ | `isValid(code: string)` | `boolean` | -- | Cheap structural check for a code arriving from outside the process |
92
+ | `resolve(code?: string)` | `string` | -- | Normalizes an absent or empty code to `DEFAULT`; otherwise lower-cases `code` |
93
+
94
+ > [!IMPORTANT]
95
+ > `ApplicationError`'s constructor always calls `MessageCode.resolve(messageCode)` -- so `error.messageCode` is **never** `undefined`, and it is **always lower-cased** regardless of the casing passed to `getError()`. A comparison like `error.messageCode === 'DUPLICATE_EMAIL'` is always false; compare against `'duplicate_email'` (or the exact string returned by `MessageCode.build()`).
96
+
97
+ ## Checking for an Application Error
98
+
99
+ Use `isApplicationError(error)` to recognize an application error by **shape** - an `Error` instance carrying a numeric `statusCode` - rather than by class identity:
100
+
101
+ ```typescript
102
+ import { isApplicationError } from '@venizia/ignis-helpers';
103
+
104
+ try {
105
+ await someOperation();
106
+ } catch (error) {
107
+ if (isApplicationError(error)) {
108
+ // already shaped (has a statusCode) - surface as-is
109
+ throw error;
110
+ }
111
+ // an unknown failure - sanitize before rethrowing
112
+ throw getError({ message: 'Operation failed', statusCode: HTTP.ResultCodes.RS_5.InternalServerError });
113
+ }
71
114
  ```
72
115
 
116
+ > [!WARNING]
117
+ > Never compare `ApplicationError` with `instanceof` across a package boundary. `inversion` ships **dual CJS + ESM** builds (its DI powers frontend libraries), so its error class deliberately has more than one runtime identity, and `helpers` keeps its own `ApplicationError` for the backend stack. Two objects that are both "an application error" can be instances of different classes, so `instanceof` gives false negatives. `isApplicationError` checks the shape and works regardless of which package threw. The search connectors use it to decide what is already shaped versus what must be sanitized as a `503`.
118
+
73
119
  ## Usage
74
120
 
75
121
  ### Throwing Errors in Services
@@ -109,23 +155,33 @@ throw getError({
109
155
 
110
156
  The built-in `appErrorHandler` middleware (from `@venizia/ignis`) catches `ApplicationError` instances and formats them into consistent JSON responses. The response shape differs by environment.
111
157
 
112
- #### Production Response
158
+ #### Sanitized (Production-Class) Response
159
+
160
+ The handler is **fail-closed**: it exposes internals only when `NODE_ENV` names a development environment - one of `local`, `debug`, `development`, `dev`, `sit` (`Environment.DEVELOPMENT_ENVS`). Everything else is sanitized as production: `production`, and also `alpha`, `beta`, `uat`, `staging`, an unrecognized name, and an unset `NODE_ENV`.
161
+
162
+ In a sanitized response, `stack` and `cause` are omitted from `details`. For unexpected errors without a `statusCode` (i.e., not thrown via `getError`), the raw message is replaced with a generic `"Internal Server Error"`, and a database error keeps only its base message - no SQL detail, no table or constraint name.
113
163
 
114
164
  ```json
115
165
  {
116
166
  "message": "User not found",
167
+ "messageCode": "core.user.not_found",
117
168
  "statusCode": 404,
118
- "requestId": "abc-123-def"
169
+ "requestId": "abc-123-def",
170
+ "details": {
171
+ "url": "http://localhost:3000/api/users/123",
172
+ "path": "/api/users/123"
173
+ }
119
174
  }
120
175
  ```
121
176
 
122
177
  #### Development Response
123
178
 
124
- In development (`NODE_ENV=development`), additional debugging fields are included:
179
+ In a development environment (see the list above), `details` additionally includes debugging fields:
125
180
 
126
181
  ```json
127
182
  {
128
183
  "message": "User not found",
184
+ "messageCode": "core.user.not_found",
129
185
  "statusCode": 404,
130
186
  "requestId": "abc-123-def",
131
187
  "details": {
@@ -197,9 +253,9 @@ throw getError({
197
253
 
198
254
  ### Error response missing `stack` and `cause`
199
255
 
200
- **Cause:** The application is running in production mode. The `appErrorHandler` middleware strips `stack`, `cause`, `url`, and `path` from responses when `NODE_ENV` is not `development`.
256
+ **Cause:** `NODE_ENV` is not one of the development environments, so the handler sanitized the response (`url` and `path` are always included). Note this is what happens on `alpha`/`staging`, on a typo'd name, and when `NODE_ENV` is unset - not only on `production`.
201
257
 
202
- **Fix:** Set `NODE_ENV=development` to see full error details during debugging.
258
+ **Fix:** Set `NODE_ENV` to a development name - `development`, `dev`, `local`, `debug` or `sit`. Anything else stays sanitized by design.
203
259
 
204
260
  ### Errors returning 500 instead of expected status code
205
261