# Logins

Keep your users' sign-ins to websites in isolance. The Logins connector holds each of your users' logins, and your workspace's own: their passwords, authenticator codes, passkeys and saved sign-ins. isolance signs in with them inside the browsers it runs for your sandboxes' AI tools (see [Browser](/docs/browser)), and adds a password or an authenticator code to command-line tools' requests on their way out of a sandbox. No secret ever enters a sandbox or reaches an AI, unless a login lets the AI read its password.

Your app manages each user's logins through the API, one user at a time, by your own ID for them, such as `user_123`: it adds their logins, sends them a link to sign in themselves, and hears when a site signed them out. Your users need no isolance account.

This guide covers the console, the CLI and the API. It assumes you know [Connectors](/docs/connectors) and [Browser](/docs/browser). The CLI examples assume you've set up `caas` (see [The CLI](/docs/cli)); the API examples send an API key as `$CAAS_API_KEY` (see [The API](/docs/api)). [The TypeScript SDK](/docs/sdk#your-users) has the same as typed methods.

## Add Logins

Only owners add it. In the [console](https://console.isolance.com/), open **Connectors**, choose **Add connector**, and pick **Logins** under **Pick a tool**. It's added at once. A workspace has one Logins connector, which holds every site's logins.

```sh
caas connectors add logins
```

With the API, `POST /v1/connectors` with `{ "catalog": "logins" }`. Its ID is `logins`, unless another connector had that ID first; `caas connectors list` shows it.

Its **Reaches** picks the sandboxes that use logins by their labels, as any connector's does (see [Reaches](/docs/connectors#reaches)); a new Logins connector reaches every sandbox. A sandbox it doesn't reach still has the browser, signed in to nothing. Removing the connector deletes every login, with its passkeys and saved sign-in, and closes every browser of your workspace.

## A login

A login is one account on one website, of one of your users or of your workspace:

| Field | What it is |
| --- | --- |
| `site` | The website, as a host such as `github.com`, with no `https://` or path. It covers the host and every name under it, so `github.com` covers `gist.github.com`. Never a domain anyone can register names under, such as `co.uk` or `github.io` |
| `username` | The account name, as the site shows it; `null` for a login that signs in with a passkey alone until the passkey names it |
| `name` | Your own label for it, or `null` |
| `methods` | How isolance signs in, in order: `passkey`, `typed` (isolance types the password; nothing shows it to the AI) or `readable` (the AI may read the password too). `["passkey", "typed"]` unless you say; `[]` signs in by hand only. `typed` and `readable` don't go together |
| `browser` | Which browser signs in to it: `native`, isolance's own browsers (the default), or `browserbase`, your Browserbase account (see [Run on your Browserbase account](/docs/browser#run-on-your-browserbase-account)) |
| `signInUrl` | Where signing in starts, an `https://` address on the site. Without it, `https://<site>/` |
| `check` | `{ "url", "text" }`: a page and a text on it that show only when signed in, such as `{ "url": "https://github.com/settings/profile", "text": "Public profile" }`. Without it, a page that asks for no account name, password, code or passkey counts as signed in |
| `password` | The password. Sent, never shown back |
| `code` | The authenticator setup key: the text under the QR code the site shows when you turn on an authenticator app, or the `otpauth://totp/…` link in it. Sent, never shown back |
| `passkeys` | `{ "signer", "creators" }`: who signs and who may make one (see [Passkeys](#passkeys)), and the passkeys themselves |
| `deliveries` | Where command-line tools get its password or code (see [Command-line tools](#command-line-tools)) |

**Whose login a sandbox uses.** A sandbox started for a user uses that user's logins; a sandbox started without a user uses your workspace's. Never another user's, and never your workspace's for a user, as for every connector (see [Whose account a sandbox uses](/docs/connectors#whose-account-a-sandbox-uses)).

A user's logins are under `/v1/users/<user>/logins`; your workspace's under `/v1/connectors/logins/account/logins`, with the same routes and fields.

## Add a login for a user

From your app, with an API key that has `sandbox:write`:

```sh
curl https://api.caas.sh/v1/users/user_123/logins \
  -H "Authorization: Bearer $CAAS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{ "site": "github.com", "username": "ana@acme.com", "methods": ["passkey", "typed"],
        "password": "…", "code": "otpauth://totp/GitHub:ana?secret=JBSWY3DPEHPK3PXP&issuer=GitHub" }'
```

The answer, `201`, is the login, which never carries the password or the code:

```json
{ "id": "lg_4f2a91c03b7d6e15", "site": "github.com", "username": "ana@acme.com", "name": null,
  "methods": ["passkey", "typed"], "browser": "native", "signInUrl": null, "check": null,
  "password": { "savedAt": 1792800000000, "savedBy": "api key backend" },
  "code": { "addedAt": 1792800000000, "digits": 6, "periodSeconds": 30 },
  "passkeys": { "signer": "caas", "creators": ["user", "platform", "agent"], "items": [] },
  "session": null, "deliveries": [], "live": null,
  "createdAt": 1792800000000, "createdBy": "api key backend", "updatedAt": 1792800000000, "lastUsedAt": null }
```

A user has one login per site and account name: a second is `409`. Don't have the password? Add the login without it and [send your user a link](#send-your-user-a-link), whose page signs them in.

In the console, a user's page (**Connectors**, then **Users**) has a **Logins** card: each login's site, account, how it signs in, which browser, its passkeys and saved sign-in, with **Edit**, **Send a link**, **Sign in by hand**, **Check now** and **Remove** in its menu, and **Add login**. Your workspace's logins are on the Logins connector's page, under **The workspace's logins**, where owners add them the same way.

With the CLI, secrets come only from standard input, never as options: one per command, or the password then the setup key on the next line.

```sh
caas logins add --user user_123 --site github.com --username ana@acme.com --methods passkey,typed --password-stdin < password.txt
caas logins list --user user_123
```

## How a login signs in

When the AI calls `login_sign_in`, isolance opens the site's sign-in page and checks whether it's already signed in, then tries the login's `methods` in order until the page is signed in:

- **`passkey`**, when the login has passkeys: isolance answers the passkey request the page makes, or presses its passkey button and answers that.
- **`typed`**: isolance types the account name and password into the page and submits it, then the current authenticator code when the site asks for one. The AI never sees either.
- **`readable`**: the same, and the AI may also read the password and the current code (`login_password`, `login_code`), such as to type them in a terminal or an app. Choose it only where the AI needs them: isolance then keeps none of that login's secrets from it.

The first that works ends it, and the sign-in is saved for the next browser. When none works, the AI gets the page as it stands, and can type the fields the site asks for with `login_fill` or ask a person (see [When a sandbox asks for a person](#when-a-sandbox-asks-for-a-person)). A login with `methods: []` is signed in only by a person, and kept as a saved sign-in.

isolance types and answers only into pages none of the AI's scripts, routes or storage changes has touched, and saves no sign-in of a site they reached from that browser (see [Signing in](/docs/browser#signing-in)).

## Passwords and authenticator codes

A password or setup key is kept in isolance's credential store and never shown again: a login shows only when it was saved and by whom, such as "Saved Oct 24 by owner@acme.com". An `otpauth://totp/` link gives its code's digits, period and algorithm; a bare setup key gets the usual 6 digits every 30 seconds.

- **Replace** one by sending it again with `PATCH /v1/users/<user>/logins/<login>`, and **remove** it with `"password": null` or `"code": null`.
- A change of the account name, the password or the setup key replaces all three at once, so send the ones the login keeps along with it; isolance keeps no copy to fill them in from (`400` names the one missing). Changing only the password of a login that also has a setup key isn't possible yet.
- A change applies from the next sign-in. A sign-in under way finishes with what it read, and the saved sign-in it made stays.
- Nothing shows a code: isolance types it where a site asks for one, adds it to a command-line tool's request, or gives it to the AI only for a `readable` login.

## Passkeys

A login can have several passkeys, listed under it with their ID, their credential ID as the site lists it (so you can match the site's own list), their name, when and how each was made, when it was last used, and how many times.

- **Making one.** A passkey is made only on the site's own page, in a browser isolance runs, when the site offers to add one: by the AI in its browser, by your user in their link's live view, or by your team or your app in a live view. `passkeys.creators` says who may: any of `agent`, `user` and `platform`, all three unless you say. A site with no login gets no passkey: add a login for it first (with `"methods": ["passkey"]` for a passkey alone). When a person finishes a sandbox's sign-in in a live view, whoever opens it may add one if the login's creators include `user` or `platform`.
- **Who signs** (`passkeys.signer`). With `caas`, the default, isolance answers each of the site's passkey requests itself, and the passkey's key never leaves isolance's credential store. With `browser`, isolance puts the key in the browser for the length of its sign-in step and takes it out after, for a site that doesn't work with the first way.
- **Only the real site.** isolance answers a passkey request only from the login's site or a name under it, or from another site the login's site itself lists as its own, and only in a page none of the AI's scripts or routes has touched. A look-alike site gets nothing.
- **What a site sees.** Every sign-in says the person was verified: your app's request that started the sandbox or the link is that verification. isolance's passkeys are kept in isolance, not on a device, as synced passkeys are. A site that refuses them, such as one that takes only passkeys bound to a device, may work with signer `browser`.
- **Rename** one with `PATCH …/logins/<login>/passkeys/<passkey>` and `{ "name" }`. **Remove** one with `DELETE …/passkeys/<passkey>`: isolance stops using it from the site's next request, but the site still lists it until you remove it there too, which the answer says.

## Saved sign-ins

A login's saved sign-in is the site's cookies and storage after a sign-in, so the next browser starts signed in and doesn't sign in again.

- **Saved** after a sign-in, at `login_save`, when a browser closes, and when a person chooses **Done** in a live view.
- **Uploaded** from your own browser automation: `PUT …/logins/<login>/session` with `{ "state": … }`, in Playwright's storage state form, as `await context.storageState()` gives it. Cookies of other sites are dropped, and the answer counts them in `dropped`. Up to 1 MiB.
- **Status**: `saved`, `signed_out` (a site signed it out, or a person marked it so) or `expired` (its last cookie ran out). The next sign-in replaces it.
- **Removed** with `DELETE …/session`, which marks it signed out.
- **Two browsers at once.** Two sandboxes of a user that use the same login each start from the same saved sign-in; the later save wins.

On Browserbase, a user's saved sign-ins live in their Browserbase profile, shared by all their Browserbase browsers and saved when each closes. Nothing can be uploaded into it (`409`): sign in by hand or send a link instead. Removing a Browserbase login's saved sign-in, or the login itself, closes the user's Browserbase browsers and starts their next one on a new profile, signed out of every site there, as Browserbase keeps one sign-in for all of them; the answer says so. Replacing or removing your Browserbase key, or removing the Browserbase connector, does the same for every user, since a new key may be another Browserbase project's.

Removing a login closes every browser using it at once, so no AI stays signed in with it.

## Send your user a link

A login's link lets your user sign in to the site themselves, on a page isolance hosts: your app never handles their password.

```sh
curl https://api.caas.sh/v1/users/user_123/logins/link \
  -H "Authorization: Bearer $CAAS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "site": "github.com", "returnUrl": "https://app.acme.com/settings" }'
```

`POST /v1/users/<user>/logins/link` with `{ "site" }` makes the login, or takes the user's one login there; `POST /v1/users/<user>/logins/<login>/link` makes a link for a login you have. The answer, `201`, is `{ "login", "connectUrl" }`. Send your user to `connectUrl`:

1. **Sign in to github.com** opens a live view: a browser isolance runs, at the site's sign-in page, which your user drives. They sign in, add a passkey if the site offers one and the login's creators include `user`, and choose **Done**. A live view of isolance's own browsers opens in a new tab; Browserbase's shows in the page.
2. Or, under **Or enter your password**, they type their account name, password, and an authenticator setup key if they have one.
3. The page says "github.com is connected" and sends them to your `returnUrl` with `login` and `status` added: `?login=lg_4f2a91c03b7d6e15&status=connected`.

A link works until it's used or a newer link for the same login replaces it, in any browser. If a sandbox is waiting for a person on that login, the link's page shows that sandbox's browser (see [When a sandbox asks for a person](#when-a-sandbox-asks-for-a-person)).

```sh
caas logins link --site github.com --user user_123 --return-url https://app.acme.com/settings
```

In the console, choose **Send a link** in the login's menu on the user's page.

## Sign in by hand

A live view is a browser isolance opens at a login's sign-in page for a person to drive: someone on your team, or your app on your user's behalf.

- `POST …/logins/<login>/live` opens it and answers `201` with `{ "live" }`: its `url`, `expiresAt` and `embeddable`. The person signs in, and adds a passkey if the login's creators include `platform`.
- `POST …/live/done` saves the sign-in when the site is signed in, and closes the browser, answering the login, whose `live` says what it found: `signedIn`, `saved` (on Browserbase the profile keeps the sign-in when the browser closes, so `false` here) and `reason`, why nothing was saved when isolance's browser wouldn't. `DELETE …/live` ends it without saving. A login has one live view at a time (`409`).
- A view's address can be opened for up to an hour on isolance's browsers, or six hours on Browserbase; one nobody finishes then closes, and its browser time counts until it would have idled out (20 minutes after it opened, on isolance's browsers).
- In the console, **Sign in by hand** in the login's menu opens it, with **Done** and **Cancel**. With the CLI, `caas logins live LOGIN --user USER` prints its address, then saves the sign-in when you press Enter, or cancels at Ctrl-C.

**Guard the address.** Whoever has a live view's `url` drives that browser: they can go anywhere in it and see every page, until **Done**, a cancel or the browser's end, and its time bills to you. isolance shows it only in the answer that opened it and in single reads of the login by owners, operators and API keys with `sandbox:write` (for your workspace's logins, owners only), never in lists, to viewers or in logs. If your app passes it on, give it only to the person who is to sign in, on a page of your own they're signed in to, never log it, and end the view when they leave. For your users, prefer their [link](#send-your-user-a-link): isolance hosts the page, it serves only that login, and your app never sees the address. A view with `embeddable: true` (Browserbase's) can be shown inside your own page; one of isolance's own browsers opens in a tab of its own.

## When a sandbox asks for a person

When an AI calls `login_handoff`, such as for a site that sends a code to a phone, the login's `live` becomes `{ "status": "waiting", "from": { "sandbox" }, "message", "url", "expiresAt" }`. The person sees the page the AI left. Your app then shows the person the live view, or sends your user the login's link, whose page shows it. While the view is open, the AI's browser tools in that browser wait, and the browser doesn't close for being idle; when the person chooses **Done**, the sign-in is saved when the site is signed in, and the AI goes on, reading where it stands with `login_status`. A site the AI's scripts ran on in that browser keeps no sign-in from it, so there **Done** saves nothing and the login's `live.reason` says why. A view nobody opens lapses (`ended`). The AI's message to the person is up to 256 characters. Only a sandbox the Logins connector picks can ask, as only those use logins.

A sandbox without a user asks for a person on your workspace's login: its address goes only to owners, in the console or a read of the login, never into the sandbox. The sandbox only ever sees `waiting`, then `done` or `ended`.

## When a site signs a user out

isolance sees that a site signed a saved sign-in out when it next uses the login: a sign-in or a check finds it signed out, or the site expires its sign-in cookies while a browser runs. It marks the saved sign-in `signed_out`, and the user's page shows **Signed out**.

A site that signs someone out on its own servers, such as after a password reset elsewhere or a device removed, while no browser uses the login, tells nobody: isolance sees it only at the login's next use. To know sooner, check:

```sh
curl -X POST https://api.caas.sh/v1/users/user_123/logins/lg_4f2a91c03b7d6e15/check \
  -H "Authorization: Bearer $CAAS_API_KEY"
```

A check opens a browser for the login, puts its saved sign-in back, sees whether the site is signed in, and closes, answering `{ "signedIn", "checkedAt", "session" }`. It bills as any browser. isolance never checks on its own: how often is yours, from your own scheduler. In the console, **Check now** in the login's menu; with the CLI, `caas logins check LOGIN --user USER`.

## Command-line tools

A login's `deliveries` add its password, account name or current authenticator code to a header of the HTTPS requests a sandbox sends to a host, on their way out, as a connector's key is added:

```json
{ "deliveries": [{ "host": "registry.npmjs.org", "header": "npm-otp", "value": "code" }] }
```

With that on a user's npm login, `npm publish` in their sandbox sends the code npm asks for without the sandbox ever holding the setup key. Each delivery is `{ "host", "header", "value", "scheme" }`:

- `value` is `password`, `username` or `code` (which needs a setup key).
- `scheme` is `raw` (the value as it is, the default), `bearer` (`Bearer <value>`) or `basic` (the account name and password as HTTP Basic authentication). A code goes `raw`.
- `host` can't be one of isolance's or one another connector covers, and the header follows a connector's rules (see [Add an API](/docs/connectors#add-an-api)).

A sandbox started for a user gets that user's logins' deliveries, one without a user your workspace's. They count toward the sandbox's 16 egress rules and 32 connector hosts, with its connectors: a login that doesn't fit beside those before it is left out whole, and the sandbox's log names it. Like connectors, a change reaches a running sandbox at its next start.

## Who can do what

| What | Who |
| --- | --- |
| See logins, their passkeys and saved sign-ins | Everyone in the workspace, and API keys with `sandbox:read` |
| Add, change and remove a user's logins, send links, open live views and see their addresses, and check | Owners and operators, and API keys with `sandbox:write` |
| The same for your workspace's logins | Owners, asked afresh |
| Use a login | The sandboxes of its user (your workspace's: sandboxes without a user) that the Logins connector reaches |

While a workspace is suspended, changes are refused. The audit log records every use of a login's secrets, every passkey signing included: `login.create`, `login.update`, `login.delete`, `login.link`, `login.check`, `login.live.open`, `login.live.done`, `login.live.cancel`, `login.session.save`, `login.session.delete`, `login.sign_in`, `login.fill`, `login.read` (the AI read a `readable` login's password or code), `login.handoff`, `login.session.end` (a site signed a saved sign-in out), `passkey.create`, `passkey.sign`, `passkey.update` and `passkey.delete`. What a sandbox's AI did carries the sandbox and its user as who did it, never the person or key that started the sandbox; what a user did through their link carries that user. No record carries a password, code, key, cookie or live view address.

```sh
caas audit --action login
caas audit --action passkey
```

The audit log keeps each record 365 days, and its newest 1,000,000. To keep every record longer, read it into your own store with `GET /v1/audit-log` (see [Settings and the audit log](/docs/api#settings-and-the-audit-log)).

## Limits

| Limit | Value |
| --- | --- |
| Logins connectors | 1 per workspace |
| Logins | One per owner, site and account name |
| A site | 253 characters |
| A password or setup key | 16 KiB |
| Deliveries | 16 per login, within a sandbox's 16 egress rules and 32 connector hosts with its connectors |
| A saved sign-in | 1 MiB, once kept for its site |
| A live view's address before it's opened | 1 hour on isolance's browsers, 6 hours on Browserbase |
| Live views | 1 at a time per login |
| Browserbase logins of a site | 1 per user, and 1 for your workspace |
| Lists | `?limit=` 1 to 100, 50 by default |

## Use the API

Every path below is under `https://api.caas.sh/v1`, where `{base}` is `/users/<user>/logins` for a user's logins, or `/connectors/<id>/account/logins` for your workspace's (`<id>` the Logins connector's). Errors are as for every route (see [The API](/docs/api#errors)); without the Logins connector, every route answers `404`.

| Request | Who | Does |
| --- | --- | --- |
| `GET {base}` | `sandbox:read` | `{ logins, nextCursor }`, by site, then account name. `?site=` for the logins that cover a site; `?limit=` and `?cursor=` |
| `POST {base}` | `sandbox:write` | Adds a login (see [A login](#a-login)); an `Idempotency-Key` adds it once |
| `GET {base}/<login>` | `sandbox:read` | The login, with its live view's address to who may see it |
| `PATCH {base}/<login>` | `sandbox:write` | Changes any field but `site`; `"password": null` and `"code": null` remove them |
| `DELETE {base}/<login>` | `sandbox:write` | Removes it, its passkeys and its saved sign-in |
| `PATCH`, `DELETE {base}/<login>/passkeys/<passkey>` | `sandbox:write` | Renames a passkey, `{ "name" }`, or removes it |
| `GET`, `PUT`, `DELETE {base}/<login>/session` | `sandbox:read`, `sandbox:write` | Reads, uploads (`{ "state" }`) or removes its saved sign-in |
| `POST {base}/<login>/live`, `POST …/live/done`, `DELETE …/live` | `sandbox:write` | Opens a live view, finishes it with the sign-in saved, or ends it unsaved |
| `POST {base}/<login>/check` | `sandbox:write` | Checks whether the site is still signed in: `{ signedIn, checkedAt, session }` |
| `POST /users/<user>/logins/<login>/link`, `POST /users/<user>/logins/link` | `sandbox:write` | A link for your user: `{ "returnUrl"? }`, or `{ "site", "returnUrl"? }`, answering `{ login, connectUrl }` |

For your workspace's logins, the changes need an owner. Refusals name the field at fault (`400`), such as a site that's a public suffix, `typed` with `readable`, `"browser": "browserbase"` without the Browserbase connector, or a delivery's host another connector covers; `409` is a live view already open, a second login of the same site and account name, or a second Browserbase login of a site, which names the way out.
