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