Tags: hexclave/stack-auth
Tags
small component ui fix (#1414) <!-- Make sure you've read the CONTRIBUTING.md guidelines: https://github.com/stack-auth/stack-auth/blob/dev/CONTRIBUTING.md --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Refactor** * Enhanced CLI authentication confirmation tracking to improve session persistence and state management during sign-in flows. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
fix team invitation email check + verification code TOCTOU (#1365) ## Summary Two authorization fixes in the backend. Both are pre-existing in `dev` and were found during a security audit of `apps/backend/src`. ### 1. Team invitation accept — email not validated [`team-invitations/accept/verification-code-handler.tsx`](https://github.com/stack-auth/stack-auth/blob/dev/apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx) destructured the invited email as `{}` and only used `data.team_id` + the accepting `user`. Any signed-in user in the tenancy who possessed the 45-char code could join the team as themselves — the invitation was not actually bound to the email it was addressed to. **Attack scenarios that work without this fix** - Forwarded invitation email (shared inbox, assistant inbox, auto-forward rules). - Screenshot of the invitation link pasted into Slack / Notion. - Insider with server-access reading the email outbox (`GET /api/latest/emails/outbox` returns rendered `html` + `variables.teamInvitationLink`). - Stale invite still sitting in spam after the invitee forwarded it elsewhere. **Fix.** The accept handler now requires that the accepting user owns the invited email as a *verified* contact channel on their account. Matches the invariant already used by the "list invitations for me" endpoint ([`team-invitations/crud.tsx:41-66`](https://github.com/stack-auth/stack-auth/blob/dev/apps/backend/src/app/api/latest/team-invitations/crud.tsx#L41-L66)). Rejections return a new `TEAM_INVITATION_EMAIL_MISMATCH` (403) error. ### 2. Verification-code handler TOCTOU [`route-handlers/verification-code-handler.tsx`](https://github.com/stack-auth/stack-auth/blob/dev/apps/backend/src/route-handlers/verification-code-handler.tsx) had a classic read-then-write TOCTOU: ```ts const verificationCode = await prisma.verificationCode.findUnique(...); if (verificationCode.usedAt) throw new KnownErrors.VerificationCodeAlreadyUsed(); // ... validation ... await prisma.verificationCode.update({ data: { usedAt: new Date() } }); // unconditional return await options.handler(...); ``` Five concurrent requests with the same code all pass the `if (usedAt)` gate, all mark the code used, all run the post-handler. For OTP sign-in the handler calls `createAuthTokens` which writes a fresh `projectUserRefreshToken` row per call — so **one OTP → N refresh tokens**. `auth/sessions/current` only revokes by `id: refreshTokenId` and there is no bulk-revoke for passwordless users (only password change in [`users/crud.tsx:1210`](https://github.com/stack-auth/stack-auth/blob/dev/apps/backend/src/app/api/latest/users/crud.tsx#L1210) does `deleteMany`). A phished OTP therefore becomes a session-persistence primitive. **Fix.** Replace the unconditional `update` with a conditional `updateMany({ where: { …, usedAt: null } })` executed before `options.handler`; if `count === 0` the race was already lost and we throw `VERIFICATION_CODE_ALREADY_USED` (409). This also benefits MFA sign-in and passkey sign-in, which share the same handler. ## Changes | File | Change | |---|---| | `team-invitations/accept/verification-code-handler.tsx` | Require verified contact channel matching `method.email` | | `route-handlers/verification-code-handler.tsx` | Atomic `updateMany` claim gated on `usedAt: null` | | `stack-shared/src/known-errors.tsx` | New `TeamInvitationEmailMismatch` (403) | | `e2e/.../team-invitations.test.ts` | Two new tests (mismatch + happy path) | | `e2e/.../auth/otp/sign-in.test.ts` | One new test: 5 parallel redemptions of one OTP → 1× 200 + 4× 409 | ## Test plan - [x] `pnpm test run apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts` — 27/27 pass - [x] `pnpm test run apps/e2e/tests/backend/endpoints/api/v1/auth/otp/sign-in.test.ts` — 12/12 (+ 4 pre-existing `it.todo`) - [x] `pnpm test run apps/e2e/tests/backend/endpoints/api/v1/auth/password` — 33/33 (+ 7 pre-existing todos) - [x] `pnpm test run apps/e2e/tests/backend/endpoints/api/v1/contact-channels` — 24/24 - [x] `pnpm test run apps/e2e/tests/backend/endpoints/api/v1/auth/passkey apps/e2e/tests/backend/endpoints/api/v1/auth/mfa` — 16/16 - [x] `pnpm --filter @stackframe/backend typecheck` — clean - [x] `pnpm --filter @stackframe/backend lint` + `pnpm --filter @stackframe/stack-shared lint` — clean ## Notes - The broader "plaintext credentials in DB + Sentry logs every header" finding from the same audit is **not** in this PR — a scrubber for `Sentry.setContext` request headers + unit tests is prepared on a local stash and will go out as a separate PR. - The team-invitation fix does not require any config change; fresh signups via the OTP / password flows that set `primary_email_verified: true` during creation already land the user with a verified channel matching the invited email, so the happy path is unaffected. ### Follow-up review (Codex) Addressed in follow-up commit `954cddb`: - **Finding 1 (High)**: mismatched invite acceptance was consuming the invitation before rejecting. Moved the email-ownership check into the pre-claim `options.validate` hook so a wrong-email attempt leaves `usedAt` untouched and the real recipient can still redeem. New test asserts this end-to-end. - **Finding 3 (Medium)**: invitation stored `body.email` raw but contact channels are stored via `normalizeEmail`, so case-varied invites (e.g. `Alice@Example.com`) wouldn't match a `alice@example.com` channel. `send-code` now normalizes on storage and `accept` normalizes on compare for back-compat with already-issued invites. New test covers the mixed-case path. - **Finding 2 (partial)**: added `expiresAt > now` to the atomic claim predicate for the boundary case where a code expires between the read and the claim. The reviewer's broader point about the `attemptCount` rate-limit check being non-atomic with its own increment **pre-dates this PR** (it reads the in-memory `verificationCode.attemptCount` from line 150, not a fresh read) and exists independently of the `usedAt` TOCTOU I'm fixing here. Tracking that as a separate follow-up so this PR stays scoped to the two originally-flagged issues. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Invite acceptance now requires the invitee’s verified, normalized (case‑insensitive) email; mismatches return HTTP 403 (TEAM_INVITATION_EMAIL_MISMATCH). * Client APIs now surface the new email-mismatch error alongside verification errors. * **Bug Fixes** * OTP verification codes are now guarded against parallel double‑redeem so only one request succeeds. * **Tests** * Added E2E tests for invitation email validation, non‑consuming rejection, case‑insensitive matching, and OTP concurrency. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
feat(analytics): gzip event batch body to bypass adblockers (#1407) ## Summary The `POST /api/latest/analytics/events/batch` endpoint was being dropped by content-blocking browser extensions (adblockers) because the JSON request body literally contains the substring `$click`. Many filter lists pattern-match on tokens like that and silently kill the request — analytics events from anyone with an adblocker enabled never reached our backend. This PR encodes the request body so keyword-matching filters can't see those tokens, while keeping the URL path unchanged (only the body was being matched here) and keeping older SDK clients working. ## Approach - **Client**: gzip the JSON payload via the browser-native `CompressionStream("gzip")` API and POST it as `application/octet-stream`. Falls back to plain JSON if `CompressionStream` isn't available (very old browsers / non-browser runtimes). - **Server**: a yup `.transform()` on the body schema detects an `ArrayBuffer`/`Uint8Array` input, gunzips it, and `JSON.parse`s before normal schema validation runs. The existing JSON path is untouched, so requests from older SDK versions in the wild continue to work without changes — and all existing schema-error snapshot tests still pass verbatim. - **Safety**: hard caps on compressed (1 MB) and decompressed (8 MB) sizes guard against zip-bomb shaped abuse. `node:zlib`'s `maxOutputLength` enforces the latter at the C++ layer. Bonus: gzip also gives a meaningful bandwidth win — click/page-view events compress very well — and keepalive bodies (which have a 64 KB cap in browsers) get more headroom. ## Files - `apps/backend/src/app/api/latest/analytics/events/batch/route.tsx` — body schema gains `.transform()` that gunzips binary inputs; size limits added; everything else unchanged. - `packages/stack-shared/src/interface/client-interface.ts` — `sendAnalyticsEventBatch` now routes through a new module-level `encodeAnalyticsBody` helper that gzips and switches Content-Type. Same outer signature; encoding is internal. - `apps/e2e/tests/backend/backend-helpers.ts` — `niceBackendFetch` gains optional `rawBody`/`rawContentType` params so tests can send non-JSON payloads. Existing JSON callers unaffected. - `apps/e2e/tests/backend/endpoints/api/v1/analytics-events-batch.test.ts` — adds two tests: - happy path: gzipped binary body returns `inserted: 1` - sad path: garbage bytes return 400 ## Out of scope (intentional) - **URL path renaming**: not all adblockers match on `/analytics/`, but some do. We're shipping the body fix first and will revisit if requests still get blocked after deployment. - **Encryption**: gzip is enough to defeat keyword filters. Encryption adds key-management cost with no real adversary. - **SDK regen**: only `client-interface.ts` (in `stack-shared`) was touched; `event-tracker.ts` (the caller) is unchanged because it already passes a JSON string. No `pnpm -w run generate-sdks` needed. ## Test plan - [x] `pnpm typecheck` — green - [x] `pnpm lint` — green - [ ] Manually verify in dev: enable adblocker, click around with analytics enabled, confirm batch requests now go through - [ ] Spot-check ClickHouse `analytics_internal.events` shows the expected rows - [ ] Run the new e2e tests (`pnpm test run apps/e2e/tests/backend/endpoints/api/v1/analytics-events-batch.test.ts`) and confirm both new cases plus all preexisting snapshots pass - [ ] Confirm the JSON back-compat path still works by hitting the route with the existing JSON-body curl/test payloads <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Analytics batch uploads now accept gzipped binary payloads; clients can send compressed bytes and the server will detect and decompress. * Client sender can gzip event batches (falls back to JSON) and uses keepalive to choose JSON vs compressed bytes. * **Bug Fixes** * Malformed, non-gzip, or overly-large compressed payloads now return a clear 400 response. * **Tests** * Added E2E and unit tests plus test-helper support for raw/gzipped request bodies and encoding behaviors. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
[Refactor][Feat] Implement Plan Limits for Hard-and-Soft Item Caps (#… …1215) ### Suggested Review Areas Please see `plans.ts` and `seed.ts` to verify whether the item caps are where they should be. Outside of that, each commit should be atomic so stepping through the commits should give you an idea of how I implemented each limit. ### Discussion Something to discuss: when a user cancels team/growth we regrant free fine, but any extra-seats they had just keeps billing. So they end up paying ~$29/mo per extra-seat on top of free's 1 seat, which is strictly worse than just staying on team. This surfaced while manually testing this PR, we only enforce the add-on base requirement at purchase time, nothing cascades on cancel. Should we cascade cancel add ons? ### Context Now that we have a stable suite of products for stack-auth, we want to limit the items under each product a customer has access to based on their plan. So for example, a free plan user has a certain amount of emails they can send out each month, and so on. We try to implement limits in this PR. ### Summary of Changes Implemented hard limits for dashboard admins, analytics per-query timeouts, sent email monthly capacity, events, and session replays. Implemented a soft cap for auth users (where if there's a signup beyond the limit, we log it to sentry so we can manually choose to email that user/team). For auth users, we do not block new user sign ups once plan limit has been hit. We also don't degrade or impact the customer experience. It logs to sentry and it is up to us to take manual action to email the user to upgrade the plan. Also, implementation wise, we count all the users across all the projects for this team and compare it to their plan item limit, rather than debiting items like we do for other approaches. As a soft cap, this should be fine plus this is a better source of truth. For email capacity, we operate a monthly limit of emails. Once this is hit, no more emails can be sent until the next month/ a plan upgrade. These emails will be treated as a send error, so they can be manually resent once the capacity is reset. With respect to the `email-queue` state engine, they go from `SENDING`->`SERVER_ERROR`, hooking into the existing state engine flow, with an external error that shows it's because of the rate limit. This is cleaner than inventing a new state that is identical for all intents and purposes to `SERVER_ERROR`. We check in processSingleEmail since that maps to the sending state. For analytics query timeouts, the backend route accepts a timeout parameter with the request. The way we implement the timeout for each query is by taking the `min(request_timeout,plan_timeout)` and using that. This determines how long a query can run for. For analytics events, there are server-side events (like refresh token refreshes or sign up rule triggers) and client side events (like page views or clicks). When these events occur, they are written to the events table in clickhouse. We choose to implement a hard cap for the total events, not just server side or client side. Once the cap is hit, we stop storing the events and display a banner on the analytics page. A different banner renders when we are at >=80% of total plan capacity. For session replays, we stop creating new session replays when the limit is hit. Old replays can still have chunks appended to them. The source of truth here is the session replay table- a new replay corresponds to a new row in the table. We have similar banners as to the events. Dashboard admins should be 4 for both team and unlimited. #### Implementation Caveats For debiting items across these limits, we now use `tryDecreaseQuantity` at the beginning. This means we debit first if possible before conducting the action (like writing events to clickhouse). In practice, this means that if clickhouse fails, then the user is debited for something that doesn't happen. However trying to build a refund workaround would be very clunky, and also, clickhouse is reliable. For debits that are very small in the order of things (say, 200 items on a 100k plan), it doesn't mean much. For emails, we don't debit items if it's a retry. This prevents the user for being charged multiple times for effectively one email. ### UI Changes The only UI changes in this PR are having certain banners render in analytics when a customer is approaching/ is at their monthly limit of session replays or events. ### Out of Scope for this PR We do not have metered pricing yet, so events/session replays/ email use beyond the limits cannot be charged yet. This is why for this implementation, we rely on hard and soft caps. We do not implement payment per-transaction pricing yet. That is deferred to a followup PR. The UI for the onboarding call will be set up as part of the overall onboarding flow which doesn't exist yet, so it has been deferred. Since the UI for the dashboard home page and project/account settings is currently being reworked, finding a better spot for plan upgrades is not handled in this PR. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Session replays added as a monthly included entitlement; onboarding calls added to Team/Growth plans. Dashboard banners warn about analytics-event and session-replay limits. Projects page adds extra-seat flow and improved invitation error handling. * **Behavior Changes** * Monthly renewal semantics for emails-per-month and analytics-events; analytics query timeouts now respect plan limits and are clamped. Email sends, analytics events, and new session creation are blocked when quotas are exhausted. Growth plan seats set to 4. * **Tests** * E2E and unit tests added to verify quota enforcement and free-plan regranting. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Mantra <87142457+mantrakp04@users.noreply.github.com>
[Dashboard] Redefine the user page with tabs and updated UI (#1351) <!-- Make sure you've read the CONTRIBUTING.md guidelines: https://github.com/stack-auth/stack-auth/blob/dev/CONTRIBUTING.md --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Tabbed user profile with Activity (30-day analytics, KPIs, daily chart, top lists, recent events), Payments (transactions, subscriptions, product/item balances) and an activity heatmap sidebar. * New internal user-activity API and admin-facing activity hook; admin API client can fetch per-user activity. * **UI/UX Improvements** * Unified menus, cards and tables; inline editable user details with accept/revert; metadata editor validates JSON; country-code input has draft editing; tabs support optional icons. * **API** * Transactions endpoint and admin transaction queries now support optional customer-scoped filtering. * **Tests** * End-to-end coverage for the user-activity endpoint. <!-- end of auto-generated comment: release notes by coderabbit.ai --> <img width="1326" height="752" alt="image" src="https://github.com/user-attachments/assets/97c04dca-db59-4357-98b1-8eae5a7a3673" /> <img width="1142" height="251" alt="image" src="https://github.com/user-attachments/assets/e1aa44fc-0d7e-436d-90a5-c7cb15155e24" /> <img width="1170" height="1125" alt="image" src="https://github.com/user-attachments/assets/bf6659fd-a9b5-4ae6-a13d-dab9956ad650" />
[codex] Fix preview dummy payments customer types (#1398) ## Summary Fixes preview dummy payments seed data so seeded products and items match their team-scoped product lines. ## Root Cause The preview seed configured `workspace` and `add_ons` product lines with `customerType: "team"`, but the products inside those lines (`starter`, `growth`, and `regression-addon`) were configured as `customerType: "user"`. Environment override writes validate against the rendered branch config, so unrelated environment updates could fail with a product/product-line customer type warning. ## Changes - Mark preview dummy payments products and included items as team-scoped. - Export the dummy payments setup helper for focused validation. - Add a regression test that validates the generated branch payments override has no config override errors or incomplete config warnings. ## Validation Passed in the original checkout with dependencies installed: - `STACK_SKIP_TEMPLATE_GENERATION=true pnpm exec vitest run --config vitest.config.ts src/lib/seed-dummy-data.test.ts --reporter=verbose --maxWorkers=1 --minWorkers=1` - `pnpm -C apps/backend lint src/lib/seed-dummy-data.ts src/lib/seed-dummy-data.test.ts` - `pnpm -C apps/backend typecheck` The temporary clean worktree used for this PR did not have `node_modules`, so dependency-backed commands were not rerun there. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Improvements** * Strengthened payment product configuration with tighter typing and validation * Normalized product customer types (switched relevant dummy data from user to team) for consistency * **Tests** * Added tests validating dummy payments configuration and branch/override validation * **Documentation** * Added Q&A documenting a configuration validation failure mode and required consistency for dummy payments data <!-- end of auto-generated comment: release notes by coderabbit.ai -->
fix(dashboard): UI bug fixes (#1377) ## Summary Rolling PR for dashboard UI bug fixes. Each fix is appended to the **Fix log** below with before/after screenshots. This PR stays open until we batch-merge or split. --- ## Fix log ### 1. Hide Alpha/Beta stage badges in onboarding "Select apps" tooltip **Bug:** On the new-project onboarding, hovering an app card showed an "Alpha" or "Beta" stage badge next to the app name in the tooltip. These shouldn't be surfaced on the onboarding step. **Fix:** Removed the stage badge from the onboarding app-card tooltip only. The "Required" badge is preserved, and stage badges on other surfaces (app management, app store, command palette) are unchanged. #### Before / After — Beta (Payments) | Before | After | | --- | --- | |  |  | #### Before / After — Alpha (Onboarding) | Before | After | | --- | --- | |  |  | --- ### 2. Eliminate full-page flash when advancing onboarding steps **Bug:** Moving between onboarding steps (e.g. Configure authentication → Select email theme) briefly blanked out the entire page — only the navbar remained visible for roughly two seconds — before the next step rendered. It felt like a complete browser reload. **Fix:** Contained the suspension inside the wizard. A local Suspense boundary around the onboarding page means that when any data cache refresh fires during the step advance, the suspension no longer bubbles up to the site-wide loading indicator. The step-advance state update is also marked as a React transition, so the current step stays rendered until the next step is ready to commit. Net effect: the previous step is visible throughout the save, then the next step swaps in without a blank frame. #### Before — full blank flash mid-transition | Auth step (start) | Mid-transition (blank) | Email theme step (end) | | --- | --- | --- | |  |  |  | #### After — previous step stays visible, no blank frame | Auth step (start) | Mid-transition (auth stays visible) | Email theme step (end) | | --- | --- | --- | |  |  |  | --- ### 3. Add a subtle back arrow to the onboarding timeline **Bug:** The only way to return to a previous step in the new-project onboarding was to click one of the tiny completed-step dots at the bottom of the page — not discoverable, and easy to miss. **Fix:** Added a small muted left-arrow next to the timeline dots. Clicking it advances back one step. It's absolute-positioned so the dots stay perfectly centered, and it hides itself on the first step (where there's nothing to go back to). #### Before / After — Select apps step | Before — dots only | After — back arrow next to the dots | | --- | --- | |  |  | ### 4. Unify onboarding step styling — cards everywhere, no glassmorphism **Bug:** Step-to-step styling in the onboarding was inconsistent. The Config and Email-theme steps used a glassmorphic surround (`backdrop-blur`, translucent whites) while the other steps used solid cards. Advancing from auth to email made it look like the visual language had changed mid-flow. **Fix:** Dropped the glassmorphic variants from the onboarding wizard. The config-choice option cards, the email-theme container, and the `ModeNotImplementedCard` surround all now use the same solid card treatment (`bg-white/90` light, `bg-white/[0.06]` dark, with subtle ring). One consistent surface across every step. #### Before / After — Config choice step | Before — glassmorphic | After — solid card | | --- | --- | |  |  | #### Before / After — Email theme step | Before — glassmorphic | After — solid card | | --- | --- | |  |  | ### 5. Add "Copy prompt" button on the project setup page **Bug:** The post-project-creation setup page surfaces a terminal command for every framework (Next.js, React, JS, Python), but there was no one-click handoff for users who drive their setup through an AI agent. Users had to manually copy the command, figure out whether the Stack Auth MCP server got registered, and add it themselves if not. **Fix:** Added a compact **✦ Copy prompt** button at the top-right above the steps list. Clicking it copies a framework-aware prompt to the clipboard — the prompt tells the user's AI agent to run the install command for the currently-selected framework, then verify the Stack Auth MCP server (`stack-auth`, transport `http`, `https://mcp.stack-auth.com/`) is registered in its client config and add it manually if the install didn't. #### Before / After — Project setup page | Before — no AI handoff | After — "Copy prompt" at the top-right | | --- | --- | |  |  | ### 6. Disable email theme cards while the onboarding step is saving **Bug:** On the "Select an email theme" step, the theme cards stayed clickable after clicking Continue. Because we keep the previous step visible during the step-advance transition (fix #2), users could click through to a different theme mid-save — the server would then commit whatever selection was active at click time, not the one on screen when Continue was pressed. **Fix:** Added `disabled={saving}` to the email theme buttons, matching the same pattern the config-choice, apps-selection, and auth-setup steps already follow. Added `disabled:cursor-not-allowed disabled:opacity-60` so users get a clear visual signal that the cards are locked while the save is in flight. --- <!-- Append new fixes above this line. Template: ### N. <title> **Bug:** … **Fix:** … #### Before / After | Before | After | | --- | --- | |  |  | --> ## Test plan - [ ] Load the new-project onboarding "Select apps" step and hover every app card — no Alpha/Beta badge appears. - [ ] Hover a required app — "Required" badge still appears. - [ ] Confirm app management tooltips, app store detail page, and command palette still show stage badges (out of scope for this PR). - [ ] Drive the onboarding from Configure authentication to Select email theme — the auth panel stays rendered throughout the save phase and the email panel swaps in without the site-wide loading indicator or a blank content area. - [ ] Repeat for other step transitions (Config → Apps, Apps → Auth, Email → Domain, Domain → Payments) — same seamless behavior. - [ ] From any step after Config, the back arrow appears to the left of the dots. Clicking it goes back one step. On the first step, the arrow is not rendered. - [ ] Walk through every onboarding step. Container surface is visually consistent across steps — no glassmorphic/card mismatch between Config, Apps, Auth, Email Theme, Payments. - [ ] On the project setup page, the "Copy prompt" button appears above the steps (top-right). Clicking it copies the prompt for the currently-selected framework (Next.js / React / JS / Python) and shows a success toast. - [ ] On the "Select an email theme" step, click Continue — the three theme cards become visibly dimmed (`opacity-60`, `cursor-not-allowed`) for the duration of the save and don't respond to clicks. Once the next step renders they stop being visible anyway. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added back navigation to onboarding wizard steps. * Added "Copy prompt" button for framework-aware terminal commands with MCP verification. * Added loading indicator during asynchronous operations. * **UI/UX Improvements** * Updated card styling for unselected options. * Disabled email theme selection during save operations. * Removed stage badges (Alpha/Beta) from app cards. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
[codex] fix OAuth redirect contract (#1393) ## Summary - Route browser OAuth redirects through the configured `redirectMethod` instead of hardcoded `window.location` calls. - Keep OAuth redirect APIs pending after navigation starts, including custom redirect methods. - Add `cliAuthConfirm` handler URL metadata and custom-page prompt coverage. - Update SDK spec text for browser OAuth callback and `returnTo` behavior. ## Root Cause OAuth helpers previously combined URL construction with direct browser navigation. That bypassed configured redirect methods and made it too easy for public redirect APIs to resolve after navigation started. ## Impact Browser SDK consumers get consistent redirect behavior across built-in and custom navigation methods. `returnTo` is handled as the post-callback destination while the OAuth callback URL remains fixed to the configured handler route. ## Validation - `pnpm test run packages/template/src/lib/auth.test.ts` - `pnpm test run apps/e2e/tests/js/oauth.test.ts` - `pnpm -C packages/template lint` - `pnpm -C apps/e2e lint` - `pnpm -C packages/template typecheck` - `pnpm -C apps/e2e typecheck` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added CLI authorization confirmation page/flow for terminal-based auth. * Added optional returnTo parameter for OAuth to control post-auth redirects. * Exposed configurable redirect behavior so apps follow the chosen redirect method. * **Bug Fixes** * OAuth callback now uses app navigation/queued redirects and shows a fallback link instead of forcing location.assign. * **Tests** * Added unit and e2e tests covering OAuth URL generation, scope handling, and CLI auth confirmation. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
PreviousNext