keyra-cli 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/LICENSE +21 -0
- package/README.md +177 -0
- package/cli/package-lock.json +2575 -0
- package/cli/package.json +36 -0
- package/cli/src/commands/guard.ts +89 -0
- package/cli/src/commands/init.ts +172 -0
- package/cli/src/commands/list.ts +60 -0
- package/cli/src/commands/login.ts +116 -0
- package/cli/src/commands/logout.ts +10 -0
- package/cli/src/commands/pull.ts +94 -0
- package/cli/src/commands/push.ts +118 -0
- package/cli/src/commands/scan.ts +145 -0
- package/cli/src/commands/share.ts +84 -0
- package/cli/src/commands/status.ts +91 -0
- package/cli/src/commands/validate.ts +101 -0
- package/cli/src/index.ts +38 -0
- package/cli/src/lib/api.ts +136 -0
- package/cli/src/lib/config.ts +94 -0
- package/cli/src/lib/encryption.ts +45 -0
- package/cli/src/lib/env-file.ts +67 -0
- package/cli/src/lib/ui.ts +87 -0
- package/cli/src/types.ts +56 -0
- package/cli/tsconfig.json +14 -0
- package/package.json +38 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
|
|
3
|
+
export function encrypt(
|
|
4
|
+
text: string,
|
|
5
|
+
passphrase: string
|
|
6
|
+
): { encrypted: string; iv: string; salt: string; authTag: string } {
|
|
7
|
+
const salt = crypto.randomBytes(16);
|
|
8
|
+
const key = crypto.pbkdf2Sync(passphrase, salt, 100000, 32, 'sha256');
|
|
9
|
+
const iv = crypto.randomBytes(12);
|
|
10
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
11
|
+
let encrypted = cipher.update(text, 'utf8', 'hex');
|
|
12
|
+
encrypted += cipher.final('hex');
|
|
13
|
+
const authTag = cipher.getAuthTag().toString('hex');
|
|
14
|
+
return {
|
|
15
|
+
encrypted,
|
|
16
|
+
iv: iv.toString('hex'),
|
|
17
|
+
salt: salt.toString('hex'),
|
|
18
|
+
authTag,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function decrypt(
|
|
23
|
+
encrypted: string,
|
|
24
|
+
iv: string,
|
|
25
|
+
salt: string,
|
|
26
|
+
authTag: string,
|
|
27
|
+
passphrase: string
|
|
28
|
+
): string {
|
|
29
|
+
const key = crypto.pbkdf2Sync(
|
|
30
|
+
passphrase,
|
|
31
|
+
Buffer.from(salt, 'hex'),
|
|
32
|
+
100000,
|
|
33
|
+
32,
|
|
34
|
+
'sha256'
|
|
35
|
+
);
|
|
36
|
+
const decipher = crypto.createDecipheriv(
|
|
37
|
+
'aes-256-gcm',
|
|
38
|
+
key,
|
|
39
|
+
Buffer.from(iv, 'hex')
|
|
40
|
+
);
|
|
41
|
+
decipher.setAuthTag(Buffer.from(authTag, 'hex'));
|
|
42
|
+
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
43
|
+
decrypted += decipher.final('utf8');
|
|
44
|
+
return decrypted;
|
|
45
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export function parseEnvFile(content: string): Record<string, string> {
|
|
5
|
+
const vars: Record<string, string> = {};
|
|
6
|
+
const lines = content.split('\n');
|
|
7
|
+
|
|
8
|
+
for (const line of lines) {
|
|
9
|
+
const trimmed = line.trim();
|
|
10
|
+
// Skip empty lines and comments
|
|
11
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
12
|
+
|
|
13
|
+
const eqIndex = trimmed.indexOf('=');
|
|
14
|
+
if (eqIndex === -1) continue;
|
|
15
|
+
|
|
16
|
+
const key = trimmed.substring(0, eqIndex).trim();
|
|
17
|
+
let value = trimmed.substring(eqIndex + 1).trim();
|
|
18
|
+
|
|
19
|
+
// Remove surrounding quotes
|
|
20
|
+
if (
|
|
21
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
22
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
23
|
+
) {
|
|
24
|
+
value = value.slice(1, -1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (key) {
|
|
28
|
+
vars[key] = value;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return vars;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function toEnvFileString(vars: Record<string, string>): string {
|
|
36
|
+
const keys = Object.keys(vars).sort();
|
|
37
|
+
return keys.map((key) => `${key}=${vars[key]}`).join('\n') + '\n';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const ENV_FILES = ['.env', '.env.local', '.env.development'];
|
|
41
|
+
|
|
42
|
+
export function readEnvFile(dir?: string): {
|
|
43
|
+
vars: Record<string, string>;
|
|
44
|
+
filename: string;
|
|
45
|
+
} | null {
|
|
46
|
+
const baseDir = dir || process.cwd();
|
|
47
|
+
|
|
48
|
+
for (const filename of ENV_FILES) {
|
|
49
|
+
const filePath = path.join(baseDir, filename);
|
|
50
|
+
if (fs.existsSync(filePath)) {
|
|
51
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
52
|
+
return { vars: parseEnvFile(content), filename };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function writeEnvFile(
|
|
60
|
+
vars: Record<string, string>,
|
|
61
|
+
dir?: string,
|
|
62
|
+
filename = '.env'
|
|
63
|
+
): void {
|
|
64
|
+
const baseDir = dir || process.cwd();
|
|
65
|
+
const filePath = path.join(baseDir, filename);
|
|
66
|
+
fs.writeFileSync(filePath, toEnvFileString(vars));
|
|
67
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import readline from 'readline';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import boxen from 'boxen';
|
|
5
|
+
|
|
6
|
+
export const spinner = (text: string) => ora({ text, color: 'green' });
|
|
7
|
+
|
|
8
|
+
export const success = (text: string) =>
|
|
9
|
+
console.log(chalk.green('✓'), text);
|
|
10
|
+
|
|
11
|
+
export const error = (text: string) =>
|
|
12
|
+
console.log(chalk.red('✗'), text);
|
|
13
|
+
|
|
14
|
+
export const warning = (text: string) =>
|
|
15
|
+
console.log(chalk.yellow('⚠'), text);
|
|
16
|
+
|
|
17
|
+
export const info = (text: string) =>
|
|
18
|
+
console.log(chalk.blue('ℹ'), text);
|
|
19
|
+
|
|
20
|
+
export function banner() {
|
|
21
|
+
console.log(
|
|
22
|
+
boxen(
|
|
23
|
+
chalk.green.bold('Keyra') +
|
|
24
|
+
chalk.dim(' v0.1.0') +
|
|
25
|
+
'\n' +
|
|
26
|
+
chalk.dim('Encrypted .env vault'),
|
|
27
|
+
{
|
|
28
|
+
padding: 1,
|
|
29
|
+
margin: 1,
|
|
30
|
+
borderStyle: 'round',
|
|
31
|
+
borderColor: 'green',
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function printTable(
|
|
38
|
+
rows: { key: string; value: string; status?: string }[]
|
|
39
|
+
) {
|
|
40
|
+
const maxKeyLen = Math.max(...rows.map((r) => r.key.length), 3);
|
|
41
|
+
|
|
42
|
+
for (const row of rows) {
|
|
43
|
+
const dots = '.'.repeat(Math.max(1, maxKeyLen - row.key.length + 3));
|
|
44
|
+
const statusIcon =
|
|
45
|
+
row.status === 'present'
|
|
46
|
+
? chalk.green('✓')
|
|
47
|
+
: row.status === 'missing'
|
|
48
|
+
? chalk.red('✗')
|
|
49
|
+
: row.status === 'empty'
|
|
50
|
+
? chalk.red('✗')
|
|
51
|
+
: chalk.dim('·');
|
|
52
|
+
const statusText =
|
|
53
|
+
row.status === 'present'
|
|
54
|
+
? chalk.green(row.value)
|
|
55
|
+
: row.status === 'missing'
|
|
56
|
+
? chalk.red(row.value)
|
|
57
|
+
: row.status === 'empty'
|
|
58
|
+
? chalk.red(row.value)
|
|
59
|
+
: chalk.dim(row.value);
|
|
60
|
+
|
|
61
|
+
console.log(` ${statusIcon} ${chalk.bold(row.key)} ${chalk.dim(dots)} ${statusText}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function printBox(content: string, color: 'green' | 'yellow' | 'red' = 'green') {
|
|
66
|
+
console.log(
|
|
67
|
+
boxen(content, {
|
|
68
|
+
padding: 1,
|
|
69
|
+
margin: { top: 1, bottom: 1, left: 0, right: 0 },
|
|
70
|
+
borderStyle: 'round',
|
|
71
|
+
borderColor: color,
|
|
72
|
+
})
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function promptSecret(question: string): Promise<string> {
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
const rl = readline.createInterface({
|
|
79
|
+
input: process.stdin,
|
|
80
|
+
output: process.stdout,
|
|
81
|
+
});
|
|
82
|
+
rl.question(question, (answer) => {
|
|
83
|
+
rl.close();
|
|
84
|
+
resolve(answer);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
package/cli/src/types.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export interface Config {
|
|
2
|
+
apiUrl: string;
|
|
3
|
+
accessToken: string;
|
|
4
|
+
passphrase: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface ProjectConfig {
|
|
8
|
+
projectId: string;
|
|
9
|
+
projectName: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface Project {
|
|
13
|
+
id: string;
|
|
14
|
+
user_id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
description: string | null;
|
|
17
|
+
var_count: number;
|
|
18
|
+
last_synced_at: string | null;
|
|
19
|
+
created_at: string;
|
|
20
|
+
updated_at: string;
|
|
21
|
+
has_project_password: boolean;
|
|
22
|
+
project_password_salt: string | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface VaultEntry {
|
|
26
|
+
id: string;
|
|
27
|
+
user_id: string;
|
|
28
|
+
project_id: string;
|
|
29
|
+
key_name: string;
|
|
30
|
+
encrypted_value: string;
|
|
31
|
+
iv: string;
|
|
32
|
+
salt: string;
|
|
33
|
+
auth_tag: string;
|
|
34
|
+
category: string;
|
|
35
|
+
created_at: string;
|
|
36
|
+
updated_at: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface EncryptedEntry {
|
|
40
|
+
key_name: string;
|
|
41
|
+
encrypted_value: string;
|
|
42
|
+
iv: string;
|
|
43
|
+
salt: string;
|
|
44
|
+
auth_tag: string;
|
|
45
|
+
category: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ShareData {
|
|
49
|
+
project_id: string;
|
|
50
|
+
encrypted_data: string;
|
|
51
|
+
iv: string;
|
|
52
|
+
salt: string;
|
|
53
|
+
auth_tag: string;
|
|
54
|
+
expires_at?: string;
|
|
55
|
+
max_views?: number;
|
|
56
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"outDir": "dist",
|
|
9
|
+
"rootDir": "src",
|
|
10
|
+
"declaration": true,
|
|
11
|
+
"skipLibCheck": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"]
|
|
14
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "keyra-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"scripts": {
|
|
5
|
+
"dev": "next dev",
|
|
6
|
+
"build": "next build",
|
|
7
|
+
"start": "next start",
|
|
8
|
+
"lint": "eslint"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@supabase/ssr": "^0.8.0",
|
|
12
|
+
"@supabase/supabase-js": "^2.95.3",
|
|
13
|
+
"@vercel/analytics": "^1.6.1",
|
|
14
|
+
"class-variance-authority": "^0.7.1",
|
|
15
|
+
"clsx": "^2.1.1",
|
|
16
|
+
"lucide-react": "^0.564.0",
|
|
17
|
+
"next": "16.1.6",
|
|
18
|
+
"next-themes": "^0.4.6",
|
|
19
|
+
"radix-ui": "^1.4.3",
|
|
20
|
+
"react": "19.2.3",
|
|
21
|
+
"react-dom": "19.2.3",
|
|
22
|
+
"sonner": "^2.0.7",
|
|
23
|
+
"stripe": "^20.3.1",
|
|
24
|
+
"tailwind-merge": "^3.4.1"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@tailwindcss/postcss": "^4",
|
|
28
|
+
"@types/node": "^20",
|
|
29
|
+
"@types/react": "^19",
|
|
30
|
+
"@types/react-dom": "^19",
|
|
31
|
+
"eslint": "^9",
|
|
32
|
+
"eslint-config-next": "16.1.6",
|
|
33
|
+
"shadcn": "^3.8.5",
|
|
34
|
+
"tailwindcss": "^4",
|
|
35
|
+
"tw-animate-css": "^1.4.0",
|
|
36
|
+
"typescript": "^5"
|
|
37
|
+
}
|
|
38
|
+
}
|