From 7f9eac40c551a173e6f88a28470f1882f08f24df Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Tue, 14 Apr 2026 12:39:55 -0700 Subject: [PATCH 1/6] Downgrade Next.js to 16.1.7 --- apps/backend/package.json | 2 +- apps/dashboard/package.json | 2 +- pnpm-lock.yaml | 315 +++++++++++++++++++++++------------- 3 files changed, 207 insertions(+), 112 deletions(-) diff --git a/apps/backend/package.json b/apps/backend/package.json index 75a8043e7..2daa79d88 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -98,7 +98,7 @@ "jiti": "^2.6.1", "jose": "^6.1.3", "json-diff": "^1.0.6", - "next": "16.2.2", + "next": "16.1.7", "nodemailer": "^6.9.10", "oidc-provider": "^8.5.1", "openid-client": "5.6.4", diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index bc2f1f956..337f6a437 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -84,7 +84,7 @@ "input-otp": "^1.4.1", "jose": "^6.1.3", "lodash": "^4.17.21", - "next": "16.2.2", + "next": "16.1.7", "next-themes": "^0.2.1", "posthog-js": "^1.336.1", "react": "19.2.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f97cd71f0..dd890a4bd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -188,7 +188,7 @@ importers: version: 1.2.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@sentry/nextjs': specifier: ^10.45.0 - version: 10.45.0(@opentelemetry/context-async-hooks@1.26.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.26.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.26.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@16.2.2(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.92.0(esbuild@0.24.2)) + version: 10.45.0(@opentelemetry/context-async-hooks@1.26.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.26.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.26.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@16.1.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.92.0(esbuild@0.24.2)) '@simplewebauthn/server': specifier: ^13.3.0 version: 13.3.0 @@ -244,8 +244,8 @@ importers: specifier: ^1.0.6 version: 1.0.6 next: - specifier: 16.2.2 - version: 16.2.2(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: 16.1.7 + version: 16.1.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) nodemailer: specifier: ^6.9.10 version: 6.9.13 @@ -474,7 +474,7 @@ importers: version: 2.0.2(react@19.2.3) '@sentry/nextjs': specifier: ^10.11.0 - version: 10.11.0(@opentelemetry/context-async-hooks@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@16.2.2(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.92.0(esbuild@0.24.2)) + version: 10.11.0(@opentelemetry/context-async-hooks@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@16.1.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.92.0(esbuild@0.24.2)) '@stackframe/dashboard-ui-components': specifier: workspace:* version: link:../../packages/dashboard-ui-components @@ -504,10 +504,10 @@ importers: version: 3.13.18(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@vercel/analytics': specifier: ^1.2.2 - version: 1.3.1(next@16.2.2(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) + version: 1.3.1(next@16.1.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) '@vercel/speed-insights': specifier: ^1.0.12 - version: 1.0.12(next@16.2.2(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) + version: 1.0.12(next@16.1.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) ai: specifier: ^6.0.0 version: 6.0.81(zod@4.1.12) @@ -537,7 +537,7 @@ importers: version: 1.4.0 geist: specifier: ^1 - version: 1.3.0(next@16.2.2(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) + version: 1.3.0(next@16.1.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) input-otp: specifier: ^1.4.1 version: 1.4.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -548,11 +548,11 @@ importers: specifier: ^4.17.21 version: 4.17.21 next: - specifier: 16.2.2 - version: 16.2.2(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: 16.1.7 + version: 16.1.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) next-themes: specifier: ^0.2.1 - version: 0.2.1(next@16.2.2(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 0.2.1(next@16.1.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) posthog-js: specifier: ^1.336.1 version: 1.336.1 @@ -5587,6 +5587,9 @@ packages: '@next/env@15.5.10': resolution: {integrity: sha512-plg+9A/KoZcTS26fe15LHg+QxReTazrIOoKKUC3Uz4leGGeNPgLHdevVraAAOX0snnUs3WkRx3eUQpj9mreG6A==} + '@next/env@16.1.7': + resolution: {integrity: sha512-rJJbIdJB/RQr2F1nylZr/PJzamvNNhfr3brdKP6s/GW850jbtR70QlSfFselvIBbcPUOlQwBakexjFzqLzF6pg==} + '@next/env@16.2.2': resolution: {integrity: sha512-LqSGz5+xGk9EL/iBDr2yo/CgNQV6cFsNhRR2xhSXYh7B/hb4nePCxlmDvGEKG30NMHDFf0raqSyOZiQrO7BkHQ==} @@ -5614,6 +5617,12 @@ packages: cpu: [arm64] os: [darwin] + '@next/swc-darwin-arm64@16.1.7': + resolution: {integrity: sha512-b2wWIE8sABdyafc4IM8r5Y/dS6kD80JRtOGrUiKTsACFQfWWgUQ2NwoUX1yjFMXVsAwcQeNpnucF2ZrujsBBPg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + '@next/swc-darwin-arm64@16.2.2': resolution: {integrity: sha512-B92G3ulrwmkDSEJEp9+XzGLex5wC1knrmCSIylyVeiAtCIfvEJYiN3v5kXPlYt5R4RFlsfO/v++aKV63Acrugg==} engines: {node: '>= 10'} @@ -5632,6 +5641,12 @@ packages: cpu: [x64] os: [darwin] + '@next/swc-darwin-x64@16.1.7': + resolution: {integrity: sha512-zcnVaaZulS1WL0Ss38R5Q6D2gz7MtBu8GZLPfK+73D/hp4GFMrC2sudLky1QibfV7h6RJBJs/gOFvYP0X7UVlQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + '@next/swc-darwin-x64@16.2.2': resolution: {integrity: sha512-7ZwSgNKJNQiwW0CKhNm9B1WS2L1Olc4B2XY0hPYCAL3epFnugMhuw5TMWzMilQ3QCZcCHoYm9NGWTHbr5REFxw==} engines: {node: '>= 10'} @@ -5652,6 +5667,13 @@ packages: os: [linux] libc: [glibc] + '@next/swc-linux-arm64-gnu@16.1.7': + resolution: {integrity: sha512-2ant89Lux/Q3VyC8vNVg7uBaFVP9SwoK2jJOOR0L8TQnX8CAYnh4uctAScy2Hwj2dgjVHqHLORQZJ2wH6VxhSQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + '@next/swc-linux-arm64-gnu@16.2.2': resolution: {integrity: sha512-c3m8kBHMziMgo2fICOP/cd/5YlrxDU5YYjAJeQLyFsCqVF8xjOTH/QYG4a2u48CvvZZSj1eHQfBCbyh7kBr30Q==} engines: {node: '>= 10'} @@ -5673,6 +5695,13 @@ packages: os: [linux] libc: [musl] + '@next/swc-linux-arm64-musl@16.1.7': + resolution: {integrity: sha512-uufcze7LYv0FQg9GnNeZ3/whYfo+1Q3HnQpm16o6Uyi0OVzLlk2ZWoY7j07KADZFY8qwDbsmFnMQP3p3+Ftprw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + '@next/swc-linux-arm64-musl@16.2.2': resolution: {integrity: sha512-VKLuscm0P/mIfzt+SDdn2+8TNNJ7f0qfEkA+az7OqQbjzKdBxAHs0UvuiVoCtbwX+dqMEL9U54b5wQ/aN3dHeg==} engines: {node: '>= 10'} @@ -5694,6 +5723,13 @@ packages: os: [linux] libc: [glibc] + '@next/swc-linux-x64-gnu@16.1.7': + resolution: {integrity: sha512-KWVf2gxYvHtvuT+c4MBOGxuse5TD7DsMFYSxVxRBnOzok/xryNeQSjXgxSv9QpIVlaGzEn/pIuI6Koosx8CGWA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + '@next/swc-linux-x64-gnu@16.2.2': resolution: {integrity: sha512-kU3OPHJq6sBUjOk7wc5zJ7/lipn8yGldMoAv4z67j6ov6Xo/JvzA7L7LCsyzzsXmgLEhk3Qkpwqaq/1+XpNR3g==} engines: {node: '>= 10'} @@ -5715,6 +5751,13 @@ packages: os: [linux] libc: [musl] + '@next/swc-linux-x64-musl@16.1.7': + resolution: {integrity: sha512-HguhaGwsGr1YAGs68uRKc4aGWxLET+NevJskOcCAwXbwj0fYX0RgZW2gsOCzr9S11CSQPIkxmoSbuVaBp4Z3dA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + '@next/swc-linux-x64-musl@16.2.2': resolution: {integrity: sha512-CKXRILyErMtUftp+coGcZ38ZwE/Aqq45VMCcRLr2I4OXKrgxIBDXHnBgeX/UMil0S09i2JXaDL3Q+TN8D/cKmg==} engines: {node: '>= 10'} @@ -5734,6 +5777,12 @@ packages: cpu: [arm64] os: [win32] + '@next/swc-win32-arm64-msvc@16.1.7': + resolution: {integrity: sha512-S0n3KrDJokKTeFyM/vGGGR8+pCmXYrjNTk2ZozOL1C/JFdfUIL9O1ATaJOl5r2POe56iRChbsszrjMAdWSv7kQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + '@next/swc-win32-arm64-msvc@16.2.2': resolution: {integrity: sha512-sS/jSk5VUoShUqINJFvNjVT7JfR5ORYj/+/ZpOYbbIohv/lQfduWnGAycq2wlknbOql2xOR0DoV0s6Xfcy49+g==} engines: {node: '>= 10'} @@ -5758,6 +5807,12 @@ packages: cpu: [x64] os: [win32] + '@next/swc-win32-x64-msvc@16.1.7': + resolution: {integrity: sha512-mwgtg8CNZGYm06LeEd+bNnOUfwOyNem/rOiP14Lsz+AnUY92Zq/LXwtebtUiaeVkhbroRCQ0c8GlR4UT1U+0yg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + '@next/swc-win32-x64-msvc@16.2.2': resolution: {integrity: sha512-aHaKceJgdySReT7qeck5oShucxWRiiEuwCGK8HHALe6yZga8uyFpLkPgaRw3kkF04U7ROogL/suYCNt/+CuXGA==} engines: {node: '>= 10'} @@ -11205,10 +11260,6 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - baseline-browser-mapping@2.8.21: - resolution: {integrity: sha512-JU0h5APyQNsHOlAM7HnQnPToSDQoEBZqzu/YBlqDnEeymPnZDREeXJA3KBMQee+dKteAxZ2AtvQEvVYdZf241Q==} - hasBin: true - basic-auth@2.0.1: resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} engines: {node: '>= 0.8'} @@ -15517,6 +15568,27 @@ packages: sass: optional: true + next@16.1.7: + resolution: {integrity: sha512-WM0L7WrSvKwoLegLYr6V+mz+RIofqQgVAfHhMp9a88ms0cFX8iX9ew+snpWlSBwpkURJOUdvCEt3uLl3NNzvWg==} + engines: {node: '>=20.9.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + next@16.2.2: resolution: {integrity: sha512-i6AJdyVa4oQjyvX/6GeER8dpY/xlIV+4NMv/svykcLtURJSy/WzDnnUk/TM4d0uewFHK7xSQz4TbIwPgjky+3A==} engines: {node: '>=20.9.0'} @@ -23274,6 +23346,8 @@ snapshots: '@next/env@15.5.10': {} + '@next/env@16.1.7': {} + '@next/env@16.2.2': {} '@next/eslint-plugin-next@14.2.17': @@ -23298,6 +23372,9 @@ snapshots: '@next/swc-darwin-arm64@15.5.7': optional: true + '@next/swc-darwin-arm64@16.1.7': + optional: true + '@next/swc-darwin-arm64@16.2.2': optional: true @@ -23307,6 +23384,9 @@ snapshots: '@next/swc-darwin-x64@15.5.7': optional: true + '@next/swc-darwin-x64@16.1.7': + optional: true + '@next/swc-darwin-x64@16.2.2': optional: true @@ -23316,6 +23396,9 @@ snapshots: '@next/swc-linux-arm64-gnu@15.5.7': optional: true + '@next/swc-linux-arm64-gnu@16.1.7': + optional: true + '@next/swc-linux-arm64-gnu@16.2.2': optional: true @@ -23325,6 +23408,9 @@ snapshots: '@next/swc-linux-arm64-musl@15.5.7': optional: true + '@next/swc-linux-arm64-musl@16.1.7': + optional: true + '@next/swc-linux-arm64-musl@16.2.2': optional: true @@ -23334,6 +23420,9 @@ snapshots: '@next/swc-linux-x64-gnu@15.5.7': optional: true + '@next/swc-linux-x64-gnu@16.1.7': + optional: true + '@next/swc-linux-x64-gnu@16.2.2': optional: true @@ -23343,6 +23432,9 @@ snapshots: '@next/swc-linux-x64-musl@15.5.7': optional: true + '@next/swc-linux-x64-musl@16.1.7': + optional: true + '@next/swc-linux-x64-musl@16.2.2': optional: true @@ -23352,6 +23444,9 @@ snapshots: '@next/swc-win32-arm64-msvc@15.5.7': optional: true + '@next/swc-win32-arm64-msvc@16.1.7': + optional: true + '@next/swc-win32-arm64-msvc@16.2.2': optional: true @@ -23364,6 +23459,9 @@ snapshots: '@next/swc-win32-x64-msvc@15.5.7': optional: true + '@next/swc-win32-x64-msvc@16.1.7': + optional: true + '@next/swc-win32-x64-msvc@16.2.2': optional: true @@ -27971,6 +28069,33 @@ snapshots: '@sentry/core@10.45.0': {} + '@sentry/nextjs@10.11.0(@opentelemetry/context-async-hooks@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@16.1.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.92.0(esbuild@0.24.2))': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.37.0 + '@rollup/plugin-commonjs': 28.0.1(rollup@4.50.1) + '@sentry-internal/browser-utils': 10.11.0 + '@sentry/bundler-plugin-core': 4.3.0(encoding@0.1.13) + '@sentry/core': 10.11.0 + '@sentry/node': 10.11.0 + '@sentry/opentelemetry': 10.11.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.37.0) + '@sentry/react': 10.11.0(react@19.2.3) + '@sentry/vercel-edge': 10.11.0 + '@sentry/webpack-plugin': 4.3.0(encoding@0.1.13)(webpack@5.92.0(esbuild@0.24.2)) + chalk: 3.0.0 + next: 16.1.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + resolve: 1.22.8 + rollup: 4.50.1 + stacktrace-parser: 0.1.11 + transitivePeerDependencies: + - '@opentelemetry/context-async-hooks' + - '@opentelemetry/core' + - '@opentelemetry/sdk-trace-base' + - encoding + - react + - supports-color + - webpack + '@sentry/nextjs@10.11.0(@opentelemetry/context-async-hooks@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@16.2.2(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)(webpack@5.92.0(esbuild@0.24.2))': dependencies: '@opentelemetry/api': 1.9.0 @@ -27998,34 +28123,7 @@ snapshots: - supports-color - webpack - '@sentry/nextjs@10.11.0(@opentelemetry/context-async-hooks@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@16.2.2(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.92.0(esbuild@0.24.2))': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/semantic-conventions': 1.37.0 - '@rollup/plugin-commonjs': 28.0.1(rollup@4.50.1) - '@sentry-internal/browser-utils': 10.11.0 - '@sentry/bundler-plugin-core': 4.3.0(encoding@0.1.13) - '@sentry/core': 10.11.0 - '@sentry/node': 10.11.0 - '@sentry/opentelemetry': 10.11.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.37.0) - '@sentry/react': 10.11.0(react@19.2.3) - '@sentry/vercel-edge': 10.11.0 - '@sentry/webpack-plugin': 4.3.0(encoding@0.1.13)(webpack@5.92.0(esbuild@0.24.2)) - chalk: 3.0.0 - next: 16.2.2(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - resolve: 1.22.8 - rollup: 4.50.1 - stacktrace-parser: 0.1.11 - transitivePeerDependencies: - - '@opentelemetry/context-async-hooks' - - '@opentelemetry/core' - - '@opentelemetry/sdk-trace-base' - - encoding - - react - - supports-color - - webpack - - '@sentry/nextjs@10.45.0(@opentelemetry/context-async-hooks@1.26.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.26.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.26.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@16.2.2(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.92.0(esbuild@0.24.2))': + '@sentry/nextjs@10.45.0(@opentelemetry/context-async-hooks@1.26.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.26.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.26.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@16.1.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.92.0(esbuild@0.24.2))': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.40.0 @@ -28038,7 +28136,7 @@ snapshots: '@sentry/react': 10.45.0(react@19.2.3) '@sentry/vercel-edge': 10.45.0 '@sentry/webpack-plugin': 5.1.1(encoding@0.1.13)(webpack@5.92.0(esbuild@0.24.2)) - next: 16.2.2(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next: 16.1.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) rollup: 4.57.1 stacktrace-parser: 0.1.11 transitivePeerDependencies: @@ -30297,11 +30395,11 @@ snapshots: jose: 5.6.3 neverthrow: 7.2.0 - '@vercel/analytics@1.3.1(next@16.2.2(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)': + '@vercel/analytics@1.3.1(next@16.1.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)': dependencies: server-only: 0.0.1 optionalDependencies: - next: 16.2.2(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next: 16.1.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 '@vercel/functions@2.0.0(@aws-sdk/credential-provider-web-identity@3.972.27)': @@ -30339,9 +30437,9 @@ snapshots: xdg-app-paths: 5.1.0 zod: 3.24.4 - '@vercel/speed-insights@1.0.12(next@16.2.2(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)': + '@vercel/speed-insights@1.0.12(next@16.1.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)': optionalDependencies: - next: 16.2.2(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next: 16.1.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 '@vitejs/plugin-react@4.3.3(vite@7.3.1(@types/node@20.17.6)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.19.3)(yaml@2.6.0))': @@ -31023,8 +31121,6 @@ snapshots: baseline-browser-mapping@2.10.16: {} - baseline-browser-mapping@2.8.21: {} - basic-auth@2.0.1: dependencies: safe-buffer: 5.1.2 @@ -31178,7 +31274,7 @@ snapshots: browserslist@4.23.1: dependencies: - caniuse-lite: 1.0.30001696 + caniuse-lite: 1.0.30001751 electron-to-chromium: 1.4.803 node-releases: 2.0.14 update-browserslist-db: 1.0.16(browserslist@4.23.1) @@ -31192,7 +31288,7 @@ snapshots: browserslist@4.27.0: dependencies: - baseline-browser-mapping: 2.8.21 + baseline-browser-mapping: 2.10.16 caniuse-lite: 1.0.30001751 electron-to-chromium: 1.5.244 node-releases: 2.0.27 @@ -33020,7 +33116,7 @@ snapshots: debug: 4.4.3 enhanced-resolve: 5.17.0 eslint: 8.57.1 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1) fast-glob: 3.3.2 get-tsconfig: 4.8.1 @@ -33070,7 +33166,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: @@ -33130,7 +33226,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -34237,9 +34333,9 @@ snapshots: transitivePeerDependencies: - supports-color - geist@1.3.0(next@16.2.2(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)): + geist@1.3.0(next@16.1.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)): dependencies: - next: 16.2.2(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next: 16.1.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) generate-function@2.3.1: dependencies: @@ -36706,9 +36802,9 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - next-themes@0.2.1(next@16.2.2(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + next-themes@0.2.1(next@16.1.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - next: 16.2.2(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next: 16.1.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) @@ -36851,6 +36947,56 @@ snapshots: - '@babel/core' - babel-plugin-macros + next@16.1.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@next/env': 16.1.7 + '@swc/helpers': 0.5.15 + baseline-browser-mapping: 2.10.16 + caniuse-lite: 1.0.30001751 + postcss: 8.4.31 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + styled-jsx: 5.1.6(@babel/core@7.26.0)(react@19.2.3) + optionalDependencies: + '@next/swc-darwin-arm64': 16.1.7 + '@next/swc-darwin-x64': 16.1.7 + '@next/swc-linux-arm64-gnu': 16.1.7 + '@next/swc-linux-arm64-musl': 16.1.7 + '@next/swc-linux-x64-gnu': 16.1.7 + '@next/swc-linux-x64-musl': 16.1.7 + '@next/swc-win32-arm64-msvc': 16.1.7 + '@next/swc-win32-x64-msvc': 16.1.7 + '@opentelemetry/api': 1.9.0 + sharp: 0.34.5 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + + next@16.1.7(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@next/env': 16.1.7 + '@swc/helpers': 0.5.15 + baseline-browser-mapping: 2.10.16 + caniuse-lite: 1.0.30001751 + postcss: 8.4.31 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.3) + optionalDependencies: + '@next/swc-darwin-arm64': 16.1.7 + '@next/swc-darwin-x64': 16.1.7 + '@next/swc-linux-arm64-gnu': 16.1.7 + '@next/swc-linux-arm64-musl': 16.1.7 + '@next/swc-linux-x64-gnu': 16.1.7 + '@next/swc-linux-x64-musl': 16.1.7 + '@next/swc-win32-arm64-msvc': 16.1.7 + '@next/swc-win32-x64-msvc': 16.1.7 + '@opentelemetry/api': 1.9.0 + sharp: 0.34.5 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + next@16.2.2(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1): dependencies: '@next/env': 16.2.2 @@ -36901,31 +37047,6 @@ snapshots: - '@babel/core' - babel-plugin-macros - next@16.2.2(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): - dependencies: - '@next/env': 16.2.2 - '@swc/helpers': 0.5.15 - baseline-browser-mapping: 2.10.16 - caniuse-lite: 1.0.30001751 - postcss: 8.4.31 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - styled-jsx: 5.1.6(@babel/core@7.26.0)(react@19.2.3) - optionalDependencies: - '@next/swc-darwin-arm64': 16.2.2 - '@next/swc-darwin-x64': 16.2.2 - '@next/swc-linux-arm64-gnu': 16.2.2 - '@next/swc-linux-arm64-musl': 16.2.2 - '@next/swc-linux-x64-gnu': 16.2.2 - '@next/swc-linux-x64-musl': 16.2.2 - '@next/swc-win32-arm64-msvc': 16.2.2 - '@next/swc-win32-x64-msvc': 16.2.2 - '@opentelemetry/api': 1.9.0 - sharp: 0.34.5 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros - next@16.2.2(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1): dependencies: '@next/env': 16.2.2 @@ -36951,31 +37072,6 @@ snapshots: - '@babel/core' - babel-plugin-macros - next@16.2.2(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): - dependencies: - '@next/env': 16.2.2 - '@swc/helpers': 0.5.15 - baseline-browser-mapping: 2.10.16 - caniuse-lite: 1.0.30001751 - postcss: 8.4.31 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.3) - optionalDependencies: - '@next/swc-darwin-arm64': 16.2.2 - '@next/swc-darwin-x64': 16.2.2 - '@next/swc-linux-arm64-gnu': 16.2.2 - '@next/swc-linux-arm64-musl': 16.2.2 - '@next/swc-linux-x64-gnu': 16.2.2 - '@next/swc-linux-x64-musl': 16.2.2 - '@next/swc-win32-arm64-msvc': 16.2.2 - '@next/swc-win32-x64-msvc': 16.2.2 - '@opentelemetry/api': 1.9.0 - sharp: 0.34.5 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros - nf3@0.1.12: {} nice-try@1.0.5: {} @@ -39392,7 +39488,7 @@ snapshots: dependencies: decode-ico: 0.4.1 ico-endec: 0.1.6 - sharp: 0.34.4 + sharp: 0.34.5 sharp@0.33.5: dependencies: @@ -39479,7 +39575,6 @@ snapshots: '@img/sharp-win32-arm64': 0.34.5 '@img/sharp-win32-ia32': 0.34.5 '@img/sharp-win32-x64': 0.34.5 - optional: true shebang-command@1.2.0: dependencies: From e68015909d493651c3f4b4bf8f8049992e823b0d Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Tue, 14 Apr 2026 13:43:33 -0700 Subject: [PATCH 2/6] Fix lint --- .../projects/[projectId]/widget-playground/page-client.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/widget-playground/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/widget-playground/page-client.tsx index bbedefbd7..39f388efd 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/widget-playground/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/widget-playground/page-client.tsx @@ -1630,9 +1630,9 @@ function Draggable(props: {
A runtime error occured while rendering this widget.

-
+ {props.reset && }

{errorToNiceString(props.error)}
From 88d3317b2240a486fe54ae627d41ac837e0c0b85 Mon Sep 17 00:00:00 2001 From: BilalG1 Date: Tue, 14 Apr 2026 15:36:24 -0700 Subject: [PATCH 3/6] local emulator security and features fixes (#1247) ## Summary by CodeRabbit ## Release Notes * **New Features** * Added Stripe, OAuth, and Freestyle mock services to the local emulator * Introduced `emulator run` CLI command to execute applications with emulator credentials automatically injected * Enhanced credential management for local development * **Improvements** * Improved ARM64 QEMU emulation with cross-architecture support * Better error detection and logging during emulator provisioning * Added example middleware configuration with authentication support --- .github/workflows/qemu-emulator-build.yaml | 2 + apps/backend/prisma/seed.ts | 61 +++-- .../internal/external-db-sync/poller/route.ts | 4 +- .../internal/local-emulator/project/route.tsx | 34 ++- apps/backend/src/lib/ai/models.ts | 3 +- apps/backend/src/lib/js-execution.tsx | 11 +- apps/backend/src/lib/payments.tsx | 5 +- apps/backend/src/lib/stripe.tsx | 7 +- apps/backend/src/lib/upstash.tsx | 6 + .../projects/[projectId]/payments/layout.tsx | 8 +- .../payments/payouts/page-client.tsx | 5 +- .../components/commands/ai-chat-shared.tsx | 4 +- .../payments/stripe-connect-provider.tsx | 5 +- docker/local-emulator/Dockerfile | 58 ++++- docker/local-emulator/clickhouse-config.xml | 2 + docker/local-emulator/entrypoint.sh | 7 + .../generate-env-development.mjs | 10 +- docker/local-emulator/qemu/build-image.sh | 2 +- .../qemu/cloud-init/emulator/user-data | 103 ++++++-- docker/local-emulator/qemu/run-emulator.sh | 21 +- docker/local-emulator/run-cron-jobs.sh | 32 +++ docker/local-emulator/supervisord.conf | 53 +++- docker/server/entrypoint.sh | 62 ++++- packages/stack-cli/package.json | 2 +- .../scripts/copy-emulator-assets.mjs | 27 ++ packages/stack-cli/src/commands/emulator.ts | 235 ++++++++++++++++-- .../apps/implementations/admin-app-impl.ts | 1 + 27 files changed, 661 insertions(+), 109 deletions(-) create mode 100755 docker/local-emulator/run-cron-jobs.sh create mode 100644 packages/stack-cli/scripts/copy-emulator-assets.mjs diff --git a/.github/workflows/qemu-emulator-build.yaml b/.github/workflows/qemu-emulator-build.yaml index a5a3f187d..5df149746 100644 --- a/.github/workflows/qemu-emulator-build.yaml +++ b/.github/workflows/qemu-emulator-build.yaml @@ -22,6 +22,8 @@ concurrency: env: EMULATOR_IMAGE_NAME: stack-local-emulator + EMULATOR_IMAGE_DIR: ${{ github.workspace }}/docker/local-emulator/qemu/images + EMULATOR_RUN_DIR: ${{ github.workspace }}/docker/local-emulator/qemu/run jobs: build: diff --git a/apps/backend/prisma/seed.ts b/apps/backend/prisma/seed.ts index 30e8aae0e..ff3715d3b 100644 --- a/apps/backend/prisma/seed.ts +++ b/apps/backend/prisma/seed.ts @@ -259,6 +259,45 @@ export async function seed() { console.log('Internal team created'); } + // Upsert the internal API key set before any flake-prone work (dummy-project + // seed, email/svix, clickhouse). The emulator CLI authenticates against the + // internal project using the pck stored here, so it must land before the rest + // of the seed even if something later fails. + const isLocalEmulator = process.env.NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR === 'true'; + const rawPck = process.env.STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY; + if (isLocalEmulator && !rawPck) { + // Emulator images build before a per-VM pck is available. Runtime boots set + // STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY from the VM-generated + // random value and re-run the seed, which upserts the internal key set then. + console.log('Skipping internal API key set (no pck provided; emulator mode).'); + } else { + const keySet = { + publishableClientKey: rawPck || throwErr('STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY is not set'), + secretServerKey: isLocalEmulator + ? (process.env.STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY ?? null) + : (process.env.STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY || throwErr('STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY is not set')), + superSecretAdminKey: isLocalEmulator + ? (process.env.STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY ?? null) + : (process.env.STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY || throwErr('STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY is not set')), + }; + + await globalPrismaClient.apiKeySet.upsert({ + where: { projectId_id: { projectId: 'internal', id: apiKeyId } }, + update: { + ...keySet, + }, + create: { + id: apiKeyId, + projectId: 'internal', + description: "Internal API key set", + expiresAt: new Date('2099-12-31T23:59:59Z'), + ...keySet, + } + }); + + console.log('Updated internal API key set'); + } + const shouldSeedDummyProject = process.env.STACK_SEED_ENABLE_DUMMY_PROJECT === 'true'; if (shouldSeedDummyProject) { await seedDummyProject({ @@ -268,28 +307,6 @@ export async function seed() { }); } - const keySet = { - publishableClientKey: process.env.STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY || throwErr('STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY is not set'), - secretServerKey: process.env.STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY || throwErr('STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY is not set'), - superSecretAdminKey: process.env.STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY || throwErr('STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY is not set'), - }; - - await globalPrismaClient.apiKeySet.upsert({ - where: { projectId_id: { projectId: 'internal', id: apiKeyId } }, - update: { - ...keySet, - }, - create: { - id: apiKeyId, - projectId: 'internal', - description: "Internal API key set", - expiresAt: new Date('2099-12-31T23:59:59Z'), - ...keySet, - } - }); - - console.log('Updated internal API key set'); - // Create optional default admin user if credentials are provided. // This user will be able to login to the dashboard with both email/password and magic link. diff --git a/apps/backend/src/app/api/latest/internal/external-db-sync/poller/route.ts b/apps/backend/src/app/api/latest/internal/external-db-sync/poller/route.ts index d37ef118d..688b9d25c 100644 --- a/apps/backend/src/app/api/latest/internal/external-db-sync/poller/route.ts +++ b/apps/backend/src/app/api/latest/internal/external-db-sync/poller/route.ts @@ -130,7 +130,7 @@ export const GET = createSmartRouteHandler({ async function processRequest(request: OutgoingRequest): Promise { // Prisma JsonValue doesn't carry a precise shape for this JSON blob. const options = request.qstashOptions as any; - const baseUrl = getEnvVariable("NEXT_PUBLIC_STACK_API_URL"); + const baseUrl = getEnvVariable("NEXT_PUBLIC_SERVER_STACK_API_URL", "") || getEnvVariable("NEXT_PUBLIC_STACK_API_URL"); let fullUrl = new URL(options.url, baseUrl).toString(); @@ -157,7 +157,7 @@ export const GET = createSmartRouteHandler({ function buildUpstashRequest(request: OutgoingRequest): UpstashRequest { // Prisma JsonValue doesn't carry a precise shape for this JSON blob. const options = request.qstashOptions as any; - const baseUrl = getEnvVariable("NEXT_PUBLIC_STACK_API_URL"); + const baseUrl = getEnvVariable("NEXT_PUBLIC_SERVER_STACK_API_URL", "") || getEnvVariable("NEXT_PUBLIC_STACK_API_URL"); let fullUrl = new URL(options.url, baseUrl).toString(); diff --git a/apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx b/apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx index e660c21c7..5e1373149 100644 --- a/apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx +++ b/apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx @@ -1,4 +1,5 @@ import { Prisma } from "@/generated/prisma/client"; +import { overrideEnvironmentConfigOverride } from "@/lib/config"; import { LOCAL_EMULATOR_ADMIN_USER_ID, LOCAL_EMULATOR_ONLY_ENDPOINT_MESSAGE, @@ -58,14 +59,15 @@ async function assertLocalEmulatorOwnerTeamReadiness() { } } -async function getOrCreateLocalEmulatorProjectId(absoluteFilePath: string): Promise { +async function getOrCreateLocalEmulatorProjectId(absoluteFilePath: string): Promise<{ projectId: string, created: boolean }> { const existingRows = await globalPrismaClient.$queryRaw(Prisma.sql` SELECT "projectId" FROM "LocalEmulatorProject" WHERE "absoluteFilePath" = ${absoluteFilePath} LIMIT 1 `); - const projectId = existingRows[0] ? existingRows[0].projectId : generateUuid(); + const existingRow = existingRows.length > 0 ? existingRows[0] : undefined; + const projectId = existingRow ? existingRow.projectId : generateUuid(); await globalPrismaClient.project.upsert({ where: { @@ -98,6 +100,25 @@ async function getOrCreateLocalEmulatorProjectId(absoluteFilePath: string): Prom }, }); + const created = existingRow === undefined; + + // Seed environment-level defaults BEFORE registering as a LocalEmulatorProject: + // once registered, setEnvironmentConfigOverride refuses to write. + // - domains.allowLocalhost: fresh emulator projects allow localhost redirects + // so developers don't hit "Redirect URL not whitelisted" before configuring + // trustedDomains. + // - payments.testMode: emulator payments always go through stripe-mock. + if (created) { + await overrideEnvironmentConfigOverride({ + projectId, + branchId: DEFAULT_BRANCH_ID, + environmentConfigOverrideOverride: { + "domains.allowLocalhost": true, + "payments.testMode": true, + }, + }); + } + await globalPrismaClient.$executeRaw(Prisma.sql` INSERT INTO "LocalEmulatorProject" ("absoluteFilePath", "projectId", "createdAt", "updatedAt") VALUES (${absoluteFilePath}, ${projectId}, NOW(), NOW()) @@ -107,7 +128,7 @@ async function getOrCreateLocalEmulatorProjectId(absoluteFilePath: string): Prom "updatedAt" = NOW() `); - return projectId; + return { projectId, created }; } async function getOrCreateCredentials(projectId: string) { @@ -142,7 +163,7 @@ async function getOrCreateCredentials(projectId: string) { }, }); - if (!keySet.secretServerKey || !keySet.superSecretAdminKey) { + if (!keySet.publishableClientKey || !keySet.secretServerKey || !keySet.superSecretAdminKey) { throw new StackAssertionError("Local emulator key set is missing required keys.", { projectId, keySetId: keySet.id, @@ -150,6 +171,7 @@ async function getOrCreateCredentials(projectId: string) { } return { + publishableClientKey: keySet.publishableClientKey, secretServerKey: keySet.secretServerKey, superSecretAdminKey: keySet.superSecretAdminKey, }; @@ -179,6 +201,7 @@ export const POST = createSmartRouteHandler({ bodyType: yupString().oneOf(["json"]).defined(), body: yupObject({ project_id: yupString().defined(), + publishable_client_key: yupString().defined(), secret_server_key: yupString().defined(), super_secret_admin_key: yupString().defined(), branch_config_override_string: yupString().defined(), @@ -215,7 +238,7 @@ export const POST = createSmartRouteHandler({ await assertLocalEmulatorOwnerTeamReadiness(); - const projectId = await getOrCreateLocalEmulatorProjectId(absoluteFilePath); + const { projectId } = await getOrCreateLocalEmulatorProjectId(absoluteFilePath); const credentials = await getOrCreateCredentials(projectId); const fileConfig = await readConfigFromFile(absoluteFilePath); @@ -224,6 +247,7 @@ export const POST = createSmartRouteHandler({ bodyType: "json" as const, body: { project_id: projectId, + publishable_client_key: credentials.publishableClientKey, secret_server_key: credentials.secretServerKey, super_secret_admin_key: credentials.superSecretAdminKey, branch_config_override_string: JSON.stringify(fileConfig), diff --git a/apps/backend/src/lib/ai/models.ts b/apps/backend/src/lib/ai/models.ts index 5635f4f8a..71693d6d7 100644 --- a/apps/backend/src/lib/ai/models.ts +++ b/apps/backend/src/lib/ai/models.ts @@ -1,3 +1,4 @@ +import { isLocalEmulatorEnabled } from "@/lib/local-emulator"; import { createOpenRouter } from "@openrouter/ai-sdk-provider"; import { getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env"; @@ -59,7 +60,7 @@ export const ALLOWED_MODEL_IDS: ReadonlySet = new Set([ ]); export function createOpenRouterProvider() { - const baseURL = getNodeEnvironment() === "development" + const baseURL = (getNodeEnvironment() === "development" || isLocalEmulatorEnabled()) ? "http://localhost:8102/api/latest/integrations/ai-proxy/v1" : "https://api.stack-auth.com/api/latest/integrations/ai-proxy/v1"; return createOpenRouter({ diff --git a/apps/backend/src/lib/js-execution.tsx b/apps/backend/src/lib/js-execution.tsx index 0a56cda58..b02b822e3 100644 --- a/apps/backend/src/lib/js-execution.tsx +++ b/apps/backend/src/lib/js-execution.tsx @@ -1,6 +1,7 @@ import { traceSpan } from '@/utils/telemetry'; import { runAsynchronouslyAndWaitUntil } from '@/utils/background-tasks'; import { getEnvVariable, getNodeEnvironment } from '@stackframe/stack-shared/dist/utils/env'; +import { isLocalEmulatorEnabled } from "@/lib/local-emulator"; import { StackAssertionError, captureError } from '@stackframe/stack-shared/dist/utils/errors'; import { Result } from '@stackframe/stack-shared/dist/utils/results'; import { Sandbox } from '@vercel/sandbox'; @@ -27,11 +28,13 @@ function createFreestyleEngine(): JsEngine { let baseUrl = getEnvVariable("STACK_FREESTYLE_API_ENDPOINT", "") || undefined; if (apiKey === "mock_stack_freestyle_key") { - if (!["development", "test"].includes(getNodeEnvironment())) { + if (!["development", "test"].includes(getNodeEnvironment()) && !isLocalEmulatorEnabled()) { throw new StackAssertionError("Mock Freestyle key used in production; please set the STACK_FREESTYLE_API_KEY environment variable."); } - const prefix = getEnvVariable("NEXT_PUBLIC_STACK_PORT_PREFIX", "81"); - baseUrl = `http://localhost:${prefix}22`; + if (!baseUrl) { + const prefix = getEnvVariable("NEXT_PUBLIC_STACK_PORT_PREFIX", "81"); + baseUrl = `http://localhost:${prefix}22`; + } } const freestyle = new FreestyleClient({ @@ -147,7 +150,7 @@ export async function executeJavascript(code: string, options: ExecuteJavascript return await runWithFallback(code, options); } else { - if (getNodeEnvironment().includes("prod")) { + if (getNodeEnvironment().includes("prod") && !isLocalEmulatorEnabled()) { throw new StackAssertionError("STACK_VERCEL_SANDBOX_TOKEN is set to the disabled sentinel value in production. Please configure a real Vercel Sandbox token."); } diff --git a/apps/backend/src/lib/payments.tsx b/apps/backend/src/lib/payments.tsx index 4d14b9e23..31a20203b 100644 --- a/apps/backend/src/lib/payments.tsx +++ b/apps/backend/src/lib/payments.tsx @@ -7,7 +7,6 @@ import type { UsersCrud } from "@stackframe/stack-shared/dist/interface/crud/use import type { inlineProductSchema, productSchema, productSchemaWithMetadata } from "@stackframe/stack-shared/dist/schema-fields"; import { SUPPORTED_CURRENCIES } from "@stackframe/stack-shared/dist/utils/currency-constants"; import { FAR_FUTURE_DATE, addInterval, getIntervalsElapsed } from "@stackframe/stack-shared/dist/utils/dates"; -import { getEnvVariable, getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env"; import { StackAssertionError, StatusError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; import { filterUndefined, getOrUndefined, has, typedEntries, typedFromEntries, typedKeys, typedValues } from "@stackframe/stack-shared/dist/utils/objects"; import { typedToUppercase } from "@stackframe/stack-shared/dist/utils/strings"; @@ -15,11 +14,9 @@ import { isUuid } from "@stackframe/stack-shared/dist/utils/uuids"; import Stripe from "stripe"; import * as yup from "yup"; import { Tenancy } from "./tenancies"; -import { getStripeForAccount } from "./stripe"; +import { getStripeForAccount, useStripeMock } from "./stripe"; const DEFAULT_PRODUCT_START_DATE = new Date("1973-01-01T12:00:00.000Z"); // monday -const stripeSecretKey = getEnvVariable("STACK_STRIPE_SECRET_KEY", ""); -const useStripeMock = stripeSecretKey === "sk_test_mockstripekey" && ["development", "test"].includes(getNodeEnvironment()); type Product = yup.InferType; type ProductWithMetadata = yup.InferType; diff --git a/apps/backend/src/lib/stripe.tsx b/apps/backend/src/lib/stripe.tsx index e722b45b1..c664f82ab 100644 --- a/apps/backend/src/lib/stripe.tsx +++ b/apps/backend/src/lib/stripe.tsx @@ -8,15 +8,18 @@ import { getEnvVariable, getNodeEnvironment } from "@stackframe/stack-shared/dis import { captureError, StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; import Stripe from "stripe"; import type * as yup from "yup"; +import { isLocalEmulatorEnabled } from "./local-emulator"; import { createStripeProxy, type StripeOverridesMap } from "./stripe-proxy"; const stripeSecretKey = getEnvVariable("STACK_STRIPE_SECRET_KEY", ""); -const useStripeMock = stripeSecretKey === "sk_test_mockstripekey" && ["development", "test"].includes(getNodeEnvironment()); +export const useStripeMock = isLocalEmulatorEnabled() + || (stripeSecretKey === "sk_test_mockstripekey" && ["development", "test"].includes(getNodeEnvironment())); const stackPortPrefix = getEnvVariable("NEXT_PUBLIC_STACK_PORT_PREFIX", "81"); +const stripeMockPort = Number(getEnvVariable("STACK_STRIPE_MOCK_PORT", "") || `${stackPortPrefix}23`); const stripeConfig: Stripe.StripeConfig = useStripeMock ? { protocol: "http", host: "localhost", - port: Number(`${stackPortPrefix}23`), + port: stripeMockPort, } : {}; /** Product type as stored in Stripe metadata (same as config product schema) */ diff --git a/apps/backend/src/lib/upstash.tsx b/apps/backend/src/lib/upstash.tsx index 6b4f48fec..e2c752096 100644 --- a/apps/backend/src/lib/upstash.tsx +++ b/apps/backend/src/lib/upstash.tsx @@ -28,6 +28,12 @@ export async function ensureUpstashSignature(fullReq: SmartRequest): Promise @@ -172,7 +174,7 @@ function PaymentsLayoutInner({ children }: { children: React.ReactNode }) { - ) : !stripeAccountInfo.details_submitted && ( + ) : stripeAccountInfo && !stripeAccountInfo.details_submitted && (
)} - {getPublicEnvVar("NEXT_PUBLIC_STACK_IS_PREVIEW") !== "true" && ( + {getPublicEnvVar("NEXT_PUBLIC_STACK_IS_PREVIEW") !== "true" && getPublicEnvVar("NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR") !== "true" && (
- {isPreview ? ( + {isPreview || isLocalEmulator ? ( - Payouts are unavailable in preview mode. + Payouts are unavailable in {isLocalEmulator ? "the local emulator" : "preview mode"}. ) : ( diff --git a/apps/dashboard/src/components/commands/ai-chat-shared.tsx b/apps/dashboard/src/components/commands/ai-chat-shared.tsx index ef955001e..75723abb4 100644 --- a/apps/dashboard/src/components/commands/ai-chat-shared.tsx +++ b/apps/dashboard/src/components/commands/ai-chat-shared.tsx @@ -211,8 +211,8 @@ export const ToolInvocationCard = memo(function ToolInvocationCard({ const { label, icon: Icon } = getToolDisplay(); - const input = invocation.input as { query?: string }; - const queryArg = input.query; + const input = invocation.input as { query?: string } | undefined; + const queryArg = input?.query; const result = invocation.output as { success?: boolean, result?: unknown[], error?: string, rowCount?: number }; return ( diff --git a/apps/dashboard/src/components/payments/stripe-connect-provider.tsx b/apps/dashboard/src/components/payments/stripe-connect-provider.tsx index 36d1fc49c..6d57a3f3d 100644 --- a/apps/dashboard/src/components/payments/stripe-connect-provider.tsx +++ b/apps/dashboard/src/components/payments/stripe-connect-provider.tsx @@ -13,6 +13,7 @@ import { useEffect } from "react"; import { appearanceVariablesForTheme } from "./stripe-theme-variables"; const isPreview = getPublicEnvVar("NEXT_PUBLIC_STACK_IS_PREVIEW") === "true"; +const isLocalEmulator = getPublicEnvVar("NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR") === "true"; type StripeConnectProviderProps = { children: React.ReactNode, @@ -36,7 +37,7 @@ export function StripeConnectProvider({ children }: StripeConnectProviderProps) const adminApp = useAdminApp(); const { resolvedTheme } = useTheme(); - const stripeConnectInstance = isPreview ? null : getStripeConnectInstance(adminApp); + const stripeConnectInstance = isPreview || isLocalEmulator ? null : getStripeConnectInstance(adminApp); useEffect(() => { if (!stripeConnectInstance) return; @@ -47,7 +48,7 @@ export function StripeConnectProvider({ children }: StripeConnectProviderProps) }); }, [resolvedTheme, stripeConnectInstance]); - // In preview mode, skip Stripe Connect initialization entirely + // In preview/emulator mode, skip Stripe Connect initialization entirely if (!stripeConnectInstance) { return <>{children}; } diff --git a/docker/local-emulator/Dockerfile b/docker/local-emulator/Dockerfile index 3ef9151a5..56deae788 100644 --- a/docker/local-emulator/Dockerfile +++ b/docker/local-emulator/Dockerfile @@ -52,6 +52,7 @@ COPY docs ./docs # https://nextjs.org/docs/pages/api-reference/next-config-js/output ENV NEXT_CONFIG_OUTPUT=standalone +ENV NEXT_PUBLIC_STACK_STRIPE_PUBLISHABLE_KEY=pk_test_mock_publishable_key_for_local_emulator # Build the backend NextJS app RUN pnpm turbo run docker-build --filter=@stackframe/backend... --filter=@stackframe/dashboard... @@ -87,8 +88,47 @@ RUN cp -a /app/node_modules /pruned-node_modules && \ date-fns@2* date-fns@3* +# ── Freestyle mock build ───────────────────────────────────────────────────── + +FROM node-base AS freestyle-mock-builder +WORKDIR /freestyle-mock +COPY docker/dependencies/freestyle-mock/Dockerfile /tmp/freestyle-mock-dockerfile +# Extract the inline package.json and server.mjs from the Dockerfile's RUN cat commands, +# then install dependencies. This avoids duplicating the source. +RUN node -e " \ + const fs = require('fs'); \ + const df = fs.readFileSync('/tmp/freestyle-mock-dockerfile', 'utf8'); \ + const pkgMatch = df.match(/cat <<'EOF' > package\\.json\\n([\\s\\S]*?)\\nEOF/); \ + fs.writeFileSync('package.json', pkgMatch[1]); \ + const srvMatch = df.match(/cat <<'EOF' > server\\.mjs\\n([\\s\\S]*?)\\nEOF/); \ + let server = srvMatch[1]; \ + server = server.replace('server.listen(8080)', 'server.listen(process.env.PORT || 8080)'); \ + server = server.replace( \ + 'from \"fs/promises\"', \ + 'from \"fs/promises\"; import { symlinkSync } from \"fs\"' \ + ); \ + server = server.replace( \ + 'await mkdir(workDir, { recursive: true });', \ + 'await mkdir(workDir, { recursive: true }); try { symlinkSync(\"/app/freestyle-mock/node_modules\", join(workDir, \"node_modules\")); } catch {}' \ + ); \ + fs.writeFileSync('server.mjs', server); \ +" +RUN npm install + + +# ── Mock OAuth server build ─────────────────────────────────────────────────── + +FROM node-base AS mock-oauth-builder +WORKDIR /mock-oauth +COPY apps/mock-oauth-server/package.json . +RUN pnpm install && pnpm add esbuild --save-dev +COPY apps/mock-oauth-server/src ./src +RUN npx esbuild src/index.ts --bundle --platform=node --target=node22 --outfile=dist/index.cjs + + # ── Service binary stages ───────────────────────────────────────────────────── +FROM stripe/stripe-mock:v0.195.0 AS stripe-mock-bin FROM inbucket/inbucket:3.1.0 AS inbucket-bin FROM svix/svix-server:v1.88.0 AS svix-bin FROM clickhouse/clickhouse-server:25.10 AS clickhouse-bin @@ -159,6 +199,9 @@ COPY --from=node-base /usr/local/bin/node /usr/local/bin/node # Inbucket COPY --from=inbucket-bin /opt/inbucket /opt/inbucket +# Stripe mock +COPY --from=stripe-mock-bin /bin/stripe-mock /usr/local/bin/stripe-mock + # Svix (UPX-compressed) COPY --from=upx-compress /out/svix-server /usr/local/bin/svix-server @@ -191,6 +234,14 @@ RUN cp -a /app/node_modules /app/node_modules.standalone 2>/dev/null || mkdir -p COPY --from=migration-pruner /pruned-node_modules ./node_modules COPY --from=builder /app/packages ./packages +# Mock OAuth server (bundled single file) +COPY --from=mock-oauth-builder /mock-oauth/dist/index.cjs /app/mock-oauth-server/index.cjs + +# Freestyle mock (JS execution for email rendering) +COPY --from=freestyle-mock-builder /freestyle-mock /app/freestyle-mock +COPY --from=node-base /usr/local/bin/npm /usr/local/bin/npm +COPY --from=node-base /usr/local/lib/node_modules/npm /usr/local/lib/node_modules/npm + RUN mkdir -p \ /data/postgres \ /data/redis \ @@ -207,17 +258,18 @@ RUN mkdir -p \ && chown -R postgres:postgres /data/postgres COPY docker/local-emulator/supervisord.conf /etc/supervisor/conf.d/supervisord.conf +COPY docker/local-emulator/run-cron-jobs.sh /run-cron-jobs.sh COPY docker/local-emulator/entrypoint.sh /entrypoint.sh COPY docker/local-emulator/init-services.sh /init-services.sh COPY docker/local-emulator/start-app.sh /start-app.sh COPY docker/local-emulator/clickhouse-config.xml /etc/clickhouse-server/config.xml COPY docker/local-emulator/clickhouse-users.xml /etc/clickhouse-server/users.xml COPY docker/server/entrypoint.sh /app-entrypoint.sh -RUN chmod +x /entrypoint.sh /init-services.sh /start-app.sh /app-entrypoint.sh +RUN chmod +x /entrypoint.sh /init-services.sh /start-app.sh /app-entrypoint.sh /run-cron-jobs.sh # PostgreSQL: 5432, Redis: 6379, Inbucket: 2500/9001/1100, # Svix: 8071, ClickHouse: 8123/9009, MinIO: 9090, QStash: 8080 -# Backend: 8102, Dashboard: 8101 -EXPOSE 5432 6379 2500 9001 1100 8071 8123 9009 9090 8080 8101 8102 +# Backend: 8102, Dashboard: 8101, Mock OAuth: 8114 +EXPOSE 5432 6379 2500 9001 1100 8071 8123 9009 9090 8080 8101 8102 8114 ENTRYPOINT ["/entrypoint.sh"] diff --git a/docker/local-emulator/clickhouse-config.xml b/docker/local-emulator/clickhouse-config.xml index 31aa71922..0ba2c03fb 100644 --- a/docker/local-emulator/clickhouse-config.xml +++ b/docker/local-emulator/clickhouse-config.xml @@ -15,6 +15,8 @@ 0.5 + SQL_ + users.xml diff --git a/docker/local-emulator/entrypoint.sh b/docker/local-emulator/entrypoint.sh index daa985465..562cb6795 100644 --- a/docker/local-emulator/entrypoint.sh +++ b/docker/local-emulator/entrypoint.sh @@ -28,4 +28,11 @@ if [ -z "$(ls -A "$PGDATA" 2>/dev/null)" ]; then gosu postgres "$PG_BIN/pg_ctl" -D "$PGDATA" stop -w fi +# Generate a fresh CRON_SECRET per container start. The cron endpoints are +# internal — nothing outside the container calls them — so we don't want the +# baked-in mock value from .env.development to be a usable credential against +# a running emulator. Overriding here propagates to both the backend and the +# run-cron-jobs.sh loop via supervisord's inherited environment. +export CRON_SECRET="$(openssl rand -hex 32)" + exec /usr/bin/supervisord -n -c /etc/supervisor/conf.d/supervisord.conf diff --git a/docker/local-emulator/generate-env-development.mjs b/docker/local-emulator/generate-env-development.mjs index f0b0b20d2..1266c2bae 100644 --- a/docker/local-emulator/generate-env-development.mjs +++ b/docker/local-emulator/generate-env-development.mjs @@ -90,9 +90,11 @@ const entries = [ fromSource("apps/backend/.env.development", backendEnv, "STACK_SEED_INTERNAL_PROJECT_ALLOW_LOCALHOST"), fromSource("apps/backend/.env.development", backendEnv, "STACK_SEED_INTERNAL_PROJECT_OAUTH_PROVIDERS"), fromSource("apps/backend/.env.development", backendEnv, "STACK_SEED_INTERNAL_PROJECT_USER_INTERNAL_ACCESS"), - fromSource("apps/backend/.env.development", backendEnv, "STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY"), - fromSource("apps/backend/.env.development", backendEnv, "STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY"), - fromSource("apps/backend/.env.development", backendEnv, "STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY"), + // STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY is generated per-VM at boot + // by docker/local-emulator/qemu/cloud-init/emulator/user-data and injected via + // /run/stack-auth/local-emulator.env. SECRET_SERVER_KEY and SUPER_SECRET_ADMIN_KEY + // are intentionally omitted so the seed script leaves them null on the internal + // project; per-project credentials come from /api/v1/internal/local-emulator/project. blank(), comment("# Third-party/test integrations"), fromSource("apps/backend/.env.development", backendEnv, "STACK_SVIX_API_KEY"), @@ -159,7 +161,7 @@ const entries = [ literal("STACK_S3_ENDPOINT", "http://127.0.0.1:9090"), literal("STACK_QSTASH_URL", "http://127.0.0.1:8080"), literal("STACK_CLICKHOUSE_URL", "http://127.0.0.1:8123"), - literal("STACK_CLICKHOUSE_DATABASE", "analytics"), + literal("STACK_CLICKHOUSE_DATABASE", "default"), literal("STACK_EMAIL_MONITOR_INBUCKET_API_URL", "http://127.0.0.1:9001"), literal("BACKEND_PORT", "8102"), literal("DASHBOARD_PORT", "8101"), diff --git a/docker/local-emulator/qemu/build-image.sh b/docker/local-emulator/qemu/build-image.sh index 498d16173..f4d91771b 100755 --- a/docker/local-emulator/qemu/build-image.sh +++ b/docker/local-emulator/qemu/build-image.sh @@ -5,7 +5,7 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" # shellcheck source=common.sh source "$SCRIPT_DIR/common.sh" -IMAGE_DIR="$SCRIPT_DIR/images" +IMAGE_DIR="${EMULATOR_IMAGE_DIR:-$HOME/.stack/emulator/images}" CLOUD_INIT_ROOT="$SCRIPT_DIR/cloud-init" REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" diff --git a/docker/local-emulator/qemu/cloud-init/emulator/user-data b/docker/local-emulator/qemu/cloud-init/emulator/user-data index 07b0bc5f4..38fe2b064 100644 --- a/docker/local-emulator/qemu/cloud-init/emulator/user-data +++ b/docker/local-emulator/qemu/cloud-init/emulator/user-data @@ -58,7 +58,7 @@ write_files: #!/bin/bash set -euo pipefail - mkdir -p /mnt/stack-runtime /run/stack-auth + mkdir -p /mnt/stack-runtime /run/stack-auth /var/lib/stack-auth runtime_device="$(readlink -f /dev/disk/by-label/STACKCFG)" mountpoint -q /mnt/stack-runtime || mount -o ro "$runtime_device" /mnt/stack-runtime @@ -67,6 +67,24 @@ write_files: source /mnt/stack-runtime/base.env set +a + # Generate and persist the internal-project keys on first boot; reuse + # across container restarts so the dashboard keeps its internal-project + # session. Reset via `stack emulator reset`. + # + # pck: used by stack-cli to auth against /api/v1/internal/local-emulator/project + # ssk/sak: required by the emulator's own dashboard (StackServerApp + # construction throws without them). Not used by user-app flows; the + # /local-emulator/project route mints separate per-project credentials. + umask 077 + for key in internal-pck internal-ssk internal-sak; do + if [ ! -s "/var/lib/stack-auth/$key" ]; then + openssl rand -hex 32 > "/var/lib/stack-auth/$key" + fi + done + INTERNAL_PCK="$(cat /var/lib/stack-auth/internal-pck)" + INTERNAL_SSK="$(cat /var/lib/stack-auth/internal-ssk)" + INTERNAL_SAK="$(cat /var/lib/stack-auth/internal-sak)" + # Container-local dependencies run on localhost. Host-only development # services (such as the OAuth mock server) are reachable via the QEMU # user-network host alias. @@ -78,6 +96,9 @@ write_files: # Static vars from base config and runtime (e.g. API keys, feature flags) cat /mnt/stack-runtime/base.env cat /mnt/stack-runtime/runtime.env + printf 'STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY=%s\n' "$INTERNAL_PCK" + printf 'STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY=%s\n' "$INTERNAL_SSK" + printf 'STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY=%s\n' "$INTERNAL_SAK" # Computed vars — depend on port prefix or deps host # Host-side ports (for browser URLs — browser runs on host, not in VM) @@ -108,7 +129,10 @@ write_files: STACK_CLICKHOUSE_URL=http://${DEPS_HOST}:8123 STACK_EMAIL_MONITOR_VERIFICATION_CALLBACK_URL=http://localhost:${HP_DASHBOARD}/handler/email-verification STACK_EMAIL_MONITOR_INBUCKET_API_URL=http://${DEPS_HOST}:9001 - STACK_OAUTH_MOCK_URL=http://${HOST_SERVICES_HOST}:${P}14 + STACK_OAUTH_MOCK_URL=http://localhost:${P}14 + STACK_FREESTYLE_API_ENDPOINT=http://${DEPS_HOST}:8180 + STACK_STRIPE_MOCK_PORT=12111 + NEXT_PUBLIC_STACK_STRIPE_PUBLISHABLE_KEY=pk_test_mock_publishable_key_for_local_emulator BACKEND_PORT=${P}02 DASHBOARD_PORT=${P}01 COMPUTED @@ -135,20 +159,54 @@ write_files: /usr/local/bin/mount-host-fs /usr/local/bin/render-stack-env + + # Publish the internal publishable client key to the host via 9p so the + # stack-cli can authenticate its bootstrap call to + # /api/v1/internal/local-emulator/project. + set -a + source /mnt/stack-runtime/runtime.env + set +a + if [ -n "${STACK_EMULATOR_VM_DIR_HOST:-}" ] && [ -s /var/lib/stack-auth/internal-pck ]; then + install -m 0600 /var/lib/stack-auth/internal-pck \ + "/host${STACK_EMULATOR_VM_DIR_HOST}/internal-pck" + fi + docker rm -f stack >/dev/null 2>&1 || true - exec docker run \ - --rm \ - --name stack \ - --network host \ - --add-host host.docker.internal:host-gateway \ - --env-file /run/stack-auth/local-emulator.env \ - -v stack-postgres-data:/data/postgres \ - -v stack-redis-data:/data/redis \ - -v stack-clickhouse-data:/data/clickhouse \ - -v stack-minio-data:/data/minio \ - -v stack-inbucket-data:/data/inbucket \ - -v /host:/host \ - stack-local-emulator + + # Mirror container stdout/stderr to a host-visible log for debugging. + # The container already bind-mounts /host:/host, so we reuse that path. + # Falls back to stdout (captured by systemd-journald) when no host log is set. + if [ -n "${STACK_EMULATOR_VM_DIR_HOST:-}" ]; then + host_log="/host${STACK_EMULATOR_VM_DIR_HOST}/stack.log" + : > "$host_log" 2>/dev/null || true + exec docker run \ + --rm \ + --name stack \ + --network host \ + --add-host host.docker.internal:host-gateway \ + --env-file /run/stack-auth/local-emulator.env \ + -v stack-postgres-data:/data/postgres \ + -v stack-redis-data:/data/redis \ + -v stack-clickhouse-data:/data/clickhouse \ + -v stack-minio-data:/data/minio \ + -v stack-inbucket-data:/data/inbucket \ + -v /host:/host \ + stack-local-emulator 2>&1 | tee -a "$host_log" + else + exec docker run \ + --rm \ + --name stack \ + --network host \ + --add-host host.docker.internal:host-gateway \ + --env-file /run/stack-auth/local-emulator.env \ + -v stack-postgres-data:/data/postgres \ + -v stack-redis-data:/data/redis \ + -v stack-clickhouse-data:/data/clickhouse \ + -v stack-minio-data:/data/minio \ + -v stack-inbucket-data:/data/inbucket \ + -v /host:/host \ + stack-local-emulator + fi - path: /usr/local/bin/wait-for-deps permissions: '0755' @@ -231,7 +289,7 @@ write_files: NEXT_PUBLIC_SERVER_STACK_DASHBOARD_URL=http://127.0.0.1:8101 NEXT_PUBLIC_STACK_SVIX_SERVER_URL=http://localhost:8071 NEXT_PUBLIC_STACK_PORT_PREFIX=81 - STACK_CLICKHOUSE_DATABASE=analytics + STACK_CLICKHOUSE_DATABASE=default BACKEND_PORT=8102 DASHBOARD_PORT=8101 @@ -369,10 +427,23 @@ write_files: log "Skipping smoke test: build arch is arm64 and cross-arch TCG can't reliably run the backend." else log "Running smoke test on slim image..." + # build.env sets NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR=true, which makes + # docker/server/entrypoint.sh require the three internal SEED keys. + # At real-VM boot those come from render-stack-env via + # /run/stack-auth/local-emulator.env, but that path doesn't run during + # the build-time smoke test. Mint throwaway hex keys for this container + # only; they must be hex because entrypoint.sh also validates that + # before the internal ApiKeySet bootstrap SQL. + SMOKE_PCK="$(openssl rand -hex 32)" + SMOKE_SSK="$(openssl rand -hex 32)" + SMOKE_SAK="$(openssl rand -hex 32)" docker run --rm --name smoke-test \ --network host \ --env-file /etc/stack-build.env \ --env-file /etc/stack-build-computed.env \ + -e STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY="$SMOKE_PCK" \ + -e STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY="$SMOKE_SSK" \ + -e STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY="$SMOKE_SAK" \ -e STACK_SKIP_MIGRATIONS=true \ -e STACK_SKIP_SEED_SCRIPT=true \ -e STACK_RUNTIME_WORK_DIR=/app \ diff --git a/docker/local-emulator/qemu/run-emulator.sh b/docker/local-emulator/qemu/run-emulator.sh index 0a82c1b88..ba905ca36 100755 --- a/docker/local-emulator/qemu/run-emulator.sh +++ b/docker/local-emulator/qemu/run-emulator.sh @@ -5,8 +5,8 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" # shellcheck source=common.sh source "$SCRIPT_DIR/common.sh" -IMAGE_DIR="$SCRIPT_DIR/images" -RUN_DIR="${EMULATOR_RUN_DIR:-$SCRIPT_DIR/run}" +IMAGE_DIR="${EMULATOR_IMAGE_DIR:-$HOME/.stack/emulator/images}" +RUN_DIR="${EMULATOR_RUN_DIR:-$HOME/.stack/emulator/run}" VM_RAM="${EMULATOR_RAM:-4096}" VM_CPUS="${EMULATOR_CPUS:-4}" @@ -89,6 +89,7 @@ prepare_runtime_config_iso() { printf "STACK_EMULATOR_BACKEND_HOST_PORT=%s\n" "$EMULATOR_BACKEND_PORT" printf "STACK_EMULATOR_MINIO_HOST_PORT=%s\n" "$EMULATOR_MINIO_PORT" printf "STACK_EMULATOR_INBUCKET_HOST_PORT=%s\n" "$EMULATOR_INBUCKET_PORT" + printf "STACK_EMULATOR_VM_DIR_HOST=%s\n" "$VM_DIR" } > "$cfg_dir/runtime.env" cp "$SCRIPT_DIR/../.env.development" "$cfg_dir/base.env" make_iso_from_dir "$cfg_iso" "STACKCFG" "$cfg_dir" @@ -201,10 +202,16 @@ build_qemu_cmd() { local netdev="user,id=net0" # Only expose user-facing services; internal deps stay inside the VM. - netdev+=",hostfwd=tcp::${EMULATOR_DASHBOARD_PORT}-:${PORT_PREFIX}01" - netdev+=",hostfwd=tcp::${EMULATOR_BACKEND_PORT}-:${PORT_PREFIX}02" - netdev+=",hostfwd=tcp::${EMULATOR_MINIO_PORT}-:9090" - netdev+=",hostfwd=tcp::${EMULATOR_INBUCKET_PORT}-:9001" + # Bind to 127.0.0.1 so the emulator is not reachable from the LAN. + netdev+=",hostfwd=tcp:127.0.0.1:${EMULATOR_DASHBOARD_PORT}-:${PORT_PREFIX}01" + netdev+=",hostfwd=tcp:127.0.0.1:${EMULATOR_BACKEND_PORT}-:${PORT_PREFIX}02" + netdev+=",hostfwd=tcp:127.0.0.1:${EMULATOR_MINIO_PORT}-:9090" + netdev+=",hostfwd=tcp:127.0.0.1:${EMULATOR_INBUCKET_PORT}-:9001" + # Mock OAuth server: browser redirects land on `localhost:${PORT_PREFIX}14` + # (backend sets STACK_OAUTH_MOCK_URL to that value), so we forward host:port + # ↔ VM:port on the same number. Collides with pnpm dev, but the two modes + # are mutually exclusive. + netdev+=",hostfwd=tcp:127.0.0.1:${PORT_PREFIX}14-:${PORT_PREFIX}14" QEMU_CMD=( "$qemu_bin" @@ -249,7 +256,7 @@ tail_vm_logs() { } ensure_ports_free() { - local ports=("$EMULATOR_DASHBOARD_PORT" "$EMULATOR_BACKEND_PORT" "$EMULATOR_MINIO_PORT" "$EMULATOR_INBUCKET_PORT") + local ports=("$EMULATOR_DASHBOARD_PORT" "$EMULATOR_BACKEND_PORT" "$EMULATOR_MINIO_PORT" "$EMULATOR_INBUCKET_PORT" "${PORT_PREFIX}14") local port for port in "${ports[@]}"; do if lsof -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1; then diff --git a/docker/local-emulator/run-cron-jobs.sh b/docker/local-emulator/run-cron-jobs.sh new file mode 100755 index 000000000..a30cf03e6 --- /dev/null +++ b/docker/local-emulator/run-cron-jobs.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# Polls backend cron endpoints in parallel background loops, matching vercel.json cron config. +# Replaces the tsx scripts used in dev mode since tsx is not in the final image. + +set -e + +BACKEND_URL="http://127.0.0.1:${BACKEND_PORT:-8102}" + +if [ -z "${CRON_SECRET:-}" ]; then + echo "CRON_SECRET is not set; refusing to start cron loops." >&2 + exit 1 +fi + +# Wait for the backend to be ready +until curl -fsS "${BACKEND_URL}/health" >/dev/null 2>&1; do sleep 2; done + +echo "Cron jobs started." + +run_loop() { + local endpoint="$1" + while true; do + curl -sf -o /dev/null --max-time 120 "${BACKEND_URL}${endpoint}" \ + -H "Authorization: Bearer ${CRON_SECRET}" || true + sleep 1 + done +} + +run_loop "/api/latest/internal/email-queue-step" & +run_loop "/api/latest/internal/external-db-sync/sequencer" & +run_loop "/api/latest/internal/external-db-sync/poller" & + +wait diff --git a/docker/local-emulator/supervisord.conf b/docker/local-emulator/supervisord.conf index e8b1fc478..32890bfe7 100644 --- a/docker/local-emulator/supervisord.conf +++ b/docker/local-emulator/supervisord.conf @@ -50,7 +50,8 @@ environment= INBUCKET_WEB_ADDR="0.0.0.0:9001", INBUCKET_POP3_ADDR="0.0.0.0:1100", INBUCKET_STORAGE_TYPE="file", - INBUCKET_STORAGE_PARAMS="path:/data/inbucket" + INBUCKET_STORAGE_PARAMS="path:/data/inbucket", + INBUCKET_WEB_UIDIR="/opt/inbucket/ui" autostart=true autorestart=true priority=20 @@ -120,6 +121,43 @@ stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 +; --- Stripe mock --- + +[program:stripe-mock] +command=/usr/local/bin/stripe-mock -port 12111 +autostart=true +autorestart=true +priority=20 +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 + +; --- Freestyle mock (JS execution for email rendering) --- + +[program:freestyle-mock] +command=/usr/local/bin/node /app/freestyle-mock/server.mjs +environment=NODE_PATH="/app/freestyle-mock/node_modules",PORT="8180" +autostart=true +autorestart=true +priority=20 +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 + +; --- Mock OAuth server --- + +[program:mock-oauth] +command=/usr/local/bin/node /app/mock-oauth-server/index.cjs +autostart=true +autorestart=true +priority=20 +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 + ; --- Post-startup init --- [program:init-services] @@ -134,6 +172,19 @@ stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 +; --- Cron jobs (email queue, external DB sync) --- + +[program:cron-jobs] +command=/run-cron-jobs.sh +autostart=true +autorestart=true +startsecs=0 +priority=70 +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 + ; --- Stack Auth backend + dashboard --- [program:stack-app] diff --git a/docker/server/entrypoint.sh b/docker/server/entrypoint.sh index da7214a01..659eb9628 100644 --- a/docker/server/entrypoint.sh +++ b/docker/server/entrypoint.sh @@ -11,14 +11,28 @@ fi # ============= ENV VARS ============= -export STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY=${STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY:-$(openssl rand -base64 32)} -export STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY=${STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY:-$(openssl rand -base64 32)} -export STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY=${STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY:-$(openssl rand -base64 32)} +if [ "$NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR" = "true" ]; then + for v in STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY; do + if [ -z "${!v:-}" ]; then + echo "$v must be set in local-emulator mode (injected by the QEMU VM)." >&2 + exit 1 + fi + done + export STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY +else + export STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY=${STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY:-$(openssl rand -base64 32)} + export STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY=${STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY:-$(openssl rand -base64 32)} + export STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY=${STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY:-$(openssl rand -base64 32)} +fi export NEXT_PUBLIC_STACK_PROJECT_ID=internal export NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=${STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY} -export STACK_SECRET_SERVER_KEY=${STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY} -export STACK_SUPER_SECRET_ADMIN_KEY=${STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY} +if [ -n "${STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY:-}" ]; then + export STACK_SECRET_SERVER_KEY=${STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY} +fi +if [ -n "${STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY:-}" ]; then + export STACK_SUPER_SECRET_ADMIN_KEY=${STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY} +fi export NEXT_PUBLIC_BROWSER_STACK_DASHBOARD_URL=${NEXT_PUBLIC_STACK_DASHBOARD_URL} export NEXT_PUBLIC_STACK_PORT_PREFIX=${NEXT_PUBLIC_STACK_PORT_PREFIX:-81} @@ -65,6 +79,44 @@ else cd ../.. fi +# ============= LOCAL EMULATOR: BOOTSTRAP INTERNAL API KEY SET ============= +# The build-time seed ran without any keys (the VM generates random ones on +# first boot). The slim image strips apps/backend/dist so we can't re-run the +# full seed here. Instead, targeted-upsert the internal api key set with the +# VM-supplied keys: +# - pck: used by stack-cli to auth against /api/v1/internal/local-emulator/project +# - ssk/sak: required by the emulator's own dashboard (StackServerApp ctor +# throws without ssk). User-app flows don't use these — per-project +# credentials come from the /local-emulator/project route. +if [ "$NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR" = "true" ] && [ -n "${STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY:-}" ] && [ -n "${STACK_DATABASE_CONNECTION_STRING:-}" ]; then + # Validate the keys are hex-only to defuse any SQL-injection risk (the VM + # generates them via `openssl rand -hex 32`, so this is an assert, not a filter). + for varname in STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY; do + val="${!varname:-}" + if [ -z "$val" ]; then + echo "ERROR: $varname is not set; refusing to bootstrap internal api key set." >&2 + exit 1 + fi + if ! printf '%s' "$val" | grep -Eq '^[0-9a-fA-F]+$'; then + echo "ERROR: $varname is not hex-only; refusing to bootstrap internal api key set." >&2 + exit 1 + fi + done + echo "Bootstrapping internal API key set (emulator runtime)..." + psql "$STACK_DATABASE_CONNECTION_STRING" -v ON_ERROR_STOP=1 < { + const path = internalPckPath(); + const deadline = Date.now() + timeoutMs; + let delay = 250; + while (Date.now() < deadline) { + if (existsSync(path)) { + const contents = readFileSync(path, "utf-8").trim(); + if (contents) return contents; + } + await new Promise((r) => setTimeout(r, delay)); + delay = Math.min(delay * 2, 2000); + } + throw new CliError(`Timed out waiting for emulator internal publishable client key at ${path}`); +} + +type EmulatorCredentials = { + project_id: string, + publishable_client_key: string, + secret_server_key: string, +}; + +async function fetchEmulatorCredentials(pck: string, backendPort: number, configFile: string): Promise { + const url = `http://127.0.0.1:${backendPort}/api/v1/internal/local-emulator/project`; + const res = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Stack-Project-Id": "internal", + "X-Stack-Access-Type": "client", + "X-Stack-Publishable-Client-Key": pck, + }, + body: JSON.stringify({ absolute_file_path: configFile }), + }); + if (!res.ok) { + throw new CliError(`Failed to initialize local emulator project (${res.status}): ${await res.text()}`); + } + const data = await res.json() as { + project_id: string, + publishable_client_key: string, + secret_server_key: string, + }; + return { + project_id: data.project_id, + publishable_client_key: data.publishable_client_key, + secret_server_key: data.secret_server_key, + }; +} + function gh(args: string[]): string { try { return execFileSync("gh", args, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim(); @@ -15,27 +93,63 @@ function gh(args: string[]): string { } } -function findQemuDir(): string { - for (const rel of ["docker/local-emulator/qemu", "../docker/local-emulator/qemu"]) { - const dir = resolve(process.cwd(), rel); - if (existsSync(join(dir, "run-emulator.sh"))) return dir; - } - throw new CliError("Could not find QEMU emulator directory. Run this from the stack-auth repo root."); +function emulatorScriptsDir(): string { + const here = dirname(fileURLToPath(import.meta.url)); + const bundled = join(here, "emulator"); + if (existsSync(join(bundled, "run-emulator.sh"))) return bundled; + const repo = resolve(here, "../../../docker/local-emulator/qemu"); + if (existsSync(join(repo, "run-emulator.sh"))) return repo; + throw new CliError("Emulator scripts not found in CLI bundle."); +} + +function emulatorSpawnEnv(extra?: Record): NodeJS.ProcessEnv { + return { + ...process.env, + EMULATOR_RUN_DIR: emulatorRunDir(), + EMULATOR_IMAGE_DIR: emulatorImageDir(), + ...extra, + }; } function runEmulator(action: string, env?: Record): Promise { - const qemuDir = findQemuDir(); - return new Promise((resolve, reject) => { - const child = spawn(join(qemuDir, "run-emulator.sh"), [action], { + const scriptsDir = emulatorScriptsDir(); + mkdirSync(emulatorRunDir(), { recursive: true }); + mkdirSync(emulatorImageDir(), { recursive: true }); + return new Promise((resolvePromise, reject) => { + const child = spawn(join(scriptsDir, "run-emulator.sh"), [action], { stdio: "inherit", - env: { ...process.env, ...env }, - cwd: qemuDir, + env: emulatorSpawnEnv(env), + cwd: scriptsDir, }); - child.on("close", (code) => code === 0 ? resolve() : reject(new CliError(`run-emulator.sh ${action} exited with code ${code}`))); + child.on("close", (code) => code === 0 ? resolvePromise() : reject(new CliError(`run-emulator.sh ${action} exited with code ${code}`))); child.on("error", (err) => reject(new CliError(`Failed to run run-emulator.sh: ${err.message}`))); }); } +function isEmulatorRunning(): boolean { + const scriptsDir = emulatorScriptsDir(); + try { + execFileSync(join(scriptsDir, "run-emulator.sh"), ["status"], { + stdio: "pipe", + cwd: scriptsDir, + env: emulatorSpawnEnv(), + }); + return true; + } catch { + return false; + } +} + +async function startEmulator(arch: "arm64" | "amd64"): Promise { + mkdirSync(emulatorImageDir(), { recursive: true }); + const img = join(emulatorImageDir(), `stack-emulator-${arch}.qcow2`); + if (!existsSync(img)) { + console.log("No emulator image found. Pulling latest..."); + pullRelease(arch); + } + await runEmulator("start", { EMULATOR_ARCH: arch }); +} + function resolveArch(raw?: string): "arm64" | "amd64" { const arch = raw ?? (process.arch === "arm64" ? "arm64" : process.arch === "x64" ? "amd64" : null); if (arch === "arm64" || arch === "amd64") return arch; @@ -47,7 +161,7 @@ function pullRelease(arch: "arm64" | "amd64", opts: { repo?: string; branch?: st const branch = opts.branch ?? "dev"; const tag = opts.tag ?? `emulator-${branch}-latest`; const asset = `stack-emulator-${arch}.qcow2`; - const imageDir = join(findQemuDir(), "images"); + const imageDir = emulatorImageDir(); mkdirSync(imageDir, { recursive: true }); const dest = join(imageDir, asset); const tmpDest = `${dest}.download`; @@ -89,7 +203,7 @@ export function registerEmulatorCommand(program: Command) { runId = String(runs[0].databaseId); } - const imageDir = join(findQemuDir(), "images"); + const imageDir = emulatorImageDir(); mkdirSync(imageDir, { recursive: true }); const dest = join(imageDir, `stack-emulator-${arch}.qcow2`); if (existsSync(dest)) unlinkSync(dest); @@ -110,14 +224,91 @@ export function registerEmulatorCommand(program: Command) { .command("start") .description("Start the emulator in the background (auto-pulls the latest image if none exists)") .option("--arch ", "Target architecture (default: current system arch). Non-native uses software emulation and is significantly slower.") - .action(async (opts) => { + .option("--config-file ", "Path to a config file; when set, credentials for this project are printed to stdout as JSON") + .action(async (opts: { arch?: string, configFile?: string }) => { const arch = resolveArch(opts.arch); - const img = join(findQemuDir(), "images", `stack-emulator-${arch}.qcow2`); - if (!existsSync(img)) { - console.log("No emulator image found. Pulling latest..."); - pullRelease(arch); + + let resolvedConfigFile: string | undefined; + if (opts.configFile) { + resolvedConfigFile = resolve(opts.configFile); + if (!existsSync(resolvedConfigFile)) { + throw new CliError(`Config file not found: ${resolvedConfigFile}`); + } } - await runEmulator("start", { EMULATOR_ARCH: arch }); + + if (isEmulatorRunning()) { + console.warn("Emulator already running, reusing existing instance."); + } else { + await startEmulator(arch); + } + + if (resolvedConfigFile) { + const pck = await readInternalPck(); + const creds = await fetchEmulatorCredentials(pck, emulatorBackendPort(), resolvedConfigFile); + console.log(JSON.stringify(creds, null, 2)); + } + }); + + emulator + .command("run") + .description("Start the emulator, run a command, and stop the emulator when the command exits") + .argument("", "Command to run (e.g. \"npm run dev\")") + .option("--arch ", "Target architecture") + .option("--config-file ", "Path to a config file; fetches credentials and injects STACK_PROJECT_ID / STACK_PUBLISHABLE_CLIENT_KEY / STACK_SECRET_SERVER_KEY into the child") + .action(async (cmd: string, opts: { arch?: string, configFile?: string }) => { + const arch = resolveArch(opts.arch); + + let resolvedConfigFile: string | undefined; + if (opts.configFile) { + resolvedConfigFile = resolve(opts.configFile); + if (!existsSync(resolvedConfigFile)) { + throw new CliError(`Config file not found: ${resolvedConfigFile}`); + } + } + + const alreadyRunning = isEmulatorRunning(); + if (alreadyRunning) { + console.log("Emulator already running, reusing existing instance."); + } else { + await startEmulator(arch); + } + + const childEnv: Record = { ...process.env as Record }; + if (resolvedConfigFile) { + const pck = await readInternalPck(); + const backendPort = emulatorBackendPort(); + const creds = await fetchEmulatorCredentials(pck, backendPort, resolvedConfigFile); + const apiUrl = `http://127.0.0.1:${backendPort}`; + childEnv.STACK_PROJECT_ID = creds.project_id; + childEnv.NEXT_PUBLIC_STACK_PROJECT_ID = creds.project_id; + childEnv.STACK_PUBLISHABLE_CLIENT_KEY = creds.publishable_client_key; + childEnv.NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY = creds.publishable_client_key; + childEnv.STACK_SECRET_SERVER_KEY = creds.secret_server_key; + childEnv.STACK_API_URL = apiUrl; + childEnv.NEXT_PUBLIC_STACK_API_URL = apiUrl; + } + + const child = spawn(cmd, { shell: true, stdio: "inherit", env: childEnv }); + + const forward = (signal: NodeJS.Signals) => () => child.kill(signal); + const onSigint = forward("SIGINT"); + const onSigterm = forward("SIGTERM"); + process.on("SIGINT", onSigint); + process.on("SIGTERM", onSigterm); + + child.on("close", (code) => { + process.off("SIGINT", onSigint); + process.off("SIGTERM", onSigterm); + const exitCode = code ?? 1; + if (alreadyRunning) { + process.exit(exitCode); + } else { + console.log("\nStopping emulator..."); + runEmulator("stop") + .catch(() => { /* best-effort stop */ }) + .finally(() => process.exit(exitCode)); + } + }); }); emulator.command("stop").description("Stop the emulator (data preserved; use 'reset' to clear)").action(() => runEmulator("stop")); diff --git a/packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts b/packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts index 2755c1a97..c34a8b8a3 100644 --- a/packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts +++ b/packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts @@ -126,6 +126,7 @@ export class _StackAdminAppImplIncomplete, extraOptions?: { uniqueIdentifier?: string, checkString?: string, interface?: StackAdminInterface }) { const resolvedOptions = resolveConstructorOptions(options); + const publishableClientKey = resolvedOptions.publishableClientKey ?? getDefaultPublishableClientKey(); super(resolvedOptions, { From b68710e98e99a7ed2276b0f548fdf17830c6341e Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Tue, 14 Apr 2026 18:06:36 -0700 Subject: [PATCH 4/6] chore: update package versions --- apps/backend/package.json | 2 +- apps/dashboard/package.json | 2 +- apps/dev-launchpad/package.json | 2 +- apps/e2e/package.json | 2 +- apps/hosted-components/package.json | 2 +- apps/mock-oauth-server/package.json | 2 +- docs-mintlify/package.json | 2 +- docs/package.json | 2 +- examples/cjs-test/package.json | 2 +- examples/convex/package.json | 2 +- examples/demo/package.json | 2 +- examples/docs-examples/package.json | 2 +- examples/e-commerce/package.json | 2 +- examples/js-example/package.json | 2 +- examples/lovable-react-18-example/package.json | 2 +- examples/middleware/package.json | 2 +- examples/react-example/package.json | 2 +- examples/supabase/package.json | 2 +- packages/dashboard-ui-components/package.json | 2 +- packages/init-stack/package.json | 2 +- packages/js/package.json | 2 +- packages/react/package.json | 2 +- packages/stack-cli/package.json | 2 +- packages/stack-sc/package.json | 2 +- packages/stack-shared/package.json | 2 +- packages/stack-ui/package.json | 2 +- packages/stack/package.json | 2 +- packages/template/package-template.json | 2 +- packages/template/package.json | 2 +- sdks/implementations/swift/package.json | 2 +- sdks/spec/package.json | 2 +- 31 files changed, 31 insertions(+), 31 deletions(-) diff --git a/apps/backend/package.json b/apps/backend/package.json index 2daa79d88..b1f80a58b 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/backend", - "version": "2.8.83", + "version": "2.8.84", "repository": "https://github.com/stack-auth/stack-auth", "private": true, "type": "module", diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 337f6a437..bbf812657 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/dashboard", - "version": "2.8.83", + "version": "2.8.84", "repository": "https://github.com/stack-auth/stack-auth", "private": true, "scripts": { diff --git a/apps/dev-launchpad/package.json b/apps/dev-launchpad/package.json index 23ea4a23b..ab70c318e 100644 --- a/apps/dev-launchpad/package.json +++ b/apps/dev-launchpad/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/dev-launchpad", - "version": "2.8.83", + "version": "2.8.84", "repository": "https://github.com/stack-auth/stack-auth", "private": true, "scripts": { diff --git a/apps/e2e/package.json b/apps/e2e/package.json index 91801cab5..6dbeab447 100644 --- a/apps/e2e/package.json +++ b/apps/e2e/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/e2e-tests", - "version": "2.8.83", + "version": "2.8.84", "repository": "https://github.com/stack-auth/stack-auth", "private": true, "type": "module", diff --git a/apps/hosted-components/package.json b/apps/hosted-components/package.json index e0a9a308a..031058657 100644 --- a/apps/hosted-components/package.json +++ b/apps/hosted-components/package.json @@ -1,7 +1,7 @@ { "name": "@stackframe/hosted-components", "private": true, - "version": "2.8.83", + "version": "2.8.84", "type": "module", "scripts": { "dev": "vite dev --port ${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}09", diff --git a/apps/mock-oauth-server/package.json b/apps/mock-oauth-server/package.json index fe0142dc2..23ed1b03e 100644 --- a/apps/mock-oauth-server/package.json +++ b/apps/mock-oauth-server/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/mock-oauth-server", - "version": "2.8.83", + "version": "2.8.84", "repository": "https://github.com/stack-auth/stack-auth", "private": true, "main": "index.js", diff --git a/docs-mintlify/package.json b/docs-mintlify/package.json index bee18ef16..d238aa70e 100644 --- a/docs-mintlify/package.json +++ b/docs-mintlify/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/docs-mintlify", - "version": "2.8.83", + "version": "2.8.84", "private": true, "scripts": { "dev": "mint dev --port ${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}04 --no-open", diff --git a/docs/package.json b/docs/package.json index a966271c5..5513b212b 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/stack-docs", - "version": "2.8.83", + "version": "2.8.84", "repository": "https://github.com/stack-auth/stack-auth", "description": "", "main": "index.js", diff --git a/examples/cjs-test/package.json b/examples/cjs-test/package.json index a2bfa9b2d..b099a0d43 100644 --- a/examples/cjs-test/package.json +++ b/examples/cjs-test/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/example-cjs-test", - "version": "2.8.83", + "version": "2.8.84", "repository": "https://github.com/stack-auth/stack-auth", "private": true, "scripts": { diff --git a/examples/convex/package.json b/examples/convex/package.json index 47b65ab1c..1bb9e7f0c 100644 --- a/examples/convex/package.json +++ b/examples/convex/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/convex-example", - "version": "2.8.83", + "version": "2.8.84", "repository": "https://github.com/stack-auth/stack-auth", "private": true, "scripts": { diff --git a/examples/demo/package.json b/examples/demo/package.json index 39bdf7784..208e8094d 100644 --- a/examples/demo/package.json +++ b/examples/demo/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/example-demo-app", - "version": "2.8.83", + "version": "2.8.84", "repository": "https://github.com/stack-auth/stack-auth", "description": "", "private": true, diff --git a/examples/docs-examples/package.json b/examples/docs-examples/package.json index 4c0dfcb3d..6c7fe9e5b 100644 --- a/examples/docs-examples/package.json +++ b/examples/docs-examples/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/docs-examples", - "version": "2.8.83", + "version": "2.8.84", "repository": "https://github.com/stack-auth/stack-auth", "description": "", "private": true, diff --git a/examples/e-commerce/package.json b/examples/e-commerce/package.json index 0fc4d5371..e5688bd7e 100644 --- a/examples/e-commerce/package.json +++ b/examples/e-commerce/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/e-commerce-demo", - "version": "2.8.83", + "version": "2.8.84", "repository": "https://github.com/stack-auth/stack-auth", "private": true, "scripts": { diff --git a/examples/js-example/package.json b/examples/js-example/package.json index 5795c1756..ab6c9031c 100644 --- a/examples/js-example/package.json +++ b/examples/js-example/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/js-example", - "version": "2.8.83", + "version": "2.8.84", "repository": "https://github.com/stack-auth/stack-auth", "private": true, "description": "", diff --git a/examples/lovable-react-18-example/package.json b/examples/lovable-react-18-example/package.json index fc236bfd6..cc05a10d8 100644 --- a/examples/lovable-react-18-example/package.json +++ b/examples/lovable-react-18-example/package.json @@ -1,7 +1,7 @@ { "name": "@stackframe/lovable-react-18-example", "private": true, - "version": "2.8.83", + "version": "2.8.84", "repository": "https://github.com/stack-auth/stack-auth", "type": "module", "scripts": { diff --git a/examples/middleware/package.json b/examples/middleware/package.json index 3f16c8914..2f9f592ad 100644 --- a/examples/middleware/package.json +++ b/examples/middleware/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/example-middleware-demo", - "version": "2.8.83", + "version": "2.8.84", "repository": "https://github.com/stack-auth/stack-auth", "private": true, "scripts": { diff --git a/examples/react-example/package.json b/examples/react-example/package.json index e6f1832cc..e3d883408 100644 --- a/examples/react-example/package.json +++ b/examples/react-example/package.json @@ -1,7 +1,7 @@ { "name": "react-example", "private": true, - "version": "2.8.83", + "version": "2.8.84", "repository": "https://github.com/stack-auth/stack-auth", "type": "module", "scripts": { diff --git a/examples/supabase/package.json b/examples/supabase/package.json index ff01c1780..03dbcd5a6 100644 --- a/examples/supabase/package.json +++ b/examples/supabase/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/example-supabase", - "version": "2.8.83", + "version": "2.8.84", "repository": "https://github.com/stack-auth/stack-auth", "private": true, "scripts": { diff --git a/packages/dashboard-ui-components/package.json b/packages/dashboard-ui-components/package.json index 77fdb4985..a22b6c129 100644 --- a/packages/dashboard-ui-components/package.json +++ b/packages/dashboard-ui-components/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/dashboard-ui-components", - "version": "2.8.83", + "version": "2.8.84", "repository": "https://github.com/stack-auth/stack-auth", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/init-stack/package.json b/packages/init-stack/package.json index 3018f09b4..ef7595657 100644 --- a/packages/init-stack/package.json +++ b/packages/init-stack/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/init-stack", - "version": "2.8.83", + "version": "2.8.84", "repository": "https://github.com/stack-auth/stack-auth", "description": "The setup wizard for Stack. https://stack-auth.com", "main": "dist/index.mjs", diff --git a/packages/js/package.json b/packages/js/package.json index c431a5dc7..2a472d38f 100644 --- a/packages/js/package.json +++ b/packages/js/package.json @@ -1,7 +1,7 @@ { "//": "THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY, INSTEAD EDIT THE CORRESPONDING FILE IN packages/template (FOR package.json FILES, PLEASE EDIT package-template.json)", "name": "@stackframe/js", - "version": "2.8.83", + "version": "2.8.84", "repository": "https://github.com/stack-auth/stack-auth", "sideEffects": false, "main": "./dist/index.js", diff --git a/packages/react/package.json b/packages/react/package.json index 62aed612e..0523bbdec 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,7 +1,7 @@ { "//": "THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY, INSTEAD EDIT THE CORRESPONDING FILE IN packages/template (FOR package.json FILES, PLEASE EDIT package-template.json)", "name": "@stackframe/react", - "version": "2.8.83", + "version": "2.8.84", "repository": "https://github.com/stack-auth/stack-auth", "sideEffects": false, "main": "./dist/index.js", diff --git a/packages/stack-cli/package.json b/packages/stack-cli/package.json index 3f574e241..482314f40 100644 --- a/packages/stack-cli/package.json +++ b/packages/stack-cli/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/stack-cli", - "version": "2.8.83", + "version": "2.8.84", "repository": "https://github.com/stack-auth/stack-auth", "description": "The CLI for Stack Auth. https://stack-auth.com", "main": "dist/index.js", diff --git a/packages/stack-sc/package.json b/packages/stack-sc/package.json index 38a75dbfe..31cdbe003 100644 --- a/packages/stack-sc/package.json +++ b/packages/stack-sc/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/stack-sc", - "version": "2.8.83", + "version": "2.8.84", "repository": "https://github.com/stack-auth/stack-auth", "exports": { "./force-react-server": { diff --git a/packages/stack-shared/package.json b/packages/stack-shared/package.json index f84bd0ee3..ea80af220 100644 --- a/packages/stack-shared/package.json +++ b/packages/stack-shared/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/stack-shared", - "version": "2.8.83", + "version": "2.8.84", "repository": "https://github.com/stack-auth/stack-auth", "scripts": { "build": "rimraf dist && tsdown", diff --git a/packages/stack-ui/package.json b/packages/stack-ui/package.json index 8011854e5..3817a73a2 100644 --- a/packages/stack-ui/package.json +++ b/packages/stack-ui/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/stack-ui", - "version": "2.8.83", + "version": "2.8.84", "repository": "https://github.com/stack-auth/stack-auth", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/stack/package.json b/packages/stack/package.json index 4f517c581..121f6c084 100644 --- a/packages/stack/package.json +++ b/packages/stack/package.json @@ -1,7 +1,7 @@ { "//": "THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY, INSTEAD EDIT THE CORRESPONDING FILE IN packages/template (FOR package.json FILES, PLEASE EDIT package-template.json)", "name": "@stackframe/stack", - "version": "2.8.83", + "version": "2.8.84", "repository": "https://github.com/stack-auth/stack-auth", "sideEffects": false, "main": "./dist/index.js", diff --git a/packages/template/package-template.json b/packages/template/package-template.json index 5507472c3..89d5a8d2d 100644 --- a/packages/template/package-template.json +++ b/packages/template/package-template.json @@ -11,7 +11,7 @@ "//": "NEXT_LINE_PLATFORM template", "private": true, - "version": "2.8.83", + "version": "2.8.84", "repository": "https://github.com/stack-auth/stack-auth", "sideEffects": false, "main": "./dist/index.js", diff --git a/packages/template/package.json b/packages/template/package.json index 8f3a0e239..2d2c3496b 100644 --- a/packages/template/package.json +++ b/packages/template/package.json @@ -2,7 +2,7 @@ "//": "THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY, INSTEAD EDIT THE CORRESPONDING FILE IN packages/template (FOR package.json FILES, PLEASE EDIT package-template.json)", "name": "@stackframe/template", "private": true, - "version": "2.8.83", + "version": "2.8.84", "repository": "https://github.com/stack-auth/stack-auth", "sideEffects": false, "main": "./dist/index.js", diff --git a/sdks/implementations/swift/package.json b/sdks/implementations/swift/package.json index 2e9365239..4ffc57001 100644 --- a/sdks/implementations/swift/package.json +++ b/sdks/implementations/swift/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/swift-sdk", - "version": "2.8.83", + "version": "2.8.84", "private": true, "description": "Stack Auth Swift SDK", "scripts": { diff --git a/sdks/spec/package.json b/sdks/spec/package.json index 5de403fca..89ba1b780 100644 --- a/sdks/spec/package.json +++ b/sdks/spec/package.json @@ -1,6 +1,6 @@ { "name": "@stackframe/sdk-spec", - "version": "2.8.83", + "version": "2.8.84", "private": true, "description": "Stack Auth SDK specification files", "scripts": {} From c66bdfb5aede479375ac804466dae6377fba91cc Mon Sep 17 00:00:00 2001 From: BilalG1 Date: Tue, 14 Apr 2026 19:38:52 -0700 Subject: [PATCH 5/6] Fix five dashboard UI issues (#1337) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes five independent UI bugs in the dashboard. Each is a narrow, localized fix — no changes to shared table / card primitives. ### 1. Auth methods preview didn't update until save Toggling Email/password, Magic link, or Passkey updated the switch UI but the right-hand sign-in preview kept rendering the pre-save config until "Save changes" was clicked. The preview was reading `project.config` instead of the local pending state. **Fix:** pass the computed local state (`passwordEnabled`, `otpEnabled`, `passkeyEnabled`) into `AuthPage`'s `mockProject.config` so the preview reflects toggles immediately. | Before | After | |---|---| | ![before](https://gist.githubusercontent.com/BilalG1/8fb37def33d42481002f02d500eb6742/raw/b6d4f39f6626b7e922c2b9d8360ac4019f379b76/01-auth-methods-before.gif) | ![after](https://gist.githubusercontent.com/BilalG1/8fb37def33d42481002f02d500eb6742/raw/b6d4f39f6626b7e922c2b9d8360ac4019f379b76/01-auth-methods-after.gif) | --- ### 2. Email-drafts "New Draft" dropdown items stacked on two rows Icon rendered above text in the dropdown because the icon was a child of a non-flex inner wrapper inside `DropdownMenuItem` and phosphor icons default to `display: block`. **Fix:** use `DropdownMenuItem`'s built-in `icon` prop (which absolute-positions the icon) instead of passing it as a child. | Before | After | |---|---| | ![before](https://gist.githubusercontent.com/BilalG1/8fb37def33d42481002f02d500eb6742/raw/b6d4f39f6626b7e922c2b9d8360ac4019f379b76/02-email-drafts-before.png) | ![after](https://gist.githubusercontent.com/BilalG1/8fb37def33d42481002f02d500eb6742/raw/b6d4f39f6626b7e922c2b9d8360ac4019f379b76/02-email-drafts-after.png) | --- ### 3. Project-keys status filter: clicking options did nothing visible `DesignDataTable` renders the toolbar outside the card when `glassmorphic && !insideDesignCard`. The table instance was captured once via `onTableReady`; filter clicks updated the table's internal state (rows actually filtered to "No results") but the toolbar's parent never re-rendered, so checkboxes, chip count, and button label stayed frozen. **Fix:** wrap `InternalApiKeyTable` in `DesignCard` so `useInsideDesignCard()` returns true, `needsOwnCard` becomes false, and the toolbar renders inside the `DataTable` where it re-renders normally. No changes to the shared `DesignDataTable` component. | Before | After | |---|---| | ![before](https://gist.githubusercontent.com/BilalG1/8fb37def33d42481002f02d500eb6742/raw/b6d4f39f6626b7e922c2b9d8360ac4019f379b76/03-project-keys-before.gif) | ![after](https://gist.githubusercontent.com/BilalG1/8fb37def33d42481002f02d500eb6742/raw/b6d4f39f6626b7e922c2b9d8360ac4019f379b76/03-project-keys-after.gif) | --- ### 4. Analytics "Tables" page only listed Events `AVAILABLE_TABLES` was hardcoded to a single entry. **Fix:** registered all 12 ClickHouse views that exist in the `default` schema (events, users, contact_channels, teams, team_member_profiles, team_permissions, team_invitations, email_outboxes, project_permissions, notification_preferences, refresh_tokens, connected_accounts) with sensible default sort columns. Widened `TableId` to `string`. | Before | After | |---|---| | ![before](https://gist.githubusercontent.com/BilalG1/8fb37def33d42481002f02d500eb6742/raw/b6d4f39f6626b7e922c2b9d8360ac4019f379b76/04-analytics-tables-before.png) | ![after](https://gist.githubusercontent.com/BilalG1/8fb37def33d42481002f02d500eb6742/raw/b6d4f39f6626b7e922c2b9d8360ac4019f379b76/04-analytics-tables-after.png) | --- ### 5. Price input `$` prefix overlapped the number on prod The Input composed `h-9 px-3 ... pl-7`. In production's CSS bundle order `.px-3` declared after `.pl-7`, so `padding-left` resolved to 12px — same as the prefix's `left-3` position — making `$` overlap the first digit. The emulator's bundle happened to order them the other way, which is why it only reproduced in prod. Verified with a devtools injection that mimics the prod CSS ordering. **Fix:** change `pl-7` → `!pl-7` in `repeating-input.tsx` so the prefix padding wins regardless of CSS order. | Before (prod CSS ordering) | After (same ordering) | |---|---| | ![before](https://gist.githubusercontent.com/BilalG1/8fb37def33d42481002f02d500eb6742/raw/b6d4f39f6626b7e922c2b9d8360ac4019f379b76/05-price-overlap-before.png) | ![after](https://gist.githubusercontent.com/BilalG1/8fb37def33d42481002f02d500eb6742/raw/b6d4f39f6626b7e922c2b9d8360ac4019f379b76/05-price-overlap-after.png) | --- ## Test plan - [x] `pnpm --filter @stackframe/dashboard typecheck` - [x] `pnpm --filter @stackframe/dashboard lint` - [x] Manual verification of each issue against the local dev dashboard at localhost:8101 - [ ] Reviewer: confirm no visual regressions on other `DesignDataTable` usages (api-key-table is the only one wrapped here) - [ ] Reviewer: confirm analytics queries on added tables work with the signed-in user's permissions ## Summary by CodeRabbit ## Release Notes * **New Features** * Added 12 new analytics tables to the dashboard for enhanced data visibility and tracking. * **Bug Fixes** * Fixed input styling issue with prefix alignment. * **Style** * Improved visual presentation of data tables with enhanced card styling. * Refined dropdown menu icon display for better UI consistency. * Enhanced authentication preview settings to reflect current configuration state. --- .../analytics/tables/page-client.tsx | 68 ++++++++++++++++++- .../[projectId]/auth-methods/page-client.tsx | 3 + .../[projectId]/email-drafts/page-client.tsx | 12 ++-- .../components/data-table/api-key-table.tsx | 19 +++--- .../src/components/repeating-input.tsx | 2 +- 5 files changed, 85 insertions(+), 19 deletions(-) diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/tables/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/tables/page-client.tsx index ddca58c8f..a3dca3d18 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/tables/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/tables/page-client.tsx @@ -49,9 +49,75 @@ const AVAILABLE_TABLES = new Map([ defaultOrderBy: "event_at", defaultOrderDir: "DESC" as const, }], + ["users", { + displayName: "Users", + baseQuery: "SELECT * FROM default.users", + defaultOrderBy: "signed_up_at", + defaultOrderDir: "DESC" as const, + }], + ["contact_channels", { + displayName: "Contact Channels", + baseQuery: "SELECT * FROM default.contact_channels", + defaultOrderBy: "created_at", + defaultOrderDir: "DESC" as const, + }], + ["teams", { + displayName: "Teams", + baseQuery: "SELECT * FROM default.teams", + defaultOrderBy: "created_at", + defaultOrderDir: "DESC" as const, + }], + ["team_member_profiles", { + displayName: "Team Member Profiles", + baseQuery: "SELECT * FROM default.team_member_profiles", + defaultOrderBy: "created_at", + defaultOrderDir: "DESC" as const, + }], + ["team_permissions", { + displayName: "Team Permissions", + baseQuery: "SELECT * FROM default.team_permissions", + defaultOrderBy: "created_at", + defaultOrderDir: "DESC" as const, + }], + ["team_invitations", { + displayName: "Team Invitations", + baseQuery: "SELECT * FROM default.team_invitations", + defaultOrderBy: "created_at", + defaultOrderDir: "DESC" as const, + }], + ["email_outboxes", { + displayName: "Email Outboxes", + baseQuery: "SELECT * FROM default.email_outboxes", + defaultOrderBy: "created_at", + defaultOrderDir: "DESC" as const, + }], + ["project_permissions", { + displayName: "Project Permissions", + baseQuery: "SELECT * FROM default.project_permissions", + defaultOrderBy: "created_at", + defaultOrderDir: "DESC" as const, + }], + ["notification_preferences", { + displayName: "Notification Preferences", + baseQuery: "SELECT * FROM default.notification_preferences", + defaultOrderBy: "user_id", + defaultOrderDir: "DESC" as const, + }], + ["refresh_tokens", { + displayName: "Refresh Tokens", + baseQuery: "SELECT * FROM default.refresh_tokens", + defaultOrderBy: "created_at", + defaultOrderDir: "DESC" as const, + }], + ["connected_accounts", { + displayName: "Connected Accounts", + baseQuery: "SELECT * FROM default.connected_accounts", + defaultOrderBy: "created_at", + defaultOrderDir: "DESC" as const, + }], ]); -type TableId = "events"; +type TableId = string; type SortDir = "ASC" | "DESC"; const PAGE_SIZE = 50; diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/page-client.tsx index 68b0738a2..190cb66cd 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/page-client.tsx @@ -482,6 +482,9 @@ export default function PageClient() { mockProject={{ config: { ...project.config, + credentialEnabled: passwordEnabled, + magicLinkEnabled: otpEnabled, + passkeyEnabled: passkeyEnabled, oauthProviders: enabledProviders .map(([, provider]) => provider) .filter((provider): provider is AdminOAuthProviderConfig => !!provider), diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-drafts/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-drafts/page-client.tsx index 42671d8f7..4aaebe0b2 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-drafts/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-drafts/page-client.tsx @@ -457,12 +457,10 @@ function NewDraftDropdown({ - - + }> Create from scratch - - + }> Create from template @@ -480,12 +478,10 @@ function NewDraftDropdown({ - - + }> Create from scratch - - + }> Create from template diff --git a/apps/dashboard/src/components/data-table/api-key-table.tsx b/apps/dashboard/src/components/data-table/api-key-table.tsx index 9f7c142eb..c7938d9c9 100644 --- a/apps/dashboard/src/components/data-table/api-key-table.tsx +++ b/apps/dashboard/src/components/data-table/api-key-table.tsx @@ -1,6 +1,6 @@ 'use client'; import { InternalApiKey } from '@stackframe/stack'; -import { DesignDataTable } from "@/components/design-components"; +import { DesignCard, DesignDataTable } from "@/components/design-components"; import { ActionCell, ActionDialog, BadgeCell, DataTableColumnHeader, DataTableFacetedFilter, DateCell, SearchToolbarItem, TextCell, standardFilterFn } from "@/components/ui"; import { ColumnDef, Row, Table } from "@tanstack/react-table"; import { useMemo, useState } from "react"; @@ -144,12 +144,13 @@ export function InternalApiKeyTable(props: { apiKeys: InternalApiKey[], showPubl }); }, [props.apiKeys]); - return ; + return + + ; } diff --git a/apps/dashboard/src/components/repeating-input.tsx b/apps/dashboard/src/components/repeating-input.tsx index 6d09b25fc..d6ced0a44 100644 --- a/apps/dashboard/src/components/repeating-input.tsx +++ b/apps/dashboard/src/components/repeating-input.tsx @@ -166,7 +166,7 @@ export function RepeatingInput({ disabled={disabled || readOnly} className={cn( "rounded-r-none border-0 focus-visible:ring-0 focus-visible:ring-offset-0", - prefix && "pl-7", + prefix && "!pl-7", inputClassName )} /> From d21bdb0ea8033c18610285ced474b5746ed051c5 Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Tue, 14 Apr 2026 20:35:20 -0700 Subject: [PATCH 6/6] Skip diagnostics for analytics requests --- .../src/interface/client-interface.ts | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/stack-shared/src/interface/client-interface.ts b/packages/stack-shared/src/interface/client-interface.ts index 727965158..6942da804 100644 --- a/packages/stack-shared/src/interface/client-interface.ts +++ b/packages/stack-shared/src/interface/client-interface.ts @@ -465,6 +465,7 @@ export class StackClientInterface { session: InternalSession | null, requestType: "client" | "server" | "admin" = "client", apiUrlOverride?: string, + retryOptions?: { maxAttempts?: number, skipDiagnostics?: boolean }, ) { session ??= this.createSession({ refreshToken: null, @@ -472,19 +473,20 @@ export class StackClientInterface { if (apiUrlOverride) { return await this._networkRetry( - () => this.sendClientRequestInner(path, requestOptions, session!, requestType, apiUrlOverride), - session, - requestType, - ); - } - - return await this._withFallback(async (apiUrl, retryOptions) => { - return await this._networkRetry( - () => this.sendClientRequestInner(path, requestOptions, session!, requestType, apiUrl), + () => this.sendClientRequestInner(path, requestOptions, session!, requestType, apiUrlOverride, retryOptions), session, requestType, retryOptions, ); + } + + return await this._withFallback(async (apiUrl, fallbackRetryOptions) => { + return await this._networkRetry( + () => this.sendClientRequestInner(path, requestOptions, session!, requestType, apiUrl, retryOptions), + session, + requestType, + { ...fallbackRetryOptions, ...retryOptions }, + ); }); } @@ -513,6 +515,7 @@ export class StackClientInterface { session, "client", this.getAnalyticsApiUrl(), + { maxAttempts: 1, skipDiagnostics: true }, ); return Result.ok(response); } catch (e) { @@ -537,6 +540,7 @@ export class StackClientInterface { session, "client", this.getAnalyticsApiUrl(), + { maxAttempts: 1, skipDiagnostics: true }, ); return Result.ok(response); } catch (e) { @@ -576,6 +580,7 @@ export class StackClientInterface { session: InternalSession, requestType: "client" | "server" | "admin", apiUrlOverride?: string, + innerOptions?: { skipDiagnostics?: boolean }, ): Promise