This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

nanopub / src / auth.ts
4.3 kB 143 lines
1/** 2 * Rookery DPoP auth (Welcome Mat v1.0) for researcher.pds.latha.org 3 * Copied from papers project — same identity, same PDS. 4 */ 5 6import crypto from "node:crypto"; 7import { readFile } from "node:fs/promises"; 8 9const PDS_ORIGIN = "https://pds.latha.org"; 10const RESEARCHER_DID = "did:plc:3kkhul7jznlb6ba7rprzawnj"; 11const RESEARCHER_HANDLE = "researcher.pds.latha.org"; 12 13function base64url(input: Buffer | Uint8Array): string { 14 return Buffer.from(input).toString("base64url"); 15} 16 17function pemToJwk(pem: string): { kty: string; n: string; e: string } { 18 const key = crypto.createPublicKey(pem); 19 const jwk = key.export({ format: "jwk" }); 20 return { kty: jwk.kty as string, n: jwk.n as string, e: jwk.e as string }; 21} 22 23function computeThumbprint(jwk: { kty: string; n: string; e: string }): string { 24 return base64url( 25 crypto.createHash("sha256") 26 .update(JSON.stringify({ e: jwk.e, kty: "RSA", n: jwk.n })) 27 .digest(), 28 ); 29} 30 31function createJwt(header: object, payload: object, privateKeyPem: string): string { 32 const encode = (obj: object) => base64url(Buffer.from(JSON.stringify(obj))); 33 const signingInput = `${encode(header)}.${encode(payload)}`; 34 const sig = crypto.createSign("SHA256"); 35 sig.update(signingInput); 36 return `${signingInput}.${base64url(sig.sign(privateKeyPem))}`; 37} 38 39function createDpopProof( 40 method: string, 41 url: string, 42 accessToken: string, 43 publicJwk: { kty: string; n: string; e: string }, 44 privateKeyPem: string, 45): string { 46 const ath = base64url(crypto.createHash("sha256").update(accessToken).digest()); 47 return createJwt( 48 { typ: "dpop+jwt", alg: "RS256", jwk: publicJwk }, 49 { 50 jti: crypto.randomUUID(), 51 htm: method, 52 htu: url, 53 iat: Math.floor(Date.now() / 1000), 54 ath, 55 }, 56 privateKeyPem, 57 ); 58} 59 60export interface PdsSession { 61 did: string; 62 handle: string; 63 accessJwt: string; 64 wmJwt: string; 65 privateKeyPem: string; 66 publicJwk: { kty: string; n: string; e: string }; 67} 68 69export async function createPdsSession(privateKeyPem: string): Promise<PdsSession> { 70 const publicJwk = pemToJwk(privateKeyPem); 71 const thumbprint = computeThumbprint(publicJwk); 72 73 const tosRes = await fetch(`${PDS_ORIGIN}/tos`); 74 if (!tosRes.ok) throw new Error(`Failed to fetch ToS: ${tosRes.status}`); 75 const tosText = await tosRes.text(); 76 const tosHash = base64url(crypto.createHash("sha256").update(tosText).digest()); 77 78 const wmJwt = createJwt( 79 { typ: "wm+jwt", alg: "RS256" }, 80 { 81 tos_hash: tosHash, 82 aud: PDS_ORIGIN, 83 cnf: { jkt: thumbprint }, 84 iat: Math.floor(Date.now() / 1000), 85 }, 86 privateKeyPem, 87 ); 88 89 const sessionUrl = `${PDS_ORIGIN}/xrpc/com.atproto.server.createSession`; 90 const dpop = createDpopProof("POST", sessionUrl, wmJwt, publicJwk, privateKeyPem); 91 92 const res = await fetch(sessionUrl, { 93 method: "POST", 94 headers: { 95 "Content-Type": "application/json", 96 Authorization: `DPoP ${wmJwt}`, 97 DPoP: dpop, 98 }, 99 body: JSON.stringify({}), 100 }); 101 102 if (!res.ok) { 103 const body = await res.text(); 104 throw new Error(`createSession failed (${res.status}): ${body}`); 105 } 106 107 const data = await res.json() as { did: string; handle: string; accessJwt: string }; 108 return { 109 did: data.did || RESEARCHER_DID, 110 handle: data.handle || RESEARCHER_HANDLE, 111 accessJwt: data.accessJwt || wmJwt, 112 wmJwt, 113 privateKeyPem, 114 publicJwk, 115 }; 116} 117 118export async function dpopFetch( 119 url: string, 120 init: RequestInit, 121 session: PdsSession, 122): Promise<Response> { 123 const method = init.method || "GET"; 124 const accessToken = session.wmJwt; 125 const dpop = createDpopProof(method, url, accessToken, session.publicJwk, session.privateKeyPem); 126 127 const headers = new Headers(init.headers); 128 headers.set("Authorization", `DPoP ${accessToken}`); 129 headers.set("DPoP", dpop); 130 if (!headers.has("Content-Type")) { 131 headers.set("Content-Type", "application/json"); 132 } 133 134 return fetch(url, { ...init, headers }); 135} 136 137export async function loadPrivateKey(): Promise<string> { 138 if (process.env.RESEARCHER_KEY_PEM) { 139 return process.env.RESEARCHER_KEY_PEM; 140 } 141 const keyPath = process.env.RESEARCHER_KEY_PATH || "/home/nandi/vault/.credentials/researcher-pds-latha-org.pem"; 142 return readFile(keyPath, "utf-8"); 143}