import { env } from "cloudflare:workers"; import { loadActiveUserById, toViewer } from "./auth"; import { hkdfSha256, utf8 } from "./sync-hash"; import { isUserContentOrigin, userContentOriginFor } from "./usercontent-origin"; import { decodeGrant, encodeGrant, USERCONTENT_GRANT_PARAM, USERCONTENT_GRANT_TTL_MS } from "./usercontent-policy"; import type { Viewer } from "~/lib/types"; const GRANT_SALT = "gitfor-usercontent"; const GRANT_INFO = "grant-v1"; let cachedKey: { secret: string; key: Uint8Array } | null = null; function grantKey(): Uint8Array | null { const secret = env.SECRETS_KEY; if (!secret) return null; if (!cachedKey || cachedKey.secret !== secret) { cachedKey = { secret, key: hkdfSha256(utf8(secret), GRANT_SALT, GRANT_INFO) }; } return cachedKey.key; } /** * The absolute user-content URL for a forge path, carrying a short-lived grant when a signed-in viewer needs * it for private content. Returns the path unchanged when no separate origin is in use. */ export function userContentUrl(forgeUrl: URL, pathname: string, search: string, viewer: Viewer | null): string { const origin = userContentOriginFor(forgeUrl); if (!origin) return `${pathname}${search}`; const target = new URL(`${origin}${pathname}${search}`); const key = grantKey(); if (viewer && key) { target.searchParams.set( USERCONTENT_GRANT_PARAM, encodeGrant(key, viewer.id, pathname, Date.now() + USERCONTENT_GRANT_TTL_MS), ); } return target.href; } /** The viewer a `?uc=` grant on this request vouches for, only when the request is on the user-content origin. */ export async function viewerFromGrant(request: Request): Promise { const url = new URL(request.url); const grant = url.searchParams.get(USERCONTENT_GRANT_PARAM); if (!grant || !isUserContentOrigin(url)) return null; const key = grantKey(); if (!key) return null; const decoded = decodeGrant(key, grant, url.pathname, Date.now()); if (!decoded) return null; const user = await loadActiveUserById(decoded.viewerId); if (!user) return null; if (user.sessions_revoked_at != null && decoded.issuedAt < user.sessions_revoked_at) return null; return toViewer(user); }