@venizia/ignis-docs 0.0.5 → 0.0.6-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 (98) hide show
  1. package/package.json +1 -1
  2. package/wiki/best-practices/architecture-decisions.md +0 -8
  3. package/wiki/best-practices/code-style-standards/control-flow.md +1 -1
  4. package/wiki/best-practices/performance-optimization.md +3 -3
  5. package/wiki/best-practices/security-guidelines.md +2 -2
  6. package/wiki/best-practices/troubleshooting-tips.md +1 -1
  7. package/wiki/guides/core-concepts/components-guide.md +1 -1
  8. package/wiki/guides/core-concepts/components.md +2 -2
  9. package/wiki/guides/core-concepts/dependency-injection.md +1 -1
  10. package/wiki/guides/core-concepts/services.md +1 -1
  11. package/wiki/guides/tutorials/building-a-crud-api.md +1 -1
  12. package/wiki/guides/tutorials/ecommerce-api.md +2 -2
  13. package/wiki/guides/tutorials/realtime-chat.md +6 -6
  14. package/wiki/guides/tutorials/testing.md +1 -1
  15. package/wiki/references/base/bootstrapping.md +0 -2
  16. package/wiki/references/base/components.md +2 -2
  17. package/wiki/references/base/controllers.md +0 -1
  18. package/wiki/references/base/datasources.md +1 -1
  19. package/wiki/references/base/dependency-injection.md +1 -1
  20. package/wiki/references/base/filter-system/quick-reference.md +0 -14
  21. package/wiki/references/base/middlewares.md +0 -8
  22. package/wiki/references/base/providers.md +0 -9
  23. package/wiki/references/base/services.md +0 -1
  24. package/wiki/references/components/authentication/api.md +444 -0
  25. package/wiki/references/components/authentication/errors.md +177 -0
  26. package/wiki/references/components/authentication/index.md +571 -0
  27. package/wiki/references/components/authentication/usage.md +781 -0
  28. package/wiki/references/components/health-check.md +292 -103
  29. package/wiki/references/components/index.md +14 -12
  30. package/wiki/references/components/mail/api.md +505 -0
  31. package/wiki/references/components/mail/errors.md +176 -0
  32. package/wiki/references/components/mail/index.md +535 -0
  33. package/wiki/references/components/mail/usage.md +404 -0
  34. package/wiki/references/components/request-tracker.md +229 -25
  35. package/wiki/references/components/socket-io/api.md +1051 -0
  36. package/wiki/references/components/socket-io/errors.md +119 -0
  37. package/wiki/references/components/socket-io/index.md +410 -0
  38. package/wiki/references/components/socket-io/usage.md +322 -0
  39. package/wiki/references/components/static-asset/api.md +261 -0
  40. package/wiki/references/components/static-asset/errors.md +89 -0
  41. package/wiki/references/components/static-asset/index.md +617 -0
  42. package/wiki/references/components/static-asset/usage.md +364 -0
  43. package/wiki/references/components/swagger.md +390 -110
  44. package/wiki/references/components/template/api-page.md +125 -0
  45. package/wiki/references/components/template/errors-page.md +100 -0
  46. package/wiki/references/components/template/index.md +104 -0
  47. package/wiki/references/components/template/setup-page.md +134 -0
  48. package/wiki/references/components/template/single-page.md +132 -0
  49. package/wiki/references/components/template/usage-page.md +127 -0
  50. package/wiki/references/components/websocket/api.md +508 -0
  51. package/wiki/references/components/websocket/errors.md +123 -0
  52. package/wiki/references/components/websocket/index.md +453 -0
  53. package/wiki/references/components/websocket/usage.md +475 -0
  54. package/wiki/references/helpers/cron/index.md +224 -0
  55. package/wiki/references/helpers/crypto/index.md +537 -0
  56. package/wiki/references/helpers/env/index.md +214 -0
  57. package/wiki/references/helpers/error/index.md +232 -0
  58. package/wiki/references/helpers/index.md +16 -15
  59. package/wiki/references/helpers/inversion/index.md +608 -0
  60. package/wiki/references/helpers/logger/index.md +600 -0
  61. package/wiki/references/helpers/network/api.md +986 -0
  62. package/wiki/references/helpers/network/index.md +620 -0
  63. package/wiki/references/helpers/queue/index.md +589 -0
  64. package/wiki/references/helpers/redis/index.md +495 -0
  65. package/wiki/references/helpers/socket-io/api.md +497 -0
  66. package/wiki/references/helpers/socket-io/index.md +513 -0
  67. package/wiki/references/helpers/storage/api.md +705 -0
  68. package/wiki/references/helpers/storage/index.md +583 -0
  69. package/wiki/references/helpers/template/index.md +66 -0
  70. package/wiki/references/helpers/template/single-page.md +126 -0
  71. package/wiki/references/helpers/testing/index.md +510 -0
  72. package/wiki/references/helpers/types/index.md +512 -0
  73. package/wiki/references/helpers/uid/index.md +272 -0
  74. package/wiki/references/helpers/websocket/api.md +736 -0
  75. package/wiki/references/helpers/websocket/index.md +574 -0
  76. package/wiki/references/helpers/worker-thread/index.md +470 -0
  77. package/wiki/references/quick-reference.md +3 -18
  78. package/wiki/references/utilities/jsx.md +1 -8
  79. package/wiki/references/utilities/statuses.md +0 -7
  80. package/wiki/references/components/authentication.md +0 -476
  81. package/wiki/references/components/mail.md +0 -687
  82. package/wiki/references/components/socket-io.md +0 -562
  83. package/wiki/references/components/static-asset.md +0 -1277
  84. package/wiki/references/helpers/cron.md +0 -108
  85. package/wiki/references/helpers/crypto.md +0 -132
  86. package/wiki/references/helpers/env.md +0 -83
  87. package/wiki/references/helpers/error.md +0 -97
  88. package/wiki/references/helpers/inversion.md +0 -176
  89. package/wiki/references/helpers/logger.md +0 -296
  90. package/wiki/references/helpers/network.md +0 -396
  91. package/wiki/references/helpers/queue.md +0 -150
  92. package/wiki/references/helpers/redis.md +0 -142
  93. package/wiki/references/helpers/socket-io.md +0 -932
  94. package/wiki/references/helpers/storage.md +0 -665
  95. package/wiki/references/helpers/testing.md +0 -133
  96. package/wiki/references/helpers/types.md +0 -167
  97. package/wiki/references/helpers/uid.md +0 -167
  98. package/wiki/references/helpers/worker-thread.md +0 -178
@@ -0,0 +1,781 @@
1
+ # Authentication -- Usage & Examples
2
+
3
+ > Securing routes, authentication flows, entity helpers, and API endpoint specifications. See [Setup & Configuration](./) for initial setup.
4
+
5
+ ## Securing Routes
6
+
7
+ Use `authStrategies` and `authMode` in route configurations:
8
+
9
+ ```typescript
10
+ // Single strategy
11
+ const SECURE_ROUTE_CONFIG = {
12
+ path: '/secure-data',
13
+ method: HTTP.Methods.GET,
14
+ authStrategies: [Authentication.STRATEGY_JWT],
15
+ responses: jsonResponse({
16
+ description: 'Protected data',
17
+ schema: z.object({ message: z.string() }),
18
+ }),
19
+ } as const;
20
+
21
+ // Multiple strategies with fallback (any mode)
22
+ const FALLBACK_AUTH_CONFIG = {
23
+ path: '/api/data',
24
+ method: HTTP.Methods.GET,
25
+ authStrategies: [Authentication.STRATEGY_JWT, Authentication.STRATEGY_BASIC],
26
+ authMode: 'any',
27
+ responses: jsonResponse({
28
+ description: 'Data accessible via JWT or Basic auth',
29
+ schema: z.object({ data: z.any() }),
30
+ }),
31
+ } as const;
32
+
33
+ // Skip authentication
34
+ const PUBLIC_ROUTE_CONFIG = {
35
+ path: '/public',
36
+ method: HTTP.Methods.GET,
37
+ skipAuth: true,
38
+ responses: jsonResponse({
39
+ description: 'Public endpoint',
40
+ schema: z.object({ message: z.string() }),
41
+ }),
42
+ } as const;
43
+ ```
44
+
45
+ ## Using the `authenticate()` Standalone Function
46
+
47
+ The `authenticate()` function is a convenience wrapper around `AuthenticationStrategyRegistry.getInstance().authenticate()`. It returns a Hono `MiddlewareHandler` suitable for direct middleware usage:
48
+
49
+ ```typescript
50
+ import { authenticate, Authentication } from '@venizia/ignis';
51
+
52
+ // Use as Hono middleware directly
53
+ const authMiddleware = authenticate({
54
+ strategies: [Authentication.STRATEGY_JWT],
55
+ mode: 'any',
56
+ });
57
+
58
+ // Apply to a Hono route
59
+ app.get('/protected', authMiddleware, (c) => {
60
+ const user = c.get(Authentication.CURRENT_USER);
61
+ return c.json({ userId: user.userId });
62
+ });
63
+ ```
64
+
65
+ ## Accessing the Current User
66
+
67
+ After authentication, the user payload is available on the Hono `Context`:
68
+
69
+ ```typescript
70
+ import { Context } from 'hono';
71
+ import { Authentication, IJWTTokenPayload } from '@venizia/ignis';
72
+
73
+ // Inside a route handler
74
+ const user = c.get(Authentication.CURRENT_USER) as IJWTTokenPayload | undefined;
75
+
76
+ if (user) {
77
+ console.log('Authenticated user ID:', user.userId);
78
+ console.log('User roles:', user.roles);
79
+ }
80
+ ```
81
+
82
+ ## Dynamic Skip Authentication
83
+
84
+ Use `Authentication.SKIP_AUTHENTICATION` to dynamically skip auth in middleware:
85
+
86
+ ```typescript
87
+ import { Authentication } from '@venizia/ignis';
88
+ import { createMiddleware } from 'hono/factory';
89
+
90
+ const conditionalAuthMiddleware = createMiddleware(async (c, next) => {
91
+ if (c.req.header('X-API-Key') === 'valid-api-key') {
92
+ c.set(Authentication.SKIP_AUTHENTICATION, true);
93
+ }
94
+ return next();
95
+ });
96
+ ```
97
+
98
+ ## Implementing an AuthenticationService
99
+
100
+ The `AuthenticateComponent` depends on a service implementing the `IAuthService` interface when using the built-in auth controller:
101
+
102
+ ```typescript
103
+ import {
104
+ BaseService,
105
+ inject,
106
+ IAuthService,
107
+ IJWTTokenPayload,
108
+ JWTTokenService,
109
+ TSignInRequest,
110
+ getError,
111
+ } from '@venizia/ignis';
112
+ import { Context } from 'hono';
113
+
114
+ export class AuthenticationService extends BaseService implements IAuthService {
115
+ constructor(
116
+ @inject({ key: 'services.JWTTokenService' })
117
+ private _jwtTokenService: JWTTokenService,
118
+ ) {
119
+ super({ scope: AuthenticationService.name });
120
+ }
121
+
122
+ async signIn(context: Context, opts: TSignInRequest): Promise<{ token: string }> {
123
+ const { identifier, credential } = opts;
124
+ const user = await this.userRepo.findByIdentifier(identifier);
125
+
126
+ if (!user || !await this.verifyCredential(credential, user)) {
127
+ throw getError({ message: 'Invalid credentials' });
128
+ }
129
+
130
+ const payload: IJWTTokenPayload = {
131
+ userId: user.id,
132
+ roles: user.roles,
133
+ };
134
+
135
+ const token = await this._jwtTokenService.generate({ payload });
136
+ return { token };
137
+ }
138
+
139
+ async signUp(context: Context, opts: any): Promise<any> {
140
+ // Implement your sign-up logic
141
+ }
142
+
143
+ async changePassword(context: Context, opts: any): Promise<any> {
144
+ // Implement your change password logic
145
+ }
146
+ }
147
+ ```
148
+
149
+ ## Entity Column Helpers
150
+
151
+ The authentication module provides a set of **column helper functions** designed to be spread into Drizzle `pgTable()` definitions. These functions return pre-configured column objects for common auth-related entities, saving you from manually defining columns for users, roles, permissions, and their relationships.
152
+
153
+ ### Pattern
154
+
155
+ Each helper function returns an object of Drizzle column builders that you spread into your `pgTable()` call alongside any custom columns:
156
+
157
+ ```typescript
158
+ import { pgTable, serial, text } from 'drizzle-orm/pg-core';
159
+ import {
160
+ extraUserColumns,
161
+ extraRoleColumns,
162
+ extraPermissionColumns,
163
+ extraPermissionMappingColumns,
164
+ extraUserRoleColumns,
165
+ } from '@venizia/ignis';
166
+ import { withSerialId, withTimestamps } from '@venizia/ignis';
167
+
168
+ // User table with auth columns
169
+ export const users = pgTable('users', {
170
+ ...withSerialId(),
171
+ ...withTimestamps(),
172
+ ...extraUserColumns(),
173
+ username: text('username').unique().notNull(),
174
+ passwordHash: text('password_hash').notNull(),
175
+ email: text('email').unique(),
176
+ });
177
+
178
+ // Role table with auth columns
179
+ export const roles = pgTable('roles', {
180
+ ...withSerialId(),
181
+ ...withTimestamps(),
182
+ ...extraRoleColumns(),
183
+ });
184
+
185
+ // Permission table
186
+ export const permissions = pgTable('permissions', {
187
+ ...withSerialId(),
188
+ ...withTimestamps(),
189
+ ...extraPermissionColumns(),
190
+ });
191
+
192
+ // Permission mapping (role-to-permission or user-to-permission)
193
+ export const permissionMappings = pgTable('permission_mappings', {
194
+ ...withSerialId(),
195
+ ...extraPermissionMappingColumns(),
196
+ });
197
+
198
+ // User-role junction table
199
+ export const userRoles = pgTable('user_roles', {
200
+ ...withSerialId(),
201
+ ...extraUserRoleColumns(),
202
+ });
203
+ ```
204
+
205
+ ### extraUserColumns
206
+
207
+ Returns columns for user-related fields with status and type defaults from `UserStatuses` and `UserTypes`.
208
+
209
+ ```typescript
210
+ extraUserColumns(opts?: { idType: 'string' | 'number' })
211
+ ```
212
+
213
+ | Column | Type | Default | Description |
214
+ |--------|------|---------|-------------|
215
+ | `realm` | `text` | `''` | Multi-tenancy realm identifier |
216
+ | `status` | `text` | `UserStatuses.UNKNOWN` (`'000_UNKNOWN'`) | User status |
217
+ | `type` | `text` | `UserTypes.SYSTEM` (`'SYSTEM'`) | User type |
218
+ | `activatedAt` | `timestamp (tz)` | `null` | Activation timestamp |
219
+ | `lastLoginAt` | `timestamp (tz)` | `null` | Last login timestamp |
220
+ | `parentId` | `text` or `integer` | `null` | Parent user ID (type depends on `idType`) |
221
+
222
+ ### extraRoleColumns
223
+
224
+ Returns columns for role definitions. No options parameter.
225
+
226
+ ```typescript
227
+ extraRoleColumns()
228
+ ```
229
+
230
+ | Column | Type | Default | Description |
231
+ |--------|------|---------|-------------|
232
+ | `identifier` | `text` | -- | Unique role identifier (e.g., `'admin'`, `'user'`) |
233
+ | `name` | `text` | -- | Human-readable role name |
234
+ | `description` | `text` | `null` | Optional role description |
235
+ | `priority` | `integer` | -- | Role priority (lower = higher priority) |
236
+ | `status` | `text` | `RoleStatuses.ACTIVATED` (`'201_ACTIVATED'`) | Role status |
237
+
238
+ ### extraPermissionColumns
239
+
240
+ Returns columns for permission definitions. Supports `idType` option for the `parentId` column type.
241
+
242
+ ```typescript
243
+ extraPermissionColumns(opts?: { idType: 'string' | 'number' })
244
+ ```
245
+
246
+ | Column | Type | Default | Description |
247
+ |--------|------|---------|-------------|
248
+ | `code` | `text` | -- | Unique permission code |
249
+ | `name` | `text` | -- | Permission name |
250
+ | `subject` | `text` | -- | Permission subject (e.g., `'User'`, `'Order'`) |
251
+ | `pType` | `text` | -- | Permission type (maps to DB column `p_type`) |
252
+ | `action` | `text` | -- | Permitted action (e.g., `'read'`, `'write'`) |
253
+ | `scope` | `text` | -- | Permission scope |
254
+ | `parentId` | `text` or `integer` | `null` | Parent permission ID |
255
+
256
+ ### extraPermissionMappingColumns
257
+
258
+ Returns columns for mapping permissions to users or roles.
259
+
260
+ ```typescript
261
+ extraPermissionMappingColumns(opts?: { idType: 'string' | 'number' })
262
+ ```
263
+
264
+ | Column | Type | Default | Description |
265
+ |--------|------|---------|-------------|
266
+ | `effect` | `text` | `null` | Permission effect (e.g., `'allow'`, `'deny'`) |
267
+ | `userId` | `text` or `integer` | `null` | Associated user ID |
268
+ | `roleId` | `text` or `integer` | `null` | Associated role ID |
269
+ | `permissionId` | `text` or `integer` | -- | Associated permission ID (not null) |
270
+
271
+ ### extraUserRoleColumns
272
+
273
+ Returns columns for the user-role junction table. Includes principal columns (polymorphic type/ID fields) via `generatePrincipalColumnDefs` with a default polymorphic value of `'Role'`.
274
+
275
+ ```typescript
276
+ extraUserRoleColumns(opts?: { idType: 'string' | 'number' })
277
+ ```
278
+
279
+ | Column | Type | Default | Description |
280
+ |--------|------|---------|-------------|
281
+ | _(principal columns)_ | _various_ | -- | Polymorphic columns from `generatePrincipalColumnDefs` |
282
+ | `userId` | `text` or `integer` | -- | Associated user ID (not null) |
283
+
284
+ ### ID Type Polymorphism
285
+
286
+ All column helpers that accept `opts.idType` default to `'number'` (producing `integer` columns). Pass `'string'` to use `text` columns instead:
287
+
288
+ ```typescript
289
+ // Number IDs (default) -- uses integer columns for FK references
290
+ extraUserColumns()
291
+ extraPermissionColumns()
292
+
293
+ // String IDs (e.g., UUID) -- uses text columns for FK references
294
+ extraUserColumns({ idType: 'string' })
295
+ extraPermissionColumns({ idType: 'string' })
296
+ ```
297
+
298
+ ## Status Constants
299
+
300
+ The authentication module uses status classes from `@/common/statuses`. These extend `CommonStatuses` and provide lifecycle state management for auth entities.
301
+
302
+ ### UserStatuses
303
+
304
+ Inherits all statuses from `CommonStatuses`:
305
+
306
+ | Constant | Value | Description |
307
+ |----------|-------|-------------|
308
+ | `UserStatuses.UNKNOWN` | `'000_UNKNOWN'` | Initial/unverified state |
309
+ | `UserStatuses.ACTIVATED` | `'201_ACTIVATED'` | Active user |
310
+ | `UserStatuses.DEACTIVATED` | `'401_DEACTIVATED'` | Deactivated user |
311
+ | `UserStatuses.BLOCKED` | `'403_BLOCKED'` | Blocked user |
312
+ | `UserStatuses.ARCHIVED` | `'405_ARCHIVED'` | Archived user |
313
+
314
+ ### UserTypes
315
+
316
+ | Constant | Value | Description |
317
+ |----------|-------|-------------|
318
+ | `UserTypes.SYSTEM` | `'SYSTEM'` | System-created user (default) |
319
+ | `UserTypes.LINKED` | `'LINKED'` | Linked/external user |
320
+
321
+ ### RoleStatuses
322
+
323
+ Inherits all statuses from `CommonStatuses` (same values as `UserStatuses`):
324
+
325
+ | Constant | Value | Description |
326
+ |----------|-------|-------------|
327
+ | `RoleStatuses.UNKNOWN` | `'000_UNKNOWN'` | Initial state |
328
+ | `RoleStatuses.ACTIVATED` | `'201_ACTIVATED'` | Active role (default for `extraRoleColumns`) |
329
+ | `RoleStatuses.DEACTIVATED` | `'401_DEACTIVATED'` | Deactivated role |
330
+ | `RoleStatuses.BLOCKED` | `'403_BLOCKED'` | Blocked role |
331
+ | `RoleStatuses.ARCHIVED` | `'405_ARCHIVED'` | Archived role |
332
+
333
+ ## Auth Flows
334
+
335
+ ### JWT Authentication Flow
336
+
337
+ 1. **Client sends request** with <code v-pre>Authorization: Bearer &lt;token&gt;</code> header
338
+ 2. **JWTAuthenticationStrategy.authenticate()** is called by the Hono middleware
339
+ 3. **JWTTokenService.extractCredentials()** extracts the token from the Authorization header
340
+ 4. **JWTTokenService.verify()** verifies the JWT signature using `jose.jwtVerify()`
341
+ 5. **JWTTokenService.decryptPayload()** decrypts the AES-encrypted payload fields
342
+ 6. **User payload is set** on `context.get(Authentication.CURRENT_USER)`
343
+
344
+ > [!NOTE]
345
+ > JWT payloads are encrypted field-by-field for additional security. Standard JWT fields (`iss`, `sub`, `aud`, etc.) remain unencrypted, while custom fields like `userId` and `roles` are AES-encrypted.
346
+
347
+ ### Basic Authentication Flow
348
+
349
+ 1. **Client sends request** with <code v-pre>Authorization: Basic &lt;base64(username:password)&gt;</code> header
350
+ 2. **BasicAuthenticationStrategy.authenticate()** is called by the Hono middleware
351
+ 3. **BasicTokenService.extractCredentials()** decodes the Base64 credentials
352
+ 4. **BasicTokenService.verify()** calls the user-provided `verifyCredentials` callback
353
+ 5. **User payload is set** on `context.get(Authentication.CURRENT_USER)` if verification succeeds
354
+
355
+ > [!IMPORTANT]
356
+ > The `verifyCredentials` callback must perform all necessary validation (password hashing comparison, user lookup, etc.) and return an `IAuthUser` object or `null`.
357
+
358
+ ## Multi-Strategy Authentication
359
+
360
+ When multiple strategies are configured on a route via `authStrategies: ['jwt', 'basic']`:
361
+
362
+ **`any` mode (default):**
363
+ - Strategies are tried in the order specified
364
+ - The first successful strategy wins
365
+ - If all strategies fail, a `401 Unauthorized` error is thrown listing all tried strategies
366
+ - **Use case:** Fallback authentication (try JWT, fallback to Basic)
367
+
368
+ **`all` mode:**
369
+ - Every strategy must pass successfully
370
+ - If any strategy fails, the request is immediately rejected (exception propagates)
371
+ - The last strategy's user payload is used
372
+ - **Use case:** Multi-factor authentication (both JWT and Basic required)
373
+
374
+ > [!TIP]
375
+ > Use `'any'` mode for graceful fallback (e.g., allow mobile apps to use JWT while legacy systems use Basic). Use `'all'` mode for high-security endpoints requiring multiple forms of authentication.
376
+
377
+ ## Token Encryption
378
+
379
+ JWT payloads are encrypted field-by-field using AES (default `aes-256-cbc`) via the `@venizia/ignis-helpers` AES utility:
380
+
381
+ **Encryption process:**
382
+ 1. Standard JWT fields (`iss`, `sub`, `aud`, `jti`, `nbf`, `exp`, `iat`) are preserved as-is
383
+ 2. All other fields have both their **keys** and **values** AES-encrypted
384
+ 3. The `roles` field is serialized as `id|identifier|priority` pipe-separated strings before encryption
385
+ 4. `null` and `undefined` values are skipped during encryption
386
+
387
+ **Encryption code walkthrough:**
388
+
389
+ The `encryptPayload()` method processes each field:
390
+ 1. Standard JWT fields (`iss`, `sub`, `aud`, `jti`, `nbf`, `exp`, `iat`) are copied as-is
391
+ 2. `null`/`undefined` values are skipped entirely
392
+ 3. For the `roles` field: values are serialized as `"id|identifier|priority"` pipe-separated strings, then the array is JSON-stringified before encryption
393
+ 4. For all other fields: values are converted to string via template literal (<code v-pre>`${value}`</code>), then both key and value are AES-encrypted independently
394
+ 5. The encrypted key becomes the new field name, the encrypted value becomes its value
395
+
396
+ **Decryption process:**
397
+ 1. Standard JWT fields are extracted directly
398
+ 2. Encrypted fields have their keys decrypted first, then their values
399
+ 3. The `roles` field is deserialized: JSON-parsed to a string array, then each entry is split on `|` to reconstruct objects with `id`, `identifier`, and `priority` (where `priority` is converted to integer via `int()`)
400
+
401
+ > [!WARNING]
402
+ > The `applicationSecret` must remain constant across all instances of your application. Changing it will invalidate all existing tokens, as they cannot be decrypted with a different secret.
403
+
404
+ ## Hono Context Extension
405
+
406
+ The Authentication module extends Hono's `ContextVariableMap` to provide type-safe access to auth data:
407
+
408
+ ```typescript
409
+ declare module 'hono' {
410
+ interface ContextVariableMap<User extends IAuthUser = IAuthUser> {
411
+ [Authentication.CURRENT_USER]: User;
412
+ [Authentication.AUDIT_USER_ID]: IdType;
413
+ }
414
+ }
415
+ ```
416
+
417
+ This enables type-safe access in route handlers:
418
+
419
+ ```typescript
420
+ // TypeScript knows this is IAuthUser
421
+ const user = c.get(Authentication.CURRENT_USER);
422
+ ```
423
+
424
+ **Context variable keys (from `Authentication` constants):**
425
+
426
+ | Key | Constant | Type | Description |
427
+ |-----|----------|------|-------------|
428
+ | `'auth.current.user'` | `Authentication.CURRENT_USER` | `IAuthUser` | The authenticated user payload |
429
+ | `'audit.user.id'` | `Authentication.AUDIT_USER_ID` | `IdType` | The authenticated user's ID (extracted from `userId`) |
430
+ | `'authentication.skip'` | `Authentication.SKIP_AUTHENTICATION` | `boolean` | Set to `true` to bypass authentication on a request |
431
+
432
+ ## Request Schemas
433
+
434
+ ### SignInRequestSchema
435
+
436
+ The built-in schema uses a nested `identifier` + `credential` structure:
437
+
438
+ ```typescript
439
+ const SignInRequestSchema = z.object({
440
+ identifier: z.object({
441
+ scheme: requiredString({ min: 4 }), // e.g., 'username', 'email'
442
+ value: requiredString({ min: 8 }), // the actual identifier value
443
+ }),
444
+ credential: z.object({
445
+ scheme: requiredString(), // e.g., 'basic', 'password'
446
+ value: requiredString({ min: 8 }), // the actual credential value
447
+ }),
448
+ clientId: z.string().optional(), // optional auth provider
449
+ });
450
+
451
+ type TSignInRequest = z.infer<typeof SignInRequestSchema>;
452
+ ```
453
+
454
+ | Field | Type | Constraints |
455
+ |-------|------|-------------|
456
+ | `identifier.scheme` | `string` | Non-empty, min 4 chars |
457
+ | `identifier.value` | `string` | Non-empty, min 8 chars |
458
+ | `credential.scheme` | `string` | Non-empty |
459
+ | `credential.value` | `string` | Non-empty, min 8 chars |
460
+ | `clientId` | `string` | Optional |
461
+
462
+ **OpenAPI examples** (from source):
463
+ ```json
464
+ [
465
+ {
466
+ "identifier": { "scheme": "username", "value": "test_username" },
467
+ "credential": { "scheme": "basic", "value": "test_password" }
468
+ },
469
+ {
470
+ "identifier": { "scheme": "username", "value": "test_username" },
471
+ "credential": { "scheme": "basic", "value": "test_password" },
472
+ "clientId": "auth-provider"
473
+ }
474
+ ]
475
+ ```
476
+
477
+ ### SignUpRequestSchema
478
+
479
+ The built-in schema uses a **flat structure** -- not the nested `identifier`/`credential` pattern used by sign-in:
480
+
481
+ ```typescript
482
+ const SignUpRequestSchema = z.object({
483
+ username: z.string().nonempty().min(8),
484
+ credential: z.string().nonempty().min(8),
485
+ });
486
+
487
+ type TSignUpRequest = z.infer<typeof SignUpRequestSchema>;
488
+ ```
489
+
490
+ | Field | Type | Constraints |
491
+ |-------|------|-------------|
492
+ | `username` | `string` | Non-empty, min 8 chars |
493
+ | `credential` | `string` | Non-empty, min 8 chars |
494
+
495
+ **OpenAPI examples** (from source):
496
+ ```json
497
+ [
498
+ {
499
+ "username": "example_username",
500
+ "credential": "example_credential"
501
+ }
502
+ ]
503
+ ```
504
+
505
+ ### ChangePasswordRequestSchema
506
+
507
+ The built-in schema uses scheme-based credential naming with a `userId` field:
508
+
509
+ ```typescript
510
+ const ChangePasswordRequestSchema = z.object({
511
+ scheme: z.string(),
512
+ oldCredential: requiredString({ min: 8 }),
513
+ newCredential: requiredString({ min: 8 }),
514
+ userId: z.string().or(z.number()),
515
+ });
516
+
517
+ type TChangePasswordRequest = z.infer<typeof ChangePasswordRequestSchema>;
518
+ ```
519
+
520
+ | Field | Type | Constraints |
521
+ |-------|------|-------------|
522
+ | `scheme` | `string` | Required (e.g., `'basic'`) |
523
+ | `oldCredential` | `string` | Non-empty, min 8 chars |
524
+ | `newCredential` | `string` | Non-empty, min 8 chars |
525
+ | `userId` | `string \| number` | Required |
526
+
527
+ **OpenAPI examples** (from source):
528
+ ```json
529
+ [
530
+ {
531
+ "scheme": "basic",
532
+ "oldCredential": "old_password",
533
+ "newCredential": "new_password"
534
+ }
535
+ ]
536
+ ```
537
+
538
+ ### JWTTokenPayloadSchema
539
+
540
+ Exported from the controller factory module. Used as the response schema for the `/who-am-i` endpoint:
541
+
542
+ ```typescript
543
+ const JWTTokenPayloadSchema = z.object({
544
+ userId: z.string().or(z.number()),
545
+ roles: z.array(
546
+ z.object({
547
+ id: z.string().or(z.number()),
548
+ identifier: z.string(),
549
+ priority: z.number().int(),
550
+ }),
551
+ ),
552
+ clientId: z.string().optional(),
553
+ provider: z.string().optional(),
554
+ email: z.email().optional(),
555
+ });
556
+ ```
557
+
558
+ ### Custom Schema Example
559
+
560
+ ```typescript
561
+ import { z } from 'zod';
562
+
563
+ this.bind<TAuthenticationRestOptions>({ key: AuthenticateBindingKeys.REST_OPTIONS }).toValue({
564
+ useAuthController: true,
565
+ controllerOpts: {
566
+ restPath: '/auth',
567
+ payload: {
568
+ signIn: {
569
+ request: {
570
+ schema: z.object({
571
+ email: z.string().email(),
572
+ password: z.string().min(8),
573
+ }),
574
+ },
575
+ response: {
576
+ schema: z.object({
577
+ accessToken: z.string(),
578
+ refreshToken: z.string(),
579
+ expiresIn: z.number(),
580
+ }),
581
+ },
582
+ },
583
+ },
584
+ },
585
+ });
586
+ ```
587
+
588
+ ## API Endpoints
589
+
590
+ The built-in auth controller is created by the `defineAuthController()` factory function and is only available when `useAuthController: true` is set in `REST_OPTIONS`.
591
+
592
+ | Method | Path | Auth Required | Description |
593
+ |--------|------|---------------|-------------|
594
+ | `POST` | `/auth/sign-in` | No | Authenticate and receive a JWT token |
595
+ | `POST` | `/auth/sign-up` | Configurable | Create a new user account |
596
+ | `POST` | `/auth/change-password` | JWT | Change the authenticated user's password |
597
+ | `GET` | `/auth/who-am-i` | JWT | Return the current user's JWT payload |
598
+
599
+ > [!NOTE]
600
+ > The base path `/auth` is configurable via `controllerOpts.restPath`. All paths shown above use the default.
601
+
602
+ ### POST /auth/sign-in
603
+
604
+ **Authentication:** None
605
+
606
+ **Request Body:**
607
+
608
+ Uses `SignInRequestSchema` by default, or a custom schema via `payload.signIn.request.schema`.
609
+
610
+ Default schema:
611
+ ```typescript
612
+ {
613
+ identifier: {
614
+ scheme: string; // min 4 chars, e.g., 'username', 'email'
615
+ value: string; // min 8 chars
616
+ };
617
+ credential: {
618
+ scheme: string; // e.g., 'basic', 'password'
619
+ value: string; // min 8 chars
620
+ };
621
+ clientId?: string;
622
+ }
623
+ ```
624
+
625
+ **Response 200:**
626
+
627
+ Uses `payload.signIn.response.schema` if provided, otherwise `AnyObjectSchema`.
628
+
629
+ ```json
630
+ {
631
+ "token": "eyJhbGciOiJIUzI1NiJ9..."
632
+ }
633
+ ```
634
+
635
+ **Example:**
636
+ ```typescript
637
+ const response = await fetch('/auth/sign-in', {
638
+ method: 'POST',
639
+ headers: { 'Content-Type': 'application/json' },
640
+ body: JSON.stringify({
641
+ identifier: { scheme: 'email', value: 'user@example.com' },
642
+ credential: { scheme: 'password', value: 'my-password' },
643
+ }),
644
+ });
645
+
646
+ const { token } = await response.json();
647
+ ```
648
+
649
+
650
+ ### POST /auth/sign-up
651
+
652
+ **Authentication:** Configurable via `requireAuthenticatedSignUp` (default: `false`)
653
+
654
+ When `requireAuthenticatedSignUp: true`, requires JWT authentication (strategy: `Authentication.STRATEGY_JWT`). When `false`, the `strategies` array is empty (public endpoint).
655
+
656
+ **Request Body:**
657
+
658
+ Uses `SignUpRequestSchema` by default, or a custom schema via `payload.signUp.request.schema`.
659
+
660
+ Default schema (flat structure):
661
+ ```typescript
662
+ {
663
+ username: string; // non-empty, min 8 chars
664
+ credential: string; // non-empty, min 8 chars
665
+ }
666
+ ```
667
+
668
+ **Response 200:**
669
+
670
+ Uses `payload.signUp.response.schema` if provided, otherwise `AnyObjectSchema`.
671
+
672
+ ```json
673
+ {
674
+ "id": "user-id",
675
+ "username": "newuser123"
676
+ }
677
+ ```
678
+
679
+ **Example:**
680
+ ```typescript
681
+ const response = await fetch('/auth/sign-up', {
682
+ method: 'POST',
683
+ headers: { 'Content-Type': 'application/json' },
684
+ body: JSON.stringify({
685
+ username: 'newuser123',
686
+ credential: 'secure-password',
687
+ }),
688
+ });
689
+
690
+ const user = await response.json();
691
+ ```
692
+
693
+
694
+ ### POST /auth/change-password
695
+
696
+ **Authentication:** Always requires JWT (`Authentication.STRATEGY_JWT`)
697
+
698
+ **Request Body:**
699
+
700
+ Uses `ChangePasswordRequestSchema` by default, or a custom schema via `payload.changePassword.request.schema`.
701
+
702
+ Default schema:
703
+ ```typescript
704
+ {
705
+ scheme: string; // e.g., 'basic'
706
+ oldCredential: string; // non-empty, min 8 chars
707
+ newCredential: string; // non-empty, min 8 chars
708
+ userId: string | number;
709
+ }
710
+ ```
711
+
712
+ **Response 200:**
713
+
714
+ Uses `payload.changePassword.response.schema` if provided, otherwise `AnyObjectSchema`.
715
+
716
+ ```json
717
+ {
718
+ "success": true
719
+ }
720
+ ```
721
+
722
+ **Example:**
723
+ ```typescript
724
+ const response = await fetch('/auth/change-password', {
725
+ method: 'POST',
726
+ headers: {
727
+ 'Content-Type': 'application/json',
728
+ 'Authorization': `Bearer ${token}`,
729
+ },
730
+ body: JSON.stringify({
731
+ scheme: 'basic',
732
+ oldCredential: 'old-password',
733
+ newCredential: 'new-secure-password',
734
+ userId: '123',
735
+ }),
736
+ });
737
+
738
+ const result = await response.json();
739
+ ```
740
+
741
+
742
+ ### GET /auth/who-am-i
743
+
744
+ **Authentication:** Always requires JWT (`Authentication.STRATEGY_JWT`)
745
+
746
+ **Request Body:** None
747
+
748
+ **Response 200:**
749
+
750
+ Uses the `JWTTokenPayloadSchema` Zod schema. Returns the current user's decrypted JWT payload directly from context:
751
+
752
+ ```json
753
+ {
754
+ "userId": "123",
755
+ "roles": [
756
+ { "id": "1", "identifier": "admin", "priority": 0 }
757
+ ],
758
+ "clientId": "optional-client-id",
759
+ "provider": "optional-provider",
760
+ "email": "user@example.com"
761
+ }
762
+ ```
763
+
764
+ **Example:**
765
+ ```typescript
766
+ const response = await fetch('/auth/who-am-i', {
767
+ method: 'GET',
768
+ headers: {
769
+ 'Authorization': `Bearer ${token}`,
770
+ },
771
+ });
772
+
773
+ const user = await response.json();
774
+ console.log('Current user:', user);
775
+ ```
776
+
777
+ ## See Also
778
+
779
+ - [Setup & Configuration](./) -- Binding keys, options interfaces, and initial setup
780
+ - [API Reference](./api) -- Architecture, service internals, and strategy registry
781
+ - [Error Reference](./errors) -- Error messages and troubleshooting