[WIP] A (somewhat barebones) atproto app for creating custom sites without hosting!
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}