import { LibSQLDatabase } from "drizzle-orm/libsql"; import { Client } from "@libsql/client"; import * as schema from "./db/schema.ts"; import { CompositeDidDocumentResolver, PlcDidDocumentResolver, WebDidDocumentResolver, } from "@atcute/identity-resolver"; import { FetchHandler, SimpleFetchHandlerOptions } from "@atcute/client"; import { safeFetchWrap } from "@atproto-labs/fetch-node"; export type db = LibSQLDatabase & { $client: Client; }; export const ROOT_DOMAIN = Deno.env.get("HOSTNAME") || "localhost"; export const PORT = Number(Deno.env.get("PORT")) || 80; export const MAX_SITE_SIZE = Number(Deno.env.get("MAX_SITE_SIZE")) || 250000000; export const SUBDOMAIN_REGEX = new RegExp(`.+(?=\\.${ROOT_DOMAIN}$)`, "gm"); export function clearCookies(req: Request): Headers { const cookie_header = req.headers.get("Cookie"); // cookies are unset so return empty headers if (!cookie_header) return new Headers(); // get each kv pair and extract the key const cookies = cookie_header.split("; ").map((x) => x.split("=")[0]); const head = new Headers(); for (const key of cookies) { // max-age <= 0 means instant expiry .: deleted instantly head.append("Set-Cookie", `${key}=; Max-Age=-1`); } return head; } const docResolver = new CompositeDidDocumentResolver({ methods: { plc: new PlcDidDocumentResolver(), web: new WebDidDocumentResolver(), }, }); export const fetchHandler: ({ service, }: SimpleFetchHandlerOptions) => FetchHandler = ({ service, }: SimpleFetchHandlerOptions): FetchHandler => { const safeFetch = safeFetchWrap(); return async (pathname, init) => { const url = new URL(pathname, service); return await safeFetch(url, init); }; }; export async function getPds( did: `did:${"plc" | "web"}:${string}` ): Promise { try { const doc = await docResolver.resolve(did); const pds = doc.service?.filter( (x) => x.id.endsWith("#atproto_pds") && x.type === "AtprotoPersonalDataServer" )[0].serviceEndpoint; return typeof pds === "string" ? pds : undefined; } catch { return undefined; } } export function isDid(did: unknown): did is `did:${"plc" | "web"}:${string}` { return ( typeof did === "string" && (did.startsWith("did:web:") || did.startsWith("did:plc:")) ); } /** * given a valid url path string containing * - `/` for seperating characters * - a-zA-Z0-9 `-._~` as unreserved * - `!$&'()*+,;=` as reserved but valid in paths * - `:@` as neither reserved or unreserved but valid in paths * - %XX where X are hex digits for percent encoding * * we need to consistently and bidirectionally convert it into a string containing the characters A-Z, a-z, 0-9, `.-_:~` for an atproto rkey * A-Z a-z 0-9 are covered easily * we can also take -._~ as they are also unreserved * leaving : as a valid rkey character which looks nice for encoding * the uppercase versions MUST be used to prevent ambiguity * a colon which isnt followed by a valid character is an invalid rkey and should be ignored * - `/` `::` * - `%` `:~` * - `!` `:21` * - `$` `:24` * - `&` `:26` * - `'` `:27` * - `(` `:28` * - `)` `:29` * - `*` `:2A` * - `+` `:2B` * - `,` `:2C` * - `:` `:3A` * - `;` `:3B` * - `=` `:3D` * - `@` `:40` * @returns {string | undefined} undefined when input is invalid */ export function urlToRkey(url: string): string | undefined { // contains 0-9A-Za-z + special valid chars and / seperator. also can contain %XX with XX being hex if ( !url.match(/^(?:[a-zA-Z0-9/\-._~!$&'()*+,;=:@]|(?:%[0-9a-fA-F]{2}))*$/gm) ) { return; } return ( url // : replace is hoisted so it doesnt replace colons from elsewhere .replaceAll(":", ":3A") .replaceAll("/", "::") .replaceAll("%", ":~") .replaceAll("!", ":21") .replaceAll("$", ":24") .replaceAll("&", ":26") .replaceAll("'", ":27") .replaceAll("(", ":28") .replaceAll(")", ":29") .replaceAll("*", ":2A") .replaceAll("+", ":2B") .replaceAll(",", ":2C") .replaceAll(";", ":3B") .replaceAll("=", ":3D") .replaceAll("@", ":40") ); } /** * @see {@link urlToRkey} for rkey <=> url conversion syntax * @returns {string | undefined} undefined when input is invalid */ export function rkeyToUrl(rkey: string): string | undefined { // contains 0-9A-Za-z .-_~ // or any valid colon escape sequence if ( !rkey.match( /^(?:[A-Za-z0-9.\-_~]|(?:::)|(?::~)|(?::21)|(?::24)|(?::26)|(?::27)|(?::28)|(?::29)|(?::2A)|(?::2B)|(?::2C)|(?::3A)|(?::3B)|(?::3D)|(?::40))*$/gm ) ) return; return rkey .replaceAll("::", "/") .replaceAll(":~", "%") .replaceAll(":21", "!") .replaceAll(":24", "$") .replaceAll(":26", "&") .replaceAll(":27", "'") .replaceAll(":28", "(") .replaceAll(":29", ")") .replaceAll(":2A", "*") .replaceAll(":2B", "+") .replaceAll(":2C", ",") .replaceAll(":3A", ":") .replaceAll(":3B", ";") .replaceAll(":3D", "=") .replaceAll(":40", "@"); }