refactor(scripts): extract atproto session helpers to shared module
This commit is contained in:
97
scripts/_atproto-session.ts
Normal file
97
scripts/_atproto-session.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
// 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<string, string> => {
|
||||
const out: Record<string, string> = {};
|
||||
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<string> => {
|
||||
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<string> => {
|
||||
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<CreateSessionResponse> => {
|
||||
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<string, string>,
|
||||
): 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 };
|
||||
};
|
||||
@@ -32,28 +32,12 @@ Inputs (env or flag, env preferred):
|
||||
Your PDS is resolved automatically from the handle.
|
||||
`;
|
||||
|
||||
type ResolveHandleResponse = { did: string };
|
||||
type DidDocument = {
|
||||
service?: { id: string; serviceEndpoint: string }[];
|
||||
};
|
||||
type CreateSessionResponse = { did: string; accessJwt: string };
|
||||
|
||||
const parseArgs = (args: string[]): Record<string, string> => {
|
||||
const out: Record<string, string> = {};
|
||||
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;
|
||||
};
|
||||
import {
|
||||
createSession,
|
||||
parseArgs,
|
||||
resolveDidToPds,
|
||||
resolveHandleToDid,
|
||||
} from "./_atproto-session.ts";
|
||||
|
||||
const die = (msg: string): never => {
|
||||
console.error(`error: ${msg}`);
|
||||
@@ -61,53 +45,6 @@ const die = (msg: string): never => {
|
||||
Deno.exit(1);
|
||||
};
|
||||
|
||||
const resolveHandleToDid = async (handle: string): Promise<string> => {
|
||||
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;
|
||||
};
|
||||
|
||||
const resolveDidToPds = async (did: string): Promise<string> => {
|
||||
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;
|
||||
};
|
||||
|
||||
const createSession = async (
|
||||
pds: string,
|
||||
identifier: string,
|
||||
password: string,
|
||||
): Promise<CreateSessionResponse> => {
|
||||
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();
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
const [command, ...rest] = Deno.args;
|
||||
if (!command || command === "--help" || command === "-h") {
|
||||
|
||||
Reference in New Issue
Block a user