Uh oh!
There was an error while loading. Please reload this page.
feat: show bluesky avatar in header - #896
Conversation
danielroe
commented
Feb 3, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.
1 Skipped Deployment
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
📝 WalkthroughWalkthroughBackend session data now includes an optional 🚥 Pre-merge checks | ❌ 1❌ Failed checks (1 inconclusive)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
| // Fetch the user's profile record to get their avatar blob reference | ||
| // We use com.atproto.repo.getRecord to fetch directly from the user's PDS | ||
| // This works with any PDS, not just Bluesky | ||
| let avatar: string | undefined | ||
| try { | ||
| const profileResponse = await fetch( | ||
| `${miniDoc.pds}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(agent.did!)}&collection=app.bsky.actor.profile&rkey=self`, | ||
| { headers: { 'User-Agent': 'npmx' } }, | ||
| ) | ||
| if (profileResponse.ok) { | ||
| const record = (await profileResponse.json()) as { value: ProfileRecord } | ||
| const avatarBlob = record.value.avatar | ||
| if (avatarBlob?.ref?.$link) { | ||
| // Construct the blob URL from the user's PDS | ||
| avatar = `${miniDoc.pds}/xrpc/com.atproto.sync.getBlob?did=${encodeURIComponent(agent.did!)}&cid=${encodeURIComponent(avatarBlob.ref.$link)}` | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's read the file to understand the full context
wc -l server/api/auth/atproto.get.tsRepository: npmx-dev/npmx.dev
Length of output: 95
🏁 Script executed:
# Read the entire file to understand variable declarations and context
cat -n server/api/auth/atproto.get.tsRepository: npmx-dev/npmx.dev
Length of output: 3923
🏁 Script executed:
# Find the PublicUserSession type definition
fd -t f "publicUserSession" --exec cat {}Repository: npmx-dev/npmx.dev
Length of output: 438
🏁 Script executed:
# Search for PublicUserSession type in shared directory
rg "interface PublicUserSession|type PublicUserSession" -A 10Repository: npmx-dev/npmx.dev
Length of output: 172
🏁 Script executed:
# Check the Agent type from `@atproto/api` to see if did is optional
rg "class Agent|interface Agent" -A 20 | head -50Repository: npmx-dev/npmx.dev
Length of output: 43
🏁 Script executed:
# Search for any SSRF or URL validation patterns in the codebase
rg "ssrf|SSRF|private.*host|localhost|127.0.0.1" -iRepository: npmx-dev/npmx.dev
Length of output: 14997
🏁 Script executed:
# Check what valibot's url() validator does and how it's used
rg "from 'valibot'" -B 2 -A 2Repository: npmx-dev/npmx.dev
Length of output: 8676
🏁 Script executed:
# Check the Agent type and its did property
rg "class Agent|did\?" -A 5 | grep -A 5 "class Agent"| head -20Repository: npmx-dev/npmx.dev
Length of output: 43
🏁 Script executed:
# Search for what valibot's url() validator does - check documentation or usage
rg "url\(\)" -B 3 -A 3 | head -40Repository: npmx-dev/npmx.dev
Length of output: 1870
🏁 Script executed:
# Check if miniDoc validation happens on line 69
sed -n '64,70p' server/api/auth/atproto.get.tsRepository: npmx-dev/npmx.dev
Length of output: 324
🏁 Script executed:
# Check if PublicUserSessionSchema is used to validate miniDoc anywhere
rg "PublicUserSessionSchema" -B 2 -A 2Repository: npmx-dev/npmx.dev
Length of output: 1119
🏁 Script executed:
# Look at atproto Agent type - search npm packages or type definitions in node_modules# First, let's see if there are any .d.ts files or type definitions
fd -e "d.ts""@atproto"2>/dev/null | head -5Repository: npmx-dev/npmx.dev
Length of output: 43
🏁 Script executed:
# Check if agent.did can be undefined by looking at the Agent constructor
rg "@atproto/api" -A 10 | grep -A 10 "Agent"Repository: npmx-dev/npmx.dev
Length of output: 914
🏁 Script executed:
# Check if miniDoc is validated using safeParse in atproto.get.ts
sed -n '58,75p' server/api/auth/atproto.get.tsRepository: npmx-dev/npmx.dev
Length of output: 763
🏁 Script executed:
# Search for any Agent type definitions or usage to understand if did is optional
rg "did\?" -i -B 2 -A 2 | head -50Repository: npmx-dev/npmx.dev
Length of output: 551
🏁 Script executed:
# Check if miniDoc is ever validated with safeParse in the atproto file
rg "safeParse.*miniDoc|miniDoc.*safeParse" server/api/auth/atproto.get.tsRepository: npmx-dev/npmx.dev
Length of output: 43
🏁 Script executed:
# Look for all uses of agent.did to see if it's ever checked for undefined
rg "agent\.did" server/api/auth/atproto.get.ts -B 2 -A 2Repository: npmx-dev/npmx.dev
Length of output: 761
🏁 Script executed:
# Let's verify if valibot's url() validator provides any SSRF protection# by checking what it validates
web_search "valibot url validator what does it validate"Repository: npmx-dev/npmx.dev
Length of output: 111
🏁 Script executed:
# Check if there's any validation of miniDoc using safeParse in the complete file
cat -n server/api/auth/atproto.get.ts | grep -A 5 -B 5 "safeParse\|parse"Repository: npmx-dev/npmx.dev
Length of output: 43
🏁 Script executed:
# Let's also check the complete context around agent initialization
sed -n '58,77p' server/api/auth/atproto.get.tsRepository: npmx-dev/npmx.dev
Length of output: 947
🌐 Web query:
valibot url validator what does it check validate
💡 Result:
Valibot’s url()only validates that a string is “badly formatted” vs “validly formatted” as a URL (i.e., it’s a URL format check on a string, returning a url validation issue if it isn’t). [1]
What it does not check by itself:
- Protocol safety / allowlist (e.g., it won’t inherently block
javascript:...; you must add your own constraints). [2] - Specific scheme requirements like “must be https” (you’d add an additional rule such as
startsWith('https://')). [3] - Host/domain allowlists, URL reachability, DNS/HTTP status, etc. (those are outside “formatting”). [1]
Sources:
[1] Valibot url action docs: https://valibot.dev/api/url/
[2] Discussion referencing javascript: concerns with extending url: open-circle/valibot#835
[3] Valibot startsWith docs (HTTPS URL schema example): https://valibot.dev/api/startsWith/
🏁 Script executed:
# Check if miniDoc is cast without validation (on line 69)
sed -n '68,70p' server/api/auth/atproto.get.tsRepository: npmx-dev/npmx.dev
Length of output: 143
🏁 Script executed:
# Look for any valibot documentation comments or usage patterns in the codebase
rg "valibot.*url|url.*valibot" -A 3 -B 3 | head -40Repository: npmx-dev/npmx.dev
Length of output: 617
Validate miniDoc.pds and guard agent.did before server-side fetches.
miniDoc.pds is cast to type without validation—valibot's url() only checks format, not SSRF protections. This allows fetches to localhost, private IPs, or restricted hosts. Additionally, agent.did! uses a non-null assertion without checking if it's defined, bypassing type safety.
Before fetching from the user's PDS, ensure you:
- Validate
miniDocagainstPublicUserSessionSchemausingsafeParse() - Check
agent.didis defined before using it in URLs - Restrict PDS URLs to HTTPS and exclude private hosts
🔒 Suggested guard + validation
- const profileResponse = await fetch(- `${miniDoc.pds}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(agent.did!)}&collection=app.bsky.actor.profile&rkey=self`,- { headers: { 'User-Agent': 'npmx' } },- )- if (profileResponse.ok) {- const record = (await profileResponse.json()) as { value: ProfileRecord }- const avatarBlob = record.value.avatar- if (avatarBlob?.ref?.$link) {- // Construct the blob URL from the user's PDS- avatar = `${miniDoc.pds}/xrpc/com.atproto.sync.getBlob?did=${encodeURIComponent(agent.did!)}&cid=${encodeURIComponent(avatarBlob.ref.$link)}`- }- }+ const did = agent.did+ const pdsUrl = new URL(miniDoc.pds)+ if (did && pdsUrl.protocol === 'https:' /* && isPublicHost(pdsUrl.hostname) */) {+ const profileResponse = await fetch(+ `${pdsUrl.origin}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(did)}&collection=app.bsky.actor.profile&rkey=self`,+ { headers: { 'User-Agent': 'npmx' } },+ )+ if (profileResponse.ok) {+ const record = (await profileResponse.json()) as { value: ProfileRecord }+ const avatarBlob = record.value.avatar+ if (avatarBlob?.ref?.$link) {+ avatar = `${pdsUrl.origin}/xrpc/com.atproto.sync.getBlob?did=${encodeURIComponent(did)}&cid=${encodeURIComponent(avatarBlob.ref.$link)}`+ }+ }+ }
fatfingers23
left a comment
There was a problem hiding this comment.
Looks good! Listed the couple of edge cases I notice.
| avatar = `${miniDoc.pds}/xrpc/com.atproto.sync.getBlob?did=${encodeURIComponent(agent.did!)}&cid=${encodeURIComponent(avatarBlob.ref.$link)}` | ||
| } | ||
| } | ||
| } catch { |
There was a problem hiding this comment.
This catch will happen a good bit for our npmx.social users. They do not get a app.bsky.actor.profile on signup. Nothing we have to change now since the cloud will be good back up still
| const avatarBlob = record.value.avatar | ||
| if (avatarBlob?.ref?.$link) { | ||
| // Construct the blob URL from the user's PDS | ||
| avatar = `${miniDoc.pds}/xrpc/com.atproto.sync.getBlob?did=${encodeURIComponent(agent.did!)}&cid=${encodeURIComponent(avatarBlob.ref.$link)}` |
There was a problem hiding this comment.
Since it's a bluesky profile we can leverage their CDN to show these images. Usually loads a bit faster as well
https://cdn.bsky.app/img/feed_thumbnail/plain/{agent.did!}/{avatarBlob.ref.$link)}@jpeg
There was a problem hiding this comment.
Is there any equivalent (of what I'm doing in the PR) that isn't bluesky-specific?
There was a problem hiding this comment.
Whoops sorry I missed this. Nah. I think using the Bluesky profile for now and loading from their CDN is good since that's what most people have @zeucapua is working on a profile lexicon with an avatar blob ref we can probably map over to as well. Default to bsky, if the npmx profile has one use it, etc
Uh oh!
There was an error while loading. Please reload this page.