Skip to content

Commit 008adce

Browse files
authored
Merge branch 'dev' into partial-refunds-frontend
2 parents 75e6e42 + 2a6b173 commit 008adce

44 files changed

Lines changed: 1293 additions & 218 deletions

File tree

Some content is hidden

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

apps/backend/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@stackframe/stack-backend",
3-
"version": "2.8.60",
3+
"version": "2.8.62",
44
"repository": "https://github.com/stack-auth/stack-auth",
55
"private": true,
66
"type": "module",
@@ -41,7 +41,7 @@
4141
"codegen-docs:watch": "pnpm run with-env tsx watch --exclude '**/node_modules/**' --clear-screen=false scripts/generate-openapi-fumadocs.ts",
4242
"generate-keys": "pnpm run with-env tsx scripts/generate-keys.ts",
4343
"db-seed-script": "pnpm run db:seed",
44-
"verify-data-integrity": "pnpm run with-env:dev tsx scripts/verify-data-integrity.ts",
44+
"verify-data-integrity": "pnpm run with-env:dev tsx scripts/verify-data-integrity/index.ts",
4545
"run-email-queue": "pnpm run with-env:dev tsx scripts/run-email-queue.ts"
4646
},
4747
"prisma": {
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
2+
import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors";
3+
import { deepPlainEquals, filterUndefined } from "@stackframe/stack-shared/dist/utils/objects";
4+
import { deindent } from "@stackframe/stack-shared/dist/utils/strings";
5+
6+
export type EndpointOutput = {
7+
status: number,
8+
responseJson: any,
9+
};
10+
11+
export type OutputData = Record<string, EndpointOutput[]>;
12+
13+
export type ExpectStatusCode = <T = any>(
14+
expectedStatusCode: number,
15+
endpoint: string,
16+
request: RequestInit,
17+
) => Promise<T>;
18+
19+
export function createApiHelpers(options: {
20+
currentOutputData: OutputData,
21+
targetOutputData?: OutputData,
22+
}) {
23+
const { currentOutputData, targetOutputData } = options;
24+
25+
function appendOutputData(endpoint: string, output: EndpointOutput) {
26+
if (!(endpoint in currentOutputData)) {
27+
currentOutputData[endpoint] = [];
28+
}
29+
const newLength = currentOutputData[endpoint].push(output);
30+
if (targetOutputData) {
31+
if (!(endpoint in targetOutputData)) {
32+
throw new StackAssertionError(deindent`
33+
Output data mismatch for endpoint ${endpoint}:
34+
Expected ${endpoint} to be in targetOutputData, but it is not.
35+
`, { endpoint });
36+
}
37+
if (targetOutputData[endpoint].length < newLength) {
38+
throw new StackAssertionError(deindent`
39+
Output data mismatch for endpoint ${endpoint}:
40+
Expected ${targetOutputData[endpoint].length} outputs but got at least ${newLength}.
41+
`, { endpoint });
42+
}
43+
if (!(deepPlainEquals(targetOutputData[endpoint][newLength - 1], output))) {
44+
throw new StackAssertionError(deindent`
45+
Output data mismatch for endpoint ${endpoint}:
46+
Expected output[${JSON.stringify(endpoint)}][${newLength - 1}] to be:
47+
${JSON.stringify(targetOutputData[endpoint][newLength - 1], null, 2)}
48+
but got:
49+
${JSON.stringify(output, null, 2)}.
50+
`, { endpoint });
51+
}
52+
}
53+
}
54+
55+
const expectStatusCode: ExpectStatusCode = async (expectedStatusCode, endpoint, request) => {
56+
const apiUrl = new URL(getEnvVariable("NEXT_PUBLIC_STACK_API_URL"));
57+
const response = await fetch(new URL(endpoint, apiUrl), {
58+
...request,
59+
headers: {
60+
"x-stack-disable-artificial-development-delay": "yes",
61+
"x-stack-development-disable-extended-logging": "yes",
62+
...filterUndefined(request.headers ?? {}),
63+
},
64+
});
65+
66+
const responseText = await response.text();
67+
68+
if (response.status !== expectedStatusCode) {
69+
throw new StackAssertionError(deindent`
70+
Expected status code ${expectedStatusCode} but got ${response.status} for ${endpoint}:
71+
72+
${responseText}
73+
`, { request, response });
74+
}
75+
76+
const responseJson = JSON.parse(responseText);
77+
const currentOutput: EndpointOutput = {
78+
status: response.status,
79+
responseJson,
80+
};
81+
82+
appendOutputData(endpoint, currentOutput);
83+
84+
return responseJson;
85+
};
86+
87+
return {
88+
appendOutputData,
89+
expectStatusCode,
90+
};
91+
}
92+

apps/backend/scripts/verify-data-integrity.ts renamed to apps/backend/scripts/verify-data-integrity/index.ts

Lines changed: 79 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,27 @@
1-
import { globalPrismaClient } from "@/prisma-client";
1+
import { DEFAULT_BRANCH_ID, getSoleTenancyFromProjectBranch } from "@/lib/tenancies";
2+
import { getPrismaClientForTenancy, globalPrismaClient } from "@/prisma-client";
3+
import type { OrganizationRenderedConfig } from "@stackframe/stack-shared/dist/config/schema";
24
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
35
import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors";
4-
import { deepPlainEquals, filterUndefined, omit } from "@stackframe/stack-shared/dist/utils/objects";
6+
import { deepPlainEquals, omit } from "@stackframe/stack-shared/dist/utils/objects";
57
import { wait } from "@stackframe/stack-shared/dist/utils/promises";
68
import { deindent } from "@stackframe/stack-shared/dist/utils/strings";
79
import fs from "fs";
810

11+
import { createApiHelpers, type OutputData } from "./api";
12+
import { createPaymentsVerifier } from "./payments-verifier";
13+
import { createRecurse } from "./recurse";
14+
import { verifyStripePayoutIntegrity } from "./stripe-payout-integrity";
15+
916
const prismaClient = globalPrismaClient;
1017
const OUTPUT_FILE_PATH = "./verify-data-integrity-output.untracked.json";
11-
12-
type EndpointOutput = {
13-
status: number,
14-
responseJson: any,
15-
};
16-
17-
type OutputData = Record<string, EndpointOutput[]>;
18+
const STRIPE_SECRET_KEY = getEnvVariable("STACK_STRIPE_SECRET_KEY", "");
19+
const USE_MOCK_STRIPE_API = STRIPE_SECRET_KEY === "sk_test_mockstripekey";
1820

1921
let targetOutputData: OutputData | undefined = undefined;
2022
const currentOutputData: OutputData = {};
2123

24+
const recurse = createRecurse();
2225

2326
async function main() {
2427
console.log();
@@ -78,7 +81,6 @@ async function main() {
7881
const shouldSkipNeon = flags.includes("--skip-neon");
7982
const recentFirst = flags.includes("--recent-first");
8083

81-
8284
if (shouldSaveOutput) {
8385
console.log(`Will save output to ${OUTPUT_FILE_PATH}`);
8486
}
@@ -91,14 +93,16 @@ async function main() {
9193
throw new Error(`Cannot verify output: ${OUTPUT_FILE_PATH} does not exist`);
9294
}
9395
try {
94-
targetOutputData = JSON.parse(fs.readFileSync(OUTPUT_FILE_PATH, 'utf8'));
96+
targetOutputData = JSON.parse(fs.readFileSync(OUTPUT_FILE_PATH, "utf8"));
9597

9698
// TODO next-release these are hacks for the migration, delete them
9799
if (targetOutputData) {
98100
targetOutputData["/api/v1/internal/projects/current"] = targetOutputData["/api/v1/internal/projects/current"].map(output => {
99101
if ("config" in output.responseJson) {
100102
delete output.responseJson.config.id;
101103
output.responseJson.config.oauth_providers = output.responseJson.config.oauth_providers
104+
// `any` because this is historical output JSON from disk.
105+
// We intentionally keep this "migration hack" untyped.
102106
.filter((provider: any) => provider.enabled)
103107
.map((provider: any) => omit(provider, ["enabled"]));
104108
}
@@ -112,11 +116,17 @@ async function main() {
112116
}
113117
}
114118

119+
const { expectStatusCode } = createApiHelpers({
120+
currentOutputData,
121+
targetOutputData,
122+
});
123+
115124
const projects = await prismaClient.project.findMany({
116125
select: {
117126
id: true,
118127
displayName: true,
119128
description: true,
129+
stripeAccountId: true,
120130
},
121131
orderBy: recentFirst ? {
122132
updatedAt: "desc",
@@ -128,6 +138,9 @@ async function main() {
128138
if (startAt !== 0) {
129139
console.log(`Starting at project ${startAt}.`);
130140
}
141+
if (USE_MOCK_STRIPE_API) {
142+
console.warn("Using mock Stripe server (STACK_STRIPE_SECRET_KEY=sk_test_mockstripekey); skipping Stripe payout integrity checks.");
143+
}
131144

132145
const maxUsersPerProject = 100;
133146

@@ -173,6 +186,32 @@ async function main() {
173186
},
174187
}),
175188
]);
189+
void currentProject;
190+
191+
const tenancy = await getSoleTenancyFromProjectBranch(projectId, DEFAULT_BRANCH_ID, true);
192+
const paymentsConfig = tenancy ? (tenancy.config as OrganizationRenderedConfig).payments : undefined;
193+
const paymentsVerifier = tenancy && paymentsConfig
194+
? await createPaymentsVerifier({
195+
projectId,
196+
tenancyId: tenancy.id,
197+
tenancy,
198+
paymentsConfig,
199+
prisma: await getPrismaClientForTenancy(tenancy),
200+
expectStatusCode,
201+
})
202+
: null;
203+
204+
const stripeAccountId = projects[i].stripeAccountId;
205+
if (!USE_MOCK_STRIPE_API && tenancy && stripeAccountId != null) {
206+
await verifyStripePayoutIntegrity({
207+
projectId,
208+
tenancy,
209+
stripeAccountId,
210+
expectStatusCode,
211+
});
212+
}
213+
214+
const verifiedTeams = new Set<string>();
176215

177216
if (!skipUsers) {
178217
for (let j = 0; j < users.items.length; j++) {
@@ -198,6 +237,8 @@ async function main() {
198237
},
199238
});
200239
for (const projectPermission of projectPermissions.items) {
240+
// `any` because these endpoint response types aren't imported here,
241+
// and this script is intentionally tolerant of response shape changes.
201242
if (!projectPermissionDefinitions.items.some((p: any) => p.id === projectPermission.id)) {
202243
throw new StackAssertionError(deindent`
203244
Project permission ${projectPermission.id} not found in project permission definitions.
@@ -227,16 +268,42 @@ async function main() {
227268
},
228269
});
229270
for (const teamPermission of teamPermissions.items) {
271+
// `any` because these endpoint response types aren't imported here,
272+
// and this script is intentionally tolerant of response shape changes.
230273
if (!teamPermissionDefinitions.items.some((p: any) => p.id === teamPermission.id)) {
231274
throw new StackAssertionError(deindent`
232275
Team permission ${teamPermission.id} not found in team permission definitions.
233276
`);
234277
}
235278
}
236279
});
280+
281+
if (paymentsVerifier && !verifiedTeams.has(team.id)) {
282+
await paymentsVerifier.verifyCustomerPayments({
283+
customerType: "team",
284+
customerId: team.id,
285+
});
286+
verifiedTeams.add(team.id);
287+
}
288+
}
289+
290+
if (paymentsVerifier) {
291+
await paymentsVerifier.verifyCustomerPayments({
292+
customerType: "user",
293+
customerId: user.id,
294+
});
237295
}
238296
});
239297
}
298+
299+
if (paymentsVerifier) {
300+
for (const customCustomerId of paymentsVerifier.customCustomerIds) {
301+
await paymentsVerifier.verifyCustomerPayments({
302+
customerType: "custom",
303+
customerId: customCustomerId,
304+
});
305+
}
306+
}
240307
}
241308
});
242309
}
@@ -267,6 +334,7 @@ async function main() {
267334
console.log();
268335
console.log();
269336
}
337+
270338
// eslint-disable-next-line no-restricted-syntax
271339
main().catch((...args) => {
272340
console.error();
@@ -276,90 +344,3 @@ main().catch((...args) => {
276344
process.exit(1);
277345
});
278346

279-
async function expectStatusCode(expectedStatusCode: number, endpoint: string, request: RequestInit) {
280-
const apiUrl = new URL(getEnvVariable("NEXT_PUBLIC_STACK_API_URL"));
281-
const response = await fetch(new URL(endpoint, apiUrl), {
282-
...request,
283-
headers: {
284-
"x-stack-disable-artificial-development-delay": "yes",
285-
"x-stack-development-disable-extended-logging": "yes",
286-
...filterUndefined(request.headers ?? {}),
287-
},
288-
});
289-
290-
const responseText = await response.text();
291-
292-
if (response.status !== expectedStatusCode) {
293-
throw new StackAssertionError(deindent`
294-
Expected status code ${expectedStatusCode} but got ${response.status} for ${endpoint}:
295-
296-
${responseText}
297-
`, { request, response });
298-
}
299-
300-
const responseJson = JSON.parse(responseText);
301-
const currentOutput: EndpointOutput = {
302-
status: response.status,
303-
responseJson,
304-
};
305-
306-
appendOutputData(endpoint, currentOutput);
307-
308-
return responseJson;
309-
}
310-
311-
function appendOutputData(endpoint: string, output: EndpointOutput) {
312-
if (!(endpoint in currentOutputData)) {
313-
currentOutputData[endpoint] = [];
314-
}
315-
const newLength = currentOutputData[endpoint].push(output);
316-
if (targetOutputData) {
317-
if (!(endpoint in targetOutputData)) {
318-
throw new StackAssertionError(deindent`
319-
Output data mismatch for endpoint ${endpoint}:
320-
Expected ${endpoint} to be in targetOutputData, but it is not.
321-
`, { endpoint });
322-
}
323-
if (targetOutputData[endpoint].length < newLength) {
324-
throw new StackAssertionError(deindent`
325-
Output data mismatch for endpoint ${endpoint}:
326-
Expected ${targetOutputData[endpoint].length} outputs but got at least ${newLength}.
327-
`, { endpoint });
328-
}
329-
if (!(deepPlainEquals(targetOutputData[endpoint][newLength - 1], output))) {
330-
throw new StackAssertionError(deindent`
331-
Output data mismatch for endpoint ${endpoint}:
332-
Expected output[${JSON.stringify(endpoint)}][${newLength - 1}] to be:
333-
${JSON.stringify(targetOutputData[endpoint][newLength - 1], null, 2)}
334-
but got:
335-
${JSON.stringify(output, null, 2)}.
336-
`, { endpoint });
337-
}
338-
}
339-
}
340-
341-
let lastProgress = performance.now() - 9999999999;
342-
343-
type RecurseFunction = (progressPrefix: string, inner: (recurse: RecurseFunction) => Promise<void>) => Promise<void>;
344-
345-
const _recurse = async (progressPrefix: string | ((...args: any[]) => void), inner: Parameters<RecurseFunction>[1]): Promise<void> => {
346-
const progressFunc = typeof progressPrefix === "function" ? progressPrefix : (...args: any[]) => {
347-
console.log(`${progressPrefix}`, ...args);
348-
};
349-
if (performance.now() - lastProgress > 1000) {
350-
progressFunc();
351-
lastProgress = performance.now();
352-
}
353-
try {
354-
return await inner(
355-
(progressPrefix, inner) => _recurse(
356-
(...args) => progressFunc(progressPrefix, ...args),
357-
inner,
358-
),
359-
);
360-
} catch (error) {
361-
progressFunc(`\x1b[41mERROR\x1b[0m!`);
362-
throw error;
363-
}
364-
};
365-
const recurse: RecurseFunction = _recurse;

0 commit comments

Comments
 (0)