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" ;
24import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env" ;
35import { 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" ;
57import { wait } from "@stackframe/stack-shared/dist/utils/promises" ;
68import { deindent } from "@stackframe/stack-shared/dist/utils/strings" ;
79import 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+
916const prismaClient = globalPrismaClient ;
1017const 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
1921let targetOutputData : OutputData | undefined = undefined ;
2022const currentOutputData : OutputData = { } ;
2123
24+ const recurse = createRecurse ( ) ;
2225
2326async 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
271339main ( ) . 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