Files
roacont/PRD_efactura_oauth2_autoflow.md
Marius Mutu 0acdff9647 Initial import of ROACONT (Visual FoxPro accounting app) into git
Migrated from the ROACONT SVN trunk working copy for hosting on gitea.romfast.ro.
2026-07-07 13:16:59 +03:00

58 KiB
Raw Blame History

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.prgnewToken) 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)):

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 ( 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 ( 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 . 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: . 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: . 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 " — 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