goes to a random website hosted on wisp.place

init

ptr.pet e7db2446

+358
+4
.gitignore
··· 1 + random-wisp-place.kv* 2 + /.envrc 3 + /.direnv 4 + hydrant.db
+8
deno.json
··· 1 + { 2 + "name": "random-wisp-place", 3 + "version": "0.1.0", 4 + "tasks": { 5 + "start": "deno run -A --unstable-kv main.ts", 6 + "dev": "deno run -A --unstable-kv --watch main.ts" 7 + } 8 + }
+61
flake.lock
··· 1 + { 2 + "nodes": { 3 + "nixpkgs": { 4 + "locked": { 5 + "lastModified": 1772173633, 6 + "narHash": "sha256-MOH58F4AIbCkh6qlQcwMycyk5SWvsqnS/TCfnqDlpj4=", 7 + "owner": "nixos", 8 + "repo": "nixpkgs", 9 + "rev": "c0f3d81a7ddbc2b1332be0d8481a672b4f6004d6", 10 + "type": "github" 11 + }, 12 + "original": { 13 + "owner": "nixos", 14 + "ref": "nixpkgs-unstable", 15 + "repo": "nixpkgs", 16 + "type": "github" 17 + } 18 + }, 19 + "nixpkgs-lib": { 20 + "locked": { 21 + "lastModified": 1769909678, 22 + "narHash": "sha256-cBEymOf4/o3FD5AZnzC3J9hLbiZ+QDT/KDuyHXVJOpM=", 23 + "owner": "nix-community", 24 + "repo": "nixpkgs.lib", 25 + "rev": "72716169fe93074c333e8d0173151350670b824c", 26 + "type": "github" 27 + }, 28 + "original": { 29 + "owner": "nix-community", 30 + "repo": "nixpkgs.lib", 31 + "type": "github" 32 + } 33 + }, 34 + "parts": { 35 + "inputs": { 36 + "nixpkgs-lib": "nixpkgs-lib" 37 + }, 38 + "locked": { 39 + "lastModified": 1769996383, 40 + "narHash": "sha256-AnYjnFWgS49RlqX7LrC4uA+sCCDBj0Ry/WOJ5XWAsa0=", 41 + "owner": "hercules-ci", 42 + "repo": "flake-parts", 43 + "rev": "57928607ea566b5db3ad13af0e57e921e6b12381", 44 + "type": "github" 45 + }, 46 + "original": { 47 + "owner": "hercules-ci", 48 + "repo": "flake-parts", 49 + "type": "github" 50 + } 51 + }, 52 + "root": { 53 + "inputs": { 54 + "nixpkgs": "nixpkgs", 55 + "parts": "parts" 56 + } 57 + } 58 + }, 59 + "root": "root", 60 + "version": 7 61 + }
+25
flake.nix
··· 1 + { 2 + inputs.parts.url = "github:hercules-ci/flake-parts"; 3 + inputs.nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; 4 + 5 + outputs = 6 + inp: 7 + inp.parts.lib.mkFlake { inputs = inp; } { 8 + systems = [ "x86_64-linux" ]; 9 + perSystem = 10 + { 11 + pkgs, 12 + ... 13 + }: 14 + { 15 + packages.default = pkgs.callPackage ./default.nix {}; 16 + devShells = { 17 + default = pkgs.mkShell { 18 + packages = with pkgs; [ 19 + deno 20 + ]; 21 + }; 22 + }; 23 + }; 24 + }; 25 + }
+260
main.ts
··· 1 + const WISP_API = Deno.env.get("WISP_API_URL") ?? "https://wisp.place"; 2 + const HYDRANT_BIN = Deno.env.get("HYDRANT_BIN") ?? "hydrant"; 3 + const PORT = parseInt(Deno.env.get("PORT") ?? "8080"); 4 + const KV_PATH = Deno.env.get("KV_PATH") ?? "random-wisp-place.kv"; 5 + 6 + const getFreePort = () => { 7 + const listener = Deno.listen({ port: 0 }); 8 + const port = (listener.addr as Deno.NetAddr).port; 9 + listener.close(); 10 + return port; 11 + }; 12 + 13 + const HYDRANT_PORT = getFreePort(); 14 + const HYDRANT_URL = `http://localhost:${HYDRANT_PORT}`; 15 + 16 + const FS_COLLECTION = "place.wisp.fs"; 17 + const DOMAIN_COLLECTION = "place.wisp.domain"; 18 + 19 + type SiteValue = { 20 + fallbackUrl: string; 21 + domainUrl: string | null; 22 + }; 23 + 24 + // secondary index: domain -> site key components 25 + type DomainIndexValue = { 26 + did: string; 27 + siteName: string; 28 + }; 29 + 30 + type HydrantRecord = { 31 + readonly type: "record"; 32 + readonly id: number; 33 + readonly record: { 34 + readonly did: string; 35 + readonly collection: string; 36 + readonly rkey: string; 37 + readonly action: "create" | "update" | "delete"; 38 + }; 39 + }; 40 + 41 + type HydrantEvent = HydrantRecord | { readonly type: "identity" | "account" }; 42 + 43 + type DomainRegistered = { 44 + readonly registered: true; 45 + readonly type: "wisp" | "custom"; 46 + readonly domain: string; 47 + readonly did: string; 48 + readonly rkey: string | null; 49 + }; 50 + 51 + type DomainStatus = DomainRegistered | { readonly registered: false }; 52 + 53 + const siteKey = (did: string, siteName: string) => ["sites", did, siteName] as const; 54 + const domainKey = (domain: string) => ["domain_idx", domain] as const; 55 + const cursorKey = () => ["cursor"] as const; 56 + 57 + const fallbackUrl = (did: string, siteName: string): string => 58 + `https://sites.wisp.place/${did}/${siteName}`; 59 + const resolveUrl = (site: SiteValue): string => 60 + site.domainUrl ?? site.fallbackUrl; 61 + 62 + const kv = await Deno.openKv(KV_PATH); 63 + 64 + const allSites = async (): Promise<SiteValue[]> => { 65 + const entries: SiteValue[] = []; 66 + for await (const entry of kv.list<SiteValue>({ prefix: ["sites"] })) { 67 + entries.push(entry.value); 68 + } 69 + return entries; 70 + }; 71 + 72 + const queryDomainRegistered = async (domain: string): Promise<DomainStatus | null> => { 73 + const url = new URL(`${WISP_API}/api/domain/registered`); 74 + url.searchParams.set("domain", domain); 75 + try { 76 + const res = await fetch(url, { signal: AbortSignal.timeout(5_000) }); 77 + return res.ok ? await res.json() as DomainStatus : null; 78 + } catch { 79 + return null; 80 + } 81 + }; 82 + 83 + const handleFsEvent = async ( 84 + did: string, 85 + rkey: string, 86 + action: "create" | "update" | "delete", 87 + ): Promise<void> => { 88 + const key = siteKey(did, rkey); 89 + 90 + if (action === "delete") { 91 + await kv.delete(key); 92 + console.log(`[-] fs ${did}:${rkey}`); 93 + return; 94 + } 95 + 96 + // preserve existing domainUrl on upsert 97 + const existing = await kv.get<SiteValue>(key); 98 + await kv.set(key, { 99 + fallbackUrl: fallbackUrl(did, rkey), 100 + domainUrl: existing.value?.domainUrl ?? null, 101 + }); 102 + console.log(`[+] fs ${action} ${did}:${rkey}`); 103 + }; 104 + 105 + const handleDomainEvent = async ( 106 + _did: string, 107 + rkey: string, 108 + action: "create" | "update" | "delete", 109 + ): Promise<void> => { 110 + // rkey is the subdomain label e.g. "alice" -> alice.wisp.place 111 + const domain = `${rkey}.wisp.place`; 112 + const dKey = domainKey(domain); 113 + 114 + if (action === "delete") { 115 + const idx = await kv.get<DomainIndexValue>(dKey); 116 + if (idx.value) { 117 + const sKey = siteKey(idx.value.did, idx.value.siteName); 118 + const site = await kv.get<SiteValue>(sKey); 119 + if (site.value) { 120 + await kv.set(sKey, { ...site.value, domainUrl: null }); 121 + } 122 + } 123 + await kv.delete(dKey); 124 + console.log(`[-] domain ${domain} unlinked`); 125 + return; 126 + } 127 + 128 + const status = await queryDomainRegistered(domain); 129 + if (!status?.registered || !status.rkey) { 130 + console.warn(`[!] domain ${domain}: not registered, no site mapped, or api error`); 131 + return; 132 + } 133 + 134 + const domainUrl = `https://${status.domain}/`; 135 + const sKey = siteKey(status.did, status.rkey); 136 + 137 + // update or pre-create the site row with the resolved domainUrl 138 + const existing = await kv.get<SiteValue>(sKey); 139 + await kv.atomic() 140 + .set(sKey, { 141 + fallbackUrl: existing.value?.fallbackUrl ?? fallbackUrl(status.did, status.rkey), 142 + domainUrl, 143 + }) 144 + .set(dKey, { did: status.did, siteName: status.rkey } satisfies DomainIndexValue) 145 + .commit(); 146 + 147 + console.log(`[+] domain ${domain} -> ${status.did}:${status.rkey} (${status.type})`); 148 + }; 149 + 150 + const handleEvent = async (raw: string): Promise<void> => { 151 + let event: HydrantEvent; 152 + try { event = JSON.parse(raw) as HydrantEvent; } 153 + catch { return; } 154 + if (event.type !== "record") return; 155 + 156 + const { did, collection, rkey, action } = event.record; 157 + await kv.set(cursorKey(), event.id); 158 + 159 + if (collection === FS_COLLECTION) { 160 + await handleFsEvent(did, rkey, action); 161 + } else if (collection === DOMAIN_COLLECTION) { 162 + await handleDomainEvent(did, rkey, action); 163 + } 164 + }; 165 + 166 + const connectToHydrant = async (cursor?: number): Promise<void> => { 167 + const wsUrl = new URL(`${HYDRANT_URL.replace(/^http/, "ws")}/stream`); 168 + if (cursor !== undefined) wsUrl.searchParams.set("cursor", String(cursor)); 169 + 170 + console.log(`[?] connecting to hydrant: ${wsUrl}`); 171 + const ws = new WebSocket(wsUrl.toString()); 172 + 173 + ws.onopen = () => console.log("[?] hydrant stream connected"); 174 + ws.onmessage = ({ data }) => { handleEvent(String(data)).catch(console.error); }; 175 + ws.onerror = (e) => console.error("[!] ws error:", e); 176 + ws.onclose = async () => { 177 + const saved = (await kv.get<number>(cursorKey())).value ?? undefined; 178 + console.log(`[!] ws closed (cursor=${saved ?? "none"}), reconnecting in 5s...`); 179 + setTimeout(() => connectToHydrant(saved), 5_000); 180 + }; 181 + }; 182 + 183 + const isReachable = async (url: string): Promise<boolean> => { 184 + try { 185 + const res = await fetch(url, { method: "HEAD", signal: AbortSignal.timeout(3_000) }); 186 + return res.status !== 404; 187 + } catch { 188 + return false; 189 + } 190 + }; 191 + 192 + const PROBE_BATCH = 10; 193 + const pickRandomReachable = async (sites: SiteValue[]): Promise<SiteValue | null> => { 194 + const shuffled = [...sites].sort(() => Math.random() - 0.5); 195 + for (let i = 0; i < shuffled.length; i += PROBE_BATCH) { 196 + const batch = shuffled.slice(i, i + PROBE_BATCH); 197 + const results = await Promise.all( 198 + batch.map(async (site) => ({ site, ok: await isReachable(resolveUrl(site)) })) 199 + ); 200 + const found = results.find((r) => r.ok); 201 + if (found) return found.site; 202 + } 203 + return null; 204 + }; 205 + 206 + Deno.serve({ port: PORT }, async (req) => { 207 + const { pathname } = new URL(req.url); 208 + 209 + if (pathname === "/health") { 210 + const sites = await allSites(); 211 + return Response.json({ 212 + total: sites.length, 213 + withDomain: sites.filter((s) => s.domainUrl).length, 214 + }); 215 + } 216 + 217 + const site = await pickRandomReachable(await allSites()); 218 + return site 219 + ? Response.redirect(resolveUrl(site), 302) 220 + : new Response("no sites discovered yet, try again later", { status: 503 }); 221 + }); 222 + console.log(`[?] listening on :${PORT}`); 223 + 224 + console.log(`[?] starting hydrant on :${HYDRANT_PORT}...`); 225 + try { 226 + const conf = (name: string, value: string) => Deno.env.set(`HYDRANT_${name}`, value); 227 + conf("API_PORT", `${HYDRANT_PORT}`); 228 + conf("ENABLE_CRAWLER", "true"); 229 + conf("FILTER_SIGNALS", [FS_COLLECTION]); 230 + conf("FILTER_COLLECTIONS", [FS_COLLECTION, DOMAIN_COLLECTION].join(",")); 231 + conf("PLC_URL", "https://plc.directory"); 232 + conf("ENABLE_DEBUG", "true"); 233 + 234 + const cmd = new Deno.Command(HYDRANT_BIN, { 235 + stdout: "inherit", 236 + stderr: "inherit", 237 + }); 238 + const child = cmd.spawn(); 239 + 240 + const cleanup = () => { 241 + console.log("[?] shutting down hydrant..."); 242 + child.kill("SIGTERM"); 243 + Deno.exit(); 244 + }; 245 + 246 + Deno.addSignalListener("SIGTERM", cleanup); 247 + Deno.addSignalListener("SIGINT", cleanup); 248 + 249 + child.status.then((status) => { 250 + console.error(`[!] hydrant process exited with code ${status.code}`); 251 + Deno.exit(1); 252 + }); 253 + } catch (e) { 254 + console.error(`[!] failed to start hydrant: ${e.message}`); 255 + Deno.exit(2); 256 + } 257 + 258 + const savedCursor = (await kv.get<number>(cursorKey())).value ?? undefined; 259 + console.log(`[?] resuming from cursor ${savedCursor ?? "start (0)"}`); 260 + connectToHydrant(savedCursor ?? 0);