(
input: RegisterDynamicClientInput,
)
| 1387 | }); |
| 1388 | |
| 1389 | const registerDynamicClient = ( |
| 1390 | input: RegisterDynamicClientInput, |
| 1391 | ): Effect.Effect< |
| 1392 | OAuthClientSlug, |
| 1393 | OAuthRegisterDynamicError | OrgWriteDeniedError | StorageFailure |
| 1394 | > => |
| 1395 | Effect.gen(function* () { |
| 1396 | yield* deps.guardOrgWrite(input.owner); |
| 1397 | const issuer = canonicalDcrIssuer(input.issuer, input.registrationEndpoint); |
| 1398 | // Resolved before the reuse decision: a persisted client registered with |
| 1399 | // a DIFFERENT callback must not be reused (strict servers 400 the |
| 1400 | // authorize request), so the reuse lookup compares against this value. |
| 1401 | const flowRedirectUri = input.redirectUri ?? redirectUri ?? null; |
| 1402 | const reuse = yield* decideDcrClientReuse(input, issuer, flowRedirectUri); |
| 1403 | if (reuse.existingSlug !== null) return reuse.existingSlug; |
| 1404 | |
| 1405 | const slug = reuse.registrationSlug; |
| 1406 | // DCR registers our callback as the client's redirect_uri — fail loudly |
| 1407 | // if the executor has none rather than registering a localhost URL. |
| 1408 | if (flowRedirectUri == null) { |
| 1409 | return yield* new OAuthRegisterDynamicError({ |
| 1410 | message: REDIRECT_URI_REQUIRED_MESSAGE, |
| 1411 | }); |
| 1412 | } |
| 1413 | const authMethod = pickDcrAuthMethod(input.tokenEndpointAuthMethodsSupported); |
| 1414 | const information = yield* registerDynamicClientDcr( |
| 1415 | { |
| 1416 | registrationEndpoint: input.registrationEndpoint, |
| 1417 | metadata: { |
| 1418 | client_name: input.clientName, |
| 1419 | redirect_uris: [flowRedirectUri], |
| 1420 | grant_types: ["authorization_code", "refresh_token"], |
| 1421 | response_types: ["code"], |
| 1422 | token_endpoint_auth_method: authMethod, |
| 1423 | application_type: isLoopbackHttpUrl(flowRedirectUri) ? "native" : "web", |
| 1424 | scope: input.scopes.length > 0 ? input.scopes.join(" ") : undefined, |
| 1425 | }, |
| 1426 | }, |
| 1427 | { httpClientLayer, endpointUrlPolicy: deps.endpointUrlPolicy }, |
| 1428 | ).pipe( |
| 1429 | Effect.mapError((cause) => { |
| 1430 | // Some authorization servers (Vercel, and others that follow RFC 8252 |
| 1431 | // strictly) reject anonymous Dynamic Client Registration unless the |
| 1432 | // redirect URI is loopback (http://localhost or http://127.0.0.1). |
| 1433 | // Executor registers its browser origin, so any hosted, tailnet, or |
| 1434 | // LAN origin trips `invalid_redirect_uri`. Turn that opaque RFC code |
| 1435 | // into guidance the user can act on instead of the raw error. |
| 1436 | // oxlint-disable-next-line executor/no-unknown-error-message -- boundary: OAuthDiscoveryError carries a typed `message` |
| 1437 | const rawMessage = cause.message; |
| 1438 | const message = |
| 1439 | cause.error === "invalid_redirect_uri" && !isLoopbackHttpUrl(flowRedirectUri) |
| 1440 | ? `Automatic OAuth setup failed: this server only approves loopback redirect ` + |
| 1441 | `URLs (http://localhost or http://127.0.0.1) for automatic registration, but ` + |
| 1442 | `Executor is using ${flowRedirectUri}. Register an OAuth app manually with that ` + |
| 1443 | `redirect URL approved by the server, or run Executor on http://localhost.` |
| 1444 | : `Dynamic Client Registration failed: ${rawMessage}`; |
| 1445 | return new OAuthRegisterDynamicError({ message }); |
| 1446 | }), |
nothing calls this directly
no test coverage detected