Skip to content

Tags: hexclave/stack-auth

Tags

emulator-dev-20260506-24245ae5

Toggle emulator-dev-20260506-24245ae5's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
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 -->

emulator-main-20260505-0ab26540

Toggle emulator-main-20260505-0ab26540's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
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 -->

emulator-dev-20260506-b0812c88

Toggle emulator-dev-20260506-b0812c88's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
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 -->

emulator-dev-20260505-c69a2701

Toggle emulator-dev-20260505-c69a2701's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
[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>

emulator-dev-20260505-185bddec

Toggle emulator-dev-20260505-185bddec's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
[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"
/>

emulator-dev-20260505-7a54e828

Toggle emulator-dev-20260505-7a54e828's commit message
Revert globe coloring to old algorithm

emulator-dev-20260505-3a5153f4

Toggle emulator-dev-20260505-3a5153f4's commit message
Revert globe coloring to old algorithm

emulator-dev-20260501-d2f2fb0e

Toggle emulator-dev-20260501-d2f2fb0e's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
[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 -->

emulator-dev-20260429-ed896106

Toggle emulator-dev-20260429-ed896106's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
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-payments](https://gist.githubusercontent.com/aadesh18/948fc31499e8bca4943199173cbe0e00/raw/before-hover-beta-payments.png)
|
![after-payments](https://gist.githubusercontent.com/aadesh18/948fc31499e8bca4943199173cbe0e00/raw/after-hover-beta-payments.png)
|

#### Before / After — Alpha (Onboarding)

| Before | After |
| --- | --- |
|
![before-onboarding](https://gist.githubusercontent.com/aadesh18/948fc31499e8bca4943199173cbe0e00/raw/before-hover-alpha-onboarding.png)
|
![after-onboarding](https://gist.githubusercontent.com/aadesh18/948fc31499e8bca4943199173cbe0e00/raw/after-hover-alpha-onboarding.png)
|

---

### 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) |
| --- | --- | --- |
|
![before-auth](https://gist.githubusercontent.com/aadesh18/948fc31499e8bca4943199173cbe0e00/raw/before-01-auth-step.png)
|
![before-flash](https://gist.githubusercontent.com/aadesh18/948fc31499e8bca4943199173cbe0e00/raw/before-02-suspense-flash.png)
|
![before-email](https://gist.githubusercontent.com/aadesh18/948fc31499e8bca4943199173cbe0e00/raw/before-03-email-theme-step.png)
|

#### After — previous step stays visible, no blank frame

| Auth step (start) | Mid-transition (auth stays visible) | Email theme
step (end) |
| --- | --- | --- |
|
![after-auth](https://gist.githubusercontent.com/aadesh18/948fc31499e8bca4943199173cbe0e00/raw/after-suspense-01-auth-step.png)
|
![after-mid](https://gist.githubusercontent.com/aadesh18/948fc31499e8bca4943199173cbe0e00/raw/after-suspense-02-mid-transition.png)
|
![after-email](https://gist.githubusercontent.com/aadesh18/948fc31499e8bca4943199173cbe0e00/raw/after-suspense-03-email-step.png)
|

---

### 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 |
| --- | --- |
|
![before-back-arrow](https://gist.githubusercontent.com/aadesh18/948fc31499e8bca4943199173cbe0e00/raw/before-back-arrow-apps.png)
|
![after-back-arrow](https://gist.githubusercontent.com/aadesh18/948fc31499e8bca4943199173cbe0e00/raw/after-back-arrow-apps.png)
|

### 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-glass-config](https://gist.githubusercontent.com/aadesh18/948fc31499e8bca4943199173cbe0e00/raw/before-glass-config-choice.png)
|
![after-glass-config](https://gist.githubusercontent.com/aadesh18/948fc31499e8bca4943199173cbe0e00/raw/after-glass-config-choice.png?v=2)
|

#### Before / After — Email theme step

| Before — glassmorphic | After — solid card |
| --- | --- |
|
![before-glass-email](https://gist.githubusercontent.com/aadesh18/948fc31499e8bca4943199173cbe0e00/raw/before-glass-email-theme.png)
|
![after-glass-email](https://gist.githubusercontent.com/aadesh18/948fc31499e8bca4943199173cbe0e00/raw/after-glass-email-theme.png)
|

### 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 |
| --- | --- |
|
![before-copy-prompt](https://gist.githubusercontent.com/aadesh18/948fc31499e8bca4943199173cbe0e00/raw/before-copy-prompt-setup.png)
|
![after-copy-prompt](https://gist.githubusercontent.com/aadesh18/948fc31499e8bca4943199173cbe0e00/raw/after-copy-prompt-setup.png)
|

### 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 |
| --- | --- |
| ![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 -->

emulator-dev-20260429-e2dc5f5e

Toggle emulator-dev-20260429-e2dc5f5e's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
[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 -->