@@ -13,6 +13,10 @@ const mocks = vi.hoisted(() => ({
1313 authenticatePat : vi . fn ( ) ,
1414 createOrganization : vi . fn ( ) ,
1515 findManyProjects : vi . fn ( ) ,
16+ env : { SESSION_SECRET : "test-session-secret" , ORG_CREATION_API_ENABLED : "1" } as {
17+ SESSION_SECRET : string ;
18+ ORG_CREATION_API_ENABLED ?: string ;
19+ } ,
1620} ) ) ;
1721
1822vi . mock ( "~/services/rbac.server" , ( ) => ( {
@@ -25,9 +29,7 @@ vi.mock("~/db.server", () => ({
2529 prisma : { project : { findMany : mocks . findManyProjects } } ,
2630 $replica : { } ,
2731} ) ) ;
28- vi . mock ( "~/env.server" , ( ) => ( {
29- env : { SESSION_SECRET : "test-session-secret" , ORG_CREATION_API_ENABLED : "1" } ,
30- } ) ) ;
32+ vi . mock ( "~/env.server" , ( ) => ( { env : mocks . env } ) ) ;
3133vi . mock ( "~/models/organization.server" , ( ) => ( { createOrganization : mocks . createOrganization } ) ) ;
3234vi . mock ( "~/services/personalAccessToken.server" , ( ) => ( {
3335 updateLastAccessedAtIfStale : vi . fn ( ) ,
@@ -77,6 +79,30 @@ async function createOrg(cap: string[]): Promise<{ status: number; body: any }>
7779 return { status : response . status , body : await response . json ( ) } ;
7880}
7981
82+ // An ordinary PAT, paired with an ability that denies everything. Nothing on this route may
83+ // consult it — the route has no org to scope a gate to, and on cloud the plugin returns a
84+ // deny-shaped ability when there is no org context.
85+ async function createOrgWithPat ( ) : Promise < { status : number ; body : any } > {
86+ mocks . authenticatePat . mockImplementation ( async ( ) => ( {
87+ ok : true ,
88+ userId : USER_ID ,
89+ tokenId : "pat_1" ,
90+ lastAccessedAt : new Date ( ) ,
91+ ability : { can : ( ) => false , canSuper : ( ) => false } ,
92+ } ) ) ;
93+
94+ const response = await action ( {
95+ request : new Request ( "https://api.trigger.dev/api/v1/orgs" , {
96+ method : "POST" ,
97+ headers : { Authorization : "Bearer tr_pat_1234" , "Content-Type" : "application/json" } ,
98+ body : JSON . stringify ( { title : "New Org" } ) ,
99+ } ) ,
100+ params : { } ,
101+ context : { } ,
102+ } as any ) ;
103+ return { status : response . status , body : await response . json ( ) } ;
104+ }
105+
80106const AGENT_ENVIRONMENT_ID = "env_dev" ;
81107
82108async function listProjects ( ) : Promise < { status : number ; body : any } > {
@@ -146,6 +172,29 @@ describe("creating an organization over the API", () => {
146172 expect ( mocks . createOrganization ) . not . toHaveBeenCalled ( ) ;
147173 } ) ;
148174
175+ it ( "admits an ordinary PAT without consulting its ability" , async ( ) => {
176+ const result = await createOrgWithPat ( ) ;
177+
178+ expect ( result . status ) . toBe ( 201 ) ;
179+ expect ( result . body . slug ) . toBe ( "new-org" ) ;
180+ } ) ;
181+
182+ // The env gate runs before the capability gate, so an install with the API disabled tells
183+ // every caller the same thing: the route does not exist. A capped token must not learn from a
184+ // 403 that it would have been the only thing standing in its way.
185+ it ( "hides the route from a capped token when the API is disabled" , async ( ) => {
186+ mocks . env . ORG_CREATION_API_ENABLED = undefined ;
187+
188+ try {
189+ const result = await createOrg ( [ "read:all" ] ) ;
190+
191+ expect ( result . status ) . toBe ( 404 ) ;
192+ expect ( mocks . createOrganization ) . not . toHaveBeenCalled ( ) ;
193+ } finally {
194+ mocks . env . ORG_CREATION_API_ENABLED = "1" ;
195+ }
196+ } ) ;
197+
149198 it ( "still admits a token that carries the universal grant" , async ( ) => {
150199 const result = await createOrg ( [ "admin" ] ) ;
151200
0 commit comments