@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
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { Command } from "commander";
|
|
2
|
-
import { pushSecret, pullSecrets } from "./secret";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Register top-level shortcut commands:
|
|
6
|
-
* et push <wallet> <key> <value> → secret set
|
|
7
|
-
* et pull <wallet> → secret pull --sync
|
|
8
|
-
*/
|
|
9
|
-
export function registerShortcuts(program: Command): void {
|
|
10
|
-
// ── et push ──
|
|
11
|
-
program
|
|
12
|
-
.command("push <wallet> <key> <value>")
|
|
13
|
-
.description("Shortcut: Encrypt & store a secret (same as: secret set)")
|
|
14
|
-
.action(pushSecret);
|
|
15
|
-
|
|
16
|
-
// ── et pull ──
|
|
17
|
-
program
|
|
18
|
-
.command("pull <wallet>")
|
|
19
|
-
.description("Shortcut: Sync all secrets to local .env (same as: secret pull --sync)")
|
|
20
|
-
.option("-o, --output <file>", "Write to a file instead of syncing to .env")
|
|
21
|
-
.action(async (walletName: string, opts: { output?: string }) => {
|
|
22
|
-
// Default behavior: --sync (write to .env)
|
|
23
|
-
await pullSecrets(walletName, { sync: !opts.output, output: opts.output });
|
|
24
|
-
});
|
|
25
|
-
}
|
package/src/commands/wallet.ts
DELETED
|
@@ -1,97 +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
|
-
export function registerWalletCommands(program: Command): void {
|
|
7
|
-
const wallet = program.command("wallet").description("Manage key wallets");
|
|
8
|
-
|
|
9
|
-
// ── wallet create ──
|
|
10
|
-
wallet
|
|
11
|
-
.command("create <name>")
|
|
12
|
-
.description("Create a new key wallet")
|
|
13
|
-
.option("-d, --description <desc>", "Wallet description")
|
|
14
|
-
.action(async (name: string, opts: { description?: string }) => {
|
|
15
|
-
const spinner = ora("Creating wallet...").start();
|
|
16
|
-
try {
|
|
17
|
-
const result = await api.post("/wallets", {
|
|
18
|
-
name,
|
|
19
|
-
description: opts.description,
|
|
20
|
-
});
|
|
21
|
-
spinner.succeed(chalk.green(`Wallet "${name}" created`));
|
|
22
|
-
console.log(chalk.dim(` ID: ${result.wallet.id}`));
|
|
23
|
-
} catch (err: any) {
|
|
24
|
-
spinner.fail(chalk.red(`Failed: ${err.message}`));
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
// ── wallet list ──
|
|
29
|
-
wallet
|
|
30
|
-
.command("list")
|
|
31
|
-
.alias("ls")
|
|
32
|
-
.description("List all your wallets")
|
|
33
|
-
.action(async () => {
|
|
34
|
-
const spinner = ora("Fetching wallets...").start();
|
|
35
|
-
try {
|
|
36
|
-
const result = await api.get("/wallets");
|
|
37
|
-
spinner.stop();
|
|
38
|
-
|
|
39
|
-
if (result.owned.length === 0 && result.shared.length === 0) {
|
|
40
|
-
console.log(
|
|
41
|
-
chalk.yellow(
|
|
42
|
-
"No wallets found. Create one with: envtree wallet create <name>",
|
|
43
|
-
),
|
|
44
|
-
);
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (result.owned.length > 0) {
|
|
49
|
-
console.log(chalk.bold("\nOwned Wallets"));
|
|
50
|
-
for (const w of result.owned) {
|
|
51
|
-
console.log(
|
|
52
|
-
` ${chalk.cyan(w.name)} ${chalk.dim(`(${w.id.substring(0, 8)}...)`)}`,
|
|
53
|
-
);
|
|
54
|
-
if (w.description) console.log(` ${chalk.dim(w.description)}`);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (result.shared.length > 0) {
|
|
59
|
-
console.log(chalk.bold("\nShared Wallets"));
|
|
60
|
-
for (const w of result.shared) {
|
|
61
|
-
console.log(
|
|
62
|
-
` ${chalk.cyan(w.name)} ${chalk.dim(`[${w.role}]`)} ${chalk.dim(`(${w.id?.substring(0, 8)}...)`)}`,
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
console.log("");
|
|
67
|
-
} catch (err: any) {
|
|
68
|
-
spinner.fail(chalk.red(`Failed: ${err.message}`));
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
// ── wallet delete ──
|
|
73
|
-
wallet
|
|
74
|
-
.command("delete <name>")
|
|
75
|
-
.description("Delete a wallet (owner only)")
|
|
76
|
-
.action(async (name: string) => {
|
|
77
|
-
const spinner = ora("Finding wallet...").start();
|
|
78
|
-
try {
|
|
79
|
-
// Find wallet by name
|
|
80
|
-
const listResult = await api.get("/wallets");
|
|
81
|
-
const target = listResult.owned.find((w: any) => w.name === name);
|
|
82
|
-
|
|
83
|
-
if (!target) {
|
|
84
|
-
spinner.fail(
|
|
85
|
-
chalk.red(`Wallet "${name}" not found or you are not the owner`),
|
|
86
|
-
);
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
spinner.text = "Deleting wallet...";
|
|
91
|
-
await api.delete(`/wallets/${target.id}`);
|
|
92
|
-
spinner.succeed(chalk.green(`Wallet "${name}" deleted`));
|
|
93
|
-
} catch (err: any) {
|
|
94
|
-
spinner.fail(chalk.red(`Failed: ${err.message}`));
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { Command } from "commander";
|
|
4
|
-
import { registerAuthCommands } from "./commands/auth";
|
|
5
|
-
import { registerWalletCommands } from "./commands/wallet";
|
|
6
|
-
import { registerMemberCommands } from "./commands/member";
|
|
7
|
-
import { registerSecretCommands } from "./commands/secret";
|
|
8
|
-
import { registerInitCommand } from "./commands/init";
|
|
9
|
-
import { registerConfigCommands } from "./commands/config";
|
|
10
|
-
import { registerShortcuts } from "./commands/shortcuts";
|
|
11
|
-
|
|
12
|
-
const program = new Command();
|
|
13
|
-
|
|
14
|
-
program
|
|
15
|
-
.name("et")
|
|
16
|
-
.description("EnvTree — Securely manage and share environment variables")
|
|
17
|
-
.version("1.0.0")
|
|
18
|
-
.addHelpText(
|
|
19
|
-
"after",
|
|
20
|
-
`
|
|
21
|
-
Quick Start:
|
|
22
|
-
$ et init First-time setup (signup/login)
|
|
23
|
-
$ et push <wallet> <key> <value> Encrypt & store a secret
|
|
24
|
-
$ et pull <wallet> Sync all secrets to local .env
|
|
25
|
-
$ et whoami Check current user
|
|
26
|
-
|
|
27
|
-
Examples:
|
|
28
|
-
$ et init
|
|
29
|
-
$ et push production DB_URL "postgres://..."
|
|
30
|
-
$ et pull production
|
|
31
|
-
$ et pull production -o .env.production
|
|
32
|
-
$ et secret list production
|
|
33
|
-
$ et member add production alice --role write
|
|
34
|
-
`,
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
// Core commands
|
|
38
|
-
registerAuthCommands(program);
|
|
39
|
-
registerInitCommand(program);
|
|
40
|
-
registerConfigCommands(program);
|
|
41
|
-
|
|
42
|
-
// Resource commands
|
|
43
|
-
registerWalletCommands(program);
|
|
44
|
-
registerMemberCommands(program);
|
|
45
|
-
registerSecretCommands(program);
|
|
46
|
-
|
|
47
|
-
// Top-level shortcuts
|
|
48
|
-
registerShortcuts(program);
|
|
49
|
-
|
|
50
|
-
program.parse(process.argv);
|
package/src/lib/api.ts
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { getConfig, requireSession } from "./config";
|
|
2
|
-
|
|
3
|
-
interface ApiResponse {
|
|
4
|
-
[key: string]: any;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Make an authenticated API request to the EnvTree server.
|
|
9
|
-
*/
|
|
10
|
-
export async function apiRequest(
|
|
11
|
-
method: string,
|
|
12
|
-
path: string,
|
|
13
|
-
body?: Record<string, any>,
|
|
14
|
-
requireAuth = true,
|
|
15
|
-
): Promise<ApiResponse> {
|
|
16
|
-
const config = getConfig();
|
|
17
|
-
const url = `${config.server_url}${path}`;
|
|
18
|
-
|
|
19
|
-
const headers: Record<string, string> = {
|
|
20
|
-
"Content-Type": "application/json",
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
if (requireAuth) {
|
|
24
|
-
const session = requireSession();
|
|
25
|
-
headers["Authorization"] = `Bearer ${session.access_token}`;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const options: RequestInit = {
|
|
29
|
-
method,
|
|
30
|
-
headers,
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
if (body && method !== "GET") {
|
|
34
|
-
options.body = JSON.stringify(body);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const response = await fetch(url, options);
|
|
38
|
-
const data = (await response.json()) as ApiResponse;
|
|
39
|
-
|
|
40
|
-
if (!response.ok) {
|
|
41
|
-
throw new Error(
|
|
42
|
-
data.error || `Request failed with status ${response.status}`,
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return data;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Shorthand methods */
|
|
50
|
-
export const api = {
|
|
51
|
-
get: (path: string) => apiRequest("GET", path),
|
|
52
|
-
post: (path: string, body: Record<string, any>, auth = true) =>
|
|
53
|
-
apiRequest("POST", path, body, auth),
|
|
54
|
-
put: (path: string, body: Record<string, any>) =>
|
|
55
|
-
apiRequest("PUT", path, body),
|
|
56
|
-
delete: (path: string) => apiRequest("DELETE", path),
|
|
57
|
-
};
|
package/src/lib/config.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import * as fs from "fs";
|
|
2
|
-
import * as path from "path";
|
|
3
|
-
import * as os from "os";
|
|
4
|
-
|
|
5
|
-
const CONFIG_DIR = path.join(os.homedir(), ".envtree");
|
|
6
|
-
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
7
|
-
|
|
8
|
-
export interface Session {
|
|
9
|
-
access_token: string;
|
|
10
|
-
refresh_token: string;
|
|
11
|
-
user_id: string;
|
|
12
|
-
email: string;
|
|
13
|
-
username: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface Config {
|
|
17
|
-
server_url: string;
|
|
18
|
-
session?: Session;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function ensureConfigDir(): void {
|
|
22
|
-
if (!fs.existsSync(CONFIG_DIR)) {
|
|
23
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function getConfig(): Config {
|
|
28
|
-
ensureConfigDir();
|
|
29
|
-
if (!fs.existsSync(CONFIG_FILE)) {
|
|
30
|
-
const defaults: Config = { server_url: "http://localhost:3001" };
|
|
31
|
-
fs.writeFileSync(CONFIG_FILE, JSON.stringify(defaults, null, 2), {
|
|
32
|
-
mode: 0o600,
|
|
33
|
-
});
|
|
34
|
-
return defaults;
|
|
35
|
-
}
|
|
36
|
-
return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function saveConfig(config: Config): void {
|
|
40
|
-
ensureConfigDir();
|
|
41
|
-
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), {
|
|
42
|
-
mode: 0o600,
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function getSession(): Session | null {
|
|
47
|
-
const config = getConfig();
|
|
48
|
-
return config.session || null;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function saveSession(session: Session): void {
|
|
52
|
-
const config = getConfig();
|
|
53
|
-
config.session = session;
|
|
54
|
-
saveConfig(config);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function clearSession(): void {
|
|
58
|
-
const config = getConfig();
|
|
59
|
-
delete config.session;
|
|
60
|
-
saveConfig(config);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function requireSession(): Session {
|
|
64
|
-
const session = getSession();
|
|
65
|
-
if (!session) {
|
|
66
|
-
console.error("Not logged in. Run: envtree login");
|
|
67
|
-
process.exit(1);
|
|
68
|
-
}
|
|
69
|
-
return session;
|
|
70
|
-
}
|
package/src/lib/crypto.ts
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
import { spawn, ChildProcessWithoutNullStreams } from "child_process";
|
|
2
|
-
import * as path from "path";
|
|
3
|
-
|
|
4
|
-
const CRYPTO_ENGINE_PATH = path.resolve(
|
|
5
|
-
__dirname,
|
|
6
|
-
"../../../../packages/crypto/engine.py",
|
|
7
|
-
);
|
|
8
|
-
|
|
9
|
-
let engineProcess: ChildProcessWithoutNullStreams | null = null;
|
|
10
|
-
|
|
11
|
-
interface CryptoRequest {
|
|
12
|
-
action: string;
|
|
13
|
-
data?: Record<string, any>;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
interface CryptoResponse {
|
|
17
|
-
status: string;
|
|
18
|
-
[key: string]: any;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Get or spawn the crypto engine Python process.
|
|
23
|
-
*/
|
|
24
|
-
function getEngine(): ChildProcessWithoutNullStreams {
|
|
25
|
-
if (engineProcess && !engineProcess.killed) {
|
|
26
|
-
return engineProcess;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Try venv python first, fall back to system python
|
|
30
|
-
const venvDir = path.resolve(__dirname, "../../../../packages/crypto/.venv");
|
|
31
|
-
const isWindows = process.platform === "win32";
|
|
32
|
-
|
|
33
|
-
// Potential Venv Paths
|
|
34
|
-
const potentialVenvPaths = isWindows
|
|
35
|
-
? [
|
|
36
|
-
path.join(venvDir, "Scripts", "python.exe"),
|
|
37
|
-
path.join(venvDir, "bin", "python.exe"),
|
|
38
|
-
]
|
|
39
|
-
: [path.join(venvDir, "bin", "python")];
|
|
40
|
-
|
|
41
|
-
let pythonCmd = isWindows ? "python" : "python3";
|
|
42
|
-
|
|
43
|
-
for (const venvPath of potentialVenvPaths) {
|
|
44
|
-
if (require("fs").existsSync(venvPath)) {
|
|
45
|
-
pythonCmd = venvPath;
|
|
46
|
-
break;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// If we haven't found a venv python, verify the system command exists
|
|
51
|
-
if (pythonCmd === "python" || pythonCmd === "python3") {
|
|
52
|
-
try {
|
|
53
|
-
// Use system python if it exists and works
|
|
54
|
-
require("child_process").execSync(`${pythonCmd} --version`, {
|
|
55
|
-
stdio: "ignore",
|
|
56
|
-
});
|
|
57
|
-
} catch {
|
|
58
|
-
// If the preferred one fails, try the fallback
|
|
59
|
-
pythonCmd = pythonCmd === "python3" ? "python" : "python3";
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
engineProcess = spawn(pythonCmd, [CRYPTO_ENGINE_PATH], {
|
|
64
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
65
|
-
cwd: path.resolve(__dirname, "../../../../packages/crypto"),
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
engineProcess.on("exit", () => {
|
|
69
|
-
engineProcess = null;
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
return engineProcess;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Send a command to the crypto engine and get the response.
|
|
77
|
-
*/
|
|
78
|
-
function sendCommand(request: CryptoRequest): Promise<CryptoResponse> {
|
|
79
|
-
return new Promise((resolve, reject) => {
|
|
80
|
-
const engine = getEngine();
|
|
81
|
-
|
|
82
|
-
const onData = (data: Buffer) => {
|
|
83
|
-
try {
|
|
84
|
-
const response = JSON.parse(data.toString().trim());
|
|
85
|
-
engine.stdout.removeListener("data", onData);
|
|
86
|
-
engine.stderr.removeListener("data", onError);
|
|
87
|
-
|
|
88
|
-
if (response.status === "error") {
|
|
89
|
-
reject(new Error(response.message));
|
|
90
|
-
} else {
|
|
91
|
-
resolve(response);
|
|
92
|
-
}
|
|
93
|
-
} catch (e) {
|
|
94
|
-
// partial data, wait for more
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
const onError = (data: Buffer) => {
|
|
99
|
-
engine.stdout.removeListener("data", onData);
|
|
100
|
-
engine.stderr.removeListener("data", onError);
|
|
101
|
-
reject(new Error(`Crypto engine error: ${data.toString()}`));
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
engine.stdout.on("data", onData);
|
|
105
|
-
engine.stderr.on("data", onError);
|
|
106
|
-
|
|
107
|
-
engine.stdin.write(JSON.stringify(request) + "\n");
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Generate a new X25519 key pair.
|
|
113
|
-
*/
|
|
114
|
-
export async function generateKeys(): Promise<{
|
|
115
|
-
private_key: string;
|
|
116
|
-
public_key: string;
|
|
117
|
-
}> {
|
|
118
|
-
const result = await sendCommand({ action: "generate_keys" });
|
|
119
|
-
return {
|
|
120
|
-
private_key: result.private_key,
|
|
121
|
-
public_key: result.public_key,
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Derive the public key from an existing private key.
|
|
127
|
-
*/
|
|
128
|
-
export async function derivePublicKey(privateKey: string): Promise<string> {
|
|
129
|
-
const result = await sendCommand({
|
|
130
|
-
action: "derive_public_key",
|
|
131
|
-
data: { private_key: privateKey },
|
|
132
|
-
});
|
|
133
|
-
return result.public_key;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Encrypt a plaintext value using the recipient's public key (SealedBox).
|
|
138
|
-
*/
|
|
139
|
-
export async function encrypt(
|
|
140
|
-
plaintext: string,
|
|
141
|
-
publicKey: string,
|
|
142
|
-
): Promise<string> {
|
|
143
|
-
const result = await sendCommand({
|
|
144
|
-
action: "encrypt",
|
|
145
|
-
data: { plaintext, public_key: publicKey },
|
|
146
|
-
});
|
|
147
|
-
return result.ciphertext;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Decrypt a ciphertext using the recipient's private key (SealedBox).
|
|
152
|
-
*/
|
|
153
|
-
export async function decrypt(
|
|
154
|
-
ciphertext: string,
|
|
155
|
-
privateKey: string,
|
|
156
|
-
): Promise<string> {
|
|
157
|
-
const result = await sendCommand({
|
|
158
|
-
action: "decrypt",
|
|
159
|
-
data: { ciphertext, private_key: privateKey },
|
|
160
|
-
});
|
|
161
|
-
return result.plaintext;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Shut down the crypto engine process.
|
|
166
|
-
*/
|
|
167
|
-
export function shutdown(): void {
|
|
168
|
-
if (engineProcess && !engineProcess.killed) {
|
|
169
|
-
engineProcess.stdin.end();
|
|
170
|
-
engineProcess.kill();
|
|
171
|
-
engineProcess = null;
|
|
172
|
-
}
|
|
173
|
-
}
|
package/src/lib/env-manager.ts
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import * as fs from "fs";
|
|
2
|
-
import * as path from "path";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Updates a key=value pair in a .env file.
|
|
6
|
-
* Creates the file if it doesn't exist.
|
|
7
|
-
* If the key exists, it updates its value.
|
|
8
|
-
* If the key doesn't exist, it appends to the end.
|
|
9
|
-
*/
|
|
10
|
-
export function updateEnvFile(key: string, value: string): void {
|
|
11
|
-
const envPath = path.resolve(process.cwd(), ".env");
|
|
12
|
-
let content = "";
|
|
13
|
-
|
|
14
|
-
if (fs.existsSync(envPath)) {
|
|
15
|
-
content = fs.readFileSync(envPath, "utf-8");
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const lines = content.split("\n");
|
|
19
|
-
const keyPattern = new RegExp(`^${key}=.*`);
|
|
20
|
-
let found = false;
|
|
21
|
-
|
|
22
|
-
const newLines = lines.map((line) => {
|
|
23
|
-
if (keyPattern.test(line.trim())) {
|
|
24
|
-
found = true;
|
|
25
|
-
return `${key}=${value}`;
|
|
26
|
-
}
|
|
27
|
-
return line;
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
if (!found) {
|
|
31
|
-
if (newLines.length > 0 && newLines[newLines.length - 1].trim() !== "") {
|
|
32
|
-
newLines.push("");
|
|
33
|
-
}
|
|
34
|
-
newLines.push(`${key}=${value}`);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
fs.writeFileSync(envPath, newLines.join("\n"), "utf-8");
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Ensures that a .gitignore file exists and contains the specified entry.
|
|
42
|
-
*/
|
|
43
|
-
export function ensureGitIgnore(entry: string = ".env"): void {
|
|
44
|
-
const gitIgnorePath = path.resolve(process.cwd(), ".gitignore");
|
|
45
|
-
let content = "";
|
|
46
|
-
|
|
47
|
-
if (fs.existsSync(gitIgnorePath)) {
|
|
48
|
-
content = fs.readFileSync(gitIgnorePath, "utf-8");
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const lines = content.split("\n").map((l) => l.trim());
|
|
52
|
-
|
|
53
|
-
if (!lines.includes(entry)) {
|
|
54
|
-
if (content.length > 0 && !content.endsWith("\n")) {
|
|
55
|
-
content += "\n";
|
|
56
|
-
}
|
|
57
|
-
content += `${entry}\n`;
|
|
58
|
-
fs.writeFileSync(gitIgnorePath, content, "utf-8");
|
|
59
|
-
}
|
|
60
|
-
}
|
package/src/lib/key-store.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import * as fs from "fs";
|
|
2
|
-
import * as path from "path";
|
|
3
|
-
import * as os from "os";
|
|
4
|
-
|
|
5
|
-
const KEY_DIR = path.join(os.homedir(), ".envtree");
|
|
6
|
-
const KEY_FILE = path.join(KEY_DIR, "private_key");
|
|
7
|
-
|
|
8
|
-
function ensureKeyDir(): void {
|
|
9
|
-
if (!fs.existsSync(KEY_DIR)) {
|
|
10
|
-
fs.mkdirSync(KEY_DIR, { recursive: true, mode: 0o700 });
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Save the private key to ~/.envtree/private_key
|
|
16
|
-
*/
|
|
17
|
-
export function savePrivateKey(privateKey: string): void {
|
|
18
|
-
ensureKeyDir();
|
|
19
|
-
fs.writeFileSync(KEY_FILE, privateKey, { mode: 0o600 });
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Read the private key from ~/.envtree/private_key
|
|
24
|
-
*/
|
|
25
|
-
export function getPrivateKey(): string | null {
|
|
26
|
-
if (!fs.existsSync(KEY_FILE)) {
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
return fs.readFileSync(KEY_FILE, "utf-8").trim();
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Get private key or exit with error.
|
|
34
|
-
*/
|
|
35
|
-
export function requirePrivateKey(): string {
|
|
36
|
-
const key = getPrivateKey();
|
|
37
|
-
if (!key) {
|
|
38
|
-
console.error(
|
|
39
|
-
"Private key not found. Run: envtree signup (or envtree login)",
|
|
40
|
-
);
|
|
41
|
-
process.exit(1);
|
|
42
|
-
}
|
|
43
|
-
return key;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Check if a private key exists locally.
|
|
48
|
-
*/
|
|
49
|
-
export function hasPrivateKey(): boolean {
|
|
50
|
-
return fs.existsSync(KEY_FILE);
|
|
51
|
-
}
|
package/src/test-e2e.ts
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import { apiRequest } from "./lib/api";
|
|
2
|
-
import { saveSession } from "./lib/config";
|
|
3
|
-
import { generateKeys, encrypt, decrypt, shutdown } from "./lib/crypto";
|
|
4
|
-
import { savePrivateKey } from "./lib/key-store";
|
|
5
|
-
import * as fs from "fs";
|
|
6
|
-
import * as path from "os";
|
|
7
|
-
|
|
8
|
-
async function testE2E() {
|
|
9
|
-
const email = `test-${Date.now()}@example.com`;
|
|
10
|
-
const username = `user-${Date.now()}`;
|
|
11
|
-
const password = "password123";
|
|
12
|
-
|
|
13
|
-
console.log("--- Phase 1: Signup ---");
|
|
14
|
-
try {
|
|
15
|
-
const signupResult = await apiRequest(
|
|
16
|
-
"POST",
|
|
17
|
-
"/auth/signup",
|
|
18
|
-
{
|
|
19
|
-
email,
|
|
20
|
-
password,
|
|
21
|
-
username,
|
|
22
|
-
},
|
|
23
|
-
false,
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
console.log("Signup response:", JSON.stringify(signupResult, null, 2));
|
|
27
|
-
|
|
28
|
-
if (signupResult.session) {
|
|
29
|
-
saveSession({
|
|
30
|
-
access_token: signupResult.session.access_token,
|
|
31
|
-
refresh_token: signupResult.session.refresh_token,
|
|
32
|
-
user_id: signupResult.user.id,
|
|
33
|
-
email,
|
|
34
|
-
username,
|
|
35
|
-
});
|
|
36
|
-
console.log(" Signup successful");
|
|
37
|
-
} else {
|
|
38
|
-
console.log(
|
|
39
|
-
" User created but NO session returned (possibly email verification required)",
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (!signupResult.session) {
|
|
44
|
-
console.warn(
|
|
45
|
-
"⚠️ Ending test early: No session available to continue authenticated requests.",
|
|
46
|
-
);
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
console.log("--- Phase 2: Key Generation ---");
|
|
51
|
-
const { private_key, public_key } = await generateKeys();
|
|
52
|
-
savePrivateKey(private_key);
|
|
53
|
-
await apiRequest("PUT", "/profile/public-key", { public_key });
|
|
54
|
-
console.log(" Keypair generated and public key uploaded");
|
|
55
|
-
|
|
56
|
-
console.log("--- Phase 3: Wallet Creation ---");
|
|
57
|
-
const walletName = `test-wallet-${Date.now()}`;
|
|
58
|
-
const walletResult = await apiRequest("POST", "/wallets", {
|
|
59
|
-
name: walletName,
|
|
60
|
-
});
|
|
61
|
-
const walletId = walletResult.wallet.id;
|
|
62
|
-
console.log(` Wallet "${walletName}" created with ID: ${walletId}`);
|
|
63
|
-
|
|
64
|
-
console.log("--- Phase 4: Secret Encryption & Storage ---");
|
|
65
|
-
const secretKey = "MY_API_KEY";
|
|
66
|
-
const secretValue = "super-secret-value-123";
|
|
67
|
-
const encryptedValue = await encrypt(secretValue, public_key);
|
|
68
|
-
|
|
69
|
-
await apiRequest("POST", `/wallets/${walletId}/secrets`, {
|
|
70
|
-
secrets: [
|
|
71
|
-
{
|
|
72
|
-
key_name: secretKey,
|
|
73
|
-
encrypted_value: encryptedValue,
|
|
74
|
-
encrypted_for: public_key,
|
|
75
|
-
},
|
|
76
|
-
],
|
|
77
|
-
});
|
|
78
|
-
console.log(" Secret encrypted and uploaded");
|
|
79
|
-
|
|
80
|
-
console.log("--- Phase 5: Secret Retrieval & Decryption ---");
|
|
81
|
-
const getResult = await apiRequest("GET", `/wallets/${walletId}/secrets`);
|
|
82
|
-
const foundSecret = getResult.secrets.find(
|
|
83
|
-
(s: any) => s.key_name === secretKey,
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
if (!foundSecret) throw new Error("Secret not found in response");
|
|
87
|
-
|
|
88
|
-
const decryptedValue = await decrypt(
|
|
89
|
-
foundSecret.encrypted_value,
|
|
90
|
-
private_key,
|
|
91
|
-
);
|
|
92
|
-
console.log(` Secret retrieved and decrypted: ${decryptedValue}`);
|
|
93
|
-
|
|
94
|
-
if (decryptedValue === secretValue) {
|
|
95
|
-
console.log(" E2E TRANSACTION SUCCESSFUL");
|
|
96
|
-
} else {
|
|
97
|
-
throw new Error("Decrypted value mismatch");
|
|
98
|
-
}
|
|
99
|
-
} catch (error) {
|
|
100
|
-
console.error(" E2E TEST FAILED:", error);
|
|
101
|
-
} finally {
|
|
102
|
-
shutdown();
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
testE2E();
|