Skip to content
Open
8 changes: 8 additions & 0 deletions app/components/Header/AuthModal.client.vue
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ watch(handleInput, newHandleInput => {
handleInput.value = normalized
}
})

watch(user, async newUser => {
if (newUser?.relogin) {
await authRedirect(newUser.did, {
redirectTo: route.fullPath,
})
}
})
</script>

<template>
Expand Down
7 changes: 7 additions & 0 deletions modules/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { provider } from 'std-env'

// Storage key for fetch cache - must match shared/utils/fetch-cache-config.ts
const FETCH_CACHE_STORAGE_BASE = 'fetch-cache'
// Storage key for OAuth cache - must match server/utils/atproto/storage.ts
const OAUTH_CACHE_STORAGE_BASE = 'atproto:oauth'

export default defineNuxtModule({
meta: {
Expand Down Expand Up @@ -37,6 +39,11 @@ export default defineNuxtModule({
...nitroConfig.storage[FETCH_CACHE_STORAGE_BASE],
driver: 'vercel-runtime-cache',
}

nitroConfig.storage[OAUTH_CACHE_STORAGE_BASE] = {
...nitroConfig.storage[OAUTH_CACHE_STORAGE_BASE],
driver: 'vercel-runtime-cache',
}
}

const env = process.env.VERCEL_ENV
Expand Down
2 changes: 2 additions & 0 deletions nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export default defineNuxtConfig({
github: {
orgToken: '',
},
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 || '',
Expand Down Expand Up @@ -122,6 +123,7 @@ export default defineNuxtConfig({
'/_avatar/**': { isr: 3600, proxy: 'https://www.gravatar.com/avatar/**' },
'/opensearch.xml': { isr: true },
'/oauth-client-metadata.json': { prerender: true },
'/.well-known/jwks.json': { prerender: true },
Comment on lines 125 to +126
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, find and examine the nuxt.config.ts file
find . -name "nuxt.config.ts" -type f

Repository: npmx-dev/npmx.dev

Length of output: 99


🏁 Script executed:

# Search for references to oauthJwkOne to understand how it's used
rg "oauthJwkOne" -A 3 -B 3

Repository: npmx-dev/npmx.dev

Length of output: 991


🏁 Script executed:

# Search for JWKS endpoint implementation
rg "well-known/jwks" -A 5 -B 5

Repository: npmx-dev/npmx.dev

Length of output: 1504


🏁 Script executed:

# Look for runtimeConfig configuration in the project
rg "runtimeConfig" -A 3 -B 1

Repository: npmx-dev/npmx.dev

Length of output: 470


🏁 Script executed:

# Search for the actual JWKS endpoint implementation
rg "jwks\.json" -A 10 -B 2

Repository: npmx-dev/npmx.dev

Length of output: 1545


🏁 Script executed:

# Look for server routes
find . -path "./server/routes/**" -type f | head -20

Repository: npmx-dev/npmx.dev

Length of output: 352


🏁 Script executed:

# Search for any route handling JWKS
rg "\.well-known" -A 10 -B 2

Repository: npmx-dev/npmx.dev

Length of output: 1998


🏁 Script executed:

# Look at the full nuxt.config.ts to understand the routeRules context
sed -n '115,140p' ./nuxt.config.ts

Repository: npmx-dev/npmx.dev

Length of output: 1334


🏁 Script executed:

# Read the actual JWKS endpoint implementation
cat -n ./server/routes/.well-known/jwks.json.get.ts

Repository: npmx-dev/npmx.dev

Length of output: 449


🏁 Script executed:

# Also check the oauth-client-metadata endpoint for comparison
cat -n ./server/routes/oauth-client-metadata.json.get.ts

Repository: npmx-dev/npmx.dev

Length of output: 413


🏁 Script executed:

# Search for how loadJWKs is used in the codebase
rg "loadJWKs" -A 5 -B 2

Repository: npmx-dev/npmx.dev

Length of output: 1834


🌐 Web query:

Nuxt 3 prerender routeRules runtimeConfig access build time vs runtime

💡 Result:

What happens when you set routeRules: { '/some/**': { prerender: true } }

  • prerender: true means Nuxt/Nitro will render that route at build time and write a static HTML file into .output/public/... (or equivalent) which is then served as a file, not computed per request. (nuxt.com)
  • This is part of Nuxt “hybrid rendering” where different routes can be prerendered/SSR/CSR/ISR/SWR, etc. (nuxt.com)

runtimeConfig: build-time vs runtime (the key gotcha with prerender)

Nuxt has two “times” where code can run:

  • Build time (prerender/generate time): your page’s server-render step is executed during the build.
  • Runtime (request time): your server renders pages when a request comes in.

When a route is prerendered, the “server-side render” for that page happens at build time, so:

  • Any useRuntimeConfig() usage that influences HTML for prerendered pages will be evaluated at build time, and the resulting HTML is frozen until the next build. (Because the output is a static file.) (nuxt.com)
  • Nuxt explicitly supports reading the resolved runtime config at build time (e.g., in modules) via useRuntimeConfig from Nuxt Kit. (nuxt.com)

Public vs private config and what you can “access”

  • runtimeConfig.public is intended to be exposed to client-side code; non-public keys are server-only. (nuxt.com)
  • But even public runtimeConfig values used during prerender become baked into the generated output, because prerender output is static HTML generated at build time. (nuxt.com)

Practical rule of thumb

If a value must change without rebuilding (per environment, per deployment, per request), then:

  • Don’t prerender that page’s HTML (use SSR/SWR/ISR instead), or
  • Keep the page prerendered but fetch dynamic/runtime values at runtime (e.g., from an API endpoint) so they aren’t baked into the HTML. (Prerendered HTML itself will not change without a rebuild.) (nuxt.com)

Citations:


🌐 Web query:

Nuxt 3 useRuntimeConfig during prerender SSG build

💡 Result:

During prerender / SSG (nuxi generate or nuxi build --prerender), Nuxt boots a Nuxt/Nitro instance at build time, renders your pages, and writes out static HTML + payload.json into .output/public. (nuxt.com)
So useRuntimeConfig() is evaluated with whatever config/env vars exist at build time—not at “runtime on the static host”.

What you can (and can’t) do

  • You can use useRuntimeConfig() while prerendering (it’s server-side during the build). It will see full config on the server, including private keys. (dev.nuxt.com)
  • In the generated static site, there is no server runtime config. Only runtimeConfig.public is available client-side, and its values are effectively baked into the generated output/payload. (new.nuxtjs.cn)
  • If you were hoping to change useRuntimeConfig() values after deployment (without rebuilding), that won’t work with pure static hosting—you need an SSR/server deployment (or rebuild per environment).

Common gotchas / fixes

  1. Set env vars for the build step (CI/CD), not just on the hosting platform
    Nuxt notes .env/process.env are used during development and build/generate, while production runtime should use platform env vars (which won’t apply to already-generated static files). (nuxt.com)

  2. Don’t read private config in client code
    Client-side only has access to runtimeConfig.public. (new.nuxtjs.cn)

  3. If you need config at Nuxt “build time” (module/hooks), use Nuxt Kit
    In modules/build tooling, @nuxt/kit exposes useRuntimeConfig() to access the resolved runtime config at build time. (nuxt.com)

If you share what you’re trying to accomplish (e.g., “different API base URL per environment without rebuild”, or “use secret to fetch data during prerender”), I can suggest the cleanest Nuxt pattern for that setup.

Citations:


Prerendered JWKS endpoint will serve empty keys at runtime, breaking OAuth authentication.

The /.well-known/jwks.json endpoint calls useRuntimeConfig() to read oauthJwkOne from environment. With prerender: true, this evaluation happens at build time (where OAUTH_JWK_ONE is typically unavailable in CI/CD), causing the endpoint to return an empty array []. This empty response gets baked into the prerendered HTML file and is served to all requests, rendering OAuth inoperative regardless of whether the environment variable exists at runtime.

Change to isr: 60 to allow server-side regeneration with access to runtime configuration:

Suggested change
-    '/.well-known/jwks.json': { prerender: true },
+    '/.well-known/jwks.json': { isr: 60 },

Note: /oauth-client-metadata.json has the same dependency on oauthJwkOne and should also be reconsidered for ISR.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
'/oauth-client-metadata.json': { prerender: true },
'/.well-known/jwks.json': { prerender: true },
'/oauth-client-metadata.json': { prerender: true },
'/.well-known/jwks.json': { isr: 60 },

// never cache
'/api/auth/**': { isr: false, cache: false },
'/api/social/**': { isr: false, cache: false },
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"generate:sprite": "node scripts/generate-file-tree-sprite.ts",
"generate:fixtures": "node scripts/generate-fixtures.ts",
"generate:lexicons": "lex build --lexicons lexicons --out shared/types/lexicons --clear",
"generate:jwk": "node scripts/gen-jwk.ts",
"test": "vite test",
"test:a11y": "pnpm build:test && LIGHTHOUSE_COLOR_MODE=dark pnpm test:a11y:prebuilt && LIGHTHOUSE_COLOR_MODE=light pnpm test:a11y:prebuilt",
"test:a11y:prebuilt": "./scripts/lighthouse.sh",
Expand Down
11 changes: 11 additions & 0 deletions scripts/gen-jwk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { JoseKey } from '@atproto/oauth-client-node'

async function run() {
const kid = Date.now().toString()
const key = await JoseKey.generate(['ES256'], kid)
const jwk = key.privateJwk

console.log(JSON.stringify(jwk))
}

await run()
26 changes: 8 additions & 18 deletions server/api/auth/atproto.get.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
import type { OAuthSession } from '@atproto/oauth-client-node'
import { NodeOAuthClient, OAuthCallbackError } from '@atproto/oauth-client-node'
import { OAuthCallbackError } from '@atproto/oauth-client-node'
import { createError, getQuery, sendRedirect, setCookie, getCookie, deleteCookie } from 'h3'
import type { H3Event } from 'h3'
import { getOAuthLock } from '#server/utils/atproto/lock'
import { useOAuthStorage } from '#server/utils/atproto/storage'
import { SLINGSHOT_HOST } from '#shared/utils/constants'
import { useServerSession } from '#server/utils/server-session'
import { handleResolver } from '#server/utils/atproto/oauth'
import { handleApiError } from '#server/utils/error-handler'
import type { DidString } from '@atproto/lex'
import { Client } from '@atproto/lex'
import * as com from '#shared/types/lexicons/com'
import * as app from '#shared/types/lexicons/app'
import { isAtIdentifierString } from '@atproto/lex'
import { scope, getOauthClientMetadata } from '#server/utils/atproto/oauth'
import { scope } from '#server/utils/atproto/oauth'
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 | 🔴 Critical

Import getNodeOAuthClient where it is used.

It is referenced on Line 29 but not imported, so this will throw at runtime.

🔧 Proposed fix
-import { scope } from '#server/utils/atproto/oauth'
+import { getNodeOAuthClient, scope } from '#server/utils/atproto/oauth'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { scope } from '#server/utils/atproto/oauth'
import { getNodeOAuthClient, scope } from '#server/utils/atproto/oauth'

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 | 🔴 Critical

Missing import for getNodeOAuthClient.

The function getNodeOAuthClient is called on line 29 but is not imported. This will cause a runtime error.

Proposed fix
-import { scope } from '#server/utils/atproto/oauth'
+import { scope, getNodeOAuthClient } from '#server/utils/atproto/oauth'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { scope } from '#server/utils/atproto/oauth'
import { scope, getNodeOAuthClient } from '#server/utils/atproto/oauth'

import { UNSET_NUXT_SESSION_PASSWORD } from '#shared/utils/constants'
// @ts-expect-error virtual file from oauth module
import { clientUri } from '#oauth/config'
Expand All @@ -28,17 +25,7 @@ export default defineEventHandler(async event => {
}

const query = getQuery(event)
const clientMetadata = getOauthClientMetadata()
const session = await useServerSession(event)
const { stateStore, sessionStore } = useOAuthStorage(session)

const atclient = new NodeOAuthClient({
stateStore,
sessionStore,
clientMetadata,
requestLock: getOAuthLock(),
handleResolver,
})

if (query.handle) {
// Initiate auth flow
Expand Down Expand Up @@ -66,10 +53,13 @@ export default defineEventHandler(async event => {
}

try {
const redirectUrl = await atclient.authorize(query.handle, {
const redirectUrl = await event.context.oauthClient.authorize(query.handle, {
scope,
prompt: query.create ? 'create' : undefined,
ui_locales: query.locale?.toString(),
// TODO: I do not beleive this is working as expected on
// a unsupported locale on the PDS. Gives Invalid at body.ui_locales
// Commenting out for now
// ui_locales: query.locale?.toString(),
state: encodeOAuthState(event, { redirectPath }),
})

Expand All @@ -87,7 +77,7 @@ export default defineEventHandler(async event => {
// Handle callback
try {
const params = new URLSearchParams(query as Record<string, string>)
const result = await atclient.callback(params)
const result = await event.context.oauthClient.callback(params)
try {
const state = decodeOAuthState(event, result.state)
const profile = await getMiniProfile(result.session)
Expand Down
15 changes: 13 additions & 2 deletions server/api/auth/session.get.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
import { PublicUserSessionSchema } from '#shared/schemas/publicUserSession'
import { safeParse } from 'valibot'

export default defineEventHandler(async event => {
const serverSession = await useServerSession(event)
export default eventHandlerWithOAuthSession(async (event, _, serverSession) => {
const result = safeParse(PublicUserSessionSchema, serverSession.data.public)
if (!result.success) {
return null
}

// A one time redirect to upgrade the previous sessions.
// Can remove in 2 weeks from merge if we'd like
if (serverSession.data.oauthSession && serverSession.data?.public?.did) {
await serverSession.update({
oauthSession: undefined,
})
return {
...result.output,
relogin: true,
}
}
Comment on lines +4 to +20
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 | 🟡 Minor

Time-relative migration comment is likely to go stale; consider a dated TODO/issue reference.

The one-time migration behaviour is reasonable, but the comment on Line 11-12 (“remove in 2 weeks from merge”) is hard to track over time. Consider replacing it with a dated TODO (or a tracked issue ID) so it doesn’t linger indefinitely.

As per coding guidelines, “Add comments only to explain complex logic or non-obvious implementations”.


return result.output
})
20 changes: 20 additions & 0 deletions server/plugins/oauth-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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()

// Attach to event context for access in composables via useRequestEvent()
nitroApp.hooks.hook('request', event => {
event.context.oauthClient = oauthClient
})
})

// Extend the H3EventContext type
declare module 'h3' {
interface H3EventContext {
oauthClient: NodeOAuthClient
}
}
11 changes: 11 additions & 0 deletions server/routes/.well-known/jwks.json.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
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 []
}
Comment on lines +5 to +8
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 | 🟡 Minor

Inconsistent JWKS response format on failure.

When JWKs fail to load, returning an empty array [] does not conform to the JWKS specification, which expects { "keys": [] }. OAuth clients parsing this endpoint may fail if they expect the standard JWKS structure.

Proposed fix
   if (!keys) {
     console.error('Failed to load JWKs. May not be set')
-    return []
+    return { keys: [] }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!keys) {
console.error('Failed to load JWKs. May not be set')
return []
}
if (!keys) {
console.error('Failed to load JWKs. May not be set')
return { keys: [] }
}


return keys.publicJwks
})
6 changes: 4 additions & 2 deletions server/routes/oauth-client-metadata.json.get.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export default defineEventHandler(() => {
return getOauthClientMetadata()
export default defineEventHandler(async _ => {
const keyset = await loadJWKs()
const pk = keyset?.findPrivateKey({ usage: 'sign' })
return getOauthClientMetadata(pk?.alg)
})
47 changes: 20 additions & 27 deletions server/utils/atproto/oauth-session-store.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,32 @@
import type { NodeSavedSession, NodeSavedSessionStore } from '@atproto/oauth-client-node'
import type { UserServerSession } from '#shared/types/userSession'
import type { SessionManager } from 'h3'
import { OAUTH_CACHE_STORAGE_BASE } from '#server/utils/atproto/storage'

// Refresh tokens from a confidential client should last for 180 days, each new refresh of access token resets
// the expiration with the new refresh token. Shorting to 179 days to keep it a bit simpler since we rely on redis to clear sessions
// Note: This expiration only lasts this long in production. Local dev is 2 weeks
const SESSION_EXPIRATION = CACHE_MAX_AGE_ONE_DAY * 179

export class OAuthSessionStore implements NodeSavedSessionStore {
private readonly session: SessionManager<UserServerSession>
private readonly cache: CacheAdapter

constructor() {
this.cache = getCacheAdapter(OAUTH_CACHE_STORAGE_BASE)
}

constructor(session: SessionManager<UserServerSession>) {
this.session = session
private createStorageKey(did: string) {
return `sessions:${did}`
}

async get(): Promise<NodeSavedSession | undefined> {
const sessionData = this.session.data
if (!sessionData) return undefined
return sessionData.oauthSession
async get(key: string): Promise<NodeSavedSession | undefined> {
let session = await this.cache.get<NodeSavedSession>(this.createStorageKey(key))
return session ?? undefined
}

async set(_key: string, val: NodeSavedSession) {
// We are ignoring the key since the mapping is already done in the session
try {
await this.session.update({
oauthSession: val,
})
} catch (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',
)
throw error
}
async set(key: string, val: NodeSavedSession) {
await this.cache.set<NodeSavedSession>(this.createStorageKey(key), val, SESSION_EXPIRATION)
}

async del() {
await this.session.update({
oauthSession: undefined,
})
async del(key: string) {
await this.cache.delete(this.createStorageKey(key))
}
}
36 changes: 18 additions & 18 deletions server/utils/atproto/oauth-state-store.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
import type { NodeSavedState, NodeSavedStateStore } from '@atproto/oauth-client-node'
import type { UserServerSession } from '#shared/types/userSession'
import type { SessionManager } from 'h3'
import { OAUTH_CACHE_STORAGE_BASE } from './storage'

// It is recommended that oauth state is only saved for 30 minutes
const STATE_EXPIRATION = CACHE_MAX_AGE_ONE_MINUTE * 30

export class OAuthStateStore implements NodeSavedStateStore {
private readonly session: SessionManager<UserServerSession>
private readonly cache: CacheAdapter

constructor() {
this.cache = getCacheAdapter(OAUTH_CACHE_STORAGE_BASE)
}

Comment on lines 4 to 13
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cat -n server/utils/atproto/oauth-state-store.ts

Repository: npmx-dev/npmx.dev

Length of output: 2150


🌐 Web query:

@atproto/oauth-client-node NodeSavedStateStore key parameter DID state concurrent

💡 Result:

In @atproto/oauth-client-node, the NodeSavedStateStore key is the OAuth state value for a single authorization attempt (the same state you pass to client.authorize(...) and get back from client.callback(...)). (gitea.moe)

Practical implications:

  • Don’t use the DID as the key. The DID (sub) is used for the sessionStore key, not the stateStore key. (gitea.moe)
  • To support concurrent sign-ins (even for the same user), generate a fresh, unguessable, unique state per attempt, and store the internal state under that state string; each concurrent flow gets a different key. (atproto.com)
  • You should expire/delete state entries quickly (the README suggests ~1 hour is plenty). (gitea.moe)
  • For concurrent token refresh of the same DID/session across processes, use requestLock (the README notes “one lock per user id”). (gitea.moe)

Citations:


Add missing imports and fix concurrent OAuth state handling.

  1. Missing imports (build blocker): Import CACHE_MAX_AGE_ONE_MINUTE, CacheAdapter, getCacheAdapter, and OAUTH_CACHE_STORAGE_BASE before use. Also import crypto for line 32.

  2. Concurrent state bug: Regenerating oauthStateId on every set() (line 32–35) and clearing it on del() (line 44–46) breaks concurrent OAuth flows. Per @atproto/oauth-client-node spec, the key parameter is the unique OAuth state nonce for each flow. The current approach overwrites oauthStateId on each set(), causing subsequent flows to corrupt earlier state lookups.

Either reuse the same oauthStateId across concurrent states (create once, persist), or simplify by using the key parameter directly as the storage key since it is already unique and unguessable per the OAuth spec.

Example fix (one-time session identifier)
+import { CACHE_MAX_AGE_ONE_MINUTE } from '#shared/utils/constants'
+import type { CacheAdapter } from '#server/utils/cache/shared'
+import { getCacheAdapter } from '#server/utils/cache/adapter'
+import { OAUTH_CACHE_STORAGE_BASE } from '#server/utils/atproto/storage'

 // It is recommended that oauth state is only saved for 30 minutes
 const STATE_EXPIRATION = CACHE_MAX_AGE_ONE_MINUTE * 30

 export class OAuthStateStore implements NodeSavedStateStore {
   private readonly serverSession: SessionManager<UserServerSession>
   private readonly cache: CacheAdapter

   constructor(session: SessionManager<UserServerSession>) {
     this.serverSession = session
     this.cache = getCacheAdapter(OAUTH_CACHE_STORAGE_BASE)
   }

   private createStorageKey(stateKey: string, browserStateId: string) {
-    return `state:${did}:${sessionId}`
+    return `state:${browserStateId}:${stateKey}`
   }

   async get(key: string): Promise<NodeSavedState | undefined> {
-    const serverSessionData = this.serverSession.data
-    if (!serverSessionData) return undefined
-    if (!serverSessionData.oauthStateId) return undefined
+    const browserStateId = this.serverSession.data?.oauthStateId
+    if (!browserStateId) return undefined
     const state = await this.cache.get<NodeSavedState>(
-      this.createStorageKey(key, serverSessionData.oauthStateId),
+      this.createStorageKey(key, browserStateId),
     )
     return state ?? undefined
   }

   async set(key: string, val: NodeSavedState) {
-    let stateId = crypto.randomUUID()
-    await this.serverSession.update({
-      oauthStateId: stateId,
-    })
-    await this.cache.set<NodeSavedState>(this.createStorageKey(key, stateId), val, STATE_EXPIRATION)
+    const existing = this.serverSession.data?.oauthStateId
+    const browserStateId = existing ?? crypto.randomUUID()
+    if (!existing) {
+      await this.serverSession.update({ oauthStateId: browserStateId })
+    }
+    await this.cache.set<NodeSavedState>(
+      this.createStorageKey(key, browserStateId),
+      val,
+      STATE_EXPIRATION,
+    )
   }

   async del(key: string) {
-    const serverSessionData = this.serverSession.data
-    if (!serverSessionData) return undefined
-    if (!serverSessionData.oauthStateId) return undefined
+    const browserStateId = this.serverSession.data?.oauthStateId
+    if (!browserStateId) return
     await this.cache.delete(this.createStorageKey(key, serverSessionData.oauthStateId))
-    await this.serverSession.update({
-      oauthStateId: undefined,
-    })
   }
 }

constructor(session: SessionManager<UserServerSession>) {
this.session = session
private createStorageKey(key: string) {
return `state:${key}`
}

async get(): Promise<NodeSavedState | undefined> {
const sessionData = this.session.data
if (!sessionData) return undefined
return sessionData.oauthState
async get(key: string): Promise<NodeSavedState | undefined> {
const state = await this.cache.get<NodeSavedState>(this.createStorageKey(key))
return state ?? undefined
}

async set(_key: string, val: NodeSavedState) {
// We are ignoring the key since the mapping is already done in the session
await this.session.update({
oauthState: val,
})
async set(key: string, val: NodeSavedState) {
await this.cache.set<NodeSavedState>(this.createStorageKey(key), val, STATE_EXPIRATION)
}

async del() {
await this.session.update({
oauthState: undefined,
})
async del(key: string) {
await this.cache.delete(this.createStorageKey(key))
}
}
Loading
Loading