import { run } from "../db"; import { accountDb, stmt, type ScopedStatement } from "../scoped-db"; import { locate, registerLocator } from "../scoped-db/locators"; import { sha256Hex, toHex } from "../crypto"; import { accountToken, embeddedAccountId } from "../account-scope"; import type { GrantSpec } from "./grants"; export type StoredGrant = GrantSpec & { repoNames: string[]; forgeAccess: boolean }; export const CODE_PREFIX = "code"; export const REFRESH_PREFIX = "gitfor_rt"; export const REFRESH_TTL_MS = 30 * 24 * 60 * 60 * 1000; /** OAuth codes and refresh tokens embed the account that holds them, so exchanges skip the locator. */ export function secretFor(prefix: string, userId: string): string { return accountToken(prefix, userId, toHex(crypto.getRandomValues(new Uint8Array(20)))); } /** The account holding a code, refresh or access token: embedded in the secret, else from its locator. */ export async function holderOf( secret: string, prefix: string, kind: "oauth_code" | "oauth_refresh" | "access_token", hash: string, ): Promise { return embeddedAccountId(secret, prefix) ?? (await locate(kind, hash)); } export async function refreshTokenInsert( userId: string, clientId: string, accessTokenId: string, resource: string, grant: StoredGrant, ): Promise<{ refresh: string; hash: string; statement: ScopedStatement }> { const refresh = secretFor(REFRESH_PREFIX, userId); const hash = await sha256Hex(refresh); const now = Date.now(); const statement = stmt( `INSERT INTO oauth_refresh_tokens (token_hash, client_id, user_id, access_token_id, resource, grant_json, expires_at, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, hash, clientId, userId, accessTokenId, resource, JSON.stringify(grant), now + REFRESH_TTL_MS, now, ); return { refresh, hash, statement }; } export async function storeRefreshToken( userId: string, clientId: string, accessTokenId: string, resource: string, grant: StoredGrant, ): Promise { const { refresh, hash, statement } = await refreshTokenInsert(userId, clientId, accessTokenId, resource, grant); await accountDb(userId).run(statement.sql, ...statement.params); await registerLocator("oauth_refresh", hash, userId); await run("UPDATE oauth_clients SET last_used_at = ? WHERE id = ?", Date.now(), clientId); return refresh; }