@sebspark/environment 0.1.0 → 0.1.2
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/README.md +69 -1
- package/dist/index.d.mts +7 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +25 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,3 +1,71 @@
|
|
|
1
1
|
# `@sebspark/environment`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Utilities for reading environment variables and secrets in a type-safe, lazy way.
|
|
4
|
+
|
|
5
|
+
## `initEnvironment<T>()`
|
|
6
|
+
|
|
7
|
+
Returns a typed proxy that reads from `process.env` on each property access. Required variables are asserted at access time — an error is thrown if the value is missing.
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { initEnvironment } from '@sebspark/environment'
|
|
11
|
+
|
|
12
|
+
type Env = {
|
|
13
|
+
API_URL: string
|
|
14
|
+
PORT: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const env = initEnvironment<Env>()
|
|
18
|
+
|
|
19
|
+
env.API_URL // reads process.env.API_URL, throws if missing
|
|
20
|
+
env.optional.PORT // reads process.env.PORT, returns undefined if missing
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Values are cached after the first access.
|
|
24
|
+
|
|
25
|
+
## `initSecretStore<T>(opts?)`
|
|
26
|
+
|
|
27
|
+
Returns a typed proxy that reads secrets from the filesystem. Useful for Kubernetes-mounted secrets where each secret is a file named after the key.
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { initSecretStore } from '@sebspark/environment'
|
|
31
|
+
|
|
32
|
+
type Secrets = {
|
|
33
|
+
DB_PASSWORD: string
|
|
34
|
+
API_KEY: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const secrets = initSecretStore<Secrets>({
|
|
38
|
+
dir: '/var/secrets', // directory containing secret files (optional)
|
|
39
|
+
fallback: true, // fall back to process.env if file is missing (optional)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
secrets.DB_PASSWORD // reads /var/secrets/DB_PASSWORD
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Options:
|
|
46
|
+
|
|
47
|
+
| Option | Type | Description |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| `dir` | `string` | Directory to read secret files from. Defaults to CWD. |
|
|
50
|
+
| `fallback` | `boolean \| Partial<T>` | If `true`, falls back to `process.env`. If an object, uses its values as fallback. |
|
|
51
|
+
|
|
52
|
+
Values are cached after the first access. An error is thrown if the secret cannot be resolved.
|
|
53
|
+
|
|
54
|
+
## `mergeEnvironments(env, secrets)`
|
|
55
|
+
|
|
56
|
+
Merges the results of `initEnvironment` and `initSecretStore` into a single typed proxy. Secrets take precedence over environment variables for overlapping keys. Falls back to the environment proxy if a secret is not found.
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { initEnvironment, initSecretStore, mergeEnvironments } from '@sebspark/environment'
|
|
60
|
+
|
|
61
|
+
type Env = { API_URL: string; DB_PASSWORD: string }
|
|
62
|
+
type Secrets = { DB_PASSWORD: string }
|
|
63
|
+
|
|
64
|
+
const env = initEnvironment<Env>()
|
|
65
|
+
const secrets = initSecretStore<Secrets>({ dir: '/var/secrets', fallback: true })
|
|
66
|
+
|
|
67
|
+
export const Environment = mergeEnvironments(env, secrets)
|
|
68
|
+
|
|
69
|
+
Environment.API_URL // from process.env
|
|
70
|
+
Environment.DB_PASSWORD // from /var/secrets/DB_PASSWORD, falls back to process.env
|
|
71
|
+
```
|
package/dist/index.d.mts
CHANGED
|
@@ -4,9 +4,14 @@ declare const initEnvironment: <T extends Record<string, string>>() => T & {
|
|
|
4
4
|
} & {
|
|
5
5
|
[key: string]: string;
|
|
6
6
|
};
|
|
7
|
-
declare const
|
|
7
|
+
declare const mergeEnvironments: <T extends Record<string, string>, U extends Record<string, string>>(env: T, secrets: U) => T & U;
|
|
8
|
+
type SecretStoreOptions<T> = {
|
|
9
|
+
dir?: string;
|
|
10
|
+
fallback?: boolean | Partial<T>;
|
|
11
|
+
};
|
|
12
|
+
declare const initSecretStore: <T extends Record<string, string>>(opts?: SecretStoreOptions<T>) => T & {
|
|
8
13
|
[key: string]: string;
|
|
9
14
|
};
|
|
10
15
|
//#endregion
|
|
11
|
-
export { initEnvironment, initSecretStore };
|
|
16
|
+
export { initEnvironment, initSecretStore, mergeEnvironments };
|
|
12
17
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";cAIa,eAAA,aAA6B,MAAA,uBAiCnC,CAAA;EAAM,QAAA,EAAU,OAAA,CAAQ,CAAA;AAAA;EAAA,CAAU,GAAA;AAAA;AAAA,cAG5B,iBAAA,aACD,MAAA,4BACA,MAAA,kBAEV,GAAA,EAAK,CAAA,EACL,OAAA,EAAS,CAAA,KACR,CAAA,GAAI,CAAA;AAAA,KAYF,kBAAA;EACH,GAAA;EACA,QAAA,aAAqB,OAAA,CAAQ,CAAA;AAAA;AAAA,cAGlB,eAAA,aAA6B,MAAA,kBACxC,IAAA,GAAO,kBAAA,CAAmB,CAAA,MAwCrB,CAAA;EAAA,CAAO,GAAA;AAAA"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
3
4
|
//#region src/index.ts
|
|
4
5
|
const initEnvironment = () => {
|
|
5
6
|
const environmentCache = {};
|
|
@@ -19,20 +20,39 @@ const initEnvironment = () => {
|
|
|
19
20
|
return environmentCache[key];
|
|
20
21
|
} });
|
|
21
22
|
};
|
|
22
|
-
const
|
|
23
|
+
const mergeEnvironments = (env, secrets) => new Proxy({}, { get(_, key) {
|
|
24
|
+
if (typeof key !== "string") return void 0;
|
|
25
|
+
try {
|
|
26
|
+
return secrets[key];
|
|
27
|
+
} catch {
|
|
28
|
+
return env[key];
|
|
29
|
+
}
|
|
30
|
+
} });
|
|
31
|
+
const initSecretStore = (opts) => {
|
|
23
32
|
const secretCache = {};
|
|
33
|
+
const { dir, fallback } = opts ?? {};
|
|
24
34
|
return new Proxy({}, { get(_, k) {
|
|
25
35
|
const key = k;
|
|
26
36
|
if (key in secretCache) return secretCache[key];
|
|
37
|
+
const filePath = dir ? join(dir, key) : key;
|
|
27
38
|
try {
|
|
28
|
-
secretCache[key] = readFileSync(
|
|
39
|
+
secretCache[key] = readFileSync(filePath, "utf-8").trim();
|
|
40
|
+
return secretCache[key];
|
|
41
|
+
} catch {}
|
|
42
|
+
if (fallback === true) {
|
|
43
|
+
const { [key]: value } = process.env;
|
|
44
|
+
if (value !== void 0) {
|
|
45
|
+
secretCache[key] = value;
|
|
46
|
+
return secretCache[key];
|
|
47
|
+
}
|
|
48
|
+
} else if (fallback && key in fallback) {
|
|
49
|
+
secretCache[key] = fallback[key];
|
|
29
50
|
return secretCache[key];
|
|
30
|
-
} catch (err) {
|
|
31
|
-
throw new Error(`Secret ${key} is required`);
|
|
32
51
|
}
|
|
52
|
+
throw new Error(`Secret ${key} is required`);
|
|
33
53
|
} });
|
|
34
54
|
};
|
|
35
55
|
//#endregion
|
|
36
|
-
export { initEnvironment, initSecretStore };
|
|
56
|
+
export { initEnvironment, initSecretStore, mergeEnvironments };
|
|
37
57
|
|
|
38
58
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import assert from 'node:assert'\nimport { readFileSync } from 'node:fs'\n\nexport const initEnvironment = <T extends Record<string, string>>() => {\n const environmentCache: Record<string, string | undefined> = {}\n\n return new Proxy(\n {},\n {\n get(_, k: keyof T | 'optional' | string | symbol) {\n const key = k as string\n if (key === 'optional') {\n return new Proxy(\n {},\n {\n get(_, k: keyof T | string | symbol) {\n const key = k as string\n if (key in environmentCache) {\n return environmentCache[key]\n }\n const { [key]: value } = process.env\n environmentCache[key] = value\n return environmentCache[key]\n },\n }\n ) as T\n }\n if (key in environmentCache) {\n return environmentCache[key]\n }\n const { [key]: value } = process.env\n assert(value, `${key} is required`)\n environmentCache[key] = value\n return environmentCache[key]\n },\n }\n ) as T & { optional: Partial<T> } & { [key: string]: string }\n}\n\nexport const initSecretStore = <T extends Record<string, string>>() => {\n const secretCache: Record<string, string | undefined> = {}\n\n return new Proxy(\n {},\n {\n get(_, k: keyof T | string | symbol) {\n const key = k as string\n if (key in secretCache) {\n return secretCache[key]\n }\n try {\n const value = readFileSync(
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import assert from 'node:assert'\nimport { readFileSync } from 'node:fs'\nimport { join } from 'node:path'\n\nexport const initEnvironment = <T extends Record<string, string>>() => {\n const environmentCache: Record<string, string | undefined> = {}\n\n return new Proxy(\n {},\n {\n get(_, k: keyof T | 'optional' | string | symbol) {\n const key = k as string\n if (key === 'optional') {\n return new Proxy(\n {},\n {\n get(_, k: keyof T | string | symbol) {\n const key = k as string\n if (key in environmentCache) {\n return environmentCache[key]\n }\n const { [key]: value } = process.env\n environmentCache[key] = value\n return environmentCache[key]\n },\n }\n ) as T\n }\n if (key in environmentCache) {\n return environmentCache[key]\n }\n const { [key]: value } = process.env\n assert(value, `${key} is required`)\n environmentCache[key] = value\n return environmentCache[key]\n },\n }\n ) as T & { optional: Partial<T> } & { [key: string]: string }\n}\n\nexport const mergeEnvironments = <\n T extends Record<string, string>,\n U extends Record<string, string>,\n>(\n env: T,\n secrets: U\n): T & U =>\n new Proxy({} as T & U, {\n get(_, key) {\n if (typeof key !== 'string') return undefined\n try {\n return (secrets as Record<string, string>)[key]\n } catch {\n return (env as Record<string, string>)[key]\n }\n },\n })\n\ntype SecretStoreOptions<T> = {\n dir?: string\n fallback?: boolean | Partial<T>\n}\n\nexport const initSecretStore = <T extends Record<string, string>>(\n opts?: SecretStoreOptions<T>\n) => {\n const secretCache: Record<string, string | undefined> = {}\n const { dir, fallback } = opts ?? {}\n\n return new Proxy(\n {},\n {\n get(_, k: keyof T | string | symbol) {\n const key = k as string\n if (key in secretCache) {\n return secretCache[key]\n }\n\n // 1. Try reading from file (dir/KEY or just KEY if no dir)\n const filePath = dir ? join(dir, key) : key\n try {\n const value = readFileSync(filePath, 'utf-8').trim()\n secretCache[key] = value\n return secretCache[key]\n } catch {\n // file not found — continue to fallback\n }\n\n // 2. Try fallback\n if (fallback === true) {\n // auto-read from process.env using the same key name\n const { [key]: value } = process.env\n if (value !== undefined) {\n secretCache[key] = value\n return secretCache[key]\n }\n } else if (fallback && key in fallback) {\n secretCache[key] = fallback[key as keyof T] as string\n return secretCache[key]\n }\n\n throw new Error(`Secret ${key} is required`)\n },\n }\n ) as T & { [key: string]: string }\n}\n"],"mappings":";;;;AAIA,MAAa,wBAA0D;CACrE,MAAM,mBAAuD,EAAE;CAE/D,OAAO,IAAI,MACT,EAAE,EACF,EACE,IAAI,GAAG,GAA2C;EAChD,MAAM,MAAM;EACZ,IAAI,QAAQ,YACV,OAAO,IAAI,MACT,EAAE,EACF,EACE,IAAI,GAAG,GAA8B;GACnC,MAAM,MAAM;GACZ,IAAI,OAAO,kBACT,OAAO,iBAAiB;GAE1B,MAAM,GAAG,MAAM,UAAU,QAAQ;GACjC,iBAAiB,OAAO;GACxB,OAAO,iBAAiB;KAE3B,CACF;EAEH,IAAI,OAAO,kBACT,OAAO,iBAAiB;EAE1B,MAAM,GAAG,MAAM,UAAU,QAAQ;EACjC,OAAO,OAAO,GAAG,IAAI,cAAc;EACnC,iBAAiB,OAAO;EACxB,OAAO,iBAAiB;IAE3B,CACF;;AAGH,MAAa,qBAIX,KACA,YAEA,IAAI,MAAM,EAAE,EAAW,EACrB,IAAI,GAAG,KAAK;CACV,IAAI,OAAO,QAAQ,UAAU,OAAO,KAAA;CACpC,IAAI;EACF,OAAQ,QAAmC;SACrC;EACN,OAAQ,IAA+B;;GAG5C,CAAC;AAOJ,MAAa,mBACX,SACG;CACH,MAAM,cAAkD,EAAE;CAC1D,MAAM,EAAE,KAAK,aAAa,QAAQ,EAAE;CAEpC,OAAO,IAAI,MACT,EAAE,EACF,EACE,IAAI,GAAG,GAA8B;EACnC,MAAM,MAAM;EACZ,IAAI,OAAO,aACT,OAAO,YAAY;EAIrB,MAAM,WAAW,MAAM,KAAK,KAAK,IAAI,GAAG;EACxC,IAAI;GAEF,YAAY,OADE,aAAa,UAAU,QAAQ,CAAC,MACtB;GACxB,OAAO,YAAY;UACb;EAKR,IAAI,aAAa,MAAM;GAErB,MAAM,GAAG,MAAM,UAAU,QAAQ;GACjC,IAAI,UAAU,KAAA,GAAW;IACvB,YAAY,OAAO;IACnB,OAAO,YAAY;;SAEhB,IAAI,YAAY,OAAO,UAAU;GACtC,YAAY,OAAO,SAAS;GAC5B,OAAO,YAAY;;EAGrB,MAAM,IAAI,MAAM,UAAU,IAAI,cAAc;IAE/C,CACF"}
|