emilsoftware-utilities 2.0.0-dev.72 → 2.0.0-dev.74

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.
@@ -0,0 +1,79 @@
1
+ # ADR 0001 — Split in pacchetti separati: contracts, client, resource-server, server
2
+
3
+ - **Stato:** accettato
4
+ - **Data:** 2026-10-30
5
+ - **Issue:** #9
6
+ - **Ambito:** packaging e rilascio di `emilsoftware-utilities`
7
+
8
+ ## Contesto
9
+
10
+ Il package espone gia entry point separati (`accessi/server`, `accessi/resource-server`,
11
+ `accessi/client` con gli scope `public`/`admin`/`service`, `accessi/contracts`, `accessi/openapi`,
12
+ `accessi/console`, `accessi/ui`). Hanno però un unico `package.json`, un unico numero di versione e
13
+ un unico ciclo di rilascio, mentre i consumer reali hanno esigenze diverse:
14
+
15
+ - i frontend usano solo `contracts` e `client/public` e non devono scaricare Nest, Firebird o SMTP;
16
+ - i backend fratelli usano `resource-server` e `client/service`;
17
+ - solo il backend proprietario usa `server` (database, migrazioni, console).
18
+
19
+ Oggi il tarball contiene tutto (~950 KB) e una modifica server-side obbliga un bump che i frontend
20
+ devono comunque recepire. Lo split è però un cambio invasivo per i consumer e per la release.
21
+
22
+ ## Opzioni valutate
23
+
24
+ | Opzione | Pro | Contro |
25
+ | --- | --- | --- |
26
+ | **A. Pacchetto singolo con entry point per scope (attuale)** | Zero migrazioni; un solo `npm ci`; types condivisi per costruzione; release unica e semplice; il tree-shaking elimina il codice non importato | Il tarball contiene dipendenze server anche per i frontend; versione unica per domini diversi |
27
+ | **B. Monorepo npm workspaces con 4 pacchetti** | Versioning indipendente; dipendenze minime per pacchetto; build/publish selettivi | Richiede cambiare toolchain, CI e release; 4 tarball da pubblicare; gestione versioni incrociate (`contracts` → `client`); migrazione dei consumer |
28
+ | **C. Pacchetti in repository separati** | Massima indipendenza | Frammentazione, PR incrociate, release coordinate manualmente, overhead alto per un team piccolo |
29
+
30
+ ## Decisione
31
+
32
+ Si adotta **l'opzione A** nel breve periodo, rafforzata dagli entry point per scope introdotti
33
+ nell'issue #7 e verificati dal test `npm run test:pack`.
34
+
35
+ Motivazioni:
36
+
37
+ 1. **Nessuna regressione:** gli import esistenti (`emilsoftware-utilities`,
38
+ `.../accessi/client`, ...) restano identici; una migrazione forzata avrebbe costo alto e
39
+ beneficio non dimostrato.
40
+ 2. **Tipi condivisi per costruzione:** un solo set di modelli generati da OpenAPI evita divergenze
41
+ tra client e contracts, che con pacchetti separati andrebbero versionati insieme.
42
+ 3. **Bundle:** i consumer browser usano `accessi/client/public` e `accessi/contracts`; con
43
+ `sideEffects` limitato ai soli moduli con effetti, i bundler eliminano il resto.
44
+ 4. **Release riproducibile:** `npm ci && npm run build && npm run test:pack` produce e verifica lo
45
+ stesso tarball; semantic-release pubblica un solo artefatto.
46
+
47
+ Lo split resta un'opzione futura, non un rifiuto: le soglie che lo giustificherebbero sono elencate
48
+ sotto.
49
+
50
+ ## Quando rivalutare
51
+
52
+ Riaprire la decisione (ADR successivo) se si verifica almeno una di queste condizioni:
53
+
54
+ - i frontend richiedono un rilascio di `contracts`/`client` indipendente da `server`;
55
+ - il peso delle dipendenze server nel bundle browser diventa un problema non risolvibile con il
56
+ tree-shaking (es. consumer senza bundler);
57
+ - il team cresce al punto da avere cicli di rilascio separati per server e client;
58
+ - si rende necessario pubblicare su registry diversi o con licenze diverse.
59
+
60
+ ## Piano di migrazione (se e quando)
61
+
62
+ 1. Introdurre i workspace (`npm workspaces`) mantenendo i path pubblici attuali come facade.
63
+ 2. Estrarre per primo `contracts` (zero dipendenze runtime), poi `client`, poi `resource-server`,
64
+ infine `server`.
65
+ 3. Ogni pacchetto facade re-esporta dal nuovo package, così `accessi/client`, `accessi/contracts`,
66
+ ecc. continuano a funzionare senza modifiche ai consumer.
67
+ 4. Aggiungere una test matrix che installa i tarball di tutti i pacchetti insieme e verifica i path
68
+ legacy.
69
+ 5. Deprecare i facade solo in una major successiva, con guida di migrazione.
70
+
71
+ ## Conseguenze
72
+
73
+ - **Versioning:** unico, gestito da semantic-release sul branch `dev` → `main`.
74
+ - **Compatibilita:** garantita dall'export map e dallo smoke test del tarball
75
+ (`scripts/pack-smoke-consumer.cjs`) che copre ogni subpath e gli scope client.
76
+ - **Build/publish riproducibili:** build dichiarativa (`npm run build`), configurazione npm
77
+ isolata nello smoke test, nessun passaggio manuale oltre al merge.
78
+ - **Debito accettato:** il tarball include dipendenze server anche per i consumer frontend; è un
79
+ costo noto e monitorato, non un bug.
@@ -0,0 +1,99 @@
1
+ # IP reali dietro proxy
2
+
3
+ Rate limit, audit e revoca usano `req.ip`. Se il servizio sta dietro un load balancer o un reverse
4
+ proxy, `req.ip` e l'indirizzo del proxy finche `http.trustProxy` non e configurato correttamente:
5
+
6
+ - tutti i client finiscono nello stesso bucket → il rate limit blocca utenti legittimi;
7
+ - oppure un header non fidato diventa la chiave → il rate limit si aggira cambiando
8
+ `X-Forwarded-For` a ogni richiesta;
9
+ - i log di audit riportano l'IP del proxy invece del client.
10
+
11
+ `http.trustProxy` viene applicato sia all'host sia al sotto-app Accessi (`app.set('trust proxy', …)`)
12
+ e accetta la stessa sintassi di Express: `false`, numero di hop, `true`, `'loopback'`, CIDR.
13
+
14
+ ## Valore consigliato
15
+
16
+ | Deploy | `http.trustProxy` | Note |
17
+ | --- | --- | --- |
18
+ | Servizio esposto direttamente | `false` (default) | `X-Forwarded-For` viene ignorato: nessun bypass via header |
19
+ | Un proxy davanti (Nginx, Traefik, ALB) | `1` | Il proxy deve **accodare** l'IP reale in `X-Forwarded-For` |
20
+ | Due proxy (Cloudflare + Nginx, CDN + ingress) | `2` | Contare solo i proxy che il servizio vede davvero |
21
+ | Solo proxy locali | `'loopback'` | Fidarsi di `127.0.0.1`/`::1`, non di altre reti |
22
+ | Rete privata dedicata ai proxy | `'loopback, 10.0.0.0/8'` | Elencare le CIDR dei proxy, non usare `true` |
23
+ | `trustProxy: true` | **sconsigliato** | Fidarsi di tutti gli hop rende scegliibile dal client il valore piu a sinistra |
24
+
25
+ `npm run test:proxy-ip` verifica questi comportamenti su Express: senza trust proxy l'header non
26
+ cambia `req.ip`; con `1` il client reale e l'ultimo hop; un header anteposto dal client non sposta il
27
+ bucket; con `true` il valore piu a sinistra (falsificabile) vince.
28
+
29
+ ## Regola d'oro: il proxy deve accodare, non fidarsi
30
+
31
+ `X-Forwarded-For` e una lista `client, proxy1, proxy2`. Il client puo inviare il proprio header con
32
+ qualsiasi valore: il proxy deve **accodare** (`$proxy_add_x_forwarded_for`) o **sovrascrivere**
33
+ (`$remote_addr`). Con `trustProxy` numerico Express parte da destra e si ferma dopo N hop fidati,
34
+ quindi i valori anteposti dal client vengono ignorati.
35
+
36
+ Non usare `X-Real-IP` da solo: se il servizio e raggiungibile direttamente, e falsificabile quanto
37
+ `X-Forwarded-For`.
38
+
39
+ ## Nginx
40
+
41
+ ```nginx
42
+ location /api/accessi/ {
43
+ proxy_pass http://accessi_service;
44
+
45
+ # Un solo proxy: sovrascrivere l'header e evitare valori del client.
46
+ proxy_set_header X-Forwarded-For $remote_addr;
47
+ proxy_set_header X-Forwarded-Proto $scheme;
48
+ proxy_set_header Host $host;
49
+ }
50
+ ```
51
+
52
+ Con piu proxy davanti a Nginx usare `$proxy_add_x_forwarded_for` e impostare `trustProxy` al numero
53
+ totale di hop fidati.
54
+
55
+ ## Traefik
56
+
57
+ Traefik aggiunge `X-Forwarded-For` per impostazione predefinita. Con un solo Traefik davanti al
58
+ servizio usare `trustProxy: 1`. Se Traefik e dietro un altro ingress, contare anche quello.
59
+
60
+ ## Cloudflare
61
+
62
+ Cloudflare invia `CF-Connecting-IP` e accoda `X-Forwarded-For`. Due opzioni sicure:
63
+
64
+ 1. il servizio e raggiungibile **solo** da Cloudflare (firewall/authenticated origin pulls):
65
+ `trustProxy: 1`, perche l'unico hop e Cloudflare;
66
+ 2. c'e anche un ingress/Nginx tra Cloudflare e il servizio: `trustProxy: 2`.
67
+
68
+ Non impostare `trustProxy: true`: chi raggiunge l'origin direttamente potrebbe falsificare l'IP.
69
+ Se serve `CF-Connecting-IP`, va letto solo dopo aver verificato che la richiesta arrivi da un IP
70
+ Cloudflare.
71
+
72
+ ## Configurazione
73
+
74
+ ```ts
75
+ export const accessiOptions: AccessiOptions = {
76
+ // ...
77
+ http: {
78
+ trustProxy: 1, // un solo proxy davanti
79
+ },
80
+ edgeRateLimit: {
81
+ enabled: true,
82
+ windowMs: 60_000,
83
+ maxAttempts: 300,
84
+ },
85
+ };
86
+ ```
87
+
88
+ ## Verifica in produzione
89
+
90
+ 1. Da un client esterno, chiamare un endpoint pubblico con un header inventato:
91
+
92
+ ```bash
93
+ curl -H 'X-Forwarded-For: 6.6.6.6' https://accessi.example.com/api/accessi/auth/login
94
+ ```
95
+
96
+ 2. Controllare che l'audit (`accessi_rate_limited`, `edge_rate_limited`) riporti l'IP reale del
97
+ client e non `6.6.6.6`.
98
+ 3. Superare il limite da due IP diversi: se il secondo IP e bloccato subito, il bucket e condiviso e
99
+ `trustProxy` e troppo basso. Se cambiando header si sblocca, `trustProxy` e troppo alto.
@@ -50,6 +50,31 @@ GET /health/metrics -> metriche Prometheus testuali
50
50
  - `healthCheck()` degli store Redis configurati
51
51
  - `readinessChecks` aggiuntivi passati dal chiamante
52
52
 
53
+ ### Host Nest con `AccessiModule.forRoot`
54
+
55
+ Un'app Nest che monta Accessi in-process ottiene gli stessi endpoint con il modulo di health:
56
+
57
+ ```ts
58
+ import { AccessiModule, createAccessiHealthModule } from "emilsoftware-utilities/accessi/server";
59
+
60
+ @Module({
61
+ imports: [
62
+ AccessiModule.forRoot(accessiOptions),
63
+ createAccessiHealthModule({
64
+ accessiOptions,
65
+ readinessChecks: [{ name: "coda", check: async () => queue.isReady() }],
66
+ path: "/health", // default
67
+ }),
68
+ ],
69
+ })
70
+ export class AppModule {}
71
+ ```
72
+
73
+ Il contratto e identico al servizio standalone (200/503, `checks` con `bootstrap`, `database`,
74
+ store configurati e check aggiuntivi). Per un uso programmatico (senza controller) ci sono
75
+ `checkAccessiReadiness(accessiOptions, checks)`, `createAccessiLiveReport()` e
76
+ `readinessHttpStatus(report)`.
77
+
53
78
  ## Esecuzione come processo
54
79
 
55
80
  In produzione il servizio viene eseguito come processo Node gestito da systemd, PM2 o dal supervisor del cliente:
package/package.json CHANGED
@@ -1,10 +1,22 @@
1
1
  {
2
2
  "name": "emilsoftware-utilities",
3
- "version": "2.0.0-dev.72",
3
+ "version": "2.0.0-dev.74",
4
4
  "description": "Utilities for EmilSoftware",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
+ "typesVersions": {
9
+ "*": {
10
+ "accessi/*": [
11
+ "dist/accessi/*"
12
+ ]
13
+ }
14
+ },
15
+ "sideEffects": [
16
+ "./dist/index.js",
17
+ "./dist/firebird-compat.js",
18
+ "./dist/accessi-module/Console/console.js"
19
+ ],
8
20
  "exports": {
9
21
  ".": {
10
22
  "types": "./dist/index.d.ts",
@@ -105,9 +117,11 @@
105
117
  },
106
118
  "scripts": {
107
119
  "release": "semantic-release",
108
- "build": "npm run generate:accessi-openapi && npm run check:accessi-openapi && npm run generate:accessi-client && npm run generate:accessi-wiki && tsc && tsc -p tsconfig.console.json --noEmit && npm run build:accessi-console && npm run copy-views",
120
+ "build": "npm run generate:accessi-openapi && npm run check:accessi-openapi && npm run check:accessi-openapi-diff && npm run generate:accessi-client && npm run generate:accessi-wiki && tsc && tsc -p tsconfig.console.json --noEmit && npm run build:accessi-console && npm run copy-views",
109
121
  "check:accessi-openapi": "node scripts/check-accessi-openapi.cjs",
110
122
  "accessi:update-openapi-fingerprint": "node scripts/check-accessi-openapi.cjs --update",
123
+ "check:accessi-openapi-diff": "node scripts/openapi-semantic-diff.cjs",
124
+ "accessi:update-openapi-baseline": "node scripts/openapi-semantic-diff.cjs --update",
111
125
  "generate:accessi-openapi": "ts-node --transpile-only src/scripts/generateAccessiOpenApi.ts",
112
126
  "generate:accessi-client": "orval --config orval.config.ts",
113
127
  "generate:accessi-wiki": "ts-node --transpile-only src/scripts/generateAccessiWiki.ts",
@@ -124,8 +138,8 @@
124
138
  "typecheck": "tsc --noEmit",
125
139
  "lint": "node scripts/lint.cjs",
126
140
  "test": "npm run typecheck && npm run lint && npm run test:unit",
127
- "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",
128
- "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",
141
+ "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",
142
+ "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",
129
143
  "test:accessi": "npm run test:unit",
130
144
  "test:all": "npm test && npm run test:integration && npm run test:integration:redis",
131
145
  "test:integration": "node test/integration/run-firebird.cjs 5",
@@ -134,6 +148,8 @@
134
148
  "test:integration:fb3": "node test/integration/run-firebird.cjs 3",
135
149
  "test:integration:fb25": "node test/integration/run-firebird.cjs 2.5",
136
150
  "test:accessi-console": "npm run build && node test/console-two-factor.cjs",
151
+ "test:pack": "node scripts/pack-smoke-test.cjs",
152
+ "test:proxy-ip": "node --test test/proxy-ip.cjs test/readiness.cjs",
137
153
  "accessi:seed-admin": "node scripts/seed-accessi-admin.cjs",
138
154
  "accessi:console": "node scripts/run-accessi-console.cjs",
139
155
  "test:accessi-schema": "node --test test/accessi-schema.cjs",