@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 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":[]}
@@ -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"]}
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,2 @@
1
+
2
+ export { }
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"]}
@@ -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 };
@@ -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
+ }