# Source credentials

Source: https://mostlyright.md/docs/build/credentials/

Store a source credential once, then refer to it by name without putting its value in a recipe or log.

Store the value once and use its name in the recipe. When the worker fetches the source, it redeems
a one-time handle for the credential. The value never appears in the recipe, receipt, events or
command line.

## Check saved connections first

A warehouse or API your organization already connected stores its credential already. It needs no second copy under keys.

```bash
mr-data connections --dataset DATASET_ID --json
```

Use the matching active connection's `connection_id` and `version_digest` in the source's `connection` member, with the query in `parameter_values` and a response reader in `response`. Set `dataset.id` to the granted dataset. Do not ask a person to re-enter a saved credential. If the connection is not granted to that dataset, an owner or administrator has to allow it in Settings → Connections & secrets.

A source without a saved connection uses a named secret.

## Store a credential

```bash
mr-data keys set noaa_token --value-file token.txt --json
mr-data keys set noaa_token --value-file - --json      # read it from standard input
mr-data keys list --json
mr-data keys rm noaa_token --json
```

| Argument | What it does |
| --- | --- |
| `set` \| `list` \| `rm` | `set` stores a credential under `NAME`, `list` names the ones this workspace holds, `rm` withdraws one. |
| `NAME` | The credential's name: lowercase letters, digits, dashes and underscores, at most 63 characters, starting with a letter. Not needed by `list`. |
| `--value-file FILE` | With `set`, the file holding the value. `-` reads it from standard input instead. The client removes one trailing newline, because a credential stored with the one `echo` added fails at the source and nothing says why. |
| `--part NAME=VALUE` | With `set`, one named part of a shaped credential. `NAME=@PATH` reads that part off disk. Repeat once per part. It cannot be combined with `--value-file`. |
| `--json` | One JSON object instead of human lines. |

The value cannot be given on the command line. No argument holds one, so it cannot land in `ps`, in a shell history file, or in a CI log. The client checks the name before it resolves a credential and before it reads a value from disk. A mistyped name fails locally before any network request. A value is at most 65,536 bytes.

`keys list` returns names and their `status` (`active` or `revoked`), `created_at` and `rotated_at`. `keys set` returns the same plus `rotation_generation`. Nothing reads a value back.

Storing a name that already exists rotates it. Existing recipes keep working, because they reference the name and not the value.

## Shaped credentials for a stream venue

A keyed `wss://` venue authenticates with two or three named parts rather than one opaque string. The vault stores one value per name, so this command composes the parts into one envelope.

```bash
mr-data keys set venue_key --part key_id=ABC123 --part private_key_pem=@signing.pem --json
```

The vault accepts four shapes, each by the set of parts it holds. The parts you name decide the shape, and a set that matches none of them is refused by name.

| Shape | Parts |
| --- | --- |
| `token` | `token` |
| `key_pair` | `key_id`, `secret` |
| `key_pair_passphrase` | `key_id`, `secret`, `passphrase` |
| `key_id_pem` | `key_id`, `private_key_pem` |

`--part NAME=@PATH` reads that part off disk, which is how a signing key arrives. A PEM is multi-kilobyte and has newlines in it. At most four parts compose one envelope, and the envelope is canonical JSON built locally and never echoed.

The venue's connector document declares which parts its shape holds, and a set that does not match is refused.

## Reference the name from a recipe

The credential reference is `credential`, a sibling of `connector` on the source. It takes either a `secret_name`, or a saved connection's `connection_id` and `connection_version_digest`.

```json
{
  "name": "agency_feed",
  "source_class": "user_api",
  "data_classification": "public",
  "locator": {
    "kind": "https_url",
    "display_locator": "https://api.example.org/export.csv"
  },
  "rights_claim": {
    "claimed_basis": "terms_of_service_asserted",
    "claim_evidence_digest": "sha256:…"
  },
  "connector": {
    "adapter_id": "authenticated.https.api_key@1.0.0",
    "credential_mode": "opaque_reference",
    "origin": "https://api.example.org",
    "parameters": [{"name": "credential_scheme", "value": "bearer"}]
  },
  "credential": {"secret_name": "agency_feed_key"},
  "limits": {"max_source_bytes": 3145728}
}
```

Two rules the schema enforces:

- A credentialed source states `credential_mode: "opaque_reference"` and has a sibling `credential`.
- A credential-free source states `credential_mode: "none"` and does not have one.

The `display_locator` is the credential-free target. Never put a key, a token or a signed URL in a locator, a parameter, a prompt or a log.

## Which source kinds accept a credential

| Adapter | Credential | How it is presented |
| --- | --- | --- |
| `authenticated.https.api_key@1.0.0` | `secret_name` | Exactly one connector parameter named `credential_scheme`, valued `bearer` or `x-api-key`. The worker does not infer a scheme from the URL. The adapter is one single-request CSV GET, and other connector parameters do not widen it. |
| `authenticated.https.api_key@2.0.0` | `secret_name` | `generic_api.auth`, and nothing else. It does not accept a `credential_scheme` parameter. `auth.kind` is `bearer`, `header` (with `header_name`, `value_prefix`, and optional `key_encoding`, `pre_encoding_prefix`, `pre_encoding_suffix`) or `query` (with `query_name`). |
| `authenticated.https.connector@3.0.0` | `secret_name` | Same `generic_api` shape, plus a required `generic_api.limits` and an optional `job` block. |
| Any adapter, through a saved connection | `connection_id` + `connection_version_digest`, or the `connection` shorthand | The connection owns the endpoint, the authentication and the fixed inputs. The recipe supplies the query and how to read the result. |
| `stream.wss@2.0.0` | Not in the recipe. | A keyed venue's credential is named on the recording, with `mr-data stream capture --secret NAME`, because a build never reaches the venue. See [Live streams](/docs/build/live-streams/). |

These take no credential and must state `credential_mode: "none"`: `public.https`, `public.https.collection@2.0.0`, `stream.wss@1.0.0`, `snapshot.artifact@1.0.0`, `document.projection@1.0.0` and `external.openligadb`. A page collection reads public addresses, and there is no credential to present to a link discovery found.

The worker never downgrades an authenticated user API into the credential-free fetcher. Hosted authenticated acquisition uses an allowlisted fixed adapter and a brokered single-use handle, bound to that attempt and that approved source. The worker has no direct vault access. It lends the redacted in-memory lease to that adapter for one call.

The full request grammar of the generic adapters covers path templates, static query and headers, typed parameters, pagination and the Reader pin. It is in [Source kinds and connectors](/docs/reference/sources/).

## The dashboard as the alternative

Settings → Connections & secrets at [app.mostlyright.md](https://app.mostlyright.md) writes to the same vault. Use it when the person holding the credential is not the person at the terminal. Use it too when the credential comes out of a password manager that will not hand it to a file. What lands there is what `mr-data keys list` names, and a recipe references it the same way.

## When a person has to finish it in a browser

One thing in this product needs a fresh interactive verification that a device credential cannot give, and it is not `--confirm`: publishing a dataset publicly, on organizations that require it. The refusal is `THIN_CLOUD_STEP_UP_REQUIRED`, and it names the dashboard address to open.

Releasing a held full run in the `awaiting_sample_approval` state used to be the second. It is not any more: any editor credential of the workspace may release it once its preview has sealed a table, so an agent can do it on your word. The browser path is unchanged and still works — open the run's page and release it there. The platform writes a bounded preview you can download either way, and the release is fenced on the run's `expected_version` so a run that moved in between is refused rather than released.

An ordinary large run gets a run confirmation instead. That is `--confirm`, and any authenticated caller of the paying workspace may settle it.

## If a source refuses for want of a credential

Say that the source needs a credential, and name which one. Say who issues it, where to apply for it, and the command that enrols it. A rejected source with a route back is a decision somebody can revisit.

Say nothing about the storage mechanism, and never print, log or echo a credential value while diagnosing.

## Next

[Write a recipe](/docs/build/write-a-recipe/) for where `credential` sits in the document. [Fix a failed run](/docs/build/troubleshooting/) for what an authentication refusal looks like on a run.
