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
@@ -26,7 +26,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
26
26
  return (mod && mod.__esModule) ? mod : { "default": mod };
27
27
  };
28
28
  Object.defineProperty(exports, "__esModule", { value: true });
29
- exports.ACCESSI_MODULE_SERVICES_LOCALS_KEY = exports.getAccessiModuleServices = exports.registerAccessiModuleServices = exports.accessiRequirement = exports.authenticateGen = exports.authorizeAccessi = exports.FederatedAuthService = exports.LOGIN_AS_QUERY_PARAM = exports.buildLoginAsUrl = exports.ACCESSI_EMAIL_NOT_CONFIGURED = exports.AccessiEmailNotConfiguredError = exports.isEmailConfigured = exports.assertEmailConfigured = exports.getAccessiServiceToken = exports.RequireServiceTokenScopes = exports.ServiceTokenGuard = exports.isValidAdminBootstrapToken = exports.clearAdminBootstrapToken = exports.getAdminBootstrapToken = exports.getOrCreateAdminBootstrapToken = exports.AdminBootstrapService = exports.SchemaExportService = exports.ServiceTokenService = exports.discoverCatalogScripts = exports.computeCatalogScriptChecksum = exports.leadingStatementKeyword = exports.analyzeCatalogScript = exports.splitSqlStatements = exports.listPendingCatalogScripts = exports.applyCatalogScripts = exports.AccessiCatalogMigrator = exports.AccessiDatabaseUpdater = exports.AccessiIntrospectionController = exports.AccessiModule = void 0;
29
+ exports.ACCESSI_MODULE_SERVICES_LOCALS_KEY = exports.getAccessiModuleServices = exports.registerAccessiModuleServices = exports.accessiRequirement = exports.authenticateGen = exports.authorizeAccessi = exports.FederatedAuthService = exports.setupAccessiJwks = exports.getAccessiJwksPath = exports.createAccessiJwksHandler = exports.buildAccessiJwks = exports.LOGIN_AS_QUERY_PARAM = exports.buildLoginAsUrl = exports.ACCESSI_EMAIL_NOT_CONFIGURED = exports.AccessiEmailNotConfiguredError = exports.isEmailConfigured = exports.assertEmailConfigured = exports.getAccessiServiceToken = exports.RequireServiceTokenScopes = exports.ServiceTokenGuard = exports.isValidAdminBootstrapToken = exports.clearAdminBootstrapToken = exports.getAdminBootstrapToken = exports.getOrCreateAdminBootstrapToken = exports.AdminBootstrapService = exports.SchemaExportService = exports.ServiceTokenService = exports.discoverCatalogScripts = exports.computeCatalogScriptChecksum = exports.leadingStatementKeyword = exports.analyzeCatalogScript = exports.splitSqlStatements = exports.listPendingCatalogScripts = exports.applyCatalogScripts = exports.AccessiCatalogMigrator = exports.createAccessiEdgeRateLimitMiddleware = exports.getDefaultRateLimitStore = exports.createRedisRateLimitStore = exports.createMemoryRateLimitStore = exports.getDefaultConsoleEntryStore = exports.createRedisConsoleEntryStore = exports.createMemoryConsoleEntryStore = exports.AccessiDatabaseUpdater = exports.AccessiIntrospectionController = exports.AccessiModule = void 0;
30
30
  exports.initializeAccessiModule = initializeAccessiModule;
31
31
  const express_1 = __importDefault(require("express"));
32
32
  const core_1 = require("@nestjs/core");
@@ -41,6 +41,8 @@ const startupLog_1 = require("./startupLog");
41
41
  const accessiValidation_1 = require("./security/accessiValidation");
42
42
  const AccessiHttpExceptionFilter_1 = require("./security/AccessiHttpExceptionFilter");
43
43
  const emailConfiguration_1 = require("./security/emailConfiguration");
44
+ const jwks_1 = require("./security/jwks");
45
+ const edgeRateLimit_1 = require("./security/edgeRateLimit");
44
46
  function describeDatabaseTarget(options) {
45
47
  var _a, _b, _c, _d;
46
48
  const dbOptions = options.databaseOptions;
@@ -61,6 +63,7 @@ function describeDatabaseTarget(options) {
61
63
  */
62
64
  function initializeAccessiModule(app, options) {
63
65
  return __awaiter(this, void 0, void 0, function* () {
66
+ var _a;
64
67
  // Il servizio email e obbligatorio: reset password, 2FA e creazione utente ne dipendono.
65
68
  (0, emailConfiguration_1.assertEmailConfigured)(options);
66
69
  const logger = new Logger_1.Logger("initializeAccessiModule");
@@ -72,15 +75,27 @@ function initializeAccessiModule(app, options) {
72
75
  logger.info(`Target database accessi: ${describeDatabaseTarget(options)}`);
73
76
  logger.info(`Configurazione accessi: autoUpdateDatabase=${options.autoUpdateDatabase !== false}, legacyPasswordMigrationOnStartup=${options.legacyPasswordMigrationOnStartup !== false}`);
74
77
  const nestHostApp = (0, express_1.default)();
75
- const isNestRoute = (path) => path === "/api/accessi" ||
78
+ const jwksPath = (0, jwks_1.getAccessiJwksPath)(options);
79
+ const isAccessiRoute = (path) => path === "/api/accessi" ||
76
80
  path.startsWith("/api/accessi/") ||
77
- (0, OpenApiConfig_1.isOpenApiRequestPath)(path, resolvedOpenApi);
81
+ (0, OpenApiConfig_1.isOpenApiRequestPath)(path, resolvedOpenApi) ||
82
+ (jwksPath !== undefined && path === jwksPath);
83
+ const edgeRateLimit = options.edgeRateLimit
84
+ ? (0, edgeRateLimit_1.createAccessiEdgeRateLimitMiddleware)(Object.assign(Object.assign({}, options.edgeRateLimit), { match: (_a = options.edgeRateLimit.match) !== null && _a !== void 0 ? _a : ((path) => isAccessiRoute(path)) }))
85
+ : undefined;
86
+ if (edgeRateLimit) {
87
+ app.use(edgeRateLimit);
88
+ }
78
89
  app.use((req, res, next) => {
79
- if (!isNestRoute(req.path)) {
90
+ if (!isAccessiRoute(req.path)) {
80
91
  return next();
81
92
  }
82
93
  return nestHostApp(req, res, next);
83
94
  });
95
+ (0, jwks_1.setupAccessiJwks)(app, options);
96
+ if (jwksPath) {
97
+ logger.info(`JWKS accessi disponibile su: ${jwksPath}`);
98
+ }
84
99
  const nestExpressInstance = new platform_express_1.ExpressAdapter(nestHostApp);
85
100
  const nestApp = yield core_1.NestFactory.create(AccessiModule_1.AccessiModule.forRoot(options), nestExpressInstance, Object.assign({ bufferLogs: options.bufferLogs !== false }, (options.logger ? { logger: options.logger } : {})));
86
101
  logger.info("Applicazione Nest accessi creata.");
@@ -123,6 +138,16 @@ var AccessiIntrospectionController_1 = require("./Controllers/AccessiIntrospecti
123
138
  Object.defineProperty(exports, "AccessiIntrospectionController", { enumerable: true, get: function () { return AccessiIntrospectionController_1.AccessiIntrospectionController; } });
124
139
  var AccessiDatabaseUpdater_1 = require("./database-updates/AccessiDatabaseUpdater");
125
140
  Object.defineProperty(exports, "AccessiDatabaseUpdater", { enumerable: true, get: function () { return AccessiDatabaseUpdater_1.AccessiDatabaseUpdater; } });
141
+ var consoleEntry_1 = require("./security/consoleEntry");
142
+ Object.defineProperty(exports, "createMemoryConsoleEntryStore", { enumerable: true, get: function () { return consoleEntry_1.createMemoryConsoleEntryStore; } });
143
+ Object.defineProperty(exports, "createRedisConsoleEntryStore", { enumerable: true, get: function () { return consoleEntry_1.createRedisConsoleEntryStore; } });
144
+ Object.defineProperty(exports, "getDefaultConsoleEntryStore", { enumerable: true, get: function () { return consoleEntry_1.getDefaultConsoleEntryStore; } });
145
+ var rateLimitStore_1 = require("./security/rateLimitStore");
146
+ Object.defineProperty(exports, "createMemoryRateLimitStore", { enumerable: true, get: function () { return rateLimitStore_1.createMemoryRateLimitStore; } });
147
+ Object.defineProperty(exports, "createRedisRateLimitStore", { enumerable: true, get: function () { return rateLimitStore_1.createRedisRateLimitStore; } });
148
+ Object.defineProperty(exports, "getDefaultRateLimitStore", { enumerable: true, get: function () { return rateLimitStore_1.getDefaultRateLimitStore; } });
149
+ var edgeRateLimit_2 = require("./security/edgeRateLimit");
150
+ Object.defineProperty(exports, "createAccessiEdgeRateLimitMiddleware", { enumerable: true, get: function () { return edgeRateLimit_2.createAccessiEdgeRateLimitMiddleware; } });
126
151
  var AccessiCatalogMigrator_1 = require("./database-updates/AccessiCatalogMigrator");
127
152
  Object.defineProperty(exports, "AccessiCatalogMigrator", { enumerable: true, get: function () { return AccessiCatalogMigrator_1.AccessiCatalogMigrator; } });
128
153
  Object.defineProperty(exports, "applyCatalogScripts", { enumerable: true, get: function () { return AccessiCatalogMigrator_1.applyCatalogScripts; } });
@@ -155,6 +180,11 @@ Object.defineProperty(exports, "ACCESSI_EMAIL_NOT_CONFIGURED", { enumerable: tru
155
180
  var loginAs_1 = require("./security/loginAs");
156
181
  Object.defineProperty(exports, "buildLoginAsUrl", { enumerable: true, get: function () { return loginAs_1.buildLoginAsUrl; } });
157
182
  Object.defineProperty(exports, "LOGIN_AS_QUERY_PARAM", { enumerable: true, get: function () { return loginAs_1.LOGIN_AS_QUERY_PARAM; } });
183
+ var jwks_2 = require("./security/jwks");
184
+ Object.defineProperty(exports, "buildAccessiJwks", { enumerable: true, get: function () { return jwks_2.buildAccessiJwks; } });
185
+ Object.defineProperty(exports, "createAccessiJwksHandler", { enumerable: true, get: function () { return jwks_2.createAccessiJwksHandler; } });
186
+ Object.defineProperty(exports, "getAccessiJwksPath", { enumerable: true, get: function () { return jwks_2.getAccessiJwksPath; } });
187
+ Object.defineProperty(exports, "setupAccessiJwks", { enumerable: true, get: function () { return jwks_2.setupAccessiJwks; } });
158
188
  var FederatedAuthService_1 = require("./federated-auth/FederatedAuthService");
159
189
  Object.defineProperty(exports, "FederatedAuthService", { enumerable: true, get: function () { return FederatedAuthService_1.FederatedAuthService; } });
160
190
  __exportStar(require("./Dtos"), exports);
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/accessi-module/index.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 { AccessiModule, AccessiOptions } from \"./AccessiModule\";\nimport { Logger } from \"../Logger\";\nimport { AuthService } from \"./Services/AuthService/AuthService\";\nimport {\n buildOpenApiExcludePaths,\n isOpenApiRequestPath,\n resolveAccessiOpenApiOptions,\n setupOpenApi\n} from \"../openapi/OpenApiConfig\";\nimport {\n beginAccessiAuthInitialization,\n failAccessiAuthInitialization,\n setAccessiAuthService\n} from \"./middleware/authenticateGen\";\nimport { registerAccessiModuleServices } from \"./accessiRuntime\";\nimport { logAccessiStartup } from \"./startupLog\";\nimport { createAccessiValidationPipe } from './security/accessiValidation';\nimport { AccessiHttpExceptionFilter } from './security/AccessiHttpExceptionFilter';\nimport { assertEmailConfigured } from './security/emailConfiguration';\n\n/** Esito del bootstrap Express di Accessi. */\nexport interface AccessiModuleInitializationResult {\n /** Sotto-app Nest che serve `/api/accessi/*` e la documentazione. */\n nestApp: INestApplication;\n /** Documento OpenAPI generato, mergiabile nel documento unico dell'host. */\n document: OpenAPIObject;\n}\n\nfunction describeDatabaseTarget(options: AccessiOptions): string {\n const dbOptions = options.databaseOptions as {\n host?: string;\n port?: number;\n database?: string;\n user?: string;\n };\n\n return `${dbOptions.host ?? \"?\"}:${dbOptions.port ?? \"?\"} -> ${dbOptions.database ?? \"?\"} as ${dbOptions.user ?? \"?\"}`;\n}\n\n/**\n * Monta il modulo Accessi in un'applicazione Express gia esistente.\n *\n * Il bootstrap crea un host Nest isolato per le rotte `/api/accessi`, inizializza la\n * documentazione OpenAPI, validazione ed error handler, quindi rende disponibile\n * `authorizeAccessi` al resto dell'app.\n * Attendere questa Promise prima di aprire la porta HTTP: durante il bootstrap il middleware\n * restituisce `ACCESSI_AUTH_INITIALIZING` invece di autorizzare richieste parzialmente inizializzate.\n *\n * @param app Applicazione Express ospitante. Non viene sostituita ne vengono intercettate rotte estranee ad Accessi.\n * @param options Configurazione riservata del modulo; proviene normalmente da variabili d'ambiente o secret manager.\n * @returns Il sotto-app Nest e il documento OpenAPI generato (utile per il merge nell'host).\n */\nexport async function initializeAccessiModule(\n app: Application,\n options: AccessiOptions\n): Promise<AccessiModuleInitializationResult> {\n // Il servizio email e obbligatorio: reset password, 2FA e creazione utente ne dipendono.\n assertEmailConfigured(options);\n const logger: Logger = new Logger(\"initializeAccessiModule\");\n beginAccessiAuthInitialization();\n const startedAt = performance.now();\n const resolvedOpenApi = resolveAccessiOpenApiOptions(options.openapi);\n\n try {\n logger.info(\"Inizializzazione modulo accessi avviata.\");\n logger.info(`Target database accessi: ${describeDatabaseTarget(options)}`);\n logger.info(\n `Configurazione accessi: autoUpdateDatabase=${options.autoUpdateDatabase !== false}, legacyPasswordMigrationOnStartup=${options.legacyPasswordMigrationOnStartup !== false}`\n );\n const nestHostApp = express();\n const isNestRoute = (path: string) =>\n path === \"/api/accessi\" ||\n path.startsWith(\"/api/accessi/\") ||\n isOpenApiRequestPath(path, resolvedOpenApi);\n\n app.use((req: Request, res, next) => {\n if (!isNestRoute(req.path)) {\n return next();\n }\n\n return nestHostApp(req, res, next);\n });\n\n const nestExpressInstance = new ExpressAdapter(nestHostApp);\n\n const nestApp = await NestFactory.create(AccessiModule.forRoot(options), nestExpressInstance, {\n bufferLogs: options.bufferLogs !== false,\n ...(options.logger ? { logger: options.logger } : {})\n });\n logger.info(\"Applicazione Nest accessi creata.\");\n\n nestApp.enableCors({ exposedHeaders: ['X-Total-Count'] });\n nestApp.useGlobalPipes(createAccessiValidationPipe());\n nestApp.useGlobalFilters(new AccessiHttpExceptionFilter());\n\n nestApp.setGlobalPrefix('api', {\n exclude: buildOpenApiExcludePaths(resolvedOpenApi)\n });\n\n const document = setupOpenApi(nestApp, resolvedOpenApi);\n logger.info(\"Avvio init del modulo accessi.\");\n await nestApp.init();\n logger.info(\"Init del modulo accessi completata.\");\n if (options.legacyPasswordMigrationOnStartup !== false) {\n logger.info(\"Avvio migrazione password legacy accessi.\");\n const passwordMigrationService = nestApp.get(AuthService);\n await passwordMigrationService.migrateLegacyEncryptedPasswords();\n logger.info(\"Migrazione password legacy accessi completata.\");\n }\n\n const accessiServices = registerAccessiModuleServices(app, nestApp);\n app.locals.accessiAuthService = accessiServices.authenticateGenService;\n setAccessiAuthService(accessiServices.authenticateGenService);\n logAccessiStartup(options, resolvedOpenApi);\n const elapsedMs = performance.now() - startedAt;\n logger.info(`Accessi initialized. Tempo totale bootstrap: ${elapsedMs.toFixed(2)} ms`);\n\n return { nestApp, document };\n } catch (error) {\n failAccessiAuthInitialization(error);\n logger.error(\n \"Errore in initialize AccessiModule:\",\n error instanceof Error\n ? { name: error.name, message: error.message, stack: error.stack }\n : { error: String(error) }\n );\n throw error;\n }\n}\n\nexport { AccessiModule } from \"./AccessiModule\";\nexport { AccessiIntrospectionController } from \"./Controllers/AccessiIntrospectionController\";\nexport type { AccessiTokenIntrospectionResult } from \"./Services/AuthService/AuthService\";\nexport { AccessiDatabaseUpdater } from \"./database-updates/AccessiDatabaseUpdater\";\nexport type { AccessiOptions, EmailOptions, JwtOptions, ExtensionFieldsOptions, PublicAuthRateLimitOptions, PublicAuthRateLimitRuleOptions } from \"./AccessiModule\";\nexport type { PublicRegistrationOptions, FederatedAuthenticationOptions, ServiceTokenOptions } from \"./AccessiModule\";\nexport type { CatalogScriptsOptions, CatalogScriptChecksumPolicy } from \"./AccessiModule\";\nexport {\n AccessiCatalogMigrator,\n applyCatalogScripts,\n listPendingCatalogScripts,\n splitSqlStatements,\n analyzeCatalogScript,\n leadingStatementKeyword,\n computeCatalogScriptChecksum,\n discoverCatalogScripts\n} from \"./database-updates/AccessiCatalogMigrator\";\nexport type { CatalogMigrationReport, CatalogScriptMigration } from \"./database-updates/AccessiCatalogMigrator\";\nexport { ServiceTokenService } from './Services/ServiceTokenService/ServiceTokenService';\nexport type {\n ServiceTokenMetadata,\n IssuedServiceToken,\n VerifiedServiceToken,\n IssueServiceTokenInput\n} from './Services/ServiceTokenService/ServiceTokenService';\nexport { SchemaExportService } from './Services/SchemaExportService/SchemaExportService';\nexport type { SchemaDdlResult, SchemaDdlSection, SchemaEntitiesResult } from './Services/SchemaExportService/SchemaExportService';\nexport { AdminBootstrapService } from './Services/AdminBootstrapService/AdminBootstrapService';\nexport type { BootstrapAdminResult } from './Services/AdminBootstrapService/AdminBootstrapService';\nexport {\n getOrCreateAdminBootstrapToken,\n getAdminBootstrapToken,\n clearAdminBootstrapToken,\n isValidAdminBootstrapToken\n} from './security/adminBootstrap';\nexport { ServiceTokenGuard, RequireServiceTokenScopes, getAccessiServiceToken } from './security/serviceTokenGuard';\nexport { assertEmailConfigured, isEmailConfigured, AccessiEmailNotConfiguredError, ACCESSI_EMAIL_NOT_CONFIGURED } from './security/emailConfiguration';\nexport { buildLoginAsUrl, LOGIN_AS_QUERY_PARAM } from './security/loginAs';\nexport { FederatedAuthService } from './federated-auth/FederatedAuthService';\nexport type { FederatedAuthenticationResult, FederatedIdentity, FederatedProvider, VerifiedFederatedIdentity } from './federated-auth/FederatedAuthTypes';\nexport * from \"./Dtos\";\nexport {\n authorizeAccessi,\n authenticateGen\n} from \"./middleware/authenticateGen\";\nexport { accessiRequirement } from \"./middleware/accessiRequirements\";\nexport {\n registerAccessiModuleServices,\n getAccessiModuleServices,\n ACCESSI_MODULE_SERVICES_LOCALS_KEY\n} from \"./accessiRuntime\";\nexport type { AccessiModuleServices } from \"./accessiRuntime\";\nexport type {\n AccessiAuthorizationOptions,\n AccessiRequirementNode,\n AccessiCustomRequirementContext,\n AccessiCustomRequirementHandler\n} from \"./middleware/accessiRequirements\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/accessi-module/index.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 { AccessiModule, AccessiOptions } from \"./AccessiModule\";\nimport { Logger } from \"../Logger\";\nimport { AuthService } from \"./Services/AuthService/AuthService\";\nimport {\n buildOpenApiExcludePaths,\n isOpenApiRequestPath,\n resolveAccessiOpenApiOptions,\n setupOpenApi\n} from \"../openapi/OpenApiConfig\";\nimport {\n beginAccessiAuthInitialization,\n failAccessiAuthInitialization,\n setAccessiAuthService\n} from \"./middleware/authenticateGen\";\nimport { registerAccessiModuleServices } from \"./accessiRuntime\";\nimport { logAccessiStartup } from \"./startupLog\";\nimport { createAccessiValidationPipe } from './security/accessiValidation';\nimport { AccessiHttpExceptionFilter } from './security/AccessiHttpExceptionFilter';\nimport { assertEmailConfigured } from './security/emailConfiguration';\nimport { getAccessiJwksPath, setupAccessiJwks } from './security/jwks';\nimport { createAccessiEdgeRateLimitMiddleware } from './security/edgeRateLimit';\n\n/** Esito del bootstrap Express di Accessi. */\nexport interface AccessiModuleInitializationResult {\n /** Sotto-app Nest che serve `/api/accessi/*` e la documentazione. */\n nestApp: INestApplication;\n /** Documento OpenAPI generato, mergiabile nel documento unico dell'host. */\n document: OpenAPIObject;\n}\n\nfunction describeDatabaseTarget(options: AccessiOptions): string {\n const dbOptions = options.databaseOptions as {\n host?: string;\n port?: number;\n database?: string;\n user?: string;\n };\n\n return `${dbOptions.host ?? \"?\"}:${dbOptions.port ?? \"?\"} -> ${dbOptions.database ?? \"?\"} as ${dbOptions.user ?? \"?\"}`;\n}\n\n/**\n * Monta il modulo Accessi in un'applicazione Express gia esistente.\n *\n * Il bootstrap crea un host Nest isolato per le rotte `/api/accessi`, inizializza la\n * documentazione OpenAPI, validazione ed error handler, quindi rende disponibile\n * `authorizeAccessi` al resto dell'app.\n * Attendere questa Promise prima di aprire la porta HTTP: durante il bootstrap il middleware\n * restituisce `ACCESSI_AUTH_INITIALIZING` invece di autorizzare richieste parzialmente inizializzate.\n *\n * @param app Applicazione Express ospitante. Non viene sostituita ne vengono intercettate rotte estranee ad Accessi.\n * @param options Configurazione riservata del modulo; proviene normalmente da variabili d'ambiente o secret manager.\n * @returns Il sotto-app Nest e il documento OpenAPI generato (utile per il merge nell'host).\n */\nexport async function initializeAccessiModule(\n app: Application,\n options: AccessiOptions\n): Promise<AccessiModuleInitializationResult> {\n // Il servizio email e obbligatorio: reset password, 2FA e creazione utente ne dipendono.\n assertEmailConfigured(options);\n const logger: Logger = new Logger(\"initializeAccessiModule\");\n beginAccessiAuthInitialization();\n const startedAt = performance.now();\n const resolvedOpenApi = resolveAccessiOpenApiOptions(options.openapi);\n\n try {\n logger.info(\"Inizializzazione modulo accessi avviata.\");\n logger.info(`Target database accessi: ${describeDatabaseTarget(options)}`);\n logger.info(\n `Configurazione accessi: autoUpdateDatabase=${options.autoUpdateDatabase !== false}, legacyPasswordMigrationOnStartup=${options.legacyPasswordMigrationOnStartup !== false}`\n );\n const nestHostApp = express();\n const jwksPath = getAccessiJwksPath(options);\n const isAccessiRoute = (path: string) =>\n path === \"/api/accessi\" ||\n path.startsWith(\"/api/accessi/\") ||\n isOpenApiRequestPath(path, resolvedOpenApi) ||\n (jwksPath !== undefined && path === jwksPath);\n const edgeRateLimit = options.edgeRateLimit\n ? createAccessiEdgeRateLimitMiddleware({\n ...options.edgeRateLimit,\n match: options.edgeRateLimit.match ?? ((path) => isAccessiRoute(path)),\n })\n : undefined;\n if (edgeRateLimit) {\n app.use(edgeRateLimit);\n }\n\n app.use((req: Request, res, next) => {\n if (!isAccessiRoute(req.path)) {\n return next();\n }\n\n return nestHostApp(req, res, next);\n });\n\n setupAccessiJwks(app, options);\n if (jwksPath) {\n logger.info(`JWKS accessi disponibile su: ${jwksPath}`);\n }\n\n const nestExpressInstance = new ExpressAdapter(nestHostApp);\n\n const nestApp = await NestFactory.create(AccessiModule.forRoot(options), nestExpressInstance, {\n bufferLogs: options.bufferLogs !== false,\n ...(options.logger ? { logger: options.logger } : {})\n });\n logger.info(\"Applicazione Nest accessi creata.\");\n\n nestApp.enableCors({ exposedHeaders: ['X-Total-Count'] });\n nestApp.useGlobalPipes(createAccessiValidationPipe());\n nestApp.useGlobalFilters(new AccessiHttpExceptionFilter());\n\n nestApp.setGlobalPrefix('api', {\n exclude: buildOpenApiExcludePaths(resolvedOpenApi)\n });\n\n const document = setupOpenApi(nestApp, resolvedOpenApi);\n logger.info(\"Avvio init del modulo accessi.\");\n await nestApp.init();\n logger.info(\"Init del modulo accessi completata.\");\n if (options.legacyPasswordMigrationOnStartup !== false) {\n logger.info(\"Avvio migrazione password legacy accessi.\");\n const passwordMigrationService = nestApp.get(AuthService);\n await passwordMigrationService.migrateLegacyEncryptedPasswords();\n logger.info(\"Migrazione password legacy accessi completata.\");\n }\n\n const accessiServices = registerAccessiModuleServices(app, nestApp);\n app.locals.accessiAuthService = accessiServices.authenticateGenService;\n setAccessiAuthService(accessiServices.authenticateGenService);\n logAccessiStartup(options, resolvedOpenApi);\n const elapsedMs = performance.now() - startedAt;\n logger.info(`Accessi initialized. Tempo totale bootstrap: ${elapsedMs.toFixed(2)} ms`);\n\n return { nestApp, document };\n } catch (error) {\n failAccessiAuthInitialization(error);\n logger.error(\n \"Errore in initialize AccessiModule:\",\n error instanceof Error\n ? { name: error.name, message: error.message, stack: error.stack }\n : { error: String(error) }\n );\n throw error;\n }\n}\n\nexport { AccessiModule } from \"./AccessiModule\";\nexport { AccessiIntrospectionController } from \"./Controllers/AccessiIntrospectionController\";\nexport type { AccessiTokenIntrospectionResult } from \"./Services/AuthService/AuthService\";\nexport { AccessiDatabaseUpdater } from \"./database-updates/AccessiDatabaseUpdater\";\nexport type { AccessiOptions, EmailOptions, JwtOptions, ExtensionFieldsOptions, PublicAuthRateLimitOptions, PublicAuthRateLimitRuleOptions } from \"./AccessiModule\";\nexport type { PublicRegistrationOptions, FederatedAuthenticationOptions, ServiceTokenOptions } from \"./AccessiModule\";\nexport type { CatalogScriptsOptions, CatalogScriptChecksumPolicy, ConsoleEntryOptions } from \"./AccessiModule\";\nexport {\n createMemoryConsoleEntryStore,\n createRedisConsoleEntryStore,\n getDefaultConsoleEntryStore\n} from './security/consoleEntry';\nexport type {\n ConsoleEntryRedisClient,\n ConsoleEntryRecord,\n ConsoleEntryStore,\n ConsoleEntryTicket,\n MemoryConsoleEntryStore,\n RedisConsoleEntryStoreOptions\n} from './security/consoleEntry';\nexport {\n createMemoryRateLimitStore,\n createRedisRateLimitStore,\n getDefaultRateLimitStore\n} from './security/rateLimitStore';\nexport type {\n MemoryRateLimitStore,\n RateLimitDecision,\n RateLimitRedisClient,\n RateLimitStore,\n RedisRateLimitStoreOptions\n} from './security/rateLimitStore';\nexport { createAccessiEdgeRateLimitMiddleware } from './security/edgeRateLimit';\nexport type {\n AccessiEdgeRateLimitMiddleware,\n AccessiEdgeRateLimitOptions\n} from './security/edgeRateLimit';\nexport {\n AccessiCatalogMigrator,\n applyCatalogScripts,\n listPendingCatalogScripts,\n splitSqlStatements,\n analyzeCatalogScript,\n leadingStatementKeyword,\n computeCatalogScriptChecksum,\n discoverCatalogScripts\n} from \"./database-updates/AccessiCatalogMigrator\";\nexport type { CatalogMigrationReport, CatalogScriptMigration } from \"./database-updates/AccessiCatalogMigrator\";\nexport { ServiceTokenService } from './Services/ServiceTokenService/ServiceTokenService';\nexport type {\n ServiceTokenMetadata,\n IssuedServiceToken,\n VerifiedServiceToken,\n IssueServiceTokenInput\n} from './Services/ServiceTokenService/ServiceTokenService';\nexport { SchemaExportService } from './Services/SchemaExportService/SchemaExportService';\nexport type { SchemaDdlResult, SchemaDdlSection, SchemaEntitiesResult } from './Services/SchemaExportService/SchemaExportService';\nexport { AdminBootstrapService } from './Services/AdminBootstrapService/AdminBootstrapService';\nexport type { BootstrapAdminResult } from './Services/AdminBootstrapService/AdminBootstrapService';\nexport {\n getOrCreateAdminBootstrapToken,\n getAdminBootstrapToken,\n clearAdminBootstrapToken,\n isValidAdminBootstrapToken\n} from './security/adminBootstrap';\nexport { ServiceTokenGuard, RequireServiceTokenScopes, getAccessiServiceToken } from './security/serviceTokenGuard';\nexport { assertEmailConfigured, isEmailConfigured, AccessiEmailNotConfiguredError, ACCESSI_EMAIL_NOT_CONFIGURED } from './security/emailConfiguration';\nexport { buildLoginAsUrl, LOGIN_AS_QUERY_PARAM } from './security/loginAs';\nexport {\n buildAccessiJwks,\n createAccessiJwksHandler,\n getAccessiJwksPath,\n setupAccessiJwks\n} from './security/jwks';\nexport type { AccessiJwksDocument, AccessiJwksKey } from './security/jwks';\nexport { FederatedAuthService } from './federated-auth/FederatedAuthService';\nexport type { FederatedAuthenticationResult, FederatedIdentity, FederatedProvider, VerifiedFederatedIdentity } from './federated-auth/FederatedAuthTypes';\nexport * from \"./Dtos\";\nexport {\n authorizeAccessi,\n authenticateGen\n} from \"./middleware/authenticateGen\";\nexport { accessiRequirement } from \"./middleware/accessiRequirements\";\nexport {\n registerAccessiModuleServices,\n getAccessiModuleServices,\n ACCESSI_MODULE_SERVICES_LOCALS_KEY\n} from \"./accessiRuntime\";\nexport type { AccessiModuleServices } from \"./accessiRuntime\";\nexport type {\n AccessiAuthorizationOptions,\n AccessiRequirementNode,\n AccessiCustomRequirementContext,\n AccessiCustomRequirementHandler\n} from \"./middleware/accessiRequirements\";\n"]}
@@ -1,16 +1,61 @@
1
+ /**
2
+ * Ticket di ingresso alla console, monouso e a breve scadenza.
3
+ *
4
+ * Serve a passare una sessione gia autenticata (frontend che possiede un JWT Accessi)
5
+ * alla console amministrativa senza reinserire le credenziali e senza mettere il JWT
6
+ * nell'URL: il frontend scambia il proprio JWT con un ticket opaco, apre la console
7
+ * con `#entry=<ticket>` e la console lo scambia con il token originale.
8
+ *
9
+ * Il ticket e valido una sola volta, per pochi secondi, ed e legato all'utente che
10
+ * lo ha richiesto (che deve avere accesso alla console). Lo store di default e in
11
+ * memoria; in multi-istanza usare `createRedisConsoleEntryStore`.
12
+ */
13
+ export interface ConsoleEntryRecord {
14
+ token: string;
15
+ codiceUtente: number;
16
+ }
1
17
  export interface ConsoleEntryTicket {
2
18
  ticket: string;
3
19
  expiresInSeconds: number;
4
20
  }
5
- /** Emette un ticket monouso per l'utente indicato, conservando il suo JWT per lo scambio. */
6
- export declare function issueConsoleEntryTicket(record: {
7
- token: string;
8
- codiceUtente: number;
9
- }, ttlMs?: number): ConsoleEntryTicket;
10
- /** Consuma (monouso) un ticket e restituisce il JWT collegato, oppure null se scaduto/assente/invalido. */
11
- export declare function consumeConsoleEntryTicket(ticket: unknown): {
12
- token: string;
13
- codiceUtente: number;
14
- } | null;
15
- /** Solo per i test: azzera lo store in memoria. */
21
+ /** Store pluggable: memoria per single instance, Redis per multi-istanza. */
22
+ export interface ConsoleEntryStore {
23
+ issue(record: ConsoleEntryRecord, ttlMs: number): Promise<ConsoleEntryTicket>;
24
+ consume(ticket: string): Promise<ConsoleEntryRecord | null>;
25
+ }
26
+ /** Store in memoria con API sincrona per retrocompatibilita e API async per l'interfaccia. */
27
+ export interface MemoryConsoleEntryStore extends ConsoleEntryStore {
28
+ issueSync(record: ConsoleEntryRecord, ttlMs?: number): ConsoleEntryTicket;
29
+ consumeSync(ticket: string): ConsoleEntryRecord | null;
30
+ clear(): void;
31
+ }
32
+ /** Store in memoria: adatto a sviluppo e a una singola istanza. */
33
+ export declare function createMemoryConsoleEntryStore(maxEntries?: number): MemoryConsoleEntryStore;
34
+ /** Store di default in memoria, usato quando `AccessiOptions.consoleEntry.store` non e configurato. */
35
+ export declare function getDefaultConsoleEntryStore(): ConsoleEntryStore;
36
+ export declare function issueConsoleEntryTicket(record: ConsoleEntryRecord, ttlMs?: number): ConsoleEntryTicket;
37
+ export declare function consumeConsoleEntryTicket(ticket: unknown): ConsoleEntryRecord | null;
38
+ /** Solo per i test: azzera lo store in memoria di default. */
16
39
  export declare function clearConsoleEntryTickets(): void;
40
+ /** Client Redis minimale: compatibile con ioredis (`redis.eval(script, numKeys, ...args)`). */
41
+ export interface ConsoleEntryRedisClient {
42
+ eval(script: string, numKeys: number, ...args: Array<string | number>): Promise<unknown>;
43
+ }
44
+ export interface RedisConsoleEntryStoreOptions {
45
+ client: ConsoleEntryRedisClient;
46
+ /** Segreto per cifrare il JWT nel Redis. Non usare il JWT in chiaro. */
47
+ encryptionKey: string | Buffer;
48
+ /** Default: `accessi:console-entry:`. */
49
+ keyPrefix?: string;
50
+ /** Tentativi in caso di collisione del ticket. Default: 5. */
51
+ maxIssueAttempts?: number;
52
+ }
53
+ /**
54
+ * Store Redis per ticket console.
55
+ *
56
+ * - il JWT viene cifrato con AES-256-GCM prima di essere scritto;
57
+ * - `SET ... PX ... NX` evita collisioni;
58
+ * - il consume usa `GET` + `DEL` atomici in Lua, quindi il ticket e monouso
59
+ * anche con piu istanze.
60
+ */
61
+ export declare function createRedisConsoleEntryStore(options: RedisConsoleEntryStoreOptions): ConsoleEntryStore;
@@ -1,50 +1,184 @@
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 });
12
+ exports.createMemoryConsoleEntryStore = createMemoryConsoleEntryStore;
13
+ exports.getDefaultConsoleEntryStore = getDefaultConsoleEntryStore;
3
14
  exports.issueConsoleEntryTicket = issueConsoleEntryTicket;
4
15
  exports.consumeConsoleEntryTicket = consumeConsoleEntryTicket;
5
16
  exports.clearConsoleEntryTickets = clearConsoleEntryTickets;
17
+ exports.createRedisConsoleEntryStore = createRedisConsoleEntryStore;
6
18
  const crypto_1 = require("crypto");
7
19
  const DEFAULT_TTL_MS = 60000;
8
- const MAX_ENTRIES = 5000;
9
- const entries = new Map();
10
- function sweep(now) {
11
- for (const [ticket, record] of entries) {
12
- if (record.expiresAt <= now)
13
- entries.delete(ticket);
14
- }
20
+ const DEFAULT_MAX_ENTRIES = 5000;
21
+ const TICKET_PATTERN = /^[a-f0-9]{64}$/i;
22
+ function createTicket() {
23
+ return (0, crypto_1.randomBytes)(32).toString('hex');
24
+ }
25
+ function normalizeTtl(ttlMs) {
26
+ return Math.max(1, Math.trunc(ttlMs));
27
+ }
28
+ /** Store in memoria: adatto a sviluppo e a una singola istanza. */
29
+ function createMemoryConsoleEntryStore(maxEntries = DEFAULT_MAX_ENTRIES) {
30
+ const entries = new Map();
31
+ const sweep = (now) => {
32
+ for (const [ticket, record] of entries) {
33
+ if (record.expiresAt <= now)
34
+ entries.delete(ticket);
35
+ }
36
+ };
37
+ const issueSync = (record, ttlMs = DEFAULT_TTL_MS) => {
38
+ // Il TTL negativo resta supportato per test e per il comportamento storico:
39
+ // il ticket e immediatamente scaduto.
40
+ const ttl = Math.trunc(ttlMs);
41
+ const now = Date.now();
42
+ sweep(now);
43
+ while (entries.size >= maxEntries) {
44
+ const oldest = entries.keys().next().value;
45
+ if (oldest === undefined)
46
+ break;
47
+ entries.delete(oldest);
48
+ }
49
+ const ticket = createTicket();
50
+ entries.set(ticket, Object.assign(Object.assign({}, record), { expiresAt: now + ttl }));
51
+ return { ticket, expiresInSeconds: Math.max(0, Math.ceil(ttl / 1000)) };
52
+ };
53
+ const consumeSync = (ticket) => {
54
+ if (typeof ticket !== 'string' || !TICKET_PATTERN.test(ticket)) {
55
+ return null;
56
+ }
57
+ const record = entries.get(ticket);
58
+ if (!record)
59
+ return null;
60
+ entries.delete(ticket);
61
+ if (record.expiresAt <= Date.now())
62
+ return null;
63
+ return { token: record.token, codiceUtente: record.codiceUtente };
64
+ };
65
+ return {
66
+ issueSync,
67
+ consumeSync,
68
+ clear: () => entries.clear(),
69
+ issue(record, ttlMs) {
70
+ return __awaiter(this, void 0, void 0, function* () {
71
+ return issueSync(record, ttlMs);
72
+ });
73
+ },
74
+ consume(ticket) {
75
+ return __awaiter(this, void 0, void 0, function* () {
76
+ return consumeSync(ticket);
77
+ });
78
+ },
79
+ };
80
+ }
81
+ const defaultStore = createMemoryConsoleEntryStore();
82
+ /** Store di default in memoria, usato quando `AccessiOptions.consoleEntry.store` non e configurato. */
83
+ function getDefaultConsoleEntryStore() {
84
+ return defaultStore;
15
85
  }
16
- /** Emette un ticket monouso per l'utente indicato, conservando il suo JWT per lo scambio. */
86
+ // Funzioni storiche sincrone: mantengono il comportamento per test e consumer esistenti.
17
87
  function issueConsoleEntryTicket(record, ttlMs = DEFAULT_TTL_MS) {
18
- const now = Date.now();
19
- sweep(now);
20
- // Limita la memoria anche sotto attacco: rimuove la voce piu vecchia.
21
- while (entries.size >= MAX_ENTRIES) {
22
- const oldest = entries.keys().next().value;
23
- if (oldest === undefined)
24
- break;
25
- entries.delete(oldest);
26
- }
27
- const ticket = (0, crypto_1.randomBytes)(32).toString('hex');
28
- entries.set(ticket, Object.assign(Object.assign({}, record), { expiresAt: now + ttlMs }));
29
- return { ticket, expiresInSeconds: Math.ceil(ttlMs / 1000) };
88
+ return defaultStore.issueSync(record, ttlMs);
30
89
  }
31
- /** Consuma (monouso) un ticket e restituisce il JWT collegato, oppure null se scaduto/assente/invalido. */
32
90
  function consumeConsoleEntryTicket(ticket) {
33
- if (typeof ticket !== 'string' || !/^[a-f0-9]{64}$/i.test(ticket)) {
34
- return null;
91
+ return typeof ticket === 'string' ? defaultStore.consumeSync(ticket) : null;
92
+ }
93
+ /** Solo per i test: azzera lo store in memoria di default. */
94
+ function clearConsoleEntryTickets() {
95
+ defaultStore.clear();
96
+ }
97
+ const SET_SCRIPT = "return redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2], 'NX')";
98
+ const CONSUME_SCRIPT = "local value = redis.call('GET', KEYS[1]) if value then redis.call('DEL', KEYS[1]) end return value";
99
+ function deriveEncryptionKey(secret) {
100
+ const source = Buffer.isBuffer(secret) ? secret : Buffer.from(secret, 'utf8');
101
+ if (source.length === 0) {
102
+ throw new Error('Redis console entry encryptionKey non configurata.');
35
103
  }
36
- const record = entries.get(ticket);
37
- if (!record) {
104
+ return source.length === 32 ? source : (0, crypto_1.createHash)('sha256').update(source).digest();
105
+ }
106
+ function encryptRecord(record, key) {
107
+ const iv = (0, crypto_1.randomBytes)(12);
108
+ const cipher = (0, crypto_1.createCipheriv)('aes-256-gcm', key, iv);
109
+ const ciphertext = Buffer.concat([
110
+ cipher.update(JSON.stringify(record), 'utf8'),
111
+ cipher.final(),
112
+ ]);
113
+ const tag = cipher.getAuthTag();
114
+ return [iv, tag, ciphertext].map((part) => part.toString('base64url')).join('.');
115
+ }
116
+ function decryptRecord(value, key) {
117
+ const [ivPart, tagPart, ciphertextPart] = value.split('.');
118
+ if (!ivPart || !tagPart || !ciphertextPart)
38
119
  return null;
120
+ try {
121
+ const decipher = (0, crypto_1.createDecipheriv)('aes-256-gcm', key, Buffer.from(ivPart, 'base64url'));
122
+ decipher.setAuthTag(Buffer.from(tagPart, 'base64url'));
123
+ const plaintext = Buffer.concat([
124
+ decipher.update(Buffer.from(ciphertextPart, 'base64url')),
125
+ decipher.final(),
126
+ ]).toString('utf8');
127
+ const parsed = JSON.parse(plaintext);
128
+ if (!parsed ||
129
+ typeof parsed !== 'object' ||
130
+ typeof parsed.token !== 'string' ||
131
+ typeof parsed.codiceUtente !== 'number') {
132
+ return null;
133
+ }
134
+ return parsed;
39
135
  }
40
- entries.delete(ticket);
41
- if (record.expiresAt <= Date.now()) {
136
+ catch (_a) {
42
137
  return null;
43
138
  }
44
- return { token: record.token, codiceUtente: record.codiceUtente };
45
139
  }
46
- /** Solo per i test: azzera lo store in memoria. */
47
- function clearConsoleEntryTickets() {
48
- entries.clear();
140
+ /**
141
+ * Store Redis per ticket console.
142
+ *
143
+ * - il JWT viene cifrato con AES-256-GCM prima di essere scritto;
144
+ * - `SET ... PX ... NX` evita collisioni;
145
+ * - il consume usa `GET` + `DEL` atomici in Lua, quindi il ticket e monouso
146
+ * anche con piu istanze.
147
+ */
148
+ function createRedisConsoleEntryStore(options) {
149
+ var _a, _b;
150
+ if (!(options === null || options === void 0 ? void 0 : options.client) || typeof options.client.eval !== 'function') {
151
+ throw new Error('Redis console entry store: client.eval non configurato.');
152
+ }
153
+ const key = deriveEncryptionKey(options.encryptionKey);
154
+ const keyPrefix = ((_a = options.keyPrefix) !== null && _a !== void 0 ? _a : 'accessi:console-entry:').replace(/:+$/, ':');
155
+ const maxAttempts = Math.max(1, (_b = options.maxIssueAttempts) !== null && _b !== void 0 ? _b : 5);
156
+ return {
157
+ issue(record, ttlMs) {
158
+ return __awaiter(this, void 0, void 0, function* () {
159
+ const safeTtl = normalizeTtl(ttlMs);
160
+ const value = encryptRecord(record, key);
161
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
162
+ const ticket = createTicket();
163
+ const result = yield options.client.eval(SET_SCRIPT, 1, `${keyPrefix}${ticket}`, value, safeTtl);
164
+ if (result) {
165
+ return { ticket, expiresInSeconds: Math.ceil(safeTtl / 1000) };
166
+ }
167
+ }
168
+ throw new Error('Impossibile emettere un ticket console univoco.');
169
+ });
170
+ },
171
+ consume(ticket) {
172
+ return __awaiter(this, void 0, void 0, function* () {
173
+ if (typeof ticket !== 'string' || !TICKET_PATTERN.test(ticket)) {
174
+ return null;
175
+ }
176
+ const value = yield options.client.eval(CONSUME_SCRIPT, 1, `${keyPrefix}${ticket}`);
177
+ if (typeof value !== 'string')
178
+ return null;
179
+ return decryptRecord(value, key);
180
+ });
181
+ },
182
+ };
49
183
  }
50
184
  //# sourceMappingURL=consoleEntry.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"consoleEntry.js","sourceRoot":"","sources":["../../../src/accessi-module/security/consoleEntry.ts"],"names":[],"mappings":";;AAqCA,0DAiBC;AAGD,8DAeC;AAGD,4DAEC;AA7ED,mCAAqC;AAqBrC,MAAM,cAAc,GAAG,KAAM,CAAC;AAC9B,MAAM,WAAW,GAAG,IAAI,CAAC;AACzB,MAAM,OAAO,GAAG,IAAI,GAAG,EAA8B,CAAC;AAOtD,SAAS,KAAK,CAAC,GAAW;IACxB,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACvC,IAAI,MAAM,CAAC,SAAS,IAAI,GAAG;YAAE,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACtD,CAAC;AACH,CAAC;AAED,6FAA6F;AAC7F,SAAgB,uBAAuB,CACrC,MAA+C,EAC/C,QAAgB,cAAc;IAE9B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,KAAK,CAAC,GAAG,CAAC,CAAC;IAEX,sEAAsE;IACtE,OAAO,OAAO,CAAC,IAAI,IAAI,WAAW,EAAE,CAAC;QACnC,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC;QAC3C,IAAI,MAAM,KAAK,SAAS;YAAE,MAAM;QAChC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACzB,CAAC;IAED,MAAM,MAAM,GAAG,IAAA,oBAAW,EAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC/C,OAAO,CAAC,GAAG,CAAC,MAAM,kCAAO,MAAM,KAAE,SAAS,EAAE,GAAG,GAAG,KAAK,IAAG,CAAC;IAC3D,OAAO,EAAE,MAAM,EAAE,gBAAgB,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,EAAE,CAAC;AAC/D,CAAC;AAED,2GAA2G;AAC3G,SAAgB,yBAAyB,CACvC,MAAe;IAEf,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QAClE,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACnC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACvB,IAAI,MAAM,CAAC,SAAS,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;QACnC,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,YAAY,EAAE,MAAM,CAAC,YAAY,EAAE,CAAC;AACpE,CAAC;AAED,mDAAmD;AACnD,SAAgB,wBAAwB;IACtC,OAAO,CAAC,KAAK,EAAE,CAAC;AAClB,CAAC","sourcesContent":["import { randomBytes } from 'crypto';\n\n/**\n * Ticket di ingresso alla console, monouso e a breve scadenza.\n *\n * Serve a passare una sessione gia autenticata (frontend che possiede un JWT Accessi)\n * alla console amministrativa senza reinserire le credenziali e senza mettere il JWT\n * nell'URL: il frontend scambia il proprio JWT con un ticket opaco, apre la console\n * con `#entry=<ticket>` e la console lo scambia con il token originale.\n *\n * Il ticket e' valido una sola volta, per pochi secondi, ed e' legato all'utente che\n * lo ha richiesto (che deve avere accesso alla console). Resta in memoria del processo:\n * con piu istanze del backend il ticket va consumato dalla stessa istanza che lo emette.\n */\n\ninterface ConsoleEntryRecord {\n token: string;\n codiceUtente: number;\n expiresAt: number;\n}\n\nconst DEFAULT_TTL_MS = 60_000;\nconst MAX_ENTRIES = 5000;\nconst entries = new Map<string, ConsoleEntryRecord>();\n\nexport interface ConsoleEntryTicket {\n ticket: string;\n expiresInSeconds: number;\n}\n\nfunction sweep(now: number): void {\n for (const [ticket, record] of entries) {\n if (record.expiresAt <= now) entries.delete(ticket);\n }\n}\n\n/** Emette un ticket monouso per l'utente indicato, conservando il suo JWT per lo scambio. */\nexport function issueConsoleEntryTicket(\n record: { token: string; codiceUtente: number },\n ttlMs: number = DEFAULT_TTL_MS,\n): ConsoleEntryTicket {\n const now = Date.now();\n sweep(now);\n\n // Limita la memoria anche sotto attacco: rimuove la voce piu vecchia.\n while (entries.size >= MAX_ENTRIES) {\n const oldest = entries.keys().next().value;\n if (oldest === undefined) break;\n entries.delete(oldest);\n }\n\n const ticket = randomBytes(32).toString('hex');\n entries.set(ticket, { ...record, expiresAt: now + ttlMs });\n return { ticket, expiresInSeconds: Math.ceil(ttlMs / 1000) };\n}\n\n/** Consuma (monouso) un ticket e restituisce il JWT collegato, oppure null se scaduto/assente/invalido. */\nexport function consumeConsoleEntryTicket(\n ticket: unknown,\n): { token: string; codiceUtente: number } | null {\n if (typeof ticket !== 'string' || !/^[a-f0-9]{64}$/i.test(ticket)) {\n return null;\n }\n const record = entries.get(ticket);\n if (!record) {\n return null;\n }\n entries.delete(ticket);\n if (record.expiresAt <= Date.now()) {\n return null;\n }\n return { token: record.token, codiceUtente: record.codiceUtente };\n}\n\n/** Solo per i test: azzera lo store in memoria. */\nexport function clearConsoleEntryTickets(): void {\n entries.clear();\n}\n"]}
1
+ {"version":3,"file":"consoleEntry.js","sourceRoot":"","sources":["../../../src/accessi-module/security/consoleEntry.ts"],"names":[],"mappings":"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","sourcesContent":["import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'crypto';\n\n/**\n * Ticket di ingresso alla console, monouso e a breve scadenza.\n *\n * Serve a passare una sessione gia autenticata (frontend che possiede un JWT Accessi)\n * alla console amministrativa senza reinserire le credenziali e senza mettere il JWT\n * nell'URL: il frontend scambia il proprio JWT con un ticket opaco, apre la console\n * con `#entry=<ticket>` e la console lo scambia con il token originale.\n *\n * Il ticket e valido una sola volta, per pochi secondi, ed e legato all'utente che\n * lo ha richiesto (che deve avere accesso alla console). Lo store di default e in\n * memoria; in multi-istanza usare `createRedisConsoleEntryStore`.\n */\n\nexport interface ConsoleEntryRecord {\n token: string;\n codiceUtente: number;\n}\n\nexport interface ConsoleEntryTicket {\n ticket: string;\n expiresInSeconds: number;\n}\n\n/** Store pluggable: memoria per single instance, Redis per multi-istanza. */\nexport interface ConsoleEntryStore {\n issue(record: ConsoleEntryRecord, ttlMs: number): Promise<ConsoleEntryTicket>;\n consume(ticket: string): Promise<ConsoleEntryRecord | null>;\n}\n\nconst DEFAULT_TTL_MS = 60_000;\nconst DEFAULT_MAX_ENTRIES = 5000;\nconst TICKET_PATTERN = /^[a-f0-9]{64}$/i;\n\ninterface StoredEntry extends ConsoleEntryRecord {\n expiresAt: number;\n}\n\n/** Store in memoria con API sincrona per retrocompatibilita e API async per l'interfaccia. */\nexport interface MemoryConsoleEntryStore extends ConsoleEntryStore {\n issueSync(record: ConsoleEntryRecord, ttlMs?: number): ConsoleEntryTicket;\n consumeSync(ticket: string): ConsoleEntryRecord | null;\n clear(): void;\n}\n\nfunction createTicket(): string {\n return randomBytes(32).toString('hex');\n}\n\nfunction normalizeTtl(ttlMs: number): number {\n return Math.max(1, Math.trunc(ttlMs));\n}\n\n/** Store in memoria: adatto a sviluppo e a una singola istanza. */\nexport function createMemoryConsoleEntryStore(\n maxEntries: number = DEFAULT_MAX_ENTRIES,\n): MemoryConsoleEntryStore {\n const entries = new Map<string, StoredEntry>();\n\n const sweep = (now: number): void => {\n for (const [ticket, record] of entries) {\n if (record.expiresAt <= now) entries.delete(ticket);\n }\n };\n\n const issueSync = (record: ConsoleEntryRecord, ttlMs: number = DEFAULT_TTL_MS): ConsoleEntryTicket => {\n // Il TTL negativo resta supportato per test e per il comportamento storico:\n // il ticket e immediatamente scaduto.\n const ttl = Math.trunc(ttlMs);\n const now = Date.now();\n sweep(now);\n\n while (entries.size >= maxEntries) {\n const oldest = entries.keys().next().value;\n if (oldest === undefined) break;\n entries.delete(oldest);\n }\n\n const ticket = createTicket();\n entries.set(ticket, { ...record, expiresAt: now + ttl });\n return { ticket, expiresInSeconds: Math.max(0, Math.ceil(ttl / 1000)) };\n };\n\n const consumeSync = (ticket: string): ConsoleEntryRecord | null => {\n if (typeof ticket !== 'string' || !TICKET_PATTERN.test(ticket)) {\n return null;\n }\n const record = entries.get(ticket);\n if (!record) return null;\n entries.delete(ticket);\n if (record.expiresAt <= Date.now()) return null;\n return { token: record.token, codiceUtente: record.codiceUtente };\n };\n\n return {\n issueSync,\n consumeSync,\n clear: () => entries.clear(),\n async issue(record, ttlMs) {\n return issueSync(record, ttlMs);\n },\n async consume(ticket) {\n return consumeSync(ticket);\n },\n };\n}\n\nconst defaultStore = createMemoryConsoleEntryStore();\n\n/** Store di default in memoria, usato quando `AccessiOptions.consoleEntry.store` non e configurato. */\nexport function getDefaultConsoleEntryStore(): ConsoleEntryStore {\n return defaultStore;\n}\n\n// Funzioni storiche sincrone: mantengono il comportamento per test e consumer esistenti.\nexport function issueConsoleEntryTicket(\n record: ConsoleEntryRecord,\n ttlMs: number = DEFAULT_TTL_MS,\n): ConsoleEntryTicket {\n return defaultStore.issueSync(record, ttlMs);\n}\n\nexport function consumeConsoleEntryTicket(ticket: unknown): ConsoleEntryRecord | null {\n return typeof ticket === 'string' ? defaultStore.consumeSync(ticket) : null;\n}\n\n/** Solo per i test: azzera lo store in memoria di default. */\nexport function clearConsoleEntryTickets(): void {\n defaultStore.clear();\n}\n\n/** Client Redis minimale: compatibile con ioredis (`redis.eval(script, numKeys, ...args)`). */\nexport interface ConsoleEntryRedisClient {\n eval(script: string, numKeys: number, ...args: Array<string | number>): Promise<unknown>;\n}\n\nexport interface RedisConsoleEntryStoreOptions {\n client: ConsoleEntryRedisClient;\n /** Segreto per cifrare il JWT nel Redis. Non usare il JWT in chiaro. */\n encryptionKey: string | Buffer;\n /** Default: `accessi:console-entry:`. */\n keyPrefix?: string;\n /** Tentativi in caso di collisione del ticket. Default: 5. */\n maxIssueAttempts?: number;\n}\n\nconst SET_SCRIPT = \"return redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2], 'NX')\";\nconst CONSUME_SCRIPT =\n \"local value = redis.call('GET', KEYS[1]) if value then redis.call('DEL', KEYS[1]) end return value\";\n\nfunction deriveEncryptionKey(secret: string | Buffer): Buffer {\n const source = Buffer.isBuffer(secret) ? secret : Buffer.from(secret, 'utf8');\n if (source.length === 0) {\n throw new Error('Redis console entry encryptionKey non configurata.');\n }\n return source.length === 32 ? source : createHash('sha256').update(source).digest();\n}\n\nfunction encryptRecord(record: ConsoleEntryRecord, key: Buffer): string {\n const iv = randomBytes(12);\n const cipher = createCipheriv('aes-256-gcm', key, iv);\n const ciphertext = Buffer.concat([\n cipher.update(JSON.stringify(record), 'utf8'),\n cipher.final(),\n ]);\n const tag = cipher.getAuthTag();\n return [iv, tag, ciphertext].map((part) => part.toString('base64url')).join('.');\n}\n\nfunction decryptRecord(value: string, key: Buffer): ConsoleEntryRecord | null {\n const [ivPart, tagPart, ciphertextPart] = value.split('.');\n if (!ivPart || !tagPart || !ciphertextPart) return null;\n try {\n const decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(ivPart, 'base64url'));\n decipher.setAuthTag(Buffer.from(tagPart, 'base64url'));\n const plaintext = Buffer.concat([\n decipher.update(Buffer.from(ciphertextPart, 'base64url')),\n decipher.final(),\n ]).toString('utf8');\n const parsed: unknown = JSON.parse(plaintext);\n if (\n !parsed ||\n typeof parsed !== 'object' ||\n typeof (parsed as ConsoleEntryRecord).token !== 'string' ||\n typeof (parsed as ConsoleEntryRecord).codiceUtente !== 'number'\n ) {\n return null;\n }\n return parsed as ConsoleEntryRecord;\n } catch {\n return null;\n }\n}\n\n/**\n * Store Redis per ticket console.\n *\n * - il JWT viene cifrato con AES-256-GCM prima di essere scritto;\n * - `SET ... PX ... NX` evita collisioni;\n * - il consume usa `GET` + `DEL` atomici in Lua, quindi il ticket e monouso\n * anche con piu istanze.\n */\nexport function createRedisConsoleEntryStore(\n options: RedisConsoleEntryStoreOptions,\n): ConsoleEntryStore {\n if (!options?.client || typeof options.client.eval !== 'function') {\n throw new Error('Redis console entry store: client.eval non configurato.');\n }\n\n const key = deriveEncryptionKey(options.encryptionKey);\n const keyPrefix = (options.keyPrefix ?? 'accessi:console-entry:').replace(/:+$/, ':');\n const maxAttempts = Math.max(1, options.maxIssueAttempts ?? 5);\n\n return {\n async issue(record, ttlMs) {\n const safeTtl = normalizeTtl(ttlMs);\n const value = encryptRecord(record, key);\n\n for (let attempt = 0; attempt < maxAttempts; attempt += 1) {\n const ticket = createTicket();\n const result = await options.client.eval(\n SET_SCRIPT,\n 1,\n `${keyPrefix}${ticket}`,\n value,\n safeTtl,\n );\n if (result) {\n return { ticket, expiresInSeconds: Math.ceil(safeTtl / 1000) };\n }\n }\n\n throw new Error('Impossibile emettere un ticket console univoco.');\n },\n\n async consume(ticket) {\n if (typeof ticket !== 'string' || !TICKET_PATTERN.test(ticket)) {\n return null;\n }\n const value = await options.client.eval(\n CONSUME_SCRIPT,\n 1,\n `${keyPrefix}${ticket}`,\n );\n if (typeof value !== 'string') return null;\n return decryptRecord(value, key);\n },\n };\n}\n"]}
@@ -0,0 +1,31 @@
1
+ import type { NextFunction, Request, Response } from 'express';
2
+ import type { RateLimitStore } from './rateLimitStore';
3
+ /** Opzioni del limite grossolano per IP applicato davanti alle rotte Accessi. */
4
+ export interface AccessiEdgeRateLimitOptions {
5
+ /** Default: true quando l'oggetto viene configurato. */
6
+ enabled?: boolean;
7
+ /** Finestra del limite. Default: 60000 ms. */
8
+ windowMs?: number;
9
+ /** Richieste consentite per IP nella finestra. Default: 300. */
10
+ maxAttempts?: number;
11
+ /** Store condiviso. Default: memoria. */
12
+ store?: RateLimitStore;
13
+ /** Prefisso delle chiavi. Default: `edge`. */
14
+ keyPrefix?: string;
15
+ /**
16
+ * Decide se applicare il limite alla richiesta.
17
+ *
18
+ * Default: `/api` e `/api/*`. Il bootstrap Accessi passa un matcher che include
19
+ * anche OpenAPI e JWKS.
20
+ */
21
+ match?: (path: string, req: Request) => boolean;
22
+ }
23
+ export type AccessiEdgeRateLimitMiddleware = (req: Request, res: Response, next: NextFunction) => Promise<void>;
24
+ /**
25
+ * Middleware edge per Accessi Service.
26
+ *
27
+ * Non sostituisce il rate limit applicativo per account: aggiunge una soglia
28
+ * grossolana per IP, utile contro bot e floods prima che la richiesta raggiunga
29
+ * controller, database o introspection.
30
+ */
31
+ export declare function createAccessiEdgeRateLimitMiddleware(options?: AccessiEdgeRateLimitOptions): AccessiEdgeRateLimitMiddleware | undefined;
@@ -0,0 +1,70 @@
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
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.createAccessiEdgeRateLimitMiddleware = createAccessiEdgeRateLimitMiddleware;
13
+ const rateLimitStore_1 = require("./rateLimitStore");
14
+ const DEFAULT_WINDOW_MS = 60000;
15
+ const DEFAULT_MAX_ATTEMPTS = 300;
16
+ function defaultMatch(path) {
17
+ return path === '/api' || path.startsWith('/api/');
18
+ }
19
+ /**
20
+ * Middleware edge per Accessi Service.
21
+ *
22
+ * Non sostituisce il rate limit applicativo per account: aggiunge una soglia
23
+ * grossolana per IP, utile contro bot e floods prima che la richiesta raggiunga
24
+ * controller, database o introspection.
25
+ */
26
+ function createAccessiEdgeRateLimitMiddleware(options = {}) {
27
+ var _a, _b, _c, _d, _e;
28
+ if (options.enabled === false)
29
+ return undefined;
30
+ const windowMs = Math.max(1, Math.trunc((_a = options.windowMs) !== null && _a !== void 0 ? _a : DEFAULT_WINDOW_MS));
31
+ const maxAttempts = Math.max(1, Math.trunc((_b = options.maxAttempts) !== null && _b !== void 0 ? _b : DEFAULT_MAX_ATTEMPTS));
32
+ const store = (_c = options.store) !== null && _c !== void 0 ? _c : (0, rateLimitStore_1.getDefaultRateLimitStore)();
33
+ const keyPrefix = ((_d = options.keyPrefix) !== null && _d !== void 0 ? _d : 'edge').replace(/\|+$/, '');
34
+ const match = (_e = options.match) !== null && _e !== void 0 ? _e : defaultMatch;
35
+ return (req, res, next) => __awaiter(this, void 0, void 0, function* () {
36
+ var _a, _b, _c;
37
+ if (!match(req.path, req)) {
38
+ next();
39
+ return;
40
+ }
41
+ const ip = (_c = (_a = req.ip) !== null && _a !== void 0 ? _a : (_b = req.socket) === null || _b === void 0 ? void 0 : _b.remoteAddress) !== null && _c !== void 0 ? _c : 'unknown';
42
+ try {
43
+ const decision = yield store.consume(`${keyPrefix}|ip|${ip}`, windowMs, maxAttempts);
44
+ if (!decision.allowed) {
45
+ res.setHeader('Retry-After', String(decision.retryAfterSeconds));
46
+ res.status(429).json({
47
+ severity: 'error',
48
+ status: 429,
49
+ statusCode: 2,
50
+ code: 'ACCESSI_EDGE_RATE_LIMITED',
51
+ error: 'ACCESSI_EDGE_RATE_LIMITED',
52
+ message: 'Troppe richieste. Riprova piu tardi.',
53
+ });
54
+ return;
55
+ }
56
+ next();
57
+ }
58
+ catch (_d) {
59
+ res.status(503).json({
60
+ severity: 'error',
61
+ status: 503,
62
+ statusCode: 2,
63
+ code: 'ACCESSI_RATE_LIMIT_UNAVAILABLE',
64
+ error: 'ACCESSI_RATE_LIMIT_UNAVAILABLE',
65
+ message: 'Rate limit non disponibile. Riprova piu tardi.',
66
+ });
67
+ }
68
+ });
69
+ }
70
+ //# sourceMappingURL=edgeRateLimit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edgeRateLimit.js","sourceRoot":"","sources":["../../../src/accessi-module/security/edgeRateLimit.ts"],"names":[],"mappings":";;;;;;;;;;;AA6CA,oFAgDC;AA5FD,qDAA4D;AA8B5D,MAAM,iBAAiB,GAAG,KAAM,CAAC;AACjC,MAAM,oBAAoB,GAAG,GAAG,CAAC;AAEjC,SAAS,YAAY,CAAC,IAAY;IAChC,OAAO,IAAI,KAAK,MAAM,IAAI,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;AACrD,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,oCAAoC,CAClD,UAAuC,EAAE;;IAEzC,IAAI,OAAO,CAAC,OAAO,KAAK,KAAK;QAAE,OAAO,SAAS,CAAC;IAEhD,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,MAAA,OAAO,CAAC,QAAQ,mCAAI,iBAAiB,CAAC,CAAC,CAAC;IAChF,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,MAAA,OAAO,CAAC,WAAW,mCAAI,oBAAoB,CAAC,CAAC,CAAC;IACzF,MAAM,KAAK,GAAG,MAAA,OAAO,CAAC,KAAK,mCAAI,IAAA,yCAAwB,GAAE,CAAC;IAC1D,MAAM,SAAS,GAAG,CAAC,MAAA,OAAO,CAAC,SAAS,mCAAI,MAAM,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACpE,MAAM,KAAK,GAAG,MAAA,OAAO,CAAC,KAAK,mCAAI,YAAY,CAAC;IAE5C,OAAO,CAAO,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAiB,EAAE;;QAC9E,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC;YAC1B,IAAI,EAAE,CAAC;YACP,OAAO;QACT,CAAC;QAED,MAAM,EAAE,GAAG,MAAA,MAAA,GAAG,CAAC,EAAE,mCAAI,MAAA,GAAG,CAAC,MAAM,0CAAE,aAAa,mCAAI,SAAS,CAAC;QAC5D,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,OAAO,CAClC,GAAG,SAAS,OAAO,EAAE,EAAE,EACvB,QAAQ,EACR,WAAW,CACZ,CAAC;YACF,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;gBACtB,GAAG,CAAC,SAAS,CAAC,aAAa,EAAE,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAAC,CAAC,CAAC;gBACjE,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;oBACnB,QAAQ,EAAE,OAAO;oBACjB,MAAM,EAAE,GAAG;oBACX,UAAU,EAAE,CAAC;oBACb,IAAI,EAAE,2BAA2B;oBACjC,KAAK,EAAE,2BAA2B;oBAClC,OAAO,EAAE,sCAAsC;iBAChD,CAAC,CAAC;gBACH,OAAO;YACT,CAAC;YACD,IAAI,EAAE,CAAC;QACT,CAAC;QAAC,WAAM,CAAC;YACP,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;gBACnB,QAAQ,EAAE,OAAO;gBACjB,MAAM,EAAE,GAAG;gBACX,UAAU,EAAE,CAAC;gBACb,IAAI,EAAE,gCAAgC;gBACtC,KAAK,EAAE,gCAAgC;gBACvC,OAAO,EAAE,gDAAgD;aAC1D,CAAC,CAAC;QACL,CAAC;IACH,CAAC,CAAA,CAAC;AACJ,CAAC","sourcesContent":["import type { NextFunction, Request, Response } from 'express';\nimport { getDefaultRateLimitStore } from './rateLimitStore';\nimport type { RateLimitStore } from './rateLimitStore';\n\n/** Opzioni del limite grossolano per IP applicato davanti alle rotte Accessi. */\nexport interface AccessiEdgeRateLimitOptions {\n /** Default: true quando l'oggetto viene configurato. */\n enabled?: boolean;\n /** Finestra del limite. Default: 60000 ms. */\n windowMs?: number;\n /** Richieste consentite per IP nella finestra. Default: 300. */\n maxAttempts?: number;\n /** Store condiviso. Default: memoria. */\n store?: RateLimitStore;\n /** Prefisso delle chiavi. Default: `edge`. */\n keyPrefix?: string;\n /**\n * Decide se applicare il limite alla richiesta.\n *\n * Default: `/api` e `/api/*`. Il bootstrap Accessi passa un matcher che include\n * anche OpenAPI e JWKS.\n */\n match?: (path: string, req: Request) => boolean;\n}\n\nexport type AccessiEdgeRateLimitMiddleware = (\n req: Request,\n res: Response,\n next: NextFunction,\n) => Promise<void>;\n\nconst DEFAULT_WINDOW_MS = 60_000;\nconst DEFAULT_MAX_ATTEMPTS = 300;\n\nfunction defaultMatch(path: string): boolean {\n return path === '/api' || path.startsWith('/api/');\n}\n\n/**\n * Middleware edge per Accessi Service.\n *\n * Non sostituisce il rate limit applicativo per account: aggiunge una soglia\n * grossolana per IP, utile contro bot e floods prima che la richiesta raggiunga\n * controller, database o introspection.\n */\nexport function createAccessiEdgeRateLimitMiddleware(\n options: AccessiEdgeRateLimitOptions = {},\n): AccessiEdgeRateLimitMiddleware | undefined {\n if (options.enabled === false) return undefined;\n\n const windowMs = Math.max(1, Math.trunc(options.windowMs ?? DEFAULT_WINDOW_MS));\n const maxAttempts = Math.max(1, Math.trunc(options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS));\n const store = options.store ?? getDefaultRateLimitStore();\n const keyPrefix = (options.keyPrefix ?? 'edge').replace(/\\|+$/, '');\n const match = options.match ?? defaultMatch;\n\n return async (req: Request, res: Response, next: NextFunction): Promise<void> => {\n if (!match(req.path, req)) {\n next();\n return;\n }\n\n const ip = req.ip ?? req.socket?.remoteAddress ?? 'unknown';\n try {\n const decision = await store.consume(\n `${keyPrefix}|ip|${ip}`,\n windowMs,\n maxAttempts,\n );\n if (!decision.allowed) {\n res.setHeader('Retry-After', String(decision.retryAfterSeconds));\n res.status(429).json({\n severity: 'error',\n status: 429,\n statusCode: 2,\n code: 'ACCESSI_EDGE_RATE_LIMITED',\n error: 'ACCESSI_EDGE_RATE_LIMITED',\n message: 'Troppe richieste. Riprova piu tardi.',\n });\n return;\n }\n next();\n } catch {\n res.status(503).json({\n severity: 'error',\n status: 503,\n statusCode: 2,\n code: 'ACCESSI_RATE_LIMIT_UNAVAILABLE',\n error: 'ACCESSI_RATE_LIMIT_UNAVAILABLE',\n message: 'Rate limit non disponibile. Riprova piu tardi.',\n });\n }\n };\n}\n"]}