@venizia/ignis-docs 0.2.0 → 0.2.1-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/README.md +14 -14
  2. package/content/best-practices/api-usage-examples.md +39 -19
  3. package/content/best-practices/architectural-patterns.md +24 -13
  4. package/content/best-practices/architecture-decisions.md +29 -16
  5. package/content/best-practices/code-style-standards/advanced-patterns.md +43 -31
  6. package/content/best-practices/code-style-standards/constants-configuration.md +19 -4
  7. package/content/best-practices/code-style-standards/control-flow.md +33 -1
  8. package/content/best-practices/code-style-standards/documentation.md +28 -8
  9. package/content/best-practices/code-style-standards/function-patterns.md +15 -4
  10. package/content/best-practices/code-style-standards/index.md +7 -2
  11. package/content/best-practices/code-style-standards/naming-conventions.md +27 -3
  12. package/content/best-practices/code-style-standards/route-definitions.md +9 -5
  13. package/content/best-practices/code-style-standards/tooling.md +14 -1
  14. package/content/best-practices/code-style-standards/type-safety.md +39 -6
  15. package/content/best-practices/common-pitfalls.md +59 -35
  16. package/content/best-practices/contribution-workflow.md +8 -4
  17. package/content/best-practices/data-modeling.md +78 -62
  18. package/content/best-practices/deployment-strategies.md +56 -19
  19. package/content/best-practices/error-handling.md +247 -153
  20. package/content/best-practices/index.md +6 -0
  21. package/content/best-practices/performance-optimization.md +26 -13
  22. package/content/best-practices/security-guidelines.md +35 -9
  23. package/content/best-practices/testing-strategies.md +7 -2
  24. package/content/best-practices/troubleshooting-tips.md +27 -17
  25. package/content/extensions/components/api-reference.md +109 -323
  26. package/content/extensions/components/authentication/api.md +489 -602
  27. package/content/extensions/components/authentication/errors.md +135 -498
  28. package/content/extensions/components/authentication/index.md +89 -801
  29. package/content/extensions/components/authentication/usage.md +231 -955
  30. package/content/extensions/components/authorization/api.md +991 -644
  31. package/content/extensions/components/authorization/errors.md +204 -208
  32. package/content/extensions/components/authorization/getting-started.md +227 -0
  33. package/content/extensions/components/authorization/index.md +88 -795
  34. package/content/extensions/components/authorization/usage.md +219 -528
  35. package/content/extensions/components/health-check.md +77 -243
  36. package/content/extensions/components/index.md +24 -90
  37. package/content/extensions/components/mail/api.md +553 -285
  38. package/content/extensions/components/mail/errors.md +76 -62
  39. package/content/extensions/components/mail/index.md +111 -463
  40. package/content/extensions/components/mail/usage.md +139 -173
  41. package/content/extensions/components/request-tracker.md +70 -173
  42. package/content/extensions/components/socket-io/api.md +462 -785
  43. package/content/extensions/components/socket-io/errors.md +49 -51
  44. package/content/extensions/components/socket-io/index.md +69 -372
  45. package/content/extensions/components/socket-io/usage.md +212 -105
  46. package/content/extensions/components/static-asset/api.md +461 -141
  47. package/content/extensions/components/static-asset/errors.md +121 -53
  48. package/content/extensions/components/static-asset/index.md +84 -606
  49. package/content/extensions/components/static-asset/usage.md +184 -299
  50. package/content/extensions/components/template/index.md +3 -3
  51. package/content/extensions/components/websocket/api.md +311 -404
  52. package/content/extensions/components/websocket/errors.md +47 -56
  53. package/content/extensions/components/websocket/index.md +75 -407
  54. package/content/extensions/components/websocket/usage.md +120 -338
  55. package/content/extensions/helpers/cron/index.md +52 -160
  56. package/content/extensions/helpers/crypto/index.md +67 -480
  57. package/content/extensions/helpers/crypto/reference.md +528 -0
  58. package/content/extensions/helpers/env/index.md +64 -178
  59. package/content/extensions/helpers/error/index.md +286 -196
  60. package/content/extensions/helpers/index.md +61 -47
  61. package/content/extensions/helpers/inversion/index.md +76 -550
  62. package/content/extensions/helpers/inversion/reference.md +530 -0
  63. package/content/extensions/helpers/kafka/admin.md +23 -3
  64. package/content/extensions/helpers/kafka/compile-binary.md +83 -52
  65. package/content/extensions/helpers/kafka/consumer.md +65 -28
  66. package/content/extensions/helpers/kafka/examples.md +46 -253
  67. package/content/extensions/helpers/kafka/index.md +55 -617
  68. package/content/extensions/helpers/kafka/producer.md +156 -22
  69. package/content/extensions/helpers/kafka/schema-registry.md +59 -79
  70. package/content/extensions/helpers/logger/hf-logger.md +220 -0
  71. package/content/extensions/helpers/logger/index.md +78 -552
  72. package/content/extensions/helpers/logger/pino.md +105 -0
  73. package/content/extensions/helpers/logger/reference.md +937 -0
  74. package/content/extensions/helpers/network/api.md +276 -195
  75. package/content/extensions/helpers/network/index.md +87 -524
  76. package/content/extensions/helpers/queue/index.md +80 -897
  77. package/content/extensions/helpers/queue/reference.md +494 -0
  78. package/content/extensions/helpers/redis/index.md +86 -640
  79. package/content/extensions/helpers/redis/reference.md +784 -0
  80. package/content/extensions/helpers/secrets/index.md +136 -0
  81. package/content/extensions/helpers/socket-io/api.md +325 -204
  82. package/content/extensions/helpers/socket-io/index.md +79 -429
  83. package/content/extensions/helpers/storage/api.md +584 -464
  84. package/content/extensions/helpers/storage/index.md +80 -573
  85. package/content/extensions/helpers/types/index.md +75 -495
  86. package/content/extensions/helpers/types/reference.md +689 -0
  87. package/content/extensions/helpers/uid/index.md +176 -189
  88. package/content/extensions/helpers/websocket/api.md +366 -214
  89. package/content/extensions/helpers/websocket/index.md +68 -503
  90. package/content/extensions/helpers/worker-thread/index.md +62 -396
  91. package/content/extensions/helpers/worker-thread/reference.md +428 -0
  92. package/content/extensions/index.md +38 -39
  93. package/content/extensions/src-details/mcp-server.md +96 -548
  94. package/content/guides/core-concepts/persistent/datasources.md +2 -2
  95. package/content/guides/core-concepts/persistent/index.md +5 -1
  96. package/content/guides/core-concepts/persistent/models.md +1 -1
  97. package/content/guides/core-concepts/persistent/pglite.md +218 -0
  98. package/content/guides/core-concepts/persistent/postgres-drivers.md +21 -15
  99. package/content/guides/core-concepts/persistent/search-meilisearch.md +22 -12
  100. package/content/guides/core-concepts/persistent/search-typesense.md +28 -16
  101. package/content/guides/core-concepts/persistent/sqlite.md +296 -0
  102. package/content/guides/core-concepts/persistent/transactions.md +12 -11
  103. package/content/guides/core-concepts/secrets-vault.md +177 -0
  104. package/content/guides/core-concepts/services.md +1 -1
  105. package/content/guides/get-started/5-minute-quickstart.md +59 -206
  106. package/content/guides/get-started/philosophy.md +135 -670
  107. package/content/guides/get-started/setup.md +53 -74
  108. package/content/guides/migrations/redis-helpers-migration.md +5 -4
  109. package/content/guides/migrations/scoped-rbac-migration.md +5 -5
  110. package/content/guides/migrations/unified-connectors-migration.md +8 -7
  111. package/content/guides/tutorials/ecommerce-api.md +3 -8
  112. package/content/guides/tutorials/realtime-chat.md +1 -1
  113. package/content/references/base/application.md +64 -22
  114. package/content/references/base/components.md +3 -3
  115. package/content/references/base/connectors.md +79 -136
  116. package/content/references/base/controllers.md +12 -12
  117. package/content/references/base/datasources-reference.md +600 -0
  118. package/content/references/base/datasources.md +84 -444
  119. package/content/references/base/dependency-injection.md +25 -46
  120. package/content/references/base/filter-system/application-usage.md +95 -123
  121. package/content/references/base/filter-system/array-operators.md +33 -43
  122. package/content/references/base/filter-system/comparison-operators.md +55 -63
  123. package/content/references/base/filter-system/default-filter.md +144 -350
  124. package/content/references/base/filter-system/fields-order-pagination.md +116 -148
  125. package/content/references/base/filter-system/index.md +114 -253
  126. package/content/references/base/filter-system/json-filtering.md +52 -181
  127. package/content/references/base/filter-system/list-operators.md +28 -44
  128. package/content/references/base/filter-system/logical-operators.md +69 -114
  129. package/content/references/base/filter-system/null-operators.md +41 -98
  130. package/content/references/base/filter-system/pattern-matching.md +48 -51
  131. package/content/references/base/filter-system/quick-reference.md +93 -196
  132. package/content/references/base/filter-system/range-operators.md +22 -38
  133. package/content/references/base/filter-system/tips.md +70 -133
  134. package/content/references/base/filter-system/use-cases.md +173 -232
  135. package/content/references/base/grpc-controllers.md +53 -17
  136. package/content/references/base/index.md +5 -3
  137. package/content/references/base/middlewares.md +43 -28
  138. package/content/references/base/models-reference.md +886 -0
  139. package/content/references/base/models.md +81 -1452
  140. package/content/references/base/providers.md +8 -8
  141. package/content/references/base/repositories/advanced.md +281 -419
  142. package/content/references/base/repositories/index.md +84 -644
  143. package/content/references/base/repositories/mixins.md +25 -21
  144. package/content/references/base/repositories/relations.md +194 -375
  145. package/content/references/base/repositories/soft-deletable.md +67 -55
  146. package/content/references/base/secrets.md +267 -0
  147. package/content/references/base/services.md +8 -6
  148. package/content/references/configuration/environment-variables.md +73 -21
  149. package/content/references/configuration/index.md +51 -31
  150. package/content/references/index.md +1 -1
  151. package/content/references/quick-reference.md +3 -16
  152. package/content/references/utilities/crypto.md +35 -76
  153. package/content/references/utilities/date.md +33 -73
  154. package/content/references/utilities/duration.md +85 -0
  155. package/content/references/utilities/index.md +6 -2
  156. package/content/references/utilities/jsx-reference.md +298 -0
  157. package/content/references/utilities/jsx.md +82 -525
  158. package/content/references/utilities/module.md +81 -61
  159. package/content/references/utilities/parse.md +34 -64
  160. package/content/references/utilities/performance.md +33 -58
  161. package/content/references/utilities/promise.md +28 -62
  162. package/content/references/utilities/request.md +58 -218
  163. package/content/references/utilities/retry.md +139 -0
  164. package/content/references/utilities/schema.md +43 -137
  165. package/content/references/utilities/statuses-reference.md +361 -0
  166. package/content/references/utilities/statuses.md +63 -667
  167. package/dist/mcp-server/index.js +0 -0
  168. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
  169. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts.map +1 -1
  170. package/dist/mcp-server/tools/github/list-project-files.tool.js +3 -3
  171. package/dist/mcp-server/tools/github/search-code.tool.js +1 -1
  172. package/dist/mcp-server/tools/github/verify-dependencies.tool.js +1 -1
  173. package/dist/mcp-server/tools/github/view-source-file.tool.js +1 -1
  174. package/package.json +24 -23
@@ -1,89 +1,87 @@
1
- # Authentication -- Usage & Examples
1
+ ---
2
+ title: Authentication Usage
3
+ description: Securing routes, implementing IAuthService, JWKS microservice patterns, and the built-in auth controller endpoints
4
+ difficulty: intermediate
5
+ ---
2
6
 
3
- > Securing routes, authentication flows, JWKS microservice patterns, entity helpers, and API endpoint specifications. See [Setup & Configuration](./) for initial setup.
7
+ # Authentication Usage
4
8
 
5
- ## Securing Routes
9
+ Task-oriented examples for the Authentication component. See the [Overview](./) for initial setup and the [API Reference](./api) for every option and class.
6
10
 
7
- Use the `authenticate` field in route configurations. The field accepts `TRouteAuthenticateConfig`:
11
+ ## Find what you need
12
+
13
+ | You want to | Go to |
14
+ |---|---|
15
+ | Require auth on a route, or make one public | [Securing routes](#securing-routes) |
16
+ | Implement sign-in, sign-up, or change-password | [Implementing IAuthService](#implementing-iauthservice) |
17
+ | Split issuer and verifier across services | [JWKS microservice patterns](#jwks-microservice-patterns) |
18
+ | See what happens on each request, step by step | [Auth flows](#auth-flows) |
19
+ | Accept both JWT and Basic on one route | [Multi-strategy authentication](#multi-strategy-authentication) |
20
+ | Encrypt JWT payload fields | [Token encryption (optional AES)](#token-encryption-optional-aes) |
21
+ | Read `CURRENT_USER` in a handler, with types | [Hono context extension](#hono-context-extension) |
22
+ | Call the built-in `/auth` endpoints | [API endpoints](#api-endpoints) |
23
+ | Add auth columns to a Drizzle table | [Entity column helpers](#entity-column-helpers) |
24
+
25
+ ## Securing routes
26
+
27
+ **Require one strategy.** Add `authenticate` to the route config.
8
28
 
9
29
  ```typescript
10
- // Single strategy
11
30
  const SECURE_ROUTE_CONFIG = {
12
31
  path: '/secure-data',
13
32
  method: HTTP.Methods.GET,
14
33
  authenticate: { strategies: [Authentication.STRATEGY_JWT] },
15
- responses: jsonResponse({
16
- description: 'Protected data',
17
- schema: z.object({ message: z.string() }),
18
- }),
34
+ responses: jsonResponse({ description: 'Protected data', schema: z.object({ message: z.string() }) }),
19
35
  } as const;
36
+ ```
37
+
38
+ **Accept multiple strategies with fallback.** `mode: 'any'` (default) tries each strategy in order. The first success wins.
20
39
 
21
- // Multiple strategies with fallback (any mode)
40
+ ```typescript
22
41
  const FALLBACK_AUTH_CONFIG = {
23
42
  path: '/api/data',
24
43
  method: HTTP.Methods.GET,
25
- authenticate: {
26
- strategies: [Authentication.STRATEGY_JWT, Authentication.STRATEGY_BASIC],
27
- mode: AuthenticationModes.ANY,
28
- },
29
- responses: jsonResponse({
30
- description: 'Data accessible via JWT or Basic auth',
31
- schema: z.object({ data: z.any() }),
32
- }),
44
+ authenticate: { strategies: [Authentication.STRATEGY_JWT, Authentication.STRATEGY_BASIC], mode: AuthenticationModes.ANY },
45
+ responses: jsonResponse({ description: 'Data via JWT or Basic', schema: z.object({ data: z.any() }) }),
33
46
  } as const;
47
+ ```
48
+
49
+ **Make a route public.** `skip: true` bypasses authentication entirely.
34
50
 
35
- // Skip authentication
51
+ ```typescript
36
52
  const PUBLIC_ROUTE_CONFIG = {
37
53
  path: '/public',
38
54
  method: HTTP.Methods.GET,
39
55
  authenticate: { skip: true },
40
- responses: jsonResponse({
41
- description: 'Public endpoint',
42
- schema: z.object({ message: z.string() }),
43
- }),
56
+ responses: jsonResponse({ description: 'Public endpoint', schema: z.object({ message: z.string() }) }),
44
57
  } as const;
45
58
  ```
46
59
 
47
- ## Using the `authenticate()` Standalone Function
48
-
49
- The `authenticate()` function creates an `AuthenticationProvider` instance and uses its middleware factory. It returns a Hono `MiddlewareHandler` suitable for direct middleware usage:
60
+ **Use `authenticate()` as raw Hono middleware.** Use this outside route configs - for a plain Hono sub-app, for example.
50
61
 
51
62
  ```typescript
52
63
  import { authenticate, Authentication, AuthenticationModes } from '@venizia/ignis';
53
64
 
54
- // Use as Hono middleware directly
55
- const authMiddleware = authenticate({
56
- strategies: [Authentication.STRATEGY_JWT],
57
- mode: AuthenticationModes.ANY,
58
- });
65
+ const authMiddleware = authenticate({ strategies: [Authentication.STRATEGY_JWT], mode: AuthenticationModes.ANY });
59
66
 
60
- // Apply to a Hono route
61
- app.get('/protected', authMiddleware, (c) => {
67
+ app.get('/protected', authMiddleware, c => {
62
68
  const user = c.get(Authentication.CURRENT_USER);
63
69
  return c.json({ userId: user.userId });
64
70
  });
65
71
  ```
66
72
 
67
- ## Accessing the Current User
68
-
69
- After authentication, the user payload is available on the Hono `Context`:
73
+ **Read the authenticated user in a handler.**
70
74
 
71
75
  ```typescript
72
- import { Context } from 'hono';
73
76
  import { Authentication, IJWTTokenPayload } from '@venizia/ignis';
74
77
 
75
- // Inside a route handler
76
78
  const user = c.get(Authentication.CURRENT_USER) as IJWTTokenPayload | undefined;
77
-
78
79
  if (user) {
79
- console.log('Authenticated user ID:', user.userId);
80
- console.log('User roles:', user.roles);
80
+ console.log('User ID:', user.userId, 'Roles:', user.roles);
81
81
  }
82
82
  ```
83
83
 
84
- ## Dynamic Skip Authentication
85
-
86
- Use `Authentication.SKIP_AUTHENTICATION` to dynamically skip auth in middleware:
84
+ **Skip authentication dynamically from a preceding middleware.** Useful for internal API keys or webhooks.
87
85
 
88
86
  ```typescript
89
87
  import { Authentication } from '@venizia/ignis';
@@ -97,35 +95,23 @@ const conditionalAuthMiddleware = createMiddleware(async (c, next) => {
97
95
  });
98
96
  ```
99
97
 
100
- ## Implementing an AuthenticationService
98
+ ## Implementing IAuthService
101
99
 
102
- The `AuthenticateComponent` depends on a service implementing the `IAuthService` interface when using the built-in auth controller.
100
+ The built-in auth controller (`useAuthController: true`) delegates every route to a service you provide. That service implements `IAuthService`.
103
101
 
104
- ### JWS Example
102
+ **JWS-backed service.**
105
103
 
106
104
  ```typescript
107
105
  import {
108
- BaseService,
109
- inject,
110
- IAuthService,
111
- IJWTTokenPayload,
112
- JWSTokenService,
113
- BindingKeys,
114
- BindingNamespaces,
115
- TSignInRequest,
116
- TContext,
106
+ BaseService, inject, IAuthService, IJWTTokenPayload, JWSTokenService,
107
+ BindingKeys, BindingNamespaces, TSignInRequest, TContext,
117
108
  } from '@venizia/ignis';
118
109
  import { getError } from '@venizia/ignis-helpers';
119
110
  import { Env } from 'hono';
120
111
 
121
112
  export class AuthenticationService extends BaseService implements IAuthService {
122
113
  constructor(
123
- @inject({
124
- key: BindingKeys.build({
125
- namespace: BindingNamespaces.SERVICE,
126
- key: JWSTokenService.name,
127
- }),
128
- })
114
+ @inject({ key: BindingKeys.build({ namespace: BindingNamespaces.SERVICE, key: JWSTokenService.name }) })
129
115
  private _tokenService: JWSTokenService,
130
116
  ) {
131
117
  super({ scope: AuthenticationService.name });
@@ -139,611 +125,200 @@ export class AuthenticationService extends BaseService implements IAuthService {
139
125
  throw getError({ message: 'Invalid credentials' });
140
126
  }
141
127
 
142
- const payload: IJWTTokenPayload = {
143
- userId: user.id,
144
- roles: user.roles,
145
- };
146
-
128
+ const payload: IJWTTokenPayload = { userId: user.id, roles: user.roles };
147
129
  const token = await this._tokenService.generate({ payload });
148
130
  return { token };
149
131
  }
150
132
 
151
- async signUp(context: TContext<Env>, opts: any): Promise<any> {
152
- // Implement your sign-up logic
153
- }
154
-
155
- async changePassword(context: TContext<Env>, opts: any): Promise<any> {
156
- // Implement your change password logic
157
- }
133
+ async signUp(context: TContext<Env>, opts: any): Promise<any> { /* your logic */ }
134
+ async changePassword(context: TContext<Env>, opts: any): Promise<any> { /* your logic */ }
158
135
  }
159
136
  ```
160
137
 
161
- ### JWKS Issuer Example
138
+ **JWKS-backed service.** Same shape, inject `JWKSIssuerTokenService` instead.
162
139
 
163
140
  ```typescript
164
- import {
165
- BaseService,
166
- inject,
167
- IAuthService,
168
- IJWTTokenPayload,
169
- JWKSIssuerTokenService,
170
- BindingKeys,
171
- BindingNamespaces,
172
- TSignInRequest,
173
- TContext,
174
- } from '@venizia/ignis';
175
- import { getError } from '@venizia/ignis-helpers';
176
- import { Env } from 'hono';
141
+ constructor(
142
+ @inject({ key: BindingKeys.build({ namespace: BindingNamespaces.SERVICE, key: JWKSIssuerTokenService.name }) })
143
+ private _tokenService: JWKSIssuerTokenService,
144
+ ) { super({ scope: AuthenticationService.name }); }
145
+ ```
177
146
 
178
- export class AuthenticationService extends BaseService implements IAuthService {
179
- constructor(
180
- @inject({
181
- key: BindingKeys.build({
182
- namespace: BindingNamespaces.SERVICE,
183
- key: JWKSIssuerTokenService.name,
184
- }),
185
- })
186
- private _tokenService: JWKSIssuerTokenService,
187
- ) {
188
- super({ scope: AuthenticationService.name });
189
- }
147
+ **Implement `refreshToken` (optional).** It re-issues a token from the caller's currently valid one. IGNIS has no separate refresh token.
190
148
 
191
- async signIn(context: TContext<Env>, opts: TSignInRequest): Promise<{ token: string }> {
192
- const { identifier, credential } = opts;
193
- // ... lookup and verify user ...
149
+ ```typescript
150
+ async refreshToken(context: TContext<Env>): Promise<{ token: string }> {
151
+ const currentUser = context.get(Authentication.CURRENT_USER);
152
+ const token = await this._tokenService.generate({ payload: currentUser });
153
+ return { token };
154
+ }
155
+ ```
194
156
 
195
- const payload: IJWTTokenPayload = {
196
- userId: user.id,
197
- roles: user.roles,
198
- };
157
+ > [!NOTE]
158
+ > IGNIS does not enforce rotation or revocation policy. If you need to invalidate old tokens after refresh, implement that inside your `refreshToken`.
199
159
 
200
- const token = await this._tokenService.generate({ payload });
201
- return { token };
202
- }
160
+ **Implement `getUserInformation` (optional).** Backs both `GET /me` and `GET /who-am-i?withUserInformation=true`.
203
161
 
204
- // ... signUp, changePassword ...
162
+ ```typescript
163
+ async getUserInformation(context: TContext<Env>, _opts: AnyObject): Promise<AnyObject> {
164
+ const currentUser = context.get(Authentication.CURRENT_USER);
165
+ return this.userRepository.findById({ id: currentUser.userId });
205
166
  }
206
167
  ```
207
168
 
208
- ## JWKS Microservice Patterns
209
-
210
- ### Issuer + Verifier Architecture
169
+ ## JWKS microservice patterns
211
170
 
212
- In a microservice architecture, one service issues tokens (issuer) and other services verify them (verifier):
171
+ **Issuer + verifier split.** One service signs, others only verify - no shared secret to distribute.
213
172
 
214
173
  ```mermaid
215
174
  flowchart LR
216
175
  CLIENT["Client App"]
217
-
218
176
  subgraph AUTH["Auth Service (JWKS Issuer)"]
219
177
  SIGNIN["POST /auth/sign-in"]
220
178
  CERTS["GET /certs"]
221
179
  end
222
-
223
180
  subgraph API["API Service (JWKS Verifier)"]
224
181
  DATA["GET /api/data"]
225
182
  end
226
-
227
183
  CLIENT -->|"1. Sign in"| SIGNIN
228
184
  SIGNIN -->|"2. JWT token"| CLIENT
229
185
  CLIENT -->|"3. Request + Bearer token"| DATA
230
186
  DATA -->|"4. Fetch JWKS"| CERTS
231
187
  CERTS -->|"5. Public keys"| DATA
232
188
  DATA -->|"6. Verified response"| CLIENT
233
-
234
- style AUTH fill:#e8f4fd,stroke:#0d6efd
235
- style API fill:#d4edda,stroke:#28a745
236
189
  ```
237
190
 
238
- **Auth Service (Issuer):**
239
191
  ```typescript
192
+ // Auth service (issuer)
240
193
  this.bind<TJWTTokenServiceOptions>({ key: AuthenticateBindingKeys.JWT_OPTIONS }).toValue({
241
194
  standard: JOSEStandards.JWKS,
242
195
  options: {
243
- mode: JWKSModes.ISSUER,
244
- algorithm: 'ES256',
245
- keys: {
246
- driver: JWKSKeyDrivers.FILE,
247
- format: JWKSKeyFormats.PEM,
248
- private: './keys/private.pem',
249
- public: './keys/public.pem',
250
- },
251
- kid: 'auth-key-1',
252
- getTokenExpiresFn: () => 86400,
196
+ mode: JWKSModes.ISSUER, algorithm: 'ES256',
197
+ keys: { driver: JWKSKeyDrivers.FILE, format: JWKSKeyFormats.PEM, private: './keys/private.pem', public: './keys/public.pem' },
198
+ kid: 'auth-key-1', getTokenExpiresFn: () => 86400,
253
199
  },
254
200
  });
255
- ```
256
201
 
257
- **API Service (Verifier):**
258
- ```typescript
202
+ // API service (verifier)
259
203
  this.bind<TJWTTokenServiceOptions>({ key: AuthenticateBindingKeys.JWT_OPTIONS }).toValue({
260
204
  standard: JOSEStandards.JWKS,
261
- options: {
262
- mode: JWKSModes.VERIFIER,
263
- jwksUrl: 'https://auth-service.internal/certs',
264
- cacheTtlMs: 43_200_000, // Cache for 12 hours
265
- cooldownMs: 30_000, // Min 30s between refreshes
266
- },
205
+ options: { mode: JWKSModes.VERIFIER, jwksUrl: 'https://auth-service.internal/certs', cacheTtlMs: 43_200_000, cooldownMs: 30_000 },
267
206
  });
268
207
  ```
269
208
 
270
- ### JWKS with AES Payload Encryption
271
-
272
- When using AES payload encryption across services, **both issuer and verifier must share the same `applicationSecret`**:
273
-
274
- **Issuer:**
275
- ```typescript
276
- {
277
- mode: JWKSModes.ISSUER,
278
- algorithm: 'ES256',
279
- keys: { /* ... */ },
280
- kid: 'auth-key-1',
281
- getTokenExpiresFn: () => 86400,
282
- applicationSecret: process.env.APP_ENV_APPLICATION_SECRET,
283
- }
284
- ```
285
-
286
- **Verifier:**
287
- ```typescript
288
- {
289
- mode: JWKSModes.VERIFIER,
290
- jwksUrl: 'https://auth-service.internal/certs',
291
- applicationSecret: process.env.APP_ENV_APPLICATION_SECRET, // Must match issuer
292
- }
293
- ```
294
-
295
- ### JWKS Key Generation
296
-
297
- Generate ES256 keys for JWKS:
209
+ **Generate ES256 or RS256 keys.**
298
210
 
299
211
  ```bash
300
- # Generate private key
212
+ # ES256
301
213
  openssl ecparam -genkey -name prime256v1 -noout -out private.pem
302
-
303
- # Generate public key from private key
304
214
  openssl ec -in private.pem -pubout -out public.pem
305
- ```
306
-
307
- Generate RS256 keys:
308
215
 
309
- ```bash
310
- # Generate private key
216
+ # RS256
311
217
  openssl genrsa -out private.pem 2048
312
-
313
- # Generate public key from private key
314
218
  openssl rsa -in private.pem -pubout -out public.pem
315
219
  ```
316
220
 
317
221
  > [!WARNING]
318
- > Never commit private keys to version control. The `.gitignore` includes patterns for `*.pem`, `*.key`, and `keys/` directories.
319
-
320
- ### Inline Keys (Text Driver)
222
+ > Never commit private keys to version control.
321
223
 
322
- For environments where file access is restricted (e.g., serverless), use the `text` driver:
224
+ **Use inline keys instead of files.** For serverless or restricted-filesystem environments, switch `driver` to `text`.
323
225
 
324
226
  ```typescript
325
- {
326
- mode: JWKSModes.ISSUER,
327
- algorithm: 'ES256',
328
- keys: {
329
- driver: JWKSKeyDrivers.TEXT,
330
- format: JWKSKeyFormats.PEM,
331
- private: process.env.JWKS_PRIVATE_KEY!, // PEM string from env
332
- public: process.env.JWKS_PUBLIC_KEY!, // PEM string from env
333
- },
334
- kid: 'auth-key-1',
335
- getTokenExpiresFn: () => 86400,
227
+ keys: {
228
+ driver: JWKSKeyDrivers.TEXT,
229
+ format: JWKSKeyFormats.PEM,
230
+ private: process.env.JWKS_PRIVATE_KEY!, // PEM string from env
231
+ public: process.env.JWKS_PUBLIC_KEY!,
336
232
  }
337
233
  ```
338
234
 
339
- ## Entity Column Helpers
340
-
341
- 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.
342
-
343
- ### Pattern
344
-
345
- Each helper function returns an object of Drizzle column builders that you spread into your `pgTable()` call alongside any custom columns:
346
-
347
- ```typescript
348
- import { pgTable, serial, text } from 'drizzle-orm/pg-core';
349
- import {
350
- extraUserColumns,
351
- extraRoleColumns,
352
- extraPermissionColumns,
353
- extraPolicyDefinitionColumns,
354
- } from '@venizia/ignis';
355
- import { withSerialId, withTimestamps } from '@venizia/ignis';
356
-
357
- // User table with auth columns
358
- export const users = pgTable('users', {
359
- ...withSerialId(),
360
- ...withTimestamps(),
361
- ...extraUserColumns(),
362
- username: text('username').unique().notNull(),
363
- passwordHash: text('password_hash').notNull(),
364
- email: text('email').unique(),
365
- });
366
-
367
- // Role table with auth columns
368
- export const roles = pgTable('roles', {
369
- ...withSerialId(),
370
- ...withTimestamps(),
371
- ...extraRoleColumns(),
372
- });
373
-
374
- // Permission table
375
- export const permissions = pgTable('permissions', {
376
- ...withSerialId(),
377
- ...withTimestamps(),
378
- ...extraPermissionColumns(),
379
- });
380
-
381
- // Policy definition table (Casbin-style policies)
382
- export const policyDefinitions = pgTable('policy_definitions', {
383
- ...withSerialId(),
384
- ...withTimestamps(),
385
- ...extraPolicyDefinitionColumns(),
386
- });
387
- ```
388
-
389
- ### extraUserColumns
390
-
391
- Returns columns for user-related fields with status and type defaults from `UserStatuses` and `UserTypes`.
392
-
393
- ```typescript
394
- extraUserColumns(opts?: { idType: 'string' | 'number' })
395
- ```
396
-
397
- | Column | Type | Default | Description |
398
- |--------|------|---------|-------------|
399
- | `realm` | `text` | `''` | Multi-tenancy realm identifier |
400
- | `status` | `text` | `UserStatuses.UNKNOWN` (`'000_UNKNOWN'`) | User status |
401
- | `type` | `text` | `UserTypes.SYSTEM` (`'SYSTEM'`) | User type |
402
- | `activatedAt` | `timestamp (tz)` | `null` | Activation timestamp |
403
- | `lastLoginAt` | `timestamp (tz)` | `null` | Last login timestamp |
404
- | `parentId` | `text` or `integer` | `null` | Parent user ID (type depends on `idType`) |
405
-
406
- ### extraRoleColumns
407
-
408
- Returns columns for role definitions. No options parameter.
409
-
410
- ```typescript
411
- extraRoleColumns()
412
- ```
413
-
414
- | Column | Type | Default | Description |
415
- |--------|------|---------|-------------|
416
- | `identifier` | `text` | -- | Unique role identifier (e.g., `'admin'`, `'user'`) |
417
- | `name` | `text` | -- | Human-readable role name |
418
- | `description` | `text` | `null` | Optional role description |
419
- | `priority` | `integer` | -- | Role priority (lower = higher priority) |
420
- | `status` | `text` | `RoleStatuses.ACTIVATED` (`'201_ACTIVATED'`) | Role status |
421
-
422
- ### extraPermissionColumns
423
-
424
- Returns columns for permission definitions. Supports `idType` option for the `parentId` column type.
425
-
426
- ```typescript
427
- extraPermissionColumns(opts?: { idType: 'string' | 'number' })
428
- ```
429
-
430
- | Column | Type | Default | Description |
431
- |--------|------|---------|-------------|
432
- | `code` | `text` (unique) | -- | Unique permission code |
433
- | `name` | `text` | -- | Permission name |
434
- | `subject` | `text` | -- | Permission subject (e.g., `'User'`, `'Order'`) |
435
- | `method` | `text` | -- | HTTP method (e.g., `'GET'`, `'POST'`) |
436
- | `action` | `text` | -- | Permitted action (e.g., `'read'`, `'write'`) |
437
- | `scope` | `text` | -- | Permission scope |
438
- | `description` | `text` | `null` | Optional permission description |
439
- | `parentId` | `text` or `integer` | `null` | Parent permission ID |
440
-
441
- ### extraPolicyDefinitionColumns
442
-
443
- Returns columns for Casbin-style policy definitions that map subjects (users/roles) to targets (resources/permissions).
235
+ **Share AES payload encryption across issuer and verifier.** Both sides need the identical `applicationSecret` - the verifier decrypts what the issuer encrypted.
444
236
 
445
237
  ```typescript
446
- extraPolicyDefinitionColumns(opts?: { idType: 'string' | 'number' })
447
- ```
448
-
449
- | Column | DB Column | Type | Nullable | Default | Description |
450
- |--------|-----------|------|----------|---------|-------------|
451
- | `variant` | `variant` | `text` | No | -- | Policy variant (e.g., `'p'` for policy, `'g'` for grouping) |
452
- | `subjectType` | `subject_type` | `text` | No | -- | Type of subject (e.g., `'user'`, `'role'`) |
453
- | `targetType` | `target_type` | `text` | No | -- | Type of target (e.g., `'permission'`, `'role'`) |
454
- | `action` | `action` | `text` | Yes | `null` | Policy action |
455
- | `effect` | `effect` | `text` | Yes | `null` | Policy effect (e.g., `'allow'`, `'deny'`) |
456
- | `domain` | `domain` | `text` | Yes | `null` | Policy domain for multi-tenancy |
457
- | `subjectId` | `subject_id` | `text` or `integer` | No | -- | Subject ID (type depends on `idType`) |
458
- | `targetId` | `target_id` | `text` or `integer` | No | -- | Target ID (type depends on `idType`) |
459
-
460
- ### ID Type Polymorphism
238
+ // Issuer
239
+ { mode: JWKSModes.ISSUER, /* ... */ applicationSecret: process.env.APP_ENV_APPLICATION_SECRET }
461
240
 
462
- All column helpers that accept `opts.idType` default to `'number'` (producing `integer` columns). Pass `'string'` to use `text` columns instead:
463
-
464
- ```typescript
465
- // Number IDs (default) -- uses integer columns for FK references
466
- extraUserColumns()
467
- extraPermissionColumns()
468
-
469
- // String IDs (e.g., UUID) -- uses text columns for FK references
470
- extraUserColumns({ idType: 'string' })
471
- extraPermissionColumns({ idType: 'string' })
241
+ // Verifier - must match
242
+ { mode: JWKSModes.VERIFIER, jwksUrl: '...', applicationSecret: process.env.APP_ENV_APPLICATION_SECRET }
472
243
  ```
473
244
 
474
- ## Status Constants
475
-
476
- The authentication module uses status classes from `@/common/statuses`. These extend `CommonStatuses` and provide lifecycle state management for auth entities.
477
-
478
- ### UserStatuses
245
+ ## Auth flows
479
246
 
480
- Inherits all statuses from `CommonStatuses`:
481
-
482
- | Constant | Value | Description |
483
- |----------|-------|-------------|
484
- | `UserStatuses.UNKNOWN` | `'000_UNKNOWN'` | Initial/unverified state |
485
- | `UserStatuses.ACTIVATED` | `'201_ACTIVATED'` | Active user |
486
- | `UserStatuses.DEACTIVATED` | `'401_DEACTIVATED'` | Deactivated user |
487
- | `UserStatuses.BLOCKED` | `'403_BLOCKED'` | Blocked user |
488
- | `UserStatuses.ARCHIVED` | `'405_ARCHIVED'` | Archived user |
489
-
490
- ### UserTypes
491
-
492
- | Constant | Value | Description |
493
- |----------|-------|-------------|
494
- | `UserTypes.SYSTEM` | `'SYSTEM'` | System-created user (default) |
495
- | `UserTypes.LINKED` | `'LINKED'` | Linked/external user |
496
-
497
- ### RoleStatuses
498
-
499
- Inherits all statuses from `CommonStatuses` (same values as `UserStatuses`):
500
-
501
- | Constant | Value | Description |
502
- |----------|-------|-------------|
503
- | `RoleStatuses.UNKNOWN` | `'000_UNKNOWN'` | Initial state |
504
- | `RoleStatuses.ACTIVATED` | `'201_ACTIVATED'` | Active role (default for `extraRoleColumns`) |
505
- | `RoleStatuses.DEACTIVATED` | `'401_DEACTIVATED'` | Deactivated role |
506
- | `RoleStatuses.BLOCKED` | `'403_BLOCKED'` | Blocked role |
507
- | `RoleStatuses.ARCHIVED` | `'405_ARCHIVED'` | Archived role |
508
-
509
- ## Auth Flows
510
-
511
- ### JWS Authentication Flow
247
+ - **JWS:** extract Bearer token -> `jose.jwtVerify()` with the shared secret -> decrypt payload (if AES configured) -> set `CURRENT_USER`.
248
+ - **JWKS Issuer:** extract Bearer token -> `ensureInitialized()` (lazy-loads keys once) -> `jwtVerify()` with the public key -> decrypt payload -> set `CURRENT_USER`.
249
+ - **JWKS Verifier:** extract Bearer token -> `ensureInitialized()` (creates the remote JWKS verifier once) -> `jwtVerify()` with the remote JWKS -> decrypt payload -> set `CURRENT_USER`.
250
+ - **Basic:** decode `Authorization: Basic <base64>` -> call your `verifyCredentials` callback -> on `null`, throw `401`; on a user, set `CURRENT_USER`.
512
251
 
513
252
  ```mermaid
514
253
  sequenceDiagram
515
254
  participant C as Client
516
255
  participant MW as Auth Middleware
517
- participant S as JWSAuthenticationStrategy
518
- participant SVC as JWSTokenService
519
- participant JOSE as jose library
256
+ participant S as Strategy
257
+ participant SVC as TokenService
520
258
 
521
- C->>MW: Request + Authorization: Bearer <token>
259
+ C->>MW: Request + Authorization header
522
260
  MW->>S: authenticate(context)
523
261
  S->>SVC: extractCredentials(context)
524
- SVC-->>S: { type: "Bearer", token }
525
- S->>SVC: verify({ type, token })
526
- SVC->>JOSE: jwtVerify(token, jwtSecret)
527
- JOSE-->>SVC: JWTVerifyResult
528
- SVC->>SVC: decryptPayload() (if AES configured)
529
- SVC-->>S: IJWTTokenPayload
262
+ SVC-->>S: { type, token } | { username, password }
263
+ S->>SVC: verify(...)
264
+ SVC-->>S: IAuthUser
530
265
  S-->>MW: IAuthUser
531
266
  MW->>MW: Set CURRENT_USER + AUDIT_USER_ID
532
267
  MW->>C: Continue to handler
533
268
  ```
534
269
 
535
- 1. **Client sends request** with <code v-pre>Authorization: Bearer &lt;token&gt;</code> header
536
- 2. **JWSAuthenticationStrategy.authenticate()** is called by the Hono middleware
537
- 3. **AbstractBearerTokenService.extractCredentials()** extracts the token from the Authorization header
538
- 4. **JWSTokenService.doVerify()** verifies the JWT signature using `jose.jwtVerify()` with the shared `jwtSecret`
539
- 5. **AbstractBearerTokenService.decryptPayload()** decrypts the AES-encrypted payload fields (if AES configured)
540
- 6. **User payload is set** on `context.get(Authentication.CURRENT_USER)`
270
+ ## Multi-strategy authentication
541
271
 
542
- ### JWKS Issuer Authentication Flow
543
-
544
- ```mermaid
545
- sequenceDiagram
546
- participant C as Client
547
- participant MW as Auth Middleware
548
- participant S as JWKSIssuerStrategy
549
- participant SVC as JWKSIssuerTokenService
550
- participant INIT as Lazy Init
551
- participant JOSE as jose library
552
-
553
- C->>MW: Request + Authorization: Bearer <token>
554
- MW->>S: authenticate(context)
555
- S->>SVC: extractCredentials(context)
556
- SVC-->>S: { type: "Bearer", token }
557
- S->>SVC: verify({ type, token })
558
- SVC->>INIT: ensureInitialized()
559
- Note over INIT: Load keys from file/text<br/>Parse PEM/JWK<br/>Cache JWKS
560
- INIT-->>SVC: initialized
561
- SVC->>JOSE: jwtVerify(token, publicKey)
562
- JOSE-->>SVC: JWTVerifyResult
563
- SVC->>SVC: decryptPayload() (if AES configured)
564
- SVC-->>S: IJWTTokenPayload
565
- S-->>MW: IAuthUser
566
- MW->>C: Continue to handler
567
- ```
568
-
569
- 1. **Client sends request** with <code v-pre>Authorization: Bearer &lt;token&gt;</code> header
570
- 2. **JWKSIssuerAuthenticationStrategy.authenticate()** is called by the Hono middleware
571
- 3. **AbstractBearerTokenService.extractCredentials()** extracts the token from the Authorization header
572
- 4. **JWKSIssuerTokenService.doVerify()** calls `ensureInitialized()` (lazy-loads keys on first call), then verifies the JWT using the public key
573
- 5. **AbstractBearerTokenService.decryptPayload()** decrypts the AES-encrypted payload fields (if AES configured)
574
- 6. **User payload is set** on `context.get(Authentication.CURRENT_USER)`
575
-
576
- ### JWKS Verifier Authentication Flow
577
-
578
- ```mermaid
579
- sequenceDiagram
580
- participant C as Client
581
- participant MW as Auth Middleware
582
- participant S as JWKSVerifierStrategy
583
- participant SVC as JWKSVerifierTokenService
584
- participant INIT as Lazy Init
585
- participant REMOTE as Remote JWKS URL
586
-
587
- C->>MW: Request + Authorization: Bearer <token>
588
- MW->>S: authenticate(context)
589
- S->>SVC: extractCredentials(context)
590
- SVC-->>S: { type: "Bearer", token }
591
- S->>SVC: verify({ type, token })
592
- SVC->>INIT: ensureInitialized()
593
- INIT->>REMOTE: createRemoteJWKSet(jwksUrl)
594
- REMOTE-->>INIT: JWKS verifier function
595
- INIT-->>SVC: initialized
596
- SVC->>SVC: jwtVerify(token, jwksVerifier)
597
- SVC->>SVC: decryptPayload() (if AES configured)
598
- SVC-->>S: IJWTTokenPayload
599
- S-->>MW: IAuthUser
600
- MW->>C: Continue to handler
601
- ```
602
-
603
- 1. **Client sends request** with <code v-pre>Authorization: Bearer &lt;token&gt;</code> header
604
- 2. **JWKSVerifierAuthenticationStrategy.authenticate()** is called by the Hono middleware
605
- 3. **AbstractBearerTokenService.extractCredentials()** extracts the token from the Authorization header
606
- 4. **JWKSVerifierTokenService.doVerify()** calls `ensureInitialized()` (creates remote JWKS verifier on first call), then verifies the JWT using the remote JWKS
607
- 5. **AbstractBearerTokenService.decryptPayload()** decrypts the AES-encrypted payload fields (if AES configured)
608
- 6. **User payload is set** on `context.get(Authentication.CURRENT_USER)`
609
-
610
- ### Basic Authentication Flow
611
-
612
- ```mermaid
613
- sequenceDiagram
614
- participant C as Client
615
- participant MW as Auth Middleware
616
- participant S as BasicAuthStrategy
617
- participant SVC as BasicTokenService
618
- participant CB as verifyCredentials callback
619
-
620
- C->>MW: Request + Authorization: Basic <base64>
621
- MW->>S: authenticate(context)
622
- S->>SVC: extractCredentials(context)
623
- SVC->>SVC: Base64 decode
624
- SVC-->>S: { username, password }
625
- S->>SVC: verify({ credentials, context })
626
- SVC->>CB: verifyCredentials({ credentials, context })
627
- CB-->>SVC: IAuthUser | null
628
- alt valid user
629
- SVC-->>S: IAuthUser
630
- S-->>MW: IAuthUser
631
- MW->>MW: Set CURRENT_USER + AUDIT_USER_ID
632
- MW->>C: Continue to handler
633
- else null (invalid)
634
- SVC-->>S: throw 401
635
- S-->>MW: throw 401
636
- MW->>C: 401 Unauthorized
637
- end
638
- ```
639
-
640
- 1. **Client sends request** with <code v-pre>Authorization: Basic &lt;base64(username:password)&gt;</code> header
641
- 2. **BasicAuthenticationStrategy.authenticate()** is called by the Hono middleware
642
- 3. **BasicTokenService.extractCredentials()** decodes the Base64 credentials
643
- 4. **BasicTokenService.verify()** calls the user-provided `verifyCredentials` callback with `{ credentials, context }`
644
- 5. **User payload is set** on `context.get(Authentication.CURRENT_USER)` if verification succeeds
645
-
646
- > [!IMPORTANT]
647
- > The `verifyCredentials` callback must perform all necessary validation (password hashing comparison, user lookup, etc.) and return an `IAuthUser` object or `null`.
648
-
649
- ## Multi-Strategy Authentication
650
-
651
- When multiple strategies are configured on a route via `authenticate: { strategies: ['jwt', 'basic'] }`:
272
+ | Mode | Behavior | Use case |
273
+ |------|----------|----------|
274
+ | `'any'` (default) | Tried in order.<br>First success wins.<br>Failures are logged at debug, not returned.<br>All fail -> `401` listing the tried strategies. | Fallback auth (JWT primary, Basic for legacy clients) |
275
+ | `'all'` | Every strategy must pass.<br>First failure rejects immediately.<br>The **first** strategy's user payload is the identity source. | Multi-factor authentication |
652
276
 
653
277
  ```mermaid
654
278
  flowchart TD
655
279
  REQ["Request arrives"] --> MODE{"mode?"}
656
-
657
- MODE -->|"any (default)"| ANY["Try strategies in order"]
658
- ANY --> S1{"Strategy 1"}
280
+ MODE -->|"any"| S1{"Strategy 1"}
659
281
  S1 -->|"Success"| WIN["Set user, continue"]
660
282
  S1 -->|"Fail"| S2{"Strategy 2"}
661
283
  S2 -->|"Success"| WIN
662
- S2 -->|"Fail"| FAIL_ANY["401: Tried strategies: jwt, basic"]
663
-
664
- MODE -->|"all"| ALL["Run all strategies"]
665
- ALL --> A1{"Strategy 1"}
284
+ S2 -->|"Fail"| FAIL_ANY["401: Tried strategies"]
285
+ MODE -->|"all"| A1{"Strategy 1"}
666
286
  A1 -->|"Fail"| FAIL_ALL["Exception propagates"]
667
287
  A1 -->|"Pass"| A2{"Strategy 2"}
668
288
  A2 -->|"Fail"| FAIL_ALL
669
289
  A2 -->|"Pass"| CHECK{"userId?"}
670
290
  CHECK -->|"Yes"| WIN2["Set user, continue"]
671
291
  CHECK -->|"No"| FAIL_ID["401: Failed to identify user"]
672
-
673
- style WIN fill:#d4edda,stroke:#28a745
674
- style WIN2 fill:#d4edda,stroke:#28a745
675
- style FAIL_ANY fill:#f8d7da,stroke:#dc3545
676
- style FAIL_ALL fill:#f8d7da,stroke:#dc3545
677
- style FAIL_ID fill:#f8d7da,stroke:#dc3545
678
292
  ```
679
293
 
680
- **`any` mode (default):**
681
- - Strategies are tried in the order specified
682
- - The first successful strategy wins
683
- - Errors from failing strategies are **discarded** (logged at debug level)
684
- - If all strategies fail, a `401 Unauthorized` error is thrown listing all tried strategies
685
- - **Use case:** Fallback authentication (try JWT, fallback to Basic)
294
+ ## Token encryption (optional AES)
686
295
 
687
- **`all` mode:**
688
- - Every strategy must pass successfully
689
- - If any strategy fails, the request is immediately rejected (exception propagates)
690
- - The **first** strategy's user payload is used as the identity source
691
- - **Use case:** Multi-factor authentication (both JWT and Basic required)
296
+ AES payload encryption is off by default. It only activates when you set `applicationSecret` on the JWS/JWKS options.
692
297
 
693
- > [!TIP]
694
- > 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.
695
-
696
- ## Token Encryption (Optional AES)
697
-
698
- ```mermaid
699
- flowchart LR
700
- subgraph GENERATE["generate() - Token Creation"]
701
- direction TB
702
- P["Payload: { userId, roles, email }"]
703
- P --> CHECK1{"applicationSecret?"}
704
- CHECK1 -->|"Yes"| ENC["encryptPayload()"]
705
- ENC --> E1["Keep: iss, sub, aud, exp, iat"]
706
- ENC --> E2["Encrypt keys + values"]
707
- CHECK1 -->|"No"| PLAIN1["Use payload as-is"]
708
- end
298
+ | Aspect | Behavior |
299
+ |---|---|
300
+ | Default | Off. Without `applicationSecret`, payloads stay standard plaintext JWT. |
301
+ | Standard fields | `iss`, `sub`, `aud`, `jti`, `nbf`, `exp`, `iat` are never encrypted, on either side. |
302
+ | Other fields | Both the key and the value are AES-encrypted. `null` and `undefined` values are skipped. |
303
+ | Serialization | `JSON.stringify` by default. Opt in to `AuthenticationFieldCodecs.ROLES_CODEC` for `roles` - it serializes as pipe-separated `id\|identifier\|priority` strings instead. |
304
+ | Secret | Must stay constant. Changing `applicationSecret` invalidates every existing token. |
305
+ | Issuer/verifier match | Both sides need the identical secret, and identical `fieldCodecs` if you use them. |
709
306
 
710
- subgraph VERIFY["verify() - Token Verification"]
711
- direction TB
712
- T["Verified JWT payload"]
713
- T --> CHECK2{"applicationSecret?"}
714
- CHECK2 -->|"Yes"| DEC["decryptPayload()"]
715
- DEC --> D1["Extract: iss, sub, aud, exp, iat"]
716
- DEC --> D2["Decrypt keys + values"]
717
- CHECK2 -->|"No"| PLAIN2["Use payload as-is"]
718
- end
719
-
720
- style GENERATE fill:#e8f4fd,stroke:#0d6efd
721
- style VERIFY fill:#d4edda,stroke:#28a745
307
+ ```typescript
308
+ this.bind<TJWTTokenServiceOptions>({ key: AuthenticateBindingKeys.JWT_OPTIONS }).toValue({
309
+ standard: JOSEStandards.JWS,
310
+ options: {
311
+ jwtSecret: process.env.APP_ENV_JWT_SECRET!,
312
+ getTokenExpiresFn: () => 86400,
313
+ applicationSecret: process.env.APP_ENV_APPLICATION_SECRET, // enables AES encryption
314
+ fieldCodecs: [AuthenticationFieldCodecs.ROLES_CODEC], // optional, opt-in
315
+ },
316
+ });
722
317
  ```
723
318
 
724
- JWT payloads can optionally be encrypted field-by-field using AES (default `aes-256-cbc`) via the `@venizia/ignis-helpers` AES utility. This is configured by providing `applicationSecret` in the service options.
725
-
726
- > [!NOTE]
727
- > AES payload encryption is **optional** for all JOSE standards (JWS and JWKS). When `applicationSecret` is not provided, payloads are stored in standard plaintext JWT format.
728
-
729
- **Encryption process (when `applicationSecret` is provided):**
730
- 1. Standard JWT fields (`iss`, `sub`, `aud`, `jti`, `nbf`, `exp`, `iat`) are preserved as-is
731
- 2. All other fields have both their **keys** and **values** AES-encrypted
732
- 3. The `roles` field is serialized as `id|identifier|priority` pipe-separated strings before encryption
733
- 4. `null` and `undefined` values are skipped during encryption
734
-
735
- **Decryption process:**
736
- 1. If AES is not configured (`this.aes` is null), the payload is returned as-is
737
- 2. Standard JWT fields are extracted directly
738
- 3. Encrypted fields have their keys decrypted first, then their values
739
- 4. 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()`)
740
-
741
- > [!WARNING]
742
- > 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. In JWKS microservice setups, the issuer and all verifiers must share the same `applicationSecret`.
743
-
744
- ## Hono Context Extension
319
+ ## Hono context extension
745
320
 
746
- The Authentication module extends Hono's `ContextVariableMap` to provide type-safe access to auth data. Note: `ContextVariableMap` does **not** take a generic parameter - it is a plain interface augmentation:
321
+ The module augments Hono's `ContextVariableMap` (a plain interface, not generic) so `c.get()` is type-safe:
747
322
 
748
323
  ```typescript
749
324
  declare module 'hono' {
@@ -754,435 +329,136 @@ declare module 'hono' {
754
329
  }
755
330
  ```
756
331
 
757
- This enables type-safe access in route handlers:
758
-
759
- ```typescript
760
- // TypeScript knows this is IAuthUser
761
- const user = c.get(Authentication.CURRENT_USER);
762
- ```
763
-
764
- **Context variable keys (from `Authentication` constants):**
765
-
766
- | Key | Constant | Type | Description |
767
- |-----|----------|------|-------------|
768
- | `'auth.current.user'` | `Authentication.CURRENT_USER` | `IAuthUser` | The authenticated user payload |
769
- | `'audit.user.id'` | `Authentication.AUDIT_USER_ID` | `IdType` | The authenticated user's ID (extracted from `userId`) |
770
- | `'authentication.skip'` | `Authentication.SKIP_AUTHENTICATION` | `boolean` | Set to `true` to bypass authentication on a request |
771
-
772
- ## Request Schemas
773
-
774
- ### SignInRequestSchema
775
-
776
- The built-in schema uses a nested `identifier` + `credential` structure:
777
-
778
- ```typescript
779
- const SignInRequestSchema = z.object({
780
- identifier: z.object({
781
- scheme: requiredString({ min: 4 }), // e.g., 'username', 'email'
782
- value: requiredString({ min: 8 }), // the actual identifier value
783
- }),
784
- credential: z.object({
785
- scheme: requiredString(), // e.g., 'basic', 'password'
786
- value: requiredString({ min: 8 }), // the actual credential value
787
- }),
788
- clientId: z.string().optional(), // optional auth provider
789
- });
790
-
791
- type TSignInRequest = z.infer<typeof SignInRequestSchema>;
792
- ```
793
-
794
- ### SignUpRequestSchema
795
-
796
- The built-in schema uses a **flat structure**:
797
-
798
- ```typescript
799
- const SignUpRequestSchema = z.object({
800
- username: z.string().nonempty().min(8),
801
- credential: z.string().nonempty().min(8),
802
- });
803
-
804
- type TSignUpRequest = z.infer<typeof SignUpRequestSchema>;
805
- ```
806
-
807
- ### ChangePasswordRequestSchema
808
-
809
- ```typescript
810
- const ChangePasswordRequestSchema = z.object({
811
- scheme: z.string(),
812
- oldCredential: requiredString({ min: 8 }),
813
- newCredential: requiredString({ min: 8 }),
814
- userId: z.string().or(z.number()),
815
- });
816
-
817
- type TChangePasswordRequest = z.infer<typeof ChangePasswordRequestSchema>;
818
- ```
819
-
820
- ### JWTTokenPayloadSchema
821
-
822
- Exported from the controller factory module. Used as the response schema for the `/who-am-i` endpoint:
823
-
824
- ```typescript
825
- const JWTTokenPayloadSchema = z.object({
826
- userId: z.string().or(z.number()),
827
- roles: z.array(
828
- z.object({
829
- id: z.string().or(z.number()),
830
- identifier: z.string(),
831
- priority: z.number().int(),
832
- }),
833
- ),
834
- clientId: z.string().optional(),
835
- provider: z.string().optional(),
836
- email: z.email().optional(),
837
- });
838
- ```
839
-
840
- For the `/who-am-i` response, this schema is extended at runtime with an optional `userInformation` field (typed from `payload.getUserInformation.response.schema`, falling back to `AnyObjectSchema`) so the `withUserInformation` shape is OpenAPI-documented.
332
+ | Constant | Key string | Type | Description |
333
+ |----------|-----------|------|-------------|
334
+ | `Authentication.CURRENT_USER` | `auth.current.user` | `IAuthUser` | Authenticated user payload |
335
+ | `Authentication.AUDIT_USER_ID` | `audit.user.id` | `IdType` | Authenticated user's ID |
336
+ | `Authentication.SKIP_AUTHENTICATION` | `authentication.skip` | `boolean` | Set `true` to bypass authentication |
841
337
 
842
- ## API Endpoints
338
+ ## API endpoints
843
339
 
844
- The built-in auth controller is created by the `defineAuthController()` factory function and is only available when `useAuthController: true` is set in `REST_OPTIONS`.
340
+ The built-in auth controller exists only when `REST_OPTIONS.useAuthController: true` is set - see [Setup](./#common-tasks).
845
341
 
846
- | Method | Path | Auth Required | Description |
342
+ | Method | Path | Auth required | Description |
847
343
  |--------|------|---------------|-------------|
848
- | `POST` | `/auth/sign-in` | No | Authenticate and receive a JWT token |
849
- | `POST` | `/auth/sign-up` | Configurable | Create a new user account |
344
+ | `POST` | `/auth/sign-in` | No | Authenticate, receive a JWT |
345
+ | `POST` | `/auth/sign-up` | Configurable (`requireAuthenticatedSignUp`) | Create a user account |
850
346
  | `POST` | `/auth/change-password` | JWT | Change the authenticated user's password |
851
- | `POST` | `/auth/token/refresh` | JWT | Re-issue an access token using a valid JWT |
852
- | `GET` | `/auth/who-am-i` | JWT | Return the current user's JWT payload (optionally with attached user information) |
853
- | `GET` | `/auth/me` | JWT | Return the current user's information from `getUserInformation` |
854
- | `GET` | `/certs` | No | JWKS endpoint (JWKS Issuer mode only) |
347
+ | `POST` | `/auth/token/refresh` | JWT | Re-issue a token from the caller's valid JWT |
348
+ | `GET` | `/auth/who-am-i` | JWT | Return the JWT payload, optionally merged with `getUserInformation` |
349
+ | `GET` | `/auth/me` | JWT | Return `getUserInformation` result directly |
350
+ | `GET` | `/certs` | No | JWKS endpoint (Issuer mode only) |
855
351
 
856
352
  > [!NOTE]
857
- > The base path `/auth` is configurable via `controllerOpts.restPath`. The `/certs` path is configurable via `rest.path` in `IJWKSIssuerOptions`. The `/certs` endpoint is intentionally unauthenticated - it serves the public keys needed by external verifiers.
858
-
859
- ### POST /auth/sign-in
860
-
861
- **Authentication:** None
862
-
863
- **Request Body:**
353
+ > `/auth` is configurable via `controllerOpts.restPath`. `/certs` is configurable via `rest.path` in `IJWKSIssuerOptions`, and is intentionally unauthenticated.
864
354
 
865
- Uses `SignInRequestSchema` by default, or a custom schema via `payload.signIn.request.schema`.
866
-
867
- **Response 200:**
868
-
869
- Uses `payload.signIn.response.schema` if provided, otherwise `AnyObjectSchema`.
355
+ **`POST /auth/sign-in`** - body defaults to `SignInRequestSchema` (nested `identifier`/`credential`), overridable via `payload.signIn`.
870
356
 
871
357
  ```json
872
- {
873
- "token": "eyJhbGciOiJFUzI1NiIsImtpZCI6Im15LWtleS1pZC0xIn0..."
874
- }
358
+ { "token": "eyJhbGciOiJFUzI1NiIsImtpZCI6Im15LWtleS1pZC0xIn0..." }
875
359
  ```
876
360
 
877
- ### POST /auth/sign-up
878
-
879
- **Authentication:** Configurable via `requireAuthenticatedSignUp` (default: `false`)
880
-
881
- When `requireAuthenticatedSignUp: true`, requires JWT authentication. When `false`, the endpoint is public.
361
+ **`POST /auth/sign-up`** - public unless `requireAuthenticatedSignUp: true`. Body defaults to a flat `SignUpRequestSchema` (`username`, `credential`) - unlike sign-in, the shape isn't nested.
882
362
 
883
- ### POST /auth/change-password
363
+ **`POST /auth/change-password`** - always requires JWT. Body defaults to `ChangePasswordRequestSchema` (`scheme`, `oldCredential`, `newCredential`, `userId`).
884
364
 
885
- **Authentication:** Always requires JWT (`Authentication.STRATEGY_JWT`)
886
-
887
- ### POST /auth/token/refresh
888
-
889
- **Authentication:** Always requires JWT (`Authentication.STRATEGY_JWT`)
890
-
891
- Re-issues an access token using the caller's currently valid JWT. There is no separate refresh token - the caller must send a valid Bearer token in the `Authorization` header.
892
-
893
- **Request Body:** None
894
-
895
- **Response 200:**
896
-
897
- Uses `payload.refreshToken.response.schema` if provided, otherwise `AnyObjectSchema`. The response shape is entirely defined by your `IAuthService.refreshToken()` implementation.
898
-
899
- **Response 501 (Not Implemented):**
900
-
901
- Returned when the bound `IAuthService` does not implement the optional `refreshToken` method.
902
-
903
- **Implementing `refreshToken` in your service:**
904
-
905
- ```typescript
906
- export class AuthenticationService extends BaseService implements IAuthService {
907
- async refreshToken(context: TContext<Env>): Promise<{ token: string }> {
908
- // The current user is already verified by JWT middleware
909
- const currentUser = context.get(Authentication.CURRENT_USER);
910
-
911
- // Re-issue a new token with the same payload
912
- const token = await this._tokenService.generate({ payload: currentUser });
913
- return { token };
914
- }
915
-
916
- // ... signIn, signUp, changePassword ...
917
- }
918
- ```
919
-
920
- > [!NOTE]
921
- > The framework does not enforce rotation or revocation policy. If you need to invalidate old tokens after refresh, implement that logic (e.g., a token blocklist or short expiry) inside your `refreshToken` implementation.
365
+ **`POST /auth/token/refresh`** - always requires JWT, no request body. Returns `501` if `IAuthService.refreshToken` isn't implemented.
922
366
 
923
- ### GET /auth/who-am-i
924
-
925
- **Authentication:** Always requires JWT (`Authentication.STRATEGY_JWT`)
926
-
927
- Returns the current user's decrypted JWT payload directly from context.
367
+ **`GET /auth/who-am-i`** - always requires JWT. The `withUserInformation` query param (`true`, `false`, `1`, `0`; default `false`) attaches a `userInformation` field from `getUserInformation`. Returns `501` if you request that field without implementing `getUserInformation`.
928
368
 
929
369
  ```json
930
- {
931
- "userId": "123",
932
- "roles": [
933
- { "id": "1", "identifier": "admin", "priority": 0 }
934
- ],
935
- "clientId": "optional-client-id",
936
- "provider": "optional-provider",
937
- "email": "user@example.com"
938
- }
370
+ { "userId": "123", "roles": [{ "id": "1", "identifier": "admin", "priority": 0 }] }
939
371
  ```
940
372
 
941
- **Query Parameters:**
942
-
943
- | Name | Type | Description |
944
- |------|------|-------------|
945
- | `withUserInformation` | `true` \| `false` \| `1` \| `0` | When truthy, attaches a `userInformation` field built from `IAuthService.getUserInformation`. Defaults to `false`. |
946
-
947
- When `withUserInformation` is truthy, the response merges the `getUserInformation` result into the payload:
948
-
949
- ```json
950
- {
951
- "userId": "123",
952
- "roles": [{ "id": "1", "identifier": "admin", "priority": 0 }],
953
- "userInformation": { "fullName": "Ada Lovelace", "department": "R&D" }
954
- }
955
- ```
956
-
957
- **Response 501 (Not Implemented):**
958
-
959
- Returned when `withUserInformation` is truthy but the bound `IAuthService` does not implement the optional `getUserInformation` method.
960
-
961
- ### GET /auth/me
962
-
963
- **Authentication:** Always requires JWT (`Authentication.STRATEGY_JWT`)
964
-
965
- Returns the current user's information by delegating to `IAuthService.getUserInformation(context, {})`. Unlike `who-am-i`, the response is entirely the service result -- it is not merged with the JWT payload.
966
-
967
- **Response 200:**
968
-
969
- Uses `payload.getUserInformation.response.schema` if provided, otherwise `AnyObjectSchema`. The shape is defined by your `getUserInformation` implementation.
970
-
971
- **Response 501 (Not Implemented):**
972
-
973
- Returned when the bound `IAuthService` does not implement the optional `getUserInformation` method.
974
-
975
- **Implementing `getUserInformation` in your service:**
976
-
977
- ```typescript
978
- export class AuthenticationService extends BaseService implements IAuthService {
979
- async getUserInformation(context: TContext<Env>, _opts: AnyObject): Promise<AnyObject> {
980
- // The current user is already verified by JWT middleware
981
- const currentUser = context.get(Authentication.CURRENT_USER);
982
- return this.userRepository.findById({ id: currentUser.userId });
983
- }
984
-
985
- // ... signIn, signUp, changePassword ...
986
- }
987
- ```
373
+ **`GET /auth/me`** - always requires JWT, delegating entirely to `getUserInformation(context, {})`. The response is not merged with the JWT payload. Returns `501` if `getUserInformation` isn't implemented.
988
374
 
989
375
  > [!TIP]
990
- > One `getUserInformation` implementation backs both routes. Use `GET /me` when you want the raw profile, or `GET /who-am-i?withUserInformation=true` when you want it merged into the principal in a single round-trip.
376
+ > One `getUserInformation` implementation backs both routes. Use `GET /me` for the raw profile. Use `GET /who-am-i?withUserInformation=true` to get it merged with the principal in one round-trip.
991
377
 
992
- ### GET /certs (JWKS Issuer Only)
993
-
994
- **Authentication:** None (intentionally public)
995
-
996
- Returns the JSON Web Key Set for external verifiers.
378
+ **`GET /certs`** (Issuer mode only) - public, returns the JSON Web Key Set with `Cache-Control: public, max-age=3600, stale-while-revalidate=86400`.
997
379
 
998
380
  ```json
999
- {
1000
- "keys": [
1001
- {
1002
- "kty": "EC",
1003
- "kid": "my-key-id-1",
1004
- "use": "sig",
1005
- "alg": "ES256",
1006
- "crv": "P-256",
1007
- "x": "...",
1008
- "y": "..."
1009
- }
1010
- ]
1011
- }
381
+ { "keys": [{ "kty": "EC", "kid": "my-key-id-1", "use": "sig", "alg": "ES256", "crv": "P-256", "x": "...", "y": "..." }] }
1012
382
  ```
1013
383
 
1014
- **Cache headers:** `Cache-Control: public, max-age=3600, stale-while-revalidate=86400`
1015
-
1016
- ## Auth Entity Column Helpers
1017
-
1018
- IGNIS provides column helper functions that return pre-configured Drizzle column objects for common auth-related database tables. These functions are designed to be spread into `pgTable()` definitions, giving you standardized columns for User, Role, Permission, and PolicyDefinition entities without manually defining each column.
1019
-
1020
- All helpers that accept an `opts` parameter support `{ idType: 'string' | 'number' }` to control whether foreign key columns use `text` (for UUIDs) or `integer` (for serial IDs). The default is `'number'`.
1021
-
1022
- ### extraUserColumns
1023
-
1024
- **Import:** `import { extraUserColumns } from '@venizia/ignis';`
1025
-
1026
- **Signature:** `extraUserColumns(opts?: { idType: 'string' | 'number' })`
1027
-
1028
- | Column | DB Column | Type | Nullable | Default | Description |
1029
- |--------|-----------|------|----------|---------|-------------|
1030
- | `realm` | `realm` | `text` | Yes | `''` | Multi-tenancy realm identifier |
1031
- | `status` | `status` | `text` | No | `UserStatuses.UNKNOWN` | User lifecycle status |
1032
- | `type` | `type` | `text` | No | `UserTypes.SYSTEM` | User type (`SYSTEM` or `LINKED`) |
1033
- | `activatedAt` | `activated_at` | `timestamp (tz)` | Yes | `null` | When the user was activated |
1034
- | `lastLoginAt` | `last_login_at` | `timestamp (tz)` | Yes | `null` | Last login timestamp |
1035
- | `parentId` | `parent_id` | `text` or `integer` | Yes | `null` | Parent user ID (type depends on `idType`) |
1036
-
1037
- ### extraRoleColumns
1038
-
1039
- **Import:** `import { extraRoleColumns } from '@venizia/ignis';`
1040
-
1041
- **Signature:** `extraRoleColumns()`
1042
-
1043
- | Column | DB Column | Type | Nullable | Default | Description |
1044
- |--------|-----------|------|----------|---------|-------------|
1045
- | `identifier` | `identifier` | `text` (unique) | No | -- | Unique role identifier (e.g., `'admin'`, `'editor'`) |
1046
- | `name` | `name` | `text` | No | -- | Human-readable role name |
1047
- | `description` | `description` | `text` | Yes | `null` | Optional role description |
1048
- | `priority` | `priority` | `integer` | No | -- | Role priority (lower = higher priority) |
1049
- | `status` | `status` | `text` | No | `RoleStatuses.ACTIVATED` | Role lifecycle status |
384
+ ## Entity column helpers
1050
385
 
1051
- ### extraPermissionColumns
1052
-
1053
- **Import:** `import { extraPermissionColumns } from '@venizia/ignis';`
1054
-
1055
- **Signature:** `extraPermissionColumns(opts?: { idType: 'string' | 'number' })`
1056
-
1057
- | Column | DB Column | Type | Nullable | Default | Description |
1058
- |--------|-----------|------|----------|---------|-------------|
1059
- | `code` | `code` | `text` (unique) | No | -- | Unique permission code |
1060
- | `name` | `name` | `text` | No | -- | Permission display name |
1061
- | `subject` | `subject` | `text` | No | -- | Permission subject (e.g., `'User'`, `'Order'`) |
1062
- | `method` | `method` | `text` | No | -- | HTTP method (e.g., `'GET'`, `'POST'`) |
1063
- | `action` | `action` | `text` | No | -- | Permitted action (e.g., `'read'`, `'write'`) |
1064
- | `scope` | `scope` | `text` | No | -- | Permission scope |
1065
- | `description` | `description` | `text` | Yes | `null` | Optional permission description |
1066
- | `parentId` | `parent_id` | `text` or `integer` | Yes | `null` | Parent permission ID (type depends on `idType`) |
1067
-
1068
- ### extraPolicyDefinitionColumns
1069
-
1070
- **Import:** `import { extraPolicyDefinitionColumns } from '@venizia/ignis';`
1071
-
1072
- **Signature:** `extraPolicyDefinitionColumns(opts?: { idType: 'string' | 'number' })`
1073
-
1074
- Provides columns for Casbin-style policy definitions that map subjects (users/roles) to targets (resources/permissions).
1075
-
1076
- | Column | DB Column | Type | Nullable | Default | Description |
1077
- |--------|-----------|------|----------|---------|-------------|
1078
- | `variant` | `variant` | `text` | No | -- | Policy variant (e.g., `'p'` for policy, `'g'` for grouping) |
1079
- | `subjectType` | `subject_type` | `text` | No | -- | Type of subject (e.g., `'user'`, `'role'`) |
1080
- | `targetType` | `target_type` | `text` | No | -- | Type of target (e.g., `'permission'`, `'role'`) |
1081
- | `action` | `action` | `text` | Yes | `null` | Policy action |
1082
- | `effect` | `effect` | `text` | Yes | `null` | Policy effect (e.g., `'allow'`, `'deny'`) |
1083
- | `domain` | `domain` | `text` | Yes | `null` | Policy domain for multi-tenancy |
1084
- | `subjectId` | `subject_id` | `text` or `integer` | No | -- | Subject ID (type depends on `idType`) |
1085
- | `targetId` | `target_id` | `text` or `integer` | No | -- | Target ID (type depends on `idType`) |
1086
-
1087
- ### Usage Example
386
+ Column helper functions return pre-configured Drizzle columns for auth-related tables - spread them into `pgTable()` alongside your own columns.
1088
387
 
1089
388
  ```typescript
1090
- import { pgTable, serial, text } from 'drizzle-orm/pg-core';
389
+ import { pgTable, text } from 'drizzle-orm/pg-core';
1091
390
  import {
1092
- extraUserColumns,
1093
- extraRoleColumns,
1094
- extraPermissionColumns,
1095
- extraPolicyDefinitionColumns,
391
+ extraUserColumns, extraRoleColumns, extraPermissionColumns, extraPolicyDefinitionColumns,
392
+ generateIdColumnDefs, generateTzColumnDefs,
1096
393
  } from '@venizia/ignis';
1097
- import { withSerialId, withTimestamps } from '@venizia/ignis';
1098
394
 
1099
- // User table
1100
395
  export const users = pgTable('users', {
1101
- ...withSerialId(),
1102
- ...withTimestamps(),
396
+ ...generateIdColumnDefs(),
397
+ ...generateTzColumnDefs(),
1103
398
  ...extraUserColumns(),
1104
399
  username: text('username').unique().notNull(),
1105
400
  passwordHash: text('password_hash').notNull(),
1106
- email: text('email').unique(),
1107
- });
1108
-
1109
- // Role table
1110
- export const roles = pgTable('roles', {
1111
- ...withSerialId(),
1112
- ...withTimestamps(),
1113
- ...extraRoleColumns(),
1114
401
  });
1115
402
 
1116
- // Permission table
1117
- export const permissions = pgTable('permissions', {
1118
- ...withSerialId(),
1119
- ...withTimestamps(),
1120
- ...extraPermissionColumns(),
1121
- });
1122
-
1123
- // Policy definition table (Casbin-style policies)
1124
- export const policyDefinitions = pgTable('policy_definitions', {
1125
- ...withSerialId(),
1126
- ...withTimestamps(),
1127
- ...extraPolicyDefinitionColumns(),
1128
- });
1129
-
1130
- // With UUID-based IDs
1131
- export const uuidUsers = pgTable('users', {
1132
- ...withUuidId(),
1133
- ...withTimestamps(),
1134
- ...extraUserColumns({ idType: 'string' }),
1135
- username: text('username').unique().notNull(),
1136
- });
1137
-
1138
- export const uuidPolicies = pgTable('policy_definitions', {
1139
- ...withUuidId(),
1140
- ...withTimestamps(),
1141
- ...extraPolicyDefinitionColumns({ idType: 'string' }),
1142
- });
1143
- ```
1144
-
1145
- ### Context Variables
1146
-
1147
- The auth middleware sets several variables on the Hono `Context` object during request processing. These are declared via a `ContextVariableMap` module augmentation and can be accessed with `c.get()` / `c.set()`.
1148
-
1149
- | Constant | Key String | Type | Description |
1150
- |----------|-----------|------|-------------|
1151
- | `Authentication.CURRENT_USER` | `'auth.current.user'` | `IAuthUser` | The authenticated user payload, set after successful authentication |
1152
- | `Authentication.AUDIT_USER_ID` | `'audit.user.id'` | `IdType` | The authenticated user's ID, extracted from the user payload |
1153
- | `Authentication.SKIP_AUTHENTICATION` | `'authentication.skip'` | `boolean` | Set to `true` in a preceding middleware to bypass authentication for the current request |
1154
- | `Authorization.RULES` | `'authorization.rules'` | `unknown` | Authorization rules resolved for the current request |
1155
- | `Authorization.SKIP_AUTHORIZATION` | `'authorization.skip'` | `boolean` | Set to `true` to bypass authorization checks for the current request |
1156
-
1157
- **Reading context variables in a handler:**
1158
-
1159
- ```typescript
1160
- import { Authentication, Authorization } from '@venizia/ignis';
1161
-
1162
- // Inside a route handler
1163
- const currentUser = c.get(Authentication.CURRENT_USER);
1164
- const userId = c.get(Authentication.AUDIT_USER_ID);
1165
- const skipAuth = c.get(Authentication.SKIP_AUTHENTICATION);
1166
- const authzRules = c.get(Authorization.RULES);
1167
- ```
1168
-
1169
- **Skipping auth dynamically from middleware:**
1170
-
1171
- ```typescript
1172
- import { Authentication, Authorization } from '@venizia/ignis';
1173
- import { createMiddleware } from 'hono/factory';
1174
-
1175
- const apiKeyMiddleware = createMiddleware(async (c, next) => {
1176
- if (c.req.header('X-API-Key') === process.env.INTERNAL_API_KEY) {
1177
- c.set(Authentication.SKIP_AUTHENTICATION, true);
1178
- c.set(Authorization.SKIP_AUTHORIZATION, true);
1179
- }
1180
- return next();
1181
- });
1182
- ```
1183
-
1184
- ## See Also
1185
-
1186
- - [Setup & Configuration](./) -- Binding keys, options interfaces, and initial setup
1187
- - [API Reference](./api) -- Architecture, service internals, and strategy registry
1188
- - [Error Reference](./errors) -- Error messages and troubleshooting
403
+ export const roles = pgTable('roles', { ...generateIdColumnDefs(), ...generateTzColumnDefs(), ...extraRoleColumns() });
404
+ export const permissions = pgTable('permissions', { ...generateIdColumnDefs(), ...generateTzColumnDefs(), ...extraPermissionColumns() });
405
+ export const policyDefinitions = pgTable('policy_definitions', { ...generateIdColumnDefs(), ...generateTzColumnDefs(), ...extraPolicyDefinitionColumns() });
406
+ ```
407
+
408
+ **`extraUserColumns(opts?: { idType })`**
409
+
410
+ | Column | DB column | Type | Default | Description |
411
+ |--------|-----------|------|---------|-------------|
412
+ | `realm` | `realm` | `text` | `''` | Multi-tenancy realm identifier |
413
+ | `status` | `status` | `text` | `UserStatuses.UNKNOWN` | User lifecycle status |
414
+ | `type` | `type` | `text` | `UserTypes.SYSTEM` | `SYSTEM` or `LINKED` |
415
+ | `activatedAt` | `activated_at` | `timestamp (tz)` | `null` | Activation timestamp |
416
+ | `lastLoginAt` | `last_login_at` | `timestamp (tz)` | `null` | Last login timestamp |
417
+ | `parentId` | `parent_id` | `text` or `integer` | `null` | Depends on `idType` |
418
+
419
+ **`extraRoleColumns()`** - no options.
420
+
421
+ | Column | DB column | Type | Default | Description |
422
+ |--------|-----------|------|---------|-------------|
423
+ | `identifier` | `identifier` | `text` (unique) | -- | For example, `'admin'`, `'editor'` |
424
+ | `name` | `name` | `text` | -- | Human-readable name |
425
+ | `description` | `description` | `text` | `null` | Optional |
426
+ | `priority` | `priority` | `integer` | -- | Lower = higher priority |
427
+ | `status` | `status` | `text` | `RoleStatuses.ACTIVATED` | Role lifecycle status |
428
+
429
+ **`extraPermissionColumns(opts?: { idType })`**
430
+
431
+ | Column | DB column | Type | Default | Description |
432
+ |--------|-----------|------|---------|-------------|
433
+ | `code` | `code` | `text` (unique) | -- | Unique permission code |
434
+ | `name` | `name` | `text` | -- | Display name |
435
+ | `subject` | `subject` | `text` | -- | For example, `'User'`, `'Order'` |
436
+ | `method` | `method` | `text` | -- | For example, `'GET'`, `'POST'` |
437
+ | `action` | `action` | `text` | -- | For example, `'read'`, `'write'` |
438
+ | `scope` | `scope` | `text` | -- | Permission scope |
439
+ | `description` | `description` | `text` | `null` | Optional |
440
+ | `parentId` | `parent_id` | `text` or `integer` | `null` | Depends on `idType` |
441
+
442
+ **`extraPolicyDefinitionColumns(opts?: { idType })`** - Casbin-style policies mapping subjects to targets.
443
+
444
+ | Column | DB column | Type | Nullable | Description |
445
+ |--------|-----------|------|----------|-------------|
446
+ | `variant` | `variant` | `text` | No | One of the seven `AuthorizationPolicyVariants` edge kinds: `grant`, `assign_role`, `role_inherits`, `join_domain`, `domain_inherits`, `resource_inherits`, `action_inherits` |
447
+ | `subjectType` | `subject_type` | `text` | No | For example, `'user'`, `'Role'` |
448
+ | `targetType` | `target_type` | `text` | No | For example, `'Permission'`, `'Role'` |
449
+ | `action` | `action` | `text` | Yes | Policy action |
450
+ | `effect` | `effect` | `text` | Yes | `'allow'` / `'deny'` |
451
+ | `domain` | `domain` | `text` | Yes | Multi-tenancy domain |
452
+ | `subjectId` | `subject_id` | `text` or `integer` | No | Depends on `idType` |
453
+ | `targetId` | `target_id` | `text` or `integer` | No | Depends on `idType` |
454
+ | `metadata` | `metadata` | `jsonb` | Yes | Free-form metadata. Only some grants populate it |
455
+
456
+ All `idType` options default to `'number'` (`integer` columns). Pass `'string'` for `text` columns - UUID primary keys, for example.
457
+
458
+ `'p'` and `'g'` are Casbin rule prefixes, not `variant` values - each `variant` maps to one of them internally. See the [Authorization component](../authorization/) to build these tables end to end. Its [Usage guide](../authorization/usage) covers policy definitions, domain scoping, and the adapter that reads this table.
459
+
460
+ ## See also
461
+
462
+ - [Overview](./) - initial setup and binding keys
463
+ - [API Reference](./api) - full option tables, service class hierarchy, strategy registry
464
+ - [Error Reference](./errors) - every error message and how to fix it