@phake/mcp 0.0.3 → 0.0.5
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 +198 -148
- package/dist/index-zhw318hb.js +3149 -0
- package/dist/index.js +5 -5
- package/dist/mcp-server.d.ts +4 -0
- package/dist/runtime/worker/index.js +1 -1
- package/dist/shared/utils/elicitation.d.ts +1 -1
- package/package.json +3 -5
- package/dist/index-1zv84kf7.js +0 -22961
|
@@ -0,0 +1,3149 @@
|
|
|
1
|
+
// src/shared/utils/base64.ts
|
|
2
|
+
function base64Encode(input) {
|
|
3
|
+
return btoa(input);
|
|
4
|
+
}
|
|
5
|
+
function base64Decode(input) {
|
|
6
|
+
return atob(input);
|
|
7
|
+
}
|
|
8
|
+
function base64UrlEncode(bytes) {
|
|
9
|
+
let binary = "";
|
|
10
|
+
for (let i = 0;i < bytes.length; i++) {
|
|
11
|
+
binary += String.fromCharCode(bytes[i]);
|
|
12
|
+
}
|
|
13
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
14
|
+
}
|
|
15
|
+
function base64UrlDecode(str) {
|
|
16
|
+
let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
17
|
+
const padLength = (4 - base64.length % 4) % 4;
|
|
18
|
+
base64 += "=".repeat(padLength);
|
|
19
|
+
const binary = atob(base64);
|
|
20
|
+
const bytes = new Uint8Array(binary.length);
|
|
21
|
+
for (let i = 0;i < binary.length; i++) {
|
|
22
|
+
bytes[i] = binary.charCodeAt(i);
|
|
23
|
+
}
|
|
24
|
+
return bytes;
|
|
25
|
+
}
|
|
26
|
+
function base64UrlEncodeString(input) {
|
|
27
|
+
return base64UrlEncode(new TextEncoder().encode(input));
|
|
28
|
+
}
|
|
29
|
+
function base64UrlDecodeString(input) {
|
|
30
|
+
return new TextDecoder().decode(base64UrlDecode(input));
|
|
31
|
+
}
|
|
32
|
+
function base64UrlEncodeJson(obj) {
|
|
33
|
+
try {
|
|
34
|
+
return base64UrlEncodeString(JSON.stringify(obj));
|
|
35
|
+
} catch {
|
|
36
|
+
return "";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function base64UrlDecodeJson(value) {
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(base64UrlDecodeString(value));
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/shared/crypto/aes-gcm.ts
|
|
48
|
+
var ALGORITHM = "AES-GCM";
|
|
49
|
+
var KEY_LENGTH = 256;
|
|
50
|
+
var IV_LENGTH = 12;
|
|
51
|
+
var TAG_LENGTH = 128;
|
|
52
|
+
async function deriveKey(secret) {
|
|
53
|
+
const keyBytes = base64UrlDecode(secret);
|
|
54
|
+
if (keyBytes.length !== 32) {
|
|
55
|
+
throw new Error(`Invalid key length: expected 32 bytes, got ${keyBytes.length}`);
|
|
56
|
+
}
|
|
57
|
+
return crypto.subtle.importKey("raw", keyBytes.buffer, { name: ALGORITHM, length: KEY_LENGTH }, false, ["encrypt", "decrypt"]);
|
|
58
|
+
}
|
|
59
|
+
async function encrypt(plaintext, secret) {
|
|
60
|
+
const key = await deriveKey(secret);
|
|
61
|
+
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
|
|
62
|
+
const plaintextBytes = new TextEncoder().encode(plaintext);
|
|
63
|
+
const ciphertext = await crypto.subtle.encrypt({ name: ALGORITHM, iv, tagLength: TAG_LENGTH }, key, plaintextBytes);
|
|
64
|
+
const combined = new Uint8Array(iv.length + ciphertext.byteLength);
|
|
65
|
+
combined.set(iv, 0);
|
|
66
|
+
combined.set(new Uint8Array(ciphertext), iv.length);
|
|
67
|
+
return base64UrlEncode(combined);
|
|
68
|
+
}
|
|
69
|
+
async function decrypt(ciphertext, secret) {
|
|
70
|
+
const key = await deriveKey(secret);
|
|
71
|
+
const combined = base64UrlDecode(ciphertext);
|
|
72
|
+
if (combined.length < IV_LENGTH + 16) {
|
|
73
|
+
throw new Error("Invalid ciphertext: too short");
|
|
74
|
+
}
|
|
75
|
+
const iv = combined.slice(0, IV_LENGTH);
|
|
76
|
+
const encrypted = combined.slice(IV_LENGTH);
|
|
77
|
+
const plaintextBytes = await crypto.subtle.decrypt({ name: ALGORITHM, iv, tagLength: TAG_LENGTH }, key, encrypted);
|
|
78
|
+
return new TextDecoder().decode(plaintextBytes);
|
|
79
|
+
}
|
|
80
|
+
function generateKey() {
|
|
81
|
+
const bytes = crypto.getRandomValues(new Uint8Array(32));
|
|
82
|
+
return base64UrlEncode(bytes);
|
|
83
|
+
}
|
|
84
|
+
function createEncryptor(secret) {
|
|
85
|
+
return {
|
|
86
|
+
encrypt: (plaintext) => encrypt(plaintext, secret),
|
|
87
|
+
decrypt: (ciphertext) => decrypt(ciphertext, secret)
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/shared/http/cors.ts
|
|
92
|
+
var DEFAULT_CORS = {
|
|
93
|
+
origin: "*",
|
|
94
|
+
methods: ["GET", "POST", "DELETE", "OPTIONS"],
|
|
95
|
+
headers: ["*"],
|
|
96
|
+
credentials: false,
|
|
97
|
+
maxAge: 86400
|
|
98
|
+
};
|
|
99
|
+
function withCors(response, options = {}) {
|
|
100
|
+
const opts = { ...DEFAULT_CORS, ...options };
|
|
101
|
+
response.headers.set("Access-Control-Allow-Origin", opts.origin ?? "*");
|
|
102
|
+
response.headers.set("Access-Control-Allow-Methods", (opts.methods ?? []).join(", "));
|
|
103
|
+
response.headers.set("Access-Control-Allow-Headers", (opts.headers ?? []).join(", "));
|
|
104
|
+
if (opts.credentials) {
|
|
105
|
+
response.headers.set("Access-Control-Allow-Credentials", "true");
|
|
106
|
+
}
|
|
107
|
+
if (opts.maxAge) {
|
|
108
|
+
response.headers.set("Access-Control-Max-Age", String(opts.maxAge));
|
|
109
|
+
}
|
|
110
|
+
return response;
|
|
111
|
+
}
|
|
112
|
+
function corsPreflightResponse(options = {}) {
|
|
113
|
+
return withCors(new Response(null, { status: 204 }), options);
|
|
114
|
+
}
|
|
115
|
+
function buildCorsHeaders(options = {}) {
|
|
116
|
+
const opts = { ...DEFAULT_CORS, ...options };
|
|
117
|
+
const headers = {
|
|
118
|
+
"Access-Control-Allow-Origin": opts.origin ?? "*",
|
|
119
|
+
"Access-Control-Allow-Methods": (opts.methods ?? []).join(", "),
|
|
120
|
+
"Access-Control-Allow-Headers": (opts.headers ?? []).join(", ")
|
|
121
|
+
};
|
|
122
|
+
if (opts.credentials) {
|
|
123
|
+
headers["Access-Control-Allow-Credentials"] = "true";
|
|
124
|
+
}
|
|
125
|
+
if (opts.maxAge) {
|
|
126
|
+
headers["Access-Control-Max-Age"] = String(opts.maxAge);
|
|
127
|
+
}
|
|
128
|
+
return headers;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/shared/storage/interface.ts
|
|
132
|
+
var MAX_SESSIONS_PER_API_KEY = 5;
|
|
133
|
+
|
|
134
|
+
// src/shared/storage/memory.ts
|
|
135
|
+
var DEFAULT_TXN_TTL_MS = 10 * 60 * 1000;
|
|
136
|
+
var DEFAULT_CODE_TTL_MS = 10 * 60 * 1000;
|
|
137
|
+
var DEFAULT_SESSION_TTL_MS = 24 * 60 * 60 * 1000;
|
|
138
|
+
var DEFAULT_RS_TOKEN_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
139
|
+
var MAX_RS_RECORDS = 1e4;
|
|
140
|
+
var MAX_TRANSACTIONS = 1000;
|
|
141
|
+
var MAX_SESSIONS = 1e4;
|
|
142
|
+
var CLEANUP_INTERVAL_MS = 60000;
|
|
143
|
+
function evictOldest(map, maxSize, countToRemove = 1) {
|
|
144
|
+
if (map.size < maxSize)
|
|
145
|
+
return;
|
|
146
|
+
const entries = [...map.entries()].sort((a, b) => {
|
|
147
|
+
const aTime = a[1].created_at ?? a[1].createdAt ?? 0;
|
|
148
|
+
const bTime = b[1].created_at ?? b[1].createdAt ?? 0;
|
|
149
|
+
return aTime - bTime;
|
|
150
|
+
});
|
|
151
|
+
for (let i = 0;i < countToRemove && i < entries.length; i++) {
|
|
152
|
+
map.delete(entries[i][0]);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function cleanupExpired(map) {
|
|
156
|
+
const now = Date.now();
|
|
157
|
+
let removed = 0;
|
|
158
|
+
for (const [key, entry] of map) {
|
|
159
|
+
if (now >= entry.expiresAt) {
|
|
160
|
+
map.delete(key);
|
|
161
|
+
removed++;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return removed;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
class MemoryTokenStore {
|
|
168
|
+
rsAccessMap = new Map;
|
|
169
|
+
rsRefreshMap = new Map;
|
|
170
|
+
transactions = new Map;
|
|
171
|
+
codes = new Map;
|
|
172
|
+
cleanupIntervalId = null;
|
|
173
|
+
constructor() {
|
|
174
|
+
this.startCleanup();
|
|
175
|
+
}
|
|
176
|
+
startCleanup() {
|
|
177
|
+
if (this.cleanupIntervalId)
|
|
178
|
+
return;
|
|
179
|
+
this.cleanupIntervalId = setInterval(() => {
|
|
180
|
+
this.cleanup();
|
|
181
|
+
}, CLEANUP_INTERVAL_MS);
|
|
182
|
+
if (typeof this.cleanupIntervalId === "object" && "unref" in this.cleanupIntervalId) {
|
|
183
|
+
this.cleanupIntervalId.unref();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
stopCleanup() {
|
|
187
|
+
if (this.cleanupIntervalId) {
|
|
188
|
+
clearInterval(this.cleanupIntervalId);
|
|
189
|
+
this.cleanupIntervalId = null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
cleanup() {
|
|
193
|
+
const now = Date.now();
|
|
194
|
+
let tokensRemoved = 0;
|
|
195
|
+
for (const [key, entry] of this.rsAccessMap) {
|
|
196
|
+
if (now >= entry.expiresAt) {
|
|
197
|
+
this.rsAccessMap.delete(key);
|
|
198
|
+
tokensRemoved++;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
for (const [key, entry] of this.rsRefreshMap) {
|
|
202
|
+
if (now >= entry.expiresAt) {
|
|
203
|
+
this.rsRefreshMap.delete(key);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const transactionsRemoved = cleanupExpired(this.transactions);
|
|
207
|
+
const codesRemoved = cleanupExpired(this.codes);
|
|
208
|
+
return {
|
|
209
|
+
tokens: tokensRemoved,
|
|
210
|
+
transactions: transactionsRemoved,
|
|
211
|
+
codes: codesRemoved
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
async storeRsMapping(rsAccess, provider, rsRefresh, ttlMs = DEFAULT_RS_TOKEN_TTL_MS) {
|
|
215
|
+
const now = Date.now();
|
|
216
|
+
const expiresAt = now + ttlMs;
|
|
217
|
+
evictOldest(this.rsAccessMap, MAX_RS_RECORDS, 10);
|
|
218
|
+
if (rsRefresh) {
|
|
219
|
+
const existing = this.rsRefreshMap.get(rsRefresh);
|
|
220
|
+
if (existing) {
|
|
221
|
+
this.rsAccessMap.delete(existing.rs_access_token);
|
|
222
|
+
existing.rs_access_token = rsAccess;
|
|
223
|
+
existing.provider = { ...provider };
|
|
224
|
+
existing.expiresAt = expiresAt;
|
|
225
|
+
this.rsAccessMap.set(rsAccess, existing);
|
|
226
|
+
return existing;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const record = {
|
|
230
|
+
rs_access_token: rsAccess,
|
|
231
|
+
rs_refresh_token: rsRefresh ?? crypto.randomUUID(),
|
|
232
|
+
provider: { ...provider },
|
|
233
|
+
created_at: now,
|
|
234
|
+
expiresAt
|
|
235
|
+
};
|
|
236
|
+
this.rsAccessMap.set(record.rs_access_token, record);
|
|
237
|
+
this.rsRefreshMap.set(record.rs_refresh_token, record);
|
|
238
|
+
return record;
|
|
239
|
+
}
|
|
240
|
+
async getByRsAccess(rsAccess) {
|
|
241
|
+
const entry = this.rsAccessMap.get(rsAccess);
|
|
242
|
+
if (!entry)
|
|
243
|
+
return null;
|
|
244
|
+
if (Date.now() >= entry.expiresAt) {
|
|
245
|
+
this.rsAccessMap.delete(rsAccess);
|
|
246
|
+
this.rsRefreshMap.delete(entry.rs_refresh_token);
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
return entry;
|
|
250
|
+
}
|
|
251
|
+
async getByRsRefresh(rsRefresh) {
|
|
252
|
+
const entry = this.rsRefreshMap.get(rsRefresh);
|
|
253
|
+
if (!entry)
|
|
254
|
+
return null;
|
|
255
|
+
if (Date.now() >= entry.expiresAt) {
|
|
256
|
+
this.rsAccessMap.delete(entry.rs_access_token);
|
|
257
|
+
this.rsRefreshMap.delete(rsRefresh);
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
return entry;
|
|
261
|
+
}
|
|
262
|
+
async updateByRsRefresh(rsRefresh, provider, maybeNewRsAccess, ttlMs = DEFAULT_RS_TOKEN_TTL_MS) {
|
|
263
|
+
const rec = this.rsRefreshMap.get(rsRefresh);
|
|
264
|
+
if (!rec)
|
|
265
|
+
return null;
|
|
266
|
+
const now = Date.now();
|
|
267
|
+
if (maybeNewRsAccess) {
|
|
268
|
+
this.rsAccessMap.delete(rec.rs_access_token);
|
|
269
|
+
rec.rs_access_token = maybeNewRsAccess;
|
|
270
|
+
rec.created_at = now;
|
|
271
|
+
}
|
|
272
|
+
rec.provider = { ...provider };
|
|
273
|
+
rec.expiresAt = now + ttlMs;
|
|
274
|
+
this.rsAccessMap.set(rec.rs_access_token, rec);
|
|
275
|
+
this.rsRefreshMap.set(rsRefresh, rec);
|
|
276
|
+
return rec;
|
|
277
|
+
}
|
|
278
|
+
async saveTransaction(txnId, txn, ttlSeconds) {
|
|
279
|
+
const ttlMs = ttlSeconds ? ttlSeconds * 1000 : DEFAULT_TXN_TTL_MS;
|
|
280
|
+
const now = Date.now();
|
|
281
|
+
evictOldest(this.transactions, MAX_TRANSACTIONS, 10);
|
|
282
|
+
this.transactions.set(txnId, {
|
|
283
|
+
value: txn,
|
|
284
|
+
expiresAt: now + ttlMs,
|
|
285
|
+
createdAt: now
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
async getTransaction(txnId) {
|
|
289
|
+
const entry = this.transactions.get(txnId);
|
|
290
|
+
if (!entry)
|
|
291
|
+
return null;
|
|
292
|
+
if (Date.now() >= entry.expiresAt) {
|
|
293
|
+
this.transactions.delete(txnId);
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
return entry.value;
|
|
297
|
+
}
|
|
298
|
+
async deleteTransaction(txnId) {
|
|
299
|
+
this.transactions.delete(txnId);
|
|
300
|
+
}
|
|
301
|
+
async saveCode(code, txnId, ttlSeconds) {
|
|
302
|
+
const ttlMs = ttlSeconds ? ttlSeconds * 1000 : DEFAULT_CODE_TTL_MS;
|
|
303
|
+
const now = Date.now();
|
|
304
|
+
this.codes.set(code, {
|
|
305
|
+
value: txnId,
|
|
306
|
+
expiresAt: now + ttlMs,
|
|
307
|
+
createdAt: now
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
async getTxnIdByCode(code) {
|
|
311
|
+
const entry = this.codes.get(code);
|
|
312
|
+
if (!entry)
|
|
313
|
+
return null;
|
|
314
|
+
if (Date.now() >= entry.expiresAt) {
|
|
315
|
+
this.codes.delete(code);
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
return entry.value;
|
|
319
|
+
}
|
|
320
|
+
async deleteCode(code) {
|
|
321
|
+
this.codes.delete(code);
|
|
322
|
+
}
|
|
323
|
+
getStats() {
|
|
324
|
+
return {
|
|
325
|
+
rsTokens: this.rsAccessMap.size,
|
|
326
|
+
transactions: this.transactions.size,
|
|
327
|
+
codes: this.codes.size
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
class MemorySessionStore {
|
|
333
|
+
sessions = new Map;
|
|
334
|
+
cleanupIntervalId = null;
|
|
335
|
+
constructor() {
|
|
336
|
+
this.startCleanup();
|
|
337
|
+
}
|
|
338
|
+
startCleanup() {
|
|
339
|
+
if (this.cleanupIntervalId)
|
|
340
|
+
return;
|
|
341
|
+
this.cleanupIntervalId = setInterval(() => {
|
|
342
|
+
this.cleanup();
|
|
343
|
+
}, CLEANUP_INTERVAL_MS);
|
|
344
|
+
if (typeof this.cleanupIntervalId === "object" && "unref" in this.cleanupIntervalId) {
|
|
345
|
+
this.cleanupIntervalId.unref();
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
stopCleanup() {
|
|
349
|
+
if (this.cleanupIntervalId) {
|
|
350
|
+
clearInterval(this.cleanupIntervalId);
|
|
351
|
+
this.cleanupIntervalId = null;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
cleanup() {
|
|
355
|
+
const now = Date.now();
|
|
356
|
+
let removed = 0;
|
|
357
|
+
for (const [sessionId, session] of this.sessions) {
|
|
358
|
+
if (now >= session.expiresAt) {
|
|
359
|
+
this.sessions.delete(sessionId);
|
|
360
|
+
removed++;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return removed;
|
|
364
|
+
}
|
|
365
|
+
async create(sessionId, apiKey, ttlMs = DEFAULT_SESSION_TTL_MS) {
|
|
366
|
+
const count = await this.countByApiKey(apiKey);
|
|
367
|
+
if (count >= MAX_SESSIONS_PER_API_KEY) {
|
|
368
|
+
await this.deleteOldestByApiKey(apiKey);
|
|
369
|
+
}
|
|
370
|
+
if (this.sessions.size >= MAX_SESSIONS) {
|
|
371
|
+
const oldest = [...this.sessions.entries()].sort((a, b) => a[1].created_at - b[1].created_at)[0];
|
|
372
|
+
if (oldest) {
|
|
373
|
+
this.sessions.delete(oldest[0]);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const now = Date.now();
|
|
377
|
+
const record = {
|
|
378
|
+
sessionId,
|
|
379
|
+
apiKey,
|
|
380
|
+
created_at: now,
|
|
381
|
+
last_accessed: now,
|
|
382
|
+
initialized: false,
|
|
383
|
+
expiresAt: now + ttlMs
|
|
384
|
+
};
|
|
385
|
+
this.sessions.set(sessionId, record);
|
|
386
|
+
return record;
|
|
387
|
+
}
|
|
388
|
+
async get(sessionId) {
|
|
389
|
+
const session = this.sessions.get(sessionId);
|
|
390
|
+
if (!session)
|
|
391
|
+
return null;
|
|
392
|
+
const now = Date.now();
|
|
393
|
+
if (now >= session.expiresAt) {
|
|
394
|
+
this.sessions.delete(sessionId);
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
session.last_accessed = now;
|
|
398
|
+
return session;
|
|
399
|
+
}
|
|
400
|
+
async update(sessionId, data) {
|
|
401
|
+
const session = this.sessions.get(sessionId);
|
|
402
|
+
if (!session)
|
|
403
|
+
return;
|
|
404
|
+
const now = Date.now();
|
|
405
|
+
Object.assign(session, data, { last_accessed: now });
|
|
406
|
+
}
|
|
407
|
+
async delete(sessionId) {
|
|
408
|
+
this.sessions.delete(sessionId);
|
|
409
|
+
}
|
|
410
|
+
async getByApiKey(apiKey) {
|
|
411
|
+
const results = [];
|
|
412
|
+
const now = Date.now();
|
|
413
|
+
for (const session of this.sessions.values()) {
|
|
414
|
+
if (session.apiKey === apiKey && now < session.expiresAt) {
|
|
415
|
+
results.push(session);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return results.sort((a, b) => b.last_accessed - a.last_accessed);
|
|
419
|
+
}
|
|
420
|
+
async countByApiKey(apiKey) {
|
|
421
|
+
let count = 0;
|
|
422
|
+
const now = Date.now();
|
|
423
|
+
for (const session of this.sessions.values()) {
|
|
424
|
+
if (session.apiKey === apiKey && now < session.expiresAt) {
|
|
425
|
+
count++;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return count;
|
|
429
|
+
}
|
|
430
|
+
async deleteOldestByApiKey(apiKey) {
|
|
431
|
+
let oldest = null;
|
|
432
|
+
const now = Date.now();
|
|
433
|
+
for (const session of this.sessions.values()) {
|
|
434
|
+
if (session.apiKey === apiKey && now < session.expiresAt) {
|
|
435
|
+
if (!oldest || session.last_accessed < oldest.last_accessed) {
|
|
436
|
+
oldest = session;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (oldest) {
|
|
441
|
+
this.sessions.delete(oldest.sessionId);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
async ensure(sessionId, ttlMs = DEFAULT_SESSION_TTL_MS) {
|
|
445
|
+
const existing = this.sessions.get(sessionId);
|
|
446
|
+
if (existing) {
|
|
447
|
+
existing.expiresAt = Date.now() + ttlMs;
|
|
448
|
+
existing.last_accessed = Date.now();
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
if (this.sessions.size >= MAX_SESSIONS) {
|
|
452
|
+
const oldest = [...this.sessions.entries()].sort((a, b) => a[1].created_at - b[1].created_at)[0];
|
|
453
|
+
if (oldest) {
|
|
454
|
+
this.sessions.delete(oldest[0]);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
const now = Date.now();
|
|
458
|
+
this.sessions.set(sessionId, {
|
|
459
|
+
sessionId,
|
|
460
|
+
created_at: now,
|
|
461
|
+
last_accessed: now,
|
|
462
|
+
expiresAt: now + ttlMs
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
async put(sessionId, value, ttlMs = DEFAULT_SESSION_TTL_MS) {
|
|
466
|
+
const now = Date.now();
|
|
467
|
+
this.sessions.set(sessionId, {
|
|
468
|
+
...value,
|
|
469
|
+
sessionId,
|
|
470
|
+
last_accessed: value.last_accessed ?? now,
|
|
471
|
+
expiresAt: now + ttlMs
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
getSessionCount() {
|
|
475
|
+
return this.sessions.size;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// src/shared/storage/kv.ts
|
|
480
|
+
function ttl(seconds) {
|
|
481
|
+
return Math.floor(Date.now() / 1000) + seconds;
|
|
482
|
+
}
|
|
483
|
+
function toJson(value) {
|
|
484
|
+
return JSON.stringify(value);
|
|
485
|
+
}
|
|
486
|
+
function fromJson(value) {
|
|
487
|
+
if (!value) {
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
try {
|
|
491
|
+
return JSON.parse(value);
|
|
492
|
+
} catch {
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
class KvTokenStore {
|
|
498
|
+
kv;
|
|
499
|
+
encrypt;
|
|
500
|
+
decrypt;
|
|
501
|
+
fallback;
|
|
502
|
+
constructor(kv, options) {
|
|
503
|
+
this.kv = kv;
|
|
504
|
+
this.encrypt = options?.encrypt ?? ((s) => s);
|
|
505
|
+
this.decrypt = options?.decrypt ?? ((s) => s);
|
|
506
|
+
this.fallback = options?.fallback ?? new MemoryTokenStore;
|
|
507
|
+
}
|
|
508
|
+
async putJson(key, value, options) {
|
|
509
|
+
try {
|
|
510
|
+
const raw = await this.encrypt(toJson(value));
|
|
511
|
+
await this.kv.put(key, raw, options);
|
|
512
|
+
} catch (error) {
|
|
513
|
+
console.error("[KV] Write failed:", error.message);
|
|
514
|
+
throw error;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
async getJson(key) {
|
|
518
|
+
const raw = await this.kv.get(key);
|
|
519
|
+
if (!raw) {
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
const plain = await this.decrypt(raw);
|
|
523
|
+
return fromJson(plain);
|
|
524
|
+
}
|
|
525
|
+
async storeRsMapping(rsAccess, provider, rsRefresh) {
|
|
526
|
+
const rec = {
|
|
527
|
+
rs_access_token: rsAccess,
|
|
528
|
+
rs_refresh_token: rsRefresh ?? crypto.randomUUID(),
|
|
529
|
+
provider: { ...provider },
|
|
530
|
+
created_at: Date.now()
|
|
531
|
+
};
|
|
532
|
+
await this.fallback.storeRsMapping(rsAccess, provider, rsRefresh);
|
|
533
|
+
try {
|
|
534
|
+
await Promise.all([
|
|
535
|
+
this.putJson(`rs:access:${rec.rs_access_token}`, rec),
|
|
536
|
+
this.putJson(`rs:refresh:${rec.rs_refresh_token}`, rec)
|
|
537
|
+
]);
|
|
538
|
+
} catch (error) {
|
|
539
|
+
console.warn("[KV] Failed to persist RS mapping (using memory fallback):", error.message);
|
|
540
|
+
}
|
|
541
|
+
return rec;
|
|
542
|
+
}
|
|
543
|
+
async getByRsAccess(rsAccess) {
|
|
544
|
+
const rec = await this.getJson(`rs:access:${rsAccess}`);
|
|
545
|
+
return rec ?? await this.fallback.getByRsAccess(rsAccess);
|
|
546
|
+
}
|
|
547
|
+
async getByRsRefresh(rsRefresh) {
|
|
548
|
+
const rec = await this.getJson(`rs:refresh:${rsRefresh}`);
|
|
549
|
+
return rec ?? await this.fallback.getByRsRefresh(rsRefresh);
|
|
550
|
+
}
|
|
551
|
+
async updateByRsRefresh(rsRefresh, provider, maybeNewRsAccess) {
|
|
552
|
+
const existing = await this.getJson(`rs:refresh:${rsRefresh}`);
|
|
553
|
+
if (!existing) {
|
|
554
|
+
return this.fallback.updateByRsRefresh(rsRefresh, provider, maybeNewRsAccess);
|
|
555
|
+
}
|
|
556
|
+
const rsAccessChanged = maybeNewRsAccess && maybeNewRsAccess !== existing.rs_access_token;
|
|
557
|
+
const next = {
|
|
558
|
+
rs_access_token: maybeNewRsAccess || existing.rs_access_token,
|
|
559
|
+
rs_refresh_token: rsRefresh,
|
|
560
|
+
provider: { ...provider },
|
|
561
|
+
created_at: Date.now()
|
|
562
|
+
};
|
|
563
|
+
await this.fallback.updateByRsRefresh(rsRefresh, provider, maybeNewRsAccess);
|
|
564
|
+
try {
|
|
565
|
+
if (rsAccessChanged) {
|
|
566
|
+
await Promise.all([
|
|
567
|
+
this.kv.delete(`rs:access:${existing.rs_access_token}`),
|
|
568
|
+
this.putJson(`rs:access:${next.rs_access_token}`, next),
|
|
569
|
+
this.putJson(`rs:refresh:${rsRefresh}`, next)
|
|
570
|
+
]);
|
|
571
|
+
} else {
|
|
572
|
+
await Promise.all([
|
|
573
|
+
this.putJson(`rs:access:${existing.rs_access_token}`, next),
|
|
574
|
+
this.putJson(`rs:refresh:${rsRefresh}`, next)
|
|
575
|
+
]);
|
|
576
|
+
}
|
|
577
|
+
} catch (error) {
|
|
578
|
+
console.warn("[KV] Failed to update RS mapping (using memory fallback):", error.message);
|
|
579
|
+
}
|
|
580
|
+
return next;
|
|
581
|
+
}
|
|
582
|
+
async saveTransaction(txnId, txn, ttlSeconds = 600) {
|
|
583
|
+
await this.fallback.saveTransaction(txnId, txn);
|
|
584
|
+
try {
|
|
585
|
+
await this.putJson(`txn:${txnId}`, txn, { expiration: ttl(ttlSeconds) });
|
|
586
|
+
} catch (error) {
|
|
587
|
+
console.warn("[KV] Failed to save transaction (using memory):", error.message);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
async getTransaction(txnId) {
|
|
591
|
+
const txn = await this.getJson(`txn:${txnId}`);
|
|
592
|
+
return txn ?? await this.fallback.getTransaction(txnId);
|
|
593
|
+
}
|
|
594
|
+
async deleteTransaction(txnId) {
|
|
595
|
+
await this.fallback.deleteTransaction(txnId);
|
|
596
|
+
}
|
|
597
|
+
async saveCode(code, txnId, ttlSeconds = 600) {
|
|
598
|
+
await this.fallback.saveCode(code, txnId);
|
|
599
|
+
try {
|
|
600
|
+
await this.putJson(`code:${code}`, { v: txnId }, { expiration: ttl(ttlSeconds) });
|
|
601
|
+
} catch (error) {
|
|
602
|
+
console.warn("[KV] Failed to save code (using memory):", error.message);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
async getTxnIdByCode(code) {
|
|
606
|
+
const obj = await this.getJson(`code:${code}`);
|
|
607
|
+
return obj?.v ?? await this.fallback.getTxnIdByCode(code);
|
|
608
|
+
}
|
|
609
|
+
async deleteCode(code) {
|
|
610
|
+
await this.fallback.deleteCode(code);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
var SESSION_KEY_PREFIX = "session:";
|
|
614
|
+
var SESSION_APIKEY_PREFIX = "session:apikey:";
|
|
615
|
+
var SESSION_TTL_SECONDS = 24 * 60 * 60;
|
|
616
|
+
|
|
617
|
+
class KvSessionStore {
|
|
618
|
+
kv;
|
|
619
|
+
encrypt;
|
|
620
|
+
decrypt;
|
|
621
|
+
fallback;
|
|
622
|
+
constructor(kv, options) {
|
|
623
|
+
this.kv = kv;
|
|
624
|
+
this.encrypt = options?.encrypt ?? ((s) => s);
|
|
625
|
+
this.decrypt = options?.decrypt ?? ((s) => s);
|
|
626
|
+
this.fallback = options?.fallback ?? new MemorySessionStore;
|
|
627
|
+
}
|
|
628
|
+
async putSession(sessionId, value) {
|
|
629
|
+
const raw = await this.encrypt(toJson(value));
|
|
630
|
+
await this.kv.put(`${SESSION_KEY_PREFIX}${sessionId}`, raw, {
|
|
631
|
+
expiration: ttl(SESSION_TTL_SECONDS)
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
async getSession(sessionId) {
|
|
635
|
+
const raw = await this.kv.get(`${SESSION_KEY_PREFIX}${sessionId}`);
|
|
636
|
+
if (!raw) {
|
|
637
|
+
return this.fallback.get(sessionId);
|
|
638
|
+
}
|
|
639
|
+
const plain = await this.decrypt(raw);
|
|
640
|
+
return fromJson(plain);
|
|
641
|
+
}
|
|
642
|
+
async getApiKeySessionIds(apiKey) {
|
|
643
|
+
const raw = await this.kv.get(`${SESSION_APIKEY_PREFIX}${apiKey}`);
|
|
644
|
+
if (!raw)
|
|
645
|
+
return [];
|
|
646
|
+
return fromJson(raw) ?? [];
|
|
647
|
+
}
|
|
648
|
+
async setApiKeySessionIds(apiKey, sessionIds) {
|
|
649
|
+
if (sessionIds.length === 0) {
|
|
650
|
+
await this.kv.delete(`${SESSION_APIKEY_PREFIX}${apiKey}`);
|
|
651
|
+
} else {
|
|
652
|
+
await this.kv.put(`${SESSION_APIKEY_PREFIX}${apiKey}`, toJson(sessionIds), {
|
|
653
|
+
expiration: ttl(SESSION_TTL_SECONDS)
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
async create(sessionId, apiKey) {
|
|
658
|
+
const currentCount = await this.countByApiKey(apiKey);
|
|
659
|
+
if (currentCount >= MAX_SESSIONS_PER_API_KEY) {
|
|
660
|
+
await this.deleteOldestByApiKey(apiKey);
|
|
661
|
+
}
|
|
662
|
+
const now = Date.now();
|
|
663
|
+
const record = {
|
|
664
|
+
apiKey,
|
|
665
|
+
created_at: now,
|
|
666
|
+
last_accessed: now,
|
|
667
|
+
initialized: false
|
|
668
|
+
};
|
|
669
|
+
await this.putSession(sessionId, record);
|
|
670
|
+
await this.fallback.create(sessionId, apiKey);
|
|
671
|
+
const sessionIds = await this.getApiKeySessionIds(apiKey);
|
|
672
|
+
if (!sessionIds.includes(sessionId)) {
|
|
673
|
+
sessionIds.push(sessionId);
|
|
674
|
+
await this.setApiKeySessionIds(apiKey, sessionIds);
|
|
675
|
+
}
|
|
676
|
+
return record;
|
|
677
|
+
}
|
|
678
|
+
async get(sessionId) {
|
|
679
|
+
const session = await this.getSession(sessionId);
|
|
680
|
+
if (!session)
|
|
681
|
+
return null;
|
|
682
|
+
const now = Date.now();
|
|
683
|
+
session.last_accessed = now;
|
|
684
|
+
this.putSession(sessionId, session).catch(() => {});
|
|
685
|
+
return session;
|
|
686
|
+
}
|
|
687
|
+
async update(sessionId, data) {
|
|
688
|
+
const session = await this.getSession(sessionId);
|
|
689
|
+
if (!session)
|
|
690
|
+
return;
|
|
691
|
+
const updated = {
|
|
692
|
+
...session,
|
|
693
|
+
...data,
|
|
694
|
+
last_accessed: Date.now()
|
|
695
|
+
};
|
|
696
|
+
await this.putSession(sessionId, updated);
|
|
697
|
+
await this.fallback.update(sessionId, data);
|
|
698
|
+
}
|
|
699
|
+
async delete(sessionId) {
|
|
700
|
+
const session = await this.getSession(sessionId);
|
|
701
|
+
await this.kv.delete(`${SESSION_KEY_PREFIX}${sessionId}`);
|
|
702
|
+
await this.fallback.delete(sessionId);
|
|
703
|
+
if (session?.apiKey) {
|
|
704
|
+
const sessionIds = await this.getApiKeySessionIds(session.apiKey);
|
|
705
|
+
const filtered = sessionIds.filter((id) => id !== sessionId);
|
|
706
|
+
await this.setApiKeySessionIds(session.apiKey, filtered);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
async getByApiKey(apiKey) {
|
|
710
|
+
const sessionIds = await this.getApiKeySessionIds(apiKey);
|
|
711
|
+
const sessions = [];
|
|
712
|
+
for (const sessionId of sessionIds) {
|
|
713
|
+
const session = await this.getSession(sessionId);
|
|
714
|
+
if (session) {
|
|
715
|
+
sessions.push(session);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return sessions.sort((a, b) => b.last_accessed - a.last_accessed);
|
|
719
|
+
}
|
|
720
|
+
async countByApiKey(apiKey) {
|
|
721
|
+
const sessionIds = await this.getApiKeySessionIds(apiKey);
|
|
722
|
+
return sessionIds.length;
|
|
723
|
+
}
|
|
724
|
+
async deleteOldestByApiKey(apiKey) {
|
|
725
|
+
const sessions = await this.getByApiKey(apiKey);
|
|
726
|
+
if (sessions.length === 0)
|
|
727
|
+
return;
|
|
728
|
+
const oldest = sessions[sessions.length - 1];
|
|
729
|
+
const sessionIds = await this.getApiKeySessionIds(apiKey);
|
|
730
|
+
for (const sessionId of sessionIds) {
|
|
731
|
+
const session = await this.getSession(sessionId);
|
|
732
|
+
if (session && session.created_at === oldest.created_at) {
|
|
733
|
+
await this.delete(sessionId);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
async ensure(sessionId) {
|
|
739
|
+
const existing = await this.fallback.get(sessionId);
|
|
740
|
+
if (!existing) {
|
|
741
|
+
const now = Date.now();
|
|
742
|
+
await this.fallback.put(sessionId, {
|
|
743
|
+
created_at: now,
|
|
744
|
+
last_accessed: now
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
async put(sessionId, value) {
|
|
749
|
+
await this.putSession(sessionId, value);
|
|
750
|
+
await this.fallback.put(sessionId, value);
|
|
751
|
+
if (value.apiKey) {
|
|
752
|
+
const sessionIds = await this.getApiKeySessionIds(value.apiKey);
|
|
753
|
+
if (!sessionIds.includes(sessionId)) {
|
|
754
|
+
sessionIds.push(sessionId);
|
|
755
|
+
await this.setApiKeySessionIds(value.apiKey, sessionIds);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// src/shared/storage/singleton.ts
|
|
762
|
+
var tokenStoreInstance = null;
|
|
763
|
+
var sessionStoreInstance = null;
|
|
764
|
+
function initializeStorage(tokenStore, sessionStore) {
|
|
765
|
+
tokenStoreInstance = tokenStore;
|
|
766
|
+
sessionStoreInstance = sessionStore;
|
|
767
|
+
}
|
|
768
|
+
function getTokenStore() {
|
|
769
|
+
if (!tokenStoreInstance) {
|
|
770
|
+
throw new Error("TokenStore not initialized. Call initializeStorage first.");
|
|
771
|
+
}
|
|
772
|
+
return tokenStoreInstance;
|
|
773
|
+
}
|
|
774
|
+
function getSessionStore() {
|
|
775
|
+
if (!sessionStoreInstance) {
|
|
776
|
+
throw new Error("SessionStore not initialized. Call initializeStorage first.");
|
|
777
|
+
}
|
|
778
|
+
return sessionStoreInstance;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// src/shared/utils/logger.ts
|
|
782
|
+
var LOG_LEVELS = {
|
|
783
|
+
debug: 0,
|
|
784
|
+
info: 1,
|
|
785
|
+
warning: 2,
|
|
786
|
+
error: 3
|
|
787
|
+
};
|
|
788
|
+
var currentLevel = "info";
|
|
789
|
+
function shouldLog(level) {
|
|
790
|
+
return LOG_LEVELS[level] >= LOG_LEVELS[currentLevel];
|
|
791
|
+
}
|
|
792
|
+
function formatLog(level, logger, data) {
|
|
793
|
+
const timestamp = new Date().toISOString();
|
|
794
|
+
const { message, ...rest } = data;
|
|
795
|
+
const extra = Object.keys(rest).length > 0 ? ` ${JSON.stringify(rest)}` : "";
|
|
796
|
+
return `[${timestamp}] ${level.toUpperCase()} [${logger}] ${message}${extra}`;
|
|
797
|
+
}
|
|
798
|
+
function sanitize(data) {
|
|
799
|
+
const sanitized = { ...data };
|
|
800
|
+
const sensitiveKeys = [
|
|
801
|
+
"password",
|
|
802
|
+
"token",
|
|
803
|
+
"secret",
|
|
804
|
+
"key",
|
|
805
|
+
"authorization",
|
|
806
|
+
"access_token",
|
|
807
|
+
"refresh_token"
|
|
808
|
+
];
|
|
809
|
+
for (const key of Object.keys(sanitized)) {
|
|
810
|
+
if (sensitiveKeys.some((sk) => key.toLowerCase().includes(sk))) {
|
|
811
|
+
const value = sanitized[key];
|
|
812
|
+
if (typeof value === "string" && value.length > 8) {
|
|
813
|
+
sanitized[key] = `${value.substring(0, 8)}...`;
|
|
814
|
+
} else {
|
|
815
|
+
sanitized[key] = "[REDACTED]";
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
return sanitized;
|
|
820
|
+
}
|
|
821
|
+
var sharedLogger = {
|
|
822
|
+
setLevel(level) {
|
|
823
|
+
currentLevel = level;
|
|
824
|
+
},
|
|
825
|
+
debug(logger, data) {
|
|
826
|
+
if (shouldLog("debug")) {
|
|
827
|
+
console.log(formatLog("debug", logger, sanitize(data)));
|
|
828
|
+
}
|
|
829
|
+
},
|
|
830
|
+
info(logger, data) {
|
|
831
|
+
if (shouldLog("info")) {
|
|
832
|
+
console.log(formatLog("info", logger, sanitize(data)));
|
|
833
|
+
}
|
|
834
|
+
},
|
|
835
|
+
warning(logger, data) {
|
|
836
|
+
if (shouldLog("warning")) {
|
|
837
|
+
console.warn(formatLog("warning", logger, sanitize(data)));
|
|
838
|
+
}
|
|
839
|
+
},
|
|
840
|
+
error(logger, data) {
|
|
841
|
+
if (shouldLog("error")) {
|
|
842
|
+
console.error(formatLog("error", logger, sanitize(data)));
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
var logger = sharedLogger;
|
|
847
|
+
|
|
848
|
+
// src/shared/http/response.ts
|
|
849
|
+
function jsonResponse(data, options = {}) {
|
|
850
|
+
const { status = 200, headers = {}, cors = true } = options;
|
|
851
|
+
const response = new Response(JSON.stringify(data), {
|
|
852
|
+
status,
|
|
853
|
+
headers: {
|
|
854
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
855
|
+
...headers
|
|
856
|
+
}
|
|
857
|
+
});
|
|
858
|
+
if (cors) {
|
|
859
|
+
return withCors(response, typeof cors === "object" ? cors : undefined);
|
|
860
|
+
}
|
|
861
|
+
return response;
|
|
862
|
+
}
|
|
863
|
+
function textError(message, options = {}) {
|
|
864
|
+
const { status = 400, cors = true } = options;
|
|
865
|
+
const response = new Response(message, { status });
|
|
866
|
+
if (cors) {
|
|
867
|
+
return withCors(response, typeof cors === "object" ? cors : undefined);
|
|
868
|
+
}
|
|
869
|
+
return response;
|
|
870
|
+
}
|
|
871
|
+
function oauthError(error, description, options = {}) {
|
|
872
|
+
const body = { error };
|
|
873
|
+
if (description) {
|
|
874
|
+
body.error_description = description;
|
|
875
|
+
}
|
|
876
|
+
return jsonResponse(body, {
|
|
877
|
+
status: options.status ?? 400,
|
|
878
|
+
cors: options.cors
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
function redirectResponse(url, status = 302) {
|
|
882
|
+
return Response.redirect(url, status);
|
|
883
|
+
}
|
|
884
|
+
var JsonRpcErrorCode = {
|
|
885
|
+
ParseError: -32700,
|
|
886
|
+
InvalidRequest: -32600,
|
|
887
|
+
MethodNotFound: -32601,
|
|
888
|
+
InvalidParams: -32602,
|
|
889
|
+
InternalError: -32603,
|
|
890
|
+
ServerError: -32000
|
|
891
|
+
};
|
|
892
|
+
|
|
893
|
+
// src/shared/utils/cancellation.ts
|
|
894
|
+
class CancellationError extends Error {
|
|
895
|
+
constructor(message = "Operation was cancelled") {
|
|
896
|
+
super(message);
|
|
897
|
+
this.name = "CancellationError";
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
class CancellationToken {
|
|
902
|
+
_isCancelled = false;
|
|
903
|
+
_listeners = [];
|
|
904
|
+
get isCancelled() {
|
|
905
|
+
return this._isCancelled;
|
|
906
|
+
}
|
|
907
|
+
cancel() {
|
|
908
|
+
if (this._isCancelled)
|
|
909
|
+
return;
|
|
910
|
+
this._isCancelled = true;
|
|
911
|
+
this._listeners.forEach((listener) => {
|
|
912
|
+
try {
|
|
913
|
+
listener();
|
|
914
|
+
} catch (error) {
|
|
915
|
+
console.error("Error in cancellation listener:", error);
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
this._listeners.length = 0;
|
|
919
|
+
}
|
|
920
|
+
onCancelled(listener) {
|
|
921
|
+
if (this._isCancelled) {
|
|
922
|
+
listener();
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
this._listeners.push(listener);
|
|
926
|
+
}
|
|
927
|
+
throwIfCancelled() {
|
|
928
|
+
if (this._isCancelled) {
|
|
929
|
+
throw new CancellationError;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
function createCancellationToken() {
|
|
934
|
+
return new CancellationToken;
|
|
935
|
+
}
|
|
936
|
+
async function withCancellation(operation, token) {
|
|
937
|
+
token.throwIfCancelled();
|
|
938
|
+
return new Promise((resolve, reject) => {
|
|
939
|
+
let completed = false;
|
|
940
|
+
token.onCancelled(() => {
|
|
941
|
+
if (!completed) {
|
|
942
|
+
completed = true;
|
|
943
|
+
reject(new CancellationError);
|
|
944
|
+
}
|
|
945
|
+
});
|
|
946
|
+
operation(token).then((result) => {
|
|
947
|
+
if (!completed) {
|
|
948
|
+
completed = true;
|
|
949
|
+
resolve(result);
|
|
950
|
+
}
|
|
951
|
+
}).catch((error) => {
|
|
952
|
+
if (!completed) {
|
|
953
|
+
completed = true;
|
|
954
|
+
reject(error);
|
|
955
|
+
}
|
|
956
|
+
});
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// src/shared/types/provider.ts
|
|
961
|
+
function toProviderInfo(tokens) {
|
|
962
|
+
return {
|
|
963
|
+
accessToken: tokens.access_token,
|
|
964
|
+
refreshToken: tokens.refresh_token,
|
|
965
|
+
expiresAt: tokens.expires_at,
|
|
966
|
+
scopes: tokens.scopes
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
function toProviderTokens(info) {
|
|
970
|
+
return {
|
|
971
|
+
access_token: info.accessToken,
|
|
972
|
+
refresh_token: info.refreshToken,
|
|
973
|
+
expires_at: info.expiresAt,
|
|
974
|
+
scopes: info.scopes
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// src/shared/tools/types.ts
|
|
979
|
+
function assertProviderToken(context) {
|
|
980
|
+
if (!context.providerToken) {
|
|
981
|
+
throw new Error("Authentication required");
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
function defineTool(def) {
|
|
985
|
+
return def;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// src/shared/tools/echo.ts
|
|
989
|
+
import { z } from "zod";
|
|
990
|
+
var echoInputSchema = z.object({
|
|
991
|
+
message: z.string().min(1).describe("Message to echo back"),
|
|
992
|
+
uppercase: z.boolean().optional().describe("Convert message to uppercase")
|
|
993
|
+
});
|
|
994
|
+
var echoTool = defineTool({
|
|
995
|
+
name: "echo",
|
|
996
|
+
title: "Echo",
|
|
997
|
+
description: "Echo back a message, optionally transformed",
|
|
998
|
+
inputSchema: echoInputSchema,
|
|
999
|
+
outputSchema: {
|
|
1000
|
+
echoed: z.string().describe("The echoed message"),
|
|
1001
|
+
length: z.number().describe("Message length")
|
|
1002
|
+
},
|
|
1003
|
+
annotations: {
|
|
1004
|
+
title: "Echo Message",
|
|
1005
|
+
readOnlyHint: true,
|
|
1006
|
+
destructiveHint: false,
|
|
1007
|
+
idempotentHint: true,
|
|
1008
|
+
openWorldHint: false
|
|
1009
|
+
},
|
|
1010
|
+
handler: async (args) => {
|
|
1011
|
+
const message = args.message;
|
|
1012
|
+
const echoed = args.uppercase ? message.toUpperCase() : message;
|
|
1013
|
+
const result = {
|
|
1014
|
+
echoed,
|
|
1015
|
+
length: echoed.length
|
|
1016
|
+
};
|
|
1017
|
+
return {
|
|
1018
|
+
content: [{ type: "text", text: echoed }],
|
|
1019
|
+
structuredContent: result
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
// src/shared/tools/health.ts
|
|
1025
|
+
import { z as z2 } from "zod";
|
|
1026
|
+
var healthInputSchema = z2.object({
|
|
1027
|
+
verbose: z2.boolean().optional().describe("Include additional runtime details")
|
|
1028
|
+
});
|
|
1029
|
+
var healthTool = defineTool({
|
|
1030
|
+
name: "health",
|
|
1031
|
+
title: "Health Check",
|
|
1032
|
+
description: "Check server health, uptime, and runtime information",
|
|
1033
|
+
inputSchema: healthInputSchema,
|
|
1034
|
+
outputSchema: {
|
|
1035
|
+
status: z2.string().describe("Server status"),
|
|
1036
|
+
timestamp: z2.number().describe("Current timestamp"),
|
|
1037
|
+
runtime: z2.string().describe("Runtime environment"),
|
|
1038
|
+
uptime: z2.number().optional().describe("Uptime in seconds (if available)")
|
|
1039
|
+
},
|
|
1040
|
+
annotations: {
|
|
1041
|
+
title: "Server Health Check",
|
|
1042
|
+
readOnlyHint: true,
|
|
1043
|
+
destructiveHint: false,
|
|
1044
|
+
idempotentHint: true,
|
|
1045
|
+
openWorldHint: false
|
|
1046
|
+
},
|
|
1047
|
+
handler: async (args) => {
|
|
1048
|
+
const verbose = Boolean(args.verbose);
|
|
1049
|
+
const g = globalThis;
|
|
1050
|
+
const isWorkers = typeof g.caches !== "undefined" && !("process" in g);
|
|
1051
|
+
const runtime = isWorkers ? "cloudflare-workers" : "node";
|
|
1052
|
+
const result = {
|
|
1053
|
+
status: "ok",
|
|
1054
|
+
timestamp: Date.now(),
|
|
1055
|
+
runtime
|
|
1056
|
+
};
|
|
1057
|
+
if (verbose) {
|
|
1058
|
+
if (!isWorkers && typeof process !== "undefined") {
|
|
1059
|
+
result.uptime = Math.floor(process.uptime());
|
|
1060
|
+
result.nodeVersion = process.version;
|
|
1061
|
+
result.memoryUsage = process.memoryUsage().heapUsed;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
return {
|
|
1065
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
1066
|
+
structuredContent: result
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
// src/runtime/node/context.ts
|
|
1072
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
1073
|
+
var authContextStorage = new AsyncLocalStorage;
|
|
1074
|
+
function getCurrentAuthContext() {
|
|
1075
|
+
return authContextStorage.getStore();
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
class ContextRegistry {
|
|
1079
|
+
contexts = new Map;
|
|
1080
|
+
create(requestId, sessionId, authData) {
|
|
1081
|
+
const context = {
|
|
1082
|
+
sessionId,
|
|
1083
|
+
cancellationToken: createCancellationToken(),
|
|
1084
|
+
requestId,
|
|
1085
|
+
timestamp: Date.now(),
|
|
1086
|
+
authStrategy: authData?.authStrategy,
|
|
1087
|
+
authHeaders: authData?.authHeaders,
|
|
1088
|
+
resolvedHeaders: authData?.resolvedHeaders,
|
|
1089
|
+
rsToken: authData?.rsToken,
|
|
1090
|
+
providerToken: authData?.providerToken,
|
|
1091
|
+
provider: authData?.provider,
|
|
1092
|
+
serviceToken: authData?.serviceToken ?? authData?.providerToken
|
|
1093
|
+
};
|
|
1094
|
+
this.contexts.set(requestId, context);
|
|
1095
|
+
return context;
|
|
1096
|
+
}
|
|
1097
|
+
get(requestId) {
|
|
1098
|
+
return this.contexts.get(requestId);
|
|
1099
|
+
}
|
|
1100
|
+
getCancellationToken(requestId) {
|
|
1101
|
+
return this.contexts.get(requestId)?.cancellationToken;
|
|
1102
|
+
}
|
|
1103
|
+
cancel(requestId, _reason) {
|
|
1104
|
+
const context = this.contexts.get(requestId);
|
|
1105
|
+
if (!context)
|
|
1106
|
+
return false;
|
|
1107
|
+
context.cancellationToken.cancel();
|
|
1108
|
+
return true;
|
|
1109
|
+
}
|
|
1110
|
+
delete(requestId) {
|
|
1111
|
+
return this.contexts.delete(requestId);
|
|
1112
|
+
}
|
|
1113
|
+
deleteBySession(sessionId) {
|
|
1114
|
+
let deleted = 0;
|
|
1115
|
+
for (const [requestId, context] of this.contexts.entries()) {
|
|
1116
|
+
if (context.sessionId === sessionId) {
|
|
1117
|
+
this.contexts.delete(requestId);
|
|
1118
|
+
deleted++;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
if (deleted > 0) {
|
|
1122
|
+
sharedLogger.debug("context_registry", {
|
|
1123
|
+
message: "Cleaned up contexts for session",
|
|
1124
|
+
sessionId,
|
|
1125
|
+
count: deleted
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
return deleted;
|
|
1129
|
+
}
|
|
1130
|
+
get size() {
|
|
1131
|
+
return this.contexts.size;
|
|
1132
|
+
}
|
|
1133
|
+
cleanupExpired(maxAgeMs = 10 * 60 * 1000) {
|
|
1134
|
+
const now = Date.now();
|
|
1135
|
+
let cleaned = 0;
|
|
1136
|
+
for (const [requestId, context] of this.contexts.entries()) {
|
|
1137
|
+
if (now - context.timestamp > maxAgeMs) {
|
|
1138
|
+
this.contexts.delete(requestId);
|
|
1139
|
+
cleaned++;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
if (cleaned > 0) {
|
|
1143
|
+
sharedLogger.warning("context_registry", {
|
|
1144
|
+
message: "Cleaned up expired contexts (this indicates missing cleanup calls)",
|
|
1145
|
+
count: cleaned,
|
|
1146
|
+
maxAgeMs
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
return cleaned;
|
|
1150
|
+
}
|
|
1151
|
+
clear() {
|
|
1152
|
+
this.contexts.clear();
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
var contextRegistry = new ContextRegistry;
|
|
1156
|
+
|
|
1157
|
+
// src/shared/tools/registry.ts
|
|
1158
|
+
function getSchemaShape(schema) {
|
|
1159
|
+
if ("shape" in schema && typeof schema.shape === "object") {
|
|
1160
|
+
return schema.shape;
|
|
1161
|
+
}
|
|
1162
|
+
if ("_def" in schema && schema._def && typeof schema._def === "object") {
|
|
1163
|
+
const def = schema._def;
|
|
1164
|
+
if (def.schema) {
|
|
1165
|
+
return getSchemaShape(def.schema);
|
|
1166
|
+
}
|
|
1167
|
+
if (def.innerType) {
|
|
1168
|
+
return getSchemaShape(def.innerType);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
function asRegisteredTool(tool) {
|
|
1174
|
+
return tool;
|
|
1175
|
+
}
|
|
1176
|
+
var sharedTools = [
|
|
1177
|
+
asRegisteredTool(healthTool),
|
|
1178
|
+
asRegisteredTool(echoTool)
|
|
1179
|
+
];
|
|
1180
|
+
function getSharedTool(name) {
|
|
1181
|
+
return sharedTools.find((t) => t.name === name);
|
|
1182
|
+
}
|
|
1183
|
+
function getSharedToolNames() {
|
|
1184
|
+
return sharedTools.map((t) => t.name);
|
|
1185
|
+
}
|
|
1186
|
+
async function executeSharedTool(name, args, context, tools) {
|
|
1187
|
+
const toolList = tools ?? sharedTools;
|
|
1188
|
+
const tool = toolList.find((t) => t.name === name);
|
|
1189
|
+
if (!tool) {
|
|
1190
|
+
return {
|
|
1191
|
+
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
1192
|
+
isError: true
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
try {
|
|
1196
|
+
if (context.signal?.aborted) {
|
|
1197
|
+
return {
|
|
1198
|
+
content: [{ type: "text", text: "Operation was cancelled" }],
|
|
1199
|
+
isError: true
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
const parseResult = tool.inputSchema.safeParse(args);
|
|
1203
|
+
if (!parseResult.success) {
|
|
1204
|
+
const errors = parseResult.error.issues.map((e) => `${e.path.join(".")}: ${e.message}`).join(", ");
|
|
1205
|
+
return {
|
|
1206
|
+
content: [{ type: "text", text: `Invalid input: ${errors}` }],
|
|
1207
|
+
isError: true
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
const result = await tool.handler(parseResult.data, context);
|
|
1211
|
+
if (tool.outputSchema && !result.isError) {
|
|
1212
|
+
if (!result.structuredContent) {
|
|
1213
|
+
return {
|
|
1214
|
+
content: [
|
|
1215
|
+
{
|
|
1216
|
+
type: "text",
|
|
1217
|
+
text: "Tool with outputSchema must return structuredContent (unless isError is true)"
|
|
1218
|
+
}
|
|
1219
|
+
],
|
|
1220
|
+
isError: true
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
return result;
|
|
1225
|
+
} catch (error) {
|
|
1226
|
+
if (context.signal?.aborted) {
|
|
1227
|
+
return {
|
|
1228
|
+
content: [{ type: "text", text: "Operation was cancelled" }],
|
|
1229
|
+
isError: true
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
return {
|
|
1233
|
+
content: [
|
|
1234
|
+
{ type: "text", text: `Tool error: ${error.message}` }
|
|
1235
|
+
],
|
|
1236
|
+
isError: true
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
function registerTools(server, contextResolver) {
|
|
1241
|
+
for (const tool of sharedTools) {
|
|
1242
|
+
const inputSchemaShape = getSchemaShape(tool.inputSchema);
|
|
1243
|
+
if (!inputSchemaShape) {
|
|
1244
|
+
logger.error("tools", {
|
|
1245
|
+
message: "Failed to extract schema shape",
|
|
1246
|
+
toolName: tool.name
|
|
1247
|
+
});
|
|
1248
|
+
throw new Error(`Failed to extract schema shape for tool: ${tool.name}`);
|
|
1249
|
+
}
|
|
1250
|
+
server.registerTool(tool.name, {
|
|
1251
|
+
description: tool.description,
|
|
1252
|
+
inputSchema: inputSchemaShape,
|
|
1253
|
+
...tool.outputSchema && { outputSchema: tool.outputSchema },
|
|
1254
|
+
...tool.annotations && { annotations: tool.annotations }
|
|
1255
|
+
}, async (args, extra) => {
|
|
1256
|
+
const authContext = extra.requestId && contextResolver ? contextResolver(extra.requestId) : undefined;
|
|
1257
|
+
const ctx = authContext ?? getCurrentAuthContext();
|
|
1258
|
+
const authCtx = ctx;
|
|
1259
|
+
const providerInfo = authCtx?.provider ? toProviderInfo(authCtx.provider) : undefined;
|
|
1260
|
+
const context = {
|
|
1261
|
+
sessionId: extra.sessionId ?? crypto.randomUUID(),
|
|
1262
|
+
signal: extra.signal,
|
|
1263
|
+
meta: {
|
|
1264
|
+
progressToken: extra._meta?.progressToken,
|
|
1265
|
+
requestId: extra.requestId?.toString()
|
|
1266
|
+
},
|
|
1267
|
+
authStrategy: authCtx?.authStrategy,
|
|
1268
|
+
providerToken: authCtx?.providerToken,
|
|
1269
|
+
provider: providerInfo,
|
|
1270
|
+
resolvedHeaders: authCtx?.resolvedHeaders
|
|
1271
|
+
};
|
|
1272
|
+
const result = await executeSharedTool(tool.name, args, context);
|
|
1273
|
+
return result;
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
logger.info("tools", { message: `Registered ${sharedTools.length} tools` });
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// src/shared/mcp/dispatcher.ts
|
|
1280
|
+
import { z as z3 } from "zod";
|
|
1281
|
+
|
|
1282
|
+
// src/runtime/node/capabilities.ts
|
|
1283
|
+
function buildCapabilities() {
|
|
1284
|
+
return {
|
|
1285
|
+
logging: {},
|
|
1286
|
+
prompts: {
|
|
1287
|
+
listChanged: true
|
|
1288
|
+
},
|
|
1289
|
+
resources: {
|
|
1290
|
+
listChanged: true,
|
|
1291
|
+
subscribe: true
|
|
1292
|
+
},
|
|
1293
|
+
tools: {
|
|
1294
|
+
listChanged: true
|
|
1295
|
+
}
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
// src/shared/config/metadata.ts
|
|
1300
|
+
var serverMetadata = {
|
|
1301
|
+
title: "MCP Server",
|
|
1302
|
+
version: "0.0.1",
|
|
1303
|
+
instructions: "Use these tools to interact with the server."
|
|
1304
|
+
};
|
|
1305
|
+
|
|
1306
|
+
// src/shared/mcp/dispatcher.ts
|
|
1307
|
+
var LATEST_PROTOCOL_VERSION = "2025-06-18";
|
|
1308
|
+
var SUPPORTED_PROTOCOL_VERSIONS = [
|
|
1309
|
+
"2025-06-18",
|
|
1310
|
+
"2025-03-26",
|
|
1311
|
+
"2024-11-05",
|
|
1312
|
+
"2024-10-07"
|
|
1313
|
+
];
|
|
1314
|
+
var JsonRpcErrorCode2 = {
|
|
1315
|
+
ParseError: -32700,
|
|
1316
|
+
InvalidRequest: -32600,
|
|
1317
|
+
MethodNotFound: -32601,
|
|
1318
|
+
InvalidParams: -32602,
|
|
1319
|
+
InternalError: -32603
|
|
1320
|
+
};
|
|
1321
|
+
async function handleInitialize(params, ctx) {
|
|
1322
|
+
const clientInfo = params?.clientInfo;
|
|
1323
|
+
const requestedVersion = String(params?.protocolVersion || LATEST_PROTOCOL_VERSION);
|
|
1324
|
+
const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.includes(requestedVersion) ? requestedVersion : LATEST_PROTOCOL_VERSION;
|
|
1325
|
+
ctx.setSessionState({
|
|
1326
|
+
initialized: false,
|
|
1327
|
+
clientInfo,
|
|
1328
|
+
protocolVersion
|
|
1329
|
+
});
|
|
1330
|
+
sharedLogger.info("mcp_dispatch", {
|
|
1331
|
+
message: "Initialize request",
|
|
1332
|
+
sessionId: ctx.sessionId,
|
|
1333
|
+
clientInfo,
|
|
1334
|
+
requestedVersion,
|
|
1335
|
+
negotiatedVersion: protocolVersion
|
|
1336
|
+
});
|
|
1337
|
+
return {
|
|
1338
|
+
result: {
|
|
1339
|
+
protocolVersion,
|
|
1340
|
+
capabilities: buildCapabilities(),
|
|
1341
|
+
serverInfo: {
|
|
1342
|
+
name: ctx.config.title || serverMetadata.title,
|
|
1343
|
+
version: ctx.config.version || "0.0.1"
|
|
1344
|
+
},
|
|
1345
|
+
instructions: ctx.config.instructions || serverMetadata.instructions
|
|
1346
|
+
}
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
async function handleToolsList(ctx) {
|
|
1350
|
+
const tools = (ctx.tools ?? sharedTools).map((tool) => ({
|
|
1351
|
+
name: tool.name,
|
|
1352
|
+
description: tool.description,
|
|
1353
|
+
inputSchema: z3.toJSONSchema(tool.inputSchema),
|
|
1354
|
+
...tool.outputSchema && {
|
|
1355
|
+
outputSchema: z3.toJSONSchema(z3.object(tool.outputSchema))
|
|
1356
|
+
},
|
|
1357
|
+
...tool.annotations && { annotations: tool.annotations }
|
|
1358
|
+
}));
|
|
1359
|
+
return { result: { tools } };
|
|
1360
|
+
}
|
|
1361
|
+
async function handleToolsCall(params, ctx, requestId) {
|
|
1362
|
+
const toolName = String(params?.name || "");
|
|
1363
|
+
const toolArgs = params?.arguments || {};
|
|
1364
|
+
const meta = params?._meta;
|
|
1365
|
+
const abortController = new AbortController;
|
|
1366
|
+
if (requestId !== undefined && ctx.cancellationRegistry) {
|
|
1367
|
+
ctx.cancellationRegistry.set(requestId, abortController);
|
|
1368
|
+
}
|
|
1369
|
+
const toolContext = {
|
|
1370
|
+
...ctx.auth,
|
|
1371
|
+
sessionId: ctx.sessionId,
|
|
1372
|
+
signal: abortController.signal,
|
|
1373
|
+
meta: {
|
|
1374
|
+
progressToken: meta?.progressToken,
|
|
1375
|
+
requestId: requestId !== undefined ? String(requestId) : undefined
|
|
1376
|
+
}
|
|
1377
|
+
};
|
|
1378
|
+
sharedLogger.debug("mcp_dispatch", {
|
|
1379
|
+
message: "Calling tool",
|
|
1380
|
+
tool: toolName,
|
|
1381
|
+
sessionId: ctx.sessionId,
|
|
1382
|
+
requestId,
|
|
1383
|
+
hasProviderToken: Boolean(ctx.auth.providerToken)
|
|
1384
|
+
});
|
|
1385
|
+
const toolList = ctx.tools ?? sharedTools;
|
|
1386
|
+
const tool = toolList.find((t) => t.name === toolName);
|
|
1387
|
+
if (tool?.requiresAuth && !ctx.auth.providerToken) {
|
|
1388
|
+
return {
|
|
1389
|
+
error: {
|
|
1390
|
+
code: JsonRpcErrorCode2.InvalidRequest,
|
|
1391
|
+
message: "Authentication required. Please complete OAuth flow first."
|
|
1392
|
+
}
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
try {
|
|
1396
|
+
const result = await executeSharedTool(toolName, toolArgs, toolContext, ctx.tools);
|
|
1397
|
+
return { result };
|
|
1398
|
+
} catch (error) {
|
|
1399
|
+
if (abortController.signal.aborted) {
|
|
1400
|
+
sharedLogger.info("mcp_dispatch", {
|
|
1401
|
+
message: "Tool execution cancelled",
|
|
1402
|
+
tool: toolName,
|
|
1403
|
+
requestId
|
|
1404
|
+
});
|
|
1405
|
+
return {
|
|
1406
|
+
error: {
|
|
1407
|
+
code: JsonRpcErrorCode2.InternalError,
|
|
1408
|
+
message: "Request was cancelled"
|
|
1409
|
+
}
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
sharedLogger.error("mcp_dispatch", {
|
|
1413
|
+
message: "Tool execution failed",
|
|
1414
|
+
tool: toolName,
|
|
1415
|
+
error: error.message
|
|
1416
|
+
});
|
|
1417
|
+
return {
|
|
1418
|
+
error: {
|
|
1419
|
+
code: JsonRpcErrorCode2.InternalError,
|
|
1420
|
+
message: `Tool execution failed: ${error.message}`
|
|
1421
|
+
}
|
|
1422
|
+
};
|
|
1423
|
+
} finally {
|
|
1424
|
+
if (requestId !== undefined && ctx.cancellationRegistry) {
|
|
1425
|
+
ctx.cancellationRegistry.delete(requestId);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
async function handleResourcesList() {
|
|
1430
|
+
return { result: { resources: [] } };
|
|
1431
|
+
}
|
|
1432
|
+
async function handleResourcesTemplatesList() {
|
|
1433
|
+
return { result: { resourceTemplates: [] } };
|
|
1434
|
+
}
|
|
1435
|
+
async function handlePromptsList() {
|
|
1436
|
+
return { result: { prompts: [] } };
|
|
1437
|
+
}
|
|
1438
|
+
async function handlePing() {
|
|
1439
|
+
return { result: {} };
|
|
1440
|
+
}
|
|
1441
|
+
var currentLogLevel = "info";
|
|
1442
|
+
async function handleLoggingSetLevel(params) {
|
|
1443
|
+
const level = params?.level;
|
|
1444
|
+
const validLevels = [
|
|
1445
|
+
"debug",
|
|
1446
|
+
"info",
|
|
1447
|
+
"notice",
|
|
1448
|
+
"warning",
|
|
1449
|
+
"error",
|
|
1450
|
+
"critical",
|
|
1451
|
+
"alert",
|
|
1452
|
+
"emergency"
|
|
1453
|
+
];
|
|
1454
|
+
if (!level || !validLevels.includes(level)) {
|
|
1455
|
+
return {
|
|
1456
|
+
error: {
|
|
1457
|
+
code: JsonRpcErrorCode2.InvalidParams,
|
|
1458
|
+
message: `Invalid log level. Must be one of: ${validLevels.join(", ")}`
|
|
1459
|
+
}
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
currentLogLevel = level;
|
|
1463
|
+
sharedLogger.info("mcp_dispatch", {
|
|
1464
|
+
message: "Log level changed",
|
|
1465
|
+
level: currentLogLevel
|
|
1466
|
+
});
|
|
1467
|
+
return { result: {} };
|
|
1468
|
+
}
|
|
1469
|
+
function getLogLevel() {
|
|
1470
|
+
return currentLogLevel;
|
|
1471
|
+
}
|
|
1472
|
+
async function dispatchMcpMethod(method, params, ctx, requestId) {
|
|
1473
|
+
if (!method) {
|
|
1474
|
+
return {
|
|
1475
|
+
error: {
|
|
1476
|
+
code: JsonRpcErrorCode2.InvalidRequest,
|
|
1477
|
+
message: "Missing method"
|
|
1478
|
+
}
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
switch (method) {
|
|
1482
|
+
case "initialize":
|
|
1483
|
+
return handleInitialize(params, ctx);
|
|
1484
|
+
case "tools/list":
|
|
1485
|
+
return handleToolsList(ctx);
|
|
1486
|
+
case "tools/call":
|
|
1487
|
+
return handleToolsCall(params, ctx, requestId);
|
|
1488
|
+
case "resources/list":
|
|
1489
|
+
return handleResourcesList();
|
|
1490
|
+
case "resources/templates/list":
|
|
1491
|
+
return handleResourcesTemplatesList();
|
|
1492
|
+
case "prompts/list":
|
|
1493
|
+
return handlePromptsList();
|
|
1494
|
+
case "ping":
|
|
1495
|
+
return handlePing();
|
|
1496
|
+
case "logging/setLevel":
|
|
1497
|
+
return handleLoggingSetLevel(params);
|
|
1498
|
+
default:
|
|
1499
|
+
sharedLogger.debug("mcp_dispatch", { message: "Unknown method", method });
|
|
1500
|
+
return {
|
|
1501
|
+
error: {
|
|
1502
|
+
code: JsonRpcErrorCode2.MethodNotFound,
|
|
1503
|
+
message: `Method not found: ${method}`
|
|
1504
|
+
}
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
function handleMcpNotification(method, params, ctx) {
|
|
1509
|
+
if (method === "notifications/initialized") {
|
|
1510
|
+
const session = ctx.getSessionState();
|
|
1511
|
+
if (session) {
|
|
1512
|
+
ctx.setSessionState({ ...session, initialized: true });
|
|
1513
|
+
}
|
|
1514
|
+
sharedLogger.info("mcp_dispatch", {
|
|
1515
|
+
message: "Client initialized",
|
|
1516
|
+
sessionId: ctx.sessionId
|
|
1517
|
+
});
|
|
1518
|
+
return true;
|
|
1519
|
+
}
|
|
1520
|
+
if (method === "notifications/cancelled") {
|
|
1521
|
+
const cancelParams = params;
|
|
1522
|
+
const requestId = cancelParams?.requestId;
|
|
1523
|
+
if (requestId !== undefined && ctx.cancellationRegistry) {
|
|
1524
|
+
const controller = ctx.cancellationRegistry.get(requestId);
|
|
1525
|
+
if (controller) {
|
|
1526
|
+
sharedLogger.info("mcp_dispatch", {
|
|
1527
|
+
message: "Cancelling request",
|
|
1528
|
+
requestId,
|
|
1529
|
+
reason: cancelParams?.reason,
|
|
1530
|
+
sessionId: ctx.sessionId
|
|
1531
|
+
});
|
|
1532
|
+
controller.abort(cancelParams?.reason ?? "Client requested cancellation");
|
|
1533
|
+
return true;
|
|
1534
|
+
}
|
|
1535
|
+
sharedLogger.debug("mcp_dispatch", {
|
|
1536
|
+
message: "Cancellation request for unknown requestId",
|
|
1537
|
+
requestId,
|
|
1538
|
+
sessionId: ctx.sessionId
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
return true;
|
|
1542
|
+
}
|
|
1543
|
+
sharedLogger.debug("mcp_dispatch", {
|
|
1544
|
+
message: "Unhandled notification",
|
|
1545
|
+
method,
|
|
1546
|
+
sessionId: ctx.sessionId
|
|
1547
|
+
});
|
|
1548
|
+
return false;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
// src/shared/oauth/refresh.ts
|
|
1552
|
+
import * as oauth from "oauth4webapi";
|
|
1553
|
+
function buildProviderRefreshConfig(config) {
|
|
1554
|
+
if (!config.PROVIDER_CLIENT_ID || !config.PROVIDER_CLIENT_SECRET || !config.PROVIDER_ACCOUNTS_URL) {
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
return {
|
|
1558
|
+
clientId: config.PROVIDER_CLIENT_ID,
|
|
1559
|
+
clientSecret: config.PROVIDER_CLIENT_SECRET,
|
|
1560
|
+
accountsUrl: config.PROVIDER_ACCOUNTS_URL,
|
|
1561
|
+
tokenEndpointPath: config.OAUTH_TOKEN_URL
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1564
|
+
function buildAuthorizationServer(config) {
|
|
1565
|
+
const tokenEndpoint = config.tokenEndpointPath || "/token";
|
|
1566
|
+
return {
|
|
1567
|
+
issuer: config.accountsUrl,
|
|
1568
|
+
token_endpoint: new URL(tokenEndpoint, config.accountsUrl).toString()
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
async function refreshProviderToken(refreshToken, config) {
|
|
1572
|
+
const authServer = buildAuthorizationServer(config);
|
|
1573
|
+
const client = {
|
|
1574
|
+
client_id: config.clientId,
|
|
1575
|
+
token_endpoint_auth_method: "client_secret_post"
|
|
1576
|
+
};
|
|
1577
|
+
sharedLogger.debug("oauth_refresh", {
|
|
1578
|
+
message: "Refreshing provider token",
|
|
1579
|
+
tokenUrl: authServer.token_endpoint
|
|
1580
|
+
});
|
|
1581
|
+
try {
|
|
1582
|
+
const clientAuth = oauth.ClientSecretPost(config.clientSecret);
|
|
1583
|
+
const response = await oauth.refreshTokenGrantRequest(authServer, client, clientAuth, refreshToken);
|
|
1584
|
+
const result = await oauth.processRefreshTokenResponse(authServer, client, response);
|
|
1585
|
+
const accessToken = result.access_token;
|
|
1586
|
+
if (!accessToken) {
|
|
1587
|
+
return {
|
|
1588
|
+
success: false,
|
|
1589
|
+
error: "No access_token in provider response"
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
sharedLogger.info("oauth_refresh", {
|
|
1593
|
+
message: "Provider token refreshed",
|
|
1594
|
+
hasNewRefreshToken: !!result.refresh_token
|
|
1595
|
+
});
|
|
1596
|
+
return {
|
|
1597
|
+
success: true,
|
|
1598
|
+
tokens: {
|
|
1599
|
+
access_token: accessToken,
|
|
1600
|
+
refresh_token: result.refresh_token ?? refreshToken,
|
|
1601
|
+
expires_at: Date.now() + (result.expires_in ?? 3600) * 1000,
|
|
1602
|
+
scopes: (result.scope || "").split(/\s+/).filter(Boolean)
|
|
1603
|
+
}
|
|
1604
|
+
};
|
|
1605
|
+
} catch (error) {
|
|
1606
|
+
if (error instanceof oauth.ResponseBodyError) {
|
|
1607
|
+
sharedLogger.error("oauth_refresh", {
|
|
1608
|
+
message: "Provider refresh failed",
|
|
1609
|
+
error: error.error,
|
|
1610
|
+
description: error.error_description
|
|
1611
|
+
});
|
|
1612
|
+
return {
|
|
1613
|
+
success: false,
|
|
1614
|
+
error: `Provider returned ${error.error}: ${error.error_description || ""}`.trim()
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
sharedLogger.error("oauth_refresh", {
|
|
1618
|
+
message: "Token refresh network error",
|
|
1619
|
+
error: error.message
|
|
1620
|
+
});
|
|
1621
|
+
return {
|
|
1622
|
+
success: false,
|
|
1623
|
+
error: `Network error: ${error.message}`
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
var EXPIRY_BUFFER_MS = 60000;
|
|
1628
|
+
var REFRESH_COOLDOWN_MS = 30000;
|
|
1629
|
+
var recentlyRefreshed = new Map;
|
|
1630
|
+
function shouldSkipRefresh(rsToken) {
|
|
1631
|
+
const lastRefresh = recentlyRefreshed.get(rsToken);
|
|
1632
|
+
if (lastRefresh && Date.now() - lastRefresh < REFRESH_COOLDOWN_MS) {
|
|
1633
|
+
return true;
|
|
1634
|
+
}
|
|
1635
|
+
return false;
|
|
1636
|
+
}
|
|
1637
|
+
function markRefreshed(rsToken) {
|
|
1638
|
+
recentlyRefreshed.set(rsToken, Date.now());
|
|
1639
|
+
if (recentlyRefreshed.size > 1000) {
|
|
1640
|
+
const now = Date.now();
|
|
1641
|
+
for (const [key, timestamp] of recentlyRefreshed) {
|
|
1642
|
+
if (now - timestamp > REFRESH_COOLDOWN_MS) {
|
|
1643
|
+
recentlyRefreshed.delete(key);
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
function isTokenExpiredOrExpiring(expiresAt, bufferMs = EXPIRY_BUFFER_MS) {
|
|
1649
|
+
if (!expiresAt)
|
|
1650
|
+
return false;
|
|
1651
|
+
return Date.now() >= expiresAt - bufferMs;
|
|
1652
|
+
}
|
|
1653
|
+
async function ensureFreshToken(rsAccessToken, tokenStore, providerConfig) {
|
|
1654
|
+
const record = await tokenStore.getByRsAccess(rsAccessToken);
|
|
1655
|
+
if (!record?.provider?.access_token) {
|
|
1656
|
+
return { accessToken: "", wasRefreshed: false };
|
|
1657
|
+
}
|
|
1658
|
+
if (!isTokenExpiredOrExpiring(record.provider.expires_at)) {
|
|
1659
|
+
return { accessToken: record.provider.access_token, wasRefreshed: false };
|
|
1660
|
+
}
|
|
1661
|
+
if (shouldSkipRefresh(rsAccessToken)) {
|
|
1662
|
+
sharedLogger.debug("oauth_refresh", {
|
|
1663
|
+
message: "Token refresh throttled (recently refreshed in this process)"
|
|
1664
|
+
});
|
|
1665
|
+
return { accessToken: record.provider.access_token, wasRefreshed: false };
|
|
1666
|
+
}
|
|
1667
|
+
sharedLogger.info("oauth_refresh", {
|
|
1668
|
+
message: "Token near expiry, attempting refresh",
|
|
1669
|
+
expiresAt: record.provider.expires_at,
|
|
1670
|
+
now: Date.now()
|
|
1671
|
+
});
|
|
1672
|
+
if (!record.provider.refresh_token) {
|
|
1673
|
+
sharedLogger.warning("oauth_refresh", {
|
|
1674
|
+
message: "Token near expiry but no refresh token available"
|
|
1675
|
+
});
|
|
1676
|
+
return { accessToken: record.provider.access_token, wasRefreshed: false };
|
|
1677
|
+
}
|
|
1678
|
+
if (!providerConfig) {
|
|
1679
|
+
sharedLogger.warning("oauth_refresh", {
|
|
1680
|
+
message: "Token near expiry but no provider config for refresh"
|
|
1681
|
+
});
|
|
1682
|
+
return { accessToken: record.provider.access_token, wasRefreshed: false };
|
|
1683
|
+
}
|
|
1684
|
+
const result = await refreshProviderToken(record.provider.refresh_token, providerConfig);
|
|
1685
|
+
if (!result.success || !result.tokens) {
|
|
1686
|
+
sharedLogger.error("oauth_refresh", {
|
|
1687
|
+
message: "Token refresh failed, using existing token",
|
|
1688
|
+
error: result.error
|
|
1689
|
+
});
|
|
1690
|
+
return { accessToken: record.provider.access_token, wasRefreshed: false };
|
|
1691
|
+
}
|
|
1692
|
+
const providerRefreshRotated = result.tokens.refresh_token !== record.provider.refresh_token;
|
|
1693
|
+
const newRsAccess = providerRefreshRotated ? undefined : record.rs_access_token;
|
|
1694
|
+
try {
|
|
1695
|
+
await tokenStore.updateByRsRefresh(record.rs_refresh_token, result.tokens, newRsAccess);
|
|
1696
|
+
markRefreshed(rsAccessToken);
|
|
1697
|
+
sharedLogger.info("oauth_refresh", {
|
|
1698
|
+
message: "Token store updated with refreshed tokens",
|
|
1699
|
+
rsAccessRotated: providerRefreshRotated
|
|
1700
|
+
});
|
|
1701
|
+
return { accessToken: result.tokens.access_token, wasRefreshed: true };
|
|
1702
|
+
} catch (error) {
|
|
1703
|
+
sharedLogger.error("oauth_refresh", {
|
|
1704
|
+
message: "Failed to update token store",
|
|
1705
|
+
error: error.message
|
|
1706
|
+
});
|
|
1707
|
+
return { accessToken: result.tokens.access_token, wasRefreshed: true };
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
// src/shared/mcp/security.ts
|
|
1712
|
+
function validateOrigin(headers, isDev) {
|
|
1713
|
+
const origin = headers.get("Origin") || headers.get("origin");
|
|
1714
|
+
if (!origin) {
|
|
1715
|
+
return;
|
|
1716
|
+
}
|
|
1717
|
+
if (isDev) {
|
|
1718
|
+
if (!isLocalhostOrigin(origin)) {
|
|
1719
|
+
throw new Error(`Invalid origin: ${origin}. Only localhost allowed in development`);
|
|
1720
|
+
}
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1723
|
+
if (!isAllowedOrigin(origin)) {
|
|
1724
|
+
throw new Error(`Invalid origin: ${origin}`);
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
var SUPPORTED_PROTOCOL_VERSIONS2 = [
|
|
1728
|
+
"2025-11-25",
|
|
1729
|
+
"2025-06-18",
|
|
1730
|
+
"2025-03-26",
|
|
1731
|
+
"2024-11-05"
|
|
1732
|
+
];
|
|
1733
|
+
function validateProtocolVersion(headers, _expected) {
|
|
1734
|
+
const header = headers.get("Mcp-Protocol-Version") || headers.get("MCP-Protocol-Version");
|
|
1735
|
+
if (!header) {
|
|
1736
|
+
return;
|
|
1737
|
+
}
|
|
1738
|
+
const clientVersions = header.split(",").map((v) => v.trim()).filter(Boolean);
|
|
1739
|
+
const hasSupported = clientVersions.some((v) => SUPPORTED_PROTOCOL_VERSIONS2.includes(v));
|
|
1740
|
+
if (!hasSupported) {
|
|
1741
|
+
throw new Error(`Unsupported MCP protocol version: ${header}. Supported: ${SUPPORTED_PROTOCOL_VERSIONS2.join(", ")}`);
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
function isLocalhostOrigin(origin) {
|
|
1745
|
+
try {
|
|
1746
|
+
const url = new URL(origin);
|
|
1747
|
+
const hostname = url.hostname.toLowerCase();
|
|
1748
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname.startsWith("192.168.") || hostname.startsWith("10.") || hostname.endsWith(".local");
|
|
1749
|
+
} catch {
|
|
1750
|
+
return false;
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
function isAllowedOrigin(_origin) {
|
|
1754
|
+
return true;
|
|
1755
|
+
}
|
|
1756
|
+
function buildUnauthorizedChallenge(args) {
|
|
1757
|
+
const resourcePath = args.resourcePath || "/.well-known/oauth-protected-resource";
|
|
1758
|
+
const resourceMd = `${args.origin}${resourcePath}?sid=${encodeURIComponent(args.sid)}`;
|
|
1759
|
+
return {
|
|
1760
|
+
status: 401,
|
|
1761
|
+
headers: {
|
|
1762
|
+
"WWW-Authenticate": `Bearer realm="MCP", authorization_uri="${resourceMd}"`,
|
|
1763
|
+
"Mcp-Session-Id": args.sid
|
|
1764
|
+
},
|
|
1765
|
+
body: {
|
|
1766
|
+
jsonrpc: "2.0",
|
|
1767
|
+
error: {
|
|
1768
|
+
code: -32000,
|
|
1769
|
+
message: args.message || "Unauthorized"
|
|
1770
|
+
},
|
|
1771
|
+
id: null
|
|
1772
|
+
}
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
// src/shared/oauth/discovery.ts
|
|
1777
|
+
function buildAuthorizationServerMetadata(baseUrl, scopes, overrides) {
|
|
1778
|
+
return {
|
|
1779
|
+
issuer: baseUrl,
|
|
1780
|
+
authorization_endpoint: overrides?.authorizationEndpoint || `${baseUrl}/authorize`,
|
|
1781
|
+
token_endpoint: overrides?.tokenEndpoint || `${baseUrl}/token`,
|
|
1782
|
+
revocation_endpoint: overrides?.revocationEndpoint || `${baseUrl}/revoke`,
|
|
1783
|
+
registration_endpoint: `${baseUrl}/register`,
|
|
1784
|
+
response_types_supported: ["code"],
|
|
1785
|
+
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
1786
|
+
code_challenge_methods_supported: ["S256"],
|
|
1787
|
+
token_endpoint_auth_methods_supported: ["none"],
|
|
1788
|
+
scopes_supported: scopes,
|
|
1789
|
+
client_id_metadata_document_supported: overrides?.cimdEnabled ?? true
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
function buildProtectedResourceMetadata(resourceUrl, authorizationServerUrl, sid) {
|
|
1793
|
+
const resource = (() => {
|
|
1794
|
+
if (!sid) {
|
|
1795
|
+
return resourceUrl;
|
|
1796
|
+
}
|
|
1797
|
+
try {
|
|
1798
|
+
const u = new URL(resourceUrl);
|
|
1799
|
+
u.searchParams.set("sid", sid);
|
|
1800
|
+
return u.toString();
|
|
1801
|
+
} catch {
|
|
1802
|
+
return resourceUrl;
|
|
1803
|
+
}
|
|
1804
|
+
})();
|
|
1805
|
+
return {
|
|
1806
|
+
authorization_servers: [authorizationServerUrl],
|
|
1807
|
+
resource
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
// src/shared/oauth/discovery-handlers.ts
|
|
1812
|
+
function createDiscoveryHandlers(config, strategy) {
|
|
1813
|
+
const scopes = config.OAUTH_SCOPES.split(/\s+/).map((scope) => scope.trim()).filter(Boolean);
|
|
1814
|
+
return {
|
|
1815
|
+
authorizationMetadata: (requestUrl) => {
|
|
1816
|
+
const baseUrl = strategy.resolveAuthBaseUrl(requestUrl, config);
|
|
1817
|
+
return buildAuthorizationServerMetadata(baseUrl, scopes, {
|
|
1818
|
+
authorizationEndpoint: `${baseUrl}/authorize`,
|
|
1819
|
+
tokenEndpoint: `${baseUrl}/token`,
|
|
1820
|
+
revocationEndpoint: `${baseUrl}/revoke`,
|
|
1821
|
+
cimdEnabled: config.CIMD_ENABLED
|
|
1822
|
+
});
|
|
1823
|
+
},
|
|
1824
|
+
protectedResourceMetadata: (requestUrl, sid) => {
|
|
1825
|
+
const resourceBase = strategy.resolveResourceBaseUrl(requestUrl, config);
|
|
1826
|
+
const authorizationServerUrl = config.AUTH_DISCOVERY_URL || strategy.resolveAuthorizationServerUrl(requestUrl, config);
|
|
1827
|
+
return buildProtectedResourceMetadata(resourceBase, authorizationServerUrl, sid);
|
|
1828
|
+
}
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1831
|
+
var workerDiscoveryStrategy = {
|
|
1832
|
+
resolveAuthBaseUrl: (requestUrl) => requestUrl.origin,
|
|
1833
|
+
resolveAuthorizationServerUrl: (requestUrl) => `${requestUrl.origin}/.well-known/oauth-authorization-server`,
|
|
1834
|
+
resolveResourceBaseUrl: (requestUrl) => `${requestUrl.origin}/mcp`
|
|
1835
|
+
};
|
|
1836
|
+
var nodeDiscoveryStrategy = {
|
|
1837
|
+
resolveAuthBaseUrl: (requestUrl, config) => {
|
|
1838
|
+
const authPort = Number(config.PORT) + 1;
|
|
1839
|
+
return `${requestUrl.protocol}//${requestUrl.hostname}:${authPort}`;
|
|
1840
|
+
},
|
|
1841
|
+
resolveAuthorizationServerUrl: (requestUrl, config) => {
|
|
1842
|
+
const authPort = Number(config.PORT) + 1;
|
|
1843
|
+
return `${requestUrl.protocol}//${requestUrl.hostname}:${authPort}/.well-known/oauth-authorization-server`;
|
|
1844
|
+
},
|
|
1845
|
+
resolveResourceBaseUrl: (requestUrl) => `${requestUrl.protocol}//${requestUrl.host}/mcp`
|
|
1846
|
+
};
|
|
1847
|
+
|
|
1848
|
+
// src/shared/oauth/ssrf.ts
|
|
1849
|
+
var BLOCKED_HOSTS = new Set([
|
|
1850
|
+
"localhost",
|
|
1851
|
+
"127.0.0.1",
|
|
1852
|
+
"::1",
|
|
1853
|
+
"0.0.0.0",
|
|
1854
|
+
"[::1]"
|
|
1855
|
+
]);
|
|
1856
|
+
var PRIVATE_IP_PATTERNS = [
|
|
1857
|
+
/^10\./,
|
|
1858
|
+
/^172\.(1[6-9]|2\d|3[01])\./,
|
|
1859
|
+
/^192\.168\./,
|
|
1860
|
+
/^169\.254\./,
|
|
1861
|
+
/^fc00:/i,
|
|
1862
|
+
/^fd00:/i,
|
|
1863
|
+
/^fe80:/i
|
|
1864
|
+
];
|
|
1865
|
+
var BLOCKED_DOMAIN_PATTERNS = [
|
|
1866
|
+
/\.local$/i,
|
|
1867
|
+
/\.internal$/i,
|
|
1868
|
+
/\.localhost$/i,
|
|
1869
|
+
/\.localdomain$/i,
|
|
1870
|
+
/\.corp$/i,
|
|
1871
|
+
/\.lan$/i
|
|
1872
|
+
];
|
|
1873
|
+
function isPrivateIp(hostname) {
|
|
1874
|
+
for (const pattern of PRIVATE_IP_PATTERNS) {
|
|
1875
|
+
if (pattern.test(hostname)) {
|
|
1876
|
+
return true;
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
return false;
|
|
1880
|
+
}
|
|
1881
|
+
function isBlockedDomain(hostname) {
|
|
1882
|
+
const lower = hostname.toLowerCase();
|
|
1883
|
+
for (const pattern of BLOCKED_DOMAIN_PATTERNS) {
|
|
1884
|
+
if (pattern.test(lower)) {
|
|
1885
|
+
return true;
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
return false;
|
|
1889
|
+
}
|
|
1890
|
+
function checkSsrfSafe(urlString, options) {
|
|
1891
|
+
const requireNonRootPath = options?.requireNonRootPath ?? true;
|
|
1892
|
+
let url;
|
|
1893
|
+
try {
|
|
1894
|
+
url = new URL(urlString);
|
|
1895
|
+
} catch {
|
|
1896
|
+
return { safe: false, reason: "invalid_url" };
|
|
1897
|
+
}
|
|
1898
|
+
if (url.protocol !== "https:") {
|
|
1899
|
+
return { safe: false, reason: "https_required" };
|
|
1900
|
+
}
|
|
1901
|
+
const hostname = url.hostname.toLowerCase();
|
|
1902
|
+
if (BLOCKED_HOSTS.has(hostname)) {
|
|
1903
|
+
return { safe: false, reason: "blocked_host" };
|
|
1904
|
+
}
|
|
1905
|
+
if (isPrivateIp(hostname)) {
|
|
1906
|
+
return { safe: false, reason: "private_ip" };
|
|
1907
|
+
}
|
|
1908
|
+
if (isBlockedDomain(hostname)) {
|
|
1909
|
+
return { safe: false, reason: "internal_domain" };
|
|
1910
|
+
}
|
|
1911
|
+
if (requireNonRootPath && (url.pathname === "/" || url.pathname === "")) {
|
|
1912
|
+
return { safe: false, reason: "root_path_not_allowed" };
|
|
1913
|
+
}
|
|
1914
|
+
return { safe: true };
|
|
1915
|
+
}
|
|
1916
|
+
function isSsrfSafe(urlString, options) {
|
|
1917
|
+
return checkSsrfSafe(urlString, options).safe;
|
|
1918
|
+
}
|
|
1919
|
+
function assertSsrfSafe(urlString, options) {
|
|
1920
|
+
const result = checkSsrfSafe(urlString, options);
|
|
1921
|
+
if (result.safe === false) {
|
|
1922
|
+
throw new Error(`ssrf_blocked: ${result.reason}`);
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
// src/shared/oauth/cimd.ts
|
|
1927
|
+
import { z as z4 } from "zod";
|
|
1928
|
+
var ClientMetadataSchema = z4.object({
|
|
1929
|
+
client_id: z4.string().url(),
|
|
1930
|
+
client_name: z4.string().optional(),
|
|
1931
|
+
redirect_uris: z4.array(z4.string().url()),
|
|
1932
|
+
client_uri: z4.string().url().optional(),
|
|
1933
|
+
logo_uri: z4.string().url().optional(),
|
|
1934
|
+
tos_uri: z4.string().url().optional(),
|
|
1935
|
+
policy_uri: z4.string().url().optional(),
|
|
1936
|
+
jwks_uri: z4.string().url().optional(),
|
|
1937
|
+
software_statement: z4.string().optional()
|
|
1938
|
+
});
|
|
1939
|
+
function isClientIdUrl(clientId) {
|
|
1940
|
+
if (!clientId.startsWith("https://")) {
|
|
1941
|
+
return false;
|
|
1942
|
+
}
|
|
1943
|
+
try {
|
|
1944
|
+
const url = new URL(clientId);
|
|
1945
|
+
return url.pathname !== "/" && url.pathname.length > 1;
|
|
1946
|
+
} catch {
|
|
1947
|
+
return false;
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
function isDomainAllowed(clientIdUrl, allowedDomains) {
|
|
1951
|
+
if (!allowedDomains || allowedDomains.length === 0) {
|
|
1952
|
+
return true;
|
|
1953
|
+
}
|
|
1954
|
+
try {
|
|
1955
|
+
const url = new URL(clientIdUrl);
|
|
1956
|
+
const hostname = url.hostname.toLowerCase();
|
|
1957
|
+
return allowedDomains.some((domain) => {
|
|
1958
|
+
const d = domain.toLowerCase();
|
|
1959
|
+
return hostname === d || hostname.endsWith(`.${d}`);
|
|
1960
|
+
});
|
|
1961
|
+
} catch {
|
|
1962
|
+
return false;
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
async function fetchClientMetadata(clientIdUrl, config) {
|
|
1966
|
+
const timeoutMs = config?.timeoutMs ?? 5000;
|
|
1967
|
+
const maxBytes = config?.maxBytes ?? 65536;
|
|
1968
|
+
const allowedDomains = config?.allowedDomains;
|
|
1969
|
+
sharedLogger.debug("cimd", {
|
|
1970
|
+
message: "Fetching client metadata",
|
|
1971
|
+
url: clientIdUrl
|
|
1972
|
+
});
|
|
1973
|
+
try {
|
|
1974
|
+
assertSsrfSafe(clientIdUrl, { requireNonRootPath: true });
|
|
1975
|
+
} catch (error) {
|
|
1976
|
+
sharedLogger.warning("cimd", {
|
|
1977
|
+
message: "SSRF check failed",
|
|
1978
|
+
url: clientIdUrl,
|
|
1979
|
+
error: error.message
|
|
1980
|
+
});
|
|
1981
|
+
return { success: false, error: error.message };
|
|
1982
|
+
}
|
|
1983
|
+
if (!isDomainAllowed(clientIdUrl, allowedDomains)) {
|
|
1984
|
+
sharedLogger.warning("cimd", {
|
|
1985
|
+
message: "Domain not in allowlist",
|
|
1986
|
+
url: clientIdUrl
|
|
1987
|
+
});
|
|
1988
|
+
return { success: false, error: "domain_not_allowed" };
|
|
1989
|
+
}
|
|
1990
|
+
const controller = new AbortController;
|
|
1991
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
1992
|
+
try {
|
|
1993
|
+
const response = await fetch(clientIdUrl, {
|
|
1994
|
+
signal: controller.signal,
|
|
1995
|
+
headers: {
|
|
1996
|
+
Accept: "application/json",
|
|
1997
|
+
"User-Agent": "MCP-Server/1.0 CIMD-Fetcher"
|
|
1998
|
+
},
|
|
1999
|
+
redirect: "manual"
|
|
2000
|
+
});
|
|
2001
|
+
if (!response.ok) {
|
|
2002
|
+
sharedLogger.warning("cimd", {
|
|
2003
|
+
message: "Fetch failed",
|
|
2004
|
+
url: clientIdUrl,
|
|
2005
|
+
status: response.status
|
|
2006
|
+
});
|
|
2007
|
+
return { success: false, error: `fetch_failed: ${response.status}` };
|
|
2008
|
+
}
|
|
2009
|
+
const contentLength = response.headers.get("content-length");
|
|
2010
|
+
if (contentLength && parseInt(contentLength, 10) > maxBytes) {
|
|
2011
|
+
sharedLogger.warning("cimd", {
|
|
2012
|
+
message: "Response too large",
|
|
2013
|
+
url: clientIdUrl,
|
|
2014
|
+
contentLength
|
|
2015
|
+
});
|
|
2016
|
+
return { success: false, error: "metadata_too_large" };
|
|
2017
|
+
}
|
|
2018
|
+
const contentType = response.headers.get("content-type") || "";
|
|
2019
|
+
if (!contentType.includes("application/json") && !contentType.includes("text/json")) {
|
|
2020
|
+
sharedLogger.warning("cimd", {
|
|
2021
|
+
message: "Invalid content type",
|
|
2022
|
+
url: clientIdUrl,
|
|
2023
|
+
contentType
|
|
2024
|
+
});
|
|
2025
|
+
return { success: false, error: "invalid_content_type" };
|
|
2026
|
+
}
|
|
2027
|
+
const text = await response.text();
|
|
2028
|
+
if (text.length > maxBytes) {
|
|
2029
|
+
return { success: false, error: "metadata_too_large" };
|
|
2030
|
+
}
|
|
2031
|
+
let data;
|
|
2032
|
+
try {
|
|
2033
|
+
data = JSON.parse(text);
|
|
2034
|
+
} catch {
|
|
2035
|
+
return { success: false, error: "invalid_json" };
|
|
2036
|
+
}
|
|
2037
|
+
const parsed = ClientMetadataSchema.safeParse(data);
|
|
2038
|
+
if (!parsed.success) {
|
|
2039
|
+
sharedLogger.warning("cimd", {
|
|
2040
|
+
message: "Invalid metadata schema",
|
|
2041
|
+
url: clientIdUrl,
|
|
2042
|
+
errors: parsed.error.issues
|
|
2043
|
+
});
|
|
2044
|
+
return {
|
|
2045
|
+
success: false,
|
|
2046
|
+
error: `invalid_metadata: ${parsed.error.message}`
|
|
2047
|
+
};
|
|
2048
|
+
}
|
|
2049
|
+
if (parsed.data.client_id !== clientIdUrl) {
|
|
2050
|
+
sharedLogger.warning("cimd", {
|
|
2051
|
+
message: "client_id mismatch",
|
|
2052
|
+
url: clientIdUrl,
|
|
2053
|
+
metadataClientId: parsed.data.client_id
|
|
2054
|
+
});
|
|
2055
|
+
return { success: false, error: "client_id_mismatch" };
|
|
2056
|
+
}
|
|
2057
|
+
sharedLogger.info("cimd", {
|
|
2058
|
+
message: "Client metadata fetched",
|
|
2059
|
+
url: clientIdUrl,
|
|
2060
|
+
clientName: parsed.data.client_name,
|
|
2061
|
+
redirectUrisCount: parsed.data.redirect_uris.length
|
|
2062
|
+
});
|
|
2063
|
+
return { success: true, metadata: parsed.data };
|
|
2064
|
+
} catch (error) {
|
|
2065
|
+
if (error.name === "AbortError") {
|
|
2066
|
+
sharedLogger.warning("cimd", {
|
|
2067
|
+
message: "Fetch timeout",
|
|
2068
|
+
url: clientIdUrl
|
|
2069
|
+
});
|
|
2070
|
+
return { success: false, error: "fetch_timeout" };
|
|
2071
|
+
}
|
|
2072
|
+
sharedLogger.error("cimd", {
|
|
2073
|
+
message: "Fetch error",
|
|
2074
|
+
url: clientIdUrl,
|
|
2075
|
+
error: error.message
|
|
2076
|
+
});
|
|
2077
|
+
return {
|
|
2078
|
+
success: false,
|
|
2079
|
+
error: `fetch_error: ${error.message}`
|
|
2080
|
+
};
|
|
2081
|
+
} finally {
|
|
2082
|
+
clearTimeout(timeout);
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
function validateRedirectUri(metadata, redirectUri) {
|
|
2086
|
+
return metadata.redirect_uris.includes(redirectUri);
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
// src/shared/oauth/flow.ts
|
|
2090
|
+
import * as oauth2 from "oauth4webapi";
|
|
2091
|
+
function generateOpaqueToken(bytes = 32) {
|
|
2092
|
+
const array = new Uint8Array(bytes);
|
|
2093
|
+
crypto.getRandomValues(array);
|
|
2094
|
+
return base64UrlEncode(array);
|
|
2095
|
+
}
|
|
2096
|
+
function buildAuthorizationServer2(providerConfig) {
|
|
2097
|
+
const authEndpoint = providerConfig.authorizationEndpointPath || "/authorize";
|
|
2098
|
+
const tokenEndpoint = providerConfig.tokenEndpointPath || "/token";
|
|
2099
|
+
return {
|
|
2100
|
+
issuer: providerConfig.accountsUrl,
|
|
2101
|
+
authorization_endpoint: new URL(authEndpoint, providerConfig.accountsUrl).toString(),
|
|
2102
|
+
token_endpoint: new URL(tokenEndpoint, providerConfig.accountsUrl).toString()
|
|
2103
|
+
};
|
|
2104
|
+
}
|
|
2105
|
+
function buildOAuthClient(providerConfig) {
|
|
2106
|
+
return {
|
|
2107
|
+
client_id: providerConfig.clientId || "",
|
|
2108
|
+
token_endpoint_auth_method: "client_secret_post"
|
|
2109
|
+
};
|
|
2110
|
+
}
|
|
2111
|
+
function isAllowedRedirect(uri, config, isDev) {
|
|
2112
|
+
try {
|
|
2113
|
+
const allowed = new Set(config.redirectAllowlist.concat([config.redirectUri]).filter(Boolean));
|
|
2114
|
+
const url = new URL(uri);
|
|
2115
|
+
if (isDev) {
|
|
2116
|
+
const loopback = new Set(["localhost", "127.0.0.1", "::1"]);
|
|
2117
|
+
if (loopback.has(url.hostname)) {
|
|
2118
|
+
return true;
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
if (config.redirectAllowAll) {
|
|
2122
|
+
return true;
|
|
2123
|
+
}
|
|
2124
|
+
return allowed.has(`${url.protocol}//${url.host}${url.pathname}`) || allowed.has(uri);
|
|
2125
|
+
} catch {
|
|
2126
|
+
return false;
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
async function handleAuthorize(input, store, providerConfig, oauthConfig, options) {
|
|
2130
|
+
if (!input.redirectUri) {
|
|
2131
|
+
throw new Error("invalid_request: redirect_uri is required");
|
|
2132
|
+
}
|
|
2133
|
+
if (!input.codeChallenge || input.codeChallengeMethod !== "S256") {
|
|
2134
|
+
throw new Error("invalid_request: PKCE code_challenge with S256 method is required");
|
|
2135
|
+
}
|
|
2136
|
+
let clientMetadata = null;
|
|
2137
|
+
const cimdEnabled = options.cimd?.enabled ?? true;
|
|
2138
|
+
if (input.clientId && isClientIdUrl(input.clientId)) {
|
|
2139
|
+
if (!cimdEnabled) {
|
|
2140
|
+
sharedLogger.warning("oauth_authorize", {
|
|
2141
|
+
message: "CIMD client_id received but CIMD is disabled",
|
|
2142
|
+
clientId: input.clientId
|
|
2143
|
+
});
|
|
2144
|
+
throw new Error("invalid_client: URL-based client_id not supported");
|
|
2145
|
+
}
|
|
2146
|
+
sharedLogger.debug("oauth_authorize", {
|
|
2147
|
+
message: "CIMD client_id detected, fetching metadata",
|
|
2148
|
+
clientId: input.clientId
|
|
2149
|
+
});
|
|
2150
|
+
const result = await fetchClientMetadata(input.clientId, {
|
|
2151
|
+
timeoutMs: options.cimd?.timeoutMs,
|
|
2152
|
+
maxBytes: options.cimd?.maxBytes,
|
|
2153
|
+
allowedDomains: options.cimd?.allowedDomains
|
|
2154
|
+
});
|
|
2155
|
+
if (result.success === false) {
|
|
2156
|
+
sharedLogger.error("oauth_authorize", {
|
|
2157
|
+
message: "CIMD metadata fetch failed",
|
|
2158
|
+
clientId: input.clientId,
|
|
2159
|
+
error: result.error
|
|
2160
|
+
});
|
|
2161
|
+
throw new Error(`invalid_client: ${result.error}`);
|
|
2162
|
+
}
|
|
2163
|
+
clientMetadata = result.metadata;
|
|
2164
|
+
if (!validateRedirectUri(clientMetadata, input.redirectUri)) {
|
|
2165
|
+
sharedLogger.error("oauth_authorize", {
|
|
2166
|
+
message: "redirect_uri not in client metadata",
|
|
2167
|
+
clientId: input.clientId,
|
|
2168
|
+
redirectUri: input.redirectUri,
|
|
2169
|
+
allowedUris: clientMetadata.redirect_uris
|
|
2170
|
+
});
|
|
2171
|
+
throw new Error("invalid_request: redirect_uri not registered for this client");
|
|
2172
|
+
}
|
|
2173
|
+
sharedLogger.info("oauth_authorize", {
|
|
2174
|
+
message: "CIMD client validated",
|
|
2175
|
+
clientId: input.clientId,
|
|
2176
|
+
clientName: clientMetadata.client_name
|
|
2177
|
+
});
|
|
2178
|
+
}
|
|
2179
|
+
const txnId = generateOpaqueToken(16);
|
|
2180
|
+
await store.saveTransaction(txnId, {
|
|
2181
|
+
codeChallenge: input.codeChallenge,
|
|
2182
|
+
state: input.state,
|
|
2183
|
+
createdAt: Date.now(),
|
|
2184
|
+
scope: input.requestedScope,
|
|
2185
|
+
sid: input.sid,
|
|
2186
|
+
clientRedirectUri: input.redirectUri
|
|
2187
|
+
});
|
|
2188
|
+
sharedLogger.debug("oauth_authorize", {
|
|
2189
|
+
message: "Transaction saved",
|
|
2190
|
+
txnId,
|
|
2191
|
+
redirectUri: input.redirectUri
|
|
2192
|
+
});
|
|
2193
|
+
sharedLogger.debug("oauth_authorize", {
|
|
2194
|
+
message: "Checking provider configuration",
|
|
2195
|
+
hasClientId: !!providerConfig.clientId,
|
|
2196
|
+
hasClientSecret: !!providerConfig.clientSecret
|
|
2197
|
+
});
|
|
2198
|
+
if (providerConfig.clientId && providerConfig.clientSecret) {
|
|
2199
|
+
sharedLogger.info("oauth_authorize", {
|
|
2200
|
+
message: "Using production flow - redirecting to provider"
|
|
2201
|
+
});
|
|
2202
|
+
const authServer = buildAuthorizationServer2(providerConfig);
|
|
2203
|
+
const authorizationEndpoint = authServer.authorization_endpoint;
|
|
2204
|
+
if (!authorizationEndpoint) {
|
|
2205
|
+
throw new Error("Authorization endpoint not configured");
|
|
2206
|
+
}
|
|
2207
|
+
const authUrl = new URL(authorizationEndpoint);
|
|
2208
|
+
authUrl.searchParams.set("response_type", "code");
|
|
2209
|
+
authUrl.searchParams.set("client_id", providerConfig.clientId);
|
|
2210
|
+
const callbackPath = options.callbackPath || "/oauth/callback";
|
|
2211
|
+
const cb = new URL(callbackPath, options.baseUrl).toString();
|
|
2212
|
+
authUrl.searchParams.set("redirect_uri", cb);
|
|
2213
|
+
const scopeToUse = providerConfig.oauthScopes || input.requestedScope || "";
|
|
2214
|
+
if (scopeToUse) {
|
|
2215
|
+
authUrl.searchParams.set("scope", scopeToUse);
|
|
2216
|
+
}
|
|
2217
|
+
const compositeState = base64UrlEncodeJson({
|
|
2218
|
+
tid: txnId,
|
|
2219
|
+
cs: input.state,
|
|
2220
|
+
cr: input.redirectUri,
|
|
2221
|
+
sid: input.sid
|
|
2222
|
+
}) || txnId;
|
|
2223
|
+
authUrl.searchParams.set("state", compositeState);
|
|
2224
|
+
if (providerConfig.extraAuthParams) {
|
|
2225
|
+
const extraParams = new URLSearchParams(providerConfig.extraAuthParams);
|
|
2226
|
+
for (const [key, value] of extraParams) {
|
|
2227
|
+
authUrl.searchParams.set(key, value);
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
sharedLogger.debug("oauth_authorize", {
|
|
2231
|
+
message: "Redirect URL constructed",
|
|
2232
|
+
url: authUrl.origin + authUrl.pathname,
|
|
2233
|
+
hasExtraParams: !!providerConfig.extraAuthParams
|
|
2234
|
+
});
|
|
2235
|
+
return {
|
|
2236
|
+
redirectTo: authUrl.toString(),
|
|
2237
|
+
txnId
|
|
2238
|
+
};
|
|
2239
|
+
}
|
|
2240
|
+
sharedLogger.warning("oauth_authorize", {
|
|
2241
|
+
message: "Missing provider credentials - using dev shortcut"
|
|
2242
|
+
});
|
|
2243
|
+
const code = generateOpaqueToken(16);
|
|
2244
|
+
await store.saveCode(code, txnId);
|
|
2245
|
+
const safe = isAllowedRedirect(input.redirectUri, oauthConfig, options.isDev) ? input.redirectUri : oauthConfig.redirectUri;
|
|
2246
|
+
const redirect = new URL(safe);
|
|
2247
|
+
redirect.searchParams.set("code", code);
|
|
2248
|
+
if (input.state) {
|
|
2249
|
+
redirect.searchParams.set("state", input.state);
|
|
2250
|
+
}
|
|
2251
|
+
return {
|
|
2252
|
+
redirectTo: redirect.toString(),
|
|
2253
|
+
txnId
|
|
2254
|
+
};
|
|
2255
|
+
}
|
|
2256
|
+
async function handleProviderCallback(input, store, providerConfig, oauthConfig, options) {
|
|
2257
|
+
const decodedObj = base64UrlDecodeJson(input.compositeState);
|
|
2258
|
+
let decoded;
|
|
2259
|
+
if (decodedObj) {
|
|
2260
|
+
decoded = decodedObj;
|
|
2261
|
+
sharedLogger.debug("oauth_callback", {
|
|
2262
|
+
message: "State decoded successfully",
|
|
2263
|
+
decoded
|
|
2264
|
+
});
|
|
2265
|
+
} else {
|
|
2266
|
+
sharedLogger.debug("oauth_callback", {
|
|
2267
|
+
message: "State is not JSON-encoded, treating as raw txnId",
|
|
2268
|
+
compositeState: input.compositeState
|
|
2269
|
+
});
|
|
2270
|
+
decoded = {};
|
|
2271
|
+
}
|
|
2272
|
+
sharedLogger.debug("oauth_callback", {
|
|
2273
|
+
message: "State decoded",
|
|
2274
|
+
compositeState: input.compositeState,
|
|
2275
|
+
decoded
|
|
2276
|
+
});
|
|
2277
|
+
if (!decoded.tid && !input.compositeState) {
|
|
2278
|
+
sharedLogger.error("oauth_callback", {
|
|
2279
|
+
message: "Invalid state parameter",
|
|
2280
|
+
compositeState: input.compositeState
|
|
2281
|
+
});
|
|
2282
|
+
throw new Error("invalid_state");
|
|
2283
|
+
}
|
|
2284
|
+
const txnId = decoded.tid || input.compositeState;
|
|
2285
|
+
const txn = await store.getTransaction(txnId);
|
|
2286
|
+
if (!txn) {
|
|
2287
|
+
sharedLogger.error("oauth_callback", {
|
|
2288
|
+
message: "Transaction not found",
|
|
2289
|
+
txnId,
|
|
2290
|
+
decoded,
|
|
2291
|
+
compositeStateLength: input.compositeState?.length
|
|
2292
|
+
});
|
|
2293
|
+
throw new Error("unknown_txn");
|
|
2294
|
+
}
|
|
2295
|
+
const callbackPath = options.callbackPath || "/oauth/callback";
|
|
2296
|
+
const redirectUri = new URL(callbackPath, options.baseUrl).toString();
|
|
2297
|
+
const authServer = buildAuthorizationServer2(providerConfig);
|
|
2298
|
+
const client = buildOAuthClient(providerConfig);
|
|
2299
|
+
sharedLogger.debug("oauth_callback", {
|
|
2300
|
+
message: "Exchanging code for tokens",
|
|
2301
|
+
tokenUrl: authServer.token_endpoint
|
|
2302
|
+
});
|
|
2303
|
+
const rawParams = new URLSearchParams;
|
|
2304
|
+
rawParams.set("code", input.providerCode);
|
|
2305
|
+
const callbackParams = oauth2.validateAuthResponse(authServer, client, rawParams, oauth2.skipStateCheck);
|
|
2306
|
+
if (!providerConfig.clientSecret) {
|
|
2307
|
+
throw new Error("Server misconfigured: PROVIDER_CLIENT_SECRET is not set");
|
|
2308
|
+
}
|
|
2309
|
+
const clientAuth = oauth2.ClientSecretPost(providerConfig.clientSecret);
|
|
2310
|
+
try {
|
|
2311
|
+
const response = await oauth2.authorizationCodeGrantRequest(authServer, client, clientAuth, callbackParams, redirectUri, oauth2.nopkce);
|
|
2312
|
+
sharedLogger.debug("oauth_callback", {
|
|
2313
|
+
message: "Token response received",
|
|
2314
|
+
status: response.status
|
|
2315
|
+
});
|
|
2316
|
+
const result = await oauth2.processAuthorizationCodeResponse(authServer, client, response);
|
|
2317
|
+
const accessToken = result.access_token;
|
|
2318
|
+
if (!accessToken) {
|
|
2319
|
+
sharedLogger.error("oauth_callback", {
|
|
2320
|
+
message: "No access token in provider response"
|
|
2321
|
+
});
|
|
2322
|
+
throw new Error("provider_no_token");
|
|
2323
|
+
}
|
|
2324
|
+
const expiresIn = result.expires_in ?? 3600;
|
|
2325
|
+
const expiresAt = Date.now() + expiresIn * 1000;
|
|
2326
|
+
const scopes = (result.scope || "").split(/\s+/).filter(Boolean);
|
|
2327
|
+
const providerTokens = {
|
|
2328
|
+
access_token: accessToken,
|
|
2329
|
+
refresh_token: result.refresh_token,
|
|
2330
|
+
expires_at: expiresAt,
|
|
2331
|
+
scopes
|
|
2332
|
+
};
|
|
2333
|
+
sharedLogger.info("oauth_callback", {
|
|
2334
|
+
message: "Provider tokens received",
|
|
2335
|
+
hasRefreshToken: !!result.refresh_token,
|
|
2336
|
+
expiresIn
|
|
2337
|
+
});
|
|
2338
|
+
txn.provider = providerTokens;
|
|
2339
|
+
await store.saveTransaction(txnId, txn);
|
|
2340
|
+
const asCode = generateOpaqueToken(24);
|
|
2341
|
+
await store.saveCode(asCode, txnId);
|
|
2342
|
+
sharedLogger.debug("oauth_callback", {
|
|
2343
|
+
message: "RS code generated"
|
|
2344
|
+
});
|
|
2345
|
+
const safe = txn.clientRedirectUri ?? (decoded.cr && isAllowedRedirect(decoded.cr, oauthConfig, options.isDev) ? decoded.cr : oauthConfig.redirectUri);
|
|
2346
|
+
const redirect = new URL(safe);
|
|
2347
|
+
redirect.searchParams.set("code", asCode);
|
|
2348
|
+
if (decoded.cs) {
|
|
2349
|
+
redirect.searchParams.set("state", decoded.cs);
|
|
2350
|
+
}
|
|
2351
|
+
return {
|
|
2352
|
+
redirectTo: redirect.toString(),
|
|
2353
|
+
txnId,
|
|
2354
|
+
providerTokens
|
|
2355
|
+
};
|
|
2356
|
+
} catch (error) {
|
|
2357
|
+
if (error instanceof oauth2.ResponseBodyError) {
|
|
2358
|
+
sharedLogger.error("oauth_callback", {
|
|
2359
|
+
message: "Provider token error",
|
|
2360
|
+
error: error.error,
|
|
2361
|
+
description: error.error_description
|
|
2362
|
+
});
|
|
2363
|
+
throw new Error(`provider_token_error: ${error.error} ${error.error_description || ""}`.trim());
|
|
2364
|
+
}
|
|
2365
|
+
sharedLogger.error("oauth_callback", {
|
|
2366
|
+
message: "Token fetch failed",
|
|
2367
|
+
error: error.message
|
|
2368
|
+
});
|
|
2369
|
+
throw new Error(`fetch_failed: ${error.message}`);
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
async function handleToken(input, store, providerConfig) {
|
|
2373
|
+
if (input.grant === "refresh_token") {
|
|
2374
|
+
sharedLogger.debug("oauth_token", {
|
|
2375
|
+
message: "Processing refresh_token grant"
|
|
2376
|
+
});
|
|
2377
|
+
const rec = await store.getByRsRefresh(input.refreshToken);
|
|
2378
|
+
if (!rec) {
|
|
2379
|
+
sharedLogger.error("oauth_token", {
|
|
2380
|
+
message: "Invalid refresh token"
|
|
2381
|
+
});
|
|
2382
|
+
throw new Error("invalid_grant");
|
|
2383
|
+
}
|
|
2384
|
+
const now = Date.now();
|
|
2385
|
+
const providerExpiresAt = rec.provider.expires_at ?? 0;
|
|
2386
|
+
const isExpiringSoon = now >= providerExpiresAt - 60000;
|
|
2387
|
+
let provider = rec.provider;
|
|
2388
|
+
if (isExpiringSoon && providerConfig) {
|
|
2389
|
+
sharedLogger.info("oauth_token", {
|
|
2390
|
+
message: "Provider token expired/expiring, refreshing",
|
|
2391
|
+
expiresAt: providerExpiresAt,
|
|
2392
|
+
now
|
|
2393
|
+
});
|
|
2394
|
+
if (!rec.provider.refresh_token) {
|
|
2395
|
+
sharedLogger.error("oauth_token", {
|
|
2396
|
+
message: "No provider refresh token available"
|
|
2397
|
+
});
|
|
2398
|
+
throw new Error("provider_token_expired");
|
|
2399
|
+
}
|
|
2400
|
+
const refreshResult = await refreshProviderToken(rec.provider.refresh_token, {
|
|
2401
|
+
clientId: providerConfig.clientId || "",
|
|
2402
|
+
clientSecret: providerConfig.clientSecret || "",
|
|
2403
|
+
accountsUrl: providerConfig.accountsUrl,
|
|
2404
|
+
tokenEndpointPath: providerConfig.tokenEndpointPath
|
|
2405
|
+
});
|
|
2406
|
+
if (!refreshResult.success || !refreshResult.tokens) {
|
|
2407
|
+
sharedLogger.error("oauth_token", {
|
|
2408
|
+
message: "Provider refresh failed",
|
|
2409
|
+
error: refreshResult.error
|
|
2410
|
+
});
|
|
2411
|
+
throw new Error("provider_refresh_failed");
|
|
2412
|
+
}
|
|
2413
|
+
provider = refreshResult.tokens;
|
|
2414
|
+
}
|
|
2415
|
+
const providerRefreshRotated = provider.refresh_token !== rec.provider.refresh_token;
|
|
2416
|
+
const newAccess = providerRefreshRotated ? generateOpaqueToken(24) : undefined;
|
|
2417
|
+
const updated = await store.updateByRsRefresh(input.refreshToken, provider, newAccess);
|
|
2418
|
+
const expiresIn = provider.expires_at ? Math.max(1, Math.floor((provider.expires_at - Date.now()) / 1000)) : 3600;
|
|
2419
|
+
sharedLogger.info("oauth_token", {
|
|
2420
|
+
message: "Token refreshed successfully",
|
|
2421
|
+
providerRefreshed: isExpiringSoon,
|
|
2422
|
+
rsAccessRotated: providerRefreshRotated
|
|
2423
|
+
});
|
|
2424
|
+
return {
|
|
2425
|
+
access_token: newAccess ?? rec.rs_access_token,
|
|
2426
|
+
refresh_token: input.refreshToken,
|
|
2427
|
+
token_type: "bearer",
|
|
2428
|
+
expires_in: expiresIn,
|
|
2429
|
+
scope: (updated?.provider.scopes || []).join(" ")
|
|
2430
|
+
};
|
|
2431
|
+
}
|
|
2432
|
+
sharedLogger.debug("oauth_token", {
|
|
2433
|
+
message: "Processing authorization_code grant"
|
|
2434
|
+
});
|
|
2435
|
+
const txnId = await store.getTxnIdByCode(input.code);
|
|
2436
|
+
if (!txnId) {
|
|
2437
|
+
sharedLogger.error("oauth_token", {
|
|
2438
|
+
message: "Authorization code not found"
|
|
2439
|
+
});
|
|
2440
|
+
throw new Error("invalid_grant");
|
|
2441
|
+
}
|
|
2442
|
+
const txn = await store.getTransaction(txnId);
|
|
2443
|
+
if (!txn) {
|
|
2444
|
+
sharedLogger.error("oauth_token", {
|
|
2445
|
+
message: "Transaction not found for code"
|
|
2446
|
+
});
|
|
2447
|
+
throw new Error("invalid_grant");
|
|
2448
|
+
}
|
|
2449
|
+
const expected = txn.codeChallenge;
|
|
2450
|
+
const actual = await oauth2.calculatePKCECodeChallenge(input.codeVerifier);
|
|
2451
|
+
if (expected !== actual) {
|
|
2452
|
+
sharedLogger.error("oauth_token", {
|
|
2453
|
+
message: "PKCE verification failed"
|
|
2454
|
+
});
|
|
2455
|
+
throw new Error("invalid_grant");
|
|
2456
|
+
}
|
|
2457
|
+
const rsAccess = generateOpaqueToken(24);
|
|
2458
|
+
const rsRefresh = generateOpaqueToken(24);
|
|
2459
|
+
sharedLogger.debug("oauth_token", {
|
|
2460
|
+
message: "Minting RS tokens",
|
|
2461
|
+
hasProviderTokens: !!txn.provider?.access_token
|
|
2462
|
+
});
|
|
2463
|
+
if (txn.provider?.access_token) {
|
|
2464
|
+
await store.storeRsMapping(rsAccess, txn.provider, rsRefresh);
|
|
2465
|
+
sharedLogger.info("oauth_token", {
|
|
2466
|
+
message: "RS→Provider mapping stored"
|
|
2467
|
+
});
|
|
2468
|
+
} else {
|
|
2469
|
+
sharedLogger.warning("oauth_token", {
|
|
2470
|
+
message: "No provider tokens in transaction - RS mapping not created"
|
|
2471
|
+
});
|
|
2472
|
+
}
|
|
2473
|
+
await store.deleteTransaction(txnId);
|
|
2474
|
+
await store.deleteCode(input.code);
|
|
2475
|
+
sharedLogger.info("oauth_token", {
|
|
2476
|
+
message: "Token exchange completed"
|
|
2477
|
+
});
|
|
2478
|
+
return {
|
|
2479
|
+
access_token: rsAccess,
|
|
2480
|
+
refresh_token: rsRefresh,
|
|
2481
|
+
token_type: "bearer",
|
|
2482
|
+
expires_in: 3600,
|
|
2483
|
+
scope: (txn.provider?.scopes || []).join(" ") || txn.scope || ""
|
|
2484
|
+
};
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
// src/shared/oauth/endpoints.ts
|
|
2488
|
+
async function handleRegister(input, baseUrl, defaultRedirectUri) {
|
|
2489
|
+
const now = Math.floor(Date.now() / 1000);
|
|
2490
|
+
const clientId = generateOpaqueToken(12);
|
|
2491
|
+
const redirectUris = Array.isArray(input.redirect_uris) ? input.redirect_uris : [defaultRedirectUri];
|
|
2492
|
+
const grantTypes = Array.isArray(input.grant_types) ? input.grant_types : ["authorization_code", "refresh_token"];
|
|
2493
|
+
const responseTypes = Array.isArray(input.response_types) ? input.response_types : ["code"];
|
|
2494
|
+
return {
|
|
2495
|
+
client_id: clientId,
|
|
2496
|
+
client_id_issued_at: now,
|
|
2497
|
+
client_secret_expires_at: 0,
|
|
2498
|
+
token_endpoint_auth_method: "none",
|
|
2499
|
+
redirect_uris: redirectUris,
|
|
2500
|
+
grant_types: grantTypes,
|
|
2501
|
+
response_types: responseTypes,
|
|
2502
|
+
registration_client_uri: `${baseUrl}/register/${clientId}`,
|
|
2503
|
+
registration_access_token: generateOpaqueToken(12),
|
|
2504
|
+
...input.client_name ? { client_name: input.client_name } : {}
|
|
2505
|
+
};
|
|
2506
|
+
}
|
|
2507
|
+
async function handleRevoke() {
|
|
2508
|
+
return { status: "ok" };
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
// src/shared/oauth/input-parsers.ts
|
|
2512
|
+
function parseAuthorizeInput(url, sessionId) {
|
|
2513
|
+
return {
|
|
2514
|
+
clientId: url.searchParams.get("client_id") ?? undefined,
|
|
2515
|
+
codeChallenge: url.searchParams.get("code_challenge") || "",
|
|
2516
|
+
codeChallengeMethod: url.searchParams.get("code_challenge_method") || "",
|
|
2517
|
+
redirectUri: url.searchParams.get("redirect_uri") || "",
|
|
2518
|
+
requestedScope: url.searchParams.get("scope") ?? undefined,
|
|
2519
|
+
state: url.searchParams.get("state") ?? undefined,
|
|
2520
|
+
sid: url.searchParams.get("sid") || sessionId || undefined
|
|
2521
|
+
};
|
|
2522
|
+
}
|
|
2523
|
+
function parseCallbackInput(url) {
|
|
2524
|
+
return {
|
|
2525
|
+
code: url.searchParams.get("code"),
|
|
2526
|
+
state: url.searchParams.get("state")
|
|
2527
|
+
};
|
|
2528
|
+
}
|
|
2529
|
+
async function parseTokenInput(request) {
|
|
2530
|
+
const contentType = request.headers.get("content-type") || "";
|
|
2531
|
+
if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
2532
|
+
const text = await request.text();
|
|
2533
|
+
return new URLSearchParams(text);
|
|
2534
|
+
}
|
|
2535
|
+
const json = await request.json().catch(() => ({}));
|
|
2536
|
+
return new URLSearchParams(json);
|
|
2537
|
+
}
|
|
2538
|
+
function buildTokenInput(form) {
|
|
2539
|
+
const grant = form.get("grant_type");
|
|
2540
|
+
if (grant === "refresh_token") {
|
|
2541
|
+
const refreshToken = form.get("refresh_token");
|
|
2542
|
+
if (!refreshToken) {
|
|
2543
|
+
return { error: "missing_refresh_token" };
|
|
2544
|
+
}
|
|
2545
|
+
return { grant: "refresh_token", refreshToken };
|
|
2546
|
+
}
|
|
2547
|
+
if (grant === "authorization_code") {
|
|
2548
|
+
const code = form.get("code");
|
|
2549
|
+
const codeVerifier = form.get("code_verifier");
|
|
2550
|
+
if (!code || !codeVerifier) {
|
|
2551
|
+
return { error: "missing_code_or_verifier" };
|
|
2552
|
+
}
|
|
2553
|
+
return { grant: "authorization_code", code, codeVerifier };
|
|
2554
|
+
}
|
|
2555
|
+
return { error: "unsupported_grant_type" };
|
|
2556
|
+
}
|
|
2557
|
+
function buildProviderConfig(config) {
|
|
2558
|
+
return {
|
|
2559
|
+
clientId: config.PROVIDER_CLIENT_ID,
|
|
2560
|
+
clientSecret: config.PROVIDER_CLIENT_SECRET,
|
|
2561
|
+
accountsUrl: config.PROVIDER_ACCOUNTS_URL || "https://provider.example.com",
|
|
2562
|
+
oauthScopes: config.OAUTH_SCOPES,
|
|
2563
|
+
extraAuthParams: config.OAUTH_EXTRA_AUTH_PARAMS,
|
|
2564
|
+
authorizationEndpointPath: config.OAUTH_AUTHORIZATION_URL,
|
|
2565
|
+
tokenEndpointPath: config.OAUTH_TOKEN_URL
|
|
2566
|
+
};
|
|
2567
|
+
}
|
|
2568
|
+
function buildOAuthConfig(config) {
|
|
2569
|
+
return {
|
|
2570
|
+
redirectUri: config.OAUTH_REDIRECT_URI,
|
|
2571
|
+
redirectAllowlist: config.OAUTH_REDIRECT_ALLOWLIST,
|
|
2572
|
+
redirectAllowAll: config.OAUTH_REDIRECT_ALLOW_ALL
|
|
2573
|
+
};
|
|
2574
|
+
}
|
|
2575
|
+
function buildFlowOptions(url, config, overrides = {}) {
|
|
2576
|
+
return {
|
|
2577
|
+
baseUrl: config.BASE_URL ?? url.origin,
|
|
2578
|
+
isDev: config.NODE_ENV === "development",
|
|
2579
|
+
callbackPath: overrides.callbackPath ?? "/oauth/provider-callback",
|
|
2580
|
+
tokenEndpointPath: overrides.tokenEndpointPath ?? "/api/token"
|
|
2581
|
+
};
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
// src/adapters/http-worker/index.ts
|
|
2585
|
+
import { Router } from "itty-router";
|
|
2586
|
+
|
|
2587
|
+
// src/adapters/http-worker/security.ts
|
|
2588
|
+
async function checkAuthAndChallenge(request, store, config, sid) {
|
|
2589
|
+
try {
|
|
2590
|
+
validateOrigin(request.headers, config.NODE_ENV === "development");
|
|
2591
|
+
validateProtocolVersion(request.headers, config.MCP_PROTOCOL_VERSION);
|
|
2592
|
+
} catch (error) {
|
|
2593
|
+
const challenge = buildUnauthorizedChallenge({
|
|
2594
|
+
origin: new URL(request.url).origin,
|
|
2595
|
+
sid,
|
|
2596
|
+
message: error.message
|
|
2597
|
+
});
|
|
2598
|
+
const resp = new Response(JSON.stringify(challenge.body), {
|
|
2599
|
+
status: challenge.status,
|
|
2600
|
+
headers: {
|
|
2601
|
+
"Content-Type": "application/json",
|
|
2602
|
+
"Mcp-Session-Id": sid,
|
|
2603
|
+
"WWW-Authenticate": challenge.headers["WWW-Authenticate"]
|
|
2604
|
+
}
|
|
2605
|
+
});
|
|
2606
|
+
return withCors(resp);
|
|
2607
|
+
}
|
|
2608
|
+
if (!config.AUTH_ENABLED) {
|
|
2609
|
+
return null;
|
|
2610
|
+
}
|
|
2611
|
+
const authHeader = request.headers.get("Authorization");
|
|
2612
|
+
const apiKeyHeader = request.headers.get("x-api-key") || request.headers.get("x-auth-token");
|
|
2613
|
+
if (!authHeader && !apiKeyHeader) {
|
|
2614
|
+
const origin = new URL(request.url).origin;
|
|
2615
|
+
const challenge = buildUnauthorizedChallenge({ origin, sid });
|
|
2616
|
+
const resp = new Response(JSON.stringify(challenge.body), {
|
|
2617
|
+
status: challenge.status,
|
|
2618
|
+
headers: {
|
|
2619
|
+
"Content-Type": "application/json",
|
|
2620
|
+
"Mcp-Session-Id": sid,
|
|
2621
|
+
"WWW-Authenticate": challenge.headers["WWW-Authenticate"]
|
|
2622
|
+
}
|
|
2623
|
+
});
|
|
2624
|
+
return withCors(resp);
|
|
2625
|
+
}
|
|
2626
|
+
if (config.AUTH_REQUIRE_RS && authHeader) {
|
|
2627
|
+
const match = authHeader.match(/^\s*Bearer\s+(.+)$/i);
|
|
2628
|
+
const bearer = match?.[1];
|
|
2629
|
+
if (bearer) {
|
|
2630
|
+
const record = await store.getByRsAccess(bearer);
|
|
2631
|
+
const hasMapping = !!record?.provider?.access_token;
|
|
2632
|
+
if (!hasMapping && !config.AUTH_ALLOW_DIRECT_BEARER) {
|
|
2633
|
+
const origin = new URL(request.url).origin;
|
|
2634
|
+
const challenge = buildUnauthorizedChallenge({ origin, sid });
|
|
2635
|
+
const resp = new Response(JSON.stringify(challenge.body), {
|
|
2636
|
+
status: challenge.status,
|
|
2637
|
+
headers: {
|
|
2638
|
+
"Content-Type": "application/json",
|
|
2639
|
+
"Mcp-Session-Id": sid,
|
|
2640
|
+
"WWW-Authenticate": challenge.headers["WWW-Authenticate"]
|
|
2641
|
+
}
|
|
2642
|
+
});
|
|
2643
|
+
return withCors(resp);
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
return null;
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
// src/adapters/http-worker/mcp.handler.ts
|
|
2651
|
+
var sessionStateMap = new Map;
|
|
2652
|
+
var cancellationRegistryMap = new Map;
|
|
2653
|
+
function getCancellationRegistry(sessionId) {
|
|
2654
|
+
let registry = cancellationRegistryMap.get(sessionId);
|
|
2655
|
+
if (!registry) {
|
|
2656
|
+
registry = new Map;
|
|
2657
|
+
cancellationRegistryMap.set(sessionId, registry);
|
|
2658
|
+
}
|
|
2659
|
+
return registry;
|
|
2660
|
+
}
|
|
2661
|
+
function getJsonRpcMessages(body) {
|
|
2662
|
+
if (!body || typeof body !== "object")
|
|
2663
|
+
return [];
|
|
2664
|
+
if (Array.isArray(body)) {
|
|
2665
|
+
return body.filter((msg) => msg && typeof msg === "object");
|
|
2666
|
+
}
|
|
2667
|
+
return [body];
|
|
2668
|
+
}
|
|
2669
|
+
function resolveSessionApiKey(headers, config) {
|
|
2670
|
+
const apiKeyHeader = config.API_KEY_HEADER.toLowerCase();
|
|
2671
|
+
const directApiKey = headers.get(apiKeyHeader) || headers.get("x-api-key") || headers.get("x-auth-token");
|
|
2672
|
+
if (directApiKey)
|
|
2673
|
+
return directApiKey;
|
|
2674
|
+
const authHeader = headers.get("authorization") || headers.get("Authorization");
|
|
2675
|
+
if (authHeader) {
|
|
2676
|
+
const match = authHeader.match(/^\s*Bearer\s+(.+)$/i);
|
|
2677
|
+
return match?.[1] ?? authHeader;
|
|
2678
|
+
}
|
|
2679
|
+
if (config.API_KEY)
|
|
2680
|
+
return config.API_KEY;
|
|
2681
|
+
return "public";
|
|
2682
|
+
}
|
|
2683
|
+
function parseCustomHeaders(value) {
|
|
2684
|
+
if (!value)
|
|
2685
|
+
return {};
|
|
2686
|
+
const headers = {};
|
|
2687
|
+
for (const pair of value.split(",")) {
|
|
2688
|
+
const colonIndex = pair.indexOf(":");
|
|
2689
|
+
if (colonIndex === -1)
|
|
2690
|
+
continue;
|
|
2691
|
+
const key = pair.slice(0, colonIndex).trim();
|
|
2692
|
+
const val = pair.slice(colonIndex + 1).trim();
|
|
2693
|
+
if (key && val) {
|
|
2694
|
+
headers[key.toLowerCase()] = val;
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
return headers;
|
|
2698
|
+
}
|
|
2699
|
+
function buildStaticAuthHeaders(config) {
|
|
2700
|
+
const headers = {};
|
|
2701
|
+
switch (config.AUTH_STRATEGY) {
|
|
2702
|
+
case "api_key":
|
|
2703
|
+
if (config.API_KEY) {
|
|
2704
|
+
headers[config.API_KEY_HEADER.toLowerCase()] = config.API_KEY;
|
|
2705
|
+
}
|
|
2706
|
+
break;
|
|
2707
|
+
case "bearer":
|
|
2708
|
+
if (config.BEARER_TOKEN) {
|
|
2709
|
+
headers.authorization = `Bearer ${config.BEARER_TOKEN}`;
|
|
2710
|
+
}
|
|
2711
|
+
break;
|
|
2712
|
+
case "custom":
|
|
2713
|
+
Object.assign(headers, parseCustomHeaders(config.CUSTOM_HEADERS));
|
|
2714
|
+
break;
|
|
2715
|
+
}
|
|
2716
|
+
return headers;
|
|
2717
|
+
}
|
|
2718
|
+
function buildProviderRefreshConfig2(config) {
|
|
2719
|
+
if (!config.PROVIDER_CLIENT_ID || !config.PROVIDER_CLIENT_SECRET || !config.PROVIDER_ACCOUNTS_URL) {
|
|
2720
|
+
return;
|
|
2721
|
+
}
|
|
2722
|
+
return {
|
|
2723
|
+
clientId: config.PROVIDER_CLIENT_ID,
|
|
2724
|
+
clientSecret: config.PROVIDER_CLIENT_SECRET,
|
|
2725
|
+
accountsUrl: config.PROVIDER_ACCOUNTS_URL
|
|
2726
|
+
};
|
|
2727
|
+
}
|
|
2728
|
+
async function resolveAuthContext(request, tokenStore, config) {
|
|
2729
|
+
const rawHeaders = {};
|
|
2730
|
+
request.headers.forEach((value, key) => {
|
|
2731
|
+
rawHeaders[key.toLowerCase()] = value;
|
|
2732
|
+
});
|
|
2733
|
+
const strategy = config.AUTH_STRATEGY;
|
|
2734
|
+
let providerToken;
|
|
2735
|
+
let provider;
|
|
2736
|
+
let resolvedHeaders = { ...rawHeaders };
|
|
2737
|
+
if (strategy === "oauth") {
|
|
2738
|
+
const authHeader = rawHeaders.authorization;
|
|
2739
|
+
const match = authHeader?.match(/^\s*Bearer\s+(.+)$/i);
|
|
2740
|
+
const rsToken = match?.[1];
|
|
2741
|
+
if (rsToken) {
|
|
2742
|
+
try {
|
|
2743
|
+
const providerConfig = buildProviderRefreshConfig2(config);
|
|
2744
|
+
const { accessToken, wasRefreshed } = await ensureFreshToken(rsToken, tokenStore, providerConfig);
|
|
2745
|
+
if (accessToken) {
|
|
2746
|
+
providerToken = accessToken;
|
|
2747
|
+
const record = await tokenStore.getByRsAccess(rsToken);
|
|
2748
|
+
if (record?.provider) {
|
|
2749
|
+
provider = {
|
|
2750
|
+
accessToken: record.provider.access_token,
|
|
2751
|
+
refreshToken: record.provider.refresh_token,
|
|
2752
|
+
expiresAt: record.provider.expires_at,
|
|
2753
|
+
scopes: record.provider.scopes
|
|
2754
|
+
};
|
|
2755
|
+
}
|
|
2756
|
+
resolvedHeaders.authorization = `Bearer ${accessToken}`;
|
|
2757
|
+
if (wasRefreshed) {
|
|
2758
|
+
sharedLogger.info("mcp_handler", {
|
|
2759
|
+
message: "Using proactively refreshed token"
|
|
2760
|
+
});
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
} catch (error) {
|
|
2764
|
+
sharedLogger.debug("mcp_handler", {
|
|
2765
|
+
message: "Token resolution failed",
|
|
2766
|
+
error: error.message
|
|
2767
|
+
});
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
} else if (strategy === "bearer" || strategy === "api_key" || strategy === "custom") {
|
|
2771
|
+
const staticHeaders = buildStaticAuthHeaders(config);
|
|
2772
|
+
resolvedHeaders = { ...rawHeaders, ...staticHeaders };
|
|
2773
|
+
providerToken = strategy === "bearer" ? config.BEARER_TOKEN : config.API_KEY;
|
|
2774
|
+
}
|
|
2775
|
+
return {
|
|
2776
|
+
sessionId: "",
|
|
2777
|
+
authStrategy: strategy,
|
|
2778
|
+
providerToken,
|
|
2779
|
+
provider,
|
|
2780
|
+
resolvedHeaders,
|
|
2781
|
+
authHeaders: rawHeaders
|
|
2782
|
+
};
|
|
2783
|
+
}
|
|
2784
|
+
async function handleMcpRequest(request, deps) {
|
|
2785
|
+
const { tokenStore, sessionStore, config } = deps;
|
|
2786
|
+
const body = await request.json().catch(() => ({}));
|
|
2787
|
+
const { method, params, id } = body;
|
|
2788
|
+
const messages = getJsonRpcMessages(body);
|
|
2789
|
+
const isInitialize = messages.some((msg) => msg.method === "initialize");
|
|
2790
|
+
const isInitialized = messages.some((msg) => msg.method === "initialized");
|
|
2791
|
+
const initMessage = messages.find((msg) => msg.method === "initialize");
|
|
2792
|
+
const protocolVersion = typeof initMessage?.params?.protocolVersion === "string" ? (initMessage?.params).protocolVersion : undefined;
|
|
2793
|
+
const incomingSessionId = request.headers.get("Mcp-Session-Id")?.trim();
|
|
2794
|
+
const sessionId = isInitialize ? crypto.randomUUID() : incomingSessionId || crypto.randomUUID();
|
|
2795
|
+
const apiKey = resolveSessionApiKey(request.headers, config);
|
|
2796
|
+
if (!isInitialize && !incomingSessionId) {
|
|
2797
|
+
return jsonResponse({
|
|
2798
|
+
jsonrpc: "2.0",
|
|
2799
|
+
error: {
|
|
2800
|
+
code: -32000,
|
|
2801
|
+
message: "Bad Request: Mcp-Session-Id required"
|
|
2802
|
+
},
|
|
2803
|
+
id: null
|
|
2804
|
+
}, { status: 400 });
|
|
2805
|
+
}
|
|
2806
|
+
if (!isInitialize && incomingSessionId) {
|
|
2807
|
+
let existingSession = null;
|
|
2808
|
+
try {
|
|
2809
|
+
existingSession = await sessionStore.get(incomingSessionId);
|
|
2810
|
+
} catch (error) {
|
|
2811
|
+
sharedLogger.warning("mcp_session", {
|
|
2812
|
+
message: "Session lookup failed",
|
|
2813
|
+
error: error.message
|
|
2814
|
+
});
|
|
2815
|
+
}
|
|
2816
|
+
if (!existingSession) {
|
|
2817
|
+
return withCors(new Response("Invalid session", { status: 404 }));
|
|
2818
|
+
}
|
|
2819
|
+
if (existingSession.apiKey && existingSession.apiKey !== apiKey) {
|
|
2820
|
+
sharedLogger.warning("mcp_session", {
|
|
2821
|
+
message: "Request API key differs from session binding",
|
|
2822
|
+
sessionId: incomingSessionId,
|
|
2823
|
+
originalApiKey: `${existingSession.apiKey.slice(0, 8)}...`,
|
|
2824
|
+
requestApiKey: `${apiKey.slice(0, 8)}...`
|
|
2825
|
+
});
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
const challengeResponse = await checkAuthAndChallenge(request, tokenStore, config, sessionId);
|
|
2829
|
+
if (challengeResponse) {
|
|
2830
|
+
return challengeResponse;
|
|
2831
|
+
}
|
|
2832
|
+
const authContext = await resolveAuthContext(request, tokenStore, config);
|
|
2833
|
+
authContext.sessionId = sessionId;
|
|
2834
|
+
if (isInitialize) {
|
|
2835
|
+
try {
|
|
2836
|
+
await sessionStore.create(sessionId, apiKey);
|
|
2837
|
+
if (protocolVersion) {
|
|
2838
|
+
await sessionStore.update(sessionId, { protocolVersion });
|
|
2839
|
+
}
|
|
2840
|
+
} catch (error) {
|
|
2841
|
+
sharedLogger.warning("mcp_session", {
|
|
2842
|
+
message: "Failed to create session record",
|
|
2843
|
+
error: error.message
|
|
2844
|
+
});
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
if (isInitialized) {
|
|
2848
|
+
try {
|
|
2849
|
+
await sessionStore.update(sessionId, { initialized: true });
|
|
2850
|
+
} catch (error) {
|
|
2851
|
+
sharedLogger.warning("mcp_session", {
|
|
2852
|
+
message: "Failed to update session initialized flag",
|
|
2853
|
+
error: error.message
|
|
2854
|
+
});
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
const cancellationRegistry = getCancellationRegistry(sessionId);
|
|
2858
|
+
const dispatchContext = {
|
|
2859
|
+
sessionId,
|
|
2860
|
+
auth: authContext,
|
|
2861
|
+
config: {
|
|
2862
|
+
title: config.MCP_TITLE,
|
|
2863
|
+
version: config.MCP_VERSION,
|
|
2864
|
+
instructions: config.MCP_INSTRUCTIONS
|
|
2865
|
+
},
|
|
2866
|
+
getSessionState: () => sessionStateMap.get(sessionId),
|
|
2867
|
+
setSessionState: (state) => sessionStateMap.set(sessionId, state),
|
|
2868
|
+
cancellationRegistry,
|
|
2869
|
+
tools: deps.tools
|
|
2870
|
+
};
|
|
2871
|
+
if (!("id" in body) || id === null || id === undefined) {
|
|
2872
|
+
if (method) {
|
|
2873
|
+
handleMcpNotification(method, params, dispatchContext);
|
|
2874
|
+
}
|
|
2875
|
+
return withCors(new Response(null, { status: 202 }));
|
|
2876
|
+
}
|
|
2877
|
+
const result = await dispatchMcpMethod(method, params, dispatchContext, id);
|
|
2878
|
+
const response = jsonResponse({
|
|
2879
|
+
jsonrpc: "2.0",
|
|
2880
|
+
...result.error ? { error: result.error } : { result: result.result },
|
|
2881
|
+
id
|
|
2882
|
+
});
|
|
2883
|
+
response.headers.set("Mcp-Session-Id", sessionId);
|
|
2884
|
+
return withCors(response);
|
|
2885
|
+
}
|
|
2886
|
+
function handleMcpGet() {
|
|
2887
|
+
return withCors(new Response("Method Not Allowed", { status: 405 }));
|
|
2888
|
+
}
|
|
2889
|
+
async function handleMcpDelete(request, deps) {
|
|
2890
|
+
const { sessionStore } = deps;
|
|
2891
|
+
const sessionId = request.headers.get("Mcp-Session-Id")?.trim();
|
|
2892
|
+
if (!sessionId) {
|
|
2893
|
+
return withCors(jsonResponse({
|
|
2894
|
+
jsonrpc: "2.0",
|
|
2895
|
+
error: {
|
|
2896
|
+
code: -32000,
|
|
2897
|
+
message: "Bad Request: Mcp-Session-Id required"
|
|
2898
|
+
},
|
|
2899
|
+
id: null
|
|
2900
|
+
}, { status: 400 }));
|
|
2901
|
+
}
|
|
2902
|
+
let existingSession = null;
|
|
2903
|
+
try {
|
|
2904
|
+
existingSession = await sessionStore.get(sessionId);
|
|
2905
|
+
} catch (error) {
|
|
2906
|
+
sharedLogger.warning("mcp_session", {
|
|
2907
|
+
message: "Session lookup failed on DELETE",
|
|
2908
|
+
error: error.message
|
|
2909
|
+
});
|
|
2910
|
+
}
|
|
2911
|
+
if (!existingSession) {
|
|
2912
|
+
return withCors(new Response("Invalid session", { status: 404 }));
|
|
2913
|
+
}
|
|
2914
|
+
sessionStateMap.delete(sessionId);
|
|
2915
|
+
cancellationRegistryMap.delete(sessionId);
|
|
2916
|
+
try {
|
|
2917
|
+
await sessionStore.delete(sessionId);
|
|
2918
|
+
sharedLogger.info("mcp_session", {
|
|
2919
|
+
message: "Session terminated via DELETE",
|
|
2920
|
+
sessionId
|
|
2921
|
+
});
|
|
2922
|
+
} catch (error) {
|
|
2923
|
+
sharedLogger.warning("mcp_session", {
|
|
2924
|
+
message: "Failed to delete session record",
|
|
2925
|
+
error: error.message
|
|
2926
|
+
});
|
|
2927
|
+
}
|
|
2928
|
+
return withCors(new Response(null, { status: 202 }));
|
|
2929
|
+
}
|
|
2930
|
+
|
|
2931
|
+
// src/adapters/http-worker/routes.discovery.ts
|
|
2932
|
+
function attachDiscoveryRoutes(router, config) {
|
|
2933
|
+
const { authorizationMetadata, protectedResourceMetadata } = createDiscoveryHandlers(config, workerDiscoveryStrategy);
|
|
2934
|
+
router.get("/.well-known/oauth-authorization-server", async (request) => {
|
|
2935
|
+
const metadata = authorizationMetadata(new URL(request.url));
|
|
2936
|
+
return jsonResponse(metadata);
|
|
2937
|
+
});
|
|
2938
|
+
router.get("/.well-known/oauth-protected-resource", async (request) => {
|
|
2939
|
+
const here = new URL(request.url);
|
|
2940
|
+
const sid = here.searchParams.get("sid") ?? undefined;
|
|
2941
|
+
const metadata = protectedResourceMetadata(here, sid);
|
|
2942
|
+
return jsonResponse(metadata);
|
|
2943
|
+
});
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
// src/adapters/http-worker/routes.oauth.ts
|
|
2947
|
+
function attachOAuthRoutes(router, store, config) {
|
|
2948
|
+
const providerConfig = buildProviderConfig(config);
|
|
2949
|
+
const oauthConfig = buildOAuthConfig(config);
|
|
2950
|
+
router.get("/authorize", async (request) => {
|
|
2951
|
+
sharedLogger.info("oauth_workers", { message: "Authorize request received" });
|
|
2952
|
+
try {
|
|
2953
|
+
const url = new URL(request.url);
|
|
2954
|
+
const sessionId = request.headers.get("Mcp-Session-Id") ?? undefined;
|
|
2955
|
+
const input = parseAuthorizeInput(url, sessionId);
|
|
2956
|
+
const options = {
|
|
2957
|
+
...buildFlowOptions(url, config),
|
|
2958
|
+
cimd: {
|
|
2959
|
+
enabled: config.CIMD_ENABLED,
|
|
2960
|
+
timeoutMs: config.CIMD_FETCH_TIMEOUT_MS,
|
|
2961
|
+
maxBytes: config.CIMD_MAX_RESPONSE_BYTES,
|
|
2962
|
+
allowedDomains: config.CIMD_ALLOWED_DOMAINS
|
|
2963
|
+
}
|
|
2964
|
+
};
|
|
2965
|
+
const result = await handleAuthorize(input, store, providerConfig, oauthConfig, options);
|
|
2966
|
+
sharedLogger.info("oauth_workers", {
|
|
2967
|
+
message: "Authorize redirect",
|
|
2968
|
+
redirectTo: result.redirectTo
|
|
2969
|
+
});
|
|
2970
|
+
return redirectResponse(result.redirectTo);
|
|
2971
|
+
} catch (error) {
|
|
2972
|
+
sharedLogger.error("oauth_workers", {
|
|
2973
|
+
message: "Authorize failed",
|
|
2974
|
+
error: error.message
|
|
2975
|
+
});
|
|
2976
|
+
return textError(error.message || "Authorization failed");
|
|
2977
|
+
}
|
|
2978
|
+
});
|
|
2979
|
+
router.get("/oauth/provider-callback", async (request) => {
|
|
2980
|
+
const url = new URL(request.url);
|
|
2981
|
+
const { code, state } = parseCallbackInput(url);
|
|
2982
|
+
sharedLogger.info("oauth_workers", {
|
|
2983
|
+
message: "Callback request received",
|
|
2984
|
+
hasCode: !!code,
|
|
2985
|
+
hasState: !!state,
|
|
2986
|
+
stateLength: state?.length
|
|
2987
|
+
});
|
|
2988
|
+
try {
|
|
2989
|
+
if (!code || !state) {
|
|
2990
|
+
return textError("invalid_callback: missing code or state");
|
|
2991
|
+
}
|
|
2992
|
+
if (!config.PROVIDER_CLIENT_ID || !config.PROVIDER_CLIENT_SECRET) {
|
|
2993
|
+
sharedLogger.error("oauth_workers", {
|
|
2994
|
+
message: "Missing provider credentials"
|
|
2995
|
+
});
|
|
2996
|
+
return textError("Server misconfigured: Missing provider credentials", {
|
|
2997
|
+
status: 500
|
|
2998
|
+
});
|
|
2999
|
+
}
|
|
3000
|
+
const options = buildFlowOptions(url, config);
|
|
3001
|
+
const result = await handleProviderCallback({ providerCode: code, compositeState: state }, store, providerConfig, oauthConfig, options);
|
|
3002
|
+
sharedLogger.info("oauth_workers", {
|
|
3003
|
+
message: "Callback success",
|
|
3004
|
+
redirectTo: result.redirectTo
|
|
3005
|
+
});
|
|
3006
|
+
return redirectResponse(result.redirectTo);
|
|
3007
|
+
} catch (error) {
|
|
3008
|
+
sharedLogger.error("oauth_workers", {
|
|
3009
|
+
message: "Callback failed",
|
|
3010
|
+
error: error.message
|
|
3011
|
+
});
|
|
3012
|
+
return textError(error.message || "Callback failed", {
|
|
3013
|
+
status: 500
|
|
3014
|
+
});
|
|
3015
|
+
}
|
|
3016
|
+
});
|
|
3017
|
+
router.get("/oauth/callback", async (request) => {
|
|
3018
|
+
const url = new URL(request.url);
|
|
3019
|
+
const code = url.searchParams.get("code");
|
|
3020
|
+
const state = url.searchParams.get("state");
|
|
3021
|
+
sharedLogger.info("oauth_workers", {
|
|
3022
|
+
message: "Client callback received",
|
|
3023
|
+
hasCode: !!code,
|
|
3024
|
+
hasState: !!state
|
|
3025
|
+
});
|
|
3026
|
+
return new Response(`<!DOCTYPE html><html><head><title>Authentication Complete</title></head><body>
|
|
3027
|
+
<h2>Authentication successful</h2>
|
|
3028
|
+
<p>You can close this window and return to your application.</p>
|
|
3029
|
+
<script>
|
|
3030
|
+
// Some MCP clients read the URL params from the opener window
|
|
3031
|
+
if (window.opener) {
|
|
3032
|
+
window.opener.postMessage({ type: 'oauth_callback', code: ${JSON.stringify(code)}, state: ${JSON.stringify(state)} }, '*');
|
|
3033
|
+
window.close();
|
|
3034
|
+
}
|
|
3035
|
+
</script>
|
|
3036
|
+
</body></html>`, { headers: { "content-type": "text/html; charset=utf-8" } });
|
|
3037
|
+
});
|
|
3038
|
+
router.post("/token", async (request) => {
|
|
3039
|
+
sharedLogger.debug("oauth_workers", { message: "Token request received" });
|
|
3040
|
+
try {
|
|
3041
|
+
const form = await parseTokenInput(request);
|
|
3042
|
+
const tokenInput = buildTokenInput(form);
|
|
3043
|
+
if ("error" in tokenInput) {
|
|
3044
|
+
return oauthError(tokenInput.error);
|
|
3045
|
+
}
|
|
3046
|
+
const result = await handleToken(tokenInput, store, providerConfig);
|
|
3047
|
+
sharedLogger.info("oauth_workers", { message: "Token exchange success" });
|
|
3048
|
+
return jsonResponse(result);
|
|
3049
|
+
} catch (error) {
|
|
3050
|
+
sharedLogger.error("oauth_workers", {
|
|
3051
|
+
message: "Token exchange failed",
|
|
3052
|
+
error: error.message
|
|
3053
|
+
});
|
|
3054
|
+
return oauthError(error.message || "invalid_grant");
|
|
3055
|
+
}
|
|
3056
|
+
});
|
|
3057
|
+
router.post("/revoke", async () => {
|
|
3058
|
+
const result = await handleRevoke();
|
|
3059
|
+
return jsonResponse(result);
|
|
3060
|
+
});
|
|
3061
|
+
router.post("/register", async (request) => {
|
|
3062
|
+
try {
|
|
3063
|
+
const body = await request.json().catch(() => ({}));
|
|
3064
|
+
const url = new URL(request.url);
|
|
3065
|
+
sharedLogger.debug("oauth_workers", { message: "Register request" });
|
|
3066
|
+
const result = await handleRegister({
|
|
3067
|
+
redirect_uris: Array.isArray(body.redirect_uris) ? body.redirect_uris : undefined,
|
|
3068
|
+
grant_types: Array.isArray(body.grant_types) ? body.grant_types : undefined,
|
|
3069
|
+
response_types: Array.isArray(body.response_types) ? body.response_types : undefined,
|
|
3070
|
+
client_name: typeof body.client_name === "string" ? body.client_name : undefined
|
|
3071
|
+
}, url.origin, config.OAUTH_REDIRECT_URI);
|
|
3072
|
+
sharedLogger.info("oauth_workers", { message: "Client registered" });
|
|
3073
|
+
return jsonResponse(result, { status: 201 });
|
|
3074
|
+
} catch (error) {
|
|
3075
|
+
return oauthError(error.message);
|
|
3076
|
+
}
|
|
3077
|
+
});
|
|
3078
|
+
}
|
|
3079
|
+
|
|
3080
|
+
// src/adapters/http-worker/index.ts
|
|
3081
|
+
var sharedTokenStore = null;
|
|
3082
|
+
var sharedSessionStore = null;
|
|
3083
|
+
function initializeWorkerStorage(env, config) {
|
|
3084
|
+
const kvNamespace = env.TOKENS;
|
|
3085
|
+
if (!kvNamespace) {
|
|
3086
|
+
sharedLogger.error("worker_storage", {
|
|
3087
|
+
message: "No KV namespace bound - storage unavailable"
|
|
3088
|
+
});
|
|
3089
|
+
return null;
|
|
3090
|
+
}
|
|
3091
|
+
if (!sharedTokenStore || !sharedSessionStore) {
|
|
3092
|
+
sharedTokenStore = new MemoryTokenStore;
|
|
3093
|
+
sharedSessionStore = new MemorySessionStore;
|
|
3094
|
+
}
|
|
3095
|
+
let encrypt2;
|
|
3096
|
+
let decrypt2;
|
|
3097
|
+
if (env.RS_TOKENS_ENC_KEY) {
|
|
3098
|
+
const encryptor = createEncryptor(env.RS_TOKENS_ENC_KEY);
|
|
3099
|
+
encrypt2 = encryptor.encrypt;
|
|
3100
|
+
decrypt2 = encryptor.decrypt;
|
|
3101
|
+
sharedLogger.debug("worker_storage", { message: "KV encryption enabled" });
|
|
3102
|
+
} else {
|
|
3103
|
+
encrypt2 = async (s) => s;
|
|
3104
|
+
decrypt2 = async (s) => s;
|
|
3105
|
+
if (config.NODE_ENV === "production") {
|
|
3106
|
+
sharedLogger.warning("worker_storage", {
|
|
3107
|
+
message: "RS_TOKENS_ENC_KEY not set! KV data is unencrypted."
|
|
3108
|
+
});
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
const tokenStore = new KvTokenStore(kvNamespace, {
|
|
3112
|
+
encrypt: encrypt2,
|
|
3113
|
+
decrypt: decrypt2,
|
|
3114
|
+
fallback: sharedTokenStore
|
|
3115
|
+
});
|
|
3116
|
+
const sessionStore = new KvSessionStore(kvNamespace, {
|
|
3117
|
+
encrypt: encrypt2,
|
|
3118
|
+
decrypt: decrypt2,
|
|
3119
|
+
fallback: sharedSessionStore
|
|
3120
|
+
});
|
|
3121
|
+
initializeStorage(tokenStore, sessionStore);
|
|
3122
|
+
return { tokenStore, sessionStore };
|
|
3123
|
+
}
|
|
3124
|
+
var MCP_ENDPOINT_PATH = "/mcp";
|
|
3125
|
+
function createWorkerRouter(ctx) {
|
|
3126
|
+
const router = Router();
|
|
3127
|
+
const { tokenStore, sessionStore, config, tools } = ctx;
|
|
3128
|
+
router.options("*", () => corsPreflightResponse());
|
|
3129
|
+
attachDiscoveryRoutes(router, config);
|
|
3130
|
+
attachOAuthRoutes(router, tokenStore, config);
|
|
3131
|
+
router.get(MCP_ENDPOINT_PATH, () => handleMcpGet());
|
|
3132
|
+
router.post(MCP_ENDPOINT_PATH, (request) => handleMcpRequest(request, { tokenStore, sessionStore, config, tools }));
|
|
3133
|
+
router.delete(MCP_ENDPOINT_PATH, (request) => handleMcpDelete(request, { tokenStore, sessionStore, config, tools }));
|
|
3134
|
+
router.get("/health", () => withCors(new Response(JSON.stringify({ status: "ok", timestamp: Date.now() }), {
|
|
3135
|
+
headers: { "Content-Type": "application/json" }
|
|
3136
|
+
})));
|
|
3137
|
+
router.all("*", () => withCors(new Response("Not Found", { status: 404 })));
|
|
3138
|
+
return router;
|
|
3139
|
+
}
|
|
3140
|
+
function shimProcessEnv(env) {
|
|
3141
|
+
const g = globalThis;
|
|
3142
|
+
g.process = g.process || {};
|
|
3143
|
+
g.process.env = {
|
|
3144
|
+
...g.process.env ?? {},
|
|
3145
|
+
...env
|
|
3146
|
+
};
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
export { base64Encode, base64Decode, base64UrlEncode, base64UrlDecode, base64UrlEncodeString, base64UrlDecodeString, base64UrlEncodeJson, base64UrlDecodeJson, encrypt, decrypt, generateKey, createEncryptor, withCors, corsPreflightResponse, buildCorsHeaders, MAX_SESSIONS_PER_API_KEY, MemoryTokenStore, MemorySessionStore, KvTokenStore, KvSessionStore, initializeStorage, getTokenStore, getSessionStore, sharedLogger, logger, JsonRpcErrorCode, CancellationError, CancellationToken, createCancellationToken, withCancellation, toProviderInfo, toProviderTokens, assertProviderToken, defineTool, echoInputSchema, echoTool, healthInputSchema, healthTool, sharedTools, getSharedTool, getSharedToolNames, executeSharedTool, registerTools, LATEST_PROTOCOL_VERSION, SUPPORTED_PROTOCOL_VERSIONS, getLogLevel, dispatchMcpMethod, handleMcpNotification, buildProviderRefreshConfig, refreshProviderToken, isTokenExpiredOrExpiring, ensureFreshToken, validateOrigin, validateProtocolVersion, buildUnauthorizedChallenge, buildAuthorizationServerMetadata, buildProtectedResourceMetadata, createDiscoveryHandlers, workerDiscoveryStrategy, nodeDiscoveryStrategy, checkSsrfSafe, isSsrfSafe, assertSsrfSafe, ClientMetadataSchema, isClientIdUrl, fetchClientMetadata, validateRedirectUri, generateOpaqueToken, handleAuthorize, handleProviderCallback, handleToken, handleRegister, handleRevoke, parseAuthorizeInput, parseCallbackInput, parseTokenInput, buildTokenInput, buildProviderConfig, buildOAuthConfig, buildFlowOptions, initializeWorkerStorage, createWorkerRouter, shimProcessEnv };
|