Skip to content

Commit 8be613c

Browse files
committed
Merge branch 'dev' into feat/dev-tool
2 parents b1780b4 + 8aa80ce commit 8be613c

71 files changed

Lines changed: 3114 additions & 1340 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.

.cursor/commands/pre-push.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Please compare `dev` to `main` and ensure that all migrations are backwards compatible. In what ways could breakage occur? Report the result to me in detail.
1+
Please compare `dev` to `main` and ensure that all migrations are backwards compatible. In what ways could breakage occur? Report the result to me in detail. Anything else that's scary that could occur, or that we should think about while migrating?

apps/backend/.env

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,6 @@ STACK_STRIPE_SECRET_KEY=# enter your stripe api key
115115
STACK_STRIPE_WEBHOOK_SECRET=# enter your stripe webhook secret
116116
STACK_TELEGRAM_BOT_TOKEN= # enter you telegram bot token
117117
STACK_TELEGRAM_CHAT_ID=# enter your telegram chat id
118+
119+
# Docs AI tool bundle
120+
STACK_DOCS_INTERNAL_BASE_URL=# override the docs origin used by the backend's AI tool bundle to call the docs app's `/api/internal/docs-tools` endpoint. Defaults to http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}04 in dev, https://mcp.stack-auth.com in prod

apps/backend/.env.development

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ STACK_STRIPE_SECRET_KEY=sk_test_mockstripekey
7878
STACK_STRIPE_WEBHOOK_SECRET=mock_stripe_webhook_secret
7979
STACK_OPENROUTER_API_KEY=FORWARD_TO_PRODUCTION
8080
STACK_FEEDBACK_MODE=FORWARD_TO_PRODUCTION
81+
# STACK_DOCS_INTERNAL_BASE_URL=http://localhost:8104
8182
# Email monitor configuration for tests
8283
STACK_EMAIL_MONITOR_VERIFICATION_CALLBACK_URL=http://localhost:8101/handler/email-verification
8384
STACK_EMAIL_MONITOR_PROJECT_ID=internal

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.81",
3+
"version": "2.8.82",
44
"repository": "https://github.com/stack-auth/stack-auth",
55
"private": true,
66
"type": "module",
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
-- CreateTable
2+
CREATE TABLE "AiConversation" (
3+
"id" UUID NOT NULL,
4+
"projectUserId" UUID NOT NULL,
5+
"projectId" TEXT NOT NULL REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE,
6+
"title" TEXT NOT NULL,
7+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
8+
"updatedAt" TIMESTAMP(3) NOT NULL,
9+
10+
CONSTRAINT "AiConversation_pkey" PRIMARY KEY ("id")
11+
);
12+
13+
-- CreateTable
14+
CREATE TABLE "AiMessage" (
15+
"id" UUID NOT NULL,
16+
"conversationId" UUID NOT NULL REFERENCES "AiConversation"("id") ON DELETE CASCADE ON UPDATE CASCADE,
17+
"position" INTEGER NOT NULL,
18+
"role" TEXT NOT NULL,
19+
"content" JSONB NOT NULL,
20+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
21+
22+
CONSTRAINT "AiMessage_pkey" PRIMARY KEY ("id")
23+
);
24+
25+
-- CreateIndex
26+
CREATE INDEX "AiConversation_projectUserId_projectId_updatedAt_idx" ON "AiConversation"("projectUserId", "projectId", "updatedAt" DESC);
27+
28+
-- CreateIndex
29+
CREATE INDEX "AiMessage_conversationId_position_idx" ON "AiMessage"("conversationId", "position" ASC);
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-- SPLIT_STATEMENT_SENTINEL
2+
-- SINGLE_STATEMENT_SENTINEL
3+
-- RUN_OUTSIDE_TRANSACTION_SENTINEL
4+
CREATE INDEX CONCURRENTLY IF NOT EXISTS "ProjectUser_signUpEmailNormalized_recent_idx"
5+
ON "ProjectUser"("tenancyId", "isAnonymous", "signUpEmailNormalized", "signedUpAt");

apps/backend/prisma/schema.prisma

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ model Project {
4343
branchConfigOverrides BranchConfigOverride[]
4444
environmentConfigOverrides EnvironmentConfigOverride[]
4545
localEmulatorProject LocalEmulatorProject?
46+
aiConversations AiConversation[]
4647
4748
@@index([ownerTeamId], map: "Project_ownerTeamId_idx")
4849
}
@@ -332,6 +333,7 @@ model ProjectUser {
332333
@@index([tenancyId, createdAt(sort: Desc)], name: "ProjectUser_createdAt_desc")
333334
@@index([tenancyId, isAnonymous, signedUpAt(sort: Asc)], name: "ProjectUser_signedUpAt_asc")
334335
@@index([tenancyId, isAnonymous, signUpIp, signedUpAt], name: "ProjectUser_signUpIp_recent_idx")
336+
@@index([tenancyId, isAnonymous, signUpEmailNormalized, signedUpAt], name: "ProjectUser_signUpEmailNormalized_recent_idx")
335337
@@index([tenancyId, isAnonymous, signUpEmailBase, signedUpAt], name: "ProjectUser_signUpEmailBase_recent_idx")
336338
@@index([tenancyId, sequenceId], name: "ProjectUser_tenancyId_sequenceId_idx")
337339
@@index([shouldUpdateSequenceId, tenancyId], name: "ProjectUser_shouldUpdateSequenceId_idx")
@@ -1142,6 +1144,31 @@ model ThreadMessage {
11421144
@@id([tenancyId, id])
11431145
}
11441146

1147+
model AiConversation {
1148+
id String @id @default(uuid()) @db.Uuid
1149+
projectUserId String @db.Uuid
1150+
projectId String
1151+
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
1152+
title String
1153+
createdAt DateTime @default(now())
1154+
updatedAt DateTime @updatedAt
1155+
messages AiMessage[]
1156+
1157+
@@index([projectUserId, projectId, updatedAt(sort: Desc)])
1158+
}
1159+
1160+
model AiMessage {
1161+
id String @id @default(uuid()) @db.Uuid
1162+
conversationId String @db.Uuid
1163+
position Int
1164+
role String
1165+
content Json
1166+
createdAt DateTime @default(now())
1167+
conversation AiConversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
1168+
1169+
@@index([conversationId, position])
1170+
}
1171+
11451172
enum CustomerType {
11461173
USER
11471174
TEAM

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export const POST = createSmartRouteHandler({
118118
return {
119119
statusCode: 200,
120120
bodyType: "json" as const,
121-
body: { content: contentBlocks },
121+
body: { content: contentBlocks, finalText: result.text },
122122
};
123123
}
124124
},

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

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,23 @@ async function createProjectUserOAuthAccountForLink(prisma: PrismaClientTransact
4646
});
4747
}
4848

49-
const redirectOrThrowError = (error: KnownError, tenancy: Tenancy, errorRedirectUrl?: string) => {
50-
if (!errorRedirectUrl || (!validateRedirectUrl(errorRedirectUrl, tenancy) && !isAcceptedNativeAppUrl(errorRedirectUrl))) {
49+
const redirectOrThrowError = (error: KnownError, tenancy: Tenancy, options: {
50+
oauthCallbackRedirectUrl?: string,
51+
errorRedirectUrl?: string,
52+
}) => {
53+
const targetRedirectUrl =
54+
options.oauthCallbackRedirectUrl && (validateRedirectUrl(options.oauthCallbackRedirectUrl, tenancy) || isAcceptedNativeAppUrl(options.oauthCallbackRedirectUrl))
55+
? options.oauthCallbackRedirectUrl
56+
: options.errorRedirectUrl && (validateRedirectUrl(options.errorRedirectUrl, tenancy) || isAcceptedNativeAppUrl(options.errorRedirectUrl))
57+
? options.errorRedirectUrl
58+
: null;
59+
if (!targetRedirectUrl) {
5160
throw error;
5261
}
5362

54-
const url = new URL(errorRedirectUrl);
63+
const url = new URL(targetRedirectUrl);
64+
url.searchParams.set("error", "server_error");
65+
url.searchParams.set("error_description", error.message);
5566
url.searchParams.set("errorCode", error.errorCode);
5667
url.searchParams.set("message", error.message);
5768
url.searchParams.set("details", error.details ? JSON.stringify(error.details) : JSON.stringify({}));
@@ -113,6 +124,7 @@ const handler = createSmartRouteHandler({
113124
projectUserId,
114125
providerScope,
115126
errorRedirectUrl,
127+
redirectUri,
116128
afterCallbackRedirectUrl,
117129
} = outerInfo;
118130

@@ -152,7 +164,7 @@ const handler = createSmartRouteHandler({
152164
});
153165
} catch (error) {
154166
if (KnownErrors['OAuthProviderAccessDenied'].isInstance(error)) {
155-
redirectOrThrowError(error, tenancy, errorRedirectUrl);
167+
redirectOrThrowError(error, tenancy, { oauthCallbackRedirectUrl: redirectUri, errorRedirectUrl });
156168
}
157169
throw error;
158170
}
@@ -387,7 +399,7 @@ const handler = createSmartRouteHandler({
387399
return oauthResponseToSmartResponse(oauthResponse);
388400
} catch (error) {
389401
if (KnownError.isKnownError(error)) {
390-
redirectOrThrowError(error, tenancy, errorRedirectUrl);
402+
redirectOrThrowError(error, tenancy, { oauthCallbackRedirectUrl: redirectUri, errorRedirectUrl });
391403
}
392404
throw error;
393405
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { globalPrismaClient } from "@/prisma-client";
2+
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
3+
import { adaptSchema, yupArray, yupMixed, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
4+
import { getOwnedConversation } from "../../utils";
5+
6+
export const PUT = createSmartRouteHandler({
7+
metadata: {
8+
summary: "Replace conversation messages",
9+
description: "Replace all messages in a conversation",
10+
},
11+
request: yupObject({
12+
auth: yupObject({
13+
type: adaptSchema,
14+
user: adaptSchema.defined(),
15+
project: yupObject({
16+
id: yupString().oneOf(["internal"]).defined(),
17+
}).defined(),
18+
}).defined(),
19+
params: yupObject({
20+
conversationId: yupString().defined(),
21+
}),
22+
body: yupObject({
23+
messages: yupArray(
24+
yupObject({
25+
role: yupString().oneOf(["user", "assistant"]).defined(),
26+
content: yupMixed().defined(),
27+
})
28+
).defined(),
29+
}),
30+
}),
31+
response: yupObject({
32+
statusCode: yupNumber().oneOf([200]).defined(),
33+
bodyType: yupString().oneOf(["json"]).defined(),
34+
body: yupObject({}).defined(),
35+
}),
36+
handler: async ({ auth, params, body }) => {
37+
await getOwnedConversation(params.conversationId, auth.user.id);
38+
39+
await globalPrismaClient.$executeRaw`
40+
WITH input AS (
41+
SELECT
42+
(ord - 1)::int AS position,
43+
(elem->>'role')::text AS role,
44+
elem->'content' AS content
45+
FROM jsonb_array_elements(${JSON.stringify(body.messages)}::jsonb)
46+
WITH ORDINALITY AS t(elem, ord)
47+
),
48+
deleted AS (
49+
DELETE FROM "AiMessage" WHERE "conversationId" = ${params.conversationId}::uuid
50+
),
51+
inserted AS (
52+
INSERT INTO "AiMessage" ("id", "conversationId", "position", "role", "content")
53+
SELECT gen_random_uuid(), ${params.conversationId}::uuid, position, role, content
54+
FROM input
55+
)
56+
UPDATE "AiConversation"
57+
SET "updatedAt" = NOW()
58+
WHERE "id" = ${params.conversationId}::uuid
59+
`;
60+
61+
return {
62+
statusCode: 200 as const,
63+
bodyType: "json" as const,
64+
body: {},
65+
};
66+
},
67+
});

0 commit comments

Comments
 (0)