# Environment connect

Connect an environment: sign in with its provider (OAuth), or enter the credentials it needs, then test the connection and see what the test found. The header names the environment in its own colour, as `environment-band` draws it, warns when it's production, and says whether it's connected now. Open it from `environment-band`'s `onReconnect`. Secret fields never show a saved value back; left blank on an existing connection, they keep it. With `requireTest`, Connect stays locked, saying why, until a test with the current values has passed.

- Registry name: `environment-connect` (`@candor/environment-connect`)
- Tier: Pro. Installing it needs a Pro, Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/environment-connect

## Install

```bash
npx candor-ui add environment-connect
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/environment-connect`.
It brings in: button, environment-band, input, locale, locked, team, terms, tokens, utils.
npm packages: lucide-react@^1.67.0.

## Example

```tsx
"use client"

import { EnvironmentConnect } from "@/components/environment-connect"

export default function Example() {
  return (
    <EnvironmentConnect
      className="max-w-md"
      environment={{ id: "e1", name: "Shop", detail: "staging", production: false, hue: 2, connection: "expired" }}
      oauth={{ provider: "the provider", onStart: () => {} }}
      fields={[
        { id: "url", label: "Instance URL", type: "url", placeholder: "https://shop.example.com" },
        { id: "clientId", label: "Client ID" },
        { id: "secret", label: "Client secret", type: "secret" },
      ]}
      values={{ url: "https://staging.shop.example.com", clientId: "agents-staging" }}
      onTest={async (v) => {
        await new Promise((r) => setTimeout(r, 500))
        return v.url?.startsWith("https://")
          ? { ok: true, checkedAt: new Date().toISOString(), detail: "Signed in as agents-staging; 3 of 3 permissions granted." }
          : { ok: false, checkedAt: new Date().toISOString(), reason: "The instance didn’t answer at that address." }
      }}
      onConnect={async () => {
        await new Promise((r) => setTimeout(r, 400))
      }}
      requireTest
    />
  )
}
```

## <EnvironmentConnect>

Props:

- `environment: Environment`: The environment being connected: its name, colour, production flag and current connection.
- `fields?: ConnectionField[] | undefined` (default `[]`): The credentials to ask for. Leave it empty for OAuth only.
- `values?: Record<string, string> | undefined`: Saved values for the non-secret fields, by field id. Secret values are never passed back to the browser.
- `oauth?: { provider: string; onStart: () => void } | undefined`: Shows a button to connect through the provider's own sign-in.
- `onTest?: ((values: Record<string, string>) => Promise<ConnectionTestResult>) | undefined`: Shows Test connection. Called with the values entered; resolve with what the server found. If it throws or rejects, the form says the test couldn't run, with the error's message as the reason.
- `onConnect?: ((values: Record<string, string>) => void | Promise<void>) | undefined`: Called with the values entered when the person connects. The button waits on a returned promise.
- `requireTest?: boolean | undefined` (default `false`): Connect stays locked until a test with the current values passed. Without `onTest` no test can run, so Connect stays locked and says so.
- `busy?: boolean | undefined` (default `false`): The app is connecting: the button says so, and stays locked, saying why, until it is done.
- `error?: string | undefined`: Why connecting failed, in the app's words.
- `className?: string | undefined`

## Strings (`environmentConnect`)

The fixed words, English by default. Override any of them under `environmentConnect` in `<LocaleProvider strings={{ environmentConnect: { … } }}>`; the table is exported as `environmentConnectStrings`.

- `heading`: Connect {name}
- `nameDetail`: {name} · {detail}
- `production`: Production: what agents do here reaches real users.
- `connected`: Connected
- `expired`: The connection expired. Connect again to keep agents working here.
- `notConnected`: Not connected yet
- `oauth`: Connect with {provider}
- `oauthHint`: You sign in at {provider} and come back here. Your password stays with {provider}.
- `or`: or enter credentials
- `optional`: {label} (optional)
- `secretKept`: Leave blank to keep the saved value.
- `test`: Test connection
- `testing`: Testing…
- `testPassed`: Connection works
- `testFailed`: Connection failed
- `testNotRun`: Couldn’t run the test
- `testNotRunWhy`: The test gave no result, so the connection is unchecked.
- `checkedAt`: checked {when}
- `stale`: The values changed since this test.
- `connect`: Connect
- `save`: Save connection
- `saving`: Saving…
- `connectFailed`: Saving the connection failed.
- `needsField`: Fill in {label}.
- `needsTest`: Test the connection first.
- `needsPassingTest`: The last test failed. Fix the values and test again.
- `cantTest`: This connection has to pass a test first, and there’s no way to run one here. Ask whoever set it up.
- `waitTest`: Wait for the test to finish.
- `waitSave`: Wait for the connection to be saved.

## Types

### ConnectionField

A credential the environment needs, as the app defines it.

```ts
{
  id: string;
  /** What it is, e.g. "Instance URL" or "Client secret". */
  label: string;
  /** `secret` masks it and never shows a saved value back. Defaults to text. */
  type?: "text" | "url" | "secret" | undefined;
  /** A line under the field: where to find it. */
  hint?: string | undefined;
  placeholder?: string | undefined;
  /** Connect works without it. */
  optional?: boolean | undefined;
}
```

### ConnectionTestResult

What a connection test found, as the app's server reports it.

```ts
| { ok: true; checkedAt: string; detail?: string | undefined }
  | { ok: false; checkedAt: string; reason: string }
```

## Types from other items

The props above take these. Each is defined by another item; import it from the path shown.

### Environment

Import: `import type { Environment } from "@/lib/team"` (item `team`, https://candor.themethodic.com/docs/team.md)

Where agents act: a CRM instance, a database, a repository, a project. The app names them; TermsProvider sets what they're called ("project", "workspace").

```ts
{
  id: string;
  /** The name shown everywhere, e.g. "Shop". */
  name: string;
  /** What tells same-named ones apart, e.g. "dev-2" or "staging". */
  detail?: string | undefined;
  /** Changes here reach real users, so components warn and ask for more care. */
  production: boolean;
  /** A chart slot (1–5) so each environment keeps one colour everywhere it appears. */
  hue?: 1 | 2 | 3 | 4 | 5 | undefined;
  connection: "connected" | "expired" | "not-connected";
}
```

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider` (fixed strings) and `TermsProvider` (nouns); entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
