@voltro/env 0.1.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.
@@ -0,0 +1,352 @@
1
+ import { Schema } from 'effect';
2
+
3
+ /**
4
+ * The type-erased field shape, used where the value type is irrelevant (the
5
+ * declared map, iteration). It is NOT `EnvFieldSpec<unknown>` because
6
+ * effect's `Schema` is invariant in its decoded type — a concrete
7
+ * `EnvFieldSpec<number>` would not widen to that. Typing the schema as
8
+ * `Schema.Schema.Any` lets any concrete field assign here.
9
+ */
10
+ export declare interface AnyEnvFieldSpec {
11
+ readonly kind: EnvFieldKind;
12
+ readonly access: EnvAccess;
13
+ readonly schema: Schema.Schema.Any;
14
+ readonly tsType: string;
15
+ readonly optional: boolean;
16
+ readonly default?: string | undefined;
17
+ readonly description?: string | undefined;
18
+ readonly example?: string | undefined;
19
+ }
20
+
21
+ /** Project an app's `defineEnv` contract onto manifest entries. */
22
+ export declare const appEnvEntries: (contract: EnvContract | undefined) => ReadonlyArray<EnvManifestEntry>;
23
+
24
+ /**
25
+ * Enforce the public-prefix invariant (t3-env's client-prefix rule,
26
+ * framework-branded). Returns naming errors to merge with the value errors:
27
+ *
28
+ * - On a WEB app, every `access: 'public'` var (browser-exposed) MUST start
29
+ * with the contract's `publicPrefix` (default `VOLTRO_PUBLIC_`).
30
+ * - ANY `access: 'secret'` var MUST NOT start with the prefix (the prefix
31
+ * marks browser-bound public values; a secret carrying it is a contradiction).
32
+ *
33
+ * `publicPrefix: false` opts out. API apps skip the public-prefix requirement
34
+ * (their public vars are non-sensitive SERVER config, never browser-bound) but
35
+ * still get the secret-must-not-be-prefixed check.
36
+ */
37
+ export declare const checkEnvNaming: (contract: EnvContract | undefined, appType: "api" | "web") => ReadonlyArray<EnvResolveError>;
38
+
39
+ /** A plugin's declared env need — metadata only, NOT a read path. Mirror what
40
+ * the plugin actually reads via `options.X ?? process.env.X`. */
41
+ export declare interface DeclaredEnvVar {
42
+ readonly name: string;
43
+ readonly required: boolean;
44
+ readonly secret: boolean;
45
+ readonly description?: string;
46
+ readonly example?: string;
47
+ }
48
+
49
+ /** The framework's default prefix for browser-exposed PUBLIC env vars. */
50
+ export declare const DEFAULT_PUBLIC_PREFIX = "VOLTRO_PUBLIC_";
51
+
52
+ /**
53
+ * Declare an app's environment contract. Call this in `app.config.ts` and put
54
+ * the result on the default export's `env` field:
55
+ *
56
+ * ```ts
57
+ * // app.config.ts
58
+ * import { defineEnv, envVar } from '@voltro/env'
59
+ *
60
+ * export const env = defineEnv({
61
+ * SENTRY_DSN: envVar.string({ access: 'public' }),
62
+ * STRIPE_KEY: envVar.string({ access: 'secret' }),
63
+ * })
64
+ *
65
+ * export default { type: 'api', name: 'myApi', env }
66
+ * ```
67
+ *
68
+ * The returned contract is the single source of truth: the CLI reads it to
69
+ * validate `process.env` at boot, to bake the public subset into the web
70
+ * bundle, to generate the typed `@voltro/env/server` + `@voltro/env/public`
71
+ * accessors, and to emit `.env.example` + the inspect manifest.
72
+ *
73
+ * You do NOT read values off the contract directly — read `serverEnv` /
74
+ * `getSecret` from `@voltro/env/server` (server) or `publicEnv` from
75
+ * `@voltro/env/public` (browser).
76
+ *
77
+ * `M` is inferred from the literal map, so the contract's phantom value types
78
+ * are exact (`{ SENTRY_DSN: string; STRIPE_KEY: string }` above).
79
+ */
80
+ export declare const defineEnv: <const M extends EnvSchemaMap>(vars: M, options?: DefineEnvOptions) => EnvContract<M>;
81
+
82
+ export declare interface DefineEnvOptions {
83
+ /**
84
+ * Required name prefix for browser-exposed public vars. Default
85
+ * `VOLTRO_PUBLIC_`. Pass a different string to brand it your own way, or
86
+ * `false` to disable the prefix invariant entirely.
87
+ */
88
+ readonly publicPrefix?: string | false;
89
+ }
90
+
91
+ declare const ENV_CONTRACT_BRAND: unique symbol;
92
+
93
+ /** Whether a variable may cross the browser boundary. */
94
+ export declare type EnvAccess = 'public' | 'secret';
95
+
96
+ /**
97
+ * The frozen result of `defineEnv(...)`. Carries the declaration the CLI
98
+ * reads at boot, plus phantom type members the framework's codegen uses to
99
+ * derive the typed `@voltro/env/server` + `@voltro/env/public` accessors.
100
+ *
101
+ * You reference it from `app.config.ts` (`export const env = defineEnv(...)`,
102
+ * then `env` on the default export). You do NOT read values off it directly —
103
+ * read `serverEnv` / `getSecret` (server) or `publicEnv` (browser).
104
+ */
105
+ export declare interface EnvContract<M extends EnvSchemaMap = EnvSchemaMap> {
106
+ readonly [ENV_CONTRACT_BRAND]: true;
107
+ /** The declared variables, keyed by env-var name. */
108
+ readonly vars: M;
109
+ /**
110
+ * Required name prefix for BROWSER-EXPOSED public vars (default
111
+ * `VOLTRO_PUBLIC_`). On a web app, every `access: 'public'` var (which ships
112
+ * to the browser) MUST carry this prefix, and no `access: 'secret'` var may —
113
+ * so the name and the access tag can never silently disagree (t3-env's
114
+ * client-prefix invariant, framework-branded). `false` opts out of the check.
115
+ */
116
+ readonly publicPrefix: string | false;
117
+ /** Phantom — resolved server value shape (never present at runtime). */
118
+ readonly _values?: EnvValues<M>;
119
+ /** Phantom — browser-visible (public) value shape. */
120
+ readonly _public?: PublicEnvValues<M>;
121
+ }
122
+
123
+ /** The concrete coercion a field performs on the raw string env value. */
124
+ export declare type EnvFieldKind = 'string' | 'number' | 'boolean' | 'enum' | 'url' | 'port';
125
+
126
+ /**
127
+ * A single declared environment variable. Produced by the `envVar.*`
128
+ * builders — you never construct this by hand. `A` is the decoded value
129
+ * type; `Optional` lifts "may be absent" to the type level so the inferred
130
+ * accessor is `A | undefined` for optional vars and `A` otherwise.
131
+ */
132
+ export declare interface EnvFieldSpec<A = unknown, Optional extends boolean = boolean> {
133
+ readonly kind: EnvFieldKind;
134
+ /** Browser boundary classification — mandatory, no default. */
135
+ readonly access: EnvAccess;
136
+ /** effect/Schema that decodes the RAW string env value → typed `A`. */
137
+ readonly schema: Schema.Schema<A, string>;
138
+ /**
139
+ * TypeScript type literal for the codegen'd typed accessors — e.g.
140
+ * `"string"`, `"number"`, `"'dev' | 'prod'"`. Bounded to what the
141
+ * builders emit, so the generated `.d.ts` augmentation is precise.
142
+ */
143
+ readonly tsType: string;
144
+ /** When true, an unset var (no value, no default) is allowed → `A | undefined`. */
145
+ readonly optional: Optional;
146
+ /** Fallback applied (in raw string form) when the env var is unset. */
147
+ readonly default?: string | undefined;
148
+ /** One-line description — surfaced in `.env.example` + the inspect manifest. */
149
+ readonly description?: string | undefined;
150
+ /** Example value for `.env.example`. NEVER a real secret — a placeholder. */
151
+ readonly example?: string | undefined;
152
+ }
153
+
154
+ export declare interface EnvManifestEntry {
155
+ readonly key: string;
156
+ readonly owner: EnvOwner;
157
+ /** `'secret'` → never print the value (sensitive: token / password / URL
158
+ * with credentials). `'public'` → a non-sensitive knob. For app vars this
159
+ * is also the browser-bundle boundary. */
160
+ readonly access: EnvAccess;
161
+ /** Required (no default, not optional). Drives `.env.example` ordering +
162
+ * the "missing required" boot report. */
163
+ readonly required: boolean;
164
+ readonly description?: string;
165
+ readonly example?: string;
166
+ readonly default?: string;
167
+ /** App fields carry their coercion kind; framework/plugin entries omit it. */
168
+ readonly kind?: EnvFieldKind;
169
+ /** Framework grouping for `.env.example` section headers. */
170
+ readonly group?: string;
171
+ }
172
+
173
+ export declare type EnvOwner = 'app' | 'framework' | `plugin:${string}`;
174
+
175
+ export declare interface EnvResolveError {
176
+ readonly key: string;
177
+ readonly access: EnvAccess;
178
+ /** `'missing'` (required but unset) or `'invalid'` (failed the Schema). */
179
+ readonly reason: 'missing' | 'invalid';
180
+ readonly message: string;
181
+ }
182
+
183
+ /** The declared variable map passed to `defineEnv`. */
184
+ export declare type EnvSchemaMap = Record<string, AnyEnvFieldSpec>;
185
+
186
+ /** The fully-resolved server value shape for a contract (public + secret). */
187
+ export declare type EnvValues<M extends EnvSchemaMap> = {
188
+ readonly [K in keyof M]: InferEnvValue<M[K]>;
189
+ };
190
+
191
+ /**
192
+ * The field-builder surface used inside `defineEnv({...})`.
193
+ *
194
+ * ```ts
195
+ * env: defineEnv({
196
+ * SENTRY_DSN: envVar.string({ access: 'public', description: 'Browser Sentry DSN' }),
197
+ * APP_TITLE: envVar.string({ access: 'public', default: 'Voltro' }),
198
+ * PORT: envVar.port({ access: 'public', default: '4000' }),
199
+ * FEATURE_X: envVar.boolean({ access: 'public', default: 'false' }),
200
+ * LOG_LEVEL: envVar.enum(['debug', 'info', 'warn'], { access: 'public', default: 'info' }),
201
+ * STRIPE_KEY: envVar.string({ access: 'secret' }),
202
+ * WEBHOOK_URL: envVar.url({ access: 'secret', optional: true }),
203
+ * })
204
+ * ```
205
+ */
206
+ export declare const envVar: {
207
+ /** A plain string. */
208
+ readonly string: <O extends boolean = false>(opts: FieldOptions<O>) => EnvFieldSpec<string, O>;
209
+ /** A number, coerced from its string form (`"5"` → `5`). */
210
+ readonly number: <O extends boolean = false>(opts: FieldOptions<O>) => EnvFieldSpec<number, O>;
211
+ /** A TCP port — number in `1..65535`, coerced from string. */
212
+ readonly port: <O extends boolean = false>(opts: FieldOptions<O>) => EnvFieldSpec<number, O>;
213
+ /** A boolean, coerced from `true/false/1/0/yes/no/on/off`. */
214
+ readonly boolean: <O extends boolean = false>(opts: FieldOptions<O>) => EnvFieldSpec<boolean, O>;
215
+ /** A URL string, validated with the `URL` constructor. */
216
+ readonly url: <O extends boolean = false>(opts: FieldOptions<O>) => EnvFieldSpec<string, O>;
217
+ /** A string constrained to a closed set — typed as the literal union. */
218
+ readonly enum: <const T extends readonly [string, ...string[]], O extends boolean = false>(values: T, opts: FieldOptions<O>) => EnvFieldSpec<T[number], O>;
219
+ };
220
+
221
+ /** Common options every field accepts. `O` lifts `optional` to the type level. */
222
+ export declare interface FieldOptions<O extends boolean = false> {
223
+ /** Browser boundary classification — MANDATORY. `'public'` is bundled into
224
+ * the browser; `'secret'` stays server-only. There is no default. */
225
+ readonly access: EnvAccess;
226
+ /** Allow the variable to be absent (no value, no default). Makes the
227
+ * accessor type `A | undefined`. Use `default` instead when there is a
228
+ * sensible fallback. */
229
+ readonly optional?: O;
230
+ /** Fallback (raw string) applied when the env var is unset. With a default
231
+ * the value is always present, so the accessor type stays `A`. */
232
+ readonly default?: string;
233
+ /** One-line description — shown in `.env.example` + the inspect manifest. */
234
+ readonly description?: string;
235
+ /** Example value for `.env.example`. Never a real secret — a placeholder. */
236
+ readonly example?: string;
237
+ }
238
+
239
+ /**
240
+ * Render aggregated errors into a single multi-line message for a boot abort.
241
+ * Mirrors the per-plugin config-decode failure shape so the boot log reads
242
+ * consistently across config validation surfaces.
243
+ */
244
+ export declare const formatEnvErrors: (errors: ReadonlyArray<EnvResolveError>) => string;
245
+
246
+ /**
247
+ * The framework's own user-facing env vars. Curated, not exhaustive — the
248
+ * goal is "what an operator configures", not every internal/test knob. This
249
+ * is the single source for the `.env.example` framework section + the turbo
250
+ * `globalPassThroughEnv` list. Add new operator-facing framework vars HERE.
251
+ */
252
+ export declare const FRAMEWORK_ENV_CATALOG: ReadonlyArray<EnvManifestEntry>;
253
+
254
+ /** Just the framework var NAMES — the source for turbo `globalPassThroughEnv`. */
255
+ export declare const frameworkEnvNames: () => ReadonlyArray<string>;
256
+
257
+ /** True once the boot gate has installed a snapshot. */
258
+ export declare const hasEnvSnapshot: () => boolean;
259
+
260
+ /** Lift a field's optionality + value type to the accessor's value type. */
261
+ export declare type InferEnvValue<F> = F extends EnvFieldSpec<infer A, infer O> ? (O extends true ? A | undefined : A) : never;
262
+
263
+ /** Install the boot-resolved value map. Called once by the CLI after the
264
+ * env-validation gate passes. Idempotent: a second install replaces the
265
+ * first (a supervised dev restart re-resolves cleanly). */
266
+ export declare const installEnvSnapshot: (values: Record<string, unknown>) => void;
267
+
268
+ /** Runtime type guard: is `value` an `EnvContract`? Used by the CLI loader,
269
+ * which sees the app.config default export as `unknown`. */
270
+ export declare const isEnvContract: (value: unknown) => value is EnvContract;
271
+
272
+ export declare interface PluginEnv {
273
+ /** The declared variables — assign this to the plugin's `declaredEnv`. */
274
+ readonly declared: ReadonlyArray<DeclaredEnvVar>;
275
+ /**
276
+ * Read a DECLARED env var: `override ?? process.env[name]`. Throws if `name`
277
+ * is not in the declaration — so a typo or an undeclared read fails loudly at
278
+ * the call site instead of silently drifting from the manifest.
279
+ */
280
+ read(name: string, override?: string): string | undefined;
281
+ }
282
+
283
+ export declare const pluginEnv: (declared: ReadonlyArray<DeclaredEnvVar>) => PluginEnv;
284
+
285
+ /** Project a plugin's `declaredEnv` onto manifest entries. */
286
+ export declare const pluginEnvEntries: (pluginName: string, declared: ReadonlyArray<DeclaredEnvVar> | undefined) => ReadonlyArray<EnvManifestEntry>;
287
+
288
+ /** The browser-visible value shape (public fields only). */
289
+ export declare type PublicEnvValues<M extends EnvSchemaMap> = {
290
+ readonly [K in PublicKeys<M>]: InferEnvValue<M[K]>;
291
+ };
292
+
293
+ /** Keys of `M` whose field is `access: 'public'`. */
294
+ export declare type PublicKeys<M extends EnvSchemaMap> = {
295
+ [K in keyof M]: M[K]['access'] extends 'public' ? K : never;
296
+ }[keyof M];
297
+
298
+ /** Read a resolved value. Returns `undefined` for unknown / unset keys. */
299
+ export declare const readEnvValue: (key: string) => unknown;
300
+
301
+ /**
302
+ * Render a grouped `.env.example`. App + plugin entries come first (the things
303
+ * a developer must fill in), then the framework catalog grouped by area.
304
+ */
305
+ export declare const renderDotEnvExample: (entries: ReadonlyArray<EnvManifestEntry>, opts?: RenderDotEnvOptions) => string;
306
+
307
+ export declare interface RenderDotEnvOptions {
308
+ /** Header comment lines (without the leading `# `). */
309
+ readonly header?: ReadonlyArray<string>;
310
+ /** Include the framework catalog section. Default true. */
311
+ readonly includeFramework?: boolean;
312
+ }
313
+
314
+ /**
315
+ * Read a resolved value, throwing a clear error if the snapshot is missing.
316
+ * The missing-snapshot case means env was read before the boot gate ran (e.g.
317
+ * at module-import time in a context the framework doesn't drive) — almost
318
+ * always a wiring bug, so we fail loudly rather than hand back `undefined`.
319
+ */
320
+ export declare const requireEnvValue: (key: string) => unknown;
321
+
322
+ /** Reset the snapshot (tests). */
323
+ export declare const resetEnvSnapshot: () => void;
324
+
325
+ export declare interface ResolvedEnv {
326
+ /** Every resolved value (public + secret), keyed by env-var name. Optional
327
+ * vars that are absent appear as `undefined`. */
328
+ readonly values: Record<string, unknown>;
329
+ /** The public subset only — the exact, secret-free map that may be baked
330
+ * into the browser bundle. */
331
+ readonly publicValues: Record<string, unknown>;
332
+ /** Decode failures + missing-required, aggregated so the CLI can report
333
+ * ALL problems at once instead of crashing on the first. */
334
+ readonly errors: ReadonlyArray<EnvResolveError>;
335
+ /** Convenience: `errors.length === 0`. */
336
+ readonly ok: boolean;
337
+ }
338
+
339
+ export declare const resolveEnv: (opts: ResolveEnvOptions) => Promise<ResolvedEnv>;
340
+
341
+ export declare interface ResolveEnvOptions {
342
+ /** The contract from `app.config.ts`. `undefined` (no `env` field) → an
343
+ * empty, OK result, so the gate is a no-op for apps that declare nothing. */
344
+ readonly contract: EnvContract | undefined;
345
+ /** Read a raw public value. Defaults to `process.env`. */
346
+ readonly readEnv?: (key: string) => string | undefined;
347
+ /** Resolve a raw secret value through the configured backend. Defaults to
348
+ * `readEnv` (so without a backend, secrets just come from `process.env`). */
349
+ readonly resolveSecret?: (key: string) => Promise<string | undefined>;
350
+ }
351
+
352
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,169 @@
1
+ import { a as e, i as t, n, r, t as i } from "./snapshot-qX0GrQfx.js";
2
+ import { Either as a, ParseResult as o, Schema as s } from "effect";
3
+ //#region src/define.ts
4
+ var c = "VOLTRO_PUBLIC_", l = (e, t) => Object.freeze({
5
+ vars: Object.freeze({ ...e }),
6
+ publicPrefix: t?.publicPrefix ?? "VOLTRO_PUBLIC_"
7
+ }), u = (e) => typeof e == "object" && !!e && "vars" in e && typeof e.vars == "object" && e.vars !== null, d = (e, t, n, r) => ({
8
+ kind: e,
9
+ access: r.access,
10
+ schema: t,
11
+ tsType: n,
12
+ optional: r.optional ?? !1,
13
+ ...r.default === void 0 ? {} : { default: r.default },
14
+ ...r.description === void 0 ? {} : { description: r.description },
15
+ ...r.example === void 0 ? {} : { example: r.example }
16
+ }), f = s.transformOrFail(s.String, s.Boolean, {
17
+ strict: !0,
18
+ decode: (e, t, n) => {
19
+ let r = e.trim().toLowerCase();
20
+ return r === "true" || r === "1" || r === "yes" || r === "on" ? o.succeed(!0) : r === "false" || r === "0" || r === "no" || r === "off" || r === "" ? o.succeed(!1) : o.fail(new o.Type(n, e, `expected a boolean (true/false/1/0/yes/no/on/off), got "${e}"`));
21
+ },
22
+ encode: (e) => o.succeed(e ? "true" : "false")
23
+ }), p = (e) => URL.canParse(e), m = {
24
+ string: (e) => d("string", s.String, "string", e),
25
+ number: (e) => d("number", s.NumberFromString, "number", e),
26
+ port: (e) => d("port", s.NumberFromString.pipe(s.int(), s.between(1, 65535)), "number", e),
27
+ boolean: (e) => d("boolean", f, "boolean", e),
28
+ url: (e) => d("url", s.String.pipe(s.filter((e) => p(e) || `expected a valid URL, got "${e}"`)), "string", e),
29
+ enum: (e, t) => d("enum", s.Literal(...e), e.map((e) => `'${e}'`).join(" | "), t)
30
+ }, h = (e, t, n) => {
31
+ let r = t.schema, i = s.decodeUnknownEither(r)(n);
32
+ return a.mapLeft(i, (n) => ({
33
+ key: e,
34
+ access: t.access,
35
+ reason: "invalid",
36
+ message: n.message
37
+ }));
38
+ }, g = async (e) => {
39
+ let t = e.readEnv ?? ((e) => process.env[e]), n = e.resolveSecret ?? ((e) => Promise.resolve(t(e))), r = {}, i = {}, o = [], s = e.contract ? Object.entries(e.contract.vars) : [];
40
+ for (let [e, c] of s) {
41
+ let s = (c.access === "secret" ? await n(e) : t(e)) ?? c.default;
42
+ if (s === void 0) {
43
+ c.optional ? (r[e] = void 0, c.access === "public" && (i[e] = void 0)) : o.push({
44
+ key: e,
45
+ access: c.access,
46
+ reason: "missing",
47
+ message: `required ${c.access} env var "${e}" is unset` + (c.description ? ` — ${c.description}` : "")
48
+ });
49
+ continue;
50
+ }
51
+ let l = h(e, c, s);
52
+ if (a.isLeft(l)) {
53
+ o.push(l.left);
54
+ continue;
55
+ }
56
+ r[e] = l.right, c.access === "public" && (i[e] = l.right);
57
+ }
58
+ return {
59
+ values: r,
60
+ publicValues: i,
61
+ errors: o,
62
+ ok: o.length === 0
63
+ };
64
+ }, _ = (e, t) => {
65
+ let n = e?.publicPrefix;
66
+ if (!e || typeof n != "string") return [];
67
+ let r = [];
68
+ for (let [i, a] of Object.entries(e.vars)) a.access === "secret" && i.startsWith(n) && r.push({
69
+ key: i,
70
+ access: "secret",
71
+ reason: "invalid",
72
+ message: `secret env var "${i}" must NOT use the public prefix "${n}" — that prefix marks browser-exposed PUBLIC values; a secret carrying it is a contradiction.`
73
+ }), t === "web" && a.access === "public" && !i.startsWith(n) && r.push({
74
+ key: i,
75
+ access: "public",
76
+ reason: "invalid",
77
+ message: `public env var "${i}" on a web app ships to the browser and must be named "${n}…" (e.g. "${n}${i}"). Rename it, mark it access:'secret' to keep it server-only, or set publicPrefix:false in defineEnv to opt out.`
78
+ });
79
+ return r;
80
+ }, v = (e) => {
81
+ let t = e.map((e) => ` • ${e.key} (${e.access}, ${e.reason}): ${e.message}`);
82
+ return `environment validation failed (${e.length} ${e.length === 1 ? "error" : "errors"}):\n${t.join("\n")}`;
83
+ }, y = (e) => e ? Object.entries(e.vars).map(([e, t]) => ({
84
+ key: e,
85
+ owner: "app",
86
+ access: t.access,
87
+ required: !t.optional && t.default === void 0,
88
+ ...t.description === void 0 ? {} : { description: t.description },
89
+ ...t.example === void 0 ? {} : { example: t.example },
90
+ ...t.default === void 0 ? {} : { default: t.default },
91
+ kind: t.kind
92
+ })) : [], b = (e, t) => t ? t.map((t) => ({
93
+ key: t.name,
94
+ owner: `plugin:${e}`,
95
+ access: t.secret ? "secret" : "public",
96
+ required: t.required,
97
+ ...t.description === void 0 ? {} : { description: t.description },
98
+ ...t.example === void 0 ? {} : { example: t.example }
99
+ })) : [], x = (e, t, n, r, i) => ({
100
+ key: e,
101
+ owner: "framework",
102
+ access: n,
103
+ required: !1,
104
+ description: r,
105
+ group: t,
106
+ ...i === void 0 ? {} : { example: i }
107
+ }), S = [
108
+ x("NODE_ENV", "runtime", "public", "Node environment (development | production).", "development"),
109
+ x("PORT", "runtime", "public", "Port the api/web server binds to. Overrides app.config `port`.", "4000"),
110
+ x("VOLTRO_REGION", "runtime", "public", "Region tag for replica locality (else AWS_REGION/FLY_REGION/…).", "us-east-1"),
111
+ x("VOLTRO_AUTO_MIGRATE", "runtime", "public", "Set 0 to skip boot auto-migrate (CI-applied migrations).", "1"),
112
+ x("DB_DIALECT", "database", "public", "postgres | mysql | mariadb | mssql | sqlite | memory.", "postgres"),
113
+ x("DB_URL", "database", "secret", "Connection URL (carries credentials). Preferred over discrete DB_* fields.", "postgres://user:pw@localhost:5432/app"),
114
+ x("DB_HOST", "database", "public", "Database host (discrete-field form; DB_URL preferred).", "localhost"),
115
+ x("DB_PORT", "database", "public", "Database port.", "5432"),
116
+ x("DB_USER", "database", "public", "Database user.", "postgres"),
117
+ x("DB_PASSWORD", "database", "secret", "Database password."),
118
+ x("DB_DATABASE", "database", "public", "Database name.", "app"),
119
+ x("DB_MAX_CONNECTIONS", "database", "public", "Per-process connection pool cap.", "10"),
120
+ x("DB_REPLICA_URLS", "database", "secret", "Comma-separated read-replica URLs (carry credentials)."),
121
+ x("DB_REPLICA_REGIONS", "database", "public", "Comma-separated region tags matching DB_REPLICA_URLS order."),
122
+ x("RYW_STORE", "database", "public", "Read-your-writes position store: memory | redis.", "memory"),
123
+ x("CACHE_BACKEND", "cache", "public", "@voltro/cache backend: memory | redis.", "memory"),
124
+ x("CACHE_REDIS_URL", "cache", "secret", "Redis/Valkey/KeyDB/Dragonfly/Upstash URL (may carry credentials).", "redis://localhost:6379"),
125
+ x("CACHE_REDIS_DRIVER", "cache", "public", "resp (ioredis TCP) | http (Upstash REST, edge).", "resp"),
126
+ x("CACHE_REDIS_TOKEN", "cache", "secret", "Upstash REST token (http driver)."),
127
+ x("CACHE_KEY_PREFIX", "cache", "public", "Cache key namespace.", "voltro:cache"),
128
+ x("REDIS_URL", "cache", "secret", "Generic Redis URL fallback (cache / ryw / ratelimit).", "redis://localhost:6379"),
129
+ x("VOLTRO_SESSION_SECRET", "security", "secret", "HMAC signing key for session cookies. REQUIRED in production."),
130
+ x("VOLTRO_INSPECT", "security", "public", "Set off to disable the /_voltro/inspect surface entirely.", "on"),
131
+ x("VOLTRO_INSPECT_TOKEN", "security", "secret", "Bearer token gating /_voltro/inspect/* on public deploys."),
132
+ x("VOLTRO_TENANT_ISOLATION", "security", "public", "shared-schema (default) | namespace (physical per-tenant)."),
133
+ x("FRAMEWORK_TRACING", "observability", "public", "off | console | otlp. Auto-otlp when OTEL_* is set."),
134
+ x("OTEL_EXPORTER_OTLP_ENDPOINT", "observability", "public", "OTLP/HTTP collector endpoint (traces + metrics).", "http://localhost:4318"),
135
+ x("OTEL_SERVICE_NAME", "observability", "public", "OTel service name.", "voltro-api"),
136
+ x("SSR_CACHE", "web", "public", "ISR cache backend for `voltro start`: memory | postgres.", "memory")
137
+ ], C = () => S.map((e) => e.key), w = (e) => {
138
+ let t = [];
139
+ e.description && t.push(`# ${e.description}`);
140
+ let n = [
141
+ e.required ? "required" : "optional",
142
+ e.access,
143
+ ...e.kind ? [e.kind] : []
144
+ ].join(", ");
145
+ t.push(`# (${n})`);
146
+ let r = e.example ?? e.default ?? "";
147
+ return t.push(`${e.key}=${e.access === "secret" && !e.example ? "" : r}`), t.join("\n");
148
+ }, T = (e, t = {}) => {
149
+ let n = [], r = t.header ?? ["Environment for this app — generated from app.config.ts `env` + plugin declarations.", "Regenerate with `voltro env sync`. Copy to `.env` and fill in the blanks."];
150
+ n.push(...r.map((e) => `# ${e}`), "");
151
+ let i = e.filter((e) => e.owner === "app"), a = e.filter((e) => e.owner.startsWith("plugin:"));
152
+ if (i.length > 0 && (n.push("# ── App ──────────────────────────────────────────────", ""), n.push(...i.map(w).flatMap((e) => [e, ""]))), a.length > 0 && (n.push("# ── Plugins ──────────────────────────────────────────", ""), n.push(...a.map(w).flatMap((e) => [e, ""]))), t.includeFramework !== !1) {
153
+ let e = [...new Set(S.map((e) => e.group))];
154
+ n.push("# ── Framework ────────────────────────────────────────", "");
155
+ for (let t of e) n.push(`# [${t}]`), n.push(...S.filter((e) => e.group === t).map(w).flatMap((e) => [e, ""]));
156
+ }
157
+ return n.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
158
+ }, E = (e) => {
159
+ let t = new Set(e.map((e) => e.name));
160
+ return {
161
+ declared: e,
162
+ read: (e, n) => {
163
+ if (!t.has(e)) throw Error(`@voltro/env: plugin read of undeclared env var "${e}" — add it to the pluginEnv([...]) declaration so the manifest stays in sync.`);
164
+ return n ?? process.env[e];
165
+ }
166
+ };
167
+ };
168
+ //#endregion
169
+ export { c as DEFAULT_PUBLIC_PREFIX, S as FRAMEWORK_ENV_CATALOG, y as appEnvEntries, _ as checkEnvNaming, l as defineEnv, m as envVar, v as formatEnvErrors, C as frameworkEnvNames, i as hasEnvSnapshot, n as installEnvSnapshot, u as isEnvContract, E as pluginEnv, b as pluginEnvEntries, r as readEnvValue, T as renderDotEnvExample, t as requireEnvValue, e as resetEnvSnapshot, g as resolveEnv };
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Internal: set the public-env literal. The web codegen calls this from the
3
+ * generated entry so SSR and the browser share one install path. App code
4
+ * does not call this.
5
+ */
6
+ export declare const __installPublicEnv: (values: Readonly<Record<string, unknown>>) => void;
7
+
8
+ declare const PUBLIC_ENV_BRAND: unique symbol;
9
+
10
+ /**
11
+ * The typed public env accessor. The framework's web codegen augments this
12
+ * interface (in `src/voltro-env.d.ts`) with the declared PUBLIC keys, so
13
+ * `publicEnv.SENTRY_DSN` autocompletes to the declared type — and naming a
14
+ * SECRET or undeclared key is a **compile error** (it is not in the augmented
15
+ * surface; there is deliberately NO permissive index signature). Before the
16
+ * codegen has run (a fresh checkout, pre-`voltro dev`), no public keys are
17
+ * known yet — run `voltro dev` to generate the augmentation, like
18
+ * `rpcGroup.generated.ts`.
19
+ *
20
+ * The branded member is an internal augmentation anchor; it is never a real key.
21
+ */
22
+ export declare interface PublicEnv {
23
+ readonly [PUBLIC_ENV_BRAND]?: never;
24
+ }
25
+
26
+ /**
27
+ * Read a PUBLIC env value in the browser.
28
+ *
29
+ * ```tsx
30
+ * import { publicEnv } from '@voltro/env/public'
31
+ * <Sentry dsn={publicEnv.SENTRY_DSN} />
32
+ * ```
33
+ *
34
+ * Returns the baked value, or `undefined` if the key was not declared
35
+ * `access: 'public'` (e.g. read before the bundle set the literal, or a
36
+ * typo). The interface augmentation makes valid keys autocomplete.
37
+ */
38
+ export declare const publicEnv: PublicEnv;
39
+
40
+ /**
41
+ * The raw frozen public-env object (all public keys at once). Prefer
42
+ * `publicEnv.KEY`; use this when you need to spread or iterate.
43
+ */
44
+ export declare const publicEnvSnapshot: () => Readonly<Record<string, unknown>>;
45
+
46
+ export { }
package/dist/public.js ADDED
@@ -0,0 +1,15 @@
1
+ //#region src/public.ts
2
+ var e = () => globalThis.__voltro_env__ ?? {}, t = new Proxy({}, {
3
+ get: (t, n) => typeof n == "string" ? e()[n] : void 0,
4
+ has: (t, n) => typeof n == "string" && n in e(),
5
+ ownKeys: () => Reflect.ownKeys(e()),
6
+ getOwnPropertyDescriptor: (t, n) => typeof n == "string" && n in e() ? {
7
+ enumerable: !0,
8
+ configurable: !0,
9
+ value: e()[n]
10
+ } : void 0
11
+ }), n = () => e(), r = (e) => {
12
+ globalThis.__voltro_env__ = Object.freeze({ ...e });
13
+ };
14
+ //#endregion
15
+ export { r as __installPublicEnv, t as publicEnv, n as publicEnvSnapshot };
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Read a secret by name from the boot snapshot. Identical to `serverEnv[key]`
3
+ * but signals intent at the call site ("this is a secret"). Typed `string`
4
+ * for the common case; cast if you declared a coerced secret.
5
+ *
6
+ * ```ts
7
+ * import { getSecret } from '@voltro/env/server'
8
+ * const key = getSecret('STRIPE_KEY')
9
+ * ```
10
+ */
11
+ export declare const getSecret: (key: string) => string | undefined;
12
+
13
+ /**
14
+ * Resolve a secret LIVE through the configured backend (Vault / Doppler / http
15
+ * / env), bypassing the boot snapshot. Use this only when you genuinely need a
16
+ * post-boot re-read (e.g. a rotated remote secret) — the snapshot is correct
17
+ * for ~99% of reads and avoids a per-call backend round-trip.
18
+ */
19
+ export declare const resolveSecretLive: (key: string) => Promise<string | undefined>;
20
+
21
+ /**
22
+ * The typed server env accessor. The framework's codegen augments this
23
+ * interface in a generated `.d.ts` so `serverEnv.MY_VAR` autocompletes to the
24
+ * exact declared type. Without codegen it falls back to `unknown` per key —
25
+ * still safe, just untyped.
26
+ */
27
+ declare const SERVER_ENV_BRAND: unique symbol;
28
+
29
+ export declare interface ServerEnv {
30
+ readonly [SERVER_ENV_BRAND]?: never;
31
+ }
32
+
33
+ /**
34
+ * Read any server-readable env value (public or secret) by name.
35
+ *
36
+ * ```ts
37
+ * import { serverEnv } from '@voltro/env/server'
38
+ * const dsn = serverEnv.SENTRY_DSN // typed via generated augmentation
39
+ * ```
40
+ *
41
+ * Throws if read before the boot env gate ran (a wiring bug — env is only
42
+ * available inside handlers / loaders / startup hooks, never at import time).
43
+ */
44
+ export declare const serverEnv: ServerEnv;
45
+
46
+ export { }
package/dist/server.js ADDED
@@ -0,0 +1,12 @@
1
+ import { i as e, r as t } from "./snapshot-qX0GrQfx.js";
2
+ import { resolveSecret as n } from "@voltro/runtime";
3
+ //#region src/server.ts
4
+ var r = new Proxy({}, {
5
+ get: (t, n) => typeof n == "string" ? e(n) : void 0,
6
+ has: (e, n) => typeof n == "string" && t(n) !== void 0
7
+ }), i = (t) => {
8
+ let n = e(t);
9
+ return n === void 0 ? void 0 : String(n);
10
+ }, a = (e) => n(e);
11
+ //#endregion
12
+ export { i as getSecret, a as resolveSecretLive, r as serverEnv };
@@ -0,0 +1,11 @@
1
+ //#region src/snapshot.ts
2
+ var e = null, t = (t) => {
3
+ e = Object.freeze({ ...t });
4
+ }, n = () => e !== null, r = (t) => e === null ? void 0 : e[t], i = (t) => {
5
+ if (e === null) throw Error(`@voltro/env: read of "${t}" before the boot env gate ran. Env values are only available after the framework validates them at startup — read them inside a handler / loader / startup hook, not at module top-level.`);
6
+ return e[t];
7
+ }, a = () => {
8
+ e = null;
9
+ };
10
+ //#endregion
11
+ export { a, i, t as n, r, n as t };