@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/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/lib/app.test.ts +68 -0
- package/src/lib/app.ts +22 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hexclave/cli",
|
|
3
|
-
"version": "1.0.
|
|
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.
|
|
29
|
-
"@hexclave/shared": "1.0.
|
|
30
|
-
"@hexclave/js": "1.0.
|
|
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
|
-
|
|
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
|
|