[WIP] A (somewhat barebones) atproto app for creating custom sites without hosting!

server: resolve handle && did to pds

+134 -7
+4 -1
deno.json
··· 1 1 { 2 2 "tasks": { 3 - "dev": "PORT=8000 deno run --watch --allow-net --allow-env=HOSTNAME,PORT main.ts" 3 + "dev": "PORT=8000 deno run --watch --allow-net --allow-env=HOSTNAME,PORT,NODE_ENV main.ts" 4 + }, 5 + "imports": { 6 + "@atcute/identity-resolver": "npm:@atcute/identity-resolver@^1.1.3" 4 7 }, 5 8 "unstable": ["raw-imports"] 6 9 }
+47
deno.lock
··· 1 + { 2 + "version": "5", 3 + "specifiers": { 4 + "npm:@atcute/identity-resolver@^1.1.3": "1.1.3_@atcute+identity@1.1.0" 5 + }, 6 + "npm": { 7 + "@atcute/identity-resolver@1.1.3_@atcute+identity@1.1.0": { 8 + "integrity": "sha512-KZgGgg99CWaV7Df3+h3X/WMrDzTPQVfsaoIVbTNLx2B56BvCL2EmaxPSVw/7BFUJMZHlVU4rtoEB4lyvNyMswA==", 9 + "dependencies": [ 10 + "@atcute/identity", 11 + "@atcute/lexicons", 12 + "@atcute/util-fetch", 13 + "@badrap/valita" 14 + ] 15 + }, 16 + "@atcute/identity@1.1.0": { 17 + "integrity": "sha512-6vRvRqJatDB+JUQsb+UswYmtBGQnSZcqC3a2y6H5DB/v5KcIh+6nFFtc17G0+3W9rxdk7k9M4KkgkdKf/YDNoQ==", 18 + "dependencies": [ 19 + "@atcute/lexicons", 20 + "@badrap/valita" 21 + ] 22 + }, 23 + "@atcute/lexicons@1.1.1": { 24 + "integrity": "sha512-k6qy5p3j9fJJ6ekaMPfEfp3ni4TW/XNuH9ZmsuwC0fi0tOjp+Fa8ZQakHwnqOzFt/cVBfGcmYE/lKNAbeTjgUg==", 25 + "dependencies": [ 26 + "esm-env" 27 + ] 28 + }, 29 + "@atcute/util-fetch@1.0.1": { 30 + "integrity": "sha512-Clc0E/5ufyGBVfYBUwWNlHONlZCoblSr4Ho50l1LhmRPGB1Wu/AQ9Sz+rsBg7fdaW/auve8ulmwhRhnX2cGRow==", 31 + "dependencies": [ 32 + "@badrap/valita" 33 + ] 34 + }, 35 + "@badrap/valita@0.4.6": { 36 + "integrity": "sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg==" 37 + }, 38 + "esm-env@1.2.2": { 39 + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==" 40 + } 41 + }, 42 + "workspace": { 43 + "dependencies": [ 44 + "npm:@atcute/identity-resolver@^1.1.3" 45 + ] 46 + } 47 + }
+4 -2
main.ts
··· 6 6 7 7 const SUBDOMAIN_REGEX = new RegExp(`.+(?=\\.${ROOT_DOMAIN}$)`, "gm"); 8 8 9 - Deno.serve({ port: PORT, hostname: ROOT_DOMAIN }, (req) => { 9 + Deno.serve({ port: PORT, hostname: ROOT_DOMAIN }, async (req) => { 10 10 const reqUrl = new URL(req.url); 11 11 const subdomain = reqUrl.hostname.match(SUBDOMAIN_REGEX)?.at(0)?.split("."); 12 12 ··· 36 36 // ex: vielle.dev.ROOT_DOMAIN 37 37 // cannot contain hyphen in top level domain 38 38 if (!subdomain.at(-1)?.startsWith("did-") && subdomain.length > 1) { 39 - return user(req, { handle: subdomain.join(".") }); 39 + return await user(req, { 40 + handle: subdomain.join(".") as `${string}.${string}`, 41 + }); 40 42 } 41 43 42 44 return new Response(
+79 -4
user.ts
··· 1 - export default function ( 1 + import { 2 + CompositeHandleResolver, 3 + DohJsonHandleResolver, 4 + WellKnownHandleResolver, 5 + CompositeDidDocumentResolver, 6 + PlcDidDocumentResolver, 7 + WebDidDocumentResolver, 8 + } from "@atcute/identity-resolver"; 9 + 10 + const handleResolver = new CompositeHandleResolver({ 11 + strategy: "race", 12 + methods: { 13 + dns: new DohJsonHandleResolver({ 14 + dohUrl: "https://mozilla.cloudflare-dns.com/dns-query", 15 + }), 16 + http: new WellKnownHandleResolver(), 17 + }, 18 + }); 19 + 20 + const docResolver = new CompositeDidDocumentResolver({ 21 + methods: { 22 + plc: new PlcDidDocumentResolver(), 23 + web: new WebDidDocumentResolver(), 24 + }, 25 + }); 26 + 27 + export default async function ( 2 28 _req: Request, 3 - user: { handle: string } | { did: `did:plc:${string}` | `did:web:${string}` } 4 - ): Response { 5 - return new Response(`${"handle" in user ? user.handle : user.did}`); 29 + user: 30 + | { handle: `${string}.${string}` } 31 + | { did: `did:plc:${string}` | `did:web:${string}` } 32 + ): Promise<Response> { 33 + // if handle: resolve did 34 + let did: `did:${"plc" | "web"}:${string}`; 35 + if ("handle" in user) { 36 + try { 37 + // cast bc i know it will be `string.string` 38 + did = await handleResolver.resolve(user.handle); 39 + } catch { 40 + return new Response("Failed to resolve handle", { 41 + status: 500, 42 + }); 43 + } 44 + } else did = user.did; 45 + 46 + // resolve did doc 47 + const doc = await docResolver.resolve(did); 48 + 49 + // handle must be in did document 50 + if ("handle" in user && !doc.alsoKnownAs?.includes(`at://${user.handle}`)) { 51 + return new Response( 52 + `Provided handle (at://${user.handle}) was not found in the did document for ${did}. Found handles:\n${doc.alsoKnownAs?.map((x) => "- " + x).join("\n") ?? "None"}`, 53 + { 54 + status: 500, 55 + } 56 + ); 57 + } 58 + 59 + const pds = doc.service?.filter( 60 + (x) => 61 + x.id.endsWith("#atproto_pds") && 62 + x.type === "AtprotoPersonalDataServer" && 63 + typeof x.serviceEndpoint === "string" 64 + // cast as we type checked it above but it didnt acc apply? 65 + )[0].serviceEndpoint as string | undefined; 66 + if (!pds) 67 + return new Response( 68 + `Could not find a valid pds for ${"handle" in user ? user.handle : user.did}`, 69 + { 70 + status: 500, 71 + } 72 + ); 73 + 74 + pds; 75 + 76 + return new Response(`Resolved: ${"handle" in user ? user.handle : user.did} 77 + 78 + handle: ${"handle" in user ? user.handle : (doc.alsoKnownAs?.filter((x) => x.startsWith("at://"))[0] ?? "N/A")} 79 + did: ${did} 80 + pds: ${pds}`); 6 81 }