Merge branch 'dev' into s3

This commit is contained in:
Zai Shi 2025-07-30 22:03:22 +02:00 committed by GitHub
commit f5d66561dc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
66 changed files with 629 additions and 182 deletions

View File

@ -1,31 +1,76 @@
# Development Guidelines for Stack Auth
# CLAUDE.md
## Build/Test/Lint Commands
- Build: `pnpm build` (all), `pnpm build:packages` (packages only), `pnpm build:backend` (backend)
- Lint: `pnpm lint` (zero warnings allowed)
- Typecheck: `pnpm typecheck`
- Test: `pnpm test` (all), `pnpm test:unit` (unit tests), `pnpm test:e2e` (e2e tests)
- Run single test: `pnpm test path/to/test.test.ts` or `pnpm test -t "test name pattern"`
- Start dependencies: `pnpm start-deps` (DB, services), `pnpm stop-deps` (shutdown)
- Dev mode: `pnpm dev` (all services) or `pnpm dev:basic` (backend+dashboard)
- Prisma CLI: `pnpm prisma` (use instead of the `prisma` command)
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Coding Guidelines
- TypeScript with strict types, prefer `type` over `interface`
- Avoid casting to `any`; Prefer making changes to the API so that `any` casts are unnecessary to access a property or method
- 2-space indentation, spaces in braces, semicolons required
- Return promises with `return await`, no floating promises
- Proper error handling for async code with try/catch
- Use helper functions: `yupXyz()` for validation, `getPublicEnvVar()` for env
- Switch cases must use blocks
- React Server Components preferred where applicable
- No direct 'use' imports from React (use React.use instead)
- Follow existing file structure and naming patterns
## Development Commands
## Testing Guidelines
- Import test utilities from `/apps/e2e/test/helpers.ts`
- Prefer inline snapshot testing with `expect(response).toMatchInlineSnapshot(...)`
### Essential Commands
- **Install dependencies**: `pnpm install`
- **Build packages**: `pnpm build:packages`
- **Generate code**: `pnpm codegen`
- **Start dependencies**: `pnpm restart-deps` (resets & restarts Docker containers for DB, Inbucket, etc. Usually already called by the user)
- **Run development**: `pnpm dev` (starts all services on different ports. Usually already started by the user in the background)
- **Run minimal dev**: `pnpm dev:basic` (only backend and dashboard for resource-limited systems)
- **Run tests**: `pnpm test --no-watch` (uses Vitest). You can filter with `pnpm test --no-watch <file-filters>`
- **Lint code**: `pnpm lint`
- **Type check**: `pnpm typecheck`
## Monorepo Structure
Managed with Turbo and pnpm workspaces. Core packages in `packages/`, apps in `apps/`.
`packages/stack` is generated and will not be committed into the repository; change the files in `packages/template` instead.
### Testing
- **Run all tests**: `pnpm test --no-watch`
- **Run some tests**: `pnpm test --no-watch <file-filters>`
### Database Commands
- **Generate migration**: `pnpm db:migration-gen`
- **Reset database** (rarely used): `pnpm db:reset`
- **Seed database** (rarely used): `pnpm db:seed`
- **Initialize database** (rarely used): `pnpm db:init`
- **Run migrations** (rarely used): `pnpm db:migrate`
## Architecture Overview
Stack Auth is a monorepo using Turbo for build orchestration. The main components are:
### Apps (`/apps`)
- **backend** (`/apps/backend`): Next.js API backend running on port 8102
- Main API routes in `/apps/backend/src/app/api/latest`
- Database models using Prisma
- **dashboard** (`/apps/dashboard`): Admin dashboard on port 8101
- **dev-launchpad**: Development portal on port 8100
- **e2e**: End-to-end tests
### Packages (`/packages`)
- **stack** (`/packages/stack`): Main Next.js SDK
- **stack-shared** (`/packages/stack-shared`): Shared utilities and types
- **stack-ui** (`/packages/stack-ui`): UI components
- **react** (`/packages/react`): React SDK
- **js** (`/packages/js`): JavaScript SDK
### Key Technologies
- **Framework**: Next.js (with App Router)
- **Database**: PostgreSQL with Prisma ORM
- **Testing**: Vitest
- **Package Manager**: pnpm with workspaces
- **Build Tool**: Turbo
- **TypeScript**: Used throughout
- **Styling**: Tailwind CSS
### API Structure
The API follows a RESTful design with routes organized by resource type:
- Auth endpoints: `/api/latest/auth/*`
- User management: `/api/latest/users/*`
- Team management: `/api/latest/teams/*`
- OAuth providers: `/api/latest/oauth-providers/*`
### Development Ports
- 8100: Dev launchpad
- 8101: Dashboard
- 8102: Backend API
- 8103: Demo app
- 8104: Documentation
- 8105: Inbucket (email testing)
- 8106: Prisma Studio
## Important Notes
- Environment variables are pre-configured in `.env.development` files
- Code generation (`pnpm codegen`) must be run after schema changes
- The project uses a custom route handler system in the backend for consistent API responses

View File

@ -1,5 +1,13 @@
# @stackframe/stack-backend
## 2.8.27
### Patch Changes
- Various changes
- Updated dependencies
- @stackframe/stack-shared@2.8.27
## 2.8.26
### Patch Changes

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/stack-backend",
"version": "2.8.26",
"version": "2.8.27",
"private": true,
"scripts": {
"clean": "rimraf src/generated && rimraf .next && rimraf node_modules",

View File

@ -1,6 +1,5 @@
import { overrideEnvironmentConfigOverride } from "@/lib/config";
import { getActiveEmailTheme, renderEmailWithTemplate } from "@/lib/email-rendering";
import { globalPrismaClient } from "@/prisma-client";
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
import { KnownErrors } from "@stackframe/stack-shared/dist/known-errors";
import { adaptSchema, templateThemeIdSchema, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
@ -55,7 +54,6 @@ export const PATCH = createSmartRouteHandler({
}
await overrideEnvironmentConfigOverride({
tx: globalPrismaClient,
projectId: tenancy.project.id,
branchId: tenancy.branchId,
environmentConfigOverrideOverride: {

View File

@ -1,5 +1,4 @@
import { overrideEnvironmentConfigOverride } from "@/lib/config";
import { globalPrismaClient } from "@/prisma-client";
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
import { adaptSchema, templateThemeIdSchema, yupArray, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
import { filterUndefined, typedEntries } from "@stackframe/stack-shared/dist/utils/objects";
@ -94,7 +93,6 @@ export const POST = createSmartRouteHandler({
`;
await overrideEnvironmentConfigOverride({
tx: globalPrismaClient,
projectId: tenancy.project.id,
branchId: tenancy.branchId,
environmentConfigOverrideOverride: {

View File

@ -83,7 +83,6 @@ export const PATCH = createSmartRouteHandler({
throw new KnownErrors.EmailRenderingError(result.error);
}
await overrideEnvironmentConfigOverride({
tx: globalPrismaClient,
projectId: tenancy.project.id,
branchId: tenancy.branchId,
environmentConfigOverrideOverride: {

View File

@ -1,5 +1,4 @@
import { overrideEnvironmentConfigOverride } from "@/lib/config";
import { globalPrismaClient } from "@/prisma-client";
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
import { LightEmailTheme } from "@stackframe/stack-shared/dist/helpers/emails";
import { adaptSchema, yupArray, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
@ -30,7 +29,6 @@ export const POST = createSmartRouteHandler({
async handler({ body, auth: { tenancy } }) {
const id = generateUuid();
await overrideEnvironmentConfigOverride({
tx: globalPrismaClient,
projectId: tenancy.project.id,
branchId: tenancy.branchId,
environmentConfigOverrideOverride: {

View File

@ -32,7 +32,13 @@ export const adminUserProjectsCrudHandlers = createLazyProxy(() => createCrudHan
const project = await createOrUpdateProject({
ownerIds: userIds,
type: 'create',
data,
data: {
...data,
config: {
allow_localhost: true,
...data.config,
},
},
});
const tenancy = await getSoleTenancyFromProjectBranch(project.id, DEFAULT_BRANCH_ID);
return {

View File

@ -199,18 +199,17 @@ export function getOrganizationConfigOverrideQuery(options: OrganizationOptions)
export async function overrideProjectConfigOverride(options: {
projectId: string,
projectConfigOverrideOverride: ProjectConfigOverrideOverride,
tx: PrismaClientTransaction,
}): Promise<void> {
// set project config override on our own DB
// TODO put this in a serializable transaction (or a single SQL query) to prevent race conditions
const oldConfig = await rawQuery(options.tx, getProjectConfigOverrideQuery(options));
const oldConfig = await rawQuery(globalPrismaClient, getProjectConfigOverrideQuery(options));
const newConfig = override(
oldConfig,
options.projectConfigOverrideOverride,
);
await assertNoConfigOverrideErrors(projectConfigSchema, newConfig);
await options.tx.project.update({
await globalPrismaClient.project.update({
where: {
id: options.projectId,
},
@ -224,7 +223,6 @@ export function overrideBranchConfigOverride(options: {
projectId: string,
branchId: string,
branchConfigOverrideOverride: BranchConfigOverrideOverride,
tx: PrismaClientTransaction,
}): Promise<void> {
// update config.json if on local emulator
// throw error otherwise
@ -235,18 +233,17 @@ export async function overrideEnvironmentConfigOverride(options: {
projectId: string,
branchId: string,
environmentConfigOverrideOverride: EnvironmentConfigOverrideOverride,
tx: PrismaClientTransaction,
}): Promise<void> {
// save environment config override on DB
// TODO put this in a serializable transaction (or a single SQL query) to prevent race conditions
const oldConfig = await rawQuery(options.tx, getEnvironmentConfigOverrideQuery(options));
const oldConfig = await rawQuery(globalPrismaClient, getEnvironmentConfigOverrideQuery(options));
const newConfig = override(
oldConfig,
options.environmentConfigOverrideOverride,
);
await assertNoConfigOverrideErrors(environmentConfigSchema, newConfig);
await options.tx.environmentConfigOverride.upsert({
await globalPrismaClient.environmentConfigOverride.upsert({
where: {
projectId_branchId: {
projectId: options.projectId,
@ -269,7 +266,6 @@ export function overrideOrganizationConfigOverride(options: {
branchId: string,
organizationId: string | null,
organizationConfigOverrideOverride: OrganizationConfigOverrideOverride,
tx: PrismaClientTransaction,
}): Promise<void> {
// save organization config override on DB (either our own, or the source of truth one)
throw new StackAssertionError('Not implemented');

View File

@ -17,7 +17,12 @@ export class TracedFreestyleSandboxes {
'freestyle.nodeModules.count': options?.nodeModules ? Object.keys(options.nodeModules).length.toString() : '0',
}
}, async () => {
return await this.freestyle.executeScript(script, options);
try {
return await this.freestyle.executeScript(script, options);
} catch (error) {
captureError("freestyle.executeScript", error);
throw new StackAssertionError("Error executing script with Freestyle! " + errorToNiceString(error), { cause: error });
}
});
}
}

View File

@ -213,7 +213,6 @@ export async function createPermissionDefinition(
await overrideEnvironmentConfigOverride({
branchId: options.tenancy.branchId,
projectId: options.tenancy.project.id,
tx: globalTx,
environmentConfigOverrideOverride: {
"rbac.permissions": {
...oldConfig.rbac.permissions,
@ -272,7 +271,6 @@ export async function updatePermissionDefinition(
await overrideEnvironmentConfigOverride({
branchId: options.tenancy.branchId,
projectId: options.tenancy.project.id,
tx: globalTx,
environmentConfigOverrideOverride: {
"rbac.permissions": {
...typedFromEntries(
@ -347,7 +345,6 @@ export async function deletePermissionDefinition(
await overrideEnvironmentConfigOverride({
branchId: options.tenancy.branchId,
projectId: options.tenancy.project.id,
tx: globalTx,
environmentConfigOverrideOverride: {
"rbac.permissions": typedFromEntries(
typedEntries(oldConfig.rbac.permissions)

View File

@ -122,114 +122,112 @@ export async function createOrUpdateProject(
branchId = options.branchId;
}
const translateDefaultPermissions = (permissions: { id: string }[] | undefined) => {
return permissions ? typedFromEntries(permissions.map((permission) => [permission.id, true])) : undefined;
};
return [project.id, branchId];
});
await overrideProjectConfigOverride({
tx,
projectId: project.id,
projectConfigOverrideOverride: {
sourceOfTruth: options.sourceOfTruth || (JSON.parse(getEnvVariable("STACK_OVERRIDE_SOURCE_OF_TRUTH", "null")) ?? undefined),
},
});
// Update project config override
await overrideProjectConfigOverride({
projectId: projectId,
projectConfigOverrideOverride: {
sourceOfTruth: options.sourceOfTruth || (JSON.parse(getEnvVariable("STACK_OVERRIDE_SOURCE_OF_TRUTH", "null")) ?? undefined),
},
});
const dataOptions = options.data.config || {};
const configOverrideOverride: EnvironmentConfigOverrideOverride = filterUndefined({
// ======================= auth =======================
'auth.allowSignUp': dataOptions.sign_up_enabled,
'auth.password.allowSignIn': dataOptions.credential_enabled,
'auth.otp.allowSignIn': dataOptions.magic_link_enabled,
'auth.passkey.allowSignIn': dataOptions.passkey_enabled,
'auth.oauth.accountMergeStrategy': dataOptions.oauth_account_merge_strategy,
'auth.oauth.providers': dataOptions.oauth_providers ? typedFromEntries(dataOptions.oauth_providers
.map((provider) => {
return [
provider.id,
{
type: provider.id,
isShared: provider.type === "shared",
clientId: provider.client_id,
clientSecret: provider.client_secret,
facebookConfigId: provider.facebook_config_id,
microsoftTenantId: provider.microsoft_tenant_id,
allowSignIn: true,
allowConnectedAccounts: true,
} satisfies OrganizationRenderedConfig['auth']['oauth']['providers'][string]
];
})) : undefined,
// ======================= users =======================
'users.allowClientUserDeletion': dataOptions.client_user_deletion_enabled,
// ======================= teams =======================
'teams.allowClientTeamCreation': dataOptions.client_team_creation_enabled,
'teams.createPersonalTeamOnSignUp': dataOptions.create_team_on_sign_up,
// ======================= domains =======================
'domains.allowLocalhost': dataOptions.allow_localhost ?? true,
'domains.trustedDomains': dataOptions.domains ? typedFromEntries(dataOptions.domains.map((domain) => {
// Update environment config override
const translateDefaultPermissions = (permissions: { id: string }[] | undefined) => {
return permissions ? typedFromEntries(permissions.map((permission) => [permission.id, true])) : undefined;
};
const dataOptions = options.data.config || {};
const configOverrideOverride: EnvironmentConfigOverrideOverride = filterUndefined({
// ======================= auth =======================
'auth.allowSignUp': dataOptions.sign_up_enabled,
'auth.password.allowSignIn': dataOptions.credential_enabled,
'auth.otp.allowSignIn': dataOptions.magic_link_enabled,
'auth.passkey.allowSignIn': dataOptions.passkey_enabled,
'auth.oauth.accountMergeStrategy': dataOptions.oauth_account_merge_strategy,
'auth.oauth.providers': dataOptions.oauth_providers ? typedFromEntries(dataOptions.oauth_providers
.map((provider) => {
return [
generateUuid(),
provider.id,
{
baseUrl: domain.domain,
handlerPath: domain.handler_path,
} satisfies OrganizationRenderedConfig['domains']['trustedDomains'][string],
type: provider.id,
isShared: provider.type === "shared",
clientId: provider.client_id,
clientSecret: provider.client_secret,
facebookConfigId: provider.facebook_config_id,
microsoftTenantId: provider.microsoft_tenant_id,
allowSignIn: true,
allowConnectedAccounts: true,
} satisfies OrganizationRenderedConfig['auth']['oauth']['providers'][string]
];
})) : undefined,
// ======================= api keys =======================
'apiKeys.enabled.user': dataOptions.allow_user_api_keys,
'apiKeys.enabled.team': dataOptions.allow_team_api_keys,
// ======================= emails =======================
'emails.server': dataOptions.email_config ? {
isShared: dataOptions.email_config.type === 'shared',
host: dataOptions.email_config.host,
port: dataOptions.email_config.port,
username: dataOptions.email_config.username,
password: dataOptions.email_config.password,
senderName: dataOptions.email_config.sender_name,
senderEmail: dataOptions.email_config.sender_email,
} satisfies OrganizationRenderedConfig['emails']['server'] : undefined,
'emails.selectedThemeId': dataOptions.email_theme,
// ======================= rbac =======================
'rbac.defaultPermissions.teamMember': translateDefaultPermissions(dataOptions.team_member_default_permissions),
'rbac.defaultPermissions.teamCreator': translateDefaultPermissions(dataOptions.team_creator_default_permissions),
'rbac.defaultPermissions.signUp': translateDefaultPermissions(dataOptions.user_default_permissions),
});
// ======================= users =======================
'users.allowClientUserDeletion': dataOptions.client_user_deletion_enabled,
// ======================= teams =======================
'teams.allowClientTeamCreation': dataOptions.client_team_creation_enabled,
'teams.createPersonalTeamOnSignUp': dataOptions.create_team_on_sign_up,
// ======================= domains =======================
'domains.allowLocalhost': dataOptions.allow_localhost,
'domains.trustedDomains': dataOptions.domains ? typedFromEntries(dataOptions.domains.map((domain) => {
return [
generateUuid(),
{
baseUrl: domain.domain,
handlerPath: domain.handler_path,
} satisfies OrganizationRenderedConfig['domains']['trustedDomains'][string],
];
})) : undefined,
// ======================= api keys =======================
'apiKeys.enabled.user': dataOptions.allow_user_api_keys,
'apiKeys.enabled.team': dataOptions.allow_team_api_keys,
// ======================= emails =======================
'emails.server': dataOptions.email_config ? {
isShared: dataOptions.email_config.type === 'shared',
host: dataOptions.email_config.host,
port: dataOptions.email_config.port,
username: dataOptions.email_config.username,
password: dataOptions.email_config.password,
senderName: dataOptions.email_config.sender_name,
senderEmail: dataOptions.email_config.sender_email,
} satisfies OrganizationRenderedConfig['emails']['server'] : undefined,
'emails.selectedThemeId': dataOptions.email_theme,
// ======================= rbac =======================
'rbac.defaultPermissions.teamMember': translateDefaultPermissions(dataOptions.team_member_default_permissions),
'rbac.defaultPermissions.teamCreator': translateDefaultPermissions(dataOptions.team_creator_default_permissions),
'rbac.defaultPermissions.signUp': translateDefaultPermissions(dataOptions.user_default_permissions),
});
if (options.type === "create") {
configOverrideOverride['rbac.permissions.team_member'] ??= {
description: "Default permission for team members",
scope: "team",
containedPermissionIds: {
'$read_members': true,
'$invite_members': true,
},
} satisfies OrganizationRenderedConfig['rbac']['permissions'][string];
configOverrideOverride['rbac.permissions.team_admin'] ??= {
description: "Default permission for team admins",
scope: "team",
containedPermissionIds: {
'$update_team': true,
'$delete_team': true,
'$read_members': true,
'$remove_members': true,
'$invite_members': true,
'$manage_api_keys': true,
},
} satisfies OrganizationRenderedConfig['rbac']['permissions'][string];
if (options.type === "create") {
configOverrideOverride['rbac.permissions.team_member'] ??= {
description: "Default permission for team members",
scope: "team",
containedPermissionIds: {
'$read_members': true,
'$invite_members': true,
},
} satisfies OrganizationRenderedConfig['rbac']['permissions'][string];
configOverrideOverride['rbac.permissions.team_admin'] ??= {
description: "Default permission for team admins",
scope: "team",
containedPermissionIds: {
'$update_team': true,
'$delete_team': true,
'$read_members': true,
'$remove_members': true,
'$invite_members': true,
'$manage_api_keys': true,
},
} satisfies OrganizationRenderedConfig['rbac']['permissions'][string];
configOverrideOverride['rbac.defaultPermissions.teamCreator'] ??= { 'team_admin': true };
configOverrideOverride['rbac.defaultPermissions.teamMember'] ??= { 'team_member': true };
configOverrideOverride['rbac.defaultPermissions.teamCreator'] ??= { 'team_admin': true };
configOverrideOverride['rbac.defaultPermissions.teamMember'] ??= { 'team_member': true };
configOverrideOverride['auth.password.allowSignIn'] ??= true;
}
await overrideEnvironmentConfigOverride({
tx,
projectId: project.id,
branchId: branchId,
environmentConfigOverrideOverride: configOverrideOverride,
});
return [project.id, branchId];
configOverrideOverride['auth.password.allowSignIn'] ??= true;
}
await overrideEnvironmentConfigOverride({
projectId: projectId,
branchId: branchId,
environmentConfigOverrideOverride: configOverrideOverride,
});

View File

@ -1,5 +1,15 @@
# @stackframe/stack-dashboard
## 2.8.27
### Patch Changes
- Various changes
- Updated dependencies
- @stackframe/stack-shared@2.8.27
- @stackframe/stack@2.8.27
- @stackframe/stack-ui@2.8.27
## 2.8.26
### Patch Changes

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/stack-dashboard",
"version": "2.8.26",
"version": "2.8.27",
"private": true,
"scripts": {
"clean": "rimraf .next && rimraf node_modules",

View File

@ -2,8 +2,13 @@ import { stackServerApp } from "@/stack";
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
import { urlString } from "@stackframe/stack-shared/dist/utils/urls";
import * as jose from "jose";
import { Metadata } from "next";
import { redirect } from "next/navigation";
export const metadata: Metadata = {
title: "Signing you in...",
};
export default async function FeaturebaseSSO({
searchParams,
}: {

View File

@ -1,5 +1,11 @@
# @stackframe/dev-launchpad
## 2.8.27
### Patch Changes
- Various changes
## 2.8.26
## 2.8.25

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/dev-launchpad",
"version": "2.8.26",
"version": "2.8.27",
"private": true,
"scripts": {
"dev": "serve -p 8100 -s public",

View File

@ -1,5 +1,14 @@
# @stackframe/e2e-tests
## 2.8.27
### Patch Changes
- Various changes
- Updated dependencies
- @stackframe/stack-shared@2.8.27
- @stackframe/js@2.8.27
## 2.8.26
### Patch Changes

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/e2e-tests",
"version": "2.8.26",
"version": "2.8.27",
"private": true,
"type": "module",
"scripts": {

View File

@ -496,6 +496,104 @@ it("verifies email_theme update persists", async ({ expect }) => {
expect(response.body.config.email_theme).toBe("a0172b5d-cff0-463b-83bb-85124697373a"); // default-dark
});
it("updates trusted domains without modifying allow_localhost", async ({ expect }) => {
await Project.createAndSwitch();
const response1 = await niceBackendFetch("/api/v1/internal/projects/current", {
method: "PATCH",
accessType: "admin",
body: {
config: {
allow_localhost: false,
},
},
});
expect(response1).toMatchInlineSnapshot(`
NiceResponse {
"status": 200,
"body": {
"config": {
"allow_localhost": false,
"allow_team_api_keys": false,
"allow_user_api_keys": false,
"client_team_creation_enabled": false,
"client_user_deletion_enabled": false,
"create_team_on_sign_up": false,
"credential_enabled": true,
"domains": [],
"email_config": { "type": "shared" },
"email_theme": "<stripped UUID>",
"enabled_oauth_providers": [],
"magic_link_enabled": false,
"oauth_account_merge_strategy": "link_method",
"oauth_providers": [],
"passkey_enabled": false,
"sign_up_enabled": true,
"team_creator_default_permissions": [{ "id": "team_admin" }],
"team_member_default_permissions": [{ "id": "team_member" }],
"user_default_permissions": [],
},
"created_at_millis": <stripped field 'created_at_millis'>,
"description": "",
"display_name": "New Project",
"id": "<stripped UUID>",
"is_production_mode": false,
},
"headers": Headers { <some fields may have been hidden> },
}
`);
const response2 = await niceBackendFetch("/api/v1/internal/projects/current", {
method: "PATCH",
accessType: "admin",
body: {
config: {
domains: [
{ domain: "https://example.com", handler_path: "/handler" },
],
},
},
});
expect(response2).toMatchInlineSnapshot(`
NiceResponse {
"status": 200,
"body": {
"config": {
"allow_localhost": false,
"allow_team_api_keys": false,
"allow_user_api_keys": false,
"client_team_creation_enabled": false,
"client_user_deletion_enabled": false,
"create_team_on_sign_up": false,
"credential_enabled": true,
"domains": [
{
"domain": "https://example.com",
"handler_path": "/handler",
},
],
"email_config": { "type": "shared" },
"email_theme": "<stripped UUID>",
"enabled_oauth_providers": [],
"magic_link_enabled": false,
"oauth_account_merge_strategy": "link_method",
"oauth_providers": [],
"passkey_enabled": false,
"sign_up_enabled": true,
"team_creator_default_permissions": [{ "id": "team_admin" }],
"team_member_default_permissions": [{ "id": "team_member" }],
"user_default_permissions": [],
},
"created_at_millis": <stripped field 'created_at_millis'>,
"description": "",
"display_name": "New Project",
"id": "<stripped UUID>",
"is_production_mode": false,
},
"headers": Headers { <some fields may have been hidden> },
}
`);
});
it("gives an error when updating email_theme with an invalid value", async ({ expect }) => {
await Project.createAndSwitch();

View File

@ -1,5 +1,7 @@
# @stackframe/mock-oauth-server
## 2.8.27
## 2.8.26
## 2.8.25

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/mock-oauth-server",
"version": "2.8.26",
"version": "2.8.27",
"private": true,
"main": "index.js",
"scripts": {

View File

@ -1,5 +1,14 @@
# @stackframe/stack-docs
## 2.8.27
### Patch Changes
- Various changes
- Updated dependencies
- @stackframe/stack-shared@2.8.27
- @stackframe/stack@2.8.27
## 2.8.26
### Patch Changes

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/stack-docs",
"version": "2.8.26",
"version": "2.8.27",
"description": "",
"main": "index.js",
"private": true,

View File

@ -0,0 +1,31 @@
import { source } from 'lib/source';
import { notFound, redirect } from 'next/navigation';
import { NextRequest } from 'next/server';
export function GET(request: NextRequest) {
const pathname = new URL(request.url).pathname;
// Ensure we have the correct target path without double prefixes using proper URL construction
let targetPath: string;
if (pathname.startsWith('/docs')) {
targetPath = pathname;
} else {
// Remove leading slash and use as relative path to properly construct /docs prefix
targetPath = new URL(pathname.substring(1), 'file:///docs/').pathname;
}
// Extract slug by removing any '/docs' prefix and splitting by '/'
const cleanPath = pathname.startsWith('/docs') ? pathname.substring(5) : pathname;
const slug = cleanPath.substring(1).split('/').filter(Boolean);
// Check if the target page exists
const page = source.getPage(slug);
if (page) {
// Page exists, redirect to the full path
return redirect(targetPath);
} else {
// Page doesn't exist, return 404
return notFound();
}
}

View File

@ -0,0 +1,31 @@
import { source } from 'lib/source';
import { notFound, redirect } from 'next/navigation';
import { NextRequest } from 'next/server';
export function GET(request: NextRequest) {
const pathname = new URL(request.url).pathname;
// Ensure we have the correct target path without double prefixes using proper URL construction
let targetPath: string;
if (pathname.startsWith('/docs')) {
targetPath = pathname;
} else {
// Remove leading slash and use as relative path to properly construct /docs prefix
targetPath = new URL(pathname.substring(1), 'file:///docs/').pathname;
}
// Extract slug by removing any '/docs' prefix and splitting by '/'
const cleanPath = pathname.startsWith('/docs') ? pathname.substring(5) : pathname;
const slug = cleanPath.substring(1).split('/').filter(Boolean);
// Check if the target page exists
const page = source.getPage(slug);
if (page) {
// Page exists, redirect to the full path
return redirect(targetPath);
} else {
// Page doesn't exist, redirect to overview
return notFound();
}
}

View File

@ -0,0 +1,11 @@
import Link from "next/link";
export default function NotFound() {
return (
<div>
<h2>Not Found</h2>
<p>Could not find requested resource</p>
<Link href="/">Return Home</Link>
</div>
);
}

View File

@ -0,0 +1,31 @@
import { source } from 'lib/source';
import { notFound, redirect } from 'next/navigation';
import { NextRequest } from 'next/server';
export function GET(request: NextRequest) {
const pathname = new URL(request.url).pathname;
// Ensure we have the correct target path without double prefixes using proper URL construction
let targetPath: string;
if (pathname.startsWith('/docs')) {
targetPath = pathname;
} else {
// Remove leading slash and use as relative path to properly construct /docs prefix
targetPath = new URL(pathname.substring(1), 'file:///docs/').pathname;
}
// Extract slug by removing any '/docs' prefix and splitting by '/'
const cleanPath = pathname.startsWith('/docs') ? pathname.substring(5) : pathname;
const slug = cleanPath.substring(1).split('/').filter(Boolean);
// Check if the target page exists
const page = source.getPage(slug);
if (page) {
// Page exists, redirect to the full path
return redirect(targetPath);
} else {
// Page doesn't exist, redirect to overview
return notFound();
}
}

View File

@ -0,0 +1,31 @@
import { source } from 'lib/source';
import { notFound, redirect } from 'next/navigation';
import { NextRequest } from 'next/server';
export function GET(request: NextRequest) {
const pathname = new URL(request.url).pathname;
// Ensure we have the correct target path without double prefixes using proper URL construction
let targetPath: string;
if (pathname.startsWith('/docs')) {
targetPath = pathname;
} else {
// Remove leading slash and use as relative path to properly construct /docs prefix
targetPath = new URL(pathname.substring(1), 'file:///docs/').pathname;
}
// Extract slug by removing any '/docs' prefix and splitting by '/'
const cleanPath = pathname.startsWith('/docs') ? pathname.substring(5) : pathname;
const slug = cleanPath.substring(1).split('/').filter(Boolean);
// Check if the target page exists
const page = source.getPage(slug);
if (page) {
// Page exists, redirect to the full path
return redirect(targetPath);
} else {
// Page doesn't exist, redirect to overview
return notFound();
}
}

View File

@ -0,0 +1,31 @@
import { apiSource } from 'lib/source';
import { notFound, redirect } from 'next/navigation';
import { NextRequest } from 'next/server';
export function GET(request: NextRequest) {
const pathname = new URL(request.url).pathname;
// For rest-api, we redirect to /api not /docs using proper URL construction
let targetPath: string;
if (pathname.startsWith('/api')) {
targetPath = pathname;
} else {
// Remove leading slash and use as relative path to properly construct /api prefix
targetPath = new URL(pathname.substring(1), 'file:///api/').pathname;
}
// Extract slug by removing any '/api' prefix and splitting by '/'
const cleanPath = pathname.startsWith('/api') ? pathname.substring(4) : pathname;
const slug = cleanPath.substring(1).split('/').filter(Boolean);
// Check if the target page exists using apiSource for API docs
const page = apiSource.getPage(slug);
if (page) {
// Page exists, redirect to the full path
return redirect(targetPath);
} else {
// Page doesn't exist, redirect to overview
return notFound();
}
}

View File

@ -507,7 +507,7 @@ Like `StackClientApp`, but with [server permissions](../../concepts/stack-app.md
<Info type="warning">
Since this functionality should only be available in environments you trust (ie. your own server), it requires a [`SECRET_SERVER_KEY`](../../rest-api/overview.mdx).
In some cases, you may want to use a [`StackServerApp`](#stackserverapp) on the client; an example for this is an internal dashboard that only your own employees have access to.
We generally recommend against doing this unless you are aware of and protected against the (potentially severe) secutiry implications of
We generally recommend against doing this unless you are aware of and protected against the (potentially severe) security implications of
exposing [`SECRET_SERVER_KEY`](../../rest-api/overview.mdx) on the client.
</Info>

View File

@ -1,5 +1,11 @@
# @stackframe/example-cjs-test
## 2.8.27
### Patch Changes
- @stackframe/stack@2.8.27
## 2.8.26
### Patch Changes

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/example-cjs-test",
"version": "2.8.26",
"version": "2.8.27",
"private": true,
"scripts": {
"dev": "next dev --port 8110",

View File

@ -1,5 +1,14 @@
# @stackframe/example-demo-app
## 2.8.27
### Patch Changes
- Updated dependencies
- @stackframe/stack-shared@2.8.27
- @stackframe/stack@2.8.27
- @stackframe/stack-ui@2.8.27
## 2.8.26
### Patch Changes

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/example-demo-app",
"version": "2.8.26",
"version": "2.8.27",
"description": "",
"private": true,
"scripts": {

View File

@ -1,5 +1,14 @@
# @stackframe/docs-examples
## 2.8.27
### Patch Changes
- Updated dependencies
- @stackframe/stack-shared@2.8.27
- @stackframe/stack@2.8.27
- @stackframe/stack-ui@2.8.27
## 2.8.26
### Patch Changes

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/docs-examples",
"version": "2.8.26",
"version": "2.8.27",
"description": "",
"private": true,
"scripts": {

View File

@ -1,5 +1,11 @@
# @stackframe/e-commerce-demo
## 2.8.27
### Patch Changes
- @stackframe/stack@2.8.27
## 2.8.26
### Patch Changes

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/e-commerce-demo",
"version": "2.8.26",
"version": "2.8.27",
"private": true,
"scripts": {
"dev": "next dev --port 8111",

View File

@ -1,5 +1,11 @@
# @stackframe/js-example
## 2.8.27
### Patch Changes
- @stackframe/js@2.8.27
## 2.8.26
### Patch Changes

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/js-example",
"version": "2.8.26",
"version": "2.8.27",
"private": true,
"description": "",
"main": "index.js",

View File

@ -1,5 +1,11 @@
# @stackframe/example-middleware-demo
## 2.8.27
### Patch Changes
- @stackframe/stack@2.8.27
## 2.8.26
### Patch Changes

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/example-middleware-demo",
"version": "2.8.26",
"version": "2.8.27",
"private": true,
"scripts": {
"dev": "next dev --port 8112",

View File

@ -1,5 +1,11 @@
# @stackframe/example-partial-prerendering
## 2.8.27
### Patch Changes
- @stackframe/stack@2.8.27
## 2.8.26
### Patch Changes

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/example-partial-prerendering",
"version": "2.8.26",
"version": "2.8.27",
"private": true,
"scripts": {
"dev": "next dev --port 8109",

View File

@ -1,5 +1,11 @@
# react-example
## 2.8.27
### Patch Changes
- @stackframe/react@2.8.27
## 2.8.26
### Patch Changes

View File

@ -1,7 +1,7 @@
{
"name": "react-example",
"private": true,
"version": "2.8.26",
"version": "2.8.27",
"type": "module",
"scripts": {
"dev": "vite --force --port 8120",

View File

@ -1,5 +1,11 @@
# @stackframe/example-supabase
## 2.8.27
### Patch Changes
- @stackframe/stack@2.8.27
## 2.8.26
### Patch Changes

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/example-supabase",
"version": "2.8.26",
"version": "2.8.27",
"private": true,
"scripts": {
"dev": "next dev --turbo --port 8115",

View File

@ -1,5 +1,12 @@
# @stackframe/init-stack
## 2.8.27
### Patch Changes
- Updated dependencies
- @stackframe/stack-shared@2.8.27
## 2.8.26
### Patch Changes

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/init-stack",
"version": "2.8.26",
"version": "2.8.27",
"description": "The setup wizard for Stack. https://stack-auth.com",
"main": "dist/index.js",
"type": "module",

View File

@ -1,7 +1,7 @@
{
"//": "THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY",
"name": "@stackframe/js",
"version": "2.8.26",
"version": "2.8.27",
"sideEffects": false,
"main": "./dist/index.js",
"types": "./dist/index.d.ts",

View File

@ -1,7 +1,7 @@
{
"//": "THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY",
"name": "@stackframe/react",
"version": "2.8.26",
"version": "2.8.27",
"sideEffects": false,
"main": "./dist/index.js",
"types": "./dist/index.d.ts",

View File

@ -1,5 +1,7 @@
# @stackframe/stack-sc
## 2.8.27
## 2.8.26
## 2.8.25

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/stack-sc",
"version": "2.8.26",
"version": "2.8.27",
"exports": {
"./force-react-server": {
"types": "./dist/index.react-server.d.ts",

View File

@ -1,5 +1,11 @@
# @stackframe/stack-shared
## 2.8.27
### Patch Changes
- Various changes
## 2.8.26
### Patch Changes

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/stack-shared",
"version": "2.8.26",
"version": "2.8.27",
"scripts": {
"build": "rimraf dist && tsup-node",
"typecheck": "tsc --noEmit",

View File

@ -10,16 +10,18 @@ let esbuildInitializePromise: Promise<void> | null = null;
export async function initializeEsbuild() {
if (!esbuildInitializePromise) {
esbuildInitializePromise = esbuild.initialize(isBrowserLike() ? {
wasmURL: `https://unpkg.com/esbuild-wasm@${esbuild.version}/esbuild.wasm`,
} : {
wasmModule: (
await fetch(`https://unpkg.com/esbuild-wasm@${esbuild.version}/esbuild.wasm`)
.then(wasm => wasm.arrayBuffer())
.then(wasm => new WebAssembly.Module(wasm))
),
worker: false,
});
esbuildInitializePromise = (async () => {
await esbuild.initialize(isBrowserLike() ? {
wasmURL: `https://unpkg.com/esbuild-wasm@${esbuild.version}/esbuild.wasm`,
} : {
wasmModule: (
await fetch(`https://unpkg.com/esbuild-wasm@${esbuild.version}/esbuild.wasm`)
.then(wasm => wasm.arrayBuffer())
.then(wasm => new WebAssembly.Module(wasm))
),
worker: false,
});
})();
}
await esbuildInitializePromise;
}

View File

@ -340,7 +340,7 @@ export function runAsynchronously(
promiseOrFunc?.catch(error => {
options.onError?.(error);
const newError = new StackAssertionError(
"Uncaught error in asynchronous function: " + error.toString(),
"Uncaught error in asynchronous function: " + errorToNiceString(error),
{ cause: error },
);
concatStacktraces(newError, duringError);

View File

@ -1,5 +1,12 @@
# @stackframe/stack-ui
## 2.8.27
### Patch Changes
- Updated dependencies
- @stackframe/stack-shared@2.8.27
## 2.8.26
### Patch Changes

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/stack-ui",
"version": "2.8.26",
"version": "2.8.27",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"sideEffects": false,

View File

@ -1,7 +1,7 @@
{
"//": "THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY",
"name": "@stackframe/stack",
"version": "2.8.26",
"version": "2.8.27",
"sideEffects": false,
"main": "./dist/index.js",
"types": "./dist/index.d.ts",

View File

@ -1,5 +1,15 @@
# @stackframe/stack
## 2.8.27
### Patch Changes
- Various changes
- Updated dependencies
- @stackframe/stack-shared@2.8.27
- @stackframe/stack-ui@2.8.27
- @stackframe/stack-sc@2.8.27
## 2.8.26
### Patch Changes

View File

@ -11,7 +11,7 @@
"//": "NEXT_LINE_PLATFORM template",
"private": true,
"version": "2.8.26",
"version": "2.8.27",
"sideEffects": false,
"main": "./dist/index.js",
"types": "./dist/index.d.ts",

View File

@ -2,7 +2,7 @@
"//": "THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY",
"name": "@stackframe/template",
"private": true,
"version": "2.8.26",
"version": "2.8.27",
"sideEffects": false,
"main": "./dist/index.js",
"types": "./dist/index.d.ts",