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.
- package/README.MD +44 -0
- package/dist/accessi/server/standalone.d.ts +2 -4
- package/dist/accessi/server/standalone.js +4 -79
- package/dist/accessi/server/standalone.js.map +1 -1
- package/dist/accessi-module/Console/console.js +4 -1
- package/dist/accessi-module/Console/wiki.js +4 -1
- package/dist/accessi-module/Console/wiki.js.map +1 -1
- package/dist/accessi-module/health/nest.d.ts +31 -0
- package/dist/accessi-module/health/nest.js +89 -0
- package/dist/accessi-module/health/nest.js.map +1 -0
- package/dist/accessi-module/health/readiness.d.ts +53 -0
- package/dist/accessi-module/health/readiness.js +110 -0
- package/dist/accessi-module/health/readiness.js.map +1 -0
- package/dist/accessi-module/index.d.ts +4 -0
- package/dist/accessi-module/index.js +9 -1
- package/dist/accessi-module/index.js.map +1 -1
- package/dist/emilsoftware-utilities-2.0.0-dev.74.tgz +0 -0
- package/docs/accessi-openapi-baseline.json +4500 -0
- package/docs/accessi-openapi-breaking-allowlist.json +1 -0
- package/docs/adr/0001-split-package.md +79 -0
- package/docs/proxy-ip.md +99 -0
- package/docs/standalone-service.md +25 -0
- package/package.json +20 -4
- package/dist/emilsoftware-utilities-2.0.0-dev.72.tgz +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -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.
|
package/docs/proxy-ip.md
ADDED
|
@@ -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.
|
|
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",
|
|
Binary file
|