// Shared helpers for admin scripts that authenticate against the Remanso API // using an AT Protocol session. The handle is resolved to a DID, the DID is // resolved to a PDS, and a session is created against that PDS — yielding an // access JWT the API can verify with `authenticateRequest`. type ResolveHandleResponse = { did: string }; type DidDocument = { service?: { id: string; serviceEndpoint: string }[]; }; export type CreateSessionResponse = { did: string; accessJwt: string }; export const parseArgs = (args: string[]): Record => { const out: Record = {}; for (let i = 0; i < args.length; i++) { const arg = args[i]; if (!arg.startsWith("--")) continue; const key = arg.slice(2); const next = args[i + 1]; if (next === undefined || next.startsWith("--")) { out[key] = "true"; } else { out[key] = next; i++; } } return out; }; export const resolveHandleToDid = async (handle: string): Promise => { const url = `https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${ encodeURIComponent(handle) }`; const res = await fetch(url); if (!res.ok) { throw new Error( `resolveHandle failed for ${handle} (${res.status}): ${await res.text()}`, ); } const { did } = await res.json() as ResolveHandleResponse; return did; }; export const resolveDidToPds = async (did: string): Promise => { if (!did.startsWith("did:plc:")) { throw new Error( `Unsupported DID method (server only handles did:plc): ${did}`, ); } const res = await fetch(`https://plc.directory/${did}`); if (!res.ok) { throw new Error(`plc.directory lookup failed (${res.status})`); } const doc = await res.json() as DidDocument; const pds = doc.service?.find((s) => s.id === "#atproto_pds"); if (!pds) throw new Error("No #atproto_pds service in DID document"); return pds.serviceEndpoint; }; export const createSession = async ( pds: string, identifier: string, password: string, ): Promise => { const res = await fetch(`${pds}/xrpc/com.atproto.server.createSession`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ identifier, password }), }); if (!res.ok) { throw new Error( `createSession failed (${res.status}): ${await res.text()}`, ); } return await res.json(); }; // Convenience: pull credentials from flags or env, resolve the full session // chain in one call. Throws if required inputs are missing. export const sessionFromFlagsOrEnv = async ( flags: Record, ): Promise<{ session: CreateSessionResponse; api: string }> => { const handle = flags.handle ?? Deno.env.get("ATPROTO_HANDLE"); const password = flags["app-password"] ?? Deno.env.get("ATPROTO_APP_PASSWORD"); const api = flags.api ?? Deno.env.get("REMANSO_API") ?? "https://api.remanso.space"; if (!handle) throw new Error("ATPROTO_HANDLE (or --handle) is required"); if (!password) { throw new Error("ATPROTO_APP_PASSWORD (or --app-password) is required"); } const did = await resolveHandleToDid(handle); const pds = await resolveDidToPds(did); console.log(`[resolve] ${handle} → ${did} via ${pds}`); const session = await createSession(pds, handle, password); return { session, api }; };