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