(
input: OAuthStartInput,
)
| 1043 | // start — begin a flow through a client to mint a connection. |
| 1044 | // ----------------------------------------------------------------------- |
| 1045 | const start = ( |
| 1046 | input: OAuthStartInput, |
| 1047 | ): Effect.Effect<ConnectResult, OAuthStartError | StorageFailure> => |
| 1048 | Effect.gen(function* () { |
| 1049 | const keys = yield* Effect.try({ |
| 1050 | try: () => deps.ownedKeys(input.owner), |
| 1051 | catch: (cause) => |
| 1052 | new StorageError({ |
| 1053 | message: "Cannot start OAuth flow for owner without a subject", |
| 1054 | cause, |
| 1055 | }), |
| 1056 | }); |
| 1057 | // Sharing is one-directional (org → members): a Workspace (org) connection |
| 1058 | // cannot be backed by a member's private (user) app. The connection owner |
| 1059 | // and the app owner are otherwise independent — a Personal connection |
| 1060 | // through a shared Workspace app is the supported cross-owner case. |
| 1061 | if (input.owner === "org" && input.clientOwner === "user") { |
| 1062 | return yield* new OAuthStartError({ |
| 1063 | message: "A Workspace connection must use a Workspace app.", |
| 1064 | }); |
| 1065 | } |
| 1066 | // Load the app by its EXPLICIT owner (the caller knows it — no derivation). |
| 1067 | // The connection is still minted under `input.owner`. Storage visibility |
| 1068 | // policy hides apps the actor cannot see, so a wrong owner yields null. |
| 1069 | const client = yield* loadClient(input.clientOwner, input.client); |
| 1070 | if (!client) { |
| 1071 | return yield* new OAuthStartError({ |
| 1072 | message: `OAuth client not found: ${input.client}`, |
| 1073 | }); |
| 1074 | } |
| 1075 | |
| 1076 | // Normalize the name the same way the mint stores it, so the free-name |
| 1077 | // guard below compares against the exact stored form. |
| 1078 | const requestedName = connectionIdentifier(String(input.name)); |
| 1079 | // newConnection: resolve the requested name to a FREE one against the |
| 1080 | // stored rows (not a client-side, policy-filtered view), so a second |
| 1081 | // untyped connect mints `personalGmail2` instead of silently re-minting |
| 1082 | // the first account's row. Reconnects omit the flag and keep targeting |
| 1083 | // their existing row. Bounded: a pathological owner with 1000 same-named |
| 1084 | // connections fails loudly rather than scanning forever. |
| 1085 | let name = requestedName; |
| 1086 | if (input.newConnection === true) { |
| 1087 | let suffix = 2; |
| 1088 | while ( |
| 1089 | yield* deps.connectionNameTaken({ |
| 1090 | owner: input.owner, |
| 1091 | integration: input.integration, |
| 1092 | name, |
| 1093 | }) |
| 1094 | ) { |
| 1095 | if (suffix > 1000) { |
| 1096 | return yield* new OAuthStartError({ |
| 1097 | message: `No free connection name derivable from ${input.name}.`, |
| 1098 | }); |
| 1099 | } |
| 1100 | name = ConnectionName.make(`${String(requestedName)}${suffix}`); |
| 1101 | suffix++; |
| 1102 | } |
nothing calls this directly
no test coverage detected