@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.
@@ -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 (49 packages).
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. Returns `undefined` for unknown / unset keys. */
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, 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";
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 c = "VOLTRO_PUBLIC_", l = (e, t) => Object.freeze({
4
+ var f = "VOLTRO_PUBLIC_", p = (e, t) => Object.freeze({
5
5
  vars: Object.freeze({ ...e }),
6
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) => ({
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
- }), f = s.transformOrFail(s.String, s.Boolean, {
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" ? 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}"`));
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) => 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),
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
- ...d("secret", s.String.pipe(s.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", {
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) => d("url", s.String.pipe(s.filter((e) => p(e) || `expected a valid URL, got "${e}"`)), "string", e),
39
- enum: (e, t) => d("enum", s.Literal(...e), e.map((e) => `'${e}'`).join(" | "), t)
40
- }, h = (e, t, n) => {
41
- let r = t.schema, i = s.decodeUnknownEither(r)(n);
42
- return a.mapLeft(i, (n) => ({
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
- }, g = async (e) => {
49
- 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) : [];
50
- for (let [e, c] of s) {
51
- let s = (c.access === "secret" ? await n(e) : t(e)) ?? c.default;
52
- if (s === void 0) {
53
- c.optional ? (r[e] = void 0, c.access === "public" && (i[e] = void 0)) : o.push({
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: c.access,
55
+ access: s.access,
56
56
  reason: "missing",
57
- message: `required ${c.access} env var "${e}" is unset` + (c.description ? ` — ${c.description}` : "")
57
+ message: `required ${s.access} env var "${e}" is unset` + (s.description ? ` — ${s.description}` : "")
58
58
  });
59
59
  continue;
60
60
  }
61
- let l = h(e, c, s);
62
- if (a.isLeft(l)) {
63
- o.push(l.left);
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] = l.right, c.access === "public" && (i[e] = l.right);
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: o,
72
- ok: o.length === 0
71
+ errors: a,
72
+ ok: a.length === 0
73
73
  };
74
- }, _ = (e, t) => {
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
- }, v = (e) => {
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
- }, y = (e) => e ? Object.entries(e.vars).map(([e, t]) => ({
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
- })) : [], b = (e, t) => t ? t.map((t) => ({
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
- })) : [], x = (e, t, n, r, i) => ({
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
- }), S = [
118
- x("NODE_ENV", "runtime", "public", "Node environment (development | production).", "development"),
119
- x("PORT", "runtime", "public", "Port the api/web server binds to. Overrides app.config `port`.", "4000"),
120
- x("VOLTRO_REGION", "runtime", "public", "Region tag for replica locality (else AWS_REGION/FLY_REGION/…).", "us-east-1"),
121
- x("VOLTRO_AUTO_MIGRATE", "runtime", "public", "Set 0 to skip boot auto-migrate (CI-applied migrations).", "1"),
122
- x("DB_DIALECT", "database", "public", "postgres | mysql | mariadb | mssql | sqlite | memory.", "postgres"),
123
- x("DB_URL", "database", "secret", "Connection URL (carries credentials). Preferred over discrete DB_* fields.", "postgres://user:pw@localhost:5432/app"),
124
- x("DB_HOST", "database", "public", "Database host (discrete-field form; DB_URL preferred).", "localhost"),
125
- x("DB_PORT", "database", "public", "Database port.", "5432"),
126
- x("DB_USER", "database", "public", "Database user.", "postgres"),
127
- x("DB_PASSWORD", "database", "secret", "Database password."),
128
- x("DB_DATABASE", "database", "public", "Database name.", "app"),
129
- x("DB_MAX_CONNECTIONS", "database", "public", "Per-process connection pool cap.", "10"),
130
- x("DB_REPLICA_URLS", "database", "secret", "Comma-separated read-replica URLs (carry credentials)."),
131
- x("DB_REPLICA_REGIONS", "database", "public", "Comma-separated region tags matching DB_REPLICA_URLS order."),
132
- x("RYW_STORE", "database", "public", "Read-your-writes position store: memory | redis.", "memory"),
133
- x("CACHE_BACKEND", "cache", "public", "@voltro/cache backend: memory | redis.", "memory"),
134
- x("CACHE_REDIS_URL", "cache", "secret", "Redis/Valkey/KeyDB/Dragonfly/Upstash URL (may carry credentials).", "redis://localhost:6379"),
135
- x("CACHE_REDIS_DRIVER", "cache", "public", "resp (ioredis TCP) | http (Upstash REST, edge).", "resp"),
136
- x("CACHE_REDIS_TOKEN", "cache", "secret", "Upstash REST token (http driver)."),
137
- x("CACHE_KEY_PREFIX", "cache", "public", "Cache key namespace.", "voltro:cache"),
138
- x("REDIS_URL", "cache", "secret", "Generic Redis URL fallback (cache / ryw / ratelimit).", "redis://localhost:6379"),
139
- x("VOLTRO_SESSION_SECRET", "security", "secret", "HMAC signing key for session cookies. REQUIRED in production."),
140
- x("VOLTRO_INSPECT", "security", "public", "Set off to disable the /_voltro/inspect surface entirely.", "on"),
141
- x("VOLTRO_INSPECT_TOKEN", "security", "secret", "Bearer token gating /_voltro/inspect/* on public deploys."),
142
- x("VOLTRO_TENANT_ISOLATION", "security", "public", "shared-schema (default) | namespace (physical per-tenant)."),
143
- x("FRAMEWORK_TRACING", "observability", "public", "off | console | otlp. Auto-otlp when OTEL_* is set."),
144
- x("OTEL_EXPORTER_OTLP_ENDPOINT", "observability", "public", "OTLP/HTTP collector endpoint (traces + metrics).", "http://localhost:4318"),
145
- x("OTEL_SERVICE_NAME", "observability", "public", "OTel service name.", "voltro-api"),
146
- x("SSR_CACHE", "web", "public", "ISR cache backend for `voltro start`: memory | postgres.", "memory")
147
- ], C = () => S.map((e) => e.key), w = (e) => {
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
- }, T = (e, t = {}) => {
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(w).flatMap((e) => [e, ""]))), a.length > 0 && (n.push("# ── Plugins ──────────────────────────────────────────", ""), n.push(...a.map(w).flatMap((e) => [e, ""]))), t.includeFramework !== !1) {
163
- let e = [...new Set(S.map((e) => e.group))];
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(...S.filter((e) => e.group === t).map(w).flatMap((e) => [e, ""]));
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
- }, E = (e) => {
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 { 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 };
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 { i as e, r as t } from "./snapshot-qX0GrQfx.js";
2
- import { resolveSecret as n } from "@voltro/runtime";
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 r = new Proxy({}, {
5
- get: (t, n) => typeof n == "string" ? e(n) : void 0,
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
- }), i = (t) => {
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 n === void 0 ? void 0 : String(n);
10
- }, a = (e) => n(e);
23
+ return {
24
+ current: o(t),
25
+ previous: n === void 0 ? void 0 : String(n)
26
+ };
27
+ };
11
28
  //#endregion
12
- export { i as getSecret, a as resolveSecretLive, r as serverEnv };
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.28.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.28.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 };