# where a secret may be sent — secretIn

> A secret may end only at a sink: a request header, the key of a signing call, the mTLS certificate — or a URL query parameter or request-body field that the code sending it declares on its egress line, with `egress "host" { secretIn query "api_key"; }`. In a package that line lives in package.osy and the app's grant of the host accepts it; for the app's own code it lives in app.osy. Every such value is redacted in logs and traces.

<!-- id: project-secret-sinks · area: project · stability: preview · html: https://osysharp.com/reference/project/secret-sinks/ -->

## Summary        {#summary}
A secret your code reads can travel freely through your code — locals, helpers, collections, what a helper returns —
but it may **end** in only a few places, and the compiler refuses every other one ([[function-secret-read#confinement]]).
Most APIs take a credential in a header, which is always allowed. Some take it in the **query string** or in a **form
or JSON body field**; those slots are allowed only when the code that sends the secret says so on its egress line:

```osy syntax
egress "api2.postnord.com" { secretIn query "apikey"; }
```

That line means exactly one thing: *a secret may be sent to this host in this slot*. The platform redacts that slot in
every log and trace of those calls, and `osy explain` lists it.

## Signature      {#signature}
```osy syntax
egress "<host>" {
  secretIn query "<parameter>";   // a URL query parameter
  secretIn body "<field>";        // a form field (a=b&c=d) or a JSON body field
}
```

The host is written as an egress host is anywhere else — `api.example.com`, or `*.example.com` for every subdomain.
The slot kinds are `query` and `body`; a request **header** needs no line, because a header value is already a place a
secret may go.

## Description    {#description}

### Where a secret may end      {#the-rule}
| sink | declared by |
|---|---|
| a request **header** value — `Authorization`, `X-Api-Key`, … | the platform (always allowed) |
| the **key** of a signing call — `Crypto.HmacSha256Hex(key, …)`, `Crypto.SignRs256(key, …)` | the platform |
| the **mTLS client certificate** — a `clientCert` grant | the platform |
| a URL **query parameter** of one host | a `secretIn query` line |
| a request **body field** of one host | a `secretIn body` line |

A signature, an HMAC or a digest is **not** the secret: what comes out of a signing call is an ordinary value.

### In a package      {#in-a-package}
A package's code often sends a secret the APP fills in — PostNord's client holds an `ApiKey` field the app sets from
its own `Secret.X`. The package declares where that goes, on the host line in its `package.osy`:

```osy syntax
package Osysharp.Carriers.PostNord {
  version "0.1.0";
  use Osysharp.Http;
  egress "api2.postnord.com" { secretIn query "apikey"; }
}
```

The app's grant of that host — `use Osysharp.Carriers.PostNord@0 { egress "api2.postnord.com"; }` — is its acceptance.
A grant left out is a compile error that says a secret travels there in the URL, so nobody accepts it without being
told. A package's line covers **that package's code only**: the app's own code, and another package, cannot use it.

### For your app's own code      {#in-your-app}
Your own code declares the same line in the `app { }` manifest. An `egress` line in `app.osy` declares ONLY this —
your app's outbound rules still live in `.egress` ([Where your app may connect — the .egress file](https://osysharp.com/reference/project/egress/)) — so an `egress "h";` there with no body is
refused, by name.

For your own code the compiler must also be able to READ the host: write it at the start of the URL
(`"https://api.example.com/v1?api_key=" + key`), or build it from a class field that only literal URLs are ever written
into — a generated client's `BaseUrl = "https://api.example.com"` — and then every host that field can hold must be
covered. A URL built from a host nothing pins (`baseUrl + "?api_key=" + key`, with `baseUrl` a parameter) is refused in
app code, because nothing else bounds where it goes; in a package, the hosts the app granted it do. `osy import-api`
prints the exact line to add when the API it imports takes its key in the query string.

### Checked again as it is sent      {#at-run-time}
The compiler follows a secret through your code; it cannot follow one through DATA. An API that echoes the key back in
its response, or a value put back together at run time, reaches the next call looking like any other text. So every
outbound call is checked again, as it actually is: once a run has read a secret or a token, any value it read that
appears in a call's URL or body must sit in a slot declared for **that host** — a package's own `secretIn` line for
package code, your `app.osy` line for yours — or the call is refused before it is sent. The refusal names the secret,
the host and the slot, never the value. A secret in a URL path, or in a body whose fields cannot be told, has no line
that could cover it.

```osy syntax
egress "api.example.com" { secretIn query "api_key"; }

var echoed = Http.Get("https://api.example.com/v1/whoami?api_key=" + Secret.ApiKey);   // declared: sent
Http.Get("https://collector.example.org/v1/log?k=" + echoed.Body);
//  ✗ at run time, when the body IS the key: "This app's own code tried to send `Secret.ApiKey` to
//    `collector.example.org` in the query parameter `k` … and the call was not sent."
```

A run that has read no secret is not checked at all, so code that never touches one pays nothing for it.

### Which slot is which      {#which-slot}
The compiler reads the slot off how the value is built:

- a URL: the text right before the secret ends in `?name=` or `&name=`;
- a form body: the text before it ends in `name=` (at the start, or after `&`);
- a JSON body: an anonymous object's member (`new { client_secret = key }`), a dictionary key, or the text before it
  ending in `"name":`.

A secret whose slot cannot be read that way is refused, and the message says why.

### Redaction      {#redaction}
Every value the platform hands out — a `Secret.X`, an OAuth token — and every credential header value a call sends is
remembered for your app, with its URL-escaped and base64 spellings, and scrubbed from every trace before it is kept:
the steps and locals `osy inspect` shows, a fault's frames and message, the values that crossed between browser and
server. An outbound call's log line and a test's egress journal already drop the whole query string.

## Examples       {#examples}
An API that takes its key in the query string, called from the app's own code:

```osy title="a key in the query string, declared" test app=project-secret-sinks-query
app Weather {
  model "model/**/*.osy";
  use Osysharp.Http;
  egress "api.weather.example" { secretIn query "appid"; }
}

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

string Forecast(string city) {
  var r = Http.Get("https://api.weather.example/v2/forecast?q=" + Uri.EscapeDataString(city)
    + "&appid=" + Uri.EscapeDataString(Secret.WeatherKey));
  return r.IsSuccess ? r.Body : "";
}
```

The ordinary shape needs no declaration at all — a helper that builds the header map is legal to write, and whatever
calls it may hand the map to the call:

```osy title="a header helper, no declaration needed" test app=project-secret-sinks-header
app Ledger {
  model "model/**/*.osy";
  use Osysharp.Http;
}

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

Dictionary<string, string> Auth() {
  var h = new Dictionary<string, string>();
  h.Add("Authorization", "Bearer " + Secret.LedgerKey);
  return h;
}

int Balance(string account) {
  return Http.Get("https://api.ledger.example/v1/accounts/" + account, Auth()).StatusCode;
}
```

Without the line, the same query-string call is refused:

```osy title="the query-string call, refused when nothing declares its parameter" syntax
// ✗ it would travel in this request's URL as the query parameter `appid` to `api.weather.example`, and nothing
//   declares `appid` a credential slot
Http.Get("https://api.weather.example/v2/forecast?appid=" + Secret.WeatherKey);
```

## See also       {#see-also}
- [reading a secret's value (Secret.Name)](https://osysharp.com/reference/function/secret-read/) — reading a secret, and everything it may not do
- [package.osy](https://osysharp.com/reference/project/package/) — the package manifest this line lives in, and how an app grants a package its hosts
- [Where your app may connect — the .egress file](https://osysharp.com/reference/project/egress/) — the app's own outbound allow-list and deny-list
- [Explaining your app's security](https://osysharp.com/reference/local/explaining-your-app/) — `osy explain` lists every place each secret leaves, including these
