@kya-os/mcp-i-cloudflare 1.5.8-canary.34 → 1.5.8-canary.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +130 -0
- package/dist/adapter.d.ts +13 -0
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +334 -120
- package/dist/adapter.js.map +1 -1
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +47 -42
- package/dist/agent.js.map +1 -1
- package/dist/runtime/oauth-handler.d.ts +5 -0
- package/dist/runtime/oauth-handler.d.ts.map +1 -1
- package/dist/runtime/oauth-handler.js +152 -35
- package/dist/runtime/oauth-handler.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +7 -1
- package/dist/server.js.map +1 -1
- package/dist/services/consent-config.service.d.ts +2 -2
- package/dist/services/consent-config.service.d.ts.map +1 -1
- package/dist/services/consent-config.service.js +40 -24
- package/dist/services/consent-config.service.js.map +1 -1
- package/dist/services/consent.service.d.ts +6 -1
- package/dist/services/consent.service.d.ts.map +1 -1
- package/dist/services/consent.service.js +71 -8
- package/dist/services/consent.service.js.map +1 -1
- package/dist/services/delegation.service.d.ts.map +1 -1
- package/dist/services/delegation.service.js +67 -29
- package/dist/services/delegation.service.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/adapter.js
CHANGED
|
@@ -122,13 +122,62 @@ class CloudflareMCPServer {
|
|
|
122
122
|
isProvided: !!providedSessionId,
|
|
123
123
|
isEphemeral: !providedSessionId,
|
|
124
124
|
});
|
|
125
|
+
// Extract OAuth identity from request or session cache for persistent userDid lookup
|
|
126
|
+
let oauthIdentity = undefined;
|
|
127
|
+
if (meta?.request) {
|
|
128
|
+
oauthIdentity = this.extractOAuthIdentityFromRequest(meta.request);
|
|
129
|
+
if (oauthIdentity) {
|
|
130
|
+
console.log("[CloudflareMCPServer] Extracted OAuth identity from request:", {
|
|
131
|
+
provider: oauthIdentity.provider,
|
|
132
|
+
subject: oauthIdentity.subject.substring(0, 20) + "...",
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// If OAuth identity not in request, try to get from session cache
|
|
137
|
+
if (!oauthIdentity && sessionId && this.delegationStorage) {
|
|
138
|
+
try {
|
|
139
|
+
const sessionKey = STORAGE_KEYS.session(sessionId);
|
|
140
|
+
const sessionData = (await this.delegationStorage.get(sessionKey, "json"));
|
|
141
|
+
if (sessionData?.oauthIdentity?.provider) {
|
|
142
|
+
// Note: We only have subjectHash in session cache (PII protection)
|
|
143
|
+
// We can't fully reconstruct OAuth identity, but we can use it to lookup userDid
|
|
144
|
+
console.log("[CloudflareMCPServer] Found OAuth identity in session cache:", {
|
|
145
|
+
provider: sessionData.oauthIdentity.provider,
|
|
146
|
+
hasSubjectHash: !!sessionData.oauthIdentity.subjectHash,
|
|
147
|
+
});
|
|
148
|
+
// OAuth identity from session cache is incomplete (subjectHash only), so we'll use it for userDid lookup via storage
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
console.warn("[CloudflareMCPServer] Failed to get OAuth identity from session:", error);
|
|
153
|
+
// Non-fatal - continue without OAuth identity
|
|
154
|
+
}
|
|
155
|
+
}
|
|
125
156
|
// Check KV storage for stored delegation token if not provided in params
|
|
126
157
|
let delegationToken = params.delegationToken;
|
|
127
158
|
if (!delegationToken && this.delegationStorage) {
|
|
128
159
|
try {
|
|
129
160
|
// Get userDID from params first
|
|
130
161
|
let userDid = params.clientDid || params.userDid;
|
|
131
|
-
// ✅
|
|
162
|
+
// ✅ PRIORITY 1: If OAuth identity available, use it to lookup persistent userDid
|
|
163
|
+
if (!userDid && oauthIdentity && this.delegationStorage) {
|
|
164
|
+
try {
|
|
165
|
+
const oauthKey = STORAGE_KEYS.oauthIdentity(oauthIdentity.provider, oauthIdentity.subject);
|
|
166
|
+
const mappedUserDid = await this.delegationStorage.get(oauthKey, "text");
|
|
167
|
+
if (mappedUserDid) {
|
|
168
|
+
userDid = mappedUserDid;
|
|
169
|
+
console.log("[CloudflareMCPServer] ✅ Retrieved persistent userDid from OAuth mapping:", {
|
|
170
|
+
provider: oauthIdentity.provider,
|
|
171
|
+
userDid: userDid.slice(0, 20) + "...",
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
console.warn("[CloudflareMCPServer] Failed to lookup userDid from OAuth mapping:", error);
|
|
177
|
+
// Non-fatal - continue with session cache lookup
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// ✅ PRIORITY 2: If userDid not in params or OAuth mapping, try to retrieve from session cache
|
|
132
181
|
if (!userDid && sessionId) {
|
|
133
182
|
try {
|
|
134
183
|
const sessionKey = STORAGE_KEYS.session(sessionId);
|
|
@@ -152,56 +201,17 @@ class CloudflareMCPServer {
|
|
|
152
201
|
sessionId: sessionId?.slice(0, 20) + "..." || "none",
|
|
153
202
|
hasDelegationStorage: !!this.delegationStorage,
|
|
154
203
|
});
|
|
155
|
-
// PRIORITY 1: Try agent
|
|
156
|
-
// This is the
|
|
157
|
-
|
|
158
|
-
const agentKey = STORAGE_KEYS.legacyDelegation(agentDid);
|
|
159
|
-
console.log("[CloudflareMCPServer] 🔍 PRIORITY 1: Checking agent-scoped key:", {
|
|
160
|
-
key: agentKey,
|
|
161
|
-
agentDid: agentDid.slice(0, 20) + "...",
|
|
162
|
-
});
|
|
163
|
-
const agentToken = await this.delegationStorage.get(agentKey, "text");
|
|
164
|
-
console.log("[CloudflareMCPServer] 🔍 PRIORITY 1: Agent-scoped lookup result:", {
|
|
165
|
-
key: agentKey,
|
|
166
|
-
found: !!agentToken,
|
|
167
|
-
tokenLength: agentToken?.length || 0,
|
|
168
|
-
tokenPreview: agentToken ? agentToken.substring(0, 20) + "..." : null,
|
|
169
|
-
});
|
|
170
|
-
if (agentToken) {
|
|
171
|
-
delegationToken = agentToken;
|
|
172
|
-
console.log("[CloudflareMCPServer] ✅ Delegation token retrieved from agent-scoped key:", {
|
|
173
|
-
agentDid: agentDid.slice(0, 20) + "...",
|
|
174
|
-
key: agentKey,
|
|
175
|
-
tokenLength: agentToken.length,
|
|
176
|
-
});
|
|
177
|
-
// Cache it for this session for faster future lookups
|
|
178
|
-
// Store full session data object to match consent service format
|
|
179
|
-
if (sessionId) {
|
|
180
|
-
const sessionKey = STORAGE_KEYS.session(sessionId);
|
|
181
|
-
// Read existing session data to preserve userDid and other fields
|
|
182
|
-
const existingSession = (await this.delegationStorage.get(sessionKey, "json"));
|
|
183
|
-
await this.delegationStorage.put(sessionKey, JSON.stringify({
|
|
184
|
-
...existingSession,
|
|
185
|
-
userDid,
|
|
186
|
-
agentDid,
|
|
187
|
-
delegationToken: agentToken,
|
|
188
|
-
cachedAt: Date.now(),
|
|
189
|
-
}), {
|
|
190
|
-
expirationTtl: 1800, // 30 minutes
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
// PRIORITY 2: Try user+agent scoped token (if userDID is available)
|
|
195
|
-
// This is the preferred approach for multi-user scenarios
|
|
196
|
-
if (!delegationToken && userDid) {
|
|
204
|
+
// PRIORITY 1: Try user+agent scoped token (user-specific, most secure)
|
|
205
|
+
// This is the preferred approach for multi-user scenarios with proper user isolation
|
|
206
|
+
if (userDid) {
|
|
197
207
|
const userAgentKey = STORAGE_KEYS.delegation(userDid, agentDid);
|
|
198
|
-
console.log("[CloudflareMCPServer] 🔍 PRIORITY
|
|
208
|
+
console.log("[CloudflareMCPServer] 🔍 PRIORITY 1: Checking user+agent scoped key:", {
|
|
199
209
|
key: userAgentKey,
|
|
200
210
|
userDid: userDid.slice(0, 20) + "...",
|
|
201
211
|
agentDid: agentDid.slice(0, 20) + "...",
|
|
202
212
|
});
|
|
203
213
|
const userAgentToken = await this.delegationStorage.get(userAgentKey, "text");
|
|
204
|
-
console.log("[CloudflareMCPServer] 🔍 PRIORITY
|
|
214
|
+
console.log("[CloudflareMCPServer] 🔍 PRIORITY 1: User+agent scoped lookup result:", {
|
|
205
215
|
key: userAgentKey,
|
|
206
216
|
found: !!userAgentToken,
|
|
207
217
|
tokenLength: userAgentToken?.length || 0,
|
|
@@ -233,17 +243,17 @@ class CloudflareMCPServer {
|
|
|
233
243
|
}
|
|
234
244
|
}
|
|
235
245
|
}
|
|
236
|
-
// PRIORITY
|
|
246
|
+
// PRIORITY 2: Try session-scoped token (if session ID was persisted)
|
|
237
247
|
// Note: Sessions are ephemeral in Cloudflare Workers, so this is mainly
|
|
238
248
|
// useful for caching tokens within a single request chain
|
|
239
249
|
if (!delegationToken && sessionId) {
|
|
240
250
|
const sessionKey = STORAGE_KEYS.session(sessionId);
|
|
241
|
-
console.log("[CloudflareMCPServer] 🔍 PRIORITY
|
|
251
|
+
console.log("[CloudflareMCPServer] 🔍 PRIORITY 2: Checking session-scoped key:", {
|
|
242
252
|
key: sessionKey,
|
|
243
253
|
sessionId: sessionId.slice(0, 20) + "...",
|
|
244
254
|
});
|
|
245
255
|
const sessionData = (await this.delegationStorage.get(sessionKey, "json"));
|
|
246
|
-
console.log("[CloudflareMCPServer] 🔍 PRIORITY
|
|
256
|
+
console.log("[CloudflareMCPServer] 🔍 PRIORITY 2: Session-scoped lookup result:", {
|
|
247
257
|
key: sessionKey,
|
|
248
258
|
found: !!sessionData,
|
|
249
259
|
hasDelegationToken: !!sessionData?.delegationToken,
|
|
@@ -258,18 +268,71 @@ class CloudflareMCPServer {
|
|
|
258
268
|
});
|
|
259
269
|
}
|
|
260
270
|
}
|
|
271
|
+
// PRIORITY 3: Try agent-scoped token (legacy fallback - DEPRECATED)
|
|
272
|
+
// Only use when userDid is unavailable (backward compatibility)
|
|
273
|
+
// WARNING: This allows cross-user delegation sharing - migrate to user+agent scoped tokens
|
|
274
|
+
if (!delegationToken && !userDid) {
|
|
275
|
+
const agentKey = STORAGE_KEYS.legacyDelegation(agentDid);
|
|
276
|
+
console.warn("[CloudflareMCPServer] ⚠️ DEPRECATION: Using agent-scoped token (legacy format). Migrate to user+agent scoped tokens for proper user isolation.", {
|
|
277
|
+
key: agentKey,
|
|
278
|
+
agentDid: agentDid.slice(0, 20) + "...",
|
|
279
|
+
reason: "userDid unavailable",
|
|
280
|
+
});
|
|
281
|
+
console.log("[CloudflareMCPServer] 🔍 PRIORITY 3: Checking agent-scoped key (legacy):", {
|
|
282
|
+
key: agentKey,
|
|
283
|
+
agentDid: agentDid.slice(0, 20) + "...",
|
|
284
|
+
});
|
|
285
|
+
const agentToken = await this.delegationStorage.get(agentKey, "text");
|
|
286
|
+
console.log("[CloudflareMCPServer] 🔍 PRIORITY 3: Agent-scoped lookup result:", {
|
|
287
|
+
key: agentKey,
|
|
288
|
+
found: !!agentToken,
|
|
289
|
+
tokenLength: agentToken?.length || 0,
|
|
290
|
+
tokenPreview: agentToken ? agentToken.substring(0, 20) + "..." : null,
|
|
291
|
+
});
|
|
292
|
+
if (agentToken) {
|
|
293
|
+
delegationToken = agentToken;
|
|
294
|
+
console.log("[CloudflareMCPServer] ✅ Delegation token retrieved from agent-scoped key (legacy):", {
|
|
295
|
+
agentDid: agentDid.slice(0, 20) + "...",
|
|
296
|
+
key: agentKey,
|
|
297
|
+
tokenLength: agentToken.length,
|
|
298
|
+
});
|
|
299
|
+
// Cache it for this session for faster future lookups
|
|
300
|
+
// Store full session data object to match consent service format
|
|
301
|
+
if (sessionId) {
|
|
302
|
+
const sessionKey = STORAGE_KEYS.session(sessionId);
|
|
303
|
+
// Read existing session data to preserve userDid and other fields
|
|
304
|
+
const existingSession = (await this.delegationStorage.get(sessionKey, "json"));
|
|
305
|
+
await this.delegationStorage.put(sessionKey, JSON.stringify({
|
|
306
|
+
...existingSession,
|
|
307
|
+
userDid,
|
|
308
|
+
agentDid,
|
|
309
|
+
delegationToken: agentToken,
|
|
310
|
+
cachedAt: Date.now(),
|
|
311
|
+
}), {
|
|
312
|
+
expirationTtl: 1800, // 30 minutes
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
261
317
|
if (!delegationToken) {
|
|
318
|
+
const checkedKeys = [];
|
|
319
|
+
// Only include agent-scoped key if it was checked (when userDid unavailable)
|
|
320
|
+
if (!userDid) {
|
|
321
|
+
checkedKeys.push(STORAGE_KEYS.legacyDelegation(agentDid));
|
|
322
|
+
}
|
|
323
|
+
// Always include user+agent scoped key if userDid is available
|
|
324
|
+
if (userDid) {
|
|
325
|
+
checkedKeys.push(STORAGE_KEYS.delegation(userDid, agentDid));
|
|
326
|
+
}
|
|
327
|
+
// Always include session-scoped key if sessionId is available
|
|
328
|
+
if (sessionId) {
|
|
329
|
+
checkedKeys.push(STORAGE_KEYS.session(sessionId));
|
|
330
|
+
}
|
|
262
331
|
console.log("[CloudflareMCPServer] ⚠️ No delegation token found in KV storage:", {
|
|
263
332
|
agentDid: agentDid.slice(0, 20) + "...",
|
|
264
333
|
userDid: userDid?.slice(0, 20) + "..." || "none",
|
|
265
334
|
sessionId: sessionId?.slice(0, 20) + "..." || "none",
|
|
266
|
-
checkedKeys:
|
|
267
|
-
agentKey,
|
|
268
|
-
userDid
|
|
269
|
-
? STORAGE_KEYS.delegation(userDid, agentDid)
|
|
270
|
-
: null,
|
|
271
|
-
sessionId ? STORAGE_KEYS.session(sessionId) : null,
|
|
272
|
-
].filter(Boolean),
|
|
335
|
+
checkedKeys: checkedKeys.filter(Boolean),
|
|
273
336
|
});
|
|
274
337
|
}
|
|
275
338
|
}
|
|
@@ -322,28 +385,18 @@ class CloudflareMCPServer {
|
|
|
322
385
|
userDid: userDid?.slice(0, 20) + "...",
|
|
323
386
|
agentDid: agentDid?.slice(0, 20) + "...",
|
|
324
387
|
});
|
|
325
|
-
// PRIORITY 1: Try agent
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
agentDid: agentDid.slice(0, 20) + "...",
|
|
330
|
-
});
|
|
331
|
-
let delegationToken = await this.delegationStorage.get(agentKey, "text");
|
|
332
|
-
console.log("[CloudflareMCPServer] 🔍 PRIORITY 1: Agent-scoped lookup result:", {
|
|
333
|
-
key: agentKey,
|
|
334
|
-
found: !!delegationToken,
|
|
335
|
-
tokenLength: delegationToken?.length || 0,
|
|
336
|
-
});
|
|
337
|
-
// PRIORITY 2: Try user+agent scoped token (if userDID is available)
|
|
338
|
-
if (!delegationToken && userDid) {
|
|
388
|
+
// PRIORITY 1: Try user+agent scoped token (user-specific, most secure)
|
|
389
|
+
// This is the preferred approach for multi-user scenarios with proper user isolation
|
|
390
|
+
let delegationToken = undefined;
|
|
391
|
+
if (userDid) {
|
|
339
392
|
const userAgentKey = STORAGE_KEYS.delegation(userDid, agentDid);
|
|
340
|
-
console.log("[CloudflareMCPServer] 🔍 PRIORITY
|
|
393
|
+
console.log("[CloudflareMCPServer] 🔍 PRIORITY 1: Checking user+agent scoped key (existing session):", {
|
|
341
394
|
key: userAgentKey,
|
|
342
395
|
userDid: userDid.slice(0, 20) + "...",
|
|
343
396
|
agentDid: agentDid.slice(0, 20) + "...",
|
|
344
397
|
});
|
|
345
398
|
const userAgentToken = await this.delegationStorage.get(userAgentKey, "text");
|
|
346
|
-
console.log("[CloudflareMCPServer] 🔍 PRIORITY
|
|
399
|
+
console.log("[CloudflareMCPServer] 🔍 PRIORITY 1: User+agent scoped lookup result:", {
|
|
347
400
|
key: userAgentKey,
|
|
348
401
|
found: !!userAgentToken,
|
|
349
402
|
tokenLength: userAgentToken?.length || 0,
|
|
@@ -352,27 +405,55 @@ class CloudflareMCPServer {
|
|
|
352
405
|
delegationToken = userAgentToken;
|
|
353
406
|
}
|
|
354
407
|
}
|
|
355
|
-
// PRIORITY
|
|
408
|
+
// PRIORITY 2: Try session-scoped token (if session ID was persisted)
|
|
356
409
|
if (!delegationToken && sessionId) {
|
|
357
410
|
const sessionKey = STORAGE_KEYS.session(sessionId);
|
|
358
|
-
console.log("[CloudflareMCPServer] 🔍 PRIORITY
|
|
359
|
-
sessionKey,
|
|
411
|
+
console.log("[CloudflareMCPServer] 🔍 PRIORITY 2: Checking session-scoped key (existing session):", {
|
|
412
|
+
key: sessionKey,
|
|
360
413
|
sessionId: sessionId.slice(0, 20) + "...",
|
|
361
414
|
});
|
|
415
|
+
const sessionData = (await this.delegationStorage.get(sessionKey, "json"));
|
|
416
|
+
console.log("[CloudflareMCPServer] 🔍 PRIORITY 2: Session-scoped lookup result:", {
|
|
417
|
+
key: sessionKey,
|
|
418
|
+
found: !!sessionData,
|
|
419
|
+
hasDelegationToken: !!sessionData?.delegationToken,
|
|
420
|
+
tokenLength: sessionData?.delegationToken?.length || 0,
|
|
421
|
+
});
|
|
422
|
+
if (sessionData?.delegationToken) {
|
|
423
|
+
delegationToken = sessionData.delegationToken;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
// PRIORITY 3: Try agent-scoped token (legacy fallback - DEPRECATED)
|
|
427
|
+
// Only use when userDid is unavailable (backward compatibility)
|
|
428
|
+
if (!delegationToken && !userDid) {
|
|
429
|
+
const agentKey = STORAGE_KEYS.legacyDelegation(agentDid);
|
|
430
|
+
console.warn("[CloudflareMCPServer] ⚠️ DEPRECATION: Using agent-scoped token (legacy format). Migrate to user+agent scoped tokens for proper user isolation.", {
|
|
431
|
+
key: agentKey,
|
|
432
|
+
agentDid: agentDid.slice(0, 20) + "...",
|
|
433
|
+
reason: "userDid unavailable",
|
|
434
|
+
});
|
|
435
|
+
console.log("[CloudflareMCPServer] 🔍 PRIORITY 3: Checking agent-scoped key (legacy, existing session):", {
|
|
436
|
+
key: agentKey,
|
|
437
|
+
agentDid: agentDid.slice(0, 20) + "...",
|
|
438
|
+
});
|
|
362
439
|
try {
|
|
363
|
-
const
|
|
364
|
-
console.log("[CloudflareMCPServer] 🔍 PRIORITY 3:
|
|
365
|
-
key:
|
|
366
|
-
found: !!
|
|
367
|
-
|
|
368
|
-
tokenLength: sessionData?.delegationToken?.length || 0,
|
|
440
|
+
const agentToken = await this.delegationStorage.get(agentKey, "text");
|
|
441
|
+
console.log("[CloudflareMCPServer] 🔍 PRIORITY 3: Agent-scoped lookup result:", {
|
|
442
|
+
key: agentKey,
|
|
443
|
+
found: !!agentToken,
|
|
444
|
+
tokenLength: agentToken?.length || 0,
|
|
369
445
|
});
|
|
370
|
-
if (
|
|
371
|
-
delegationToken =
|
|
446
|
+
if (agentToken) {
|
|
447
|
+
delegationToken = agentToken;
|
|
448
|
+
console.log("[CloudflareMCPServer] ✅ Delegation token retrieved from agent-scoped key (legacy, existing session):", {
|
|
449
|
+
agentDid: agentDid.slice(0, 20) + "...",
|
|
450
|
+
key: agentKey,
|
|
451
|
+
tokenLength: agentToken.length,
|
|
452
|
+
});
|
|
372
453
|
}
|
|
373
454
|
}
|
|
374
455
|
catch (error) {
|
|
375
|
-
console.error("[CloudflareMCPServer] Error retrieving
|
|
456
|
+
console.error("[CloudflareMCPServer] Error retrieving agent token:", error);
|
|
376
457
|
}
|
|
377
458
|
}
|
|
378
459
|
// ✅ CRITICAL: Set delegation token on session if found
|
|
@@ -389,13 +470,22 @@ class CloudflareMCPServer {
|
|
|
389
470
|
});
|
|
390
471
|
}
|
|
391
472
|
else {
|
|
473
|
+
const checkedKeys = [];
|
|
474
|
+
// Only include agent-scoped key if it was checked (when userDid unavailable)
|
|
475
|
+
if (!userDid) {
|
|
476
|
+
checkedKeys.push(STORAGE_KEYS.legacyDelegation(agentDid));
|
|
477
|
+
}
|
|
478
|
+
// Always include user+agent scoped key if userDid is available
|
|
479
|
+
if (userDid) {
|
|
480
|
+
checkedKeys.push(STORAGE_KEYS.delegation(userDid, agentDid));
|
|
481
|
+
}
|
|
482
|
+
// Always include session-scoped key if sessionId is available
|
|
483
|
+
if (sessionId) {
|
|
484
|
+
checkedKeys.push(STORAGE_KEYS.session(sessionId));
|
|
485
|
+
}
|
|
392
486
|
console.log("[CloudflareMCPServer] ⚠️ No delegation token found in KV storage for existing session:", {
|
|
393
487
|
sessionId: sessionId?.slice(0, 20) + "..." || "none",
|
|
394
|
-
checkedKeys:
|
|
395
|
-
agentKey,
|
|
396
|
-
userDid ? STORAGE_KEYS.delegation(userDid, agentDid) : null,
|
|
397
|
-
sessionId ? STORAGE_KEYS.session(sessionId) : null,
|
|
398
|
-
].filter(Boolean),
|
|
488
|
+
checkedKeys: checkedKeys.filter(Boolean),
|
|
399
489
|
});
|
|
400
490
|
}
|
|
401
491
|
// PRIORITY 2: Try user+agent scoped token (if userDID is available)
|
|
@@ -434,10 +524,16 @@ class CloudflareMCPServer {
|
|
|
434
524
|
hasUserDid: !!userDid,
|
|
435
525
|
});
|
|
436
526
|
}
|
|
437
|
-
// PRIORITY 3: Try agent-scoped token (fallback)
|
|
438
|
-
|
|
527
|
+
// PRIORITY 3: Try agent-scoped token (legacy fallback - DEPRECATED)
|
|
528
|
+
// Only use when userDid is unavailable (backward compatibility)
|
|
529
|
+
if (!session.delegationToken && !userDid) {
|
|
439
530
|
const agentKey = STORAGE_KEYS.legacyDelegation(agentDid);
|
|
440
|
-
console.
|
|
531
|
+
console.warn("[CloudflareMCPServer] ⚠️ DEPRECATION: Using agent-scoped token (legacy format). Migrate to user+agent scoped tokens for proper user isolation.", {
|
|
532
|
+
key: agentKey,
|
|
533
|
+
agentDid: agentDid.slice(0, 20) + "...",
|
|
534
|
+
reason: "userDid unavailable",
|
|
535
|
+
});
|
|
536
|
+
console.log("[CloudflareMCPServer] PRIORITY 3: Looking up agent key (legacy):", {
|
|
441
537
|
agentKey,
|
|
442
538
|
agentDid: agentDid.slice(0, 20) + "...",
|
|
443
539
|
});
|
|
@@ -448,7 +544,7 @@ class CloudflareMCPServer {
|
|
|
448
544
|
...session,
|
|
449
545
|
delegationToken: agentToken,
|
|
450
546
|
};
|
|
451
|
-
console.log("[CloudflareMCPServer] ✅ Delegation token retrieved from agent-scoped key (existing session):", {
|
|
547
|
+
console.log("[CloudflareMCPServer] ✅ Delegation token retrieved from agent-scoped key (legacy, existing session):", {
|
|
452
548
|
agentDid: agentDid.slice(0, 20) + "...",
|
|
453
549
|
key: agentKey,
|
|
454
550
|
tokenLength: agentToken.length,
|
|
@@ -526,17 +622,38 @@ class CloudflareMCPServer {
|
|
|
526
622
|
session.scopeId = scopeId; // ✅ ADDED: Pass scopeId for tool auto-discovery
|
|
527
623
|
// ✅ CRITICAL: Verify delegation token is set on session before processToolCall
|
|
528
624
|
// Also do a final KV lookup if token is still missing (defensive check)
|
|
625
|
+
// Use same 3-priority lookup: user+agent scoped, then session-scoped, then agent-scoped (last resort)
|
|
529
626
|
if (!session?.delegationToken && this.delegationStorage) {
|
|
530
627
|
try {
|
|
531
628
|
const agentDid = (await this.runtime.getIdentity()).did;
|
|
532
|
-
const
|
|
533
|
-
|
|
629
|
+
const userDid = session.userDid || params.clientDid || params.userDid;
|
|
630
|
+
let finalToken = undefined;
|
|
631
|
+
// PRIORITY 1: User+agent scoped token
|
|
632
|
+
if (userDid) {
|
|
633
|
+
const userAgentKey = STORAGE_KEYS.delegation(userDid, agentDid);
|
|
634
|
+
const token = await this.delegationStorage.get(userAgentKey, "text");
|
|
635
|
+
finalToken = token || undefined; // Convert null to undefined
|
|
636
|
+
}
|
|
637
|
+
// PRIORITY 2: Session-scoped token
|
|
638
|
+
if (!finalToken && session.id) {
|
|
639
|
+
const sessionKey = STORAGE_KEYS.session(session.id);
|
|
640
|
+
const sessionData = (await this.delegationStorage.get(sessionKey, "json"));
|
|
641
|
+
finalToken = sessionData?.delegationToken;
|
|
642
|
+
}
|
|
643
|
+
// PRIORITY 3: Agent-scoped token (legacy fallback - only if userDid unavailable)
|
|
644
|
+
if (!finalToken && !userDid) {
|
|
645
|
+
const agentKey = STORAGE_KEYS.legacyDelegation(agentDid);
|
|
646
|
+
const token = await this.delegationStorage.get(agentKey, "text");
|
|
647
|
+
finalToken = token || undefined; // Convert null to undefined
|
|
648
|
+
}
|
|
534
649
|
if (finalToken) {
|
|
535
650
|
session.delegationToken = finalToken;
|
|
536
651
|
console.log("[CloudflareMCPServer] ✅ Final KV lookup succeeded, token set on session:", {
|
|
537
652
|
toolName,
|
|
538
653
|
agentDid: agentDid.slice(0, 20) + "...",
|
|
654
|
+
userDid: userDid?.slice(0, 20) + "..." || "none",
|
|
539
655
|
tokenLength: finalToken.length,
|
|
656
|
+
source: userDid ? (session.id ? "session-scoped" : "user+agent-scoped") : "agent-scoped (legacy)",
|
|
540
657
|
});
|
|
541
658
|
}
|
|
542
659
|
}
|
|
@@ -602,31 +719,27 @@ class CloudflareMCPServer {
|
|
|
602
719
|
normalizedClientInfo.capabilities;
|
|
603
720
|
}
|
|
604
721
|
}
|
|
605
|
-
// Phase 4 PR #5: Extract OAuth identity
|
|
606
|
-
let
|
|
607
|
-
if (
|
|
608
|
-
|
|
722
|
+
// Phase 4 PR #5: Extract OAuth identity BEFORE handshake and pass to runtime
|
|
723
|
+
let oauthIdentity = undefined;
|
|
724
|
+
if (meta?.request) {
|
|
725
|
+
oauthIdentity = this.extractOAuthIdentityFromRequest(meta.request);
|
|
609
726
|
if (oauthIdentity) {
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
});
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
catch (error) {
|
|
622
|
-
console.warn("[Adapter] Failed to lookup persistent User DID:", error);
|
|
623
|
-
// Non-fatal - continue with handshake
|
|
624
|
-
}
|
|
727
|
+
console.log("[Adapter] Extracted OAuth identity for handshake:", {
|
|
728
|
+
provider: oauthIdentity.provider,
|
|
729
|
+
subject: oauthIdentity.subject.substring(0, 20) + "...",
|
|
730
|
+
});
|
|
731
|
+
// Add OAuth identity to handshake payload for persistent user DID lookup
|
|
732
|
+
// Type assertion needed because HandshakeRequest doesn't include oauthIdentity,
|
|
733
|
+
// but handleHandshake accepts it via intersection type
|
|
734
|
+
handshakePayload.oauthIdentity = oauthIdentity;
|
|
625
735
|
}
|
|
626
736
|
}
|
|
627
737
|
const handshakeResult = await this.runtime.handleHandshake(handshakePayload);
|
|
628
|
-
//
|
|
629
|
-
|
|
738
|
+
// Get userDid from handshake result (may have been retrieved via OAuth mapping)
|
|
739
|
+
const userDid = handshakeResult.userDid;
|
|
740
|
+
// Phase 4 PR #5: Store User DID, OAuth identity (with redacted subject), and clientId in session AFTER handshake
|
|
741
|
+
// Only store if userDid is available (from OAuth mapping or handshake result)
|
|
742
|
+
if (this.delegationStorage && handshakeResult.sessionId && userDid) {
|
|
630
743
|
try {
|
|
631
744
|
const sessionKey = STORAGE_KEYS.session(handshakeResult.sessionId);
|
|
632
745
|
const existingSession = (await this.delegationStorage.get(sessionKey, "json"));
|
|
@@ -634,16 +747,29 @@ class CloudflareMCPServer {
|
|
|
634
747
|
const clientId = handshakePayload.clientInfo?.clientId;
|
|
635
748
|
// Get agentDid from runtime identity
|
|
636
749
|
const agentDid = (await this.runtime.getIdentity()).did;
|
|
750
|
+
// Store OAuth identity with redacted subject for PII protection
|
|
751
|
+
const oauthIdentityForStorage = oauthIdentity
|
|
752
|
+
? {
|
|
753
|
+
provider: oauthIdentity.provider,
|
|
754
|
+
subjectHash: oauthIdentity.subject.substring(0, 8), // Redact full subject
|
|
755
|
+
// Don't store email, name, or full subject for PII protection
|
|
756
|
+
}
|
|
757
|
+
: undefined;
|
|
637
758
|
await this.delegationStorage.put(sessionKey, JSON.stringify({
|
|
638
759
|
...(existingSession || {}),
|
|
639
760
|
userDid,
|
|
640
761
|
agentDid,
|
|
641
762
|
...(clientId && { clientId }),
|
|
763
|
+
...(oauthIdentityForStorage && { oauthIdentity: oauthIdentityForStorage }),
|
|
642
764
|
}), { expirationTtl: DEFAULT_SESSION_CACHE_TTL });
|
|
643
|
-
console.log("[Adapter] Stored User DID, agentDid, and
|
|
765
|
+
console.log("[Adapter] Stored User DID, agentDid, clientId, and OAuth identity (redacted) in session", {
|
|
766
|
+
hasUserDid: !!userDid,
|
|
767
|
+
hasOAuth: !!oauthIdentity,
|
|
768
|
+
provider: oauthIdentity?.provider,
|
|
769
|
+
});
|
|
644
770
|
}
|
|
645
771
|
catch (error) {
|
|
646
|
-
console.warn("[Adapter] Failed to store
|
|
772
|
+
console.warn("[Adapter] Failed to store session data:", error);
|
|
647
773
|
// Non-fatal - continue
|
|
648
774
|
}
|
|
649
775
|
}
|
|
@@ -872,16 +998,104 @@ class CloudflareMCPServer {
|
|
|
872
998
|
return null;
|
|
873
999
|
const cookieValue = oauthCookie.substring(equalsIndex + 1);
|
|
874
1000
|
const parsed = JSON.parse(decodeURIComponent(cookieValue));
|
|
875
|
-
// Validate
|
|
876
|
-
|
|
877
|
-
|
|
1001
|
+
// ✅ SECURITY: Validate OAuth identity format and content
|
|
1002
|
+
const validationResult = this.validateOAuthIdentity(parsed);
|
|
1003
|
+
if (!validationResult.valid) {
|
|
1004
|
+
console.warn("[Adapter] ⚠️ OAuth identity validation failed:", validationResult.reason, { parsed });
|
|
1005
|
+
return null;
|
|
878
1006
|
}
|
|
1007
|
+
return parsed;
|
|
879
1008
|
}
|
|
880
1009
|
catch (error) {
|
|
881
1010
|
console.warn("[Adapter] Failed to extract OAuth identity from cookies:", error);
|
|
882
1011
|
}
|
|
883
1012
|
return null;
|
|
884
1013
|
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Validate OAuth identity format and content
|
|
1016
|
+
*
|
|
1017
|
+
* Ensures:
|
|
1018
|
+
* - Provider is non-empty string (1-50 chars)
|
|
1019
|
+
* - Subject is non-empty string (1-255 chars)
|
|
1020
|
+
* - Provider matches expected format (alphanumeric, hyphens, underscores)
|
|
1021
|
+
* - Subject matches expected format (non-empty, reasonable length)
|
|
1022
|
+
*
|
|
1023
|
+
* @param identity - Parsed OAuth identity object
|
|
1024
|
+
* @returns Validation result
|
|
1025
|
+
*/
|
|
1026
|
+
validateOAuthIdentity(identity) {
|
|
1027
|
+
// Check if identity is an object
|
|
1028
|
+
if (!identity || typeof identity !== "object") {
|
|
1029
|
+
return { valid: false, reason: "OAuth identity must be an object" };
|
|
1030
|
+
}
|
|
1031
|
+
const oauth = identity;
|
|
1032
|
+
// Validate provider
|
|
1033
|
+
if (!oauth.provider || typeof oauth.provider !== "string") {
|
|
1034
|
+
return { valid: false, reason: "OAuth provider is required and must be a string" };
|
|
1035
|
+
}
|
|
1036
|
+
const provider = oauth.provider.trim();
|
|
1037
|
+
if (provider.length === 0) {
|
|
1038
|
+
return { valid: false, reason: "OAuth provider cannot be empty" };
|
|
1039
|
+
}
|
|
1040
|
+
if (provider.length > 50) {
|
|
1041
|
+
return { valid: false, reason: "OAuth provider must be 50 characters or less" };
|
|
1042
|
+
}
|
|
1043
|
+
// Provider format: alphanumeric, hyphens, underscores, dots (e.g., "google", "microsoft", "github", "custom-provider")
|
|
1044
|
+
const providerPattern = /^[a-zA-Z0-9._-]+$/;
|
|
1045
|
+
if (!providerPattern.test(provider)) {
|
|
1046
|
+
return {
|
|
1047
|
+
valid: false,
|
|
1048
|
+
reason: `OAuth provider must match pattern [a-zA-Z0-9._-]: "${provider}"`,
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
// Validate subject
|
|
1052
|
+
if (!oauth.subject || typeof oauth.subject !== "string") {
|
|
1053
|
+
return { valid: false, reason: "OAuth subject is required and must be a string" };
|
|
1054
|
+
}
|
|
1055
|
+
const subject = oauth.subject.trim();
|
|
1056
|
+
if (subject.length === 0) {
|
|
1057
|
+
return { valid: false, reason: "OAuth subject cannot be empty" };
|
|
1058
|
+
}
|
|
1059
|
+
if (subject.length > 255) {
|
|
1060
|
+
return { valid: false, reason: "OAuth subject must be 255 characters or less" };
|
|
1061
|
+
}
|
|
1062
|
+
// Subject format: non-empty, reasonable characters (allows most Unicode, but prevents control chars)
|
|
1063
|
+
// OAuth subjects can be numeric IDs, email-like strings, or other identifiers
|
|
1064
|
+
const subjectPattern = /^[\S]+$/; // At least one non-whitespace character
|
|
1065
|
+
if (!subjectPattern.test(subject)) {
|
|
1066
|
+
return {
|
|
1067
|
+
valid: false,
|
|
1068
|
+
reason: `OAuth subject contains invalid characters: "${subject.substring(0, 20)}..."`,
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
// Validate optional email if present
|
|
1072
|
+
if (oauth.email !== undefined) {
|
|
1073
|
+
if (typeof oauth.email !== "string") {
|
|
1074
|
+
return { valid: false, reason: "OAuth email must be a string if provided" };
|
|
1075
|
+
}
|
|
1076
|
+
const email = oauth.email.trim();
|
|
1077
|
+
if (email.length > 0) {
|
|
1078
|
+
// Basic email format validation
|
|
1079
|
+
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
1080
|
+
if (!emailPattern.test(email)) {
|
|
1081
|
+
return { valid: false, reason: `OAuth email format invalid: "${email}"` };
|
|
1082
|
+
}
|
|
1083
|
+
if (email.length > 255) {
|
|
1084
|
+
return { valid: false, reason: "OAuth email must be 255 characters or less" };
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
// Validate optional name if present
|
|
1089
|
+
if (oauth.name !== undefined) {
|
|
1090
|
+
if (typeof oauth.name !== "string") {
|
|
1091
|
+
return { valid: false, reason: "OAuth name must be a string if provided" };
|
|
1092
|
+
}
|
|
1093
|
+
if (oauth.name.length > 255) {
|
|
1094
|
+
return { valid: false, reason: "OAuth name must be 255 characters or less" };
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
return { valid: true };
|
|
1098
|
+
}
|
|
885
1099
|
}
|
|
886
1100
|
function buildRequestMeta(request) {
|
|
887
1101
|
const ip = request.headers.get("cf-connecting-ip") ??
|