emilsoftware-utilities 2.0.0-dev.66 → 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 (165) hide show
  1. package/dist/EmilsoftwareModule.d.ts +11 -3
  2. package/dist/EmilsoftwareModule.js +1 -1
  3. package/dist/EmilsoftwareModule.js.map +1 -1
  4. package/dist/accessi/client/index.d.ts +26 -0
  5. package/dist/accessi/client/index.js +49 -0
  6. package/dist/accessi/client/index.js.map +1 -0
  7. package/dist/accessi/console/index.d.ts +15 -0
  8. package/dist/accessi/console/index.js +25 -0
  9. package/dist/accessi/console/index.js.map +1 -0
  10. package/dist/accessi/contracts/dtos.d.ts +8 -0
  11. package/dist/accessi/contracts/dtos.js +25 -0
  12. package/dist/accessi/contracts/dtos.js.map +1 -0
  13. package/dist/accessi/contracts/index.d.ts +25 -0
  14. package/dist/accessi/contracts/index.js +28 -0
  15. package/dist/accessi/contracts/index.js.map +1 -0
  16. package/dist/accessi/openapi/index.d.ts +2 -0
  17. package/dist/accessi/openapi/index.js +19 -0
  18. package/dist/accessi/openapi/index.js.map +1 -0
  19. package/dist/accessi/resource-server/core.d.ts +6 -0
  20. package/dist/accessi/resource-server/core.js +23 -0
  21. package/dist/accessi/resource-server/core.js.map +1 -0
  22. package/dist/accessi/resource-server/errors.d.ts +10 -0
  23. package/dist/accessi/resource-server/errors.js +19 -0
  24. package/dist/accessi/resource-server/errors.js.map +1 -0
  25. package/dist/accessi/resource-server/express.d.ts +17 -0
  26. package/dist/accessi/resource-server/express.js +66 -0
  27. package/dist/accessi/resource-server/express.js.map +1 -0
  28. package/dist/accessi/resource-server/index.d.ts +3 -0
  29. package/dist/accessi/resource-server/index.js +20 -0
  30. package/dist/accessi/resource-server/index.js.map +1 -0
  31. package/dist/accessi/resource-server/introspection.d.ts +16 -0
  32. package/dist/accessi/resource-server/introspection.js +115 -0
  33. package/dist/accessi/resource-server/introspection.js.map +1 -0
  34. package/dist/accessi/resource-server/keys.d.ts +11 -0
  35. package/dist/accessi/resource-server/keys.js +22 -0
  36. package/dist/accessi/resource-server/keys.js.map +1 -0
  37. package/dist/accessi/resource-server/nest.d.ts +9 -0
  38. package/dist/accessi/resource-server/nest.js +62 -0
  39. package/dist/accessi/resource-server/nest.js.map +1 -0
  40. package/dist/accessi/resource-server/runtime.d.ts +27 -0
  41. package/dist/accessi/resource-server/runtime.js +128 -0
  42. package/dist/accessi/resource-server/runtime.js.map +1 -0
  43. package/dist/accessi/resource-server/token.d.ts +23 -0
  44. package/dist/accessi/resource-server/token.js +230 -0
  45. package/dist/accessi/resource-server/token.js.map +1 -0
  46. package/dist/accessi/resource-server/types.d.ts +78 -0
  47. package/dist/accessi/resource-server/types.js +3 -0
  48. package/dist/accessi/resource-server/types.js.map +1 -0
  49. package/dist/accessi/server/index.d.ts +10 -0
  50. package/dist/accessi/server/index.js +29 -0
  51. package/dist/accessi/server/index.js.map +1 -0
  52. package/dist/accessi/ui/index.d.ts +13 -0
  53. package/dist/accessi/ui/index.js +19 -0
  54. package/dist/accessi/ui/index.js.map +1 -0
  55. package/dist/accessi-module/AccessiModule.d.ts +52 -2
  56. package/dist/accessi-module/AccessiModule.js +2 -0
  57. package/dist/accessi-module/AccessiModule.js.map +1 -1
  58. package/dist/accessi-module/Console/accessiFetch.d.ts +4 -0
  59. package/dist/accessi-module/Console/accessiFetch.js +18 -4
  60. package/dist/accessi-module/Console/accessiFetch.js.map +1 -1
  61. package/dist/accessi-module/Console/console.js +122 -11
  62. package/dist/accessi-module/Console/console.js.map +1 -1
  63. package/dist/accessi-module/Console/generated/{accessiApi.d.ts → emilsoftware-utilities.d.ts} +11 -1
  64. package/dist/accessi-module/Console/generated/{accessiApi.js → emilsoftware-utilities.js} +10 -2
  65. package/dist/accessi-module/Console/generated/emilsoftware-utilities.js.map +1 -0
  66. package/dist/accessi-module/Console/generated/model/accessiIntrospectionResultDto.d.ts +16 -0
  67. package/dist/accessi-module/Console/generated/model/accessiIntrospectionResultDto.js +3 -0
  68. package/dist/accessi-module/Console/generated/model/accessiIntrospectionResultDto.js.map +1 -0
  69. package/dist/accessi-module/Console/generated/model/index.d.ts +3 -0
  70. package/dist/accessi-module/Console/generated/model/index.js +3 -0
  71. package/dist/accessi-module/Console/generated/model/index.js.map +1 -1
  72. package/dist/accessi-module/Console/generated/model/introspectAccessiTokenRequest.d.ts +10 -0
  73. package/dist/accessi-module/Console/generated/model/introspectAccessiTokenRequest.js +9 -0
  74. package/dist/accessi-module/Console/generated/model/introspectAccessiTokenRequest.js.map +1 -0
  75. package/dist/accessi-module/Console/generated/model/introspectAccessiTokenResponse.d.ts +14 -0
  76. package/dist/accessi-module/Console/generated/model/introspectAccessiTokenResponse.js +3 -0
  77. package/dist/accessi-module/Console/generated/model/introspectAccessiTokenResponse.js.map +1 -0
  78. package/dist/accessi-module/Console/wiki.js +106 -5
  79. package/dist/accessi-module/Console/wiki.js.map +1 -1
  80. package/dist/accessi-module/Controllers/AccessiIntrospectionController.d.ts +17 -0
  81. package/dist/accessi-module/Controllers/AccessiIntrospectionController.js +93 -0
  82. package/dist/accessi-module/Controllers/AccessiIntrospectionController.js.map +1 -0
  83. package/dist/accessi-module/Controllers/AuthController.js +6 -6
  84. package/dist/accessi-module/Controllers/AuthController.js.map +1 -1
  85. package/dist/accessi-module/Controllers/ConsoleEntryController.d.ts +2 -0
  86. package/dist/accessi-module/Controllers/ConsoleEntryController.js +11 -3
  87. package/dist/accessi-module/Controllers/ConsoleEntryController.js.map +1 -1
  88. package/dist/accessi-module/Controllers/EmailController.js +1 -1
  89. package/dist/accessi-module/Controllers/EmailController.js.map +1 -1
  90. package/dist/accessi-module/Controllers/UserController.js +1 -1
  91. package/dist/accessi-module/Controllers/UserController.js.map +1 -1
  92. package/dist/accessi-module/Dtos/IntrospectionDtos.d.ts +16 -0
  93. package/dist/accessi-module/Dtos/IntrospectionDtos.js +59 -0
  94. package/dist/accessi-module/Dtos/IntrospectionDtos.js.map +1 -0
  95. package/dist/accessi-module/Dtos/index.d.ts +1 -0
  96. package/dist/accessi-module/Dtos/index.js +1 -0
  97. package/dist/accessi-module/Dtos/index.js.map +1 -1
  98. package/dist/accessi-module/Services/AuthService/AuthService.d.ts +13 -0
  99. package/dist/accessi-module/Services/AuthService/AuthService.js +42 -10
  100. package/dist/accessi-module/Services/AuthService/AuthService.js.map +1 -1
  101. package/dist/accessi-module/database-updates/accessiOptionsFromEnv.js +18 -19
  102. package/dist/accessi-module/database-updates/accessiOptionsFromEnv.js.map +1 -1
  103. package/dist/accessi-module/database-updates/menuScopeSeed.d.ts +2 -2
  104. package/dist/accessi-module/database-updates/menuScopeSeed.js +101 -53
  105. package/dist/accessi-module/database-updates/menuScopeSeed.js.map +1 -1
  106. package/dist/accessi-module/database-updates/menuScopeSeeder.js +1 -1
  107. package/dist/accessi-module/database-updates/menuScopeSeeder.js.map +1 -1
  108. package/dist/accessi-module/index.d.ts +25 -4
  109. package/dist/accessi-module/index.js +47 -21
  110. package/dist/accessi-module/index.js.map +1 -1
  111. package/dist/accessi-module/jwt/jwt.strategy.js +5 -5
  112. package/dist/accessi-module/jwt/jwt.strategy.js.map +1 -1
  113. package/dist/accessi-module/middleware/authenticateGen.js +5 -5
  114. package/dist/accessi-module/middleware/authenticateGen.js.map +1 -1
  115. package/dist/accessi-module/security/consoleEntry.d.ts +56 -11
  116. package/dist/accessi-module/security/consoleEntry.js +165 -31
  117. package/dist/accessi-module/security/consoleEntry.js.map +1 -1
  118. package/dist/accessi-module/security/edgeRateLimit.d.ts +31 -0
  119. package/dist/accessi-module/security/edgeRateLimit.js +70 -0
  120. package/dist/accessi-module/security/edgeRateLimit.js.map +1 -0
  121. package/dist/accessi-module/security/jwks.d.ts +33 -0
  122. package/dist/accessi-module/security/jwks.js +83 -0
  123. package/dist/accessi-module/security/jwks.js.map +1 -0
  124. package/dist/accessi-module/security/jwtTokenOptions.d.ts +15 -0
  125. package/dist/accessi-module/security/jwtTokenOptions.js +44 -0
  126. package/dist/accessi-module/security/jwtTokenOptions.js.map +1 -0
  127. package/dist/accessi-module/security/publicAuthRateLimit.d.ts +8 -6
  128. package/dist/accessi-module/security/publicAuthRateLimit.js +54 -78
  129. package/dist/accessi-module/security/publicAuthRateLimit.js.map +1 -1
  130. package/dist/accessi-module/security/rateLimitStore.d.ts +32 -0
  131. package/dist/accessi-module/security/rateLimitStore.js +129 -0
  132. package/dist/accessi-module/security/rateLimitStore.js.map +1 -0
  133. package/dist/accessi-module/security/serviceTokenGuard.d.ts +3 -1
  134. package/dist/accessi-module/security/serviceTokenGuard.js +11 -6
  135. package/dist/accessi-module/security/serviceTokenGuard.js.map +1 -1
  136. package/dist/accessi-module/security/serviceTokenRateLimit.d.ts +5 -4
  137. package/dist/accessi-module/security/serviceTokenRateLimit.js +33 -29
  138. package/dist/accessi-module/security/serviceTokenRateLimit.js.map +1 -1
  139. package/dist/accessi-module/startupLog.d.ts +3 -2
  140. package/dist/accessi-module/startupLog.js +8 -4
  141. package/dist/accessi-module/startupLog.js.map +1 -1
  142. package/dist/allegati-module/index.js +2 -2
  143. package/dist/allegati-module/index.js.map +1 -1
  144. package/dist/angular-components/aree-disponibili/demo.html +4 -4
  145. package/dist/emilsoftware-utilities-2.0.0-dev.68.tgz +0 -0
  146. package/dist/index.d.ts +2 -1
  147. package/dist/index.js +2 -1
  148. package/dist/index.js.map +1 -1
  149. package/dist/initEmilsoftwareModule.d.ts +11 -1
  150. package/dist/initEmilsoftwareModule.js +29 -23
  151. package/dist/initEmilsoftwareModule.js.map +1 -1
  152. package/dist/openapi/OpenApiConfig.d.ts +68 -0
  153. package/dist/openapi/OpenApiConfig.js +157 -0
  154. package/dist/openapi/OpenApiConfig.js.map +1 -0
  155. package/dist/openapi/OpenApiMerge.d.ts +15 -0
  156. package/dist/openapi/OpenApiMerge.js +119 -0
  157. package/dist/openapi/OpenApiMerge.js.map +1 -0
  158. package/docs/gateway-rate-limit.md +98 -0
  159. package/integration-prompt.md +612 -0
  160. package/package.json +89 -5
  161. package/dist/accessi-module/Console/generated/accessiApi.js.map +0 -1
  162. package/dist/emilsoftware-utilities-2.0.0-dev.66.tgz +0 -0
  163. package/dist/swagger/SwaggerConfig.d.ts +0 -8
  164. package/dist/swagger/SwaggerConfig.js +0 -41
  165. package/dist/swagger/SwaggerConfig.js.map +0 -1
@@ -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"]}
@@ -0,0 +1,33 @@
1
+ import type { JsonWebKey } from "crypto";
2
+ import type { Request, Response } from "express";
3
+ import type { AccessiOptions } from "../AccessiModule";
4
+ /** Chiave pubblica in formato JWK, pronta per una risposta JWKS. */
5
+ export type AccessiJwksKey = JsonWebKey & {
6
+ kid: string;
7
+ alg: string;
8
+ use: "sig";
9
+ };
10
+ export interface AccessiJwksDocument {
11
+ keys: AccessiJwksKey[];
12
+ }
13
+ /** Path pubblico della JWKS, oppure `undefined` quando si usa HS256. */
14
+ export declare function getAccessiJwksPath(options: AccessiOptions): string | undefined;
15
+ /**
16
+ * Costruisce la JWKS pubblica con chiave corrente e chiavi precedenti.
17
+ *
18
+ * Restituisce `null` per HS256 o quando non e configurata una chiave
19
+ * pubblica/privata asimmetrica.
20
+ */
21
+ export declare function buildAccessiJwks(options: AccessiOptions): AccessiJwksDocument | null;
22
+ /** Handler Express per la JWKS. Risponde 404 se la configurazione e HS256. */
23
+ export declare function createAccessiJwksHandler(options: AccessiOptions): (req: Request, res: Response) => Response | void;
24
+ /**
25
+ * Monta la route JWKS sull'Express host.
26
+ *
27
+ * Per un'app Nest esistente passare `app.getHttpAdapter().getInstance()`.
28
+ * Restituisce il path montato, oppure `undefined` se HS256.
29
+ */
30
+ export interface AccessiJwksExpressApp {
31
+ get(path: string, handler: (req: Request, res: Response) => unknown): unknown;
32
+ }
33
+ export declare function setupAccessiJwks(app: AccessiJwksExpressApp, options: AccessiOptions): string | undefined;
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getAccessiJwksPath = getAccessiJwksPath;
4
+ exports.buildAccessiJwks = buildAccessiJwks;
5
+ exports.createAccessiJwksHandler = createAccessiJwksHandler;
6
+ exports.setupAccessiJwks = setupAccessiJwks;
7
+ const crypto_1 = require("crypto");
8
+ const DEFAULT_JWKS_PATH = "accessi/.well-known/jwks.json";
9
+ function normalizePath(value) {
10
+ return `/${value.replace(/^\/+|\/+$/g, "")}`;
11
+ }
12
+ function fingerprintPublicKey(publicKey) {
13
+ const der = (0, crypto_1.createPublicKey)(publicKey).export({ type: "spki", format: "der" });
14
+ return (0, crypto_1.createHash)("sha256").update(der).digest("base64url").slice(0, 16);
15
+ }
16
+ function toJwksKey(publicKey, keyId, algorithm) {
17
+ const jwk = (0, crypto_1.createPublicKey)(publicKey).export({ format: "jwk" });
18
+ return Object.assign(Object.assign({}, jwk), { kid: keyId !== null && keyId !== void 0 ? keyId : fingerprintPublicKey(publicKey), alg: algorithm, use: "sig" });
19
+ }
20
+ /** Path pubblico della JWKS, oppure `undefined` quando si usa HS256. */
21
+ function getAccessiJwksPath(options) {
22
+ var _a, _b;
23
+ const algorithm = (_a = options.jwtOptions.algorithm) !== null && _a !== void 0 ? _a : "HS256";
24
+ if (algorithm.startsWith("HS"))
25
+ return undefined;
26
+ return normalizePath((_b = options.jwtOptions.jwksPath) !== null && _b !== void 0 ? _b : DEFAULT_JWKS_PATH);
27
+ }
28
+ /**
29
+ * Costruisce la JWKS pubblica con chiave corrente e chiavi precedenti.
30
+ *
31
+ * Restituisce `null` per HS256 o quando non e configurata una chiave
32
+ * pubblica/privata asimmetrica.
33
+ */
34
+ function buildAccessiJwks(options) {
35
+ var _a, _b, _c, _d;
36
+ const algorithm = (_a = options.jwtOptions.algorithm) !== null && _a !== void 0 ? _a : "HS256";
37
+ if (algorithm.startsWith("HS"))
38
+ return null;
39
+ const currentPublicKey = (_b = options.jwtOptions.publicKey) !== null && _b !== void 0 ? _b : options.jwtOptions.privateKey;
40
+ if (!currentPublicKey)
41
+ return null;
42
+ const keys = [
43
+ toJwksKey(currentPublicKey, options.jwtOptions.keyId, algorithm),
44
+ ];
45
+ for (const previous of (_c = options.jwtOptions.previousPublicKeys) !== null && _c !== void 0 ? _c : []) {
46
+ const previousAlgorithm = (_d = previous.algorithm) !== null && _d !== void 0 ? _d : algorithm;
47
+ keys.push(toJwksKey(previous.publicKey, previous.keyId, previousAlgorithm));
48
+ }
49
+ // Deduplica per kid: la chiave corrente vince sulle precedenti.
50
+ const unique = new Map();
51
+ for (const key of keys) {
52
+ if (!unique.has(key.kid))
53
+ unique.set(key.kid, key);
54
+ }
55
+ return { keys: [...unique.values()] };
56
+ }
57
+ /** Handler Express per la JWKS. Risponde 404 se la configurazione e HS256. */
58
+ function createAccessiJwksHandler(options) {
59
+ return (_req, res) => {
60
+ const document = buildAccessiJwks(options);
61
+ if (!document) {
62
+ return res.status(404).json({
63
+ severity: "error",
64
+ status: 404,
65
+ statusCode: 2,
66
+ code: "ACCESSI_JWKS_NOT_AVAILABLE",
67
+ error: "ACCESSI_JWKS_NOT_AVAILABLE",
68
+ message: "JWKS non disponibile: Accessi usa HS256 o non ha una chiave asimmetrica.",
69
+ });
70
+ }
71
+ res.setHeader("Content-Type", "application/json");
72
+ res.setHeader("Cache-Control", "public, max-age=300");
73
+ return res.status(200).send(document);
74
+ };
75
+ }
76
+ function setupAccessiJwks(app, options) {
77
+ const path = getAccessiJwksPath(options);
78
+ if (!path)
79
+ return undefined;
80
+ app.get(path, createAccessiJwksHandler(options));
81
+ return path;
82
+ }
83
+ //# sourceMappingURL=jwks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jwks.js","sourceRoot":"","sources":["../../../src/accessi-module/security/jwks.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash, createPublicKey } from \"crypto\";\nimport type { JsonWebKey } from \"crypto\";\nimport type { Request, Response } from \"express\";\nimport type { AccessiOptions } from \"../AccessiModule\";\n\n/** Chiave pubblica in formato JWK, pronta per una risposta JWKS. */\nexport type AccessiJwksKey = JsonWebKey & {\n kid: string;\n alg: string;\n use: \"sig\";\n};\n\nexport interface AccessiJwksDocument {\n keys: AccessiJwksKey[];\n}\n\nconst DEFAULT_JWKS_PATH = \"accessi/.well-known/jwks.json\";\n\nfunction normalizePath(value: string): string {\n return `/${value.replace(/^\\/+|\\/+$/g, \"\")}`;\n}\n\nfunction fingerprintPublicKey(publicKey: string | Buffer): string {\n const der = createPublicKey(publicKey).export({ type: \"spki\", format: \"der\" });\n return createHash(\"sha256\").update(der).digest(\"base64url\").slice(0, 16);\n}\n\nfunction toJwksKey(\n publicKey: string | Buffer,\n keyId: string | undefined,\n algorithm: string,\n): AccessiJwksKey {\n const jwk = createPublicKey(publicKey).export({ format: \"jwk\" }) as JsonWebKey;\n return {\n ...jwk,\n kid: keyId ?? fingerprintPublicKey(publicKey),\n alg: algorithm,\n use: \"sig\",\n };\n}\n\n/** Path pubblico della JWKS, oppure `undefined` quando si usa HS256. */\nexport function getAccessiJwksPath(options: AccessiOptions): string | undefined {\n const algorithm = options.jwtOptions.algorithm ?? \"HS256\";\n if (algorithm.startsWith(\"HS\")) return undefined;\n return normalizePath(options.jwtOptions.jwksPath ?? DEFAULT_JWKS_PATH);\n}\n\n/**\n * Costruisce la JWKS pubblica con chiave corrente e chiavi precedenti.\n *\n * Restituisce `null` per HS256 o quando non e configurata una chiave\n * pubblica/privata asimmetrica.\n */\nexport function buildAccessiJwks(options: AccessiOptions): AccessiJwksDocument | null {\n const algorithm = options.jwtOptions.algorithm ?? \"HS256\";\n if (algorithm.startsWith(\"HS\")) return null;\n\n const currentPublicKey = options.jwtOptions.publicKey ?? options.jwtOptions.privateKey;\n if (!currentPublicKey) return null;\n\n const keys: AccessiJwksKey[] = [\n toJwksKey(currentPublicKey, options.jwtOptions.keyId, algorithm),\n ];\n\n for (const previous of options.jwtOptions.previousPublicKeys ?? []) {\n const previousAlgorithm = previous.algorithm ?? algorithm;\n keys.push(toJwksKey(previous.publicKey, previous.keyId, previousAlgorithm));\n }\n\n // Deduplica per kid: la chiave corrente vince sulle precedenti.\n const unique = new Map<string, AccessiJwksKey>();\n for (const key of keys) {\n if (!unique.has(key.kid)) unique.set(key.kid, key);\n }\n\n return { keys: [...unique.values()] };\n}\n\n/** Handler Express per la JWKS. Risponde 404 se la configurazione e HS256. */\nexport function createAccessiJwksHandler(\n options: AccessiOptions,\n): (req: Request, res: Response) => Response | void {\n return (_req: Request, res: Response): Response | void => {\n const document = buildAccessiJwks(options);\n if (!document) {\n return res.status(404).json({\n severity: \"error\",\n status: 404,\n statusCode: 2,\n code: \"ACCESSI_JWKS_NOT_AVAILABLE\",\n error: \"ACCESSI_JWKS_NOT_AVAILABLE\",\n message: \"JWKS non disponibile: Accessi usa HS256 o non ha una chiave asimmetrica.\",\n });\n }\n\n res.setHeader(\"Content-Type\", \"application/json\");\n res.setHeader(\"Cache-Control\", \"public, max-age=300\");\n return res.status(200).send(document);\n };\n}\n\n/**\n * Monta la route JWKS sull'Express host.\n *\n * Per un'app Nest esistente passare `app.getHttpAdapter().getInstance()`.\n * Restituisce il path montato, oppure `undefined` se HS256.\n */\nexport interface AccessiJwksExpressApp {\n get(path: string, handler: (req: Request, res: Response) => unknown): unknown;\n}\n\nexport function setupAccessiJwks(\n app: AccessiJwksExpressApp,\n options: AccessiOptions,\n): string | undefined {\n const path = getAccessiJwksPath(options);\n if (!path) return undefined;\n app.get(path, createAccessiJwksHandler(options));\n return path;\n}\n"]}
@@ -0,0 +1,15 @@
1
+ import type * as jwt from "jsonwebtoken";
2
+ import type { AccessiOptions } from "../AccessiModule";
3
+ /** Algoritmi supportati per gli access token Accessi. */
4
+ export type AccessiAccessTokenAlgorithm = "HS256" | "HS384" | "HS512" | "RS256" | "RS384" | "RS512" | "ES256" | "ES384" | "ES512";
5
+ /** Configurazione di firma: chiave, algoritmo e claim standard. */
6
+ export declare function getAccessiAccessTokenSigningConfig(options: AccessiOptions): {
7
+ key: jwt.Secret;
8
+ algorithm: AccessiAccessTokenAlgorithm;
9
+ signOptions: Pick<jwt.SignOptions, "issuer" | "audience" | "keyid">;
10
+ };
11
+ /** Configurazione di verifica: chiave pubblica per RS/ES, segreto derivato per HS. */
12
+ export declare function getAccessiAccessTokenVerificationConfig(options: AccessiOptions): {
13
+ key: jwt.Secret;
14
+ verifyOptions: jwt.VerifyOptions;
15
+ };
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getAccessiAccessTokenSigningConfig = getAccessiAccessTokenSigningConfig;
4
+ exports.getAccessiAccessTokenVerificationConfig = getAccessiAccessTokenVerificationConfig;
5
+ const passwordResetToken_1 = require("./passwordResetToken");
6
+ function resolveAlgorithm(options) {
7
+ var _a;
8
+ return (_a = options.jwtOptions.algorithm) !== null && _a !== void 0 ? _a : "HS256";
9
+ }
10
+ /** Configurazione di firma: chiave, algoritmo e claim standard. */
11
+ function getAccessiAccessTokenSigningConfig(options) {
12
+ const algorithm = resolveAlgorithm(options);
13
+ const key = algorithm.startsWith("HS")
14
+ ? (0, passwordResetToken_1.getAccessiJwtSecret)(options)
15
+ : options.jwtOptions.privateKey;
16
+ if (!key) {
17
+ throw new Error(`jwtOptions.privateKey e obbligatoria per l'algoritmo ${algorithm}.`);
18
+ }
19
+ return {
20
+ key,
21
+ algorithm,
22
+ signOptions: Object.assign(Object.assign(Object.assign({}, (options.jwtOptions.issuer ? { issuer: options.jwtOptions.issuer } : {})), (options.jwtOptions.audience
23
+ ? { audience: options.jwtOptions.audience }
24
+ : {})), (options.jwtOptions.keyId ? { keyid: options.jwtOptions.keyId } : {})),
25
+ };
26
+ }
27
+ /** Configurazione di verifica: chiave pubblica per RS/ES, segreto derivato per HS. */
28
+ function getAccessiAccessTokenVerificationConfig(options) {
29
+ var _a;
30
+ const algorithm = resolveAlgorithm(options);
31
+ const key = algorithm.startsWith("HS")
32
+ ? (0, passwordResetToken_1.getAccessiJwtSecret)(options)
33
+ : ((_a = options.jwtOptions.publicKey) !== null && _a !== void 0 ? _a : options.jwtOptions.privateKey);
34
+ if (!key) {
35
+ throw new Error(`jwtOptions.publicKey e obbligatoria per verificare token ${algorithm}.`);
36
+ }
37
+ return {
38
+ key,
39
+ verifyOptions: Object.assign(Object.assign({ algorithms: [algorithm] }, (options.jwtOptions.issuer ? { issuer: options.jwtOptions.issuer } : {})), (options.jwtOptions.audience
40
+ ? { audience: options.jwtOptions.audience }
41
+ : {})),
42
+ };
43
+ }
44
+ //# sourceMappingURL=jwtTokenOptions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jwtTokenOptions.js","sourceRoot":"","sources":["../../../src/accessi-module/security/jwtTokenOptions.ts"],"names":[],"mappings":";;AAqBA,gFAyBC;AAGD,0FAyBC;AAxED,6DAA2D;AAc3D,SAAS,gBAAgB,CAAC,OAAuB;;IAC/C,OAAO,MAAA,OAAO,CAAC,UAAU,CAAC,SAAS,mCAAI,OAAO,CAAC;AACjD,CAAC;AAED,mEAAmE;AACnE,SAAgB,kCAAkC,CAAC,OAAuB;IAKxE,MAAM,SAAS,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAC5C,MAAM,GAAG,GAAG,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC;QACpC,CAAC,CAAC,IAAA,wCAAmB,EAAC,OAAO,CAAC;QAC9B,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,UAAU,CAAC;IAElC,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,KAAK,CAAC,wDAAwD,SAAS,GAAG,CAAC,CAAC;IACxF,CAAC;IAED,OAAO;QACL,GAAG;QACH,SAAS;QACT,WAAW,gDACN,CAAC,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,GACxE,CAAC,OAAO,CAAC,UAAU,CAAC,QAAQ;YAC7B,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,UAAU,CAAC,QAAuC,EAAE;YAC1E,CAAC,CAAC,EAAE,CAAC,GACJ,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CACzE;KACF,CAAC;AACJ,CAAC;AAED,sFAAsF;AACtF,SAAgB,uCAAuC,CAAC,OAAuB;;IAI7E,MAAM,SAAS,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAC5C,MAAM,GAAG,GAAG,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC;QACpC,CAAC,CAAC,IAAA,wCAAmB,EAAC,OAAO,CAAC;QAC9B,CAAC,CAAC,CAAC,MAAA,OAAO,CAAC,UAAU,CAAC,SAAS,mCAAI,OAAO,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC;IAEpE,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,KAAK,CACb,4DAA4D,SAAS,GAAG,CACzE,CAAC;IACJ,CAAC;IAED,OAAO;QACL,GAAG;QACH,aAAa,gCACX,UAAU,EAAE,CAAC,SAAS,CAAC,IACpB,CAAC,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,GACxE,CAAC,OAAO,CAAC,UAAU,CAAC,QAAQ;YAC7B,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,UAAU,CAAC,QAAyC,EAAE;YAC5E,CAAC,CAAC,EAAE,CAAC,CACR;KACF,CAAC;AACJ,CAAC","sourcesContent":["import type * as jwt from \"jsonwebtoken\";\nimport type { AccessiOptions } from \"../AccessiModule\";\nimport { getAccessiJwtSecret } from \"./passwordResetToken\";\n\n/** Algoritmi supportati per gli access token Accessi. */\nexport type AccessiAccessTokenAlgorithm =\n | \"HS256\"\n | \"HS384\"\n | \"HS512\"\n | \"RS256\"\n | \"RS384\"\n | \"RS512\"\n | \"ES256\"\n | \"ES384\"\n | \"ES512\";\n\nfunction resolveAlgorithm(options: AccessiOptions): AccessiAccessTokenAlgorithm {\n return options.jwtOptions.algorithm ?? \"HS256\";\n}\n\n/** Configurazione di firma: chiave, algoritmo e claim standard. */\nexport function getAccessiAccessTokenSigningConfig(options: AccessiOptions): {\n key: jwt.Secret;\n algorithm: AccessiAccessTokenAlgorithm;\n signOptions: Pick<jwt.SignOptions, \"issuer\" | \"audience\" | \"keyid\">;\n} {\n const algorithm = resolveAlgorithm(options);\n const key = algorithm.startsWith(\"HS\")\n ? getAccessiJwtSecret(options)\n : options.jwtOptions.privateKey;\n\n if (!key) {\n throw new Error(`jwtOptions.privateKey e obbligatoria per l'algoritmo ${algorithm}.`);\n }\n\n return {\n key,\n algorithm,\n signOptions: {\n ...(options.jwtOptions.issuer ? { issuer: options.jwtOptions.issuer } : {}),\n ...(options.jwtOptions.audience\n ? { audience: options.jwtOptions.audience as jwt.SignOptions[\"audience\"] }\n : {}),\n ...(options.jwtOptions.keyId ? { keyid: options.jwtOptions.keyId } : {}),\n },\n };\n}\n\n/** Configurazione di verifica: chiave pubblica per RS/ES, segreto derivato per HS. */\nexport function getAccessiAccessTokenVerificationConfig(options: AccessiOptions): {\n key: jwt.Secret;\n verifyOptions: jwt.VerifyOptions;\n} {\n const algorithm = resolveAlgorithm(options);\n const key = algorithm.startsWith(\"HS\")\n ? getAccessiJwtSecret(options)\n : (options.jwtOptions.publicKey ?? options.jwtOptions.privateKey);\n\n if (!key) {\n throw new Error(\n `jwtOptions.publicKey e obbligatoria per verificare token ${algorithm}.`,\n );\n }\n\n return {\n key,\n verifyOptions: {\n algorithms: [algorithm],\n ...(options.jwtOptions.issuer ? { issuer: options.jwtOptions.issuer } : {}),\n ...(options.jwtOptions.audience\n ? { audience: options.jwtOptions.audience as jwt.VerifyOptions[\"audience\"] }\n : {}),\n },\n };\n}\n"]}
@@ -1,10 +1,12 @@
1
1
  import { Request, Response } from 'express';
2
2
  import type { AccessiOptions } from '../AccessiModule';
3
+ import type { RateLimitDecision } from './rateLimitStore';
3
4
  export type PublicAuthRateLimitScope = 'login' | 'twoFactorVerify' | 'twoFactorResend' | 'register' | 'passwordResetEmail' | 'passwordResetConfirm' | 'getUserByToken' | 'adminBootstrap' | 'consoleEntry';
4
- type RateLimitDecision = {
5
- allowed: boolean;
6
- retryAfterSeconds: number;
7
- };
8
- export declare function checkPublicAuthRateLimit(options: AccessiOptions, scope: PublicAuthRateLimitScope, req: Request, identifiers?: string[]): RateLimitDecision;
5
+ /**
6
+ * Applica il rate limit per IP e, opzionalmente, per soggetto.
7
+ *
8
+ * Con `publicAuthRateLimit.store` e possibile usare uno store Redis condiviso;
9
+ * senza store usa la memoria del processo.
10
+ */
11
+ export declare function checkPublicAuthRateLimit(options: AccessiOptions, scope: PublicAuthRateLimitScope, req: Request, identifiers?: string[]): Promise<RateLimitDecision>;
9
12
  export declare function sendPublicAuthRateLimitExceeded(res: Response, retryAfterSeconds: number): Response;
10
- export {};