@secretstash/cli 0.1.4 → 0.1.8

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 (3) hide show
  1. package/README.md +68 -17
  2. package/dist/index.js +97 -46
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -32,13 +32,38 @@ brew install secretstash/tap/sstash
32
32
 
33
33
  ### Authentication
34
34
 
35
+ The CLI uses browser-based authentication by default, supporting passkeys, OAuth (GitHub/Google), and 2FA:
36
+
37
+ ```bash
38
+ # Login via browser (opens browser window)
39
+ sstash login
40
+
41
+ # This will:
42
+ # 1. Generate a session code
43
+ # 2. Open your default browser to complete authentication
44
+ # 3. Wait for you to sign in (passkey, OAuth, or email/password)
45
+ # 4. Automatically complete CLI authentication
46
+
47
+ # Check who you're logged in as
48
+ sstash whoami
49
+
50
+ # Logout
51
+ sstash logout
52
+ ```
53
+
54
+ #### Service Token Authentication (CI/CD)
55
+
56
+ For automated workflows, use service tokens instead of browser authentication:
57
+
35
58
  ```bash
36
- # Login to SecretStash
37
- sstash auth login
59
+ # Set service token as environment variable
60
+ export SECRETSTASH_TOKEN=stk_your-service-token
61
+
62
+ # Or login with token directly
63
+ sstash login --token stk_your-service-token
38
64
 
39
- # Login with service token (for CI/CD)
40
- export SECRETSTASH_TOKEN=your-service-token
41
- sstash auth status
65
+ # Verify token works
66
+ sstash teams
42
67
  ```
43
68
 
44
69
  ### Working with Secrets
@@ -285,25 +310,51 @@ For CI/CD and automated workflows, use service tokens instead of user credential
285
310
 
286
311
  ## Commands Reference
287
312
 
313
+ ### Authentication
314
+ | Command | Description |
315
+ |---------|-------------|
316
+ | `sstash login` | Authenticate via browser (passkeys, OAuth, 2FA) |
317
+ | `sstash login --token <token>` | Authenticate with service token |
318
+ | `sstash logout` | Clear authentication |
319
+ | `sstash whoami` | Show current user/token info |
320
+ | `sstash 2fa setup` | Set up two-factor authentication |
321
+ | `sstash register` | Create a new SecretStash account |
322
+
323
+ ### Secrets
288
324
  | Command | Description |
289
325
  |---------|-------------|
290
- | `sstash auth login` | Authenticate with SecretStash |
291
- | `sstash auth logout` | Clear authentication |
292
- | `sstash auth status` | Show authentication status |
293
326
  | `sstash pull` | Pull secrets from SecretStash |
294
327
  | `sstash push` | Push secrets to SecretStash |
295
- | `sstash list` | List secrets in an environment |
296
- | `sstash get <key>` | Get a specific secret |
297
- | `sstash set <key>=<value>` | Set a specific secret |
298
- | `sstash delete <key>` | Delete a specific secret |
328
+ | `sstash secrets list` | List secrets in an environment |
329
+ | `sstash secrets get <key>` | Get a specific secret |
330
+ | `sstash secrets set <key>=<value>` | Set a specific secret |
331
+ | `sstash secrets delete <key>` | Delete a specific secret |
332
+ | `sstash secrets history <key>` | View version history for a secret |
333
+ | `sstash secrets rollback <key>` | Rollback to a previous version |
334
+ | `sstash secrets tag <key> <tag>` | Add a tag to a secret |
335
+ | `sstash secrets untag <key> <tag>` | Remove a tag from a secret |
336
+ | `sstash secrets expiring` | List secrets expiring soon |
299
337
  | `sstash run` | Run a command with secrets injected |
300
338
  | `sstash diff` | Compare local and remote secrets |
301
- | `sstash projects list` | List available projects |
302
- | `sstash projects use <name>` | Switch project context |
303
- | `sstash environments list` | List environments |
339
+
340
+ ### Organization
341
+ | Command | Description |
342
+ |---------|-------------|
343
+ | `sstash teams` | List teams |
344
+ | `sstash teams use <slug>` | Switch team context |
345
+ | `sstash projects` | List projects in current team |
346
+ | `sstash projects use <slug>` | Switch project context |
347
+ | `sstash environments` | List environments in current project |
304
348
  | `sstash environments create <name>` | Create a new environment |
305
- | `sstash teams list` | List teams |
306
- | `sstash teams switch <name>` | Switch team context |
349
+
350
+ ### Tags & Shares
351
+ | Command | Description |
352
+ |---------|-------------|
353
+ | `sstash tags` | List tags in current team |
354
+ | `sstash tags create <name>` | Create a new tag |
355
+ | `sstash share create <key>` | Create a share link for a secret |
356
+ | `sstash share list` | List active share links |
357
+ | `sstash share revoke <id>` | Revoke a share link |
307
358
 
308
359
  Use `sstash --help` or `sstash <command> --help` for detailed usage information.
309
360
 
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import { Command } from "commander";
5
5
  import chalk3 from "chalk";
6
6
  import { readFileSync as readFileSync4 } from "fs";
7
7
  import { fileURLToPath } from "url";
8
- import { dirname as dirname2, join as join2 } from "path";
8
+ import { dirname, join as join2 } from "path";
9
9
 
10
10
  // src/commands/auth.ts
11
11
  import enquirer from "enquirer";
@@ -306,7 +306,7 @@ var ApiClient = class {
306
306
  const refreshToken = configManager.getRefreshToken();
307
307
  if (!refreshToken) return false;
308
308
  try {
309
- const response = await fetch(`${configManager.getApiUrl()}/api/auth/refresh`, {
309
+ const response = await fetch(`${configManager.getApiUrl()}/api/v1/auth/refresh`, {
310
310
  method: "POST",
311
311
  headers: {
312
312
  "Content-Type": "application/json"
@@ -317,11 +317,12 @@ var ApiClient = class {
317
317
  configManager.logout();
318
318
  return false;
319
319
  }
320
- const data = await response.json();
320
+ const json = await response.json();
321
+ const tokens = json.data?.tokens || json.data || json;
321
322
  configManager.setTokens(
322
- data.accessToken,
323
- data.refreshToken,
324
- data.expiresIn || 900
323
+ tokens.accessToken,
324
+ tokens.refreshToken,
325
+ tokens.expiresIn || 900
325
326
  );
326
327
  return true;
327
328
  } catch {
@@ -358,17 +359,17 @@ var ApiClient = class {
358
359
  headers: requestHeaders,
359
360
  body: body ? JSON.stringify(body) : void 0
360
361
  });
361
- const data = await response.json();
362
+ const json = await response.json();
362
363
  if (!response.ok) {
363
364
  return {
364
365
  error: {
365
- code: data.code || "API_ERROR",
366
- message: data.message || `Request failed with status ${response.status}`,
367
- details: data.details
366
+ code: json.code || json.error?.code || "API_ERROR",
367
+ message: json.message || json.error?.message || `Request failed with status ${response.status}`,
368
+ details: json.details || json.error?.details
368
369
  }
369
370
  };
370
371
  }
371
- return { data };
372
+ return { data: json.data };
372
373
  } catch (error) {
373
374
  return {
374
375
  error: {
@@ -380,7 +381,7 @@ var ApiClient = class {
380
381
  }
381
382
  // Auth endpoints
382
383
  async login(email, password) {
383
- return this.request("/api/auth/login", {
384
+ return this.request("/api/v1/auth/login", {
384
385
  method: "POST",
385
386
  body: { email, password, client: "cli" },
386
387
  requireAuth: false
@@ -399,14 +400,14 @@ var ApiClient = class {
399
400
  });
400
401
  }
401
402
  async verify2FA(tempToken, code) {
402
- return this.request("/api/auth/2fa/verify", {
403
+ return this.request("/api/v1/auth/verify-2fa", {
403
404
  method: "POST",
404
- body: { tempToken, code },
405
+ body: { tempToken, token: code },
405
406
  requireAuth: false
406
407
  });
407
408
  }
408
409
  async register(email, password) {
409
- return this.request("/api/auth/register", {
410
+ return this.request("/api/v1/auth/register", {
410
411
  method: "POST",
411
412
  body: { email, password },
412
413
  requireAuth: false
@@ -415,7 +416,7 @@ var ApiClient = class {
415
416
  async logout() {
416
417
  const refreshToken = configManager.getRefreshToken();
417
418
  if (refreshToken) {
418
- await this.request("/api/auth/logout", {
419
+ await this.request("/api/v1/auth/logout", {
419
420
  method: "POST",
420
421
  body: { refreshToken }
421
422
  });
@@ -423,39 +424,39 @@ var ApiClient = class {
423
424
  configManager.logout();
424
425
  }
425
426
  async getMe() {
426
- return this.request("/api/users/me");
427
+ return this.request("/api/v1/users/me");
427
428
  }
428
429
  // Team endpoints
429
430
  async getTeams() {
430
- return this.request("/api/teams");
431
+ return this.request("/api/v1/teams");
431
432
  }
432
433
  async createTeam(name) {
433
- return this.request("/api/teams", {
434
+ return this.request("/api/v1/teams", {
434
435
  method: "POST",
435
436
  body: { name }
436
437
  });
437
438
  }
438
439
  // Project endpoints
439
440
  async getProjects(teamId) {
440
- return this.request(`/api/teams/${teamId}/projects`);
441
+ return this.request(`/api/v1/teams/${teamId}/projects`);
441
442
  }
442
443
  async createProject(teamId, name, description) {
443
- return this.request(`/api/teams/${teamId}/projects`, {
444
+ return this.request(`/api/v1/teams/${teamId}/projects`, {
444
445
  method: "POST",
445
446
  body: { name, description }
446
447
  });
447
448
  }
448
449
  async deleteProject(projectId) {
449
- return this.request(`/api/projects/${projectId}`, {
450
+ return this.request(`/api/v1/projects/${projectId}`, {
450
451
  method: "DELETE"
451
452
  });
452
453
  }
453
454
  // Environment endpoints
454
455
  async getEnvironments(projectId) {
455
- return this.request(`/api/projects/${projectId}/environments`);
456
+ return this.request(`/api/v1/projects/${projectId}/environments`);
456
457
  }
457
458
  async createEnvironment(projectId, name, parentId) {
458
- return this.request(`/api/projects/${projectId}/environments`, {
459
+ return this.request(`/api/v1/projects/${projectId}/environments`, {
459
460
  method: "POST",
460
461
  body: { name, parentId }
461
462
  });
@@ -467,29 +468,29 @@ var ApiClient = class {
467
468
  }
468
469
  // Secret endpoints (keys only, no decryption)
469
470
  async getSecrets(environmentId) {
470
- return this.request(`/api/environments/${environmentId}/secrets`);
471
+ return this.request(`/api/v1/environments/${environmentId}/secrets`);
471
472
  }
472
473
  // Decrypted secrets (requires password)
473
474
  async getDecryptedSecrets(environmentId, password) {
474
- return this.request(`/api/environments/${environmentId}/secrets/decrypt`, {
475
+ return this.request(`/api/v1/environments/${environmentId}/secrets/decrypt`, {
475
476
  method: "POST",
476
477
  body: { password }
477
478
  });
478
479
  }
479
480
  async createSecret(environmentId, key, value, password, description) {
480
- return this.request(`/api/environments/${environmentId}/secrets`, {
481
+ return this.request(`/api/v1/environments/${environmentId}/secrets`, {
481
482
  method: "POST",
482
483
  body: { key, value, password, description }
483
484
  });
484
485
  }
485
486
  async updateSecret(secretId, value, password, description) {
486
- return this.request(`/api/secrets/${secretId}`, {
487
+ return this.request(`/api/v1/secrets/${secretId}`, {
487
488
  method: "PATCH",
488
489
  body: { value, password, description }
489
490
  });
490
491
  }
491
492
  async deleteSecret(secretId) {
492
- return this.request(`/api/secrets/${secretId}`, {
493
+ return this.request(`/api/v1/secrets/${secretId}`, {
493
494
  method: "DELETE"
494
495
  });
495
496
  }
@@ -497,58 +498,58 @@ var ApiClient = class {
497
498
  async pullSecrets(teamSlug, projectSlug, environment, password, options) {
498
499
  const includeInherited = options?.includeInherited ?? true;
499
500
  const query = includeInherited ? "" : "?includeInherited=false";
500
- return this.request(`/api/teams/${teamSlug}/projects/${projectSlug}/environments/${environment}/secrets/decrypt${query}`, {
501
+ return this.request(`/api/v1/teams/${teamSlug}/projects/${projectSlug}/environments/${environment}/secrets/decrypt${query}`, {
501
502
  method: "POST",
502
503
  body: { password }
503
504
  });
504
505
  }
505
506
  async pushSecrets(teamSlug, projectSlug, environment, secrets, password) {
506
- return this.request(`/api/teams/${teamSlug}/projects/${projectSlug}/environments/${environment}/secrets/bulk`, {
507
+ return this.request(`/api/v1/teams/${teamSlug}/projects/${projectSlug}/environments/${environment}/secrets/bulk`, {
507
508
  method: "PUT",
508
509
  body: { secrets, password }
509
510
  });
510
511
  }
511
512
  // 2FA endpoints
512
513
  async setup2FA() {
513
- return this.request("/api/users/me/2fa/setup", {
514
+ return this.request("/api/v1/users/me/2fa/setup", {
514
515
  method: "POST"
515
516
  });
516
517
  }
517
518
  async enable2FA(code) {
518
- return this.request("/api/users/me/2fa/enable", {
519
+ return this.request("/api/v1/users/me/2fa/verify", {
519
520
  method: "POST",
520
521
  body: { code }
521
522
  });
522
523
  }
523
524
  async disable2FA(code) {
524
- return this.request("/api/users/me/2fa/disable", {
525
- method: "POST",
525
+ return this.request("/api/v1/users/me/2fa", {
526
+ method: "DELETE",
526
527
  body: { code }
527
528
  });
528
529
  }
529
530
  // Team member endpoints
530
531
  async getTeamMembers(teamId) {
531
- return this.request(`/api/teams/${teamId}/members`);
532
+ return this.request(`/api/v1/teams/${teamId}/members`);
532
533
  }
533
534
  async inviteTeamMember(teamId, email, role) {
534
- return this.request(`/api/teams/${teamId}/invitations`, {
535
+ return this.request(`/api/v1/teams/${teamId}/invitations`, {
535
536
  method: "POST",
536
537
  body: { email, role }
537
538
  });
538
539
  }
539
540
  async removeTeamMember(teamId, memberId) {
540
- return this.request(`/api/teams/${teamId}/members/${memberId}`, {
541
+ return this.request(`/api/v1/teams/${teamId}/members/${memberId}`, {
541
542
  method: "DELETE"
542
543
  });
543
544
  }
544
545
  async updateTeamMemberRole(teamId, memberId, role) {
545
- return this.request(`/api/teams/${teamId}/members/${memberId}`, {
546
+ return this.request(`/api/v1/teams/${teamId}/members/${memberId}`, {
546
547
  method: "PATCH",
547
548
  body: { role }
548
549
  });
549
550
  }
550
551
  async getPendingInvitations(teamId) {
551
- return this.request(`/api/teams/${teamId}/invitations`);
552
+ return this.request(`/api/v1/teams/${teamId}/invitations`);
552
553
  }
553
554
  // Share link endpoints
554
555
  async createShare(teamSlug, projectSlug, environment, secretKey, options) {
@@ -574,7 +575,7 @@ var ApiClient = class {
574
575
  }
575
576
  // Service token verification - make a request to validate the token works
576
577
  async verifyServiceToken(token) {
577
- return this.request("/api/teams", {
578
+ return this.request("/api/v1/teams", {
578
579
  headers: {
579
580
  "Authorization": `Bearer ${token}`
580
581
  },
@@ -1045,7 +1046,7 @@ function registerAuthCommands(program2) {
1045
1046
  ui.heading("Two-Factor Authentication Setup");
1046
1047
  ui.info("Scan this QR code with your authenticator app:");
1047
1048
  ui.br();
1048
- qrcode.generate(result.data.qrCodeUrl, { small: true });
1049
+ qrcode.generate(result.data.otpauthUrl, { small: true });
1049
1050
  ui.br();
1050
1051
  ui.subheading("Or enter this secret manually:");
1051
1052
  console.log(colors.primary.bold(` ${result.data.secret}`));
@@ -1106,7 +1107,7 @@ function registerAuthCommands(program2) {
1106
1107
  return true;
1107
1108
  }
1108
1109
  });
1109
- const { confirmPassword } = await prompt({
1110
+ await prompt({
1110
1111
  type: "password",
1111
1112
  name: "confirmPassword",
1112
1113
  message: "Confirm Password:",
@@ -2834,12 +2835,37 @@ function registerSecretCommands(program2) {
2834
2835
  }
2835
2836
  const ctx = requireContext();
2836
2837
  const environment = options.env || ctx.environment;
2838
+ const projectSlug = options.project || ctx.projectSlug;
2837
2839
  const currentTeam = configManager.getCurrentTeam();
2838
2840
  if (!currentTeam) {
2839
2841
  ui.error("No team selected. Run `sstash teams use <slug>` first.");
2840
2842
  process.exit(1);
2841
2843
  }
2842
2844
  const spinner = ui.spinner("Looking up secret and tag...");
2845
+ const projectsResult = await apiClient.getProjects(currentTeam.id);
2846
+ if (projectsResult.error) {
2847
+ spinner.fail();
2848
+ ui.error(projectsResult.error.message);
2849
+ process.exit(1);
2850
+ }
2851
+ const project = projectsResult.data?.projects.find((p) => p.slug === projectSlug);
2852
+ if (!project) {
2853
+ spinner.fail();
2854
+ ui.error(`Project "${projectSlug}" not found`);
2855
+ process.exit(1);
2856
+ }
2857
+ const envsResult = await apiClient.getEnvironments(project.id);
2858
+ if (envsResult.error) {
2859
+ spinner.fail();
2860
+ ui.error(envsResult.error.message);
2861
+ process.exit(1);
2862
+ }
2863
+ const env = envsResult.data?.environments.find((e) => e.slug === environment || e.name.toLowerCase() === environment.toLowerCase());
2864
+ if (!env) {
2865
+ spinner.fail();
2866
+ ui.error(`Environment "${environment}" not found`);
2867
+ process.exit(1);
2868
+ }
2843
2869
  const tagsResult = await apiClient.getTags(currentTeam.id);
2844
2870
  if (tagsResult.error) {
2845
2871
  spinner.fail();
@@ -2855,7 +2881,7 @@ function registerSecretCommands(program2) {
2855
2881
  ui.info(`Create it with ${ui.code(`sstash tags create ${tagname}`)}`);
2856
2882
  process.exit(1);
2857
2883
  }
2858
- const secretsResult = await apiClient.getSecrets(environment);
2884
+ const secretsResult = await apiClient.getSecrets(env.id);
2859
2885
  if (secretsResult.error) {
2860
2886
  spinner.fail();
2861
2887
  ui.error(secretsResult.error.message);
@@ -2889,12 +2915,37 @@ function registerSecretCommands(program2) {
2889
2915
  }
2890
2916
  const ctx = requireContext();
2891
2917
  const environment = options.env || ctx.environment;
2918
+ const projectSlug = options.project || ctx.projectSlug;
2892
2919
  const currentTeam = configManager.getCurrentTeam();
2893
2920
  if (!currentTeam) {
2894
2921
  ui.error("No team selected. Run `sstash teams use <slug>` first.");
2895
2922
  process.exit(1);
2896
2923
  }
2897
2924
  const spinner = ui.spinner("Looking up secret and tag...");
2925
+ const projectsResult = await apiClient.getProjects(currentTeam.id);
2926
+ if (projectsResult.error) {
2927
+ spinner.fail();
2928
+ ui.error(projectsResult.error.message);
2929
+ process.exit(1);
2930
+ }
2931
+ const project = projectsResult.data?.projects.find((p) => p.slug === projectSlug);
2932
+ if (!project) {
2933
+ spinner.fail();
2934
+ ui.error(`Project "${projectSlug}" not found`);
2935
+ process.exit(1);
2936
+ }
2937
+ const envsResult = await apiClient.getEnvironments(project.id);
2938
+ if (envsResult.error) {
2939
+ spinner.fail();
2940
+ ui.error(envsResult.error.message);
2941
+ process.exit(1);
2942
+ }
2943
+ const env = envsResult.data?.environments.find((e) => e.slug === environment || e.name.toLowerCase() === environment.toLowerCase());
2944
+ if (!env) {
2945
+ spinner.fail();
2946
+ ui.error(`Environment "${environment}" not found`);
2947
+ process.exit(1);
2948
+ }
2898
2949
  const tagsResult = await apiClient.getTags(currentTeam.id);
2899
2950
  if (tagsResult.error) {
2900
2951
  spinner.fail();
@@ -2909,7 +2960,7 @@ function registerSecretCommands(program2) {
2909
2960
  ui.error(`Tag "${tagname}" not found`);
2910
2961
  process.exit(1);
2911
2962
  }
2912
- const secretsResult = await apiClient.getSecrets(environment);
2963
+ const secretsResult = await apiClient.getSecrets(env.id);
2913
2964
  if (secretsResult.error) {
2914
2965
  spinner.fail();
2915
2966
  ui.error(secretsResult.error.message);
@@ -4076,7 +4127,7 @@ function registerExecCommand(program2) {
4076
4127
  }
4077
4128
 
4078
4129
  // src/index.ts
4079
- var __dirname = dirname2(fileURLToPath(import.meta.url));
4130
+ var __dirname = dirname(fileURLToPath(import.meta.url));
4080
4131
  var version = "0.1.0";
4081
4132
  try {
4082
4133
  const pkg = JSON.parse(readFileSync4(join2(__dirname, "..", "package.json"), "utf-8"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@secretstash/cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.8",
4
4
  "description": "CLI tool for SecretStash - secure team secrets management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",