163 lines · 6.3 KB
import { env } from "cloudflare:workers";
import type { RepoRow } from "~/lib/types";
import { artifactsCall } from "./artifacts-call";
import { artifactsBinding } from "./artifacts-shards";

const TOKEN_TTL_SECONDS = 3600;
const TOKEN_CACHE_SECONDS = 3000;
/** A cached token must outlive its longest use (a git request) by this much. */
const TOKEN_EXPIRY_MARGIN_SECONDS = 600;
/** KV's minimum expirationTtl. */
const MIN_KV_TTL_SECONDS = 60;

export type TokenScope = "read" | "write";

export function tokenSecret(plaintext: string): string {
  return plaintext.split("?expires=")[0];
}

/** Seconds until the `?expires=<unix>` suffix of a token plaintext, or null when it has none. */
export function tokenLifetimeSeconds(plaintext: string, now = Date.now()): number | null {
  const expires = Number(plaintext.split("?expires=")[1]);
  return Number.isFinite(expires) && expires > 0 ? Math.floor(expires - now / 1000) : null;
}

function tokenCacheKey(artifactsName: string, scope: TokenScope): string {
  return `artifacts-token:${artifactsName}:${scope}`;
}

async function cacheToken(artifactsName: string, scope: TokenScope, plaintext: string): Promise<void> {
  const lifetime = tokenLifetimeSeconds(plaintext) ?? TOKEN_TTL_SECONDS;
  const ttl = Math.min(TOKEN_CACHE_SECONDS, lifetime - TOKEN_EXPIRY_MARGIN_SECONDS);
  if (ttl < MIN_KV_TTL_SECONDS) return;
  await env.KV.put(tokenCacheKey(artifactsName, scope), tokenSecret(plaintext), { expirationTtl: ttl });
}

/**
 * `create()` and `fork()` return a write token; caching it saves the `get()` + `createToken()` pair the
 * first push (e.g. the initial README commit) would otherwise make. Best effort.
 */
export async function rememberInitialToken(artifactsName: string, plaintext: string | undefined): Promise<void> {