You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs(ai-chat): bind chatId to the caller in the migration guide examples
The server action examples checked only that a session existed. Since chatId
comes from the browser, any signed-in user copying them could mint a token
scoped read/write to someone else's chat session. Both actions now bind the
two, and the head-start route handler gets the same check.
@@ -217,6 +218,8 @@ export async function startChatSession(params: ChatStartSessionParams<typeof myC
217
218
const session =awaitauth();
218
219
if (!session) thrownewError("Unauthorized");
219
220
221
+
awaitclaimChat(params.chatId, session.user.id);
222
+
220
223
returnstart(params);
221
224
}
222
225
@@ -225,6 +228,8 @@ export async function mintChatAccessToken(chatId: string) {
225
228
const session =awaitauth();
226
229
if (!session) thrownewError("Unauthorized");
227
230
231
+
awaitassertChatOwner(chatId, session.user.id);
232
+
228
233
returntriggerAuth.createPublicToken({
229
234
scopes: {
230
235
read: { sessions: chatId },
@@ -237,6 +242,10 @@ export async function mintChatAccessToken(chatId: string) {
237
242
238
243
Both run on your server, so the browser never sees `TRIGGER_SECRET_KEY`. This is where per-user and per-plan authorization belongs, alongside any database writes you want paired with session creation.
239
244
245
+
<Warning>
246
+
Signed in is not the same as entitled to this chat, and `chatId` arrives from the browser. Bind the two yourself: `claimChat` records the owner the first time a chat id is seen and rejects it if someone else already holds it, and `assertChatOwner` requires a row the caller owns. Check only that a session exists and any signed-in user can mint a read/write token for someone else's conversation.
247
+
</Warning>
248
+
240
249
<Note>
241
250
If you'd rather keep REST endpoints than use server actions, both callbacks accept any async function — see [calling a fetch endpoint instead](/ai-chat/frontend#calling-a-fetch-endpoint-instead-of-a-server-action).
242
251
</Note>
@@ -475,10 +484,11 @@ Head Start brings the route handler back for exactly that first turn. It runs st
475
484
Your provider keys never leave your server — the first-turn model call runs in your process, so that environment needs whatever the model requires.
476
485
</Step>
477
486
<Steptitle="Mount it where the old handler was, auth check and all">
478
-
The authorization check you moved into the server actions belongs here too, in the same place it always was. Wrap the handler rather than exporting it directly:
487
+
The authorization check you moved into the server actions belongs here too, in the same place it always was, ownership check included. Wrap the handler rather than exporting it directly:
479
488
480
489
```ts app/api/chat/route.ts
481
490
import { auth } from"@/lib/auth";
491
+
import { claimChat } from"@/lib/chat-access";
482
492
import { chatHandler } from"@/lib/chat-handler";
483
493
484
494
// The handler holds the SSE response open until the agent signals
@@ -489,6 +499,10 @@ Head Start brings the route handler back for exactly that first turn. It runs st
489
499
const session =awaitauth();
490
500
if (!session) returnnewResponse("Unauthorized", { status: 401 });
491
501
502
+
// Clone so the handler still gets an unread body.
0 commit comments