beakcrypt 0.0.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.
- package/dist/index.js +1370 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1370 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/lib/errors.ts
|
|
7
|
+
import pc from "picocolors";
|
|
8
|
+
var CliError = class extends Error {
|
|
9
|
+
constructor(message, hint) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.hint = hint;
|
|
12
|
+
this.name = "CliError";
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
function handleResultError(result) {
|
|
16
|
+
throw new CliError(result.error, `Error code: ${result.code}`);
|
|
17
|
+
}
|
|
18
|
+
function unwrapResult(result) {
|
|
19
|
+
if (!result.ok) {
|
|
20
|
+
handleResultError(result);
|
|
21
|
+
}
|
|
22
|
+
return result.data;
|
|
23
|
+
}
|
|
24
|
+
function handleError(error) {
|
|
25
|
+
if (error instanceof CliError) {
|
|
26
|
+
console.error(`
|
|
27
|
+
${pc.red("Error:")} ${error.message}`);
|
|
28
|
+
if (error.hint) {
|
|
29
|
+
console.error(`${pc.dim(error.hint)}`);
|
|
30
|
+
}
|
|
31
|
+
} else if (error instanceof Error) {
|
|
32
|
+
console.error(`
|
|
33
|
+
${pc.red("Error:")} ${error.message}`);
|
|
34
|
+
} else {
|
|
35
|
+
console.error(`
|
|
36
|
+
${pc.red("Error:")} An unexpected error occurred.`);
|
|
37
|
+
}
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/commands/login.ts
|
|
42
|
+
import { api } from "@beakcrypt/convex";
|
|
43
|
+
|
|
44
|
+
// src/lib/global-config.ts
|
|
45
|
+
import { mkdir, readFile, writeFile, rm } from "fs/promises";
|
|
46
|
+
import { existsSync } from "fs";
|
|
47
|
+
import { homedir } from "os";
|
|
48
|
+
import { join } from "path";
|
|
49
|
+
var CONFIG_DIR = join(homedir(), ".beakcrypt");
|
|
50
|
+
var AUTH_FILE = join(CONFIG_DIR, "auth.json");
|
|
51
|
+
function getConfigDir() {
|
|
52
|
+
return CONFIG_DIR;
|
|
53
|
+
}
|
|
54
|
+
async function ensureConfigDir() {
|
|
55
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
56
|
+
}
|
|
57
|
+
async function getAuthConfig() {
|
|
58
|
+
if (!existsSync(AUTH_FILE)) return null;
|
|
59
|
+
try {
|
|
60
|
+
const data = await readFile(AUTH_FILE, "utf-8");
|
|
61
|
+
return JSON.parse(data);
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function saveAuthConfig(config) {
|
|
67
|
+
await ensureConfigDir();
|
|
68
|
+
await writeFile(AUTH_FILE, JSON.stringify(config, null, 2), { mode: 384 });
|
|
69
|
+
}
|
|
70
|
+
async function clearAllConfig() {
|
|
71
|
+
if (existsSync(CONFIG_DIR)) {
|
|
72
|
+
await rm(CONFIG_DIR, { recursive: true });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function isLoggedIn() {
|
|
76
|
+
return existsSync(AUTH_FILE);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/lib/auth.ts
|
|
80
|
+
import {
|
|
81
|
+
createServer
|
|
82
|
+
} from "http";
|
|
83
|
+
import { URL } from "url";
|
|
84
|
+
import open from "open";
|
|
85
|
+
import ora from "ora";
|
|
86
|
+
import pc3 from "picocolors";
|
|
87
|
+
|
|
88
|
+
// src/lib/output.ts
|
|
89
|
+
import pc2 from "picocolors";
|
|
90
|
+
function success(message) {
|
|
91
|
+
console.log(`${pc2.green("\u2713")} ${message}`);
|
|
92
|
+
}
|
|
93
|
+
function info(message) {
|
|
94
|
+
console.log(`${pc2.blue("\u2139")} ${message}`);
|
|
95
|
+
}
|
|
96
|
+
function warn(message) {
|
|
97
|
+
console.log(`${pc2.yellow("\u26A0")} ${message}`);
|
|
98
|
+
}
|
|
99
|
+
function dim(message) {
|
|
100
|
+
return pc2.dim(message);
|
|
101
|
+
}
|
|
102
|
+
function bold(message) {
|
|
103
|
+
return pc2.bold(message);
|
|
104
|
+
}
|
|
105
|
+
function link(url) {
|
|
106
|
+
return pc2.cyan(pc2.underline(url));
|
|
107
|
+
}
|
|
108
|
+
function table(rows, headers) {
|
|
109
|
+
const allRows = headers ? [headers, ...rows] : rows;
|
|
110
|
+
const colWidths = [];
|
|
111
|
+
for (const row of allRows) {
|
|
112
|
+
for (let i = 0; i < row.length; i++) {
|
|
113
|
+
colWidths[i] = Math.max(colWidths[i] ?? 0, (row[i] ?? "").length);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
for (let i = 0; i < allRows.length; i++) {
|
|
117
|
+
const row = allRows[i];
|
|
118
|
+
const line = row.map((cell, j) => (cell ?? "").padEnd(colWidths[j] ?? 0)).join(" ");
|
|
119
|
+
if (i === 0 && headers) {
|
|
120
|
+
console.log(pc2.bold(line));
|
|
121
|
+
console.log(colWidths.map((w) => "\u2500".repeat(w)).join("\u2500\u2500"));
|
|
122
|
+
} else {
|
|
123
|
+
console.log(line);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/lib/auth.ts
|
|
129
|
+
function startCallbackServer() {
|
|
130
|
+
return new Promise((resolve3, reject) => {
|
|
131
|
+
let resolveToken;
|
|
132
|
+
const tokenPromise = new Promise((res) => {
|
|
133
|
+
resolveToken = res;
|
|
134
|
+
});
|
|
135
|
+
const server = createServer((req, res) => {
|
|
136
|
+
const url = new URL(req.url ?? "/", `http://localhost`);
|
|
137
|
+
if (url.pathname === "/callback") {
|
|
138
|
+
const sessionToken = url.searchParams.get("session_token");
|
|
139
|
+
if (sessionToken) {
|
|
140
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
141
|
+
res.end(`
|
|
142
|
+
<html>
|
|
143
|
+
<body style="display:flex;justify-content:center;align-items:center;height:100vh;font-family:system-ui;background:#0a0a0a;color:#fafafa">
|
|
144
|
+
<div style="text-align:center">
|
|
145
|
+
<h1>Authenticated!</h1>
|
|
146
|
+
<p>You can close this tab and return to the terminal.</p>
|
|
147
|
+
</div>
|
|
148
|
+
</body>
|
|
149
|
+
</html>
|
|
150
|
+
`);
|
|
151
|
+
resolveToken({ sessionToken });
|
|
152
|
+
setTimeout(() => server.close(), 500);
|
|
153
|
+
} else {
|
|
154
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
155
|
+
res.end("Missing session_token parameter");
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
159
|
+
res.end("Not found");
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
server.listen(0, "127.0.0.1", () => {
|
|
163
|
+
const addr = server.address();
|
|
164
|
+
if (!addr || typeof addr === "string") {
|
|
165
|
+
reject(new Error("Failed to start callback server"));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
resolve3({
|
|
169
|
+
port: addr.port,
|
|
170
|
+
waitForToken: () => tokenPromise
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
server.on("error", reject);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
async function loginFlow(siteUrl, convexUrl, convexSiteUrl) {
|
|
177
|
+
const { port, waitForToken } = await startCallbackServer();
|
|
178
|
+
const callbackUrl = `http://localhost:${port}/callback`;
|
|
179
|
+
const loginUrl = `${siteUrl}/auth/cli?callback=${encodeURIComponent(callbackUrl)}`;
|
|
180
|
+
const isTTY = process.stdin.isTTY;
|
|
181
|
+
if (isTTY) {
|
|
182
|
+
info(`Opening browser to log in...`);
|
|
183
|
+
console.log(`${pc3.dim("If the browser doesn't open, visit:")}`);
|
|
184
|
+
console.log(`${link(loginUrl)}
|
|
185
|
+
`);
|
|
186
|
+
try {
|
|
187
|
+
await open(loginUrl);
|
|
188
|
+
} catch {
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
console.log(`Visit this URL to log in:
|
|
192
|
+
${link(loginUrl)}
|
|
193
|
+
`);
|
|
194
|
+
}
|
|
195
|
+
const spinner = ora("Waiting for authentication...").start();
|
|
196
|
+
const result = await waitForToken();
|
|
197
|
+
spinner.stop();
|
|
198
|
+
const config = {
|
|
199
|
+
sessionToken: result.sessionToken,
|
|
200
|
+
convexUrl,
|
|
201
|
+
convexSiteUrl,
|
|
202
|
+
siteUrl
|
|
203
|
+
};
|
|
204
|
+
await saveAuthConfig(config);
|
|
205
|
+
return config;
|
|
206
|
+
}
|
|
207
|
+
function getDefaultUrls() {
|
|
208
|
+
return {
|
|
209
|
+
siteUrl: process.env.BEAKCRYPT_SITE_URL ?? "https://app.beakcrypt.com",
|
|
210
|
+
convexUrl: process.env.BEAKCRYPT_CONVEX_URL ?? "",
|
|
211
|
+
convexSiteUrl: process.env.BEAKCRYPT_CONVEX_SITE_URL ?? ""
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function validateUrls(urls) {
|
|
215
|
+
if (!urls.convexUrl) {
|
|
216
|
+
throw new CliError(
|
|
217
|
+
"BEAKCRYPT_CONVEX_URL is not configured.",
|
|
218
|
+
"Set the BEAKCRYPT_CONVEX_URL environment variable or configure it in ~/.beakcrypt/auth.json"
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
if (!urls.convexSiteUrl) {
|
|
222
|
+
throw new CliError(
|
|
223
|
+
"BEAKCRYPT_CONVEX_SITE_URL is not configured.",
|
|
224
|
+
"Set the BEAKCRYPT_CONVEX_SITE_URL environment variable or configure it in ~/.beakcrypt/auth.json"
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/lib/convex-client.ts
|
|
230
|
+
import { ConvexHttpClient } from "convex/browser";
|
|
231
|
+
var clientInstance = null;
|
|
232
|
+
var cachedAuth = null;
|
|
233
|
+
async function getConvexToken(auth) {
|
|
234
|
+
const url = `${auth.convexSiteUrl}/api/auth/convex/token`;
|
|
235
|
+
const res = await fetch(url, {
|
|
236
|
+
method: "GET",
|
|
237
|
+
headers: {
|
|
238
|
+
Authorization: `Bearer ${auth.sessionToken}`
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
if (!res.ok) {
|
|
242
|
+
const body = await res.text().catch(() => "");
|
|
243
|
+
console.error(`Token exchange failed: ${res.status} ${res.statusText}`);
|
|
244
|
+
console.error(`URL: ${url}`);
|
|
245
|
+
if (body) console.error(`Body: ${body}`);
|
|
246
|
+
throw new CliError("Session expired. Please run `beakcrypt login` again.");
|
|
247
|
+
}
|
|
248
|
+
const data = await res.json();
|
|
249
|
+
return data.token;
|
|
250
|
+
}
|
|
251
|
+
async function getClient() {
|
|
252
|
+
const auth = await getAuthConfig();
|
|
253
|
+
if (!auth) {
|
|
254
|
+
throw new CliError("Not logged in. Run `beakcrypt login` first.");
|
|
255
|
+
}
|
|
256
|
+
cachedAuth = auth;
|
|
257
|
+
if (!clientInstance) {
|
|
258
|
+
clientInstance = new ConvexHttpClient(auth.convexUrl);
|
|
259
|
+
}
|
|
260
|
+
const token = await getConvexToken(auth);
|
|
261
|
+
clientInstance.setAuth(token);
|
|
262
|
+
return clientInstance;
|
|
263
|
+
}
|
|
264
|
+
async function query(fn, args) {
|
|
265
|
+
const client = await getClient();
|
|
266
|
+
return client.query(fn, args);
|
|
267
|
+
}
|
|
268
|
+
async function mutation(fn, args) {
|
|
269
|
+
const client = await getClient();
|
|
270
|
+
return client.mutation(fn, args);
|
|
271
|
+
}
|
|
272
|
+
function getSessionToken() {
|
|
273
|
+
return cachedAuth?.sessionToken ?? null;
|
|
274
|
+
}
|
|
275
|
+
function getSiteUrl() {
|
|
276
|
+
if (!cachedAuth) throw new CliError("Not logged in.");
|
|
277
|
+
return cachedAuth.siteUrl;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// src/commands/login.ts
|
|
281
|
+
async function loginCommand() {
|
|
282
|
+
const existing = await getAuthConfig();
|
|
283
|
+
if (existing) {
|
|
284
|
+
info("Already logged in. Refreshing session...");
|
|
285
|
+
}
|
|
286
|
+
const urls = existing ? {
|
|
287
|
+
siteUrl: existing.siteUrl,
|
|
288
|
+
convexUrl: existing.convexUrl,
|
|
289
|
+
convexSiteUrl: existing.convexSiteUrl
|
|
290
|
+
} : getDefaultUrls();
|
|
291
|
+
validateUrls(urls);
|
|
292
|
+
await loginFlow(urls.siteUrl, urls.convexUrl, urls.convexSiteUrl);
|
|
293
|
+
try {
|
|
294
|
+
const user = await query(api.auth.getCurrentUser, {});
|
|
295
|
+
if (user) {
|
|
296
|
+
success(`Logged in as ${bold(user.email)}`);
|
|
297
|
+
} else {
|
|
298
|
+
success("Logged in successfully.");
|
|
299
|
+
}
|
|
300
|
+
} catch {
|
|
301
|
+
success("Logged in successfully.");
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// src/lib/interactive.ts
|
|
306
|
+
import prompts from "prompts";
|
|
307
|
+
function isInteractive() {
|
|
308
|
+
return Boolean(process.stdin.isTTY);
|
|
309
|
+
}
|
|
310
|
+
function requireInteractive(command) {
|
|
311
|
+
if (!isInteractive()) {
|
|
312
|
+
throw new CliError(
|
|
313
|
+
`Cannot run interactive prompts in non-interactive mode.`,
|
|
314
|
+
`Run \`beakcrypt ${command}\` in an interactive terminal, or provide required flags.`
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
async function select(opts) {
|
|
319
|
+
const { value } = await prompts(
|
|
320
|
+
{
|
|
321
|
+
type: "select",
|
|
322
|
+
name: "value",
|
|
323
|
+
message: opts.message,
|
|
324
|
+
choices: opts.choices
|
|
325
|
+
},
|
|
326
|
+
{ onCancel: () => process.exit(0) }
|
|
327
|
+
);
|
|
328
|
+
return value;
|
|
329
|
+
}
|
|
330
|
+
async function confirm(message, initial = false) {
|
|
331
|
+
const { value } = await prompts(
|
|
332
|
+
{
|
|
333
|
+
type: "confirm",
|
|
334
|
+
name: "value",
|
|
335
|
+
message,
|
|
336
|
+
initial
|
|
337
|
+
},
|
|
338
|
+
{ onCancel: () => process.exit(0) }
|
|
339
|
+
);
|
|
340
|
+
return value;
|
|
341
|
+
}
|
|
342
|
+
async function text(opts) {
|
|
343
|
+
const { value } = await prompts(
|
|
344
|
+
{
|
|
345
|
+
type: "text",
|
|
346
|
+
name: "value",
|
|
347
|
+
message: opts.message,
|
|
348
|
+
...opts.placeholder ? { initial: opts.placeholder } : {},
|
|
349
|
+
validate: opts.validate ? (v) => {
|
|
350
|
+
const result = opts.validate(v);
|
|
351
|
+
return result === true ? true : result;
|
|
352
|
+
} : void 0
|
|
353
|
+
},
|
|
354
|
+
{ onCancel: () => process.exit(0) }
|
|
355
|
+
);
|
|
356
|
+
return value;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// src/commands/logout.ts
|
|
360
|
+
async function logoutCommand(opts) {
|
|
361
|
+
if (!isLoggedIn()) {
|
|
362
|
+
info("Not currently logged in.");
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (!opts.yes) {
|
|
366
|
+
const confirmed = await confirm(
|
|
367
|
+
"This will remove all credentials and device keys. Continue?"
|
|
368
|
+
);
|
|
369
|
+
if (!confirmed) return;
|
|
370
|
+
}
|
|
371
|
+
await clearAllConfig();
|
|
372
|
+
success("Logged out. All credentials and keys removed.");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// src/commands/whoami.ts
|
|
376
|
+
import { api as api3 } from "@beakcrypt/convex";
|
|
377
|
+
|
|
378
|
+
// src/lib/context.ts
|
|
379
|
+
import { api as api2 } from "@beakcrypt/convex";
|
|
380
|
+
|
|
381
|
+
// src/lib/project-config.ts
|
|
382
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, appendFile } from "fs/promises";
|
|
383
|
+
import { existsSync as existsSync2 } from "fs";
|
|
384
|
+
import { join as join2, dirname } from "path";
|
|
385
|
+
var CONFIG_DIR_NAME = ".beakcrypt";
|
|
386
|
+
var CONFIG_FILE_NAME = "project.json";
|
|
387
|
+
function findProjectConfigDir(startDir) {
|
|
388
|
+
let dir = startDir ?? process.cwd();
|
|
389
|
+
const root = dirname(dir) === dir ? dir : "/";
|
|
390
|
+
while (true) {
|
|
391
|
+
const configPath = join2(dir, CONFIG_DIR_NAME, CONFIG_FILE_NAME);
|
|
392
|
+
if (existsSync2(configPath)) {
|
|
393
|
+
return join2(dir, CONFIG_DIR_NAME);
|
|
394
|
+
}
|
|
395
|
+
const parent = dirname(dir);
|
|
396
|
+
if (parent === dir || dir === root) break;
|
|
397
|
+
dir = parent;
|
|
398
|
+
}
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
async function getProjectConfig(startDir) {
|
|
402
|
+
const configDir = findProjectConfigDir(startDir);
|
|
403
|
+
if (!configDir) return null;
|
|
404
|
+
try {
|
|
405
|
+
const data = await readFile2(join2(configDir, CONFIG_FILE_NAME), "utf-8");
|
|
406
|
+
return JSON.parse(data);
|
|
407
|
+
} catch {
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
async function saveProjectConfig(config, dir) {
|
|
412
|
+
const baseDir = dir ?? process.cwd();
|
|
413
|
+
const configDir = join2(baseDir, CONFIG_DIR_NAME);
|
|
414
|
+
await mkdir2(configDir, { recursive: true });
|
|
415
|
+
await writeFile2(
|
|
416
|
+
join2(configDir, CONFIG_FILE_NAME),
|
|
417
|
+
JSON.stringify(config, null, 2)
|
|
418
|
+
);
|
|
419
|
+
await ensureGitignore(baseDir);
|
|
420
|
+
return configDir;
|
|
421
|
+
}
|
|
422
|
+
async function ensureGitignore(baseDir) {
|
|
423
|
+
const gitignorePath = join2(baseDir, ".gitignore");
|
|
424
|
+
if (existsSync2(gitignorePath)) {
|
|
425
|
+
const content = await readFile2(gitignorePath, "utf-8");
|
|
426
|
+
if (content.includes(CONFIG_DIR_NAME)) return;
|
|
427
|
+
const suffix = content.endsWith("\n") ? "" : "\n";
|
|
428
|
+
await appendFile(
|
|
429
|
+
gitignorePath,
|
|
430
|
+
`${suffix}
|
|
431
|
+
# Beakcrypt CLI
|
|
432
|
+
${CONFIG_DIR_NAME}
|
|
433
|
+
`
|
|
434
|
+
);
|
|
435
|
+
} else {
|
|
436
|
+
await writeFile2(gitignorePath, `# Beakcrypt CLI
|
|
437
|
+
${CONFIG_DIR_NAME}
|
|
438
|
+
`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// src/lib/context.ts
|
|
443
|
+
async function ensureAuth() {
|
|
444
|
+
const auth = await getAuthConfig();
|
|
445
|
+
if (auth) return;
|
|
446
|
+
if (!isInteractive()) {
|
|
447
|
+
throw new CliError("Not logged in. Run `beakcrypt login` first.");
|
|
448
|
+
}
|
|
449
|
+
const shouldLogin = await confirm(
|
|
450
|
+
"You're not logged in. Log in now?",
|
|
451
|
+
true
|
|
452
|
+
);
|
|
453
|
+
if (!shouldLogin) process.exit(0);
|
|
454
|
+
await loginCommand();
|
|
455
|
+
}
|
|
456
|
+
async function resolveContext(flags) {
|
|
457
|
+
await ensureAuth();
|
|
458
|
+
const projectConfig = await getProjectConfig();
|
|
459
|
+
const orgId = flags.org ? await resolveOrgBySlug(flags.org) : projectConfig?.orgId;
|
|
460
|
+
const orgSlug = flags.org ?? projectConfig?.orgSlug;
|
|
461
|
+
const projectId = flags.project ? await resolveProjectByName(orgId, flags.project) : projectConfig?.projectId;
|
|
462
|
+
const projectName = flags.project ?? projectConfig?.projectName;
|
|
463
|
+
const envName = flags.env ?? projectConfig?.defaultEnv;
|
|
464
|
+
if (!orgId || !projectId || !envName || !orgSlug || !projectName) {
|
|
465
|
+
if (!isInteractive()) {
|
|
466
|
+
throw new CliError(
|
|
467
|
+
"No project linked in this directory.",
|
|
468
|
+
"Run `beakcrypt link` to connect this directory to a project."
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
const config = await interactiveLink();
|
|
472
|
+
const envId2 = await resolveEnvId(
|
|
473
|
+
config.projectId,
|
|
474
|
+
flags.env ?? config.defaultEnv
|
|
475
|
+
);
|
|
476
|
+
return {
|
|
477
|
+
orgId: config.orgId,
|
|
478
|
+
orgSlug: config.orgSlug,
|
|
479
|
+
projectId: config.projectId,
|
|
480
|
+
projectName: config.projectName,
|
|
481
|
+
envName: flags.env ?? config.defaultEnv,
|
|
482
|
+
environmentId: envId2
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
const envId = await resolveEnvId(projectId, envName);
|
|
486
|
+
return {
|
|
487
|
+
orgId,
|
|
488
|
+
orgSlug,
|
|
489
|
+
projectId,
|
|
490
|
+
projectName,
|
|
491
|
+
envName,
|
|
492
|
+
environmentId: envId
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
async function resolveOrgBySlug(slug) {
|
|
496
|
+
const result = await query(api2.organizations.getBySlug, { slug });
|
|
497
|
+
const org2 = unwrapResult(result);
|
|
498
|
+
return org2._id;
|
|
499
|
+
}
|
|
500
|
+
async function resolveProjectByName(orgId, name) {
|
|
501
|
+
const result = await query(api2.projects.getByName, {
|
|
502
|
+
orgId,
|
|
503
|
+
name
|
|
504
|
+
});
|
|
505
|
+
const project = unwrapResult(result);
|
|
506
|
+
return project._id;
|
|
507
|
+
}
|
|
508
|
+
async function resolveEnvId(projectId, envName) {
|
|
509
|
+
if (envName === "local") {
|
|
510
|
+
await mutation(api2.environments.ensurePersonalLocal, {
|
|
511
|
+
projectId,
|
|
512
|
+
syncFromDev: false
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
const result = await query(api2.environments.list, {
|
|
516
|
+
projectId
|
|
517
|
+
});
|
|
518
|
+
const envs = unwrapResult(result);
|
|
519
|
+
const env2 = envs.find((e) => e.name === envName);
|
|
520
|
+
if (!env2) {
|
|
521
|
+
throw new CliError(`Environment "${envName}" not found.`);
|
|
522
|
+
}
|
|
523
|
+
return env2._id;
|
|
524
|
+
}
|
|
525
|
+
async function interactiveLink() {
|
|
526
|
+
requireInteractive("link");
|
|
527
|
+
const orgsResult = await query(api2.organizations.list, {});
|
|
528
|
+
const orgs = unwrapResult(orgsResult);
|
|
529
|
+
if (orgs.length === 0) {
|
|
530
|
+
throw new CliError("You don't belong to any organizations.");
|
|
531
|
+
}
|
|
532
|
+
const orgId = await select({
|
|
533
|
+
message: "Select an organization:",
|
|
534
|
+
choices: orgs.map((o) => ({
|
|
535
|
+
title: `${o.name} (${o.slug})`,
|
|
536
|
+
value: o._id
|
|
537
|
+
}))
|
|
538
|
+
});
|
|
539
|
+
const selectedOrg = orgs.find((o) => o._id === orgId);
|
|
540
|
+
const action = await select({
|
|
541
|
+
message: "What would you like to do?",
|
|
542
|
+
choices: [
|
|
543
|
+
{ title: "Link to an existing project", value: "link" },
|
|
544
|
+
{ title: "Create a new project", value: "create" }
|
|
545
|
+
]
|
|
546
|
+
});
|
|
547
|
+
let projectId;
|
|
548
|
+
let projectName;
|
|
549
|
+
if (action === "create") {
|
|
550
|
+
const name = await text({
|
|
551
|
+
message: "Enter project name:",
|
|
552
|
+
validate: (v) => v.trim().length > 0 ? true : "Project name is required"
|
|
553
|
+
});
|
|
554
|
+
const createResult = await mutation(api2.projects.create, {
|
|
555
|
+
name: name.trim(),
|
|
556
|
+
orgId
|
|
557
|
+
});
|
|
558
|
+
const project = unwrapResult(createResult);
|
|
559
|
+
projectId = project._id;
|
|
560
|
+
projectName = project.name;
|
|
561
|
+
success(`Created project "${projectName}" in ${selectedOrg.name}`);
|
|
562
|
+
} else {
|
|
563
|
+
const projectsResult = await query(api2.projects.list, {
|
|
564
|
+
orgId
|
|
565
|
+
});
|
|
566
|
+
const projects = unwrapResult(projectsResult);
|
|
567
|
+
if (projects.length === 0) {
|
|
568
|
+
throw new CliError("No projects in this organization. Create one first.");
|
|
569
|
+
}
|
|
570
|
+
projectId = await select({
|
|
571
|
+
message: "Select a project:",
|
|
572
|
+
choices: projects.map((p) => ({
|
|
573
|
+
title: p.name,
|
|
574
|
+
value: p._id
|
|
575
|
+
}))
|
|
576
|
+
});
|
|
577
|
+
const selectedProject = projects.find(
|
|
578
|
+
(p) => p._id === projectId
|
|
579
|
+
);
|
|
580
|
+
projectName = selectedProject.name;
|
|
581
|
+
}
|
|
582
|
+
await mutation(api2.environments.ensurePersonalLocal, {
|
|
583
|
+
projectId,
|
|
584
|
+
syncFromDev: false
|
|
585
|
+
});
|
|
586
|
+
const envsResult = await query(api2.environments.list, {
|
|
587
|
+
projectId
|
|
588
|
+
});
|
|
589
|
+
const envs = unwrapResult(envsResult);
|
|
590
|
+
const defaultEnv = await select({
|
|
591
|
+
message: "Select a default environment:",
|
|
592
|
+
choices: envs.map(
|
|
593
|
+
(e) => ({
|
|
594
|
+
title: e.isPersonal ? `${e.name} (personal)` : e.name,
|
|
595
|
+
value: e.name
|
|
596
|
+
})
|
|
597
|
+
)
|
|
598
|
+
});
|
|
599
|
+
const config = {
|
|
600
|
+
orgId,
|
|
601
|
+
orgSlug: selectedOrg.slug,
|
|
602
|
+
projectId,
|
|
603
|
+
projectName,
|
|
604
|
+
defaultEnv
|
|
605
|
+
};
|
|
606
|
+
const configDir = await saveProjectConfig(config);
|
|
607
|
+
success(
|
|
608
|
+
`Linked to ${selectedOrg.slug}/${projectName} (${defaultEnv})`
|
|
609
|
+
);
|
|
610
|
+
info(`Created ${configDir}/project.json`);
|
|
611
|
+
info(`Added .beakcrypt to .gitignore`);
|
|
612
|
+
return config;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// src/commands/whoami.ts
|
|
616
|
+
async function whoamiCommand() {
|
|
617
|
+
await ensureAuth();
|
|
618
|
+
const user = await query(api3.auth.getCurrentUser, {});
|
|
619
|
+
if (!user) {
|
|
620
|
+
throw new CliError(
|
|
621
|
+
"Could not retrieve user info. Try `beakcrypt login` again."
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
console.log();
|
|
625
|
+
console.log(` ${bold("Email:")} ${user.email}`);
|
|
626
|
+
console.log(` ${bold("Name:")} ${user.name}`);
|
|
627
|
+
console.log();
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// src/lib/key-manager.ts
|
|
631
|
+
import ora2 from "ora";
|
|
632
|
+
import open2 from "open";
|
|
633
|
+
|
|
634
|
+
// ../crypto/src/core.ts
|
|
635
|
+
var RSA_ALGORITHM = {
|
|
636
|
+
name: "RSA-OAEP",
|
|
637
|
+
modulusLength: 4096,
|
|
638
|
+
publicExponent: new Uint8Array([1, 0, 1]),
|
|
639
|
+
hash: "SHA-256"
|
|
640
|
+
};
|
|
641
|
+
var AES_ALGORITHM = "AES-GCM";
|
|
642
|
+
var AES_KEY_LENGTH = 256;
|
|
643
|
+
var IV_BYTE_LENGTH = 12;
|
|
644
|
+
async function generateKeyPair() {
|
|
645
|
+
const keyPair = await crypto.subtle.generateKey(RSA_ALGORITHM, true, [
|
|
646
|
+
"wrapKey",
|
|
647
|
+
"unwrapKey"
|
|
648
|
+
]);
|
|
649
|
+
const [publicKey, privateKey] = await Promise.all([
|
|
650
|
+
crypto.subtle.exportKey("jwk", keyPair.publicKey),
|
|
651
|
+
crypto.subtle.exportKey("jwk", keyPair.privateKey)
|
|
652
|
+
]);
|
|
653
|
+
return { publicKey, privateKey };
|
|
654
|
+
}
|
|
655
|
+
async function unwrapOrgKey(wrappedKeyBase64, privateKeyJwk) {
|
|
656
|
+
const privateKey = await crypto.subtle.importKey(
|
|
657
|
+
"jwk",
|
|
658
|
+
privateKeyJwk,
|
|
659
|
+
RSA_ALGORITHM,
|
|
660
|
+
false,
|
|
661
|
+
["unwrapKey"]
|
|
662
|
+
);
|
|
663
|
+
const orgKey = await crypto.subtle.unwrapKey(
|
|
664
|
+
"raw",
|
|
665
|
+
base64ToBuffer(wrappedKeyBase64),
|
|
666
|
+
privateKey,
|
|
667
|
+
{ name: "RSA-OAEP" },
|
|
668
|
+
{ name: AES_ALGORITHM, length: AES_KEY_LENGTH },
|
|
669
|
+
true,
|
|
670
|
+
["encrypt", "decrypt"]
|
|
671
|
+
);
|
|
672
|
+
const raw = await crypto.subtle.exportKey("raw", orgKey);
|
|
673
|
+
return bufferToBase64(raw);
|
|
674
|
+
}
|
|
675
|
+
async function encryptSecret(plaintext, orgKeyBase64) {
|
|
676
|
+
const key = await crypto.subtle.importKey(
|
|
677
|
+
"raw",
|
|
678
|
+
base64ToBuffer(orgKeyBase64),
|
|
679
|
+
{ name: AES_ALGORITHM },
|
|
680
|
+
false,
|
|
681
|
+
["encrypt"]
|
|
682
|
+
);
|
|
683
|
+
const iv = crypto.getRandomValues(new Uint8Array(IV_BYTE_LENGTH));
|
|
684
|
+
const encoded = new TextEncoder().encode(plaintext);
|
|
685
|
+
const ciphertext = await crypto.subtle.encrypt(
|
|
686
|
+
{ name: AES_ALGORITHM, iv },
|
|
687
|
+
key,
|
|
688
|
+
encoded
|
|
689
|
+
);
|
|
690
|
+
return `${bufferToBase64(iv.buffer)}:${bufferToBase64(ciphertext)}`;
|
|
691
|
+
}
|
|
692
|
+
async function decryptSecret(encryptedValue, orgKeyBase64) {
|
|
693
|
+
const [ivBase64, ciphertextBase64] = encryptedValue.split(":");
|
|
694
|
+
if (!ivBase64 || !ciphertextBase64) {
|
|
695
|
+
throw new Error(
|
|
696
|
+
"Invalid encrypted value format \u2014 expected 'iv:ciphertext'"
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
const key = await crypto.subtle.importKey(
|
|
700
|
+
"raw",
|
|
701
|
+
base64ToBuffer(orgKeyBase64),
|
|
702
|
+
{ name: AES_ALGORITHM },
|
|
703
|
+
false,
|
|
704
|
+
["decrypt"]
|
|
705
|
+
);
|
|
706
|
+
const decrypted = await crypto.subtle.decrypt(
|
|
707
|
+
{ name: AES_ALGORITHM, iv: base64ToBuffer(ivBase64) },
|
|
708
|
+
key,
|
|
709
|
+
base64ToBuffer(ciphertextBase64)
|
|
710
|
+
);
|
|
711
|
+
return new TextDecoder().decode(decrypted);
|
|
712
|
+
}
|
|
713
|
+
function bufferToBase64(buffer) {
|
|
714
|
+
const bytes = new Uint8Array(buffer);
|
|
715
|
+
let binary = "";
|
|
716
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
717
|
+
binary += String.fromCharCode(bytes[i]);
|
|
718
|
+
}
|
|
719
|
+
return btoa(binary);
|
|
720
|
+
}
|
|
721
|
+
function base64ToBuffer(base64) {
|
|
722
|
+
const binary = atob(base64);
|
|
723
|
+
const bytes = new Uint8Array(binary.length);
|
|
724
|
+
for (let i = 0; i < binary.length; i++) {
|
|
725
|
+
bytes[i] = binary.charCodeAt(i);
|
|
726
|
+
}
|
|
727
|
+
return bytes.buffer;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// src/lib/key-manager.ts
|
|
731
|
+
import { api as api4 } from "@beakcrypt/convex";
|
|
732
|
+
|
|
733
|
+
// src/lib/key-store.ts
|
|
734
|
+
import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3, rm as rm2 } from "fs/promises";
|
|
735
|
+
import { existsSync as existsSync3 } from "fs";
|
|
736
|
+
import { join as join3 } from "path";
|
|
737
|
+
function getKeysDir() {
|
|
738
|
+
return join3(getConfigDir(), "keys");
|
|
739
|
+
}
|
|
740
|
+
function getKeyFile(orgId) {
|
|
741
|
+
return join3(getKeysDir(), `${orgId}.json`);
|
|
742
|
+
}
|
|
743
|
+
async function getStoredKey(orgId) {
|
|
744
|
+
const file = getKeyFile(orgId);
|
|
745
|
+
if (!existsSync3(file)) return null;
|
|
746
|
+
try {
|
|
747
|
+
const data = await readFile3(file, "utf-8");
|
|
748
|
+
return JSON.parse(data);
|
|
749
|
+
} catch {
|
|
750
|
+
return null;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
async function saveKey(orgId, data) {
|
|
754
|
+
const dir = getKeysDir();
|
|
755
|
+
await mkdir3(dir, { recursive: true });
|
|
756
|
+
await writeFile3(getKeyFile(orgId), JSON.stringify(data, null, 2), {
|
|
757
|
+
mode: 384
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
async function updateWrappedOrgKey(orgId, wrappedOrgKey) {
|
|
761
|
+
const existing = await getStoredKey(orgId);
|
|
762
|
+
if (!existing) throw new Error(`No key stored for org ${orgId}`);
|
|
763
|
+
existing.wrappedOrgKey = wrappedOrgKey;
|
|
764
|
+
await saveKey(orgId, existing);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// src/lib/key-manager.ts
|
|
768
|
+
async function ensureOrgKey(orgId) {
|
|
769
|
+
const stored = await getStoredKey(orgId);
|
|
770
|
+
if (stored?.wrappedOrgKey) {
|
|
771
|
+
return await unwrapOrgKey(stored.wrappedOrgKey, stored.privateKey);
|
|
772
|
+
}
|
|
773
|
+
if (stored?.keyId) {
|
|
774
|
+
const keyResult = await query(api4.keys.getMyKey, {
|
|
775
|
+
orgId,
|
|
776
|
+
publicKey: JSON.stringify(stored.publicKey)
|
|
777
|
+
});
|
|
778
|
+
const keyRecord = unwrapResult(keyResult);
|
|
779
|
+
if (keyRecord?.status === "active" && keyRecord.wrappedOrgKey) {
|
|
780
|
+
await updateWrappedOrgKey(orgId, keyRecord.wrappedOrgKey);
|
|
781
|
+
return await unwrapOrgKey(
|
|
782
|
+
keyRecord.wrappedOrgKey,
|
|
783
|
+
stored.privateKey
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
if (keyRecord?.status === "pending") {
|
|
787
|
+
return await waitForApproval(orgId, stored);
|
|
788
|
+
}
|
|
789
|
+
throw new Error(
|
|
790
|
+
"Device key is not active. An admin may need to approve this device."
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
return await registerNewDevice(orgId);
|
|
794
|
+
}
|
|
795
|
+
async function registerNewDevice(orgId) {
|
|
796
|
+
const sessionToken = getSessionToken();
|
|
797
|
+
if (!sessionToken) throw new Error("Not logged in");
|
|
798
|
+
const spinner = ora2("Generating device keys...").start();
|
|
799
|
+
let keyPair;
|
|
800
|
+
let keyRecord;
|
|
801
|
+
try {
|
|
802
|
+
keyPair = await generateKeyPair();
|
|
803
|
+
spinner.text = "Registering device with organization...";
|
|
804
|
+
const result = await mutation(api4.keys.registerKey, {
|
|
805
|
+
orgId,
|
|
806
|
+
publicKey: JSON.stringify(keyPair.publicKey),
|
|
807
|
+
sessionToken
|
|
808
|
+
});
|
|
809
|
+
keyRecord = unwrapResult(result);
|
|
810
|
+
spinner.stop();
|
|
811
|
+
} catch (err) {
|
|
812
|
+
spinner.fail("Device registration failed.");
|
|
813
|
+
throw err;
|
|
814
|
+
}
|
|
815
|
+
const stored = {
|
|
816
|
+
publicKey: keyPair.publicKey,
|
|
817
|
+
privateKey: keyPair.privateKey,
|
|
818
|
+
keyId: keyRecord._id,
|
|
819
|
+
wrappedOrgKey: keyRecord.wrappedOrgKey
|
|
820
|
+
};
|
|
821
|
+
await saveKey(orgId, stored);
|
|
822
|
+
if (keyRecord.status === "active" && keyRecord.wrappedOrgKey) {
|
|
823
|
+
success("Device registered and activated.");
|
|
824
|
+
return await unwrapOrgKey(
|
|
825
|
+
keyRecord.wrappedOrgKey,
|
|
826
|
+
keyPair.privateKey
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
return await waitForApproval(orgId, stored);
|
|
830
|
+
}
|
|
831
|
+
async function waitForApproval(orgId, stored) {
|
|
832
|
+
try {
|
|
833
|
+
const projectConfig = await getProjectConfig();
|
|
834
|
+
if (projectConfig?.orgSlug && stored.keyId) {
|
|
835
|
+
const siteUrl = getSiteUrl();
|
|
836
|
+
const approveUrl = `${siteUrl}/${projectConfig.orgSlug}/sessions?approveSession=${stored.keyId}`;
|
|
837
|
+
info("Opening browser to approve this device...");
|
|
838
|
+
console.log(`${dim("If the browser doesn't open, visit:")}`);
|
|
839
|
+
console.log(`${link(approveUrl)}
|
|
840
|
+
`);
|
|
841
|
+
try {
|
|
842
|
+
await open2(approveUrl);
|
|
843
|
+
} catch {
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
} catch {
|
|
847
|
+
}
|
|
848
|
+
const spinner = ora2("Waiting for device to be approved...").start();
|
|
849
|
+
spinner.indent = 2;
|
|
850
|
+
while (true) {
|
|
851
|
+
await new Promise((r) => setTimeout(r, 5e3));
|
|
852
|
+
const result = await query(api4.keys.getMyKey, {
|
|
853
|
+
orgId,
|
|
854
|
+
publicKey: JSON.stringify(stored.publicKey)
|
|
855
|
+
});
|
|
856
|
+
const keyRecord = unwrapResult(result);
|
|
857
|
+
if (keyRecord?.status === "active" && keyRecord.wrappedOrgKey) {
|
|
858
|
+
spinner.stop();
|
|
859
|
+
await updateWrappedOrgKey(orgId, keyRecord.wrappedOrgKey);
|
|
860
|
+
success("Device approved!");
|
|
861
|
+
return await unwrapOrgKey(
|
|
862
|
+
keyRecord.wrappedOrgKey,
|
|
863
|
+
stored.privateKey
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
if (keyRecord?.status === "revoked") {
|
|
867
|
+
spinner.stop();
|
|
868
|
+
throw new Error("Device key was revoked. Re-run `beakcrypt login`.");
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// src/commands/link.ts
|
|
874
|
+
async function linkCommand() {
|
|
875
|
+
await ensureAuth();
|
|
876
|
+
const config = await interactiveLink();
|
|
877
|
+
await ensureOrgKey(config.orgId);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// src/commands/pull.ts
|
|
881
|
+
import { resolve } from "path";
|
|
882
|
+
import ora3 from "ora";
|
|
883
|
+
import { api as api5 } from "@beakcrypt/convex";
|
|
884
|
+
|
|
885
|
+
// src/lib/env-file.ts
|
|
886
|
+
import { readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
887
|
+
import { existsSync as existsSync4 } from "fs";
|
|
888
|
+
function parseEnvFile(content) {
|
|
889
|
+
const env2 = {};
|
|
890
|
+
for (const line of content.split("\n")) {
|
|
891
|
+
const trimmed = line.trim();
|
|
892
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
893
|
+
const eqIndex = trimmed.indexOf("=");
|
|
894
|
+
if (eqIndex === -1) continue;
|
|
895
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
896
|
+
let value = trimmed.slice(eqIndex + 1).trim();
|
|
897
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
898
|
+
value = value.slice(1, -1);
|
|
899
|
+
}
|
|
900
|
+
if (key) {
|
|
901
|
+
env2[key] = value;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
return env2;
|
|
905
|
+
}
|
|
906
|
+
function serializeEnvFile(env2) {
|
|
907
|
+
const lines = [
|
|
908
|
+
"# Generated by Beakcrypt CLI",
|
|
909
|
+
`# ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
910
|
+
""
|
|
911
|
+
];
|
|
912
|
+
const sorted = Object.entries(env2).sort(([a], [b]) => a.localeCompare(b));
|
|
913
|
+
for (const [key, value] of sorted) {
|
|
914
|
+
if (/[\s#]/.test(value)) {
|
|
915
|
+
lines.push(`${key}="${value}"`);
|
|
916
|
+
} else {
|
|
917
|
+
lines.push(`${key}=${value}`);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
lines.push("");
|
|
921
|
+
return lines.join("\n");
|
|
922
|
+
}
|
|
923
|
+
async function readEnvFile(filePath) {
|
|
924
|
+
if (!existsSync4(filePath)) {
|
|
925
|
+
return {};
|
|
926
|
+
}
|
|
927
|
+
const content = await readFile4(filePath, "utf-8");
|
|
928
|
+
return parseEnvFile(content);
|
|
929
|
+
}
|
|
930
|
+
async function writeEnvFile(filePath, env2) {
|
|
931
|
+
await writeFile4(filePath, serializeEnvFile(env2));
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// src/commands/pull.ts
|
|
935
|
+
async function pullCommand(file, opts) {
|
|
936
|
+
const filePath = resolve(file ?? ".env.local");
|
|
937
|
+
const ctx = await resolveContext(opts);
|
|
938
|
+
const spinner = ora3("Pulling secrets...").start();
|
|
939
|
+
const orgKey = await ensureOrgKey(ctx.orgId);
|
|
940
|
+
const secretsResult = await query(api5.secrets.list, {
|
|
941
|
+
environmentId: ctx.environmentId
|
|
942
|
+
});
|
|
943
|
+
const secrets2 = unwrapResult(secretsResult);
|
|
944
|
+
const decrypted = {};
|
|
945
|
+
for (const secret of secrets2) {
|
|
946
|
+
decrypted[secret.key] = await decryptSecret(secret.encryptedValue, orgKey);
|
|
947
|
+
}
|
|
948
|
+
await writeEnvFile(filePath, decrypted);
|
|
949
|
+
spinner.stop();
|
|
950
|
+
success(
|
|
951
|
+
`Pulled ${secrets2.length} secret${secrets2.length === 1 ? "" : "s"} to ${file ?? ".env.local"}`
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// src/commands/push.ts
|
|
956
|
+
import { resolve as resolve2 } from "path";
|
|
957
|
+
import { existsSync as existsSync5 } from "fs";
|
|
958
|
+
import ora4 from "ora";
|
|
959
|
+
import { api as api6 } from "@beakcrypt/convex";
|
|
960
|
+
async function pushCommand(file, opts) {
|
|
961
|
+
const filePath = resolve2(file ?? ".env.local");
|
|
962
|
+
if (!existsSync5(filePath)) {
|
|
963
|
+
throw new CliError(`File not found: ${filePath}`);
|
|
964
|
+
}
|
|
965
|
+
const ctx = await resolveContext(opts);
|
|
966
|
+
const env2 = await readEnvFile(filePath);
|
|
967
|
+
const keys = Object.keys(env2);
|
|
968
|
+
if (keys.length === 0) {
|
|
969
|
+
info("No secrets found in file.");
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
if (!opts.yes) {
|
|
973
|
+
info(
|
|
974
|
+
`Pushing ${keys.length} secret${keys.length === 1 ? "" : "s"} to ${ctx.orgSlug}/${ctx.projectName} (${ctx.envName})`
|
|
975
|
+
);
|
|
976
|
+
const confirmed = await confirm("Continue?", true);
|
|
977
|
+
if (!confirmed) return;
|
|
978
|
+
}
|
|
979
|
+
const spinner = ora4("Encrypting and pushing secrets...").start();
|
|
980
|
+
const orgKey = await ensureOrgKey(ctx.orgId);
|
|
981
|
+
const encryptedSecrets = [];
|
|
982
|
+
for (const [key, value] of Object.entries(env2)) {
|
|
983
|
+
const encrypted = await encryptSecret(value, orgKey);
|
|
984
|
+
encryptedSecrets.push({ key, encryptedValue: encrypted });
|
|
985
|
+
}
|
|
986
|
+
const result = await mutation(api6.secrets.bulkCreate, {
|
|
987
|
+
environmentId: ctx.environmentId,
|
|
988
|
+
secrets: encryptedSecrets,
|
|
989
|
+
overwrite: true
|
|
990
|
+
});
|
|
991
|
+
const summary = unwrapResult(result);
|
|
992
|
+
spinner.stop();
|
|
993
|
+
success(
|
|
994
|
+
`Pushed ${summary.created} created, ${summary.updated} updated, ${summary.skipped} skipped`
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// src/commands/run.ts
|
|
999
|
+
import { spawn } from "child_process";
|
|
1000
|
+
import ora5 from "ora";
|
|
1001
|
+
import { api as api7 } from "@beakcrypt/convex";
|
|
1002
|
+
async function runCommand(args, opts) {
|
|
1003
|
+
if (args.length === 0) {
|
|
1004
|
+
throw new CliError(
|
|
1005
|
+
"No command provided. Usage: beakcrypt run -- <command>"
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
const ctx = await resolveContext(opts);
|
|
1009
|
+
const spinner = ora5("Loading secrets...").start();
|
|
1010
|
+
const orgKey = await ensureOrgKey(ctx.orgId);
|
|
1011
|
+
const secretsResult = await query(api7.secrets.list, {
|
|
1012
|
+
environmentId: ctx.environmentId
|
|
1013
|
+
});
|
|
1014
|
+
const secrets2 = unwrapResult(secretsResult);
|
|
1015
|
+
const decrypted = {};
|
|
1016
|
+
for (const secret of secrets2) {
|
|
1017
|
+
decrypted[secret.key] = await decryptSecret(secret.encryptedValue, orgKey);
|
|
1018
|
+
}
|
|
1019
|
+
spinner.stop();
|
|
1020
|
+
const [command, ...commandArgs] = args;
|
|
1021
|
+
const child = spawn(command, commandArgs, {
|
|
1022
|
+
stdio: "inherit",
|
|
1023
|
+
env: { ...process.env, ...decrypted }
|
|
1024
|
+
});
|
|
1025
|
+
child.on("close", (code) => {
|
|
1026
|
+
process.exit(code ?? 0);
|
|
1027
|
+
});
|
|
1028
|
+
child.on("error", (err) => {
|
|
1029
|
+
throw new CliError(`Failed to run command: ${err.message}`);
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// src/commands/secrets/list.ts
|
|
1034
|
+
import { api as api8 } from "@beakcrypt/convex";
|
|
1035
|
+
async function secretsListCommand(opts) {
|
|
1036
|
+
const ctx = await resolveContext(opts);
|
|
1037
|
+
const result = await query(api8.secrets.list, {
|
|
1038
|
+
environmentId: ctx.environmentId
|
|
1039
|
+
});
|
|
1040
|
+
const secrets2 = unwrapResult(result);
|
|
1041
|
+
if (secrets2.length === 0) {
|
|
1042
|
+
info(
|
|
1043
|
+
`No secrets in ${ctx.orgSlug}/${ctx.projectName} (${ctx.envName})`
|
|
1044
|
+
);
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
console.log(
|
|
1048
|
+
`
|
|
1049
|
+
${bold(`${ctx.orgSlug}/${ctx.projectName}`)} ${dim(`(${ctx.envName})`)}
|
|
1050
|
+
`
|
|
1051
|
+
);
|
|
1052
|
+
table(
|
|
1053
|
+
secrets2.map((s) => [s.key, "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022"]),
|
|
1054
|
+
["Key", "Value"]
|
|
1055
|
+
);
|
|
1056
|
+
console.log(
|
|
1057
|
+
`
|
|
1058
|
+
${dim(`${secrets2.length} secret${secrets2.length === 1 ? "" : "s"}`)}
|
|
1059
|
+
`
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// src/commands/secrets/set.ts
|
|
1064
|
+
import ora6 from "ora";
|
|
1065
|
+
import { api as api9 } from "@beakcrypt/convex";
|
|
1066
|
+
async function secretsSetCommand(pairs, opts) {
|
|
1067
|
+
if (pairs.length === 0) {
|
|
1068
|
+
throw new CliError(
|
|
1069
|
+
"No key=value pairs provided. Usage: beakcrypt secrets set KEY=VALUE"
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
const parsed = [];
|
|
1073
|
+
for (const pair of pairs) {
|
|
1074
|
+
const eqIndex = pair.indexOf("=");
|
|
1075
|
+
if (eqIndex === -1) {
|
|
1076
|
+
throw new CliError(`Invalid format: "${pair}". Use KEY=VALUE.`);
|
|
1077
|
+
}
|
|
1078
|
+
parsed.push({
|
|
1079
|
+
key: pair.slice(0, eqIndex),
|
|
1080
|
+
value: pair.slice(eqIndex + 1)
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
const ctx = await resolveContext(opts);
|
|
1084
|
+
const spinner = ora6("Setting secrets...").start();
|
|
1085
|
+
const orgKey = await ensureOrgKey(ctx.orgId);
|
|
1086
|
+
const encryptedSecrets = [];
|
|
1087
|
+
for (const { key, value } of parsed) {
|
|
1088
|
+
const encrypted = await encryptSecret(value, orgKey);
|
|
1089
|
+
encryptedSecrets.push({ key, encryptedValue: encrypted });
|
|
1090
|
+
}
|
|
1091
|
+
const result = await mutation(api9.secrets.bulkCreate, {
|
|
1092
|
+
environmentId: ctx.environmentId,
|
|
1093
|
+
secrets: encryptedSecrets,
|
|
1094
|
+
overwrite: true
|
|
1095
|
+
});
|
|
1096
|
+
unwrapResult(result);
|
|
1097
|
+
spinner.stop();
|
|
1098
|
+
success(
|
|
1099
|
+
`Set ${parsed.length} secret${parsed.length === 1 ? "" : "s"} in ${ctx.envName}`
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// src/commands/secrets/remove.ts
|
|
1104
|
+
import ora7 from "ora";
|
|
1105
|
+
import { api as api10 } from "@beakcrypt/convex";
|
|
1106
|
+
async function secretsRemoveCommand(keys, opts) {
|
|
1107
|
+
if (keys.length === 0) {
|
|
1108
|
+
throw new CliError(
|
|
1109
|
+
"No keys provided. Usage: beakcrypt secrets remove KEY [KEY2...]"
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
const ctx = await resolveContext(opts);
|
|
1113
|
+
const spinner = ora7("Removing secrets...").start();
|
|
1114
|
+
const secretsResult = await query(api10.secrets.list, {
|
|
1115
|
+
environmentId: ctx.environmentId
|
|
1116
|
+
});
|
|
1117
|
+
const secrets2 = unwrapResult(secretsResult);
|
|
1118
|
+
let removed = 0;
|
|
1119
|
+
const notFound = [];
|
|
1120
|
+
for (const key of keys) {
|
|
1121
|
+
const secret = secrets2.find((s) => s.key === key);
|
|
1122
|
+
if (secret) {
|
|
1123
|
+
const result = await mutation(api10.secrets.remove, {
|
|
1124
|
+
id: secret._id
|
|
1125
|
+
});
|
|
1126
|
+
unwrapResult(result);
|
|
1127
|
+
removed++;
|
|
1128
|
+
} else {
|
|
1129
|
+
notFound.push(key);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
spinner.stop();
|
|
1133
|
+
if (removed > 0) {
|
|
1134
|
+
success(
|
|
1135
|
+
`Removed ${removed} secret${removed === 1 ? "" : "s"} from ${ctx.envName}`
|
|
1136
|
+
);
|
|
1137
|
+
}
|
|
1138
|
+
if (notFound.length > 0) {
|
|
1139
|
+
warn(`Not found: ${notFound.join(", ")}`);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// src/commands/secrets/clear.ts
|
|
1144
|
+
import ora8 from "ora";
|
|
1145
|
+
import { api as api11 } from "@beakcrypt/convex";
|
|
1146
|
+
async function secretsClearCommand(opts) {
|
|
1147
|
+
const ctx = await resolveContext(opts);
|
|
1148
|
+
if (!opts.yes) {
|
|
1149
|
+
const confirmed = await confirm(
|
|
1150
|
+
`Delete ALL secrets in ${ctx.orgSlug}/${ctx.projectName} (${ctx.envName})?`
|
|
1151
|
+
);
|
|
1152
|
+
if (!confirmed) return;
|
|
1153
|
+
}
|
|
1154
|
+
const spinner = ora8("Clearing secrets...").start();
|
|
1155
|
+
const result = await mutation(api11.secrets.removeAll, {
|
|
1156
|
+
environmentId: ctx.environmentId
|
|
1157
|
+
});
|
|
1158
|
+
const summary = unwrapResult(result);
|
|
1159
|
+
spinner.stop();
|
|
1160
|
+
success(
|
|
1161
|
+
`Deleted ${summary.deleted} secret${summary.deleted === 1 ? "" : "s"}`
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// src/commands/org/list.ts
|
|
1166
|
+
import { api as api12 } from "@beakcrypt/convex";
|
|
1167
|
+
async function orgListCommand() {
|
|
1168
|
+
await ensureAuth();
|
|
1169
|
+
const result = await query(api12.organizations.list, {});
|
|
1170
|
+
const orgs = unwrapResult(result);
|
|
1171
|
+
if (orgs.length === 0) {
|
|
1172
|
+
info("You don't belong to any organizations.");
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
console.log();
|
|
1176
|
+
table(
|
|
1177
|
+
orgs.map((o) => [o.name, o.slug]),
|
|
1178
|
+
["Name", "Slug"]
|
|
1179
|
+
);
|
|
1180
|
+
console.log();
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// src/commands/env/list.ts
|
|
1184
|
+
import { api as api13 } from "@beakcrypt/convex";
|
|
1185
|
+
async function envListCommand(opts) {
|
|
1186
|
+
const ctx = await resolveContext({ ...opts, env: "development" });
|
|
1187
|
+
const result = await query(api13.environments.list, {
|
|
1188
|
+
projectId: ctx.projectId
|
|
1189
|
+
});
|
|
1190
|
+
const envs = unwrapResult(result);
|
|
1191
|
+
if (envs.length === 0) {
|
|
1192
|
+
info("No environments found.");
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
console.log(
|
|
1196
|
+
`
|
|
1197
|
+
${bold(`${ctx.orgSlug}/${ctx.projectName}`)} environments:
|
|
1198
|
+
`
|
|
1199
|
+
);
|
|
1200
|
+
table(
|
|
1201
|
+
envs.map((e) => [
|
|
1202
|
+
e.name,
|
|
1203
|
+
e.isPersonal ? "personal" : "shared"
|
|
1204
|
+
]),
|
|
1205
|
+
["Name", "Type"]
|
|
1206
|
+
);
|
|
1207
|
+
console.log();
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// src/interactive-mode.ts
|
|
1211
|
+
import pc4 from "picocolors";
|
|
1212
|
+
async function interactiveMode() {
|
|
1213
|
+
requireInteractive("(no args)");
|
|
1214
|
+
console.log(`
|
|
1215
|
+
${pc4.bold("Beakcrypt")} ${pc4.dim("v0.1.0")}
|
|
1216
|
+
`);
|
|
1217
|
+
const auth = await getAuthConfig();
|
|
1218
|
+
if (!auth) {
|
|
1219
|
+
const shouldLogin = await confirm(
|
|
1220
|
+
"You're not logged in. Log in now?",
|
|
1221
|
+
true
|
|
1222
|
+
);
|
|
1223
|
+
if (!shouldLogin) process.exit(0);
|
|
1224
|
+
await loginCommand();
|
|
1225
|
+
}
|
|
1226
|
+
const config = await getProjectConfig();
|
|
1227
|
+
if (!config) {
|
|
1228
|
+
info("No project linked in this directory.");
|
|
1229
|
+
const shouldLink = await confirm(
|
|
1230
|
+
"Link to a project now?",
|
|
1231
|
+
true
|
|
1232
|
+
);
|
|
1233
|
+
if (!shouldLink) process.exit(0);
|
|
1234
|
+
await linkCommand();
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
console.log(
|
|
1238
|
+
`Linked to ${bold(`${config.orgSlug}/${config.projectName}`)} ${dim(`(${config.defaultEnv}`)})`
|
|
1239
|
+
);
|
|
1240
|
+
console.log();
|
|
1241
|
+
const action = await select({
|
|
1242
|
+
message: "What would you like to do?",
|
|
1243
|
+
choices: [
|
|
1244
|
+
{ title: "Pull secrets (.env.local)", value: "pull" },
|
|
1245
|
+
{ title: "Push secrets (.env.local)", value: "push" },
|
|
1246
|
+
{ title: "List secrets", value: "list" },
|
|
1247
|
+
{ title: "Change project link", value: "link" }
|
|
1248
|
+
]
|
|
1249
|
+
});
|
|
1250
|
+
switch (action) {
|
|
1251
|
+
case "pull":
|
|
1252
|
+
await pullCommand(void 0, {});
|
|
1253
|
+
break;
|
|
1254
|
+
case "push":
|
|
1255
|
+
await pushCommand(void 0, {});
|
|
1256
|
+
break;
|
|
1257
|
+
case "list":
|
|
1258
|
+
await secretsListCommand({});
|
|
1259
|
+
break;
|
|
1260
|
+
case "link":
|
|
1261
|
+
await linkCommand();
|
|
1262
|
+
break;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// src/index.ts
|
|
1267
|
+
var program = new Command();
|
|
1268
|
+
program.name("beakcrypt").description("Secure environment variable management with E2E encryption").version("0.1.0").action(async () => {
|
|
1269
|
+
try {
|
|
1270
|
+
await interactiveMode();
|
|
1271
|
+
} catch (err) {
|
|
1272
|
+
handleError(err);
|
|
1273
|
+
}
|
|
1274
|
+
});
|
|
1275
|
+
program.command("login").description("Log in to Beakcrypt via GitHub OAuth").action(async () => {
|
|
1276
|
+
try {
|
|
1277
|
+
await loginCommand();
|
|
1278
|
+
} catch (err) {
|
|
1279
|
+
handleError(err);
|
|
1280
|
+
}
|
|
1281
|
+
});
|
|
1282
|
+
program.command("logout").description("Log out and remove all credentials").option("-y, --yes", "Skip confirmation prompt").action(async (opts) => {
|
|
1283
|
+
try {
|
|
1284
|
+
await logoutCommand(opts);
|
|
1285
|
+
} catch (err) {
|
|
1286
|
+
handleError(err);
|
|
1287
|
+
}
|
|
1288
|
+
});
|
|
1289
|
+
program.command("whoami").description("Show current user info").action(async () => {
|
|
1290
|
+
try {
|
|
1291
|
+
await whoamiCommand();
|
|
1292
|
+
} catch (err) {
|
|
1293
|
+
handleError(err);
|
|
1294
|
+
}
|
|
1295
|
+
});
|
|
1296
|
+
program.command("link").description("Link this directory to a Beakcrypt project").action(async () => {
|
|
1297
|
+
try {
|
|
1298
|
+
await linkCommand();
|
|
1299
|
+
} catch (err) {
|
|
1300
|
+
handleError(err);
|
|
1301
|
+
}
|
|
1302
|
+
});
|
|
1303
|
+
program.command("pull [file]").description("Pull secrets to a local .env file").option("-o, --org <slug>", "Organization slug").option("-p, --project <name>", "Project name").option("-e, --env <name>", "Environment name").action(async (file, opts) => {
|
|
1304
|
+
try {
|
|
1305
|
+
await pullCommand(file, opts);
|
|
1306
|
+
} catch (err) {
|
|
1307
|
+
handleError(err);
|
|
1308
|
+
}
|
|
1309
|
+
});
|
|
1310
|
+
program.command("push [file]").description("Push secrets from a local .env file").option("-o, --org <slug>", "Organization slug").option("-p, --project <name>", "Project name").option("-e, --env <name>", "Environment name").option("-y, --yes", "Skip confirmation prompt").action(async (file, opts) => {
|
|
1311
|
+
try {
|
|
1312
|
+
await pushCommand(file, opts);
|
|
1313
|
+
} catch (err) {
|
|
1314
|
+
handleError(err);
|
|
1315
|
+
}
|
|
1316
|
+
});
|
|
1317
|
+
program.command("run").description("Run a command with injected secrets").option("-o, --org <slug>", "Organization slug").option("-p, --project <name>", "Project name").option("-e, --env <name>", "Environment name").allowUnknownOption().action(async (opts, cmd) => {
|
|
1318
|
+
try {
|
|
1319
|
+
await runCommand(cmd.args, opts);
|
|
1320
|
+
} catch (err) {
|
|
1321
|
+
handleError(err);
|
|
1322
|
+
}
|
|
1323
|
+
});
|
|
1324
|
+
var secrets = program.command("secrets").description("Manage secrets");
|
|
1325
|
+
secrets.command("list").description("List secrets (values masked)").option("-o, --org <slug>", "Organization slug").option("-p, --project <name>", "Project name").option("-e, --env <name>", "Environment name").action(async (opts) => {
|
|
1326
|
+
try {
|
|
1327
|
+
await secretsListCommand(opts);
|
|
1328
|
+
} catch (err) {
|
|
1329
|
+
handleError(err);
|
|
1330
|
+
}
|
|
1331
|
+
});
|
|
1332
|
+
secrets.command("set <pairs...>").description("Set secrets (KEY=VALUE)").option("-o, --org <slug>", "Organization slug").option("-p, --project <name>", "Project name").option("-e, --env <name>", "Environment name").action(async (pairs, opts) => {
|
|
1333
|
+
try {
|
|
1334
|
+
await secretsSetCommand(pairs, opts);
|
|
1335
|
+
} catch (err) {
|
|
1336
|
+
handleError(err);
|
|
1337
|
+
}
|
|
1338
|
+
});
|
|
1339
|
+
secrets.command("remove <keys...>").description("Remove secrets by key").option("-o, --org <slug>", "Organization slug").option("-p, --project <name>", "Project name").option("-e, --env <name>", "Environment name").action(async (keys, opts) => {
|
|
1340
|
+
try {
|
|
1341
|
+
await secretsRemoveCommand(keys, opts);
|
|
1342
|
+
} catch (err) {
|
|
1343
|
+
handleError(err);
|
|
1344
|
+
}
|
|
1345
|
+
});
|
|
1346
|
+
secrets.command("clear").description("Delete all secrets in an environment").option("-o, --org <slug>", "Organization slug").option("-p, --project <name>", "Project name").option("-e, --env <name>", "Environment name").option("-y, --yes", "Skip confirmation prompt").action(async (opts) => {
|
|
1347
|
+
try {
|
|
1348
|
+
await secretsClearCommand(opts);
|
|
1349
|
+
} catch (err) {
|
|
1350
|
+
handleError(err);
|
|
1351
|
+
}
|
|
1352
|
+
});
|
|
1353
|
+
var org = program.command("org").description("Manage organizations");
|
|
1354
|
+
org.command("list").description("List your organizations").action(async () => {
|
|
1355
|
+
try {
|
|
1356
|
+
await orgListCommand();
|
|
1357
|
+
} catch (err) {
|
|
1358
|
+
handleError(err);
|
|
1359
|
+
}
|
|
1360
|
+
});
|
|
1361
|
+
var env = program.command("env").description("Manage environments");
|
|
1362
|
+
env.command("list").description("List environments for the linked project").option("-o, --org <slug>", "Organization slug").option("-p, --project <name>", "Project name").action(async (opts) => {
|
|
1363
|
+
try {
|
|
1364
|
+
await envListCommand(opts);
|
|
1365
|
+
} catch (err) {
|
|
1366
|
+
handleError(err);
|
|
1367
|
+
}
|
|
1368
|
+
});
|
|
1369
|
+
program.parse();
|
|
1370
|
+
//# sourceMappingURL=index.js.map
|