Osy#betaRun it on Osyrin: osyrin.com

Reference / Function

reading a secret's value (Secret.Name)

Secret.<Name> -> string // a declared secret's value, server-side only

`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.

stable3 examples compiled by CIfunctionsecretssecuritystdlib

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)). 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.

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

Signature#

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#

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); 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#

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:

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#

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) — it can never be evaluated in a browser, and no component that runs there can reach one.

Where the value may go#

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).

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:

refusedwhy
returning it from a function a request can call — [AllowAnonymous], [Authorize(…)], an [AuthMethod] — or from a component's bodythe caller is a visitor or a page, and the key would land on the screen
storing it in an entity fielda 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 thereevaluating 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 payloadeach 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 messagea 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 calla URL is written into access logs, proxies and traces; a body is data the far side stores
[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). osy explain lists every place each secret leaves the server (Explaining your app's security).

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?#

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#

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

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:

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:

// `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#

Related

declaring secrets (app.Secrets)

`app.Secrets` declares the named secrets your app uses — API keys, tokens, client secrets. Each is `new…

Giving a secret its value

Declaring `app.Secrets` names a secret; it does not give it a value, and an app cannot run until something does. `osy…

Crypto.HmacSha256Hex and Crypto.FixedTimeEquals

HMAC-SHA-256 authenticates a message under a shared key — proving it came from a key holder and was not altered, which…

Crossing the network

An Osy# app runs in two places — the server, which holds the data, and the browser, which holds the screen — and the…

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…

Explaining your app's security

Explains your app's declared authorization in plain English — who can read, create, update and delete each entity…