Skip to content

Comments

feat: show bluesky avatar in header#896

Merged
danielroe merged 3 commits intomainfrom
feat/at-avatar
Feb 4, 2026
Merged

feat: show bluesky avatar in header#896
danielroe merged 3 commits intomainfrom
feat/at-avatar

Conversation

@danielroe
Copy link
Member

SCR-20260203-ueee

@vercel
Copy link

vercel bot commented Feb 3, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs.npmx.dev Error Error Feb 4, 2026 0:25am
npmx.dev Ready Ready Preview, Comment Feb 4, 2026 0:25am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
npmx-lunaria Ignored Ignored Feb 4, 2026 0:25am

Request Review

@codecov
Copy link

codecov bot commented Feb 3, 2026

Codecov Report

❌ Patch coverage is 0% with 5 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
app/components/Header/AccountMenu.client.vue 0.00% 5 Missing ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 3, 2026

📝 Walkthrough

Walkthrough

Backend session data now includes an optional avatar URL sourced from the PDS profile blob when available. The public session schema and server-side UserServerSession type were extended with an optional avatar field. The header component was updated to render Atmosphere avatars where present, retain the existing cloud-icon fallback when absent, and apply conditional z-indexing to ensure consistent overlapping when both npm and Atmosphere connections exist. Error handling prevents avatar-fetch failures from breaking the session flow.

🚥 Pre-merge checks | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Description check ❓ Inconclusive The pull request description contains only an image screenshot showing the feature implementation, with no textual explanation of the changes. Add a brief textual description explaining what the pull request does, why the changes were made, and any relevant implementation details or testing performed.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/at-avatar

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Comment on lines 71 to 86
// 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)}`
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's read the file to understand the full context
wc -l server/api/auth/atproto.get.ts

Repository: 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.ts

Repository: 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 10

Repository: 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 -50

Repository: 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" -i

Repository: 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 2

Repository: 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 -20

Repository: 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 -40

Repository: 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.ts

Repository: npmx-dev/npmx.dev

Length of output: 324


🏁 Script executed:

# Check if PublicUserSessionSchema is used to validate miniDoc anywhere
rg "PublicUserSessionSchema" -B 2 -A 2

Repository: 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 -5

Repository: 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.ts

Repository: 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 -50

Repository: 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.ts

Repository: 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 2

Repository: 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.ts

Repository: 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.ts

Repository: 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 -40

Repository: 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 miniDoc against PublicUserSessionSchema using safeParse()
  • Check agent.did is 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)}`
+          }
+        }
+      }

Copy link
Contributor

@fatfingers23 fatfingers23 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {
Copy link
Contributor

@fatfingers23 fatfingers23 Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)}`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Member Author

@danielroe danielroe Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any equivalent (of what I'm doing in the PR) that isn't bluesky-specific?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants