Alternative web application for the pdsadmin command
at main 4.9 kB view raw
1import { Client, simpleFetchHandler } from "@atcute/client"; 2 3import type { Did } from "./types"; 4 5export class PDS { 6 readonly #rpc; 7 readonly #headers; 8 readonly #service: string; 9 10 constructor({ 11 service, 12 adminPassword, 13 }: { 14 service: string; 15 adminPassword: string; 16 }) { 17 const handler = simpleFetchHandler({ service }); 18 this.#rpc = new Client({ handler }); 19 this.#headers = { 20 Authorization: `Basic ${btoa(`admin:${adminPassword}`)}`, 21 }; 22 this.#service = service; 23 } 24 25 async listRepos(params?: { limit?: number; cursor?: string }) { 26 const { data, ok } = await this.#rpc.get("com.atproto.sync.listRepos", { 27 params: { 28 limit: params?.limit, 29 cursor: params?.cursor, 30 }, 31 headers: this.#headers, 32 }); 33 if (!ok) { 34 throw new Error(data.message ?? data.error); 35 } 36 const dids = data.repos.map((repo) => repo.did); 37 if (dids.length === 0) { 38 return { repos: [], cursor: data.cursor }; 39 } 40 const accountInfos = await this.#getAccountInfos(dids); 41 const accountInfoMap = new Map( 42 accountInfos.map((info) => [info.did, info]), 43 ); 44 const repos = data.repos.map((repoInfo) => { 45 const accountInfo = accountInfoMap.get(repoInfo.did); 46 if (!accountInfo) { 47 throw new Error(`Account info not found for DID: ${repoInfo.did}`); 48 } 49 return { 50 repoInfo, 51 accountInfo, 52 }; 53 }); 54 return { repos, cursor: data.cursor }; 55 } 56 57 async #getAccountInfos(dids: Did[]) { 58 const { data, ok } = await this.#rpc.get( 59 "com.atproto.admin.getAccountInfos", 60 { 61 params: { 62 dids, 63 }, 64 headers: this.#headers, 65 }, 66 ); 67 if (!ok) { 68 throw new Error(data.message ?? data.error); 69 } 70 return data.infos; 71 } 72 73 async createInviteCode() { 74 const { data, ok } = await this.#rpc.post( 75 "com.atproto.server.createInviteCode", 76 { 77 input: { 78 useCount: 1, 79 }, 80 headers: this.#headers, 81 }, 82 ); 83 if (!ok) { 84 throw new Error(data.message ?? data.error); 85 } 86 return data.code; 87 } 88 89 async createAccount({ 90 handle, 91 email, 92 password, 93 }: { 94 handle: `${string}.${string}`; 95 email: string; 96 password: string; 97 }) { 98 const inviteCode = await this.createInviteCode(); 99 const { data, ok } = await this.#rpc.post( 100 "com.atproto.server.createAccount", 101 { 102 input: { 103 handle, 104 email, 105 password, 106 inviteCode, 107 }, 108 as: "json", 109 }, 110 ); 111 if (!ok) { 112 throw new Error(data.message ?? data.error); 113 } 114 return data.did; 115 } 116 117 async resetPassword(did: Did, password: string) { 118 const { data, ok } = await this.#rpc.post( 119 "com.atproto.admin.updateAccountPassword", 120 { 121 input: { 122 did, 123 password, 124 }, 125 headers: this.#headers, 126 as: "blob", 127 }, 128 ); 129 if (!ok) { 130 throw new Error(data.message ?? data.error); 131 } 132 } 133 134 async takedown(did: Did, ref: string) { 135 const { data, ok } = await this.#rpc.post( 136 "com.atproto.admin.updateSubjectStatus", 137 { 138 input: { 139 subject: { 140 $type: "com.atproto.admin.defs#repoRef", 141 did, 142 }, 143 takedown: { 144 applied: true, 145 ref, 146 }, 147 }, 148 headers: this.#headers, 149 as: "json", 150 }, 151 ); 152 if (!ok) { 153 throw new Error(data.message ?? data.error); 154 } 155 } 156 157 async untakedown(did: Did) { 158 const { data, ok } = await this.#rpc.post( 159 "com.atproto.admin.updateSubjectStatus", 160 { 161 input: { 162 subject: { 163 $type: "com.atproto.admin.defs#repoRef", 164 did, 165 }, 166 takedown: { 167 applied: false, 168 }, 169 }, 170 headers: this.#headers, 171 as: "json", 172 }, 173 ); 174 if (!ok) { 175 throw new Error(data.message ?? data.error); 176 } 177 } 178 179 async deleteAccount(did: Did) { 180 const { data, ok } = await this.#rpc.post( 181 "com.atproto.admin.deleteAccount", 182 { 183 input: { 184 did, 185 }, 186 headers: this.#headers, 187 as: "blob", 188 }, 189 ); 190 if (!ok) { 191 throw new Error(data.message ?? data.error); 192 } 193 } 194 195 async requestCrawl(relayService: string) { 196 const handler = simpleFetchHandler({ 197 service: relayService, 198 }); 199 const rpc = new Client({ handler }); 200 const { data, ok } = await rpc.post("com.atproto.sync.requestCrawl", { 201 input: { 202 hostname: new URL(this.#service).hostname, 203 }, 204 headers: this.#headers, 205 as: "blob", 206 }); 207 if (!ok) { 208 throw new Error(data.message ?? data.error); 209 } 210 } 211} 212 213export type Repository = Awaited< 214 ReturnType<typeof PDS.prototype.listRepos> 215>["repos"][number];