@rusamer/envgod 0.0.1 → 0.0.4
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 +9 -1
- package/dist/index.js +65 -15
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +61 -15
- package/dist/index.mjs.map +1 -1
- package/dist/next.js +63 -14
- package/dist/next.js.map +1 -1
- package/dist/next.mjs +59 -14
- package/dist/next.mjs.map +1 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -33,6 +33,14 @@ The SDK automatically reads the following environment variables:
|
|
|
33
33
|
| `ENVGOD_ENV` | Environment Name (e.g., prod) |
|
|
34
34
|
| `ENVGOD_SERVICE` | Service Name |
|
|
35
35
|
|
|
36
|
+
```env
|
|
37
|
+
ENVGOD_API_URL=https://api.example.com
|
|
38
|
+
ENVGOD_API_KEY=sk_xxx
|
|
39
|
+
ENVGOD_PROJECT=myapp
|
|
40
|
+
ENVGOD_ENV=prod
|
|
41
|
+
ENVGOD_SERVICE=web
|
|
42
|
+
```
|
|
43
|
+
|
|
36
44
|
## Usage
|
|
37
45
|
|
|
38
46
|
### Node.js
|
|
@@ -76,7 +84,7 @@ export default async function Page() {
|
|
|
76
84
|
## Security Notes
|
|
77
85
|
|
|
78
86
|
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
|
|
87
|
+
2. **No Persistence**: Secrets are held in memory. Restarting the server will trigger a fresh fetch.
|
|
80
88
|
3. **Logs**: The SDK does not log secret values.
|
|
81
89
|
|
|
82
90
|
## For Maintainers
|
package/dist/index.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var keytar = require('keytar');
|
|
4
|
+
|
|
5
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
6
|
+
|
|
7
|
+
var keytar__default = /*#__PURE__*/_interopDefault(keytar);
|
|
8
|
+
|
|
3
9
|
// src/index.ts
|
|
4
|
-
var
|
|
5
|
-
token: null,
|
|
6
|
-
tokenExpiresAt: null,
|
|
7
|
-
bundle: null
|
|
8
|
-
};
|
|
10
|
+
var cache = /* @__PURE__ */ new Map();
|
|
9
11
|
var pendingLoadPromise = null;
|
|
10
12
|
function _resetState() {
|
|
11
|
-
|
|
12
|
-
state.tokenExpiresAt = null;
|
|
13
|
-
state.bundle = null;
|
|
13
|
+
cache.clear();
|
|
14
14
|
pendingLoadPromise = null;
|
|
15
15
|
}
|
|
16
16
|
function getEnvGodConfig(options) {
|
|
@@ -22,9 +22,8 @@ function getEnvGodConfig(options) {
|
|
|
22
22
|
env: options?.config?.env ?? env.ENVGOD_ENV,
|
|
23
23
|
service: options?.config?.service ?? env.ENVGOD_SERVICE
|
|
24
24
|
};
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
throw new Error(`[EnvGod] Missing required configuration: ${missing.join(", ")}`);
|
|
25
|
+
if (!config.apiUrl) {
|
|
26
|
+
throw new Error("[EnvGod] Missing required configuration: apiUrl is required.");
|
|
28
27
|
}
|
|
29
28
|
return config;
|
|
30
29
|
}
|
|
@@ -49,7 +48,7 @@ async function fetchWithTimeout(url, init) {
|
|
|
49
48
|
clearTimeout(id);
|
|
50
49
|
}
|
|
51
50
|
}
|
|
52
|
-
async function exchangeToken(config, timeout) {
|
|
51
|
+
async function exchangeToken(config, timeout, state) {
|
|
53
52
|
const res = await fetchWithTimeout(`${config.apiUrl}/v1/auth/exchange`, {
|
|
54
53
|
method: "POST",
|
|
55
54
|
headers: {
|
|
@@ -71,6 +70,38 @@ async function exchangeToken(config, timeout) {
|
|
|
71
70
|
state.tokenExpiresAt = new Date(data.expiresAt).getTime();
|
|
72
71
|
return data.token;
|
|
73
72
|
}
|
|
73
|
+
var KEYTAR_SERVICE = "envgod-cli";
|
|
74
|
+
var KEYTAR_ACCOUNT = "default";
|
|
75
|
+
async function getAccessToken() {
|
|
76
|
+
const data = await keytar__default.default.getPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNT);
|
|
77
|
+
if (!data) return null;
|
|
78
|
+
try {
|
|
79
|
+
const parsed = JSON.parse(data);
|
|
80
|
+
return parsed.accessToken || parsed.refreshToken || null;
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function fetchSecretsWithUserToken(config, userToken, timeout) {
|
|
86
|
+
const res = await fetchWithTimeout(`${config.apiUrl}/v1/secrets`, {
|
|
87
|
+
method: "POST",
|
|
88
|
+
headers: {
|
|
89
|
+
"Content-Type": "application/json",
|
|
90
|
+
"Authorization": `Bearer ${userToken}`
|
|
91
|
+
},
|
|
92
|
+
body: JSON.stringify({
|
|
93
|
+
project: config.project,
|
|
94
|
+
env: config.env,
|
|
95
|
+
service: config.service
|
|
96
|
+
}),
|
|
97
|
+
timeout
|
|
98
|
+
});
|
|
99
|
+
if (!res.ok) {
|
|
100
|
+
throw new Error(`[EnvGod] Failed to fetch secrets: ${res.status} ${res.statusText}`);
|
|
101
|
+
}
|
|
102
|
+
const data = await res.json();
|
|
103
|
+
return data.values;
|
|
104
|
+
}
|
|
74
105
|
async function fetchBundle(config, token, timeout) {
|
|
75
106
|
const res = await fetchWithTimeout(`${config.apiUrl}/v1/bundle`, {
|
|
76
107
|
method: "GET",
|
|
@@ -88,15 +119,34 @@ async function fetchBundle(config, token, timeout) {
|
|
|
88
119
|
const data = await res.json();
|
|
89
120
|
return data.values;
|
|
90
121
|
}
|
|
122
|
+
function getConfigFingerprint(config) {
|
|
123
|
+
const keyPrefix = config.apiKey ? config.apiKey.substring(0, 8) : "interactive";
|
|
124
|
+
return [config.apiUrl, keyPrefix, config.project, config.env, config.service].join("|");
|
|
125
|
+
}
|
|
91
126
|
async function loadEnvInternal(options) {
|
|
92
127
|
checkBrowser();
|
|
93
128
|
const config = getEnvGodConfig(options);
|
|
129
|
+
if (!config.apiKey) {
|
|
130
|
+
const userToken = await getAccessToken();
|
|
131
|
+
if (!userToken) {
|
|
132
|
+
throw new Error("[EnvGod] Not logged in. Please run `envgod login`.");
|
|
133
|
+
}
|
|
134
|
+
const values = await fetchSecretsWithUserToken(config, userToken, options?.timeout ?? 5e3);
|
|
135
|
+
Object.assign(process.env, values);
|
|
136
|
+
return values;
|
|
137
|
+
}
|
|
94
138
|
const timeout = options?.timeout ?? 5e3;
|
|
95
139
|
const now = Date.now();
|
|
140
|
+
const TOKEN_SKEW_MS = 30 * 1e3;
|
|
141
|
+
const fingerprint = getConfigFingerprint(config);
|
|
142
|
+
if (!cache.has(fingerprint)) {
|
|
143
|
+
cache.set(fingerprint, { token: null, tokenExpiresAt: null, bundle: null });
|
|
144
|
+
}
|
|
145
|
+
const state = cache.get(fingerprint);
|
|
96
146
|
let token = state.token;
|
|
97
|
-
let tokenValid = token && state.tokenExpiresAt && state.tokenExpiresAt > now;
|
|
147
|
+
let tokenValid = token && state.tokenExpiresAt && state.tokenExpiresAt > now + TOKEN_SKEW_MS;
|
|
98
148
|
if (!tokenValid) {
|
|
99
|
-
token = await exchangeToken(config, timeout);
|
|
149
|
+
token = await exchangeToken(config, timeout, state);
|
|
100
150
|
}
|
|
101
151
|
if (state.bundle && tokenValid) {
|
|
102
152
|
Object.assign(process.env, state.bundle);
|
|
@@ -111,7 +161,7 @@ async function loadEnvInternal(options) {
|
|
|
111
161
|
if (err.message === "401") {
|
|
112
162
|
state.token = null;
|
|
113
163
|
state.bundle = null;
|
|
114
|
-
const newToken = await exchangeToken(config, timeout);
|
|
164
|
+
const newToken = await exchangeToken(config, timeout, state);
|
|
115
165
|
const values = await fetchBundle(config, newToken, timeout);
|
|
116
166
|
state.bundle = values;
|
|
117
167
|
Object.assign(process.env, values);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +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"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["keytar"],"mappings":"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","file":"index.js","sourcesContent":["import keytar from 'keytar';\r\nimport 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 cache = new Map<string, CacheState>();\r\nlet pendingLoadPromise: Promise<Record<string, string>> | null = null;\r\n\r\n/** @internal For testing only */\r\nexport function _resetState() {\r\n cache.clear();\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 if (!config.apiUrl) {\r\n throw new Error('[EnvGod] Missing required configuration: apiUrl is required.');\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, state: CacheState): 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\nconst KEYTAR_SERVICE = 'envgod-cli';\r\nconst KEYTAR_ACCOUNT = 'default';\r\n\r\nasync function getAccessToken(): Promise<string | null> {\r\n const data = await keytar.getPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNT);\r\n if (!data) return null;\r\n try {\r\n const parsed = JSON.parse(data);\r\n // The CLI's device flow currently stores the access token in both slots.\r\n return parsed.accessToken || parsed.refreshToken || null;\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\nasync function fetchSecretsWithUserToken(config: EnvGodConfig, userToken: string, timeout: number): Promise<Record<string, string>> {\r\n const res = await fetchWithTimeout(`${config.apiUrl}/v1/secrets`, {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n 'Authorization': `Bearer ${userToken}`,\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] Failed to fetch secrets: ${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 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\nfunction getConfigFingerprint(config: EnvGodConfig): string {\r\n const keyPrefix = config.apiKey ? config.apiKey.substring(0, 8) : 'interactive';\r\n return [config.apiUrl, keyPrefix, config.project, config.env, config.service].join('|');\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\r\n // Interactive mode: Use user token if no API key is provided\r\n if (!config.apiKey) {\r\n const userToken = await getAccessToken();\r\n if (!userToken) {\r\n throw new Error('[EnvGod] Not logged in. Please run `envgod login`.');\r\n }\r\n const values = await fetchSecretsWithUserToken(config, userToken, options?.timeout ?? 5000);\r\n Object.assign(process.env, values);\r\n return values;\r\n }\r\n\r\n const timeout = options?.timeout ?? 5000;\r\n const now = Date.now();\r\n const TOKEN_SKEW_MS = 30 * 1000; // 30 seconds\r\n\r\n const fingerprint = getConfigFingerprint(config);\r\n if (!cache.has(fingerprint)) {\r\n cache.set(fingerprint, { token: null, tokenExpiresAt: null, bundle: null });\r\n }\r\n const state = cache.get(fingerprint)!;\r\n\r\n let token = state.token;\r\n let tokenValid = token && state.tokenExpiresAt && state.tokenExpiresAt > (now + TOKEN_SKEW_MS);\r\n\r\n if (!tokenValid) {\r\n token = await exchangeToken(config, timeout, state);\r\n }\r\n\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 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 state.token = null;\r\n state.bundle = null;\r\n const newToken = await exchangeToken(config, timeout, state);\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
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
|
+
import keytar from 'keytar';
|
|
2
|
+
|
|
1
3
|
// src/index.ts
|
|
2
|
-
var
|
|
3
|
-
token: null,
|
|
4
|
-
tokenExpiresAt: null,
|
|
5
|
-
bundle: null
|
|
6
|
-
};
|
|
4
|
+
var cache = /* @__PURE__ */ new Map();
|
|
7
5
|
var pendingLoadPromise = null;
|
|
8
6
|
function _resetState() {
|
|
9
|
-
|
|
10
|
-
state.tokenExpiresAt = null;
|
|
11
|
-
state.bundle = null;
|
|
7
|
+
cache.clear();
|
|
12
8
|
pendingLoadPromise = null;
|
|
13
9
|
}
|
|
14
10
|
function getEnvGodConfig(options) {
|
|
@@ -20,9 +16,8 @@ function getEnvGodConfig(options) {
|
|
|
20
16
|
env: options?.config?.env ?? env.ENVGOD_ENV,
|
|
21
17
|
service: options?.config?.service ?? env.ENVGOD_SERVICE
|
|
22
18
|
};
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
throw new Error(`[EnvGod] Missing required configuration: ${missing.join(", ")}`);
|
|
19
|
+
if (!config.apiUrl) {
|
|
20
|
+
throw new Error("[EnvGod] Missing required configuration: apiUrl is required.");
|
|
26
21
|
}
|
|
27
22
|
return config;
|
|
28
23
|
}
|
|
@@ -47,7 +42,7 @@ async function fetchWithTimeout(url, init) {
|
|
|
47
42
|
clearTimeout(id);
|
|
48
43
|
}
|
|
49
44
|
}
|
|
50
|
-
async function exchangeToken(config, timeout) {
|
|
45
|
+
async function exchangeToken(config, timeout, state) {
|
|
51
46
|
const res = await fetchWithTimeout(`${config.apiUrl}/v1/auth/exchange`, {
|
|
52
47
|
method: "POST",
|
|
53
48
|
headers: {
|
|
@@ -69,6 +64,38 @@ async function exchangeToken(config, timeout) {
|
|
|
69
64
|
state.tokenExpiresAt = new Date(data.expiresAt).getTime();
|
|
70
65
|
return data.token;
|
|
71
66
|
}
|
|
67
|
+
var KEYTAR_SERVICE = "envgod-cli";
|
|
68
|
+
var KEYTAR_ACCOUNT = "default";
|
|
69
|
+
async function getAccessToken() {
|
|
70
|
+
const data = await keytar.getPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNT);
|
|
71
|
+
if (!data) return null;
|
|
72
|
+
try {
|
|
73
|
+
const parsed = JSON.parse(data);
|
|
74
|
+
return parsed.accessToken || parsed.refreshToken || null;
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async function fetchSecretsWithUserToken(config, userToken, timeout) {
|
|
80
|
+
const res = await fetchWithTimeout(`${config.apiUrl}/v1/secrets`, {
|
|
81
|
+
method: "POST",
|
|
82
|
+
headers: {
|
|
83
|
+
"Content-Type": "application/json",
|
|
84
|
+
"Authorization": `Bearer ${userToken}`
|
|
85
|
+
},
|
|
86
|
+
body: JSON.stringify({
|
|
87
|
+
project: config.project,
|
|
88
|
+
env: config.env,
|
|
89
|
+
service: config.service
|
|
90
|
+
}),
|
|
91
|
+
timeout
|
|
92
|
+
});
|
|
93
|
+
if (!res.ok) {
|
|
94
|
+
throw new Error(`[EnvGod] Failed to fetch secrets: ${res.status} ${res.statusText}`);
|
|
95
|
+
}
|
|
96
|
+
const data = await res.json();
|
|
97
|
+
return data.values;
|
|
98
|
+
}
|
|
72
99
|
async function fetchBundle(config, token, timeout) {
|
|
73
100
|
const res = await fetchWithTimeout(`${config.apiUrl}/v1/bundle`, {
|
|
74
101
|
method: "GET",
|
|
@@ -86,15 +113,34 @@ async function fetchBundle(config, token, timeout) {
|
|
|
86
113
|
const data = await res.json();
|
|
87
114
|
return data.values;
|
|
88
115
|
}
|
|
116
|
+
function getConfigFingerprint(config) {
|
|
117
|
+
const keyPrefix = config.apiKey ? config.apiKey.substring(0, 8) : "interactive";
|
|
118
|
+
return [config.apiUrl, keyPrefix, config.project, config.env, config.service].join("|");
|
|
119
|
+
}
|
|
89
120
|
async function loadEnvInternal(options) {
|
|
90
121
|
checkBrowser();
|
|
91
122
|
const config = getEnvGodConfig(options);
|
|
123
|
+
if (!config.apiKey) {
|
|
124
|
+
const userToken = await getAccessToken();
|
|
125
|
+
if (!userToken) {
|
|
126
|
+
throw new Error("[EnvGod] Not logged in. Please run `envgod login`.");
|
|
127
|
+
}
|
|
128
|
+
const values = await fetchSecretsWithUserToken(config, userToken, options?.timeout ?? 5e3);
|
|
129
|
+
Object.assign(process.env, values);
|
|
130
|
+
return values;
|
|
131
|
+
}
|
|
92
132
|
const timeout = options?.timeout ?? 5e3;
|
|
93
133
|
const now = Date.now();
|
|
134
|
+
const TOKEN_SKEW_MS = 30 * 1e3;
|
|
135
|
+
const fingerprint = getConfigFingerprint(config);
|
|
136
|
+
if (!cache.has(fingerprint)) {
|
|
137
|
+
cache.set(fingerprint, { token: null, tokenExpiresAt: null, bundle: null });
|
|
138
|
+
}
|
|
139
|
+
const state = cache.get(fingerprint);
|
|
94
140
|
let token = state.token;
|
|
95
|
-
let tokenValid = token && state.tokenExpiresAt && state.tokenExpiresAt > now;
|
|
141
|
+
let tokenValid = token && state.tokenExpiresAt && state.tokenExpiresAt > now + TOKEN_SKEW_MS;
|
|
96
142
|
if (!tokenValid) {
|
|
97
|
-
token = await exchangeToken(config, timeout);
|
|
143
|
+
token = await exchangeToken(config, timeout, state);
|
|
98
144
|
}
|
|
99
145
|
if (state.bundle && tokenValid) {
|
|
100
146
|
Object.assign(process.env, state.bundle);
|
|
@@ -109,7 +155,7 @@ async function loadEnvInternal(options) {
|
|
|
109
155
|
if (err.message === "401") {
|
|
110
156
|
state.token = null;
|
|
111
157
|
state.bundle = null;
|
|
112
|
-
const newToken = await exchangeToken(config, timeout);
|
|
158
|
+
const newToken = await exchangeToken(config, timeout, state);
|
|
113
159
|
const values = await fetchBundle(config, newToken, timeout);
|
|
114
160
|
state.bundle = values;
|
|
115
161
|
Object.assign(process.env, values);
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +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"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["import keytar from 'keytar';\r\nimport 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 cache = new Map<string, CacheState>();\r\nlet pendingLoadPromise: Promise<Record<string, string>> | null = null;\r\n\r\n/** @internal For testing only */\r\nexport function _resetState() {\r\n cache.clear();\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 if (!config.apiUrl) {\r\n throw new Error('[EnvGod] Missing required configuration: apiUrl is required.');\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, state: CacheState): 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\nconst KEYTAR_SERVICE = 'envgod-cli';\r\nconst KEYTAR_ACCOUNT = 'default';\r\n\r\nasync function getAccessToken(): Promise<string | null> {\r\n const data = await keytar.getPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNT);\r\n if (!data) return null;\r\n try {\r\n const parsed = JSON.parse(data);\r\n // The CLI's device flow currently stores the access token in both slots.\r\n return parsed.accessToken || parsed.refreshToken || null;\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\nasync function fetchSecretsWithUserToken(config: EnvGodConfig, userToken: string, timeout: number): Promise<Record<string, string>> {\r\n const res = await fetchWithTimeout(`${config.apiUrl}/v1/secrets`, {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n 'Authorization': `Bearer ${userToken}`,\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] Failed to fetch secrets: ${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 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\nfunction getConfigFingerprint(config: EnvGodConfig): string {\r\n const keyPrefix = config.apiKey ? config.apiKey.substring(0, 8) : 'interactive';\r\n return [config.apiUrl, keyPrefix, config.project, config.env, config.service].join('|');\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\r\n // Interactive mode: Use user token if no API key is provided\r\n if (!config.apiKey) {\r\n const userToken = await getAccessToken();\r\n if (!userToken) {\r\n throw new Error('[EnvGod] Not logged in. Please run `envgod login`.');\r\n }\r\n const values = await fetchSecretsWithUserToken(config, userToken, options?.timeout ?? 5000);\r\n Object.assign(process.env, values);\r\n return values;\r\n }\r\n\r\n const timeout = options?.timeout ?? 5000;\r\n const now = Date.now();\r\n const TOKEN_SKEW_MS = 30 * 1000; // 30 seconds\r\n\r\n const fingerprint = getConfigFingerprint(config);\r\n if (!cache.has(fingerprint)) {\r\n cache.set(fingerprint, { token: null, tokenExpiresAt: null, bundle: null });\r\n }\r\n const state = cache.get(fingerprint)!;\r\n\r\n let token = state.token;\r\n let tokenValid = token && state.tokenExpiresAt && state.tokenExpiresAt > (now + TOKEN_SKEW_MS);\r\n\r\n if (!tokenValid) {\r\n token = await exchangeToken(config, timeout, state);\r\n }\r\n\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 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 state.token = null;\r\n state.bundle = null;\r\n const newToken = await exchangeToken(config, timeout, state);\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.js
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
require('server-only');
|
|
4
|
+
var keytar = require('keytar');
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
7
|
+
|
|
8
|
+
var keytar__default = /*#__PURE__*/_interopDefault(keytar);
|
|
6
9
|
|
|
7
|
-
// src/
|
|
8
|
-
var
|
|
9
|
-
token: null,
|
|
10
|
-
tokenExpiresAt: null,
|
|
11
|
-
bundle: null
|
|
12
|
-
};
|
|
10
|
+
// src/next.ts
|
|
11
|
+
var cache = /* @__PURE__ */ new Map();
|
|
13
12
|
var pendingLoadPromise = null;
|
|
14
13
|
function getEnvGodConfig(options) {
|
|
15
14
|
const env = process.env;
|
|
@@ -20,9 +19,8 @@ function getEnvGodConfig(options) {
|
|
|
20
19
|
env: options?.config?.env ?? env.ENVGOD_ENV,
|
|
21
20
|
service: options?.config?.service ?? env.ENVGOD_SERVICE
|
|
22
21
|
};
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
throw new Error(`[EnvGod] Missing required configuration: ${missing.join(", ")}`);
|
|
22
|
+
if (!config.apiUrl) {
|
|
23
|
+
throw new Error("[EnvGod] Missing required configuration: apiUrl is required.");
|
|
26
24
|
}
|
|
27
25
|
return config;
|
|
28
26
|
}
|
|
@@ -47,7 +45,7 @@ async function fetchWithTimeout(url, init) {
|
|
|
47
45
|
clearTimeout(id);
|
|
48
46
|
}
|
|
49
47
|
}
|
|
50
|
-
async function exchangeToken(config, timeout) {
|
|
48
|
+
async function exchangeToken(config, timeout, state) {
|
|
51
49
|
const res = await fetchWithTimeout(`${config.apiUrl}/v1/auth/exchange`, {
|
|
52
50
|
method: "POST",
|
|
53
51
|
headers: {
|
|
@@ -69,6 +67,38 @@ async function exchangeToken(config, timeout) {
|
|
|
69
67
|
state.tokenExpiresAt = new Date(data.expiresAt).getTime();
|
|
70
68
|
return data.token;
|
|
71
69
|
}
|
|
70
|
+
var KEYTAR_SERVICE = "envgod-cli";
|
|
71
|
+
var KEYTAR_ACCOUNT = "default";
|
|
72
|
+
async function getAccessToken() {
|
|
73
|
+
const data = await keytar__default.default.getPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNT);
|
|
74
|
+
if (!data) return null;
|
|
75
|
+
try {
|
|
76
|
+
const parsed = JSON.parse(data);
|
|
77
|
+
return parsed.accessToken || parsed.refreshToken || null;
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function fetchSecretsWithUserToken(config, userToken, timeout) {
|
|
83
|
+
const res = await fetchWithTimeout(`${config.apiUrl}/v1/secrets`, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: {
|
|
86
|
+
"Content-Type": "application/json",
|
|
87
|
+
"Authorization": `Bearer ${userToken}`
|
|
88
|
+
},
|
|
89
|
+
body: JSON.stringify({
|
|
90
|
+
project: config.project,
|
|
91
|
+
env: config.env,
|
|
92
|
+
service: config.service
|
|
93
|
+
}),
|
|
94
|
+
timeout
|
|
95
|
+
});
|
|
96
|
+
if (!res.ok) {
|
|
97
|
+
throw new Error(`[EnvGod] Failed to fetch secrets: ${res.status} ${res.statusText}`);
|
|
98
|
+
}
|
|
99
|
+
const data = await res.json();
|
|
100
|
+
return data.values;
|
|
101
|
+
}
|
|
72
102
|
async function fetchBundle(config, token, timeout) {
|
|
73
103
|
const res = await fetchWithTimeout(`${config.apiUrl}/v1/bundle`, {
|
|
74
104
|
method: "GET",
|
|
@@ -86,15 +116,34 @@ async function fetchBundle(config, token, timeout) {
|
|
|
86
116
|
const data = await res.json();
|
|
87
117
|
return data.values;
|
|
88
118
|
}
|
|
119
|
+
function getConfigFingerprint(config) {
|
|
120
|
+
const keyPrefix = config.apiKey ? config.apiKey.substring(0, 8) : "interactive";
|
|
121
|
+
return [config.apiUrl, keyPrefix, config.project, config.env, config.service].join("|");
|
|
122
|
+
}
|
|
89
123
|
async function loadEnvInternal(options) {
|
|
90
124
|
checkBrowser();
|
|
91
125
|
const config = getEnvGodConfig(options);
|
|
126
|
+
if (!config.apiKey) {
|
|
127
|
+
const userToken = await getAccessToken();
|
|
128
|
+
if (!userToken) {
|
|
129
|
+
throw new Error("[EnvGod] Not logged in. Please run `envgod login`.");
|
|
130
|
+
}
|
|
131
|
+
const values = await fetchSecretsWithUserToken(config, userToken, options?.timeout ?? 5e3);
|
|
132
|
+
Object.assign(process.env, values);
|
|
133
|
+
return values;
|
|
134
|
+
}
|
|
92
135
|
const timeout = options?.timeout ?? 5e3;
|
|
93
136
|
const now = Date.now();
|
|
137
|
+
const TOKEN_SKEW_MS = 30 * 1e3;
|
|
138
|
+
const fingerprint = getConfigFingerprint(config);
|
|
139
|
+
if (!cache.has(fingerprint)) {
|
|
140
|
+
cache.set(fingerprint, { token: null, tokenExpiresAt: null, bundle: null });
|
|
141
|
+
}
|
|
142
|
+
const state = cache.get(fingerprint);
|
|
94
143
|
let token = state.token;
|
|
95
|
-
let tokenValid = token && state.tokenExpiresAt && state.tokenExpiresAt > now;
|
|
144
|
+
let tokenValid = token && state.tokenExpiresAt && state.tokenExpiresAt > now + TOKEN_SKEW_MS;
|
|
96
145
|
if (!tokenValid) {
|
|
97
|
-
token = await exchangeToken(config, timeout);
|
|
146
|
+
token = await exchangeToken(config, timeout, state);
|
|
98
147
|
}
|
|
99
148
|
if (state.bundle && tokenValid) {
|
|
100
149
|
Object.assign(process.env, state.bundle);
|
|
@@ -109,7 +158,7 @@ async function loadEnvInternal(options) {
|
|
|
109
158
|
if (err.message === "401") {
|
|
110
159
|
state.token = null;
|
|
111
160
|
state.bundle = null;
|
|
112
|
-
const newToken = await exchangeToken(config, timeout);
|
|
161
|
+
const newToken = await exchangeToken(config, timeout, state);
|
|
113
162
|
const values = await fetchBundle(config, newToken, timeout);
|
|
114
163
|
state.bundle = values;
|
|
115
164
|
Object.assign(process.env, values);
|
package/dist/next.js.map
CHANGED
|
@@ -1 +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"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/next.ts"],"names":["keytar"],"mappings":"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","file":"next.js","sourcesContent":["import keytar from 'keytar';\r\nimport 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 cache = new Map<string, CacheState>();\r\nlet pendingLoadPromise: Promise<Record<string, string>> | null = null;\r\n\r\n/** @internal For testing only */\r\nexport function _resetState() {\r\n cache.clear();\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 if (!config.apiUrl) {\r\n throw new Error('[EnvGod] Missing required configuration: apiUrl is required.');\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, state: CacheState): 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\nconst KEYTAR_SERVICE = 'envgod-cli';\r\nconst KEYTAR_ACCOUNT = 'default';\r\n\r\nasync function getAccessToken(): Promise<string | null> {\r\n const data = await keytar.getPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNT);\r\n if (!data) return null;\r\n try {\r\n const parsed = JSON.parse(data);\r\n // The CLI's device flow currently stores the access token in both slots.\r\n return parsed.accessToken || parsed.refreshToken || null;\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\nasync function fetchSecretsWithUserToken(config: EnvGodConfig, userToken: string, timeout: number): Promise<Record<string, string>> {\r\n const res = await fetchWithTimeout(`${config.apiUrl}/v1/secrets`, {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n 'Authorization': `Bearer ${userToken}`,\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] Failed to fetch secrets: ${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 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\nfunction getConfigFingerprint(config: EnvGodConfig): string {\r\n const keyPrefix = config.apiKey ? config.apiKey.substring(0, 8) : 'interactive';\r\n return [config.apiUrl, keyPrefix, config.project, config.env, config.service].join('|');\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\r\n // Interactive mode: Use user token if no API key is provided\r\n if (!config.apiKey) {\r\n const userToken = await getAccessToken();\r\n if (!userToken) {\r\n throw new Error('[EnvGod] Not logged in. Please run `envgod login`.');\r\n }\r\n const values = await fetchSecretsWithUserToken(config, userToken, options?.timeout ?? 5000);\r\n Object.assign(process.env, values);\r\n return values;\r\n }\r\n\r\n const timeout = options?.timeout ?? 5000;\r\n const now = Date.now();\r\n const TOKEN_SKEW_MS = 30 * 1000; // 30 seconds\r\n\r\n const fingerprint = getConfigFingerprint(config);\r\n if (!cache.has(fingerprint)) {\r\n cache.set(fingerprint, { token: null, tokenExpiresAt: null, bundle: null });\r\n }\r\n const state = cache.get(fingerprint)!;\r\n\r\n let token = state.token;\r\n let tokenValid = token && state.tokenExpiresAt && state.tokenExpiresAt > (now + TOKEN_SKEW_MS);\r\n\r\n if (!tokenValid) {\r\n token = await exchangeToken(config, timeout, state);\r\n }\r\n\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 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 state.token = null;\r\n state.bundle = null;\r\n const newToken = await exchangeToken(config, timeout, state);\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
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
import 'server-only';
|
|
2
|
+
import keytar from 'keytar';
|
|
2
3
|
|
|
3
4
|
// src/next.ts
|
|
4
|
-
|
|
5
|
-
// src/index.ts
|
|
6
|
-
var state = {
|
|
7
|
-
token: null,
|
|
8
|
-
tokenExpiresAt: null,
|
|
9
|
-
bundle: null
|
|
10
|
-
};
|
|
5
|
+
var cache = /* @__PURE__ */ new Map();
|
|
11
6
|
var pendingLoadPromise = null;
|
|
12
7
|
function getEnvGodConfig(options) {
|
|
13
8
|
const env = process.env;
|
|
@@ -18,9 +13,8 @@ function getEnvGodConfig(options) {
|
|
|
18
13
|
env: options?.config?.env ?? env.ENVGOD_ENV,
|
|
19
14
|
service: options?.config?.service ?? env.ENVGOD_SERVICE
|
|
20
15
|
};
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
throw new Error(`[EnvGod] Missing required configuration: ${missing.join(", ")}`);
|
|
16
|
+
if (!config.apiUrl) {
|
|
17
|
+
throw new Error("[EnvGod] Missing required configuration: apiUrl is required.");
|
|
24
18
|
}
|
|
25
19
|
return config;
|
|
26
20
|
}
|
|
@@ -45,7 +39,7 @@ async function fetchWithTimeout(url, init) {
|
|
|
45
39
|
clearTimeout(id);
|
|
46
40
|
}
|
|
47
41
|
}
|
|
48
|
-
async function exchangeToken(config, timeout) {
|
|
42
|
+
async function exchangeToken(config, timeout, state) {
|
|
49
43
|
const res = await fetchWithTimeout(`${config.apiUrl}/v1/auth/exchange`, {
|
|
50
44
|
method: "POST",
|
|
51
45
|
headers: {
|
|
@@ -67,6 +61,38 @@ async function exchangeToken(config, timeout) {
|
|
|
67
61
|
state.tokenExpiresAt = new Date(data.expiresAt).getTime();
|
|
68
62
|
return data.token;
|
|
69
63
|
}
|
|
64
|
+
var KEYTAR_SERVICE = "envgod-cli";
|
|
65
|
+
var KEYTAR_ACCOUNT = "default";
|
|
66
|
+
async function getAccessToken() {
|
|
67
|
+
const data = await keytar.getPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNT);
|
|
68
|
+
if (!data) return null;
|
|
69
|
+
try {
|
|
70
|
+
const parsed = JSON.parse(data);
|
|
71
|
+
return parsed.accessToken || parsed.refreshToken || null;
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function fetchSecretsWithUserToken(config, userToken, timeout) {
|
|
77
|
+
const res = await fetchWithTimeout(`${config.apiUrl}/v1/secrets`, {
|
|
78
|
+
method: "POST",
|
|
79
|
+
headers: {
|
|
80
|
+
"Content-Type": "application/json",
|
|
81
|
+
"Authorization": `Bearer ${userToken}`
|
|
82
|
+
},
|
|
83
|
+
body: JSON.stringify({
|
|
84
|
+
project: config.project,
|
|
85
|
+
env: config.env,
|
|
86
|
+
service: config.service
|
|
87
|
+
}),
|
|
88
|
+
timeout
|
|
89
|
+
});
|
|
90
|
+
if (!res.ok) {
|
|
91
|
+
throw new Error(`[EnvGod] Failed to fetch secrets: ${res.status} ${res.statusText}`);
|
|
92
|
+
}
|
|
93
|
+
const data = await res.json();
|
|
94
|
+
return data.values;
|
|
95
|
+
}
|
|
70
96
|
async function fetchBundle(config, token, timeout) {
|
|
71
97
|
const res = await fetchWithTimeout(`${config.apiUrl}/v1/bundle`, {
|
|
72
98
|
method: "GET",
|
|
@@ -84,15 +110,34 @@ async function fetchBundle(config, token, timeout) {
|
|
|
84
110
|
const data = await res.json();
|
|
85
111
|
return data.values;
|
|
86
112
|
}
|
|
113
|
+
function getConfigFingerprint(config) {
|
|
114
|
+
const keyPrefix = config.apiKey ? config.apiKey.substring(0, 8) : "interactive";
|
|
115
|
+
return [config.apiUrl, keyPrefix, config.project, config.env, config.service].join("|");
|
|
116
|
+
}
|
|
87
117
|
async function loadEnvInternal(options) {
|
|
88
118
|
checkBrowser();
|
|
89
119
|
const config = getEnvGodConfig(options);
|
|
120
|
+
if (!config.apiKey) {
|
|
121
|
+
const userToken = await getAccessToken();
|
|
122
|
+
if (!userToken) {
|
|
123
|
+
throw new Error("[EnvGod] Not logged in. Please run `envgod login`.");
|
|
124
|
+
}
|
|
125
|
+
const values = await fetchSecretsWithUserToken(config, userToken, options?.timeout ?? 5e3);
|
|
126
|
+
Object.assign(process.env, values);
|
|
127
|
+
return values;
|
|
128
|
+
}
|
|
90
129
|
const timeout = options?.timeout ?? 5e3;
|
|
91
130
|
const now = Date.now();
|
|
131
|
+
const TOKEN_SKEW_MS = 30 * 1e3;
|
|
132
|
+
const fingerprint = getConfigFingerprint(config);
|
|
133
|
+
if (!cache.has(fingerprint)) {
|
|
134
|
+
cache.set(fingerprint, { token: null, tokenExpiresAt: null, bundle: null });
|
|
135
|
+
}
|
|
136
|
+
const state = cache.get(fingerprint);
|
|
92
137
|
let token = state.token;
|
|
93
|
-
let tokenValid = token && state.tokenExpiresAt && state.tokenExpiresAt > now;
|
|
138
|
+
let tokenValid = token && state.tokenExpiresAt && state.tokenExpiresAt > now + TOKEN_SKEW_MS;
|
|
94
139
|
if (!tokenValid) {
|
|
95
|
-
token = await exchangeToken(config, timeout);
|
|
140
|
+
token = await exchangeToken(config, timeout, state);
|
|
96
141
|
}
|
|
97
142
|
if (state.bundle && tokenValid) {
|
|
98
143
|
Object.assign(process.env, state.bundle);
|
|
@@ -107,7 +152,7 @@ async function loadEnvInternal(options) {
|
|
|
107
152
|
if (err.message === "401") {
|
|
108
153
|
state.token = null;
|
|
109
154
|
state.bundle = null;
|
|
110
|
-
const newToken = await exchangeToken(config, timeout);
|
|
155
|
+
const newToken = await exchangeToken(config, timeout, state);
|
|
111
156
|
const values = await fetchBundle(config, newToken, timeout);
|
|
112
157
|
state.bundle = values;
|
|
113
158
|
Object.assign(process.env, values);
|
package/dist/next.mjs.map
CHANGED
|
@@ -1 +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"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/next.ts"],"names":[],"mappings":"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","file":"next.mjs","sourcesContent":["import keytar from 'keytar';\r\nimport 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 cache = new Map<string, CacheState>();\r\nlet pendingLoadPromise: Promise<Record<string, string>> | null = null;\r\n\r\n/** @internal For testing only */\r\nexport function _resetState() {\r\n cache.clear();\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 if (!config.apiUrl) {\r\n throw new Error('[EnvGod] Missing required configuration: apiUrl is required.');\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, state: CacheState): 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\nconst KEYTAR_SERVICE = 'envgod-cli';\r\nconst KEYTAR_ACCOUNT = 'default';\r\n\r\nasync function getAccessToken(): Promise<string | null> {\r\n const data = await keytar.getPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNT);\r\n if (!data) return null;\r\n try {\r\n const parsed = JSON.parse(data);\r\n // The CLI's device flow currently stores the access token in both slots.\r\n return parsed.accessToken || parsed.refreshToken || null;\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\nasync function fetchSecretsWithUserToken(config: EnvGodConfig, userToken: string, timeout: number): Promise<Record<string, string>> {\r\n const res = await fetchWithTimeout(`${config.apiUrl}/v1/secrets`, {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n 'Authorization': `Bearer ${userToken}`,\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] Failed to fetch secrets: ${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 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\nfunction getConfigFingerprint(config: EnvGodConfig): string {\r\n const keyPrefix = config.apiKey ? config.apiKey.substring(0, 8) : 'interactive';\r\n return [config.apiUrl, keyPrefix, config.project, config.env, config.service].join('|');\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\r\n // Interactive mode: Use user token if no API key is provided\r\n if (!config.apiKey) {\r\n const userToken = await getAccessToken();\r\n if (!userToken) {\r\n throw new Error('[EnvGod] Not logged in. Please run `envgod login`.');\r\n }\r\n const values = await fetchSecretsWithUserToken(config, userToken, options?.timeout ?? 5000);\r\n Object.assign(process.env, values);\r\n return values;\r\n }\r\n\r\n const timeout = options?.timeout ?? 5000;\r\n const now = Date.now();\r\n const TOKEN_SKEW_MS = 30 * 1000; // 30 seconds\r\n\r\n const fingerprint = getConfigFingerprint(config);\r\n if (!cache.has(fingerprint)) {\r\n cache.set(fingerprint, { token: null, tokenExpiresAt: null, bundle: null });\r\n }\r\n const state = cache.get(fingerprint)!;\r\n\r\n let token = state.token;\r\n let tokenValid = token && state.tokenExpiresAt && state.tokenExpiresAt > (now + TOKEN_SKEW_MS);\r\n\r\n if (!tokenValid) {\r\n token = await exchangeToken(config, timeout, state);\r\n }\r\n\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 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 state.token = null;\r\n state.bundle = null;\r\n const newToken = await exchangeToken(config, timeout, state);\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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rusamer/envgod",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "Secure Node.js/Next.js SDK for EnvGod",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -45,13 +45,14 @@
|
|
|
45
45
|
"license": "MIT",
|
|
46
46
|
"sideEffects": false,
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@types/node": "^20.
|
|
48
|
+
"@types/node": "^20.19.27",
|
|
49
49
|
"tsup": "^8.0.0",
|
|
50
50
|
"typescript": "^5.0.0",
|
|
51
51
|
"undici": "^6.0.0",
|
|
52
52
|
"vitest": "^1.0.0"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
+
"keytar": "^7.9.0",
|
|
55
56
|
"server-only": "^0.0.1"
|
|
56
57
|
}
|
|
57
|
-
}
|
|
58
|
+
}
|