@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.
@@ -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 };