Merge branch 'dev' into promptless/update-analytics-tables-documentation

This commit is contained in:
promptless[bot] 2026-04-24 19:00:34 +00:00
commit ae7643d41b
2 changed files with 40 additions and 3 deletions

View File

@ -215,6 +215,39 @@ it("should fail if the neon client details are missing", async ({ expect }) => {
`);
});
it("should fail if the neon client authorization header is malformed", async ({ expect }) => {
// This project ID is arbitrary because malformed Basic auth is rejected before any project lookup runs.
const projectId = "73782539-cf39-486b-a9f8-9b2893f79ef2";
const response = await niceBackendFetch(urlString`/api/v1/integrations/neon/projects/transfer?project_id=${projectId}`, {
method: "GET",
headers: {
"Authorization": "Basic",
},
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 400,
"body": {
"code": "SCHEMA_ERROR",
"details": {
"message": deindent\`
Request validation failed on GET /api/v1/integrations/neon/projects/transfer:
- Authorization header must be in the format "Basic <base64>"
\`,
},
"error": deindent\`
Request validation failed on GET /api/v1/integrations/neon/projects/transfer:
- Authorization header must be in the format "Basic <base64>"
\`,
},
"headers": Headers {
"x-stack-known-error": "SCHEMA_ERROR",
<some fields may have been hidden>,
},
}
`);
});
it("should fail to transfer project if the user is not signed in", async ({ expect }) => {
const provisioned = await provisionProject();
const projectId = provisioned.body.project_id;
@ -305,4 +338,3 @@ it("should fail the check if project was already transferred", async ({ expect }
}
`);
});

View File

@ -3,7 +3,7 @@ import { KnownErrors } from "./known-errors";
import { isBase64 } from "./utils/bytes";
import { SUPPORTED_CURRENCIES, type Currency, type MoneyAmount } from "./utils/currency-constants";
import type { DayInterval, Interval } from "./utils/dates";
import { StackAssertionError, throwErr } from "./utils/errors";
import { StackAssertionError } from "./utils/errors";
import { decodeBasicAuthorizationHeader } from "./utils/http";
import { allProviders } from "./utils/oauth";
import { deepPlainClone, omit, typedFromEntries } from "./utils/objects";
@ -878,12 +878,17 @@ export const basicAuthorizationHeaderSchema = yupString().test('is-basic-authori
// Neon integration
export const neonAuthorizationHeaderSchema = basicAuthorizationHeaderSchema.test('is-authorization-header', 'Invalid client_id:client_secret values; did you use the correct values for the integration?', (value) => {
if (!value) return true;
const [clientId, clientSecret] = decodeBasicAuthorizationHeader(value) ?? throwErr(`Authz header invalid? This should've been validated by basicAuthorizationHeaderSchema: ${value}`);
const decoded = decodeBasicAuthorizationHeader(value);
if (decoded === null) return true;
const [clientId, clientSecret] = decoded;
for (const neonClientConfig of JSON.parse(process.env.STACK_INTEGRATION_CLIENTS_CONFIG || '[]')) {
if (clientId === neonClientConfig.client_id && clientSecret === neonClientConfig.client_secret) return true;
}
return false;
});
import.meta.vitest?.test("neonAuthorizationHeaderSchema handles malformed Basic auth as a validation error", async ({ expect }) => {
await expect(neonAuthorizationHeaderSchema.validate("Basic", { abortEarly: false })).rejects.toThrow('Authorization header must be in the format "Basic <base64>"');
});
// Utils
export function yupDefinedWhen<S extends yup.AnyObject>(