67 lines · 2.3 KB
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<string | null> {
  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)