馃馃攽 PAM provider for ATProto
at main 102 lines 3.6 kB view raw
1import { AtpAgent } from "https://esm.sh/@atproto/api@0.14?target=deno"; 2 3const SOCK = "/run/atpam.sock"; 4const PID = "/run/atpam.pid"; 5const DIDMAP = "/etc/atpam/dids.json"; 6const AT_ENDPOINT = "https://zio.blue"; // NOTE: hardcoded because lazy rn 7 8function exists(path: string): boolean { 9 try { Deno.statSync(path); return true; } 10 catch (e: any) { 11 if (e instanceof Deno.errors.NotFound) return false; 12 throw e; 13 } 14} 15const loadMap = () => exists(DIDMAP) ? JSON.parse(Deno.readTextFileSync(DIDMAP)) : {}; 16const saveMap = (m: Record<string,string>) => 17 Deno.writeTextFileSync(DIDMAP, JSON.stringify(m, null, 2)); 18 19const didToUnix = (did: string) => 20 did.replace(/^did:/, "").replace(/:/g, "-").replace(/\./g, "-"); 21 22async function resolveCurrentHandle(did: string): Promise<string> { 23 const [meth, id] = did.replace(/^did:/, "").split(":"); 24 if (meth === "plc") { 25 const doc = await fetch(`https://plc.directory/${did}`).then(r => r.json()); 26 const aka = doc.alsoKnownAs?.find((s: string) => s.startsWith("at://")); 27 return aka ? aka.slice(5) : "handle.invalid"; 28 } else if (meth === "web") { 29 const doc = await fetch(`https://${id}/.well-known/did.json`).then(r => r.json()); 30 const aka = doc.alsoKnownAs?.find((s: string) => s.startsWith("at://")); 31 return aka ? aka.slice(5) : "handle.invalid"; 32 } 33 return "handle.invalid"; 34} 35 36async function authenticate( 37 cred: {handle?:string; did?:string; unix?:string; password:string} 38): Promise<{ok:boolean; unixUser?:string; error?:string}> { 39 const map = loadMap(); 40 let did: string; 41 42 if (cred.did) { 43 did = cred.did; 44 } else if (cred.unix) { 45 const expand = (u: string): string | null => { 46 if (u.startsWith("didplc-")) return "did:plc:" + u.slice(7); 47 if (u.startsWith("didweb-")) return "did:web:" + u.slice(7).replace(/-/g, "."); 48 return null; 49 }; 50 const exp = expand(cred.unix); 51 if (!exp) return {ok:false, error:"Bad unix name"}; 52 did = exp; 53 } else if (cred.handle) { 54 let cached = map[cred.handle]; 55 if (!cached) { 56 const u = new URL(`${AT_ENDPOINT}/xrpc/com.atproto.identity.resolveHandle`); 57 u.searchParams.set("handle", cred.handle); 58 const {did: resolved} = await fetch(u).then(r => r.json()); 59 if (!resolved) return {ok:false, error:"Handle not found"}; 60 cached = resolved; 61 map[cred.handle] = cached; 62 saveMap(map); 63 } 64 did = cached; 65 } else { 66 return {ok:false, error:"No identity provided"}; 67 } 68 69 const current = await resolveCurrentHandle(did); 70 if (current === "handle.invalid") 71 return {ok:false, error:"DID has no valid handle"}; 72 73 const agent = new AtpAgent({service: AT_ENDPOINT}); 74 //try { 75 await agent.login({identifier:current, password:cred.password}); 76 //} catch { 77 // return {ok:false, error:"Auth failed"}; 78 //} 79 return {ok:true, unixUser: didToUnix(did)}; 80} 81 82/* ---------- socket server ---------- */ 83try { await Deno.remove(SOCK); } catch {} 84const listener = Deno.listen({ path: SOCK, transport: "unix" }); 85Deno.writeTextFileSync(PID, String(Deno.pid)); 86console.log(`atpam listening on ${SOCK} (PID ${Deno.pid})`); 87 88for await (const conn of listener) { 89 (async () => { 90 const buf = new Uint8Array(4096); 91 const n = await conn.read(buf); 92 if (!n) return conn.close(); 93 //try { 94 const cred = JSON.parse(new TextDecoder().decode(buf.slice(0, n))); 95 const res = await authenticate(cred); 96 await conn.write(new TextEncoder().encode(JSON.stringify(res))); 97 //} catch { 98 // await conn.write(new TextEncoder().encode(JSON.stringify({ok:false}))); 99 //} 100 conn.close(); 101 })(); 102}