54 lines · 2.1 KB
import { env } from "cloudflare:workers";
import { loadUserById, 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. */