functions/_lib/zoho.ts — Cliente Zoho API per-user (s68)
Librería central para la integración Zoho en CreaRack Workspace. Reescrita en s68 (commit 7617371, 2026-05-17) para pasar de Service Account singleton a modelo per-user con team_member como PK en D1.
⚠️ Cambio de modelo en s68: todas las funciones añaden parámetro
teamMember: TeamMember. Ver tabla de migración al final.
Interfaces principales
ZohoEnv
interface ZohoEnv {
DB: D1Database;
ZOHO_CLIENT_ID: string;
ZOHO_CLIENT_SECRET: string;
}
ZohoTokenRow
interface ZohoTokenRow {
team_member: TeamMember; // PK (era: id INTEGER)
zoho_email: string; // email Zoho real resuelto en callback
refresh_token: string;
access_token: string | null;
access_token_expires_at: string | null;
dc: string; // 'eu' | 'com' | 'in'
granted_by_cf_email: string; // era: granted_by
granted_at: string;
updated_at: string;
}
Scopes OAuth activos (s68)
export const ZOHO_SCOPES = [
'ZohoCalendar.event.READ',
'ZohoCalendar.event.CREATE', // nuevo en s68
'ZohoCalendar.calendar.READ',
'ZohoMail.messages.READ', // nuevo en s68 (sustituye tasks.READ)
'ZohoMail.accounts.READ',
].join(',');
Retirados en s68: ZohoMail.tasks.READ, ZohoMail.tasks.CREATE.
API pública
getStoredToken(env, teamMember) → ZohoTokenRow | null
Lee el row del miembro desde D1.
getValidAccessToken(env, teamMember) → { token, dc, zoho_email }
Punto de entrada preferido. Auto-refresca el token si expirado. Lanza error si el miembro no ha autorizado.
zohoFetch(env, teamMember, url, init?) → Response
Wrapper HTTP con auth automática. Añade Authorization: Zoho-oauthtoken <token>.
upsertToken(env, teamMember, zohoEmail, refreshToken, accessToken, expiresIn, dc, grantedByCfEmail) → void
Insert-or-update en zoho_oauth_tokens. PK conflict en team_member hace UPDATE.
deleteToken(env, teamMember) → void
Borra el row del miembro (desconectar desde Settings).
getAllTokensSummary(env) → Array<{team_member, zoho_email, dc, ...}>
Resumen de todos los miembros autorizados. Sin tokens. Usado en GET /api/oauth/zoho/status.
fetchPrimaryZohoEmail(env, teamMember) → string
Llama /api/accounts Zoho Mail para resolver email primario real. Devuelve 'unknown' si falla (soft fail). Usado en callback OAuth.
exchangeCodeForTokens(env, code, redirectUri, dc) → ExchangeResult
Intercambia authorization code por tokens.
buildAuthorizeUrl(env, state, redirectUri, dc?) → string
Construye URL consent screen Zoho con access_type=offline.
Helpers de host
calendarApiHost(dc) // 'calendar.zoho.eu' | 'calendar.zoho.com' | ...
mailApiHost(dc) // 'mail.zoho.eu' | ...
Migración s67 → s68
| Antes (s67) | Después (s68) |
|---|---|
WHERE id = 1 | WHERE team_member = ? |
getValidAccessToken(env) | getValidAccessToken(env, teamMember) |
zohoFetch(env, url, init) | zohoFetch(env, teamMember, url, init) |
upsertToken(env, rt, at, exp, dc, by) | upsertToken(env, tm, email, rt, at, exp, dc, by) |
deleteToken(env) | deleteToken(env, teamMember) |
getStoredToken(env) | getStoredToken(env, teamMember) |
Véase también
- [[feature—zoho—per-user-oauth]] — Flujo completo OAuth per-user
- [[entity—workers—service—staff-lib]] — Módulo que exporta
TeamMember - [[decision—20260517—integracion-zoho-calendar-mail-pivot]] — ADR: pivot a Calendar + Mail
- [[entity—workers—endpoint—task-calendar-links]] — Endpoints vínculos task↔Calendar
- [[entity—workers—endpoint—task-email-links]] — Endpoints vínculos task↔Mail
Referenciado desde
- Endpoints vínculos task ↔ Zoho Calendar (GET/POST/DELETE /api/tasks/{id}/links/calendar)
- Endpoints vínculos task ↔ Zoho Mail (GET/POST/DELETE /api/tasks/{id}/links/email)
- Integración Zoho Calendar + Mail con OAuth per-user (s68)
- Lib staff.ts — Resolución de TeamMember desde CF Access
- staff.ts — Resolución de identidad CF Access → TeamMember
- Zoho OAuth per-user: autorización individual por miembro del staff (s68)