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&hellip;</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&hellip;</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&hellip;</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>