Jadpo is experimental and not ready for production. These docs follow the compiler's main branch as of 30 Oct 2026. What's built · Install 0.1.0
jadpo
Reference: Standard library

Standard library

Secret values

On this page

A secret value is a password, key, token or code that your app receives and checks, but never sends, logs, stores or shows. It arrives in a request's Input, or through text.parse or json.read, and the compiler keeps it from going any further. There are seven:

Type What it holds
Password 8 to 256 characters with no control character; any other Unicode and spaces are allowed. 8 is NIST SP 800-63B's minimum. Raise it with min_length:; NIST asks for 15 where a password is the only factor
ApiKey 1 to 512 printable ASCII characters with no spaces, whatever prefix the provider uses
BearerToken RFC 6750's b64token, at most 8,192 characters
Jwt A JSON Web Token, which crypto.verify_jwt checks. Its text is RFC 6750's b64token, at most 8,192 characters
TotpSecret RFC 4648 Base32 in upper case without padding, in its one spelling, holding 16 to 64 bytes (at least RFC 4226's 128 bits)
SecretText 1 to 8,192 characters of anything, line breaks included, so it has room for a PEM key
OneTimeCode The code a person types: six to eight ASCII digits (at least RFC 4226's six, at most RFC 6238's eight), a leading zero kept

None of them is ordered: your code can't compare one with < or sort by it. Sets and maps still keep secrets in an order of their own, which Comparing secrets describes.

Here a sign-up form takes a password, which goes no further than its hash, and a partner's API key is recognised by what is kept of it:

example
type MemberHandle = Text {    max_length: 40}type ChosenPassword = Password {    min_length: 15}/// What the sign-up form sends: the password arrives, and goes no further/// than its hash.type SignUpForm = Input {    handle: MemberHandle    chosen_password: ChosenPassword}type KeyVerifier = Bytes {}type Recognised = Bool {}/// The hash a new member's password is kept as.action password_to_keep(sign_up: SignUpForm) -> PasswordHash {    return crypto.hash_password(sign_up.chosen_password)}/// What is kept of a partner's API key: its SHA-256, which signs no one/// in.function key_verifier(partner_key: ApiKey) -> KeyVerifier {    return KeyVerifier(hash.sha256(partner_key))}/// Whether a key a partner sends is the one it was issued.function recognised(offered_key: ApiKey, kept_verifier: KeyVerifier) -> Recognised {    return Recognised(key_verifier(offered_key) == kept_verifier)}
example
test "a password is kept as its hash" {    var sign_up: SignUpForm = SignUpForm {        handle: "ada"        chosen_password: "correct horse battery staple"    }    var kept_hash: PasswordHash = password_to_keep(sign_up)    assert crypto.verify_password(sign_up.chosen_password, kept_hash) == PasswordCheck.correct}test "a partner's key is recognised by its verifier" {    var kept_verifier: KeyVerifier = key_verifier("example_key_0123456789abcdef")    assert recognised("example_key_0123456789abcdef", kept_verifier)    assert not recognised("example_key_0123456789abcdeX", kept_verifier)}

Where a secret can go

A secret stays on the server. An Input, an Object, a variable and a parameter may hold one, and json.read reads one, since none of them leaves it.

Nothing that leaves the server may hold one, however deeply it is nested in lists, maps, options, objects, Inputs or a variant's data. The compiler refuses a secret in each of these with type.secret_exposed:

  • an Output;
  • a failure's client or internal fields. A failure's fields are logged, so this keeps secrets out of the logs too;
  • an event, which is kept in the outbox and handed on;
  • an email or a message.

json.write can't write one either (type.mismatch). A PasswordHash has a rule of its own, type.password_hash_exposed, because a hash, unlike a secret, is stored.

How a secret is kept

A secret is never stored as itself. Each kind is kept as something that does the same job:

  • A password is kept as its PasswordHash.
  • An API key or a bearer token is kept as hash.sha256 of it, as Identity keeps a token's verifier.
  • A Jwt is checked when it arrives and never kept.
  • A one-time code is kept only as the time it was used.
  • A TotpSecret, which checking a code needs back, and a SecretText are stored sealed with an encrypted field's key (Encrypted fields).

An entity's field that holds any other secret is type.secret_exposed, and the error's next: line names what to keep instead. Keeping the password itself is refused:

example
type MemberHandle = Text {    max_length: 40}entity Member {    id: Uuid    handle: MemberHandle {        unique: true    }    chosen_password: Password}
text
error: `Member.chosen_password` keeps a `Password` --> main.jadpo:10:22   |10 |     chosen_password: Password   |                      ^^^^^^^^ a secret, stored  reason: A `Password` is never stored as itself, so a copy of the database holds none. A password is hashed with `crypto.hash_password` and checked with `crypto.verify_password`.  next: Store `crypto.hash_password(...)` of it in a `PasswordHash` field in `chosen_password`'s place (agent)  rule: type.secret_exposed  help: https://jadpo.dev/docs/errors/type.secret_exposedcheck failed: 1 error, 1 file

What may read a secret

Only the operation made for a secret reads it:

  • crypto.hash_password and crypto.verify_password take a Password.
  • crypto.verify_jwt takes a Jwt.
  • hash.sha256 takes an ApiKey or a BearerToken. It never takes a Password, whose only hash is Argon2id's, nor a OneTimeCode, since the digest of a six-digit code is found in a million tries.

Everything else that would read a secret is type.secret_exposed, so no length, part, text or format for people can carry one out:

  • any other operation, one over a collection of secrets included, so text.join can't join them;
  • any operator but == and !=, since an order or matches would answer questions about the text;
  • text.from and text.format;
  • building any type except one built on the same secret.

Building a type that is built on it is allowed, as in StaffPassword(password).

Comparing secrets

== and != on secrets take a time that depends on the two values' lengths alone, so how long a comparison takes says nothing of where two values differ. Every byte up to the longer value's length is compared, and the answer is decided only once all of them have been, so the comparison never stops early. This is how Identity compares its verifiers, and a secret compared with text written in source is compared the same way.

A set or a map orders secrets by a comparison that also looks at every byte.

A secret value equals its text wherever the two meet, in time that depends on the lengths alone: inside a list, a set, a map's keys and values, an object and an enum's data, however deep ([p] == ["hunter2hunter2"]). In the order sets and maps keep, a secret value sits among text values, ordered by its text. So set.contains finds a secret by its text, and finds text by an equal secret, and that order always agrees with ==.

Reports show a secret redacted

Wherever Jadpo shows a value, it shows a secret as <redacted ApiKey>, naming its type: a failed test's values, jadpo test's output and its --json, a failure's report, a fault, and anything a debugger or jadpo watch shows. Every one of them writes a value the same way.

A test that fails over a secret shows it redacted wherever it shows a value, and its diff still says which field differs:

example
type PartnerName = Text {    max_length: 100}type PartnerLogin = Object {    name: PartnerName    partner_key: ApiKey}/// A partner, signed in with the key it sent.function signed_in(name: PartnerName, offered_key: ApiKey) -> PartnerLogin {    return PartnerLogin {        name: name        partner_key: offered_key    }}
example
test "a partner keeps the key it signed in with" {    var issued_key: ApiKey = "example_key_0123456789abcdef"    assert signed_in("Northwind", issued_key) == PartnerLogin {        name: "Northwind"        partner_key: "example_key_9876543210fedcba"    }}
text
FAIL  a partner keeps the key it signed in with --> main.test.jadpo:3  assert signed_in("Northwind", issued_key) == PartnerLogin { ...         |                      |              |         |                      |              PartnerLogin { name: "Northwind", partner_key: <redacted ApiKey> }         |                      <redacted ApiKey>         PartnerLogin { name: "Northwind", partner_key: <redacted ApiKey> }  expected: PartnerLogin { name: "Northwind", partner_key: <redacted ApiKey> }  actual: PartnerLogin { name: "Northwind", partner_key: <redacted ApiKey> }  diff: ~ partner_key: expected <redacted ApiKey>, actual <redacted ApiKey>  rule: test.assertion_failed  rerun: jadpo test main.test.jadpo:3 --seed 1test failed: 1 failed, 1 pure test

A setting that holds a secret

A setting whose type is a secret value follows its secret: flag, as every setting does (Secrets). With secret: true, it goes only to places the compiler owns. Without it, your code reads it as a secret value, which the rules above keep in and show redacted.

Recordings for replay

A recording of a request, kept so it can be replayed (What the graph and production evidence add), holds no credential the runtime made. The bytes the runtime makes a session, an API token, a service key or a CSRF token from are never kept on a recording's tape, nor taken from a replayed one, so a replay makes fresh ones. The program's own random draws still replay in order.

No crypto.encrypt nonce is kept either. It is drawn as a credential's bytes are, so a replay never seals with a nonce that a production run used under the same key. A password's salt isn't secret and stays on the tape, so a replayed hash_password gives the hash the recorded run made.

OpenAPI

OpenAPI describes a secret value as a writeOnly string, and a Password with format: password as well, so generated clients hide it as it is typed.

Not built yet: storing a TotpSecret or a SecretText needs keys held as data (Language gaps this page depends on). Until that is built, storing one is toolchain.not_built_yet rather than refused. No operation reads a OneTimeCode yet: crypto.verify_totp waits for keys held as data. jadpo replay, which the recordings above are for, isn't built; when it is, it keeps out the draws a secret value was made from.

Markdown for agents: /docs/secret-values.md.Built from Jadpo main at a707efb.