stack/apps/backend/scripts/verify-data-integrity/api.ts
BilalG1 e439bd0b7e
verify payment transactions integrity (#1128)
<!--

Make sure you've read the CONTRIBUTING.md guidelines:
https://github.com/stack-auth/stack-auth/blob/dev/CONTRIBUTING.md

-->


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added a comprehensive payments data-integrity verifier, Stripe payout
reconciliation, API validation helpers, and a throttled progress utility
for long-running checks.

* **Bug Fixes**
* Improved subscription/product filtering to correctly respect customer
type during verification.

* **Chores**
* Reorganized verification scripts and updated the verification
entrypoint invocation.

* **Tests**
* Enhanced test fixtures to include full product data for subscriptions.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-27 21:17:43 +00:00

93 lines
3.0 KiB
TypeScript

import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors";
import { deepPlainEquals, filterUndefined } from "@stackframe/stack-shared/dist/utils/objects";
import { deindent } from "@stackframe/stack-shared/dist/utils/strings";
export type EndpointOutput = {
status: number,
responseJson: any,
};
export type OutputData = Record<string, EndpointOutput[]>;
export type ExpectStatusCode = <T = any>(
expectedStatusCode: number,
endpoint: string,
request: RequestInit,
) => Promise<T>;
export function createApiHelpers(options: {
currentOutputData: OutputData,
targetOutputData?: OutputData,
}) {
const { currentOutputData, targetOutputData } = options;
function appendOutputData(endpoint: string, output: EndpointOutput) {
if (!(endpoint in currentOutputData)) {
currentOutputData[endpoint] = [];
}
const newLength = currentOutputData[endpoint].push(output);
if (targetOutputData) {
if (!(endpoint in targetOutputData)) {
throw new StackAssertionError(deindent`
Output data mismatch for endpoint ${endpoint}:
Expected ${endpoint} to be in targetOutputData, but it is not.
`, { endpoint });
}
if (targetOutputData[endpoint].length < newLength) {
throw new StackAssertionError(deindent`
Output data mismatch for endpoint ${endpoint}:
Expected ${targetOutputData[endpoint].length} outputs but got at least ${newLength}.
`, { endpoint });
}
if (!(deepPlainEquals(targetOutputData[endpoint][newLength - 1], output))) {
throw new StackAssertionError(deindent`
Output data mismatch for endpoint ${endpoint}:
Expected output[${JSON.stringify(endpoint)}][${newLength - 1}] to be:
${JSON.stringify(targetOutputData[endpoint][newLength - 1], null, 2)}
but got:
${JSON.stringify(output, null, 2)}.
`, { endpoint });
}
}
}
const expectStatusCode: ExpectStatusCode = async (expectedStatusCode, endpoint, request) => {
const apiUrl = new URL(getEnvVariable("NEXT_PUBLIC_STACK_API_URL"));
const response = await fetch(new URL(endpoint, apiUrl), {
...request,
headers: {
"x-stack-disable-artificial-development-delay": "yes",
"x-stack-development-disable-extended-logging": "yes",
...filterUndefined(request.headers ?? {}),
},
});
const responseText = await response.text();
if (response.status !== expectedStatusCode) {
throw new StackAssertionError(deindent`
Expected status code ${expectedStatusCode} but got ${response.status} for ${endpoint}:
${responseText}
`, { request, response });
}
const responseJson = JSON.parse(responseText);
const currentOutput: EndpointOutput = {
status: response.status,
responseJson,
};
appendOutputData(endpoint, currentOutput);
return responseJson;
};
return {
appendOutputData,
expectStatusCode,
};
}