@rusamer/envgod 0.0.1
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/README.md +99 -0
- package/dist/index.d.mts +17 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +137 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +133 -0
- package/dist/index.mjs.map +1 -0
- package/dist/next.d.mts +6 -0
- package/dist/next.d.ts +6 -0
- package/dist/next.js +138 -0
- package/dist/next.js.map +1 -0
- package/dist/next.mjs +136 -0
- package/dist/next.mjs.map +1 -0
- package/dist/types-BJpNm9Vm.d.mts +22 -0
- package/dist/types-BJpNm9Vm.d.ts +22 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 EnvGod
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# EnvGod SDK
|
|
2
|
+
|
|
3
|
+
A secure, server-side Node.js and Next.js SDK for fetching environment bundles from the EnvGod Data Plane.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Secure by Default**: Throws if executed in a browser environment.
|
|
8
|
+
- **In-Memory Only**: Never writes secrets to disk or logs them.
|
|
9
|
+
- **Auto-Auth**: Exchanges `ENVGOD_API_KEY` for short-lived JWTs.
|
|
10
|
+
- **Smart Caching**: Caches tokens and bundles in memory until expiry.
|
|
11
|
+
- **Reliable**: Automatic retry on 401 (token expiry) and network resiliency.
|
|
12
|
+
- **Next.js Ready**: Dedicated `envgod/next` entry point with `server-only` guards.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @rusamer/envgod
|
|
18
|
+
# or
|
|
19
|
+
pnpm add @rusamer/envgod
|
|
20
|
+
# or
|
|
21
|
+
yarn add @rusamer/envgod
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Configuration
|
|
25
|
+
|
|
26
|
+
The SDK automatically reads the following environment variables:
|
|
27
|
+
|
|
28
|
+
| Variable | Description |
|
|
29
|
+
|C...|...|
|
|
30
|
+
| `ENVGOD_API_URL` | URL of the EnvGod Data Plane |
|
|
31
|
+
| `ENVGOD_API_KEY` | Your project Service Key |
|
|
32
|
+
| `ENVGOD_PROJECT` | Project ID |
|
|
33
|
+
| `ENVGOD_ENV` | Environment Name (e.g., prod) |
|
|
34
|
+
| `ENVGOD_SERVICE` | Service Name |
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
### Node.js
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
import { loadEnv } from '@rusamer/envgod';
|
|
42
|
+
|
|
43
|
+
async function main() {
|
|
44
|
+
const env = await loadEnv();
|
|
45
|
+
|
|
46
|
+
console.log(process.env.MY_SECRET); // Accessed from process.env
|
|
47
|
+
console.log(env.MY_SECRET); // Or from the returned object
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
main();
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Next.js (App Router / Server Actions)
|
|
54
|
+
|
|
55
|
+
Use the Next.js specific helper to ensure server-side only execution.
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
// src/lib/env.ts
|
|
59
|
+
import { loadServerEnv } from '@rusamer/envgod/next';
|
|
60
|
+
|
|
61
|
+
export async function getSecrets() {
|
|
62
|
+
return loadServerEnv();
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
// src/app/page.tsx
|
|
68
|
+
import { getSecrets } from '@/lib/env';
|
|
69
|
+
|
|
70
|
+
export default async function Page() {
|
|
71
|
+
const env = await getSecrets();
|
|
72
|
+
return <div>Secret length: {env.API_KEY.length}</div>;
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Security Notes
|
|
77
|
+
|
|
78
|
+
1. **Server-Only**: This SDK is designed strictly for server environments. It explicitly checks for `window` and imports `server-only` in the Next.js entrypoint.
|
|
79
|
+
2. **No Persistance**: Secrets are held in memory. Restarting the server will trigger a fresh fetch.
|
|
80
|
+
3. **Logs**: The SDK does not log secret values.
|
|
81
|
+
|
|
82
|
+
## For Maintainers
|
|
83
|
+
|
|
84
|
+
### Build
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
pnpm build
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Test
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
pnpm test
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Publish
|
|
97
|
+
|
|
98
|
+
1. `npm version patch`
|
|
99
|
+
2. `npm publish`
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { L as LoadEnvOptions, E as EnvGodConfig } from './types-BJpNm9Vm.mjs';
|
|
2
|
+
export { A as AuthExchangeResponse, B as BundleResponse } from './types-BJpNm9Vm.mjs';
|
|
3
|
+
|
|
4
|
+
/** @internal For testing only */
|
|
5
|
+
declare function _resetState(): void;
|
|
6
|
+
/**
|
|
7
|
+
* Validates and returns the configuration.
|
|
8
|
+
* Prioritizes options > process.env.
|
|
9
|
+
*/
|
|
10
|
+
declare function getEnvGodConfig(options?: LoadEnvOptions): EnvGodConfig;
|
|
11
|
+
/**
|
|
12
|
+
* Main entry point to load environment variables.
|
|
13
|
+
* Uses Singleflight pattern to prevent concurrent network requests.
|
|
14
|
+
*/
|
|
15
|
+
declare function loadEnv(options?: LoadEnvOptions): Promise<Record<string, string>>;
|
|
16
|
+
|
|
17
|
+
export { EnvGodConfig, LoadEnvOptions, _resetState, getEnvGodConfig, loadEnv };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { L as LoadEnvOptions, E as EnvGodConfig } from './types-BJpNm9Vm.js';
|
|
2
|
+
export { A as AuthExchangeResponse, B as BundleResponse } from './types-BJpNm9Vm.js';
|
|
3
|
+
|
|
4
|
+
/** @internal For testing only */
|
|
5
|
+
declare function _resetState(): void;
|
|
6
|
+
/**
|
|
7
|
+
* Validates and returns the configuration.
|
|
8
|
+
* Prioritizes options > process.env.
|
|
9
|
+
*/
|
|
10
|
+
declare function getEnvGodConfig(options?: LoadEnvOptions): EnvGodConfig;
|
|
11
|
+
/**
|
|
12
|
+
* Main entry point to load environment variables.
|
|
13
|
+
* Uses Singleflight pattern to prevent concurrent network requests.
|
|
14
|
+
*/
|
|
15
|
+
declare function loadEnv(options?: LoadEnvOptions): Promise<Record<string, string>>;
|
|
16
|
+
|
|
17
|
+
export { EnvGodConfig, LoadEnvOptions, _resetState, getEnvGodConfig, loadEnv };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
var state = {
|
|
5
|
+
token: null,
|
|
6
|
+
tokenExpiresAt: null,
|
|
7
|
+
bundle: null
|
|
8
|
+
};
|
|
9
|
+
var pendingLoadPromise = null;
|
|
10
|
+
function _resetState() {
|
|
11
|
+
state.token = null;
|
|
12
|
+
state.tokenExpiresAt = null;
|
|
13
|
+
state.bundle = null;
|
|
14
|
+
pendingLoadPromise = null;
|
|
15
|
+
}
|
|
16
|
+
function getEnvGodConfig(options) {
|
|
17
|
+
const env = process.env;
|
|
18
|
+
const config = {
|
|
19
|
+
apiUrl: options?.config?.apiUrl ?? env.ENVGOD_API_URL,
|
|
20
|
+
apiKey: options?.config?.apiKey ?? env.ENVGOD_API_KEY,
|
|
21
|
+
project: options?.config?.project ?? env.ENVGOD_PROJECT,
|
|
22
|
+
env: options?.config?.env ?? env.ENVGOD_ENV,
|
|
23
|
+
service: options?.config?.service ?? env.ENVGOD_SERVICE
|
|
24
|
+
};
|
|
25
|
+
const missing = Object.entries(config).filter(([_, v]) => !v).map(([k]) => k);
|
|
26
|
+
if (missing.length > 0) {
|
|
27
|
+
throw new Error(`[EnvGod] Missing required configuration: ${missing.join(", ")}`);
|
|
28
|
+
}
|
|
29
|
+
return config;
|
|
30
|
+
}
|
|
31
|
+
function checkBrowser() {
|
|
32
|
+
if (typeof window !== "undefined") {
|
|
33
|
+
throw new Error("[EnvGod] Security Warning: SDK execution attempting in browser environment. This SDK is server-only.");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function fetchWithTimeout(url, init) {
|
|
37
|
+
const { timeout = 5e3, ...rest } = init;
|
|
38
|
+
const controller = new AbortController();
|
|
39
|
+
const id = setTimeout(() => controller.abort(), timeout);
|
|
40
|
+
try {
|
|
41
|
+
const res = await fetch(url, { ...rest, signal: controller.signal });
|
|
42
|
+
return res;
|
|
43
|
+
} catch (err) {
|
|
44
|
+
if (err.name === "AbortError") {
|
|
45
|
+
throw new Error(`[EnvGod] Request timed out after ${timeout}ms`);
|
|
46
|
+
}
|
|
47
|
+
throw err;
|
|
48
|
+
} finally {
|
|
49
|
+
clearTimeout(id);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function exchangeToken(config, timeout) {
|
|
53
|
+
const res = await fetchWithTimeout(`${config.apiUrl}/v1/auth/exchange`, {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: {
|
|
56
|
+
"Content-Type": "application/json",
|
|
57
|
+
"Authorization": `Bearer ${config.apiKey}`
|
|
58
|
+
},
|
|
59
|
+
body: JSON.stringify({
|
|
60
|
+
project: config.project,
|
|
61
|
+
env: config.env,
|
|
62
|
+
service: config.service
|
|
63
|
+
}),
|
|
64
|
+
timeout
|
|
65
|
+
});
|
|
66
|
+
if (!res.ok) {
|
|
67
|
+
throw new Error(`[EnvGod] Auth exchange failed: ${res.status} ${res.statusText}`);
|
|
68
|
+
}
|
|
69
|
+
const data = await res.json();
|
|
70
|
+
state.token = data.token;
|
|
71
|
+
state.tokenExpiresAt = new Date(data.expiresAt).getTime();
|
|
72
|
+
return data.token;
|
|
73
|
+
}
|
|
74
|
+
async function fetchBundle(config, token, timeout) {
|
|
75
|
+
const res = await fetchWithTimeout(`${config.apiUrl}/v1/bundle`, {
|
|
76
|
+
method: "GET",
|
|
77
|
+
headers: {
|
|
78
|
+
"Authorization": `Bearer ${token}`
|
|
79
|
+
},
|
|
80
|
+
timeout
|
|
81
|
+
});
|
|
82
|
+
if (res.status === 401) {
|
|
83
|
+
throw new Error("401");
|
|
84
|
+
}
|
|
85
|
+
if (!res.ok) {
|
|
86
|
+
throw new Error(`[EnvGod] Fetch bundle failed: ${res.status} ${res.statusText}`);
|
|
87
|
+
}
|
|
88
|
+
const data = await res.json();
|
|
89
|
+
return data.values;
|
|
90
|
+
}
|
|
91
|
+
async function loadEnvInternal(options) {
|
|
92
|
+
checkBrowser();
|
|
93
|
+
const config = getEnvGodConfig(options);
|
|
94
|
+
const timeout = options?.timeout ?? 5e3;
|
|
95
|
+
const now = Date.now();
|
|
96
|
+
let token = state.token;
|
|
97
|
+
let tokenValid = token && state.tokenExpiresAt && state.tokenExpiresAt > now;
|
|
98
|
+
if (!tokenValid) {
|
|
99
|
+
token = await exchangeToken(config, timeout);
|
|
100
|
+
}
|
|
101
|
+
if (state.bundle && tokenValid) {
|
|
102
|
+
Object.assign(process.env, state.bundle);
|
|
103
|
+
return state.bundle;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const values = await fetchBundle(config, token, timeout);
|
|
107
|
+
state.bundle = values;
|
|
108
|
+
Object.assign(process.env, values);
|
|
109
|
+
return values;
|
|
110
|
+
} catch (err) {
|
|
111
|
+
if (err.message === "401") {
|
|
112
|
+
state.token = null;
|
|
113
|
+
state.bundle = null;
|
|
114
|
+
const newToken = await exchangeToken(config, timeout);
|
|
115
|
+
const values = await fetchBundle(config, newToken, timeout);
|
|
116
|
+
state.bundle = values;
|
|
117
|
+
Object.assign(process.env, values);
|
|
118
|
+
return values;
|
|
119
|
+
}
|
|
120
|
+
throw err;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function loadEnv(options) {
|
|
124
|
+
if (pendingLoadPromise) {
|
|
125
|
+
return pendingLoadPromise;
|
|
126
|
+
}
|
|
127
|
+
pendingLoadPromise = loadEnvInternal(options).finally(() => {
|
|
128
|
+
pendingLoadPromise = null;
|
|
129
|
+
});
|
|
130
|
+
return pendingLoadPromise;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
exports._resetState = _resetState;
|
|
134
|
+
exports.getEnvGodConfig = getEnvGodConfig;
|
|
135
|
+
exports.loadEnv = loadEnv;
|
|
136
|
+
//# sourceMappingURL=index.js.map
|
|
137
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { EnvGodConfig, LoadEnvOptions, AuthExchangeResponse, BundleResponse } from './types.js';\r\n\r\n// --- State ---\r\ninterface CacheState {\r\n token: string | null;\r\n tokenExpiresAt: number | null; // Timestamp in ms\r\n bundle: Record<string, string> | null;\r\n}\r\n\r\nconst state: CacheState = {\r\n token: null,\r\n tokenExpiresAt: null,\r\n bundle: null,\r\n};\r\n\r\nlet pendingLoadPromise: Promise<Record<string, string>> | null = null;\r\n\r\n/** @internal For testing only */\r\nexport function _resetState() {\r\n state.token = null;\r\n state.tokenExpiresAt = null;\r\n state.bundle = null;\r\n pendingLoadPromise = null;\r\n}\r\n\r\n// --- Helpers ---\r\n\r\n/**\r\n * Validates and returns the configuration.\r\n * Prioritizes options > process.env.\r\n */\r\nexport function getEnvGodConfig(options?: LoadEnvOptions): EnvGodConfig {\r\n const env = process.env;\r\n const config = {\r\n apiUrl: options?.config?.apiUrl ?? env.ENVGOD_API_URL,\r\n apiKey: options?.config?.apiKey ?? env.ENVGOD_API_KEY,\r\n project: options?.config?.project ?? env.ENVGOD_PROJECT,\r\n env: options?.config?.env ?? env.ENVGOD_ENV,\r\n service: options?.config?.service ?? env.ENVGOD_SERVICE,\r\n };\r\n\r\n const missing = Object.entries(config)\r\n .filter(([_, v]) => !v)\r\n .map(([k]) => k);\r\n\r\n if (missing.length > 0) {\r\n throw new Error(`[EnvGod] Missing required configuration: ${missing.join(', ')}`);\r\n }\r\n\r\n return config as EnvGodConfig;\r\n}\r\n\r\n/**\r\n * Checks if the current environment is a browser.\r\n */\r\nfunction checkBrowser() {\r\n if (typeof window !== 'undefined') {\r\n throw new Error('[EnvGod] Security Warning: SDK execution attempting in browser environment. This SDK is server-only.');\r\n }\r\n}\r\n\r\n/**\r\n * Fetches with timeout.\r\n */\r\nasync function fetchWithTimeout(url: string, init: RequestInit & { timeout?: number }) {\r\n const { timeout = 5000, ...rest } = init;\r\n const controller = new AbortController();\r\n const id = setTimeout(() => controller.abort(), timeout);\r\n\r\n try {\r\n const res = await fetch(url, { ...rest, signal: controller.signal });\r\n return res;\r\n } catch (err: any) {\r\n if (err.name === 'AbortError') {\r\n throw new Error(`[EnvGod] Request timed out after ${timeout}ms`);\r\n }\r\n throw err;\r\n } finally {\r\n clearTimeout(id);\r\n }\r\n}\r\n\r\n// --- Core Logic ---\r\n\r\nasync function exchangeToken(config: EnvGodConfig, timeout: number): Promise<string> {\r\n const res = await fetchWithTimeout(`${config.apiUrl}/v1/auth/exchange`, {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n 'Authorization': `Bearer ${config.apiKey}`,\r\n },\r\n body: JSON.stringify({\r\n project: config.project,\r\n env: config.env,\r\n service: config.service,\r\n }),\r\n timeout,\r\n });\r\n\r\n if (!res.ok) {\r\n throw new Error(`[EnvGod] Auth exchange failed: ${res.status} ${res.statusText}`);\r\n }\r\n\r\n const data = (await res.json()) as AuthExchangeResponse;\r\n state.token = data.token;\r\n state.tokenExpiresAt = new Date(data.expiresAt).getTime();\r\n return data.token;\r\n}\r\n\r\nasync function fetchBundle(config: EnvGodConfig, token: string, timeout: number): Promise<Record<string, string>> {\r\n const res = await fetchWithTimeout(`${config.apiUrl}/v1/bundle`, {\r\n method: 'GET',\r\n headers: {\r\n 'Authorization': `Bearer ${token}`,\r\n },\r\n timeout,\r\n });\r\n\r\n if (res.status === 401) {\r\n throw new Error('401'); // Signal to retry\r\n }\r\n\r\n if (!res.ok) {\r\n throw new Error(`[EnvGod] Fetch bundle failed: ${res.status} ${res.statusText}`);\r\n }\r\n\r\n const data = (await res.json()) as BundleResponse;\r\n return data.values;\r\n}\r\n\r\nasync function loadEnvInternal(options?: LoadEnvOptions): Promise<Record<string, string>> {\r\n checkBrowser();\r\n const config = getEnvGodConfig(options);\r\n const timeout = options?.timeout ?? 5000;\r\n const now = Date.now();\r\n\r\n // 1. Check if we have a valid token\r\n let token = state.token;\r\n let tokenValid = token && state.tokenExpiresAt && state.tokenExpiresAt > now;\r\n\r\n // 2. If token invalid, exchange\r\n if (!tokenValid) {\r\n token = await exchangeToken(config, timeout);\r\n }\r\n\r\n // 3. If we have a cached bundle and the token is still the same/valid, return it?\r\n if (state.bundle && tokenValid) {\r\n Object.assign(process.env, state.bundle);\r\n return state.bundle;\r\n }\r\n\r\n // 4. Fetch bundle with retry logic\r\n try {\r\n const values = await fetchBundle(config, token!, timeout);\r\n state.bundle = values;\r\n Object.assign(process.env, values);\r\n return values;\r\n } catch (err: any) {\r\n if (err.message === '401') {\r\n // Retry ONCE: Re-exchange and Re-fetch\r\n state.token = null;\r\n state.bundle = null;\r\n\r\n const newToken = await exchangeToken(config, timeout);\r\n const values = await fetchBundle(config, newToken, timeout);\r\n state.bundle = values;\r\n Object.assign(process.env, values);\r\n return values;\r\n }\r\n throw err;\r\n }\r\n}\r\n\r\n/**\r\n * Main entry point to load environment variables.\r\n * Uses Singleflight pattern to prevent concurrent network requests.\r\n */\r\nexport function loadEnv(options?: LoadEnvOptions): Promise<Record<string, string>> {\r\n if (pendingLoadPromise) {\r\n return pendingLoadPromise;\r\n }\r\n\r\n pendingLoadPromise = loadEnvInternal(options)\r\n .finally(() => {\r\n pendingLoadPromise = null;\r\n });\r\n\r\n return pendingLoadPromise;\r\n}\r\n\r\nexport * from './types.js';\r\n"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var state = {
|
|
3
|
+
token: null,
|
|
4
|
+
tokenExpiresAt: null,
|
|
5
|
+
bundle: null
|
|
6
|
+
};
|
|
7
|
+
var pendingLoadPromise = null;
|
|
8
|
+
function _resetState() {
|
|
9
|
+
state.token = null;
|
|
10
|
+
state.tokenExpiresAt = null;
|
|
11
|
+
state.bundle = null;
|
|
12
|
+
pendingLoadPromise = null;
|
|
13
|
+
}
|
|
14
|
+
function getEnvGodConfig(options) {
|
|
15
|
+
const env = process.env;
|
|
16
|
+
const config = {
|
|
17
|
+
apiUrl: options?.config?.apiUrl ?? env.ENVGOD_API_URL,
|
|
18
|
+
apiKey: options?.config?.apiKey ?? env.ENVGOD_API_KEY,
|
|
19
|
+
project: options?.config?.project ?? env.ENVGOD_PROJECT,
|
|
20
|
+
env: options?.config?.env ?? env.ENVGOD_ENV,
|
|
21
|
+
service: options?.config?.service ?? env.ENVGOD_SERVICE
|
|
22
|
+
};
|
|
23
|
+
const missing = Object.entries(config).filter(([_, v]) => !v).map(([k]) => k);
|
|
24
|
+
if (missing.length > 0) {
|
|
25
|
+
throw new Error(`[EnvGod] Missing required configuration: ${missing.join(", ")}`);
|
|
26
|
+
}
|
|
27
|
+
return config;
|
|
28
|
+
}
|
|
29
|
+
function checkBrowser() {
|
|
30
|
+
if (typeof window !== "undefined") {
|
|
31
|
+
throw new Error("[EnvGod] Security Warning: SDK execution attempting in browser environment. This SDK is server-only.");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function fetchWithTimeout(url, init) {
|
|
35
|
+
const { timeout = 5e3, ...rest } = init;
|
|
36
|
+
const controller = new AbortController();
|
|
37
|
+
const id = setTimeout(() => controller.abort(), timeout);
|
|
38
|
+
try {
|
|
39
|
+
const res = await fetch(url, { ...rest, signal: controller.signal });
|
|
40
|
+
return res;
|
|
41
|
+
} catch (err) {
|
|
42
|
+
if (err.name === "AbortError") {
|
|
43
|
+
throw new Error(`[EnvGod] Request timed out after ${timeout}ms`);
|
|
44
|
+
}
|
|
45
|
+
throw err;
|
|
46
|
+
} finally {
|
|
47
|
+
clearTimeout(id);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function exchangeToken(config, timeout) {
|
|
51
|
+
const res = await fetchWithTimeout(`${config.apiUrl}/v1/auth/exchange`, {
|
|
52
|
+
method: "POST",
|
|
53
|
+
headers: {
|
|
54
|
+
"Content-Type": "application/json",
|
|
55
|
+
"Authorization": `Bearer ${config.apiKey}`
|
|
56
|
+
},
|
|
57
|
+
body: JSON.stringify({
|
|
58
|
+
project: config.project,
|
|
59
|
+
env: config.env,
|
|
60
|
+
service: config.service
|
|
61
|
+
}),
|
|
62
|
+
timeout
|
|
63
|
+
});
|
|
64
|
+
if (!res.ok) {
|
|
65
|
+
throw new Error(`[EnvGod] Auth exchange failed: ${res.status} ${res.statusText}`);
|
|
66
|
+
}
|
|
67
|
+
const data = await res.json();
|
|
68
|
+
state.token = data.token;
|
|
69
|
+
state.tokenExpiresAt = new Date(data.expiresAt).getTime();
|
|
70
|
+
return data.token;
|
|
71
|
+
}
|
|
72
|
+
async function fetchBundle(config, token, timeout) {
|
|
73
|
+
const res = await fetchWithTimeout(`${config.apiUrl}/v1/bundle`, {
|
|
74
|
+
method: "GET",
|
|
75
|
+
headers: {
|
|
76
|
+
"Authorization": `Bearer ${token}`
|
|
77
|
+
},
|
|
78
|
+
timeout
|
|
79
|
+
});
|
|
80
|
+
if (res.status === 401) {
|
|
81
|
+
throw new Error("401");
|
|
82
|
+
}
|
|
83
|
+
if (!res.ok) {
|
|
84
|
+
throw new Error(`[EnvGod] Fetch bundle failed: ${res.status} ${res.statusText}`);
|
|
85
|
+
}
|
|
86
|
+
const data = await res.json();
|
|
87
|
+
return data.values;
|
|
88
|
+
}
|
|
89
|
+
async function loadEnvInternal(options) {
|
|
90
|
+
checkBrowser();
|
|
91
|
+
const config = getEnvGodConfig(options);
|
|
92
|
+
const timeout = options?.timeout ?? 5e3;
|
|
93
|
+
const now = Date.now();
|
|
94
|
+
let token = state.token;
|
|
95
|
+
let tokenValid = token && state.tokenExpiresAt && state.tokenExpiresAt > now;
|
|
96
|
+
if (!tokenValid) {
|
|
97
|
+
token = await exchangeToken(config, timeout);
|
|
98
|
+
}
|
|
99
|
+
if (state.bundle && tokenValid) {
|
|
100
|
+
Object.assign(process.env, state.bundle);
|
|
101
|
+
return state.bundle;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const values = await fetchBundle(config, token, timeout);
|
|
105
|
+
state.bundle = values;
|
|
106
|
+
Object.assign(process.env, values);
|
|
107
|
+
return values;
|
|
108
|
+
} catch (err) {
|
|
109
|
+
if (err.message === "401") {
|
|
110
|
+
state.token = null;
|
|
111
|
+
state.bundle = null;
|
|
112
|
+
const newToken = await exchangeToken(config, timeout);
|
|
113
|
+
const values = await fetchBundle(config, newToken, timeout);
|
|
114
|
+
state.bundle = values;
|
|
115
|
+
Object.assign(process.env, values);
|
|
116
|
+
return values;
|
|
117
|
+
}
|
|
118
|
+
throw err;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function loadEnv(options) {
|
|
122
|
+
if (pendingLoadPromise) {
|
|
123
|
+
return pendingLoadPromise;
|
|
124
|
+
}
|
|
125
|
+
pendingLoadPromise = loadEnvInternal(options).finally(() => {
|
|
126
|
+
pendingLoadPromise = null;
|
|
127
|
+
});
|
|
128
|
+
return pendingLoadPromise;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export { _resetState, getEnvGodConfig, loadEnv };
|
|
132
|
+
//# sourceMappingURL=index.mjs.map
|
|
133
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["import type { EnvGodConfig, LoadEnvOptions, AuthExchangeResponse, BundleResponse } from './types.js';\r\n\r\n// --- State ---\r\ninterface CacheState {\r\n token: string | null;\r\n tokenExpiresAt: number | null; // Timestamp in ms\r\n bundle: Record<string, string> | null;\r\n}\r\n\r\nconst state: CacheState = {\r\n token: null,\r\n tokenExpiresAt: null,\r\n bundle: null,\r\n};\r\n\r\nlet pendingLoadPromise: Promise<Record<string, string>> | null = null;\r\n\r\n/** @internal For testing only */\r\nexport function _resetState() {\r\n state.token = null;\r\n state.tokenExpiresAt = null;\r\n state.bundle = null;\r\n pendingLoadPromise = null;\r\n}\r\n\r\n// --- Helpers ---\r\n\r\n/**\r\n * Validates and returns the configuration.\r\n * Prioritizes options > process.env.\r\n */\r\nexport function getEnvGodConfig(options?: LoadEnvOptions): EnvGodConfig {\r\n const env = process.env;\r\n const config = {\r\n apiUrl: options?.config?.apiUrl ?? env.ENVGOD_API_URL,\r\n apiKey: options?.config?.apiKey ?? env.ENVGOD_API_KEY,\r\n project: options?.config?.project ?? env.ENVGOD_PROJECT,\r\n env: options?.config?.env ?? env.ENVGOD_ENV,\r\n service: options?.config?.service ?? env.ENVGOD_SERVICE,\r\n };\r\n\r\n const missing = Object.entries(config)\r\n .filter(([_, v]) => !v)\r\n .map(([k]) => k);\r\n\r\n if (missing.length > 0) {\r\n throw new Error(`[EnvGod] Missing required configuration: ${missing.join(', ')}`);\r\n }\r\n\r\n return config as EnvGodConfig;\r\n}\r\n\r\n/**\r\n * Checks if the current environment is a browser.\r\n */\r\nfunction checkBrowser() {\r\n if (typeof window !== 'undefined') {\r\n throw new Error('[EnvGod] Security Warning: SDK execution attempting in browser environment. This SDK is server-only.');\r\n }\r\n}\r\n\r\n/**\r\n * Fetches with timeout.\r\n */\r\nasync function fetchWithTimeout(url: string, init: RequestInit & { timeout?: number }) {\r\n const { timeout = 5000, ...rest } = init;\r\n const controller = new AbortController();\r\n const id = setTimeout(() => controller.abort(), timeout);\r\n\r\n try {\r\n const res = await fetch(url, { ...rest, signal: controller.signal });\r\n return res;\r\n } catch (err: any) {\r\n if (err.name === 'AbortError') {\r\n throw new Error(`[EnvGod] Request timed out after ${timeout}ms`);\r\n }\r\n throw err;\r\n } finally {\r\n clearTimeout(id);\r\n }\r\n}\r\n\r\n// --- Core Logic ---\r\n\r\nasync function exchangeToken(config: EnvGodConfig, timeout: number): Promise<string> {\r\n const res = await fetchWithTimeout(`${config.apiUrl}/v1/auth/exchange`, {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n 'Authorization': `Bearer ${config.apiKey}`,\r\n },\r\n body: JSON.stringify({\r\n project: config.project,\r\n env: config.env,\r\n service: config.service,\r\n }),\r\n timeout,\r\n });\r\n\r\n if (!res.ok) {\r\n throw new Error(`[EnvGod] Auth exchange failed: ${res.status} ${res.statusText}`);\r\n }\r\n\r\n const data = (await res.json()) as AuthExchangeResponse;\r\n state.token = data.token;\r\n state.tokenExpiresAt = new Date(data.expiresAt).getTime();\r\n return data.token;\r\n}\r\n\r\nasync function fetchBundle(config: EnvGodConfig, token: string, timeout: number): Promise<Record<string, string>> {\r\n const res = await fetchWithTimeout(`${config.apiUrl}/v1/bundle`, {\r\n method: 'GET',\r\n headers: {\r\n 'Authorization': `Bearer ${token}`,\r\n },\r\n timeout,\r\n });\r\n\r\n if (res.status === 401) {\r\n throw new Error('401'); // Signal to retry\r\n }\r\n\r\n if (!res.ok) {\r\n throw new Error(`[EnvGod] Fetch bundle failed: ${res.status} ${res.statusText}`);\r\n }\r\n\r\n const data = (await res.json()) as BundleResponse;\r\n return data.values;\r\n}\r\n\r\nasync function loadEnvInternal(options?: LoadEnvOptions): Promise<Record<string, string>> {\r\n checkBrowser();\r\n const config = getEnvGodConfig(options);\r\n const timeout = options?.timeout ?? 5000;\r\n const now = Date.now();\r\n\r\n // 1. Check if we have a valid token\r\n let token = state.token;\r\n let tokenValid = token && state.tokenExpiresAt && state.tokenExpiresAt > now;\r\n\r\n // 2. If token invalid, exchange\r\n if (!tokenValid) {\r\n token = await exchangeToken(config, timeout);\r\n }\r\n\r\n // 3. If we have a cached bundle and the token is still the same/valid, return it?\r\n if (state.bundle && tokenValid) {\r\n Object.assign(process.env, state.bundle);\r\n return state.bundle;\r\n }\r\n\r\n // 4. Fetch bundle with retry logic\r\n try {\r\n const values = await fetchBundle(config, token!, timeout);\r\n state.bundle = values;\r\n Object.assign(process.env, values);\r\n return values;\r\n } catch (err: any) {\r\n if (err.message === '401') {\r\n // Retry ONCE: Re-exchange and Re-fetch\r\n state.token = null;\r\n state.bundle = null;\r\n\r\n const newToken = await exchangeToken(config, timeout);\r\n const values = await fetchBundle(config, newToken, timeout);\r\n state.bundle = values;\r\n Object.assign(process.env, values);\r\n return values;\r\n }\r\n throw err;\r\n }\r\n}\r\n\r\n/**\r\n * Main entry point to load environment variables.\r\n * Uses Singleflight pattern to prevent concurrent network requests.\r\n */\r\nexport function loadEnv(options?: LoadEnvOptions): Promise<Record<string, string>> {\r\n if (pendingLoadPromise) {\r\n return pendingLoadPromise;\r\n }\r\n\r\n pendingLoadPromise = loadEnvInternal(options)\r\n .finally(() => {\r\n pendingLoadPromise = null;\r\n });\r\n\r\n return pendingLoadPromise;\r\n}\r\n\r\nexport * from './types.js';\r\n"]}
|
package/dist/next.d.mts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { L as LoadEnvOptions } from './types-BJpNm9Vm.mjs';
|
|
2
|
+
export { A as AuthExchangeResponse, B as BundleResponse, E as EnvGodConfig } from './types-BJpNm9Vm.mjs';
|
|
3
|
+
|
|
4
|
+
declare function loadServerEnv(options?: LoadEnvOptions): Promise<Record<string, string>>;
|
|
5
|
+
|
|
6
|
+
export { LoadEnvOptions, loadServerEnv };
|
package/dist/next.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { L as LoadEnvOptions } from './types-BJpNm9Vm.js';
|
|
2
|
+
export { A as AuthExchangeResponse, B as BundleResponse, E as EnvGodConfig } from './types-BJpNm9Vm.js';
|
|
3
|
+
|
|
4
|
+
declare function loadServerEnv(options?: LoadEnvOptions): Promise<Record<string, string>>;
|
|
5
|
+
|
|
6
|
+
export { LoadEnvOptions, loadServerEnv };
|
package/dist/next.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
require('server-only');
|
|
4
|
+
|
|
5
|
+
// src/next.ts
|
|
6
|
+
|
|
7
|
+
// src/index.ts
|
|
8
|
+
var state = {
|
|
9
|
+
token: null,
|
|
10
|
+
tokenExpiresAt: null,
|
|
11
|
+
bundle: null
|
|
12
|
+
};
|
|
13
|
+
var pendingLoadPromise = null;
|
|
14
|
+
function getEnvGodConfig(options) {
|
|
15
|
+
const env = process.env;
|
|
16
|
+
const config = {
|
|
17
|
+
apiUrl: options?.config?.apiUrl ?? env.ENVGOD_API_URL,
|
|
18
|
+
apiKey: options?.config?.apiKey ?? env.ENVGOD_API_KEY,
|
|
19
|
+
project: options?.config?.project ?? env.ENVGOD_PROJECT,
|
|
20
|
+
env: options?.config?.env ?? env.ENVGOD_ENV,
|
|
21
|
+
service: options?.config?.service ?? env.ENVGOD_SERVICE
|
|
22
|
+
};
|
|
23
|
+
const missing = Object.entries(config).filter(([_, v]) => !v).map(([k]) => k);
|
|
24
|
+
if (missing.length > 0) {
|
|
25
|
+
throw new Error(`[EnvGod] Missing required configuration: ${missing.join(", ")}`);
|
|
26
|
+
}
|
|
27
|
+
return config;
|
|
28
|
+
}
|
|
29
|
+
function checkBrowser() {
|
|
30
|
+
if (typeof window !== "undefined") {
|
|
31
|
+
throw new Error("[EnvGod] Security Warning: SDK execution attempting in browser environment. This SDK is server-only.");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function fetchWithTimeout(url, init) {
|
|
35
|
+
const { timeout = 5e3, ...rest } = init;
|
|
36
|
+
const controller = new AbortController();
|
|
37
|
+
const id = setTimeout(() => controller.abort(), timeout);
|
|
38
|
+
try {
|
|
39
|
+
const res = await fetch(url, { ...rest, signal: controller.signal });
|
|
40
|
+
return res;
|
|
41
|
+
} catch (err) {
|
|
42
|
+
if (err.name === "AbortError") {
|
|
43
|
+
throw new Error(`[EnvGod] Request timed out after ${timeout}ms`);
|
|
44
|
+
}
|
|
45
|
+
throw err;
|
|
46
|
+
} finally {
|
|
47
|
+
clearTimeout(id);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function exchangeToken(config, timeout) {
|
|
51
|
+
const res = await fetchWithTimeout(`${config.apiUrl}/v1/auth/exchange`, {
|
|
52
|
+
method: "POST",
|
|
53
|
+
headers: {
|
|
54
|
+
"Content-Type": "application/json",
|
|
55
|
+
"Authorization": `Bearer ${config.apiKey}`
|
|
56
|
+
},
|
|
57
|
+
body: JSON.stringify({
|
|
58
|
+
project: config.project,
|
|
59
|
+
env: config.env,
|
|
60
|
+
service: config.service
|
|
61
|
+
}),
|
|
62
|
+
timeout
|
|
63
|
+
});
|
|
64
|
+
if (!res.ok) {
|
|
65
|
+
throw new Error(`[EnvGod] Auth exchange failed: ${res.status} ${res.statusText}`);
|
|
66
|
+
}
|
|
67
|
+
const data = await res.json();
|
|
68
|
+
state.token = data.token;
|
|
69
|
+
state.tokenExpiresAt = new Date(data.expiresAt).getTime();
|
|
70
|
+
return data.token;
|
|
71
|
+
}
|
|
72
|
+
async function fetchBundle(config, token, timeout) {
|
|
73
|
+
const res = await fetchWithTimeout(`${config.apiUrl}/v1/bundle`, {
|
|
74
|
+
method: "GET",
|
|
75
|
+
headers: {
|
|
76
|
+
"Authorization": `Bearer ${token}`
|
|
77
|
+
},
|
|
78
|
+
timeout
|
|
79
|
+
});
|
|
80
|
+
if (res.status === 401) {
|
|
81
|
+
throw new Error("401");
|
|
82
|
+
}
|
|
83
|
+
if (!res.ok) {
|
|
84
|
+
throw new Error(`[EnvGod] Fetch bundle failed: ${res.status} ${res.statusText}`);
|
|
85
|
+
}
|
|
86
|
+
const data = await res.json();
|
|
87
|
+
return data.values;
|
|
88
|
+
}
|
|
89
|
+
async function loadEnvInternal(options) {
|
|
90
|
+
checkBrowser();
|
|
91
|
+
const config = getEnvGodConfig(options);
|
|
92
|
+
const timeout = options?.timeout ?? 5e3;
|
|
93
|
+
const now = Date.now();
|
|
94
|
+
let token = state.token;
|
|
95
|
+
let tokenValid = token && state.tokenExpiresAt && state.tokenExpiresAt > now;
|
|
96
|
+
if (!tokenValid) {
|
|
97
|
+
token = await exchangeToken(config, timeout);
|
|
98
|
+
}
|
|
99
|
+
if (state.bundle && tokenValid) {
|
|
100
|
+
Object.assign(process.env, state.bundle);
|
|
101
|
+
return state.bundle;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const values = await fetchBundle(config, token, timeout);
|
|
105
|
+
state.bundle = values;
|
|
106
|
+
Object.assign(process.env, values);
|
|
107
|
+
return values;
|
|
108
|
+
} catch (err) {
|
|
109
|
+
if (err.message === "401") {
|
|
110
|
+
state.token = null;
|
|
111
|
+
state.bundle = null;
|
|
112
|
+
const newToken = await exchangeToken(config, timeout);
|
|
113
|
+
const values = await fetchBundle(config, newToken, timeout);
|
|
114
|
+
state.bundle = values;
|
|
115
|
+
Object.assign(process.env, values);
|
|
116
|
+
return values;
|
|
117
|
+
}
|
|
118
|
+
throw err;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function loadEnv(options) {
|
|
122
|
+
if (pendingLoadPromise) {
|
|
123
|
+
return pendingLoadPromise;
|
|
124
|
+
}
|
|
125
|
+
pendingLoadPromise = loadEnvInternal(options).finally(() => {
|
|
126
|
+
pendingLoadPromise = null;
|
|
127
|
+
});
|
|
128
|
+
return pendingLoadPromise;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/next.ts
|
|
132
|
+
async function loadServerEnv(options) {
|
|
133
|
+
return loadEnv(options);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
exports.loadServerEnv = loadServerEnv;
|
|
137
|
+
//# sourceMappingURL=next.js.map
|
|
138
|
+
//# sourceMappingURL=next.js.map
|
package/dist/next.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/next.ts"],"names":[],"mappings":"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","file":"next.js","sourcesContent":["import type { EnvGodConfig, LoadEnvOptions, AuthExchangeResponse, BundleResponse } from './types.js';\r\n\r\n// --- State ---\r\ninterface CacheState {\r\n token: string | null;\r\n tokenExpiresAt: number | null; // Timestamp in ms\r\n bundle: Record<string, string> | null;\r\n}\r\n\r\nconst state: CacheState = {\r\n token: null,\r\n tokenExpiresAt: null,\r\n bundle: null,\r\n};\r\n\r\nlet pendingLoadPromise: Promise<Record<string, string>> | null = null;\r\n\r\n/** @internal For testing only */\r\nexport function _resetState() {\r\n state.token = null;\r\n state.tokenExpiresAt = null;\r\n state.bundle = null;\r\n pendingLoadPromise = null;\r\n}\r\n\r\n// --- Helpers ---\r\n\r\n/**\r\n * Validates and returns the configuration.\r\n * Prioritizes options > process.env.\r\n */\r\nexport function getEnvGodConfig(options?: LoadEnvOptions): EnvGodConfig {\r\n const env = process.env;\r\n const config = {\r\n apiUrl: options?.config?.apiUrl ?? env.ENVGOD_API_URL,\r\n apiKey: options?.config?.apiKey ?? env.ENVGOD_API_KEY,\r\n project: options?.config?.project ?? env.ENVGOD_PROJECT,\r\n env: options?.config?.env ?? env.ENVGOD_ENV,\r\n service: options?.config?.service ?? env.ENVGOD_SERVICE,\r\n };\r\n\r\n const missing = Object.entries(config)\r\n .filter(([_, v]) => !v)\r\n .map(([k]) => k);\r\n\r\n if (missing.length > 0) {\r\n throw new Error(`[EnvGod] Missing required configuration: ${missing.join(', ')}`);\r\n }\r\n\r\n return config as EnvGodConfig;\r\n}\r\n\r\n/**\r\n * Checks if the current environment is a browser.\r\n */\r\nfunction checkBrowser() {\r\n if (typeof window !== 'undefined') {\r\n throw new Error('[EnvGod] Security Warning: SDK execution attempting in browser environment. This SDK is server-only.');\r\n }\r\n}\r\n\r\n/**\r\n * Fetches with timeout.\r\n */\r\nasync function fetchWithTimeout(url: string, init: RequestInit & { timeout?: number }) {\r\n const { timeout = 5000, ...rest } = init;\r\n const controller = new AbortController();\r\n const id = setTimeout(() => controller.abort(), timeout);\r\n\r\n try {\r\n const res = await fetch(url, { ...rest, signal: controller.signal });\r\n return res;\r\n } catch (err: any) {\r\n if (err.name === 'AbortError') {\r\n throw new Error(`[EnvGod] Request timed out after ${timeout}ms`);\r\n }\r\n throw err;\r\n } finally {\r\n clearTimeout(id);\r\n }\r\n}\r\n\r\n// --- Core Logic ---\r\n\r\nasync function exchangeToken(config: EnvGodConfig, timeout: number): Promise<string> {\r\n const res = await fetchWithTimeout(`${config.apiUrl}/v1/auth/exchange`, {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n 'Authorization': `Bearer ${config.apiKey}`,\r\n },\r\n body: JSON.stringify({\r\n project: config.project,\r\n env: config.env,\r\n service: config.service,\r\n }),\r\n timeout,\r\n });\r\n\r\n if (!res.ok) {\r\n throw new Error(`[EnvGod] Auth exchange failed: ${res.status} ${res.statusText}`);\r\n }\r\n\r\n const data = (await res.json()) as AuthExchangeResponse;\r\n state.token = data.token;\r\n state.tokenExpiresAt = new Date(data.expiresAt).getTime();\r\n return data.token;\r\n}\r\n\r\nasync function fetchBundle(config: EnvGodConfig, token: string, timeout: number): Promise<Record<string, string>> {\r\n const res = await fetchWithTimeout(`${config.apiUrl}/v1/bundle`, {\r\n method: 'GET',\r\n headers: {\r\n 'Authorization': `Bearer ${token}`,\r\n },\r\n timeout,\r\n });\r\n\r\n if (res.status === 401) {\r\n throw new Error('401'); // Signal to retry\r\n }\r\n\r\n if (!res.ok) {\r\n throw new Error(`[EnvGod] Fetch bundle failed: ${res.status} ${res.statusText}`);\r\n }\r\n\r\n const data = (await res.json()) as BundleResponse;\r\n return data.values;\r\n}\r\n\r\nasync function loadEnvInternal(options?: LoadEnvOptions): Promise<Record<string, string>> {\r\n checkBrowser();\r\n const config = getEnvGodConfig(options);\r\n const timeout = options?.timeout ?? 5000;\r\n const now = Date.now();\r\n\r\n // 1. Check if we have a valid token\r\n let token = state.token;\r\n let tokenValid = token && state.tokenExpiresAt && state.tokenExpiresAt > now;\r\n\r\n // 2. If token invalid, exchange\r\n if (!tokenValid) {\r\n token = await exchangeToken(config, timeout);\r\n }\r\n\r\n // 3. If we have a cached bundle and the token is still the same/valid, return it?\r\n if (state.bundle && tokenValid) {\r\n Object.assign(process.env, state.bundle);\r\n return state.bundle;\r\n }\r\n\r\n // 4. Fetch bundle with retry logic\r\n try {\r\n const values = await fetchBundle(config, token!, timeout);\r\n state.bundle = values;\r\n Object.assign(process.env, values);\r\n return values;\r\n } catch (err: any) {\r\n if (err.message === '401') {\r\n // Retry ONCE: Re-exchange and Re-fetch\r\n state.token = null;\r\n state.bundle = null;\r\n\r\n const newToken = await exchangeToken(config, timeout);\r\n const values = await fetchBundle(config, newToken, timeout);\r\n state.bundle = values;\r\n Object.assign(process.env, values);\r\n return values;\r\n }\r\n throw err;\r\n }\r\n}\r\n\r\n/**\r\n * Main entry point to load environment variables.\r\n * Uses Singleflight pattern to prevent concurrent network requests.\r\n */\r\nexport function loadEnv(options?: LoadEnvOptions): Promise<Record<string, string>> {\r\n if (pendingLoadPromise) {\r\n return pendingLoadPromise;\r\n }\r\n\r\n pendingLoadPromise = loadEnvInternal(options)\r\n .finally(() => {\r\n pendingLoadPromise = null;\r\n });\r\n\r\n return pendingLoadPromise;\r\n}\r\n\r\nexport * from './types.js';\r\n","import 'server-only';\r\nimport { loadEnv, type LoadEnvOptions } from './index.js';\r\n\r\nexport async function loadServerEnv(options?: LoadEnvOptions) {\r\n return loadEnv(options);\r\n}\r\n\r\nexport * from './types.js';\r\n"]}
|
package/dist/next.mjs
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
|
|
3
|
+
// src/next.ts
|
|
4
|
+
|
|
5
|
+
// src/index.ts
|
|
6
|
+
var state = {
|
|
7
|
+
token: null,
|
|
8
|
+
tokenExpiresAt: null,
|
|
9
|
+
bundle: null
|
|
10
|
+
};
|
|
11
|
+
var pendingLoadPromise = null;
|
|
12
|
+
function getEnvGodConfig(options) {
|
|
13
|
+
const env = process.env;
|
|
14
|
+
const config = {
|
|
15
|
+
apiUrl: options?.config?.apiUrl ?? env.ENVGOD_API_URL,
|
|
16
|
+
apiKey: options?.config?.apiKey ?? env.ENVGOD_API_KEY,
|
|
17
|
+
project: options?.config?.project ?? env.ENVGOD_PROJECT,
|
|
18
|
+
env: options?.config?.env ?? env.ENVGOD_ENV,
|
|
19
|
+
service: options?.config?.service ?? env.ENVGOD_SERVICE
|
|
20
|
+
};
|
|
21
|
+
const missing = Object.entries(config).filter(([_, v]) => !v).map(([k]) => k);
|
|
22
|
+
if (missing.length > 0) {
|
|
23
|
+
throw new Error(`[EnvGod] Missing required configuration: ${missing.join(", ")}`);
|
|
24
|
+
}
|
|
25
|
+
return config;
|
|
26
|
+
}
|
|
27
|
+
function checkBrowser() {
|
|
28
|
+
if (typeof window !== "undefined") {
|
|
29
|
+
throw new Error("[EnvGod] Security Warning: SDK execution attempting in browser environment. This SDK is server-only.");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function fetchWithTimeout(url, init) {
|
|
33
|
+
const { timeout = 5e3, ...rest } = init;
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
const id = setTimeout(() => controller.abort(), timeout);
|
|
36
|
+
try {
|
|
37
|
+
const res = await fetch(url, { ...rest, signal: controller.signal });
|
|
38
|
+
return res;
|
|
39
|
+
} catch (err) {
|
|
40
|
+
if (err.name === "AbortError") {
|
|
41
|
+
throw new Error(`[EnvGod] Request timed out after ${timeout}ms`);
|
|
42
|
+
}
|
|
43
|
+
throw err;
|
|
44
|
+
} finally {
|
|
45
|
+
clearTimeout(id);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function exchangeToken(config, timeout) {
|
|
49
|
+
const res = await fetchWithTimeout(`${config.apiUrl}/v1/auth/exchange`, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: {
|
|
52
|
+
"Content-Type": "application/json",
|
|
53
|
+
"Authorization": `Bearer ${config.apiKey}`
|
|
54
|
+
},
|
|
55
|
+
body: JSON.stringify({
|
|
56
|
+
project: config.project,
|
|
57
|
+
env: config.env,
|
|
58
|
+
service: config.service
|
|
59
|
+
}),
|
|
60
|
+
timeout
|
|
61
|
+
});
|
|
62
|
+
if (!res.ok) {
|
|
63
|
+
throw new Error(`[EnvGod] Auth exchange failed: ${res.status} ${res.statusText}`);
|
|
64
|
+
}
|
|
65
|
+
const data = await res.json();
|
|
66
|
+
state.token = data.token;
|
|
67
|
+
state.tokenExpiresAt = new Date(data.expiresAt).getTime();
|
|
68
|
+
return data.token;
|
|
69
|
+
}
|
|
70
|
+
async function fetchBundle(config, token, timeout) {
|
|
71
|
+
const res = await fetchWithTimeout(`${config.apiUrl}/v1/bundle`, {
|
|
72
|
+
method: "GET",
|
|
73
|
+
headers: {
|
|
74
|
+
"Authorization": `Bearer ${token}`
|
|
75
|
+
},
|
|
76
|
+
timeout
|
|
77
|
+
});
|
|
78
|
+
if (res.status === 401) {
|
|
79
|
+
throw new Error("401");
|
|
80
|
+
}
|
|
81
|
+
if (!res.ok) {
|
|
82
|
+
throw new Error(`[EnvGod] Fetch bundle failed: ${res.status} ${res.statusText}`);
|
|
83
|
+
}
|
|
84
|
+
const data = await res.json();
|
|
85
|
+
return data.values;
|
|
86
|
+
}
|
|
87
|
+
async function loadEnvInternal(options) {
|
|
88
|
+
checkBrowser();
|
|
89
|
+
const config = getEnvGodConfig(options);
|
|
90
|
+
const timeout = options?.timeout ?? 5e3;
|
|
91
|
+
const now = Date.now();
|
|
92
|
+
let token = state.token;
|
|
93
|
+
let tokenValid = token && state.tokenExpiresAt && state.tokenExpiresAt > now;
|
|
94
|
+
if (!tokenValid) {
|
|
95
|
+
token = await exchangeToken(config, timeout);
|
|
96
|
+
}
|
|
97
|
+
if (state.bundle && tokenValid) {
|
|
98
|
+
Object.assign(process.env, state.bundle);
|
|
99
|
+
return state.bundle;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const values = await fetchBundle(config, token, timeout);
|
|
103
|
+
state.bundle = values;
|
|
104
|
+
Object.assign(process.env, values);
|
|
105
|
+
return values;
|
|
106
|
+
} catch (err) {
|
|
107
|
+
if (err.message === "401") {
|
|
108
|
+
state.token = null;
|
|
109
|
+
state.bundle = null;
|
|
110
|
+
const newToken = await exchangeToken(config, timeout);
|
|
111
|
+
const values = await fetchBundle(config, newToken, timeout);
|
|
112
|
+
state.bundle = values;
|
|
113
|
+
Object.assign(process.env, values);
|
|
114
|
+
return values;
|
|
115
|
+
}
|
|
116
|
+
throw err;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function loadEnv(options) {
|
|
120
|
+
if (pendingLoadPromise) {
|
|
121
|
+
return pendingLoadPromise;
|
|
122
|
+
}
|
|
123
|
+
pendingLoadPromise = loadEnvInternal(options).finally(() => {
|
|
124
|
+
pendingLoadPromise = null;
|
|
125
|
+
});
|
|
126
|
+
return pendingLoadPromise;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// src/next.ts
|
|
130
|
+
async function loadServerEnv(options) {
|
|
131
|
+
return loadEnv(options);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export { loadServerEnv };
|
|
135
|
+
//# sourceMappingURL=next.mjs.map
|
|
136
|
+
//# sourceMappingURL=next.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/next.ts"],"names":[],"mappings":"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","file":"next.mjs","sourcesContent":["import type { EnvGodConfig, LoadEnvOptions, AuthExchangeResponse, BundleResponse } from './types.js';\r\n\r\n// --- State ---\r\ninterface CacheState {\r\n token: string | null;\r\n tokenExpiresAt: number | null; // Timestamp in ms\r\n bundle: Record<string, string> | null;\r\n}\r\n\r\nconst state: CacheState = {\r\n token: null,\r\n tokenExpiresAt: null,\r\n bundle: null,\r\n};\r\n\r\nlet pendingLoadPromise: Promise<Record<string, string>> | null = null;\r\n\r\n/** @internal For testing only */\r\nexport function _resetState() {\r\n state.token = null;\r\n state.tokenExpiresAt = null;\r\n state.bundle = null;\r\n pendingLoadPromise = null;\r\n}\r\n\r\n// --- Helpers ---\r\n\r\n/**\r\n * Validates and returns the configuration.\r\n * Prioritizes options > process.env.\r\n */\r\nexport function getEnvGodConfig(options?: LoadEnvOptions): EnvGodConfig {\r\n const env = process.env;\r\n const config = {\r\n apiUrl: options?.config?.apiUrl ?? env.ENVGOD_API_URL,\r\n apiKey: options?.config?.apiKey ?? env.ENVGOD_API_KEY,\r\n project: options?.config?.project ?? env.ENVGOD_PROJECT,\r\n env: options?.config?.env ?? env.ENVGOD_ENV,\r\n service: options?.config?.service ?? env.ENVGOD_SERVICE,\r\n };\r\n\r\n const missing = Object.entries(config)\r\n .filter(([_, v]) => !v)\r\n .map(([k]) => k);\r\n\r\n if (missing.length > 0) {\r\n throw new Error(`[EnvGod] Missing required configuration: ${missing.join(', ')}`);\r\n }\r\n\r\n return config as EnvGodConfig;\r\n}\r\n\r\n/**\r\n * Checks if the current environment is a browser.\r\n */\r\nfunction checkBrowser() {\r\n if (typeof window !== 'undefined') {\r\n throw new Error('[EnvGod] Security Warning: SDK execution attempting in browser environment. This SDK is server-only.');\r\n }\r\n}\r\n\r\n/**\r\n * Fetches with timeout.\r\n */\r\nasync function fetchWithTimeout(url: string, init: RequestInit & { timeout?: number }) {\r\n const { timeout = 5000, ...rest } = init;\r\n const controller = new AbortController();\r\n const id = setTimeout(() => controller.abort(), timeout);\r\n\r\n try {\r\n const res = await fetch(url, { ...rest, signal: controller.signal });\r\n return res;\r\n } catch (err: any) {\r\n if (err.name === 'AbortError') {\r\n throw new Error(`[EnvGod] Request timed out after ${timeout}ms`);\r\n }\r\n throw err;\r\n } finally {\r\n clearTimeout(id);\r\n }\r\n}\r\n\r\n// --- Core Logic ---\r\n\r\nasync function exchangeToken(config: EnvGodConfig, timeout: number): Promise<string> {\r\n const res = await fetchWithTimeout(`${config.apiUrl}/v1/auth/exchange`, {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n 'Authorization': `Bearer ${config.apiKey}`,\r\n },\r\n body: JSON.stringify({\r\n project: config.project,\r\n env: config.env,\r\n service: config.service,\r\n }),\r\n timeout,\r\n });\r\n\r\n if (!res.ok) {\r\n throw new Error(`[EnvGod] Auth exchange failed: ${res.status} ${res.statusText}`);\r\n }\r\n\r\n const data = (await res.json()) as AuthExchangeResponse;\r\n state.token = data.token;\r\n state.tokenExpiresAt = new Date(data.expiresAt).getTime();\r\n return data.token;\r\n}\r\n\r\nasync function fetchBundle(config: EnvGodConfig, token: string, timeout: number): Promise<Record<string, string>> {\r\n const res = await fetchWithTimeout(`${config.apiUrl}/v1/bundle`, {\r\n method: 'GET',\r\n headers: {\r\n 'Authorization': `Bearer ${token}`,\r\n },\r\n timeout,\r\n });\r\n\r\n if (res.status === 401) {\r\n throw new Error('401'); // Signal to retry\r\n }\r\n\r\n if (!res.ok) {\r\n throw new Error(`[EnvGod] Fetch bundle failed: ${res.status} ${res.statusText}`);\r\n }\r\n\r\n const data = (await res.json()) as BundleResponse;\r\n return data.values;\r\n}\r\n\r\nasync function loadEnvInternal(options?: LoadEnvOptions): Promise<Record<string, string>> {\r\n checkBrowser();\r\n const config = getEnvGodConfig(options);\r\n const timeout = options?.timeout ?? 5000;\r\n const now = Date.now();\r\n\r\n // 1. Check if we have a valid token\r\n let token = state.token;\r\n let tokenValid = token && state.tokenExpiresAt && state.tokenExpiresAt > now;\r\n\r\n // 2. If token invalid, exchange\r\n if (!tokenValid) {\r\n token = await exchangeToken(config, timeout);\r\n }\r\n\r\n // 3. If we have a cached bundle and the token is still the same/valid, return it?\r\n if (state.bundle && tokenValid) {\r\n Object.assign(process.env, state.bundle);\r\n return state.bundle;\r\n }\r\n\r\n // 4. Fetch bundle with retry logic\r\n try {\r\n const values = await fetchBundle(config, token!, timeout);\r\n state.bundle = values;\r\n Object.assign(process.env, values);\r\n return values;\r\n } catch (err: any) {\r\n if (err.message === '401') {\r\n // Retry ONCE: Re-exchange and Re-fetch\r\n state.token = null;\r\n state.bundle = null;\r\n\r\n const newToken = await exchangeToken(config, timeout);\r\n const values = await fetchBundle(config, newToken, timeout);\r\n state.bundle = values;\r\n Object.assign(process.env, values);\r\n return values;\r\n }\r\n throw err;\r\n }\r\n}\r\n\r\n/**\r\n * Main entry point to load environment variables.\r\n * Uses Singleflight pattern to prevent concurrent network requests.\r\n */\r\nexport function loadEnv(options?: LoadEnvOptions): Promise<Record<string, string>> {\r\n if (pendingLoadPromise) {\r\n return pendingLoadPromise;\r\n }\r\n\r\n pendingLoadPromise = loadEnvInternal(options)\r\n .finally(() => {\r\n pendingLoadPromise = null;\r\n });\r\n\r\n return pendingLoadPromise;\r\n}\r\n\r\nexport * from './types.js';\r\n","import 'server-only';\r\nimport { loadEnv, type LoadEnvOptions } from './index.js';\r\n\r\nexport async function loadServerEnv(options?: LoadEnvOptions) {\r\n return loadEnv(options);\r\n}\r\n\r\nexport * from './types.js';\r\n"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
interface EnvGodConfig {
|
|
2
|
+
apiUrl: string;
|
|
3
|
+
apiKey: string;
|
|
4
|
+
project: string;
|
|
5
|
+
env: string;
|
|
6
|
+
service: string;
|
|
7
|
+
}
|
|
8
|
+
interface LoadEnvOptions {
|
|
9
|
+
/** Override default configuration */
|
|
10
|
+
config?: Partial<EnvGodConfig>;
|
|
11
|
+
/** Timeout in milliseconds (default: 5000) */
|
|
12
|
+
timeout?: number;
|
|
13
|
+
}
|
|
14
|
+
interface AuthExchangeResponse {
|
|
15
|
+
token: string;
|
|
16
|
+
expiresAt: string;
|
|
17
|
+
}
|
|
18
|
+
interface BundleResponse {
|
|
19
|
+
values: Record<string, string>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type { AuthExchangeResponse as A, BundleResponse as B, EnvGodConfig as E, LoadEnvOptions as L };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
interface EnvGodConfig {
|
|
2
|
+
apiUrl: string;
|
|
3
|
+
apiKey: string;
|
|
4
|
+
project: string;
|
|
5
|
+
env: string;
|
|
6
|
+
service: string;
|
|
7
|
+
}
|
|
8
|
+
interface LoadEnvOptions {
|
|
9
|
+
/** Override default configuration */
|
|
10
|
+
config?: Partial<EnvGodConfig>;
|
|
11
|
+
/** Timeout in milliseconds (default: 5000) */
|
|
12
|
+
timeout?: number;
|
|
13
|
+
}
|
|
14
|
+
interface AuthExchangeResponse {
|
|
15
|
+
token: string;
|
|
16
|
+
expiresAt: string;
|
|
17
|
+
}
|
|
18
|
+
interface BundleResponse {
|
|
19
|
+
values: Record<string, string>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type { AuthExchangeResponse as A, BundleResponse as B, EnvGodConfig as E, LoadEnvOptions as L };
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rusamer/envgod",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Secure Node.js/Next.js SDK for EnvGod",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/rubric-labs/envgod-sdk"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"module": "./dist/index.mjs",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.mjs",
|
|
16
|
+
"require": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./next": {
|
|
19
|
+
"types": "./dist/next.d.ts",
|
|
20
|
+
"import": "./dist/next.mjs",
|
|
21
|
+
"require": "./dist/next.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsup",
|
|
34
|
+
"test": "vitest run",
|
|
35
|
+
"prepublishOnly": "npm run build"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"env",
|
|
39
|
+
"secrets",
|
|
40
|
+
"nextjs",
|
|
41
|
+
"security",
|
|
42
|
+
"configuration"
|
|
43
|
+
],
|
|
44
|
+
"author": "",
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"sideEffects": false,
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^20.0.0",
|
|
49
|
+
"tsup": "^8.0.0",
|
|
50
|
+
"typescript": "^5.0.0",
|
|
51
|
+
"undici": "^6.0.0",
|
|
52
|
+
"vitest": "^1.0.0"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"server-only": "^0.0.1"
|
|
56
|
+
}
|
|
57
|
+
}
|