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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/dist/accessi/resource-server/errors.d.ts +1 -1
  2. package/dist/accessi/resource-server/errors.js.map +1 -1
  3. package/dist/accessi/resource-server/express.js +21 -0
  4. package/dist/accessi/resource-server/express.js.map +1 -1
  5. package/dist/accessi/resource-server/nest.js +13 -0
  6. package/dist/accessi/resource-server/nest.js.map +1 -1
  7. package/dist/accessi/resource-server/runtime.js +11 -1
  8. package/dist/accessi/resource-server/runtime.js.map +1 -1
  9. package/dist/accessi/resource-server/types.d.ts +7 -0
  10. package/dist/accessi/resource-server/types.js.map +1 -1
  11. package/dist/accessi/server/index.d.ts +1 -0
  12. package/dist/accessi/server/index.js +1 -0
  13. package/dist/accessi/server/index.js.map +1 -1
  14. package/dist/accessi/server/standalone.d.ts +41 -0
  15. package/dist/accessi/server/standalone.js +264 -0
  16. package/dist/accessi/server/standalone.js.map +1 -0
  17. package/dist/accessi-module/AccessiModule.d.ts +26 -1
  18. package/dist/accessi-module/AccessiModule.js +5 -2
  19. package/dist/accessi-module/AccessiModule.js.map +1 -1
  20. package/dist/accessi-module/Console/accessiFetch.d.ts +5 -0
  21. package/dist/accessi-module/Console/accessiFetch.js +7 -4
  22. package/dist/accessi-module/Console/accessiFetch.js.map +1 -1
  23. package/dist/accessi-module/Console/console.js +12 -4
  24. package/dist/accessi-module/Console/generated/emilsoftware-utilities.d.ts +31 -1
  25. package/dist/accessi-module/Console/generated/emilsoftware-utilities.js +28 -3
  26. package/dist/accessi-module/Console/generated/emilsoftware-utilities.js.map +1 -1
  27. package/dist/accessi-module/Console/generated/model/exchangeFederatedIdentityRequest.d.ts +12 -0
  28. package/dist/accessi-module/Console/generated/model/exchangeFederatedIdentityRequest.js +9 -0
  29. package/dist/accessi-module/Console/generated/model/exchangeFederatedIdentityRequest.js.map +1 -0
  30. package/dist/accessi-module/Console/generated/model/federatedAuthenticationResultDto.d.ts +15 -0
  31. package/dist/accessi-module/Console/generated/model/federatedAuthenticationResultDto.js +3 -0
  32. package/dist/accessi-module/Console/generated/model/federatedAuthenticationResultDto.js.map +1 -0
  33. package/dist/accessi-module/Console/generated/model/federatedExchangeResponse.d.ts +14 -0
  34. package/dist/accessi-module/Console/generated/model/federatedExchangeResponse.js +3 -0
  35. package/dist/accessi-module/Console/generated/model/federatedExchangeResponse.js.map +1 -0
  36. package/dist/accessi-module/Console/generated/model/index.d.ts +6 -0
  37. package/dist/accessi-module/Console/generated/model/index.js +6 -0
  38. package/dist/accessi-module/Console/generated/model/index.js.map +1 -1
  39. package/dist/accessi-module/Console/generated/model/loginResult.d.ts +2 -0
  40. package/dist/accessi-module/Console/generated/model/loginResult.js.map +1 -1
  41. package/dist/accessi-module/Console/generated/model/refreshTokenRequest.d.ts +10 -0
  42. package/dist/accessi-module/Console/generated/model/refreshTokenRequest.js +9 -0
  43. package/dist/accessi-module/Console/generated/model/refreshTokenRequest.js.map +1 -0
  44. package/dist/accessi-module/Console/generated/model/refreshTokenResponse.d.ts +14 -0
  45. package/dist/accessi-module/Console/generated/model/refreshTokenResponse.js +3 -0
  46. package/dist/accessi-module/Console/generated/model/refreshTokenResponse.js.map +1 -0
  47. package/dist/accessi-module/Console/generated/model/refreshTokenResult.d.ts +13 -0
  48. package/dist/accessi-module/Console/generated/model/refreshTokenResult.js +3 -0
  49. package/dist/accessi-module/Console/generated/model/refreshTokenResult.js.map +1 -0
  50. package/dist/accessi-module/Console/wiki.js +8 -1
  51. package/dist/accessi-module/Console/wiki.js.map +1 -1
  52. package/dist/accessi-module/Controllers/AccessiConsoleController.d.ts +6 -1
  53. package/dist/accessi-module/Controllers/AccessiConsoleController.js +24 -1
  54. package/dist/accessi-module/Controllers/AccessiConsoleController.js.map +1 -1
  55. package/dist/accessi-module/Controllers/AuthController.d.ts +3 -1
  56. package/dist/accessi-module/Controllers/AuthController.js +84 -2
  57. package/dist/accessi-module/Controllers/AuthController.js.map +1 -1
  58. package/dist/accessi-module/Dtos/FederatedExchangeDtos.d.ts +21 -0
  59. package/dist/accessi-module/Dtos/FederatedExchangeDtos.js +74 -0
  60. package/dist/accessi-module/Dtos/FederatedExchangeDtos.js.map +1 -0
  61. package/dist/accessi-module/Dtos/LoginResponse.d.ts +1 -0
  62. package/dist/accessi-module/Dtos/LoginResponse.js +6 -0
  63. package/dist/accessi-module/Dtos/LoginResponse.js.map +1 -1
  64. package/dist/accessi-module/Dtos/RefreshTokenDtos.d.ts +12 -0
  65. package/dist/accessi-module/Dtos/RefreshTokenDtos.js +50 -0
  66. package/dist/accessi-module/Dtos/RefreshTokenDtos.js.map +1 -0
  67. package/dist/accessi-module/Dtos/index.d.ts +2 -0
  68. package/dist/accessi-module/Dtos/index.js +2 -0
  69. package/dist/accessi-module/Dtos/index.js.map +1 -1
  70. package/dist/accessi-module/Services/AuthService/AuthService.d.ts +14 -0
  71. package/dist/accessi-module/Services/AuthService/AuthService.js +99 -6
  72. package/dist/accessi-module/Services/AuthService/AuthService.js.map +1 -1
  73. package/dist/accessi-module/database-updates/accessiOptionsFromEnv.js +1 -1
  74. package/dist/accessi-module/database-updates/accessiOptionsFromEnv.js.map +1 -1
  75. package/dist/accessi-module/federated-auth/FederatedAuthExchangeController.d.ts +15 -0
  76. package/dist/accessi-module/federated-auth/FederatedAuthExchangeController.js +85 -0
  77. package/dist/accessi-module/federated-auth/FederatedAuthExchangeController.js.map +1 -0
  78. package/dist/accessi-module/federated-auth/FederatedAuthService.js +7 -1
  79. package/dist/accessi-module/federated-auth/FederatedAuthService.js.map +1 -1
  80. package/dist/accessi-module/index.d.ts +8 -0
  81. package/dist/accessi-module/index.js +40 -4
  82. package/dist/accessi-module/index.js.map +1 -1
  83. package/dist/accessi-module/jwt/jwt.strategy.js +4 -0
  84. package/dist/accessi-module/jwt/jwt.strategy.js.map +1 -1
  85. package/dist/accessi-module/middleware/authenticateGen.js +4 -0
  86. package/dist/accessi-module/middleware/authenticateGen.js.map +1 -1
  87. package/dist/accessi-module/security/audit.d.ts +7 -0
  88. package/dist/accessi-module/security/audit.js +14 -0
  89. package/dist/accessi-module/security/audit.js.map +1 -0
  90. package/dist/accessi-module/security/authenticatedToken.d.ts +1 -1
  91. package/dist/accessi-module/security/authenticatedToken.js +3 -3
  92. package/dist/accessi-module/security/authenticatedToken.js.map +1 -1
  93. package/dist/accessi-module/security/consoleEntry.d.ts +2 -0
  94. package/dist/accessi-module/security/consoleEntry.js +16 -0
  95. package/dist/accessi-module/security/consoleEntry.js.map +1 -1
  96. package/dist/accessi-module/security/edgeRateLimit.d.ts +2 -0
  97. package/dist/accessi-module/security/edgeRateLimit.js +11 -0
  98. package/dist/accessi-module/security/edgeRateLimit.js.map +1 -1
  99. package/dist/accessi-module/security/jwksBootstrap.d.ts +15 -0
  100. package/dist/accessi-module/security/jwksBootstrap.js +44 -0
  101. package/dist/accessi-module/security/jwksBootstrap.js.map +1 -0
  102. package/dist/accessi-module/security/jwtTokenOptions.d.ts +9 -2
  103. package/dist/accessi-module/security/jwtTokenOptions.js +18 -4
  104. package/dist/accessi-module/security/jwtTokenOptions.js.map +1 -1
  105. package/dist/accessi-module/security/passwordResetToken.d.ts +1 -1
  106. package/dist/accessi-module/security/passwordResetToken.js.map +1 -1
  107. package/dist/accessi-module/security/publicAuthRateLimit.d.ts +1 -1
  108. package/dist/accessi-module/security/publicAuthRateLimit.js +1 -0
  109. package/dist/accessi-module/security/publicAuthRateLimit.js.map +1 -1
  110. package/dist/accessi-module/security/rateLimitStore.d.ts +2 -0
  111. package/dist/accessi-module/security/rateLimitStore.js +16 -0
  112. package/dist/accessi-module/security/rateLimitStore.js.map +1 -1
  113. package/dist/accessi-module/security/refreshTokenRotation.d.ts +40 -0
  114. package/dist/accessi-module/security/refreshTokenRotation.js +91 -0
  115. package/dist/accessi-module/security/refreshTokenRotation.js.map +1 -0
  116. package/dist/accessi-module/security/sessionRevocation.d.ts +52 -0
  117. package/dist/accessi-module/security/sessionRevocation.js +124 -0
  118. package/dist/accessi-module/security/sessionRevocation.js.map +1 -0
  119. package/dist/accessi-module/security/validateOptions.d.ts +19 -0
  120. package/dist/accessi-module/security/validateOptions.js +115 -0
  121. package/dist/accessi-module/security/validateOptions.js.map +1 -0
  122. package/dist/emilsoftware-utilities-2.0.0-dev.69.tgz +0 -0
  123. package/dist/initEmilsoftwareModule.js +22 -4
  124. package/dist/initEmilsoftwareModule.js.map +1 -1
  125. package/dist/scripts/startAccessiService.d.ts +1 -0
  126. package/dist/scripts/startAccessiService.js +11 -0
  127. package/dist/scripts/startAccessiService.js.map +1 -0
  128. package/docs/accessi-openapi-fingerprint.json +190 -0
  129. package/docs/standalone-service.md +176 -0
  130. package/integration-prompt.md +160 -0
  131. package/package.json +9 -4
  132. package/dist/emilsoftware-utilities-2.0.0-dev.68.tgz +0 -0
@@ -1 +1 @@
1
- {"version":3,"file":"rateLimitStore.js","sourceRoot":"","sources":["../../../src/accessi-module/security/rateLimitStore.ts"],"names":[],"mappings":"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","sourcesContent":["/** Esito di un controllo di rate limit. */\nexport interface RateLimitDecision {\n allowed: boolean;\n retryAfterSeconds: number;\n}\n\n/** Store pluggable per rate limit: memoria per singola istanza, Redis per cluster. */\nexport interface RateLimitStore {\n consume(key: string, windowMs: number, maxAttempts: number): Promise<RateLimitDecision>;\n}\n\nexport interface MemoryRateLimitStore extends RateLimitStore {\n reset(): void;\n}\n\ninterface RateLimitBucket {\n count: number;\n resetAt: number;\n lastSeenAt: number;\n}\n\nconst DEFAULT_MAX_BUCKETS = 20_000;\n\nfunction normalizeLimits(windowMs: number, maxAttempts: number): { windowMs: number; maxAttempts: number } {\n return {\n windowMs: Math.max(1, Math.trunc(windowMs)),\n maxAttempts: Math.max(1, Math.trunc(maxAttempts)),\n };\n}\n\n/** Store in memoria con finestra fissa e limite di bucket. */\nexport function createMemoryRateLimitStore(\n maxBuckets: number = DEFAULT_MAX_BUCKETS,\n): MemoryRateLimitStore {\n const buckets = new Map<string, RateLimitBucket>();\n const safeMaxBuckets = Math.max(1, Math.trunc(maxBuckets));\n\n const prune = (now: number): void => {\n if (buckets.size < safeMaxBuckets) return;\n for (const [key, bucket] of buckets) {\n if (bucket.resetAt <= now) buckets.delete(key);\n }\n };\n\n const enforceCap = (): void => {\n while (buckets.size >= safeMaxBuckets) {\n let oldestKey: string | undefined;\n let oldestSeen = Number.POSITIVE_INFINITY;\n for (const [key, bucket] of buckets) {\n if (bucket.lastSeenAt < oldestSeen) {\n oldestSeen = bucket.lastSeenAt;\n oldestKey = key;\n }\n }\n if (oldestKey === undefined) return;\n buckets.delete(oldestKey);\n }\n };\n\n return {\n async consume(key: string, windowMs: number, maxAttempts: number): Promise<RateLimitDecision> {\n const limits = normalizeLimits(windowMs, maxAttempts);\n const now = Date.now();\n prune(now);\n\n const existing = buckets.get(key);\n if (!existing || existing.resetAt <= now) {\n enforceCap();\n buckets.set(key, {\n count: 1,\n resetAt: now + limits.windowMs,\n lastSeenAt: now,\n });\n return { allowed: true, retryAfterSeconds: 0 };\n }\n\n existing.lastSeenAt = now;\n if (existing.count >= limits.maxAttempts) {\n return {\n allowed: false,\n retryAfterSeconds: Math.max(1, Math.ceil((existing.resetAt - now) / 1000)),\n };\n }\n\n existing.count += 1;\n return { allowed: true, retryAfterSeconds: 0 };\n },\n reset() {\n buckets.clear();\n },\n };\n}\n\nconst defaultStore = createMemoryRateLimitStore();\n\n/** Store di default in memoria, usato quando non viene configurato uno store condiviso. */\nexport function getDefaultRateLimitStore(): MemoryRateLimitStore {\n return defaultStore;\n}\n\n/** Client Redis minimale: compatibile con ioredis (`redis.eval(script, numKeys, ...args)`). */\nexport interface RateLimitRedisClient {\n eval(script: string, numKeys: number, ...args: Array<string | number>): Promise<unknown>;\n}\n\nexport interface RedisRateLimitStoreOptions {\n client: RateLimitRedisClient;\n /** Default: `accessi:rate:`. */\n keyPrefix?: string;\n}\n\nconst REDIS_SCRIPT = `\nlocal current = redis.call('INCR', KEYS[1])\nif current == 1 then\n redis.call('PEXPIRE', KEYS[1], ARGV[1])\nend\nlocal ttl = redis.call('PTTL', KEYS[1])\nif current > tonumber(ARGV[2]) then\n return {0, ttl}\nend\nreturn {1, ttl}\n`;\n\n/**\n * Store Redis per rate limit distribuito.\n *\n * Usa `INCR` + `PEXPIRE` atomici. Adatto a login, reset password, 2FA,\n * console entry e service token in deployment multi-istanza.\n */\nexport function createRedisRateLimitStore(\n options: RedisRateLimitStoreOptions,\n): RateLimitStore {\n if (!options?.client || typeof options.client.eval !== 'function') {\n throw new Error('Redis rate limit store: client.eval non configurato.');\n }\n\n const keyPrefix = (options.keyPrefix ?? 'accessi:rate:').replace(/:+$/, ':');\n\n return {\n async consume(key, windowMs, maxAttempts) {\n const limits = normalizeLimits(windowMs, maxAttempts);\n const raw = await options.client.eval(\n REDIS_SCRIPT,\n 1,\n `${keyPrefix}${key}`,\n limits.windowMs,\n limits.maxAttempts,\n );\n\n if (!Array.isArray(raw) || raw.length < 2) {\n throw new Error('Redis rate limit store: risposta non valida.');\n }\n\n const allowed = Number(raw[0]) === 1;\n const ttlMs = Number(raw[1]);\n return {\n allowed,\n retryAfterSeconds: allowed\n ? 0\n : Math.max(1, Number.isFinite(ttlMs) && ttlMs > 0 ? Math.ceil(ttlMs / 1000) : 1),\n };\n },\n };\n}\n"]}
1
+ {"version":3,"file":"rateLimitStore.js","sourceRoot":"","sources":["../../../src/accessi-module/security/rateLimitStore.ts"],"names":[],"mappings":"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","sourcesContent":["/** Esito di un controllo di rate limit. */\nexport interface RateLimitDecision {\n allowed: boolean;\n retryAfterSeconds: number;\n}\n\n/** Store pluggable per rate limit: memoria per singola istanza, Redis per cluster. */\nexport interface RateLimitStore {\n consume(key: string, windowMs: number, maxAttempts: number): Promise<RateLimitDecision>;\n /** Health check opzionale per readiness del servizio. */\n healthCheck?(): Promise<boolean>;\n}\n\nexport interface MemoryRateLimitStore extends RateLimitStore {\n reset(): void;\n}\n\ninterface RateLimitBucket {\n count: number;\n resetAt: number;\n lastSeenAt: number;\n}\n\nconst DEFAULT_MAX_BUCKETS = 20_000;\n\nfunction normalizeLimits(windowMs: number, maxAttempts: number): { windowMs: number; maxAttempts: number } {\n return {\n windowMs: Math.max(1, Math.trunc(windowMs)),\n maxAttempts: Math.max(1, Math.trunc(maxAttempts)),\n };\n}\n\n/** Store in memoria con finestra fissa e limite di bucket. */\nexport function createMemoryRateLimitStore(\n maxBuckets: number = DEFAULT_MAX_BUCKETS,\n): MemoryRateLimitStore {\n const buckets = new Map<string, RateLimitBucket>();\n const safeMaxBuckets = Math.max(1, Math.trunc(maxBuckets));\n\n const prune = (now: number): void => {\n if (buckets.size < safeMaxBuckets) return;\n for (const [key, bucket] of buckets) {\n if (bucket.resetAt <= now) buckets.delete(key);\n }\n };\n\n const enforceCap = (): void => {\n while (buckets.size >= safeMaxBuckets) {\n let oldestKey: string | undefined;\n let oldestSeen = Number.POSITIVE_INFINITY;\n for (const [key, bucket] of buckets) {\n if (bucket.lastSeenAt < oldestSeen) {\n oldestSeen = bucket.lastSeenAt;\n oldestKey = key;\n }\n }\n if (oldestKey === undefined) return;\n buckets.delete(oldestKey);\n }\n };\n\n return {\n async consume(key: string, windowMs: number, maxAttempts: number): Promise<RateLimitDecision> {\n const limits = normalizeLimits(windowMs, maxAttempts);\n const now = Date.now();\n prune(now);\n\n const existing = buckets.get(key);\n if (!existing || existing.resetAt <= now) {\n enforceCap();\n buckets.set(key, {\n count: 1,\n resetAt: now + limits.windowMs,\n lastSeenAt: now,\n });\n return { allowed: true, retryAfterSeconds: 0 };\n }\n\n existing.lastSeenAt = now;\n if (existing.count >= limits.maxAttempts) {\n return {\n allowed: false,\n retryAfterSeconds: Math.max(1, Math.ceil((existing.resetAt - now) / 1000)),\n };\n }\n\n existing.count += 1;\n return { allowed: true, retryAfterSeconds: 0 };\n },\n reset() {\n buckets.clear();\n },\n async healthCheck() {\n return true;\n },\n };\n}\n\nconst defaultStore = createMemoryRateLimitStore();\n\n/** Store di default in memoria, usato quando non viene configurato uno store condiviso. */\nexport function getDefaultRateLimitStore(): MemoryRateLimitStore {\n return defaultStore;\n}\n\n/** Client Redis minimale: compatibile con ioredis (`redis.eval(script, numKeys, ...args)`). */\nexport interface RateLimitRedisClient {\n eval(script: string, numKeys: number, ...args: Array<string | number>): Promise<unknown>;\n}\n\nexport interface RedisRateLimitStoreOptions {\n client: RateLimitRedisClient;\n /** Default: `accessi:rate:`. */\n keyPrefix?: string;\n}\n\nconst REDIS_SCRIPT = `\nlocal current = redis.call('INCR', KEYS[1])\nif current == 1 then\n redis.call('PEXPIRE', KEYS[1], ARGV[1])\nend\nlocal ttl = redis.call('PTTL', KEYS[1])\nif current > tonumber(ARGV[2]) then\n return {0, ttl}\nend\nreturn {1, ttl}\n`;\n\n/**\n * Store Redis per rate limit distribuito.\n *\n * Usa `INCR` + `PEXPIRE` atomici. Adatto a login, reset password, 2FA,\n * console entry e service token in deployment multi-istanza.\n */\nexport function createRedisRateLimitStore(\n options: RedisRateLimitStoreOptions,\n): RateLimitStore {\n if (!options?.client || typeof options.client.eval !== 'function') {\n throw new Error('Redis rate limit store: client.eval non configurato.');\n }\n\n const keyPrefix = (options.keyPrefix ?? 'accessi:rate:').replace(/:+$/, ':');\n\n return {\n async consume(key, windowMs, maxAttempts) {\n const limits = normalizeLimits(windowMs, maxAttempts);\n const raw = await options.client.eval(\n REDIS_SCRIPT,\n 1,\n `${keyPrefix}${key}`,\n limits.windowMs,\n limits.maxAttempts,\n );\n\n if (!Array.isArray(raw) || raw.length < 2) {\n throw new Error('Redis rate limit store: risposta non valida.');\n }\n\n const allowed = Number(raw[0]) === 1;\n const ttlMs = Number(raw[1]);\n return {\n allowed,\n retryAfterSeconds: allowed\n ? 0\n : Math.max(1, Number.isFinite(ttlMs) && ttlMs > 0 ? Math.ceil(ttlMs / 1000) : 1),\n };\n },\n async healthCheck() {\n try {\n const result = await options.client.eval(\"return redis.call('PING')\", 0);\n return String(result).toUpperCase() === 'PONG';\n } catch {\n return false;\n }\n },\n };\n}\n"]}
@@ -0,0 +1,40 @@
1
+ import type { AccessiOptions } from '../AccessiModule';
2
+ /**
3
+ * Store di rotazione refresh token.
4
+ *
5
+ * `consume` e atomico: restituisce true una sola volta per `jti`, poi il token
6
+ * e considerato usato fino alla sua scadenza. In multi-istanza usare Redis.
7
+ */
8
+ export interface RefreshTokenRotationStore {
9
+ consume(jti: string, expiresAtMs: number): Promise<boolean>;
10
+ /** Health check opzionale per readiness del servizio. */
11
+ healthCheck?(): Promise<boolean>;
12
+ }
13
+ export interface RefreshTokenRotationOptions {
14
+ /** Default: disabilitata se non viene fornito uno store. */
15
+ enabled?: boolean;
16
+ /** Store condiviso (es. Redis) per il monouso tra istanze. */
17
+ store?: RefreshTokenRotationStore;
18
+ }
19
+ export interface MemoryRefreshTokenRotationStore extends RefreshTokenRotationStore {
20
+ clear(): void;
21
+ }
22
+ export declare function createMemoryRefreshTokenRotationStore(): MemoryRefreshTokenRotationStore;
23
+ export interface RefreshTokenRotationRedisClient {
24
+ eval(script: string, numKeys: number, ...args: Array<string | number>): Promise<unknown>;
25
+ }
26
+ export interface RedisRefreshTokenRotationStoreOptions {
27
+ client: RefreshTokenRotationRedisClient;
28
+ /** Default: `accessi:refresh-used:`. */
29
+ keyPrefix?: string;
30
+ }
31
+ /**
32
+ * Store Redis per refresh token monouso.
33
+ *
34
+ * Usa `SET ... PX ... NX` atomico: due refresh concorrenti dello stesso token
35
+ * producono un solo successo.
36
+ */
37
+ export declare function createRedisRefreshTokenRotationStore(options: RedisRefreshTokenRotationStoreOptions): RefreshTokenRotationStore;
38
+ export declare function getRefreshTokenRotationStore(options: AccessiOptions): RefreshTokenRotationStore | undefined;
39
+ /** Solo per test: azzera lo store in memoria di default. */
40
+ export declare function clearDefaultRefreshTokenRotations(): void;
@@ -0,0 +1,91 @@
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.createMemoryRefreshTokenRotationStore = createMemoryRefreshTokenRotationStore;
13
+ exports.createRedisRefreshTokenRotationStore = createRedisRefreshTokenRotationStore;
14
+ exports.getRefreshTokenRotationStore = getRefreshTokenRotationStore;
15
+ exports.clearDefaultRefreshTokenRotations = clearDefaultRefreshTokenRotations;
16
+ const CONSUME_SCRIPT = `
17
+ local ttl = tonumber(ARGV[2])
18
+ if not ttl or ttl <= 0 then return nil end
19
+ return redis.call('SET', KEYS[1], '1', 'PX', ttl, 'NX')
20
+ `;
21
+ function createMemoryRefreshTokenRotationStore() {
22
+ const used = new Map();
23
+ return {
24
+ consume(jti, expiresAtMs) {
25
+ return __awaiter(this, void 0, void 0, function* () {
26
+ const now = Date.now();
27
+ const existing = used.get(jti);
28
+ if (existing !== undefined && existing > now) {
29
+ return false;
30
+ }
31
+ used.set(jti, expiresAtMs);
32
+ return true;
33
+ });
34
+ },
35
+ healthCheck() {
36
+ return __awaiter(this, void 0, void 0, function* () {
37
+ return true;
38
+ });
39
+ },
40
+ clear() {
41
+ used.clear();
42
+ },
43
+ };
44
+ }
45
+ /**
46
+ * Store Redis per refresh token monouso.
47
+ *
48
+ * Usa `SET ... PX ... NX` atomico: due refresh concorrenti dello stesso token
49
+ * producono un solo successo.
50
+ */
51
+ function createRedisRefreshTokenRotationStore(options) {
52
+ var _a;
53
+ if (!(options === null || options === void 0 ? void 0 : options.client) || typeof options.client.eval !== 'function') {
54
+ throw new Error('Redis refresh rotation store: client.eval non configurato.');
55
+ }
56
+ const keyPrefix = ((_a = options.keyPrefix) !== null && _a !== void 0 ? _a : 'accessi:refresh-used:').replace(/:+$/, ':');
57
+ return {
58
+ consume(jti, expiresAtMs) {
59
+ return __awaiter(this, void 0, void 0, function* () {
60
+ const ttlMs = Math.max(1, Math.trunc(expiresAtMs - Date.now()));
61
+ const result = yield options.client.eval(CONSUME_SCRIPT, 1, `${keyPrefix}${jti}`, ttlMs);
62
+ return result !== null && result !== undefined && result !== false;
63
+ });
64
+ },
65
+ healthCheck() {
66
+ return __awaiter(this, void 0, void 0, function* () {
67
+ try {
68
+ const result = yield options.client.eval("return redis.call('PING')", 0);
69
+ return String(result).toUpperCase() === 'PONG';
70
+ }
71
+ catch (_a) {
72
+ return false;
73
+ }
74
+ });
75
+ },
76
+ };
77
+ }
78
+ const defaultMemoryStore = createMemoryRefreshTokenRotationStore();
79
+ function getRefreshTokenRotationStore(options) {
80
+ var _a, _b, _c;
81
+ if (((_a = options.refreshTokenRotation) === null || _a === void 0 ? void 0 : _a.enabled) === false)
82
+ return undefined;
83
+ if ((_b = options.refreshTokenRotation) === null || _b === void 0 ? void 0 : _b.store)
84
+ return options.refreshTokenRotation.store;
85
+ return ((_c = options.refreshTokenRotation) === null || _c === void 0 ? void 0 : _c.enabled) === true ? defaultMemoryStore : undefined;
86
+ }
87
+ /** Solo per test: azzera lo store in memoria di default. */
88
+ function clearDefaultRefreshTokenRotations() {
89
+ defaultMemoryStore.clear();
90
+ }
91
+ //# sourceMappingURL=refreshTokenRotation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refreshTokenRotation.js","sourceRoot":"","sources":["../../../src/accessi-module/security/refreshTokenRotation.ts"],"names":[],"mappings":";;;;;;;;;;;AA+BA,sFAmBC;AAkBD,oFA6BC;AAID,oEAMC;AAGD,8EAEC;AAvFD,MAAM,cAAc,GAAG;;;;CAItB,CAAC;AAEF,SAAgB,qCAAqC;IACnD,MAAM,IAAI,GAAG,IAAI,GAAG,EAAkB,CAAC;IACvC,OAAO;QACC,OAAO,CAAC,GAAG,EAAE,WAAW;;gBAC5B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;gBACvB,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;gBAC/B,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,GAAG,GAAG,EAAE,CAAC;oBAC7C,OAAO,KAAK,CAAC;gBACf,CAAC;gBACD,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;gBAC3B,OAAO,IAAI,CAAC;YACd,CAAC;SAAA;QACK,WAAW;;gBACf,OAAO,IAAI,CAAC;YACd,CAAC;SAAA;QACD,KAAK;YACH,IAAI,CAAC,KAAK,EAAE,CAAC;QACf,CAAC;KACF,CAAC;AACJ,CAAC;AAYD;;;;;GAKG;AACH,SAAgB,oCAAoC,CAClD,OAA8C;;IAE9C,IAAI,CAAC,CAAA,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,MAAM,CAAA,IAAI,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;QAClE,MAAM,IAAI,KAAK,CAAC,4DAA4D,CAAC,CAAC;IAChF,CAAC;IAED,MAAM,SAAS,GAAG,CAAC,MAAA,OAAO,CAAC,SAAS,mCAAI,uBAAuB,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IAErF,OAAO;QACC,OAAO,CAAC,GAAG,EAAE,WAAW;;gBAC5B,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;gBAChE,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,IAAI,CACtC,cAAc,EACd,CAAC,EACD,GAAG,SAAS,GAAG,GAAG,EAAE,EACpB,KAAK,CACN,CAAC;gBACF,OAAO,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,KAAK,CAAC;YACrE,CAAC;SAAA;QACK,WAAW;;gBACf,IAAI,CAAC;oBACH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,2BAA2B,EAAE,CAAC,CAAC,CAAC;oBACzE,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,WAAW,EAAE,KAAK,MAAM,CAAC;gBACjD,CAAC;gBAAC,WAAM,CAAC;oBACP,OAAO,KAAK,CAAC;gBACf,CAAC;YACH,CAAC;SAAA;KACF,CAAC;AACJ,CAAC;AAED,MAAM,kBAAkB,GAAG,qCAAqC,EAAE,CAAC;AAEnE,SAAgB,4BAA4B,CAC1C,OAAuB;;IAEvB,IAAI,CAAA,MAAA,OAAO,CAAC,oBAAoB,0CAAE,OAAO,MAAK,KAAK;QAAE,OAAO,SAAS,CAAC;IACtE,IAAI,MAAA,OAAO,CAAC,oBAAoB,0CAAE,KAAK;QAAE,OAAO,OAAO,CAAC,oBAAoB,CAAC,KAAK,CAAC;IACnF,OAAO,CAAA,MAAA,OAAO,CAAC,oBAAoB,0CAAE,OAAO,MAAK,IAAI,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,SAAS,CAAC;AACzF,CAAC;AAED,4DAA4D;AAC5D,SAAgB,iCAAiC;IAC/C,kBAAkB,CAAC,KAAK,EAAE,CAAC;AAC7B,CAAC","sourcesContent":["import type { AccessiOptions } from '../AccessiModule';\n\n/**\n * Store di rotazione refresh token.\n *\n * `consume` e atomico: restituisce true una sola volta per `jti`, poi il token\n * e considerato usato fino alla sua scadenza. In multi-istanza usare Redis.\n */\nexport interface RefreshTokenRotationStore {\n consume(jti: string, expiresAtMs: number): Promise<boolean>;\n /** Health check opzionale per readiness del servizio. */\n healthCheck?(): Promise<boolean>;\n}\n\nexport interface RefreshTokenRotationOptions {\n /** Default: disabilitata se non viene fornito uno store. */\n enabled?: boolean;\n /** Store condiviso (es. Redis) per il monouso tra istanze. */\n store?: RefreshTokenRotationStore;\n}\n\nexport interface MemoryRefreshTokenRotationStore extends RefreshTokenRotationStore {\n clear(): void;\n}\n\nconst CONSUME_SCRIPT = `\nlocal ttl = tonumber(ARGV[2])\nif not ttl or ttl <= 0 then return nil end\nreturn redis.call('SET', KEYS[1], '1', 'PX', ttl, 'NX')\n`;\n\nexport function createMemoryRefreshTokenRotationStore(): MemoryRefreshTokenRotationStore {\n const used = new Map<string, number>();\n return {\n async consume(jti, expiresAtMs) {\n const now = Date.now();\n const existing = used.get(jti);\n if (existing !== undefined && existing > now) {\n return false;\n }\n used.set(jti, expiresAtMs);\n return true;\n },\n async healthCheck() {\n return true;\n },\n clear() {\n used.clear();\n },\n };\n}\n\nexport interface RefreshTokenRotationRedisClient {\n eval(script: string, numKeys: number, ...args: Array<string | number>): Promise<unknown>;\n}\n\nexport interface RedisRefreshTokenRotationStoreOptions {\n client: RefreshTokenRotationRedisClient;\n /** Default: `accessi:refresh-used:`. */\n keyPrefix?: string;\n}\n\n/**\n * Store Redis per refresh token monouso.\n *\n * Usa `SET ... PX ... NX` atomico: due refresh concorrenti dello stesso token\n * producono un solo successo.\n */\nexport function createRedisRefreshTokenRotationStore(\n options: RedisRefreshTokenRotationStoreOptions,\n): RefreshTokenRotationStore {\n if (!options?.client || typeof options.client.eval !== 'function') {\n throw new Error('Redis refresh rotation store: client.eval non configurato.');\n }\n\n const keyPrefix = (options.keyPrefix ?? 'accessi:refresh-used:').replace(/:+$/, ':');\n\n return {\n async consume(jti, expiresAtMs) {\n const ttlMs = Math.max(1, Math.trunc(expiresAtMs - Date.now()));\n const result = await options.client.eval(\n CONSUME_SCRIPT,\n 1,\n `${keyPrefix}${jti}`,\n ttlMs,\n );\n return result !== null && result !== undefined && result !== false;\n },\n async healthCheck() {\n try {\n const result = await options.client.eval(\"return redis.call('PING')\", 0);\n return String(result).toUpperCase() === 'PONG';\n } catch {\n return false;\n }\n },\n };\n}\n\nconst defaultMemoryStore = createMemoryRefreshTokenRotationStore();\n\nexport function getRefreshTokenRotationStore(\n options: AccessiOptions,\n): RefreshTokenRotationStore | undefined {\n if (options.refreshTokenRotation?.enabled === false) return undefined;\n if (options.refreshTokenRotation?.store) return options.refreshTokenRotation.store;\n return options.refreshTokenRotation?.enabled === true ? defaultMemoryStore : undefined;\n}\n\n/** Solo per test: azzera lo store in memoria di default. */\nexport function clearDefaultRefreshTokenRotations(): void {\n defaultMemoryStore.clear();\n}\n"]}
@@ -0,0 +1,52 @@
1
+ import type { AccessiOptions } from '../AccessiModule';
2
+ /**
3
+ * Revoca delle sessioni per utente.
4
+ *
5
+ * La revoca e un timestamp: un token con `iat` precedente al cutoff viene
6
+ * rifiutato. I token emessi dopo il cutoff restano validi. La granularita di
7
+ * `iat` e in secondi, quindi un token emesso nello stesso secondo della revoca
8
+ * puo sopravvivere: e una finestra massima di 1 secondo.
9
+ */
10
+ export interface SessionRevocationStore {
11
+ revokeUser(codiceUtente: number, revokedAtMs: number): Promise<void>;
12
+ getUserRevocation(codiceUtente: number): Promise<number | null>;
13
+ /** Health check opzionale per readiness del servizio. */
14
+ healthCheck?(): Promise<boolean>;
15
+ }
16
+ export interface SessionRevocationOptions {
17
+ /** Default: disabilitata se non viene fornito uno store. */
18
+ enabled?: boolean;
19
+ /** Store condiviso (es. Redis) per invalidare le sessioni tra istanze. */
20
+ store?: SessionRevocationStore;
21
+ /** TTL delle chiavi Redis. Default: 365 giorni. */
22
+ ttlMs?: number;
23
+ }
24
+ export interface MemorySessionRevocationStore extends SessionRevocationStore {
25
+ clear(): void;
26
+ }
27
+ export declare function createMemorySessionRevocationStore(): MemorySessionRevocationStore;
28
+ export interface SessionRevocationRedisClient {
29
+ eval(script: string, numKeys: number, ...args: Array<string | number>): Promise<unknown>;
30
+ }
31
+ export interface RedisSessionRevocationStoreOptions {
32
+ client: SessionRevocationRedisClient;
33
+ /** Default: `accessi:revoked-user:`. */
34
+ keyPrefix?: string;
35
+ /** Default: 365 giorni. */
36
+ ttlMs?: number;
37
+ }
38
+ /**
39
+ * Store Redis per la revoca sessioni.
40
+ *
41
+ * `revokeUser` scrive un timestamp con TTL; `getUserRevocation` lo legge.
42
+ * Il TTL deve essere maggiore della durata massima dei token.
43
+ */
44
+ export declare function createRedisSessionRevocationStore(options: RedisSessionRevocationStoreOptions): SessionRevocationStore;
45
+ /** Store di default in memoria, usato quando `sessionRevocation.store` non e configurato ma la revoca e abilitata. */
46
+ export declare function getSessionRevocationStore(options: AccessiOptions): SessionRevocationStore | undefined;
47
+ /** True se il token e stato emesso prima dell'ultima revoca sessioni dell'utente. */
48
+ export declare function isAccessiTokenRevoked(options: AccessiOptions, payload: Record<string, unknown>, codiceUtente: number): Promise<boolean>;
49
+ /** Revoca tutte le sessioni dell'utente. Richiede uno store configurato. */
50
+ export declare function revokeAccessiUserSessions(options: AccessiOptions, codiceUtente: number, revokedAtMs?: number): Promise<void>;
51
+ /** Solo per test: azzera lo store in memoria di default. */
52
+ export declare function clearDefaultSessionRevocations(): void;
@@ -0,0 +1,124 @@
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.createMemorySessionRevocationStore = createMemorySessionRevocationStore;
13
+ exports.createRedisSessionRevocationStore = createRedisSessionRevocationStore;
14
+ exports.getSessionRevocationStore = getSessionRevocationStore;
15
+ exports.isAccessiTokenRevoked = isAccessiTokenRevoked;
16
+ exports.revokeAccessiUserSessions = revokeAccessiUserSessions;
17
+ exports.clearDefaultSessionRevocations = clearDefaultSessionRevocations;
18
+ const REVOKE_USER_SCRIPT = "if tonumber(ARGV[2]) > 0 then return redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2]) else return redis.call('SET', KEYS[1], ARGV[1]) end";
19
+ const GET_USER_SCRIPT = "return redis.call('GET', KEYS[1])";
20
+ function createMemorySessionRevocationStore() {
21
+ const revokedAt = new Map();
22
+ return {
23
+ revokeUser(codiceUtente, timestamp) {
24
+ return __awaiter(this, void 0, void 0, function* () {
25
+ revokedAt.set(codiceUtente, timestamp);
26
+ });
27
+ },
28
+ getUserRevocation(codiceUtente) {
29
+ return __awaiter(this, void 0, void 0, function* () {
30
+ var _a;
31
+ return (_a = revokedAt.get(codiceUtente)) !== null && _a !== void 0 ? _a : null;
32
+ });
33
+ },
34
+ healthCheck() {
35
+ return __awaiter(this, void 0, void 0, function* () {
36
+ return true;
37
+ });
38
+ },
39
+ clear() {
40
+ revokedAt.clear();
41
+ },
42
+ };
43
+ }
44
+ /**
45
+ * Store Redis per la revoca sessioni.
46
+ *
47
+ * `revokeUser` scrive un timestamp con TTL; `getUserRevocation` lo legge.
48
+ * Il TTL deve essere maggiore della durata massima dei token.
49
+ */
50
+ function createRedisSessionRevocationStore(options) {
51
+ var _a, _b;
52
+ if (!(options === null || options === void 0 ? void 0 : options.client) || typeof options.client.eval !== 'function') {
53
+ throw new Error('Redis session revocation store: client.eval non configurato.');
54
+ }
55
+ const keyPrefix = ((_a = options.keyPrefix) !== null && _a !== void 0 ? _a : 'accessi:revoked-user:').replace(/:+$/, ':');
56
+ const ttlMs = Math.max(0, Math.trunc((_b = options.ttlMs) !== null && _b !== void 0 ? _b : 365 * 24 * 60 * 60 * 1000));
57
+ return {
58
+ revokeUser(codiceUtente, revokedAtMs) {
59
+ return __awaiter(this, void 0, void 0, function* () {
60
+ yield options.client.eval(REVOKE_USER_SCRIPT, 1, `${keyPrefix}${codiceUtente}`, String(revokedAtMs), ttlMs);
61
+ });
62
+ },
63
+ getUserRevocation(codiceUtente) {
64
+ return __awaiter(this, void 0, void 0, function* () {
65
+ const value = yield options.client.eval(GET_USER_SCRIPT, 1, `${keyPrefix}${codiceUtente}`);
66
+ if (typeof value !== 'string' && typeof value !== 'number')
67
+ return null;
68
+ const timestamp = Number(value);
69
+ return Number.isFinite(timestamp) ? timestamp : null;
70
+ });
71
+ },
72
+ healthCheck() {
73
+ return __awaiter(this, void 0, void 0, function* () {
74
+ try {
75
+ const result = yield options.client.eval("return redis.call('PING')", 0);
76
+ return String(result).toUpperCase() === 'PONG';
77
+ }
78
+ catch (_a) {
79
+ return false;
80
+ }
81
+ });
82
+ },
83
+ };
84
+ }
85
+ const defaultMemoryStore = createMemorySessionRevocationStore();
86
+ /** Store di default in memoria, usato quando `sessionRevocation.store` non e configurato ma la revoca e abilitata. */
87
+ function getSessionRevocationStore(options) {
88
+ var _a, _b, _c;
89
+ if (((_a = options.sessionRevocation) === null || _a === void 0 ? void 0 : _a.enabled) === false)
90
+ return undefined;
91
+ if ((_b = options.sessionRevocation) === null || _b === void 0 ? void 0 : _b.store)
92
+ return options.sessionRevocation.store;
93
+ return ((_c = options.sessionRevocation) === null || _c === void 0 ? void 0 : _c.enabled) === true ? defaultMemoryStore : undefined;
94
+ }
95
+ /** True se il token e stato emesso prima dell'ultima revoca sessioni dell'utente. */
96
+ function isAccessiTokenRevoked(options, payload, codiceUtente) {
97
+ return __awaiter(this, void 0, void 0, function* () {
98
+ const store = getSessionRevocationStore(options);
99
+ if (!store)
100
+ return false;
101
+ const issuedAt = Number(payload.iat);
102
+ if (!Number.isFinite(issuedAt))
103
+ return true;
104
+ const revokedAt = yield store.getUserRevocation(codiceUtente);
105
+ if (revokedAt === null)
106
+ return false;
107
+ return issuedAt < Math.floor(revokedAt / 1000);
108
+ });
109
+ }
110
+ /** Revoca tutte le sessioni dell'utente. Richiede uno store configurato. */
111
+ function revokeAccessiUserSessions(options_1, codiceUtente_1) {
112
+ return __awaiter(this, arguments, void 0, function* (options, codiceUtente, revokedAtMs = Date.now()) {
113
+ const store = getSessionRevocationStore(options);
114
+ if (!store) {
115
+ throw new Error('Session revocation store non configurato.');
116
+ }
117
+ yield store.revokeUser(codiceUtente, revokedAtMs);
118
+ });
119
+ }
120
+ /** Solo per test: azzera lo store in memoria di default. */
121
+ function clearDefaultSessionRevocations() {
122
+ defaultMemoryStore.clear();
123
+ }
124
+ //# sourceMappingURL=sessionRevocation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessionRevocation.js","sourceRoot":"","sources":["../../../src/accessi-module/security/sessionRevocation.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AccessiOptions } from '../AccessiModule';\n\n/**\n * Revoca delle sessioni per utente.\n *\n * La revoca e un timestamp: un token con `iat` precedente al cutoff viene\n * rifiutato. I token emessi dopo il cutoff restano validi. La granularita di\n * `iat` e in secondi, quindi un token emesso nello stesso secondo della revoca\n * puo sopravvivere: e una finestra massima di 1 secondo.\n */\nexport interface SessionRevocationStore {\n revokeUser(codiceUtente: number, revokedAtMs: number): Promise<void>;\n getUserRevocation(codiceUtente: number): Promise<number | null>;\n /** Health check opzionale per readiness del servizio. */\n healthCheck?(): Promise<boolean>;\n}\n\nexport interface SessionRevocationOptions {\n /** Default: disabilitata se non viene fornito uno store. */\n enabled?: boolean;\n /** Store condiviso (es. Redis) per invalidare le sessioni tra istanze. */\n store?: SessionRevocationStore;\n /** TTL delle chiavi Redis. Default: 365 giorni. */\n ttlMs?: number;\n}\n\nexport interface MemorySessionRevocationStore extends SessionRevocationStore {\n clear(): void;\n}\n\nconst REVOKE_USER_SCRIPT =\n \"if tonumber(ARGV[2]) > 0 then return redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2]) else return redis.call('SET', KEYS[1], ARGV[1]) end\";\nconst GET_USER_SCRIPT = \"return redis.call('GET', KEYS[1])\";\n\nexport function createMemorySessionRevocationStore(): MemorySessionRevocationStore {\n const revokedAt = new Map<number, number>();\n return {\n async revokeUser(codiceUtente, timestamp) {\n revokedAt.set(codiceUtente, timestamp);\n },\n async getUserRevocation(codiceUtente) {\n return revokedAt.get(codiceUtente) ?? null;\n },\n async healthCheck() {\n return true;\n },\n clear() {\n revokedAt.clear();\n },\n };\n}\n\nexport interface SessionRevocationRedisClient {\n eval(script: string, numKeys: number, ...args: Array<string | number>): Promise<unknown>;\n}\n\nexport interface RedisSessionRevocationStoreOptions {\n client: SessionRevocationRedisClient;\n /** Default: `accessi:revoked-user:`. */\n keyPrefix?: string;\n /** Default: 365 giorni. */\n ttlMs?: number;\n}\n\n/**\n * Store Redis per la revoca sessioni.\n *\n * `revokeUser` scrive un timestamp con TTL; `getUserRevocation` lo legge.\n * Il TTL deve essere maggiore della durata massima dei token.\n */\nexport function createRedisSessionRevocationStore(\n options: RedisSessionRevocationStoreOptions,\n): SessionRevocationStore {\n if (!options?.client || typeof options.client.eval !== 'function') {\n throw new Error('Redis session revocation store: client.eval non configurato.');\n }\n\n const keyPrefix = (options.keyPrefix ?? 'accessi:revoked-user:').replace(/:+$/, ':');\n const ttlMs = Math.max(0, Math.trunc(options.ttlMs ?? 365 * 24 * 60 * 60 * 1000));\n\n return {\n async revokeUser(codiceUtente, revokedAtMs) {\n await options.client.eval(\n REVOKE_USER_SCRIPT,\n 1,\n `${keyPrefix}${codiceUtente}`,\n String(revokedAtMs),\n ttlMs,\n );\n },\n\n async getUserRevocation(codiceUtente) {\n const value = await options.client.eval(\n GET_USER_SCRIPT,\n 1,\n `${keyPrefix}${codiceUtente}`,\n );\n if (typeof value !== 'string' && typeof value !== 'number') return null;\n const timestamp = Number(value);\n return Number.isFinite(timestamp) ? timestamp : null;\n },\n\n async healthCheck() {\n try {\n const result = await options.client.eval(\"return redis.call('PING')\", 0);\n return String(result).toUpperCase() === 'PONG';\n } catch {\n return false;\n }\n },\n };\n}\n\nconst defaultMemoryStore = createMemorySessionRevocationStore();\n\n/** Store di default in memoria, usato quando `sessionRevocation.store` non e configurato ma la revoca e abilitata. */\nexport function getSessionRevocationStore(options: AccessiOptions): SessionRevocationStore | undefined {\n if (options.sessionRevocation?.enabled === false) return undefined;\n if (options.sessionRevocation?.store) return options.sessionRevocation.store;\n return options.sessionRevocation?.enabled === true ? defaultMemoryStore : undefined;\n}\n\n/** True se il token e stato emesso prima dell'ultima revoca sessioni dell'utente. */\nexport async function isAccessiTokenRevoked(\n options: AccessiOptions,\n payload: Record<string, unknown>,\n codiceUtente: number,\n): Promise<boolean> {\n const store = getSessionRevocationStore(options);\n if (!store) return false;\n\n const issuedAt = Number(payload.iat);\n if (!Number.isFinite(issuedAt)) return true;\n\n const revokedAt = await store.getUserRevocation(codiceUtente);\n if (revokedAt === null) return false;\n\n return issuedAt < Math.floor(revokedAt / 1000);\n}\n\n/** Revoca tutte le sessioni dell'utente. Richiede uno store configurato. */\nexport async function revokeAccessiUserSessions(\n options: AccessiOptions,\n codiceUtente: number,\n revokedAtMs: number = Date.now(),\n): Promise<void> {\n const store = getSessionRevocationStore(options);\n if (!store) {\n throw new Error('Session revocation store non configurato.');\n }\n await store.revokeUser(codiceUtente, revokedAtMs);\n}\n\n/** Solo per test: azzera lo store in memoria di default. */\nexport function clearDefaultSessionRevocations(): void {\n defaultMemoryStore.clear();\n}\n"]}
@@ -0,0 +1,19 @@
1
+ import type { AccessiOptions } from '../AccessiModule';
2
+ export interface AccessiOptionsValidationIssue {
3
+ code: string;
4
+ message: string;
5
+ }
6
+ export interface AccessiOptionsValidationResult {
7
+ errors: AccessiOptionsValidationIssue[];
8
+ warnings: AccessiOptionsValidationIssue[];
9
+ }
10
+ /**
11
+ * Validazione fail-fast della configurazione Accessi.
12
+ *
13
+ * `createAccessiService` usa `assertValidAccessiOptions`; i bootstrap esistenti
14
+ * possono usare `validateAccessiOptions` per loggare warning senza rompere il
15
+ * comportamento storico.
16
+ */
17
+ export declare function validateAccessiOptions(options: AccessiOptions): AccessiOptionsValidationResult;
18
+ /** Lancia un errore aggregato se la configurazione non e valida. */
19
+ export declare function assertValidAccessiOptions(options: AccessiOptions): void;
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateAccessiOptions = validateAccessiOptions;
4
+ exports.assertValidAccessiOptions = assertValidAccessiOptions;
5
+ function isHttpUrl(value) {
6
+ if (!value)
7
+ return false;
8
+ try {
9
+ const url = new URL(value);
10
+ return url.protocol === 'http:' || url.protocol === 'https:';
11
+ }
12
+ catch (_a) {
13
+ return false;
14
+ }
15
+ }
16
+ function positive(value) {
17
+ return typeof value === 'number' && Number.isFinite(value) && value > 0;
18
+ }
19
+ /**
20
+ * Validazione fail-fast della configurazione Accessi.
21
+ *
22
+ * `createAccessiService` usa `assertValidAccessiOptions`; i bootstrap esistenti
23
+ * possono usare `validateAccessiOptions` per loggare warning senza rompere il
24
+ * comportamento storico.
25
+ */
26
+ function validateAccessiOptions(options) {
27
+ var _a, _b, _c, _d;
28
+ const errors = [];
29
+ const warnings = [];
30
+ const addError = (code, message) => errors.push({ code, message });
31
+ const addWarning = (code, message) => warnings.push({ code, message });
32
+ if (!(options === null || options === void 0 ? void 0 : options.databaseOptions)) {
33
+ addError('ACCESSI_DB_OPTIONS_MISSING', 'databaseOptions e obbligatorio.');
34
+ }
35
+ const jwt = options === null || options === void 0 ? void 0 : options.jwtOptions;
36
+ const secret = (jwt === null || jwt === void 0 ? void 0 : jwt.secret) || process.env.ACC_JWT_SECRET;
37
+ if (!secret) {
38
+ addError('ACCESSI_JWT_SECRET_MISSING', 'jwtOptions.secret e obbligatorio.');
39
+ }
40
+ else if (secret.length < 32) {
41
+ addWarning('ACCESSI_JWT_SECRET_WEAK', 'jwtOptions.secret dovrebbe avere almeno 32 caratteri.');
42
+ }
43
+ if (!(jwt === null || jwt === void 0 ? void 0 : jwt.expiresIn)) {
44
+ addError('ACCESSI_JWT_EXPIRES_MISSING', 'jwtOptions.expiresIn e obbligatorio.');
45
+ }
46
+ const algorithm = (_a = jwt === null || jwt === void 0 ? void 0 : jwt.algorithm) !== null && _a !== void 0 ? _a : 'HS256';
47
+ if (algorithm.startsWith('HS')) {
48
+ if ((jwt === null || jwt === void 0 ? void 0 : jwt.privateKey) || (jwt === null || jwt === void 0 ? void 0 : jwt.publicKey)) {
49
+ addWarning('ACCESSI_JWT_ASYMMETRIC_KEYS_WITH_HMAC', 'Sono configurate chiavi asimmetriche ma algorithm e HS256: verranno ignorate.');
50
+ }
51
+ }
52
+ else {
53
+ if (!(jwt === null || jwt === void 0 ? void 0 : jwt.privateKey)) {
54
+ addError('ACCESSI_JWT_PRIVATE_KEY_MISSING', `jwtOptions.privateKey e obbligatoria per ${algorithm}.`);
55
+ }
56
+ if (!(jwt === null || jwt === void 0 ? void 0 : jwt.publicKey) && !(jwt === null || jwt === void 0 ? void 0 : jwt.privateKey)) {
57
+ addError('ACCESSI_JWT_PUBLIC_KEY_MISSING', `jwtOptions.publicKey e obbligatoria per ${algorithm}.`);
58
+ }
59
+ if (!(jwt === null || jwt === void 0 ? void 0 : jwt.keyId)) {
60
+ addWarning('ACCESSI_JWT_KEY_ID_MISSING', 'jwtOptions.keyId e consigliato per la rotazione JWKS.');
61
+ }
62
+ if (!(jwt === null || jwt === void 0 ? void 0 : jwt.jwksPath)) {
63
+ addWarning('ACCESSI_JWKS_PATH_DEFAULT', 'jwksPath non configurato: verra usato accessi/.well-known/jwks.json.');
64
+ }
65
+ }
66
+ if (!(jwt === null || jwt === void 0 ? void 0 : jwt.issuer)) {
67
+ addWarning('ACCESSI_JWT_ISSUER_MISSING', 'jwtOptions.issuer e consigliato in multi-servizio.');
68
+ }
69
+ if (!(jwt === null || jwt === void 0 ? void 0 : jwt.audience)) {
70
+ addWarning('ACCESSI_JWT_AUDIENCE_MISSING', 'jwtOptions.audience e consigliato in multi-servizio.');
71
+ }
72
+ if (!(options === null || options === void 0 ? void 0 : options.encryptionKey)) {
73
+ addError('ACCESSI_ENCRYPTION_KEY_MISSING', 'encryptionKey e obbligatoria.');
74
+ }
75
+ else if (options.encryptionKey.length < 16) {
76
+ addWarning('ACCESSI_ENCRYPTION_KEY_WEAK', 'encryptionKey dovrebbe avere almeno 16 caratteri.');
77
+ }
78
+ if (!isHttpUrl(options === null || options === void 0 ? void 0 : options.confirmationEmailUrl)) {
79
+ addWarning('ACCESSI_CONFIRMATION_URL_INVALID', 'confirmationEmailUrl dovrebbe essere un URL http/https.');
80
+ }
81
+ if (!isHttpUrl(options === null || options === void 0 ? void 0 : options.confirmationEmailReturnUrl)) {
82
+ addWarning('ACCESSI_CONFIRMATION_RETURN_URL_INVALID', 'confirmationEmailReturnUrl dovrebbe essere un URL http/https.');
83
+ }
84
+ if ((options === null || options === void 0 ? void 0 : options.publicBaseUrl) && !isHttpUrl(options.publicBaseUrl)) {
85
+ addWarning('ACCESSI_PUBLIC_BASE_URL_INVALID', 'publicBaseUrl dovrebbe essere un URL http/https.');
86
+ }
87
+ if (((_b = options === null || options === void 0 ? void 0 : options.publicAuthRateLimit) === null || _b === void 0 ? void 0 : _b.login) && !positive(options.publicAuthRateLimit.login.windowMs)) {
88
+ addError('ACCESSI_RATE_LIMIT_WINDOW_INVALID', 'publicAuthRateLimit.login.windowMs deve essere positivo.');
89
+ }
90
+ if (((_c = options === null || options === void 0 ? void 0 : options.publicAuthRateLimit) === null || _c === void 0 ? void 0 : _c.login) && !positive(options.publicAuthRateLimit.login.maxAttempts)) {
91
+ addError('ACCESSI_RATE_LIMIT_ATTEMPTS_INVALID', 'publicAuthRateLimit.login.maxAttempts deve essere positivo.');
92
+ }
93
+ if (options === null || options === void 0 ? void 0 : options.edgeRateLimit) {
94
+ if (options.edgeRateLimit.windowMs !== undefined && !positive(options.edgeRateLimit.windowMs)) {
95
+ addError('ACCESSI_EDGE_RATE_LIMIT_WINDOW_INVALID', 'edgeRateLimit.windowMs deve essere positivo.');
96
+ }
97
+ if (options.edgeRateLimit.maxAttempts !== undefined && !positive(options.edgeRateLimit.maxAttempts)) {
98
+ addError('ACCESSI_EDGE_RATE_LIMIT_ATTEMPTS_INVALID', 'edgeRateLimit.maxAttempts deve essere positivo.');
99
+ }
100
+ }
101
+ if (((_d = options === null || options === void 0 ? void 0 : options.consoleEntry) === null || _d === void 0 ? void 0 : _d.ttlMs) !== undefined && !positive(options.consoleEntry.ttlMs)) {
102
+ addError('ACCESSI_CONSOLE_ENTRY_TTL_INVALID', 'consoleEntry.ttlMs deve essere positivo.');
103
+ }
104
+ return { errors, warnings };
105
+ }
106
+ /** Lancia un errore aggregato se la configurazione non e valida. */
107
+ function assertValidAccessiOptions(options) {
108
+ const result = validateAccessiOptions(options);
109
+ if (result.errors.length === 0)
110
+ return;
111
+ throw new Error(`Configurazione Accessi non valida:\n${result.errors
112
+ .map((issue) => `- ${issue.code}: ${issue.message}`)
113
+ .join('\n')}`);
114
+ }
115
+ //# sourceMappingURL=validateOptions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validateOptions.js","sourceRoot":"","sources":["../../../src/accessi-module/security/validateOptions.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AccessiOptions } from '../AccessiModule';\n\nexport interface AccessiOptionsValidationIssue {\n code: string;\n message: string;\n}\n\nexport interface AccessiOptionsValidationResult {\n errors: AccessiOptionsValidationIssue[];\n warnings: AccessiOptionsValidationIssue[];\n}\n\nfunction isHttpUrl(value: string | undefined): boolean {\n if (!value) return false;\n try {\n const url = new URL(value);\n return url.protocol === 'http:' || url.protocol === 'https:';\n } catch {\n return false;\n }\n}\n\nfunction positive(value: number | undefined): boolean {\n return typeof value === 'number' && Number.isFinite(value) && value > 0;\n}\n\n/**\n * Validazione fail-fast della configurazione Accessi.\n *\n * `createAccessiService` usa `assertValidAccessiOptions`; i bootstrap esistenti\n * possono usare `validateAccessiOptions` per loggare warning senza rompere il\n * comportamento storico.\n */\nexport function validateAccessiOptions(options: AccessiOptions): AccessiOptionsValidationResult {\n const errors: AccessiOptionsValidationIssue[] = [];\n const warnings: AccessiOptionsValidationIssue[] = [];\n\n const addError = (code: string, message: string) => errors.push({ code, message });\n const addWarning = (code: string, message: string) => warnings.push({ code, message });\n\n if (!options?.databaseOptions) {\n addError('ACCESSI_DB_OPTIONS_MISSING', 'databaseOptions e obbligatorio.');\n }\n\n const jwt = options?.jwtOptions;\n const secret = jwt?.secret || process.env.ACC_JWT_SECRET;\n if (!secret) {\n addError('ACCESSI_JWT_SECRET_MISSING', 'jwtOptions.secret e obbligatorio.');\n } else if (secret.length < 32) {\n addWarning('ACCESSI_JWT_SECRET_WEAK', 'jwtOptions.secret dovrebbe avere almeno 32 caratteri.');\n }\n\n if (!jwt?.expiresIn) {\n addError('ACCESSI_JWT_EXPIRES_MISSING', 'jwtOptions.expiresIn e obbligatorio.');\n }\n\n const algorithm = jwt?.algorithm ?? 'HS256';\n if (algorithm.startsWith('HS')) {\n if (jwt?.privateKey || jwt?.publicKey) {\n addWarning(\n 'ACCESSI_JWT_ASYMMETRIC_KEYS_WITH_HMAC',\n 'Sono configurate chiavi asimmetriche ma algorithm e HS256: verranno ignorate.',\n );\n }\n } else {\n if (!jwt?.privateKey) {\n addError('ACCESSI_JWT_PRIVATE_KEY_MISSING', `jwtOptions.privateKey e obbligatoria per ${algorithm}.`);\n }\n if (!jwt?.publicKey && !jwt?.privateKey) {\n addError('ACCESSI_JWT_PUBLIC_KEY_MISSING', `jwtOptions.publicKey e obbligatoria per ${algorithm}.`);\n }\n if (!jwt?.keyId) {\n addWarning('ACCESSI_JWT_KEY_ID_MISSING', 'jwtOptions.keyId e consigliato per la rotazione JWKS.');\n }\n if (!jwt?.jwksPath) {\n addWarning(\n 'ACCESSI_JWKS_PATH_DEFAULT',\n 'jwksPath non configurato: verra usato accessi/.well-known/jwks.json.',\n );\n }\n }\n\n if (!jwt?.issuer) {\n addWarning('ACCESSI_JWT_ISSUER_MISSING', 'jwtOptions.issuer e consigliato in multi-servizio.');\n }\n if (!jwt?.audience) {\n addWarning('ACCESSI_JWT_AUDIENCE_MISSING', 'jwtOptions.audience e consigliato in multi-servizio.');\n }\n\n if (!options?.encryptionKey) {\n addError('ACCESSI_ENCRYPTION_KEY_MISSING', 'encryptionKey e obbligatoria.');\n } else if (options.encryptionKey.length < 16) {\n addWarning('ACCESSI_ENCRYPTION_KEY_WEAK', 'encryptionKey dovrebbe avere almeno 16 caratteri.');\n }\n\n if (!isHttpUrl(options?.confirmationEmailUrl)) {\n addWarning('ACCESSI_CONFIRMATION_URL_INVALID', 'confirmationEmailUrl dovrebbe essere un URL http/https.');\n }\n if (!isHttpUrl(options?.confirmationEmailReturnUrl)) {\n addWarning('ACCESSI_CONFIRMATION_RETURN_URL_INVALID', 'confirmationEmailReturnUrl dovrebbe essere un URL http/https.');\n }\n if (options?.publicBaseUrl && !isHttpUrl(options.publicBaseUrl)) {\n addWarning('ACCESSI_PUBLIC_BASE_URL_INVALID', 'publicBaseUrl dovrebbe essere un URL http/https.');\n }\n\n if (options?.publicAuthRateLimit?.login && !positive(options.publicAuthRateLimit.login.windowMs)) {\n addError('ACCESSI_RATE_LIMIT_WINDOW_INVALID', 'publicAuthRateLimit.login.windowMs deve essere positivo.');\n }\n if (options?.publicAuthRateLimit?.login && !positive(options.publicAuthRateLimit.login.maxAttempts)) {\n addError('ACCESSI_RATE_LIMIT_ATTEMPTS_INVALID', 'publicAuthRateLimit.login.maxAttempts deve essere positivo.');\n }\n if (options?.edgeRateLimit) {\n if (options.edgeRateLimit.windowMs !== undefined && !positive(options.edgeRateLimit.windowMs)) {\n addError('ACCESSI_EDGE_RATE_LIMIT_WINDOW_INVALID', 'edgeRateLimit.windowMs deve essere positivo.');\n }\n if (options.edgeRateLimit.maxAttempts !== undefined && !positive(options.edgeRateLimit.maxAttempts)) {\n addError('ACCESSI_EDGE_RATE_LIMIT_ATTEMPTS_INVALID', 'edgeRateLimit.maxAttempts deve essere positivo.');\n }\n }\n if (options?.consoleEntry?.ttlMs !== undefined && !positive(options.consoleEntry.ttlMs)) {\n addError('ACCESSI_CONSOLE_ENTRY_TTL_INVALID', 'consoleEntry.ttlMs deve essere positivo.');\n }\n\n return { errors, warnings };\n}\n\n/** Lancia un errore aggregato se la configurazione non e valida. */\nexport function assertValidAccessiOptions(options: AccessiOptions): void {\n const result = validateAccessiOptions(options);\n if (result.errors.length === 0) return;\n throw new Error(\n `Configurazione Accessi non valida:\\n${result.errors\n .map((issue) => `- ${issue.code}: ${issue.message}`)\n .join('\\n')}`,\n );\n}\n"]}
@@ -34,7 +34,7 @@ const edgeRateLimit_1 = require("./accessi-module/security/edgeRateLimit");
34
34
  */
35
35
  function initEmilsoftwareModule(app, options) {
36
36
  return __awaiter(this, void 0, void 0, function* () {
37
- var _a, _b, _c;
37
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
38
38
  const logger = new Logger_1.Logger(initEmilsoftwareModule.name);
39
39
  if (options === null || options === void 0 ? void 0 : options.accessiOptions) {
40
40
  (0, authenticateGen_1.beginAccessiAuthInitialization)();
@@ -48,6 +48,10 @@ function initEmilsoftwareModule(app, options) {
48
48
  // Precedenza: EmilsoftwareOptions.openapi > AccessiOptions.openapi > default.
49
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);
50
50
  const nestHostApp = (0, express_1.default)();
51
+ if (((_c = (_b = options === null || options === void 0 ? void 0 : options.accessiOptions) === null || _b === void 0 ? void 0 : _b.http) === null || _c === void 0 ? void 0 : _c.trustProxy) !== undefined) {
52
+ app.set('trust proxy', options.accessiOptions.http.trustProxy);
53
+ nestHostApp.set('trust proxy', options.accessiOptions.http.trustProxy);
54
+ }
51
55
  const jwksPath = (options === null || options === void 0 ? void 0 : options.accessiOptions)
52
56
  ? (0, jwks_1.getAccessiJwksPath)(options.accessiOptions)
53
57
  : undefined;
@@ -55,12 +59,24 @@ function initEmilsoftwareModule(app, options) {
55
59
  path.startsWith("/api/") ||
56
60
  (0, OpenApiConfig_1.isOpenApiRequestPath)(path, resolvedOpenApi) ||
57
61
  (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)) }))
62
+ const edgeRateLimit = ((_d = options === null || options === void 0 ? void 0 : options.accessiOptions) === null || _d === void 0 ? void 0 : _d.edgeRateLimit)
63
+ ? (0, edgeRateLimit_1.createAccessiEdgeRateLimitMiddleware)(Object.assign(Object.assign({}, options.accessiOptions.edgeRateLimit), { match: (_e = options.accessiOptions.edgeRateLimit.match) !== null && _e !== void 0 ? _e : ((path) => isNestRoute(path)) }))
60
64
  : undefined;
61
65
  if (edgeRateLimit) {
62
66
  app.use(edgeRateLimit);
63
67
  }
68
+ if (((_g = (_f = options === null || options === void 0 ? void 0 : options.accessiOptions) === null || _f === void 0 ? void 0 : _f.http) === null || _g === void 0 ? void 0 : _g.securityHeaders) !== false) {
69
+ app.use((req, res, next) => {
70
+ if (!isNestRoute(req.path)) {
71
+ return next();
72
+ }
73
+ res.setHeader('X-Content-Type-Options', 'nosniff');
74
+ res.setHeader('X-Frame-Options', 'SAMEORIGIN');
75
+ res.setHeader('Referrer-Policy', 'no-referrer');
76
+ res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
77
+ return next();
78
+ });
79
+ }
64
80
  app.use((req, res, next) => {
65
81
  if (!isNestRoute(req.path)) {
66
82
  return next();
@@ -75,7 +91,9 @@ function initEmilsoftwareModule(app, options) {
75
91
  }
76
92
  const nestExpressInstance = new platform_express_1.ExpressAdapter(nestHostApp);
77
93
  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 } : {})));
78
- nestApp.enableCors();
94
+ if (((_j = (_h = options === null || options === void 0 ? void 0 : options.accessiOptions) === null || _h === void 0 ? void 0 : _h.http) === null || _j === void 0 ? void 0 : _j.cors) !== false) {
95
+ nestApp.enableCors((_l = (_k = options === null || options === void 0 ? void 0 : options.accessiOptions) === null || _k === void 0 ? void 0 : _k.http) === null || _l === void 0 ? void 0 : _l.cors);
96
+ }
79
97
  if (options === null || options === void 0 ? void 0 : options.accessiOptions) {
80
98
  // Keep the Accessi API contract identical for standalone and unified bootstraps.
81
99
  nestApp.useGlobalPipes((0, accessiValidation_1.createAccessiValidationPipe)());