@stacksjs/env 0.72.33 → 0.72.36
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.d.ts +27 -0
- package/dist/cli.js +21 -20
- package/dist/index.js +22 -21
- package/package.json +3 -3
package/dist/cli.d.ts
CHANGED
|
@@ -28,6 +28,33 @@ export declare function resolveEnvFile(file?: string, environment?: string): str
|
|
|
28
28
|
* survived: the default path agreed with itself.
|
|
29
29
|
*/
|
|
30
30
|
export declare function envKeyNames(file?: string): { publicKeyName: string, privateKeyName: string };
|
|
31
|
+
/**
|
|
32
|
+
* The public key an env file's existing ciphertext was encrypted under.
|
|
33
|
+
*
|
|
34
|
+
* Encrypting needs only the PUBLIC half, and an encrypted env file already
|
|
35
|
+
* carries it on its `DOTENV_PUBLIC_KEY*` line. Resolving the keypair from
|
|
36
|
+
* `.env.keys` instead is what let one file end up holding two generations at
|
|
37
|
+
* once (stacksjs/stacks#2348):
|
|
38
|
+
*
|
|
39
|
+
* `.env.keys` is gitignored, so on a CI runner it does not exist. The private
|
|
40
|
+
* key arrives as `DOTENV_PRIVATE_KEY_<ENV>` in the environment, which this
|
|
41
|
+
* path never consulted. A file with any plaintext line therefore got a
|
|
42
|
+
* BRAND NEW keypair, its four plaintext values encrypted under generation 2,
|
|
43
|
+
* its public-key line replaced with generation 2's, and a fresh `.env.keys`
|
|
44
|
+
* written. The thirty-three values already encrypted under generation 1 were
|
|
45
|
+
* left behind, decryptable by a key the file no longer names.
|
|
46
|
+
*
|
|
47
|
+
* Reusing the file's own public key makes that structurally impossible: every
|
|
48
|
+
* value in the file stays under one key, and the operator's existing private
|
|
49
|
+
* key keeps working.
|
|
50
|
+
*
|
|
51
|
+
* Only reused when the file actually holds ciphertext. A scaffolded file can
|
|
52
|
+
* ship a demo `DOTENV_PUBLIC_KEY` whose private half was never generated, and
|
|
53
|
+
* encrypting real secrets under that would make them permanently unreadable.
|
|
54
|
+
* With no ciphertext there is nothing to stay consistent with, so a fresh
|
|
55
|
+
* keypair is both safe and correct.
|
|
56
|
+
*/
|
|
57
|
+
export declare function reusableEnvPublicKey(envContent: string, publicKeyName: string): string | undefined;
|
|
31
58
|
/**
|
|
32
59
|
* Encrypt .env file
|
|
33
60
|
*/
|
package/dist/cli.js
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{createCipheriv as m,createDecipheriv as n,createHash as
|
|
3
|
-
`);for(let
|
|
4
|
-
`)}if(A.startsWith("encrypted:")||A.startsWith("enc:")){if(!E.privateKey){B.push(T);continue}try{let X=A.startsWith("enc:")?`encrypted:${A.slice(4)}`:A;A=C(X,E.privateKey)}catch(X){$.push(`Failed to decrypt ${T}: ${X instanceof Error?X.message:"Unknown error"}`)}}A=
|
|
5
|
-
`)){let I
|
|
6
|
-
`)}function
|
|
2
|
+
import{createCipheriv as m,createDecipheriv as n,createHash as b,createPrivateKey as Af,createPublicKey as v,diffieHellman as o,generateKeyPairSync as s,hkdfSync as i,randomBytes as N}from"crypto";var K="x25519-public:",k="x25519-private:",F="encrypted:v2:",d="encrypted:",y=Buffer.from("stacks-env:v2","utf8"),p="Environment decryption failed: authentication or format error";function l(g){if(g.length===32)return g;return b("sha256").update(g).digest()}function Mf(g,E){let f=N(16),$=m("aes-256-gcm",l(E),f);return{ciphertext:Buffer.concat([$.update(g,"utf8"),$.final()]).toString("hex"),iv:f.toString("hex"),authTag:$.getAuthTag().toString("hex")}}function _f(g,E,f,$){try{let B=n("aes-256-gcm",l(E),Buffer.from(f,"hex"));return B.setAuthTag(Buffer.from($,"hex")),Buffer.concat([B.update(Buffer.from(g,"hex")),B.final()]).toString("utf8")}catch{throw Error("Decryption failed: authentication or format error")}}function x(g){return g.toString("base64url")}function h(g,E,f){if(typeof g!=="string"||g.length>0&&!/^[A-Za-z0-9_-]+$/.test(g))throw Error(`${E} is not canonical base64url`);let $=Buffer.from(g,"base64url");if(x($)!==g)throw Error(`${E} is not canonical base64url`);if(f!==void 0&&$.length!==f)throw Error(`${E} has an invalid length`);return $}function Tf(g){if(!g.startsWith(K))throw Error("Legacy or invalid public key. Run buddy env:rotate before creating new ciphertext.");let E=v({key:h(g.slice(K.length),"public key"),format:"der",type:"spki"});if(E.asymmetricKeyType!=="x25519")throw Error("Public key is not X25519");return E}function If(g){if(!g.startsWith(k))throw Error("Private key is not a version 2 X25519 key");let E=Af({key:h(g.slice(k.length),"private key"),format:"der",type:"pkcs8"});if(E.asymmetricKeyType!=="x25519")throw Error("Private key is not X25519");return E}function a(g){return Buffer.from(`stacks-env:v2;${g.epk};${g.salt};${g.nonce}`,"utf8")}function M(){let{publicKey:g,privateKey:E}=s("x25519"),f=g.export({format:"der",type:"spki"}),$=E.export({format:"der",type:"pkcs8"});return{publicKey:`${K}${x(f)}`,privateKey:`${k}${x($)}`}}function S(g,E){let f=Tf(E),$=s("x25519"),B=x($.publicKey.export({format:"der",type:"spki"})),w=N(16),I=N(12),D=o({privateKey:$.privateKey,publicKey:f}),_=Buffer.from(i("sha256",D,w,y,32)),T={epk:B,salt:x(w),nonce:x(I)};try{let A=m("aes-256-gcm",_,I);A.setAAD(a(T));let X=Buffer.concat([A.update(g,"utf8"),A.final()]),O={v:2,...T,ciphertext:x(X),tag:x(A.getAuthTag())};return`${F}${x(Buffer.from(JSON.stringify(O),"utf8"))}`}finally{D.fill(0),_.fill(0)}}function Df(g){let E=h(g.slice(F.length),"envelope").toString("utf8"),f=JSON.parse(E);if(!f||typeof f!=="object"||Array.isArray(f))throw Error("envelope is not an object");let $=Object.keys(f).sort(),B=["ciphertext","epk","nonce","salt","tag","v"];if($.length!==B.length||$.some((w,I)=>w!==B[I]))throw Error("envelope fields are invalid");if(f.v!==2)throw Error("envelope version is unsupported");for(let w of["epk","salt","nonce","ciphertext","tag"])if(typeof f[w]!=="string")throw Error(`${w} is invalid`);return f}function Xf(g,E){try{let f=Df(g),$=If(E),B=v({key:h(f.epk,"ephemeral public key"),format:"der",type:"spki"});if(B.asymmetricKeyType!=="x25519")throw Error("ephemeral key is not X25519");let w=h(f.salt,"salt",16),I=h(f.nonce,"nonce",12),D=h(f.ciphertext,"ciphertext"),_=h(f.tag,"tag",16),T=o({privateKey:$,publicKey:B}),A=Buffer.from(i("sha256",T,w,y,32));try{let X=n("aes-256-gcm",A,I);return X.setAAD(a(f)),X.setAuthTag(_),Buffer.concat([X.update(D),X.final()]).toString("utf8")}finally{T.fill(0),A.fill(0)}}catch{throw Error(p)}}function Of(g,E){try{if(!/^[0-9a-f]{64}$/.test(E))throw Error("invalid legacy private key");let f=Buffer.from(g.slice(d.length),"base64");if(f.length<32)throw Error("invalid legacy payload");let $=b("sha256").update(Buffer.from(E,"hex")).digest(),B=b("sha256").update($).digest();return _f(f.subarray(32).toString("hex"),B,f.subarray(0,16).toString("hex"),f.subarray(16,32).toString("hex"))}catch{throw Error(p)}}function Cf(g){return g.startsWith(d)&&!g.startsWith(F)}function C(g,E){if(!g.startsWith(d))return g;return g.startsWith(F)?Xf(g,E):Of(g,E)}function Ff(g,E,f){return S(C(g,E),f)}function Sf(g){let E=g.match(/^DOTENV_PRIVATE_KEY_(.+)$/);return E&&E[1]?E[1].toLowerCase():""}function Pf(g=""){return g?process.env[`DOTENV_PRIVATE_KEY_${g.toUpperCase()}`]:process.env.DOTENV_PRIVATE_KEY}import{spawnSync as Rf}from"child_process";import{readFileSync as Yf}from"fs";import{arch as Hf,hostname as Wf,platform as e,release as jf,userInfo as Gf}from"os";import{basename as Jf,dirname as Uf,resolve as zf}from"path";function L(g,E={}){let f={},$=[],B=[],w=g.split(`
|
|
3
|
+
`);for(let I of w){let D=I.trim();if(!D||D.startsWith("#"))continue;if(D.startsWith("DOTENV_PUBLIC_KEY=")){let X=D.match(/^DOTENV_PUBLIC_KEY=["']?([^"'\n]+)["']?/);if(X&&X[1]!==void 0)f.DOTENV_PUBLIC_KEY=X[1];continue}let _=D.match(/^([^=]+)=(.*)$/);if(!_||_[1]===void 0||_[2]===void 0)continue;let T=_[1].trim(),A=_[2].trim();if(A.startsWith('"')&&A.endsWith('"')||A.startsWith("'")&&A.endsWith("'")){if(A=A.slice(1,-1),A.includes("\\n"))A=A.replace(/\\n/g,`
|
|
4
|
+
`)}if(A.startsWith("encrypted:")||A.startsWith("enc:")){if(!E.privateKey){B.push(T);continue}try{let X=A.startsWith("enc:")?`encrypted:${A.slice(4)}`:A;A=C(X,E.privateKey)}catch(X){$.push(`Failed to decrypt ${T}: ${X instanceof Error?X.message:"Unknown error"}`)}}A=Zf(A,{...E.processEnv||process.env,...f}),A=hf(A),f[T]=A}return{parsed:f,errors:$,skippedEncrypted:B}}function Zf(g,E){return g.replace(/\$\{([^}]+)\}/g,(f,$)=>{let B=$.match(/^([^:\-+]+)(:-|-)(.+)$/);if(B){let[,I,D,_]=B,T=E[I];if(D===":-")return T||_;else return T!==void 0?T:_}let w=$.match(/^([^:\-+]+)(:?\+)(.+)$/);if(w){let[,I,D,_]=w,T=E[I];if(D===":+")return T?_:"";else return T!==void 0?_:""}return E[$]||""})}var Qf=new Set(["date","hostname","whoami","uname","pwd","echo","printf","cat","basename","dirname"]);function xf(g){let E=[],f="",$=null,B=!1;for(let w of g.trim()){if(B){f+=w,B=!1;continue}if(w==="\\"&&$!=="'"){B=!0;continue}if($){if(w===$)$=null;else f+=w;continue}if(w==='"'||w==="'"){$=w;continue}if(/\s/.test(w)){if(f)E.push(f),f="";continue}f+=w}if(B||$)throw Error("Command substitution contains an unterminated escape or quote");if(f)E.push(f);return E}function Vf(g){let[E,...f]=g,$=f.some((B)=>B.startsWith("-"));if(E==="echo"&&!$)return f.join(" ");if(E==="printf"&&f.length===1&&!f[0]?.includes("%"))return f[0];if(E==="hostname"&&f.length===0)return Wf();if(E==="whoami"&&f.length===0)return Gf().username;if(E==="pwd"&&f.length===0)return process.cwd();if(E==="basename"&&f.length===1)return Jf(f[0]??"");if(E==="dirname"&&f.length===1)return Uf(f[0]??"");if(E==="cat"&&f.length>0&&!$)return f.map((B)=>Yf(zf(process.cwd(),B),"utf8")).join("");if(E==="uname"){let B=f[0]??"-s";if(f.length<=1&&B==="-m")return Hf();if(f.length<=1&&B==="-r")return jf();if(f.length<=1&&B==="-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"}[e()]??e()}return}function hf(g){return g.replace(/\$\(([^)]+)\)/g,(E,f)=>{try{let $=xf(f),B=$[0];if(!B||!Qf.has(B))return console.warn(`[env] Blocked command substitution for disallowed command: ${B}`),"";let w=Vf($);if(w!==void 0)return w.trim();let I=Rf(B,$.slice(1),{encoding:"utf8",env:process.env});if(I.status===0)return I.stdout.trim();let D=I.stderr.trim();console.warn(`[env] Command substitution failed (exit code ${I.status??"unknown"}): ${B}${D?`: ${D}`:""}`)}catch($){console.warn(`[env] Command substitution error: ${$ instanceof Error?$.message:String($)}`)}return""})}async function df(g,E={}){let f={},$=[],B=[];for(let w of g)try{let I=Bun.file(w);if(!I.size)continue;let D=await I.text(),{parsed:_,errors:T,skippedEncrypted:A}=L(D,E);$.push(...T),B.push(...A);for(let[X,O]of Object.entries(_)){if(!E.overload&&f[X]!==void 0)continue;f[X]=O}}catch(I){if(I?.code==="ENOENT")continue;$.push(`Failed to read ${w}: ${I instanceof Error?I.message:String(I)}`)}return{parsed:f,errors:$,skippedEncrypted:B}}import{existsSync as Z,readFileSync as z,renameSync as t,rmSync as ff,writeFileSync as Q}from"fs";import{basename as u,dirname as gf,resolve as U}from"path";function of(g,E){if(g)return g;let f=String(E??"").trim();if(!f||f==="development"||f==="dev"||f==="local")return"";if(!/^[\w.-]+$/.test(f))return"";return`.env.${f}`}function Ef(g,E,f,$){let B=["#/-------------------[DOTENV_PUBLIC_KEY]--------------------/","#/ versioned X25519 encryption for .env files /","#/ [how it works](https://stacksjs.com/encryption) /","#/----------------------------------------------------------/",`${f}="${E}"`,""],w=!1;for(let I of g.split(`
|
|
5
|
+
`)){let D=I.trim();if(D.startsWith("#/"))continue;if(D.startsWith("DOTENV_PUBLIC_KEY"))continue;if(!D&&!w)continue;if(!D||D.startsWith("#")){w=!0,B.push(I);continue}w=!0;let _=D.match(/^([^=]+)=(.*)$/);if(!_||_[1]===void 0||_[2]===void 0){B.push(I);continue}let T=_[1].trim(),A=_[2].trim();if(A.startsWith('"')&&A.endsWith('"')||A.startsWith("'")&&A.endsWith("'"))A=A.slice(1,-1);if((!$.key||T.includes($.key))&&(!$.excludeKey||!T.includes($.excludeKey))&&!A.startsWith("encrypted:"))A=S(A,E);B.push(`${T}="${A}"`)}return B.join(`
|
|
6
|
+
`)}function P(g){let f=((g||".env").split("/").pop()||"").replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase();return{publicKeyName:f?`DOTENV_PUBLIC_KEY_${f}`:"DOTENV_PUBLIC_KEY",privateKeyName:f?`DOTENV_PRIVATE_KEY_${f}`:"DOTENV_PRIVATE_KEY"}}function Bf(g,E){let f,$=!1;for(let B of g.split(`
|
|
7
|
+
`)){let w=B.trim();if(!w||w.startsWith("#"))continue;let I=w.match(/^([^=]+)=(.*)$/);if(!I?.[1]||I[2]===void 0)continue;let D=I[1].trim(),_=I[2].trim().replace(/^["']|["']$/g,"");if(D===E)f=_;else if(_.startsWith("encrypted:")||_.startsWith("enc:"))$=!0}return f&&$?f:void 0}function sf(g={}){let E=g.cwd||process.cwd(),f=U(E,g.file||".env"),$=U(E,g.keysFile||".env.keys");if(!Z(f))return{success:!1,error:`File not found: ${f}`};try{let B,w,I=Bf(z(f,"utf-8"),P(g.file).publicKeyName);if(I)B=I;else if(Z($)){let T=z($,"utf-8"),{parsed:A}=L(T),{publicKeyName:X,privateKeyName:O}=P(g.file);if(B=A[X]||"",w=A[O]||"",!B||!w){let H=M();B=H.publicKey,w=H.privateKey;let Y=`
|
|
8
|
+
${X}="${B}"
|
|
9
|
+
${O}="${w}"
|
|
10
|
+
`;Q($,T+Y,"utf-8")}}else{let T=M();B=T.publicKey,w=T.privateKey;let{publicKeyName:A,privateKeyName:X}=P(g.file),O=`# .env.keys - Keep this file secure and never commit to source control
|
|
7
11
|
${A}="${B}"
|
|
8
12
|
${X}="${w}"
|
|
9
|
-
`;
|
|
10
|
-
${
|
|
11
|
-
${
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
${
|
|
18
|
-
${
|
|
19
|
-
|
|
20
|
-
`);return z(B,Q,"utf-8"),{success:!0,output:`set ${f}${g.plain?"":" with encryption"} (${g.file||".env"})`}}catch(D){return{success:!1,error:`Failed to set: ${D instanceof Error?D.message:"Unknown error"}`}}}function yf(f,E={}){let g=E.cwd||process.cwd(),$=G(g,E.file||".env");if(!U($))return{success:!1,error:`File not found: ${$}`};try{let B,w=G(g,E.keysFile||".env.keys"),_=((E.file||".env").split("/").pop()||"").replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase(),T=_?`DOTENV_PRIVATE_KEY_${_}`:"DOTENV_PRIVATE_KEY";if(U(w)){let h=J(w,"utf-8"),{parsed:x}=L(h);B=x[T]}if(!B)B=process.env[T];let A=J($,"utf-8"),{parsed:X}=L(A,{privateKey:B});if(f&&!E.all){let h=X[f];if(h===void 0)return{success:!1,error:`Key not found: ${f}`};return{success:!0,output:h}}let R=E.all?{...process.env,...X}:X,W;switch(E.format){case"shell":W=Object.entries(R).map(([h,x])=>`${h}=${x}`).join(" ");break;case"eval":W=Object.entries(R).map(([h,x])=>`${h}="${x}"`).join(`
|
|
21
|
-
`);break;case"json":default:W=E.prettyPrint?JSON.stringify(R,null,2):JSON.stringify(R);break}return{success:!0,output:W}}catch(B){return{success:!1,error:`Failed to get: ${B instanceof Error?B.message:"Unknown error"}`}}}function pf(f,E={}){let g=E.cwd||process.cwd(),$=G(g,E.keysFile||".env.keys");if(!U($))return{success:!1,error:`Keys file not found: ${$}`};try{let B=J($,"utf-8"),{parsed:w}=L(B),_=((E.file||".env").split("/").pop()||"").replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase(),T=_?`DOTENV_PUBLIC_KEY_${_}`:"DOTENV_PUBLIC_KEY",A=_?`DOTENV_PRIVATE_KEY_${_}`:"DOTENV_PRIVATE_KEY";if(f){let W=w[f];if(!W)return{success:!1,error:`Key not found: ${f}`};return{success:!0,output:W}}let X={[T]:w[T],[A]:w[A]};return{success:!0,output:E.format==="shell"?`${T}=${X[T]} ${A}=${X[A]}`:JSON.stringify(X)}}catch(B){return{success:!1,error:`Failed to get keypair: ${B instanceof Error?B.message:"Unknown error"}`}}}function lf(f={}){let E=Vf({file:f.file,keysFile:f.keysFile,stdout:!0,cwd:f.cwd});if(!E.success)return E;let g=f.cwd||process.cwd(),$=G(g,f.file||".env"),B=G(g,f.keysFile||".env.keys");if(E.output===void 0)return{success:!1,error:"Rotation produced no decrypted content"};let w=M(),D=J($),I=J(B),_=I.toString("utf8").split(`
|
|
22
|
-
`),T=f.file||".env",A=d(T).replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase(),X=A?`DOTENV_PUBLIC_KEY_${A}`:"DOTENV_PUBLIC_KEY",R=A?`DOTENV_PRIVATE_KEY_${A}`:"DOTENV_PRIVATE_KEY",W=!1,h=!1;for(let O=0;O<_.length;O++){let q=_[O];if(q===void 0)continue;if(q.startsWith(`${X}=`))_[O]=`${X}="${w.publicKey}"`,W=!0;else if(q.startsWith(`${R}=`))_[O]=`${R}="${w.privateKey}"`,h=!0}if(!W)_.push(`${X}="${w.publicKey}"`);if(!h)_.push(`${R}="${w.privateKey}"`);let x=`${_.join(`
|
|
13
|
+
`;Q($,O,"utf-8")}let D=z(f,"utf-8"),_=Ef(D,B,P(g.file).publicKeyName,g);if(g.stdout)return{success:!0,output:_};return Q(f,_,"utf-8"),{success:!0,output:`\u2714 encrypted (${g.file||".env"})
|
|
14
|
+
\u2714 key added to ${g.keysFile||".env.keys"}`}}catch(B){return{success:!1,error:`Failed to encrypt: ${B instanceof Error?B.message:"Unknown error"}`}}}function Lf(g={}){let E=g.cwd||process.cwd(),f=U(E,g.file||".env"),$=U(E,g.keysFile||".env.keys");if(!Z(f))return{success:!1,error:`File not found: ${f}`};if(!Z($))return{success:!1,error:`Keys file not found: ${$}`};try{let B=z($,"utf-8"),{parsed:w}=L(B),_=((g.file||".env").split("/").pop()||"").replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase(),T=_?`DOTENV_PRIVATE_KEY_${_}`:"DOTENV_PRIVATE_KEY",A=w[T];if(!A)return{success:!1,error:`Private key not found: ${T}`};let X=[A];if(T!=="DOTENV_PRIVATE_KEY"&&w.DOTENV_PRIVATE_KEY&&w.DOTENV_PRIVATE_KEY!==A)X.push(w.DOTENV_PRIVATE_KEY);let H=z(f,"utf-8").split(`
|
|
15
|
+
`),Y=[];for(let V of H){let W=V.trim();if(!W||W.startsWith("#")){Y.push(V);continue}if(W.startsWith("DOTENV_PUBLIC_KEY"))continue;let j=W.match(/^([^=]+)=(.*)$/);if(!j||j[1]===void 0||j[2]===void 0){Y.push(V);continue}let G=j[1].trim(),R=j[2].trim();if(R.startsWith('"')&&R.endsWith('"')||R.startsWith("'")&&R.endsWith("'"))R=R.slice(1,-1);let q=R.startsWith("encrypted:");if(g.key&&!G.includes(g.key))q=!1;if(q){let c,r;for(let $f of X)try{c=C(R,$f);break}catch(wf){r=wf}if(c===void 0)throw r instanceof Error?r:Error(`Unable to decrypt ${G}`);R=c}Y.push(`${G}="${R}"`)}let J=Y.join(`
|
|
16
|
+
`);if(g.stdout)return{success:!0,output:J};return Q(f,J,"utf-8"),{success:!0,output:`\u2714 decrypted (${g.file||".env"})`}}catch(B){return{success:!1,error:`Failed to decrypt: ${B instanceof Error?B.message:"Unknown error"}`}}}function yf(g,E,f={}){let $=f.cwd||process.cwd(),B=U($,f.file||".env"),w=U($,f.keysFile||".env.keys");try{let I="";if(Z(B))I=z(B,"utf-8");let D=I.split(`
|
|
17
|
+
`),_=!1,T,O=((f.file||".env").split("/").pop()||"").replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase(),H=O?`DOTENV_PUBLIC_KEY_${O}`:"DOTENV_PUBLIC_KEY",Y=O?`DOTENV_PRIVATE_KEY_${O}`:"DOTENV_PRIVATE_KEY";for(let W of D){let j=W.trim();if(j.startsWith(`${H}=`)){let G=j.match(/^[^=]+=["']?([^"'\n]+)["']?/);if(G)T=G[1];break}}if(T){let W=!1;if(Z(w)){let j=z(w,"utf-8"),{parsed:G}=L(j);W=Boolean(G[Y])}if(!W&&!process.env[Y]&&!Bf(I,H))T=void 0}if(!f.plain&&!T){let W=M();T=W.publicKey;let j="";if(Z(w))j=z(w,"utf-8");j+=`
|
|
18
|
+
${H}="${W.publicKey}"
|
|
19
|
+
${Y}="${W.privateKey}"
|
|
20
|
+
`,Q(w,j,"utf-8");for(let G=D.length-1;G>=0;G--){let R=D[G];if(R!==void 0&&R.trim().startsWith(`${H}=`))D.splice(G,1)}D.unshift(`${H}="${T}"`)}let J=E;if(!f.plain&&T)J=S(E,T);for(let W=0;W<D.length;W++){let j=D[W];if(j===void 0)continue;if(j.trim().startsWith(`${g}=`)){D[W]=`${g}="${J}"`,_=!0;break}}if(!_)D.push(`${g}="${J}"`);let V=D.join(`
|
|
21
|
+
`);return Q(B,V,"utf-8"),{success:!0,output:`set ${g}${f.plain?"":" with encryption"} (${f.file||".env"})`}}catch(I){return{success:!1,error:`Failed to set: ${I instanceof Error?I.message:"Unknown error"}`}}}function pf(g,E={}){let f=E.cwd||process.cwd(),$=U(f,E.file||".env");if(!Z($))return{success:!1,error:`File not found: ${$}`};try{let B,w=U(f,E.keysFile||".env.keys"),_=((E.file||".env").split("/").pop()||"").replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase(),T=_?`DOTENV_PRIVATE_KEY_${_}`:"DOTENV_PRIVATE_KEY";if(Z(w)){let Y=z(w,"utf-8"),{parsed:J}=L(Y);B=J[T]}if(!B)B=process.env[T];let A=z($,"utf-8"),{parsed:X}=L(A,{privateKey:B});if(g&&!E.all){let Y=X[g];if(Y===void 0)return{success:!1,error:`Key not found: ${g}`};return{success:!0,output:Y}}let O=E.all?{...process.env,...X}:X,H;switch(E.format){case"shell":H=Object.entries(O).map(([Y,J])=>`${Y}=${J}`).join(" ");break;case"eval":H=Object.entries(O).map(([Y,J])=>`${Y}="${J}"`).join(`
|
|
22
|
+
`);break;case"json":default:H=E.prettyPrint?JSON.stringify(O,null,2):JSON.stringify(O);break}return{success:!0,output:H}}catch(B){return{success:!1,error:`Failed to get: ${B instanceof Error?B.message:"Unknown error"}`}}}function lf(g,E={}){let f=E.cwd||process.cwd(),$=U(f,E.keysFile||".env.keys");if(!Z($))return{success:!1,error:`Keys file not found: ${$}`};try{let B=z($,"utf-8"),{parsed:w}=L(B),_=((E.file||".env").split("/").pop()||"").replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase(),T=_?`DOTENV_PUBLIC_KEY_${_}`:"DOTENV_PUBLIC_KEY",A=_?`DOTENV_PRIVATE_KEY_${_}`:"DOTENV_PRIVATE_KEY";if(g){let H=w[g];if(!H)return{success:!1,error:`Key not found: ${g}`};return{success:!0,output:H}}let X={[T]:w[T],[A]:w[A]};return{success:!0,output:E.format==="shell"?`${T}=${X[T]} ${A}=${X[A]}`:JSON.stringify(X)}}catch(B){return{success:!1,error:`Failed to get keypair: ${B instanceof Error?B.message:"Unknown error"}`}}}function af(g={}){let E=Lf({file:g.file,keysFile:g.keysFile,stdout:!0,cwd:g.cwd});if(!E.success)return E;let f=g.cwd||process.cwd(),$=U(f,g.file||".env"),B=U(f,g.keysFile||".env.keys");if(E.output===void 0)return{success:!1,error:"Rotation produced no decrypted content"};let w=M(),I=z($),D=z(B),_=D.toString("utf8").split(`
|
|
23
|
+
`),T=g.file||".env",A=u(T).replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase(),X=A?`DOTENV_PUBLIC_KEY_${A}`:"DOTENV_PUBLIC_KEY",O=A?`DOTENV_PRIVATE_KEY_${A}`:"DOTENV_PRIVATE_KEY",H=!1,Y=!1;for(let R=0;R<_.length;R++){let q=_[R];if(q===void 0)continue;if(q.startsWith(`${X}=`))_[R]=`${X}="${w.publicKey}"`,H=!0;else if(q.startsWith(`${O}=`))_[R]=`${O}="${w.privateKey}"`,Y=!0}if(!H)_.push(`${X}="${w.publicKey}"`);if(!Y)_.push(`${O}="${w.privateKey}"`);let J=`${_.join(`
|
|
23
24
|
`).replace(/\n+$/,"")}
|
|
24
|
-
`,
|
|
25
|
+
`,V=Ef(E.output,w.publicKey,X,g),W=`${process.pid}-${Date.now()}`,j=U(gf($),`.${u($)}.${W}.rotate`),G=U(gf(B),`.${u(B)}.${W}.rotate`);try{if(Q(j,V,{encoding:"utf8",mode:384}),Q(G,J,{encoding:"utf8",mode:384}),g.stdout)return t(G,B),{success:!0,output:V};t(j,$);try{t(G,B)}catch(R){throw Q($,I),R}return{success:!0,output:`\u2714 rotated (${g.file||".env"}) to encrypted:v2`}}catch(R){if(!Z(B))Q(B,D,{mode:384});return{success:!1,error:`Failed to rotate without exposing plaintext: ${R instanceof Error?R.message:"Unknown error"}`}}finally{ff(j,{force:!0}),ff(G,{force:!0})}}export{yf as setEnv,af as rotateKeypair,Bf as reusableEnvPublicKey,of as resolveEnvFile,lf as getKeypair,pf as getEnv,P as envKeyNames,sf as encryptEnv,Lf as decryptEnv};
|
package/dist/index.js
CHANGED
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{createCipheriv as B2,createDecipheriv as $2,createHash as
|
|
2
|
+
import{createCipheriv as B2,createDecipheriv as $2,createHash as y,createPrivateKey as x2,createPublicKey as X2,diffieHellman as D2,generateKeyPairSync as H2,hkdfSync as _2,randomBytes as v}from"crypto";var n="x25519-public:",o="x25519-private:",P="encrypted:v2:",p="encrypted:",A2=Buffer.from("stacks-env:v2","utf8"),W2="Environment decryption failed: authentication or format error";function Y2(f){if(f.length===32)return f;return y("sha256").update(f).digest()}function Qf(f,g){let B=v(16),X=B2("aes-256-gcm",Y2(g),B);return{ciphertext:Buffer.concat([X.update(f,"utf8"),X.final()]).toString("hex"),iv:B.toString("hex"),authTag:X.getAuthTag().toString("hex")}}function F2(f,g,B,X){try{let $=$2("aes-256-gcm",Y2(g),Buffer.from(B,"hex"));return $.setAuthTag(Buffer.from(X,"hex")),Buffer.concat([$.update(Buffer.from(f,"hex")),$.final()]).toString("utf8")}catch{throw Error("Decryption failed: authentication or format error")}}function C(f){return f.toString("base64url")}function S(f,g,B){if(typeof f!=="string"||f.length>0&&!/^[A-Za-z0-9_-]+$/.test(f))throw Error(`${g} is not canonical base64url`);let X=Buffer.from(f,"base64url");if(C(X)!==f)throw Error(`${g} is not canonical base64url`);if(B!==void 0&&X.length!==B)throw Error(`${g} has an invalid length`);return X}function S2(f){if(!f.startsWith(n))throw Error("Legacy or invalid public key. Run buddy env:rotate before creating new ciphertext.");let g=X2({key:S(f.slice(n.length),"public key"),format:"der",type:"spki"});if(g.asymmetricKeyType!=="x25519")throw Error("Public key is not X25519");return g}function b2(f){if(!f.startsWith(o))throw Error("Private key is not a version 2 X25519 key");let g=x2({key:S(f.slice(o.length),"private key"),format:"der",type:"pkcs8"});if(g.asymmetricKeyType!=="x25519")throw Error("Private key is not X25519");return g}function j2(f){return Buffer.from(`stacks-env:v2;${f.epk};${f.salt};${f.nonce}`,"utf8")}function K(){let{publicKey:f,privateKey:g}=H2("x25519"),B=f.export({format:"der",type:"spki"}),X=g.export({format:"der",type:"pkcs8"});return{publicKey:`${n}${C(B)}`,privateKey:`${o}${C(X)}`}}function k(f,g){let B=S2(g),X=H2("x25519"),$=C(X.publicKey.export({format:"der",type:"spki"})),D=v(16),H=v(12),W=D2({privateKey:X.privateKey,publicKey:B}),A=Buffer.from(_2("sha256",W,D,A2,32)),Y={epk:$,salt:C(D),nonce:C(H)};try{let _=B2("aes-256-gcm",A,H);_.setAAD(j2(Y));let j=Buffer.concat([_.update(f,"utf8"),_.final()]),O={v:2,...Y,ciphertext:C(j),tag:C(_.getAuthTag())};return`${P}${C(Buffer.from(JSON.stringify(O),"utf8"))}`}finally{W.fill(0),A.fill(0)}}function h2(f){let g=S(f.slice(P.length),"envelope").toString("utf8"),B=JSON.parse(g);if(!B||typeof B!=="object"||Array.isArray(B))throw Error("envelope is not an object");let X=Object.keys(B).sort(),$=["ciphertext","epk","nonce","salt","tag","v"];if(X.length!==$.length||X.some((D,H)=>D!==$[H]))throw Error("envelope fields are invalid");if(B.v!==2)throw Error("envelope version is unsupported");for(let D of["epk","salt","nonce","ciphertext","tag"])if(typeof B[D]!=="string")throw Error(`${D} is invalid`);return B}function K2(f,g){try{let B=h2(f),X=b2(g),$=X2({key:S(B.epk,"ephemeral public key"),format:"der",type:"spki"});if($.asymmetricKeyType!=="x25519")throw Error("ephemeral key is not X25519");let D=S(B.salt,"salt",16),H=S(B.nonce,"nonce",12),W=S(B.ciphertext,"ciphertext"),A=S(B.tag,"tag",16),Y=D2({privateKey:X,publicKey:$}),_=Buffer.from(_2("sha256",Y,D,A2,32));try{let j=$2("aes-256-gcm",_,H);return j.setAAD(j2(B)),j.setAuthTag(A),Buffer.concat([j.update(W),j.final()]).toString("utf8")}finally{Y.fill(0),_.fill(0)}}catch{throw Error(W2)}}function N2(f,g){try{if(!/^[0-9a-f]{64}$/.test(g))throw Error("invalid legacy private key");let B=Buffer.from(f.slice(p.length),"base64");if(B.length<32)throw Error("invalid legacy payload");let X=y("sha256").update(Buffer.from(g,"hex")).digest(),$=y("sha256").update(X).digest();return F2(B.subarray(32).toString("hex"),$,B.subarray(0,16).toString("hex"),B.subarray(16,32).toString("hex"))}catch{throw Error(W2)}}function Rf(f){return f.startsWith(p)&&!f.startsWith(P)}function h(f,g){if(!f.startsWith(p))return f;return f.startsWith(P)?K2(f,g):N2(f,g)}function If(f,g,B){return k(h(f,g),B)}function wf(f){let g=f.match(/^DOTENV_PRIVATE_KEY_(.+)$/);return g&&g[1]?g[1].toLowerCase():""}function i(f=""){return f?process.env[`DOTENV_PRIVATE_KEY_${f.toUpperCase()}`]:process.env.DOTENV_PRIVATE_KEY}import{spawnSync as P2}from"child_process";import{readFileSync as k2}from"fs";import{arch as c2,hostname as m2,platform as G2,release as d2,userInfo as u2}from"os";import{basename as r2,dirname as t2,resolve as y2}from"path";function q(f,g={}){let B={},X=[],$=[],D=f.split(`
|
|
3
3
|
`);for(let H of D){let W=H.trim();if(!W||W.startsWith("#"))continue;if(W.startsWith("DOTENV_PUBLIC_KEY=")){let j=W.match(/^DOTENV_PUBLIC_KEY=["']?([^"'\n]+)["']?/);if(j&&j[1]!==void 0)B.DOTENV_PUBLIC_KEY=j[1];continue}let A=W.match(/^([^=]+)=(.*)$/);if(!A||A[1]===void 0||A[2]===void 0)continue;let Y=A[1].trim(),_=A[2].trim();if(_.startsWith('"')&&_.endsWith('"')||_.startsWith("'")&&_.endsWith("'")){if(_=_.slice(1,-1),_.includes("\\n"))_=_.replace(/\\n/g,`
|
|
4
|
-
`)}if(_.startsWith("encrypted:")||_.startsWith("enc:")){if(!g.privateKey){$.push(Y);continue}try{let j=_.startsWith("enc:")?`encrypted:${_.slice(4)}`:_;_=h(j,g.privateKey)}catch(j){X.push(`Failed to decrypt ${Y}: ${j instanceof Error?j.message:"Unknown error"}`)}}_=
|
|
5
|
-
`)){let W=H.trim();if(W.startsWith("#/"))continue;if(W.startsWith("DOTENV_PUBLIC_KEY"))continue;if(!W&&!D)continue;if(!W||W.startsWith("#")){D=!0,$.push(H);continue}D=!0;let A=W.match(/^([^=]+)=(.*)$/);if(!A||A[1]===void 0||A[2]===void 0){$.push(H);continue}let Y=A[1].trim(),_=A[2].trim();if(_.startsWith('"')&&_.endsWith('"')||_.startsWith("'")&&_.endsWith("'"))_=_.slice(1,-1);if((!X.key||Y.includes(X.key))&&(!X.excludeKey||!Y.includes(X.excludeKey))&&!_.startsWith("encrypted:"))_=
|
|
6
|
-
`)}function
|
|
4
|
+
`)}if(_.startsWith("encrypted:")||_.startsWith("enc:")){if(!g.privateKey){$.push(Y);continue}try{let j=_.startsWith("enc:")?`encrypted:${_.slice(4)}`:_;_=h(j,g.privateKey)}catch(j){X.push(`Failed to decrypt ${Y}: ${j instanceof Error?j.message:"Unknown error"}`)}}_=v2(_,{...g.processEnv||process.env,...B}),_=i2(_),B[Y]=_}return{parsed:B,errors:X,skippedEncrypted:$}}function v2(f,g){return f.replace(/\$\{([^}]+)\}/g,(B,X)=>{let $=X.match(/^([^:\-+]+)(:-|-)(.+)$/);if($){let[,H,W,A]=$,Y=g[H];if(W===":-")return Y||A;else return Y!==void 0?Y:A}let D=X.match(/^([^:\-+]+)(:?\+)(.+)$/);if(D){let[,H,W,A]=D,Y=g[H];if(W===":+")return Y?A:"";else return Y!==void 0?A:""}return g[X]||""})}var n2=new Set(["date","hostname","whoami","uname","pwd","echo","printf","cat","basename","dirname"]);function o2(f){let g=[],B="",X=null,$=!1;for(let D of f.trim()){if($){B+=D,$=!1;continue}if(D==="\\"&&X!=="'"){$=!0;continue}if(X){if(D===X)X=null;else B+=D;continue}if(D==='"'||D==="'"){X=D;continue}if(/\s/.test(D)){if(B)g.push(B),B="";continue}B+=D}if($||X)throw Error("Command substitution contains an unterminated escape or quote");if(B)g.push(B);return g}function p2(f){let[g,...B]=f,X=B.some(($)=>$.startsWith("-"));if(g==="echo"&&!X)return B.join(" ");if(g==="printf"&&B.length===1&&!B[0]?.includes("%"))return B[0];if(g==="hostname"&&B.length===0)return m2();if(g==="whoami"&&B.length===0)return u2().username;if(g==="pwd"&&B.length===0)return process.cwd();if(g==="basename"&&B.length===1)return r2(B[0]??"");if(g==="dirname"&&B.length===1)return t2(B[0]??"");if(g==="cat"&&B.length>0&&!X)return B.map(($)=>k2(y2(process.cwd(),$),"utf8")).join("");if(g==="uname"){let $=B[0]??"-s";if(B.length<=1&&$==="-m")return c2();if(B.length<=1&&$==="-r")return d2();if(B.length<=1&&$==="-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"}[G2()]??G2()}return}function i2(f){return f.replace(/\$\(([^)]+)\)/g,(g,B)=>{try{let X=o2(B),$=X[0];if(!$||!n2.has($))return console.warn(`[env] Blocked command substitution for disallowed command: ${$}`),"";let D=p2(X);if(D!==void 0)return D.trim();let H=P2($,X.slice(1),{encoding:"utf8",env:process.env});if(H.status===0)return H.stdout.trim();let W=H.stderr.trim();console.warn(`[env] Command substitution failed (exit code ${H.status??"unknown"}): ${$}${W?`: ${W}`:""}`)}catch(X){console.warn(`[env] Command substitution error: ${X instanceof Error?X.message:String(X)}`)}return""})}async function Vf(f,g={}){let B={},X=[],$=[];for(let D of f)try{let H=Bun.file(D);if(!H.size)continue;let W=await H.text(),{parsed:A,errors:Y,skippedEncrypted:_}=q(W,g);X.push(...Y),$.push(..._);for(let[j,O]of Object.entries(A)){if(!g.overload&&B[j]!==void 0)continue;B[j]=O}}catch(H){if(H?.code==="ENOENT")continue;X.push(`Failed to read ${D}: ${H instanceof Error?H.message:String(H)}`)}return{parsed:B,errors:X,skippedEncrypted:$}}import{existsSync as c}from"fs";import{readFileSync as l}from"fs";import{resolve as m}from"path";function N(f){return typeof f==="string"&&(f.startsWith("encrypted:")||f.startsWith("enc:"))}function b(f){if(!f||N(f))return;let g=f.trim().toLowerCase();if(!/^[a-z0-9_-]+$/.test(g))return;if(g==="prod")return"production";if(g==="stage")return"staging";if(g==="dev")return"development";return g}var d=null,u,r;function l2(f,g,B=".env.keys"){let X=m(g,B);if(!c(X))return;try{let{parsed:$}=q(l(X,"utf-8"));if(f){let D=$[`DOTENV_PRIVATE_KEY_${f.toUpperCase()}`];if(D)return D}return $.DOTENV_PRIVATE_KEY}catch{return}}function s(f={}){return b(f.env)||r||b("development")||b(process.env.DOTENV_ENV)||b(process.env.APP_ENV)||"development"}function s2(f={}){let g=s(f),B=f.cwd||process.cwd();if(d===g)return u;let X=i(g);if(!X)X=process.env.DOTENV_PRIVATE_KEY;if(!X)X=l2(g,B);return d=g,u=X,X}function Kf(){d=null,u=void 0,r=void 0}function J2(f,g={}){if(!N(f))return f;let B=s2(g);if(!B)return;try{let X=f.startsWith("enc:")?`encrypted:${f.slice(4)}`:f;return h(X,B)}catch{return}}function O2(f={}){let{path:g=[".env"],overload:B=!1,env:X,privateKey:$,keysFile:D=".env.keys",quiet:H=!1,cwd:W=process.cwd()}=f,A=Array.isArray(g)?g:[g],Y=[],_=0,j=new Set(Object.entries(process.env).filter(([,G])=>G!==void 0&&!N(G)).map(([G])=>G)),O=$;if(!O&&D){let G=m(W,D);if(c(G))try{let J=l(G,"utf-8"),{parsed:w}=q(J);if(X){let T=`DOTENV_PRIVATE_KEY_${X.toUpperCase()}`;O=w[T]}else O=w.DOTENV_PRIVATE_KEY}catch(J){Y.push(`Failed to load keys file: ${J instanceof Error?J.message:"Unknown error"}`)}}if(!O)O=i(X||"");for(let G of A){let J=m(W,G);if(!c(J)){if(!H)Y.push(`File not found: ${J}`);continue}try{let w=l(J,"utf-8"),{parsed:T,errors:I,skippedEncrypted:R}=q(w,{privateKey:O,processEnv:process.env});if(Y.push(...I),R.length>0){for(let F of R)if(N(process.env[F]))delete process.env[F];let Z=X?`DOTENV_PRIVATE_KEY_${X.toUpperCase()}`:"DOTENV_PRIVATE_KEY",z=R.slice(0,5).join(", "),x=R.length>5?`, \u2026 +${R.length-5} more`:"";console.warn(`[env] warning: skipped ${R.length} encrypted value(s) in ${G} (${Z} not set; defaults apply): ${z}${x}`)}let U=0;for(let[Z,z]of Object.entries(T)){if(Z==="DOTENV_PUBLIC_KEY")continue;let x=process.env[Z],F=N(x)&&z!==x;if(B||!j.has(Z)||F)process.env[Z]=z,_++,U++}if(!H&&!process.env.__ENV_LOADED__)console.error(`[env] loaded ${U}/${Object.keys(T).length} variables from ${G}`),process.env.__ENV_LOADED__="1"}catch(w){Y.push(`Failed to load ${J}: ${w instanceof Error?w.message:"Unknown error"}`)}}return{loaded:_,errors:Y}}function a2(f={}){return{name:"env-plugin",setup(g){O2(f)}}}function Nf(f={}){let g=b(f.env)||b("development")||b(process.env.DOTENV_ENV)||b(process.env.APP_ENV)||"development",B=f.cwd||process.cwd();if(r!==g)r=g,d=null,u=void 0;let X=[],$=(W)=>{if(!X.includes(W)&&c(m(B,W)))X.push(W)};$(".env"),$(".env.local");let D=`.env.${g}`,H=`.env.${g}.local`;return $(D),$(H),O2({...f,path:X,env:g})}var kf=a2;import{existsSync as V,readFileSync as M,renameSync as a,rmSync as Z2,writeFileSync as E}from"fs";import{basename as e,dirname as Q2,resolve as L}from"path";function rf(f,g){if(f)return f;let B=String(g??"").trim();if(!B||B==="development"||B==="dev"||B==="local")return"";if(!/^[\w.-]+$/.test(B))return"";return`.env.${B}`}function R2(f,g,B,X){let $=["#/-------------------[DOTENV_PUBLIC_KEY]--------------------/","#/ versioned X25519 encryption for .env files /","#/ [how it works](https://stacksjs.com/encryption) /","#/----------------------------------------------------------/",`${B}="${g}"`,""],D=!1;for(let H of f.split(`
|
|
5
|
+
`)){let W=H.trim();if(W.startsWith("#/"))continue;if(W.startsWith("DOTENV_PUBLIC_KEY"))continue;if(!W&&!D)continue;if(!W||W.startsWith("#")){D=!0,$.push(H);continue}D=!0;let A=W.match(/^([^=]+)=(.*)$/);if(!A||A[1]===void 0||A[2]===void 0){$.push(H);continue}let Y=A[1].trim(),_=A[2].trim();if(_.startsWith('"')&&_.endsWith('"')||_.startsWith("'")&&_.endsWith("'"))_=_.slice(1,-1);if((!X.key||Y.includes(X.key))&&(!X.excludeKey||!Y.includes(X.excludeKey))&&!_.startsWith("encrypted:"))_=k(_,g);$.push(`${Y}="${_}"`)}return $.join(`
|
|
6
|
+
`)}function t(f){let B=((f||".env").split("/").pop()||"").replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase();return{publicKeyName:B?`DOTENV_PUBLIC_KEY_${B}`:"DOTENV_PUBLIC_KEY",privateKeyName:B?`DOTENV_PRIVATE_KEY_${B}`:"DOTENV_PRIVATE_KEY"}}function I2(f,g){let B,X=!1;for(let $ of f.split(`
|
|
7
|
+
`)){let D=$.trim();if(!D||D.startsWith("#"))continue;let H=D.match(/^([^=]+)=(.*)$/);if(!H?.[1]||H[2]===void 0)continue;let W=H[1].trim(),A=H[2].trim().replace(/^["']|["']$/g,"");if(W===g)B=A;else if(A.startsWith("encrypted:")||A.startsWith("enc:"))X=!0}return B&&X?B:void 0}function tf(f={}){let g=f.cwd||process.cwd(),B=L(g,f.file||".env"),X=L(g,f.keysFile||".env.keys");if(!V(B))return{success:!1,error:`File not found: ${B}`};try{let $,D,H=I2(M(B,"utf-8"),t(f.file).publicKeyName);if(H)$=H;else if(V(X)){let Y=M(X,"utf-8"),{parsed:_}=q(Y),{publicKeyName:j,privateKeyName:O}=t(f.file);if($=_[j]||"",D=_[O]||"",!$||!D){let G=K();$=G.publicKey,D=G.privateKey;let J=`
|
|
8
|
+
${j}="${$}"
|
|
9
|
+
${O}="${D}"
|
|
10
|
+
`;E(X,Y+J,"utf-8")}}else{let Y=K();$=Y.publicKey,D=Y.privateKey;let{publicKeyName:_,privateKeyName:j}=t(f.file),O=`# .env.keys - Keep this file secure and never commit to source control
|
|
7
11
|
${_}="${$}"
|
|
8
12
|
${j}="${D}"
|
|
9
|
-
`;
|
|
10
|
-
${
|
|
11
|
-
${
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
`),Q=[];for(let T of G){let I=T.trim();if(!I||I.startsWith("#")){Q.push(T);continue}if(I.startsWith("DOTENV_PUBLIC_KEY"))continue;let R=I.match(/^([^=]+)=(.*)$/);if(!R||R[1]===void 0||R[2]===void 0){Q.push(T);continue}let U=R[1].trim(),J=R[2].trim();if(J.startsWith('"')&&J.endsWith('"')||J.startsWith("'")&&J.endsWith("'"))J=J.slice(1,-1);let z=J.startsWith("encrypted:");if(f.key&&!U.includes(f.key))z=!1;if(z){let x,F;for(let V2 of j)try{x=h(J,V2);break}catch(C2){F=C2}if(x===void 0)throw F instanceof Error?F:Error(`Unable to decrypt ${U}`);J=x}Q.push(`${U}="${J}"`)}let w=Q.join(`
|
|
15
|
-
`);if(f.stdout)return{success:!0,output:w};return C(B,w,"utf-8"),{success:!0,output:`\u2714 decrypted (${f.file||".env"})`}}catch($){return{success:!1,error:`Failed to decrypt: ${$ instanceof Error?$.message:"Unknown error"}`}}}function tf(f,g,B={}){let X=B.cwd||process.cwd(),$=L(X,B.file||".env"),D=L(X,B.keysFile||".env.keys");try{let H="";if(V($))H=q($,"utf-8");let W=H.split(`
|
|
16
|
-
`),A=!1,Y,Z=((B.file||".env").split("/").pop()||"").replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase(),G=Z?`DOTENV_PUBLIC_KEY_${Z}`:"DOTENV_PUBLIC_KEY",Q=Z?`DOTENV_PRIVATE_KEY_${Z}`:"DOTENV_PRIVATE_KEY";for(let I of W){let R=I.trim();if(R.startsWith(`${G}=`)){let U=R.match(/^[^=]+=["']?([^"'\n]+)["']?/);if(U)Y=U[1];break}}if(Y){let I=!1;if(V(D)){let R=q(D,"utf-8"),{parsed:U}=M(R);I=Boolean(U[Q])}if(!I)Y=void 0}if(!B.plain&&!Y){let I=K();Y=I.publicKey;let R="";if(V(D))R=q(D,"utf-8");R+=`
|
|
13
|
+
`;E(X,O,"utf-8")}let W=M(B,"utf-8"),A=R2(W,$,t(f.file).publicKeyName,f);if(f.stdout)return{success:!0,output:A};return E(B,A,"utf-8"),{success:!0,output:`\u2714 encrypted (${f.file||".env"})
|
|
14
|
+
\u2714 key added to ${f.keysFile||".env.keys"}`}}catch($){return{success:!1,error:`Failed to encrypt: ${$ instanceof Error?$.message:"Unknown error"}`}}}function e2(f={}){let g=f.cwd||process.cwd(),B=L(g,f.file||".env"),X=L(g,f.keysFile||".env.keys");if(!V(B))return{success:!1,error:`File not found: ${B}`};if(!V(X))return{success:!1,error:`Keys file not found: ${X}`};try{let $=M(X,"utf-8"),{parsed:D}=q($),A=((f.file||".env").split("/").pop()||"").replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase(),Y=A?`DOTENV_PRIVATE_KEY_${A}`:"DOTENV_PRIVATE_KEY",_=D[Y];if(!_)return{success:!1,error:`Private key not found: ${Y}`};let j=[_];if(Y!=="DOTENV_PRIVATE_KEY"&&D.DOTENV_PRIVATE_KEY&&D.DOTENV_PRIVATE_KEY!==_)j.push(D.DOTENV_PRIVATE_KEY);let G=M(B,"utf-8").split(`
|
|
15
|
+
`),J=[];for(let T of G){let I=T.trim();if(!I||I.startsWith("#")){J.push(T);continue}if(I.startsWith("DOTENV_PUBLIC_KEY"))continue;let R=I.match(/^([^=]+)=(.*)$/);if(!R||R[1]===void 0||R[2]===void 0){J.push(T);continue}let U=R[1].trim(),Z=R[2].trim();if(Z.startsWith('"')&&Z.endsWith('"')||Z.startsWith("'")&&Z.endsWith("'"))Z=Z.slice(1,-1);let z=Z.startsWith("encrypted:");if(f.key&&!U.includes(f.key))z=!1;if(z){let x,F;for(let E2 of j)try{x=h(Z,E2);break}catch(C2){F=C2}if(x===void 0)throw F instanceof Error?F:Error(`Unable to decrypt ${U}`);Z=x}J.push(`${U}="${Z}"`)}let w=J.join(`
|
|
16
|
+
`);if(f.stdout)return{success:!0,output:w};return E(B,w,"utf-8"),{success:!0,output:`\u2714 decrypted (${f.file||".env"})`}}catch($){return{success:!1,error:`Failed to decrypt: ${$ instanceof Error?$.message:"Unknown error"}`}}}function yf(f,g,B={}){let X=B.cwd||process.cwd(),$=L(X,B.file||".env"),D=L(X,B.keysFile||".env.keys");try{let H="";if(V($))H=M($,"utf-8");let W=H.split(`
|
|
17
|
+
`),A=!1,Y,O=((B.file||".env").split("/").pop()||"").replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase(),G=O?`DOTENV_PUBLIC_KEY_${O}`:"DOTENV_PUBLIC_KEY",J=O?`DOTENV_PRIVATE_KEY_${O}`:"DOTENV_PRIVATE_KEY";for(let I of W){let R=I.trim();if(R.startsWith(`${G}=`)){let U=R.match(/^[^=]+=["']?([^"'\n]+)["']?/);if(U)Y=U[1];break}}if(Y){let I=!1;if(V(D)){let R=M(D,"utf-8"),{parsed:U}=q(R);I=Boolean(U[J])}if(!I&&!process.env[J]&&!I2(H,G))Y=void 0}if(!B.plain&&!Y){let I=K();Y=I.publicKey;let R="";if(V(D))R=M(D,"utf-8");R+=`
|
|
17
18
|
${G}="${I.publicKey}"
|
|
18
|
-
${
|
|
19
|
-
`,
|
|
20
|
-
`);return
|
|
21
|
-
`);break;case"json":default:G=g.prettyPrint?JSON.stringify(
|
|
22
|
-
`),Y=f.file||".env",_=
|
|
19
|
+
${J}="${I.privateKey}"
|
|
20
|
+
`,E(D,R,"utf-8");for(let U=W.length-1;U>=0;U--){let Z=W[U];if(Z!==void 0&&Z.trim().startsWith(`${G}=`))W.splice(U,1)}W.unshift(`${G}="${Y}"`)}let w=g;if(!B.plain&&Y)w=k(g,Y);for(let I=0;I<W.length;I++){let R=W[I];if(R===void 0)continue;if(R.trim().startsWith(`${f}=`)){W[I]=`${f}="${w}"`,A=!0;break}}if(!A)W.push(`${f}="${w}"`);let T=W.join(`
|
|
21
|
+
`);return E($,T,"utf-8"),{success:!0,output:`set ${f}${B.plain?"":" with encryption"} (${B.file||".env"})`}}catch(H){return{success:!1,error:`Failed to set: ${H instanceof Error?H.message:"Unknown error"}`}}}function vf(f,g={}){let B=g.cwd||process.cwd(),X=L(B,g.file||".env");if(!V(X))return{success:!1,error:`File not found: ${X}`};try{let $,D=L(B,g.keysFile||".env.keys"),A=((g.file||".env").split("/").pop()||"").replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase(),Y=A?`DOTENV_PRIVATE_KEY_${A}`:"DOTENV_PRIVATE_KEY";if(V(D)){let J=M(D,"utf-8"),{parsed:w}=q(J);$=w[Y]}if(!$)$=process.env[Y];let _=M(X,"utf-8"),{parsed:j}=q(_,{privateKey:$});if(f&&!g.all){let J=j[f];if(J===void 0)return{success:!1,error:`Key not found: ${f}`};return{success:!0,output:J}}let O=g.all?{...process.env,...j}:j,G;switch(g.format){case"shell":G=Object.entries(O).map(([J,w])=>`${J}=${w}`).join(" ");break;case"eval":G=Object.entries(O).map(([J,w])=>`${J}="${w}"`).join(`
|
|
22
|
+
`);break;case"json":default:G=g.prettyPrint?JSON.stringify(O,null,2):JSON.stringify(O);break}return{success:!0,output:G}}catch($){return{success:!1,error:`Failed to get: ${$ instanceof Error?$.message:"Unknown error"}`}}}function nf(f,g={}){let B=g.cwd||process.cwd(),X=L(B,g.keysFile||".env.keys");if(!V(X))return{success:!1,error:`Keys file not found: ${X}`};try{let $=M(X,"utf-8"),{parsed:D}=q($),A=((g.file||".env").split("/").pop()||"").replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase(),Y=A?`DOTENV_PUBLIC_KEY_${A}`:"DOTENV_PUBLIC_KEY",_=A?`DOTENV_PRIVATE_KEY_${A}`:"DOTENV_PRIVATE_KEY";if(f){let G=D[f];if(!G)return{success:!1,error:`Key not found: ${f}`};return{success:!0,output:G}}let j={[Y]:D[Y],[_]:D[_]};return{success:!0,output:g.format==="shell"?`${Y}=${j[Y]} ${_}=${j[_]}`:JSON.stringify(j)}}catch($){return{success:!1,error:`Failed to get keypair: ${$ instanceof Error?$.message:"Unknown error"}`}}}function of(f={}){let g=e2({file:f.file,keysFile:f.keysFile,stdout:!0,cwd:f.cwd});if(!g.success)return g;let B=f.cwd||process.cwd(),X=L(B,f.file||".env"),$=L(B,f.keysFile||".env.keys");if(g.output===void 0)return{success:!1,error:"Rotation produced no decrypted content"};let D=K(),H=M(X),W=M($),A=W.toString("utf8").split(`
|
|
23
|
+
`),Y=f.file||".env",_=e(Y).replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase(),j=_?`DOTENV_PUBLIC_KEY_${_}`:"DOTENV_PUBLIC_KEY",O=_?`DOTENV_PRIVATE_KEY_${_}`:"DOTENV_PRIVATE_KEY",G=!1,J=!1;for(let Z=0;Z<A.length;Z++){let z=A[Z];if(z===void 0)continue;if(z.startsWith(`${j}=`))A[Z]=`${j}="${D.publicKey}"`,G=!0;else if(z.startsWith(`${O}=`))A[Z]=`${O}="${D.privateKey}"`,J=!0}if(!G)A.push(`${j}="${D.publicKey}"`);if(!J)A.push(`${O}="${D.privateKey}"`);let w=`${A.join(`
|
|
23
24
|
`).replace(/\n+$/,"")}
|
|
24
|
-
`,T=R2(g.output,D.publicKey,j,f),I=`${process.pid}-${Date.now()}`,R=L(Q2(X),`.${
|
|
25
|
-
`).map((g)=>g.trim()).filter(Boolean).filter((g)=>g!==".env.example").filter((g)=>/(?:^|\/)\.env(?:\.|$)/.test(g)).filter((g)=>!g.endsWith(".env.keys"))}function
|
|
26
|
-
`),H=D.findIndex((Y)=>Y.startsWith(`${f}=`)),A=/[\s"'#$\\]/.test(g)?`"${g.replace(/"/g,"\\\"")}"`:g;if(H!==-1)D[H]=`${f}=${A}`;else D.push(`${f}=${A}`);
|
|
27
|
-
`))}function
|
|
25
|
+
`,T=R2(g.output,D.publicKey,j,f),I=`${process.pid}-${Date.now()}`,R=L(Q2(X),`.${e(X)}.${I}.rotate`),U=L(Q2($),`.${e($)}.${I}.rotate`);try{if(E(R,T,{encoding:"utf8",mode:384}),E(U,w,{encoding:"utf8",mode:384}),f.stdout)return a(U,$),{success:!0,output:T};a(R,X);try{a(U,$)}catch(Z){throw E(X,H),Z}return{success:!0,output:`\u2714 rotated (${f.file||".env"}) to encrypted:v2`}}catch(Z){if(!V($))E($,W,{mode:384});return{success:!1,error:`Failed to rotate without exposing plaintext: ${Z instanceof Error?Z.message:"Unknown error"}`}}finally{Z2(R,{force:!0}),Z2(U,{force:!0})}}import Q from"process";import{platform as ff}from"os";var w2=typeof Bun<"u",U2=typeof Q<"u"&&Q.versions?.node!==void 0,gf=w2?"bun":U2?"node":"unknown",af={name:gf,version:w2?Bun.version:U2?Q.version:void 0},g2=ff(),ef=g2==="win32",f5=g2==="darwin",g5=g2==="linux",T2=Boolean(Q.stdout?.isTTY),B5=typeof globalThis.window<"u",Bf=Boolean(Q.env.CI||Q.env.CONTINUOUS_INTEGRATION||Q.env.BUILD_NUMBER||Q.env.RUN_ID),$5=Boolean(Q.env.DEBUG||Q.env.VERBOSE||Q.argv.includes("--debug")||Q.argv.includes("--verbose")),$f=Bf||!T2,X5=Boolean(!$f&&(T2||Q.env.COLORTERM||Q.env.FORCE_COLOR||Q.env.TERM&&Q.env.TERM!=="dumb")),f2={github:{name:"GitHub Actions",detected:Boolean(Q.env.GITHUB_ACTIONS)},gitlab:{name:"GitLab CI",detected:Boolean(Q.env.GITLAB_CI)},circle:{name:"CircleCI",detected:Boolean(Q.env.CIRCLECI)},travis:{name:"Travis CI",detected:Boolean(Q.env.TRAVIS)},jenkins:{name:"Jenkins",detected:Boolean(Q.env.JENKINS_URL)},vercel:{name:"Vercel",detected:Boolean(Q.env.VERCEL)},netlify:{name:"Netlify",detected:Boolean(Q.env.NETLIFY)},heroku:{name:"Heroku",detected:Boolean(Q.env.DYNO)},aws:{name:"AWS",detected:Boolean(Q.env.AWS_REGION||Q.env.AWS_LAMBDA_FUNCTION_NAME)},azure:{name:"Azure",detected:Boolean(Q.env.AZURE_HTTP_USER_AGENT)},cloudflare:{name:"Cloudflare",detected:Boolean(Q.env.CF_PAGES)},railway:{name:"Railway",detected:Boolean(Q.env.RAILWAY_ENVIRONMENT)},render:{name:"Render",detected:Boolean(Q.env.RENDER)}},Xf=Object.keys(f2).find((f)=>{let g=f2[f];return g!==void 0&&g.detected})||"unknown",D5=f2[Xf]||{name:"Unknown",detected:!1};import Yf from"process";import{projectPath as jf}from"@stacksjs/path";import q2 from"fs";var L2={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 Df(f){return typeof f==="string"&&(f.startsWith("encrypted:")||f.startsWith("enc:"))}var Hf=/(?:^|_)(?:PASSWORD|PASSWD|SECRET|TOKEN|APIKEY|API_KEY|CREDENTIAL|CREDENTIALS|PRIVATE|SALT|DSN|CERT|CIPHER|SIGNATURE|WEBHOOK)(?:_|$)|(?:^|_)(?:ACCESS|SECRET)_KEY(?:_|$)|_KEY$/,_f=/^DOTENV_(?:PUBLIC|PRIVATE)_KEY/,Af=/^\$\{[^}]+\}$/;function A5(f,g={}){let{placeholders:B={},strict:X=!1}=g,$=[];for(let[D,H]of Object.entries(f)){if(!H||H.trim()==="")continue;if(Df(H))continue;if(_f.test(D))continue;if(Af.test(H.trim()))continue;let W=Hf.test(D),A=Object.prototype.hasOwnProperty.call(B,D)&&B[D]===H;if(W&&!A){$.push({key:D,reason:"plaintext-secret"});continue}if(X)$.push({key:D,reason:"plaintext-value"})}return $}function W5(f){return f.split(`
|
|
26
|
+
`).map((g)=>g.trim()).filter(Boolean).filter((g)=>g!==".env.example").filter((g)=>/(?:^|\/)\.env(?:\.|$)/.test(g)).filter((g)=>!g.endsWith(".env.keys"))}function M2(f){return`${f.replace(/[^a-z0-9]/gi,"").toUpperCase()}_`}function Wf(f,g={}){let B=g.self?M2(g.self):void 0,X=(g.tenants??[]).filter((H)=>typeof H==="string"&&H.trim().length>0).map((H)=>({slug:H,prefix:M2(H)})).filter(({prefix:H})=>H!==B).sort((H,W)=>W.prefix.length-H.prefix.length),$={},D={};for(let[H,W]of Object.entries(f)){let A=X.find(({prefix:Y})=>H.startsWith(Y));if(A){(D[A.slug]??={})[H]=W;continue}$[H]=W}return{own:$,foreign:D}}function j5(f,g={}){return Wf(f,g).own}function G5(f){return Object.entries(f.foreign).map(([g,B])=>({tenant:g,keys:Object.keys(B).sort()})).sort((g,B)=>g.tenant.localeCompare(B.tenant))}var z2=new Set;function Gf(f){if(z2.has(f))return;z2.add(f);let g=s();console.warn(`[env] warning: "${f}" is encrypted but no usable private key was found for the "${g}" environment; falling back to its default. Set DOTENV_PRIVATE_KEY_${g.toUpperCase()} or ship .env.keys.`)}var Jf={get:(f,g)=>{let B=f[g];if(typeof B==="string"&&(B.startsWith("encrypted:")||B.startsWith("enc:"))){let $=J2(B);if($===void 0){Gf(g),delete f[g];return}f[g]=$,B=$}let X=["_PORT","_TIMEOUT","_TTL","_SIZE","_LIMIT","_MAX","_MIN","_INTERVAL","_RETRIES","_CONCURRENCY","_WORKERS","_CONNECTIONS"];if(typeof B==="string"&&/^\d+$/.test(B)&&!B.startsWith("0")&&X.some(($)=>g.endsWith($)))return Number(B);if(typeof B==="string"){let $=B.toLowerCase();if($==="true")return!0;if($==="false")return!1}return B}};function Of(){return typeof Bun<"u"?Bun.env:Yf.env}var V2=new Proxy(Of(),Jf);function w5(f,g,B){let X=B?.path||jf(".env"),D=q2.readFileSync(X,"utf-8").split(`
|
|
27
|
+
`),H=D.findIndex((Y)=>Y.startsWith(`${f}=`)),A=/[\s"'#$\\]/.test(g)?`"${g.replace(/"/g,"\\\"")}"`:g;if(H!==-1)D[H]=`${f}=${A}`;else D.push(`${f}=${A}`);q2.writeFileSync(X,D.join(`
|
|
28
|
+
`))}function U5(f=V2){let g=[];for(let[B,X]of Object.entries(L2)){let $=f[B];if($!==void 0&&$!==""&&!X.includes(String($)))g.push(`${B}="${$}" is not valid. Allowed values: ${X.join(", ")}`)}return g}function T5(f,g=V2){let B=[];for(let X of f){let $=g[X];if($===void 0||$===""||$===null)B.push(X)}if(B.length>0)throw Error(`[env] Missing required environment variable(s): ${B.join(", ")}. Set them in .env or your process environment before booting.`);return g}export{w5 as writeEnv,U5 as validateEnv,W5 as trackedEnvFiles,M2 as tenantEnvPrefix,j5 as stripForeignTenantEnv,yf as setEnv,af as runtimeInfo,gf as runtime,of as rotateKeypair,I2 as reusableEnvPublicKey,s2 as resolvePrivateKey,rf as resolveEnvFile,Kf as resetPrivateKeyCache,T5 as requireEnv,D5 as providerInfo,Xf as provider,Of as process,g2 as platform,A5 as plaintextSecrets,Wf as partitionTenantEnv,wf as parseEnvFromKey,q as parse,If as migrateEncryptedValue,Vf as loadEnvFiles,O2 as loadEnv,ef as isWindows,U2 as isNode,$f as isMinimal,f5 as isMacOS,g5 as isLinux,Rf as isLegacyEncryptedValue,Df as isEncryptedValue,$5 as isDebug,X5 as isColorSupported,Bf as isCI,w2 as isBun,B5 as hasWindow,T2 as hasTTY,i as getPrivateKey,nf as getKeypair,vf as getEnv,K as generateKeypair,G5 as foreignTenantKeys,a2 as envPlugin,t as envKeyNames,L2 as envEnum,V2 as env,k as encryptValue,tf as encryptEnv,h as decryptValue,J2 as decryptEnvValue,e2 as decryptEnv,Nf as autoLoadEnv,Qf as aesEncrypt,F2 as aesDecrypt,s as activeEnvName,Hf as SECRET_NAME_PATTERN};
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/env",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.72.
|
|
5
|
+
"version": "0.72.36",
|
|
6
6
|
"description": "Stacks env helper methods.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -55,10 +55,10 @@
|
|
|
55
55
|
"prepublishOnly": "bun run build"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@stacksjs/path": "0.72.
|
|
58
|
+
"@stacksjs/path": "0.72.36"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"better-dx": "^0.2.24",
|
|
62
|
-
"@stacksjs/validation": "0.72.
|
|
62
|
+
"@stacksjs/validation": "0.72.36"
|
|
63
63
|
}
|
|
64
64
|
}
|