Alchemy Effect resource for decrypting SOPS files into redacted secret outputs.
alchemy-sops decrypts SOPS files, parses the decrypted document, and returns
Alchemy outputs whose scalar leaves are Redacted<string>. It prefers the
native sops-age backend for age-encrypted JSON/YAML/dotenv files and keeps the
sops CLI backend for binary files, custom SOPS flags, and non-age backends.
bun add alchemy-sopsThe native backend does not require a sops binary. Install sops only when
you need backend: "cli" or automatic fallback for SOPS features not supported
by sops-age.
import * as Alchemy from "alchemy";
import * as Output from "alchemy/Output";
import { SopsFile, SopsFileProvider } from "alchemy-sops";
import * as Config from "effect/Config";
import * as Effect from "effect/Effect";
export default Alchemy.Stack(
"App",
{
providers: SopsFileProvider(),
state: Alchemy.localState(),
},
Effect.gen(function* () {
const secrets = yield* SopsFile("Secrets", {
path: "./secrets.enc.yaml",
format: "yaml",
ageKey: Config.redacted("SOPS_AGE_KEY"),
secrets: {
DATABASE_URL: "database.url",
API_TOKEN: "api.token",
},
});
return {
sourceHash: secrets.sourceHash,
databaseUrl: Output.map(secrets.secrets, (s) => s.DATABASE_URL),
};
}),
);For local files, backend: "auto" is the default. It tries sops-age first for
structured age-encrypted files, then falls back to the CLI when a local path
source is available. Use backend: "sops-age" to require the native backend or
backend: "cli" to force the binary.
Alchemy programs can avoid local filesystem and process APIs by using inline encrypted content or a URL source with the native backend:
import { SopsFile } from "alchemy-sops";
const secrets = yield* SopsFile("WorkerSecrets", {
content: encryptedSopsJson,
format: "json",
backend: "sops-age",
ageKey: workerEnv.SOPS_AGE_KEY,
});The Alchemy resource entrypoint still imports Alchemy. For code that is bundled
directly into an edge runtime, use the low-level alchemy-sops/edge subpath:
import { runSopsAge } from "alchemy-sops/edge";Every string-like option accepts the same shapes as Alchemy SecretInput:
stringRedacted<string>Effect<string | Redacted<string>>Config<string | Redacted<string>>
Supported options:
path,content, orurl: exactly one encrypted source is requiredcwd,sopsBinarybackend:auto,sops-age, orcliformat:auto,json,yaml,dotenv,text, orbinaryinputType,outputType: input/output format hintsextract: passed tosops --extractfor CLI and as a key path forsops-agesopsArgs: extra CLI args; requiresbackend: "cli"or CLI fallbackenv,ageKey,ageKeyFile: SOPS environment inputs;sops-ageuses directageKey/SOPS_AGE_KEYsecrets: output-name to dot-path selectorscache,timeoutMs,retry
The resource returns:
data: nested document with scalar leaves redactedflat: dot-path map of all redacted leavessecrets: selected redacted leaves, or all leaves whensecretsis omittedsourceHash: SHA-256 digest of the encrypted source plus non-secret optionspath,format,version
cache defaults to true. If the encrypted source digest and resource version
are unchanged, the provider returns the previous redacted output without
decrypting again. Set cache: false to force decryption on every deploy.
Redacted<string> prevents accidental printing and logging, but Alchemy state
stores still persist values so they can be revived later. Use a state store you
trust for decrypted secrets.