(
input: CreateConnectionInput,
)
| 3333 | // ------------------------------------------------------------------ |
| 3334 | |
| 3335 | const connectionsCreate = ( |
| 3336 | input: CreateConnectionInput, |
| 3337 | ): Effect.Effect< |
| 3338 | Connection, |
| 3339 | | IntegrationNotFoundError |
| 3340 | | CredentialProviderNotRegisteredError |
| 3341 | | InvalidConnectionInputError |
| 3342 | | StorageFailure |
| 3343 | > => |
| 3344 | Effect.gen(function* () { |
| 3345 | const name = connectionIdentifier(String(input.name)); |
| 3346 | // Typed (not StorageError) so the HTTP edge can answer 400 with the |
| 3347 | // reason instead of an opaque 500 — callers can act on it. |
| 3348 | if (input.owner === "user" && subject == null) { |
| 3349 | return yield* new InvalidConnectionInputError({ |
| 3350 | message: |
| 3351 | 'Cannot create a personal connection: this context has no user subject. Create it with owner "org", or connect as a signed-in user.', |
| 3352 | }); |
| 3353 | } |
| 3354 | const integrationRow = yield* findIntegrationRow(input.integration); |
| 3355 | if (!integrationRow) { |
| 3356 | return yield* new IntegrationNotFoundError({ |
| 3357 | slug: input.integration, |
| 3358 | }); |
| 3359 | } |
| 3360 | |
| 3361 | // Resolve the value origin(s) → one provider + an item_ids map (one entry |
| 3362 | // per named input). All of a connection's inputs share a single provider: |
| 3363 | // pasted inputs go to the default writable store, external `from` inputs to |
| 3364 | // their provider. Mixing pasted + external, or two external providers, is |
| 3365 | // rejected (the connection row carries one `provider`). |
| 3366 | const inputs = normalizeConnectionInputs(input); |
| 3367 | const pasted = inputs.filter((i) => "value" in i.origin); |
| 3368 | const external = inputs.filter((i) => "from" in i.origin); |
| 3369 | // A credentialed connection is born wired: it must reference at least |
| 3370 | // one credential input. An empty binding (no inputs at all — e.g. an |
| 3371 | // empty `values`/`inputs` map) is a credential with no credential: it |
| 3372 | // would persist, produce a full tool catalog, and then fail every |
| 3373 | // invocation with `connection_value_missing`. Reject it here — EXCEPT |
| 3374 | // for the no-auth template ("none"), where zero inputs and an empty |
| 3375 | // `item_ids` map are the canonical shape (public MCP servers; the UI |
| 3376 | // submits `values: {}` for them). OAuth connections are minted via |
| 3377 | // `mintOAuthConnection`, not this path; an external `from` reference |
| 3378 | // may resolve to null and is surfaced at invoke time, not here. |
| 3379 | const isNoAuth = String(input.template) === String(NO_AUTH_TEMPLATE); |
| 3380 | if (inputs.length === 0 && !isNoAuth) { |
| 3381 | return yield* new InvalidConnectionInputError({ |
| 3382 | message: "A connection must supply at least one credential input.", |
| 3383 | }); |
| 3384 | } |
| 3385 | let providerKey: string; |
| 3386 | const itemIds: Record<string, string> = {}; |
| 3387 | if (external.length > 0 && pasted.length > 0) { |
| 3388 | return yield* new InvalidConnectionInputError({ |
| 3389 | message: "A connection cannot mix pasted and external-provider inputs.", |
| 3390 | }); |
| 3391 | } |
| 3392 | if (external.length > 0) { |
no test coverage detected