@penvhq/runtime 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/LICENSE +21 -0
- package/dist/chunk-57AVY2K5.js +63 -0
- package/dist/chunk-57AVY2K5.js.map +1 -0
- package/dist/config.cjs +71 -0
- package/dist/config.cjs.map +1 -0
- package/dist/config.d.cts +2 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +21 -0
- package/dist/config.js.map +1 -0
- package/dist/index.cjs +140 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +24 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +68 -0
- package/dist/index.js.map +1 -0
- package/package.json +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Itzfeminisce
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// src/resolve.ts
|
|
2
|
+
import { dirname, resolve as resolvePath } from "path";
|
|
3
|
+
import {
|
|
4
|
+
candidatesFor,
|
|
5
|
+
formatValueFile,
|
|
6
|
+
loadConfig,
|
|
7
|
+
openValue,
|
|
8
|
+
parameterId,
|
|
9
|
+
resolveEnvironment,
|
|
10
|
+
resolveKeySource,
|
|
11
|
+
UndecryptableValueError
|
|
12
|
+
} from "@penvhq/core";
|
|
13
|
+
import { createFilesystemProvider } from "@penvhq/provider-filesystem";
|
|
14
|
+
function refsFrom(files) {
|
|
15
|
+
const refs = /* @__PURE__ */ new Map();
|
|
16
|
+
for (const file of files) {
|
|
17
|
+
const ref = { namespace: file.namespace, name: file.name };
|
|
18
|
+
const id = parameterId(ref);
|
|
19
|
+
if (!refs.has(id)) {
|
|
20
|
+
refs.set(id, ref);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return [...refs.values()].sort((a, b) => {
|
|
24
|
+
const left = parameterId(a);
|
|
25
|
+
const right = parameterId(b);
|
|
26
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function resolveSync(cwd, environment) {
|
|
30
|
+
const { config, file } = loadConfig(cwd);
|
|
31
|
+
const target = resolveEnvironment(config, environment);
|
|
32
|
+
const provider = createFilesystemProvider({
|
|
33
|
+
root: resolvePath(dirname(file), ".penv"),
|
|
34
|
+
config
|
|
35
|
+
});
|
|
36
|
+
const keys = resolveKeySource(config, target);
|
|
37
|
+
const values = [];
|
|
38
|
+
for (const ref of refsFrom(provider.listSync())) {
|
|
39
|
+
for (const candidate of candidatesFor(ref, target)) {
|
|
40
|
+
const read = provider.readSync(candidate);
|
|
41
|
+
if (read === void 0) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const opened = openValue(candidate, read, keys);
|
|
45
|
+
if (opened.kind === "failed") {
|
|
46
|
+
throw new UndecryptableValueError(
|
|
47
|
+
parameterId(ref),
|
|
48
|
+
target,
|
|
49
|
+
formatValueFile(candidate),
|
|
50
|
+
opened.failure
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
values.push({ ref, value: opened.value });
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { config, environment: target, values };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export {
|
|
61
|
+
resolveSync
|
|
62
|
+
};
|
|
63
|
+
//# sourceMappingURL=chunk-57AVY2K5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/resolve.ts"],"sourcesContent":["/**\n * The synchronous half of the value cascade, shared by `load` and the\n * `penv/config` compatibility entry.\n *\n * `resolveParameter` in `@penvhq/core` is async because the provider contract is,\n * and `load` is synchronous — so this module walks the cascade against the\n * filesystem provider's synchronous reads instead. It does not restate the\n * precedence rule: `candidatesFor` owns the order and everything here only\n * walks the list it returns, so the two paths cannot drift apart.\n *\n * The runtime reads the local tree for every environment, whatever provider\n * that environment declares, and this is the design rather than a limitation.\n * A provider is where an environment's source of truth *lives*, not where the\n * runtime reads from: `penv pull` materialises the tree from the provider, and\n * the runtime then reads what is on disk. So `load` never inspects\n * `providers.*.type` — a Vault-backed environment resolves through exactly the\n * code path a filesystem-backed one does, which is what makes changing provider\n * a configuration change rather than an application rewrite.\n *\n * Decryption happens here, synchronously, and that is the same knife applied a\n * second time. Key *acquisition* is async and happens before the process starts:\n * a deploy unwraps the KMS-derived data key and exports it, exactly as it already\n * runs `penv pull` to materialise the tree. Key *use* is a synchronous pure\n * function over bytes. penv never calls a KMS in-process — a key is where an\n * environment's secret material *lives*, not what the runtime dials at boot, just\n * as a provider is a sync target and not a runtime source. So invariant 3 is not\n * traded against encryption: `load` stays generic and synchronous because the\n * network half of both problems was moved out of the process entirely, rather\n * than awaited inside it.\n */\n\nimport { dirname, resolve as resolvePath } from \"node:path\";\nimport type { ParameterRef, PenvConfig, ValueFile } from \"@penvhq/core\";\nimport {\n candidatesFor,\n formatValueFile,\n loadConfig,\n openValue,\n parameterId,\n resolveEnvironment,\n resolveKeySource,\n UndecryptableValueError,\n} from \"@penvhq/core\";\nimport { createFilesystemProvider } from \"@penvhq/provider-filesystem\";\n\n/** One parameter that resolved to a present value for the target environment. */\nexport interface ResolvedValue {\n readonly ref: ParameterRef;\n readonly value: string;\n}\n\nexport interface ResolvedConfig {\n readonly config: PenvConfig;\n readonly environment: string;\n /**\n * Only parameters with a present candidate. A parameter that resolved to\n * nothing is absent rather than `undefined`, so requiredness stays the\n * schema's call.\n */\n readonly values: readonly ResolvedValue[];\n}\n\n/**\n * The parameters behind a set of value files, scopes collapsed —\n * `redis/password.production` and `redis/password` are one parameter. Ordered\n * so a `process.env` population is identical on every machine.\n */\nfunction refsFrom(files: readonly ValueFile[]): ParameterRef[] {\n const refs = new Map<string, ParameterRef>();\n for (const file of files) {\n const ref: ParameterRef = { namespace: file.namespace, name: file.name };\n const id = parameterId(ref);\n if (!refs.has(id)) {\n refs.set(id, ref);\n }\n }\n return [...refs.values()].sort((a, b) => {\n const left = parameterId(a);\n const right = parameterId(b);\n return left < right ? -1 : left > right ? 1 : 0;\n });\n}\n\n/**\n * Loads the config, settles the environment, and resolves every parameter the\n * local tree holds — the work `load` and `penv/config` both start from.\n */\nexport function resolveSync(cwd: string, environment?: string): ResolvedConfig {\n const { config, file } = loadConfig(cwd);\n const target = resolveEnvironment(config, environment);\n\n const provider = createFilesystemProvider({\n root: resolvePath(dirname(file), \".penv\"),\n config,\n });\n\n const keys = resolveKeySource(config, target);\n\n const values: ResolvedValue[] = [];\n for (const ref of refsFrom(provider.listSync())) {\n for (const candidate of candidatesFor(ref, target)) {\n const read = provider.readSync(candidate);\n if (read === undefined) {\n continue;\n }\n // The winner is the winner whether or not it opens. Continuing the walk on\n // a failed decrypt would serve a lower scope's plaintext twin instead —\n // the scope widening the cascade exists to prevent, arrived at by treating\n // \"I cannot read this\" as \"this is not here\".\n const opened = openValue(candidate, read, keys);\n if (opened.kind === \"failed\") {\n throw new UndecryptableValueError(\n parameterId(ref),\n target,\n formatValueFile(candidate),\n opened.failure,\n );\n }\n values.push({ ref, value: opened.value });\n break;\n }\n }\n\n return { config, environment: target, values };\n}\n"],"mappings":";AA+BA,SAAS,SAAS,WAAW,mBAAmB;AAEhD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,gCAAgC;AAwBzC,SAAS,SAAS,OAA6C;AAC7D,QAAM,OAAO,oBAAI,IAA0B;AAC3C,aAAW,QAAQ,OAAO;AACxB,UAAM,MAAoB,EAAE,WAAW,KAAK,WAAW,MAAM,KAAK,KAAK;AACvE,UAAM,KAAK,YAAY,GAAG;AAC1B,QAAI,CAAC,KAAK,IAAI,EAAE,GAAG;AACjB,WAAK,IAAI,IAAI,GAAG;AAAA,IAClB;AAAA,EACF;AACA,SAAO,CAAC,GAAG,KAAK,OAAO,CAAC,EAAE,KAAK,CAAC,GAAG,MAAM;AACvC,UAAM,OAAO,YAAY,CAAC;AAC1B,UAAM,QAAQ,YAAY,CAAC;AAC3B,WAAO,OAAO,QAAQ,KAAK,OAAO,QAAQ,IAAI;AAAA,EAChD,CAAC;AACH;AAMO,SAAS,YAAY,KAAa,aAAsC;AAC7E,QAAM,EAAE,QAAQ,KAAK,IAAI,WAAW,GAAG;AACvC,QAAM,SAAS,mBAAmB,QAAQ,WAAW;AAErD,QAAM,WAAW,yBAAyB;AAAA,IACxC,MAAM,YAAY,QAAQ,IAAI,GAAG,OAAO;AAAA,IACxC;AAAA,EACF,CAAC;AAED,QAAM,OAAO,iBAAiB,QAAQ,MAAM;AAE5C,QAAM,SAA0B,CAAC;AACjC,aAAW,OAAO,SAAS,SAAS,SAAS,CAAC,GAAG;AAC/C,eAAW,aAAa,cAAc,KAAK,MAAM,GAAG;AAClD,YAAM,OAAO,SAAS,SAAS,SAAS;AACxC,UAAI,SAAS,QAAW;AACtB;AAAA,MACF;AAKA,YAAM,SAAS,UAAU,WAAW,MAAM,IAAI;AAC9C,UAAI,OAAO,SAAS,UAAU;AAC5B,cAAM,IAAI;AAAA,UACR,YAAY,GAAG;AAAA,UACf;AAAA,UACA,gBAAgB,SAAS;AAAA,UACzB,OAAO;AAAA,QACT;AAAA,MACF;AACA,aAAO,KAAK,EAAE,KAAK,OAAO,OAAO,MAAM,CAAC;AACxC;AAAA,IACF;AAAA,EACF;AAEA,SAAO,EAAE,QAAQ,aAAa,QAAQ,OAAO;AAC/C;","names":[]}
|
package/dist/config.cjs
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// src/config.ts
|
|
4
|
+
var import_core2 = require("@penvhq/core");
|
|
5
|
+
|
|
6
|
+
// src/resolve.ts
|
|
7
|
+
var import_node_path = require("path");
|
|
8
|
+
var import_core = require("@penvhq/core");
|
|
9
|
+
var import_provider_filesystem = require("@penvhq/provider-filesystem");
|
|
10
|
+
function refsFrom(files) {
|
|
11
|
+
const refs = /* @__PURE__ */ new Map();
|
|
12
|
+
for (const file of files) {
|
|
13
|
+
const ref = { namespace: file.namespace, name: file.name };
|
|
14
|
+
const id = (0, import_core.parameterId)(ref);
|
|
15
|
+
if (!refs.has(id)) {
|
|
16
|
+
refs.set(id, ref);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return [...refs.values()].sort((a, b) => {
|
|
20
|
+
const left = (0, import_core.parameterId)(a);
|
|
21
|
+
const right = (0, import_core.parameterId)(b);
|
|
22
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function resolveSync(cwd, environment) {
|
|
26
|
+
const { config: config2, file } = (0, import_core.loadConfig)(cwd);
|
|
27
|
+
const target = (0, import_core.resolveEnvironment)(config2, environment);
|
|
28
|
+
const provider = (0, import_provider_filesystem.createFilesystemProvider)({
|
|
29
|
+
root: (0, import_node_path.resolve)((0, import_node_path.dirname)(file), ".penv"),
|
|
30
|
+
config: config2
|
|
31
|
+
});
|
|
32
|
+
const keys = (0, import_core.resolveKeySource)(config2, target);
|
|
33
|
+
const values2 = [];
|
|
34
|
+
for (const ref of refsFrom(provider.listSync())) {
|
|
35
|
+
for (const candidate of (0, import_core.candidatesFor)(ref, target)) {
|
|
36
|
+
const read = provider.readSync(candidate);
|
|
37
|
+
if (read === void 0) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const opened = (0, import_core.openValue)(candidate, read, keys);
|
|
41
|
+
if (opened.kind === "failed") {
|
|
42
|
+
throw new import_core.UndecryptableValueError(
|
|
43
|
+
(0, import_core.parameterId)(ref),
|
|
44
|
+
target,
|
|
45
|
+
(0, import_core.formatValueFile)(candidate),
|
|
46
|
+
opened.failure
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
values2.push({ ref, value: opened.value });
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { config: config2, environment: target, values: values2 };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/config.ts
|
|
57
|
+
var { config, values } = resolveSync(process.cwd());
|
|
58
|
+
var collision = (0, import_core2.checkNameCollisions)(
|
|
59
|
+
values.map(({ ref }) => ref),
|
|
60
|
+
config
|
|
61
|
+
)[0];
|
|
62
|
+
if (collision !== void 0) {
|
|
63
|
+
throw collision;
|
|
64
|
+
}
|
|
65
|
+
for (const { ref, value } of values) {
|
|
66
|
+
const variable = (0, import_core2.variableName)(ref, config);
|
|
67
|
+
if (process.env[variable] === void 0) {
|
|
68
|
+
process.env[variable] = value;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=config.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/config.ts","../src/resolve.ts"],"sourcesContent":["/**\n * The `import \"@penvhq/penv/config\"` compatibility entry: populates `process.env`,\n * dotenv-shaped, so penv can be adopted without changing existing code.\n *\n * ESM ordering caveat: this module must run before any module that reads\n * `process.env`. ES imports are hoisted and evaluated before the importing\n * module's body, but sibling imports evaluate in source order — so a module\n * imported above this one, and reading `process.env` at its top level, reads it\n * before penv has populated it. This is the same hazard `dotenv/config` carries.\n * The typed `import { env } from \"@env\"` surface has no ordering hazard and is\n * the recommended path.\n */\n\nimport { checkNameCollisions, variableName } from \"@penvhq/core\";\nimport { resolveSync } from \"./resolve.js\";\n\nconst { config, values } = resolveSync(process.cwd());\n\n// Invariant 12, enforced where the loss would happen: two parameters mapping to\n// one variable would otherwise resolve first-write-wins, dropping the second\n// silently. That is the same sin as last-write-wins, so throw before touching\n// process.env — a half-populated environment is worse than none.\nconst collision = checkNameCollisions(\n values.map(({ ref }) => ref),\n config,\n)[0];\nif (collision !== undefined) {\n throw collision;\n}\n\nfor (const { ref, value } of values) {\n const variable = variableName(ref, config);\n // dotenv semantics: an already-set variable is the caller's deliberate\n // override and is never clobbered.\n if (process.env[variable] === undefined) {\n process.env[variable] = value;\n }\n}\n","/**\n * The synchronous half of the value cascade, shared by `load` and the\n * `penv/config` compatibility entry.\n *\n * `resolveParameter` in `@penvhq/core` is async because the provider contract is,\n * and `load` is synchronous — so this module walks the cascade against the\n * filesystem provider's synchronous reads instead. It does not restate the\n * precedence rule: `candidatesFor` owns the order and everything here only\n * walks the list it returns, so the two paths cannot drift apart.\n *\n * The runtime reads the local tree for every environment, whatever provider\n * that environment declares, and this is the design rather than a limitation.\n * A provider is where an environment's source of truth *lives*, not where the\n * runtime reads from: `penv pull` materialises the tree from the provider, and\n * the runtime then reads what is on disk. So `load` never inspects\n * `providers.*.type` — a Vault-backed environment resolves through exactly the\n * code path a filesystem-backed one does, which is what makes changing provider\n * a configuration change rather than an application rewrite.\n *\n * Decryption happens here, synchronously, and that is the same knife applied a\n * second time. Key *acquisition* is async and happens before the process starts:\n * a deploy unwraps the KMS-derived data key and exports it, exactly as it already\n * runs `penv pull` to materialise the tree. Key *use* is a synchronous pure\n * function over bytes. penv never calls a KMS in-process — a key is where an\n * environment's secret material *lives*, not what the runtime dials at boot, just\n * as a provider is a sync target and not a runtime source. So invariant 3 is not\n * traded against encryption: `load` stays generic and synchronous because the\n * network half of both problems was moved out of the process entirely, rather\n * than awaited inside it.\n */\n\nimport { dirname, resolve as resolvePath } from \"node:path\";\nimport type { ParameterRef, PenvConfig, ValueFile } from \"@penvhq/core\";\nimport {\n candidatesFor,\n formatValueFile,\n loadConfig,\n openValue,\n parameterId,\n resolveEnvironment,\n resolveKeySource,\n UndecryptableValueError,\n} from \"@penvhq/core\";\nimport { createFilesystemProvider } from \"@penvhq/provider-filesystem\";\n\n/** One parameter that resolved to a present value for the target environment. */\nexport interface ResolvedValue {\n readonly ref: ParameterRef;\n readonly value: string;\n}\n\nexport interface ResolvedConfig {\n readonly config: PenvConfig;\n readonly environment: string;\n /**\n * Only parameters with a present candidate. A parameter that resolved to\n * nothing is absent rather than `undefined`, so requiredness stays the\n * schema's call.\n */\n readonly values: readonly ResolvedValue[];\n}\n\n/**\n * The parameters behind a set of value files, scopes collapsed —\n * `redis/password.production` and `redis/password` are one parameter. Ordered\n * so a `process.env` population is identical on every machine.\n */\nfunction refsFrom(files: readonly ValueFile[]): ParameterRef[] {\n const refs = new Map<string, ParameterRef>();\n for (const file of files) {\n const ref: ParameterRef = { namespace: file.namespace, name: file.name };\n const id = parameterId(ref);\n if (!refs.has(id)) {\n refs.set(id, ref);\n }\n }\n return [...refs.values()].sort((a, b) => {\n const left = parameterId(a);\n const right = parameterId(b);\n return left < right ? -1 : left > right ? 1 : 0;\n });\n}\n\n/**\n * Loads the config, settles the environment, and resolves every parameter the\n * local tree holds — the work `load` and `penv/config` both start from.\n */\nexport function resolveSync(cwd: string, environment?: string): ResolvedConfig {\n const { config, file } = loadConfig(cwd);\n const target = resolveEnvironment(config, environment);\n\n const provider = createFilesystemProvider({\n root: resolvePath(dirname(file), \".penv\"),\n config,\n });\n\n const keys = resolveKeySource(config, target);\n\n const values: ResolvedValue[] = [];\n for (const ref of refsFrom(provider.listSync())) {\n for (const candidate of candidatesFor(ref, target)) {\n const read = provider.readSync(candidate);\n if (read === undefined) {\n continue;\n }\n // The winner is the winner whether or not it opens. Continuing the walk on\n // a failed decrypt would serve a lower scope's plaintext twin instead —\n // the scope widening the cascade exists to prevent, arrived at by treating\n // \"I cannot read this\" as \"this is not here\".\n const opened = openValue(candidate, read, keys);\n if (opened.kind === \"failed\") {\n throw new UndecryptableValueError(\n parameterId(ref),\n target,\n formatValueFile(candidate),\n opened.failure,\n );\n }\n values.push({ ref, value: opened.value });\n break;\n }\n }\n\n return { config, environment: target, values };\n}\n"],"mappings":";;;AAaA,IAAAA,eAAkD;;;ACkBlD,uBAAgD;AAEhD,kBASO;AACP,iCAAyC;AAwBzC,SAAS,SAAS,OAA6C;AAC7D,QAAM,OAAO,oBAAI,IAA0B;AAC3C,aAAW,QAAQ,OAAO;AACxB,UAAM,MAAoB,EAAE,WAAW,KAAK,WAAW,MAAM,KAAK,KAAK;AACvE,UAAM,SAAK,yBAAY,GAAG;AAC1B,QAAI,CAAC,KAAK,IAAI,EAAE,GAAG;AACjB,WAAK,IAAI,IAAI,GAAG;AAAA,IAClB;AAAA,EACF;AACA,SAAO,CAAC,GAAG,KAAK,OAAO,CAAC,EAAE,KAAK,CAAC,GAAG,MAAM;AACvC,UAAM,WAAO,yBAAY,CAAC;AAC1B,UAAM,YAAQ,yBAAY,CAAC;AAC3B,WAAO,OAAO,QAAQ,KAAK,OAAO,QAAQ,IAAI;AAAA,EAChD,CAAC;AACH;AAMO,SAAS,YAAY,KAAa,aAAsC;AAC7E,QAAM,EAAE,QAAAC,SAAQ,KAAK,QAAI,wBAAW,GAAG;AACvC,QAAM,aAAS,gCAAmBA,SAAQ,WAAW;AAErD,QAAM,eAAW,qDAAyB;AAAA,IACxC,UAAM,iBAAAC,aAAY,0BAAQ,IAAI,GAAG,OAAO;AAAA,IACxC,QAAAD;AAAA,EACF,CAAC;AAED,QAAM,WAAO,8BAAiBA,SAAQ,MAAM;AAE5C,QAAME,UAA0B,CAAC;AACjC,aAAW,OAAO,SAAS,SAAS,SAAS,CAAC,GAAG;AAC/C,eAAW,iBAAa,2BAAc,KAAK,MAAM,GAAG;AAClD,YAAM,OAAO,SAAS,SAAS,SAAS;AACxC,UAAI,SAAS,QAAW;AACtB;AAAA,MACF;AAKA,YAAM,aAAS,uBAAU,WAAW,MAAM,IAAI;AAC9C,UAAI,OAAO,SAAS,UAAU;AAC5B,cAAM,IAAI;AAAA,cACR,yBAAY,GAAG;AAAA,UACf;AAAA,cACA,6BAAgB,SAAS;AAAA,UACzB,OAAO;AAAA,QACT;AAAA,MACF;AACA,MAAAA,QAAO,KAAK,EAAE,KAAK,OAAO,OAAO,MAAM,CAAC;AACxC;AAAA,IACF;AAAA,EACF;AAEA,SAAO,EAAE,QAAAF,SAAQ,aAAa,QAAQ,QAAAE,QAAO;AAC/C;;;AD5GA,IAAM,EAAE,QAAQ,OAAO,IAAI,YAAY,QAAQ,IAAI,CAAC;AAMpD,IAAM,gBAAY;AAAA,EAChB,OAAO,IAAI,CAAC,EAAE,IAAI,MAAM,GAAG;AAAA,EAC3B;AACF,EAAE,CAAC;AACH,IAAI,cAAc,QAAW;AAC3B,QAAM;AACR;AAEA,WAAW,EAAE,KAAK,MAAM,KAAK,QAAQ;AACnC,QAAM,eAAW,2BAAa,KAAK,MAAM;AAGzC,MAAI,QAAQ,IAAI,QAAQ,MAAM,QAAW;AACvC,YAAQ,IAAI,QAAQ,IAAI;AAAA,EAC1B;AACF;","names":["import_core","config","resolvePath","values"]}
|
package/dist/config.d.ts
ADDED
package/dist/config.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveSync
|
|
3
|
+
} from "./chunk-57AVY2K5.js";
|
|
4
|
+
|
|
5
|
+
// src/config.ts
|
|
6
|
+
import { checkNameCollisions, variableName } from "@penvhq/core";
|
|
7
|
+
var { config, values } = resolveSync(process.cwd());
|
|
8
|
+
var collision = checkNameCollisions(
|
|
9
|
+
values.map(({ ref }) => ref),
|
|
10
|
+
config
|
|
11
|
+
)[0];
|
|
12
|
+
if (collision !== void 0) {
|
|
13
|
+
throw collision;
|
|
14
|
+
}
|
|
15
|
+
for (const { ref, value } of values) {
|
|
16
|
+
const variable = variableName(ref, config);
|
|
17
|
+
if (process.env[variable] === void 0) {
|
|
18
|
+
process.env[variable] = value;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/config.ts"],"sourcesContent":["/**\n * The `import \"@penvhq/penv/config\"` compatibility entry: populates `process.env`,\n * dotenv-shaped, so penv can be adopted without changing existing code.\n *\n * ESM ordering caveat: this module must run before any module that reads\n * `process.env`. ES imports are hoisted and evaluated before the importing\n * module's body, but sibling imports evaluate in source order — so a module\n * imported above this one, and reading `process.env` at its top level, reads it\n * before penv has populated it. This is the same hazard `dotenv/config` carries.\n * The typed `import { env } from \"@env\"` surface has no ordering hazard and is\n * the recommended path.\n */\n\nimport { checkNameCollisions, variableName } from \"@penvhq/core\";\nimport { resolveSync } from \"./resolve.js\";\n\nconst { config, values } = resolveSync(process.cwd());\n\n// Invariant 12, enforced where the loss would happen: two parameters mapping to\n// one variable would otherwise resolve first-write-wins, dropping the second\n// silently. That is the same sin as last-write-wins, so throw before touching\n// process.env — a half-populated environment is worse than none.\nconst collision = checkNameCollisions(\n values.map(({ ref }) => ref),\n config,\n)[0];\nif (collision !== undefined) {\n throw collision;\n}\n\nfor (const { ref, value } of values) {\n const variable = variableName(ref, config);\n // dotenv semantics: an already-set variable is the caller's deliberate\n // override and is never clobbered.\n if (process.env[variable] === undefined) {\n process.env[variable] = value;\n }\n}\n"],"mappings":";;;;;AAaA,SAAS,qBAAqB,oBAAoB;AAGlD,IAAM,EAAE,QAAQ,OAAO,IAAI,YAAY,QAAQ,IAAI,CAAC;AAMpD,IAAM,YAAY;AAAA,EAChB,OAAO,IAAI,CAAC,EAAE,IAAI,MAAM,GAAG;AAAA,EAC3B;AACF,EAAE,CAAC;AACH,IAAI,cAAc,QAAW;AAC3B,QAAM;AACR;AAEA,WAAW,EAAE,KAAK,MAAM,KAAK,QAAQ;AACnC,QAAM,WAAW,aAAa,KAAK,MAAM;AAGzC,MAAI,QAAQ,IAAI,QAAQ,MAAM,QAAW;AACvC,YAAQ,IAAI,QAAQ,IAAI;AAAA,EAC1B;AACF;","names":[]}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
ConfigError: () => import_core3.ConfigError,
|
|
24
|
+
FilenameGrammarError: () => import_core3.FilenameGrammarError,
|
|
25
|
+
MissingParameterError: () => import_core3.MissingParameterError,
|
|
26
|
+
NameCollisionError: () => import_core3.NameCollisionError,
|
|
27
|
+
PenvError: () => import_core3.PenvError,
|
|
28
|
+
ReservedTokenError: () => import_core3.ReservedTokenError,
|
|
29
|
+
UnknownEnvironmentError: () => import_core3.UnknownEnvironmentError,
|
|
30
|
+
ValidationError: () => import_core3.ValidationError,
|
|
31
|
+
defineConfig: () => import_core3.defineConfig,
|
|
32
|
+
load: () => load
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(index_exports);
|
|
35
|
+
var import_core3 = require("@penvhq/core");
|
|
36
|
+
|
|
37
|
+
// src/load.ts
|
|
38
|
+
var import_core2 = require("@penvhq/core");
|
|
39
|
+
|
|
40
|
+
// src/resolve.ts
|
|
41
|
+
var import_node_path = require("path");
|
|
42
|
+
var import_core = require("@penvhq/core");
|
|
43
|
+
var import_provider_filesystem = require("@penvhq/provider-filesystem");
|
|
44
|
+
function refsFrom(files) {
|
|
45
|
+
const refs = /* @__PURE__ */ new Map();
|
|
46
|
+
for (const file of files) {
|
|
47
|
+
const ref = { namespace: file.namespace, name: file.name };
|
|
48
|
+
const id = (0, import_core.parameterId)(ref);
|
|
49
|
+
if (!refs.has(id)) {
|
|
50
|
+
refs.set(id, ref);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return [...refs.values()].sort((a, b) => {
|
|
54
|
+
const left = (0, import_core.parameterId)(a);
|
|
55
|
+
const right = (0, import_core.parameterId)(b);
|
|
56
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function resolveSync(cwd, environment) {
|
|
60
|
+
const { config, file } = (0, import_core.loadConfig)(cwd);
|
|
61
|
+
const target = (0, import_core.resolveEnvironment)(config, environment);
|
|
62
|
+
const provider = (0, import_provider_filesystem.createFilesystemProvider)({
|
|
63
|
+
root: (0, import_node_path.resolve)((0, import_node_path.dirname)(file), ".penv"),
|
|
64
|
+
config
|
|
65
|
+
});
|
|
66
|
+
const keys = (0, import_core.resolveKeySource)(config, target);
|
|
67
|
+
const values = [];
|
|
68
|
+
for (const ref of refsFrom(provider.listSync())) {
|
|
69
|
+
for (const candidate of (0, import_core.candidatesFor)(ref, target)) {
|
|
70
|
+
const read = provider.readSync(candidate);
|
|
71
|
+
if (read === void 0) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const opened = (0, import_core.openValue)(candidate, read, keys);
|
|
75
|
+
if (opened.kind === "failed") {
|
|
76
|
+
throw new import_core.UndecryptableValueError(
|
|
77
|
+
(0, import_core.parameterId)(ref),
|
|
78
|
+
target,
|
|
79
|
+
(0, import_core.formatValueFile)(candidate),
|
|
80
|
+
opened.failure
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
values.push({ ref, value: opened.value });
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { config, environment: target, values };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/load.ts
|
|
91
|
+
function place(root, path, value) {
|
|
92
|
+
const leaf = path[path.length - 1];
|
|
93
|
+
if (leaf === void 0) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
let node = root;
|
|
97
|
+
for (const key of path.slice(0, -1)) {
|
|
98
|
+
const existing = node[key];
|
|
99
|
+
if (typeof existing === "object" && existing !== null) {
|
|
100
|
+
node = existing;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const child = {};
|
|
104
|
+
node[key] = child;
|
|
105
|
+
node = child;
|
|
106
|
+
}
|
|
107
|
+
node[leaf] = value;
|
|
108
|
+
}
|
|
109
|
+
function load(schema, options) {
|
|
110
|
+
const { environment, values } = resolveSync(options?.cwd ?? process.cwd(), options?.environment);
|
|
111
|
+
const object = {};
|
|
112
|
+
for (const { ref, value } of values) {
|
|
113
|
+
place(object, (0, import_core2.accessPath)(ref), value);
|
|
114
|
+
}
|
|
115
|
+
const result = schema.safeParse(object);
|
|
116
|
+
if (!result.success) {
|
|
117
|
+
throw new import_core2.ValidationError(
|
|
118
|
+
environment,
|
|
119
|
+
result.error.issues.map((issue) => ({
|
|
120
|
+
parameter: issue.path.join("."),
|
|
121
|
+
message: issue.message
|
|
122
|
+
}))
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return result.data;
|
|
126
|
+
}
|
|
127
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
128
|
+
0 && (module.exports = {
|
|
129
|
+
ConfigError,
|
|
130
|
+
FilenameGrammarError,
|
|
131
|
+
MissingParameterError,
|
|
132
|
+
NameCollisionError,
|
|
133
|
+
PenvError,
|
|
134
|
+
ReservedTokenError,
|
|
135
|
+
UnknownEnvironmentError,
|
|
136
|
+
ValidationError,
|
|
137
|
+
defineConfig,
|
|
138
|
+
load
|
|
139
|
+
});
|
|
140
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/load.ts","../src/resolve.ts"],"sourcesContent":["/**\n * The public surface of `@penvhq/runtime` — what `penv` re-exports.\n *\n * `defineConfig` is re-exported from `@penvhq/core` because the docs show\n * `penv.config.ts` importing it from `penv`: one import specifier for the whole\n * tool, whatever package a symbol happens to live in.\n */\n\nexport {\n ConfigError,\n defineConfig,\n FilenameGrammarError,\n MissingParameterError,\n NameCollisionError,\n PenvError,\n ReservedTokenError,\n UnknownEnvironmentError,\n ValidationError,\n} from \"@penvhq/core\";\nexport type { LoadOptions } from \"./load.js\";\nexport { load } from \"./load.js\";\n","/**\n * The runtime loader. `load` is generic and returns `z.infer<T>` — the type the\n * caller's own schema describes, never a widened one. The inferred type is only\n * true because the same schema validates the values before they are returned,\n * so the type you code against and the value you receive cannot diverge.\n */\n\nimport { accessPath, ValidationError } from \"@penvhq/core\";\nimport type { z } from \"zod\";\nimport { resolveSync } from \"./resolve.js\";\n\nexport interface LoadOptions {\n /** Where to start looking for `penv.config.ts`. Defaults to `process.cwd()`. */\n readonly cwd?: string;\n /** Overrides `PENV_ENV` / `NODE_ENV`. Must be a declared environment. */\n readonly environment?: string;\n}\n\n/**\n * Places a value at its access path, creating namespaces on the way.\n * Values are placed exactly as the provider holds them: coercion is the\n * schema's job, so a value file's contents stay a string here.\n */\nfunction place(root: Record<string, unknown>, path: readonly string[], value: string): void {\n const leaf = path[path.length - 1];\n if (leaf === undefined) {\n return;\n }\n\n let node = root;\n for (const key of path.slice(0, -1)) {\n const existing = node[key];\n if (typeof existing === \"object\" && existing !== null) {\n node = existing as Record<string, unknown>;\n continue;\n }\n const child: Record<string, unknown> = {};\n node[key] = child;\n node = child;\n }\n node[leaf] = value;\n}\n\n/**\n * Loads, validates, and returns configuration for the current environment.\n * Eager and synchronous: invalid configuration fails at startup with a\n * parameter-named error rather than later at first use.\n */\nexport function load<T extends z.ZodType>(schema: T, options?: LoadOptions): z.infer<T> {\n const { environment, values } = resolveSync(options?.cwd ?? process.cwd(), options?.environment);\n\n const object: Record<string, unknown> = {};\n for (const { ref, value } of values) {\n place(object, accessPath(ref), value);\n }\n\n const result = schema.safeParse(object);\n if (!result.success) {\n throw new ValidationError(\n environment,\n result.error.issues.map((issue) => ({\n parameter: issue.path.join(\".\"),\n message: issue.message,\n })),\n );\n }\n return result.data;\n}\n","/**\n * The synchronous half of the value cascade, shared by `load` and the\n * `penv/config` compatibility entry.\n *\n * `resolveParameter` in `@penvhq/core` is async because the provider contract is,\n * and `load` is synchronous — so this module walks the cascade against the\n * filesystem provider's synchronous reads instead. It does not restate the\n * precedence rule: `candidatesFor` owns the order and everything here only\n * walks the list it returns, so the two paths cannot drift apart.\n *\n * The runtime reads the local tree for every environment, whatever provider\n * that environment declares, and this is the design rather than a limitation.\n * A provider is where an environment's source of truth *lives*, not where the\n * runtime reads from: `penv pull` materialises the tree from the provider, and\n * the runtime then reads what is on disk. So `load` never inspects\n * `providers.*.type` — a Vault-backed environment resolves through exactly the\n * code path a filesystem-backed one does, which is what makes changing provider\n * a configuration change rather than an application rewrite.\n *\n * Decryption happens here, synchronously, and that is the same knife applied a\n * second time. Key *acquisition* is async and happens before the process starts:\n * a deploy unwraps the KMS-derived data key and exports it, exactly as it already\n * runs `penv pull` to materialise the tree. Key *use* is a synchronous pure\n * function over bytes. penv never calls a KMS in-process — a key is where an\n * environment's secret material *lives*, not what the runtime dials at boot, just\n * as a provider is a sync target and not a runtime source. So invariant 3 is not\n * traded against encryption: `load` stays generic and synchronous because the\n * network half of both problems was moved out of the process entirely, rather\n * than awaited inside it.\n */\n\nimport { dirname, resolve as resolvePath } from \"node:path\";\nimport type { ParameterRef, PenvConfig, ValueFile } from \"@penvhq/core\";\nimport {\n candidatesFor,\n formatValueFile,\n loadConfig,\n openValue,\n parameterId,\n resolveEnvironment,\n resolveKeySource,\n UndecryptableValueError,\n} from \"@penvhq/core\";\nimport { createFilesystemProvider } from \"@penvhq/provider-filesystem\";\n\n/** One parameter that resolved to a present value for the target environment. */\nexport interface ResolvedValue {\n readonly ref: ParameterRef;\n readonly value: string;\n}\n\nexport interface ResolvedConfig {\n readonly config: PenvConfig;\n readonly environment: string;\n /**\n * Only parameters with a present candidate. A parameter that resolved to\n * nothing is absent rather than `undefined`, so requiredness stays the\n * schema's call.\n */\n readonly values: readonly ResolvedValue[];\n}\n\n/**\n * The parameters behind a set of value files, scopes collapsed —\n * `redis/password.production` and `redis/password` are one parameter. Ordered\n * so a `process.env` population is identical on every machine.\n */\nfunction refsFrom(files: readonly ValueFile[]): ParameterRef[] {\n const refs = new Map<string, ParameterRef>();\n for (const file of files) {\n const ref: ParameterRef = { namespace: file.namespace, name: file.name };\n const id = parameterId(ref);\n if (!refs.has(id)) {\n refs.set(id, ref);\n }\n }\n return [...refs.values()].sort((a, b) => {\n const left = parameterId(a);\n const right = parameterId(b);\n return left < right ? -1 : left > right ? 1 : 0;\n });\n}\n\n/**\n * Loads the config, settles the environment, and resolves every parameter the\n * local tree holds — the work `load` and `penv/config` both start from.\n */\nexport function resolveSync(cwd: string, environment?: string): ResolvedConfig {\n const { config, file } = loadConfig(cwd);\n const target = resolveEnvironment(config, environment);\n\n const provider = createFilesystemProvider({\n root: resolvePath(dirname(file), \".penv\"),\n config,\n });\n\n const keys = resolveKeySource(config, target);\n\n const values: ResolvedValue[] = [];\n for (const ref of refsFrom(provider.listSync())) {\n for (const candidate of candidatesFor(ref, target)) {\n const read = provider.readSync(candidate);\n if (read === undefined) {\n continue;\n }\n // The winner is the winner whether or not it opens. Continuing the walk on\n // a failed decrypt would serve a lower scope's plaintext twin instead —\n // the scope widening the cascade exists to prevent, arrived at by treating\n // \"I cannot read this\" as \"this is not here\".\n const opened = openValue(candidate, read, keys);\n if (opened.kind === \"failed\") {\n throw new UndecryptableValueError(\n parameterId(ref),\n target,\n formatValueFile(candidate),\n opened.failure,\n );\n }\n values.push({ ref, value: opened.value });\n break;\n }\n }\n\n return { config, environment: target, values };\n}\n"],"mappings":"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","names":["import_core","import_core","resolvePath"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export { ConfigError, FilenameGrammarError, MissingParameterError, NameCollisionError, PenvError, ReservedTokenError, UnknownEnvironmentError, ValidationError, defineConfig } from '@penvhq/core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The runtime loader. `load` is generic and returns `z.infer<T>` — the type the
|
|
6
|
+
* caller's own schema describes, never a widened one. The inferred type is only
|
|
7
|
+
* true because the same schema validates the values before they are returned,
|
|
8
|
+
* so the type you code against and the value you receive cannot diverge.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
interface LoadOptions {
|
|
12
|
+
/** Where to start looking for `penv.config.ts`. Defaults to `process.cwd()`. */
|
|
13
|
+
readonly cwd?: string;
|
|
14
|
+
/** Overrides `PENV_ENV` / `NODE_ENV`. Must be a declared environment. */
|
|
15
|
+
readonly environment?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Loads, validates, and returns configuration for the current environment.
|
|
19
|
+
* Eager and synchronous: invalid configuration fails at startup with a
|
|
20
|
+
* parameter-named error rather than later at first use.
|
|
21
|
+
*/
|
|
22
|
+
declare function load<T extends z.ZodType>(schema: T, options?: LoadOptions): z.infer<T>;
|
|
23
|
+
|
|
24
|
+
export { type LoadOptions, load };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export { ConfigError, FilenameGrammarError, MissingParameterError, NameCollisionError, PenvError, ReservedTokenError, UnknownEnvironmentError, ValidationError, defineConfig } from '@penvhq/core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The runtime loader. `load` is generic and returns `z.infer<T>` — the type the
|
|
6
|
+
* caller's own schema describes, never a widened one. The inferred type is only
|
|
7
|
+
* true because the same schema validates the values before they are returned,
|
|
8
|
+
* so the type you code against and the value you receive cannot diverge.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
interface LoadOptions {
|
|
12
|
+
/** Where to start looking for `penv.config.ts`. Defaults to `process.cwd()`. */
|
|
13
|
+
readonly cwd?: string;
|
|
14
|
+
/** Overrides `PENV_ENV` / `NODE_ENV`. Must be a declared environment. */
|
|
15
|
+
readonly environment?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Loads, validates, and returns configuration for the current environment.
|
|
19
|
+
* Eager and synchronous: invalid configuration fails at startup with a
|
|
20
|
+
* parameter-named error rather than later at first use.
|
|
21
|
+
*/
|
|
22
|
+
declare function load<T extends z.ZodType>(schema: T, options?: LoadOptions): z.infer<T>;
|
|
23
|
+
|
|
24
|
+
export { type LoadOptions, load };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveSync
|
|
3
|
+
} from "./chunk-57AVY2K5.js";
|
|
4
|
+
|
|
5
|
+
// src/index.ts
|
|
6
|
+
import {
|
|
7
|
+
ConfigError,
|
|
8
|
+
defineConfig,
|
|
9
|
+
FilenameGrammarError,
|
|
10
|
+
MissingParameterError,
|
|
11
|
+
NameCollisionError,
|
|
12
|
+
PenvError,
|
|
13
|
+
ReservedTokenError,
|
|
14
|
+
UnknownEnvironmentError,
|
|
15
|
+
ValidationError as ValidationError2
|
|
16
|
+
} from "@penvhq/core";
|
|
17
|
+
|
|
18
|
+
// src/load.ts
|
|
19
|
+
import { accessPath, ValidationError } from "@penvhq/core";
|
|
20
|
+
function place(root, path, value) {
|
|
21
|
+
const leaf = path[path.length - 1];
|
|
22
|
+
if (leaf === void 0) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
let node = root;
|
|
26
|
+
for (const key of path.slice(0, -1)) {
|
|
27
|
+
const existing = node[key];
|
|
28
|
+
if (typeof existing === "object" && existing !== null) {
|
|
29
|
+
node = existing;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const child = {};
|
|
33
|
+
node[key] = child;
|
|
34
|
+
node = child;
|
|
35
|
+
}
|
|
36
|
+
node[leaf] = value;
|
|
37
|
+
}
|
|
38
|
+
function load(schema, options) {
|
|
39
|
+
const { environment, values } = resolveSync(options?.cwd ?? process.cwd(), options?.environment);
|
|
40
|
+
const object = {};
|
|
41
|
+
for (const { ref, value } of values) {
|
|
42
|
+
place(object, accessPath(ref), value);
|
|
43
|
+
}
|
|
44
|
+
const result = schema.safeParse(object);
|
|
45
|
+
if (!result.success) {
|
|
46
|
+
throw new ValidationError(
|
|
47
|
+
environment,
|
|
48
|
+
result.error.issues.map((issue) => ({
|
|
49
|
+
parameter: issue.path.join("."),
|
|
50
|
+
message: issue.message
|
|
51
|
+
}))
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return result.data;
|
|
55
|
+
}
|
|
56
|
+
export {
|
|
57
|
+
ConfigError,
|
|
58
|
+
FilenameGrammarError,
|
|
59
|
+
MissingParameterError,
|
|
60
|
+
NameCollisionError,
|
|
61
|
+
PenvError,
|
|
62
|
+
ReservedTokenError,
|
|
63
|
+
UnknownEnvironmentError,
|
|
64
|
+
ValidationError2 as ValidationError,
|
|
65
|
+
defineConfig,
|
|
66
|
+
load
|
|
67
|
+
};
|
|
68
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/load.ts"],"sourcesContent":["/**\n * The public surface of `@penvhq/runtime` — what `penv` re-exports.\n *\n * `defineConfig` is re-exported from `@penvhq/core` because the docs show\n * `penv.config.ts` importing it from `penv`: one import specifier for the whole\n * tool, whatever package a symbol happens to live in.\n */\n\nexport {\n ConfigError,\n defineConfig,\n FilenameGrammarError,\n MissingParameterError,\n NameCollisionError,\n PenvError,\n ReservedTokenError,\n UnknownEnvironmentError,\n ValidationError,\n} from \"@penvhq/core\";\nexport type { LoadOptions } from \"./load.js\";\nexport { load } from \"./load.js\";\n","/**\n * The runtime loader. `load` is generic and returns `z.infer<T>` — the type the\n * caller's own schema describes, never a widened one. The inferred type is only\n * true because the same schema validates the values before they are returned,\n * so the type you code against and the value you receive cannot diverge.\n */\n\nimport { accessPath, ValidationError } from \"@penvhq/core\";\nimport type { z } from \"zod\";\nimport { resolveSync } from \"./resolve.js\";\n\nexport interface LoadOptions {\n /** Where to start looking for `penv.config.ts`. Defaults to `process.cwd()`. */\n readonly cwd?: string;\n /** Overrides `PENV_ENV` / `NODE_ENV`. Must be a declared environment. */\n readonly environment?: string;\n}\n\n/**\n * Places a value at its access path, creating namespaces on the way.\n * Values are placed exactly as the provider holds them: coercion is the\n * schema's job, so a value file's contents stay a string here.\n */\nfunction place(root: Record<string, unknown>, path: readonly string[], value: string): void {\n const leaf = path[path.length - 1];\n if (leaf === undefined) {\n return;\n }\n\n let node = root;\n for (const key of path.slice(0, -1)) {\n const existing = node[key];\n if (typeof existing === \"object\" && existing !== null) {\n node = existing as Record<string, unknown>;\n continue;\n }\n const child: Record<string, unknown> = {};\n node[key] = child;\n node = child;\n }\n node[leaf] = value;\n}\n\n/**\n * Loads, validates, and returns configuration for the current environment.\n * Eager and synchronous: invalid configuration fails at startup with a\n * parameter-named error rather than later at first use.\n */\nexport function load<T extends z.ZodType>(schema: T, options?: LoadOptions): z.infer<T> {\n const { environment, values } = resolveSync(options?.cwd ?? process.cwd(), options?.environment);\n\n const object: Record<string, unknown> = {};\n for (const { ref, value } of values) {\n place(object, accessPath(ref), value);\n }\n\n const result = schema.safeParse(object);\n if (!result.success) {\n throw new ValidationError(\n environment,\n result.error.issues.map((issue) => ({\n parameter: issue.path.join(\".\"),\n message: issue.message,\n })),\n );\n }\n return result.data;\n}\n"],"mappings":";;;;;AAQA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,mBAAAA;AAAA,OACK;;;ACXP,SAAS,YAAY,uBAAuB;AAgB5C,SAAS,MAAM,MAA+B,MAAyB,OAAqB;AAC1F,QAAM,OAAO,KAAK,KAAK,SAAS,CAAC;AACjC,MAAI,SAAS,QAAW;AACtB;AAAA,EACF;AAEA,MAAI,OAAO;AACX,aAAW,OAAO,KAAK,MAAM,GAAG,EAAE,GAAG;AACnC,UAAM,WAAW,KAAK,GAAG;AACzB,QAAI,OAAO,aAAa,YAAY,aAAa,MAAM;AACrD,aAAO;AACP;AAAA,IACF;AACA,UAAM,QAAiC,CAAC;AACxC,SAAK,GAAG,IAAI;AACZ,WAAO;AAAA,EACT;AACA,OAAK,IAAI,IAAI;AACf;AAOO,SAAS,KAA0B,QAAW,SAAmC;AACtF,QAAM,EAAE,aAAa,OAAO,IAAI,YAAY,SAAS,OAAO,QAAQ,IAAI,GAAG,SAAS,WAAW;AAE/F,QAAM,SAAkC,CAAC;AACzC,aAAW,EAAE,KAAK,MAAM,KAAK,QAAQ;AACnC,UAAM,QAAQ,WAAW,GAAG,GAAG,KAAK;AAAA,EACtC;AAEA,QAAM,SAAS,OAAO,UAAU,MAAM;AACtC,MAAI,CAAC,OAAO,SAAS;AACnB,UAAM,IAAI;AAAA,MACR;AAAA,MACA,OAAO,MAAM,OAAO,IAAI,CAAC,WAAW;AAAA,QAClC,WAAW,MAAM,KAAK,KAAK,GAAG;AAAA,QAC9B,SAAS,MAAM;AAAA,MACjB,EAAE;AAAA,IACJ;AAAA,EACF;AACA,SAAO,OAAO;AAChB;","names":["ValidationError"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@penvhq/runtime",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "penv's load() and the typed env surface. Ships into user apps — keep it light.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.cjs",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"require": "./dist/index.cjs"
|
|
15
|
+
},
|
|
16
|
+
"./config": {
|
|
17
|
+
"types": "./dist/config.d.ts",
|
|
18
|
+
"import": "./dist/config.js",
|
|
19
|
+
"require": "./dist/config.cjs"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@penvhq/core": "0.1.0",
|
|
27
|
+
"@penvhq/provider-filesystem": "0.1.0"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"zod": "^4.4.3"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"zod": "4.4.3"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsup"
|
|
37
|
+
}
|
|
38
|
+
}
|