@rusamer/envgod 0.0.1 → 0.0.3

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,14 @@
1
1
  'use strict';
2
2
 
3
+ var os = require('os');
4
+ var path = require('path');
5
+ var fs = require('fs');
6
+
3
7
  // src/index.ts
4
- var state = {
5
- token: null,
6
- tokenExpiresAt: null,
7
- bundle: null
8
- };
8
+ var cache = /* @__PURE__ */ new Map();
9
9
  var pendingLoadPromise = null;
10
10
  function _resetState() {
11
- state.token = null;
12
- state.tokenExpiresAt = null;
13
- state.bundle = null;
11
+ cache.clear();
14
12
  pendingLoadPromise = null;
15
13
  }
16
14
  function getEnvGodConfig(options) {
@@ -22,9 +20,8 @@ function getEnvGodConfig(options) {
22
20
  env: options?.config?.env ?? env.ENVGOD_ENV,
23
21
  service: options?.config?.service ?? env.ENVGOD_SERVICE
24
22
  };
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(", ")}`);
23
+ if (!config.apiUrl) {
24
+ throw new Error("[EnvGod] Missing required configuration: apiUrl is required.");
28
25
  }
29
26
  return config;
30
27
  }
@@ -49,7 +46,7 @@ async function fetchWithTimeout(url, init) {
49
46
  clearTimeout(id);
50
47
  }
51
48
  }
52
- async function exchangeToken(config, timeout) {
49
+ async function exchangeToken(config, timeout, state) {
53
50
  const res = await fetchWithTimeout(`${config.apiUrl}/v1/auth/exchange`, {
54
51
  method: "POST",
55
52
  headers: {
@@ -71,6 +68,36 @@ async function exchangeToken(config, timeout) {
71
68
  state.tokenExpiresAt = new Date(data.expiresAt).getTime();
72
69
  return data.token;
73
70
  }
71
+ async function readUserToken() {
72
+ const tokenPath = path.join(os.homedir(), ".envgod", "token.json");
73
+ try {
74
+ const content = await fs.promises.readFile(tokenPath, "utf-8");
75
+ const data = JSON.parse(content);
76
+ return data?.token || null;
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+ async function fetchSecretsWithUserToken(config, userToken, timeout) {
82
+ const res = await fetchWithTimeout(`${config.apiUrl}/v1/secrets`, {
83
+ method: "POST",
84
+ headers: {
85
+ "Content-Type": "application/json",
86
+ "Authorization": `Bearer ${userToken}`
87
+ },
88
+ body: JSON.stringify({
89
+ project: config.project,
90
+ env: config.env,
91
+ service: config.service
92
+ }),
93
+ timeout
94
+ });
95
+ if (!res.ok) {
96
+ throw new Error(`[EnvGod] Failed to fetch secrets: ${res.status} ${res.statusText}`);
97
+ }
98
+ const data = await res.json();
99
+ return data.values;
100
+ }
74
101
  async function fetchBundle(config, token, timeout) {
75
102
  const res = await fetchWithTimeout(`${config.apiUrl}/v1/bundle`, {
76
103
  method: "GET",
@@ -88,15 +115,34 @@ async function fetchBundle(config, token, timeout) {
88
115
  const data = await res.json();
89
116
  return data.values;
90
117
  }
118
+ function getConfigFingerprint(config) {
119
+ const keyPrefix = config.apiKey ? config.apiKey.substring(0, 8) : "interactive";
120
+ return [config.apiUrl, keyPrefix, config.project, config.env, config.service].join("|");
121
+ }
91
122
  async function loadEnvInternal(options) {
92
123
  checkBrowser();
93
124
  const config = getEnvGodConfig(options);
125
+ if (!config.apiKey) {
126
+ const userToken = await readUserToken();
127
+ if (!userToken) {
128
+ throw new Error("[EnvGod] Not logged in. Please run `envgod login`.");
129
+ }
130
+ const values = await fetchSecretsWithUserToken(config, userToken, options?.timeout ?? 5e3);
131
+ Object.assign(process.env, values);
132
+ return values;
133
+ }
94
134
  const timeout = options?.timeout ?? 5e3;
95
135
  const now = Date.now();
136
+ const TOKEN_SKEW_MS = 30 * 1e3;
137
+ const fingerprint = getConfigFingerprint(config);
138
+ if (!cache.has(fingerprint)) {
139
+ cache.set(fingerprint, { token: null, tokenExpiresAt: null, bundle: null });
140
+ }
141
+ const state = cache.get(fingerprint);
96
142
  let token = state.token;
97
- let tokenValid = token && state.tokenExpiresAt && state.tokenExpiresAt > now;
143
+ let tokenValid = token && state.tokenExpiresAt && state.tokenExpiresAt > now + TOKEN_SKEW_MS;
98
144
  if (!tokenValid) {
99
- token = await exchangeToken(config, timeout);
145
+ token = await exchangeToken(config, timeout, state);
100
146
  }
101
147
  if (state.bundle && tokenValid) {
102
148
  Object.assign(process.env, state.bundle);
@@ -111,7 +157,7 @@ async function loadEnvInternal(options) {
111
157
  if (err.message === "401") {
112
158
  state.token = null;
113
159
  state.bundle = null;
114
- const newToken = await exchangeToken(config, timeout);
160
+ const newToken = await exchangeToken(config, timeout, state);
115
161
  const values = await fetchBundle(config, newToken, timeout);
116
162
  state.bundle = values;
117
163
  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":["join","homedir","fs"],"mappings":"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","file":"index.js","sourcesContent":["import { homedir } from 'os';\r\nimport { join } from 'path';\r\nimport { promises as fs } from 'fs';\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\nasync function readUserToken(): Promise<string | null> {\r\n const tokenPath = join(homedir(), '.envgod', 'token.json');\r\n try {\r\n const content = await fs.readFile(tokenPath, 'utf-8');\r\n const data = JSON.parse(content);\r\n return data?.token || 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 readUserToken();\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,12 @@
1
+ import { homedir } from 'os';
2
+ import { join } from 'path';
3
+ import { promises } from 'fs';
4
+
1
5
  // src/index.ts
2
- var state = {
3
- token: null,
4
- tokenExpiresAt: null,
5
- bundle: null
6
- };
6
+ var cache = /* @__PURE__ */ new Map();
7
7
  var pendingLoadPromise = null;
8
8
  function _resetState() {
9
- state.token = null;
10
- state.tokenExpiresAt = null;
11
- state.bundle = null;
9
+ cache.clear();
12
10
  pendingLoadPromise = null;
13
11
  }
14
12
  function getEnvGodConfig(options) {
@@ -20,9 +18,8 @@ function getEnvGodConfig(options) {
20
18
  env: options?.config?.env ?? env.ENVGOD_ENV,
21
19
  service: options?.config?.service ?? env.ENVGOD_SERVICE
22
20
  };
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(", ")}`);
21
+ if (!config.apiUrl) {
22
+ throw new Error("[EnvGod] Missing required configuration: apiUrl is required.");
26
23
  }
27
24
  return config;
28
25
  }
@@ -47,7 +44,7 @@ async function fetchWithTimeout(url, init) {
47
44
  clearTimeout(id);
48
45
  }
49
46
  }
50
- async function exchangeToken(config, timeout) {
47
+ async function exchangeToken(config, timeout, state) {
51
48
  const res = await fetchWithTimeout(`${config.apiUrl}/v1/auth/exchange`, {
52
49
  method: "POST",
53
50
  headers: {
@@ -69,6 +66,36 @@ async function exchangeToken(config, timeout) {
69
66
  state.tokenExpiresAt = new Date(data.expiresAt).getTime();
70
67
  return data.token;
71
68
  }
69
+ async function readUserToken() {
70
+ const tokenPath = join(homedir(), ".envgod", "token.json");
71
+ try {
72
+ const content = await promises.readFile(tokenPath, "utf-8");
73
+ const data = JSON.parse(content);
74
+ return data?.token || 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 readUserToken();
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":["fs"],"mappings":"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","file":"index.mjs","sourcesContent":["import { homedir } from 'os';\r\nimport { join } from 'path';\r\nimport { promises as fs } from 'fs';\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\nasync function readUserToken(): Promise<string | null> {\r\n const tokenPath = join(homedir(), '.envgod', 'token.json');\r\n try {\r\n const content = await fs.readFile(tokenPath, 'utf-8');\r\n const data = JSON.parse(content);\r\n return data?.token || 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 readUserToken();\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,12 @@
1
1
  'use strict';
2
2
 
3
3
  require('server-only');
4
+ var os = require('os');
5
+ var path = require('path');
6
+ var fs = require('fs');
4
7
 
5
8
  // src/next.ts
6
-
7
- // src/index.ts
8
- var state = {
9
- token: null,
10
- tokenExpiresAt: null,
11
- bundle: null
12
- };
9
+ var cache = /* @__PURE__ */ new Map();
13
10
  var pendingLoadPromise = null;
14
11
  function getEnvGodConfig(options) {
15
12
  const env = process.env;
@@ -20,9 +17,8 @@ function getEnvGodConfig(options) {
20
17
  env: options?.config?.env ?? env.ENVGOD_ENV,
21
18
  service: options?.config?.service ?? env.ENVGOD_SERVICE
22
19
  };
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(", ")}`);
20
+ if (!config.apiUrl) {
21
+ throw new Error("[EnvGod] Missing required configuration: apiUrl is required.");
26
22
  }
27
23
  return config;
28
24
  }
@@ -47,7 +43,7 @@ async function fetchWithTimeout(url, init) {
47
43
  clearTimeout(id);
48
44
  }
49
45
  }
50
- async function exchangeToken(config, timeout) {
46
+ async function exchangeToken(config, timeout, state) {
51
47
  const res = await fetchWithTimeout(`${config.apiUrl}/v1/auth/exchange`, {
52
48
  method: "POST",
53
49
  headers: {
@@ -69,6 +65,36 @@ async function exchangeToken(config, timeout) {
69
65
  state.tokenExpiresAt = new Date(data.expiresAt).getTime();
70
66
  return data.token;
71
67
  }
68
+ async function readUserToken() {
69
+ const tokenPath = path.join(os.homedir(), ".envgod", "token.json");
70
+ try {
71
+ const content = await fs.promises.readFile(tokenPath, "utf-8");
72
+ const data = JSON.parse(content);
73
+ return data?.token || null;
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+ async function fetchSecretsWithUserToken(config, userToken, timeout) {
79
+ const res = await fetchWithTimeout(`${config.apiUrl}/v1/secrets`, {
80
+ method: "POST",
81
+ headers: {
82
+ "Content-Type": "application/json",
83
+ "Authorization": `Bearer ${userToken}`
84
+ },
85
+ body: JSON.stringify({
86
+ project: config.project,
87
+ env: config.env,
88
+ service: config.service
89
+ }),
90
+ timeout
91
+ });
92
+ if (!res.ok) {
93
+ throw new Error(`[EnvGod] Failed to fetch secrets: ${res.status} ${res.statusText}`);
94
+ }
95
+ const data = await res.json();
96
+ return data.values;
97
+ }
72
98
  async function fetchBundle(config, token, timeout) {
73
99
  const res = await fetchWithTimeout(`${config.apiUrl}/v1/bundle`, {
74
100
  method: "GET",
@@ -86,15 +112,34 @@ async function fetchBundle(config, token, timeout) {
86
112
  const data = await res.json();
87
113
  return data.values;
88
114
  }
115
+ function getConfigFingerprint(config) {
116
+ const keyPrefix = config.apiKey ? config.apiKey.substring(0, 8) : "interactive";
117
+ return [config.apiUrl, keyPrefix, config.project, config.env, config.service].join("|");
118
+ }
89
119
  async function loadEnvInternal(options) {
90
120
  checkBrowser();
91
121
  const config = getEnvGodConfig(options);
122
+ if (!config.apiKey) {
123
+ const userToken = await readUserToken();
124
+ if (!userToken) {
125
+ throw new Error("[EnvGod] Not logged in. Please run `envgod login`.");
126
+ }
127
+ const values = await fetchSecretsWithUserToken(config, userToken, options?.timeout ?? 5e3);
128
+ Object.assign(process.env, values);
129
+ return values;
130
+ }
92
131
  const timeout = options?.timeout ?? 5e3;
93
132
  const now = Date.now();
133
+ const TOKEN_SKEW_MS = 30 * 1e3;
134
+ const fingerprint = getConfigFingerprint(config);
135
+ if (!cache.has(fingerprint)) {
136
+ cache.set(fingerprint, { token: null, tokenExpiresAt: null, bundle: null });
137
+ }
138
+ const state = cache.get(fingerprint);
94
139
  let token = state.token;
95
- let tokenValid = token && state.tokenExpiresAt && state.tokenExpiresAt > now;
140
+ let tokenValid = token && state.tokenExpiresAt && state.tokenExpiresAt > now + TOKEN_SKEW_MS;
96
141
  if (!tokenValid) {
97
- token = await exchangeToken(config, timeout);
142
+ token = await exchangeToken(config, timeout, state);
98
143
  }
99
144
  if (state.bundle && tokenValid) {
100
145
  Object.assign(process.env, state.bundle);
@@ -109,7 +154,7 @@ async function loadEnvInternal(options) {
109
154
  if (err.message === "401") {
110
155
  state.token = null;
111
156
  state.bundle = null;
112
- const newToken = await exchangeToken(config, timeout);
157
+ const newToken = await exchangeToken(config, timeout, state);
113
158
  const values = await fetchBundle(config, newToken, timeout);
114
159
  state.bundle = values;
115
160
  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":["join","homedir","fs"],"mappings":"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","file":"next.js","sourcesContent":["import { homedir } from 'os';\r\nimport { join } from 'path';\r\nimport { promises as fs } from 'fs';\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\nasync function readUserToken(): Promise<string | null> {\r\n const tokenPath = join(homedir(), '.envgod', 'token.json');\r\n try {\r\n const content = await fs.readFile(tokenPath, 'utf-8');\r\n const data = JSON.parse(content);\r\n return data?.token || 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 readUserToken();\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,10 @@
1
1
  import 'server-only';
2
+ import { homedir } from 'os';
3
+ import { join } from 'path';
4
+ import { promises } from 'fs';
2
5
 
3
6
  // src/next.ts
4
-
5
- // src/index.ts
6
- var state = {
7
- token: null,
8
- tokenExpiresAt: null,
9
- bundle: null
10
- };
7
+ var cache = /* @__PURE__ */ new Map();
11
8
  var pendingLoadPromise = null;
12
9
  function getEnvGodConfig(options) {
13
10
  const env = process.env;
@@ -18,9 +15,8 @@ function getEnvGodConfig(options) {
18
15
  env: options?.config?.env ?? env.ENVGOD_ENV,
19
16
  service: options?.config?.service ?? env.ENVGOD_SERVICE
20
17
  };
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(", ")}`);
18
+ if (!config.apiUrl) {
19
+ throw new Error("[EnvGod] Missing required configuration: apiUrl is required.");
24
20
  }
25
21
  return config;
26
22
  }
@@ -45,7 +41,7 @@ async function fetchWithTimeout(url, init) {
45
41
  clearTimeout(id);
46
42
  }
47
43
  }
48
- async function exchangeToken(config, timeout) {
44
+ async function exchangeToken(config, timeout, state) {
49
45
  const res = await fetchWithTimeout(`${config.apiUrl}/v1/auth/exchange`, {
50
46
  method: "POST",
51
47
  headers: {
@@ -67,6 +63,36 @@ async function exchangeToken(config, timeout) {
67
63
  state.tokenExpiresAt = new Date(data.expiresAt).getTime();
68
64
  return data.token;
69
65
  }
66
+ async function readUserToken() {
67
+ const tokenPath = join(homedir(), ".envgod", "token.json");
68
+ try {
69
+ const content = await promises.readFile(tokenPath, "utf-8");
70
+ const data = JSON.parse(content);
71
+ return data?.token || 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 readUserToken();
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":["fs"],"mappings":"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","file":"next.mjs","sourcesContent":["import { homedir } from 'os';\r\nimport { join } from 'path';\r\nimport { promises as fs } from 'fs';\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\nasync function readUserToken(): Promise<string | null> {\r\n const tokenPath = join(homedir(), '.envgod', 'token.json');\r\n try {\r\n const content = await fs.readFile(tokenPath, 'utf-8');\r\n const data = JSON.parse(content);\r\n return data?.token || 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 readUserToken();\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.3",
4
4
  "description": "Secure Node.js/Next.js SDK for EnvGod",
5
5
  "repository": {
6
6
  "type": "git",
@@ -45,7 +45,7 @@
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",
@@ -54,4 +54,4 @@
54
54
  "dependencies": {
55
55
  "server-only": "^0.0.1"
56
56
  }
57
- }
57
+ }