# Setting a secret from a page — the value goes from the browser to each zone

> An application's secret is sealed in each zone that runs it, under a short-lived grant the control plane signs. From a page, `Secrets.SendToZones` reads the value out of an UNBOUND password input, posts it straight from the browser to each zone's gateway, empties the input, and answers one receipt per zone. The value is never an Osy# value, so it never reaches the control plane — not in a request, a continuation, or a trace.

<!-- id: zones-setting-a-secret-from-a-page · area: zones · stability: preview · html: https://osysharp.com/reference/zones/setting-a-secret-from-a-page/ -->

## Summary        {#summary}

A secret's value is sealed by the **zone** that runs the application, with that zone's own key ring — a value the
control plane sealed is one the zone cannot open. So setting one is two steps. First the control plane issues a
**plan**: one signed grant per zone, naming the application, the secret and the zone, and nothing else. Then the value
is posted to each zone under its grant.

On the command line `osy secret set` does both. On a page, the second step is **`Secrets.SendToZones`**. It is built so
that the value never becomes something the program holds:

| | what it is |
|---|---|
| `destinations` | the plan's `List<SecretSetDestination>` — where to post, and the signed grant for each zone |
| `fromField:` | the **name** of a password input on the page, which must have **no** `value:` binding |
| the answer | a `List<SecretSetResult>` — one receipt per zone: `Zone`, `Environment`, `Grant`, `Accepted`, `Code`, `Message` |

## Signature      {#signature}

```osy syntax
List<SecretSetResult> results = Secrets.SendToZones(plan.Destinations, fromField: "secret-value");
List<SecretSetResult> cleared = Secrets.ClearInZones(plan.Destinations);   // sends an empty value — a zone's store has no delete
```

## Description    {#description}

### A page that sets one secret   {#example}
```osy title="plan, send, confirm each zone that took it" test app=zones-setting-a-secret-from-a-page
List<SecretSetDestination> PlanFor(string secret) {
  return new List<SecretSetDestination>();   // your control plane's plan — the Admin portal calls RequestSecretSet
}

void Confirm(Guid grant) {
  Log.Information("confirmed {Grant}", grant);
}

[Route("/set-secret")]
[AllowAnonymous]
component SetSecret() {
  string notice = "";

  action Set() {
    var results = Secrets.SendToZones(PlanFor("Stripe"), fromField: "secret-value");
    foreach (var r in results) {
      if (r.Accepted) { Confirm(r.Grant); }
      else { notice = notice + r.Environment + " (zone " + r.Zone + "): " + r.Message + " "; }
    }
  }

  render {
    Input(type: "password", name: "secret-value");   // NO `value:` — the verb reads it, posts it, and empties it
    Button("Set", onPress: Set);
    Text(notice);
  }
}
```

### Why the value is a field NAME and not an argument   {#why}

When an action awaits a server function — `Confirm` above — the page hands the server a **continuation**: the action's
locals, so the server can resume it. A trace records the same locals. If the value were an argument, or a local, or a
bound component field, it would ride that continuation to the control plane that issued the grant, which is exactly
what the zone route exists to prevent. Naming the input keeps the value in two places only: the input, and the request
to each zone.

So the verb **refuses a bound input** (`Input(value: draft, …)`), before it sends anything, with a message saying why:
a binding has already copied every keystroke into component state. And the compiler refuses a value handed to it
directly — `Secrets.SendToZones(destinations, "sk_live_…")` does not compile.

### What is checked before anything is sent   {#checks}

Every destination is checked first, and one bad destination sends **nothing**, to any zone:

| check | why |
|---|---|
| `https`, or plain `http` only to `localhost` | a secret goes to a zone over TLS; a local platform is the one exception |
| the path is exactly `/_osyrin/zones/{Zone}/secrets/{Grant}` | a destination cannot aim one zone's grant — or the value — at another route |
| no query, fragment or credentials in the URL | nothing rides alongside the value |
| a signed grant is present | the zone admits nothing without one |

⚠ A browser cannot prove that the **host** is really that zone's gateway: the only statement of which host serves a
zone is the plan itself. The zone, for its part, verifies the grant's signature, its path, its zone and its expiry, and
seals into the application **the grant** names — nothing in the request can choose another.

The request carries no cookies and no credentials, and the zone's secret route answers CORS for that one route only:
any origin, `POST`, and the grant header — never credentials.

### Reading the receipts   {#receipts}

A zone that refuses is a **receipt**, not an exception: `Accepted` is false and `Code`/`Message` are the zone's own
words — `SECRET_NOT_DECLARED` when the running version does not declare that secret, `GRANT_REFUSED`, `GRANT_USED`.
A zone the browser could not reach answers `UNREACHABLE`. Confirm each accepted grant (`ConfirmSecretSet` in the Admin
portal) so the console can say where the secret is set, and name every zone that did not take it — a secret set in two
of three zones is the state nobody can reason about later.

A page that is wrong — no input by that name, two of them, a bound one, a bad destination — throws an
`InvalidOperationException` and sends nothing.

### In the Admin portal   {#portal}

An application's **Secrets** tab lists the secrets its deploys declare and, for whoever may set them, where each is set
and by whom. **Set or replace** opens the password field; **Clear** sends an empty value, which the application reads
exactly as a secret nobody set. Setting a secret is for an owner or admin of the organization, or a member of the
application who is not a Developer.

Beside it, the **Config** tab shows the application's `app.Config` values per environment and the hosts its packages
are granted — read-only, each saying where in the repository it is set ([per-environment config (app.Config)](https://osysharp.com/reference/config/app-config/)).

## See also   {#see-also}
- [declaring secrets (app.Secrets)](https://osysharp.com/reference/config/secrets/) — declaring a secret
- [Giving a secret its value](https://osysharp.com/reference/local/giving-a-secret-its-value/) — `osy secret set`, the same flow from a terminal
- [Deploying an application to a cloud platform — the zone builds it](https://osysharp.com/reference/zones/deploying-an-application/) — the other thing a zone accepts under a signed grant
- [component](https://osysharp.com/reference/ui/component/) — actions, and where a client verb is called from
