208 lines · 8.4 KB
import { env } from "cloudflare:workers";
import { z } from "zod";
import { all, chunks, first, placeholders, run } from "../db";
import { randomId } from "../crypto";
import { enforceRateLimit, requestIp } from "../limits";
import { assertResolvesPublic } from "../net/resolve-guard";

const MAX_REDIRECT_URIS = 10;
const CIMD_FETCH_TIMEOUT_MS = 5000;
const CIMD_MAX_BYTES = 16 * 1024;
const CIMD_REFRESH_MS = 24 * 60 * 60 * 1000;
/** Unused RFC 7591 registrations are dropped after this; used ones after this long without a token issued (AUTH-A9). */
const DCR_UNUSED_TTL_MS = 24 * 60 * 60 * 1000;
const DCR_IDLE_TTL_MS = 90 * 24 * 60 * 60 * 1000;
const LOOPBACK = new Set(["localhost", "127.0.0.1", "[::1]"]);
const BLOCKED_SCHEMES = new Set(["javascript:", "data:", "file:", "vbscript:", "blob:", "about:", "ws:", "wss:"]);

export type OAuthClient = { id: string; name: string; redirectUris: string[] };

type ClientRow = { id: string; name: string; redirect_uris: string; kind: "dcr" | "cimd"; created_at: number };

export class OAuthError extends Error {
  constructor(
    readonly error: string,
    message: string,
    readonly status = 400,
  ) {
    super(message);
  }
}

/** https anywhere, plain http only on loopback (RFC 8252), or a private-use scheme for native apps. */
function validRedirectUri(raw: string): boolean {
  let url: URL;
  try {
    url = new URL(raw);
  } catch {
    return false;
  }
  if (url.hash || BLOCKED_SCHEMES.has(url.protocol)) return false;