diff --git a/PRD_efactura_oauth2_autoflow.md b/PRD_efactura_oauth2_autoflow.md deleted file mode 100644 index 25ca31a..0000000 --- a/PRD_efactura_oauth2_autoflow.md +++ /dev/null @@ -1,755 +0,0 @@ - -# 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/.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 `.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 + ``. - -**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 │ │ .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