@stacksjs/env 0.75.34 → 0.75.35

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.
@@ -18,9 +18,11 @@ export declare function normalizeEnvironmentName(value: string | undefined | nul
18
18
  * module graph loads - a test harness pinning it, a CLI resolving `--env` - and
19
19
  * a const would freeze whatever happened to be set first (stacksjs/stacks#2581).
20
20
  *
21
- * Unlike `appEnv()`, which answers `local` so callers always have a string, this
22
- * answers `undefined`. The difference matters here: an unset or unreadable
23
- * `APP_ENV` must not be *guessed* into a label that an allowlist might admit.
21
+ * Unlike `appEnv()`, which falls back to `NODE_ENV` and answers `local` so
22
+ * callers always have a string, this reads only `APP_ENV` and answers
23
+ * `undefined` when it is absent. The difference matters here: an unset or
24
+ * unreadable `APP_ENV` must not be *guessed* into a label that an allowlist
25
+ * might admit.
24
26
  */
25
27
  export declare function integrationEnvironment(): string | undefined;
26
28
  /**
@@ -1 +1 @@
1
- export const REMOTE_TELEMETRY_ENVIRONMENTS=["production","staging"];export function normalizeEnvironmentName(value){if(typeof value!=="string")return;const normalized=value.trim().toLowerCase();if(!/^[a-z0-9_-]+$/.test(normalized))return;if(normalized==="prod")return"production";if(normalized==="stage")return"staging";if(normalized==="dev")return"development";return normalized}export function integrationEnvironment(){return normalizeEnvironmentName(process.env.APP_ENV)??normalizeEnvironmentName("development")}export function integrationGate(options,defaultEnvironments=REMOTE_TELEMETRY_ENVIRONMENTS){const environment=integrationEnvironment();if(options?.enabled===!1)return{enabled:!1,environment,reason:"disabled-explicitly"};const allowlist=options?.environments??defaultEnvironments;if(allowlist.length===0)return{enabled:!1,environment,reason:"empty-allowlist"};if(environment===void 0)return{enabled:!1,environment,reason:"environment-unknown"};const permitted=new Set;for(const entry of allowlist){const name=normalizeEnvironmentName(entry);if(name!==void 0)permitted.add(name)}return permitted.has(environment)?{enabled:!0,environment,reason:"environment-allowed"}:{enabled:!1,environment,reason:"environment-excluded"}}export function initializeIntegration(options,initialize,defaultEnvironments=REMOTE_TELEMETRY_ENVIRONMENTS){const gate=integrationGate(options,defaultEnvironments);if(!gate.enabled||gate.environment===void 0)return{initialized:!1,gate};return{initialized:!0,gate,value:initialize({environment:gate.environment})}}
1
+ export const REMOTE_TELEMETRY_ENVIRONMENTS=["production","staging"];export function normalizeEnvironmentName(value){if(typeof value!=="string")return;const normalized=value.trim().toLowerCase();if(!/^[a-z0-9_-]+$/.test(normalized))return;if(normalized==="prod")return"production";if(normalized==="stage")return"staging";if(normalized==="dev")return"development";return normalized}export function integrationEnvironment(){return normalizeEnvironmentName(process.env.APP_ENV)}export function integrationGate(options,defaultEnvironments=REMOTE_TELEMETRY_ENVIRONMENTS){const environment=integrationEnvironment();if(options?.enabled===!1)return{enabled:!1,environment,reason:"disabled-explicitly"};const allowlist=options?.environments??defaultEnvironments;if(allowlist.length===0)return{enabled:!1,environment,reason:"empty-allowlist"};if(environment===void 0)return{enabled:!1,environment,reason:"environment-unknown"};const permitted=new Set;for(const entry of allowlist){const name=normalizeEnvironmentName(entry);if(name!==void 0)permitted.add(name)}return permitted.has(environment)?{enabled:!0,environment,reason:"environment-allowed"}:{enabled:!1,environment,reason:"environment-excluded"}}export function initializeIntegration(options,initialize,defaultEnvironments=REMOTE_TELEMETRY_ENVIRONMENTS){const gate=integrationGate(options,defaultEnvironments);if(!gate.enabled||gate.environment===void 0)return{initialized:!1,gate};return{initialized:!0,gate,value:initialize({environment:gate.environment})}}
package/dist/plugin.js CHANGED
@@ -1 +1 @@
1
- import{existsSync}from"node:fs";import{readFileSync}from"node:fs";import{resolve}from"node:path";import{decryptValue,getPrivateKey}from"./crypto";import{parse}from"./parser";import{isEncryptedValue,plaintextEnv}from"./plaintext-env";function normalizeEnvName(value){if(!value||isEncryptedValue(value))return;const normalized=value.trim().toLowerCase();if(!/^[a-z0-9_-]+$/.test(normalized))return;if(normalized==="prod")return"production";if(normalized==="stage")return"staging";if(normalized==="dev")return"development";return normalized}let cachedKeyEnv=null,cachedKeyCwd=null,cachedPrivateKey,loadedEnvName;function keyFromKeysFile(envName,cwd,keysFile=".env.keys"){const keysPath=resolve(cwd,keysFile);if(!existsSync(keysPath))return;try{const{parsed}=parse(readFileSync(keysPath,"utf-8"));if(envName){const specific=parsed[`DOTENV_PRIVATE_KEY_${envName.toUpperCase()}`];if(specific)return specific}return parsed.DOTENV_PRIVATE_KEY}catch{return}}export function activeEnvName(options={}){return normalizeEnvName(options.env)||loadedEnvName||normalizeEnvName("development")||normalizeEnvName(process.env.DOTENV_ENV)||normalizeEnvName(process.env.APP_ENV)||"development"}export function resolvePrivateKey(options={}){const envName=activeEnvName(options),cwd=resolve(options.cwd||process.cwd());if(cachedKeyEnv===envName&&cachedKeyCwd===cwd)return cachedPrivateKey;let key=getPrivateKey(envName);if(!key)key=process.env.DOTENV_PRIVATE_KEY;if(!key)key=keyFromKeysFile(envName,cwd);cachedKeyEnv=envName;cachedKeyCwd=cwd;cachedPrivateKey=key;return key}export function resetPrivateKeyCache(){cachedKeyEnv=null;cachedKeyCwd=null;cachedPrivateKey=void 0;loadedEnvName=void 0}export function decryptEnvValue(value,options={}){if(!isEncryptedValue(value))return value;const key=resolvePrivateKey(options);if(!key)return;try{const normalized=value.startsWith("enc:")?`encrypted:${value.slice(4)}`:value;return decryptValue(normalized,key)}catch{return}}export function loadEnv(options={}){const{path:envPaths=[".env"],overload=!1,env:envName,privateKey:customPrivateKey,keysFile=".env.keys",quiet=!1,cwd=process.cwd()}=options,paths=Array.isArray(envPaths)?envPaths:[envPaths],errors=[];let loaded=0;const externalOverrides=new Set(Object.entries(plaintextEnv(process.env)).filter(([,value])=>value!==void 0).map(([key])=>key));let privateKey=customPrivateKey;if(!privateKey&&keysFile){const keysPath=resolve(cwd,keysFile);if(existsSync(keysPath))try{const keysContent=readFileSync(keysPath,"utf-8"),{parsed}=parse(keysContent);if(envName){const keyName=`DOTENV_PRIVATE_KEY_${envName.toUpperCase()}`;privateKey=parsed[keyName]||parsed.DOTENV_PRIVATE_KEY}else privateKey=parsed.DOTENV_PRIVATE_KEY}catch(error){errors.push(`Failed to load keys file: ${error instanceof Error?error.message:"Unknown error"}`)}}if(!privateKey)privateKey=getPrivateKey(envName||"")||getPrivateKey();for(const envPath of paths){const fullPath=resolve(cwd,envPath);if(!existsSync(fullPath)){if(!quiet)errors.push(`File not found: ${fullPath}`);continue}try{const content=readFileSync(fullPath,"utf-8"),{parsed,errors:parseErrors,skippedEncrypted}=parse(content,{privateKey,processEnv:process.env});errors.push(...parseErrors);if(skippedEncrypted.length>0){const usable=plaintextEnv(process.env,skippedEncrypted);for(const key of Object.keys(process.env))if(!Object.hasOwn(usable,key))delete process.env[key];const keyName=envName?`DOTENV_PRIVATE_KEY_${envName.toUpperCase()}`:"DOTENV_PRIVATE_KEY",reason=privateKey?"decryption failed":`${keyName} not set`,preview=skippedEncrypted.slice(0,5).join(", "),rest=skippedEncrypted.length>5?`, \u2026 +${skippedEncrypted.length-5} more`:"";console.warn(`[env] warning: skipped ${skippedEncrypted.length} encrypted value(s) in ${envPath} (${reason}; existing plaintext or defaults apply): ${preview}${rest}`)}let applied=0;for(const[key,value]of Object.entries(parsed)){if(key==="DOTENV_PUBLIC_KEY")continue;const existing=process.env[key],isStaleCiphertext=isEncryptedValue(existing)&&value!==existing;if(overload||!externalOverrides.has(key)||isStaleCiphertext){process.env[key]=value;loaded++;applied++}}if(!quiet&&!process.env.__ENV_LOADED__){console.error(`[env] loaded ${applied}/${Object.keys(parsed).length} variables from ${envPath}`);process.env.__ENV_LOADED__="1"}}catch(error){errors.push(`Failed to load ${fullPath}: ${error instanceof Error?error.message:"Unknown error"}`)}}return{loaded,errors}}export function envPlugin(options={}){return{name:"env-plugin",setup(build){loadEnv(options)}}}export function autoLoadEnv(options={}){const env=normalizeEnvName(options.env)||normalizeEnvName("development")||normalizeEnvName(process.env.DOTENV_ENV)||normalizeEnvName(process.env.APP_ENV)||"development",cwd=options.cwd||process.cwd();if(loadedEnvName!==env){loadedEnvName=env;cachedKeyEnv=null;cachedKeyCwd=null;cachedPrivateKey=void 0}const paths=[],addIfPresent=(file)=>{if(!paths.includes(file)&&existsSync(resolve(cwd,file)))paths.push(file)};addIfPresent(".env");addIfPresent(".env.local");const envFile=`.env.${env}`,envLocalFile=`.env.${env}.local`;addIfPresent(envFile);addIfPresent(envLocalFile);return loadEnv({...options,path:paths,env})}export default envPlugin;
1
+ import{existsSync}from"node:fs";import{readFileSync}from"node:fs";import{resolve}from"node:path";import{decryptValue,getPrivateKey}from"./crypto";import{parse}from"./parser";import{isEncryptedValue,plaintextEnv}from"./plaintext-env";function normalizeEnvName(value){if(!value||isEncryptedValue(value))return;const normalized=value.trim().toLowerCase();if(!/^[a-z0-9_-]+$/.test(normalized))return;if(normalized==="prod")return"production";if(normalized==="stage")return"staging";if(normalized==="dev")return"development";return normalized}let cachedKeyEnv=null,cachedKeyCwd=null,cachedPrivateKey,loadedEnvName;function keyFromKeysFile(envName,cwd,keysFile=".env.keys"){const keysPath=resolve(cwd,keysFile);if(!existsSync(keysPath))return;try{const{parsed}=parse(readFileSync(keysPath,"utf-8"));if(envName){const specific=parsed[`DOTENV_PRIVATE_KEY_${envName.toUpperCase()}`];if(specific)return specific}return parsed.DOTENV_PRIVATE_KEY}catch{return}}export function activeEnvName(options={}){return normalizeEnvName(options.env)||loadedEnvName||normalizeEnvName("development")||normalizeEnvName(process.env.DOTENV_ENV)||normalizeEnvName(process.env.APP_ENV)||"development"}export function resolvePrivateKey(options={}){const envName=activeEnvName(options),cwd=resolve(options.cwd||process.cwd());if(cachedKeyEnv===envName&&cachedKeyCwd===cwd)return cachedPrivateKey;let key=getPrivateKey(envName);if(!key)key=process.env.DOTENV_PRIVATE_KEY;if(!key)key=keyFromKeysFile(envName,cwd);cachedKeyEnv=envName;cachedKeyCwd=cwd;cachedPrivateKey=key;return key}export function resetPrivateKeyCache(){cachedKeyEnv=null;cachedKeyCwd=null;cachedPrivateKey=void 0;loadedEnvName=void 0}export function decryptEnvValue(value,options={}){if(!isEncryptedValue(value))return value;const key=resolvePrivateKey(options);if(!key)return;try{const normalized=value.startsWith("enc:")?`encrypted:${value.slice(4)}`:value;return decryptValue(normalized,key)}catch{return}}export function loadEnv(options={}){const{path:envPaths=[".env"],overload=!1,env:envName,privateKey:customPrivateKey,keysFile=".env.keys",quiet=!1,cwd=process.cwd()}=options,paths=Array.isArray(envPaths)?envPaths:[envPaths],errors=[];let loaded=0;const externalOverrides=new Set(Object.entries(plaintextEnv(process.env)).filter(([,value])=>value!==void 0).map(([key])=>key));let privateKey=customPrivateKey;if(!privateKey&&keysFile){const keysPath=resolve(cwd,keysFile);if(existsSync(keysPath))try{const keysContent=readFileSync(keysPath,"utf-8"),{parsed}=parse(keysContent);if(envName){const keyName=`DOTENV_PRIVATE_KEY_${envName.toUpperCase()}`;privateKey=parsed[keyName]||parsed.DOTENV_PRIVATE_KEY}else privateKey=parsed.DOTENV_PRIVATE_KEY}catch(error){errors.push(`Failed to load keys file: ${error instanceof Error?error.message:"Unknown error"}`)}}if(!privateKey)privateKey=getPrivateKey(envName||"")||getPrivateKey();for(const envPath of paths){const fullPath=resolve(cwd,envPath);if(!existsSync(fullPath)){if(!quiet)errors.push(`File not found: ${fullPath}`);continue}try{const content=readFileSync(fullPath,"utf-8"),{parsed,errors:parseErrors,skippedEncrypted}=parse(content,{privateKey,processEnv:process.env});errors.push(...parseErrors);if(skippedEncrypted.length>0){const usable=plaintextEnv(process.env,skippedEncrypted);for(const key of Object.keys(process.env))if(!Object.hasOwn(usable,key))delete process.env[key];const keyName=envName?`DOTENV_PRIVATE_KEY_${envName.toUpperCase()}`:"DOTENV_PRIVATE_KEY",reason=privateKey?"decryption failed":`${keyName} not set`,preview=skippedEncrypted.slice(0,5).join(", "),rest=skippedEncrypted.length>5?`, \u2026 +${skippedEncrypted.length-5} more`:"";console.warn(`[env] warning: skipped ${skippedEncrypted.length} encrypted value(s) in ${envPath} (${reason}; existing plaintext or defaults apply): ${preview}${rest}`)}let applied=0;for(const[key,value]of Object.entries(parsed)){if(key==="DOTENV_PUBLIC_KEY")continue;const existing=process.env[key],isStaleCiphertext=isEncryptedValue(existing)&&value!==existing;if(overload||!externalOverrides.has(key)||isStaleCiphertext){process.env[key]=value;loaded++;applied++}}if(!quiet&&!process.env.__ENV_LOADED__){console.error(`[env] loaded ${applied}/${Object.keys(parsed).length} variables from ${envPath}`);process.env.__ENV_LOADED__="1"}}catch(error){errors.push(`Failed to load ${fullPath}: ${error instanceof Error?error.message:"Unknown error"}`)}}scrubForeignCiphertext(privateKey);return{loaded,errors}}const reportedForeignCiphertext=new Set;function scrubForeignCiphertext(privateKey){const undecryptable=[];for(const[key,value]of Object.entries(process.env)){if(!isEncryptedValue(value))continue;let plaintext;if(privateKey)try{plaintext=decryptValue(value.startsWith("enc:")?`encrypted:${value.slice(4)}`:value,privateKey)}catch{}if(plaintext===void 0)undecryptable.push(key);else process.env[key]=plaintext}if(!undecryptable.length)return;const usable=plaintextEnv(process.env,undecryptable);for(const key of Object.keys(process.env))if(!Object.hasOwn(usable,key))delete process.env[key];const fresh=undecryptable.filter((key)=>!reportedForeignCiphertext.has(key));if(!fresh.length)return;for(const key of fresh)reportedForeignCiphertext.add(key);const preview=fresh.slice(0,5).join(", "),rest=fresh.length>5?`, \u2026 +${fresh.length-5} more`:"";console.warn(`[env] warning: unset ${fresh.length} encrypted value(s) no loaded env file could decrypt (Bun preloads .env.<NODE_ENV> on its own): ${preview}${rest}`)}export function envPlugin(options={}){return{name:"env-plugin",setup(build){loadEnv(options)}}}export function autoLoadEnv(options={}){const env=normalizeEnvName(options.env)||normalizeEnvName("development")||normalizeEnvName(process.env.DOTENV_ENV)||normalizeEnvName(process.env.APP_ENV)||"development",cwd=options.cwd||process.cwd();if(loadedEnvName!==env){loadedEnvName=env;cachedKeyEnv=null;cachedKeyCwd=null;cachedPrivateKey=void 0}const paths=[],addIfPresent=(file)=>{if(!paths.includes(file)&&existsSync(resolve(cwd,file)))paths.push(file)};addIfPresent(".env");addIfPresent(".env.local");const envFile=`.env.${env}`,envLocalFile=`.env.${env}.local`;addIfPresent(envFile);addIfPresent(envLocalFile);return loadEnv({...options,path:paths,env})}export default envPlugin;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/env",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.34",
5
+ "version": "0.75.35",
6
6
  "description": "Stacks env helper methods.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -56,10 +56,10 @@
56
56
  "prepublishOnly": "bun run build"
57
57
  },
58
58
  "dependencies": {
59
- "@stacksjs/path": "0.75.34"
59
+ "@stacksjs/path": "0.75.35"
60
60
  },
61
61
  "devDependencies": {
62
62
  "better-dx": "^0.2.24",
63
- "@stacksjs/validation": "0.75.34"
63
+ "@stacksjs/validation": "0.75.35"
64
64
  }
65
65
  }