import { describe, expect, it } from "@effect/vitest";import { Cause, Effect, Exit, Ref, Schema } from "effect";import { HttpServerResponse } from "effect/unstable/http"; import { OAuthDiscoveryError, beginDynamicAuthorization, canonicalResourceUrl, discoverAuthorizationServerMetadata, discoverProtectedResourceMetadata, registerDynamicClient,} from "./oauth-discovery";import { serveTestHttpApp } from "./testing"; interface CapturedRequest { readonly method: string; readonly url: string; readonly headers: Readonly<Record<string, string>>; readonly body: string;} type Handler = (request: CapturedRequest, baseUrl: string) => HttpServerResponse.HttpServerResponse; const DcrRequestBody = Schema.Struct({ redirect_uris: Schema.Array(Schema.String), token_endpoint_auth_method: Schema.String, scope: Schema.optional(Schema.String), client_uri: Schema.optional(Schema.String),});const decodeDcrRequestBody = Schema.decodeUnknownSync(Schema.fromJsonString(DcrRequestBody)); const sendJson = (body: unknown, status = 200): HttpServerResponse.HttpServerResponse => HttpServerResponse.jsonUnsafe(body, { status }); const notFound = (): HttpServerResponse.HttpServerResponse => HttpServerResponse.empty({ status: 404 }); const serveOAuthFixture = (handler: Handler) => Effect.gen(function* () { const requests = yield* Ref.make<readonly CapturedRequest[]>([]); const baseUrlRef = { value: "" }; const server = yield* serveTestHttpApp((request) => Effect.gen(function* () { const body = yield* request.text; const captured = { method: request.method, url: request.url ?? "/", headers: request.headers, body, }; yield* Ref.update(requests, (all) => [...all, captured]); return handler(captured, baseUrlRef.value); }).pipe( Effect.catch(() => Effect.succeed(HttpServerResponse.text("oauth fixture failed", { status: 500 })), ), ), ); baseUrlRef.value = server.baseUrl; return { baseUrl: baseUrlRef.value, requests: Ref.get(requests), } as const; }); const withOAuthFixture = <A, E>( handler: Handler, use: (fixture: { readonly baseUrl: string; readonly requests: Effect.Effect<readonly CapturedRequest[]>; }) => Effect.Effect<A, E>,) => Effect.scoped( Effect.gen(function* () { const fixture = yield* serveOAuthFixture(handler); return yield* use(fixture); }), ); describe("canonicalResourceUrl", () => { it("lowercases scheme + host, drops trailing slash, fragment, and query", () => { expect(canonicalResourceUrl("https://API.Example.com/v1/mcp/")).toBe( "https://api.example.com/v1/mcp", ); expect(canonicalResourceUrl("HTTPS://api.example.com/v1/mcp?x=1#frag")).toBe( "https://api.example.com/v1/mcp", ); expect(canonicalResourceUrl("https://api.example.com/")).toBe("https://api.example.com"); });}); describe("discoverProtectedResourceMetadata", () => { it.effect("fetches RFC 9728 well-known metadata on the resource's origin", () => withOAuthFixture( (request, baseUrl) => { if (request.url === "/.well-known/oauth-protected-resource/graphql") { return notFound(); } if (request.url === "/.well-known/oauth-protected-resource") { return sendJson({ resource: baseUrl, authorization_servers: [baseUrl], scopes_supported: ["read"], }); } return notFound(); }, ({ baseUrl }) => Effect.gen(function* () { const result = yield* discoverProtectedResourceMetadata(`${baseUrl}/graphql`); expect(result).not.toBeNull(); expect(result!.metadata.authorization_servers?.[0]).toBe(baseUrl); expect(result!.metadataUrl).toBe(`${baseUrl}/.well-known/oauth-protected-resource`); }), ), ); it.effect("returns null when every well-known candidate 404s", () => withOAuthFixture( () => notFound(), ({ baseUrl }) => Effect.gen(function* () { const result = yield* discoverProtectedResourceMetadata(`${baseUrl}/graphql`); expect(result).toBeNull(); }), ), ); it.effect("surfaces malformed metadata bodies as OAuthDiscoveryError", () => withOAuthFixture( () => HttpServerResponse.text("not json", { status: 200, contentType: "application/json" }), ({ baseUrl }) => Effect.gen(function* () { const exit = yield* Effect.exit(discoverProtectedResourceMetadata(baseUrl)); expect(Exit.isFailure(exit)).toBe(true); if (!Exit.isFailure(exit)) return; const reason = exit.cause.reasons.find(Cause.isFailReason); expect(reason?.error).toBeInstanceOf(OAuthDiscoveryError); }), ), );}); describe("discoverAuthorizationServerMetadata", () => { it.effect("falls back to openid-configuration when oauth-authorization-server is absent", () => withOAuthFixture( (request, baseUrl) => { if (request.url === "/.well-known/oauth-authorization-server") { return notFound(); } if (request.url === "/.well-known/openid-configuration") { return sendJson({ issuer: baseUrl, authorization_endpoint: `${baseUrl}/authorize`, token_endpoint: `${baseUrl}/token`, code_challenge_methods_supported: ["S256"], response_types_supported: ["code"], }); } return notFound(); }, ({ baseUrl }) => Effect.gen(function* () { const result = yield* discoverAuthorizationServerMetadata(baseUrl); expect(result).not.toBeNull(); expect(result!.metadata.token_endpoint).toBe(`${baseUrl}/token`); expect(result!.metadataUrl.endsWith("openid-configuration")).toBe(true); }), ), ); it.effect("requires issuer + authorize + token endpoints", () => withOAuthFixture( () => sendJson({ issuer: "http://127.0.0.1" }), ({ baseUrl }) => Effect.gen(function* () { const exit = yield* Effect.exit(discoverAuthorizationServerMetadata(baseUrl)); expect(Exit.isFailure(exit)).toBe(true); }), ), ); it.effect("rejects unsupported authorization server endpoint URLs", () => withOAuthFixture( () => sendJson({ issuer: "http://example.com", authorization_endpoint: "javascript:alert(1)", token_endpoint: "http://example.com/token", response_types_supported: ["code"], }), ({ baseUrl }) => Effect.gen(function* () { const exit = yield* Effect.exit(discoverAuthorizationServerMetadata(baseUrl)); expect(Exit.isFailure(exit)).toBe(true); if (!Exit.isFailure(exit)) return; const reason = exit.cause.reasons.find(Cause.isFailReason); expect(reason?.error).toEqual( expect.objectContaining({ _tag: "OAuthDiscoveryError", message: expect.stringMatching(/must use https: or loopback http:/), }), ); }), ), ); it.effect("allows HTTP authorization server endpoints when the caller opts into local HTTP", () => withOAuthFixture( () => sendJson({ issuer: "http://example.com", authorization_endpoint: "http://example.com/authorize", token_endpoint: "http://example.com/token", response_types_supported: ["code"], }), ({ baseUrl }) => Effect.gen(function* () { const result = yield* discoverAuthorizationServerMetadata(baseUrl, { endpointUrlPolicy: { allowHttp: true }, }); expect(result).not.toBeNull(); expect(result!.metadata.authorization_endpoint).toBe("http://example.com/authorize"); expect(result!.metadata.token_endpoint).toBe("http://example.com/token"); }), ), );}); describe("registerDynamicClient", () => { it.effect("POSTs RFC 7591 metadata and parses the client information response", () => withOAuthFixture( (request) => { if (request.url !== "/register") { return notFound(); } return sendJson( { client_id: "generated-client-id", client_id_issued_at: 1_700_000_000, redirect_uris: ["https://app.example.com/cb"], token_endpoint_auth_method: "none", }, 201, ); }, ({ baseUrl, requests }) => Effect.gen(function* () { const info = yield* registerDynamicClient({ registrationEndpoint: `${baseUrl}/register`, metadata: { redirect_uris: ["https://app.example.com/cb"], client_name: "Executor", grant_types: ["authorization_code", "refresh_token"], response_types: ["code"], token_endpoint_auth_method: "none", }, }); expect(info.client_id).toBe("generated-client-id"); const call = (yield* requests)[0]!; expect(call.method).toBe("POST"); const body = decodeDcrRequestBody(call.body); expect(body.redirect_uris).toEqual(["https://app.example.com/cb"]); expect(body.token_endpoint_auth_method).toBe("none"); }), ), ); it.effect("treats HTTP 200 as success (Todoist-style non-conformance)", () => withOAuthFixture( () => sendJson({ client_id: "tdd_abc", redirect_uris: ["https://app.example.com/cb"], }), ({ baseUrl }) => Effect.gen(function* () { const info = yield* registerDynamicClient({ registrationEndpoint: `${baseUrl}/register`, metadata: { redirect_uris: ["https://app.example.com/cb"] }, }); expect(info.client_id).toBe("tdd_abc"); }), ), ); it.effect("surfaces AS error responses with the error body", () => withOAuthFixture( () => sendJson( { error: "invalid_client_metadata", error_description: "redirect_uris must be https", }, 400, ), ({ baseUrl }) => Effect.gen(function* () { const exit = yield* Effect.exit( registerDynamicClient({ registrationEndpoint: `${baseUrl}/register`, metadata: { redirect_uris: ["http://localhost/cb"] }, }), ); expect(Exit.isFailure(exit)).toBe(true); if (!Exit.isFailure(exit)) return; const reason = exit.cause.reasons.find(Cause.isFailReason); const error = reason?.error; expect(error).toEqual( expect.objectContaining({ _tag: "OAuthDiscoveryError", status: 400, message: expect.stringMatching(/invalid_client_metadata/), }), ); }), ), );}); describe("beginDynamicAuthorization", () => { it.effect("runs the full discovery + DCR + PKCE chain for a Railway-shaped endpoint", () => withOAuthFixture( (request, baseUrl) => { if (request.url === "/.well-known/oauth-protected-resource/graphql/v2") { return notFound(); } if (request.url === "/.well-known/oauth-protected-resource") { return sendJson({ resource: baseUrl, authorization_servers: [baseUrl], scopes_supported: ["openid", "profile", "email", "offline_access", "workspace:member"], }); } if (request.url === "/.well-known/oauth-authorization-server") { return sendJson({ issuer: baseUrl, authorization_endpoint: `${baseUrl}/oauth/auth`, token_endpoint: `${baseUrl}/oauth/token`, registration_endpoint: `${baseUrl}/oauth/register`, scopes_supported: ["openid", "profile", "email", "offline_access", "workspace:member"], response_types_supported: ["code"], code_challenge_methods_supported: ["S256"], }); } if (request.url === "/oauth/register") { return sendJson( { client_id: "dyn-client-42", redirect_uris: ["https://app.example/cb"], token_endpoint_auth_method: "none", }, 201, ); } return notFound(); }, ({ baseUrl }) => Effect.gen(function* () { const result = yield* beginDynamicAuthorization({ endpoint: `${baseUrl}/graphql/v2`, redirectUrl: "https://app.example/cb", state: "state-xyz", }); const url = new URL(result.authorizationUrl); expect(url.origin + url.pathname).toBe(`${baseUrl}/oauth/auth`); expect(url.searchParams.get("client_id")).toBe("dyn-client-42"); expect(url.searchParams.get("redirect_uri")).toBe("https://app.example/cb"); expect(url.searchParams.get("response_type")).toBe("code"); expect(url.searchParams.get("state")).toBe("state-xyz"); expect(url.searchParams.get("code_challenge_method")).toBe("S256"); expect(url.searchParams.get("resource")).toBe(baseUrl); expect(result.state.authorizationServerMetadata.token_endpoint).toBe( `${baseUrl}/oauth/token`, ); expect(result.state.resourceMetadata?.resource).toBe(baseUrl); }), ), ); it.effect("declares requested scopes in the DCR body when caller passes them explicitly", () => withOAuthFixture( (request, baseUrl) => { if (request.url === "/.well-known/oauth-protected-resource") { return notFound(); } if (request.url === "/.well-known/oauth-authorization-server") { return sendJson({ issuer: baseUrl, authorization_endpoint: `${baseUrl}/authorize`, token_endpoint: `${baseUrl}/token`, registration_endpoint: `${baseUrl}/register`, response_types_supported: ["code"], code_challenge_methods_supported: ["S256"], }); } if (request.url === "/register") { return sendJson( { client_id: "scope-client", redirect_uris: ["https://app/cb"], token_endpoint_auth_method: "none", }, 201, ); } return notFound(); }, ({ baseUrl, requests }) => Effect.gen(function* () { const result = yield* beginDynamicAuthorization({ endpoint: baseUrl, redirectUrl: "https://app/cb", state: "s", scopes: ["openid", "email", "offline_access"], }); const dcrCall = (yield* requests).find((request) => request.url === "/register")!; const body = decodeDcrRequestBody(dcrCall.body); expect(body.scope).toBe("openid email offline_access"); const authUrl = new URL(result.authorizationUrl); expect(authUrl.searchParams.get("scope")).toBe("openid email offline_access"); }), ), ); it.effect("requests only PRM scopes_supported when advertised (RFC 9728 §2 limited scope)", () => withOAuthFixture( (request, baseUrl) => { if (request.url === "/.well-known/oauth-protected-resource/mcp") { return sendJson({ resource: `${baseUrl}/mcp`, authorization_servers: [baseUrl], scopes_supported: ["mcp"], }); } if (request.url === "/.well-known/oauth-authorization-server") { return sendJson({ issuer: baseUrl, authorization_endpoint: `${baseUrl}/authorize`, token_endpoint: `${baseUrl}/token`, registration_endpoint: `${baseUrl}/register`, scopes_supported: ["openid", "profile", "email", "offline_access", "mcp"], response_types_supported: ["code"], code_challenge_methods_supported: ["S256"], }); } if (request.url === "/register") { return sendJson( { client_id: "prm-scope-client", redirect_uris: ["https://app/cb"], token_endpoint_auth_method: "none", }, 201, ); } return notFound(); }, ({ baseUrl, requests }) => Effect.gen(function* () { const result = yield* beginDynamicAuthorization({ endpoint: `${baseUrl}/mcp`, redirectUrl: "https://app/cb", state: "s", }); const dcrCall = (yield* requests).find((request) => request.url === "/register")!; const body = decodeDcrRequestBody(dcrCall.body); expect(body.scope).toBe("mcp"); const authUrl = new URL(result.authorizationUrl); expect(authUrl.searchParams.get("scope")).toBe("mcp"); }), ), ); it.effect("omits scope when only AS-level scopes_supported is advertised", () => withOAuthFixture( (request, baseUrl) => { if (request.url === "/.well-known/oauth-protected-resource") { return notFound(); } if (request.url === "/.well-known/oauth-authorization-server") { return sendJson({ issuer: baseUrl, authorization_endpoint: `${baseUrl}/authorize`, token_endpoint: `${baseUrl}/token`, registration_endpoint: `${baseUrl}/register`, scopes_supported: ["openid", "profile", "email", "offline_access"], response_types_supported: ["code"], code_challenge_methods_supported: ["S256"], }); } if (request.url === "/register") { return sendJson(Lines 1–500 of 983. Load more
import { describe, expect, it } from "@effect/vitest"; import { Cause, Effect, Exit, Ref, Schema } from "effect"; import { HttpServerResponse } from "effect/unstable/http"; import { OAuthDiscoveryError, beginDynamicAuthorization, canonicalResourceUrl, discoverAuthorizationServerMetadata, discoverProtectedResourceMetadata, registerDynamicClient, } from "./oauth-discovery"; import { serveTestHttpApp } from "./testing"; interface CapturedRequest { readonly method: string; readonly url: string; readonly headers: Readonly<Record<string, string>>; readonly body: string; } type Handler = (request: CapturedRequest, baseUrl: string) => HttpServerResponse.HttpServerResponse; const DcrRequestBody = Schema.Struct({ redirect_uris: Schema.Array(Schema.String), token_endpoint_auth_method: Schema.String, scope: Schema.optional(Schema.String), client_uri: Schema.optional(Schema.String), }); const decodeDcrRequestBody = Schema.decodeUnknownSync(Schema.fromJsonString(DcrRequestBody)); const sendJson = (body: unknown, status = 200): HttpServerResponse.HttpServerResponse => HttpServerResponse.jsonUnsafe(body, { status }); const notFound = (): HttpServerResponse.HttpServerResponse => HttpServerResponse.empty({ status: 404 }); const serveOAuthFixture = (handler: Handler) => Effect.gen(function* () { const requests = yield* Ref.make<readonly CapturedRequest[]>([]); const baseUrlRef = { value: "" }; const server = yield* serveTestHttpApp((request) => Effect.gen(function* () { const body = yield* request.text; const captured = { method: request.method, url: request.url ?? "/", headers: request.headers, body, }; yield* Ref.update(requests, (all) => [...all, captured]); return handler(captured, baseUrlRef.value); }).pipe( Effect.catch(() => Effect.succeed(HttpServerResponse.text("oauth fixture failed", { status: 500 })), ), ), ); baseUrlRef.value = server.baseUrl; return { baseUrl: baseUrlRef.value, requests: Ref.get(requests), } as const; }); const withOAuthFixture = <A, E>( handler: Handler, use: (fixture: { readonly baseUrl: string; readonly requests: Effect.Effect<readonly CapturedRequest[]>; }) => Effect.Effect<A, E>, ) => Effect.scoped( Effect.gen(function* () { const fixture = yield* serveOAuthFixture(handler); return yield* use(fixture); }), ); describe("canonicalResourceUrl", () => { it("lowercases scheme + host, drops trailing slash, fragment, and query", () => { expect(canonicalResourceUrl("https://API.Example.com/v1/mcp/")).toBe( "https://api.example.com/v1/mcp", ); expect(canonicalResourceUrl("HTTPS://api.example.com/v1/mcp?x=1#frag")).toBe( "https://api.example.com/v1/mcp", ); expect(canonicalResourceUrl("https://api.example.com/")).toBe("https://api.example.com"); }); }); describe("discoverProtectedResourceMetadata", () => { it.effect("fetches RFC 9728 well-known metadata on the resource's origin", () => withOAuthFixture( (request, baseUrl) => { if (request.url === "/.well-known/oauth-protected-resource/graphql") { return notFound(); } if (request.url === "/.well-known/oauth-protected-resource") { return sendJson({ resource: baseUrl, authorization_servers: [baseUrl], scopes_supported: ["read"], }); } return notFound(); }, ({ baseUrl }) => Effect.gen(function* () { const result = yield* discoverProtectedResourceMetadata(`${baseUrl}/graphql`); expect(result).not.toBeNull(); expect(result!.metadata.authorization_servers?.[0]).toBe(baseUrl); expect(result!.metadataUrl).toBe(`${baseUrl}/.well-known/oauth-protected-resource`); }), ), ); it.effect("returns null when every well-known candidate 404s", () => withOAuthFixture( () => notFound(), ({ baseUrl }) => Effect.gen(function* () { const result = yield* discoverProtectedResourceMetadata(`${baseUrl}/graphql`); expect(result).toBeNull(); }), ), ); it.effect("surfaces malformed metadata bodies as OAuthDiscoveryError", () => withOAuthFixture( () => HttpServerResponse.text("not json", { status: 200, contentType: "application/json" }), ({ baseUrl }) => Effect.gen(function* () { const exit = yield* Effect.exit(discoverProtectedResourceMetadata(baseUrl)); expect(Exit.isFailure(exit)).toBe(true); if (!Exit.isFailure(exit)) return; const reason = exit.cause.reasons.find(Cause.isFailReason); expect(reason?.error).toBeInstanceOf(OAuthDiscoveryError); }), ), ); }); describe("discoverAuthorizationServerMetadata", () => { it.effect("falls back to openid-configuration when oauth-authorization-server is absent", () => withOAuthFixture( (request, baseUrl) => { if (request.url === "/.well-known/oauth-authorization-server") { return notFound(); } if (request.url === "/.well-known/openid-configuration") { return sendJson({ issuer: baseUrl, authorization_endpoint: `${baseUrl}/authorize`, token_endpoint: `${baseUrl}/token`, code_challenge_methods_supported: ["S256"], response_types_supported: ["code"], }); } return notFound(); }, ({ baseUrl }) => Effect.gen(function* () { const result = yield* discoverAuthorizationServerMetadata(baseUrl); expect(result).not.toBeNull(); expect(result!.metadata.token_endpoint).toBe(`${baseUrl}/token`); expect(result!.metadataUrl.endsWith("openid-configuration")).toBe(true); }), ), ); it.effect("requires issuer + authorize + token endpoints", () => withOAuthFixture( () => sendJson({ issuer: "http://127.0.0.1" }), ({ baseUrl }) => Effect.gen(function* () { const exit = yield* Effect.exit(discoverAuthorizationServerMetadata(baseUrl)); expect(Exit.isFailure(exit)).toBe(true); }), ), ); it.effect("rejects unsupported authorization server endpoint URLs", () => withOAuthFixture( () => sendJson({ issuer: "http://example.com", authorization_endpoint: "javascript:alert(1)", token_endpoint: "http://example.com/token", response_types_supported: ["code"], }), ({ baseUrl }) => Effect.gen(function* () { const exit = yield* Effect.exit(discoverAuthorizationServerMetadata(baseUrl)); expect(Exit.isFailure(exit)).toBe(true); if (!Exit.isFailure(exit)) return; const reason = exit.cause.reasons.find(Cause.isFailReason); expect(reason?.error).toEqual( expect.objectContaining({ _tag: "OAuthDiscoveryError", message: expect.stringMatching(/must use https: or loopback http:/), }), ); }), ), ); it.effect("allows HTTP authorization server endpoints when the caller opts into local HTTP", () => withOAuthFixture( () => sendJson({ issuer: "http://example.com", authorization_endpoint: "http://example.com/authorize", token_endpoint: "http://example.com/token", response_types_supported: ["code"], }), ({ baseUrl }) => Effect.gen(function* () { const result = yield* discoverAuthorizationServerMetadata(baseUrl, { endpointUrlPolicy: { allowHttp: true }, }); expect(result).not.toBeNull(); expect(result!.metadata.authorization_endpoint).toBe("http://example.com/authorize"); expect(result!.metadata.token_endpoint).toBe("http://example.com/token"); }), ), ); }); describe("registerDynamicClient", () => { it.effect("POSTs RFC 7591 metadata and parses the client information response", () => withOAuthFixture( (request) => { if (request.url !== "/register") { return notFound(); } return sendJson( { client_id: "generated-client-id", client_id_issued_at: 1_700_000_000, redirect_uris: ["https://app.example.com/cb"], token_endpoint_auth_method: "none", }, 201, ); }, ({ baseUrl, requests }) => Effect.gen(function* () { const info = yield* registerDynamicClient({ registrationEndpoint: `${baseUrl}/register`, metadata: { redirect_uris: ["https://app.example.com/cb"], client_name: "Executor", grant_types: ["authorization_code", "refresh_token"], response_types: ["code"], token_endpoint_auth_method: "none", }, }); expect(info.client_id).toBe("generated-client-id"); const call = (yield* requests)[0]!; expect(call.method).toBe("POST"); const body = decodeDcrRequestBody(call.body); expect(body.redirect_uris).toEqual(["https://app.example.com/cb"]); expect(body.token_endpoint_auth_method).toBe("none"); }), ), ); it.effect("treats HTTP 200 as success (Todoist-style non-conformance)", () => withOAuthFixture( () => sendJson({ client_id: "tdd_abc", redirect_uris: ["https://app.example.com/cb"], }), ({ baseUrl }) => Effect.gen(function* () { const info = yield* registerDynamicClient({ registrationEndpoint: `${baseUrl}/register`, metadata: { redirect_uris: ["https://app.example.com/cb"] }, }); expect(info.client_id).toBe("tdd_abc"); }), ), ); it.effect("surfaces AS error responses with the error body", () => withOAuthFixture( () => sendJson( { error: "invalid_client_metadata", error_description: "redirect_uris must be https", }, 400, ), ({ baseUrl }) => Effect.gen(function* () { const exit = yield* Effect.exit( registerDynamicClient({ registrationEndpoint: `${baseUrl}/register`, metadata: { redirect_uris: ["http://localhost/cb"] }, }), ); expect(Exit.isFailure(exit)).toBe(true); if (!Exit.isFailure(exit)) return; const reason = exit.cause.reasons.find(Cause.isFailReason); const error = reason?.error; expect(error).toEqual( expect.objectContaining({ _tag: "OAuthDiscoveryError", status: 400, message: expect.stringMatching(/invalid_client_metadata/), }), ); }), ), ); }); describe("beginDynamicAuthorization", () => { it.effect("runs the full discovery + DCR + PKCE chain for a Railway-shaped endpoint", () => withOAuthFixture( (request, baseUrl) => { if (request.url === "/.well-known/oauth-protected-resource/graphql/v2") { return notFound(); } if (request.url === "/.well-known/oauth-protected-resource") { return sendJson({ resource: baseUrl, authorization_servers: [baseUrl], scopes_supported: ["openid", "profile", "email", "offline_access", "workspace:member"], }); } if (request.url === "/.well-known/oauth-authorization-server") { return sendJson({ issuer: baseUrl, authorization_endpoint: `${baseUrl}/oauth/auth`, token_endpoint: `${baseUrl}/oauth/token`, registration_endpoint: `${baseUrl}/oauth/register`, scopes_supported: ["openid", "profile", "email", "offline_access", "workspace:member"], response_types_supported: ["code"], code_challenge_methods_supported: ["S256"], }); } if (request.url === "/oauth/register") { return sendJson( { client_id: "dyn-client-42", redirect_uris: ["https://app.example/cb"], token_endpoint_auth_method: "none", }, 201, ); } return notFound(); }, ({ baseUrl }) => Effect.gen(function* () { const result = yield* beginDynamicAuthorization({ endpoint: `${baseUrl}/graphql/v2`, redirectUrl: "https://app.example/cb", state: "state-xyz", }); const url = new URL(result.authorizationUrl); expect(url.origin + url.pathname).toBe(`${baseUrl}/oauth/auth`); expect(url.searchParams.get("client_id")).toBe("dyn-client-42"); expect(url.searchParams.get("redirect_uri")).toBe("https://app.example/cb"); expect(url.searchParams.get("response_type")).toBe("code"); expect(url.searchParams.get("state")).toBe("state-xyz"); expect(url.searchParams.get("code_challenge_method")).toBe("S256"); expect(url.searchParams.get("resource")).toBe(baseUrl); expect(result.state.authorizationServerMetadata.token_endpoint).toBe( `${baseUrl}/oauth/token`, ); expect(result.state.resourceMetadata?.resource).toBe(baseUrl); }), ), ); it.effect("declares requested scopes in the DCR body when caller passes them explicitly", () => withOAuthFixture( (request, baseUrl) => { if (request.url === "/.well-known/oauth-protected-resource") { return notFound(); } if (request.url === "/.well-known/oauth-authorization-server") { return sendJson({ issuer: baseUrl, authorization_endpoint: `${baseUrl}/authorize`, token_endpoint: `${baseUrl}/token`, registration_endpoint: `${baseUrl}/register`, response_types_supported: ["code"], code_challenge_methods_supported: ["S256"], }); } if (request.url === "/register") { return sendJson( { client_id: "scope-client", redirect_uris: ["https://app/cb"], token_endpoint_auth_method: "none", }, 201, ); } return notFound(); }, ({ baseUrl, requests }) => Effect.gen(function* () { const result = yield* beginDynamicAuthorization({ endpoint: baseUrl, redirectUrl: "https://app/cb", state: "s", scopes: ["openid", "email", "offline_access"], }); const dcrCall = (yield* requests).find((request) => request.url === "/register")!; const body = decodeDcrRequestBody(dcrCall.body); expect(body.scope).toBe("openid email offline_access"); const authUrl = new URL(result.authorizationUrl); expect(authUrl.searchParams.get("scope")).toBe("openid email offline_access"); }), ), ); it.effect("requests only PRM scopes_supported when advertised (RFC 9728 §2 limited scope)", () => withOAuthFixture( (request, baseUrl) => { if (request.url === "/.well-known/oauth-protected-resource/mcp") { return sendJson({ resource: `${baseUrl}/mcp`, authorization_servers: [baseUrl], scopes_supported: ["mcp"], }); } if (request.url === "/.well-known/oauth-authorization-server") { return sendJson({ issuer: baseUrl, authorization_endpoint: `${baseUrl}/authorize`, token_endpoint: `${baseUrl}/token`, registration_endpoint: `${baseUrl}/register`, scopes_supported: ["openid", "profile", "email", "offline_access", "mcp"], response_types_supported: ["code"], code_challenge_methods_supported: ["S256"], }); } if (request.url === "/register") { return sendJson( { client_id: "prm-scope-client", redirect_uris: ["https://app/cb"], token_endpoint_auth_method: "none", }, 201, ); } return notFound(); }, ({ baseUrl, requests }) => Effect.gen(function* () { const result = yield* beginDynamicAuthorization({ endpoint: `${baseUrl}/mcp`, redirectUrl: "https://app/cb", state: "s", }); const dcrCall = (yield* requests).find((request) => request.url === "/register")!; const body = decodeDcrRequestBody(dcrCall.body); expect(body.scope).toBe("mcp"); const authUrl = new URL(result.authorizationUrl); expect(authUrl.searchParams.get("scope")).toBe("mcp"); }), ), ); it.effect("omits scope when only AS-level scopes_supported is advertised", () => withOAuthFixture( (request, baseUrl) => { if (request.url === "/.well-known/oauth-protected-resource") { return notFound(); } if (request.url === "/.well-known/oauth-authorization-server") { return sendJson({ issuer: baseUrl, authorization_endpoint: `${baseUrl}/authorize`, token_endpoint: `${baseUrl}/token`, registration_endpoint: `${baseUrl}/register`, scopes_supported: ["openid", "profile", "email", "offline_access"], response_types_supported: ["code"], code_challenge_methods_supported: ["S256"], }); } if (request.url === "/register") { return sendJson(