# reading a secret's value (Secret.Name)

> `Secret.Name` in a function body evaluates to the declared secret's value — the key itself, as a string, read at the moment it is used; `Secret.Name.IsSet` asks whether it has one without reading it. It is how a webhook signature gets its shared key or an outbound call gets its token. The name must be one your app declares in `app.Secrets`; an undeclared name is a compile error, not a runtime null. The value never enters your source, and the compiler follows it through your code and helpers and lets it end only at a sink — a request header, a signing key, or a URL/body slot an egress line declares — refusing every other way it could leave the server.

<!-- id: function-secret-read · area: function · stability: stable · html: https://osysharp.com/reference/function/secret-read/ -->

## Summary        {#summary}
`Secret.Name` reads a **declared secret's value** inside a function body. It evaluates to the stored string — an API
key, a shared signing key, a token — at the moment the line runs.

The same `Secret.Name` handle also appears in config slots such as `app.Embedding`'s `ApiKey`
([declaring secrets (app.Secrets)](https://osysharp.com/reference/config/secrets/)). There it *points at* a secret for the platform to resolve; in a function body it *is* the value,
because that is what code needs in order to sign, compare, or send it.

```osy syntax
var expected = Crypto.HmacSha256Hex(Secret.CarrierWebhook, payload);
```

## Signature      {#signature}
```osy syntax
Secret.<Name> -> string
```

`<Name>` is written as an identifier, exactly as declared — `Secret.CarrierWebhook` for
`new Secret("CarrierWebhook")`. It is not a string, not a lookup, and takes no arguments.

## Description    {#description}
A secret has to be declared before it can be read. `app.Secrets = [ new Secret("CarrierWebhook") ];` declares it;
`osy secret set CarrierWebhook` gives it a value on your machine ([Giving a secret its value](https://osysharp.com/reference/local/giving-a-secret-its-value/)); a deployed
app's values are supplied by whoever operates its platform.

**An undeclared name is a compile error.** That is deliberate and it is worth knowing why: a mistyped credential that
resolved to nothing would make every signature check compare against a key of empty string, which fails exactly like
a wrong signature from the sender. You would go looking at the sender, and you would keep looking. The compiler
refuses the typo instead.

**A declared secret with no value throws when it is read**, and says which secret and how to set it. This is also not
an empty string, for the same reason — an unset key is the ordinary state of a fresh checkout, and it must not
silently degrade into a check that always fails.

### Asking whether it is set      {#is-set}
`Secret.Name.IsSet` answers a `bool` — whether the secret has a value — **without reading it**. It is how an app
that works without a key (a payment method offered only once its keys are set, posters fetched only when there is
an API key) decides, instead of wrapping each read in a `try`:

```osy syntax
if (Secret.StripeApiKey.IsSet) { providers.Add(new StripePayments { ApiKey = Secret.StripeApiKey }); }
```

A `bool` is not a credential, so `IsSet` is not confined: return it from a helper, show it on a page, test it. The
name must still be declared — `IsSet` on an undeclared name is the same compile error as reading it.

### It is server-side, always      {#server-side}
Reading a secret is a **server** operation. A credential has no client-side producer, so an expression containing
`Secret.Name` pins its containing code to the server ([Crossing the network](https://osysharp.com/reference/function/execution-side/)) — it can never be evaluated in a
browser, and no component that runs there can reach one.

### Where the value may go      {#confinement}
The read gives you the plaintext, so the compiler follows it. A value built from a secret travels **freely** through
your code — locals, concatenation and interpolation, collections you put it in, a helper's parameters, and what a
helper **returns**. It may **end** only at a **sink**:

- a request **header** value — the `Authorization` of an `Http.*` call ([[project-secret-sinks#the-rule]]);
- the **key** of a signing call — `Crypto.HmacSha256Hex(key, …)`, `Crypto.SignRs256(key, …)`;
- the **mTLS client certificate** a package is granted;
- a URL **query parameter** or request **body field** that an `egress "host" { secretIn … }` line declares for that
  host ([where a secret may be sent — secretIn](https://osysharp.com/reference/project/secret-sinks/)).

Everything else is a compile error, not a warning — a warning can be ignored and a leaked credential cannot — and the
error names the secret, the helpers it came through, and where it ended:

| refused | why |
|---|---|
| **returning** it from a function a request can call — `[AllowAnonymous]`, `[Authorize(…)]`, an `[AuthMethod]` — or from a component's body | the caller is a visitor or a page, and the key would land on the screen |
| **storing** it in an entity field | a stored credential is readable by everything that can read that row, rides into backups and exports, and can no longer be rotated by `osy secret set` |
| **reading** it in anything sent to the browser — an `action`, a component `method`, a render slot — or **calling** a helper that returns it there | evaluating it there means the plaintext was shipped |
| **handing** it to an effect — `Log.*`, a file write, the clipboard, a dialog, a topic publish, a workflow event payload | each of those puts what it is given somewhere durable or observable. `Log` is the one that surprises people: it is **dual-sided**, so a client log line is in the visitor's own browser console as well as in `osy logs` |
| putting it in an **exception's message** | a message is logged, traced, and readable by whoever catches it |
| putting it in a **URL or request body** no `secretIn` line declares — or in any other argument of an outbound call | a URL is written into access logs, proxies and traces; a body is data the far side stores |

```osy syntax
[AllowAnonymous] string SigningKey() { return Secret.CarrierWebhook; }   // ✗ a visitor could call it
new AuditRow { Token = Secret.CarrierWebhook };                          // ✗ stored
Log.Information(Secret.CarrierWebhook);                                  // ✗ logged — and shown, on the client
Http.Get("https://api.example.com/v1?key=" + Secret.CarrierWebhook);     // ✗ in the URL, undeclared
```

**A helper that returns what it built from a secret is legal** — every provider's `Headers()` is one. It becomes a
**source** itself: each call of it is confined exactly as the read would be, so its result may go into a header and
nowhere else. And it is not callable by a request: a page, an API client or an agent that names it is refused, because
its answer would carry the key.

**What stays legal is the whole point of the feature**: read the secret and *use* it, then let the RESULT travel.
Signing with it, comparing with it, and presenting it to the service it authenticates to are all ordinary code — both
examples below do exactly that. A signature or a token you were issued is not a credential of yours, so it may be
returned, stored and logged like any other string.

The rule follows the value into your own helpers and back out, so renaming it or passing it through a function does
not evade it. What it does not decide is WHICH host a header goes to — the sanctioned use of a secret is handing it to
an outbound call, and where your code may connect is the egress rules' job ([Where your app may connect — the .egress file](https://osysharp.com/reference/project/egress/)). `osy explain` lists
every place each secret leaves the server ([Explaining your app's security](https://osysharp.com/reference/local/explaining-your-app/)).

What is persisted is the ciphertext on the secret's own row, and nothing else — a function's expression tree carries
only the NAME.

### Do secrets work under `osy test`?      {#in-a-test}
`osy test` runs against a throwaway branch of your app built from source, and your project's `.secrets` values travel
with the run — so a `[Test]` that exercises a signature check reads the same key the app does, and the credential
path is testable rather than the one part you have to take on faith.

## Examples       {#examples}
Verifying a signed carrier webhook — the key comes from the secret store, and the tag is compared in constant time:

```osy title="verify an inbound signature" test app=function-secret-read
app.Secrets = [ new Secret("CarrierWebhook") ];

bool IsAuthenticScan(string trackingNumber, string location, string signature) {
  var expected = Crypto.HmacSha256Hex(Secret.CarrierWebhook, trackingNumber + "|" + location);
  return Crypto.FixedTimeEquals(expected, signature);
}
```

Degrading without a key — a lookup that is offered only when its key has been set, and reports why when it is not:

```osy title="work without the key" test app=function-secret-read-is-set
app.Secrets = [ new Secret("PosterApiKey") ];

bool PostersAvailable() => Secret.PosterApiKey.IsSet;

string PosterSource() {
  if (!Secret.PosterApiKey.IsSet) { return "title cards — no poster key is set"; }
  return Crypto.HmacSha256Hex(Secret.PosterApiKey, "posters");
}
```

Sending one outbound — the same read, used as a bearer token rather than a signing key:

```osy title="authenticate an outbound call" test app=function-secret-read-outbound
// `use` is a MANIFEST declaration — it belongs in your app.osy, not in a model file.
app Dispatch {
  model "model/**/*.osy";
  use Osysharp.Http;
}

app.Secrets = [ new Secret("DispatchApi") ];

string FetchManifest(string depot) {
  var headers = new Dictionary<string, string>();
  headers.Add("Authorization", "Bearer " + Secret.DispatchApi);
  var r = Http.Get("https://api.example.com/manifests/" + depot, headers);
  return r.IsSuccess ? r.Body : "";
}
```

## See also       {#see-also}
- [declaring secrets (app.Secrets)](https://osysharp.com/reference/config/secrets/) — `app.Secrets`, where a secret is declared and named
- [Giving a secret its value](https://osysharp.com/reference/local/giving-a-secret-its-value/) — `osy secret set`, and which secrets are still empty
- [Crypto.HmacSha256Hex and Crypto.FixedTimeEquals](https://osysharp.com/reference/function/crypto-hmac/) — verifying a signature with the key you just read, in constant time
- [Crossing the network](https://osysharp.com/reference/function/execution-side/) — why an expression that reads a secret pins its code to the server
- [where a secret may be sent — secretIn](https://osysharp.com/reference/project/secret-sinks/) — `egress "host" { secretIn … }`: sending a secret in a URL or a body
- [Explaining your app's security](https://osysharp.com/reference/local/explaining-your-app/) — where each secret leaves the server, as a report
