Skip to content

Commit 4c25368

Browse files
authored
Merge branch 'dev' into custom-dashboards-versioning-fix
2 parents 8299c9d + bd8c448 commit 4c25368

107 files changed

Lines changed: 34157 additions & 1622 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: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,33 @@ Then restart the dev server. This rebuilds all packages and generates the necess
356356
## Q: How is backwards compatibility for the offer→product rename handled in the payments purchase APIs?
357357
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.
358358

359+
### Q: What's the reliable way to run targeted tests across backend, dashboard, stack-shared, and e2e at once?
360+
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`.
361+
362+
### Q: What's the new Authorization header format for Stack token forwarding?
363+
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.
364+
365+
### Q: What RequestLike header shapes are supported by tokenStore overrides?
366+
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`.
367+
368+
### Q: Which env var should emulator onboarding URLs use for dashboard port?
369+
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.
370+
371+
### Q: Why does `PATCH /api/v1/internal/projects/current` fail in local emulator when updating only `onboarding_state`?
372+
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.
373+
374+
### Q: Why might local emulator UI changes in `apps/dashboard` not appear immediately at `localhost:26700`?
375+
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`.
376+
377+
### Q: Why can local emulator onboarding break with `ParseError` on non-`.ts` config files (e.g. `test-config.untracked`)?
378+
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.
379+
380+
### Q: How should docs fetch the canonical AI setup prompt text?
381+
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.
382+
383+
### Q: Can Mintlify snippets import other snippets?
384+
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.
385+
359386
## Q: How does `/api/v1/ai/query/generate` reject invalid AI tool names?
360387
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.
361388

@@ -364,3 +391,6 @@ A: The `/api/v1/internal/metrics` response now intentionally includes `analytics
364391

365392
## Q: Why can environment config override writes fail with a product/product-line customer type warning after creating a preview project?
366393
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"`.
394+
395+
## Q: Why can `pnpm run dev` fail with `ERR_MODULE_NOT_FOUND` for `@stackframe/stack/dist/esm/index.js` during OpenAPI docs generation?
396+
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.

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 & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ STACK_SEED_INTERNAL_PROJECT_USER_EMAIL=# default user added to the dashboard
1414
STACK_SEED_INTERNAL_PROJECT_USER_PASSWORD=# default user's password, paired with STACK_SEED_INTERNAL_PROJECT_USER_EMAIL
1515
STACK_SEED_INTERNAL_PROJECT_USER_INTERNAL_ACCESS=# if the default user has access to the internal dashboard project
1616
STACK_SEED_INTERNAL_PROJECT_USER_GITHUB_ID=# add github oauth id to the default user
17-
STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY=# default publishable client key for the internal project
18-
STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY=# default secret server key for the internal project
17+
STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY=# default publishable client key for the internal project
18+
STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY=# default secret server key for the internal project
1919
STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY=# default super secret admin key for the internal project
2020

2121
# OAuth mock provider settings

apps/backend/.env.development

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ STACK_SEED_INTERNAL_PROJECT_ALLOW_LOCALHOST=true
1313
STACK_SEED_INTERNAL_PROJECT_OAUTH_PROVIDERS=github,spotify,google,microsoft
1414
STACK_SEED_INTERNAL_PROJECT_USER_GITHUB_ID=admin@example.com
1515
STACK_SEED_INTERNAL_PROJECT_USER_INTERNAL_ACCESS=true
16-
STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
17-
STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY=this-secret-server-key-is-for-local-development-only
16+
STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
17+
STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY=this-secret-server-key-is-for-local-development-only
1818
STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY=this-super-secret-admin-key-is-for-local-development-only
1919

2020
STACK_OAUTH_MOCK_URL=http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}14

apps/backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@stackframe/backend",
3-
"version": "2.8.87",
3+
"version": "2.8.88",
44
"repository": "https://github.com/stack-auth/stack-auth",
55
"private": true,
66
"type": "module",
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"
38+
FROM "Project"
39+
WHERE "id" = ${ctx.projectId}
40+
`;
41+
expect(updatedRows).toHaveLength(1);
42+
expect(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/prisma/seed.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -362,18 +362,18 @@ export async function seed() {
362362
// internal project using the pck stored here, so it must land before the rest
363363
// of the seed even if something later fails.
364364
const isLocalEmulator = process.env.NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR === 'true';
365-
const rawPck = process.env.STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY;
365+
const rawPck = process.env.STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY;
366366
if (isLocalEmulator && !rawPck) {
367367
// Emulator images build before a per-VM pck is available. Runtime boots set
368-
// STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY from the VM-generated
368+
// STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY from the VM-generated
369369
// random value and re-run the seed, which upserts the internal key set then.
370370
console.log('Skipping internal API key set (no pck provided; emulator mode).');
371371
} else {
372372
const keySet = {
373-
publishableClientKey: rawPck || throwErr('STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY is not set'),
373+
publishableClientKey: rawPck || throwErr('STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY is not set'),
374374
secretServerKey: isLocalEmulator
375-
? (process.env.STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY ?? null)
376-
: (process.env.STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY || throwErr('STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY is not set')),
375+
? (process.env.STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY ?? null)
376+
: (process.env.STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY || throwErr('STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY is not set')),
377377
superSecretAdminKey: isLocalEmulator
378378
? (process.env.STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY ?? null)
379379
: (process.env.STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY || throwErr('STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY is not set')),

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)) {

0 commit comments

Comments
 (0)