@hexclave/cli 1.0.64 → 1.0.66

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hexclave/cli",
3
- "version": "1.0.64",
3
+ "version": "1.0.66",
4
4
  "repository": "https://github.com/hexclave/hexclave",
5
5
  "description": "The CLI for Hexclave. https://hexclave.com",
6
6
  "main": "dist/index.js",
@@ -25,9 +25,9 @@
25
25
  "commander": "^13.1.0",
26
26
  "extract-zip": "^2.0.1",
27
27
  "jiti": "^2.4.2",
28
- "@hexclave/shared-backend": "1.0.64",
29
- "@hexclave/shared": "1.0.64",
30
- "@hexclave/js": "1.0.64"
28
+ "@hexclave/shared-backend": "1.0.66",
29
+ "@hexclave/shared": "1.0.66",
30
+ "@hexclave/js": "1.0.66"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "20.17.6",
@@ -0,0 +1,68 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { AuthError } from "./errors.js";
3
+
4
+ const { getUser } = vi.hoisted(() => ({
5
+ getUser: vi.fn(),
6
+ }));
7
+
8
+ vi.mock("@hexclave/js", () => ({
9
+ StackClientApp: class {
10
+ getUser = getUser;
11
+ },
12
+ }));
13
+
14
+ import { getInternalUser } from "./app.js";
15
+
16
+ const auth = {
17
+ apiUrl: "https://api.hexclave.com",
18
+ dashboardUrl: "https://app.hexclave.com",
19
+ publishableClientKey: "pck_test",
20
+ refreshToken: "refresh-token",
21
+ };
22
+
23
+ describe("getInternalUser", () => {
24
+ beforeEach(() => {
25
+ getUser.mockReset();
26
+ });
27
+
28
+ it("throws a friendly auth error when there is no signed-in user", async () => {
29
+ getUser.mockResolvedValue(null);
30
+
31
+ const result = getInternalUser(auth);
32
+ await expect(result).rejects.toBeInstanceOf(AuthError);
33
+ await expect(result).rejects.toThrowErrorMatchingInlineSnapshot(
34
+ `[AuthError: Your session is invalid or expired. Run \`hexclave login\` again.]`,
35
+ );
36
+ await expect(result).rejects.not.toThrow("User is not signed in but getUser was called with");
37
+ expect(getUser).toHaveBeenCalledWith({ includeRestricted: true });
38
+ });
39
+
40
+ it("throws a friendly onboarding error for restricted users", async () => {
41
+ getUser.mockResolvedValue({ isRestricted: true });
42
+
43
+ const result = getInternalUser(auth);
44
+ await expect(result).rejects.toBeInstanceOf(AuthError);
45
+ await expect(result).rejects.toThrowErrorMatchingInlineSnapshot(
46
+ `[AuthError: Finish setting up your account at https://app.hexclave.com/onboarding before using the CLI.]`,
47
+ );
48
+ });
49
+
50
+ it("keeps the onboarding auth error friendly for malformed and nested dashboard URLs", async () => {
51
+ getUser.mockResolvedValue({ isRestricted: true });
52
+
53
+ const malformedResult = getInternalUser({ ...auth, dashboardUrl: "not-a-url" });
54
+ await expect(malformedResult).rejects.toBeInstanceOf(AuthError);
55
+ await expect(malformedResult).rejects.toThrowErrorMatchingInlineSnapshot(
56
+ `[AuthError: Finish setting up your account at not-a-url/onboarding before using the CLI.]`,
57
+ );
58
+
59
+ const nestedResult = getInternalUser({
60
+ ...auth,
61
+ dashboardUrl: "https://app.hexclave.com/console?tenant=one#settings",
62
+ });
63
+ await expect(nestedResult).rejects.toBeInstanceOf(AuthError);
64
+ await expect(nestedResult).rejects.toThrowErrorMatchingInlineSnapshot(
65
+ `[AuthError: Finish setting up your account at https://app.hexclave.com/console/onboarding before using the CLI.]`,
66
+ );
67
+ });
68
+ });
package/src/lib/app.ts CHANGED
@@ -1,8 +1,21 @@
1
1
  import { StackClientApp } from "@hexclave/js";
2
2
  import type { CurrentInternalUser, AdminOwnedProject } from "@hexclave/js";
3
+ import { createUrlIfValid } from "@hexclave/shared/dist/utils/urls";
3
4
  import { AuthError } from "./errors.js";
4
5
  import type { SessionAuth, ProjectAuthWithRefreshToken } from "./auth.js";
5
6
 
7
+ function onboardingUrlFor(dashboardUrl: string): string {
8
+ const parsedDashboardUrl = createUrlIfValid(dashboardUrl);
9
+ if (parsedDashboardUrl == null) {
10
+ // Configured dashboard URLs are unvalidated, so this fallback must not throw.
11
+ return `${dashboardUrl.replace(/\/+$/, "")}/onboarding`;
12
+ }
13
+ parsedDashboardUrl.pathname = `${parsedDashboardUrl.pathname.replace(/\/+$/, "")}/onboarding`;
14
+ parsedDashboardUrl.search = "";
15
+ parsedDashboardUrl.hash = "";
16
+ return parsedDashboardUrl.toString();
17
+ }
18
+
6
19
  export function getInternalApp(auth: SessionAuth): StackClientApp<true, "internal"> {
7
20
  return new StackClientApp({
8
21
  projectId: "internal",
@@ -18,7 +31,15 @@ export function getInternalApp(auth: SessionAuth): StackClientApp<true, "interna
18
31
 
19
32
  export async function getInternalUser(auth: SessionAuth): Promise<CurrentInternalUser> {
20
33
  const app = getInternalApp(auth);
21
- const user = await app.getUser({ or: "throw" });
34
+ // Avoid `or: "throw"` here: its internal error message leaked to CLI users
35
+ // and was reported as a fatal Sentry event instead of a normal auth error.
36
+ const user = await app.getUser({ includeRestricted: true });
37
+ if (user == null) {
38
+ throw new AuthError("Your session is invalid or expired. Run `hexclave login` again.");
39
+ }
40
+ if (user.isRestricted) {
41
+ throw new AuthError(`Finish setting up your account at ${onboardingUrlFor(auth.dashboardUrl)} before using the CLI.`);
42
+ }
22
43
  return user as CurrentInternalUser;
23
44
  }
24
45