# 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.

When the other side has to act first, `hold` says so in place of the ways of connecting: an admin there must approve the app (who, and where they do it, with Check again), or their policy blocks it (what is blocked, and who can unblock it). Each has a way back (`onBack`). When a hold comes or goes while the person is working here, focus moves to the hold's heading, or back to the environment's.

- 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: bidi, button, environment-band, input, locale, locked, relative-time, team, tokens, use-announcer, utils.
npm packages: lucide-react@^1.67.0.

## Example

```tsx
"use client"

import * as React from "react"
import { EnvironmentConnect, type ConnectionHold } from "@/components/candor/environment-connect"

export default function Example() {
  // Signing in with the provider here stands for an install that an admin on the other side has to approve: the
  // panel says who and where, until Check again finds it approved or Back returns to the ways of connecting.
  const [hold, setHold] = React.useState<ConnectionHold | undefined>(undefined)
  return (
    <EnvironmentConnect
      className="max-w-md"
      environment={{ id: "e1", name: "Shop", detail: "staging", production: false, hue: 2, connection: "expired" }}
      oauth={{
        provider: "the provider",
        onStart: () => setHold({ kind: "waiting", who: "An admin of the Shop account", where: "Settings › Connected apps", askedAt: new Date().toISOString() }),
      }}
      hold={hold}
      onCheckAgain={async () => {
        await new Promise((r) => setTimeout(r, 500))
        setHold((h) => (h?.kind === "waiting" ? { ...h, checkedAt: new Date().toISOString() } : h))
      }}
      onBack={() => setHold(undefined)}
      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.
- `hold?: ConnectionHold | undefined`: The other side has to act first: an admin there must approve the app, or their policy blocks it. While it is set, a panel saying who has to act, and where or what, stands in place of the ways of connecting.
- `onCheckAgain?: (() => void | Promise<void>) | undefined`: Shows Check again while `hold` is waiting; the button waits on a returned promise. Update `hold` with what the check found.
- `onBack?: (() => void) | undefined`: Shows the way back from a hold, to the ways of connecting: clear `hold` here. Pass it whenever you pass `hold`, or the person has no way back.
- `className?: string | undefined`: Classes, merged with Candor's.

## 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}
- `formLabel`: {Environment}
- `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.
- `waitingTitle`: Waiting for approval on their side
- `waitingNote`: Nothing connects until they approve the app.
- `blockedTitle`: Blocked by their policy
- `blockedNote`: Nothing connects while their policy blocks it.
- `approveWho`: Who has to approve
- `approveWhere`: Where
- `blockedWhat`: What’s blocked
- `unblocker`: Who can unblock it
- `askedAt`: Asked {when}
- `holdCheckedAt`: Last checked {when}
- `checkAgain`: Check again
- `checking`: Checking…
- `waitCheck`: Wait for the check to finish.
- `checkFailed`: Checking failed. Try again in a moment.
- `back`: Back to connecting

## Types

### ConnectionHold

The other system has to act before the connection can finish, as the app's server reports it: an admin there must approve the app (`waiting`), or their policy blocks it (`blocked`). Every string is the app's words, or the other system's, and is drawn as text.

```ts
{
  kind: "waiting";
  /** Who on the other side has to approve the app, e.g. "An admin of the Shop account". */
  who: string;
  /** Where they approve it, e.g. "Settings › Connected apps". */
  where: string;
  /** When the app asked, as an ISO time. Left out, no time is shown. */
  askedAt?: string | undefined;
  /** When the app last checked whether they approved, as an ISO time. Left out, no time is shown. */
  checkedAt?: string | undefined;
} | {
  kind: "blocked";
  /** What their policy blocks, e.g. "Apps from outside the company signing in with OAuth". */
  what: string;
  /** Who can unblock it, e.g. "Their security team". */
  who: string;
}
```

### 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/candor/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; LocaleProvider's `nouns` 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`: its fixed strings in `strings`, its nouns in `nouns`; entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
