@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.
Files changed (2) hide show
  1. package/dist/index.js +117 -117
  2. 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").option("-e, --email <email>", "Email address").option("-t, --token <token>", "Service token for CI/CD authentication (must start with stk_)").action(async (options) => {
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
- ui.heading("SecretStash Login");
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()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@secretstash/cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "CLI tool for SecretStash - secure team secrets management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",