@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.
- package/CHANGELOG.md +52 -0
- package/LICENSE +57 -0
- package/README.md +26 -0
- package/SECURITY.md +56 -0
- package/THIRD-PARTY-NOTICES.md +4596 -0
- package/dist/index.d.ts +352 -0
- package/dist/index.js +169 -0
- package/dist/public.d.ts +46 -0
- package/dist/public.js +15 -0
- package/dist/server.d.ts +46 -0
- package/dist/server.js +12 -0
- package/dist/snapshot-qX0GrQfx.js +11 -0
- package/package.json +53 -0
package/dist/index.d.ts
ADDED
|
@@ -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 };
|
package/dist/public.d.ts
ADDED
|
@@ -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 };
|
package/dist/server.d.ts
ADDED
|
@@ -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 };
|