Protected Customer Data: autovalutazione + macchina sempre calda

PROTECTED-CUSTOMER-DATA.md - verdetto: la distribuzione CUSTOM ha accesso
'Always available' a Level 1 e Level 2 (email, statusPageUrl): nessuna approvazione
Shopify da attendere per gli store live. La review servira' per l'app pubblica (R7).
Il fatto che funzioni oggi su pcrt-reso-test non prova nulla: sui development store
la review non e' richiesta comunque.

I requisiti di sicurezza L1/L2 restano pero' obbligatori. Autovalutazione con
evidenze: cifratura a riposo OK (volume Fly ENCRYPTED=true), backup cifrati OK
(snapshot Fly), separazione test/prod OK. Gap: retention non documentata, privacy
policy assente, incident response assente, password Fly da cambiare, snapshot con
sola retention 5gg e nodo singolo (i record legali sono la PROVA del recesso).

fly.toml: min_machines_running 0 -> 1. Un cold start misurato ha richiesto ~38s;
l'art. 54-bis pretende una funzione sempre accessibile e facilmente utilizzabile.
This commit is contained in:
2026-07-10 12:55:00 +02:00
parent 5796e94154
commit 787553af22
2 changed files with 123 additions and 1 deletions

119
PROTECTED-CUSTOMER-DATA.md Normal file
View File

@@ -0,0 +1,119 @@
# Protected Customer Data — verdetto e autovalutazione
Stato: **2026-07-10**. App: `Legal Return PCRT` (custom distribution) su Fly `recesso-custom`.
---
## 1. Serve l'approvazione di Shopify?
**No, non per la distribuzione custom.** Dalla documentazione ufficiale
([shopify.dev/docs/apps/launch/protected-customer-data](https://shopify.dev/docs/apps/launch/protected-customer-data)):
| Livello | Public app | **Custom app** | Admin-created custom app |
|---|---|---|---|
| Level 1 | Requires review | **Always available** | Always available |
| Level 2 (nome, indirizzo, email, telefono) | Requires review | **Always available** | Varies by plan |
Conseguenze:
- **Oggi (custom, store live): nessuna approvazione da attendere.** `order.email` e
`order.statusPageUrl` sono accessibili.
- **Domani (R7, app pubblica): la review sarà obbligatoria.** Questo documento e' la base
della futura richiesta.
- ⚠ Il fatto che oggi funzioni su `pcrt-reso-test` **non prova nulla**: sui development store
la review non e' richiesta comunque. La prova e' la tabella qui sopra, non il test.
**Da fare comunque nel Dev Dashboard:** dichiarare quali dati e campi protetti si usano
(App → API access requests → Protected customer data). Testi pronti al §3.
**⚠ Attenzione:** `order.statusPageUrl` richiede **Level 2** dal 15 marzo 2024
([changelog](https://shopify.dev/changelog/level-2-protected-customer-data-requirements-are-now-needed-to-access-the-order-statuspageurl-field)).
Lo usiamo solo per il bottone "Vedi il tuo ordine" nella ricevuta al cliente: e' una comodita',
non un dato necessario. Per la data minimization della futura app pubblica **valutare di
rimuoverlo**.
---
## 2. Dati protetti effettivamente trattati
| Dato | Da dove | Perche' e' il minimo necessario |
|---|---|---|
| `order.email` | Admin API (lookup ordine) | Verifica d'identita' del consumatore **guest** (senza login, come impone l'art. 54-bis) e destinatario della **ricevuta su supporto durevole** (obbligo di legge) |
| Nome del consumatore | Inserito dal consumatore nel form | Elemento richiesto dall'art. 54-bis nella dichiarazione |
| Testo della dichiarazione | Inserito dal consumatore | E' l'atto giuridico stesso; va conservato come prova |
| `order.statusPageUrl` | Admin API | Solo comodita' (link "Vedi il tuo ordine"). **Non necessario** |
NON trattiamo: indirizzi, telefono, dati di pagamento, profilazione. Nessuna vendita o
condivisione a terzi. Unico sub-processor: il **provider SMTP scelto dal merchant**.
---
## 3. Testi per la dichiarazione nel Dev Dashboard
**Protected customer data — motivazione:**
> L'app implementa la funzione di recesso obbligatoria ex art. 54-bis del Codice del Consumo
> italiano (D.Lgs 209/2025, Dir. UE 2023/2673). Per legge la funzione deve essere utilizzabile
> senza login: l'app deve quindi verificare l'identita' del consumatore confrontando il numero
> d'ordine con l'email associata all'ordine, e inviare a quell'indirizzo la ricevuta su supporto
> durevole con il timestamp di trasmissione. Nessun altro dato del cliente viene letto.
**Protected customer field — `email`:**
> Necessaria per due obblighi di legge: (1) verificare che chi esercita il recesso sia il
> titolare dell'ordine (accesso guest, senza autenticazione); (2) recapitare la ricevuta su
> supporto durevole, che l'art. 54-bis impone di inviare senza indebito ritardo.
**Protected customer field — `name`:** non richiesto via API (lo inserisce il consumatore).
---
## 4. Requisiti Level 1 / Level 2 — stato reale
| # | Requisito | Stato | Evidenza / Gap |
|---|---|---|---|
| L1 | Minimizzazione dei dati | 🟡 | Ok tranne `statusPageUrl` (non necessario) |
| L1 | Informare il merchant su dati e finalita' | 🟡 | `CHECKLIST-COMPLIANCE-MERCHANT.md` c'e'; manca una **privacy policy dell'app** |
| L1 | Uso limitato alle finalita' dichiarate | ✅ | Nessun uso secondario |
| L1 | Periodi di retention definiti | ❌ | **Non documentati.** `shop/redact` fa purge totale; `customers/redact` pseudonimizza. Manca la policy scritta |
| L1 | Cifratura in transito | ✅ | HTTPS ovunque; SMTP con STARTTLS/TLS |
| L1 | Cifratura a riposo | ✅ | Volume Fly `pg_data` **ENCRYPTED = true**. Password SMTP per-shop cifrate AES-256-GCM (`crypto.server.ts`) |
| L2 | Backup cifrati | ✅ | Snapshot automatici Fly (cifrati perche' il volume lo e') |
| L2 | Separazione test / produzione | ✅ | Dev = Postgres in Docker locale; prod = Fly Postgres. DB e app distinti |
| L2 | Accesso staff limitato | 🟡 | Un solo titolare. **Ma la password master Fly era in chiaro nel file `Cred Fly`: da cambiare** |
| L2 | Password robuste | ❌ | Vedi sopra: cambio password Fly **non confermato** |
| L2 | Access log | 🟡 | Log applicativi Fly (effimeri) + `AuditLog` append-only con payload hashati. Nessun log di accesso al DB |
| L2 | Incident response policy | ❌ | **Non esiste.** Va scritta |
| L2 | Data loss prevention | ❌ | Snapshot con **retention 5 giorni**, nodo singolo, nessuna copia off-site. Vedi §5 |
---
## 5. Due rischi che non sono formalita'
### 5.1 Durabilita' dei record legali
Gli snapshot Fly hanno **retention 5 giorni** e vivono nella stessa infrastruttura. Sono
*disaster recovery*, non *archivio*. Ma le `WithdrawalRequest` e l'`AuditLog` sono la **prova**
del recesso: se il termine si estende a 12 mesi per mancata informativa, o se sorge una
controversia, quei record devono esistere ben oltre 5 giorni. Un solo nodo, nessuna copia
off-site: se l'app Postgres viene cancellata, la prova sparisce.
**Fix:** dump logico periodico verso storage esterno (o Fly Managed Postgres / Supabase, che
hanno backup gestiti con retention lunga). Non e' un requisito Shopify — e' il motivo per cui
l'app esiste.
### 5.2 Cold start sulla funzione di recesso
`min_machines_running = 0`: la macchina si spegne. Una richiesta a freddo ha impiegato
**38 secondi**. L'art. 54-bis pretende una funzione **"sempre accessibile"** e **"facilmente
utilizzabile"**: 38 secondi di attesa dopo il clic sono un ostacolo, e con ogni probabilita'
il consumatore abbandona.
**Fix:** `min_machines_running = 1` (una macchina sempre calda).
---
## 6. Azioni, in ordine
1. **Cambiare la password Fly** (era in chiaro). — *utente*
2. **`min_machines_running = 1`** nel `fly.toml`. — *codice, banale*
3. **Backup off-site** dei record legali (dump periodico). — *da progettare*
4. **Privacy policy dell'app** + **retention policy** scritte. — *documenti*
5. **Incident response policy** (chi, cosa, entro quanto). — *documento*
6. Dichiarare dati e campi nel **Dev Dashboard** (testi al §3). — *utente*
7. *(Per R7, app pubblica)* valutare la **rimozione di `statusPageUrl`** per minimizzazione.

View File

@@ -17,7 +17,10 @@ primary_region = "fra" # Frankfurt (EU data residency)
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 0
# Una macchina sempre calda. Con 0, un cold start misurato ha richiesto ~38s:
# l'art. 54-bis pretende una funzione "sempre accessibile" e "facilmente
# utilizzabile", e 38 secondi dopo il clic sono un ostacolo. Costo: 1 macchina.
min_machines_running = 1
[[vm]]
size = "shared-cpu-1x"