Merge branch 'external-db-sync' of https://github.com/stack-auth/stack-auth into external-db-sync

This commit is contained in:
Bilal Godil 2026-02-03 11:35:58 -08:00
commit 61f2b79f46
33 changed files with 107 additions and 119 deletions

View File

@ -159,22 +159,6 @@ jobs:
- name: Wait 10 seconds
run: sleep 10
- name: Prime external DB sync
run: |
set -euo pipefail
set -a
source apps/backend/.env.test.local
set +a
baseUrl="http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}02"
maxDurationMs="${STACK_EXTERNAL_DB_SYNC_MAX_DURATION_MS:-20000}"
for _ in 1 2 3; do
curl -fsS -H "Authorization: Bearer ${CRON_SECRET}" \
"${baseUrl}/api/latest/internal/external-db-sync/sequencer?maxDurationMs=${maxDurationMs}&stopWhenIdle=true" >/dev/null
curl -fsS -H "Authorization: Bearer ${CRON_SECRET}" \
"${baseUrl}/api/latest/internal/external-db-sync/poller?maxDurationMs=${maxDurationMs}&stopWhenIdle=true" >/dev/null
sleep 2
done
- name: Run tests
run: pnpm test run ${{ matrix.freestyle-mode == 'prod' && '--min-workers=1 --max-workers=1' || '' }}

View File

@ -153,21 +153,6 @@ jobs:
- name: Wait 10 seconds
run: sleep 10
- name: Prime external DB sync
run: |
set -euo pipefail
set -a
source apps/backend/.env.test.local
set +a
baseUrl="http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}02"
for _ in 1 2 3; do
curl -fsS -H "Authorization: Bearer ${CRON_SECRET}" \
"${baseUrl}/api/latest/internal/external-db-sync/sequencer" >/dev/null
curl -fsS -H "Authorization: Bearer ${CRON_SECRET}" \
"${baseUrl}/api/latest/internal/external-db-sync/poller" >/dev/null
sleep 2
done
- name: Run tests
run: pnpm test run

View File

@ -160,21 +160,6 @@ jobs:
- name: Wait 10 seconds
run: sleep 10
- name: Prime external DB sync
run: |
set -euo pipefail
set -a
source apps/backend/.env.test.local
set +a
baseUrl="http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}02"
for _ in 1 2 3; do
curl -fsS -H "Authorization: Bearer ${CRON_SECRET}" \
"${baseUrl}/api/latest/internal/external-db-sync/sequencer" >/dev/null
curl -fsS -H "Authorization: Bearer ${CRON_SECRET}" \
"${baseUrl}/api/latest/internal/external-db-sync/poller" >/dev/null
sleep 2
done
- name: Run tests
run: pnpm test run

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/stack-backend",
"version": "2.8.63",
"version": "2.8.64",
"repository": "https://github.com/stack-auth/stack-auth",
"private": true,
"type": "module",

View File

@ -79,6 +79,10 @@ async function main() {
const shouldSkipNeon = flags.includes("--skip-neon");
const recentFirst = flags.includes("--recent-first");
const noBail = flags.includes("--no-bail");
const maxUsersPerProjectFlag = flags.find(f => f.startsWith("--max-users-per-project="));
const maxUsersPerProject = maxUsersPerProjectFlag
? parseInt(maxUsersPerProjectFlag.split("=")[1], 10)
: Infinity;
const { recurse, collectedErrors } = createRecurse({ noBail });
@ -147,7 +151,9 @@ async function main() {
console.warn("Using mock Stripe server (STACK_STRIPE_SECRET_KEY=sk_test_mockstripekey); skipping Stripe payout integrity checks.");
}
const maxUsersPerProject = 100;
if (maxUsersPerProject !== Infinity) {
console.log(`Will check at most ${maxUsersPerProject} users per project.`);
}
const endAt = Math.min(startAt + count, projects.length);
for (let i = startAt; i < endAt; i++) {
@ -157,7 +163,7 @@ async function main() {
return;
}
const [currentProject, users, projectPermissionDefinitions, teamPermissionDefinitions] = await Promise.all([
const [currentProject, projectPermissionDefinitions, teamPermissionDefinitions] = await Promise.all([
expectStatusCode(200, `/api/v1/internal/projects/current`, {
method: "GET",
headers: {
@ -166,14 +172,6 @@ async function main() {
"x-stack-development-override-key": getEnvVariable("STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY"),
},
}),
expectStatusCode(200, `/api/v1/users?limit=${maxUsersPerProject}`, {
method: "GET",
headers: {
"x-stack-project-id": projectId,
"x-stack-access-type": "admin",
"x-stack-development-override-key": getEnvVariable("STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY"),
},
}),
expectStatusCode(200, `/api/v1/project-permission-definitions`, {
method: "GET",
headers: {
@ -193,6 +191,30 @@ async function main() {
]);
void currentProject;
// Fetch users with pagination
const PAGE_LIMIT = 1000;
const allUsers: any[] = [];
let cursor: string | undefined = undefined;
while (allUsers.length < maxUsersPerProject) {
const remainingToFetch = maxUsersPerProject - allUsers.length;
const limit = Math.min(PAGE_LIMIT, remainingToFetch);
const cursorParam: string = cursor ? `&cursor=${encodeURIComponent(cursor)}` : "";
const usersPage = await expectStatusCode(200, `/api/v1/users?limit=${limit}${cursorParam}`, {
method: "GET",
headers: {
"x-stack-project-id": projectId,
"x-stack-access-type": "admin",
"x-stack-development-override-key": getEnvVariable("STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY"),
},
});
allUsers.push(...usersPage.items);
if (!usersPage.pagination?.next_cursor) {
break;
}
cursor = usersPage.pagination.next_cursor;
}
const users = { items: allUsers.slice(0, maxUsersPerProject) };
const tenancy = await getSoleTenancyFromProjectBranch(projectId, DEFAULT_BRANCH_ID, true);
const paymentsConfig = tenancy ? (tenancy.config as OrganizationRenderedConfig).payments : undefined;
const paymentsVerifier = tenancy && paymentsConfig

View File

@ -1,4 +1,5 @@
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
import { traceSpan } from "@/utils/telemetry";
import { yupNumber, yupObject, yupString, yupTuple } from "@stackframe/stack-shared/dist/schema-fields";
import { generateSecureRandomString } from "@stackframe/stack-shared/dist/utils/crypto";
import { getEnvVariable, getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env";
@ -62,29 +63,31 @@ const fetchFromResend = async (): Promise<{ data: ResendEmail[] }> => {
};
const performSignUp = async (email: string, password: string) => {
const apiBaseUrl = getEnvVariable("NEXT_PUBLIC_STACK_API_URL");
const response = await fetch(`${apiBaseUrl}/api/v1/auth/password/sign-up`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Stack-Access-Type": "client",
"X-Stack-Publishable-Client-Key": getEnvVariable("STACK_EMAIL_MONITOR_PUBLISHABLE_CLIENT_KEY"),
"X-Stack-Project-Id": getEnvVariable("STACK_EMAIL_MONITOR_PROJECT_ID"),
},
body: JSON.stringify({
email,
password,
verification_callback_url: getEnvVariable("STACK_EMAIL_MONITOR_VERIFICATION_CALLBACK_URL"),
}),
});
const responseBody = await response.text();
if (!response.ok) {
throw new StackAssertionError(`Sign-up failed: ${response.status} - ${responseBody}`, {
responseBody,
await traceSpan("performing sign-up", async () => {
const apiBaseUrl = getEnvVariable("NEXT_PUBLIC_STACK_API_URL");
const response = await fetch(`${apiBaseUrl}/api/v1/auth/password/sign-up`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Stack-Access-Type": "client",
"X-Stack-Publishable-Client-Key": getEnvVariable("STACK_EMAIL_MONITOR_PUBLISHABLE_CLIENT_KEY"),
"X-Stack-Project-Id": getEnvVariable("STACK_EMAIL_MONITOR_PROJECT_ID"),
},
body: JSON.stringify({
email,
password,
verification_callback_url: getEnvVariable("STACK_EMAIL_MONITOR_VERIFICATION_CALLBACK_URL"),
}),
});
}
const responseBody = await response.text();
if (!response.ok) {
throw new StackAssertionError(`Sign-up failed: ${response.status} - ${responseBody}`, {
responseBody,
});
}
});
};
const isExpectedVerificationEmail = (email: ResendEmail, testEmail: string): boolean => {
@ -99,25 +102,34 @@ const isExpectedVerificationEmail = (email: ResendEmail, testEmail: string): boo
};
const waitForVerificationEmail = async (testEmail: string, useInbucket: boolean) => {
const MAX_POLL_ATTEMPTS = 24;
const POLL_INTERVAL_MS = 5000;
await traceSpan("waiting for verification email", async () => {
const MAX_POLL_ATTEMPTS = 24;
const POLL_INTERVAL_MS = 5000;
for (let attempt = 1; attempt <= MAX_POLL_ATTEMPTS; attempt++) {
await wait(POLL_INTERVAL_MS);
for (let attempt = 1; attempt <= MAX_POLL_ATTEMPTS; attempt++) {
const done = await traceSpan(`waiting for verification email - attempt ${attempt}`, async () => {
await wait(POLL_INTERVAL_MS);
const listData = useInbucket
? await fetchFromInbucket(testEmail)
: await fetchFromResend();
const listData = useInbucket
? await fetchFromInbucket(testEmail)
: await fetchFromResend();
const emails = listData.data;
const verificationEmail = emails.find((email) => isExpectedVerificationEmail(email, testEmail));
const emails = listData.data;
const verificationEmail = emails.find((email) => isExpectedVerificationEmail(email, testEmail));
if (verificationEmail) {
return;
if (verificationEmail) {
return true;
}
return false;
});
if (done) {
return;
}
}
}
throw new StackAssertionError(`Couldn't find verification email in time limit`, { recipient_email: testEmail, max_poll_attempts: MAX_POLL_ATTEMPTS, poll_interval_ms: POLL_INTERVAL_MS });
throw new StackAssertionError(`Couldn't find verification email in time limit`, { recipient_email: testEmail, max_poll_attempts: MAX_POLL_ATTEMPTS, poll_interval_ms: POLL_INTERVAL_MS });
});
};
export const POST = createSmartRouteHandler({

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/stack-dashboard",
"version": "2.8.63",
"version": "2.8.64",
"repository": "https://github.com/stack-auth/stack-auth",
"private": true,
"scripts": {

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/dev-launchpad",
"version": "2.8.63",
"version": "2.8.64",
"repository": "https://github.com/stack-auth/stack-auth",
"private": true,
"scripts": {

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/e2e-tests",
"version": "2.8.63",
"version": "2.8.64",
"repository": "https://github.com/stack-auth/stack-auth",
"private": true,
"type": "module",

View File

@ -914,7 +914,7 @@ $$;`);
expect(res.rows.length).toBe(1);
expect(res.rows[0].display_name).toBe('Final Name');
expect(res.rows[0].id).toBe(newId);
}, TEST_TIMEOUT);
}, COMPLEX_SEQUENCE_TIMEOUT);
/**
* What it does:

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/mock-oauth-server",
"version": "2.8.63",
"version": "2.8.64",
"repository": "https://github.com/stack-auth/stack-auth",
"private": true,
"main": "index.js",

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/stack-docs",
"version": "2.8.63",
"version": "2.8.64",
"repository": "https://github.com/stack-auth/stack-auth",
"description": "",
"main": "index.js",

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/example-cjs-test",
"version": "2.8.63",
"version": "2.8.64",
"repository": "https://github.com/stack-auth/stack-auth",
"private": true,
"scripts": {

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/convex-example",
"version": "2.8.63",
"version": "2.8.64",
"repository": "https://github.com/stack-auth/stack-auth",
"private": true,
"scripts": {

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/example-demo-app",
"version": "2.8.63",
"version": "2.8.64",
"repository": "https://github.com/stack-auth/stack-auth",
"description": "",
"private": true,

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/docs-examples",
"version": "2.8.63",
"version": "2.8.64",
"repository": "https://github.com/stack-auth/stack-auth",
"description": "",
"private": true,

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/e-commerce-demo",
"version": "2.8.63",
"version": "2.8.64",
"repository": "https://github.com/stack-auth/stack-auth",
"private": true,
"scripts": {

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/js-example",
"version": "2.8.63",
"version": "2.8.64",
"repository": "https://github.com/stack-auth/stack-auth",
"private": true,
"description": "",

View File

@ -1,7 +1,7 @@
{
"name": "@stackframe/lovable-react-18-example",
"private": true,
"version": "2.8.63",
"version": "2.8.64",
"repository": "https://github.com/stack-auth/stack-auth",
"type": "module",
"scripts": {

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/example-middleware-demo",
"version": "2.8.63",
"version": "2.8.64",
"repository": "https://github.com/stack-auth/stack-auth",
"private": true,
"scripts": {

View File

@ -1,7 +1,7 @@
{
"name": "react-example",
"private": true,
"version": "2.8.63",
"version": "2.8.64",
"repository": "https://github.com/stack-auth/stack-auth",
"type": "module",
"scripts": {

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/example-supabase",
"version": "2.8.63",
"version": "2.8.64",
"repository": "https://github.com/stack-auth/stack-auth",
"private": true,
"scripts": {

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/init-stack",
"version": "2.8.63",
"version": "2.8.64",
"repository": "https://github.com/stack-auth/stack-auth",
"description": "The setup wizard for Stack. https://stack-auth.com",
"main": "dist/index.js",

View File

@ -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.63",
"version": "2.8.64",
"repository": "https://github.com/stack-auth/stack-auth",
"sideEffects": false,
"main": "./dist/index.js",

View File

@ -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.63",
"version": "2.8.64",
"repository": "https://github.com/stack-auth/stack-auth",
"sideEffects": false,
"main": "./dist/index.js",

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/stack-sc",
"version": "2.8.63",
"version": "2.8.64",
"repository": "https://github.com/stack-auth/stack-auth",
"exports": {
"./force-react-server": {

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/stack-shared",
"version": "2.8.63",
"version": "2.8.64",
"repository": "https://github.com/stack-auth/stack-auth",
"scripts": {
"build": "rimraf dist && tsup-node",

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/stack-ui",
"version": "2.8.63",
"version": "2.8.64",
"repository": "https://github.com/stack-auth/stack-auth",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",

View File

@ -1,7 +1,7 @@
{
"//": "THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY, INSTEAD EDIT THE CORRESPONDING FILE IN packages/template (FOR package.json FILES, PLEASE EDIT package-template.json)",
"name": "@stackframe/stack",
"version": "2.8.63",
"version": "2.8.64",
"repository": "https://github.com/stack-auth/stack-auth",
"sideEffects": false,
"main": "./dist/index.js",

View File

@ -11,7 +11,7 @@
"//": "NEXT_LINE_PLATFORM template",
"private": true,
"version": "2.8.63",
"version": "2.8.64",
"repository": "https://github.com/stack-auth/stack-auth",
"sideEffects": false,
"main": "./dist/index.js",

View File

@ -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.63",
"version": "2.8.64",
"repository": "https://github.com/stack-auth/stack-auth",
"sideEffects": false,
"main": "./dist/index.js",

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/swift-sdk",
"version": "2.8.63",
"version": "2.8.64",
"private": true,
"description": "Stack Auth Swift SDK",
"scripts": {

View File

@ -1,6 +1,6 @@
{
"name": "@stackframe/sdk-spec",
"version": "2.8.63",
"version": "2.8.64",
"private": true,
"description": "Stack Auth SDK specification files",
"scripts": {}