@venizia/ignis-docs 0.0.6-2 → 0.0.7-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 (120) 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 +1 -20
  15. package/dist/mcp-server/index.js.map +1 -1
  16. package/dist/mcp-server/tools/base.tool.d.ts +4 -85
  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 +8 -2
  21. package/dist/mcp-server/tools/docs/get-document-content.tool.d.ts.map +1 -1
  22. package/dist/mcp-server/tools/docs/get-document-content.tool.js +1 -10
  23. package/dist/mcp-server/tools/docs/get-document-content.tool.js.map +1 -1
  24. package/dist/mcp-server/tools/docs/get-document-metadata.tool.d.ts +13 -2
  25. package/dist/mcp-server/tools/docs/get-document-metadata.tool.d.ts.map +1 -1
  26. package/dist/mcp-server/tools/docs/get-document-metadata.tool.js +1 -10
  27. package/dist/mcp-server/tools/docs/get-document-metadata.tool.js.map +1 -1
  28. package/dist/mcp-server/tools/docs/get-package-overview.tool.d.ts +16 -8
  29. package/dist/mcp-server/tools/docs/get-package-overview.tool.d.ts.map +1 -1
  30. package/dist/mcp-server/tools/docs/get-package-overview.tool.js +2 -25
  31. package/dist/mcp-server/tools/docs/get-package-overview.tool.js.map +1 -1
  32. package/dist/mcp-server/tools/docs/list-categories.tool.d.ts +5 -2
  33. package/dist/mcp-server/tools/docs/list-categories.tool.d.ts.map +1 -1
  34. package/dist/mcp-server/tools/docs/list-categories.tool.js +1 -10
  35. package/dist/mcp-server/tools/docs/list-categories.tool.js.map +1 -1
  36. package/dist/mcp-server/tools/docs/list-documents.tool.d.ts +11 -2
  37. package/dist/mcp-server/tools/docs/list-documents.tool.d.ts.map +1 -1
  38. package/dist/mcp-server/tools/docs/list-documents.tool.js +1 -10
  39. package/dist/mcp-server/tools/docs/list-documents.tool.js.map +1 -1
  40. package/dist/mcp-server/tools/docs/search-documents.tool.d.ts +13 -2
  41. package/dist/mcp-server/tools/docs/search-documents.tool.d.ts.map +1 -1
  42. package/dist/mcp-server/tools/docs/search-documents.tool.js +1 -10
  43. package/dist/mcp-server/tools/docs/search-documents.tool.js.map +1 -1
  44. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +9 -2
  45. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts.map +1 -1
  46. package/dist/mcp-server/tools/github/list-project-files.tool.js +1 -10
  47. package/dist/mcp-server/tools/github/list-project-files.tool.js.map +1 -1
  48. package/dist/mcp-server/tools/github/search-code.tool.d.ts +16 -2
  49. package/dist/mcp-server/tools/github/search-code.tool.d.ts.map +1 -1
  50. package/dist/mcp-server/tools/github/search-code.tool.js +2 -14
  51. package/dist/mcp-server/tools/github/search-code.tool.js.map +1 -1
  52. package/dist/mcp-server/tools/github/verify-dependencies.tool.d.ts +19 -6
  53. package/dist/mcp-server/tools/github/verify-dependencies.tool.d.ts.map +1 -1
  54. package/dist/mcp-server/tools/github/verify-dependencies.tool.js +2 -19
  55. package/dist/mcp-server/tools/github/verify-dependencies.tool.js.map +1 -1
  56. package/dist/mcp-server/tools/github/view-source-file.tool.d.ts +8 -2
  57. package/dist/mcp-server/tools/github/view-source-file.tool.d.ts.map +1 -1
  58. package/dist/mcp-server/tools/github/view-source-file.tool.js +1 -10
  59. package/dist/mcp-server/tools/github/view-source-file.tool.js.map +1 -1
  60. package/dist/mcp-server/tools/index.d.ts.map +1 -1
  61. package/dist/mcp-server/tools/index.js +0 -2
  62. package/dist/mcp-server/tools/index.js.map +1 -1
  63. package/package.json +68 -54
  64. package/wiki/best-practices/api-usage-examples.md +7 -5
  65. package/wiki/best-practices/code-style-standards/advanced-patterns.md +1 -1
  66. package/wiki/best-practices/code-style-standards/constants-configuration.md +1 -1
  67. package/wiki/best-practices/code-style-standards/control-flow.md +1 -1
  68. package/wiki/best-practices/code-style-standards/function-patterns.md +1 -1
  69. package/wiki/best-practices/common-pitfalls.md +1 -1
  70. package/wiki/best-practices/data-modeling.md +33 -1
  71. package/wiki/best-practices/error-handling.md +7 -4
  72. package/wiki/best-practices/performance-optimization.md +1 -1
  73. package/wiki/best-practices/security-guidelines.md +5 -4
  74. package/wiki/guides/core-concepts/components-guide.md +1 -1
  75. package/wiki/guides/core-concepts/controllers.md +14 -8
  76. package/wiki/guides/core-concepts/persistent/models.md +32 -0
  77. package/wiki/guides/core-concepts/services.md +2 -1
  78. package/wiki/guides/get-started/5-minute-quickstart.md +1 -1
  79. package/wiki/guides/reference/mcp-docs-server.md +0 -134
  80. package/wiki/guides/tutorials/building-a-crud-api.md +2 -1
  81. package/wiki/guides/tutorials/complete-installation.md +2 -2
  82. package/wiki/guides/tutorials/ecommerce-api.md +3 -3
  83. package/wiki/guides/tutorials/realtime-chat.md +7 -6
  84. package/wiki/index.md +2 -1
  85. package/wiki/references/base/components.md +2 -1
  86. package/wiki/references/base/controllers.md +19 -12
  87. package/wiki/references/base/middlewares.md +2 -1
  88. package/wiki/references/base/models.md +11 -2
  89. package/wiki/references/base/services.md +2 -1
  90. package/wiki/references/components/authentication/api.md +525 -205
  91. package/wiki/references/components/authentication/errors.md +502 -105
  92. package/wiki/references/components/authentication/index.md +388 -75
  93. package/wiki/references/components/authentication/usage.md +428 -266
  94. package/wiki/references/components/authorization/api.md +1213 -0
  95. package/wiki/references/components/authorization/errors.md +387 -0
  96. package/wiki/references/components/authorization/index.md +712 -0
  97. package/wiki/references/components/authorization/usage.md +758 -0
  98. package/wiki/references/components/health-check.md +2 -1
  99. package/wiki/references/components/index.md +2 -0
  100. package/wiki/references/components/socket-io/index.md +9 -4
  101. package/wiki/references/components/socket-io/usage.md +1 -1
  102. package/wiki/references/components/static-asset/index.md +3 -5
  103. package/wiki/references/components/swagger.md +2 -1
  104. package/wiki/references/configuration/environment-variables.md +2 -1
  105. package/wiki/references/configuration/index.md +2 -1
  106. package/wiki/references/helpers/error/index.md +1 -1
  107. package/wiki/references/helpers/index.md +1 -0
  108. package/wiki/references/helpers/inversion/index.md +1 -1
  109. package/wiki/references/helpers/kafka/index.md +305 -0
  110. package/wiki/references/helpers/redis/index.md +2 -9
  111. package/wiki/references/quick-reference.md +3 -5
  112. package/wiki/references/utilities/crypto.md +2 -2
  113. package/wiki/references/utilities/date.md +5 -5
  114. package/wiki/references/utilities/index.md +3 -11
  115. package/wiki/references/utilities/jsx.md +4 -2
  116. package/wiki/references/utilities/module.md +1 -1
  117. package/wiki/references/utilities/parse.md +4 -4
  118. package/wiki/references/utilities/performance.md +2 -2
  119. package/wiki/references/utilities/promise.md +4 -4
  120. package/wiki/references/utilities/request.md +2 -2
@@ -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: '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,15 +46,15 @@ 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
- import { authenticate, Authentication } from '@venizia/ignis';
52
+ import { authenticate, Authentication, AuthenticationModes } from '@venizia/ignis';
51
53
 
52
54
  // Use as Hono middleware directly
53
55
  const authMiddleware = authenticate({
54
56
  strategies: [Authentication.STRATEGY_JWT],
55
- mode: 'any',
57
+ mode: AuthenticationModes.ANY,
56
58
  });
57
59
 
58
60
  // Apply to a Hono route
@@ -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
378
723
 
379
- JWT payloads are encrypted field-by-field using AES (default `aes-256-cbc`) via the `@venizia/ignis-helpers` AES utility:
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
380
733
 
381
- **Encryption process:**
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.
739
+
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.
742
+
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,147 +874,31 @@ 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
- });
645
-
646
- const { token } = await response.json();
647
- ```
648
-
649
-
650
887
  ### POST /auth/sign-up
651
888
 
652
889
  **Authentication:** Configurable via `requireAuthenticatedSignUp` (default: `false`)
653
890
 
654
- When `requireAuthenticatedSignUp: true`, requires JWT authentication (strategy: `Authentication.STRATEGY_JWT`). When `false`, the `strategies` array is empty (public endpoint).
655
-
656
- **Request Body:**
657
-
658
- Uses `SignUpRequestSchema` by default, or a custom schema via `payload.signUp.request.schema`.
659
-
660
- Default schema (flat structure):
661
- ```typescript
662
- {
663
- username: string; // non-empty, min 8 chars
664
- credential: string; // non-empty, min 8 chars
665
- }
666
- ```
667
-
668
- **Response 200:**
669
-
670
- Uses `payload.signUp.response.schema` if provided, otherwise `AnyObjectSchema`.
671
-
672
- ```json
673
- {
674
- "id": "user-id",
675
- "username": "newuser123"
676
- }
677
- ```
678
-
679
- **Example:**
680
- ```typescript
681
- const response = await fetch('/auth/sign-up', {
682
- method: 'POST',
683
- headers: { 'Content-Type': 'application/json' },
684
- body: JSON.stringify({
685
- username: 'newuser123',
686
- credential: 'secure-password',
687
- }),
688
- });
689
-
690
- const user = await response.json();
691
- ```
692
-
891
+ When `requireAuthenticatedSignUp: true`, requires JWT authentication. When `false`, the endpoint is public.
693
892
 
694
893
  ### POST /auth/change-password
695
894
 
696
895
  **Authentication:** Always requires JWT (`Authentication.STRATEGY_JWT`)
697
896
 
698
- **Request Body:**
699
-
700
- Uses `ChangePasswordRequestSchema` by default, or a custom schema via `payload.changePassword.request.schema`.
701
-
702
- Default schema:
703
- ```typescript
704
- {
705
- scheme: string; // e.g., 'basic'
706
- oldCredential: string; // non-empty, min 8 chars
707
- newCredential: string; // non-empty, min 8 chars
708
- userId: string | number;
709
- }
710
- ```
711
-
712
- **Response 200:**
713
-
714
- Uses `payload.changePassword.response.schema` if provided, otherwise `AnyObjectSchema`.
715
-
716
- ```json
717
- {
718
- "success": true
719
- }
720
- ```
721
-
722
- **Example:**
723
- ```typescript
724
- const response = await fetch('/auth/change-password', {
725
- method: 'POST',
726
- headers: {
727
- 'Content-Type': 'application/json',
728
- 'Authorization': `Bearer ${token}`,
729
- },
730
- body: JSON.stringify({
731
- scheme: 'basic',
732
- oldCredential: 'old-password',
733
- newCredential: 'new-secure-password',
734
- userId: '123',
735
- }),
736
- });
737
-
738
- const result = await response.json();
739
- ```
740
-
741
-
742
897
  ### GET /auth/who-am-i
743
898
 
744
899
  **Authentication:** Always requires JWT (`Authentication.STRATEGY_JWT`)
745
900
 
746
- **Request Body:** None
747
-
748
- **Response 200:**
749
-
750
- Uses the `JWTTokenPayloadSchema` Zod schema. Returns the current user's decrypted JWT payload directly from context:
901
+ Returns the current user's decrypted JWT payload directly from context.
751
902
 
752
903
  ```json
753
904
  {
@@ -761,19 +912,30 @@ Uses the `JWTTokenPayloadSchema` Zod schema. Returns the current user's decrypte
761
912
  }
762
913
  ```
763
914
 
764
- **Example:**
765
- ```typescript
766
- const response = await fetch('/auth/who-am-i', {
767
- method: 'GET',
768
- headers: {
769
- 'Authorization': `Bearer ${token}`,
770
- },
771
- });
915
+ ### GET /certs (JWKS Issuer Only)
916
+
917
+ **Authentication:** None (intentionally public)
772
918
 
773
- const user = await response.json();
774
- console.log('Current user:', user);
919
+ Returns the JSON Web Key Set for external verifiers.
920
+
921
+ ```json
922
+ {
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
+ ]
934
+ }
775
935
  ```
776
936
 
937
+ **Cache headers:** `Cache-Control: public, max-age=3600, stale-while-revalidate=86400`
938
+
777
939
  ## See Also
778
940
 
779
941
  - [Setup & Configuration](./) -- Binding keys, options interfaces, and initial setup