@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
|
@@ -10,14 +10,14 @@ Every binding, endpoint, type, and internal mechanism of `StaticAssetComponent`.
|
|
|
10
10
|
|
|
11
11
|
**Files:**
|
|
12
12
|
|
|
13
|
-
- [`packages/core/src/components/static-asset/component.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/static-asset/component.ts)
|
|
14
|
-
- [`packages/core/src/components/static-asset/common/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/static-asset/common/types.ts)
|
|
15
|
-
- [`packages/core/src/components/static-asset/common/constants.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/static-asset/common/constants.ts)
|
|
16
|
-
- [`packages/core/src/components/static-asset/common/keys.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/static-asset/common/keys.ts)
|
|
17
|
-
- [`packages/core/src/components/static-asset/controller/factory.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/static-asset/controller/factory.ts)
|
|
18
|
-
- [`packages/core/src/components/static-asset/controller/base.definition.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/static-asset/controller/base.definition.ts)
|
|
19
|
-
- [`packages/core/src/components/static-asset/models/base.model.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/static-asset/models/base.model.ts)
|
|
20
|
-
- [`packages/core/src/components/static-asset/repositories/base.repository.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/static-asset/repositories/base.repository.ts)
|
|
13
|
+
- [`packages/core-server/src/components/static-asset/component.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/static-asset/component.ts)
|
|
14
|
+
- [`packages/core-server/src/components/static-asset/common/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/static-asset/common/types.ts)
|
|
15
|
+
- [`packages/core-server/src/components/static-asset/common/constants.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/static-asset/common/constants.ts)
|
|
16
|
+
- [`packages/core-server/src/components/static-asset/common/keys.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/static-asset/common/keys.ts)
|
|
17
|
+
- [`packages/core-server/src/components/static-asset/controller/factory.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/static-asset/controller/factory.ts)
|
|
18
|
+
- [`packages/core-server/src/components/static-asset/controller/base.definition.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/static-asset/controller/base.definition.ts)
|
|
19
|
+
- [`packages/core-server/src/components/static-asset/models/base.model.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/static-asset/models/base.model.ts)
|
|
20
|
+
- [`packages/core-server/src/components/static-asset/repositories/base.repository.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/static-asset/repositories/base.repository.ts)
|
|
21
21
|
- [`packages/helpers/src/modules/storage/base.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/storage/base.ts)
|
|
22
22
|
- [`packages/helpers/src/modules/storage/disk/helper.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/storage/disk/helper.ts)
|
|
23
23
|
- [`packages/helpers/src/modules/storage/minio/helper.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/storage/minio/helper.ts)
|
|
@@ -106,7 +106,7 @@ type TStaticAssetsComponentOptions = {
|
|
|
106
106
|
| Field | Type | Default | Description |
|
|
107
107
|
|-------|------|---------|--------------|
|
|
108
108
|
| `controller.name` | `string` | - | Class name given to the generated controller (via `Object.defineProperty`) |
|
|
109
|
-
| `controller.basePath` | `string` | - | Mount path,
|
|
109
|
+
| `controller.basePath` | `string` | - | Mount path, for example `'/assets'` |
|
|
110
110
|
| `controller.isStrict` | `boolean` | `true` | Passed through to `BaseRestController`'s strict routing mode |
|
|
111
111
|
| `controller.routes` | object | `undefined` | Per-route overrides - see [Per-route overrides](#per-route-overrides) |
|
|
112
112
|
| `storage` | `'disk' \| 'minio' \| 'bun-s3'` | - | Selects which `helper` type is required (discriminated union) |
|
|
@@ -149,7 +149,7 @@ type TStaticAssetExtraOptions = {
|
|
|
149
149
|
```
|
|
150
150
|
|
|
151
151
|
> [!NOTE]
|
|
152
|
-
> `normalizeNameFn` receives **both** `originalName` and `folderPath
|
|
152
|
+
> `normalizeNameFn` receives **both** `originalName` and `folderPath`. The second argument lets a custom implementation decide how to fold the target folder into the stored name. Omit `folderPath` handling and nested uploads flatten into the bucket root.
|
|
153
153
|
|
|
154
154
|
| Field | Default | Notes |
|
|
155
155
|
|-------|---------|-------|
|
|
@@ -170,7 +170,9 @@ type TStaticAssetExtraOptions = {
|
|
|
170
170
|
};
|
|
171
171
|
```
|
|
172
172
|
|
|
173
|
-
This is why every generated link points back at the `objects/{objectName}` stream route by default, regardless of storage backend.
|
|
173
|
+
This is why every generated link points back at the `objects/{objectName}` stream route by default, regardless of storage backend.
|
|
174
|
+
|
|
175
|
+
`BaseStorageHelper` has its own backend-specific `normalizeObjectLink()`. It only runs when you call a helper's `upload()` directly, outside the component. Through `StaticAssetComponent`, the component's default `normalizeLinkFn` always takes priority instead.
|
|
174
176
|
|
|
175
177
|
## Storage types
|
|
176
178
|
|
|
@@ -294,9 +296,29 @@ interface IListObjectsOptions {
|
|
|
294
296
|
|
|
295
297
|
### Name and path validation (`BaseStorageHelper`)
|
|
296
298
|
|
|
297
|
-
`isValidName(name)` rejects
|
|
298
|
-
|
|
299
|
-
|
|
299
|
+
`isValidName(name)` rejects any of the following, in order:
|
|
300
|
+
|
|
301
|
+
| Check | Rejects |
|
|
302
|
+
|-------|---------|
|
|
303
|
+
| Type | Non-string input |
|
|
304
|
+
| Empty | Empty string |
|
|
305
|
+
| Path traversal | `..`, `/`, or `\` |
|
|
306
|
+
| Hidden file | A leading `.` |
|
|
307
|
+
| Shell metacharacters | `;`, `\|`, `&`, `$`, `` ` ``, `<`, `>`, `{`, `}`, `[`, `]`, `!`, `#` |
|
|
308
|
+
| Control characters | `\n`, `\r`, `\0` |
|
|
309
|
+
| Length | Over 255 characters |
|
|
310
|
+
| Whitespace-only | Empty after trimming |
|
|
311
|
+
|
|
312
|
+
`isValidPath(pathStr, { maxDepth })` runs these checks, in order:
|
|
313
|
+
|
|
314
|
+
| Step | Behavior |
|
|
315
|
+
|------|----------|
|
|
316
|
+
| Normalize | Trims leading/trailing slashes |
|
|
317
|
+
| Empty check | Rejects if the result is empty |
|
|
318
|
+
| Double slashes | Rejects empty segments, for example `a//b` |
|
|
319
|
+
| Folder depth | `folderDepth = segments.length - 1`; rejects if it exceeds `maxDepth` (default `BaseStorageHelper.DEFAULT_MAX_FOLDER_DEPTH = 2`) |
|
|
320
|
+
| Segment names | Every segment must pass `isValidName()` |
|
|
321
|
+
| Path length | Rejects a normalized length over 1024 characters |
|
|
300
322
|
|
|
301
323
|
### `DiskHelper`
|
|
302
324
|
|
|
@@ -349,7 +371,7 @@ interface IBunS3HelperOptions {
|
|
|
349
371
|
}
|
|
350
372
|
```
|
|
351
373
|
|
|
352
|
-
Wraps Bun's native `S3Client`, imported from the `bun` builtin module
|
|
374
|
+
Wraps Bun's native `S3Client`, imported from the `bun` builtin module. That module only resolves under the Bun runtime. This is why `BunS3Helper` is exported from the separate `@venizia/ignis-helpers/bun-s3` subpath, not the main entry point.
|
|
353
375
|
|
|
354
376
|
```typescript
|
|
355
377
|
import { BunS3Helper } from '@venizia/ignis-helpers/bun-s3';
|
|
@@ -412,7 +434,7 @@ const MultipartBodySchema = z.object({
|
|
|
412
434
|
|--------|------|-------|
|
|
413
435
|
| `GET` | `/buckets` | No params. Returns `IBucketInfo[]` |
|
|
414
436
|
| `GET` | <code v-pre>/buckets/{bucketName}</code> | Returns `IBucketInfo \| null` |
|
|
415
|
-
| `POST` | <code v-pre>/buckets/{bucketName}</code> | Returns
|
|
437
|
+
| `POST` | <code v-pre>/buckets/{bucketName}</code> | Returns the created `IBucketInfo`. Throws if the bucket already exists or the name is invalid - see [Error Reference](./errors) |
|
|
416
438
|
| `DELETE` | <code v-pre>/buckets/{bucketName}</code> | Returns <code v-pre>{ isDeleted: boolean }</code> |
|
|
417
439
|
| `POST` | <code v-pre>/buckets/{bucketName}/upload</code> | `multipart/form-data` body; query: `principalType?`, `principalId?`, `variant?`, `folderPath?`. Returns `IUploadResult[]` |
|
|
418
440
|
| `GET` | <code v-pre>/buckets/{bucketName}/objects</code> | Query: `prefix?`, `recursive?` (`'true'` string only), `maxKeys?` (positive integer string). Returns `IObjectInfo[]` |
|
|
@@ -424,25 +446,33 @@ const MultipartBodySchema = z.object({
|
|
|
424
446
|
### Upload validation order
|
|
425
447
|
|
|
426
448
|
1. `bucketName` validated with `isValidName()` - `400 "Invalid bucket name"` on failure.
|
|
427
|
-
2. If `folderPath` is present
|
|
449
|
+
2. If `folderPath` is present, three checks run in order:
|
|
450
|
+
|
|
451
|
+
| Check | Failure |
|
|
452
|
+
|-------|---------|
|
|
453
|
+
| Trim leading/trailing slashes | `400 "Invalid folder path"` if empty after trimming |
|
|
454
|
+
| Segment count vs. `maxFolderDepth` | `400 "Folder path exceeds max depth of {n}"` if over |
|
|
455
|
+
| Each segment via `isValidName()` | `400 "Invalid folder path segment: {segment}"` if any fails |
|
|
456
|
+
|
|
428
457
|
3. `multipart/form-data` parsed via `parseMultipartBody()`.
|
|
429
|
-
4. Each file's effective buffer
|
|
458
|
+
4. Each file's effective buffer is checked non-empty - direct `buffer`, or `readFileSync(file.path)` when `storage: 'disk'` was used. Empty content returns `400 "Empty file content | name: {originalName}"`.
|
|
430
459
|
5. `helper.upload()` runs the storage-helper-level checks below.
|
|
431
|
-
6. Spool files written by `storage: 'disk'` parsing are removed in a `finally` block via `rmSync({ force: true })
|
|
460
|
+
6. Spool files written by `storage: 'disk'` parsing are removed in a `finally` block via `rmSync({ force: true })` - regardless of success or failure. Removal errors are logged, never thrown.
|
|
432
461
|
|
|
433
462
|
### Storage-helper-level upload checks (`BaseStorageHelper.upload`)
|
|
434
463
|
|
|
435
|
-
These run inside `helper.upload()`,
|
|
464
|
+
These run inside `helper.upload()`, separate from the controller checks above. They are reachable even when a caller uses the storage helper directly:
|
|
436
465
|
|
|
437
466
|
| Check | Error message | Default status |
|
|
438
467
|
|-------|----------------|-----------------|
|
|
439
468
|
| Bucket does not exist (`isBucketExists()` false) | <code v-pre>[upload] Bucket does not exist \| name: {bucket}</code> | `400` |
|
|
440
469
|
| `originalName` fails `isValidName()` | `[upload] Invalid original file name` | `400` |
|
|
441
|
-
| `folderPath`
|
|
470
|
+
| `folderPath` segment count exceeds `maxFolderDepth` | <code v-pre>[upload] Invalid folder path \| depth: {n} \| max: {m}</code> | `400` |
|
|
471
|
+
| `folderPath` fails `isValidPath()` for any other reason | `[upload] Invalid folder path` | `400` |
|
|
442
472
|
| `size` is `undefined`, `null`, or negative | <code v-pre>[upload] Invalid file size \| size: {size}</code> | `400` |
|
|
443
473
|
| Normalized name (post `normalizeNameFn`) fails `isValidPath()` | <code v-pre>[upload] Invalid normalized object name \| name: {name}</code> | `400` |
|
|
444
474
|
|
|
445
|
-
`getError()` defaults `statusCode` to `400` when the caller does not pass one explicitly
|
|
475
|
+
`getError()` defaults `statusCode` to `400` when the caller does not pass one explicitly. Every message above is thrown without an explicit status, so all resolve to `400`.
|
|
446
476
|
|
|
447
477
|
## Header sanitization
|
|
448
478
|
|
|
@@ -456,7 +486,9 @@ const WHITELIST_HEADERS = [
|
|
|
456
486
|
] as const;
|
|
457
487
|
```
|
|
458
488
|
|
|
459
|
-
These correspond to `HTTP.Headers.CONTENT_TYPE`, `CONTENT_ENCODING`, `CACHE_CONTROL`, `ETAG`, and `LAST_MODIFIED` from `@venizia/ignis-helpers`.
|
|
489
|
+
These correspond to `HTTP.Headers.CONTENT_TYPE`, `CONTENT_ENCODING`, `CACHE_CONTROL`, `ETAG`, and `LAST_MODIFIED` from `@venizia/ignis-helpers`.
|
|
490
|
+
|
|
491
|
+
When streaming a file - both `objects/{objectName}` and `download/{objectName}` - the controller copies only these keys from the storage metadata onto the response. Every other metadata header is dropped. Each forwarded value is sanitized with `String(value).replace(/[\r\n]/g, '')` before being set, to prevent HTTP header injection.
|
|
460
492
|
|
|
461
493
|
All streaming responses also set:
|
|
462
494
|
|
|
@@ -471,7 +503,7 @@ Content-Disposition: attachment; filename="..." (download endpoint only, via c
|
|
|
471
503
|
|
|
472
504
|
Hono percent-decodes a path param before the handler reads it. The controller's `readObjectName()` is therefore a deliberate no-op - it does not run a second `decodeURIComponent()`:
|
|
473
505
|
|
|
474
|
-
- `report_100%.pdf` is a legal object name. Its link is `.../objects/report_100%25.pdf
|
|
506
|
+
- `report_100%.pdf` is a legal object name. Its link is `.../objects/report_100%25.pdf`. Hono hands the handler back `report_100%.pdf`. A second decode would hit the invalid escape `%.p` and throw - the object would become permanently unfetchable and undeletable.
|
|
475
507
|
- An object named `a%2Fb.png` would decode twice into `a/b.png` - a different object than the one requested.
|
|
476
508
|
|
|
477
509
|
`isValidName()`/`isValidPath()` still run on the singly-decoded value, so a traversal payload is rejected exactly as before.
|
|
@@ -490,7 +522,12 @@ type TMetaLinkConfig<Schema extends TMetaLinkSchema = TMetaLinkSchema> = {
|
|
|
490
522
|
};
|
|
491
523
|
```
|
|
492
524
|
|
|
493
|
-
|
|
525
|
+
| Canonical name | Alias |
|
|
526
|
+
|----------------|-------|
|
|
527
|
+
| `BaseRelationalEntity` | `BasePostgresEntity` |
|
|
528
|
+
| `DefaultRelationalRepository` | `DefaultCRUDRepository` |
|
|
529
|
+
|
|
530
|
+
Both canonical classes live in `packages/core-server/src/connectors/postgres/`. Each alias re-exports the same class under a different name.
|
|
494
531
|
|
|
495
532
|
## MetaLink SQL schema
|
|
496
533
|
|
|
@@ -510,19 +547,34 @@ type TMetaLinkConfig<Schema extends TMetaLinkSchema = TMetaLinkSchema> = {
|
|
|
510
547
|
| `metadata` | JSONB | Yes | - | Additional file metadata |
|
|
511
548
|
| `storage_type` | TEXT | No | - | `'disk'`, `'minio'`, or `'bun-s3'` |
|
|
512
549
|
| `is_synced` | BOOLEAN | No | `false` | Set `true` on every upload and every meta-links sync |
|
|
513
|
-
| `variant` | TEXT | Yes | - | Upload variant tag (
|
|
550
|
+
| `variant` | TEXT | Yes | - | Upload variant tag (for example `'thumbnail'`, `'original'`) |
|
|
514
551
|
| `principal_type` | TEXT | Yes | - | Associated principal type |
|
|
515
552
|
| `principal_id` | TEXT | Yes | - | Associated principal ID, always stored as a string |
|
|
516
553
|
|
|
517
554
|
**Indexes:** `bucket_name`, `object_name`, `storage_type`, `is_synced`.
|
|
518
555
|
|
|
519
|
-
`@model({ type: 'entity', skipMigrate: true })` on `BaseMetaLinkModel` means IGNIS's schema migration skips this table
|
|
556
|
+
`@model({ type: 'entity', skipMigrate: true })` on `BaseMetaLinkModel` means IGNIS's schema migration skips this table. Create it manually, once, per database.
|
|
520
557
|
|
|
521
558
|
### MetaLink lifecycle
|
|
522
559
|
|
|
523
|
-
- **On upload:**
|
|
524
|
-
-
|
|
525
|
-
-
|
|
560
|
+
- **On upload:**
|
|
561
|
+
- Creates one MetaLink row per uploaded file, after fetching fresh stats via `helper.getStat()`.
|
|
562
|
+
- Uses `metaLink.createMetaLink()` when provided, otherwise a default insert that covers every standard field.
|
|
563
|
+
- `principalType`, `principalId`, and `variant` come from the upload's query parameters.
|
|
564
|
+
- If the insert throws, the upload still succeeds. The file's response entry gets `metaLink: null` plus a `metaLinkError` string, and the error is logged.
|
|
565
|
+
- **On delete:**
|
|
566
|
+
- The storage delete happens first and is awaited.
|
|
567
|
+
- The MetaLink row delete (`deleteAll({ where: { bucketName, objectName } })`) fires without being awaited.
|
|
568
|
+
- The HTTP response returns as soon as the storage delete resolves - the database delete may still be in flight.
|
|
569
|
+
- Errors there are logged, never surfaced to the client.
|
|
570
|
+
- **On sync (`PUT meta-links/:objectName`):** looks up an existing row by `bucketName` + `objectName`.
|
|
571
|
+
|
|
572
|
+
| Row found? | Action |
|
|
573
|
+
|------------|--------|
|
|
574
|
+
| Yes | `updateById()`, then re-fetches with `findById()` |
|
|
575
|
+
| No | `create()` |
|
|
576
|
+
|
|
577
|
+
Either path sets `isSynced: true` and returns `{ success: true, metaLink }`.
|
|
526
578
|
|
|
527
579
|
## Component lifecycle
|
|
528
580
|
|
|
@@ -532,7 +584,7 @@ type TMetaLinkConfig<Schema extends TMetaLinkSchema = TMetaLinkSchema> = {
|
|
|
532
584
|
4. Calls `AssetControllerFactory.defineAssetController()` and registers the result with `this.application.controller()`.
|
|
533
585
|
5. Logs the storage key, storage type, and whether MetaLink is enabled for each registered backend.
|
|
534
586
|
|
|
535
|
-
`StaticAssetComponent` itself performs no eager configuration validation beyond the options type
|
|
587
|
+
`StaticAssetComponent` itself performs no eager configuration validation beyond the options type. A missing `metaLink` when `useMetaLink: true` is caught at compile time by the discriminated union, not at `binding()` runtime.
|
|
536
588
|
|
|
537
589
|
## See also
|
|
538
590
|
|
|
@@ -21,18 +21,24 @@ Every error condition the static asset controller and storage helpers can raise,
|
|
|
21
21
|
| <code v-pre>"Invalid maxKeys \| Expected a positive integer \| value: {value}"</code> | `listObjects`'s `maxKeys` query param does not parse to a positive integer | `400` |
|
|
22
22
|
| <code v-pre>[upload] Bucket does not exist \| name: {bucket}</code> | `helper.upload()` found no matching bucket via `isBucketExists()` | `400` (default) |
|
|
23
23
|
| `[upload] Invalid original file name` | A file's `originalName` fails `isValidName()`, checked inside `helper.upload()` | `400` (default) |
|
|
24
|
-
|
|
|
24
|
+
| <code v-pre>[upload] Invalid folder path \| depth: {n} \| max: {m}</code> | `helper.upload()`'s own check found more `folderPath` segments than `maxFolderDepth` allows | `400` (default) |
|
|
25
|
+
| `[upload] Invalid folder path` | `helper.upload()`'s own `isValidPath()` check failed for any other reason | `400` (default) |
|
|
25
26
|
| <code v-pre>[upload] Invalid file size \| size: {size}</code> | A file's `size` is `undefined`, `null`, or negative | `400` (default) |
|
|
26
27
|
| <code v-pre>[upload] Invalid normalized object name \| name: {name}</code> | The name returned by `normalizeNameFn` fails `isValidPath()` | `400` (default) |
|
|
27
|
-
| `[createBucket] Invalid name to create bucket!` | `
|
|
28
|
+
| `[createBucket] Invalid name to create bucket!` | `createBucket()` called with a name failing `isValidName()` - only reachable calling a helper directly, the controller validates first | `400` (default) |
|
|
29
|
+
| <code v-pre>[createBucket] Bucket already exists \| name: {name}</code> | `DiskHelper.createBucket()` called with a name that already exists on disk. `MinioHelper` and `BunS3Helper` throw their own SDK/S3 error text for the same case instead | `400` (default) |
|
|
30
|
+
| `[removeBucket] Invalid name to remove bucket!` | `removeBucket()` called with a name failing `isValidName()` - only reachable calling a helper directly | `400` (default) |
|
|
31
|
+
| <code v-pre>[removeBucket] Bucket does not exist \| name: {name}</code> | `DiskHelper.removeBucket()` - no directory at that bucket name | `400` (default) |
|
|
32
|
+
| <code v-pre>[removeBucket] Bucket is not empty \| name: {name}</code> | `DiskHelper.removeBucket()` - the bucket directory still has files in it | `400` (default) |
|
|
33
|
+
| <code v-pre>[getFile] File not found \| bucket: {bucket} \| name: {name}</code> (also `[getStat]`, `[removeObject]`) | `DiskHelper` - no file at that bucket/object path | `400` (default) |
|
|
28
34
|
| <code v-pre>[parseMultipartBody] storage: {storage} \| Invalid storage type \| Valids: ['memory', 'disk']</code> | `extra.parseMultipartBody.storage` set to something other than `'memory'`/`'disk'` - a configuration error, not user input | `400` (default) |
|
|
29
35
|
|
|
30
36
|
> [!NOTE]
|
|
31
|
-
> Entries marked "default" pass no explicit `statusCode` to `getError()
|
|
37
|
+
> Entries marked "default" pass no explicit `statusCode` to `getError()`. `ApplicationError` defaults `statusCode` to `400` in that case.
|
|
32
38
|
|
|
33
39
|
## Name validation rules
|
|
34
40
|
|
|
35
|
-
Bucket names are validated with `isValidName()` (single segment, no path separators). Object names, which may include folder segments (
|
|
41
|
+
Bucket names are validated with `isValidName()` (single segment, no path separators). Object names, which may include folder segments (for example `2026/uploads/report.pdf`), are validated with `isValidPath()` - every segment still runs through `isValidName()`.
|
|
36
42
|
|
|
37
43
|
| Pattern | Example | Reason |
|
|
38
44
|
|---------|---------|--------|
|
|
@@ -51,7 +57,7 @@ Bucket names are validated with `isValidName()` (single segment, no path separat
|
|
|
51
57
|
### "Invalid bucket name" / "Invalid object name or path"
|
|
52
58
|
|
|
53
59
|
- **Cause:** the name fails the rules above.
|
|
54
|
-
- **Fix:** strip path separators, leading dots, shell metacharacters, and control characters
|
|
60
|
+
- **Fix:** strip path separators, leading dots, shell metacharacters, and control characters. Keep names under 255 characters (1024 for a full object path). Never send an empty or whitespace-only value.
|
|
55
61
|
|
|
56
62
|
```typescript
|
|
57
63
|
// Wrong - contains a path separator, rejected by isValidName()
|
|
@@ -77,11 +83,11 @@ await fetch('/assets/buckets/user-uploads', { method: 'POST' });
|
|
|
77
83
|
### "Empty file content"
|
|
78
84
|
|
|
79
85
|
- **Cause:** the uploaded file's buffer is empty - either the client sent a zero-byte file field, or `parseMultipartBody` produced no readable content.
|
|
80
|
-
- **Fix:** verify the `FormData` field actually carries file bytes before submitting
|
|
86
|
+
- **Fix:** verify the `FormData` field actually carries file bytes before submitting. A zero-byte file is rejected - this is not a size-limit issue.
|
|
81
87
|
|
|
82
88
|
### "Invalid maxKeys"
|
|
83
89
|
|
|
84
|
-
- **Cause:** `listObjects`'s `maxKeys` query string does not parse to a positive integer via `Number(maxKeys)` + `Number.isInteger()` -
|
|
90
|
+
- **Cause:** `listObjects`'s `maxKeys` query string does not parse to a positive integer via `Number(maxKeys)` + `Number.isInteger()` - for example `maxKeys=abc` or `maxKeys=-1`.
|
|
85
91
|
- **Fix:** only send positive integer strings, or omit the parameter entirely.
|
|
86
92
|
|
|
87
93
|
```typescript
|
|
@@ -94,6 +100,11 @@ url.searchParams.set('maxKeys', '50'); // OK
|
|
|
94
100
|
- **Cause:** `helper.upload()` checked `isBucketExists()` and found no match - the bucket was never created, or was deleted between requests.
|
|
95
101
|
- **Fix:** create the bucket first with `POST /buckets/:bucketName`, or check `GET /buckets/:bucketName` before uploading.
|
|
96
102
|
|
|
103
|
+
### "Bucket already exists"
|
|
104
|
+
|
|
105
|
+
- **Cause:** `POST /buckets/:bucketName` was called for a bucket that already exists. `createBucket()` throws rather than returning `null` - it does not silently succeed on a duplicate.
|
|
106
|
+
- **Fix:** check `GET /buckets/:bucketName` first, or treat the throw as "already there" and continue.
|
|
107
|
+
|
|
97
108
|
### Controller not registering / no routes appear
|
|
98
109
|
|
|
99
110
|
- **Cause:** `STATIC_ASSET_COMPONENT_OPTIONS` was never bound, or was bound to `{}` - the component's default. `binding()` iterates the options object and produces zero controllers for an empty map, without throwing.
|
|
@@ -45,12 +45,19 @@ This registers `GET`/`POST`/`DELETE` on `/assets/buckets/:bucketName`, `POST /as
|
|
|
45
45
|
|
|
46
46
|
## How it works
|
|
47
47
|
|
|
48
|
-
- **One options key, one generated controller.** `TStaticAssetsComponentOptions` is a record
|
|
49
|
-
- **`storage` and `helper` are a discriminated pair.**
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
- **One options key, one generated controller.** `TStaticAssetsComponentOptions` is a record. Each key (`staticAsset` above) becomes an independently configured storage backend with its own `basePath`. `AssetControllerFactory.defineAssetController()` builds the controller inside `StaticAssetComponent.binding()`.
|
|
49
|
+
- **`storage` and `helper` are a discriminated pair.** Mismatching them fails at compile time, not at runtime:
|
|
50
|
+
|
|
51
|
+
| `storage` | Required `helper` |
|
|
52
|
+
|-----------|--------------------|
|
|
53
|
+
| `'disk'` | `DiskHelper` |
|
|
54
|
+
| `'minio'` | `MinioHelper` |
|
|
55
|
+
| `'bun-s3'` | `BunS3Helper` |
|
|
56
|
+
|
|
57
|
+
- **Every backend implements the same `IStorageHelper` contract.** `DiskHelper`, `MinioHelper`, and `BunS3Helper` all extend `BaseStorageHelper`. Bucket/object operations, name validation (`isValidName`/`isValidPath`), and upload normalization behave identically regardless of backend.
|
|
58
|
+
- **Object names can embed folder paths, encoded as one segment.** `objects/{objectName}` percent-encodes the whole `folder/file.ext` string via `encodeURIComponent()`, which also escapes `/`. Hono decodes it before your handler runs - encode the name client-side, and never decode it again.
|
|
59
|
+
- **MetaLink is opt-in.** Set `useMetaLink: true` and provide `metaLink.repository`. IGNIS then persists a database row (`bucket/object/mimetype/size/etag/principal/variant`) alongside every upload. That row lets you query "which files does user X own" without listing a whole bucket.
|
|
60
|
+
- **The default binding is empty.** `StaticAssetComponentBindingKeys.STATIC_ASSET_COMPONENT_OPTIONS` defaults to `{}`. Bind it with at least one storage backend before `this.component(StaticAssetComponent)` - an empty binding produces zero routes.
|
|
54
61
|
|
|
55
62
|
## Common tasks
|
|
56
63
|
|
|
@@ -145,9 +152,9 @@ extra: {
|
|
|
145
152
|
|
|
146
153
|
**Files:**
|
|
147
154
|
|
|
148
|
-
- [`packages/core/src/components/static-asset/component.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/static-asset/component.ts) - `StaticAssetComponent`
|
|
149
|
-
- [`packages/core/src/components/static-asset/common/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/static-asset/common/types.ts) - `TStaticAssetsComponentOptions`, `TStaticAssetExtraOptions`, `TMetaLinkConfig`
|
|
150
|
-
- [`packages/core/src/components/static-asset/common/constants.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/static-asset/common/constants.ts) - `StaticAssetStorageTypes`
|
|
151
|
-
- [`packages/core/src/components/static-asset/controller/factory.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/static-asset/controller/factory.ts) - `AssetControllerFactory`
|
|
152
|
-
- [`packages/core/src/components/static-asset/models/base.model.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/static-asset/models/base.model.ts) - `BaseMetaLinkModel`
|
|
155
|
+
- [`packages/core-server/src/components/static-asset/component.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/static-asset/component.ts) - `StaticAssetComponent`
|
|
156
|
+
- [`packages/core-server/src/components/static-asset/common/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/static-asset/common/types.ts) - `TStaticAssetsComponentOptions`, `TStaticAssetExtraOptions`, `TMetaLinkConfig`
|
|
157
|
+
- [`packages/core-server/src/components/static-asset/common/constants.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/static-asset/common/constants.ts) - `StaticAssetStorageTypes`
|
|
158
|
+
- [`packages/core-server/src/components/static-asset/controller/factory.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/static-asset/controller/factory.ts) - `AssetControllerFactory`
|
|
159
|
+
- [`packages/core-server/src/components/static-asset/models/base.model.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/static-asset/models/base.model.ts) - `BaseMetaLinkModel`
|
|
153
160
|
- [`packages/helpers/src/modules/storage/base.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/storage/base.ts) - `BaseStorageHelper`, `IStorageHelper`
|
|
@@ -44,9 +44,14 @@ const [result] = await response.json();
|
|
|
44
44
|
// { bucketName: 'user-uploads', objectName: 'invoices/2026/document.pdf', link: '/assets/buckets/user-uploads/objects/invoices%2F2026%2Fdocument.pdf' }
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
- **`folderPath` is validated separately from the filename.** Each segment must pass `isValidName()
|
|
47
|
+
- **`folderPath` is validated separately from the filename.** Each segment must pass `isValidName()`. The segment count must stay within `maxFolderDepth` (default `2`). Both checks return `400` before the file is even parsed.
|
|
48
48
|
- **`principalId` is always stored as a string**, coerced with `String()` regardless of whether you send a number or a string.
|
|
49
|
-
- With MetaLink enabled, the
|
|
49
|
+
- **With MetaLink enabled, the upload always succeeds - even if the tracking write fails.** The response carries one of two shapes:
|
|
50
|
+
|
|
51
|
+
| Outcome | Response field |
|
|
52
|
+
|---------|-----------------|
|
|
53
|
+
| MetaLink write succeeded | `metaLink`: the created database record |
|
|
54
|
+
| MetaLink write failed | `metaLink: null` plus a `metaLinkError` string |
|
|
50
55
|
|
|
51
56
|
## Stream or download an object
|
|
52
57
|
|
|
@@ -61,10 +66,10 @@ const downloadUrl = `/assets/buckets/user-uploads/download/${encodeURIComponent(
|
|
|
61
66
|
window.open(downloadUrl, '_blank');
|
|
62
67
|
```
|
|
63
68
|
|
|
64
|
-
Both routes validate `bucketName` with `isValidName()` and `objectName` with `isValidPath()
|
|
69
|
+
Both routes validate `bucketName` with `isValidName()` and `objectName` with `isValidPath()`. Both then forward a fixed whitelist of metadata headers - `content-type`, `content-encoding`, `cache-control`, `etag`, `last-modified` - plus `X-Content-Type-Options: nosniff`. See [Header Sanitization](./api#header-sanitization) for the full list and why it exists.
|
|
65
70
|
|
|
66
71
|
> [!TIP]
|
|
67
|
-
> `objectName` may embed folder segments
|
|
72
|
+
> `objectName` may embed folder segments, for example `invoices/2026/document.pdf`. Always pass the whole thing through `encodeURIComponent()`. Hono decodes it exactly once before the handler reads it - a second `decodeURIComponent()` on your end is wrong. It can corrupt names that contain a literal `%`.
|
|
68
73
|
|
|
69
74
|
## List objects in a bucket
|
|
70
75
|
|
|
@@ -92,7 +97,7 @@ const { success } = await fetch(
|
|
|
92
97
|
```
|
|
93
98
|
|
|
94
99
|
> [!NOTE]
|
|
95
|
-
> When MetaLink is enabled, the database record deletion is **fire-and-forget
|
|
100
|
+
> When MetaLink is enabled, the database record deletion is **fire-and-forget**. The response returns as soon as the storage delete completes - it does not wait on the `deleteAll()` call. Deletion errors are logged but never fail the request.
|
|
96
101
|
|
|
97
102
|
## Sync a MetaLink record manually
|
|
98
103
|
|
|
@@ -184,7 +189,7 @@ export class Application extends BaseApplication {
|
|
|
184
189
|
|
|
185
190
|
### Custom MetaLink creation
|
|
186
191
|
|
|
187
|
-
Provide `createMetaLink` on `TMetaLinkConfig` to fully replace the default insert -
|
|
192
|
+
Provide `createMetaLink` on `TMetaLinkConfig` to fully replace the default insert - for example to add extra fields or run validation before persisting.
|
|
188
193
|
|
|
189
194
|
```typescript
|
|
190
195
|
metaLink: {
|
|
@@ -69,9 +69,9 @@ When documenting a component, find source material here:
|
|
|
69
69
|
|
|
70
70
|
| What | Path |
|
|
71
71
|
|------|------|
|
|
72
|
-
| Binding keys | `packages/core/src/components/{name}/common/keys.ts` |
|
|
73
|
-
| Config types | `packages/core/src/components/{name}/common/types.ts` |
|
|
74
|
-
| Error messages | `packages/core/src/components/{name}/component.ts` -- look for `throw getError()` |
|
|
72
|
+
| Binding keys | `packages/core-server/src/components/{name}/common/keys.ts` |
|
|
73
|
+
| Config types | `packages/core-server/src/components/{name}/common/types.ts` |
|
|
74
|
+
| Error messages | `packages/core-server/src/components/{name}/component.ts` -- look for `throw getError()` |
|
|
75
75
|
| Helper source | `packages/helpers/src/helpers/{name}/` |
|
|
76
76
|
|
|
77
77
|
## Callout Standard
|
|
@@ -10,9 +10,9 @@ Every binding key, configuration option, callback signature, and internal mechan
|
|
|
10
10
|
|
|
11
11
|
**Files:**
|
|
12
12
|
|
|
13
|
-
- [`packages/core/src/components/websocket/component.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/websocket/component.ts)
|
|
14
|
-
- [`packages/core/src/components/websocket/common/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/websocket/common/types.ts)
|
|
15
|
-
- [`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)
|
|
13
|
+
- [`packages/core-server/src/components/websocket/component.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/websocket/component.ts)
|
|
14
|
+
- [`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)
|
|
15
|
+
- [`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)
|
|
16
16
|
- [`packages/helpers/src/modules/socket/websocket/server/helper.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/socket/websocket/server/helper.ts)
|
|
17
17
|
- [`packages/helpers/src/modules/socket/websocket/emitter/helper.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/socket/websocket/emitter/helper.ts)
|
|
18
18
|
|
|
@@ -65,7 +65,7 @@ import type {
|
|
|
65
65
|
|
|
66
66
|
## Configuration
|
|
67
67
|
|
|
68
|
-
`WebSocketComponent`'s own `IServerOptions` interface is a **subset** of the helper's `IWebSocketServerOptions
|
|
68
|
+
`WebSocketComponent`'s own `IServerOptions` interface is a **subset** of the helper's `IWebSocketServerOptions`. Before constructing the helper, the component fills in `server` and `redisConnection` from the running application, plus every callback function resolved from the DI container. It does not forward `authTimeout` or `encryptedBatchLimit` - see the note below.
|
|
69
69
|
|
|
70
70
|
```typescript
|
|
71
71
|
interface IServerOptions {
|
|
@@ -162,7 +162,7 @@ Passed straight through to Bun's native WebSocket handler via `serverOptions` in
|
|
|
162
162
|
| `CLIENT_CONNECTED_HANDLER` | `TWebSocketClientConnectedFn` | No | Called after successful authentication |
|
|
163
163
|
| `CLIENT_DISCONNECTED_HANDLER` | `TWebSocketClientDisconnectedFn` | No | Called on disconnect, after cleanup |
|
|
164
164
|
| `MESSAGE_HANDLER` | `TWebSocketMessageHandler` | No | Handles non-system messages from authenticated clients |
|
|
165
|
-
| `OUTBOUND_TRANSFORMER` | `TWebSocketOutboundTransformer` | No | Transforms outbound messages (
|
|
165
|
+
| `OUTBOUND_TRANSFORMER` | `TWebSocketOutboundTransformer` | No | Transforms outbound messages (for example per-client encryption) |
|
|
166
166
|
| `HANDSHAKE_HANDLER` | `TWebSocketHandshakeFn` | When `requireEncryption: true` | Returns <code v-pre>{ serverPublicKey, salt }</code> or `null`/`false` to reject |
|
|
167
167
|
|
|
168
168
|
```typescript
|
|
@@ -205,14 +205,14 @@ type TWebSocketHandshakeFn<AuthDataType extends Record<string, unknown> = Record
|
|
|
205
205
|
|
|
206
206
|
- **`VALIDATE_ROOM_HANDLER` receives sanitized rooms.** Internal `ws:`-prefixed rooms are already filtered out before this callback runs. Without it bound, **all** join requests are rejected.
|
|
207
207
|
- **`CLIENT_CONNECTED_HANDLER` / `CLIENT_DISCONNECTED_HANDLER` errors are caught and logged**, never thrown - a broken hook cannot disconnect a client or crash the server.
|
|
208
|
-
- **`MESSAGE_HANDLER` only sees non-system events
|
|
208
|
+
- **`MESSAGE_HANDLER` only sees non-system events.** `authenticate`, `connected`, `disconnect`, `join`, `leave`, `error`, `heartbeat`, and `encrypted` are all handled internally. Unbound, non-system messages are silently dropped.
|
|
209
209
|
- **`OUTBOUND_TRANSFORMER` only runs for encrypted clients** (`client.encrypted === true`). Non-encrypted clients bypass it entirely - zero overhead until encryption is enabled.
|
|
210
210
|
|
|
211
211
|
## Architecture
|
|
212
212
|
|
|
213
213
|
### Lifecycle integration
|
|
214
214
|
|
|
215
|
-
The component uses the application's **post-start hook** system to solve a timing problem
|
|
215
|
+
The component uses the application's **post-start hook** system to solve a timing problem. WebSocket needs a running Bun server instance, but components initialize before the server starts.
|
|
216
216
|
|
|
217
217
|
```
|
|
218
218
|
preConfigure() <- register WebSocketComponent here
|
|
@@ -252,7 +252,7 @@ function createBunFetchHandler(opts: {
|
|
|
252
252
|
|
|
253
253
|
## `WebSocketEmitter` API
|
|
254
254
|
|
|
255
|
-
Standalone, lightweight Redis-only publisher for processes that do not run a `WebSocketServerHelper`. Extends `BaseHelper
|
|
255
|
+
Standalone, lightweight Redis-only publisher for processes that do not run a `WebSocketServerHelper`. Extends `BaseHelper`. Uses a single Redis pub client.
|
|
256
256
|
|
|
257
257
|
```typescript
|
|
258
258
|
interface IWebSocketEmitterOptions {
|
|
@@ -261,8 +261,11 @@ interface IWebSocketEmitterOptions {
|
|
|
261
261
|
}
|
|
262
262
|
```
|
|
263
263
|
|
|
264
|
-
- **Constructor
|
|
265
|
-
-
|
|
264
|
+
- **Constructor:**
|
|
265
|
+
- Calls `super({ scope })`.
|
|
266
|
+
- Throws `"Invalid redis connection!"` if `redisConnection` is falsy.
|
|
267
|
+
- Calls `redisConnection.duplicateClient()` to create an isolated pub client.
|
|
268
|
+
- **`EMITTER_SERVER_ID = 'emitter'`.** Every message the emitter publishes carries this fixed `serverId`. No `WebSocketServerHelper` ever has this ID - they use `crypto.randomUUID()` - so no server self-dedups an emitter message.
|
|
266
269
|
|
|
267
270
|
| Method | Signature | Behavior |
|
|
268
271
|
|--------|-----------|----------|
|
|
@@ -282,7 +285,7 @@ Reads all binding keys and validates the required ones, throwing before the post
|
|
|
282
285
|
| Binding | Validation | Error on failure |
|
|
283
286
|
|---------|-----------|------------------|
|
|
284
287
|
| `SERVER_OPTIONS` | Optional, merged with `DEFAULT_SERVER_OPTIONS` via `Object.assign()` | -- |
|
|
285
|
-
| `REDIS_CONNECTION` | Must
|
|
288
|
+
| `REDIS_CONNECTION` | Must pass `isRedisHelper()` | `"Invalid instance of redisConnection ..."` |
|
|
286
289
|
| `AUTHENTICATE_HANDLER` | Must be truthy | `"Invalid authenticateFn to setup WebSocket server!"` |
|
|
287
290
|
| `VALIDATE_ROOM_HANDLER` / `CLIENT_CONNECTED_HANDLER` / `CLIENT_DISCONNECTED_HANDLER` / `MESSAGE_HANDLER` / `OUTBOUND_TRANSFORMER` / `HANDSHAKE_HANDLER` | Optional, `null` coerced to `undefined` | -- |
|
|
288
291
|
|
|
@@ -290,7 +293,7 @@ Reads all binding keys and validates the required ones, throwing before the post
|
|
|
290
293
|
|
|
291
294
|
Registers the `websocket-initialize` post-start hook:
|
|
292
295
|
|
|
293
|
-
1. Gets the Bun server instance
|
|
296
|
+
1. Gets the Bun server instance via `getServerInstance()` and the Hono server via `getServer()`. Throws `"[WebSocketComponent] Bun server instance not available!"` if the Bun instance is missing.
|
|
294
297
|
2. Constructs `WebSocketServerHelper` with all resolved bindings plus the running server instance.
|
|
295
298
|
3. Awaits `wsHelper.configure()` - connects Redis clients, sets up subscriptions, starts the heartbeat timer.
|
|
296
299
|
4. Binds the helper to `WEBSOCKET_INSTANCE`.
|
|
@@ -314,10 +317,21 @@ if (runtime === RuntimeModules.NODE) {
|
|
|
314
317
|
| Callback | Responsibility |
|
|
315
318
|
|----------|---------------|
|
|
316
319
|
| `open` | Creates the `IWebSocketClient` entry in state `UNAUTHORIZED`, subscribes the socket to its own `clientId` topic, starts the auth timer (skips if `clientId` already exists) |
|
|
317
|
-
| `message` | Updates `lastActivity
|
|
320
|
+
| `message` | Updates `lastActivity`, then routes the parsed event - see the table below |
|
|
318
321
|
| `close` | Clears the auth timer, removes the client from `users`/`rooms`/`clients`, invokes `clientDisconnectedFn` (errors caught and logged) |
|
|
319
322
|
| `drain` | Resets `client.backpressured = false` |
|
|
320
323
|
|
|
324
|
+
The `message` callback routes each event like this:
|
|
325
|
+
|
|
326
|
+
| Event | Handling |
|
|
327
|
+
|-------|----------|
|
|
328
|
+
| Not valid JSON | Sends an `error` event back to the client |
|
|
329
|
+
| `heartbeat` | No-op - `lastActivity` was already updated |
|
|
330
|
+
| `authenticate` | Runs the authentication flow |
|
|
331
|
+
| Any other event, client not yet authenticated | Sends `error: "Not authenticated"` |
|
|
332
|
+
| `join` / `leave` | Runs the room join/leave flow |
|
|
333
|
+
| Any other event, client authenticated | Forwarded to `messageHandler` |
|
|
334
|
+
|
|
321
335
|
### `deliverToSocket()` backpressure handling
|
|
322
336
|
|
|
323
337
|
| `socket.send()` return | Meaning | Action |
|
|
@@ -343,25 +357,42 @@ send({ destination, payload: { topic, data } })
|
|
|
343
357
|
|
|
344
358
|
### Room join / leave validation
|
|
345
359
|
|
|
346
|
-
- **Server-side sanitization
|
|
347
|
-
|
|
348
|
-
|
|
360
|
+
- **Server-side sanitization always applies:**
|
|
361
|
+
|
|
362
|
+
| Rule | Requirement |
|
|
363
|
+
|------|-------------|
|
|
364
|
+
| Type | Non-empty string |
|
|
365
|
+
| Length | At most 256 characters |
|
|
366
|
+
| Prefix | Must not start with the reserved `ws:` prefix |
|
|
367
|
+
|
|
368
|
+
- **`validateRoomFn` gates joins.** Only sanitized rooms reach it. It returns the subset the client may actually join. If unbound, every join is rejected with a warning log.
|
|
369
|
+
- **Leave is filtered against joined rooms.** `handleLeave()` computes `rooms.filter(r => client.rooms.has(r))` before leaving. A client can never unsubscribe from a room it never joined - or an internal topic it was auto-subscribed to.
|
|
370
|
+
- If nothing remains after filtering, the leave is silently ignored.
|
|
349
371
|
|
|
350
372
|
### Graceful shutdown
|
|
351
373
|
|
|
374
|
+
`WebSocketServerHelper` has a real `shutdown()` method, but `WebSocketComponent` never calls it automatically. Components have no `stop()` lifecycle, and the component does not register a post-stop hook for you.
|
|
375
|
+
|
|
376
|
+
To disconnect clients cleanly when your application stops, register your own hook with `registerPostStopHook()` - the same mechanism IGNIS's secrets provider uses internally:
|
|
377
|
+
|
|
352
378
|
```typescript
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
379
|
+
export class Application extends BaseApplication {
|
|
380
|
+
preConfigure(): ValueOrPromise<void> {
|
|
381
|
+
// ... bind REDIS_CONNECTION, AUTHENTICATE_HANDLER, register WebSocketComponent ...
|
|
382
|
+
|
|
383
|
+
this.registerPostStopHook({
|
|
384
|
+
identifier: 'websocket-shutdown',
|
|
385
|
+
hook: async () => {
|
|
386
|
+
const wsHelper = this.get<WebSocketServerHelper>({
|
|
387
|
+
key: WebSocketBindingKeys.WEBSOCKET_INSTANCE,
|
|
388
|
+
isOptional: true,
|
|
389
|
+
});
|
|
390
|
+
if (wsHelper) {
|
|
391
|
+
await wsHelper.shutdown();
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
});
|
|
363
395
|
}
|
|
364
|
-
await super.stop();
|
|
365
396
|
}
|
|
366
397
|
```
|
|
367
398
|
|
|
@@ -32,7 +32,7 @@ Every error condition the WebSocket component and helper can raise, plus fixes f
|
|
|
32
32
|
|--------|-----------|---------------|
|
|
33
33
|
| `binding()` | `application` is falsy | `"[binding] Invalid application to bind WebSocketComponent"` |
|
|
34
34
|
| `binding()` | Node.js runtime detected | `"[WebSocketComponent] Node.js runtime is not supported yet. Please use Bun runtime."` |
|
|
35
|
-
| `resolveBindings()` | `REDIS_CONNECTION`
|
|
35
|
+
| `resolveBindings()` | `REDIS_CONNECTION` fails `isRedisHelper()` | `"[WebSocketComponent][resolveBindings] Invalid instance of redisConnection ..."` |
|
|
36
36
|
| `resolveBindings()` | `AUTHENTICATE_HANDLER` is falsy | `"[WebSocketComponent] Invalid authenticateFn to setup WebSocket server!"` |
|
|
37
37
|
| `registerBunHook()` | Bun server instance not available | `"[WebSocketComponent] Bun server instance not available!"` |
|
|
38
38
|
|
|
@@ -40,12 +40,12 @@ Every error condition the WebSocket component and helper can raise, plus fixes f
|
|
|
40
40
|
|
|
41
41
|
### "WebSocket not initialized"
|
|
42
42
|
|
|
43
|
-
- **Cause:** `WebSocketServerHelper` was accessed before the server started -
|
|
43
|
+
- **Cause:** `WebSocketServerHelper` was accessed before the server started - for example during DI construction.
|
|
44
44
|
- **Fix:** Use the lazy getter pattern from [Usage & Examples](./usage). Never `@inject` `WEBSOCKET_INSTANCE` in a constructor - it does not exist yet at that point.
|
|
45
45
|
|
|
46
46
|
### "Invalid instance of redisConnection"
|
|
47
47
|
|
|
48
|
-
- **Cause:** The value bound to `REDIS_CONNECTION` is not an `AbstractRedisHelper` instance
|
|
48
|
+
- **Cause:** The value bound to `REDIS_CONNECTION` is not an `AbstractRedisHelper` instance. It is not a `RedisSingleHelper`, `RedisClusterHelper`, or `RedisSentinelHelper`.
|
|
49
49
|
- **Fix:** Bind one of the concrete topology helpers, not a raw `ioredis` client.
|
|
50
50
|
|
|
51
51
|
```typescript
|