@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/README.md +130 -0
- package/dist/adapter.d.ts +13 -0
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +356 -121
- 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 +55 -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
|
}
|
|
@@ -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
|
-
|
|
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
|
|
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) {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
364
|
-
console.log("[CloudflareMCPServer] 🔍 PRIORITY 3:
|
|
365
|
-
key:
|
|
366
|
-
found: !!
|
|
367
|
-
|
|
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 (
|
|
371
|
-
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
533
|
-
|
|
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
|
|
606
|
-
let
|
|
607
|
-
if (
|
|
608
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
//
|
|
629
|
-
|
|
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
|
|
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
|
|
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
|
|
876
|
-
|
|
877
|
-
|
|
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") ??
|