Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@N2D4
Copy link
Contributor

@N2D4 N2D4 commented Oct 30, 2025

Summary by CodeRabbit

  • New Features

    • Passkey registration added for users (client and server-side flows).
    • New auth APIs to retrieve auth headers and token JSON; sign-out now accepts optional token-store handling.
  • Tests

    • Added end-to-end tests covering sign-in, sign-out, token retrieval/headers, token-store behavior, and parity between app and user auth methods.

Copilot AI review requested due to automatic review settings October 30, 2025 01:41
@vercel
Copy link

vercel bot commented Oct 30, 2025

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

Project Deployment Preview Comments Updated (UTC)
stack-backend Ready Ready Preview Comment Nov 6, 2025 6:05pm
stack-dashboard Ready Ready Preview Comment Nov 6, 2025 6:05pm
stack-demo Ready Ready Preview Comment Nov 6, 2025 6:05pm
stack-docs Ready Ready Preview Comment Nov 6, 2025 6:05pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 30, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds an AuthLike authentication surface and integrates it into client and user types, implements getAuthJson/getAuthHeaders/signOut (tokenStore-aware) on client, moves registerPasskey to user-facing APIs (and implements server/client flows), and adds e2e tests for auth-like behavior.

Changes

Cohort / File(s) Change Summary
E2E tests
apps/e2e/tests/js/auth-like.test.ts
New end-to-end test suite covering sign-in, getAuthJson, getAuthHeaders, tokenStore behavior, sign-out, and parity between app- and user-level auth methods.
Auth common types
packages/template/src/lib/stack-app/common.ts
Added exported AuthLike<ExtraOptions> type with JSDoc and members: overloaded signOut, getAuthHeaders, and getAuthJson.
Client public types
packages/template/src/lib/stack-app/apps/interfaces/client-app.ts
StackClientApp and constructor types now intersect with AuthLike<...> (conditional tokenStore optionality based on HasTokenStore).
Client implementation
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
Added getAuthJson and getAuthHeaders (supporting tokenStore), updated signOut to accept tokenStore and route via current user when present, and reintroduced registerPasskey surface/flows on client side.
Server user implementation
packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts
Added ServerUser.registerPasskey implementation: hostname inference, initiate registration via server interface, WebAuthn start/finish, error mapping, and user refresh.
Server interface
packages/stack-shared/src/interface/server-interface.ts
Added initiateServerPasskeyRegistration(userId) to create a temporary session and delegate to passkey initiation.
Users API surface
packages/template/src/lib/stack-app/users/index.ts
Replaced concrete Auth methods with AuthLike<{}> & { _internalSession, currentSession }; moved registerPasskey from Auth to UserExtra (new public method).
Tooling config
.claude/settings.json
Removed a pre-submit UserPromptSubmit hook that performed a backend reachability check; other hooks unchanged.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor ClientCode
  participant ClientApp
  participant CurrentUser
  participant Server
  participant TokenStore as TokenStore (opt)

  note right of ClientApp `#D7EAF0`: New/changed APIs\ngetAuthJson / getAuthHeaders / signOut\nregisterPasskey flows

  ClientCode->>ClientApp: getAuthJson(options?)
  alt options.tokenStore provided
    ClientApp->>TokenStore: resolve tokens (via CurrentUser session)
    TokenStore-->>ClientApp: { accessToken, refreshToken }
  else no tokenStore
    ClientApp-->>ClientCode: { accessToken: string|null, refreshToken: string|null }
  end

  ClientCode->>ClientApp: getAuthHeaders(options?)
  ClientApp->>ClientApp: call getAuthJson(options?)
  ClientApp-->>ClientCode: { "x-stack-auth": JSON.stringify(tokens) }

  ClientCode->>ClientApp: signOut(options?)
  alt ClientApp has CurrentUser
    ClientApp->>CurrentUser: signOut(options?)
    CurrentUser-->>ClientApp: OK
  else no CurrentUser
    ClientApp->>Server: app-level signOut
    Server-->>ClientApp: OK
  end

  ClientCode->>ClientApp: registerPasskey(options?)
  ClientApp->>Server: initiatePasskeyRegistration (may use hostname)
  Server-->>ClientApp: initiation data (code, options)
  ClientApp->>ClientCode: perform WebAuthn start/finish (navigator.credentials)
  ClientCode-->>ClientApp: attestation / clientData
  ClientApp->>Server: complete registration (attestation + code)
  Server-->>ClientApp: registration result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Attention points:
    • Type intersection and conditional tokenStore optionality across public surfaces.
    • Correct token selection/fallback logic in getAuthJson/getAuthHeaders.
    • WebAuthn flows and error mapping in registerPasskey (client + server + temp session).
    • New server interface initiateServerPasskeyRegistration and its session handling.
    • E2E tests: ensure they reflect runtime token-store and sign-out semantics.

Poem

🐇
I hop with keys and tokens bright,
I tuck them safe both day and night,
App and user now agree,
Passkeys shifted, headers free,
A joyful rabbit skips in light!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description check ⚠️ Warning No pull request description was provided by the author, only the minimal contributing guidelines template. Add a detailed description explaining the purpose of these new methods, the changes made across files (client-app, server-app, common types), and any breaking changes or migration notes.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: adding signOut, getAuthJson, and getAuthHeaders methods to the StackApp types across client and server implementations.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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 sign-out-on-app

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.

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 refactors authentication methods by extracting them into a shared AuthLike type and adds these methods directly to the client app interface. This allows authentication operations to be performed at the app level without requiring a user object.

  • Created AuthLike<ExtraOptions> type containing signOut, getAuthHeaders, and getAuthJson methods
  • Extended both Auth and StackClientApp to implement AuthLike with appropriate option types
  • Moved registerPasskey from Auth to UserExtra type

Reviewed Changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/template/src/lib/stack-app/common.ts Added new AuthLike type with authentication methods and documentation
packages/template/src/lib/stack-app/users/index.ts Refactored Auth to extend AuthLike and moved registerPasskey to UserExtra
packages/template/src/lib/stack-app/apps/interfaces/client-app.ts Extended StackClientApp with AuthLike methods supporting optional tokenStore
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts Implemented app-level auth methods that delegate to user methods
apps/e2e/tests/js/auth-like.test.ts Added comprehensive tests for new app-level authentication methods

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

*/
export type AuthLike<ExtraOptions = {}> = {
signOut(options?: { redirectUrl?: URL | string } & ExtraOptions): Promise<void>,
signOut(options?: { redirectUrl?: URL | string }): Promise<void>,
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

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

The duplicate signOut method signature is redundant. TypeScript function overloads should only include the implementation signature once. The first signature with ExtraOptions already covers the second signature when ExtraOptions is an empty object type. Consider removing line 109.

Suggested change
signOut(options?: { redirectUrl?: URL | string }): Promise<void>,

Copilot uses AI. Check for mistakes.
async signOut(options?: { redirectUrl?: URL | string }): Promise<void> {
const user = await this.getUser();
async signOut(options?: { redirectUrl?: URL | string, tokenStore?: TokenStoreInit }): Promise<void> {
const user = await this.getUser({ tokenStore: options?.tokenStore ?? undefined as any });
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

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

The type assertion as any is used to bypass type checking. Consider using a more type-safe approach. Since getUser expects GetCurrentUserOptions<HasTokenStore>, you could pass options?.tokenStore directly without the ?? undefined as any pattern, or properly type the options parameter based on HasTokenStore.

Copilot uses AI. Check for mistakes.
}

async getAuthJson(options?: { tokenStore?: TokenStoreInit }): Promise<{ accessToken: string | null, refreshToken: string | null }> {
const user = await this.getUser({ tokenStore: options?.tokenStore ?? undefined as any });
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

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

The type assertion as any is used to bypass type checking. Consider using a more type-safe approach. This is the same issue as in the signOut method on line 2173.

Copilot uses AI. Check for mistakes.
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.

Greptile Overview

Greptile Summary

Exposed signOut, getAuthJson, and getAuthHeaders methods on StackClientApp to provide consistent authentication API across the app and user interfaces.

Key changes:

  • Created reusable AuthLike<ExtraOptions> type in common.ts with comprehensive documentation for authentication methods
  • Extended StackClientApp interface to implement AuthLike with conditional tokenStore parameter (required when HasTokenStore is false, optional otherwise)
  • Implemented the three methods on client app by delegating to corresponding user methods
  • Moved registerPasskey from Auth type to UserExtra type for better organization
  • Added comprehensive test coverage in new test file auth-like.test.ts

Implementation approach:
The new app-level methods retrieve the current user (with optional tokenStore parameter) and delegate to the user's existing authentication methods. This ensures consistency between user.getAuthJson() and app.getAuthJson() while providing convenience for cases where the user object isn't readily available.

Confidence Score: 4/5

  • Safe to merge with one security fix needed for redirect URL validation
  • The PR implements a clean abstraction with proper delegation and comprehensive tests. However, there's a pre-existing security issue with unvalidated redirect URLs in _signOut that this PR continues to expose through the new public API, making it more important to address
  • Pay attention to packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts - the redirect URL validation issue at line 2165 should be fixed before merging

Important Files Changed

File Analysis

Filename Score Overview
packages/template/src/lib/stack-app/common.ts 5/5 Added AuthLike<ExtraOptions> type to define reusable authentication interface with detailed documentation
packages/template/src/lib/stack-app/apps/interfaces/client-app.ts 5/5 Extended StackClientApp interface with AuthLike to expose authentication methods with conditional tokenStore option based on HasTokenStore generic
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts 4/5 Implemented signOut, getAuthJson, and getAuthHeaders on client app by delegating to user methods, and moved registerPasskey from Auth to UserExtra

Sequence Diagram

sequenceDiagram
    participant Client as Client Code
    participant App as StackClientApp
    participant User as CurrentUser
    participant Session as InternalSession
    participant TokenStore as Token Store

    Note over Client,TokenStore: getAuthJson flow
    Client->>App: getAuthJson(options?)
    App->>App: getUser({ tokenStore })
    App->>TokenStore: retrieve user session
    TokenStore-->>App: user or null
    alt user exists
        App->>User: getAuthJson()
        User->>Session: getTokens()
        Session-->>User: { accessToken, refreshToken }
        User-->>App: { accessToken, refreshToken }
    else no user
        App->>App: return { accessToken: null, refreshToken: null }
    end
    App-->>Client: auth tokens

    Note over Client,TokenStore: getAuthHeaders flow
    Client->>App: getAuthHeaders(options?)
    App->>App: getAuthJson(options)
    App->>App: JSON.stringify(authJson)
    App-->>Client: { "x-stack-auth": stringified_json }

    Note over Client,TokenStore: signOut flow
    Client->>App: signOut({ redirectUrl?, tokenStore? })
    App->>App: getUser({ tokenStore })
    App->>TokenStore: retrieve user session
    TokenStore-->>App: user or null
    alt user exists
        App->>User: signOut({ redirectUrl })
        User->>App: _signOut(session, options)
        App->>Session: signOut(session)
        Session-->>App: clear tokens
        alt redirectUrl provided
            App->>App: _redirectTo(redirectUrl)
        else no redirectUrl
            App->>App: redirectToAfterSignOut()
        end
    end
Loading

Additional Comments (1)

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

    logic: redirect URL should be validated before use to prevent open redirect vulnerabilities. use _redirectIfTrusted instead of _redirectTo

    Context Used: Rule from dashboard - Always validate redirect URLs for security reasons before using them for redirects, especially in cl... (source)

5 files reviewed, 1 comment

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: 2

♻️ Duplicate comments (1)
packages/template/src/lib/stack-app/common.ts (1)

107-110: Remove the duplicate signOut overload to preserve tokenStore requirements
The second overload drops ExtraOptions, so for StackClientApp where ExtraOptions requires a tokenStore, TypeScript will still accept clientApp.signOut() without one. That reintroduces the very runtime error this refactor was preventing. Please remove the redundant overload and rely on the single signature that carries ExtraOptions.

 export type AuthLike<ExtraOptions = {}> = {
   signOut(options?: { redirectUrl?: URL | string } & ExtraOptions): Promise<void>,
-  signOut(options?: { redirectUrl?: URL | string }): Promise<void>,
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between aea7ad0 and fb6819c.

📒 Files selected for processing (5)
  • apps/e2e/tests/js/auth-like.test.ts (1 hunks)
  • packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (2 hunks)
  • packages/template/src/lib/stack-app/apps/interfaces/client-app.ts (2 hunks)
  • packages/template/src/lib/stack-app/common.ts (1 hunks)
  • packages/template/src/lib/stack-app/users/index.ts (3 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use ES6 Maps instead of Records wherever possible in TypeScript code

Files:

  • apps/e2e/tests/js/auth-like.test.ts
  • packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
  • packages/template/src/lib/stack-app/common.ts
  • packages/template/src/lib/stack-app/apps/interfaces/client-app.ts
  • packages/template/src/lib/stack-app/users/index.ts
**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

When writing tests, prefer .toMatchInlineSnapshot over other selectors where possible

Files:

  • apps/e2e/tests/js/auth-like.test.ts
packages/template/**

📄 CodeRabbit inference engine (AGENTS.md)

When changes are needed for stack or js packages, make them in packages/template instead

Files:

  • packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
  • packages/template/src/lib/stack-app/common.ts
  • packages/template/src/lib/stack-app/apps/interfaces/client-app.ts
  • packages/template/src/lib/stack-app/users/index.ts
🧠 Learnings (1)
📚 Learning: 2025-10-11T04:13:19.308Z
Learnt from: N2D4
PR: stack-auth/stack-auth#943
File: examples/convex/app/action/page.tsx:23-28
Timestamp: 2025-10-11T04:13:19.308Z
Learning: In the stack-auth codebase, use `runAsynchronouslyWithAlert` from `stackframe/stack-shared/dist/utils/promises` for async button click handlers and form submissions instead of manual try/catch blocks. This utility automatically handles errors and shows alerts to users.

Applied to files:

  • packages/template/src/lib/stack-app/apps/interfaces/client-app.ts
🧬 Code graph analysis (4)
apps/e2e/tests/js/auth-like.test.ts (2)
apps/e2e/tests/helpers.ts (1)
  • it (12-12)
apps/e2e/tests/js/js-helpers.ts (1)
  • createApp (41-86)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (4)
packages/stack-shared/src/known-errors.tsx (2)
  • KnownErrors (1589-1591)
  • KnownErrors (1593-1716)
packages/stack-shared/src/utils/errors.tsx (2)
  • StackAssertionError (69-85)
  • captureError (126-134)
packages/stack-shared/src/utils/results.tsx (1)
  • error (36-41)
packages/template/src/lib/stack-app/common.ts (1)
  • TokenStoreInit (68-77)
packages/template/src/lib/stack-app/apps/interfaces/client-app.ts (1)
packages/template/src/lib/stack-app/common.ts (2)
  • AuthLike (107-178)
  • TokenStoreInit (68-77)
packages/template/src/lib/stack-app/users/index.ts (2)
packages/template/src/lib/stack-app/common.ts (1)
  • AuthLike (107-178)
packages/stack-shared/src/known-errors.tsx (2)
  • KnownErrors (1589-1591)
  • KnownErrors (1593-1716)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Security Check

Comment on lines +83 to +92
const userBefore = await clientApp.getUser();
expect(userBefore).not.toBeNull();

// clientApp.signOut delegates to user.signOut, which triggers redirect
// So we just verify it doesn't throw
// In a real scenario, this would redirect the browser
// For this test, we're just verifying the method exists and can be called
const authJsonBefore = await clientApp.getAuthJson();
expect(authJsonBefore.accessToken).not.toBeNull();
});
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

Actually exercise signOut in the test
The test titled “clientApp.signOut should sign out the user” never calls clientApp.signOut, so it will succeed even if the implementation is entirely broken. Call the method and assert the post-sign-out state to make the test meaningful.

   const authJsonBefore = await clientApp.getAuthJson();
   expect(authJsonBefore.accessToken).not.toBeNull();
+
+  await clientApp.signOut();
+
+  const authJsonAfter = await clientApp.getAuthJson();
+  expect(authJsonAfter.accessToken).toBeNull();
+  expect(authJsonAfter.refreshToken).toBeNull();
📝 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
const userBefore = await clientApp.getUser();
expect(userBefore).not.toBeNull();
// clientApp.signOut delegates to user.signOut, which triggers redirect
// So we just verify it doesn't throw
// In a real scenario, this would redirect the browser
// For this test, we're just verifying the method exists and can be called
const authJsonBefore = await clientApp.getAuthJson();
expect(authJsonBefore.accessToken).not.toBeNull();
});
const userBefore = await clientApp.getUser();
expect(userBefore).not.toBeNull();
// clientApp.signOut delegates to user.signOut, which triggers redirect
// So we just verify it doesn't throw
// In a real scenario, this would redirect the browser
// For this test, we're just verifying the method exists and can be called
const authJsonBefore = await clientApp.getAuthJson();
expect(authJsonBefore.accessToken).not.toBeNull();
await clientApp.signOut();
const authJsonAfter = await clientApp.getAuthJson();
expect(authJsonAfter.accessToken).toBeNull();
expect(authJsonAfter.refreshToken).toBeNull();
});
🤖 Prompt for AI Agents
In apps/e2e/tests/js/auth-like.test.ts around lines 83 to 92, the test never
actually calls clientApp.signOut so it passes even if signOut is broken; update
the test to call await clientApp.signOut() and then assert the post-sign-out
state (for example await clientApp.getUser() returns null or
clientApp.getAuthJson().accessToken is null) to verify signOut was exercised and
had the expected effect.

Comment on lines +1245 to +1264
async registerPasskey(options?: { hostname?: string }): Promise<Result<undefined, KnownErrors["PasskeyRegistrationFailed"] | KnownErrors["PasskeyWebAuthnError"]>> {
const hostname = (await app._getCurrentUrl())?.hostname;
if (!hostname) {
throw new StackAssertionError("hostname must be provided if the Stack App does not have a redirect method");
}

const initiationResult = await app._interface.initiatePasskeyRegistration({}, session);

if (initiationResult.status !== "ok") {
return Result.error(new KnownErrors.PasskeyRegistrationFailed("Failed to get initiation options for passkey registration"));
}

const { options_json, code } = initiationResult.data;

// HACK: Override the rpID to be the actual domain
if (options_json.rp.id !== "THIS_VALUE_WILL_BE_REPLACED.example.com") {
throw new StackAssertionError(`Expected returned RP ID from server to equal sentinel, but found ${options_json.rp.id}`);
}

options_json.rp.id = hostname;
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

Use the caller-provided hostname when inferring the RP ID
registerPasskey accepts options.hostname precisely to support environments where _getCurrentUrl() is unavailable (e.g., redirectMethod: "none"). Ignoring that argument means we still throw even if the caller supplied the hostname. Wire the option into the RP ID selection so the method works off-browser.

-      const hostname = (await app._getCurrentUrl())?.hostname;
+      const hostname = options?.hostname ?? (await app._getCurrentUrl())?.hostname;
       if (!hostname) {
         throw new StackAssertionError("hostname must be provided if the Stack App does not have a redirect method");
       }
📝 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
async registerPasskey(options?: { hostname?: string }): Promise<Result<undefined, KnownErrors["PasskeyRegistrationFailed"] | KnownErrors["PasskeyWebAuthnError"]>> {
const hostname = (await app._getCurrentUrl())?.hostname;
if (!hostname) {
throw new StackAssertionError("hostname must be provided if the Stack App does not have a redirect method");
}
const initiationResult = await app._interface.initiatePasskeyRegistration({}, session);
if (initiationResult.status !== "ok") {
return Result.error(new KnownErrors.PasskeyRegistrationFailed("Failed to get initiation options for passkey registration"));
}
const { options_json, code } = initiationResult.data;
// HACK: Override the rpID to be the actual domain
if (options_json.rp.id !== "THIS_VALUE_WILL_BE_REPLACED.example.com") {
throw new StackAssertionError(`Expected returned RP ID from server to equal sentinel, but found ${options_json.rp.id}`);
}
options_json.rp.id = hostname;
async registerPasskey(options?: { hostname?: string }): Promise<Result<undefined, KnownErrors["PasskeyRegistrationFailed"] | KnownErrors["PasskeyWebAuthnError"]>> {
const hostname = options?.hostname ?? (await app._getCurrentUrl())?.hostname;
if (!hostname) {
throw new StackAssertionError("hostname must be provided if the Stack App does not have a redirect method");
}
const initiationResult = await app._interface.initiatePasskeyRegistration({}, session);
if (initiationResult.status !== "ok") {
return Result.error(new KnownErrors.PasskeyRegistrationFailed("Failed to get initiation options for passkey registration"));
}
const { options_json, code } = initiationResult.data;
// HACK: Override the rpID to be the actual domain
if (options_json.rp.id !== "THIS_VALUE_WILL_BE_REPLACED.example.com") {
throw new StackAssertionError(`Expected returned RP ID from server to equal sentinel, but found ${options_json.rp.id}`);
}
options_json.rp.id = hostname;
🤖 Prompt for AI Agents
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
around lines 1245 to 1264: the code always derives hostname from
app._getCurrentUrl() and throws if missing, ignoring the caller-provided
options.hostname; update the logic to prefer options.hostname when present (fall
back to _getCurrentUrl()), use that resolved hostname for the RP ID override and
for the missing-hostname check, and only throw a StackAssertionError if neither
options.hostname nor _getCurrentUrl() provides a hostname; ensure the rest of
the flow uses this resolved hostname variable.


async signOut(options?: { redirectUrl?: URL | string }): Promise<void> {
const user = await this.getUser();
async signOut(options?: { redirectUrl?: URL | string, tokenStore?: TokenStoreInit }): Promise<void> {
Copy link

Choose a reason for hiding this comment

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

The signOut, getAuthHeaders, and getAuthJson methods have incorrect type signatures that don't properly enforce the conditional tokenStore requirement when HasTokenStore extends false.

View Details
📝 Patch Details
diff --git a/packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts b/packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
index a88469d8..4ab51c07 100644
--- a/packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
+++ b/packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
@@ -1957,7 +1957,7 @@ export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, Projec
     }
   }
   // END_PLATFORM
-  getConvexClientAuth(options: { tokenStore: TokenStoreInit }): (args: { forceRefreshToken: boolean }) => Promise<string | null> {
+  getConvexClientAuth(options: {} & (HasTokenStore extends false ? { tokenStore: TokenStoreInit } : { tokenStore?: TokenStoreInit })): (args: { forceRefreshToken: boolean }) => Promise<string | null> {
     return async (args: { forceRefreshToken: boolean }) => {
       const session = await this._getSession(options.tokenStore ?? this._tokenStoreInit);
       if (!args.forceRefreshToken) {
@@ -2379,20 +2379,20 @@ export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, Projec
     });
   }
 
-  async signOut(options?: { redirectUrl?: URL | string, tokenStore?: TokenStoreInit }): Promise<void> {
+  async signOut(options?: { redirectUrl?: URL | string } & (HasTokenStore extends false ? { tokenStore: TokenStoreInit } : { tokenStore?: TokenStoreInit })): Promise<void> {
     const user = await this.getUser({ tokenStore: options?.tokenStore ?? undefined as any });
     if (user) {
       await user.signOut({ redirectUrl: options?.redirectUrl });
     }
   }
 
-  async getAuthHeaders(options?: { tokenStore?: TokenStoreInit }): Promise<{ "x-stack-auth": string }> {
+  async getAuthHeaders(options?: {} & (HasTokenStore extends false ? { tokenStore: TokenStoreInit } : { tokenStore?: TokenStoreInit })): Promise<{ "x-stack-auth": string }> {
     return {
       "x-stack-auth": JSON.stringify(await this.getAuthJson(options)),
     };
   }
 
-  async getAuthJson(options?: { tokenStore?: TokenStoreInit }): Promise<{ accessToken: string | null, refreshToken: string | null }> {
+  async getAuthJson(options?: {} & (HasTokenStore extends false ? { tokenStore: TokenStoreInit } : { tokenStore?: TokenStoreInit })): Promise<{ accessToken: string | null, refreshToken: string | null }> {
     const user = await this.getUser({ tokenStore: options?.tokenStore ?? undefined as any });
     if (user) {
       return await user.getAuthJson();

Analysis

Incorrect type signatures for auth methods when HasTokenStore extends false

What fails: signOut(), getAuthHeaders(), and getAuthJson() in StackClientApp incorrectly make tokenStore optional instead of required, violating the interface contract

How to reproduce:

// TypeScript should error but doesn't before fix:
const app: StackClientApp<false> = new StackClientApp({ projectId: "test" });
await app.signOut(); // Missing required tokenStore
await app.getAuthHeaders(); // Missing required tokenStore  
await app.getAuthJson(); // Missing required tokenStore

Result: Code compiles without type errors, then fails at runtime with "Cannot call this function on a Stack app without a persistent token store"

Expected: TypeScript should enforce required tokenStore parameter per AuthLike interface when HasTokenStore extends false

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: 2

🧹 Nitpick comments (1)
packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts (1)

726-730: Fragile sentinel validation.

The hardcoded sentinel value "THIS_VALUE_WILL_BE_REPLACED.example.com" creates a tight coupling between server and client. If the server changes this value, the client will break with an assertion error.

Consider:

  1. Making the sentinel value a shared constant
  2. Using a more robust validation pattern (e.g., checking for .example.com domain)
  3. Logging a warning instead of throwing if the sentinel doesn't match, then proceeding with replacement anyway

</comment_end>

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 589bbf1 and fffa091.

📒 Files selected for processing (3)
  • .claude/settings.json (0 hunks)
  • packages/stack-shared/src/interface/server-interface.ts (1 hunks)
  • packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • .claude/settings.json
🧰 Additional context used
🧬 Code graph analysis (2)
packages/stack-shared/src/interface/server-interface.ts (2)
packages/stack-shared/src/known-errors.tsx (2)
  • KnownErrors (1570-1572)
  • KnownErrors (1574-1695)
packages/stack-shared/src/sessions.ts (1)
  • InternalSession (71-241)
packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts (3)
packages/stack-shared/src/known-errors.tsx (2)
  • KnownErrors (1570-1572)
  • KnownErrors (1574-1695)
packages/stack-shared/src/utils/errors.tsx (2)
  • StackAssertionError (69-85)
  • captureError (126-134)
packages/stack-shared/src/sessions.ts (1)
  • InternalSession (71-241)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Vercel Agent Review
  • GitHub Check: build (22.x)
  • GitHub Check: lint_and_build (latest)
  • GitHub Check: setup-tests
  • GitHub Check: build (22.x)
  • GitHub Check: build (22.x)
  • GitHub Check: check_prisma_migrations (22.x)
  • GitHub Check: restart-dev-and-test-with-custom-base-port
  • GitHub Check: docker
  • GitHub Check: restart-dev-and-test
  • GitHub Check: all-good
🔇 Additional comments (3)
packages/stack-shared/src/interface/server-interface.ts (1)

914-927: Acknowledge TODO: Temporary session approach adds session history entries.

The TODO comment correctly identifies that creating a temporary session for passkey registration is not ideal because it appears in the user's session history. While this implementation is functional, consider prioritizing the endpoint refactor to allow passkey registration without requiring a session.

In the interim, this approach is acceptable given:

  • The 2-minute expiration minimizes exposure
  • The session is marked as non-impersonation (false)
  • The refresh callback returning null prevents unintended token refresh

</comment_end>

packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts (2)

709-714: LGTM: Hostname resolution is appropriate.

The method correctly handles hostname resolution with a fallback to the current URL, and throws a clear assertion error if neither is available.


732-746: LGTM: WebAuthn error handling is thorough.

The dynamic import and error handling properly distinguish between expected WebAuthn errors and unexpected failures, with appropriate logging via captureError.

return providers.find((p) => p.id === id) ?? null;
},
async registerPasskey(options?: { hostname?: string }): Promise<Result<undefined, KnownErrors["PasskeyRegistrationFailed"] | KnownErrors["PasskeyWebAuthnError"]>> {
// TODO remove duplicated code between this and the function in client-app-impl.ts
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Verify code duplication with client-app-impl.ts.

The TODO indicates duplicated code between this method and one in client-app-impl.ts. If significant portions are duplicated, consider extracting shared logic into a common utility function.


🏁 Script executed:

#!/bin/bash
# Check for similar registerPasskey implementation in client-app-impl.ts
rg -nP -A 50 'async registerPasskey\(' --type=ts -g 'client-app-impl.ts'

Length of output: 6394


The web search isn't helpful for this task. Let me search the codebase directly for the server version of registerPasskey.


---

🏁 Script executed:

```shell
#!/bin/bash
# Search for registerPasskey in server-app-impl.ts to see the actual server implementation
rg -nP -A 60 'async registerPasskey\(' --type=ts -g 'server-app-impl.ts'

Length of output: 8146


Extract duplicated passkey registration logic into shared utilities.

The TODO is valid. Both implementations share identical logic for hostname resolution, rpID override validation, and WebAuthn error handling. Consider extracting these into shared utility functions:

  • validateAndOverrideRpId() — handles sentinel validation and hostname substitution (lines 726–730 in server, 1470–1474 in client)
  • handleWebAuthnRegistrationError() — standardizes WebAuthn error handling (lines 733–746 in server, 1477–1487 in client)
  • resolveHostname() — centralizes hostname resolution (lines 711–714 in server, 1456–1459 in client)

The core difference—client using the existing session vs. server creating a temporary session—can be parameterized or handled by the caller, leaving the shared logic focused on credential generation and error handling.

🤖 Prompt for AI Agents
packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts
around line 710: duplicate passkey registration logic exists in both
server-app-impl.ts and client-app-impl.ts (hostname resolution, rpID override
validation, and WebAuthn error handling). Extract three shared utility functions
into a common module (e.g., lib/auth/webauthn-utils.ts):
resolveHostname(hostnameOrSentinel) to centralize hostname resolution;
validateAndOverrideRpId(rpIdOverride, resolvedHostname) to perform sentinel
validation and return the effective rpId or throw; and
handleWebAuthnRegistrationError(err) to normalize and rethrow/log WebAuthn
errors. Replace the duplicated blocks in both server and client implementations
to call these utilities, and parameterize the remaining difference (session
creation vs. using existing session) by having the caller provide a session or a
session-creator callback so the shared code only handles credential generation
and error handling.

Comment on lines +748 to +761
// Create a temporary session to complete the registration
// TODO instead of creating a new session, this should just call the endpoint in a way in which it doesn't require a session
// (currently this shows up on session history etc... not ideal)
const { accessToken, refreshToken } = await app._interface.createServerUserSession(crud.id, 60000 * 2, false);
const tempSession = new InternalSession({
accessToken,
refreshToken,
refreshAccessTokenCallback: async () => null,
});

const registrationResult = await app._interface.registerPasskey({ credential: attResp, code }, tempSession);

await app._serverUserCache.refresh([crud.id]);
return registrationResult;
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

Double temporary session creation.

This method creates a second temporary session (in addition to the one created by initiateServerPasskeyRegistration), resulting in two session history entries for a single passkey registration. This compounds the concern noted in the TODO comment.

Consider refactoring both initiateServerPasskeyRegistration and the registration completion to use a single server-side flow that doesn't require user sessions, or reuse the initial temporary session for the completion step.

</comment_end>

🤖 Prompt for AI Agents
packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts
lines 748-761: the code creates a second temporary session for completing
passkey registration which produces duplicate session history entries; instead
reuse the existing temporary session created by
initiateServerPasskeyRegistration (pass it through or return it from that
function) or change both initiation and completion to a server-side flow that
does not require a user session; update initiateServerPasskeyRegistration to
return the temp session (or a token object) and change this block to call
registerPasskey with that session rather than calling createServerUserSession
again, then remove the duplicate session creation and ensure
serverUserCache.refresh still receives the user id.

@N2D4 N2D4 merged commit 89e6d8a into dev Nov 6, 2025
23 checks passed
@N2D4 N2D4 deleted the sign-out-on-app branch November 6, 2025 19:41
@coderabbitai coderabbitai bot mentioned this pull request Nov 24, 2025
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.

2 participants