[WIP] A (somewhat barebones) atproto app for creating custom sites without hosting!
at 97aab93726982e4bbef08cac0db8f488a2ce62ec 143 lines 4.5 kB view raw
1import { LibSQLDatabase } from "drizzle-orm/libsql"; 2import { Client } from "@libsql/client"; 3import * as schema from "./db/schema.ts"; 4import { 5 CompositeDidDocumentResolver, 6 PlcDidDocumentResolver, 7 WebDidDocumentResolver, 8} from "@atcute/identity-resolver"; 9 10export type db = LibSQLDatabase<typeof schema> & { 11 $client: Client; 12}; 13 14export const ROOT_DOMAIN = Deno.env.get("HOSTNAME") || "localhost"; 15export const PORT = Number(Deno.env.get("PORT")) || 80; 16export const MAX_SITE_SIZE = Number(Deno.env.get("MAX_SITE_SIZE")) || 250000000; 17 18export const SUBDOMAIN_REGEX = new RegExp(`.+(?=\\.${ROOT_DOMAIN}$)`, "gm"); 19 20export function clearCookies(req: Request): Headers { 21 const cookie_header = req.headers.get("Cookie"); 22 // cookies are unset so return empty headers 23 if (!cookie_header) return new Headers(); 24 // get each kv pair and extract the key 25 const cookies = cookie_header.split("; ").map((x) => x.split("=")[0]); 26 const head = new Headers(); 27 for (const key of cookies) { 28 // max-age <= 0 means instant expiry .: deleted instantly 29 head.append("Set-Cookie", `${key}=; Max-Age=-1`); 30 } 31 return head; 32} 33 34const docResolver = new CompositeDidDocumentResolver({ 35 methods: { 36 plc: new PlcDidDocumentResolver(), 37 web: new WebDidDocumentResolver(), 38 }, 39}); 40 41export async function getPds( 42 did: `did:${"plc" | "web"}:${string}` 43): Promise<string | undefined> { 44 try { 45 const doc = await docResolver.resolve(did); 46 const pds = doc.service?.filter( 47 (x) => 48 x.id.endsWith("#atproto_pds") && x.type === "AtprotoPersonalDataServer" 49 )[0].serviceEndpoint; 50 return typeof pds === "string" ? pds : undefined; 51 } catch { 52 return undefined; 53 } 54} 55 56export function isDid(did: unknown): did is `did:${"plc" | "web"}:${string}` { 57 return ( 58 typeof did === "string" && 59 (did.startsWith("did:web:") || did.startsWith("did:plc:")) 60 ); 61} 62 63/** 64 * given a valid url path string containing 65 * - `/` for seperating characters 66 * - a-zA-Z0-9 `-._~` as unreserved 67 * - `!$&'()*+,;=` as reserved but valid in paths 68 * - `:@` as neither reserved or unreserved but valid in paths 69 * - %XX where X are hex digits for percent encoding 70 * 71 * we need to consistently and bidirectionally convert it into a string containing the characters A-Z, a-z, 0-9, `.-_:~` for an atproto rkey 72 * A-Z a-z 0-9 are covered easily 73 * we can also take -._~ as they are also unreserved 74 * leaving : as a valid rkey character which looks nice for encoding 75 * the uppercase versions MUST be used to prevent ambiguity 76 * a colon which isnt followed by a valid character is an invalid rkey and should be ignored 77 * - `/` `::` 78 * - `%` `:~` 79 * - `!` `:21` 80 * - `$` `:24` 81 * - `&` `:26` 82 * - `'` `:27` 83 * - `(` `:28` 84 * - `)` `:29` 85 * - `*` `:2A` 86 * - `+` `:2B` 87 * - `,` `:2C` 88 * - `:` `:3A` 89 * - `;` `:3B` 90 * - `=` `:3D` 91 * - `@` `:40` 92 * @returns {string | undefined} undefined when input is invalid 93 */ 94export function urlToRkey(url: string): string | undefined { 95 // contains 0-9A-Za-z + special valid chars and / seperator. also can contain %XX with XX being hex 96 if (!url.match(/^([a-zA-Z0-9/\-._~!$&'()*+,;=:@]|(%[0-9a-fA-F]{2}))*$/gm)) { 97 return; 98 } 99 return ( 100 url 101 // : replace is hoisted so it doesnt replace colons from elsewhere 102 .replaceAll(":", ":3A") 103 .replaceAll("/", "::") 104 .replaceAll("%", ":~") 105 .replaceAll("!", ":21") 106 .replaceAll("$", ":24") 107 .replaceAll("&", ":26") 108 .replaceAll("'", ":27") 109 .replaceAll("(", ":28") 110 .replaceAll(")", ":29") 111 .replaceAll("*", ":2A") 112 .replaceAll("+", ":2B") 113 .replaceAll(",", ":2C") 114 .replaceAll(";", ":3B") 115 .replaceAll("=", ":3D") 116 .replaceAll("@", ":40") 117 ); 118} 119 120/** 121 * @see {@link urlToRkey} for rkey <=> url conversion syntax 122 * @returns {string | undefined} undefined when input is invalid 123 */ 124export function rkeyToUrl(rkey: string): string | undefined { 125 // contains 0-9A-Za-z .-_:~ 126 if (!rkey.match(/^[A-Za-z0-9.\-_:~]*$/gm)) return; 127 return rkey 128 .replaceAll("::", "/") 129 .replaceAll(":~", "%") 130 .replaceAll(":21", "!") 131 .replaceAll(":24", "$") 132 .replaceAll(":26", "&") 133 .replaceAll(":27", "'") 134 .replaceAll(":28", "(") 135 .replaceAll(":29", ")") 136 .replaceAll(":2A", "*") 137 .replaceAll(":2B", "+") 138 .replaceAll(":2C", ",") 139 .replaceAll(":3A", ":") 140 .replaceAll(":3B", ";") 141 .replaceAll(":3D", "=") 142 .replaceAll(":40", "@"); 143}