From e0fe4ce16f2c6f055c487a2ab606253d3649c82f Mon Sep 17 00:00:00 2001 From: Julien Calixte Date: Tue, 5 May 2026 12:32:43 +0200 Subject: [PATCH] chore(scripts): add deno task webhooks for register and delete-all Wraps the createSession + Authorization: Bearer flow so callers don't need to assemble curl by hand. Reads BSKY_IDENTIFIER / BSKY_PASSWORD / BSKY_PDS / REMANSO_API from env (or matching flags). Defaults to bsky.social so non-bsky-hosted accounts must set BSKY_PDS explicitly, e.g. BSKY_PDS=https://eurosky.social. --- deno.json | 3 +- scripts/manage-webhooks.ts | 143 +++++++++++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 scripts/manage-webhooks.ts diff --git a/deno.json b/deno.json index bc6d46b..d42734d 100644 --- a/deno.json +++ b/deno.json @@ -4,7 +4,8 @@ "jetstream": "deno run --watch --allow-net --allow-read --allow-write --allow-env --allow-ffi --unstable-ffi jetstream.ts", "server": "deno run --watch --allow-net --allow-read --allow-write --allow-env --allow-ffi --unstable-ffi server.ts", "server:prod": "deno run --allow-net --allow-read --allow-write --allow-env --allow-ffi --unstable-ffi server.ts", - "migrate": "deno run --allow-net --allow-read --allow-write --allow-env --allow-ffi --unstable-ffi src/migrations/init.ts" + "migrate": "deno run --allow-net --allow-read --allow-write --allow-env --allow-ffi --unstable-ffi src/migrations/init.ts", + "webhooks": "deno run --allow-net --allow-env scripts/manage-webhooks.ts" }, "imports": { "@db/sqlite": "jsr:@db/sqlite@^0.13.0", diff --git a/scripts/manage-webhooks.ts b/scripts/manage-webhooks.ts new file mode 100644 index 0000000..35bdcd3 --- /dev/null +++ b/scripts/manage-webhooks.ts @@ -0,0 +1,143 @@ +// Manage webhook subscriptions on the Remanso API using a Bluesky session +// token from your own PDS. Usage: +// +// 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 + +const HELP = ` +Usage: + deno task webhooks register \\ + --url \\ + [--verb create|delete|bulk-create] \\ + [--method POST] \\ + [--token ] + + 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) +`; + +type CreateSessionResponse = { + did: string; + accessJwt: string; +}; + +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; +}; + +const die = (msg: string): never => { + console.error(`error: ${msg}`); + console.error(HELP); + Deno.exit(1); +}; + +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(); +}; + +const main = async () => { + const [command, ...rest] = Deno.args; + if (!command || command === "--help" || command === "-h") { + console.log(HELP); + Deno.exit(command ? 0 : 1); + } + + 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 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"); + + const session = await createSession(pds, identifier, password); + console.log(`[auth] verified DID: ${session.did} via ${pds}`); + + if (command === "register") { + const url = flags.url ?? die("--url is required for register"); + const body: Record = { + method: flags.method ?? "POST", + url, + }; + if (flags.verb) body.verb = flags.verb; + if (flags.token) body.token = flags.token; + const res = await fetch(`${api}/${session.did}/webhooks`, { + method: "POST", + headers: { + "Authorization": `Bearer ${session.accessJwt}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + if (!res.ok) { + console.error(`register failed (${res.status}): ${await res.text()}`); + Deno.exit(1); + } + console.log(JSON.stringify(await res.json(), null, 2)); + return; + } + + if (command === "delete-all") { + const res = await fetch(`${api}/${session.did}/webhooks`, { + method: "DELETE", + headers: { "Authorization": `Bearer ${session.accessJwt}` }, + }); + if (!res.ok) { + console.error(`delete failed (${res.status}): ${await res.text()}`); + Deno.exit(1); + } + console.log(`[done] all webhooks for ${session.did} deleted`); + return; + } + + die(`unknown command: ${command}`); +}; + +await main();