Diagnostics for atproto PDS hosts, DIDs, and handles: https://debug.hose.cam

Compare changes

Choose any two refs to compare.

icons/bsky-favicon.png

This is a binary file and will not be displayed.

icons/microcosm-favicon.png

This is a binary file and will not be displayed.

+1190 -117
index.html
··· 3 3 <head> 4 4 <meta charset="utf-8"> 5 5 <meta name="viewport" content="width=device-width"/> 6 + <title>atproto PDS & account debugger</title> 7 + <meta name="description" content="Quick diagnostics for PDS hosts, handles, relay connections, handles, DIDs, ..." /> 8 + 9 + <script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/intersect@3.x.x/dist/cdn.min.js"></script> 6 10 <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script> 7 11 <link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css"/> 8 12 <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script> 9 13 10 14 <script type="module"> 11 - import { Client, ClientResponseError, ok, simpleFetchHandler } from 'https://esm.sh/@atcute/client@4.1.1'; 15 + import { 16 + Client, 17 + ClientResponseError, 18 + ok, 19 + simpleFetchHandler, 20 + } from 'https://esm.sh/@atcute/client@4.1.1'; 21 + import { 22 + DohJsonHandleResolver, 23 + WellKnownHandleResolver, 24 + } from 'https://esm.sh/@atcute/identity-resolver@1.2.1'; 25 + 12 26 window.SimpleQuery = service => { 13 27 const client = new Client({ handler: simpleFetchHandler({ service }) }); 14 28 return (...args) => ok(client.get(...args)); ··· 18 32 return (...args) => ok(client.post(...args)); 19 33 }; 20 34 window.isXrpcErr = e => e instanceof ClientResponseError; 35 + 36 + window.isBeforeNow = iso => new Date(iso) < new Date(); 37 + 38 + window.dnsResolver = new DohJsonHandleResolver({ 39 + dohUrl: 'https://mozilla.cloudflare-dns.com/dns-query', 40 + }); 41 + window.httpResolver = new WellKnownHandleResolver(); 42 + 21 43 window.slingshot = window.SimpleQuery('https://slingshot.microcosm.blue'); 22 44 window.relays = [ 23 45 { 24 - name: 'Bluesky production', 46 + name: 'Bluesky', 47 + icon: './icons/bsky-favicon.png', 25 48 hostname: 'bsky.network', 49 + note: 'current', 50 + missingApis: { 51 + ['com.atproto.sync.getHostStatus']: 'missing API (old relay code)', 52 + ['com.atproto.sync.getRepoStatus']: 'missing API (old relay code)', 53 + }, 26 54 }, 27 55 { 28 - name: 'Bluesky sync1.1 East', 29 - hostname: 'relay1.us-east.bsky.network', 56 + name: 'Microcosm Montreal', 57 + icon: './icons/microcosm-favicon.png', 58 + hostname: 'relay.fire.hose.cam', 30 59 }, 31 60 { 32 - name: 'Bluesky sync1.1 West', 33 - hostname: 'relay1.us-west.bsky.network', 61 + name: 'Microcosm France', 62 + icon: './icons/microcosm-favicon.png', 63 + hostname: 'relay3.fr.hose.cam', 64 + }, 65 + { 66 + name: 'Upcloud', 67 + icon: 'https://upcloud.com/media/android-chrome-512x512-2-150x150.png', 68 + hostname: 'relay.upcloud.world', 34 69 }, 35 70 { 36 71 name: 'Blacksky', 72 + icon: 'https://blacksky.community/static/favicon-32x32.png', 37 73 hostname: 'atproto.africa', 74 + missingApis: { 75 + ['com.atproto.sync.getHostStatus']: 'API not yet deployed', 76 + ['com.atproto.sync.getRepoStatus']: 'API not implemented', 77 + }, 38 78 }, 39 79 { 40 - name: 'Microcosm Montreal', 41 - hostname: 'relay.fire.hose.cam', 80 + name: 'Bluesky East', 81 + icon: './icons/bsky-favicon.png', 82 + note: 'future', 83 + hostname: 'relay1.us-east.bsky.network', 42 84 }, 43 85 { 44 - name: 'Microcosm France', 45 - hostname: 'relay3.fr.hose.cam', 86 + name: 'Bluesky West', 87 + icon: './icons/bsky-favicon.png', 88 + note: 'future', 89 + hostname: 'relay1.us-west.bsky.network', 46 90 }, 47 91 ]; 92 + 93 + window.regionalModAccounts = [ // https://github.com/mary-ext/atproto-scraping?tab=readme-ov-file#bluesky-labelers 94 + 'https://mod-br.bsky.app', 95 + 'https://mod-de.bsky.app', 96 + 'https://mod-in.bsky.app', 97 + 'https://mod-ru.bsky.app', 98 + 'https://mod-tr.bsky.app', 99 + ]; 100 + 101 + window.bskyAccountDeathLabels = { 102 + ['needs-review']: 'Automated action, cleared by manual review from Bluesky moderation team. Your content can ve accessed via direct links on Bluesky, but invisible in feeds and replies to posts.', 103 + ['!suspend']: 'Moderation action from Bluesky moderation team. Makes your content inaccessible on the Bluesky app.', 104 + ['!takedown']: 'Moderation action from Bluesky moderation team. Makes your content inaccessible on the Bluesky app.', 105 + ['!hide']: 'Almost always used with !takedown, makes your content inaccessible on the Bluesky app.', 106 + // other labels shouldn't cause problems that make you think your pds is broken 107 + // 'spam': just hides replies by default + makes your posts click-through 108 + // 'intolerant', etc: similar to spam 109 + }; 110 + 111 + if (window.blehYeahReady) blehYeahReady(); 112 + else window.yeahBlehIsReady = true; 48 113 </script> 49 114 115 + <style> 116 + body:not(.ready) .hide-until-ready, 117 + body.ready .show-until-ready { 118 + display: none; 119 + } 120 + </style> 121 + 50 122 <script> 51 123 document.addEventListener('alpine:init', () => { 124 + if (window.yeahBlehIsReady) { 125 + document.body.classList.add('ready'); 126 + } else { 127 + window.blehYeahReady = () => document.body.classList.add('ready'); 128 + } 129 + 52 130 Alpine.data('debug', () => ({ 53 131 // form input 54 132 identifier: '', ··· 61 139 pds: null, 62 140 did: null, 63 141 handle: null, 142 + 143 + async goto(identifier) { 144 + this.identifier = identifier; 145 + await this.diagnose(); 146 + }, 64 147 65 148 async diagnose() { 66 149 this.identifierLoading = true; 67 150 this.identifierError = null; 68 151 this.pds = null; 69 152 this.did = null; 70 - if (this.identifier.startsWith('https://')) { 153 + this.handle = null; 154 + this.identifier = this.identifier.trim(); 155 + if (this.identifier === '') { 156 + // do nothing 157 + } else if (this.identifier.startsWith('https://')) { 71 158 this.pds = this.identifier; 72 159 } else { 73 160 if (this.identifier.startsWith('at://')) { ··· 75 162 } 76 163 if (this.identifier.startsWith('did:')) { 77 164 this.did = this.identifier; 78 - } else { 79 165 let data; 80 166 try { 81 167 data = await window.slingshot('com.bad-example.identity.resolveMiniDoc', { 82 168 params: { identifier: this.identifier }, 83 169 }); 170 + this.pds = data.pds; 171 + this.handle = data.handle; 172 + } catch (e) { 173 + if (window.isXrpcErr(e)) { 174 + this.identifierError = e.error; 175 + if (e.message) this.description += ` ${e.description}`; 176 + } else { 177 + this.identifierError = 'Failed to resolve identifier, see console for error.'; 178 + console.error(e); 179 + } 180 + } 181 + } else { 182 + this.handle = this.identifier.toLowerCase(); 183 + let data; 184 + try { 185 + data = await window.slingshot('com.bad-example.identity.resolveMiniDoc', { 186 + params: { identifier: this.identifier.toLowerCase() }, 187 + }); 84 188 this.did = data.did; 85 189 this.pds = data.pds; 86 190 } catch (e) { ··· 98 202 }, 99 203 })); 100 204 205 + Alpine.data('pdsCheck', pds => ({ 206 + loadingDesc: false, 207 + error: null, 208 + description: null, 209 + accounts: [], 210 + accountsComplete: false, 211 + version: null, 212 + 213 + async init() { 214 + await this.update(pds); 215 + }, 216 + 217 + async update(pds) { 218 + this.loadingDesc = true; 219 + this.error = null; 220 + this.description = null; 221 + this.accounts = []; 222 + this.accountsComplete = false; 223 + this.version = null; 224 + 225 + if (!pds) { 226 + this.loadingDesc = false; 227 + return; 228 + } 229 + 230 + let query = window.SimpleQuery(pds); 231 + try { 232 + this.description = await query('com.atproto.server.describeServer'); 233 + } catch (e) { 234 + if (window.isXrpcErr(e)) { 235 + this.error = e.error; 236 + } else { 237 + this.error = 'Failed to reach (see console)'; 238 + console.error(e); 239 + } 240 + } 241 + let health 242 + try { 243 + health = await query('_health'); 244 + this.version = health.version; 245 + } catch (e) { 246 + if (window.isXrpcErr(e)) { 247 + this.error = e.error; 248 + } else { 249 + this.error = 'Failed to reach (see console)'; 250 + console.error(e); 251 + } 252 + } 253 + let accountsRes; 254 + try { 255 + accountsRes = await query('com.atproto.sync.listRepos', { 256 + params: { limit: 100 }, 257 + }); 258 + this.accounts = accountsRes.repos; 259 + 260 + // weird thing with the ref pds: it *always* has a cursor on the first page 261 + if (accountsRes.cursor) { 262 + // so grab a second page just to see if there really is a second page 263 + try { 264 + const secondPage = await query('com.atproto.sync.listRepos', { 265 + params: { limit: 1, cursor: accountsRes.cursor }, 266 + }); 267 + this.accountsComplete = !secondPage.cursor || secondPage.repos.length == 0; 268 + } catch (e) { 269 + // we're in a niche spot. ignore errors and look at the original (faulty) cursor 270 + this.accountsComplete = !accountsRes.cursor; // ๐Ÿคทโ€โ™€๏ธ 271 + } 272 + } else { 273 + this.accountsComplete = true; 274 + } 275 + 276 + } catch (e) { 277 + if (window.isXrpcErr(e)) { 278 + this.error = e.error; 279 + } else { 280 + this.error = 'Failed to reach (see console)'; 281 + console.error(e); 282 + } 283 + } 284 + this.loadingDesc = false; 285 + }, 286 + })); 287 + 101 288 Alpine.data('relayCheckHost', (pds, relay) => ({ 102 289 loading: false, 103 290 error: null, 104 291 status: null, 292 + expectedErrorInfo: null, 105 293 reqCrawlStatus: null, 106 294 reqCrawlError: null, 107 295 108 296 async init() { 109 - await this.check(); 297 + await this.check(pds, relay); 110 298 }, 111 299 112 - async check() { 300 + async check(pds, relay) { 113 301 this.loading = true; 114 302 this.error = null; 115 303 this.status = null; 116 - let query = window.SimpleQuery(`https://${relay.hostname}`); 304 + this.expectedError = false; 305 + const query = window.SimpleQuery(`https://${relay.hostname}`); 117 306 const hostname = pds.split('://')[1]; 118 307 let data; 119 308 try { ··· 122 311 }); 123 312 this.status = data.status; 124 313 } catch(e) { 125 - if (window.isXrpcErr(e)) { 314 + if (relay.missingApis?.['com.atproto.sync.getHostStatus']) { 315 + this.error = 'Can\'t check'; 316 + this.expectedErrorInfo = relay.missingApis?.['com.atproto.sync.getHostStatus']; 317 + } else if (window.isXrpcErr(e)) { 126 318 this.error = e.error; 127 319 } else { 128 320 this.error = 'Failed to check (see console)'; ··· 134 326 this.reqCrawlError = null; 135 327 }, 136 328 137 - async requestCrawl() { 329 + async requestCrawl(pds, relay) { 138 330 this.reqCrawlStatus = "loading"; 139 331 const proc = window.SimpleProc(`https://${relay.hostname}`); 140 332 const hostname = pds.split('://')[1]; ··· 153 345 } 154 346 this.reqCrawlStatus = "done"; 155 347 }, 156 - })) 348 + })); 349 + 350 + Alpine.data('checkHandle', handle => ({ 351 + loading: false, 352 + dnsDid: null, 353 + dnsErr: null, 354 + httpDid: null, 355 + httpErr: null, 356 + 357 + async init() { 358 + await this.updateHandle(handle); 359 + }, 360 + async updateHandle(handle) { 361 + this.loading = true; 362 + this.dnsDid = null; 363 + this.dnsErr = null; 364 + this.httpDid = null; 365 + this.httpErr = null; 366 + try { 367 + this.dnsDid = await window.dnsResolver.resolve(handle); 368 + } catch (e) { 369 + this.dnsErr = e.name; 370 + } 371 + try { 372 + this.httpDid = await window.httpResolver.resolve(handle); 373 + } catch (e) { 374 + this.httpErr = e.name; 375 + } 376 + this.loading = false; 377 + }, 378 + })); 379 + 380 + Alpine.data('didToHandle', did => ({ 381 + loading: false, 382 + error: null, 383 + handle: null, 384 + async load() { 385 + loading = true; 386 + error = null; 387 + handle = null; 388 + let data; 389 + try { 390 + data = await window.slingshot('com.bad-example.identity.resolveMiniDoc', { 391 + params: { identifier: did }, 392 + }); 393 + this.handle = data.handle; 394 + } catch (e) { 395 + if (window.isXrpcErr(e)) { 396 + this.error = e.error; 397 + } else { 398 + this.error = 'failed (see console)'; 399 + console.error(e); 400 + } 401 + } 402 + loading = false; 403 + } 404 + })); 405 + 406 + Alpine.data('didRepoState', (did, pds) => ({ 407 + loading: false, 408 + error: null, 409 + state: null, 410 + 411 + async init() { 412 + await this.checkRepoState(did, pds); 413 + }, 414 + async checkRepoState(did, pds) { 415 + this.loading = true; 416 + this.error = null; 417 + this.state = null; 418 + 419 + if (!did || !pds) { 420 + this.loading = false; 421 + return; 422 + } 423 + const query = window.SimpleQuery(pds); 424 + try { 425 + this.state = await query('com.atproto.sync.getRepoStatus', { 426 + params: { did }, 427 + }); 428 + } catch (e) { 429 + if (window.isXrpcErr(e)) { 430 + this.error = e.error; 431 + } else { 432 + this.error = 'failed (see console)'; 433 + console.error(e); 434 + } 435 + } 436 + this.loading = false; 437 + }, 438 + })); 439 + 440 + Alpine.data('repoSize', () => ({ 441 + lading: false, 442 + error: null, 443 + size: null, 444 + 445 + async loadRepoForSize(did, pds) { 446 + this.loading = true; 447 + 448 + if (!did || !pds) { 449 + this.loading = false; 450 + return; 451 + } 452 + const query = window.SimpleQuery(pds); 453 + try { 454 + const res = await query('com.atproto.sync.getRepo', { 455 + params: { did }, 456 + as: 'blob', 457 + }); 458 + let bytes = res.size; 459 + let mbs = bytes / Math.pow(2, 20); 460 + this.size = mbs.toFixed(1); 461 + } catch (e) { 462 + if (window.isXrpcErr(e)) { 463 + this.error = e.error; 464 + } else { 465 + this.error = 'failed (see console)'; 466 + console.error(e); 467 + } 468 + } 469 + this.loading = false; 470 + }, 471 + })); 472 + 473 + Alpine.data('relayCheckRepo', (did, relay) => ({ 474 + loading: false, 475 + error: null, 476 + status: null, 477 + expectedErrorInfo: null, 478 + 479 + async init() { 480 + await this.check(did, relay); 481 + }, 482 + 483 + async check(did, relay) { 484 + this.loading = true; 485 + this.error = null; 486 + this.status = null; 487 + this.expectedErrorInfo = null; 488 + 489 + const query = window.SimpleQuery(`https://${relay.hostname}`); 490 + try { 491 + this.status = await query('com.atproto.sync.getRepoStatus', { 492 + params: { did }, 493 + }); 494 + } catch(e) { 495 + if (relay.missingApis?.['com.atproto.sync.getRepoStatus']) { 496 + this.error = 'Can\'t check'; 497 + this.expectedErrorInfo = relay.missingApis?.['com.atproto.sync.getRepoStatus']; 498 + } else if (window.isXrpcErr(e)) { 499 + this.error = e.error; 500 + } else { 501 + this.error = 'Failed to check (see console)'; 502 + console.error(e); 503 + } 504 + } 505 + 506 + this.loading = false; 507 + }, 508 + 509 + revStatus(repoRev) { 510 + if ( 511 + !repoRev || 512 + !(this.status && this.status.rev) 513 + ) return null; 514 + 515 + if (this.status.rev < repoRev) return 'behind'; 516 + if (this.status.rev === repoRev) return 'current'; 517 + if (this.status.rev > repoRev) return 'ahead'; 518 + } 519 + })); 520 + 521 + Alpine.data('modLabels', did => ({ 522 + loading: false, 523 + error: null, 524 + regionalErrors: [], 525 + labels: [], 526 + 527 + async init() { 528 + this.loading = true; 529 + this.error = null; 530 + this.regionalErrors = []; 531 + this.labels = []; 532 + 533 + const query = window.SimpleQuery('https://mod.bsky.app'); 534 + 535 + try { 536 + const res = await query('com.atproto.label.queryLabels', { 537 + params: { uriPatterns: [did] }, 538 + }); 539 + this.labels = res.labels ?? []; 540 + // TODO: handle cursors? 541 + 542 + for (const region of window.regionalModAccounts) { 543 + // intentionally no await, these come in async 544 + // (...and could get messy if we start re-checking labels before they're done) 545 + this.checkRegionLabels(region); 546 + } 547 + } catch (e) { 548 + if (window.isXrpcErr(e)) { 549 + this.error = e.error; 550 + } else { 551 + this.error = 'Failed to check (see console)'; 552 + console.error(e); 553 + } 554 + } 555 + this.loading = false; 556 + }, 557 + 558 + async checkRegionLabels(labeler) { 559 + const query = window.SimpleQuery(labeler); 560 + try { 561 + const res = await query('com.atproto.label.queryLabels', { 562 + params: { uriPatterns: [did] }, 563 + }); 564 + if (res?.labels?.length > 0) this.labels.push(...res.labels); 565 + } catch (e) { 566 + if (window.isXrpcErr(e)) { 567 + this.regionalErrors.push(`${labeler}: ${e.error}`); 568 + } else { 569 + this.regionalErrors.push(`Failed to check ${labeler} (see console)`); 570 + console.error(`labeler: ${labeler}`, e); 571 + } 572 + } 573 + } 574 + })); 575 + 576 + Alpine.data('pdsHistory', (did, currentPds) => ({ 577 + loading: false, 578 + error: null, 579 + history: [], 580 + 581 + async init() { 582 + this.loading = true; 583 + this.error = null; 584 + this.history = []; 585 + try { 586 + const res = await fetch(`https://plc.directory/${did}/log/audit`); 587 + if (res.ok) { 588 + const log = await res.json(); 589 + let prev = null; 590 + for (op of log) { 591 + let opPds = null; 592 + const services = op.operation.services; 593 + if (services) { 594 + const app = services.atproto_pds; 595 + if (app) { 596 + opPds = app.endpoint; 597 + } 598 + } 599 + if (opPds === prev) continue; 600 + prev = opPds; 601 + this.history.push({ 602 + pds: opPds, 603 + date: op.createdAt, 604 + }); 605 + } 606 + this.history.reverse(); 607 + if (this.history[0]) this.history[0].current = true; 608 + } else { 609 + this.error = `${res.status}: ${await res.text()}`; 610 + } 611 + } catch (e) { 612 + this.error = 'failed to get history'; 613 + console.error(e); 614 + } 615 + this.loading = false; 616 + }, 617 + })); 618 + 619 + Alpine.data('handleHistory', (did, currentHandle) => ({ 620 + loading: false, 621 + error: null, 622 + history: [], 623 + 624 + async init() { 625 + this.loading = true; 626 + this.error = null; 627 + this.history = []; 628 + try { 629 + const res = await fetch(`https://plc.directory/${did}/log/audit`); 630 + if (res.ok) { 631 + const log = await res.json(); 632 + let prev = null; 633 + for (op of log) { 634 + let opHandle = null; 635 + if (op.operation.alsoKnownAs) { 636 + for (aka of op.operation.alsoKnownAs) { 637 + if (aka.startsWith("at://")) { 638 + opHandle = aka.slice("at://".length); 639 + break; 640 + } 641 + } 642 + } 643 + if (opHandle === prev) continue; 644 + prev = opHandle; 645 + this.history.push({ 646 + handle: opHandle, 647 + date: op.createdAt, 648 + }); 649 + } 650 + this.history.reverse(); 651 + } else { 652 + this.error = `${res.status}: ${await res.text()}`; 653 + } 654 + } catch (e) { 655 + this.error = 'failed to get history'; 656 + console.error(e); 657 + } 658 + this.loading = false; 659 + }, 660 + })); 157 661 }) 158 662 </script> 159 663 </head> 160 - <body x-data="debug"> 161 - <div class="hero bg-base-200"> 664 + <body x-data="debug" class="bg-base-200"> 665 + <div class="hero bg-base-200 p-8"> 162 666 <div class="hero-content flex-col"> 163 - <h1>PDS Debugger</h1> 667 + <h1 class="text-2xl mb-8">PDS Debugger</h1> 668 + <p class="show-until-ready"><em>Loading&hellip;</em></p> 669 + <form class="hide-until-ready" @submit.prevent="await diagnose()"> 670 + <label class="text-sm text-primary" for="identifier"> 671 + atproto handle, DID, or HTTPS PDS URL 672 + </label> 673 + <br/> 674 + <div class="join"> 675 + <input 676 + id="identifier" 677 + class="input join-item" 678 + x-model="identifier" 679 + :disabled="identifierLoading" 680 + autofocus 681 + /> 682 + <button 683 + class="btn btn-primary join-item" 684 + type="submit" 685 + >go</button> 686 + </div> 687 + </form> 688 + </div> 689 + </div> 690 + 691 + <div class="w-full max-w-lg mx-auto"> 692 + <template x-if="identifierError"> 693 + <p>uh oh: <span x-text="identifierError"></span></p> 694 + </template> 164 695 165 - <p>Work in progress!</p> 166 - <details class="text-xs"> 167 - <summary>Would be nice</summary> 168 - <ul> 169 - <li>anything that actually works</li> 170 - <li>firehose listener for missing pds events</li> 171 - <li>jetstream listener for missing pds events</li> 172 - <li>check relays for account status</li> 173 - <li>check relays for pds state</li> 174 - <li>plc: check old pds hosts for active account state</li> 175 - </ul> 176 - </details> 177 - <details class="text-xs"> 178 - <summary>Limitations</summary> 179 - <ul> 180 - <li>it's all client-side</li> 181 - </ul> 182 - </details> 696 + <template x-if="pds != null"> 183 697 184 - <div class="card bg-base-100 w-full max-w-sm shrink-0 shadow-2xl"> 698 + <div class="card bg-base-100 w-full max-w-2xl shrink-0 shadow-2xl m-4"> 185 699 <div class="card-body"> 186 - <form @submit.prevent="await diagnose()"> 187 - <label> 188 - Enter an atproto handle, DID, or HTTPS PDS URL 189 - <input 190 - class="input" 191 - x-model="identifier" 192 - :disabled="identifierLoading" 193 - autofocus 194 - /> 195 - </label> 196 - </form> 700 + <h2 class="card-title"> 701 + <span class="badge badge-secondary">PDS</span> 702 + <span x-text="pds"></span> 703 + </h2> 704 + 705 + <div 706 + x-data="pdsCheck(pds)" 707 + x-init="$watch('pds', v => update(v))" 708 + > 709 + <h3 class="text-lg mt-3"> 710 + Server 711 + <span 712 + x-show="description !== null" 713 + class="badge badge-sm badge-soft badge-success" 714 + >online</span> 715 + </h3> 716 + <p x-show="loadingDesc">Loading&hellip;</p> 717 + <p x-show="error" class="text-warning" x-text="error"></p> 718 + <template x-if="description !== null"> 719 + <div> 720 + <div class="overflow-x-auto"> 721 + <table class="table table-xs"> 722 + <tbody> 723 + <tr> 724 + <td class="text-sm"> 725 + Open registration: 726 + <span 727 + x-text="!description.inviteCodeRequired" 728 + ></span> 729 + </td> 730 + </tr> 731 + <tr> 732 + <td class="text-sm"> 733 + Version: 734 + <code 735 + class="text-xs" 736 + x-text="version" 737 + ></code> 738 + </td> 739 + </tr> 740 + </tbody> 741 + </table> 742 + </div> 743 + <h4 class="font-bold"> 744 + Accounts 745 + </h4> 746 + <div class="overflow-x-auto overflow-y-auto max-h-26"> 747 + <table class="table table-xs"> 748 + <tbody> 749 + <template x-for="account in accounts"> 750 + <tr> 751 + <td> 752 + <code> 753 + <a 754 + href="#" 755 + class="link" 756 + x-text="account.did" 757 + @click.prevent="goto(account.did)" 758 + ></a> 759 + </code> 760 + </td> 761 + <td> 762 + <span 763 + x-show="account.active" 764 + class="badge badge-sm badge-soft badge-success" 765 + > 766 + active 767 + </span> 768 + <span 769 + x-show="!account.active" 770 + x-text="account.status" 771 + class="badge badge-sm badge-soft badge-warning" 772 + ></span> 773 + </td> 774 + <td 775 + x-data="didToHandle(account.did)" 776 + x-intersect:enter.once="load" 777 + > 778 + <span x-show="loading">Loading&hellip;</span> 779 + <span x-show="error !== null" x-text="error"></span> 780 + <a 781 + href="#" 782 + class="link" 783 + @click.prevent="goto(handle)" 784 + x-show="handle !== null" 785 + x-text="`@${handle}`" 786 + ></a> 787 + </td> 788 + </tr> 789 + </template> 790 + <template x-if="!loadingDesc && !accountsComplete"> 791 + <tr> 792 + <td colspan="2" class="text-xs text-warning-content"> 793 + (more accounts not shown) 794 + </td> 795 + </tr> 796 + </template> 797 + </tbody> 798 + </table> 799 + </div> 800 + </div> 801 + </template> 802 + </div> 803 + 804 + <h3 class="text-lg mt-3">Relay host status</h3> 805 + <div class="overflow-x-auto"> 806 + <table class="table table-xs"> 807 + <tbody> 808 + <template x-for="relay in window.relays"> 809 + <tr 810 + x-data="relayCheckHost(pds, relay)" 811 + x-init="$watch('pds', pds => check(pds, relay))" 812 + > 813 + <td class="text-sm"> 814 + <div class="tooltip tooltip-right" :data-tip="relay.hostname"> 815 + <img 816 + class="inline-block h-4 w-4" 817 + :src="relay.icon" 818 + alt="" 819 + /> 820 + <span x-text="relay.name"></span> 821 + <span 822 + x-show="!!relay.note" 823 + x-text="relay.note" 824 + class="badge badge-soft badge-neutral badge-xs" 825 + ></span> 826 + </div> 827 + </td> 828 + <td> 829 + <template x-if="loading"> 830 + <em>loading&hellip;</em> 831 + </template> 832 + <template x-if="error"> 833 + <div 834 + class="text-xs" 835 + :class="expectedErrorInfo 836 + ? 'text-info tooltip tooltip-left cursor-help' 837 + : 'text-warning'" 838 + :data-tip="expectedErrorInfo" 839 + > 840 + <span x-text="error"></span> 841 + <span 842 + x-show="!!expectedErrorInfo" 843 + class="badge badge-soft badge-info badge-xs" 844 + >i</span> 845 + </div> 846 + </template> 847 + <template x-if="status"> 848 + <span 849 + x-text="status" 850 + class="badge badge-sm" 851 + :class="status === 'active' && 'badge-soft badge-success'" 852 + ></span> 853 + </template> 854 + </td> 855 + <td> 856 + <div x-show="status !== 'active' && !expectedErrorInfo"> 857 + <button 858 + x-show="reqCrawlStatus !== 'done'" 859 + class="btn btn-xs btn-ghost whitespace-nowrap" 860 + :disabled="reqCrawlStatus === 'loading'" 861 + @click="requestCrawl(pds, relay)" 862 + > 863 + request crawl 864 + </button> 865 + <span 866 + x-show="reqCrawlError !== null" 867 + x-text="reqCrawlError" 868 + class="text-xs text-warning" 869 + ></span> 870 + <button 871 + x-show="reqCrawlError === null && reqCrawlStatus === 'done'" 872 + class="btn btn-xs btn-soft btn-primary whitespace-nowrap" 873 + @click="check" 874 + > 875 + refresh 876 + </button> 877 + </div> 878 + </td> 879 + </tr> 880 + </template> 881 + </tbody> 882 + </table> 883 + </div> 197 884 </div> 198 885 </div> 886 + </template> 199 887 200 - <template x-if="identifierError"> 201 - <p>uh oh: <span x-text="identifierError"></span></p> 202 - </template> 888 + <template x-if="did != null"> 889 + <div class="card bg-base-100 w-full max-w-2xl shrink-0 shadow-2xl m-4"> 890 + <div class="card-body"> 891 + <h2 class="card-title"> 892 + <span class="badge badge-secondary">DID</span> 893 + <code x-text="did"></code> 894 + </h2> 895 + <template x-if="pds != null"> 896 + <div x-data="didRepoState(did, pds)"> 897 + <h3 class="text-lg mt-3"> 898 + Repo 899 + <span 900 + x-show="state && state.active" 901 + class="badge badge-sm badge-soft badge-success" 902 + >active</span> 903 + </h3> 904 + <div class="overflow-x-auto"> 905 + <table class="table table-xs"> 906 + <tbody> 907 + <tr> 908 + <td class="text-sm"> 909 + Rev: 910 + <code x-text="state && state.rev"></code> 911 + </td> 912 + </tr> 913 + <tr> 914 + <td class="text-sm"> 915 + Size: 916 + <span x-data="repoSize"> 917 + <template x-if="loading"> 918 + <em>loading&hellip;</em> 919 + </template> 920 + <template x-if="error"> 921 + <span class="text-xs text-warning" x-text="error"></span> 922 + </template> 923 + <template x-if="size"> 924 + <code> 925 + <span x-text="size"></span> MiB 926 + </code> 927 + </template> 928 + <template x-if="!size && !error && !loading"> 929 + <button 930 + class="btn btn-xs btn-soft btn-primary" 931 + @click.prevent="loadRepoForSize(did, pds)" 932 + >load</button> 933 + </template> 934 + </span> 935 + </td> 936 + </tr> 937 + <tr> 938 + <td class="text-sm"> 939 + PDS: 940 + <a 941 + href="#" 942 + class="link" 943 + @click.prevent="goto(pds)" 944 + x-text="pds" 945 + ></a> 946 + </td> 947 + </tr> 948 + <!--<tr> 949 + <td 950 + class="text-sm" 951 + x-data="repoMonitor(did, pds)" 952 + > 953 + <button 954 + class="btn btn-xs btn-success" 955 + >Start live monitoring</button> 956 + </td> 957 + </tr>--> 958 + </tbody> 959 + </table> 960 + </div> 961 + 962 + <h3 class="text-lg mt-3"> 963 + Relay repo status 964 + </h3> 965 + <div class="overflow-x-auto"> 966 + <table class="table table-xs"> 967 + <tbody> 968 + <template x-for="relay in window.relays"> 969 + <tr 970 + x-data="relayCheckRepo(did, relay)" 971 + x-init="$watch('pds', pds => check(did, relay))" 972 + > 973 + <td class="text-sm"> 974 + <div class="tooltip tooltip-right" :data-tip="relay.hostname"> 975 + <img 976 + class="inline-block h-4 w-4" 977 + :src="relay.icon" 978 + alt="" 979 + /> 980 + <span x-text="relay.name"></span> 981 + <span 982 + x-show="!!relay.note" 983 + x-text="relay.note" 984 + class="badge badge-neutral badge-soft badge-xs" 985 + ></span> 986 + </div> 987 + </td> 988 + <template x-if="loading"> 989 + <td> 990 + <em>loading&hellip;</em> 991 + </td> 992 + </template> 993 + <template x-if="error"> 994 + <td> 995 + <div 996 + class="text-xs" 997 + :class="expectedErrorInfo 998 + ? 'text-info tooltip tooltip-left cursor-help' 999 + : 'text-warning'" 1000 + :data-tip="expectedErrorInfo" 1001 + > 1002 + <span x-text="error"></span> 1003 + <span 1004 + x-show="!!expectedErrorInfo" 1005 + class="badge badge-soft badge-info badge-xs" 1006 + >i</span> 1007 + </div> 1008 + </td> 1009 + </template> 1010 + <template x-if="status"> 1011 + <td> 1012 + <span 1013 + x-show="status.active" 1014 + class="badge badge-sm badge-soft badge-success" 1015 + > 1016 + active 1017 + </span> 1018 + <span 1019 + x-show="!status.active" 1020 + x-text="status.status" 1021 + class="badge badge-sm badge-soft badge-warning" 1022 + ></span> 1023 + </td> 1024 + </template> 1025 + <template x-if="revStatus(state && state.rev)"> 1026 + <td x-data="{ asdf: revStatus(state.rev) }"> 1027 + <span 1028 + x-show="asdf === 'current'" 1029 + class="badge badge-sm badge-soft badge-success" 1030 + >current</span> 1031 + <span 1032 + x-show="asdf === 'behind'" 1033 + class="badge badge-sm badge-soft badge-warning tooltip tooltip-left" 1034 + :data-tip="status.rev" 1035 + >behind</span> 1036 + <span 1037 + x-show="asdf === 'ahead'" 1038 + class="badge badge-sm badge-soft badge-success tooltip tooltip-left" 1039 + :data-tip="`Account may have updated between checks? ${status.rev}`" 1040 + >ahead</span> 1041 + </td> 1042 + </template> 1043 + <template x-if="!revStatus(state && state.rev)"> 1044 + <td></td> 1045 + </template> 1046 + </tr> 1047 + </template> 1048 + </tbody> 1049 + </table> 1050 + </div> 203 1051 204 - <template x-if="pds != null"> 205 - <div class="card bg-base-100 w-full max-w-lg shrink-0 shadow-2xl"> 206 - <div class="card-body"> 207 - <h2 class="card-title"> 208 - <span class="badge badge-secondary">PDS</span> 209 - <span x-text="pds"></span> 210 - </h2> 1052 + <div x-data="modLabels(did)"> 1053 + <h3 class="text-lg mt-3"> 1054 + Labels 1055 + </h3> 1056 + <div class="overflow-x-auto"> 1057 + <table class="table table-xs"> 1058 + <tbody> 1059 + <template x-if="loading"> 1060 + <tr> 1061 + <td>Loading&hellip;</td> 1062 + </tr> 1063 + </template> 1064 + <template x-if="error"> 1065 + <tr> 1066 + <td>Error: <span x-text="error"></span></td> 1067 + </tr> 1068 + </template> 1069 + <template x-if="!loading && !error && labels.length === 0"> 1070 + <tr> 1071 + <td class="text-xs"> 1072 + <em>No Bluesky moderation labels found</em> 1073 + </td> 1074 + </tr> 1075 + </template> 1076 + <template x-for="label in labels"> 1077 + <template x-if="!!label"> 1078 + <tr x-data="{ expired: isBeforeNow(label.exp) }"> 1079 + <td> 1080 + <span x-show="label.neg">removed</span> 1081 + <code 1082 + x-text="label.cts.split('T')[0]" 1083 + :title="label.cts" 1084 + ></code> 1085 + </td> 1086 + <td> 1087 + <template x-if="!!label.exp"> 1088 + <span x-text="expired ? 'expired' : 'expires'"></span> 1089 + <code 1090 + x-text="label.exp.split('T')[0]" 1091 + :title="label.exp" 1092 + ></code> 1093 + </template> 1094 + </td> 1095 + <td> 1096 + <code 1097 + x-text="label.val" 1098 + class="badge badge-sm badge-soft" 1099 + :class="(label.neg || expired) 1100 + ? 'badge-neutral line-through' 1101 + : !!window.bskyAccountDeathLabels[label.val] 1102 + ? 'badge-warning' 1103 + : 'badge-info'" 1104 + :title="label.neg 1105 + ? 'label negated' 1106 + : expired 1107 + ? 'label expired' 1108 + : window.bskyAccountDeathLabels[label.val] ?? ''" 1109 + ></code> 1110 + </td> 1111 + <td 1112 + x-data="didToHandle(label.src)" 1113 + x-intersect:enter.once="load" 1114 + > 1115 + <span x-show="loading">Loading&hellip;</span> 1116 + <span x-show="error !== null" x-text="error"></span> 1117 + <a 1118 + href="#" 1119 + class="link" 1120 + @click.prevent="goto(handle)" 1121 + x-show="handle !== null" 1122 + x-text="`@${handle}`" 1123 + ></a> 1124 + </td> 1125 + </tr> 1126 + </template> 1127 + </template> 1128 + </tbody> 1129 + </table> 1130 + </div> 1131 + <template x-for="error in regionalErrors"> 1132 + <p 1133 + x-text="error" 1134 + class="text-xs text-warning" 1135 + ></p> 1136 + </template> 1137 + </div> 211 1138 212 - <h3 class="text-lg">Relay host status</h3> 213 - <div class="overflow-x-auto"> 214 - <table class="table table-xs"> 215 - <tbody> 216 - <template x-for="relay in window.relays"> 217 - <tr x-data="relayCheckHost(pds, relay)"> 218 - <td x-text="relay.name" class="text-sm"></td> 219 - <td> 1139 + <template x-if="did.startsWith('did:plc:')"> 1140 + <div x-data="pdsHistory(did, pds)"> 1141 + <h3 class="text-lg mt-3"> 1142 + PLC PDS history 1143 + </h3> 1144 + <div class="overflow-x-auto"> 1145 + <table class="table table-xs"> 1146 + <tbody> 220 1147 <template x-if="loading"> 221 - <em>loading&hellip;</em> 1148 + <tr> 1149 + <td>Loading&hellip;</td> 1150 + </tr> 222 1151 </template> 223 1152 <template x-if="error"> 224 - <span 225 - x-text="error" 226 - class="text-xs text-warning" 227 - ></span> 1153 + <tr> 1154 + <td>Error: <span x-text="error"></span></td> 1155 + </tr> 228 1156 </template> 229 - <template x-if="status"> 230 - <span 231 - x-text="status" 232 - class="badge badge-sm" 233 - :class="status === 'active' && 'badge-soft badge-success'" 234 - ></span> 1157 + <template x-if="!loading && !error && history.length === 0"> 1158 + <tr> 1159 + <td class="text-sm"> 1160 + <em>no previous PDS</em> 1161 + </td> 1162 + </tr> 235 1163 </template> 236 - </td> 237 - <td> 238 - <div x-show="status !== 'active'"> 239 - <button 240 - x-show="reqCrawlStatus !== 'done'" 241 - class="btn btn-xs btn-ghost whitespace-nowrap" 242 - :disabled="reqCrawlStatus === 'loading'" 243 - @click="requestCrawl" 244 - > 245 - request crawl 246 - </button> 247 - <span 248 - x-show="reqCrawlError !== null" 249 - x-text="reqCrawlError" 250 - class="text-xs text-warning" 251 - ></span> 252 - <button 253 - x-show="reqCrawlError === null && reqCrawlStatus === 'done'" 254 - class="btn btn-xs btn-soft btn-primary whitespace-nowrap" 255 - @click="check" 256 - > 257 - refresh 258 - </button> 259 - </div> 260 - </td> 261 - </tr> 262 - </template> 263 - </tbody> 264 - </table> 1164 + <template x-for="event in history"> 1165 + <tr x-data="didRepoState(did, event.pds)"> 1166 + <td> 1167 + <code x-text="event.date.split('T')[0]"></code> 1168 + </td> 1169 + <td> 1170 + <a 1171 + href="#" 1172 + class="link" 1173 + @click.prevent="goto(event.pds)" 1174 + x-text="event.pds" 1175 + ></a> 1176 + </td> 1177 + <template x-if="event.current"> 1178 + <td> 1179 + <span 1180 + x-show="state && !state.active" 1181 + x-text="state && state.status" 1182 + class="badge badge-sm badge-soft badge-warning" 1183 + ></span> 1184 + <span 1185 + x-show="state && state.active" 1186 + class="badge badge-sm badge-soft badge-success" 1187 + >current</span> 1188 + </td> 1189 + </template> 1190 + <template x-if="!event.current"> 1191 + <td> 1192 + <span 1193 + x-show="state && !state.active" 1194 + x-text="state && state.status" 1195 + class="badge badge-sm badge-soft badge-success" 1196 + ></span> 1197 + <span 1198 + x-show="state && state.active" 1199 + class="badge badge-sm badge-soft badge-warning" 1200 + >active</span> 1201 + </td> 1202 + </template> 1203 + </tr> 1204 + </template> 1205 + </tbody> 1206 + </table> 1207 + </div> 1208 + </div> 1209 + </template> 265 1210 </div> 266 - </div> 1211 + </template> 267 1212 </div> 268 - </template> 1213 + </div> 1214 + </template> 269 1215 270 - <template x-if="did != null"> 271 - <div class="card bg-base-100 w-full max-w-sm shrink-0 shadow-2xl"> 272 - <div class="card-body"> 273 - <p x-text="`DID: ${did}`"></p> 1216 + <template x-if="handle !== null"> 1217 + <div class="card bg-base-100 w-full max-w-2xl shrink-0 shadow-2xl m-4"> 1218 + <div 1219 + x-data="checkHandle(handle)" 1220 + x-init="$watch('handle', h => updateHandle(h))" 1221 + class="card-body" 1222 + > 1223 + <h2 class="card-title"> 1224 + <span class="badge badge-secondary">Handle</span> 1225 + <span x-text="handle"></span> 1226 + </h2> 1227 + 1228 + <h3 class="text-lg mt-3"> 1229 + Resolution 1230 + </h3> 1231 + <p x-show="loading" class="text-i">Loading&hellip;</p> 1232 + <div x-show="!loading" class="overflow-x-auto"> 1233 + <table class="table table-xs"> 1234 + <tbody> 1235 + <tr> 1236 + <td class="text-sm">DNS</td> 1237 + <td class="text-sm"> 1238 + <code x-text="dnsDid"></code> 1239 + </td> 1240 + <td> 1241 + <div 1242 + class="badge badge-sm badge-soft badge-neutral" 1243 + x-show="dnsErr !== null" 1244 + x-text="dnsErr" 1245 + ></div> 1246 + </td> 1247 + </tr> 1248 + <tr> 1249 + <td class="text-sm">Http</td> 1250 + <td class="text-sm"> 1251 + <code x-text="httpDid"></code> 1252 + </td> 1253 + <td> 1254 + <div 1255 + class="badge badge-sm badge-soft badge-neutral" 1256 + x-show="httpErr !== null" 1257 + x-text="httpErr" 1258 + ></div> 1259 + </td> 1260 + </tr> 1261 + </tbody> 1262 + </table> 274 1263 </div> 1264 + 1265 + <template x-if="did !== null && did.startsWith('did:plc:')"> 1266 + 1267 + <div x-data="handleHistory(did, handle)"> 1268 + <h3 class="text-lg mt-3"> 1269 + PLC handle history 1270 + </h3> 1271 + <div class="overflow-x-auto"> 1272 + <table class="table table-xs"> 1273 + <tbody> 1274 + <template x-if="loading"> 1275 + <tr> 1276 + <td>Loading&hellip;</td> 1277 + </tr> 1278 + </template> 1279 + <template x-if="error"> 1280 + <tr> 1281 + <td>Error: <span x-text="error"></span></td> 1282 + </tr> 1283 + </template> 1284 + <template x-if="!loading && !error && history.length === 0"> 1285 + <tr> 1286 + <td class="text-sm"> 1287 + <em>no previous handle</em> 1288 + </td> 1289 + </tr> 1290 + </template> 1291 + <template x-for="event in history"> 1292 + <tr> 1293 + <td> 1294 + <code x-text="event.date.split('T')[0]"></code> 1295 + </td> 1296 + <td> 1297 + <a 1298 + href="#" 1299 + class="link" 1300 + @click.prevent="goto(event.handle)" 1301 + x-text="event.handle" 1302 + ></a> 1303 + </td> 1304 + </tr> 1305 + </template> 1306 + </tbody> 1307 + </table> 1308 + </div> 1309 + </div> 1310 + 1311 + </template> 275 1312 </div> 276 - </template> 277 - </div> 1313 + </div> 1314 + </template> 278 1315 </div> 1316 + 1317 + 1318 + 1319 + <div class="footer text-xs sm:footer-horizontal text-neutral mt-32 p-8 max-w-2xl mx-auto"> 1320 + <nav> 1321 + <h3 class="footer-title mt-3">Current limitations</h3> 1322 + <p>PDS hosts without CORS will fail tests.</p> 1323 + <p>Bluesky relay is missing API endpoints.</p> 1324 + <p>Blacksky relay is also missing API endpoints.</p> 1325 + <p>The requestCrawl button is not well tested.</p> 1326 + 1327 + <h3 class="footer-title mt-3">Future features</h3> 1328 + <p>Firehose listener</p> 1329 + <p>URL routing</p> 1330 + <p>Less strict identity resolution</p> 1331 + </nav> 1332 + 1333 + <nav> 1334 + <h3 class="footer-title mt-3">Places</h3> 1335 + <p><a href="https://tangled.org/microcosm.blue/pds-debug">Source code (tangled.org)</a></p> 1336 + <p><a href="https://discord.gg/Vwamex5UFS">Discord (microcosm)</a></p> 1337 + <p><a href="https://pdsmoover.com/">PDS Moover</a></p> 1338 + <p><a href="https://microcosm.blue">microcosm</a></p> 1339 + 1340 + <h3 class="footer-title mt-3">Made by</h3> 1341 + <p> 1342 + <a href="https://bsky.app/profile/did:plc:hdhoaan3xa3jiuq4fg4mefid">fig</a> 1343 + <a href="https://github.com/sponsors/uniphil">(sponsor)</a> 1344 + </p> 1345 + <p> 1346 + <a href="https://bsky.app/profile/did:plc:rnpkyqnmsw4ipey6eotbdnnf">bailey</a> 1347 + <a href="https://github.com/sponsors/fatfingers23">(sponsor)</a> 1348 + </p> 1349 + </nav> 1350 + </div> 1351 + 279 1352 </body> 280 1353 </html>
+5
readme.md
··· 1 + # PDS Debugger 2 + 3 + https://debug.hose.cam 4 + 5 + Diagnostics for atproto PDS hosts, DIDs, and handles
+13
useful-accounts.txt
··· 1 + some accounts that show things useful for testing the debugger 2 + 3 + 4 + Labels 5 + 6 + - did:plc:bnwrgnvwkg2n5cbvk4xodb3h | !hide | no other labels 7 + - did:plc:qhl3vg5tmwey536z2fil2lrh | !hide | from moderation-tr.bsky.app 8 + - did:plc:fsmaoqqnm6knqh4cuphb4jow | !hide, ~!takedown | takedown negated 9 + - did:plc:iv3yod6zf2j4zaakq6qyiz46 | !takedown | 10 + - did:plc:nwrcwcrhpkgrqqvkg3lmaqky | ~needs-review, ~!takedown | both negated 11 + - did:plc:2tinwgqvf4asiwh36ii6ko7l | needs-review | expired 12 + - did:plc:5plqrpw3x6j5wzaosssqams7 | spam | no other labels 13 + - did:plc:tqww7jdpqx5tb3w435fugmxi | intolerant |