/** * Workspace token resolver — server-only helper. * * Cycle 7 — extracted from `src/actions/smartsheet/workspace.ts` to * avoid a `"use server"` leak: every async export of a "use server" * file is reachable as a server action RPC, and we don't want this * function callable from the client (it returns the plaintext API * token). Living in a regular module means it's only importable from * other server code (`smartsheet-sync.ts`, `time-tracking-smartsheet- * sync.ts`) — the client surface is in the workspace actions file, * which gates with `requireManageSettings` and never returns * plaintext. */ import "server-only"; import { eq } from "drizzle-orm"; import { withBrand } from "@/db/client"; import { smartsheetWorkspace } from "@/db/schema/smartsheet-workspace"; import { decryptToken } from "@/lib/crypto"; export type ResolvedWorkspaceToken = | { ok: true; token: string } | { ok: false; reason: "no_workspace" } | { ok: false; reason: "connection_disabled" } | { ok: false; reason: "decryption_failed"; error: string }; export async function resolveWorkspaceToken( brandId: string, ): Promise { return withBrand(brandId, async (db) => { const rows = await db .select() .from(smartsheetWorkspace) .where(eq(smartsheetWorkspace.brandId, brandId)) .limit(1); const ws = rows[0]; if (!ws) return { ok: false, reason: "no_workspace" }; if (!ws.connectionEnabled) return { ok: false, reason: "connection_disabled" }; try { const token = decryptToken({ cipher: ws.encryptedApiToken, iv: ws.tokenIv, authTag: ws.tokenAuthTag, }); return { ok: true, token }; } catch (err) { return { ok: false, reason: "decryption_failed", error: err instanceof Error ? err.message : String(err), }; } }); }