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.
This commit is contained in:
Julien Calixte
2026-05-05 12:32:43 +02:00
parent 1ce5d9150d
commit e0fe4ce16f
2 changed files with 145 additions and 1 deletions

View File

@@ -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",

143
scripts/manage-webhooks.ts Normal file
View File

@@ -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 <receiver-url> \\
[--verb create|delete|bulk-create] \\
[--method POST] \\
[--token <outbound-bearer>]
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<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;
};
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<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") {
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<string, unknown> = {
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();