Skip to content

Commit 883aa07

Browse files
committed
merge dev into
1 parent 27b1e1c commit 883aa07

110 files changed

Lines changed: 34314 additions & 1598 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/CLAUDE-KNOWLEDGE.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,9 @@ A: Check the error location:
217217
- Callback endpoint (400 error) - Validation failed during callback
218218
- Token endpoint (400 error) - Validation failed during token exchange
219219

220+
### Q: How should connected-account OAuth access-token refresh errors be classified?
221+
A: In `apps/backend/src/oauth/providers/base.tsx`, invalid/revoked refresh-token provider errors such as `invalid_grant` return `Result.error({ type: "invalid-refresh-token", ... })` so `access-token-helpers.tsx` can invalidate that stored refresh token and try another. Transient provider/network failures such as openid-client `RPError: outgoing request timed out after 3500ms` return `Result.error({ type: "temporarily-unavailable", cause })`; the connected-account helper converts that to `OAuthProviderTemporarilyUnavailable` without invalidating the refresh token. Expected refresh outcomes should be represented in the provider return type instead of thrown as known/status errors. Refresh requests use a 6s openid-client HTTP timeout and retry transient failures once. If a retry sees `invalid_grant` after an ambiguous transient failure, keep treating it as temporarily unavailable rather than invalidating the refresh token, because the first request may have reached the provider and rotated the token before our client timed out. Sentry should capture non-revocation refresh issues (temporary provider failure, invalid client, unexpected) with provider id/class, attempts, retry count, ambiguity state, final cause, and all provider errors seen during attempts; normal revoked/expired refresh tokens should not be reported.
222+
220223
## Git and Development Workflow
221224

222225
### Q: How should you format git commit messages in this project?
@@ -356,6 +359,33 @@ Then restart the dev server. This rebuilds all packages and generates the necess
356359
## Q: How is backwards compatibility for the offer→product rename handled in the payments purchase APIs?
357360
A: API v1 requests are routed through the `v2beta1` migration. The migration wraps the latest handlers, accepts legacy `offer_id`/`offer_inline` request fields, translates product-related errors back to the old offer error codes/messages, and augments responses (like `validate-code`) with `offer`/`conflicting_group_offers` aliases alongside the new `product` fields. Newer API versions keep the product-only contract.
358361

362+
### Q: What's the reliable way to run targeted tests across backend, dashboard, stack-shared, and e2e at once?
363+
A: Run from the monorepo root with explicit file paths: `pnpm test run "<path1>" "<path2>" ...`. This works even when individual packages do not define a local `test` script. Also avoid passing an extra `run` argument to package-level `test` scripts that already execute `vitest run`.
364+
365+
### Q: What's the new Authorization header format for Stack token forwarding?
366+
A: Use `getAuthorizationHeader()`, which returns `Bearer stackauth_<base64(getAuthJson())>`. The payload encodes both `accessToken` and `refreshToken`, and request-like token stores should parse this format first, with legacy `x-stack-auth` remaining as a backward-compatible fallback.
367+
368+
### Q: What RequestLike header shapes are supported by tokenStore overrides?
369+
A: `RequestLike` accepts both `{ headers: { get(name): string | null } }` and `{ headers: Record<string, string | null> }`. Header lookup is case-insensitive for record-style headers, and supports `authorization`, `x-stack-auth`, and `cookie`.
370+
371+
### Q: Which env var should emulator onboarding URLs use for dashboard port?
372+
A: Use `EMULATOR_DASHBOARD_PORT` (default `26700`) or explicit `STACK_LOCAL_EMULATOR_DASHBOARD_URL`. Do not derive emulator URLs from `NEXT_PUBLIC_STACK_PORT_PREFIX`, because that points to the host dev environment ports (e.g. `92xx`) rather than the emulator host-forwarded ports.
373+
374+
### Q: Why does `PATCH /api/v1/internal/projects/current` fail in local emulator when updating only `onboarding_state`?
375+
A: `createOrUpdateProjectWithLegacyConfig` always called `overrideEnvironmentConfigOverride`, even when there were zero config override keys to apply. In local emulator mode, environment config overrides are intentionally blocked, so this threw `Environment configuration overrides cannot be changed in the local emulator` and returned 500. The fix is to skip `overrideEnvironmentConfigOverride` unless `configOverrideOverride` has at least one key.
376+
377+
### Q: Why might local emulator UI changes in `apps/dashboard` not appear immediately at `localhost:26700`?
378+
A: The QEMU local emulator serves the dashboard from the Docker image bundled inside the VM, not from the host repo's live source tree. Source edits in `apps/dashboard` are reflected in lint/typecheck/tests immediately, but you need an updated emulator image/runtime to see the visual change on `26700`.
379+
380+
### Q: Why can local emulator onboarding break with `ParseError` on non-`.ts` config files (e.g. `test-config.untracked`)?
381+
A: The emulator writes TypeScript-style config source (`import type ...` and `config: StackConfig`) and later evaluates it with Jiti. If the filename has a non-TS extension, Jiti may parse it as plain JS and fail. Fix by evaluating unknown extensions as TypeScript (use a `.ts` eval filename fallback) and add regression coverage for non-`.ts` config paths.
382+
383+
### Q: How should docs fetch the canonical AI setup prompt text?
384+
A: Expose an unauthenticated backend endpoint at `/api/v1/setup-prompt` that returns `getSdkSetupPrompt("ai-prompt", { tanstackQuery: false })` as plain text and sets `Cache-Control: public, max-age=60`. Mintlify docs should fetch `https://api.stack-auth.com/api/v1/setup-prompt` directly when docs and API are on different origins.
385+
386+
### Q: Can Mintlify snippets import other snippets?
387+
A: No. Keep snippet logic inline within each snippet file; avoid snippet-to-snippet imports. For setup prompt fetching, point directly to `https://api.stack-auth.com/api/v1/setup-prompt` when docs run on a different origin/port than the API.
388+
359389
## Q: How does `/api/v1/ai/query/generate` reject invalid AI tool names?
360390
A: Invalid `tools` entries are rejected by `requestBodySchema` in `apps/backend/src/lib/ai/schema.ts` via `yupString().oneOf(TOOL_NAMES)`, so the endpoint returns a structured `SCHEMA_ERROR` object mentioning `body.tools[n]` rather than a custom `"Invalid tool names"` string from handler logic.
361391

@@ -364,3 +394,9 @@ A: The `/api/v1/internal/metrics` response now intentionally includes `analytics
364394

365395
## Q: Why can environment config override writes fail with a product/product-line customer type warning after creating a preview project?
366396
A: The environment override endpoint validates the new environment override against the rendered branch config. Preview dummy payments data must therefore be internally coherent: products assigned to a product line need the same `customerType` as that product line, otherwise unrelated environment patches can fail with warnings like `Product "growth" has customer type "user" but its product line "workspace" has customer type "team"`.
397+
398+
## Q: Why can `pnpm run dev` fail with `ERR_MODULE_NOT_FOUND` for `@stackframe/stack/dist/esm/index.js` during OpenAPI docs generation?
399+
A: Root `dev` starts the OpenAPI docs watcher at the same time as package `dev` watchers. If a package `dev` script removes `dist` before `tsdown --watch` recreates it, the docs generator can import `apps/backend/src/stack.tsx` while `@stackframe/stack`'s ESM entrypoint is temporarily missing. Package watch scripts should update `dist` in place, and eager generators should wait for package imports to resolve before running.
400+
401+
## Q: How do SDK source tests replace the compile-time client version sentinel?
402+
A: `packages/template/vitest.config.ts` installs a Vite transform plugin for Vitest that replaces `STACK_COMPILE_TIME_CLIENT_PACKAGE_VERSION_SENTINEL` with `js <package-name>@<version>` from the local package.json. Keep the plugin in `packages/template` so `pnpm pre`/`scripts/generate-sdks.ts` propagates it to `packages/js`, `packages/react`, and `packages/stack`; otherwise tests importing `common.ts` throw `Client version was not replaced` before test collection.

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ To see all development ports, refer to the index.html of `apps/dev-launchpad/pub
7474
- Always run typecheck, lint, and test to make sure your changes are working as expected. You can save time by only linting and testing the files you've changed (and/or related E2E tests).
7575
- The project uses a custom route handler system in the backend for consistent API responses
7676
- When writing tests, prefer .toMatchInlineSnapshot over other selectors, if possible. You can check (and modify) the snapshot-serializer.ts file to see how the snapshots are formatted and how non-deterministic values are handled.
77-
- Whenever you learn something new, or at the latest right before you call the `Stop` tool, write whatever you learned into the ./claude/CLAUDE-KNOWLEDGE.md file, in the Q&A format in there. You will later be able to look up knowledge from there (based on the question you asked).
77+
- Whenever you learn something new, or at the latest right before you call the `Stop` tool, write whatever you learned into the .claude/CLAUDE-KNOWLEDGE.md file, in the Q&A format in there. You will later be able to look up knowledge from there (based on the question you asked).
7878
- Animations: Keep hover/click transitions snappy and fast. Don't delay the action with a pre-transition (e.g. no fade-in when hovering a button) — it makes the UI feel sluggish. Instead, apply transitions after the action, like a smooth fade-out when the hover ends.
7979
- Whenever you make changes in the dashboard, provide the user with a deep link to the dashboard page that you've just changed. Usually, this takes the form of `http://localhost:<whatever-is-in-$NEXT_PUBLIC_STACK_PORT_PREFIX>01/projects/-selector-/...`, although sometimes it's different. If $NEXT_PUBLIC_STACK_PORT_PREFIX is set to 91, 92, or 93, use `a.localhost`, `b.localhost`, and `c.localhost` for the domains, respectively.
8080
- To update the list of apps available, edit `apps-frontend.tsx` and `apps-config.ts`. When you're tasked to implement a new app or a new page, always check existing apps for inspiration on how you could implement the new app or page.

apps/backend/.env

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ STACK_SPOTIFY_CLIENT_SECRET=# client secret
3434

3535
STACK_ALLOW_SHARED_OAUTH_ACCESS_TOKENS=# allow shared oauth provider to also use connected account access token, this should only be used for development and testing
3636

37+
STACK_DISABLE_PLAN_LIMITS=# set to "true" to bypass enforcement of Stack Auth's own internal-tenancy plan limits (analytics_events, session_replays, emails_per_month, dashboard_admins seat cap, auth_users soft cap, analytics_timeout_seconds). Default unset/false preserves enforcement. Intended as a temporary cutover safety net while the plan-limits infrastructure rolls out — customer projects' own item APIs are unaffected by this flag.
38+
3739
# Email
3840
# For local development, you can spin up a local SMTP server like inbucket
3941
STACK_EMAIL_HOST=# for local inbucket: 127.0.0.1

apps/backend/.env.development

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ STACK_SPOTIFY_CLIENT_SECRET=MOCK
4343

4444
STACK_ALLOW_SHARED_OAUTH_ACCESS_TOKENS=true
4545

46+
# Default to enforcing plan limits in local dev so behavior matches prod.
47+
# Flip to "true" to bypass every Stack-Auth-internal plan-limit enforcement
48+
# site (e.g. session_replays, analytics_events, emails_per_month). See
49+
# apps/backend/src/lib/plan-entitlements.ts:arePlanLimitsEnforced.
50+
STACK_DISABLE_PLAN_LIMITS=false
51+
4652
STACK_DATABASE_CONNECTION_STRING=postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}28/stackframe
4753
STACK_DATABASE_REPLICA_CONNECTION_STRING=postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}34/stackframe
4854
STACK_DATABASE_REPLICATION_WAIT_STRATEGY=pg-stat-replication

apps/backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@stackframe/backend",
33
"version": "2.8.88",
4-
"repository": "https://github.com/stack-auth/stack-auth",
4+
"repository": "https://github.com/hexclave/stack-auth",
55
"private": true,
66
"type": "module",
77
"scripts": {
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ALTER TABLE "Project"
2+
ADD COLUMN "onboardingState" JSONB;
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { randomUUID } from "crypto";
2+
import type { Sql } from "postgres";
3+
import { expect } from "vitest";
4+
5+
export const preMigration = async (sql: Sql) => {
6+
const projectId = `test-${randomUUID()}`;
7+
await sql`
8+
INSERT INTO "Project" ("id", "createdAt", "updatedAt", "displayName", "description", "isProductionMode")
9+
VALUES (${projectId}, NOW(), NOW(), 'Onboarding State Project', '', false)
10+
`;
11+
return { projectId };
12+
};
13+
14+
export const postMigration = async (sql: Sql, ctx: Awaited<ReturnType<typeof preMigration>>) => {
15+
const rows = await sql`
16+
SELECT "onboardingState"
17+
FROM "Project"
18+
WHERE "id" = ${ctx.projectId}
19+
`;
20+
expect(rows).toHaveLength(1);
21+
expect(rows[0].onboardingState).toBeNull();
22+
23+
const onboardingState = {
24+
selected_config_choice: "create-new",
25+
selected_apps: ["authentication", "emails"],
26+
selected_sign_in_methods: ["credential", "magicLink"],
27+
selected_email_theme_id: null,
28+
selected_payments_country: "US",
29+
};
30+
await sql`
31+
UPDATE "Project"
32+
SET "onboardingState" = ${JSON.stringify(onboardingState)}::jsonb
33+
WHERE "id" = ${ctx.projectId}
34+
`;
35+
36+
const updatedRows = await sql`
37+
SELECT "onboardingState"::text AS "onboardingState"
38+
FROM "Project"
39+
WHERE "id" = ${ctx.projectId}
40+
`;
41+
expect(updatedRows).toHaveLength(1);
42+
expect(JSON.parse(updatedRows[0].onboardingState)).toMatchInlineSnapshot(`
43+
{
44+
"selected_apps": [
45+
"authentication",
46+
"emails",
47+
],
48+
"selected_config_choice": "create-new",
49+
"selected_email_theme_id": null,
50+
"selected_payments_country": "US",
51+
"selected_sign_in_methods": [
52+
"credential",
53+
"magicLink",
54+
],
55+
}
56+
`);
57+
};

apps/backend/prisma/schema.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ model Project {
2727
isProductionMode Boolean
2828
ownerTeamId String? @db.Uuid
2929
onboardingStatus String @default("completed")
30+
onboardingState Json?
3031
3132
logoUrl String?
3233
logoFullUrl String?

apps/backend/scripts/generate-openapi-fumadocs.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { webhookEvents } from '@stackframe/stack-shared/dist/interface/webhooks'
44
import { writeFileSyncIfChanged } from '@stackframe/stack-shared/dist/utils/fs';
55
import { HTTP_METHODS } from '@stackframe/stack-shared/dist/utils/http';
66
import { typedKeys } from '@stackframe/stack-shared/dist/utils/objects';
7+
import { stringCompare } from '@stackframe/stack-shared/dist/utils/strings';
78
import fs from 'fs';
89
import { glob } from 'glob';
910
import path from 'path';
@@ -29,7 +30,7 @@ async function main() {
2930
// Generate OpenAPI specs for each audience (let parseOpenAPI handle the filtering)
3031
const filePathPrefix = path.resolve(process.platform === "win32" ? "apps/src/app/api/latest" : "src/app/api/latest");
3132
const importPathPrefix = "@/app/api/latest";
32-
const filePaths = [...await glob(filePathPrefix + "/**/route.{js,jsx,ts,tsx}")];
33+
const filePaths = [...await glob(filePathPrefix + "/**/route.{js,jsx,ts,tsx}")].sort((a, b) => stringCompare(a, b));
3334

3435
const endpoints = new Map(await Promise.all(filePaths.map(async (filePath) => {
3536
if (!filePath.startsWith(filePathPrefix)) {

apps/backend/src/app/api/latest/analytics/events/batch/route.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { getClickhouseAdminClient } from "@/lib/clickhouse";
2-
import { getBillingTeamId } from "@/lib/plan-entitlements";
2+
import { arePlanLimitsEnforced, getBillingTeamId } from "@/lib/plan-entitlements";
33
import { findRecentSessionReplay } from "@/lib/session-replays";
44
import { getStackServerApp } from "@/stack";
55
import { getPrismaClientForTenancy } from "@/prisma-client";
@@ -121,7 +121,7 @@ export const POST = createSmartRouteHandler({
121121
const app = getStackServerApp();
122122

123123
const billingTeamId = getBillingTeamId(auth.tenancy.project);
124-
if (billingTeamId != null) {
124+
if (billingTeamId != null && arePlanLimitsEnforced()) {
125125
const eventsItem = await app.getItem({ itemId: ITEM_IDS.analyticsEvents, teamId: billingTeamId });
126126
const isDebited = await eventsItem.tryDecreaseQuantity(body.events.length);
127127
if (!isDebited) {

0 commit comments

Comments
 (0)