diff --git a/.env.example b/.env.example
index 37a4b02..8006cdd 100644
--- a/.env.example
+++ b/.env.example
@@ -48,13 +48,13 @@ LOCALSTACK_AWS_SESSION_TOKEN=
# -----------------------------------------------------------------------------
-# Frontend SPA statico locale (S3 LocalStack + CloudFront locale)
+# Frontend SPA statico locale (S3 LocalStack + CDN/edge locale)
# -----------------------------------------------------------------------------
# Queste variabili riguardano SOLO il serving della SPA Angular.
# Non usarle per i documenti e non sostituirle con bucket S3 reali.
# I documenti restano governati da POC_DOCUMENT_DISK/AWS_REAL_* più sotto.
FRONTEND_STATIC_BUCKET=poc-frontend-static-local
-FRONTEND_CLOUDFRONT_LOCAL_URL=https://localhost:8443
+EDGE_CDN_LOCAL_URL=https://localhost:8443
# -----------------------------------------------------------------------------
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7b5ec63..97f0541 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -256,7 +256,7 @@ jobs:
- name: Apply LocalStack infrastructure
run: docker compose --profile tools run --rm terraform init && docker compose --profile tools run --rm terraform apply -auto-approve
- # Il percorso di serving default e' Traefik -> frontend-cloudfront -> S3
+ # Il percorso di serving default e' Traefik -> edge-cdn -> S3
# LocalStack: lo smoke e gli audit a11y devono colpire la SPA realmente
# caricata nel bucket, non un bucket vuoto. Si riusa la stessa logica di
# make setup (build + sync con cache-control differenziato).
@@ -310,7 +310,7 @@ jobs:
echo "$spa_headers" | grep -i '^x-content-type-options: nosniff'
echo "$spa_headers" | grep -i '^referrer-policy: strict-origin-when-cross-origin'
- # La SPA Angular deve essere servita da S3 LocalStack via frontend-cloudfront,
+ # La SPA Angular deve essere servita da S3 LocalStack via edge-cdn,
# sia sulla root sia su un deep link (fallback SPA verso index.html)
curl --silent --fail --insecure https://localhost:8443/ | grep -q 'TLS + routing per hostname]
- FC[frontend-cloudfront CloudFront locale: SPA da S3 + proxy /api]
+ FC[edge-cdn emulatore CDN locale: SPA da S3 + proxy /api]
N[Nginx proxy /api + fastcgi]
end
subgraph backend[rete backend]
@@ -100,7 +100,7 @@ flowchart LR
AL -->|docker logs| LK
```
-Confini di responsabilità: Traefik termina TLS e applica auth alle dashboard; il CloudFront locale (`frontend-cloudfront`) serve la SPA da S3 LocalStack e inoltra `/api/`, `/health` e `/ready` a Nginx, che resta il proxy applicativo verso PHP-FPM (oltre a poter servire la SPA in modalità standard dall'immagine); Laravel gestisce validazione, identità, persistenza e orchestrazione; Step Functions (LocalStack) detiene lo stato del workflow; il worker esegue i task e risponde con i task token; il collector è l'unico punto di raccolta telemetria.
+Confini di responsabilità: Traefik termina TLS e applica auth alle dashboard; l'emulatore CDN locale (`edge-cdn`, un secondo Nginx) serve la SPA da S3 LocalStack e inoltra `/api/`, `/health` e `/ready` all'Nginx applicativo, che resta il proxy verso PHP-FPM (oltre a poter servire la SPA in modalità standard dall'immagine); Laravel gestisce validazione, identità, persistenza e orchestrazione; Step Functions (LocalStack) detiene lo stato del workflow; il worker esegue i task e risponde con i task token; il collector è l'unico punto di raccolta telemetria.
---
@@ -152,16 +152,16 @@ Confini di responsabilità: Traefik termina TLS e applica auth alle dashboard; i
### Nginx 1.27 (static + fastcgi)
**Dove**: `docker/nginx/Dockerfile` (multi-stage: build SPA con node:22 → runtime `nginx:1.27-alpine`, `USER nginx`), `docker/nginx/default.conf`.
-**Ruolo**: nel flusso default è il proxy applicativo — inoltra `/api/` e gli endpoint di sistema a PHP-FPM e applica i security header (X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Content-Security-Policy). L'immagine include anche la SPA Angular buildata e può servirla in modalità standard con fallback `try_files ... /index.html`, percorso alternativo al CloudFront locale (`frontend-cloudfront`), che invece è l'origine default della SPA da S3 LocalStack.
+**Ruolo**: nel flusso default è il proxy applicativo — inoltra `/api/` e gli endpoint di sistema a PHP-FPM e applica i security header (X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Content-Security-Policy). L'immagine include anche la SPA Angular buildata e può servirla in modalità standard con fallback `try_files ... /index.html`, percorso alternativo all'emulatore CDN locale (`edge-cdn`), che invece è l'origine default della SPA da S3 LocalStack.
**Dettaglio rilevante**: `/internal/metrics` non è servito dal listener edge `:8080` usato da Traefik; ritorna 404 dall'esterno. Lo scrape passa dal listener interno `nginx:8081/internal/metrics`, raggiungibile solo sulle reti Docker interne. La CI verifica sia il 404 esterno sia la raggiungibilità interna.
**Gap**: la CSP è locale e mirata alla SPA attuale; eventuali nuovi asset remoti o embedding esterni richiedono aggiornamento esplicito della policy.
-### CloudFront locale (frontend-cloudfront)
+### Emulatore CDN locale (edge-cdn)
-**Dove**: `docker/cloudfront/default.conf.template` (config Nginx parametrizzata via `envsubst`), servizio `frontend-cloudfront` in `docker-compose.yml` (immagine `nginx:1.29-alpine`), target `make frontend-s3-local-deploy`/`frontend-s3-local-upload`.
-**Ruolo**: emula in locale il layer CDN/edge che in produzione sarebbe Amazon CloudFront. È l'**origine di default della SPA**: serve gli asset statici Angular leggendoli da S3 LocalStack (`/` → bucket `index.html`; le altre richieste sono proxate al bucket con fallback deep-link `@spa_index` su `index.html`, intercettando 403/404), e fa da **reverse proxy** verso Nginx per `/api/`, `/health`, `/ready`. È il primo hop dopo Traefik (`Traefik → frontend-cloudfront → {S3 LocalStack | Nginx}`).
+**Dove**: `docker/edge-cdn/default.conf.template` (config Nginx parametrizzata via `envsubst`), servizio `edge-cdn` in `docker-compose.yml` (immagine `nginx:1.29-alpine`), target `make frontend-s3-local-deploy`/`frontend-s3-local-upload`.
+**Ruolo**: emula in locale il layer CDN/edge che in produzione sarebbe Amazon CloudFront. È l'**origine di default della SPA**: serve gli asset statici Angular leggendoli da S3 LocalStack (`/` → bucket `index.html`; le altre richieste sono proxate al bucket con fallback deep-link `@spa_index` su `index.html`, intercettando 403/404), e fa da **reverse proxy** verso Nginx per `/api/`, `/health`, `/ready`. È il primo hop dopo Traefik (`Traefik → edge-cdn → {S3 LocalStack | Nginx}`).
**Security header**: applica a livello server gli header edge (CSP `frame-ancestors 'none'`, X-Frame-Options `SAMEORIGIN`, X-Content-Type-Options, Referrer-Policy) sugli asset statici. Sulle risposte `/api/` non sovrascrive gli header dell'origine: un `add_header` dedicato nella `location /api/` interrompe l'ereditarietà nginx, così resta valida la CSP differenziata dell'app (in particolare `frame-ancestors 'self'` per l'anteprima PDF in iframe, che altrimenti verrebbe bloccata dalla doppia CSP).
-**Motivazione**: validare il percorso reale build → upload su object storage → distribuzione edge senza dipendere da CloudFront vero; tiene il serving statico della SPA disaccoppiato dall'immagine applicativa.
+**Motivazione**: validare il percorso reale build → upload su object storage → distribuzione edge senza dipendere da CloudFront vero; tiene il serving statico della SPA (e ogni riferimento a LocalStack) fuori dall'immagine Nginx applicativa, che è un artefatto di produzione. In produzione il ruolo sarebbe ricoperto da AWS CloudFront.
**Valutazione**: riproduce in modo fedele il flusso S3-origin + edge proxy e la separazione asset/API. Gap: non copre OAC, invalidation della cache, signed URL né la propagazione edge reali (limiti dichiarati, §11); convive con la versione 1.27 dell'Nginx applicativo (due immagini Nginx distinte con ruoli diversi).
### AWS Bedrock (LLM)
@@ -222,7 +222,7 @@ Confini di responsabilità: Traefik termina TLS e applica auth alle dashboard; i
### CI — GitHub Actions
**Dove**: `.github/workflows/ci.yml` (3 job), `mirror-images.yml`, `scripts/ci/`.
-**Pipeline**: `backend` (Composer validate, Pint, Larastan, Pest), `frontend` (lint OpenAPI, generazione client + check committed, ESLint, typecheck, Jest, build Angular, `npm audit --audit-level=high`), `stack` (mirroring/caching immagini, `terraform fmt/validate`, validazione config osservabilità, build immagini dev+prod, **Trivy** `--exit-code 1 --severity HIGH,CRITICAL` su app e nginx, avvio stack completo con LocalStack+Terraform apply, build e upload della SPA Angular su S3 LocalStack, smoke HTTPS via Traefik — SPA servita dal CloudFront locale con fallback deep-link, check 401 sulle dashboard — audit axe e pa11y contro lo stack reale, push condizionale delle immagini su GHCR solo per i non-PR). Concurrency con cancel-in-progress.
+**Pipeline**: `backend` (Composer validate, Pint, Larastan, Pest), `frontend` (lint OpenAPI, generazione client + check committed, ESLint, typecheck, Jest, build Angular, `npm audit --audit-level=high`), `stack` (mirroring/caching immagini, `terraform fmt/validate`, validazione config osservabilità, build immagini dev+prod, **Trivy** `--exit-code 1 --severity HIGH,CRITICAL` su app e nginx, avvio stack completo con LocalStack+Terraform apply, build e upload della SPA Angular su S3 LocalStack, smoke HTTPS via Traefik — SPA servita dall'emulatore CDN locale con fallback deep-link, check 401 sulle dashboard — audit axe e pa11y contro lo stack reale, push condizionale delle immagini su GHCR solo per i non-PR). Concurrency con cancel-in-progress.
**Valutazione**: copertura larga e realistica (lo smoke testa il sistema integrato, non i mock); il quality gate include sicurezza (Trivy, npm audit) e accessibilità. Gap: niente coverage report; i job non pubblicano artefatti di debug in caso di fallimento smoke.
---
@@ -368,7 +368,7 @@ Coperte in §5 (Bedrock, Textract) e §6. Punti trasversali:
Coperto in §5; valutazione sintetica:
- **Solido**: data layer uniforme (servizi Angular + client generato), feedback espliciti (loading con `aria-live`, error, empty), SSE per progress reale dell'elaborazione (`DocumentWorkflowService` con EventSource), design token centralizzati con dark mode, test Jest mirati e audit a11y automatizzati in CI.
-- **Limiti**: il CloudFront locale valida il flusso build → S3 locale → distribuzione CDN-like, ma non copre OAC/invalidation/propagazione edge reali; la copertura test frontend resta minima e va ampliata sui componenti visuali e sui flussi SSE end-to-end.
+- **Limiti**: l'emulatore CDN locale (Nginx) valida il flusso build → S3 locale → distribuzione edge, ma non copre OAC/invalidation/propagazione edge reali (in produzione: AWS CloudFront); la copertura test frontend resta minima e va ampliata sui componenti visuali e sui flussi SSE end-to-end.
---
diff --git a/docs/architecture-decisions/0008-angular-frontend-static-serving.md b/docs/architecture-decisions/0008-angular-frontend-static-serving.md
index 6cd7508..f520abe 100644
--- a/docs/architecture-decisions/0008-angular-frontend-static-serving.md
+++ b/docs/architecture-decisions/0008-angular-frontend-static-serving.md
@@ -23,10 +23,20 @@ vive in uno store Angular a signal; SSE, upload multipart, preview PDF, dark mod
loading states restano espliciti.
Terraform LocalStack provisiona anche un bucket S3 dedicato agli asset Angular. Il percorso
-default locale passa da Traefik al servizio Docker `frontend-cloudfront`, che emula il ruolo di
-CloudFront davanti al bucket S3 LocalStack e inoltra `/api/`, `/health` e `/ready` a
-Nginx/Laravel. Il deploy locale carica `apps/frontend/dist` nel bucket con cache-control
-differenziato: `index.html` no-cache, bundle hashati immutable, altri asset con cache breve.
+default locale passa da Traefik al servizio Docker `edge-cdn`: un **secondo Nginx**
+che **emula in locale il ruolo di una CDN/edge** davanti al bucket S3 LocalStack — serve gli
+asset statici e inoltra `/api/`, `/health` e `/ready` all'Nginx applicativo/Laravel. Non emula
+Amazon CloudFront né la sua semantica; ne riproduce solo il *ruolo* di distribuzione edge. In
+produzione quel ruolo sarebbe ricoperto da AWS CloudFront (vedi *Evoluzione*). Il deploy locale
+carica `apps/frontend/dist` nel bucket con cache-control differenziato: `index.html` no-cache,
+bundle hashati immutable, altri asset con cache breve.
+
+Il serving CDN locale è un container Nginx **separato** dall'Nginx applicativo, per due motivi:
+(1) **sostanziale** — l'Nginx applicativo è un'immagine di produzione (buildata, scansionata da
+Trivy, pubblicata su GHCR) e non deve contenere riferimenti a LocalStack; isolare il serving da
+S3 emulato in un container scaffolding solo-locale mantiene pulito l'artefatto di produzione;
+(2) **di forma** — riflette la topologia reale CDN → origin, dove l'edge è distinto dall'origin
+applicativo.

@@ -36,18 +46,29 @@ differenziato: `index.html` no-cache, bundle hashati immutable, altri asset con
- Il frontend applicativo resta su Angular, Angular Router, HttpClient, RxJS e signal store.
- La build statica Angular non dipende dal dev server.
-- Traefik e `frontend-cloudfront` sono il percorso integrato default per demo end-to-end.
-- S3 LocalStack + CloudFront locale valida il pattern build → bucket → CDN-like distribution,
- non la semantica completa di CloudFront reale.
+- Traefik e `edge-cdn` (emulatore CDN locale, Nginx) sono il percorso integrato
+ default per demo end-to-end.
+- S3 LocalStack + emulatore CDN locale (Nginx) valida il pattern build → bucket → distribuzione
+ edge, **non** la semantica di Amazon CloudFront (OAC, invalidation, signed URL, edge propagation).
- I documenti possono usare `POC_DOCUMENT_DISK=real_s3` e `AWS_REAL_*` per Textract reale senza
toccare `FRONTEND_STATIC_BUCKET`, che resta dedicato alla SPA locale.
+## Evoluzione
+
+Il target di produzione è sostituire l'emulatore CDN locale con **Amazon CloudFront** davanti a
+un bucket S3 **privato** con Origin Access Control (OAC), invalidation di `index.html` al deploy
+e response headers policy gestite. L'Nginx `edge-cdn` resta solo lo stand-in locale
+per le demo offline, dato che l'immagine LocalStack Community non espone l'API CloudFront.
+
## Alternatives considered
-- **Mantenere solo Nginx**: semplice, ma non valida il pattern static-hosting richiesto.
-- **Usare l'API CloudFront LocalStack Terraform**: scartato perché l'immagine LocalStack locale
- risponde `501` per CloudFront; il default resta comunque S3 locale + CloudFront locale tramite
- servizio Docker.
+- **Servire la SPA da S3 con il solo Nginx applicativo** (un container): scartato perché
+ imporrebbe un `proxy_pass` verso LocalStack dentro l'immagine di produzione, accoppiandola
+ all'ambiente locale; il container CDN separato evita questo.
+- **Servire la SPA dal filesystem dell'Nginx applicativo**: possibile (l'immagine include già la
+ build), ma non eserciterebbe il percorso via object storage S3.
+- **Usare l'API CloudFront di LocalStack via Terraform**: scartato perché l'immagine LocalStack
+ Community risponde `501` per CloudFront; il ruolo edge resta emulato dal servizio Docker Nginx.
- **Client API scritto a mano**: scartato per evitare deriva dal contratto OpenAPI.
## Implementation evidence
@@ -56,8 +77,8 @@ differenziato: `index.html` no-cache, bundle hashati immutable, altri asset con
- `apps/frontend/orval.config.ts` con `client: "angular"`.
- `docker/nginx/Dockerfile` builda Angular con `node:22-bookworm-slim`.
- `infra/localstack/main.tf` risorsa `aws_s3_bucket.frontend_static`.
-- `docker/cloudfront/default.conf.template` e servizio Compose `frontend-cloudfront`.
-- `Makefile` target `frontend-s3-local-*`, `frontend-cloudfront-local-url` e
+- `docker/edge-cdn/default.conf.template` e servizio Compose `edge-cdn`.
+- `Makefile` target `frontend-s3-local-*`, `edge-cdn-local-url` e
`frontend-serving-local-test`.
## Related documents
diff --git a/docs/architecture-decisions/README.md b/docs/architecture-decisions/README.md
index 06f4aff..df1ff6e 100644
--- a/docs/architecture-decisions/README.md
+++ b/docs/architecture-decisions/README.md
@@ -38,7 +38,7 @@ ADR che referenzia il precedente. La numerazione è progressiva e a quattro cifr
| [0005](0005-no-automatic-fallbacks.md) | Nessun fallback automatico dei servizi AI: stato `failed` esplicito | Accepted, implemented |
| [0006](0006-observability-and-audit.md) | Osservabilità (OTel/Prometheus) e audit trail append-only | Accepted, implemented baseline |
| [0007](0007-authn-authz-boundary.md) | Confine authn/authz: IdP simulato, RBAC/ABAC server-side | Accepted, implemented baseline |
-| [0008](0008-angular-frontend-static-serving.md) | Frontend Angular e serving statico S3 locale/CloudFront locale | Accepted, implemented |
+| [0008](0008-angular-frontend-static-serving.md) | Frontend Angular e serving statico S3 locale + emulatore CDN locale (Nginx) | Accepted, implemented |
## Aggiungere un ADR
diff --git a/docs/architecture/aws-well-architected-mapping.md b/docs/architecture/aws-well-architected-mapping.md
index cb10bf7..716b374 100644
--- a/docs/architecture/aws-well-architected-mapping.md
+++ b/docs/architecture/aws-well-architected-mapping.md
@@ -13,7 +13,7 @@ per il perimetro funzionale [`../poc-scope.md`](../poc-scope.md).
| Operational excellence | Ciclo di vita Docker-first, target Make per setup/verifica, provisioning AWS-like via Terraform, job di migrazione al release, endpoint `/health` e `/ready`, OTel Collector, regole Prometheus con runbook, CI a tre job. | `Makefile`, `docker-compose.yml`, `infra/localstack/*.tf`, `routes/web.php` (`/health`, `/ready`), `docker/prometheus/rules/`, `docs/runbooks/`, `.github/workflows/ci.yml` | Nessun SLO/error budget formalizzato, nessun processo di on-call o di incident management, nessuna policy di approvazione del rilascio. | Definire SLO posseduti, dashboard di capacity, processo di incident e approvazioni di rilascio. |
| Security | Nessuna UI di amministrazione runtime, configurazione separata tra SSM Parameter Store e Secrets Manager, middleware di identità strutturato, controlli RBAC/ABAC con tenant scoping, audit append-only, route legacy bloccate, header di sicurezza e CSP in nginx, nessuna credenziale IAM statica nella CI ordinaria, gate Trivy HIGH/CRITICAL. | `app/Copilot/Support/RuntimeConfigurationLoader.php`, `app/Http/Middleware/`, `app/Copilot/Audit/`, `docker/nginx/default.conf`, `.github/workflows/ci.yml` (Trivy) | Identità simulata via header trusted/config locale (nessun IdP), segreti di default committati come fallback compose, niente AV/CDR sull'upload. | Sostituire l'identità locale con un confine IdP aziendale, collegare un ruolo IAM con privilegio minimo, aggiungere scansione AV/CDR e SBOM gate. |
| Reliability | Orchestrazione Step Functions **end-to-end** (LocalStack) con retry/catch dichiarativi, timeout e heartbeat per task, coda SQS con DLQ (redrive `maxReceiveCount` 3), stati `failed` espliciti, **nessun fallback automatico** dei servizi AI (ADR 0005), tabella di workflow idempotente, readiness check reale. | `infra/localstack/state-machines/document-pipeline.asl.json`, `infra/localstack/main.tf` (DLQ, redrive), `app/Console/Commands/ConsumeWorkflowTasks.php`, `app/Copilot/Workflow/Services/`, migrazione `document_workflow_tasks` (`task_token_hash` unique) | Una sola replica del worker in compose (design già concorrenza-safe), nessun backup/PITR dei dati, procedura di redrive dalla DLQ non automatizzata. | ≥2 repliche del worker, RDS/PITR o backup schedulati con restore testato, runbook di redrive DLQ e retry budget espliciti. |
-| Performance efficiency | SPA servita staticamente da S3 LocalStack via CloudFront locale (cache-control differenziato: `index.html` no-cache, bundle hashati immutable; Nginx come fallback statico e proxy `/api`), PHP-FPM con OPcache, pipeline documentale asincrona fuori dal ciclo HTTP, istogramma di latenza HTTP, dimensione massima del body limitata. | `docker/cloudfront/`, `docker/nginx/`, `docker/php/`, `app/Copilot/Observability/MetricsRecorder.php`, `routes/api.php` (throttle) | Upload gestito in-process anziché con finalizzazione presigned S3; nessun target di capacity derivato dal traffico osservato. | Spostare gli upload pesanti su finalizzazione presigned S3 e fissare target di capacity dai dati osservati. |
+| Performance efficiency | SPA servita staticamente da S3 LocalStack via emulatore CDN locale (Nginx; in produzione AWS CloudFront) con cache-control differenziato (`index.html` no-cache, bundle hashati immutable; Nginx applicativo come fallback statico e proxy `/api`), PHP-FPM con OPcache, pipeline documentale asincrona fuori dal ciclo HTTP, istogramma di latenza HTTP, dimensione massima del body limitata. | `docker/edge-cdn/`, `docker/nginx/`, `docker/php/`, `app/Copilot/Observability/MetricsRecorder.php`, `routes/api.php` (throttle) | Upload gestito in-process anziché con finalizzazione presigned S3; nessun target di capacity derivato dal traffico osservato. | Spostare gli upload pesanti su finalizzazione presigned S3 e fissare target di capacity dai dati osservati. |
| Cost optimization | Lo smoke su AWS reale è opzionale/manuale, le risorse AWS-like girano in LocalStack, Prometheus è locale. | `.github/workflows/aws-smoke.yml`, `docker-compose.yml` (LocalStack), `infra/localstack/` | Nessun tag di allocazione costi, nessun budget, nessuna lifecycle policy sugli oggetti S3. | Aggiungere tag di cost allocation, budget, lifecycle policy sugli oggetti e schedulazioni limitate dello smoke reale. |
| Sustainability | Tooling dockerizzato che evita drift dell'host, build frontend multi-stage, teardown locale via Compose/Terraform, esecuzione su cloud reale minimizzata. | `docker/nginx/Dockerfile` (multi-stage), `docker-compose.yml`, `infra/localstack/` | Nessun budget sulla dimensione delle immagini, nessuna retention esplicita per log, metriche e documenti. | Definire budget di dimensione immagini e policy di retention per log, metriche e documenti. |
diff --git a/docs/architecture/capitolato-traceability.md b/docs/architecture/capitolato-traceability.md
index 2d3d672..d01334b 100644
--- a/docs/architecture/capitolato-traceability.md
+++ b/docs/architecture/capitolato-traceability.md
@@ -343,10 +343,10 @@ ne fornisce l'emulazione locale ripetibile (LocalStack), nello spirito di:
## 16. Hardening di rete e perimetro (Traefik/Nginx, IAM a minimo privilegio)
-**Scelta nella PoC:** TLS edge via Traefik, CloudFront locale (`frontend-cloudfront`) che serve
-la SPA statica da S3 LocalStack e Nginx come proxy applicativo con superfici non
+**Scelta nella PoC:** TLS edge via Traefik, emulatore CDN locale (`edge-cdn`, Nginx) che serve
+la SPA statica da S3 LocalStack e Nginx applicativo come proxy con superfici non
API bloccate, IAM role granulari sulle risorse LocalStack
-([`docker-compose.yml`](../../docker-compose.yml): `traefik`, `frontend-cloudfront`, `nginx`;
+([`docker-compose.yml`](../../docker-compose.yml): `traefik`, `edge-cdn`, `nginx`;
[`infra/localstack/main.tf`](../../infra/localstack/main.tf): `aws_iam_role`). Cifrare il
traffico all'edge, ridurre la superficie esposta e applicare il minimo privilegio sono misure
di igiene di base che riducono il rischio a prescindere dall'ambiente.
@@ -404,7 +404,7 @@ nella libertà tecnologica della sezione «Vincoli».
- **Stack di osservabilità open source (OpenTelemetry Collector, Prometheus, Tempo, Grafana,
Loki, Alertmanager):** adottato come equivalente locale, vendor-neutral, di CloudWatch/X-Ray,
che il Capitolato indica come telemetria target.
-- **Edge/runtime locale (Traefik, CloudFront locale, Nginx):** equivalenti locali di CloudFront/ALB, che il
+- **Edge/runtime locale (Traefik, emulatore CDN locale Nginx, Nginx applicativo):** equivalenti locali di CloudFront/ALB, che il
Capitolato nomina nella loro forma AWS gestita.
- **Librerie di manipolazione PDF (`setasign/fpdf`, `setasign/fpdi`) per lo split documentale:**
dettaglio implementativo non coperto dal Capitolato.
diff --git a/docs/architecture/diagrams/04_edge_runtime_backend_api.drawio b/docs/architecture/diagrams/04_edge_runtime_backend_api.drawio
index 8b80a38..ba8fefc 100644
--- a/docs/architecture/diagrams/04_edge_runtime_backend_api.drawio
+++ b/docs/architecture/diagrams/04_edge_runtime_backend_api.drawio
@@ -112,7 +112,7 @@
-
+
diff --git a/docs/architecture/diagrams/04_edge_runtime_backend_api.drawio.png b/docs/architecture/diagrams/04_edge_runtime_backend_api.drawio.png
index b4d2c8f..a8f1bc1 100644
Binary files a/docs/architecture/diagrams/04_edge_runtime_backend_api.drawio.png and b/docs/architecture/diagrams/04_edge_runtime_backend_api.drawio.png differ
diff --git a/docs/architecture/diagrams/04_edge_runtime_backend_api.drawio.svg b/docs/architecture/diagrams/04_edge_runtime_backend_api.drawio.svg
index 95354fa..0665c93 100644
--- a/docs/architecture/diagrams/04_edge_runtime_backend_api.drawio.svg
+++ b/docs/architecture/diagrams/04_edge_runtime_backend_api.drawio.svg
@@ -1,3 +1,3 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/docs/architecture/diagrams/final-architecture.drawio b/docs/architecture/diagrams/final-architecture.drawio
index ce37838..c4791cf 100644
--- a/docs/architecture/diagrams/final-architecture.drawio
+++ b/docs/architecture/diagrams/final-architecture.drawio
@@ -61,7 +61,7 @@
-
+
diff --git a/docs/architecture/diagrams/final-architecture.drawio.png b/docs/architecture/diagrams/final-architecture.drawio.png
index dcce3cc..599c03f 100644
Binary files a/docs/architecture/diagrams/final-architecture.drawio.png and b/docs/architecture/diagrams/final-architecture.drawio.png differ
diff --git a/docs/architecture/diagrams/final-architecture.drawio.svg b/docs/architecture/diagrams/final-architecture.drawio.svg
index 13f472c..c5e6818 100644
--- a/docs/architecture/diagrams/final-architecture.drawio.svg
+++ b/docs/architecture/diagrams/final-architecture.drawio.svg
@@ -1,3 +1,3 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/docs/architecture/final-architecture.md b/docs/architecture/final-architecture.md
index 6f10601..bba4a51 100644
--- a/docs/architecture/final-architecture.md
+++ b/docs/architecture/final-architecture.md
@@ -25,7 +25,7 @@ export PNG/SVG vanno rigenerati da draw.io dopo ogni modifica (`drawio -x -f png
| Livello | Componente implementato | Ruolo |
| --- | --- | --- |
| Frontend | SPA Angular/TypeScript in `apps/frontend` | Upload, stato di elaborazione, revisione, flussi di anteprima/eliminazione. |
-| Edge | Traefik (TLS), CloudFront locale e Nginx | HTTPS locale, serving della SPA da S3 LocalStack, proxy API, blocco delle superfici `/admin`. |
+| Edge | Traefik (TLS), emulatore CDN locale (Nginx) e Nginx applicativo | HTTPS locale, serving della SPA da S3 LocalStack, proxy API, blocco delle superfici `/admin`. |
| API | API JSON Laravel in `app/Http` | Validazione, controlli di tenant, audit event, avvio del workflow. |
| Workflow | Step Functions e SQS (LocalStack) | Orchestrazione production-like con callback task token, end-to-end. |
| Worker | `php artisan poc:workflow:consume` | Ricezione SQS, esecuzione dei task, `SendTaskSuccess`/`SendTaskFailure`, `SendTaskHeartbeat`. |
@@ -44,12 +44,19 @@ Step Functions, SQS/DLQ, S3, EventBridge, SSM Parameter Store e Secrets Manager.
parla con i servizi AWS, reali o emulati, **senza cambiare codice**: cambiano solo endpoint e
credenziali.
-La SPA Angular usa come default locale il percorso `Traefik -> frontend-cloudfront -> S3
+La SPA Angular usa come default locale il percorso `Traefik -> edge-cdn -> S3
LocalStack`: `make frontend-s3-local-deploy` carica `apps/frontend/dist` nel bucket
`FRONTEND_STATIC_BUCKET`, poi `https://localhost:8443` serve gli asset da quel bucket. Il
-CloudFront locale inoltra `/api/`, `/health` e `/ready` a Nginx/Laravel. Nginx resta il proxy
-applicativo e il percorso interno di compatibilità, ma non è più l'origine primaria della SPA
-nel flusso default. La simulazione non sostituisce una validazione CloudFront reale su AWS.
+servizio `edge-cdn` è un **secondo Nginx** che emula in locale il **ruolo di una
+CDN/edge** (non Amazon CloudFront): serve gli asset statici e inoltra `/api/`, `/health` e
+`/ready` all'Nginx applicativo/Laravel. È un container separato dall'Nginx applicativo di
+proposito — quest'ultimo è un'immagine di produzione (buildata, scansionata, pubblicata) e non
+deve conoscere LocalStack, mentre il serving da S3 emulato resta confinato in uno scaffolding
+solo-locale; la separazione riflette anche la topologia reale CDN → origin. L'Nginx applicativo
+resta il proxy verso PHP-FPM e il percorso interno di compatibilità (include la build SPA), ma
+non è l'origine primaria della SPA nel flusso default. L'emulazione non sostituisce una CDN
+reale: in produzione il ruolo sarebbe ricoperto da AWS CloudFront (bucket privato + OAC,
+invalidation, edge propagation).
### Dettaglio edge/runtime/API
@@ -77,7 +84,7 @@ credenziali e configurazione esplicite:
- `BEDROCK_MODEL_ID`
- `TEXTRACT_ENABLED=true`
-Le variabili `FRONTEND_STATIC_BUCKET` e `FRONTEND_CLOUDFRONT_LOCAL_URL` sono locali e dedicate
+Le variabili `FRONTEND_STATIC_BUCKET` e `EDGE_CDN_LOCAL_URL` sono locali e dedicate
alla SPA: non devono puntare a bucket reali e non governano il caricamento documenti.
I test e la CI standard non chiamano S3, Textract o Bedrock reali.
diff --git a/docs/runbooks/ci-cd.md b/docs/runbooks/ci-cd.md
index 0b2aeac..bba7df7 100644
--- a/docs/runbooks/ci-cd.md
+++ b/docs/runbooks/ci-cd.md
@@ -7,7 +7,7 @@ Ordinary CI runs without enterprise IAM credentials. It is a single pipeline,
- **backend** — builds the app image and runs the PHP checks: Composer manifest validation, Pint (format), Larastan/PHPStan (static analysis) and Pest (tests).
- **frontend** — runs the Angular SPA suite on the Node tool image: OpenAPI contract lint, generated client drift check, ESLint, typecheck, Jest tests, production build, and a production-only `npm audit` at HIGH.
-- **stack** — static infrastructure/observability checks (Terraform `fmt`/`init`/`validate`, OTel Collector and Prometheus config), production image build, Trivy scan (`vuln,secret,config` at HIGH/CRITICAL), LocalStack Terraform apply, Angular SPA build and upload to the LocalStack S3 bucket, HTTPS smoke of the served stack (SPA served via the CloudFront emulator with deep-link fallback, `/api`/`/health`/`/ready`, blocked surfaces, observability dashboards behind basic auth), accessibility (axe/Pa11y plus an enforced-CSP smoke), and conditional publish of the two custom images (`poc-app`, `poc-nginx`) to GHCR.
+- **stack** — static infrastructure/observability checks (Terraform `fmt`/`init`/`validate`, OTel Collector and Prometheus config), production image build, Trivy scan (`vuln,secret,config` at HIGH/CRITICAL), LocalStack Terraform apply, Angular SPA build and upload to the LocalStack S3 bucket, HTTPS smoke of the served stack (SPA served via the local CDN emulator — a separate Nginx — with deep-link fallback, `/api`/`/health`/`/ready`, blocked surfaces, observability dashboards behind basic auth), accessibility (axe/Pa11y plus an enforced-CSP smoke), and conditional publish of the two custom images (`poc-app`, `poc-nginx`) to GHCR.
Supporting workflows:
diff --git a/docs/security/iam-permissions-matrix.md b/docs/security/iam-permissions-matrix.md
index 22ea056..808a315 100644
--- a/docs/security/iam-permissions-matrix.md
+++ b/docs/security/iam-permissions-matrix.md
@@ -9,7 +9,7 @@ realmente; servono come riferimento per il passaggio ad AWS reale.
| API Laravel | `s3:PutObject` | Bucket/prefix documenti reale | Salvare il documento caricato per OCR/AI | Smoke AWS reale / futura prod | Preferire prefix scoped per tenant/ambiente. |
| API Laravel | `s3:GetObject` | Bucket/prefix documenti reale | Leggere il file originale come input Bedrock | Smoke AWS reale / futura prod | Necessario quando l'elaborazione legge dal disco S3. |
| Deploy SPA (CI / `make frontend-s3-local-upload`) | `s3:PutObject` / `s3:DeleteObject` / `s3:ListBucket` | Bucket frontend statico (`FRONTEND_STATIC_BUCKET`) | Caricare/sincronizzare la build Angular (`apps/frontend/dist`) | LocalStack / futura prod | Bucket dedicato alla SPA, separato dai documenti. In prod aggiungere `cloudfront:CreateInvalidation` per invalidare `index.html` dopo il deploy. |
-| Serving SPA (CloudFront → S3) | `s3:GetObject` | Oggetti del bucket frontend statico | Servire gli asset della SPA Angular | LocalStack / futura prod | In LocalStack il bucket è **public-read** (`Principal: "*"`) solo per l'emulazione del CloudFront locale. In prod: bucket **privato** con CloudFront **OAC** (principal `cloudfront.amazonaws.com`, condizione `AWS:SourceArn` sulla distribuzione), **mai** `Principal: "*"`. |
+| Serving SPA (CDN/edge → S3) | `s3:GetObject` | Oggetti del bucket frontend statico | Servire gli asset della SPA Angular | LocalStack / futura prod | In LocalStack il bucket è **public-read** (`Principal: "*"`) solo per l'emulatore CDN locale (Nginx). In prod: bucket **privato** con CloudFront **OAC** (principal `cloudfront.amazonaws.com`, condizione `AWS:SourceArn` sulla distribuzione), **mai** `Principal: "*"`. |
| API Laravel | `states:StartExecution` | State machine della pipeline documentale | Avviare il workflow documentale | LocalStack / futura prod | In LocalStack usa ruolo/risorse create da Terraform. |
| Worker queue | `sqs:ReceiveMessage` | Coda dei task documentali | Consumare i task con callback token | LocalStack / futura prod | Solo coda sorgente. |
| Worker queue | `sqs:DeleteMessage` | Coda dei task documentali | Confermare il task completato/segnalato | LocalStack / futura prod | Eliminare solo dopo la decisione di callback. |
diff --git a/infra/localstack/README.md b/infra/localstack/README.md
index b7aa6f8..4cbb2da 100644
--- a/infra/localstack/README.md
+++ b/infra/localstack/README.md
@@ -29,21 +29,24 @@ Resources modelled here:
- SES local sender identity.
Compose starts LocalStack and application processes. Terraform creates AWS-like resources. The
-default frontend path is still S3 local + CloudFront local: Terraform owns the LocalStack S3
-bucket, while the `frontend-cloudfront` Docker service fronts that bucket and proxies API calls
-to Nginx. This avoids mixing frontend static assets with the optional real S3 bucket used by
+default frontend path is S3 local + a local CDN emulator: Terraform owns the LocalStack S3
+bucket, while the `edge-cdn` Docker service — a second Nginx that emulates the role of
+a CDN/edge (not Amazon CloudFront) — fronts that bucket and proxies API calls to the application
+Nginx. It is a separate container on purpose: the application Nginx is a production image and must
+not reference LocalStack, so the emulated S3 serving stays confined to a local-only scaffold. This
+also avoids mixing frontend static assets with the optional real S3 bucket used by
documents/Textract.
Frontend static serving flow:
```bash
make frontend-s3-local-deploy
-make frontend-cloudfront-local-url
+make edge-cdn-local-url
make frontend-serving-local-test
```
-The CloudFront emulator is local and Docker-based because the LocalStack image used by this PoC
-does not expose the CloudFront API in the default local license. It validates the local
-build-to-bucket-to-CDN flow, but it does not replace testing real CloudFront behavior such as TLS
-certificates, edge propagation, invalidations, OAC/OAI, response headers policies or AWS IAM
-enforcement.
+The CDN emulator is local and Docker-based (a plain Nginx) because the LocalStack image used by
+this PoC does not expose the CloudFront API in the default local license. It validates the local
+build-to-bucket-to-edge flow, but it does not replace a real CDN: in production the role would be
+filled by AWS CloudFront (TLS certificates, edge propagation, invalidations, OAC/OAI, response
+headers policies, AWS IAM enforcement).
diff --git a/infra/localstack/outputs.tf b/infra/localstack/outputs.tf
index 8581c5e..d899f62 100644
--- a/infra/localstack/outputs.tf
+++ b/infra/localstack/outputs.tf
@@ -18,8 +18,8 @@ output "frontend_s3_website_endpoint" {
value = "http://${aws_s3_bucket.frontend_static.bucket}.s3-website.localhost.localstack.cloud:4566"
}
-output "frontend_cloudfront_local_url" {
- value = var.frontend_cloudfront_local_url
+output "edge_cdn_local_url" {
+ value = var.edge_cdn_local_url
}
output "event_bus_name" {
diff --git a/infra/localstack/variables.tf b/infra/localstack/variables.tf
index 22c6efb..e25d3a5 100644
--- a/infra/localstack/variables.tf
+++ b/infra/localstack/variables.tf
@@ -40,8 +40,8 @@ variable "frontend_static_bucket" {
default = "poc-frontend-static-local"
}
-variable "frontend_cloudfront_local_url" {
- description = "Local URL of the Docker CloudFront emulator that fronts the LocalStack S3 frontend bucket."
+variable "edge_cdn_local_url" {
+ description = "Local URL of the Docker CDN/edge emulator (Nginx) that fronts the LocalStack S3 frontend bucket."
type = string
default = "https://localhost:8443"
}