Commit Graph

3063 Commits

Author SHA1 Message Date
BilalG1
314fa1780e
Merge branch 'pr/saml-backend' into pr/saml-client 2026-04-30 09:33:52 -07:00
Bilal Godil
90a9852c99 fix(test): add saml key to config.test.ts inline snapshots
Branch-level config schema now includes auth.saml.connections (default
{}), so getConfig surfaces it. Update the three inline snapshots that
serialize the full auth object to match.
2026-04-30 09:33:35 -07:00
BilalG1
f7a487f001
Merge branch 'pr/saml-mock-idp' into pr/saml-backend 2026-04-30 09:30:27 -07:00
Bilal Godil
7dacbc4c08 fix(test): use matched cookie prefix in snapshot serializer
`${keyedCookieNamePrefixes}` interpolated the entire array, which was
harmless when the array had one entry but produced
"stack-oauth-inner-,stack-saml-inner-" once a second prefix was added,
breaking every existing OAuth set-cookie snapshot.
2026-04-30 09:29:38 -07:00
Bilal Godil
e3e09f5464 fix(saml): expose SAML SDK methods on StackClientApp interface
The implementation in client-app-impl.ts already defines
signInWithSaml, signInWithSso, and getSamlConnectionForEmail, but the
public StackClientApp interface in template/.../client-app.ts didn't
list them. TypeScript users got no autocomplete and no type-checking
for the methods, and the regenerated js/react/stack packages
inherited the gap.

Add the three signatures to the interface near signInWithOAuth.
Run \`pnpm -w run generate-sdks\` to propagate to the downstream
packages (js/react/stack). Also add a saml-sso → Building2 mapping to
the docs APP_ICONS map (broke when the type added "saml-sso" to AppId).

Tests:
- New regression test in discover.test.ts confirming the new
  allowSignIn=false filter (#1396 fix) returns 404, so a disabled
  connection no longer leaks into the signInWithSso flow.
2026-04-29 18:50:26 -07:00
Bilal Godil
a007318b49 Merge branch 'pr/saml-backend' into pr/saml-client
# Conflicts:
#	apps/backend/src/lib/seed-dummy-data.ts
2026-04-29 18:50:09 -07:00
Bilal Godil
8f6ad97e40 fix(saml): skip allowSignIn=false connections in /auth/saml/discover
Discover is the entry point for the SDK's signInWithSso({ email }) flow.
Previously it returned every connection whose domain matched, including
ones the project admin had disabled (`allowSignIn: false`). The SDK
would then send the user through /auth/saml/login, which intentionally
403s for disabled connections — so disabling a connection was a sharp
UX cliff: domain match → branded "Sign in with Acme SSO" CTA → 403.

Treat disabled connections as if they didn't exist for discovery
purposes. Direct sign-in via signInWithSaml({ connectionId }) is still
gated separately in the login route, which is the right place for
"intentional, explicit access by ID."
2026-04-29 18:45:02 -07:00
Bilal Godil
75f8c69dd8 Merge branch 'pr/saml-mock-idp' into pr/saml-backend 2026-04-29 18:43:11 -07:00
Bilal Godil
1e912c7548 fix(seed): serialize SAML seed; drop overlay cast
Two cleanups in seed-dummy-data, both flagged on PR #1395:

- The parallel `Promise.all` block ran `seedSamlConnections(projectId)`
  alongside another `overrideEnvironmentConfigOverride` write on the same
  project (`payments.testMode`). Both are read-modify-write, so concurrent
  reads of the env config can each see the stale state and the second
  write clobbers the first — the existing TODO at `config.ts:491` already
  documents the underlying race. Sequence the SAML seed after the
  parallel block to avoid the race until the override is wrapped in a
  serializable txn.

- Type the SAML overlay with the target parameter type directly instead
  of using an `as Parameters<...>` cast, per project style.
2026-04-29 18:42:59 -07:00
BilalG1
b00fc53748
Merge branch 'pr/saml-backend' into pr/saml-client 2026-04-29 18:31:35 -07:00
BilalG1
8c45c0607b
Merge branch 'pr/saml-mock-idp' into pr/saml-backend 2026-04-29 18:31:28 -07:00
Bilal Godil
aaeb8318a8 fix(test): drop overbroad SAML ID strip regex from snapshot serializer
The pattern \`/_[a-zA-Z][a-zA-Z0-9_.-]{8,}/g\` matched any SCREAMING_SNAKE_CASE
identifier with an underscore followed by 9+ chars — e.g.
\`STRIPE_ACCOUNT_INFO_NOT_FOUND\` became \`STRIPE<stripped SAML id>\`,
breaking unrelated Stripe / payments / OAuth e2e snapshots.

The regex isn't load-bearing today: no current SAML test snapshots a
random AuthnRequest / Response / Assertion ID. Cookie-name SAML IDs
are already covered by \`keyedCookieNamePrefixes\` ("stack-saml-inner-"),
URL path segments only carry the deterministic connection_id, and no
test snapshots raw SAML XML. If a future test ever does, follow the
precedent of the timestamp strip on the next line and anchor the
replacement to specific XML attributes (\`ID="..."\`, \`InResponseTo="..."\`)
rather than matching loose \`_<chars>\` strings everywhere.
2026-04-29 18:28:43 -07:00
Bilal Godil
8facb272a6 fix(saml): admin POST /saml-connections must write whole entry
Previously wrote per-field deep dot-keys
(`auth.saml.connections.X.displayName`, ...). When the parent record
entry didn't yet exist, normalization with `onDotIntoNonObject="ignore"`
silently dropped them — POST returned 200 but persisted nothing.

Mirror the dashboard's create dialog (commit 5fa9629de) by writing the
whole connection object as a single overlay entry. Add a regression test
that creates a connection from empty and verifies it round-trips through
LIST + GET; covers the gate too.
2026-04-29 18:14:32 -07:00
Bilal Godil
9cf7e8f943 fix(saml): use backend origin for SP base URL in login + ACS routes
The login route built the SP `callbackUrl` from `query.redirect_uri.origin`,
which is the customer's app — not the backend. The IdP would then POST
the assertion to e.g. `http://localhost:8103/api/v1/auth/saml/acs/acme`
(the demo app), which 404s because the ACS handler only exists on the
backend.

Fix both login and ACS to derive `baseUrl` from the incoming request's
own origin, matching what the metadata route already does. The e2e
round-trip test didn't catch this because in tests the customer and
backend run on the same host.
2026-04-29 18:08:41 -07:00
Bilal Godil
82424d1be9 feat(saml): gate SAML SSO behind alpha-stage saml-sso app
Register a new alpha-stage `saml-sso` app rather than exposing SAML to
every project. Users opt in from the App Store; the dashboard SSO page,
admin CRUD, and SDK routes all 400 with `SAML_SSO_NOT_ENABLED` until
the app is installed. Alpha apps stay hidden in production via the
existing NODE_ENV filter in `getAllAvailableAppIds`.

- Add `saml-sso` to `ALL_APPS` + `ALL_APPS_FRONTEND` (icon, store copy,
  nav item pointing at the existing /sso route)
- Wrap SSO page with `AppEnabledGuard` for the redirect-on-disabled UX
- Backend: each SAML route checks `apps.installed["saml-sso"]?.enabled`
  and throws the new `KnownErrors.SamlSsoNotEnabled`
- E2E: setup helpers enable the app on test projects; new discover
  test verifies the gate fires for unconfigured projects
- Seed dummy data: enable saml-sso when `STACK_SEED_ENABLE_SAML=true`
  so the seeded acme/globex connections work without an extra click
- Fix a pre-existing indent slip in the delete-dialog updateConfig call
2026-04-29 17:23:10 -07:00
Bilal Godil
fe8197ca8c fix(dashboard): drop yup.url() validator and use env-only pushable=false
Surfaced while taking screenshots for the PR description:

- yup.string().url() rejects http://localhost which breaks any local
  or test-env IdP setup. The backend SAML wrapper validates the URL
  on use anyway. Drop the client-side .url() check.

- The form was set to pushable=true which routes through the
  GitHub-pushable config dialog. SAML connection fields (cert,
  IdP URLs) live at the environment level (not the branch level
  that's pushed to GitHub) — same as OAuth client secrets in the
  auth-methods page. Set pushable=false to write directly via env
  config override.
2026-04-29 16:59:52 -07:00
Bilal Godil
5fa9629ded fix(saml): use whole-entry config writes; correct test request shapes
E2E tests + dashboard SSO page were writing per-field deep dot-keys
like `auth.saml.connections.X.displayName`, which the config
normalizer drops because the parent record entry doesn't yet exist
when the connection is being created. Match the existing
auth.oauth.providers convention: write the whole connection entry
as a single value on create.

Also fixed two test-harness issues uncovered while running the suite:
- Round-trip ACS POST was using niceBackendFetch which always
  JSON.stringifies the body. Switched to plain niceFetch so
  URLSearchParams gets sent as application/x-www-form-urlencoded.
- Mock IdP /metadata returns application/xml, which makes niceFetch
  return ArrayBuffer; added a TextDecoder pass before regex matching.
2026-04-29 16:47:17 -07:00
Bilal Godil
edb13f3107 feat(dashboard): add SAML SSO management page
Single page at /projects/[projectId]/sso for managing SAML connections.
Lists existing connections (read from project.useConfig() — same source
as the e2e tests and seed script use), with add + delete dialogs.

Add dialog: ID, display name, optional email domain (for discovery),
IdP entity ID, IdP SSO URL, and IdP signing certificate. PEM headers
are stripped from the cert automatically before saving.

Delete dialog warns that user accounts linked via the connection
remain in the database — they just become unable to sign in until a
connection with the same id is recreated.

Each connection card surfaces the SP metadata URL + ACS URL so the
customer's IT admin can copy them into the IdP console without
manually composing the URLs.

V1 limitations (planned follow-ups):
- Edit happens by deleting + recreating; no in-place edit dialog yet.
- No "paste IdP metadata XML" auto-fill (the backend metadata-parser
  exists; just needs to be wired up to a paste box).
- No separate detail page; everything is on the list.
2026-04-29 16:47:17 -07:00
Bilal Godil
36d00f2c4d test(e2e): add full SAML SP-initiated round-trip tests via mock IdP
apps/e2e/tests/backend/endpoints/api/v1/auth/saml/round-trip.test.ts
exercises the entire SP-initiated flow against the running mock IdP on
port 8115:

  GET /auth/saml/login → IdP URL with SAMLRequest
  POST mock /idp/[tenant]/login → auto-POST HTML with signed SAMLResponse
  POST /auth/saml/acs → backend verifies + issues OAuth code

Five test cases:

1. Happy path: new user JIT-created, ACS responds with 303/307 + OAuth
   code in the redirect.

2. Wrong audience: mock IdP misbehaves via /test-controls
   { kind: 'wrong-audience' }, backend rejects.

3. Bad signature (cross-tenant forgery): mock signs with another
   tenant's key via { kind: 'bad-signature' }, backend rejects.

4. Expired assertion: NotOnOrAfter in the past via { kind: 'expired' },
   backend rejects.

5. Replay: same SAMLResponse POSTed twice — second attempt rejected
   because SamlOuterInfo was consumed by the first ACS call.

Fetches the mock IdP's cert at test setup time so the SAML
verification chain is real (the mock regenerates keys per startup, so
hardcoded certs would never match).

Test integrity reaffirmed: the test file imports only from helpers,
backend-helpers, and ports — NO imports from apps/backend/src/saml/.
Negative cases come from the mock deliberately misbehaving, never from
injecting bad data into the backend's own validator. Mock IdP uses
samlify; backend uses @node-saml/node-saml — different libraries on
each side mean a bug in either surfaces as a test failure rather than
canceling out.

Tests written and lint/typecheck clean; runtime verification needs the
backend + mock-saml-idp services up (CI workflow already wired).
2026-04-29 16:47:17 -07:00
Bilal Godil
f8093a31c1 test(e2e): add SAML discover, metadata, and login route tests
Three test files exercising the SAML routes that don't require a full
IdP round-trip:

- discover.test.ts: 5 cases for /auth/saml/discover — happy path,
  unknown domain (404), case-insensitivity, unknown project_id,
  cross-project isolation (project A's connection isn't visible from
  a query against project B's domain).

- metadata.test.ts: 3 cases for /auth/saml/metadata — XML contains
  entityID + ACS URL embedded, 404 for unknown connection, 404 when
  connection exists but has no IdP cert (incomplete configuration).

- login.test.ts: 5 cases for /auth/saml/login — JSON-mode returns
  the IdP redirect URL with SAMLRequest+RelayState, browser-redirect
  mode sets the stack-saml-inner- CSRF cookie, 404 unknown connection,
  403 when allowSignIn=false, invalid client_id rejected.

Test integrity: all tests drive the API only — no imports from
apps/backend/src/saml/. SAML config is set via the standard config
override endpoint (no test-only mutator), so the routes run through
the same code path real customers would hit.

Full SAML round-trip tests (login → mock IdP → ACS → session)
deferred to a follow-up — they need a sequenced flow against the
mock-saml-idp service that's separate from these endpoint-level tests.
2026-04-29 16:47:16 -07:00
Bilal Godil
958407d0c3 feat(examples/demo): add SAML SSO demo page
examples/demo/src/app/saml-demo/page.tsx — manual end-to-end check for
the SAML round-trip. Two flows:

1. Email-domain discovery: enter alice@acme.test, click "Sign in via
   SSO". The SDK calls getSamlConnectionForEmail then redirects via
   the matched connection.

2. Direct connection ID: per-tenant buttons that call signInWithSaml
   with explicit connectionId (the pattern most B2B login pages use
   when they brand each tenant separately).

Page also shows the current signed-in state + an SDK snippet so a
developer can see exactly what to copy. Pairs with seed-dummy-data's
STACK_SEED_ENABLE_SAML=true block which pre-creates the matching
acme + globex connections.
2026-04-29 16:47:16 -07:00
Bilal Godil
ce66a1908d feat(sdk): add signInWithSaml, signInWithSso, getSamlConnectionForEmail
Three methods on StackClientApp that mirror signInWithOAuth:

- signInWithSaml({ connectionId, returnTo }) — explicit connection
  selection. Calls /auth/saml/login/[connectionId] in stack_response_mode
  =json so the SDK can intercept the redirect URL.

- signInWithSso({ email, returnTo }) — email-domain discovery via
  /auth/saml/discover, then redirects through the matched connection.
  Throws when no connection matches so callers can fall back to other
  sign-in methods.

- getSamlConnectionForEmail(email) — pure lookup with no redirect, so
  the customer's UI can render branding ("Sign in with Acme SSO")
  before the user clicks.

Backed by getSamlUrl + authorizeSaml + discoverSamlConnection on
StackClientInterface (mirrors getOAuthUrl + authorizeOAuth pattern,
without provider_scope or bot challenge — SAML originates from a
corporate IdP, not a public form).

Generated via pnpm -w run generate-sdks; propagates from
packages/template into packages/js, packages/react, packages/stack.
2026-04-29 16:47:16 -07:00
Bilal Godil
2d1db7a56e fix(saml): harden route guards and accept Response-level signature
Two bugs surfaced when running the SAML e2e suite against the live
backend (in a separate PR):

1. Routes accessed `tenancy.config.auth.saml.connections[id].field`
   without first checking that the entry exists. With strict null
   checks off, TS types this as always-defined and the route 500'd
   with a TypeError on missing connections instead of returning 404.
   Add an explicit `id in connections` guard at the top of each
   route (login, acs, metadata).

2. SAML responses signed at the Response element (samlify default,
   also what Okta + Azure AD emit) failed verification because the
   backend was configured with wantAssertionsSigned=true,
   wantAuthnResponseSigned=false — i.e. demanded an Assertion-level
   signature. Per SAML 2.0 §4.1.4.2 either is valid. Flip to
   wantAuthnResponseSigned=true so we accept what real-world IdPs
   actually send.
2026-04-29 16:46:22 -07:00
Bilal Godil
b4bc68750e feat(backend): add admin CRUD for SAML connections
Three endpoints under /api/v1/saml-connections — admin-only thin REST
wrappers around the JSON-config storage so the dashboard SSO pages
don't compose key paths manually:

- GET    /saml-connections           list all (omits cert)
- POST   /saml-connections           upsert by id
- DELETE /saml-connections           delete by id
- GET    /saml-connections/[id]      full detail (includes cert)

User accounts linked via a deleted connection remain in the DB; they
just become unable to sign in until a connection with the same id is
recreated. (Dashboard delete UX should warn on this.)

Underlying storage is the same overrideEnvironmentConfigOverride /
resetEnvironmentConfigOverrideKeys flow used by the seed script and
the e2e tests, so behavior is identical across all surfaces.
2026-04-29 16:46:22 -07:00
Bilal Godil
191ad700bd feat(backend): add SAML login + ACS routes with OAuth2 integration
Two routes that complete the SAML SP-initiated round trip:

- GET /api/v1/auth/saml/login/[connection_id]
  Receives the same Stack Auth OAuth client params as
  /auth/oauth/authorize (client_id, redirect_uri, scope, state, etc.),
  builds an AuthnRequest, persists the OAuth context + AuthnRequest ID
  in SamlOuterInfo, sets a CSRF cookie keyed to the request ID, and
  redirects to the IdP. Honors stack_response_mode=json so the SDK
  can intercept programmatically. V1 scope: SP-initiated only, no
  signed AuthnRequests, no link/upgrade flow.

- POST /api/v1/auth/saml/acs/[connection_id]
  Receives the IdP's POST. Parses InResponseTo from the response
  WITHOUT verifying the signature, looks up SamlOuterInfo to recover
  tenancy/connection (this is necessary because the connection ID
  alone doesn't index a tenancy in the JSON-config storage model).
  Validates CSRF cookie, then runs node-saml's full
  validatePostResponseAsync (signature + audience + clock skew +
  InResponseTo). Defense-in-depth re-checks InResponseTo and
  cross-connection mismatch (the latter handles 'assertion sent to
  the wrong ACS endpoint' forgery, e2e test #10).

  On success, runs find-existing / link / create via the
  saml-account.tsx helpers, then hands off to oauthServer.authorize
  so Stack Auth issues a customer-facing OAuth code (mirrors the
  oauth/callback pattern). Deletes SamlOuterInfo at the end for
  replay protection.

Adds extractInResponseTo helper to saml/saml.tsx for the pre-validation
parse described above.

Routes typecheck and lint clean. Runtime untested — needs the e2e test
matrix (task #15) to exercise the round-trip end-to-end against the
mock IdP.
2026-04-29 16:46:22 -07:00
Bilal Godil
189a543a31 feat(stack-shared): add SAML connection config to project schema
Adds tenancy.config.auth.saml — mirrors the auth.oauth shape:

- branchAuthSchema gains saml.{accountMergeStrategy, connections}
  with non-sensitive per-connection fields (displayName, allowSignIn,
  domain). domain feeds /auth/saml/discover.

- environmentConfigSchema extends saml.connections with IdP-side
  fields (idpEntityId, idpSsoUrl, idpCertificate, attributeMapping).
  These belong at the environment level — different per IdP deployment
  even though the cert is technically a public key — same way
  oauth.providers splits clientId/clientSecret out of branch config.

- Defaults block adds an empty saml block; per-connection defaults set
  allowSignIn=true and a placeholder displayName so partial configs
  validate cleanly.

Also drops the temporary unknown-cast workaround in saml-account.tsx
(handleSamlEmailMergeStrategy) and updates the metadata + discover
routes to construct SamlConnectionConfig from the typed config record
(injecting the connection ID since it's stored as the record key).

Adds matching coverage in schema-fuzzer.test.ts so the fuzzed config
shape includes a sample SAML connection.
2026-04-29 16:46:22 -07:00
Bilal Godil
11239b4687 feat(backend): add SAML metadata + discovery HTTP routes
Two of the four planned SAML routes — the public-fetchable / read-only
ones with no OAuth2-server integration:

- GET /api/v1/auth/saml/metadata/[connection_id]?project_id=...
  SP metadata XML for the IdP admin to paste into their IdP console.
  Includes the project_id query param because connection IDs alone
  don't identify a tenancy (config lives in JSON, not a Prisma table).

- GET /api/v1/auth/saml/discover?email=...&project_id=...
  Email-domain → connection lookup for the SDK's signInWithSso flow.
  Returns 404 (not 200 with null) when no connection matches so the
  SDK can fall back to other sign-in methods on status alone.

login + acs routes are the next chunk. They need to mirror the OAuth
callback's oauthServer.authorize integration so the customer's app
receives a Stack Auth OAuth code on success — that's a meaningful
copy-from-pattern job and is left for the next commit so it can be
reviewed and tested in isolation.
2026-04-29 16:46:22 -07:00
Bilal Godil
0e542f72f5 feat(backend): add SAML protocol wrapper around @node-saml/node-saml
Three modules under apps/backend/src/saml/:

- saml.tsx — buildSamlClient (per-connection SAML instance), build
  AuthnRequestUrl (returns URL + extracted requestId for replay
  protection), parseAndVerifyAssertion (signature + audience + clock-skew
  + InResponseTo are all enforced by node-saml), getSpMetadataXml.
  Defines SamlConnectionConfig locally so the wrapper doesn't depend on
  the project-config schema work.

- metadata-parser.tsx — pulls entityId, ssoUrl, and the signing X509
  certificate out of pasted IdP metadata XML. Uses xmldom + xpath rather
  than regex so it handles attribute-order variations across IdPs.

- discovery.tsx — email-domain to connection lookup for the
  signInWithSso({ email }) flow. Iterates the project's connections and
  returns the first whose `domain` matches.

The clock-skew tolerance is set to 60s, matching the e2e test matrix
item #16. The 'wantAssertionsSigned: true' default means an unsigned
assertion is rejected even if the response itself is signed — which is
the safer default per OWASP SAML guidance.
2026-04-29 16:46:22 -07:00
Bilal Godil
c1b7bed261 feat(backend): extract email-merge helper and add SAML account helpers
Splits the email-merge strategy out of oauth.tsx into a small shared
external-auth.tsx so the upcoming SAML ACS handler can reuse the same
contact-channel lookup + link_method/raise_error/allow_duplicates switch
without duplicating it.

Also adds saml-account.tsx with the SAML-side parallel of OAuth's
findExisting / link / create user-linking helpers, operating on
ProjectUserSamlAccount and SamlAuthMethod. Each helper is keyed by
(tenancyId, samlConnectionId, nameId), so a NameID arriving from a
different connection is treated as a separate identity — connection
isolation is enforced at the DB level.

Schema strategy fallback: handleSamlEmailMergeStrategy reads
tenancy.config.auth.saml.accountMergeStrategy if set, otherwise falls
back to the OAuth strategy. The SAML config field will be added with
the project config schema work.

Adds @xmldom/xmldom and xpath as direct backend deps for the upcoming
SAML protocol wrapper (currently transitive through @node-saml/node-saml).
2026-04-29 16:46:22 -07:00
Bilal Godil
cbd2e3fca3 feat(backend): add SAML SSO Prisma models + migration
Adds three tables to back per-user SAML accounts and the in-flight
AuthnRequest temp store:

- ProjectUserSamlAccount (mirrors ProjectUserOAuthAccount): one row per
  (tenancy, samlConnectionId, NameID). The unique constraint on
  (tenancyId, samlConnectionId, nameId) is what enforces multi-tenant
  connection isolation at the DB level — the same NameID from a
  different connection is treated as a distinct identity.

- SamlAuthMethod (mirrors OAuthAuthMethod): connects an AuthMethod to a
  ProjectUserSamlAccount via composite FK.

- SamlOuterInfo (mirrors OAuthOuterInfo): keyed by AuthnRequest ID so
  the ACS handler can look up the original context when the IdP POSTs
  the assertion back via the browser. ID is TEXT (not UUID) because
  SAML AuthnRequest IDs are XML xs:ID strings.

Per-connection config (entity ID, IdP cert, ACS URL, attribute mapping,
domain) is intentionally NOT a Prisma model — it lives in
tenancy.config.auth.saml.connections JSON, matching how OAuth provider
config (clientId/clientSecret) is stored.
2026-04-29 16:46:22 -07:00
Bilal Godil
0e59570bc9 fix(lockfile): regenerate after removing body-parser dep from mock-saml-idp
mock-saml-idp originally depended on body-parser for the parser
middleware, but switched to using express.urlencoded()/express.json()
directly. The package.json dep was removed but the lockfile entry
remained, breaking 'pnpm install --frozen-lockfile' in CI.
2026-04-29 16:46:13 -07:00
Bilal Godil
4949a9cfc2 fix(seed): use whole-entry config writes for SAML connections
Deep dot-keys like `auth.saml.connections.X.field` get dropped by
config normalization with onDotIntoNonObject=ignore when the parent
record entry doesn't yet exist. Match the existing convention from
auth.oauth.providers and write the whole connection entry as a
single value.

(Bug surfaced when running the SAML e2e tests against a live
backend in a separate PR. Applied here so the seed function works
on its own without requiring downstream PRs.)
2026-04-29 16:38:35 -07:00
Bilal Godil
6c7b14b3bc feat: wire mock-saml-idp into CI, snapshots, and seed dummy data
Three smaller pieces that unlock e2e testing:

- .github/workflows/e2e-api-tests.yaml: starts mock-saml-idp on port
  8115 alongside mock-oauth-server, with /idp as the readiness probe.
  Root package.json adds start:mock-saml-idp script and includes the
  mock in dev:basic.

- apps/e2e/tests/snapshot-serializer.ts: strips SAMLRequest /
  SAMLResponse / RelayState query+form params, adds stack-saml-inner-
  to keyed cookie name prefixes (so the per-AuthnRequest CSRF cookie
  doesn't reroll snapshots), and adds regex replacements for SAML xs:ID
  identifiers and IssueInstant/NotBefore/NotOnOrAfter timestamps.

- apps/backend/src/lib/seed-dummy-data.ts: STACK_SEED_ENABLE_SAML=true
  pre-creates acme + globex SAML connections on the dummy project,
  fetching the IdP metadata from the running mock at seed time so the
  seeded cert matches what the mock generated at startup. The mock
  regenerates keys per restart, so re-seed if you restart it. Mock URL
  configurable via STACK_MOCK_SAML_URL (default localhost:8115).
2026-04-29 16:38:03 -07:00
Bilal Godil
d4d25f6255 feat(mock-saml-idp): scaffold mock SAML 2.0 IdP for SAML SSO testing
Adds apps/mock-saml-idp, a multi-tenant SAML 2.0 Identity Provider mock
mirroring apps/mock-oauth-server. Each tenant has its own RSA keypair
and self-signed cert generated at startup, so one mock service can back
many SamlConnection rows in tests and exercise per-connection isolation.

Uses samlify deliberately because the upcoming backend SAML wrapper will
use @node-saml/node-saml. Different libraries on each side means a bug
in either library's signature canonicalization surfaces as a test
failure instead of being masked by both sides agreeing.

Endpoints:
- GET  /idp/:tenant/metadata        IdP metadata XML
- GET  /idp/:tenant/sso             AuthnRequest receiver, renders login form
- POST /idp/:tenant/login           builds and auto-POSTs signed assertion
- POST /idp/:tenant/test-controls   queues misbehaviors (bad-signature,
                                    expired, wrong-audience, replay, etc.)
- GET  /idp                         introspection

Also adds @node-saml/node-saml to apps/backend deps for the upcoming
backend SAML protocol wrapper.
2026-04-29 16:38:03 -07:00
Mantra
e831972c4c
Move internal MCP server to backend, use Mintlify MCP for docs tools (#1389)
## Summary
- Move the `/api/internal/[transport]` MCP route from the docs app to
the backend, so the public `ask_stack_auth` MCP tool is served from the
same origin as the AI query API it proxies to.
- Replace the bespoke docs-tools HTTP client in
`apps/backend/src/lib/ai/tools/docs.ts` with an `@ai-sdk/mcp` client
that talks to Mintlify's generated MCP server. The backend AI agent now
consumes Mintlify's lower-level search/fetch tools directly instead of
going through the docs app.
- Swap `STACK_DOCS_INTERNAL_BASE_URL` for `STACK_MINTLIFY_MCP_URL`
(defaults to the Mintlify-hosted MCP URL).
- Move the `@vercel/mcp-adapter` dependency from `docs` to
`apps/backend`.

## Test plan
- [ ] `pnpm typecheck`
- [ ] `pnpm lint`
- [ ] e2e: new
`apps/e2e/tests/backend/endpoints/api/v1/internal/mcp.test.ts` covers
`tools/list` and validation on `tools/call`
- [ ] Manual: hit `POST /api/internal/mcp` on the backend and confirm
`ask_stack_auth` is listed and callable
- [ ] Manual: confirm backend AI agent docs tools resolve via the
Mintlify MCP URL

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Backend docs tooling now uses a Mintlify MCP server for documentation
tools and discovery.

* **Chores**
* Development environment variables updated to point to the Mintlify MCP
endpoint.
* Backend dependency added to support MCP integration; docs package
dependency removed.

* **Tests**
* Added end-to-end tests for the internal MCP endpoint and tool
validation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-29 09:45:52 -07:00
aadesh18
ed8961069c
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 -->
2026-04-28 18:49:28 -07:00
Mantra
e2dc5f5ee0
[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 -->
2026-04-28 16:33:59 -07:00
Madison
5e5cfdec4f
[Dashboard][Backend][SDK] - Adds sharable session replay ids. (#1294)
# Shareable Session Replay Links
Adds the ability to share individual session replays via unique, direct
URLs.

https://www.loom.com/share/1e3298a19b114fc38af4bc43dcd5ec48

## What changed
- New admin endpoint — GET /api/v1/internal/session-replays/:id
- Fetches a single session replay by ID with user metadata (display
name, primary email) and chunk/event counts
- Returns 404 if the replay doesn't exist
- Admin-only access, consistent with the existing list endpoint
## New standalone replay page —
/projects/:projectId/analytics/replays/:replayId
- Thin server page wrapper that passes the replay ID to the existing
PageClient
- PageClient detects standalone mode via initialReplayId prop and
fetches replay metadata directly instead of loading the full session
list
- Sidebar is hidden; the replay viewer takes the full width
- "Back to all replays" link shown under the page title
## Copy link button
- Moved from per-session sidebar items to the replay viewer header (next
to the settings gear)
- Copies a direct URL to the currently selected replay
## SDK plumbing
- AdminGetSessionReplayResponse type in stack-shared
- getSessionReplay() on StackAdminInterface, StackAdminApp interface,
and _StackAdminAppImplIncomplete
## Tests
- Happy path: fetch single replay by ID with inline snapshot
- 404 for nonexistent replay ID
- 401 for non-admin access (client and server)
## Test plan
- [ ] Open /analytics/replays, select a replay, click the link icon in
the header — verify URL is copied to clipboard
- [ ] Paste that URL in a new tab — verify the standalone replay page
loads and plays the correct replay
- [ ] Verify "Back to all replays" link navigates back to the list page
- [ ] Verify the original /analytics/replays list page still works as
before (selecting, filtering, pagination)
- [ ] Run pnpm test run session-replays


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Backend: internal endpoint to fetch a single session replay with user
info, millisecond timestamps, and chunk/event counts.
* Admin SDK/App: added response type and admin method to retrieve a
single session replay; admin app maps response into the app model.
* Dashboard: standalone session-replay page, UI adjustments for
standalone mode, and a “copy replay link” button.

* **Tests**
* Added end-to-end tests for retrieval, not-found, and access-control
scenarios.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-28 17:57:07 -05:00
Mantra
0207721f68
fix(dashboard): improve analytics replay replayer lifecycle (#1349)
## Summary

Improves reliability of the session replay viewer on the project
analytics replays page by tracking replayer staleness, coordinating
pause/restart with effects, and cleaning up instances to avoid leaks.

## Changes

- Add `isReplayerStale` and wire replayer lifecycle into
`executeEffects` so playback and pause stay in sync with the replayer
state.
- Pause/restart and teardown when the replayer becomes stale or
unmounts.

## Test plan

- [ ] Open a project’s **Analytics → Replays**, load a replay, scrub
timeline, pause/resume, and switch replays; confirm no stuck playback or
console errors.
- [ ] `pnpm lint` / `pnpm typecheck` on touched packages if CI does not
cover.

## Notes

Small `CLAUDE.md` tweak included in the same commit.

Made with [Cursor](https://cursor.com)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Chores**
  * Disabled automatic session recording in the dashboard.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-28 15:29:54 -07:00
Mantra
9d1eee8ab8
Add useCliAuthConfirmation hook and customizable cliAuthConfirm URL target (#1388)
## Summary
- Extract CLI auth confirmation into a `useCliAuthConfirmation()` hook
(status / error / isLoading / authorize / retry) so custom pages don't
have to reimplement the protocol; `CliAuthConfirmation` now consumes the
hook.
- Make `cliAuthConfirm` a first-class handler URL target — resolved via
`resolveHandlerUrls`, customizable per project, and used by
`promptCliLogin` through a new `buildCliAuthConfirmUrl()` helper.
- Move `StackContext` to its own module so the hook can be unit-tested
with a test double without tripping the client-version sentinel;
register `cliAuthConfirm` in custom-page prompts and the dev-tool
components tab; export the hook + types from `@stackframe/stack`.

## Test plan
- [ ] `pnpm typecheck`
- [ ] `pnpm lint`
- [ ] `pnpm --filter @stackframe/stack test cli-auth-confirm
url-targets`
- [ ] Manually verify default `/handler/cli-auth-confirm` flow + a
project with a custom `cliAuthConfirm` URL

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Adds a CLI authentication confirmation page with clear states
(invalid, authorizing, redirecting, success, error), retry action, and
flows for signed-in and anonymous users.
* CLI login URL generation now derives from the configured handler
target and app base, improving reliability.
* CLI confirmation page exposed in the components/dev UI for previewing.

* **Tests**
* End-to-end and unit tests covering confirmation behaviors and URL
generation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-28 15:29:46 -07:00
Mantra
a82097db62
refactor(dashboard): use getEnabledAppIds on metrics page (#1394)
## Summary
Uses the shared `getEnabledAppIds` helper from `@/lib/apps-utils`
instead of manually filtering installed apps with `typedEntries` on the
project metrics page.

## Why
Keeps enabled-app logic consistent with other dashboard code paths and
slightly reduces duplication.

## Test plan
- [ ] Smoke: open project metrics / overview and confirm installed
app-dependent UI (e.g. analytics) still behaves as before.

Made with [Cursor](https://cursor.com)
2026-04-28 13:16:33 -07:00
BilalG1
b3d0ab66cc
fix(stack-shared): make process.env access browser-safe (#1391)
## Summary

- Bare `process.env.X` accesses in `stack-shared` throw `ReferenceError:
process is not defined` when the package is bundled into a browser app
without a `process` shim (e.g. a plain Vite app). The most reachable
offenders are in `StackAssertionError`'s constructor and
`schema-fields.ts`'s Neon Basic-auth validator, both of which can run on
the client during normal sign-in flows with `@stackframe/react`.
- Extracted a zero-dependency `getProcessEnv` helper at
`packages/stack-shared/src/utils/process-env.tsx` and routed the bare
references through it. Returns `undefined` when `process` is not
defined; otherwise behaves like a normal `process.env[name]` read, so
Next.js/webpack inlining is unchanged on the server.
- Touched: `schema-fields.ts:884` (`STACK_INTEGRATION_CLIENTS_CONFIG`),
`utils/errors.tsx:81` (`NEXT_PUBLIC_STACK_DEBUGGER_ON_ASSERTION_ERROR`),
`utils/promises.tsx` (`NODE_ENV` in `runAsynchronouslyWithAlert`),
`utils/esbuild.tsx:16` (`NODE_ENV`, also reordered the `typeof process`
guard so the env access is unreachable in browsers).

## Why a separate helper module

`utils/env.tsx` already exists but its `getEnvVariable` explicitly
throws in the browser, so it can't be reused here. The new module has
zero imports so it can be safely consumed from low-level utilities like
`errors.tsx` without creating a cycle (env.tsx ↔ errors.tsx).

## Test plan

- [x] `pnpm lint` passes
- [x] `pnpm typecheck` passes
- [ ] Reproduced the original failure in a Vite + `@stackframe/react`
app: sign-in flow logged `ReferenceError: process is not defined` from
`StackAssertionError`, plus `clientSecret must not be empty` cascading
from the same path
- [ ] Verify the same flow in a Vite app no longer throws once
`@stackframe/react` is rebuilt against this `stack-shared` change
- [ ] Confirm Next.js consumer behavior is unchanged (env vars still
inlined at build time for `NEXT_PUBLIC_*`)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Release Notes

* **Refactor**
* Improved environment variable handling across shared utilities for
enhanced browser compatibility and safety. Introduced a new utility for
dynamic, browser-safe environment variable access that prevents errors
in non-Node.js environments.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 10:59:49 -07:00
Mantra
65d87a4836
Dashboard: DataGrid refactor + layout (stacked on overview-revamp) (#1338)
Some checks failed
all-good: Did all the other checks pass? / all-good (push) Has been cancelled
Ensure Prisma migrations are in sync with the schema / check_prisma_migrations (22.x) (push) Has been cancelled
DB migration compat / Check if migrations changed (push) Has been cancelled
Docker Server Build and Push / Docker Build and Push Server (push) Has been cancelled
Docker Server Build and Run / docker (push) Has been cancelled
Runs E2E API Tests (Local Emulator) / E2E Tests (Local Emulator, Node ${{ matrix.node-version }}) (22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (mock, 22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (prod, 22.x) (push) Has been cancelled
Runs E2E API Tests with custom port prefix / build (22.x) (push) Has been cancelled
Runs E2E Fallback Tests / E2E Fallback Tests (Node ${{ matrix.node-version }}) (22.x) (push) Has been cancelled
Lint & build / lint_and_build (24) (push) Has been cancelled
TOC Generator / TOC Generator (push) Has been cancelled
DB migration compat / Back-compat — Current branch migrations with ${{ needs.check-migrations-changed.outputs.base_branch }} branch code (push) Has been cancelled
DB migration compat / Forward-compat — Current branch code with ${{ needs.check-migrations-changed.outputs.base_branch }} branch migrations (push) Has been cancelled
DB migration compat / No migration changes (skipped) (push) Has been cancelled
## Summary

Stacked on `overview-revamp` (now rebased against `dev`). Introduces a
first-class `DataGrid` component in
`@stackframe/dashboard-ui-components`, migrates every dashboard table
off the legacy `DesignDataTable` / hand-rolled `<Table>` pattern to it,
and ships a matching dashboard design guide.

Since the last writeup the `DataGrid` runtime has been substantially
rewritten: the virtualizer now supports `rowHeight="auto"` with
`estimatedRowHeight`, every column can opt into `cellOverflow: "wrap"`,
the toolbar + header stick under a configurable `stickyTop`, and the
seeded dummy data has been fleshed out so the migrated surfaces render
with realistic density. The AI-analytics prompt was also extended with
full schema docs for the auth / team / email / payments tables so
natural-language queries produce better SQL.

**Base:** `dev` → **Head:** `ui-fixes-minor`
**Scope:** 39 files, ~+6.5k / -2.4k

## Screenshots

Captured against the seeded Demo Project on the local dashboard
(`admin@example.com` via mock GitHub OAuth). Viewport: **1920×1200**
(standard) and **2560×1440** (widescreen). Assets hosted in [this
gist](https://gist.github.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9).

### Overview — revamped metrics + line chart

| Light | Dark |
| --- | --- |
|
![overview-light](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/overview-light.jpg)
|
![overview-dark](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/overview-dark.jpg)
|

Widescreen:

| Light | Dark |
| --- | --- |
|
![overview-wide-light](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/overview-wide-light.jpg)
|
![overview-wide-dark](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/overview-wide-dark.jpg)
|

### Users — DataGrid with seeded rows

| Light | Dark |
| --- | --- |
|
![users-light](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/users-light.jpg)
|
![users-dark](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/users-dark.jpg)
|

Widescreen:

| Light | Dark |
| --- | --- |
|
![users-wide-light](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/users-wide-light.jpg)
|
![users-wide-dark](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/users-wide-dark.jpg)
|

### Transactions — new DataGridToolbar + sticky chrome

| Light | Dark |
| --- | --- |
|
![transactions-light](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/transactions-light.jpg)
|
![transactions-dark](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/transactions-dark.jpg)
|

Widescreen:

| Light | Dark |
| --- | --- |
|
![transactions-wide-light](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/transactions-wide-light.jpg)
|
![transactions-wide-dark](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/transactions-wide-dark.jpg)
|

### Teams

| Light | Dark |
| --- | --- |
|
![teams-light](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/teams-light.jpg)
|
![teams-dark](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/teams-dark.jpg)
|

Widescreen:

| Light | Dark |
| --- | --- |
|
![teams-wide-light](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/teams-wide-light.jpg)
|
![teams-wide-dark](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/teams-wide-dark.jpg)
|

### Email Outbox

| Light | Dark |
| --- | --- |
|
![email-outbox-light](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/email-outbox-light.jpg)
|
![email-outbox-dark](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/email-outbox-dark.jpg)
|

Widescreen:

| Light | Dark |
| --- | --- |
|
![email-outbox-wide-light](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/email-outbox-wide-light.jpg)
|
![email-outbox-wide-dark](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/email-outbox-wide-dark.jpg)
|

### Payments — Customers

| Light | Dark |
| --- | --- |
|
![payments-customers-light](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/payments-customers-light.jpg)
|
![payments-customers-dark](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/payments-customers-dark.jpg)
|

Widescreen:

| Light | Dark |
| --- | --- |
|
![payments-customers-wide-light](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/payments-customers-wide-light.jpg)
|
![payments-customers-wide-dark](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/payments-customers-wide-dark.jpg)
|

### Sticky behaviour — scrolled views

Grids scrolled down ~600px. The page header is still pinned, and the
`DataGrid` toolbar + column header row stay put under it (backdrop-blur
+ `stickyTop` offset) while the virtualized body rows scroll past.
Compare the scrolled view against the top-of-page view above.

| Page | Light | Dark |
| --- | --- | --- |
| Users |
![users-light-scrolled](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/users-light-scrolled.jpg)
|
![users-dark-scrolled](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/users-dark-scrolled.jpg)
|
| Teams |
![teams-light-scrolled](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/teams-light-scrolled.jpg)
|
![teams-dark-scrolled](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/teams-dark-scrolled.jpg)
|
| Transactions |
![transactions-light-scrolled](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/transactions-light-scrolled.jpg)
|
![transactions-dark-scrolled](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/transactions-dark-scrolled.jpg)
|
| Payments Customers |
![payments-customers-light-scrolled](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/payments-customers-light-scrolled.jpg)
|
![payments-customers-dark-scrolled](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/payments-customers-dark-scrolled.jpg)
|
| Email Outbox |
![email-outbox-light-scrolled](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/email-outbox-light-scrolled.jpg)
|
![email-outbox-dark-scrolled](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/email-outbox-dark-scrolled.jpg)
|
| Analytics Tables |
![analytics-tables-light-scrolled](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/analytics-tables-light-scrolled.jpg)
|
![analytics-tables-dark-scrolled](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/analytics-tables-dark-scrolled.jpg)
|

### Other migrated surfaces

| Page | Light | Dark |
| --- | --- | --- |
| Analytics Tables |
![analytics-tables-light](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/analytics-tables-light.jpg)
|
![analytics-tables-dark](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/analytics-tables-dark.jpg)
|
| Emails |
![emails-light](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/emails-light.jpg)
|
![emails-dark](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/emails-dark.jpg)
|
| Email Sent |
![email-sent-light](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/email-sent-light.jpg)
|
![email-sent-dark](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/email-sent-dark.jpg)
|
| Domains |
![domains-light](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/domains-light.jpg)
|
![domains-dark](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/domains-dark.jpg)
|
| Webhooks |
![webhooks-light](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/webhooks-light.jpg)
|
![webhooks-dark](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/webhooks-dark.jpg)
|
| External DB Sync |
![external-db-sync-light](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/external-db-sync-light.jpg)
|
![external-db-sync-dark](https://gist.githubusercontent.com/mantrakp04/2fe05ddbb2d2d7cd2d237027c909c1b9/raw/external-db-sync-dark.jpg)
|

## What's new

### `DataGrid` in `@stackframe/dashboard-ui-components`

A new, fully-typed, fully-controlled grid component under
`packages/dashboard-ui-components/src/components/data-grid/`. Single
source of truth for tabular UI across the dashboard.

Package files:
- `data-grid.tsx` — main grid renderer (virtualized rows, sticky toolbar
+ header)
- `data-grid-toolbar.tsx` — built-in toolbar (search, columns, density,
export)
- `data-grid-sizing.ts` — column width / flex / min-width resolution
- `state.ts` — state helpers (`createDefaultDataGridState`, sort /
select / paginate utilities, `exportToCsv`, date formatters)
- `strings.ts` — i18n string table + `resolveDataGridStrings`
- `types.ts` — public types (`DataGridColumnDef`, `DataGridProps`,
`DataGridState`, `DataGridDataSource`, etc.)
- `use-data-source.ts` — `useDataSource` hook with `client` / `server` /
`infinite` modes
- `index.ts` — package entrypoint

Features:
- Controlled state (`state` + `onChange`) covering sorting, pagination,
column visibility, column widths, column pinning, selection,
date-display mode, and quick search.
- Column definitions with `string` / `number` / `date` / `dateTime` /
`boolean` / `singleSelect` / `custom` types, custom `renderCell`, custom
sort comparators, per-column `parseValue` / `dateFormat`, pinning,
align, flex / min / max width.
- **Cell overflow control** — new `cellOverflow: "truncate" | "wrap"`
per column. `"wrap"` + `rowHeight="auto"` lets rows grow to fit
multi-line content.
- **Dynamic row heights** — `rowHeight` now accepts `"auto"` with an
`estimatedRowHeight` hint for the virtualizer, eliminating
scroll-position jank while rows are still being measured.
- **Sticky chrome with `stickyTop`** — the toolbar and header stick
under a caller-provided offset (matching the page header height) with a
proper blur backdrop. See the _Sticky behaviour — scrolled views_
section above for the visual.
- Client-side sort + quick-search + pagination via `useDataSource` —
consumer never pre-sorts / paginates.
- Server-side and async-generator data sources for streaming / cursor
pagination.
- Paginated and infinite-scroll UI modes.
- CSV export + clipboard copy.
- Row single / multi selection with shift-range anchor.
- Row + cell click / double-click callbacks.
- Pluggable toolbar / footer / empty / loading states and i18n strings.

### Dashboard design guide

New `apps/dashboard/DESIGN-GUIDE.md`: prescriptive, AI-readable source
of truth for dashboard UI. Documents when to use each
`design-components` primitive, the `DataGrid` canonical pattern, color /
typography / spacing / motion rules, route-specific guidance, and the
migration priority. Now also documents the new `cellOverflow` and
dynamic-`rowHeight` patterns, and marks `DesignDataTable` as deprecated
in favor of `DataGrid` + `useDataSource` + `createDefaultDataGridState`.

### Overview page revamp


`apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/line-chart.tsx`
— line chart rewritten on top of the shared `AnalyticsChart` /
`DonutChartDisplay` primitives, feeding the revamped Overview.

### Data-table migrations

Every shared table under `apps/dashboard/src/components/data-table/` has
been rewritten on top of `DataGrid`:

- `api-key-table.tsx`
- `payment-product-table.tsx`
- `permission-table.tsx`
- `team-member-search-table.tsx`
- `team-member-table.tsx`
- `team-search-table.tsx`
- `team-table.tsx`
- `transaction-table.tsx` — now also wires in `DataGridToolbar` with
search / column visibility
- `user-search-picker.tsx`
- `user-table.tsx` — extracted `USER_TABLE_COLUMNS` for readability /
reuse

### Page adoption

Page-level tables migrated to `DataGrid` (or the new `useDataSource` +
`createDefaultDataGridState` pattern):

- `(overview)/line-chart.tsx`
- `analytics/tables/query-data-grid.tsx` (now with sticky header)
- `domains/page-client.tsx`
- `email-drafts/[draftId]/page-client.tsx`
- `email-outbox/page-client.tsx` (with `DataGridToolbar`)
- `email-sent/page-client.tsx`, `grouped-email-table.tsx`,
`sent-emails-view.tsx`
- `emails/page-client.tsx`
- `external-db-sync/page-client.tsx`
- `payments/layout.tsx`, `payments/customers/page-client.tsx`,
`payments/products/[productId]/page-client.tsx`
- `users/[userId]/page-client.tsx`
- `webhooks/page-client.tsx`, `webhooks/[endpointId]/page-client.tsx`
- `design-language/page-client.tsx`,
`design-language/realistic-demo/page-client.tsx`
- `playground/page-client.tsx`

### Backend & supporting changes

- `apps/backend/src/lib/ai/prompts.ts` — extends the AI-analytics prompt
with detailed schema docs for `contact_channels`, `teams`,
`team_member_profiles`, `team_permissions`, `team_invitations`,
`email_outboxes`, `project_permissions`, `notification_preferences`,
`refresh_tokens`, and `connected_accounts`, so natural-language queries
have richer context to compile against.
- `apps/backend/src/lib/seed-dummy-data.ts` — additional OAuth providers
on seed users, improving dummy-data coverage for the migrated tables
(visible on the Users grid).
- `apps/dashboard/src/app/globals.css` — adds `--data-grid-sticky-top`
token used to derive the grid's sticky offset under the page header.
- `packages/template/src/dev-tool/dev-tool-core.ts` — persist the
"closed" state when the user closes the dev-tool panel so it doesn't
reopen on next load.

## Notes for reviewers

- Rebased onto latest `dev`; conflict in `api-key-table.tsx` resolved by
keeping the `DataGrid` implementation (consistent with the other
migrated tables).
- `DesignDataTable` is still in the codebase but marked deprecated in
the design guide — new code must use `DataGrid`.
- `DataGrid` is fully controlled: consumers must pass state + onChange,
must feed `rows` from `useDataSource` (never raw arrays), and must
define columns outside the component or via `useMemo`. The guide's §4.12
spells this out.
- `rowHeight="auto"` is opt-in; the default fixed-height virtualization
path is unchanged and remains the fast path for dense, single-line grids
(users, transactions, etc.).
- Screenshots are JPEG this round — the local capture tooling's PNG path
was producing blank frames, so the new set is `.jpg` end-to-end. Same
viewports, same seeded project.

## Test plan

- [ ] `pnpm lint` passes
- [ ] `pnpm typecheck` passes
- [ ] Load the dashboard and verify every migrated surface renders,
sorts, searches, paginates, and handles row-click navigation:
  - [ ] Overview (line chart + donut metrics)
- [ ] Users list + user detail (teams, sessions, permissions, API keys)
  - [ ] Teams list + team detail (members, permissions)
  - [ ] Domains
  - [ ] Emails, email-sent, email-outbox, email-drafts
  - [ ] Webhooks list + endpoint detail
  - [ ] Payments customers, product detail, transactions (new toolbar)
  - [ ] External DB sync
  - [ ] Analytics query table (sticky header)
- [ ] Verify infinite-scroll surfaces (domains, etc.) load additional
rows on scroll
- [ ] Verify sticky header stays below the page header in light and dark
themes
- [ ] Verify CSV export produces correct output on a representative
table
- [ ] Verify column resize, visibility toggle, and sort work across
themes
- [ ] Verify `cellOverflow: "wrap"` rows grow to fit when
`rowHeight="auto"` and clip when `rowHeight` is numeric
- [ ] Spot-check AI analytics queries against the new schema context
(contact_channels, teams, email_outboxes, …)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **New Features**
* Unified table components across dashboard with improved infinite
pagination and quick search.

* **Improvements**
* Enhanced table performance with sticky headers and better row height
handling.
* Improved sorting, filtering, and data loading with consistent state
management.
  * Better visual consistency across all data grids and table layouts.

* **UI/Styling**
* Refined table styling for better text truncation and content wrapping.
  * Optimized layout spacing and alignment across dashboard tables.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Developing-Gamer <maxcodes11110@gmail.com>
Co-authored-by: Armaan Jain <84474476+Developing-Gamer@users.noreply.github.com>
Co-authored-by: Konstantin Wohlwend <n2d4xc@gmail.com>
2026-04-27 13:50:24 -07:00
BilalG1
5423d774a2
feat(stack-cli): auto-install emulator deps on pull (#1384)
## Summary
- `stack emulator pull` now preflights VM dependencies (QEMU binaries,
socat/curl/nc/lsof/openssl/zstd, and aarch64 UEFI firmware on arm64)
before downloading.
- Missing deps are listed, then installed with user confirmation via
`brew` on macOS (bootstrapping Homebrew itself if absent) or `sudo
apt-get` on Linux.
- Skipped when `--skip-snapshot` is passed, since that path never boots
the VM.
- `gh` / `GITHUB_TOKEN` are intentionally excluded from the auto-install
set.

## Test plan
- [ ] `node packages/stack-cli/dist/index.js emulator pull` on a machine
with all deps present → no prompt, proceeds as before.
- [ ] Unlink a dep (e.g. `brew unlink zstd`) and rerun → missing dep
listed, decline prompt → exits with a clear error; accept prompt → brew
install runs and pull continues.
- [ ] `emulator pull --skip-snapshot` still bypasses the dep check.
- [ ] Linux path: missing binaries trigger `sudo apt-get update && sudo
apt-get install -y …`.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Emulator pull now detects missing host dependencies and, on
macOS/Linux with an interactive terminal and supported package tools,
shows a proposed install plan, prompts for confirmation, and can
auto-install required packages (including optional ARM64 firmware).
Homebrew will be bootstrapped if absent.
* Use --skip-snapshot to bypass the interactive dependency check and
installation.

* **Behavior**
* In non-interactive or unsupported environments, the tool falls back to
the prior preflight behavior instead of attempting installation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-27 13:01:39 -07:00
BilalG1
3b8667d5f8
cli add back init options (#1379)
<!--

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**
  * Added a "create-cloud" mode to the CLI init flow.
* New interactive project creation flow that can prompt for display name
and select/create a team-backed project.

* **Behavior Changes**
* Init now resolves mode from flags, config, or interactive prompts;
prompts to choose linking vs creating when inputs are missing.
* Non-interactive runs now error when required inputs are absent; cloud
linking offers auto-create in interactive mode.

* **Refactor**
* Centralized auth, project-creation, and env key writing for clearer,
safer linking and creation flows.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: aadesh18 <110230993+aadesh18@users.noreply.github.com>
2026-04-27 11:45:44 -07:00
BilalG1
04d57d91ed
fix(emulator): move mock OAuth off 8114 to avoid pnpm dev conflict (#1385)
## Summary
- The emulator's mock OAuth server bound to `${PORT_PREFIX}14` (8114)
inside the VM and the host forwarded the same port, colliding with `pnpm
dev`'s mock-oauth-server on 8114.
- Moves the emulator's mock OAuth to `EMULATOR_MOCK_OAUTH_PORT` (default
`26704`, joining the existing `267xx` host port block) and binds the
VM-internal mock to the same port. Same port on both sides keeps the
OIDC issuer URL (`http://localhost:26704`) resolvable identically from
the browser and from the backend inside the VM.
- Plumbed via `runtime-config.iso` as
`STACK_EMULATOR_MOCK_OAUTH_HOST_PORT`, read by cloud-init into
`STACK_OAUTH_MOCK_URL` + new `STACK_OAUTH_MOCK_PORT`;
`mock-oauth-server` now prefers `STACK_OAUTH_MOCK_PORT` so `pnpm dev`
(which doesn't set it) stays on 8114.

## Files
- `docker/local-emulator/qemu/run-emulator.sh` — new
`EMULATOR_MOCK_OAUTH_PORT`, hostfwd/ensure_ports_free/runtime.env
updates
- `docker/local-emulator/qemu/cloud-init/emulator/user-data` — reads the
host port, sets `STACK_OAUTH_MOCK_URL` + `STACK_OAUTH_MOCK_PORT`
- `apps/mock-oauth-server/src/index.ts` — honors `STACK_OAUTH_MOCK_PORT`
- `packages/stack-cli/src/commands/emulator.ts` — default + runtime.env
entry

## Test plan
- [ ] `pnpm emulator:build` succeeds and new snapshot boots
- [ ] `stack emulator start` with `pnpm dev` running on 8114 — no port
collision
- [ ] OAuth sign-in via mock provider completes end-to-end in the
emulator
- [ ] `pnpm dev` mock OAuth unchanged (still 8114)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **New Features**
* The mock OAuth server port is now configurable in the local emulator
with a sensible default, allowing custom port assignments via
environment variable.

* **Improvements**
* Updated port forwarding and environment variable handling to ensure
consistent mock OAuth endpoint configuration across host and guest
systems in the emulator.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-27 09:39:34 -07:00
BilalG1
2f719903b1
Redesign Email Server settings + managed domain flow (#1373)
Some checks failed
all-good: Did all the other checks pass? / all-good (push) Has been cancelled
Ensure Prisma migrations are in sync with the schema / check_prisma_migrations (22.x) (push) Has been cancelled
DB migration compat / Check if migrations changed (push) Has been cancelled
Docker Server Build and Push / Docker Build and Push Server (push) Has been cancelled
Docker Server Build and Run / docker (push) Has been cancelled
Runs E2E API Tests (Local Emulator) / E2E Tests (Local Emulator, Node ${{ matrix.node-version }}) (22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (mock, 22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (prod, 22.x) (push) Has been cancelled
Runs E2E API Tests with custom port prefix / build (22.x) (push) Has been cancelled
Runs E2E Fallback Tests / E2E Fallback Tests (Node ${{ matrix.node-version }}) (22.x) (push) Has been cancelled
Lint & build / lint_and_build (24) (push) Has been cancelled
TOC Generator / TOC Generator (push) Has been cancelled
DB migration compat / Back-compat — Current branch migrations with ${{ needs.check-migrations-changed.outputs.base_branch }} branch code (push) Has been cancelled
DB migration compat / Forward-compat — Current branch code with ${{ needs.check-migrations-changed.outputs.base_branch }} branch migrations (push) Has been cancelled
DB migration compat / No migration changes (skipped) (push) Has been cancelled
## Summary

Rewrites the **Email Server** section of the project email settings page
and the managed-domain setup flow. Replaces the dropdown +
conditional-fields layout with a visual four-card picker, a clearer
unsaved-state model, a stepper dialog for managed-domain onboarding, and
a consistent tracked-domains list. Also fixes two data-correctness bugs
in the managed-domain backend.

## Walkthrough (2×, dead-frames trimmed)


![walkthrough](https://raw.githubusercontent.com/stack-auth/stack-auth/pr-assets-email-ui/pr-assets-walkthrough.gif)

## Before

The saved state was a minimal dropdown, but choosing Custom SMTP /
Resend revealed a long conditional form with a hidden gear toggle for
server config, no clear "what is saved" signal, and a separate dialog
pattern for managed domains.

| Saved (Managed) | Custom SMTP selected |
|---|---|
|
![before-managed](https://raw.githubusercontent.com/stack-auth/stack-auth/pr-assets-email-ui/pr-assets-01-before-shared.png)
|
![before-smtp](https://raw.githubusercontent.com/stack-auth/stack-auth/pr-assets-email-ui/pr-assets-02-before-smtp.png)
|

## After — Provider cards

Four visual cards (Stack Shared, Managed Domain, Resend, Custom SMTP)
with updated copy. The saved provider shows a green **Current** pill;
the card the user is previewing shows an amber dashed **Draft** pill. An
amber unsaved-changes banner appears between the picker and the form
when state diverges from saved, so it is unambiguous that a click is not
yet committed.

| Saved state | Previewing a different provider |
|---|---|
|
![after-saved](https://raw.githubusercontent.com/stack-auth/stack-auth/pr-assets-email-ui/pr-assets-03-after-saved.png)
|
![after-draft](https://raw.githubusercontent.com/stack-auth/stack-auth/pr-assets-email-ui/pr-assets-04-after-draft.png)
|

Copy changes:
- **Stack Shared** — "Only default emails — no custom templates, themes,
or sender identity." (was: "Shared (noreply@stackframe.co)")
- **Managed Domain** — "Bring your own domain. You add DNS records; we
handle signing & delivery." (was: "Managed (via managed domain setup)")
- **Resend** uses the official Resend brand mark (light/dark variants in
`apps/dashboard/public/assets/`)

## After — Managed domain list + stepper dialog

Selecting **Managed Domain** immediately shows the tracked-domain list
with an **Add domain** button. Each row reflects real status (Active /
Verified / Waiting for DNS / Verifying / Failed). Exactly one domain can
be **Active** — the one matching the saved email config; every other
verified/applied domain shows a **Use this domain** button so switching
is always possible.

Adding a domain opens a 3-stage dialog with a horizontal stepper (Verify
is right-aligned for the final step). Stage 2 replaces the old bare
NS-list with a proper **Type / Name / Content** DNS records table with
per-row copy buttons.

| Tracked domains list | DNS records table |
|---|---|
|
![after-list](https://raw.githubusercontent.com/stack-auth/stack-auth/pr-assets-email-ui/pr-assets-05-after-managed-list.png)
|
![after-dns-table](https://raw.githubusercontent.com/stack-auth/stack-auth/pr-assets-email-ui/pr-assets-06-after-dns-table.png)
|

## Bug fixes

- **Backend: applying a managed domain did not demote previously-applied
ones.** Multiple rows could end up with status `APPLIED` even though
only one could be in the saved config. New helper
`demoteOtherAppliedManagedEmailDomains({ tenancyId, keepId })` runs
inside `applyManagedEmailProvider` to demote all other applied rows in
the tenancy back to `VERIFIED` before marking the new one.
- **Frontend: "Use this domain" only appeared for `status ===
verified`.** A domain that had been applied then replaced could never be
re-applied from the UI. Button now appears for any `verified` or
`applied` row that is not currently in use; the **Active** label is
derived from config match instead of DB status.
- **Dev mock onboarding now mirrors production timing.**
`shouldUseMockManagedEmailOnboarding()` used to insert domains as
`verified` synchronously. Now the domain is created as
`pending_verification`, and a fire-and-forget `runAsynchronously(() =>
wait(1000))` updates it to `verified` — mirroring the real Resend
webhook flow so the UI states (pending → verifying → verified) are
exercised in local dev.

## Test plan
- [ ] Cards: clicking each card shows `Draft` pill + amber banner;
Discard restores; Save commits and flips `Current` to the new card
- [ ] Managed: Add domain → stage 1 input → stage 2 DNS table + copy →
Check verification flips to stage 3 → Use this domain sets it Active and
demotes the previously-active domain in the list
- [ ] Managed: clicking **Use this domain** on a non-active verified row
makes it Active and the previously-active row back to Verified
- [ ] Shared / Resend / SMTP: existing save + test-email flows still
work (logic preserved verbatim)
- [ ] `pnpm typecheck` (dashboard + backend) and `pnpm lint` pass

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Redesigned email domain setup flow with multi-step verification dialog
  * Added copy-to-clipboard for DNS records
* Enhanced provider selection interface with improved visual
presentation
* Onboarding now shows initial "pending verification" state and
completes verification asynchronously

* **Bug Fixes**
* Ensures only one managed domain becomes active when applying a domain
  * Improved error handling for email configuration saves

* **Tests**
  * Updated end-to-end tests to reflect async verification timing
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-24 13:35:03 -07:00
BilalG1
d1751a7634
fix(init-prompt): require StackProvider for all frameworks (#1374)
## Summary
- The init prompt marked the `StackProvider` step as *React only* and
placed it after the `StackHandler` step. Following it on a Next.js
project produced a layout with no provider, so `StackHandler` crashed at
runtime with `useStackApp must be used within a StackProvider`.
- Make the provider step unconditional and move it ahead of the handler
step so the dependency order matches the instruction order. Also quote
the exact error message so the model won't skip it.

## Test plan
- [ ] Run `npx @stackframe/stack-cli init` (or the web flow) against a
fresh Next.js app and confirm `/handler/[...stack]` renders without the
`useStackApp` error.
- [ ] Re-run against a Vite/React app to confirm the reordered
instructions still produce a working setup.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Documentation**
* Added explicit global MCP config file path guidance for several coding
agents.
* Documented required provider configuration across supported
frameworks.
* Clarified where to place provider wrappers in root app layouts
(including Next.js app/layout).
* Reordered setup steps to surface the required "Wrap your app in a
Stack provider" step and updated step numbering.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-24 12:27:49 -07:00
BilalG1
4a2595d9f7
Classify ClickHouse NO_COMMON_TYPE (386) as unsafe (#1380)
## Summary
- Add ClickHouse error code `386` (`NO_COMMON_TYPE`) to
`UNSAFE_CLICKHOUSE_ERROR_CODES` in
`apps/backend/src/lib/clickhouse-errors.ts`. This stops the Sentry
`StackAssertionError` (`Unknown Clickhouse error: code 386 not in safe
or unsafe codes`) that was firing whenever an admin wrote a query like
`SELECT [1, 'a']` or `SELECT if(1, 'a', 1)`, while keeping the raw error
message out of prod responses.
- Add two e2e regression tests: one against the cross-project
`analytics_internal.users` table, and one against `system.query_log`, to
pin that 386 is wrapped with the generic `Error during execution of this
query.` message in prod (full detail only surfaces in dev/test).

## Why unsafe, not safe
Both callers of `getSafeClickhouseErrorMessage`
(`apps/backend/src/app/api/latest/internal/analytics/query/route.ts:59`
and `apps/backend/src/lib/ai/tools/sql-query.ts:80`) execute
caller-authored SQL under `readonly: "1"` with
`SQL_project_id`/`SQL_branch_id` scoping. The ClickHouse client runs
under a `limited_user` whose grants restrict most tables — but
ClickHouse resolves types **before** enforcing ACL. That means a query
like `SELECT if(1, query, 1) FROM system.query_log` surfaces code 386
with a message like `There is no supertype for types String, UInt8 ...`,
leaking that `system.query_log.query` is a `String` — schema info from a
table the caller can't actually read.

This is the same type-before-ACL class as code 43
(`ILLEGAL_TYPE_OF_ARGUMENT`), which is already classified unsafe.
Classifying 386 as unsafe keeps the defense-in-depth consistent: if
per-customer tables are ever introduced and grants don't block
reference-resolution in time, 386 won't leak their schema.

Cost: in prod, an admin writing a malformed type-mismatch query sees
only `Error during execution of this query.` instead of the supertype
hint. Dev and test environments still show the full error via the
existing `getNodeEnvironment()` branch, so local iteration is
unaffected.

## Test plan
- [x] `pnpm test run
apps/e2e/tests/backend/endpoints/api/v1/analytics-query.test.ts` — all
64 tests pass, including the two 386 regression tests.
- [ ] Monitor Sentry after deploy to confirm the
`unknown-clickhouse-error-for-query` events for code 386 stop firing.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved handling of a ClickHouse type-mismatch error to prevent
exposure of sensitive data and ensure sanitized error responses.

* **Tests**
* Added regression tests that verify error responses are sanitized,
return consistent error codes, and include expected headers without
leaking internal details.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-24 12:07:16 -07:00
Mantra
cbd945e3a6
[codex] Fix Neon malformed Basic auth validation (#1381)
## What changed

This fixes Sentry issue
[STACK-BACKEND-1A3](https://stackframe-pw.sentry.io/issues/7436639623/?project=4507442898272256&query=is%3Aunresolved&referrer=issue-stream&seerDrawer=true).

A request with this malformed header:

```http
Authorization: Basic
```

used to crash the Neon auth validator with a `StackAssertionError`,
which turned a bad client request into a 500.

The fix makes `neonAuthorizationHeaderSchema` only validate Neon client
credentials after the Basic auth header successfully decodes. If
decoding fails, the Neon-specific validator returns `true` and lets
`basicAuthorizationHeaderSchema` produce the intended 400 schema error:
`Authorization header must be in the format "Basic <base64>"`.

## Reviewer walkthrough

There are two checks chained together:

1. `basicAuthorizationHeaderSchema` checks that the header is
structurally valid Basic auth.
2. `neonAuthorizationHeaderSchema` checks that the decoded
`client_id:client_secret` matches a configured Neon client.

Yup may still run the second check after the first one has failed,
because route validation collects errors with `abortEarly: false`. The
old code assumed the first check had already passed and called
`throwErr(...)` when decoding returned `null`. This PR changes that path
to return `true`, because the format error is already owned by the first
check.

## Tests

- `pnpm -C packages/stack-shared exec vitest run --maxWorkers=1
--minWorkers=1 src/schema-fields.ts`
- `pnpm -C apps/e2e exec vitest run --maxWorkers=1 --minWorkers=1
tests/backend/endpoints/api/v1/integrations/neon/projects/transfer.test.ts
-t "malformed"`
- `pnpm -C packages/stack-shared lint`
- `pnpm -C packages/stack-shared typecheck`
- `pnpm -C apps/e2e lint`
- `pnpm -C apps/e2e typecheck`

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Enhanced authorization header validation in API endpoints with
improved error handling, ensuring malformed credentials return clear,
specific validation error messages.

* **Tests**
* Added comprehensive end-to-end test coverage for API request
validation, including edge cases for authorization headers.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-24 11:59:18 -07:00