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.
-- 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). El nombre
que elegís es además el scope de la capability: require secret("JWT_KEY") habilita ese y
ningún otro.
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).