dotsnap 1.0.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/.env +11 -0
- package/.env.safe +11 -0
- package/README.md +105 -0
- package/bin/dotsnap.js +29 -0
- package/package.json +39 -0
- package/src/commands/mask.js +60 -0
- package/src/commands/web.js +30 -0
- package/src/index.js +6 -0
- package/src/utils/fileSystem.js +28 -0
- package/src/utils/logger.js +24 -0
- package/src/utils/mask.js +56 -0
package/.env
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Production Environment
|
|
2
|
+
TIMOTHY_KEY=sk-live-1234567890abcdef
|
|
3
|
+
ABIGAIL_PASSWORD=myPassword123
|
|
4
|
+
STRIPE_SECRET_KEY=sk_test_51ABC123xyz789
|
|
5
|
+
DATABASE_URL=postgresql://user:pass@localhost:5432/db
|
|
6
|
+
JWT_SECRET=super-secret-jwt-token-12345
|
|
7
|
+
|
|
8
|
+
# AWS Configuration
|
|
9
|
+
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
|
|
10
|
+
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
|
|
11
|
+
NEXT_PUBLIC_APP_URL=https://myapp.com
|
package/.env.safe
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Production Environment
|
|
2
|
+
TIMOTHY_KEY=sk-*****************cdef
|
|
3
|
+
ABIGAIL_PASSWORD=myP******d123
|
|
4
|
+
STRIPE_SECRET_KEY=sk_***************z789
|
|
5
|
+
DATABASE_URL=pos*********************************2/db
|
|
6
|
+
JWT_SECRET=sup*********************2345
|
|
7
|
+
|
|
8
|
+
# AWS Configuration
|
|
9
|
+
AWS_ACCESS_KEY_ID=AKI*************MPLE
|
|
10
|
+
AWS_SECRET_ACCESS_KEY=wJa*********************************EKEY
|
|
11
|
+
NEXT_PUBLIC_APP_URL=htt**********.com
|
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# DotSnap CLI
|
|
2
|
+
|
|
3
|
+
Mask your .env files securely - from the command line.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
### Global Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g dotsnap
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### NPX (No Installation)
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx dotsnap mask
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
### Mask your .env file
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
dotsnap mask
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
This will:
|
|
28
|
+
|
|
29
|
+
- Read your `.env` file
|
|
30
|
+
- Mask all secret values
|
|
31
|
+
- Save to `.env.safe`
|
|
32
|
+
|
|
33
|
+
### Custom input/output
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
dotsnap mask -i .env.production -o .env.production.safe
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Fully hide values
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
dotsnap mask --fully-hide
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Open Web UI
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
dotsnap web
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Options
|
|
52
|
+
|
|
53
|
+
### `dotsnap mask`
|
|
54
|
+
|
|
55
|
+
| Option | Description | Default |
|
|
56
|
+
| --------------------- | --------------------------- | ---------------- |
|
|
57
|
+
| `-i, --input <file>` | Input .env file | `.env` |
|
|
58
|
+
| `-o, --output <file>` | Output file | `.env.safe` |
|
|
59
|
+
| `--fully-hide` | Hide all values completely | `false` |
|
|
60
|
+
| `--no-first-last` | Don't show first/last chars | Shows by default |
|
|
61
|
+
|
|
62
|
+
### `dotsnap web`
|
|
63
|
+
|
|
64
|
+
| Option | Description | Default |
|
|
65
|
+
| ------------------- | -------------- | ------- |
|
|
66
|
+
| `-p, --port <port>` | Port to run on | `3000` |
|
|
67
|
+
|
|
68
|
+
## Examples
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Basic usage
|
|
72
|
+
dotsnap mask
|
|
73
|
+
|
|
74
|
+
# Custom files
|
|
75
|
+
dotsnap mask -i .env.prod -o .env.prod.safe
|
|
76
|
+
|
|
77
|
+
# Fully hide all values
|
|
78
|
+
dotsnap mask --fully-hide
|
|
79
|
+
|
|
80
|
+
# Open web UI
|
|
81
|
+
dotsnap web
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Security
|
|
85
|
+
|
|
86
|
+
- 100% local processing
|
|
87
|
+
- No network requests
|
|
88
|
+
- No data collection
|
|
89
|
+
- Open source
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
MIT
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
# cli/.npmignore
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
node_modules/
|
|
101
|
+
\*.log
|
|
102
|
+
.DS_Store
|
|
103
|
+
test/
|
|
104
|
+
.git/
|
|
105
|
+
.github/
|
package/bin/dotsnap.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { program } = require("commander");
|
|
4
|
+
const chalk = require("chalk");
|
|
5
|
+
const packageJson = require("../package.json");
|
|
6
|
+
const { maskCommand } = require("../src/commands/mask");
|
|
7
|
+
const { webCommand } = require("../src/commands/web");
|
|
8
|
+
|
|
9
|
+
program
|
|
10
|
+
.name("dotsnap")
|
|
11
|
+
.description("Mask your .env files securely")
|
|
12
|
+
.version(packageJson.version);
|
|
13
|
+
|
|
14
|
+
program
|
|
15
|
+
.command("mask")
|
|
16
|
+
.description("Mask secrets in your .env file")
|
|
17
|
+
.option("-i, --input <file>", "Input .env file", ".env")
|
|
18
|
+
.option("-o, --output <file>", "Output file", ".env.safe")
|
|
19
|
+
.option("--fully-hide", "Completely hide all values with ****")
|
|
20
|
+
.option("--no-first-last", "Don't show first/last characters")
|
|
21
|
+
.action(maskCommand);
|
|
22
|
+
|
|
23
|
+
program
|
|
24
|
+
.command("web")
|
|
25
|
+
.description("Open the web UI in your browser")
|
|
26
|
+
.option("-p, --port <port>", "Port to run on", "3000")
|
|
27
|
+
.action(webCommand);
|
|
28
|
+
|
|
29
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dotsnap",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Mask your .env files securely - CLI and Web UI",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"dotsnap": "bin/dotsnap.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "echo \"No tests yet\" && exit 0"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"env",
|
|
14
|
+
"environment",
|
|
15
|
+
"secrets",
|
|
16
|
+
"security",
|
|
17
|
+
"mask",
|
|
18
|
+
"dotenv",
|
|
19
|
+
"cli"
|
|
20
|
+
],
|
|
21
|
+
"author": "Your Name <Timothy Okoduwa>",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/timothy-okoduwa/dotsnap.git"
|
|
26
|
+
},
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/timothy-okoduwa/dotsnap/issues"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/timothy-okoduwa/dotsnap#readme",
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"chalk": "^4.1.2",
|
|
33
|
+
"commander": "^11.1.0",
|
|
34
|
+
"ora": "^5.4.1"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=14.0.0"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const chalk = require("chalk");
|
|
2
|
+
const ora = require("ora");
|
|
3
|
+
const {
|
|
4
|
+
readEnvFile,
|
|
5
|
+
writeEnvFile,
|
|
6
|
+
fileExists,
|
|
7
|
+
} = require("../utils/fileSystem");
|
|
8
|
+
const { processEnvContent } = require("../utils/mask");
|
|
9
|
+
const { logSuccess, logError, logInfo } = require("../utils/logger");
|
|
10
|
+
|
|
11
|
+
async function maskCommand(options) {
|
|
12
|
+
const spinner = ora("Processing .env file...").start();
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
// Check if input file exists
|
|
16
|
+
if (!fileExists(options.input)) {
|
|
17
|
+
spinner.fail();
|
|
18
|
+
logError(`File not found: ${options.input}`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Read the .env file
|
|
23
|
+
spinner.text = "Reading .env file...";
|
|
24
|
+
const content = readEnvFile(options.input);
|
|
25
|
+
|
|
26
|
+
// Count secrets
|
|
27
|
+
const secretCount = content
|
|
28
|
+
.split("\n")
|
|
29
|
+
.filter(
|
|
30
|
+
(line) => !line.trim().startsWith("#") && line.includes("="),
|
|
31
|
+
).length;
|
|
32
|
+
|
|
33
|
+
// Mask the content
|
|
34
|
+
spinner.text = "Masking secrets...";
|
|
35
|
+
const showFirstLast = options.firstLast !== false;
|
|
36
|
+
const fullyHide = options.fullyHide || false;
|
|
37
|
+
const maskedContent = processEnvContent(content, showFirstLast, fullyHide);
|
|
38
|
+
|
|
39
|
+
// Write to output file
|
|
40
|
+
spinner.text = "Saving masked file...";
|
|
41
|
+
writeEnvFile(options.output, maskedContent);
|
|
42
|
+
|
|
43
|
+
spinner.succeed();
|
|
44
|
+
|
|
45
|
+
// Success messages
|
|
46
|
+
console.log("");
|
|
47
|
+
logSuccess(`Found .env file`);
|
|
48
|
+
logSuccess(`Masked ${secretCount} secrets`);
|
|
49
|
+
logSuccess(`Saved to ${options.output}`);
|
|
50
|
+
console.log("");
|
|
51
|
+
logInfo(`You can now safely share ${chalk.cyan(options.output)}`);
|
|
52
|
+
console.log("");
|
|
53
|
+
} catch (error) {
|
|
54
|
+
spinner.fail();
|
|
55
|
+
logError(`Failed to mask secrets: ${error.message}`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { maskCommand };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const chalk = require("chalk");
|
|
2
|
+
const { logInfo, logSuccess, logError } = require("../utils/logger");
|
|
3
|
+
|
|
4
|
+
async function webCommand(options) {
|
|
5
|
+
const port = options.port || 3000;
|
|
6
|
+
|
|
7
|
+
console.log("");
|
|
8
|
+
logInfo("Opening DotSnap Web UI...");
|
|
9
|
+
console.log("");
|
|
10
|
+
|
|
11
|
+
// Try to open the hosted web version
|
|
12
|
+
const url = "https://dotsnap.com/tool"; // Replace with your actual domain
|
|
13
|
+
|
|
14
|
+
logSuccess(`Opening browser at: ${chalk.cyan(url)}`);
|
|
15
|
+
console.log("");
|
|
16
|
+
logInfo("If the browser doesn't open automatically, visit:");
|
|
17
|
+
console.log(` ${chalk.cyan(url)}`);
|
|
18
|
+
console.log("");
|
|
19
|
+
|
|
20
|
+
// Try to open browser
|
|
21
|
+
const open = require("open");
|
|
22
|
+
try {
|
|
23
|
+
await open(url);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
logError("Could not open browser automatically");
|
|
26
|
+
logInfo(`Please visit: ${chalk.cyan(url)}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = { webCommand };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
function fileExists(filePath) {
|
|
5
|
+
return fs.existsSync(filePath);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function readEnvFile(filePath) {
|
|
9
|
+
try {
|
|
10
|
+
return fs.readFileSync(filePath, "utf8");
|
|
11
|
+
} catch (error) {
|
|
12
|
+
throw new Error(`Failed to read file: ${error.message}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function writeEnvFile(filePath, content) {
|
|
17
|
+
try {
|
|
18
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
19
|
+
} catch (error) {
|
|
20
|
+
throw new Error(`Failed to write file: ${error.message}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
fileExists,
|
|
26
|
+
readEnvFile,
|
|
27
|
+
writeEnvFile,
|
|
28
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const chalk = require("chalk");
|
|
2
|
+
|
|
3
|
+
function logSuccess(message) {
|
|
4
|
+
console.log(chalk.green("✓"), message);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function logError(message) {
|
|
8
|
+
console.log(chalk.red("✗"), message);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function logInfo(message) {
|
|
12
|
+
console.log(chalk.blue("ℹ"), message);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function logWarning(message) {
|
|
16
|
+
console.log(chalk.yellow("⚠"), message);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = {
|
|
20
|
+
logSuccess,
|
|
21
|
+
logError,
|
|
22
|
+
logInfo,
|
|
23
|
+
logWarning,
|
|
24
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
function maskSecret(value, showFirstLast = true, fullyHide = false) {
|
|
2
|
+
if (!value) return value;
|
|
3
|
+
|
|
4
|
+
const unquoted = value.replace(/^["']|["']$/g, "");
|
|
5
|
+
|
|
6
|
+
if (fullyHide) {
|
|
7
|
+
return "****";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const len = unquoted.length;
|
|
11
|
+
if (len <= 4) {
|
|
12
|
+
return "****";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (!showFirstLast) {
|
|
16
|
+
return "*".repeat(len);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const firstChars = unquoted.slice(0, 3);
|
|
20
|
+
const lastChars = unquoted.slice(-4);
|
|
21
|
+
const maskedMiddle = "*".repeat(Math.max(len - 7, 4));
|
|
22
|
+
|
|
23
|
+
return firstChars + maskedMiddle + lastChars;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function processEnvContent(content, showFirstLast = true, fullyHide = false) {
|
|
27
|
+
const lines = content.split("\n");
|
|
28
|
+
|
|
29
|
+
return lines
|
|
30
|
+
.map((line) => {
|
|
31
|
+
if (!line.trim() || line.trim().startsWith("#")) {
|
|
32
|
+
return line;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const equalsIndex = line.indexOf("=");
|
|
36
|
+
if (equalsIndex === -1) {
|
|
37
|
+
return line;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const key = line.slice(0, equalsIndex).trim();
|
|
41
|
+
const value = line.slice(equalsIndex + 1).trim();
|
|
42
|
+
|
|
43
|
+
if (!value) {
|
|
44
|
+
return line;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const maskedValue = maskSecret(value, showFirstLast, fullyHide);
|
|
48
|
+
return `${key}=${maskedValue}`;
|
|
49
|
+
})
|
|
50
|
+
.join("\n");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = {
|
|
54
|
+
maskSecret,
|
|
55
|
+
processEnvContent,
|
|
56
|
+
};
|