Skip to content

Commit e47a9f1

Browse files
committed
Test v2's connected-account callbacks and the shipped v2 edge settings
1 parent 706f299 commit e47a9f1

2 files changed

Lines changed: 250 additions & 37 deletions

File tree

‎apps/cloud/src/edge/marketing.test.ts‎

Lines changed: 197 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
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

185187
describe("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

211232
describe("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

Comments
 (0)