Haskell SDK
The Haskell SDK (secretspec-hs) is a thin client over the secretspec-ffi C
ABI, linked at build time via the Haskell FFI. Resolution happens in the Rust
core, so the SDK inherits every provider with no Haskell-side logic.
Quick start
Section titled “Quick start”{-# LANGUAGE OverloadedStrings #-}
import qualified Data.Map.Strict as Map
import Data.Function ((&))
import qualified SecretSpec as S
main :: IO ()
main = do
resolved <-
S.load
( S.builder
& S.withProvider "keyring://"
& S.withProfile "production"
& S.withReason "boot web app"
)
print (S.resolvedProvider resolved, S.resolvedProfile resolved)
case Map.lookup "DATABASE_URL" (S.resolvedSecrets resolved) of
Just db -> print (S.get db) -- the value, or the file path for as_path secrets
Nothing -> pure ()
S.setAsEnv resolved -- export everything into the process environment
S.close resolved
A missing required secret throws MissingRequiredError; any other failure
throws SecretSpecError (with a stable errorKind).
as_path secrets are materialized to temp files that outlive the call; call
S.close resolved when done so they do not accumulate in the temp dir.
Scopes (0.17+)
Section titled “Scopes (0.17+)”Use withScope "api" to resolve only a named [scopes.api] subset. The
selected name is available through resolvedScope and reportScope:
{-# LANGUAGE OverloadedStrings #-}
import Data.Function ((&))
import qualified SecretSpec as S
main :: IO ()
main = do
resolved <- S.load (S.builder & S.withScope "api")
S.close resolved
Value-free report
Section titled “Value-free report”S.report returns the inventory/preflight view: per-secret status and
provenance, never a value. Unlike load, it does not throw when a required
secret is missing — that secret appears as a SecretReport with srStatus
"missing_required".
{-# LANGUAGE OverloadedStrings #-}
import Data.Function ((&))
import qualified SecretSpec as S
main :: IO ()
main = do
rep <- S.report (S.builder & S.withProfile "production")
mapM_ (\s -> print (S.srName s, S.srStatus s, S.srRequired s)) (S.reportSecrets rep)
Typed access (codegen)
Section titled “Typed access (codegen)”Generate a typed record with secretspec schema plus
quicktype, then decode S.fieldsJson resolved:
secretspec schema | quicktype -s schema --top-level SecretSpec --lang haskell -o Secrets.hsBuilding
Section titled “Building”The build links the secretspec-ffi archive statically. Stage the .a in a
directory of its own (so the linker picks the archive, not the co-located
.so) and pass its native dependencies to the linker:
cargo build -p secretspec-ffiTARGET="$(cargo metadata --no-deps --format-version 1 \ | grep -o '"target_directory":"[^"]*"' | head -1 | sed 's/.*:"\(.*\)"/\1/')"
# Stage the staticlib alone, and capture its native-static-libs for the linker.LIBDIR="$(mktemp -d)"cp "$TARGET/debug/libsecretspec_ffi.a" "$LIBDIR/"NATIVE_LIBS="$(cargo rustc -q -p secretspec-ffi --crate-type staticlib -- \ --print native-static-libs 2>&1 | sed -n 's/^note: native-static-libs: //p' | tail -1)"
cabal build --extra-lib-dirs="$LIBDIR" --ghc-options="-optl${NATIVE_LIBS// / -optl}"cabal test --extra-lib-dirs="$LIBDIR" --ghc-options="-optl${NATIVE_LIBS// / -optl}"Linking with pkg-config (0.19+)
Section titled “Linking with pkg-config (0.19+)”Install one library type with cargo-c:
# Use "static" (the default) or "shared"; use separate prefixes for both.bash secretspec-ffi/scripts/cinstall.sh "$PREFIX" staticThen use the same Cabal flag for either type:
cd secretspec-hsPKG_CONFIG_PATH="$PREFIX/lib/pkgconfig" cabal build -f use-pkg-configPKG_CONFIG_PATH="$PREFIX/lib/pkgconfig" cabal test -f use-pkg-configA shared install in a non-system prefix also requires PREFIX/lib in the
platform’s runtime library search path.