@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 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 Persistance**: Secrets are held in memory. Restarting the server will trigger a fresh fetch.
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 state = {
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
- state.token = null;
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
- 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(", ")}`);
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 state = {
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
- state.token = null;
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
- 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(", ")}`);
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);
@@ -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":";;;AAUA,IAAM,KAAA,uBAAY,GAAA,EAAwB;AAC1C,IAAI,kBAAA,GAA6D,IAAA;AAG1D,SAAS,WAAA,GAAc;AAC1B,EAAA,KAAA,CAAM,KAAA,EAAM;AACZ,EAAA,kBAAA,GAAqB,IAAA;AACzB;AAQO,SAAS,gBAAgB,OAAA,EAAwC;AACpE,EAAA,MAAM,MAAM,OAAA,CAAQ,GAAA;AACpB,EAAA,MAAM,MAAA,GAAS;AAAA,IACX,MAAA,EAAQ,OAAA,EAAS,MAAA,EAAQ,MAAA,IAAU,GAAA,CAAI,cAAA;AAAA,IACvC,MAAA,EAAQ,OAAA,EAAS,MAAA,EAAQ,MAAA,IAAU,GAAA,CAAI,cAAA;AAAA,IACvC,OAAA,EAAS,OAAA,EAAS,MAAA,EAAQ,OAAA,IAAW,GAAA,CAAI,cAAA;AAAA,IACzC,GAAA,EAAK,OAAA,EAAS,MAAA,EAAQ,GAAA,IAAO,GAAA,CAAI,UAAA;AAAA,IACjC,OAAA,EAAS,OAAA,EAAS,MAAA,EAAQ,OAAA,IAAW,GAAA,CAAI;AAAA,GAC7C;AAEA,EAAA,IAAI,CAAC,OAAO,MAAA,EAAQ;AAChB,IAAA,MAAM,IAAI,MAAM,8DAA8D,CAAA;AAAA,EAClF;AAEA,EAAA,OAAO,MAAA;AACX;AAKA,SAAS,YAAA,GAAe;AACpB,EAAA,IAAI,OAAO,WAAW,WAAA,EAAa;AAC/B,IAAA,MAAM,IAAI,MAAM,sGAAsG,CAAA;AAAA,EAC1H;AACJ;AAKA,eAAe,gBAAA,CAAiB,KAAa,IAAA,EAA0C;AACnF,EAAA,MAAM,EAAE,OAAA,GAAU,GAAA,EAAM,GAAG,MAAK,GAAI,IAAA;AACpC,EAAA,MAAM,UAAA,GAAa,IAAI,eAAA,EAAgB;AACvC,EAAA,MAAM,KAAK,UAAA,CAAW,MAAM,UAAA,CAAW,KAAA,IAAS,OAAO,CAAA;AAEvD,EAAA,IAAI;AACA,IAAA,MAAM,GAAA,GAAM,MAAM,KAAA,CAAM,GAAA,EAAK,EAAE,GAAG,IAAA,EAAM,MAAA,EAAQ,UAAA,CAAW,MAAA,EAAQ,CAAA;AACnE,IAAA,OAAO,GAAA;AAAA,EACX,SAAS,GAAA,EAAU;AACf,IAAA,IAAI,GAAA,CAAI,SAAS,YAAA,EAAc;AAC3B,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,iCAAA,EAAoC,OAAO,CAAA,EAAA,CAAI,CAAA;AAAA,IACnE;AACA,IAAA,MAAM,GAAA;AAAA,EACV,CAAA,SAAE;AACE,IAAA,YAAA,CAAa,EAAE,CAAA;AAAA,EACnB;AACJ;AAIA,eAAe,aAAA,CAAc,MAAA,EAAsB,OAAA,EAAiB,KAAA,EAAoC;AACpG,EAAA,MAAM,MAAM,MAAM,gBAAA,CAAiB,CAAA,EAAG,MAAA,CAAO,MAAM,CAAA,iBAAA,CAAA,EAAqB;AAAA,IACpE,MAAA,EAAQ,MAAA;AAAA,IACR,OAAA,EAAS;AAAA,MACL,cAAA,EAAgB,kBAAA;AAAA,MAChB,eAAA,EAAiB,CAAA,OAAA,EAAU,MAAA,CAAO,MAAM,CAAA;AAAA,KAC5C;AAAA,IACA,IAAA,EAAM,KAAK,SAAA,CAAU;AAAA,MACjB,SAAS,MAAA,CAAO,OAAA;AAAA,MAChB,KAAK,MAAA,CAAO,GAAA;AAAA,MACZ,SAAS,MAAA,CAAO;AAAA,KACnB,CAAA;AAAA,IACD;AAAA,GACH,CAAA;AAED,EAAA,IAAI,CAAC,IAAI,EAAA,EAAI;AACT,IAAA,MAAM,IAAI,MAAM,CAAA,+BAAA,EAAkC,GAAA,CAAI,MAAM,CAAA,CAAA,EAAI,GAAA,CAAI,UAAU,CAAA,CAAE,CAAA;AAAA,EACpF;AAEA,EAAA,MAAM,IAAA,GAAQ,MAAM,GAAA,CAAI,IAAA,EAAK;AAC7B,EAAA,KAAA,CAAM,QAAQ,IAAA,CAAK,KAAA;AACnB,EAAA,KAAA,CAAM,iBAAiB,IAAI,IAAA,CAAK,IAAA,CAAK,SAAS,EAAE,OAAA,EAAQ;AACxD,EAAA,OAAO,IAAA,CAAK,KAAA;AAChB;AAEA,IAAM,cAAA,GAAiB,YAAA;AACvB,IAAM,cAAA,GAAiB,SAAA;AAEvB,eAAe,cAAA,GAAyC;AACpD,EAAA,MAAM,IAAA,GAAO,MAAM,MAAA,CAAO,WAAA,CAAY,gBAAgB,cAAc,CAAA;AACpE,EAAA,IAAI,CAAC,MAAM,OAAO,IAAA;AAClB,EAAA,IAAI;AACA,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA;AAE9B,IAAA,OAAO,MAAA,CAAO,WAAA,IAAe,MAAA,CAAO,YAAA,IAAgB,IAAA;AAAA,EACxD,CAAA,CAAA,MAAQ;AACJ,IAAA,OAAO,IAAA;AAAA,EACX;AACJ;AAEA,eAAe,yBAAA,CAA0B,MAAA,EAAsB,SAAA,EAAmB,OAAA,EAAkD;AAChI,EAAA,MAAM,MAAM,MAAM,gBAAA,CAAiB,CAAA,EAAG,MAAA,CAAO,MAAM,CAAA,WAAA,CAAA,EAAe;AAAA,IAC9D,MAAA,EAAQ,MAAA;AAAA,IACR,OAAA,EAAS;AAAA,MACL,cAAA,EAAgB,kBAAA;AAAA,MAChB,eAAA,EAAiB,UAAU,SAAS,CAAA;AAAA,KACxC;AAAA,IACA,IAAA,EAAM,KAAK,SAAA,CAAU;AAAA,MACjB,SAAS,MAAA,CAAO,OAAA;AAAA,MAChB,KAAK,MAAA,CAAO,GAAA;AAAA,MACZ,SAAS,MAAA,CAAO;AAAA,KACnB,CAAA;AAAA,IACD;AAAA,GACH,CAAA;AAED,EAAA,IAAI,CAAC,IAAI,EAAA,EAAI;AACT,IAAA,MAAM,IAAI,MAAM,CAAA,kCAAA,EAAqC,GAAA,CAAI,MAAM,CAAA,CAAA,EAAI,GAAA,CAAI,UAAU,CAAA,CAAE,CAAA;AAAA,EACvF;AAEA,EAAA,MAAM,IAAA,GAAQ,MAAM,GAAA,CAAI,IAAA,EAAK;AAC7B,EAAA,OAAO,IAAA,CAAK,MAAA;AAChB;AAEA,eAAe,WAAA,CAAY,MAAA,EAAsB,KAAA,EAAe,OAAA,EAAkD;AAC9G,EAAA,MAAM,MAAM,MAAM,gBAAA,CAAiB,CAAA,EAAG,MAAA,CAAO,MAAM,CAAA,UAAA,CAAA,EAAc;AAAA,IAC7D,MAAA,EAAQ,KAAA;AAAA,IACR,OAAA,EAAS;AAAA,MACL,eAAA,EAAiB,UAAU,KAAK,CAAA;AAAA,KACpC;AAAA,IACA;AAAA,GACH,CAAA;AAED,EAAA,IAAI,GAAA,CAAI,WAAW,GAAA,EAAK;AACpB,IAAA,MAAM,IAAI,MAAM,KAAK,CAAA;AAAA,EACzB;AAEA,EAAA,IAAI,CAAC,IAAI,EAAA,EAAI;AACT,IAAA,MAAM,IAAI,MAAM,CAAA,8BAAA,EAAiC,GAAA,CAAI,MAAM,CAAA,CAAA,EAAI,GAAA,CAAI,UAAU,CAAA,CAAE,CAAA;AAAA,EACnF;AAEA,EAAA,MAAM,IAAA,GAAQ,MAAM,GAAA,CAAI,IAAA,EAAK;AAC7B,EAAA,OAAO,IAAA,CAAK,MAAA;AAChB;AAEA,SAAS,qBAAqB,MAAA,EAA8B;AACxD,EAAA,MAAM,SAAA,GAAY,OAAO,MAAA,GAAS,MAAA,CAAO,OAAO,SAAA,CAAU,CAAA,EAAG,CAAC,CAAA,GAAI,aAAA;AAClE,EAAA,OAAO,CAAC,MAAA,CAAO,MAAA,EAAQ,SAAA,EAAW,MAAA,CAAO,OAAA,EAAS,MAAA,CAAO,GAAA,EAAK,MAAA,CAAO,OAAO,CAAA,CAAE,IAAA,CAAK,GAAG,CAAA;AAC1F;AAEA,eAAe,gBAAgB,OAAA,EAA2D;AACtF,EAAA,YAAA,EAAa;AACb,EAAA,MAAM,MAAA,GAAS,gBAAgB,OAAO,CAAA;AAGtC,EAAA,IAAI,CAAC,OAAO,MAAA,EAAQ;AAChB,IAAA,MAAM,SAAA,GAAY,MAAM,cAAA,EAAe;AACvC,IAAA,IAAI,CAAC,SAAA,EAAW;AACZ,MAAA,MAAM,IAAI,MAAM,oDAAoD,CAAA;AAAA,IACxE;AACA,IAAA,MAAM,SAAS,MAAM,yBAAA,CAA0B,QAAQ,SAAA,EAAW,OAAA,EAAS,WAAW,GAAI,CAAA;AAC1F,IAAA,MAAA,CAAO,MAAA,CAAO,OAAA,CAAQ,GAAA,EAAK,MAAM,CAAA;AACjC,IAAA,OAAO,MAAA;AAAA,EACX;AAEA,EAAA,MAAM,OAAA,GAAU,SAAS,OAAA,IAAW,GAAA;AACpC,EAAA,MAAM,GAAA,GAAM,KAAK,GAAA,EAAI;AACrB,EAAA,MAAM,gBAAgB,EAAA,GAAK,GAAA;AAE3B,EAAA,MAAM,WAAA,GAAc,qBAAqB,MAAM,CAAA;AAC/C,EAAA,IAAI,CAAC,KAAA,CAAM,GAAA,CAAI,WAAW,CAAA,EAAG;AACzB,IAAA,KAAA,CAAM,GAAA,CAAI,aAAa,EAAE,KAAA,EAAO,MAAM,cAAA,EAAgB,IAAA,EAAM,MAAA,EAAQ,IAAA,EAAM,CAAA;AAAA,EAC9E;AACA,EAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,GAAA,CAAI,WAAW,CAAA;AAEnC,EAAA,IAAI,QAAQ,KAAA,CAAM,KAAA;AAClB,EAAA,IAAI,aAAa,KAAA,IAAS,KAAA,CAAM,cAAA,IAAkB,KAAA,CAAM,iBAAkB,GAAA,GAAM,aAAA;AAEhF,EAAA,IAAI,CAAC,UAAA,EAAY;AACb,IAAA,KAAA,GAAQ,MAAM,aAAA,CAAc,MAAA,EAAQ,OAAA,EAAS,KAAK,CAAA;AAAA,EACtD;AAEA,EAAA,IAAI,KAAA,CAAM,UAAU,UAAA,EAAY;AAC5B,IAAA,MAAA,CAAO,MAAA,CAAO,OAAA,CAAQ,GAAA,EAAK,KAAA,CAAM,MAAM,CAAA;AACvC,IAAA,OAAO,KAAA,CAAM,MAAA;AAAA,EACjB;AAEA,EAAA,IAAI;AACA,IAAA,MAAM,MAAA,GAAS,MAAM,WAAA,CAAY,MAAA,EAAQ,OAAQ,OAAO,CAAA;AACxD,IAAA,KAAA,CAAM,MAAA,GAAS,MAAA;AACf,IAAA,MAAA,CAAO,MAAA,CAAO,OAAA,CAAQ,GAAA,EAAK,MAAM,CAAA;AACjC,IAAA,OAAO,MAAA;AAAA,EACX,SAAS,GAAA,EAAU;AACf,IAAA,IAAI,GAAA,CAAI,YAAY,KAAA,EAAO;AACvB,MAAA,KAAA,CAAM,KAAA,GAAQ,IAAA;AACd,MAAA,KAAA,CAAM,MAAA,GAAS,IAAA;AACf,MAAA,MAAM,QAAA,GAAW,MAAM,aAAA,CAAc,MAAA,EAAQ,SAAS,KAAK,CAAA;AAC3D,MAAA,MAAM,MAAA,GAAS,MAAM,WAAA,CAAY,MAAA,EAAQ,UAAU,OAAO,CAAA;AAC1D,MAAA,KAAA,CAAM,MAAA,GAAS,MAAA;AACf,MAAA,MAAA,CAAO,MAAA,CAAO,OAAA,CAAQ,GAAA,EAAK,MAAM,CAAA;AACjC,MAAA,OAAO,MAAA;AAAA,IACX;AACA,IAAA,MAAM,GAAA;AAAA,EACV;AACJ;AAMO,SAAS,QAAQ,OAAA,EAA2D;AAC/E,EAAA,IAAI,kBAAA,EAAoB;AACpB,IAAA,OAAO,kBAAA;AAAA,EACX;AAEA,EAAA,kBAAA,GAAqB,eAAA,CAAgB,OAAO,CAAA,CACvC,OAAA,CAAQ,MAAM;AACX,IAAA,kBAAA,GAAqB,IAAA;AAAA,EACzB,CAAC,CAAA;AAEL,EAAA,OAAO,kBAAA;AACX","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
- // src/next.ts
6
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
7
+
8
+ var keytar__default = /*#__PURE__*/_interopDefault(keytar);
6
9
 
7
- // src/index.ts
8
- var state = {
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
- 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(", ")}`);
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
- 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(", ")}`);
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.1",
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.0.0",
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
+ }