@stacksjs/env 0.74.51 → 0.74.53

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/cli.js CHANGED
@@ -1,32 +1,29 @@
1
- // @bun
2
- import{existsSync as K,readFileSync as k,renameSync as re,rmSync as se,writeFileSync as b}from"fs";import{basename as ie,dirname as j,resolve as E}from"path";import{createCipheriv as X,createDecipheriv as H,createHash as I,createPrivateKey as le,createPublicKey as G,diffieHellman as J,generateKeyPairSync as M,hkdfSync as z,randomBytes as S}from"crypto";var Y="x25519-public:",A="x25519-private:",W="encrypted:v2:",U="encrypted:",q=Buffer.from("stacks-env:v2","utf8"),Z="Environment decryption failed: authentication or format error";function Q(t){if(t.length===32)return t;return I("sha256").update(t).digest()}function Se(t,n){let e=S(16),s=X("aes-256-gcm",Q(n),e);return{ciphertext:Buffer.concat([s.update(t,"utf8"),s.final()]).toString("hex"),iv:e.toString("hex"),authTag:s.getAuthTag().toString("hex")}}function pe(t,n,e,s){try{let r=H("aes-256-gcm",Q(n),Buffer.from(e,"hex"));return r.setAuthTag(Buffer.from(s,"hex")),Buffer.concat([r.update(Buffer.from(t,"hex")),r.final()]).toString("utf8")}catch{throw Error("Decryption failed: authentication or format error")}}function _(t){return t.toString("base64url")}function V(t,n,e){if(typeof t!=="string"||t.length>0&&!/^[A-Za-z0-9_-]+$/.test(t))throw Error(`${n} is not canonical base64url`);let s=Buffer.from(t,"base64url");if(_(s)!==t)throw Error(`${n} is not canonical base64url`);if(e!==void 0&&s.length!==e)throw Error(`${n} has an invalid length`);return s}function de(t){if(!t.startsWith(Y))throw Error("Legacy or invalid public key. Run buddy env:rotate before creating new ciphertext.");let n=G({key:V(t.slice(Y.length),"public key"),format:"der",type:"spki"});if(n.asymmetricKeyType!=="x25519")throw Error("Public key is not X25519");return n}function ye(t){if(!t.startsWith(A))throw Error("Private key is not a version 2 X25519 key");let n=le({key:V(t.slice(A.length),"private key"),format:"der",type:"pkcs8"});if(n.asymmetricKeyType!=="x25519")throw Error("Private key is not X25519");return n}function ee(t){return Buffer.from(`stacks-env:v2;${t.epk};${t.salt};${t.nonce}`,"utf8")}function R(){let{publicKey:t,privateKey:n}=M("x25519"),e=t.export({format:"der",type:"spki"}),s=n.export({format:"der",type:"pkcs8"});return{publicKey:`${Y}${_(e)}`,privateKey:`${A}${_(s)}`}}function C(t,n){let e=de(n),s=M("x25519"),r=_(s.publicKey.export({format:"der",type:"spki"})),i=S(16),o=S(12),u=J({privateKey:s.privateKey,publicKey:e}),a=Buffer.from(z("sha256",u,i,q,32)),c={epk:r,salt:_(i),nonce:_(o)};try{let l=X("aes-256-gcm",a,o);l.setAAD(ee(c));let f=Buffer.concat([l.update(t,"utf8"),l.final()]),p={v:2,...c,ciphertext:_(f),tag:_(l.getAuthTag())};return`${W}${_(Buffer.from(JSON.stringify(p),"utf8"))}`}finally{u.fill(0),a.fill(0)}}function ge(t){let n=V(t.slice(W.length),"envelope").toString("utf8"),e=JSON.parse(n);if(!e||typeof e!=="object"||Array.isArray(e))throw Error("envelope is not an object");let s=Object.keys(e).sort(),r=["ciphertext","epk","nonce","salt","tag","v"];if(s.length!==r.length||s.some((i,o)=>i!==r[o]))throw Error("envelope fields are invalid");if(e.v!==2)throw Error("envelope version is unsupported");for(let i of["epk","salt","nonce","ciphertext","tag"])if(typeof e[i]!=="string")throw Error(`${i} is invalid`);return e}function he(t,n){try{let e=ge(t),s=ye(n),r=G({key:V(e.epk,"ephemeral public key"),format:"der",type:"spki"});if(r.asymmetricKeyType!=="x25519")throw Error("ephemeral key is not X25519");let i=V(e.salt,"salt",16),o=V(e.nonce,"nonce",12),u=V(e.ciphertext,"ciphertext"),a=V(e.tag,"tag",16),c=J({privateKey:s,publicKey:r}),l=Buffer.from(z("sha256",c,i,q,32));try{let f=H("aes-256-gcm",l,o);return f.setAAD(ee(e)),f.setAuthTag(a),Buffer.concat([f.update(u),f.final()]).toString("utf8")}finally{c.fill(0),l.fill(0)}}catch{throw Error(Z)}}function me(t,n){try{if(!/^[0-9a-f]{64}$/.test(n))throw Error("invalid legacy private key");let e=Buffer.from(t.slice(U.length),"base64");if(e.length<32)throw Error("invalid legacy payload");let s=I("sha256").update(Buffer.from(n,"hex")).digest(),r=I("sha256").update(s).digest();return pe(e.subarray(32).toString("hex"),r,e.subarray(0,16).toString("hex"),e.subarray(16,32).toString("hex"))}catch{throw Error(Z)}}function Ye(t){return t.startsWith(U)&&!t.startsWith(W)}function F(t,n){if(!t.startsWith(U))return t;return t.startsWith(W)?he(t,n):me(t,n)}function Ae(t,n,e){return C(F(t,n),e)}function Ue(t){let n=t.match(/^DOTENV_PRIVATE_KEY_(.+)$/);return n&&n[1]?n[1].toLowerCase():""}function Le(t=""){return t?process.env[`DOTENV_PRIVATE_KEY_${t.toUpperCase()}`]:process.env.DOTENV_PRIVATE_KEY}import{spawnSync as Ee}from"child_process";import{readFileSync as $e}from"fs";import{arch as ke,hostname as Ke,platform as ne,release as we,userInfo as be}from"os";import{basename as Ne,dirname as xe,resolve as _e}from"path";function ve(t){return typeof t==="string"&&(t.startsWith("encrypted:")||t.startsWith("enc:"))}function te(t,n=Object.keys(t)){let e=n.map((s)=>t[s]).filter((s)=>ve(s));return Object.fromEntries(Object.entries(t).filter(([,s])=>s===void 0||!e.some((r)=>s.includes(r))))}function O(t,n={}){let e={},s=[],r=[],i=t.split(`
3
- `),o=te(n.processEnv||process.env);for(let u of i){let a=u.trim();if(!a||a.startsWith("#"))continue;if(a.startsWith("DOTENV_PUBLIC_KEY=")){let p=a.match(/^DOTENV_PUBLIC_KEY=["']?([^"'\n]+)["']?/);if(p&&p[1]!==void 0)e.DOTENV_PUBLIC_KEY=p[1];continue}let c=a.match(/^([^=]+)=(.*)$/);if(!c||c[1]===void 0||c[2]===void 0)continue;let l=c[1].trim(),f=c[2].trim();if(f.startsWith('"')&&f.endsWith('"')||f.startsWith("'")&&f.endsWith("'")){if(f=f.slice(1,-1),f.includes("\\n"))f=f.replace(/\\n/g,`
4
- `)}if(f.startsWith("encrypted:")||f.startsWith("enc:")){if(!n.privateKey){r.push(l);continue}try{let p=f.startsWith("enc:")?`encrypted:${f.slice(4)}`:f;f=F(p,n.privateKey)}catch(p){s.push(`Failed to decrypt ${l}: ${p instanceof Error?p.message:"Unknown error"}`),r.push(l);continue}}f=Pe(f,{...o,...e}),f=De(f),e[l]=f}return{parsed:e,errors:s,skippedEncrypted:r}}function Pe(t,n){return t.replace(/\$\{([^}]+)\}/g,(e,s)=>{let r=s.match(/^([^:\-+]+)(:-|-)(.+)$/);if(r){let[,o,u,a]=r,c=n[o];if(u===":-")return c||a;else return c!==void 0?c:a}let i=s.match(/^([^:\-+]+)(:?\+)(.+)$/);if(i){let[,o,u,a]=i,c=n[o];if(u===":+")return c?a:"";else return c!==void 0?a:""}return n[s]||""})}var Ve=new Set(["date","hostname","whoami","uname","pwd","echo","printf","cat","basename","dirname"]);function Oe(t){let n=[],e="",s=null,r=!1;for(let i of t.trim()){if(r){e+=i,r=!1;continue}if(i==="\\"&&s!=="'"){r=!0;continue}if(s){if(i===s)s=null;else e+=i;continue}if(i==='"'||i==="'"){s=i;continue}if(/\s/.test(i)){if(e)n.push(e),e="";continue}e+=i}if(r||s)throw Error("Command substitution contains an unterminated escape or quote");if(e)n.push(e);return n}function Te(t){let[n,...e]=t,s=e.some((r)=>r.startsWith("-"));if(n==="echo"&&!s)return e.join(" ");if(n==="printf"&&e.length===1&&!e[0]?.includes("%"))return e[0];if(n==="hostname"&&e.length===0)return Ke();if(n==="whoami"&&e.length===0)return be().username;if(n==="pwd"&&e.length===0)return process.cwd();if(n==="basename"&&e.length===1)return Ne(e[0]??"");if(n==="dirname"&&e.length===1)return xe(e[0]??"");if(n==="cat"&&e.length>0&&!s)return e.map((r)=>$e(_e(process.cwd(),r),"utf8")).join("");if(n==="uname"){let r=e[0]??"-s";if(e.length<=1&&r==="-m")return ke();if(e.length<=1&&r==="-r")return we();if(e.length<=1&&r==="-s")return{aix:"AIX",android:"Android",cygwin:"CYGWIN",darwin:"Darwin",freebsd:"FreeBSD",haiku:"Haiku",linux:"Linux",netbsd:"NetBSD",openbsd:"OpenBSD",sunos:"SunOS",win32:"Windows_NT"}[ne()]??ne()}return}function De(t){return t.replace(/\$\(([^)]+)\)/g,(n,e)=>{try{let s=Oe(e),r=s[0];if(!r||!Ve.has(r))return console.warn(`[env] Blocked command substitution for disallowed command: ${r}`),"";let i=Te(s);if(i!==void 0)return i.trim();let o=Ee(r,s.slice(1),{encoding:"utf8",env:process.env});if(o.status===0)return o.stdout.trim();let u=o.stderr.trim();console.warn(`[env] Command substitution failed (exit code ${o.status??"unknown"}): ${r}${u?`: ${u}`:""}`)}catch(s){console.warn(`[env] Command substitution error: ${s instanceof Error?s.message:String(s)}`)}return""})}async function Ze(t,n={}){let e={},s=[],r=[];for(let i of t)try{let o=Bun.file(i);if(!o.size)continue;let u=await o.text(),{parsed:a,errors:c,skippedEncrypted:l}=O(u,n);s.push(...c),r.push(...l);for(let[f,p]of Object.entries(a)){if(!n.overload&&e[f]!==void 0)continue;e[f]=p}}catch(o){if(o?.code==="ENOENT")continue;s.push(`Failed to read ${i}: ${o instanceof Error?o.message:String(o)}`)}return{parsed:e,errors:s,skippedEncrypted:r}}function st(t,n){if(t)return t;let e=String(n??"").trim();if(!e||e==="development"||e==="dev"||e==="local")return"";if(!/^[\w.-]+$/.test(e))return"";return`.env.${e}`}function ce(t,n){return(!n.key||t.includes(n.key))&&(!n.excludeKey||!t.includes(n.excludeKey))}function Re(t,n){let e=[];for(let s of t.split(`
5
- `)){let r=s.trim();if(!r||r.startsWith("#"))continue;if(r.startsWith("DOTENV_PUBLIC_KEY"))continue;let i=r.match(/^([^=]+)=(.*)$/);if(!i||i[1]===void 0)continue;let o=i[1].trim();if(ce(o,n))e.push(o)}return e}function ae(t,n,e,s){let r=["#/-------------------[DOTENV_PUBLIC_KEY]--------------------/","#/ versioned X25519 encryption for .env files /","#/ [how it works](https://stacksjs.com/encryption) /","#/----------------------------------------------------------/",`${e}="${n}"`,""],i=!1;for(let o of t.split(`
6
- `)){let u=o.trim();if(u.startsWith("#/"))continue;if(u.startsWith("DOTENV_PUBLIC_KEY"))continue;if(!u&&!i)continue;if(!u||u.startsWith("#")){i=!0,r.push(o);continue}i=!0;let a=u.match(/^([^=]+)=(.*)$/);if(!a||a[1]===void 0||a[2]===void 0){r.push(o);continue}let c=a[1].trim(),l=a[2].trim();if(l.startsWith('"')&&l.endsWith('"')||l.startsWith("'")&&l.endsWith("'"))l=l.slice(1,-1);if(ce(c,s)&&!l.startsWith("encrypted:"))l=C(l,n);r.push(`${c}="${l}"`)}return r.join(`
7
- `)}function D(t){let e=((t||".env").split("/").pop()||"").replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase();return{publicKeyName:e?`DOTENV_PUBLIC_KEY_${e}`:"DOTENV_PUBLIC_KEY",privateKeyName:e?`DOTENV_PRIVATE_KEY_${e}`:"DOTENV_PRIVATE_KEY"}}function ue(t){let{publicKeyName:n}=D(t);return n==="DOTENV_PUBLIC_KEY"?[n]:[n,"DOTENV_PUBLIC_KEY"]}function L(t){let{privateKeyName:n}=D(t);return n==="DOTENV_PRIVATE_KEY"?[n]:[n,"DOTENV_PRIVATE_KEY"]}function oe(t){for(let n of t.split(`
8
- `)){let e=n.trim();if(!e||e.startsWith("#"))continue;let s=e.match(/^([^=]+)=(.*)$/);if(!s?.[1]||s[2]===void 0)continue;let r=s[2].trim().replace(/^["']|["']$/g,"");if(r.startsWith("encrypted:")||r.startsWith("enc:"))return!0}return!1}function Fe(t,n){let e=ue(n);for(let s of t.split(`
9
- `)){let r=s.trim();if(!r||r.startsWith("#"))continue;let i=r.match(/^([^=]+)=["']?([^"'\n]+)["']?/);if(!i?.[1]||!i[2])continue;let o=i[1].trim();if(e.includes(o))return{name:o,key:i[2]}}return}function We(t,n){let e,s=!1;for(let r of t.split(`
10
- `)){let i=r.trim();if(!i||i.startsWith("#"))continue;let o=i.match(/^([^=]+)=(.*)$/);if(!o?.[1]||o[2]===void 0)continue;let u=o[1].trim(),a=o[2].trim().replace(/^["']|["']$/g,"");if(u===n)e=a;else if(a.startsWith("encrypted:")||a.startsWith("enc:"))s=!0}return e&&s?e:void 0}function it(t={}){let n=t.cwd||process.cwd(),e=E(n,t.file||".env"),s=E(n,t.keysFile||".env.keys");if(!K(e))return{success:!1,error:`File not found: ${e}`};try{let r,i,o=We(k(e,"utf-8"),D(t.file).publicKeyName);if(o)r=o;else if(K(s)){let c=k(s,"utf-8"),{parsed:l}=O(c),{publicKeyName:f,privateKeyName:p}=D(t.file);if(r=l[f]||"",i=l[p]||"",!r||!i){let g=R();r=g.publicKey,i=g.privateKey;let d=`
11
- ${f}="${r}"
12
- ${p}="${i}"
13
- `;b(s,c+d,"utf-8")}}else{let c=R();r=c.publicKey,i=c.privateKey;let{publicKeyName:l,privateKeyName:f}=D(t.file),p=`# .env.keys - Keep this file secure and never commit to source control
14
- ${l}="${r}"
15
- ${f}="${i}"
16
- `;b(s,p,"utf-8")}let u=k(e,"utf-8"),a=ae(u,r,D(t.file).publicKeyName,t);if(t.stdout)return{success:!0,output:a};return b(e,a,"utf-8"),{success:!0,output:`\u2714 encrypted (${t.file||".env"})
17
- \u2714 key added to ${t.keysFile||".env.keys"}`}}catch(r){return{success:!1,error:`Failed to encrypt: ${r instanceof Error?r.message:"Unknown error"}`}}}function Ce(t={}){let n=t.cwd||process.cwd(),e=E(n,t.file||".env"),s=E(n,t.keysFile||".env.keys");if(!K(e))return{success:!1,error:`File not found: ${e}`};if(!K(s))return{success:!1,error:`Keys file not found: ${s}`};try{let r=k(s,"utf-8"),{parsed:i}=O(r),a=((t.file||".env").split("/").pop()||"").replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase(),c=a?`DOTENV_PRIVATE_KEY_${a}`:"DOTENV_PRIVATE_KEY",l=i[c];if(!l)return{success:!1,error:`Private key not found: ${c}`};let f=[l];if(c!=="DOTENV_PRIVATE_KEY"&&i.DOTENV_PRIVATE_KEY&&i.DOTENV_PRIVATE_KEY!==l)f.push(i.DOTENV_PRIVATE_KEY);let g=k(e,"utf-8").split(`
18
- `),d=[];for(let N of g){let h=N.trim();if(!h||h.startsWith("#")){d.push(N);continue}if(h.startsWith("DOTENV_PUBLIC_KEY"))continue;let T=h.match(/^([^=]+)=(.*)$/);if(!T||T[1]===void 0||T[2]===void 0){d.push(N);continue}let v=T[1].trim(),y=T[2].trim();if(y.startsWith('"')&&y.endsWith('"')||y.startsWith("'")&&y.endsWith("'"))y=y.slice(1,-1);let x=y.startsWith("encrypted:");if(t.key&&!v.includes(t.key))x=!1;if(x){let w,P;for(let B of f)try{w=F(y,B);break}catch(fe){P=fe}if(w===void 0)throw P instanceof Error?P:Error(`Unable to decrypt ${v}`);y=w}d.push(`${v}="${y}"`)}let m=d.join(`
19
- `);if(t.stdout)return{success:!0,output:m};return b(e,m,"utf-8"),{success:!0,output:`\u2714 decrypted (${t.file||".env"})`}}catch(r){return{success:!1,error:`Failed to decrypt: ${r instanceof Error?r.message:"Unknown error"}`}}}function ot(t,n,e={}){let s=e.cwd||process.cwd(),r=E(s,e.file||".env"),i=e.keysFile?E(s,e.keysFile):E(j(r),".env.keys");try{let o="";if(K(r))o=k(r,"utf-8");let u=o.split(`
20
- `),a=!1,c,p=((e.file||".env").split("/").pop()||"").replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase(),g=p?`DOTENV_PUBLIC_KEY_${p}`:"DOTENV_PUBLIC_KEY",d=p?`DOTENV_PRIVATE_KEY_${p}`:"DOTENV_PRIVATE_KEY",m=Fe(o,e.file),N=m?.name??g;if(c=m?.key,c){let v=!1;if(K(i)){let x=k(i,"utf-8"),{parsed:w}=O(x);v=L(e.file).some((P)=>Boolean(w[P]))}let y=L(e.file).some((x)=>Boolean(process.env[x]));if(!v&&!y&&!oe(o))c=void 0}if(!e.plain&&!c&&oe(o))return{success:!1,error:`${e.file||".env"} already contains encrypted values, but no key that can be used to add another was found.
1
+ import{existsSync,readFileSync,renameSync,rmSync,writeFileSync}from"node:fs";import{basename,dirname,resolve}from"node:path";import{decryptValue,encryptValue,generateKeypair}from"./crypto";import{parse}from"./parser";export function resolveEnvFile(file,environment){if(file)return file;const name=String(environment??"").trim();if(!name||name==="development"||name==="dev"||name==="local")return"";if(!/^[\w.-]+$/.test(name))return"";return`.env.${name}`}function inEncryptScope(key,options){return(!options.key||key.includes(options.key))&&(!options.excludeKey||!key.includes(options.excludeKey))}function encryptableKeys(envContent,options){const keys=[];for(const line of envContent.split(`
2
+ `)){const trimmed=line.trim();if(!trimmed||trimmed.startsWith("#"))continue;if(trimmed.startsWith("DOTENV_PUBLIC_KEY"))continue;const match=trimmed.match(/^([^=]+)=(.*)$/);if(!match||match[1]===void 0)continue;const key=match[1].trim();if(inEncryptScope(key,options))keys.push(key)}return keys}function encryptedEnvContent(envContent,publicKey,publicKeyName,options){const encryptedLines=["#/-------------------[DOTENV_PUBLIC_KEY]--------------------/","#/ versioned X25519 encryption for .env files /","#/ [how it works](https://stacksjs.com/encryption) /","#/----------------------------------------------------------/",`${publicKeyName}="${publicKey}"`,""];let seenBody=!1;for(const line of envContent.split(`
3
+ `)){const trimmed=line.trim();if(trimmed.startsWith("#/"))continue;if(trimmed.startsWith("DOTENV_PUBLIC_KEY"))continue;if(!trimmed&&!seenBody)continue;if(!trimmed||trimmed.startsWith("#")){seenBody=!0;encryptedLines.push(line);continue}seenBody=!0;const match=trimmed.match(/^([^=]+)=(.*)$/);if(!match||match[1]===void 0||match[2]===void 0){encryptedLines.push(line);continue}const key=match[1].trim();let value=match[2].trim();if(value.startsWith('"')&&value.endsWith('"')||value.startsWith("'")&&value.endsWith("'"))value=value.slice(1,-1);if(inEncryptScope(key,options)&&!value.startsWith("encrypted:"))value=encryptValue(value,publicKey);encryptedLines.push(`${key}="${value}"`)}return encryptedLines.join(`
4
+ `)}export function envKeyNames(file){const suffix=((file||".env").split("/").pop()||"").replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase();return{publicKeyName:suffix?`DOTENV_PUBLIC_KEY_${suffix}`:"DOTENV_PUBLIC_KEY",privateKeyName:suffix?`DOTENV_PRIVATE_KEY_${suffix}`:"DOTENV_PRIVATE_KEY"}}export function envPublicKeyNames(file){const{publicKeyName}=envKeyNames(file);return publicKeyName==="DOTENV_PUBLIC_KEY"?[publicKeyName]:[publicKeyName,"DOTENV_PUBLIC_KEY"]}export function envPrivateKeyNames(file){const{privateKeyName}=envKeyNames(file);return privateKeyName==="DOTENV_PRIVATE_KEY"?[privateKeyName]:[privateKeyName,"DOTENV_PRIVATE_KEY"]}export function envHasCiphertext(envContent){for(const line of envContent.split(`
5
+ `)){const trimmed=line.trim();if(!trimmed||trimmed.startsWith("#"))continue;const match=trimmed.match(/^([^=]+)=(.*)$/);if(!match?.[1]||match[2]===void 0)continue;const value=match[2].trim().replace(/^["']|["']$/g,"");if(value.startsWith("encrypted:")||value.startsWith("enc:"))return!0}return!1}export function declaredEnvPublicKey(envContent,file){const names=envPublicKeyNames(file);for(const line of envContent.split(`
6
+ `)){const trimmed=line.trim();if(!trimmed||trimmed.startsWith("#"))continue;const match=trimmed.match(/^([^=]+)=["']?([^"'\n]+)["']?/);if(!match?.[1]||!match[2])continue;const key=match[1].trim();if(names.includes(key))return{name:key,key:match[2]}}return}export function reusableEnvPublicKey(envContent,publicKeyName){let publicKey,hasCiphertext=!1;for(const line of envContent.split(`
7
+ `)){const trimmed=line.trim();if(!trimmed||trimmed.startsWith("#"))continue;const match=trimmed.match(/^([^=]+)=(.*)$/);if(!match?.[1]||match[2]===void 0)continue;const key=match[1].trim(),value=match[2].trim().replace(/^["']|["']$/g,"");if(key===publicKeyName)publicKey=value;else if(value.startsWith("encrypted:")||value.startsWith("enc:"))hasCiphertext=!0}return publicKey&&hasCiphertext?publicKey:void 0}export function encryptEnv(options={}){const cwd=options.cwd||process.cwd(),envPath=resolve(cwd,options.file||".env"),keysPath=resolve(cwd,options.keysFile||".env.keys");if(!existsSync(envPath))return{success:!1,error:`File not found: ${envPath}`};try{let publicKey,privateKey;const existingPublicKey=reusableEnvPublicKey(readFileSync(envPath,"utf-8"),envKeyNames(options.file).publicKeyName);if(existingPublicKey)publicKey=existingPublicKey;else if(existsSync(keysPath)){const keysContent=readFileSync(keysPath,"utf-8"),{parsed}=parse(keysContent),{publicKeyName,privateKeyName}=envKeyNames(options.file);publicKey=parsed[publicKeyName]||"";privateKey=parsed[privateKeyName]||"";if(!publicKey||!privateKey){const keypair=generateKeypair();publicKey=keypair.publicKey;privateKey=keypair.privateKey;const newKeys=`
8
+ ${publicKeyName}="${publicKey}"
9
+ ${privateKeyName}="${privateKey}"
10
+ `;writeFileSync(keysPath,keysContent+newKeys,"utf-8")}}else{const keypair=generateKeypair();publicKey=keypair.publicKey;privateKey=keypair.privateKey;const{publicKeyName,privateKeyName}=envKeyNames(options.file),keysContent=`# .env.keys - Keep this file secure and never commit to source control
11
+ ${publicKeyName}="${publicKey}"
12
+ ${privateKeyName}="${privateKey}"
13
+ `;writeFileSync(keysPath,keysContent,"utf-8")}const envContent=readFileSync(envPath,"utf-8"),output=encryptedEnvContent(envContent,publicKey,envKeyNames(options.file).publicKeyName,options);if(options.stdout)return{success:!0,output};writeFileSync(envPath,output,"utf-8");return{success:!0,output:`\u2714 encrypted (${options.file||".env"})
14
+ \u2714 key added to ${options.keysFile||".env.keys"}`}}catch(error){return{success:!1,error:`Failed to encrypt: ${error instanceof Error?error.message:"Unknown error"}`}}}export function decryptEnv(options={}){const cwd=options.cwd||process.cwd(),envPath=resolve(cwd,options.file||".env"),keysPath=resolve(cwd,options.keysFile||".env.keys");if(!existsSync(envPath))return{success:!1,error:`File not found: ${envPath}`};if(!existsSync(keysPath))return{success:!1,error:`Keys file not found: ${keysPath}`};try{const keysContent=readFileSync(keysPath,"utf-8"),{parsed:keys}=parse(keysContent),env=((options.file||".env").split("/").pop()||"").replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase(),privateKeyName=env?`DOTENV_PRIVATE_KEY_${env}`:"DOTENV_PRIVATE_KEY",privateKey=keys[privateKeyName];if(!privateKey)return{success:!1,error:`Private key not found: ${privateKeyName}`};const privateKeys=[privateKey];if(privateKeyName!=="DOTENV_PRIVATE_KEY"&&keys.DOTENV_PRIVATE_KEY&&keys.DOTENV_PRIVATE_KEY!==privateKey)privateKeys.push(keys.DOTENV_PRIVATE_KEY);const lines=readFileSync(envPath,"utf-8").split(`
15
+ `),decryptedLines=[];for(const line of lines){const trimmed=line.trim();if(!trimmed||trimmed.startsWith("#")){decryptedLines.push(line);continue}if(trimmed.startsWith("DOTENV_PUBLIC_KEY"))continue;const match=trimmed.match(/^([^=]+)=(.*)$/);if(!match||match[1]===void 0||match[2]===void 0){decryptedLines.push(line);continue}const key=match[1].trim();let value=match[2].trim();if(value.startsWith('"')&&value.endsWith('"')||value.startsWith("'")&&value.endsWith("'"))value=value.slice(1,-1);let shouldDecrypt=value.startsWith("encrypted:");if(options.key&&!key.includes(options.key))shouldDecrypt=!1;if(shouldDecrypt){let decrypted,lastError;for(const candidate of privateKeys)try{decrypted=decryptValue(value,candidate);break}catch(error){lastError=error}if(decrypted===void 0)throw lastError instanceof Error?lastError:Error(`Unable to decrypt ${key}`);value=decrypted}decryptedLines.push(`${key}="${value}"`)}const output=decryptedLines.join(`
16
+ `);if(options.stdout)return{success:!0,output};writeFileSync(envPath,output,"utf-8");return{success:!0,output:`\u2714 decrypted (${options.file||".env"})`}}catch(error){return{success:!1,error:`Failed to decrypt: ${error instanceof Error?error.message:"Unknown error"}`}}}export function setEnv(key,value,options={}){const cwd=options.cwd||process.cwd(),envPath=resolve(cwd,options.file||".env"),keysPath=options.keysFile?resolve(cwd,options.keysFile):resolve(dirname(envPath),".env.keys");try{let content="";if(existsSync(envPath))content=readFileSync(envPath,"utf-8");const lines=content.split(`
17
+ `);let found=!1,publicKey;const env=((options.file||".env").split("/").pop()||"").replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase(),publicKeyName=env?`DOTENV_PUBLIC_KEY_${env}`:"DOTENV_PUBLIC_KEY",privateKeyName=env?`DOTENV_PRIVATE_KEY_${env}`:"DOTENV_PRIVATE_KEY",declared=declaredEnvPublicKey(content,options.file),declaredName=declared?.name??publicKeyName;publicKey=declared?.key;if(publicKey){let hasMatchingPrivateKey=!1;if(existsSync(keysPath)){const keysContent=readFileSync(keysPath,"utf-8"),{parsed:keys}=parse(keysContent);hasMatchingPrivateKey=envPrivateKeyNames(options.file).some((name)=>Boolean(keys[name]))}const privateKeyInEnv=envPrivateKeyNames(options.file).some((name)=>Boolean(process.env[name]));if(!hasMatchingPrivateKey&&!privateKeyInEnv&&!envHasCiphertext(content))publicKey=void 0}if(!options.plain&&!publicKey&&envHasCiphertext(content))return{success:!1,error:`${options.file||".env"} already contains encrypted values, but no key that can be used to add another was found.
21
18
  Generating one would leave every existing encrypted value unreadable.
22
- Provide the private key as ${L(e.file)[0]} in the environment, restore ${i}, or use --plain to write this value unencrypted.`};if(!e.plain&&!c){let v=R();c=v.publicKey;let y="";if(K(i))y=k(i,"utf-8");y+=`
23
- ${g}="${v.publicKey}"
24
- ${d}="${v.privateKey}"
25
- `,b(i,y,"utf-8");let x=ue(e.file);for(let w=u.length-1;w>=0;w--){let P=u[w];if(P!==void 0&&x.some((B)=>P.trim().startsWith(`${B}=`)))u.splice(w,1)}u.unshift(`${N}="${c}"`)}let h=n;if(!e.plain&&c)h=C(n,c);for(let v=0;v<u.length;v++){let y=u[v];if(y===void 0)continue;if(y.trim().startsWith(`${t}=`)){u[v]=`${t}="${h}"`,a=!0;break}}if(!a)u.push(`${t}="${h}"`);let T=u.join(`
26
- `);return b(r,T,"utf-8"),{success:!0,output:`set ${t}${e.plain?"":" with encryption"} (${e.file||".env"})`}}catch(o){return{success:!1,error:`Failed to set: ${o instanceof Error?o.message:"Unknown error"}`}}}function ct(t,n={}){let e=n.cwd||process.cwd(),s=E(e,n.file||".env");if(!K(s))return{success:!1,error:`File not found: ${s}`};try{let r,i=E(e,n.keysFile||".env.keys"),a=((n.file||".env").split("/").pop()||"").replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase(),c=a?`DOTENV_PRIVATE_KEY_${a}`:"DOTENV_PRIVATE_KEY";if(K(i)){let d=k(i,"utf-8"),{parsed:m}=O(d);r=m[c]}if(!r)r=process.env[c];let l=k(s,"utf-8"),{parsed:f}=O(l,{privateKey:r});if(t&&!n.all){let d=f[t];if(d===void 0)return{success:!1,error:`Key not found: ${t}`};return{success:!0,output:d}}let p=n.all?{...process.env,...f}:f,g;switch(n.format){case"shell":g=Object.entries(p).map(([d,m])=>`${d}=${m}`).join(" ");break;case"eval":g=Object.entries(p).map(([d,m])=>`${d}="${m}"`).join(`
27
- `);break;case"json":default:g=n.prettyPrint?JSON.stringify(p,null,2):JSON.stringify(p);break}return{success:!0,output:g}}catch(r){return{success:!1,error:`Failed to get: ${r instanceof Error?r.message:"Unknown error"}`}}}function at(t,n={}){let e=n.cwd||process.cwd(),s=E(e,n.keysFile||".env.keys");if(!K(s))return{success:!1,error:`Keys file not found: ${s}`};try{let r=k(s,"utf-8"),{parsed:i}=O(r),a=((n.file||".env").split("/").pop()||"").replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase(),c=a?`DOTENV_PUBLIC_KEY_${a}`:"DOTENV_PUBLIC_KEY",l=a?`DOTENV_PRIVATE_KEY_${a}`:"DOTENV_PRIVATE_KEY";if(t){let g=i[t];if(!g)return{success:!1,error:`Key not found: ${t}`};return{success:!0,output:g}}let f={[c]:i[c],[l]:i[l]};return{success:!0,output:n.format==="shell"?`${c}=${f[c]} ${l}=${f[l]}`:JSON.stringify(f)}}catch(r){return{success:!1,error:`Failed to get keypair: ${r instanceof Error?r.message:"Unknown error"}`}}}function Be(t,n,e,s){let r=t.split(`
28
- `),i=!1,o=!1;for(let u=0;u<r.length;u++){let a=r[u];if(a===void 0)continue;if(a.startsWith(`${e}=`))r[u]=`${e}="${n.publicKey}"`,i=!0;else if(a.startsWith(`${s}=`))r[u]=`${s}="${n.privateKey}"`,o=!0}if(!i)r.push(`${e}="${n.publicKey}"`);if(!o)r.push(`${s}="${n.privateKey}"`);return`${r.join(`
19
+ Provide the private key as ${envPrivateKeyNames(options.file)[0]} in the environment, restore ${keysPath}, or use --plain to write this value unencrypted.`};if(!options.plain&&!publicKey){const keypair=generateKeypair();publicKey=keypair.publicKey;let keysContent="";if(existsSync(keysPath))keysContent=readFileSync(keysPath,"utf-8");keysContent+=`
20
+ ${publicKeyName}="${keypair.publicKey}"
21
+ ${privateKeyName}="${keypair.privateKey}"
22
+ `;writeFileSync(keysPath,keysContent,"utf-8");const staleNames=envPublicKeyNames(options.file);for(let i=lines.length-1;i>=0;i--){const lineEntry=lines[i];if(lineEntry!==void 0&&staleNames.some((name)=>lineEntry.trim().startsWith(`${name}=`)))lines.splice(i,1)}lines.unshift(`${declaredName}="${publicKey}"`)}let finalValue=value;if(!options.plain&&publicKey)finalValue=encryptValue(value,publicKey);for(let i=0;i<lines.length;i++){const lineEntry=lines[i];if(lineEntry===void 0)continue;if(lineEntry.trim().startsWith(`${key}=`)){lines[i]=`${key}="${finalValue}"`;found=!0;break}}if(!found)lines.push(`${key}="${finalValue}"`);const output=lines.join(`
23
+ `);writeFileSync(envPath,output,"utf-8");return{success:!0,output:`set ${key}${options.plain?"":" with encryption"} (${options.file||".env"})`}}catch(error){return{success:!1,error:`Failed to set: ${error instanceof Error?error.message:"Unknown error"}`}}}export function getEnv(key,options={}){const cwd=options.cwd||process.cwd(),envPath=resolve(cwd,options.file||".env");if(!existsSync(envPath))return{success:!1,error:`File not found: ${envPath}`};try{let privateKey;const keysPath=resolve(cwd,options.keysFile||".env.keys"),env=((options.file||".env").split("/").pop()||"").replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase(),privateKeyName=env?`DOTENV_PRIVATE_KEY_${env}`:"DOTENV_PRIVATE_KEY";if(existsSync(keysPath)){const keysContent=readFileSync(keysPath,"utf-8"),{parsed:keys}=parse(keysContent);privateKey=keys[privateKeyName]}if(!privateKey)privateKey=process.env[privateKeyName];const envContent=readFileSync(envPath,"utf-8"),{parsed}=parse(envContent,{privateKey});if(key&&!options.all){const value=parsed[key];if(value===void 0)return{success:!1,error:`Key not found: ${key}`};return{success:!0,output:value}}const result=options.all?{...process.env,...parsed}:parsed;let output;switch(options.format){case"shell":output=Object.entries(result).map(([k,v])=>`${k}=${v}`).join(" ");break;case"eval":output=Object.entries(result).map(([k,v])=>`${k}="${v}"`).join(`
24
+ `);break;case"json":default:output=options.prettyPrint?JSON.stringify(result,null,2):JSON.stringify(result);break}return{success:!0,output}}catch(error){return{success:!1,error:`Failed to get: ${error instanceof Error?error.message:"Unknown error"}`}}}export function getKeypair(keyName,options={}){const cwd=options.cwd||process.cwd(),keysPath=resolve(cwd,options.keysFile||".env.keys");if(!existsSync(keysPath))return{success:!1,error:`Keys file not found: ${keysPath}`};try{const keysContent=readFileSync(keysPath,"utf-8"),{parsed}=parse(keysContent),env=((options.file||".env").split("/").pop()||"").replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase(),publicKeyName=env?`DOTENV_PUBLIC_KEY_${env}`:"DOTENV_PUBLIC_KEY",privateKeyName=env?`DOTENV_PRIVATE_KEY_${env}`:"DOTENV_PRIVATE_KEY";if(keyName){const value=parsed[keyName];if(!value)return{success:!1,error:`Key not found: ${keyName}`};return{success:!0,output:value}}const keypair={[publicKeyName]:parsed[publicKeyName],[privateKeyName]:parsed[privateKeyName]};return{success:!0,output:options.format==="shell"?`${publicKeyName}=${keypair[publicKeyName]} ${privateKeyName}=${keypair[privateKeyName]}`:JSON.stringify(keypair)}}catch(error){return{success:!1,error:`Failed to get keypair: ${error instanceof Error?error.message:"Unknown error"}`}}}function rotatedKeysContent(keysContent,keypair,publicKeyName,privateKeyName){const lines=keysContent.split(`
25
+ `);let replacedPublic=!1,replacedPrivate=!1;for(let i=0;i<lines.length;i++){const entry=lines[i];if(entry===void 0)continue;if(entry.startsWith(`${publicKeyName}=`)){lines[i]=`${publicKeyName}="${keypair.publicKey}"`;replacedPublic=!0}else if(entry.startsWith(`${privateKeyName}=`)){lines[i]=`${privateKeyName}="${keypair.privateKey}"`;replacedPrivate=!0}}if(!replacedPublic)lines.push(`${publicKeyName}="${keypair.publicKey}"`);if(!replacedPrivate)lines.push(`${privateKeyName}="${keypair.privateKey}"`);return`${lines.join(`
29
26
  `).replace(/\n+$/,"")}
30
- `}function ut(t={}){let n=Ce({file:t.file,keysFile:t.keysFile,stdout:!0,cwd:t.cwd});if(!n.success)return n;if(n.output===void 0)return{success:!1,error:"Rotation produced no decrypted content"};let e=t.cwd||process.cwd(),s=t.file||".env",r=t.keysFile||".env.keys",i=E(e,s),o=E(e,r),{publicKeyName:u,privateKeyName:a}=D(t.file);if(t.dryRun){let h=Re(n.output,t);return{success:!0,output:["Dry run: nothing was written.",`${s}: ${h.length} value${h.length===1?"":"s"} would be re-encrypted under a new ${u}.`,`${r}: ${u} and ${a} would be replaced.`,`The current ${a} decrypts nothing once that happens, so keep a copy until the rotation is verified.`].join(`
31
- `)}}let c=R(),l=ae(n.output,c.publicKey,u,t);if(t.stdout)return{success:!0,output:l,notice:[`Nothing was written. To adopt the output above, put this keypair in ${r}:`,"",`${u}="${c.publicKey}"`,`${a}="${c.privateKey}"`,"","Without it that output cannot be decrypted. Run without --stdout to have both applied together."].join(`
32
- `)};let f=k(i),p=k(o),g=Be(p.toString("utf8"),c,u,a),d=`${process.pid}-${Date.now()}`,m=E(j(i),`.${ie(i)}.${d}.rotate`),N=E(j(o),`.${ie(o)}.${d}.rotate`);try{b(m,l,{encoding:"utf8",mode:384}),b(N,g,{encoding:"utf8",mode:384}),re(m,i);try{re(N,o)}catch(h){throw b(i,f),h}return{success:!0,output:`\u2714 rotated (${s}) to encrypted:v2`}}catch(h){if(!K(o))b(o,p,{mode:384});return{success:!1,error:`Failed to rotate without exposing plaintext: ${h instanceof Error?h.message:"Unknown error"}`}}finally{se(m,{force:!0}),se(N,{force:!0})}}export{Fe as declaredEnvPublicKey,Ce as decryptEnv,it as encryptEnv,oe as envHasCiphertext,D as envKeyNames,L as envPrivateKeyNames,ue as envPublicKeyNames,ct as getEnv,at as getKeypair,st as resolveEnvFile,We as reusableEnvPublicKey,ut as rotateKeypair,ot as setEnv};
27
+ `}export function rotateKeypair(options={}){const decryptResult=decryptEnv({file:options.file,keysFile:options.keysFile,stdout:!0,cwd:options.cwd});if(!decryptResult.success)return decryptResult;if(decryptResult.output===void 0)return{success:!1,error:"Rotation produced no decrypted content"};const cwd=options.cwd||process.cwd(),envFileName=options.file||".env",keysFileName=options.keysFile||".env.keys",envPath=resolve(cwd,envFileName),keysPath=resolve(cwd,keysFileName),{publicKeyName,privateKeyName}=envKeyNames(options.file);if(options.dryRun){const values=encryptableKeys(decryptResult.output,options);return{success:!0,output:["Dry run: nothing was written.",`${envFileName}: ${values.length} value${values.length===1?"":"s"} would be re-encrypted under a new ${publicKeyName}.`,`${keysFileName}: ${publicKeyName} and ${privateKeyName} would be replaced.`,`The current ${privateKeyName} decrypts nothing once that happens, so keep a copy until the rotation is verified.`].join(`
28
+ `)}}const keypair=generateKeypair(),nextEnv=encryptedEnvContent(decryptResult.output,keypair.publicKey,publicKeyName,options);if(options.stdout)return{success:!0,output:nextEnv,notice:[`Nothing was written. To adopt the output above, put this keypair in ${keysFileName}:`,"",`${publicKeyName}="${keypair.publicKey}"`,`${privateKeyName}="${keypair.privateKey}"`,"","Without it that output cannot be decrypted. Run without --stdout to have both applied together."].join(`
29
+ `)};const originalEnv=readFileSync(envPath),originalKeys=readFileSync(keysPath),nextKeys=rotatedKeysContent(originalKeys.toString("utf8"),keypair,publicKeyName,privateKeyName),token=`${process.pid}-${Date.now()}`,envTemp=resolve(dirname(envPath),`.${basename(envPath)}.${token}.rotate`),keysTemp=resolve(dirname(keysPath),`.${basename(keysPath)}.${token}.rotate`);try{writeFileSync(envTemp,nextEnv,{encoding:"utf8",mode:384});writeFileSync(keysTemp,nextKeys,{encoding:"utf8",mode:384});renameSync(envTemp,envPath);try{renameSync(keysTemp,keysPath)}catch(error){writeFileSync(envPath,originalEnv);throw error}return{success:!0,output:`\u2714 rotated (${envFileName}) to encrypted:v2`}}catch(error){if(!existsSync(keysPath))writeFileSync(keysPath,originalKeys,{mode:384});return{success:!1,error:`Failed to rotate without exposing plaintext: ${error instanceof Error?error.message:"Unknown error"}`}}finally{rmSync(envTemp,{force:!0});rmSync(keysTemp,{force:!0})}}
package/dist/crypto.js CHANGED
@@ -1,2 +1 @@
1
- // @bun
2
- import{createCipheriv as x,createDecipheriv as w,createHash as l,createPrivateKey as S,createPublicKey as k,diffieHellman as K,generateKeyPairSync as b,hkdfSync as B,randomBytes as d}from"crypto";var E="x25519-public:",m="x25519-private:",h="encrypted:v2:",v="encrypted:",P=Buffer.from("stacks-env:v2","utf8"),T="Environment decryption failed: authentication or format error";function _(t){if(t.length===32)return t;return l("sha256").update(t).digest()}function L(t,e){let r=d(16),n=x("aes-256-gcm",_(e),r);return{ciphertext:Buffer.concat([n.update(t,"utf8"),n.final()]).toString("hex"),iv:r.toString("hex"),authTag:n.getAuthTag().toString("hex")}}function V(t,e,r,n){try{let i=w("aes-256-gcm",_(e),Buffer.from(r,"hex"));return i.setAuthTag(Buffer.from(n,"hex")),Buffer.concat([i.update(Buffer.from(t,"hex")),i.final()]).toString("utf8")}catch{throw Error("Decryption failed: authentication or format error")}}function s(t){return t.toString("base64url")}function c(t,e,r){if(typeof t!=="string"||t.length>0&&!/^[A-Za-z0-9_-]+$/.test(t))throw Error(`${e} is not canonical base64url`);let n=Buffer.from(t,"base64url");if(s(n)!==t)throw Error(`${e} is not canonical base64url`);if(r!==void 0&&n.length!==r)throw Error(`${e} has an invalid length`);return n}function D(t){if(!t.startsWith(E))throw Error("Legacy or invalid public key. Run buddy env:rotate before creating new ciphertext.");let e=k({key:c(t.slice(E.length),"public key"),format:"der",type:"spki"});if(e.asymmetricKeyType!=="x25519")throw Error("Public key is not X25519");return e}function I(t){if(!t.startsWith(m))throw Error("Private key is not a version 2 X25519 key");let e=S({key:c(t.slice(m.length),"private key"),format:"der",type:"pkcs8"});if(e.asymmetricKeyType!=="x25519")throw Error("Private key is not X25519");return e}function A(t){return Buffer.from(`stacks-env:v2;${t.epk};${t.salt};${t.nonce}`,"utf8")}function W(){let{publicKey:t,privateKey:e}=b("x25519"),r=t.export({format:"der",type:"spki"}),n=e.export({format:"der",type:"pkcs8"});return{publicKey:`${E}${s(r)}`,privateKey:`${m}${s(n)}`}}function O(t,e){let r=D(e),n=b("x25519"),i=s(n.publicKey.export({format:"der",type:"spki"})),o=d(16),f=d(12),g=K({privateKey:n.privateKey,publicKey:r}),y=Buffer.from(B("sha256",g,o,P,32)),u={epk:i,salt:s(o),nonce:s(f)};try{let a=x("aes-256-gcm",y,f);a.setAAD(A(u));let p=Buffer.concat([a.update(t,"utf8"),a.final()]),R={v:2,...u,ciphertext:s(p),tag:s(a.getAuthTag())};return`${h}${s(Buffer.from(JSON.stringify(R),"utf8"))}`}finally{g.fill(0),y.fill(0)}}function N(t){let e=c(t.slice(h.length),"envelope").toString("utf8"),r=JSON.parse(e);if(!r||typeof r!=="object"||Array.isArray(r))throw Error("envelope is not an object");let n=Object.keys(r).sort(),i=["ciphertext","epk","nonce","salt","tag","v"];if(n.length!==i.length||n.some((o,f)=>o!==i[f]))throw Error("envelope fields are invalid");if(r.v!==2)throw Error("envelope version is unsupported");for(let o of["epk","salt","nonce","ciphertext","tag"])if(typeof r[o]!=="string")throw Error(`${o} is invalid`);return r}function X(t,e){try{let r=N(t),n=I(e),i=k({key:c(r.epk,"ephemeral public key"),format:"der",type:"spki"});if(i.asymmetricKeyType!=="x25519")throw Error("ephemeral key is not X25519");let o=c(r.salt,"salt",16),f=c(r.nonce,"nonce",12),g=c(r.ciphertext,"ciphertext"),y=c(r.tag,"tag",16),u=K({privateKey:n,publicKey:i}),a=Buffer.from(B("sha256",u,o,P,32));try{let p=w("aes-256-gcm",a,f);return p.setAAD(A(r)),p.setAuthTag(y),Buffer.concat([p.update(g),p.final()]).toString("utf8")}finally{u.fill(0),a.fill(0)}}catch{throw Error(T)}}function F(t,e){try{if(!/^[0-9a-f]{64}$/.test(e))throw Error("invalid legacy private key");let r=Buffer.from(t.slice(v.length),"base64");if(r.length<32)throw Error("invalid legacy payload");let n=l("sha256").update(Buffer.from(e,"hex")).digest(),i=l("sha256").update(n).digest();return V(r.subarray(32).toString("hex"),i,r.subarray(0,16).toString("hex"),r.subarray(16,32).toString("hex"))}catch{throw Error(T)}}function H(t){return t.startsWith(v)&&!t.startsWith(h)}function Y(t,e){if(!t.startsWith(v))return t;return t.startsWith(h)?X(t,e):F(t,e)}function j(t,e,r){return O(Y(t,e),r)}function z(t){let e=t.match(/^DOTENV_PRIVATE_KEY_(.+)$/);return e&&e[1]?e[1].toLowerCase():""}function J(t=""){return t?process.env[`DOTENV_PRIVATE_KEY_${t.toUpperCase()}`]:process.env.DOTENV_PRIVATE_KEY}export{V as aesDecrypt,L as aesEncrypt,Y as decryptValue,O as encryptValue,W as generateKeypair,J as getPrivateKey,H as isLegacyEncryptedValue,j as migrateEncryptedValue,z as parseEnvFromKey};
1
+ import{createCipheriv,createDecipheriv,createHash,createPrivateKey,createPublicKey,diffieHellman,generateKeyPairSync,hkdfSync,randomBytes}from"node:crypto";const PUBLIC_KEY_PREFIX="x25519-public:",PRIVATE_KEY_PREFIX="x25519-private:",ENVELOPE_PREFIX="encrypted:v2:",LEGACY_PREFIX="encrypted:",HKDF_INFO=Buffer.from("stacks-env:v2","utf8"),DECRYPTION_ERROR="Environment decryption failed: authentication or format error";function encryptionKey(key){if(key.length===32)return key;return createHash("sha256").update(key).digest()}export function aesEncrypt(plaintext,key){const iv=randomBytes(16),cipher=createCipheriv("aes-256-gcm",encryptionKey(key),iv);return{ciphertext:Buffer.concat([cipher.update(plaintext,"utf8"),cipher.final()]).toString("hex"),iv:iv.toString("hex"),authTag:cipher.getAuthTag().toString("hex")}}export function aesDecrypt(ciphertext,key,iv,authTag){try{const decipher=createDecipheriv("aes-256-gcm",encryptionKey(key),Buffer.from(iv,"hex"));decipher.setAuthTag(Buffer.from(authTag,"hex"));return Buffer.concat([decipher.update(Buffer.from(ciphertext,"hex")),decipher.final()]).toString("utf8")}catch{throw Error("Decryption failed: authentication or format error")}}function encode(data){return data.toString("base64url")}function decode(value,label,expectedLength){if(typeof value!=="string"||value.length>0&&!/^[A-Za-z0-9_-]+$/.test(value))throw Error(`${label} is not canonical base64url`);const decoded=Buffer.from(value,"base64url");if(encode(decoded)!==value)throw Error(`${label} is not canonical base64url`);if(expectedLength!==void 0&&decoded.length!==expectedLength)throw Error(`${label} has an invalid length`);return decoded}function parsePublicKey(value){if(!value.startsWith(PUBLIC_KEY_PREFIX))throw Error("Legacy or invalid public key. Run buddy env:rotate before creating new ciphertext.");const key=createPublicKey({key:decode(value.slice(PUBLIC_KEY_PREFIX.length),"public key"),format:"der",type:"spki"});if(key.asymmetricKeyType!=="x25519")throw Error("Public key is not X25519");return key}function parsePrivateKey(value){if(!value.startsWith(PRIVATE_KEY_PREFIX))throw Error("Private key is not a version 2 X25519 key");const key=createPrivateKey({key:decode(value.slice(PRIVATE_KEY_PREFIX.length),"private key"),format:"der",type:"pkcs8"});if(key.asymmetricKeyType!=="x25519")throw Error("Private key is not X25519");return key}function aad(envelope){return Buffer.from(`stacks-env:v2;${envelope.epk};${envelope.salt};${envelope.nonce}`,"utf8")}export function generateKeypair(){const{publicKey,privateKey}=generateKeyPairSync("x25519"),publicDer=publicKey.export({format:"der",type:"spki"}),privateDer=privateKey.export({format:"der",type:"pkcs8"});return{publicKey:`${PUBLIC_KEY_PREFIX}${encode(publicDer)}`,privateKey:`${PRIVATE_KEY_PREFIX}${encode(privateDer)}`}}export function encryptValue(value,recipientPublicKey){const recipient=parsePublicKey(recipientPublicKey),ephemeral=generateKeyPairSync("x25519"),epk=encode(ephemeral.publicKey.export({format:"der",type:"spki"})),salt=randomBytes(16),nonce=randomBytes(12),shared=diffieHellman({privateKey:ephemeral.privateKey,publicKey:recipient}),key=Buffer.from(hkdfSync("sha256",shared,salt,HKDF_INFO,32)),metadata={epk,salt:encode(salt),nonce:encode(nonce)};try{const cipher=createCipheriv("aes-256-gcm",key,nonce);cipher.setAAD(aad(metadata));const ciphertext=Buffer.concat([cipher.update(value,"utf8"),cipher.final()]),envelope={v:2,...metadata,ciphertext:encode(ciphertext),tag:encode(cipher.getAuthTag())};return`${ENVELOPE_PREFIX}${encode(Buffer.from(JSON.stringify(envelope),"utf8"))}`}finally{shared.fill(0);key.fill(0)}}function parseEnvelope(value){const serialized=decode(value.slice(ENVELOPE_PREFIX.length),"envelope").toString("utf8"),parsed=JSON.parse(serialized);if(!parsed||typeof parsed!=="object"||Array.isArray(parsed))throw Error("envelope is not an object");const keys=Object.keys(parsed).sort(),expected=["ciphertext","epk","nonce","salt","tag","v"];if(keys.length!==expected.length||keys.some((key,index)=>key!==expected[index]))throw Error("envelope fields are invalid");if(parsed.v!==2)throw Error("envelope version is unsupported");for(const field of["epk","salt","nonce","ciphertext","tag"])if(typeof parsed[field]!=="string")throw Error(`${field} is invalid`);return parsed}function decryptV2(encryptedValue,recipientPrivateKey){try{const envelope=parseEnvelope(encryptedValue),recipient=parsePrivateKey(recipientPrivateKey),ephemeral=createPublicKey({key:decode(envelope.epk,"ephemeral public key"),format:"der",type:"spki"});if(ephemeral.asymmetricKeyType!=="x25519")throw Error("ephemeral key is not X25519");const salt=decode(envelope.salt,"salt",16),nonce=decode(envelope.nonce,"nonce",12),ciphertext=decode(envelope.ciphertext,"ciphertext"),tag=decode(envelope.tag,"tag",16),shared=diffieHellman({privateKey:recipient,publicKey:ephemeral}),key=Buffer.from(hkdfSync("sha256",shared,salt,HKDF_INFO,32));try{const decipher=createDecipheriv("aes-256-gcm",key,nonce);decipher.setAAD(aad(envelope));decipher.setAuthTag(tag);return Buffer.concat([decipher.update(ciphertext),decipher.final()]).toString("utf8")}finally{shared.fill(0);key.fill(0)}}catch{throw Error(DECRYPTION_ERROR)}}function decryptLegacy(encryptedValue,privateKey){try{if(!/^[0-9a-f]{64}$/.test(privateKey))throw Error("invalid legacy private key");const data=Buffer.from(encryptedValue.slice(LEGACY_PREFIX.length),"base64");if(data.length<32)throw Error("invalid legacy payload");const publicKeyHash=createHash("sha256").update(Buffer.from(privateKey,"hex")).digest(),key=createHash("sha256").update(publicKeyHash).digest();return aesDecrypt(data.subarray(32).toString("hex"),key,data.subarray(0,16).toString("hex"),data.subarray(16,32).toString("hex"))}catch{throw Error(DECRYPTION_ERROR)}}export function isLegacyEncryptedValue(value){return value.startsWith(LEGACY_PREFIX)&&!value.startsWith(ENVELOPE_PREFIX)}export function decryptValue(encryptedValue,privateKey){if(!encryptedValue.startsWith(LEGACY_PREFIX))return encryptedValue;return encryptedValue.startsWith(ENVELOPE_PREFIX)?decryptV2(encryptedValue,privateKey):decryptLegacy(encryptedValue,privateKey)}export function migrateEncryptedValue(encryptedValue,oldPrivateKey,newPublicKey){return encryptValue(decryptValue(encryptedValue,oldPrivateKey),newPublicKey)}export function parseEnvFromKey(keyName){const match=keyName.match(/^DOTENV_PRIVATE_KEY_(.+)$/);return match&&match[1]?match[1].toLowerCase():""}export function getPrivateKey(env=""){return env?process.env[`DOTENV_PRIVATE_KEY_${env.toUpperCase()}`]:process.env.DOTENV_PRIVATE_KEY}
@@ -0,0 +1 @@
1
+ import process from"node:process";export function isLoopbackHost(hostname){const host=hostname.toLowerCase();return host==="localhost"||host.endsWith(".localhost")||host==="127.0.0.1"||host==="0.0.0.0"||host==="::1"||host==="[::1]"}export function isLocalDeployment(){const name=(process.env.APP_ENV??process.env.NODE_ENV??"").toLowerCase();if(name==="production"||name==="prod"||name==="staging")return!1;const url=(process.env.APP_URL??"").trim();if(url)try{return isLoopbackHost(new URL(url.includes("://")?url:`https://${url}`).hostname)}catch{}return name===""||name==="local"||name==="dev"||name==="test"||name==="testing"}
package/dist/index.d.ts CHANGED
@@ -1,23 +1,4 @@
1
- import type { EnvKey, StacksEnv } from './types';
2
- export declare function process(): StacksEnv;
3
- // eslint-disable-next-line pickier/no-unused-vars
4
- export declare function writeEnv(key: EnvKey, value: string, options?: { path: string }): void;
5
- /**
6
- * Validate environment variables against the envEnum constraints.
7
- * Returns an array of validation error messages (empty if all valid).
8
- */
9
- export declare function validateEnv(envProxy?: StacksEnv): string[];
10
- /**
11
- * Assert that every key in `keys` is set on `process.env` (non-empty).
12
- *
13
- * Use this at boot to fail fast on misconfigured environments —
14
- * `requireEnv(['APP_KEY', 'STRIPE_SECRET'])` throws a single error
15
- * listing every missing key, which is much friendlier than "your
16
- * Stripe API key is empty" mid-checkout. Returns the env proxy so it
17
- * can be chained: `const envOk = requireEnv([...])`.
18
- */
19
- export declare function requireEnv(keys: ReadonlyArray<string>, envProxy?: StacksEnv): StacksEnv;
20
- export declare const env: StacksEnv;
1
+ export * from './runtime';
21
2
  export * from './deployment';
22
3
  export * from './types';
23
4
  export * from './crypto';
package/dist/index.js CHANGED
@@ -1,35 +1 @@
1
- // @bun
2
- import Sn from"process";import{projectPath as In}from"@stacksjs/path";import xe from"fs";import{existsSync as H}from"fs";import{readFileSync as te}from"fs";import{resolve as V}from"path";import{createCipheriv as ae,createDecipheriv as ue,createHash as z,createPrivateKey as $e,createPublicKey as fe,diffieHellman as le,generateKeyPairSync as Ee,hkdfSync as pe,randomBytes as J}from"crypto";var Z="x25519-public:",ee="x25519-private:",Y="encrypted:v2:",ne="encrypted:",ge=Buffer.from("stacks-env:v2","utf8"),_e="Environment decryption failed: authentication or format error";function ye(e){if(e.length===32)return e;return z("sha256").update(e).digest()}function Cn(e,n){let t=J(16),r=ae("aes-256-gcm",ye(n),t);return{ciphertext:Buffer.concat([r.update(e,"utf8"),r.final()]).toString("hex"),iv:t.toString("hex"),authTag:r.getAuthTag().toString("hex")}}function we(e,n,t,r){try{let i=ue("aes-256-gcm",ye(n),Buffer.from(t,"hex"));return i.setAuthTag(Buffer.from(r,"hex")),Buffer.concat([i.update(Buffer.from(e,"hex")),i.final()]).toString("utf8")}catch{throw Error("Decryption failed: authentication or format error")}}function C(e){return e.toString("base64url")}function b(e,n,t){if(typeof e!=="string"||e.length>0&&!/^[A-Za-z0-9_-]+$/.test(e))throw Error(`${n} is not canonical base64url`);let r=Buffer.from(e,"base64url");if(C(r)!==e)throw Error(`${n} is not canonical base64url`);if(t!==void 0&&r.length!==t)throw Error(`${n} has an invalid length`);return r}function Be(e){if(!e.startsWith(Z))throw Error("Legacy or invalid public key. Run buddy env:rotate before creating new ciphertext.");let n=fe({key:b(e.slice(Z.length),"public key"),format:"der",type:"spki"});if(n.asymmetricKeyType!=="x25519")throw Error("Public key is not X25519");return n}function Ve(e){if(!e.startsWith(ee))throw Error("Private key is not a version 2 X25519 key");let n=$e({key:b(e.slice(ee.length),"private key"),format:"der",type:"pkcs8"});if(n.asymmetricKeyType!=="x25519")throw Error("Private key is not X25519");return n}function me(e){return Buffer.from(`stacks-env:v2;${e.epk};${e.salt};${e.nonce}`,"utf8")}function U(){let{publicKey:e,privateKey:n}=Ee("x25519"),t=e.export({format:"der",type:"spki"}),r=n.export({format:"der",type:"pkcs8"});return{publicKey:`${Z}${C(t)}`,privateKey:`${ee}${C(r)}`}}function W(e,n){let t=Be(n),r=Ee("x25519"),i=C(r.publicKey.export({format:"der",type:"spki"})),s=J(16),o=J(12),d=le({privateKey:r.privateKey,publicKey:t}),a=Buffer.from(pe("sha256",d,s,ge,32)),c={epk:i,salt:C(s),nonce:C(o)};try{let f=ae("aes-256-gcm",a,o);f.setAAD(me(c));let u=Buffer.concat([f.update(e,"utf8"),f.final()]),l={v:2,...c,ciphertext:C(u),tag:C(f.getAuthTag())};return`${Y}${C(Buffer.from(JSON.stringify(l),"utf8"))}`}finally{d.fill(0),a.fill(0)}}function Me(e){let n=b(e.slice(Y.length),"envelope").toString("utf8"),t=JSON.parse(n);if(!t||typeof t!=="object"||Array.isArray(t))throw Error("envelope is not an object");let r=Object.keys(t).sort(),i=["ciphertext","epk","nonce","salt","tag","v"];if(r.length!==i.length||r.some((s,o)=>s!==i[o]))throw Error("envelope fields are invalid");if(t.v!==2)throw Error("envelope version is unsupported");for(let s of["epk","salt","nonce","ciphertext","tag"])if(typeof t[s]!=="string")throw Error(`${s} is invalid`);return t}function Fe(e,n){try{let t=Me(e),r=Ve(n),i=fe({key:b(t.epk,"ephemeral public key"),format:"der",type:"spki"});if(i.asymmetricKeyType!=="x25519")throw Error("ephemeral key is not X25519");let s=b(t.salt,"salt",16),o=b(t.nonce,"nonce",12),d=b(t.ciphertext,"ciphertext"),a=b(t.tag,"tag",16),c=le({privateKey:r,publicKey:i}),f=Buffer.from(pe("sha256",c,s,ge,32));try{let u=ue("aes-256-gcm",f,o);return u.setAAD(me(t)),u.setAuthTag(a),Buffer.concat([u.update(d),u.final()]).toString("utf8")}finally{c.fill(0),f.fill(0)}}catch{throw Error(_e)}}function Ye(e,n){try{if(!/^[0-9a-f]{64}$/.test(n))throw Error("invalid legacy private key");let t=Buffer.from(e.slice(ne.length),"base64");if(t.length<32)throw Error("invalid legacy payload");let r=z("sha256").update(Buffer.from(n,"hex")).digest(),i=z("sha256").update(r).digest();return we(t.subarray(32).toString("hex"),i,t.subarray(0,16).toString("hex"),t.subarray(16,32).toString("hex"))}catch{throw Error(_e)}}function bn(e){return e.startsWith(ne)&&!e.startsWith(Y)}function K(e,n){if(!e.startsWith(ne))return e;return e.startsWith(Y)?Fe(e,n):Ye(e,n)}function Ln(e,n,t){return W(K(e,n),t)}function Kn(e){let n=e.match(/^DOTENV_PRIVATE_KEY_(.+)$/);return n&&n[1]?n[1].toLowerCase():""}function G(e=""){return e?process.env[`DOTENV_PRIVATE_KEY_${e.toUpperCase()}`]:process.env.DOTENV_PRIVATE_KEY}import{spawnSync as We}from"child_process";import{readFileSync as Ge}from"fs";import{arch as He,hostname as je,platform as Te,release as Xe,userInfo as Qe}from"os";import{basename as qe,dirname as ze,resolve as Je}from"path";function w(e){return typeof e==="string"&&(e.startsWith("encrypted:")||e.startsWith("enc:"))}function B(e,n=Object.keys(e)){let t=n.map((r)=>e[r]).filter((r)=>w(r));return Object.fromEntries(Object.entries(e).filter(([,r])=>r===void 0||!t.some((i)=>r.includes(i))))}function h(e,n={}){let t={},r=[],i=[],s=e.split(`
3
- `),o=B(n.processEnv||process.env);for(let d of s){let a=d.trim();if(!a||a.startsWith("#"))continue;if(a.startsWith("DOTENV_PUBLIC_KEY=")){let l=a.match(/^DOTENV_PUBLIC_KEY=["']?([^"'\n]+)["']?/);if(l&&l[1]!==void 0)t.DOTENV_PUBLIC_KEY=l[1];continue}let c=a.match(/^([^=]+)=(.*)$/);if(!c||c[1]===void 0||c[2]===void 0)continue;let f=c[1].trim(),u=c[2].trim();if(u.startsWith('"')&&u.endsWith('"')||u.startsWith("'")&&u.endsWith("'")){if(u=u.slice(1,-1),u.includes("\\n"))u=u.replace(/\\n/g,`
4
- `)}if(u.startsWith("encrypted:")||u.startsWith("enc:")){if(!n.privateKey){i.push(f);continue}try{let l=u.startsWith("enc:")?`encrypted:${u.slice(4)}`:u;u=K(l,n.privateKey)}catch(l){r.push(`Failed to decrypt ${f}: ${l instanceof Error?l.message:"Unknown error"}`),i.push(f);continue}}u=Ze(u,{...o,...t}),u=rn(u),t[f]=u}return{parsed:t,errors:r,skippedEncrypted:i}}function Ze(e,n){return e.replace(/\$\{([^}]+)\}/g,(t,r)=>{let i=r.match(/^([^:\-+]+)(:-|-)(.+)$/);if(i){let[,o,d,a]=i,c=n[o];if(d===":-")return c||a;else return c!==void 0?c:a}let s=r.match(/^([^:\-+]+)(:?\+)(.+)$/);if(s){let[,o,d,a]=s,c=n[o];if(d===":+")return c?a:"";else return c!==void 0?a:""}return n[r]||""})}var en=new Set(["date","hostname","whoami","uname","pwd","echo","printf","cat","basename","dirname"]);function nn(e){let n=[],t="",r=null,i=!1;for(let s of e.trim()){if(i){t+=s,i=!1;continue}if(s==="\\"&&r!=="'"){i=!0;continue}if(r){if(s===r)r=null;else t+=s;continue}if(s==='"'||s==="'"){r=s;continue}if(/\s/.test(s)){if(t)n.push(t),t="";continue}t+=s}if(i||r)throw Error("Command substitution contains an unterminated escape or quote");if(t)n.push(t);return n}function tn(e){let[n,...t]=e,r=t.some((i)=>i.startsWith("-"));if(n==="echo"&&!r)return t.join(" ");if(n==="printf"&&t.length===1&&!t[0]?.includes("%"))return t[0];if(n==="hostname"&&t.length===0)return je();if(n==="whoami"&&t.length===0)return Qe().username;if(n==="pwd"&&t.length===0)return process.cwd();if(n==="basename"&&t.length===1)return qe(t[0]??"");if(n==="dirname"&&t.length===1)return ze(t[0]??"");if(n==="cat"&&t.length>0&&!r)return t.map((i)=>Ge(Je(process.cwd(),i),"utf8")).join("");if(n==="uname"){let i=t[0]??"-s";if(t.length<=1&&i==="-m")return He();if(t.length<=1&&i==="-r")return Xe();if(t.length<=1&&i==="-s")return{aix:"AIX",android:"Android",cygwin:"CYGWIN",darwin:"Darwin",freebsd:"FreeBSD",haiku:"Haiku",linux:"Linux",netbsd:"NetBSD",openbsd:"OpenBSD",sunos:"SunOS",win32:"Windows_NT"}[Te()]??Te()}return}function rn(e){return e.replace(/\$\(([^)]+)\)/g,(n,t)=>{try{let r=nn(t),i=r[0];if(!i||!en.has(i))return console.warn(`[env] Blocked command substitution for disallowed command: ${i}`),"";let s=tn(r);if(s!==void 0)return s.trim();let o=We(i,r.slice(1),{encoding:"utf8",env:process.env});if(o.status===0)return o.stdout.trim();let d=o.stderr.trim();console.warn(`[env] Command substitution failed (exit code ${o.status??"unknown"}): ${i}${d?`: ${d}`:""}`)}catch(r){console.warn(`[env] Command substitution error: ${r instanceof Error?r.message:String(r)}`)}return""})}async function Fn(e,n={}){let t={},r=[],i=[];for(let s of e)try{let o=Bun.file(s);if(!o.size)continue;let d=await o.text(),{parsed:a,errors:c,skippedEncrypted:f}=h(d,n);r.push(...c),i.push(...f);for(let[u,l]of Object.entries(a)){if(!n.overload&&t[u]!==void 0)continue;t[u]=l}}catch(o){if(o?.code==="ENOENT")continue;r.push(`Failed to read ${s}: ${o instanceof Error?o.message:String(o)}`)}return{parsed:t,errors:r,skippedEncrypted:i}}function L(e){if(!e||w(e))return;let n=e.trim().toLowerCase();if(!/^[a-z0-9_-]+$/.test(n))return;if(n==="prod")return"production";if(n==="stage")return"staging";if(n==="dev")return"development";return n}var j=null,X=null,Q,q;function sn(e,n,t=".env.keys"){let r=V(n,t);if(!H(r))return;try{let{parsed:i}=h(te(r,"utf-8"));if(e){let s=i[`DOTENV_PRIVATE_KEY_${e.toUpperCase()}`];if(s)return s}return i.DOTENV_PRIVATE_KEY}catch{return}}function re(e={}){return L(e.env)||q||L("development")||L(process.env.DOTENV_ENV)||L(process.env.APP_ENV)||"development"}function on(e={}){let n=re(e),t=V(e.cwd||process.cwd());if(j===n&&X===t)return Q;let r=G(n);if(!r)r=process.env.DOTENV_PRIVATE_KEY;if(!r)r=sn(n,t);return j=n,X=t,Q=r,r}function qn(){j=null,X=null,Q=void 0,q=void 0}function Re(e,n={}){if(!w(e))return e;let t=on(n);if(!t)return;try{let r=e.startsWith("enc:")?`encrypted:${e.slice(4)}`:e;return K(r,t)}catch{return}}function ve(e={}){let{path:n=[".env"],overload:t=!1,env:r,privateKey:i,keysFile:s=".env.keys",quiet:o=!1,cwd:d=process.cwd()}=e,a=Array.isArray(n)?n:[n],c=[],f=0,u=new Set(Object.entries(B(process.env)).filter(([,g])=>g!==void 0).map(([g])=>g)),l=i;if(!l&&s){let g=V(d,s);if(H(g))try{let p=te(g,"utf-8"),{parsed:y}=h(p);if(r){let v=`DOTENV_PRIVATE_KEY_${r.toUpperCase()}`;l=y[v]||y.DOTENV_PRIVATE_KEY}else l=y.DOTENV_PRIVATE_KEY}catch(p){c.push(`Failed to load keys file: ${p instanceof Error?p.message:"Unknown error"}`)}}if(!l)l=G(r||"")||G();for(let g of a){let p=V(d,g);if(!H(p)){if(!o)c.push(`File not found: ${p}`);continue}try{let y=te(p,"utf-8"),{parsed:v,errors:m,skippedEncrypted:O}=h(y,{privateKey:l,processEnv:process.env});if(c.push(...m),O.length>0){let _=B(process.env,O);for(let F of Object.keys(process.env))if(!Object.hasOwn(_,F))delete process.env[F];let A=r?`DOTENV_PRIVATE_KEY_${r.toUpperCase()}`:"DOTENV_PRIVATE_KEY",R=l?"decryption failed":`${A} not set`,N=O.slice(0,5).join(", "),k=O.length>5?`, \u2026 +${O.length-5} more`:"";console.warn(`[env] warning: skipped ${O.length} encrypted value(s) in ${g} (${R}; existing plaintext or defaults apply): ${N}${k}`)}let T=0;for(let[_,A]of Object.entries(v)){if(_==="DOTENV_PUBLIC_KEY")continue;let R=process.env[_],N=w(R)&&A!==R;if(t||!u.has(_)||N)process.env[_]=A,f++,T++}if(!o&&!process.env.__ENV_LOADED__)console.error(`[env] loaded ${T}/${Object.keys(v).length} variables from ${g}`),process.env.__ENV_LOADED__="1"}catch(y){c.push(`Failed to load ${p}: ${y instanceof Error?y.message:"Unknown error"}`)}}return{loaded:f,errors:c}}function dn(e={}){return{name:"env-plugin",setup(n){ve(e)}}}function zn(e={}){let n=L(e.env)||L("development")||L(process.env.DOTENV_ENV)||L(process.env.APP_ENV)||"development",t=e.cwd||process.cwd();if(q!==n)q=n,j=null,X=null,Q=void 0;let r=[],i=(d)=>{if(!r.includes(d)&&H(V(t,d)))r.push(d)};i(".env"),i(".env.local");let s=`.env.${n}`,o=`.env.${n}.local`;return i(s),i(o),ve({...e,path:r,env:n})}var Zn=dn;var Oe={APP_ENV:["local","dev","development","staging","prod","production"],DB_CONNECTION:["mysql","sqlite","postgres","singlestore","vitess","dynamodb"],DB_MIGRATE_FRESH:["allow","confirm","disabled"],MAIL_MAILER:["smtp","mailgun","ses","log","sendgrid","mailtrap","capture"],SEARCH_ENGINE_DRIVER:["opensearch","meilisearch","algolia","typesense"],FRONTEND_APP_ENV:["development","staging","production"]};function et(e){return e}import ie from"process";function cn(e){let n=e.toLowerCase();return n==="localhost"||n.endsWith(".localhost")||n==="127.0.0.1"||n==="0.0.0.0"||n==="::1"||n==="[::1]"}function rt(){let e=(ie.env.APP_ENV??ie.env.NODE_ENV??"").toLowerCase();if(e==="production"||e==="prod"||e==="staging")return!1;let n=(ie.env.APP_URL??"").trim();if(n)try{return cn(new URL(n.includes("://")?n:`https://${n}`).hostname)}catch{}return e===""||e==="local"||e==="dev"||e==="test"||e==="testing"}import{existsSync as P,readFileSync as I,renameSync as Ae,rmSync as Se,writeFileSync as D}from"fs";import{basename as Ie,dirname as oe,resolve as S}from"path";function at(e,n){if(e)return e;let t=String(n??"").trim();if(!t||t==="development"||t==="dev"||t==="local")return"";if(!/^[\w.-]+$/.test(t))return"";return`.env.${t}`}function he(e,n){return(!n.key||e.includes(n.key))&&(!n.excludeKey||!e.includes(n.excludeKey))}function an(e,n){let t=[];for(let r of e.split(`
5
- `)){let i=r.trim();if(!i||i.startsWith("#"))continue;if(i.startsWith("DOTENV_PUBLIC_KEY"))continue;let s=i.match(/^([^=]+)=(.*)$/);if(!s||s[1]===void 0)continue;let o=s[1].trim();if(he(o,n))t.push(o)}return t}function Pe(e,n,t,r){let i=["#/-------------------[DOTENV_PUBLIC_KEY]--------------------/","#/ versioned X25519 encryption for .env files /","#/ [how it works](https://stacksjs.com/encryption) /","#/----------------------------------------------------------/",`${t}="${n}"`,""],s=!1;for(let o of e.split(`
6
- `)){let d=o.trim();if(d.startsWith("#/"))continue;if(d.startsWith("DOTENV_PUBLIC_KEY"))continue;if(!d&&!s)continue;if(!d||d.startsWith("#")){s=!0,i.push(o);continue}s=!0;let a=d.match(/^([^=]+)=(.*)$/);if(!a||a[1]===void 0||a[2]===void 0){i.push(o);continue}let c=a[1].trim(),f=a[2].trim();if(f.startsWith('"')&&f.endsWith('"')||f.startsWith("'")&&f.endsWith("'"))f=f.slice(1,-1);if(he(c,r)&&!f.startsWith("encrypted:"))f=W(f,n);i.push(`${c}="${f}"`)}return i.join(`
7
- `)}function x(e){let t=((e||".env").split("/").pop()||"").replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase();return{publicKeyName:t?`DOTENV_PUBLIC_KEY_${t}`:"DOTENV_PUBLIC_KEY",privateKeyName:t?`DOTENV_PRIVATE_KEY_${t}`:"DOTENV_PRIVATE_KEY"}}function De(e){let{publicKeyName:n}=x(e);return n==="DOTENV_PUBLIC_KEY"?[n]:[n,"DOTENV_PUBLIC_KEY"]}function se(e){let{privateKeyName:n}=x(e);return n==="DOTENV_PRIVATE_KEY"?[n]:[n,"DOTENV_PRIVATE_KEY"]}function Ne(e){for(let n of e.split(`
8
- `)){let t=n.trim();if(!t||t.startsWith("#"))continue;let r=t.match(/^([^=]+)=(.*)$/);if(!r?.[1]||r[2]===void 0)continue;let i=r[2].trim().replace(/^["']|["']$/g,"");if(i.startsWith("encrypted:")||i.startsWith("enc:"))return!0}return!1}function un(e,n){let t=De(n);for(let r of e.split(`
9
- `)){let i=r.trim();if(!i||i.startsWith("#"))continue;let s=i.match(/^([^=]+)=["']?([^"'\n]+)["']?/);if(!s?.[1]||!s[2])continue;let o=s[1].trim();if(t.includes(o))return{name:o,key:s[2]}}return}function fn(e,n){let t,r=!1;for(let i of e.split(`
10
- `)){let s=i.trim();if(!s||s.startsWith("#"))continue;let o=s.match(/^([^=]+)=(.*)$/);if(!o?.[1]||o[2]===void 0)continue;let d=o[1].trim(),a=o[2].trim().replace(/^["']|["']$/g,"");if(d===n)t=a;else if(a.startsWith("encrypted:")||a.startsWith("enc:"))r=!0}return t&&r?t:void 0}function ut(e={}){let n=e.cwd||process.cwd(),t=S(n,e.file||".env"),r=S(n,e.keysFile||".env.keys");if(!P(t))return{success:!1,error:`File not found: ${t}`};try{let i,s,o=fn(I(t,"utf-8"),x(e.file).publicKeyName);if(o)i=o;else if(P(r)){let c=I(r,"utf-8"),{parsed:f}=h(c),{publicKeyName:u,privateKeyName:l}=x(e.file);if(i=f[u]||"",s=f[l]||"",!i||!s){let g=U();i=g.publicKey,s=g.privateKey;let p=`
11
- ${u}="${i}"
12
- ${l}="${s}"
13
- `;D(r,c+p,"utf-8")}}else{let c=U();i=c.publicKey,s=c.privateKey;let{publicKeyName:f,privateKeyName:u}=x(e.file),l=`# .env.keys - Keep this file secure and never commit to source control
14
- ${f}="${i}"
15
- ${u}="${s}"
16
- `;D(r,l,"utf-8")}let d=I(t,"utf-8"),a=Pe(d,i,x(e.file).publicKeyName,e);if(e.stdout)return{success:!0,output:a};return D(t,a,"utf-8"),{success:!0,output:`\u2714 encrypted (${e.file||".env"})
17
- \u2714 key added to ${e.keysFile||".env.keys"}`}}catch(i){return{success:!1,error:`Failed to encrypt: ${i instanceof Error?i.message:"Unknown error"}`}}}function ln(e={}){let n=e.cwd||process.cwd(),t=S(n,e.file||".env"),r=S(n,e.keysFile||".env.keys");if(!P(t))return{success:!1,error:`File not found: ${t}`};if(!P(r))return{success:!1,error:`Keys file not found: ${r}`};try{let i=I(r,"utf-8"),{parsed:s}=h(i),a=((e.file||".env").split("/").pop()||"").replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase(),c=a?`DOTENV_PRIVATE_KEY_${a}`:"DOTENV_PRIVATE_KEY",f=s[c];if(!f)return{success:!1,error:`Private key not found: ${c}`};let u=[f];if(c!=="DOTENV_PRIVATE_KEY"&&s.DOTENV_PRIVATE_KEY&&s.DOTENV_PRIVATE_KEY!==f)u.push(s.DOTENV_PRIVATE_KEY);let g=I(t,"utf-8").split(`
18
- `),p=[];for(let v of g){let m=v.trim();if(!m||m.startsWith("#")){p.push(v);continue}if(m.startsWith("DOTENV_PUBLIC_KEY"))continue;let O=m.match(/^([^=]+)=(.*)$/);if(!O||O[1]===void 0||O[2]===void 0){p.push(v);continue}let T=O[1].trim(),_=O[2].trim();if(_.startsWith('"')&&_.endsWith('"')||_.startsWith("'")&&_.endsWith("'"))_=_.slice(1,-1);let A=_.startsWith("encrypted:");if(e.key&&!T.includes(e.key))A=!1;if(A){let R,N;for(let k of u)try{R=K(_,k);break}catch(F){N=F}if(R===void 0)throw N instanceof Error?N:Error(`Unable to decrypt ${T}`);_=R}p.push(`${T}="${_}"`)}let y=p.join(`
19
- `);if(e.stdout)return{success:!0,output:y};return D(t,y,"utf-8"),{success:!0,output:`\u2714 decrypted (${e.file||".env"})`}}catch(i){return{success:!1,error:`Failed to decrypt: ${i instanceof Error?i.message:"Unknown error"}`}}}function ft(e,n,t={}){let r=t.cwd||process.cwd(),i=S(r,t.file||".env"),s=t.keysFile?S(r,t.keysFile):S(oe(i),".env.keys");try{let o="";if(P(i))o=I(i,"utf-8");let d=o.split(`
20
- `),a=!1,c,l=((t.file||".env").split("/").pop()||"").replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase(),g=l?`DOTENV_PUBLIC_KEY_${l}`:"DOTENV_PUBLIC_KEY",p=l?`DOTENV_PRIVATE_KEY_${l}`:"DOTENV_PRIVATE_KEY",y=un(o,t.file),v=y?.name??g;if(c=y?.key,c){let T=!1;if(P(s)){let A=I(s,"utf-8"),{parsed:R}=h(A);T=se(t.file).some((N)=>Boolean(R[N]))}let _=se(t.file).some((A)=>Boolean(process.env[A]));if(!T&&!_&&!Ne(o))c=void 0}if(!t.plain&&!c&&Ne(o))return{success:!1,error:`${t.file||".env"} already contains encrypted values, but no key that can be used to add another was found.
21
- Generating one would leave every existing encrypted value unreadable.
22
- Provide the private key as ${se(t.file)[0]} in the environment, restore ${s}, or use --plain to write this value unencrypted.`};if(!t.plain&&!c){let T=U();c=T.publicKey;let _="";if(P(s))_=I(s,"utf-8");_+=`
23
- ${g}="${T.publicKey}"
24
- ${p}="${T.privateKey}"
25
- `,D(s,_,"utf-8");let A=De(t.file);for(let R=d.length-1;R>=0;R--){let N=d[R];if(N!==void 0&&A.some((k)=>N.trim().startsWith(`${k}=`)))d.splice(R,1)}d.unshift(`${v}="${c}"`)}let m=n;if(!t.plain&&c)m=W(n,c);for(let T=0;T<d.length;T++){let _=d[T];if(_===void 0)continue;if(_.trim().startsWith(`${e}=`)){d[T]=`${e}="${m}"`,a=!0;break}}if(!a)d.push(`${e}="${m}"`);let O=d.join(`
26
- `);return D(i,O,"utf-8"),{success:!0,output:`set ${e}${t.plain?"":" with encryption"} (${t.file||".env"})`}}catch(o){return{success:!1,error:`Failed to set: ${o instanceof Error?o.message:"Unknown error"}`}}}function lt(e,n={}){let t=n.cwd||process.cwd(),r=S(t,n.file||".env");if(!P(r))return{success:!1,error:`File not found: ${r}`};try{let i,s=S(t,n.keysFile||".env.keys"),a=((n.file||".env").split("/").pop()||"").replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase(),c=a?`DOTENV_PRIVATE_KEY_${a}`:"DOTENV_PRIVATE_KEY";if(P(s)){let p=I(s,"utf-8"),{parsed:y}=h(p);i=y[c]}if(!i)i=process.env[c];let f=I(r,"utf-8"),{parsed:u}=h(f,{privateKey:i});if(e&&!n.all){let p=u[e];if(p===void 0)return{success:!1,error:`Key not found: ${e}`};return{success:!0,output:p}}let l=n.all?{...process.env,...u}:u,g;switch(n.format){case"shell":g=Object.entries(l).map(([p,y])=>`${p}=${y}`).join(" ");break;case"eval":g=Object.entries(l).map(([p,y])=>`${p}="${y}"`).join(`
27
- `);break;case"json":default:g=n.prettyPrint?JSON.stringify(l,null,2):JSON.stringify(l);break}return{success:!0,output:g}}catch(i){return{success:!1,error:`Failed to get: ${i instanceof Error?i.message:"Unknown error"}`}}}function Et(e,n={}){let t=n.cwd||process.cwd(),r=S(t,n.keysFile||".env.keys");if(!P(r))return{success:!1,error:`Keys file not found: ${r}`};try{let i=I(r,"utf-8"),{parsed:s}=h(i),a=((n.file||".env").split("/").pop()||"").replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase(),c=a?`DOTENV_PUBLIC_KEY_${a}`:"DOTENV_PUBLIC_KEY",f=a?`DOTENV_PRIVATE_KEY_${a}`:"DOTENV_PRIVATE_KEY";if(e){let g=s[e];if(!g)return{success:!1,error:`Key not found: ${e}`};return{success:!0,output:g}}let u={[c]:s[c],[f]:s[f]};return{success:!0,output:n.format==="shell"?`${c}=${u[c]} ${f}=${u[f]}`:JSON.stringify(u)}}catch(i){return{success:!1,error:`Failed to get keypair: ${i instanceof Error?i.message:"Unknown error"}`}}}function En(e,n,t,r){let i=e.split(`
28
- `),s=!1,o=!1;for(let d=0;d<i.length;d++){let a=i[d];if(a===void 0)continue;if(a.startsWith(`${t}=`))i[d]=`${t}="${n.publicKey}"`,s=!0;else if(a.startsWith(`${r}=`))i[d]=`${r}="${n.privateKey}"`,o=!0}if(!s)i.push(`${t}="${n.publicKey}"`);if(!o)i.push(`${r}="${n.privateKey}"`);return`${i.join(`
29
- `).replace(/\n+$/,"")}
30
- `}function pt(e={}){let n=ln({file:e.file,keysFile:e.keysFile,stdout:!0,cwd:e.cwd});if(!n.success)return n;if(n.output===void 0)return{success:!1,error:"Rotation produced no decrypted content"};let t=e.cwd||process.cwd(),r=e.file||".env",i=e.keysFile||".env.keys",s=S(t,r),o=S(t,i),{publicKeyName:d,privateKeyName:a}=x(e.file);if(e.dryRun){let m=an(n.output,e);return{success:!0,output:["Dry run: nothing was written.",`${r}: ${m.length} value${m.length===1?"":"s"} would be re-encrypted under a new ${d}.`,`${i}: ${d} and ${a} would be replaced.`,`The current ${a} decrypts nothing once that happens, so keep a copy until the rotation is verified.`].join(`
31
- `)}}let c=U(),f=Pe(n.output,c.publicKey,d,e);if(e.stdout)return{success:!0,output:f,notice:[`Nothing was written. To adopt the output above, put this keypair in ${i}:`,"",`${d}="${c.publicKey}"`,`${a}="${c.privateKey}"`,"","Without it that output cannot be decrypted. Run without --stdout to have both applied together."].join(`
32
- `)};let u=I(s),l=I(o),g=En(l.toString("utf8"),c,d,a),p=`${process.pid}-${Date.now()}`,y=S(oe(s),`.${Ie(s)}.${p}.rotate`),v=S(oe(o),`.${Ie(o)}.${p}.rotate`);try{D(y,f,{encoding:"utf8",mode:384}),D(v,g,{encoding:"utf8",mode:384}),Ae(y,s);try{Ae(v,o)}catch(m){throw D(s,u),m}return{success:!0,output:`\u2714 rotated (${r}) to encrypted:v2`}}catch(m){if(!P(o))D(o,l,{mode:384});return{success:!1,error:`Failed to rotate without exposing plaintext: ${m instanceof Error?m.message:"Unknown error"}`}}finally{Se(y,{force:!0}),Se(v,{force:!0})}}function pn(e){return typeof e==="string"&&(e.startsWith("encrypted:")||e.startsWith("enc:"))}var gn=/(?:^|_)(?:PASSWORD|PASSWD|SECRET|TOKEN|APIKEY|API_KEY|CREDENTIAL|CREDENTIALS|PRIVATE|SALT|DSN|CERT|CIPHER|SIGNATURE|WEBHOOK)(?:_|$)|(?:^|_)(?:ACCESS|SECRET)_KEY(?:_|$)|_KEY$/,_n=/^DOTENV_(?:PUBLIC|PRIVATE)_KEY/,yn=/^\$\{[^}]+\}$/;function _t(e,n={}){let{placeholders:t={},strict:r=!1}=n,i=[];for(let[s,o]of Object.entries(e)){if(!o||o.trim()==="")continue;if(pn(o))continue;if(_n.test(s))continue;if(yn.test(o.trim()))continue;let d=gn.test(s),a=Object.prototype.hasOwnProperty.call(t,s)&&t[s]===o;if(d&&!a){i.push({key:s,reason:"plaintext-secret"});continue}if(r)i.push({key:s,reason:"plaintext-value"})}return i}function yt(e){return e.split(`
33
- `).map((n)=>n.trim()).filter(Boolean).filter((n)=>n!==".env.example").filter((n)=>/(?:^|\/)\.env(?:\.|$)/.test(n)).filter((n)=>!n.endsWith(".env.keys"))}function Ce(e){return`${e.replace(/[^a-z0-9]/gi,"").toUpperCase()}_`}function mn(e,n={}){let t=n.self?Ce(n.self):void 0,r=(n.tenants??[]).filter((o)=>typeof o==="string"&&o.trim().length>0).map((o)=>({slug:o,prefix:Ce(o)})).filter(({prefix:o})=>o!==t).sort((o,d)=>d.prefix.length-o.prefix.length),i={},s={};for(let[o,d]of Object.entries(e)){let a=r.find(({prefix:c})=>o.startsWith(c));if(a){(s[a.slug]??={})[o]=d;continue}i[o]=d}return{own:i,foreign:s}}function Tt(e,n={}){return mn(e,n).own}function Rt(e){return Object.entries(e.foreign).map(([n,t])=>({tenant:n,keys:Object.keys(t).sort()})).sort((n,t)=>n.tenant.localeCompare(t.tenant))}import E from"process";import{platform as Tn}from"os";var be=typeof Bun<"u",Le=typeof E<"u"&&E.versions?.node!==void 0,Rn=be?"bun":Le?"node":"unknown",St={name:Rn,version:be?Bun.version:Le?E.version:void 0},ce=Tn(),It=ce==="win32",Nt=ce==="darwin",ht=ce==="linux",Ke=Boolean(E.stdout?.isTTY),Pt=typeof globalThis.window<"u";function M(){return E.env.APP_ENV||E.env.NODE_ENV||"local"}function Dt(){return M()==="local"}function Ct(){let e=M();return e==="development"||e==="dev"||e==="local"}function bt(){return M()==="staging"}function Lt(){let e=M();return e==="production"||e==="prod"}function Kt(){let e=M();return e==="test"||e==="testing"}var vn=Boolean(E.env.CI||E.env.CONTINUOUS_INTEGRATION||E.env.BUILD_NUMBER||E.env.RUN_ID),xt=Boolean(E.env.DEBUG||E.env.VERBOSE||E.argv.includes("--debug")||E.argv.includes("--verbose")),On=vn||!Ke,kt=Boolean(!On&&(Ke||E.env.COLORTERM||E.env.FORCE_COLOR||E.env.TERM&&E.env.TERM!=="dumb")),de={github:{name:"GitHub Actions",detected:Boolean(E.env.GITHUB_ACTIONS)},gitlab:{name:"GitLab CI",detected:Boolean(E.env.GITLAB_CI)},circle:{name:"CircleCI",detected:Boolean(E.env.CIRCLECI)},travis:{name:"Travis CI",detected:Boolean(E.env.TRAVIS)},jenkins:{name:"Jenkins",detected:Boolean(E.env.JENKINS_URL)},vercel:{name:"Vercel",detected:Boolean(E.env.VERCEL)},netlify:{name:"Netlify",detected:Boolean(E.env.NETLIFY)},heroku:{name:"Heroku",detected:Boolean(E.env.DYNO)},aws:{name:"AWS",detected:Boolean(E.env.AWS_REGION||E.env.AWS_LAMBDA_FUNCTION_NAME)},azure:{name:"Azure",detected:Boolean(E.env.AZURE_HTTP_USER_AGENT)},cloudflare:{name:"Cloudflare",detected:Boolean(E.env.CF_PAGES)},railway:{name:"Railway",detected:Boolean(E.env.RAILWAY_ENVIRONMENT)},render:{name:"Render",detected:Boolean(E.env.RENDER)}},An=Object.keys(de).find((e)=>{let n=de[e];return n!==void 0&&n.detected})||"unknown",Ut=de[An]||{name:"Unknown",detected:!1};var ke=new Set;function Nn(e){if(ke.has(e))return;ke.add(e);let n=re();console.warn(`[env] warning: "${e}" is encrypted but no usable private key was found for the "${n}" environment; falling back to its default. Set DOTENV_PRIVATE_KEY_${n.toUpperCase()} or ship .env.keys.`)}var hn={get:(e,n)=>{let t=e,r=t[n];if(typeof r==="string"&&(r.startsWith("encrypted:")||r.startsWith("enc:"))){let s=Re(r);if(s===void 0){Nn(n),delete t[n];return}t[n]=s,r=s}let i=["_PORT","_TIMEOUT","_TTL","_SIZE","_LIMIT","_MAX","_MIN","_INTERVAL","_RETRIES","_CONCURRENCY","_WORKERS","_CONNECTIONS"];if(typeof r==="string"&&/^\d+$/.test(r)&&!r.startsWith("0")&&i.some((s)=>n.endsWith(s)))return Number(r);if(typeof r==="string"){let s=r.toLowerCase();if(s==="true")return!0;if(s==="false")return!1}return r},set:(e,n,t)=>Reflect.set(e,n,t,e),deleteProperty:(e,n)=>Reflect.deleteProperty(e,n)};function Pn(){return typeof Bun<"u"?Bun.env:Sn.env}var Ue=new Proxy(Pn(),hn);function Yt(e,n,t){let r=t?.path||In(".env"),s=xe.readFileSync(r,"utf-8").split(`
34
- `),o=s.findIndex((c)=>c.startsWith(`${e}=`)),a=/[\s"'#$\\]/.test(n)?`"${n.replace(/"/g,"\\\"")}"`:n;if(o!==-1)s[o]=`${e}=${a}`;else s.push(`${e}=${a}`);xe.writeFileSync(r,s.join(`
35
- `))}function Wt(e=Ue){let n=[],t=e;for(let[r,i]of Object.entries(Oe)){let s=t[r];if(s!==void 0&&s!==""&&!i.includes(String(s)))n.push(`${r}="${s}" is not valid. Allowed values: ${i.join(", ")}`)}return n}function Gt(e,n=Ue){let t=[],r=n;for(let i of e){let s=r[i];if(s===void 0||s===""||s===null)t.push(i)}if(t.length>0)throw Error(`[env] Missing required environment variable(s): ${t.join(", ")}. Set them in .env or your process environment before booting.`);return n}export{gn as SECRET_NAME_PATTERN,re as activeEnvName,we as aesDecrypt,Cn as aesEncrypt,M as appEnv,zn as autoLoadEnv,un as declaredEnvPublicKey,ln as decryptEnv,Re as decryptEnvValue,K as decryptValue,et as defineEnv,ut as encryptEnv,W as encryptValue,Ue as env,Oe as envEnum,Ne as envHasCiphertext,x as envKeyNames,dn as envPlugin,se as envPrivateKeyNames,De as envPublicKeyNames,Rt as foreignTenantKeys,U as generateKeypair,lt as getEnv,Et as getKeypair,G as getPrivateKey,Ke as hasTTY,Pt as hasWindow,be as isBun,vn as isCI,kt as isColorSupported,xt as isDebug,Ct as isDevelopment,pn as isEncryptedValue,bn as isLegacyEncryptedValue,ht as isLinux,Dt as isLocal,rt as isLocalDeployment,cn as isLoopbackHost,Nt as isMacOS,On as isMinimal,Le as isNode,Lt as isProduction,bt as isStaging,Kt as isTesting,It as isWindows,ve as loadEnv,Fn as loadEnvFiles,Ln as migrateEncryptedValue,h as parse,Kn as parseEnvFromKey,mn as partitionTenantEnv,_t as plaintextSecrets,ce as platform,Pn as process,An as provider,Ut as providerInfo,Gt as requireEnv,qn as resetPrivateKeyCache,at as resolveEnvFile,on as resolvePrivateKey,fn as reusableEnvPublicKey,pt as rotateKeypair,Rn as runtime,St as runtimeInfo,ft as setEnv,Tt as stripForeignTenantEnv,Ce as tenantEnvPrefix,yt as trackedEnvFiles,Wt as validateEnv,Yt as writeEnv};
1
+ export*from"./runtime";export*from"./deployment";export*from"./types";export*from"./crypto";export*from"./parser";export*from"./plugin";export*from"./cli";export*from"./plaintext";export*from"./tenants";export*from"./utils";
package/dist/parser.js CHANGED
@@ -1,4 +1,3 @@
1
- // @bun
2
- import{spawnSync as X}from"child_process";import{readFileSync as U}from"fs";import{arch as H,hostname as z,platform as T,release as M,userInfo as q}from"os";import{basename as G,dirname as J,resolve as Z}from"path";import{createCipheriv as k,createDecipheriv as b,createHash as m,createPrivateKey as D,createPublicKey as K,diffieHellman as B,generateKeyPairSync as P,hkdfSync as _,randomBytes as y}from"crypto";var E="x25519-public:",w="x25519-private:",h="encrypted:v2:",v="encrypted:",R=Buffer.from("stacks-env:v2","utf8"),S="Environment decryption failed: authentication or format error";function O(e){if(e.length===32)return e;return m("sha256").update(e).digest()}function ot(e,r){let t=y(16),n=k("aes-256-gcm",O(r),t);return{ciphertext:Buffer.concat([n.update(e,"utf8"),n.final()]).toString("hex"),iv:t.toString("hex"),authTag:n.getAuthTag().toString("hex")}}function W(e,r,t,n){try{let i=b("aes-256-gcm",O(r),Buffer.from(t,"hex"));return i.setAuthTag(Buffer.from(n,"hex")),Buffer.concat([i.update(Buffer.from(e,"hex")),i.final()]).toString("utf8")}catch{throw Error("Decryption failed: authentication or format error")}}function d(e){return e.toString("base64url")}function g(e,r,t){if(typeof e!=="string"||e.length>0&&!/^[A-Za-z0-9_-]+$/.test(e))throw Error(`${r} is not canonical base64url`);let n=Buffer.from(e,"base64url");if(d(n)!==e)throw Error(`${r} is not canonical base64url`);if(t!==void 0&&n.length!==t)throw Error(`${r} has an invalid length`);return n}function A(e){if(!e.startsWith(E))throw Error("Legacy or invalid public key. Run buddy env:rotate before creating new ciphertext.");let r=K({key:g(e.slice(E.length),"public key"),format:"der",type:"spki"});if(r.asymmetricKeyType!=="x25519")throw Error("Public key is not X25519");return r}function C(e){if(!e.startsWith(w))throw Error("Private key is not a version 2 X25519 key");let r=D({key:g(e.slice(w.length),"private key"),format:"der",type:"pkcs8"});if(r.asymmetricKeyType!=="x25519")throw Error("Private key is not X25519");return r}function V(e){return Buffer.from(`stacks-env:v2;${e.epk};${e.salt};${e.nonce}`,"utf8")}function at(){let{publicKey:e,privateKey:r}=P("x25519"),t=e.export({format:"der",type:"spki"}),n=r.export({format:"der",type:"pkcs8"});return{publicKey:`${E}${d(t)}`,privateKey:`${w}${d(n)}`}}function I(e,r){let t=A(r),n=P("x25519"),i=d(n.publicKey.export({format:"der",type:"spki"})),s=y(16),a=y(12),u=B({privateKey:n.privateKey,publicKey:t}),f=Buffer.from(_("sha256",u,s,R,32)),c={epk:i,salt:d(s),nonce:d(a)};try{let p=k("aes-256-gcm",f,a);p.setAAD(V(c));let o=Buffer.concat([p.update(e,"utf8"),p.final()]),l={v:2,...c,ciphertext:d(o),tag:d(p.getAuthTag())};return`${h}${d(Buffer.from(JSON.stringify(l),"utf8"))}`}finally{u.fill(0),f.fill(0)}}function F(e){let r=g(e.slice(h.length),"envelope").toString("utf8"),t=JSON.parse(r);if(!t||typeof t!=="object"||Array.isArray(t))throw Error("envelope is not an object");let n=Object.keys(t).sort(),i=["ciphertext","epk","nonce","salt","tag","v"];if(n.length!==i.length||n.some((s,a)=>s!==i[a]))throw Error("envelope fields are invalid");if(t.v!==2)throw Error("envelope version is unsupported");for(let s of["epk","salt","nonce","ciphertext","tag"])if(typeof t[s]!=="string")throw Error(`${s} is invalid`);return t}function L(e,r){try{let t=F(e),n=C(r),i=K({key:g(t.epk,"ephemeral public key"),format:"der",type:"spki"});if(i.asymmetricKeyType!=="x25519")throw Error("ephemeral key is not X25519");let s=g(t.salt,"salt",16),a=g(t.nonce,"nonce",12),u=g(t.ciphertext,"ciphertext"),f=g(t.tag,"tag",16),c=B({privateKey:n,publicKey:i}),p=Buffer.from(_("sha256",c,s,R,32));try{let o=b("aes-256-gcm",p,a);return o.setAAD(V(t)),o.setAuthTag(f),Buffer.concat([o.update(u),o.final()]).toString("utf8")}finally{c.fill(0),p.fill(0)}}catch{throw Error(S)}}function Y(e,r){try{if(!/^[0-9a-f]{64}$/.test(r))throw Error("invalid legacy private key");let t=Buffer.from(e.slice(v.length),"base64");if(t.length<32)throw Error("invalid legacy payload");let n=m("sha256").update(Buffer.from(r,"hex")).digest(),i=m("sha256").update(n).digest();return W(t.subarray(32).toString("hex"),i,t.subarray(0,16).toString("hex"),t.subarray(16,32).toString("hex"))}catch{throw Error(S)}}function ct(e){return e.startsWith(v)&&!e.startsWith(h)}function x(e,r){if(!e.startsWith(v))return e;return e.startsWith(h)?L(e,r):Y(e,r)}function ft(e,r,t){return I(x(e,r),t)}function ut(e){let r=e.match(/^DOTENV_PRIVATE_KEY_(.+)$/);return r&&r[1]?r[1].toLowerCase():""}function pt(e=""){return e?process.env[`DOTENV_PRIVATE_KEY_${e.toUpperCase()}`]:process.env.DOTENV_PRIVATE_KEY}function j(e){return typeof e==="string"&&(e.startsWith("encrypted:")||e.startsWith("enc:"))}function N(e,r=Object.keys(e)){let t=r.map((n)=>e[n]).filter((n)=>j(n));return Object.fromEntries(Object.entries(e).filter(([,n])=>n===void 0||!t.some((i)=>n.includes(i))))}function Q(e,r={}){let t={},n=[],i=[],s=e.split(`
3
- `),a=N(r.processEnv||process.env);for(let u of s){let f=u.trim();if(!f||f.startsWith("#"))continue;if(f.startsWith("DOTENV_PUBLIC_KEY=")){let l=f.match(/^DOTENV_PUBLIC_KEY=["']?([^"'\n]+)["']?/);if(l&&l[1]!==void 0)t.DOTENV_PUBLIC_KEY=l[1];continue}let c=f.match(/^([^=]+)=(.*)$/);if(!c||c[1]===void 0||c[2]===void 0)continue;let p=c[1].trim(),o=c[2].trim();if(o.startsWith('"')&&o.endsWith('"')||o.startsWith("'")&&o.endsWith("'")){if(o=o.slice(1,-1),o.includes("\\n"))o=o.replace(/\\n/g,`
4
- `)}if(o.startsWith("encrypted:")||o.startsWith("enc:")){if(!r.privateKey){i.push(p);continue}try{let l=o.startsWith("enc:")?`encrypted:${o.slice(4)}`:o;o=x(l,r.privateKey)}catch(l){n.push(`Failed to decrypt ${p}: ${l instanceof Error?l.message:"Unknown error"}`),i.push(p);continue}}o=tt(o,{...a,...t}),o=it(o),t[p]=o}return{parsed:t,errors:n,skippedEncrypted:i}}function tt(e,r){return e.replace(/\$\{([^}]+)\}/g,(t,n)=>{let i=n.match(/^([^:\-+]+)(:-|-)(.+)$/);if(i){let[,a,u,f]=i,c=r[a];if(u===":-")return c||f;else return c!==void 0?c:f}let s=n.match(/^([^:\-+]+)(:?\+)(.+)$/);if(s){let[,a,u,f]=s,c=r[a];if(u===":+")return c?f:"";else return c!==void 0?f:""}return r[n]||""})}var et=new Set(["date","hostname","whoami","uname","pwd","echo","printf","cat","basename","dirname"]);function rt(e){let r=[],t="",n=null,i=!1;for(let s of e.trim()){if(i){t+=s,i=!1;continue}if(s==="\\"&&n!=="'"){i=!0;continue}if(n){if(s===n)n=null;else t+=s;continue}if(s==='"'||s==="'"){n=s;continue}if(/\s/.test(s)){if(t)r.push(t),t="";continue}t+=s}if(i||n)throw Error("Command substitution contains an unterminated escape or quote");if(t)r.push(t);return r}function nt(e){let[r,...t]=e,n=t.some((i)=>i.startsWith("-"));if(r==="echo"&&!n)return t.join(" ");if(r==="printf"&&t.length===1&&!t[0]?.includes("%"))return t[0];if(r==="hostname"&&t.length===0)return z();if(r==="whoami"&&t.length===0)return q().username;if(r==="pwd"&&t.length===0)return process.cwd();if(r==="basename"&&t.length===1)return G(t[0]??"");if(r==="dirname"&&t.length===1)return J(t[0]??"");if(r==="cat"&&t.length>0&&!n)return t.map((i)=>U(Z(process.cwd(),i),"utf8")).join("");if(r==="uname"){let i=t[0]??"-s";if(t.length<=1&&i==="-m")return H();if(t.length<=1&&i==="-r")return M();if(t.length<=1&&i==="-s")return{aix:"AIX",android:"Android",cygwin:"CYGWIN",darwin:"Darwin",freebsd:"FreeBSD",haiku:"Haiku",linux:"Linux",netbsd:"NetBSD",openbsd:"OpenBSD",sunos:"SunOS",win32:"Windows_NT"}[T()]??T()}return}function it(e){return e.replace(/\$\(([^)]+)\)/g,(r,t)=>{try{let n=rt(t),i=n[0];if(!i||!et.has(i))return console.warn(`[env] Blocked command substitution for disallowed command: ${i}`),"";let s=nt(n);if(s!==void 0)return s.trim();let a=X(i,n.slice(1),{encoding:"utf8",env:process.env});if(a.status===0)return a.stdout.trim();let u=a.stderr.trim();console.warn(`[env] Command substitution failed (exit code ${a.status??"unknown"}): ${i}${u?`: ${u}`:""}`)}catch(n){console.warn(`[env] Command substitution error: ${n instanceof Error?n.message:String(n)}`)}return""})}async function vt(e,r={}){let t={},n=[],i=[];for(let s of e)try{let a=Bun.file(s);if(!a.size)continue;let u=await a.text(),{parsed:f,errors:c,skippedEncrypted:p}=Q(u,r);n.push(...c),i.push(...p);for(let[o,l]of Object.entries(f)){if(!r.overload&&t[o]!==void 0)continue;t[o]=l}}catch(a){if(a?.code==="ENOENT")continue;n.push(`Failed to read ${s}: ${a instanceof Error?a.message:String(a)}`)}return{parsed:t,errors:n,skippedEncrypted:i}}export{vt as loadEnvFiles,Q as parse};
1
+ import{spawnSync}from"node:child_process";import{readFileSync}from"node:fs";import{arch,hostname,platform,release,userInfo}from"node:os";import{basename,dirname,resolve}from"node:path";import{decryptValue}from"./crypto";import{plaintextEnv}from"./plaintext-env";export function parse(src,options={}){const parsed={},errors=[],skippedEncrypted=[],lines=src.split(`
2
+ `),processEnv=plaintextEnv(options.processEnv||process.env);for(const rawLine of lines){const line=rawLine.trim();if(!line||line.startsWith("#"))continue;if(line.startsWith("DOTENV_PUBLIC_KEY=")){const match=line.match(/^DOTENV_PUBLIC_KEY=["']?([^"'\n]+)["']?/);if(match&&match[1]!==void 0)parsed.DOTENV_PUBLIC_KEY=match[1];continue}const match=line.match(/^([^=]+)=(.*)$/);if(!match||match[1]===void 0||match[2]===void 0)continue;const key=match[1].trim();let value=match[2].trim();if(value.startsWith('"')&&value.endsWith('"')||value.startsWith("'")&&value.endsWith("'")){value=value.slice(1,-1);if(value.includes("\\n"))value=value.replace(/\\n/g,`
3
+ `)}if(value.startsWith("encrypted:")||value.startsWith("enc:")){if(!options.privateKey){skippedEncrypted.push(key);continue}try{const normalized=value.startsWith("enc:")?`encrypted:${value.slice(4)}`:value;value=decryptValue(normalized,options.privateKey)}catch(error){errors.push(`Failed to decrypt ${key}: ${error instanceof Error?error.message:"Unknown error"}`);skippedEncrypted.push(key);continue}}value=expandVariables(value,{...processEnv,...parsed});value=expandCommands(value);parsed[key]=value}return{parsed,errors,skippedEncrypted}}function expandVariables(value,env){return value.replace(/\$\{([^}]+)\}/g,(match,expression)=>{const defaultMatch=expression.match(/^([^:\-+]+)(:-|-)(.+)$/);if(defaultMatch){const[,varName,operator,defaultValue]=defaultMatch,varValue=env[varName];if(operator===":-")return varValue||defaultValue;else return varValue!==void 0?varValue:defaultValue}const alternateMatch=expression.match(/^([^:\-+]+)(:?\+)(.+)$/);if(alternateMatch){const[,varName,operator,alternateValue]=alternateMatch,varValue=env[varName];if(operator===":+")return varValue?alternateValue:"";else return varValue!==void 0?alternateValue:""}return env[expression]||""})}const ALLOWED_ENV_COMMANDS=new Set(["date","hostname","whoami","uname","pwd","echo","printf","cat","basename","dirname"]);function splitCommand(command){const parts=[];let current="",quote=null,escaped=!1;for(const character of command.trim()){if(escaped){current+=character;escaped=!1;continue}if(character==="\\"&&quote!=="'"){escaped=!0;continue}if(quote){if(character===quote)quote=null;else current+=character;continue}if(character==='"'||character==="'"){quote=character;continue}if(/\s/.test(character)){if(current){parts.push(current);current=""}continue}current+=character}if(escaped||quote)throw Error("Command substitution contains an unterminated escape or quote");if(current)parts.push(current);return parts}function portableCommand(parts){const[executable,...args]=parts,hasFlags=args.some((argument)=>argument.startsWith("-"));if(executable==="echo"&&!hasFlags)return args.join(" ");if(executable==="printf"&&args.length===1&&!args[0]?.includes("%"))return args[0];if(executable==="hostname"&&args.length===0)return hostname();if(executable==="whoami"&&args.length===0)return userInfo().username;if(executable==="pwd"&&args.length===0)return process.cwd();if(executable==="basename"&&args.length===1)return basename(args[0]??"");if(executable==="dirname"&&args.length===1)return dirname(args[0]??"");if(executable==="cat"&&args.length>0&&!hasFlags)return args.map((path)=>readFileSync(resolve(process.cwd(),path),"utf8")).join("");if(executable==="uname"){const flag=args[0]??"-s";if(args.length<=1&&flag==="-m")return arch();if(args.length<=1&&flag==="-r")return release();if(args.length<=1&&flag==="-s")return{aix:"AIX",android:"Android",cygwin:"CYGWIN",darwin:"Darwin",freebsd:"FreeBSD",haiku:"Haiku",linux:"Linux",netbsd:"NetBSD",openbsd:"OpenBSD",sunos:"SunOS",win32:"Windows_NT"}[platform()]??platform()}return}function expandCommands(value){return value.replace(/\$\(([^)]+)\)/g,(_match,command)=>{try{const parts=splitCommand(command),executable=parts[0];if(!executable||!ALLOWED_ENV_COMMANDS.has(executable)){console.warn(`[env] Blocked command substitution for disallowed command: ${executable}`);return""}const portableResult=portableCommand(parts);if(portableResult!==void 0)return portableResult.trim();const result=spawnSync(executable,parts.slice(1),{encoding:"utf8",env:process.env});if(result.status===0)return result.stdout.trim();const detail=result.stderr.trim();console.warn(`[env] Command substitution failed (exit code ${result.status??"unknown"}): ${executable}${detail?`: ${detail}`:""}`)}catch(error){console.warn(`[env] Command substitution error: ${error instanceof Error?error.message:String(error)}`)}return""})}export async function loadEnvFiles(files,options={}){const allParsed={},allErrors=[],allSkipped=[];for(const file of files)try{const content=Bun.file(file);if(!content.size)continue;const src=await content.text(),{parsed,errors,skippedEncrypted}=parse(src,options);allErrors.push(...errors);allSkipped.push(...skippedEncrypted);for(const[key,value]of Object.entries(parsed)){if(!options.overload&&allParsed[key]!==void 0)continue;allParsed[key]=value}}catch(error){if(error?.code==="ENOENT")continue;allErrors.push(`Failed to read ${file}: ${error instanceof Error?error.message:String(error)}`)}return{parsed:allParsed,errors:allErrors,skippedEncrypted:allSkipped}}
@@ -0,0 +1 @@
1
+ export function isEncryptedValue(value){return typeof value==="string"&&(value.startsWith("encrypted:")||value.startsWith("enc:"))}export function plaintextEnv(values,encryptedKeys=Object.keys(values)){const ciphertext=encryptedKeys.map((key)=>values[key]).filter((value)=>isEncryptedValue(value));return Object.fromEntries(Object.entries(values).filter(([,value])=>value===void 0||!ciphertext.some((encrypted)=>value.includes(encrypted))))}
@@ -0,0 +1,2 @@
1
+ export function isEncryptedValue(value){return typeof value==="string"&&(value.startsWith("encrypted:")||value.startsWith("enc:"))}export const SECRET_NAME_PATTERN=/(?:^|_)(?:PASSWORD|PASSWD|SECRET|TOKEN|APIKEY|API_KEY|CREDENTIAL|CREDENTIALS|PRIVATE|SALT|DSN|CERT|CIPHER|SIGNATURE|WEBHOOK)(?:_|$)|(?:^|_)(?:ACCESS|SECRET)_KEY(?:_|$)|_KEY$/;const CRYPTO_METADATA_PATTERN=/^DOTENV_(?:PUBLIC|PRIVATE)_KEY/,EXPANSION_PATTERN=/^\$\{[^}]+\}$/;export function plaintextSecrets(values,options={}){const{placeholders={},strict=!1}=options,findings=[];for(const[key,value]of Object.entries(values)){if(!value||value.trim()==="")continue;if(isEncryptedValue(value))continue;if(CRYPTO_METADATA_PATTERN.test(key))continue;if(EXPANSION_PATTERN.test(value.trim()))continue;const looksSecret=SECRET_NAME_PATTERN.test(key),isPlaceholder=Object.prototype.hasOwnProperty.call(placeholders,key)&&placeholders[key]===value;if(looksSecret&&!isPlaceholder){findings.push({key,reason:"plaintext-secret"});continue}if(strict)findings.push({key,reason:"plaintext-value"})}return findings}export function trackedEnvFiles(gitLsFilesOutput){return gitLsFilesOutput.split(`
2
+ `).map((line)=>line.trim()).filter(Boolean).filter((file)=>file!==".env.example").filter((file)=>/(?:^|\/)\.env(?:\.|$)/.test(file)).filter((file)=>!file.endsWith(".env.keys"))}
package/dist/plugin.js CHANGED
@@ -1,4 +1 @@
1
- // @bun
2
- import{existsSync as D}from"fs";import{readFileSync as j}from"fs";import{resolve as b}from"path";import{createCipheriv as z,createDecipheriv as H,createHash as C,createPrivateKey as oe,createPublicKey as q,diffieHellman as M,generateKeyPairSync as G,hkdfSync as J,randomBytes as F}from"crypto";var W="x25519-public:",Y="x25519-private:",N="encrypted:v2:",L="encrypted:",Z=Buffer.from("stacks-env:v2","utf8"),Q="Environment decryption failed: authentication or format error";function ee(e){if(e.length===32)return e;return C("sha256").update(e).digest()}function Be(e,n){let t=F(16),r=z("aes-256-gcm",ee(n),t);return{ciphertext:Buffer.concat([r.update(e,"utf8"),r.final()]).toString("hex"),iv:t.toString("hex"),authTag:r.getAuthTag().toString("hex")}}function ce(e,n,t,r){try{let s=H("aes-256-gcm",ee(n),Buffer.from(t,"hex"));return s.setAuthTag(Buffer.from(r,"hex")),Buffer.concat([s.update(Buffer.from(e,"hex")),s.final()]).toString("utf8")}catch{throw Error("Decryption failed: authentication or format error")}}function h(e){return e.toString("base64url")}function m(e,n,t){if(typeof e!=="string"||e.length>0&&!/^[A-Za-z0-9_-]+$/.test(e))throw Error(`${n} is not canonical base64url`);let r=Buffer.from(e,"base64url");if(h(r)!==e)throw Error(`${n} is not canonical base64url`);if(t!==void 0&&r.length!==t)throw Error(`${n} has an invalid length`);return r}function ae(e){if(!e.startsWith(W))throw Error("Legacy or invalid public key. Run buddy env:rotate before creating new ciphertext.");let n=q({key:m(e.slice(W.length),"public key"),format:"der",type:"spki"});if(n.asymmetricKeyType!=="x25519")throw Error("Public key is not X25519");return n}function fe(e){if(!e.startsWith(Y))throw Error("Private key is not a version 2 X25519 key");let n=oe({key:m(e.slice(Y.length),"private key"),format:"der",type:"pkcs8"});if(n.asymmetricKeyType!=="x25519")throw Error("Private key is not X25519");return n}function te(e){return Buffer.from(`stacks-env:v2;${e.epk};${e.salt};${e.nonce}`,"utf8")}function Re(){let{publicKey:e,privateKey:n}=G("x25519"),t=e.export({format:"der",type:"spki"}),r=n.export({format:"der",type:"pkcs8"});return{publicKey:`${W}${h(t)}`,privateKey:`${Y}${h(r)}`}}function ue(e,n){let t=ae(n),r=G("x25519"),s=h(r.publicKey.export({format:"der",type:"spki"})),i=F(16),c=F(12),f=M({privateKey:r.privateKey,publicKey:t}),u=Buffer.from(J("sha256",f,i,Z,32)),a={epk:s,salt:h(i),nonce:h(c)};try{let d=z("aes-256-gcm",u,c);d.setAAD(te(a));let o=Buffer.concat([d.update(e,"utf8"),d.final()]),p={v:2,...a,ciphertext:h(o),tag:h(d.getAuthTag())};return`${N}${h(Buffer.from(JSON.stringify(p),"utf8"))}`}finally{f.fill(0),u.fill(0)}}function pe(e){let n=m(e.slice(N.length),"envelope").toString("utf8"),t=JSON.parse(n);if(!t||typeof t!=="object"||Array.isArray(t))throw Error("envelope is not an object");let r=Object.keys(t).sort(),s=["ciphertext","epk","nonce","salt","tag","v"];if(r.length!==s.length||r.some((i,c)=>i!==s[c]))throw Error("envelope fields are invalid");if(t.v!==2)throw Error("envelope version is unsupported");for(let i of["epk","salt","nonce","ciphertext","tag"])if(typeof t[i]!=="string")throw Error(`${i} is invalid`);return t}function de(e,n){try{let t=pe(e),r=fe(n),s=q({key:m(t.epk,"ephemeral public key"),format:"der",type:"spki"});if(s.asymmetricKeyType!=="x25519")throw Error("ephemeral key is not X25519");let i=m(t.salt,"salt",16),c=m(t.nonce,"nonce",12),f=m(t.ciphertext,"ciphertext"),u=m(t.tag,"tag",16),a=M({privateKey:r,publicKey:s}),d=Buffer.from(J("sha256",a,i,Z,32));try{let o=H("aes-256-gcm",d,c);return o.setAAD(te(t)),o.setAuthTag(u),Buffer.concat([o.update(f),o.final()]).toString("utf8")}finally{a.fill(0),d.fill(0)}}catch{throw Error(Q)}}function le(e,n){try{if(!/^[0-9a-f]{64}$/.test(n))throw Error("invalid legacy private key");let t=Buffer.from(e.slice(L.length),"base64");if(t.length<32)throw Error("invalid legacy payload");let r=C("sha256").update(Buffer.from(n,"hex")).digest(),s=C("sha256").update(r).digest();return ce(t.subarray(32).toString("hex"),s,t.subarray(0,16).toString("hex"),t.subarray(16,32).toString("hex"))}catch{throw Error(Q)}}function Ae(e){return e.startsWith(L)&&!e.startsWith(N)}function k(e,n){if(!e.startsWith(L))return e;return e.startsWith(N)?de(e,n):le(e,n)}function Se(e,n,t){return ue(k(e,n),t)}function Ie(e){let n=e.match(/^DOTENV_PRIVATE_KEY_(.+)$/);return n&&n[1]?n[1].toLowerCase():""}function T(e=""){return e?process.env[`DOTENV_PRIVATE_KEY_${e.toUpperCase()}`]:process.env.DOTENV_PRIVATE_KEY}import{spawnSync as ge}from"child_process";import{readFileSync as he}from"fs";import{arch as ye,hostname as me,platform as ne,release as Ee,userInfo as ve}from"os";import{basename as we,dirname as ke,resolve as xe}from"path";function x(e){return typeof e==="string"&&(e.startsWith("encrypted:")||e.startsWith("enc:"))}function _(e,n=Object.keys(e)){let t=n.map((r)=>e[r]).filter((r)=>x(r));return Object.fromEntries(Object.entries(e).filter(([,r])=>r===void 0||!t.some((s)=>r.includes(s))))}function P(e,n={}){let t={},r=[],s=[],i=e.split(`
3
- `),c=_(n.processEnv||process.env);for(let f of i){let u=f.trim();if(!u||u.startsWith("#"))continue;if(u.startsWith("DOTENV_PUBLIC_KEY=")){let p=u.match(/^DOTENV_PUBLIC_KEY=["']?([^"'\n]+)["']?/);if(p&&p[1]!==void 0)t.DOTENV_PUBLIC_KEY=p[1];continue}let a=u.match(/^([^=]+)=(.*)$/);if(!a||a[1]===void 0||a[2]===void 0)continue;let d=a[1].trim(),o=a[2].trim();if(o.startsWith('"')&&o.endsWith('"')||o.startsWith("'")&&o.endsWith("'")){if(o=o.slice(1,-1),o.includes("\\n"))o=o.replace(/\\n/g,`
4
- `)}if(o.startsWith("encrypted:")||o.startsWith("enc:")){if(!n.privateKey){s.push(d);continue}try{let p=o.startsWith("enc:")?`encrypted:${o.slice(4)}`:o;o=k(p,n.privateKey)}catch(p){r.push(`Failed to decrypt ${d}: ${p instanceof Error?p.message:"Unknown error"}`),s.push(d);continue}}o=_e(o,{...c,...t}),o=Ve(o),t[d]=o}return{parsed:t,errors:r,skippedEncrypted:s}}function _e(e,n){return e.replace(/\$\{([^}]+)\}/g,(t,r)=>{let s=r.match(/^([^:\-+]+)(:-|-)(.+)$/);if(s){let[,c,f,u]=s,a=n[c];if(f===":-")return a||u;else return a!==void 0?a:u}let i=r.match(/^([^:\-+]+)(:?\+)(.+)$/);if(i){let[,c,f,u]=i,a=n[c];if(f===":+")return a?u:"";else return a!==void 0?u:""}return n[r]||""})}var Pe=new Set(["date","hostname","whoami","uname","pwd","echo","printf","cat","basename","dirname"]);function be(e){let n=[],t="",r=null,s=!1;for(let i of e.trim()){if(s){t+=i,s=!1;continue}if(i==="\\"&&r!=="'"){s=!0;continue}if(r){if(i===r)r=null;else t+=i;continue}if(i==='"'||i==="'"){r=i;continue}if(/\s/.test(i)){if(t)n.push(t),t="";continue}t+=i}if(s||r)throw Error("Command substitution contains an unterminated escape or quote");if(t)n.push(t);return n}function Ke(e){let[n,...t]=e,r=t.some((s)=>s.startsWith("-"));if(n==="echo"&&!r)return t.join(" ");if(n==="printf"&&t.length===1&&!t[0]?.includes("%"))return t[0];if(n==="hostname"&&t.length===0)return me();if(n==="whoami"&&t.length===0)return ve().username;if(n==="pwd"&&t.length===0)return process.cwd();if(n==="basename"&&t.length===1)return we(t[0]??"");if(n==="dirname"&&t.length===1)return ke(t[0]??"");if(n==="cat"&&t.length>0&&!r)return t.map((s)=>he(xe(process.cwd(),s),"utf8")).join("");if(n==="uname"){let s=t[0]??"-s";if(t.length<=1&&s==="-m")return ye();if(t.length<=1&&s==="-r")return Ee();if(t.length<=1&&s==="-s")return{aix:"AIX",android:"Android",cygwin:"CYGWIN",darwin:"Darwin",freebsd:"FreeBSD",haiku:"Haiku",linux:"Linux",netbsd:"NetBSD",openbsd:"OpenBSD",sunos:"SunOS",win32:"Windows_NT"}[ne()]??ne()}return}function Ve(e){return e.replace(/\$\(([^)]+)\)/g,(n,t)=>{try{let r=be(t),s=r[0];if(!s||!Pe.has(s))return console.warn(`[env] Blocked command substitution for disallowed command: ${s}`),"";let i=Ke(r);if(i!==void 0)return i.trim();let c=ge(s,r.slice(1),{encoding:"utf8",env:process.env});if(c.status===0)return c.stdout.trim();let f=c.stderr.trim();console.warn(`[env] Command substitution failed (exit code ${c.status??"unknown"}): ${s}${f?`: ${f}`:""}`)}catch(r){console.warn(`[env] Command substitution error: ${r instanceof Error?r.message:String(r)}`)}return""})}async function ze(e,n={}){let t={},r=[],s=[];for(let i of e)try{let c=Bun.file(i);if(!c.size)continue;let f=await c.text(),{parsed:u,errors:a,skippedEncrypted:d}=P(f,n);r.push(...a),s.push(...d);for(let[o,p]of Object.entries(u)){if(!n.overload&&t[o]!==void 0)continue;t[o]=p}}catch(c){if(c?.code==="ENOENT")continue;r.push(`Failed to read ${i}: ${c instanceof Error?c.message:String(c)}`)}return{parsed:t,errors:r,skippedEncrypted:s}}function E(e){if(!e||x(e))return;let n=e.trim().toLowerCase();if(!/^[a-z0-9_-]+$/.test(n))return;if(n==="prod")return"production";if(n==="stage")return"staging";if(n==="dev")return"development";return n}var B=null,R=null,A,S;function $e(e,n,t=".env.keys"){let r=b(n,t);if(!D(r))return;try{let{parsed:s}=P(j(r,"utf-8"));if(e){let i=s[`DOTENV_PRIVATE_KEY_${e.toUpperCase()}`];if(i)return i}return s.DOTENV_PRIVATE_KEY}catch{return}}function Oe(e={}){return E(e.env)||S||E("development")||E(process.env.DOTENV_ENV)||E(process.env.APP_ENV)||"development"}function Ne(e={}){let n=Oe(e),t=b(e.cwd||process.cwd());if(B===n&&R===t)return A;let r=T(n);if(!r)r=process.env.DOTENV_PRIVATE_KEY;if(!r)r=$e(n,t);return B=n,R=t,A=r,r}function et(){B=null,R=null,A=void 0,S=void 0}function tt(e,n={}){if(!x(e))return e;let t=Ne(n);if(!t)return;try{let r=e.startsWith("enc:")?`encrypted:${e.slice(4)}`:e;return k(r,t)}catch{return}}function re(e={}){let{path:n=[".env"],overload:t=!1,env:r,privateKey:s,keysFile:i=".env.keys",quiet:c=!1,cwd:f=process.cwd()}=e,u=Array.isArray(n)?n:[n],a=[],d=0,o=new Set(Object.entries(_(process.env)).filter(([,l])=>l!==void 0).map(([l])=>l)),p=s;if(!p&&i){let l=b(f,i);if(D(l))try{let g=j(l,"utf-8"),{parsed:y}=P(g);if(r){let K=`DOTENV_PRIVATE_KEY_${r.toUpperCase()}`;p=y[K]||y.DOTENV_PRIVATE_KEY}else p=y.DOTENV_PRIVATE_KEY}catch(g){a.push(`Failed to load keys file: ${g instanceof Error?g.message:"Unknown error"}`)}}if(!p)p=T(r||"")||T();for(let l of u){let g=b(f,l);if(!D(g)){if(!c)a.push(`File not found: ${g}`);continue}try{let y=j(g,"utf-8"),{parsed:K,errors:se,skippedEncrypted:v}=P(y,{privateKey:p,processEnv:process.env});if(a.push(...se),v.length>0){let w=_(process.env,v);for(let X of Object.keys(process.env))if(!Object.hasOwn(w,X))delete process.env[X];let V=r?`DOTENV_PRIVATE_KEY_${r.toUpperCase()}`:"DOTENV_PRIVATE_KEY",O=p?"decryption failed":`${V} not set`,I=v.slice(0,5).join(", "),ie=v.length>5?`, \u2026 +${v.length-5} more`:"";console.warn(`[env] warning: skipped ${v.length} encrypted value(s) in ${l} (${O}; existing plaintext or defaults apply): ${I}${ie}`)}let U=0;for(let[w,V]of Object.entries(K)){if(w==="DOTENV_PUBLIC_KEY")continue;let O=process.env[w],I=x(O)&&V!==O;if(t||!o.has(w)||I)process.env[w]=V,d++,U++}if(!c&&!process.env.__ENV_LOADED__)console.error(`[env] loaded ${U}/${Object.keys(K).length} variables from ${l}`),process.env.__ENV_LOADED__="1"}catch(y){a.push(`Failed to load ${g}: ${y instanceof Error?y.message:"Unknown error"}`)}}return{loaded:d,errors:a}}function Te(e={}){return{name:"env-plugin",setup(n){re(e)}}}function nt(e={}){let n=E(e.env)||E("development")||E(process.env.DOTENV_ENV)||E(process.env.APP_ENV)||"development",t=e.cwd||process.cwd();if(S!==n)S=n,B=null,R=null,A=void 0;let r=[],s=(f)=>{if(!r.includes(f)&&D(b(t,f)))r.push(f)};s(".env"),s(".env.local");let i=`.env.${n}`,c=`.env.${n}.local`;return s(i),s(c),re({...e,path:r,env:n})}var st=Te;export{Oe as activeEnvName,nt as autoLoadEnv,tt as decryptEnvValue,st as default,Te as envPlugin,re as loadEnv,et as resetPrivateKeyCache,Ne as resolvePrivateKey};
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;
@@ -0,0 +1,21 @@
1
+ import type { EnvKey, StacksEnv } from './types';
2
+ export declare function process(): StacksEnv;
3
+ // eslint-disable-next-line pickier/no-unused-vars
4
+ export declare function writeEnv(key: EnvKey, value: string, options?: { path: string }): void;
5
+ /**
6
+ * Validate environment variables against the envEnum constraints.
7
+ * Returns an array of validation error messages (empty if all valid).
8
+ */
9
+ export declare function validateEnv(envProxy?: StacksEnv): string[];
10
+ /**
11
+ * Assert that every key in `keys` is set on `process.env` (non-empty).
12
+ *
13
+ * Use this at boot to fail fast on misconfigured environments —
14
+ * `requireEnv(['APP_KEY', 'STRIPE_SECRET'])` throws a single error
15
+ * listing every missing key, which is much friendlier than "your
16
+ * Stripe API key is empty" mid-checkout. Returns the env proxy so it
17
+ * can be chained: `const envOk = requireEnv([...])`.
18
+ */
19
+ export declare function requireEnv(keys: ReadonlyArray<string>, envProxy?: StacksEnv): StacksEnv;
20
+ export declare const env: StacksEnv;
21
+ declare type DecryptionRuntime = Pick<typeof import('./plugin'), 'activeEnvName' | 'decryptEnvValue'>;
@@ -0,0 +1,3 @@
1
+ var {require}=import.meta;import p from"node:process";import{projectPath}from"@stacksjs/path";import fs from"node:fs";import{envEnum}from"./types";let decryptionRuntime;function getDecryptionRuntime(){decryptionRuntime||=require("./plugin");return decryptionRuntime}const warnedUndecryptable=new Set;function warnUndecryptable(key){if(warnedUndecryptable.has(key))return;warnedUndecryptable.add(key);const envName=getDecryptionRuntime().activeEnvName();console.warn(`[env] warning: "${key}" is encrypted but no usable private key was found for the "${envName}" environment; falling back to its default. Set DOTENV_PRIVATE_KEY_${envName.toUpperCase()} or ship .env.keys.`)}const handler={get:(target,key)=>{const bag=target;let value=bag[key];if(typeof value==="string"&&(value.startsWith("encrypted:")||value.startsWith("enc:"))){const decrypted=getDecryptionRuntime().decryptEnvValue(value);if(decrypted===void 0){warnUndecryptable(key);delete bag[key];return}bag[key]=decrypted;value=decrypted}const NUMERIC_SUFFIXES=["_PORT","_TIMEOUT","_TTL","_SIZE","_LIMIT","_MAX","_MIN","_INTERVAL","_RETRIES","_CONCURRENCY","_WORKERS","_CONNECTIONS"];if(typeof value==="string"&&/^\d+$/.test(value)&&!value.startsWith("0")&&NUMERIC_SUFFIXES.some((s)=>key.endsWith(s)))return Number(value);if(typeof value==="string"){const lower=value.toLowerCase();if(lower==="true")return!0;if(lower==="false")return!1}return value},set:(target,key,value)=>{return Reflect.set(target,key,value,target)},deleteProperty:(target,key)=>Reflect.deleteProperty(target,key)};export function process(){return typeof Bun<"u"?Bun.env:p.env}export const env=new Proxy(process(),handler);export function writeEnv(key,value,options){const envPath=options?.path||projectPath(".env"),lines=fs.readFileSync(envPath,"utf-8").split(`
2
+ `),index=lines.findIndex((line)=>line.startsWith(`${key}=`)),quotedValue=/[\s"'#$\\]/.test(value)?`"${value.replace(/"/g,"\\\"")}"`:value;if(index!==-1)lines[index]=`${key}=${quotedValue}`;else lines.push(`${key}=${quotedValue}`);fs.writeFileSync(envPath,lines.join(`
3
+ `))}export function validateEnv(envProxy=env){const errors=[],bag=envProxy;for(const[key,allowedValues]of Object.entries(envEnum)){const value=bag[key];if(value!==void 0&&value!==""&&!allowedValues.includes(String(value)))errors.push(`${key}="${value}" is not valid. Allowed values: ${allowedValues.join(", ")}`)}return errors}export function requireEnv(keys,envProxy=env){const missing=[],bag=envProxy;for(const key of keys){const value=bag[key];if(value===void 0||value===""||value===null)missing.push(key)}if(missing.length>0)throw Error(`[env] Missing required environment variable(s): ${missing.join(", ")}. Set them in .env or your process environment before booting.`);return envProxy}
@@ -0,0 +1 @@
1
+ export function tenantEnvPrefix(slug){return`${slug.replace(/[^a-z0-9]/gi,"").toUpperCase()}_`}export function partitionTenantEnv(values,options={}){const selfPrefix=options.self?tenantEnvPrefix(options.self):void 0,tenants=(options.tenants??[]).filter((slug)=>typeof slug==="string"&&slug.trim().length>0).map((slug)=>({slug,prefix:tenantEnvPrefix(slug)})).filter(({prefix})=>prefix!==selfPrefix).sort((a,b)=>b.prefix.length-a.prefix.length),own={},foreign={};for(const[key,value]of Object.entries(values)){const match=tenants.find(({prefix})=>key.startsWith(prefix));if(match){(foreign[match.slug]??={})[key]=value;continue}own[key]=value}return{own,foreign}}export function stripForeignTenantEnv(values,options={}){return partitionTenantEnv(values,options).own}export function foreignTenantKeys(partition){return Object.entries(partition.foreign).map(([tenant,values])=>({tenant,keys:Object.keys(values).sort()})).sort((a,b)=>a.tenant.localeCompare(b.tenant))}
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export const envEnum={APP_ENV:["local","dev","development","staging","prod","production"],DB_CONNECTION:["mysql","sqlite","postgres","singlestore","vitess","dynamodb"],DB_MIGRATE_FRESH:["allow","confirm","disabled"],MAIL_MAILER:["smtp","mailgun","ses","log","sendgrid","mailtrap","capture"],SEARCH_ENGINE_DRIVER:["opensearch","meilisearch","algolia","typesense"],FRONTEND_APP_ENV:["development","staging","production"]};export function defineEnv(schema){return schema}
package/dist/utils.js CHANGED
@@ -1,2 +1 @@
1
- // @bun
2
- import e from"process";import{platform as l}from"os";var i=typeof Bun<"u",d=typeof e<"u"&&e.versions?.node!==void 0,c=i?"bun":d?"node":"unknown",f={name:c,version:i?Bun.version:d?e.version:void 0},r=l(),E=r==="win32",B=r==="darwin",N=r==="linux",s=Boolean(e.stdout?.isTTY),I=typeof globalThis.window<"u";function o(){return e.env.APP_ENV||e.env.NODE_ENV||"local"}function x(){return o()==="local"}function R(){let n=o();return n==="development"||n==="dev"||n==="local"}function T(){return o()==="staging"}function C(){let n=o();return n==="production"||n==="prod"}function g(){let n=o();return n==="test"||n==="testing"}var u=Boolean(e.env.CI||e.env.CONTINUOUS_INTEGRATION||e.env.BUILD_NUMBER||e.env.RUN_ID),A=Boolean(e.env.DEBUG||e.env.VERBOSE||e.argv.includes("--debug")||e.argv.includes("--verbose")),v=u||!s,_=Boolean(!v&&(s||e.env.COLORTERM||e.env.FORCE_COLOR||e.env.TERM&&e.env.TERM!=="dumb")),t={github:{name:"GitHub Actions",detected:Boolean(e.env.GITHUB_ACTIONS)},gitlab:{name:"GitLab CI",detected:Boolean(e.env.GITLAB_CI)},circle:{name:"CircleCI",detected:Boolean(e.env.CIRCLECI)},travis:{name:"Travis CI",detected:Boolean(e.env.TRAVIS)},jenkins:{name:"Jenkins",detected:Boolean(e.env.JENKINS_URL)},vercel:{name:"Vercel",detected:Boolean(e.env.VERCEL)},netlify:{name:"Netlify",detected:Boolean(e.env.NETLIFY)},heroku:{name:"Heroku",detected:Boolean(e.env.DYNO)},aws:{name:"AWS",detected:Boolean(e.env.AWS_REGION||e.env.AWS_LAMBDA_FUNCTION_NAME)},azure:{name:"Azure",detected:Boolean(e.env.AZURE_HTTP_USER_AGENT)},cloudflare:{name:"Cloudflare",detected:Boolean(e.env.CF_PAGES)},railway:{name:"Railway",detected:Boolean(e.env.RAILWAY_ENVIRONMENT)},render:{name:"Render",detected:Boolean(e.env.RENDER)}},p=Object.keys(t).find((n)=>{let a=t[n];return a!==void 0&&a.detected})||"unknown",O=t[p]||{name:"Unknown",detected:!1};export{o as appEnv,s as hasTTY,I as hasWindow,i as isBun,u as isCI,_ as isColorSupported,A as isDebug,R as isDevelopment,N as isLinux,x as isLocal,B as isMacOS,v as isMinimal,d as isNode,C as isProduction,T as isStaging,g as isTesting,E as isWindows,r as platform,p as provider,O as providerInfo,c as runtime,f as runtimeInfo};
1
+ import process from"node:process";import{platform as osPlatform}from"node:os";export const isBun=typeof Bun<"u",isNode=typeof process<"u"&&process.versions?.node!==void 0,runtime=isBun?"bun":isNode?"node":"unknown",runtimeInfo={name:runtime,version:isBun?Bun.version:isNode?process.version:void 0},platform=osPlatform(),isWindows=platform==="win32",isMacOS=platform==="darwin",isLinux=platform==="linux",hasTTY=Boolean(process.stdout?.isTTY),hasWindow=typeof globalThis.window<"u";export function appEnv(){return process.env.APP_ENV||process.env.NODE_ENV||"local"}export function isLocal(){return appEnv()==="local"}export function isDevelopment(){const name=appEnv();return name==="development"||name==="dev"||name==="local"}export function isStaging(){return appEnv()==="staging"}export function isProduction(){const name=appEnv();return name==="production"||name==="prod"}export function isTesting(){const name=appEnv();return name==="test"||name==="testing"}export const isCI=Boolean(process.env.CI||process.env.CONTINUOUS_INTEGRATION||process.env.BUILD_NUMBER||process.env.RUN_ID),isDebug=Boolean(process.env.DEBUG||process.env.VERBOSE||process.argv.includes("--debug")||process.argv.includes("--verbose")),isMinimal=isCI||!hasTTY,isColorSupported=Boolean(!isMinimal&&(hasTTY||process.env.COLORTERM||process.env.FORCE_COLOR||process.env.TERM&&process.env.TERM!=="dumb"));const providers={github:{name:"GitHub Actions",detected:Boolean(process.env.GITHUB_ACTIONS)},gitlab:{name:"GitLab CI",detected:Boolean(process.env.GITLAB_CI)},circle:{name:"CircleCI",detected:Boolean(process.env.CIRCLECI)},travis:{name:"Travis CI",detected:Boolean(process.env.TRAVIS)},jenkins:{name:"Jenkins",detected:Boolean(process.env.JENKINS_URL)},vercel:{name:"Vercel",detected:Boolean(process.env.VERCEL)},netlify:{name:"Netlify",detected:Boolean(process.env.NETLIFY)},heroku:{name:"Heroku",detected:Boolean(process.env.DYNO)},aws:{name:"AWS",detected:Boolean(process.env.AWS_REGION||process.env.AWS_LAMBDA_FUNCTION_NAME)},azure:{name:"Azure",detected:Boolean(process.env.AZURE_HTTP_USER_AGENT)},cloudflare:{name:"Cloudflare",detected:Boolean(process.env.CF_PAGES)},railway:{name:"Railway",detected:Boolean(process.env.RAILWAY_ENVIRONMENT)},render:{name:"Render",detected:Boolean(process.env.RENDER)}};export const provider=Object.keys(providers).find((key)=>{const entry=providers[key];return entry!==void 0&&entry.detected})||"unknown",providerInfo=providers[provider]||{name:"Unknown",detected:!1};
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/env",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.74.51",
5
+ "version": "0.74.53",
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.74.51"
59
+ "@stacksjs/path": "0.74.53"
60
60
  },
61
61
  "devDependencies": {
62
62
  "better-dx": "^0.2.24",
63
- "@stacksjs/validation": "0.74.51"
63
+ "@stacksjs/validation": "0.74.53"
64
64
  }
65
65
  }