envpkt 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -141,9 +141,9 @@ See [`examples/`](./examples/) for more configurations.
141
141
 
142
142
  Sealed packets embed age-encrypted secret values directly in `envpkt.toml`. This makes your config fully self-contained — no external secrets backend needed at runtime.
143
143
 
144
- > **Requires the [`age`](https://github.com/FiloSottile/age) CLI.** envpkt shells out to `age` to seal and unseal `encrypted_value` — it's a runtime dependency, not bundled, and there is **no built-in / JS fallback**. Every path that decrypts a sealed value (`boot()`, `exec`, `env export`, `env github`, the GitHub Action) needs `age` on `PATH` plus the private key.
144
+ > **No `age` install needed.** envpkt seals, unseals and makes keys in-process with [age-encryption](https://github.com/FiloSottile/typage), the TypeScript implementation by age's author. The format is standard [age](https://github.com/FiloSottile/age): `age -d` decrypts what envpkt seals, and envpkt unseals values sealed by the `age` CLI. Every path that decrypts a sealed value (`boot()`, `exec`, `env export`, `env github`, the GitHub Action) needs only the private key.
145
145
  >
146
- > **Gotcha — partial resolution hides a missing `age`.** Plaintext `[env.*]` defaults and fnox-backed values resolve _without_ `age`, so a config can look like it's working while every sealed secret silently fails to decrypt. If sealed values aren't showing up, confirm `age` is installed — `envpkt doctor` reports its presence.
146
+ > envpkt reads plain age key files, like the ones `envpkt keygen` and `age-keygen` write. A passphrase-protected key file, an age plugin key or an SSH key fails with an error that names the file; `envpkt doctor` shows which key it found and why it can't use it.
147
147
 
148
148
  ### Setup
149
149
 
@@ -180,7 +180,7 @@ At runtime, sealed values are automatically decrypted:
180
180
  ```typescript
181
181
  import { boot } from "envpkt"
182
182
 
183
- const result = boot() // decrypts sealed values, injects into process.env
183
+ const result = await boot() // decrypts sealed values, injects into process.env
184
184
  ```
185
185
 
186
186
  Mixed mode is supported — sealed values take priority, with fnox as fallback for keys without `encrypted_value`.
@@ -190,7 +190,7 @@ Mixed mode is supported — sealed values take priority, with fnox as fallback f
190
190
  Whether envpkt can replace a cloud secret store depends on one thing: **do you control how the process starts?**
191
191
 
192
192
  - **You launch it** (server, container, VM, k8s, systemd) → `envpkt exec -- yourapp` or `boot()` injects the decrypted values straight into the process environment. No cloud secret store needed: the sealed `envpkt.toml` ships with the app, and only the age key has to reach the runtime (N secrets collapse to 1 key).
193
- - **A platform launches it** (Cloudflare Workers, AWS Lambda, Vercel) → you don't own the launch, and the `age` CLI can't run inside the per-request isolate, so the platform's secret store is unavoidable. envpkt becomes the source of truth that populates and audits that store — not the injector.
193
+ - **A platform launches it** (Cloudflare Workers, AWS Lambda, Vercel) → you don't own the launch, so the platform's secret store is unavoidable. envpkt becomes the source of truth that populates and audits that store — not the injector.
194
194
 
195
195
  > Rule of thumb: **own the launch → `exec`/`boot`, no store. Platform owns the launch → store required, envpkt feeds and audits it.** Full details: [Runtime injection guide](https://envpkt.dev/guides/runtime-injection/).
196
196
 
@@ -201,9 +201,6 @@ A composite action resolves the credentials in `envpkt.toml` into the CI job —
201
201
  ```yaml
202
202
  - uses: actions/checkout@v5
203
203
 
204
- # Sealed packets are decrypted with the `age` CLI (not preinstalled on runners).
205
- - run: sudo apt-get update && sudo apt-get install -y age
206
-
207
204
  - uses: jordanburke/envpkt@v0
208
205
  with:
209
206
  config: ./envpkt.toml
@@ -214,11 +211,11 @@ A composite action resolves the credentials in `envpkt.toml` into the CI job —
214
211
  - run: ./deploy.sh # sees the resolved vars; secret values redacted in the log
215
212
  ```
216
213
 
217
- **How it works.** Commit sealed (`encrypted_value`) packets to the repo and supply the age private key as the `ENVPKT_AGE_KEY` secret. `boot()` materializes the inline key to a `0600` temp file to decrypt, then [`env github`](#envpkt-env-github) masks each secret (`::add-mask::`) and writes it to `$GITHUB_ENV`. Identity precedence: `identity.key_file` → `ENVPKT_AGE_KEY_FILE` → `ENVPKT_AGE_KEY` (inline) → `~/.envpkt/age-key.txt`.
214
+ **How it works.** Commit sealed (`encrypted_value`) packets to the repo and supply the age private key as the `ENVPKT_AGE_KEY` secret. `boot()` decrypts with the inline key in memory (it never touches disk), then [`env github`](#envpkt-env-github) masks each secret (`::add-mask::`) and writes it to `$GITHUB_ENV`. Identity precedence: `identity.key_file` → `ENVPKT_AGE_KEY_FILE` → `ENVPKT_AGE_KEY` (inline) → `~/.envpkt/age-key.txt`.
218
215
 
219
216
  **Inputs:** `config`, `version` (npm version to run, default `latest`), `strict`, `profile`.
220
217
 
221
- > **The [`age`](https://github.com/FiloSottile/age) CLI is required on the runner to unseal `encrypted_value` packets** (install it first, as above) — envpkt shells out to `age`, with no bundled/JS fallback. Without it, sealed secrets won't decrypt or be injected, even though plaintext `[env.*]` defaults and fnox-backed values still resolve — which can mask the missing dependency until a sealed value turns up empty downstream. Reference `@v0` for the moving major tag (re-pointed to each `0.x` release), or pin an exact release like `@v0.13.6` for immutability. `@v1` ships when envpkt reaches 1.0. Node is assumed present; add `actions/setup-node` first to pin a version.
218
+ > The runner needs no `age` install: envpkt unseals in-process. Reference `@v0` for the moving major tag (re-pointed to each `0.x` release), or pin an exact release like `@v0.13.6` for immutability. `@v1` ships when envpkt reaches 1.0. The action does not set up Node: it runs on the runner's Node, which must be 22.12 or later. Add `actions/setup-node` first if the runner's Node is older.
222
219
 
223
220
  ### Anti-rot CI gate
224
221
 
@@ -408,16 +405,19 @@ Exit codes: `0` = healthy, `1` = degraded, `2` = critical.
408
405
 
409
406
  ### `envpkt doctor`
410
407
 
411
- One-shot environment check: is the `age` CLI installed, is a config resolvable here, and do its sealed secrets decrypt with an available key?
408
+ One-shot environment check: is a config resolvable here, is a usable age key found, and do its sealed secrets decrypt with it? It also reports the built-in age-encryption version.
412
409
 
413
410
  ```bash
414
411
  envpkt doctor
415
- # ✓ age v1.2.0
416
- # ✓ config /path/to/envpkt.toml
417
- # ✓ secrets 5 resolved, 0 skipped
412
+ # ✓ age built in (age-encryption 0.3.1)
413
+ # ✓ config /path/to/envpkt.toml
414
+ # ✓ secrets 5 resolved, 0 skipped
415
+ # • audit healthy
416
+ #
417
+ # ✓ no issues
418
418
  ```
419
419
 
420
- If `age` is missing it prints the platform-specific install command; if a sealed packet has no key, it lists the paths it searched. Exits non-zero when a check fails.
420
+ If a sealed packet has no key, it lists the paths it searched; if the key file can't be used (unreadable, passphrase-protected, a plugin or SSH key), it says why. Exits non-zero when a check fails.
421
421
 
422
422
  ### `envpkt resolve`
423
423
 
@@ -648,18 +648,18 @@ These are searched after CWD but before the built-in candidate paths. Useful for
648
648
 
649
649
  ## Library API
650
650
 
651
- envpkt is also available as a TypeScript library with a functional programming API built on [functype](https://github.com/jordanburke/functype). All functions return `Either<Error, Result>` or `Option<T>` — no thrown exceptions.
651
+ envpkt is also available as a TypeScript library with a functional programming API built on [functype](https://github.com/jordanburke/functype). Fallible functions return `Either<Error, Result>` or `Option<T>` instead of throwing; `boot()` is the one throwing convenience wrapper. Functions that encrypt or decrypt (`boot`, `bootSafe`, `ageEncrypt`, `ageDecrypt`, `sealSecrets`, `unsealSecrets`, `generateKeypair`) return Promises, so `await` them. Without `await`, `boot()` returns before it injects anything.
652
652
 
653
653
  ```typescript
654
654
  import { boot, bootSafe, loadConfig, computeAudit, scanFleet, resolveConfig } from "envpkt"
655
655
 
656
656
  // Boot API — load config, resolve catalog, audit, inject secrets
657
- const result = boot({ configPath: "envpkt.toml", inject: true })
657
+ const result = await boot({ configPath: "envpkt.toml", inject: true })
658
658
  console.log(result.audit.status) // "healthy" | "degraded" | "critical"
659
659
  console.log(result.configSource) // "flag" | "env" | "cwd" | "search"
660
660
 
661
661
  // Safe variant returns Either instead of throwing
662
- const safe = bootSafe({ configPath: "envpkt.toml" })
662
+ const safe = await bootSafe({ configPath: "envpkt.toml" })
663
663
  safe.fold(
664
664
  (err) => console.error("Boot failed:", err._tag),
665
665
  (result) => console.log(`${result.injected.length} secrets injected`),
@@ -693,7 +693,7 @@ console.log(`${fleet.total_agents} agents, ${fleet.total_secrets} secrets`)
693
693
  import { boot } from "envpkt"
694
694
 
695
695
  // Resolve and inject credentials before agent startup
696
- const result = boot({ configPath: "envpkt.toml", inject: true })
696
+ const result = await boot({ configPath: "envpkt.toml", inject: true })
697
697
  console.log(`${result.audit.status} — ${result.injected.length} secrets loaded`)
698
698
 
699
699
  // Now start your agent framework — process.env is populated
@@ -764,23 +764,31 @@ matchEnvVar("OPENAI_API_KEY", "sk-test123").fold(
764
764
  ### Seal API
765
765
 
766
766
  ```typescript
767
- import { ageEncrypt, ageDecrypt, sealSecrets, unsealSecrets } from "envpkt"
767
+ import { ageEncrypt, ageDecrypt, describeIdentityError, sealSecrets, unsealSecrets, unwrapAgentKey } from "envpkt"
768
768
 
769
769
  // Encrypt a single value
770
- const encrypted = ageEncrypt("sk-my-api-key", "age1ql3z7hjy...")
770
+ const encrypted = await ageEncrypt("sk-my-api-key", "age1ql3z7hjy...")
771
771
  encrypted.fold(
772
772
  (err) => console.error("Encrypt failed:", err.message),
773
773
  (ciphertext) => console.log(ciphertext), // -----BEGIN AGE ENCRYPTED FILE-----
774
774
  )
775
775
 
776
+ // Decrypting takes secret keys (AGE-SECRET-KEY-…), not a path; unwrapAgentKey reads them from a key file
777
+ const keys = unwrapAgentKey("/path/to/identity.txt").fold(
778
+ (err) => {
779
+ throw new Error(describeIdentityError(err))
780
+ },
781
+ (k) => k,
782
+ )
783
+
776
784
  // Decrypt a single value
777
- const decrypted = ageDecrypt(ciphertext, "/path/to/identity.txt")
785
+ const decrypted = await ageDecrypt(ciphertext, keys)
778
786
 
779
- // Seal all secrets in a config's meta
780
- const sealed = sealSecrets(config.meta, { OPENAI_API_KEY: "sk-..." }, recipientPublicKey)
787
+ // Seal all secrets in a config's [secret] table
788
+ const sealed = await sealSecrets(config.secret ?? {}, { OPENAI_API_KEY: "sk-..." }, recipientPublicKey)
781
789
 
782
790
  // Unseal all encrypted_value entries
783
- const values = unsealSecrets(config.meta, "/path/to/identity.txt")
791
+ const values = await unsealSecrets(config.secret ?? {}, keys)
784
792
  values.fold(
785
793
  (err) => console.error("Unseal failed:", err.message),
786
794
  (secrets) => console.log(secrets), // { OPENAI_API_KEY: "sk-..." }