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