77 marketingProxyRequest ,
88 parseV2Edge ,
99 v2EdgeResponse ,
10+ type V2EdgeEnv ,
1011 type V2Service ,
1112} from "./marketing" ;
1213
@@ -151,8 +152,9 @@ describe("isV2Path", () => {
151152 "/github" ,
152153 "/.well-known/agent-skills" ,
153154 "/.well-known/agent-skillset/index.json" ,
154- // Not yet: connected-account callback and marketing .
155+ // The connected-account callback goes by its state, not its path .
155156 "/api/oauth/callback" ,
157+ // Not yet: marketing.
156158 "/pricing" ,
157159 // v1 dashboard, org pages and MCP, including org slugs that look similar.
158160 "/" ,
@@ -184,32 +186,59 @@ describe("isSignUpPath", () => {
184186
185187describe ( "parseV2Edge" , ( ) => {
186188 const service : V2Service = { fetch : ( ) => Promise . resolve ( new Response ( null ) ) } ;
189+ const settings = {
190+ V2 : service ,
191+ V2_SIGN_UP_URL : "https://app.executor.sh/login?mode=signup" ,
192+ V2_OAUTH_STATE_PREFIX : "x2." ,
193+ } ;
194+
195+ it ( "is off when no setting is present" , ( ) => {
196+ expect ( parseV2Edge ( { } ) ) . toBeNull ( ) ;
197+ } ) ;
187198
188- it ( "is off when neither setting is present" , ( ) => {
189- expect ( parseV2Edge ( undefined , undefined ) ) . toBeNull ( ) ;
199+ it ( "refuses any setting set without the others" , ( ) => {
200+ expect ( typeof parseV2Edge ( { ...settings , V2 : undefined } ) ) . toBe ( "string" ) ;
201+ expect ( typeof parseV2Edge ( { ...settings , V2_SIGN_UP_URL : undefined } ) ) . toBe ( "string" ) ;
202+ expect ( typeof parseV2Edge ( { ...settings , V2_OAUTH_STATE_PREFIX : undefined } ) ) . toBe ( "string" ) ;
203+ expect ( typeof parseV2Edge ( { V2_OAUTH_STATE_PREFIX : "x2." } ) ) . toBe ( "string" ) ;
190204 } ) ;
191205
192- it ( "refuses a binding or sign-up URL set without the other" , ( ) => {
193- expect ( typeof parseV2Edge ( undefined , "https://v2.executor.sh/login?mode=signup" ) ) . toBe (
206+ it ( "refuses a sign-up URL that is not absolute http(s)" , ( ) => {
207+ expect ( typeof parseV2Edge ( { ...settings , V2_SIGN_UP_URL : "/login?mode=signup" } ) ) . toBe (
208+ "string" ,
209+ ) ;
210+ expect ( typeof parseV2Edge ( { ...settings , V2_SIGN_UP_URL : "javascript:alert(1)" } ) ) . toBe (
194211 "string" ,
195212 ) ;
196- expect ( typeof parseV2Edge ( service , undefined ) ) . toBe ( "string" ) ;
197213 } ) ;
198214
199- it ( "refuses a sign-up URL that is not absolute http(s)" , ( ) => {
200- expect ( typeof parseV2Edge ( service , "/login?mode=signup" ) ) . toBe ( "string" ) ;
201- expect ( typeof parseV2Edge ( service , "javascript:alert(1)" ) ) . toBe ( "string" ) ;
215+ // v1's states are base64url (raw, or the org-wrapped JSON encoding), so a
216+ // prefix made only of base64url characters could claim a v1 callback.
217+ it ( "refuses a state prefix that a v1 state could start with" , ( ) => {
218+ for ( const prefix of [ "" , "x2" , "x2-" , "x2_" , "eyJ" , "x2 ." , "x2.%" , "x2./" ] ) {
219+ expect ( typeof parseV2Edge ( { ...settings , V2_OAUTH_STATE_PREFIX : prefix } ) ) . toBe ( "string" ) ;
220+ }
202221 } ) ;
203222
204- it ( "parses both settings" , ( ) => {
205- const edge = parseV2Edge ( service , "https://v2.executor.sh/login?mode=signup" ) ;
223+ it ( "parses all settings" , ( ) => {
224+ const edge = parseV2Edge ( settings ) ;
206225 if ( edge === null || typeof edge === "string" ) return expect . unreachable ( "settings must parse" ) ;
207- expect ( edge . signUpUrl . href ) . toBe ( "https://v2.executor.sh/login?mode=signup" ) ;
226+ expect ( edge . signUpUrl . href ) . toBe ( "https://app.executor.sh/login?mode=signup" ) ;
227+ expect ( edge . oauthStatePrefix ) . toBe ( "x2." ) ;
228+ expect ( typeof parseV2Edge ( { ...settings , V2_OAUTH_STATE_PREFIX : "v2~" } ) ) . toBe ( "object" ) ;
208229 } ) ;
209230} ) ;
210231
211232describe ( "v2EdgeResponse" , ( ) => {
212233 const SIGN_UP_URL = "https://v2.executor.sh/login?mode=signup" ;
234+ const STATE_PREFIX = "x2." ;
235+
236+ /** The edge's settings with `service` as v2's Worker. */
237+ const settings = ( service : V2Service , signUpUrl = SIGN_UP_URL ) : V2EdgeEnv => ( {
238+ V2 : service ,
239+ V2_SIGN_UP_URL : signUpUrl ,
240+ V2_OAUTH_STATE_PREFIX : STATE_PREFIX ,
241+ } ) ;
213242
214243 /** A v2 service that records what it receives and answers with `respond`. */
215244 const recordingService = ( respond : ( request : Request ) => Promise < Response > | Response ) => {
@@ -227,7 +256,7 @@ describe("v2EdgeResponse", () => {
227256 const { received, service } = recordingService ( ( ) => new Response ( null ) ) ;
228257
229258 for ( const url of [ "https://executor.sh/sign-up" , "https://executor.sh/signup?ref=docs" ] ) {
230- const response = await v2EdgeResponse ( new Request ( url ) , service , SIGN_UP_URL ) ;
259+ const response = await v2EdgeResponse ( new Request ( url ) , settings ( service ) ) ;
231260 expect ( response ?. status ) . toBe ( 302 ) ;
232261 expect ( response ?. headers . get ( "location" ) ) . toBe ( SIGN_UP_URL ) ;
233262 }
@@ -237,8 +266,10 @@ describe("v2EdgeResponse", () => {
237266 it ( "follows the configured sign-up URL" , async ( ) => {
238267 const response = await v2EdgeResponse (
239268 new Request ( "https://executor.sh/signup" ) ,
240- { fetch : ( ) => Promise . resolve ( new Response ( null ) ) } ,
241- "https://app.executor.sh/sign-up" ,
269+ settings (
270+ { fetch : ( ) => Promise . resolve ( new Response ( null ) ) } ,
271+ "https://app.executor.sh/sign-up" ,
272+ ) ,
242273 ) ;
243274 expect ( response ?. headers . get ( "location" ) ) . toBe ( "https://app.executor.sh/sign-up" ) ;
244275 } ) ;
@@ -248,8 +279,7 @@ describe("v2EdgeResponse", () => {
248279 expect (
249280 v2EdgeResponse (
250281 new Request ( "https://executor.sh/sign-up" , { method : "POST" } ) ,
251- service ,
252- SIGN_UP_URL ,
282+ settings ( service ) ,
253283 ) ,
254284 ) . toBeNull ( ) ;
255285 } ) ;
@@ -258,32 +288,28 @@ describe("v2EdgeResponse", () => {
258288 const { received, service } = recordingService ( ( ) => new Response ( null ) ) ;
259289 const response = await v2EdgeResponse (
260290 new Request ( "https://executor.sh/sign-up" ) ,
261- service ,
262- "/relative" ,
291+ settings ( service , "/relative" ) ,
263292 ) ;
264293 expect ( response ?. status ) . toBe ( 500 ) ;
265294 expect (
266- v2EdgeResponse ( new Request ( "https://executor.sh/acme/mcp" ) , service , "/relative" ) ,
295+ v2EdgeResponse ( new Request ( "https://executor.sh/acme/mcp" ) , settings ( service , "/relative" ) ) ,
267296 ) . toBeNull ( ) ;
268297 expect ( received ) . toHaveLength ( 0 ) ;
269298 } ) ;
270299
271300 it ( "is off without settings" , ( ) => {
272- expect (
273- v2EdgeResponse ( new Request ( "https://executor.sh/sign-up" ) , undefined , undefined ) ,
274- ) . toBeNull ( ) ;
301+ expect ( v2EdgeResponse ( new Request ( "https://executor.sh/sign-up" ) , { } ) ) . toBeNull ( ) ;
275302 } ) ;
276303
277304 it ( "only acts on executor.sh" , ( ) => {
278305 const { service } = recordingService ( ( ) => new Response ( null ) ) ;
279306 expect (
280- v2EdgeResponse ( new Request ( "http://executor-cloud.localhost/sign-up" ) , service , SIGN_UP_URL ) ,
307+ v2EdgeResponse ( new Request ( "http://executor-cloud.localhost/sign-up" ) , settings ( service ) ) ,
281308 ) . toBeNull ( ) ;
282309 expect (
283310 v2EdgeResponse (
284311 new Request ( "https://v2.executor.sh/api/auth/callback/google" ) ,
285- service ,
286- SIGN_UP_URL ,
312+ settings ( service ) ,
287313 ) ,
288314 ) . toBeNull ( ) ;
289315 } ) ;
@@ -293,12 +319,11 @@ describe("v2EdgeResponse", () => {
293319 expect (
294320 v2EdgeResponse (
295321 new Request ( "https://executor.sh/api/auth/callback?code=c" ) ,
296- service ,
297- SIGN_UP_URL ,
322+ settings ( service ) ,
298323 ) ,
299324 ) . toBeNull ( ) ;
300325 expect (
301- v2EdgeResponse ( new Request ( "https://executor.sh/gitlab/x/info/refs" ) , service , SIGN_UP_URL ) ,
326+ v2EdgeResponse ( new Request ( "https://executor.sh/gitlab/x/info/refs" ) , settings ( service ) ) ,
302327 ) . toBeNull ( ) ;
303328 expect ( received ) . toHaveLength ( 0 ) ;
304329 } ) ;
@@ -314,8 +339,7 @@ describe("v2EdgeResponse", () => {
314339
315340 const response = await v2EdgeResponse (
316341 new Request ( "https://executor.sh/api/auth/callback/google?code=c&state=s" ) ,
317- service ,
318- SIGN_UP_URL ,
342+ settings ( service ) ,
319343 ) ;
320344
321345 expect ( response ?. status ) . toBe ( 302 ) ;
@@ -340,8 +364,7 @@ describe("v2EdgeResponse", () => {
340364 "x-forwarded-proto" : "http" ,
341365 } ,
342366 } ) ,
343- service ,
344- SIGN_UP_URL ,
367+ settings ( service ) ,
345368 ) ;
346369
347370 const forwarded = received [ 0 ] ;
@@ -362,8 +385,7 @@ describe("v2EdgeResponse", () => {
362385
363386 const response = await v2EdgeResponse (
364387 new Request ( "https://executor.sh/git/acme/tools/info/refs" ) ,
365- service ,
366- SIGN_UP_URL ,
388+ settings ( service ) ,
367389 ) ;
368390
369391 expect ( response ) . toBe ( upstream ) ;
@@ -402,12 +424,150 @@ describe("v2EdgeResponse", () => {
402424 // @ts -expect-error -- Node's fetch needs `duplex` for a stream body; workerd does not.
403425 duplex : "half" ,
404426 } ) ,
405- service ,
406- SIGN_UP_URL ,
427+ settings ( service ) ,
407428 ) ;
408429
409430 expect ( received [ 0 ] ?. method ) . toBe ( "POST" ) ;
410431 expect ( received [ 0 ] ?. headers . get ( "content-type" ) ) . toBe ( "application/x-git-receive-pack-request" ) ;
411432 expect ( await response ?. text ( ) ) . toBe ( "first-pack-chunk|second-pack-chunk|true" ) ;
412433 } ) ;
413434} ) ;
435+
436+ // v1 and v2 share `/api/oauth/callback` on executor.sh. v2 starts its state
437+ // with a fixed prefix; the edge reads only the query's `state` to decide.
438+ describe ( "v2EdgeResponse connected-account callback" , ( ) => {
439+ const settings = ( service : V2Service ) : V2EdgeEnv => ( {
440+ V2 : service ,
441+ V2_SIGN_UP_URL : "https://app.executor.sh/login?mode=signup" ,
442+ V2_OAUTH_STATE_PREFIX : "x2." ,
443+ } ) ;
444+
445+ const recordingService = ( ) => {
446+ const received : Request [ ] = [ ] ;
447+ const service : V2Service = {
448+ fetch : async ( request ) => {
449+ received . push ( request ) ;
450+ return new Response ( null , {
451+ status : 302 ,
452+ headers : { location : `https://app.executor.sh${ new URL ( request . url ) . search } ` } ,
453+ } ) ;
454+ } ,
455+ } ;
456+ return { received, service } ;
457+ } ;
458+
459+ it ( "forwards a callback whose state carries v2's prefix, query unchanged" , async ( ) => {
460+ const { received, service } = recordingService ( ) ;
461+ const callback = "https://executor.sh/api/oauth/callback?code=c&state=x2.abc123" ;
462+
463+ const response = await v2EdgeResponse (
464+ new Request ( callback , { headers : { cookie : "wos-session=sealed" } } ) ,
465+ settings ( service ) ,
466+ ) ;
467+
468+ expect ( response ?. status ) . toBe ( 302 ) ;
469+ expect ( received ) . toHaveLength ( 1 ) ;
470+ expect ( received [ 0 ] ?. url ) . toBe ( callback ) ;
471+ expect ( received [ 0 ] ?. redirect ) . toBe ( "manual" ) ;
472+ expect ( received [ 0 ] ?. headers . has ( "cookie" ) ) . toBe ( false ) ;
473+ } ) ;
474+
475+ it ( "reads a percent-encoded prefix as the prefix" , async ( ) => {
476+ const { received, service } = recordingService ( ) ;
477+
478+ await v2EdgeResponse (
479+ new Request ( "https://executor.sh/api/oauth/callback?state=x2%2Eabc&code=c" ) ,
480+ settings ( service ) ,
481+ ) ;
482+
483+ expect ( received ) . toHaveLength ( 1 ) ;
484+ } ) ;
485+
486+ it ( "forwards v2's provider errors, which carry the state but no code" , async ( ) => {
487+ const { received, service } = recordingService ( ) ;
488+
489+ await v2EdgeResponse (
490+ new Request ( "https://executor.sh/api/oauth/callback?error=access_denied&state=x2.abc" ) ,
491+ settings ( service ) ,
492+ ) ;
493+
494+ expect ( received ) . toHaveLength ( 1 ) ;
495+ } ) ;
496+
497+ const v1Callbacks = [
498+ // v1's raw state and its org-wrapped base64url JSON state.
499+ "https://executor.sh/api/oauth/callback?code=c&state=Q2xpZW50U3RhdGUxMjM0NTY3ODkw" ,
500+ "https://executor.sh/api/oauth/callback?code=c&state=eyJzdGF0ZSI6InMiLCJvcmdTbHVnIjoiYWNtZSJ9" ,
501+ // No state, or an empty one.
502+ "https://executor.sh/api/oauth/callback?code=c" ,
503+ "https://executor.sh/api/oauth/callback?code=c&state=" ,
504+ "https://executor.sh/api/oauth/callback" ,
505+ // Lookalikes: the prefix without its dot, another case, inside the
506+ // value, or in another parameter.
507+ "https://executor.sh/api/oauth/callback?code=c&state=x2abc" ,
508+ "https://executor.sh/api/oauth/callback?code=c&state=x2-abc" ,
509+ "https://executor.sh/api/oauth/callback?code=c&state=X2.abc" ,
510+ "https://executor.sh/api/oauth/callback?code=c&state=ax2.abc" ,
511+ "https://executor.sh/api/oauth/callback?code=c&state=%20x2.abc" ,
512+ "https://executor.sh/api/oauth/callback?code=x2.abc&state=v1state" ,
513+ "https://executor.sh/api/oauth/callback?code=c&xstate=x2.abc" ,
514+ // Only the first state counts.
515+ "https://executor.sh/api/oauth/callback?state=v1state&state=x2.abc" ,
516+ // Other paths with a v2 state.
517+ "https://executor.sh/api/oauth/callback/?state=x2.abc" ,
518+ "https://executor.sh/api/oauth/callbacks?state=x2.abc" ,
519+ "https://executor.sh/api/oauth/callback/extra?state=x2.abc" ,
520+ "https://executor.sh/acme/api/oauth/callback?state=x2.abc" ,
521+ ] ;
522+ for ( const url of v1Callbacks ) {
523+ it ( `leaves ${ new URL ( url ) . pathname } ${ new URL ( url ) . search } with v1` , ( ) => {
524+ const { received, service } = recordingService ( ) ;
525+ expect ( v2EdgeResponse ( new Request ( url ) , settings ( service ) ) ) . toBeNull ( ) ;
526+ expect ( received ) . toHaveLength ( 0 ) ;
527+ } ) ;
528+ }
529+
530+ it ( "decides from the query without reading a posted body" , ( ) => {
531+ const { received, service } = recordingService ( ) ;
532+ const body = new ReadableStream < Uint8Array > ( {
533+ pull : ( ) => expect . unreachable ( "the edge must not read the body" ) ,
534+ } ) ;
535+
536+ expect (
537+ v2EdgeResponse (
538+ new Request ( "https://executor.sh/api/oauth/callback" , {
539+ method : "POST" ,
540+ headers : { "content-type" : "application/x-www-form-urlencoded" } ,
541+ body,
542+ // @ts -expect-error -- Node's fetch needs `duplex` for a stream body; workerd does not.
543+ duplex : "half" ,
544+ } ) ,
545+ settings ( service ) ,
546+ ) ,
547+ ) . toBeNull ( ) ;
548+ expect ( received ) . toHaveLength ( 0 ) ;
549+ } ) ;
550+
551+ it ( "only acts on executor.sh" , ( ) => {
552+ const { received, service } = recordingService ( ) ;
553+ expect (
554+ v2EdgeResponse (
555+ new Request ( "https://v2.executor.sh/api/oauth/callback?state=x2.abc" ) ,
556+ settings ( service ) ,
557+ ) ,
558+ ) . toBeNull ( ) ;
559+ expect ( received ) . toHaveLength ( 0 ) ;
560+ } ) ;
561+
562+ it ( "leaves every callback with v1 when the settings are absent or broken" , ( ) => {
563+ const { received, service } = recordingService ( ) ;
564+ const request = ( ) => new Request ( "https://executor.sh/api/oauth/callback?state=x2.abc" ) ;
565+
566+ expect ( v2EdgeResponse ( request ( ) , { } ) ) . toBeNull ( ) ;
567+ expect ( v2EdgeResponse ( request ( ) , { ...settings ( service ) , V2_SIGN_UP_URL : "/x" } ) ) . toBeNull ( ) ;
568+ expect (
569+ v2EdgeResponse ( request ( ) , { ...settings ( service ) , V2_OAUTH_STATE_PREFIX : "x2" } ) ,
570+ ) . toBeNull ( ) ;
571+ expect ( received ) . toHaveLength ( 0 ) ;
572+ } ) ;
573+ } ) ;
0 commit comments