emilsoftware-utilities 2.0.0-dev.67 → 2.0.0-dev.69

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 (149) hide show
  1. package/dist/accessi/resource-server/errors.d.ts +1 -1
  2. package/dist/accessi/resource-server/errors.js.map +1 -1
  3. package/dist/accessi/resource-server/express.js +21 -0
  4. package/dist/accessi/resource-server/express.js.map +1 -1
  5. package/dist/accessi/resource-server/nest.js +13 -0
  6. package/dist/accessi/resource-server/nest.js.map +1 -1
  7. package/dist/accessi/resource-server/runtime.js +11 -1
  8. package/dist/accessi/resource-server/runtime.js.map +1 -1
  9. package/dist/accessi/resource-server/types.d.ts +7 -0
  10. package/dist/accessi/resource-server/types.js.map +1 -1
  11. package/dist/accessi/server/index.d.ts +1 -0
  12. package/dist/accessi/server/index.js +1 -0
  13. package/dist/accessi/server/index.js.map +1 -1
  14. package/dist/accessi/server/standalone.d.ts +41 -0
  15. package/dist/accessi/server/standalone.js +264 -0
  16. package/dist/accessi/server/standalone.js.map +1 -0
  17. package/dist/accessi-module/AccessiModule.d.ts +53 -2
  18. package/dist/accessi-module/AccessiModule.js +5 -2
  19. package/dist/accessi-module/AccessiModule.js.map +1 -1
  20. package/dist/accessi-module/Console/accessiFetch.d.ts +5 -0
  21. package/dist/accessi-module/Console/accessiFetch.js +7 -4
  22. package/dist/accessi-module/Console/accessiFetch.js.map +1 -1
  23. package/dist/accessi-module/Console/console.js +21 -7
  24. package/dist/accessi-module/Console/generated/emilsoftware-utilities.d.ts +31 -1
  25. package/dist/accessi-module/Console/generated/emilsoftware-utilities.js +28 -3
  26. package/dist/accessi-module/Console/generated/emilsoftware-utilities.js.map +1 -1
  27. package/dist/accessi-module/Console/generated/model/exchangeFederatedIdentityRequest.d.ts +12 -0
  28. package/dist/accessi-module/Console/generated/model/exchangeFederatedIdentityRequest.js +9 -0
  29. package/dist/accessi-module/Console/generated/model/exchangeFederatedIdentityRequest.js.map +1 -0
  30. package/dist/accessi-module/Console/generated/model/federatedAuthenticationResultDto.d.ts +15 -0
  31. package/dist/accessi-module/Console/generated/model/federatedAuthenticationResultDto.js +3 -0
  32. package/dist/accessi-module/Console/generated/model/federatedAuthenticationResultDto.js.map +1 -0
  33. package/dist/accessi-module/Console/generated/model/federatedExchangeResponse.d.ts +14 -0
  34. package/dist/accessi-module/Console/generated/model/federatedExchangeResponse.js +3 -0
  35. package/dist/accessi-module/Console/generated/model/federatedExchangeResponse.js.map +1 -0
  36. package/dist/accessi-module/Console/generated/model/index.d.ts +6 -0
  37. package/dist/accessi-module/Console/generated/model/index.js +6 -0
  38. package/dist/accessi-module/Console/generated/model/index.js.map +1 -1
  39. package/dist/accessi-module/Console/generated/model/loginResult.d.ts +2 -0
  40. package/dist/accessi-module/Console/generated/model/loginResult.js.map +1 -1
  41. package/dist/accessi-module/Console/generated/model/refreshTokenRequest.d.ts +10 -0
  42. package/dist/accessi-module/Console/generated/model/refreshTokenRequest.js +9 -0
  43. package/dist/accessi-module/Console/generated/model/refreshTokenRequest.js.map +1 -0
  44. package/dist/accessi-module/Console/generated/model/refreshTokenResponse.d.ts +14 -0
  45. package/dist/accessi-module/Console/generated/model/refreshTokenResponse.js +3 -0
  46. package/dist/accessi-module/Console/generated/model/refreshTokenResponse.js.map +1 -0
  47. package/dist/accessi-module/Console/generated/model/refreshTokenResult.d.ts +13 -0
  48. package/dist/accessi-module/Console/generated/model/refreshTokenResult.js +3 -0
  49. package/dist/accessi-module/Console/generated/model/refreshTokenResult.js.map +1 -0
  50. package/dist/accessi-module/Console/wiki.js +16 -3
  51. package/dist/accessi-module/Console/wiki.js.map +1 -1
  52. package/dist/accessi-module/Controllers/AccessiConsoleController.d.ts +6 -1
  53. package/dist/accessi-module/Controllers/AccessiConsoleController.js +24 -1
  54. package/dist/accessi-module/Controllers/AccessiConsoleController.js.map +1 -1
  55. package/dist/accessi-module/Controllers/AuthController.d.ts +3 -1
  56. package/dist/accessi-module/Controllers/AuthController.js +90 -8
  57. package/dist/accessi-module/Controllers/AuthController.js.map +1 -1
  58. package/dist/accessi-module/Controllers/ConsoleEntryController.d.ts +2 -0
  59. package/dist/accessi-module/Controllers/ConsoleEntryController.js +11 -3
  60. package/dist/accessi-module/Controllers/ConsoleEntryController.js.map +1 -1
  61. package/dist/accessi-module/Controllers/EmailController.js +1 -1
  62. package/dist/accessi-module/Controllers/EmailController.js.map +1 -1
  63. package/dist/accessi-module/Controllers/UserController.js +1 -1
  64. package/dist/accessi-module/Controllers/UserController.js.map +1 -1
  65. package/dist/accessi-module/Dtos/FederatedExchangeDtos.d.ts +21 -0
  66. package/dist/accessi-module/Dtos/FederatedExchangeDtos.js +74 -0
  67. package/dist/accessi-module/Dtos/FederatedExchangeDtos.js.map +1 -0
  68. package/dist/accessi-module/Dtos/LoginResponse.d.ts +1 -0
  69. package/dist/accessi-module/Dtos/LoginResponse.js +6 -0
  70. package/dist/accessi-module/Dtos/LoginResponse.js.map +1 -1
  71. package/dist/accessi-module/Dtos/RefreshTokenDtos.d.ts +12 -0
  72. package/dist/accessi-module/Dtos/RefreshTokenDtos.js +50 -0
  73. package/dist/accessi-module/Dtos/RefreshTokenDtos.js.map +1 -0
  74. package/dist/accessi-module/Dtos/index.d.ts +2 -0
  75. package/dist/accessi-module/Dtos/index.js +2 -0
  76. package/dist/accessi-module/Dtos/index.js.map +1 -1
  77. package/dist/accessi-module/Services/AuthService/AuthService.d.ts +14 -0
  78. package/dist/accessi-module/Services/AuthService/AuthService.js +99 -6
  79. package/dist/accessi-module/Services/AuthService/AuthService.js.map +1 -1
  80. package/dist/accessi-module/database-updates/accessiOptionsFromEnv.js +8 -16
  81. package/dist/accessi-module/database-updates/accessiOptionsFromEnv.js.map +1 -1
  82. package/dist/accessi-module/federated-auth/FederatedAuthExchangeController.d.ts +15 -0
  83. package/dist/accessi-module/federated-auth/FederatedAuthExchangeController.js +85 -0
  84. package/dist/accessi-module/federated-auth/FederatedAuthExchangeController.js.map +1 -0
  85. package/dist/accessi-module/federated-auth/FederatedAuthService.js +7 -1
  86. package/dist/accessi-module/federated-auth/FederatedAuthService.js.map +1 -1
  87. package/dist/accessi-module/index.d.ts +17 -1
  88. package/dist/accessi-module/index.js +71 -5
  89. package/dist/accessi-module/index.js.map +1 -1
  90. package/dist/accessi-module/jwt/jwt.strategy.js +4 -0
  91. package/dist/accessi-module/jwt/jwt.strategy.js.map +1 -1
  92. package/dist/accessi-module/middleware/authenticateGen.js +4 -0
  93. package/dist/accessi-module/middleware/authenticateGen.js.map +1 -1
  94. package/dist/accessi-module/security/audit.d.ts +7 -0
  95. package/dist/accessi-module/security/audit.js +14 -0
  96. package/dist/accessi-module/security/audit.js.map +1 -0
  97. package/dist/accessi-module/security/authenticatedToken.d.ts +1 -1
  98. package/dist/accessi-module/security/authenticatedToken.js +3 -3
  99. package/dist/accessi-module/security/authenticatedToken.js.map +1 -1
  100. package/dist/accessi-module/security/consoleEntry.d.ts +58 -11
  101. package/dist/accessi-module/security/consoleEntry.js +181 -31
  102. package/dist/accessi-module/security/consoleEntry.js.map +1 -1
  103. package/dist/accessi-module/security/edgeRateLimit.d.ts +33 -0
  104. package/dist/accessi-module/security/edgeRateLimit.js +81 -0
  105. package/dist/accessi-module/security/edgeRateLimit.js.map +1 -0
  106. package/dist/accessi-module/security/jwks.d.ts +33 -0
  107. package/dist/accessi-module/security/jwks.js +83 -0
  108. package/dist/accessi-module/security/jwks.js.map +1 -0
  109. package/dist/accessi-module/security/jwksBootstrap.d.ts +15 -0
  110. package/dist/accessi-module/security/jwksBootstrap.js +44 -0
  111. package/dist/accessi-module/security/jwksBootstrap.js.map +1 -0
  112. package/dist/accessi-module/security/jwtTokenOptions.d.ts +9 -2
  113. package/dist/accessi-module/security/jwtTokenOptions.js +18 -4
  114. package/dist/accessi-module/security/jwtTokenOptions.js.map +1 -1
  115. package/dist/accessi-module/security/passwordResetToken.d.ts +1 -1
  116. package/dist/accessi-module/security/passwordResetToken.js.map +1 -1
  117. package/dist/accessi-module/security/publicAuthRateLimit.d.ts +9 -7
  118. package/dist/accessi-module/security/publicAuthRateLimit.js +55 -78
  119. package/dist/accessi-module/security/publicAuthRateLimit.js.map +1 -1
  120. package/dist/accessi-module/security/rateLimitStore.d.ts +34 -0
  121. package/dist/accessi-module/security/rateLimitStore.js +145 -0
  122. package/dist/accessi-module/security/rateLimitStore.js.map +1 -0
  123. package/dist/accessi-module/security/refreshTokenRotation.d.ts +40 -0
  124. package/dist/accessi-module/security/refreshTokenRotation.js +91 -0
  125. package/dist/accessi-module/security/refreshTokenRotation.js.map +1 -0
  126. package/dist/accessi-module/security/serviceTokenGuard.d.ts +3 -1
  127. package/dist/accessi-module/security/serviceTokenGuard.js +11 -6
  128. package/dist/accessi-module/security/serviceTokenGuard.js.map +1 -1
  129. package/dist/accessi-module/security/serviceTokenRateLimit.d.ts +5 -4
  130. package/dist/accessi-module/security/serviceTokenRateLimit.js +33 -29
  131. package/dist/accessi-module/security/serviceTokenRateLimit.js.map +1 -1
  132. package/dist/accessi-module/security/sessionRevocation.d.ts +52 -0
  133. package/dist/accessi-module/security/sessionRevocation.js +124 -0
  134. package/dist/accessi-module/security/sessionRevocation.js.map +1 -0
  135. package/dist/accessi-module/security/validateOptions.d.ts +19 -0
  136. package/dist/accessi-module/security/validateOptions.js +115 -0
  137. package/dist/accessi-module/security/validateOptions.js.map +1 -0
  138. package/dist/emilsoftware-utilities-2.0.0-dev.69.tgz +0 -0
  139. package/dist/initEmilsoftwareModule.js +39 -3
  140. package/dist/initEmilsoftwareModule.js.map +1 -1
  141. package/dist/scripts/startAccessiService.d.ts +1 -0
  142. package/dist/scripts/startAccessiService.js +11 -0
  143. package/dist/scripts/startAccessiService.js.map +1 -0
  144. package/docs/accessi-openapi-fingerprint.json +190 -0
  145. package/docs/gateway-rate-limit.md +98 -0
  146. package/docs/standalone-service.md +176 -0
  147. package/integration-prompt.md +299 -8
  148. package/package.json +11 -5
  149. package/dist/emilsoftware-utilities-2.0.0-dev.67.tgz +0 -0
@@ -74,6 +74,60 @@ const { nestApp, document } = await initializeAccessiModule(app, options);
74
74
 
75
75
  Aprire la porta HTTP solo dopo l'await.
76
76
 
77
+ ### Avvio standalone
78
+
79
+ Per un servizio Accessi autonomo:
80
+
81
+ ```ts
82
+ import { startAccessiService } from "emilsoftware-utilities/accessi/server";
83
+
84
+ await startAccessiService({
85
+ port: Number(process.env.PORT ?? 3000),
86
+ healthPath: "/health",
87
+ metrics: true,
88
+ });
89
+ ```
90
+
91
+ Oppure:
92
+
93
+ ```bash
94
+ npm run accessi:serve
95
+ ```
96
+
97
+ Endpoint:
98
+
99
+ ```text
100
+ GET /health/live
101
+ GET /health/ready
102
+ GET /health/metrics
103
+ ```
104
+
105
+ `/health/ready` controlla bootstrap, database, schema, store Redis configurati e `readinessChecks` custom. `startAccessiService` registra SIGTERM/SIGINT e chiude Nest e i pool Firebird.
106
+
107
+ In produzione il servizio viene eseguito come processo Node con systemd/PM2/supervisor:
108
+
109
+ ```bash
110
+ npm run accessi:serve:dist
111
+ ```
112
+
113
+ Dettagli e esempio systemd: `docs/standalone-service.md`.
114
+
115
+ ### Validazione configurazione
116
+
117
+ ```ts
118
+ import {
119
+ assertValidAccessiOptions,
120
+ validateAccessiOptions,
121
+ } from "emilsoftware-utilities/accessi/server";
122
+
123
+ const validation = validateAccessiOptions(options);
124
+ // validation.errors / validation.warnings
125
+
126
+ assertValidAccessiOptions(options); // fail-fast
127
+ ```
128
+
129
+ `createAccessiService` la applica automaticamente. Controlla JWT/key, DB, encryption key, URL email, rate limit, console entry e `http`.
130
+
77
131
  ### Bootstrap Nest
78
132
 
79
133
  ```ts
@@ -150,6 +204,151 @@ const merged = mergeOpenApiDocuments(hostDocument, accessiDocument, {
150
204
  });
151
205
  ```
152
206
 
207
+ ### HTTP, CORS e proxy
208
+
209
+ ```ts
210
+ http: {
211
+ cors: {
212
+ origin: ["https://app.example.com"],
213
+ credentials: false,
214
+ },
215
+ trustProxy: 1, // load balancer/reverse proxy
216
+ securityHeaders: true,
217
+ }
218
+ ```
219
+
220
+ - `cors: false` disabilita CORS.
221
+ - `trustProxy` e obbligatorio per rate limit corretti dietro proxy.
222
+ - `securityHeaders` aggiunge `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`, `Permissions-Policy`.
223
+ - Con Bearer token non serve CSRF. Se usi cookie httpOnly, aggiungi CSRF e cookie same-site.
224
+
225
+ ### JWKS e rotazione chiavi
226
+
227
+ Con RS256/ES256 Accessi pubblica automaticamente:
228
+
229
+ ```text
230
+ GET /accessi/.well-known/jwks.json
231
+ ```
232
+
233
+ Configurazione:
234
+
235
+ ```ts
236
+ jwtOptions: {
237
+ algorithm: "RS256",
238
+ privateKey: process.env.ACCESSI_JWT_PRIVATE_KEY,
239
+ publicKey: process.env.ACCESSI_JWT_PUBLIC_KEY,
240
+ keyId: "accessi-2026-02",
241
+ jwksPath: "accessi/.well-known/jwks.json",
242
+ previousPublicKeys: [
243
+ {
244
+ publicKey: process.env.ACCESSI_JWT_PUBLIC_KEY_2026_01!,
245
+ keyId: "accessi-2026-01",
246
+ algorithm: "RS256",
247
+ },
248
+ ],
249
+ }
250
+ ```
251
+
252
+ Rotazione:
253
+
254
+ 1. generare la nuova coppia
255
+ 2. pubblicare la nuova chiave mantenendo la precedente in `previousPublicKeys`
256
+ 3. firmare con la nuova `keyId`
257
+ 4. attendere la scadenza massima dei token + il TTL cache della JWKS (default 5 minuti)
258
+ 5. rimuovere la chiave precedente
259
+
260
+ Per un'app Nest esistente si può usare `setupAccessiJwks(app.getHttpAdapter().getInstance(), options)`.
261
+
262
+ ### Store ticket console
263
+
264
+ Il default è in memoria, adatto a una sola istanza. In multi-istanza configurare uno store condiviso:
265
+
266
+ ```ts
267
+ import { createRedisConsoleEntryStore } from "emilsoftware-utilities/accessi/server";
268
+
269
+ consoleEntry: {
270
+ ttlMs: 60_000,
271
+ store: createRedisConsoleEntryStore({
272
+ client: redis, // ioredis o client compatibile con eval(script, numKeys, ...args)
273
+ encryptionKey: process.env.ACCESSI_CONSOLE_ENTRY_KEY!,
274
+ keyPrefix: "accessi:console-entry:",
275
+ }),
276
+ }
277
+ ```
278
+
279
+ Lo store Redis:
280
+
281
+ - cifra il JWT con AES-256-GCM
282
+ - usa `SET ... PX ... NX` con TTL
283
+ - consuma con `GET` + `DEL` atomici in Lua
284
+ - rende il ticket monouso anche tra istanze diverse
285
+
286
+ Se lo store condiviso non è raggiungibile, la console deve fallire chiusa.
287
+
288
+ ### Rate limit condiviso
289
+
290
+ Con più istanze anche i rate limit devono usare uno store condiviso:
291
+
292
+ ```ts
293
+ import { createRedisRateLimitStore } from "emilsoftware-utilities/accessi/server";
294
+
295
+ const rateLimitStore = createRedisRateLimitStore({
296
+ client: redis, // ioredis o client compatibile con eval(script, numKeys, ...args)
297
+ keyPrefix: "accessi:rate:",
298
+ });
299
+
300
+ publicAuthRateLimit: {
301
+ enabled: true,
302
+ store: rateLimitStore,
303
+ login: { windowMs: 15 * 60_000, maxAttempts: 10 },
304
+ twoFactorVerify: { windowMs: 15 * 60_000, maxAttempts: 20 },
305
+ },
306
+
307
+ serviceTokens: {
308
+ enabled: true,
309
+ rateLimitStore: rateLimitStore,
310
+ },
311
+ ```
312
+
313
+ Lo store Redis usa `INCR` + `PEXPIRE` atomici:
314
+
315
+ - conta tentativi per IP e per soggetto (email, challenge, ticket, tokenId);
316
+ - il limite è globale tra le istanze;
317
+ - non viene azzerato da un restart;
318
+ - se Redis non è raggiungibile il controllo fallisce chiuso: l’endpoint risponde con errore, non autorizza.
319
+
320
+ Default: store in memoria. Va bene per una singola istanza o per ambienti di sviluppo.
321
+
322
+ ### Edge rate limit per IP
323
+
324
+ Per una soglia grossolana davanti alle rotte Accessi:
325
+
326
+ ```ts
327
+ edgeRateLimit: {
328
+ enabled: true,
329
+ windowMs: 60_000,
330
+ maxAttempts: 300,
331
+ store: rateLimitStore, // stesso store Redis, opzionale
332
+ }
333
+ ```
334
+
335
+ Viene applicato prima del dispatcher Accessi e copre `/api/accessi/*`, OpenAPI e JWKS. In caso di store non disponibile risponde `503 ACCESSI_RATE_LIMIT_UNAVAILABLE`; quando la soglia è superata risponde `429 ACCESSI_EDGE_RATE_LIMITED` con `Retry-After`.
336
+
337
+ Il gateway/API gateway resta consigliato per:
338
+
339
+ - limiti di connessione e burst;
340
+ - bot protection e WAF;
341
+ - rate limit per IP a livello di rete;
342
+ - blocco geografico o reputazionale.
343
+
344
+ Configurazioni di riferimento: `docs/gateway-rate-limit.md`.
345
+
346
+ ### Audit
347
+
348
+ Resource server ed edge rate limit producono eventi strutturati tramite `auditAccessi`. I campi sensibili vengono redatti dal Logger. Disattivabile con `audit: false` nelle rispettive opzioni.
349
+
350
+ Nel servizio standalone le metriche Prometheus sono su `/health/metrics`.
351
+
153
352
  ---
154
353
 
155
354
  ## 4. Backend fratello — Express
@@ -190,7 +389,9 @@ Dopo il middleware:
190
389
  - **HS256**: usare `jwt.serverSecret` con `ACCESSI_JWT_SECRET`. Il resource server deriva la chiave access con la stessa KDF del server.
191
390
  - **RS256/ES256**: usare `jwt.publicKey` PEM oppure `jwt.jwks.url`. Consigliato in multi-servizio.
192
391
  - Il server Accessi supporta `jwtOptions.algorithm`, `privateKey`, `publicKey`, `keyId`.
193
- - Accessi non espone ancora un endpoint JWKS integrato: pubblicare la chiave pubblica con il proprio hosting/reverse proxy oppure usare `publicKey` PEM.
392
+ - Accessi espone la JWKS su `jwtOptions.jwksPath` (default `accessi/.well-known/jwks.json`) quando usa RS/ES.
393
+ - La JWKS include la chiave corrente e le `previousPublicKeys`, per la rotazione senza downtime.
394
+ - Configurare il resource server con `jwks: { url: "https://accessi.example.com/accessi/.well-known/jwks.json" }`.
194
395
 
195
396
  ### Errori
196
397
 
@@ -274,6 +475,60 @@ Risposta:
274
475
  - Il resource server cachea i risultati attivi per `cacheTtlMs` (default 30s). I risultati inattivi non sono cachati.
275
476
  - Fail closed: se Accessi Service non risponde, la richiesta non viene autorizzata.
276
477
 
478
+ ### SSO exchange service-to-service
479
+
480
+ Se un backend fratello valida un provider SSO proprio, non deve duplicare utenti o grant. Dopo aver verificato firma, issuer, audience e subject del provider, chiama:
481
+
482
+ ```http
483
+ POST /api/accessi/federated-auth/exchange
484
+ Authorization: Bearer <service-token>
485
+ Content-Type: application/json
486
+
487
+ { "provider": "azure-ad-acme-produzione", "subject": "<oid o sub>" }
488
+ ```
489
+
490
+ Scope richiesto: `accessi:federated:exchange`.
491
+
492
+ La risposta contiene:
493
+
494
+ - `codiceUtente`
495
+ - `login` con profilo, grant e token
496
+ - `token` assente quando è richiesta la verifica 2FA
497
+
498
+ Accessi non riceve il token del provider: riceve solo l'identità esterna già verificata.
499
+
500
+ ### Revoca sessioni
501
+
502
+ Configurazione:
503
+
504
+ ```ts
505
+ import { createRedisSessionRevocationStore } from "emilsoftware-utilities/accessi/server";
506
+
507
+ sessionRevocation: {
508
+ store: createRedisSessionRevocationStore({
509
+ client: redis,
510
+ ttlMs: 30 * 24 * 60 * 60 * 1000, // maggiore della durata massima dei token
511
+ }),
512
+ }
513
+ ```
514
+
515
+ Endpoint utente:
516
+
517
+ ```http
518
+ POST /api/accessi/auth/revoke-sessions
519
+ Authorization: Bearer <access-token>
520
+ ```
521
+
522
+ Invalida i token emessi prima della chiamata. I token emessi dopo restano validi. Per i backend fratelli configurare lo stesso store anche nel resource server:
523
+
524
+ ```ts
525
+ revocation: {
526
+ store: revocationStore,
527
+ }
528
+ ```
529
+
530
+ Senza store condiviso la revoca vale solo nel processo che la esegue.
531
+
277
532
  ---
278
533
 
279
534
  ## 7. Frontend
@@ -303,6 +558,29 @@ configureAccessiClient({
303
558
  });
304
559
  ```
305
560
 
561
+ Se `jwtOptions.refreshExpiresIn` è configurato, la risposta contiene anche `refreshToken`. Conservarlo separatamente dall’access token e usarlo su:
562
+
563
+ ```http
564
+ POST /api/accessi/auth/refresh
565
+ Content-Type: application/json
566
+
567
+ { "refreshToken": "<refresh-token>" }
568
+ ```
569
+
570
+ La risposta contiene un nuovo `token` e un nuovo `refreshToken`. Il refresh token ha `typ: "refresh"` e non va mai usato come Bearer sulle API.
571
+
572
+ Per rendere il refresh token monouso anche in multi-istanza:
573
+
574
+ ```ts
575
+ import { createRedisRefreshTokenRotationStore } from "emilsoftware-utilities/accessi/server";
576
+
577
+ refreshTokenRotation: {
578
+ store: createRedisRefreshTokenRotationStore({ client: redis }),
579
+ }
580
+ ```
581
+
582
+ Ogni refresh consuma il vecchio `jti` in modo atomico. Due refresh concorrenti dello stesso token producono un solo successo.
583
+
306
584
  Per token che cambiano nel tempo:
307
585
 
308
586
  ```ts
@@ -332,6 +610,7 @@ const consoleUrl = buildAccessiConsoleUrl("https://accessi.example.com", {
332
610
  - Il ticket e monouso, dura 60 secondi.
333
611
  - Per il 2FA handoff usare `twoFactorChallengeId`.
334
612
  - Se la console deve stare sullo stesso dominio, fare reverse proxy di `/api/accessi/*` verso Accessi Service, non copiare gli asset.
613
+ - Se la UI della console è su un dominio diverso dall’API, configurare `consoleApiBaseUrl`: Accessi inietta `window.__ACCESSI_API_BASE_URL__` nella pagina servita.
335
614
 
336
615
  ### Web component
337
616
 
@@ -362,6 +641,7 @@ Per chiamare l'introspection o API tecniche:
362
641
  2. Creare un token dalla console o con `POST /api/accessi/service-token`.
363
642
  3. Assegnare lo scope minimo necessario:
364
643
  - `accessi:introspect` per l'introspection
644
+ - `accessi:federated:exchange` per scambiare un'identita SSO verificata
365
645
  - altri scope applicativi secondo necessita
366
646
  4. Conservarlo come secret nel backend fratello.
367
647
  5. Ruotarlo periodicamente; il segreto e mostrato una sola volta.
@@ -397,6 +677,7 @@ ACCESSI_DB_PASSWORD=
397
677
  ACCESSI_ENCRYPTION_KEY=
398
678
  ACCESSI_JWT_SECRET=
399
679
  ACCESSI_JWT_EXPIRES=24h
680
+ ACCESSI_JWT_REFRESH_EXPIRES=30d
400
681
  ACCESSI_JWT_ISSUER=accessi
401
682
  ACCESSI_JWT_AUDIENCE=backend-app
402
683
  # Opzionale RS256
@@ -404,6 +685,9 @@ ACCESSI_JWT_ALGORITHM=RS256
404
685
  ACCESSI_JWT_PRIVATE_KEY=
405
686
  ACCESSI_JWT_PUBLIC_KEY=
406
687
  ACCESSI_JWT_KEY_ID=
688
+ ACCESSI_JWKS_PATH=accessi/.well-known/jwks.json
689
+ ACCESSI_CONSOLE_ENTRY_TTL_MS=60000
690
+ ACCESSI_CONSOLE_ENTRY_KEY=
407
691
  SMTP_HOST=
408
692
  SMTP_PORT=587
409
693
  SMTP_USER=
@@ -428,6 +712,7 @@ Frontend:
428
712
  ```env
429
713
  ACCESSI_API_BASE_URL=https://accessi.example.com
430
714
  ACCESSI_CONSOLE_URL=https://accessi.example.com/api/accessi/console
715
+ ACCESSI_JWKS_URL=https://accessi.example.com/accessi/.well-known/jwks.json
431
716
  ```
432
717
 
433
718
  ---
@@ -443,16 +728,22 @@ Checklist:
443
728
  5. Spostare la console una sola volta.
444
729
  6. Passare i backend fratelli a `resource-server`.
445
730
  7. Configurare un service token con `accessi:introspect`.
446
- 8. Testare:
731
+ 8. Pubblicare la JWKS e configurare i resource server su `jwks.url`.
732
+ 9. In multi-istanza configurare `consoleEntry.store`, `publicAuthRateLimit.store` e `serviceTokens.rateLimitStore` con Redis.
733
+ 10. Testare:
447
734
  - login
448
735
  - token valido
449
736
  - token scaduto
450
737
  - utente disabilitato
451
738
  - revoca grants
452
739
  - Accessi Service spento (fail closed)
453
- - console entry ticket
454
- 9. Aggiornare CORS se i frontend cambiano dominio.
455
- 10. Aggiornare monitoraggio e log senza includere token.
740
+ - console entry ticket su istanze diverse
741
+ - rate limit condiviso tra istanze
742
+ - rotazione JWKS con vecchia chiave ancora valida
743
+ 11. Aggiornare CORS se i frontend cambiano dominio.
744
+ 12. Aggiornare monitoraggio e log senza includere token.
745
+ 13. Eseguire `npm run check:accessi-openapi` e `npm run typecheck` prima del rilascio.
746
+ 14. Dopo una modifica intenzionale al contratto, aggiornare il fingerprint con `npm run accessi:update-openapi-fingerprint` e includerlo nel commit.
456
747
 
457
748
  ---
458
749
 
@@ -474,8 +765,8 @@ Checklist:
474
765
 
475
766
  ## 14. Limiti attuali da conoscere
476
767
 
477
- - I ticket di ingresso console sono in memoria del processo: con piu istanze di Accessi Service servono sticky session o uno store condiviso.
478
- - I rate limit pubblici e dei service token sono in memoria.
479
- - Accessi non espone ancora un endpoint JWKS pronto: il resource server sa verificare una JWKS, ma la chiave pubblica va pubblicata dall'host.
768
+ - I ticket di ingresso console usano lo store in memoria per default: in multi-istanza configurare `consoleEntry.store` con Redis. Lo store Redis e cifrato e monouso, ma richiede un client Redis compatibile.
769
+ - I rate limit usano lo store in memoria per default: in multi-istanza configurare `publicAuthRateLimit.store` e `serviceTokens.rateLimitStore` con Redis. Il gateway resta comunque consigliato per i limiti grossolani per IP.
480
770
  - Il client generato e completo (include endpoint amministrativi): non distribuirlo a frontend non amministrativi senza valutare gli endpoint esposti. Per un client pubblico ridotto, generare un sottoinsieme per tag con Orval.
481
771
  - I DTO in `accessi/contracts/dtos` dipendono da Nest/class-validator: non sono pensati per bundle frontend minimali.
772
+ - La JWKS viene pubblicata solo con algoritmi asimmetrici: con HS256 non esiste una chiave pubblica da distribuire.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "emilsoftware-utilities",
3
- "version": "2.0.0-dev.67",
3
+ "version": "2.0.0-dev.69",
4
4
  "description": "Utilities for EmilSoftware",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -87,7 +87,9 @@
87
87
  },
88
88
  "scripts": {
89
89
  "release": "semantic-release",
90
- "build": "npm run generate:accessi-openapi && npm run generate:accessi-client && npm run generate:accessi-wiki && tsc && tsc -p tsconfig.console.json --noEmit && npm run build:accessi-console && npm run copy-views",
90
+ "build": "npm run generate:accessi-openapi && npm run check:accessi-openapi && npm run generate:accessi-client && npm run generate:accessi-wiki && tsc && tsc -p tsconfig.console.json --noEmit && npm run build:accessi-console && npm run copy-views",
91
+ "check:accessi-openapi": "node scripts/check-accessi-openapi.cjs",
92
+ "accessi:update-openapi-fingerprint": "node scripts/check-accessi-openapi.cjs --update",
91
93
  "generate:accessi-openapi": "ts-node --transpile-only src/scripts/generateAccessiOpenApi.ts",
92
94
  "generate:accessi-client": "orval --config orval.config.ts",
93
95
  "generate:accessi-wiki": "ts-node --transpile-only src/scripts/generateAccessiWiki.ts",
@@ -95,14 +97,17 @@
95
97
  "copy-views": "node copy-views.js",
96
98
  "db:update:accessi": "ts-node src/accessi-module/database-updates/runAccessiDbUpdate.ts",
97
99
  "db:update:accessi:dist": "npm run build && node dist/accessi-module/database-updates/runAccessiDbUpdate.js",
100
+ "accessi:serve": "ts-node src/scripts/startAccessiService.ts",
101
+ "accessi:serve:dist": "node dist/scripts/startAccessiService.js",
102
+ "accessi:generate-jwt-keys": "node scripts/generate-accessi-jwt-keys.cjs",
98
103
  "db:catalog:accessi": "ts-node src/accessi-module/database-updates/runAccessiCatalogMigrations.ts",
99
104
  "db:catalog:accessi:check": "ts-node src/accessi-module/database-updates/runAccessiCatalogMigrations.ts --check",
100
105
  "prepublishOnly": "npm run build",
101
106
  "typecheck": "tsc --noEmit",
102
107
  "lint": "node scripts/lint.cjs",
103
108
  "test": "npm run typecheck && npm run lint && npm run test:unit",
104
- "test:unit": "node --test test/angular-components.cjs test/menus-catalog.mjs test/menu-scopes.cjs test/accessi-regressions.cjs test/password-reset-regressions.cjs test/two-factor.cjs test/accessi-schema.cjs test/utilities-regressions.cjs test/service-token-regressions.cjs test/accessi-email-config.cjs test/accessi-catalog-scripts.cjs test/openapi-config.cjs test/resource-server.cjs",
105
- "test:coverage": "node --experimental-test-coverage --test-coverage-lines=50 --test-coverage-functions=60 --test-coverage-branches=60 --test test/angular-components.cjs test/menus-catalog.mjs test/menu-scopes.cjs test/accessi-regressions.cjs test/password-reset-regressions.cjs test/two-factor.cjs test/accessi-schema.cjs test/utilities-regressions.cjs test/service-token-regressions.cjs test/accessi-email-config.cjs test/accessi-catalog-scripts.cjs test/openapi-config.cjs test/resource-server.cjs",
109
+ "test:unit": "node --test test/angular-components.cjs test/menus-catalog.mjs test/menu-scopes.cjs test/accessi-regressions.cjs test/password-reset-regressions.cjs test/two-factor.cjs test/accessi-schema.cjs test/utilities-regressions.cjs test/service-token-regressions.cjs test/accessi-email-config.cjs test/accessi-catalog-scripts.cjs test/openapi-config.cjs test/resource-server.cjs test/accessi-security-standards.cjs test/accessi-service.cjs test/refresh-token.cjs",
110
+ "test:coverage": "node --experimental-test-coverage --test-coverage-lines=50 --test-coverage-functions=60 --test-coverage-branches=60 --test test/angular-components.cjs test/menus-catalog.mjs test/menu-scopes.cjs test/accessi-regressions.cjs test/password-reset-regressions.cjs test/two-factor.cjs test/accessi-schema.cjs test/utilities-regressions.cjs test/service-token-regressions.cjs test/accessi-email-config.cjs test/accessi-catalog-scripts.cjs test/openapi-config.cjs test/resource-server.cjs test/accessi-security-standards.cjs test/accessi-service.cjs test/refresh-token.cjs",
106
111
  "test:accessi": "npm run test:unit",
107
112
  "test:all": "npm test && npm run test:integration",
108
113
  "test:integration": "node test/integration/run-firebird.cjs 5",
@@ -183,7 +188,8 @@
183
188
  },
184
189
  "files": [
185
190
  "dist",
186
- "integration-prompt.md"
191
+ "integration-prompt.md",
192
+ "docs"
187
193
  ],
188
194
  "config": {
189
195
  "commitizen": {