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
server/api/auth/atproto.get.ts
Outdated
| 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