253 lines · 10.5 KB
import { env } from "cloudflare:workers";
import { z } from "zod";
import { all, first } from "./db";
import { accountDb, repoDb } from "./scoped-db";
import { forEachScope } from "./account-scope";
import { randomId } from "./crypto";
import { notFoundError, HttpError } from "./errors";
import { repoReadableSql } from "./repos";
import { listSigningKeys } from "./security/signatures";
import type { Viewer } from "~/lib/types";

/** One archive per user per day; the archive itself lives in KV for the same window and then expires. */
const EXPORT_TTL_SECONDS = 24 * 60 * 60;
/** Authored issues/comments per kind; a busier account gets a truncated flag rather than a timeout. */
export const EXPORT_ROW_CAP = 5000;
const MAX_EXPORT_BYTES = 20 * 1024 * 1024;
const HTTP_TOO_MANY = 429;

const exportMeta = z.object({ id: z.string(), createdAt: z.number(), bytes: z.number() });
export type ExportMeta = z.infer<typeof exportMeta> & { expiresAt: number };

const metaKey = (userId: string) => `account-export:${userId}`;
const dataKey = (userId: string, id: string) => `account-export:${userId}:${id}`;

function withExpiry(meta: z.infer<typeof exportMeta>): ExportMeta {
  return { ...meta, expiresAt: meta.createdAt + EXPORT_TTL_SECONDS * 1000 };
}

export async function currentExport(userId: string): Promise<ExportMeta | null> {
  const parsed = exportMeta.safeParse(await env.KV.get(metaKey(userId), "json"));
  if (!parsed.success) return null;
  const meta = withExpiry(parsed.data);
  return meta.expiresAt > Date.now() ? meta : null;
}

/** Per-repository queries for authored content (`?1` the user, `?2` the repository), run in its RepoState. */
const AUTHORED_SQL = {
  issues: `SELECT i.number, i.is_pull, i.title, i.body, i.state, i.created_at, i.updated_at FROM issues i
    WHERE i.author_id = ?1 AND i.repo_id = ?2 ORDER BY i.created_at`,
  comments: `SELECT i.number, c.body, c.created_at, c.updated_at FROM comments c JOIN issues i ON i.id = c.issue_id