secenvs 0.1.0 → 0.1.2
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 +2 -0
- package/bin/secenvs +9 -9
- package/lib/index.cjs +16 -4
- package/lib/index.d.cts +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.js +16 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
**Make `.env` secure again. Commit to GitHub without fear.**
|
|
4
4
|
|
|
5
|
+
Follow the journey on X: [@YogeshDev215](https://x.com/YogeshDev215) 🚀
|
|
6
|
+
|
|
5
7
|
secenvs encrypts your environment variables so you can safely commit them to version control. It's `.env` you
|
|
6
8
|
can actually share—without the security headaches.
|
|
7
9
|
|
package/bin/secenvs
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import*as u from"fs";import*as de from"path";import*as ue from"readline";import*as
|
|
3
|
-
`),r=[],s=new Set,
|
|
4
|
-
`),
|
|
2
|
+
import*as u from"fs";import*as de from"path";import*as ue from"readline";import*as h from"age-encryption";import*as C from"fs";import*as Y from"path";import*as J from"os";var D={IDENTITY_NOT_FOUND:"IDENTITY_NOT_FOUND",DECRYPTION_FAILED:"DECRYPTION_FAILED",SECRET_NOT_FOUND:"SECRET_NOT_FOUND",PARSE_ERROR:"PARSE_ERROR",FILE_ERROR:"FILE_ERROR",ENCRYPTION_FAILED:"ENCRYPTION_FAILED",VALIDATION_ERROR:"VALIDATION_ERROR"},p=class extends Error{code;constructor(t,n){super(n),this.name="SecenvError",this.code=t}},$=class extends p{constructor(t){super(D.VALIDATION_ERROR,t)}},E=class extends p{constructor(t){super(D.IDENTITY_NOT_FOUND,`Identity key not found at ${t}. Run 'secenv init' to create one.`)}},F=class extends p{constructor(t="Failed to decrypt value. Check identity key."){super(D.DECRYPTION_FAILED,t)}},I=class extends p{constructor(t){super(D.SECRET_NOT_FOUND,`Secret '${t}' not found in .secenvs or process.env.`)}},v=class extends p{line;raw;constructor(t,n,r){super(D.PARSE_ERROR,r),this.line=t,this.raw=n}},d=class extends p{constructor(t){super(D.FILE_ERROR,t)}},k=class extends p{constructor(t="Failed to encrypt value."){super(D.ENCRYPTION_FAILED,t)}};import*as m from"fs";import*as U from"path";function N(e){try{if(m.lstatSync(e).isSymbolicLink())throw new d(`Symlink detected at ${e}. Security policy prohibits following symlinks.`);return m.readFileSync(e,"utf-8")}catch(t){throw t instanceof d?t:new d(`Failed to read file ${e}: ${t.message}`)}}function G(e,t){let n=U.resolve(e);try{if(m.existsSync(e)&&m.lstatSync(e).isSymbolicLink())throw new d(`Symlink detected at ${e}. Security policy prohibits following symlinks.`)}catch(r){if(r instanceof d)throw r}if(t){let r=U.resolve(t);if(!n.startsWith(r))throw new d(`Directory traversal detected: ${e} is outside of ${t}`)}return n}function Z(e){try{if(!m.existsSync(e)){m.mkdirSync(e,{recursive:!0,mode:448});return}if(m.lstatSync(e).isSymbolicLink())throw new d(`Symlink detected at directory ${e}. Security policy prohibits following symlinks.`)}catch(t){throw t instanceof d?t:new d(`Failed to ensure safe directory ${e}: ${t.message}`)}}var pe=".secenvs",me="keys",we="default.key";function Q(e){return new ReadableStream({start(t){t.enqueue(e),t.close()}})}async function ee(e){if(!(e instanceof ReadableStream))throw new Error("readAll expects a ReadableStream<Uint8Array>");return new Uint8Array(await new Response(e).arrayBuffer())}function te(){let e=process.env.SECENV_HOME||J.homedir(),t=G(e);return Y.join(t,pe,me)}function x(){return Y.join(te(),we)}function ge(){let e=te();Z(e)}async function ne(){return h.generateX25519Identity()}async function re(e){ge();let t=x();return C.writeFileSync(t,e,{mode:384}),t}async function P(){let e=x();if(!C.existsSync(e))throw new E(e);return C.readFileSync(e,"utf-8")}async function se(e,t){let n=await h.identityToRecipient(e),r=new h.Encrypter;r.addRecipient(n);let s=typeof t=="string"?Buffer.from(t):t,o=await r.encrypt(Q(s instanceof Buffer?s:new Uint8Array(s))),i=await ee(o);return Buffer.from(i).toString("base64")}async function _(e,t){try{let n=new h.Decrypter;n.addIdentity(e);let r=Q(Buffer.from(t,"base64")),s=await n.decrypt(r),o=await ee(s);return Buffer.from(o)}catch(n){throw new F(`Failed to decrypt value: ${n}`)}}function S(){let e=x();return C.existsSync(e)}async function V(e){return h.identityToRecipient(e)}import*as y from"fs";import*as ae from"path";var Ee=/^[A-Z0-9_]+$/,he=5*1024*1024,xe=new Set(["CON","PRN","AUX","NUL","COM1","COM2","COM3","COM4","COM5","COM6","COM7","COM8","COM9","LPT1","LPT2","LPT3","LPT4","LPT5","LPT6","LPT7","LPT8","LPT9"]);function A(e){if(!e)throw new $("Key cannot be empty");if(!Ee.test(e))throw new $(`Invalid key: '${e}'. Keys must only contain uppercase letters, numbers, and underscores (^[A-Z0-9_]+$).`);if(xe.has(e))throw new $(`Invalid key: '${e}' is a reserved system name.`)}function ie(e){if(Buffer.byteLength(e,"utf-8")>he)throw new $("Value size exceeds maximum limit of 5MB")}import{timingSafeEqual as ve}from"node:crypto";function oe(e,t){return e.length!==t.length?!1:ve(Buffer.from(e),Buffer.from(t))}var Se="enc:age:";function Re(e){return e.startsWith(Se)}function L(e){if(!y.existsSync(e))return{lines:[],keys:new Set,encryptedCount:0,plaintextCount:0};let t=N(e);t.startsWith("\uFEFF")&&(t=t.slice(1));let n=t.split(`
|
|
3
|
+
`),r=[],s=new Set,o=0,i=0;for(let c=0;c<n.length;c++){let f=c+1,l=n[c],g=l.trim();if(!g||g.startsWith("#")){r.push({key:"",value:g,encrypted:!1,lineNumber:f,raw:l});continue}let T=g.indexOf("=");if(T===-1)throw new v(f,l,"Invalid line: missing '=' separator");let O=g.slice(0,T),H=g.slice(T+1);if(!O)throw new v(f,l,"Invalid line: missing key before '='");if(A(O),s.has(O))throw new v(f,l,`Duplicate key '${O}'`);let z=Re(H);r.push({key:O,value:H,encrypted:z,lineNumber:f,raw:l}),s.add(O),z?o++:i++}return{lines:r,keys:s,encryptedCount:o,plaintextCount:i}}function B(e,t){let n=null;for(let r of e.lines)oe(r.key,t)&&(n=r);return n}async function ce(e,t,n){A(t),ie(n),await M(e,async()=>{let s=(y.existsSync(e)?N(e):"").split(`
|
|
4
|
+
`),o=!1,i=[];for(let f of s){let l=f.trim();if(!l||l.startsWith("#")){i.push(f);continue}let g=l.indexOf("=");if(g!==-1&&l.slice(0,g)===t){i.push(`${t}=${n}`),o=!0;continue}i.push(f)}o||i.push(`${t}=${n}`);let c=i.filter((f,l)=>f.trim()!==""||l<i.length-1).join(`
|
|
5
5
|
`).trim()+`
|
|
6
6
|
`;await W(e,c)})}async function fe(e,t){A(t),await M(e,async()=>{let r=(y.existsSync(e)?N(e):"").split(`
|
|
7
|
-
`),s=[];for(let
|
|
7
|
+
`),s=[];for(let i of r){let c=i.trim();if(!c||c.startsWith("#")){s.push(i);continue}let f=c.indexOf("=");f!==-1&&c.slice(0,f)===t||s.push(i)}let o=s.filter((i,c)=>i.trim()!==""||c<s.length-1).join(`
|
|
8
8
|
`).trim()+`
|
|
9
|
-
`;await W(e,
|
|
10
|
-
`)}function w(e){a(`\u2713 ${e}`,"green")}function
|
|
11
|
-
`;s.includes(
|
|
9
|
+
`;await W(e,o)})}async function M(e,t){let n=`${e}.lock`,r=null,s=100,o=10;for(;s>0;)try{r=await y.promises.open(n,"wx"),await r.write(process.pid.toString());break}catch(i){if(i.code==="EEXIST"){try{let c=await y.promises.readFile(n,"utf-8"),f=parseInt(c.trim(),10);if(!isNaN(f))try{process.kill(f,0)}catch(l){if(l.code==="ESRCH")try{await y.promises.unlink(n);continue}catch{}}}catch{}s--,await new Promise(c=>setTimeout(c,o)),o=Math.min(o*1.5+Math.random()*50,5e3)}else throw new d(`Failed to acquire lock on ${e}: ${i}`)}if(!r)throw new d(`Timeout waiting for lock on ${e}`);try{await t()}finally{await r.close();try{await y.promises.unlink(n)}catch{}}}async function le(e,t){await M(e,async()=>{await W(e,t)})}async function W(e,t){let n=`${e}.tmp.${Date.now()}`;try{await y.promises.writeFile(n,t,{mode:420});let r=await y.promises.open(n,"r");await r.sync(),await r.close(),await y.promises.rename(n,e)}catch(r){try{y.existsSync(n)&&await y.promises.unlink(n)}catch{}throw new d(`Failed to write ${e}: ${r}`)}}function R(){return ae.join(process.cwd(),".secenvs")}var K="enc:age:";function a(e,t="reset",n=!1){let r={reset:"\x1B[0m",green:"\x1B[32m",red:"\x1B[31m",yellow:"\x1B[33m",blue:"\x1B[34m",cyan:"\x1B[36m"};(n?process.stderr:process.stdout).write(`${r[t]||r.reset}${e}${r.reset}
|
|
10
|
+
`)}function w(e){a(`\u2713 ${e}`,"green")}function b(e){a(`\u2717 ${e}`,"red",!0)}function X(e){a(`\u26A0 ${e}`,"yellow")}function q(e){a(`\u2139 ${e}`,"cyan")}async function j(e){let t=ue.createInterface({input:process.stdin,output:process.stdout});return new Promise((n,r)=>{t.question(e,s=>{t.close(),n(s)})})}async function be(e){return(await j(`${e} (yes/no): `)).toLowerCase()==="yes"}async function $e(){if(S()){X('Identity already exists. Run "secenvs doctor" to check.');return}q("Generating identity key...");let e=await ne(),t=await re(e);w(`Identity created at ${t}`);let n=R();u.existsSync(n)||(await le(n,""),w(`Created ${n}`));let r=de.join(process.cwd(),".gitignore"),s="";u.existsSync(r)&&(s=u.readFileSync(r,"utf-8"));let o=`.secenvs
|
|
11
|
+
`;s.includes(o)||(s&&!s.endsWith(`
|
|
12
12
|
`)&&(s+=`
|
|
13
|
-
`),s+=
|
|
14
|
-
`)||r.includes("\r"))throw new
|
|
13
|
+
`),s+=o,u.writeFileSync(r,s),w("Updated .gitignore"));let i=await V(e);q(`Your public key: ${i}`),q("Keep your private key safe!")}async function ye(e,t,n=!1){if(!S())throw new E(x());let r=t;if(r||(r=await j(`Enter value for ${e}: `)),!r)throw new k("Value cannot be empty");if(n)try{Buffer.from(r,"base64")}catch{throw new k("Invalid base64 value")}else if(r.includes(`
|
|
14
|
+
`)||r.includes("\r"))throw new k("Multiline values are not allowed. Use --base64 for binary data.");let s=await P(),o=n?Buffer.from(r,"base64"):r,i=await se(s,o),c=`${K}${i}`,f=R();await ce(f,e,c),w(`Encrypted and stored ${e}`)}async function Ie(e){if(!S())throw new E(x());let t=await P(),n=R();if(!u.existsSync(n))throw new I(e);let r=L(n),s=B(r,e);if(!s)throw new I(e);if(s.encrypted){let o=await _(t,s.value.slice(K.length));process.stdout.write(o.toString("utf-8"))}else process.stdout.write(s.value)}async function ke(){let e=R();if(!u.existsSync(e))return;let t=L(e);for(let n of t.lines)if(n.key){let r=n.encrypted?"[encrypted]":"[plaintext]";a(`${n.key} ${r}`)}}async function De(e){let t=R();if(!u.existsSync(t))throw new I(e);let n=L(t);if(!B(n,e))throw new I(e);await fe(t,e),w(`Deleted ${e}`)}async function Ce(e,t){let n=t;if(n||(n=await j(`Enter new value for ${e}: `)),!n)throw new k("Value cannot be empty");await ye(e,n),w(`Rotated ${e}`)}async function Pe(e=!1){if(!e&&!await be("WARNING: You are about to export ALL secrets in PLAINTEXT")){a("Export cancelled.");return}if(!S())throw new E(x());let t=await P(),n=R();if(!u.existsSync(n)){a("No .secenvs file found.");return}let r=L(n);for(let s of r.lines)if(s.key){let o;s.encrypted?o=(await _(t,s.value.slice(K.length))).toString("utf-8"):o=s.value,a(`${s.key}=${o}`)}}async function Le(){let e=0,t=0;e++;let n=x();if(S())try{let s=u.statSync(n);process.platform!=="win32"&&(s.mode&511)!==384?X(`Identity: ${n} (exists, but permissions should be 0600, found ${(s.mode&511).toString(8)})`):w(`Identity: ${n}`);let i=await P();await V(i),w(`Identity: ${n}`),t++}catch{b(`Identity: ${n} (invalid)`)}else b(`Identity: ${n} (not found)`);e++;let r=R();if(u.existsSync(r)?(w(`File: ${r} (exists)`),t++):(X(`File: ${r} (not found)`),t++),e++,u.existsSync(r))try{let s=L(r);w(`Syntax: ${s.lines.length} lines, ${s.encryptedCount} encrypted, ${s.plaintextCount} plaintext`),t++}catch(s){s instanceof v?b(`Syntax: Line ${s.line}: ${s.message}`):b(`Syntax: ${s}`)}else a("Syntax: (no file)"),t++;if(e++,S()&&u.existsSync(r))try{let s=await P(),o=L(r),i=0,c=0;for(let f of o.lines)if(f.encrypted)try{await _(s,f.value.slice(K.length)),i++}catch{c++}c===0?(w(`Decryption: ${i}/${i} keys verified`),t++):b(`Decryption: ${i} succeeded, ${c} failed`)}catch(s){b(`Decryption: ${s}`)}else a("Decryption: (skipped - no identity or file)"),t++;a(""),a(`Doctor: ${t}/${e} checks passed`)}async function Oe(){let e=process.argv.slice(2),t=e[0]||"help";try{switch(t){case"init":await $e();break;case"set":{let n=e.includes("--base64"),r=e.filter(i=>i!=="--base64"),s=r[1];if(!s)throw new Error("Missing KEY argument. Usage: secenvs set KEY [VALUE] [--base64]");let o=r[2];await ye(s,o,n);break}case"get":{let n=e[1];if(!n)throw new Error("Missing KEY argument. Usage: secenvs get KEY");await Ie(n);break}case"list":await ke();break;case"delete":{let n=e[1];if(!n)throw new Error("Missing KEY argument. Usage: secenvs delete KEY");await De(n);break}case"rotate":{let n=e[1];if(!n)throw new Error("Missing KEY argument. Usage: secenvs rotate KEY [VALUE]");let r=e[2];await Ce(n,r);break}case"export":{let n=e.includes("--force");await Pe(n);break}case"key":{if(e[1]==="export"){if(!S())throw new E(x());let r=await P();process.stdout.write(r);break}throw new Error("Invalid key subcommand. Usage: secenvs key export")}case"doctor":await Le();break;case"help":default:a("secenvs - The Breeze: Secret management without the overhead"),a(""),a("Usage: secenvs <command> [arguments]"),a(""),a("Commands:"),a(" init Bootstrap identity and create .secenvs/.gitignore"),a(" set KEY [VALUE] Encrypt a value into .secenvs (primary method)"),a(" set KEY [VALUE] --base64 Encrypt a base64 value (for binary data)"),a(" get KEY Decrypt and print a specific key value"),a(" list List all available key names (values hidden)"),a(" delete KEY Remove a key from .secenvs"),a(" rotate KEY [VALUE] Update a secret value and re-encrypt"),a(" export [--force] Dump all decrypted secrets (requires --force)"),a(" key export Export private key for CI/CD"),a(" doctor Health check: identity, file integrity, decryption"),a("");break}}catch(n){throw n instanceof p&&(b(n.message),process.exit(1)),n}}Oe().catch(e=>{b(e.message),process.exit(1)});
|
package/lib/index.cjs
CHANGED
|
@@ -198,7 +198,7 @@ async function decrypt(identity, encryptedMessage) {
|
|
|
198
198
|
const armoredStream = stream(Buffer.from(encryptedMessage, "base64"));
|
|
199
199
|
const decryptedStream = await decrypter.decrypt(armoredStream);
|
|
200
200
|
const decryptedBytes = await readAll(decryptedStream);
|
|
201
|
-
return
|
|
201
|
+
return Buffer.from(decryptedBytes);
|
|
202
202
|
} catch (error) {
|
|
203
203
|
throw new DecryptionError(`Failed to decrypt value: ${error}`);
|
|
204
204
|
}
|
|
@@ -447,8 +447,9 @@ var SecenvSDK = class {
|
|
|
447
447
|
const identity = await this.loadIdentity();
|
|
448
448
|
const encryptedMessage = line.value.slice(ENCRYPTED_PREFIX.length);
|
|
449
449
|
const decrypted = await decrypt(identity, encryptedMessage);
|
|
450
|
-
|
|
451
|
-
|
|
450
|
+
const decryptedString = decrypted.toString("utf-8");
|
|
451
|
+
this.#cache.set(key, { value: decryptedString, decryptedAt: Date.now() });
|
|
452
|
+
return decryptedString;
|
|
452
453
|
}
|
|
453
454
|
has(key) {
|
|
454
455
|
let found = false;
|
|
@@ -486,7 +487,18 @@ var globalSDK = new SecenvSDK();
|
|
|
486
487
|
function createSecenv() {
|
|
487
488
|
return new SecenvSDK();
|
|
488
489
|
}
|
|
489
|
-
var env = globalSDK
|
|
490
|
+
var env = new Proxy(globalSDK, {
|
|
491
|
+
get(target, prop) {
|
|
492
|
+
if (typeof prop === "string" && !Reflect.has(target, prop)) {
|
|
493
|
+
return target.get(prop);
|
|
494
|
+
}
|
|
495
|
+
const value = Reflect.get(target, prop);
|
|
496
|
+
if (typeof value === "function") {
|
|
497
|
+
return value.bind(target);
|
|
498
|
+
}
|
|
499
|
+
return value;
|
|
500
|
+
}
|
|
501
|
+
});
|
|
490
502
|
// Annotate the CommonJS export names for ESM import in node:
|
|
491
503
|
0 && (module.exports = {
|
|
492
504
|
DecryptionError,
|
package/lib/index.d.cts
CHANGED
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -146,7 +146,7 @@ async function decrypt(identity, encryptedMessage) {
|
|
|
146
146
|
const armoredStream = stream(Buffer.from(encryptedMessage, "base64"));
|
|
147
147
|
const decryptedStream = await decrypter.decrypt(armoredStream);
|
|
148
148
|
const decryptedBytes = await readAll(decryptedStream);
|
|
149
|
-
return
|
|
149
|
+
return Buffer.from(decryptedBytes);
|
|
150
150
|
} catch (error) {
|
|
151
151
|
throw new DecryptionError(`Failed to decrypt value: ${error}`);
|
|
152
152
|
}
|
|
@@ -395,8 +395,9 @@ var SecenvSDK = class {
|
|
|
395
395
|
const identity = await this.loadIdentity();
|
|
396
396
|
const encryptedMessage = line.value.slice(ENCRYPTED_PREFIX.length);
|
|
397
397
|
const decrypted = await decrypt(identity, encryptedMessage);
|
|
398
|
-
|
|
399
|
-
|
|
398
|
+
const decryptedString = decrypted.toString("utf-8");
|
|
399
|
+
this.#cache.set(key, { value: decryptedString, decryptedAt: Date.now() });
|
|
400
|
+
return decryptedString;
|
|
400
401
|
}
|
|
401
402
|
has(key) {
|
|
402
403
|
let found = false;
|
|
@@ -434,7 +435,18 @@ var globalSDK = new SecenvSDK();
|
|
|
434
435
|
function createSecenv() {
|
|
435
436
|
return new SecenvSDK();
|
|
436
437
|
}
|
|
437
|
-
var env = globalSDK
|
|
438
|
+
var env = new Proxy(globalSDK, {
|
|
439
|
+
get(target, prop) {
|
|
440
|
+
if (typeof prop === "string" && !Reflect.has(target, prop)) {
|
|
441
|
+
return target.get(prop);
|
|
442
|
+
}
|
|
443
|
+
const value = Reflect.get(target, prop);
|
|
444
|
+
if (typeof value === "function") {
|
|
445
|
+
return value.bind(target);
|
|
446
|
+
}
|
|
447
|
+
return value;
|
|
448
|
+
}
|
|
449
|
+
});
|
|
438
450
|
export {
|
|
439
451
|
DecryptionError,
|
|
440
452
|
EncryptionError,
|