@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/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
  }
@@ -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-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) {
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 2: Checking user+agent scoped key:", {
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 2: User+agent scoped lookup result:", {
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 3: Try session-scoped token (if session ID was persisted)
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 3: Looking up session key:", {
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 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,
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 (sessionData?.delegationToken) {
371
- delegationToken = sessionData.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 session data:", error);
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
- if (!session.delegationToken) {
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.log("[CloudflareMCPServer] PRIORITY 3: Looking up agent key:", {
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 agentKey = STORAGE_KEYS.legacyDelegation(agentDid);
533
- const finalToken = await this.delegationStorage.get(agentKey, "text");
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 and lookup persistent User DID BEFORE handshake
606
- let userDid = undefined;
607
- if (this.delegationStorage && meta?.request) {
608
- const oauthIdentity = this.extractOAuthIdentityFromRequest(meta.request);
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
- 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
- }
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
- // Phase 4 PR #5: Store User DID and clientId in session AFTER handshake
629
- if (userDid && this.delegationStorage && handshakeResult.sessionId) {
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 clientId in session for return user");
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 User DID in session:", error);
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 required fields
876
- if (parsed && parsed.provider && parsed.subject) {
877
- return parsed;
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") ??