This repository has no description
0

Configure Feed

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

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