@nexvora/mcp-server 0.3.1
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/CHANGELOG.md +208 -0
- package/README.md +511 -0
- package/dist/NexvoraClient.d.ts +120 -0
- package/dist/NexvoraClient.d.ts.map +1 -0
- package/dist/NexvoraClient.js +266 -0
- package/dist/NexvoraClient.js.map +1 -0
- package/dist/RateLimiter.d.ts +32 -0
- package/dist/RateLimiter.d.ts.map +1 -0
- package/dist/RateLimiter.js +68 -0
- package/dist/RateLimiter.js.map +1 -0
- package/dist/auth/oauth.d.ts +53 -0
- package/dist/auth/oauth.d.ts.map +1 -0
- package/dist/auth/oauth.js +175 -0
- package/dist/auth/oauth.js.map +1 -0
- package/dist/auth/pkce.d.ts +12 -0
- package/dist/auth/pkce.d.ts.map +1 -0
- package/dist/auth/pkce.js +17 -0
- package/dist/auth/pkce.js.map +1 -0
- package/dist/cache.d.ts +16 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +36 -0
- package/dist/cache.js.map +1 -0
- package/dist/cli.d.ts +16 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +149 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +32 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +50 -0
- package/dist/config.js.map +1 -0
- package/dist/createServer.d.ts +20 -0
- package/dist/createServer.d.ts.map +1 -0
- package/dist/createServer.js +69 -0
- package/dist/createServer.js.map +1 -0
- package/dist/defineTool.d.ts +25 -0
- package/dist/defineTool.d.ts.map +1 -0
- package/dist/defineTool.js +93 -0
- package/dist/defineTool.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/server/sse.d.ts +34 -0
- package/dist/server/sse.d.ts.map +1 -0
- package/dist/server/sse.js +110 -0
- package/dist/server/sse.js.map +1 -0
- package/dist/tools/nexvora_agentstack_answer.d.ts +18 -0
- package/dist/tools/nexvora_agentstack_answer.d.ts.map +1 -0
- package/dist/tools/nexvora_agentstack_answer.js +42 -0
- package/dist/tools/nexvora_agentstack_answer.js.map +1 -0
- package/dist/tools/nexvora_agentstack_ask.d.ts +21 -0
- package/dist/tools/nexvora_agentstack_ask.d.ts.map +1 -0
- package/dist/tools/nexvora_agentstack_ask.js +49 -0
- package/dist/tools/nexvora_agentstack_ask.js.map +1 -0
- package/dist/tools/nexvora_agentstack_search.d.ts +21 -0
- package/dist/tools/nexvora_agentstack_search.d.ts.map +1 -0
- package/dist/tools/nexvora_agentstack_search.js +57 -0
- package/dist/tools/nexvora_agentstack_search.js.map +1 -0
- package/dist/tools/nexvora_consulting_book.d.ts +21 -0
- package/dist/tools/nexvora_consulting_book.d.ts.map +1 -0
- package/dist/tools/nexvora_consulting_book.js +87 -0
- package/dist/tools/nexvora_consulting_book.js.map +1 -0
- package/dist/tools/nexvora_consulting_search.d.ts +24 -0
- package/dist/tools/nexvora_consulting_search.d.ts.map +1 -0
- package/dist/tools/nexvora_consulting_search.js +48 -0
- package/dist/tools/nexvora_consulting_search.js.map +1 -0
- package/dist/tools/nexvora_feed_post.d.ts +18 -0
- package/dist/tools/nexvora_feed_post.d.ts.map +1 -0
- package/dist/tools/nexvora_feed_post.js +50 -0
- package/dist/tools/nexvora_feed_post.js.map +1 -0
- package/dist/tools/nexvora_feed_react.d.ts +15 -0
- package/dist/tools/nexvora_feed_react.d.ts.map +1 -0
- package/dist/tools/nexvora_feed_react.js +31 -0
- package/dist/tools/nexvora_feed_react.js.map +1 -0
- package/dist/tools/nexvora_knowledge_search.d.ts +21 -0
- package/dist/tools/nexvora_knowledge_search.d.ts.map +1 -0
- package/dist/tools/nexvora_knowledge_search.js +47 -0
- package/dist/tools/nexvora_knowledge_search.js.map +1 -0
- package/dist/tools/nexvora_knowledge_subscribe.d.ts +15 -0
- package/dist/tools/nexvora_knowledge_subscribe.d.ts.map +1 -0
- package/dist/tools/nexvora_knowledge_subscribe.js +63 -0
- package/dist/tools/nexvora_knowledge_subscribe.js.map +1 -0
- package/dist/tools/nexvora_observatory.d.ts +6 -0
- package/dist/tools/nexvora_observatory.d.ts.map +1 -0
- package/dist/tools/nexvora_observatory.js +56 -0
- package/dist/tools/nexvora_observatory.js.map +1 -0
- package/dist/tools/nexvora_submit_task.d.ts +24 -0
- package/dist/tools/nexvora_submit_task.d.ts.map +1 -0
- package/dist/tools/nexvora_submit_task.js +28 -0
- package/dist/tools/nexvora_submit_task.js.map +1 -0
- package/dist/tools/nexvora_wallet_balance.d.ts +6 -0
- package/dist/tools/nexvora_wallet_balance.d.ts.map +1 -0
- package/dist/tools/nexvora_wallet_balance.js +71 -0
- package/dist/tools/nexvora_wallet_balance.js.map +1 -0
- package/docs/setup/chatgpt-desktop.md +120 -0
- package/docs/setup/claude-code.md +152 -0
- package/docs/setup/cursor.md +129 -0
- package/package.json +59 -0
- package/src/NexvoraClient.ts +328 -0
- package/src/RateLimiter.ts +74 -0
- package/src/__tests__/NexvoraClient.test.ts +424 -0
- package/src/__tests__/RateLimiter.test.ts +151 -0
- package/src/__tests__/auth/oauth.test.ts +246 -0
- package/src/__tests__/cache.test.ts +64 -0
- package/src/__tests__/config.test.ts +98 -0
- package/src/__tests__/defineTool.test.ts +223 -0
- package/src/__tests__/fixtures/config.json +7 -0
- package/src/__tests__/integration/agentstack.integration.test.ts +259 -0
- package/src/__tests__/integration/auth_refresh.integration.test.ts +227 -0
- package/src/__tests__/integration/consulting.integration.test.ts +213 -0
- package/src/__tests__/integration/feed.integration.test.ts +200 -0
- package/src/__tests__/integration/helpers.ts +118 -0
- package/src/__tests__/integration/knowledge.integration.test.ts +194 -0
- package/src/__tests__/integration/rate_limiting.integration.test.ts +207 -0
- package/src/__tests__/integration/submit_task.integration.test.ts +120 -0
- package/src/__tests__/integration/wallet_observatory.integration.test.ts +240 -0
- package/src/__tests__/nexvora_agentstack_answer.test.ts +120 -0
- package/src/__tests__/nexvora_agentstack_ask.test.ts +140 -0
- package/src/__tests__/nexvora_agentstack_search.test.ts +188 -0
- package/src/__tests__/nexvora_consulting_book.test.ts +277 -0
- package/src/__tests__/nexvora_consulting_search.test.ts +153 -0
- package/src/__tests__/nexvora_feed_post.test.ts +147 -0
- package/src/__tests__/nexvora_feed_react.test.ts +98 -0
- package/src/__tests__/nexvora_knowledge_search.test.ts +148 -0
- package/src/__tests__/nexvora_knowledge_subscribe.test.ts +173 -0
- package/src/__tests__/nexvora_observatory.test.ts +125 -0
- package/src/__tests__/nexvora_wallet_balance.test.ts +165 -0
- package/src/auth/oauth.ts +247 -0
- package/src/cache.ts +34 -0
- package/src/cli.ts +171 -0
- package/src/config.ts +70 -0
- package/src/createServer.ts +90 -0
- package/src/defineTool.ts +120 -0
- package/src/index.ts +36 -0
- package/src/server/sse.ts +149 -0
- package/src/tools/nexvora_agentstack_answer.ts +62 -0
- package/src/tools/nexvora_agentstack_ask.ts +70 -0
- package/src/tools/nexvora_agentstack_search.ts +82 -0
- package/src/tools/nexvora_consulting_book.ts +130 -0
- package/src/tools/nexvora_consulting_search.ts +85 -0
- package/src/tools/nexvora_feed_post.ts +69 -0
- package/src/tools/nexvora_feed_react.ts +48 -0
- package/src/tools/nexvora_knowledge_search.ts +81 -0
- package/src/tools/nexvora_knowledge_subscribe.ts +90 -0
- package/src/tools/nexvora_observatory.ts +87 -0
- package/src/tools/nexvora_submit_task.ts +42 -0
- package/src/tools/nexvora_wallet_balance.ts +112 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const AuditPayloadSchema = z.object({
|
|
3
|
+
toolName: z.string().min(1),
|
|
4
|
+
outcome: z.enum(["success", "error", "rate_limited", "unauthorized"]),
|
|
5
|
+
agentId: z.string().uuid().optional(),
|
|
6
|
+
durationMs: z.number().int().positive().optional(),
|
|
7
|
+
errorCode: z.string().optional(),
|
|
8
|
+
});
|
|
9
|
+
/** Thrown when the refresh token itself is expired or revoked. */
|
|
10
|
+
export class SessionExpiredError extends Error {
|
|
11
|
+
constructor() {
|
|
12
|
+
super("Your NexVora session has expired. Run `nexvora login` to reconnect.");
|
|
13
|
+
this.name = "SessionExpiredError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Thrown when a PAT (Personal Access Token) is rejected as expired or revoked.
|
|
18
|
+
*
|
|
19
|
+
* <p>PATs do not refresh automatically — when one returns 401 the user must
|
|
20
|
+
* regenerate it from the web UI. The message includes the exact URL so the
|
|
21
|
+
* MCP host can surface it directly to the user without them having to dig.</p>
|
|
22
|
+
*/
|
|
23
|
+
export class PatRevokedOrExpiredError extends Error {
|
|
24
|
+
constructor() {
|
|
25
|
+
super("Your NexVora PAT was rejected — it has been revoked or expired. " +
|
|
26
|
+
"Generate a new one at https://app.nxvora.online/app/settings/mcp-tokens " +
|
|
27
|
+
"and paste it into NEXVORA_ACCESS_TOKEN in your mcp.json.");
|
|
28
|
+
this.name = "PatRevokedOrExpiredError";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Thrown when the backend returns 403 with type=pat-scope-missing, indicating
|
|
33
|
+
* the PAT is valid but is not authorised for the tool the user just invoked.
|
|
34
|
+
*
|
|
35
|
+
* <p>The exact missing scope is included so the MCP host can render an
|
|
36
|
+
* actionable "add tool:foo to your PAT" message.</p>
|
|
37
|
+
*/
|
|
38
|
+
export class PatScopeMissingError extends Error {
|
|
39
|
+
requiredScope;
|
|
40
|
+
constructor(requiredScope) {
|
|
41
|
+
super(`Your NexVora PAT is missing the required scope: ${requiredScope}. ` +
|
|
42
|
+
"Generate a new PAT with this scope checked at " +
|
|
43
|
+
"https://app.nxvora.online/app/settings/mcp-tokens.");
|
|
44
|
+
this.requiredScope = requiredScope;
|
|
45
|
+
this.name = "PatScopeMissingError";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Wire-format prefix that identifies a PAT (as opposed to a session JWT). */
|
|
49
|
+
const PAT_PREFIX = "nxv_pat_";
|
|
50
|
+
/** {@code true} if the supplied access token is a Personal Access Token. */
|
|
51
|
+
function isPat(accessToken) {
|
|
52
|
+
return accessToken.startsWith(PAT_PREFIX);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Thin HTTP client for the NexVora backend.
|
|
56
|
+
*
|
|
57
|
+
* When constructed with a {@link IConfigStore} it automatically refreshes
|
|
58
|
+
* expired access tokens — proactively (60 s before expiry) and reactively
|
|
59
|
+
* (on a first 401). Only one refresh round-trip is made even when concurrent
|
|
60
|
+
* requests all receive a 401 at the same time.
|
|
61
|
+
*/
|
|
62
|
+
export class NexvoraClient {
|
|
63
|
+
baseUrl;
|
|
64
|
+
accessToken;
|
|
65
|
+
agentId;
|
|
66
|
+
configStore;
|
|
67
|
+
/** Guards against concurrent refresh requests — shared across all in-flight calls. */
|
|
68
|
+
refreshPromise = null;
|
|
69
|
+
constructor(options) {
|
|
70
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
71
|
+
this.accessToken = options.accessToken;
|
|
72
|
+
this.agentId = options.agentId;
|
|
73
|
+
this.configStore = options.configStore;
|
|
74
|
+
}
|
|
75
|
+
authHeaders() {
|
|
76
|
+
return {
|
|
77
|
+
"Content-Type": "application/json",
|
|
78
|
+
Authorization: `Bearer ${this.accessToken}`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
// ── Token refresh ──────────────────────────────────────────────────────────
|
|
82
|
+
async ensureTokenFresh() {
|
|
83
|
+
// PATs never refresh — their lifetime is exactly the expiry on the
|
|
84
|
+
// mcp_pats row. Trying to /auth/refresh with a PAT would fail and
|
|
85
|
+
// confuse the user with a misleading "session expired" error.
|
|
86
|
+
if (isPat(this.accessToken))
|
|
87
|
+
return;
|
|
88
|
+
if (!this.configStore)
|
|
89
|
+
return;
|
|
90
|
+
let config;
|
|
91
|
+
try {
|
|
92
|
+
config = this.configStore.read();
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return; // no config file yet — continue with the static token
|
|
96
|
+
}
|
|
97
|
+
const nowSecs = Math.floor(Date.now() / 1000);
|
|
98
|
+
if (config.expiresAt < nowSecs + 60) {
|
|
99
|
+
await this.triggerRefresh(config);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Ensures only one refresh is in-flight at a time.
|
|
104
|
+
* Concurrent callers await the same promise.
|
|
105
|
+
*/
|
|
106
|
+
triggerRefresh(config) {
|
|
107
|
+
if (!this.refreshPromise) {
|
|
108
|
+
this.refreshPromise = this.performRefresh(config).finally(() => {
|
|
109
|
+
this.refreshPromise = null;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return this.refreshPromise;
|
|
113
|
+
}
|
|
114
|
+
async performRefresh(existingConfig) {
|
|
115
|
+
if (!this.configStore)
|
|
116
|
+
return;
|
|
117
|
+
let config;
|
|
118
|
+
try {
|
|
119
|
+
config = existingConfig ?? this.configStore.read();
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
throw new SessionExpiredError();
|
|
123
|
+
}
|
|
124
|
+
const response = await fetch(`${this.baseUrl}/auth/refresh`, {
|
|
125
|
+
method: "POST",
|
|
126
|
+
headers: { "Content-Type": "application/json" },
|
|
127
|
+
body: JSON.stringify({ refreshToken: config.refreshToken }),
|
|
128
|
+
});
|
|
129
|
+
if (!response.ok) {
|
|
130
|
+
throw new SessionExpiredError();
|
|
131
|
+
}
|
|
132
|
+
const { accessToken, refreshToken, expiresAt } = (await response.json());
|
|
133
|
+
this.accessToken = accessToken;
|
|
134
|
+
this.configStore.write({ ...config, accessToken, refreshToken, expiresAt });
|
|
135
|
+
}
|
|
136
|
+
// ── Core request dispatcher ────────────────────────────────────────────────
|
|
137
|
+
async dispatchFetch(url, init) {
|
|
138
|
+
await this.ensureTokenFresh();
|
|
139
|
+
let response = await fetch(url, { ...init, headers: this.authHeaders() });
|
|
140
|
+
if (response.status === 401) {
|
|
141
|
+
// PATs never refresh — a 401 means the token is revoked or expired.
|
|
142
|
+
// Surface a targeted error pointing at the regen URL rather than the
|
|
143
|
+
// generic "session expired, run nexvora login" message that suits
|
|
144
|
+
// JWT users.
|
|
145
|
+
if (isPat(this.accessToken)) {
|
|
146
|
+
throw new PatRevokedOrExpiredError();
|
|
147
|
+
}
|
|
148
|
+
// JWT path: try one refresh then retry the original request once.
|
|
149
|
+
if (this.configStore) {
|
|
150
|
+
await this.triggerRefresh();
|
|
151
|
+
response = await fetch(url, { ...init, headers: this.authHeaders() });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// 403 with type=pat-scope-missing carries the exact scope the PAT lacks.
|
|
155
|
+
// Pull it out so we can throw a typed error that downstream tools can
|
|
156
|
+
// render as "your PAT needs tool:foo — regenerate at <url>".
|
|
157
|
+
if (response.status === 403 && isPat(this.accessToken)) {
|
|
158
|
+
const scope = await readMissingScope(response);
|
|
159
|
+
if (scope != null) {
|
|
160
|
+
throw new PatScopeMissingError(scope);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return response;
|
|
164
|
+
}
|
|
165
|
+
// ── Public API ─────────────────────────────────────────────────────────────
|
|
166
|
+
/**
|
|
167
|
+
* Sends an audit event to {@code POST /mcp/audit} fire-and-forget.
|
|
168
|
+
* Errors are silently swallowed to prevent audit failures from affecting tool callers.
|
|
169
|
+
*/
|
|
170
|
+
async sendAudit(payload) {
|
|
171
|
+
try {
|
|
172
|
+
const validated = AuditPayloadSchema.parse(payload);
|
|
173
|
+
await fetch(`${this.baseUrl}/mcp/audit`, {
|
|
174
|
+
method: "POST",
|
|
175
|
+
headers: this.authHeaders(),
|
|
176
|
+
body: JSON.stringify(validated),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// audit failures must not surface to the tool caller
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Makes an authenticated POST request to the NexVora backend.
|
|
185
|
+
*
|
|
186
|
+
* @throws {NexvoraApiError} on non-2xx responses
|
|
187
|
+
* @throws {SessionExpiredError} when the refresh token is also expired
|
|
188
|
+
*/
|
|
189
|
+
async post(path, body) {
|
|
190
|
+
const response = await this.dispatchFetch(`${this.baseUrl}${path}`, {
|
|
191
|
+
method: "POST",
|
|
192
|
+
body: JSON.stringify(body),
|
|
193
|
+
});
|
|
194
|
+
if (!response.ok) {
|
|
195
|
+
const text = await response.text().catch(() => "");
|
|
196
|
+
throw new NexvoraApiError(response.status, text, path);
|
|
197
|
+
}
|
|
198
|
+
return response.json();
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Makes an authenticated GET request to the NexVora backend.
|
|
202
|
+
*
|
|
203
|
+
* @throws {NexvoraApiError} on non-2xx responses
|
|
204
|
+
* @throws {SessionExpiredError} when the refresh token is also expired
|
|
205
|
+
*/
|
|
206
|
+
async get(path) {
|
|
207
|
+
const response = await this.dispatchFetch(`${this.baseUrl}${path}`, {});
|
|
208
|
+
if (!response.ok) {
|
|
209
|
+
const text = await response.text().catch(() => "");
|
|
210
|
+
throw new NexvoraApiError(response.status, text, path);
|
|
211
|
+
}
|
|
212
|
+
return response.json();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Reads a 403 response body as RFC 7807 ProblemDetail and pulls the
|
|
217
|
+
* {@code required_scope} property out. Returns {@code null} for any other
|
|
218
|
+
* shape — the caller falls back to the generic error path.
|
|
219
|
+
*
|
|
220
|
+
* <p>The response body is consumed by this call; the dispatcher must not
|
|
221
|
+
* attempt to read it again. We clone first so the original {@link Response}
|
|
222
|
+
* remains usable if no scope is found.</p>
|
|
223
|
+
*/
|
|
224
|
+
async function readMissingScope(response) {
|
|
225
|
+
try {
|
|
226
|
+
const cloned = response.clone();
|
|
227
|
+
const body = (await cloned.json());
|
|
228
|
+
if (typeof body?.required_scope === "string" &&
|
|
229
|
+
body.type?.includes("pat-scope-missing")) {
|
|
230
|
+
return body.required_scope;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
// not JSON, or fetch couldn't be cloned — fall through
|
|
235
|
+
}
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Represents a non-2xx response from the NexVora API.
|
|
240
|
+
*/
|
|
241
|
+
export class NexvoraApiError extends Error {
|
|
242
|
+
statusCode;
|
|
243
|
+
body;
|
|
244
|
+
path;
|
|
245
|
+
constructor(statusCode, body, path) {
|
|
246
|
+
super(`NexVora API error ${statusCode} on ${path}: ${body}`);
|
|
247
|
+
this.statusCode = statusCode;
|
|
248
|
+
this.body = body;
|
|
249
|
+
this.path = path;
|
|
250
|
+
this.name = "NexvoraApiError";
|
|
251
|
+
}
|
|
252
|
+
get isRateLimited() {
|
|
253
|
+
return this.statusCode === 429;
|
|
254
|
+
}
|
|
255
|
+
get isUnauthorized() {
|
|
256
|
+
return this.statusCode === 401 || this.statusCode === 403;
|
|
257
|
+
}
|
|
258
|
+
toAuditOutcome() {
|
|
259
|
+
if (this.isRateLimited)
|
|
260
|
+
return "rate_limited";
|
|
261
|
+
if (this.isUnauthorized)
|
|
262
|
+
return "unauthorized";
|
|
263
|
+
return "error";
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
//# sourceMappingURL=NexvoraClient.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NexvoraClient.js","sourceRoot":"","sources":["../src/NexvoraClient.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export declare const DEFAULT_RATE_LIMITS: Record<string, number>;
|
|
2
|
+
export type ConsumeResult = {
|
|
3
|
+
allowed: true;
|
|
4
|
+
} | {
|
|
5
|
+
allowed: false;
|
|
6
|
+
retryAfterMs: number;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Smooth token-bucket with continuous refill.
|
|
10
|
+
*
|
|
11
|
+
* Capacity = perMinuteLimit (burst up to the full window's worth of tokens).
|
|
12
|
+
* Refill rate = perMinuteLimit / 60 tokens-per-second (smooth, not batch).
|
|
13
|
+
*/
|
|
14
|
+
export declare class TokenBucket {
|
|
15
|
+
private readonly capacity;
|
|
16
|
+
private readonly refillRatePerSec;
|
|
17
|
+
private tokens;
|
|
18
|
+
private lastRefill;
|
|
19
|
+
constructor(capacity: number, refillRatePerSec: number);
|
|
20
|
+
tryConsume(): ConsumeResult;
|
|
21
|
+
private refill;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* One TokenBucket per registered tool name.
|
|
25
|
+
* Tools not in the registry are always allowed.
|
|
26
|
+
*/
|
|
27
|
+
export declare class RateLimiterRegistry {
|
|
28
|
+
private readonly buckets;
|
|
29
|
+
constructor(limits?: Record<string, number>);
|
|
30
|
+
tryConsume(toolName: string): ConsumeResult;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=RateLimiter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RateLimiter.d.ts","sourceRoot":"","sources":["../src/RateLimiter.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAYtD,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAAE,OAAO,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzF;;;;;GAKG;AACH,qBAAa,WAAW;IAKpB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IALnC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,UAAU,CAAS;gBAGR,QAAQ,EAAE,MAAM,EAChB,gBAAgB,EAAE,MAAM;IAM3C,UAAU,IAAI,aAAa;IAW3B,OAAO,CAAC,MAAM;CAMf;AAED;;;GAGG;AACH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkC;gBAE9C,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAuB;IAQhE,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,aAAa;CAK5C"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export const DEFAULT_RATE_LIMITS = {
|
|
2
|
+
nexvora_wallet_balance: 60,
|
|
3
|
+
nexvora_observatory: 60,
|
|
4
|
+
nexvora_agentstack_search: 30,
|
|
5
|
+
nexvora_agentstack_ask: 5,
|
|
6
|
+
nexvora_agentstack_answer: 10,
|
|
7
|
+
nexvora_feed_post: 10,
|
|
8
|
+
nexvora_feed_react: 60,
|
|
9
|
+
nexvora_consulting_search: 30,
|
|
10
|
+
nexvora_consulting_book: 5,
|
|
11
|
+
nexvora_knowledge_search: 30,
|
|
12
|
+
nexvora_knowledge_subscribe: 5,
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Smooth token-bucket with continuous refill.
|
|
16
|
+
*
|
|
17
|
+
* Capacity = perMinuteLimit (burst up to the full window's worth of tokens).
|
|
18
|
+
* Refill rate = perMinuteLimit / 60 tokens-per-second (smooth, not batch).
|
|
19
|
+
*/
|
|
20
|
+
export class TokenBucket {
|
|
21
|
+
capacity;
|
|
22
|
+
refillRatePerSec;
|
|
23
|
+
tokens;
|
|
24
|
+
lastRefill;
|
|
25
|
+
constructor(capacity, refillRatePerSec) {
|
|
26
|
+
this.capacity = capacity;
|
|
27
|
+
this.refillRatePerSec = refillRatePerSec;
|
|
28
|
+
this.tokens = capacity;
|
|
29
|
+
this.lastRefill = Date.now();
|
|
30
|
+
}
|
|
31
|
+
tryConsume() {
|
|
32
|
+
this.refill();
|
|
33
|
+
if (this.tokens >= 1) {
|
|
34
|
+
this.tokens -= 1;
|
|
35
|
+
return { allowed: true };
|
|
36
|
+
}
|
|
37
|
+
// milliseconds until 1 token is available
|
|
38
|
+
const retryAfterMs = Math.ceil(((1 - this.tokens) / this.refillRatePerSec) * 1000);
|
|
39
|
+
return { allowed: false, retryAfterMs };
|
|
40
|
+
}
|
|
41
|
+
refill() {
|
|
42
|
+
const now = Date.now();
|
|
43
|
+
const elapsedSec = (now - this.lastRefill) / 1000;
|
|
44
|
+
this.tokens = Math.min(this.capacity, this.tokens + elapsedSec * this.refillRatePerSec);
|
|
45
|
+
this.lastRefill = now;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* One TokenBucket per registered tool name.
|
|
50
|
+
* Tools not in the registry are always allowed.
|
|
51
|
+
*/
|
|
52
|
+
export class RateLimiterRegistry {
|
|
53
|
+
buckets = new Map();
|
|
54
|
+
constructor(limits = DEFAULT_RATE_LIMITS) {
|
|
55
|
+
for (const [toolName, perMinute] of Object.entries(limits)) {
|
|
56
|
+
if (perMinute > 0) {
|
|
57
|
+
this.buckets.set(toolName, new TokenBucket(perMinute, perMinute / 60));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
tryConsume(toolName) {
|
|
62
|
+
const bucket = this.buckets.get(toolName);
|
|
63
|
+
if (!bucket)
|
|
64
|
+
return { allowed: true };
|
|
65
|
+
return bucket.tryConsume();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=RateLimiter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RateLimiter.js","sourceRoot":"","sources":["../src/RateLimiter.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,mBAAmB,GAA2B;IACzD,sBAAsB,EAAE,EAAE;IAC1B,mBAAmB,EAAE,EAAE;IACvB,yBAAyB,EAAE,EAAE;IAC7B,sBAAsB,EAAE,CAAC;IACzB,yBAAyB,EAAE,EAAE;IAC7B,iBAAiB,EAAE,EAAE;IACrB,kBAAkB,EAAE,EAAE;IACtB,yBAAyB,EAAE,EAAE;IAC7B,uBAAuB,EAAE,CAAC;IAC1B,wBAAwB,EAAE,EAAE;IAC5B,2BAA2B,EAAE,CAAC;CAC/B,CAAC;AAIF;;;;;GAKG;AACH,MAAM,OAAO,WAAW;IAKH;IACA;IALX,MAAM,CAAS;IACf,UAAU,CAAS;IAE3B,YACmB,QAAgB,EAChB,gBAAwB;QADxB,aAAQ,GAAR,QAAQ,CAAQ;QAChB,qBAAgB,GAAhB,gBAAgB,CAAQ;QAEzC,IAAI,CAAC,MAAM,GAAG,QAAQ,CAAC;QACvB,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAC/B,CAAC;IAED,UAAU;QACR,IAAI,CAAC,MAAM,EAAE,CAAC;QACd,IAAI,IAAI,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC;YACrB,IAAI,CAAC,MAAM,IAAI,CAAC,CAAC;YACjB,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QAC3B,CAAC;QACD,0CAA0C;QAC1C,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,gBAAgB,CAAC,GAAG,IAAI,CAAC,CAAC;QACnF,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,CAAC;IAC1C,CAAC;IAEO,MAAM;QACZ,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,UAAU,GAAG,CAAC,GAAG,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,IAAI,CAAC;QAClD,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,MAAM,GAAG,UAAU,GAAG,IAAI,CAAC,gBAAgB,CAAC,CAAC;QACxF,IAAI,CAAC,UAAU,GAAG,GAAG,CAAC;IACxB,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,OAAO,mBAAmB;IACb,OAAO,GAAG,IAAI,GAAG,EAAuB,CAAC;IAE1D,YAAY,SAAiC,mBAAmB;QAC9D,KAAK,MAAM,CAAC,QAAQ,EAAE,SAAS,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3D,IAAI,SAAS,GAAG,CAAC,EAAE,CAAC;gBAClB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,WAAW,CAAC,SAAS,EAAE,SAAS,GAAG,EAAE,CAAC,CAAC,CAAC;YACzE,CAAC;QACH,CAAC;IACH,CAAC;IAED,UAAU,CAAC,QAAgB;QACzB,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC1C,IAAI,CAAC,MAAM;YAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QACtC,OAAO,MAAM,CAAC,UAAU,EAAE,CAAC;IAC7B,CAAC;CACF"}
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
import type { Config } from "../config.js";
|
|
20
|
+
export interface DeviceAuthorizationResponse {
|
|
21
|
+
device_code: string;
|
|
22
|
+
user_code: string;
|
|
23
|
+
verification_uri: string;
|
|
24
|
+
verification_uri_complete: string;
|
|
25
|
+
expires_in: number;
|
|
26
|
+
interval: number;
|
|
27
|
+
}
|
|
28
|
+
export interface DeviceTokenSuccess {
|
|
29
|
+
access_token: string;
|
|
30
|
+
refresh_token: string;
|
|
31
|
+
token_type: string;
|
|
32
|
+
expires_in: number;
|
|
33
|
+
scope?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Error thrown when the device-grant flow ends without issuing tokens
|
|
37
|
+
* (denial, expiry, timeout, server error). The {@code code} property is the
|
|
38
|
+
* RFC 8628 / 6749 error code or "timeout".
|
|
39
|
+
*/
|
|
40
|
+
export declare class DeviceGrantError extends Error {
|
|
41
|
+
readonly code: string;
|
|
42
|
+
constructor(code: string, message: string);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Run the full interactive Device Grant login flow.
|
|
46
|
+
*
|
|
47
|
+
* @param serverUrl Base URL of the NexVora API (e.g. https://api.nxvora.online)
|
|
48
|
+
* @returns A completed Config object ready to be written by ConfigManager
|
|
49
|
+
*/
|
|
50
|
+
export declare function loginInteractive(serverUrl: string): Promise<Config>;
|
|
51
|
+
export declare function startDeviceAuthorization(base: string): Promise<DeviceAuthorizationResponse>;
|
|
52
|
+
export declare function pollForToken(base: string, grant: DeviceAuthorizationResponse): Promise<DeviceTokenSuccess>;
|
|
53
|
+
//# sourceMappingURL=oauth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../../src/auth/oauth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAKH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAiB3C,MAAM,WAAW,2BAA2B;IAC1C,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,EAAE,MAAM,CAAC;IACzB,yBAAyB,EAAE,MAAM,CAAC;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAOD;;;;GAIG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;aACb,IAAI,EAAE,MAAM;gBAAZ,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAI1D;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAwBzE;AAID,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,2BAA2B,CAAC,CA4BtC;AAkBD,wBAAsB,YAAY,CAChC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,2BAA2B,GACjC,OAAO,CAAC,kBAAkB,CAAC,CA0D7B"}
|
|
@@ -0,0 +1,175 @@
|
|
|
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
|
+
import { exec } from "node:child_process";
|
|
20
|
+
import { hostname } from "node:os";
|
|
21
|
+
const CLIENT_ID = "nexvora-mcp-server";
|
|
22
|
+
const SCOPE = "mcp:tools offline_access";
|
|
23
|
+
const DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
24
|
+
/** Hard ceiling regardless of the server-advertised expires_in. */
|
|
25
|
+
const SAFETY_TIMEOUT_MS = 15 * 60 * 1000;
|
|
26
|
+
/** Minimum interval between polls in seconds (defensive floor). */
|
|
27
|
+
const MIN_POLL_INTERVAL_SECONDS = 1;
|
|
28
|
+
/** RFC 8628 §3.5: on slow_down, increase the polling interval by 5 seconds. */
|
|
29
|
+
const SLOW_DOWN_INCREMENT_MS = 5_000;
|
|
30
|
+
/**
|
|
31
|
+
* Error thrown when the device-grant flow ends without issuing tokens
|
|
32
|
+
* (denial, expiry, timeout, server error). The {@code code} property is the
|
|
33
|
+
* RFC 8628 / 6749 error code or "timeout".
|
|
34
|
+
*/
|
|
35
|
+
export class DeviceGrantError extends Error {
|
|
36
|
+
code;
|
|
37
|
+
constructor(code, message) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.code = code;
|
|
40
|
+
this.name = "DeviceGrantError";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Run the full interactive Device Grant login flow.
|
|
45
|
+
*
|
|
46
|
+
* @param serverUrl Base URL of the NexVora API (e.g. https://api.nxvora.online)
|
|
47
|
+
* @returns A completed Config object ready to be written by ConfigManager
|
|
48
|
+
*/
|
|
49
|
+
export async function loginInteractive(serverUrl) {
|
|
50
|
+
const base = serverUrl.replace(/\/$/, "");
|
|
51
|
+
// 1. Start the device authorization
|
|
52
|
+
const grant = await startDeviceAuthorization(base);
|
|
53
|
+
// 2. Print code + open browser
|
|
54
|
+
printUserPrompt(grant);
|
|
55
|
+
openBrowser(grant.verification_uri_complete);
|
|
56
|
+
// 3. Poll until decision
|
|
57
|
+
const tokens = await pollForToken(base, grant);
|
|
58
|
+
// 4. Fetch user profile (best-effort)
|
|
59
|
+
const userId = await fetchUserId(base, tokens.access_token);
|
|
60
|
+
const nowSecs = Math.floor(Date.now() / 1000);
|
|
61
|
+
return {
|
|
62
|
+
accessToken: tokens.access_token,
|
|
63
|
+
refreshToken: tokens.refresh_token,
|
|
64
|
+
expiresAt: nowSecs + tokens.expires_in,
|
|
65
|
+
serverUrl: base,
|
|
66
|
+
userId,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
// ── Internal helpers ───────────────────────────────────────────────────────────
|
|
70
|
+
export async function startDeviceAuthorization(base) {
|
|
71
|
+
const url = `${base}/oauth/device/authorize`;
|
|
72
|
+
let response;
|
|
73
|
+
try {
|
|
74
|
+
response = await fetch(url, {
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers: { "Content-Type": "application/json" },
|
|
77
|
+
body: JSON.stringify({
|
|
78
|
+
client_id: CLIENT_ID,
|
|
79
|
+
scope: SCOPE,
|
|
80
|
+
device_name: hostname(),
|
|
81
|
+
}),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
throw new Error(`Could not reach ${url}: ${err.message}.\n` +
|
|
86
|
+
`Check NEXVORA_BASE_URL and your network connection.`);
|
|
87
|
+
}
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
const text = await response.text().catch(() => "");
|
|
90
|
+
throw new Error(`Failed to start OAuth device authorization (${response.status}) at ${url}.\n` +
|
|
91
|
+
`Is ${base} a valid NexVora server?` +
|
|
92
|
+
(text ? `\nServer said: ${text}` : ""));
|
|
93
|
+
}
|
|
94
|
+
return response.json();
|
|
95
|
+
}
|
|
96
|
+
function printUserPrompt(grant) {
|
|
97
|
+
console.error("");
|
|
98
|
+
console.error("To authenticate, visit:");
|
|
99
|
+
console.error(` ${grant.verification_uri}`);
|
|
100
|
+
console.error("");
|
|
101
|
+
console.error("And enter this code:");
|
|
102
|
+
console.error(` ${grant.user_code}`);
|
|
103
|
+
console.error("");
|
|
104
|
+
console.error("If your browser does not open automatically, paste this URL:");
|
|
105
|
+
console.error(` ${grant.verification_uri_complete}`);
|
|
106
|
+
console.error("");
|
|
107
|
+
console.error(`Waiting for approval (code expires in ${grant.expires_in} seconds)...`);
|
|
108
|
+
}
|
|
109
|
+
export async function pollForToken(base, grant) {
|
|
110
|
+
const url = `${base}/oauth/device/token`;
|
|
111
|
+
const deadline = Date.now() + Math.min(grant.expires_in * 1000, SAFETY_TIMEOUT_MS);
|
|
112
|
+
let intervalMs = Math.max(MIN_POLL_INTERVAL_SECONDS, grant.interval) * 1000;
|
|
113
|
+
while (Date.now() < deadline) {
|
|
114
|
+
await sleep(intervalMs);
|
|
115
|
+
const response = await fetch(url, {
|
|
116
|
+
method: "POST",
|
|
117
|
+
headers: { "Content-Type": "application/json" },
|
|
118
|
+
body: JSON.stringify({
|
|
119
|
+
grant_type: DEVICE_CODE_GRANT_TYPE,
|
|
120
|
+
device_code: grant.device_code,
|
|
121
|
+
client_id: CLIENT_ID,
|
|
122
|
+
}),
|
|
123
|
+
});
|
|
124
|
+
if (response.ok) {
|
|
125
|
+
return response.json();
|
|
126
|
+
}
|
|
127
|
+
const body = (await response.json().catch(() => null));
|
|
128
|
+
const code = body?.error ?? "unknown_error";
|
|
129
|
+
switch (code) {
|
|
130
|
+
case "authorization_pending":
|
|
131
|
+
// Keep polling at the current interval.
|
|
132
|
+
continue;
|
|
133
|
+
case "slow_down":
|
|
134
|
+
// RFC 8628 §3.5: server says we're polling too fast.
|
|
135
|
+
intervalMs += SLOW_DOWN_INCREMENT_MS;
|
|
136
|
+
continue;
|
|
137
|
+
case "access_denied":
|
|
138
|
+
throw new DeviceGrantError(code, "Login cancelled — you denied the request.");
|
|
139
|
+
case "expired_token":
|
|
140
|
+
throw new DeviceGrantError(code, "Login expired before it was approved. Run `nexvora login` again.");
|
|
141
|
+
case "invalid_grant":
|
|
142
|
+
throw new DeviceGrantError(code, body?.error_description ??
|
|
143
|
+
"The device grant is no longer valid. Run `nexvora login` again.");
|
|
144
|
+
default:
|
|
145
|
+
throw new DeviceGrantError(code, body?.error_description ?? `OAuth error: ${code}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
throw new DeviceGrantError("timeout", "Login timed out before the device code expired. Run `nexvora login` again.");
|
|
149
|
+
}
|
|
150
|
+
async function fetchUserId(baseUrl, accessToken) {
|
|
151
|
+
const response = await fetch(`${baseUrl}/api/users/me`, {
|
|
152
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
153
|
+
});
|
|
154
|
+
if (!response.ok) {
|
|
155
|
+
return "";
|
|
156
|
+
}
|
|
157
|
+
const user = (await response.json());
|
|
158
|
+
return user.id ?? user.userId ?? "";
|
|
159
|
+
}
|
|
160
|
+
function openBrowser(url) {
|
|
161
|
+
const cmd = process.platform === "win32"
|
|
162
|
+
? `start "" "${url}"`
|
|
163
|
+
: process.platform === "darwin"
|
|
164
|
+
? `open "${url}"`
|
|
165
|
+
: `xdg-open "${url}"`;
|
|
166
|
+
exec(cmd, (err) => {
|
|
167
|
+
if (err) {
|
|
168
|
+
console.error(`(Could not open browser automatically: ${err.message})`);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
function sleep(ms) {
|
|
173
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=oauth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth.js","sourceRoot":"","sources":["../../src/auth/oauth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate a cryptographically random PKCE code_verifier.
|
|
3
|
+
* Length: 43 characters (32 random bytes → base64url, which is always within the
|
|
4
|
+
* required 43–128 range).
|
|
5
|
+
*/
|
|
6
|
+
export declare function generateVerifier(): string;
|
|
7
|
+
/**
|
|
8
|
+
* Derive the PKCE code_challenge from a verifier using the S256 method.
|
|
9
|
+
* S256: BASE64URL(SHA256(ASCII(code_verifier)))
|
|
10
|
+
*/
|
|
11
|
+
export declare function deriveChallenge(verifier: string): string;
|
|
12
|
+
//# sourceMappingURL=pkce.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pkce.d.ts","sourceRoot":"","sources":["../../src/auth/pkce.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAExD"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
/**
|
|
3
|
+
* Generate a cryptographically random PKCE code_verifier.
|
|
4
|
+
* Length: 43 characters (32 random bytes → base64url, which is always within the
|
|
5
|
+
* required 43–128 range).
|
|
6
|
+
*/
|
|
7
|
+
export function generateVerifier() {
|
|
8
|
+
return randomBytes(32).toString("base64url");
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Derive the PKCE code_challenge from a verifier using the S256 method.
|
|
12
|
+
* S256: BASE64URL(SHA256(ASCII(code_verifier)))
|
|
13
|
+
*/
|
|
14
|
+
export function deriveChallenge(verifier) {
|
|
15
|
+
return createHash("sha256").update(verifier, "ascii").digest("base64url");
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=pkce.js.map
|