keyra-cli 0.1.0 → 0.1.1

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.
@@ -1,118 +0,0 @@
1
- import { Command } from 'commander';
2
- import chalk from 'chalk';
3
- import { requireAuth, requireProjectConfig } from '../lib/config.js';
4
- import { upsertVaultEntries, getProjects } from '../lib/api.js';
5
- import { readEnvFile } from '../lib/env-file.js';
6
- import { encrypt, decrypt } from '../lib/encryption.js';
7
- import { success, error, warning, spinner, promptSecret } from '../lib/ui.js';
8
-
9
- export const pushCommand = new Command('push')
10
- .description('Push local .env to vault')
11
- .action(async () => {
12
- let config;
13
- try {
14
- config = requireAuth();
15
- } catch (err: any) {
16
- error(err.message);
17
- return;
18
- }
19
-
20
- let projectConfig;
21
- try {
22
- projectConfig = requireProjectConfig();
23
- } catch (err: any) {
24
- error(err.message);
25
- return;
26
- }
27
-
28
- const envData = readEnvFile();
29
- if (!envData) {
30
- warning('No .env file found in current directory.');
31
- return;
32
- }
33
-
34
- const vars = envData.vars;
35
- const varCount = Object.keys(vars).length;
36
-
37
- if (varCount === 0) {
38
- warning('No variables found in .env file.');
39
- return;
40
- }
41
-
42
- // Check if project has a password
43
- let projectPassword: string | null = null;
44
- try {
45
- const projects = await getProjects();
46
- const project = projects.find((p) => p.id === projectConfig.projectId);
47
- if (project?.has_project_password) {
48
- projectPassword = await promptSecret('Project password: ');
49
- const blob = JSON.parse(project.project_password_salt as string) as { encrypted: string; iv: string; salt: string; authTag: string };
50
- try {
51
- const result = decrypt(blob.encrypted, blob.iv, blob.salt, blob.authTag, projectPassword);
52
- if (result !== 'keyra-verified') {
53
- error('Incorrect project password');
54
- return;
55
- }
56
- } catch {
57
- error('Incorrect project password');
58
- return;
59
- }
60
- }
61
- } catch (err: any) {
62
- error('Failed to fetch project info: ' + err.message);
63
- return;
64
- }
65
-
66
- const s = spinner('Encrypting and uploading ' + varCount + ' variable' + (varCount !== 1 ? 's' : '') + '...');
67
- s.start();
68
-
69
- try {
70
- const entries = Object.entries(vars).map(([key, value]) => {
71
- let encrypted;
72
- if (projectPassword) {
73
- // Double encrypt: inner with project password, outer with vault passphrase
74
- const inner = encrypt(value, projectPassword);
75
- const innerJson = JSON.stringify(inner);
76
- encrypted = encrypt(innerJson, config.passphrase);
77
- } else {
78
- encrypted = encrypt(value, config.passphrase);
79
- }
80
- return {
81
- key_name: key,
82
- encrypted_value: encrypted.encrypted,
83
- iv: encrypted.iv,
84
- salt: encrypted.salt,
85
- auth_tag: encrypted.authTag,
86
- category: guessCategory(key),
87
- };
88
- });
89
-
90
- await upsertVaultEntries(projectConfig.projectId, entries);
91
- s.stop();
92
- success(
93
- 'Pushed ' + varCount + ' variable' + (varCount !== 1 ? 's' : '') + ' to vault ' + chalk.dim('(' + projectConfig.projectName + ')')
94
- );
95
- } catch (err: any) {
96
- s.stop();
97
- error(err.message);
98
- }
99
- });
100
-
101
- function guessCategory(key: string): string {
102
- const k = key.toUpperCase();
103
- if (k.includes('OPENAI') || k.includes('ANTHROPIC') || k.includes('AI'))
104
- return 'ai';
105
- if (k.includes('DATABASE') || k.includes('DB_') || k.includes('POSTGRES') || k.includes('MYSQL') || k.includes('MONGO') || k.includes('REDIS') || k.includes('SUPABASE'))
106
- return 'database';
107
- if (k.includes('AUTH') || k.includes('JWT') || k.includes('SESSION') || k.includes('NEXTAUTH') || k.includes('CLERK'))
108
- return 'auth';
109
- if (k.includes('STRIPE') || k.includes('PAYMENT') || k.includes('PAYPAL'))
110
- return 'payment';
111
- if (k.includes('VERCEL') || k.includes('AWS') || k.includes('GCP') || k.includes('AZURE') || k.includes('HEROKU'))
112
- return 'hosting';
113
- if (k.includes('SMTP') || k.includes('EMAIL') || k.includes('SENDGRID') || k.includes('RESEND') || k.includes('MAILGUN'))
114
- return 'email';
115
- if (k.includes('ANALYTICS') || k.includes('MIXPANEL') || k.includes('GA_') || k.includes('POSTHOG') || k.includes('SENTRY'))
116
- return 'analytics';
117
- return 'general';
118
- }
@@ -1,145 +0,0 @@
1
- import { Command } from 'commander';
2
- import fs from 'fs';
3
- import path from 'path';
4
- import crypto from 'crypto';
5
- import chalk from 'chalk';
6
- import { success, error, warning, info, spinner, printBox } from '../lib/ui.js';
7
- import { requireAuth } from '../lib/config.js';
8
- import { decrypt } from '../lib/encryption.js';
9
- import { apiGet } from '../lib/api.js';
10
-
11
- async function checkHibp(value: string): Promise<number> {
12
- const hash = crypto.createHash('sha1').update(value).digest('hex').toUpperCase();
13
- const prefix = hash.slice(0, 5);
14
- const suffix = hash.slice(5);
15
-
16
- try {
17
- const res = await fetch(`https://api.pwnedpasswords.com/range/${prefix}`, {
18
- headers: { 'Add-Padding': 'true' },
19
- });
20
- if (!res.ok) return 0;
21
- const text = await res.text();
22
- for (const line of text.split('\n')) {
23
- const [lineSuffix, countStr] = line.trim().split(':');
24
- if (lineSuffix === suffix) return parseInt(countStr, 10) || 0;
25
- }
26
- return 0;
27
- } catch {
28
- return 0;
29
- }
30
- }
31
-
32
- async function parseEnvFile(filePath: string): Promise<Record<string, string>> {
33
- const content = fs.readFileSync(filePath, 'utf-8');
34
- const result: Record<string, string> = {};
35
- for (const line of content.split('\n')) {
36
- const trimmed = line.trim();
37
- if (!trimmed || trimmed.startsWith('#')) continue;
38
- const eqIdx = trimmed.indexOf('=');
39
- if (eqIdx === -1) continue;
40
- const key = trimmed.slice(0, eqIdx).trim();
41
- const value = trimmed.slice(eqIdx + 1).trim().replace(/^['"]|['"]$/g, '');
42
- if (key && value) result[key] = value;
43
- }
44
- return result;
45
- }
46
-
47
- export const scanCommand = new Command('scan')
48
- .description('Check secrets for known data breaches using HaveIBeenPwned')
49
- .option('-f, --file <path>', 'Path to .env file to scan', '.env')
50
- .option('--project <id>', 'Scan a vault project instead of a local file')
51
- .action(async (opts) => {
52
- const s = spinner('Preparing scan...').start();
53
-
54
- let secrets: Record<string, string> = {};
55
-
56
- if (opts.project) {
57
- // Scan from vault
58
- const config = requireAuth();
59
- try {
60
- const entries = await apiGet(`/api/vault?project_id=${opts.project}`);
61
- for (const entry of entries) {
62
- try {
63
- const value = decrypt(
64
- entry.encrypted_value,
65
- entry.iv,
66
- entry.salt,
67
- entry.auth_tag,
68
- config.passphrase
69
- );
70
- secrets[entry.key_name] = value;
71
- } catch {
72
- // skip undecrytable entries
73
- }
74
- }
75
- } catch (err: any) {
76
- s.fail('Failed to fetch vault entries.');
77
- error(err.message);
78
- process.exit(1);
79
- }
80
- } else {
81
- // Scan local .env file
82
- const filePath = path.resolve(process.cwd(), opts.file);
83
- if (!fs.existsSync(filePath)) {
84
- s.fail(`File not found: ${filePath}`);
85
- process.exit(1);
86
- }
87
- try {
88
- secrets = await parseEnvFile(filePath);
89
- } catch {
90
- s.fail(`Failed to read ${opts.file}`);
91
- process.exit(1);
92
- }
93
- }
94
-
95
- const keys = Object.keys(secrets);
96
- if (keys.length === 0) {
97
- s.stop();
98
- info('No secrets found to scan.');
99
- return;
100
- }
101
-
102
- s.text = `Scanning ${keys.length} secret${keys.length === 1 ? '' : 's'} against breach databases...`;
103
-
104
- const breached: { key: string; count: number }[] = [];
105
-
106
- for (const key of keys) {
107
- const value = secrets[key];
108
- // Skip empty or very short values (env flags, etc.)
109
- if (!value || value.length < 8) continue;
110
- const count = await checkHibp(value);
111
- if (count > 0) {
112
- breached.push({ key, count });
113
- }
114
- }
115
-
116
- s.stop();
117
- console.log('');
118
-
119
- if (breached.length === 0) {
120
- printBox(
121
- chalk.green.bold('No breaches detected') +
122
- '\n\n' +
123
- chalk.dim(`Scanned ${keys.length} secret${keys.length === 1 ? '' : 's'} · 0 found in breach databases`),
124
- 'green'
125
- );
126
- } else {
127
- printBox(
128
- chalk.red.bold(`${breached.length} secret${breached.length === 1 ? '' : 's'} found in breach databases`) +
129
- '\n\n' +
130
- breached
131
- .map(
132
- ({ key, count }) =>
133
- chalk.red('✗') +
134
- ' ' +
135
- chalk.bold(key) +
136
- chalk.dim(` — seen ${count.toLocaleString()} times`)
137
- )
138
- .join('\n') +
139
- '\n\n' +
140
- chalk.yellow('Rotate these secrets immediately.'),
141
- 'red'
142
- );
143
- process.exit(1);
144
- }
145
- });
@@ -1,84 +0,0 @@
1
- import { Command } from 'commander';
2
- import crypto from 'crypto';
3
- import chalk from 'chalk';
4
- import { requireAuth, requireProjectConfig } from '../lib/config.js';
5
- import { createShareLink } from '../lib/api.js';
6
- import { readEnvFile, toEnvFileString } from '../lib/env-file.js';
7
- import { encrypt } from '../lib/encryption.js';
8
- import { error, warning, spinner, printBox } from '../lib/ui.js';
9
-
10
- export const shareCommand = new Command('share')
11
- .description('Create an encrypted share link for your .env')
12
- .action(async () => {
13
- let config;
14
- try {
15
- config = requireAuth();
16
- } catch (err: any) {
17
- error(err.message);
18
- return;
19
- }
20
-
21
- let projectConfig;
22
- try {
23
- projectConfig = requireProjectConfig();
24
- } catch (err: any) {
25
- error(err.message);
26
- return;
27
- }
28
-
29
- const envData = readEnvFile();
30
- if (!envData) {
31
- warning('No .env file found in current directory.');
32
- return;
33
- }
34
-
35
- const varCount = Object.keys(envData.vars).length;
36
- if (varCount === 0) {
37
- warning('No variables found in .env file.');
38
- return;
39
- }
40
-
41
- const s = spinner('Creating share link...');
42
- s.start();
43
-
44
- try {
45
- // Generate a one-time passphrase (NOT the user's vault passphrase)
46
- const oneTimeKey = crypto.randomBytes(32).toString('hex');
47
-
48
- // Encrypt the entire .env content with the one-time key
49
- const envContent = toEnvFileString(envData.vars);
50
- const encrypted = encrypt(envContent, oneTimeKey);
51
-
52
- const result = await createShareLink({
53
- project_id: projectConfig.projectId,
54
- encrypted_data: encrypted.encrypted,
55
- iv: encrypted.iv,
56
- salt: encrypted.salt,
57
- auth_tag: encrypted.authTag,
58
- });
59
-
60
- s.stop();
61
-
62
- const shareUrl = result.url || `${config.apiUrl}/share/${result.token}`;
63
-
64
- printBox(
65
- chalk.green.bold('Share link created!') +
66
- chalk.dim(` (${varCount} variables)`) +
67
- '\n\n' +
68
- chalk.white.bold('Link ') +
69
- chalk.dim('(expires in 24h, viewable once):') +
70
- '\n' +
71
- chalk.cyan(shareUrl) +
72
- '\n\n' +
73
- chalk.white.bold('Decryption key ') +
74
- chalk.dim('(send separately!):') +
75
- '\n' +
76
- chalk.yellow(oneTimeKey) +
77
- '\n\n' +
78
- chalk.yellow('⚠ The link and key should be sent via different channels for security.')
79
- );
80
- } catch (err: any) {
81
- s.stop();
82
- error(err.message);
83
- }
84
- });
@@ -1,91 +0,0 @@
1
- import { Command } from 'commander';
2
- import chalk from 'chalk';
3
- import {
4
- isLoggedIn,
5
- requireAuth,
6
- loadProjectConfig,
7
- } from '../lib/config.js';
8
- import { getVaultEntries } from '../lib/api.js';
9
- import { readEnvFile } from '../lib/env-file.js';
10
- import { error, info, spinner } from '../lib/ui.js';
11
-
12
- export const statusCommand = new Command('status')
13
- .description('Show current project status')
14
- .action(async () => {
15
- const projectConfig = loadProjectConfig();
16
-
17
- if (!projectConfig) {
18
- info('Not in a Keyra project. Run `keyra init` first.');
19
- return;
20
- }
21
-
22
- console.log();
23
- console.log(chalk.bold(' Keyra Status'));
24
- console.log(chalk.dim(` ${'─'.repeat(40)}`));
25
- console.log(` ${chalk.dim('Project:')} ${chalk.green(projectConfig.projectName)}`);
26
- console.log(` ${chalk.dim('ID:')} ${chalk.dim(projectConfig.projectId)}`);
27
-
28
- if (!isLoggedIn()) {
29
- console.log();
30
- info('Not logged in. Run `keyra login` to sync.');
31
- return;
32
- }
33
-
34
- let config;
35
- try {
36
- config = requireAuth();
37
- } catch (err: any) {
38
- error(err.message);
39
- return;
40
- }
41
-
42
- const s = spinner('Checking vault...');
43
- s.start();
44
-
45
- try {
46
- const entries = await getVaultEntries(projectConfig.projectId);
47
- s.stop();
48
-
49
- console.log(` ${chalk.dim('Vault:')} ${entries.length} variable${entries.length !== 1 ? 's' : ''}`);
50
-
51
- // Compare with local .env
52
- const envData = readEnvFile();
53
- if (envData) {
54
- const localKeys = new Set(Object.keys(envData.vars));
55
- const vaultKeys = new Set(entries.map((e) => e.key_name));
56
-
57
- const onlyLocal = [...localKeys].filter((k) => !vaultKeys.has(k));
58
- const onlyVault = [...vaultKeys].filter((k) => !localKeys.has(k));
59
- const inBoth = [...localKeys].filter((k) => vaultKeys.has(k));
60
-
61
- console.log(` ${chalk.dim('Local:')} ${localKeys.size} variable${localKeys.size !== 1 ? 's' : ''} (${envData.filename})`);
62
- console.log();
63
-
64
- if (onlyLocal.length === 0 && onlyVault.length === 0) {
65
- console.log(chalk.green(' ✓ Local and vault keys are in sync'));
66
- } else {
67
- if (onlyLocal.length > 0) {
68
- console.log(
69
- chalk.yellow(
70
- ` ⚠ ${onlyLocal.length} key${onlyLocal.length !== 1 ? 's' : ''} only in local: ${onlyLocal.join(', ')}`
71
- )
72
- );
73
- }
74
- if (onlyVault.length > 0) {
75
- console.log(
76
- chalk.yellow(
77
- ` ⚠ ${onlyVault.length} key${onlyVault.length !== 1 ? 's' : ''} only in vault: ${onlyVault.join(', ')}`
78
- )
79
- );
80
- }
81
- }
82
- } else {
83
- console.log(` ${chalk.dim('Local:')} ${chalk.yellow('no .env file found')}`);
84
- }
85
-
86
- console.log();
87
- } catch (err: any) {
88
- s.stop();
89
- error(err.message);
90
- }
91
- });
@@ -1,101 +0,0 @@
1
- import { Command } from 'commander';
2
- import chalk from 'chalk';
3
- import { requireAuth, requireProjectConfig } from '../lib/config.js';
4
- import { getVaultEntries } from '../lib/api.js';
5
- import { readEnvFile } from '../lib/env-file.js';
6
- import { error, warning, spinner, printTable } from '../lib/ui.js';
7
-
8
- export const validateCommand = new Command('validate')
9
- .description('Check local .env has all required vault variables')
10
- .action(async () => {
11
- let projectConfig;
12
- try {
13
- projectConfig = requireProjectConfig();
14
- } catch (err: any) {
15
- error(err.message);
16
- process.exit(1);
17
- return;
18
- }
19
-
20
- // Need auth to fetch vault key names
21
- let config;
22
- try {
23
- config = requireAuth();
24
- } catch (err: any) {
25
- error(err.message);
26
- process.exit(1);
27
- return;
28
- }
29
-
30
- const s = spinner('Checking vault...');
31
- s.start();
32
-
33
- let entries;
34
- try {
35
- entries = await getVaultEntries(projectConfig.projectId);
36
- s.stop();
37
- } catch (err: any) {
38
- s.stop();
39
- error(err.message);
40
- process.exit(1);
41
- return;
42
- }
43
-
44
- if (entries.length === 0) {
45
- warning('No variables in vault to validate against.');
46
- return;
47
- }
48
-
49
- const envData = readEnvFile();
50
- const localVars = envData?.vars ?? {};
51
-
52
- const rows: { key: string; value: string; status: string }[] = [];
53
- let passed = 0;
54
- let issues = 0;
55
-
56
- for (const entry of entries) {
57
- const localValue = localVars[entry.key_name];
58
- if (localValue === undefined) {
59
- rows.push({
60
- key: entry.key_name,
61
- value: 'MISSING',
62
- status: 'missing',
63
- });
64
- issues++;
65
- } else if (localValue.trim() === '') {
66
- rows.push({
67
- key: entry.key_name,
68
- value: 'EMPTY',
69
- status: 'empty',
70
- });
71
- issues++;
72
- } else {
73
- rows.push({
74
- key: entry.key_name,
75
- value: 'present',
76
- status: 'present',
77
- });
78
- passed++;
79
- }
80
- }
81
-
82
- console.log();
83
- printTable(rows);
84
- console.log();
85
-
86
- const total = entries.length;
87
- if (issues === 0) {
88
- console.log(
89
- chalk.green.bold(` ${passed} of ${total} checks passed. All good!`)
90
- );
91
- } else {
92
- console.log(
93
- chalk.yellow(
94
- ` ${passed} of ${total} checks passed. ${issues} issue${issues !== 1 ? 's' : ''} found.`
95
- )
96
- );
97
- }
98
-
99
- console.log();
100
- process.exit(issues > 0 ? 1 : 0);
101
- });
package/cli/src/index.ts DELETED
@@ -1,38 +0,0 @@
1
- #!/usr/bin/env node
2
- import { Command } from 'commander';
3
- import chalk from 'chalk';
4
- import { loginCommand } from './commands/login.js';
5
- import { logoutCommand } from './commands/logout.js';
6
- import { initCommand } from './commands/init.js';
7
- import { pushCommand } from './commands/push.js';
8
- import { pullCommand } from './commands/pull.js';
9
- import { validateCommand } from './commands/validate.js';
10
- import { shareCommand } from './commands/share.js';
11
- import { listCommand } from './commands/list.js';
12
- import { statusCommand } from './commands/status.js';
13
- import { guardCommand } from './commands/guard.js';
14
- import { scanCommand } from './commands/scan.js';
15
-
16
- const program = new Command();
17
-
18
- program
19
- .name('keyra')
20
- .description('Encrypted .env vault. Sync, share, never lose an API key.')
21
- .version('0.1.0');
22
-
23
- program.addCommand(loginCommand);
24
- program.addCommand(logoutCommand);
25
- program.addCommand(initCommand);
26
- program.addCommand(pushCommand);
27
- program.addCommand(pullCommand);
28
- program.addCommand(validateCommand);
29
- program.addCommand(shareCommand);
30
- program.addCommand(listCommand);
31
- program.addCommand(statusCommand);
32
- program.addCommand(guardCommand);
33
- program.addCommand(scanCommand);
34
-
35
- program.parseAsync(process.argv).catch((err) => {
36
- console.error(chalk.red('Error:'), err.message);
37
- process.exit(1);
38
- });