@nexvora/mcp-server 0.3.1 → 0.3.3
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 +15 -13
- package/dist/NexvoraClient.d.ts.map +1 -1
- package/dist/NexvoraClient.js +21 -3
- package/dist/NexvoraClient.js.map +1 -1
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +26 -20
- package/dist/cli.js.map +1 -1
- package/dist/createServer.d.ts +7 -0
- package/dist/createServer.d.ts.map +1 -1
- package/dist/createServer.js +3 -3
- package/dist/createServer.js.map +1 -1
- package/package.json +6 -2
- package/CHANGELOG.md +0 -208
- package/docs/setup/chatgpt-desktop.md +0 -120
- package/docs/setup/claude-code.md +0 -152
- package/docs/setup/cursor.md +0 -129
- package/src/NexvoraClient.ts +0 -328
- package/src/RateLimiter.ts +0 -74
- package/src/__tests__/NexvoraClient.test.ts +0 -424
- package/src/__tests__/RateLimiter.test.ts +0 -151
- package/src/__tests__/auth/oauth.test.ts +0 -246
- package/src/__tests__/cache.test.ts +0 -64
- package/src/__tests__/config.test.ts +0 -98
- package/src/__tests__/defineTool.test.ts +0 -223
- package/src/__tests__/fixtures/config.json +0 -7
- package/src/__tests__/integration/agentstack.integration.test.ts +0 -259
- package/src/__tests__/integration/auth_refresh.integration.test.ts +0 -227
- package/src/__tests__/integration/consulting.integration.test.ts +0 -213
- package/src/__tests__/integration/feed.integration.test.ts +0 -200
- package/src/__tests__/integration/helpers.ts +0 -118
- package/src/__tests__/integration/knowledge.integration.test.ts +0 -194
- package/src/__tests__/integration/rate_limiting.integration.test.ts +0 -207
- package/src/__tests__/integration/submit_task.integration.test.ts +0 -120
- package/src/__tests__/integration/wallet_observatory.integration.test.ts +0 -240
- package/src/__tests__/nexvora_agentstack_answer.test.ts +0 -120
- package/src/__tests__/nexvora_agentstack_ask.test.ts +0 -140
- package/src/__tests__/nexvora_agentstack_search.test.ts +0 -188
- package/src/__tests__/nexvora_consulting_book.test.ts +0 -277
- package/src/__tests__/nexvora_consulting_search.test.ts +0 -153
- package/src/__tests__/nexvora_feed_post.test.ts +0 -147
- package/src/__tests__/nexvora_feed_react.test.ts +0 -98
- package/src/__tests__/nexvora_knowledge_search.test.ts +0 -148
- package/src/__tests__/nexvora_knowledge_subscribe.test.ts +0 -173
- package/src/__tests__/nexvora_observatory.test.ts +0 -125
- package/src/__tests__/nexvora_wallet_balance.test.ts +0 -165
- package/src/auth/oauth.ts +0 -247
- package/src/cache.ts +0 -34
- package/src/cli.ts +0 -171
- package/src/config.ts +0 -70
- package/src/createServer.ts +0 -90
- package/src/defineTool.ts +0 -120
- package/src/index.ts +0 -36
- package/src/server/sse.ts +0 -149
- package/src/tools/nexvora_agentstack_answer.ts +0 -62
- package/src/tools/nexvora_agentstack_ask.ts +0 -70
- package/src/tools/nexvora_agentstack_search.ts +0 -82
- package/src/tools/nexvora_consulting_book.ts +0 -130
- package/src/tools/nexvora_consulting_search.ts +0 -85
- package/src/tools/nexvora_feed_post.ts +0 -69
- package/src/tools/nexvora_feed_react.ts +0 -48
- package/src/tools/nexvora_knowledge_search.ts +0 -81
- package/src/tools/nexvora_knowledge_subscribe.ts +0 -90
- package/src/tools/nexvora_observatory.ts +0 -87
- package/src/tools/nexvora_submit_task.ts +0 -42
- package/src/tools/nexvora_wallet_balance.ts +0 -112
- package/tsconfig.json +0 -19
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
import { jest } from "@jest/globals";
|
|
2
|
-
|
|
3
|
-
import { NexvoraApiError, NexvoraClient } from "../NexvoraClient.js";
|
|
4
|
-
import { nexvora_knowledge_subscribe } from "../tools/nexvora_knowledge_subscribe.js";
|
|
5
|
-
|
|
6
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
7
|
-
function makeClient(): any {
|
|
8
|
-
const client = new NexvoraClient({
|
|
9
|
-
baseUrl: "https://api.nxvora.online",
|
|
10
|
-
accessToken: "token",
|
|
11
|
-
});
|
|
12
|
-
(client as any).get = jest.fn();
|
|
13
|
-
(client as any).post = jest.fn();
|
|
14
|
-
(client as any).sendAudit = jest.fn().mockResolvedValue(undefined);
|
|
15
|
-
return client;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const KB_ID = "aaaaaaaa-0000-0000-0000-000000000001";
|
|
19
|
-
|
|
20
|
-
const KB = {
|
|
21
|
-
id: KB_ID,
|
|
22
|
-
agentName: "DataBot",
|
|
23
|
-
title: "ML Engineering Handbook",
|
|
24
|
-
monthlyPriceCoins: 100,
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const SUB_RESPONSE = {
|
|
28
|
-
id: "cccccccc-0000-0000-0000-000000000001",
|
|
29
|
-
knowledgeId: KB_ID,
|
|
30
|
-
status: "ACTIVE",
|
|
31
|
-
nextBillingAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
describe("nexvora_knowledge_subscribe tool", () => {
|
|
35
|
-
beforeEach(() => {
|
|
36
|
-
jest.clearAllMocks();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("defaults confirm to false", () => {
|
|
40
|
-
const parsed = nexvora_knowledge_subscribe.inputSchema.parse({ knowledgeId: KB_ID });
|
|
41
|
-
expect(parsed.confirm).toBe(false);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
describe("preview mode (confirm=false)", () => {
|
|
45
|
-
it("returns subscription preview without posting", async () => {
|
|
46
|
-
const client = makeClient();
|
|
47
|
-
client.get.mockResolvedValueOnce(KB);
|
|
48
|
-
|
|
49
|
-
const result = await nexvora_knowledge_subscribe.handler(
|
|
50
|
-
{ knowledgeId: KB_ID, confirm: false },
|
|
51
|
-
client,
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
expect(result).toContain("## Subscription Preview");
|
|
55
|
-
expect(result).toContain("ML Engineering Handbook");
|
|
56
|
-
expect(result).toContain("DataBot");
|
|
57
|
-
expect(result).toContain("100");
|
|
58
|
-
expect(result).toContain("renews automatically");
|
|
59
|
-
expect(result).toContain("confirm: true");
|
|
60
|
-
expect(client.post).not.toHaveBeenCalled();
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("shows next bill date as today + 30 days", async () => {
|
|
64
|
-
const client = makeClient();
|
|
65
|
-
client.get.mockResolvedValueOnce(KB);
|
|
66
|
-
|
|
67
|
-
const result = await nexvora_knowledge_subscribe.handler(
|
|
68
|
-
{ knowledgeId: KB_ID, confirm: false },
|
|
69
|
-
client,
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
const expectedDate = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
|
|
73
|
-
.toISOString()
|
|
74
|
-
.slice(0, 10);
|
|
75
|
-
expect(result).toContain(expectedDate);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("calls GET /knowledge/{id} to fetch KB details", async () => {
|
|
79
|
-
const client = makeClient();
|
|
80
|
-
client.get.mockResolvedValueOnce(KB);
|
|
81
|
-
|
|
82
|
-
await nexvora_knowledge_subscribe.handler({ knowledgeId: KB_ID, confirm: false }, client);
|
|
83
|
-
|
|
84
|
-
expect(client.get).toHaveBeenCalledWith(`/knowledge/${KB_ID}`);
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
describe("confirm mode (confirm=true)", () => {
|
|
89
|
-
it("submits subscription and returns confirmation", async () => {
|
|
90
|
-
const client = makeClient();
|
|
91
|
-
client.get.mockResolvedValueOnce(KB);
|
|
92
|
-
client.post.mockResolvedValueOnce(SUB_RESPONSE);
|
|
93
|
-
|
|
94
|
-
const result = await nexvora_knowledge_subscribe.handler(
|
|
95
|
-
{ knowledgeId: KB_ID, confirm: true },
|
|
96
|
-
client,
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
expect(result).toContain("## Subscription Confirmed");
|
|
100
|
-
expect(result).toContain(SUB_RESPONSE.id);
|
|
101
|
-
expect(result).toContain("ACTIVE");
|
|
102
|
-
expect(result).toContain("100");
|
|
103
|
-
expect(result).toContain("nexvora_knowledge_unsubscribe");
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it("posts to /knowledge/{id}/subscribe with empty body", async () => {
|
|
107
|
-
const client = makeClient();
|
|
108
|
-
client.get.mockResolvedValueOnce(KB);
|
|
109
|
-
client.post.mockResolvedValueOnce(SUB_RESPONSE);
|
|
110
|
-
|
|
111
|
-
await nexvora_knowledge_subscribe.handler({ knowledgeId: KB_ID, confirm: true }, client);
|
|
112
|
-
|
|
113
|
-
expect(client.post).toHaveBeenCalledWith(`/knowledge/${KB_ID}/subscribe`, {});
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
describe("error handling", () => {
|
|
118
|
-
it("returns 404 message when knowledge base not found", async () => {
|
|
119
|
-
const client = makeClient();
|
|
120
|
-
client.get.mockRejectedValueOnce(
|
|
121
|
-
new NexvoraApiError(404, "Not Found", `/knowledge/${KB_ID}`),
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
const result = await nexvora_knowledge_subscribe.handler(
|
|
125
|
-
{ knowledgeId: KB_ID, confirm: false },
|
|
126
|
-
client,
|
|
127
|
-
);
|
|
128
|
-
|
|
129
|
-
expect(result).toContain("not found");
|
|
130
|
-
expect(result).toContain(KB_ID);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("returns 409 message when already subscribed", async () => {
|
|
134
|
-
const client = makeClient();
|
|
135
|
-
client.get.mockResolvedValueOnce(KB);
|
|
136
|
-
client.post.mockRejectedValueOnce(
|
|
137
|
-
new NexvoraApiError(409, "Conflict", `/knowledge/${KB_ID}/subscribe`),
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
const result = await nexvora_knowledge_subscribe.handler(
|
|
141
|
-
{ knowledgeId: KB_ID, confirm: true },
|
|
142
|
-
client,
|
|
143
|
-
);
|
|
144
|
-
|
|
145
|
-
expect(result).toContain("already subscribed");
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it("returns auth error message on 401", async () => {
|
|
149
|
-
const client = makeClient();
|
|
150
|
-
client.get.mockRejectedValueOnce(
|
|
151
|
-
new NexvoraApiError(401, "Unauthorized", `/knowledge/${KB_ID}`),
|
|
152
|
-
);
|
|
153
|
-
|
|
154
|
-
const result = await nexvora_knowledge_subscribe.handler(
|
|
155
|
-
{ knowledgeId: KB_ID, confirm: false },
|
|
156
|
-
client,
|
|
157
|
-
);
|
|
158
|
-
|
|
159
|
-
expect(result).toContain("nexvora login");
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it("re-throws non-handled errors", async () => {
|
|
163
|
-
const client = makeClient();
|
|
164
|
-
client.get.mockRejectedValueOnce(
|
|
165
|
-
new NexvoraApiError(500, "Server Error", `/knowledge/${KB_ID}`),
|
|
166
|
-
);
|
|
167
|
-
|
|
168
|
-
await expect(
|
|
169
|
-
nexvora_knowledge_subscribe.handler({ knowledgeId: KB_ID, confirm: false }, client),
|
|
170
|
-
).rejects.toBeInstanceOf(NexvoraApiError);
|
|
171
|
-
});
|
|
172
|
-
});
|
|
173
|
-
});
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import { jest } from "@jest/globals";
|
|
2
|
-
|
|
3
|
-
import { NexvoraApiError, NexvoraClient } from "../NexvoraClient.js";
|
|
4
|
-
import { nexvora_observatory } from "../tools/nexvora_observatory.js";
|
|
5
|
-
|
|
6
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
7
|
-
function makeClient(): any {
|
|
8
|
-
const client = new NexvoraClient({
|
|
9
|
-
baseUrl: "https://api.nxvora.online",
|
|
10
|
-
accessToken: "token",
|
|
11
|
-
});
|
|
12
|
-
(client as any).get = jest.fn();
|
|
13
|
-
(client as any).sendAudit = jest.fn().mockResolvedValue(undefined);
|
|
14
|
-
return client;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const SNAPSHOT = {
|
|
18
|
-
onlineAgentsCount: 42,
|
|
19
|
-
tasksCompletedToday: 1337,
|
|
20
|
-
tasksInQueue: 7,
|
|
21
|
-
totalCoinsInCirculation: 999999,
|
|
22
|
-
avgTaskLatencyMs: 2450.5,
|
|
23
|
-
topDonors: [
|
|
24
|
-
{
|
|
25
|
-
agentId: "agent-uuid-1",
|
|
26
|
-
agentName: "AlphaBot",
|
|
27
|
-
tasksCompleted: 200,
|
|
28
|
-
reputationScore: 4.9,
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
agentId: "agent-uuid-2",
|
|
32
|
-
agentName: "BetaBot",
|
|
33
|
-
tasksCompleted: 150,
|
|
34
|
-
reputationScore: 4.7,
|
|
35
|
-
},
|
|
36
|
-
],
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
describe("nexvora_observatory tool", () => {
|
|
40
|
-
// Advance 1.5x TTL (90s) per test so the module-level TtlCache expires between tests.
|
|
41
|
-
let epoch = 1_000_000_000_000;
|
|
42
|
-
|
|
43
|
-
beforeEach(() => {
|
|
44
|
-
jest.clearAllMocks();
|
|
45
|
-
jest.useFakeTimers({ now: epoch });
|
|
46
|
-
epoch += 90_000;
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
afterEach(() => {
|
|
50
|
-
jest.useRealTimers();
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("returns formatted markdown health snapshot", async () => {
|
|
54
|
-
const client = makeClient();
|
|
55
|
-
client.get.mockResolvedValueOnce(SNAPSHOT);
|
|
56
|
-
|
|
57
|
-
const result = await nexvora_observatory.handler({}, client);
|
|
58
|
-
|
|
59
|
-
expect(result).toContain("## NexVora Platform Health");
|
|
60
|
-
expect(result).toContain("42");
|
|
61
|
-
expect(result).toContain("1,337");
|
|
62
|
-
expect(result).toContain("7");
|
|
63
|
-
expect(result).toContain("999,999");
|
|
64
|
-
expect(result).toContain("2,451 ms");
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("shows top donor table with names and reputation", async () => {
|
|
68
|
-
const client = makeClient();
|
|
69
|
-
client.get.mockResolvedValueOnce(SNAPSHOT);
|
|
70
|
-
|
|
71
|
-
const result = await nexvora_observatory.handler({}, client);
|
|
72
|
-
|
|
73
|
-
expect(result).toContain("Top Donor Agents");
|
|
74
|
-
expect(result).toContain("AlphaBot");
|
|
75
|
-
expect(result).toContain("BetaBot");
|
|
76
|
-
expect(result).toContain("4.9");
|
|
77
|
-
expect(result).toContain("200");
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("shows no donor data message when topDonors is empty", async () => {
|
|
81
|
-
const client = makeClient();
|
|
82
|
-
client.get.mockResolvedValueOnce({ ...SNAPSHOT, topDonors: [] });
|
|
83
|
-
|
|
84
|
-
const result = await nexvora_observatory.handler({}, client);
|
|
85
|
-
|
|
86
|
-
expect(result).toContain("No donor agent data available yet");
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it("shows N/A for latency when avgTaskLatencyMs is 0", async () => {
|
|
90
|
-
const client = makeClient();
|
|
91
|
-
client.get.mockResolvedValueOnce({ ...SNAPSHOT, avgTaskLatencyMs: 0 });
|
|
92
|
-
|
|
93
|
-
const result = await nexvora_observatory.handler({}, client);
|
|
94
|
-
|
|
95
|
-
expect(result).toContain("N/A");
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("calls GET /observatory", async () => {
|
|
99
|
-
const client = makeClient();
|
|
100
|
-
client.get.mockResolvedValueOnce(SNAPSHOT);
|
|
101
|
-
|
|
102
|
-
await nexvora_observatory.handler({}, client);
|
|
103
|
-
|
|
104
|
-
expect(client.get).toHaveBeenCalledWith("/observatory");
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it("returns auth error message on 401", async () => {
|
|
108
|
-
const client = makeClient();
|
|
109
|
-
client.get.mockRejectedValueOnce(new NexvoraApiError(401, "Unauthorized", "/observatory"));
|
|
110
|
-
|
|
111
|
-
const result = await nexvora_observatory.handler({}, client);
|
|
112
|
-
|
|
113
|
-
expect(result).toContain("nexvora login");
|
|
114
|
-
expect(result).toContain("Not authenticated");
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it("re-throws non-auth errors", async () => {
|
|
118
|
-
const client = makeClient();
|
|
119
|
-
client.get.mockRejectedValueOnce(new NexvoraApiError(500, "Server Error", "/observatory"));
|
|
120
|
-
|
|
121
|
-
await expect(nexvora_observatory.handler({}, client)).rejects.toBeInstanceOf(
|
|
122
|
-
NexvoraApiError,
|
|
123
|
-
);
|
|
124
|
-
});
|
|
125
|
-
});
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
import { jest } from "@jest/globals";
|
|
2
|
-
|
|
3
|
-
import { NexvoraApiError, NexvoraClient } from "../NexvoraClient.js";
|
|
4
|
-
import { nexvora_wallet_balance } from "../tools/nexvora_wallet_balance.js";
|
|
5
|
-
|
|
6
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
7
|
-
function makeClient(): any {
|
|
8
|
-
const client = new NexvoraClient({
|
|
9
|
-
baseUrl: "https://api.nxvora.online",
|
|
10
|
-
accessToken: "token",
|
|
11
|
-
});
|
|
12
|
-
(client as any).get = jest.fn();
|
|
13
|
-
(client as any).sendAudit = jest.fn().mockResolvedValue(undefined);
|
|
14
|
-
return client;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// Backend region-filters the /wallet response — non-IN users get only usd*
|
|
18
|
-
// fields, IN users get only inr*. Reflect that here so the test data matches
|
|
19
|
-
// real wire shape.
|
|
20
|
-
const WALLET_RESPONSE_USD = {
|
|
21
|
-
coinBalance: 1500,
|
|
22
|
-
usdEquivalent: "7.50",
|
|
23
|
-
coinUsdRate: "0.005",
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const WALLET_RESPONSE_INR = {
|
|
27
|
-
coinBalance: 1500,
|
|
28
|
-
inrEquivalent: "750.00",
|
|
29
|
-
coinInrRate: "0.50",
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const WALLET_RESPONSE = WALLET_RESPONSE_USD;
|
|
33
|
-
|
|
34
|
-
const LEDGER_RESPONSE = {
|
|
35
|
-
content: [
|
|
36
|
-
{
|
|
37
|
-
id: "uuid-1",
|
|
38
|
-
amount: 500,
|
|
39
|
-
txType: "SIGNUP_BONUS",
|
|
40
|
-
note: "Welcome bonus",
|
|
41
|
-
createdAt: "2026-05-01T10:00:00Z",
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
id: "uuid-2",
|
|
45
|
-
amount: -10,
|
|
46
|
-
txType: "TASK_FEE",
|
|
47
|
-
note: "Task: Summarise PDF",
|
|
48
|
-
createdAt: "2026-05-02T12:00:00Z",
|
|
49
|
-
},
|
|
50
|
-
],
|
|
51
|
-
totalElements: 2,
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
describe("nexvora_wallet_balance tool", () => {
|
|
55
|
-
// Advance 2x TTL (120s) per test so the module-level TtlCache expires between tests.
|
|
56
|
-
let epoch = 1_000_000_000_000;
|
|
57
|
-
|
|
58
|
-
beforeEach(() => {
|
|
59
|
-
jest.clearAllMocks();
|
|
60
|
-
jest.useFakeTimers({ now: epoch });
|
|
61
|
-
epoch += 120_000;
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
afterEach(() => {
|
|
65
|
-
jest.useRealTimers();
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("returns formatted markdown with balance and transactions (USD region)", async () => {
|
|
69
|
-
const client = makeClient();
|
|
70
|
-
client.get
|
|
71
|
-
.mockResolvedValueOnce(WALLET_RESPONSE_USD)
|
|
72
|
-
.mockResolvedValueOnce(LEDGER_RESPONSE);
|
|
73
|
-
|
|
74
|
-
const result = await nexvora_wallet_balance.handler({}, client);
|
|
75
|
-
|
|
76
|
-
expect(result).toContain("## NexVora Wallet");
|
|
77
|
-
expect(result).toContain("1,500 coins");
|
|
78
|
-
expect(result).toContain("$7.50");
|
|
79
|
-
expect(result).toContain("1 coin = $0.005");
|
|
80
|
-
// INR fields are absent in the response and must NOT leak into the output.
|
|
81
|
-
expect(result).not.toContain("INR");
|
|
82
|
-
expect(result).not.toContain("undefined");
|
|
83
|
-
expect(result).toContain("SIGNUP_BONUS");
|
|
84
|
-
expect(result).toContain("Welcome bonus");
|
|
85
|
-
expect(result).toContain("+500 coins");
|
|
86
|
-
expect(result).toContain("-10 coins");
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it("renders INR-only when the backend returns the India currency block", async () => {
|
|
90
|
-
const client = makeClient();
|
|
91
|
-
client.get
|
|
92
|
-
.mockResolvedValueOnce(WALLET_RESPONSE_INR)
|
|
93
|
-
.mockResolvedValueOnce(LEDGER_RESPONSE);
|
|
94
|
-
|
|
95
|
-
const result = await nexvora_wallet_balance.handler({}, client);
|
|
96
|
-
|
|
97
|
-
expect(result).toContain("₹750.00");
|
|
98
|
-
expect(result).toContain("1 coin = ₹0.50");
|
|
99
|
-
expect(result).not.toContain("USD");
|
|
100
|
-
expect(result).not.toContain("undefined");
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("calls /wallet and /wallet/ledger?size=10 in parallel", async () => {
|
|
104
|
-
const client = makeClient();
|
|
105
|
-
client.get
|
|
106
|
-
.mockResolvedValueOnce(WALLET_RESPONSE)
|
|
107
|
-
.mockResolvedValueOnce(LEDGER_RESPONSE);
|
|
108
|
-
|
|
109
|
-
await nexvora_wallet_balance.handler({}, client);
|
|
110
|
-
|
|
111
|
-
expect(client.get).toHaveBeenCalledWith("/wallet");
|
|
112
|
-
expect(client.get).toHaveBeenCalledWith("/wallet/ledger?size=10");
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("shows total count when more transactions exist", async () => {
|
|
116
|
-
const client = makeClient();
|
|
117
|
-
const manyLedger = { content: LEDGER_RESPONSE.content, totalElements: 50 };
|
|
118
|
-
client.get
|
|
119
|
-
.mockResolvedValueOnce(WALLET_RESPONSE)
|
|
120
|
-
.mockResolvedValueOnce(manyLedger);
|
|
121
|
-
|
|
122
|
-
const result = await nexvora_wallet_balance.handler({}, client);
|
|
123
|
-
|
|
124
|
-
expect(result).toContain("48 more");
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it("shows empty state when no transactions exist", async () => {
|
|
128
|
-
const client = makeClient();
|
|
129
|
-
client.get
|
|
130
|
-
.mockResolvedValueOnce(WALLET_RESPONSE)
|
|
131
|
-
.mockResolvedValueOnce({ content: [], totalElements: 0 });
|
|
132
|
-
|
|
133
|
-
const result = await nexvora_wallet_balance.handler({}, client);
|
|
134
|
-
|
|
135
|
-
expect(result).toContain("No transactions yet");
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it("returns auth error message on 401", async () => {
|
|
139
|
-
const client = makeClient();
|
|
140
|
-
client.get.mockRejectedValueOnce(new NexvoraApiError(401, "Unauthorized", "/wallet"));
|
|
141
|
-
|
|
142
|
-
const result = await nexvora_wallet_balance.handler({}, client);
|
|
143
|
-
|
|
144
|
-
expect(result).toContain("nexvora login");
|
|
145
|
-
expect(result).toContain("Not authenticated");
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it("returns auth error message on 403", async () => {
|
|
149
|
-
const client = makeClient();
|
|
150
|
-
client.get.mockRejectedValueOnce(new NexvoraApiError(403, "Forbidden", "/wallet"));
|
|
151
|
-
|
|
152
|
-
const result = await nexvora_wallet_balance.handler({}, client);
|
|
153
|
-
|
|
154
|
-
expect(result).toContain("nexvora login");
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it("re-throws non-auth errors", async () => {
|
|
158
|
-
const client = makeClient();
|
|
159
|
-
client.get.mockRejectedValueOnce(new NexvoraApiError(500, "Server Error", "/wallet"));
|
|
160
|
-
|
|
161
|
-
await expect(nexvora_wallet_balance.handler({}, client)).rejects.toBeInstanceOf(
|
|
162
|
-
NexvoraApiError,
|
|
163
|
-
);
|
|
164
|
-
});
|
|
165
|
-
});
|
package/src/auth/oauth.ts
DELETED
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OAuth 2.0 Device Authorization Grant (RFC 8628) for the NexVora MCP CLI.
|
|
3
|
-
*
|
|
4
|
-
* This is the same flow the Rust daemon uses (see
|
|
5
|
-
* nexvora-daemon/crates/daemon-main/src/auth.rs). It is the locked auth model
|
|
6
|
-
* for headless clients per the daemon-auth-model design.
|
|
7
|
-
*
|
|
8
|
-
* Flow:
|
|
9
|
-
* 1. POST /oauth/device/authorize → device_code + user_code +
|
|
10
|
-
* verification_uri_complete
|
|
11
|
-
* 2. Print the user_code in the terminal and open
|
|
12
|
-
* verification_uri_complete in the user's browser
|
|
13
|
-
* 3. Poll POST /oauth/device/token at the negotiated interval, honouring
|
|
14
|
-
* authorization_pending / slow_down, until the user approves, denies,
|
|
15
|
-
* or the code expires
|
|
16
|
-
* 4. Fetch the authenticated user profile to obtain the userId
|
|
17
|
-
* 5. Return a Config-compatible object for the caller to persist
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import { exec } from "node:child_process";
|
|
21
|
-
import { hostname } from "node:os";
|
|
22
|
-
|
|
23
|
-
import type { Config } from "../config.js";
|
|
24
|
-
|
|
25
|
-
const CLIENT_ID = "nexvora-mcp-server";
|
|
26
|
-
const SCOPE = "mcp:tools offline_access";
|
|
27
|
-
const DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
28
|
-
|
|
29
|
-
/** Hard ceiling regardless of the server-advertised expires_in. */
|
|
30
|
-
const SAFETY_TIMEOUT_MS = 15 * 60 * 1000;
|
|
31
|
-
|
|
32
|
-
/** Minimum interval between polls in seconds (defensive floor). */
|
|
33
|
-
const MIN_POLL_INTERVAL_SECONDS = 1;
|
|
34
|
-
|
|
35
|
-
/** RFC 8628 §3.5: on slow_down, increase the polling interval by 5 seconds. */
|
|
36
|
-
const SLOW_DOWN_INCREMENT_MS = 5_000;
|
|
37
|
-
|
|
38
|
-
// ── Public API ─────────────────────────────────────────────────────────────────
|
|
39
|
-
|
|
40
|
-
export interface DeviceAuthorizationResponse {
|
|
41
|
-
device_code: string;
|
|
42
|
-
user_code: string;
|
|
43
|
-
verification_uri: string;
|
|
44
|
-
verification_uri_complete: string;
|
|
45
|
-
expires_in: number;
|
|
46
|
-
interval: number;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface DeviceTokenSuccess {
|
|
50
|
-
access_token: string;
|
|
51
|
-
refresh_token: string;
|
|
52
|
-
token_type: string;
|
|
53
|
-
expires_in: number;
|
|
54
|
-
scope?: string;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
interface OAuthErrorBody {
|
|
58
|
-
error: string;
|
|
59
|
-
error_description?: string;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Error thrown when the device-grant flow ends without issuing tokens
|
|
64
|
-
* (denial, expiry, timeout, server error). The {@code code} property is the
|
|
65
|
-
* RFC 8628 / 6749 error code or "timeout".
|
|
66
|
-
*/
|
|
67
|
-
export class DeviceGrantError extends Error {
|
|
68
|
-
constructor(public readonly code: string, message: string) {
|
|
69
|
-
super(message);
|
|
70
|
-
this.name = "DeviceGrantError";
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Run the full interactive Device Grant login flow.
|
|
76
|
-
*
|
|
77
|
-
* @param serverUrl Base URL of the NexVora API (e.g. https://api.nxvora.online)
|
|
78
|
-
* @returns A completed Config object ready to be written by ConfigManager
|
|
79
|
-
*/
|
|
80
|
-
export async function loginInteractive(serverUrl: string): Promise<Config> {
|
|
81
|
-
const base = serverUrl.replace(/\/$/, "");
|
|
82
|
-
|
|
83
|
-
// 1. Start the device authorization
|
|
84
|
-
const grant = await startDeviceAuthorization(base);
|
|
85
|
-
|
|
86
|
-
// 2. Print code + open browser
|
|
87
|
-
printUserPrompt(grant);
|
|
88
|
-
openBrowser(grant.verification_uri_complete);
|
|
89
|
-
|
|
90
|
-
// 3. Poll until decision
|
|
91
|
-
const tokens = await pollForToken(base, grant);
|
|
92
|
-
|
|
93
|
-
// 4. Fetch user profile (best-effort)
|
|
94
|
-
const userId = await fetchUserId(base, tokens.access_token);
|
|
95
|
-
|
|
96
|
-
const nowSecs = Math.floor(Date.now() / 1000);
|
|
97
|
-
return {
|
|
98
|
-
accessToken: tokens.access_token,
|
|
99
|
-
refreshToken: tokens.refresh_token,
|
|
100
|
-
expiresAt: nowSecs + tokens.expires_in,
|
|
101
|
-
serverUrl: base,
|
|
102
|
-
userId,
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// ── Internal helpers ───────────────────────────────────────────────────────────
|
|
107
|
-
|
|
108
|
-
export async function startDeviceAuthorization(
|
|
109
|
-
base: string,
|
|
110
|
-
): Promise<DeviceAuthorizationResponse> {
|
|
111
|
-
const url = `${base}/oauth/device/authorize`;
|
|
112
|
-
let response: Response;
|
|
113
|
-
try {
|
|
114
|
-
response = await fetch(url, {
|
|
115
|
-
method: "POST",
|
|
116
|
-
headers: { "Content-Type": "application/json" },
|
|
117
|
-
body: JSON.stringify({
|
|
118
|
-
client_id: CLIENT_ID,
|
|
119
|
-
scope: SCOPE,
|
|
120
|
-
device_name: hostname(),
|
|
121
|
-
}),
|
|
122
|
-
});
|
|
123
|
-
} catch (err) {
|
|
124
|
-
throw new Error(
|
|
125
|
-
`Could not reach ${url}: ${(err as Error).message}.\n` +
|
|
126
|
-
`Check NEXVORA_BASE_URL and your network connection.`,
|
|
127
|
-
);
|
|
128
|
-
}
|
|
129
|
-
if (!response.ok) {
|
|
130
|
-
const text = await response.text().catch(() => "");
|
|
131
|
-
throw new Error(
|
|
132
|
-
`Failed to start OAuth device authorization (${response.status}) at ${url}.\n` +
|
|
133
|
-
`Is ${base} a valid NexVora server?` +
|
|
134
|
-
(text ? `\nServer said: ${text}` : ""),
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
return response.json() as Promise<DeviceAuthorizationResponse>;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function printUserPrompt(grant: DeviceAuthorizationResponse): void {
|
|
141
|
-
console.error("");
|
|
142
|
-
console.error("To authenticate, visit:");
|
|
143
|
-
console.error(` ${grant.verification_uri}`);
|
|
144
|
-
console.error("");
|
|
145
|
-
console.error("And enter this code:");
|
|
146
|
-
console.error(` ${grant.user_code}`);
|
|
147
|
-
console.error("");
|
|
148
|
-
console.error("If your browser does not open automatically, paste this URL:");
|
|
149
|
-
console.error(` ${grant.verification_uri_complete}`);
|
|
150
|
-
console.error("");
|
|
151
|
-
console.error(
|
|
152
|
-
`Waiting for approval (code expires in ${grant.expires_in} seconds)...`,
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export async function pollForToken(
|
|
157
|
-
base: string,
|
|
158
|
-
grant: DeviceAuthorizationResponse,
|
|
159
|
-
): Promise<DeviceTokenSuccess> {
|
|
160
|
-
const url = `${base}/oauth/device/token`;
|
|
161
|
-
const deadline = Date.now() + Math.min(grant.expires_in * 1000, SAFETY_TIMEOUT_MS);
|
|
162
|
-
let intervalMs = Math.max(MIN_POLL_INTERVAL_SECONDS, grant.interval) * 1000;
|
|
163
|
-
|
|
164
|
-
while (Date.now() < deadline) {
|
|
165
|
-
await sleep(intervalMs);
|
|
166
|
-
|
|
167
|
-
const response = await fetch(url, {
|
|
168
|
-
method: "POST",
|
|
169
|
-
headers: { "Content-Type": "application/json" },
|
|
170
|
-
body: JSON.stringify({
|
|
171
|
-
grant_type: DEVICE_CODE_GRANT_TYPE,
|
|
172
|
-
device_code: grant.device_code,
|
|
173
|
-
client_id: CLIENT_ID,
|
|
174
|
-
}),
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
if (response.ok) {
|
|
178
|
-
return response.json() as Promise<DeviceTokenSuccess>;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const body = (await response.json().catch(() => null)) as OAuthErrorBody | null;
|
|
182
|
-
const code = body?.error ?? "unknown_error";
|
|
183
|
-
|
|
184
|
-
switch (code) {
|
|
185
|
-
case "authorization_pending":
|
|
186
|
-
// Keep polling at the current interval.
|
|
187
|
-
continue;
|
|
188
|
-
case "slow_down":
|
|
189
|
-
// RFC 8628 §3.5: server says we're polling too fast.
|
|
190
|
-
intervalMs += SLOW_DOWN_INCREMENT_MS;
|
|
191
|
-
continue;
|
|
192
|
-
case "access_denied":
|
|
193
|
-
throw new DeviceGrantError(code, "Login cancelled — you denied the request.");
|
|
194
|
-
case "expired_token":
|
|
195
|
-
throw new DeviceGrantError(
|
|
196
|
-
code,
|
|
197
|
-
"Login expired before it was approved. Run `nexvora login` again.",
|
|
198
|
-
);
|
|
199
|
-
case "invalid_grant":
|
|
200
|
-
throw new DeviceGrantError(
|
|
201
|
-
code,
|
|
202
|
-
body?.error_description ??
|
|
203
|
-
"The device grant is no longer valid. Run `nexvora login` again.",
|
|
204
|
-
);
|
|
205
|
-
default:
|
|
206
|
-
throw new DeviceGrantError(
|
|
207
|
-
code,
|
|
208
|
-
body?.error_description ?? `OAuth error: ${code}`,
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
throw new DeviceGrantError(
|
|
214
|
-
"timeout",
|
|
215
|
-
"Login timed out before the device code expired. Run `nexvora login` again.",
|
|
216
|
-
);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
async function fetchUserId(baseUrl: string, accessToken: string): Promise<string> {
|
|
220
|
-
const response = await fetch(`${baseUrl}/api/users/me`, {
|
|
221
|
-
headers: { Authorization: `Bearer ${accessToken}` },
|
|
222
|
-
});
|
|
223
|
-
if (!response.ok) {
|
|
224
|
-
return "";
|
|
225
|
-
}
|
|
226
|
-
const user = (await response.json()) as { id?: string; userId?: string };
|
|
227
|
-
return user.id ?? user.userId ?? "";
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function openBrowser(url: string): void {
|
|
231
|
-
const cmd =
|
|
232
|
-
process.platform === "win32"
|
|
233
|
-
? `start "" "${url}"`
|
|
234
|
-
: process.platform === "darwin"
|
|
235
|
-
? `open "${url}"`
|
|
236
|
-
: `xdg-open "${url}"`;
|
|
237
|
-
|
|
238
|
-
exec(cmd, (err) => {
|
|
239
|
-
if (err) {
|
|
240
|
-
console.error(`(Could not open browser automatically: ${err.message})`);
|
|
241
|
-
}
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function sleep(ms: number): Promise<void> {
|
|
246
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
247
|
-
}
|