secenvs 0.1.0
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 +149 -0
- package/bin/secenvs +14 -0
- package/lib/index.cjs +509 -0
- package/lib/index.d.cts +74 -0
- package/lib/index.d.ts +74 -0
- package/lib/index.js +456 -0
- package/package.json +67 -0
package/README.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# secenvs
|
|
2
|
+
|
|
3
|
+
**Make `.env` secure again. Commit to GitHub without fear.**
|
|
4
|
+
|
|
5
|
+
secenvs encrypts your environment variables so you can safely commit them to version control. It's `.env` you
|
|
6
|
+
can actually share—without the security headaches.
|
|
7
|
+
|
|
8
|
+
## Why secenvs?
|
|
9
|
+
|
|
10
|
+
- **Zero wrapper needed** — Import and use secrets directly in your code
|
|
11
|
+
- **Lightning fast** — Cold start <50ms, cached access <1ms
|
|
12
|
+
- **Battle-tested encryption** — AEAD encryption via [age](https://github.com/FiloSottile/age)
|
|
13
|
+
- **CI/CD ready** — Works seamlessly with GitHub Actions and other pipelines
|
|
14
|
+
- **Defense-first** — Private fields prevent memory-scanning attacks
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# Install
|
|
20
|
+
npm install secenvs
|
|
21
|
+
|
|
22
|
+
# Initialize (one-time setup)
|
|
23
|
+
npx secenvs init
|
|
24
|
+
|
|
25
|
+
# Add a secret
|
|
26
|
+
npx secenvs set API_KEY "your-secret-key"
|
|
27
|
+
|
|
28
|
+
# Use in code
|
|
29
|
+
import { env } from 'secenvs';
|
|
30
|
+
|
|
31
|
+
const apiKey = await env.API_KEY;
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## CLI Commands
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
secenvs init # Initialize identity and project
|
|
38
|
+
secenvs set KEY VALUE # Set a secret (encrypted)
|
|
39
|
+
secenvs get KEY # Get a secret (decrypted)
|
|
40
|
+
secenvs list # List all keys
|
|
41
|
+
secenvs rotate KEY # Rotate a secret
|
|
42
|
+
secenvs delete KEY # Delete a secret
|
|
43
|
+
secenvs doctor # Verify setup and encryption
|
|
44
|
+
secenvs key export # Export private key for CI
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## SDK Usage
|
|
48
|
+
|
|
49
|
+
### Basic Access
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import { env } from "secenvs"
|
|
53
|
+
|
|
54
|
+
const dbUrl = await env.DATABASE_URL
|
|
55
|
+
const apiKey = await env.API_KEY
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Fallback to process.env
|
|
59
|
+
|
|
60
|
+
The SDK checks `process.env` first, then falls back to your `.secenvs` file:
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
// In production, set DATABASE_URL in your deployment platform
|
|
64
|
+
// Locally, use .secenvs
|
|
65
|
+
const dbUrl = await env.DATABASE_URL
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Error Handling
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import { env, SecretNotFoundError } from "secenvs"
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const key = await env.MISSING_KEY
|
|
75
|
+
} catch (e) {
|
|
76
|
+
if (e instanceof SecretNotFoundError) {
|
|
77
|
+
console.error("Secret not found in .secenvs")
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Programmatic API
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import { createSecenv } from "secenvs"
|
|
86
|
+
|
|
87
|
+
const sdk = await createSecenv()
|
|
88
|
+
await sdk.set("API_KEY", "secret-value")
|
|
89
|
+
const value = await sdk.get("API_KEY")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## The `.secenvs` File
|
|
93
|
+
|
|
94
|
+
Store secrets in `.secenvs` in your project root:
|
|
95
|
+
|
|
96
|
+
```env
|
|
97
|
+
# Encrypted values (auto-decrypted)
|
|
98
|
+
API_KEY=enc:age:AGE-SECRET-KEY-1XYZ...
|
|
99
|
+
|
|
100
|
+
# Plaintext values (non-sensitive config)
|
|
101
|
+
PORT=3000
|
|
102
|
+
NODE_ENV=development
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Use `--base64` flag for binary values like certificates:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
secenvs set TLS_CERT --base64 < server.crt
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## CI/CD Integration
|
|
112
|
+
|
|
113
|
+
### 1. Export your identity
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
secenvs key export
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 2. Add to CI secrets
|
|
120
|
+
|
|
121
|
+
Add the output as `SECENV_ENCODED_IDENTITY` in your CI provider (GitHub Secrets, etc.).
|
|
122
|
+
|
|
123
|
+
### 3. Use in CI
|
|
124
|
+
|
|
125
|
+
```yaml
|
|
126
|
+
# GitHub Actions example
|
|
127
|
+
- name: Run app
|
|
128
|
+
env:
|
|
129
|
+
SECENV_ENCODED_IDENTITY: ${{ secrets.SECENV_ENCODED_IDENTITY }}
|
|
130
|
+
run: npm run start
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Security
|
|
134
|
+
|
|
135
|
+
- **AEAD encryption** — Each secret is encrypted separately with age
|
|
136
|
+
- **Private fields** — JavaScript private class fields protect against memory-scanning
|
|
137
|
+
- **Constant-time lookups** — Prevents timing attacks
|
|
138
|
+
- **Symlink protection** — Blocks symlink attacks
|
|
139
|
+
|
|
140
|
+
For full security details, see [SECURITY.md](./SECURITY.md).
|
|
141
|
+
|
|
142
|
+
## Requirements
|
|
143
|
+
|
|
144
|
+
- Node.js 18+
|
|
145
|
+
- No external dependencies (age encryption bundled)
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
MIT
|
package/bin/secenvs
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import*as u from"fs";import*as de from"path";import*as ue from"readline";import*as E from"age-encryption";import*as D from"fs";import*as Y from"path";import*as J from"os";var k={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}},b=class extends p{constructor(t){super(k.VALIDATION_ERROR,t)}},h=class extends p{constructor(t){super(k.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(k.DECRYPTION_FAILED,t)}},$=class extends p{constructor(t){super(k.SECRET_NOT_FOUND,`Secret '${t}' not found in .secenvs or process.env.`)}},x=class extends p{line;raw;constructor(t,n,r){super(k.PARSE_ERROR,r),this.line=t,this.raw=n}},d=class extends p{constructor(t){super(k.FILE_ERROR,t)}},I=class extends p{constructor(t="Failed to encrypt value."){super(k.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 v(){return Y.join(te(),we)}function ge(){let e=te();Z(e)}async function ne(){return E.generateX25519Identity()}async function re(e){ge();let t=v();return D.writeFileSync(t,e,{mode:384}),t}async function C(){let e=v();if(!D.existsSync(e))throw new h(e);return D.readFileSync(e,"utf-8")}async function se(e,t){let n=await E.identityToRecipient(e),r=new E.Encrypter;r.addRecipient(n);let s=await r.encrypt(Q(Buffer.from(t))),i=await ee(s);return Buffer.from(i).toString("base64")}async function _(e,t){try{let n=new E.Decrypter;n.addIdentity(e);let r=Q(Buffer.from(t,"base64")),s=await n.decrypt(r),i=await ee(s);return new TextDecoder().decode(i)}catch(n){throw new F(`Failed to decrypt value: ${n}`)}}function P(){let e=v();return D.existsSync(e)}async function V(e){return E.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 b("Key cannot be empty");if(!Ee.test(e))throw new b(`Invalid key: '${e}'. Keys must only contain uppercase letters, numbers, and underscores (^[A-Z0-9_]+$).`);if(xe.has(e))throw new b(`Invalid key: '${e}' is a reserved system name.`)}function ie(e){if(Buffer.byteLength(e,"utf-8")>he)throw new b("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,i=0,o=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 x(f,l,"Invalid line: missing '=' separator");let O=g.slice(0,T),H=g.slice(T+1);if(!O)throw new x(f,l,"Invalid line: missing key before '='");if(A(O),s.has(O))throw new x(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?i++:o++}return{lines:r,keys:s,encryptedCount:i,plaintextCount:o}}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
|
+
`),i=!1,o=[];for(let f of s){let l=f.trim();if(!l||l.startsWith("#")){o.push(f);continue}let g=l.indexOf("=");if(g!==-1&&l.slice(0,g)===t){o.push(`${t}=${n}`),i=!0;continue}o.push(f)}i||o.push(`${t}=${n}`);let c=o.filter((f,l)=>f.trim()!==""||l<o.length-1).join(`
|
|
5
|
+
`).trim()+`
|
|
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 o of r){let c=o.trim();if(!c||c.startsWith("#")){s.push(o);continue}let f=c.indexOf("=");f!==-1&&c.slice(0,f)===t||s.push(o)}let i=s.filter((o,c)=>o.trim()!==""||c<s.length-1).join(`
|
|
8
|
+
`).trim()+`
|
|
9
|
+
`;await W(e,i)})}async function M(e,t){let n=`${e}.lock`,r=null,s=100,i=10;for(;s>0;)try{r=await y.promises.open(n,"wx"),await r.write(process.pid.toString());break}catch(o){if(o.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,i)),i=Math.min(i*1.5+Math.random()*50,5e3)}else throw new d(`Failed to acquire lock on ${e}: ${o}`)}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 S(){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 R(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(P()){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=S();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 i=`.secenvs
|
|
11
|
+
`;s.includes(i)||(s&&!s.endsWith(`
|
|
12
|
+
`)&&(s+=`
|
|
13
|
+
`),s+=i,u.writeFileSync(r,s),w("Updated .gitignore"));let o=await V(e);q(`Your public key: ${o}`),q("Keep your private key safe!")}async function ye(e,t,n=!1){if(!P())throw new h(v());let r=t;if(r||(r=await j(`Enter value for ${e}: `)),!r)throw new I("Value cannot be empty");if(n)try{Buffer.from(r,"base64")}catch{throw new I("Invalid base64 value")}else if(r.includes(`
|
|
14
|
+
`)||r.includes("\r"))throw new I("Multiline values are not allowed. Use --base64 for binary data.");let s=await C(),i=await se(s,r),o=`${K}${i}`,c=S();await ce(c,e,o),w(`Encrypted and stored ${e}`)}async function Ie(e){if(!P())throw new h(v());let t=await C(),n=S();if(!u.existsSync(n))throw new $(e);let r=L(n),s=B(r,e);if(!s)throw new $(e);if(s.encrypted){let i=await _(t,s.value.slice(K.length));process.stdout.write(i)}else process.stdout.write(s.value)}async function ke(){let e=S();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=S();if(!u.existsSync(t))throw new $(e);let n=L(t);if(!B(n,e))throw new $(e);await fe(t,e),w(`Deleted ${e}`)}async function Pe(e,t){let n=t;if(n||(n=await j(`Enter new value for ${e}: `)),!n)throw new I("Value cannot be empty");await ye(e,n),w(`Rotated ${e}`)}async function Le(e=!1){if(!e&&!await be("WARNING: You are about to export ALL secrets in PLAINTEXT")){a("Export cancelled.");return}if(!P())throw new h(v());let t=await C(),n=S();if(!u.existsSync(n)){a("No .secenvs file found.");return}let r=L(n);for(let s of r.lines)if(s.key){let i;s.encrypted?i=await _(t,s.value.slice(K.length)):i=s.value,a(`${s.key}=${i}`)}}async function Oe(){let e=0,t=0;e++;let n=v();if(P())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 o=await C();await V(o),w(`Identity: ${n}`),t++}catch{R(`Identity: ${n} (invalid)`)}else R(`Identity: ${n} (not found)`);e++;let r=S();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 x?R(`Syntax: Line ${s.line}: ${s.message}`):R(`Syntax: ${s}`)}else a("Syntax: (no file)"),t++;if(e++,P()&&u.existsSync(r))try{let s=await C(),i=L(r),o=0,c=0;for(let f of i.lines)if(f.encrypted)try{await _(s,f.value.slice(K.length)),o++}catch{c++}c===0?(w(`Decryption: ${o}/${o} keys verified`),t++):R(`Decryption: ${o} succeeded, ${c} failed`)}catch(s){R(`Decryption: ${s}`)}else a("Decryption: (skipped - no identity or file)"),t++;a(""),a(`Doctor: ${t}/${e} checks passed`)}async function Ce(){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(o=>o!=="--base64"),s=r[1];if(!s)throw new Error("Missing KEY argument. Usage: secenvs set KEY [VALUE] [--base64]");let i=r[2];await ye(s,i,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 Pe(n,r);break}case"export":{let n=e.includes("--force");await Le(n);break}case"doctor":await Oe();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 values (requires --force)"),a(" doctor Health check: identity, file integrity, decryption"),a("");break}}catch(n){throw n instanceof p&&(R(n.message),process.exit(1)),n}}Ce().catch(e=>{R(e.message),process.exit(1)});
|
package/lib/index.cjs
ADDED
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
DecryptionError: () => DecryptionError,
|
|
34
|
+
EncryptionError: () => EncryptionError,
|
|
35
|
+
FileError: () => FileError,
|
|
36
|
+
IdentityNotFoundError: () => IdentityNotFoundError,
|
|
37
|
+
ParseError: () => ParseError,
|
|
38
|
+
SECENV_ERROR_CODES: () => SECENV_ERROR_CODES,
|
|
39
|
+
SecenvError: () => SecenvError,
|
|
40
|
+
SecenvSDK: () => SecenvSDK,
|
|
41
|
+
SecretNotFoundError: () => SecretNotFoundError,
|
|
42
|
+
ValidationError: () => ValidationError,
|
|
43
|
+
createSecenv: () => createSecenv,
|
|
44
|
+
env: () => env,
|
|
45
|
+
getDefaultKeyPath: () => getDefaultKeyPath,
|
|
46
|
+
getEnvPath: () => getEnvPath,
|
|
47
|
+
identityExists: () => identityExists,
|
|
48
|
+
loadIdentity: () => loadIdentity,
|
|
49
|
+
parseEnvFile: () => parseEnvFile
|
|
50
|
+
});
|
|
51
|
+
module.exports = __toCommonJS(index_exports);
|
|
52
|
+
|
|
53
|
+
// src/env.ts
|
|
54
|
+
var fs4 = __toESM(require("fs"), 1);
|
|
55
|
+
|
|
56
|
+
// src/age.ts
|
|
57
|
+
var age = __toESM(require("age-encryption"), 1);
|
|
58
|
+
var fs2 = __toESM(require("fs"), 1);
|
|
59
|
+
var path2 = __toESM(require("path"), 1);
|
|
60
|
+
var os = __toESM(require("os"), 1);
|
|
61
|
+
|
|
62
|
+
// src/errors.ts
|
|
63
|
+
var SECENV_ERROR_CODES = {
|
|
64
|
+
IDENTITY_NOT_FOUND: "IDENTITY_NOT_FOUND",
|
|
65
|
+
DECRYPTION_FAILED: "DECRYPTION_FAILED",
|
|
66
|
+
SECRET_NOT_FOUND: "SECRET_NOT_FOUND",
|
|
67
|
+
PARSE_ERROR: "PARSE_ERROR",
|
|
68
|
+
FILE_ERROR: "FILE_ERROR",
|
|
69
|
+
ENCRYPTION_FAILED: "ENCRYPTION_FAILED",
|
|
70
|
+
VALIDATION_ERROR: "VALIDATION_ERROR"
|
|
71
|
+
};
|
|
72
|
+
var SecenvError = class extends Error {
|
|
73
|
+
code;
|
|
74
|
+
constructor(code, message) {
|
|
75
|
+
super(message);
|
|
76
|
+
this.name = "SecenvError";
|
|
77
|
+
this.code = code;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
var ValidationError = class extends SecenvError {
|
|
81
|
+
constructor(message) {
|
|
82
|
+
super(SECENV_ERROR_CODES.VALIDATION_ERROR, message);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
var IdentityNotFoundError = class extends SecenvError {
|
|
86
|
+
constructor(path4) {
|
|
87
|
+
super(
|
|
88
|
+
SECENV_ERROR_CODES.IDENTITY_NOT_FOUND,
|
|
89
|
+
`Identity key not found at ${path4}. Run 'secenv init' to create one.`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
var DecryptionError = class extends SecenvError {
|
|
94
|
+
constructor(message = "Failed to decrypt value. Check identity key.") {
|
|
95
|
+
super(SECENV_ERROR_CODES.DECRYPTION_FAILED, message);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
var SecretNotFoundError = class extends SecenvError {
|
|
99
|
+
constructor(key) {
|
|
100
|
+
super(SECENV_ERROR_CODES.SECRET_NOT_FOUND, `Secret '${key}' not found in .secenvs or process.env.`);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
var ParseError = class extends SecenvError {
|
|
104
|
+
line;
|
|
105
|
+
raw;
|
|
106
|
+
constructor(line, raw, message) {
|
|
107
|
+
super(SECENV_ERROR_CODES.PARSE_ERROR, message);
|
|
108
|
+
this.line = line;
|
|
109
|
+
this.raw = raw;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
var FileError = class extends SecenvError {
|
|
113
|
+
constructor(message) {
|
|
114
|
+
super(SECENV_ERROR_CODES.FILE_ERROR, message);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
var EncryptionError = class extends SecenvError {
|
|
118
|
+
constructor(message = "Failed to encrypt value.") {
|
|
119
|
+
super(SECENV_ERROR_CODES.ENCRYPTION_FAILED, message);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// src/filesystem.ts
|
|
124
|
+
var fs = __toESM(require("fs"), 1);
|
|
125
|
+
var path = __toESM(require("path"), 1);
|
|
126
|
+
function safeReadFile(filePath) {
|
|
127
|
+
try {
|
|
128
|
+
const stats = fs.lstatSync(filePath);
|
|
129
|
+
if (stats.isSymbolicLink()) {
|
|
130
|
+
throw new FileError(`Symlink detected at ${filePath}. Security policy prohibits following symlinks.`);
|
|
131
|
+
}
|
|
132
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (error instanceof FileError) throw error;
|
|
135
|
+
throw new FileError(`Failed to read file ${filePath}: ${error.message}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function sanitizePath(filePath, baseDir) {
|
|
139
|
+
const absolutePath = path.resolve(filePath);
|
|
140
|
+
try {
|
|
141
|
+
if (fs.existsSync(filePath)) {
|
|
142
|
+
const stats = fs.lstatSync(filePath);
|
|
143
|
+
if (stats.isSymbolicLink()) {
|
|
144
|
+
throw new FileError(
|
|
145
|
+
`Symlink detected at ${filePath}. Security policy prohibits following symlinks.`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} catch (error) {
|
|
150
|
+
if (error instanceof FileError) throw error;
|
|
151
|
+
}
|
|
152
|
+
if (baseDir) {
|
|
153
|
+
const absoluteBaseDir = path.resolve(baseDir);
|
|
154
|
+
if (!absolutePath.startsWith(absoluteBaseDir)) {
|
|
155
|
+
throw new FileError(`Directory traversal detected: ${filePath} is outside of ${baseDir}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return absolutePath;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/age.ts
|
|
162
|
+
var SECENV_DIR = ".secenvs";
|
|
163
|
+
var KEYS_DIR = "keys";
|
|
164
|
+
var DEFAULT_KEY_FILE = "default.key";
|
|
165
|
+
function stream(a) {
|
|
166
|
+
return new ReadableStream({
|
|
167
|
+
start(controller) {
|
|
168
|
+
controller.enqueue(a);
|
|
169
|
+
controller.close();
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
async function readAll(stream2) {
|
|
174
|
+
if (!(stream2 instanceof ReadableStream)) {
|
|
175
|
+
throw new Error("readAll expects a ReadableStream<Uint8Array>");
|
|
176
|
+
}
|
|
177
|
+
return new Uint8Array(await new Response(stream2).arrayBuffer());
|
|
178
|
+
}
|
|
179
|
+
function getKeysDir() {
|
|
180
|
+
const baseDir = process.env.SECENV_HOME || os.homedir();
|
|
181
|
+
const sanitizedBase = sanitizePath(baseDir);
|
|
182
|
+
return path2.join(sanitizedBase, SECENV_DIR, KEYS_DIR);
|
|
183
|
+
}
|
|
184
|
+
function getDefaultKeyPath() {
|
|
185
|
+
return path2.join(getKeysDir(), DEFAULT_KEY_FILE);
|
|
186
|
+
}
|
|
187
|
+
async function loadIdentity() {
|
|
188
|
+
const keyPath = getDefaultKeyPath();
|
|
189
|
+
if (!fs2.existsSync(keyPath)) {
|
|
190
|
+
throw new IdentityNotFoundError(keyPath);
|
|
191
|
+
}
|
|
192
|
+
return fs2.readFileSync(keyPath, "utf-8");
|
|
193
|
+
}
|
|
194
|
+
async function decrypt(identity, encryptedMessage) {
|
|
195
|
+
try {
|
|
196
|
+
const decrypter = new age.Decrypter();
|
|
197
|
+
decrypter.addIdentity(identity);
|
|
198
|
+
const armoredStream = stream(Buffer.from(encryptedMessage, "base64"));
|
|
199
|
+
const decryptedStream = await decrypter.decrypt(armoredStream);
|
|
200
|
+
const decryptedBytes = await readAll(decryptedStream);
|
|
201
|
+
return new TextDecoder().decode(decryptedBytes);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
throw new DecryptionError(`Failed to decrypt value: ${error}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function identityExists() {
|
|
207
|
+
const keyPath = getDefaultKeyPath();
|
|
208
|
+
return fs2.existsSync(keyPath);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/parse.ts
|
|
212
|
+
var fs3 = __toESM(require("fs"), 1);
|
|
213
|
+
var path3 = __toESM(require("path"), 1);
|
|
214
|
+
|
|
215
|
+
// src/validators.ts
|
|
216
|
+
var KEY_REGEX = /^[A-Z0-9_]+$/;
|
|
217
|
+
var MAX_VALUE_SIZE = 5 * 1024 * 1024;
|
|
218
|
+
var WINDOWS_RESERVED_NAMES = /* @__PURE__ */ new Set([
|
|
219
|
+
"CON",
|
|
220
|
+
"PRN",
|
|
221
|
+
"AUX",
|
|
222
|
+
"NUL",
|
|
223
|
+
"COM1",
|
|
224
|
+
"COM2",
|
|
225
|
+
"COM3",
|
|
226
|
+
"COM4",
|
|
227
|
+
"COM5",
|
|
228
|
+
"COM6",
|
|
229
|
+
"COM7",
|
|
230
|
+
"COM8",
|
|
231
|
+
"COM9",
|
|
232
|
+
"LPT1",
|
|
233
|
+
"LPT2",
|
|
234
|
+
"LPT3",
|
|
235
|
+
"LPT4",
|
|
236
|
+
"LPT5",
|
|
237
|
+
"LPT6",
|
|
238
|
+
"LPT7",
|
|
239
|
+
"LPT8",
|
|
240
|
+
"LPT9"
|
|
241
|
+
]);
|
|
242
|
+
function validateKey(key) {
|
|
243
|
+
if (!key) {
|
|
244
|
+
throw new ValidationError("Key cannot be empty");
|
|
245
|
+
}
|
|
246
|
+
if (!KEY_REGEX.test(key)) {
|
|
247
|
+
throw new ValidationError(
|
|
248
|
+
`Invalid key: '${key}'. Keys must only contain uppercase letters, numbers, and underscores (^[A-Z0-9_]+$).`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
if (WINDOWS_RESERVED_NAMES.has(key)) {
|
|
252
|
+
throw new ValidationError(`Invalid key: '${key}' is a reserved system name.`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// src/crypto-utils.ts
|
|
257
|
+
var import_node_crypto = require("crypto");
|
|
258
|
+
function constantTimeEqual(a, b) {
|
|
259
|
+
if (a.length !== b.length) {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
return (0, import_node_crypto.timingSafeEqual)(Buffer.from(a), Buffer.from(b));
|
|
263
|
+
}
|
|
264
|
+
function constantTimeHas(keys, target) {
|
|
265
|
+
let found = false;
|
|
266
|
+
const targetBuffer = Buffer.from(target);
|
|
267
|
+
for (const key of keys) {
|
|
268
|
+
const keyBuffer = Buffer.from(key);
|
|
269
|
+
if (keyBuffer.length === targetBuffer.length) {
|
|
270
|
+
if ((0, import_node_crypto.timingSafeEqual)(keyBuffer, targetBuffer)) {
|
|
271
|
+
found = true;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return found;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// src/parse.ts
|
|
279
|
+
var ENCRYPTED_PREFIX = "enc:age:";
|
|
280
|
+
function isEncryptedValue(value) {
|
|
281
|
+
return value.startsWith(ENCRYPTED_PREFIX);
|
|
282
|
+
}
|
|
283
|
+
function parseEnvFile(filePath) {
|
|
284
|
+
if (!fs3.existsSync(filePath)) {
|
|
285
|
+
return { lines: [], keys: /* @__PURE__ */ new Set(), encryptedCount: 0, plaintextCount: 0 };
|
|
286
|
+
}
|
|
287
|
+
let content = safeReadFile(filePath);
|
|
288
|
+
if (content.startsWith("\uFEFF")) {
|
|
289
|
+
content = content.slice(1);
|
|
290
|
+
}
|
|
291
|
+
const lines = content.split("\n");
|
|
292
|
+
const parsedLines = [];
|
|
293
|
+
const keys = /* @__PURE__ */ new Set();
|
|
294
|
+
let encryptedCount = 0;
|
|
295
|
+
let plaintextCount = 0;
|
|
296
|
+
for (let i = 0; i < lines.length; i++) {
|
|
297
|
+
const lineNumber = i + 1;
|
|
298
|
+
const raw = lines[i];
|
|
299
|
+
const trimmed = raw.trim();
|
|
300
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
301
|
+
parsedLines.push({
|
|
302
|
+
key: "",
|
|
303
|
+
value: trimmed,
|
|
304
|
+
encrypted: false,
|
|
305
|
+
lineNumber,
|
|
306
|
+
raw
|
|
307
|
+
});
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
const eqIndex = trimmed.indexOf("=");
|
|
311
|
+
if (eqIndex === -1) {
|
|
312
|
+
throw new ParseError(lineNumber, raw, `Invalid line: missing '=' separator`);
|
|
313
|
+
}
|
|
314
|
+
const key = trimmed.slice(0, eqIndex);
|
|
315
|
+
const value = trimmed.slice(eqIndex + 1);
|
|
316
|
+
if (!key) {
|
|
317
|
+
throw new ParseError(lineNumber, raw, `Invalid line: missing key before '='`);
|
|
318
|
+
}
|
|
319
|
+
validateKey(key);
|
|
320
|
+
if (keys.has(key)) {
|
|
321
|
+
throw new ParseError(lineNumber, raw, `Duplicate key '${key}'`);
|
|
322
|
+
}
|
|
323
|
+
const encrypted = isEncryptedValue(value);
|
|
324
|
+
parsedLines.push({
|
|
325
|
+
key,
|
|
326
|
+
value,
|
|
327
|
+
encrypted,
|
|
328
|
+
lineNumber,
|
|
329
|
+
raw
|
|
330
|
+
});
|
|
331
|
+
keys.add(key);
|
|
332
|
+
if (encrypted) {
|
|
333
|
+
encryptedCount++;
|
|
334
|
+
} else {
|
|
335
|
+
plaintextCount++;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
lines: parsedLines,
|
|
340
|
+
keys,
|
|
341
|
+
encryptedCount,
|
|
342
|
+
plaintextCount
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
function findKey(env2, key) {
|
|
346
|
+
let result = null;
|
|
347
|
+
for (const line of env2.lines) {
|
|
348
|
+
if (constantTimeEqual(line.key, key)) {
|
|
349
|
+
result = line;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return result;
|
|
353
|
+
}
|
|
354
|
+
function getEnvPath() {
|
|
355
|
+
return path3.join(process.cwd(), ".secenvs");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// src/env.ts
|
|
359
|
+
var SecenvSDK = class {
|
|
360
|
+
#identity = null;
|
|
361
|
+
#identityPromise = null;
|
|
362
|
+
#cache = /* @__PURE__ */ new Map();
|
|
363
|
+
#cacheTimestamp = 0;
|
|
364
|
+
#cacheSize = 0;
|
|
365
|
+
#envPath = "";
|
|
366
|
+
#parsedEnv = null;
|
|
367
|
+
constructor() {
|
|
368
|
+
this.#envPath = getEnvPath();
|
|
369
|
+
}
|
|
370
|
+
async loadIdentity() {
|
|
371
|
+
if (this.#identity) {
|
|
372
|
+
return this.#identity;
|
|
373
|
+
}
|
|
374
|
+
if (this.#identityPromise) {
|
|
375
|
+
return this.#identityPromise;
|
|
376
|
+
}
|
|
377
|
+
if (process.env.SECENV_ENCODED_IDENTITY) {
|
|
378
|
+
try {
|
|
379
|
+
const decoded = Buffer.from(process.env.SECENV_ENCODED_IDENTITY, "base64");
|
|
380
|
+
const privateKey = decoded.toString("utf-8");
|
|
381
|
+
this.#identity = privateKey;
|
|
382
|
+
return this.#identity;
|
|
383
|
+
} catch (error) {
|
|
384
|
+
throw new IdentityNotFoundError("SECENV_ENCODED_IDENTITY");
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (!identityExists()) {
|
|
388
|
+
throw new IdentityNotFoundError(getDefaultKeyPath());
|
|
389
|
+
}
|
|
390
|
+
this.#identityPromise = loadIdentity().then((identity) => {
|
|
391
|
+
this.#identity = identity;
|
|
392
|
+
this.#identityPromise = null;
|
|
393
|
+
return this.#identity;
|
|
394
|
+
});
|
|
395
|
+
return this.#identityPromise;
|
|
396
|
+
}
|
|
397
|
+
reloadEnv() {
|
|
398
|
+
if (!fs4.existsSync(this.#envPath)) {
|
|
399
|
+
this.#parsedEnv = null;
|
|
400
|
+
this.#cache.clear();
|
|
401
|
+
this.#cacheTimestamp = 0;
|
|
402
|
+
this.#cacheSize = 0;
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const stats = fs4.statSync(this.#envPath);
|
|
406
|
+
const newTimestamp = stats.mtimeMs;
|
|
407
|
+
const newSize = stats.size;
|
|
408
|
+
if (newTimestamp !== this.#cacheTimestamp || newSize !== this.#cacheSize) {
|
|
409
|
+
this.#parsedEnv = parseEnvFile(this.#envPath);
|
|
410
|
+
this.#cacheTimestamp = newTimestamp;
|
|
411
|
+
this.#cacheSize = newSize;
|
|
412
|
+
this.#cache.clear();
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
async get(key) {
|
|
416
|
+
let envValue = void 0;
|
|
417
|
+
for (const k in process.env) {
|
|
418
|
+
if (k === key) {
|
|
419
|
+
envValue = process.env[k];
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
if (envValue !== void 0) {
|
|
423
|
+
return envValue;
|
|
424
|
+
}
|
|
425
|
+
this.reloadEnv();
|
|
426
|
+
let cachedValue = void 0;
|
|
427
|
+
for (const [k, entry] of this.#cache.entries()) {
|
|
428
|
+
if (k === key) {
|
|
429
|
+
cachedValue = entry.value;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (cachedValue !== void 0) {
|
|
433
|
+
return cachedValue;
|
|
434
|
+
}
|
|
435
|
+
if (!this.#parsedEnv) {
|
|
436
|
+
throw new SecretNotFoundError(key);
|
|
437
|
+
}
|
|
438
|
+
const line = findKey(this.#parsedEnv, key);
|
|
439
|
+
if (!line) {
|
|
440
|
+
throw new SecretNotFoundError(key);
|
|
441
|
+
}
|
|
442
|
+
if (!line.encrypted) {
|
|
443
|
+
const value = line.value;
|
|
444
|
+
this.#cache.set(key, { value, decryptedAt: Date.now() });
|
|
445
|
+
return value;
|
|
446
|
+
}
|
|
447
|
+
const identity = await this.loadIdentity();
|
|
448
|
+
const encryptedMessage = line.value.slice(ENCRYPTED_PREFIX.length);
|
|
449
|
+
const decrypted = await decrypt(identity, encryptedMessage);
|
|
450
|
+
this.#cache.set(key, { value: decrypted, decryptedAt: Date.now() });
|
|
451
|
+
return decrypted;
|
|
452
|
+
}
|
|
453
|
+
has(key) {
|
|
454
|
+
let found = false;
|
|
455
|
+
for (const k in process.env) {
|
|
456
|
+
if (k === key) {
|
|
457
|
+
found = true;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
this.reloadEnv();
|
|
461
|
+
if (this.#parsedEnv) {
|
|
462
|
+
if (constantTimeHas(this.#parsedEnv.keys, key)) {
|
|
463
|
+
found = true;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return found;
|
|
467
|
+
}
|
|
468
|
+
keys() {
|
|
469
|
+
const allKeys = new Set(Object.keys(process.env));
|
|
470
|
+
this.reloadEnv();
|
|
471
|
+
if (this.#parsedEnv) {
|
|
472
|
+
for (const key of this.#parsedEnv.keys) {
|
|
473
|
+
allKeys.add(key);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return Array.from(allKeys);
|
|
477
|
+
}
|
|
478
|
+
clearCache() {
|
|
479
|
+
this.#cache.clear();
|
|
480
|
+
this.#cacheTimestamp = 0;
|
|
481
|
+
this.#cacheSize = 0;
|
|
482
|
+
this.#parsedEnv = null;
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
var globalSDK = new SecenvSDK();
|
|
486
|
+
function createSecenv() {
|
|
487
|
+
return new SecenvSDK();
|
|
488
|
+
}
|
|
489
|
+
var env = globalSDK;
|
|
490
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
491
|
+
0 && (module.exports = {
|
|
492
|
+
DecryptionError,
|
|
493
|
+
EncryptionError,
|
|
494
|
+
FileError,
|
|
495
|
+
IdentityNotFoundError,
|
|
496
|
+
ParseError,
|
|
497
|
+
SECENV_ERROR_CODES,
|
|
498
|
+
SecenvError,
|
|
499
|
+
SecenvSDK,
|
|
500
|
+
SecretNotFoundError,
|
|
501
|
+
ValidationError,
|
|
502
|
+
createSecenv,
|
|
503
|
+
env,
|
|
504
|
+
getDefaultKeyPath,
|
|
505
|
+
getEnvPath,
|
|
506
|
+
identityExists,
|
|
507
|
+
loadIdentity,
|
|
508
|
+
parseEnvFile
|
|
509
|
+
});
|
package/lib/index.d.cts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
declare class SecenvSDK {
|
|
2
|
+
#private;
|
|
3
|
+
constructor();
|
|
4
|
+
private loadIdentity;
|
|
5
|
+
private reloadEnv;
|
|
6
|
+
get<T extends string = string>(key: string): Promise<T>;
|
|
7
|
+
has(key: string): boolean;
|
|
8
|
+
keys(): string[];
|
|
9
|
+
clearCache(): void;
|
|
10
|
+
}
|
|
11
|
+
declare function createSecenv(): SecenvSDK;
|
|
12
|
+
declare const env: {
|
|
13
|
+
[key: string]: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
declare const SECENV_ERROR_CODES: {
|
|
17
|
+
readonly IDENTITY_NOT_FOUND: "IDENTITY_NOT_FOUND";
|
|
18
|
+
readonly DECRYPTION_FAILED: "DECRYPTION_FAILED";
|
|
19
|
+
readonly SECRET_NOT_FOUND: "SECRET_NOT_FOUND";
|
|
20
|
+
readonly PARSE_ERROR: "PARSE_ERROR";
|
|
21
|
+
readonly FILE_ERROR: "FILE_ERROR";
|
|
22
|
+
readonly ENCRYPTION_FAILED: "ENCRYPTION_FAILED";
|
|
23
|
+
readonly VALIDATION_ERROR: "VALIDATION_ERROR";
|
|
24
|
+
};
|
|
25
|
+
type SecenvErrorCode = (typeof SECENV_ERROR_CODES)[keyof typeof SECENV_ERROR_CODES];
|
|
26
|
+
declare class SecenvError extends Error {
|
|
27
|
+
code: SecenvErrorCode;
|
|
28
|
+
constructor(code: SecenvErrorCode, message: string);
|
|
29
|
+
}
|
|
30
|
+
declare class ValidationError extends SecenvError {
|
|
31
|
+
constructor(message: string);
|
|
32
|
+
}
|
|
33
|
+
declare class IdentityNotFoundError extends SecenvError {
|
|
34
|
+
constructor(path: string);
|
|
35
|
+
}
|
|
36
|
+
declare class DecryptionError extends SecenvError {
|
|
37
|
+
constructor(message?: string);
|
|
38
|
+
}
|
|
39
|
+
declare class SecretNotFoundError extends SecenvError {
|
|
40
|
+
constructor(key: string);
|
|
41
|
+
}
|
|
42
|
+
declare class ParseError extends SecenvError {
|
|
43
|
+
line: number;
|
|
44
|
+
raw: string;
|
|
45
|
+
constructor(line: number, raw: string, message: string);
|
|
46
|
+
}
|
|
47
|
+
declare class FileError extends SecenvError {
|
|
48
|
+
constructor(message: string);
|
|
49
|
+
}
|
|
50
|
+
declare class EncryptionError extends SecenvError {
|
|
51
|
+
constructor(message?: string);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
declare function getDefaultKeyPath(): string;
|
|
55
|
+
declare function loadIdentity(): Promise<string>;
|
|
56
|
+
declare function identityExists(): boolean;
|
|
57
|
+
|
|
58
|
+
interface ParsedLine {
|
|
59
|
+
key: string;
|
|
60
|
+
value: string;
|
|
61
|
+
encrypted: boolean;
|
|
62
|
+
lineNumber: number;
|
|
63
|
+
raw: string;
|
|
64
|
+
}
|
|
65
|
+
interface ParsedEnv {
|
|
66
|
+
lines: ParsedLine[];
|
|
67
|
+
keys: Set<string>;
|
|
68
|
+
encryptedCount: number;
|
|
69
|
+
plaintextCount: number;
|
|
70
|
+
}
|
|
71
|
+
declare function parseEnvFile(filePath: string): ParsedEnv;
|
|
72
|
+
declare function getEnvPath(): string;
|
|
73
|
+
|
|
74
|
+
export { DecryptionError, EncryptionError, FileError, IdentityNotFoundError, ParseError, SECENV_ERROR_CODES, SecenvError, type SecenvErrorCode, SecenvSDK, SecretNotFoundError, ValidationError, createSecenv, env, getDefaultKeyPath, getEnvPath, identityExists, loadIdentity, parseEnvFile };
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
declare class SecenvSDK {
|
|
2
|
+
#private;
|
|
3
|
+
constructor();
|
|
4
|
+
private loadIdentity;
|
|
5
|
+
private reloadEnv;
|
|
6
|
+
get<T extends string = string>(key: string): Promise<T>;
|
|
7
|
+
has(key: string): boolean;
|
|
8
|
+
keys(): string[];
|
|
9
|
+
clearCache(): void;
|
|
10
|
+
}
|
|
11
|
+
declare function createSecenv(): SecenvSDK;
|
|
12
|
+
declare const env: {
|
|
13
|
+
[key: string]: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
declare const SECENV_ERROR_CODES: {
|
|
17
|
+
readonly IDENTITY_NOT_FOUND: "IDENTITY_NOT_FOUND";
|
|
18
|
+
readonly DECRYPTION_FAILED: "DECRYPTION_FAILED";
|
|
19
|
+
readonly SECRET_NOT_FOUND: "SECRET_NOT_FOUND";
|
|
20
|
+
readonly PARSE_ERROR: "PARSE_ERROR";
|
|
21
|
+
readonly FILE_ERROR: "FILE_ERROR";
|
|
22
|
+
readonly ENCRYPTION_FAILED: "ENCRYPTION_FAILED";
|
|
23
|
+
readonly VALIDATION_ERROR: "VALIDATION_ERROR";
|
|
24
|
+
};
|
|
25
|
+
type SecenvErrorCode = (typeof SECENV_ERROR_CODES)[keyof typeof SECENV_ERROR_CODES];
|
|
26
|
+
declare class SecenvError extends Error {
|
|
27
|
+
code: SecenvErrorCode;
|
|
28
|
+
constructor(code: SecenvErrorCode, message: string);
|
|
29
|
+
}
|
|
30
|
+
declare class ValidationError extends SecenvError {
|
|
31
|
+
constructor(message: string);
|
|
32
|
+
}
|
|
33
|
+
declare class IdentityNotFoundError extends SecenvError {
|
|
34
|
+
constructor(path: string);
|
|
35
|
+
}
|
|
36
|
+
declare class DecryptionError extends SecenvError {
|
|
37
|
+
constructor(message?: string);
|
|
38
|
+
}
|
|
39
|
+
declare class SecretNotFoundError extends SecenvError {
|
|
40
|
+
constructor(key: string);
|
|
41
|
+
}
|
|
42
|
+
declare class ParseError extends SecenvError {
|
|
43
|
+
line: number;
|
|
44
|
+
raw: string;
|
|
45
|
+
constructor(line: number, raw: string, message: string);
|
|
46
|
+
}
|
|
47
|
+
declare class FileError extends SecenvError {
|
|
48
|
+
constructor(message: string);
|
|
49
|
+
}
|
|
50
|
+
declare class EncryptionError extends SecenvError {
|
|
51
|
+
constructor(message?: string);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
declare function getDefaultKeyPath(): string;
|
|
55
|
+
declare function loadIdentity(): Promise<string>;
|
|
56
|
+
declare function identityExists(): boolean;
|
|
57
|
+
|
|
58
|
+
interface ParsedLine {
|
|
59
|
+
key: string;
|
|
60
|
+
value: string;
|
|
61
|
+
encrypted: boolean;
|
|
62
|
+
lineNumber: number;
|
|
63
|
+
raw: string;
|
|
64
|
+
}
|
|
65
|
+
interface ParsedEnv {
|
|
66
|
+
lines: ParsedLine[];
|
|
67
|
+
keys: Set<string>;
|
|
68
|
+
encryptedCount: number;
|
|
69
|
+
plaintextCount: number;
|
|
70
|
+
}
|
|
71
|
+
declare function parseEnvFile(filePath: string): ParsedEnv;
|
|
72
|
+
declare function getEnvPath(): string;
|
|
73
|
+
|
|
74
|
+
export { DecryptionError, EncryptionError, FileError, IdentityNotFoundError, ParseError, SECENV_ERROR_CODES, SecenvError, type SecenvErrorCode, SecenvSDK, SecretNotFoundError, ValidationError, createSecenv, env, getDefaultKeyPath, getEnvPath, identityExists, loadIdentity, parseEnvFile };
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
// src/env.ts
|
|
2
|
+
import * as fs4 from "fs";
|
|
3
|
+
|
|
4
|
+
// src/age.ts
|
|
5
|
+
import * as age from "age-encryption";
|
|
6
|
+
import * as fs2 from "fs";
|
|
7
|
+
import * as path2 from "path";
|
|
8
|
+
import * as os from "os";
|
|
9
|
+
|
|
10
|
+
// src/errors.ts
|
|
11
|
+
var SECENV_ERROR_CODES = {
|
|
12
|
+
IDENTITY_NOT_FOUND: "IDENTITY_NOT_FOUND",
|
|
13
|
+
DECRYPTION_FAILED: "DECRYPTION_FAILED",
|
|
14
|
+
SECRET_NOT_FOUND: "SECRET_NOT_FOUND",
|
|
15
|
+
PARSE_ERROR: "PARSE_ERROR",
|
|
16
|
+
FILE_ERROR: "FILE_ERROR",
|
|
17
|
+
ENCRYPTION_FAILED: "ENCRYPTION_FAILED",
|
|
18
|
+
VALIDATION_ERROR: "VALIDATION_ERROR"
|
|
19
|
+
};
|
|
20
|
+
var SecenvError = class extends Error {
|
|
21
|
+
code;
|
|
22
|
+
constructor(code, message) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = "SecenvError";
|
|
25
|
+
this.code = code;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
var ValidationError = class extends SecenvError {
|
|
29
|
+
constructor(message) {
|
|
30
|
+
super(SECENV_ERROR_CODES.VALIDATION_ERROR, message);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
var IdentityNotFoundError = class extends SecenvError {
|
|
34
|
+
constructor(path4) {
|
|
35
|
+
super(
|
|
36
|
+
SECENV_ERROR_CODES.IDENTITY_NOT_FOUND,
|
|
37
|
+
`Identity key not found at ${path4}. Run 'secenv init' to create one.`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
var DecryptionError = class extends SecenvError {
|
|
42
|
+
constructor(message = "Failed to decrypt value. Check identity key.") {
|
|
43
|
+
super(SECENV_ERROR_CODES.DECRYPTION_FAILED, message);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
var SecretNotFoundError = class extends SecenvError {
|
|
47
|
+
constructor(key) {
|
|
48
|
+
super(SECENV_ERROR_CODES.SECRET_NOT_FOUND, `Secret '${key}' not found in .secenvs or process.env.`);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
var ParseError = class extends SecenvError {
|
|
52
|
+
line;
|
|
53
|
+
raw;
|
|
54
|
+
constructor(line, raw, message) {
|
|
55
|
+
super(SECENV_ERROR_CODES.PARSE_ERROR, message);
|
|
56
|
+
this.line = line;
|
|
57
|
+
this.raw = raw;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
var FileError = class extends SecenvError {
|
|
61
|
+
constructor(message) {
|
|
62
|
+
super(SECENV_ERROR_CODES.FILE_ERROR, message);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
var EncryptionError = class extends SecenvError {
|
|
66
|
+
constructor(message = "Failed to encrypt value.") {
|
|
67
|
+
super(SECENV_ERROR_CODES.ENCRYPTION_FAILED, message);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// src/filesystem.ts
|
|
72
|
+
import * as fs from "fs";
|
|
73
|
+
import * as path from "path";
|
|
74
|
+
function safeReadFile(filePath) {
|
|
75
|
+
try {
|
|
76
|
+
const stats = fs.lstatSync(filePath);
|
|
77
|
+
if (stats.isSymbolicLink()) {
|
|
78
|
+
throw new FileError(`Symlink detected at ${filePath}. Security policy prohibits following symlinks.`);
|
|
79
|
+
}
|
|
80
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (error instanceof FileError) throw error;
|
|
83
|
+
throw new FileError(`Failed to read file ${filePath}: ${error.message}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function sanitizePath(filePath, baseDir) {
|
|
87
|
+
const absolutePath = path.resolve(filePath);
|
|
88
|
+
try {
|
|
89
|
+
if (fs.existsSync(filePath)) {
|
|
90
|
+
const stats = fs.lstatSync(filePath);
|
|
91
|
+
if (stats.isSymbolicLink()) {
|
|
92
|
+
throw new FileError(
|
|
93
|
+
`Symlink detected at ${filePath}. Security policy prohibits following symlinks.`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (error instanceof FileError) throw error;
|
|
99
|
+
}
|
|
100
|
+
if (baseDir) {
|
|
101
|
+
const absoluteBaseDir = path.resolve(baseDir);
|
|
102
|
+
if (!absolutePath.startsWith(absoluteBaseDir)) {
|
|
103
|
+
throw new FileError(`Directory traversal detected: ${filePath} is outside of ${baseDir}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return absolutePath;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/age.ts
|
|
110
|
+
var SECENV_DIR = ".secenvs";
|
|
111
|
+
var KEYS_DIR = "keys";
|
|
112
|
+
var DEFAULT_KEY_FILE = "default.key";
|
|
113
|
+
function stream(a) {
|
|
114
|
+
return new ReadableStream({
|
|
115
|
+
start(controller) {
|
|
116
|
+
controller.enqueue(a);
|
|
117
|
+
controller.close();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
async function readAll(stream2) {
|
|
122
|
+
if (!(stream2 instanceof ReadableStream)) {
|
|
123
|
+
throw new Error("readAll expects a ReadableStream<Uint8Array>");
|
|
124
|
+
}
|
|
125
|
+
return new Uint8Array(await new Response(stream2).arrayBuffer());
|
|
126
|
+
}
|
|
127
|
+
function getKeysDir() {
|
|
128
|
+
const baseDir = process.env.SECENV_HOME || os.homedir();
|
|
129
|
+
const sanitizedBase = sanitizePath(baseDir);
|
|
130
|
+
return path2.join(sanitizedBase, SECENV_DIR, KEYS_DIR);
|
|
131
|
+
}
|
|
132
|
+
function getDefaultKeyPath() {
|
|
133
|
+
return path2.join(getKeysDir(), DEFAULT_KEY_FILE);
|
|
134
|
+
}
|
|
135
|
+
async function loadIdentity() {
|
|
136
|
+
const keyPath = getDefaultKeyPath();
|
|
137
|
+
if (!fs2.existsSync(keyPath)) {
|
|
138
|
+
throw new IdentityNotFoundError(keyPath);
|
|
139
|
+
}
|
|
140
|
+
return fs2.readFileSync(keyPath, "utf-8");
|
|
141
|
+
}
|
|
142
|
+
async function decrypt(identity, encryptedMessage) {
|
|
143
|
+
try {
|
|
144
|
+
const decrypter = new age.Decrypter();
|
|
145
|
+
decrypter.addIdentity(identity);
|
|
146
|
+
const armoredStream = stream(Buffer.from(encryptedMessage, "base64"));
|
|
147
|
+
const decryptedStream = await decrypter.decrypt(armoredStream);
|
|
148
|
+
const decryptedBytes = await readAll(decryptedStream);
|
|
149
|
+
return new TextDecoder().decode(decryptedBytes);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
throw new DecryptionError(`Failed to decrypt value: ${error}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function identityExists() {
|
|
155
|
+
const keyPath = getDefaultKeyPath();
|
|
156
|
+
return fs2.existsSync(keyPath);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/parse.ts
|
|
160
|
+
import * as fs3 from "fs";
|
|
161
|
+
import * as path3 from "path";
|
|
162
|
+
|
|
163
|
+
// src/validators.ts
|
|
164
|
+
var KEY_REGEX = /^[A-Z0-9_]+$/;
|
|
165
|
+
var MAX_VALUE_SIZE = 5 * 1024 * 1024;
|
|
166
|
+
var WINDOWS_RESERVED_NAMES = /* @__PURE__ */ new Set([
|
|
167
|
+
"CON",
|
|
168
|
+
"PRN",
|
|
169
|
+
"AUX",
|
|
170
|
+
"NUL",
|
|
171
|
+
"COM1",
|
|
172
|
+
"COM2",
|
|
173
|
+
"COM3",
|
|
174
|
+
"COM4",
|
|
175
|
+
"COM5",
|
|
176
|
+
"COM6",
|
|
177
|
+
"COM7",
|
|
178
|
+
"COM8",
|
|
179
|
+
"COM9",
|
|
180
|
+
"LPT1",
|
|
181
|
+
"LPT2",
|
|
182
|
+
"LPT3",
|
|
183
|
+
"LPT4",
|
|
184
|
+
"LPT5",
|
|
185
|
+
"LPT6",
|
|
186
|
+
"LPT7",
|
|
187
|
+
"LPT8",
|
|
188
|
+
"LPT9"
|
|
189
|
+
]);
|
|
190
|
+
function validateKey(key) {
|
|
191
|
+
if (!key) {
|
|
192
|
+
throw new ValidationError("Key cannot be empty");
|
|
193
|
+
}
|
|
194
|
+
if (!KEY_REGEX.test(key)) {
|
|
195
|
+
throw new ValidationError(
|
|
196
|
+
`Invalid key: '${key}'. Keys must only contain uppercase letters, numbers, and underscores (^[A-Z0-9_]+$).`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
if (WINDOWS_RESERVED_NAMES.has(key)) {
|
|
200
|
+
throw new ValidationError(`Invalid key: '${key}' is a reserved system name.`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// src/crypto-utils.ts
|
|
205
|
+
import { timingSafeEqual } from "crypto";
|
|
206
|
+
function constantTimeEqual(a, b) {
|
|
207
|
+
if (a.length !== b.length) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
return timingSafeEqual(Buffer.from(a), Buffer.from(b));
|
|
211
|
+
}
|
|
212
|
+
function constantTimeHas(keys, target) {
|
|
213
|
+
let found = false;
|
|
214
|
+
const targetBuffer = Buffer.from(target);
|
|
215
|
+
for (const key of keys) {
|
|
216
|
+
const keyBuffer = Buffer.from(key);
|
|
217
|
+
if (keyBuffer.length === targetBuffer.length) {
|
|
218
|
+
if (timingSafeEqual(keyBuffer, targetBuffer)) {
|
|
219
|
+
found = true;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return found;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// src/parse.ts
|
|
227
|
+
var ENCRYPTED_PREFIX = "enc:age:";
|
|
228
|
+
function isEncryptedValue(value) {
|
|
229
|
+
return value.startsWith(ENCRYPTED_PREFIX);
|
|
230
|
+
}
|
|
231
|
+
function parseEnvFile(filePath) {
|
|
232
|
+
if (!fs3.existsSync(filePath)) {
|
|
233
|
+
return { lines: [], keys: /* @__PURE__ */ new Set(), encryptedCount: 0, plaintextCount: 0 };
|
|
234
|
+
}
|
|
235
|
+
let content = safeReadFile(filePath);
|
|
236
|
+
if (content.startsWith("\uFEFF")) {
|
|
237
|
+
content = content.slice(1);
|
|
238
|
+
}
|
|
239
|
+
const lines = content.split("\n");
|
|
240
|
+
const parsedLines = [];
|
|
241
|
+
const keys = /* @__PURE__ */ new Set();
|
|
242
|
+
let encryptedCount = 0;
|
|
243
|
+
let plaintextCount = 0;
|
|
244
|
+
for (let i = 0; i < lines.length; i++) {
|
|
245
|
+
const lineNumber = i + 1;
|
|
246
|
+
const raw = lines[i];
|
|
247
|
+
const trimmed = raw.trim();
|
|
248
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
249
|
+
parsedLines.push({
|
|
250
|
+
key: "",
|
|
251
|
+
value: trimmed,
|
|
252
|
+
encrypted: false,
|
|
253
|
+
lineNumber,
|
|
254
|
+
raw
|
|
255
|
+
});
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
const eqIndex = trimmed.indexOf("=");
|
|
259
|
+
if (eqIndex === -1) {
|
|
260
|
+
throw new ParseError(lineNumber, raw, `Invalid line: missing '=' separator`);
|
|
261
|
+
}
|
|
262
|
+
const key = trimmed.slice(0, eqIndex);
|
|
263
|
+
const value = trimmed.slice(eqIndex + 1);
|
|
264
|
+
if (!key) {
|
|
265
|
+
throw new ParseError(lineNumber, raw, `Invalid line: missing key before '='`);
|
|
266
|
+
}
|
|
267
|
+
validateKey(key);
|
|
268
|
+
if (keys.has(key)) {
|
|
269
|
+
throw new ParseError(lineNumber, raw, `Duplicate key '${key}'`);
|
|
270
|
+
}
|
|
271
|
+
const encrypted = isEncryptedValue(value);
|
|
272
|
+
parsedLines.push({
|
|
273
|
+
key,
|
|
274
|
+
value,
|
|
275
|
+
encrypted,
|
|
276
|
+
lineNumber,
|
|
277
|
+
raw
|
|
278
|
+
});
|
|
279
|
+
keys.add(key);
|
|
280
|
+
if (encrypted) {
|
|
281
|
+
encryptedCount++;
|
|
282
|
+
} else {
|
|
283
|
+
plaintextCount++;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return {
|
|
287
|
+
lines: parsedLines,
|
|
288
|
+
keys,
|
|
289
|
+
encryptedCount,
|
|
290
|
+
plaintextCount
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function findKey(env2, key) {
|
|
294
|
+
let result = null;
|
|
295
|
+
for (const line of env2.lines) {
|
|
296
|
+
if (constantTimeEqual(line.key, key)) {
|
|
297
|
+
result = line;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
function getEnvPath() {
|
|
303
|
+
return path3.join(process.cwd(), ".secenvs");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// src/env.ts
|
|
307
|
+
var SecenvSDK = class {
|
|
308
|
+
#identity = null;
|
|
309
|
+
#identityPromise = null;
|
|
310
|
+
#cache = /* @__PURE__ */ new Map();
|
|
311
|
+
#cacheTimestamp = 0;
|
|
312
|
+
#cacheSize = 0;
|
|
313
|
+
#envPath = "";
|
|
314
|
+
#parsedEnv = null;
|
|
315
|
+
constructor() {
|
|
316
|
+
this.#envPath = getEnvPath();
|
|
317
|
+
}
|
|
318
|
+
async loadIdentity() {
|
|
319
|
+
if (this.#identity) {
|
|
320
|
+
return this.#identity;
|
|
321
|
+
}
|
|
322
|
+
if (this.#identityPromise) {
|
|
323
|
+
return this.#identityPromise;
|
|
324
|
+
}
|
|
325
|
+
if (process.env.SECENV_ENCODED_IDENTITY) {
|
|
326
|
+
try {
|
|
327
|
+
const decoded = Buffer.from(process.env.SECENV_ENCODED_IDENTITY, "base64");
|
|
328
|
+
const privateKey = decoded.toString("utf-8");
|
|
329
|
+
this.#identity = privateKey;
|
|
330
|
+
return this.#identity;
|
|
331
|
+
} catch (error) {
|
|
332
|
+
throw new IdentityNotFoundError("SECENV_ENCODED_IDENTITY");
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (!identityExists()) {
|
|
336
|
+
throw new IdentityNotFoundError(getDefaultKeyPath());
|
|
337
|
+
}
|
|
338
|
+
this.#identityPromise = loadIdentity().then((identity) => {
|
|
339
|
+
this.#identity = identity;
|
|
340
|
+
this.#identityPromise = null;
|
|
341
|
+
return this.#identity;
|
|
342
|
+
});
|
|
343
|
+
return this.#identityPromise;
|
|
344
|
+
}
|
|
345
|
+
reloadEnv() {
|
|
346
|
+
if (!fs4.existsSync(this.#envPath)) {
|
|
347
|
+
this.#parsedEnv = null;
|
|
348
|
+
this.#cache.clear();
|
|
349
|
+
this.#cacheTimestamp = 0;
|
|
350
|
+
this.#cacheSize = 0;
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const stats = fs4.statSync(this.#envPath);
|
|
354
|
+
const newTimestamp = stats.mtimeMs;
|
|
355
|
+
const newSize = stats.size;
|
|
356
|
+
if (newTimestamp !== this.#cacheTimestamp || newSize !== this.#cacheSize) {
|
|
357
|
+
this.#parsedEnv = parseEnvFile(this.#envPath);
|
|
358
|
+
this.#cacheTimestamp = newTimestamp;
|
|
359
|
+
this.#cacheSize = newSize;
|
|
360
|
+
this.#cache.clear();
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
async get(key) {
|
|
364
|
+
let envValue = void 0;
|
|
365
|
+
for (const k in process.env) {
|
|
366
|
+
if (k === key) {
|
|
367
|
+
envValue = process.env[k];
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (envValue !== void 0) {
|
|
371
|
+
return envValue;
|
|
372
|
+
}
|
|
373
|
+
this.reloadEnv();
|
|
374
|
+
let cachedValue = void 0;
|
|
375
|
+
for (const [k, entry] of this.#cache.entries()) {
|
|
376
|
+
if (k === key) {
|
|
377
|
+
cachedValue = entry.value;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (cachedValue !== void 0) {
|
|
381
|
+
return cachedValue;
|
|
382
|
+
}
|
|
383
|
+
if (!this.#parsedEnv) {
|
|
384
|
+
throw new SecretNotFoundError(key);
|
|
385
|
+
}
|
|
386
|
+
const line = findKey(this.#parsedEnv, key);
|
|
387
|
+
if (!line) {
|
|
388
|
+
throw new SecretNotFoundError(key);
|
|
389
|
+
}
|
|
390
|
+
if (!line.encrypted) {
|
|
391
|
+
const value = line.value;
|
|
392
|
+
this.#cache.set(key, { value, decryptedAt: Date.now() });
|
|
393
|
+
return value;
|
|
394
|
+
}
|
|
395
|
+
const identity = await this.loadIdentity();
|
|
396
|
+
const encryptedMessage = line.value.slice(ENCRYPTED_PREFIX.length);
|
|
397
|
+
const decrypted = await decrypt(identity, encryptedMessage);
|
|
398
|
+
this.#cache.set(key, { value: decrypted, decryptedAt: Date.now() });
|
|
399
|
+
return decrypted;
|
|
400
|
+
}
|
|
401
|
+
has(key) {
|
|
402
|
+
let found = false;
|
|
403
|
+
for (const k in process.env) {
|
|
404
|
+
if (k === key) {
|
|
405
|
+
found = true;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
this.reloadEnv();
|
|
409
|
+
if (this.#parsedEnv) {
|
|
410
|
+
if (constantTimeHas(this.#parsedEnv.keys, key)) {
|
|
411
|
+
found = true;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return found;
|
|
415
|
+
}
|
|
416
|
+
keys() {
|
|
417
|
+
const allKeys = new Set(Object.keys(process.env));
|
|
418
|
+
this.reloadEnv();
|
|
419
|
+
if (this.#parsedEnv) {
|
|
420
|
+
for (const key of this.#parsedEnv.keys) {
|
|
421
|
+
allKeys.add(key);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return Array.from(allKeys);
|
|
425
|
+
}
|
|
426
|
+
clearCache() {
|
|
427
|
+
this.#cache.clear();
|
|
428
|
+
this.#cacheTimestamp = 0;
|
|
429
|
+
this.#cacheSize = 0;
|
|
430
|
+
this.#parsedEnv = null;
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
var globalSDK = new SecenvSDK();
|
|
434
|
+
function createSecenv() {
|
|
435
|
+
return new SecenvSDK();
|
|
436
|
+
}
|
|
437
|
+
var env = globalSDK;
|
|
438
|
+
export {
|
|
439
|
+
DecryptionError,
|
|
440
|
+
EncryptionError,
|
|
441
|
+
FileError,
|
|
442
|
+
IdentityNotFoundError,
|
|
443
|
+
ParseError,
|
|
444
|
+
SECENV_ERROR_CODES,
|
|
445
|
+
SecenvError,
|
|
446
|
+
SecenvSDK,
|
|
447
|
+
SecretNotFoundError,
|
|
448
|
+
ValidationError,
|
|
449
|
+
createSecenv,
|
|
450
|
+
env,
|
|
451
|
+
getDefaultKeyPath,
|
|
452
|
+
getEnvPath,
|
|
453
|
+
identityExists,
|
|
454
|
+
loadIdentity,
|
|
455
|
+
parseEnvFile
|
|
456
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "secenvs",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The Breeze: Secret management without the overhead",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/secenv-org/secenvs.git"
|
|
8
|
+
},
|
|
9
|
+
"bugs": {
|
|
10
|
+
"url": "https://github.com/secenv-org/secenvs/issues"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/secenv-org/secenvs#readme",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"bin": {
|
|
15
|
+
"secenvs": "./bin/secenvs"
|
|
16
|
+
},
|
|
17
|
+
"main": "./lib/index.js",
|
|
18
|
+
"types": "./lib/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./lib/index.d.ts",
|
|
22
|
+
"import": "./lib/index.js",
|
|
23
|
+
"require": "./lib/index.cjs"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"bin",
|
|
28
|
+
"lib",
|
|
29
|
+
"README.md"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "bun run scripts/build.js",
|
|
33
|
+
"prepare": "bun run scripts/build.js",
|
|
34
|
+
"test": "npm run build && node --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand",
|
|
35
|
+
"test:unit": "node --experimental-vm-modules node_modules/jest/bin/jest.js tests/unit",
|
|
36
|
+
"test:integration": "npm run build && node --experimental-vm-modules node_modules/jest/bin/jest.js tests/integration",
|
|
37
|
+
"test:e2e": "npm run build && node --experimental-vm-modules node_modules/jest/bin/jest.js tests/e2e --runInBand",
|
|
38
|
+
"test:performance": "npm run build && node --experimental-vm-modules node_modules/jest/bin/jest.js tests/performance --runInBand",
|
|
39
|
+
"test:security": "npm run build && node --experimental-vm-modules node_modules/jest/bin/jest.js tests/security",
|
|
40
|
+
"test:coverage": "npm run build && node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
|
|
41
|
+
"dev": "tsc --watch",
|
|
42
|
+
"prepublishOnly": "npm run build",
|
|
43
|
+
"pretty": "prettier --config .prettierrc --write './src/*'"
|
|
44
|
+
},
|
|
45
|
+
"keywords": [
|
|
46
|
+
"secrets",
|
|
47
|
+
"encryption",
|
|
48
|
+
"environment",
|
|
49
|
+
"age",
|
|
50
|
+
"dotenv"
|
|
51
|
+
],
|
|
52
|
+
"author": "",
|
|
53
|
+
"license": "MIT",
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"age-encryption": "^0.3.0"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@types/jest": "^29.5.5",
|
|
59
|
+
"@types/node": "^20.10.0",
|
|
60
|
+
"esbuild": "^0.19.0",
|
|
61
|
+
"execa": "^6.1.0",
|
|
62
|
+
"jest": "^29.7.0",
|
|
63
|
+
"ts-jest": "^29.1.1",
|
|
64
|
+
"tsup": "^8.5.1",
|
|
65
|
+
"typescript": "^5.3.0"
|
|
66
|
+
}
|
|
67
|
+
}
|