diff --git a/scripts/manage-webhooks.ts b/scripts/manage-webhooks.ts index 35bdcd3..517f418 100644 --- a/scripts/manage-webhooks.ts +++ b/scripts/manage-webhooks.ts @@ -1,21 +1,14 @@ -// Manage webhook subscriptions on the Remanso API using a Bluesky session -// token from your own PDS. Usage: +// Manage webhook subscriptions on the Remanso API. Resolves your PDS from +// your AT Protocol handle, logs in with an app password, then registers or +// deletes webhooks against the Remanso API. // // deno task webhooks register --url https://your-receiver --verb bulk-create // deno task webhooks delete-all // -// Required env vars (or matching --flags): -// BSKY_IDENTIFIER your handle (e.g. alice.eurosky.social) or email -// BSKY_PASSWORD app password (NOT your main password) -// BSKY_PDS your PDS base URL (default: https://bsky.social) -// REMANSO_API Remanso API base URL (default: https://api.remanso.space) -// -// Example for an eurosky-hosted account: -// -// BSKY_IDENTIFIER=alice.eurosky.social \ -// BSKY_PASSWORD='xxxx-xxxx-xxxx-xxxx' \ -// BSKY_PDS=https://eurosky.social \ -// deno task webhooks register --url https://example.com/hook --verb bulk-create +// Inputs (env or flag, env preferred): +// ATPROTO_HANDLE / --handle e.g. alice.eurosky.social +// ATPROTO_APP_PASSWORD / --app-password app password (NOT your account password) +// REMANSO_API / --api default: https://api.remanso.space const HELP = ` Usage: @@ -27,17 +20,19 @@ Usage: deno task webhooks delete-all -Auth (env or flag, env preferred): - BSKY_IDENTIFIER / --identifier - BSKY_PASSWORD / --password - BSKY_PDS / --pds (default: https://bsky.social) - REMANSO_API / --api (default: https://api.remanso.space) +Inputs (env or flag, env preferred): + ATPROTO_HANDLE / --handle your AT Protocol handle + ATPROTO_APP_PASSWORD / --app-password app password (NOT your account password) + REMANSO_API / --api default: https://api.remanso.space + +Your PDS is resolved automatically from the handle. `; -type CreateSessionResponse = { - did: string; - accessJwt: string; +type ResolveHandleResponse = { did: string }; +type DidDocument = { + service?: { id: string; serviceEndpoint: string }[]; }; +type CreateSessionResponse = { did: string; accessJwt: string }; const parseArgs = (args: string[]): Record => { const out: Record = {}; @@ -62,6 +57,35 @@ const die = (msg: string): never => { Deno.exit(1); }; +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; +}; + +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; +}; + const createSession = async ( pds: string, identifier: string, @@ -88,17 +112,19 @@ const main = async () => { } const flags = parseArgs(rest); - const identifier = flags.identifier ?? Deno.env.get("BSKY_IDENTIFIER"); - const password = flags.password ?? Deno.env.get("BSKY_PASSWORD"); - const pds = flags.pds ?? Deno.env.get("BSKY_PDS") ?? "https://bsky.social"; + 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 (!identifier) die("BSKY_IDENTIFIER (or --identifier) is required"); - if (!password) die("BSKY_PASSWORD (or --password) is required"); + if (!handle) die("ATPROTO_HANDLE (or --handle) is required"); + if (!password) die("ATPROTO_APP_PASSWORD (or --app-password) is required"); - const session = await createSession(pds, identifier, password); - console.log(`[auth] verified DID: ${session.did} via ${pds}`); + const did = await resolveHandleToDid(handle); + const pds = await resolveDidToPds(did); + console.log(`[resolve] ${handle} → ${did} via ${pds}`); + const session = await createSession(pds, handle, password); if (command === "register") { const url = flags.url ?? die("--url is required for register");