Skip to content

Commit 26254c4

Browse files
authored
Merge branch 'dev' into emu-with-a-q
2 parents 950d74a + 13fccd3 commit 26254c4

467 files changed

Lines changed: 32652 additions & 460 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.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Please review the PR comments with the `gh` CLI and fix those issues that are valid and relevant. Resolve the comments when you fix them. Also resolve all those comments that no longer exist or have already been resolved. Leave those comments that are mostly bullshit unresolved. Report the result to me in detail. Do NOT automatically commit or stage the changes back to the PR!
1+
Please review the PR comments with the `gh` CLI and fix those issues that are valid and relevant. Resolve the comments when you fix them. Also resolve all those comments that no longer exist or have already been resolved. Leave those comments that are mostly bullshit unresolved. Also, create or modify tests to make sure the fixed behavior works as expected. Report the result to me in detail (for the most important issues, whether resolved or unresolved, mark them with a ‼️ emoji). Do NOT automatically commit or stage the changes back to the PR!

.github/workflows/e2e-api-tests.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ jobs:
2121
STACK_DATABASE_CONNECTION_STRING: "postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:8128/stackframe"
2222
STACK_EXTERNAL_DB_SYNC_MAX_DURATION_MS: "20000"
2323
STACK_EXTERNAL_DB_SYNC_DIRECT: "false"
24+
STACK_RUN_SETUP_WIZARD_TESTS: ${{ matrix.freestyle-mode != 'prod' && 'true' || '' }}
2425

2526
strategy:
2627
matrix:

apps/backend/.env.development

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
NEXT_PUBLIC_STACK_API_URL=http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}02
22
NEXT_PUBLIC_STACK_DASHBOARD_URL=http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}01
3+
NEXT_PUBLIC_STACK_HOSTED_HANDLER_DOMAIN_SUFFIX=.localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}09
34
NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR=false
45
STACK_SERVER_SECRET=23-wuNpik0gIW4mruTz25rbIvhuuvZFrLOLtL7J4tyo
56

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.78",
3+
"version": "2.8.80",
44
"repository": "https://github.com/stack-auth/stack-auth",
55
"private": true,
66
"type": "module",
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ALTER TABLE "ProjectUserAuthorizationCode"
2+
ADD COLUMN "grantedRefreshTokenId" UUID;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { randomUUID } from "crypto";
2+
import type { Sql } from "postgres";
3+
import { expect } from "vitest";
4+
5+
export const preMigration = async (sql: Sql) => {
6+
const projectId = `test-${randomUUID()}`;
7+
const tenancyId = randomUUID();
8+
const projectUserId = randomUUID();
9+
const authorizationCode = `code-${randomUUID()}`;
10+
11+
await sql`
12+
INSERT INTO "Project" ("id", "createdAt", "updatedAt", "displayName", "description", "isProductionMode")
13+
VALUES (${projectId}, NOW(), NOW(), 'Test', '', false)
14+
`;
15+
await sql`
16+
INSERT INTO "Tenancy" ("id", "createdAt", "updatedAt", "projectId", "branchId", "hasNoOrganization")
17+
VALUES (${tenancyId}::uuid, NOW(), NOW(), ${projectId}, 'main', 'TRUE'::"BooleanTrue")
18+
`;
19+
await sql`
20+
INSERT INTO "ProjectUser" ("projectUserId", "tenancyId", "mirroredProjectId", "mirroredBranchId", "createdAt", "updatedAt", "lastActiveAt")
21+
VALUES (${projectUserId}::uuid, ${tenancyId}::uuid, ${projectId}, 'main', NOW(), NOW(), NOW())
22+
`;
23+
await sql`
24+
INSERT INTO "ProjectUserAuthorizationCode" (
25+
"tenancyId",
26+
"projectUserId",
27+
"authorizationCode",
28+
"redirectUri",
29+
"expiresAt",
30+
"codeChallenge",
31+
"codeChallengeMethod",
32+
"newUser",
33+
"afterCallbackRedirectUrl",
34+
"createdAt",
35+
"updatedAt"
36+
)
37+
VALUES (
38+
${tenancyId}::uuid,
39+
${projectUserId}::uuid,
40+
${authorizationCode},
41+
'https://example.com/callback',
42+
NOW() + INTERVAL '10 minutes',
43+
'challenge',
44+
'S256',
45+
false,
46+
'https://example.com/after-auth',
47+
NOW(),
48+
NOW()
49+
)
50+
`;
51+
52+
return { tenancyId, projectUserId, authorizationCode };
53+
};
54+
55+
export const postMigration = async (sql: Sql, ctx: Awaited<ReturnType<typeof preMigration>>) => {
56+
const existing = await sql`
57+
SELECT "grantedRefreshTokenId"
58+
FROM "ProjectUserAuthorizationCode"
59+
WHERE "tenancyId" = ${ctx.tenancyId}::uuid
60+
AND "authorizationCode" = ${ctx.authorizationCode}
61+
`;
62+
expect(existing).toHaveLength(1);
63+
expect(existing[0].grantedRefreshTokenId).toBeNull();
64+
65+
const grantedRefreshTokenId = randomUUID();
66+
await sql`
67+
UPDATE "ProjectUserAuthorizationCode"
68+
SET "grantedRefreshTokenId" = ${grantedRefreshTokenId}::uuid
69+
WHERE "tenancyId" = ${ctx.tenancyId}::uuid
70+
AND "authorizationCode" = ${ctx.authorizationCode}
71+
`;
72+
73+
const updated = await sql`
74+
SELECT "grantedRefreshTokenId"
75+
FROM "ProjectUserAuthorizationCode"
76+
WHERE "tenancyId" = ${ctx.tenancyId}::uuid
77+
AND "authorizationCode" = ${ctx.authorizationCode}
78+
`;
79+
expect(updated).toHaveLength(1);
80+
expect(updated[0].grantedRefreshTokenId).toBe(grantedRefreshTokenId);
81+
};

apps/backend/prisma/schema.prisma

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,10 @@ model Tenancy {
6969
branchId String
7070
7171
// If organizationId is NULL, hasNoOrganization must be TRUE. If organizationId is not NULL, hasNoOrganization must be NULL.
72-
organizationId String? @db.Uuid
73-
hasNoOrganization BooleanTrue?
74-
emailOutboxes EmailOutbox[]
75-
sessionReplays SessionReplay[]
72+
organizationId String? @db.Uuid
73+
hasNoOrganization BooleanTrue?
74+
emailOutboxes EmailOutbox[]
75+
sessionReplays SessionReplay[]
7676
sessionReplayChunks SessionReplayChunk[]
7777
managedEmailDomains ManagedEmailDomain[]
7878
@@ -94,24 +94,24 @@ enum ManagedEmailDomainStatus {
9494
model ManagedEmailDomain {
9595
id String @id @default(uuid()) @db.Uuid
9696
97-
tenancyId String @db.Uuid
98-
tenancy Tenancy @relation(fields: [tenancyId], references: [id], onDelete: Cascade)
97+
tenancyId String @db.Uuid
98+
tenancy Tenancy @relation(fields: [tenancyId], references: [id], onDelete: Cascade)
9999
100100
projectId String
101-
branchId String
101+
branchId String
102102
103-
subdomain String
104-
senderLocalPart String
105-
resendDomainId String @unique
103+
subdomain String
104+
senderLocalPart String
105+
resendDomainId String @unique
106106
nameServerRecords Json
107107
108-
status ManagedEmailDomainStatus @default(PENDING_VERIFICATION)
108+
status ManagedEmailDomainStatus @default(PENDING_VERIFICATION)
109109
providerStatusRaw String?
110-
isActive Boolean @default(true)
111-
lastError String?
112-
verifiedAt DateTime?
113-
appliedAt DateTime?
114-
lastWebhookAt DateTime?
110+
isActive Boolean @default(true)
111+
lastError String?
112+
verifiedAt DateTime?
113+
appliedAt DateTime?
114+
lastWebhookAt DateTime?
115115
116116
createdAt DateTime @default(now())
117117
updatedAt DateTime @updatedAt
@@ -367,18 +367,18 @@ model SessionReplay {
367367
chunks SessionReplayChunk[]
368368
369369
@@id([tenancyId, id])
370-
@@map("SessionReplay")
371370
@@index([tenancyId, projectUserId, startedAt])
372371
@@index([tenancyId, lastEventAt])
373372
// index by updatedAt instead of lastEventAt because event timing can be spoofed
374373
@@index([tenancyId, refreshTokenId, updatedAt])
374+
@@map("SessionReplay")
375375
}
376376

377377
model SessionReplayChunk {
378378
id String @id @default(uuid()) @db.Uuid
379379
380-
tenancyId String @db.Uuid
381-
sessionReplayId String @db.Uuid @map("sessionReplayId")
380+
tenancyId String @db.Uuid
381+
sessionReplayId String @map("sessionReplayId") @db.Uuid
382382
383383
// Unique per uploaded batch for a given session id.
384384
batchId String @db.Uuid
@@ -402,8 +402,8 @@ model SessionReplayChunk {
402402
tenancy Tenancy @relation(fields: [tenancyId], references: [id], onDelete: Cascade)
403403
404404
@@unique([tenancyId, sessionReplayId, batchId])
405-
@@map("SessionReplayChunk")
406405
@@index([tenancyId, sessionReplayId, createdAt])
406+
@@map("SessionReplayChunk")
407407
}
408408

409409
enum ContactChannelType {
@@ -631,6 +631,9 @@ model ProjectUserAuthorizationCode {
631631
632632
newUser Boolean
633633
afterCallbackRedirectUrl String?
634+
/// Refresh token ID that should be granted when this authorization code is exchanged.
635+
/// NULL means no specific refresh token is preselected, so the token endpoint follows normal issuance/reuse logic.
636+
grantedRefreshTokenId String? @db.Uuid
634637
635638
@@id([tenancyId, authorizationCode])
636639
}

apps/backend/src/app/api/latest/ai/query/[mode]/route.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,10 @@ export const POST = createSmartRouteHandler({
4747

4848
// Verify user has access to the target project
4949
if (projectId != null) {
50-
const user = fullReq.auth?.user;
50+
if (fullReq.auth?.project.id !== "internal") {
51+
throw new StatusError(StatusError.Forbidden, "You do not have access to this project");
52+
}
53+
const user = fullReq.auth.user;
5154
if (user == null) {
5255
throw new StatusError(StatusError.Forbidden, "You do not have access to this project");
5356
}

apps/backend/src/app/api/latest/auth/oauth/authorize/[provider_id]/route.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { checkApiKeySet, throwCheckApiKeySetError } from "@/lib/internal-api-keys";
2+
import { isAcceptedNativeAppUrl, validateRedirectUrl } from "@/lib/redirect-urls";
23
import { getSoleTenancyFromProjectBranch } from "@/lib/tenancies";
34
import { decodeAccessToken, oauthCookieSchema } from "@/lib/tokens";
4-
import { getRequestContextAndBotChallengeAssessment, botChallengeFlowRequestSchemaFields } from "@/lib/turnstile";
5+
import { botChallengeFlowRequestSchemaFields, getRequestContextAndBotChallengeAssessment } from "@/lib/turnstile";
56
import { getProjectBranchFromClientId, getProvider } from "@/oauth";
67
import { globalPrismaClient } from "@/prisma-client";
7-
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
88
import type { SmartResponse } from "@/route-handlers/smart-response";
9+
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
910
import { KnownErrors } from "@stackframe/stack-shared/dist/known-errors";
1011
import { urlSchema, yupArray, yupNumber, yupObject, yupString, yupUnion } from "@stackframe/stack-shared/dist/schema-fields";
1112
import { getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env";
@@ -37,7 +38,7 @@ export const GET = createSmartRouteHandler({
3738
*/
3839
error_redirect_url: urlSchema.optional().meta({ openapiField: { hidden: true } }),
3940
error_redirect_uri: urlSchema.optional(),
40-
after_callback_redirect_url: yupString().optional(),
41+
after_callback_redirect_url: urlSchema.optional(),
4142
stack_response_mode: yupString().oneOf(["json", "redirect"]).default("redirect"),
4243
...botChallengeFlowRequestSchemaFields,
4344

@@ -93,6 +94,13 @@ export const GET = createSmartRouteHandler({
9394
if (query.type === "link" && !query.token) {
9495
throw new StatusError(StatusError.BadRequest, "?token= query parameter is required for link type");
9596
}
97+
if (
98+
query.after_callback_redirect_url
99+
&& !validateRedirectUrl(query.after_callback_redirect_url, tenancy)
100+
&& !isAcceptedNativeAppUrl(query.after_callback_redirect_url)
101+
) {
102+
throw new KnownErrors.RedirectUrlNotWhitelisted();
103+
}
96104

97105
const { turnstileAssessment } = await getRequestContextAndBotChallengeAssessment(query, "oauth_authenticate", tenancy);
98106

0 commit comments

Comments
 (0)