@arki/env 0.0.9 → 0.2.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.
Files changed (130) hide show
  1. package/CHANGELOG.md +196 -0
  2. package/README.md +231 -62
  3. package/dist/api.json +597 -0
  4. package/dist/core/define-env.d.ts +29 -4
  5. package/dist/core/define-env.d.ts.map +1 -1
  6. package/dist/core/define-env.js +85 -15
  7. package/dist/core/define-env.js.map +1 -1
  8. package/dist/core/describe.d.ts +29 -0
  9. package/dist/core/describe.d.ts.map +1 -0
  10. package/dist/core/describe.js +25 -0
  11. package/dist/core/describe.js.map +1 -0
  12. package/dist/core/errors.d.ts +10 -0
  13. package/dist/core/errors.d.ts.map +1 -0
  14. package/dist/core/errors.js +13 -0
  15. package/dist/core/errors.js.map +1 -0
  16. package/dist/core/index.d.ts +7 -2
  17. package/dist/core/index.d.ts.map +1 -1
  18. package/dist/core/index.js +6 -2
  19. package/dist/core/index.js.map +1 -1
  20. package/dist/core/secret.d.ts +56 -0
  21. package/dist/core/secret.d.ts.map +1 -0
  22. package/dist/core/secret.js +85 -0
  23. package/dist/core/secret.js.map +1 -0
  24. package/dist/errors.json +128 -0
  25. package/dist/index.d.ts +2 -1
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +3 -2
  28. package/dist/index.js.map +1 -1
  29. package/dist/node/codec.d.ts +63 -0
  30. package/dist/node/codec.d.ts.map +1 -0
  31. package/dist/node/codec.js +182 -0
  32. package/dist/node/codec.js.map +1 -0
  33. package/dist/node/edit.d.ts +112 -0
  34. package/dist/node/edit.d.ts.map +1 -0
  35. package/dist/node/edit.js +268 -0
  36. package/dist/node/edit.js.map +1 -0
  37. package/dist/node/errors.d.ts +119 -0
  38. package/dist/node/errors.d.ts.map +1 -0
  39. package/dist/node/errors.js +144 -0
  40. package/dist/node/errors.js.map +1 -0
  41. package/dist/node/files.d.ts +9 -0
  42. package/dist/node/files.d.ts.map +1 -0
  43. package/dist/node/files.js +35 -0
  44. package/dist/node/files.js.map +1 -0
  45. package/dist/node/index.d.ts +20 -0
  46. package/dist/node/index.d.ts.map +1 -0
  47. package/dist/node/index.js +13 -0
  48. package/dist/node/index.js.map +1 -0
  49. package/dist/node/key-schemas.d.ts +25 -0
  50. package/dist/node/key-schemas.d.ts.map +1 -0
  51. package/dist/node/key-schemas.js +38 -0
  52. package/dist/node/key-schemas.js.map +1 -0
  53. package/dist/node/keys.d.ts +54 -0
  54. package/dist/node/keys.d.ts.map +1 -0
  55. package/dist/node/keys.js +76 -0
  56. package/dist/node/keys.js.map +1 -0
  57. package/dist/node/load.d.ts +103 -0
  58. package/dist/node/load.d.ts.map +1 -0
  59. package/dist/node/load.js +306 -0
  60. package/dist/node/load.js.map +1 -0
  61. package/dist/node/operation-telemetry.d.ts +3 -0
  62. package/dist/node/operation-telemetry.d.ts.map +1 -0
  63. package/dist/node/operation-telemetry.js +53 -0
  64. package/dist/node/operation-telemetry.js.map +1 -0
  65. package/dist/node/references.d.ts +16 -0
  66. package/dist/node/references.d.ts.map +1 -0
  67. package/dist/node/references.js +72 -0
  68. package/dist/node/references.js.map +1 -0
  69. package/dist/node/register.d.ts +2 -0
  70. package/dist/node/register.d.ts.map +1 -0
  71. package/dist/node/register.js +19 -0
  72. package/dist/node/register.js.map +1 -0
  73. package/dist/{contracts → presets}/common.d.ts +1 -1
  74. package/dist/presets/common.d.ts.map +1 -0
  75. package/dist/{contracts → presets}/common.js +4 -8
  76. package/dist/presets/common.js.map +1 -0
  77. package/dist/{contracts → presets}/index.d.ts +5 -5
  78. package/dist/presets/index.d.ts.map +1 -0
  79. package/dist/presets/index.js +15 -0
  80. package/dist/presets/index.js.map +1 -0
  81. package/dist/{contracts → presets}/parsing.d.ts +2 -2
  82. package/dist/presets/parsing.d.ts.map +1 -0
  83. package/dist/{contracts → presets}/parsing.js +13 -5
  84. package/dist/presets/parsing.js.map +1 -0
  85. package/dist/{contracts → presets}/urls.d.ts +1 -1
  86. package/dist/presets/urls.d.ts.map +1 -0
  87. package/dist/{contracts → presets}/urls.js +1 -1
  88. package/dist/presets/urls.js.map +1 -0
  89. package/docs/agent-guide.md +121 -0
  90. package/docs/upgrades/0.0.11-to-0.1.0.md +62 -0
  91. package/docs/upgrades/0.1.0-to-0.2.0.md +73 -0
  92. package/docs/upgrades/typed-node-outcomes.md +13 -0
  93. package/llms.txt +30 -0
  94. package/package.json +27 -20
  95. package/src/core/define-env.ts +101 -20
  96. package/src/core/describe.ts +52 -0
  97. package/src/core/errors.ts +14 -0
  98. package/src/core/index.ts +7 -2
  99. package/src/core/secret.ts +127 -0
  100. package/src/index.ts +3 -2
  101. package/src/node/codec.ts +220 -0
  102. package/src/node/edit.ts +279 -0
  103. package/src/node/errors.ts +147 -0
  104. package/src/node/files.ts +41 -0
  105. package/src/node/index.ts +48 -0
  106. package/src/node/key-schemas.ts +44 -0
  107. package/src/node/keys.ts +93 -0
  108. package/src/node/load.ts +386 -0
  109. package/src/node/operation-telemetry.ts +56 -0
  110. package/src/node/references.ts +90 -0
  111. package/src/node/register.ts +18 -0
  112. package/src/{contracts → presets}/common.ts +4 -8
  113. package/src/presets/index.ts +18 -0
  114. package/src/{contracts → presets}/parsing.ts +12 -5
  115. package/src/{contracts → presets}/urls.ts +1 -1
  116. package/dist/contracts/common.d.ts.map +0 -1
  117. package/dist/contracts/common.js.map +0 -1
  118. package/dist/contracts/index.d.ts.map +0 -1
  119. package/dist/contracts/index.js +0 -14
  120. package/dist/contracts/index.js.map +0 -1
  121. package/dist/contracts/parsing.d.ts.map +0 -1
  122. package/dist/contracts/parsing.js.map +0 -1
  123. package/dist/contracts/urls.d.ts.map +0 -1
  124. package/dist/contracts/urls.js.map +0 -1
  125. package/dist/dot.d.ts +0 -67
  126. package/dist/dot.d.ts.map +0 -1
  127. package/dist/dot.js +0 -65
  128. package/dist/dot.js.map +0 -1
  129. package/src/contracts/index.ts +0 -17
  130. package/src/dot.ts +0 -94
package/CHANGELOG.md ADDED
@@ -0,0 +1,196 @@
1
+ # @arki/env
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 307dcf6: Encrypted env files in the dotenvx 2.x format. New `@arki/env/node`: `loadEnvFiles()` reads `.env.<environment>` and `.env.local` from the repository root down to the working directory, decrypts `encrypted:` values (via `eciesjs`, byte-compatible with dotenvx), resolves `op://`, `infisical://` and `doppler://` references, and writes what the real environment does not set into it (real values win; differing ones are reported as shadowed). Also the codec and file edits behind `dot env`. New side-effect entry `@arki/env/node/register`. `@arki/env/core` adds `describeEnv`, `isSecretSchema` and `SECRET_SCHEMA`, and `defineEnv` now refuses a value that is still `encrypted:` instead of accepting ciphertext as a string. New codes `ARKI_ENV_FILES_E001`–`E003`.
8
+ - a7a4a9d: Fix the foundation findings of the T08 review.
9
+
10
+ - `@arki/log`: `maskSecrets` and `otelSink` handle an object that contains itself (the cycle becomes `[Circular]`) instead of dropping the record. A `.to('name')` logger whose channel is not defined no longer throws into the caller at log time; the fault is reported once and the channel is skipped. `file()` throws a coded `LogError` (`ARKI_LOG_E006`) when it cannot open its path.
11
+ - `@arki/async`: `debounce` and `throttle` take an `onError` option. An error thrown by the callback, or a promise it returns that rejects, goes there. Without it the error is rethrown from a microtask. The schedule stays consistent and a leading call never throws into the caller.
12
+ - `@arki/env`: `rotateEnvFiles` returns an `InvalidInput` failure for an empty list or files of two environments (it threw a plain `Error`). The default `onValidationError` of `defineEnv` throws an `UnexpectedFailure` that carries the new `env.invalid_environment` failure (`InvalidEnvironment`).
13
+
14
+ - 818fd9c: The Dot adapter moved from `@arki/env/dot` to `@arki/dot/env`. `@arki/env` drops its `@arki/dot` peer; `@arki/dot` depends on `@arki/env`. `@arki/env/core` exports `formatIssues`. Migration: `packages/env/docs/upgrades/0.1.0-to-0.2.0.md`.
15
+ - 0511f4a: Return precise canonical env node outcomes, sanitize provider diagnostics, validate distinct public/private key domains and preserve recovery keys across partial rotation. Share bounded load deadlines and cooperative cancellation with provider reads, and instrument owned node/provider operations through OpenTelemetry without installing an SDK. Migrate Dot env commands and database preparation to handle the returned outcomes.
16
+
17
+ Keep explicit zero/empty defaults and report JSON parsing/output rejection through schema issues. Storage and crypto remain dotenvx-compatible; see `@arki/env/docs/upgrades/typed-node-outcomes.md`.
18
+
19
+ no demo impact: node env operations are exercised through the CLI and package tests.
20
+
21
+ - cd969bd: Breaking: `@arki/env/contracts` is now `@arki/env/presets`. `contracts` is `presets` (also from `@arki/env` and `@arki/env/core`), and `commonContracts`, `urlContracts`, `parseContracts` are `commonPresets`, `urlPresets`, `parsePresets`. The validators are unchanged. See `docs/upgrades/0.1.0-to-0.2.0.md`.
22
+
23
+ ### Patch Changes
24
+
25
+ - 1e47612: `Lazy<T>` and `resolveLazy` move from `@arki/ts` to `@arki/dot/plugin` as `Deferred<T>` and `resolveDeferred`. They are the "value now, or a thunk the adapter calls at boot" shape that Dot adapters accept, and the old name clashed with Dot's own `Lazy<Value, Cleanup>` service handle. Behaviour is unchanged. `@arki/ts` no longer exports them; to upgrade, import `Deferred` and `resolveDeferred` from `@arki/dot/plugin`.
26
+
27
+ `@arki/http` (`port`) and `@arki/kv` (`kv()`, `kv.optional()`) now type their deferred options as `Deferred<T>`, the same shape as before, and no longer depend on `@arki/ts`. `@arki/env` drops an `@arki/ts` dependency it never imported.
28
+
29
+ demo impact: none (neither demo imports `Lazy` or `resolveLazy`).
30
+
31
+ ## 0.1.0
32
+
33
+ ### Minor Changes
34
+
35
+ - 1e15576: `defineEnv()` `server`, `client` and `shared` take records of any Standard Schema (Zod, Valibot, ArkType, hand-written), not only `ZodType`; values are typed as each schema's output.
36
+ `@arki/env/dot`: `EnvDotOptions`, `EnvServices` and `env()` take `Record<string, StandardSchemaV1>` instead of `Record<string, ZodType>`.
37
+ - 75d6fb9: **Validation now runs in production by default.** `defineEnv` used to skip validation whenever `NODE_ENV !== 'development'`, so an app without an explicit `skipValidation` booted production on an unchecked config. The default now skips only when `CI` is set (Docker build stages) or during `lint` runs, the same rule most apps already passed by hand. Apps that set `options.skipValidation` are unaffected.
38
+
39
+ New `secret(schema)`, `reveal(value)` and `Redacted` (from `@arki/env/core`): a secret variable becomes a `Redacted` value that prints as `[redacted]` in logs, JSON, template strings and `util.inspect`, and is read with a deliberate `reveal(env.KEY)`. `reveal` also accepts the raw string, for runs where validation is skipped.
40
+
41
+ - cc7facf: Breaking: `defineEnv` now validates in every environment. `CI` and `lint` runs no longer skip validation. The only implicit opt-out is `SKIP_ENV_VALIDATION=1` (exactly `1`); set it only in build stages that run without secrets. `options.skipValidation` still overrides it. The `@arki/env/dot` plugin now validates at boot and fails with `ARKI_ENV_PLUGIN_E001` (new `ENV_PLUGIN_ERROR_CODES`), and `EnvServices['env']` is derived from `defineEnv` without a cast.
42
+ - a67cd66: `secret()` takes any Standard Schema (zod, valibot, arktype, hand-written) and returns a Standard Schema, not a zod schema. `.optional()` is still there: it accepts a missing variable and leaves `undefined` unwrapped. The result no longer has zod methods such as `.parse`; use `secret(schema)['~standard'].validate(value)`.
43
+
44
+ A rejected secret's issues keep their path but get a fixed message, because some validators (valibot) repeat the received value in theirs and `defineEnv` logs issues.
45
+
46
+ ### Patch Changes
47
+
48
+ - 064e096: Make failure the acyclic validation foundation. Schema parsing now returns exact typed validation failures, and failure revival returns typed protocol/validator defects instead of throwing. Add validated versioned JSON envelopes; retain legacy envelope reads while rejecting non-JSON data and changing output transforms. oRPC error maps validate output-only wire data.
49
+
50
+ Migrate consumers to check failures, propagate them through Journal event/write/replay and queue/mail operations, and use explicit exception bridges at foreign callbacks. Journal rejects invalid event entries before inserting them. See `@arki/failure/docs/upgrade-typed-validation.md` for breaking API and stored-data migration details.
51
+
52
+ - 652eb4b: `env()`, `resilience()`, `auth()`, `journal()`, `journalProjections()`, `queue()`, `durableScheduler()` and `outboxPlugin()` keep their plugin name in the type, so their handles carry `provider: 'env' | 'resilience' | …` instead of `string` and the feature builder's handle-identity checks apply around them. The three queue plugins gain an overload: with no `name` option the type carries the default name (`'queue'`, `'durable-scheduler'`, `'outbox'`); with `name: 'jobs'` it carries `'jobs'`.
53
+ - cbefc52: Dot plugins no longer pass a hard-coded `version` (the field was removed from `@arki/dot`).
54
+ - 5bc6390: `env()` publishes through a `provides` factory, so `envPlugin.provides.env` is a typed handle before boot (`@arki/dot` plugin handles).
55
+ - fd18765: `defineEnv` reads `globalThis.process?.env` instead of the bare `process` global, so it no longer throws a ReferenceError on engines without `process` (React Native Hermes, browsers). It still reads nothing at import. Pass `runtimeEnv` on those engines. Added the library lint preset and a cross-runtime smoke test.
56
+ - fe5a391: README follows the docs standard (install, quick start, limits, failure behaviour), with `llms.txt`, an agent guide, `AGENTS.md`, `test:docs` and the 0.0.11 to 0.1.0 upgrade guide. `ARKI_ENV_PLUGIN_E001` gains `@fix` and `@verify` tags.
57
+
58
+ no demo impact: documentation and doc-example changes only, runtime behaviour unchanged
59
+
60
+ - Updated dependencies [dcf2cef]
61
+ - Updated dependencies [afecc7a]
62
+ - Updated dependencies [5bc6390]
63
+ - Updated dependencies [899d0a9]
64
+ - Updated dependencies [ce8d707]
65
+ - Updated dependencies [92b38ed]
66
+ - Updated dependencies [9a4babb]
67
+ - Updated dependencies [80a4aa5]
68
+ - Updated dependencies [232d3e8]
69
+ - Updated dependencies [69d054f]
70
+ - Updated dependencies [cbefc52]
71
+ - Updated dependencies [7b19289]
72
+ - Updated dependencies [923124f]
73
+ - Updated dependencies [5721742]
74
+ - Updated dependencies [01400a0]
75
+ - Updated dependencies [0ab11d8]
76
+ - Updated dependencies [a531063]
77
+ - Updated dependencies [9b35520]
78
+ - Updated dependencies [5721742]
79
+ - Updated dependencies [232d3e8]
80
+ - Updated dependencies [9192d0e]
81
+ - Updated dependencies [3230f02]
82
+ - Updated dependencies [232d3e8]
83
+ - Updated dependencies [232d3e8]
84
+ - Updated dependencies [932c9e4]
85
+ - Updated dependencies [232d3e8]
86
+ - Updated dependencies [cc656e3]
87
+ - Updated dependencies [ff8e384]
88
+ - Updated dependencies [ce8d707]
89
+ - Updated dependencies [847f025]
90
+ - Updated dependencies [232d3e8]
91
+ - Updated dependencies [d9864f3]
92
+ - Updated dependencies [742f4c3]
93
+ - Updated dependencies [115aa87]
94
+ - Updated dependencies [6e4d7da]
95
+ - Updated dependencies [8b3cc80]
96
+ - @arki/dot@0.7.0
97
+
98
+ ## 0.0.11
99
+
100
+ ### Patch Changes
101
+
102
+ - @arki/dot@0.6.1
103
+
104
+ ## 0.0.10
105
+
106
+ ### Patch Changes
107
+
108
+ - Updated dependencies [228b4f5]
109
+ - Updated dependencies [9fa13ce]
110
+ - @arki/dot@0.6.0
111
+
112
+ ## 0.0.9
113
+
114
+ ### Patch Changes
115
+
116
+ - 91efd52: Reading a value from a `defineEnv()` env is faster once the env is in regular use. After 64 reads, an env whose values are private to it (validated on the server, or skipped with the runtime env `defineEnv` built itself) copies its values into the lazy object and reads them directly, instead of going through two proxies on every read. Writes and deletes through the env update the copy too. An env over your own `runtimeEnv` with validation skipped still reads it live.
117
+ - Updated dependencies [0685dfa]
118
+ - Updated dependencies
119
+ - Updated dependencies [2854df5]
120
+ - @arki/dot@0.5.0
121
+
122
+ ## 0.0.8
123
+
124
+ ### Patch Changes
125
+
126
+ - Dependency refresh: `@t3-oss/env-core` ^0.13.11, `zod` 4.6.5. No API changes.
127
+
128
+ ## 0.0.7
129
+
130
+ ### Patch Changes
131
+
132
+ - Lazy primitives family + client-surface hardening.
133
+
134
+ - `@arki/ts`: new `once()` (zero-arg memo; memoizes `undefined`, sync throws retry, promises shared), hardened `lazyObject()` (`set`/`deleteProperty`/`getPrototypeOf` traps — writes land on the real instance, `instanceof` answers truthfully), new `Lazy<T>`/`resolveLazy` deferred-config vocabulary, and a `./testing` entry with `expectImportPure()` (subprocess import-purity harness).
135
+ - `@arki/env`: `defineEnv` now delegates its lazy proxy to `lazyObject` (single trap implementation).
136
+ - `@arki/kv`: `kv()` accepts a whole-options thunk — `kv(() => ({ url: env.REDIS_URL }))` — keeping declarations import-pure; per-field `url` thunk kept for compatibility.
137
+ - `@arki/http`: `port` adopts `Lazy<number>`; documented why `bundles`/`features` tokens cannot defer (declaration-time wiring).
138
+ - `@arki/contracts`: `browser` exports condition serves a zod-only build (no `drizzle-orm`) to client bundlers; table wrappers are node-only and fail loudly at bundle time from browser code.
139
+ - `@arki/db` / `@arki/event-sourcing`: republished to align the `@arki/dot` peer range with 0.4.0.
140
+
141
+ - Updated dependencies
142
+ - Updated dependencies
143
+ - @arki/dot@0.4.0
144
+ - @arki/ts@0.0.3
145
+
146
+ ## 0.0.6
147
+
148
+ ### Patch Changes
149
+
150
+ - Updated dependencies [4b1c59f]
151
+ - @arki/dot@0.3.0
152
+
153
+ ## 0.0.5
154
+
155
+ ### Patch Changes
156
+
157
+ - Updated dependencies [48a8733]
158
+ - @arki/dot@0.2.0
159
+
160
+ ## 0.0.4
161
+
162
+ ### Patch Changes
163
+
164
+ - Updated dependencies [9871fce]
165
+ - @arki/dot@0.1.3
166
+
167
+ ## 0.0.3
168
+
169
+ ### Patch Changes
170
+
171
+ - Ship TypeScript source in the npm tarball (`src/**` added to `files`). npm installs and the read-only GitHub mirrors now carry the original `.ts` source alongside compiled `dist/` — better debuggability on the experimental track. No runtime changes.
172
+ - Updated dependencies
173
+ - @arki/dot@0.1.2
174
+
175
+ ## 0.0.2
176
+
177
+ ### Patch Changes
178
+
179
+ - DOT v2 pip contract — typed dependency injection (BREAKING on the experimental track).
180
+
181
+ `pip()` replaces `defineDotPip`: pips declare consumed services as `needs` shapes of type witnesses (`service<T>()` app-local, `token<T>()('key')` cross-package) and hooks destructure them fully typed; provides are inferred from `boot`'s return value. `.use()` is compile-time guarded (unsatisfied needs, type mismatches, wire-key collisions fail at the call site). Declaration order is boot order — the topological sort and name-based `dependencies` are gone; manifest dependency edges are observed from actual wiring. `rename(pip, map, name?)` is the multi-instance primitive, replacing the adapters' silent last-writer-wins `name` options. `lazy(init, { dispose? })` defers expensive opens behind a memoized single-flight `Lazy<T>` handle the kernel auto-disposes; `service.lazy<T>()` lifts eager or lazy providers into a uniform handle. The four `/dot` adapters (`@arki/env`, `@arki/kv`, `@arki/db`, `@arki/event-sourcing`) are migrated to the new contract.
182
+
183
+ - Updated dependencies
184
+ - @arki/dot@0.1.1
185
+
186
+ ## 0.0.1
187
+
188
+ ### Initial release
189
+
190
+ - First public publication on npm (experimental track) — 2026-05-21.
191
+
192
+ Part of **Wave 1**, the ARKI foundation: zero-dependency utilities and runtime primitives shipped together because every downstream wave (DOT kernel, infrastructure adapters) consumes them. Wave 1 packages: `assert`, `clock`, `contracts`, `date`, `env`, `log`, `resilience`, `slugify`, `string`, `ts`.
193
+
194
+ **Why 0.0.x, not 1.0.0**: ARKI is on an experimental track. The 0.0.x prefix is an honest signal — APIs may change without semver discipline until the kernel is field-tested. The release scorecard (metadata, privacy, dependencies, build-output, packlist, docs, fixtures, DOT gates, agent-native gates) is satisfied at this version; the gate certifies _the artifact_, not _API stability_. Stability is earned through reuse, not declared at first publication.
195
+
196
+ Wave 2 (`@arki/dot`) and Wave 3 (`@arki/db`, `@arki/kv`, `@arki/event-sourcing`) publish in subsequent releases once their inter-package transitive resolution against the public npm registry is verified.
package/README.md CHANGED
@@ -1,80 +1,249 @@
1
1
  # @arki/env
2
2
 
3
- Type-safe environment variable parsing for ARKI. `defineEnv()` is a thin wrapper around [`@t3-oss/env-core`](https://env.t3.gg/) with Zod contracts for common variable shapes — ports, URLs, comma-separated lists, JSON, and more.
3
+ Type-safe environment variables for ARKI. `defineEnv()` wraps [`@t3-oss/env-core`](https://env.t3.gg/). It validates on the first read and returns a typed object. Any [Standard Schema](https://standardschema.dev) works: Zod, Valibot, ArkType or a hand-written `~standard` object.
4
4
 
5
- ## Installation
5
+ ## Install
6
6
 
7
7
  ```sh
8
- npm install @arki/env
9
- # or
10
8
  bun add @arki/env
11
- # or
12
- pnpm add @arki/env
13
9
  ```
14
10
 
15
- ## Usage
11
+ ## Quick start
16
12
 
17
- ### `defineEnv`
13
+ Declare the variables once. Read them anywhere. A bad value throws on the first read, never at import.
18
14
 
19
- ```ts
15
+ ```ts run expect
20
16
  import { defineEnv } from '@arki/env/core';
21
- import { contracts } from '@arki/env/contracts';
17
+ import { presets } from '@arki/env/presets';
22
18
 
23
- export const env = defineEnv({
19
+ const env = defineEnv({
24
20
  server: {
25
- NODE_ENV: contracts.nodeEnv(),
26
- PORT: contracts.port(3000),
27
- DATABASE_URL: contracts.url(),
28
- LOG_LEVEL: contracts.string('info'),
21
+ PORT: presets.port(3000),
22
+ DATABASE_URL: presets.url(),
29
23
  },
24
+ runtimeEnv: { PORT: '8080', DATABASE_URL: 'https://db.example.com' },
25
+ });
26
+
27
+ console.log(env.PORT, typeof env.PORT);
28
+ console.log(env.DATABASE_URL);
29
+ ```
30
+
31
+ ```text
32
+ 8080 number
33
+ https://db.example.com
34
+ ```
35
+
36
+ Leave out `runtimeEnv` in a server app. `defineEnv` then reads `globalThis.process?.env` on the first access.
37
+
38
+ ## When not to use this
39
+
40
+ - You need a config file, layered sources or hot reload. This package reads environment variables only.
41
+ - Your validator is asynchronous. Validation is synchronous.
42
+ - You need the secret to stay hidden from code in the same process. `secret()` stops accidents in logs. It is not encryption.
43
+
44
+ ## Runtime support
45
+
46
+ `@arki/env/core` and `@arki/env/presets` run on Node, Bun, Deno, browsers and React Native (Hermes). Nothing reads the environment at import. On an engine with no `process`, pass `runtimeEnv`. A browser cannot read server keys, so declare client keys (`clientPrefix`) for code that ships to it. The Dot adapter lives in `@arki/dot/env`. `bun run test:runtimes` runs a smoke test of the built package on each runtime. The Hermes leg needs `HERMES_BIN`.
47
+
48
+ `@arki/env/node` (encrypted env files) runs on Node, Bun and Deno only. It reads files and starts provider CLIs.
49
+
50
+ ## Public imports
51
+
52
+ - `@arki/env/core`: `defineEnv`, `secret`, `reveal`, `Redacted`, `describeEnv`, `isSecretSchema`, `formatIssues`.
53
+ - `@arki/env/presets`: `presets` and the per-category namespaces `commonPresets`, `urlPresets`, `parsePresets`.
54
+ - `@arki/env`: re-exports `presets`.
55
+ - `@arki/env/node`: `loadEnvFiles`, `secretValues`, `formatEnvFilesErrors`, `ENV_FILES_ERROR_CODES`, the codec (`encryptValue`, `decryptValue`, `generateKeypair`, `parseEnv`, …), the file edits behind `dot env`, `findPrivateKey` and `DEFAULT_RESOLVERS`.
56
+ - `@arki/env/node/register`: a side-effect import that loads the env files into `process.env`.
57
+
58
+ ## Secrets
59
+
60
+ Wrap a secret's schema in `secret(...)`. The variable becomes a `Redacted` value. It prints as `[redacted]` in logs, JSON, template strings and `console.log`. The compiler will not pass it where a string is expected. Read it with `reveal(...)` at the one place that needs it. For an optional secret, call `.optional()` on the result. A missing variable then stays `undefined`.
61
+
62
+ ```ts run expect
63
+ import { z } from 'zod';
64
+
65
+ import { defineEnv, reveal, secret } from '@arki/env/core';
66
+
67
+ const env = defineEnv({
68
+ server: { STRIPE_KEY: secret(z.string().min(1)) },
69
+ runtimeEnv: { STRIPE_KEY: 'sk_live_123' },
30
70
  });
31
71
 
32
- console.log(env.PORT); // number
33
- console.log(env.DATABASE_URL); // string (validated URL)
72
+ console.log(String(env.STRIPE_KEY));
73
+ console.log(reveal(env.STRIPE_KEY));
74
+ ```
75
+
76
+ ```text
77
+ [redacted]
78
+ sk_live_123
79
+ ```
80
+
81
+ `reveal` also accepts the raw string, because schemas do not run when validation is skipped.
82
+
83
+ ## Encrypted env files
84
+
85
+ Commit `.env.<environment>` files with their secrets encrypted. The format is dotenvx 2.x, so the `dotenvx` CLI reads the same files. Each file holds a public key, so anyone can add a value. Only a holder of the private key can read one. The private key lives in `DOTENV_PRIVATE_KEY_<ENV>` (for `.env.production`: `DOTENV_PRIVATE_KEY_PRODUCTION`) or in the gitignored `.env.keys` file.
86
+
87
+ Edit the files with `dot env` from `@arki/dot`:
88
+
89
+ ```sh
90
+ dot env init # public key header + private key for .env.development
91
+ dot env set STRIPE_KEY --stdin # stored encrypted; the value never appears in argv
92
+ dot env set PORT 3000 --plain # refused when the schema declares PORT secret()
93
+ dot env run -- bun run dev # load, decrypt, run; secrets redacted when output is piped
94
+ dot env check # CI gate: no tracked key file, no plaintext secret()
34
95
  ```
35
96
 
36
- `defineEnv` reads from `process.env`, validates against the Zod schemas in `server`, and returns a fully typed object. It skips validation when `CI` is set or `NODE_ENV !== 'development'` unless you override via `options.skipValidation`.
97
+ Load the files once, at the top of a process. Use `dot env run -- <command>` (the app templates' `with-env` script does), or make `import '@arki/env/node/register'` the first import of an entry. Both write into `process.env`, because package envs such as `@arki/db/env` read it.
98
+
99
+ - The real environment wins. A file never replaces a value that is already set; a different value is reported as shadowed.
100
+ - The environment is `NODE_ENV`, else `development`. Files load from the repository root down to the working directory. A deeper folder wins, and `.env.local` wins over `.env.<environment>`. Under `test`, `.env.local` is skipped.
101
+ - A value can be a reference: `op://vault/item/field`, `infisical://…` or `doppler://…`. The provider's CLI resolves it at load. A public key (`VITE_`, `NEXT_PUBLIC_`, `PUBLIC_`, `EXPO_PUBLIC_`) is never filled from a reference.
102
+ - Keys that change how a process runs (`PATH`, `NODE_OPTIONS`, `LD_*`, TLS settings and more) are never loaded from a file.
103
+ - A private key is needed only when a value needs it. A key already set in the environment needs no decryption.
104
+ - `defineEnv` refuses a value that is still `encrypted:`, so ciphertext never passes as a string.
37
105
 
38
- ### Contracts
106
+ Mask loaded secrets in logs, wherever they appear:
39
107
 
40
- The `contracts` namespace bundles common variable shapes so you do not have to assemble them by hand. The categories:
108
+ ```ts skip="needs encrypted env files on disk"
109
+ import { secretValues } from '@arki/env/node';
110
+ import { configure, maskSecrets } from '@arki/log';
41
111
 
42
- - **Common**: `string`, `number`, `boolean`, `port`, `nodeEnv`.
43
- - **URLs**: `url`, `httpsUrl`, `postgresUrl`, `redisUrl`.
44
- - **Parsing**: `csv`, `json`, `keyValue`.
112
+ configure({ processors: [maskSecrets(secretValues)] });
113
+ ```
45
114
 
46
- ```ts
47
- import { contracts } from '@arki/env/contracts';
115
+ | Code | Meaning |
116
+ | ------------------------- | --------------------------------------------------------- |
117
+ | `env.missing_private_key` | A file has encrypted values and no private key was found. |
118
+ | `env.decryption_failed` | A value did not decrypt with the key that was found. |
119
+ | `env.reference_failed` | A reference could not be resolved by its provider CLI. |
120
+
121
+ Messages name the file and the key, never a value. `loadEnvFiles` returns `EnvFilesFailed` with these entries in `data.errors`; native file-read problems use `env.file_read_failed`. No entries are applied when loading fails. Raw provider errors and stderr stay in the failure's private cause.
122
+
123
+ ### Node file API
124
+
125
+ These come from `@arki/env/node`. The codec functions work on text and never touch the disk. The file functions read and write, and return a failure (check `isFailure`) instead of throwing.
126
+
127
+ | Export | What it does and when to use it |
128
+ | --- | --- |
129
+ | `encryptEnvFile(file, keys?)` | Encrypts plaintext values in place, for the listed keys or every value. Returns the keys it encrypted. Use it to import an old plain file. Returns `MissingPublicKey` when the file has no header. |
130
+ | `decryptEnvFile(file, privateKeys, keys?)` | Writes values back as plaintext, for the listed keys or every value. Use it for a one-off migration, never on a tracked file you plan to commit. |
131
+ | `rotateEnvFiles(...)` | Gives every file of one environment a new keypair and encrypts it again. It decrypts everything first, so a wrong key changes nothing. Returns `RotationIncomplete` if it stops half way. |
132
+ | `publicKeyIn(file)` | Reads the public key from the file header, or `undefined` when there is none. Use it to check that a file was initialised. |
133
+ | `envHeader(fileName, publicKey)` | Builds the dotenvx header text (banner, public key, file line). `initEnvFile` uses it; call it to write a header yourself. |
134
+ | `keyNamesFor(fileName)` | Gives the key variable names for a file: `.env.production` gives `DOTENV_PUBLIC_KEY_PRODUCTION` and `DOTENV_PRIVATE_KEY_PRODUCTION`. |
135
+ | `isKeyName(key)` | True for `DOTENV_PUBLIC_KEY*` and `DOTENV_PRIVATE_KEY*`. Use it to skip key lines when you list app values. |
136
+ | `isEncrypted(value)` | True for a value that starts with `encrypted:`. Use it to tell ciphertext from plaintext. |
137
+ | `envValues(document)` | Every key of a parsed file with its last value, in first-seen order. |
138
+ | `readEnvValue(document, key)` | The last value of one key in a parsed file, or `undefined`. |
139
+ | `setEnvValue(text, key, value)` | Returns the text with the key set. It rewrites an existing line in place and appends a new key. Other lines stay as written. |
140
+ | `removeEnvValue(text, key)` | Returns the text without any line that sets the key. |
141
+ | `keysFileCandidates(file, root)` | The `.env.keys` paths searched for a file, nearest first. |
142
+ | `findRoot(cwd)` | The nearest folder above `cwd` that has `.git`, else one with a `workspaces` package.json, else `cwd`. This is where file loading stops. |
143
+ | `envDirs(root, cwd)` | The folders from `root` down to `cwd`, root first. Files load in this order, so a deeper folder wins. |
144
+ | `referenceScheme(value, resolvers)` | Returns the scheme (`op`, `infisical`, `doppler`) when the value is a registered reference, else `undefined`. Unknown URLs stay plain values. |
145
+ | `envPublicKeySchema`, `envPrivateKeySchema` | Standard Schemas that check a secp256k1 key and brand it as `EnvPublicKey` or `EnvPrivateKey`. They never repeat the key in an issue. `parsePublicKey` and `parsePrivateKey` call them. |
146
+
147
+ ### Node failures
148
+
149
+ | Failure | Code | When it is returned | How to fix it |
150
+ | --- | --- | --- | --- |
151
+ | `MissingPublicKey` | `env.missing_public_key` | You tried to encrypt into a file that has no public key header. `data.file` names it. | Initialise the file first (`dot env init`, or `initEnvFile`). |
152
+ | `MissingPrivateKey` | `env.missing_private_key` | A file has encrypted values and no private key is available. `data.file` names it. | Restore the key from its backup: set `DOTENV_PRIVATE_KEY_<ENV>` or restore `.env.keys`. |
153
+ | `ReferenceFailed` | `env.reference_failed` | A provider CLI (`op`, `infisical`, `doppler`) rejected a read. `data.scheme` names the provider. HTTP status 502. | Check that the CLI is installed and signed in, and that the reference is right. The raw cause stays private. |
154
+
155
+ ## Presets
156
+
157
+ The `presets` namespace holds common variable shapes:
158
+
159
+ - Common: `string`, `number`, `boolean`, `port`, `nodeEnv`, `logLevel`.
160
+ - URLs: `url`, `databaseUrl`, `redisUrl`.
161
+ - Parsing: `json(schema)`, `csvArray(itemSchema)`, `csvSet(itemSchema)`.
162
+
163
+ ```ts run expect
164
+ import { z } from 'zod';
165
+
166
+ import { defineEnv } from '@arki/env/core';
167
+ import { presets } from '@arki/env/presets';
48
168
 
49
169
  const env = defineEnv({
50
170
  server: {
51
- ALLOWED_ORIGINS: contracts.csv(), // 'a,b,c' → ['a', 'b', 'c']
52
- FEATURE_FLAGS: contracts.json(), // JSON-parsed
53
- SERVICE_TAGS: contracts.keyValue(), // 'a=1,b=2' → { a: '1', b: '2' }
171
+ ALLOWED_ORIGINS: presets.csvArray(z.string()),
172
+ FEATURES: presets.json(z.object({ beta: z.boolean() })),
54
173
  },
174
+ runtimeEnv: { ALLOWED_ORIGINS: 'a,b,c', FEATURES: '{"beta":true}' },
175
+ });
176
+
177
+ console.log(env.ALLOWED_ORIGINS);
178
+ console.log(env.FEATURES);
179
+ ```
180
+
181
+ ```text
182
+ [ "a", "b", "c" ]
183
+ {
184
+ beta: true,
185
+ }
186
+ ```
187
+
188
+ ## Failure behaviour
189
+
190
+ `defineEnv` throws an `Error` on the first read of a bad environment. The message starts with your `name` and lists one `path: message` line per issue. The thrown error is an `UnexpectedFailure` that carries the `InvalidEnvironment` failure (code `env.invalid_environment`, exported from `@arki/env/core`), so a catcher can match on the code. Set `options.onValidationError` to throw something else.
191
+
192
+ ```ts run expect
193
+ import { defineEnv } from '@arki/env/core';
194
+ import { presets } from '@arki/env/presets';
195
+
196
+ const env = defineEnv({
197
+ server: { PORT: presets.port(), DATABASE_URL: presets.url() },
198
+ runtimeEnv: { PORT: 'abc' },
55
199
  });
200
+
201
+ try {
202
+ console.log(env.PORT);
203
+ } catch (error) {
204
+ console.log(error instanceof Error ? 'invalid environment' : 'unknown');
205
+ }
56
206
  ```
57
207
 
58
- ## API
208
+ ```text
209
+ invalid environment
210
+ ```
211
+
212
+ The Dot adapter (`env()` from `@arki/dot/env`) validates at boot. A bad environment stops `boot()` with `ARKI_ENV_PLUGIN_E001`. Its `remediation` tells you to set the listed variables. `dot explain` never validates.
213
+
214
+ Handle it by fixing the variables. Do not catch the error and continue with defaults.
215
+
216
+ Node execution is a separate contract. Codec, filesystem and provider operations return exact canonical failures from `@arki/failure`; check `isFailure(result)` before using a result. Key generation returns a keypair or `KeyGenerationFailed`; encryption returns text or `EncryptionFailed`; decryption returns text or `DecryptionFailed`; an unrepresentable dotenv value returns `UnrepresentableValue`. File operations return `FileReadFailed`, `FileWriteFailed` and their precise missing-key/crypto outcomes. Native errors remain private causes, never public messages.
59
217
 
60
- - `@arki/env/core`
61
- - `defineEnv(config)` — validate `process.env` and return a typed object.
62
- - `@arki/env/contracts`
63
- - `contracts` — flattened namespace of all built-in Zod contracts.
64
- - `commonContracts`, `urlContracts`, `parseContracts` — per-category namespaces.
65
- - `@arki/env`
66
- - Re-exports `contracts` from `@arki/env/contracts`.
218
+ Parse external keys with `parsePublicKey` / `parsePrivateKey`. Their schemas create distinct `EnvPublicKey` and `EnvPrivateKey` types; a private key cannot be passed where a public key is required. Invalid keys return canonical schema failures without echoing their bytes.
219
+
220
+ `loadEnvFiles({ signal, timeoutMs })` returns a report, `EnvFilesFailed`, `EnvCancelled` or `EnvDeadline`. Its default 30-second budget is shared by all reference reads. Synchronous provider callbacks receive the remaining budget and signal; cancellation is checked between calls. A synchronous syscall cannot be interrupted by a JavaScript abort handler while the event loop is blocked. Native provider subprocesses enforce their remaining timeout. `register` bridges these outcomes to a boot exception by explicit import.
221
+
222
+ Rotation first atomically stores a comma-separated list containing the new and previous private keys in the root and every existing candidate local key store. Staging all stores finishes before ciphertext changes, preserving nearest-store precedence. This is the existing dotenvx multi-key format. It then replaces and reads back every env file before retiring the previous keys. `RotationIncomplete` identifies the recovery key store and committed count; keep that store and retry with it. Both generations remain usable after a partial rotation. Writes use exclusive temp files plus rename; this protects process interruption, not a power-loss durability guarantee.
223
+
224
+ Owned node loads/edits, filesystem reads/writes and provider reads emit default OpenTelemetry spans and duration metrics through the API. No SDK is installed and no export I/O occurs without application configuration. Attributes contain bounded operation/outcome labels; paths, references, key bytes, values and raw causes are excluded. Pure definitions, schemas and local text/crypto transforms need no provider span.
225
+
226
+ ## Limits
227
+
228
+ - Validation runs in every environment, production, CI and tests included. `NODE_ENV`, `CI` and `lint` never skip it.
229
+ - The only implicit opt-out is `SKIP_ENV_VALIDATION=1`, exactly `1`. Use it only in build stages that run without secrets. `SKIP_ENV_VALIDATION=true` does not skip.
230
+ - `options.skipValidation` overrides the variable in code.
231
+ - Validation is synchronous. Validation is lazy, so a bad value shows at the first read, not at import.
232
+ - With validation skipped, schemas do not run. Values are raw strings. `reveal` still works on them.
67
233
 
68
234
  ## Documentation
69
235
 
70
- `@arki/env` ships an optional `@arki/env/dot` adapter for the
71
- [`@arki/dot`](https://www.npmjs.com/package/@arki/dot) framework.
236
+ - [Agent guide](docs/agent-guide.md): tasks and rules for agents.
237
+ - [Upgrade 0.0.11 to 0.1.0](docs/upgrades/0.0.11-to-0.1.0.md): what broke and how to migrate.
238
+ - [Upgrade 0.1.0 to 0.2.0](docs/upgrades/0.1.0-to-0.2.0.md): the Dot adapter moved to `@arki/dot/env`.
239
+ - The Dot adapter `@arki/dot/env` publishes `env` through `provides`. Consumers write `needs: { env: envPlugin.provides.env }`. See the [`@arki/dot`](https://www.npmjs.com/package/@arki/dot) package for plugin authoring and lifecycle.
240
+
241
+ ## Support
72
242
 
73
- - See `packages/dot/docs/` in the [`@arki/dot`](https://www.npmjs.com/package/@arki/dot)
74
- package for plugin authoring, lifecycle, diagnostics, and the
75
- [adapter authoring guide](https://github.com/arkijs/arki/blob/main/packages/dot/docs/adapter-authoring.md).
243
+ Report bugs at [github.com/arki-labs/arki/issues](https://github.com/arki-labs/arki/issues).
76
244
 
77
245
  <!-- bench:start -->
246
+
78
247
  ## Benchmarks
79
248
 
80
249
  Defining and validating a 15-variable server environment with `defineEnv()`, against `@t3-oss/env-core` (which it wraps), envalid, and a plain zod object parse.
@@ -85,39 +254,39 @@ Node v22.13.1 on Apple M2 Max, 2026-10-13. Median time per operation, lower is b
85
254
 
86
255
  Each call builds a fresh environment from scratch, validates every variable, then reads `PORT`.
87
256
 
88
- | Library | Time / op | vs fastest |
89
- | --- | ---: | ---: |
90
- | @arki/env defineEnv() | 25.03 µs | fastest |
91
- | @t3-oss/env-core createEnv() (used directly) | 306.02 µs | 12.22× |
92
- | envalid cleanEnv() | 59.23 µs | 2.37× |
93
- | zod z.object().parse(process.env) | 177.92 µs | 7.11× |
257
+ | Library | Time / op | vs fastest |
258
+ | -------------------------------------------- | --------: | ---------: |
259
+ | @arki/env defineEnv() | 25.03 µs | fastest |
260
+ | @t3-oss/env-core createEnv() (used directly) | 306.02 µs | 12.22× |
261
+ | envalid cleanEnv() | 59.23 µs | 2.37× |
262
+ | zod z.object().parse(process.env) | 177.92 µs | 7.11× |
94
263
 
95
264
  ### Define only (no read)
96
265
 
97
266
  Declare the environment and never read it. `defineEnv()` defers all work to the first read; the others validate immediately.
98
267
 
99
- | Library | Time / op | vs fastest |
100
- | --- | ---: | ---: |
101
- | @arki/env defineEnv() (lazy, no validation yet) | 126.8 ns | fastest |
102
- | @t3-oss/env-core createEnv() (validates now) | 519.07 µs | 4094.95× |
103
- | envalid cleanEnv() (validates now) | 115.66 µs | 912.46× |
104
- | zod z.object().parse(process.env) (validates now) | 431.26 µs | 3402.26× |
268
+ | Library | Time / op | vs fastest |
269
+ | ------------------------------------------------- | --------: | ---------: |
270
+ | @arki/env defineEnv() (lazy, no validation yet) | 126.8 ns | fastest |
271
+ | @t3-oss/env-core createEnv() (validates now) | 519.07 µs | 4094.95× |
272
+ | envalid cleanEnv() (validates now) | 115.66 µs | 912.46× |
273
+ | zod z.object().parse(process.env) (validates now) | 431.26 µs | 3402.26× |
105
274
 
106
275
  ### Read one value from an already-validated environment
107
276
 
108
277
  The hot path: `env.PORT` after the environment exists.
109
278
 
110
- | Library | Time / op | vs fastest |
111
- | --- | ---: | ---: |
112
- | @arki/env (lazy object over createEnv) | 229.1 ns | 28.25× |
113
- | @t3-oss/env-core createEnv() | 50.3 ns | 6.20× |
114
- | envalid cleanEnv() | 83.8 ns | 10.34× |
115
- | zod parsed plain object | 8.1 ns | fastest |
279
+ | Library | Time / op | vs fastest |
280
+ | -------------------------------------- | --------: | ---------: |
281
+ | @arki/env (lazy object over createEnv) | 229.1 ns | 28.25× |
282
+ | @t3-oss/env-core createEnv() | 50.3 ns | 6.20× |
283
+ | envalid cleanEnv() | 83.8 ns | 10.34× |
284
+ | zod parsed plain object | 8.1 ns | fastest |
116
285
 
117
- Read the rows with the contracts in mind:
286
+ Read the rows with the presets in mind:
118
287
 
119
288
  - `defineEnv()` is lazy: it builds and validates the environment on the first property read, so "define + first read" is the fair row for it. The "define only" group shows what declaring an env costs before anything reads it.
120
- - Validation is forced on (`skipValidation: false`) for `defineEnv()` and `createEnv`, because `defineEnv()` skips validation outside development by default.
289
+ - Validation is forced on (`skipValidation: false`) for `defineEnv()` and `createEnv`, so the numbers do not depend on `SKIP_ENV_VALIDATION`.
121
290
  - `defineEnv()` and `createEnv` share the same zod schemas, so the gap between them is the wrapper alone: building `runtimeEnv` from `process.env`, a labelled error handler, and the lazy object. envalid uses its own validators, and the plain zod parse does no env-specific work (no empty-string handling, no frozen result).
122
291
  - `createEnv`, envalid and the zod parse are given the whole `process.env`, as their docs show, so their cost grows with how many variables the process has. `defineEnv()` reads only the keys it declares. The gap therefore depends on the size of the environment it runs in.
123
292
 
@@ -126,4 +295,4 @@ Reproduce with `bun run bench` in the package directory; regenerate this section
126
295
 
127
296
  ## License
128
297
 
129
- MIT — see [LICENSE](./LICENSE).
298
+ MIT. See [LICENSE](./LICENSE).