Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -34,6 +34,7 @@ export default defineNuxtConfig({

runtimeConfig: {
sessionPassword: '',
oauthJwkOne: process.env.OAUTH_JWK_ONE || undefined,
// Upstash Redis for distributed OAuth token refresh locking in production
Comment on lines 35 to 38
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:

#!/bin/bash
# Check where each env var name is referenced
rg -n "OAUTH_JWK_ONE|NUXT_OAUTH_JWK_ONE" --type-list

Repository: npmx-dev/npmx.dev

Length of output: 450


🏁 Script executed:

#!/bin/bash
# Search for both env var references across the codebase
rg -n "OAUTH_JWK_ONE|NUXT_OAUTH_JWK_ONE"

Repository: npmx-dev/npmx.dev

Length of output: 135


🏁 Script executed:

#!/bin/bash
# Search for other NUXT_ prefixed env vars in the codebase
rg -n "process\.env\.NUXT_"

Repository: npmx-dev/npmx.dev

Length of output: 644


🏁 Script executed:

#!/bin/bash
# Look for .env files and environment documentation
fd -type f "\.env" -o "\.env\.*" -o "*env*example*" -o "*README*" -o "*ENV*"

Repository: npmx-dev/npmx.dev

Length of output: 231


🏁 Script executed:

#!/bin/bash
# Check the nuxt.config.ts file to see the full context around line 37
cat -n nuxt.config.ts | head -50

Repository: npmx-dev/npmx.dev

Length of output: 1726


🏁 Script executed:

#!/bin/bash
# Search for sessionPassword references
rg -n "sessionPassword|SESSION_PASSWORD"

Repository: npmx-dev/npmx.dev

Length of output: 1138


🏁 Script executed:

#!/bin/bash
# Look for documentation or config files mentioning env vars
fd -type f -name "README*" -o -name "*.md" | head -10

Repository: npmx-dev/npmx.dev

Length of output: 231


🏁 Script executed:

#!/bin/bash
# Check if there's a .env.example or similar
find . -maxdepth 2 -type f \( -name ".env*" -o -name "env.*" \) 2>/dev/null

Repository: npmx-dev/npmx.dev

Length of output: 91


🏁 Script executed:

#!/bin/bash
# Read the .env.example file
cat .env.example

Repository: npmx-dev/npmx.dev

Length of output: 132


🏁 Script executed:

#!/bin/bash
# Search for OAUTH_JWK in documentation or comments
rg -n "OAUTH_JWK" -B 2 -A 2

Repository: npmx-dev/npmx.dev

Length of output: 345


🏁 Script executed:

#!/bin/bash
# Check modules/oauth.ts to understand the OAuth configuration pattern
cat -n modules/oauth.ts | head -50

Repository: npmx-dev/npmx.dev

Length of output: 1727


🏁 Script executed:

#!/bin/bash
# Search for JWKS or jwks references
rg -n "jwks|JWKS|JWK" -i

Repository: npmx-dev/npmx.dev

Length of output: 3401


🏁 Script executed:

#!/bin/bash
# Check git log for context around this change
git log --oneline -20 -- nuxt.config.ts

Repository: npmx-dev/npmx.dev

Length of output: 96


🏁 Script executed:

#!/bin/bash
# Look for any documentation about OAuth setup
fd -type f -name "*.md" | xargs rg -l "oauth|OAUTH" -i 2>/dev/null | head -5

Repository: npmx-dev/npmx.dev

Length of output: 445


Use standard Nuxt env var naming for consistency.

The oauthJwkOne config reads from OAUTH_JWK_ONE, but Nuxt convention (as used for sessionPassword with NUXT_SESSION_PASSWORD) suggests it should be NUXT_OAUTH_JWK_ONE. To avoid confusion and follow the established pattern, either rename to the prefixed variant or support both for backwards compatibility.

💡 Suggested approach (support both names)
-    oauthJwkOne: process.env.OAUTH_JWK_ONE || undefined,
+    oauthJwkOne: process.env.NUXT_OAUTH_JWK_ONE || process.env.OAUTH_JWK_ONE || undefined,
📝 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
runtimeConfig: {
sessionPassword: '',
oauthJwkOne: process.env.OAUTH_JWK_ONE || undefined,
// Upstash Redis for distributed OAuth token refresh locking in production
runtimeConfig: {
sessionPassword: '',
oauthJwkOne: process.env.NUXT_OAUTH_JWK_ONE || process.env.OAUTH_JWK_ONE || undefined,
// Upstash Redis for distributed OAuth token refresh locking in production

Comment on lines 35 to 38
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:

rg -n "OAUTH_JWK_ONE|NUXT_OAUTH_JWK_ONE"

Repository: npmx-dev/npmx.dev

Length of output: 135


🏁 Script executed:

# Check for environment variable references in deployment or config documentation
fd -e md -e yml -e yaml -e json | head -20 | xargs rg -l "OAUTH_JWK|env var|environment" 2>/dev/null || true

Repository: npmx-dev/npmx.dev

Length of output: 140


🏁 Script executed:

# Examine other runtimeConfig entries to understand naming pattern
cat -n nuxt.config.ts | head -50

Repository: npmx-dev/npmx.dev

Length of output: 1726


🏁 Script executed:

rg -A 3 -B 3 "OAUTH_JWK|oauth" docs/ README.md CONTRIBUTING.md 2>/dev/null || true

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

# Check for deployment or environment setup documentation
rg -i "env.*var|environment.*setup|deployment" docs/ README.md --max-count=5

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

# Search for any reference to how runtimeConfig env vars should be named
rg -i "nuxt.*env.*prefix|NUXT_|runtimeConfig" README.md docs/ CONTRIBUTING.md --max-count=10

Repository: npmx-dev/npmx.dev

Length of output: 316


Align the environment variable name with Nuxt 3 conventions.

Nuxt 3 automatically prefixes runtimeConfig entries with NUXT_ when reading environment variables (as seen in CONTRIBUTING.md with NUXT_TEST_FIXTURES). The current code reads process.env.OAUTH_JWK_ONE directly, bypassing this auto-prefixing. In standard Nuxt deployments, the variable is set as NUXT_OAUTH_JWK_ONE, so the current code will always resolve to undefined.

Either allow Nuxt's auto-prefixing to work (remove process.env access), or explicitly support both the prefixed and non-prefixed variants:

🔧 Suggested adjustment
-    oauthJwkOne: process.env.OAUTH_JWK_ONE || undefined,
+    oauthJwkOne:
+      process.env.NUXT_OAUTH_JWK_ONE ??
+      process.env.OAUTH_JWK_ONE ??
+      undefined,
📝 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
runtimeConfig: {
sessionPassword: '',
oauthJwkOne: process.env.OAUTH_JWK_ONE || undefined,
// Upstash Redis for distributed OAuth token refresh locking in production
runtimeConfig: {
sessionPassword: '',
oauthJwkOne:
process.env.NUXT_OAUTH_JWK_ONE ??
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 @@ -119,6 +120,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 },
// 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 @@ -33,6 +33,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()
23 changes: 7 additions & 16 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,8 @@ 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,
})
const atclient = await getNodeOAuthClient(session, config)

if (query.handle) {
// Initiate auth flow
Expand Down Expand Up @@ -69,7 +57,10 @@ export default defineEventHandler(async event => {
const redirectUrl = await atclient.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 Down
12 changes: 12 additions & 0 deletions server/api/auth/session.get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,17 @@ export default defineEventHandler(async event => {
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,
}
}

return result.output
})
12 changes: 12 additions & 0 deletions server/routes/.well-known/jwks.json.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { loadJWKs } from '#server/utils/atproto/oauth'

export default defineEventHandler(async event => {
const config = useRuntimeConfig(event)
const keys = await loadJWKs(config)
if (!keys) {
console.error('Failed to load JWKs. May not be set')
return []
}
Comment on lines +6 to +9
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
})
8 changes: 6 additions & 2 deletions server/routes/oauth-client-metadata.json.get.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
export default defineEventHandler(() => {
return getOauthClientMetadata()
export default defineEventHandler(async event => {
const config = useRuntimeConfig(event)
const keyset = await loadJWKs(config)
// @ts-expect-error Taken from statusphere-example-app. Throws a ts error
const pk = keyset?.findPrivateKey({ use: 'sig' })
Comment on lines +3 to +5
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

🧩 Analysis chain

🏁 Script executed:

# First, locate and examine the file in question
find . -name "oauth-client-metadata.json.get.ts" -type f

Repository: npmx-dev/npmx.dev

Length of output: 110


🏁 Script executed:

# Check for existing oauth utils
find . -path "*/server/utils/atproto/*" -type f

Repository: npmx-dev/npmx.dev

Length of output: 331


🏁 Script executed:

# Search for Keyset imports and usage patterns
rg "import.*Keyset|from.*oauth-client" --type ts --type tsx

Repository: npmx-dev/npmx.dev

Length of output: 88


🏁 Script executed:

# Check package.json for `@atproto/oauth-client-node` version
cat package.json | grep -A 2 -B 2 "@atproto/oauth-client"

Repository: npmx-dev/npmx.dev

Length of output: 235


🏁 Script executed:

# Read the file with the issue
cat -n ./server/routes/oauth-client-metadata.json.get.ts

Repository: npmx-dev/npmx.dev

Length of output: 413


🏁 Script executed:

# Read oauth.ts to check for existing helpers
cat -n ./server/utils/atproto/oauth.ts

Repository: npmx-dev/npmx.dev

Length of output: 7060


🏁 Script executed:

# Search for Keyset type and how it's used elsewhere
rg "Keyset|findPrivateKey" --type ts -A 3 -B 1

Repository: npmx-dev/npmx.dev

Length of output: 2001


🌐 Web query:

@atproto/oauth-client-node Keyset type findPrivateKey method

💡 Result:

In @atproto/oauth-client-node (and the core @atproto/oauth-client), the keyset you pass is not a Keyset object with a findPrivateKey() method—it’s simply a list/array of private Key objects used for private_key_jwt client authentication. The README shows keyset: [ await JoseKey.fromImportable(...), ... ]. [1]

So there is no keyset.findPrivateKey(...) to call. If you need that behavior, implement it yourself by selecting from your array (usually by kid), e.g. keyset.find(k => k.kid === kid).

To construct keys for that array, use JoseKey.fromImportable(input, kid?) (optionally supplying a kid). [2]

Sources
[1] @atproto/oauth-client README example showing keyset as an array of keys: (npmjs.com)
[2] JoseKey.fromImportable(input, kid?) in @atproto/jwk-jose: (app.unpkg.com)

Citations:


🏁 Script executed:

# Check the `@atproto/oauth-client-node` types for Keyset
find node_modules/@atproto/oauth-client-node -name "*.d.ts" -type f | head -20

Repository: npmx-dev/npmx.dev

Length of output: 135


🏁 Script executed:

# Look for Keyset type definition
rg "class Keyset|interface Keyset|type Keyset" --type ts

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

# Check if there are any other implementations or references to findPrivateKey
rg "findPrivateKey" --type ts -B 2 -A 2

Repository: npmx-dev/npmx.dev

Length of output: 891


🌐 Web query:

@atproto/oauth-client-node 0.3.15 Keyset API documentation

💡 Result:

keyset (confidential-client signing keys)

In @atproto/oauth-client-node@0.3.15, the “Keyset API” is the keyset option you pass into the underlying OAuthClient configuration to enable confidential client auth (token_endpoint_auth_method=private_key_jwt). The type is:

  • keyset?: Keyset | Iterable<Key> (imported from @atproto/jwk) [2]

What it’s for:

  • Provide one or more private keys used to sign the client assertion JWT for authenticated token requests.
  • These must correspond to the public keys you publish in client metadata (jwks / jwks_uri). [1][3]
  • The public JWKS corresponding to your configured private keys can be obtained via the client.jwks getter. [1]

Minimal example (Node / backend / BFF):

import { NodeOAuthClient } from '@atproto/oauth-client-node'
import { JoseKey } from '@atproto/jwk-jose'

const client = new NodeOAuthClient({
  responseMode: 'query',
  clientMetadata: {
    client_id: 'https://example.com/oauth-client-metadata.json',
    jwks_uri: 'https://example.com/jwks.json',
    token_endpoint_auth_method: 'private_key_jwt',
  },

  // ...stateStore, sessionStore, etc...

  keyset: [
    await JoseKey.fromImportable(process.env.PRIVATE_KEY_1!),
    await JoseKey.fromImportable(process.env.PRIVATE_KEY_2!),
  ],
})

Notes:

  • If you’re building a native/Electron app without a backend, you typically do not provide a keyset (you can’t safely store the private key client-side). [4]
  • Atproto OAuth expects confidential clients to publish JWKS and rotate keys; authorization servers may re-fetch metadata/JWKS and can revoke sessions if keys disappear. [3]

Sources: @atproto/oauth-client config example showing keyset and client.jwks [1], OAuthClientOptions type including keyset?: Keyset | Iterable<Key> [2], atproto OAuth spec on jwks / jwks_uri and confidential-client key expectations [3], @atproto/oauth-client-node readme note about no keyset in app-only clients [4].


🌐 Web query:

statusphere-example-app oauth keyset findPrivateKey

💡 Result:

In bluesky-social/statusphere-example-app, the OAuth “private key / keyset” is configured via the PRIVATE_KEYS environment variable (a JSON array of JWKs). In production you’re expected to generate a JWK (private key) with ./bin/gen-jwk and paste the generated JWK JSON into PRIVATE_KEYS (example shown in the README). The app uses the first key in that array as the current signing key (older keys can remain for validating existing sessions). [1]

The OAuth client in the app is built on @atproto/oauth-client-node and is initialized in the app’s authentication code (src/auth/client.ts per the project docs). [2]

Sources: GitHub README for the repo (key generation + PRIVATE_KEYS behavior) [1]; DeepWiki “Authentication System” page (OAuth client package + file locations) [2].

[1] (github.com)
[2] (deepwiki.com)

Citations:


Replace the @ts-expect-error suppression with a typed helper in both locations.

The suppression weakens type safety and violates the "strictly type-safe" guideline. This pattern appears in both server/routes/oauth-client-metadata.json.get.ts (lines 3–5) and server/utils/atproto/oauth.ts (lines 80–81), indicating a systemic need for a typed solution. Extract a helper function (e.g., getPrivateSigningKey) in server/utils/atproto/oauth.ts to encapsulate the private key selection logic and eliminate the type suppression across both files.

Example change
-  // `@ts-expect-error` Taken from statusphere-example-app. Throws a ts error
-  const pk = keyset?.findPrivateKey({ use: 'sig' })
+  const pk = getPrivateSigningKey(keyset)
🧰 Tools
🪛 GitHub Check: 💪 Type check

[failure] 3-3:
Argument of type 'RuntimeConfig' is not assignable to parameter of type 'NitroRuntimeConfig'.

return getOauthClientMetadata(pk?.alg)
Comment on lines +1 to +6
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:

# Find package.json to check `@atproto/oauth-client-node` version
find . -name "package.json" -type f | head -5

Repository: npmx-dev/npmx.dev

Length of output: 114


🏁 Script executed:

# Look at the file in question to understand full context
cat -n server/routes/oauth-client-metadata.json.get.ts

Repository: npmx-dev/npmx.dev

Length of output: 413


🏁 Script executed:

# Search for loadJWKs import to find its source
rg "loadJWKs" --type ts --type tsx -B2 -A2

Repository: npmx-dev/npmx.dev

Length of output: 88


🏁 Script executed:

# Search for loadJWKs import more broadly
rg "loadJWKs" -B2 -A2

Repository: npmx-dev/npmx.dev

Length of output: 1356


🏁 Script executed:

# Check main package.json for `@atproto` dependencies
cat package.json | grep -A5 -B5 "@atproto"

Repository: npmx-dev/npmx.dev

Length of output: 626


🏁 Script executed:

# Read the oauth.ts file to understand Keyset type and loadJWKs implementation
cat -n server/utils/atproto/oauth.ts

Repository: npmx-dev/npmx.dev

Length of output: 7039


🌐 Web query:

@atproto/oauth-client-node v0.3.15 Keyset findPrivateKey TypeScript types

💡 Result:

In @atproto/oauth-client-node@0.3.15, the keyset you pass in is typed by the core package as:

keyset?: Keyset | Iterable<Key | undefined | null | false>

where Keyset and Key come from @atproto/jwk. (app.unpkg.com)

In current @atproto/jwk typings, Keyset is a class and it does not have a findPrivateKey method. Instead, it exposes methods like:

  • get(search: KeySearch): K
  • list(search: KeySearch): Generator<K>
  • findKey(search: KeySearch): [key: Key, alg: string]

So keyset.findPrivateKey(...) will fail type-checking because it’s not part of the Keyset API. (app.unpkg.com)

If your goal is “get the signing key for a given kid”, you’d do something like:

const key = keyset.get({ kid, use: 'sig' }) // returns a Key
// or:
const [key, alg] = keyset.findKey({ kid, use: 'sig' })

Citations:


Remove @ts-expect-error and use the correct Keyset API method.

The suppression masks a genuine type error: Keyset (from @atproto/jwk) does not provide a findPrivateKey() method. Use keyset.get({ use: 'sig' }) or keyset.findKey({ use: 'sig' }) instead to obtain the signing key with proper type safety.

🔧 Suggested fix using Keyset.get()
 export default defineEventHandler(async event => {
   const config = useRuntimeConfig(event)
   const keyset = await loadJWKs(config)
-  // `@ts-expect-error` Taken from statusphere-example-app. Throws a ts error
-  const pk = keyset?.findPrivateKey({ use: 'sig' })
+  const pk = keyset?.get({ use: 'sig' })
   return getOauthClientMetadata(pk?.alg)
 })

})
56 changes: 42 additions & 14 deletions server/utils/atproto/oauth-session-store.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,47 @@
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'

export class OAuthSessionStore implements NodeSavedSessionStore {
private readonly session: SessionManager<UserServerSession>
private readonly serverSession: SessionManager<UserServerSession>
private readonly storage = useStorage(OAUTH_CACHE_STORAGE_BASE)

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

async get(): Promise<NodeSavedSession | undefined> {
const sessionData = this.session.data
if (!sessionData) return undefined
return sessionData.oauthSession
private createStorageKey(did: string, sessionId: string) {
return `sessions:${did}:${sessionId}`
}

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,
async get(key: string): Promise<NodeSavedSession | undefined> {
const serverSessionData = this.serverSession.data
if (!serverSessionData) return undefined
if (!serverSessionData.oauthSessionId) {
console.warn('[oauth session store] No oauthSessionId found in session data')
return undefined
}

let session = await this.storage.getItem<NodeSavedSession>(
this.createStorageKey(key, serverSessionData.oauthSessionId),
)
return session ?? undefined
}

async set(key: string, val: NodeSavedSession) {
const serverSessionData = this.serverSession.data
let sessionId
if (!serverSessionData?.oauthSessionId) {
sessionId = crypto.randomUUID()
await this.serverSession.update({
oauthSessionId: sessionId,
})
} else {
sessionId = serverSessionData.oauthSessionId
}
try {
await this.storage.setItem<NodeSavedSession>(this.createStorageKey(key, sessionId), val)
} catch (error) {
// Not sure if this has been happening. But helps with debugging
console.error(
Expand All @@ -31,9 +52,16 @@ export class OAuthSessionStore implements NodeSavedSessionStore {
}
}

async del() {
await this.session.update({
oauthSession: undefined,
async del(key: string) {
const serverSessionData = this.serverSession.data
if (!serverSessionData) return undefined
if (!serverSessionData.oauthSessionId) {
console.warn('[oauth session store] No oauthSessionId found in session data')
return undefined
}
await this.storage.removeItem(this.createStorageKey(key, serverSessionData.oauthSessionId))
await this.serverSession.update({
oauthSessionId: undefined,
})
}
}
41 changes: 28 additions & 13 deletions server/utils/atproto/oauth-state-store.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,45 @@
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 '#server/utils/atproto/storage'

export class OAuthStateStore implements NodeSavedStateStore {
private readonly session: SessionManager<UserServerSession>
private readonly serverSession: SessionManager<UserServerSession>
private readonly storage = useStorage(OAUTH_CACHE_STORAGE_BASE)

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

async get(): Promise<NodeSavedState | undefined> {
const sessionData = this.session.data
if (!sessionData) return undefined
return sessionData.oauthState
private createStorageKey(did: string, sessionId: string) {
return `state:${did}:${sessionId}`
}

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 get(key: string): Promise<NodeSavedState | undefined> {
const serverSessionData = this.serverSession.data
if (!serverSessionData) return undefined
if (!serverSessionData.oauthStateId) return undefined
const state = await this.storage.getItem<NodeSavedState>(
this.createStorageKey(key, serverSessionData.oauthStateId),
)
return state ?? undefined
}

async set(key: string, val: NodeSavedState) {
let stateId = crypto.randomUUID()
await this.serverSession.update({
oauthStateId: stateId,
})
await this.storage.setItem<NodeSavedState>(this.createStorageKey(key, stateId), val)
}

async del() {
await this.session.update({
oauthState: undefined,
async del(key: string) {
const serverSessionData = this.serverSession.data
if (!serverSessionData) return undefined
if (!serverSessionData.oauthStateId) return undefined
await this.storage.removeItem(this.createStorageKey(key, serverSessionData.oauthStateId))
await this.serverSession.update({
oauthStateId: undefined,
})
}
}
Loading
Loading