curatare
This commit is contained in:
@@ -1,755 +0,0 @@
|
||||
<!-- /autoplan restore point: ~/.gstack/projects/ROACONT/main-autoplan-restore-20260707-114014.md -->
|
||||
# PRD — Obținere automată token OAuth2 ANAF eFactura (fără copy/paste)
|
||||
|
||||
**Data:** 2026-07-07
|
||||
**Proiect:** ROACONT (ERP contabilitate, Visual FoxPro) + site romfast.ro (PHP)
|
||||
**Status:** Draft
|
||||
|
||||
## 1. Problemă
|
||||
|
||||
Funcționalitatea eFactura SPV necesită un token OAuth2 emis de ANAF (`logincert.anaf.ro`).
|
||||
Obținerea unui token NOU cere autentificare mutual-TLS cu certificatul calificat de pe
|
||||
tokenul USB, care funcționează doar într-un browser real (utilizatorul introduce PIN-ul
|
||||
semnăturii electronice). Fluxul actual:
|
||||
|
||||
1. ROACONT (`anaf_efactura.prg` → `newToken`) deschide browserul pe `https://romfast.ro/oauth2/`.
|
||||
2. `index.php` redirecționează la ANAF authorize; ANAF apelează înapoi callback-ul cu `?code=...`.
|
||||
3. `index.php` schimbă codul pe token și **afișează Access Token + Refresh Token în pagină**.
|
||||
4. Utilizatorul **copiază manual** cele două valori și le lipește în formularul `anaf_token` din ROACONT.
|
||||
|
||||
Pasul 4 este sursă de erori (trunchieri, spații, confuzie între cele două tokenuri) și
|
||||
o experiență proastă pentru utilizatori. Refresh-ul tokenului (fără browser) funcționează
|
||||
deja corect și NU se modifică.
|
||||
|
||||
**Variante respinse:**
|
||||
- Apel direct `/authorize` din VFP cu `WinHttp.SetClientCertificate` — testat anterior,
|
||||
probleme la citirea certificatelor de pe tokenurile USB (drivere capricioase).
|
||||
- WebView2 încorporat în VFP — runtime-ul e azi preinstalat pe Windows 10/11 (via Edge),
|
||||
dar integrarea cere wrapper ActiveX/OLE per stație și tratarea dialogului de certificat
|
||||
în control; efort și suprafață de suport disproporționate față de varianta aleasă.
|
||||
- Protocol handler custom (`roacont://`) — setup registry per stație, prompt browser, fragil.
|
||||
- **Token broker pe romfast.ro** (serverul păstrează refresh token-urile per client și
|
||||
servește access token-uri la cerere; zero gestiune de token în aplicație, sincronizare
|
||||
multi-stație automată) — respins CA SCOPE AICI: vendorul ar deveni custode al accesului
|
||||
la datele fiscale ale clienților (răspundere, GDPR, consimțământ contractual). Decizie
|
||||
de business, nu tehnică. Arhitectura aleasă (serverul e deja intermediar la schimbul
|
||||
code→token) lasă ușa deschisă pentru broker mai târziu. Mutat în TODOS ca direcție
|
||||
de evaluat comercial.
|
||||
|
||||
**Încadrare:** funcționalitatea e igienă/paritate competitivă (aplicațiile cloud nu au
|
||||
deloc problema; competitorii desktop rezolvă similar), nu diferențiator. Beneficiul
|
||||
măsurabil: reducere tichete suport pe subiectul "token" și un motiv de churn în minus.
|
||||
|
||||
## 2. Soluție aleasă
|
||||
|
||||
**Browser + polling cu identificator de sesiune (`state`).** Browserul se deschide în
|
||||
continuare (necesar pentru PIN-ul certificatului), dar tokenurile nu mai sunt afișate:
|
||||
serverul romfast.ro le stochează temporar sub un ID unic generat de ROACONT, iar ROACONT
|
||||
le ridică automat prin polling. Utilizatorul doar introduce PIN-ul și revine în aplicație.
|
||||
|
||||
### Flux nou
|
||||
|
||||
```
|
||||
ROACONT romfast.ro/oauth2/ ANAF
|
||||
| | |
|
||||
|-- genereaza STATE (unic, secret) ->| |
|
||||
|-- deschide browser ?state=STATE -->| |
|
||||
| |-- redirect authorize&state --->|
|
||||
| | [salveaza state in SESIUNE] |
|
||||
| | (utilizator: PIN) |
|
||||
| |<-- callback ?code[&state?] ----|
|
||||
| | [state OPTIONAL in callback; |
|
||||
| | legarea reala = sesiunea PHP] |
|
||||
| |-- POST code -> token --------->|
|
||||
| |<-- access+refresh token -------|
|
||||
| | salveaza STATE.json in |
|
||||
| | oauth2_tokens/ (extra-docroot) |
|
||||
| | afiseaza "Autorizare reusita" |
|
||||
|-- polling pick.php?state=STATE --->| |
|
||||
|<-- JSON tokens (apoi sterge) ------| |
|
||||
| SaveToken() in baza de date | |
|
||||
```
|
||||
|
||||
## 3. Cerințe funcționale
|
||||
|
||||
### 3.0 Faza 0 — Spike de validare ANAF (~1 oră, ÎNAINTE de restul implementării)
|
||||
|
||||
Se adaugă DOAR propagarea `state` + sesiunea + logarea în `index.php` (fără `pick.php`,
|
||||
fără modificări VFP) și se rulează o generare reală de token cu certificat + tokenul USB.
|
||||
Se verifică empiric: (a) ANAF acceptă parametrul `state` la authorize fără eroare;
|
||||
(b) ANAF îl returnează sau nu în callback; (c) cookie-ul de sesiune supraviețuiește
|
||||
redirectului (SameSite). Rezultatul decide detaliile FR-2 înainte de a construi restul.
|
||||
|
||||
### 3.1 Server PHP (`D:\PROIECTE\SITE_ROMFAST_2025\oauth2\`)
|
||||
|
||||
**FR-1 — `index.php`: propagare `state`.** La intrarea inițială (fără `code`), dacă
|
||||
există parametrul `state`, acesta se adaugă la URL-ul de authorize ANAF (parametru
|
||||
standard OAuth2, returnat neschimbat în callback).
|
||||
|
||||
**FR-2 — `index.php`: legare prin sesiune (PRIMAR).** Verificare 2026-07-07: documentul
|
||||
oficial ANAF (Oauth_procedura, pag. 23) instruiește „State se lasă necompletat" — nu ne
|
||||
putem baza pe propagarea `state` de către ANAF. Prin urmare **sesiunea PHP (cookie) este
|
||||
mecanismul primar de legare**: la intrarea inițială `state` se salvează în sesiune
|
||||
(`$_SESSION['roa_state']`); la callback, identificatorul se ia din sesiune. Dacă ANAF
|
||||
returnează totuși `state` în callback, acesta se folosește suplimentar ca validare CSRF
|
||||
(trebuie să coincidă cu cel din sesiune; nepotrivire → eroare, fără stocare de tokenuri).
|
||||
Primul test real logează empiric dacă ANAF propagă `state` (vezi planul de teste).
|
||||
**Cerință cookie:** callback-ul ANAF e o navigare top-level dintr-un alt domeniu —
|
||||
cookie-ul de sesiune trebuie emis cu `SameSite=Lax; Secure`
|
||||
(`session_set_cookie_params(['samesite' => 'Lax', 'secure' => true])`), altfel cu
|
||||
`SameSite=Strict` legarea pică silențios și fluxul cade mereu pe timeout.
|
||||
Sintaxa cu array cere **PHP ≥ 7.3** — versiunea se verifică la spike (Faza 0); sub 7.3,
|
||||
fallback documentat `session_set_cookie_params(0, '/; SameSite=Lax', ...)`. Apelul
|
||||
precede `session_start()` pe AMBELE intrări (inițială și callback).
|
||||
**Igienă sesiune (anti fund-de-sac):**
|
||||
- `roa_state` se stochează cu timestamp și se IGNORĂ dacă e mai vechi de 15 minute;
|
||||
- la intrarea inițială FĂRĂ `state` (fluxul manual/fallback), `unset($_SESSION['roa_state'])`
|
||||
explicit — altfel o sesiune rămasă de la o tentativă automată eșuată ar redirecționa
|
||||
fallback-ul manual pe pagina de succes fără tokenuri (fund de sac);
|
||||
- la intrarea CU `state` când există deja un `roa_state` proaspăt neconsumat (altă
|
||||
instanță ROACONT în același browser — două firme deschise simultan), pagina afișează
|
||||
avertisment „O altă generare de token este în curs dintr-o altă fereastră a aplicației.
|
||||
Finalizați-o pe aceea sau reluați peste câteva minute." și NU suprascrie tăcut.
|
||||
|
||||
**FR-3 — `index.php`: stocare tokenuri.** La callback cu `code` + `state` cunoscut,
|
||||
după schimbul code→token, răspunsul JSON de la ANAF se salvează în directorul de
|
||||
tokenuri (conform FR-11: extra-docroot, ex. `../oauth2_tokens/<state>.json`) și pagina
|
||||
afișează doar un mesaj de succes ("Autorizare reușită. Reveniți în aplicația ROA.
|
||||
Puteți închide această pagină."), fără a mai afișa tokenurile. Dacă schimbul eșuează,
|
||||
se salvează în același director `{"error": "..."}` ca ROACONT să afle imediat, și
|
||||
pagina afișează eroarea (fără a suprascrie un fișier de succes existent — FR-10).
|
||||
|
||||
**FR-4 — `index.php`: compatibilitate retro.** Fără `state` (versiuni vechi ROACONT),
|
||||
comportamentul actual rămâne neschimbat: tokenurile se afișează în pagină pentru
|
||||
copiere manuală. Ramura `refresh_token` rămâne neatinsă.
|
||||
|
||||
**FR-5 — `pick.php` (nou).** Endpoint de polling; clientul trimite `state` prin
|
||||
**POST body** (nu în query string — nu ajunge în access-log-urile hostingului);
|
||||
GET rămâne acceptat pentru diagnosticare, cu răspunsuri identice:
|
||||
- `state` valid + fișier existent → „claim" atomic prin `rename()` către un nume
|
||||
temporar propriu, apoi citire + ștergere (citire unică; pickup-uri concurente au
|
||||
un singur câștigător), răspuns JSON, status 200.
|
||||
- fișier inexistent → **status 200** cu `{"status":"pending"}` (NU 404: pe shared
|
||||
hosting, lanțuri Apache/WAF pot înlocui corpul 404-urilor cu pagini custom, iar
|
||||
clientul ar declara eronat „pick.php lipsește"). Răspunsul pentru „state necunoscut"
|
||||
și „încă negenerat" este IDENTIC (fără oracle de existență). HTTP 404 real rămâne
|
||||
astfel semnalul natural pentru „pick.php nedesfășurat" (M10).
|
||||
- `state` invalid ca format → 400, fără acces la disc; validarea formatului se face
|
||||
ÎNAINTE de orice logare (anti log-injection: nimic din state-urile invalide nu se
|
||||
scrie în log neescapat; prefixul logat se sanitizează la `[A-Za-z0-9]`).
|
||||
- `pick.php` NU apelează `session_start()` (evită serializarea pe lock-ul de sesiune).
|
||||
- La fiecare apel, curăță fișierele de tokenuri mai vechi de 10 minute (TTL).
|
||||
|
||||
**FR-10 — Curățare securitate `index.php` (în aceeași livrare).**
|
||||
- Eliminare `parse_str()` peste query string (injecție de variabile); citire explicită
|
||||
`$_GET['refresh_token']`, `$_GET['code']`, `$_GET['state']`, `$_GET['error']`.
|
||||
- `client_secret` NU se mai trimite în query-ul URL-ului de authorize (ajunge în istoricul
|
||||
browserului fiecărui client și în log-uri; OAuth2 nu îl cere la authorize).
|
||||
- Tratare explicită callback de eroare ANAF (`?error=...`): pagină de eroare + scriere
|
||||
`{"error":...}` pentru state-ul sesiunii (fără buclă de re-redirect la authorize).
|
||||
- Tratare erori curl / status HTTP / JSON de eroare de la ANAF la schimbul code→token
|
||||
(azi un eșec afișează tokenuri goale).
|
||||
- Regulă anti-suprascriere: un rezultat de eroare NU suprascrie un fișier de tokenuri
|
||||
deja scris pentru același `state` (cazul refresh pe pagina de callback după succes).
|
||||
- Arhivare/eliminare fișiere moarte din `oauth2/`: `index1.php`, `index2.php`, `info.php`
|
||||
(expunere `phpinfo()`).
|
||||
|
||||
**FR-11 — Protecție și observabilitate `pick.php` + stocare.**
|
||||
- Fișierele de tokenuri se stochează ÎN AFARA docroot-ului (ex. `../oauth2_tokens/`);
|
||||
dacă hostingul nu permite, fallback: director în docroot cu `.htaccess` deny all.
|
||||
- **Nume de fișier = `hash('sha256', state)` ÎNTOTDEAUNA** (nu doar în fallback) —
|
||||
secretul nu apare în listing-uri de directoare sau backup-uri de hosting.
|
||||
- **Scriere atomică**: callback-ul scrie în `<hash>.json.tmp` apoi `rename()` (atomic
|
||||
pe același filesystem) — pick.php nu poate citi niciodată un JSON parțial scris.
|
||||
- Fișierul JSON include și **IP-ul callback-ului**; la pickup, nepotrivirea IP pickup
|
||||
vs IP callback se LOGHEAZĂ (nu se blochează — proxy/VPN split ar produce false
|
||||
pozitive), ca semnal forensic pentru un eventual pickup furat.
|
||||
- Rate-limit per IP pe `pick.php`: **max 240 cereri/min/IP** → 429 (headroom pentru
|
||||
birouri cu mai multe stații după același NAT: un client la 2s = 30 req/min; 240
|
||||
acoperă 8 stații simultane). ROACONT trece la 5s după un 429 și rămâne la 5s până
|
||||
la finalul fluxului curent. Contorul per IP: fișiere per-minut per-IP în directorul
|
||||
extra-docroot (shared hosting nu garantează APCu); curățate de același TTL.
|
||||
- Log de evenimente: fișier text pe server cu numele lunar `oauth2_events_YYYYMM.log`
|
||||
(rotire implicită prin numele fișierului — fără cron): timestamp, IP, eveniment
|
||||
(`authorize_start`, `callback_ok`, `callback_error`, `pickup_ok`, `pickup_pending`,
|
||||
`pickup_ip_mismatch`, `rate_limited`, `ttl_cleanup`), prefix 8 caractere sanitizat
|
||||
din `state`. **Niciodată valori de tokenuri.** Contoare zilnice succes/eroare/timeout.
|
||||
- Schimbul code→token din `index.php`: `CURLOPT_CONNECTTIMEOUT=10`, `CURLOPT_TIMEOUT=30`
|
||||
și `session_write_close()` înainte de apelul curl (nu ținem lock-ul de sesiune cât
|
||||
răspunde ANAF).
|
||||
|
||||
### 3.2 Client VFP (`D:\ROA\ROACONT\COMUN\programe\anaf_efactura.prg`)
|
||||
|
||||
**FR-6 — Generare `state`.** Funcția `newToken` (ambele clase: `AnafeFacturaServer`
|
||||
~linia 505 și `ANAFeFactura` ~linia 2724) generează un identificator unic cu minim
|
||||
128 biți de entropie criptografică. **Obligatoriu prin API Windows**: două GUID-uri
|
||||
generate cu `CoCreateGuid` concatenate și normalizate la `[A-Za-z0-9]`. Declarația
|
||||
exactă (ca implementarea să nu alunece înapoi spre SYS(2015)):
|
||||
```foxpro
|
||||
DECLARE INTEGER CoCreateGuid IN ole32 STRING @pguid
|
||||
lcBuf = REPLICATE(CHR(0), 16)
|
||||
CoCreateGuid(@lcBuf) && de 2 ori; cele 32 de octeți se convertesc hex → 64 caractere
|
||||
```
|
||||
`SYS(2015)` este INTERZIS ca sursă de entropie (derivat din timestamp, predictibil) —
|
||||
`state` e singurul secret care protejează tokenurile la `pick.php`.
|
||||
|
||||
**FR-7 — Deschidere browser + polling.** După `open_default_app(url + '?state=' + state)`,
|
||||
ROACONT intră în buclă de polling pe `pick.php` (cu `state` în POST body — FR-5)
|
||||
folosind `WinHttp.WinHttpRequest.5.1` (fallback `MSXML2.ServerXMLHTTP.6.0` — vezi
|
||||
punctul despre timeout-uri):
|
||||
- interval: 2 secunde (5s după un răspuns 429, rămâne 5s până la finalul fluxului);
|
||||
**timeout total: 10 minute**, aliniat cu TTL-ul serverului (PIN greșit, dialog
|
||||
selecție certificat, lentoare ANAF — utilizatorii nu sunt tehnici; 3 minute ar fi
|
||||
subdimensionat);
|
||||
- **timeout-uri HTTP explicite per iterație**: `loHTTP.SetTimeouts(5000,5000,5000,5000)`
|
||||
— fără ele, default-urile WinHttp (30-60s) îngheață Esc-ul și WAIT WINDOW la o rețea
|
||||
căzută; fallback-ul este `MSXML2.ServerXMLHTTP.6.0` (are `setTimeouts`), NU
|
||||
`MSXML2.XMLHTTP.6.0` (nu are);
|
||||
- **helper HTTP cu erori garantat prinse**: TRY imbricat și pentru transportul de
|
||||
fallback (pattern-ul din refreshToken lasă excepția din CATCH nehandled), iar flag-ul
|
||||
gărzii de reintrare se resetează garantat (FINALLY) — altfel un crash în buclă lasă
|
||||
aplicația blocată pe M8 până la restart;
|
||||
- clientul apelează `pick.php` cu `state` în **POST body** (FR-5 — nu în query string);
|
||||
- feedback vizual: `WAIT WINDOW ... NOWAIT` cu mesaje PROGRESIVE (M2 la start, M3 după
|
||||
90s, M4 după 5 min, cu minutele scurse — texte finale în §3.3; fereastra statică
|
||||
10 minute ar arăta a aplicație blocată); verificare `INKEY(0.5)` (Esc <1s);
|
||||
- gardă de reintrare (flag „flux în curs" → M8) și verificare rezultat
|
||||
lansării browserului (eșec → M9 + fallback manual) printr-un **wrapper local** în
|
||||
`anaf_efactura.prg` care returnează `ShellExecute(...) > 32` — funcția globală
|
||||
`open_default_app` (oexport.prg:302) NU returnează nimic, deci M9 ar fi
|
||||
neimplementabil prin ea; helper-ul global (folosit în zeci de locuri) nu se atinge;
|
||||
- **gardă de context interactiv**: implementarea unică stă în `AnafeFacturaServer.newToken`,
|
||||
iar clasa `ANAFeFactura` DELEAGĂ (fără duplicare); în context non-interactiv/headless
|
||||
(ROAEFACTURA server, `_SCREEN` invizibil) funcția refuză cu mesaj în log, fără browser
|
||||
și fără WAIT WINDOW;
|
||||
- erori de rețea tranzitorii per iterație → se continuă polling-ul; **HTTP 404** repetat
|
||||
(N≥5 consecutiv) → `pick.php` nedesfășurat pe server (pending-ul legitim e 200 — FR-5,
|
||||
deci 404 e neambiguu) → M10 + fallback manual;
|
||||
- la succes: parsare cu `nfjsonread()`, populare `cToken`, `cRefreshToken`,
|
||||
`dTokenGendate = DATE()`, `dTokenExpdate` din `expires_in`, apel `SaveToken()`,
|
||||
mesaj de confirmare cu data expirării;
|
||||
- la `{"error":...}` sau timeout: mesaj clar + **fallback pe fluxul vechi** (FR-8);
|
||||
mesajul de timeout recomandă regenerarea tokenului dacă autorizarea în browser
|
||||
păruse reușită (protecție împotriva unui pickup furat — forensics în log server FR-11);
|
||||
- **kill switch**: opțiune `ANAF_TOKEN_AUTOFLOW` (implicit 1); pe 0, `newToken` revine
|
||||
la comportamentul actual (doar deschidere browser, completare manuală) — rollback
|
||||
fără redistribuire de exe. Citirea opțiunii e robustă la NULL/opțiune inexistentă/
|
||||
Oracle indisponibil (`NVL(...)`, default 1 — atenție: pattern-ul de la linia 462 nu
|
||||
are NVL). Opțiunile fiind per firmă, kill switch-ul e **per firmă** (intenționat —
|
||||
permite dezactivare selectivă la un client cu probleme).
|
||||
- **Comportament headless (clasa server / ROAEFACTURA) — DECIS la poarta finală (D4)**:
|
||||
în context non-interactiv, funcția unificată păstrează EXACT comportamentul actual
|
||||
al `AnafeFacturaServer.newToken` (linia ~505): log cu instrucțiunile de completare
|
||||
a tokenurilor în `gcGeneralIniFile` + deschiderea browserului. Fluxul automat nou
|
||||
(polling) rulează DOAR în context interactiv (ROACONT).
|
||||
- **Intrarea pe fluxul nou la expirarea refresh token-ului — preluată de utilizator
|
||||
(D3)**: legarea mesajului de eșec de refresh de apelul `newToken` o implementează
|
||||
utilizatorul direct în VFP; NU face parte din acest plan.
|
||||
|
||||
**FR-8 — Fallback manual.** Dacă polling-ul eșuează (timeout, eroare rețea), utilizatorul
|
||||
primește opțiunea de a relua fluxul vechi (mesajul actual + completare manuală în
|
||||
formularul `anaf_token`). Formularul existent nu se elimină. **Detalii obligatorii:**
|
||||
- Fallback-ul deschide URL-ul **FĂRĂ parametrul `?state`** — doar așa `index.php`
|
||||
afișează tokenurile pentru copiere (ramura retro-compat FR-4). Reluarea cu `state`
|
||||
ar duce pe pagina de succes fără tokenuri vizibile = fund de sac.
|
||||
- Mesajul de fallback avertizează explicit: va fi necesară reintroducerea PIN-ului
|
||||
(codul de autorizare e single-use), iar codurile vor fi afișate pentru copiere manuală.
|
||||
- Textele exacte: vezi §3.3 (tabelul de mesaje).
|
||||
|
||||
**FR-9 — Mesaj utilizator actualizat.** Mesajul inițial din `newToken` nu mai cere
|
||||
copierea tokenurilor. Devine dialog **OK/Anulare** (nu OK-only ca azi — utilizatorul
|
||||
fără tokenul USB la îndemână trebuie să poată renunța curat înainte de deschiderea
|
||||
browserului) și menționează explicit pasul de **selecție a certificatului** (dialogul
|
||||
Windows care sperie utilizatorii netehnici). Textul exact: vezi §3.3. Ghidarea
|
||||
detaliată NU se încarcă toată în modalul inițial — se mută în mesajele progresive
|
||||
din fereastra de așteptare (FR-7/§3.3).
|
||||
|
||||
### 3.3 Specificații UI/UX (texte finale și layout — Faza 2, design review)
|
||||
|
||||
**Regula diacriticelor (decisă):** mesajele VFP se scriu FĂRĂ diacritice (consistent
|
||||
cu toată aplicația — codepage 1250/852 corupe diacriticele; vezi mesajele existente
|
||||
„Introduceti", „Copiati" la liniile 508-510/2725-2727). Paginile de browser (PHP) se
|
||||
scriu CU diacritice + `<meta charset="utf-8">`.
|
||||
|
||||
**Gardă de reintrare:** flag „flux în curs" pe clasă; re-apăsarea „Token nou" în timpul
|
||||
polling-ului afișează M8 fără a porni un al doilea flux.
|
||||
|
||||
**Eșec lansare browser:** rezultatul `open_default_app()` se verifică; la eșec →
|
||||
mesaj imediat M9 + fallback manual (fără a intra în polling).
|
||||
|
||||
**Kill switch (`ANAF_TOKEN_AUTOFLOW=0`):** se afișează mesajul VECHI, nemodificat
|
||||
(cel cu instrucțiuni de copiere) — aplicația nu promite „preluare automată" când
|
||||
pagina va afișa tokenuri de copiat.
|
||||
|
||||
**Tabelul mesajelor VFP (texte finale, fără diacritice):**
|
||||
|
||||
| # | Moment | Text | Butoane |
|
||||
|---|--------|------|---------|
|
||||
| M1 | Modal inițial (FR-9) | „Se va genera un token nou eFactura.(CR)1. Introduceti tokenul USB cu semnatura electronica in calculator.(CR)2. Se va deschide pagina ANAF in browser: selectati certificatul dvs. si introduceti PIN-ul.(CR)3. Reveniti in aplicatie — tokenul se preia automat, fara copiere manuala.(CR)(CR)Continuati?" | OK / Anulare |
|
||||
| M2 | WAIT WINDOW 0-90s | „Asteptare autorizare ANAF... Continuati in fereastra browserului: selectati certificatul si introduceti PIN-ul. (Esc = anulare)" | — |
|
||||
| M3 | WAIT WINDOW 90s-5min | „Inca astept autorizarea ANAF (<N> min)... Nu a aparut fereastra de PIN? Verificati daca browserul s-a deschis (poate fi in spatele acestei ferestre). (Esc = anulare)" | — |
|
||||
| M4 | WAIT WINDOW 5-10min | „Inca astept autorizarea ANAF (<N> min)... Daca nu reusiti, apasati Esc pentru anulare si completare manuala." | — |
|
||||
| M5 | Timeout 10 min | „Nu s-a primit tokenul in 10 minute. Verificati ca tokenul USB este introdus si ca browserul a deschis pagina ANAF.(CR)Reluati generarea? (Va fi necesar PIN-ul din nou.)(CR)Da = reluare automata, Nu = completare manuala (se deschide pagina care afiseaza codurile)" | Da / Nu |
|
||||
| M6 | Anulare Esc | „Ati anulat asteptarea. Daca ati introdus deja PIN-ul, autorizarea respectiva va fi ignorata; la reluare va trebui sa introduceti PIN-ul din nou." | OK |
|
||||
| M7 | Succes | „S-a generat token-ul eFactura, valabil pana la <data>. Nu este necesara nicio copiere manuala." | OK |
|
||||
| M8 | Reintrare in flux | „O generare de token este deja in curs. Asteptati finalizarea sau anulati-o cu Esc." | OK |
|
||||
| M9 | Esec lansare browser | „Nu s-a putut deschide browserul. Se continua cu fluxul manual: se va deschide pagina care afiseaza codurile pentru copiere." | OK |
|
||||
| M10 | pick.php absent (HTTP 404 ≥5) | „Componenta de server pentru preluarea automata nu este inca instalata. Se continua cu fluxul manual: copiati codurile din pagina de browser. Anuntati administratorul (actualizare site romfast.ro)." | OK |
|
||||
| M11 | Esec SaveToken | „Tokenul a fost generat, dar salvarea in baza de date a esuat: <detaliu>. Reincercati salvarea?" — **Da = retry SaveToken** (tokenul e inca in memorie in `This.cToken`; reluarea PIN-ului ar fi disproportionata pentru un esec de scriere DB). Dupa 3 esecuri: „Salvarea nu a reusit. Reluati operatia Token nou (va fi necesar PIN-ul din nou) sau contactati suportul ROA." | Da / Nu |
|
||||
| M12 | Eroare ANAF (din fisierul {"error"}) | vezi tabelul de traduceri de mai jos | OK |
|
||||
|
||||
**Traducerea erorilor ANAF în limbaj uman (M12):**
|
||||
|
||||
| Cod ANAF | Text VFP |
|
||||
|---|---|
|
||||
| `invalid_grant` / cod expirat | „Codul de autorizare a expirat (valabilitate ~1 minut). Reluati operatia Token nou." |
|
||||
| `access_denied` | „Autorizarea a fost refuzata sau anulata in pagina ANAF. Reluati operatia daca doriti." |
|
||||
| orice alt cod | „Eroare la autorizarea ANAF: <cod>. Reluati operatia sau contactati suportul ROA." |
|
||||
|
||||
**Layout pagini browser (PHP, UTF-8, cu diacritice):**
|
||||
- Reguli comune: un H1 mare (scanabil dintr-o privire), o singură propoziție de corp,
|
||||
font ≥16px, contrast înalt (text închis pe fundal deschis), fără linkuri, fără
|
||||
butoane, fără carduri/decor; logo/numele ROA opțional, discret, sus. **Un singur
|
||||
job per pagină.** Nu se promite auto-close (`window.close()` nu funcționează pe
|
||||
taburi deschise de utilizator).
|
||||
- **Succes:** H1 „Autorizare reușită" (verde). Corp: „Reveniți în aplicația ROA —
|
||||
tokenul se preia automat. Puteți închide această pagină."
|
||||
- **Eroare:** H1 „Autorizarea nu a reușit" (roșu). Corp: cauza pe scurt în limbaj
|
||||
uman (o propoziție, fără jargon) + „Reveniți în aplicația ROA — de acolo puteți
|
||||
relua operația sau folosi completarea manuală." **Fără link de reluare în browser**
|
||||
(reluarea cere `state` nou generat de aplicație; un retry în browser ar produce
|
||||
buclă de confuzie).
|
||||
- Ramura retro-compat (fără `state`, FR-4): pagina actuală cu tokenurile afișate
|
||||
rămâne neschimbată.
|
||||
|
||||
## 4. Cerințe non-funcționale / securitate
|
||||
|
||||
**NFR-1 — Entropie `state`.** Minim 128 biți de entropie; doar caractere `[A-Za-z0-9]`;
|
||||
lungime fixă validată strict în PHP (protecție path traversal — `state` intră în nume
|
||||
de fișier).
|
||||
|
||||
**NFR-2 — Fereastră de expunere minimă.** Tokenurile stau pe server doar între callback
|
||||
și pick (secunde). Ștergere la prima citire + TTL 10 minute + stocare în afara
|
||||
docroot-ului conform FR-11 (fallback: `.htaccess` deny all + nume fișier `hash(state)`).
|
||||
|
||||
**NFR-3 — HTTPS obligatoriu** pe toate apelurile (deja existent pe romfast.ro).
|
||||
|
||||
**NFR-4 — Fără logging de tokenuri** în PHP sau în log-ul ROACONT (`This.Log` nu
|
||||
primește valorile tokenurilor, doar statusuri).
|
||||
|
||||
**NFR-5 — Non-blocant rezonabil.** Polling-ul din VFP nu blochează complet UI-ul
|
||||
(WAIT WINDOW + INKEY permit anularea); nu se folosește `SLEEP` blocant fără ieșire.
|
||||
|
||||
## 5. În afara scope-ului
|
||||
|
||||
- Refresh-ul tokenului (funcționează, rămâne neschimbat).
|
||||
- Apelurile API eFactura (upload, listare mesaje, descărcare) — neschimbate.
|
||||
- Stocarea tokenurilor în ROACONT (`scrie_optiune`/`citeste_optiune`) — neschimbată.
|
||||
- Rotația `client_id`/`client_secret` ANAF.
|
||||
- Formularul `anaf_token` (rămâne neschimbat, ca fallback manual — e VCX binar, editabil
|
||||
doar din IDE-ul VFP; butonul existent apelează deja `newToken`, deci fluxul nou nu
|
||||
necesită modificări de formular).
|
||||
|
||||
## 6. Riscuri și mitigări
|
||||
|
||||
| Risc | Impact | Mitigare |
|
||||
|---|---|---|
|
||||
| ANAF nu returnează `state` în callback | Fără impact pe legare (aceasta e prin sesiune) | FR-2: legarea primară e sesiunea PHP (cookie, același browser face ambele request-uri); `state` e doar validare CSRF oportunistă |
|
||||
| Utilizatorul are browser default cu profil fără certificat | Autorizarea eșuează în browser | Mesaj + fallback manual (FR-8); comportament identic cu azi |
|
||||
| Două stații generează token simultan | Fișiere separate per `state`, fără coliziune | Entropia `state` (NFR-1) |
|
||||
| Firewall/proxy blochează polling-ul | Timeout | FR-8 fallback manual (validat implicit: refreshToken face deja apeluri identice către romfast.ro) |
|
||||
| Fișier de token orfan în `oauth2_tokens/` (utilizator abandonează) | Token valid rămas pe disc | TTL 10 min (FR-5) + ștergere la citire |
|
||||
| Cookie de sesiune pierdut la redirectul cross-site (SameSite=Strict, browser agresiv) | Legarea primară pică silențios → timeout | `SameSite=Lax; Secure` explicit (FR-2) + verificat la spike (Faza 0) |
|
||||
| Migrare hosting Apache→nginx ignoră `.htaccess` | Director de tokenuri devine public | Stocare în afara docroot-ului ca variantă principală (FR-11) |
|
||||
| ANAF respinge parametrul `state` la authorize | Fluxul nou nu pornește | Spike Faza 0 detectează; fallback: flux doar pe sesiune, fără state în URL-ul ANAF |
|
||||
| Link forjat cu state-ul atacatorului (CSRF de autorizare) | Victima autorizează, atacatorul culege tokenul | Risc rezidual acceptat: cere ca victima să parcurgă deliberat PIN-ul; TTL scurt + citire unică + log forensics (FR-11); clasă de risc echivalentă cu phishing-ul pe fluxul manual actual |
|
||||
|
||||
## 7. Criterii de acceptanță
|
||||
|
||||
1. Pe o stație cu token USB funcțional: utilizatorul apasă "Token nou" în ROA,
|
||||
introduce PIN-ul în browser, iar în maximum ~10 secunde ROA afișează
|
||||
"S-a generat token-ul, valabil până la <data>" — **fără nicio operație de copy/paste**.
|
||||
2. Tokenul și refresh tokenul salvate în baza de date sunt identice cu cele emise de ANAF
|
||||
(verificabil printr-un apel API eFactura reușit imediat după).
|
||||
3. Anularea cu Esc oprește polling-ul fără eroare.
|
||||
4. Timeout după 10 minute → mesaj clar + instrucțiuni fallback manual.
|
||||
5. Un client cu versiune veche ROACONT (fără `state`) vede exact pagina actuală cu
|
||||
tokenurile afișate (regresie zero).
|
||||
6. `pick.php` returnează 404/`pending` după prima citire (citire unică) și 400 la
|
||||
`state` malformat.
|
||||
7. Ramura refresh (`?refresh_token=...`) funcționează identic cu înainte.
|
||||
|
||||
## 8. Fișiere afectate
|
||||
|
||||
| Fișier | Modificare |
|
||||
|---|---|
|
||||
| `D:\PROIECTE\SITE_ROMFAST_2025\oauth2\index.php` | propagare `state`, legare primară prin sesiune (state = CSRF oportunist), stocare tokenuri extra-docroot, pagină succes, curățare securitate (FR-10) |
|
||||
| `D:\PROIECTE\SITE_ROMFAST_2025\oauth2\pick.php` | NOU — endpoint polling, citire unică, TTL, rate-limit, log evenimente |
|
||||
| `D:\ROA\ROACONT\COMUN\programe\anaf_efactura.prg` | `newToken`: implementare unică în `AnafeFacturaServer` (state + browser + polling + salvare automată + gardă headless + kill switch); clasa `ANAFeFactura` deleagă; mesaje actualizate |
|
||||
| `D:\PROIECTE\SITE_ROMFAST_2025\oauth2_tokens\` (extra-docroot) | NOU — stocare temporară tokenuri (FR-11) |
|
||||
|
||||
---
|
||||
|
||||
# REVIZIE CEO (via /autoplan, 2026-07-07) — Faza 1
|
||||
|
||||
Mod: SELECTIVE EXPANSION. Voci: [subagent-only] — Codex CLI indisponibil.
|
||||
Premise confirmate de utilizator la poarta D2, cu verificarea P4 efectuată
|
||||
(doc oficial ANAF: „State se lasă necompletat" → FR-2 inversat pe sesiune-primar).
|
||||
Planul CEO complet: `~/.gstack/projects/ROACONT/ceo-plans/2026-07-07-efactura-oauth2-autoflow.md`.
|
||||
|
||||
## Arhitectură (Secțiunea 1)
|
||||
|
||||
```
|
||||
ROACONT (VFP, per stație) romfast.ro (PHP) ANAF
|
||||
┌────────────────────────┐ ┌─────────────────────────┐ ┌──────────────┐
|
||||
│ AnafeFacturaServer │ 1. gen │ index.php │ 2. 302 │ logincert │
|
||||
│ .newToken() │ state ───▶│ session[roa_state] │─────────▶│ /authorize │
|
||||
│ (gardă headless, │ browser │ (+state la ANAF, CSRF) │ │ (mTLS + PIN) │
|
||||
│ kill switch) │ │ │◀─ 3. cb ─│ │
|
||||
│ │ │ code→token POST ───────┼─────────▶│ /token │
|
||||
│ bucla polling ◀───────┼─ 4. GET ──▶│ pick.php │ └──────────────┘
|
||||
│ 2s/10min, INKEY(0.5) │ JSON │ ../oauth2_tokens/ │
|
||||
│ nfjsonread→SaveToken │ │ <hash(state)>.json │
|
||||
└────────────────────────┘ │ TTL 10m, citire unică │
|
||||
ANAFeFactura.newToken ──deleagă──▶ │ rate-limit 60/min/IP │
|
||||
└─────────────────────────┘
|
||||
```
|
||||
|
||||
Mașina de stări a rezultatului (server): `PENDING` (fără fișier) → `READY` (fișier scris
|
||||
la callback) → `CONSUMED` (șters la prima citire) sau `EXPIRED` (TTL 10 min). Tranziții
|
||||
invalide prevenite prin ștergere-la-citire + TTL; eroarea nu suprascrie `READY` (FR-10).
|
||||
|
||||
Constatări: cuplare nouă VFP↔pick.php — URL-ul `pick.php` se derivă din aceeași bază
|
||||
configurată `ANAF_URL_TOKEN` folosită de `cTokenUrl` (o singură sursă, fallback hardcodat).
|
||||
SPOF: romfast.ro (pre-existent, identic cu fluxul actual). Scalare: ~zeci de generări/zi
|
||||
la vârf — nicio problemă. Rollback: kill switch (FR-7) + retro-compat PHP (FR-4).
|
||||
|
||||
## Registru erori & recuperare (Secțiunea 2)
|
||||
|
||||
| Codepath | Ce poate eșua | Tratare | Utilizatorul vede |
|
||||
|---|---|---|---|
|
||||
| index.php intrare | sesiune indisponibilă / state malformat | validare strictă format; eroare explicită | pagină de eroare clară |
|
||||
| ANAF authorize | utilizator anulează / cert respins → `?error=` | FR-10: pagină eroare + fișier `{"error"}` pentru state | mesaj în browser + ROA oprește polling-ul cu mesaj |
|
||||
| callback fără code și fără error | buclă redirect (bug actual) | FR-10: detectare și pagină de eroare, fără re-redirect | mesaj clar, nu buclă |
|
||||
| code→token (curl) | timeout/eroare rețea/HTTP≠200/JSON eroare (code expirat >60s) | FR-10: verificare curl_errno + status + câmp `error`; scriere fișier eroare | mesaj în browser + ROA afișează eroarea |
|
||||
| pick.php | state malformat → 400; necunoscut/negenerat → 200 pending; 429 la abuz | FR-5/FR-11 | ROA: continuă (2s→5s la 429) / abandonează cu mesaj |
|
||||
| VFP polling | eroare rețea tranzitorie / iterație lentă | continuă bucla; SetTimeouts(5s) previne înghețarea Esc | nimic (transparent) |
|
||||
| VFP polling | HTTP 404 ≥5 consecutiv (pick.php nedesfășurat) | M10 + fallback | mesaj + flux manual |
|
||||
| VFP parse JSON | răspuns corupt (fișier parțial imposibil — scriere atomică FR-11) | abort + mesaj + fallback manual | mesaj de eroare |
|
||||
| VFP buclă | excepție în helper HTTP (ambele transporturi) | TRY imbricat + resetare gardă în FINALLY | mesaj de eroare, fără blocare pe M8 |
|
||||
| SaveToken (Oracle) | eșec scriere | M11 cu retry — tokenul e încă în memorie; după 3 eșecuri → reluare flux | dialog Da/Nu |
|
||||
| pick→răspuns pierdut pe transport | tokenul șters pe server, neprimit de client | risc asumat (rar): recuperare = reluare flux (M5) | timeout + mesaj |
|
||||
| Fallback manual în același browser | sesiune stale ar redirecționa pe pagina fără tokenuri | FR-2: unset roa_state la intrarea fără state + timestamp 15 min | pagina cu tokenuri, corectă |
|
||||
| Esc / timeout 10 min | abandon | curățare stare locală; TTL curăță serverul | mesaj cu opțiuni (reluare / manual) |
|
||||
|
||||
**GAP-uri pre-existente închise de FR-10:** eșec curl afișa tokenuri goale; callback de
|
||||
eroare ANAF intra în buclă de redirect. Zero GAP-uri rămase cunoscute.
|
||||
|
||||
## Securitate & threat model (Secțiunea 3)
|
||||
|
||||
| Amenințare | Prob. | Impact | Mitigare |
|
||||
|---|---|---|---|
|
||||
| Ghicire state pe pick.php | Foarte mică (128 biți) | Mare | entropie FR-6, rate-limit, răspuns uniform pending |
|
||||
| Interceptare state local (istoric browser) | Mică | Mare | fereastră secunde (ROA culege imediat), citire unică, TTL |
|
||||
| CSRF autorizare cu state forjat | Mică-medie | Mare | risc rezidual acceptat, dar MAI GRAV decât phishing-ul pe fluxul manual (exfiltrare silențioasă vs. copiere activă de către victimă); mitigare forensic: IP callback stocat în JSON + log `pickup_ip_mismatch` (FR-11) |
|
||||
| `state` în access-log-urile hostingului | Mică | Mare | `state` prin POST body la pick.php (FR-5); la index.php rămâne în URL (necesar pentru flux) — fereastră de secunde + delete-on-read |
|
||||
| Injecție în log prin state invalid | Mică | Mică | validare format înainte de orice logare + prefix sanitizat [A-Za-z0-9] (FR-5/FR-11) |
|
||||
| Expunere director tokenuri la migrare hosting | Medie | Mare | stocare extra-docroot (FR-11) |
|
||||
| client_secret în istoric browser | — (pre-existent) | Mare | eliminat din authorize URL (FR-10) |
|
||||
| Injecție variabile parse_str | — (pre-existent) | Medie | eliminat (FR-10) |
|
||||
| info.php / fișiere moarte | — (pre-existent) | Medie | eliminate/arhivate (FR-10) |
|
||||
|
||||
## Edge cases date/interacțiune (Secțiunea 4) — mapate, toate tratate
|
||||
Dublă lansare newToken (state-uri independente, ultimul salvat câștigă) · browser închis
|
||||
înainte de PIN (timeout+mesaj) · refresh pe pagina de callback (code single-use → eroare,
|
||||
anti-suprascriere FR-10) · abandon ROA + finalizare browser (TTL curăță) · Esc în timpul
|
||||
polling (curat) · două stații simultan (state-uri distincte) · ceas server (TTL pe mtime local).
|
||||
|
||||
## Calitate cod (Secțiunea 5)
|
||||
Implementare unică `newToken` în clasa server + delegare (decis, FR-7) · helper HTTP GET
|
||||
JSON nou, reutilizând pattern-ul WinHttp→MSXML din `refreshToken` (fără refactorizarea
|
||||
acestuia — diff minim) · numele `pick.php` OK · complexitate buclă polling ținută sub
|
||||
control prin extragerea parsării în helper.
|
||||
|
||||
## Teste (Secțiunea 6) — detaliat în Faza 3 (Eng); cerințe CEO:
|
||||
Spike Faza 0 obligatoriu · test E2E cu certificat real (criteriu de acceptanță #1) ·
|
||||
pick.php testabil complet cu curl (toate ramurile) · index.php ramuri de eroare testabile
|
||||
fără certificat (code fals → eroare ANAF) · test empiric propagare state (logat).
|
||||
|
||||
## Performanță (Secțiunea 7)
|
||||
≤300 cereri pick/generare (10 min @2s), o generare la ~90 zile/client — neglijabil.
|
||||
INKEY(0.5) păstrează UI-ul responsiv. Fără probleme.
|
||||
|
||||
## Observabilitate (Secțiunea 8)
|
||||
Log evenimente server (FR-11, fără tokenuri) + `This.Log` la fiecare tranziție în VFP +
|
||||
contoare zilnice succes/eroare/timeout (măsurarea adopției — încadrarea „reducere tichete
|
||||
suport" devine măsurabilă). Runbook: eșec raportat → log server + log ROA reconstruiesc
|
||||
fluxul după prefixul de state.
|
||||
|
||||
## Deployment (Secțiunea 9)
|
||||
Ordine obligatorie: 1) PHP (retro-compat, FR-4) → 2) verificare curl → 3) exe ROACONT.
|
||||
Fereastră mixtă exe-nou/PHP-vechi: detectată de client (HTTP 404 pe pick.php) → M10 + fallback.
|
||||
Precondiții verificate la spike: PHP ≥ 7.3, comportamentul 404 al hostingului, SameSite.
|
||||
Kill switch ANAF_TOKEN_AUTOFLOW pentru rollback fără redistribuire. Post-deploy: curl
|
||||
pick.php cu state fals → `{"status":"pending"}`; index.php fără parametri → 302 către ANAF.
|
||||
|
||||
## Traiectorie (Secțiunea 10)
|
||||
Reversibilitate 4/5 · datorie tehnică minimă (stocare pe fișiere OK la scara actuală) ·
|
||||
potențial de platformă: aceeași infrastructură poate servi celelalte produse ROA
|
||||
(COMUNROA) — în TODOS · întrebarea de 1 an: PRD + comentarii în cod suficiente.
|
||||
|
||||
## Design & UX (Secțiunea 11) — CEO-level; adâncime în Faza 2
|
||||
Harta stărilor: LOADING (wait window cu Esc) / ERROR (timeout·ANAF·rețea, mesaje distincte)
|
||||
/ SUCCESS (data expirării) / PARTIAL (browser OK, pick eșuat → fallback manual).
|
||||
Pagina browser: succes (revenire în ROA) și eroare (detaliu + reluare). Arc emoțional:
|
||||
anxietate → ghidare → confirmare.
|
||||
|
||||
## Tabel consens voci CEO (Codex indisponibil → [subagent-only])
|
||||
|
||||
| Dimensiune | Claude (principal) | Subagent independent | Consens |
|
||||
|---|---|---|---|
|
||||
| Premise valide? | DA (validate la gate) | PARȚIAL (cerea verificări) | REZOLVAT — spike Faza 0 + SameSite + extra-docroot adoptate |
|
||||
| Problema corectă? | DA | DA | CONFIRMAT |
|
||||
| Calibrare scope? | DA | DA (cu fix client_secret) | CONFIRMAT — FR-10 adoptat |
|
||||
| Alternative explorate? | PARȚIAL | PARȚIAL | REZOLVAT — broker + WebView2 documentate |
|
||||
| Riscuri competitive? | neacoperit | NU | REZOLVAT — încadrare igienă/paritate adăugată |
|
||||
| Traiectorie 6 luni? | DA | PARȚIAL | REZOLVAT — telemetrie + securitate hosting adoptate |
|
||||
|
||||
## NOT in scope / Ce există deja / Dream state — vezi §5 PRD și planul CEO (leverage map complet acolo).
|
||||
|
||||
## Decision Audit Trail (Faza 1)
|
||||
|
||||
| # | Faza | Decizie | Clasificare | Principiu | Rațiune | Respins |
|
||||
|---|------|---------|-------------|-----------|---------|---------|
|
||||
| 1 | CEO | Abordare A (polling state+sesiune) vs B (protocol handler) vs C (WebView2) | Mecanică | P1 | Completitudine 9/10, zero deployment per stație; B/C respinse empiric | B, C |
|
||||
| 2 | CEO | Cross-project learnings activat | Mecanică | P6 | Dezvoltator solo, produse ROA înrudite | — |
|
||||
| 3 | CEO | Sesiune primară, state doar CSRF (FR-2 inversat) | Mecanică | P1/P5 | Doc oficial ANAF nu garantează state; sesiunea funcționează oricum | state-primar |
|
||||
| 4 | CEO | Curățare securitate index.php (FR-10) | Mecanică | P2 | Blast radius (fișier oricum modificat), <1 zi, severitate high | amânare |
|
||||
| 5 | CEO | Rate-limit + log evenimente pick.php (FR-11) | Mecanică | P1/P2 | Observabilitate = scope; S effort | fără log |
|
||||
| 6 | CEO | Buton nou în formularul anaf_token | Mecanică | P4 | Duplicat — butonul existent apelează newToken; VCX binar | adăugare buton |
|
||||
| 7 | CEO | Notificare expirare token | Mecanică | P4 | Duplicat — RefreshTokenAuto există | — |
|
||||
| 8 | CEO | Auto-lansare flux la eșec definitiv refresh | DECIS la gate (D3) | — | Utilizatorul o implementează direct în VFP; în afara planului | includere în plan |
|
||||
| 9 | CEO | Timeout 10 min (nu 3) | Mecanică | P1 | Consistență cu TTL; utilizatori netehnici | 3 min |
|
||||
| 10 | CEO | Kill switch ANAF_TOKEN_AUTOFLOW | Mecanică | P2 | Rollback fără exe; deploy SVN lent | fără flag |
|
||||
| 11 | CEO | Stocare extra-docroot | Mecanică | P1 | Migrarea hosting nu trebuie să producă leak | doar .htaccess |
|
||||
| 12 | CEO | state doar din CoCreateGuid/BCryptGenRandom | Mecanică | P1 | SYS(2015) predictibil; state = secret unic | SYS(2015) |
|
||||
| 13 | CEO | Spike Faza 0 validare ANAF | Mecanică | P3/P6 | 1 oră care decide FR-2 înainte de a construi restul | build-then-learn |
|
||||
| 14 | CEO | Implementare unică newToken + delegare | Mecanică | P4/P5 | Două implementări diverg în timp | duplicare |
|
||||
| 15 | CEO | Token broker | Mecanică (defer) | P3 | Decizie de business (GDPR/răspundere) — TODOS | adoptare acum |
|
||||
| 16 | CEO | Delete-on-read păstrat (fără two-phase confirm) | Mecanică | P3/P5 | Eșecul SaveToken e rar; recuperare = reluare flux; two-phase adaugă complexitate | two-phase pickup |
|
||||
| 17 | CEO | Risc CSRF autorizare = rezidual acceptat | Mecanică | P3 | Cere PIN deliberat al victimei; echivalent phishing flux manual; forensics în log | pickup_secret suplimentar |
|
||||
|
||||
# REVIZIE ENG (via /autoplan, 2026-07-07) — Faza 3
|
||||
|
||||
Voci: [subagent-only] (Codex indisponibil). Subagentul independent a citit codul real
|
||||
(anaf_efactura.prg 447-586/2690-2746, index.php, oexport.prg:302) și a returnat 18
|
||||
constatări (1 critică, 5 high, 7 medium, 5 low) — TOATE adoptate prin auto-decizie și
|
||||
propagate în FR-2/FR-5/FR-6/FR-7/FR-11, în tabelele de mesaje, registre și threat model.
|
||||
Punctul C9 (comportamentul headless al `AnafeFacturaServer.newToken`, care azi
|
||||
instruiește completarea în `gcGeneralIniFile`) → DECIZIE DE GUST la poarta finală.
|
||||
|
||||
## Constatări cheie adoptate (cu confidence)
|
||||
|
||||
| # | Constatare | Conf. | Rezolvare |
|
||||
|---|---|---|---|
|
||||
| C1 | [P1] Sesiune stale rupe fallback-ul manual (același browser, roa_state rămas → pagina de succes fără tokenuri = fund de sac) | 9/10 | FR-2: unset la intrarea fără state + timestamp 15 min + test #18 |
|
||||
| C2 | [P1] Două instanțe ROACONT (firme diferite) → coliziune sesiune, tokenul firmei A salvat la firma B | 8/10 | FR-2: avertisment pe pagină la state proaspăt neconsumat; test #20 |
|
||||
| C3 | [P1] Scriere fișier non-atomică → pick citește JSON parțial → PIN refăcut degeaba | 8/10 | FR-11: temp+rename ambele direcții (write și claim) |
|
||||
| C4 | [P1] WinHttp default 30-60s → Esc mort la rețea căzută; XMLHTTP nu are setTimeouts | 9/10 | FR-7: SetTimeouts(5s×4) + fallback ServerXMLHTTP |
|
||||
| C5 | [P1] Pattern TRY/CATCH din refreshToken:536-547 lasă excepția din CATCH nehandled → garda blocată pe M8 | 9/10 | FR-7: TRY imbricat + FINALLY resetează garda |
|
||||
| C6 | [P1] open_default_app (oexport.prg:302) nu returnează nimic → M9 neimplementabil | 9/10 | FR-7: wrapper local ShellExecute>32 |
|
||||
| C7 | [P2] 60/min/IP fără headroom NAT (2 stații = exact limita) | 8/10 | FR-11: 240/min/IP + contor pe fișiere + 5s persistent |
|
||||
| C8 | [P2] 404-pending fragil pe shared hosting (ErrorDocument suprascrie corpul) | 8/10 | FR-5: pending = 200; 404 devine semnal pur pentru M10 |
|
||||
| C10-C11 | [P2] PHP≥7.3 necertificat; session lock ținut în timpul curl | 8/10 | FR-2 spike + FR-11 session_write_close + curl timeouts |
|
||||
| C12-C15 | [P2/P3] CSRF mai grav decât echivalarea inițială; state în access-log; log injection; nume fișier | 7-8/10 | threat model actualizat; POST body; hash(state) întotdeauna; sanitizare |
|
||||
| C16-C18 | [P3] răspuns pierdut după delete; NVL la kill switch; M11 retry (tokenul încă în memorie) | 8/10 | registru + FR-7 + M11 rescris cu retry |
|
||||
|
||||
## Diagramă acoperire teste (Secțiunea 3 Eng)
|
||||
|
||||
```
|
||||
CĂI DE COD FLUXURI UTILIZATOR
|
||||
[+] oauth2/index.php [+] Generare token automată
|
||||
├── intrare fără parametri (302, fără secret) ├── [TEST #11] happy path E2E cu USB
|
||||
├── intrare cu state (+sesiune, SameSite) ├── [TEST #12] Esc înainte/după PIN
|
||||
├── intrare fără state → unset roa_state [#9,18] ├── [TEST #3,#13,#19] timeout + rețea moartă
|
||||
├── state peste state proaspăt → avertisment[#20] ├── [TEST #14] exe nou + PHP vechi → M10
|
||||
├── callback ?code (schimb OK → fișier atomic) ├── [TEST #5,#16,#24] kill switch/firmă
|
||||
├── callback ?code fals → {"error"} [#6] ├── [TEST #6] gardă reintrare M8
|
||||
├── callback ?error= → {"error"}, fără buclă [#7] ├── [TEST #8,#23] browser eșuat/fără cert
|
||||
├── anti-suprascriere succes [#6] └── [TEST #17,#20] dublă instanță/reluare
|
||||
└── ?refresh_token → REGRESIE [#12,curl] [+] Fallback manual
|
||||
[+] oauth2/pick.php ├── [TEST #3-Nu] pagina CU tokenuri (C1!)
|
||||
├── POST state valid+fișier → claim+200+șterge └── [TEST #13-form] completare manuală OK
|
||||
├── pending 200 uniform [#1,#3] [+] Server headless
|
||||
├── malformat → 400 fără log injection [#2] └── [TEST #9-headless] refuz/flux INI (C9)
|
||||
├── TTL cleanup [#4] · rate limit 429 [#5,#22]
|
||||
└── fără session_start
|
||||
[+] anaf_efactura.prg (newToken unificat + helper HTTP + wrapper ShellExecute)
|
||||
├── state CoCreateGuid [#11-format] · polling 2s/5s/10min [#3,#19,#22]
|
||||
├── mesaje M1-M12 progresive [#4] · SaveToken retry [#21] · delegare client→server
|
||||
└── TRY imbricat + FINALLY gardă [#19 implicit]
|
||||
ACOPERIRE: 25/25 căi au test în matricea manuală (VFP #11-25, curl #1-10) — 100% din
|
||||
căile noi; fără test runner automat (constrângere de proiect), matricea e scriptată
|
||||
curl unde se poate și manuală unde nu.
|
||||
```
|
||||
|
||||
Artefact plan de teste (consumat de /qa): `~/.gstack/projects/ROACONT/mmari-main-eng-review-test-plan-20260707-123000.md`
|
||||
|
||||
## Paralelizare implementare (lanes)
|
||||
|
||||
| Pas | Module atinse | Depinde de |
|
||||
|---|---|---|
|
||||
| Spike Faza 0 | oauth2/ (index.php minim) | — |
|
||||
| PHP complet (FR-1..5, 10, 11) | oauth2/ | Spike (decide detaliile FR-2) |
|
||||
| VFP (FR-6..9, §3.3) | COMUN/programe/ | Spike (contractul e fixat de PRD; poate începe în paralel cu PHP) |
|
||||
| Deploy + verificare | ambele | PHP + VFP |
|
||||
|
||||
Lane A: Spike → PHP complet (secvențial, același fișier). Lane B: VFP (paralel cu PHP
|
||||
după spike). Merge: deploy ordonat PHP→exe. Fără conflicte (module disjuncte).
|
||||
|
||||
## Tabel consens voci Eng ([subagent-only])
|
||||
|
||||
| Dimensiune | Claude (principal) | Subagent independent | Consens |
|
||||
|---|---|---|---|
|
||||
| Arhitectură solidă? | DA (cu fixurile adoptate) | PARȚIAL (C1-C3) | REZOLVAT — C1-C3 adoptate în FR-2/FR-11 |
|
||||
| Acoperire teste suficientă? | PARȚIAL (matrice de bază) | PARȚIAL (lipsă edge) | REZOLVAT — matricea extinsă la 25 teste |
|
||||
| Riscuri performanță? | DA (volum trivial) | PARȚIAL (C4, C7) | REZOLVAT — SetTimeouts + 240/min |
|
||||
| Securitate acoperită? | DA (FR-10/11) | PARȚIAL (C12-C15) | REZOLVAT — threat model extins, POST body, hash, sanitizare |
|
||||
| Căi de eroare tratate? | DA (registru 11 căi) | PARȚIAL (C1, C5, C6) | REZOLVAT — registru extins la 14 căi, 0 GAP critice |
|
||||
| Risc deployment gestionabil? | DA | DA | CONFIRMAT |
|
||||
|
||||
## Decision Audit Trail (Faza 3)
|
||||
|
||||
| # | Faza | Decizie | Clasificare | Principiu | Rațiune | Respins |
|
||||
|---|------|---------|-------------|-----------|---------|---------|
|
||||
| 27 | Eng | Igienă sesiune: unset la intrare fără state + timestamp 15 min | Mecanică | P1/P5 | C1 (9/10): fallback-ul manual nu trebuie să moștenească sesiunea tentativei eșuate | ignorare |
|
||||
| 28 | Eng | Avertisment la suprascriere de state proaspăt (multi-instanță) | Mecanică | P5 | C2: suprascrierea tăcută trimite tokenul firmei A la firma B | listă de state-uri (fără state în callback nu discriminezi) |
|
||||
| 29 | Eng | Scriere atomică temp+rename + claim prin rename la pickup | Mecanică | P1 | C3: JSON parțial = PIN refăcut degeaba; cost o linie | write direct |
|
||||
| 30 | Eng | SetTimeouts(5s×4) + fallback ServerXMLHTTP.6.0 | Mecanică | P1/P5 | C4: default 30-60s îngheață Esc; XMLHTTP nu are setTimeouts | XMLHTTP |
|
||||
| 31 | Eng | TRY imbricat + resetare gardă în FINALLY | Mecanică | P1 | C5: excepția din CATCH ar bloca aplicația pe M8 | pattern refreshToken |
|
||||
| 32 | Eng | Wrapper local ShellExecute>32 (nu modificăm open_default_app global) | Mecanică | P3/P5 | C6: helper-ul global nu returnează nimic și e folosit în zeci de locuri | modificare globală |
|
||||
| 33 | Eng | Rate limit 240/min/IP, contor pe fișiere, 5s persistent după 429 | Mecanică | P1 | C7: birou cu >2 stații după NAT ar lovi 60/min exact la deploy | 60/min |
|
||||
| 34 | Eng | pending = HTTP 200; 404 doar pentru pick.php lipsă | Mecanică | P5 | C8: hostingurile suprascriu corpul 404 → M10 fals pozitiv | 404-pending |
|
||||
| 35 | Eng | Verificare PHP≥7.3 la spike + fallback SameSite hack | Mecanică | P6 | C10: sintaxa array nu există sub 7.3 | presupunere |
|
||||
| 36 | Eng | session_write_close înainte de curl + CURLOPT timeouts | Mecanică | P1 | C11: lock de sesiune ținut cât răspunde ANAF | fără timeouts |
|
||||
| 37 | Eng | IP callback în JSON + log pickup_ip_mismatch (fără blocare) | Mecanică | P1 | C12: CSRF-ul e mai grav decât echivalarea inițială; forensic ieftin | blocare strictă (false pozitive VPN) |
|
||||
| 38 | Eng | hash('sha256', state) ca nume fișier ÎNTOTDEAUNA | Mecanică | P5 | C13: rezolvă și contradicția FR-3 vs diagramă; secret scos din listing | doar în fallback |
|
||||
| 39 | Eng | state prin POST body la pick.php | Mecanică | P1 | C14: scoate secretul din access-log-urile hostingului | GET query |
|
||||
| 40 | Eng | Validare înainte de logare + prefix sanitizat | Mecanică | P1 | C15: anti log-injection prin CR/LF în state invalid | logare brută |
|
||||
| 41 | Eng | Risc „răspuns pierdut după delete" documentat ca asumat | Mecanică | P3 | C16: two-phase respins deja (#16); recuperare = M5 | two-phase |
|
||||
| 42 | Eng | NVL + default 1 la citirea kill switch; scope per firmă intenționat | Mecanică | P5 | C17: pattern-ul liniei 462 crapă pe NULL | copiere pattern |
|
||||
| 43 | Eng | M11 cu retry SaveToken (tokenul încă în This.cToken) | Mecanică | P1 | C18: reluarea PIN-ului e disproporționată pentru un eșec de scriere DB | doar mesaj |
|
||||
| 44 | Eng | Comportament headless server (INI flow vs refuz) | DECIS la gate (D4) | — | Utilizatorul a ales: se păstrează mecanismul INI actual, neatins | refuz simplu |
|
||||
|
||||
## Sumar completare Faza 3 (Eng)
|
||||
|
||||
```
|
||||
Scope challenge: 4 fișiere, 0 clase noi — fără declanșare | Cod citit: prg (4 funcții), index.php, oexport.prg
|
||||
S1 Arhitectură: 6 constatări critice/high (C1-C6) — toate adoptate | S2 Calitate: helper HTTP nou,
|
||||
wrapper local, dedup cu decizie C9 la gate | S3 Teste: diagramă 25 căi / 25 acoperite, artefact
|
||||
scris pe disc | S4 Performanță: C4+C7 adoptate, volum trivial confirmat
|
||||
Registru erori: 14 căi, 0 GAP critice | Failure modes: toate cu tratare + test + mesaj vizibil
|
||||
Paralelizare: 2 lanes după spike | Voci: [subagent-only], 18 constatări, consens 6/6 după adoptare
|
||||
Decizii: 17 mecanice adoptate (#27-43), 1 taste la gate (#44) | Lake Score: 17/17 varianta completă
|
||||
```
|
||||
|
||||
# REVIZIE DESIGN (via /autoplan, 2026-07-07) — Faza 2
|
||||
|
||||
Voci: [subagent-only] (Codex indisponibil). Designer vizual indisponibil (necesită bun)
|
||||
→ review text-only, permis de skill. DESIGN.md inexistent — sistemul de design efectiv:
|
||||
dialoguri Windows clasice VFP (fără diacritice) + pagini server simple (UTF-8).
|
||||
|
||||
## Scoruri pe pase (înainte → după fixuri)
|
||||
|
||||
| Pasa | Scor | Ce lipsea → ce s-a adăugat |
|
||||
|---|---|---|
|
||||
| 1. Ierarhia informației | 6 → 9 | ghidarea era integral front-loaded în modalul inițial; mutată progresiv în WAIT WINDOW (M2-M4), pasul de selecție certificat numit explicit |
|
||||
| 2. Acoperirea stărilor | 6 → 9 | adăugate: eșec lansare browser (M9), browser ascuns (M3), reintrare (M8), kill-switch (mesaj vechi), Esc după PIN (M6), eșec SaveToken (M11) |
|
||||
| 3. Arc emoțional | 6 → 9 | storyboard mai jos; punctele de rupere fixate: dialog certificat anunțat (M1), gol de feedback umplut (M3/M4), fallback cu avertisment PIN (FR-8) |
|
||||
| 4. Specificitate (AI slop) | 5 → 9 | 12 texte finale definite verbatim (§3.3) + layout pagini browser (un job/pagină, H1 scanabil, fără retry în browser) |
|
||||
| 5. Aliniere sistem design | 7 → 8 | regula diacriticelor decisă (VFP fără, PHP cu + UTF-8) — consistent cu codul existent; fără DESIGN.md (nejustificat pentru 1 pagină server) |
|
||||
| 6. Responsive & accesibilitate | 5 → 8 | pagini browser: ≥16px, contrast înalt, zero interacțiune necesară, fără motion; VFP: Esc <1s (INKEY 0.5) |
|
||||
| 7. Decizii nerezolvate | 6 rezolvate, 0 amânate | F1 fallback fără state, F5 diacritice, F6 job pagină eroare, F7 OK/Anulare, F8 reintrare, F9 kill-switch |
|
||||
|
||||
**Scor design general: 6/10 → 9/10.**
|
||||
|
||||
## Storyboard călătorie utilizator (Pasa 3)
|
||||
|
||||
| Pas | Utilizatorul face | Simte | Planul susține cu |
|
||||
|---|---|---|---|
|
||||
| 1 | Apasă „Token nou" | incertitudine („iar tokenul...") | M1: 3 pași numerotați, promisiunea „fara copiere manuala", opțiune Anulare |
|
||||
| 2 | Introduce tokenul USB | teamă de dialoguri tehnice | M1 anunță explicit selecția certificatului |
|
||||
| 3 | Browser: selectează certificat, PIN | anxietate maximă | pagina ANAF (nu a noastră); M2 în ROA numește exact acțiunea |
|
||||
| 4 | Așteaptă / revine în ROA | „a mers?" | pagina de succes cu un singur mesaj + M3/M4 progresive în ROA |
|
||||
| 5 | Vede confirmarea | ușurare, încredere | M7 cu data expirării; zero copy/paste |
|
||||
| 5' | (eșec) | frustrare | M5/M6/M10-M12: cauză în limbaj uman + următorul pas concret, niciodată fund de sac |
|
||||
|
||||
## Litmus scorecard (voci design — [subagent-only])
|
||||
|
||||
| Verificare (pagini browser) | Claude | Codex | Consens |
|
||||
|---|---|---|---|
|
||||
| Headline scanabil dintr-o privire? | DA (după fix F6) | N/A | DA (post-fix) |
|
||||
| Un job per pagină? | DA (după eliminarea retry din pagina de eroare) | N/A | DA (post-fix) |
|
||||
| Acțiune următoare clară? | DA („Reveniți în aplicația ROA" pe ambele) | N/A | DA (post-fix) |
|
||||
| Hard rejections declanșate | 0 (pagini minimale, fără carduri/decor) | N/A | 0 |
|
||||
|
||||
Constatările subagentului: 1 critică (F1 — fallback cu `state` = fund de sac), 5 high
|
||||
(F2-F6), 4 medium (F7-F10). Toate structurale, toate rezolvate prin editările §3.3 +
|
||||
FR-7/FR-8/FR-9. Zero decizii de gust rămase pentru gate.
|
||||
|
||||
## Decision Audit Trail (Faza 2)
|
||||
|
||||
| # | Faza | Decizie | Clasificare | Principiu | Rațiune | Respins |
|
||||
|---|------|---------|-------------|-----------|---------|---------|
|
||||
| 18 | Design | Fallback manual = URL fără `state` + avertisment PIN | Mecanică | P1 | F1 critică: cu `state`, pagina nu afișează tokenuri = fund de sac | reluare cu state |
|
||||
| 19 | Design | Mesaje VFP fără diacritice, pagini PHP UTF-8 cu diacritice | Mecanică | P5 | Codepage 1250/852 corupe diacriticele; consistent cu tot codul existent | diacritice în VFP |
|
||||
| 20 | Design | Mesaje progresive în WAIT WINDOW (M2-M4) | Mecanică | P1 | Fereastră statică 10 min = aplicație aparent blocată | text static |
|
||||
| 21 | Design | 12 texte finale definite în PRD (M1-M12 + traduceri erori ANAF) | Mecanică | P1/P5 | „Textul exact" nu se lasă implementatorului; momentul emoțional prost (M11) cere formulare atentă | parafrazare |
|
||||
| 22 | Design | Modal inițial OK/Anulare + anunțarea selecției certificatului | Mecanică | P1 | Utilizatorul fără USB la îndemână trebuie să poată renunța curat | OK-only |
|
||||
| 23 | Design | Pagina de eroare: un job, fără retry în browser | Mecanică | P5 | Retry în browser nu poate funcționa (state nou cere aplicația) — buclă de confuzie | detaliu+reluare |
|
||||
| 24 | Design | Gardă de reintrare + verificare lansare browser | Mecanică | P1 | Stări nespecificate care produc tăcere de 10 min sau haos de bucle | nespecificat |
|
||||
| 25 | Design | Kill-switch afișează mesajul VECHI nemodificat | Mecanică | P5 | Aplicația nu promite automat când pagina afișează tokenuri de copiat | mesaj nou |
|
||||
| 26 | Design | Fără /design-consultation (DESIGN.md) | Mecanică | P3 | O pagină server minimală + dialoguri standard nu justifică sistem de design | consultanță |
|
||||
|
||||
## Sumar completare Faza 2 (Design)
|
||||
|
||||
```
|
||||
Audit: DESIGN.md absent | UI scope: dialoguri VFP + 2 pagini browser | Mockups: indisponibil (text-only)
|
||||
Pasa 1: 6→9 | Pasa 2: 6→9 | Pasa 3: 6→9 | Pasa 4: 5→9 | Pasa 5: 7→8 | Pasa 6: 5→8
|
||||
Pasa 7: 6 rezolvate, 0 amânate | Scor general: 6/10 → 9/10
|
||||
Voci: [subagent-only] — 10 constatări (1 critică, 5 high, 4 medium), toate fixate în PRD
|
||||
Decizii adăugate în plan: 9 (audit #18-26) | Decizii de gust pentru gate: 0
|
||||
```
|
||||
|
||||
## Sumar completare Faza 1 (CEO)
|
||||
|
||||
```
|
||||
Mod: SELECTIVE EXPANSION | Audit sistem: SVN trunk, formular VCX binar, fișiere moarte oauth2/
|
||||
Step 0: premise confirmate (gate D2, P4 verificat empiric → FR-2 inversat pe sesiune)
|
||||
S1 Arhitectură: 1 constatare (bază URL unică) — rezolvată | S2 Erori: 11 căi mapate, 2 GAP-uri
|
||||
pre-existente închise (FR-10), 0 rămase | S3 Securitate: 7 amenințări, 3 high pre-existente
|
||||
fixate (FR-10/FR-11) | S4 Edge cases: 7 mapate, toate tratate | S5 Calitate: dedup newToken
|
||||
decis | S6 Teste: cerințe CEO fixate, detaliu în Faza 3 | S7 Perf: fără probleme |
|
||||
S8 Observabilitate: log evenimente + contoare (FR-11) | S9 Deploy: ordine + kill switch |
|
||||
S10 Traiectorie: reversibilitate 4/5 | S11 Design: harta stărilor, adâncime în Faza 2
|
||||
NOT in scope: 5 iteme | Ce există deja: 7 reutilizări | Registru erori: 11 căi, 0 GAP critice
|
||||
Scope: 11 propuneri, 8 acceptate, 2 respinse (duplicate), 1 PENDING gate (#7)
|
||||
Plan CEO: scris + spec review 3 iterații (8/10 → PASS 9/10, 11 probleme fixate)
|
||||
Voce externă: [subagent-only] (Codex neinstalat) | Lake Score: 8/8 recomandări = varianta completă
|
||||
Diagrame: arhitectură, flux+shadow paths, mașină de stări | Decizii nerezolvate: 1 (gate #7)
|
||||
```
|
||||
(Nota post-gate: itemul #7/#8 din sumar a fost DECIS la poarta finală — vezi audit trail #8 și #44.)
|
||||
|
||||
## GSTACK REVIEW REPORT
|
||||
|
||||
| Review | Trigger | Why | Runs | Status | Findings |
|
||||
|--------|---------|-----|------|--------|----------|
|
||||
| CEO Review | `/plan-ceo-review` | Scope & strategy | 1 | CLEAR (PLAN via /autoplan) | 11 proposals, 8 accepted, 2 deferred; spec review PASS 9/10 |
|
||||
| Codex Review | `/codex review` | Independent 2nd opinion | 0 | — (Codex CLI neinstalat; voci = subagent Claude) | — |
|
||||
| Eng Review | `/plan-eng-review` | Architecture & tests (required) | 1 | CLEAR (PLAN via /autoplan) | 18 issues, 0 critical gaps rămase; acoperire teste 25/25 căi |
|
||||
| Design Review | `/plan-design-review` | UI/UX gaps | 1 | CLEAR (FULL via /autoplan) | score: 6/10 → 9/10, 9 decisions |
|
||||
| DX Review | `/plan-devex-review` | Developer experience gaps | 0 | SKIPPED | fără scope developer-facing (pick.php e endpoint intern) |
|
||||
|
||||
- **VERDICT:** CEO + ENG + DESIGN CLEARED — ready to implement (începând cu spike-ul Faza 0).
|
||||
|
||||
NO UNRESOLVED DECISIONS
|
||||
Reference in New Issue
Block a user