@stacksjs/env 0.75.336 → 0.75.337

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/dist/index.d.ts CHANGED
@@ -6,6 +6,7 @@ export * from './types';
6
6
  export * from './crypto';
7
7
  export * from './parser';
8
8
  export * from './plugin';
9
+ export * from './withheld';
9
10
  export * from './cli';
10
11
  export * from './plaintext';
11
12
  export * from './tenants';
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export*from"./runtime";export*from"./numeric-keys";export*from"./deployment";export*from"./integrations";export*from"./types";export*from"./crypto";export*from"./parser";export*from"./plugin";export*from"./cli";export*from"./plaintext";export*from"./tenants";export*from"./utils";
1
+ export*from"./runtime";export*from"./numeric-keys";export*from"./deployment";export*from"./integrations";export*from"./types";export*from"./crypto";export*from"./parser";export*from"./plugin";export*from"./withheld";export*from"./cli";export*from"./plaintext";export*from"./tenants";export*from"./utils";
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}from"./encrypted-value";import{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(process.env.NODE_ENV)||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(process.env.NODE_ENV)||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}from"./encrypted-value";import{plaintextEnv}from"./plaintext-env";import{applyWithheldEnv,withheldEnvKeys}from"./withheld";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(process.env.NODE_ENV)||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 withheld=withheldEnvKeys(),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"||withheld.has(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);applyWithheldEnv();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(process.env.NODE_ENV)||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;
@@ -0,0 +1,23 @@
1
+ /** The keys this process's parent withheld from it. */
2
+ export declare function withheldEnvKeys(env?: Record<string, string | undefined>): Set<string>;
3
+ /**
4
+ * The environment entries that withhold `keys` from a child process, to
5
+ * spread into the child's `env`. Keys only, never values.
6
+ */
7
+ export declare function withholdEnv(keys: readonly string[]): Record<string, string>;
8
+ /**
9
+ * Remove every withheld key from `env`, including a copy Bun loaded from a
10
+ * file before any of this process's code ran. Returns the keys removed.
11
+ */
12
+ export declare function applyWithheldEnv(env?: Record<string, string | undefined>): string[];
13
+ /**
14
+ * The variable a parent process sets to keep keys out of a child it spawns.
15
+ *
16
+ * Deleting a key before spawning does not keep it out of the child: Bun loads
17
+ * `.env` natively, and `loadEnv` fills anything absent, so the child reads the
18
+ * deleted value straight back from disk. That is how `buddy release:ios`
19
+ * shipped a `localhost` dev server in a release build (stacksjs/stacks#3028).
20
+ * A parent that needs a key absent names it here instead, and the child's
21
+ * loaders honour that whichever file defines it.
22
+ */
23
+ export declare const WITHHELD_ENV: 'STACKS_ENV_WITHHOLD';
@@ -0,0 +1 @@
1
+ import process from"node:process";export const WITHHELD_ENV="STACKS_ENV_WITHHOLD";export function withheldEnvKeys(env=process.env){return new Set((env[WITHHELD_ENV]??"").split(",").map((key)=>key.trim()).filter(Boolean))}export function withholdEnv(keys){const merged=new Set([...withheldEnvKeys(),...keys]);return{[WITHHELD_ENV]:[...merged].join(",")}}export function applyWithheldEnv(env=process.env){const removed=[];for(const key of withheldEnvKeys(env))if(key in env){delete env[key];removed.push(key)}return removed}
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.336",
5
+ "version": "0.75.337",
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.336"
59
+ "@stacksjs/path": "0.75.337"
60
60
  },
61
61
  "devDependencies": {
62
62
  "better-dx": "^0.2.24",
63
- "@stacksjs/validation": "0.75.336"
63
+ "@stacksjs/validation": "0.75.337"
64
64
  }
65
65
  }