@voltro/env 0.28.0 → 0.30.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 +655 -0
- package/THIRD-PARTY-NOTICES.md +87 -1
- package/dist/index.d.ts +38 -3
- package/dist/index.js +74 -74
- package/dist/server.d.ts +54 -0
- package/dist/server.js +25 -8
- package/dist/snapshot-DLnTMXyV.js +38 -0
- package/package.json +2 -2
- package/dist/snapshot-qX0GrQfx.js +0 -11
package/THIRD-PARTY-NOTICES.md
CHANGED
|
@@ -5,7 +5,7 @@ property of its respective copyright holders and is used under the terms of
|
|
|
5
5
|
its license. This file is provided for attribution; it grants no rights in
|
|
6
6
|
@voltro/env itself, which is proprietary (see LICENSE).
|
|
7
7
|
|
|
8
|
-
Generated from the resolved runtime dependency closure (
|
|
8
|
+
Generated from the resolved runtime dependency closure (52 packages).
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
@@ -3956,6 +3956,34 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
|
3956
3956
|
THE SOFTWARE.
|
|
3957
3957
|
```
|
|
3958
3958
|
|
|
3959
|
+
## isomorphic.js@0.2.5
|
|
3960
|
+
|
|
3961
|
+
License: MIT
|
|
3962
|
+
|
|
3963
|
+
```
|
|
3964
|
+
The MIT License (MIT)
|
|
3965
|
+
|
|
3966
|
+
Copyright (c) 2020 Kevin Jahns <kevin.jahns@protonmail.com>.
|
|
3967
|
+
|
|
3968
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
3969
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
3970
|
+
in the Software without restriction, including without limitation the rights
|
|
3971
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
3972
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
3973
|
+
furnished to do so, subject to the following conditions:
|
|
3974
|
+
|
|
3975
|
+
The above copyright notice and this permission notice shall be included in all
|
|
3976
|
+
copies or substantial portions of the Software.
|
|
3977
|
+
|
|
3978
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
3979
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
3980
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
3981
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
3982
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
3983
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
3984
|
+
SOFTWARE.
|
|
3985
|
+
```
|
|
3986
|
+
|
|
3959
3987
|
## jose@6.2.4
|
|
3960
3988
|
|
|
3961
3989
|
License: MIT
|
|
@@ -4018,6 +4046,34 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
|
4018
4046
|
SOFTWARE.
|
|
4019
4047
|
```
|
|
4020
4048
|
|
|
4049
|
+
## lib0@0.2.117
|
|
4050
|
+
|
|
4051
|
+
License: MIT
|
|
4052
|
+
|
|
4053
|
+
```
|
|
4054
|
+
The MIT License (MIT)
|
|
4055
|
+
|
|
4056
|
+
Copyright (c) 2019 Kevin Jahns <kevin.jahns@protonmail.com>.
|
|
4057
|
+
|
|
4058
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4059
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
4060
|
+
in the Software without restriction, including without limitation the rights
|
|
4061
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
4062
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
4063
|
+
furnished to do so, subject to the following conditions:
|
|
4064
|
+
|
|
4065
|
+
The above copyright notice and this permission notice shall be included in all
|
|
4066
|
+
copies or substantial portions of the Software.
|
|
4067
|
+
|
|
4068
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
4069
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
4070
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
4071
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
4072
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
4073
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
4074
|
+
SOFTWARE.
|
|
4075
|
+
```
|
|
4076
|
+
|
|
4021
4077
|
## mime@3.0.0
|
|
4022
4078
|
|
|
4023
4079
|
License: MIT
|
|
@@ -4566,3 +4622,33 @@ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
|
4566
4622
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
4567
4623
|
```
|
|
4568
4624
|
|
|
4625
|
+
## yjs@13.6.31
|
|
4626
|
+
|
|
4627
|
+
License: MIT
|
|
4628
|
+
|
|
4629
|
+
```
|
|
4630
|
+
The MIT License (MIT)
|
|
4631
|
+
|
|
4632
|
+
Copyright (c) 2023
|
|
4633
|
+
- Kevin Jahns <kevin.jahns@protonmail.com>.
|
|
4634
|
+
- Chair of Computer Science 5 (Databases & Information Systems), RWTH Aachen University, Germany
|
|
4635
|
+
|
|
4636
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4637
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
4638
|
+
in the Software without restriction, including without limitation the rights
|
|
4639
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
4640
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
4641
|
+
furnished to do so, subject to the following conditions:
|
|
4642
|
+
|
|
4643
|
+
The above copyright notice and this permission notice shall be included in all
|
|
4644
|
+
copies or substantial portions of the Software.
|
|
4645
|
+
|
|
4646
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
4647
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
4648
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
4649
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
4650
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
4651
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
4652
|
+
SOFTWARE.
|
|
4653
|
+
```
|
|
4654
|
+
|
package/dist/index.d.ts
CHANGED
|
@@ -289,12 +289,18 @@ export declare const frameworkEnvNames: () => ReadonlyArray<string>;
|
|
|
289
289
|
/** True once the boot gate has installed a snapshot. */
|
|
290
290
|
export declare const hasEnvSnapshot: () => boolean;
|
|
291
291
|
|
|
292
|
+
/** Whether a live rotation is in its overlap window for `key` (previous still
|
|
293
|
+
* valid). Prunes an elapsed window as a side effect, same as the read. */
|
|
294
|
+
export declare const hasLiveOverlap: (key: string) => boolean;
|
|
295
|
+
|
|
292
296
|
/** Lift a field's optionality + value type to the accessor's value type. */
|
|
293
297
|
export declare type InferEnvValue<F> = F extends EnvFieldSpec<infer A, infer O> ? (O extends true ? A | undefined : A) : never;
|
|
294
298
|
|
|
295
299
|
/** Install the boot-resolved value map. Called once by the CLI after the
|
|
296
300
|
* env-validation gate passes. Idempotent: a second install replaces the
|
|
297
|
-
* first (a supervised dev restart re-resolves cleanly)
|
|
301
|
+
* first (a supervised dev restart re-resolves cleanly) AND clears any live
|
|
302
|
+
* overlay — a fresh boot re-resolves every value, so a stale rotation must
|
|
303
|
+
* not survive it. */
|
|
298
304
|
export declare const installEnvSnapshot: (values: Record<string, unknown>) => void;
|
|
299
305
|
|
|
300
306
|
/** Runtime type guard: is `value` an `EnvContract`? Used by the CLI loader,
|
|
@@ -327,9 +333,35 @@ export declare type PublicKeys<M extends EnvSchemaMap> = {
|
|
|
327
333
|
[K in keyof M]: M[K]['access'] extends 'public' ? K : never;
|
|
328
334
|
}[keyof M];
|
|
329
335
|
|
|
330
|
-
/** Read a resolved value.
|
|
336
|
+
/** Read a resolved value. A live-rotated value wins over the frozen snapshot.
|
|
337
|
+
* Returns `undefined` for unknown / unset keys. */
|
|
331
338
|
export declare const readEnvValue: (key: string) => unknown;
|
|
332
339
|
|
|
340
|
+
/**
|
|
341
|
+
* The PREVIOUS value of a live-rotated key, while its grace window is open.
|
|
342
|
+
*
|
|
343
|
+
* A verifier that must accept work created just before a rotation (a signature,
|
|
344
|
+
* a token, a ciphertext) reads BOTH the current value and this one during
|
|
345
|
+
* cutover — the same current/previous overlap sessions/webhooks use. Returns
|
|
346
|
+
* `undefined` once the window has elapsed (the value is REVOKED, and pruned on
|
|
347
|
+
* read so it cannot linger), or when the key was never rotated with an overlap.
|
|
348
|
+
*/
|
|
349
|
+
export declare const readEnvValuePrevious: (key: string) => unknown;
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Install a live-rotated value over the frozen snapshot.
|
|
353
|
+
*
|
|
354
|
+
* `previous` + `graceMs` record the pre-rotation value as valid for a grace
|
|
355
|
+
* window: `readEnvValuePrevious` returns it until the window elapses, then
|
|
356
|
+
* revokes it. Omit them for a hard cutover with no overlap. Requires a boot
|
|
357
|
+
* snapshot to already exist — a live refresh is a POST-boot operation, and
|
|
358
|
+
* refreshing before boot is the same wiring bug `requireEnvValue` names.
|
|
359
|
+
*/
|
|
360
|
+
export declare const refreshEnvValue: (key: string, value: unknown, options?: {
|
|
361
|
+
readonly previous?: unknown;
|
|
362
|
+
readonly graceMs?: number;
|
|
363
|
+
}) => void;
|
|
364
|
+
|
|
333
365
|
/**
|
|
334
366
|
* Render a grouped `.env.example`. App + plugin entries come first (the things
|
|
335
367
|
* a developer must fill in), then the framework catalog grouped by area.
|
|
@@ -351,7 +383,7 @@ export declare interface RenderDotEnvOptions {
|
|
|
351
383
|
*/
|
|
352
384
|
export declare const requireEnvValue: (key: string) => unknown;
|
|
353
385
|
|
|
354
|
-
/** Reset the snapshot (tests). */
|
|
386
|
+
/** Reset the snapshot + any live overlay + the clock (tests). */
|
|
355
387
|
export declare const resetEnvSnapshot: () => void;
|
|
356
388
|
|
|
357
389
|
export declare interface ResolvedEnv {
|
|
@@ -395,4 +427,7 @@ declare interface SecretFieldOptions {
|
|
|
395
427
|
readonly generate?: 'base64url' | 'hex';
|
|
396
428
|
}
|
|
397
429
|
|
|
430
|
+
/** Test seam — override the clock the grace window measures against. */
|
|
431
|
+
export declare const setEnvClock: (fn: () => number) => void;
|
|
432
|
+
|
|
398
433
|
export { }
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { a as e,
|
|
2
|
-
import { Either as
|
|
1
|
+
import { a as e, c as t, i as n, l as r, n as i, o as a, r as o, s, t as c } from "./snapshot-DLnTMXyV.js";
|
|
2
|
+
import { Either as l, ParseResult as u, Schema as d } from "effect";
|
|
3
3
|
//#region src/define.ts
|
|
4
|
-
var
|
|
4
|
+
var f = "VOLTRO_PUBLIC_", p = (e, t) => Object.freeze({
|
|
5
5
|
vars: Object.freeze({ ...e }),
|
|
6
6
|
publicPrefix: t?.publicPrefix ?? "VOLTRO_PUBLIC_"
|
|
7
|
-
}),
|
|
7
|
+
}), m = (e) => typeof e == "object" && !!e && "vars" in e && typeof e.vars == "object" && e.vars !== null, h = (e, t, n, r) => ({
|
|
8
8
|
kind: e,
|
|
9
9
|
access: r.access,
|
|
10
10
|
schema: t,
|
|
@@ -13,65 +13,65 @@ var c = "VOLTRO_PUBLIC_", l = (e, t) => Object.freeze({
|
|
|
13
13
|
...r.default === void 0 ? {} : { default: r.default },
|
|
14
14
|
...r.description === void 0 ? {} : { description: r.description },
|
|
15
15
|
...r.example === void 0 ? {} : { example: r.example }
|
|
16
|
-
}),
|
|
16
|
+
}), g = d.transformOrFail(d.String, d.Boolean, {
|
|
17
17
|
strict: !0,
|
|
18
18
|
decode: (e, t, n) => {
|
|
19
19
|
let r = e.trim().toLowerCase();
|
|
20
|
-
return r === "true" || r === "1" || r === "yes" || r === "on" ?
|
|
20
|
+
return r === "true" || r === "1" || r === "yes" || r === "on" ? u.succeed(!0) : r === "false" || r === "0" || r === "no" || r === "off" || r === "" ? u.succeed(!1) : u.fail(new u.Type(n, e, `expected a boolean (true/false/1/0/yes/no/on/off), got "${e}"`));
|
|
21
21
|
},
|
|
22
|
-
encode: (e) =>
|
|
23
|
-
}),
|
|
24
|
-
string: (e) =>
|
|
25
|
-
number: (e) =>
|
|
26
|
-
port: (e) =>
|
|
27
|
-
boolean: (e) =>
|
|
22
|
+
encode: (e) => u.succeed(e ? "true" : "false")
|
|
23
|
+
}), _ = (e) => URL.canParse(e), v = {
|
|
24
|
+
string: (e) => h("string", d.String, "string", e),
|
|
25
|
+
number: (e) => h("number", d.NumberFromString, "number", e),
|
|
26
|
+
port: (e) => h("port", d.NumberFromString.pipe(d.int(), d.between(1, 65535)), "number", e),
|
|
27
|
+
boolean: (e) => h("boolean", g, "boolean", e),
|
|
28
28
|
secret: (e) => {
|
|
29
29
|
let t = e.minLength ?? 32;
|
|
30
30
|
return {
|
|
31
|
-
...
|
|
31
|
+
...h("secret", d.String.pipe(d.filter((e) => e.trim().length >= t || `expected at least ${t} characters — this looks like a placeholder, not a secret. Generate one with \`voltro secret generate\``)), "string", {
|
|
32
32
|
...e,
|
|
33
33
|
access: "secret"
|
|
34
34
|
}),
|
|
35
35
|
...e.generate === void 0 ? {} : { generate: e.generate }
|
|
36
36
|
};
|
|
37
37
|
},
|
|
38
|
-
url: (e) =>
|
|
39
|
-
enum: (e, t) =>
|
|
40
|
-
},
|
|
41
|
-
let r = t.schema, i =
|
|
42
|
-
return
|
|
38
|
+
url: (e) => h("url", d.String.pipe(d.filter((e) => _(e) || `expected a valid URL, got "${e}"`)), "string", e),
|
|
39
|
+
enum: (e, t) => h("enum", d.Literal(...e), e.map((e) => `'${e}'`).join(" | "), t)
|
|
40
|
+
}, y = (e, t, n) => {
|
|
41
|
+
let r = t.schema, i = d.decodeUnknownEither(r)(n);
|
|
42
|
+
return l.mapLeft(i, (n) => ({
|
|
43
43
|
key: e,
|
|
44
44
|
access: t.access,
|
|
45
45
|
reason: "invalid",
|
|
46
46
|
message: n.message
|
|
47
47
|
}));
|
|
48
|
-
},
|
|
49
|
-
let t = e.readEnv ?? ((e) => process.env[e]), n = e.resolveSecret ?? ((e) => Promise.resolve(t(e))), r = {}, i = {},
|
|
50
|
-
for (let [e,
|
|
51
|
-
let
|
|
52
|
-
if (
|
|
53
|
-
|
|
48
|
+
}, b = async (e) => {
|
|
49
|
+
let t = e.readEnv ?? ((e) => process.env[e]), n = e.resolveSecret ?? ((e) => Promise.resolve(t(e))), r = {}, i = {}, a = [], o = e.contract ? Object.entries(e.contract.vars) : [];
|
|
50
|
+
for (let [e, s] of o) {
|
|
51
|
+
let o = (s.access === "secret" ? await n(e) : t(e)) ?? s.default;
|
|
52
|
+
if (o === void 0) {
|
|
53
|
+
s.optional ? (r[e] = void 0, s.access === "public" && (i[e] = void 0)) : a.push({
|
|
54
54
|
key: e,
|
|
55
|
-
access:
|
|
55
|
+
access: s.access,
|
|
56
56
|
reason: "missing",
|
|
57
|
-
message: `required ${
|
|
57
|
+
message: `required ${s.access} env var "${e}" is unset` + (s.description ? ` — ${s.description}` : "")
|
|
58
58
|
});
|
|
59
59
|
continue;
|
|
60
60
|
}
|
|
61
|
-
let
|
|
62
|
-
if (
|
|
63
|
-
|
|
61
|
+
let c = y(e, s, o);
|
|
62
|
+
if (l.isLeft(c)) {
|
|
63
|
+
a.push(c.left);
|
|
64
64
|
continue;
|
|
65
65
|
}
|
|
66
|
-
r[e] =
|
|
66
|
+
r[e] = c.right, s.access === "public" && (i[e] = c.right);
|
|
67
67
|
}
|
|
68
68
|
return {
|
|
69
69
|
values: r,
|
|
70
70
|
publicValues: i,
|
|
71
|
-
errors:
|
|
72
|
-
ok:
|
|
71
|
+
errors: a,
|
|
72
|
+
ok: a.length === 0
|
|
73
73
|
};
|
|
74
|
-
},
|
|
74
|
+
}, x = (e, t) => {
|
|
75
75
|
let n = e?.publicPrefix;
|
|
76
76
|
if (!e || typeof n != "string") return [];
|
|
77
77
|
let r = [];
|
|
@@ -87,10 +87,10 @@ var c = "VOLTRO_PUBLIC_", l = (e, t) => Object.freeze({
|
|
|
87
87
|
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.`
|
|
88
88
|
});
|
|
89
89
|
return r;
|
|
90
|
-
},
|
|
90
|
+
}, S = (e) => {
|
|
91
91
|
let t = e.map((e) => ` • ${e.key} (${e.access}, ${e.reason}): ${e.message}`);
|
|
92
92
|
return `environment validation failed (${e.length} ${e.length === 1 ? "error" : "errors"}):\n${t.join("\n")}`;
|
|
93
|
-
},
|
|
93
|
+
}, C = (e) => e ? Object.entries(e.vars).map(([e, t]) => ({
|
|
94
94
|
key: e,
|
|
95
95
|
owner: "app",
|
|
96
96
|
access: t.access,
|
|
@@ -99,14 +99,14 @@ var c = "VOLTRO_PUBLIC_", l = (e, t) => Object.freeze({
|
|
|
99
99
|
...t.example === void 0 ? {} : { example: t.example },
|
|
100
100
|
...t.default === void 0 ? {} : { default: t.default },
|
|
101
101
|
kind: t.kind
|
|
102
|
-
})) : [],
|
|
102
|
+
})) : [], w = (e, t) => t ? t.map((t) => ({
|
|
103
103
|
key: t.name,
|
|
104
104
|
owner: `plugin:${e}`,
|
|
105
105
|
access: t.secret ? "secret" : "public",
|
|
106
106
|
required: t.required,
|
|
107
107
|
...t.description === void 0 ? {} : { description: t.description },
|
|
108
108
|
...t.example === void 0 ? {} : { example: t.example }
|
|
109
|
-
})) : [],
|
|
109
|
+
})) : [], T = (e, t, n, r, i) => ({
|
|
110
110
|
key: e,
|
|
111
111
|
owner: "framework",
|
|
112
112
|
access: n,
|
|
@@ -114,37 +114,37 @@ var c = "VOLTRO_PUBLIC_", l = (e, t) => Object.freeze({
|
|
|
114
114
|
description: r,
|
|
115
115
|
group: t,
|
|
116
116
|
...i === void 0 ? {} : { example: i }
|
|
117
|
-
}),
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
],
|
|
117
|
+
}), E = [
|
|
118
|
+
T("NODE_ENV", "runtime", "public", "Node environment (development | production).", "development"),
|
|
119
|
+
T("PORT", "runtime", "public", "Port the api/web server binds to. Overrides app.config `port`.", "4000"),
|
|
120
|
+
T("VOLTRO_REGION", "runtime", "public", "Region tag for replica locality (else AWS_REGION/FLY_REGION/…).", "us-east-1"),
|
|
121
|
+
T("VOLTRO_AUTO_MIGRATE", "runtime", "public", "Set 0 to skip boot auto-migrate (CI-applied migrations).", "1"),
|
|
122
|
+
T("DB_DIALECT", "database", "public", "postgres | mysql | mariadb | mssql | sqlite | memory.", "postgres"),
|
|
123
|
+
T("DB_URL", "database", "secret", "Connection URL (carries credentials). Preferred over discrete DB_* fields.", "postgres://user:pw@localhost:5432/app"),
|
|
124
|
+
T("DB_HOST", "database", "public", "Database host (discrete-field form; DB_URL preferred).", "localhost"),
|
|
125
|
+
T("DB_PORT", "database", "public", "Database port.", "5432"),
|
|
126
|
+
T("DB_USER", "database", "public", "Database user.", "postgres"),
|
|
127
|
+
T("DB_PASSWORD", "database", "secret", "Database password."),
|
|
128
|
+
T("DB_DATABASE", "database", "public", "Database name.", "app"),
|
|
129
|
+
T("DB_MAX_CONNECTIONS", "database", "public", "Per-process connection pool cap.", "10"),
|
|
130
|
+
T("DB_REPLICA_URLS", "database", "secret", "Comma-separated read-replica URLs (carry credentials)."),
|
|
131
|
+
T("DB_REPLICA_REGIONS", "database", "public", "Comma-separated region tags matching DB_REPLICA_URLS order."),
|
|
132
|
+
T("RYW_STORE", "database", "public", "Read-your-writes position store: memory | redis.", "memory"),
|
|
133
|
+
T("CACHE_BACKEND", "cache", "public", "@voltro/cache backend: memory | redis.", "memory"),
|
|
134
|
+
T("CACHE_REDIS_URL", "cache", "secret", "Redis/Valkey/KeyDB/Dragonfly/Upstash URL (may carry credentials).", "redis://localhost:6379"),
|
|
135
|
+
T("CACHE_REDIS_DRIVER", "cache", "public", "resp (ioredis TCP) | http (Upstash REST, edge).", "resp"),
|
|
136
|
+
T("CACHE_REDIS_TOKEN", "cache", "secret", "Upstash REST token (http driver)."),
|
|
137
|
+
T("CACHE_KEY_PREFIX", "cache", "public", "Cache key namespace.", "voltro:cache"),
|
|
138
|
+
T("REDIS_URL", "cache", "secret", "Generic Redis URL fallback (cache / ryw / ratelimit).", "redis://localhost:6379"),
|
|
139
|
+
T("VOLTRO_SESSION_SECRET", "security", "secret", "HMAC signing key for session cookies. REQUIRED in production."),
|
|
140
|
+
T("VOLTRO_INSPECT", "security", "public", "Set off to disable the /_voltro/inspect surface entirely.", "on"),
|
|
141
|
+
T("VOLTRO_INSPECT_TOKEN", "security", "secret", "Bearer token gating /_voltro/inspect/* on public deploys."),
|
|
142
|
+
T("VOLTRO_TENANT_ISOLATION", "security", "public", "shared-schema (default) | namespace (physical per-tenant)."),
|
|
143
|
+
T("FRAMEWORK_TRACING", "observability", "public", "off | console | otlp. Auto-otlp when OTEL_* is set."),
|
|
144
|
+
T("OTEL_EXPORTER_OTLP_ENDPOINT", "observability", "public", "OTLP/HTTP collector endpoint (traces + metrics).", "http://localhost:4318"),
|
|
145
|
+
T("OTEL_SERVICE_NAME", "observability", "public", "OTel service name.", "voltro-api"),
|
|
146
|
+
T("SSR_CACHE", "web", "public", "ISR cache backend for `voltro start`: memory | postgres.", "memory")
|
|
147
|
+
], D = () => E.map((e) => e.key), O = (e) => {
|
|
148
148
|
let t = [];
|
|
149
149
|
e.description && t.push(`# ${e.description}`);
|
|
150
150
|
let n = [
|
|
@@ -155,17 +155,17 @@ var c = "VOLTRO_PUBLIC_", l = (e, t) => Object.freeze({
|
|
|
155
155
|
t.push(`# (${n})`);
|
|
156
156
|
let r = e.example ?? e.default ?? "";
|
|
157
157
|
return t.push(`${e.key}=${e.access === "secret" && !e.example ? "" : r}`), t.join("\n");
|
|
158
|
-
},
|
|
158
|
+
}, k = (e, t = {}) => {
|
|
159
159
|
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."];
|
|
160
160
|
n.push(...r.map((e) => `# ${e}`), "");
|
|
161
161
|
let i = e.filter((e) => e.owner === "app"), a = e.filter((e) => e.owner.startsWith("plugin:"));
|
|
162
|
-
if (i.length > 0 && (n.push("# ── App ──────────────────────────────────────────────", ""), n.push(...i.map(
|
|
163
|
-
let e = [...new Set(
|
|
162
|
+
if (i.length > 0 && (n.push("# ── App ──────────────────────────────────────────────", ""), n.push(...i.map(O).flatMap((e) => [e, ""]))), a.length > 0 && (n.push("# ── Plugins ──────────────────────────────────────────", ""), n.push(...a.map(O).flatMap((e) => [e, ""]))), t.includeFramework !== !1) {
|
|
163
|
+
let e = [...new Set(E.map((e) => e.group))];
|
|
164
164
|
n.push("# ── Framework ────────────────────────────────────────", "");
|
|
165
|
-
for (let t of e) n.push(`# [${t}]`), n.push(...
|
|
165
|
+
for (let t of e) n.push(`# [${t}]`), n.push(...E.filter((e) => e.group === t).map(O).flatMap((e) => [e, ""]));
|
|
166
166
|
}
|
|
167
167
|
return n.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
|
|
168
|
-
},
|
|
168
|
+
}, A = (e) => {
|
|
169
169
|
let t = new Set(e.map((e) => e.name));
|
|
170
170
|
return {
|
|
171
171
|
declared: e,
|
|
@@ -176,4 +176,4 @@ var c = "VOLTRO_PUBLIC_", l = (e, t) => Object.freeze({
|
|
|
176
176
|
};
|
|
177
177
|
};
|
|
178
178
|
//#endregion
|
|
179
|
-
export {
|
|
179
|
+
export { f as DEFAULT_PUBLIC_PREFIX, E as FRAMEWORK_ENV_CATALOG, C as appEnvEntries, x as checkEnvNaming, p as defineEnv, v as envVar, S as formatEnvErrors, D as frameworkEnvNames, c as hasEnvSnapshot, i as hasLiveOverlap, o as installEnvSnapshot, m as isEnvContract, A as pluginEnv, w as pluginEnvEntries, n as readEnvValue, e as readEnvValuePrevious, a as refreshEnvValue, k as renderDotEnvExample, s as requireEnvValue, t as resetEnvSnapshot, b as resolveEnv, r as setEnvClock };
|
package/dist/server.d.ts
CHANGED
|
@@ -10,6 +10,19 @@
|
|
|
10
10
|
*/
|
|
11
11
|
export declare const getSecret: (key: string) => string | undefined;
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Read a secret together with its previous value during a rotation overlap.
|
|
15
|
+
*
|
|
16
|
+
* `current` is what everything else now sees; `previous` is present only while
|
|
17
|
+
* a `rotateSecretLive` grace window is open (then `undefined` — revoked). A
|
|
18
|
+
* verifier tries `current` first and falls back to `previous`, exactly like
|
|
19
|
+
* session verification honoring `VOLTRO_SESSION_SECRET` + `_PREVIOUS`.
|
|
20
|
+
*/
|
|
21
|
+
export declare const getSecretWithOverlap: (key: string) => {
|
|
22
|
+
readonly current: string | undefined;
|
|
23
|
+
readonly previous: string | undefined;
|
|
24
|
+
};
|
|
25
|
+
|
|
13
26
|
/**
|
|
14
27
|
* Resolve a secret LIVE through the configured backend (Vault / Doppler / http
|
|
15
28
|
* / env), bypassing the boot snapshot. Use this only when you genuinely need a
|
|
@@ -18,6 +31,47 @@ export declare const getSecret: (key: string) => string | undefined;
|
|
|
18
31
|
*/
|
|
19
32
|
export declare const resolveSecretLive: (key: string) => Promise<string | undefined>;
|
|
20
33
|
|
|
34
|
+
/**
|
|
35
|
+
* The blessed live-rotation flow: re-resolve `key` through the configured
|
|
36
|
+
* backend, install the NEW value over the boot snapshot so a running process
|
|
37
|
+
* serves it immediately, and hold the OLD value for `graceMs` so work
|
|
38
|
+
* created just before the rotation still verifies during cutover.
|
|
39
|
+
*
|
|
40
|
+
* This is the "set new → overlap → revoke" runbook made mechanical: after
|
|
41
|
+
* `graceMs` the previous value is revoked automatically (pruned on read — no
|
|
42
|
+
* timer, no dev/serve boot wiring), so an operator drives only the ONE call and
|
|
43
|
+
* the grace duration, not a sequence of manual `.env` edits. Reads through
|
|
44
|
+
* `getSecret` / `serverEnv` see the new value at once; a verifier that must
|
|
45
|
+
* accept both during cutover reads `getSecretWithOverlap`.
|
|
46
|
+
*
|
|
47
|
+
* IMPORTANT — scope of "without a restart". This updates what CODE that reads a
|
|
48
|
+
* secret PER USE sees (an outbound API key, a webhook-signing secret, a
|
|
49
|
+
* field-encryption key). It does NOT re-establish a connection that captured the
|
|
50
|
+
* old value at construction — a live DB pool built with the old `DB_URL`
|
|
51
|
+
* password keeps that connection until it is rebuilt (still a restart, or a
|
|
52
|
+
* pool-level reconnect). Rotate connection credentials with that in mind.
|
|
53
|
+
*
|
|
54
|
+
* The default grace is 5 minutes — long enough for in-flight requests and
|
|
55
|
+
* short-TTL tokens to drain, short enough that a leaked value's window closes
|
|
56
|
+
* promptly. Pass `graceMs: 0` for a hard cutover with no overlap.
|
|
57
|
+
*/
|
|
58
|
+
export declare const rotateSecretLive: (key: string, options?: {
|
|
59
|
+
readonly graceMs?: number;
|
|
60
|
+
}) => Promise<SecretRotation>;
|
|
61
|
+
|
|
62
|
+
/** The outcome of a live rotation — what `getSecret` now returns and, while the
|
|
63
|
+
* grace window is open, the value that still verifies during cutover. */
|
|
64
|
+
export declare interface SecretRotation {
|
|
65
|
+
readonly key: string;
|
|
66
|
+
/** The freshly-resolved value now served by `getSecret` / `serverEnv`. */
|
|
67
|
+
readonly current: string | undefined;
|
|
68
|
+
/** The pre-rotation value, valid until `graceMs` elapses (`undefined` on a
|
|
69
|
+
* hard cutover, or when there was no prior value). */
|
|
70
|
+
readonly previous: string | undefined;
|
|
71
|
+
/** The overlap window, in ms, during which `previous` still verifies. */
|
|
72
|
+
readonly graceMs: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
21
75
|
/**
|
|
22
76
|
* The typed server env accessor. The framework's codegen augments this
|
|
23
77
|
* interface in a generated `.d.ts` so `serverEnv.MY_VAR` autocompletes to the
|
package/dist/server.js
CHANGED
|
@@ -1,12 +1,29 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { resolveSecret as
|
|
1
|
+
import { a as e, i as t, o as n, s as r } from "./snapshot-DLnTMXyV.js";
|
|
2
|
+
import { resolveSecret as i } from "@voltro/runtime";
|
|
3
3
|
//#region src/server.ts
|
|
4
|
-
var
|
|
5
|
-
get: (
|
|
4
|
+
var a = new Proxy({}, {
|
|
5
|
+
get: (e, t) => typeof t == "string" ? r(t) : void 0,
|
|
6
6
|
has: (e, n) => typeof n == "string" && t(n) !== void 0
|
|
7
|
-
}),
|
|
7
|
+
}), o = (e) => {
|
|
8
|
+
let t = r(e);
|
|
9
|
+
return t === void 0 ? void 0 : String(t);
|
|
10
|
+
}, s = (e) => i(e), c = async (e, r) => {
|
|
11
|
+
let a = r?.graceMs ?? 5 * 6e4, o = t(e), s = o === void 0 ? void 0 : String(o), c = await i(e);
|
|
12
|
+
return n(e, c, {
|
|
13
|
+
...s === void 0 ? {} : { previous: s },
|
|
14
|
+
graceMs: a
|
|
15
|
+
}), {
|
|
16
|
+
key: e,
|
|
17
|
+
current: c,
|
|
18
|
+
previous: s,
|
|
19
|
+
graceMs: a
|
|
20
|
+
};
|
|
21
|
+
}, l = (t) => {
|
|
8
22
|
let n = e(t);
|
|
9
|
-
return
|
|
10
|
-
|
|
23
|
+
return {
|
|
24
|
+
current: o(t),
|
|
25
|
+
previous: n === void 0 ? void 0 : String(n)
|
|
26
|
+
};
|
|
27
|
+
};
|
|
11
28
|
//#endregion
|
|
12
|
-
export {
|
|
29
|
+
export { o as getSecret, l as getSecretWithOverlap, s as resolveSecretLive, c as rotateSecretLive, a as serverEnv };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//#region src/snapshot.ts
|
|
2
|
+
var e = null, t = null, n = () => Date.now(), r = (n) => {
|
|
3
|
+
e = Object.freeze({ ...n }), t = null;
|
|
4
|
+
}, i = () => e !== null, a = (n) => {
|
|
5
|
+
let r = t?.get(n);
|
|
6
|
+
return r === void 0 ? e === null ? void 0 : e[n] : r.value;
|
|
7
|
+
}, o = (n) => {
|
|
8
|
+
let r = t?.get(n);
|
|
9
|
+
if (r !== void 0) return r.value;
|
|
10
|
+
if (e === null) throw Error(`@voltro/env: read of "${n}" 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.`);
|
|
11
|
+
return e[n];
|
|
12
|
+
}, s = (r, i, a) => {
|
|
13
|
+
if (e === null) throw Error(`@voltro/env: refreshEnvValue("${r}") before the boot env gate ran. A live secret rotation is a post-boot operation — it replaces a value the boot gate already resolved.`);
|
|
14
|
+
t === null && (t = /* @__PURE__ */ new Map());
|
|
15
|
+
let o = a?.previous !== void 0 && a.graceMs !== void 0 && a.graceMs > 0;
|
|
16
|
+
t.set(r, {
|
|
17
|
+
value: i,
|
|
18
|
+
...o ? {
|
|
19
|
+
previous: a.previous,
|
|
20
|
+
previousExpiresAt: n() + a.graceMs
|
|
21
|
+
} : {}
|
|
22
|
+
});
|
|
23
|
+
}, c = (e) => {
|
|
24
|
+
let r = t?.get(e);
|
|
25
|
+
if (!(r === void 0 || r.previousExpiresAt === void 0)) {
|
|
26
|
+
if (n() >= r.previousExpiresAt) {
|
|
27
|
+
t.set(e, { value: r.value });
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
return r.previous;
|
|
31
|
+
}
|
|
32
|
+
}, l = (e) => c(e) !== void 0, u = (e) => {
|
|
33
|
+
n = e;
|
|
34
|
+
}, d = () => {
|
|
35
|
+
e = null, t = null, n = () => Date.now();
|
|
36
|
+
};
|
|
37
|
+
//#endregion
|
|
38
|
+
export { c as a, d as c, a as i, u as l, l as n, s as o, r, o as s, i as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/env",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
4
4
|
"description": "Typed, schema-validated environment variables with a structural public/secret boundary. Declare once in app.config.ts; read public vars in the browser, secrets only on the server.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"voltro",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"node": ">=24.0.0"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@voltro/runtime": "0.
|
|
45
|
+
"@voltro/runtime": "0.30.0"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"effect": "^3.22.0"
|
|
@@ -1,11 +0,0 @@
|
|
|
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 };
|