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

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 (47) hide show
  1. package/dist/accessi-module/AccessiModule.d.ts +27 -1
  2. package/dist/accessi-module/AccessiModule.js.map +1 -1
  3. package/dist/accessi-module/Console/console.js +12 -6
  4. package/dist/accessi-module/Console/wiki.js +9 -3
  5. package/dist/accessi-module/Console/wiki.js.map +1 -1
  6. package/dist/accessi-module/Controllers/AuthController.js +6 -6
  7. package/dist/accessi-module/Controllers/AuthController.js.map +1 -1
  8. package/dist/accessi-module/Controllers/ConsoleEntryController.d.ts +2 -0
  9. package/dist/accessi-module/Controllers/ConsoleEntryController.js +11 -3
  10. package/dist/accessi-module/Controllers/ConsoleEntryController.js.map +1 -1
  11. package/dist/accessi-module/Controllers/EmailController.js +1 -1
  12. package/dist/accessi-module/Controllers/EmailController.js.map +1 -1
  13. package/dist/accessi-module/Controllers/UserController.js +1 -1
  14. package/dist/accessi-module/Controllers/UserController.js.map +1 -1
  15. package/dist/accessi-module/database-updates/accessiOptionsFromEnv.js +8 -16
  16. package/dist/accessi-module/database-updates/accessiOptionsFromEnv.js.map +1 -1
  17. package/dist/accessi-module/index.d.ts +9 -1
  18. package/dist/accessi-module/index.js +34 -4
  19. package/dist/accessi-module/index.js.map +1 -1
  20. package/dist/accessi-module/security/consoleEntry.d.ts +56 -11
  21. package/dist/accessi-module/security/consoleEntry.js +165 -31
  22. package/dist/accessi-module/security/consoleEntry.js.map +1 -1
  23. package/dist/accessi-module/security/edgeRateLimit.d.ts +31 -0
  24. package/dist/accessi-module/security/edgeRateLimit.js +70 -0
  25. package/dist/accessi-module/security/edgeRateLimit.js.map +1 -0
  26. package/dist/accessi-module/security/jwks.d.ts +33 -0
  27. package/dist/accessi-module/security/jwks.js +83 -0
  28. package/dist/accessi-module/security/jwks.js.map +1 -0
  29. package/dist/accessi-module/security/publicAuthRateLimit.d.ts +8 -6
  30. package/dist/accessi-module/security/publicAuthRateLimit.js +54 -78
  31. package/dist/accessi-module/security/publicAuthRateLimit.js.map +1 -1
  32. package/dist/accessi-module/security/rateLimitStore.d.ts +32 -0
  33. package/dist/accessi-module/security/rateLimitStore.js +129 -0
  34. package/dist/accessi-module/security/rateLimitStore.js.map +1 -0
  35. package/dist/accessi-module/security/serviceTokenGuard.d.ts +3 -1
  36. package/dist/accessi-module/security/serviceTokenGuard.js +11 -6
  37. package/dist/accessi-module/security/serviceTokenGuard.js.map +1 -1
  38. package/dist/accessi-module/security/serviceTokenRateLimit.d.ts +5 -4
  39. package/dist/accessi-module/security/serviceTokenRateLimit.js +33 -29
  40. package/dist/accessi-module/security/serviceTokenRateLimit.js.map +1 -1
  41. package/dist/emilsoftware-utilities-2.0.0-dev.68.tgz +0 -0
  42. package/dist/initEmilsoftwareModule.js +20 -2
  43. package/dist/initEmilsoftwareModule.js.map +1 -1
  44. package/docs/gateway-rate-limit.md +98 -0
  45. package/integration-prompt.md +139 -8
  46. package/package.json +5 -4
  47. package/dist/emilsoftware-utilities-2.0.0-dev.67.tgz +0 -0
@@ -1 +1 @@
1
- {"version":3,"file":"serviceTokenGuard.js","sourceRoot":"","sources":["../../../src/accessi-module/security/serviceTokenGuard.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;AAuBA,wDAMC;AA7BD,2CAOwB;AACxB,uCAAyC;AAEzC,6FAAsI;AACtI,6DAAiE;AACjE,mEAAsE;AAEtE,iEAAiE;AACpD,QAAA,6BAA6B,GAAG,8BAA8B,CAAC;AAE5E,2EAA2E;AACpE,MAAM,yBAAyB,GAAG,CAAC,GAAG,MAAgB,EAAE,EAAE,CAAC,IAAA,oBAAW,EAAC,qCAA6B,EAAE,MAAM,CAAC,CAAC;AAAxG,QAAA,yBAAyB,6BAA+E;AAIrH,iGAAiG;AACjG,SAAgB,sBAAsB,CAAC,GAAY;IACjD,MAAM,QAAQ,GAAI,GAAkC,CAAC,mBAAmB,CAAC;IACzE,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,8BAAqB,CAAC,mCAAmC,CAAC,CAAC;IACvE,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;GAKG;AAEI,IAAM,iBAAiB,GAAvB,MAAM,iBAAiB;IAC5B,YACmB,mBAAwC,EACxC,SAAoB;QADpB,wBAAmB,GAAnB,mBAAmB,CAAqB;QACxC,cAAS,GAAT,SAAS,CAAW;IACpC,CAAC;IAEE,WAAW,CAAC,OAAyB;;;YACzC,MAAM,OAAO,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,UAAU,EAAW,CAAC;YAC7D,MAAM,EAAE,GAAG,MAAA,MAAA,OAAO,CAAC,EAAE,mCAAI,MAAA,OAAO,CAAC,MAAM,0CAAE,aAAa,mCAAI,SAAS,CAAC;YACpE,IAAA,mDAA2B,EAAC,EAAE,CAAC,CAAC;YAEhC,MAAM,KAAK,GAAG,IAAA,8CAAyB,EAAC,OAAO,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC;YAC1E,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,MAAM,IAAI,8BAAqB,CAAC,6BAA6B,CAAC,CAAC;YACjE,CAAC;YAED,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,mBAAmB,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;YACtE,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACd,MAAM,IAAI,8BAAqB,CAAC,mDAAmD,CAAC,CAAC;YACvF,CAAC;YAEA,OAAsC,CAAC,mBAAmB,GAAG,QAAQ,CAAC;YAEvE,MAAM,cAAc,GAAG,MAAA,IAAI,CAAC,SAAS,CAAC,iBAAiB,CAAW,qCAA6B,EAAE;gBAC/F,OAAO,CAAC,UAAU,EAAE;gBACpB,OAAO,CAAC,QAAQ,EAAE;aACnB,CAAC,mCAAI,EAAE,CAAC;YACT,MAAM,YAAY,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,IAAA,0CAAoB,EAAC,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC;YACnG,IAAI,YAAY,EAAE,CAAC;gBACjB,MAAM,IAAI,2BAAkB,CAAC,UAAU,YAAY,qCAAqC,CAAC,CAAC;YAC5F,CAAC;YAED,OAAO,IAAI,CAAC;QACd,CAAC;KAAA;CACF,CAAA;AAlCY,8CAAiB;4BAAjB,iBAAiB;IAD7B,IAAA,mBAAU,GAAE;qCAG6B,yCAAmB;QAC7B,gBAAS;GAH5B,iBAAiB,CAkC7B","sourcesContent":["import {\n CanActivate,\n ExecutionContext,\n ForbiddenException,\n Injectable,\n SetMetadata,\n UnauthorizedException,\n} from '@nestjs/common';\nimport { Reflector } from '@nestjs/core';\nimport type { Request } from 'express';\nimport { ServiceTokenService, serviceTokenHasScope, VerifiedServiceToken } from '../Services/ServiceTokenService/ServiceTokenService';\nimport { extractAccessiBearerToken } from './authenticatedToken';\nimport { assertServiceTokenRateLimit } from './serviceTokenRateLimit';\n\n/** Chiave dei metadata usata da `@RequireServiceTokenScopes`. */\nexport const SERVICE_TOKEN_SCOPES_METADATA = 'accessi:service-token-scopes';\n\n/** Richiede che il token di servizio presenti tutti gli scope indicati. */\nexport const RequireServiceTokenScopes = (...scopes: string[]) => SetMetadata(SERVICE_TOKEN_SCOPES_METADATA, scopes);\n\nexport type AccessiServiceTokenRequest = Request & { accessiServiceToken?: VerifiedServiceToken };\n\n/** Restituisce il token di servizio verificato da `ServiceTokenGuard`; lancia 401 se assente. */\nexport function getAccessiServiceToken(req: Request): VerifiedServiceToken {\n const verified = (req as AccessiServiceTokenRequest).accessiServiceToken;\n if (!verified) {\n throw new UnauthorizedException('Token di servizio non verificato.');\n }\n return verified;\n}\n\n/**\n * Autentica le chiamate tecniche macchina-a-macchina tramite `Authorization: Bearer <token>`.\n *\n * Il token e per installazione/servizio, non legato a un utente. Gli scope richiesti si dichiarano\n * con `@RequireServiceTokenScopes('ia')`.\n */\n@Injectable()\nexport class ServiceTokenGuard implements CanActivate {\n constructor(\n private readonly serviceTokenService: ServiceTokenService,\n private readonly reflector: Reflector,\n ) {}\n\n async canActivate(context: ExecutionContext): Promise<boolean> {\n const request = context.switchToHttp().getRequest<Request>();\n const ip = request.ip ?? request.socket?.remoteAddress ?? 'unknown';\n assertServiceTokenRateLimit(ip);\n\n const token = extractAccessiBearerToken(request.headers['authorization']);\n if (!token) {\n throw new UnauthorizedException('Token di servizio mancante.');\n }\n\n const verified = await this.serviceTokenService.verify(token, { ip });\n if (!verified) {\n throw new UnauthorizedException('Token di servizio non valido, scaduto o revocato.');\n }\n\n (request as AccessiServiceTokenRequest).accessiServiceToken = verified;\n\n const requiredScopes = this.reflector.getAllAndOverride<string[]>(SERVICE_TOKEN_SCOPES_METADATA, [\n context.getHandler(),\n context.getClass(),\n ]) ?? [];\n const missingScope = requiredScopes.find((scope) => !serviceTokenHasScope(verified.scopes, scope));\n if (missingScope) {\n throw new ForbiddenException(`Scope '${missingScope}' non autorizzato per questo token.`);\n }\n\n return true;\n }\n}\n"]}
1
+ {"version":3,"file":"serviceTokenGuard.js","sourceRoot":"","sources":["../../../src/accessi-module/security/serviceTokenGuard.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAyBA,wDAMC;AA/BD,2CAQwB;AACxB,uCAAyC;AAGzC,6FAAsI;AACtI,6DAAiE;AACjE,mEAAsE;AAEtE,iEAAiE;AACpD,QAAA,6BAA6B,GAAG,8BAA8B,CAAC;AAE5E,2EAA2E;AACpE,MAAM,yBAAyB,GAAG,CAAC,GAAG,MAAgB,EAAE,EAAE,CAAC,IAAA,oBAAW,EAAC,qCAA6B,EAAE,MAAM,CAAC,CAAC;AAAxG,QAAA,yBAAyB,6BAA+E;AAIrH,iGAAiG;AACjG,SAAgB,sBAAsB,CAAC,GAAY;IACjD,MAAM,QAAQ,GAAI,GAAkC,CAAC,mBAAmB,CAAC;IACzE,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,8BAAqB,CAAC,mCAAmC,CAAC,CAAC;IACvE,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;GAKG;AAEI,IAAM,iBAAiB,GAAvB,MAAM,iBAAiB;IAC5B,YACmB,mBAAwC,EACxC,SAAoB,EACO,cAA8B;QAFzD,wBAAmB,GAAnB,mBAAmB,CAAqB;QACxC,cAAS,GAAT,SAAS,CAAW;QACO,mBAAc,GAAd,cAAc,CAAgB;IACzE,CAAC;IAEE,WAAW,CAAC,OAAyB;;;YACzC,MAAM,OAAO,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,UAAU,EAAW,CAAC;YAC7D,MAAM,EAAE,GAAG,MAAA,MAAA,OAAO,CAAC,EAAE,mCAAI,MAAA,OAAO,CAAC,MAAM,0CAAE,aAAa,mCAAI,SAAS,CAAC;YACpE,MAAM,IAAA,mDAA2B,EAAC,EAAE,EAAE,MAAA,IAAI,CAAC,cAAc,CAAC,aAAa,0CAAE,cAAc,CAAC,CAAC;YAEzF,MAAM,KAAK,GAAG,IAAA,8CAAyB,EAAC,OAAO,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC;YAC1E,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,MAAM,IAAI,8BAAqB,CAAC,6BAA6B,CAAC,CAAC;YACjE,CAAC;YAED,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,mBAAmB,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;YACtE,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACd,MAAM,IAAI,8BAAqB,CAAC,mDAAmD,CAAC,CAAC;YACvF,CAAC;YAEA,OAAsC,CAAC,mBAAmB,GAAG,QAAQ,CAAC;YAEvE,MAAM,cAAc,GAAG,MAAA,IAAI,CAAC,SAAS,CAAC,iBAAiB,CAAW,qCAA6B,EAAE;gBAC/F,OAAO,CAAC,UAAU,EAAE;gBACpB,OAAO,CAAC,QAAQ,EAAE;aACnB,CAAC,mCAAI,EAAE,CAAC;YACT,MAAM,YAAY,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,IAAA,0CAAoB,EAAC,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC;YACnG,IAAI,YAAY,EAAE,CAAC;gBACjB,MAAM,IAAI,2BAAkB,CAAC,UAAU,YAAY,qCAAqC,CAAC,CAAC;YAC5F,CAAC;YAED,OAAO,IAAI,CAAC;QACd,CAAC;KAAA;CACF,CAAA;AAnCY,8CAAiB;4BAAjB,iBAAiB;IAD7B,IAAA,mBAAU,GAAE;IAKR,WAAA,IAAA,eAAM,EAAC,iBAAiB,CAAC,CAAA;qCAFY,yCAAmB;QAC7B,gBAAS;GAH5B,iBAAiB,CAmC7B","sourcesContent":["import {\n CanActivate,\n ExecutionContext,\n ForbiddenException,\n Inject,\n Injectable,\n SetMetadata,\n UnauthorizedException,\n} from '@nestjs/common';\nimport { Reflector } from '@nestjs/core';\nimport type { Request } from 'express';\nimport type { AccessiOptions } from '../AccessiModule';\nimport { ServiceTokenService, serviceTokenHasScope, VerifiedServiceToken } from '../Services/ServiceTokenService/ServiceTokenService';\nimport { extractAccessiBearerToken } from './authenticatedToken';\nimport { assertServiceTokenRateLimit } from './serviceTokenRateLimit';\n\n/** Chiave dei metadata usata da `@RequireServiceTokenScopes`. */\nexport const SERVICE_TOKEN_SCOPES_METADATA = 'accessi:service-token-scopes';\n\n/** Richiede che il token di servizio presenti tutti gli scope indicati. */\nexport const RequireServiceTokenScopes = (...scopes: string[]) => SetMetadata(SERVICE_TOKEN_SCOPES_METADATA, scopes);\n\nexport type AccessiServiceTokenRequest = Request & { accessiServiceToken?: VerifiedServiceToken };\n\n/** Restituisce il token di servizio verificato da `ServiceTokenGuard`; lancia 401 se assente. */\nexport function getAccessiServiceToken(req: Request): VerifiedServiceToken {\n const verified = (req as AccessiServiceTokenRequest).accessiServiceToken;\n if (!verified) {\n throw new UnauthorizedException('Token di servizio non verificato.');\n }\n return verified;\n}\n\n/**\n * Autentica le chiamate tecniche macchina-a-macchina tramite `Authorization: Bearer <token>`.\n *\n * Il token e per installazione/servizio, non legato a un utente. Gli scope richiesti si dichiarano\n * con `@RequireServiceTokenScopes('ia')`.\n */\n@Injectable()\nexport class ServiceTokenGuard implements CanActivate {\n constructor(\n private readonly serviceTokenService: ServiceTokenService,\n private readonly reflector: Reflector,\n @Inject('ACCESSI_OPTIONS') private readonly accessiOptions: AccessiOptions,\n ) {}\n\n async canActivate(context: ExecutionContext): Promise<boolean> {\n const request = context.switchToHttp().getRequest<Request>();\n const ip = request.ip ?? request.socket?.remoteAddress ?? 'unknown';\n await assertServiceTokenRateLimit(ip, this.accessiOptions.serviceTokens?.rateLimitStore);\n\n const token = extractAccessiBearerToken(request.headers['authorization']);\n if (!token) {\n throw new UnauthorizedException('Token di servizio mancante.');\n }\n\n const verified = await this.serviceTokenService.verify(token, { ip });\n if (!verified) {\n throw new UnauthorizedException('Token di servizio non valido, scaduto o revocato.');\n }\n\n (request as AccessiServiceTokenRequest).accessiServiceToken = verified;\n\n const requiredScopes = this.reflector.getAllAndOverride<string[]>(SERVICE_TOKEN_SCOPES_METADATA, [\n context.getHandler(),\n context.getClass(),\n ]) ?? [];\n const missingScope = requiredScopes.find((scope) => !serviceTokenHasScope(verified.scopes, scope));\n if (missingScope) {\n throw new ForbiddenException(`Scope '${missingScope}' non autorizzato per questo token.`);\n }\n\n return true;\n }\n}\n"]}
@@ -1,8 +1,9 @@
1
- export declare function checkServiceTokenRateLimit(identifier: string): {
1
+ import type { RateLimitStore } from './rateLimitStore';
2
+ export declare function checkServiceTokenRateLimit(identifier: string, store?: RateLimitStore): Promise<{
2
3
  allowed: boolean;
3
4
  retryAfterSeconds: number;
4
- };
5
- /** Svuota i bucket (usato nei test). */
5
+ }>;
6
+ /** Svuota i bucket dello store di default (usato nei test). */
6
7
  export declare function resetServiceTokenRateLimit(): void;
7
8
  /** Lancia 429 con `Retry-After` quando il limite e superato. */
8
- export declare function assertServiceTokenRateLimit(identifier: string): void;
9
+ export declare function assertServiceTokenRateLimit(identifier: string, store?: RateLimitStore): Promise<void>;
@@ -1,49 +1,53 @@
1
1
  "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
2
11
  Object.defineProperty(exports, "__esModule", { value: true });
3
12
  exports.checkServiceTokenRateLimit = checkServiceTokenRateLimit;
4
13
  exports.resetServiceTokenRateLimit = resetServiceTokenRateLimit;
5
14
  exports.assertServiceTokenRateLimit = assertServiceTokenRateLimit;
6
15
  const common_1 = require("@nestjs/common");
16
+ const rateLimitStore_1 = require("./rateLimitStore");
7
17
  /**
8
18
  * Limite anti-abuso per la verifica dei token di servizio.
9
19
  *
10
20
  * Il segreto ha 256 bit di entropia (brute force non praticabile), ma il limite evita
11
- * che un client difettoso o malevolo martelli l'endpoint. In-memory: in cluster va
12
- * aggiunto un limite anche al gateway.
21
+ * che un client difettoso o malevolo martelli l'endpoint. Con `serviceTokens.rateLimitStore`
22
+ * e possibile usare uno store condiviso in cluster.
13
23
  */
14
24
  const WINDOW_MS = 60000;
15
25
  const MAX_ATTEMPTS = 120;
16
- const MAX_BUCKETS = 20000;
17
- const buckets = new Map();
18
- function checkServiceTokenRateLimit(identifier) {
19
- const now = Date.now();
20
- const key = identifier && identifier.trim() !== '' ? identifier : 'unknown';
21
- const bucket = buckets.get(key);
22
- if (!bucket || bucket.resetAt <= now) {
23
- if (buckets.size >= MAX_BUCKETS) {
24
- const oldestKey = buckets.keys().next().value;
25
- if (oldestKey !== undefined) {
26
- buckets.delete(oldestKey);
27
- }
26
+ function checkServiceTokenRateLimit(identifier_1) {
27
+ return __awaiter(this, arguments, void 0, function* (identifier, store = (0, rateLimitStore_1.getDefaultRateLimitStore)()) {
28
+ const key = identifier && identifier.trim() !== '' ? identifier.trim() : 'unknown';
29
+ try {
30
+ return yield store.consume(`service-token|${key}`, WINDOW_MS, MAX_ATTEMPTS);
28
31
  }
29
- buckets.set(key, { count: 1, resetAt: now + WINDOW_MS });
30
- return { allowed: true, retryAfterSeconds: 0 };
31
- }
32
- if (bucket.count >= MAX_ATTEMPTS) {
33
- return { allowed: false, retryAfterSeconds: Math.max(1, Math.ceil((bucket.resetAt - now) / 1000)) };
34
- }
35
- bucket.count += 1;
36
- return { allowed: true, retryAfterSeconds: 0 };
32
+ catch (_a) {
33
+ throw new common_1.HttpException({
34
+ code: 'ACCESSI_RATE_LIMIT_UNAVAILABLE',
35
+ message: 'Rate limit non disponibile. Riprova piu tardi.',
36
+ }, common_1.HttpStatus.SERVICE_UNAVAILABLE);
37
+ }
38
+ });
37
39
  }
38
- /** Svuota i bucket (usato nei test). */
40
+ /** Svuota i bucket dello store di default (usato nei test). */
39
41
  function resetServiceTokenRateLimit() {
40
- buckets.clear();
42
+ (0, rateLimitStore_1.getDefaultRateLimitStore)().reset();
41
43
  }
42
44
  /** Lancia 429 con `Retry-After` quando il limite e superato. */
43
- function assertServiceTokenRateLimit(identifier) {
44
- const decision = checkServiceTokenRateLimit(identifier);
45
- if (!decision.allowed) {
46
- throw new common_1.HttpException({ code: 'ACCESSI_SERVICE_TOKEN_RATE_LIMITED', message: 'Troppi tentativi. Riprova piu tardi.' }, common_1.HttpStatus.TOO_MANY_REQUESTS);
47
- }
45
+ function assertServiceTokenRateLimit(identifier, store) {
46
+ return __awaiter(this, void 0, void 0, function* () {
47
+ const decision = yield checkServiceTokenRateLimit(identifier, store);
48
+ if (!decision.allowed) {
49
+ throw new common_1.HttpException({ code: 'ACCESSI_SERVICE_TOKEN_RATE_LIMITED', message: 'Troppi tentativi. Riprova piu tardi.' }, common_1.HttpStatus.TOO_MANY_REQUESTS);
50
+ }
51
+ });
48
52
  }
49
53
  //# sourceMappingURL=serviceTokenRateLimit.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"serviceTokenRateLimit.js","sourceRoot":"","sources":["../../../src/accessi-module/security/serviceTokenRateLimit.ts"],"names":[],"mappings":";;AAgBA,gEAsBC;AAGD,gEAEC;AAGD,kEAQC;AAtDD,2CAA2D;AAI3D;;;;;;GAMG;AACH,MAAM,SAAS,GAAG,KAAM,CAAC;AACzB,MAAM,YAAY,GAAG,GAAG,CAAC;AACzB,MAAM,WAAW,GAAG,KAAM,CAAC;AAC3B,MAAM,OAAO,GAAG,IAAI,GAAG,EAA2B,CAAC;AAEnD,SAAgB,0BAA0B,CAAC,UAAkB;IAC3D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,MAAM,GAAG,GAAG,UAAU,IAAI,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC;IAC5E,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAEhC,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,OAAO,IAAI,GAAG,EAAE,CAAC;QACrC,IAAI,OAAO,CAAC,IAAI,IAAI,WAAW,EAAE,CAAC;YAChC,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC;YAC9C,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;gBAC5B,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;YAC5B,CAAC;QACH,CAAC;QACD,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,GAAG,GAAG,SAAS,EAAE,CAAC,CAAC;QACzD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,iBAAiB,EAAE,CAAC,EAAE,CAAC;IACjD,CAAC;IAED,IAAI,MAAM,CAAC,KAAK,IAAI,YAAY,EAAE,CAAC;QACjC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,iBAAiB,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC,EAAE,CAAC;IACtG,CAAC;IAED,MAAM,CAAC,KAAK,IAAI,CAAC,CAAC;IAClB,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,iBAAiB,EAAE,CAAC,EAAE,CAAC;AACjD,CAAC;AAED,wCAAwC;AACxC,SAAgB,0BAA0B;IACxC,OAAO,CAAC,KAAK,EAAE,CAAC;AAClB,CAAC;AAED,gEAAgE;AAChE,SAAgB,2BAA2B,CAAC,UAAkB;IAC5D,MAAM,QAAQ,GAAG,0BAA0B,CAAC,UAAU,CAAC,CAAC;IACxD,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;QACtB,MAAM,IAAI,sBAAa,CACrB,EAAE,IAAI,EAAE,oCAAoC,EAAE,OAAO,EAAE,sCAAsC,EAAE,EAC/F,mBAAU,CAAC,iBAAiB,CAC7B,CAAC;IACJ,CAAC;AACH,CAAC","sourcesContent":["import { HttpException, HttpStatus } from '@nestjs/common';\n\ntype RateLimitBucket = { count: number; resetAt: number };\n\n/**\n * Limite anti-abuso per la verifica dei token di servizio.\n *\n * Il segreto ha 256 bit di entropia (brute force non praticabile), ma il limite evita\n * che un client difettoso o malevolo martelli l'endpoint. In-memory: in cluster va\n * aggiunto un limite anche al gateway.\n */\nconst WINDOW_MS = 60_000;\nconst MAX_ATTEMPTS = 120;\nconst MAX_BUCKETS = 20_000;\nconst buckets = new Map<string, RateLimitBucket>();\n\nexport function checkServiceTokenRateLimit(identifier: string): { allowed: boolean; retryAfterSeconds: number } {\n const now = Date.now();\n const key = identifier && identifier.trim() !== '' ? identifier : 'unknown';\n const bucket = buckets.get(key);\n\n if (!bucket || bucket.resetAt <= now) {\n if (buckets.size >= MAX_BUCKETS) {\n const oldestKey = buckets.keys().next().value;\n if (oldestKey !== undefined) {\n buckets.delete(oldestKey);\n }\n }\n buckets.set(key, { count: 1, resetAt: now + WINDOW_MS });\n return { allowed: true, retryAfterSeconds: 0 };\n }\n\n if (bucket.count >= MAX_ATTEMPTS) {\n return { allowed: false, retryAfterSeconds: Math.max(1, Math.ceil((bucket.resetAt - now) / 1000)) };\n }\n\n bucket.count += 1;\n return { allowed: true, retryAfterSeconds: 0 };\n}\n\n/** Svuota i bucket (usato nei test). */\nexport function resetServiceTokenRateLimit(): void {\n buckets.clear();\n}\n\n/** Lancia 429 con `Retry-After` quando il limite e superato. */\nexport function assertServiceTokenRateLimit(identifier: string): void {\n const decision = checkServiceTokenRateLimit(identifier);\n if (!decision.allowed) {\n throw new HttpException(\n { code: 'ACCESSI_SERVICE_TOKEN_RATE_LIMITED', message: 'Troppi tentativi. Riprova piu tardi.' },\n HttpStatus.TOO_MANY_REQUESTS,\n );\n }\n}\n"]}
1
+ {"version":3,"file":"serviceTokenRateLimit.js","sourceRoot":"","sources":["../../../src/accessi-module/security/serviceTokenRateLimit.ts"],"names":[],"mappings":";;;;;;;;;;;AAcA,gEAgBC;AAGD,gEAEC;AAGD,kEAWC;AAjDD,2CAA2D;AAC3D,qDAA4D;AAG5D;;;;;;GAMG;AACH,MAAM,SAAS,GAAG,KAAM,CAAC;AACzB,MAAM,YAAY,GAAG,GAAG,CAAC;AAEzB,SAAsB,0BAA0B;yDAC9C,UAAkB,EAClB,QAAwB,IAAA,yCAAwB,GAAE;QAElD,MAAM,GAAG,GAAG,UAAU,IAAI,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;QACnF,IAAI,CAAC;YACH,OAAO,MAAM,KAAK,CAAC,OAAO,CAAC,iBAAiB,GAAG,EAAE,EAAE,SAAS,EAAE,YAAY,CAAC,CAAC;QAC9E,CAAC;QAAC,WAAM,CAAC;YACP,MAAM,IAAI,sBAAa,CACrB;gBACE,IAAI,EAAE,gCAAgC;gBACtC,OAAO,EAAE,gDAAgD;aAC1D,EACD,mBAAU,CAAC,mBAAmB,CAC/B,CAAC;QACJ,CAAC;IACH,CAAC;CAAA;AAED,+DAA+D;AAC/D,SAAgB,0BAA0B;IACxC,IAAA,yCAAwB,GAAE,CAAC,KAAK,EAAE,CAAC;AACrC,CAAC;AAED,gEAAgE;AAChE,SAAsB,2BAA2B,CAC/C,UAAkB,EAClB,KAAsB;;QAEtB,MAAM,QAAQ,GAAG,MAAM,0BAA0B,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;QACrE,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;YACtB,MAAM,IAAI,sBAAa,CACrB,EAAE,IAAI,EAAE,oCAAoC,EAAE,OAAO,EAAE,sCAAsC,EAAE,EAC/F,mBAAU,CAAC,iBAAiB,CAC7B,CAAC;QACJ,CAAC;IACH,CAAC;CAAA","sourcesContent":["import { HttpException, HttpStatus } from '@nestjs/common';\nimport { getDefaultRateLimitStore } from './rateLimitStore';\nimport type { RateLimitStore } from './rateLimitStore';\n\n/**\n * Limite anti-abuso per la verifica dei token di servizio.\n *\n * Il segreto ha 256 bit di entropia (brute force non praticabile), ma il limite evita\n * che un client difettoso o malevolo martelli l'endpoint. Con `serviceTokens.rateLimitStore`\n * e possibile usare uno store condiviso in cluster.\n */\nconst WINDOW_MS = 60_000;\nconst MAX_ATTEMPTS = 120;\n\nexport async function checkServiceTokenRateLimit(\n identifier: string,\n store: RateLimitStore = getDefaultRateLimitStore(),\n): Promise<{ allowed: boolean; retryAfterSeconds: number }> {\n const key = identifier && identifier.trim() !== '' ? identifier.trim() : 'unknown';\n try {\n return await store.consume(`service-token|${key}`, WINDOW_MS, MAX_ATTEMPTS);\n } catch {\n throw new HttpException(\n {\n code: 'ACCESSI_RATE_LIMIT_UNAVAILABLE',\n message: 'Rate limit non disponibile. Riprova piu tardi.',\n },\n HttpStatus.SERVICE_UNAVAILABLE,\n );\n }\n}\n\n/** Svuota i bucket dello store di default (usato nei test). */\nexport function resetServiceTokenRateLimit(): void {\n getDefaultRateLimitStore().reset();\n}\n\n/** Lancia 429 con `Retry-After` quando il limite e superato. */\nexport async function assertServiceTokenRateLimit(\n identifier: string,\n store?: RateLimitStore,\n): Promise<void> {\n const decision = await checkServiceTokenRateLimit(identifier, store);\n if (!decision.allowed) {\n throw new HttpException(\n { code: 'ACCESSI_SERVICE_TOKEN_RATE_LIMITED', message: 'Troppi tentativi. Riprova piu tardi.' },\n HttpStatus.TOO_MANY_REQUESTS,\n );\n }\n}\n"]}
@@ -24,6 +24,8 @@ const accessiRuntime_1 = require("./accessi-module/accessiRuntime");
24
24
  const startupLog_1 = require("./accessi-module/startupLog");
25
25
  const accessiValidation_1 = require("./accessi-module/security/accessiValidation");
26
26
  const AccessiHttpExceptionFilter_1 = require("./accessi-module/security/AccessiHttpExceptionFilter");
27
+ const jwks_1 = require("./accessi-module/security/jwks");
28
+ const edgeRateLimit_1 = require("./accessi-module/security/edgeRateLimit");
27
29
  /**
28
30
  * Initializes the unified module that combines Accessi and Allegati functionality
29
31
  * @param app Express application instance
@@ -32,7 +34,7 @@ const AccessiHttpExceptionFilter_1 = require("./accessi-module/security/AccessiH
32
34
  */
33
35
  function initEmilsoftwareModule(app, options) {
34
36
  return __awaiter(this, void 0, void 0, function* () {
35
- var _a;
37
+ var _a, _b, _c;
36
38
  const logger = new Logger_1.Logger(initEmilsoftwareModule.name);
37
39
  if (options === null || options === void 0 ? void 0 : options.accessiOptions) {
38
40
  (0, authenticateGen_1.beginAccessiAuthInitialization)();
@@ -46,15 +48,31 @@ function initEmilsoftwareModule(app, options) {
46
48
  // Precedenza: EmilsoftwareOptions.openapi > AccessiOptions.openapi > default.
47
49
  const resolvedOpenApi = (0, OpenApiConfig_1.resolveAccessiOpenApiOptions)((_a = options === null || options === void 0 ? void 0 : options.accessiOptions) === null || _a === void 0 ? void 0 : _a.openapi, options === null || options === void 0 ? void 0 : options.openapi);
48
50
  const nestHostApp = (0, express_1.default)();
51
+ const jwksPath = (options === null || options === void 0 ? void 0 : options.accessiOptions)
52
+ ? (0, jwks_1.getAccessiJwksPath)(options.accessiOptions)
53
+ : undefined;
49
54
  const isNestRoute = (path) => path === "/api" ||
50
55
  path.startsWith("/api/") ||
51
- (0, OpenApiConfig_1.isOpenApiRequestPath)(path, resolvedOpenApi);
56
+ (0, OpenApiConfig_1.isOpenApiRequestPath)(path, resolvedOpenApi) ||
57
+ (jwksPath !== undefined && path === jwksPath);
58
+ const edgeRateLimit = ((_b = options === null || options === void 0 ? void 0 : options.accessiOptions) === null || _b === void 0 ? void 0 : _b.edgeRateLimit)
59
+ ? (0, edgeRateLimit_1.createAccessiEdgeRateLimitMiddleware)(Object.assign(Object.assign({}, options.accessiOptions.edgeRateLimit), { match: (_c = options.accessiOptions.edgeRateLimit.match) !== null && _c !== void 0 ? _c : ((path) => isNestRoute(path)) }))
60
+ : undefined;
61
+ if (edgeRateLimit) {
62
+ app.use(edgeRateLimit);
63
+ }
52
64
  app.use((req, res, next) => {
53
65
  if (!isNestRoute(req.path)) {
54
66
  return next();
55
67
  }
56
68
  return nestHostApp(req, res, next);
57
69
  });
70
+ if (options === null || options === void 0 ? void 0 : options.accessiOptions) {
71
+ (0, jwks_1.setupAccessiJwks)(app, options.accessiOptions);
72
+ if (jwksPath) {
73
+ logger.info(`JWKS accessi disponibile su: ${jwksPath}`);
74
+ }
75
+ }
58
76
  const nestExpressInstance = new platform_express_1.ExpressAdapter(nestHostApp);
59
77
  const nestApp = yield core_1.NestFactory.create(EmilsoftwareModule_1.EmilsoftwareModule.forRoot(options), nestExpressInstance, Object.assign({ bufferLogs: (options === null || options === void 0 ? void 0 : options.bufferLogs) !== false }, ((options === null || options === void 0 ? void 0 : options.logger) ? { logger: options.logger } : {})));
60
78
  nestApp.enableCors();
@@ -1 +1 @@
1
- {"version":3,"file":"initEmilsoftwareModule.js","sourceRoot":"","sources":["../src/initEmilsoftwareModule.ts"],"names":[],"mappings":"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","sourcesContent":["import express, { Application, Request } from \"express\";\nimport { NestFactory } from \"@nestjs/core\";\nimport { ExpressAdapter } from \"@nestjs/platform-express\";\nimport type { INestApplication } from \"@nestjs/common\";\nimport type { OpenAPIObject } from \"@nestjs/swagger\";\nimport {\n buildOpenApiExcludePaths,\n isOpenApiRequestPath,\n resolveAccessiOpenApiOptions,\n setupOpenApi\n} from \"./openapi/OpenApiConfig\";\nimport { Logger } from \"./Logger\";\nimport { EmilsoftwareModule, EmilsoftwareOptions } from \"./EmilsoftwareModule\";\nimport {\n beginAccessiAuthInitialization,\n failAccessiAuthInitialization,\n setAccessiAuthService\n} from \"./accessi-module/middleware/authenticateGen\";\nimport { registerAccessiModuleServices } from \"./accessi-module/accessiRuntime\";\nimport { logAccessiStartup } from \"./accessi-module/startupLog\";\nimport { createAccessiValidationPipe } from './accessi-module/security/accessiValidation';\nimport { AccessiHttpExceptionFilter } from './accessi-module/security/AccessiHttpExceptionFilter';\n\n/** Esito del bootstrap del modulo unificato. */\nexport interface EmilsoftwareModuleInitializationResult {\n /** Sotto-app Nest che serve Accessi, Allegati e la documentazione. */\n nestApp: INestApplication;\n /** Documento OpenAPI generato, mergiabile nel documento unico dell'host. */\n document: OpenAPIObject;\n}\n\n/**\n * Initializes the unified module that combines Accessi and Allegati functionality\n * @param app Express application instance\n * @param options Configuration options for both modules\n * @returns Il sotto-app Nest e il documento OpenAPI generato.\n */\nexport async function initEmilsoftwareModule(\n app: Application,\n options: EmilsoftwareOptions\n): Promise<EmilsoftwareModuleInitializationResult> {\n const logger: Logger = new Logger(initEmilsoftwareModule.name);\n if (options?.accessiOptions) {\n beginAccessiAuthInitialization();\n }\n\n try {\n // Isolate Nest on a dedicated Express instance and dispatch only module routes.\n // This keeps pre-existing routes/middlewares on the host app untouched.\n // Prefisso OpenAPI configurabile (default `accessi/openapi`), cosi' piu'\n // documentazioni possono convivere (backend su `/openapi`, moduli sotto\n // `/<modulo>/openapi`).\n // Precedenza: EmilsoftwareOptions.openapi > AccessiOptions.openapi > default.\n const resolvedOpenApi = resolveAccessiOpenApiOptions(\n options?.accessiOptions?.openapi,\n options?.openapi\n );\n\n const nestHostApp = express();\n const isNestRoute = (path: string) =>\n path === \"/api\" ||\n path.startsWith(\"/api/\") ||\n isOpenApiRequestPath(path, resolvedOpenApi);\n\n app.use((req: Request, res, next) => {\n if (!isNestRoute(req.path)) {\n return next();\n }\n return nestHostApp(req, res, next);\n });\n\n const nestExpressInstance = new ExpressAdapter(nestHostApp);\n const nestApp = await NestFactory.create(EmilsoftwareModule.forRoot(options), nestExpressInstance, {\n bufferLogs: options?.bufferLogs !== false,\n ...(options?.logger ? { logger: options.logger } : {})\n });\n\n nestApp.enableCors();\n if (options?.accessiOptions) {\n // Keep the Accessi API contract identical for standalone and unified bootstraps.\n nestApp.useGlobalPipes(createAccessiValidationPipe());\n nestApp.useGlobalFilters(new AccessiHttpExceptionFilter());\n }\n\n nestApp.setGlobalPrefix('api', {\n exclude: buildOpenApiExcludePaths(resolvedOpenApi)\n });\n\n // Documentazione OpenAPI del modulo unificato (Accessi + Allegati).\n const document = setupOpenApi(nestApp, resolvedOpenApi);\n\n await nestApp.init();\n if (options?.accessiOptions) {\n const accessiServices = registerAccessiModuleServices(app, nestApp);\n app.locals.accessiAuthService = accessiServices.authenticateGenService;\n setAccessiAuthService(accessiServices.authenticateGenService);\n logAccessiStartup(options.accessiOptions, resolvedOpenApi);\n }\n\n logger.info(\"Emilsoftware module initialized successfully\");\n\n return { nestApp, document };\n } catch (error) {\n if (options?.accessiOptions) {\n failAccessiAuthInitialization(error);\n }\n logger.error(\"Error initializing Emilsoftware Module:\", error);\n throw error;\n }\n}\n"]}
1
+ {"version":3,"file":"initEmilsoftwareModule.js","sourceRoot":"","sources":["../src/initEmilsoftwareModule.ts"],"names":[],"mappings":"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","sourcesContent":["import express, { Application, Request } from \"express\";\nimport { NestFactory } from \"@nestjs/core\";\nimport { ExpressAdapter } from \"@nestjs/platform-express\";\nimport type { INestApplication } from \"@nestjs/common\";\nimport type { OpenAPIObject } from \"@nestjs/swagger\";\nimport {\n buildOpenApiExcludePaths,\n isOpenApiRequestPath,\n resolveAccessiOpenApiOptions,\n setupOpenApi\n} from \"./openapi/OpenApiConfig\";\nimport { Logger } from \"./Logger\";\nimport { EmilsoftwareModule, EmilsoftwareOptions } from \"./EmilsoftwareModule\";\nimport {\n beginAccessiAuthInitialization,\n failAccessiAuthInitialization,\n setAccessiAuthService\n} from \"./accessi-module/middleware/authenticateGen\";\nimport { registerAccessiModuleServices } from \"./accessi-module/accessiRuntime\";\nimport { logAccessiStartup } from \"./accessi-module/startupLog\";\nimport { createAccessiValidationPipe } from './accessi-module/security/accessiValidation';\nimport { AccessiHttpExceptionFilter } from './accessi-module/security/AccessiHttpExceptionFilter';\nimport { getAccessiJwksPath, setupAccessiJwks } from './accessi-module/security/jwks';\nimport { createAccessiEdgeRateLimitMiddleware } from './accessi-module/security/edgeRateLimit';\n\n/** Esito del bootstrap del modulo unificato. */\nexport interface EmilsoftwareModuleInitializationResult {\n /** Sotto-app Nest che serve Accessi, Allegati e la documentazione. */\n nestApp: INestApplication;\n /** Documento OpenAPI generato, mergiabile nel documento unico dell'host. */\n document: OpenAPIObject;\n}\n\n/**\n * Initializes the unified module that combines Accessi and Allegati functionality\n * @param app Express application instance\n * @param options Configuration options for both modules\n * @returns Il sotto-app Nest e il documento OpenAPI generato.\n */\nexport async function initEmilsoftwareModule(\n app: Application,\n options: EmilsoftwareOptions\n): Promise<EmilsoftwareModuleInitializationResult> {\n const logger: Logger = new Logger(initEmilsoftwareModule.name);\n if (options?.accessiOptions) {\n beginAccessiAuthInitialization();\n }\n\n try {\n // Isolate Nest on a dedicated Express instance and dispatch only module routes.\n // This keeps pre-existing routes/middlewares on the host app untouched.\n // Prefisso OpenAPI configurabile (default `accessi/openapi`), cosi' piu'\n // documentazioni possono convivere (backend su `/openapi`, moduli sotto\n // `/<modulo>/openapi`).\n // Precedenza: EmilsoftwareOptions.openapi > AccessiOptions.openapi > default.\n const resolvedOpenApi = resolveAccessiOpenApiOptions(\n options?.accessiOptions?.openapi,\n options?.openapi\n );\n\n const nestHostApp = express();\n const jwksPath = options?.accessiOptions\n ? getAccessiJwksPath(options.accessiOptions)\n : undefined;\n const isNestRoute = (path: string) =>\n path === \"/api\" ||\n path.startsWith(\"/api/\") ||\n isOpenApiRequestPath(path, resolvedOpenApi) ||\n (jwksPath !== undefined && path === jwksPath);\n const edgeRateLimit = options?.accessiOptions?.edgeRateLimit\n ? createAccessiEdgeRateLimitMiddleware({\n ...options.accessiOptions.edgeRateLimit,\n match: options.accessiOptions.edgeRateLimit.match ?? ((path) => isNestRoute(path)),\n })\n : undefined;\n if (edgeRateLimit) {\n app.use(edgeRateLimit);\n }\n\n app.use((req: Request, res, next) => {\n if (!isNestRoute(req.path)) {\n return next();\n }\n return nestHostApp(req, res, next);\n });\n\n if (options?.accessiOptions) {\n setupAccessiJwks(app, options.accessiOptions);\n if (jwksPath) {\n logger.info(`JWKS accessi disponibile su: ${jwksPath}`);\n }\n }\n\n const nestExpressInstance = new ExpressAdapter(nestHostApp);\n const nestApp = await NestFactory.create(EmilsoftwareModule.forRoot(options), nestExpressInstance, {\n bufferLogs: options?.bufferLogs !== false,\n ...(options?.logger ? { logger: options.logger } : {})\n });\n\n nestApp.enableCors();\n if (options?.accessiOptions) {\n // Keep the Accessi API contract identical for standalone and unified bootstraps.\n nestApp.useGlobalPipes(createAccessiValidationPipe());\n nestApp.useGlobalFilters(new AccessiHttpExceptionFilter());\n }\n\n nestApp.setGlobalPrefix('api', {\n exclude: buildOpenApiExcludePaths(resolvedOpenApi)\n });\n\n // Documentazione OpenAPI del modulo unificato (Accessi + Allegati).\n const document = setupOpenApi(nestApp, resolvedOpenApi);\n\n await nestApp.init();\n if (options?.accessiOptions) {\n const accessiServices = registerAccessiModuleServices(app, nestApp);\n app.locals.accessiAuthService = accessiServices.authenticateGenService;\n setAccessiAuthService(accessiServices.authenticateGenService);\n logAccessiStartup(options.accessiOptions, resolvedOpenApi);\n }\n\n logger.info(\"Emilsoftware module initialized successfully\");\n\n return { nestApp, document };\n } catch (error) {\n if (options?.accessiOptions) {\n failAccessiAuthInitialization(error);\n }\n logger.error(\"Error initializing Emilsoftware Module:\", error);\n throw error;\n }\n}\n"]}
@@ -0,0 +1,98 @@
1
+ # Gateway rate limit per Accessi Service
2
+
3
+ Il rate limit applicativo di Accessi protegge login, 2FA, reset password, console entry e service token. Il gateway/API gateway resta utile per una protezione per IP, connessioni e bot prima che la richiesta raggiunga Node.
4
+
5
+ Regole consigliate:
6
+
7
+ - `/api/accessi/auth/*`: soglia bassa, burst contenuto
8
+ - `/api/accessi/console/entry/exchange`: soglia bassa
9
+ - `/api/accessi/*`: soglia media
10
+ - `/accessi/openapi*` e `/accessi/.well-known/jwks.json`: soglia alta o cache CDN
11
+
12
+ ## Nginx
13
+
14
+ ```nginx
15
+ limit_req_zone $binary_remote_addr zone=accessi_auth:10m rate=2r/s;
16
+ limit_req_zone $binary_remote_addr zone=accessi_api:10m rate=10r/s;
17
+ limit_conn_zone $binary_remote_addr zone=accessi_conn:10m;
18
+
19
+ server {
20
+ # Se Accessi e dietro un proxy/load balancer, configura real_ip
21
+ # prima di usare $binary_remote_addr.
22
+
23
+ location /api/accessi/auth/ {
24
+ limit_req zone=accessi_auth burst=10 nodelay;
25
+ limit_conn accessi_conn 20;
26
+ proxy_pass http://accessi_service;
27
+ }
28
+
29
+ location /api/accessi/ {
30
+ limit_req zone=accessi_api burst=50 nodelay;
31
+ limit_conn accessi_conn 20;
32
+ proxy_pass http://accessi_service;
33
+ }
34
+
35
+ location /accessi/ {
36
+ limit_req zone=accessi_api burst=50 nodelay;
37
+ proxy_pass http://accessi_service;
38
+ }
39
+ }
40
+ ```
41
+
42
+ ## Traefik
43
+
44
+ ```yaml
45
+ http:
46
+ middlewares:
47
+ accessi-auth-ratelimit:
48
+ rateLimit:
49
+ average: 2
50
+ burst: 10
51
+ period: 1s
52
+ accessi-api-ratelimit:
53
+ rateLimit:
54
+ average: 10
55
+ burst: 50
56
+ period: 1s
57
+ routers:
58
+ accessi-auth:
59
+ rule: PathPrefix(`/api/accessi/auth/`)
60
+ middlewares: [accessi-auth-ratelimit]
61
+ service: accessi
62
+ accessi-api:
63
+ rule: PathPrefix(`/api/accessi/`) || PathPrefix(`/accessi/`)
64
+ middlewares: [accessi-api-ratelimit]
65
+ service: accessi
66
+ ```
67
+
68
+ ## Cloudflare
69
+
70
+ Regole di rate limiting:
71
+
72
+ - path `/api/accessi/auth/*`: 10 richieste/minuto per IP, azione `managed challenge`
73
+ - path `/api/accessi/console/entry/exchange`: 30 richieste/5 minuti per IP
74
+ - path `/api/accessi/*`: 300 richieste/minuto per IP, azione `block`
75
+ - path `/accessi/.well-known/jwks.json`: cache CDN, limite alto
76
+
77
+ Aggiungere bot fight mode/WAF secondo il piano disponibile.
78
+
79
+ ## AWS WAF / API Gateway
80
+
81
+ - rate-based rule: 300 richieste/5 minuti per IP
82
+ - scope-down statement: `URI starts with /api/accessi`
83
+ - regola più severa su `/api/accessi/auth/*`
84
+ - AWS Shield/WAF Bot Control se disponibile
85
+
86
+ ## Rapporto con `edgeRateLimit` della libreria
87
+
88
+ `edgeRateLimit` e un ultimo livello applicativo:
89
+
90
+ - usa lo stesso `RateLimitStore` Redis dei rate limit per account;
91
+ - copre Accessi anche se il gateway non ha regole specifiche;
92
+ - non sostituisce WAF, bot protection, limiti di connessione o protezione DDoS.
93
+
94
+ In produzione usare entrambi:
95
+
96
+ ```text
97
+ Client -> CDN/WAF -> API gateway -> edgeRateLimit Accessi -> rate limit per account
98
+ ```
@@ -150,6 +150,127 @@ const merged = mergeOpenApiDocuments(hostDocument, accessiDocument, {
150
150
  });
151
151
  ```
152
152
 
153
+ ### JWKS e rotazione chiavi
154
+
155
+ Con RS256/ES256 Accessi pubblica automaticamente:
156
+
157
+ ```text
158
+ GET /accessi/.well-known/jwks.json
159
+ ```
160
+
161
+ Configurazione:
162
+
163
+ ```ts
164
+ jwtOptions: {
165
+ algorithm: "RS256",
166
+ privateKey: process.env.ACCESSI_JWT_PRIVATE_KEY,
167
+ publicKey: process.env.ACCESSI_JWT_PUBLIC_KEY,
168
+ keyId: "accessi-2026-02",
169
+ jwksPath: "accessi/.well-known/jwks.json",
170
+ previousPublicKeys: [
171
+ {
172
+ publicKey: process.env.ACCESSI_JWT_PUBLIC_KEY_2026_01!,
173
+ keyId: "accessi-2026-01",
174
+ algorithm: "RS256",
175
+ },
176
+ ],
177
+ }
178
+ ```
179
+
180
+ Rotazione:
181
+
182
+ 1. generare la nuova coppia
183
+ 2. pubblicare la nuova chiave mantenendo la precedente in `previousPublicKeys`
184
+ 3. firmare con la nuova `keyId`
185
+ 4. attendere la scadenza massima dei token + il TTL cache della JWKS (default 5 minuti)
186
+ 5. rimuovere la chiave precedente
187
+
188
+ Per un'app Nest esistente si può usare `setupAccessiJwks(app.getHttpAdapter().getInstance(), options)`.
189
+
190
+ ### Store ticket console
191
+
192
+ Il default è in memoria, adatto a una sola istanza. In multi-istanza configurare uno store condiviso:
193
+
194
+ ```ts
195
+ import { createRedisConsoleEntryStore } from "emilsoftware-utilities/accessi/server";
196
+
197
+ consoleEntry: {
198
+ ttlMs: 60_000,
199
+ store: createRedisConsoleEntryStore({
200
+ client: redis, // ioredis o client compatibile con eval(script, numKeys, ...args)
201
+ encryptionKey: process.env.ACCESSI_CONSOLE_ENTRY_KEY!,
202
+ keyPrefix: "accessi:console-entry:",
203
+ }),
204
+ }
205
+ ```
206
+
207
+ Lo store Redis:
208
+
209
+ - cifra il JWT con AES-256-GCM
210
+ - usa `SET ... PX ... NX` con TTL
211
+ - consuma con `GET` + `DEL` atomici in Lua
212
+ - rende il ticket monouso anche tra istanze diverse
213
+
214
+ Se lo store condiviso non è raggiungibile, la console deve fallire chiusa.
215
+
216
+ ### Rate limit condiviso
217
+
218
+ Con più istanze anche i rate limit devono usare uno store condiviso:
219
+
220
+ ```ts
221
+ import { createRedisRateLimitStore } from "emilsoftware-utilities/accessi/server";
222
+
223
+ const rateLimitStore = createRedisRateLimitStore({
224
+ client: redis, // ioredis o client compatibile con eval(script, numKeys, ...args)
225
+ keyPrefix: "accessi:rate:",
226
+ });
227
+
228
+ publicAuthRateLimit: {
229
+ enabled: true,
230
+ store: rateLimitStore,
231
+ login: { windowMs: 15 * 60_000, maxAttempts: 10 },
232
+ twoFactorVerify: { windowMs: 15 * 60_000, maxAttempts: 20 },
233
+ },
234
+
235
+ serviceTokens: {
236
+ enabled: true,
237
+ rateLimitStore: rateLimitStore,
238
+ },
239
+ ```
240
+
241
+ Lo store Redis usa `INCR` + `PEXPIRE` atomici:
242
+
243
+ - conta tentativi per IP e per soggetto (email, challenge, ticket, tokenId);
244
+ - il limite è globale tra le istanze;
245
+ - non viene azzerato da un restart;
246
+ - se Redis non è raggiungibile il controllo fallisce chiuso: l’endpoint risponde con errore, non autorizza.
247
+
248
+ Default: store in memoria. Va bene per una singola istanza o per ambienti di sviluppo.
249
+
250
+ ### Edge rate limit per IP
251
+
252
+ Per una soglia grossolana davanti alle rotte Accessi:
253
+
254
+ ```ts
255
+ edgeRateLimit: {
256
+ enabled: true,
257
+ windowMs: 60_000,
258
+ maxAttempts: 300,
259
+ store: rateLimitStore, // stesso store Redis, opzionale
260
+ }
261
+ ```
262
+
263
+ 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`.
264
+
265
+ Il gateway/API gateway resta consigliato per:
266
+
267
+ - limiti di connessione e burst;
268
+ - bot protection e WAF;
269
+ - rate limit per IP a livello di rete;
270
+ - blocco geografico o reputazionale.
271
+
272
+ Configurazioni di riferimento: `docs/gateway-rate-limit.md`.
273
+
153
274
  ---
154
275
 
155
276
  ## 4. Backend fratello — Express
@@ -190,7 +311,9 @@ Dopo il middleware:
190
311
  - **HS256**: usare `jwt.serverSecret` con `ACCESSI_JWT_SECRET`. Il resource server deriva la chiave access con la stessa KDF del server.
191
312
  - **RS256/ES256**: usare `jwt.publicKey` PEM oppure `jwt.jwks.url`. Consigliato in multi-servizio.
192
313
  - 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.
314
+ - Accessi espone la JWKS su `jwtOptions.jwksPath` (default `accessi/.well-known/jwks.json`) quando usa RS/ES.
315
+ - La JWKS include la chiave corrente e le `previousPublicKeys`, per la rotazione senza downtime.
316
+ - Configurare il resource server con `jwks: { url: "https://accessi.example.com/accessi/.well-known/jwks.json" }`.
194
317
 
195
318
  ### Errori
196
319
 
@@ -404,6 +527,9 @@ ACCESSI_JWT_ALGORITHM=RS256
404
527
  ACCESSI_JWT_PRIVATE_KEY=
405
528
  ACCESSI_JWT_PUBLIC_KEY=
406
529
  ACCESSI_JWT_KEY_ID=
530
+ ACCESSI_JWKS_PATH=accessi/.well-known/jwks.json
531
+ ACCESSI_CONSOLE_ENTRY_TTL_MS=60000
532
+ ACCESSI_CONSOLE_ENTRY_KEY=
407
533
  SMTP_HOST=
408
534
  SMTP_PORT=587
409
535
  SMTP_USER=
@@ -428,6 +554,7 @@ Frontend:
428
554
  ```env
429
555
  ACCESSI_API_BASE_URL=https://accessi.example.com
430
556
  ACCESSI_CONSOLE_URL=https://accessi.example.com/api/accessi/console
557
+ ACCESSI_JWKS_URL=https://accessi.example.com/accessi/.well-known/jwks.json
431
558
  ```
432
559
 
433
560
  ---
@@ -443,16 +570,20 @@ Checklist:
443
570
  5. Spostare la console una sola volta.
444
571
  6. Passare i backend fratelli a `resource-server`.
445
572
  7. Configurare un service token con `accessi:introspect`.
446
- 8. Testare:
573
+ 8. Pubblicare la JWKS e configurare i resource server su `jwks.url`.
574
+ 9. In multi-istanza configurare `consoleEntry.store`, `publicAuthRateLimit.store` e `serviceTokens.rateLimitStore` con Redis.
575
+ 10. Testare:
447
576
  - login
448
577
  - token valido
449
578
  - token scaduto
450
579
  - utente disabilitato
451
580
  - revoca grants
452
581
  - 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.
582
+ - console entry ticket su istanze diverse
583
+ - rate limit condiviso tra istanze
584
+ - rotazione JWKS con vecchia chiave ancora valida
585
+ 11. Aggiornare CORS se i frontend cambiano dominio.
586
+ 12. Aggiornare monitoraggio e log senza includere token.
456
587
 
457
588
  ---
458
589
 
@@ -474,8 +605,8 @@ Checklist:
474
605
 
475
606
  ## 14. Limiti attuali da conoscere
476
607
 
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.
608
+ - 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.
609
+ - 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
610
  - 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
611
  - I DTO in `accessi/contracts/dtos` dipendono da Nest/class-validator: non sono pensati per bundle frontend minimali.
612
+ - 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.68",
4
4
  "description": "Utilities for EmilSoftware",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -101,8 +101,8 @@
101
101
  "typecheck": "tsc --noEmit",
102
102
  "lint": "node scripts/lint.cjs",
103
103
  "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",
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 test/accessi-security-standards.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 test/accessi-security-standards.cjs",
106
106
  "test:accessi": "npm run test:unit",
107
107
  "test:all": "npm test && npm run test:integration",
108
108
  "test:integration": "node test/integration/run-firebird.cjs 5",
@@ -183,7 +183,8 @@
183
183
  },
184
184
  "files": [
185
185
  "dist",
186
- "integration-prompt.md"
186
+ "integration-prompt.md",
187
+ "docs"
187
188
  ],
188
189
  "config": {
189
190
  "commitizen": {