@venizia/ignis-docs 0.0.6-3 → 0.0.7-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 (113) hide show
  1. package/README.md +125 -388
  2. package/dist/mcp-server/common/config.d.ts +0 -21
  3. package/dist/mcp-server/common/config.d.ts.map +1 -1
  4. package/dist/mcp-server/common/config.js +1 -36
  5. package/dist/mcp-server/common/config.js.map +1 -1
  6. package/dist/mcp-server/helpers/docs.helper.d.ts +0 -24
  7. package/dist/mcp-server/helpers/docs.helper.d.ts.map +1 -1
  8. package/dist/mcp-server/helpers/docs.helper.js +0 -25
  9. package/dist/mcp-server/helpers/docs.helper.js.map +1 -1
  10. package/dist/mcp-server/helpers/github.helper.d.ts +0 -13
  11. package/dist/mcp-server/helpers/github.helper.d.ts.map +1 -1
  12. package/dist/mcp-server/helpers/github.helper.js +3 -20
  13. package/dist/mcp-server/helpers/github.helper.js.map +1 -1
  14. package/dist/mcp-server/index.js +0 -20
  15. package/dist/mcp-server/index.js.map +1 -1
  16. package/dist/mcp-server/tools/base.tool.d.ts +2 -79
  17. package/dist/mcp-server/tools/base.tool.d.ts.map +1 -1
  18. package/dist/mcp-server/tools/base.tool.js +1 -38
  19. package/dist/mcp-server/tools/base.tool.js.map +1 -1
  20. package/dist/mcp-server/tools/docs/get-document-content.tool.d.ts.map +1 -1
  21. package/dist/mcp-server/tools/docs/get-document-content.tool.js +0 -9
  22. package/dist/mcp-server/tools/docs/get-document-content.tool.js.map +1 -1
  23. package/dist/mcp-server/tools/docs/get-document-metadata.tool.d.ts.map +1 -1
  24. package/dist/mcp-server/tools/docs/get-document-metadata.tool.js +0 -9
  25. package/dist/mcp-server/tools/docs/get-document-metadata.tool.js.map +1 -1
  26. package/dist/mcp-server/tools/docs/get-package-overview.tool.d.ts +0 -6
  27. package/dist/mcp-server/tools/docs/get-package-overview.tool.d.ts.map +1 -1
  28. package/dist/mcp-server/tools/docs/get-package-overview.tool.js +1 -24
  29. package/dist/mcp-server/tools/docs/get-package-overview.tool.js.map +1 -1
  30. package/dist/mcp-server/tools/docs/list-categories.tool.d.ts.map +1 -1
  31. package/dist/mcp-server/tools/docs/list-categories.tool.js +0 -9
  32. package/dist/mcp-server/tools/docs/list-categories.tool.js.map +1 -1
  33. package/dist/mcp-server/tools/docs/list-documents.tool.d.ts.map +1 -1
  34. package/dist/mcp-server/tools/docs/list-documents.tool.js +0 -9
  35. package/dist/mcp-server/tools/docs/list-documents.tool.js.map +1 -1
  36. package/dist/mcp-server/tools/docs/search-documents.tool.d.ts.map +1 -1
  37. package/dist/mcp-server/tools/docs/search-documents.tool.js +0 -9
  38. package/dist/mcp-server/tools/docs/search-documents.tool.js.map +1 -1
  39. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts.map +1 -1
  40. package/dist/mcp-server/tools/github/list-project-files.tool.js +0 -9
  41. package/dist/mcp-server/tools/github/list-project-files.tool.js.map +1 -1
  42. package/dist/mcp-server/tools/github/search-code.tool.d.ts.map +1 -1
  43. package/dist/mcp-server/tools/github/search-code.tool.js +1 -13
  44. package/dist/mcp-server/tools/github/search-code.tool.js.map +1 -1
  45. package/dist/mcp-server/tools/github/verify-dependencies.tool.d.ts +0 -4
  46. package/dist/mcp-server/tools/github/verify-dependencies.tool.d.ts.map +1 -1
  47. package/dist/mcp-server/tools/github/verify-dependencies.tool.js +1 -18
  48. package/dist/mcp-server/tools/github/verify-dependencies.tool.js.map +1 -1
  49. package/dist/mcp-server/tools/github/view-source-file.tool.d.ts.map +1 -1
  50. package/dist/mcp-server/tools/github/view-source-file.tool.js +0 -9
  51. package/dist/mcp-server/tools/github/view-source-file.tool.js.map +1 -1
  52. package/dist/mcp-server/tools/index.d.ts.map +1 -1
  53. package/dist/mcp-server/tools/index.js +0 -2
  54. package/dist/mcp-server/tools/index.js.map +1 -1
  55. package/package.json +1 -1
  56. package/wiki/best-practices/api-usage-examples.md +7 -5
  57. package/wiki/best-practices/code-style-standards/advanced-patterns.md +1 -1
  58. package/wiki/best-practices/code-style-standards/constants-configuration.md +1 -1
  59. package/wiki/best-practices/code-style-standards/control-flow.md +1 -1
  60. package/wiki/best-practices/code-style-standards/function-patterns.md +1 -1
  61. package/wiki/best-practices/common-pitfalls.md +1 -1
  62. package/wiki/best-practices/data-modeling.md +33 -1
  63. package/wiki/best-practices/error-handling.md +7 -4
  64. package/wiki/best-practices/performance-optimization.md +1 -1
  65. package/wiki/best-practices/security-guidelines.md +5 -4
  66. package/wiki/guides/core-concepts/components-guide.md +1 -1
  67. package/wiki/guides/core-concepts/controllers.md +14 -8
  68. package/wiki/guides/core-concepts/persistent/models.md +32 -0
  69. package/wiki/guides/core-concepts/services.md +2 -1
  70. package/wiki/guides/get-started/5-minute-quickstart.md +1 -1
  71. package/wiki/guides/tutorials/building-a-crud-api.md +2 -1
  72. package/wiki/guides/tutorials/complete-installation.md +2 -2
  73. package/wiki/guides/tutorials/ecommerce-api.md +3 -3
  74. package/wiki/guides/tutorials/realtime-chat.md +7 -6
  75. package/wiki/index.md +2 -1
  76. package/wiki/references/base/application.md +28 -0
  77. package/wiki/references/base/components.md +2 -1
  78. package/wiki/references/base/controllers.md +31 -4
  79. package/wiki/references/base/datasources.md +6 -2
  80. package/wiki/references/base/dependency-injection.md +31 -0
  81. package/wiki/references/base/filter-system/fields-order-pagination.md +8 -1
  82. package/wiki/references/base/middlewares.md +2 -1
  83. package/wiki/references/base/models.md +144 -2
  84. package/wiki/references/base/repositories/advanced.md +2 -2
  85. package/wiki/references/base/repositories/index.md +24 -1
  86. package/wiki/references/base/repositories/soft-deletable.md +213 -0
  87. package/wiki/references/base/services.md +2 -1
  88. package/wiki/references/components/authentication/api.md +525 -205
  89. package/wiki/references/components/authentication/errors.md +502 -105
  90. package/wiki/references/components/authentication/index.md +388 -75
  91. package/wiki/references/components/authentication/usage.md +575 -247
  92. package/wiki/references/components/authorization/usage.md +62 -0
  93. package/wiki/references/components/health-check.md +2 -1
  94. package/wiki/references/components/socket-io/index.md +9 -4
  95. package/wiki/references/components/socket-io/usage.md +1 -1
  96. package/wiki/references/components/static-asset/index.md +3 -5
  97. package/wiki/references/components/swagger.md +2 -1
  98. package/wiki/references/configuration/environment-variables.md +2 -1
  99. package/wiki/references/configuration/index.md +40 -1
  100. package/wiki/references/helpers/error/index.md +1 -1
  101. package/wiki/references/helpers/inversion/index.md +1 -1
  102. package/wiki/references/helpers/redis/index.md +2 -9
  103. package/wiki/references/quick-reference.md +3 -5
  104. package/wiki/references/utilities/crypto.md +2 -2
  105. package/wiki/references/utilities/date.md +5 -5
  106. package/wiki/references/utilities/index.md +3 -11
  107. package/wiki/references/utilities/jsx.md +4 -2
  108. package/wiki/references/utilities/module.md +1 -1
  109. package/wiki/references/utilities/parse.md +24 -4
  110. package/wiki/references/utilities/performance.md +2 -2
  111. package/wiki/references/utilities/promise.md +4 -4
  112. package/wiki/references/utilities/request.md +2 -2
  113. package/wiki/references/utilities/schema.md +17 -8
@@ -1,17 +1,17 @@
1
1
  # Authentication -- Usage & Examples
2
2
 
3
- > Securing routes, authentication flows, entity helpers, and API endpoint specifications. See [Setup & Configuration](./) for initial setup.
3
+ > Securing routes, authentication flows, JWKS microservice patterns, entity helpers, and API endpoint specifications. See [Setup & Configuration](./) for initial setup.
4
4
 
5
5
  ## Securing Routes
6
6
 
7
- Use `authStrategies` and `authMode` in route configurations:
7
+ Use the `authenticate` field in route configurations. The field accepts `TRouteAuthenticateConfig`:
8
8
 
9
9
  ```typescript
10
10
  // Single strategy
11
11
  const SECURE_ROUTE_CONFIG = {
12
12
  path: '/secure-data',
13
13
  method: HTTP.Methods.GET,
14
- authStrategies: [Authentication.STRATEGY_JWT],
14
+ authenticate: { strategies: [Authentication.STRATEGY_JWT] },
15
15
  responses: jsonResponse({
16
16
  description: 'Protected data',
17
17
  schema: z.object({ message: z.string() }),
@@ -22,8 +22,10 @@ const SECURE_ROUTE_CONFIG = {
22
22
  const FALLBACK_AUTH_CONFIG = {
23
23
  path: '/api/data',
24
24
  method: HTTP.Methods.GET,
25
- authStrategies: [Authentication.STRATEGY_JWT, Authentication.STRATEGY_BASIC],
26
- authMode: AuthenticationModes.ANY,
25
+ authenticate: {
26
+ strategies: [Authentication.STRATEGY_JWT, Authentication.STRATEGY_BASIC],
27
+ mode: AuthenticationModes.ANY,
28
+ },
27
29
  responses: jsonResponse({
28
30
  description: 'Data accessible via JWT or Basic auth',
29
31
  schema: z.object({ data: z.any() }),
@@ -34,7 +36,7 @@ const FALLBACK_AUTH_CONFIG = {
34
36
  const PUBLIC_ROUTE_CONFIG = {
35
37
  path: '/public',
36
38
  method: HTTP.Methods.GET,
37
- skipAuth: true,
39
+ authenticate: { skip: true },
38
40
  responses: jsonResponse({
39
41
  description: 'Public endpoint',
40
42
  schema: z.object({ message: z.string() }),
@@ -44,7 +46,7 @@ const PUBLIC_ROUTE_CONFIG = {
44
46
 
45
47
  ## Using the `authenticate()` Standalone Function
46
48
 
47
- The `authenticate()` function is a convenience wrapper around `AuthenticationStrategyRegistry.getInstance().authenticate()`. It returns a Hono `MiddlewareHandler` suitable for direct middleware usage:
49
+ The `authenticate()` function creates an `AuthenticationProvider` instance and uses its middleware factory. It returns a Hono `MiddlewareHandler` suitable for direct middleware usage:
48
50
 
49
51
  ```typescript
50
52
  import { authenticate, Authentication, AuthenticationModes } from '@venizia/ignis';
@@ -97,7 +99,9 @@ const conditionalAuthMiddleware = createMiddleware(async (c, next) => {
97
99
 
98
100
  ## Implementing an AuthenticationService
99
101
 
100
- The `AuthenticateComponent` depends on a service implementing the `IAuthService` interface when using the built-in auth controller:
102
+ The `AuthenticateComponent` depends on a service implementing the `IAuthService` interface when using the built-in auth controller.
103
+
104
+ ### JWS Example
101
105
 
102
106
  ```typescript
103
107
  import {
@@ -105,21 +109,29 @@ import {
105
109
  inject,
106
110
  IAuthService,
107
111
  IJWTTokenPayload,
108
- JWTTokenService,
112
+ JWSTokenService,
113
+ BindingKeys,
114
+ BindingNamespaces,
109
115
  TSignInRequest,
110
- getError,
116
+ TContext,
111
117
  } from '@venizia/ignis';
112
- import { Context } from 'hono';
118
+ import { getError } from '@venizia/ignis-helpers';
119
+ import { Env } from 'hono';
113
120
 
114
121
  export class AuthenticationService extends BaseService implements IAuthService {
115
122
  constructor(
116
- @inject({ key: 'services.JWTTokenService' })
117
- private _jwtTokenService: JWTTokenService,
123
+ @inject({
124
+ key: BindingKeys.build({
125
+ namespace: BindingNamespaces.SERVICE,
126
+ key: JWSTokenService.name,
127
+ }),
128
+ })
129
+ private _tokenService: JWSTokenService,
118
130
  ) {
119
131
  super({ scope: AuthenticationService.name });
120
132
  }
121
133
 
122
- async signIn(context: Context, opts: TSignInRequest): Promise<{ token: string }> {
134
+ async signIn(context: TContext<Env>, opts: TSignInRequest): Promise<{ token: string }> {
123
135
  const { identifier, credential } = opts;
124
136
  const user = await this.userRepo.findByIdentifier(identifier);
125
137
 
@@ -132,20 +144,198 @@ export class AuthenticationService extends BaseService implements IAuthService {
132
144
  roles: user.roles,
133
145
  };
134
146
 
135
- const token = await this._jwtTokenService.generate({ payload });
147
+ const token = await this._tokenService.generate({ payload });
136
148
  return { token };
137
149
  }
138
150
 
139
- async signUp(context: Context, opts: any): Promise<any> {
151
+ async signUp(context: TContext<Env>, opts: any): Promise<any> {
140
152
  // Implement your sign-up logic
141
153
  }
142
154
 
143
- async changePassword(context: Context, opts: any): Promise<any> {
155
+ async changePassword(context: TContext<Env>, opts: any): Promise<any> {
144
156
  // Implement your change password logic
145
157
  }
146
158
  }
147
159
  ```
148
160
 
161
+ ### JWKS Issuer Example
162
+
163
+ ```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';
177
+
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
+ }
190
+
191
+ async signIn(context: TContext<Env>, opts: TSignInRequest): Promise<{ token: string }> {
192
+ const { identifier, credential } = opts;
193
+ // ... lookup and verify user ...
194
+
195
+ const payload: IJWTTokenPayload = {
196
+ userId: user.id,
197
+ roles: user.roles,
198
+ };
199
+
200
+ const token = await this._tokenService.generate({ payload });
201
+ return { token };
202
+ }
203
+
204
+ // ... signUp, changePassword ...
205
+ }
206
+ ```
207
+
208
+ ## JWKS Microservice Patterns
209
+
210
+ ### Issuer + Verifier Architecture
211
+
212
+ In a microservice architecture, one service issues tokens (issuer) and other services verify them (verifier):
213
+
214
+ ```mermaid
215
+ flowchart LR
216
+ CLIENT["Client App"]
217
+
218
+ subgraph AUTH["Auth Service (JWKS Issuer)"]
219
+ SIGNIN["POST /auth/sign-in"]
220
+ CERTS["GET /certs"]
221
+ end
222
+
223
+ subgraph API["API Service (JWKS Verifier)"]
224
+ DATA["GET /api/data"]
225
+ end
226
+
227
+ CLIENT -->|"1. Sign in"| SIGNIN
228
+ SIGNIN -->|"2. JWT token"| CLIENT
229
+ CLIENT -->|"3. Request + Bearer token"| DATA
230
+ DATA -->|"4. Fetch JWKS"| CERTS
231
+ CERTS -->|"5. Public keys"| DATA
232
+ DATA -->|"6. Verified response"| CLIENT
233
+
234
+ style AUTH fill:#e8f4fd,stroke:#0d6efd
235
+ style API fill:#d4edda,stroke:#28a745
236
+ ```
237
+
238
+ **Auth Service (Issuer):**
239
+ ```typescript
240
+ this.bind<TJWTTokenServiceOptions>({ key: AuthenticateBindingKeys.JWT_OPTIONS }).toValue({
241
+ standard: JOSEStandards.JWKS,
242
+ 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,
253
+ },
254
+ });
255
+ ```
256
+
257
+ **API Service (Verifier):**
258
+ ```typescript
259
+ this.bind<TJWTTokenServiceOptions>({ key: AuthenticateBindingKeys.JWT_OPTIONS }).toValue({
260
+ 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
+ },
267
+ });
268
+ ```
269
+
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:
298
+
299
+ ```bash
300
+ # Generate private key
301
+ openssl ecparam -genkey -name prime256v1 -noout -out private.pem
302
+
303
+ # Generate public key from private key
304
+ openssl ec -in private.pem -pubout -out public.pem
305
+ ```
306
+
307
+ Generate RS256 keys:
308
+
309
+ ```bash
310
+ # Generate private key
311
+ openssl genrsa -out private.pem 2048
312
+
313
+ # Generate public key from private key
314
+ openssl rsa -in private.pem -pubout -out public.pem
315
+ ```
316
+
317
+ > [!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)
321
+
322
+ For environments where file access is restricted (e.g., serverless), use the `text` driver:
323
+
324
+ ```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,
336
+ }
337
+ ```
338
+
149
339
  ## Entity Column Helpers
150
340
 
151
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.
@@ -332,24 +522,139 @@ Inherits all statuses from `CommonStatuses` (same values as `UserStatuses`):
332
522
 
333
523
  ## Auth Flows
334
524
 
335
- ### JWT Authentication Flow
525
+ ### JWS Authentication Flow
526
+
527
+ ```mermaid
528
+ sequenceDiagram
529
+ participant C as Client
530
+ participant MW as Auth Middleware
531
+ participant S as JWSAuthenticationStrategy
532
+ participant SVC as JWSTokenService
533
+ participant JOSE as jose library
534
+
535
+ C->>MW: Request + Authorization: Bearer <token>
536
+ MW->>S: authenticate(context)
537
+ S->>SVC: extractCredentials(context)
538
+ SVC-->>S: { type: "Bearer", token }
539
+ S->>SVC: verify({ type, token })
540
+ SVC->>JOSE: jwtVerify(token, jwtSecret)
541
+ JOSE-->>SVC: JWTVerifyResult
542
+ SVC->>SVC: decryptPayload() (if AES configured)
543
+ SVC-->>S: IJWTTokenPayload
544
+ S-->>MW: IAuthUser
545
+ MW->>MW: Set CURRENT_USER + AUDIT_USER_ID
546
+ MW->>C: Continue to handler
547
+ ```
336
548
 
337
549
  1. **Client sends request** with <code v-pre>Authorization: Bearer &lt;token&gt;</code> header
338
- 2. **JWTAuthenticationStrategy.authenticate()** is called by the Hono middleware
339
- 3. **JWTTokenService.extractCredentials()** extracts the token from the Authorization header
340
- 4. **JWTTokenService.verify()** verifies the JWT signature using `jose.jwtVerify()`
341
- 5. **JWTTokenService.decryptPayload()** decrypts the AES-encrypted payload fields
550
+ 2. **JWSAuthenticationStrategy.authenticate()** is called by the Hono middleware
551
+ 3. **AbstractBearerTokenService.extractCredentials()** extracts the token from the Authorization header
552
+ 4. **JWSTokenService.doVerify()** verifies the JWT signature using `jose.jwtVerify()` with the shared `jwtSecret`
553
+ 5. **AbstractBearerTokenService.decryptPayload()** decrypts the AES-encrypted payload fields (if AES configured)
342
554
  6. **User payload is set** on `context.get(Authentication.CURRENT_USER)`
343
555
 
344
- > [!NOTE]
345
- > JWT payloads are encrypted field-by-field for additional security. Standard JWT fields (`iss`, `sub`, `aud`, etc.) remain unencrypted, while custom fields like `userId` and `roles` are AES-encrypted.
556
+ ### JWKS Issuer Authentication Flow
557
+
558
+ ```mermaid
559
+ sequenceDiagram
560
+ participant C as Client
561
+ participant MW as Auth Middleware
562
+ participant S as JWKSIssuerStrategy
563
+ participant SVC as JWKSIssuerTokenService
564
+ participant INIT as Lazy Init
565
+ participant JOSE as jose library
566
+
567
+ C->>MW: Request + Authorization: Bearer <token>
568
+ MW->>S: authenticate(context)
569
+ S->>SVC: extractCredentials(context)
570
+ SVC-->>S: { type: "Bearer", token }
571
+ S->>SVC: verify({ type, token })
572
+ SVC->>INIT: ensureInitialized()
573
+ Note over INIT: Load keys from file/text<br/>Parse PEM/JWK<br/>Cache JWKS
574
+ INIT-->>SVC: initialized
575
+ SVC->>JOSE: jwtVerify(token, publicKey)
576
+ JOSE-->>SVC: JWTVerifyResult
577
+ SVC->>SVC: decryptPayload() (if AES configured)
578
+ SVC-->>S: IJWTTokenPayload
579
+ S-->>MW: IAuthUser
580
+ MW->>C: Continue to handler
581
+ ```
582
+
583
+ 1. **Client sends request** with <code v-pre>Authorization: Bearer &lt;token&gt;</code> header
584
+ 2. **JWKSIssuerAuthenticationStrategy.authenticate()** is called by the Hono middleware
585
+ 3. **AbstractBearerTokenService.extractCredentials()** extracts the token from the Authorization header
586
+ 4. **JWKSIssuerTokenService.doVerify()** calls `ensureInitialized()` (lazy-loads keys on first call), then verifies the JWT using the public key
587
+ 5. **AbstractBearerTokenService.decryptPayload()** decrypts the AES-encrypted payload fields (if AES configured)
588
+ 6. **User payload is set** on `context.get(Authentication.CURRENT_USER)`
589
+
590
+ ### JWKS Verifier Authentication Flow
591
+
592
+ ```mermaid
593
+ sequenceDiagram
594
+ participant C as Client
595
+ participant MW as Auth Middleware
596
+ participant S as JWKSVerifierStrategy
597
+ participant SVC as JWKSVerifierTokenService
598
+ participant INIT as Lazy Init
599
+ participant REMOTE as Remote JWKS URL
600
+
601
+ C->>MW: Request + Authorization: Bearer <token>
602
+ MW->>S: authenticate(context)
603
+ S->>SVC: extractCredentials(context)
604
+ SVC-->>S: { type: "Bearer", token }
605
+ S->>SVC: verify({ type, token })
606
+ SVC->>INIT: ensureInitialized()
607
+ INIT->>REMOTE: createRemoteJWKSet(jwksUrl)
608
+ REMOTE-->>INIT: JWKS verifier function
609
+ INIT-->>SVC: initialized
610
+ SVC->>SVC: jwtVerify(token, jwksVerifier)
611
+ SVC->>SVC: decryptPayload() (if AES configured)
612
+ SVC-->>S: IJWTTokenPayload
613
+ S-->>MW: IAuthUser
614
+ MW->>C: Continue to handler
615
+ ```
616
+
617
+ 1. **Client sends request** with <code v-pre>Authorization: Bearer &lt;token&gt;</code> header
618
+ 2. **JWKSVerifierAuthenticationStrategy.authenticate()** is called by the Hono middleware
619
+ 3. **AbstractBearerTokenService.extractCredentials()** extracts the token from the Authorization header
620
+ 4. **JWKSVerifierTokenService.doVerify()** calls `ensureInitialized()` (creates remote JWKS verifier on first call), then verifies the JWT using the remote JWKS
621
+ 5. **AbstractBearerTokenService.decryptPayload()** decrypts the AES-encrypted payload fields (if AES configured)
622
+ 6. **User payload is set** on `context.get(Authentication.CURRENT_USER)`
346
623
 
347
624
  ### Basic Authentication Flow
348
625
 
626
+ ```mermaid
627
+ sequenceDiagram
628
+ participant C as Client
629
+ participant MW as Auth Middleware
630
+ participant S as BasicAuthStrategy
631
+ participant SVC as BasicTokenService
632
+ participant CB as verifyCredentials callback
633
+
634
+ C->>MW: Request + Authorization: Basic <base64>
635
+ MW->>S: authenticate(context)
636
+ S->>SVC: extractCredentials(context)
637
+ SVC->>SVC: Base64 decode
638
+ SVC-->>S: { username, password }
639
+ S->>SVC: verify({ credentials, context })
640
+ SVC->>CB: verifyCredentials({ credentials, context })
641
+ CB-->>SVC: IAuthUser | null
642
+ alt valid user
643
+ SVC-->>S: IAuthUser
644
+ S-->>MW: IAuthUser
645
+ MW->>MW: Set CURRENT_USER + AUDIT_USER_ID
646
+ MW->>C: Continue to handler
647
+ else null (invalid)
648
+ SVC-->>S: throw 401
649
+ S-->>MW: throw 401
650
+ MW->>C: 401 Unauthorized
651
+ end
652
+ ```
653
+
349
654
  1. **Client sends request** with <code v-pre>Authorization: Basic &lt;base64(username:password)&gt;</code> header
350
655
  2. **BasicAuthenticationStrategy.authenticate()** is called by the Hono middleware
351
656
  3. **BasicTokenService.extractCredentials()** decodes the Base64 credentials
352
- 4. **BasicTokenService.verify()** calls the user-provided `verifyCredentials` callback
657
+ 4. **BasicTokenService.verify()** calls the user-provided `verifyCredentials` callback with `{ credentials, context }`
353
658
  5. **User payload is set** on `context.get(Authentication.CURRENT_USER)` if verification succeeds
354
659
 
355
660
  > [!IMPORTANT]
@@ -357,11 +662,39 @@ Inherits all statuses from `CommonStatuses` (same values as `UserStatuses`):
357
662
 
358
663
  ## Multi-Strategy Authentication
359
664
 
360
- When multiple strategies are configured on a route via `authStrategies: ['jwt', 'basic']`:
665
+ When multiple strategies are configured on a route via `authenticate: { strategies: ['jwt', 'basic'] }`:
666
+
667
+ ```mermaid
668
+ flowchart TD
669
+ REQ["Request arrives"] --> MODE{"mode?"}
670
+
671
+ MODE -->|"any (default)"| ANY["Try strategies in order"]
672
+ ANY --> S1{"Strategy 1"}
673
+ S1 -->|"Success"| WIN["Set user, continue"]
674
+ S1 -->|"Fail"| S2{"Strategy 2"}
675
+ S2 -->|"Success"| WIN
676
+ S2 -->|"Fail"| FAIL_ANY["401: Tried strategies: jwt, basic"]
677
+
678
+ MODE -->|"all"| ALL["Run all strategies"]
679
+ ALL --> A1{"Strategy 1"}
680
+ A1 -->|"Fail"| FAIL_ALL["Exception propagates"]
681
+ A1 -->|"Pass"| A2{"Strategy 2"}
682
+ A2 -->|"Fail"| FAIL_ALL
683
+ A2 -->|"Pass"| CHECK{"userId?"}
684
+ CHECK -->|"Yes"| WIN2["Set user, continue"]
685
+ CHECK -->|"No"| FAIL_ID["401: Failed to identify user"]
686
+
687
+ style WIN fill:#d4edda,stroke:#28a745
688
+ style WIN2 fill:#d4edda,stroke:#28a745
689
+ style FAIL_ANY fill:#f8d7da,stroke:#dc3545
690
+ style FAIL_ALL fill:#f8d7da,stroke:#dc3545
691
+ style FAIL_ID fill:#f8d7da,stroke:#dc3545
692
+ ```
361
693
 
362
694
  **`any` mode (default):**
363
695
  - Strategies are tried in the order specified
364
696
  - The first successful strategy wins
697
+ - Errors from failing strategies are **discarded** (logged at debug level)
365
698
  - If all strategies fail, a `401 Unauthorized` error is thrown listing all tried strategies
366
699
  - **Use case:** Fallback authentication (try JWT, fallback to Basic)
367
700
 
@@ -374,41 +707,62 @@ When multiple strategies are configured on a route via `authStrategies: ['jwt',
374
707
  > [!TIP]
375
708
  > Use `'any'` mode for graceful fallback (e.g., allow mobile apps to use JWT while legacy systems use Basic). Use `'all'` mode for high-security endpoints requiring multiple forms of authentication.
376
709
 
377
- ## Token Encryption
710
+ ## Token Encryption (Optional AES)
711
+
712
+ ```mermaid
713
+ flowchart LR
714
+ subgraph GENERATE["generate() — Token Creation"]
715
+ direction TB
716
+ P["Payload: { userId, roles, email }"]
717
+ P --> CHECK1{"applicationSecret?"}
718
+ CHECK1 -->|"Yes"| ENC["encryptPayload()"]
719
+ ENC --> E1["Keep: iss, sub, aud, exp, iat"]
720
+ ENC --> E2["Encrypt keys + values"]
721
+ CHECK1 -->|"No"| PLAIN1["Use payload as-is"]
722
+ end
723
+
724
+ subgraph VERIFY["verify() — Token Verification"]
725
+ direction TB
726
+ T["Verified JWT payload"]
727
+ T --> CHECK2{"applicationSecret?"}
728
+ CHECK2 -->|"Yes"| DEC["decryptPayload()"]
729
+ DEC --> D1["Extract: iss, sub, aud, exp, iat"]
730
+ DEC --> D2["Decrypt keys + values"]
731
+ CHECK2 -->|"No"| PLAIN2["Use payload as-is"]
732
+ end
733
+
734
+ style GENERATE fill:#e8f4fd,stroke:#0d6efd
735
+ style VERIFY fill:#d4edda,stroke:#28a745
736
+ ```
737
+
738
+ 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.
378
739
 
379
- JWT payloads are encrypted field-by-field using AES (default `aes-256-cbc`) via the `@venizia/ignis-helpers` AES utility:
740
+ > [!NOTE]
741
+ > 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.
380
742
 
381
- **Encryption process:**
743
+ **Encryption process (when `applicationSecret` is provided):**
382
744
  1. Standard JWT fields (`iss`, `sub`, `aud`, `jti`, `nbf`, `exp`, `iat`) are preserved as-is
383
745
  2. All other fields have both their **keys** and **values** AES-encrypted
384
746
  3. The `roles` field is serialized as `id|identifier|priority` pipe-separated strings before encryption
385
747
  4. `null` and `undefined` values are skipped during encryption
386
748
 
387
- **Encryption code walkthrough:**
388
-
389
- The `encryptPayload()` method processes each field:
390
- 1. Standard JWT fields (`iss`, `sub`, `aud`, `jti`, `nbf`, `exp`, `iat`) are copied as-is
391
- 2. `null`/`undefined` values are skipped entirely
392
- 3. For the `roles` field: values are serialized as `"id|identifier|priority"` pipe-separated strings, then the array is JSON-stringified before encryption
393
- 4. For all other fields: values are converted to string via template literal (<code v-pre>`${value}`</code>), then both key and value are AES-encrypted independently
394
- 5. The encrypted key becomes the new field name, the encrypted value becomes its value
395
-
396
749
  **Decryption process:**
397
- 1. Standard JWT fields are extracted directly
398
- 2. Encrypted fields have their keys decrypted first, then their values
399
- 3. The `roles` field is deserialized: JSON-parsed to a string array, then each entry is split on `|` to reconstruct objects with `id`, `identifier`, and `priority` (where `priority` is converted to integer via `int()`)
750
+ 1. If AES is not configured (`this.aes` is null), the payload is returned as-is
751
+ 2. Standard JWT fields are extracted directly
752
+ 3. Encrypted fields have their keys decrypted first, then their values
753
+ 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()`)
400
754
 
401
755
  > [!WARNING]
402
- > The `applicationSecret` must remain constant across all instances of your application. Changing it will invalidate all existing tokens, as they cannot be decrypted with a different secret.
756
+ > 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`.
403
757
 
404
758
  ## Hono Context Extension
405
759
 
406
- The Authentication module extends Hono's `ContextVariableMap` to provide type-safe access to auth data:
760
+ 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:
407
761
 
408
762
  ```typescript
409
763
  declare module 'hono' {
410
- interface ContextVariableMap<User extends IAuthUser = IAuthUser> {
411
- [Authentication.CURRENT_USER]: User;
764
+ interface ContextVariableMap {
765
+ [Authentication.CURRENT_USER]: IAuthUser;
412
766
  [Authentication.AUDIT_USER_ID]: IdType;
413
767
  }
414
768
  }
@@ -451,32 +805,9 @@ const SignInRequestSchema = z.object({
451
805
  type TSignInRequest = z.infer<typeof SignInRequestSchema>;
452
806
  ```
453
807
 
454
- | Field | Type | Constraints |
455
- |-------|------|-------------|
456
- | `identifier.scheme` | `string` | Non-empty, min 4 chars |
457
- | `identifier.value` | `string` | Non-empty, min 8 chars |
458
- | `credential.scheme` | `string` | Non-empty |
459
- | `credential.value` | `string` | Non-empty, min 8 chars |
460
- | `clientId` | `string` | Optional |
461
-
462
- **OpenAPI examples** (from source):
463
- ```json
464
- [
465
- {
466
- "identifier": { "scheme": "username", "value": "test_username" },
467
- "credential": { "scheme": "basic", "value": "test_password" }
468
- },
469
- {
470
- "identifier": { "scheme": "username", "value": "test_username" },
471
- "credential": { "scheme": "basic", "value": "test_password" },
472
- "clientId": "auth-provider"
473
- }
474
- ]
475
- ```
476
-
477
808
  ### SignUpRequestSchema
478
809
 
479
- The built-in schema uses a **flat structure** -- not the nested `identifier`/`credential` pattern used by sign-in:
810
+ The built-in schema uses a **flat structure**:
480
811
 
481
812
  ```typescript
482
813
  const SignUpRequestSchema = z.object({
@@ -487,25 +818,8 @@ const SignUpRequestSchema = z.object({
487
818
  type TSignUpRequest = z.infer<typeof SignUpRequestSchema>;
488
819
  ```
489
820
 
490
- | Field | Type | Constraints |
491
- |-------|------|-------------|
492
- | `username` | `string` | Non-empty, min 8 chars |
493
- | `credential` | `string` | Non-empty, min 8 chars |
494
-
495
- **OpenAPI examples** (from source):
496
- ```json
497
- [
498
- {
499
- "username": "example_username",
500
- "credential": "example_credential"
501
- }
502
- ]
503
- ```
504
-
505
821
  ### ChangePasswordRequestSchema
506
822
 
507
- The built-in schema uses scheme-based credential naming with a `userId` field:
508
-
509
823
  ```typescript
510
824
  const ChangePasswordRequestSchema = z.object({
511
825
  scheme: z.string(),
@@ -517,24 +831,6 @@ const ChangePasswordRequestSchema = z.object({
517
831
  type TChangePasswordRequest = z.infer<typeof ChangePasswordRequestSchema>;
518
832
  ```
519
833
 
520
- | Field | Type | Constraints |
521
- |-------|------|-------------|
522
- | `scheme` | `string` | Required (e.g., `'basic'`) |
523
- | `oldCredential` | `string` | Non-empty, min 8 chars |
524
- | `newCredential` | `string` | Non-empty, min 8 chars |
525
- | `userId` | `string \| number` | Required |
526
-
527
- **OpenAPI examples** (from source):
528
- ```json
529
- [
530
- {
531
- "scheme": "basic",
532
- "oldCredential": "old_password",
533
- "newCredential": "new_password"
534
- }
535
- ]
536
- ```
537
-
538
834
  ### JWTTokenPayloadSchema
539
835
 
540
836
  Exported from the controller factory module. Used as the response schema for the `/who-am-i` endpoint:
@@ -555,36 +851,6 @@ const JWTTokenPayloadSchema = z.object({
555
851
  });
556
852
  ```
557
853
 
558
- ### Custom Schema Example
559
-
560
- ```typescript
561
- import { z } from 'zod';
562
-
563
- this.bind<TAuthenticationRestOptions>({ key: AuthenticateBindingKeys.REST_OPTIONS }).toValue({
564
- useAuthController: true,
565
- controllerOpts: {
566
- restPath: '/auth',
567
- payload: {
568
- signIn: {
569
- request: {
570
- schema: z.object({
571
- email: z.string().email(),
572
- password: z.string().min(8),
573
- }),
574
- },
575
- response: {
576
- schema: z.object({
577
- accessToken: z.string(),
578
- refreshToken: z.string(),
579
- expiresIn: z.number(),
580
- }),
581
- },
582
- },
583
- },
584
- },
585
- });
586
- ```
587
-
588
854
  ## API Endpoints
589
855
 
590
856
  The built-in auth controller is created by the `defineAuthController()` factory function and is only available when `useAuthController: true` is set in `REST_OPTIONS`.
@@ -595,9 +861,10 @@ The built-in auth controller is created by the `defineAuthController()` factory
595
861
  | `POST` | `/auth/sign-up` | Configurable | Create a new user account |
596
862
  | `POST` | `/auth/change-password` | JWT | Change the authenticated user's password |
597
863
  | `GET` | `/auth/who-am-i` | JWT | Return the current user's JWT payload |
864
+ | `GET` | `/certs` | No | JWKS endpoint (JWKS Issuer mode only) |
598
865
 
599
866
  > [!NOTE]
600
- > The base path `/auth` is configurable via `controllerOpts.restPath`. All paths shown above use the default.
867
+ > 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.
601
868
 
602
869
  ### POST /auth/sign-in
603
870
 
@@ -607,171 +874,232 @@ The built-in auth controller is created by the `defineAuthController()` factory
607
874
 
608
875
  Uses `SignInRequestSchema` by default, or a custom schema via `payload.signIn.request.schema`.
609
876
 
610
- Default schema:
611
- ```typescript
612
- {
613
- identifier: {
614
- scheme: string; // min 4 chars, e.g., 'username', 'email'
615
- value: string; // min 8 chars
616
- };
617
- credential: {
618
- scheme: string; // e.g., 'basic', 'password'
619
- value: string; // min 8 chars
620
- };
621
- clientId?: string;
622
- }
623
- ```
624
-
625
877
  **Response 200:**
626
878
 
627
879
  Uses `payload.signIn.response.schema` if provided, otherwise `AnyObjectSchema`.
628
880
 
629
881
  ```json
630
882
  {
631
- "token": "eyJhbGciOiJIUzI1NiJ9..."
883
+ "token": "eyJhbGciOiJFUzI1NiIsImtpZCI6Im15LWtleS1pZC0xIn0..."
632
884
  }
633
885
  ```
634
886
 
635
- **Example:**
636
- ```typescript
637
- const response = await fetch('/auth/sign-in', {
638
- method: 'POST',
639
- headers: { 'Content-Type': 'application/json' },
640
- body: JSON.stringify({
641
- identifier: { scheme: 'email', value: 'user@example.com' },
642
- credential: { scheme: 'password', value: 'my-password' },
643
- }),
644
- });
887
+ ### POST /auth/sign-up
645
888
 
646
- const { token } = await response.json();
647
- ```
889
+ **Authentication:** Configurable via `requireAuthenticatedSignUp` (default: `false`)
648
890
 
891
+ When `requireAuthenticatedSignUp: true`, requires JWT authentication. When `false`, the endpoint is public.
649
892
 
650
- ### POST /auth/sign-up
893
+ ### POST /auth/change-password
651
894
 
652
- **Authentication:** Configurable via `requireAuthenticatedSignUp` (default: `false`)
895
+ **Authentication:** Always requires JWT (`Authentication.STRATEGY_JWT`)
653
896
 
654
- When `requireAuthenticatedSignUp: true`, requires JWT authentication (strategy: `Authentication.STRATEGY_JWT`). When `false`, the `strategies` array is empty (public endpoint).
897
+ ### GET /auth/who-am-i
655
898
 
656
- **Request Body:**
899
+ **Authentication:** Always requires JWT (`Authentication.STRATEGY_JWT`)
657
900
 
658
- Uses `SignUpRequestSchema` by default, or a custom schema via `payload.signUp.request.schema`.
901
+ Returns the current user's decrypted JWT payload directly from context.
659
902
 
660
- Default schema (flat structure):
661
- ```typescript
903
+ ```json
662
904
  {
663
- username: string; // non-empty, min 8 chars
664
- credential: string; // non-empty, min 8 chars
905
+ "userId": "123",
906
+ "roles": [
907
+ { "id": "1", "identifier": "admin", "priority": 0 }
908
+ ],
909
+ "clientId": "optional-client-id",
910
+ "provider": "optional-provider",
911
+ "email": "user@example.com"
665
912
  }
666
913
  ```
667
914
 
668
- **Response 200:**
915
+ ### GET /certs (JWKS Issuer Only)
916
+
917
+ **Authentication:** None (intentionally public)
669
918
 
670
- Uses `payload.signUp.response.schema` if provided, otherwise `AnyObjectSchema`.
919
+ Returns the JSON Web Key Set for external verifiers.
671
920
 
672
921
  ```json
673
922
  {
674
- "id": "user-id",
675
- "username": "newuser123"
923
+ "keys": [
924
+ {
925
+ "kty": "EC",
926
+ "kid": "my-key-id-1",
927
+ "use": "sig",
928
+ "alg": "ES256",
929
+ "crv": "P-256",
930
+ "x": "...",
931
+ "y": "..."
932
+ }
933
+ ]
676
934
  }
677
935
  ```
678
936
 
679
- **Example:**
680
- ```typescript
681
- const response = await fetch('/auth/sign-up', {
682
- method: 'POST',
683
- headers: { 'Content-Type': 'application/json' },
684
- body: JSON.stringify({
685
- username: 'newuser123',
686
- credential: 'secure-password',
687
- }),
688
- });
937
+ **Cache headers:** `Cache-Control: public, max-age=3600, stale-while-revalidate=86400`
689
938
 
690
- const user = await response.json();
691
- ```
939
+ ## Auth Entity Column Helpers
692
940
 
941
+ 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.
693
942
 
694
- ### POST /auth/change-password
943
+ 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'`.
695
944
 
696
- **Authentication:** Always requires JWT (`Authentication.STRATEGY_JWT`)
945
+ ### extraUserColumns
697
946
 
698
- **Request Body:**
947
+ **Import:** `import { extraUserColumns } from '@venizia/ignis';`
699
948
 
700
- Uses `ChangePasswordRequestSchema` by default, or a custom schema via `payload.changePassword.request.schema`.
949
+ **Signature:** `extraUserColumns(opts?: { idType: 'string' | 'number' })`
701
950
 
702
- Default schema:
703
- ```typescript
704
- {
705
- scheme: string; // e.g., 'basic'
706
- oldCredential: string; // non-empty, min 8 chars
707
- newCredential: string; // non-empty, min 8 chars
708
- userId: string | number;
709
- }
710
- ```
951
+ | Column | DB Column | Type | Nullable | Default | Description |
952
+ |--------|-----------|------|----------|---------|-------------|
953
+ | `realm` | `realm` | `text` | Yes | `''` | Multi-tenancy realm identifier |
954
+ | `status` | `status` | `text` | No | `UserStatuses.UNKNOWN` | User lifecycle status |
955
+ | `type` | `type` | `text` | No | `UserTypes.SYSTEM` | User type (`SYSTEM` or `LINKED`) |
956
+ | `activatedAt` | `activated_at` | `timestamp (tz)` | Yes | `null` | When the user was activated |
957
+ | `lastLoginAt` | `last_login_at` | `timestamp (tz)` | Yes | `null` | Last login timestamp |
958
+ | `parentId` | `parent_id` | `text` or `integer` | Yes | `null` | Parent user ID (type depends on `idType`) |
711
959
 
712
- **Response 200:**
960
+ ### extraRoleColumns
713
961
 
714
- Uses `payload.changePassword.response.schema` if provided, otherwise `AnyObjectSchema`.
962
+ **Import:** `import { extraRoleColumns } from '@venizia/ignis';`
715
963
 
716
- ```json
717
- {
718
- "success": true
719
- }
720
- ```
964
+ **Signature:** `extraRoleColumns()`
965
+
966
+ | Column | DB Column | Type | Nullable | Default | Description |
967
+ |--------|-----------|------|----------|---------|-------------|
968
+ | `identifier` | `identifier` | `text` (unique) | No | -- | Unique role identifier (e.g., `'admin'`, `'editor'`) |
969
+ | `name` | `name` | `text` | No | -- | Human-readable role name |
970
+ | `description` | `description` | `text` | Yes | `null` | Optional role description |
971
+ | `priority` | `priority` | `integer` | No | -- | Role priority (lower = higher priority) |
972
+ | `status` | `status` | `text` | No | `RoleStatuses.ACTIVATED` | Role lifecycle status |
973
+
974
+ ### extraPermissionColumns
975
+
976
+ **Import:** `import { extraPermissionColumns } from '@venizia/ignis';`
977
+
978
+ **Signature:** `extraPermissionColumns(opts?: { idType: 'string' | 'number' })`
979
+
980
+ | Column | DB Column | Type | Nullable | Default | Description |
981
+ |--------|-----------|------|----------|---------|-------------|
982
+ | `code` | `code` | `text` (unique) | No | -- | Unique permission code |
983
+ | `name` | `name` | `text` | No | -- | Permission display name |
984
+ | `subject` | `subject` | `text` | No | -- | Permission subject (e.g., `'User'`, `'Order'`) |
985
+ | `action` | `action` | `text` | No | -- | Permitted action (e.g., `'read'`, `'write'`) |
986
+ | `scope` | `scope` | `text` | No | -- | Permission scope |
987
+ | `parentId` | `parent_id` | `text` or `integer` | Yes | `null` | Parent permission ID (type depends on `idType`) |
988
+
989
+ ### extraPolicyDefinitionColumns
990
+
991
+ **Import:** `import { extraPolicyDefinitionColumns } from '@venizia/ignis';`
992
+
993
+ **Signature:** `extraPolicyDefinitionColumns(opts?: { idType: 'string' | 'number' })`
994
+
995
+ Provides columns for Casbin-style policy definitions that map subjects (users/roles) to targets (resources/permissions).
996
+
997
+ | Column | DB Column | Type | Nullable | Default | Description |
998
+ |--------|-----------|------|----------|---------|-------------|
999
+ | `variant` | `variant` | `text` | No | -- | Policy variant (e.g., `'p'` for policy, `'g'` for grouping) |
1000
+ | `subjectType` | `subject_type` | `text` | No | -- | Type of subject (e.g., `'user'`, `'role'`) |
1001
+ | `targetType` | `target_type` | `text` | No | -- | Type of target (e.g., `'permission'`, `'role'`) |
1002
+ | `action` | `action` | `text` | Yes | `null` | Policy action |
1003
+ | `effect` | `effect` | `text` | Yes | `null` | Policy effect (e.g., `'allow'`, `'deny'`) |
1004
+ | `domain` | `domain` | `text` | Yes | `null` | Policy domain for multi-tenancy |
1005
+ | `subjectId` | `subject_id` | `text` or `integer` | No | -- | Subject ID (type depends on `idType`) |
1006
+ | `targetId` | `target_id` | `text` or `integer` | No | -- | Target ID (type depends on `idType`) |
1007
+
1008
+ ### Usage Example
721
1009
 
722
- **Example:**
723
1010
  ```typescript
724
- const response = await fetch('/auth/change-password', {
725
- method: 'POST',
726
- headers: {
727
- 'Content-Type': 'application/json',
728
- 'Authorization': `Bearer ${token}`,
729
- },
730
- body: JSON.stringify({
731
- scheme: 'basic',
732
- oldCredential: 'old-password',
733
- newCredential: 'new-secure-password',
734
- userId: '123',
735
- }),
1011
+ import { pgTable, serial, text } from 'drizzle-orm/pg-core';
1012
+ import {
1013
+ extraUserColumns,
1014
+ extraRoleColumns,
1015
+ extraPermissionColumns,
1016
+ extraPolicyDefinitionColumns,
1017
+ } from '@venizia/ignis';
1018
+ import { withSerialId, withTimestamps } from '@venizia/ignis';
1019
+
1020
+ // User table
1021
+ export const users = pgTable('users', {
1022
+ ...withSerialId(),
1023
+ ...withTimestamps(),
1024
+ ...extraUserColumns(),
1025
+ username: text('username').unique().notNull(),
1026
+ passwordHash: text('password_hash').notNull(),
1027
+ email: text('email').unique(),
736
1028
  });
737
1029
 
738
- const result = await response.json();
739
- ```
1030
+ // Role table
1031
+ export const roles = pgTable('roles', {
1032
+ ...withSerialId(),
1033
+ ...withTimestamps(),
1034
+ ...extraRoleColumns(),
1035
+ });
740
1036
 
1037
+ // Permission table
1038
+ export const permissions = pgTable('permissions', {
1039
+ ...withSerialId(),
1040
+ ...withTimestamps(),
1041
+ ...extraPermissionColumns(),
1042
+ });
741
1043
 
742
- ### GET /auth/who-am-i
1044
+ // Policy definition table (Casbin-style policies)
1045
+ export const policyDefinitions = pgTable('policy_definitions', {
1046
+ ...withSerialId(),
1047
+ ...withTimestamps(),
1048
+ ...extraPolicyDefinitionColumns(),
1049
+ });
743
1050
 
744
- **Authentication:** Always requires JWT (`Authentication.STRATEGY_JWT`)
1051
+ // With UUID-based IDs
1052
+ export const uuidUsers = pgTable('users', {
1053
+ ...withUuidId(),
1054
+ ...withTimestamps(),
1055
+ ...extraUserColumns({ idType: 'string' }),
1056
+ username: text('username').unique().notNull(),
1057
+ });
745
1058
 
746
- **Request Body:** None
1059
+ export const uuidPolicies = pgTable('policy_definitions', {
1060
+ ...withUuidId(),
1061
+ ...withTimestamps(),
1062
+ ...extraPolicyDefinitionColumns({ idType: 'string' }),
1063
+ });
1064
+ ```
747
1065
 
748
- **Response 200:**
1066
+ ### Context Variables
749
1067
 
750
- Uses the `JWTTokenPayloadSchema` Zod schema. Returns the current user's decrypted JWT payload directly from context:
1068
+ 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()`.
751
1069
 
752
- ```json
753
- {
754
- "userId": "123",
755
- "roles": [
756
- { "id": "1", "identifier": "admin", "priority": 0 }
757
- ],
758
- "clientId": "optional-client-id",
759
- "provider": "optional-provider",
760
- "email": "user@example.com"
761
- }
1070
+ | Constant | Key String | Type | Description |
1071
+ |----------|-----------|------|-------------|
1072
+ | `Authentication.CURRENT_USER` | `'auth.current.user'` | `IAuthUser` | The authenticated user payload, set after successful authentication |
1073
+ | `Authentication.AUDIT_USER_ID` | `'audit.user.id'` | `IdType` | The authenticated user's ID, extracted from the user payload |
1074
+ | `Authentication.SKIP_AUTHENTICATION` | `'authentication.skip'` | `boolean` | Set to `true` in a preceding middleware to bypass authentication for the current request |
1075
+ | `Authorization.RULES` | `'authorization.rules'` | `unknown` | Authorization rules resolved for the current request |
1076
+ | `Authorization.SKIP_AUTHORIZATION` | `'authorization.skip'` | `boolean` | Set to `true` to bypass authorization checks for the current request |
1077
+
1078
+ **Reading context variables in a handler:**
1079
+
1080
+ ```typescript
1081
+ import { Authentication, Authorization } from '@venizia/ignis';
1082
+
1083
+ // Inside a route handler
1084
+ const currentUser = c.get(Authentication.CURRENT_USER);
1085
+ const userId = c.get(Authentication.AUDIT_USER_ID);
1086
+ const skipAuth = c.get(Authentication.SKIP_AUTHENTICATION);
1087
+ const authzRules = c.get(Authorization.RULES);
762
1088
  ```
763
1089
 
764
- **Example:**
1090
+ **Skipping auth dynamically from middleware:**
1091
+
765
1092
  ```typescript
766
- const response = await fetch('/auth/who-am-i', {
767
- method: 'GET',
768
- headers: {
769
- 'Authorization': `Bearer ${token}`,
770
- },
771
- });
1093
+ import { Authentication, Authorization } from '@venizia/ignis';
1094
+ import { createMiddleware } from 'hono/factory';
772
1095
 
773
- const user = await response.json();
774
- console.log('Current user:', user);
1096
+ const apiKeyMiddleware = createMiddleware(async (c, next) => {
1097
+ if (c.req.header('X-API-Key') === process.env.INTERNAL_API_KEY) {
1098
+ c.set(Authentication.SKIP_AUTHENTICATION, true);
1099
+ c.set(Authorization.SKIP_AUTHORIZATION, true);
1100
+ }
1101
+ return next();
1102
+ });
775
1103
  ```
776
1104
 
777
1105
  ## See Also