@crossmint/cli 1.1.2
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/README.md +111 -0
- package/build-pkg.js +13 -0
- package/dist/bin/crossmint.d.ts +3 -0
- package/dist/bin/crossmint.d.ts.map +1 -0
- package/dist/bin/crossmint.js +18 -0
- package/dist/commands/keys/create.d.ts +2 -0
- package/dist/commands/keys/create.d.ts.map +1 -0
- package/dist/commands/keys/create.js +2 -0
- package/dist/commands/keys/create.test.d.ts +2 -0
- package/dist/commands/keys/create.test.d.ts.map +1 -0
- package/dist/commands/keys/delete.d.ts +2 -0
- package/dist/commands/keys/delete.d.ts.map +1 -0
- package/dist/commands/keys/delete.js +1 -0
- package/dist/commands/keys/delete.test.d.ts +2 -0
- package/dist/commands/keys/delete.test.d.ts.map +1 -0
- package/dist/commands/keys/edit.d.ts +2 -0
- package/dist/commands/keys/edit.d.ts.map +1 -0
- package/dist/commands/keys/edit.js +2 -0
- package/dist/commands/keys/edit.test.d.ts +2 -0
- package/dist/commands/keys/edit.test.d.ts.map +1 -0
- package/dist/commands/keys/index.d.ts +5 -0
- package/dist/commands/keys/index.d.ts.map +1 -0
- package/dist/commands/keys/index.js +11 -0
- package/dist/commands/keys/list.d.ts +3 -0
- package/dist/commands/keys/list.d.ts.map +1 -0
- package/dist/commands/keys/list.js +10 -0
- package/dist/commands/keys/list.test.d.ts +2 -0
- package/dist/commands/keys/list.test.d.ts.map +1 -0
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +1 -0
- package/dist/commands/login.test.d.ts +2 -0
- package/dist/commands/login.test.d.ts.map +1 -0
- package/dist/commands/logout.d.ts +2 -0
- package/dist/commands/logout.d.ts.map +1 -0
- package/dist/commands/logout.js +1 -0
- package/dist/commands/projects/create.d.ts +6 -0
- package/dist/commands/projects/create.d.ts.map +1 -0
- package/dist/commands/projects/create.js +6 -0
- package/dist/commands/projects/create.test.d.ts +2 -0
- package/dist/commands/projects/create.test.d.ts.map +1 -0
- package/dist/commands/projects/details.d.ts +2 -0
- package/dist/commands/projects/details.d.ts.map +1 -0
- package/dist/commands/projects/details.js +2 -0
- package/dist/commands/projects/details.test.d.ts +2 -0
- package/dist/commands/projects/details.test.d.ts.map +1 -0
- package/dist/commands/projects/index.d.ts +4 -0
- package/dist/commands/projects/index.d.ts.map +1 -0
- package/dist/commands/projects/index.js +7 -0
- package/dist/commands/projects/select.d.ts +2 -0
- package/dist/commands/projects/select.d.ts.map +1 -0
- package/dist/commands/projects/select.js +1 -0
- package/dist/commands/projects/select.test.d.ts +2 -0
- package/dist/commands/projects/select.test.d.ts.map +1 -0
- package/dist/commands/whoami.d.ts +2 -0
- package/dist/commands/whoami.d.ts.map +1 -0
- package/dist/commands/whoami.js +1 -0
- package/dist/types/keys.d.ts +11 -0
- package/dist/types/keys.d.ts.map +1 -0
- package/dist/types/keys.js +0 -0
- package/dist/utils/Pager.d.ts +9 -0
- package/dist/utils/Pager.d.ts.map +1 -0
- package/dist/utils/Pager.js +2 -0
- package/dist/utils/Pager.test.d.ts +2 -0
- package/dist/utils/Pager.test.d.ts.map +1 -0
- package/dist/utils/fetch.d.ts +5 -0
- package/dist/utils/fetch.d.ts.map +1 -0
- package/dist/utils/fetch.js +1 -0
- package/dist/utils/getProject.d.ts +4 -0
- package/dist/utils/getProject.d.ts.map +1 -0
- package/dist/utils/getProject.js +1 -0
- package/dist/utils/getWhitelistedOriginsAndAppIdentifiersPrompt.d.ts +6 -0
- package/dist/utils/getWhitelistedOriginsAndAppIdentifiersPrompt.d.ts.map +1 -0
- package/dist/utils/getWhitelistedOriginsAndAppIdentifiersPrompt.js +2 -0
- package/dist/utils/keytar.d.ts +4 -0
- package/dist/utils/keytar.d.ts.map +1 -0
- package/dist/utils/keytar.js +1 -0
- package/dist/utils/oauth/codeChallenge.d.ts +3 -0
- package/dist/utils/oauth/codeChallenge.d.ts.map +1 -0
- package/dist/utils/oauth/codeChallenge.js +1 -0
- package/dist/utils/oauth/getStytchConfig.d.ts +6 -0
- package/dist/utils/oauth/getStytchConfig.d.ts.map +1 -0
- package/dist/utils/oauth/getStytchConfig.js +1 -0
- package/dist/utils/oauth/server.d.ts +3 -0
- package/dist/utils/oauth/server.d.ts.map +1 -0
- package/dist/utils/oauth/server.js +1 -0
- package/dist/utils/scopes.d.ts +14 -0
- package/dist/utils/scopes.d.ts.map +1 -0
- package/dist/utils/scopes.js +1 -0
- package/dist/utils/spinner.d.ts +6 -0
- package/dist/utils/spinner.d.ts.map +1 -0
- package/dist/utils/spinner.js +1 -0
- package/dist/utils/store.d.ts +11 -0
- package/dist/utils/store.d.ts.map +1 -0
- package/dist/utils/store.js +1 -0
- package/dist/utils/urls.d.ts +9 -0
- package/dist/utils/urls.d.ts.map +1 -0
- package/dist/utils/urls.js +1 -0
- package/package.json +50 -0
- package/shims/prelude.js +33 -0
- package/src/bin/crossmint.ts +34 -0
- package/src/commands/keys/create.test.ts +230 -0
- package/src/commands/keys/create.ts +96 -0
- package/src/commands/keys/delete.test.ts +87 -0
- package/src/commands/keys/delete.ts +36 -0
- package/src/commands/keys/edit.test.ts +159 -0
- package/src/commands/keys/edit.ts +98 -0
- package/src/commands/keys/index.ts +4 -0
- package/src/commands/keys/list.test.ts +123 -0
- package/src/commands/keys/list.ts +83 -0
- package/src/commands/login.test.ts +87 -0
- package/src/commands/login.ts +59 -0
- package/src/commands/logout.ts +9 -0
- package/src/commands/projects/create.test.ts +82 -0
- package/src/commands/projects/create.ts +45 -0
- package/src/commands/projects/details.test.ts +64 -0
- package/src/commands/projects/details.ts +36 -0
- package/src/commands/projects/index.ts +3 -0
- package/src/commands/projects/select.test.ts +86 -0
- package/src/commands/projects/select.ts +50 -0
- package/src/commands/whoami.ts +19 -0
- package/src/types/keys.ts +13 -0
- package/src/utils/Pager.test.ts +70 -0
- package/src/utils/Pager.ts +34 -0
- package/src/utils/fetch.ts +63 -0
- package/src/utils/getProject.ts +10 -0
- package/src/utils/getWhitelistedOriginsAndAppIdentifiersPrompt.ts +52 -0
- package/src/utils/keytar.ts +15 -0
- package/src/utils/oauth/codeChallenge.ts +18 -0
- package/src/utils/oauth/getStytchConfig.ts +28 -0
- package/src/utils/oauth/server.ts +66 -0
- package/src/utils/scopes.ts +66 -0
- package/src/utils/spinner.ts +20 -0
- package/src/utils/store.ts +38 -0
- package/src/utils/urls.ts +11 -0
- package/tsconfig.json +35 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/tsup.config.ts +11 -0
- package/vitest.config.ts +9 -0
- package/vitest.setup.ts +8 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { HttpMethods, routes } from "@crossmint/common-consts";
|
|
3
|
+
import { fetchAuthenticated } from "@/utils/fetch";
|
|
4
|
+
import { getProjectId } from "@/utils/store";
|
|
5
|
+
import { confirm } from "@inquirer/prompts";
|
|
6
|
+
import deleteKey from "./delete";
|
|
7
|
+
import { createSpinner } from "@/utils/spinner";
|
|
8
|
+
|
|
9
|
+
// Mock dependencies
|
|
10
|
+
vi.mock("@/utils/fetch", () => ({
|
|
11
|
+
fetchAuthenticated: vi.fn(),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
vi.mock("@/utils/store", () => ({
|
|
15
|
+
getProjectId: vi.fn(),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
vi.mock("@inquirer/prompts", () => ({
|
|
19
|
+
confirm: vi.fn(),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
vi.mock("@/utils/spinner");
|
|
23
|
+
|
|
24
|
+
describe("deleteKey", () => {
|
|
25
|
+
const mockProjectId = "test-project-id";
|
|
26
|
+
const mockKeyId = "test-key-id";
|
|
27
|
+
|
|
28
|
+
let spinner: any;
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
vi.clearAllMocks();
|
|
32
|
+
(getProjectId as any).mockResolvedValue(mockProjectId);
|
|
33
|
+
(confirm as any).mockResolvedValue(true);
|
|
34
|
+
spinner = {
|
|
35
|
+
fail: vi.fn(),
|
|
36
|
+
succeed: vi.fn(),
|
|
37
|
+
start: vi.fn(),
|
|
38
|
+
};
|
|
39
|
+
(createSpinner as any).mockReturnValue(spinner);
|
|
40
|
+
vi.spyOn(console, "error");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should successfully delete an API key", async () => {
|
|
44
|
+
await deleteKey(mockKeyId);
|
|
45
|
+
|
|
46
|
+
// Verify getProjectId was called
|
|
47
|
+
expect(getProjectId).toHaveBeenCalled();
|
|
48
|
+
|
|
49
|
+
// Verify confirm was called with the correct message
|
|
50
|
+
expect(confirm).toHaveBeenCalledWith({
|
|
51
|
+
message: "Are you sure you want to delete this API key? This action cannot be undone.",
|
|
52
|
+
default: false,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Verify fetchAuthenticated was called with the correct parameters
|
|
56
|
+
expect(fetchAuthenticated).toHaveBeenCalledWith(
|
|
57
|
+
routes.api.console.projects.apiKeys(mockProjectId, mockKeyId),
|
|
58
|
+
HttpMethods.DELETE
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("should exit if no project is selected", async () => {
|
|
63
|
+
(getProjectId as any).mockResolvedValue(null);
|
|
64
|
+
|
|
65
|
+
await expect(deleteKey(mockKeyId)).rejects.toThrow();
|
|
66
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
67
|
+
"There is no project selected, please first run `crossmint project select`"
|
|
68
|
+
);
|
|
69
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should exit if user cancels the operation", async () => {
|
|
73
|
+
(confirm as any).mockResolvedValueOnce(false);
|
|
74
|
+
|
|
75
|
+
await deleteKey(mockKeyId);
|
|
76
|
+
expect(spinner.fail).toHaveBeenCalledWith("Operation cancelled");
|
|
77
|
+
expect(process.exit).toHaveBeenCalledWith(0);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("should handle delete error", async () => {
|
|
81
|
+
(fetchAuthenticated as any).mockRejectedValue(new Error("Delete failed"));
|
|
82
|
+
|
|
83
|
+
await expect(deleteKey(mockKeyId)).rejects.toThrow();
|
|
84
|
+
expect(spinner.fail).toHaveBeenCalledWith("❌ Failed to delete API key: Delete failed");
|
|
85
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { HttpMethods } from "@crossmint/common-consts";
|
|
2
|
+
import { routes } from "@crossmint/common-consts";
|
|
3
|
+
import { fetchAuthenticated } from "@/utils/fetch";
|
|
4
|
+
import { getProjectId } from "@/utils/store";
|
|
5
|
+
import { confirm } from "@inquirer/prompts";
|
|
6
|
+
import { createSpinner } from "@/utils/spinner";
|
|
7
|
+
|
|
8
|
+
export default async function deleteKey(keyId: string): Promise<void> {
|
|
9
|
+
const projectId = await getProjectId();
|
|
10
|
+
if (!projectId) {
|
|
11
|
+
console.error("There is no project selected, please first run `crossmint project select`");
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const spinner = createSpinner("Deleting API key...");
|
|
16
|
+
try {
|
|
17
|
+
const confirmed = await confirm({
|
|
18
|
+
message: "Are you sure you want to delete this API key? This action cannot be undone.",
|
|
19
|
+
default: false,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
if (!confirmed) {
|
|
23
|
+
spinner.fail("Operation cancelled");
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
spinner.start();
|
|
28
|
+
|
|
29
|
+
await fetchAuthenticated(routes.api.console.projects.apiKeys(projectId, keyId), HttpMethods.DELETE);
|
|
30
|
+
|
|
31
|
+
spinner.succeed("✅ API key deleted successfully!");
|
|
32
|
+
} catch (error) {
|
|
33
|
+
spinner.fail(`❌ Failed to delete API key: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { HttpMethods } from "@crossmint/common-consts";
|
|
3
|
+
import { routes } from "@crossmint/common-consts";
|
|
4
|
+
import { fetchAuthenticated } from "@/utils/fetch";
|
|
5
|
+
import { getEnvironment, getProjectId } from "@/utils/store";
|
|
6
|
+
import { checkbox } from "@inquirer/prompts";
|
|
7
|
+
import editKey from "./edit";
|
|
8
|
+
import { APIKeyDocument } from "@crossmint/products-console-types";
|
|
9
|
+
import { getScopeChoices } from "@/utils/scopes";
|
|
10
|
+
import { createSpinner } from "@/utils/spinner";
|
|
11
|
+
import { getProject } from "@/utils/getProject";
|
|
12
|
+
import { getWhitelistedOriginsAndAppIdentifiersPrompt } from "@/utils/getWhitelistedOriginsAndAppIdentifiersPrompt";
|
|
13
|
+
|
|
14
|
+
vi.mock("@/utils/fetch", () => ({
|
|
15
|
+
fetchAuthenticated: vi.fn(),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
vi.mock("@/utils/store", () => ({
|
|
19
|
+
getProjectId: vi.fn(),
|
|
20
|
+
getEnvironment: vi.fn(),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
vi.mock("@inquirer/prompts", async (importOriginal) => ({
|
|
24
|
+
...(await importOriginal()),
|
|
25
|
+
checkbox: vi.fn(),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
vi.mock("@/utils/getProject", () => ({
|
|
29
|
+
getProject: vi.fn(),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
vi.mock("@/utils/getWhitelistedOriginsAndAppIdentifiersPrompt", () => ({
|
|
33
|
+
getWhitelistedOriginsAndAppIdentifiersPrompt: vi.fn(),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
vi.mock("@/utils/spinner");
|
|
37
|
+
|
|
38
|
+
describe("editKey", () => {
|
|
39
|
+
const mockProjectId = "test-project-id";
|
|
40
|
+
const mockKeyId = "test-key-id";
|
|
41
|
+
const mockKey: APIKeyDocument = {
|
|
42
|
+
_id: mockKeyId as any,
|
|
43
|
+
usageOrigin: "server",
|
|
44
|
+
projectId: mockProjectId as any,
|
|
45
|
+
clientId: "test-client-id",
|
|
46
|
+
clientSecret: "test-client-secret",
|
|
47
|
+
hashedSecret: "test-hashed-secret",
|
|
48
|
+
scopes: ["wallets.read"],
|
|
49
|
+
isDeleted: false,
|
|
50
|
+
createdAt: Date.now(),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
let spinner: any;
|
|
54
|
+
const activeProject: any = {};
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
spinner = {
|
|
57
|
+
start: vi.fn(),
|
|
58
|
+
succeed: vi.fn(),
|
|
59
|
+
fail: vi.fn(),
|
|
60
|
+
};
|
|
61
|
+
vi.clearAllMocks();
|
|
62
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
63
|
+
(getProjectId as any).mockResolvedValue(mockProjectId);
|
|
64
|
+
(fetchAuthenticated as any).mockResolvedValue([mockKey]);
|
|
65
|
+
(checkbox as any).mockResolvedValue(["wallets.read", "wallets.create"]);
|
|
66
|
+
(createSpinner as any).mockReturnValue(spinner);
|
|
67
|
+
(getEnvironment as any).mockResolvedValue("production");
|
|
68
|
+
(getProject as any).mockResolvedValue(activeProject);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should successfully edit an API key", async () => {
|
|
72
|
+
await editKey(mockKeyId);
|
|
73
|
+
|
|
74
|
+
expect(getProjectId).toHaveBeenCalled();
|
|
75
|
+
|
|
76
|
+
expect(fetchAuthenticated).toHaveBeenCalledWith(routes.api.console.projects.apiKeys(mockProjectId));
|
|
77
|
+
|
|
78
|
+
expect(checkbox).toHaveBeenCalledWith({
|
|
79
|
+
message: `Select scopes for ${mockKey.usageOrigin} key:`,
|
|
80
|
+
choices: getScopeChoices(mockKey.usageOrigin, "production", activeProject, mockKey.scopes),
|
|
81
|
+
validate: expect.any(Function),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
expect(fetchAuthenticated).toHaveBeenCalledWith(
|
|
85
|
+
routes.api.console.projects.apiKeys(mockProjectId),
|
|
86
|
+
HttpMethods.PUT,
|
|
87
|
+
expect.objectContaining({
|
|
88
|
+
apiKeyId: mockKeyId,
|
|
89
|
+
scopes: ["wallets.read", "wallets.create"],
|
|
90
|
+
})
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
it("should not update API key scopes if they are the same", async () => {
|
|
94
|
+
(checkbox as any).mockResolvedValue(["wallets.read"]);
|
|
95
|
+
await editKey(mockKeyId);
|
|
96
|
+
|
|
97
|
+
expect(fetchAuthenticated).not.toHaveBeenCalledWith(
|
|
98
|
+
routes.api.console.projects.apiKeys(mockProjectId),
|
|
99
|
+
HttpMethods.PUT,
|
|
100
|
+
expect.objectContaining({
|
|
101
|
+
apiKeyId: mockKeyId,
|
|
102
|
+
scopes: ["wallets.read", "wallets.create"],
|
|
103
|
+
})
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("should exit if no project is selected", async () => {
|
|
108
|
+
(getProjectId as any).mockResolvedValue(null);
|
|
109
|
+
|
|
110
|
+
await expect(editKey(mockKeyId)).rejects.toThrow();
|
|
111
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
112
|
+
"There is no project selected, please first run `crossmint project select`"
|
|
113
|
+
);
|
|
114
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("should handle API key not found error", async () => {
|
|
118
|
+
(fetchAuthenticated as any).mockResolvedValue([]);
|
|
119
|
+
await expect(editKey(mockKeyId)).rejects.toThrow();
|
|
120
|
+
expect(spinner.fail).toHaveBeenCalledWith("API key not found");
|
|
121
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("should handle update error", async () => {
|
|
125
|
+
(fetchAuthenticated as any).mockRejectedValueOnce(new Error("Update failed"));
|
|
126
|
+
await expect(editKey(mockKeyId)).rejects.toThrow();
|
|
127
|
+
expect(spinner.fail).toHaveBeenCalledWith(expect.stringContaining("❌ Failed to update API key"));
|
|
128
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("should update domains for client API key", async () => {
|
|
132
|
+
const clientKey: APIKeyDocument = {
|
|
133
|
+
...mockKey,
|
|
134
|
+
usageOrigin: "client",
|
|
135
|
+
whitelistedOrigins: ["https://old-domain.com"],
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
vi.mocked(checkbox).mockResolvedValue(["wallets.read"]);
|
|
139
|
+
(fetchAuthenticated as any).mockResolvedValue([clientKey]);
|
|
140
|
+
(getWhitelistedOriginsAndAppIdentifiersPrompt as any).mockResolvedValue({
|
|
141
|
+
whitelistedOrigins: ["https://new-domain.com"],
|
|
142
|
+
whitelistedAppIdentifiers: [],
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
await editKey(mockKeyId);
|
|
146
|
+
|
|
147
|
+
expect(getWhitelistedOriginsAndAppIdentifiersPrompt).toHaveBeenCalledWith("web", ["https://old-domain.com"]);
|
|
148
|
+
|
|
149
|
+
expect(fetchAuthenticated).toHaveBeenCalledWith(
|
|
150
|
+
routes.api.console.projects.apiKeys(mockProjectId),
|
|
151
|
+
HttpMethods.PUT,
|
|
152
|
+
expect.objectContaining({
|
|
153
|
+
apiKeyId: mockKeyId,
|
|
154
|
+
whitelistedOrigins: ["https://new-domain.com"],
|
|
155
|
+
whitelistedAppIdentifiers: [],
|
|
156
|
+
})
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { HttpMethods, routes } from "@crossmint/common-consts";
|
|
2
|
+
import { fetchAuthenticated } from "@/utils/fetch";
|
|
3
|
+
import { getEnvironment, getProjectId } from "@/utils/store";
|
|
4
|
+
import { APIKeyDocument, APIKeyScopes } from "@crossmint/products-console-types";
|
|
5
|
+
import { getScopeChoices } from "@/utils/scopes";
|
|
6
|
+
import { checkbox } from "@inquirer/prompts";
|
|
7
|
+
import { createSpinner } from "@/utils/spinner";
|
|
8
|
+
import { getProject } from "@/utils/getProject";
|
|
9
|
+
import { getWhitelistedOriginsAndAppIdentifiersPrompt } from "@/utils/getWhitelistedOriginsAndAppIdentifiersPrompt";
|
|
10
|
+
import { APIKeyPayload } from "@/types/keys";
|
|
11
|
+
|
|
12
|
+
export default async function editKey(keyId: string) {
|
|
13
|
+
const environment = await getEnvironment();
|
|
14
|
+
if (!environment) {
|
|
15
|
+
console.error("There is no environment selected, please first run `crossmint login`");
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
const projectId = await getProjectId();
|
|
19
|
+
if (!projectId) {
|
|
20
|
+
console.error("There is no project selected, please first run `crossmint project select`");
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const spinner = createSpinner("Fetching API key...");
|
|
25
|
+
try {
|
|
26
|
+
spinner.start();
|
|
27
|
+
|
|
28
|
+
const keys: APIKeyDocument[] = await fetchAuthenticated(routes.api.console.projects.apiKeys(projectId));
|
|
29
|
+
const key = keys.find((key) => key._id.toString() === keyId);
|
|
30
|
+
if (!key) {
|
|
31
|
+
spinner.fail("API key not found");
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
let whitelistedOrigins: string[] | undefined;
|
|
35
|
+
let whitelistedAppIdentifiers: string[] | undefined;
|
|
36
|
+
if (key.usageOrigin === "client") {
|
|
37
|
+
spinner.succeed("📌 Current API Key Whitelisted Origins:");
|
|
38
|
+
({ whitelistedOrigins, whitelistedAppIdentifiers } = await getWhitelistedOriginsAndAppIdentifiersPrompt(
|
|
39
|
+
key.whitelistedOrigins?.length > 0 ? "web" : "mobile",
|
|
40
|
+
key.whitelistedOrigins?.length > 0 ? key.whitelistedOrigins : key.whitelistedAppIdentifiers
|
|
41
|
+
));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
spinner.succeed("📌 Current API Key Scopes:");
|
|
45
|
+
const activeProject = await getProject(projectId);
|
|
46
|
+
if (!activeProject) {
|
|
47
|
+
console.error("❌ Project not found, please run `crossmint project select`");
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
const choices = getScopeChoices(key.usageOrigin, environment, activeProject, key.scopes);
|
|
51
|
+
|
|
52
|
+
const selectedScopes = await checkbox({
|
|
53
|
+
message: `Select scopes for ${key.usageOrigin} key:`,
|
|
54
|
+
choices,
|
|
55
|
+
validate: (input) => {
|
|
56
|
+
if (input.length === 0) {
|
|
57
|
+
return "At least one scope must be selected";
|
|
58
|
+
}
|
|
59
|
+
return true;
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
if (scopesChanged(key, selectedScopes) || originsChanged(key, whitelistedOrigins, whitelistedAppIdentifiers)) {
|
|
63
|
+
spinner.start("Updating API key scopes...");
|
|
64
|
+
const payload: APIKeyPayload = {
|
|
65
|
+
apiKeyId: keyId,
|
|
66
|
+
...key,
|
|
67
|
+
scopes: selectedScopes,
|
|
68
|
+
};
|
|
69
|
+
if (key.usageOrigin === "client") {
|
|
70
|
+
payload.whitelistedOrigins = whitelistedOrigins;
|
|
71
|
+
payload.whitelistedAppIdentifiers = whitelistedAppIdentifiers;
|
|
72
|
+
}
|
|
73
|
+
await fetchAuthenticated(routes.api.console.projects.apiKeys(projectId), HttpMethods.PUT, payload);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
spinner.succeed("✅ API key scopes updated successfully!");
|
|
77
|
+
} catch (error) {
|
|
78
|
+
spinner.fail(`❌ Failed to update API key: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function scopesChanged(key: APIKeyDocument, selectedScopes: APIKeyScopes[]) {
|
|
84
|
+
return (
|
|
85
|
+
key.scopes.some((scope) => !selectedScopes.includes(scope)) ||
|
|
86
|
+
selectedScopes.some((scope) => !key.scopes.includes(scope))
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function originsChanged(key: APIKeyDocument, selectedOrigins?: string[], selectedAppIdentifiers?: string[]) {
|
|
91
|
+
return (
|
|
92
|
+
key.usageOrigin === "client" &&
|
|
93
|
+
(key.whitelistedOrigins?.some((origin) => !selectedOrigins?.includes(origin)) ||
|
|
94
|
+
selectedOrigins?.some((origin) => !key.whitelistedOrigins?.includes(origin)) ||
|
|
95
|
+
key.whitelistedAppIdentifiers?.some((appIdentifier) => !selectedAppIdentifiers?.includes(appIdentifier)) ||
|
|
96
|
+
selectedAppIdentifiers?.some((appIdentifier) => !key.whitelistedAppIdentifiers?.includes(appIdentifier)))
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, Mock } from "vitest";
|
|
2
|
+
import listKeys from "./list";
|
|
3
|
+
import { fetchAuthenticated } from "@/utils/fetch";
|
|
4
|
+
import { getEnvironment, getProjectId } from "@/utils/store";
|
|
5
|
+
import { Pager } from "@/utils/Pager";
|
|
6
|
+
import { createSpinner } from "@/utils/spinner";
|
|
7
|
+
|
|
8
|
+
// Mock dependencies
|
|
9
|
+
vi.mock("@/utils/fetch");
|
|
10
|
+
vi.mock("@/utils/store");
|
|
11
|
+
vi.mock("@/utils/Pager");
|
|
12
|
+
vi.mock("@/utils/spinner");
|
|
13
|
+
|
|
14
|
+
describe("listKeys", () => {
|
|
15
|
+
const mockProjectId = "test-project-id";
|
|
16
|
+
const mockEnvironment = "development";
|
|
17
|
+
const mockKeys = [
|
|
18
|
+
{
|
|
19
|
+
_id: "key1",
|
|
20
|
+
clientSecret: "secret1",
|
|
21
|
+
usageOrigin: "server",
|
|
22
|
+
createdBy: "user1",
|
|
23
|
+
createdAt: "2024-01-01T00:00:00Z",
|
|
24
|
+
scopes: ["read", "write"],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
_id: "key2",
|
|
28
|
+
clientSecret: "secret2",
|
|
29
|
+
usageOrigin: "client",
|
|
30
|
+
createdBy: "user2",
|
|
31
|
+
createdAt: "2024-01-02T00:00:00Z",
|
|
32
|
+
scopes: ["read"],
|
|
33
|
+
whitelistedOrigins: ["https://example.com"],
|
|
34
|
+
whitelistedAppIdentifiers: ["app1"],
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
let addSpy: Mock;
|
|
38
|
+
let writeSpy: Mock;
|
|
39
|
+
let succeedSpy: Mock;
|
|
40
|
+
let failSpy: Mock;
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
vi.clearAllMocks();
|
|
43
|
+
(getProjectId as any).mockResolvedValue(mockProjectId);
|
|
44
|
+
(getEnvironment as any).mockResolvedValue(mockEnvironment);
|
|
45
|
+
(fetchAuthenticated as any).mockResolvedValue(mockKeys);
|
|
46
|
+
addSpy = vi.fn();
|
|
47
|
+
writeSpy = vi.fn();
|
|
48
|
+
succeedSpy = vi.fn();
|
|
49
|
+
failSpy = vi.fn();
|
|
50
|
+
(Pager as any).mockImplementation(() => ({
|
|
51
|
+
add: addSpy,
|
|
52
|
+
write: writeSpy,
|
|
53
|
+
}));
|
|
54
|
+
(createSpinner as any).mockImplementation(() => ({
|
|
55
|
+
start: vi.fn(),
|
|
56
|
+
succeed: succeedSpy,
|
|
57
|
+
fail: failSpy,
|
|
58
|
+
}));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should exit if no project is selected", async () => {
|
|
62
|
+
vi.spyOn(console, "error");
|
|
63
|
+
(getProjectId as any).mockResolvedValue(null);
|
|
64
|
+
|
|
65
|
+
await expect(listKeys()).rejects.toThrow();
|
|
66
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("should exit if invalid key type is provided", async () => {
|
|
70
|
+
vi.spyOn(console, "error");
|
|
71
|
+
await expect(listKeys("invalid" as any)).rejects.toThrow();
|
|
72
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("should display message when no keys are found", async () => {
|
|
76
|
+
(fetchAuthenticated as any).mockResolvedValue([]);
|
|
77
|
+
|
|
78
|
+
await listKeys();
|
|
79
|
+
expect(succeedSpy).toHaveBeenCalledWith(
|
|
80
|
+
"🔍 No API keys found for this project. To create a new API key, run `crossmint keys create`"
|
|
81
|
+
);
|
|
82
|
+
expect(process.exit).toHaveBeenCalledWith(0);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("should filter server keys when specified", async () => {
|
|
86
|
+
await listKeys("server");
|
|
87
|
+
|
|
88
|
+
expect(fetchAuthenticated).toHaveBeenCalled();
|
|
89
|
+
expect(addSpy).not.toHaveBeenCalledWith(expect.stringContaining("Client"));
|
|
90
|
+
expect(addSpy).toHaveBeenCalledWith(expect.stringContaining("Server"));
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should filter client keys when specified", async () => {
|
|
94
|
+
await listKeys("client");
|
|
95
|
+
|
|
96
|
+
expect(fetchAuthenticated).toHaveBeenCalled();
|
|
97
|
+
expect(addSpy).toHaveBeenCalledWith(expect.stringContaining("Client"));
|
|
98
|
+
expect(addSpy).not.toHaveBeenCalledWith(expect.stringContaining("Server"));
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should handle errors gracefully", async () => {
|
|
102
|
+
const mockError = new Error("Test error");
|
|
103
|
+
(fetchAuthenticated as any).mockRejectedValue(mockError);
|
|
104
|
+
|
|
105
|
+
await expect(listKeys()).rejects.toThrow();
|
|
106
|
+
expect(failSpy).toHaveBeenCalledWith(`❌ Failed to fetch keys: ${mockError.message}`);
|
|
107
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should format server key secret correctly in production", async () => {
|
|
111
|
+
(getEnvironment as any).mockResolvedValue("production");
|
|
112
|
+
await listKeys("server");
|
|
113
|
+
|
|
114
|
+
expect(addSpy).toHaveBeenCalledWith(expect.stringContaining("******************"));
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("should show full key secret in development", async () => {
|
|
118
|
+
(getEnvironment as any).mockResolvedValue("development");
|
|
119
|
+
await listKeys("server");
|
|
120
|
+
|
|
121
|
+
expect(addSpy).toHaveBeenCalledWith(expect.stringContaining("secret1"));
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { routes } from "@crossmint/common-consts";
|
|
2
|
+
import { fetchAuthenticated } from "@/utils/fetch";
|
|
3
|
+
import { getEnvironment, getProjectId } from "@/utils/store";
|
|
4
|
+
import { APIKeyUsageOrigin, APIKeyUsageOriginArray, APIKeyDocument } from "@crossmint/products-console-types";
|
|
5
|
+
import { capitalizeFirstLetter } from "@crossmint/common-string-utils";
|
|
6
|
+
import { Pager } from "@/utils/Pager";
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
import { createSpinner } from "@/utils/spinner";
|
|
9
|
+
|
|
10
|
+
export default async function listKeys(keyType?: APIKeyUsageOrigin) {
|
|
11
|
+
const environment = await getEnvironment();
|
|
12
|
+
const projectId = await getProjectId();
|
|
13
|
+
if (!projectId) {
|
|
14
|
+
console.error("There is no project selected, please first run `crossmint project select`");
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
if (keyType && !APIKeyUsageOriginArray.includes(keyType)) {
|
|
18
|
+
console.error("Invalid key type. Please use 'server' or 'client'.");
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const spinner = createSpinner("Fetching API keys...");
|
|
23
|
+
try {
|
|
24
|
+
spinner.start();
|
|
25
|
+
|
|
26
|
+
const keys: APIKeyDocument[] = await fetchAuthenticated(routes.api.console.projects.apiKeys(projectId));
|
|
27
|
+
|
|
28
|
+
const filteredKeys = keyType ? keys.filter((key) => key.usageOrigin === keyType) : keys;
|
|
29
|
+
|
|
30
|
+
if (filteredKeys.length === 0) {
|
|
31
|
+
spinner.succeed(
|
|
32
|
+
"🔍 No API keys found for this project. To create a new API key, run `crossmint keys create`"
|
|
33
|
+
);
|
|
34
|
+
return process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
spinner.succeed("📌 List of API Keys:");
|
|
38
|
+
|
|
39
|
+
const pager = new Pager();
|
|
40
|
+
|
|
41
|
+
const separator = chalk.bold.magenta("=========");
|
|
42
|
+
|
|
43
|
+
filteredKeys.forEach((key, index) => {
|
|
44
|
+
pager.add(`\n${separator}\n\n`);
|
|
45
|
+
pager.add(`${chalk.bold(`${index + 1}. Key ID:`)} ${key._id}\n`);
|
|
46
|
+
|
|
47
|
+
const keySecret =
|
|
48
|
+
key.usageOrigin === "server" && environment === "production"
|
|
49
|
+
? `******************${key.clientSecret.slice(-4)}`
|
|
50
|
+
: key.clientSecret;
|
|
51
|
+
|
|
52
|
+
pager.add(` ${chalk.bold(`Key Secret:`)} ${keySecret}\n\n`);
|
|
53
|
+
|
|
54
|
+
const details: Array<[string, string | undefined]> = [
|
|
55
|
+
["Type", capitalizeFirstLetter(key.usageOrigin)],
|
|
56
|
+
["Created By", key.createdBy],
|
|
57
|
+
["Created", key.createdAt ? new Date(key.createdAt).toLocaleString() : undefined],
|
|
58
|
+
["Scopes", key.scopes.join(", ")],
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
if (key.usageOrigin === "client") {
|
|
62
|
+
if (key.whitelistedOrigins.length > 0) {
|
|
63
|
+
details.push(["Whitelisted Origins", key.whitelistedOrigins.join(", ")]);
|
|
64
|
+
}
|
|
65
|
+
if (key.whitelistedAppIdentifiers && key.whitelistedAppIdentifiers.length > 0) {
|
|
66
|
+
details.push(["Whitelisted App IDs", key.whitelistedAppIdentifiers.join(", ")]);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
details.forEach(([label, value]) => {
|
|
71
|
+
if (value !== undefined) {
|
|
72
|
+
pager.add(`${chalk.cyan(label.padEnd(20))} ${value}\n`);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
pager.add(`\n${separator}`);
|
|
77
|
+
|
|
78
|
+
pager.write();
|
|
79
|
+
} catch (error) {
|
|
80
|
+
spinner.fail(`❌ Failed to fetch keys: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { select } from "@inquirer/prompts";
|
|
3
|
+
import { getCrossmintUrl } from "@/utils/urls";
|
|
4
|
+
import { setEnvironment } from "@/utils/store";
|
|
5
|
+
import login from "./login";
|
|
6
|
+
import selectProject from "./projects/select";
|
|
7
|
+
import open from "open";
|
|
8
|
+
import { generateCodeChallenge } from "@/utils/oauth/codeChallenge";
|
|
9
|
+
import server from "@/utils/oauth/server";
|
|
10
|
+
import { ChildProcess } from "child_process";
|
|
11
|
+
|
|
12
|
+
vi.mock("@inquirer/prompts");
|
|
13
|
+
vi.mock("@/utils/urls");
|
|
14
|
+
vi.mock("@/utils/store");
|
|
15
|
+
vi.mock("@/utils/oauth/codeChallenge");
|
|
16
|
+
vi.mock("@/utils/oauth/server");
|
|
17
|
+
vi.mock("open");
|
|
18
|
+
vi.mock("./projects/select");
|
|
19
|
+
|
|
20
|
+
describe("login", () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
vi.resetAllMocks();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
vi.resetAllMocks();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should successfully log in a user using OAuth", async () => {
|
|
30
|
+
const mockEnvironment = "production";
|
|
31
|
+
const mockUrl = "https://api.crossmint.com";
|
|
32
|
+
const mockCodeChallenge = "mock-code-challenge";
|
|
33
|
+
const mockChildProcess = { pid: 123 } as ChildProcess;
|
|
34
|
+
|
|
35
|
+
vi.mocked(select).mockResolvedValueOnce(mockEnvironment);
|
|
36
|
+
vi.mocked(getCrossmintUrl).mockReturnValue(mockUrl);
|
|
37
|
+
vi.mocked(generateCodeChallenge).mockResolvedValue(mockCodeChallenge);
|
|
38
|
+
vi.mocked(server).mockResolvedValue(undefined);
|
|
39
|
+
vi.mocked(open).mockResolvedValue(mockChildProcess);
|
|
40
|
+
|
|
41
|
+
const consoleLogSpy = vi.spyOn(console, "log");
|
|
42
|
+
await login();
|
|
43
|
+
|
|
44
|
+
expect(getCrossmintUrl).toHaveBeenCalledWith(mockEnvironment);
|
|
45
|
+
expect(setEnvironment).toHaveBeenCalledWith(mockEnvironment);
|
|
46
|
+
expect(generateCodeChallenge).toHaveBeenCalled();
|
|
47
|
+
expect(server).toHaveBeenCalledWith(mockEnvironment);
|
|
48
|
+
expect(open).toHaveBeenCalledWith(expect.stringContaining(mockUrl));
|
|
49
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining("Opening browser for authentication"));
|
|
50
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining("Login successful"));
|
|
51
|
+
expect(selectProject).toHaveBeenCalled();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("should exit if no URL is found for the environment", async () => {
|
|
55
|
+
vi.mocked(select).mockResolvedValueOnce("not-valid");
|
|
56
|
+
vi.mocked(getCrossmintUrl).mockReturnValue("");
|
|
57
|
+
|
|
58
|
+
const consoleErrorSpy = vi.spyOn(console, "error");
|
|
59
|
+
const processExitSpy = vi.spyOn(process, "exit").mockImplementation(() => undefined as never);
|
|
60
|
+
|
|
61
|
+
await login();
|
|
62
|
+
|
|
63
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("No URL found"));
|
|
64
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("should handle server error", async () => {
|
|
68
|
+
const mockEnvironment = "production";
|
|
69
|
+
const mockUrl = "https://api.crossmint.com";
|
|
70
|
+
const mockCodeChallenge = "mock-code-challenge";
|
|
71
|
+
const mockChildProcess = { pid: 123 } as ChildProcess;
|
|
72
|
+
|
|
73
|
+
vi.mocked(select).mockResolvedValueOnce(mockEnvironment);
|
|
74
|
+
vi.mocked(getCrossmintUrl).mockReturnValue(mockUrl);
|
|
75
|
+
vi.mocked(generateCodeChallenge).mockResolvedValue(mockCodeChallenge);
|
|
76
|
+
vi.mocked(server).mockRejectedValue(new Error("Server error"));
|
|
77
|
+
vi.mocked(open).mockResolvedValue(mockChildProcess);
|
|
78
|
+
|
|
79
|
+
const consoleErrorSpy = vi.spyOn(console, "error");
|
|
80
|
+
const processExitSpy = vi.spyOn(process, "exit").mockImplementation(() => undefined as never);
|
|
81
|
+
|
|
82
|
+
await login();
|
|
83
|
+
|
|
84
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("Login failed"));
|
|
85
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
86
|
+
});
|
|
87
|
+
});
|