emilsoftware-utilities 2.0.0-dev.81 → 2.0.0-dev.82

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,45 @@
1
+ # Template di deployment multi-tenant
2
+
3
+ File pronti da adattare. Non contengono segreti reali: usare `<placeholder>` o
4
+ variabili d'ambiente/secret manager.
5
+
6
+ | Percorso | Uso |
7
+ | --- | --- |
8
+ | `systemd/accessi-tenant@.service` | unità template, una istanza per tenant |
9
+ | `systemd/tenant.secrets.example` | soli segreti, permessi `0600` |
10
+ | `docker/docker-compose.yml` | due tenant con volumi config read-only |
11
+ | `nginx/accessi-tenants.conf` | routing per host e variante path |
12
+ | `traefik/dynamic.yml` | routing host, header e health check |
13
+
14
+ ## Flusso
15
+
16
+ 1. Creare `tenants/<id>.environment.ts` (vedi `docs/multi-tenancy.md`).
17
+ 2. Validare: `npm run accessi:tenants -- --dir ./tenants --check`.
18
+ 3. Preparare i segreti fuori dal repository (`/etc/accessi/tenants/<id>.secrets`
19
+ o `secrets/<id>.env` con permessi `0600`).
20
+ 4. Avviare il processo con `ACCESSI_TENANT_ENVIRONMENT` e, se serve,
21
+ `PORT`/`ACCESSI_TENANT_ID`.
22
+ 5. Configurare il proxy sull'host o sul path del tenant.
23
+ 6. Verificare `/health/live`, `/health/ready` e `/health/metrics`.
24
+
25
+ La porta predefinita è `tenant.port`; `PORT` ha precedenza. I template non
26
+ pubblicano le porte interne: il proxy è l'unico punto esposto.
27
+
28
+ ## Variante path
29
+
30
+ Per `tenant.path = '/acme'` il proxy deve rimuovere `/acme` prima di inoltrare
31
+ (vedi `nginx/accessi-tenants.conf` e `traefik/dynamic.yml`). In alternativa
32
+ servire ogni tenant su un host dedicato.
33
+
34
+ ## Sicurezza
35
+
36
+ - Non committare `.secrets`/`secrets/*.env`.
37
+ - Montare la cartella configurazione in read-only.
38
+ - `read_only: true`, `no-new-privileges`, utente non root (immagine
39
+ personalizzata) e `ProtectSystem=strict` nell'unità systemd.
40
+ - Non usare l'header `x-accessi-tenant` dal client: solo il proxy può
41
+ impostarlo, con `allowHeader: true` lato router.
42
+ - Un processo per tenant: non condividere `AccessiModule` tra tenant diversi.
43
+
44
+ La CI esegue `npm run check:deploy` per verificare presenza e coerenza dei
45
+ template e l'assenza di segreti.
@@ -0,0 +1,51 @@
1
+ # Accessi multi-tenant con Docker Compose.
2
+ #
3
+ # I file *.environment.ts sono montati read-only; i segreti arrivano da
4
+ # env_file esterni al repository. La porta viene da tenant.port; PORT puo
5
+ # sovrascriverla.
6
+ version: "3.9"
7
+
8
+ x-accessi-tenant: &accessi-tenant
9
+ image: emilsoftware/accessi:2
10
+ restart: unless-stopped
11
+ read_only: true
12
+ tmpfs:
13
+ - /tmp
14
+ security_opt:
15
+ - no-new-privileges:true
16
+ healthcheck:
17
+ test:
18
+ - CMD-SHELL
19
+ - >-
20
+ node -e "fetch('http://127.0.0.1:' + (process.env.PORT || '3200') + '/health/live')
21
+ .then((response) => process.exit(response.ok ? 0 : 1))
22
+ .catch(() => process.exit(1))"
23
+ interval: 15s
24
+ timeout: 5s
25
+ retries: 5
26
+ start_period: 30s
27
+
28
+ services:
29
+ accessi-acme:
30
+ <<: *accessi-tenant
31
+ environment:
32
+ ACCESSI_TENANT_ENVIRONMENT: /config/acme.environment.ts
33
+ ACCESSI_TENANT_ID: acme
34
+ env_file:
35
+ - ./secrets/acme.env
36
+ volumes:
37
+ - ./tenants:/config:ro
38
+ ports:
39
+ - "127.0.0.1:3201:3201"
40
+
41
+ accessi-beta:
42
+ <<: *accessi-tenant
43
+ environment:
44
+ ACCESSI_TENANT_ENVIRONMENT: /config/beta.environment.ts
45
+ ACCESSI_TENANT_ID: beta
46
+ env_file:
47
+ - ./secrets/beta.env
48
+ volumes:
49
+ - ./tenants:/config:ro
50
+ ports:
51
+ - "127.0.0.1:3202:3202"
@@ -0,0 +1,41 @@
1
+ # Accessi multi-tenant dietro Nginx.
2
+ #
3
+ # Ogni tenant ha un processo sulla propria porta interna. Il proxy instrada per
4
+ # host e non espone mai le porte interne. Sostituire i domini e le porte.
5
+
6
+ map $host $accessi_tenant_upstream {
7
+ accessi.acme.example.com http://127.0.0.1:3201;
8
+ accessi.beta.example.com http://127.0.0.1:3202;
9
+ default http://127.0.0.1:3201;
10
+ }
11
+
12
+ server {
13
+ listen 443 ssl http2;
14
+ server_name accessi.acme.example.com accessi.beta.example.com;
15
+
16
+ ssl_certificate /etc/letsencrypt/live/accessi.example.com/fullchain.pem;
17
+ ssl_certificate_key /etc/letsencrypt/live/accessi.example.com/privkey.pem;
18
+
19
+ client_max_body_size 10m;
20
+
21
+ location / {
22
+ proxy_pass $accessi_tenant_upstream;
23
+ proxy_http_version 1.1;
24
+ proxy_set_header Host $host;
25
+ proxy_set_header X-Real-IP $remote_addr;
26
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
27
+ proxy_set_header X-Forwarded-Proto $scheme;
28
+ proxy_set_header X-Forwarded-Host $host;
29
+ proxy_read_timeout 60s;
30
+ }
31
+ }
32
+
33
+ # Variante path-based: il prefix /acme viene rimosso prima di inoltrare ad
34
+ # Accessi, che serve il tenant alla radice.
35
+ #
36
+ # location /acme/ {
37
+ # rewrite ^/acme/(.*)$ /$1 break;
38
+ # proxy_pass http://127.0.0.1:3201;
39
+ # proxy_set_header Host $host;
40
+ # proxy_set_header X-Forwarded-Proto $scheme;
41
+ # }
@@ -0,0 +1,46 @@
1
+ # Accessi multi-tenant con systemd
2
+ #
3
+ # Installazione:
4
+ # cp deploy/systemd/accessi-tenant@.service /etc/systemd/system/
5
+ # mkdir -p /etc/accessi/tenants
6
+ # cp tenants/acme.environment.ts /etc/accessi/tenants/acme.environment.ts
7
+ # install -m 0600 deploy/systemd/tenant.secrets.example /etc/accessi/tenants/acme.secrets
8
+ # systemctl daemon-reload
9
+ # systemctl enable --now accessi-tenant@acme
10
+ #
11
+ # La porta viene letta da tenant.port nel file *.environment.ts. Per forzarla:
12
+ # systemctl edit accessi-tenant@acme
13
+ # [Service]
14
+ # Environment=PORT=3201
15
+
16
+ [Unit]
17
+ Description=Accessi Service tenant %i
18
+ Documentation=file:///opt/accessi/docs/multi-tenancy.md
19
+ After=network-online.target
20
+ Wants=network-online.target
21
+
22
+ [Service]
23
+ Type=simple
24
+ WorkingDirectory=/opt/accessi
25
+ # Solo segreti (password DB, SMTP, chiavi JWT): il manifest tipizzato e' separato.
26
+ EnvironmentFile=-/etc/accessi/tenants/%i.secrets
27
+ Environment=ACCESSI_TENANT_ENVIRONMENT=/etc/accessi/tenants/%i.environment.ts
28
+ Environment=ACCESSI_TENANT_ID=%i
29
+
30
+ ExecStart=/usr/bin/node /opt/accessi/dist/scripts/startAccessiTenantService.js
31
+ Restart=on-failure
32
+ RestartSec=5
33
+ TimeoutStopSec=30
34
+ KillSignal=SIGTERM
35
+
36
+ User=accessi
37
+ Group=accessi
38
+ NoNewPrivileges=true
39
+ PrivateTmp=true
40
+ ProtectSystem=strict
41
+ ProtectHome=true
42
+ ReadWritePaths=/var/lib/accessi
43
+ LimitNOFILE=65536
44
+
45
+ [Install]
46
+ WantedBy=multi-user.target
@@ -0,0 +1,11 @@
1
+ # Esempio di file segreti per un tenant systemd.
2
+ # Copiare in /etc/accessi/tenants/<id>.secrets con permessi 0600.
3
+ # Non committare valori reali.
4
+
5
+ ACME_DB_PASSWORD=<db-password>
6
+ ACME_JWT_SECRET=<jwt-secret-almeno-32-caratteri>
7
+ ACME_JWT_PRIVATE_KEY=<pem-su-una-riga-con-\n>
8
+ ACME_JWT_PUBLIC_KEY=<pem-su-una-riga-con-\n>
9
+ ACME_ENCRYPTION_KEY=<32-caratteri-casuali>
10
+ ACME_SMTP_USER=<smtp-user>
11
+ ACME_SMTP_PASSWORD=<smtp-password>
@@ -0,0 +1,57 @@
1
+ # Accessi multi-tenant con Traefik (file provider).
2
+ #
3
+ # I servizi Accessi ascoltano su porte interne; Traefik termina TLS e instrada
4
+ # per host. Per la variante path usare un middleware StripPrefix.
5
+
6
+ http:
7
+ routers:
8
+ accessi-acme:
9
+ rule: Host(`accessi.acme.example.com`)
10
+ entryPoints:
11
+ - websecure
12
+ tls: {}
13
+ service: accessi-acme
14
+ middlewares:
15
+ - accessi-headers
16
+
17
+ accessi-beta:
18
+ rule: Host(`accessi.beta.example.com`)
19
+ entryPoints:
20
+ - websecure
21
+ tls: {}
22
+ service: accessi-beta
23
+ middlewares:
24
+ - accessi-headers
25
+
26
+ middlewares:
27
+ accessi-headers:
28
+ headers:
29
+ stsSeconds: 31536000
30
+ stsIncludeSubdomains: true
31
+ contentTypeNosniff: true
32
+ referrerPolicy: no-referrer
33
+ customResponseHeaders:
34
+ Permissions-Policy: "camera=(), microphone=(), geolocation=()"
35
+ # Variante path-based:
36
+ # accessi-strip-acme:
37
+ # stripPrefix:
38
+ # prefixes:
39
+ # - /acme
40
+
41
+ services:
42
+ accessi-acme:
43
+ loadBalancer:
44
+ healthCheck:
45
+ path: /health/ready
46
+ interval: 15s
47
+ timeout: 3s
48
+ servers:
49
+ - url: http://127.0.0.1:3201
50
+ accessi-beta:
51
+ loadBalancer:
52
+ healthCheck:
53
+ path: /health/ready
54
+ interval: 15s
55
+ timeout: 3s
56
+ servers:
57
+ - url: http://127.0.0.1:3202
@@ -1062,7 +1062,7 @@ var WIKI_SECTIONS = [
1062
1062
  ]
1063
1063
  ),
1064
1064
  p(`Topologia consigliata: un solo Accessi Service possiede database, encryptionKey, SMTP, chiavi JWT e console. Gli altri backend usano resource-server e client, senza credenziali del database Accessi.`),
1065
- p(`Multi-tenancy: un database per tenant, configurato da file <id>.environment.ts tipizzati (AccessiTenantEnvironment). Il database resta invariato (nessuna colonna tenant); ogni tenant gira in un processo separato con chiavi, issuer e store propri. Validazione e avvio con npm run accessi:tenants; dettagli in docs/multi-tenancy.md.`),
1065
+ p(`Multi-tenancy: un database per tenant, configurato da file <id>.environment.ts tipizzati (AccessiTenantEnvironment). Il database resta invariato (nessuna colonna tenant); ogni tenant gira in un processo separato con chiavi, issuer e store propri. Validazione e avvio con npm run accessi:tenants; template systemd/Docker/Nginx/Traefik in deploy/; dettagli in docs/multi-tenancy.md.`),
1066
1066
  note("warn", `Root barrel legacy`, `Il root barrel e i deep import restano supportati fino alla prossima major. I nuovi consumer devono usare gli entry point della tabella; la mappa di migrazione e in docs/import-migration.md.`),
1067
1067
  p(`Per avviare Accessi come servizio autonomo usare startAccessiService: espone /health/live, /health/ready, /health/metrics, valida la configurazione e gestisce SIGTERM/SIGINT.`),
1068
1068
  p(`Per parlare con piu Accessi Service o tenant dallo stesso processo usare createAccessiClient: ogni istanza ha base URL e token propri, isolati anche tra richieste concorrenti. configureAccessiClient resta il default globale per le funzioni esportate dal package.`),
@@ -1854,7 +1854,7 @@ function buildAiDigest() {
1854
1854
  }
1855
1855
 
1856
1856
  // src/accessi-module/Console/changelog.md
1857
- var changelog_default = "# Changelog Accessi\n\nNovit\xE0 e modifiche rilevanti della libreria `emilsoftware-utilities` (modulo Accessi).\n\n## Non rilasciato\n\n### Cambiamenti\n- Import: gli entry point (`accessi/server`, `accessi/resource-server`, `accessi/client*`, `accessi/contracts*`, `accessi/openapi`, `accessi/console`, `accessi/ui`) sono la via consigliata. Root barrel e deep import restano supportati fino alla prossima major; mappa di migrazione in `docs/import-migration.md`.\n- Sicurezza HTTP: CSP configurabile con nonce per la console, `'unsafe-inline'` documentato solo per Swagger UI, header frame/referrer/cache personalizzabili; guida in `docs/security-headers.md`.\n- Autenticazione: modalita opzionale a cookie `HttpOnly` con CSRF double-submit, refresh da cookie e supporto console; Bearer resta il default. Guida in `docs/auth-cookies.md`.\n- Observability: metriche Prometheus di dominio per login, 2FA, refresh, rate limit, introspection, JWKS, revoche, ticket console e store Redis, con alert suggeriti in `docs/metrics.md`.\n- Audit: sink memory/Redis/HTTP/callback, redazione ricorsiva, retention, query ed export NDJSON admin in `docs/audit.md`.\n- Multi-tenancy: un database per tenant senza modifiche allo schema, file `<id>.environment.ts` tipizzati (`AccessiTenantEnvironment`), validazione isolamento, risoluzione host/path/header fidato, supervisore processi e store Redis con prefisso automatico in `docs/multi-tenancy.md`.\n- Resource server: aggiunti `createAccessiAuthMiddleware`, `createAccessiAuthGuard`, `createAccessiTokenVerifier` e `createAccessiIntrospectionClient` per backend fratelli che non possiedono il database Accessi. Supporta HS256, RS/ES con chiave pubblica PEM e JWKS con cache.\n- Introspection: nuovo `POST /api/accessi/auth/introspect` protetto da service token con scope `accessi:introspect`. Restituisce `active`, profilo utente e grant correnti.\n- JWT: `jwtOptions` accetta `algorithm`, `issuer`, `audience`, `privateKey`, `publicKey`, `keyId`, `jwksPath` e `previousPublicKeys`. Default HS256 invariato; RS/ES sono consigliati per la topologia multi-servizio.\n- JWKS: con RS/ES Accessi pubblica `/accessi/.well-known/jwks.json` (path configurabile) con chiave corrente e chiavi precedenti per la rotazione. I resource server possono usare `jwks.url`.\n- Console ticket: introdotto `ConsoleEntryStore`. Default in memoria; `createRedisConsoleEntryStore` salva il JWT cifrato in Redis, usa TTL e consume atomico per il monouso multi-istanza. Configurazione in `consoleEntry.store` e `consoleEntry.ttlMs`.\n- Rate limit: introdotto `RateLimitStore`. `createRedisRateLimitStore` usa `INCR` + `PEXPIRE` atomici per contatori condivisi in cluster. Configurazione in `publicAuthRateLimit.store` e `serviceTokens.rateLimitStore`; default in memoria invariato.\n- Edge rate limit: aggiunto `createAccessiEdgeRateLimitMiddleware` e `AccessiOptions.edgeRateLimit` per una soglia grossolana per IP davanti alle rotte Accessi. Fail closed se lo store non risponde; supporta lo stesso store Redis.\n- Servizio standalone: aggiunti `createAccessiService`, `startAccessiService`, `/health/live`, `/health/ready`, `/health/metrics` e shutdown graceful. Aggiunti gli script `accessi:serve` e `accessi:serve:dist` per l'esecuzione come processo Node.\n- Configurazione: aggiunti `validateAccessiOptions` e `assertValidAccessiOptions`; il servizio standalone li applica al bootstrap.\n- HTTP: aggiunte opzioni `http.cors`, `http.trustProxy`, `http.securityHeaders`. La JWKS viene montata automaticamente anche con `AccessiModule.forRoot` tramite `HttpAdapterHost`.\n- SSO: aggiunto `POST /api/accessi/federated-auth/exchange` protetto da service token con scope `accessi:federated:exchange` per scambiare `{ provider, subject }` verificati da un backend fratello.\n- Audit: aggiunto `auditAccessi` per eventi di sicurezza strutturati; resource server ed edge rate limit registrano fallimenti di autenticazione, blocchi e rate limit non disponibile.\n- Contratto: aggiunto `npm run check:accessi-openapi`, eseguito nel build, che blocca path fuori prefisso, operationId duplicati/mancanti, $ref rotti, path legacy e rimozioni di operation/schema rispetto al fingerprint versionato.\n- Contratto: aggiunto `npm run accessi:update-openapi-fingerprint` per aggiornare intenzionalmente il fingerprint dopo una modifica breaking approvata.\n- Console: aggiunto `consoleApiBaseUrl` per servire UI e API su domini diversi; il valore viene iniettato in `window.__ACCESSI_API_BASE_URL__`.\n- Chiavi: aggiunto `npm run accessi:generate-jwt-keys` per generare coppia RS256/ES256, `keyId` e JWKS pubblica.\n- Sessioni: aggiunta la revoca per utente con `SessionRevocationStore` memory/Redis e `POST /api/accessi/auth/revoke-sessions`. Resource server e guard controllano il cutoff `iat`.\n- Refresh token: con `jwtOptions.refreshExpiresIn` login, 2FA e SSO emettono anche `refreshToken` (typ=refresh) e `POST /api/accessi/auth/refresh` restituisce una nuova coppia access+refresh. Gli access token hanno ora `jti`.\n- Refresh rotation: con `refreshTokenRotation.store` il refresh token e monouso e la rotazione e atomica tra istanze; due refresh concorrenti producono un solo successo.\n- Contratto: il fingerprint OpenAPI ora blocca anche la rimozione di operation/schema rispetto alla baseline versionata.\n- OpenAPI: aggiunto `mergeOpenApiDocuments` per unire il documento Accessi nel documento unico dell'host con conflitti espliciti.\n- Console: aggiunto `buildAccessiConsoleUrl` per linkare la console centrale senza mettere JWT nell'URL.\n- Rinomina: la configurazione e gli helper della documentazione usano `openapi`/`OpenApi` (`AccessiOptions.openapi`, `OpenApiSetupOptions`, `setupOpenApi`, `DEFAULT_ACCESSI_OPENAPI`); i vecchi nomi `swagger` non sono piu esportati.\n- Token Accessi: `TokenResult.expiresIn` riporta sempre la validita effettiva in secondi (es. `86400`), anche quando `jwtOptions.expiresIn` usa un formato leggibile come `24h`. I client possono quindi esporre `expires_in` anche per il token di login normale.\n- Console: nuova funzione **Login as** per superutenti e admin. Dalla lista utenti si genera un URL verso l'applicazione scelta con `?loginAsToken=<jwt>`; il token dura quanto la sessione Accessi standard (`jwtOptions.expiresIn`), registra l'attore nelle claim `act`/`loginAs` e resta soggetto alla gerarchia ADMIN > SUPER. Endpoint: `POST /api/accessi/user/login-as/:codiceUtente`.\n- Console: rimosse le sezioni **Filtri utente** e **Tipi filtro**. I filtri applicativi sono deprecati e non vengono pi\xF9 letti n\xE9 scritti dalla console; le tabelle restano invariate e la gestione torna manuale sul database.\n- Console: le conferme operative (es. \"Ruoli aggiornati\") ora compaiono come notifiche in alto a destra e scompaiono automaticamente.\n- Console: il salvataggio del profilo non invia pi\xF9 i campi della tabella `FILTRI`, evitando payload rifiutati dal backend. `UserService` tocca `FILTRI` solo se il chiamante invia esplicitamente almeno un campo legacy.\n- Console: scheda utente riorganizzata. \"Indietro\" resta nella barra superiore; stato registrazione, disattivazione ed eliminazione definitiva sono in un form coerente \"Stato e sicurezza\", con icone e tooltip allineati.\n- Console: pagina Changelog a larghezza piena, con intestazioni di versione evidenziate e sezioni orizzontali leggibili.\n- Seed `npm run accessi:seed-admin`: crea o aggiorna un admin completo (`FLGSUPER = 1` e `FLGADMIN = 1`) e corregge il nome colonna usato in precedenza (`FLGADMIN`).\n- Console: la pagina di default nella scheda utente \xE8 ora un autocomplete con ricerca. Propone tutte le rotte del catalogo menu, si filtra scrivendo ed \xE8 ancora possibile inserire un valore libero.\n- Autorizzazioni: i grant effettivi sommano ruoli e permessi diretti prendendo il livello massimo per menu. Un permesso diretto con livello inferiore (o 0) non annulla pi\xF9 il grant del ruolo.\n- Ruoli legacy: un menu senza descrizione non viene pi\xF9 scartato e un `TIPABI` mancante viene letto come livello 10, evitando utenti con ruolo ma senza accesso.\n- Grant dei ruoli arricchiti con i metadati di navigazione del catalogo menu (`pagina`, icona, gruppo, tipo): il frontend riceve gli stessi campi dei grant diretti e pu\xF2 costruire correttamente menu e rotte.\n- Sicurezza: `GET /api/accessi/permission/grants/:codiceUtente` accetta solo un JWT Accessi valido e consente la lettura solo al proprietario o a chi pu\xF2 gestire quel target (gerarchia ADMIN > SUPER). Un superutente non pu\xF2 leggere i grant di un admin.\n\n## 2.0.0-dev.50 (2026-10-15)\n\n### Cambiamenti\n- ORM: `Row` e `DbValue` sono i tipi predefiniti di `Orm.query` e `Orm.execute`; `transactionQuery` usa `Row[]`. Gli usi generici non devono pi\xF9 dichiarare `Record<string, unknown>`.\n- Build: il contratto OpenAPI generato (`src/accessi-module/openapi/`) \xE8 ignorato da Git e viene rigenerato da `npm run generate:accessi-openapi`.\n\n## 2.0.0-dev.49 (2026-10-15)\n\n### Cambiamenti\n- Documentazione HTTP rinominata da Swagger a **OpenAPI**: il modulo unificato espone `/accessi/openapi` e `/accessi/openapi.json`; `setupSwagger` usa `/openapi` come default.\n- Il path \xE8 configurabile tramite `EmilsoftwareOptions.swagger` (utile per far convivere pi\xF9 documentazioni).\n- Allineati log di avvio, Wiki e dispatch delle rotte del modulo.\n- L'alias storico `<path>-json` resta supportato.\n\n### Note\n- I consumer che aprivano `/swagger` o `/accessi/swagger` devono aggiornare l'URL alla nuova documentazione OpenAPI.\n\n## 2.0.0-dev.48 (2026-10-13)\n\n### Correzioni\n- `RestUtilities.sendErrorMessage` restituisce status HTTP specifici (400/401/403/4xx/5xx) e codici generici (`VALIDATION_ERROR`, `UNAUTHORIZED`, `FORBIDDEN`, `INTERNAL_ERROR`, `REQUEST_ERROR`), senza usare codici `ACCESSI_*` nel business helper.\n- Il messaggio restituito \xE8 quello reale dell'eccezione; gli errori testuali di validazione vengono riconosciuti come 400.\n\n## 2.0.0-dev.47 (2026-10-13)\n\n### Correzioni\n- Errore schema database reso generico: codice `DATABASE_SCHEMA_OUTDATED` e messaggio senza riferimenti specifici ad Accessi.\n\n## 2.0.0-dev.46 (2026-10-12)\n\n### Correzioni\n- `sendBaseResponse` resta retro-compatibile quando il terzo argomento \xE8 un tag stringa invece dello status HTTP.\n- Gestita la serializzazione dei `bigint`; gli errori di risposta vengono registrati con stack tramite `sendErrorMessage`.\n- `AccessiHttpExceptionFilter` registra gli errori con stack per facilitare la diagnosi.\n\n## 2.0.0-dev.45 (2026-10-12)\n\n### Correzioni\n- `set-gdpr` e l'aggiornamento di `flagGdpr` sono idempotenti: `UPDATE OR INSERT` su `UTENTI_GDPR` con `MATCHING (CODUTE, DATACC)`.\n- Evitate violazioni di chiave primaria e risposte 500 quando il consenso GDPR viene registrato pi\xF9 volte nello stesso giorno.\n\n## 2.0.0-dev.44\n\n### Aggiunte\n- Endpoint pubblico `GET /api/accessi/auth/bootstrap-admin/status` (`getAdminBootstrapStatus`): la console mostra la sezione \"Crea utente admin\" **solo** se `adminBootstrap.enabled` \xE8 `true`.\n\n### Correzioni\n- Rimossi i caratteri corrotti (mojibake UTF-8 \u2194 Windows-1252) da DTO, servizi, controller, console, OpenAPI e client generato (es. \"L'email \xE8 gi\xE0 associata a un utente Accessi.\").\n\n## 2.0.0-dev.43\n\n### Cambiamenti\n- Gerarchia dei ruoli formalizzata **ADMIN > SUPER > utente**: un superutente non puo' gestire un admin su `update-user`, `delete-user` (soft e hard), `set-password`, `force-password-reset`, `set-stato`, `assign-roles` e `assign-permissions` (`ensureCanManageTargetUser`, 403 se violato).\n- Guard **ultimo admin attivo**: non puo' essere disattivato, bloccato, eliminato o privato del flag Admin (400), per evitare il lockout della console.\n- Creazione utente (`create-managed-user` e registrazione pubblica) resa **atomica** rispetto all'invio email: fail-fast se SMTP non configurato e rollback dell'utente creato se l'invio fallisce (`ACCESSI_USER_INVITE_EMAIL_FAILED`).\n- Console: form password separato (Invio imposta la password invece di salvare il profilo), scheda utente in sola lettura per target di rango superiore.\n- SSO: anche il superutente puo' creare utenti SSO e gestirne identita/policy (`federated-auth/users/*`) sugli utenti non-admin; il catalogo dei provider SSO resta riservato agli admin.\n- Coerenza: i filtri utente (`saveFiltriUtente`) rispettano la gerarchia; il flag GDPR impostato dal profilo allinea anche `DATGDPR`; la scheda in sola lettura disabilita anche il collegamento SSO.\n- Eliminazione definitiva: se l'utente ha dati collegati (tabelle esterne/FK) risponde 400 con `code: ACCESSI_USER_DELETE_HAS_REFERENCES` e messaggio esplicito, suggerendo il soft delete (la hard delete resta invariata quando non ci sono vincoli).\n- Consenso GDPR completo: `update-user` con `flagGdpr` scrive anche la riga storica `UTENTI_GDPR`; in console il pulsante \"Registra consenso GDPR\" usa `set-gdpr`.\n- Console: aggiunto il pulsante \"Invia reset password legacy\" (super e admin).\n- Console: **auto-ingresso dal frontend** tramite ticket monouso (`POST /api/accessi/console/entry` + apertura di `#entry=<ticket>`), senza JWT nell'URL; il ticket e' legato all'utente, dura 60s, e' monouso e lo scambio e' protetto da rate limit (`publicAuthRateLimit.consoleEntry`).\n\n## 2.0.0-dev.42\n\n### Aggiunte\n- Gestione dei flag di privilegio dalla console Accessi:\n - un admin pu\xF2 assegnare o togliere sia `FLGADMIN` sia `FLGSUPER`;\n - un superutente senza `FLGADMIN` pu\xF2 gestire solo `FLGSUPER`;\n - i flag sono impostabili in creazione (utente locale o SSO) e in modifica profilo.\n- Eliminazione utenti con due modalit\xE0:\n - soft delete (`delete-user`, `STAREG=DELETE`) per admin e superutenti;\n - eliminazione definitiva (`delete-user-permanent`, solo admin) che rimuove in transazione utente e record collegati.\n- Gestione completa dell'utente dalla console (admin): anagrafica estesa (cellulare, lingua, avatar, metadata JSON), GDPR, scadenza password, tutti i filtri (`FILTRI`) e **impostazione diretta della password** (`POST /api/accessi/user/set-password/:codiceUtente`), senza accesso al database.\n- Scritture utente idempotenti: `UTENTI_CONFIG` in `UPDATE OR INSERT ... MATCHING (CODUTE)`; i campi non inviati restano invariati.\n- Policy password moderna quando si imposta una password (reset, set-password admin, bootstrap): minimo 8 caratteri con maiuscola, minuscola, cifra e carattere speciale, senza spazi e non comune. In caso di violazione risposta 400 con `code: ACCESSI_WEAK_PASSWORD` e `details`. La policy **non** si applica al login, cos\xEC le password preesistenti deboli continuano a funzionare.\n\n### Correzioni\n- Il backend verifica i flag in base al ruolo dell'utente connesso (`ensurePrivilegeFlagChanges`): inviare un flag non consentito restituisce 403.\n\n## 2.0.0-dev.33\n\n### Cambiamenti\n- Rimosse le logiche applicative residue dallo schema Accessi:\n - `MENU_GRP.NOMECAMPO`\n - `UTENTI.ENABLEIA`\n - `UTENTI_CONFIG.PAGDEF`\n - `UTENTI.CELLUTE`\n- Telefono canonico: `UTENTI_CONFIG.CELLULARE` (allargato a `VARCHAR(30)`).\n- Migrazione automatica: copia `UTENTI.CELLUTE` \u2192 `UTENTI_CONFIG.CELLULARE`, poi `DROP` delle colonne obsolete.\n- Ruoli chiariti:\n - `FLGSUPER`: superutente, riceve **tutte** le abilitazioni al livello massimo (30) dopo il login; in console vede **solo** la sezione **Utenti**.\n - `FLGADMIN`: accede a **tutta** la console (catalogo, token, SSO, utenti) ma **non** influisce sulle abilitazioni.\n- Aggiunta la sezione **Changelog** in console (sotto la Wiki).\n\n### Note\n- `FILTRI` e `FILTRI_TIPO` restano invariati (piano di migrazione futuro).\n- I backend che usavano i campi rimossi devono gestirli in tabelle proprie.\n\n## 2.0.0-dev.32\n\n### Cambiamenti\n- Rimosso il supporto ai flag applicativi in `UTENTI_CONFIG`: `FLGMOP`, `FLGPIANA`, `FLGADDETTI`, `FLGOSPITI`, `FLGPIANARFID`, `FLGCONTA`, `FLGCUBI`, `FLGCICLPASS`, `FLGDIPENDENTI`, `FLGINVENTARI`, `CAUMOV`, `NUMMAC`, `RAGSOCCLI`.\n- Rinominata la colonna `FLGADMINCONFIG` in `FLGADMIN` (con copia dei valori e `DROP`).\n\n## 2.0.0-dev.31\n\n### Aggiunte\n- Esposizione dei servizi Accessi all'host Express (`getAccessiModuleServices`) per delegare login, SSO e token senza reimplementarli.\n\n### Correzioni\n- `UserService.getUsers`: filtri estesi (`codiceUtente[]`, `numRep[]`, `codDipendente`, `cellulare`, `tipFil`, `flagSuper`).\n\n## 2.0.0-dev.24\n\n### Cambiamenti (breaking)\n- Chiavi JWT derivate per scopo (`access`, `reset`, `2fa`) e claim `typ` obbligatorio: gli access token precedenti non sono pi\xF9 validi.\n- Endpoint Allegati protetti di default.\n- Pool di connessioni attivo di default (`poolSize` 5).\n\n### Aggiunte\n- Service token macchina-a-macchina (gestione, guard, console).\n- CRUD completo dei cataloghi dalla console Accessi.\n- Supporto Firebird 2.5 nel migrator e nei test di integrazione.\n";
1857
+ var changelog_default = "# Changelog Accessi\n\nNovit\xE0 e modifiche rilevanti della libreria `emilsoftware-utilities` (modulo Accessi).\n\n## Non rilasciato\n\n### Cambiamenti\n- Import: gli entry point (`accessi/server`, `accessi/resource-server`, `accessi/client*`, `accessi/contracts*`, `accessi/openapi`, `accessi/console`, `accessi/ui`) sono la via consigliata. Root barrel e deep import restano supportati fino alla prossima major; mappa di migrazione in `docs/import-migration.md`.\n- Sicurezza HTTP: CSP configurabile con nonce per la console, `'unsafe-inline'` documentato solo per Swagger UI, header frame/referrer/cache personalizzabili; guida in `docs/security-headers.md`.\n- Autenticazione: modalita opzionale a cookie `HttpOnly` con CSRF double-submit, refresh da cookie e supporto console; Bearer resta il default. Guida in `docs/auth-cookies.md`.\n- Observability: metriche Prometheus di dominio per login, 2FA, refresh, rate limit, introspection, JWKS, revoche, ticket console e store Redis, con alert suggeriti in `docs/metrics.md`.\n- Audit: sink memory/Redis/HTTP/callback, redazione ricorsiva, retention, query ed export NDJSON admin in `docs/audit.md`.\n- Multi-tenancy: un database per tenant senza modifiche allo schema, file `<id>.environment.ts` tipizzati (`AccessiTenantEnvironment`), validazione isolamento, risoluzione host/path/header fidato, supervisore processi, store Redis con prefisso automatico e template systemd/Docker/Nginx/Traefik in `deploy/`. Documentazione in `docs/multi-tenancy.md`.\n- Resource server: aggiunti `createAccessiAuthMiddleware`, `createAccessiAuthGuard`, `createAccessiTokenVerifier` e `createAccessiIntrospectionClient` per backend fratelli che non possiedono il database Accessi. Supporta HS256, RS/ES con chiave pubblica PEM e JWKS con cache.\n- Introspection: nuovo `POST /api/accessi/auth/introspect` protetto da service token con scope `accessi:introspect`. Restituisce `active`, profilo utente e grant correnti.\n- JWT: `jwtOptions` accetta `algorithm`, `issuer`, `audience`, `privateKey`, `publicKey`, `keyId`, `jwksPath` e `previousPublicKeys`. Default HS256 invariato; RS/ES sono consigliati per la topologia multi-servizio.\n- JWKS: con RS/ES Accessi pubblica `/accessi/.well-known/jwks.json` (path configurabile) con chiave corrente e chiavi precedenti per la rotazione. I resource server possono usare `jwks.url`.\n- Console ticket: introdotto `ConsoleEntryStore`. Default in memoria; `createRedisConsoleEntryStore` salva il JWT cifrato in Redis, usa TTL e consume atomico per il monouso multi-istanza. Configurazione in `consoleEntry.store` e `consoleEntry.ttlMs`.\n- Rate limit: introdotto `RateLimitStore`. `createRedisRateLimitStore` usa `INCR` + `PEXPIRE` atomici per contatori condivisi in cluster. Configurazione in `publicAuthRateLimit.store` e `serviceTokens.rateLimitStore`; default in memoria invariato.\n- Edge rate limit: aggiunto `createAccessiEdgeRateLimitMiddleware` e `AccessiOptions.edgeRateLimit` per una soglia grossolana per IP davanti alle rotte Accessi. Fail closed se lo store non risponde; supporta lo stesso store Redis.\n- Servizio standalone: aggiunti `createAccessiService`, `startAccessiService`, `/health/live`, `/health/ready`, `/health/metrics` e shutdown graceful. Aggiunti gli script `accessi:serve` e `accessi:serve:dist` per l'esecuzione come processo Node.\n- Configurazione: aggiunti `validateAccessiOptions` e `assertValidAccessiOptions`; il servizio standalone li applica al bootstrap.\n- HTTP: aggiunte opzioni `http.cors`, `http.trustProxy`, `http.securityHeaders`. La JWKS viene montata automaticamente anche con `AccessiModule.forRoot` tramite `HttpAdapterHost`.\n- SSO: aggiunto `POST /api/accessi/federated-auth/exchange` protetto da service token con scope `accessi:federated:exchange` per scambiare `{ provider, subject }` verificati da un backend fratello.\n- Audit: aggiunto `auditAccessi` per eventi di sicurezza strutturati; resource server ed edge rate limit registrano fallimenti di autenticazione, blocchi e rate limit non disponibile.\n- Contratto: aggiunto `npm run check:accessi-openapi`, eseguito nel build, che blocca path fuori prefisso, operationId duplicati/mancanti, $ref rotti, path legacy e rimozioni di operation/schema rispetto al fingerprint versionato.\n- Contratto: aggiunto `npm run accessi:update-openapi-fingerprint` per aggiornare intenzionalmente il fingerprint dopo una modifica breaking approvata.\n- Console: aggiunto `consoleApiBaseUrl` per servire UI e API su domini diversi; il valore viene iniettato in `window.__ACCESSI_API_BASE_URL__`.\n- Chiavi: aggiunto `npm run accessi:generate-jwt-keys` per generare coppia RS256/ES256, `keyId` e JWKS pubblica.\n- Sessioni: aggiunta la revoca per utente con `SessionRevocationStore` memory/Redis e `POST /api/accessi/auth/revoke-sessions`. Resource server e guard controllano il cutoff `iat`.\n- Refresh token: con `jwtOptions.refreshExpiresIn` login, 2FA e SSO emettono anche `refreshToken` (typ=refresh) e `POST /api/accessi/auth/refresh` restituisce una nuova coppia access+refresh. Gli access token hanno ora `jti`.\n- Refresh rotation: con `refreshTokenRotation.store` il refresh token e monouso e la rotazione e atomica tra istanze; due refresh concorrenti producono un solo successo.\n- Contratto: il fingerprint OpenAPI ora blocca anche la rimozione di operation/schema rispetto alla baseline versionata.\n- OpenAPI: aggiunto `mergeOpenApiDocuments` per unire il documento Accessi nel documento unico dell'host con conflitti espliciti.\n- Console: aggiunto `buildAccessiConsoleUrl` per linkare la console centrale senza mettere JWT nell'URL.\n- Rinomina: la configurazione e gli helper della documentazione usano `openapi`/`OpenApi` (`AccessiOptions.openapi`, `OpenApiSetupOptions`, `setupOpenApi`, `DEFAULT_ACCESSI_OPENAPI`); i vecchi nomi `swagger` non sono piu esportati.\n- Token Accessi: `TokenResult.expiresIn` riporta sempre la validita effettiva in secondi (es. `86400`), anche quando `jwtOptions.expiresIn` usa un formato leggibile come `24h`. I client possono quindi esporre `expires_in` anche per il token di login normale.\n- Console: nuova funzione **Login as** per superutenti e admin. Dalla lista utenti si genera un URL verso l'applicazione scelta con `?loginAsToken=<jwt>`; il token dura quanto la sessione Accessi standard (`jwtOptions.expiresIn`), registra l'attore nelle claim `act`/`loginAs` e resta soggetto alla gerarchia ADMIN > SUPER. Endpoint: `POST /api/accessi/user/login-as/:codiceUtente`.\n- Console: rimosse le sezioni **Filtri utente** e **Tipi filtro**. I filtri applicativi sono deprecati e non vengono pi\xF9 letti n\xE9 scritti dalla console; le tabelle restano invariate e la gestione torna manuale sul database.\n- Console: le conferme operative (es. \"Ruoli aggiornati\") ora compaiono come notifiche in alto a destra e scompaiono automaticamente.\n- Console: il salvataggio del profilo non invia pi\xF9 i campi della tabella `FILTRI`, evitando payload rifiutati dal backend. `UserService` tocca `FILTRI` solo se il chiamante invia esplicitamente almeno un campo legacy.\n- Console: scheda utente riorganizzata. \"Indietro\" resta nella barra superiore; stato registrazione, disattivazione ed eliminazione definitiva sono in un form coerente \"Stato e sicurezza\", con icone e tooltip allineati.\n- Console: pagina Changelog a larghezza piena, con intestazioni di versione evidenziate e sezioni orizzontali leggibili.\n- Seed `npm run accessi:seed-admin`: crea o aggiorna un admin completo (`FLGSUPER = 1` e `FLGADMIN = 1`) e corregge il nome colonna usato in precedenza (`FLGADMIN`).\n- Console: la pagina di default nella scheda utente \xE8 ora un autocomplete con ricerca. Propone tutte le rotte del catalogo menu, si filtra scrivendo ed \xE8 ancora possibile inserire un valore libero.\n- Autorizzazioni: i grant effettivi sommano ruoli e permessi diretti prendendo il livello massimo per menu. Un permesso diretto con livello inferiore (o 0) non annulla pi\xF9 il grant del ruolo.\n- Ruoli legacy: un menu senza descrizione non viene pi\xF9 scartato e un `TIPABI` mancante viene letto come livello 10, evitando utenti con ruolo ma senza accesso.\n- Grant dei ruoli arricchiti con i metadati di navigazione del catalogo menu (`pagina`, icona, gruppo, tipo): il frontend riceve gli stessi campi dei grant diretti e pu\xF2 costruire correttamente menu e rotte.\n- Sicurezza: `GET /api/accessi/permission/grants/:codiceUtente` accetta solo un JWT Accessi valido e consente la lettura solo al proprietario o a chi pu\xF2 gestire quel target (gerarchia ADMIN > SUPER). Un superutente non pu\xF2 leggere i grant di un admin.\n\n## 2.0.0-dev.50 (2026-10-15)\n\n### Cambiamenti\n- ORM: `Row` e `DbValue` sono i tipi predefiniti di `Orm.query` e `Orm.execute`; `transactionQuery` usa `Row[]`. Gli usi generici non devono pi\xF9 dichiarare `Record<string, unknown>`.\n- Build: il contratto OpenAPI generato (`src/accessi-module/openapi/`) \xE8 ignorato da Git e viene rigenerato da `npm run generate:accessi-openapi`.\n\n## 2.0.0-dev.49 (2026-10-15)\n\n### Cambiamenti\n- Documentazione HTTP rinominata da Swagger a **OpenAPI**: il modulo unificato espone `/accessi/openapi` e `/accessi/openapi.json`; `setupSwagger` usa `/openapi` come default.\n- Il path \xE8 configurabile tramite `EmilsoftwareOptions.swagger` (utile per far convivere pi\xF9 documentazioni).\n- Allineati log di avvio, Wiki e dispatch delle rotte del modulo.\n- L'alias storico `<path>-json` resta supportato.\n\n### Note\n- I consumer che aprivano `/swagger` o `/accessi/swagger` devono aggiornare l'URL alla nuova documentazione OpenAPI.\n\n## 2.0.0-dev.48 (2026-10-13)\n\n### Correzioni\n- `RestUtilities.sendErrorMessage` restituisce status HTTP specifici (400/401/403/4xx/5xx) e codici generici (`VALIDATION_ERROR`, `UNAUTHORIZED`, `FORBIDDEN`, `INTERNAL_ERROR`, `REQUEST_ERROR`), senza usare codici `ACCESSI_*` nel business helper.\n- Il messaggio restituito \xE8 quello reale dell'eccezione; gli errori testuali di validazione vengono riconosciuti come 400.\n\n## 2.0.0-dev.47 (2026-10-13)\n\n### Correzioni\n- Errore schema database reso generico: codice `DATABASE_SCHEMA_OUTDATED` e messaggio senza riferimenti specifici ad Accessi.\n\n## 2.0.0-dev.46 (2026-10-12)\n\n### Correzioni\n- `sendBaseResponse` resta retro-compatibile quando il terzo argomento \xE8 un tag stringa invece dello status HTTP.\n- Gestita la serializzazione dei `bigint`; gli errori di risposta vengono registrati con stack tramite `sendErrorMessage`.\n- `AccessiHttpExceptionFilter` registra gli errori con stack per facilitare la diagnosi.\n\n## 2.0.0-dev.45 (2026-10-12)\n\n### Correzioni\n- `set-gdpr` e l'aggiornamento di `flagGdpr` sono idempotenti: `UPDATE OR INSERT` su `UTENTI_GDPR` con `MATCHING (CODUTE, DATACC)`.\n- Evitate violazioni di chiave primaria e risposte 500 quando il consenso GDPR viene registrato pi\xF9 volte nello stesso giorno.\n\n## 2.0.0-dev.44\n\n### Aggiunte\n- Endpoint pubblico `GET /api/accessi/auth/bootstrap-admin/status` (`getAdminBootstrapStatus`): la console mostra la sezione \"Crea utente admin\" **solo** se `adminBootstrap.enabled` \xE8 `true`.\n\n### Correzioni\n- Rimossi i caratteri corrotti (mojibake UTF-8 \u2194 Windows-1252) da DTO, servizi, controller, console, OpenAPI e client generato (es. \"L'email \xE8 gi\xE0 associata a un utente Accessi.\").\n\n## 2.0.0-dev.43\n\n### Cambiamenti\n- Gerarchia dei ruoli formalizzata **ADMIN > SUPER > utente**: un superutente non puo' gestire un admin su `update-user`, `delete-user` (soft e hard), `set-password`, `force-password-reset`, `set-stato`, `assign-roles` e `assign-permissions` (`ensureCanManageTargetUser`, 403 se violato).\n- Guard **ultimo admin attivo**: non puo' essere disattivato, bloccato, eliminato o privato del flag Admin (400), per evitare il lockout della console.\n- Creazione utente (`create-managed-user` e registrazione pubblica) resa **atomica** rispetto all'invio email: fail-fast se SMTP non configurato e rollback dell'utente creato se l'invio fallisce (`ACCESSI_USER_INVITE_EMAIL_FAILED`).\n- Console: form password separato (Invio imposta la password invece di salvare il profilo), scheda utente in sola lettura per target di rango superiore.\n- SSO: anche il superutente puo' creare utenti SSO e gestirne identita/policy (`federated-auth/users/*`) sugli utenti non-admin; il catalogo dei provider SSO resta riservato agli admin.\n- Coerenza: i filtri utente (`saveFiltriUtente`) rispettano la gerarchia; il flag GDPR impostato dal profilo allinea anche `DATGDPR`; la scheda in sola lettura disabilita anche il collegamento SSO.\n- Eliminazione definitiva: se l'utente ha dati collegati (tabelle esterne/FK) risponde 400 con `code: ACCESSI_USER_DELETE_HAS_REFERENCES` e messaggio esplicito, suggerendo il soft delete (la hard delete resta invariata quando non ci sono vincoli).\n- Consenso GDPR completo: `update-user` con `flagGdpr` scrive anche la riga storica `UTENTI_GDPR`; in console il pulsante \"Registra consenso GDPR\" usa `set-gdpr`.\n- Console: aggiunto il pulsante \"Invia reset password legacy\" (super e admin).\n- Console: **auto-ingresso dal frontend** tramite ticket monouso (`POST /api/accessi/console/entry` + apertura di `#entry=<ticket>`), senza JWT nell'URL; il ticket e' legato all'utente, dura 60s, e' monouso e lo scambio e' protetto da rate limit (`publicAuthRateLimit.consoleEntry`).\n\n## 2.0.0-dev.42\n\n### Aggiunte\n- Gestione dei flag di privilegio dalla console Accessi:\n - un admin pu\xF2 assegnare o togliere sia `FLGADMIN` sia `FLGSUPER`;\n - un superutente senza `FLGADMIN` pu\xF2 gestire solo `FLGSUPER`;\n - i flag sono impostabili in creazione (utente locale o SSO) e in modifica profilo.\n- Eliminazione utenti con due modalit\xE0:\n - soft delete (`delete-user`, `STAREG=DELETE`) per admin e superutenti;\n - eliminazione definitiva (`delete-user-permanent`, solo admin) che rimuove in transazione utente e record collegati.\n- Gestione completa dell'utente dalla console (admin): anagrafica estesa (cellulare, lingua, avatar, metadata JSON), GDPR, scadenza password, tutti i filtri (`FILTRI`) e **impostazione diretta della password** (`POST /api/accessi/user/set-password/:codiceUtente`), senza accesso al database.\n- Scritture utente idempotenti: `UTENTI_CONFIG` in `UPDATE OR INSERT ... MATCHING (CODUTE)`; i campi non inviati restano invariati.\n- Policy password moderna quando si imposta una password (reset, set-password admin, bootstrap): minimo 8 caratteri con maiuscola, minuscola, cifra e carattere speciale, senza spazi e non comune. In caso di violazione risposta 400 con `code: ACCESSI_WEAK_PASSWORD` e `details`. La policy **non** si applica al login, cos\xEC le password preesistenti deboli continuano a funzionare.\n\n### Correzioni\n- Il backend verifica i flag in base al ruolo dell'utente connesso (`ensurePrivilegeFlagChanges`): inviare un flag non consentito restituisce 403.\n\n## 2.0.0-dev.33\n\n### Cambiamenti\n- Rimosse le logiche applicative residue dallo schema Accessi:\n - `MENU_GRP.NOMECAMPO`\n - `UTENTI.ENABLEIA`\n - `UTENTI_CONFIG.PAGDEF`\n - `UTENTI.CELLUTE`\n- Telefono canonico: `UTENTI_CONFIG.CELLULARE` (allargato a `VARCHAR(30)`).\n- Migrazione automatica: copia `UTENTI.CELLUTE` \u2192 `UTENTI_CONFIG.CELLULARE`, poi `DROP` delle colonne obsolete.\n- Ruoli chiariti:\n - `FLGSUPER`: superutente, riceve **tutte** le abilitazioni al livello massimo (30) dopo il login; in console vede **solo** la sezione **Utenti**.\n - `FLGADMIN`: accede a **tutta** la console (catalogo, token, SSO, utenti) ma **non** influisce sulle abilitazioni.\n- Aggiunta la sezione **Changelog** in console (sotto la Wiki).\n\n### Note\n- `FILTRI` e `FILTRI_TIPO` restano invariati (piano di migrazione futuro).\n- I backend che usavano i campi rimossi devono gestirli in tabelle proprie.\n\n## 2.0.0-dev.32\n\n### Cambiamenti\n- Rimosso il supporto ai flag applicativi in `UTENTI_CONFIG`: `FLGMOP`, `FLGPIANA`, `FLGADDETTI`, `FLGOSPITI`, `FLGPIANARFID`, `FLGCONTA`, `FLGCUBI`, `FLGCICLPASS`, `FLGDIPENDENTI`, `FLGINVENTARI`, `CAUMOV`, `NUMMAC`, `RAGSOCCLI`.\n- Rinominata la colonna `FLGADMINCONFIG` in `FLGADMIN` (con copia dei valori e `DROP`).\n\n## 2.0.0-dev.31\n\n### Aggiunte\n- Esposizione dei servizi Accessi all'host Express (`getAccessiModuleServices`) per delegare login, SSO e token senza reimplementarli.\n\n### Correzioni\n- `UserService.getUsers`: filtri estesi (`codiceUtente[]`, `numRep[]`, `codDipendente`, `cellulare`, `tipFil`, `flagSuper`).\n\n## 2.0.0-dev.24\n\n### Cambiamenti (breaking)\n- Chiavi JWT derivate per scopo (`access`, `reset`, `2fa`) e claim `typ` obbligatorio: gli access token precedenti non sono pi\xF9 validi.\n- Endpoint Allegati protetti di default.\n- Pool di connessioni attivo di default (`poolSize` 5).\n\n### Aggiunte\n- Service token macchina-a-macchina (gestione, guard, console).\n- CRUD completo dei cataloghi dalla console Accessi.\n- Supporto Firebird 2.5 nel migrator e nei test di integrazione.\n";
1858
1858
 
1859
1859
  // src/accessi-module/Console/help.ts
1860
1860
  var p2 = (text) => ({ kind: "p", text });
@@ -144,7 +144,7 @@ exports.WIKI_SECTIONS = [
144
144
  ['accessi/ui', 'Percorsi degli asset Web Component frameworkless.', 'no'],
145
145
  ]),
146
146
  p(`Topologia consigliata: un solo Accessi Service possiede database, encryptionKey, SMTP, chiavi JWT e console. Gli altri backend usano resource-server e client, senza credenziali del database Accessi.`),
147
- p(`Multi-tenancy: un database per tenant, configurato da file <id>.environment.ts tipizzati (AccessiTenantEnvironment). Il database resta invariato (nessuna colonna tenant); ogni tenant gira in un processo separato con chiavi, issuer e store propri. Validazione e avvio con npm run accessi:tenants; dettagli in docs/multi-tenancy.md.`),
147
+ p(`Multi-tenancy: un database per tenant, configurato da file <id>.environment.ts tipizzati (AccessiTenantEnvironment). Il database resta invariato (nessuna colonna tenant); ogni tenant gira in un processo separato con chiavi, issuer e store propri. Validazione e avvio con npm run accessi:tenants; template systemd/Docker/Nginx/Traefik in deploy/; dettagli in docs/multi-tenancy.md.`),
148
148
  note('warn', `Root barrel legacy`, `Il root barrel e i deep import restano supportati fino alla prossima major. I nuovi consumer devono usare gli entry point della tabella; la mappa di migrazione e in docs/import-migration.md.`),
149
149
  p(`Per avviare Accessi come servizio autonomo usare startAccessiService: espone /health/live, /health/ready, /health/metrics, valida la configurazione e gestisce SIGTERM/SIGINT.`),
150
150
  p(`Per parlare con piu Accessi Service o tenant dallo stesso processo usare createAccessiClient: ogni istanza ha base URL e token propri, isolati anche tra richieste concorrenti. configureAccessiClient resta il default globale per le funzioni esportate dal package.`),
@@ -1 +1 @@
1
- {"version":3,"file":"wiki.js","sourceRoot":"","sources":["../../../src/accessi-module/Console/wiki.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Contenuto della Wiki della console Accessi.\n *\n * I contenuti sono dati strutturati (non HTML) così da poter essere resi in modo\n * coerente dalla console e convertiti in un digest compatto per l'IA. Nessun\n * segreto o dato specifico di un ambiente deve finire qui.\n */\n\nexport type WikiTone = 'info' | 'ok' | 'warn' | 'danger';\n\nexport type WikiBlock =\n | { kind: 'p'; text: string }\n | { kind: 'h'; text: string }\n | { kind: 'list'; items: string[]; ordered?: boolean }\n | { kind: 'code'; language: string; title?: string; code: string; variant?: 'good' | 'bad' }\n | { kind: 'callout'; tone: WikiTone; title?: string; text: string }\n | { kind: 'table'; head: string[]; rows: string[][] };\n\nexport interface WikiSection {\n id: string;\n group: string;\n title: string;\n summary: string;\n blocks: WikiBlock[];\n}\n\nconst p = (text: string): WikiBlock => ({ kind: 'p', text });\nconst h = (text: string): WikiBlock => ({ kind: 'h', text });\nconst list = (items: string[], ordered = false): WikiBlock => ({ kind: 'list', items, ordered });\nconst code = (language: string, codeText: string, title?: string, variant?: 'good' | 'bad'): WikiBlock => ({ kind: 'code', language, title, code: codeText, variant });\nconst good = (language: string, codeText: string, title?: string): WikiBlock => code(language, codeText, title, 'good');\nconst bad = (language: string, codeText: string, title?: string): WikiBlock => code(language, codeText, title, 'bad');\nconst note = (tone: WikiTone, title: string, text: string): WikiBlock => ({ kind: 'callout', tone, title, text });\nconst table = (head: string[], rows: string[][]): WikiBlock => ({ kind: 'table', head, rows });\n\nexport const WIKI_SECTIONS: WikiSection[] = [\n // -------------------------------------------------------------------------\n {\n id: 'panoramica',\n group: 'Per iniziare',\n title: 'Panoramica e architettura',\n summary: 'Che cosa fa Accessi, come si incastra nel backend e come scorre una richiesta.',\n blocks: [\n p(`Accessi è un modulo Node/NestJS che aggiunge a un backend già esistente: autenticazione JWT, autorizzazione per ruoli e menu, gestione utenti, reset password, codici email (2FA e passwordless), SSO generico, token macchina-a-macchina e una console amministrativa. Il database è Firebird/InterBase.`),\n h(`Che cosa possiede il modulo`),\n list([\n `Le rotte sotto /api/accessi/* (login, utenti, ruoli, menu, SSO, token, console).`,\n `Lo schema database del dominio Accessi, riconciliato automaticamente al bootstrap.`,\n `Il middleware di autorizzazione authorizeAccessi per le rotte della tua applicazione.`,\n `La console web su /api/accessi/console.`,\n ]),\n h(`Che cosa resta alla tua applicazione`),\n list([\n `Il proprio frontend e le proprie rotte di business.`,\n `La validazione dei token dei provider SSO esterni (Accessi non li vede).`,\n `I segreti: jwtOptions.secret, SMTP, database ed encryptionKey non raggiungono mai il browser.`,\n ]),\n h(`Flusso di una richiesta protetta`),\n list([\n `Il client chiama una rotta della tua app con header Authorization: Bearer <jwt>.`,\n `La tua rotta esegue authorizeAccessi, che verifica firma HS256, scadenza e stato utente corrente.`,\n `Se passi un requisito, il modulo carica ruoli e grant dell'utente e li valuta.`,\n `Su successo req.user, req.data e (se richiesto) req.userGrants sono popolati; altrimenti risponde con il contratto errore Accessi.`,\n ], true),\n note('info', `Due modalità di bootstrap`, `Express esistente: initializeAccessiModule(app, options). NestJS esistente: AccessiModule.forRoot(options) nei tuoi imports. Non usarle entrambe.`),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'installazione',\n group: 'Per iniziare',\n title: 'Installazione',\n summary: 'Dipendenze, peer e build.',\n blocks: [\n code('bash', `npm install emilsoftware-utilities`, 'Installazione'),\n p(`La libreria dipende da node-firebird per il database e da Express 5 come peer per il montaggio. È consigliato Node 18 o superiore.`),\n p(`Il pacchetto è pubblicato in CommonJS con export map esplicita per ogni entry point (accessi/server, accessi/resource-server/*, accessi/client/*, accessi/contracts*, accessi/openapi, accessi/console, accessi/ui): gli import ESM funzionano tramite interop Node. sideEffects elenca solo root barrel, firebird-compat e bundle console, così il resto è tree-shakable.`),\n note('warn', `Un solo modulo per processo`, `AccessiModule è @Global(): inizializzalo una sola volta per processo. In caso di più istanze, usa un processo per istanza.`),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'bootstrap-express',\n group: 'Per iniziare',\n title: 'Bootstrap su Express esistente',\n summary: 'initializeAccessiModule: ordine corretto e attesa prima di listen.',\n blocks: [\n p(`Chiama initializeAccessiModule prima di aprire la porta HTTP. Durante il bootstrap, authorizeAccessi risponde 503 ACCESSI_AUTH_INITIALIZING invece di autorizzare richieste parziali.`),\n good('ts', [\n \"import express from 'express';\",\n \"import { initializeAccessiModule, authorizeAccessi } from 'emilsoftware-utilities';\",\n '',\n 'const app = express();',\n 'app.use(express.json());',\n '',\n 'await initializeAccessiModule(app, options); // prima di app.listen',\n '',\n \"app.get('/api/ordini', authorizeAccessi, (req, res) => {\",\n ' res.json({ utente: req.user });',\n '});',\n '',\n \"app.get('/api/ordini/scrivi', (req, res, next) =>\",\n ' authorizeAccessi(req, res, next, {',\n \" requirements: [{ menuCode: 'ORDINI', minPermissionLevel: 20 }],\",\n ' }),',\n '(req, res) => res.json({ ok: true }));',\n '',\n 'app.listen(3000);',\n ].join('\\n'), 'Express + Accessi'),\n bad('ts', [\n 'const app = express();',\n 'app.listen(3000); // porta aperta prima del bootstrap',\n 'await initializeAccessiModule(app, options); // le richieste nel frattempo ricevono 503',\n ].join('\\n'), 'Non aprire la porta prima del bootstrap'),\n h(`Rotte montate`),\n p(`Accessi intercetta solo /api/accessi/* e i path OpenAPI configurati (default /accessi/openapi* e /accessi/openapi.json). Tutte le altre rotte passano intatte al tuo stack Express.`),\n note('ok', `req.user`, `authorizeAccessi popola req.user con il payload utente corrente (utente, codiceUtente, flag) e req.data come alias storico. req.userGrants è presente solo se hai richiesto dei requisiti.`),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'bootstrap-nest',\n group: 'Per iniziare',\n title: 'Bootstrap su NestJS esistente',\n summary: 'AccessiModule.forRoot e uso dei provider esportati.',\n blocks: [\n p(`Se la tua applicazione è già NestJS, importa AccessiModule.forRoot(options). Il modulo è @Global e riesporta i servizi (AuthService, UserService, PermissionService, FederatedAuthService, ServiceTokenService, ServiceTokenGuard, AuthenticateGenService).`),\n good('ts', [\n \"import { Module } from '@nestjs/common';\",\n \"import { AccessiModule } from 'emilsoftware-utilities';\",\n '',\n '@Module({',\n ' imports: [AccessiModule.forRoot(options)],',\n '})',\n 'export class AppModule {}',\n ].join('\\n'), 'app.module.ts'),\n p(`Il global prefix api e le rotte /api/accessi/* sono gestiti dal modulo. Le tue rotte Nest si proteggono con JwtSimpleGuard usando il JWT Accessi. Per health e readiness usare createAccessiHealthModule({ accessiOptions }): espone /health/live e /health/ready con lo stesso contratto del servizio standalone (bootstrap, database, store Redis e check aggiuntivi).`),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'distribuzione',\n group: 'Per iniziare',\n title: 'Distribuzione multi-app e package entry point',\n summary: 'Come usare Accessi come servizio centrale, come resource server o solo come contratti.',\n blocks: [\n p(`Accessi non va montato per forza in ogni backend. La distribuzione consigliata separa il servizio che possiede il database e la console dai backend fratelli che devono solo autenticare le richieste.`),\n table(\n ['Entry point', 'Uso', 'Dipendenze server'],\n [\n ['accessi/server', 'Bootstrap completo: DB, SMTP, migrazioni, console, API.', 'sì'],\n ['accessi/resource-server', 'Guard/middleware per backend fratelli: verifica JWT e autorizzazione remota.', 'no'],\n ['accessi/client', 'Client OpenAPI generato per chiamare Accessi Service.', 'no'],\n ['accessi/client/public', 'Solo API pubbliche (login, refresh, 2FA, reset password, registrazione, ticket console).', 'no'],\n ['accessi/client/admin', 'Solo API amministrative (utenti, ruoli, menu, filtri, provider federati, schema).', 'no'],\n ['accessi/client/service', 'Solo API service-to-service (service token e introspezione), mai nei frontend.', 'no'],\n ['accessi/contracts', 'Tipi, enum e helper puri browser-safe.', 'no'],\n ['accessi/contracts/dtos', 'DTO Nest con class-validator e decoratori OpenAPI.', 'no'],\n ['accessi/openapi', 'setupOpenApi e merge del documento OpenAPI.', 'no'],\n ['accessi/console', 'Helper per URL console, ticket e 2FA handoff.', 'no'],\n ['accessi/ui', 'Percorsi degli asset Web Component frameworkless.', 'no'],\n ],\n ),\n p(`Topologia consigliata: un solo Accessi Service possiede database, encryptionKey, SMTP, chiavi JWT e console. Gli altri backend usano resource-server e client, senza credenziali del database Accessi.`),\n p(`Multi-tenancy: un database per tenant, configurato da file <id>.environment.ts tipizzati (AccessiTenantEnvironment). Il database resta invariato (nessuna colonna tenant); ogni tenant gira in un processo separato con chiavi, issuer e store propri. Validazione e avvio con npm run accessi:tenants; dettagli in docs/multi-tenancy.md.`),\n note('warn', `Root barrel legacy`, `Il root barrel e i deep import restano supportati fino alla prossima major. I nuovi consumer devono usare gli entry point della tabella; la mappa di migrazione e in docs/import-migration.md.`),\n p(`Per avviare Accessi come servizio autonomo usare startAccessiService: espone /health/live, /health/ready, /health/metrics, valida la configurazione e gestisce SIGTERM/SIGINT.`),\n p(`Per parlare con piu Accessi Service o tenant dallo stesso processo usare createAccessiClient: ogni istanza ha base URL e token propri, isolati anche tra richieste concorrenti. configureAccessiClient resta il default globale per le funzioni esportate dal package.`),\n good('ts', [\n \"// Backend che ospita Accessi\",\n \"import { initializeAccessiModule } from 'emilsoftware-utilities/accessi/server';\",\n '',\n \"// Backend fratello: nessun AccessiModule, nessun DB Accessi\",\n \"import { createAccessiAuthMiddleware } from 'emilsoftware-utilities/accessi/resource-server';\",\n \"import { createAccessiClient } from 'emilsoftware-utilities/accessi/client';\",\n '',\n \"const milano = createAccessiClient({ baseUrl: 'https://accessi-milano.example.com', getToken });\",\n \"const roma = createAccessiClient({ baseUrl: 'https://accessi-roma.example.com', token: tokenRoma });\",\n \"const [utentiMilano, utentiRoma] = await Promise.all([milano.getUsers(), roma.getUsers()]);\",\n ].join('\\n'), 'Import corretti per ruolo'),\n note('warn', `Un solo proprietario`, `Non montare AccessiModule in due backend puntati allo stesso database: migrazioni, bootstrap admin, ticket console e rate limit diventano ambigui.`),\n note('info', `Client per scope`, `Usare accessi/client/public nei frontend pubblici, accessi/client/admin nella console e nei tool amministrativi, accessi/client/service solo nei backend fratelli. Gli scope partizionano il client completo e condividono gli stessi tipi; il client completo resta disponibile per compatibilita.`),\n note('info', `Split in pacchetti`, `Il package resta unico con entry point per scope: decisione, soglie per rivalutarla e piano di migrazione sono nell'ADR docs/adr/0001-split-package.md.`),\n note('info', `IP reali dietro proxy`, `Imposta http.trustProxy sul numero di proxy davanti al servizio (1 per Nginx/Traefik/ALB, 2 con Cloudflare + ingress) e lascialo false se il servizio e esposto direttamente. Rate limit e audit usano req.ip: dettagli in docs/proxy-ip.md.`),\n note('info', `Prompt di integrazione`, `Il file integration-prompt.md nella root del repository contiene il contesto completo per frontend, backend e agenti AI.`),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'opzioni',\n group: 'Configurazione',\n title: 'AccessiOptions (riferimento completo)',\n summary: 'Ogni campo di configurazione con tipo, obbligo e note.',\n blocks: [\n table(\n ['Campo', 'Tipo', 'Obbligo', 'Note'],\n [\n ['databaseOptions', 'Options (node-firebird)', 'sì', 'Connessione Firebird al database Accessi.'],\n ['confirmationEmailUrl', 'string', 'sì', 'Base URL usata per il reset password.'],\n ['confirmationEmailReturnUrl', 'string', 'sì', 'URL di ritorno dopo il reset.'],\n ['confirmationEmailPrefix', 'string', 'no', 'Prefisso passato alla pagina di reset.'],\n ['customResetPage', 'string', 'no', 'Sostituisce la pagina HTML di reset inclusa.'],\n ['encryptionKey', 'string', 'sì', 'Compatibilità e migrazione delle password legacy.'],\n ['mockDemoUser', 'boolean', 'sì', 'Utenti fittizi admin e demo solo fuori produzione.'],\n ['passwordExpiration', 'boolean', 'no', 'Attiva il controllo della scadenza password al login.'],\n ['passwordExpirationDays', 'number', 'no', 'Valore predefinito: 90 giorni.'],\n ['legacyPasswordMigrationOnStartup', 'boolean', 'no', 'Predefinito true: converte le password legacy.'],\n ['autoUpdateDatabase', 'boolean', 'no', 'Predefinito true: riconcilia lo schema al boot.'],\n ['jwtOptions', '{ secret, expiresIn, refreshExpiresIn, algorithm, issuer, audience, privateKey, publicKey, keyId, jwksPath, previousPublicKeys, onUnknownKeyId }', 'sì', 'Firma, durata, refresh token, JWKS e rotazione chiavi. Default HS256; RS/ES consigliati per resource server multi-servizio.'],\n ['emailOptions', 'EmailOptions', 'sì', 'SMTP per reset e codici email.'],\n ['publicAuthRateLimit', 'PublicAuthRateLimitOptions', 'no', 'Limiti per endpoint pubblici; store condiviso opzionale (Redis) per il cluster.'],\n ['edgeRateLimit', 'AccessiEdgeRateLimitOptions', 'no', 'Limite grossolano per IP davanti alle rotte Accessi; opzionale, store condiviso supportato.'],\n ['http', '{ cors, trustProxy, securityHeaders }', 'no', 'CORS, IP reali dietro proxy e header di sicurezza con CSP a nonce per la console e direttive configurabili.'],\n ['authCookie', '{ enabled, secure, sameSite, csrf, ... }', 'no', 'Cookie HttpOnly e CSRF double-submit al posto del Bearer. Default disattivato.'],\n ['metrics', '{ enabled, sink, prefix }', 'no', 'Metriche Prometheus di dominio (login, 2FA, refresh, rate limit, store) e sink esterno opzionale.'],\n ['audit', '{ enabled, sinks, store, retentionDays, redact }', 'no', 'Audit persistente redatto, retention e query/export admin. Default: solo log strutturato.'],\n ['publicRegistration', '{ enabled }', 'no', 'Registrazione pubblica: disattivata per impostazione predefinita.'],\n ['federatedAuthentication', 'FederatedAuthenticationOptions', 'no', `Abilita l'SSO generico.`],\n ['extensionFieldsOptions', 'ExtensionFieldsOptions[]', 'no', 'Tabelle esterne allegate al profilo.'],\n ['serviceTokens', '{ enabled, defaultTtlDays, rateLimitStore }', 'no', 'Token macchina-a-macchina, abilitati per impostazione predefinita; rate limit condiviso opzionale.'],\n ['catalogScripts', '{ enabled, folder, ... }', 'no', 'Catalog migrations SQL eseguite dopo la riconciliazione dello schema.'],\n ['adminBootstrap', '{ enabled, superUser }', 'no', 'Token casuale a ogni avvio per creare il primo admin dalla console.'],\n ['publicBaseUrl', 'string', 'no', 'Base URL usata solo per loggare console/OpenAPI/API.'],\n ['consoleApiBaseUrl', 'string', 'no', 'Base URL API per la console quando UI e API sono su domini diversi.'],\n ['openapi', 'OpenApiSetupOptions', 'no', 'Path, titolo, descrizione e enabled della documentazione OpenAPI. Default invariati.'],\n ['consoleEntry', '{ ttlMs, store }', 'no', 'TTL e store condiviso dei ticket console (Redis per multi-istanza). Default in memoria.'],\n ['sessionRevocation', '{ enabled, store, ttlMs }', 'no', 'Revoca sessioni per utente tramite store memory/Redis; abilita POST /api/accessi/auth/revoke-sessions.'],\n ['accessTokenRevocation', '{ enabled, store, ttlMs }', 'no', 'Revoca dei singoli access token tramite jti (memory/Redis); abilita POST /api/accessi/auth/revoke-token.'],\n ['refreshTokenRotation', '{ enabled, store, reuseGraceMs, revokeUserSessionsOnReuse }', 'no', 'Refresh token monouso tramite store memory/Redis; al riuso oltre la finestra di grazia revoca la famiglia di token. Consigliato in multi-istanza.'],\n ['logger', 'LoggerService', 'no', 'Logger Nest del sotto-app; se assente Nest usa il proprio logger.'],\n ['bufferLogs', 'boolean', 'no', 'Buffer dei log di bootstrap del sotto-app. Default true.'],\n ],\n ),\n good('ts', [\n \"import type { AccessiOptions } from 'emilsoftware-utilities';\",\n '',\n 'export const options: AccessiOptions = {',\n ' databaseOptions: {',\n ' host: process.env.ACCESSI_DB_HOST!,',\n ' port: Number(process.env.ACCESSI_DB_PORT ?? 3050),',\n ' database: process.env.ACCESSI_DB_NAME!,',\n ' user: process.env.ACCESSI_DB_USER!,',\n ' password: process.env.ACCESSI_DB_PASSWORD!,',\n ' },',\n \" confirmationEmailUrl: 'https://app.example.com',\",\n \" confirmationEmailReturnUrl: 'https://app.example.com/login',\",\n ' encryptionKey: process.env.ACCESSI_ENCRYPTION_KEY!,',\n ' mockDemoUser: false,',\n ' passwordExpiration: true,',\n ' passwordExpirationDays: 90,',\n ' jwtOptions: {',\n \" secret: process.env.ACCESSI_JWT_SECRET!, // almeno 32 caratteri casuali, unico per ambiente\",\n \" expiresIn: '24h',\",\n \" issuer: 'accessi',\",\n \" audience: 'backend-app',\",\n ' },',\n ' emailOptions: {',\n ' host: process.env.SMTP_HOST!,',\n ' port: Number(process.env.SMTP_PORT ?? 587),',\n ' secure: false,',\n ' requireTLS: true,',\n ' tls: { rejectUnauthorized: true },',\n \" from: 'no-reply@example.com',\",\n ' auth: { user: process.env.SMTP_USER!, pass: process.env.SMTP_PASS! },',\n ' },',\n ' publicRegistration: { enabled: false },',\n ' serviceTokens: { enabled: true, defaultTtlDays: 365 },',\n '};',\n ].join('\\n'), 'Configurazione tipica'),\n p(`options.openapi personalizza path, titolo e descrizione della documentazione OpenAPI. Con enabled: false UI e JSON non vengono montati, ma le API /api/accessi restano attive. initializeAccessiModule restituisce { nestApp, document } per mergiare il contratto nel documento OpenAPI unico dell'host.`),\n good('ts', [\n 'const { document } = await initializeAccessiModule(app, {',\n ' ...options,',\n \" openapi: { openApiPath: 'auth/docs', openApiJsonPath: 'auth/docs.json', title: 'Auth API' },\",\n '});',\n '// document.paths e document.components.schemas sono pronti per il merge',\n ].join('\\n'), 'OpenAPI personalizzato'),\n bad('ts', [\n \"jwtOptions: { secret: 'accessi', expiresIn: '24h' }, // segreto debole e condiviso\",\n 'mockDemoUser: true, // MAI in produzione',\n ].join('\\n'), 'Errori comuni'),\n note('danger', `Segreti`, `Non versionare i segreti e non passarli al frontend. jwtOptions.secret deve essere identico tra le istanze del backend e diverso da quello dei provider SSO.`),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'database',\n group: 'Configurazione',\n title: 'Schema e aggiornamento del database',\n summary: 'Tabelle, riconciliazione automatica e CLI.',\n blocks: [\n p(`Accessi possiede lo schema del dominio. Con autoUpdateDatabase true (predefinito) lo riconcilia a ogni avvio; con false verifica comunque la compatibilità in sola lettura e blocca gli avvii incompatibili.`),\n h(`Tabelle principali`),\n list([\n `PARAMETRI, UTENTI, UTENTI_CONFIG, UTENTI_PWD, UTENTI_OLDPWD, UTENTI_GDPR`,\n `RUOLI, MENU_GRP, MENU_TIPI, MENU, ABILITAZIONI, RUOLI_MNU, UTENTI_RUOLI`,\n `FILTRI, FILTRI_TIPO`,\n `SSO_PROVIDER, UTENTI_IDENTITA_EXT`,\n `ACCESSI_2FA, ACCESSI_SERVICE_TOKEN, ACCESSI_CATALOG_SCRIPT`,\n ]),\n code('bash', `npm run db:update:accessi # riconcilia lo schema\\nnpm run db:check:accessi # verifica senza modificare`, 'CLI schema'),\n note('warn', `Versioni`, `La libreria richiede uno schema compatibile con la versione corrente (ACCESSI_SCHEMA_VERSION). Se la verifica fallisce, leggi il codice errore ACCESSI_DATABASE_SCHEMA_OUTDATED ed esegui db:update:accessi.`),\n note('info', `Schema esatto`, `La riconciliazione allinea le tabelle Accessi a \"niente in piu, niente in meno\": le colonne non previste vengono segnalate come incompatibili e rimosse automaticamente (dopo rename e copie dei dati legacy). Gli indici, i trigger e i CHECK extra non previsti non vengono tolti, ma un indice non previsto su una tabella Accessi viene riportato come avviso senza bloccare l'avvio.`),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'catalog-migrations',\n group: 'Configurazione',\n title: 'Catalog migrations SQL',\n summary: 'Allinea menu, ruoli e cataloghi su piu database in modo idempotente.',\n blocks: [\n p(`Gli script SQL applicativi (menu, ruoli, tipi, cataloghi) restano nel repository del backend ospitante e Accessi li esegue dopo la riconciliazione dello schema. Ogni script viene tracciato nella tabella ACCESSI_CATALOG_SCRIPT (nome + checksum), quindi viene applicato una sola volta per database.`),\n h(`Configurazione`),\n good('ts', [\n 'catalogScripts: {',\n \" enabled: true,\",\n \" folder: './db/accessi-catalog', // versionata nel repo del backend\",\n \" onChecksumMismatch: 'error', // 'error' (default) | 'warn' | 'reapply'\",\n \" stopOnError: true,\",\n '}',\n ].join('\\n'), 'AccessiOptions'),\n list([\n 'Gli script sono ordinati per percorso (usa prefissi numerici: 0001_, 0002_, ...).',\n 'Devono essere idempotenti e ripetibili: UPDATE OR INSERT ... MATCHING, MERGE, DELETE+INSERT mirati.',\n 'Non includere utenti: le catalog migrations allineano solo catalogo e configurazione.',\n 'Supportati stringhe, commenti e statement multipli; SET TERM non serve perché gli script sono solo DML.',\n ]),\n note('danger', `Solo righe (DML)`, `Vincolo tassativo: uno script puo eseguire solo INSERT, UPDATE, DELETE, MERGE, SELECT, EXECUTE. Qualunque DDL (CREATE/ALTER/DROP, trigger, procedure, GRANT, transazioni) viene rifiutato. Lo schema si aggiorna solo con db:update:accessi.`),\n note('info', `File non conformi`, `Un file con statement non consentiti non viene applicato ne registrato nel ledger; il run prosegue con gli altri e il report riporta una voce per ogni file con i relativi messaggi di errore (visibili anche con db:catalog:accessi:check).`),\n note('info', `allowDdl`, `Disattivato di default. Impostarlo a true disabilita la validazione e permette DDL: usarlo solo consapevolmente e mai come sostituto dell'updater dello schema.`),\n code('bash', `# Applica (schema + catalog migrations) su un database\\nACCESSI_CATALOG_FOLDER=./db/accessi-catalog npm run db:catalog:accessi\\n\\n# Elenca solo quelle da applicare / modificate\\nACCESSI_CATALOG_FOLDER=./db/accessi-catalog npm run db:catalog:accessi:check`, 'CLI catalog migrations'),\n note('info', `Idempotenza`, `Uno script gia applicato con lo stesso checksum viene saltato. Se il file cambia dopo l'applicazione, con policy error il run si interrompe: non modificare uno script applicato, aggiungine uno nuovo.`),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'login',\n group: 'Autenticazione',\n title: 'Login locale e JWT',\n summary: 'Endpoint di login, risposta e gestione della password scaduta.',\n blocks: [\n code('http', `POST /api/accessi/auth/login\\nContent-Type: application/json\\n\\n{ \"email\": \"utente@example.com\", \"password\": \"Segreta123!\" }`, 'Login con password'),\n p(`La risposta include Result.token (il JWT) e il profilo utente con ruoli e permessi. Se l'account è passwordless, ometti la password: il server risponde con un challenge via email (vedi la sezione su 2FA e passwordless).`),\n note('info', `Scadenza del token`, `Result.token.expiresIn e sempre espresso in secondi (per esempio 86400 per 24h), indipendentemente dal formato usato in jwtOptions.expiresIn.`),\n note('info', `Autorizzazioni lato client`, `La fonte autorevole è Result.userGrants.grants: contiene i grant effettivi (ruoli + permessi diretti, livello massimo). Non leggere i permessi dal JWT e non usare Result.utente.permissions: non sono popolati. Per i superutenti grants contiene tutti i menu attivi al livello 30.`),\n good('ts', [\n \"const res = await fetch('/api/accessi/auth/login', {\",\n \" method: 'POST',\",\n \" headers: { 'Content-Type': 'application/json' },\",\n ' body: JSON.stringify({ email, password }),',\n '});',\n 'const body = await res.json();',\n 'if (body.Result?.challenge) { /* richiedi il codice */ }',\n 'const token = body.Result?.token;',\n ].join('\\n'), 'Chiamata dal frontend'),\n note('warn', `Password scaduta`, `Con passwordExpiration attivo, una password scaduta produce una PasswordExpiredResponse con code PASSWORD_EXPIRED. Il client deve avviare il reset della password.`),\n h(`Verifica del token`),\n code('http', `POST /api/accessi/auth/get-user-by-token\\nContent-Type: application/json\\n\\n{ \"token\": \"<jwt>\" }`, 'get-user-by-token'),\n p(`Usalo per verificare un JWT e ottenere il profilo corrente (utile al bootstrap di una SPA). Restituisce Result.userData.`),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'passwordless-2fa',\n group: 'Autenticazione',\n title: 'Codici email, 2FA e passwordless',\n summary: 'Verifica in due passi, challenge e policy per utente.',\n blocks: [\n p(`La verifica con codice email è per-utente (flagDueFattori / UTENTI_CONFIG.FLG2FATT) ed è disattivata per impostazione predefinita. Con passwordlessLoginEnabled (FLGPWDLESS, richiede la 2FA attiva) l'accesso avviene con il solo codice email.`),\n table(\n ['Configurazione', 'Accesso locale', 'Accesso SSO'],\n [\n ['2FA disattivata', 'Password', 'Provider'],\n ['2FA attiva', 'Password + codice email', 'Provider + codice email'],\n ['2FA + passwordless', 'Solo codice email', 'Provider + codice email'],\n ],\n ),\n code('json', `{\\n \"Result\": {\\n \"challenge\": {\\n \"twoFactorRequired\": true,\\n \"challengeId\": \"64-caratteri-esadecimali\",\\n \"method\": \"email\",\\n \"expiresAt\": \"2026-09-11T14:00:00.000Z\",\\n \"resendAfterSeconds\": 60\\n }\\n }\\n}`, 'Risposta con challenge'),\n code('http', `POST /api/accessi/auth/two-factor/verify\\n{ \"challengeId\": \"...\", \"code\": \"012345\" }\\n\\nPOST /api/accessi/auth/two-factor/resend\\n{ \"challengeId\": \"...\" }`, 'Verifica e reinvio'),\n list([\n `Il codice ha 6 cifre, dura 10 minuti, consente 5 tentativi totali e al massimo 3 invii per challenge.`,\n `Fino alla verifica non sono presenti token, profilo o grant.`,\n `Il challenge non è un JWT e non va usato nell'header Authorization.`,\n ]),\n bad('ts', [\n '// Sbagliato: usare il challenge come token',\n \"fetch('/api/ordini', { headers: { Authorization: 'Bearer ' + challengeId } }); // 401\",\n ].join('\\n'), 'Il challenge non è un token'),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'reset-password',\n group: 'Autenticazione',\n title: 'Reset password',\n summary: 'Invio email e conferma con token monouso.',\n blocks: [\n code('http', `POST /api/accessi/email/send-reset-password-email\\n{ \"email\": \"utente@example.com\" }\\n\\nPOST /api/accessi/auth/confirm-reset-password/{token}\\n{ \"newPassword\": \"NuovaSegreta123!\" }`, 'Reset in due passi'),\n p(`L'invio non rivela se l'email esiste (anti-enumerazione). Il token di reset è monouso ed è firmato con una chiave dedicata.`),\n note('warn', `Policy password (solo quando si imposta)`, `Reset, set-password admin e bootstrap richiedono almeno 8 caratteri con maiuscola, minuscola, cifra e carattere speciale, senza spazi e non comuni. Se non conforme l'API risponde 400 con code ACCESSI_WEAK_PASSWORD e l'elenco dei requisiti in details. La policy NON si applica al login: le password preesistenti eventualmente deboli continuano ad autenticare e vengono migrate senza blocco.`),\n note('info', `Pagina personalizzata`, `Imposta customResetPage per usare la tua pagina di reset invece di quella inclusa. confirmationEmailReturnUrl e confirmationEmailPrefix controllano il ritorno.`),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'middleware',\n group: 'Autorizzazione',\n title: 'authorizeAccessi (middleware)',\n summary: 'Proteggere le rotte della tua app con il JWT Accessi.',\n blocks: [\n p(`authorizeAccessi è un middleware Express che verifica il JWT, ricarica lo stato utente corrente e applica i requisiti. Su successo prosegue; su errore risponde direttamente con il contratto Accessi (non chiamare un tuo next(error)).`),\n good('ts', [\n \"app.get('/api/report',\",\n ' (req, res, next) => authorizeAccessi(req, res, next, {',\n \" requirements: [{ menuCode: 'REPORT', minPermissionLevel: 10 }],\",\n ' }),',\n ' (req, res) => res.json({ user: req.user, grants: req.userGrants }),',\n ');',\n ].join('\\n'), 'Rotta protetta'),\n h(`Che cosa trovi nella request`),\n list([\n `req.user e req.data: payload utente corrente (alias storici).`,\n `req.userGrants: grant effettivi, presente solo quando hai richiesto dei requisiti.`,\n ]),\n bad('ts', [\n \"app.get('/api/report', authorizeAccessi, (req, res) => {\",\n ' // Sbagliato: assumere che req.userGrants esista senza requisiti',\n ' const grants = req.userGrants.grants; // possibile TypeError',\n '});',\n ].join('\\n'), 'Non dare per scontato req.userGrants'),\n note('danger', `Lo stato utente è sempre ricontrollato`, `Un JWT valido non basta: se l'utente è stato bloccato, eliminato o ha cambiato policy, il middleware risponde 401 AUTH_USER_DISABLED.`),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'resource-server',\n group: 'Autorizzazione',\n title: 'Resource server e introspection',\n summary: 'Proteggere un backend fratello senza accedere al database Accessi.',\n blocks: [\n p(`Un backend che non possiede Accessi usa resource-server. La verifica puo essere locale (JWT) oppure delegata all'introspection di Accessi Service. La policy di autorizzazione usa gli stessi accessiRequirement del middleware storico.`),\n good('ts', [\n \"import { createAccessiAuthMiddleware } from 'emilsoftware-utilities/accessi/resource-server';\",\n '',\n 'app.use(createAccessiAuthMiddleware({',\n ' jwt: {',\n ' // In una topologia multi-servizio preferire RS256/JWKS:',\n ' // publicKey: process.env.ACCESSI_JWT_PUBLIC_KEY,',\n ' // algorithms: [\"RS256\"],',\n ' serverSecret: process.env.ACCESSI_JWT_SECRET, // HS256: deriva la chiave access',\n ' issuer: \"accessi\",',\n ' audience: \"backend-a\",',\n ' },',\n ' introspection: {',\n ' url: \"https://accessi.example.com/api/accessi/auth/introspect\",',\n ' serviceToken: process.env.ACCESSI_SERVICE_TOKEN!,',\n ' },',\n '}));',\n ].join('\\n'), 'Express resource server'),\n good('ts', [\n \"import { createAccessiAuthGuard } from 'emilsoftware-utilities/accessi/resource-server';\",\n '',\n '@UseGuards(createAccessiAuthGuard({',\n ' jwt: { publicKey: process.env.ACCESSI_JWT_PUBLIC_KEY, algorithms: [\"RS256\"] },',\n ' introspection: { url: process.env.ACCESSI_INTROSPECT_URL!, serviceToken: process.env.ACCESSI_SERVICE_TOKEN! },',\n ' authorization: { requirements: [{ menuCode: \"ORDINI\", minPermissionLevel: 20 }] },',\n '}))',\n '@Get(\"ordini\")',\n 'list(@Req() req: Request) { return req.user; }',\n ].join('\\n'), 'Guard Nest'),\n table(\n ['Campo', 'Obbligo', 'Note'],\n [\n ['jwt.serverSecret', 'HS256', 'Segreto grezzo Accessi; il resource server applica la KDF access.'],\n ['jwt.publicKey / jwt.jwks', 'RS/ES', 'Consigliato in multi-servizio. JWKS ha cache e timeout.'],\n ['jwt.issuer / jwt.audience', 'no', 'Consigliati per evitare token di altri ambienti.'],\n ['introspection.url', 'se servono grant', 'Endpoint service-to-service protetto da service token.'],\n ['introspection.serviceToken', 'se introspection', 'Scope richiesto: accessi:introspect.'],\n ['authorization', 'no', 'Policy accessiRequirement; senza introspection i grant sono obbligatori.'],\n ],\n ),\n note('warn', `Fail closed`, `Se introspection e configurata ma non raggiungibile, il resource server risponde 503. Non autorizza la richiesta.`),\n note('info', `Introspection`, `POST /api/accessi/auth/introspect restituisce active=false per token invalido, scaduto o utente disabilitato. I risultati attivi sono cachati lato resource server per 30 secondi per default.`),\n note('info', `JWKS`, `Con RS256/ES256 Accessi pubblica la JWKS su /accessi/.well-known/jwks.json (configurabile con jwtOptions.jwksPath). Il resource server puo usare jwks.url e ruotare le chiavi con previousPublicKeys.`),\n note('info', `Revoca`, `revocation.store applica la revoca per utente; revocation.accessTokenStore rifiuta i singoli jti revocati e revocation.familyStore invalida le famiglie di refresh token revocate per riuso sospetto.`),\n note('info', `Audit`, `I fallimenti di autenticazione del resource server, i blocchi edge, il riuso dei refresh token e le revoche di sessione producono eventi strutturati tramite auditAccessi; i token e i segreti vengono redatti dal Logger.`),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'requisiti',\n group: 'Autorizzazione',\n title: 'Requisiti e livelli di abilitazione',\n summary: 'DSL accessiRequirement: permission, and, or, not, custom.',\n blocks: [\n p(`I requisiti si compongono con accessiRequirement. La forma storica requirements è un AND di permessi. Il livello è TipoAbilitazione: NESSUNA=0, LETTURA=10, SCRITTURA=20, SPECIAL=30; il confronto è tipoAbilitazione >= soglia.`),\n good('ts', [\n \"import { authorizeAccessi, accessiRequirement as r } from 'emilsoftware-utilities';\",\n '',\n '// AND (entrambi i menu)',\n 'const both = r.and(r.permission(\"ORDINI\", 20), r.permission(\"CLIENTI\", 10));',\n '',\n '// OR',\n 'const either = r.or(r.permission(\"REPORT\", 10), r.permission(\"EXPORT\", 20));',\n '',\n '// NOT + custom',\n 'const policy = r.and(',\n ' r.permission(\"AREA_RISERVATA\", 10),',\n ' r.not(r.permission(\"BLOCCO_TOTALE\", 0)),',\n ');',\n '',\n 'app.get(\"/api/riservato\", (req, res, next) => authorizeAccessi(req, res, next, {',\n ' requirementTree: policy,',\n ' customRequirementHandlers: {',\n ' stessoReparto: async (ctx) => ctx.userCode === Number(ctx.req.query.reparto),',\n ' },',\n '}), handler);',\n ].join('\\n'), 'Policy composte'),\n bad('ts', [\n '// Sbagliato: AND/OR senza figli -> errore di configurazione (500 AUTH_REQUIREMENTS_MISCONFIGURED)',\n 'const empty = r.and();',\n ].join('\\n'), 'Configurazione non valida'),\n note('warn', `custom senza handler`, `Un requisito custom la cui chiave non ha un handler produce 500 AUTH_REQUIREMENTS_MISCONFIGURED: è un errore di programmazione, non un 403.`),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'utenti',\n group: 'Dominio',\n title: 'Utenti',\n summary: 'Creazione, aggiornamento, stati e GDPR.',\n blocks: [\n h(`Stati di registrazione (STAREG)`),\n table(\n ['Valore', 'Nome', 'Effetto sul login'],\n [\n ['0', 'NULL', 'Non confermato'],\n ['5', 'INSERT', 'Non confermato'],\n ['10', 'INVIO', 'Richiede il rinnovo della password'],\n ['20', 'CONF', 'Valido'],\n ['50', 'DELETE', 'Bloccato (soft delete)'],\n ['99', 'BLOCC', 'Bloccato'],\n ],\n ),\n h(`Endpoint principali (superutente salvo diversamente indicato)`),\n table(\n ['Metodo', 'Percorso', 'Descrizione'],\n [\n ['GET', '/api/accessi/user/get-users', 'Elenco con paginazione (limit e offset, header X-Total-Count).'],\n ['POST', '/api/accessi/user/create-managed-user', 'Crea un utente locale e invia il reset password.'],\n ['PUT', '/api/accessi/user/update-user/:codiceUtente', 'Aggiorna profilo e policy (campi privilegiati solo superutente).'],\n ['DELETE', '/api/accessi/user/delete-user/:codiceUtente', 'Soft delete: STAREG=DELETE. Admin o superutente.'],\n ['DELETE', '/api/accessi/user/delete-user-permanent/:codiceUtente', 'Eliminazione definitiva: rimuove utente e record collegati. Solo admin.'],\n ['POST', '/api/accessi/user/set-password/:codiceUtente', 'Imposta direttamente la password: hash lato backend e scadenza secondo policy, nessuna email.'],\n ['POST', '/api/accessi/user/set-stato', 'Imposta lo stato di registrazione.'],\n ['PATCH', '/api/accessi/user/set-gdpr/:codiceUtente', 'Registra il consenso GDPR.'],\n ['POST', '/api/accessi/user/register', 'Registrazione pubblica, solo se publicRegistration.enabled.'],\n ],\n ),\n note('warn', `Eliminazione: soft o definitiva`, `Il soft delete (STAREG=DELETE) è disponibile per admin e superutenti ed è reversibile. L'eliminazione definitiva rimuove utente e record collegati in transazione ed è riservata agli admin; il superutente senza flag Admin può solo flaggare l'utente come eliminato. Se l'utente ha dati collegati in altre tabelle (per esempio campi estesi del backend) l'eliminazione definitiva fallisce con code ACCESSI_USER_DELETE_HAS_REFERENCES e messaggio esplicito: usa allora il soft delete.`),\n note('ok', `Creazione con invito atomica`, `La creazione utente con invio email è atomica: se SMTP non è configurato si fallisce prima di scrivere, e se l'invio fallisce l'utente creato viene annullato (codice ACCESSI_USER_INVITE_EMAIL_FAILED). Non restano utenti senza password. In alternativa un admin crea l'utente e imposta la password dal form dedicato.`),\n note('ok', `Admin: controllo completo del profilo`, `Dalla scheda utente un admin modifica tutti i campi Accessi: anagrafica, cellulare, lingua, avatar, pagina di default, metadata JSON, 2FA/passwordless/login con password, flag, GDPR, stato registrazione e scadenza password. La password si imposta direttamente con set-password. Le scritture sono idempotenti (UPDATE OR INSERT su UTENTI_CONFIG) e verificate dal backend; non serve accedere al database.`),\n note('info', `Consenso GDPR`, `Il pulsante \"Registra consenso GDPR\" completa il processo: imposta FLGGDPR e DATGDPR e scrive la riga storica in UTENTI_GDPR (endpoint set-gdpr). Anche impostare flagGdpr via update-user completa il processo con la stessa riga storica.`),\n good('ts', [\n \"await fetch('/api/accessi/user/create-managed-user', {\",\n \" method: 'POST',\",\n \" headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + adminToken },\",\n ' body: JSON.stringify({',\n \" email: 'nuovo@example.com',\",\n \" nome: 'Mario',\",\n \" cognome: 'Rossi',\",\n ' roles: [1, 2],',\n \" permissions: [{ codiceMenu: 'REPORT', tipoAbilitazione: 10 }],\",\n ' }),',\n '});',\n ].join('\\n'), 'Creazione di un utente gestito'),\n bad('ts', [\n '// Sbagliato: inviare flag privilegiati dal frontend pubblico',\n 'fetch(\"/api/accessi/user/register\", {',\n ' method: \"POST\",',\n ' body: JSON.stringify({ email, flagSuper: true }), // ignorato o negato',\n '});',\n ].join('\\n'), 'Privilegi dal client'),\n note('info', `Campi di dominio`, `UTENTI_CONFIG contiene i campi di dominio Accessi: FLGSUPER, FLGADMIN, FLG2FATT (2FA), FLGPWDLESS (passwordless), FLGPASSWORD (login locale), CODLINGUA, CELLULARE, PAGDEF (pagina di default) e JSON_METADATA. I flag/colonne applicativi (FLGMOP, FLGPIANA, FLGADDETTI, FLGOSPITI, FLGPIANARFID, FLGCONTA, FLGCUBI, FLGCICLPASS, FLGDIPENDENTI, FLGINVENTARI, CAUMOV, NUMMAC, RAGSOCCLI, ENABLEIA, CELLUTE e NOMECAMPO) sono stati rimossi: i backend devono gestirli in tabelle proprie. Il telefono canonico e UTENTI_CONFIG.CELLULARE.`),\n note('info', `Autorizzazioni`, `I gruppi/menu si abilitano tramite ABILITAZIONI, RUOLI_MNU e UTENTI_RUOLI. Il vecchio meccanismo NOMECAMPO (flag applicativo su UTENTI_CONFIG) e stato rimosso.`),\n note('info', `Campi estesi`, `extensionFieldsOptions allega colonne di tabelle applicative al profilo (per esempio la ragione sociale). Le colonne sono in whitelist: non costruirle mai da input HTTP.`),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'ruoli',\n group: 'Dominio',\n title: 'Ruoli e permessi',\n summary: 'Assegnazioni sostitutive e composizione dei grant.',\n blocks: [\n p(`I ruoli contengono una lista di menu con livello. Le assegnazioni (ruoli e permessi diretti) sono SOSTITUTIVE: invia sempre la lista completa desiderata. I grant effettivi sommano ruoli e permessi diretti prendendo il livello massimo per menu; un livello 0 non annulla un grant del ruolo. I superutenti ricevono tutti i menu attivi al livello massimo.`),\n table(\n ['Metodo', 'Percorso', 'Descrizione'],\n [\n ['GET', '/api/accessi/permission/roles', 'Ruoli con i menu associati.'],\n ['POST', '/api/accessi/permission/create-role', 'Crea un ruolo (i menu sono obbligatori).'],\n ['PUT', '/api/accessi/permission/update-role/:codiceRuolo', 'Aggiorna un ruolo (menu sostitutivi).'],\n ['DELETE', '/api/accessi/permission/delete-role/:codiceRuolo', 'Elimina ruolo, mapping e assegnazioni.'],\n ['POST', '/api/accessi/permission/assign-roles/:codiceUtente', `Sostituisce i ruoli dell'utente.`],\n ['POST', '/api/accessi/permission/assign-permissions/:codiceUtente', 'Sostituisce i permessi diretti.'],\n ['GET', '/api/accessi/permission/grants/:codiceUtente', 'Grant effettivi, permessi diretti e ruoli.'],\n ],\n ),\n good('ts', [\n 'const role = {',\n \" descrizioneRuolo: 'Gestore ordini',\",\n ' menu: [',\n \" { codiceMenu: 'ORDINI', tipoAbilitazione: 20 },\",\n \" { codiceMenu: 'CLIENTI', tipoAbilitazione: 10 },\",\n ' ],',\n '};',\n 'await createRole(role); // POST /api/accessi/permission/create-role',\n '',\n \"// Sostitutivo: questa lista diventa l'intero set di ruoli dell'utente\",\n 'await assignRolesToUser(codiceUtente, { roles: [3, 7] });',\n ].join('\\n'), 'Creazione di un ruolo e assegnazione'),\n bad('ts', [\n \"// Sbagliato: pensare che l'assegnazione sia incrementale\",\n 'await assignRolesToUser(codiceUtente, { roles: [3] });',\n \"// ora l'utente ha SOLO il ruolo 3, non 3 più quelli di prima\",\n ].join('\\n'), 'Assegnazione non additiva'),\n note('warn', `Menu obbligatori`, `create-role e update-role rifiutano un ruolo senza menu. Se ti serve un ruolo senza accessi, non crearlo.`),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'menu',\n group: 'Dominio',\n title: 'Menu, gruppi e tipi menu',\n summary: 'Catalogo di navigazione e CRUD completo (superutente).',\n blocks: [\n table(\n ['Metodo', 'Percorso', 'Descrizione'],\n [\n ['GET', '/api/accessi/permission/menus', 'Menu attivi.'],\n ['GET', '/api/accessi/permission/menu-types', 'Catalogo dei tipi menu.'],\n ['GET', '/api/accessi/permission/groups-with-menus?includeDisabled=true', 'Gruppi con menu (anche disabilitati).'],\n ['POST', '/api/accessi/permission/menus', 'Crea un menu.'],\n ['PUT', '/api/accessi/permission/menus/:codiceMenu', 'Aggiorna un menu.'],\n ['DELETE', '/api/accessi/permission/menus/:codiceMenu', 'Elimina un menu (con grant e associazioni ai ruoli).'],\n ['POST', '/api/accessi/permission/menu-groups', 'Crea un gruppo.'],\n ['PUT', '/api/accessi/permission/menu-groups/:codiceGruppo', 'Aggiorna un gruppo.'],\n ['DELETE', '/api/accessi/permission/menu-groups/:codiceGruppo', 'Elimina un gruppo (solo se vuoto).'],\n ['POST/PUT/DELETE', '/api/accessi/permission/menu-types[/:codiceTipo]', 'CRUD dei tipi menu.'],\n ],\n ),\n p(`I codici (CODMNU, CODGRP, CODTIP) sono chiavi immutabili: si indicano in creazione e poi si passano nel percorso. Un gruppo o un tipo si elimina solo se non è referenziato.`),\n good('ts', [\n \"await createMenuGroup({ codiceGruppo: 'A', descrizioneGruppo: 'Amministrazione', ordineGruppo: 1, enabled: true });\",\n \"await createMenuType({ codiceTipo: 'X', descrizioneTipo: 'Operativo' });\",\n \"await createMenu({ codiceMenu: 'ORDINI', descrizioneMenu: 'Ordini', codiceGruppo: 'A', tipo: 'X', ordineMenu: 10, enabled: true });\",\n ].join('\\n'), 'Catalogo in ordine'),\n bad('ts', [\n '// Sbagliato: eliminare un gruppo che contiene menu -> 400 con spiegazione',\n \"await deleteMenuGroup('A'); // contiene N menu\",\n ].join('\\n'), 'Vincoli di integrità'),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'sso',\n group: 'SSO',\n title: 'SSO generico (federated authentication)',\n summary: 'Il backend valida il provider, Accessi gestisce identità e sessioni.',\n blocks: [\n p(`Accessi non valida i token Azure, Google o SAML: è il TUO backend a validare il flusso SSO e a passare solo { provider, subject } verificati a FederatedAuthService. Così issuer, secret e token restano fuori dal modulo.`),\n good('ts', [\n \"import { FederatedAuthService } from 'emilsoftware-utilities';\",\n '',\n '// Nel tuo callback SSO, dopo aver validato il token del provider:',\n 'const verified = { provider: \"azure-ad-acme\", subject: claims.oid };',\n 'const result = await federatedAuthService.authenticate(verified);',\n '',\n 'if (result.login.challenge) {',\n ' // 2FA attiva: restituisci il challenge al frontend',\n ' return res.json({ challenge: result.login.challenge });',\n '}',\n 'return res.json({ login: result.login, token: result.token });',\n ].join('\\n'), 'Login SSO'),\n note('info', `Scambio service-to-service`, `Se il backend fratello valida il provider SSO, può scambiare { provider, subject } con POST /api/accessi/federated-auth/exchange usando un service token con scope accessi:federated:exchange. Accessi non riceve il token del provider.`),\n h(`API amministrative (superutente)`),\n list([\n `GET/POST /api/accessi/federated-auth/providers, PATCH/DELETE providers/:provider`,\n `GET/POST /api/accessi/federated-auth/users/:codiceUtente/identities`,\n `POST /api/accessi/federated-auth/users (crea un utente SSO-only)`,\n `PATCH /api/accessi/federated-auth/users/:codiceUtente/password-login`,\n ]),\n bad('ts', [\n \"// Sbagliato: far validare il token del provider ad Accessi o fidarsi del frontend\",\n 'await federatedAuthService.authenticate({ provider, subject: req.body.email }); // subject non verificato',\n ].join('\\n'), `Non fidarti dell'input`),\n note('warn', `Provider in uso`, `Un provider si elimina solo se nessuna identità è collegata; altrimenti disabilitalo. La chiave del provider deve coincidere con la configurazione del backend.`),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'service-token',\n group: 'Token di servizio',\n title: 'Token macchina-a-macchina',\n summary: 'Autenticazione tecnica con scope: non è un superutente.',\n blocks: [\n p(`Formato: st_<id>.<segreto> (segreto da 256 bit). Nel database si salva solo lo SHA-256 del segreto; il segreto è mostrato una sola volta. La verifica restituisce { tokenId, label, scopes }: nessun ruolo o grant.`),\n note('danger', `Non è un superutente`, `Un service token non apre le API Accessi: abilita solo le rotte della tua app che proteggi con ServiceTokenGuard.`),\n note('info', `Introspection`, `Per un backend fratello creare un service token con scope accessi:introspect e usarlo in POST /api/accessi/auth/introspect. Il resource server lo configura in introspection.serviceToken.`),\n note('warn', `Rate limit condiviso`, `In cluster configurare serviceTokens.rateLimitStore e publicAuthRateLimit.store con createRedisRateLimitStore: i contatori in memoria valgono solo per singola istanza.`),\n note('info', `Edge rate limit`, `Per una soglia grossolana per IP configurare edgeRateLimit: viene applicata davanti alle rotte Accessi. Il gateway/API gateway resta comunque consigliato per bot protection e limiti di connessione.`),\n good('ts', [\n \"import { ServiceTokenGuard, RequireServiceTokenScopes } from 'emilsoftware-utilities';\",\n '',\n '@UseGuards(ServiceTokenGuard)',\n \"@RequireServiceTokenScopes('ia')\",\n '@Post(\"internal/ask\")',\n 'ask() { /* req.accessiServiceToken = { tokenId, label, scopes } */ }',\n ].join('\\n'), 'Rotta tecnica protetta'),\n h(`Gestione (superutente)`),\n list([\n `POST /api/accessi/service-token (label, scopes, expiresAt o ttlDays) -> segreto mostrato una volta`,\n `GET /api/accessi/service-token?includeRevoked=true`,\n `DELETE /api/accessi/service-token/:tokenId (revoca immediata)`,\n `POST /api/accessi/service-token/:tokenId/rotate`,\n ]),\n bad('ts', [\n '// Sbagliato: usare un service token per autenticare un utente umano',\n \"fetch('/api/profilo', { headers: { Authorization: 'Bearer ' + serviceToken } }); // req.user assente\",\n ].join('\\n'), 'Non è un utente'),\n note('warn', `Scadenza`, `Senza expiresAt né defaultTtlDays il token non scade: imposta serviceTokens.defaultTtlDays o una scadenza esplicita.`),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'console',\n group: 'Console',\n title: 'Console amministrativa',\n summary: 'Accesso, sezioni e che cosa si può fare.',\n blocks: [\n p(`La console è servita dal modulo su /api/accessi/console. Non ha stato proprio: ogni azione chiama le API protette con il JWT dell'utente connesso.`),\n list([\n `Utenti: elenco, creazione locale o SSO, profilo, ruoli, grant diretti, stato, 2FA e Login as.`,\n `Ruoli e grant: CRUD dei ruoli con l'albero delle abilitazioni.`,\n `Menu e gruppi: CRUD di gruppi, menu e tipi menu.`,\n `Token di servizio: creazione, rotazione, revoca.`,\n `SSO: provider e identità.`,\n `Wiki e Changelog: documentazione e novità della libreria.`,\n `SQL: esporta il DDL delle entità Accessi (tabelle, generatori, FK, indici, check, trigger) per replicare il database.`,\n ]),\n note('info', `Superutente (FLGSUPER)`, `Riceve TUTTE le abilitazioni al livello massimo (30) dopo il login. In console vede solo la sezione Utenti e gestisce utenti, ruoli e grant.`),\n note('info', `Admin (FLGADMIN)`, `Accede a tutta la console (catalogo, token, SSO, utenti) ma NON conferisce abilitazioni: i menu restano quelli assegnati via ABILITAZIONI/RUOLI_MNU. Gerarchia: ADMIN > SUPER > utente.`),\n note('info', `Gerarchia dei ruoli`, `Vale ADMIN (2) > SUPER (1) > utente comune (0). Un attore può gestire solo utenti di rango pari o inferiore: un superutente può amministrare totalmente utenti comuni e superutenti (profilo, password, flag, ruoli, grant, stato, eliminazione e identità/utenti SSO) ma NON può toccare un admin. Un admin può gestire chiunque. La regola è imposta dal backend (ensureCanManageTargetUser): un tentativo non consentito restituisce 403.`),\n note('info', `Gestione dei flag`, `Dalla scheda utente i flag sono mostrati in base al ruolo di chi è connesso: un admin può assegnare o togliere sia il flag Admin sia il flag Superutente; un superutente senza flag Admin può gestire solo il flag Superutente. I flag si impostano anche in creazione (utente locale o SSO).`),\n note('warn', `Ultimo admin`, `Per non restare chiusi fuori dalla console, l'ultimo admin attivo non può essere disattivato, bloccato, eliminato né privato del flag Admin: l'operazione restituisce 400 finché non viene promosso un altro admin.`),\n note('info', `Accesso`, `Apri /api/accessi/console e accedi con un utente con FLGSUPER o FLGADMIN. Le sezioni sono raggiungibili anche tramite deep link (per esempio /api/accessi/console/menu-e-gruppi/menu).`),\n note('info', `Bootstrap admin`, `Con adminBootstrap.enabled il backend stampa a ogni avvio un token casuale (monouso, valido fino al riavvio). La sezione \"Crea utente admin\" nella schermata di login compare solo quando adminBootstrap è abilitato (stato esposto da GET /api/accessi/auth/bootstrap-admin/status): incolla il token e crea il primo admin senza email di conferma. La password è opzionale: se assente viene generata e mostrata una sola volta.`),\n note('ok', `Auto-ingresso dal frontend`, `Se un frontend ha già una sessione Accessi valida e l'utente è abilitato alla console, può aprire la console senza rifare il login: \\`POST /api/accessi/console/entry\\` con \\`Authorization: Bearer <jwt>\\` restituisce un ticket monouso; apri poi \\`/api/accessi/console#entry=<ticket>\\`. Il JWT non finisce mai nell'URL; il ticket dura 60 secondi ed è utilizzabile una sola volta (endpoint di scambio protetto da rate limit).`),\n note('info', `Console centralizzata`, `In una topologia multi-backend la console va ospitata una sola volta su Accessi Service. Gli altri backend possono linkarla con \\`buildAccessiConsoleUrl\\` di \\`accessi/console\\` oppure proxare \\`/api/accessi/*\\` sullo stesso dominio. Non copiare gli asset della console in ogni applicazione.`),\n note('warn', `Ticket multi-istanza`, `I ticket di ingresso console sono in memoria per default. Con piu istanze configurare \\`consoleEntry.store\\` con \\`createRedisConsoleEntryStore\\`: il JWT viene cifrato e il consume e atomico.`),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'login-as',\n group: 'Console',\n title: 'Login as (accesso temporaneo)',\n summary: 'Superutenti e admin possono generare un URL per entrare in un applicazione come un altro utente.',\n blocks: [\n p(`Dalla lista utenti della console il pulsante Login as apre il pannello dedicato: inserisci l'URL dell'applicazione e premi \"Genera URL di accesso\". Accessi emette un JWT per l'utente target e lo allega all'URL come query parameter \\`loginAsToken\\`.`),\n code('http', `POST /api/accessi/user/login-as/:codiceUtente\\nContent-Type: application/json\\nAuthorization: Bearer <jwt amministratore>\\n\\n{ \"url\": \"https://app.example.com/dashboard\" }`, 'Richiesta'),\n code('json', `{\\n \"Result\": {\\n \"loginUrl\": \"https://app.example.com/dashboard?loginAsToken=eyJ...\",\\n \"token\": \"eyJ...\",\\n \"expiresInSeconds\": 86400,\\n \"expiresAt\": \"2026-10-24T14:05:00.000Z\"\\n }\\n}`, 'Risposta'),\n note('danger', `La durata è quella standard`, `Il token dura quanto la sessione Accessi standard (jwtOptions.expiresIn). È una credenziale: chiunque abbia l'URL può usarlo fino alla scadenza per entrare nell'applicazione come l'utente target. Non condividerlo, non salvarlo in log o documenti e non usarlo in produzione per operazioni distruttive.`),\n note('info', `Chi può generarlo`, `Solo utenti con FLGSUPER o FLGADMIN. Vale la stessa gerarchia della gestione utenti: un superutente non può impersonare un admin; un admin può impersonare chiunque. Il tentativo non consentito restituisce 403.`),\n note('warn', `Responsabilità dell applicazione`, `Accessi non controlla l'URL di destinazione né consuma il token al posto dell'applicazione. L'app deve leggere \\`loginAsToken\\`, validarlo (per esempio con \\`POST /api/accessi/auth/get-user-by-token\\` o \\`authorizeAccessi\\`) e decidere cosa consentire. Il token è un access token Accessi a tutti gli effetti, con claim \\`loginAs: true\\`, \\`amr: [\"login_as\"]\\` e \\`act\\` valorizzato con l'attore.`),\n p(`Il token viene emesso anche per utenti con 2FA attiva: l'attore ha già completato l'autenticazione amministrativa. Non usare questa funzione per aggirare la 2FA di un utente comune al di fuori di un intervento amministrativo autorizzato.`),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'errori',\n group: 'Riferimenti',\n title: 'Contratto errori e codici',\n summary: 'Formato della risposta e codici stabili ACCESSI_* e AUTH_*.',\n blocks: [\n p(`Gli errori HTTP delle API Accessi usano un contratto stabile: severity, status, statusCode, code, error (campo legacy uguale a code), message e details per gli errori di validazione.`),\n code('json', `{\\n \"severity\": \"error\",\\n \"status\": 400,\\n \"statusCode\": 2,\\n \"code\": \"ACCESSI_VALIDATION_ERROR\",\\n \"error\": \"ACCESSI_VALIDATION_ERROR\",\\n \"message\": \"La richiesta contiene dati non validi.\",\\n \"details\": [\"email: deve essere una stringa.\"]\\n}`, 'Errore di validazione'),\n table(\n ['Codice', 'Quando'],\n [\n ['ACCESSI_VALIDATION_ERROR', 'Body o parametri non validi (400).'],\n ['ACCESSI_AUTH_INITIALIZING', 'Bootstrap in corso (503).'],\n ['ACCESSI_AUTH_NOT_INITIALIZED', 'Modulo non inizializzato (500).'],\n ['AUTH_HEADER_MISSING / AUTH_TOKEN_MISSING', 'Header Authorization assente (401).'],\n ['AUTH_TOKEN_INVALID', 'JWT non valido o scaduto (401).'],\n ['AUTH_USER_DISABLED', 'Utente non più autorizzato (401).'],\n ['AUTH_INSUFFICIENT_PERMISSIONS', 'Requisiti non soddisfatti (403).'],\n ['AUTH_REQUIREMENTS_MISCONFIGURED', 'Policy malformata o handler custom mancante (500).'],\n ['ACCESSI_DATABASE_SCHEMA_OUTDATED', 'Schema del database incompatibile.'],\n ['ACCESSI_SERVICE_TOKEN_RATE_LIMITED', 'Troppi tentativi (429).'],\n ],\n ),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'endpoint',\n group: 'Riferimenti',\n title: 'Indice degli endpoint',\n summary: 'Tutte le rotte /api/accessi/* in una tabella.',\n blocks: [\n table(\n ['Area', 'Endpoint principali'],\n [\n ['Auth', 'POST auth/login, auth/get-user-by-token, auth/confirm-reset-password/:token, auth/refresh, auth/revoke-sessions, auth/revoke-token, auth/two-factor/verify, auth/two-factor/resend'],\n ['Email', 'POST email/send-reset-password-email, GET email/reset-password-page/:token'],\n ['Utenti', 'GET user/get-users, POST user/create-managed-user, PUT user/update-user/:codiceUtente, DELETE user/delete-user/:codiceUtente, DELETE user/delete-user-permanent/:codiceUtente, POST user/set-password/:codiceUtente, POST user/set-stato, PATCH user/set-gdpr/:codiceUtente, POST user/register, POST user/login-as/:codiceUtente'],\n ['Permessi', 'GET permission/roles, POST permission/create-role, PUT permission/update-role/:codiceRuolo, DELETE permission/delete-role/:codiceRuolo, POST permission/assign-roles/:codiceUtente, POST permission/assign-permissions/:codiceUtente, GET permission/grants/:codiceUtente, GET permission/menus, GET permission/menu-types, GET permission/groups-with-menus'],\n ['Catalogo', 'POST/PUT/DELETE permission/menus[/:codiceMenu], permission/menu-groups[/:codiceGruppo], permission/menu-types[/:codiceTipo]'],\n ['Configurator', 'PATCH configurator/menus/:codiceMenu/enabled, PATCH configurator/groups/:codiceGruppo/enabled'],\n ['SSO', 'GET/POST federated-auth/providers, PATCH/DELETE providers/:provider, GET/POST users/:codiceUtente/identities, POST users, PATCH users/:codiceUtente/password-login'],\n ['Token', 'POST/GET service-token, DELETE service-token/:tokenId, POST service-token/:tokenId/rotate'],\n ['Schema', 'GET schema (DDL completo + entità presenti, solo admin)'],\n ['Console', 'GET console, console/:view, console/:view/:sub, console/assets/:file'],\n ],\n ),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'antipattern',\n group: 'Riferimenti',\n title: 'Antipattern e checklist',\n summary: 'Che cosa non fare, in breve.',\n blocks: [\n list([\n `Non aprire la porta HTTP prima di initializeAccessiModule.`,\n `Non usare authorizeAccessi come unico strato: valida sempre anche l'input applicativo.`,\n `Non dare per scontato che req.userGrants esista se non hai richiesto dei requisiti.`,\n `Non considerare incrementali assignRolesToUser e assignPermissionsToUser: sono sostitutivi.`,\n `Non far validare i token SSO ad Accessi: passagli solo { provider, subject } verificati.`,\n `Non usare un service token come identità utente.`,\n `Non versionare segreti; jwtOptions.secret deve essere unico per ambiente.`,\n `Non eliminare gruppi o tipi in uso: gestisci l'errore 400 e disabilita invece.`,\n `Non usare il challenge 2FA come JWT.`,\n ]),\n note('ok', `Checklist di rilascio`, `Schema verificato (db:check), segreti da un secret manager, mockDemoUser=false, rate limit pubblico configurato, SMTP reale testato, serviceTokens.defaultTtlDays impostato.`),\n ],\n },\n];\n\n/** Elenco ordinato dei gruppi per l'indice. */\nexport function wikiGroups(): Array<{ group: string; sections: WikiSection[] }> {\n const groups: Array<{ group: string; sections: WikiSection[] }> = [];\n for (const section of WIKI_SECTIONS) {\n let bucket = groups.find((item) => item.group === section.group);\n if (!bucket) {\n bucket = { group: section.group, sections: [] };\n groups.push(bucket);\n }\n bucket.sections.push(section);\n }\n return groups;\n}\n\n/**\n * Digest compatto in Markdown per l'IA che costruisce il backend integratore.\n * Deriva dagli stessi dati della wiki, quindi resta sincronizzato.\n */\nexport function buildAiDigest(): string {\n const lines: string[] = [\n `# Accessi (emilsoftware-utilities) - guida compatta per l'integrazione backend`,\n '',\n `Modulo NestJS/Express + Firebird per autenticazione JWT, autorizzazione per menu, utenti, 2FA e passwordless, SSO generico, service token e console.`,\n '',\n ];\n\n for (const { group, sections } of wikiGroups()) {\n lines.push(`## ${group}`);\n for (const section of sections) {\n lines.push(`### ${section.title}`);\n lines.push(section.summary);\n for (const block of section.blocks) {\n switch (block.kind) {\n case 'p':\n lines.push(block.text);\n break;\n case 'h':\n lines.push(`**${block.text}**`);\n break;\n case 'list':\n block.items.forEach((item, index) => lines.push(`${block.ordered ? `${index + 1}.` : '-'} ${item}`));\n break;\n case 'code':\n lines.push('```' + block.language);\n lines.push(block.code);\n lines.push('```');\n break;\n case 'callout':\n lines.push(`[${block.tone.toUpperCase()}] ${block.title ? block.title + ': ' : ''}${block.text}`);\n break;\n case 'table':\n lines.push('| ' + block.head.join(' | ') + ' |');\n lines.push('| ' + block.head.map(() => '---').join(' | ') + ' |');\n block.rows.forEach((row) => lines.push('| ' + row.join(' | ') + ' |'));\n break;\n }\n }\n lines.push('');\n }\n }\n\n return lines.join('\\n').replace(/\\n{3,}/g, '\\n\\n').trim();\n}\n"]}
1
+ {"version":3,"file":"wiki.js","sourceRoot":"","sources":["../../../src/accessi-module/Console/wiki.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Contenuto della Wiki della console Accessi.\n *\n * I contenuti sono dati strutturati (non HTML) così da poter essere resi in modo\n * coerente dalla console e convertiti in un digest compatto per l'IA. Nessun\n * segreto o dato specifico di un ambiente deve finire qui.\n */\n\nexport type WikiTone = 'info' | 'ok' | 'warn' | 'danger';\n\nexport type WikiBlock =\n | { kind: 'p'; text: string }\n | { kind: 'h'; text: string }\n | { kind: 'list'; items: string[]; ordered?: boolean }\n | { kind: 'code'; language: string; title?: string; code: string; variant?: 'good' | 'bad' }\n | { kind: 'callout'; tone: WikiTone; title?: string; text: string }\n | { kind: 'table'; head: string[]; rows: string[][] };\n\nexport interface WikiSection {\n id: string;\n group: string;\n title: string;\n summary: string;\n blocks: WikiBlock[];\n}\n\nconst p = (text: string): WikiBlock => ({ kind: 'p', text });\nconst h = (text: string): WikiBlock => ({ kind: 'h', text });\nconst list = (items: string[], ordered = false): WikiBlock => ({ kind: 'list', items, ordered });\nconst code = (language: string, codeText: string, title?: string, variant?: 'good' | 'bad'): WikiBlock => ({ kind: 'code', language, title, code: codeText, variant });\nconst good = (language: string, codeText: string, title?: string): WikiBlock => code(language, codeText, title, 'good');\nconst bad = (language: string, codeText: string, title?: string): WikiBlock => code(language, codeText, title, 'bad');\nconst note = (tone: WikiTone, title: string, text: string): WikiBlock => ({ kind: 'callout', tone, title, text });\nconst table = (head: string[], rows: string[][]): WikiBlock => ({ kind: 'table', head, rows });\n\nexport const WIKI_SECTIONS: WikiSection[] = [\n // -------------------------------------------------------------------------\n {\n id: 'panoramica',\n group: 'Per iniziare',\n title: 'Panoramica e architettura',\n summary: 'Che cosa fa Accessi, come si incastra nel backend e come scorre una richiesta.',\n blocks: [\n p(`Accessi è un modulo Node/NestJS che aggiunge a un backend già esistente: autenticazione JWT, autorizzazione per ruoli e menu, gestione utenti, reset password, codici email (2FA e passwordless), SSO generico, token macchina-a-macchina e una console amministrativa. Il database è Firebird/InterBase.`),\n h(`Che cosa possiede il modulo`),\n list([\n `Le rotte sotto /api/accessi/* (login, utenti, ruoli, menu, SSO, token, console).`,\n `Lo schema database del dominio Accessi, riconciliato automaticamente al bootstrap.`,\n `Il middleware di autorizzazione authorizeAccessi per le rotte della tua applicazione.`,\n `La console web su /api/accessi/console.`,\n ]),\n h(`Che cosa resta alla tua applicazione`),\n list([\n `Il proprio frontend e le proprie rotte di business.`,\n `La validazione dei token dei provider SSO esterni (Accessi non li vede).`,\n `I segreti: jwtOptions.secret, SMTP, database ed encryptionKey non raggiungono mai il browser.`,\n ]),\n h(`Flusso di una richiesta protetta`),\n list([\n `Il client chiama una rotta della tua app con header Authorization: Bearer <jwt>.`,\n `La tua rotta esegue authorizeAccessi, che verifica firma HS256, scadenza e stato utente corrente.`,\n `Se passi un requisito, il modulo carica ruoli e grant dell'utente e li valuta.`,\n `Su successo req.user, req.data e (se richiesto) req.userGrants sono popolati; altrimenti risponde con il contratto errore Accessi.`,\n ], true),\n note('info', `Due modalità di bootstrap`, `Express esistente: initializeAccessiModule(app, options). NestJS esistente: AccessiModule.forRoot(options) nei tuoi imports. Non usarle entrambe.`),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'installazione',\n group: 'Per iniziare',\n title: 'Installazione',\n summary: 'Dipendenze, peer e build.',\n blocks: [\n code('bash', `npm install emilsoftware-utilities`, 'Installazione'),\n p(`La libreria dipende da node-firebird per il database e da Express 5 come peer per il montaggio. È consigliato Node 18 o superiore.`),\n p(`Il pacchetto è pubblicato in CommonJS con export map esplicita per ogni entry point (accessi/server, accessi/resource-server/*, accessi/client/*, accessi/contracts*, accessi/openapi, accessi/console, accessi/ui): gli import ESM funzionano tramite interop Node. sideEffects elenca solo root barrel, firebird-compat e bundle console, così il resto è tree-shakable.`),\n note('warn', `Un solo modulo per processo`, `AccessiModule è @Global(): inizializzalo una sola volta per processo. In caso di più istanze, usa un processo per istanza.`),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'bootstrap-express',\n group: 'Per iniziare',\n title: 'Bootstrap su Express esistente',\n summary: 'initializeAccessiModule: ordine corretto e attesa prima di listen.',\n blocks: [\n p(`Chiama initializeAccessiModule prima di aprire la porta HTTP. Durante il bootstrap, authorizeAccessi risponde 503 ACCESSI_AUTH_INITIALIZING invece di autorizzare richieste parziali.`),\n good('ts', [\n \"import express from 'express';\",\n \"import { initializeAccessiModule, authorizeAccessi } from 'emilsoftware-utilities';\",\n '',\n 'const app = express();',\n 'app.use(express.json());',\n '',\n 'await initializeAccessiModule(app, options); // prima di app.listen',\n '',\n \"app.get('/api/ordini', authorizeAccessi, (req, res) => {\",\n ' res.json({ utente: req.user });',\n '});',\n '',\n \"app.get('/api/ordini/scrivi', (req, res, next) =>\",\n ' authorizeAccessi(req, res, next, {',\n \" requirements: [{ menuCode: 'ORDINI', minPermissionLevel: 20 }],\",\n ' }),',\n '(req, res) => res.json({ ok: true }));',\n '',\n 'app.listen(3000);',\n ].join('\\n'), 'Express + Accessi'),\n bad('ts', [\n 'const app = express();',\n 'app.listen(3000); // porta aperta prima del bootstrap',\n 'await initializeAccessiModule(app, options); // le richieste nel frattempo ricevono 503',\n ].join('\\n'), 'Non aprire la porta prima del bootstrap'),\n h(`Rotte montate`),\n p(`Accessi intercetta solo /api/accessi/* e i path OpenAPI configurati (default /accessi/openapi* e /accessi/openapi.json). Tutte le altre rotte passano intatte al tuo stack Express.`),\n note('ok', `req.user`, `authorizeAccessi popola req.user con il payload utente corrente (utente, codiceUtente, flag) e req.data come alias storico. req.userGrants è presente solo se hai richiesto dei requisiti.`),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'bootstrap-nest',\n group: 'Per iniziare',\n title: 'Bootstrap su NestJS esistente',\n summary: 'AccessiModule.forRoot e uso dei provider esportati.',\n blocks: [\n p(`Se la tua applicazione è già NestJS, importa AccessiModule.forRoot(options). Il modulo è @Global e riesporta i servizi (AuthService, UserService, PermissionService, FederatedAuthService, ServiceTokenService, ServiceTokenGuard, AuthenticateGenService).`),\n good('ts', [\n \"import { Module } from '@nestjs/common';\",\n \"import { AccessiModule } from 'emilsoftware-utilities';\",\n '',\n '@Module({',\n ' imports: [AccessiModule.forRoot(options)],',\n '})',\n 'export class AppModule {}',\n ].join('\\n'), 'app.module.ts'),\n p(`Il global prefix api e le rotte /api/accessi/* sono gestiti dal modulo. Le tue rotte Nest si proteggono con JwtSimpleGuard usando il JWT Accessi. Per health e readiness usare createAccessiHealthModule({ accessiOptions }): espone /health/live e /health/ready con lo stesso contratto del servizio standalone (bootstrap, database, store Redis e check aggiuntivi).`),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'distribuzione',\n group: 'Per iniziare',\n title: 'Distribuzione multi-app e package entry point',\n summary: 'Come usare Accessi come servizio centrale, come resource server o solo come contratti.',\n blocks: [\n p(`Accessi non va montato per forza in ogni backend. La distribuzione consigliata separa il servizio che possiede il database e la console dai backend fratelli che devono solo autenticare le richieste.`),\n table(\n ['Entry point', 'Uso', 'Dipendenze server'],\n [\n ['accessi/server', 'Bootstrap completo: DB, SMTP, migrazioni, console, API.', 'sì'],\n ['accessi/resource-server', 'Guard/middleware per backend fratelli: verifica JWT e autorizzazione remota.', 'no'],\n ['accessi/client', 'Client OpenAPI generato per chiamare Accessi Service.', 'no'],\n ['accessi/client/public', 'Solo API pubbliche (login, refresh, 2FA, reset password, registrazione, ticket console).', 'no'],\n ['accessi/client/admin', 'Solo API amministrative (utenti, ruoli, menu, filtri, provider federati, schema).', 'no'],\n ['accessi/client/service', 'Solo API service-to-service (service token e introspezione), mai nei frontend.', 'no'],\n ['accessi/contracts', 'Tipi, enum e helper puri browser-safe.', 'no'],\n ['accessi/contracts/dtos', 'DTO Nest con class-validator e decoratori OpenAPI.', 'no'],\n ['accessi/openapi', 'setupOpenApi e merge del documento OpenAPI.', 'no'],\n ['accessi/console', 'Helper per URL console, ticket e 2FA handoff.', 'no'],\n ['accessi/ui', 'Percorsi degli asset Web Component frameworkless.', 'no'],\n ],\n ),\n p(`Topologia consigliata: un solo Accessi Service possiede database, encryptionKey, SMTP, chiavi JWT e console. Gli altri backend usano resource-server e client, senza credenziali del database Accessi.`),\n p(`Multi-tenancy: un database per tenant, configurato da file <id>.environment.ts tipizzati (AccessiTenantEnvironment). Il database resta invariato (nessuna colonna tenant); ogni tenant gira in un processo separato con chiavi, issuer e store propri. Validazione e avvio con npm run accessi:tenants; template systemd/Docker/Nginx/Traefik in deploy/; dettagli in docs/multi-tenancy.md.`),\n note('warn', `Root barrel legacy`, `Il root barrel e i deep import restano supportati fino alla prossima major. I nuovi consumer devono usare gli entry point della tabella; la mappa di migrazione e in docs/import-migration.md.`),\n p(`Per avviare Accessi come servizio autonomo usare startAccessiService: espone /health/live, /health/ready, /health/metrics, valida la configurazione e gestisce SIGTERM/SIGINT.`),\n p(`Per parlare con piu Accessi Service o tenant dallo stesso processo usare createAccessiClient: ogni istanza ha base URL e token propri, isolati anche tra richieste concorrenti. configureAccessiClient resta il default globale per le funzioni esportate dal package.`),\n good('ts', [\n \"// Backend che ospita Accessi\",\n \"import { initializeAccessiModule } from 'emilsoftware-utilities/accessi/server';\",\n '',\n \"// Backend fratello: nessun AccessiModule, nessun DB Accessi\",\n \"import { createAccessiAuthMiddleware } from 'emilsoftware-utilities/accessi/resource-server';\",\n \"import { createAccessiClient } from 'emilsoftware-utilities/accessi/client';\",\n '',\n \"const milano = createAccessiClient({ baseUrl: 'https://accessi-milano.example.com', getToken });\",\n \"const roma = createAccessiClient({ baseUrl: 'https://accessi-roma.example.com', token: tokenRoma });\",\n \"const [utentiMilano, utentiRoma] = await Promise.all([milano.getUsers(), roma.getUsers()]);\",\n ].join('\\n'), 'Import corretti per ruolo'),\n note('warn', `Un solo proprietario`, `Non montare AccessiModule in due backend puntati allo stesso database: migrazioni, bootstrap admin, ticket console e rate limit diventano ambigui.`),\n note('info', `Client per scope`, `Usare accessi/client/public nei frontend pubblici, accessi/client/admin nella console e nei tool amministrativi, accessi/client/service solo nei backend fratelli. Gli scope partizionano il client completo e condividono gli stessi tipi; il client completo resta disponibile per compatibilita.`),\n note('info', `Split in pacchetti`, `Il package resta unico con entry point per scope: decisione, soglie per rivalutarla e piano di migrazione sono nell'ADR docs/adr/0001-split-package.md.`),\n note('info', `IP reali dietro proxy`, `Imposta http.trustProxy sul numero di proxy davanti al servizio (1 per Nginx/Traefik/ALB, 2 con Cloudflare + ingress) e lascialo false se il servizio e esposto direttamente. Rate limit e audit usano req.ip: dettagli in docs/proxy-ip.md.`),\n note('info', `Prompt di integrazione`, `Il file integration-prompt.md nella root del repository contiene il contesto completo per frontend, backend e agenti AI.`),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'opzioni',\n group: 'Configurazione',\n title: 'AccessiOptions (riferimento completo)',\n summary: 'Ogni campo di configurazione con tipo, obbligo e note.',\n blocks: [\n table(\n ['Campo', 'Tipo', 'Obbligo', 'Note'],\n [\n ['databaseOptions', 'Options (node-firebird)', 'sì', 'Connessione Firebird al database Accessi.'],\n ['confirmationEmailUrl', 'string', 'sì', 'Base URL usata per il reset password.'],\n ['confirmationEmailReturnUrl', 'string', 'sì', 'URL di ritorno dopo il reset.'],\n ['confirmationEmailPrefix', 'string', 'no', 'Prefisso passato alla pagina di reset.'],\n ['customResetPage', 'string', 'no', 'Sostituisce la pagina HTML di reset inclusa.'],\n ['encryptionKey', 'string', 'sì', 'Compatibilità e migrazione delle password legacy.'],\n ['mockDemoUser', 'boolean', 'sì', 'Utenti fittizi admin e demo solo fuori produzione.'],\n ['passwordExpiration', 'boolean', 'no', 'Attiva il controllo della scadenza password al login.'],\n ['passwordExpirationDays', 'number', 'no', 'Valore predefinito: 90 giorni.'],\n ['legacyPasswordMigrationOnStartup', 'boolean', 'no', 'Predefinito true: converte le password legacy.'],\n ['autoUpdateDatabase', 'boolean', 'no', 'Predefinito true: riconcilia lo schema al boot.'],\n ['jwtOptions', '{ secret, expiresIn, refreshExpiresIn, algorithm, issuer, audience, privateKey, publicKey, keyId, jwksPath, previousPublicKeys, onUnknownKeyId }', 'sì', 'Firma, durata, refresh token, JWKS e rotazione chiavi. Default HS256; RS/ES consigliati per resource server multi-servizio.'],\n ['emailOptions', 'EmailOptions', 'sì', 'SMTP per reset e codici email.'],\n ['publicAuthRateLimit', 'PublicAuthRateLimitOptions', 'no', 'Limiti per endpoint pubblici; store condiviso opzionale (Redis) per il cluster.'],\n ['edgeRateLimit', 'AccessiEdgeRateLimitOptions', 'no', 'Limite grossolano per IP davanti alle rotte Accessi; opzionale, store condiviso supportato.'],\n ['http', '{ cors, trustProxy, securityHeaders }', 'no', 'CORS, IP reali dietro proxy e header di sicurezza con CSP a nonce per la console e direttive configurabili.'],\n ['authCookie', '{ enabled, secure, sameSite, csrf, ... }', 'no', 'Cookie HttpOnly e CSRF double-submit al posto del Bearer. Default disattivato.'],\n ['metrics', '{ enabled, sink, prefix }', 'no', 'Metriche Prometheus di dominio (login, 2FA, refresh, rate limit, store) e sink esterno opzionale.'],\n ['audit', '{ enabled, sinks, store, retentionDays, redact }', 'no', 'Audit persistente redatto, retention e query/export admin. Default: solo log strutturato.'],\n ['publicRegistration', '{ enabled }', 'no', 'Registrazione pubblica: disattivata per impostazione predefinita.'],\n ['federatedAuthentication', 'FederatedAuthenticationOptions', 'no', `Abilita l'SSO generico.`],\n ['extensionFieldsOptions', 'ExtensionFieldsOptions[]', 'no', 'Tabelle esterne allegate al profilo.'],\n ['serviceTokens', '{ enabled, defaultTtlDays, rateLimitStore }', 'no', 'Token macchina-a-macchina, abilitati per impostazione predefinita; rate limit condiviso opzionale.'],\n ['catalogScripts', '{ enabled, folder, ... }', 'no', 'Catalog migrations SQL eseguite dopo la riconciliazione dello schema.'],\n ['adminBootstrap', '{ enabled, superUser }', 'no', 'Token casuale a ogni avvio per creare il primo admin dalla console.'],\n ['publicBaseUrl', 'string', 'no', 'Base URL usata solo per loggare console/OpenAPI/API.'],\n ['consoleApiBaseUrl', 'string', 'no', 'Base URL API per la console quando UI e API sono su domini diversi.'],\n ['openapi', 'OpenApiSetupOptions', 'no', 'Path, titolo, descrizione e enabled della documentazione OpenAPI. Default invariati.'],\n ['consoleEntry', '{ ttlMs, store }', 'no', 'TTL e store condiviso dei ticket console (Redis per multi-istanza). Default in memoria.'],\n ['sessionRevocation', '{ enabled, store, ttlMs }', 'no', 'Revoca sessioni per utente tramite store memory/Redis; abilita POST /api/accessi/auth/revoke-sessions.'],\n ['accessTokenRevocation', '{ enabled, store, ttlMs }', 'no', 'Revoca dei singoli access token tramite jti (memory/Redis); abilita POST /api/accessi/auth/revoke-token.'],\n ['refreshTokenRotation', '{ enabled, store, reuseGraceMs, revokeUserSessionsOnReuse }', 'no', 'Refresh token monouso tramite store memory/Redis; al riuso oltre la finestra di grazia revoca la famiglia di token. Consigliato in multi-istanza.'],\n ['logger', 'LoggerService', 'no', 'Logger Nest del sotto-app; se assente Nest usa il proprio logger.'],\n ['bufferLogs', 'boolean', 'no', 'Buffer dei log di bootstrap del sotto-app. Default true.'],\n ],\n ),\n good('ts', [\n \"import type { AccessiOptions } from 'emilsoftware-utilities';\",\n '',\n 'export const options: AccessiOptions = {',\n ' databaseOptions: {',\n ' host: process.env.ACCESSI_DB_HOST!,',\n ' port: Number(process.env.ACCESSI_DB_PORT ?? 3050),',\n ' database: process.env.ACCESSI_DB_NAME!,',\n ' user: process.env.ACCESSI_DB_USER!,',\n ' password: process.env.ACCESSI_DB_PASSWORD!,',\n ' },',\n \" confirmationEmailUrl: 'https://app.example.com',\",\n \" confirmationEmailReturnUrl: 'https://app.example.com/login',\",\n ' encryptionKey: process.env.ACCESSI_ENCRYPTION_KEY!,',\n ' mockDemoUser: false,',\n ' passwordExpiration: true,',\n ' passwordExpirationDays: 90,',\n ' jwtOptions: {',\n \" secret: process.env.ACCESSI_JWT_SECRET!, // almeno 32 caratteri casuali, unico per ambiente\",\n \" expiresIn: '24h',\",\n \" issuer: 'accessi',\",\n \" audience: 'backend-app',\",\n ' },',\n ' emailOptions: {',\n ' host: process.env.SMTP_HOST!,',\n ' port: Number(process.env.SMTP_PORT ?? 587),',\n ' secure: false,',\n ' requireTLS: true,',\n ' tls: { rejectUnauthorized: true },',\n \" from: 'no-reply@example.com',\",\n ' auth: { user: process.env.SMTP_USER!, pass: process.env.SMTP_PASS! },',\n ' },',\n ' publicRegistration: { enabled: false },',\n ' serviceTokens: { enabled: true, defaultTtlDays: 365 },',\n '};',\n ].join('\\n'), 'Configurazione tipica'),\n p(`options.openapi personalizza path, titolo e descrizione della documentazione OpenAPI. Con enabled: false UI e JSON non vengono montati, ma le API /api/accessi restano attive. initializeAccessiModule restituisce { nestApp, document } per mergiare il contratto nel documento OpenAPI unico dell'host.`),\n good('ts', [\n 'const { document } = await initializeAccessiModule(app, {',\n ' ...options,',\n \" openapi: { openApiPath: 'auth/docs', openApiJsonPath: 'auth/docs.json', title: 'Auth API' },\",\n '});',\n '// document.paths e document.components.schemas sono pronti per il merge',\n ].join('\\n'), 'OpenAPI personalizzato'),\n bad('ts', [\n \"jwtOptions: { secret: 'accessi', expiresIn: '24h' }, // segreto debole e condiviso\",\n 'mockDemoUser: true, // MAI in produzione',\n ].join('\\n'), 'Errori comuni'),\n note('danger', `Segreti`, `Non versionare i segreti e non passarli al frontend. jwtOptions.secret deve essere identico tra le istanze del backend e diverso da quello dei provider SSO.`),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'database',\n group: 'Configurazione',\n title: 'Schema e aggiornamento del database',\n summary: 'Tabelle, riconciliazione automatica e CLI.',\n blocks: [\n p(`Accessi possiede lo schema del dominio. Con autoUpdateDatabase true (predefinito) lo riconcilia a ogni avvio; con false verifica comunque la compatibilità in sola lettura e blocca gli avvii incompatibili.`),\n h(`Tabelle principali`),\n list([\n `PARAMETRI, UTENTI, UTENTI_CONFIG, UTENTI_PWD, UTENTI_OLDPWD, UTENTI_GDPR`,\n `RUOLI, MENU_GRP, MENU_TIPI, MENU, ABILITAZIONI, RUOLI_MNU, UTENTI_RUOLI`,\n `FILTRI, FILTRI_TIPO`,\n `SSO_PROVIDER, UTENTI_IDENTITA_EXT`,\n `ACCESSI_2FA, ACCESSI_SERVICE_TOKEN, ACCESSI_CATALOG_SCRIPT`,\n ]),\n code('bash', `npm run db:update:accessi # riconcilia lo schema\\nnpm run db:check:accessi # verifica senza modificare`, 'CLI schema'),\n note('warn', `Versioni`, `La libreria richiede uno schema compatibile con la versione corrente (ACCESSI_SCHEMA_VERSION). Se la verifica fallisce, leggi il codice errore ACCESSI_DATABASE_SCHEMA_OUTDATED ed esegui db:update:accessi.`),\n note('info', `Schema esatto`, `La riconciliazione allinea le tabelle Accessi a \"niente in piu, niente in meno\": le colonne non previste vengono segnalate come incompatibili e rimosse automaticamente (dopo rename e copie dei dati legacy). Gli indici, i trigger e i CHECK extra non previsti non vengono tolti, ma un indice non previsto su una tabella Accessi viene riportato come avviso senza bloccare l'avvio.`),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'catalog-migrations',\n group: 'Configurazione',\n title: 'Catalog migrations SQL',\n summary: 'Allinea menu, ruoli e cataloghi su piu database in modo idempotente.',\n blocks: [\n p(`Gli script SQL applicativi (menu, ruoli, tipi, cataloghi) restano nel repository del backend ospitante e Accessi li esegue dopo la riconciliazione dello schema. Ogni script viene tracciato nella tabella ACCESSI_CATALOG_SCRIPT (nome + checksum), quindi viene applicato una sola volta per database.`),\n h(`Configurazione`),\n good('ts', [\n 'catalogScripts: {',\n \" enabled: true,\",\n \" folder: './db/accessi-catalog', // versionata nel repo del backend\",\n \" onChecksumMismatch: 'error', // 'error' (default) | 'warn' | 'reapply'\",\n \" stopOnError: true,\",\n '}',\n ].join('\\n'), 'AccessiOptions'),\n list([\n 'Gli script sono ordinati per percorso (usa prefissi numerici: 0001_, 0002_, ...).',\n 'Devono essere idempotenti e ripetibili: UPDATE OR INSERT ... MATCHING, MERGE, DELETE+INSERT mirati.',\n 'Non includere utenti: le catalog migrations allineano solo catalogo e configurazione.',\n 'Supportati stringhe, commenti e statement multipli; SET TERM non serve perché gli script sono solo DML.',\n ]),\n note('danger', `Solo righe (DML)`, `Vincolo tassativo: uno script puo eseguire solo INSERT, UPDATE, DELETE, MERGE, SELECT, EXECUTE. Qualunque DDL (CREATE/ALTER/DROP, trigger, procedure, GRANT, transazioni) viene rifiutato. Lo schema si aggiorna solo con db:update:accessi.`),\n note('info', `File non conformi`, `Un file con statement non consentiti non viene applicato ne registrato nel ledger; il run prosegue con gli altri e il report riporta una voce per ogni file con i relativi messaggi di errore (visibili anche con db:catalog:accessi:check).`),\n note('info', `allowDdl`, `Disattivato di default. Impostarlo a true disabilita la validazione e permette DDL: usarlo solo consapevolmente e mai come sostituto dell'updater dello schema.`),\n code('bash', `# Applica (schema + catalog migrations) su un database\\nACCESSI_CATALOG_FOLDER=./db/accessi-catalog npm run db:catalog:accessi\\n\\n# Elenca solo quelle da applicare / modificate\\nACCESSI_CATALOG_FOLDER=./db/accessi-catalog npm run db:catalog:accessi:check`, 'CLI catalog migrations'),\n note('info', `Idempotenza`, `Uno script gia applicato con lo stesso checksum viene saltato. Se il file cambia dopo l'applicazione, con policy error il run si interrompe: non modificare uno script applicato, aggiungine uno nuovo.`),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'login',\n group: 'Autenticazione',\n title: 'Login locale e JWT',\n summary: 'Endpoint di login, risposta e gestione della password scaduta.',\n blocks: [\n code('http', `POST /api/accessi/auth/login\\nContent-Type: application/json\\n\\n{ \"email\": \"utente@example.com\", \"password\": \"Segreta123!\" }`, 'Login con password'),\n p(`La risposta include Result.token (il JWT) e il profilo utente con ruoli e permessi. Se l'account è passwordless, ometti la password: il server risponde con un challenge via email (vedi la sezione su 2FA e passwordless).`),\n note('info', `Scadenza del token`, `Result.token.expiresIn e sempre espresso in secondi (per esempio 86400 per 24h), indipendentemente dal formato usato in jwtOptions.expiresIn.`),\n note('info', `Autorizzazioni lato client`, `La fonte autorevole è Result.userGrants.grants: contiene i grant effettivi (ruoli + permessi diretti, livello massimo). Non leggere i permessi dal JWT e non usare Result.utente.permissions: non sono popolati. Per i superutenti grants contiene tutti i menu attivi al livello 30.`),\n good('ts', [\n \"const res = await fetch('/api/accessi/auth/login', {\",\n \" method: 'POST',\",\n \" headers: { 'Content-Type': 'application/json' },\",\n ' body: JSON.stringify({ email, password }),',\n '});',\n 'const body = await res.json();',\n 'if (body.Result?.challenge) { /* richiedi il codice */ }',\n 'const token = body.Result?.token;',\n ].join('\\n'), 'Chiamata dal frontend'),\n note('warn', `Password scaduta`, `Con passwordExpiration attivo, una password scaduta produce una PasswordExpiredResponse con code PASSWORD_EXPIRED. Il client deve avviare il reset della password.`),\n h(`Verifica del token`),\n code('http', `POST /api/accessi/auth/get-user-by-token\\nContent-Type: application/json\\n\\n{ \"token\": \"<jwt>\" }`, 'get-user-by-token'),\n p(`Usalo per verificare un JWT e ottenere il profilo corrente (utile al bootstrap di una SPA). Restituisce Result.userData.`),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'passwordless-2fa',\n group: 'Autenticazione',\n title: 'Codici email, 2FA e passwordless',\n summary: 'Verifica in due passi, challenge e policy per utente.',\n blocks: [\n p(`La verifica con codice email è per-utente (flagDueFattori / UTENTI_CONFIG.FLG2FATT) ed è disattivata per impostazione predefinita. Con passwordlessLoginEnabled (FLGPWDLESS, richiede la 2FA attiva) l'accesso avviene con il solo codice email.`),\n table(\n ['Configurazione', 'Accesso locale', 'Accesso SSO'],\n [\n ['2FA disattivata', 'Password', 'Provider'],\n ['2FA attiva', 'Password + codice email', 'Provider + codice email'],\n ['2FA + passwordless', 'Solo codice email', 'Provider + codice email'],\n ],\n ),\n code('json', `{\\n \"Result\": {\\n \"challenge\": {\\n \"twoFactorRequired\": true,\\n \"challengeId\": \"64-caratteri-esadecimali\",\\n \"method\": \"email\",\\n \"expiresAt\": \"2026-09-11T14:00:00.000Z\",\\n \"resendAfterSeconds\": 60\\n }\\n }\\n}`, 'Risposta con challenge'),\n code('http', `POST /api/accessi/auth/two-factor/verify\\n{ \"challengeId\": \"...\", \"code\": \"012345\" }\\n\\nPOST /api/accessi/auth/two-factor/resend\\n{ \"challengeId\": \"...\" }`, 'Verifica e reinvio'),\n list([\n `Il codice ha 6 cifre, dura 10 minuti, consente 5 tentativi totali e al massimo 3 invii per challenge.`,\n `Fino alla verifica non sono presenti token, profilo o grant.`,\n `Il challenge non è un JWT e non va usato nell'header Authorization.`,\n ]),\n bad('ts', [\n '// Sbagliato: usare il challenge come token',\n \"fetch('/api/ordini', { headers: { Authorization: 'Bearer ' + challengeId } }); // 401\",\n ].join('\\n'), 'Il challenge non è un token'),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'reset-password',\n group: 'Autenticazione',\n title: 'Reset password',\n summary: 'Invio email e conferma con token monouso.',\n blocks: [\n code('http', `POST /api/accessi/email/send-reset-password-email\\n{ \"email\": \"utente@example.com\" }\\n\\nPOST /api/accessi/auth/confirm-reset-password/{token}\\n{ \"newPassword\": \"NuovaSegreta123!\" }`, 'Reset in due passi'),\n p(`L'invio non rivela se l'email esiste (anti-enumerazione). Il token di reset è monouso ed è firmato con una chiave dedicata.`),\n note('warn', `Policy password (solo quando si imposta)`, `Reset, set-password admin e bootstrap richiedono almeno 8 caratteri con maiuscola, minuscola, cifra e carattere speciale, senza spazi e non comuni. Se non conforme l'API risponde 400 con code ACCESSI_WEAK_PASSWORD e l'elenco dei requisiti in details. La policy NON si applica al login: le password preesistenti eventualmente deboli continuano ad autenticare e vengono migrate senza blocco.`),\n note('info', `Pagina personalizzata`, `Imposta customResetPage per usare la tua pagina di reset invece di quella inclusa. confirmationEmailReturnUrl e confirmationEmailPrefix controllano il ritorno.`),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'middleware',\n group: 'Autorizzazione',\n title: 'authorizeAccessi (middleware)',\n summary: 'Proteggere le rotte della tua app con il JWT Accessi.',\n blocks: [\n p(`authorizeAccessi è un middleware Express che verifica il JWT, ricarica lo stato utente corrente e applica i requisiti. Su successo prosegue; su errore risponde direttamente con il contratto Accessi (non chiamare un tuo next(error)).`),\n good('ts', [\n \"app.get('/api/report',\",\n ' (req, res, next) => authorizeAccessi(req, res, next, {',\n \" requirements: [{ menuCode: 'REPORT', minPermissionLevel: 10 }],\",\n ' }),',\n ' (req, res) => res.json({ user: req.user, grants: req.userGrants }),',\n ');',\n ].join('\\n'), 'Rotta protetta'),\n h(`Che cosa trovi nella request`),\n list([\n `req.user e req.data: payload utente corrente (alias storici).`,\n `req.userGrants: grant effettivi, presente solo quando hai richiesto dei requisiti.`,\n ]),\n bad('ts', [\n \"app.get('/api/report', authorizeAccessi, (req, res) => {\",\n ' // Sbagliato: assumere che req.userGrants esista senza requisiti',\n ' const grants = req.userGrants.grants; // possibile TypeError',\n '});',\n ].join('\\n'), 'Non dare per scontato req.userGrants'),\n note('danger', `Lo stato utente è sempre ricontrollato`, `Un JWT valido non basta: se l'utente è stato bloccato, eliminato o ha cambiato policy, il middleware risponde 401 AUTH_USER_DISABLED.`),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'resource-server',\n group: 'Autorizzazione',\n title: 'Resource server e introspection',\n summary: 'Proteggere un backend fratello senza accedere al database Accessi.',\n blocks: [\n p(`Un backend che non possiede Accessi usa resource-server. La verifica puo essere locale (JWT) oppure delegata all'introspection di Accessi Service. La policy di autorizzazione usa gli stessi accessiRequirement del middleware storico.`),\n good('ts', [\n \"import { createAccessiAuthMiddleware } from 'emilsoftware-utilities/accessi/resource-server';\",\n '',\n 'app.use(createAccessiAuthMiddleware({',\n ' jwt: {',\n ' // In una topologia multi-servizio preferire RS256/JWKS:',\n ' // publicKey: process.env.ACCESSI_JWT_PUBLIC_KEY,',\n ' // algorithms: [\"RS256\"],',\n ' serverSecret: process.env.ACCESSI_JWT_SECRET, // HS256: deriva la chiave access',\n ' issuer: \"accessi\",',\n ' audience: \"backend-a\",',\n ' },',\n ' introspection: {',\n ' url: \"https://accessi.example.com/api/accessi/auth/introspect\",',\n ' serviceToken: process.env.ACCESSI_SERVICE_TOKEN!,',\n ' },',\n '}));',\n ].join('\\n'), 'Express resource server'),\n good('ts', [\n \"import { createAccessiAuthGuard } from 'emilsoftware-utilities/accessi/resource-server';\",\n '',\n '@UseGuards(createAccessiAuthGuard({',\n ' jwt: { publicKey: process.env.ACCESSI_JWT_PUBLIC_KEY, algorithms: [\"RS256\"] },',\n ' introspection: { url: process.env.ACCESSI_INTROSPECT_URL!, serviceToken: process.env.ACCESSI_SERVICE_TOKEN! },',\n ' authorization: { requirements: [{ menuCode: \"ORDINI\", minPermissionLevel: 20 }] },',\n '}))',\n '@Get(\"ordini\")',\n 'list(@Req() req: Request) { return req.user; }',\n ].join('\\n'), 'Guard Nest'),\n table(\n ['Campo', 'Obbligo', 'Note'],\n [\n ['jwt.serverSecret', 'HS256', 'Segreto grezzo Accessi; il resource server applica la KDF access.'],\n ['jwt.publicKey / jwt.jwks', 'RS/ES', 'Consigliato in multi-servizio. JWKS ha cache e timeout.'],\n ['jwt.issuer / jwt.audience', 'no', 'Consigliati per evitare token di altri ambienti.'],\n ['introspection.url', 'se servono grant', 'Endpoint service-to-service protetto da service token.'],\n ['introspection.serviceToken', 'se introspection', 'Scope richiesto: accessi:introspect.'],\n ['authorization', 'no', 'Policy accessiRequirement; senza introspection i grant sono obbligatori.'],\n ],\n ),\n note('warn', `Fail closed`, `Se introspection e configurata ma non raggiungibile, il resource server risponde 503. Non autorizza la richiesta.`),\n note('info', `Introspection`, `POST /api/accessi/auth/introspect restituisce active=false per token invalido, scaduto o utente disabilitato. I risultati attivi sono cachati lato resource server per 30 secondi per default.`),\n note('info', `JWKS`, `Con RS256/ES256 Accessi pubblica la JWKS su /accessi/.well-known/jwks.json (configurabile con jwtOptions.jwksPath). Il resource server puo usare jwks.url e ruotare le chiavi con previousPublicKeys.`),\n note('info', `Revoca`, `revocation.store applica la revoca per utente; revocation.accessTokenStore rifiuta i singoli jti revocati e revocation.familyStore invalida le famiglie di refresh token revocate per riuso sospetto.`),\n note('info', `Audit`, `I fallimenti di autenticazione del resource server, i blocchi edge, il riuso dei refresh token e le revoche di sessione producono eventi strutturati tramite auditAccessi; i token e i segreti vengono redatti dal Logger.`),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'requisiti',\n group: 'Autorizzazione',\n title: 'Requisiti e livelli di abilitazione',\n summary: 'DSL accessiRequirement: permission, and, or, not, custom.',\n blocks: [\n p(`I requisiti si compongono con accessiRequirement. La forma storica requirements è un AND di permessi. Il livello è TipoAbilitazione: NESSUNA=0, LETTURA=10, SCRITTURA=20, SPECIAL=30; il confronto è tipoAbilitazione >= soglia.`),\n good('ts', [\n \"import { authorizeAccessi, accessiRequirement as r } from 'emilsoftware-utilities';\",\n '',\n '// AND (entrambi i menu)',\n 'const both = r.and(r.permission(\"ORDINI\", 20), r.permission(\"CLIENTI\", 10));',\n '',\n '// OR',\n 'const either = r.or(r.permission(\"REPORT\", 10), r.permission(\"EXPORT\", 20));',\n '',\n '// NOT + custom',\n 'const policy = r.and(',\n ' r.permission(\"AREA_RISERVATA\", 10),',\n ' r.not(r.permission(\"BLOCCO_TOTALE\", 0)),',\n ');',\n '',\n 'app.get(\"/api/riservato\", (req, res, next) => authorizeAccessi(req, res, next, {',\n ' requirementTree: policy,',\n ' customRequirementHandlers: {',\n ' stessoReparto: async (ctx) => ctx.userCode === Number(ctx.req.query.reparto),',\n ' },',\n '}), handler);',\n ].join('\\n'), 'Policy composte'),\n bad('ts', [\n '// Sbagliato: AND/OR senza figli -> errore di configurazione (500 AUTH_REQUIREMENTS_MISCONFIGURED)',\n 'const empty = r.and();',\n ].join('\\n'), 'Configurazione non valida'),\n note('warn', `custom senza handler`, `Un requisito custom la cui chiave non ha un handler produce 500 AUTH_REQUIREMENTS_MISCONFIGURED: è un errore di programmazione, non un 403.`),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'utenti',\n group: 'Dominio',\n title: 'Utenti',\n summary: 'Creazione, aggiornamento, stati e GDPR.',\n blocks: [\n h(`Stati di registrazione (STAREG)`),\n table(\n ['Valore', 'Nome', 'Effetto sul login'],\n [\n ['0', 'NULL', 'Non confermato'],\n ['5', 'INSERT', 'Non confermato'],\n ['10', 'INVIO', 'Richiede il rinnovo della password'],\n ['20', 'CONF', 'Valido'],\n ['50', 'DELETE', 'Bloccato (soft delete)'],\n ['99', 'BLOCC', 'Bloccato'],\n ],\n ),\n h(`Endpoint principali (superutente salvo diversamente indicato)`),\n table(\n ['Metodo', 'Percorso', 'Descrizione'],\n [\n ['GET', '/api/accessi/user/get-users', 'Elenco con paginazione (limit e offset, header X-Total-Count).'],\n ['POST', '/api/accessi/user/create-managed-user', 'Crea un utente locale e invia il reset password.'],\n ['PUT', '/api/accessi/user/update-user/:codiceUtente', 'Aggiorna profilo e policy (campi privilegiati solo superutente).'],\n ['DELETE', '/api/accessi/user/delete-user/:codiceUtente', 'Soft delete: STAREG=DELETE. Admin o superutente.'],\n ['DELETE', '/api/accessi/user/delete-user-permanent/:codiceUtente', 'Eliminazione definitiva: rimuove utente e record collegati. Solo admin.'],\n ['POST', '/api/accessi/user/set-password/:codiceUtente', 'Imposta direttamente la password: hash lato backend e scadenza secondo policy, nessuna email.'],\n ['POST', '/api/accessi/user/set-stato', 'Imposta lo stato di registrazione.'],\n ['PATCH', '/api/accessi/user/set-gdpr/:codiceUtente', 'Registra il consenso GDPR.'],\n ['POST', '/api/accessi/user/register', 'Registrazione pubblica, solo se publicRegistration.enabled.'],\n ],\n ),\n note('warn', `Eliminazione: soft o definitiva`, `Il soft delete (STAREG=DELETE) è disponibile per admin e superutenti ed è reversibile. L'eliminazione definitiva rimuove utente e record collegati in transazione ed è riservata agli admin; il superutente senza flag Admin può solo flaggare l'utente come eliminato. Se l'utente ha dati collegati in altre tabelle (per esempio campi estesi del backend) l'eliminazione definitiva fallisce con code ACCESSI_USER_DELETE_HAS_REFERENCES e messaggio esplicito: usa allora il soft delete.`),\n note('ok', `Creazione con invito atomica`, `La creazione utente con invio email è atomica: se SMTP non è configurato si fallisce prima di scrivere, e se l'invio fallisce l'utente creato viene annullato (codice ACCESSI_USER_INVITE_EMAIL_FAILED). Non restano utenti senza password. In alternativa un admin crea l'utente e imposta la password dal form dedicato.`),\n note('ok', `Admin: controllo completo del profilo`, `Dalla scheda utente un admin modifica tutti i campi Accessi: anagrafica, cellulare, lingua, avatar, pagina di default, metadata JSON, 2FA/passwordless/login con password, flag, GDPR, stato registrazione e scadenza password. La password si imposta direttamente con set-password. Le scritture sono idempotenti (UPDATE OR INSERT su UTENTI_CONFIG) e verificate dal backend; non serve accedere al database.`),\n note('info', `Consenso GDPR`, `Il pulsante \"Registra consenso GDPR\" completa il processo: imposta FLGGDPR e DATGDPR e scrive la riga storica in UTENTI_GDPR (endpoint set-gdpr). Anche impostare flagGdpr via update-user completa il processo con la stessa riga storica.`),\n good('ts', [\n \"await fetch('/api/accessi/user/create-managed-user', {\",\n \" method: 'POST',\",\n \" headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + adminToken },\",\n ' body: JSON.stringify({',\n \" email: 'nuovo@example.com',\",\n \" nome: 'Mario',\",\n \" cognome: 'Rossi',\",\n ' roles: [1, 2],',\n \" permissions: [{ codiceMenu: 'REPORT', tipoAbilitazione: 10 }],\",\n ' }),',\n '});',\n ].join('\\n'), 'Creazione di un utente gestito'),\n bad('ts', [\n '// Sbagliato: inviare flag privilegiati dal frontend pubblico',\n 'fetch(\"/api/accessi/user/register\", {',\n ' method: \"POST\",',\n ' body: JSON.stringify({ email, flagSuper: true }), // ignorato o negato',\n '});',\n ].join('\\n'), 'Privilegi dal client'),\n note('info', `Campi di dominio`, `UTENTI_CONFIG contiene i campi di dominio Accessi: FLGSUPER, FLGADMIN, FLG2FATT (2FA), FLGPWDLESS (passwordless), FLGPASSWORD (login locale), CODLINGUA, CELLULARE, PAGDEF (pagina di default) e JSON_METADATA. I flag/colonne applicativi (FLGMOP, FLGPIANA, FLGADDETTI, FLGOSPITI, FLGPIANARFID, FLGCONTA, FLGCUBI, FLGCICLPASS, FLGDIPENDENTI, FLGINVENTARI, CAUMOV, NUMMAC, RAGSOCCLI, ENABLEIA, CELLUTE e NOMECAMPO) sono stati rimossi: i backend devono gestirli in tabelle proprie. Il telefono canonico e UTENTI_CONFIG.CELLULARE.`),\n note('info', `Autorizzazioni`, `I gruppi/menu si abilitano tramite ABILITAZIONI, RUOLI_MNU e UTENTI_RUOLI. Il vecchio meccanismo NOMECAMPO (flag applicativo su UTENTI_CONFIG) e stato rimosso.`),\n note('info', `Campi estesi`, `extensionFieldsOptions allega colonne di tabelle applicative al profilo (per esempio la ragione sociale). Le colonne sono in whitelist: non costruirle mai da input HTTP.`),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'ruoli',\n group: 'Dominio',\n title: 'Ruoli e permessi',\n summary: 'Assegnazioni sostitutive e composizione dei grant.',\n blocks: [\n p(`I ruoli contengono una lista di menu con livello. Le assegnazioni (ruoli e permessi diretti) sono SOSTITUTIVE: invia sempre la lista completa desiderata. I grant effettivi sommano ruoli e permessi diretti prendendo il livello massimo per menu; un livello 0 non annulla un grant del ruolo. I superutenti ricevono tutti i menu attivi al livello massimo.`),\n table(\n ['Metodo', 'Percorso', 'Descrizione'],\n [\n ['GET', '/api/accessi/permission/roles', 'Ruoli con i menu associati.'],\n ['POST', '/api/accessi/permission/create-role', 'Crea un ruolo (i menu sono obbligatori).'],\n ['PUT', '/api/accessi/permission/update-role/:codiceRuolo', 'Aggiorna un ruolo (menu sostitutivi).'],\n ['DELETE', '/api/accessi/permission/delete-role/:codiceRuolo', 'Elimina ruolo, mapping e assegnazioni.'],\n ['POST', '/api/accessi/permission/assign-roles/:codiceUtente', `Sostituisce i ruoli dell'utente.`],\n ['POST', '/api/accessi/permission/assign-permissions/:codiceUtente', 'Sostituisce i permessi diretti.'],\n ['GET', '/api/accessi/permission/grants/:codiceUtente', 'Grant effettivi, permessi diretti e ruoli.'],\n ],\n ),\n good('ts', [\n 'const role = {',\n \" descrizioneRuolo: 'Gestore ordini',\",\n ' menu: [',\n \" { codiceMenu: 'ORDINI', tipoAbilitazione: 20 },\",\n \" { codiceMenu: 'CLIENTI', tipoAbilitazione: 10 },\",\n ' ],',\n '};',\n 'await createRole(role); // POST /api/accessi/permission/create-role',\n '',\n \"// Sostitutivo: questa lista diventa l'intero set di ruoli dell'utente\",\n 'await assignRolesToUser(codiceUtente, { roles: [3, 7] });',\n ].join('\\n'), 'Creazione di un ruolo e assegnazione'),\n bad('ts', [\n \"// Sbagliato: pensare che l'assegnazione sia incrementale\",\n 'await assignRolesToUser(codiceUtente, { roles: [3] });',\n \"// ora l'utente ha SOLO il ruolo 3, non 3 più quelli di prima\",\n ].join('\\n'), 'Assegnazione non additiva'),\n note('warn', `Menu obbligatori`, `create-role e update-role rifiutano un ruolo senza menu. Se ti serve un ruolo senza accessi, non crearlo.`),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'menu',\n group: 'Dominio',\n title: 'Menu, gruppi e tipi menu',\n summary: 'Catalogo di navigazione e CRUD completo (superutente).',\n blocks: [\n table(\n ['Metodo', 'Percorso', 'Descrizione'],\n [\n ['GET', '/api/accessi/permission/menus', 'Menu attivi.'],\n ['GET', '/api/accessi/permission/menu-types', 'Catalogo dei tipi menu.'],\n ['GET', '/api/accessi/permission/groups-with-menus?includeDisabled=true', 'Gruppi con menu (anche disabilitati).'],\n ['POST', '/api/accessi/permission/menus', 'Crea un menu.'],\n ['PUT', '/api/accessi/permission/menus/:codiceMenu', 'Aggiorna un menu.'],\n ['DELETE', '/api/accessi/permission/menus/:codiceMenu', 'Elimina un menu (con grant e associazioni ai ruoli).'],\n ['POST', '/api/accessi/permission/menu-groups', 'Crea un gruppo.'],\n ['PUT', '/api/accessi/permission/menu-groups/:codiceGruppo', 'Aggiorna un gruppo.'],\n ['DELETE', '/api/accessi/permission/menu-groups/:codiceGruppo', 'Elimina un gruppo (solo se vuoto).'],\n ['POST/PUT/DELETE', '/api/accessi/permission/menu-types[/:codiceTipo]', 'CRUD dei tipi menu.'],\n ],\n ),\n p(`I codici (CODMNU, CODGRP, CODTIP) sono chiavi immutabili: si indicano in creazione e poi si passano nel percorso. Un gruppo o un tipo si elimina solo se non è referenziato.`),\n good('ts', [\n \"await createMenuGroup({ codiceGruppo: 'A', descrizioneGruppo: 'Amministrazione', ordineGruppo: 1, enabled: true });\",\n \"await createMenuType({ codiceTipo: 'X', descrizioneTipo: 'Operativo' });\",\n \"await createMenu({ codiceMenu: 'ORDINI', descrizioneMenu: 'Ordini', codiceGruppo: 'A', tipo: 'X', ordineMenu: 10, enabled: true });\",\n ].join('\\n'), 'Catalogo in ordine'),\n bad('ts', [\n '// Sbagliato: eliminare un gruppo che contiene menu -> 400 con spiegazione',\n \"await deleteMenuGroup('A'); // contiene N menu\",\n ].join('\\n'), 'Vincoli di integrità'),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'sso',\n group: 'SSO',\n title: 'SSO generico (federated authentication)',\n summary: 'Il backend valida il provider, Accessi gestisce identità e sessioni.',\n blocks: [\n p(`Accessi non valida i token Azure, Google o SAML: è il TUO backend a validare il flusso SSO e a passare solo { provider, subject } verificati a FederatedAuthService. Così issuer, secret e token restano fuori dal modulo.`),\n good('ts', [\n \"import { FederatedAuthService } from 'emilsoftware-utilities';\",\n '',\n '// Nel tuo callback SSO, dopo aver validato il token del provider:',\n 'const verified = { provider: \"azure-ad-acme\", subject: claims.oid };',\n 'const result = await federatedAuthService.authenticate(verified);',\n '',\n 'if (result.login.challenge) {',\n ' // 2FA attiva: restituisci il challenge al frontend',\n ' return res.json({ challenge: result.login.challenge });',\n '}',\n 'return res.json({ login: result.login, token: result.token });',\n ].join('\\n'), 'Login SSO'),\n note('info', `Scambio service-to-service`, `Se il backend fratello valida il provider SSO, può scambiare { provider, subject } con POST /api/accessi/federated-auth/exchange usando un service token con scope accessi:federated:exchange. Accessi non riceve il token del provider.`),\n h(`API amministrative (superutente)`),\n list([\n `GET/POST /api/accessi/federated-auth/providers, PATCH/DELETE providers/:provider`,\n `GET/POST /api/accessi/federated-auth/users/:codiceUtente/identities`,\n `POST /api/accessi/federated-auth/users (crea un utente SSO-only)`,\n `PATCH /api/accessi/federated-auth/users/:codiceUtente/password-login`,\n ]),\n bad('ts', [\n \"// Sbagliato: far validare il token del provider ad Accessi o fidarsi del frontend\",\n 'await federatedAuthService.authenticate({ provider, subject: req.body.email }); // subject non verificato',\n ].join('\\n'), `Non fidarti dell'input`),\n note('warn', `Provider in uso`, `Un provider si elimina solo se nessuna identità è collegata; altrimenti disabilitalo. La chiave del provider deve coincidere con la configurazione del backend.`),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'service-token',\n group: 'Token di servizio',\n title: 'Token macchina-a-macchina',\n summary: 'Autenticazione tecnica con scope: non è un superutente.',\n blocks: [\n p(`Formato: st_<id>.<segreto> (segreto da 256 bit). Nel database si salva solo lo SHA-256 del segreto; il segreto è mostrato una sola volta. La verifica restituisce { tokenId, label, scopes }: nessun ruolo o grant.`),\n note('danger', `Non è un superutente`, `Un service token non apre le API Accessi: abilita solo le rotte della tua app che proteggi con ServiceTokenGuard.`),\n note('info', `Introspection`, `Per un backend fratello creare un service token con scope accessi:introspect e usarlo in POST /api/accessi/auth/introspect. Il resource server lo configura in introspection.serviceToken.`),\n note('warn', `Rate limit condiviso`, `In cluster configurare serviceTokens.rateLimitStore e publicAuthRateLimit.store con createRedisRateLimitStore: i contatori in memoria valgono solo per singola istanza.`),\n note('info', `Edge rate limit`, `Per una soglia grossolana per IP configurare edgeRateLimit: viene applicata davanti alle rotte Accessi. Il gateway/API gateway resta comunque consigliato per bot protection e limiti di connessione.`),\n good('ts', [\n \"import { ServiceTokenGuard, RequireServiceTokenScopes } from 'emilsoftware-utilities';\",\n '',\n '@UseGuards(ServiceTokenGuard)',\n \"@RequireServiceTokenScopes('ia')\",\n '@Post(\"internal/ask\")',\n 'ask() { /* req.accessiServiceToken = { tokenId, label, scopes } */ }',\n ].join('\\n'), 'Rotta tecnica protetta'),\n h(`Gestione (superutente)`),\n list([\n `POST /api/accessi/service-token (label, scopes, expiresAt o ttlDays) -> segreto mostrato una volta`,\n `GET /api/accessi/service-token?includeRevoked=true`,\n `DELETE /api/accessi/service-token/:tokenId (revoca immediata)`,\n `POST /api/accessi/service-token/:tokenId/rotate`,\n ]),\n bad('ts', [\n '// Sbagliato: usare un service token per autenticare un utente umano',\n \"fetch('/api/profilo', { headers: { Authorization: 'Bearer ' + serviceToken } }); // req.user assente\",\n ].join('\\n'), 'Non è un utente'),\n note('warn', `Scadenza`, `Senza expiresAt né defaultTtlDays il token non scade: imposta serviceTokens.defaultTtlDays o una scadenza esplicita.`),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'console',\n group: 'Console',\n title: 'Console amministrativa',\n summary: 'Accesso, sezioni e che cosa si può fare.',\n blocks: [\n p(`La console è servita dal modulo su /api/accessi/console. Non ha stato proprio: ogni azione chiama le API protette con il JWT dell'utente connesso.`),\n list([\n `Utenti: elenco, creazione locale o SSO, profilo, ruoli, grant diretti, stato, 2FA e Login as.`,\n `Ruoli e grant: CRUD dei ruoli con l'albero delle abilitazioni.`,\n `Menu e gruppi: CRUD di gruppi, menu e tipi menu.`,\n `Token di servizio: creazione, rotazione, revoca.`,\n `SSO: provider e identità.`,\n `Wiki e Changelog: documentazione e novità della libreria.`,\n `SQL: esporta il DDL delle entità Accessi (tabelle, generatori, FK, indici, check, trigger) per replicare il database.`,\n ]),\n note('info', `Superutente (FLGSUPER)`, `Riceve TUTTE le abilitazioni al livello massimo (30) dopo il login. In console vede solo la sezione Utenti e gestisce utenti, ruoli e grant.`),\n note('info', `Admin (FLGADMIN)`, `Accede a tutta la console (catalogo, token, SSO, utenti) ma NON conferisce abilitazioni: i menu restano quelli assegnati via ABILITAZIONI/RUOLI_MNU. Gerarchia: ADMIN > SUPER > utente.`),\n note('info', `Gerarchia dei ruoli`, `Vale ADMIN (2) > SUPER (1) > utente comune (0). Un attore può gestire solo utenti di rango pari o inferiore: un superutente può amministrare totalmente utenti comuni e superutenti (profilo, password, flag, ruoli, grant, stato, eliminazione e identità/utenti SSO) ma NON può toccare un admin. Un admin può gestire chiunque. La regola è imposta dal backend (ensureCanManageTargetUser): un tentativo non consentito restituisce 403.`),\n note('info', `Gestione dei flag`, `Dalla scheda utente i flag sono mostrati in base al ruolo di chi è connesso: un admin può assegnare o togliere sia il flag Admin sia il flag Superutente; un superutente senza flag Admin può gestire solo il flag Superutente. I flag si impostano anche in creazione (utente locale o SSO).`),\n note('warn', `Ultimo admin`, `Per non restare chiusi fuori dalla console, l'ultimo admin attivo non può essere disattivato, bloccato, eliminato né privato del flag Admin: l'operazione restituisce 400 finché non viene promosso un altro admin.`),\n note('info', `Accesso`, `Apri /api/accessi/console e accedi con un utente con FLGSUPER o FLGADMIN. Le sezioni sono raggiungibili anche tramite deep link (per esempio /api/accessi/console/menu-e-gruppi/menu).`),\n note('info', `Bootstrap admin`, `Con adminBootstrap.enabled il backend stampa a ogni avvio un token casuale (monouso, valido fino al riavvio). La sezione \"Crea utente admin\" nella schermata di login compare solo quando adminBootstrap è abilitato (stato esposto da GET /api/accessi/auth/bootstrap-admin/status): incolla il token e crea il primo admin senza email di conferma. La password è opzionale: se assente viene generata e mostrata una sola volta.`),\n note('ok', `Auto-ingresso dal frontend`, `Se un frontend ha già una sessione Accessi valida e l'utente è abilitato alla console, può aprire la console senza rifare il login: \\`POST /api/accessi/console/entry\\` con \\`Authorization: Bearer <jwt>\\` restituisce un ticket monouso; apri poi \\`/api/accessi/console#entry=<ticket>\\`. Il JWT non finisce mai nell'URL; il ticket dura 60 secondi ed è utilizzabile una sola volta (endpoint di scambio protetto da rate limit).`),\n note('info', `Console centralizzata`, `In una topologia multi-backend la console va ospitata una sola volta su Accessi Service. Gli altri backend possono linkarla con \\`buildAccessiConsoleUrl\\` di \\`accessi/console\\` oppure proxare \\`/api/accessi/*\\` sullo stesso dominio. Non copiare gli asset della console in ogni applicazione.`),\n note('warn', `Ticket multi-istanza`, `I ticket di ingresso console sono in memoria per default. Con piu istanze configurare \\`consoleEntry.store\\` con \\`createRedisConsoleEntryStore\\`: il JWT viene cifrato e il consume e atomico.`),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'login-as',\n group: 'Console',\n title: 'Login as (accesso temporaneo)',\n summary: 'Superutenti e admin possono generare un URL per entrare in un applicazione come un altro utente.',\n blocks: [\n p(`Dalla lista utenti della console il pulsante Login as apre il pannello dedicato: inserisci l'URL dell'applicazione e premi \"Genera URL di accesso\". Accessi emette un JWT per l'utente target e lo allega all'URL come query parameter \\`loginAsToken\\`.`),\n code('http', `POST /api/accessi/user/login-as/:codiceUtente\\nContent-Type: application/json\\nAuthorization: Bearer <jwt amministratore>\\n\\n{ \"url\": \"https://app.example.com/dashboard\" }`, 'Richiesta'),\n code('json', `{\\n \"Result\": {\\n \"loginUrl\": \"https://app.example.com/dashboard?loginAsToken=eyJ...\",\\n \"token\": \"eyJ...\",\\n \"expiresInSeconds\": 86400,\\n \"expiresAt\": \"2026-10-24T14:05:00.000Z\"\\n }\\n}`, 'Risposta'),\n note('danger', `La durata è quella standard`, `Il token dura quanto la sessione Accessi standard (jwtOptions.expiresIn). È una credenziale: chiunque abbia l'URL può usarlo fino alla scadenza per entrare nell'applicazione come l'utente target. Non condividerlo, non salvarlo in log o documenti e non usarlo in produzione per operazioni distruttive.`),\n note('info', `Chi può generarlo`, `Solo utenti con FLGSUPER o FLGADMIN. Vale la stessa gerarchia della gestione utenti: un superutente non può impersonare un admin; un admin può impersonare chiunque. Il tentativo non consentito restituisce 403.`),\n note('warn', `Responsabilità dell applicazione`, `Accessi non controlla l'URL di destinazione né consuma il token al posto dell'applicazione. L'app deve leggere \\`loginAsToken\\`, validarlo (per esempio con \\`POST /api/accessi/auth/get-user-by-token\\` o \\`authorizeAccessi\\`) e decidere cosa consentire. Il token è un access token Accessi a tutti gli effetti, con claim \\`loginAs: true\\`, \\`amr: [\"login_as\"]\\` e \\`act\\` valorizzato con l'attore.`),\n p(`Il token viene emesso anche per utenti con 2FA attiva: l'attore ha già completato l'autenticazione amministrativa. Non usare questa funzione per aggirare la 2FA di un utente comune al di fuori di un intervento amministrativo autorizzato.`),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'errori',\n group: 'Riferimenti',\n title: 'Contratto errori e codici',\n summary: 'Formato della risposta e codici stabili ACCESSI_* e AUTH_*.',\n blocks: [\n p(`Gli errori HTTP delle API Accessi usano un contratto stabile: severity, status, statusCode, code, error (campo legacy uguale a code), message e details per gli errori di validazione.`),\n code('json', `{\\n \"severity\": \"error\",\\n \"status\": 400,\\n \"statusCode\": 2,\\n \"code\": \"ACCESSI_VALIDATION_ERROR\",\\n \"error\": \"ACCESSI_VALIDATION_ERROR\",\\n \"message\": \"La richiesta contiene dati non validi.\",\\n \"details\": [\"email: deve essere una stringa.\"]\\n}`, 'Errore di validazione'),\n table(\n ['Codice', 'Quando'],\n [\n ['ACCESSI_VALIDATION_ERROR', 'Body o parametri non validi (400).'],\n ['ACCESSI_AUTH_INITIALIZING', 'Bootstrap in corso (503).'],\n ['ACCESSI_AUTH_NOT_INITIALIZED', 'Modulo non inizializzato (500).'],\n ['AUTH_HEADER_MISSING / AUTH_TOKEN_MISSING', 'Header Authorization assente (401).'],\n ['AUTH_TOKEN_INVALID', 'JWT non valido o scaduto (401).'],\n ['AUTH_USER_DISABLED', 'Utente non più autorizzato (401).'],\n ['AUTH_INSUFFICIENT_PERMISSIONS', 'Requisiti non soddisfatti (403).'],\n ['AUTH_REQUIREMENTS_MISCONFIGURED', 'Policy malformata o handler custom mancante (500).'],\n ['ACCESSI_DATABASE_SCHEMA_OUTDATED', 'Schema del database incompatibile.'],\n ['ACCESSI_SERVICE_TOKEN_RATE_LIMITED', 'Troppi tentativi (429).'],\n ],\n ),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'endpoint',\n group: 'Riferimenti',\n title: 'Indice degli endpoint',\n summary: 'Tutte le rotte /api/accessi/* in una tabella.',\n blocks: [\n table(\n ['Area', 'Endpoint principali'],\n [\n ['Auth', 'POST auth/login, auth/get-user-by-token, auth/confirm-reset-password/:token, auth/refresh, auth/revoke-sessions, auth/revoke-token, auth/two-factor/verify, auth/two-factor/resend'],\n ['Email', 'POST email/send-reset-password-email, GET email/reset-password-page/:token'],\n ['Utenti', 'GET user/get-users, POST user/create-managed-user, PUT user/update-user/:codiceUtente, DELETE user/delete-user/:codiceUtente, DELETE user/delete-user-permanent/:codiceUtente, POST user/set-password/:codiceUtente, POST user/set-stato, PATCH user/set-gdpr/:codiceUtente, POST user/register, POST user/login-as/:codiceUtente'],\n ['Permessi', 'GET permission/roles, POST permission/create-role, PUT permission/update-role/:codiceRuolo, DELETE permission/delete-role/:codiceRuolo, POST permission/assign-roles/:codiceUtente, POST permission/assign-permissions/:codiceUtente, GET permission/grants/:codiceUtente, GET permission/menus, GET permission/menu-types, GET permission/groups-with-menus'],\n ['Catalogo', 'POST/PUT/DELETE permission/menus[/:codiceMenu], permission/menu-groups[/:codiceGruppo], permission/menu-types[/:codiceTipo]'],\n ['Configurator', 'PATCH configurator/menus/:codiceMenu/enabled, PATCH configurator/groups/:codiceGruppo/enabled'],\n ['SSO', 'GET/POST federated-auth/providers, PATCH/DELETE providers/:provider, GET/POST users/:codiceUtente/identities, POST users, PATCH users/:codiceUtente/password-login'],\n ['Token', 'POST/GET service-token, DELETE service-token/:tokenId, POST service-token/:tokenId/rotate'],\n ['Schema', 'GET schema (DDL completo + entità presenti, solo admin)'],\n ['Console', 'GET console, console/:view, console/:view/:sub, console/assets/:file'],\n ],\n ),\n ],\n },\n // -------------------------------------------------------------------------\n {\n id: 'antipattern',\n group: 'Riferimenti',\n title: 'Antipattern e checklist',\n summary: 'Che cosa non fare, in breve.',\n blocks: [\n list([\n `Non aprire la porta HTTP prima di initializeAccessiModule.`,\n `Non usare authorizeAccessi come unico strato: valida sempre anche l'input applicativo.`,\n `Non dare per scontato che req.userGrants esista se non hai richiesto dei requisiti.`,\n `Non considerare incrementali assignRolesToUser e assignPermissionsToUser: sono sostitutivi.`,\n `Non far validare i token SSO ad Accessi: passagli solo { provider, subject } verificati.`,\n `Non usare un service token come identità utente.`,\n `Non versionare segreti; jwtOptions.secret deve essere unico per ambiente.`,\n `Non eliminare gruppi o tipi in uso: gestisci l'errore 400 e disabilita invece.`,\n `Non usare il challenge 2FA come JWT.`,\n ]),\n note('ok', `Checklist di rilascio`, `Schema verificato (db:check), segreti da un secret manager, mockDemoUser=false, rate limit pubblico configurato, SMTP reale testato, serviceTokens.defaultTtlDays impostato.`),\n ],\n },\n];\n\n/** Elenco ordinato dei gruppi per l'indice. */\nexport function wikiGroups(): Array<{ group: string; sections: WikiSection[] }> {\n const groups: Array<{ group: string; sections: WikiSection[] }> = [];\n for (const section of WIKI_SECTIONS) {\n let bucket = groups.find((item) => item.group === section.group);\n if (!bucket) {\n bucket = { group: section.group, sections: [] };\n groups.push(bucket);\n }\n bucket.sections.push(section);\n }\n return groups;\n}\n\n/**\n * Digest compatto in Markdown per l'IA che costruisce il backend integratore.\n * Deriva dagli stessi dati della wiki, quindi resta sincronizzato.\n */\nexport function buildAiDigest(): string {\n const lines: string[] = [\n `# Accessi (emilsoftware-utilities) - guida compatta per l'integrazione backend`,\n '',\n `Modulo NestJS/Express + Firebird per autenticazione JWT, autorizzazione per menu, utenti, 2FA e passwordless, SSO generico, service token e console.`,\n '',\n ];\n\n for (const { group, sections } of wikiGroups()) {\n lines.push(`## ${group}`);\n for (const section of sections) {\n lines.push(`### ${section.title}`);\n lines.push(section.summary);\n for (const block of section.blocks) {\n switch (block.kind) {\n case 'p':\n lines.push(block.text);\n break;\n case 'h':\n lines.push(`**${block.text}**`);\n break;\n case 'list':\n block.items.forEach((item, index) => lines.push(`${block.ordered ? `${index + 1}.` : '-'} ${item}`));\n break;\n case 'code':\n lines.push('```' + block.language);\n lines.push(block.code);\n lines.push('```');\n break;\n case 'callout':\n lines.push(`[${block.tone.toUpperCase()}] ${block.title ? block.title + ': ' : ''}${block.text}`);\n break;\n case 'table':\n lines.push('| ' + block.head.join(' | ') + ' |');\n lines.push('| ' + block.head.map(() => '---').join(' | ') + ' |');\n block.rows.forEach((row) => lines.push('| ' + row.join(' | ') + ' |'));\n break;\n }\n }\n lines.push('');\n }\n }\n\n return lines.join('\\n').replace(/\\n{3,}/g, '\\n\\n').trim();\n}\n"]}
@@ -176,6 +176,18 @@ services:
176
176
  - "127.0.0.1:3201:3201"
177
177
  ```
178
178
 
179
+ ## Template pronti
180
+
181
+ In `deploy/` ci sono esempi copiabili:
182
+
183
+ - `deploy/systemd/accessi-tenant@.service` e `tenant.secrets.example`;
184
+ - `deploy/docker/docker-compose.yml`;
185
+ - `deploy/nginx/accessi-tenants.conf`;
186
+ - `deploy/traefik/dynamic.yml`.
187
+
188
+ La CI esegue `npm run check:deploy` per verificare presenza, coerenza e assenza
189
+ di segreti nei template.
190
+
179
191
  ## Reverse proxy
180
192
 
181
193
  Il proxy instrada per host o path e **sovrascrive** l'header tenant se lo usa.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "emilsoftware-utilities",
3
- "version": "2.0.0-dev.81",
3
+ "version": "2.0.0-dev.82",
4
4
  "description": "Utilities for EmilSoftware",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -139,9 +139,10 @@
139
139
  "prepublishOnly": "npm run build",
140
140
  "typecheck": "tsc --noEmit",
141
141
  "lint": "node scripts/lint.cjs",
142
+ "check:deploy": "node scripts/check-deploy-templates.cjs",
142
143
  "test": "npm run typecheck && npm run lint && npm run test:unit",
143
- "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 test/audit.cjs test/import-entrypoints.cjs test/tenants.cjs test/tenant-stores.cjs",
144
- "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 test/audit.cjs test/import-entrypoints.cjs test/tenants.cjs test/tenant-stores.cjs",
144
+ "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 test/audit.cjs test/import-entrypoints.cjs test/tenants.cjs test/tenant-stores.cjs test/deploy-templates.cjs",
145
+ "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 test/audit.cjs test/import-entrypoints.cjs test/tenants.cjs test/tenant-stores.cjs test/deploy-templates.cjs",
145
146
  "test:accessi": "npm run test:unit",
146
147
  "test:all": "npm test && npm run test:integration && npm run test:integration:redis",
147
148
  "test:integration": "node test/integration/run-firebird.cjs 5",
@@ -227,6 +228,7 @@
227
228
  },
228
229
  "files": [
229
230
  "dist",
231
+ "deploy",
230
232
  "integration-prompt.md",
231
233
  "docs"
232
234
  ],