npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

alchemy-sops

v0.2.0

Published

Alchemy Effect resource for decrypting SOPS files into redacted secret outputs.

Readme

alchemy-sops

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.

Contents

Install

bun add alchemy-sops

The 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.

Usage

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.

Cloudflare Secrets Store Action

Use CloudflareSopsSecrets when Cloudflare Workers should receive secrets from Cloudflare Secrets Store instead of Alchemy state. It is the high-level wrapper around the exported CloudflareSopsSecretsAction.

The wrapper reads a local encrypted SOPS file before registering the Action, passes ciphertext into Action state, decrypts during deploy, and imports selected values into the target store. Plaintext is sent to Cloudflare Secrets Store but is not persisted as Action input.

A stack using the Action needs:

  • Cloudflare providers and state configured in the Alchemy stack
  • A Cloudflare.SecretsStore resource or { accountId, storeId } reference
  • A deploy-time SOPS identity, preferably passed as Redacted<string>
  • A secrets map from Cloudflare secret names to decrypted dot-path selectors, or no secrets map when all scalar leaves should be imported
import * as Alchemy from "alchemy";
import * as Cloudflare from "alchemy/Cloudflare";
import {
  CloudflareSopsSecrets,
  cloudflareSopsWorkerBindings,
} from "alchemy-sops";
import * as Effect from "effect/Effect";
import * as Redacted from "effect/Redacted";

export default Alchemy.Stack(
  "Worker",
  {
    providers: Cloudflare.providers(),
    state: Cloudflare.state(),
  },
  Effect.gen(function* () {
    const store = yield* Cloudflare.SecretsStore("Secrets");

    const imported = yield* CloudflareSopsSecrets("WorkerSecrets", {
      path: "./secrets.enc.yaml",
      format: "yaml",
      backend: "sops-age",
      store,
      ageKey: Redacted.make(process.env.SOPS_AGE_KEY!),
      scopes: ["workers"],
      comment: "imported by alchemy-sops",
      secrets: {
        API_TOKEN: "api.token",
        DATABASE_URL: "database.url",
      },
    });

    const worker = yield* Cloudflare.Worker("Api", {
      main: "./src/worker.ts",
    });
    yield* worker.bind(
      "sops-secrets",
      cloudflareSopsWorkerBindings(imported, [
        "API_TOKEN",
        "DATABASE_URL",
      ]),
    );

    return {
      url: worker.url,
    };
  }),
);

secrets maps Cloudflare secret names to paths in the decrypted document. Omit it to import every scalar leaf; generated names are derived from dot paths, and namePrefix can add a prefix to every generated name.

cloudflareSopsWorkerBindings(imported, ["API_TOKEN"]) binds a Worker variable to the Secrets Store secret with the same name. Pass an object when the Worker binding name should differ from the stored secret name:

yield* worker.bind(
  "sops-secrets",
  cloudflareSopsWorkerBindings(imported, {
    API_TOKEN: "WORKER_API_TOKEN",
  }),
);

Run the stack with your normal Alchemy deploy command. The Action runs when its input changes, including the encrypted file content, backend options, selected secret paths, scopes, comments, and target store.

Existing Secrets Store entries are replaced by default because Cloudflare does not allow patching a secret value. Set replaceExisting: false when you only want to converge scopes and comments for an existing secret name. The Cloudflare credentials used by the stack must be allowed to manage the target Secrets Store, and Worker deploy permissions are also needed when the same stack binds those secrets to a Worker.

Most stacks should call CloudflareSopsSecrets. Use CloudflareSopsSecretsAction directly only when the encrypted content is already available and you want to pass the Action input yourself:

import { CloudflareSopsSecretsAction } from "alchemy-sops";
import * as Redacted from "effect/Redacted";

const imported = yield* CloudflareSopsSecretsAction("WorkerSecrets", {
  path: "secrets.enc.yaml",
  content: encryptedSopsYaml,
  format: "yaml",
  backend: "sops-age",
  store: {
    accountId: "account-id",
    storeId: "store-id",
  },
  ageKey: Redacted.make(process.env.SOPS_AGE_KEY!),
  secrets: {
    API_TOKEN: "api.token",
  },
});

Edge usage

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";

Inputs

Every string-like option accepts the same shapes as Alchemy SecretInput:

  • string
  • Redacted<string>
  • Effect<string | Redacted<string>>
  • Config<string | Redacted<string>>

Supported options:

  • path, content, or url: exactly one encrypted source is required
  • cwd, sopsBinary
  • backend: auto, sops-age, or cli
  • format: auto, json, yaml, dotenv, text, or binary
  • inputType, outputType: input/output format hints
  • extract: passed to sops --extract for CLI and as a key path for sops-age
  • sopsArgs: extra CLI args; requires backend: "cli" or CLI fallback
  • env, ageKey, ageKeyFile: SOPS environment inputs; sops-age uses direct ageKey / SOPS_AGE_KEY
  • secrets: output-name to dot-path selectors
  • cache, timeoutMs, retry

CloudflareSopsSecrets shares the decrypt options above and adds:

  • store: Cloudflare Secrets Store resource or { accountId, storeId }
  • namePrefix: prefix for generated Cloudflare secret names when secrets is omitted
  • scopes: Cloudflare Secrets Store scopes; defaults to ["workers"]
  • comment: free-form Cloudflare Secrets Store comment
  • replaceExisting: delete and recreate matching existing secrets so values converge; defaults to true

CloudflareSopsSecretsAction also accepts content, the encrypted SOPS ciphertext to use as Action input. The CloudflareSopsSecrets wrapper fills that field by reading path.

Outputs

The resource returns:

  • data: nested document with scalar leaves redacted
  • flat: dot-path map of all redacted leaves
  • secrets: selected redacted leaves, or all leaves when secrets is omitted
  • sourceHash: SHA-256 digest of the encrypted source plus non-secret options
  • path, 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.

The Cloudflare Action returns accountId, storeId, path, and an imported array containing each Cloudflare secret name, source dot path, secret id, and status.

Security note

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.