(
input: OAuthStartInput,
)
| 1030 | // start — begin a flow through a client to mint a connection. |
| 1031 | // ----------------------------------------------------------------------- |
| 1032 | const start = ( |
| 1033 | input: OAuthStartInput, |
| 1034 | ): Effect.Effect<ConnectResult, OAuthStartError | StorageFailure> => |
| 1035 | Effect.gen(function* () { |
| 1036 | const keys = yield* Effect.try({ |
| 1037 | try: () => deps.ownedKeys(input.owner), |
| 1038 | catch: (cause) => |
| 1039 | new StorageError({ |
| 1040 | message: "Cannot start OAuth flow for owner without a subject", |
| 1041 | cause, |
| 1042 | }), |
| 1043 | }); |
| 1044 | // Sharing is one-directional (org → members): a Workspace (org) connection |
| 1045 | // cannot be backed by a member's private (user) app. The connection owner |
| 1046 | // and the app owner are otherwise independent — a Personal connection |
| 1047 | // through a shared Workspace app is the supported cross-owner case. |
| 1048 | if (input.owner === "org" && input.clientOwner === "user") { |
| 1049 | return yield* new OAuthStartError({ |
| 1050 | message: "A Workspace connection must use a Workspace app.", |
| 1051 | }); |
| 1052 | } |
| 1053 | // Load the app by its EXPLICIT owner (the caller knows it — no derivation). |
| 1054 | // The connection is still minted under `input.owner`. Storage visibility |
| 1055 | // policy hides apps the actor cannot see, so a wrong owner yields null. |
| 1056 | const client = yield* loadClient(input.clientOwner, input.client); |
| 1057 | if (!client) { |
| 1058 | return yield* new OAuthStartError({ |
| 1059 | message: `OAuth client not found: ${input.client}`, |
| 1060 | }); |
| 1061 | } |
| 1062 | |
| 1063 | // Declared scopes win (driven by the selected auth template). MCP-style |
| 1064 | // integrations declare none and discover them from the client's protected |
| 1065 | // resource / authorization server metadata at connect. |
| 1066 | const scopePolicy = yield* deps |
| 1067 | .resolveOAuthScopePolicy(input.integration, input.template) |
| 1068 | .pipe( |
| 1069 | Effect.mapError( |
| 1070 | (cause) => |
| 1071 | new OAuthStartError({ |
| 1072 | // oxlint-disable-next-line executor/no-unknown-error-message -- boundary: StorageFailure carries a typed `message` field |
| 1073 | message: `Failed to resolve OAuth scope policy: ${cause.message}`, |
| 1074 | }), |
| 1075 | ), |
| 1076 | ); |
| 1077 | const requestedScopes = |
| 1078 | scopePolicy.kind === "discover" |
| 1079 | ? yield* discoverScopesForResource(client.resource).pipe( |
| 1080 | Effect.mapError( |
| 1081 | (cause) => |
| 1082 | new OAuthStartError({ |
| 1083 | // oxlint-disable-next-line executor/no-unknown-error-message -- boundary: OAuthDiscoveryError carries a typed `message` field |
| 1084 | message: `Failed to discover OAuth scopes: ${cause.message}`, |
| 1085 | }), |
| 1086 | ), |
| 1087 | ) |
| 1088 | : dedupeScopes(scopePolicy.scopes); |
| 1089 |
nothing calls this directly
no test coverage detected