From 168091c84b0146e92a2d916e8c6ca1b2728ec278 Mon Sep 17 00:00:00 2001 From: Bailey Townsend Date: Tue, 3 Feb 2026 21:31:45 -0600 Subject: [PATCH 1/8] Some changes to hopefully help with oauth session --- server/api/auth/atproto.get.ts | 12 +++++++ server/utils/atproto/oauth-session-store.ts | 1 + server/utils/atproto/oauth.ts | 35 ++++++++++++++------- 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/server/api/auth/atproto.get.ts b/server/api/auth/atproto.get.ts index 39721941d..57ba873a6 100644 --- a/server/api/auth/atproto.get.ts +++ b/server/api/auth/atproto.get.ts @@ -6,6 +6,7 @@ import { useOAuthStorage } from '#server/utils/atproto/storage' import { SLINGSHOT_HOST } from '#shared/utils/constants' import { useServerSession } from '#server/utils/server-session' import type { PublicUserSession } from '#shared/schemas/publicUserSession' +import { handleResolver } from '#server/utils/atproto/oauth' interface ProfileRecord { avatar?: { @@ -35,6 +36,7 @@ export default defineEventHandler(async event => { sessionStore, clientMetadata, requestLock: getOAuthLock(), + handleResolver, }) if (!query.code) { @@ -98,6 +100,16 @@ export default defineEventHandler(async event => { avatar, }, }) + } else { + //If slingshot fails we still want to set some key info we need. + const pdsBase = (await authSession.getTokenInfo()).aud + await session.update({ + public: { + did: authSession.did, + handle: 'Not available', + pds: pdsBase, + }, + }) } return sendRedirect(event, '/') diff --git a/server/utils/atproto/oauth-session-store.ts b/server/utils/atproto/oauth-session-store.ts index 532f9894d..e29fc75f2 100644 --- a/server/utils/atproto/oauth-session-store.ts +++ b/server/utils/atproto/oauth-session-store.ts @@ -17,6 +17,7 @@ export class OAuthSessionStore implements NodeSavedSessionStore { async set(_key: string, val: NodeSavedSession) { // We are ignoring the key since the mapping is already done in the session + console.log('oauth session set', val) await this.session.update({ oauthSession: val, }) diff --git a/server/utils/atproto/oauth.ts b/server/utils/atproto/oauth.ts index 8b53dc72e..4f1a243b7 100644 --- a/server/utils/atproto/oauth.ts +++ b/server/utils/atproto/oauth.ts @@ -1,6 +1,6 @@ import type { OAuthClientMetadataInput, OAuthSession } from '@atproto/oauth-client-node' import type { EventHandlerRequest, H3Event, SessionManager } from 'h3' -import { NodeOAuthClient } from '@atproto/oauth-client-node' +import { NodeOAuthClient, AtprotoDohHandleResolver } from '@atproto/oauth-client-node' import { parse } from 'valibot' import { getOAuthLock } from '#server/utils/atproto/lock' import { useOAuthStorage } from '#server/utils/atproto/storage' @@ -11,6 +11,13 @@ import { clientUri } from '#oauth/config' // TODO: If you add writing a new record you will need to add a scope for it export const scope = `atproto ${LIKES_SCOPE}` +/** + * Resolves a did to a handle via DoH or via the http website calls + */ +export const handleResolver = new AtprotoDohHandleResolver({ + dohEndpoint: 'https://cloudflare-dns.com/dns-query', +}) + export function getOauthClientMetadata() { const dev = import.meta.dev @@ -42,10 +49,13 @@ type EventHandlerWithOAuthSession = ( serverSession: SessionManager, ) => Promise -async function getOAuthSession(event: H3Event): Promise { +async function getOAuthSession( + event: H3Event, +): Promise<{ oauthSession: OAuthSession | undefined; serverSession: SessionManager }> { + const serverSession = await useServerSession(event) + try { const clientMetadata = getOauthClientMetadata() - const serverSession = await useServerSession(event) const { stateStore, sessionStore } = useOAuthStorage(serverSession) const client = new NodeOAuthClient({ @@ -53,13 +63,18 @@ async function getOAuthSession(event: H3Event): Promise( handler: EventHandlerWithOAuthSession, ) { return defineEventHandler(async event => { - const serverSession = await useServerSession(event) - - const oAuthSession = await getOAuthSession(event) - return await handler(event, oAuthSession, serverSession) + const { oauthSession, serverSession } = await getOAuthSession(event) + return await handler(event, oauthSession, serverSession) }) } From defcb83e0d6736fc51217e6e0b83f1fe8fcb166e Mon Sep 17 00:00:00 2001 From: Bailey Townsend Date: Tue, 3 Feb 2026 22:33:14 -0600 Subject: [PATCH 2/8] return a some what user friendly error message --- server/api/auth/atproto.get.ts | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/server/api/auth/atproto.get.ts b/server/api/auth/atproto.get.ts index 57ba873a6..875bfcfae 100644 --- a/server/api/auth/atproto.get.ts +++ b/server/api/auth/atproto.get.ts @@ -40,21 +40,30 @@ export default defineEventHandler(async event => { }) if (!query.code) { - const handle = query.handle?.toString() - const create = query.create?.toString() + try { + const handle = query.handle?.toString() + const create = query.create?.toString() + + if (!handle) { + throw createError({ + statusCode: 401, + message: 'Handle not provided in query', + }) + } - if (!handle) { - throw createError({ - status: 400, - message: 'Handle not provided in query', + const redirectUrl = await atclient.authorize(handle, { + scope, + prompt: create ? 'create' : undefined, }) - } + return sendRedirect(event, redirectUrl.toString()) + } catch (error) { + const message = error instanceof Error ? error.message : 'Authentication failed.' - const redirectUrl = await atclient.authorize(handle, { - scope, - prompt: create ? 'create' : undefined, - }) - return sendRedirect(event, redirectUrl.toString()) + return handleApiError(error, { + statusCode: 401, + message: `${message}. Please login and try again.`, + }) + } } const { session: authSession } = await atclient.callback( From bb65bfa01c2608d46c8346224d01fdebef908c25 Mon Sep 17 00:00:00 2001 From: Bailey Townsend Date: Tue, 3 Feb 2026 22:51:59 -0600 Subject: [PATCH 3/8] going try cutting down the calls to refresh sessions --- server/api/auth/session.get.ts | 3 ++- server/utils/atproto/oauth-session-store.ts | 11 +++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/server/api/auth/session.get.ts b/server/api/auth/session.get.ts index 9dbd11d7a..771ab6c00 100644 --- a/server/api/auth/session.get.ts +++ b/server/api/auth/session.get.ts @@ -1,7 +1,8 @@ import { PublicUserSessionSchema } from '#shared/schemas/publicUserSession' import { safeParse } from 'valibot' -export default eventHandlerWithOAuthSession(async (event, oAuthSession, serverSession) => { +export default defineEventHandler(async event => { + const serverSession = await useServerSession(event) const result = safeParse(PublicUserSessionSchema, serverSession.data.public) if (!result.success) { return null diff --git a/server/utils/atproto/oauth-session-store.ts b/server/utils/atproto/oauth-session-store.ts index e29fc75f2..70d66e9db 100644 --- a/server/utils/atproto/oauth-session-store.ts +++ b/server/utils/atproto/oauth-session-store.ts @@ -17,10 +17,13 @@ export class OAuthSessionStore implements NodeSavedSessionStore { async set(_key: string, val: NodeSavedSession) { // We are ignoring the key since the mapping is already done in the session - console.log('oauth session set', val) - await this.session.update({ - oauthSession: val, - }) + try { + await this.session.update({ + oauthSession: val, + }) + } catch (error) { + console.error('Error setting OAuth session:', error) + } } async del() { From 66be4d4024005e1ef53a91c7e54ca29fd7408c2e Mon Sep 17 00:00:00 2001 From: Bailey Townsend Date: Wed, 4 Feb 2026 08:02:23 -0600 Subject: [PATCH 4/8] debug log --- server/utils/atproto/oauth-session-store.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/server/utils/atproto/oauth-session-store.ts b/server/utils/atproto/oauth-session-store.ts index 70d66e9db..fa5de2cd5 100644 --- a/server/utils/atproto/oauth-session-store.ts +++ b/server/utils/atproto/oauth-session-store.ts @@ -17,6 +17,7 @@ export class OAuthSessionStore implements NodeSavedSessionStore { async set(_key: string, val: NodeSavedSession) { // We are ignoring the key since the mapping is already done in the session + console.log('New set expires:', val.tokenSet.expires_at) try { await this.session.update({ oauthSession: val, From e395e785a3cf051988699c5be47b7c914b306708 Mon Sep 17 00:00:00 2001 From: Bailey Townsend Date: Wed, 4 Feb 2026 08:43:03 -0600 Subject: [PATCH 5/8] Moved raw fetch to use the client --- lexicons.json | 10 +- lexicons/app/bsky/actor/profile.json | 67 +++++++++++ lexicons/com/atproto/label/defs.json | 163 +++++++++++++++++++++++++++ server/api/auth/atproto.get.ts | 34 +++--- 4 files changed, 253 insertions(+), 21 deletions(-) create mode 100644 lexicons/app/bsky/actor/profile.json create mode 100644 lexicons/com/atproto/label/defs.json diff --git a/lexicons.json b/lexicons.json index 61b6a522e..6cd9ed1e3 100644 --- a/lexicons.json +++ b/lexicons.json @@ -1,7 +1,15 @@ { "version": 1, - "lexicons": ["site.standard.document"], + "lexicons": ["app.bsky.actor.profile", "site.standard.document"], "resolutions": { + "app.bsky.actor.profile": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.actor.profile", + "cid": "bafyreia6umzg3a6d7mjbow4p57tviey45muohklhgsvjoamcctoiusr4pe" + }, + "com.atproto.label.defs": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.label.defs", + "cid": "bafyreig4hmnb2xkecyg4aaqfhr2rrcxxb3gsr4xks4rqb7rscrycalbrji" + }, "com.atproto.repo.strongRef": { "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.repo.strongRef", "cid": "bafyreifrkdbnkvfjujntdaeigolnrjj3srrs53tfixjhmacclps72qlov4" diff --git a/lexicons/app/bsky/actor/profile.json b/lexicons/app/bsky/actor/profile.json new file mode 100644 index 000000000..1d22cc4ba --- /dev/null +++ b/lexicons/app/bsky/actor/profile.json @@ -0,0 +1,67 @@ +{ + "id": "app.bsky.actor.profile", + "defs": { + "main": { + "key": "literal:self", + "type": "record", + "record": { + "type": "object", + "properties": { + "avatar": { + "type": "blob", + "accept": ["image/png", "image/jpeg"], + "maxSize": 1000000, + "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'" + }, + "banner": { + "type": "blob", + "accept": ["image/png", "image/jpeg"], + "maxSize": 1000000, + "description": "Larger horizontal image to display behind profile view." + }, + "labels": { + "refs": ["com.atproto.label.defs#selfLabels"], + "type": "union", + "description": "Self-label values, specific to the Bluesky application, on the overall account." + }, + "website": { + "type": "string", + "format": "uri" + }, + "pronouns": { + "type": "string", + "maxLength": 200, + "description": "Free-form pronouns text.", + "maxGraphemes": 20 + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "pinnedPost": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + }, + "description": { + "type": "string", + "maxLength": 2560, + "description": "Free-form profile description text.", + "maxGraphemes": 256 + }, + "displayName": { + "type": "string", + "maxLength": 640, + "maxGraphemes": 64 + }, + "joinedViaStarterPack": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + } + } + }, + "description": "A declaration of a Bluesky account profile." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/label/defs.json b/lexicons/com/atproto/label/defs.json new file mode 100644 index 000000000..6790ebafb --- /dev/null +++ b/lexicons/com/atproto/label/defs.json @@ -0,0 +1,163 @@ +{ + "id": "com.atproto.label.defs", + "defs": { + "label": { + "type": "object", + "required": ["src", "uri", "val", "cts"], + "properties": { + "cid": { + "type": "string", + "format": "cid", + "description": "Optionally, CID specifying the specific version of 'uri' resource this label applies to." + }, + "cts": { + "type": "string", + "format": "datetime", + "description": "Timestamp when this label was created." + }, + "exp": { + "type": "string", + "format": "datetime", + "description": "Timestamp at which this label expires (no longer applies)." + }, + "neg": { + "type": "boolean", + "description": "If true, this is a negation label, overwriting a previous label." + }, + "sig": { + "type": "bytes", + "description": "Signature of dag-cbor encoded label." + }, + "src": { + "type": "string", + "format": "did", + "description": "DID of the actor who created this label." + }, + "uri": { + "type": "string", + "format": "uri", + "description": "AT URI of the record, repository (account), or other resource that this label applies to." + }, + "val": { + "type": "string", + "maxLength": 128, + "description": "The short string name of the value or type of this label." + }, + "ver": { + "type": "integer", + "description": "The AT Protocol version of the label object." + } + }, + "description": "Metadata tag on an atproto resource (eg, repo or record)." + }, + "selfLabel": { + "type": "object", + "required": ["val"], + "properties": { + "val": { + "type": "string", + "maxLength": 128, + "description": "The short string name of the value or type of this label." + } + }, + "description": "Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel." + }, + "labelValue": { + "type": "string", + "knownValues": [ + "!hide", + "!no-promote", + "!warn", + "!no-unauthenticated", + "dmca-violation", + "doxxing", + "porn", + "sexual", + "nudity", + "nsfl", + "gore" + ] + }, + "selfLabels": { + "type": "object", + "required": ["values"], + "properties": { + "values": { + "type": "array", + "items": { + "ref": "#selfLabel", + "type": "ref" + }, + "maxLength": 10 + } + }, + "description": "Metadata tags on an atproto record, published by the author within the record." + }, + "labelValueDefinition": { + "type": "object", + "required": ["identifier", "severity", "blurs", "locales"], + "properties": { + "blurs": { + "type": "string", + "description": "What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing.", + "knownValues": ["content", "media", "none"] + }, + "locales": { + "type": "array", + "items": { + "ref": "#labelValueDefinitionStrings", + "type": "ref" + } + }, + "severity": { + "type": "string", + "description": "How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.", + "knownValues": ["inform", "alert", "none"] + }, + "adultOnly": { + "type": "boolean", + "description": "Does the user need to have adult content enabled in order to configure this label?" + }, + "identifier": { + "type": "string", + "maxLength": 100, + "description": "The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).", + "maxGraphemes": 100 + }, + "defaultSetting": { + "type": "string", + "default": "warn", + "description": "The default setting for this label.", + "knownValues": ["ignore", "warn", "hide"] + } + }, + "description": "Declares a label value and its expected interpretations and behaviors." + }, + "labelValueDefinitionStrings": { + "type": "object", + "required": ["lang", "name", "description"], + "properties": { + "lang": { + "type": "string", + "format": "language", + "description": "The code of the language these strings are written in." + }, + "name": { + "type": "string", + "maxLength": 640, + "description": "A short human-readable name for the label.", + "maxGraphemes": 64 + }, + "description": { + "type": "string", + "maxLength": 100000, + "description": "A longer description of what the label means and why it might be applied.", + "maxGraphemes": 10000 + } + }, + "description": "Strings which describe the label in the UI, localized into a specific language." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/server/api/auth/atproto.get.ts b/server/api/auth/atproto.get.ts index 875bfcfae..b0287ccab 100644 --- a/server/api/auth/atproto.get.ts +++ b/server/api/auth/atproto.get.ts @@ -7,15 +7,8 @@ import { SLINGSHOT_HOST } from '#shared/utils/constants' import { useServerSession } from '#server/utils/server-session' import type { PublicUserSession } from '#shared/schemas/publicUserSession' import { handleResolver } from '#server/utils/atproto/oauth' - -interface ProfileRecord { - avatar?: { - $type: 'blob' - ref: { $link: string } - mimeType: string - size: number - } -} +import { type AtIdentifierString, Client } from '@atproto/lex' +import * as app from '#shared/types/lexicons/app' export default defineEventHandler(async event => { const config = useRuntimeConfig(event) @@ -86,17 +79,18 @@ export default defineEventHandler(async event => { const pdsUrl = new URL(miniDoc.pds) // Only fetch from HTTPS PDS endpoints to prevent SSRF if (did && pdsUrl.protocol === 'https:') { - 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) { - // Use Bluesky CDN for faster image loading - avatar = `https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${avatarBlob.ref.$link}@jpeg` - } + const client = new Client(pdsUrl) + const profileResponse = await client.get(app.bsky.actor.profile, { + // Hack for now need to find an example on how to use it properly + repo: did as AtIdentifierString, + rkey: 'self', + }) + + const validatedResponse = app.bsky.actor.profile.main.validate(profileResponse.value) + + if (validatedResponse.avatar?.ref) { + // Use Bluesky CDN for faster image loading + avatar = `https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${validatedResponse.avatar?.ref}@jpeg` } } } catch { From 01503af5a050b66b12a106c22b58ef02de5fc6bd Mon Sep 17 00:00:00 2001 From: Bailey Townsend Date: Wed, 4 Feb 2026 10:44:45 -0600 Subject: [PATCH 6/8] add get avatar to fall back on atproto.get. cleanup --- server/api/auth/atproto.get.ts | 60 ++++++++++++--------- server/utils/atproto/oauth-session-store.ts | 7 ++- server/utils/atproto/oauth.ts | 6 +-- 3 files changed, 41 insertions(+), 32 deletions(-) diff --git a/server/api/auth/atproto.get.ts b/server/api/auth/atproto.get.ts index b0287ccab..ffe45c4ff 100644 --- a/server/api/auth/atproto.get.ts +++ b/server/api/auth/atproto.get.ts @@ -10,6 +10,38 @@ import { handleResolver } from '#server/utils/atproto/oauth' import { type AtIdentifierString, Client } from '@atproto/lex' import * as app from '#shared/types/lexicons/app' +/** + * Fetch the user's profile record to get their avatar blob reference + * @param did + * @param pds + * @returns + */ +async function getAvatar(did: string, pds: string) { + let avatar: string | undefined + try { + const pdsUrl = new URL(pds) + // Only fetch from HTTPS PDS endpoints to prevent SSRF + if (did && pdsUrl.protocol === 'https:') { + const client = new Client(pdsUrl) + const profileResponse = await client.get(app.bsky.actor.profile, { + // Hack for now need to find an example on how to use it properly + repo: did as AtIdentifierString, + rkey: 'self', + }) + + const validatedResponse = app.bsky.actor.profile.main.validate(profileResponse.value) + + if (validatedResponse.avatar?.ref) { + // Use Bluesky CDN for faster image loading + avatar = `https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${validatedResponse.avatar?.ref}@jpeg` + } + } + } catch { + // Avatar fetch failed, continue without it + } + return avatar +} + export default defineEventHandler(async event => { const config = useRuntimeConfig(event) if (!config.sessionPassword) { @@ -72,30 +104,7 @@ export default defineEventHandler(async event => { if (response.ok) { const miniDoc: PublicUserSession = await response.json() - // Fetch the user's profile record to get their avatar blob reference - let avatar: string | undefined - const did = agent.did - try { - const pdsUrl = new URL(miniDoc.pds) - // Only fetch from HTTPS PDS endpoints to prevent SSRF - if (did && pdsUrl.protocol === 'https:') { - const client = new Client(pdsUrl) - const profileResponse = await client.get(app.bsky.actor.profile, { - // Hack for now need to find an example on how to use it properly - repo: did as AtIdentifierString, - rkey: 'self', - }) - - const validatedResponse = app.bsky.actor.profile.main.validate(profileResponse.value) - - if (validatedResponse.avatar?.ref) { - // Use Bluesky CDN for faster image loading - avatar = `https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${validatedResponse.avatar?.ref}@jpeg` - } - } - } catch { - // Avatar fetch failed, continue without it - } + let avatar: string | undefined = await getAvatar(authSession.did, miniDoc.pds) await session.update({ public: { @@ -106,14 +115,15 @@ export default defineEventHandler(async event => { } else { //If slingshot fails we still want to set some key info we need. const pdsBase = (await authSession.getTokenInfo()).aud + let avatar: string | undefined = await getAvatar(authSession.did, pdsBase) await session.update({ public: { did: authSession.did, handle: 'Not available', pds: pdsBase, + avatar, }, }) } - return sendRedirect(event, '/') }) diff --git a/server/utils/atproto/oauth-session-store.ts b/server/utils/atproto/oauth-session-store.ts index fa5de2cd5..64787f84c 100644 --- a/server/utils/atproto/oauth-session-store.ts +++ b/server/utils/atproto/oauth-session-store.ts @@ -17,13 +17,16 @@ export class OAuthSessionStore implements NodeSavedSessionStore { async set(_key: string, val: NodeSavedSession) { // We are ignoring the key since the mapping is already done in the session - console.log('New set expires:', val.tokenSet.expires_at) try { await this.session.update({ oauthSession: val, }) } catch (error) { - console.error('Error setting OAuth session:', error) + // Not sure if this has been happening. But helps with debugging + console.error( + '[oauth session store] Failed to set session:', + error instanceof Error ? error.message : 'Unknown error', + ) } } diff --git a/server/utils/atproto/oauth.ts b/server/utils/atproto/oauth.ts index 4f1a243b7..c8fc6cd41 100644 --- a/server/utils/atproto/oauth.ts +++ b/server/utils/atproto/oauth.ts @@ -67,12 +67,8 @@ async function getOAuthSession( }) const currentSession = serverSession.data - if (!currentSession) { - console.log('oauth session not found') - return { oauthSession: undefined, serverSession } - } + if (!currentSession) return { oauthSession: undefined, serverSession } - // restore using the subject const oauthSession = await client.restore(currentSession.public.did) return { oauthSession, serverSession } } catch (error) { From 9a95462fcc8ae7d45a991d5fd2cf7dd1fe3a1a2f Mon Sep 17 00:00:00 2001 From: Bailey Townsend Date: Wed, 4 Feb 2026 10:50:20 -0600 Subject: [PATCH 7/8] checking the did type --- package.json | 3 ++- pnpm-lock.yaml | 3 +++ server/api/auth/atproto.get.ts | 7 ++++--- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 5d8bc947d..e172d8911 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "@atproto/common": "0.5.10", "@atproto/lex": "0.0.13", "@atproto/oauth-client-node": "^0.3.15", + "@atproto/syntax": "0.4.3", "@deno/doc": "jsr:^0.189.1", "@floating-ui/vue": "1.1.10", "@iconify-json/carbon": "1.2.18", @@ -75,7 +76,6 @@ "defu": "6.1.4", "fast-npm-meta": "1.0.0", "focus-trap": "^7.8.0", - "tinyglobby": "0.2.15", "marked": "17.0.1", "module-replacements": "2.11.0", "nuxt": "4.3.0", @@ -88,6 +88,7 @@ "simple-git": "3.30.0", "spdx-license-list": "6.11.0", "std-env": "3.10.0", + "tinyglobby": "0.2.15", "ufo": "1.6.3", "unocss": "66.6.0", "unplugin-vue-router": "0.19.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1b06b4a98..085d39a9f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: '@atproto/oauth-client-node': specifier: ^0.3.15 version: 0.3.16 + '@atproto/syntax': + specifier: 0.4.3 + version: 0.4.3 '@deno/doc': specifier: jsr:^0.189.1 version: '@jsr/deno__doc@0.189.1(patch_hash=24f326e123c822a07976329a5afe91a8713e82d53134b5586625b72431c87832)' diff --git a/server/api/auth/atproto.get.ts b/server/api/auth/atproto.get.ts index ffe45c4ff..581c91294 100644 --- a/server/api/auth/atproto.get.ts +++ b/server/api/auth/atproto.get.ts @@ -7,8 +7,9 @@ import { SLINGSHOT_HOST } from '#shared/utils/constants' import { useServerSession } from '#server/utils/server-session' import type { PublicUserSession } from '#shared/schemas/publicUserSession' import { handleResolver } from '#server/utils/atproto/oauth' -import { type AtIdentifierString, Client } from '@atproto/lex' +import { Client } from '@atproto/lex' import * as app from '#shared/types/lexicons/app' +import { ensureValidAtIdentifier } from '@atproto/syntax' /** * Fetch the user's profile record to get their avatar blob reference @@ -22,10 +23,10 @@ async function getAvatar(did: string, pds: string) { const pdsUrl = new URL(pds) // Only fetch from HTTPS PDS endpoints to prevent SSRF if (did && pdsUrl.protocol === 'https:') { + ensureValidAtIdentifier(did) const client = new Client(pdsUrl) const profileResponse = await client.get(app.bsky.actor.profile, { - // Hack for now need to find an example on how to use it properly - repo: did as AtIdentifierString, + repo: did, rkey: 'self', }) From c66650692005390d0f786668101f08a371f5eb2a Mon Sep 17 00:00:00 2001 From: Bailey Townsend Date: Wed, 4 Feb 2026 12:49:54 -0600 Subject: [PATCH 8/8] forgot to re throw the error --- server/utils/atproto/oauth-session-store.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/server/utils/atproto/oauth-session-store.ts b/server/utils/atproto/oauth-session-store.ts index 64787f84c..041e09719 100644 --- a/server/utils/atproto/oauth-session-store.ts +++ b/server/utils/atproto/oauth-session-store.ts @@ -27,6 +27,7 @@ export class OAuthSessionStore implements NodeSavedSessionStore { '[oauth session store] Failed to set session:', error instanceof Error ? error.message : 'Unknown error', ) + throw error } }