@etree/cli 2.0.1 ā 2.0.3
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/dist/lib/crypto.d.ts +0 -15
- package/dist/lib/crypto.js +28 -152
- package/dist/lib/crypto.js.map +1 -1
- package/package.json +8 -3
- package/eslint.config.mjs +0 -4
- package/src/commands/auth.ts +0 -258
- package/src/commands/config.ts +0 -47
- package/src/commands/init.ts +0 -187
- package/src/commands/member.ts +0 -146
- package/src/commands/secret.ts +0 -381
- package/src/commands/shortcuts.ts +0 -25
- package/src/commands/wallet.ts +0 -97
- package/src/index.ts +0 -50
- package/src/lib/api.ts +0 -57
- package/src/lib/config.ts +0 -70
- package/src/lib/crypto.ts +0 -173
- package/src/lib/env-manager.ts +0 -60
- package/src/lib/key-store.ts +0 -51
- package/src/test-e2e.ts +0 -106
- package/tsconfig.json +0 -18
package/src/commands/init.ts
DELETED
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
import { Command } from "commander";
|
|
2
|
-
import inquirer from "inquirer";
|
|
3
|
-
import chalk from "chalk";
|
|
4
|
-
import ora from "ora";
|
|
5
|
-
import { apiRequest } from "../lib/api";
|
|
6
|
-
import { saveSession } from "../lib/config";
|
|
7
|
-
import { generateKeys, derivePublicKey, shutdown } from "../lib/crypto";
|
|
8
|
-
import { savePrivateKey, hasPrivateKey, getPrivateKey } from "../lib/key-store";
|
|
9
|
-
|
|
10
|
-
export function registerInitCommand(program: Command): void {
|
|
11
|
-
program
|
|
12
|
-
.command("init")
|
|
13
|
-
.description("First-time setup ā create an account or log in")
|
|
14
|
-
.action(async () => {
|
|
15
|
-
console.log(chalk.bold("\nš² Welcome to EnvTree!\n"));
|
|
16
|
-
|
|
17
|
-
const { action } = await inquirer.prompt([
|
|
18
|
-
{
|
|
19
|
-
type: "list",
|
|
20
|
-
name: "action",
|
|
21
|
-
message: "What would you like to do?",
|
|
22
|
-
choices: [
|
|
23
|
-
{ name: "Create a new account", value: "signup" },
|
|
24
|
-
{ name: "Log in to existing account", value: "login" },
|
|
25
|
-
],
|
|
26
|
-
},
|
|
27
|
-
]);
|
|
28
|
-
|
|
29
|
-
if (action === "signup") {
|
|
30
|
-
const answers = await inquirer.prompt([
|
|
31
|
-
{ type: "input", name: "email", message: "Email:" },
|
|
32
|
-
{ type: "input", name: "username", message: "Username:" },
|
|
33
|
-
{
|
|
34
|
-
type: "password",
|
|
35
|
-
name: "password",
|
|
36
|
-
message: "Password:",
|
|
37
|
-
mask: "*",
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
type: "password",
|
|
41
|
-
name: "confirmPassword",
|
|
42
|
-
message: "Confirm Password:",
|
|
43
|
-
mask: "*",
|
|
44
|
-
},
|
|
45
|
-
]);
|
|
46
|
-
|
|
47
|
-
if (answers.password !== answers.confirmPassword) {
|
|
48
|
-
console.log(chalk.red("\n Passwords do not match"));
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const spinner = ora("Creating account...").start();
|
|
53
|
-
try {
|
|
54
|
-
const result = await apiRequest(
|
|
55
|
-
"POST",
|
|
56
|
-
"/auth/signup",
|
|
57
|
-
{
|
|
58
|
-
email: answers.email,
|
|
59
|
-
password: answers.password,
|
|
60
|
-
username: answers.username,
|
|
61
|
-
},
|
|
62
|
-
false,
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
if (!result.session) {
|
|
66
|
-
spinner.succeed(
|
|
67
|
-
"Account created! Check your email, then run: et login",
|
|
68
|
-
);
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
saveSession({
|
|
73
|
-
access_token: result.session.access_token,
|
|
74
|
-
refresh_token: result.session.refresh_token,
|
|
75
|
-
user_id: result.user.id,
|
|
76
|
-
email: answers.email,
|
|
77
|
-
username: answers.username,
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
spinner.text = "Generating encryption keys...";
|
|
81
|
-
const keys = await generateKeys();
|
|
82
|
-
savePrivateKey(keys.private_key);
|
|
83
|
-
|
|
84
|
-
await apiRequest("PUT", "/profile/public-key", {
|
|
85
|
-
public_key: keys.public_key,
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
spinner.succeed(chalk.green("All set! You're ready to go."));
|
|
89
|
-
console.log(
|
|
90
|
-
chalk.dim(` User: ${answers.username} (${answers.email})`),
|
|
91
|
-
);
|
|
92
|
-
console.log("");
|
|
93
|
-
console.log(chalk.bold(" Next steps:"));
|
|
94
|
-
console.log(
|
|
95
|
-
chalk.cyan(" et wallet create my-project ") +
|
|
96
|
-
chalk.dim("Create a wallet"),
|
|
97
|
-
);
|
|
98
|
-
console.log(
|
|
99
|
-
chalk.cyan(' et push my-project API_KEY "sk-..." ') +
|
|
100
|
-
chalk.dim("Store a secret"),
|
|
101
|
-
);
|
|
102
|
-
console.log(
|
|
103
|
-
chalk.cyan(" et pull my-project ") +
|
|
104
|
-
chalk.dim("Sync to .env"),
|
|
105
|
-
);
|
|
106
|
-
console.log("");
|
|
107
|
-
} catch (err: any) {
|
|
108
|
-
spinner.fail(chalk.red(`Signup failed: ${err.message}`));
|
|
109
|
-
} finally {
|
|
110
|
-
shutdown();
|
|
111
|
-
}
|
|
112
|
-
} else {
|
|
113
|
-
// Login flow
|
|
114
|
-
const answers = await inquirer.prompt([
|
|
115
|
-
{ type: "input", name: "email", message: "Email:" },
|
|
116
|
-
{
|
|
117
|
-
type: "password",
|
|
118
|
-
name: "password",
|
|
119
|
-
message: "Password:",
|
|
120
|
-
mask: "*",
|
|
121
|
-
},
|
|
122
|
-
]);
|
|
123
|
-
|
|
124
|
-
const spinner = ora("Logging in...").start();
|
|
125
|
-
try {
|
|
126
|
-
const result = await apiRequest(
|
|
127
|
-
"POST",
|
|
128
|
-
"/auth/login",
|
|
129
|
-
{
|
|
130
|
-
email: answers.email,
|
|
131
|
-
password: answers.password,
|
|
132
|
-
},
|
|
133
|
-
false,
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
saveSession({
|
|
137
|
-
access_token: result.session.access_token,
|
|
138
|
-
refresh_token: result.session.refresh_token,
|
|
139
|
-
user_id: result.user.id,
|
|
140
|
-
email: result.user.email,
|
|
141
|
-
username: answers.email,
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
const profileResult = await apiRequest(
|
|
145
|
-
"GET",
|
|
146
|
-
"/profile/me",
|
|
147
|
-
undefined,
|
|
148
|
-
true,
|
|
149
|
-
);
|
|
150
|
-
if (profileResult.profile?.username) {
|
|
151
|
-
saveSession({
|
|
152
|
-
access_token: result.session.access_token,
|
|
153
|
-
refresh_token: result.session.refresh_token,
|
|
154
|
-
user_id: result.user.id,
|
|
155
|
-
email: result.user.email,
|
|
156
|
-
username: profileResult.profile.username,
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Always sync public key with server
|
|
161
|
-
if (hasPrivateKey()) {
|
|
162
|
-
spinner.text = "Syncing encryption keys...";
|
|
163
|
-
const pk = getPrivateKey()!;
|
|
164
|
-
const publicKey = await derivePublicKey(pk);
|
|
165
|
-
await apiRequest("PUT", "/profile/public-key", {
|
|
166
|
-
public_key: publicKey,
|
|
167
|
-
});
|
|
168
|
-
} else {
|
|
169
|
-
spinner.text = "Generating encryption keys...";
|
|
170
|
-
const keys = await generateKeys();
|
|
171
|
-
savePrivateKey(keys.private_key);
|
|
172
|
-
await apiRequest("PUT", "/profile/public-key", {
|
|
173
|
-
public_key: keys.public_key,
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
spinner.succeed(chalk.green("Logged in! You're ready to go."));
|
|
178
|
-
console.log(chalk.dim(` User: ${result.user.email}`));
|
|
179
|
-
console.log("");
|
|
180
|
-
} catch (err: any) {
|
|
181
|
-
spinner.fail(chalk.red(`Login failed: ${err.message}`));
|
|
182
|
-
} finally {
|
|
183
|
-
shutdown();
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
});
|
|
187
|
-
}
|
package/src/commands/member.ts
DELETED
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import { Command } from "commander";
|
|
2
|
-
import chalk from "chalk";
|
|
3
|
-
import ora from "ora";
|
|
4
|
-
import { api } from "../lib/api";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Helper: find a wallet ID by name from the user's wallets.
|
|
8
|
-
*/
|
|
9
|
-
async function findWalletId(name: string): Promise<string | null> {
|
|
10
|
-
const result = await api.get("/wallets");
|
|
11
|
-
const owned = result.owned.find((w: any) => w.name === name);
|
|
12
|
-
if (owned) return owned.id;
|
|
13
|
-
const shared = result.shared.find((w: any) => w.name === name);
|
|
14
|
-
if (shared) return shared.id;
|
|
15
|
-
return null;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function registerMemberCommands(program: Command): void {
|
|
19
|
-
const member = program.command("member").description("Manage wallet members");
|
|
20
|
-
|
|
21
|
-
// āā member add āā
|
|
22
|
-
member
|
|
23
|
-
.command("add <wallet> <username>")
|
|
24
|
-
.description("Add a member to a wallet")
|
|
25
|
-
.option("-r, --role <role>", "Role: admin, read, write", "read")
|
|
26
|
-
.action(
|
|
27
|
-
async (walletName: string, username: string, opts: { role: string }) => {
|
|
28
|
-
const spinner = ora("Adding member...").start();
|
|
29
|
-
try {
|
|
30
|
-
const walletId = await findWalletId(walletName);
|
|
31
|
-
if (!walletId) {
|
|
32
|
-
spinner.fail(chalk.red(`Wallet "${walletName}" not found`));
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const result = await api.post(`/wallets/${walletId}/members`, {
|
|
37
|
-
username,
|
|
38
|
-
role: opts.role,
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
spinner.succeed(
|
|
42
|
-
chalk.green(
|
|
43
|
-
`Added "${username}" as ${opts.role} to "${walletName}"`,
|
|
44
|
-
),
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
if (result.public_key) {
|
|
48
|
-
console.log(
|
|
49
|
-
chalk.dim(
|
|
50
|
-
` Their public key: ${result.public_key.substring(0, 20)}...`,
|
|
51
|
-
),
|
|
52
|
-
);
|
|
53
|
-
console.log(
|
|
54
|
-
chalk.yellow(
|
|
55
|
-
" ā Remember to re-encrypt secrets for this user:",
|
|
56
|
-
),
|
|
57
|
-
);
|
|
58
|
-
console.log(
|
|
59
|
-
chalk.yellow(` envtree secret share ${walletName}`),
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
} catch (err: any) {
|
|
63
|
-
spinner.fail(chalk.red(`Failed: ${err.message}`));
|
|
64
|
-
}
|
|
65
|
-
},
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
// āā member list āā
|
|
69
|
-
member
|
|
70
|
-
.command("list <wallet>")
|
|
71
|
-
.alias("ls")
|
|
72
|
-
.description("List members of a wallet")
|
|
73
|
-
.action(async (walletName: string) => {
|
|
74
|
-
const spinner = ora("Fetching members...").start();
|
|
75
|
-
try {
|
|
76
|
-
const walletId = await findWalletId(walletName);
|
|
77
|
-
if (!walletId) {
|
|
78
|
-
spinner.fail(chalk.red(`Wallet "${walletName}" not found`));
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const result = await api.get(`/wallets/${walletId}/members`);
|
|
83
|
-
spinner.stop();
|
|
84
|
-
|
|
85
|
-
console.log(chalk.bold(`\nMembers of "${walletName}"`));
|
|
86
|
-
|
|
87
|
-
// Owner
|
|
88
|
-
if (result.owner) {
|
|
89
|
-
console.log(
|
|
90
|
-
` ${chalk.cyan(result.owner.username)} ${chalk.dim(`(${result.owner.email})`)} ${chalk.green("[owner]")}`,
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Members
|
|
95
|
-
if (result.members && result.members.length > 0) {
|
|
96
|
-
for (const m of result.members) {
|
|
97
|
-
const roleColor =
|
|
98
|
-
m.role === "admin"
|
|
99
|
-
? chalk.magenta
|
|
100
|
-
: m.role === "write"
|
|
101
|
-
? chalk.blue
|
|
102
|
-
: chalk.dim;
|
|
103
|
-
console.log(
|
|
104
|
-
` ${chalk.cyan(m.user.username)} ${chalk.dim(`(${m.user.email})`)} ${roleColor(`[${m.role}]`)}`,
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
} else {
|
|
108
|
-
console.log(chalk.dim(" No additional members"));
|
|
109
|
-
}
|
|
110
|
-
console.log("");
|
|
111
|
-
} catch (err: any) {
|
|
112
|
-
spinner.fail(chalk.red(`Failed: ${err.message}`));
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
// āā member remove āā
|
|
117
|
-
member
|
|
118
|
-
.command("remove <wallet> <username>")
|
|
119
|
-
.description("Remove a member from a wallet")
|
|
120
|
-
.action(async (walletName: string, username: string) => {
|
|
121
|
-
const spinner = ora("Removing member...").start();
|
|
122
|
-
try {
|
|
123
|
-
const walletId = await findWalletId(walletName);
|
|
124
|
-
if (!walletId) {
|
|
125
|
-
spinner.fail(chalk.red(`Wallet "${walletName}" not found`));
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Look up user ID by username
|
|
130
|
-
const profileResult = await api.get(`/profile/${username}`);
|
|
131
|
-
if (!profileResult.profile) {
|
|
132
|
-
spinner.fail(chalk.red(`User "${username}" not found`));
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
await api.delete(
|
|
137
|
-
`/wallets/${walletId}/members/${profileResult.profile.id}`,
|
|
138
|
-
);
|
|
139
|
-
spinner.succeed(
|
|
140
|
-
chalk.green(`Removed "${username}" from "${walletName}"`),
|
|
141
|
-
);
|
|
142
|
-
} catch (err: any) {
|
|
143
|
-
spinner.fail(chalk.red(`Failed: ${err.message}`));
|
|
144
|
-
}
|
|
145
|
-
});
|
|
146
|
-
}
|
package/src/commands/secret.ts
DELETED
|
@@ -1,381 +0,0 @@
|
|
|
1
|
-
import { Command } from "commander";
|
|
2
|
-
import chalk from "chalk";
|
|
3
|
-
import ora from "ora";
|
|
4
|
-
import { api } from "../lib/api";
|
|
5
|
-
import { encrypt, decrypt, shutdown } from "../lib/crypto";
|
|
6
|
-
import { requirePrivateKey } from "../lib/key-store";
|
|
7
|
-
import { updateEnvFile, ensureGitIgnore } from "../lib/env-manager";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Helper: find a wallet ID by name.
|
|
11
|
-
*/
|
|
12
|
-
async function findWalletId(name: string): Promise<string | null> {
|
|
13
|
-
const result = await api.get("/wallets");
|
|
14
|
-
const owned = result.owned.find((w: any) => w.name === name);
|
|
15
|
-
if (owned) return owned.id;
|
|
16
|
-
const shared = result.shared.find((w: any) => w.name === name);
|
|
17
|
-
if (shared) return shared.id;
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Get all public keys of wallet members (owner + members).
|
|
23
|
-
*/
|
|
24
|
-
async function getWalletPublicKeys(walletId: string): Promise<string[]> {
|
|
25
|
-
const result = await api.get(`/wallets/${walletId}/members`);
|
|
26
|
-
const keys: string[] = [];
|
|
27
|
-
|
|
28
|
-
if (result.owner?.public_key) {
|
|
29
|
-
keys.push(result.owner.public_key);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (result.members) {
|
|
33
|
-
for (const m of result.members) {
|
|
34
|
-
if (m.user?.public_key) {
|
|
35
|
-
keys.push(m.user.public_key);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return keys;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Shared logic for pushing a secret (used by `secret set` and top-level `push`).
|
|
45
|
-
*/
|
|
46
|
-
export async function pushSecret(
|
|
47
|
-
walletName: string,
|
|
48
|
-
keyName: string,
|
|
49
|
-
value: string,
|
|
50
|
-
): Promise<void> {
|
|
51
|
-
const spinner = ora("Encrypting and saving secret...").start();
|
|
52
|
-
try {
|
|
53
|
-
const walletId = await findWalletId(walletName);
|
|
54
|
-
if (!walletId) {
|
|
55
|
-
spinner.fail(chalk.red(`Wallet "${walletName}" not found`));
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
spinner.text = "Fetching member public keys...";
|
|
60
|
-
const publicKeys = await getWalletPublicKeys(walletId);
|
|
61
|
-
|
|
62
|
-
if (publicKeys.length === 0) {
|
|
63
|
-
spinner.fail(chalk.red("No members with public keys found"));
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
spinner.text = `Encrypting for ${publicKeys.length} member(s)...`;
|
|
68
|
-
const secrets = [];
|
|
69
|
-
for (const pk of publicKeys) {
|
|
70
|
-
const ciphertext = await encrypt(value, pk);
|
|
71
|
-
secrets.push({
|
|
72
|
-
key_name: keyName,
|
|
73
|
-
encrypted_value: ciphertext,
|
|
74
|
-
encrypted_for: pk,
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
spinner.text = "Uploading encrypted secrets...";
|
|
79
|
-
await api.post(`/wallets/${walletId}/secrets`, { secrets });
|
|
80
|
-
|
|
81
|
-
spinner.succeed(
|
|
82
|
-
chalk.green(
|
|
83
|
-
`Secret "${keyName}" saved (encrypted for ${publicKeys.length} member(s))`,
|
|
84
|
-
),
|
|
85
|
-
);
|
|
86
|
-
} catch (err: any) {
|
|
87
|
-
spinner.fail(chalk.red(`Failed: ${err.message}`));
|
|
88
|
-
} finally {
|
|
89
|
-
shutdown();
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Shared logic for pulling secrets (used by `secret pull` and top-level `pull`).
|
|
95
|
-
*/
|
|
96
|
-
export async function pullSecrets(
|
|
97
|
-
walletName: string,
|
|
98
|
-
opts: { output?: string; sync?: boolean },
|
|
99
|
-
): Promise<void> {
|
|
100
|
-
const privateKey = requirePrivateKey();
|
|
101
|
-
const spinner = ora("Pulling secrets...").start();
|
|
102
|
-
try {
|
|
103
|
-
const walletId = await findWalletId(walletName);
|
|
104
|
-
if (!walletId) {
|
|
105
|
-
spinner.fail(chalk.red(`Wallet "${walletName}" not found`));
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const result = await api.get(`/wallets/${walletId}/secrets`);
|
|
110
|
-
|
|
111
|
-
if (result.secrets.length === 0) {
|
|
112
|
-
spinner.warn(chalk.yellow(`No secrets found in wallet "${walletName}"`));
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
spinner.text = `Decrypting ${result.secrets.length} secret(s)...`;
|
|
117
|
-
|
|
118
|
-
const lines: string[] = [];
|
|
119
|
-
lines.push(`# EnvTree ā ${walletName}`);
|
|
120
|
-
lines.push(`# Generated at ${new Date().toISOString()}`);
|
|
121
|
-
lines.push("");
|
|
122
|
-
|
|
123
|
-
const decrypted: { key: string; value: string }[] = [];
|
|
124
|
-
for (const secret of result.secrets) {
|
|
125
|
-
try {
|
|
126
|
-
const plaintext = await decrypt(secret.encrypted_value, privateKey);
|
|
127
|
-
lines.push(`${secret.key_name}=${plaintext}`);
|
|
128
|
-
decrypted.push({ key: secret.key_name, value: plaintext });
|
|
129
|
-
} catch {
|
|
130
|
-
lines.push(`# FAILED TO DECRYPT: ${secret.key_name}`);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const outputStr = lines.join("\n") + "\n";
|
|
135
|
-
|
|
136
|
-
if (opts.sync) {
|
|
137
|
-
// Sync directly into the local .env
|
|
138
|
-
spinner.text = "Syncing secrets to .env...";
|
|
139
|
-
for (const { key, value } of decrypted) {
|
|
140
|
-
updateEnvFile(key, value);
|
|
141
|
-
}
|
|
142
|
-
ensureGitIgnore(".env");
|
|
143
|
-
spinner.succeed(
|
|
144
|
-
chalk.green(
|
|
145
|
-
`${decrypted.length} secret(s) synced to .env`,
|
|
146
|
-
),
|
|
147
|
-
);
|
|
148
|
-
} else if (opts.output) {
|
|
149
|
-
const fs = require("fs");
|
|
150
|
-
fs.writeFileSync(opts.output, outputStr);
|
|
151
|
-
spinner.succeed(
|
|
152
|
-
chalk.green(
|
|
153
|
-
`${result.secrets.length} secret(s) written to ${opts.output}`,
|
|
154
|
-
),
|
|
155
|
-
);
|
|
156
|
-
} else {
|
|
157
|
-
spinner.stop();
|
|
158
|
-
console.log(outputStr);
|
|
159
|
-
}
|
|
160
|
-
} catch (err: any) {
|
|
161
|
-
spinner.fail(chalk.red(`Failed: ${err.message}`));
|
|
162
|
-
} finally {
|
|
163
|
-
shutdown();
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export function registerSecretCommands(program: Command): void {
|
|
168
|
-
const secret = program
|
|
169
|
-
.command("secret")
|
|
170
|
-
.description("Manage encrypted secrets");
|
|
171
|
-
|
|
172
|
-
// āā secret set āā
|
|
173
|
-
secret
|
|
174
|
-
.command("set <wallet> <key> <value>")
|
|
175
|
-
.description("Encrypt & store a secret for all wallet members")
|
|
176
|
-
.action(pushSecret);
|
|
177
|
-
|
|
178
|
-
// āā secret update āā
|
|
179
|
-
secret
|
|
180
|
-
.command("update <wallet> <key> <value>")
|
|
181
|
-
.description("Update an existing secret's value (re-encrypts for all members)")
|
|
182
|
-
.action(pushSecret);
|
|
183
|
-
|
|
184
|
-
// āā secret get āā
|
|
185
|
-
secret
|
|
186
|
-
.command("get <wallet> <key>")
|
|
187
|
-
.description("Decrypt and display a specific secret")
|
|
188
|
-
.option("-s, --save", "Save the value to your local .env file")
|
|
189
|
-
.action(
|
|
190
|
-
async (
|
|
191
|
-
walletName: string,
|
|
192
|
-
keyName: string,
|
|
193
|
-
opts: { save?: boolean },
|
|
194
|
-
) => {
|
|
195
|
-
const privateKey = requirePrivateKey();
|
|
196
|
-
const spinner = ora("Fetching and decrypting...").start();
|
|
197
|
-
try {
|
|
198
|
-
const walletId = await findWalletId(walletName);
|
|
199
|
-
if (!walletId) {
|
|
200
|
-
spinner.fail(chalk.red(`Wallet "${walletName}" not found`));
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const result = await api.get(`/wallets/${walletId}/secrets`);
|
|
205
|
-
const secret = result.secrets.find(
|
|
206
|
-
(s: any) => s.key_name === keyName,
|
|
207
|
-
);
|
|
208
|
-
|
|
209
|
-
if (!secret) {
|
|
210
|
-
spinner.fail(
|
|
211
|
-
chalk.red(
|
|
212
|
-
`Secret "${keyName}" not found in wallet "${walletName}"`,
|
|
213
|
-
),
|
|
214
|
-
);
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
spinner.text = "Decrypting...";
|
|
219
|
-
const plaintext = await decrypt(
|
|
220
|
-
secret.encrypted_value,
|
|
221
|
-
privateKey,
|
|
222
|
-
);
|
|
223
|
-
|
|
224
|
-
if (opts.save) {
|
|
225
|
-
spinner.text = "Saving to .env...";
|
|
226
|
-
updateEnvFile(keyName, plaintext);
|
|
227
|
-
ensureGitIgnore(".env");
|
|
228
|
-
spinner.succeed(
|
|
229
|
-
chalk.green(`Secret "${keyName}" saved to .env`),
|
|
230
|
-
);
|
|
231
|
-
} else {
|
|
232
|
-
spinner.stop();
|
|
233
|
-
console.log(`${chalk.cyan(keyName)}=${plaintext}`);
|
|
234
|
-
}
|
|
235
|
-
} catch (err: any) {
|
|
236
|
-
spinner.fail(chalk.red(`Failed: ${err.message}`));
|
|
237
|
-
} finally {
|
|
238
|
-
shutdown();
|
|
239
|
-
}
|
|
240
|
-
},
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
// āā secret list āā
|
|
244
|
-
secret
|
|
245
|
-
.command("list <wallet>")
|
|
246
|
-
.alias("ls")
|
|
247
|
-
.description("List all secret keys in a wallet")
|
|
248
|
-
.action(async (walletName: string) => {
|
|
249
|
-
const spinner = ora("Fetching secrets...").start();
|
|
250
|
-
try {
|
|
251
|
-
const walletId = await findWalletId(walletName);
|
|
252
|
-
if (!walletId) {
|
|
253
|
-
spinner.fail(chalk.red(`Wallet "${walletName}" not found`));
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const result = await api.get(`/wallets/${walletId}/secrets/all`);
|
|
258
|
-
spinner.stop();
|
|
259
|
-
|
|
260
|
-
if (result.keys.length === 0) {
|
|
261
|
-
console.log(chalk.yellow(`No secrets in wallet "${walletName}"`));
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
console.log(chalk.bold(`\nSecrets in "${walletName}"`));
|
|
266
|
-
for (const key of result.keys) {
|
|
267
|
-
console.log(` ${chalk.cyan(key)}`);
|
|
268
|
-
}
|
|
269
|
-
console.log(chalk.dim(`\n ${result.keys.length} secret(s) total\n`));
|
|
270
|
-
} catch (err: any) {
|
|
271
|
-
spinner.fail(chalk.red(`Failed: ${err.message}`));
|
|
272
|
-
}
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
// āā secret pull āā
|
|
276
|
-
secret
|
|
277
|
-
.command("pull <wallet>")
|
|
278
|
-
.description("Pull and decrypt all secrets")
|
|
279
|
-
.option("-o, --output <file>", "Write to a file instead of stdout")
|
|
280
|
-
.option("--sync", "Sync all secrets directly to your local .env")
|
|
281
|
-
.action(
|
|
282
|
-
async (
|
|
283
|
-
walletName: string,
|
|
284
|
-
opts: { output?: string; sync?: boolean },
|
|
285
|
-
) => {
|
|
286
|
-
await pullSecrets(walletName, opts);
|
|
287
|
-
},
|
|
288
|
-
);
|
|
289
|
-
|
|
290
|
-
// āā secret delete āā
|
|
291
|
-
secret
|
|
292
|
-
.command("delete <wallet> <key>")
|
|
293
|
-
.alias("rm")
|
|
294
|
-
.description("Delete a secret from a wallet")
|
|
295
|
-
.action(async (walletName: string, keyName: string) => {
|
|
296
|
-
const spinner = ora("Deleting secret...").start();
|
|
297
|
-
try {
|
|
298
|
-
const walletId = await findWalletId(walletName);
|
|
299
|
-
if (!walletId) {
|
|
300
|
-
spinner.fail(chalk.red(`Wallet "${walletName}" not found`));
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const result = await api.get(`/wallets/${walletId}/secrets`);
|
|
305
|
-
const secret = result.secrets.find(
|
|
306
|
-
(s: any) => s.key_name === keyName,
|
|
307
|
-
);
|
|
308
|
-
|
|
309
|
-
if (!secret) {
|
|
310
|
-
spinner.fail(chalk.red(`Secret "${keyName}" not found`));
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
await api.delete(`/wallets/${walletId}/secrets/${secret.id}`);
|
|
315
|
-
spinner.succeed(
|
|
316
|
-
chalk.green(`Secret "${keyName}" deleted from "${walletName}"`),
|
|
317
|
-
);
|
|
318
|
-
} catch (err: any) {
|
|
319
|
-
spinner.fail(chalk.red(`Failed: ${err.message}`));
|
|
320
|
-
}
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
// āā secret share āā
|
|
324
|
-
secret
|
|
325
|
-
.command("share <wallet>")
|
|
326
|
-
.description("Re-encrypt all secrets for all current wallet members")
|
|
327
|
-
.action(async (walletName: string) => {
|
|
328
|
-
const privateKey = requirePrivateKey();
|
|
329
|
-
const spinner = ora(
|
|
330
|
-
"Re-encrypting secrets for all members...",
|
|
331
|
-
).start();
|
|
332
|
-
try {
|
|
333
|
-
const walletId = await findWalletId(walletName);
|
|
334
|
-
if (!walletId) {
|
|
335
|
-
spinner.fail(chalk.red(`Wallet "${walletName}" not found`));
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
const mySecrets = await api.get(`/wallets/${walletId}/secrets`);
|
|
340
|
-
if (mySecrets.secrets.length === 0) {
|
|
341
|
-
spinner.warn(chalk.yellow("No secrets to share"));
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const publicKeys = await getWalletPublicKeys(walletId);
|
|
346
|
-
|
|
347
|
-
spinner.text = `Re-encrypting ${mySecrets.secrets.length} secret(s) for ${publicKeys.length} member(s)...`;
|
|
348
|
-
|
|
349
|
-
const allSecrets = [];
|
|
350
|
-
for (const secret of mySecrets.secrets) {
|
|
351
|
-
const plaintext = await decrypt(
|
|
352
|
-
secret.encrypted_value,
|
|
353
|
-
privateKey,
|
|
354
|
-
);
|
|
355
|
-
|
|
356
|
-
for (const pk of publicKeys) {
|
|
357
|
-
const ciphertext = await encrypt(plaintext, pk);
|
|
358
|
-
allSecrets.push({
|
|
359
|
-
key_name: secret.key_name,
|
|
360
|
-
encrypted_value: ciphertext,
|
|
361
|
-
encrypted_for: pk,
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
await api.post(`/wallets/${walletId}/secrets`, {
|
|
367
|
-
secrets: allSecrets,
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
spinner.succeed(
|
|
371
|
-
chalk.green(
|
|
372
|
-
`${mySecrets.secrets.length} secret(s) re-encrypted for ${publicKeys.length} member(s)`,
|
|
373
|
-
),
|
|
374
|
-
);
|
|
375
|
-
} catch (err: any) {
|
|
376
|
-
spinner.fail(chalk.red(`Failed: ${err.message}`));
|
|
377
|
-
} finally {
|
|
378
|
-
shutdown();
|
|
379
|
-
}
|
|
380
|
-
});
|
|
381
|
-
}
|