@@ -19,6 +19,8 @@ type LazyControllerInternals = {
1919} ;
2020
2121const prismaPlaceholder = { } as PrismaClient ;
22+ const ADDITIONAL_API_KEY = "tr_prod_sk_0123456789abcdefghijklmn" ;
23+ const ROOT_API_KEY = "tr_prod_0123456789abcdefghijklmn" ;
2224const environment = {
2325 id : "env_123" ,
2426 organizationId : "org_123" ,
@@ -53,6 +55,20 @@ function additionalKeyResult(scopes: string[]): AuthSuccess {
5355 } ;
5456}
5557
58+ function rootKeyResult ( ) : AuthSuccess {
59+ return {
60+ ok : true ,
61+ environment,
62+ subject : {
63+ type : "user" ,
64+ userId : "user_123" ,
65+ organizationId : environment . organizationId ,
66+ projectId : environment . projectId ,
67+ } ,
68+ ability : buildJwtAbility ( [ "admin" ] ) ,
69+ } ;
70+ }
71+
5672function publicJwtResult ( ) : AuthSuccess {
5773 return {
5874 ok : true ,
@@ -71,6 +87,12 @@ function publicJwt(payload: Record<string, unknown>) {
7187 return `header.${ Buffer . from ( JSON . stringify ( payload ) ) . toString ( "base64url" ) } .signature` ;
7288}
7389
90+ function bearerRequest ( token : string ) {
91+ return new Request ( "https://api.trigger.dev/test" , {
92+ headers : { Authorization : `Bearer ${ token } ` } ,
93+ } ) ;
94+ }
95+
7496describe ( "API-key policy controller composition" , ( ) => {
7597 it ( "routes public JWTs directly to the host without calling the plugin authenticator" , async ( ) => {
7698 const pluginAuthenticate = vi . fn ( ) ;
@@ -94,35 +116,126 @@ describe("API-key policy controller composition", () => {
94116 expect ( pluginAuthenticate ) . not . toHaveBeenCalled ( ) ;
95117 } ) ;
96118
97- it ( "uses the scoped host result unchanged when plugin auth falls back to an additional key" , async ( ) => {
98- const pluginAuthenticate = vi . fn ( async ( ) => ( {
99- ok : false as const ,
100- status : 401 as const ,
101- error : "Invalid API key" ,
102- } ) ) ;
119+ it ( "routes valid additional keys directly to the host and preserves their scopes" , async ( ) => {
120+ const pluginAuthenticate = vi . fn ( ) ;
121+ const hostAuthenticate = vi . fn ( async ( ) => additionalKeyResult ( [ "write:tasks:send-email" ] ) ) ;
103122 const plugin = {
104123 isUsingPlugin : vi . fn ( async ( ) => true ) ,
105124 authenticateBearer : pluginAuthenticate ,
106125 } as unknown as RoleBaseAccessController ;
107- const controller = installPlugin (
108- plugin ,
109- vi . fn ( async ( ) => additionalKeyResult ( [ "write:tasks:send-email" ] ) )
110- ) ;
126+ const controller = installPlugin ( plugin , hostAuthenticate ) ;
111127
112- const result = await controller . authenticateBearer (
113- new Request ( "https://api.trigger.dev/test" , {
114- headers : { Authorization : "Bearer tr_additional" } ,
115- } )
116- ) ;
128+ const result = await controller . authenticateBearer ( bearerRequest ( ADDITIONAL_API_KEY ) ) ;
117129
118130 expect ( result . ok ) . toBe ( true ) ;
131+ expect ( hostAuthenticate ) . toHaveBeenCalledOnce ( ) ;
132+ expect ( pluginAuthenticate ) . not . toHaveBeenCalled ( ) ;
119133 if ( ! result . ok ) return ;
120134 expect ( result . subject ) . toMatchObject ( { type : "apiKey" , restricted : true } ) ;
121135 expect ( result . ability . can ( "trigger" , { type : "tasks" , id : "send-email" } ) ) . toBe ( true ) ;
122136 expect ( result . ability . can ( "trigger" , { type : "tasks" , id : "other-task" } ) ) . toBe ( false ) ;
123137 expect ( result . ability . can ( "read" , { type : "runs" } ) ) . toBe ( false ) ;
124138 } ) ;
125139
140+ it ( "returns an unknown additional key's host 401 without calling the plugin" , async ( ) => {
141+ const hostFailure = {
142+ ok : false as const ,
143+ status : 401 as const ,
144+ error : "Invalid API key" ,
145+ } ;
146+ const pluginAuthenticate = vi . fn ( ) ;
147+ const hostAuthenticate = vi . fn ( async ( ) => hostFailure ) ;
148+ const plugin = {
149+ isUsingPlugin : vi . fn ( async ( ) => true ) ,
150+ authenticateBearer : pluginAuthenticate ,
151+ } as unknown as RoleBaseAccessController ;
152+ const controller = installPlugin ( plugin , hostAuthenticate ) ;
153+
154+ await expect ( controller . authenticateBearer ( bearerRequest ( ADDITIONAL_API_KEY ) ) ) . resolves . toEqual (
155+ hostFailure
156+ ) ;
157+ expect ( hostAuthenticate ) . toHaveBeenCalledOnce ( ) ;
158+ expect ( pluginAuthenticate ) . not . toHaveBeenCalled ( ) ;
159+ } ) ;
160+
161+ it ( "routes current root keys to the plugin without calling the host" , async ( ) => {
162+ const pluginAuthenticate = vi . fn ( async ( ) => rootKeyResult ( ) ) ;
163+ const hostAuthenticate = vi . fn ( ) ;
164+ const plugin = {
165+ isUsingPlugin : vi . fn ( async ( ) => true ) ,
166+ authenticateBearer : pluginAuthenticate ,
167+ } as unknown as RoleBaseAccessController ;
168+ const controller = installPlugin ( plugin , hostAuthenticate ) ;
169+
170+ await expect ( controller . authenticateBearer ( bearerRequest ( ROOT_API_KEY ) ) ) . resolves . toMatchObject (
171+ {
172+ ok : true ,
173+ subject : { type : "user" } ,
174+ }
175+ ) ;
176+ expect ( pluginAuthenticate ) . toHaveBeenCalledOnce ( ) ;
177+ expect ( hostAuthenticate ) . not . toHaveBeenCalled ( ) ;
178+ } ) ;
179+
180+ it . each ( [ 401 , 403 ] as const ) (
181+ "returns a plugin %s for root-shaped keys without calling the host" ,
182+ async ( status ) => {
183+ const pluginFailure = {
184+ ok : false as const ,
185+ status,
186+ error : "Unauthorized" ,
187+ } ;
188+ const pluginAuthenticate = vi . fn ( async ( ) => pluginFailure ) ;
189+ const hostAuthenticate = vi . fn ( ) ;
190+ const plugin = {
191+ isUsingPlugin : vi . fn ( async ( ) => true ) ,
192+ authenticateBearer : pluginAuthenticate ,
193+ } as unknown as RoleBaseAccessController ;
194+ const controller = installPlugin ( plugin , hostAuthenticate ) ;
195+
196+ await expect ( controller . authenticateBearer ( bearerRequest ( ROOT_API_KEY ) ) ) . resolves . toEqual (
197+ pluginFailure
198+ ) ;
199+ expect ( pluginAuthenticate ) . toHaveBeenCalledOnce ( ) ;
200+ expect ( hostAuthenticate ) . not . toHaveBeenCalled ( ) ;
201+ }
202+ ) ;
203+
204+ it ( "fails closed when an additional-key host route resolves a non-apiKey subject" , async ( ) => {
205+ const pluginAuthenticate = vi . fn ( ) ;
206+ const hostAuthenticate = vi . fn ( async ( ) => rootKeyResult ( ) ) ;
207+ const plugin = {
208+ isUsingPlugin : vi . fn ( async ( ) => true ) ,
209+ authenticateBearer : pluginAuthenticate ,
210+ } as unknown as RoleBaseAccessController ;
211+ const controller = installPlugin ( plugin , hostAuthenticate ) ;
212+
213+ await expect ( controller . authenticateBearer ( bearerRequest ( ADDITIONAL_API_KEY ) ) ) . resolves . toEqual (
214+ {
215+ ok : false ,
216+ status : 401 ,
217+ error : "Invalid API key" ,
218+ }
219+ ) ;
220+ expect ( pluginAuthenticate ) . not . toHaveBeenCalled ( ) ;
221+ } ) ;
222+
223+ it ( "keeps additional-key authentication in the no-plugin fallback controller" , async ( ) => {
224+ const fallbackAuthenticate = vi . fn ( async ( ) => additionalKeyResult ( [ "admin" ] ) ) ;
225+ const hostAuthenticate = vi . fn ( ) ;
226+ const fallback = {
227+ isUsingPlugin : vi . fn ( async ( ) => false ) ,
228+ authenticateBearer : fallbackAuthenticate ,
229+ } as unknown as RoleBaseAccessController ;
230+ const controller = installPlugin ( fallback , hostAuthenticate ) ;
231+
232+ await expect (
233+ controller . authenticateBearer ( bearerRequest ( ADDITIONAL_API_KEY ) )
234+ ) . resolves . toMatchObject ( { ok : true , subject : { type : "apiKey" } } ) ;
235+ expect ( fallbackAuthenticate ) . toHaveBeenCalledOnce ( ) ;
236+ expect ( hostAuthenticate ) . not . toHaveBeenCalled ( ) ;
237+ } ) ;
238+
126239 it ( "delegates API-key policy catalogue, preparation, and description" , async ( ) => {
127240 const presets = vi . fn ( async ( ) => [ ] ) ;
128241 const prepare = vi . fn ( async ( ) => ( {
0 commit comments