Skip to content

New { type: "hosted" } for page URLs#1261

Merged
N2D4 merged 9 commits intodevfrom
hosted-components-handoff
Mar 27, 2026
Merged

New { type: "hosted" } for page URLs#1261
N2D4 merged 9 commits intodevfrom
hosted-components-handoff

Conversation

@N2D4
Copy link
Copy Markdown
Contributor

@N2D4 N2D4 commented Mar 17, 2026

Other minor redirect URL changes:

  • app.urls.* for auth is now aware of after_auth_return_to
  • redirectToSignOut now sets and preserves after_auth_return_to
  • OAuth sign-in after_auth_return_to now carries callback-return context

Note

High Risk
Introduces new cross-domain OAuth authorization-code issuance and refresh-token reuse plumbing, plus a schema migration, touching security-sensitive auth and redirect validation paths.

Overview
Adds hosted handler URL targets to the JS/Template SDK (urls.default: { type: "hosted" }), including env-driven hosted domain/template resolution, unknown /handler/* fallback routing, and updated examples to use redirectTo*() instead of relying on dynamic app.urls.

Implements cross-domain auth handoff: a new backend endpoint POST /auth/oauth/cross-domain/authorize issues a one-time PKCE authorization-code redirect only for authenticated, refresh-token-bound sessions (verifies provided refresh token ownership/validity), and the SDK plans/executes these redirects while preserving after_auth_return_to and cross-domain query params.

Tightens OAuth redirect handling by validating after_callback_redirect_url as a URL and whitelist-checking it, and persists refresh-token linkage by adding ProjectUserAuthorizationCode.grantedRefreshTokenId so token exchange can reuse the intended refresh-token session. Includes new migration tests, unit tests for URL targeting, and E2E coverage for cross-domain authorize and redirect URL validation.

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

Summary by CodeRabbit

  • New Features

    • Cross-domain OAuth PKCE handoff flow (client + server) for hosted sign-in.
    • Hosted handler URL templating with local development domain suffix support.
    • Demo UI page to exercise hosted cross-domain sign-in/out and OAuth flows.
    • Authorization codes now preserve an associated refresh-token id to support cross-domain exchanges.
  • Bug Fixes

    • Stricter redirect-URL validation and stronger refresh-token ownership checks.
    • More robust event-tracker startup guards in partial DOM environments.
  • Tests

    • New E2E and unit tests covering cross-domain authorize, callback validation, and handoff flows.

N2D4 added 2 commits March 17, 2026 12:41
Other minor redirect URL changes:

- app.urls.* for auth is now aware of after_auth_return_to
- redirectToSignOut now sets and preserves after_auth_return_to
- OAuth sign-in after_auth_return_to now carries callback-return context
Copilot AI review requested due to automatic review settings March 17, 2026 19:45
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 17, 2026

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

Project Deployment Actions Updated (UTC)
stack-auth-hosted-components Ready Ready Preview, Comment Mar 27, 2026 9:53pm
stack-backend Ready Ready Preview, Comment Mar 27, 2026 9:53pm
stack-dashboard Ready Ready Preview, Comment Mar 27, 2026 9:53pm
stack-demo Ready Ready Preview, Comment Mar 27, 2026 9:53pm
stack-docs Ready Ready Preview, Comment Mar 27, 2026 9:53pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 17, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds cross-domain OAuth PKCE handoff: new backend cross-domain authorize endpoint, DB column linking granted refresh tokens, client-side URL/redirect planning and hosted-handler resolution, centralized env access, tests, demo UI, and event-tracker runtime guards.

Changes

Cohort / File(s) Summary
Env files
apps/backend/.env.development, apps/dashboard/.env.development, apps/e2e/.env.development, examples/demo/.env.development
Added NEXT_PUBLIC_STACK_HOSTED_HANDLER_DOMAIN_SUFFIX and NEXT_PUBLIC_STACK_HOSTED_HANDLER_URL_TEMPLATE dev env keys used for hosted-handler URL resolution.
DB schema & migration
apps/backend/prisma/schema.prisma, apps/backend/prisma/migrations/.../migration.sql, apps/backend/prisma/migrations/.../tests/preserve-null-and-allow-uuid.ts
Add nullable UUID grantedRefreshTokenId to ProjectUserAuthorizationCode; migration and pre/post-migration test to ensure null-preservation and allow UUID writes.
Backend: authorize handler updates
apps/backend/src/app/api/latest/auth/oauth/authorize/[provider_id]/route.tsx, apps/backend/src/oauth/model.tsx
Whitelist/validate after_callback_redirect_url, persist grantedRefreshTokenId on created codes, surface it on code retrieval, and tighten refresh-token ownership checks.
Backend: cross-domain authorize endpoint
apps/backend/src/app/api/latest/auth/oauth/cross-domain/authorize/route.tsx
New POST endpoint createCrossDomainAuthorizeRedirect validating auth + refresh-token ownership, PKCE params, publishable key; invokes oauthServer.authorize and returns provider redirect URL.
Client env centralization & lint
packages/template/src/lib/env.ts, packages/template/.eslintrc.cjs
Introduce envVars accessor for environment variables and add ESLint rule prohibiting direct process.env reads (with exemption for the env module).
URL target resolution & hosted handlers
packages/template/src/lib/stack-app/url-targets.ts, packages/template/src/lib/stack-app/url-targets.test.ts
New URL-target resolution module supporting hosted, handler-component, and custom targets, hosted-template validation, unknown-path fallback, helpers and unit tests.
Redirect planning & cross-domain handoff
packages/template/src/lib/stack-app/apps/implementations/redirect-page-urls.ts
New planner to compute redirect/back-aware handler flows and cross-domain-authorize plans (state/codeChallenge extraction/generation, afterCallbackRedirectUrl handling).
Client app impl & handler client
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts, packages/template/src/components-page/stack-handler-client.tsx
Prefetch/freshness for handoff params, local OAuth callback resolution, POST to cross-domain authorize endpoint to obtain redirect URL, plan-based redirect flow, and default unknown-path fallback support.
API/types & public surface changes
packages/template/src/lib/stack-app/common.ts, packages/template/src/lib/stack-app/interfaces/client-app.ts, packages/template/src/lib/stack-app/index.ts
Introduce HandlerUrlTarget, HandlerUrlOptions, ResolvedHandlerUrls; change getUrls contract to resolve URLs with projectId and update exports/types accordingly.
Client auth & callbacks
packages/template/src/lib/auth.ts, packages/template/src/components-page/oauth-callback.tsx
Replace process.env reads with envVars; track thrown callbackError from app.callOAuthCallback() and use it for post-callback redirect decision.
Event tracker hardening
packages/template/src/lib/stack-app/apps/implementations/event-tracker.ts
Add runtime capability guards, safer reflective history patching/restoration, and skip tracking when required browser APIs are absent.
Payments & components
packages/template/src/components-page/account-settings/payments/payments-panel.tsx
Switch Stripe key and NODE_ENV reads to envVars.
E2E & JS tests
apps/e2e/tests/backend/.../authorize.test.ts, apps/e2e/tests/backend/.../cross-domain-authorize.test.ts, apps/e2e/tests/js/cross-domain-auth.test.ts
Add tests validating after_callback_redirect_url schema/whitelisting and comprehensive cross-domain PKCE E2E/JS-hosted flow tests covering success, reuse, PKCE failure, whitelist violations, auth/session requirements, and client-hosted flows.
Demo app
examples/demo/src/app/cross-domain-handoff/page.tsx, examples/demo/src/stack.tsx
Add demo cross-domain handoff page and update demo app URLs to include hosted default target.
Docs / knowledge
claude/CLAUDE-KNOWLEDGE.md
Document cross-domain handoff mechanics, env setup, refresh-token linkage, tests, SDK bundler guidance, and handler URL/version rules.
Misc
.cursor/commands/pr-comments-review.md
Require tests for fixes and mark critical issues in reports.

Sequence Diagram(s)

sequenceDiagram
    participant Browser as Client (Browser)
    participant Handler as Hosted Auth Handler
    participant Backend as Backend OAuth Handler
    participant DB as Database
    participant User as User Agent

    Browser->>Handler: POST /auth/oauth/cross-domain/authorize (auth headers, body)
    Handler->>Backend: validate session, refresh-token ownership, publishable key
    Backend->>DB: verify refresh token record (projectUserId, id)
    DB-->>Backend: token record
    Backend->>Backend: generate/validate PKCE (state, code_challenge)
    Backend->>Backend: oauthServer.authorize(auth handler includes user + afterCallbackRedirectUrl)
    Backend->>DB: create AuthorizationCode (grantedRefreshTokenId = user.refreshTokenId)
    Backend-->>Handler: return JSON { redirect_url } (from oauthResponse.headers.location)
    Handler-->>Browser: JSON { redirect_url }
    Browser->>User: browser navigates to redirect_url (OAuth provider)
    User->>Backend: provider -> callback with code,state
    Browser->>Backend: POST /token (exchange code + code_verifier)
    Backend->>DB: lookup code -> retrieve grantedRefreshTokenId
    DB-->>Backend: code, grantedRefreshTokenId
    Backend-->>Browser: tokens (access_token, refresh_token)
    Browser->>Browser: store tokens and redirect to after_callback_redirect_url
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • BilalG1

Poem

🐰 I hopped through ports and hostnames wide,
Puffing PKCE in my fluffy stride,
I planted a token ID in rows,
So cross-domain sign-ins safely flow,
Hooray — handoffs snug and verified!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning PR description is minimal and does not follow the template structure. It lacks required sections like motivation, changes, testing, and breaking changes. Expand the description to follow the repository template. Include sections for why these changes were made, detailed explanation of key changes, testing performed, and any breaking changes or migration notes.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'New { type: "hosted" } for page URLs' clearly and concisely describes the main feature introduced in this changeset—adding a new hosted URL target type for page URLs.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch hosted-components-handoff

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
Copy Markdown
Contributor

greptile-apps bot commented Mar 17, 2026

Greptile Summary

This PR introduces a { type: "hosted" } URL target for StackClientApp that routes auth pages to a Stack-hosted domain (e.g. projectId.built-with-stack-auth.com), implementing a full cross-domain auth handoff using PKCE so that the original app's refresh token is reused across the handoff — avoiding duplicate sessions. It also centralises environment-variable access in a new env.ts and adds server-side validation of after_callback_redirect_url in the OAuth authorize endpoints.

Key changes:

  • New { type: "hosted" } and { type: "handler-component" } HandlerUrlTarget union type with resolution logic in url-targets.ts; HandlerUrls now accepts these target objects alongside plain strings
  • app.urls getter is now context-aware — reads window.location and injects after_auth_return_to / cross-domain handoff params into sign-in, sign-up, onboarding, and sign-out URLs automatically
  • New backend endpoint POST /auth/oauth/cross-domain/authorize issues a one-time PKCE authorization code tied to the caller's current refresh token (via x-stack-refresh-token possession proof); a new grantedRefreshTokenId column on ProjectUserAuthorizationCode threads this through token exchange so the original session is reused
  • after_callback_redirect_url in the OAuth authorize route is upgraded from an unvalidated string to a whitelisted URL — closing an open-redirect vector
  • signInWithOAuth now carries afterCallbackRedirectUrl from the current page context so OAuth provider flows preserve the return destination
  • New env.ts centralises all process.env reads with explicit getters for bundler-friendly tree-shaking; direct process.env usage is now lint-banned everywhere except env.ts

Issues found:

  • _createCrossDomainAuthRedirectUrl does not check the HTTP status code before parsing the JSON response, causing API errors (e.g. 401 or 400) to surface as a misleading StackAssertionError("invalid payload") instead of the real error
  • The URL returned by the cross-domain authorize endpoint is passed to _redirectTo (which skips trust validation) instead of _redirectIfTrusted, bypassing the client-side redirect-URL safety check
  • replaceStackPortPrefix is duplicated between url-targets.ts and common.ts
  • When default: { type: "hosted" } is set, afterSignOut, home, afterSignIn, and afterSignUp all resolve to the hosted handler root (/handler), which may show an unexpected page when navigated to directly (e.g. during server-side sign-out without an after_auth_return_to param)
  • All JS-layer cross-domain redirect tests mock _createCrossDomainAuthRedirectUrl, so there is no integration test verifying the HTTP request — including whether the required x-stack-refresh-token header is forwarded by sendClientRequest

Confidence Score: 3/5

  • Needs fixes before merge — two logic issues in the cross-domain redirect path could cause silent failures or bypassed trust checks in production.
  • The backend changes (new endpoint, migration, redirect-URL validation) are well-structured and safe. The client-side redirect planning logic in redirect-page-urls.ts and url-targets.ts is well-tested with unit tests. However, _createCrossDomainAuthRedirectUrl in client-app-impl.ts has two logic concerns: it does not check HTTP status before parsing JSON (API errors surface as misleading assertion errors), and it uses _redirectTo instead of _redirectIfTrusted for the backend-returned redirect URL. Additionally, all JS-layer tests mock the cross-domain authorize call, leaving a gap around whether the required x-stack-refresh-token header is actually sent.
  • Pay close attention to packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts — specifically the _createCrossDomainAuthRedirectUrl method and the cross-domain-authorize branch in _redirectToHandler.

Important Files Changed

Filename Overview
packages/template/src/lib/stack-app/url-targets.ts New central file that resolves HandlerUrlTarget (string
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts Major refactor: urls getter is now context-aware (reads window.location), _redirectToHandler delegates to a pure planner, and new _createCrossDomainAuthRedirectUrl sends a POST to the backend without checking HTTP status before parsing JSON, and uses _redirectTo (bypasses trust check) with the returned URL.
packages/template/src/lib/stack-app/apps/implementations/redirect-page-urls.ts New pure planning/URL-building module for redirect-back-aware handler URLs and cross-domain handoff params. Logic is sound but the after-auth-return policy path relies on having all three cross-domain params present in the URL at callback time.
apps/backend/src/app/api/latest/auth/oauth/cross-domain/authorize/route.tsx New SmartRouteHandler endpoint that issues a one-time PKCE authorization code for cross-domain handoff. Correctly validates redirect URLs against trusted domains and requires refresh token possession proof via x-stack-refresh-token header.
apps/backend/src/oauth/model.tsx Correctly threads grantedRefreshTokenId through the authorization code lifecycle; adds ownership verification before reusing a cross-domain refresh token. Minor rename of afterCallbackRedirectUrl local variable is a no-op functionally.
packages/template/src/lib/env.ts New centralized env-var accessor using explicit getter properties so bundlers (e.g. Next.js) can statically inline process.env.KEY. Correctly guards against environments where process is undefined.
apps/e2e/tests/js/cross-domain-auth.test.ts Comprehensive JS-layer integration tests for cross-domain auth, but _createCrossDomainAuthRedirectUrl is mocked in all redirect tests, so no test verifies the actual HTTP call (including required headers) end-to-end.

Sequence Diagram

sequenceDiagram
    participant App as App (cross-domain)
    participant SDK as StackClientApp SDK
    participant Hosted as Hosted Handler
    participant Backend as Stack Backend
    participant Provider as OAuth Provider

    App->>SDK: redirectToSignIn()
    SDK->>SDK: planRedirectToHandler() detects cross-domain target
    SDK->>Backend: POST /auth/oauth/cross-domain/authorize with PKCE params and refresh-token header
    Backend->>Backend: Validate redirect_uri and after_callback_redirect_url against trusted domains
    Backend->>Backend: Issue authorization code, store grantedRefreshTokenId
    Backend-->>SDK: Returns redirect_url pointing to hosted handler callback
    SDK->>Hosted: Redirect to hosted sign-in with after_auth_return_to set to local callback URL

    Hosted->>Provider: OAuth sign-in redirect
    Provider-->>Hosted: Returns to hosted oauth-callback with code and state
    Hosted->>Backend: Exchange OAuth code for session
    Hosted->>App: Redirect to after_auth_return_to which is local handler with stack_cross_domain_auth marker

    App->>SDK: callOAuthCallback() reconstructs redirect_uri from current URL
    SDK->>Backend: POST /auth/oauth/token to exchange cross-domain auth code with PKCE verifier
    Backend->>Backend: Verify PKCE and reuse grantedRefreshTokenId as refresh token
    Backend-->>SDK: Returns access token and reused refresh token with after_callback_redirect_url
    SDK->>App: Redirect to original page via after_callback_redirect_url
Loading

Comments Outside Diff (5)

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

    P1 API error responses misreported as invalid payload

    response.json() is called unconditionally without first checking response.ok / the HTTP status. When the backend returns a 4xx/5xx error (e.g. { code: "USER_AUTHENTICATION_REQUIRED", error: "…" }), the JSON body won't contain redirect_url, so the code throws a misleading StackAssertionError("invalid payload") instead of propagating the real API error. This makes auth failures very hard to debug.

  2. packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts, line 1870-1878 (link)

    P1 Cross-domain redirect URL used without client-side trust validation

    Per the project's redirect-URL security policy, redirect URLs must be validated before execution in client-side code. The URL returned by _createCrossDomainAuthRedirectUrl — which comes from the backend OAuth library — is passed directly to this._redirectTo (which performs no trust check), bypassing _redirectIfTrusted. Even though the URL originates from our own backend, a defence-in-depth check is warranted here, particularly because the hosted-handler domain is now an additional trusted origin.

    Rule Used: Always validate redirect URLs for security reasons... (source)

    Learnt From
    stack-auth/stack-auth#923

  3. packages/template/src/lib/stack-app/url-targets.ts, line 2587-2591 (link)

    P2 Duplicate replaceStackPortPrefix helper

    An identical helper already exists in packages/template/src/lib/stack-app/apps/implementations/common.ts. Consider extracting it into src/lib/env.ts (or a shared utils file) and importing it from both call sites to avoid the two implementations drifting apart.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

  4. packages/template/src/lib/stack-app/url-targets.ts, line 2606-2663 (link)

    P2 { type: "hosted" } resolves "after-*" and "home" to the hosted handler root

    home, afterSignIn, afterSignUp, and afterSignOut all return an empty page path "", which means getHostedHandlerUrl produces https://<projectId>.<domain>/handler for every one of them when default: { type: "hosted" } is set. In normal cross-domain flows the after_auth_return_to param overrides these — so afterSignIn/afterSignUp are fine. But afterSignOut is resolved and used directly as the post-sign-out destination when after_auth_return_to is absent (e.g. server-side sign-out paths). Users would land on the hosted handler root page, which may show a "not found" or empty handler screen rather than the app's home page.

    Consider returning / (or the local home URL) as the fallback page path for these non-navigable handler names when the hosted type is active, or document this known limitation clearly.

  5. apps/e2e/tests/js/cross-domain-auth.test.ts, line 988-990 (link)

    P2 _createCrossDomainAuthRedirectUrl is mocked in all redirect tests

    All five cross-domain redirect tests (including this one) spy on _createCrossDomainAuthRedirectUrl and replace it with a mock. This means no test exercises the real HTTP call end-to-end — in particular, whether the x-stack-refresh-token header is correctly forwarded by sendClientRequest. The backend route explicitly requires this header (and returns 400 if it's missing). Consider adding at least one integration test where the mock is not applied, or add a unit test that inspects the actual headers sent.

Last reviewed commit: ac1aa0f

Copy link
Copy Markdown
Contributor

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

Adds first-class support for hosted (cross-domain) handler URLs in the JS SDK/template, including a secure cross-domain OAuth handoff flow that reuses an existing refresh-token session, plus a centralized env-var access layer.

Changes:

  • Introduce URL target resolution (hosted vs handler-component) and hosted handler URL templating for all handler endpoints.
  • Implement cross-domain auth handoff plumbing (redirect URL planning, authorize endpoint, refresh-token reuse via authorization codes) with accompanying unit/e2e coverage.
  • Centralize env-var reads via envVars and enforce via eslint; update template components to use it.

Reviewed changes

Copilot reviewed 30 out of 30 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/template/src/lib/stack-app/url-targets.ts Resolves handler URL targets (hosted vs local component) into concrete URLs.
packages/template/src/lib/stack-app/url-targets.test.ts Unit tests for URL target resolution + hosted template handling.
packages/template/src/lib/stack-app/index.ts Re-export new URL target/option/resolved types.
packages/template/src/lib/stack-app/common.ts Introduce HandlerUrlTarget/HandlerUrlOptions/ResolvedHandlerUrls types.
packages/template/src/lib/stack-app/apps/interfaces/client-app.ts Update client app options + urls typing to use resolved URLs.
packages/template/src/lib/stack-app/apps/implementations/redirect-page-urls.ts Central redirect planning + cross-domain handoff URL construction utilities.
packages/template/src/lib/stack-app/apps/implementations/common.ts Switch URL building to resolveHandlerUrls; move env reads to envVars.
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts Wire hosted URL trust, dynamic redirect-ready app.urls, and cross-domain authorize execution.
packages/template/src/lib/env.ts Add centralized, bundler-friendly env-var getters (envVars).
packages/template/src/lib/auth.ts Plumb afterCallbackRedirectUrl through OAuth URL creation.
packages/template/src/components-page/stack-handler-client.tsx Support redirecting handler pages to hosted targets + unknown path fallback routing.
packages/template/src/components-page/oauth-callback.tsx Replace direct process.env reads with envVars.
packages/template/src/components-page/account-settings/payments/payments-panel.tsx Replace direct process.env reads with envVars.
packages/template/.eslintrc.cjs Enforce envVars usage by banning process.env reads (except env.ts).
examples/demo/src/stack.tsx Demo app configured to use hosted handler defaults.
examples/demo/src/app/cross-domain-handoff/page.tsx New manual cross-domain handoff verification page.
examples/demo/.env.development Add hosted handler URL template for local dev.
claude/CLAUDE-KNOWLEDGE.md Record cross-domain handoff + env-var conventions/notes.
apps/e2e/tests/js/cross-domain-auth.test.ts New JS e2e coverage for hosted redirect + cross-domain handoff behavior.
apps/e2e/tests/backend/endpoints/api/v1/auth/oauth/cross-domain-authorize.test.ts New backend e2e coverage for cross-domain authorize + PKCE exchange + validation.
apps/e2e/tests/backend/endpoints/api/v1/auth/oauth/authorize.test.ts Add coverage for invalid/untrusted after_callback_redirect_url.
apps/e2e/.env.development Configure local hosted handler domain suffix for e2e env.
apps/dashboard/.env.development Configure local hosted handler domain suffix for dashboard dev env.
apps/backend/src/oauth/model.tsx Add refresh-token reuse via authorization codes (grantedRefreshTokenId).
apps/backend/src/app/api/latest/auth/oauth/cross-domain/authorize/route.tsx New endpoint to mint cross-domain authorize redirects gated by refresh-token-bound session.
apps/backend/src/app/api/latest/auth/oauth/authorize/[provider_id]/route.tsx Validate + whitelist after_callback_redirect_url.
apps/backend/prisma/schema.prisma Add grantedRefreshTokenId to ProjectUserAuthorizationCode.
apps/backend/prisma/migrations/20260310150000_add_oauth_authorization_code_refresh_token_id/tests/preserve-null-and-allow-uuid.ts Migration test ensuring null preservation + UUID acceptance.
apps/backend/prisma/migrations/20260310150000_add_oauth_authorization_code_refresh_token_id/migration.sql Migration adding grantedRefreshTokenId column.
apps/backend/.env.development Configure local hosted handler domain suffix for backend dev env.

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

Copy link
Copy Markdown
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: 6

🧹 Nitpick comments (3)
apps/e2e/tests/backend/endpoints/api/v1/auth/oauth/authorize.test.ts (1)

202-255: Add a positive test for accepted native-app redirect URLs.

These additions cover invalid and untrusted values well; please also add a success case for after_callback_redirect_url using the accepted native-app scheme so the allowlist path is locked by tests.

Suggested test shape
+it("should allow accepted native-app after_callback_redirect_url", async ({ expect }) => {
+  const response = await niceBackendFetch("/api/v1/auth/oauth/authorize/spotify", {
+    redirect: "manual",
+    query: {
+      ...await Auth.OAuth.getAuthorizeQuery(),
+      after_callback_redirect_url: "stack-auth-mobile-oauth-url://post-auth",
+    },
+  });
+  expect(response.status).toBe(307);
+});

As per coding guidelines, **/apps/e2e/**/*.{ts,tsx}: "ALWAYS add new E2E tests when you change the API or SDK interface. Generally, err on the side of creating too many tests."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/e2e/tests/backend/endpoints/api/v1/auth/oauth/authorize.test.ts` around
lines 202 - 255, Add a positive E2E test in the same file to verify the
allowlist accepts native-app redirect schemes: create an it block (e.g.,
it("should accept native-app after_callback_redirect_url", ...)) that calls
niceBackendFetch("/api/v1/auth/oauth/authorize/spotify", { redirect: "manual",
query: { ...await Auth.OAuth.getAuthorizeQuery(), after_callback_redirect_url:
"com.stack.app://post-auth" } }) and assert the response is a successful
redirect (e.g., status 302) and does not return a
REDIRECT_URL_NOT_WHITELISTED/SCHEMA_ERROR; use the existing helpers
Auth.OAuth.getAuthorizeQuery and niceBackendFetch so the new test mirrors the
invalid/untrusted tests and locks the allowlist path.
apps/e2e/tests/js/cross-domain-auth.test.ts (1)

51-60: Replace as any type casts with vi.stubGlobal() and remove private method coupling.

The mocks bypass type guarantees without justification and lack required explanatory comments. Use vi.stubGlobal() instead, which is the established pattern in apps/e2e/tests/js/cookies.test.ts. Additionally, spying on _createCrossDomainAuthRedirectUrl (a private method) creates brittle test-to-implementation coupling; instead, mock the request boundary.

Affects lines: 51-60, 92-99, 132-138, 177-179, 181-190, 233-235, 237-246

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/e2e/tests/js/cross-domain-auth.test.ts` around lines 51 - 60, Replace
unsafe "as any" global assignments and the private-method spy with stable stubs:
use vi.stubGlobal() to mock document and window (matching the pattern in
apps/e2e/tests/js/cookies.test.ts) instead of casting to any, and remove the spy
on the private method _createCrossDomainAuthRedirectUrl; instead, mock the
external request boundary (the function or module that makes the HTTP/auth
request) so tests inspect the resulting behavior (redirect assignment/exception)
without coupling to a private implementation detail. Ensure comments explain why
vi.stubGlobal is used and which external boundary is being mocked.
examples/demo/src/app/cross-domain-handoff/page.tsx (1)

68-90: Pass the async handlers straight to Button.

These wrappers duplicate the button’s built-in async handling and make the page harder to read. Letting Button receive the async function directly keeps the loading/error behavior intact without the extra indirection.

♻️ Minimal cleanup
-              <Button
-                onClick={() => runAsynchronouslyWithAlert(async () => {
-                  await app.redirectToSignIn();
-                })}
-              >
+              <Button
+                onClick={async () => {
+                  await app.redirectToSignIn();
+                }}
+              >

Apply the same pattern to the other Button handlers on this page.

Based on learnings: In the stack-auth codebase, Button components from stackframe/stack-ui can accept async functions directly and automatically handle loading states with indicators. No need to wrap these with `runAsynchronouslyWithAlert`.

Also applies to: 108-129, 141-145

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/demo/src/app/cross-domain-handoff/page.tsx` around lines 68 - 90,
The Button handlers are wrapped unnecessarily with runAsynchronouslyWithAlert,
duplicating the Button component’s built-in async handling; replace each
onClick={() => runAsynchronouslyWithAlert(async () => { ... })} with a direct
async handler reference so Button receives the async function directly (e.g.,
pass async () => await user?.signOut({ redirectUrl: "/cross-domain-handoff" }),
async () => await app.redirectToSignIn(), and async () => await
app.redirectToSignUp()) so the Button component can manage loading/error UI
itself; update the other Button usages that currently wrap
runAsynchronouslyWithAlert (including the similar handlers elsewhere on the
page) to the same direct-async pattern.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/backend/src/oauth/model.tsx`:
- Around line 126-131: Replace the throw of StackAssertionError in the
refresh-token ownership check with the deterministic OAuth/auth error type used
by the OAuth routes (so the branch maps to a 401/invalid auth response instead
of an assertion). Specifically, in the block that inspects refreshTokenObj and
compares refreshTokenObj.projectUserId to user.id, remove the
StackAssertionError throw and throw or return the project's mapped auth error
(the same class or factory used elsewhere for invalid credentials/invalid_grant
responses in OAuth handling) so callers receive a consistent auth failure
instead of an unhandled server assertion.

In `@claude/CLAUDE-KNOWLEDGE.md`:
- Around line 82-110: The knowledge entry currently contradicts itself about
carrying refreshTokenId in the URL vs DB; update the text to keep only the
DB-column approach: remove references that encode `refreshTokenId` into
`afterCallbackRedirectUrl` and any mention of decoding it in
`OAuthModel._getOrCreateRefreshTokenObj` or sanitizing
`after_callback_redirect_url` in `saveToken`, and instead state that
refresh-token linkage is stored in
`ProjectUserAuthorizationCode.grantedRefreshTokenId` and returned via
`getAuthorizationCode` so token issuance can reuse it; keep mentions of
`afterCallbackRedirectUrl` as URL-only and references to
`planRedirectToHandler`/`redirect-page-urls.ts` behaviors as-is.

In `@packages/template/src/components-page/oauth-callback.tsx`:
- Around line 30-31: The stale closure `error` is being used in the redirect
check inside the OAuth callback, causing failed callbacks to still redirect;
update the logic in the handler that calls app.redirectToSignIn to read the
current/local error value from this execution (e.g., capture the callback result
into a local variable like `currentError` or `callbackError`) and use an
explicit null/undefined check (use `== null` rather than `!error`) together with
`hasRedirected` and the NODE_ENV condition before calling
`app.redirectToSignIn({ noRedirectBack: true })`; locate and update the redirect
condition that references `error` and `hasRedirected` (the block around the
OAuth callback/redirect logic) and ensure the local variable is used so the
redirect happens only when there is no actual callback error.

In `@packages/template/src/lib/auth.ts`:
- Line 26: Normalize and validate options.afterCallbackRedirectUrl before
passing it into getOAuthUrl the same way redirectUrl and errorRedirectUrl are
handled: locate where getOAuthUrl is invoked with afterCallbackRedirectUrl
(symbol: afterCallbackRedirectUrl) in auth.ts and run it through the existing
normalization/validation routine used for redirectUrl/errorRedirectUrl (or call
the same helper/validator), and if validation fails throw an error immediately
so the function fails fast rather than passing a raw value to getOAuthUrl.

In `@packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts`:
- Around line 2207-2215: The helper _getLocalOAuthCallbackHandlerUrl currently
only overrides the default target but lets callers' urls.oauthCallback
(including hosted or absolute values) pass through, causing cross-origin
callback resolution errors; fix by forcing urls.oauthCallback to a local handler
value before calling resolveHandlerUrls (e.g. set urls.oauthCallback = { type:
"handler-component" } while still spreading this._urlOptions for other keys), so
resolveHandlerUrls(...) always returns a same-origin oauthCallback; update the
call in _getLocalOAuthCallbackHandlerUrl to merge this forced oauthCallback
override and keep projectId unchanged.
- Around line 2223-2245: The code currently always parses the response into the
success shape and throws a StackAssertionError on unexpected payloads, which
hides real HTTP errors; modify the block after calling
this._interface.sendClientRequest to first check response.ok and, if false,
route the response through the normal client error path (e.g., call your
existing error handler on the interface or rethrow a parsed client error using
the response and session) instead of continuing to parse and potentially
throwing StackAssertionError; only parse JSON and validate redirect_url when
response.ok is true.

---

Nitpick comments:
In `@apps/e2e/tests/backend/endpoints/api/v1/auth/oauth/authorize.test.ts`:
- Around line 202-255: Add a positive E2E test in the same file to verify the
allowlist accepts native-app redirect schemes: create an it block (e.g.,
it("should accept native-app after_callback_redirect_url", ...)) that calls
niceBackendFetch("/api/v1/auth/oauth/authorize/spotify", { redirect: "manual",
query: { ...await Auth.OAuth.getAuthorizeQuery(), after_callback_redirect_url:
"com.stack.app://post-auth" } }) and assert the response is a successful
redirect (e.g., status 302) and does not return a
REDIRECT_URL_NOT_WHITELISTED/SCHEMA_ERROR; use the existing helpers
Auth.OAuth.getAuthorizeQuery and niceBackendFetch so the new test mirrors the
invalid/untrusted tests and locks the allowlist path.

In `@apps/e2e/tests/js/cross-domain-auth.test.ts`:
- Around line 51-60: Replace unsafe "as any" global assignments and the
private-method spy with stable stubs: use vi.stubGlobal() to mock document and
window (matching the pattern in apps/e2e/tests/js/cookies.test.ts) instead of
casting to any, and remove the spy on the private method
_createCrossDomainAuthRedirectUrl; instead, mock the external request boundary
(the function or module that makes the HTTP/auth request) so tests inspect the
resulting behavior (redirect assignment/exception) without coupling to a private
implementation detail. Ensure comments explain why vi.stubGlobal is used and
which external boundary is being mocked.

In `@examples/demo/src/app/cross-domain-handoff/page.tsx`:
- Around line 68-90: The Button handlers are wrapped unnecessarily with
runAsynchronouslyWithAlert, duplicating the Button component’s built-in async
handling; replace each onClick={() => runAsynchronouslyWithAlert(async () => {
... })} with a direct async handler reference so Button receives the async
function directly (e.g., pass async () => await user?.signOut({ redirectUrl:
"/cross-domain-handoff" }), async () => await app.redirectToSignIn(), and async
() => await app.redirectToSignUp()) so the Button component can manage
loading/error UI itself; update the other Button usages that currently wrap
runAsynchronouslyWithAlert (including the similar handlers elsewhere on the
page) to the same direct-async pattern.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 82048621-079a-4807-871c-ecbeae81c2f7

📥 Commits

Reviewing files that changed from the base of the PR and between 132cc6e and ac1aa0f.

📒 Files selected for processing (30)
  • apps/backend/.env.development
  • apps/backend/prisma/migrations/20260310150000_add_oauth_authorization_code_refresh_token_id/migration.sql
  • apps/backend/prisma/migrations/20260310150000_add_oauth_authorization_code_refresh_token_id/tests/preserve-null-and-allow-uuid.ts
  • apps/backend/prisma/schema.prisma
  • apps/backend/src/app/api/latest/auth/oauth/authorize/[provider_id]/route.tsx
  • apps/backend/src/app/api/latest/auth/oauth/cross-domain/authorize/route.tsx
  • apps/backend/src/oauth/model.tsx
  • apps/dashboard/.env.development
  • apps/e2e/.env.development
  • apps/e2e/tests/backend/endpoints/api/v1/auth/oauth/authorize.test.ts
  • apps/e2e/tests/backend/endpoints/api/v1/auth/oauth/cross-domain-authorize.test.ts
  • apps/e2e/tests/js/cross-domain-auth.test.ts
  • claude/CLAUDE-KNOWLEDGE.md
  • examples/demo/.env.development
  • examples/demo/src/app/cross-domain-handoff/page.tsx
  • examples/demo/src/stack.tsx
  • packages/template/.eslintrc.cjs
  • packages/template/src/components-page/account-settings/payments/payments-panel.tsx
  • packages/template/src/components-page/oauth-callback.tsx
  • packages/template/src/components-page/stack-handler-client.tsx
  • packages/template/src/lib/auth.ts
  • packages/template/src/lib/env.ts
  • packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
  • packages/template/src/lib/stack-app/apps/implementations/common.ts
  • packages/template/src/lib/stack-app/apps/implementations/redirect-page-urls.ts
  • packages/template/src/lib/stack-app/apps/interfaces/client-app.ts
  • packages/template/src/lib/stack-app/common.ts
  • packages/template/src/lib/stack-app/index.ts
  • packages/template/src/lib/stack-app/url-targets.test.ts
  • packages/template/src/lib/stack-app/url-targets.ts

Copy link
Copy Markdown
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/apps/implementations/client-app-impl.ts (1)

2238-2260: ⚠️ Potential issue | 🟠 Major

Surface /auth/oauth/cross-domain/authorize failures as client errors, not assertions.

A 400/401 from this endpoint is still converted into StackAssertionError, so callers lose the original KnownErrors/status information and the redirect flow only sees a generic failure. Please route non-2xx responses through the normal interface error path before validating redirect_url.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts`
around lines 2238 - 2260, The code currently throws a StackAssertionError when
response.ok is false; instead route non-2xx responses through the interface's
normal client-error path before validating the payload. Replace the current
!response.ok branch in the sendClientRequest response handling so that you call
the interface's error conversion/propagation helper (e.g. a method on
this._interface such as
createClientErrorFromResponse/convertResponseToError/throwIfResponseError) with
the response and session and rethrow that result, rather than constructing a
StackAssertionError; keep the subsequent JSON parsing and redirect_url
validation only for successful responses. Ensure you remove the
StackAssertionError throw in that branch and use the interface method so
KnownErrors/status information is preserved.
🧹 Nitpick comments (1)
.cursor/commands/pr-comments-review.md (1)

1-1: Consider more professional language for documentation.

The phrase "mostly bullshit" is informal and may not be appropriate for command documentation, even for internal tooling. Consider replacing it with more professional alternatives like "not actionable," "invalid," or "low-priority."

📝 Suggested rewording
-Please review the PR comments with the `gh` CLI and fix those issues that are valid and relevant. Resolve the comments when you fix them. Also resolve all those comments that no longer exist or have already been resolved. Leave those comments that are mostly bullshit unresolved. Also, create or modify tests to make sure the fixed behavior works as expected. Report the result to me in detail (for the most important issues, whether resolved or unresolved, mark them with a ‼️ emoji). Do NOT automatically commit or stage the changes back to the PR!
+Please review the PR comments with the `gh` CLI and address those issues that are valid and relevant. Resolve the comments when you fix them. Also resolve all those comments that no longer exist or have already been resolved. Leave comments that are not actionable or low-priority unresolved. Also, create or modify tests to make sure the fixed behavior works as expected. Report the result to me in detail (for the most important issues, whether resolved or unresolved, mark them with a ‼️ emoji). Do NOT automatically commit or stage the changes back to the PR!

Note: Also incorporated the static analysis suggestion to use "address" instead of "fix" for a more formal tone.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.cursor/commands/pr-comments-review.md at line 1, Replace the informal
phrase "mostly bullshit" in the documentation for the pr-comments-review command
with a more professional alternative (e.g., "not actionable", "invalid", or
"low-priority") and change wording from "fix" to "address" where suggested;
locate the text by searching for the exact string "mostly bullshit" in
.cursor/commands/pr-comments-review.md and update surrounding sentences to keep
tone consistent, then update any doc/help output tests that assert the command's
messaging (tests referencing pr-comments-review help text or the exact phrase)
so they assert the new phrasing; ensure to resolve the PR comment after making
these documentation and test updates.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts`:
- Around line 535-536: The constructor currently calls
_prefetchCrossDomainHandoffParamsIfNeeded() unconditionally which causes
saveVerifierAndState() to generate and store a fresh verifier/state even on
OAuth callback pages; update _prefetchCrossDomainHandoffParamsIfNeeded() (or the
constructor call) to first detect if the current URL contains OAuth callback
query parameters (both "code" and "state") and if so, return immediately (no
saveVerifierAndState call). Ensure this change touches the places mentioned (the
constructor invocation at the top-level and the code paths around
callOAuthCallback(), and the helper logic where saveVerifierAndState() is
invoked) so prefetch is skipped when code+state are present to avoid creating
unrelated verifier/state pairs.

In `@packages/template/src/lib/stack-app/apps/implementations/event-tracker.ts`:
- Around line 45-55: The null/undefined checks around reflective reads are
insufficient: update the guard in event-tracker.ts to explicitly verify
screenObject != null before calling Reflect.get(screenObject,
"width")/("height") (replace the typeof screenObject !== "object" check) and
likewise ensure historyObject != null before calling Reflect.get(historyObject,
"pushState") so reflective reads never run on null/undefined; adjust the
conditions around the existing Reflect.get calls (referencing screenObject,
historyObject, and Reflect.get) to short-circuit earlier when either object is
null or undefined.

---

Duplicate comments:
In `@packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts`:
- Around line 2238-2260: The code currently throws a StackAssertionError when
response.ok is false; instead route non-2xx responses through the interface's
normal client-error path before validating the payload. Replace the current
!response.ok branch in the sendClientRequest response handling so that you call
the interface's error conversion/propagation helper (e.g. a method on
this._interface such as
createClientErrorFromResponse/convertResponseToError/throwIfResponseError) with
the response and session and rethrow that result, rather than constructing a
StackAssertionError; keep the subsequent JSON parsing and redirect_url
validation only for successful responses. Ensure you remove the
StackAssertionError throw in that branch and use the interface method so
KnownErrors/status information is preserved.

---

Nitpick comments:
In @.cursor/commands/pr-comments-review.md:
- Line 1: Replace the informal phrase "mostly bullshit" in the documentation for
the pr-comments-review command with a more professional alternative (e.g., "not
actionable", "invalid", or "low-priority") and change wording from "fix" to
"address" where suggested; locate the text by searching for the exact string
"mostly bullshit" in .cursor/commands/pr-comments-review.md and update
surrounding sentences to keep tone consistent, then update any doc/help output
tests that assert the command's messaging (tests referencing pr-comments-review
help text or the exact phrase) so they assert the new phrasing; ensure to
resolve the PR comment after making these documentation and test updates.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 02fac0d2-9b1a-45f1-98e7-757f6c1028fc

📥 Commits

Reviewing files that changed from the base of the PR and between ac1aa0f and ea158be.

📒 Files selected for processing (9)
  • .cursor/commands/pr-comments-review.md
  • apps/backend/src/app/api/latest/auth/oauth/cross-domain/authorize/route.tsx
  • apps/backend/src/oauth/model.tsx
  • claude/CLAUDE-KNOWLEDGE.md
  • packages/template/src/components-page/oauth-callback.tsx
  • packages/template/src/lib/auth.ts
  • packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
  • packages/template/src/lib/stack-app/apps/implementations/event-tracker.ts
  • packages/template/src/lib/stack-app/apps/implementations/redirect-page-urls.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/template/src/lib/auth.ts
  • apps/backend/src/oauth/model.tsx
  • claude/CLAUDE-KNOWLEDGE.md

Copy link
Copy Markdown
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.

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

536-536: Consider skipping prefetch on OAuth callback pages.

The constructor unconditionally calls _prefetchCrossDomainHandoffParamsIfNeeded(). On OAuth callback URLs (containing code and state query parameters), this creates unnecessary verifier/state cookies that won't be used, as callOAuthCallback() consumes the verifier keyed by the state from the URL.

While this doesn't break functionality (each state has its own cookie key), it's wasteful and could be avoided:

♻️ Optional: Skip prefetch on callback URLs
 protected _prefetchCrossDomainHandoffParamsIfNeeded() {
   const canWriteOauthVerifierCookie = this._tokenStoreInit === "cookie" || this._tokenStoreInit === "nextjs-cookie";
   if (
     !isBrowserLike()
     || !canWriteOauthVerifierCookie
     || this._isPrefetchingCrossDomainHandoffParams
     || this._getFreshPrefetchedCrossDomainHandoffParams() != null
   ) {
     return;
   }
+  // Skip prefetch on OAuth callback pages - the existing verifier will be consumed
+  const currentUrl = new URL(window.location.href);
+  if (currentUrl.searchParams.has("code") && currentUrl.searchParams.has("state")) {
+    return;
+  }
   this._isPrefetchingCrossDomainHandoffParams = true;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts`
at line 536, The constructor currently always calls
_prefetchCrossDomainHandoffParamsIfNeeded(), which needlessly creates
verifier/state cookies on OAuth callback pages; update the logic to detect OAuth
callback URLs (presence of both "code" and "state" query parameters) and skip
calling _prefetchCrossDomainHandoffParamsIfNeeded() in that case (or add an
early return inside _prefetchCrossDomainHandoffParamsIfNeeded() that checks for
code+state). Ensure callOAuthCallback() continues to read the verifier keyed by
the state from the URL and that no other behavior is changed.

2352-2354: API failures are now surfaced, but consider exposing the actual error.

The code now checks response.ok before parsing, addressing the previous concern. The error message includes status and response text, which is helpful for debugging.

However, the backend may return structured error responses (e.g., KnownErrors) that are currently collapsed into a generic StackAssertionError. Consider parsing and rethrowing the actual error type for better error handling upstream.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts`
around lines 2352 - 2354, The current check throws a generic StackAssertionError
using response.status and text which loses structured backend errors; update the
error path where response.ok is false (around the response handling that throws
StackAssertionError) to attempt parsing the response body as JSON, detect known
error shapes (e.g., fields like name, code, message or the backend's KnownErrors
shape), and rethrow an appropriate typed error (or reconstruct the backend
error) instead of always throwing StackAssertionError; if parsing fails or the
payload doesn't match a known shape, then fall back to throwing
StackAssertionError including status and raw text. Ensure this logic is applied
where response is inspected and StackAssertionError is thrown so upstream code
can handle specific error types.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts`:
- Line 536: The constructor currently always calls
_prefetchCrossDomainHandoffParamsIfNeeded(), which needlessly creates
verifier/state cookies on OAuth callback pages; update the logic to detect OAuth
callback URLs (presence of both "code" and "state" query parameters) and skip
calling _prefetchCrossDomainHandoffParamsIfNeeded() in that case (or add an
early return inside _prefetchCrossDomainHandoffParamsIfNeeded() that checks for
code+state). Ensure callOAuthCallback() continues to read the verifier keyed by
the state from the URL and that no other behavior is changed.
- Around line 2352-2354: The current check throws a generic StackAssertionError
using response.status and text which loses structured backend errors; update the
error path where response.ok is false (around the response handling that throws
StackAssertionError) to attempt parsing the response body as JSON, detect known
error shapes (e.g., fields like name, code, message or the backend's KnownErrors
shape), and rethrow an appropriate typed error (or reconstruct the backend
error) instead of always throwing StackAssertionError; if parsing fails or the
payload doesn't match a known shape, then fall back to throwing
StackAssertionError including status and raw text. Ensure this logic is applied
where response is inspected and StackAssertionError is thrown so upstream code
can handle specific error types.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0cd5db2e-da6e-42af-81ee-f92efcbcd03d

📥 Commits

Reviewing files that changed from the base of the PR and between ea158be and 712c3aa.

📒 Files selected for processing (10)
  • apps/backend/.env.development
  • apps/backend/prisma/schema.prisma
  • apps/backend/src/app/api/latest/auth/oauth/authorize/[provider_id]/route.tsx
  • apps/e2e/tests/backend/endpoints/api/v1/auth/oauth/authorize.test.ts
  • claude/CLAUDE-KNOWLEDGE.md
  • examples/demo/.env.development
  • examples/demo/src/stack.tsx
  • packages/template/src/lib/auth.ts
  • packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
  • packages/template/src/lib/stack-app/apps/interfaces/client-app.ts
💤 Files with no reviewable changes (1)
  • packages/template/src/lib/auth.ts
✅ Files skipped from review due to trivial changes (1)
  • examples/demo/src/stack.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/e2e/tests/backend/endpoints/api/v1/auth/oauth/authorize.test.ts
  • apps/backend/src/app/api/latest/auth/oauth/authorize/[provider_id]/route.tsx
  • apps/backend/prisma/schema.prisma
  • claude/CLAUDE-KNOWLEDGE.md

Copy link
Copy Markdown
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/apps/implementations/client-app-impl.ts (1)

2263-2285: ⚠️ Potential issue | 🟡 Minor

Skip handoff prefetch on OAuth callback pages.

This still calls saveVerifierAndState() on pages that already have code and state in the URL. On those pages, callOAuthCallback() can only consume the verifier keyed by the callback state, so the extra pair is dead state at best and makes this auth path harder to reason about. Return early when the current URL already contains both callback params.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts`
around lines 2263 - 2285, In _prefetchCrossDomainHandoffParamsIfNeeded, avoid
calling saveVerifierAndState on OAuth callback pages by returning early if the
current browser URL already contains both "code" and "state" query params; add a
check (after isBrowserLike() and before setting
_isPrefetchingCrossDomainHandoffParams / invoking runAsynchronously) that
inspects window.location.search via URLSearchParams for "code" and "state" and
returns if both are present so callOAuthCallback can consume the existing
verifier without creating a stray verifier/state pair.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts`:
- Around line 2246-2260: The urls getter currently returns plain resolved URLs
during SSR or while async prefetch hasn't produced PKCE/handoff state, which
lets initial <a href> links miss required cross-domain params; update the urls
getter (method: urls) so that on the client (when typeof window !== "undefined")
it guarantees handoff params synchronously before calling
resolveAppUrlsForCurrentPage: call
_getCrossDomainHandoffParamsForUrlsGetter(currentUrl) and if it returns null,
synchronously generate the minimal PKCE/handoff state (reuse the same logic used
by redirect flows) or fallback to a deterministic local handoff token, then call
resolveAppUrlsForCurrentPage with that non-null crossDomainHandoffParams and
_getLocalOAuthCallbackHandlerUrl so signIn/signUp/onboarding/signOut hrefs
always include handoff params on first render.
- Around line 2356-2360: The code uses the 'in' operator on result without
ensuring result is an object, which can throw if response.json() returned null,
a primitive, an array, or if JSON parsing failed; wrap the response.json() call
in a try/catch to convert JSON parse errors into a StackAssertionError, then
validate that result is a non-null plain object (typeof result === "object" &&
result !== null && !Array.isArray(result)) before checking ("redirect_url" in
result) and typeof result.redirect_url === "string"; throw a StackAssertionError
with the original result or caught error details when validation fails
(referencing response.json(), the local variable result, and
StackAssertionError).

---

Duplicate comments:
In `@packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts`:
- Around line 2263-2285: In _prefetchCrossDomainHandoffParamsIfNeeded, avoid
calling saveVerifierAndState on OAuth callback pages by returning early if the
current browser URL already contains both "code" and "state" query params; add a
check (after isBrowserLike() and before setting
_isPrefetchingCrossDomainHandoffParams / invoking runAsynchronously) that
inspects window.location.search via URLSearchParams for "code" and "state" and
returns if both are present so callOAuthCallback can consume the existing
verifier without creating a stray verifier/state pair.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9fa1c981-5c63-41b1-8c53-882fd987518a

📥 Commits

Reviewing files that changed from the base of the PR and between 712c3aa and 8e75673.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (4)
  • claude/CLAUDE-KNOWLEDGE.md
  • packages/template/src/lib/env.ts
  • packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
  • packages/template/src/lib/stack-app/apps/implementations/event-tracker.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/template/src/lib/env.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/template/src/lib/stack-app/apps/implementations/event-tracker.ts
  • claude/CLAUDE-KNOWLEDGE.md

Copy link
Copy Markdown
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/url-targets.ts (1)

209-216: Build the hosted URL with encoded URL parts instead of raw replacement.

pagePath is inserted into the final URL verbatim here. That makes reserved characters alter the URL structure instead of staying inside the path, and it also bypasses the repo’s URL-construction convention. Please assemble this with urlString/encodeURIComponent() or URL path setters rather than raw template replacement.

As per coding guidelines: use urlString or encodeURIComponent() instead of normal string interpolation for URLs for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/template/src/lib/stack-app/url-targets.ts` around lines 209 - 216,
The getHostedHandlerUrl function currently injects options.pagePath directly
into templateFilled causing reserved characters to break URL structure; update
getHostedHandlerUrl to encode URL parts instead of raw replacement: construct
hostedPath from options.pagePath but pass its segments through
encodeURIComponent (or use a URL instance and set pathname/path segments) and
replace hostedHandlerPathPlaceholder and hostedHandlerProjectIdPlaceholder with
encoded values so the final new URL(templateFilled) receives a valid, encoded
URL string; ensure leading slashes are normalized before encoding so path
segments remain correct.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/template/src/lib/stack-app/url-targets.ts`:
- Around line 178-190: The hosted URL template accepted by
getHostedHandlerUrlTemplate currently only checks for the presence of
placeholders hostedHandlerProjectIdPlaceholder and hostedHandlerPathPlaceholder
(via NEXT_PUBLIC_STACK_HOSTED_HANDLER_URL_TEMPLATE) but later
isHostedHandlerUrlForProject trusts only origin + pathname prefix; fix by
restricting/validating the template shape to a form the trust check can verify
(e.g., enforce a scheme + host + path pattern like
https://{projectId}.domain/{hostedPath} or ensure no query parameter placement
of placeholders), or alternatively change isHostedHandlerUrlForProject to
compare against the fully expanded template including query parameters; update
getHostedHandlerUrlTemplate to reject templates that place placeholders in query
strings or other unsupported locations and add clear error hint referencing
NEXT_PUBLIC_STACK_HOSTED_HANDLER_URL_TEMPLATE, hostedHandlerProjectIdPlaceholder
and hostedHandlerPathPlaceholder so only templates that match the
origin+pathname heuristic are allowed.
- Around line 192-206: resolveCustomTargetUrl currently rejects targets whose
handlerName isn't present in customPagePrompts before honoring the legacy
fast-path for version 0, which makes { handler: { type: "custom", url, version:
0 } } fail; update resolveCustomTargetUrl to short-circuit and return
options.target.url when options.target.version === 0 (or specifically when
handlerName === "handler" && options.target.version === 0) before checking
customPagePrompts, otherwise keep the existing behavior: if handlerName exists
in customPagePrompts validate the version against customPagePrompt.versions and
throw the existing unsupported-version error, and if not present (and not the v0
legacy case) throw the "cannot be a custom page" error.

---

Nitpick comments:
In `@packages/template/src/lib/stack-app/url-targets.ts`:
- Around line 209-216: The getHostedHandlerUrl function currently injects
options.pagePath directly into templateFilled causing reserved characters to
break URL structure; update getHostedHandlerUrl to encode URL parts instead of
raw replacement: construct hostedPath from options.pagePath but pass its
segments through encodeURIComponent (or use a URL instance and set pathname/path
segments) and replace hostedHandlerPathPlaceholder and
hostedHandlerProjectIdPlaceholder with encoded values so the final new
URL(templateFilled) receives a valid, encoded URL string; ensure leading slashes
are normalized before encoding so path segments remain correct.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 477fb853-8ad5-4238-9c31-bf8b39cc5daf

📥 Commits

Reviewing files that changed from the base of the PR and between 8e75673 and 766fd02.

📒 Files selected for processing (5)
  • claude/CLAUDE-KNOWLEDGE.md
  • packages/template/src/lib/stack-app/common.ts
  • packages/template/src/lib/stack-app/index.ts
  • packages/template/src/lib/stack-app/url-targets.test.ts
  • packages/template/src/lib/stack-app/url-targets.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/template/src/lib/stack-app/url-targets.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/template/src/lib/stack-app/index.ts
  • packages/template/src/lib/stack-app/common.ts

@N2D4 N2D4 merged commit 5bfe1a7 into dev Mar 27, 2026
21 of 28 checks passed
@N2D4 N2D4 deleted the hosted-components-handoff branch March 27, 2026 21:48
Copy link
Copy Markdown

@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 1 potential issue.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.


this._prefetchCrossDomainHandoffParamsIfNeeded();
return null;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unused method left behind after static urls refactor

Low Severity

_getCrossDomainHandoffParamsForUrlsGetter is defined but never called. The urls getter was made static (confirmed by CLAUDE-KNOWLEDGE.md: "app.urls is now static and no longer injects runtime params"), so this method — which combines query-param parsing with prefetch fallback logic for the old dynamic urls getter — is dead code. The redirect flow uses _getCrossDomainHandoffParamsForRedirect instead.

Fix in Cursor Fix in Web

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