@stacksjs/env 0.70.160 → 0.70.162
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -9
- package/dist/cli.js +23 -22
- package/dist/crypto.d.ts +3 -7
- package/dist/crypto.js +1 -1
- package/dist/index.js +25 -24
- package/dist/parser.js +3 -3
- package/dist/plugin.d.ts +20 -0
- package/dist/plugin.js +3 -3
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ A secure .env file management package with built-in encryption support for Bun a
|
|
|
6
6
|
|
|
7
7
|
- 🔐 **Automatic Encryption/Decryption** - Secure your environment variables with public-key cryptography
|
|
8
8
|
- 🚀 **Bun Plugin** - Seamless integration with Bun's runtime
|
|
9
|
-
- 🔑 **
|
|
9
|
+
- 🔑 **Versioned envelope** - X25519 + HKDF-SHA-256 + AES-256-GCM
|
|
10
10
|
- 📝 **Variable Expansion** - Support for `${VAR}`, defaults, and alternates
|
|
11
11
|
- 🔧 **Command Substitution** - Execute commands with `$(command)`
|
|
12
12
|
- 🎯 **Multi-Environment** - Manage multiple .env files for different environments
|
|
@@ -98,12 +98,25 @@ buddy env:decrypt --file .env.production
|
|
|
98
98
|
|
|
99
99
|
### How Encryption Works
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
New writes use the experimental version 2 envelope proposed in
|
|
102
|
+
[`stacksjs/rfcs#6`](https://github.com/stacksjs/rfcs/issues/6):
|
|
102
103
|
|
|
103
|
-
1. A keypair is generated using
|
|
104
|
-
2.
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
1. A recipient keypair is generated using X25519.
|
|
105
|
+
2. Every value gets a fresh ephemeral X25519 pair, 16-byte HKDF salt, and
|
|
106
|
+
12-byte AES-GCM nonce.
|
|
107
|
+
3. X25519 and HKDF-SHA-256 derive a one-use AES-256-GCM key.
|
|
108
|
+
4. Ciphertext and envelope metadata are authenticated; malformed, modified, and
|
|
109
|
+
wrong-key inputs fail with the same non-secret error.
|
|
110
|
+
|
|
111
|
+
The pre-RFC `encrypted:<base64>` format can only be read for migration. New
|
|
112
|
+
writes are `encrypted:v2:<base64url>`, and legacy public keys are rejected. Run
|
|
113
|
+
`buddy env:rotate` to decrypt legacy values in memory, generate a version 2 key,
|
|
114
|
+
and replace the encrypted file without writing plaintext to disk.
|
|
115
|
+
|
|
116
|
+
This feature is not a complete secret-management system. It has not yet passed
|
|
117
|
+
the independent review required by [Stacks issue #2058](https://github.com/stacksjs/stacks/issues/2058),
|
|
118
|
+
so do not treat it as a production security boundary without your own review.
|
|
119
|
+
See [SECURITY.md](./SECURITY.md) for its threat model and non-guarantees.
|
|
107
120
|
|
|
108
121
|
**Example encrypted .env:**
|
|
109
122
|
|
|
@@ -112,11 +125,11 @@ The encryption uses **secp256k1 ECIES** (Elliptic Curve Integrated Encryption Sc
|
|
|
112
125
|
# / public-key encryption for .env files /
|
|
113
126
|
# / [how it works](https://stacksjs.com/encryption) /
|
|
114
127
|
# /----------------------------------------------------------/
|
|
115
|
-
DOTENV_PUBLIC_KEY="
|
|
128
|
+
DOTENV_PUBLIC_KEY="x25519-public:<base64url-spki>"
|
|
116
129
|
|
|
117
130
|
# .env
|
|
118
|
-
API_KEY="encrypted:
|
|
119
|
-
DB_PASSWORD="encrypted:
|
|
131
|
+
API_KEY="encrypted:v2:<base64url-envelope>"
|
|
132
|
+
DB_PASSWORD="encrypted:v2:<base64url-envelope>"
|
|
120
133
|
```
|
|
121
134
|
|
|
122
135
|
## CLI Commands
|
package/dist/cli.js
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{createCipheriv as
|
|
3
|
-
`);for(let
|
|
4
|
-
`)}if(
|
|
5
|
-
|
|
6
|
-
${
|
|
7
|
-
|
|
8
|
-
${
|
|
9
|
-
${
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
\u2714 key added to ${
|
|
14
|
-
`),
|
|
15
|
-
`);if(
|
|
16
|
-
`)
|
|
17
|
-
${
|
|
18
|
-
${R}="${
|
|
19
|
-
`,
|
|
20
|
-
`);return
|
|
21
|
-
`);break;case"json":default:
|
|
22
|
-
`),
|
|
23
|
-
`)
|
|
2
|
+
import{createCipheriv as u,createDecipheriv as K,createHash as C,createPrivateKey as e,createPublicKey as m,diffieHellman as k,generateKeyPairSync as n,hkdfSync as s,randomBytes as F}from"crypto";var b="x25519-public:",S="x25519-private:",V="encrypted:v2:",P="encrypted:",o=Buffer.from("stacks-env:v2","utf8"),i="Environment decryption failed: authentication or format error";function v(f){if(f.length===32)return f;return C("sha256").update(f).digest()}function _f(f,g){let r=F(16),c=u("aes-256-gcm",v(g),r);return{ciphertext:Buffer.concat([c.update(f,"utf8"),c.final()]).toString("hex"),iv:r.toString("hex"),authTag:c.getAuthTag().toString("hex")}}function ff(f,g,r,c){try{let E=K("aes-256-gcm",v(g),Buffer.from(r,"hex"));return E.setAuthTag(Buffer.from(c,"hex")),Buffer.concat([E.update(Buffer.from(f,"hex")),E.final()]).toString("utf8")}catch{throw Error("Decryption failed: authentication or format error")}}function U(f){return f.toString("base64url")}function z(f,g,r){if(typeof f!=="string"||f.length>0&&!/^[A-Za-z0-9_-]+$/.test(f))throw Error(`${g} is not canonical base64url`);let c=Buffer.from(f,"base64url");if(U(c)!==f)throw Error(`${g} is not canonical base64url`);if(r!==void 0&&c.length!==r)throw Error(`${g} has an invalid length`);return c}function gf(f){if(!f.startsWith(b))throw Error("Legacy or invalid public key. Run buddy env:rotate before creating new ciphertext.");let g=m({key:z(f.slice(b.length),"public key"),format:"der",type:"spki"});if(g.asymmetricKeyType!=="x25519")throw Error("Public key is not X25519");return g}function rf(f){if(!f.startsWith(S))throw Error("Private key is not a version 2 X25519 key");let g=e({key:z(f.slice(S.length),"private key"),format:"der",type:"pkcs8"});if(g.asymmetricKeyType!=="x25519")throw Error("Private key is not X25519");return g}function y(f){return Buffer.from(`stacks-env:v2;${f.epk};${f.salt};${f.nonce}`,"utf8")}function q(){let{publicKey:f,privateKey:g}=n("x25519"),r=f.export({format:"der",type:"spki"}),c=g.export({format:"der",type:"pkcs8"});return{publicKey:`${b}${U(r)}`,privateKey:`${S}${U(c)}`}}function L(f,g){let r=gf(g),c=n("x25519"),E=U(c.publicKey.export({format:"der",type:"spki"})),h=F(16),A=F(12),x=k({privateKey:c.privateKey,publicKey:r}),$=Buffer.from(s("sha256",x,h,o,32)),B={epk:E,salt:U(h),nonce:U(A)};try{let w=u("aes-256-gcm",$,A);w.setAAD(y(B));let _=Buffer.concat([w.update(f,"utf8"),w.final()]),D={v:2,...B,ciphertext:U(_),tag:U(w.getAuthTag())};return`${V}${U(Buffer.from(JSON.stringify(D),"utf8"))}`}finally{x.fill(0),$.fill(0)}}function Ef(f){let g=z(f.slice(V.length),"envelope").toString("utf8"),r=JSON.parse(g);if(!r||typeof r!=="object"||Array.isArray(r))throw Error("envelope is not an object");let c=Object.keys(r).sort(),E=["ciphertext","epk","nonce","salt","tag","v"];if(c.length!==E.length||c.some((h,A)=>h!==E[A]))throw Error("envelope fields are invalid");if(r.v!==2)throw Error("envelope version is unsupported");for(let h of["epk","salt","nonce","ciphertext","tag"])if(typeof r[h]!=="string")throw Error(`${h} is invalid`);return r}function cf(f,g){try{let r=Ef(f),c=rf(g),E=m({key:z(r.epk,"ephemeral public key"),format:"der",type:"spki"});if(E.asymmetricKeyType!=="x25519")throw Error("ephemeral key is not X25519");let h=z(r.salt,"salt",16),A=z(r.nonce,"nonce",12),x=z(r.ciphertext,"ciphertext"),$=z(r.tag,"tag",16),B=k({privateKey:c,publicKey:E}),w=Buffer.from(s("sha256",B,h,o,32));try{let _=K("aes-256-gcm",w,A);return _.setAAD(y(r)),_.setAuthTag($),Buffer.concat([_.update(x),_.final()]).toString("utf8")}finally{B.fill(0),w.fill(0)}}catch{throw Error(i)}}function Bf(f,g){try{if(!/^[0-9a-f]{64}$/.test(g))throw Error("invalid legacy private key");let r=Buffer.from(f.slice(P.length),"base64");if(r.length<32)throw Error("invalid legacy payload");let c=C("sha256").update(Buffer.from(g,"hex")).digest(),E=C("sha256").update(c).digest();return ff(r.subarray(32).toString("hex"),E,r.subarray(0,16).toString("hex"),r.subarray(16,32).toString("hex"))}catch{throw Error(i)}}function If(f){return f.startsWith(P)&&!f.startsWith(V)}function Q(f,g){if(!f.startsWith(P))return f;return f.startsWith(V)?cf(f,g):Bf(f,g)}function Tf(f,g,r){return L(Q(f,g),r)}function tf(f){let g=f.match(/^DOTENV_PRIVATE_KEY_(.+)$/);return g&&g[1]?g[1].toLowerCase():""}function Rf(f=""){return f?process.env[`DOTENV_PRIVATE_KEY_${f.toUpperCase()}`]:process.env.DOTENV_PRIVATE_KEY}function Z(f,g={}){let r={},c=[],E=[],h=f.split(`
|
|
3
|
+
`);for(let A of h){let x=A.trim();if(!x||x.startsWith("#"))continue;if(x.startsWith("DOTENV_PUBLIC_KEY=")){let _=x.match(/^DOTENV_PUBLIC_KEY=["']?([^"'\n]+)["']?/);if(_&&_[1]!==void 0)r.DOTENV_PUBLIC_KEY=_[1];continue}let $=x.match(/^([^=]+)=(.*)$/);if(!$||$[1]===void 0||$[2]===void 0)continue;let B=$[1].trim(),w=$[2].trim();if(w.startsWith('"')&&w.endsWith('"')||w.startsWith("'")&&w.endsWith("'")){if(w=w.slice(1,-1),w.includes("\\n"))w=w.replace(/\\n/g,`
|
|
4
|
+
`)}if(w.startsWith("encrypted:")||w.startsWith("enc:")){if(!g.privateKey){E.push(B);continue}try{let _=w.startsWith("enc:")?`encrypted:${w.slice(4)}`:w;w=Q(_,g.privateKey)}catch(_){c.push(`Failed to decrypt ${B}: ${_ instanceof Error?_.message:"Unknown error"}`)}}w=hf(w,{...g.processEnv||process.env,...r}),w=$f(w),r[B]=w}return{parsed:r,errors:c,skippedEncrypted:E}}function hf(f,g){return f.replace(/\$\{([^}]+)\}/g,(r,c)=>{let E=c.match(/^([^:\-+]+)(:-|-)(.+)$/);if(E){let[,A,x,$]=E,B=g[A];if(x===":-")return B||$;else return B!==void 0?B:$}let h=c.match(/^([^:\-+]+)(:?\+)(.+)$/);if(h){let[,A,x,$]=h,B=g[A];if(x===":+")return B?$:"";else return B!==void 0?$:""}return g[c]||""})}var wf=new Set(["date","hostname","whoami","uname","pwd","echo","printf","cat","basename","dirname"]);function $f(f){return f.replace(/\$\(([^)]+)\)/g,(g,r)=>{try{let c=r.trim().split(/\s+/),E=c[0];if(!E||!wf.has(E))return console.warn(`[env] Blocked command substitution for disallowed command: ${E}`),"";let h=Bun.spawnSync(c,{stdout:"pipe",stderr:"pipe"});if(h.exitCode===0)return new TextDecoder().decode(h.stdout).trim();console.warn(`[env] Command substitution failed (exit code ${h.exitCode}): ${E}`)}catch(c){console.warn(`[env] Command substitution error: ${c instanceof Error?c.message:String(c)}`)}return""})}async function Of(f,g={}){let r={},c=[],E=[];for(let h of f)try{let A=Bun.file(h);if(!A.size)continue;let x=await A.text(),{parsed:$,errors:B,skippedEncrypted:w}=Z(x,g);c.push(...B),E.push(...w);for(let[_,D]of Object.entries($)){if(!g.overload&&r[_]!==void 0)continue;r[_]=D}}catch(A){if(A?.code==="ENOENT")continue;c.push(`Failed to read ${h}: ${A instanceof Error?A.message:String(A)}`)}return{parsed:r,errors:c,skippedEncrypted:E}}import{existsSync as G,readFileSync as j,renameSync as d,rmSync as l,writeFileSync as J}from"fs";import{basename as N,dirname as p,resolve as W}from"path";function a(f,g,r,c){let E=["#/-------------------[DOTENV_PUBLIC_KEY]--------------------/","#/ versioned X25519 encryption for .env files /","#/ [how it works](https://stacksjs.com/encryption) /","#/----------------------------------------------------------/",`${r}="${g}"`,""];for(let h of f.split(`
|
|
5
|
+
`)){let A=h.trim();if(!A||A.startsWith("#")){E.push(h);continue}if(A.startsWith("DOTENV_PUBLIC_KEY"))continue;let x=A.match(/^([^=]+)=(.*)$/);if(!x||x[1]===void 0||x[2]===void 0){E.push(h);continue}let $=x[1].trim(),B=x[2].trim();if(B.startsWith('"')&&B.endsWith('"')||B.startsWith("'")&&B.endsWith("'"))B=B.slice(1,-1);if((!c.key||$.includes(c.key))&&(!c.excludeKey||!$.includes(c.excludeKey))&&!B.startsWith("encrypted:"))B=L(B,g);E.push(`${$}="${B}"`)}return E.join(`
|
|
6
|
+
`)}function Jf(f={}){let g=f.cwd||process.cwd(),r=W(g,f.file||".env"),c=W(g,f.keysFile||".env.keys");if(!G(r))return{success:!1,error:`File not found: ${r}`};try{let E,h;if(G(c)){let w=j(c,"utf-8"),{parsed:_}=Z(w),R=((f.file||".env").split("/").pop()||"").replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase(),X=R?`DOTENV_PUBLIC_KEY_${R}`:"DOTENV_PUBLIC_KEY",H=R?`DOTENV_PRIVATE_KEY_${R}`:"DOTENV_PRIVATE_KEY";if(E=_[X]||"",h=_[H]||"",!E||!h){let t=q();E=t.publicKey,h=t.privateKey;let O=`
|
|
7
|
+
${X}="${E}"
|
|
8
|
+
${H}="${h}"
|
|
9
|
+
`;J(c,w+O,"utf-8")}}else{let w=q();E=w.publicKey,h=w.privateKey;let I=((f.file||".env").split("/").pop()||"").replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase(),R=I?`DOTENV_PUBLIC_KEY_${I}`:"DOTENV_PUBLIC_KEY",X=I?`DOTENV_PRIVATE_KEY_${I}`:"DOTENV_PRIVATE_KEY",H=`# .env.keys - Keep this file secure and never commit to source control
|
|
10
|
+
${R}="${E}"
|
|
11
|
+
${X}="${h}"
|
|
12
|
+
`;J(c,H,"utf-8")}let A=j(r,"utf-8"),x=f.file?f.file.replace(/^\.env\./,"").toUpperCase():"",$=x?`DOTENV_PUBLIC_KEY_${x}`:"DOTENV_PUBLIC_KEY",B=a(A,E,$,f);if(f.stdout)return{success:!0,output:B};return J(r,B,"utf-8"),{success:!0,output:`\u2714 encrypted (${f.file||".env"})
|
|
13
|
+
\u2714 key added to ${f.keysFile||".env.keys"}`}}catch(E){return{success:!1,error:`Failed to encrypt: ${E instanceof Error?E.message:"Unknown error"}`}}}function xf(f={}){let g=f.cwd||process.cwd(),r=W(g,f.file||".env"),c=W(g,f.keysFile||".env.keys");if(!G(r))return{success:!1,error:`File not found: ${r}`};if(!G(c))return{success:!1,error:`Keys file not found: ${c}`};try{let E=j(c,"utf-8"),{parsed:h}=Z(E),$=((f.file||".env").split("/").pop()||"").replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase(),B=$?`DOTENV_PRIVATE_KEY_${$}`:"DOTENV_PRIVATE_KEY",w=h[B];if(!w)return{success:!1,error:`Private key not found: ${B}`};let D=j(r,"utf-8").split(`
|
|
14
|
+
`),I=[];for(let X of D){let H=X.trim();if(!H||H.startsWith("#")){I.push(X);continue}if(H.startsWith("DOTENV_PUBLIC_KEY"))continue;let t=H.match(/^([^=]+)=(.*)$/);if(!t||t[1]===void 0||t[2]===void 0){I.push(X);continue}let O=t[1].trim(),T=t[2].trim();if(T.startsWith('"')&&T.endsWith('"')||T.startsWith("'")&&T.endsWith("'"))T=T.slice(1,-1);let Y=T.startsWith("encrypted:");if(f.key&&!O.includes(f.key))Y=!1;if(Y)T=Q(T,w);I.push(`${O}="${T}"`)}let R=I.join(`
|
|
15
|
+
`);if(f.stdout)return{success:!0,output:R};return J(r,R,"utf-8"),{success:!0,output:`\u2714 decrypted (${f.file||".env"})`}}catch(E){return{success:!1,error:`Failed to decrypt: ${E instanceof Error?E.message:"Unknown error"}`}}}function Uf(f,g,r={}){let c=r.cwd||process.cwd(),E=W(c,r.file||".env"),h=W(c,r.keysFile||".env.keys");try{let A="";if(G(E))A=j(E,"utf-8");let x=A.split(`
|
|
16
|
+
`),$=!1,B,D=((r.file||".env").split("/").pop()||"").replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase(),I=D?`DOTENV_PUBLIC_KEY_${D}`:"DOTENV_PUBLIC_KEY",R=D?`DOTENV_PRIVATE_KEY_${D}`:"DOTENV_PRIVATE_KEY";for(let t of x){let O=t.trim();if(O.startsWith(`${I}=`)){let T=O.match(/^[^=]+=["']?([^"'\n]+)["']?/);if(T)B=T[1];break}}if(B){let t=!1;if(G(h)){let O=j(h,"utf-8"),{parsed:T}=Z(O);t=Boolean(T[R])}if(!t)B=void 0}if(!r.plain&&!B){let t=q();B=t.publicKey;let O="";if(G(h))O=j(h,"utf-8");O+=`
|
|
17
|
+
${I}="${t.publicKey}"
|
|
18
|
+
${R}="${t.privateKey}"
|
|
19
|
+
`,J(h,O,"utf-8");for(let T=x.length-1;T>=0;T--){let Y=x[T];if(Y!==void 0&&Y.trim().startsWith(`${I}=`))x.splice(T,1)}x.unshift(`${I}="${B}"`)}let X=g;if(!r.plain&&B)X=L(g,B);for(let t=0;t<x.length;t++){let O=x[t];if(O===void 0)continue;if(O.trim().startsWith(`${f}=`)){x[t]=`${f}="${X}"`,$=!0;break}}if(!$)x.push(`${f}="${X}"`);let H=x.join(`
|
|
20
|
+
`);return J(E,H,"utf-8"),{success:!0,output:`set ${f}${r.plain?"":" with encryption"} (${r.file||".env"})`}}catch(A){return{success:!1,error:`Failed to set: ${A instanceof Error?A.message:"Unknown error"}`}}}function zf(f,g={}){let r=g.cwd||process.cwd(),c=W(r,g.file||".env");if(!G(c))return{success:!1,error:`File not found: ${c}`};try{let E,h=W(r,g.keysFile||".env.keys"),$=((g.file||".env").split("/").pop()||"").replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase(),B=$?`DOTENV_PRIVATE_KEY_${$}`:"DOTENV_PRIVATE_KEY";if(G(h)){let R=j(h,"utf-8"),{parsed:X}=Z(R);E=X[B]}if(!E)E=process.env[B];let w=j(c,"utf-8"),{parsed:_}=Z(w,{privateKey:E});if(f&&!g.all){let R=_[f];if(R===void 0)return{success:!1,error:`Key not found: ${f}`};return{success:!0,output:R}}let D=g.all?{...process.env,..._}:_,I;switch(g.format){case"shell":I=Object.entries(D).map(([R,X])=>`${R}=${X}`).join(" ");break;case"eval":I=Object.entries(D).map(([R,X])=>`${R}="${X}"`).join(`
|
|
21
|
+
`);break;case"json":default:I=g.prettyPrint?JSON.stringify(D,null,2):JSON.stringify(D);break}return{success:!0,output:I}}catch(E){return{success:!1,error:`Failed to get: ${E instanceof Error?E.message:"Unknown error"}`}}}function Zf(f,g={}){let r=g.cwd||process.cwd(),c=W(r,g.keysFile||".env.keys");if(!G(c))return{success:!1,error:`Keys file not found: ${c}`};try{let E=j(c,"utf-8"),{parsed:h}=Z(E),$=((g.file||".env").split("/").pop()||"").replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase(),B=$?`DOTENV_PUBLIC_KEY_${$}`:"DOTENV_PUBLIC_KEY",w=$?`DOTENV_PRIVATE_KEY_${$}`:"DOTENV_PRIVATE_KEY";if(f){let I=h[f];if(!I)return{success:!1,error:`Key not found: ${f}`};return{success:!0,output:I}}let _={[B]:h[B],[w]:h[w]};return{success:!0,output:g.format==="shell"?`${B}=${_[B]} ${w}=${_[w]}`:JSON.stringify(_)}}catch(E){return{success:!1,error:`Failed to get keypair: ${E instanceof Error?E.message:"Unknown error"}`}}}function qf(f={}){let g=xf({file:f.file,keysFile:f.keysFile,stdout:!0,cwd:f.cwd});if(!g.success)return g;let r=f.cwd||process.cwd(),c=W(r,f.file||".env"),E=W(r,f.keysFile||".env.keys");if(g.output===void 0)return{success:!1,error:"Rotation produced no decrypted content"};let h=q(),A=j(c),x=j(E),$=x.toString("utf8").split(`
|
|
22
|
+
`),B=f.file||".env",w=N(B).replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase(),_=w?`DOTENV_PUBLIC_KEY_${w}`:"DOTENV_PUBLIC_KEY",D=w?`DOTENV_PRIVATE_KEY_${w}`:"DOTENV_PRIVATE_KEY",I=!1,R=!1;for(let Y=0;Y<$.length;Y++){let M=$[Y];if(M===void 0)continue;if(M.startsWith(`${_}=`))$[Y]=`${_}="${h.publicKey}"`,I=!0;else if(M.startsWith(`${D}=`))$[Y]=`${D}="${h.privateKey}"`,R=!0}if(!I)$.push(`${_}="${h.publicKey}"`);if(!R)$.push(`${D}="${h.privateKey}"`);let X=`${$.join(`
|
|
23
|
+
`).replace(/\n+$/,"")}
|
|
24
|
+
`,H=a(g.output,h.publicKey,_,f),t=`${process.pid}-${Date.now()}`,O=W(p(c),`.${N(c)}.${t}.rotate`),T=W(p(E),`.${N(E)}.${t}.rotate`);try{if(J(O,H,{encoding:"utf8",mode:384}),J(T,X,{encoding:"utf8",mode:384}),f.stdout)return d(T,E),{success:!0,output:H};d(O,c);try{d(T,E)}catch(Y){throw J(c,A),Y}return{success:!0,output:`\u2714 rotated (${f.file||".env"}) to encrypted:v2`}}catch(Y){if(!G(E))J(E,x,{mode:384});return{success:!1,error:`Failed to rotate without exposing plaintext: ${Y instanceof Error?Y.message:"Unknown error"}`}}finally{l(O,{force:!0}),l(T,{force:!0})}}export{Uf as setEnv,qf as rotateKeypair,Zf as getKeypair,zf as getEnv,Jf as encryptEnv,xf as decryptEnv};
|
package/dist/crypto.d.ts
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
// AES-256-GCM encryption using Node.js crypto
|
|
2
1
|
export declare function aesEncrypt(plaintext: string, key: Buffer): { ciphertext: string, iv: string, authTag: string };
|
|
3
2
|
export declare function aesDecrypt(ciphertext: string, key: Buffer, iv: string, authTag: string): string;
|
|
4
|
-
// secp256k1 key generation
|
|
5
3
|
export declare function generateKeypair(): { publicKey: string, privateKey: string };
|
|
6
|
-
|
|
7
|
-
export declare function
|
|
8
|
-
// Decrypt a value using private key
|
|
4
|
+
export declare function encryptValue(value: string, recipientPublicKey: string): string;
|
|
5
|
+
export declare function isLegacyEncryptedValue(value: string): boolean;
|
|
9
6
|
export declare function decryptValue(encryptedValue: string, privateKey: string): string;
|
|
10
|
-
|
|
7
|
+
export declare function migrateEncryptedValue(encryptedValue: string, oldPrivateKey: string, newPublicKey: string): string;
|
|
11
8
|
export declare function parseEnvFromKey(keyName: string): string;
|
|
12
|
-
// Get appropriate private key for environment
|
|
13
9
|
export declare function getPrivateKey(env?: string): string | undefined;
|
package/dist/crypto.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{createCipheriv as
|
|
2
|
+
import{createCipheriv as x,createDecipheriv as w,createHash as l,createPrivateKey as S,createPublicKey as k,diffieHellman as b,generateKeyPairSync as K,hkdfSync as B,randomBytes as d}from"crypto";var E="x25519-public:",m="x25519-private:",y="encrypted:v2:",v="encrypted:",P=Buffer.from("stacks-env:v2","utf8"),A="Environment decryption failed: authentication or format error";function T(e){if(e.length===32)return e;return l("sha256").update(e).digest()}function Y(e,t){let r=d(16),n=x("aes-256-gcm",T(t),r);return{ciphertext:Buffer.concat([n.update(e,"utf8"),n.final()]).toString("hex"),iv:r.toString("hex"),authTag:n.getAuthTag().toString("hex")}}function R(e,t,r,n){try{let i=w("aes-256-gcm",T(t),Buffer.from(r,"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 s(e){return e.toString("base64url")}function c(e,t,r){if(typeof e!=="string"||e.length>0&&!/^[A-Za-z0-9_-]+$/.test(e))throw Error(`${t} is not canonical base64url`);let n=Buffer.from(e,"base64url");if(s(n)!==e)throw Error(`${t} is not canonical base64url`);if(r!==void 0&&n.length!==r)throw Error(`${t} has an invalid length`);return n}function V(e){if(!e.startsWith(E))throw Error("Legacy or invalid public key. Run buddy env:rotate before creating new ciphertext.");let t=k({key:c(e.slice(E.length),"public key"),format:"der",type:"spki"});if(t.asymmetricKeyType!=="x25519")throw Error("Public key is not X25519");return t}function D(e){if(!e.startsWith(m))throw Error("Private key is not a version 2 X25519 key");let t=S({key:c(e.slice(m.length),"private key"),format:"der",type:"pkcs8"});if(t.asymmetricKeyType!=="x25519")throw Error("Private key is not X25519");return t}function _(e){return Buffer.from(`stacks-env:v2;${e.epk};${e.salt};${e.nonce}`,"utf8")}function H(){let{publicKey:e,privateKey:t}=K("x25519"),r=e.export({format:"der",type:"spki"}),n=t.export({format:"der",type:"pkcs8"});return{publicKey:`${E}${s(r)}`,privateKey:`${m}${s(n)}`}}function I(e,t){let r=V(t),n=K("x25519"),i=s(n.publicKey.export({format:"der",type:"spki"})),o=d(16),f=d(12),g=b({privateKey:n.privateKey,publicKey:r}),h=Buffer.from(B("sha256",g,o,P,32)),u={epk:i,salt:s(o),nonce:s(f)};try{let a=x("aes-256-gcm",h,f);a.setAAD(_(u));let p=Buffer.concat([a.update(e,"utf8"),a.final()]),$={v:2,...u,ciphertext:s(p),tag:s(a.getAuthTag())};return`${y}${s(Buffer.from(JSON.stringify($),"utf8"))}`}finally{g.fill(0),h.fill(0)}}function O(e){let t=c(e.slice(y.length),"envelope").toString("utf8"),r=JSON.parse(t);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 F(e,t){try{let r=O(e),n=D(t),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"),h=c(r.tag,"tag",16),u=b({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(_(r)),p.setAuthTag(h),Buffer.concat([p.update(g),p.final()]).toString("utf8")}finally{u.fill(0),a.fill(0)}}catch{throw Error(A)}}function N(e,t){try{if(!/^[0-9a-f]{64}$/.test(t))throw Error("invalid legacy private key");let r=Buffer.from(e.slice(v.length),"base64");if(r.length<32)throw Error("invalid legacy payload");let n=l("sha256").update(Buffer.from(t,"hex")).digest(),i=l("sha256").update(n).digest();return R(r.subarray(32).toString("hex"),i,r.subarray(0,16).toString("hex"),r.subarray(16,32).toString("hex"))}catch{throw Error(A)}}function L(e){return e.startsWith(v)&&!e.startsWith(y)}function X(e,t){if(!e.startsWith(v))return e;return e.startsWith(y)?F(e,t):N(e,t)}function W(e,t,r){return I(X(e,t),r)}function j(e){let t=e.match(/^DOTENV_PRIVATE_KEY_(.+)$/);return t&&t[1]?t[1].toLowerCase():""}function z(e=""){return e?process.env[`DOTENV_PRIVATE_KEY_${e.toUpperCase()}`]:process.env.DOTENV_PRIVATE_KEY}export{j as parseEnvFromKey,W as migrateEncryptedValue,L as isLegacyEncryptedValue,z as getPrivateKey,H as generateKeypair,I as encryptValue,X as decryptValue,Y as aesEncrypt,R as aesDecrypt};
|
package/dist/index.js
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{createCipheriv as
|
|
3
|
-
`);for(let
|
|
4
|
-
`)}if(
|
|
5
|
-
|
|
6
|
-
${
|
|
7
|
-
|
|
8
|
-
${
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
\u2714 key added to ${
|
|
14
|
-
`),
|
|
15
|
-
`);if(
|
|
16
|
-
`),
|
|
17
|
-
${
|
|
18
|
-
${
|
|
19
|
-
`,
|
|
20
|
-
`);return
|
|
21
|
-
`);break;case"json":default:
|
|
22
|
-
`),
|
|
23
|
-
`)
|
|
24
|
-
`),A
|
|
25
|
-
`)
|
|
2
|
+
import{createCipheriv as e,createDecipheriv as ff,createHash as k,createPrivateKey as xf,createPublicKey as gf,diffieHellman as Bf,generateKeyPairSync as $f,hkdfSync as Af,randomBytes as d}from"crypto";var t="x25519-public:",r="x25519-private:",c="encrypted:v2:",n="encrypted:",_f=Buffer.from("stacks-env:v2","utf8"),Xf="Environment decryption failed: authentication or format error";function Df(f){if(f.length===32)return f;return k("sha256").update(f).digest()}function vf(f,g){let B=d(16),$=e("aes-256-gcm",Df(g),B);return{ciphertext:Buffer.concat([$.update(f,"utf8"),$.final()]).toString("hex"),iv:B.toString("hex"),authTag:$.getAuthTag().toString("hex")}}function Ef(f,g,B,$){try{let A=ff("aes-256-gcm",Df(g),Buffer.from(B,"hex"));return A.setAuthTag(Buffer.from($,"hex")),Buffer.concat([A.update(Buffer.from(f,"hex")),A.final()]).toString("utf8")}catch{throw Error("Decryption failed: authentication or format error")}}function z(f){return f.toString("base64url")}function C(f,g,B){if(typeof f!=="string"||f.length>0&&!/^[A-Za-z0-9_-]+$/.test(f))throw Error(`${g} is not canonical base64url`);let $=Buffer.from(f,"base64url");if(z($)!==f)throw Error(`${g} is not canonical base64url`);if(B!==void 0&&$.length!==B)throw Error(`${g} has an invalid length`);return $}function Uf(f){if(!f.startsWith(t))throw Error("Legacy or invalid public key. Run buddy env:rotate before creating new ciphertext.");let g=gf({key:C(f.slice(t.length),"public key"),format:"der",type:"spki"});if(g.asymmetricKeyType!=="x25519")throw Error("Public key is not X25519");return g}function qf(f){if(!f.startsWith(r))throw Error("Private key is not a version 2 X25519 key");let g=xf({key:C(f.slice(r.length),"private key"),format:"der",type:"pkcs8"});if(g.asymmetricKeyType!=="x25519")throw Error("Private key is not X25519");return g}function Hf(f){return Buffer.from(`stacks-env:v2;${f.epk};${f.salt};${f.nonce}`,"utf8")}function F(){let{publicKey:f,privateKey:g}=$f("x25519"),B=f.export({format:"der",type:"spki"}),$=g.export({format:"der",type:"pkcs8"});return{publicKey:`${t}${z(B)}`,privateKey:`${r}${z($)}`}}function P(f,g){let B=Uf(g),$=$f("x25519"),A=z($.publicKey.export({format:"der",type:"spki"})),_=d(16),R=d(12),O=Bf({privateKey:$.privateKey,publicKey:B}),H=Buffer.from(Af("sha256",O,_,_f,32)),X={epk:A,salt:z(_),nonce:z(R)};try{let D=e("aes-256-gcm",H,R);D.setAAD(Hf(X));let T=Buffer.concat([D.update(f,"utf8"),D.final()]),G={v:2,...X,ciphertext:z(T),tag:z(D.getAuthTag())};return`${c}${z(Buffer.from(JSON.stringify(G),"utf8"))}`}finally{O.fill(0),H.fill(0)}}function Lf(f){let g=C(f.slice(c.length),"envelope").toString("utf8"),B=JSON.parse(g);if(!B||typeof B!=="object"||Array.isArray(B))throw Error("envelope is not an object");let $=Object.keys(B).sort(),A=["ciphertext","epk","nonce","salt","tag","v"];if($.length!==A.length||$.some((_,R)=>_!==A[R]))throw Error("envelope fields are invalid");if(B.v!==2)throw Error("envelope version is unsupported");for(let _ of["epk","salt","nonce","ciphertext","tag"])if(typeof B[_]!=="string")throw Error(`${_} is invalid`);return B}function Mf(f,g){try{let B=Lf(f),$=qf(g),A=gf({key:C(B.epk,"ephemeral public key"),format:"der",type:"spki"});if(A.asymmetricKeyType!=="x25519")throw Error("ephemeral key is not X25519");let _=C(B.salt,"salt",16),R=C(B.nonce,"nonce",12),O=C(B.ciphertext,"ciphertext"),H=C(B.tag,"tag",16),X=Bf({privateKey:$,publicKey:A}),D=Buffer.from(Af("sha256",X,_,_f,32));try{let T=ff("aes-256-gcm",D,R);return T.setAAD(Hf(B)),T.setAuthTag(H),Buffer.concat([T.update(O),T.final()]).toString("utf8")}finally{X.fill(0),D.fill(0)}}catch{throw Error(Xf)}}function zf(f,g){try{if(!/^[0-9a-f]{64}$/.test(g))throw Error("invalid legacy private key");let B=Buffer.from(f.slice(n.length),"base64");if(B.length<32)throw Error("invalid legacy payload");let $=k("sha256").update(Buffer.from(g,"hex")).digest(),A=k("sha256").update($).digest();return Ef(B.subarray(32).toString("hex"),A,B.subarray(0,16).toString("hex"),B.subarray(16,32).toString("hex"))}catch{throw Error(Xf)}}function of(f){return f.startsWith(n)&&!f.startsWith(c)}function h(f,g){if(!f.startsWith(n))return f;return f.startsWith(c)?Mf(f,g):zf(f,g)}function pf(f,g,B){return P(h(f,g),B)}function lf(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}function U(f,g={}){let B={},$=[],A=[],_=f.split(`
|
|
3
|
+
`);for(let R of _){let O=R.trim();if(!O||O.startsWith("#"))continue;if(O.startsWith("DOTENV_PUBLIC_KEY=")){let T=O.match(/^DOTENV_PUBLIC_KEY=["']?([^"'\n]+)["']?/);if(T&&T[1]!==void 0)B.DOTENV_PUBLIC_KEY=T[1];continue}let H=O.match(/^([^=]+)=(.*)$/);if(!H||H[1]===void 0||H[2]===void 0)continue;let X=H[1].trim(),D=H[2].trim();if(D.startsWith('"')&&D.endsWith('"')||D.startsWith("'")&&D.endsWith("'")){if(D=D.slice(1,-1),D.includes("\\n"))D=D.replace(/\\n/g,`
|
|
4
|
+
`)}if(D.startsWith("encrypted:")||D.startsWith("enc:")){if(!g.privateKey){A.push(X);continue}try{let T=D.startsWith("enc:")?`encrypted:${D.slice(4)}`:D;D=h(T,g.privateKey)}catch(T){$.push(`Failed to decrypt ${X}: ${T instanceof Error?T.message:"Unknown error"}`)}}D=Vf(D,{...g.processEnv||process.env,...B}),D=bf(D),B[X]=D}return{parsed:B,errors:$,skippedEncrypted:A}}function Vf(f,g){return f.replace(/\$\{([^}]+)\}/g,(B,$)=>{let A=$.match(/^([^:\-+]+)(:-|-)(.+)$/);if(A){let[,R,O,H]=A,X=g[R];if(O===":-")return X||H;else return X!==void 0?X:H}let _=$.match(/^([^:\-+]+)(:?\+)(.+)$/);if(_){let[,R,O,H]=_,X=g[R];if(O===":+")return X?H:"";else return X!==void 0?H:""}return g[$]||""})}var Cf=new Set(["date","hostname","whoami","uname","pwd","echo","printf","cat","basename","dirname"]);function bf(f){return f.replace(/\$\(([^)]+)\)/g,(g,B)=>{try{let $=B.trim().split(/\s+/),A=$[0];if(!A||!Cf.has(A))return console.warn(`[env] Blocked command substitution for disallowed command: ${A}`),"";let _=Bun.spawnSync($,{stdout:"pipe",stderr:"pipe"});if(_.exitCode===0)return new TextDecoder().decode(_.stdout).trim();console.warn(`[env] Command substitution failed (exit code ${_.exitCode}): ${A}`)}catch($){console.warn(`[env] Command substitution error: ${$ instanceof Error?$.message:String($)}`)}return""})}async function ef(f,g={}){let B={},$=[],A=[];for(let _ of f)try{let R=Bun.file(_);if(!R.size)continue;let O=await R.text(),{parsed:H,errors:X,skippedEncrypted:D}=U(O,g);$.push(...X),A.push(...D);for(let[T,G]of Object.entries(H)){if(!g.overload&&B[T]!==void 0)continue;B[T]=G}}catch(R){if(R?.code==="ENOENT")continue;$.push(`Failed to read ${_}: ${R instanceof Error?R.message:String(R)}`)}return{parsed:B,errors:$,skippedEncrypted:A}}import{existsSync as m}from"fs";import{readFileSync as y}from"fs";import{resolve as u}from"path";function S(f){return typeof f==="string"&&(f.startsWith("encrypted:")||f.startsWith("enc:"))}function b(f){if(!f||S(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 v=null,o;function hf(f,g,B=".env.keys"){let $=u(g,B);if(!m($))return;try{let{parsed:A}=U(y($,"utf-8"));if(f){let _=A[`DOTENV_PRIVATE_KEY_${f.toUpperCase()}`];if(_)return _}return A.DOTENV_PRIVATE_KEY}catch{return}}function Ff(f={}){let g=b(f.env)||b(process.env.APP_ENV)||b("development")||b(process.env.DOTENV_ENV)||"development",B=f.cwd||process.cwd();if(v===g)return o;let $=i(g);if(!$)$=process.env.DOTENV_PRIVATE_KEY;if(!$)$=hf(g,B);return v=g,o=$,$}function Dg(){v=null,o=void 0}function Of(f,g={}){if(!S(f))return f;let B=Ff(g);if(!B)return;try{let $=f.startsWith("enc:")?`encrypted:${f.slice(4)}`:f;return h($,B)}catch{return}}function Rf(f={}){let{path:g=[".env"],overload:B=!1,env:$,privateKey:A,keysFile:_=".env.keys",quiet:R=!1,cwd:O=process.cwd()}=f,H=Array.isArray(g)?g:[g],X=[],D=0,T=new Set(Object.entries(process.env).filter(([,W])=>W!==void 0&&!S(W)).map(([W])=>W)),G=A;if(!G&&_){let W=u(O,_);if(m(W))try{let Y=y(W,"utf-8"),{parsed:w}=U(Y);if($){let x=`DOTENV_PRIVATE_KEY_${$.toUpperCase()}`;G=w[x]}else G=w.DOTENV_PRIVATE_KEY}catch(Y){X.push(`Failed to load keys file: ${Y instanceof Error?Y.message:"Unknown error"}`)}}if(!G)G=i($||"");for(let W of H){let Y=u(O,W);if(!m(Y)){if(!R)X.push(`File not found: ${Y}`);continue}try{let w=y(Y,"utf-8"),{parsed:x,errors:I,skippedEncrypted:Z}=U(w,{privateKey:G,processEnv:process.env});if(X.push(...I),Z.length>0){for(let K of Z)if(S(process.env[K]))delete process.env[K];let Q=$?`DOTENV_PRIVATE_KEY_${$.toUpperCase()}`:"DOTENV_PRIVATE_KEY",V=Z.slice(0,5).join(", "),N=Z.length>5?`, \u2026 +${Z.length-5} more`:"";console.warn(`[env] warning: skipped ${Z.length} encrypted value(s) in ${W} (${Q} not set; defaults apply): ${V}${N}`)}let J=0;for(let[Q,V]of Object.entries(x)){if(Q==="DOTENV_PUBLIC_KEY")continue;let N=process.env[Q],K=S(N)&&V!==N;if(B||!T.has(Q)||K)process.env[Q]=V,D++,J++}if(!R&&!process.env.__ENV_LOADED__)console.error(`[env] loaded ${J}/${Object.keys(x).length} variables from ${W}`),process.env.__ENV_LOADED__="1"}catch(w){X.push(`Failed to load ${Y}: ${w instanceof Error?w.message:"Unknown error"}`)}}return{loaded:D,errors:X}}function Sf(f={}){return{name:"env-plugin",setup(g){Rf(f)}}}function Hg(f={}){let g=b(f.env)||b("development")||b(process.env.DOTENV_ENV)||b(process.env.APP_ENV)||"development",B=f.cwd||process.cwd(),$=[],A=(O)=>{if(!$.includes(O)&&m(u(B,O)))$.push(O)};A(".env"),A(".env.local");let _=`.env.${g}`,R=`.env.${g}.local`;return A(_),A(R),Rf({...f,path:$,env:g})}var Rg=Sf;import{existsSync as L,readFileSync as q,renameSync as p,rmSync as Tf,writeFileSync as M}from"fs";import{basename as l,dirname as Wf,resolve as E}from"path";function Yf(f,g,B,$){let A=["#/-------------------[DOTENV_PUBLIC_KEY]--------------------/","#/ versioned X25519 encryption for .env files /","#/ [how it works](https://stacksjs.com/encryption) /","#/----------------------------------------------------------/",`${B}="${g}"`,""];for(let _ of f.split(`
|
|
5
|
+
`)){let R=_.trim();if(!R||R.startsWith("#")){A.push(_);continue}if(R.startsWith("DOTENV_PUBLIC_KEY"))continue;let O=R.match(/^([^=]+)=(.*)$/);if(!O||O[1]===void 0||O[2]===void 0){A.push(_);continue}let H=O[1].trim(),X=O[2].trim();if(X.startsWith('"')&&X.endsWith('"')||X.startsWith("'")&&X.endsWith("'"))X=X.slice(1,-1);if((!$.key||H.includes($.key))&&(!$.excludeKey||!H.includes($.excludeKey))&&!X.startsWith("encrypted:"))X=P(X,g);A.push(`${H}="${X}"`)}return A.join(`
|
|
6
|
+
`)}function Gg(f={}){let g=f.cwd||process.cwd(),B=E(g,f.file||".env"),$=E(g,f.keysFile||".env.keys");if(!L(B))return{success:!1,error:`File not found: ${B}`};try{let A,_;if(L($)){let D=q($,"utf-8"),{parsed:T}=U(D),Y=((f.file||".env").split("/").pop()||"").replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase(),w=Y?`DOTENV_PUBLIC_KEY_${Y}`:"DOTENV_PUBLIC_KEY",x=Y?`DOTENV_PRIVATE_KEY_${Y}`:"DOTENV_PRIVATE_KEY";if(A=T[w]||"",_=T[x]||"",!A||!_){let I=F();A=I.publicKey,_=I.privateKey;let Z=`
|
|
7
|
+
${w}="${A}"
|
|
8
|
+
${x}="${_}"
|
|
9
|
+
`;M($,D+Z,"utf-8")}}else{let D=F();A=D.publicKey,_=D.privateKey;let W=((f.file||".env").split("/").pop()||"").replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase(),Y=W?`DOTENV_PUBLIC_KEY_${W}`:"DOTENV_PUBLIC_KEY",w=W?`DOTENV_PRIVATE_KEY_${W}`:"DOTENV_PRIVATE_KEY",x=`# .env.keys - Keep this file secure and never commit to source control
|
|
10
|
+
${Y}="${A}"
|
|
11
|
+
${w}="${_}"
|
|
12
|
+
`;M($,x,"utf-8")}let R=q(B,"utf-8"),O=f.file?f.file.replace(/^\.env\./,"").toUpperCase():"",H=O?`DOTENV_PUBLIC_KEY_${O}`:"DOTENV_PUBLIC_KEY",X=Yf(R,A,H,f);if(f.stdout)return{success:!0,output:X};return M(B,X,"utf-8"),{success:!0,output:`\u2714 encrypted (${f.file||".env"})
|
|
13
|
+
\u2714 key added to ${f.keysFile||".env.keys"}`}}catch(A){return{success:!1,error:`Failed to encrypt: ${A instanceof Error?A.message:"Unknown error"}`}}}function Nf(f={}){let g=f.cwd||process.cwd(),B=E(g,f.file||".env"),$=E(g,f.keysFile||".env.keys");if(!L(B))return{success:!1,error:`File not found: ${B}`};if(!L($))return{success:!1,error:`Keys file not found: ${$}`};try{let A=q($,"utf-8"),{parsed:_}=U(A),H=((f.file||".env").split("/").pop()||"").replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase(),X=H?`DOTENV_PRIVATE_KEY_${H}`:"DOTENV_PRIVATE_KEY",D=_[X];if(!D)return{success:!1,error:`Private key not found: ${X}`};let G=q(B,"utf-8").split(`
|
|
14
|
+
`),W=[];for(let w of G){let x=w.trim();if(!x||x.startsWith("#")){W.push(w);continue}if(x.startsWith("DOTENV_PUBLIC_KEY"))continue;let I=x.match(/^([^=]+)=(.*)$/);if(!I||I[1]===void 0||I[2]===void 0){W.push(w);continue}let Z=I[1].trim(),J=I[2].trim();if(J.startsWith('"')&&J.endsWith('"')||J.startsWith("'")&&J.endsWith("'"))J=J.slice(1,-1);let Q=J.startsWith("encrypted:");if(f.key&&!Z.includes(f.key))Q=!1;if(Q)J=h(J,D);W.push(`${Z}="${J}"`)}let Y=W.join(`
|
|
15
|
+
`);if(f.stdout)return{success:!0,output:Y};return M(B,Y,"utf-8"),{success:!0,output:`\u2714 decrypted (${f.file||".env"})`}}catch(A){return{success:!1,error:`Failed to decrypt: ${A instanceof Error?A.message:"Unknown error"}`}}}function Jg(f,g,B={}){let $=B.cwd||process.cwd(),A=E($,B.file||".env"),_=E($,B.keysFile||".env.keys");try{let R="";if(L(A))R=q(A,"utf-8");let O=R.split(`
|
|
16
|
+
`),H=!1,X,G=((B.file||".env").split("/").pop()||"").replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase(),W=G?`DOTENV_PUBLIC_KEY_${G}`:"DOTENV_PUBLIC_KEY",Y=G?`DOTENV_PRIVATE_KEY_${G}`:"DOTENV_PRIVATE_KEY";for(let I of O){let Z=I.trim();if(Z.startsWith(`${W}=`)){let J=Z.match(/^[^=]+=["']?([^"'\n]+)["']?/);if(J)X=J[1];break}}if(X){let I=!1;if(L(_)){let Z=q(_,"utf-8"),{parsed:J}=U(Z);I=Boolean(J[Y])}if(!I)X=void 0}if(!B.plain&&!X){let I=F();X=I.publicKey;let Z="";if(L(_))Z=q(_,"utf-8");Z+=`
|
|
17
|
+
${W}="${I.publicKey}"
|
|
18
|
+
${Y}="${I.privateKey}"
|
|
19
|
+
`,M(_,Z,"utf-8");for(let J=O.length-1;J>=0;J--){let Q=O[J];if(Q!==void 0&&Q.trim().startsWith(`${W}=`))O.splice(J,1)}O.unshift(`${W}="${X}"`)}let w=g;if(!B.plain&&X)w=P(g,X);for(let I=0;I<O.length;I++){let Z=O[I];if(Z===void 0)continue;if(Z.trim().startsWith(`${f}=`)){O[I]=`${f}="${w}"`,H=!0;break}}if(!H)O.push(`${f}="${w}"`);let x=O.join(`
|
|
20
|
+
`);return M(A,x,"utf-8"),{success:!0,output:`set ${f}${B.plain?"":" with encryption"} (${B.file||".env"})`}}catch(R){return{success:!1,error:`Failed to set: ${R instanceof Error?R.message:"Unknown error"}`}}}function wg(f,g={}){let B=g.cwd||process.cwd(),$=E(B,g.file||".env");if(!L($))return{success:!1,error:`File not found: ${$}`};try{let A,_=E(B,g.keysFile||".env.keys"),H=((g.file||".env").split("/").pop()||"").replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase(),X=H?`DOTENV_PRIVATE_KEY_${H}`:"DOTENV_PRIVATE_KEY";if(L(_)){let Y=q(_,"utf-8"),{parsed:w}=U(Y);A=w[X]}if(!A)A=process.env[X];let D=q($,"utf-8"),{parsed:T}=U(D,{privateKey:A});if(f&&!g.all){let Y=T[f];if(Y===void 0)return{success:!1,error:`Key not found: ${f}`};return{success:!0,output:Y}}let G=g.all?{...process.env,...T}:T,W;switch(g.format){case"shell":W=Object.entries(G).map(([Y,w])=>`${Y}=${w}`).join(" ");break;case"eval":W=Object.entries(G).map(([Y,w])=>`${Y}="${w}"`).join(`
|
|
21
|
+
`);break;case"json":default:W=g.prettyPrint?JSON.stringify(G,null,2):JSON.stringify(G);break}return{success:!0,output:W}}catch(A){return{success:!1,error:`Failed to get: ${A instanceof Error?A.message:"Unknown error"}`}}}function Ig(f,g={}){let B=g.cwd||process.cwd(),$=E(B,g.keysFile||".env.keys");if(!L($))return{success:!1,error:`Keys file not found: ${$}`};try{let A=q($,"utf-8"),{parsed:_}=U(A),H=((g.file||".env").split("/").pop()||"").replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase(),X=H?`DOTENV_PUBLIC_KEY_${H}`:"DOTENV_PUBLIC_KEY",D=H?`DOTENV_PRIVATE_KEY_${H}`:"DOTENV_PRIVATE_KEY";if(f){let W=_[f];if(!W)return{success:!1,error:`Key not found: ${f}`};return{success:!0,output:W}}let T={[X]:_[X],[D]:_[D]};return{success:!0,output:g.format==="shell"?`${X}=${T[X]} ${D}=${T[D]}`:JSON.stringify(T)}}catch(A){return{success:!1,error:`Failed to get keypair: ${A instanceof Error?A.message:"Unknown error"}`}}}function Zg(f={}){let g=Nf({file:f.file,keysFile:f.keysFile,stdout:!0,cwd:f.cwd});if(!g.success)return g;let B=f.cwd||process.cwd(),$=E(B,f.file||".env"),A=E(B,f.keysFile||".env.keys");if(g.output===void 0)return{success:!1,error:"Rotation produced no decrypted content"};let _=F(),R=q($),O=q(A),H=O.toString("utf8").split(`
|
|
22
|
+
`),X=f.file||".env",D=l(X).replace(/^\.env\./,"").replace(/^\.env$/,"").toUpperCase(),T=D?`DOTENV_PUBLIC_KEY_${D}`:"DOTENV_PUBLIC_KEY",G=D?`DOTENV_PRIVATE_KEY_${D}`:"DOTENV_PRIVATE_KEY",W=!1,Y=!1;for(let Q=0;Q<H.length;Q++){let V=H[Q];if(V===void 0)continue;if(V.startsWith(`${T}=`))H[Q]=`${T}="${_.publicKey}"`,W=!0;else if(V.startsWith(`${G}=`))H[Q]=`${G}="${_.privateKey}"`,Y=!0}if(!W)H.push(`${T}="${_.publicKey}"`);if(!Y)H.push(`${G}="${_.privateKey}"`);let w=`${H.join(`
|
|
23
|
+
`).replace(/\n+$/,"")}
|
|
24
|
+
`,x=Yf(g.output,_.publicKey,T,f),I=`${process.pid}-${Date.now()}`,Z=E(Wf($),`.${l($)}.${I}.rotate`),J=E(Wf(A),`.${l(A)}.${I}.rotate`);try{if(M(Z,x,{encoding:"utf8",mode:384}),M(J,w,{encoding:"utf8",mode:384}),f.stdout)return p(J,A),{success:!0,output:x};p(Z,$);try{p(J,A)}catch(Q){throw M($,R),Q}return{success:!0,output:`\u2714 rotated (${f.file||".env"}) to encrypted:v2`}}catch(Q){if(!L(A))M(A,O,{mode:384});return{success:!1,error:`Failed to rotate without exposing plaintext: ${Q instanceof Error?Q.message:"Unknown error"}`}}finally{Tf(Z,{force:!0}),Tf(J,{force:!0})}}import j from"process";import{platform as Kf}from"os";var jf=typeof Bun<"u",Gf=typeof j<"u"&&j.versions?.node!==void 0,cf=jf?"bun":Gf?"node":"unknown",Ug={name:cf,version:jf?Bun.version:Gf?j.version:void 0},a=Kf(),qg=a==="win32",Lg=a==="darwin",Mg=a==="linux",Jf=Boolean(j.stdout?.isTTY),zg=typeof globalThis.window<"u",Pf=Boolean(j.env.CI||j.env.CONTINUOUS_INTEGRATION||j.env.BUILD_NUMBER||j.env.RUN_ID),Vg=Boolean(j.env.DEBUG||j.env.VERBOSE||j.argv.includes("--debug")||j.argv.includes("--verbose")),mf=Pf||!Jf,Cg=Boolean(!mf&&(Jf||j.env.COLORTERM||j.env.FORCE_COLOR||j.env.TERM&&j.env.TERM!=="dumb")),s={github:{name:"GitHub Actions",detected:Boolean(j.env.GITHUB_ACTIONS)},gitlab:{name:"GitLab CI",detected:Boolean(j.env.GITLAB_CI)},circle:{name:"CircleCI",detected:Boolean(j.env.CIRCLECI)},travis:{name:"Travis CI",detected:Boolean(j.env.TRAVIS)},jenkins:{name:"Jenkins",detected:Boolean(j.env.JENKINS_URL)},vercel:{name:"Vercel",detected:Boolean(j.env.VERCEL)},netlify:{name:"Netlify",detected:Boolean(j.env.NETLIFY)},heroku:{name:"Heroku",detected:Boolean(j.env.DYNO)},aws:{name:"AWS",detected:Boolean(j.env.AWS_REGION||j.env.AWS_LAMBDA_FUNCTION_NAME)},azure:{name:"Azure",detected:Boolean(j.env.AZURE_HTTP_USER_AGENT)},cloudflare:{name:"Cloudflare",detected:Boolean(j.env.CF_PAGES)},railway:{name:"Railway",detected:Boolean(j.env.RAILWAY_ENVIRONMENT)},render:{name:"Render",detected:Boolean(j.env.RENDER)}},uf=Object.keys(s).find((f)=>{let g=s[f];return g!==void 0&&g.detected})||"unknown",bg=s[uf]||{name:"Unknown",detected:!1};import kf from"process";import{projectPath as df}from"@stacksjs/path";import If from"fs";var wf={APP_ENV:["local","dev","development","staging","prod","production"],DB_CONNECTION:["mysql","sqlite","postgres","dynamodb"],DB_MIGRATE_FRESH:["allow","confirm","disabled"],MAIL_MAILER:["smtp","mailgun","ses","log","sendgrid","mailtrap"],SEARCH_ENGINE_DRIVER:["opensearch","meilisearch","algolia","typesense"],FRONTEND_APP_ENV:["development","staging","production"]};var Zf=new Set;function tf(f){if(Zf.has(f))return;Zf.add(f),console.warn(`[env] warning: "${f}" is encrypted but no usable private key was found to decrypt it; falling back to its default. Set DOTENV_PRIVATE_KEY_<ENV> or ship .env.keys.`)}var rf={get:(f,g)=>{let B=f[g];if(typeof B==="string"&&(B.startsWith("encrypted:")||B.startsWith("enc:"))){let A=Of(B);if(A===void 0){tf(g),delete f[g];return}f[g]=A,B=A}let $=["_PORT","_TIMEOUT","_TTL","_SIZE","_LIMIT","_MAX","_MIN","_INTERVAL","_RETRIES","_CONCURRENCY","_WORKERS","_CONNECTIONS"];if(typeof B==="string"&&/^\d+$/.test(B)&&!B.startsWith("0")&&$.some((A)=>g.endsWith(A)))return Number(B);if(typeof B==="string"){let A=B.toLowerCase();if(A==="true")return!0;if(A==="false")return!1}return B}};function nf(){return typeof Bun<"u"?Bun.env:kf.env}var Qf=new Proxy(nf(),rf);function mg(f,g,B){let $=B?.path||df(".env"),_=If.readFileSync($,"utf-8").split(`
|
|
25
|
+
`),R=_.findIndex((X)=>X.startsWith(`${f}=`)),H=/[\s"'#$\\]/.test(g)?`"${g.replace(/"/g,"\\\"")}"`:g;if(R!==-1)_[R]=`${f}=${H}`;else _.push(`${f}=${H}`);If.writeFileSync($,_.join(`
|
|
26
|
+
`))}function ug(f=Qf){let g=[];for(let[B,$]of Object.entries(wf)){let A=f[B];if(A!==void 0&&A!==""&&!$.includes(String(A)))g.push(`${B}="${A}" is not valid. Allowed values: ${$.join(", ")}`)}return g}function kg(f,g=Qf){let B=[];for(let $ of f){let A=g[$];if(A===void 0||A===""||A===null)B.push($)}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{mg as writeEnv,ug as validateEnv,Jg as setEnv,Ug as runtimeInfo,cf as runtime,Zg as rotateKeypair,Ff as resolvePrivateKey,Dg as resetPrivateKeyCache,kg as requireEnv,bg as providerInfo,uf as provider,nf as process,a as platform,lf as parseEnvFromKey,U as parse,pf as migrateEncryptedValue,ef as loadEnvFiles,Rf as loadEnv,qg as isWindows,Gf as isNode,mf as isMinimal,Lg as isMacOS,Mg as isLinux,of as isLegacyEncryptedValue,Vg as isDebug,Cg as isColorSupported,Pf as isCI,jf as isBun,zg as hasWindow,Jf as hasTTY,i as getPrivateKey,Ig as getKeypair,wg as getEnv,F as generateKeypair,Sf as envPlugin,wf as envEnum,Qf as env,P as encryptValue,Gg as encryptEnv,h as decryptValue,Of as decryptEnvValue,Nf as decryptEnv,Hg as autoLoadEnv,vf as aesEncrypt,Ef as aesDecrypt};
|
package/dist/parser.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{createCipheriv as
|
|
3
|
-
`);for(let
|
|
4
|
-
`)}if(
|
|
2
|
+
import{createCipheriv as b,createDecipheriv as B,createHash as d,createPrivateKey as I,createPublicKey as P,diffieHellman as T,generateKeyPairSync as $,hkdfSync as A,randomBytes as w}from"crypto";var m="x25519-public:",x="x25519-private:",u="encrypted:v2:",K="encrypted:",R=Buffer.from("stacks-env:v2","utf8"),_="Environment decryption failed: authentication or format error";function S(t){if(t.length===32)return t;return d("sha256").update(t).digest()}function V(t,r){let e=w(16),n=b("aes-256-gcm",S(r),e);return{ciphertext:Buffer.concat([n.update(t,"utf8"),n.final()]).toString("hex"),iv:e.toString("hex"),authTag:n.getAuthTag().toString("hex")}}function F(t,r,e,n){try{let o=B("aes-256-gcm",S(r),Buffer.from(e,"hex"));return o.setAuthTag(Buffer.from(n,"hex")),Buffer.concat([o.update(Buffer.from(t,"hex")),o.final()]).toString("utf8")}catch{throw Error("Decryption failed: authentication or format error")}}function y(t){return t.toString("base64url")}function a(t,r,e){if(typeof t!=="string"||t.length>0&&!/^[A-Za-z0-9_-]+$/.test(t))throw Error(`${r} is not canonical base64url`);let n=Buffer.from(t,"base64url");if(y(n)!==t)throw Error(`${r} is not canonical base64url`);if(e!==void 0&&n.length!==e)throw Error(`${r} has an invalid length`);return n}function X(t){if(!t.startsWith(m))throw Error("Legacy or invalid public key. Run buddy env:rotate before creating new ciphertext.");let r=P({key:a(t.slice(m.length),"public key"),format:"der",type:"spki"});if(r.asymmetricKeyType!=="x25519")throw Error("Public key is not X25519");return r}function O(t){if(!t.startsWith(x))throw Error("Private key is not a version 2 X25519 key");let r=I({key:a(t.slice(x.length),"private key"),format:"der",type:"pkcs8"});if(r.asymmetricKeyType!=="x25519")throw Error("Private key is not X25519");return r}function D(t){return Buffer.from(`stacks-env:v2;${t.epk};${t.salt};${t.nonce}`,"utf8")}function G(){let{publicKey:t,privateKey:r}=$("x25519"),e=t.export({format:"der",type:"spki"}),n=r.export({format:"der",type:"pkcs8"});return{publicKey:`${m}${y(e)}`,privateKey:`${x}${y(n)}`}}function v(t,r){let e=X(r),n=$("x25519"),o=y(n.publicKey.export({format:"der",type:"spki"})),c=w(16),s=w(12),h=T({privateKey:n.privateKey,publicKey:e}),f=Buffer.from(A("sha256",h,c,R,32)),g={epk:o,salt:y(c),nonce:y(s)};try{let i=b("aes-256-gcm",f,s);i.setAAD(D(g));let p=Buffer.concat([i.update(t,"utf8"),i.final()]),E={v:2,...g,ciphertext:y(p),tag:y(i.getAuthTag())};return`${u}${y(Buffer.from(JSON.stringify(E),"utf8"))}`}finally{h.fill(0),f.fill(0)}}function Y(t){let r=a(t.slice(u.length),"envelope").toString("utf8"),e=JSON.parse(r);if(!e||typeof e!=="object"||Array.isArray(e))throw Error("envelope is not an object");let n=Object.keys(e).sort(),o=["ciphertext","epk","nonce","salt","tag","v"];if(n.length!==o.length||n.some((c,s)=>c!==o[s]))throw Error("envelope fields are invalid");if(e.v!==2)throw Error("envelope version is unsupported");for(let c of["epk","salt","nonce","ciphertext","tag"])if(typeof e[c]!=="string")throw Error(`${c} is invalid`);return e}function C(t,r){try{let e=Y(t),n=O(r),o=P({key:a(e.epk,"ephemeral public key"),format:"der",type:"spki"});if(o.asymmetricKeyType!=="x25519")throw Error("ephemeral key is not X25519");let c=a(e.salt,"salt",16),s=a(e.nonce,"nonce",12),h=a(e.ciphertext,"ciphertext"),f=a(e.tag,"tag",16),g=T({privateKey:n,publicKey:o}),i=Buffer.from(A("sha256",g,c,R,32));try{let p=B("aes-256-gcm",i,s);return p.setAAD(D(e)),p.setAuthTag(f),Buffer.concat([p.update(h),p.final()]).toString("utf8")}finally{g.fill(0),i.fill(0)}}catch{throw Error(_)}}function H(t,r){try{if(!/^[0-9a-f]{64}$/.test(r))throw Error("invalid legacy private key");let e=Buffer.from(t.slice(K.length),"base64");if(e.length<32)throw Error("invalid legacy payload");let n=d("sha256").update(Buffer.from(r,"hex")).digest(),o=d("sha256").update(n).digest();return F(e.subarray(32).toString("hex"),o,e.subarray(0,16).toString("hex"),e.subarray(16,32).toString("hex"))}catch{throw Error(_)}}function J(t){return t.startsWith(K)&&!t.startsWith(u)}function k(t,r){if(!t.startsWith(K))return t;return t.startsWith(u)?C(t,r):H(t,r)}function U(t,r,e){return v(k(t,r),e)}function z(t){let r=t.match(/^DOTENV_PRIVATE_KEY_(.+)$/);return r&&r[1]?r[1].toLowerCase():""}function Z(t=""){return t?process.env[`DOTENV_PRIVATE_KEY_${t.toUpperCase()}`]:process.env.DOTENV_PRIVATE_KEY}function N(t,r={}){let e={},n=[],o=[],c=t.split(`
|
|
3
|
+
`);for(let s of c){let h=s.trim();if(!h||h.startsWith("#"))continue;if(h.startsWith("DOTENV_PUBLIC_KEY=")){let p=h.match(/^DOTENV_PUBLIC_KEY=["']?([^"'\n]+)["']?/);if(p&&p[1]!==void 0)e.DOTENV_PUBLIC_KEY=p[1];continue}let f=h.match(/^([^=]+)=(.*)$/);if(!f||f[1]===void 0||f[2]===void 0)continue;let g=f[1].trim(),i=f[2].trim();if(i.startsWith('"')&&i.endsWith('"')||i.startsWith("'")&&i.endsWith("'")){if(i=i.slice(1,-1),i.includes("\\n"))i=i.replace(/\\n/g,`
|
|
4
|
+
`)}if(i.startsWith("encrypted:")||i.startsWith("enc:")){if(!r.privateKey){o.push(g);continue}try{let p=i.startsWith("enc:")?`encrypted:${i.slice(4)}`:i;i=k(p,r.privateKey)}catch(p){n.push(`Failed to decrypt ${g}: ${p instanceof Error?p.message:"Unknown error"}`)}}i=W(i,{...r.processEnv||process.env,...e}),i=l(i),e[g]=i}return{parsed:e,errors:n,skippedEncrypted:o}}function W(t,r){return t.replace(/\$\{([^}]+)\}/g,(e,n)=>{let o=n.match(/^([^:\-+]+)(:-|-)(.+)$/);if(o){let[,s,h,f]=o,g=r[s];if(h===":-")return g||f;else return g!==void 0?g:f}let c=n.match(/^([^:\-+]+)(:?\+)(.+)$/);if(c){let[,s,h,f]=c,g=r[s];if(h===":+")return g?f:"";else return g!==void 0?f:""}return r[n]||""})}var j=new Set(["date","hostname","whoami","uname","pwd","echo","printf","cat","basename","dirname"]);function l(t){return t.replace(/\$\(([^)]+)\)/g,(r,e)=>{try{let n=e.trim().split(/\s+/),o=n[0];if(!o||!j.has(o))return console.warn(`[env] Blocked command substitution for disallowed command: ${o}`),"";let c=Bun.spawnSync(n,{stdout:"pipe",stderr:"pipe"});if(c.exitCode===0)return new TextDecoder().decode(c.stdout).trim();console.warn(`[env] Command substitution failed (exit code ${c.exitCode}): ${o}`)}catch(n){console.warn(`[env] Command substitution error: ${n instanceof Error?n.message:String(n)}`)}return""})}async function M(t,r={}){let e={},n=[],o=[];for(let c of t)try{let s=Bun.file(c);if(!s.size)continue;let h=await s.text(),{parsed:f,errors:g,skippedEncrypted:i}=N(h,r);n.push(...g),o.push(...i);for(let[p,E]of Object.entries(f)){if(!r.overload&&e[p]!==void 0)continue;e[p]=E}}catch(s){if(s?.code==="ENOENT")continue;n.push(`Failed to read ${c}: ${s instanceof Error?s.message:String(s)}`)}return{parsed:e,errors:n,skippedEncrypted:o}}export{N as parse,M as loadEnvFiles};
|
package/dist/plugin.d.ts
CHANGED
|
@@ -1,4 +1,24 @@
|
|
|
1
1
|
import type { BunPlugin } from 'bun';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the dotenvx private key for the active environment, checking
|
|
4
|
+
* process.env (`DOTENV_PRIVATE_KEY_<ENV>` then `DOTENV_PRIVATE_KEY`) and
|
|
5
|
+
* finally a local `.env.keys` file. The result — key or `undefined` — is
|
|
6
|
+
* cached per environment so the env proxy can call this on every encrypted
|
|
7
|
+
* read without repeatedly touching disk.
|
|
8
|
+
*/
|
|
9
|
+
export declare function resolvePrivateKey(options?: { env?: string, cwd?: string }): string | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Clear the cached private key. Needed after key rotation and between tests
|
|
12
|
+
* that swap `DOTENV_PRIVATE_KEY*` or `APP_ENV`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function resetPrivateKeyCache(): void;
|
|
15
|
+
/**
|
|
16
|
+
* Decrypt a single `encrypted:` / `enc:` value on demand. Returns the
|
|
17
|
+
* plaintext, or `undefined` when the value is unusable (no key available or
|
|
18
|
+
* decryption failed) so callers can treat it exactly like an unset variable.
|
|
19
|
+
* Non-encrypted input is returned unchanged.
|
|
20
|
+
*/
|
|
21
|
+
export declare function decryptEnvValue(value: string, options?: { env?: string, cwd?: string }): string | undefined;
|
|
2
22
|
/**
|
|
3
23
|
* Load .env files and inject into process.env
|
|
4
24
|
*/
|
package/dist/plugin.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{createCipheriv as
|
|
3
|
-
`);for(let
|
|
4
|
-
`)}if(
|
|
2
|
+
import{createCipheriv as F,createDecipheriv as V,createHash as j,createPrivateKey as l,createPublicKey as M,diffieHellman as k,generateKeyPairSync as m,hkdfSync as P,randomBytes as C}from"crypto";var S="x25519-public:",G="x25519-private:",Y="encrypted:v2:",J="encrypted:",e=Buffer.from("stacks-env:v2","utf8"),z="Environment decryption failed: authentication or format error";function N(t){if(t.length===32)return t;return j("sha256").update(t).digest()}function xt(t,r){let c=C(16),n=F("aes-256-gcm",N(r),c);return{ciphertext:Buffer.concat([n.update(t,"utf8"),n.final()]).toString("hex"),iv:c.toString("hex"),authTag:n.getAuthTag().toString("hex")}}function tt(t,r,c,n){try{let i=V("aes-256-gcm",N(r),Buffer.from(c,"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 E(t){return t.toString("base64url")}function T(t,r,c){if(typeof t!=="string"||t.length>0&&!/^[A-Za-z0-9_-]+$/.test(t))throw Error(`${r} is not canonical base64url`);let n=Buffer.from(t,"base64url");if(E(n)!==t)throw Error(`${r} is not canonical base64url`);if(c!==void 0&&n.length!==c)throw Error(`${r} has an invalid length`);return n}function rt(t){if(!t.startsWith(S))throw Error("Legacy or invalid public key. Run buddy env:rotate before creating new ciphertext.");let r=M({key:T(t.slice(S.length),"public key"),format:"der",type:"spki"});if(r.asymmetricKeyType!=="x25519")throw Error("Public key is not X25519");return r}function nt(t){if(!t.startsWith(G))throw Error("Private key is not a version 2 X25519 key");let r=l({key:T(t.slice(G.length),"private key"),format:"der",type:"pkcs8"});if(r.asymmetricKeyType!=="x25519")throw Error("Private key is not X25519");return r}function d(t){return Buffer.from(`stacks-env:v2;${t.epk};${t.salt};${t.nonce}`,"utf8")}function Et(){let{publicKey:t,privateKey:r}=m("x25519"),c=t.export({format:"der",type:"spki"}),n=r.export({format:"der",type:"pkcs8"});return{publicKey:`${S}${E(c)}`,privateKey:`${G}${E(n)}`}}function ct(t,r){let c=rt(r),n=m("x25519"),i=E(n.publicKey.export({format:"der",type:"spki"})),o=C(16),g=C(12),h=k({privateKey:n.privateKey,publicKey:c}),w=Buffer.from(P("sha256",h,o,e,32)),s={epk:i,salt:E(o),nonce:E(g)};try{let f=F("aes-256-gcm",w,g);f.setAAD(d(s));let b=Buffer.concat([f.update(t,"utf8"),f.final()]),u={v:2,...s,ciphertext:E(b),tag:E(f.getAuthTag())};return`${Y}${E(Buffer.from(JSON.stringify(u),"utf8"))}`}finally{h.fill(0),w.fill(0)}}function it(t){let r=T(t.slice(Y.length),"envelope").toString("utf8"),c=JSON.parse(r);if(!c||typeof c!=="object"||Array.isArray(c))throw Error("envelope is not an object");let n=Object.keys(c).sort(),i=["ciphertext","epk","nonce","salt","tag","v"];if(n.length!==i.length||n.some((o,g)=>o!==i[g]))throw Error("envelope fields are invalid");if(c.v!==2)throw Error("envelope version is unsupported");for(let o of["epk","salt","nonce","ciphertext","tag"])if(typeof c[o]!=="string")throw Error(`${o} is invalid`);return c}function ft(t,r){try{let c=it(t),n=nt(r),i=M({key:T(c.epk,"ephemeral public key"),format:"der",type:"spki"});if(i.asymmetricKeyType!=="x25519")throw Error("ephemeral key is not X25519");let o=T(c.salt,"salt",16),g=T(c.nonce,"nonce",12),h=T(c.ciphertext,"ciphertext"),w=T(c.tag,"tag",16),s=k({privateKey:n,publicKey:i}),f=Buffer.from(P("sha256",s,o,e,32));try{let b=V("aes-256-gcm",f,g);return b.setAAD(d(c)),b.setAuthTag(w),Buffer.concat([b.update(h),b.final()]).toString("utf8")}finally{s.fill(0),f.fill(0)}}catch{throw Error(z)}}function ot(t,r){try{if(!/^[0-9a-f]{64}$/.test(r))throw Error("invalid legacy private key");let c=Buffer.from(t.slice(J.length),"base64");if(c.length<32)throw Error("invalid legacy payload");let n=j("sha256").update(Buffer.from(r,"hex")).digest(),i=j("sha256").update(n).digest();return tt(c.subarray(32).toString("hex"),i,c.subarray(0,16).toString("hex"),c.subarray(16,32).toString("hex"))}catch{throw Error(z)}}function Tt(t){return t.startsWith(J)&&!t.startsWith(Y)}function _(t,r){if(!t.startsWith(J))return t;return t.startsWith(Y)?ft(t,r):ot(t,r)}function yt(t,r,c){return ct(_(t,r),c)}function $t(t){let r=t.match(/^DOTENV_PRIVATE_KEY_(.+)$/);return r&&r[1]?r[1].toLowerCase():""}function L(t=""){return t?process.env[`DOTENV_PRIVATE_KEY_${t.toUpperCase()}`]:process.env.DOTENV_PRIVATE_KEY}function p(t,r={}){let c={},n=[],i=[],o=t.split(`
|
|
3
|
+
`);for(let g of o){let h=g.trim();if(!h||h.startsWith("#"))continue;if(h.startsWith("DOTENV_PUBLIC_KEY=")){let b=h.match(/^DOTENV_PUBLIC_KEY=["']?([^"'\n]+)["']?/);if(b&&b[1]!==void 0)c.DOTENV_PUBLIC_KEY=b[1];continue}let w=h.match(/^([^=]+)=(.*)$/);if(!w||w[1]===void 0||w[2]===void 0)continue;let s=w[1].trim(),f=w[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(!r.privateKey){i.push(s);continue}try{let b=f.startsWith("enc:")?`encrypted:${f.slice(4)}`:f;f=_(b,r.privateKey)}catch(b){n.push(`Failed to decrypt ${s}: ${b instanceof Error?b.message:"Unknown error"}`)}}f=gt(f,{...r.processEnv||process.env,...c}),f=ht(f),c[s]=f}return{parsed:c,errors:n,skippedEncrypted:i}}function gt(t,r){return t.replace(/\$\{([^}]+)\}/g,(c,n)=>{let i=n.match(/^([^:\-+]+)(:-|-)(.+)$/);if(i){let[,g,h,w]=i,s=r[g];if(h===":-")return s||w;else return s!==void 0?s:w}let o=n.match(/^([^:\-+]+)(:?\+)(.+)$/);if(o){let[,g,h,w]=o,s=r[g];if(h===":+")return s?w:"";else return s!==void 0?w:""}return r[n]||""})}var st=new Set(["date","hostname","whoami","uname","pwd","echo","printf","cat","basename","dirname"]);function ht(t){return t.replace(/\$\(([^)]+)\)/g,(r,c)=>{try{let n=c.trim().split(/\s+/),i=n[0];if(!i||!st.has(i))return console.warn(`[env] Blocked command substitution for disallowed command: ${i}`),"";let o=Bun.spawnSync(n,{stdout:"pipe",stderr:"pipe"});if(o.exitCode===0)return new TextDecoder().decode(o.stdout).trim();console.warn(`[env] Command substitution failed (exit code ${o.exitCode}): ${i}`)}catch(n){console.warn(`[env] Command substitution error: ${n instanceof Error?n.message:String(n)}`)}return""})}async function _t(t,r={}){let c={},n=[],i=[];for(let o of t)try{let g=Bun.file(o);if(!g.size)continue;let h=await g.text(),{parsed:w,errors:s,skippedEncrypted:f}=p(h,r);n.push(...s),i.push(...f);for(let[b,u]of Object.entries(w)){if(!r.overload&&c[b]!==void 0)continue;c[b]=u}}catch(g){if(g?.code==="ENOENT")continue;n.push(`Failed to read ${o}: ${g instanceof Error?g.message:String(g)}`)}return{parsed:c,errors:n,skippedEncrypted:i}}import{existsSync as H}from"fs";import{readFileSync as U}from"fs";import{resolve as W}from"path";function K(t){return typeof t==="string"&&(t.startsWith("encrypted:")||t.startsWith("enc:"))}function y(t){if(!t||K(t))return;let r=t.trim().toLowerCase();if(!/^[a-z0-9_-]+$/.test(r))return;if(r==="prod")return"production";if(r==="stage")return"staging";if(r==="dev")return"development";return r}var Z=null,Q;function wt(t,r,c=".env.keys"){let n=W(r,c);if(!H(n))return;try{let{parsed:i}=p(U(n,"utf-8"));if(t){let o=i[`DOTENV_PRIVATE_KEY_${t.toUpperCase()}`];if(o)return o}return i.DOTENV_PRIVATE_KEY}catch{return}}function bt(t={}){let r=y(t.env)||y(process.env.APP_ENV)||y("development")||y(process.env.DOTENV_ENV)||"development",c=t.cwd||process.cwd();if(Z===r)return Q;let n=L(r);if(!n)n=process.env.DOTENV_PRIVATE_KEY;if(!n)n=wt(r,c);return Z=r,Q=n,n}function Ht(){Z=null,Q=void 0}function Wt(t,r={}){if(!K(t))return t;let c=bt(r);if(!c)return;try{let n=t.startsWith("enc:")?`encrypted:${t.slice(4)}`:t;return _(n,c)}catch{return}}function a(t={}){let{path:r=[".env"],overload:c=!1,env:n,privateKey:i,keysFile:o=".env.keys",quiet:g=!1,cwd:h=process.cwd()}=t,w=Array.isArray(r)?r:[r],s=[],f=0,b=new Set(Object.entries(process.env).filter(([,B])=>B!==void 0&&!K(B)).map(([B])=>B)),u=i;if(!u&&o){let B=W(h,o);if(H(B))try{let x=U(B,"utf-8"),{parsed:$}=p(x);if(n){let D=`DOTENV_PRIVATE_KEY_${n.toUpperCase()}`;u=$[D]}else u=$.DOTENV_PRIVATE_KEY}catch(x){s.push(`Failed to load keys file: ${x instanceof Error?x.message:"Unknown error"}`)}}if(!u)u=L(n||"");for(let B of w){let x=W(h,B);if(!H(x)){if(!g)s.push(`File not found: ${x}`);continue}try{let $=U(x,"utf-8"),{parsed:D,errors:v,skippedEncrypted:A}=p($,{privateKey:u,processEnv:process.env});if(s.push(...v),A.length>0){for(let O of A)if(K(process.env[O]))delete process.env[O];let R=n?`DOTENV_PRIVATE_KEY_${n.toUpperCase()}`:"DOTENV_PRIVATE_KEY",X=A.slice(0,5).join(", "),I=A.length>5?`, \u2026 +${A.length-5} more`:"";console.warn(`[env] warning: skipped ${A.length} encrypted value(s) in ${B} (${R} not set; defaults apply): ${X}${I}`)}let q=0;for(let[R,X]of Object.entries(D)){if(R==="DOTENV_PUBLIC_KEY")continue;let I=process.env[R],O=K(I)&&X!==I;if(c||!b.has(R)||O)process.env[R]=X,f++,q++}if(!g&&!process.env.__ENV_LOADED__)console.error(`[env] loaded ${q}/${Object.keys(D).length} variables from ${B}`),process.env.__ENV_LOADED__="1"}catch($){s.push(`Failed to load ${x}: ${$ instanceof Error?$.message:"Unknown error"}`)}}return{loaded:f,errors:s}}function Bt(t={}){return{name:"env-plugin",setup(r){a(t)}}}function jt(t={}){let r=y(t.env)||y("development")||y(process.env.DOTENV_ENV)||y(process.env.APP_ENV)||"development",c=t.cwd||process.cwd(),n=[],i=(h)=>{if(!n.includes(h)&&H(W(c,h)))n.push(h)};i(".env"),i(".env.local");let o=`.env.${r}`,g=`.env.${r}.local`;return i(o),i(g),a({...t,path:n,env:r})}var St=Bt;export{bt as resolvePrivateKey,Ht as resetPrivateKeyCache,a as loadEnv,Bt as envPlugin,St as default,Wt as decryptEnvValue,jt as autoLoadEnv};
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/env",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.70.
|
|
5
|
+
"version": "0.70.162",
|
|
6
6
|
"description": "Stacks env helper methods.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"better-dx": "^0.2.17",
|
|
54
|
-
"@stacksjs/path": "0.70.
|
|
55
|
-
"@stacksjs/validation": "0.70.
|
|
54
|
+
"@stacksjs/path": "0.70.162",
|
|
55
|
+
"@stacksjs/validation": "0.70.162"
|
|
56
56
|
}
|
|
57
57
|
}
|