@secretstash/cli 0.1.3 → 0.1.5
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 +117 -117
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { dirname as dirname2, join as join2 } from "path";
|
|
|
10
10
|
// src/commands/auth.ts
|
|
11
11
|
import enquirer from "enquirer";
|
|
12
12
|
import qrcode from "qrcode-terminal";
|
|
13
|
+
import { execFile } from "child_process";
|
|
13
14
|
|
|
14
15
|
// src/config.ts
|
|
15
16
|
import Conf from "conf";
|
|
@@ -305,7 +306,7 @@ var ApiClient = class {
|
|
|
305
306
|
const refreshToken = configManager.getRefreshToken();
|
|
306
307
|
if (!refreshToken) return false;
|
|
307
308
|
try {
|
|
308
|
-
const response = await fetch(`${configManager.getApiUrl()}/api/auth/refresh`, {
|
|
309
|
+
const response = await fetch(`${configManager.getApiUrl()}/api/v1/auth/refresh`, {
|
|
309
310
|
method: "POST",
|
|
310
311
|
headers: {
|
|
311
312
|
"Content-Type": "application/json"
|
|
@@ -379,21 +380,33 @@ var ApiClient = class {
|
|
|
379
380
|
}
|
|
380
381
|
// Auth endpoints
|
|
381
382
|
async login(email, password) {
|
|
382
|
-
return this.request("/api/auth/login", {
|
|
383
|
+
return this.request("/api/v1/auth/login", {
|
|
383
384
|
method: "POST",
|
|
384
385
|
body: { email, password, client: "cli" },
|
|
385
386
|
requireAuth: false
|
|
386
387
|
});
|
|
387
388
|
}
|
|
389
|
+
// Browser-based CLI auth endpoints
|
|
390
|
+
async initBrowserAuth() {
|
|
391
|
+
return this.request("/api/v1/cli/auth/init", {
|
|
392
|
+
method: "POST",
|
|
393
|
+
requireAuth: false
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
async checkBrowserAuthStatus(code) {
|
|
397
|
+
return this.request(`/api/v1/cli/auth/status/${code}`, {
|
|
398
|
+
requireAuth: false
|
|
399
|
+
});
|
|
400
|
+
}
|
|
388
401
|
async verify2FA(tempToken, code) {
|
|
389
|
-
return this.request("/api/auth/2fa/verify", {
|
|
402
|
+
return this.request("/api/v1/auth/2fa/verify", {
|
|
390
403
|
method: "POST",
|
|
391
404
|
body: { tempToken, code },
|
|
392
405
|
requireAuth: false
|
|
393
406
|
});
|
|
394
407
|
}
|
|
395
408
|
async register(email, password) {
|
|
396
|
-
return this.request("/api/auth/register", {
|
|
409
|
+
return this.request("/api/v1/auth/register", {
|
|
397
410
|
method: "POST",
|
|
398
411
|
body: { email, password },
|
|
399
412
|
requireAuth: false
|
|
@@ -402,7 +415,7 @@ var ApiClient = class {
|
|
|
402
415
|
async logout() {
|
|
403
416
|
const refreshToken = configManager.getRefreshToken();
|
|
404
417
|
if (refreshToken) {
|
|
405
|
-
await this.request("/api/auth/logout", {
|
|
418
|
+
await this.request("/api/v1/auth/logout", {
|
|
406
419
|
method: "POST",
|
|
407
420
|
body: { refreshToken }
|
|
408
421
|
});
|
|
@@ -410,39 +423,39 @@ var ApiClient = class {
|
|
|
410
423
|
configManager.logout();
|
|
411
424
|
}
|
|
412
425
|
async getMe() {
|
|
413
|
-
return this.request("/api/users/me");
|
|
426
|
+
return this.request("/api/v1/users/me");
|
|
414
427
|
}
|
|
415
428
|
// Team endpoints
|
|
416
429
|
async getTeams() {
|
|
417
|
-
return this.request("/api/teams");
|
|
430
|
+
return this.request("/api/v1/teams");
|
|
418
431
|
}
|
|
419
432
|
async createTeam(name) {
|
|
420
|
-
return this.request("/api/teams", {
|
|
433
|
+
return this.request("/api/v1/teams", {
|
|
421
434
|
method: "POST",
|
|
422
435
|
body: { name }
|
|
423
436
|
});
|
|
424
437
|
}
|
|
425
438
|
// Project endpoints
|
|
426
439
|
async getProjects(teamId) {
|
|
427
|
-
return this.request(`/api/teams/${teamId}/projects`);
|
|
440
|
+
return this.request(`/api/v1/teams/${teamId}/projects`);
|
|
428
441
|
}
|
|
429
442
|
async createProject(teamId, name, description) {
|
|
430
|
-
return this.request(`/api/teams/${teamId}/projects`, {
|
|
443
|
+
return this.request(`/api/v1/teams/${teamId}/projects`, {
|
|
431
444
|
method: "POST",
|
|
432
445
|
body: { name, description }
|
|
433
446
|
});
|
|
434
447
|
}
|
|
435
448
|
async deleteProject(projectId) {
|
|
436
|
-
return this.request(`/api/projects/${projectId}`, {
|
|
449
|
+
return this.request(`/api/v1/projects/${projectId}`, {
|
|
437
450
|
method: "DELETE"
|
|
438
451
|
});
|
|
439
452
|
}
|
|
440
453
|
// Environment endpoints
|
|
441
454
|
async getEnvironments(projectId) {
|
|
442
|
-
return this.request(`/api/projects/${projectId}/environments`);
|
|
455
|
+
return this.request(`/api/v1/projects/${projectId}/environments`);
|
|
443
456
|
}
|
|
444
457
|
async createEnvironment(projectId, name, parentId) {
|
|
445
|
-
return this.request(`/api/projects/${projectId}/environments`, {
|
|
458
|
+
return this.request(`/api/v1/projects/${projectId}/environments`, {
|
|
446
459
|
method: "POST",
|
|
447
460
|
body: { name, parentId }
|
|
448
461
|
});
|
|
@@ -454,29 +467,29 @@ var ApiClient = class {
|
|
|
454
467
|
}
|
|
455
468
|
// Secret endpoints (keys only, no decryption)
|
|
456
469
|
async getSecrets(environmentId) {
|
|
457
|
-
return this.request(`/api/environments/${environmentId}/secrets`);
|
|
470
|
+
return this.request(`/api/v1/environments/${environmentId}/secrets`);
|
|
458
471
|
}
|
|
459
472
|
// Decrypted secrets (requires password)
|
|
460
473
|
async getDecryptedSecrets(environmentId, password) {
|
|
461
|
-
return this.request(`/api/environments/${environmentId}/secrets/decrypt`, {
|
|
474
|
+
return this.request(`/api/v1/environments/${environmentId}/secrets/decrypt`, {
|
|
462
475
|
method: "POST",
|
|
463
476
|
body: { password }
|
|
464
477
|
});
|
|
465
478
|
}
|
|
466
479
|
async createSecret(environmentId, key, value, password, description) {
|
|
467
|
-
return this.request(`/api/environments/${environmentId}/secrets`, {
|
|
480
|
+
return this.request(`/api/v1/environments/${environmentId}/secrets`, {
|
|
468
481
|
method: "POST",
|
|
469
482
|
body: { key, value, password, description }
|
|
470
483
|
});
|
|
471
484
|
}
|
|
472
485
|
async updateSecret(secretId, value, password, description) {
|
|
473
|
-
return this.request(`/api/secrets/${secretId}`, {
|
|
486
|
+
return this.request(`/api/v1/secrets/${secretId}`, {
|
|
474
487
|
method: "PATCH",
|
|
475
488
|
body: { value, password, description }
|
|
476
489
|
});
|
|
477
490
|
}
|
|
478
491
|
async deleteSecret(secretId) {
|
|
479
|
-
return this.request(`/api/secrets/${secretId}`, {
|
|
492
|
+
return this.request(`/api/v1/secrets/${secretId}`, {
|
|
480
493
|
method: "DELETE"
|
|
481
494
|
});
|
|
482
495
|
}
|
|
@@ -484,58 +497,58 @@ var ApiClient = class {
|
|
|
484
497
|
async pullSecrets(teamSlug, projectSlug, environment, password, options) {
|
|
485
498
|
const includeInherited = options?.includeInherited ?? true;
|
|
486
499
|
const query = includeInherited ? "" : "?includeInherited=false";
|
|
487
|
-
return this.request(`/api/teams/${teamSlug}/projects/${projectSlug}/environments/${environment}/secrets/decrypt${query}`, {
|
|
500
|
+
return this.request(`/api/v1/teams/${teamSlug}/projects/${projectSlug}/environments/${environment}/secrets/decrypt${query}`, {
|
|
488
501
|
method: "POST",
|
|
489
502
|
body: { password }
|
|
490
503
|
});
|
|
491
504
|
}
|
|
492
505
|
async pushSecrets(teamSlug, projectSlug, environment, secrets, password) {
|
|
493
|
-
return this.request(`/api/teams/${teamSlug}/projects/${projectSlug}/environments/${environment}/secrets/bulk`, {
|
|
506
|
+
return this.request(`/api/v1/teams/${teamSlug}/projects/${projectSlug}/environments/${environment}/secrets/bulk`, {
|
|
494
507
|
method: "PUT",
|
|
495
508
|
body: { secrets, password }
|
|
496
509
|
});
|
|
497
510
|
}
|
|
498
511
|
// 2FA endpoints
|
|
499
512
|
async setup2FA() {
|
|
500
|
-
return this.request("/api/users/me/2fa/setup", {
|
|
513
|
+
return this.request("/api/v1/users/me/2fa/setup", {
|
|
501
514
|
method: "POST"
|
|
502
515
|
});
|
|
503
516
|
}
|
|
504
517
|
async enable2FA(code) {
|
|
505
|
-
return this.request("/api/users/me/2fa/enable", {
|
|
518
|
+
return this.request("/api/v1/users/me/2fa/enable", {
|
|
506
519
|
method: "POST",
|
|
507
520
|
body: { code }
|
|
508
521
|
});
|
|
509
522
|
}
|
|
510
523
|
async disable2FA(code) {
|
|
511
|
-
return this.request("/api/users/me/2fa/disable", {
|
|
524
|
+
return this.request("/api/v1/users/me/2fa/disable", {
|
|
512
525
|
method: "POST",
|
|
513
526
|
body: { code }
|
|
514
527
|
});
|
|
515
528
|
}
|
|
516
529
|
// Team member endpoints
|
|
517
530
|
async getTeamMembers(teamId) {
|
|
518
|
-
return this.request(`/api/teams/${teamId}/members`);
|
|
531
|
+
return this.request(`/api/v1/teams/${teamId}/members`);
|
|
519
532
|
}
|
|
520
533
|
async inviteTeamMember(teamId, email, role) {
|
|
521
|
-
return this.request(`/api/teams/${teamId}/invitations`, {
|
|
534
|
+
return this.request(`/api/v1/teams/${teamId}/invitations`, {
|
|
522
535
|
method: "POST",
|
|
523
536
|
body: { email, role }
|
|
524
537
|
});
|
|
525
538
|
}
|
|
526
539
|
async removeTeamMember(teamId, memberId) {
|
|
527
|
-
return this.request(`/api/teams/${teamId}/members/${memberId}`, {
|
|
540
|
+
return this.request(`/api/v1/teams/${teamId}/members/${memberId}`, {
|
|
528
541
|
method: "DELETE"
|
|
529
542
|
});
|
|
530
543
|
}
|
|
531
544
|
async updateTeamMemberRole(teamId, memberId, role) {
|
|
532
|
-
return this.request(`/api/teams/${teamId}/members/${memberId}`, {
|
|
545
|
+
return this.request(`/api/v1/teams/${teamId}/members/${memberId}`, {
|
|
533
546
|
method: "PATCH",
|
|
534
547
|
body: { role }
|
|
535
548
|
});
|
|
536
549
|
}
|
|
537
550
|
async getPendingInvitations(teamId) {
|
|
538
|
-
return this.request(`/api/teams/${teamId}/invitations`);
|
|
551
|
+
return this.request(`/api/v1/teams/${teamId}/invitations`);
|
|
539
552
|
}
|
|
540
553
|
// Share link endpoints
|
|
541
554
|
async createShare(teamSlug, projectSlug, environment, secretKey, options) {
|
|
@@ -561,7 +574,7 @@ var ApiClient = class {
|
|
|
561
574
|
}
|
|
562
575
|
// Service token verification - make a request to validate the token works
|
|
563
576
|
async verifyServiceToken(token) {
|
|
564
|
-
return this.request("/api/teams", {
|
|
577
|
+
return this.request("/api/v1/teams", {
|
|
565
578
|
headers: {
|
|
566
579
|
"Authorization": `Bearer ${token}`
|
|
567
580
|
},
|
|
@@ -819,6 +832,79 @@ var ui = {
|
|
|
819
832
|
// src/commands/auth.ts
|
|
820
833
|
var { prompt } = enquirer;
|
|
821
834
|
var SERVICE_TOKEN_PREFIX = "stk_";
|
|
835
|
+
function openBrowser(url) {
|
|
836
|
+
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "powershell" : "xdg-open";
|
|
837
|
+
const args = process.platform === "win32" ? ["-Command", "Start-Process", url] : [url];
|
|
838
|
+
execFile(opener, args, (error) => {
|
|
839
|
+
if (error) {
|
|
840
|
+
ui.warning("Could not open browser automatically");
|
|
841
|
+
ui.info(`Please open this URL manually: ${colors.highlight(url)}`);
|
|
842
|
+
}
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
async function handleBrowserLogin() {
|
|
846
|
+
ui.heading("SecretStash Login");
|
|
847
|
+
ui.br();
|
|
848
|
+
const initSpinner = ui.spinner("Starting authentication...");
|
|
849
|
+
const initResult = await apiClient.initBrowserAuth();
|
|
850
|
+
if (initResult.error) {
|
|
851
|
+
initSpinner.fail();
|
|
852
|
+
ui.error(initResult.error.message);
|
|
853
|
+
process.exit(1);
|
|
854
|
+
}
|
|
855
|
+
initSpinner.succeed("Authentication session started");
|
|
856
|
+
const { code, authUrl, expiresIn } = initResult.data;
|
|
857
|
+
ui.br();
|
|
858
|
+
ui.box("Browser Authentication", [
|
|
859
|
+
"A browser window will open for you to sign in.",
|
|
860
|
+
"If it doesn't open automatically, visit:",
|
|
861
|
+
"",
|
|
862
|
+
` ${colors.highlight(authUrl)}`,
|
|
863
|
+
"",
|
|
864
|
+
`Code: ${colors.primary.bold(code)}`,
|
|
865
|
+
"",
|
|
866
|
+
colors.muted(`This code expires in ${Math.floor(expiresIn / 60)} minutes`)
|
|
867
|
+
]);
|
|
868
|
+
openBrowser(authUrl);
|
|
869
|
+
ui.br();
|
|
870
|
+
const pollSpinner = ui.spinner("Waiting for authentication...");
|
|
871
|
+
const pollInterval = 2e3;
|
|
872
|
+
const maxAttempts = Math.floor(expiresIn * 1e3 / pollInterval);
|
|
873
|
+
let attempts = 0;
|
|
874
|
+
while (attempts < maxAttempts) {
|
|
875
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
876
|
+
attempts++;
|
|
877
|
+
const statusResult = await apiClient.checkBrowserAuthStatus(code);
|
|
878
|
+
if (statusResult.error) {
|
|
879
|
+
if (statusResult.error.code === "EXPIRED" || statusResult.error.code === "NOT_FOUND") {
|
|
880
|
+
pollSpinner.fail();
|
|
881
|
+
ui.error("Authentication session expired. Please try again.");
|
|
882
|
+
process.exit(1);
|
|
883
|
+
}
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
const { status, accessToken, refreshToken, expiresIn: tokenExpiry, user } = statusResult.data;
|
|
887
|
+
if (status === "completed" && accessToken && refreshToken && user) {
|
|
888
|
+
pollSpinner.succeed("Authentication successful!");
|
|
889
|
+
configManager.setTokens(accessToken, refreshToken, tokenExpiry || 3600);
|
|
890
|
+
configManager.setUser(user.id, user.email);
|
|
891
|
+
ui.br();
|
|
892
|
+
ui.keyValue("Email", user.email);
|
|
893
|
+
ui.keyValue("Config", configManager.getConfigPath());
|
|
894
|
+
ui.br();
|
|
895
|
+
ui.info(`Run ${ui.code("sstash teams")} to see your teams`);
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
if (status === "expired") {
|
|
899
|
+
pollSpinner.fail();
|
|
900
|
+
ui.error("Authentication session expired. Please try again.");
|
|
901
|
+
process.exit(1);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
pollSpinner.fail();
|
|
905
|
+
ui.error("Authentication timed out. Please try again.");
|
|
906
|
+
process.exit(1);
|
|
907
|
+
}
|
|
822
908
|
async function handleServiceTokenLogin(token, fromEnvVar = false) {
|
|
823
909
|
ui.heading("SecretStash Service Token Login");
|
|
824
910
|
if (!token.startsWith(SERVICE_TOKEN_PREFIX)) {
|
|
@@ -866,7 +952,7 @@ async function handleServiceTokenLogin(token, fromEnvVar = false) {
|
|
|
866
952
|
]);
|
|
867
953
|
}
|
|
868
954
|
function registerAuthCommands(program2) {
|
|
869
|
-
program2.command("login").description("Authenticate with SecretStash
|
|
955
|
+
program2.command("login").description("Authenticate with SecretStash via browser (supports passkeys and 2FA)").option("-t, --token <token>", "Service token for CI/CD authentication (must start with stk_)").action(async (options) => {
|
|
870
956
|
if (options.token) {
|
|
871
957
|
await handleServiceTokenLogin(options.token);
|
|
872
958
|
return;
|
|
@@ -875,93 +961,7 @@ function registerAuthCommands(program2) {
|
|
|
875
961
|
await handleServiceTokenLogin(process.env.SECRETSTASH_TOKEN, true);
|
|
876
962
|
return;
|
|
877
963
|
}
|
|
878
|
-
|
|
879
|
-
let email = options.email;
|
|
880
|
-
if (!email) {
|
|
881
|
-
const response = await prompt({
|
|
882
|
-
type: "input",
|
|
883
|
-
name: "email",
|
|
884
|
-
message: "Email:",
|
|
885
|
-
validate: (value) => {
|
|
886
|
-
if (!value) return "Email is required";
|
|
887
|
-
if (!value.includes("@")) return "Invalid email format";
|
|
888
|
-
return true;
|
|
889
|
-
}
|
|
890
|
-
});
|
|
891
|
-
email = response.email;
|
|
892
|
-
}
|
|
893
|
-
const { password } = await prompt({
|
|
894
|
-
type: "password",
|
|
895
|
-
name: "password",
|
|
896
|
-
message: "Password:",
|
|
897
|
-
validate: (value) => value ? true : "Password is required"
|
|
898
|
-
});
|
|
899
|
-
const spinner = ui.spinner("Authenticating...");
|
|
900
|
-
const result = await apiClient.login(email, password);
|
|
901
|
-
if (result.error) {
|
|
902
|
-
spinner.fail();
|
|
903
|
-
if (result.error.code === "CLI_2FA_REQUIRED") {
|
|
904
|
-
ui.br();
|
|
905
|
-
ui.box("Two-Factor Authentication Required", [
|
|
906
|
-
"CLI access requires two-factor authentication.",
|
|
907
|
-
"",
|
|
908
|
-
"To set up 2FA:",
|
|
909
|
-
` 1. Visit ${colors.highlight("https://app.secretstash.dev/settings/security")}`,
|
|
910
|
-
" 2. Enable two-factor authentication",
|
|
911
|
-
" 3. Return here and try again",
|
|
912
|
-
"",
|
|
913
|
-
colors.muted("For CI/CD pipelines, use service tokens instead:"),
|
|
914
|
-
` ${colors.highlight("sstash login --token <service-token>")}`
|
|
915
|
-
]);
|
|
916
|
-
process.exit(1);
|
|
917
|
-
}
|
|
918
|
-
ui.error(result.error.message);
|
|
919
|
-
process.exit(1);
|
|
920
|
-
}
|
|
921
|
-
if (result.data?.requires2FA) {
|
|
922
|
-
spinner.stop();
|
|
923
|
-
ui.info("Two-factor authentication required");
|
|
924
|
-
const { code } = await prompt({
|
|
925
|
-
type: "input",
|
|
926
|
-
name: "code",
|
|
927
|
-
message: "2FA Code:",
|
|
928
|
-
validate: (value) => {
|
|
929
|
-
if (!value) return "2FA code is required";
|
|
930
|
-
if (!/^\d{6}$/.test(value)) return "Code must be 6 digits";
|
|
931
|
-
return true;
|
|
932
|
-
}
|
|
933
|
-
});
|
|
934
|
-
const spinner2 = ui.spinner("Verifying...");
|
|
935
|
-
const twoFAResult = await apiClient.verify2FA(
|
|
936
|
-
result.data.tempToken,
|
|
937
|
-
code
|
|
938
|
-
);
|
|
939
|
-
if (twoFAResult.error) {
|
|
940
|
-
spinner2.fail();
|
|
941
|
-
ui.error(twoFAResult.error.message);
|
|
942
|
-
process.exit(1);
|
|
943
|
-
}
|
|
944
|
-
configManager.setTokens(
|
|
945
|
-
twoFAResult.data.accessToken,
|
|
946
|
-
twoFAResult.data.refreshToken,
|
|
947
|
-
twoFAResult.data.expiresIn
|
|
948
|
-
);
|
|
949
|
-
configManager.setUser(twoFAResult.data.user.id, twoFAResult.data.user.email);
|
|
950
|
-
spinner2.succeed("Logged in successfully");
|
|
951
|
-
} else {
|
|
952
|
-
configManager.setTokens(
|
|
953
|
-
result.data.accessToken,
|
|
954
|
-
result.data.refreshToken,
|
|
955
|
-
result.data.expiresIn
|
|
956
|
-
);
|
|
957
|
-
configManager.setUser(result.data.user.id, result.data.user.email);
|
|
958
|
-
spinner.succeed("Logged in successfully");
|
|
959
|
-
}
|
|
960
|
-
ui.br();
|
|
961
|
-
ui.keyValue("Email", email);
|
|
962
|
-
ui.keyValue("Config", configManager.getConfigPath());
|
|
963
|
-
ui.br();
|
|
964
|
-
ui.info(`Run ${ui.code("sstash teams")} to see your teams`);
|
|
964
|
+
await handleBrowserLogin();
|
|
965
965
|
});
|
|
966
966
|
program2.command("logout").description("Log out from SecretStash").action(async () => {
|
|
967
967
|
if (!configManager.isAuthenticated()) {
|