name: google-oauth-callback-impl-spec-2026-05-22 description: Spec near-executable per implementare il flusso OAuth Google (start + callback) in api/reach/, post-MVP. Riusa oauth_connections. Security-sensitive (token cifrati + state CSRF). Pass dedicato. last_update: 2026-05-22 owner: gregorio (decisione schema/cifratura) · Claude (implementazione, pass dedicato) ref: frontend/multiverbe-app/src/lib/reach/adapters/ · database/migrations/2026-05-15h_reach_seo_foundation.sql · memorie feedback_endpoint_auth_robust_getuser + project_api_esbuild_local_validation status: SPEC pronta · NON implementato · non blocca gate 10 Giu (Reach C1b/E1 post-MVP)
Spec · flusso OAuth Google (Search Console E1 + Business Profile C1b)
Stato attuale (verificato 2026-05-22)
api/reach/google-oauth-callback.tsNON esiste. Nessun endpoint OAuth "start" Google.oauth_connectionsesiste già (usata oggi per i social) con colonne adatte:provider,provider_account_id/name,access_token_enc,refresh_token_enc,scopes string[],status,token_expires_at,workspace_id,user_id,metadata. → riusare questa (la tabellareach_google_connectionsdedicata nella migration è commentata/opzionale).- Gli adapter (
src/lib/reach/adapters/) sono stub finché non ricevono unGoogleConnectionProviderreale (ogginullConnectionProvider). Contratto:getConnection(workspaceId, service, siteId) → GoogleConnection | null. - Env già deployate:
GOOGLE_OAUTH_CLIENT_ID/SECRET/REDIRECT_URI(apexhttps://multiverbe.app/api/reach/google-oauth-callback),GOOGLE_*_ENABLEDflags.
🔴 Decisioni founder prima di implementare
- Storage: riusare
oauth_connections(RACCOMANDATO, zero nuove migration) — 1 rigaprovider='google',scopes=['business.manage','webmasters.readonly']. ✅/❌ - Cifratura token (
*_enc): AES-256-GCM app-side con chiave da envOAUTH_TOKEN_ENC_KEY(32 byte) — VS pgsodium server-side. Decisione di postura sicurezza. Verificare se esiste già un helper crypto inapi/_lib/; se no, crearlo.
File da creare (pass dedicato)
api/_lib/crypto.ts—encrypt(plaintext)/decrypt(ciphertext)AES-256-GCM (iv+tag prefissati), chiaveOAUTH_TOKEN_ENC_KEY. (Solo se non esiste già.)api/_lib/google-oauth.ts—buildAuthUrl(state),exchangeCode(code)→{access,refresh,expiry,scope},refreshAccessToken(refresh). Endpoint Google standard,access_type=offline+prompt=consent(per ottenere il refresh token), scopesbusiness.manage+webmasters.readonly.api/reach/google-oauth-start.ts— auth robusta (memoria):getUser()da Supabase + verifica membership viateam_members(workspace_id, user_id=auth.uid()). Generastatefirmato HMAC{workspace_id, nonce, exp}→ cookie httpOnly+SameSite=Lax+Secure → redirect 302 abuildAuthUrl(state). (Evita IDOR: il workspace_id viene dalla sessione, non dal client.)api/reach/google-oauth-callback.ts— validastate(cookie vs query, HMAC, non scaduto),exchangeCode(code), ricava account id/email (userinfo o tokeninfo), cifra i token, upsertoauth_connections(provider='google', workspace_id dallo state, scopes, status='connected', token_expires_at). Redirect 302 → pagina Reach dashboard con?google=connected. Gestireerrorquery (consenso negato) → redirect con?google=error.api/_lib/google-connection-provider.ts— implementaGoogleConnectionProvider: leggeoauth_connections(provider='google', workspace) → decifra → setoken_expires_atscaduto,refreshAccessToken+ update riga → ritornaGoogleConnection. Passare agetReachAdapters({ connectionProvider }).
Sicurezza (non negoziabile)
- Token mai in chiaro nel DB: cifrare PRIMA dell'INSERT (colonne
*_enc). stateCSRF firmato + cookie httpOnly; confronto costante-tempo sull'HMAC.- Auth pattern
getUser + team_members(no workspace_id dal client). RLS resta attiva; gli endpoint usano service-role solo dopo il check di membership. - Pseudonimizzazione N/A (no PII verso Anthropic qui).
Validazione (CI non typecheck-a api/)
esbuild api/reach/google-oauth-callback.ts --bundle --packages=external --platform=node --format=esm > NUL
esbuild api/reach/google-oauth-start.ts --bundle --packages=external --platform=node --format=esm > NUL
Eseguire dal checkout principale (memoria project_api_esbuild_local_validation).
Definition of Done
- Start → consent Google → callback → riga
oauth_connectionsprovider='google' con token cifrati + scopes. getReachAdapterscon il provider reale →envStatus.businessProfile/searchConsole = 'connected'quando il workspace ha la connessione.- esbuild EXIT 0 su entrambi gli entrypoint. NON blocca il gate 10 Giu.