@venizia/ignis-docs 0.2.0 → 0.2.1-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 (142) 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 +22 -11
  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 +26 -2
  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 +6 -2
  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 +182 -93
  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 +107 -322
  26. package/content/extensions/components/authentication/api.md +454 -603
  27. package/content/extensions/components/authentication/errors.md +121 -498
  28. package/content/extensions/components/authentication/index.md +88 -801
  29. package/content/extensions/components/authentication/usage.md +207 -956
  30. package/content/extensions/components/authorization/api.md +736 -656
  31. package/content/extensions/components/authorization/errors.md +168 -206
  32. package/content/extensions/components/authorization/index.md +82 -797
  33. package/content/extensions/components/authorization/usage.md +194 -527
  34. package/content/extensions/components/health-check.md +71 -243
  35. package/content/extensions/components/mail/api.md +504 -287
  36. package/content/extensions/components/mail/errors.md +73 -61
  37. package/content/extensions/components/mail/index.md +96 -467
  38. package/content/extensions/components/mail/usage.md +130 -172
  39. package/content/extensions/components/request-tracker.md +66 -173
  40. package/content/extensions/components/socket-io/api.md +195 -13
  41. package/content/extensions/components/socket-io/errors.md +3 -3
  42. package/content/extensions/components/socket-io/index.md +50 -337
  43. package/content/extensions/components/socket-io/usage.md +143 -26
  44. package/content/extensions/components/static-asset/api.md +410 -142
  45. package/content/extensions/components/static-asset/errors.md +110 -53
  46. package/content/extensions/components/static-asset/index.md +79 -608
  47. package/content/extensions/components/static-asset/usage.md +180 -300
  48. package/content/extensions/components/websocket/api.md +275 -399
  49. package/content/extensions/components/websocket/errors.md +47 -56
  50. package/content/extensions/components/websocket/index.md +74 -407
  51. package/content/extensions/components/websocket/usage.md +110 -341
  52. package/content/extensions/helpers/cron/index.md +51 -160
  53. package/content/extensions/helpers/crypto/index.md +62 -483
  54. package/content/extensions/helpers/crypto/reference.md +456 -0
  55. package/content/extensions/helpers/env/index.md +60 -178
  56. package/content/extensions/helpers/error/index.md +221 -207
  57. package/content/extensions/helpers/inversion/index.md +65 -556
  58. package/content/extensions/helpers/inversion/reference.md +522 -0
  59. package/content/extensions/helpers/kafka/admin.md +20 -1
  60. package/content/extensions/helpers/kafka/compile-binary.md +41 -35
  61. package/content/extensions/helpers/kafka/consumer.md +54 -20
  62. package/content/extensions/helpers/kafka/examples.md +21 -16
  63. package/content/extensions/helpers/kafka/index.md +80 -610
  64. package/content/extensions/helpers/kafka/producer.md +134 -5
  65. package/content/extensions/helpers/kafka/schema-registry.md +45 -70
  66. package/content/extensions/helpers/logger/hf-logger.md +193 -0
  67. package/content/extensions/helpers/logger/index.md +64 -563
  68. package/content/extensions/helpers/logger/pino.md +85 -0
  69. package/content/extensions/helpers/logger/reference.md +746 -0
  70. package/content/extensions/helpers/network/api.md +241 -195
  71. package/content/extensions/helpers/network/index.md +72 -530
  72. package/content/extensions/helpers/queue/index.md +72 -900
  73. package/content/extensions/helpers/queue/reference.md +467 -0
  74. package/content/extensions/helpers/redis/index.md +73 -645
  75. package/content/extensions/helpers/redis/reference.md +727 -0
  76. package/content/extensions/helpers/secrets/index.md +66 -0
  77. package/content/extensions/helpers/socket-io/api.md +305 -203
  78. package/content/extensions/helpers/socket-io/index.md +66 -432
  79. package/content/extensions/helpers/storage/api.md +564 -462
  80. package/content/extensions/helpers/storage/index.md +77 -573
  81. package/content/extensions/helpers/types/index.md +66 -499
  82. package/content/extensions/helpers/types/reference.md +650 -0
  83. package/content/extensions/helpers/uid/index.md +58 -227
  84. package/content/extensions/helpers/websocket/api.md +329 -216
  85. package/content/extensions/helpers/websocket/index.md +65 -503
  86. package/content/extensions/helpers/worker-thread/index.md +58 -396
  87. package/content/extensions/helpers/worker-thread/reference.md +428 -0
  88. package/content/guides/core-concepts/persistent/models.md +1 -1
  89. package/content/guides/core-concepts/persistent/search-typesense.md +3 -3
  90. package/content/guides/core-concepts/persistent/transactions.md +1 -1
  91. package/content/guides/core-concepts/secrets-vault.md +177 -0
  92. package/content/guides/core-concepts/services.md +1 -1
  93. package/content/guides/migrations/redis-helpers-migration.md +1 -1
  94. package/content/guides/migrations/unified-connectors-migration.md +2 -2
  95. package/content/guides/tutorials/ecommerce-api.md +3 -8
  96. package/content/references/base/application.md +1 -1
  97. package/content/references/base/connectors.md +79 -136
  98. package/content/references/base/datasources-reference.md +599 -0
  99. package/content/references/base/datasources.md +84 -444
  100. package/content/references/base/dependency-injection.md +17 -39
  101. package/content/references/base/filter-system/application-usage.md +69 -121
  102. package/content/references/base/filter-system/array-operators.md +12 -0
  103. package/content/references/base/filter-system/comparison-operators.md +12 -0
  104. package/content/references/base/filter-system/default-filter.md +136 -348
  105. package/content/references/base/filter-system/fields-order-pagination.md +38 -16
  106. package/content/references/base/filter-system/index.md +106 -257
  107. package/content/references/base/filter-system/json-filtering.md +12 -2
  108. package/content/references/base/filter-system/list-operators.md +16 -2
  109. package/content/references/base/filter-system/logical-operators.md +13 -0
  110. package/content/references/base/filter-system/null-operators.md +13 -0
  111. package/content/references/base/filter-system/pattern-matching.md +12 -0
  112. package/content/references/base/filter-system/quick-reference.md +11 -2
  113. package/content/references/base/filter-system/range-operators.md +12 -0
  114. package/content/references/base/filter-system/tips.md +70 -133
  115. package/content/references/base/filter-system/use-cases.md +156 -233
  116. package/content/references/base/middlewares.md +35 -21
  117. package/content/references/base/models-reference.md +886 -0
  118. package/content/references/base/models.md +80 -1452
  119. package/content/references/base/repositories/advanced.md +156 -192
  120. package/content/references/base/repositories/index.md +77 -650
  121. package/content/references/base/repositories/mixins.md +22 -18
  122. package/content/references/base/repositories/relations.md +123 -171
  123. package/content/references/base/repositories/soft-deletable.md +58 -56
  124. package/content/references/base/secrets.md +263 -0
  125. package/content/references/base/services.md +2 -2
  126. package/content/references/configuration/environment-variables.md +48 -4
  127. package/content/references/configuration/index.md +49 -31
  128. package/content/references/quick-reference.md +3 -16
  129. package/content/references/utilities/crypto.md +35 -76
  130. package/content/references/utilities/date.md +33 -73
  131. package/content/references/utilities/index.md +1 -1
  132. package/content/references/utilities/jsx-reference.md +298 -0
  133. package/content/references/utilities/jsx.md +82 -525
  134. package/content/references/utilities/module.md +29 -62
  135. package/content/references/utilities/parse.md +34 -64
  136. package/content/references/utilities/performance.md +33 -58
  137. package/content/references/utilities/promise.md +28 -62
  138. package/content/references/utilities/request.md +57 -218
  139. package/content/references/utilities/schema.md +43 -137
  140. package/content/references/utilities/statuses-reference.md +361 -0
  141. package/content/references/utilities/statuses.md +63 -667
  142. package/package.json +8 -8
@@ -1,89 +1,73 @@
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
+ ## Securing routes
12
+
13
+ **Require one strategy.** Add `authenticate` to the route config.
8
14
 
9
15
  ```typescript
10
- // Single strategy
11
16
  const SECURE_ROUTE_CONFIG = {
12
17
  path: '/secure-data',
13
18
  method: HTTP.Methods.GET,
14
19
  authenticate: { strategies: [Authentication.STRATEGY_JWT] },
15
- responses: jsonResponse({
16
- description: 'Protected data',
17
- schema: z.object({ message: z.string() }),
18
- }),
20
+ responses: jsonResponse({ description: 'Protected data', schema: z.object({ message: z.string() }) }),
19
21
  } as const;
22
+ ```
23
+
24
+ **Accept multiple strategies with fallback.** `mode: 'any'` (default) tries each in order; the first success wins.
20
25
 
21
- // Multiple strategies with fallback (any mode)
26
+ ```typescript
22
27
  const FALLBACK_AUTH_CONFIG = {
23
28
  path: '/api/data',
24
29
  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
- }),
30
+ authenticate: { strategies: [Authentication.STRATEGY_JWT, Authentication.STRATEGY_BASIC], mode: AuthenticationModes.ANY },
31
+ responses: jsonResponse({ description: 'Data via JWT or Basic', schema: z.object({ data: z.any() }) }),
33
32
  } as const;
33
+ ```
34
34
 
35
- // Skip authentication
35
+ **Make a route public.** `skip: true` bypasses authentication entirely.
36
+
37
+ ```typescript
36
38
  const PUBLIC_ROUTE_CONFIG = {
37
39
  path: '/public',
38
40
  method: HTTP.Methods.GET,
39
41
  authenticate: { skip: true },
40
- responses: jsonResponse({
41
- description: 'Public endpoint',
42
- schema: z.object({ message: z.string() }),
43
- }),
42
+ responses: jsonResponse({ description: 'Public endpoint', schema: z.object({ message: z.string() }) }),
44
43
  } as const;
45
44
  ```
46
45
 
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:
46
+ **Use `authenticate()` as raw Hono middleware.** Outside of route configs - e.g. for a plain Hono sub-app.
50
47
 
51
48
  ```typescript
52
49
  import { authenticate, Authentication, AuthenticationModes } from '@venizia/ignis';
53
50
 
54
- // Use as Hono middleware directly
55
- const authMiddleware = authenticate({
56
- strategies: [Authentication.STRATEGY_JWT],
57
- mode: AuthenticationModes.ANY,
58
- });
51
+ const authMiddleware = authenticate({ strategies: [Authentication.STRATEGY_JWT], mode: AuthenticationModes.ANY });
59
52
 
60
- // Apply to a Hono route
61
- app.get('/protected', authMiddleware, (c) => {
53
+ app.get('/protected', authMiddleware, c => {
62
54
  const user = c.get(Authentication.CURRENT_USER);
63
55
  return c.json({ userId: user.userId });
64
56
  });
65
57
  ```
66
58
 
67
- ## Accessing the Current User
68
-
69
- After authentication, the user payload is available on the Hono `Context`:
59
+ **Read the authenticated user in a handler.**
70
60
 
71
61
  ```typescript
72
- import { Context } from 'hono';
73
62
  import { Authentication, IJWTTokenPayload } from '@venizia/ignis';
74
63
 
75
- // Inside a route handler
76
64
  const user = c.get(Authentication.CURRENT_USER) as IJWTTokenPayload | undefined;
77
-
78
65
  if (user) {
79
- console.log('Authenticated user ID:', user.userId);
80
- console.log('User roles:', user.roles);
66
+ console.log('User ID:', user.userId, 'Roles:', user.roles);
81
67
  }
82
68
  ```
83
69
 
84
- ## Dynamic Skip Authentication
85
-
86
- Use `Authentication.SKIP_AUTHENTICATION` to dynamically skip auth in middleware:
70
+ **Skip authentication dynamically from a preceding middleware.** Useful for internal API keys or webhooks.
87
71
 
88
72
  ```typescript
89
73
  import { Authentication } from '@venizia/ignis';
@@ -97,35 +81,23 @@ const conditionalAuthMiddleware = createMiddleware(async (c, next) => {
97
81
  });
98
82
  ```
99
83
 
100
- ## Implementing an AuthenticationService
84
+ ## Implementing IAuthService
101
85
 
102
- The `AuthenticateComponent` depends on a service implementing the `IAuthService` interface when using the built-in auth controller.
86
+ The built-in auth controller (`useAuthController: true`) delegates every route to a service you provide, implementing `IAuthService`.
103
87
 
104
- ### JWS Example
88
+ **JWS-backed service.**
105
89
 
106
90
  ```typescript
107
91
  import {
108
- BaseService,
109
- inject,
110
- IAuthService,
111
- IJWTTokenPayload,
112
- JWSTokenService,
113
- BindingKeys,
114
- BindingNamespaces,
115
- TSignInRequest,
116
- TContext,
92
+ BaseService, inject, IAuthService, IJWTTokenPayload, JWSTokenService,
93
+ BindingKeys, BindingNamespaces, TSignInRequest, TContext,
117
94
  } from '@venizia/ignis';
118
95
  import { getError } from '@venizia/ignis-helpers';
119
96
  import { Env } from 'hono';
120
97
 
121
98
  export class AuthenticationService extends BaseService implements IAuthService {
122
99
  constructor(
123
- @inject({
124
- key: BindingKeys.build({
125
- namespace: BindingNamespaces.SERVICE,
126
- key: JWSTokenService.name,
127
- }),
128
- })
100
+ @inject({ key: BindingKeys.build({ namespace: BindingNamespaces.SERVICE, key: JWSTokenService.name }) })
129
101
  private _tokenService: JWSTokenService,
130
102
  ) {
131
103
  super({ scope: AuthenticationService.name });
@@ -139,611 +111,195 @@ export class AuthenticationService extends BaseService implements IAuthService {
139
111
  throw getError({ message: 'Invalid credentials' });
140
112
  }
141
113
 
142
- const payload: IJWTTokenPayload = {
143
- userId: user.id,
144
- roles: user.roles,
145
- };
146
-
114
+ const payload: IJWTTokenPayload = { userId: user.id, roles: user.roles };
147
115
  const token = await this._tokenService.generate({ payload });
148
116
  return { token };
149
117
  }
150
118
 
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
- }
119
+ async signUp(context: TContext<Env>, opts: any): Promise<any> { /* your logic */ }
120
+ async changePassword(context: TContext<Env>, opts: any): Promise<any> { /* your logic */ }
158
121
  }
159
122
  ```
160
123
 
161
- ### JWKS Issuer Example
124
+ **JWKS-backed service.** Same shape, inject `JWKSIssuerTokenService` instead.
162
125
 
163
126
  ```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';
127
+ constructor(
128
+ @inject({ key: BindingKeys.build({ namespace: BindingNamespaces.SERVICE, key: JWKSIssuerTokenService.name }) })
129
+ private _tokenService: JWKSIssuerTokenService,
130
+ ) { super({ scope: AuthenticationService.name }); }
131
+ ```
177
132
 
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
- }
133
+ **Implement `refreshToken` (optional).** Re-issues a token from the currently valid one - there is no separate refresh token.
190
134
 
191
- async signIn(context: TContext<Env>, opts: TSignInRequest): Promise<{ token: string }> {
192
- const { identifier, credential } = opts;
193
- // ... lookup and verify user ...
135
+ ```typescript
136
+ async refreshToken(context: TContext<Env>): Promise<{ token: string }> {
137
+ const currentUser = context.get(Authentication.CURRENT_USER);
138
+ const token = await this._tokenService.generate({ payload: currentUser });
139
+ return { token };
140
+ }
141
+ ```
194
142
 
195
- const payload: IJWTTokenPayload = {
196
- userId: user.id,
197
- roles: user.roles,
198
- };
143
+ > [!NOTE]
144
+ > IGNIS does not enforce rotation or revocation policy. If you need to invalidate old tokens after refresh, implement that inside your `refreshToken`.
199
145
 
200
- const token = await this._tokenService.generate({ payload });
201
- return { token };
202
- }
146
+ **Implement `getUserInformation` (optional).** Backs both `GET /me` and `GET /who-am-i?withUserInformation=true`.
203
147
 
204
- // ... signUp, changePassword ...
148
+ ```typescript
149
+ async getUserInformation(context: TContext<Env>, _opts: AnyObject): Promise<AnyObject> {
150
+ const currentUser = context.get(Authentication.CURRENT_USER);
151
+ return this.userRepository.findById({ id: currentUser.userId });
205
152
  }
206
153
  ```
207
154
 
208
- ## JWKS Microservice Patterns
209
-
210
- ### Issuer + Verifier Architecture
155
+ ## JWKS microservice patterns
211
156
 
212
- In a microservice architecture, one service issues tokens (issuer) and other services verify them (verifier):
157
+ **Issuer + verifier split.** One service signs, others only verify - no shared secret to distribute.
213
158
 
214
159
  ```mermaid
215
160
  flowchart LR
216
161
  CLIENT["Client App"]
217
-
218
162
  subgraph AUTH["Auth Service (JWKS Issuer)"]
219
163
  SIGNIN["POST /auth/sign-in"]
220
164
  CERTS["GET /certs"]
221
165
  end
222
-
223
166
  subgraph API["API Service (JWKS Verifier)"]
224
167
  DATA["GET /api/data"]
225
168
  end
226
-
227
169
  CLIENT -->|"1. Sign in"| SIGNIN
228
170
  SIGNIN -->|"2. JWT token"| CLIENT
229
171
  CLIENT -->|"3. Request + Bearer token"| DATA
230
172
  DATA -->|"4. Fetch JWKS"| CERTS
231
173
  CERTS -->|"5. Public keys"| DATA
232
174
  DATA -->|"6. Verified response"| CLIENT
233
-
234
- style AUTH fill:#e8f4fd,stroke:#0d6efd
235
- style API fill:#d4edda,stroke:#28a745
236
175
  ```
237
176
 
238
- **Auth Service (Issuer):**
239
177
  ```typescript
178
+ // Auth service (issuer)
240
179
  this.bind<TJWTTokenServiceOptions>({ key: AuthenticateBindingKeys.JWT_OPTIONS }).toValue({
241
180
  standard: JOSEStandards.JWKS,
242
181
  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,
182
+ mode: JWKSModes.ISSUER, algorithm: 'ES256',
183
+ keys: { driver: JWKSKeyDrivers.FILE, format: JWKSKeyFormats.PEM, private: './keys/private.pem', public: './keys/public.pem' },
184
+ kid: 'auth-key-1', getTokenExpiresFn: () => 86400,
253
185
  },
254
186
  });
255
- ```
256
187
 
257
- **API Service (Verifier):**
258
- ```typescript
188
+ // API service (verifier)
259
189
  this.bind<TJWTTokenServiceOptions>({ key: AuthenticateBindingKeys.JWT_OPTIONS }).toValue({
260
190
  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
- },
191
+ options: { mode: JWKSModes.VERIFIER, jwksUrl: 'https://auth-service.internal/certs', cacheTtlMs: 43_200_000, cooldownMs: 30_000 },
267
192
  });
268
193
  ```
269
194
 
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:
195
+ **Generate ES256 or RS256 keys.**
298
196
 
299
197
  ```bash
300
- # Generate private key
198
+ # ES256
301
199
  openssl ecparam -genkey -name prime256v1 -noout -out private.pem
302
-
303
- # Generate public key from private key
304
200
  openssl ec -in private.pem -pubout -out public.pem
305
- ```
306
-
307
- Generate RS256 keys:
308
201
 
309
- ```bash
310
- # Generate private key
202
+ # RS256
311
203
  openssl genrsa -out private.pem 2048
312
-
313
- # Generate public key from private key
314
204
  openssl rsa -in private.pem -pubout -out public.pem
315
205
  ```
316
206
 
317
207
  > [!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)
208
+ > Never commit private keys to version control.
321
209
 
322
- For environments where file access is restricted (e.g., serverless), use the `text` driver:
210
+ **Use inline keys instead of files.** For serverless or restricted-filesystem environments, switch `driver` to `text`.
323
211
 
324
212
  ```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,
213
+ keys: {
214
+ driver: JWKSKeyDrivers.TEXT,
215
+ format: JWKSKeyFormats.PEM,
216
+ private: process.env.JWKS_PRIVATE_KEY!, // PEM string from env
217
+ public: process.env.JWKS_PUBLIC_KEY!,
336
218
  }
337
219
  ```
338
220
 
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).
221
+ **Share AES payload encryption across issuer and verifier.** Both sides need the identical `applicationSecret` - the verifier decrypts what the issuer encrypted.
444
222
 
445
223
  ```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`) |
224
+ // Issuer
225
+ { mode: JWKSModes.ISSUER, /* ... */ applicationSecret: process.env.APP_ENV_APPLICATION_SECRET }
459
226
 
460
- ### ID Type Polymorphism
461
-
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' })
227
+ // Verifier - must match
228
+ { mode: JWKSModes.VERIFIER, jwksUrl: '...', applicationSecret: process.env.APP_ENV_APPLICATION_SECRET }
472
229
  ```
473
230
 
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.
231
+ ## Auth flows
477
232
 
478
- ### UserStatuses
479
-
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
233
+ - **JWS:** extract Bearer token -> `jose.jwtVerify()` with the shared secret -> decrypt payload (if AES configured) -> set `CURRENT_USER`.
234
+ - **JWKS Issuer:** extract Bearer token -> `ensureInitialized()` (lazy-loads keys once) -> `jwtVerify()` with the public key -> decrypt payload -> set `CURRENT_USER`.
235
+ - **JWKS Verifier:** extract Bearer token -> `ensureInitialized()` (creates the remote JWKS verifier once) -> `jwtVerify()` with the remote JWKS -> decrypt payload -> set `CURRENT_USER`.
236
+ - **Basic:** decode `Authorization: Basic <base64>` -> call your `verifyCredentials` callback -> on `null`, throw `401`; on a user, set `CURRENT_USER`.
512
237
 
513
238
  ```mermaid
514
239
  sequenceDiagram
515
240
  participant C as Client
516
241
  participant MW as Auth Middleware
517
- participant S as JWSAuthenticationStrategy
518
- participant SVC as JWSTokenService
519
- participant JOSE as jose library
242
+ participant S as Strategy
243
+ participant SVC as TokenService
520
244
 
521
- C->>MW: Request + Authorization: Bearer <token>
245
+ C->>MW: Request + Authorization header
522
246
  MW->>S: authenticate(context)
523
247
  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
248
+ SVC-->>S: { type, token } | { username, password }
249
+ S->>SVC: verify(...)
250
+ SVC-->>S: IAuthUser
530
251
  S-->>MW: IAuthUser
531
252
  MW->>MW: Set CURRENT_USER + AUDIT_USER_ID
532
253
  MW->>C: Continue to handler
533
254
  ```
534
255
 
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)`
256
+ ## Multi-strategy authentication
541
257
 
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'] }`:
258
+ | Mode | Behavior | Use case |
259
+ |------|----------|----------|
260
+ | `'any'` (default) | Strategies tried in order; first success wins; failures discarded (debug log); `401` with the tried-strategy list only if all fail | Fallback auth (JWT primary, Basic for legacy clients) |
261
+ | `'all'` | Every strategy must pass; first failure rejects immediately; the **first** strategy's user payload is the identity source | Multi-factor authentication |
652
262
 
653
263
  ```mermaid
654
264
  flowchart TD
655
265
  REQ["Request arrives"] --> MODE{"mode?"}
656
-
657
- MODE -->|"any (default)"| ANY["Try strategies in order"]
658
- ANY --> S1{"Strategy 1"}
266
+ MODE -->|"any"| S1{"Strategy 1"}
659
267
  S1 -->|"Success"| WIN["Set user, continue"]
660
268
  S1 -->|"Fail"| S2{"Strategy 2"}
661
269
  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"}
270
+ S2 -->|"Fail"| FAIL_ANY["401: Tried strategies"]
271
+ MODE -->|"all"| A1{"Strategy 1"}
666
272
  A1 -->|"Fail"| FAIL_ALL["Exception propagates"]
667
273
  A1 -->|"Pass"| A2{"Strategy 2"}
668
274
  A2 -->|"Fail"| FAIL_ALL
669
275
  A2 -->|"Pass"| CHECK{"userId?"}
670
276
  CHECK -->|"Yes"| WIN2["Set user, continue"]
671
277
  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
278
  ```
679
279
 
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)
686
-
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)
692
-
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)
280
+ ## Token encryption (optional AES)
697
281
 
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
282
+ - **Off by default.** AES payload encryption only activates when `applicationSecret` is set on the JWS/JWKS options - otherwise payloads are standard plaintext JWT.
283
+ - **Standard fields untouched.** `iss`, `sub`, `aud`, `jti`, `nbf`, `exp`, `iat` are never encrypted, on either side.
284
+ - **Everything else, key and value.** Every other payload field has both its key and its value AES-encrypted; `null`/`undefined` values are skipped.
285
+ - **Serialization is `JSON.stringify` unless you supply a codec.** `AuthenticationFieldCodecs.ROLES_CODEC` is a ready-made codec that serializes `roles` as pipe-separated `id|identifier|priority` strings - it is opt-in, not automatic. Pass it via `fieldCodecs: [AuthenticationFieldCodecs.ROLES_CODEC]` if you want that format instead of the default JSON array.
286
+ - **Secret must stay constant.** Changing `applicationSecret` invalidates every existing token. Issuer and verifier must share the identical secret (and identical `fieldCodecs`, if used).
709
287
 
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
288
+ ```typescript
289
+ this.bind<TJWTTokenServiceOptions>({ key: AuthenticateBindingKeys.JWT_OPTIONS }).toValue({
290
+ standard: JOSEStandards.JWS,
291
+ options: {
292
+ jwtSecret: process.env.APP_ENV_JWT_SECRET!,
293
+ getTokenExpiresFn: () => 86400,
294
+ applicationSecret: process.env.APP_ENV_APPLICATION_SECRET, // enables AES encryption
295
+ fieldCodecs: [AuthenticationFieldCodecs.ROLES_CODEC], // optional, opt-in
296
+ },
297
+ });
722
298
  ```
723
299
 
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
300
+ ## Hono context extension
734
301
 
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
745
-
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:
302
+ The module augments Hono's `ContextVariableMap` (a plain interface, not generic) so `c.get()` is type-safe:
747
303
 
748
304
  ```typescript
749
305
  declare module 'hono' {
@@ -754,435 +310,130 @@ declare module 'hono' {
754
310
  }
755
311
  ```
756
312
 
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.
313
+ | Constant | Key string | Type | Description |
314
+ |----------|-----------|------|-------------|
315
+ | `Authentication.CURRENT_USER` | `auth.current.user` | `IAuthUser` | Authenticated user payload |
316
+ | `Authentication.AUDIT_USER_ID` | `audit.user.id` | `IdType` | Authenticated user's ID |
317
+ | `Authentication.SKIP_AUTHENTICATION` | `authentication.skip` | `boolean` | Set `true` to bypass authentication |
841
318
 
842
- ## API Endpoints
319
+ ## API endpoints
843
320
 
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`.
321
+ The built-in auth controller exists only when `REST_OPTIONS.useAuthController: true` is set - see [Setup](./#common-tasks).
845
322
 
846
- | Method | Path | Auth Required | Description |
323
+ | Method | Path | Auth required | Description |
847
324
  |--------|------|---------------|-------------|
848
- | `POST` | `/auth/sign-in` | No | Authenticate and receive a JWT token |
849
- | `POST` | `/auth/sign-up` | Configurable | Create a new user account |
325
+ | `POST` | `/auth/sign-in` | No | Authenticate, receive a JWT |
326
+ | `POST` | `/auth/sign-up` | Configurable (`requireAuthenticatedSignUp`) | Create a user account |
850
327
  | `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) |
328
+ | `POST` | `/auth/token/refresh` | JWT | Re-issue a token from the caller's valid JWT |
329
+ | `GET` | `/auth/who-am-i` | JWT | Return the JWT payload, optionally merged with `getUserInformation` |
330
+ | `GET` | `/auth/me` | JWT | Return `getUserInformation` result directly |
331
+ | `GET` | `/certs` | No | JWKS endpoint (Issuer mode only) |
855
332
 
856
333
  > [!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
334
+ > `/auth` is configurable via `controllerOpts.restPath`; `/certs` via `rest.path` in `IJWKSIssuerOptions`. `/certs` is intentionally unauthenticated.
860
335
 
861
- **Authentication:** None
862
-
863
- **Request Body:**
864
-
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`.
336
+ **`POST /auth/sign-in`** - body defaults to `SignInRequestSchema` (nested `identifier`/`credential`), overridable via `payload.signIn`.
870
337
 
871
338
  ```json
872
- {
873
- "token": "eyJhbGciOiJFUzI1NiIsImtpZCI6Im15LWtleS1pZC0xIn0..."
874
- }
339
+ { "token": "eyJhbGciOiJFUzI1NiIsImtpZCI6Im15LWtleS1pZC0xIn0..." }
875
340
  ```
876
341
 
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.
882
-
883
- ### POST /auth/change-password
884
-
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.
922
-
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.
342
+ **`POST /auth/sign-up`** - public unless `requireAuthenticatedSignUp: true`. Body defaults to a flat `SignUpRequestSchema` (`username`, `credential` - unlike sign-in's nested shape).
928
343
 
929
- ```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
- }
939
- ```
344
+ **`POST /auth/change-password`** - always requires JWT. Body defaults to `ChangePasswordRequestSchema` (`scheme`, `oldCredential`, `newCredential`, `userId`).
940
345
 
941
- **Query Parameters:**
346
+ **`POST /auth/token/refresh`** - always requires JWT, no request body. Returns `501` if `IAuthService.refreshToken` isn't implemented.
942
347
 
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:
348
+ **`GET /auth/who-am-i`** - always requires JWT. Query param `withUserInformation` (`true`/`false`/`1`/`0`, default `false`) attaches a `userInformation` field from `getUserInformation`; returns `501` if that method is truthy-requested but not implemented.
948
349
 
949
350
  ```json
950
- {
951
- "userId": "123",
952
- "roles": [{ "id": "1", "identifier": "admin", "priority": 0 }],
953
- "userInformation": { "fullName": "Ada Lovelace", "department": "R&D" }
954
- }
351
+ { "userId": "123", "roles": [{ "id": "1", "identifier": "admin", "priority": 0 }] }
955
352
  ```
956
353
 
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
- ```
354
+ **`GET /auth/me`** - always requires JWT. Delegates entirely to `getUserInformation(context, {})` - the response is not merged with the JWT payload. Returns `501` if not implemented.
988
355
 
989
356
  > [!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.
991
-
992
- ### GET /certs (JWKS Issuer Only)
357
+ > One `getUserInformation` implementation backs both routes: use `GET /me` for the raw profile, `GET /who-am-i?withUserInformation=true` to get it merged with the principal in one round-trip.
993
358
 
994
- **Authentication:** None (intentionally public)
995
-
996
- Returns the JSON Web Key Set for external verifiers.
359
+ **`GET /certs`** (Issuer mode only) - public, returns the JSON Web Key Set with `Cache-Control: public, max-age=3600, stale-while-revalidate=86400`.
997
360
 
998
361
  ```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
- }
362
+ { "keys": [{ "kty": "EC", "kid": "my-key-id-1", "use": "sig", "alg": "ES256", "crv": "P-256", "x": "...", "y": "..." }] }
1012
363
  ```
1013
364
 
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 |
1050
-
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).
365
+ ## Entity column helpers
1075
366
 
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
367
+ Column helper functions return pre-configured Drizzle columns for auth-related tables - spread them into `pgTable()` alongside your own columns.
1088
368
 
1089
369
  ```typescript
1090
- import { pgTable, serial, text } from 'drizzle-orm/pg-core';
1091
- import {
1092
- extraUserColumns,
1093
- extraRoleColumns,
1094
- extraPermissionColumns,
1095
- extraPolicyDefinitionColumns,
1096
- } from '@venizia/ignis';
1097
- import { withSerialId, withTimestamps } from '@venizia/ignis';
370
+ import { pgTable, text } from 'drizzle-orm/pg-core';
371
+ import { extraUserColumns, extraRoleColumns, extraPermissionColumns, extraPolicyDefinitionColumns, withSerialId, withTimestamps } from '@venizia/ignis';
1098
372
 
1099
- // User table
1100
373
  export const users = pgTable('users', {
1101
374
  ...withSerialId(),
1102
375
  ...withTimestamps(),
1103
376
  ...extraUserColumns(),
1104
377
  username: text('username').unique().notNull(),
1105
378
  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
- });
1115
-
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
379
  });
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
380
 
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
381
+ export const roles = pgTable('roles', { ...withSerialId(), ...withTimestamps(), ...extraRoleColumns() });
382
+ export const permissions = pgTable('permissions', { ...withSerialId(), ...withTimestamps(), ...extraPermissionColumns() });
383
+ export const policyDefinitions = pgTable('policy_definitions', { ...withSerialId(), ...withTimestamps(), ...extraPolicyDefinitionColumns() });
384
+ ```
385
+
386
+ **`extraUserColumns(opts?: { idType })`**
387
+
388
+ | Column | DB column | Type | Default | Description |
389
+ |--------|-----------|------|---------|-------------|
390
+ | `realm` | `realm` | `text` | `''` | Multi-tenancy realm identifier |
391
+ | `status` | `status` | `text` | `UserStatuses.UNKNOWN` | User lifecycle status |
392
+ | `type` | `type` | `text` | `UserTypes.SYSTEM` | `SYSTEM` or `LINKED` |
393
+ | `activatedAt` | `activated_at` | `timestamp (tz)` | `null` | Activation timestamp |
394
+ | `lastLoginAt` | `last_login_at` | `timestamp (tz)` | `null` | Last login timestamp |
395
+ | `parentId` | `parent_id` | `text` or `integer` | `null` | Depends on `idType` |
396
+
397
+ **`extraRoleColumns()`** - no options.
398
+
399
+ | Column | DB column | Type | Default | Description |
400
+ |--------|-----------|------|---------|-------------|
401
+ | `identifier` | `identifier` | `text` (unique) | -- | e.g. `'admin'`, `'editor'` |
402
+ | `name` | `name` | `text` | -- | Human-readable name |
403
+ | `description` | `description` | `text` | `null` | Optional |
404
+ | `priority` | `priority` | `integer` | -- | Lower = higher priority |
405
+ | `status` | `status` | `text` | `RoleStatuses.ACTIVATED` | Role lifecycle status |
406
+
407
+ **`extraPermissionColumns(opts?: { idType })`**
408
+
409
+ | Column | DB column | Type | Default | Description |
410
+ |--------|-----------|------|---------|-------------|
411
+ | `code` | `code` | `text` (unique) | -- | Unique permission code |
412
+ | `name` | `name` | `text` | -- | Display name |
413
+ | `subject` | `subject` | `text` | -- | e.g. `'User'`, `'Order'` |
414
+ | `method` | `method` | `text` | -- | e.g. `'GET'`, `'POST'` |
415
+ | `action` | `action` | `text` | -- | e.g. `'read'`, `'write'` |
416
+ | `scope` | `scope` | `text` | -- | Permission scope |
417
+ | `description` | `description` | `text` | `null` | Optional |
418
+ | `parentId` | `parent_id` | `text` or `integer` | `null` | Depends on `idType` |
419
+
420
+ **`extraPolicyDefinitionColumns(opts?: { idType })`** - Casbin-style policies mapping subjects to targets.
421
+
422
+ | Column | DB column | Type | Nullable | Description |
423
+ |--------|-----------|------|----------|-------------|
424
+ | `variant` | `variant` | `text` | No | `'p'` (policy) or `'g'` (grouping) |
425
+ | `subjectType` | `subject_type` | `text` | No | e.g. `'user'`, `'role'` |
426
+ | `targetType` | `target_type` | `text` | No | e.g. `'permission'`, `'role'` |
427
+ | `action` | `action` | `text` | Yes | Policy action |
428
+ | `effect` | `effect` | `text` | Yes | `'allow'` / `'deny'` |
429
+ | `domain` | `domain` | `text` | Yes | Multi-tenancy domain |
430
+ | `subjectId` | `subject_id` | `text` or `integer` | No | Depends on `idType` |
431
+ | `targetId` | `target_id` | `text` or `integer` | No | Depends on `idType` |
432
+
433
+ All `idType` options default to `'number'` (`integer` columns); pass `'string'` for `text` (e.g. UUID) columns.
434
+
435
+ ## See also
436
+
437
+ - [Overview](./) - initial setup and binding keys
438
+ - [API Reference](./api) - full option tables, service class hierarchy, strategy registry
439
+ - [Error Reference](./errors) - every error message and how to fix it