5 min read

Over my skis; posting into the void

Over my skis; posting into the void
I am so far out over my skis here.

I managed to nerd snipe myself yesterday by seemingly breaking the Bluesky profile attached to my self-hosted did:web when I deleted and rebuilt it. I am well out of my comfort zone right now - I don't really understand JWTs, dids, or the various application layers involved in the process of actually having a self-hosted PDS talk to an atproto app yet. Only way to learn is by doing, though, so here are the results of my initial investigations.

I ended up spending a few hours on this iterating with Claude and reading and re-reading the API docs. I don't want to get in the habit of putting all the overly-verbose slop Claude creates when I'm investigating like this into GitHub, so I'll just share a few relevant scripts/curl commands. Attempting to auth with my PDS through its API results in it successfully issuing the correct tokens:

const PDS_URL = '{YOUR_PDS_URL}';
const IDENTIFIER = '{YOUR_DID}';

async function testAuthentication() {
  const password = process.argv[2];

  if (!password) {
    console.log('Error: Password argument is required.\n');
    console.log("Usage: ts-node test-authentication.ts <password>")
    process.exit(1);
  }

  console.log('Testing authentication...\n');

  try {
    const response = await fetch(
      `${PDS_URL}/xrpc/com.atproto.server.createSession`,
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          identifier: IDENTIFIER,
          password: password,
        }),
      }
    );

    console.log(`Status: ${response.status} ${response.statusText}\n`);

    if (response.ok) {
      const data = await response.json();
      console.log('✅ SUCCESS! Authentication works!\n');
      console.log('Session details:');
      console.log(`  DID: ${data.did}`);
      console.log(`  Handle: ${data.handle}`);
      console.log(`  Email: ${data.email || 'Not set'}`);
      console.log(`  Access Token: ${data.accessJwt.substring(0, 20)}...`);
      console.log(`  Refresh Token: ${data.refreshJwt.substring(0, 20)}...\n`);

      // Save token to file for use in other scripts
      const fs = await import('fs');
      const tokenData = {
        accessJwt: data.accessJwt,
        refreshJwt: data.refreshJwt,
        did: data.did,
        handle: data.handle,
        createdAt: new Date().toISOString(),
      };
      fs.writeFileSync(
        '/{your_cwd}/.session.json',
        JSON.stringify(tokenData, null, 2)
      );
    } else {
      const errorText = await response.text();
      console.log('Error response:');
      console.log(errorText);
      console.log('\n');

    }
  } catch (error) {
    console.log('❌ ERROR: Failed to connect to PDS\n');
    console.log(error);
  }
}

testAuthentication();

Which, without sharing my tokens, gives me a success:

As far as I can tell, this is because Bluesky's AppView doesn't actually touch the auth process when it's through my external PDS. It only touches the Bluesky AppView's DB when it comes to Bluesky-specific content like profile, posts, feeds, etc.

I double checked this by running a few CURL commands:

repo.getRecord against my own PDS to check if a bluesky profile record is present:

curl "https://atproto.bront.rodeo/xrpc/com.atproto.repo.getRecord?repo=did:web:atproto.bront.rodeo&collection=app.bsky.actor.profile&rkey=self"

Which succeeds:

actor.getProfile against my own PDS, which also fails:

curl -H "Authorization: Bearer $TOKEN" "https://atproto.bront.rodeo/xrpc/app.bsky.actor.getProfile?actor=did:web:atproto.bront.rodeo"

actor.getProfile against the Bluesky API:

curl "https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=did:web:atproto.bront.rodeo"

Which fails:

I double-checked with Rudy Fraser's did:web to make sure it's not a bug specific to did:web accounts:

It looks like did:web based accounts can be accessed through the public Bluesky API, but mine isn't showing up. What's interesting is that Bluesky has written back to my PDS, so something is registering. I'm able to make posts, I just can't actually see them.

For example, this posts and writes to my PDS.

Just posted this.

I tried running listRecords on my personal repo again, and Bluesky is definitely writing to it -

curl "https://atproto.bront.rodeo/xrpc/com.atproto.repo.listRecords?repo=did:web:atproto.bront.rodeo&collection=app.bsky.feed.post"

So let's see if my posts exist in the Bluesky API even though my profile apparently doesn't. I pulled the record from my PDS and ran it against app.bsky.feed.getPosts:

curl "https://public.api.bsky.app/xrpc/app.bsky.feed.getPosts?uris=at://did:web:atproto.bront.rodeo/app.bsky.feed.post/3m2mtcko26k2m"

And got {"posts":[]} - nothing. This isn't particularly surprising behavior, I assume a post without an associated profile would fire some errors in Bluesky's backend, but it is kind of wild that it can still write back to my PDS even if the write to Bluesky's AppView (or whatever) is failing. I'm not sure if this is desirable or undesirable behavior, but it's certainly interesting.

I decided to try making the did:web again with my original signing key, which I did remember to save, but ran into the same issues, which makes me feel a bit insane. I've done enough that this could be breaking for plenty of other reasons, but I would think using the original key would help with any issues on Bluesky's end.

So it's possible that the issue is with my PDS, and specifically how it's indexing profiles. I tried authing and running getProfile against my own PDS and got a "Profile not found" error, which definitely implies some issue there. There are also intermittent reports out there of being unable to use the same handle as your did:web , although again, I'm confident this worked two days ago.

Finally, I just decided to inspect the requests being made directly to my PDS by bluesky.

Yep, it looks like all requests from the app are being forwarded to my PDS and getProfile is failing at the PDS level, not the app level. I think there may still be some broader conflict since a profile should have been automatically created the first time I signed into Bluesky, but it certainly appears that all the relevant requests are going to me. But it's unclear to me why getProfile is failing locally even when I write a profile object to the PDS myself. This is weird!

Next steps: try out a new did:web on the same PDS. After that, I'd like to investigate further into the expected PDS behavior to figure out where and why this is failing - maybe trace how getProfile works. Again, it's probably good behavior for this to fail, since I basically mimicked the behavior of someone trying to steal my identity, but I'd like to know exactly why. Might be time for me to join the PDS admins Discord and see if anyone understands this better than I do.