(
input: RegisterDynamicClientInput,
)
| 807 | }); |
| 808 | |
| 809 | const registerDynamicClient = ( |
| 810 | input: RegisterDynamicClientInput, |
| 811 | ): Effect.Effect<OAuthClientSlug, OAuthRegisterDynamicError | StorageFailure> => |
| 812 | Effect.gen(function* () { |
| 813 | const issuer = canonicalDcrIssuer(input.issuer, input.registrationEndpoint); |
| 814 | const reuse = yield* decideDcrClientReuse(input, issuer); |
| 815 | if (reuse.existingSlug !== null) return reuse.existingSlug; |
| 816 | |
| 817 | const slug = reuse.registrationSlug; |
| 818 | const flowRedirectUri = input.redirectUri ?? redirectUri; |
| 819 | // DCR registers our callback as the client's redirect_uri — fail loudly |
| 820 | // if the executor has none rather than registering a localhost URL. |
| 821 | if (flowRedirectUri == null) { |
| 822 | return yield* new OAuthRegisterDynamicError({ |
| 823 | message: REDIRECT_URI_REQUIRED_MESSAGE, |
| 824 | }); |
| 825 | } |
| 826 | const authMethod = pickDcrAuthMethod(input.tokenEndpointAuthMethodsSupported); |
| 827 | const information = yield* registerDynamicClientDcr( |
| 828 | { |
| 829 | registrationEndpoint: input.registrationEndpoint, |
| 830 | metadata: { |
| 831 | client_name: input.clientName, |
| 832 | redirect_uris: [flowRedirectUri], |
| 833 | grant_types: ["authorization_code", "refresh_token"], |
| 834 | response_types: ["code"], |
| 835 | token_endpoint_auth_method: authMethod, |
| 836 | scope: input.scopes.length > 0 ? input.scopes.join(" ") : undefined, |
| 837 | }, |
| 838 | }, |
| 839 | { httpClientLayer, endpointUrlPolicy: deps.endpointUrlPolicy }, |
| 840 | ).pipe( |
| 841 | Effect.mapError((cause) => { |
| 842 | // Some authorization servers (Vercel, and others that follow RFC 8252 |
| 843 | // strictly) reject anonymous Dynamic Client Registration unless the |
| 844 | // redirect URI is loopback (http://localhost or http://127.0.0.1). |
| 845 | // Executor registers its browser origin, so any hosted, tailnet, or |
| 846 | // LAN origin trips `invalid_redirect_uri`. Turn that opaque RFC code |
| 847 | // into guidance the user can act on instead of the raw error. |
| 848 | // oxlint-disable-next-line executor/no-unknown-error-message -- boundary: OAuthDiscoveryError carries a typed `message` |
| 849 | const rawMessage = cause.message; |
| 850 | const message = |
| 851 | cause.error === "invalid_redirect_uri" && !isLoopbackHttpUrl(flowRedirectUri) |
| 852 | ? `Automatic OAuth setup failed: this server only approves loopback redirect ` + |
| 853 | `URLs (http://localhost or http://127.0.0.1) for automatic registration, but ` + |
| 854 | `Executor is using ${flowRedirectUri}. Register an OAuth app manually with that ` + |
| 855 | `redirect URL approved by the server, or run Executor on http://localhost.` |
| 856 | : `Dynamic Client Registration failed: ${rawMessage}`; |
| 857 | return new OAuthRegisterDynamicError({ message }); |
| 858 | }), |
| 859 | ); |
| 860 | |
| 861 | // Persist the minted client. DCR-minted public clients have no secret; we |
| 862 | // store "" so the PKCE-only token exchange omits `client_secret`. |
| 863 | // Confidential DCR clients keep the returned secret in the credential |
| 864 | // provider. The persisted grant is interactive authorization_code. |
| 865 | // `input.scopes` was already sent to the AS at registration above; the |
| 866 | // stored client carries no scope set (the integration drives requests). |
nothing calls this directly
no test coverage detected