Diagnostics for atproto PDS hosts, DIDs, and handles: https://debug.hose.cam
1<!doctype html>
2<html lang="en">
3 <head>
4 <meta charset="utf-8">
5 <meta name="viewport" content="width=device-width"/>
6 <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
7 <link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css"/>
8 <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
9
10 <script type="module">
11 import {
12 Client,
13 ClientResponseError,
14 ok,
15 simpleFetchHandler,
16 } from 'https://esm.sh/@atcute/client@4.1.1';
17 import {
18 DohJsonHandleResolver,
19 WellKnownHandleResolver,
20 } from 'https://esm.sh/@atcute/identity-resolver@1.2.0';
21
22 window.SimpleQuery = service => {
23 const client = new Client({ handler: simpleFetchHandler({ service }) });
24 return (...args) => ok(client.get(...args));
25 };
26 window.SimpleProc = service => {
27 const client = new Client({ handler: simpleFetchHandler({ service }) });
28 return (...args) => ok(client.post(...args));
29 };
30 window.isXrpcErr = e => e instanceof ClientResponseError;
31
32 window.dnsResolver = new DohJsonHandleResolver({
33 dohUrl: 'https://mozilla.cloudflare-dns.com/dns-query',
34 });
35 window.httpResolver = new WellKnownHandleResolver();
36
37 window.slingshot = window.SimpleQuery('https://slingshot.microcosm.blue');
38 window.relays = [
39 {
40 name: 'Bluesky production',
41 hostname: 'bsky.network',
42 },
43 {
44 name: 'Bluesky sync1.1 East',
45 hostname: 'relay1.us-east.bsky.network',
46 },
47 {
48 name: 'Bluesky sync1.1 West',
49 hostname: 'relay1.us-west.bsky.network',
50 },
51 {
52 name: 'Blacksky',
53 hostname: 'atproto.africa',
54 },
55 {
56 name: 'Microcosm Montreal',
57 hostname: 'relay.fire.hose.cam',
58 },
59 {
60 name: 'Microcosm France',
61 hostname: 'relay3.fr.hose.cam',
62 },
63 ];
64 </script>
65
66 <script>
67 document.addEventListener('alpine:init', () => {
68 Alpine.data('debug', () => ({
69 // form input
70 identifier: '',
71
72 // state
73 identifierLoading: false,
74 identifierError: null,
75
76 // stuff to check
77 pds: null,
78 did: null,
79 handle: null,
80
81 async diagnose() {
82 this.identifierLoading = true;
83 this.identifierError = null;
84 this.pds = null;
85 this.did = null;
86 this.handle = null;
87 this.identifier = this.identifier.trim();
88 if (this.identifier === '') {
89 // do nothing
90 } else if (this.identifier.startsWith('https://')) {
91 this.pds = this.identifier;
92 } else {
93 if (this.identifier.startsWith('at://')) {
94 this.identifier = this.identifier.slice('at://'.length);
95 }
96 if (this.identifier.startsWith('did:')) {
97 this.did = this.identifier;
98 let data;
99 try {
100 data = await window.slingshot('com.bad-example.identity.resolveMiniDoc', {
101 params: { identifier: this.identifier },
102 });
103 this.pds = data.pds;
104 this.handle = data.handle;
105 } catch (e) {
106 if (window.isXrpcErr(e)) {
107 this.identifierError = e.error;
108 if (e.message) this.description += ` ${e.description}`;
109 } else {
110 this.identifierError = 'Failed to resolve identifier, see console for error.';
111 console.error(e);
112 }
113 }
114 } else {
115 this.handle = this.identifier;
116 let data;
117 try {
118 data = await window.slingshot('com.bad-example.identity.resolveMiniDoc', {
119 params: { identifier: this.identifier },
120 });
121 this.did = data.did;
122 this.pds = data.pds;
123 } catch (e) {
124 if (window.isXrpcErr(e)) {
125 this.identifierError = e.error;
126 if (e.message) this.description += ` ${e.description}`;
127 } else {
128 this.identifierError = 'Failed to resolve identifier, see console for error.';
129 console.error(e);
130 }
131 }
132 }
133 }
134 this.identifierLoading = false;
135 },
136 }));
137
138 Alpine.data('pdsCheck', pds => ({
139 loadingDesc: false,
140 error: null,
141 description: null,
142
143 async init() {
144 await this.update(pds);
145 },
146
147 async update(pds) {
148 this.loadingDesc = true;
149 this.error = null;
150 this.description = null;
151 let query = window.SimpleQuery(pds);
152 try {
153 this.description = await query('com.atproto.server.describeServer');
154 } catch (e) {
155 if (window.isXrpcErr(e)) {
156 this.error = e.error;
157 } else {
158 this.error = 'Failed to reach (see console)';
159 console.error(e);
160 }
161 }
162 this.loadingDesc = false;
163 },
164 }));
165
166 Alpine.data('relayCheckHost', (pds, relay) => ({
167 loading: false,
168 error: null,
169 status: null,
170 reqCrawlStatus: null,
171 reqCrawlError: null,
172
173 async init() {
174 await this.check(pds, relay);
175 },
176
177 async check(pds, relay) {
178 this.loading = true;
179 this.error = null;
180 this.status = null;
181 const query = window.SimpleQuery(`https://${relay.hostname}`);
182 const hostname = pds.split('://')[1];
183 let data;
184 try {
185 data = await query('com.atproto.sync.getHostStatus', {
186 params: { hostname },
187 });
188 this.status = data.status;
189 } catch(e) {
190 if (window.isXrpcErr(e)) {
191 this.error = e.error;
192 } else {
193 this.error = 'Failed to check (see console)';
194 console.error(e);
195 }
196 }
197 this.loading = false;
198 this.reqCrawlStatus = null;
199 this.reqCrawlError = null;
200 },
201
202 async requestCrawl(pds, relay) {
203 this.reqCrawlStatus = "loading";
204 const proc = window.SimpleProc(`https://${relay.hostname}`);
205 const hostname = pds.split('://')[1];
206 let data;
207 try {
208 data = await proc('com.atproto.sync.requestCrawl', {
209 input: { hostname },
210 });
211 } catch (e) {
212 if (window.isXrpcErr(e)) {
213 this.reqCrawlError = e.error;
214 } else {
215 this.reqCrawlError = 'failed (see console)';
216 console.error(e);
217 }
218 }
219 this.reqCrawlStatus = "done";
220 },
221 }));
222
223 Alpine.data('checkHandle', handle => ({
224 loading: false,
225 dnsDid: null,
226 dnsErr: null,
227 httpDid: null,
228 httpErr: null,
229
230 async init() {
231 await this.updateHandle(handle);
232 },
233 async updateHandle(handle) {
234 this.loading = true;
235 this.dnsDid = null;
236 this.dnsErr = null;
237 this.httpDid = null;
238 this.httpErr = null;
239 try {
240 this.dnsDid = await window.dnsResolver.resolve(handle);
241 } catch (e) {
242 this.dnsErr = e.name;
243 }
244 try {
245 this.httpDid = await window.httpResolver.resolve(handle);
246 } catch (e) {
247 this.httpErr = e.name;
248 }
249 this.loading = false;
250 },
251 }));
252 })
253 </script>
254 </head>
255 <body x-data="debug">
256 <div class="hero bg-base-200">
257 <div class="hero-content flex-col">
258 <h1>PDS Debugger</h1>
259
260 <p>Work in progress!</p>
261 <details class="text-xs">
262 <summary>Would be nice</summary>
263 <ul>
264 <li>anything that actually works</li>
265 <li>firehose listener for missing pds events</li>
266 <li>jetstream listener for missing pds events</li>
267 <li>check relays for account status</li>
268 <li>check relays for pds state</li>
269 <li>plc: check old pds hosts for active account state</li>
270 </ul>
271 </details>
272 <details class="text-xs">
273 <summary>Limitations</summary>
274 <ul>
275 <li>it's all client-side</li>
276 </ul>
277 </details>
278
279 <div class="card bg-base-100 w-full max-w-sm shrink-0 shadow-2xl">
280 <div class="card-body">
281 <form @submit.prevent="await diagnose()">
282 <label>
283 Enter an atproto handle, DID, or HTTPS PDS URL
284 <input
285 class="input"
286 x-model="identifier"
287 :disabled="identifierLoading"
288 autofocus
289 />
290 </label>
291 </form>
292 </div>
293 </div>
294
295 <template x-if="identifierError">
296 <p>uh oh: <span x-text="identifierError"></span></p>
297 </template>
298
299 <template x-if="pds != null">
300 <div class="card bg-base-100 w-full max-w-lg shrink-0 shadow-2xl">
301 <div class="card-body">
302 <h2 class="card-title">
303 <span class="badge badge-secondary">PDS</span>
304 <span x-text="pds"></span>
305 </h2>
306
307 <div
308 x-data="pdsCheck(pds)"
309 x-init="$watch('pds', v => update(v))"
310 >
311 <h3 class="text-lg">
312 Server
313 <span
314 x-show="description !== null"
315 class="badge badge-sm badge-soft badge-success"
316 >online</span>
317 </h3>
318 <p x-show="loadingDesc">Loading…</p>
319 <p x-show="error" class="text-warning" x-text="error"></p>
320 <template x-if="description !== null">
321 <div class="overflow-x-auto">
322 <table class="table table-xs">
323 <tbody>
324 <tr>
325 <td class="text-sm">Open registration</td>
326 <td
327 class="text-sm"
328 x-text="!description.inviteCodeRequired"
329 ></td>
330 </tr>
331 </tbody>
332 </table>
333 </div>
334 </template>
335 </div>
336
337 <h3 class="text-lg">Relay host status</h3>
338 <div class="overflow-x-auto">
339 <table class="table table-xs">
340 <tbody>
341 <template x-for="relay in window.relays">
342 <tr
343 x-data="relayCheckHost(pds, relay)"
344 x-init="$watch('pds', pds => check(pds, relay))"
345 >
346 <td x-text="relay.name" class="text-sm"></td>
347 <td>
348 <template x-if="loading">
349 <em>loading…</em>
350 </template>
351 <template x-if="error">
352 <span
353 x-text="error"
354 class="text-xs text-warning"
355 ></span>
356 </template>
357 <template x-if="status">
358 <span
359 x-text="status"
360 class="badge badge-sm"
361 :class="status === 'active' && 'badge-soft badge-success'"
362 ></span>
363 </template>
364 </td>
365 <td>
366 <div x-show="status !== 'active'">
367 <button
368 x-show="reqCrawlStatus !== 'done'"
369 class="btn btn-xs btn-ghost whitespace-nowrap"
370 :disabled="reqCrawlStatus === 'loading'"
371 @click="requestCrawl(pds, relay)"
372 >
373 request crawl
374 </button>
375 <span
376 x-show="reqCrawlError !== null"
377 x-text="reqCrawlError"
378 class="text-xs text-warning"
379 ></span>
380 <button
381 x-show="reqCrawlError === null && reqCrawlStatus === 'done'"
382 class="btn btn-xs btn-soft btn-primary whitespace-nowrap"
383 @click="check"
384 >
385 refresh
386 </button>
387 </div>
388 </td>
389 </tr>
390 </template>
391 </tbody>
392 </table>
393 </div>
394 </div>
395 </div>
396 </template>
397
398 <template x-if="did != null">
399 <div class="card bg-base-100 w-full max-w-lg shrink-0 shadow-2xl">
400 <div class="card-body">
401 <h2 class="card-title">
402 <span class="badge badge-secondary">DID</span>
403 <code x-text="did"></code>
404 </h2>
405 <p>(wip)</p>
406 </div>
407 </div>
408 </template>
409
410 <template x-if="handle != null">
411 <div class="card bg-base-100 w-full max-w-lg shrink-0 shadow-2xl">
412 <div
413 x-data="checkHandle(handle)"
414 x-init="$watch('handle', h => updateHandle(h))"
415 class="card-body"
416 >
417 <h2 class="card-title">
418 <span class="badge badge-secondary">Handle</span>
419 <span x-text="handle"></span>
420 </h2>
421 <p x-show="loading" class="text-i">Loading…</p>
422 <div x-show="!loading" class="overflow-x-auto">
423 <table class="table table-xs">
424 <tbody>
425 <tr>
426 <td class="text-sm">DNS</td>
427 <td class="text-sm">
428 <code x-text="dnsDid"></code>
429 </td>
430 <td>
431 <div
432 class="badge badge-sm badge-soft badge-neutral"
433 x-show="dnsErr !== null"
434 x-text="dnsErr"
435 ></div>
436 </td>
437 </tr>
438 <tr>
439 <td class="text-sm">Http</td>
440 <td class="text-sm">
441 <code x-text="httpDid"></code>
442 </td>
443 <td>
444 <div
445 class="badge badge-sm badge-soft badge-neutral"
446 x-show="httpErr !== null"
447 x-text="httpErr"
448 ></div>
449 </td>
450 </tr>
451 </tbody>
452 </table>
453 </div>
454 </div>
455 </div>
456 </template>
457 </div>
458 </div>
459 </body>
460</html>