Alternative web application for the
pdsadmin command
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];