| @@ -107,6 +107,8 @@ const serveMetadataServer = (config: { · 106 unmodified lines |
| 107 | 107 | | "error" |
| 108 | 108 | | null; |
| 109 | 109 | readonly authServerScopes?: readonly string[]; |
| 110 | + readonly authServerAuthorizationPath?: string; |
| 111 | + readonly authServerTokenPath?: string; |
| 110 | 112 | }) => |
| 111 | 113 | Effect.gen(function* () { |
| 112 | 114 | const baseUrlRef = { value: "" }; |
| @@ -133,8 +135,8 @@ const serveMetadataServer = (config: { · 20 unmodified lines |
| 133 | 135 | ) { |
| 134 | 136 | return HttpServerResponse.jsonUnsafe({ |
| 135 | 137 | issuer: base, |
| 136 | | − authorization_endpoint: `${base}/authorize`, |
| 137 | | − token_endpoint: `${base}/token`, |
| 138 | + authorization_endpoint: `${base}${config.authServerAuthorizationPath ?? "/authorize"}`, |
| 139 | + token_endpoint: `${base}${config.authServerTokenPath ?? "/token"}`, |
| 138 | 140 | response_types_supported: ["code"], |
| 139 | 141 | code_challenge_methods_supported: ["S256"], |
| 140 | 142 | scopes_supported: config.authServerScopes, |
| @@ -325,6 +327,49 @@ describe("oauth.start integration-driven scopes", () => { · 184 unmodified lines |
| 325 | 327 | ), |
| 326 | 328 | ); |
| 327 | 329 | |
| 330 | + it.effect("keeps declared scopes when same-origin metadata describes another OAuth surface", () => |
| 331 | + Effect.scoped( |
| 332 | + Effect.gen(function* () { |
| 333 | + const server = yield* serveMetadataServer({ |
| 334 | + authServerScopes: ["mcp:connect"], |
| 335 | + authServerAuthorizationPath: "/oauth/mcp", |
| 336 | + }); |
| 337 | + const plugins = [ |
| 338 | + memoryCredentialsPlugin(), |
| 339 | + makeScopePlugin({ scopes: ["file_content:read", "file_comments:write"] }), |
| 340 | + ] as const; |
| 341 | + const { executor } = yield* makeTestWorkspaceHarness({ plugins }); |
| 342 | + yield* executor.acme.seed(); |
| 343 | + |
| 344 | + yield* executor.oauth.createClient({ |
| 345 | + owner: "org", |
| 346 | + slug: CLIENT, |
| 347 | + authorizationUrl: server.authorizationEndpoint, |
| 348 | + tokenUrl: server.tokenEndpoint, |
| 349 | + grant: "authorization_code", |
| 350 | + clientId: "test-client", |
| 351 | + clientSecret: "test-secret", |
| 352 | + }); |
| 353 | + |
| 354 | + const started = yield* executor.oauth.start({ |
| 355 | + owner: "org", |
| 356 | + client: CLIENT, |
| 357 | + clientOwner: "org", |
| 358 | + name: ConnectionName.make("main"), |
| 359 | + integration: INTEG, |
| 360 | + template: TEMPLATE, |
| 361 | + }); |
| 362 | + expect(started.status).toBe("redirect"); |
| 363 | + if (started.status !== "redirect") return; |
| 364 | + |
| 365 | + expect(scopesFromAuthorizeUrl(started.authorizationUrl)).toEqual([ |
| 366 | + "file_content:read", |
| 367 | + "file_comments:write", |
| 368 | + ]); |
| 369 | + }), |
| 370 | + ), |
| 371 | + ); |
| 372 | + |
| 328 | 373 | it.effect( |
| 329 | 374 | "(d) for MCP, the resource's own scopes_supported wins over a divergent authorization server", |
| 330 | 375 | () => |