From 183fb913e842e24a8e2d75bbeb756237ed641b58 Mon Sep 17 00:00:00 2001 From: userquin Date: Sat, 7 Mar 2026 13:17:37 +0100 Subject: [PATCH 1/8] fix: use authors hash for bsky author profile endpoint cache key --- server/api/atproto/bluesky-author-profiles.get.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server/api/atproto/bluesky-author-profiles.get.ts b/server/api/atproto/bluesky-author-profiles.get.ts index 1122376d82..4499a02a61 100644 --- a/server/api/atproto/bluesky-author-profiles.get.ts +++ b/server/api/atproto/bluesky-author-profiles.get.ts @@ -4,6 +4,7 @@ import { AuthorSchema } from '#shared/schemas/blog' import { Client } from '@atproto/lex' import type { Author, ResolvedAuthor } from '#shared/schemas/blog' import * as app from '#shared/types/lexicons/app' +import * as crypto from 'node:crypto' export default defineCachedEventHandler( async event => { @@ -75,7 +76,11 @@ export default defineCachedEventHandler( maxAge: CACHE_MAX_AGE_ONE_DAY, getKey: event => { const { authors } = getQuery(event) - return `author-profiles:${authors ?? 'npmx.dev'}` + if (!authors) { + return 'author-profiles:npmx.dev' + } + const key = crypto.createHash('sha256').update(JSON.stringify(authors)).digest('hex') + return `author-profiles:${key}` }, }, ) From d2894c36de828db0f1a619c710f0186228b06008 Mon Sep 17 00:00:00 2001 From: userquin Date: Sat, 7 Mar 2026 16:22:23 +0100 Subject: [PATCH 2/8] chore: fix JWK error --- nuxt.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nuxt.config.ts b/nuxt.config.ts index 3a4c167ece..004e77678a 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -39,7 +39,7 @@ export default defineNuxtConfig({ github: { orgToken: '', }, - oauthJwkOne: process.env.OAUTH_JWK_ONE || undefined, + oauthJwkOne: process.env.OAUTH_JWK_ONE || (process.env.TEST ? '' : undefined), // Upstash Redis for distributed OAuth token refresh locking in production upstash: { redisRestUrl: process.env.UPSTASH_KV_REST_API_URL || process.env.KV_REST_API_URL || '', From f27a3324eb57c9e715e1ecd02ca0a5d37317abb9 Mon Sep 17 00:00:00 2001 From: userquin Date: Sat, 7 Mar 2026 16:29:50 +0100 Subject: [PATCH 3/8] chore: . --- nuxt.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nuxt.config.ts b/nuxt.config.ts index 004e77678a..6f730b4b11 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -39,7 +39,7 @@ export default defineNuxtConfig({ github: { orgToken: '', }, - oauthJwkOne: process.env.OAUTH_JWK_ONE || (process.env.TEST ? '' : undefined), + oauthJwkOne: process.env.TEST ? '' : process.env.OAUTH_JWK_ONE || undefined, // Upstash Redis for distributed OAuth token refresh locking in production upstash: { redisRestUrl: process.env.UPSTASH_KV_REST_API_URL || process.env.KV_REST_API_URL || '', From 1256ba67a9d8f457c73564a8a378b152343f56fc Mon Sep 17 00:00:00 2001 From: userquin Date: Sat, 7 Mar 2026 16:38:57 +0100 Subject: [PATCH 4/8] chore: revert JWK changes --- nuxt.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nuxt.config.ts b/nuxt.config.ts index 6f730b4b11..3a4c167ece 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -39,7 +39,7 @@ export default defineNuxtConfig({ github: { orgToken: '', }, - oauthJwkOne: process.env.TEST ? '' : process.env.OAUTH_JWK_ONE || undefined, + oauthJwkOne: process.env.OAUTH_JWK_ONE || undefined, // Upstash Redis for distributed OAuth token refresh locking in production upstash: { redisRestUrl: process.env.UPSTASH_KV_REST_API_URL || process.env.KV_REST_API_URL || '', From dc27dcc3fe3e599e2a369cc4ac019087bc8fae80 Mon Sep 17 00:00:00 2001 From: userquin Date: Sat, 7 Mar 2026 17:27:46 +0100 Subject: [PATCH 5/8] chore: prevent error loading node oauth client when testing --- server/plugins/oauth-client.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/plugins/oauth-client.ts b/server/plugins/oauth-client.ts index 7f888271ba..6cb885d073 100644 --- a/server/plugins/oauth-client.ts +++ b/server/plugins/oauth-client.ts @@ -4,6 +4,10 @@ import type { NodeOAuthClient } from '@atproto/oauth-client-node' * Creates a long living instance of the NodeOAuthClient. */ export default defineNitroPlugin(async nitroApp => { + if (import.meta.test) { + return + } + const oauthClient = await getNodeOAuthClient() // Attach to event context for access in composables via useRequestEvent() From b5ec1b583181b05ef02e4129ced985397774372e Mon Sep 17 00:00:00 2001 From: userquin Date: Sat, 7 Mar 2026 17:45:49 +0100 Subject: [PATCH 6/8] Revert "chore: prevent error loading node oauth client when testing" This reverts commit dc27dcc3fe3e599e2a369cc4ac019087bc8fae80. --- server/plugins/oauth-client.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/server/plugins/oauth-client.ts b/server/plugins/oauth-client.ts index 6cb885d073..7f888271ba 100644 --- a/server/plugins/oauth-client.ts +++ b/server/plugins/oauth-client.ts @@ -4,10 +4,6 @@ import type { NodeOAuthClient } from '@atproto/oauth-client-node' * Creates a long living instance of the NodeOAuthClient. */ export default defineNitroPlugin(async nitroApp => { - if (import.meta.test) { - return - } - const oauthClient = await getNodeOAuthClient() // Attach to event context for access in composables via useRequestEvent() From 31049582a74e3388dcfde415b85dbcaca944de7f Mon Sep 17 00:00:00 2001 From: userquin Date: Sat, 7 Mar 2026 18:11:23 +0100 Subject: [PATCH 7/8] chore: wrap `loadJWKs` with `try-catch` block --- server/routes/.well-known/jwks.json.get.ts | 1 - server/utils/atproto/oauth.ts | 18 +++++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/server/routes/.well-known/jwks.json.get.ts b/server/routes/.well-known/jwks.json.get.ts index f7c2eda628..129ed5c16e 100644 --- a/server/routes/.well-known/jwks.json.get.ts +++ b/server/routes/.well-known/jwks.json.get.ts @@ -3,7 +3,6 @@ import { loadJWKs } from '#server/utils/atproto/oauth' export default defineEventHandler(async _ => { const keys = await loadJWKs() if (!keys) { - console.error('Failed to load JWKs. May not be set') return [] } diff --git a/server/utils/atproto/oauth.ts b/server/utils/atproto/oauth.ts index 58152fd2b1..dd556584e4 100644 --- a/server/utils/atproto/oauth.ts +++ b/server/utils/atproto/oauth.ts @@ -89,13 +89,25 @@ export async function loadJWKs(): Promise { // If we ever need to add multiple JWKs to rotate keys we will need to add a new one // under a new variable and update here const jwkOne = useRuntimeConfig().oauthJwkOne - if (!jwkOne) return undefined + if (!jwkOne) { + if (import.meta.test) { + // eslint-disable-next-line no-console + console.error('Failed to load JWKs (not set).') + } + return undefined + } // For multiple keys if we need to rotate // const keys = await Promise.all([JoseKey.fromImportable(jwkOne)]) - const keys = await JoseKey.fromImportable(jwkOne) - return new Keyset([keys]) + try { + const keys = await JoseKey.fromImportable(jwkOne) + return new Keyset([keys]) + } catch (e) { + // eslint-disable-next-line no-console + console.error('Failed to load JWKs.', e) + return undefined + } } async function getOAuthSession(event: H3Event): Promise<{ From 3ad466cc948e4fa17ddc880da23ddf53f16fb26a Mon Sep 17 00:00:00 2001 From: userquin Date: Sat, 7 Mar 2026 18:52:29 +0100 Subject: [PATCH 8/8] chore: protect `oauthClient` usages --- server/api/auth/atproto.get.ts | 9 ++++++++- server/plugins/oauth-client.ts | 16 +++++++++++----- server/utils/atproto/oauth.ts | 14 ++++---------- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/server/api/auth/atproto.get.ts b/server/api/auth/atproto.get.ts index 64137bdd4e..22dbf8372f 100644 --- a/server/api/auth/atproto.get.ts +++ b/server/api/auth/atproto.get.ts @@ -80,7 +80,14 @@ export default defineEventHandler(async event => { // Handle callback try { const params = new URLSearchParams(query as Record) - const result = await event.context.oauthClient.callback(params) + const result = await event.context.oauthClient?.callback(params) + if (!result) { + return handleApiError('Failed to initiate authentication', { + statusCode: 401, + statusMessage: 'Unauthorized', + message: `Failed to initiate authentication. Please login and try again.`, + }) + } try { const state = decodeOAuthState(event, result.state) const profile = await getMiniProfile(result.session) diff --git a/server/plugins/oauth-client.ts b/server/plugins/oauth-client.ts index 7f888271ba..874e9e74d5 100644 --- a/server/plugins/oauth-client.ts +++ b/server/plugins/oauth-client.ts @@ -4,12 +4,18 @@ import type { NodeOAuthClient } from '@atproto/oauth-client-node' * Creates a long living instance of the NodeOAuthClient. */ export default defineNitroPlugin(async nitroApp => { - const oauthClient = await getNodeOAuthClient() + try { + const oauthClient = await getNodeOAuthClient() - // Attach to event context for access in composables via useRequestEvent() - nitroApp.hooks.hook('request', event => { - event.context.oauthClient = oauthClient - }) + // Attach to event context for access in composables via useRequestEvent() + nitroApp.hooks.hook('request', event => { + event.context.oauthClient = oauthClient + }) + } catch (e) { + if (!import.meta.test) { + throw e + } + } }) // Extend the H3EventContext type diff --git a/server/utils/atproto/oauth.ts b/server/utils/atproto/oauth.ts index dd556584e4..b947b3df96 100644 --- a/server/utils/atproto/oauth.ts +++ b/server/utils/atproto/oauth.ts @@ -90,7 +90,7 @@ export async function loadJWKs(): Promise { // under a new variable and update here const jwkOne = useRuntimeConfig().oauthJwkOne if (!jwkOne) { - if (import.meta.test) { + if (!import.meta.test) { // eslint-disable-next-line no-console console.error('Failed to load JWKs (not set).') } @@ -100,14 +100,8 @@ export async function loadJWKs(): Promise { // For multiple keys if we need to rotate // const keys = await Promise.all([JoseKey.fromImportable(jwkOne)]) - try { - const keys = await JoseKey.fromImportable(jwkOne) - return new Keyset([keys]) - } catch (e) { - // eslint-disable-next-line no-console - console.error('Failed to load JWKs.', e) - return undefined - } + const keys = await JoseKey.fromImportable(jwkOne) + return new Keyset([keys]) } async function getOAuthSession(event: H3Event): Promise<{ @@ -123,7 +117,7 @@ async function getOAuthSession(event: H3Event): Promise<{ return { oauthSession: undefined, serverSession } } - const oauthSession = await event.context.oauthClient.restore(currentSession.public.did) + const oauthSession = await event.context.oauthClient?.restore(currentSession.public.did) return { oauthSession, serverSession } } catch (error) { // Log error safely without using util.inspect on potentially problematic objects