@kya-os/mcp-i-cloudflare 1.5.8-canary.34 → 1.5.8-canary.36

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/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
- // ✅ FIX: If userDid not in params, try to retrieve from session cache
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-scoped token FIRST (stable across sessions)
156
- // This is the most reliable lookup since sessions are ephemeral in Cloudflare Workers
157
- // The consent service stores tokens with this key format
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 2: Checking user+agent scoped key:", {
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 2: User+agent scoped lookup result:", {
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 3: Try session-scoped token (if session ID was persisted)
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 3: Checking session-scoped key:", {
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 3: Session-scoped lookup result:", {
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
  }
@@ -315,35 +378,46 @@ class CloudflareMCPServer {
315
378
  console.log("[CloudflareMCPServer] ⚠️ Session has no delegationToken, attempting KV lookup...");
316
379
  try {
317
380
  const agentDid = (await this.runtime.getIdentity()).did;
318
- const userDid = params.clientDid || params.userDid || session.clientDid;
381
+ // ✅ CRITICAL: Retrieve userDid from session cache if not in session object
382
+ // Check session cache first, then params, then session object
383
+ let userDid = session.userDid || params.clientDid || params.userDid || session.clientDid;
319
384
  const sessionId = session.id;
385
+ // If userDid still not found, try retrieving from session cache
386
+ if (!userDid && sessionId && this.delegationStorage) {
387
+ try {
388
+ const sessionKey = STORAGE_KEYS.session(sessionId);
389
+ const sessionData = (await this.delegationStorage.get(sessionKey, "json"));
390
+ if (sessionData?.userDid) {
391
+ userDid = sessionData.userDid;
392
+ // Update session object with retrieved userDid
393
+ session.userDid = userDid;
394
+ console.log("[CloudflareMCPServer] ✅ Retrieved userDid from session cache for delegation lookup:", {
395
+ userDid: userDid.slice(0, 20) + "...",
396
+ sessionId: sessionId.slice(0, 20) + "...",
397
+ });
398
+ }
399
+ }
400
+ catch (error) {
401
+ console.warn("[CloudflareMCPServer] Failed to get userDid from session cache:", error);
402
+ }
403
+ }
320
404
  console.log("[CloudflareMCPServer] 🔍 Looking up delegation token for existing session:", {
321
405
  sessionId: sessionId?.slice(0, 20) + "...",
322
406
  userDid: userDid?.slice(0, 20) + "...",
323
407
  agentDid: agentDid?.slice(0, 20) + "...",
324
408
  });
325
- // PRIORITY 1: Try agent-scoped token FIRST (stable across sessions)
326
- const agentKey = STORAGE_KEYS.legacyDelegation(agentDid);
327
- console.log("[CloudflareMCPServer] 🔍 PRIORITY 1: Checking agent-scoped key:", {
328
- key: agentKey,
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) {
409
+ // PRIORITY 1: Try user+agent scoped token (user-specific, most secure)
410
+ // This is the preferred approach for multi-user scenarios with proper user isolation
411
+ let delegationToken = undefined;
412
+ if (userDid) {
339
413
  const userAgentKey = STORAGE_KEYS.delegation(userDid, agentDid);
340
- console.log("[CloudflareMCPServer] 🔍 PRIORITY 2: Checking user+agent scoped key:", {
414
+ console.log("[CloudflareMCPServer] 🔍 PRIORITY 1: Checking user+agent scoped key (existing session):", {
341
415
  key: userAgentKey,
342
416
  userDid: userDid.slice(0, 20) + "...",
343
417
  agentDid: agentDid.slice(0, 20) + "...",
344
418
  });
345
419
  const userAgentToken = await this.delegationStorage.get(userAgentKey, "text");
346
- console.log("[CloudflareMCPServer] 🔍 PRIORITY 2: User+agent scoped lookup result:", {
420
+ console.log("[CloudflareMCPServer] 🔍 PRIORITY 1: User+agent scoped lookup result:", {
347
421
  key: userAgentKey,
348
422
  found: !!userAgentToken,
349
423
  tokenLength: userAgentToken?.length || 0,
@@ -352,27 +426,55 @@ class CloudflareMCPServer {
352
426
  delegationToken = userAgentToken;
353
427
  }
354
428
  }
355
- // PRIORITY 3: Try session-scoped token (if session ID was persisted)
429
+ // PRIORITY 2: Try session-scoped token (if session ID was persisted)
356
430
  if (!delegationToken && sessionId) {
357
431
  const sessionKey = STORAGE_KEYS.session(sessionId);
358
- console.log("[CloudflareMCPServer] 🔍 PRIORITY 3: Looking up session key:", {
359
- sessionKey,
432
+ console.log("[CloudflareMCPServer] 🔍 PRIORITY 2: Checking session-scoped key (existing session):", {
433
+ key: sessionKey,
360
434
  sessionId: sessionId.slice(0, 20) + "...",
361
435
  });
436
+ const sessionData = (await this.delegationStorage.get(sessionKey, "json"));
437
+ console.log("[CloudflareMCPServer] 🔍 PRIORITY 2: Session-scoped lookup result:", {
438
+ key: sessionKey,
439
+ found: !!sessionData,
440
+ hasDelegationToken: !!sessionData?.delegationToken,
441
+ tokenLength: sessionData?.delegationToken?.length || 0,
442
+ });
443
+ if (sessionData?.delegationToken) {
444
+ delegationToken = sessionData.delegationToken;
445
+ }
446
+ }
447
+ // PRIORITY 3: Try agent-scoped token (legacy fallback - DEPRECATED)
448
+ // Only use when userDid is unavailable (backward compatibility)
449
+ if (!delegationToken && !userDid) {
450
+ const agentKey = STORAGE_KEYS.legacyDelegation(agentDid);
451
+ console.warn("[CloudflareMCPServer] ⚠️ DEPRECATION: Using agent-scoped token (legacy format). Migrate to user+agent scoped tokens for proper user isolation.", {
452
+ key: agentKey,
453
+ agentDid: agentDid.slice(0, 20) + "...",
454
+ reason: "userDid unavailable",
455
+ });
456
+ console.log("[CloudflareMCPServer] 🔍 PRIORITY 3: Checking agent-scoped key (legacy, existing session):", {
457
+ key: agentKey,
458
+ agentDid: agentDid.slice(0, 20) + "...",
459
+ });
362
460
  try {
363
- const sessionData = (await this.delegationStorage.get(sessionKey, "json"));
364
- console.log("[CloudflareMCPServer] 🔍 PRIORITY 3: Session-scoped lookup result:", {
365
- key: sessionKey,
366
- found: !!sessionData,
367
- hasDelegationToken: !!sessionData?.delegationToken,
368
- tokenLength: sessionData?.delegationToken?.length || 0,
461
+ const agentToken = await this.delegationStorage.get(agentKey, "text");
462
+ console.log("[CloudflareMCPServer] 🔍 PRIORITY 3: Agent-scoped lookup result:", {
463
+ key: agentKey,
464
+ found: !!agentToken,
465
+ tokenLength: agentToken?.length || 0,
369
466
  });
370
- if (sessionData?.delegationToken) {
371
- delegationToken = sessionData.delegationToken;
467
+ if (agentToken) {
468
+ delegationToken = agentToken;
469
+ console.log("[CloudflareMCPServer] ✅ Delegation token retrieved from agent-scoped key (legacy, existing session):", {
470
+ agentDid: agentDid.slice(0, 20) + "...",
471
+ key: agentKey,
472
+ tokenLength: agentToken.length,
473
+ });
372
474
  }
373
475
  }
374
476
  catch (error) {
375
- console.error("[CloudflareMCPServer] Error retrieving session data:", error);
477
+ console.error("[CloudflareMCPServer] Error retrieving agent token:", error);
376
478
  }
377
479
  }
378
480
  // ✅ CRITICAL: Set delegation token on session if found
@@ -389,13 +491,22 @@ class CloudflareMCPServer {
389
491
  });
390
492
  }
391
493
  else {
494
+ const checkedKeys = [];
495
+ // Only include agent-scoped key if it was checked (when userDid unavailable)
496
+ if (!userDid) {
497
+ checkedKeys.push(STORAGE_KEYS.legacyDelegation(agentDid));
498
+ }
499
+ // Always include user+agent scoped key if userDid is available
500
+ if (userDid) {
501
+ checkedKeys.push(STORAGE_KEYS.delegation(userDid, agentDid));
502
+ }
503
+ // Always include session-scoped key if sessionId is available
504
+ if (sessionId) {
505
+ checkedKeys.push(STORAGE_KEYS.session(sessionId));
506
+ }
392
507
  console.log("[CloudflareMCPServer] ⚠️ No delegation token found in KV storage for existing session:", {
393
508
  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),
509
+ checkedKeys: checkedKeys.filter(Boolean),
399
510
  });
400
511
  }
401
512
  // PRIORITY 2: Try user+agent scoped token (if userDID is available)
@@ -434,10 +545,16 @@ class CloudflareMCPServer {
434
545
  hasUserDid: !!userDid,
435
546
  });
436
547
  }
437
- // PRIORITY 3: Try agent-scoped token (fallback)
438
- if (!session.delegationToken) {
548
+ // PRIORITY 3: Try agent-scoped token (legacy fallback - DEPRECATED)
549
+ // Only use when userDid is unavailable (backward compatibility)
550
+ if (!session.delegationToken && !userDid) {
439
551
  const agentKey = STORAGE_KEYS.legacyDelegation(agentDid);
440
- console.log("[CloudflareMCPServer] PRIORITY 3: Looking up agent key:", {
552
+ console.warn("[CloudflareMCPServer] ⚠️ DEPRECATION: Using agent-scoped token (legacy format). Migrate to user+agent scoped tokens for proper user isolation.", {
553
+ key: agentKey,
554
+ agentDid: agentDid.slice(0, 20) + "...",
555
+ reason: "userDid unavailable",
556
+ });
557
+ console.log("[CloudflareMCPServer] PRIORITY 3: Looking up agent key (legacy):", {
441
558
  agentKey,
442
559
  agentDid: agentDid.slice(0, 20) + "...",
443
560
  });
@@ -448,7 +565,7 @@ class CloudflareMCPServer {
448
565
  ...session,
449
566
  delegationToken: agentToken,
450
567
  };
451
- console.log("[CloudflareMCPServer] ✅ Delegation token retrieved from agent-scoped key (existing session):", {
568
+ console.log("[CloudflareMCPServer] ✅ Delegation token retrieved from agent-scoped key (legacy, existing session):", {
452
569
  agentDid: agentDid.slice(0, 20) + "...",
453
570
  key: agentKey,
454
571
  tokenLength: agentToken.length,
@@ -526,17 +643,38 @@ class CloudflareMCPServer {
526
643
  session.scopeId = scopeId; // ✅ ADDED: Pass scopeId for tool auto-discovery
527
644
  // ✅ CRITICAL: Verify delegation token is set on session before processToolCall
528
645
  // Also do a final KV lookup if token is still missing (defensive check)
646
+ // Use same 3-priority lookup: user+agent scoped, then session-scoped, then agent-scoped (last resort)
529
647
  if (!session?.delegationToken && this.delegationStorage) {
530
648
  try {
531
649
  const agentDid = (await this.runtime.getIdentity()).did;
532
- const agentKey = STORAGE_KEYS.legacyDelegation(agentDid);
533
- const finalToken = await this.delegationStorage.get(agentKey, "text");
650
+ const userDid = session.userDid || params.clientDid || params.userDid;
651
+ let finalToken = undefined;
652
+ // PRIORITY 1: User+agent scoped token
653
+ if (userDid) {
654
+ const userAgentKey = STORAGE_KEYS.delegation(userDid, agentDid);
655
+ const token = await this.delegationStorage.get(userAgentKey, "text");
656
+ finalToken = token || undefined; // Convert null to undefined
657
+ }
658
+ // PRIORITY 2: Session-scoped token
659
+ if (!finalToken && session.id) {
660
+ const sessionKey = STORAGE_KEYS.session(session.id);
661
+ const sessionData = (await this.delegationStorage.get(sessionKey, "json"));
662
+ finalToken = sessionData?.delegationToken;
663
+ }
664
+ // PRIORITY 3: Agent-scoped token (legacy fallback - only if userDid unavailable)
665
+ if (!finalToken && !userDid) {
666
+ const agentKey = STORAGE_KEYS.legacyDelegation(agentDid);
667
+ const token = await this.delegationStorage.get(agentKey, "text");
668
+ finalToken = token || undefined; // Convert null to undefined
669
+ }
534
670
  if (finalToken) {
535
671
  session.delegationToken = finalToken;
536
672
  console.log("[CloudflareMCPServer] ✅ Final KV lookup succeeded, token set on session:", {
537
673
  toolName,
538
674
  agentDid: agentDid.slice(0, 20) + "...",
675
+ userDid: userDid?.slice(0, 20) + "..." || "none",
539
676
  tokenLength: finalToken.length,
677
+ source: userDid ? (session.id ? "session-scoped" : "user+agent-scoped") : "agent-scoped (legacy)",
540
678
  });
541
679
  }
542
680
  }
@@ -602,31 +740,27 @@ class CloudflareMCPServer {
602
740
  normalizedClientInfo.capabilities;
603
741
  }
604
742
  }
605
- // Phase 4 PR #5: Extract OAuth identity and lookup persistent User DID BEFORE handshake
606
- let userDid = undefined;
607
- if (this.delegationStorage && meta?.request) {
608
- const oauthIdentity = this.extractOAuthIdentityFromRequest(meta.request);
743
+ // Phase 4 PR #5: Extract OAuth identity BEFORE handshake and pass to runtime
744
+ let oauthIdentity = undefined;
745
+ if (meta?.request) {
746
+ oauthIdentity = this.extractOAuthIdentityFromRequest(meta.request);
609
747
  if (oauthIdentity) {
610
- try {
611
- const oauthKey = STORAGE_KEYS.oauthIdentity(oauthIdentity.provider, oauthIdentity.subject);
612
- const mappedUserDid = await this.delegationStorage.get(oauthKey, "text");
613
- if (mappedUserDid) {
614
- userDid = mappedUserDid;
615
- console.log("[Adapter] Found persistent User DID for return user:", {
616
- provider: oauthIdentity.provider,
617
- userDid: userDid.substring(0, 20) + "...",
618
- });
619
- }
620
- }
621
- catch (error) {
622
- console.warn("[Adapter] Failed to lookup persistent User DID:", error);
623
- // Non-fatal - continue with handshake
624
- }
748
+ console.log("[Adapter] Extracted OAuth identity for handshake:", {
749
+ provider: oauthIdentity.provider,
750
+ subject: oauthIdentity.subject.substring(0, 20) + "...",
751
+ });
752
+ // Add OAuth identity to handshake payload for persistent user DID lookup
753
+ // Type assertion needed because HandshakeRequest doesn't include oauthIdentity,
754
+ // but handleHandshake accepts it via intersection type
755
+ handshakePayload.oauthIdentity = oauthIdentity;
625
756
  }
626
757
  }
627
758
  const handshakeResult = await this.runtime.handleHandshake(handshakePayload);
628
- // Phase 4 PR #5: Store User DID and clientId in session AFTER handshake
629
- if (userDid && this.delegationStorage && handshakeResult.sessionId) {
759
+ // Get userDid from handshake result (may have been retrieved via OAuth mapping)
760
+ const userDid = handshakeResult.userDid;
761
+ // Phase 4 PR #5: Store User DID, OAuth identity (with redacted subject), and clientId in session AFTER handshake
762
+ // Only store if userDid is available (from OAuth mapping or handshake result)
763
+ if (this.delegationStorage && handshakeResult.sessionId && userDid) {
630
764
  try {
631
765
  const sessionKey = STORAGE_KEYS.session(handshakeResult.sessionId);
632
766
  const existingSession = (await this.delegationStorage.get(sessionKey, "json"));
@@ -634,16 +768,29 @@ class CloudflareMCPServer {
634
768
  const clientId = handshakePayload.clientInfo?.clientId;
635
769
  // Get agentDid from runtime identity
636
770
  const agentDid = (await this.runtime.getIdentity()).did;
771
+ // Store OAuth identity with redacted subject for PII protection
772
+ const oauthIdentityForStorage = oauthIdentity
773
+ ? {
774
+ provider: oauthIdentity.provider,
775
+ subjectHash: oauthIdentity.subject.substring(0, 8), // Redact full subject
776
+ // Don't store email, name, or full subject for PII protection
777
+ }
778
+ : undefined;
637
779
  await this.delegationStorage.put(sessionKey, JSON.stringify({
638
780
  ...(existingSession || {}),
639
781
  userDid,
640
782
  agentDid,
641
783
  ...(clientId && { clientId }),
784
+ ...(oauthIdentityForStorage && { oauthIdentity: oauthIdentityForStorage }),
642
785
  }), { expirationTtl: DEFAULT_SESSION_CACHE_TTL });
643
- console.log("[Adapter] Stored User DID, agentDid, and clientId in session for return user");
786
+ console.log("[Adapter] Stored User DID, agentDid, clientId, and OAuth identity (redacted) in session", {
787
+ hasUserDid: !!userDid,
788
+ hasOAuth: !!oauthIdentity,
789
+ provider: oauthIdentity?.provider,
790
+ });
644
791
  }
645
792
  catch (error) {
646
- console.warn("[Adapter] Failed to store User DID in session:", error);
793
+ console.warn("[Adapter] Failed to store session data:", error);
647
794
  // Non-fatal - continue
648
795
  }
649
796
  }
@@ -872,16 +1019,104 @@ class CloudflareMCPServer {
872
1019
  return null;
873
1020
  const cookieValue = oauthCookie.substring(equalsIndex + 1);
874
1021
  const parsed = JSON.parse(decodeURIComponent(cookieValue));
875
- // Validate required fields
876
- if (parsed && parsed.provider && parsed.subject) {
877
- return parsed;
1022
+ // ✅ SECURITY: Validate OAuth identity format and content
1023
+ const validationResult = this.validateOAuthIdentity(parsed);
1024
+ if (!validationResult.valid) {
1025
+ console.warn("[Adapter] ⚠️ OAuth identity validation failed:", validationResult.reason, { parsed });
1026
+ return null;
878
1027
  }
1028
+ return parsed;
879
1029
  }
880
1030
  catch (error) {
881
1031
  console.warn("[Adapter] Failed to extract OAuth identity from cookies:", error);
882
1032
  }
883
1033
  return null;
884
1034
  }
1035
+ /**
1036
+ * Validate OAuth identity format and content
1037
+ *
1038
+ * Ensures:
1039
+ * - Provider is non-empty string (1-50 chars)
1040
+ * - Subject is non-empty string (1-255 chars)
1041
+ * - Provider matches expected format (alphanumeric, hyphens, underscores)
1042
+ * - Subject matches expected format (non-empty, reasonable length)
1043
+ *
1044
+ * @param identity - Parsed OAuth identity object
1045
+ * @returns Validation result
1046
+ */
1047
+ validateOAuthIdentity(identity) {
1048
+ // Check if identity is an object
1049
+ if (!identity || typeof identity !== "object") {
1050
+ return { valid: false, reason: "OAuth identity must be an object" };
1051
+ }
1052
+ const oauth = identity;
1053
+ // Validate provider
1054
+ if (!oauth.provider || typeof oauth.provider !== "string") {
1055
+ return { valid: false, reason: "OAuth provider is required and must be a string" };
1056
+ }
1057
+ const provider = oauth.provider.trim();
1058
+ if (provider.length === 0) {
1059
+ return { valid: false, reason: "OAuth provider cannot be empty" };
1060
+ }
1061
+ if (provider.length > 50) {
1062
+ return { valid: false, reason: "OAuth provider must be 50 characters or less" };
1063
+ }
1064
+ // Provider format: alphanumeric, hyphens, underscores, dots (e.g., "google", "microsoft", "github", "custom-provider")
1065
+ const providerPattern = /^[a-zA-Z0-9._-]+$/;
1066
+ if (!providerPattern.test(provider)) {
1067
+ return {
1068
+ valid: false,
1069
+ reason: `OAuth provider must match pattern [a-zA-Z0-9._-]: "${provider}"`,
1070
+ };
1071
+ }
1072
+ // Validate subject
1073
+ if (!oauth.subject || typeof oauth.subject !== "string") {
1074
+ return { valid: false, reason: "OAuth subject is required and must be a string" };
1075
+ }
1076
+ const subject = oauth.subject.trim();
1077
+ if (subject.length === 0) {
1078
+ return { valid: false, reason: "OAuth subject cannot be empty" };
1079
+ }
1080
+ if (subject.length > 255) {
1081
+ return { valid: false, reason: "OAuth subject must be 255 characters or less" };
1082
+ }
1083
+ // Subject format: non-empty, reasonable characters (allows most Unicode, but prevents control chars)
1084
+ // OAuth subjects can be numeric IDs, email-like strings, or other identifiers
1085
+ const subjectPattern = /^[\S]+$/; // At least one non-whitespace character
1086
+ if (!subjectPattern.test(subject)) {
1087
+ return {
1088
+ valid: false,
1089
+ reason: `OAuth subject contains invalid characters: "${subject.substring(0, 20)}..."`,
1090
+ };
1091
+ }
1092
+ // Validate optional email if present
1093
+ if (oauth.email !== undefined) {
1094
+ if (typeof oauth.email !== "string") {
1095
+ return { valid: false, reason: "OAuth email must be a string if provided" };
1096
+ }
1097
+ const email = oauth.email.trim();
1098
+ if (email.length > 0) {
1099
+ // Basic email format validation
1100
+ const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1101
+ if (!emailPattern.test(email)) {
1102
+ return { valid: false, reason: `OAuth email format invalid: "${email}"` };
1103
+ }
1104
+ if (email.length > 255) {
1105
+ return { valid: false, reason: "OAuth email must be 255 characters or less" };
1106
+ }
1107
+ }
1108
+ }
1109
+ // Validate optional name if present
1110
+ if (oauth.name !== undefined) {
1111
+ if (typeof oauth.name !== "string") {
1112
+ return { valid: false, reason: "OAuth name must be a string if provided" };
1113
+ }
1114
+ if (oauth.name.length > 255) {
1115
+ return { valid: false, reason: "OAuth name must be 255 characters or less" };
1116
+ }
1117
+ }
1118
+ return { valid: true };
1119
+ }
885
1120
  }
886
1121
  function buildRequestMeta(request) {
887
1122
  const ip = request.headers.get("cf-connecting-ip") ??