Skip to content

Commit 8b299b4

Browse files
committed
Merge branch 'dev' into chore/move-mcp-to-a-sep-app
2 parents 833286c + 2e41fde commit 8b299b4

126 files changed

Lines changed: 6151 additions & 1123 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/.env

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ STACK_SEED_INTERNAL_PROJECT_USER_EMAIL=# default user added to the dashboard
1414
STACK_SEED_INTERNAL_PROJECT_USER_PASSWORD=# default user's password, paired with STACK_SEED_INTERNAL_PROJECT_USER_EMAIL
1515
STACK_SEED_INTERNAL_PROJECT_USER_INTERNAL_ACCESS=# if the default user has access to the internal dashboard project
1616
STACK_SEED_INTERNAL_PROJECT_USER_GITHUB_ID=# add github oauth id to the default user
17-
STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY=# default publishable client key for the internal project
18-
STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY=# default secret server key for the internal project
17+
STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY=# default publishable client key for the internal project
18+
STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY=# default secret server key for the internal project
1919
STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY=# default super secret admin key for the internal project
2020

2121
# OAuth mock provider settings

apps/backend/.env.development

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ STACK_SEED_INTERNAL_PROJECT_ALLOW_LOCALHOST=true
1313
STACK_SEED_INTERNAL_PROJECT_OAUTH_PROVIDERS=github,spotify,google,microsoft
1414
STACK_SEED_INTERNAL_PROJECT_USER_GITHUB_ID=admin@example.com
1515
STACK_SEED_INTERNAL_PROJECT_USER_INTERNAL_ACCESS=true
16-
STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
17-
STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY=this-secret-server-key-is-for-local-development-only
16+
STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
17+
STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY=this-secret-server-key-is-for-local-development-only
1818
STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY=this-super-secret-admin-key-is-for-local-development-only
1919

2020
STACK_OAUTH_MOCK_URL=http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}14

apps/backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@stackframe/backend",
3-
"version": "2.8.86",
3+
"version": "2.8.87",
44
"repository": "https://github.com/stack-auth/stack-auth",
55
"private": true,
66
"type": "module",

apps/backend/prisma/seed.ts

Lines changed: 79 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/* eslint-disable no-restricted-syntax */
22
import { usersCrudHandlers } from '@/app/api/latest/users/crud';
3+
import { CustomerType, Prisma, PurchaseCreationSource, SubscriptionStatus } from '@/generated/prisma/client';
34
import { overrideBranchConfigOverride } from '@/lib/config';
45
import {
56
LOCAL_EMULATOR_ADMIN_EMAIL,
@@ -15,9 +16,12 @@ import { DEFAULT_BRANCH_ID, getSoleTenancyFromProjectBranch } from '@/lib/tenanc
1516
import { getPrismaClientForTenancy, globalPrismaClient } from '@/prisma-client';
1617
import { ALL_APPS } from '@stackframe/stack-shared/dist/apps/apps-config';
1718
import { ITEM_IDS, PLAN_LIMITS } from '@stackframe/stack-shared/dist/plans';
19+
import { DayInterval } from '@stackframe/stack-shared/dist/utils/dates';
1820
import { throwErr } from '@stackframe/stack-shared/dist/utils/errors';
1921
import { typedEntries, typedFromEntries } from '@stackframe/stack-shared/dist/utils/objects';
2022

23+
const MONTHLY_REPEAT: DayInterval = [1, "month"];
24+
2125
const DUMMY_PROJECT_ID = '6fbbf22e-f4b2-4c6e-95a1-beab6fa41063';
2226

2327
let didEnableSeedLogTimestamps = false;
@@ -159,9 +163,10 @@ export async function seed() {
159163
includedItems: {
160164
[ITEM_IDS.seats]: { quantity: PLAN_LIMITS.free.seats, repeat: "never" as const, expires: "when-purchase-expires" as const },
161165
[ITEM_IDS.authUsers]: { quantity: PLAN_LIMITS.free.authUsers, repeat: "never" as const, expires: "when-purchase-expires" as const },
162-
[ITEM_IDS.emailsPerMonth]: { quantity: PLAN_LIMITS.free.emailsPerMonth, repeat: "never" as const, expires: "when-purchase-expires" as const },
166+
[ITEM_IDS.emailsPerMonth]: { quantity: PLAN_LIMITS.free.emailsPerMonth, repeat: MONTHLY_REPEAT, expires: "when-repeated" as const },
163167
[ITEM_IDS.analyticsTimeoutSeconds]: { quantity: PLAN_LIMITS.free.analyticsTimeoutSeconds, repeat: "never" as const, expires: "when-purchase-expires" as const },
164-
[ITEM_IDS.analyticsEvents]: { quantity: PLAN_LIMITS.free.analyticsEvents, repeat: "never" as const, expires: "when-purchase-expires" as const },
168+
[ITEM_IDS.analyticsEvents]: { quantity: PLAN_LIMITS.free.analyticsEvents, repeat: MONTHLY_REPEAT, expires: "when-repeated" as const },
169+
[ITEM_IDS.sessionReplays]: { quantity: PLAN_LIMITS.free.sessionReplays, repeat: MONTHLY_REPEAT, expires: "when-repeated" as const },
165170
},
166171
},
167172
team: {
@@ -173,16 +178,18 @@ export async function seed() {
173178
prices: {
174179
monthly: {
175180
USD: "49",
176-
interval: [1, "month"] as any,
181+
interval: MONTHLY_REPEAT,
177182
serverOnly: false,
178183
},
179184
},
180185
includedItems: {
181186
[ITEM_IDS.seats]: { quantity: PLAN_LIMITS.team.seats, repeat: "never" as const, expires: "when-purchase-expires" as const },
182187
[ITEM_IDS.authUsers]: { quantity: PLAN_LIMITS.team.authUsers, repeat: "never" as const, expires: "when-purchase-expires" as const },
183-
[ITEM_IDS.emailsPerMonth]: { quantity: PLAN_LIMITS.team.emailsPerMonth, repeat: "never" as const, expires: "when-purchase-expires" as const },
188+
[ITEM_IDS.emailsPerMonth]: { quantity: PLAN_LIMITS.team.emailsPerMonth, repeat: MONTHLY_REPEAT, expires: "when-repeated" as const },
184189
[ITEM_IDS.analyticsTimeoutSeconds]: { quantity: PLAN_LIMITS.team.analyticsTimeoutSeconds, repeat: "never" as const, expires: "when-purchase-expires" as const },
185-
[ITEM_IDS.analyticsEvents]: { quantity: PLAN_LIMITS.team.analyticsEvents, repeat: "never" as const, expires: "when-purchase-expires" as const },
190+
[ITEM_IDS.analyticsEvents]: { quantity: PLAN_LIMITS.team.analyticsEvents, repeat: MONTHLY_REPEAT, expires: "when-repeated" as const },
191+
[ITEM_IDS.sessionReplays]: { quantity: PLAN_LIMITS.team.sessionReplays, repeat: MONTHLY_REPEAT, expires: "when-repeated" as const },
192+
[ITEM_IDS.onboardingCall]: { quantity: 1, repeat: "never" as const, expires: "when-purchase-expires" as const },
186193
},
187194
},
188195
growth: {
@@ -194,16 +201,18 @@ export async function seed() {
194201
prices: {
195202
monthly: {
196203
USD: "299",
197-
interval: [1, "month"] as any,
204+
interval: MONTHLY_REPEAT,
198205
serverOnly: false,
199206
},
200207
},
201208
includedItems: {
202209
[ITEM_IDS.seats]: { quantity: PLAN_LIMITS.growth.seats, repeat: "never" as const, expires: "when-purchase-expires" as const },
203210
[ITEM_IDS.authUsers]: { quantity: PLAN_LIMITS.growth.authUsers, repeat: "never" as const, expires: "when-purchase-expires" as const },
204-
[ITEM_IDS.emailsPerMonth]: { quantity: PLAN_LIMITS.growth.emailsPerMonth, repeat: "never" as const, expires: "when-purchase-expires" as const },
211+
[ITEM_IDS.emailsPerMonth]: { quantity: PLAN_LIMITS.growth.emailsPerMonth, repeat: MONTHLY_REPEAT, expires: "when-repeated" as const },
205212
[ITEM_IDS.analyticsTimeoutSeconds]: { quantity: PLAN_LIMITS.growth.analyticsTimeoutSeconds, repeat: "never" as const, expires: "when-purchase-expires" as const },
206-
[ITEM_IDS.analyticsEvents]: { quantity: PLAN_LIMITS.growth.analyticsEvents, repeat: "never" as const, expires: "when-purchase-expires" as const },
213+
[ITEM_IDS.analyticsEvents]: { quantity: PLAN_LIMITS.growth.analyticsEvents, repeat: MONTHLY_REPEAT, expires: "when-repeated" as const },
214+
[ITEM_IDS.sessionReplays]: { quantity: PLAN_LIMITS.growth.sessionReplays, repeat: MONTHLY_REPEAT, expires: "when-repeated" as const },
215+
[ITEM_IDS.onboardingCall]: { quantity: 1, repeat: "never" as const, expires: "when-purchase-expires" as const },
207216
},
208217
},
209218
"extra-seats": {
@@ -215,7 +224,7 @@ export async function seed() {
215224
prices: {
216225
monthly: {
217226
USD: "29",
218-
interval: [1, "month"] as any,
227+
interval: MONTHLY_REPEAT,
219228
serverOnly: false,
220229
},
221230
},
@@ -234,6 +243,8 @@ export async function seed() {
234243
[ITEM_IDS.emailsPerMonth]: { displayName: "Emails per Month", customerType: "team" as const },
235244
[ITEM_IDS.analyticsTimeoutSeconds]: { displayName: "Analytics Timeout (seconds)", customerType: "team" as const },
236245
[ITEM_IDS.analyticsEvents]: { displayName: "Analytics Events", customerType: "team" as const },
246+
[ITEM_IDS.sessionReplays]: { displayName: "Session Replays", customerType: "team" as const },
247+
[ITEM_IDS.onboardingCall]: { displayName: "Onboarding Call", customerType: "team" as const },
237248
},
238249
},
239250
apps: {
@@ -292,23 +303,77 @@ export async function seed() {
292303
console.log('Internal team created');
293304
}
294305

306+
// The team-create CRUD path auto-grants the free plan to every team in the
307+
// internal project, but the internal team itself is written directly above
308+
// (bypassing that code path), so it would otherwise end up with zero
309+
// entitlements and trip the plan-limit enforcement. Grant it the Growth plan
310+
// so Stack Auth employees using the dashboard get full quotas. Idempotent —
311+
// skipped if an active Growth subscription already exists.
312+
//
313+
// We create the subscription with raw Prisma (matching seed-dummy-data.ts)
314+
// rather than grantProductToCustomer because bulldozer storage tables
315+
// aren't initialized at this point in the seed yet. The Bulldozer init
316+
// call right below this block ingresses the row into the ledger.
317+
const growthProduct = updatedInternalTenancy.config.payments.products.growth;
318+
if (growthProduct.customerType === 'team') {
319+
const existingGrowthSub = await internalPrisma.subscription.findFirst({
320+
where: {
321+
tenancyId: internalTenancy.id,
322+
customerId: internalTeamId,
323+
customerType: CustomerType.TEAM,
324+
productId: 'growth',
325+
status: SubscriptionStatus.active,
326+
},
327+
});
328+
if (!existingGrowthSub) {
329+
const growthPrices = growthProduct.prices === 'include-by-default' ? {} : growthProduct.prices;
330+
const firstPriceId = Object.keys(growthPrices)[0] ?? null;
331+
const now = new Date();
332+
// Clone to ensure the stored JSON snapshot is independent of the config object
333+
// (mirrors the pattern used in seed-dummy-data.ts).
334+
const storedProduct = JSON.parse(JSON.stringify(growthProduct)) as Prisma.InputJsonValue;
335+
// Mirror what a real Stripe checkout would produce, based on whether
336+
// the internal project is running in test mode.
337+
const creationSource = updatedInternalTenancy.config.payments.testMode
338+
? PurchaseCreationSource.TEST_MODE
339+
: PurchaseCreationSource.PURCHASE_PAGE;
340+
await internalPrisma.subscription.create({
341+
data: {
342+
tenancyId: internalTenancy.id,
343+
customerId: internalTeamId,
344+
customerType: CustomerType.TEAM,
345+
status: SubscriptionStatus.active,
346+
productId: 'growth',
347+
priceId: firstPriceId,
348+
product: storedProduct,
349+
quantity: 1,
350+
currentPeriodStart: now,
351+
currentPeriodEnd: new Date('2099-12-31T23:59:59Z'),
352+
cancelAtPeriodEnd: false,
353+
creationSource,
354+
},
355+
});
356+
console.log('Granted Growth plan to internal team');
357+
}
358+
}
359+
295360
// Upsert the internal API key set before any flake-prone work (dummy-project
296361
// seed, email/svix, clickhouse). The emulator CLI authenticates against the
297362
// internal project using the pck stored here, so it must land before the rest
298363
// of the seed even if something later fails.
299364
const isLocalEmulator = process.env.NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR === 'true';
300-
const rawPck = process.env.STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY;
365+
const rawPck = process.env.STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY;
301366
if (isLocalEmulator && !rawPck) {
302367
// Emulator images build before a per-VM pck is available. Runtime boots set
303-
// STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY from the VM-generated
368+
// STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY from the VM-generated
304369
// random value and re-run the seed, which upserts the internal key set then.
305370
console.log('Skipping internal API key set (no pck provided; emulator mode).');
306371
} else {
307372
const keySet = {
308-
publishableClientKey: rawPck || throwErr('STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY is not set'),
373+
publishableClientKey: rawPck || throwErr('STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY is not set'),
309374
secretServerKey: isLocalEmulator
310-
? (process.env.STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY ?? null)
311-
: (process.env.STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY || throwErr('STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY is not set')),
375+
? (process.env.STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY ?? null)
376+
: (process.env.STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY || throwErr('STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY is not set')),
312377
superSecretAdminKey: isLocalEmulator
313378
? (process.env.STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY ?? null)
314379
: (process.env.STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY || throwErr('STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY is not set')),

apps/backend/src/app/api/latest/analytics/events/batch/route.tsx

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
import { getClickhouseAdminClient } from "@/lib/clickhouse";
2+
import { getBillingTeamId } from "@/lib/plan-entitlements";
23
import { findRecentSessionReplay } from "@/lib/session-replays";
4+
import { getStackServerApp } from "@/stack";
35
import { getPrismaClientForTenancy } from "@/prisma-client";
46
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
57
import { KnownErrors } from "@stackframe/stack-shared";
8+
import { ITEM_IDS } from "@stackframe/stack-shared/dist/plans";
69
import { adaptSchema, clientOrHigherAuthTypeSchema, yupArray, yupMixed, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
710
import { StatusError } from "@stackframe/stack-shared/dist/utils/errors";
11+
import * as zlib from "node:zlib";
812

913
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
1014

1115
const MAX_EVENTS = 500;
16+
const MAX_COMPRESSED_BYTES = 1 * 1024 * 1024;
17+
const MAX_DECOMPRESSED_BYTES = 8 * 1024 * 1024;
1218

1319
// Lone surrogates (\uD800-\uDFFF not part of a valid pair) are technically
1420
// representable in JS strings but rejected by ClickHouse's JSON parser.
@@ -32,6 +38,35 @@ function stripLoneSurrogates(value: unknown): unknown {
3238
return value;
3339
}
3440

41+
// Bodies sent as application/octet-stream are gzipped JSON. The encoding is
42+
// purely to evade keyword-matching adblockers (e.g. filters on "$click").
43+
// We gunzip + JSON.parse here so the rest of the schema can validate the
44+
// decoded object normally.
45+
function maybeDecodeBinaryBody(value: unknown): unknown {
46+
let bytes: Uint8Array | undefined;
47+
if (value instanceof ArrayBuffer) {
48+
bytes = new Uint8Array(value);
49+
} else if (value instanceof Uint8Array) {
50+
bytes = value;
51+
}
52+
if (!bytes) return value;
53+
54+
if (bytes.byteLength > MAX_COMPRESSED_BYTES) {
55+
throw new StatusError(StatusError.BadRequest, "Encoded analytics body too large");
56+
}
57+
let decompressed: Buffer;
58+
try {
59+
decompressed = zlib.gunzipSync(bytes, { maxOutputLength: MAX_DECOMPRESSED_BYTES });
60+
} catch {
61+
throw new StatusError(StatusError.BadRequest, "Invalid encoded analytics body");
62+
}
63+
try {
64+
return JSON.parse(decompressed.toString("utf-8"));
65+
} catch {
66+
throw new StatusError(StatusError.BadRequest, "Invalid encoded analytics body");
67+
}
68+
}
69+
3570
export const POST = createSmartRouteHandler({
3671
metadata: {
3772
summary: "Upload analytics event batch",
@@ -57,7 +92,7 @@ export const POST = createSmartRouteHandler({
5792
data: yupMixed().defined(),
5893
}).defined(),
5994
).defined().min(1).max(MAX_EVENTS),
60-
}).defined(),
95+
}).defined().transform((_value, originalValue) => maybeDecodeBinaryBody(originalValue)),
6196
}),
6297
response: yupObject({
6398
statusCode: yupNumber().oneOf([200]).defined(),
@@ -83,6 +118,17 @@ export const POST = createSmartRouteHandler({
83118
const refreshTokenId = auth.refreshTokenId;
84119
const tenancyId = auth.tenancy.id;
85120

121+
const app = getStackServerApp();
122+
123+
const billingTeamId = getBillingTeamId(auth.tenancy.project);
124+
if (billingTeamId != null) {
125+
const eventsItem = await app.getItem({ itemId: ITEM_IDS.analyticsEvents, teamId: billingTeamId });
126+
const isDebited = await eventsItem.tryDecreaseQuantity(body.events.length);
127+
if (!isDebited) {
128+
throw new KnownErrors.ItemQuantityInsufficientAmount(ITEM_IDS.analyticsEvents, billingTeamId, body.events.length);
129+
}
130+
}
131+
86132
const prisma = await getPrismaClientForTenancy(auth.tenancy);
87133
const recentSession = await findRecentSessionReplay(prisma, { tenancyId, refreshTokenId });
88134

apps/backend/src/app/api/latest/integrations/stripe/webhooks/route.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ const ignoredEvents = [
4848
"charge.failed",
4949
"balance.available",
5050
"customer.updated",
51+
"customer.created",
5152
] as const satisfies Stripe.Event.Type[];
5253

5354
const isSubscriptionChangedEvent = (event: Stripe.Event): event is Stripe.Event & { type: (typeof subscriptionChangedEvents)[number] } => {

apps/backend/src/app/api/latest/internal/analytics/query/route.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import { getClickhouseExternalClient } from "@/lib/clickhouse";
22
import { getSafeClickhouseErrorMessage } from "@/lib/clickhouse-errors";
3+
import { getBillingTeamId } from "@/lib/plan-entitlements";
34
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
5+
import { getStackServerApp } from "@/stack";
46
import { KnownErrors } from "@stackframe/stack-shared";
7+
import { ITEM_IDS, PLAN_LIMITS } from "@stackframe/stack-shared/dist/plans";
58
import { adaptSchema, adminAuthTypeSchema, jsonSchema, yupBoolean, yupMixed, yupNumber, yupObject, yupRecord, yupString } from "@stackframe/stack-shared/dist/schema-fields";
69
import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors";
710
import { Result } from "@stackframe/stack-shared/dist/utils/results";
811
import { randomUUID } from "crypto";
912

10-
const MAX_QUERY_TIMEOUT_MS = 120_000;
13+
const MAX_QUERY_TIMEOUT_MS = Math.max(...Object.values(PLAN_LIMITS).map(p => p.analyticsTimeoutSeconds)) * 1000;
1114
const DEFAULT_QUERY_TIMEOUT_MS = 10_000;
1215

1316
export const POST = createSmartRouteHandler({
@@ -36,6 +39,23 @@ export const POST = createSmartRouteHandler({
3639
if (body.include_all_branches) {
3740
throw new StackAssertionError("include_all_branches is not supported yet");
3841
}
42+
43+
let effectiveTimeoutMs = body.timeout_ms;
44+
const billingTeamId = getBillingTeamId(auth.tenancy.project);
45+
if (billingTeamId != null) {
46+
const app = getStackServerApp();
47+
const timeoutItem = await app.getItem({ itemId: ITEM_IDS.analyticsTimeoutSeconds, teamId: billingTeamId });
48+
// clickHouse treats max_execution_time=0 as
49+
// "unlimited", so a customer with zero timeout entitlement (no active
50+
// plan in the plans line, or a transient gap between paid-plan end
51+
// and free regrant) would otherwise get unbounded query execution.
52+
if (timeoutItem.quantity <= 0) {
53+
throw new KnownErrors.ItemQuantityInsufficientAmount(ITEM_IDS.analyticsTimeoutSeconds, billingTeamId, 1);
54+
}
55+
const maxAllowedMs = timeoutItem.quantity * 1000;
56+
effectiveTimeoutMs = Math.min(body.timeout_ms, maxAllowedMs);
57+
}
58+
3959
const client = getClickhouseExternalClient();
4060
const queryId = `${auth.tenancy.project.id}:${auth.tenancy.branchId}:${randomUUID()}`;
4161
const resultSet = await Result.fromPromise(client.query({
@@ -45,7 +65,7 @@ export const POST = createSmartRouteHandler({
4565
clickhouse_settings: {
4666
SQL_project_id: auth.tenancy.project.id,
4767
SQL_branch_id: auth.tenancy.branchId,
48-
max_execution_time: body.timeout_ms / 1000,
68+
max_execution_time: effectiveTimeoutMs / 1000,
4969
readonly: "1",
5070
allow_ddl: 0,
5171
max_result_rows: MAX_RESULT_ROWS.toString(),

0 commit comments

Comments
 (0)