77 type SessionItem ,
88 type SessionStatus ,
99} from "@trigger.dev/core/v3" ;
10- import type { Session } from "@trigger.dev/database" ;
10+ import type { Prisma , Session } from "@trigger.dev/database" ;
1111import { $replica , prisma , type PrismaClient } from "~/db.server" ;
1212import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server" ;
1313import { logger } from "~/services/logger.server" ;
@@ -32,7 +32,10 @@ import {
3232 createLoaderApiRoute ,
3333 everyResource ,
3434} from "~/services/routeBuilders/apiBuilder.server" ;
35- import { recordSessionCreateAuthorization } from "~/services/sessionAuthorizationTelemetry.server" ;
35+ import {
36+ recordSessionCreateAuthorization ,
37+ sessionCreateAuthorizationOutcome ,
38+ } from "~/services/sessionAuthorizationTelemetry.server" ;
3639import { ServiceValidationError } from "~/v3/services/common.server" ;
3740import { runStore } from "~/v3/runStore.server" ;
3841
@@ -148,26 +151,15 @@ const { action } = createActionApiRoute(
148151 // browser uses thereafter against `.in/append`, `.out` SSE,
149152 // `end-and-continue`, etc.
150153 //
151- // JWT is allowed when the caller holds an explicit `write:sessions` /
152- // `admin` super-scope plus a `tasks:<taskIdentifier>` scope — gates
153- // server-side surfaces like the cli-v3 MCP from creating sessions on
154- // behalf of the developer without weakening the browser model.
154+ // Creating a session requires session-write AND task-trigger permissions.
155155 allowJWT : true ,
156156 authorization : {
157- // Per-task scoping via `body.taskIdentifier` (action-route resource
158- // callbacks receive the parsed body as the 4th arg — see
159- // `apiBuilder.server.ts:710`). A JWT scoped only to `write:tasks:foo`
160- // can only create sessions whose `taskIdentifier` is `"foo"`.
161- //
162- // Multi-key resource: pre-RBAC this route had a `superScopes:
163- // ["write:sessions", "admin"]` whitelist; post-RBAC the equivalent
164- // is the `{ type: "sessions" }` element below — a `write:sessions`
165- // JWT (no id) matches it directly, deliberately bypassing the
166- // per-task check exactly as before. `admin` / `write:all` bypass
167- // via the JWT ability's wildcard branches.
168- action : "write" ,
169- resource : ( _params , _searchParams , _headers , body ) =>
170- anyResource ( [ { type : "tasks" , id : body . taskIdentifier } , { type : "sessions" } ] ) ,
157+ // Session-write is checked below after resolving alternate session IDs.
158+ action : "trigger" ,
159+ resource : ( _params , _searchParams , _headers , body ) => ( {
160+ type : "tasks" ,
161+ id : body . taskIdentifier ,
162+ } ) ,
171163 } ,
172164 corsStrategy : "all" ,
173165 } ,
@@ -182,8 +174,25 @@ const { action } = createActionApiRoute(
182174 ) ;
183175 }
184176
185- // Idempotent on (env, externalId): two concurrent POSTs converge to the same row, and
186- // `triggerConfig` is refreshed on the cached path so a redeployed config reaches the next run.
177+ const sessionIds = body . externalId ? [ body . externalId ] : [ ] ;
178+ if ( body . externalId && ! ability . can ( "write" , { type : "sessions" , id : body . externalId } ) ) {
179+ const existing = await prisma . session . findFirst ( {
180+ where : {
181+ runtimeEnvironmentId : authentication . environment . id ,
182+ externalId : body . externalId ,
183+ } ,
184+ select : { friendlyId : true } ,
185+ } ) ;
186+ if ( existing ) sessionIds . push ( existing . friendlyId ) ;
187+ }
188+ if (
189+ sessionCreateAuthorizationOutcome ( ability , body . taskIdentifier , sessionIds ) !==
190+ "both_allowed"
191+ ) {
192+ return json ( { error : "Unauthorized" } , { status : 403 } ) ;
193+ }
194+
195+ // Defer cached config changes until the stored task has been authorized.
187196 const { session, isCached } = await findOrCreateSession ( {
188197 environment : authentication . environment ,
189198 externalId : body . externalId ,
@@ -193,6 +202,7 @@ const { action } = createActionApiRoute(
193202 tags : body . tags ,
194203 metadata : body . metadata as Record < string , unknown > | undefined ,
195204 expiresAt : body . expiresAt ,
205+ refreshTriggerConfig : false ,
196206 } ) ;
197207
198208 // Reject create on a closed session. The upsert path will return
@@ -220,6 +230,26 @@ const { action } = createActionApiRoute(
220230 }
221231
222232 recordSessionCreateAuthorization ( ability , session , request , authentication . environment ) ;
233+ if (
234+ sessionCreateAuthorizationOutcome (
235+ ability ,
236+ session . taskIdentifier ,
237+ [ session . friendlyId , session . externalId ] . filter ( ( id ) : id is string => ! ! id )
238+ ) !== "both_allowed"
239+ ) {
240+ return json ( { error : "Unauthorized" } , { status : 403 } ) ;
241+ }
242+
243+ if ( isCached ) {
244+ Object . assign (
245+ session ,
246+ await prisma . session . update ( {
247+ where : { id : session . id } ,
248+ data : { triggerConfig : body . triggerConfig as unknown as Prisma . InputJsonValue } ,
249+ select : { triggerConfig : true , updatedAt : true } ,
250+ } )
251+ ) ;
252+ }
223253
224254 // Session is task-bound — every session has a live run by
225255 // construction. `ensureRunForSession` is idempotent: on the
0 commit comments