---
slug: 21-secrets
title: Secretos
description: Usá API keys, tokens y secretos de webhook en Synsema sin exponerlos nunca — secret, as_secret, bearer, hmac y el reveal scopeado.
example_ids: [secret-bearer]
---

# Secretos

Un `secret` es un valor **opaco y redactado**: lo podés *usar* pero nunca *leer* su texto
plano desde el lenguaje. Es a prueba de LLMs por diseño — nunca aparece en `print`, logs,
cuerpos de error, respuestas JSON, el blackboard ni un prompt de LLM.

## `env()` vs `secret()`

- `env(name, default?)` → configuración de texto plano y visible (URLs, puertos, flags).
- `secret(name, default?)` → una credencial opaca y redactada (API keys, tokens, secretos de firma).

Ambos son **deny-by-default**: declarás la capacidad o la lectura falla.

```synsema
-- Doc example (UNIVERSAL — one source, shared by every language).
-- Comments are English on purpose: the code is the same in /en, /es, /pt, ...
-- Doctest gate: `synsema test docs/0.5.x/examples/` must be green to publish.
intent: "doc example: a secret stays redacted everywhere except the socket"
require secret("API_KEY")

print("a secret prints as → " + text(secret("API_KEY", "sk-demo-123")))    -- redacted

test "a secret is redacted in every string surface"
    let k be secret("API_KEY", "sk-demo-123")
    assert_eq(text(k), "secret(API_KEY)")                       -- never the real value
    assert_eq(type_of(k), "secret")
    -- GOTCHA: concatenating text + secret yields a SECRET, and it redacts WHOLE —
    -- the "Authorization: " prefix is absorbed, you do NOT get "Authorization: secret(...)".
    assert_eq(type_of("Authorization: " + k), "secret")
    assert_eq(text("Authorization: " + k), "secret(API_KEY)")   -- prefix absorbed (taint)

test "bearer() builds a tainted Authorization value"
    let b be bearer(secret("API_KEY", "sk-demo-123"))
    assert_eq(type_of(b), "secret")                             -- still a secret
    assert_eq(text(b), "secret(API_KEY)")                       -- redacted, even as a Bearer value

test "a raw secret goes in ANY header, not just Bearer"
    -- materialization to the real value happens ONLY at the socket; here we just show
    -- it stays a redacted secret in your program's value space (see /docs/.../secrets).
    let headers be {"x-api-key": secret("API_KEY", "sk-demo-123")}
    assert_eq(text(headers["x-api-key"]), "secret(API_KEY)")

test "as_secret seals a value that arrives at runtime"
    let sealed be as_secret("rk-from-user", "user_key")         -- e.g. a tenant's key from a header
    assert_eq(text(sealed), "secret(user_key)")
    assert_eq(type_of(sealed), "secret")
```

## De dónde salen los valores

`secret("NOMBRE")` resuelve `entorno del proceso > .env > default`, y sin ninguno de los
tres **da error** — nunca un valor vacío en silencio. `synsema init` escribe un
`.env.example` cuya última sección es justamente esta: tus propios secretos, con los que
piden las features de auth ya nombrados (`JWT_KEY`, `CAPTOKEN_ROOT_KEY`,
`AGENT_SIGNING_KEY` — ver [Identidad de agentes](/es/0.6.x/46-agent-identity)). El nombre
que elegís es además el scope de la capability: `require secret("JWT_KEY")` habilita ese y
ningún otro.

La misma regla cubre las **claves privadas**: la clave VAPID de `push_send` (engine v0.6.15+,
`VAPID_PRIVATE_KEY`, ver [Tu app en el teléfono](/es/0.6.x/41b-pwa)) y la clave blockchain de
`sign` se aceptan **sólo como `secret`** — desde `.env` con `secret(...)`, o selladas en runtime
con `as_secret(...)`. Un texto plano se rechaza con el arreglo en el mensaje, y el valor de la
clave jamás aparece en un error. Las claves que genera el engine (`push_vapid_keys()`,
`mnemonic_generate`) vuelven ya selladas.

## Las credenciales van en **cualquier** header — no solo Bearer

Un `secret` puesto como valor de header se materializa a su valor real **solo en el
socket**, sin importar el nombre del header. O sea que **no** estás limitado a
`Authorization`:

- Header custom: `{"x-api-key": secret("API_KEY")}`
- Un header propio: `{"x-lo-que-sea": secret("API_KEY")}`
- Bearer (solo azúcar para `Authorization: Bearer <token>`): `{"Authorization": bearer(secret("API_KEY"))}`

En **query params y el body** un `secret` se **redacta** (fail-closed) — una credencial
solo sobrevive en el cable dentro de un header.

## `as_secret()` — sellar un valor que llega en runtime

`secret("NAME")` lee del config. Para un valor que llega en runtime y no está en `.env`
(la key de un usuario en un header entrante, un token de otra llamada HTTP), sellalo en el
borde con `as_secret(value, label?)`. Es puro (sin `require`), idempotente sobre un secret,
y acepta texto o bytes.

## `reveal()` — el último recurso (auditado)

`reveal(secret)` devuelve el texto plano. Exige `require reveal("NAME")` **scopeado al
nombre/label del secret**, escribe una entrada de auditoría append-only por cada intento, y
avisa con un warning en la forma pelada (sin scope). Preferí `bearer`/`hmac_sha256`/
`verify_hmac` — consumen el secret sin exponerlo.

El directorio de audit (`$SYNSEMA_AUDIT_DIR`, default `~/.synsema/audit`) guarda un log
append-only por familia gateada, todos con las mismas garantías — todo intento registrado
(concedido **o** denegado), jamás el valor sensible, fail-loud en el camino concedido (si
la entrada no se puede escribir, la operación se rehúsa): `reveal.log`, `sign.log` (firmas;
una entrada `denied_by=ceiling` marca un corte de `SYNSEMA_SIGN_CEILING`), `wallet.log`
(creación de custodia) y `spend.log` (el ledger de dinero: monto decimal canónico, unidad,
motivo entre comillas, `file:line`, programa; `denied_by=ceiling` marca un corte de
`SYNSEMA_SPEND_CEILING` — ver **[Capacidades](/es/0.6.x/20-capabilities)**).
