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 goto(identifier) {
82 this.identifier = identifier;
83 await this.diagnose();
84 },
85
86 async diagnose() {
87 this.identifierLoading = true;
88 this.identifierError = null;
89 this.pds = null;
90 this.did = null;
91 this.handle = null;
92 this.identifier = this.identifier.trim();
93 if (this.identifier === '') {
94 // do nothing
95 } else if (this.identifier.startsWith('https://')) {
96 this.pds = this.identifier;
97 } else {
98 if (this.identifier.startsWith('at://')) {
99 this.identifier = this.identifier.slice('at://'.length);
100 }
101 if (this.identifier.startsWith('did:')) {
102 this.did = this.identifier;
103 let data;
104 try {
105 data = await window.slingshot('com.bad-example.identity.resolveMiniDoc', {
106 params: { identifier: this.identifier },
107 });
108 this.pds = data.pds;
109 this.handle = data.handle;
110 } catch (e) {
111 if (window.isXrpcErr(e)) {
112 this.identifierError = e.error;
113 if (e.message) this.description += ` ${e.description}`;
114 } else {
115 this.identifierError = 'Failed to resolve identifier, see console for error.';
116 console.error(e);
117 }
118 }
119 } else {
120 this.handle = this.identifier;
121 let data;
122 try {
123 data = await window.slingshot('com.bad-example.identity.resolveMiniDoc', {
124 params: { identifier: this.identifier },
125 });
126 this.did = data.did;
127 this.pds = data.pds;
128 } catch (e) {
129 if (window.isXrpcErr(e)) {
130 this.identifierError = e.error;
131 if (e.message) this.description += ` ${e.description}`;
132 } else {
133 this.identifierError = 'Failed to resolve identifier, see console for error.';
134 console.error(e);
135 }
136 }
137 }
138 }
139 this.identifierLoading = false;
140 },
141 }));
142
143 Alpine.data('pdsCheck', pds => ({
144 loadingDesc: false,
145 error: null,
146 description: null,
147 accounts: [],
148 accountsComplete: false,
149
150 async init() {
151 await this.update(pds);
152 },
153
154 async update(pds) {
155 this.loadingDesc = true;
156 this.error = null;
157 this.description = null;
158 this.accounts = [];
159 let query = window.SimpleQuery(pds);
160 try {
161 this.description = await query('com.atproto.server.describeServer');
162 } catch (e) {
163 if (window.isXrpcErr(e)) {
164 this.error = e.error;
165 } else {
166 this.error = 'Failed to reach (see console)';
167 console.error(e);
168 }
169 }
170 let accountsRes;
171 try {
172 accountsRes = await query('com.atproto.sync.listRepos', {
173 params: { limit: 7 },
174 });
175 this.accounts = accountsRes.repos;
176 this.accountsComplete == !accountsRes.cursor;
177 } catch (e) {
178 if (window.isXrpcErr(e)) {
179 this.error = e.error;
180 } else {
181 this.error = 'Failed to reach (see console)';
182 console.error(e);
183 }
184 }
185 this.loadingDesc = false;
186 },
187 }));
188
189 Alpine.data('relayCheckHost', (pds, relay) => ({
190 loading: false,
191 error: null,
192 status: null,
193 reqCrawlStatus: null,
194 reqCrawlError: null,
195
196 async init() {
197 await this.check(pds, relay);
198 },
199
200 async check(pds, relay) {
201 this.loading = true;
202 this.error = null;
203 this.status = null;
204 const query = window.SimpleQuery(`https://${relay.hostname}`);
205 const hostname = pds.split('://')[1];
206 let data;
207 try {
208 data = await query('com.atproto.sync.getHostStatus', {
209 params: { hostname },
210 });
211 this.status = data.status;
212 } catch(e) {
213 if (window.isXrpcErr(e)) {
214 this.error = e.error;
215 } else {
216 this.error = 'Failed to check (see console)';
217 console.error(e);
218 }
219 }
220 this.loading = false;
221 this.reqCrawlStatus = null;
222 this.reqCrawlError = null;
223 },
224
225 async requestCrawl(pds, relay) {
226 this.reqCrawlStatus = "loading";
227 const proc = window.SimpleProc(`https://${relay.hostname}`);
228 const hostname = pds.split('://')[1];
229 let data;
230 try {
231 data = await proc('com.atproto.sync.requestCrawl', {
232 input: { hostname },
233 });
234 } catch (e) {
235 if (window.isXrpcErr(e)) {
236 this.reqCrawlError = e.error;
237 } else {
238 this.reqCrawlError = 'failed (see console)';
239 console.error(e);
240 }
241 }
242 this.reqCrawlStatus = "done";
243 },
244 }));
245
246 Alpine.data('checkHandle', handle => ({
247 loading: false,
248 dnsDid: null,
249 dnsErr: null,
250 httpDid: null,
251 httpErr: null,
252
253 async init() {
254 await this.updateHandle(handle);
255 },
256 async updateHandle(handle) {
257 this.loading = true;
258 this.dnsDid = null;
259 this.dnsErr = null;
260 this.httpDid = null;
261 this.httpErr = null;
262 try {
263 this.dnsDid = await window.dnsResolver.resolve(handle);
264 } catch (e) {
265 this.dnsErr = e.name;
266 }
267 try {
268 this.httpDid = await window.httpResolver.resolve(handle);
269 } catch (e) {
270 this.httpErr = e.name;
271 }
272 this.loading = false;
273 },
274 }));
275 })
276 </script>
277 </head>
278 <body x-data="debug">
279 <div class="hero bg-base-200">
280 <div class="hero-content flex-col">
281 <h1>PDS Debugger</h1>
282
283 <p>Work in progress!</p>
284 <details class="text-xs">
285 <summary>Would be nice</summary>
286 <ul>
287 <li>anything that actually works</li>
288 <li>firehose listener for missing pds events</li>
289 <li>jetstream listener for missing pds events</li>
290 <li>check relays for account status</li>
291 <li>check relays for pds state</li>
292 <li>plc: check old pds hosts for active account state</li>
293 </ul>
294 </details>
295 <details class="text-xs">
296 <summary>Limitations</summary>
297 <ul>
298 <li>it's all client-side</li>
299 </ul>
300 </details>
301
302 <div class="card bg-base-100 w-full max-w-sm shrink-0 shadow-2xl">
303 <div class="card-body">
304 <form @submit.prevent="await diagnose()">
305 <label>
306 Enter an atproto handle, DID, or HTTPS PDS URL
307 <input
308 class="input"
309 x-model="identifier"
310 :disabled="identifierLoading"
311 autofocus
312 />
313 </label>
314 </form>
315 </div>
316 </div>
317
318 <template x-if="identifierError">
319 <p>uh oh: <span x-text="identifierError"></span></p>
320 </template>
321
322 <template x-if="pds != null">
323 <div class="card bg-base-100 w-full max-w-lg shrink-0 shadow-2xl">
324 <div class="card-body">
325 <h2 class="card-title">
326 <span class="badge badge-secondary">PDS</span>
327 <span x-text="pds"></span>
328 </h2>
329
330 <div
331 x-data="pdsCheck(pds)"
332 x-init="$watch('pds', v => update(v))"
333 >
334 <h3 class="text-lg">
335 Server
336 <span
337 x-show="description !== null"
338 class="badge badge-sm badge-soft badge-success"
339 >online</span>
340 </h3>
341 <p x-show="loadingDesc">Loading…</p>
342 <p x-show="error" class="text-warning" x-text="error"></p>
343 <template x-if="description !== null">
344 <div class="overflow-x-auto">
345 <table class="table table-xs">
346 <tbody>
347 <tr>
348 <td class="text-sm">Open registration</td>
349 <td
350 class="text-sm"
351 x-text="!description.inviteCodeRequired"
352 ></td>
353 </tr>
354 </tbody>
355 </table>
356 <h4 class="font-bold">
357 Accounts
358 </h4>
359 <table class="table table-xs">
360 <tbody>
361 <template x-for="account in accounts">
362 <tr>
363 <td>
364 <code>
365 <a
366 href="#"
367 class="link"
368 x-text="account.did"
369 @click.prevent="goto(account.did)"
370 ></a>
371 </code>
372 </td>
373 <td>
374 <span
375 x-show="account.active"
376 class="badge badge-sm badge-soft badge-success"
377 >
378 active
379 </span>
380 <span
381 x-show="!account.active"
382 x-text="account.status"
383 class="badge badge-sm badge-soft badge-warning"
384 ></span>
385 </td>
386 </tr>
387 </template>
388 <template x-if="!accountsComplete">
389 <tr>
390 <td colspan="2" class="text-sm text-warning-content">
391 (account list clipped)
392 </td>
393 </tr>
394 </template>
395 </tbody>
396 </table>
397 </div>
398 </template>
399 </div>
400
401 <h3 class="text-lg">Relay host status</h3>
402 <div class="overflow-x-auto">
403 <table class="table table-xs">
404 <tbody>
405 <template x-for="relay in window.relays">
406 <tr
407 x-data="relayCheckHost(pds, relay)"
408 x-init="$watch('pds', pds => check(pds, relay))"
409 >
410 <td x-text="relay.name" class="text-sm"></td>
411 <td>
412 <template x-if="loading">
413 <em>loading…</em>
414 </template>
415 <template x-if="error">
416 <span
417 x-text="error"
418 class="text-xs text-warning"
419 ></span>
420 </template>
421 <template x-if="status">
422 <span
423 x-text="status"
424 class="badge badge-sm"
425 :class="status === 'active' && 'badge-soft badge-success'"
426 ></span>
427 </template>
428 </td>
429 <td>
430 <div x-show="status !== 'active'">
431 <button
432 x-show="reqCrawlStatus !== 'done'"
433 class="btn btn-xs btn-ghost whitespace-nowrap"
434 :disabled="reqCrawlStatus === 'loading'"
435 @click="requestCrawl(pds, relay)"
436 >
437 request crawl
438 </button>
439 <span
440 x-show="reqCrawlError !== null"
441 x-text="reqCrawlError"
442 class="text-xs text-warning"
443 ></span>
444 <button
445 x-show="reqCrawlError === null && reqCrawlStatus === 'done'"
446 class="btn btn-xs btn-soft btn-primary whitespace-nowrap"
447 @click="check"
448 >
449 refresh
450 </button>
451 </div>
452 </td>
453 </tr>
454 </template>
455 </tbody>
456 </table>
457 </div>
458 </div>
459 </div>
460 </template>
461
462 <template x-if="did != null">
463 <div class="card bg-base-100 w-full max-w-lg shrink-0 shadow-2xl">
464 <div class="card-body">
465 <h2 class="card-title">
466 <span class="badge badge-secondary">DID</span>
467 <code x-text="did"></code>
468 </h2>
469 <p>(wip)</p>
470 </div>
471 </div>
472 </template>
473
474 <template x-if="handle != null">
475 <div class="card bg-base-100 w-full max-w-lg shrink-0 shadow-2xl">
476 <div
477 x-data="checkHandle(handle)"
478 x-init="$watch('handle', h => updateHandle(h))"
479 class="card-body"
480 >
481 <h2 class="card-title">
482 <span class="badge badge-secondary">Handle</span>
483 <span x-text="handle"></span>
484 </h2>
485 <p x-show="loading" class="text-i">Loading…</p>
486 <div x-show="!loading" class="overflow-x-auto">
487 <table class="table table-xs">
488 <tbody>
489 <tr>
490 <td class="text-sm">DNS</td>
491 <td class="text-sm">
492 <code x-text="dnsDid"></code>
493 </td>
494 <td>
495 <div
496 class="badge badge-sm badge-soft badge-neutral"
497 x-show="dnsErr !== null"
498 x-text="dnsErr"
499 ></div>
500 </td>
501 </tr>
502 <tr>
503 <td class="text-sm">Http</td>
504 <td class="text-sm">
505 <code x-text="httpDid"></code>
506 </td>
507 <td>
508 <div
509 class="badge badge-sm badge-soft badge-neutral"
510 x-show="httpErr !== null"
511 x-text="httpErr"
512 ></div>
513 </td>
514 </tr>
515 </tbody>
516 </table>
517 </div>
518 </div>
519 </div>
520 </template>
521 </div>
522 </div>
523 </body>
524</html>