1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
const args = new Map<string, string>();
for (let i = 0; i < Deno.args.length; i += 2) args.set(Deno.args[i].replace(/^--/, ""), Deno.args[i + 1]);
const credsPath = args.get("creds");
const account = args.get("account");
const subjectDid = args.get("bidder-did") ?? args.get("requester-did") ?? args.get("did");
const roleArg = args.get("role")?.trim();
const serviceArg = args.get("service")?.trim();
const nonceArg = args.get("nonce")?.trim();
const deleteStale = args.get("delete-stale-service") === "true";
if (!credsPath || !account || !subjectDid) {
console.error("usage: associate-bidder.ts --creds FILE --account BOB (--bidder-did DID | --requester-did DID) [--service bidder_associate|requester_associate] [--nonce N] [--delete-stale-service true]");
Deno.exit(2);
}
const subjectFlag = args.has("requester-did") ? "requester" : args.has("bidder-did") ? "bidder" : "";
const role = roleArg ?? (subjectFlag || (serviceArg === "requester_associate" ? "requester" : "bidder"));
if (role !== "bidder" && role !== "requester") {
console.error(`unknown role ${role}; want bidder or requester`);
Deno.exit(2);
}
if (subjectFlag && roleArg && subjectFlag !== roleArg) {
console.error(`--${subjectFlag}-did does not match role ${roleArg}`);
Deno.exit(2);
}
const service = serviceArg ?? `${role}_associate`;
if (service !== "bidder_associate" && service !== "requester_associate") {
console.error(`unknown service ${service}; want bidder_associate or requester_associate`);
Deno.exit(2);
}
if (service !== `${role}_associate`) {
console.error(`role ${role} does not match service ${service}`);
Deno.exit(2);
}
const text = await Deno.readTextFile(credsPath);
const env: Record<string, string> = {};
for (const line of text.split("\n")) {
const m = line.match(/^\s*(?:export\s+)?([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
if (m) env[m[1]] = m[2].replace(/^["']|["']$/g, "");
}
const handle = env[`${account}_ATPROTO_HANDLE`];
const password = env[`${account}_ATPROTO_PASSWORD`];
if (!handle || !password) {
console.error(`missing ${account}_ATPROTO_HANDLE or ${account}_ATPROTO_PASSWORD in ${credsPath}`);
Deno.exit(2);
}
async function json(url: string, init?: RequestInit): Promise<any> {
const res = await fetch(url, init);
const body = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(`${url}: HTTP ${res.status}: ${JSON.stringify(body)}`);
return body;
}
const resolved = await json(`https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`);
const operatorDid: string = resolved.did;
const doc = await json(`https://plc.directory/${operatorDid}`);
const pds = doc.service?.find((s: any) => s.id === "#atproto_pds")?.serviceEndpoint;
if (!pds) throw new Error(`no atproto_pds service in the DID document of ${operatorDid}`);
const session = await json(`${pds}/xrpc/com.atproto.server.createSession`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ identifier: handle, password }),
});
const nonce = nonceArg || crypto.randomUUID().replace(/-/g, "");
const rkey = `${service}-${subjectDid.replace(/:/g, "-")}`;
const record = {
$type: "com.publicdomainrelay.temp.badgeBlueKeys",
keyId: subjectDid,
challenge: operatorDid,
service,
nonce,
name: `${role}-${subjectDid.slice(-8)}`,
createdAt: new Date().toISOString(),
};
const written = await json(`${pds}/xrpc/com.atproto.repo.putRecord`, {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${session.accessJwt}` },
body: JSON.stringify({
repo: operatorDid,
collection: "com.publicdomainrelay.temp.badgeBlueKeys",
rkey,
record,
}),
});
if (deleteStale) {
const staleService = service === "bidder_associate" ? "requester_associate" : "bidder_associate";
const staleRkey = `${staleService}-${subjectDid.replace(/:/g, "-")}`;
try {
await json(`${pds}/xrpc/com.atproto.repo.deleteRecord`, {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${session.accessJwt}` },
body: JSON.stringify({ repo: operatorDid, collection: "com.publicdomainrelay.temp.badgeBlueKeys", rkey: staleRkey }),
});
console.error(JSON.stringify({ deletedStaleService: staleService, rkey: staleRkey }));
} catch {
}
}
console.log(JSON.stringify({ operatorDid, handle, service, rkey, uri: written.uri, cid: written.cid }));