emilsoftware-utilities 2.0.0-dev.77 → 2.0.0-dev.78

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 (51) hide show
  1. package/dist/accessi/resource-server/token.js +2 -0
  2. package/dist/accessi/resource-server/token.js.map +1 -1
  3. package/dist/accessi/server/standalone.js +2 -1
  4. package/dist/accessi/server/standalone.js.map +1 -1
  5. package/dist/accessi-module/AccessiModule.d.ts +3 -0
  6. package/dist/accessi-module/AccessiModule.js.map +1 -1
  7. package/dist/accessi-module/Console/console.js +1 -0
  8. package/dist/accessi-module/Console/wiki.js +1 -0
  9. package/dist/accessi-module/Console/wiki.js.map +1 -1
  10. package/dist/accessi-module/Controllers/AccessiIntrospectionController.js +9 -0
  11. package/dist/accessi-module/Controllers/AccessiIntrospectionController.js.map +1 -1
  12. package/dist/accessi-module/Controllers/AuthController.js +67 -8
  13. package/dist/accessi-module/Controllers/AuthController.js.map +1 -1
  14. package/dist/accessi-module/Controllers/ConsoleEntryController.js +9 -0
  15. package/dist/accessi-module/Controllers/ConsoleEntryController.js.map +1 -1
  16. package/dist/accessi-module/Services/AuthService/AuthService.js +8 -2
  17. package/dist/accessi-module/Services/AuthService/AuthService.js.map +1 -1
  18. package/dist/accessi-module/index.d.ts +4 -0
  19. package/dist/accessi-module/index.js +15 -1
  20. package/dist/accessi-module/index.js.map +1 -1
  21. package/dist/accessi-module/observability/metrics.d.ts +79 -0
  22. package/dist/accessi-module/observability/metrics.js +216 -0
  23. package/dist/accessi-module/observability/metrics.js.map +1 -0
  24. package/dist/accessi-module/observability/redisMetrics.d.ts +12 -0
  25. package/dist/accessi-module/observability/redisMetrics.js +44 -0
  26. package/dist/accessi-module/observability/redisMetrics.js.map +1 -0
  27. package/dist/accessi-module/security/accessTokenRevocation.js +5 -3
  28. package/dist/accessi-module/security/accessTokenRevocation.js.map +1 -1
  29. package/dist/accessi-module/security/consoleEntry.js +5 -3
  30. package/dist/accessi-module/security/consoleEntry.js.map +1 -1
  31. package/dist/accessi-module/security/edgeRateLimit.js +3 -0
  32. package/dist/accessi-module/security/edgeRateLimit.js.map +1 -1
  33. package/dist/accessi-module/security/jwks.js +3 -0
  34. package/dist/accessi-module/security/jwks.js.map +1 -1
  35. package/dist/accessi-module/security/jwtTokenOptions.js +2 -0
  36. package/dist/accessi-module/security/jwtTokenOptions.js.map +1 -1
  37. package/dist/accessi-module/security/publicAuthRateLimit.js +3 -0
  38. package/dist/accessi-module/security/publicAuthRateLimit.js.map +1 -1
  39. package/dist/accessi-module/security/rateLimitStore.js +4 -2
  40. package/dist/accessi-module/security/rateLimitStore.js.map +1 -1
  41. package/dist/accessi-module/security/refreshTokenRotation.js +7 -5
  42. package/dist/accessi-module/security/refreshTokenRotation.js.map +1 -1
  43. package/dist/accessi-module/security/serviceTokenRateLimit.js +3 -0
  44. package/dist/accessi-module/security/serviceTokenRateLimit.js.map +1 -1
  45. package/dist/accessi-module/security/sessionRevocation.js +7 -3
  46. package/dist/accessi-module/security/sessionRevocation.js.map +1 -1
  47. package/dist/emilsoftware-utilities-2.0.0-dev.78.tgz +0 -0
  48. package/docs/metrics.md +153 -0
  49. package/docs/redis-load-testing.md +88 -0
  50. package/package.json +4 -3
  51. package/dist/emilsoftware-utilities-2.0.0-dev.77.tgz +0 -0
@@ -0,0 +1,153 @@
1
+ # Metriche e alert
2
+
3
+ Accessi espone metriche di dominio in formato Prometheus. Il servizio standalone
4
+ le include in `/health/metrics` insieme alle metriche HTTP di base; un host
5
+ Nest/Express può montare `renderAccessiMetrics()` su una propria rotta protetta.
6
+
7
+ ```ts
8
+ import { renderAccessiMetrics } from 'emilsoftware-utilities/accessi/server';
9
+
10
+ app.get('/internal/accessi-metrics', (_req, res) => {
11
+ res.type('text/plain; version=0.0.4').send(renderAccessiMetrics());
12
+ });
13
+ ```
14
+
15
+ Le metriche sono attive per default. Per disattivarle:
16
+
17
+ ```ts
18
+ const accessiOptions: AccessiOptions = {
19
+ // ...
20
+ metrics: { enabled: false },
21
+ };
22
+ ```
23
+
24
+ Per inviarle a OpenTelemetry, StatsD o un altro sistema:
25
+
26
+ ```ts
27
+ const accessiOptions: AccessiOptions = {
28
+ metrics: {
29
+ prefix: 'accessi',
30
+ sink: {
31
+ increment: (name, value, labels) => meter.createCounter(name).add(value, labels),
32
+ observe: (name, valueMs, labels) => histogram.record(valueMs, labels),
33
+ },
34
+ },
35
+ };
36
+ ```
37
+
38
+ `prefix` (default `accessi`) si applica alle metriche emesse dai controller; gli
39
+ store Redis usano il registry globale, sempre renderizzabile con
40
+ `renderAccessiMetrics()`.
41
+
42
+ ## Metriche di dominio
43
+
44
+ | Metrica | Tipo | Label | Significato |
45
+ | --- | --- | --- | --- |
46
+ | `accessi_auth_login_total` | counter | `result` = `success`, `failure`, `challenge`, `password_required`, `password_expired`, `sso_only`, `rate_limited`, `error` | esito del login |
47
+ | `accessi_auth_login_duration_ms` | histogram | — | durata del login |
48
+ | `accessi_auth_two_factor_total` | counter | `action` = `verify`/`resend`, `result` | verifica e reinvio 2FA |
49
+ | `accessi_auth_two_factor_duration_ms` | histogram | `action` | durata verifica 2FA |
50
+ | `accessi_auth_refresh_total` | counter | `result` = `success`, `invalid`, `reuse`, `family_revoked`, `store_unavailable`, `rate_limited` | esito del refresh |
51
+ | `accessi_auth_refresh_duration_ms` | histogram | — | durata del refresh |
52
+ | `accessi_refresh_reuse_total` | counter | `user_sessions_revoked` | riuso di refresh token oltre la grace |
53
+ | `accessi_refresh_family_revoked_total` | counter | `reason` | famiglie revocate |
54
+ | `accessi_rate_limit_blocked_total` | counter | `scope`, `layer` = `public`/`edge`/`service_token` | richieste bloccate dal rate limit |
55
+ | `accessi_introspection_total` | counter | `result` = `active`, `inactive`, `bad_request`, `error` | introspection service-to-service |
56
+ | `accessi_introspection_duration_ms` | histogram | — | durata introspection |
57
+ | `accessi_jwks_requests_total` | counter | `result` = `ok`, `unavailable` | richieste JWKS |
58
+ | `accessi_jwt_unknown_key_id_total` | counter | `purpose`, `source`, `algorithm` | token con `kid` non riconosciuto |
59
+ | `accessi_sessions_revoked_total` | counter | `reason` = `manual`, `password_change`, `disabled`, `deleted`, `refresh_token_reuse`, ... | revoche sessioni |
60
+ | `accessi_console_ticket_total` | counter | `action` = `create`/`exchange`, `result` | ticket di ingresso console |
61
+ | `accessi_store_errors_total` | counter | `store` | errori degli store Redis |
62
+ | `accessi_store_operation_duration_ms` | histogram | `store` | latenza degli script Lua Redis |
63
+
64
+ Gli store strumentati sono `rate_limit`, `edge_rate_limit`, `service_token_rate_limit`,
65
+ `console_entry`, `session_revocation`, `access_token_revocation` e
66
+ `refresh_token_rotation`.
67
+
68
+ ## Alert suggeriti
69
+
70
+ Esempio di regole Prometheus. Le soglie vanno tarate sul traffico reale.
71
+
72
+ ```yaml
73
+ groups:
74
+ - name: accessi
75
+ rules:
76
+ - alert: AccessiLoginErrorRate
77
+ expr: |
78
+ sum(rate(accessi_auth_login_total{result=~"failure|error"}[5m]))
79
+ /
80
+ clamp_min(sum(rate(accessi_auth_login_total[5m])), 0.001)
81
+ > 0.3
82
+ for: 10m
83
+ labels:
84
+ severity: warning
85
+ annotations:
86
+ summary: "Oltre il 30% dei login fallisce da 10 minuti"
87
+
88
+ - alert: AccessiRefreshReuse
89
+ expr: increase(accessi_refresh_reuse_total[5m]) > 0
90
+ for: 1m
91
+ labels:
92
+ severity: critical
93
+ annotations:
94
+ summary: "Riuso di refresh token rilevato: verificare possibili furti"
95
+
96
+ - alert: AccessiRateLimitSpike
97
+ expr: sum(rate(accessi_rate_limit_blocked_total[5m])) > 20
98
+ for: 10m
99
+ labels:
100
+ severity: warning
101
+ annotations:
102
+ summary: "Molte richieste bloccate dal rate limit: possibile brute force"
103
+
104
+ - alert: AccessiUnknownJwtKid
105
+ expr: increase(accessi_jwt_unknown_key_id_total[5m]) > 0
106
+ for: 5m
107
+ labels:
108
+ severity: warning
109
+ annotations:
110
+ summary: "Token con kid sconosciuto: rotazione chiavi incompleta?"
111
+
112
+ - alert: AccessiRedisStoreDown
113
+ expr: increase(accessi_store_errors_total[5m]) > 3
114
+ for: 5m
115
+ labels:
116
+ severity: critical
117
+ annotations:
118
+ summary: "Errori Redis: revoca, rotazione o rate limit non affidabili"
119
+
120
+ - alert: AccessiStoreLatency
121
+ expr: |
122
+ histogram_quantile(
123
+ 0.95,
124
+ sum by (le, store) (rate(accessi_store_operation_duration_ms_bucket[5m]))
125
+ ) > 50
126
+ for: 10m
127
+ labels:
128
+ severity: warning
129
+ annotations:
130
+ summary: "p95 degli script Redis sopra 50 ms per {{ $labels.store }}"
131
+
132
+ - alert: AccessiIntrospectionErrors
133
+ expr: |
134
+ sum(rate(accessi_introspection_total{result="error"}[5m]))
135
+ /
136
+ clamp_min(sum(rate(accessi_introspection_total[5m])), 0.001)
137
+ > 0.05
138
+ for: 10m
139
+ labels:
140
+ severity: warning
141
+ annotations:
142
+ summary: "Errori di introspection oltre il 5%"
143
+ ```
144
+
145
+ ## Note
146
+
147
+ - Non usare label ad alta cardinalità (email, `jti`, IP): le metriche Accessi
148
+ non le includono.
149
+ - `accessi_jwt_unknown_key_id_total` collega la rotazione chiavi descritta in
150
+ `docs/jwt-key-rotation.md`: un aumento dopo un deploy segnala che un
151
+ consumatore non ha aggiornato la JWKS o la chiave statica.
152
+ - Le metriche non sostituiscono l'audit: per gli eventi di sicurezza usare
153
+ `auditAccessi` e la sua persistenza.
@@ -0,0 +1,88 @@
1
+ # Test di concorrenza e carico su Redis
2
+
3
+ `npm run test:load:redis` verifica che rate limit, ticket console e refresh
4
+ rotation restino corretti con più istanze Accessi sullo stesso Redis, e produce
5
+ un report di throughput e latenza.
6
+
7
+ ```bash
8
+ ACCESSI_TEST_REDIS_URL=redis://127.0.0.1:6379 npm run test:load:redis
9
+ ```
10
+
11
+ La suite è opt-in: senza `ACCESSI_TEST_REDIS_URL` i test vengono saltati. In CI
12
+ si imposta `ACCESSI_TEST_REQUIRE_REDIS=1` per far fallire il job se Redis non è
13
+ raggiungibile. Usare un database Redis dedicato (es. `redis://127.0.0.1:6379/5`):
14
+ tutte le chiavi hanno un prefisso per processo e vengono cancellate a fine suite,
15
+ ma un database separato evita sorprese con Redis condivisi.
16
+
17
+ ## Scenari
18
+
19
+ | Scenario | Cosa dimostra |
20
+ | --- | --- |
21
+ | Burst login/reset/2FA | il limite globale per IP è rispettato anche se le richieste sono distribuite su due store (due istanze) |
22
+ | Ticket console | due istanze consumano lo stesso ticket: una sola ottiene il record, l'altra riceve `null` |
23
+ | Refresh rotation | due istanze consumano lo stesso `jti`: una sola vince; la revoca di famiglia è visibile all'altra istanza |
24
+ | Misure | throughput, p50, p95 e massimo per ogni scenario |
25
+
26
+ Le "due istanze" sono simulate con due connessioni Redis indipendenti e due
27
+ oggetti store distinti. La correttezza dipende dagli script Lua atomici degli
28
+ store: il test non testa il processo Node in sé, ma il comportamento condiviso
29
+ su Redis.
30
+
31
+ ## Parametri
32
+
33
+ | Variabile | Default | Note |
34
+ | --- | --- | --- |
35
+ | `ACCESSI_TEST_REDIS_URL` | assente | abilita la suite |
36
+ | `ACCESSI_TEST_REQUIRE_REDIS` | `0` | `1` trasforma lo skip in errore (CI) |
37
+ | `ACCESSI_TEST_LOAD_ITERATIONS` | `200` | operazioni per refresh rotation |
38
+ | `ACCESSI_TEST_LOAD_CONCURRENCY` | `50` | richieste concorrenti |
39
+ | `ACCESSI_TEST_REPORT_FILE` | assente | path del report Markdown |
40
+
41
+ Esempio con carico maggiore:
42
+
43
+ ```bash
44
+ ACCESSI_TEST_REDIS_URL=redis://127.0.0.1:6379/5 \
45
+ ACCESSI_TEST_LOAD_ITERATIONS=1000 \
46
+ ACCESSI_TEST_LOAD_CONCURRENCY=100 \
47
+ ACCESSI_TEST_REPORT_FILE=redis-load-report.md \
48
+ npm run test:load:redis
49
+ ```
50
+
51
+ ## Report
52
+
53
+ Il report viene sempre stampato a fine suite:
54
+
55
+ ```
56
+ | Scenario | Operazioni | Errori | Throughput (ops/s) | p50 (ms) | p95 (ms) | max (ms) |
57
+ | --- | ---: | ---: | ---: | ---: | ---: | ---: |
58
+ | rate limit login | 60 | 0 | 3200.0 | 12.10 | 24.30 | 41.00 |
59
+ | console ticket (2 istanze) | 100 | 0 | 2800.0 | 14.00 | 30.10 | 55.00 |
60
+ | refresh rotation (2 istanze) | 200 | 0 | 3000.0 | 13.00 | 27.00 | 48.00 |
61
+ ```
62
+
63
+ Con `ACCESSI_TEST_REPORT_FILE` viene scritto anche su file; la CI lo pubblica
64
+ come artifact `redis-load-report`.
65
+
66
+ Le asserzioni verificano correttezza e un limite di sicurezza sulla latenza
67
+ (p95 sotto 1–2 secondi), non un throughput minimo: il throughput dipende
68
+ dall'hardware del runner e non deve rendere la CI instabile. Per una valutazione
69
+ di capacità usare un ambiente dedicato e confrontare i numeri tra release.
70
+
71
+ ## Interpretazione
72
+
73
+ - **Errori > 0**: Redis irraggiungibile o script Lua in errore; il test fallisce.
74
+ - **p95 molto alto con throughput basso**: rete verso Redis o Redis saturo;
75
+ controllare `latency monitor` lato Redis e il pool di connessioni.
76
+ - **Più di un consumo riuscito**: violazione dell'atomicità; non distribuire la
77
+ release.
78
+ - **Limite per IP superato**: due istanze non stanno condividendo lo store o
79
+ stanno usando prefissi diversi.
80
+
81
+ ## Limiti
82
+
83
+ - Non misura il costo del database Firebird né il flusso HTTP completo: per
84
+ quello usare i test di integrazione Firebird e un ambiente di staging.
85
+ - I tempi includono il client Redis di test (RESP2 minimale) e il round-trip di
86
+ rete: sono un limite superiore, non un benchmark dell'applicazione.
87
+ - Non usare questo test contro un Redis di produzione: scrive e cancella chiavi
88
+ con prefisso, ma è comunque carico reale.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "emilsoftware-utilities",
3
- "version": "2.0.0-dev.77",
3
+ "version": "2.0.0-dev.78",
4
4
  "description": "Utilities for EmilSoftware",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -139,12 +139,13 @@
139
139
  "typecheck": "tsc --noEmit",
140
140
  "lint": "node scripts/lint.cjs",
141
141
  "test": "npm run typecheck && npm run lint && npm run test:unit",
142
- "test:unit": "node --test test/angular-components.cjs test/menus-catalog.mjs test/menu-scopes.cjs test/accessi-regressions.cjs test/password-reset-regressions.cjs test/two-factor.cjs test/accessi-schema.cjs test/utilities-regressions.cjs test/service-token-regressions.cjs test/accessi-email-config.cjs test/accessi-catalog-scripts.cjs test/openapi-config.cjs test/resource-server.cjs test/accessi-security-standards.cjs test/accessi-service.cjs test/refresh-token.cjs test/token-revocation.cjs test/accessi-client.cjs test/accessi-client-scopes.cjs test/openapi-semantic-diff.cjs test/proxy-ip.cjs test/readiness.cjs test/jwt-key-rotation.cjs test/security-headers.cjs test/auth-cookies.cjs",
143
- "test:coverage": "node --experimental-test-coverage --test-coverage-lines=50 --test-coverage-functions=60 --test-coverage-branches=60 --test test/angular-components.cjs test/menus-catalog.mjs test/menu-scopes.cjs test/accessi-regressions.cjs test/password-reset-regressions.cjs test/two-factor.cjs test/accessi-schema.cjs test/utilities-regressions.cjs test/service-token-regressions.cjs test/accessi-email-config.cjs test/accessi-catalog-scripts.cjs test/openapi-config.cjs test/resource-server.cjs test/accessi-security-standards.cjs test/accessi-service.cjs test/refresh-token.cjs test/token-revocation.cjs test/accessi-client.cjs test/accessi-client-scopes.cjs test/openapi-semantic-diff.cjs test/proxy-ip.cjs test/readiness.cjs test/jwt-key-rotation.cjs test/security-headers.cjs test/auth-cookies.cjs",
142
+ "test:unit": "node --test test/angular-components.cjs test/menus-catalog.mjs test/menu-scopes.cjs test/accessi-regressions.cjs test/password-reset-regressions.cjs test/two-factor.cjs test/accessi-schema.cjs test/utilities-regressions.cjs test/service-token-regressions.cjs test/accessi-email-config.cjs test/accessi-catalog-scripts.cjs test/openapi-config.cjs test/resource-server.cjs test/accessi-security-standards.cjs test/accessi-service.cjs test/refresh-token.cjs test/token-revocation.cjs test/accessi-client.cjs test/accessi-client-scopes.cjs test/openapi-semantic-diff.cjs test/proxy-ip.cjs test/readiness.cjs test/jwt-key-rotation.cjs test/security-headers.cjs test/auth-cookies.cjs test/metrics.cjs",
143
+ "test:coverage": "node --experimental-test-coverage --test-coverage-lines=50 --test-coverage-functions=60 --test-coverage-branches=60 --test test/angular-components.cjs test/menus-catalog.mjs test/menu-scopes.cjs test/accessi-regressions.cjs test/password-reset-regressions.cjs test/two-factor.cjs test/accessi-schema.cjs test/utilities-regressions.cjs test/service-token-regressions.cjs test/accessi-email-config.cjs test/accessi-catalog-scripts.cjs test/openapi-config.cjs test/resource-server.cjs test/accessi-security-standards.cjs test/accessi-service.cjs test/refresh-token.cjs test/token-revocation.cjs test/accessi-client.cjs test/accessi-client-scopes.cjs test/openapi-semantic-diff.cjs test/proxy-ip.cjs test/readiness.cjs test/jwt-key-rotation.cjs test/security-headers.cjs test/auth-cookies.cjs test/metrics.cjs",
144
144
  "test:accessi": "npm run test:unit",
145
145
  "test:all": "npm test && npm run test:integration && npm run test:integration:redis",
146
146
  "test:integration": "node test/integration/run-firebird.cjs 5",
147
147
  "test:integration:redis": "node --test test/integration/redis-stores.integration.cjs",
148
+ "test:load:redis": "node --test --test-concurrency=1 test/integration/redis-load.integration.cjs",
148
149
  "test:integration:fb5": "node test/integration/run-firebird.cjs 5",
149
150
  "test:integration:fb3": "node test/integration/run-firebird.cjs 3",
150
151
  "test:integration:fb25": "node test/integration/run-firebird.cjs 2.5",