Skip to content

More connected accounts#1165

Open
N2D4 wants to merge 5 commits intodevfrom
more-connected-accounts
Open

More connected accounts#1165
N2D4 wants to merge 5 commits intodevfrom
more-connected-accounts

Conversation

@N2D4
Copy link
Contributor

@N2D4 N2D4 commented Feb 6, 2026


Note

Medium Risk
Introduces new OAuth-token retrieval paths and refresh logic keyed by provider_account_id, touching authentication/authorization and token storage; changes are additive and heavily tested but failures could impact access-token issuance or expose tokens if auth checks regress.

Overview
Adds new connected-accounts capabilities that support multiple OAuth accounts per provider: a GET /connected-accounts/:user_id listing endpoint (client restricted to self; server can query any user) and a new POST /connected-accounts/:user_id/:provider_id/:provider_account_id/access-token endpoint that fetches cached access tokens or refreshes via stored refresh tokens with scope filtering.

Extends the SDK (client + server) with listConnectedAccounts, object-based getConnectedAccount({ provider, providerAccountId }), and per-account access token retrieval; the legacy provider-only APIs remain but are now explicitly deprecated and the new OAuthConnection.getAccessToken returns a Result with a new OAUTH_ACCESS_TOKEN_NOT_AVAILABLE known error instead of throwing.

Includes a demo page showcasing the new APIs and adds substantial E2E coverage for listing, per-account token retrieval, authorization rules, and edge cases; also tweaks the dev launchpad links to incorporate the STACK_PORT_PREFIX subdomain pattern.

Written by Cursor Bugbot for commit 48ace41. This will update automatically on new commits. Configure here.

Summary by CodeRabbit

  • New Features

    • APIs and SDK methods to list and manage connected OAuth accounts (client & server), plus client helpers (list/use/link/getOrLink/useOrLinkConnectedAccount).
    • Retrieve access tokens by provider + account ID with scope checks, automatic refresh, and explicit OAuth error responses.
    • Demo page with interactive UI for linking, listing, and fetching tokens.
  • Tests

    • Comprehensive E2E test suites covering API endpoints, token flows, and SDK integration.
  • Other

    • New known error: OAuth access token not available.

Copilot AI review requested due to automatic review settings February 6, 2026 03:00
@vercel
Copy link

vercel bot commented Feb 6, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
stack-backend Ready Ready Preview, Comment Feb 11, 2026 6:04am
stack-dashboard Ready Ready Preview, Comment Feb 11, 2026 6:04am
stack-demo Ready Ready Preview, Comment Feb 11, 2026 6:04am
stack-docs Ready Ready Preview, Comment Feb 11, 2026 6:04am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 6, 2026

📝 Walkthrough

Walkthrough

Adds connected-accounts feature: backend CRUD and routes for listing accounts and issuing per-account access tokens (with validation, refresh, and rotation), client/server SDK APIs and implementations with caches and object-based account lookup, demo UI, known error, and extensive E2E tests.

Changes

Cohort / File(s) Summary
Backend CRUD & Routes
apps/backend/src/app/api/latest/connected-accounts/[user_id]/[provider_id]/[provider_account_id]/access-token/crud.tsx, apps/backend/src/app/api/latest/connected-accounts/[user_id]/[provider_id]/[provider_account_id]/access-token/route.tsx, apps/backend/src/app/api/latest/connected-accounts/[user_id]/crud.tsx, apps/backend/src/app/api/latest/connected-accounts/[user_id]/route.tsx
New lazy CRUD handlers and route bindings: list connected accounts and POST to create/retrieve per-account access tokens. Enforces client/server access, resolves provider config, validates scopes/tokens, attempts refreshes, rotates refresh tokens, and surfaces OAuth/provider errors.
SDK Client Interface
packages/stack-shared/src/interface/client-interface.ts, packages/stack-shared/src/interface/crud/connected-accounts.ts
Added createProviderAccessTokenByAccount and listConnectedAccounts client methods and connected-account CRUD schemas/types (including access-token create/read shapes).
SDK Server Interface
packages/stack-shared/src/interface/server-interface.ts
Added createServerProviderAccessTokenByAccount and listServerConnectedAccounts server methods for per-user, per-account token operations.
Template: Client App Impl
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts, packages/template/src/lib/stack-app/connected-accounts/index.ts
Introduced per-session caches, mapping helper to convert CRUD items into OAuthConnection (with providerAccountId), new public APIs: listConnectedAccounts/useConnectedAccounts/link/getOrLink/useOrLink and updated getConnectedAccount/useConnectedAccount overloads (legacy + object-based).
Template: Server App Impl & Users
packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts, packages/template/src/lib/stack-app/users/index.ts
Server-side caches and mapper, object-based account lookups, expanded ServerUser API: listConnectedAccounts/useConnectedAccounts/link/getOrLink/useOrLink, and updated overloads for get/useConnectedAccount to support account objects.
Known Errors
packages/stack-shared/src/known-errors.tsx
Added new KnownErrors entry OAUTH_ACCESS_TOKEN_NOT_AVAILABLE with constructor and 400 payload shape.
Demo & Launchpad
examples/demo/src/app/connected-accounts/page.tsx, apps/dev-launchpad/public/index.html
Added demo ConnectedAccounts page and components for listing/linking/retrieving/token fetching; launchpad link logic updated to use conditional subdomain prefix based on port prefix.
E2E Tests
apps/e2e/tests/backend/endpoints/api/v1/connected-accounts.test.ts, apps/e2e/tests/js/connected-accounts.test.ts
Large new E2E test suites covering listing, retrieval, per-account token creation/refresh flows, authorization edge cases, scope validation, legacy behaviors, Unicode/provider-account edge cases, and token verification against a mock OAuth server.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client
  participant ServerAPI
  participant BackendHandler
  participant TenancyDB
  participant OAuthProvider
  Client->>ServerAPI: POST /connected-accounts/me/{provider}/{account}/access-token (scope)
  ServerAPI->>BackendHandler: forward request with session & tenancy
  BackendHandler->>TenancyDB: validate user, find connected account & access tokens
  alt valid access token present
    TenancyDB-->>BackendHandler: return valid access token
    BackendHandler-->>ServerAPI: return access token
    ServerAPI-->>Client: 200 { accessToken }
  else no valid access token
    BackendHandler->>TenancyDB: fetch refresh tokens for account
    BackendHandler->>OAuthProvider: attempt refresh using refresh token
    alt refresh success
      OAuthProvider-->>BackendHandler: new access token (+ optional refresh token)
      BackendHandler->>TenancyDB: create access token record, rotate refresh token if needed
      BackendHandler-->>ServerAPI: return access token
      ServerAPI-->>Client: 200 { accessToken }
    else refresh failed
      OAuthProvider-->>BackendHandler: error
      BackendHandler->>TenancyDB: mark token invalid
      BackendHandler-->>ServerAPI: return OAuth error
      ServerAPI-->>Client: error response
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I nibbled through tokens, hopped past a bug,
Cached accounts, spun refreshes with a flick of a hug,
Providers aligned and scopes checked just right,
Rotations complete in the soft evening light,
A little rabbit cheers: "Connected — delight!"

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title "More connected accounts" is vague and generic, using non-descriptive language that doesn't convey the specific technical changes. Use a more specific title that describes the main change, such as "Add multi-account OAuth support with per-account token retrieval" or "Implement connected-accounts endpoints and SDK APIs".
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The PR description includes a comprehensive overview with objectives, risk assessment, and implementation details. The author provided detailed context about the changes including backend API updates, SDK changes, and test coverage.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch more-connected-accounts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 6, 2026

Greptile Overview

Greptile Summary

This PR expands the “connected accounts” feature to support multiple accounts per OAuth provider.

Key changes:

  • Adds new /api/latest/connected-accounts/:user_id endpoint to list connected OAuth accounts (filtered by allowConnectedAccounts).
  • Adds new /api/latest/connected-accounts/:user_id/:provider_id/:provider_account_id/access-token endpoint to mint/refresh an access token for a specific provider account.
  • Extends shared client/server interfaces and the template SDK to expose listConnectedAccounts() / useConnectedAccounts() plus an object-based getConnectedAccount({ provider, providerAccountId }) for disambiguation.
  • Adds extensive E2E + JS SDK test coverage and a demo page.

Main required fixes are around route-handler conventions (SmartRouteHandler requirement) and demo async handler pattern (runAsynchronouslyWithAlert rule).

Confidence Score: 3/5

  • This PR is close to mergeable but needs a couple of repo-policy fixes before landing.
  • Core connected-accounts functionality and tests look coherent, but two new API route files bypass the required SmartRouteHandler pattern and the demo page violates the async handler utility rule; these should be addressed to align with repo conventions.
  • apps/backend/src/app/api/latest/connected-accounts/[user_id]/route.tsx; apps/backend/src/app/api/latest/connected-accounts/[user_id]/[provider_id]/[provider_account_id]/access-token/route.tsx; examples/demo/src/app/connected-accounts/page.tsx

Important Files Changed

Filename Overview
apps/backend/src/app/api/latest/connected-accounts/[user_id]/[provider_id]/[provider_account_id]/access-token/crud.tsx Adds CRUD handler to mint/check OAuth access tokens scoped to a specific provider_account_id; main concern is route wrapper convention handled elsewhere.
apps/backend/src/app/api/latest/connected-accounts/[user_id]/[provider_id]/[provider_account_id]/access-token/route.tsx Adds POST route exporting handler directly; needs SmartRouteHandler wrapper per repo policy.
apps/backend/src/app/api/latest/connected-accounts/[user_id]/crud.tsx Adds connected accounts list handler filtering allowConnectedAccounts and enforcing client self-access; no functional issues found.
apps/backend/src/app/api/latest/connected-accounts/[user_id]/route.tsx Adds GET route exporting handler directly; needs SmartRouteHandler wrapper per repo policy.
apps/dev-launchpad/public/index.html Updates launchpad links to include port-prefix subdomain; change is straightforward.
apps/e2e/tests/backend/endpoints/api/v1/connected-accounts.test.ts Extends backend E2E coverage for listing connected accounts and fetching access tokens by provider_account_id; no test logic issues found.
apps/e2e/tests/js/connected-accounts.test.ts Adds comprehensive JS SDK tests for listing/getting connected accounts including multiple accounts per provider and special characters; no issues found.
examples/demo/src/app/connected-accounts/page.tsx Adds demo UI for new connected-accounts APIs; async handlers should use runAsynchronouslyWithAlert per repo rule.
packages/stack-shared/src/interface/client-interface.ts Adds client interface methods for listing connected accounts and fetching access token by provider_account_id; looks consistent with existing request patterns.
packages/stack-shared/src/interface/crud/connected-accounts.ts Introduces connectedAccountCrud schemas and docs alongside existing access-token CRUD; schemas look consistent with shared schema-fields.
packages/stack-shared/src/interface/server-interface.ts Adds server interface methods for listing connected accounts and fetching access tokens by provider_account_id; consistent with existing server request style.
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts Extends client app with connected accounts list cache and object-based getConnectedAccount; no functional issues found in reviewed sections.
packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts Extends server app user API with list/get connected accounts by provider_account_id and access token retrieval; looks consistent.
packages/template/src/lib/stack-app/connected-accounts/index.ts Expands OAuthConnection type to include provider/providerAccountId while keeping deprecated id; simple type-only change.
packages/template/src/lib/stack-app/users/index.ts Adds new connected accounts APIs to UserExtra typings and deprecates provider-only redirect overloads; type change only.

Sequence Diagram

sequenceDiagram
  participant Client as SDK Client (User)
  participant ClientIF as StackClientInterface
  participant API as Backend /api/latest
  participant ConnList as GET /connected-accounts/:user_id
  participant TokenEP as POST /connected-accounts/:user_id/:provider_id/:provider_account_id/access-token
  participant Prisma as Prisma DB
  participant Provider as OAuth Provider

  Client->>ClientIF: listConnectedAccounts(session)
  ClientIF->>API: GET /connected-accounts/me
  API->>ConnList: connectedAccountCrudHandlers.listHandler
  ConnList->>Prisma: findMany(projectUserOAuthAccount)
  Prisma-->>ConnList: oauth accounts (allowConnectedAccounts=true)
  ConnList-->>ClientIF: { items: [{provider, provider_account_id, user_id}], is_paginated:false }
  ClientIF-->>Client: OAuthConnection[]

  Client->>ClientIF: createProviderAccessTokenByAccount(providerId, providerAccountId, scope)
  ClientIF->>API: POST /connected-accounts/me/{providerId}/{providerAccountId}/access-token
  API->>TokenEP: connectedAccountAccessTokenByAccountCrudHandlers.createHandler
  TokenEP->>Prisma: findMany(oAuthAccessToken)
  alt Existing valid access token
    TokenEP->>Provider: checkAccessTokenValidity(accessToken)
    Provider-->>TokenEP: valid
    TokenEP-->>ClientIF: { access_token }
  else Need refresh
    TokenEP->>Prisma: findMany(oAuthToken)
    TokenEP->>Provider: getAccessToken(refreshToken, scope)
    Provider-->>TokenEP: TokenSet(accessToken, expiresAt, refreshToken?)
    TokenEP->>Prisma: create(oAuthAccessToken)
    opt Refresh token rotated
      TokenEP->>Prisma: update(old oAuthToken invalid)
      TokenEP->>Prisma: create(new oAuthToken)
    end
    TokenEP-->>ClientIF: { access_token }
  end
  ClientIF-->>Client: { access_token }

Loading

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds support for connecting multiple OAuth accounts from the same provider to a single user, addressing a limitation where users could only link one account per OAuth provider (e.g., one Google account, one GitHub account).

Changes:

  • Adds new API methods (listConnectedAccounts(), linkConnectedAccount(), getOrLinkConnectedAccount()) to manage multiple connected accounts
  • Extends getConnectedAccount() to accept { provider, providerAccountId } for precise account lookup alongside the legacy provider-only lookup
  • Updates the Connection type with provider and providerAccountId fields, deprecating the id field for backward compatibility
  • Implements backend endpoints for listing connected accounts and retrieving access tokens by specific account IDs
  • Adds comprehensive E2E tests and a demo page

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
packages/template/src/lib/stack-app/users/index.ts Adds type definitions for new connected account methods with deprecation markers for legacy APIs
packages/template/src/lib/stack-app/connected-accounts/index.ts Extends Connection type with provider and providerAccountId fields, deprecates id field
packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts Implements server-side connected account methods with caching and overloaded signatures
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts Implements client-side connected account methods including OAuth flow initiation
packages/stack-shared/src/interface/server-interface.ts Adds server interface methods for listing accounts and getting access tokens by account ID
packages/stack-shared/src/interface/client-interface.ts Adds client interface methods for listing accounts and getting access tokens by account ID
packages/stack-shared/src/interface/crud/connected-accounts.ts Defines CRUD schemas for connected accounts list and read operations
apps/backend/src/app/api/latest/connected-accounts/[user_id]/route.tsx Implements GET endpoint for listing user's connected accounts
apps/backend/src/app/api/latest/connected-accounts/[user_id]/crud.tsx Implements connected accounts list handler with proper authorization checks
apps/backend/src/app/api/latest/connected-accounts/[user_id]/[provider_id]/[provider_account_id]/access-token/route.tsx Implements POST endpoint for getting access tokens by specific account
apps/backend/src/app/api/latest/connected-accounts/[user_id]/[provider_id]/[provider_account_id]/access-token/crud.tsx Implements access token retrieval with refresh token handling
examples/demo/src/app/connected-accounts/page.tsx Adds comprehensive demo page showcasing all new connected account APIs
apps/e2e/tests/js/connected-accounts.test.ts Adds 40+ E2E tests covering various scenarios including multiple accounts from same provider
apps/e2e/tests/backend/endpoints/api/v1/connected-accounts.test.ts Adds 20+ backend endpoint tests for connected accounts API
apps/dev-launchpad/public/index.html Unrelated change attempting to add subdomain routing (contains syntax error)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

15 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🤖 Fix all issues with AI agents
In
`@apps/backend/src/app/api/latest/connected-accounts/`[user_id]/[provider_id]/[provider_account_id]/access-token/crud.tsx:
- Around line 155-179: The refresh-token rotation is non-atomic: the sequence
around prisma.oAuthAccessToken.create, prisma.oAuthToken.update and
prisma.oAuthToken.create can race under concurrent getAccessToken calls; wrap
the rotation (invalidate old refresh token and insert new refresh token and
access token) in a single database transaction (using prisma.$transaction) or
use an optimistic lock (e.g., check and update a version/isValid predicate in
the WHERE clause) when calling prisma.oAuthToken.update so only one caller can
mark the old token invalid and create the new token; ensure the transaction
includes the access token insert and the refresh-token update/create to
guarantee atomicity and avoid duplicate refresh tokens.

In `@apps/dev-launchpad/public/index.html`:
- Line 368: The href's nested template mistakenly includes a literal "$p" prefix
in the subdomain construction (the expression `$p${stackPortPrefix}.`) which
results in invalid hostnames; update the href template so the conditional uses
either an empty string or the plain port-prefix subdomain without the stray "$"
(i.e., replace `$p${stackPortPrefix}.` with `${stackPortPrefix}.` or otherwise
remove the leading "$"), leaving the rest of the expression
(withPrefix(app.portSuffix), app.path, and the importance-based class logic)
unchanged.

In `@apps/e2e/tests/backend/endpoints/api/v1/connected-accounts.test.ts`:
- Around line 898-905: The test titled "should return 404 when trying to list
connected accounts for non-existent user via server access" is misleading
because it asserts response.status is 400 and response.body.code is
"SCHEMA_ERROR"; change the test description string to reflect the actual
behavior (e.g., "should return 400 when trying to list connected accounts for
non-existent user via server access") so the test title matches the assertions
in the it(...) block that calls niceBackendFetch and checks response.status and
response.body.code.
- Around line 877-896: The test title is incorrect: update the it(...)
description in the test block where the request to
"/api/v1/connected-accounts/me" is made (the test starting with "should return
403 when unauthenticated user tries to list connected accounts") to reflect the
actual behavior/status (400 and CANNOT_GET_OWN_USER_WITHOUT_USER) — e.g., change
the string to "should return 400 when unauthenticated user tries to list
connected accounts" so the test title matches the assertion and response.

In `@examples/demo/src/app/connected-accounts/page.tsx`:
- Around line 47-49: The displayed access token always gets "..." appended even
when its length is ≤ 50; update the rendering logic around accessToken (the
variable used in the JSX pre block) to conditionally append the ellipsis only
when accessToken.length > 50 (e.g., show accessToken.slice(0,50) + "..." when
longer, otherwise show the full accessToken without the suffix) so short tokens
are not misleadingly truncated.

In `@packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts`:
- Around line 1424-1443: getConnectedAccount currently computes scopeString but
ignores it in the object-based branch; either remove scopes from that overload
or forward scopeString when building/returning the OAuthConnection. Locate the
object-branch in getConnectedAccount and when you resolve the connected account
(where the code currently returns found), call the same token-fetch/creation
path used elsewhere but pass options?.scopes (or scopeString) into the
token/cache call (e.g., into the token-fetch cache or
_createOAuthConnectionFromCrudItem invocation) so the requested scopes are
honored; alternatively, remove the scopes option from the object-based overload
signature at the top to prevent callers from passing scopes that will be
ignored. Ensure the unique symbols getConnectedAccount and scopeString are
updated accordingly.
- Around line 1526-1534: The bare catch around existing.getAccessToken()
swallows all errors; change it to catch (err) and only handle the expected
token-invalid/revoked error (e.g., check err.name/err.message for the OAuth
"invalid_grant"/"invalid_token"/"refresh token" signatures or a specific
AssertionError), allowing other errors (network/timeouts, server errors) to be
rethrown or logged and propagated; implement a small predicate like
isTokenInvalidError(err) and use it inside the catch to decide whether to fall
through to the link flow or rethrow, and ensure you reference the existing
variable and its getAccessToken method when applying this change.
- Around line 1541-1546: In useOrLinkConnectedAccount, remove the unsafe type
cast and non-null assertion: avoid casting provider with "as ProviderType" and
replace the "result!" assertion with a defensive null-check (e.g., return result
?? throwErr("useOrLinkConnectedAccount: cache returned null") ). If you cannot
immediately reconcile string vs ProviderType for
app._currentUserOAuthConnectionCache keys, add a TODO comment explaining the
mismatch and leave a safe conversion or runtime check instead of using "as";
ensure all references to ProviderType and app._currentUserOAuthConnectionCache
are updated accordingly.
- Around line 1454-1477: The hook useConnectedAccount currently calls
useAsyncCache conditionally which violates Rules of Hooks; modify
useConnectedAccount to invoke both useAsyncCache hooks unconditionally — one
against app._currentUserConnectedAccountsCache (with [session] as args) and one
against app._currentUserOAuthConnectionCache (with [session, idOrAccount,
scopeString, options?.or === 'redirect'] or appropriate dummy args when
idOrAccount is an object) — then pick and return the correct result
(object-based match from connectedAccounts if idOrAccount is the object shape,
otherwise the provider-based OAuthConnection); alternatively you can split into
two hooks like useConnectedAccountByProvider and useConnectedAccountByObject to
avoid dummy fetches.

In `@packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts`:
- Around line 442-470: The object-overload `scopes` option is ignored because
_createServerOAuthConnectionFromCrudItem hardcodes the cache key scope element
to "" in getAccessToken and useAccessToken; update
_createServerOAuthConnectionFromCrudItem to accept/receive the requested scopes
(e.g. add a scopes parameter or accept it via the calling path) and include
those scopes when calling
app._serverUserOAuthConnectionAccessTokensByAccountCache.getOrWait and
useAsyncCache (replace the "" element with a deterministic representation of the
scopes array, e.g. joined string or stable key), so getAccessToken and
useAccessToken actually respect the `{ scopes?: string[] }` passed to
getConnectedAccount/getOrLinkConnectedAccount; alternatively, if you prefer to
keep the current cache shape, remove the `{ scopes?: string[] }` option from the
object-based getConnectedAccount/useConnectedAccount overloads to avoid the
mismatch.
- Around line 508-531: The current useConnectedAccount implementation
conditionally calls useAsyncCache in two branches which violates React hooks
ordering; refactor so both useAsyncCache hooks are invoked unconditionally and
then select the appropriate result based on idOrAccount's shape: always compute
scopeString, call useAsyncCache(app._serverUserConnectedAccountsCache, [crud.id]
as const, "user.useConnectedAccount()") to get connectedAccounts and also call
useAsyncCache(app._serverUserOAuthConnectionCache, [crud.id, idOrAccount,
scopeString, options?.or === 'redirect'] as const, "user.useConnectedAccount()")
to get oauthConnection, then if idOrAccount is the object shape (has provider
and providerAccountId) filter connectedAccounts for the matching
provider/providerAccountId and return that (or null), otherwise return the
oauthConnection result; keep all original identifiers (useConnectedAccount,
useAsyncCache, app._serverUserConnectedAccountsCache,
app._serverUserOAuthConnectionCache, crud.id, scopeString, options?.or) so the
hooks are always called in the same order.
🧹 Nitpick comments (8)
examples/demo/src/app/connected-accounts/page.tsx (2)

12-22: Guideline violation: try-catch-all with catch (e: any).

This pattern is repeated in four places (lines 18, 115, 138, 210). The coding guidelines prohibit both try-catch-all and unannoted any. Consider using runAsynchronouslyWithAlert or at least narrowing the caught type (e.g., catch (e: unknown) with an instanceof check).

Proposed fix (example for this occurrence)
- const fetchAccessToken = async () => {
-   setLoading(true);
-   setError(null);
-   try {
-     const { accessToken } = await account.getAccessToken();
-     setAccessToken(accessToken);
-   } catch (e: any) {
-     setError(e.message || "Failed to get access token");
-   } finally {
-     setLoading(false);
-   }
- };
+ const fetchAccessToken = async () => {
+   setLoading(true);
+   setError(null);
+   try {
+     const { accessToken } = await account.getAccessToken();
+     setAccessToken(accessToken);
+   } catch (e: unknown) {
+     setError(e instanceof Error ? e.message : "Failed to get access token");
+   } finally {
+     setLoading(false);
+   }
+ };

As per coding guidelines, "Never try-catch-all, never void a promise, and never use .catch(console.error) or similar; use runAsynchronously or runAsynchronouslyWithAlert instead for error handling" and "Try to avoid the any type; whenever you need to use any, leave a comment explaining why you're using it".


105-105: Avoid as any type cast to bypass the type system.

provider as any silences the type checker instead of properly handling the mismatch between the free-form string state and the expected provider union type. Consider typing provider as the expected union or validating/narrowing before the call.

As per coding guidelines, "Do not use as/any/type casts or anything else to bypass the type system unless you specifically asked the user about it".

apps/e2e/tests/js/connected-accounts.test.ts (1)

391-394: Non-null assertions in test code — consider ?? throwErr().

Lines 394 and 442 use serverUser!.listConnectedAccounts(). While the preceding expect(serverUser).not.toBeNull() makes the intent clear, the coding guideline prefers ?? throwErr(...) over non-null assertions. In test code this is low-impact, but for consistency:

🔧 Suggested change
-      const serverUser = await apps.serverApp.getUser(user.id);
-      expect(serverUser).not.toBeNull();
-
-      const connectedAccounts = await serverUser!.listConnectedAccounts();
+      const serverUser = await apps.serverApp.getUser(user.id) ?? (() => { throw new Error("Expected serverUser to be defined"); })();
+
+      const connectedAccounts = await serverUser.listConnectedAccounts();

As per coding guidelines, "Code defensively; prefer ?? throwErr(...) over non-null assertions with good error messages that explicitly state the assumption".

apps/e2e/tests/backend/endpoints/api/v1/connected-accounts.test.ts (1)

579-579: any type used without explanatory comment.

Multiple occurrences of (i: any) in .map()/.find() calls (lines 579, 613, 864, 869, 870) lack the required comment. If the response body is untyped, a single comment at the first usage would suffice.

As per coding guidelines, "whenever you need to use any, leave a comment explaining why you're using it".

apps/backend/src/app/api/latest/connected-accounts/[user_id]/[provider_id]/[provider_account_id]/access-token/crud.tsx (3)

38-46: Redundant account existence check — consider removing the first one.

The user's oauth_providers are checked at line 41 (without allowConnectedAccounts filter), and the same account is looked up again via Prisma at line 54 (with the filter). If the first check passes but allowConnectedAccounts is false, the error from line 65 is OAuthConnectionNotConnectedToUser, which is misleading — the connection exists but isn't enabled for connected accounts.

The first check could be removed entirely since the Prisma query (lines 54-62) is more authoritative and covers both existence and the allowConnectedAccounts constraint. This would also save an adminRead call.


85-99: Sequential provider-side validity checks could be slow.

Each cached access token is checked against the provider one-by-one (line 87). If a user has multiple invalidated tokens, this results in sequential HTTP calls to the provider. For most real-world cases there will be 0–1 tokens, so this is acceptable, but worth noting if performance becomes a concern with many stale tokens.


16-190: Extract shared token retrieval and refresh logic into a common utility.

This handler duplicates nearly all of the token retrieval and refresh logic from the legacy handler (apps/backend/src/app/api/latest/connected-accounts/[user_id]/[provider_id]/access-token/crud.tsx). The two handlers share:

  • Provider validation and initialization
  • Access token validity checking and filtering
  • Refresh token retrieval and processing
  • Error handling and logging during token refresh

The main difference is how they query for tokens: the legacy handler uses nested relational queries with projectUserOAuthAccount, while this handler pre-fetches the oauthAccount and uses direct oauthAccountId lookups. Consider extracting the core token refresh logic into a shared utility that accepts the OAuth account as a parameter, then update both handlers to use it. This will ensure consistency and reduce maintenance burden.

packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (1)

391-394: Prefer ?? throwErr(...) over non-null assertion and silent fallback.

user! on line 393 and ?? "" on line 394 are both fragile. Although the preceding hasConnection guard guarantees both values exist, the coding guidelines prefer explicit assertion errors over ! and silent fallbacks.

🛡️ Proposed defensive fix
-    const matchingProvider = user!.oauth_providers.find((p) => p.id === options.providerId);
-    const providerAccountId = matchingProvider?.account_id ?? "";
+    const nonNullUser = user ?? throwErr("Expected user to be non-null when hasConnection is true");
+    const matchingProvider = nonNullUser.oauth_providers.find((p) => p.id === options.providerId)
+      ?? throwErr(`Expected to find oauth provider ${options.providerId} on user`);
+    const providerAccountId = matchingProvider.account_id ?? throwErr(`Expected account_id on oauth provider ${options.providerId}`);

As per coding guidelines, "Code defensively; prefer ?? throwErr(...) over non-null assertions with good error messages that explicitly state the assumption."

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

`Failed to retrieve an OAuth access token for the connected account (provider: ${provider}). ${details}`,
] as const,
(json: any) => [json.provider, json.details] as [string, string],
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing details object in KnownError constructor breaks deserialization

Medium Severity

The OAuthAccessTokenNotAvailable error constructor's create function returns [400, message] without a details object as the third element. The constructorArgsFromJson function expects json.provider and json.details from the details object, but since no details are serialized, json will be undefined when KnownError.fromJson is called, causing a TypeError. Every other parameterized error constructor in this file (e.g., SchemaError, InvalidPublishableClientKey, TeamInvitationRestrictedUserNotAllowed) includes a { provider, details } style object as the third tuple element.

Fix in Cursor Fix in Web


throw new KnownErrors.OAuthConnectionDoesNotHaveRequiredScope();
},
}));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Near-identical endpoint files create maintenance risk

Low Severity

The new connectedAccountAccessTokenByAccountCrudHandlers file is ~190 lines that nearly duplicate the existing connectedAccountAccessTokenCrudHandlers file (~176 lines). The core logic for finding valid access tokens, checking validity, refreshing tokens, and storing new tokens is copy-pasted. The only differences are the Prisma query filter (direct oauthAccountId vs projectUserOAuthAccount relation) and the allowConnectedAccounts constraint. Extracting the shared token retrieval/refresh logic into a common helper would reduce the risk of one endpoint being updated while the other is missed.

Fix in Cursor Fix in Web

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@apps/e2e/tests/backend/endpoints/api/v1/connected-accounts.test.ts`:
- Around line 503-507: The test title string currently reads "should return 404
when trying to get access token for non-existent provider_account_id" but the
assertion/snapshot expects a 400 with OAUTH_CONNECTION_NOT_CONNECTED_TO_USER;
update the test description in the it(...) call to reflect the actual expected
status and error (for example: "should return 400 when getting access token for
non-existent provider_account_id (OAUTH_CONNECTION_NOT_CONNECTED_TO_USER)") so
the title matches the assertion in the test handling code.

In `@examples/demo/src/app/connected-accounts/page.tsx`:
- Around line 101-214: Replace the broad try/catch + any casts in
handleGetByProvider, handleGetByBothIds and handleLink with
runAsynchronouslyWithAlert: validate that provider is a non-empty string (reject
or show alert if not) instead of using provider as any, remove the `: any` error
casts, and wrap the async calls (user.getConnectedAccount(...) and
user.linkConnectedAccount(...)) inside runAsynchronouslyWithAlert to surface
errors; ensure providerAccountId handling remains the same for getByBothIds and
that scopes are parsed the same in handleLink before passing to
user.linkConnectedAccount.

In `@packages/stack-shared/src/known-errors.tsx`:
- Around line 1130-1138: The OAuthAccessTokenNotAvailable constructor currently
doesn't persist the details field so deserialization (constructorArgsFromJson)
loses provider/details and can throw; update the createKnownErrorConstructor
call for OAuthAccessTokenNotAvailable so the constructor accepts and stores a
details string (include it in the error payload/state returned by the factory)
and make the JSON-to-args function resilient by guarding missing
json.provider/json.details (e.g., default to empty string) so round-tripping via
constructorArgsFromJson works without throwing.

In `@packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts`:
- Around line 394-397: Replace the non-null assertion on user in the block that
finds the OAuth provider: instead of user!.oauth_providers, assert user exists
with a defensive check (e.g., user ?? throwErr("Expected user to be present when
hasConnection is true in client-app-impl")) and likewise ensure the found
matchingProvider is present — do not default providerAccountId to an empty
string; if matchingProvider is missing throw an explicit error (e.g., throwErr
or new Error) that includes options.providerId and the context (function/class:
client-app-impl handling connections) so broken assumptions are surfaced
clearly.

Comment on lines +503 to +507
it("should return 404 when trying to get access token for non-existent provider_account_id", async ({ expect }) => {
await Auth.OAuth.signIn();

const response = await niceBackendFetch("/api/v1/connected-accounts/me/spotify/non-existent-account-id/access-token", {
accessType: "client",
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

Test title says 404 but assertion expects 400

The snapshot shows a 400 with OAUTH_CONNECTION_NOT_CONNECTED_TO_USER. Please update the description.

✏️ Suggested fix
-it("should return 404 when trying to get access token for non-existent provider_account_id", async ({ expect }) => {
+it("should return 400 when trying to get access token for non-existent provider_account_id", async ({ expect }) => {
🤖 Prompt for AI Agents
In `@apps/e2e/tests/backend/endpoints/api/v1/connected-accounts.test.ts` around
lines 503 - 507, The test title string currently reads "should return 404 when
trying to get access token for non-existent provider_account_id" but the
assertion/snapshot expects a 400 with OAUTH_CONNECTION_NOT_CONNECTED_TO_USER;
update the test description in the it(...) call to reflect the actual expected
status and error (for example: "should return 400 when getting access token for
non-existent provider_account_id (OAUTH_CONNECTION_NOT_CONNECTED_TO_USER)") so
the title matches the assertion in the test handling code.

Comment on lines +101 to +214
const handleGetByProvider = async () => {
setLoading(true);
try {
const account = await user.getConnectedAccount(provider as any);
if (account) {
setResult(JSON.stringify({
provider: account.provider,
providerAccountId: account.providerAccountId,
id: account.id,
}, null, 2));
} else {
setResult("null (no account found)");
}
} catch (e: any) {
setResult(`Error: ${e.message}`);
} finally {
setLoading(false);
}
};

const handleGetByBothIds = async () => {
setLoading(true);
try {
const account = await user.getConnectedAccount({
provider,
providerAccountId,
});
if (account) {
setResult(JSON.stringify({
provider: account.provider,
providerAccountId: account.providerAccountId,
id: account.id,
}, null, 2));
} else {
setResult("null (no account found)");
}
} catch (e: any) {
setResult(`Error: ${e.message}`);
} finally {
setLoading(false);
}
};

return (
<Card>
<CardHeader>
<CardTitle>Get Connected Account</CardTitle>
<CardDescription>
Look up a specific connected account by provider or by both provider and account ID
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium">Provider</label>
<input
type="text"
value={provider}
onChange={(e) => setProvider(e.target.value)}
className="w-full px-3 py-2 border rounded-md dark:bg-gray-800 dark:border-gray-700"
placeholder="e.g., spotify, google, github"
/>
</div>

<div className="space-y-2">
<label className="text-sm font-medium">Provider Account ID (optional)</label>
<input
type="text"
value={providerAccountId}
onChange={(e) => setProviderAccountId(e.target.value)}
className="w-full px-3 py-2 border rounded-md dark:bg-gray-800 dark:border-gray-700"
placeholder="e.g., user123, email@example.com"
/>
</div>

<div className="flex gap-2">
<Button onClick={handleGetByProvider} loading={loading} variant="secondary">
Get by Provider Only
</Button>
<Button onClick={handleGetByBothIds} loading={loading} disabled={!providerAccountId}>
Get by Provider + Account ID
</Button>
</div>

{result && (
<div className="mt-4">
<Typography variant="secondary" type="label">Result:</Typography>
<pre className="text-xs bg-gray-100 dark:bg-gray-800 p-3 rounded overflow-x-auto mt-1">
{result}
</pre>
</div>
)}
</CardContent>
</Card>
);
}

function LinkNewAccountDemo() {
const user = useUser({ or: 'redirect' });
const [provider, setProvider] = useState("spotify");
const [scopes, setScopes] = useState("");
const [loading, setLoading] = useState(false);

const handleLink = async () => {
setLoading(true);
try {
await user.linkConnectedAccount(provider, {
scopes: scopes ? scopes.split(",").map(s => s.trim()) : undefined,
});
} catch (e: any) {
alert(`Error: ${e.message}`);
} finally {
setLoading(false);
}
};
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:

# Check if the file exists and examine the relevant lines
fd -t f "connected-accounts/page.tsx" | head -5

Repository: stack-auth/stack-auth

Length of output: 47


🏁 Script executed:

# Check the actual content around the specified lines
if [ -f "examples/demo/src/app/connected-accounts/page.tsx" ]; then
  cat -n "examples/demo/src/app/connected-accounts/page.tsx" | sed -n '95,220p'
fi

Repository: stack-auth/stack-auth

Length of output: 4847


🏁 Script executed:

# Search for runAsynchronouslyWithAlert in the codebase to verify the correct import path
rg "runAsynchronouslyWithAlert" --type ts --type tsx -B 2 -A 2 | head -40

Repository: stack-auth/stack-auth

Length of output: 92


🏁 Script executed:

# Check for Button component usage with async handlers
rg "Button.*onClick.*async|onClick=.*async" --type tsx -B 1 -A 1 | head -30

Repository: stack-auth/stack-auth

Length of output: 92


🏁 Script executed:

# Look for provider type definitions in the codebase
rg "ProviderType|type.*Provider" --type ts --type tsx packages/stack-shared | grep -v node_modules | head -20

Repository: stack-auth/stack-auth

Length of output: 92


🏁 Script executed:

# Search for runAsynchronouslyWithAlert without file type restriction
rg "runAsynchronouslyWithAlert" -B 2 -A 2 | head -50

Repository: stack-auth/stack-auth

Length of output: 4178


🏁 Script executed:

# Check the stack-shared package structure for promises utilities
find packages/stack-shared -name "*promises*" -o -name "*utils*" | grep -E "\.(ts|tsx|js|jsx)$" | head -20

Repository: stack-auth/stack-auth

Length of output: 109


🏁 Script executed:

# Look for provider type definitions
rg "export.*type.*Provider" packages/stack-shared | head -15

Repository: stack-auth/stack-auth

Length of output: 768


🏁 Script executed:

# Check Button component signature for onClick handler support
rg "interface.*Button.*Props|type.*Button.*Props" packages/stack-ui -A 10 | head -30

Repository: stack-auth/stack-auth

Length of output: 2509


🏁 Script executed:

# Look at an example of runAsynchronouslyWithAlert usage in the codebase
rg -l "runAsynchronouslyWithAlert" | head -3 | xargs -I {} sh -c 'echo "=== {} ===" && head -50 {}'

Repository: stack-auth/stack-auth

Length of output: 4620


🏁 Script executed:

# Check the runAsynchronously function signature to understand the return type
sed -n '1,100p' packages/stack-shared/src/utils/promises.tsx | head -60

Repository: stack-auth/stack-auth

Length of output: 2117


🏁 Script executed:

# Look for existing provider type usage/validation patterns in connected-accounts or similar files
rg "allProviders|standardProviders" packages/stack-shared/src/utils/oauth.tsx -B 2 -A 5 | head -30

Repository: stack-auth/stack-auth

Length of output: 627


🏁 Script executed:

# Check the exact import paths that should be used
cat packages/stack-shared/src/utils/oauth.tsx | head -30

Repository: stack-auth/stack-auth

Length of output: 627


🏁 Script executed:

# Verify if allProviders/standardProviders can be imported from the dist version
fd -type f "oauth" packages/stack-shared/dist | head -5

Repository: stack-auth/stack-auth

Length of output: 235


🏁 Script executed:

# Check how provider validation is done in other parts of the codebase
rg "isProviderType|allProviders|standardProviders" examples/ -B 2 -A 2 | head -40

Repository: stack-auth/stack-auth

Length of output: 47


🏁 Script executed:

# Check how runAsynchronously and runAsynchronouslyWithAlert handle the promise/function signature
sed -n '50,150p' packages/stack-shared/src/utils/promises.tsx

Repository: stack-auth/stack-auth

Length of output: 3822


🏁 Script executed:

# Look at the actual implementation of runAsynchronously to understand its parameters
rg "export function runAsynchronously" packages/stack-shared/src/utils/promises.tsx -A 15

Repository: stack-auth/stack-auth

Length of output: 1300


Replace try-catch-all + any cast in async handlers

These handlers use try-catch with any type casts and provider as any. Switch to runAsynchronouslyWithAlert for proper error handling and validate provider type instead of casting. Apply the same pattern to handleGetByBothIds and handleLink.

🔧 Suggested fix
-import { OAuthConnection, useUser } from "@stackframe/stack";
+import { OAuthConnection, useUser } from "@stackframe/stack";
+import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";
+import { allProviders } from "@stackframe/stack-shared/dist/utils/oauth";
 import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle, Typography } from "@stackframe/stack-ui";
 import { useState } from "react";
 
+const isProviderType = (value: string): value is typeof allProviders[number] =>
+  allProviders.includes(value as typeof allProviders[number]);
+
 function GetConnectedAccountDemo() {
   const user = useUser({ or: 'redirect' });
   const [provider, setProvider] = useState("spotify");
   const [providerAccountId, setProviderAccountId] = useState("");
   const [result, setResult] = useState<string | null>(null);
   const [loading, setLoading] = useState(false);
 
-  const handleGetByProvider = async () => {
-    setLoading(true);
-    try {
-      const account = await user.getConnectedAccount(provider as any);
-      if (account) {
-        setResult(JSON.stringify({
-          provider: account.provider,
-          providerAccountId: account.providerAccountId,
-          id: account.id,
-        }, null, 2));
-      } else {
-        setResult("null (no account found)");
-      }
-    } catch (e: any) {
-      setResult(`Error: ${e.message}`);
-    } finally {
-      setLoading(false);
-    }
-  };
+  const handleGetByProvider = () => runAsynchronouslyWithAlert(async () => {
+    setLoading(true);
+    try {
+      if (!isProviderType(provider)) throw new Error(`Unknown provider: ${provider}`);
+      const account = await user.getConnectedAccount(provider);
+      if (account) {
+        setResult(JSON.stringify({
+          provider: account.provider,
+          providerAccountId: account.providerAccountId,
+          id: account.id,
+        }, null, 2));
+      } else {
+        setResult("null (no account found)");
+      }
+    } finally {
+      setLoading(false);
+    }
+  });
 
   const handleGetByBothIds = async () => {
     setLoading(true);
     try {
       const account = await user.getConnectedAccount({
         provider,
         providerAccountId,
       });
       if (account) {
         setResult(JSON.stringify({
           provider: account.provider,
           providerAccountId: account.providerAccountId,
           id: account.id,
         }, null, 2));
       } else {
         setResult("null (no account found)");
       }
-    } catch (e: any) {
-      setResult(`Error: ${e.message}`);
-    } finally {
+    } finally {
       setLoading(false);
     }
   };
+
+  const handleGetByBothIds = () => runAsynchronouslyWithAlert(async () => {
+    setLoading(true);
+    try {
+      const account = await user.getConnectedAccount({
+        provider,
+        providerAccountId,
+      });
+      if (account) {
+        setResult(JSON.stringify({
+          provider: account.provider,
+          providerAccountId: account.providerAccountId,
+          id: account.id,
+        }, null, 2));
+      } else {
+        setResult("null (no account found)");
+      }
+    } finally {
+      setLoading(false);
+    }
+  });
 
 function LinkNewAccountDemo() {
   const user = useUser({ or: 'redirect' });
   const [provider, setProvider] = useState("spotify");
   const [scopes, setScopes] = useState("");
   const [loading, setLoading] = useState(false);
 
-  const handleLink = async () => {
-    setLoading(true);
-    try {
-      await user.linkConnectedAccount(provider, {
-        scopes: scopes ? scopes.split(",").map(s => s.trim()) : undefined,
-      });
-    } catch (e: any) {
-      alert(`Error: ${e.message}`);
-    } finally {
-      setLoading(false);
-    }
-  };
+  const handleLink = () => runAsynchronouslyWithAlert(async () => {
+    setLoading(true);
+    try {
+      await user.linkConnectedAccount(provider, {
+        scopes: scopes ? scopes.split(",").map((s) => s.trim()) : undefined,
+      });
+    } finally {
+      setLoading(false);
+    }
+  });
🤖 Prompt for AI Agents
In `@examples/demo/src/app/connected-accounts/page.tsx` around lines 101 - 214,
Replace the broad try/catch + any casts in handleGetByProvider,
handleGetByBothIds and handleLink with runAsynchronouslyWithAlert: validate that
provider is a non-empty string (reject or show alert if not) instead of using
provider as any, remove the `: any` error casts, and wrap the async calls
(user.getConnectedAccount(...) and user.linkConnectedAccount(...)) inside
runAsynchronouslyWithAlert to surface errors; ensure providerAccountId handling
remains the same for getByBothIds and that scopes are parsed the same in
handleLink before passing to user.linkConnectedAccount.

Comment on lines +1130 to +1138
const OAuthAccessTokenNotAvailable = createKnownErrorConstructor(
KnownError,
"OAUTH_ACCESS_TOKEN_NOT_AVAILABLE",
(provider: string, details: string) => [
400,
`Failed to retrieve an OAuth access token for the connected account (provider: ${provider}). ${details}`,
] as const,
(json: any) => [json.provider, json.details] as [string, string],
);
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

Include details payload so OAuthAccessTokenNotAvailable round-trips

constructorArgsFromJson is fed json.details, but the constructor never sets details, so deserialization loses provider/details (and can throw). Add a details object and guard missing fields.

🔧 Suggested fix
 const OAuthAccessTokenNotAvailable = createKnownErrorConstructor(
   KnownError,
   "OAUTH_ACCESS_TOKEN_NOT_AVAILABLE",
   (provider: string, details: string) => [
     400,
     `Failed to retrieve an OAuth access token for the connected account (provider: ${provider}). ${details}`,
+    {
+      provider,
+      details,
+    },
   ] as const,
-  (json: any) => [json.provider, json.details] as [string, string],
+  (json: { provider?: string, details?: string }) => [
+    json.provider ?? throwErr("provider not found in OAuthAccessTokenNotAvailable details"),
+    json.details ?? throwErr("details not found in OAuthAccessTokenNotAvailable details"),
+  ] as const,
 );
🤖 Prompt for AI Agents
In `@packages/stack-shared/src/known-errors.tsx` around lines 1130 - 1138, The
OAuthAccessTokenNotAvailable constructor currently doesn't persist the details
field so deserialization (constructorArgsFromJson) loses provider/details and
can throw; update the createKnownErrorConstructor call for
OAuthAccessTokenNotAvailable so the constructor accepts and stores a details
string (include it in the error payload/state returned by the factory) and make
the JSON-to-args function resilient by guarding missing
json.provider/json.details (e.g., default to empty string) so round-tripping via
constructorArgsFromJson works without throwing.

Comment on lines +394 to +397
// Find the matching oauth provider to get the providerAccountId
// At this point, user is guaranteed to be non-null because we returned early if !hasConnection
const matchingProvider = user!.oauth_providers.find((p) => p.id === options.providerId);
const providerAccountId = matchingProvider?.account_id ?? "";
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

Replace user! non-null assertion with a defensive check.

Line 396 uses user!.oauth_providers with a comment that the user is "guaranteed to be non-null." Per coding guidelines, prefer ?? throwErr(...) over non-null assertions to surface broken assumptions clearly.

Also, matchingProvider?.account_id ?? "" on line 397 silently falls back to an empty string if the provider isn't found — this could mask bugs where the provider list is inconsistent with hasConnection.

Proposed fix
-    const matchingProvider = user!.oauth_providers.find((p) => p.id === options.providerId);
-    const providerAccountId = matchingProvider?.account_id ?? "";
+    const matchingProvider = (user ?? throwErr("Expected user to be non-null because hasConnection was true")).oauth_providers.find((p) => p.id === options.providerId);
+    const providerAccountId = matchingProvider?.account_id ?? throwErr(`Expected to find provider ${options.providerId} in user's oauth_providers list`);

As per coding guidelines, "Prefer ?? throwErr(...) over non-null assertions with good error messages explicitly stating the assumption that must've been violated."

🤖 Prompt for AI Agents
In `@packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts`
around lines 394 - 397, Replace the non-null assertion on user in the block that
finds the OAuth provider: instead of user!.oauth_providers, assert user exists
with a defensive check (e.g., user ?? throwErr("Expected user to be present when
hasConnection is true in client-app-impl")) and likewise ensure the found
matchingProvider is present — do not default providerAccountId to an empty
string; if matchingProvider is missing throw an explicit error (e.g., throwErr
or new Error) that includes options.providerId and the context (function/class:
client-app-impl handling connections) so broken assumptions are surfaced
clearly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant