-
Notifications
You must be signed in to change notification settings - Fork 498
Add signOut, getAuthJson, and getAuthHeaders to Stack<Xyz>App #989
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Other AI code review bot(s) detectedCodeRabbit 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. WalkthroughAdds 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this 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 containingsignOut,getAuthHeaders, andgetAuthJsonmethods - Extended both
AuthandStackClientAppto implementAuthLikewith appropriate option types - Moved
registerPasskeyfromAuthtoUserExtratype
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>, |
Copilot
AI
Oct 30, 2025
There was a problem hiding this comment.
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.
| signOut(options?: { redirectUrl?: URL | string }): Promise<void>, |
| 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 }); |
Copilot
AI
Oct 30, 2025
There was a problem hiding this comment.
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.
| } | ||
|
|
||
| async getAuthJson(options?: { tokenStore?: TokenStoreInit }): Promise<{ accessToken: string | null, refreshToken: string | null }> { | ||
| const user = await this.getUser({ tokenStore: options?.tokenStore ?? undefined as any }); |
Copilot
AI
Oct 30, 2025
There was a problem hiding this comment.
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.
There was a problem hiding this 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 incommon.tswith comprehensive documentation for authentication methods - Extended
StackClientAppinterface to implementAuthLikewith conditionaltokenStoreparameter (required whenHasTokenStoreis false, optional otherwise) - Implemented the three methods on client app by delegating to corresponding user methods
- Moved
registerPasskeyfromAuthtype toUserExtratype 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
_signOutthat 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
Additional Comments (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
_redirectIfTrustedinstead of_redirectToContext 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
There was a problem hiding this 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 duplicatesignOutoverload to preserve tokenStore requirements
The second overload dropsExtraOptions, so forStackClientAppwhereExtraOptionsrequires atokenStore, TypeScript will still acceptclientApp.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 carriesExtraOptions.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
📒 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.tspackages/template/src/lib/stack-app/apps/implementations/client-app-impl.tspackages/template/src/lib/stack-app/common.tspackages/template/src/lib/stack-app/apps/interfaces/client-app.tspackages/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.tspackages/template/src/lib/stack-app/common.tspackages/template/src/lib/stack-app/apps/interfaces/client-app.tspackages/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
| 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(); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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> { |
There was a problem hiding this comment.
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 tokenStoreResult: 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
There was a problem hiding this 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:
- Making the sentinel value a shared constant
- Using a more robust validation pattern (e.g., checking for
.example.comdomain)- 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
📒 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
nullprevents 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 |
There was a problem hiding this comment.
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.
| // 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Summary by CodeRabbit
New Features
Tests