Releases: contact-red/sensitive
Release list
0.2.0
Add EnvSecrets for reading secrets from the environment and .env files
EnvSecrets holds the process environment and, if you read one, a .env file, and returns each value as a Sensitive[String]. Build it once at startup and hand each component the values it needs: it is val, so it can be shared between actors, and nothing downstream needs Env.vars. Every environment value is wrapped, whether or not it is a secret, so read ordinary configuration from Env.vars directly and keep a call to expose meaningful.
EnvSecrets(env.vars) reads the environment alone. ReadEnvSecrets takes a .env file into account as well. A path with no file at it is not an error — you get the environment alone. A file that is there but cannot be read, or that has a malformed line, means secrets the file was meant to supply are absent, so neither is safe to carry on from.
use "files"
use "sensitive"
actor Main
new create(env: Env) =>
let dotenv = FilePath(
FileAuth(env.root),
".env",
recover val FileCaps .> set(FileRead) .> set(FileStat) end)
let secrets =
match ReadEnvSecrets(env.vars, dotenv)
| let s: EnvSecrets => s
| let e: DotEnvError =>
env.err.print(e.string())
env.exitcode(1)
return
end
try
let password = secrets("DB_PASSWORD")?
env.out.print("The password is " + password.string())
else
env.out.print("DB_PASSWORD is not set")
endReading the file needs FileRead and FileStat on the FilePath. A relative path resolves against the working directory, so pass an absolute path when that directory is not trusted.
An environment variable overrides the .env file. When a name is set in both, the value from the environment wins and the file's value is discarded, so the file supplies only the names the environment leaves unset. A .env file is a development convenience; that ordering keeps one from shadowing a secret a deployment supplies.
An entry in a .env file is NAME=VALUE. Blank lines and lines starting with # are ignored, whitespace around the name and the value is trimmed, and a value wrapped in a matching pair of " or ' keeps the whitespace inside the quotes. A name is letters, digits and underscores, not starting with a digit; anything else is malformed, including the export NAME=VALUE a shell would accept. There is no escaping, no ${...} substitution, no value spanning two lines, and no comment after a value.
An environment variable's value is taken verbatim instead of trimmed and unquoted, so the same text can mean different things depending on which source supplied it. Where one name is set twice in the same source, the first of them wins, which is what getenv returns for the environment.
Remove Sensitive.from_env
EnvSecrets now reads environment variables, so Sensitive.from_env is gone. It required Env.vars at every place a secret was extracted, and because it was a constructor on a generic class, Sensitive[U64].from_env(...) compiled and then raised an error at runtime.
Before:
let user = Sensitive[String].from_env(env.vars, "USER")?After:
let secrets = EnvSecrets(env.vars)
let user = secrets("USER")?[0.2.0] - 2026-08-07
0.1.1
Initial Release
A package to help you manage sensitive data.
A common pattern we see while doing security audits is that programmers will unintentionally output sensitive data to logfiles, or other destinations where it may not be appropriate to write to. The purpose of this package is to provide a mechanism to "tag" sensitive variables in such a way that accessing them becomes a deliberate act, and simply calling .string() will return "[REDACTED]".
There is an additional helpful constructor which reads a specified Environmental Variable into a Sensitive[String], a very common pattern when passing sensitive data into applications.
Example
use "sensitive"
actor Main
let life_the_universe_and_everything: Sensitive[U8]
let user: Sensitive[String]
new create(env: Env) =>
// Secrets passed via direct assignment:
life_the_universe_and_everything = Sensitive[U8](42)
env.out.print("The secret to Life, the Universe, and Everything is… " + life_the_universe_and_everything.string())
if (life_the_universe_and_everything.expose() == 42) then
env.out.print("… yet internally, I know the score.")
end
// Secrets passed via Environmental Variable:
try
user = Sensitive[String].from_env(env.vars, "USER")?
env.out.print("I read the contents of $USER, and it's: " + user.string())
env.out.print("… but it does have " + user.expose().size().string() + " characters")
else
user = Sensitive[String]("")
env.out.print("Apparently there is no $USER here")
endAPI Documentation
https://sensitive.contact.red/
[0.1.1] - 2026-04-14
Added
- Initial Release - Documentation https://sensitive.contact.red/