@kya-os/mcp-i-cloudflare 1.5.1-canary.1 → 1.5.1-canary.11

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.
Files changed (49) hide show
  1. package/dist/adapter.d.ts +31 -1
  2. package/dist/adapter.d.ts.map +1 -1
  3. package/dist/adapter.js +284 -23
  4. package/dist/adapter.js.map +1 -1
  5. package/dist/constants/storage-keys.d.ts +89 -0
  6. package/dist/constants/storage-keys.d.ts.map +1 -0
  7. package/dist/constants/storage-keys.js +142 -0
  8. package/dist/constants/storage-keys.js.map +1 -0
  9. package/dist/index.d.ts +7 -3
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +9 -3
  12. package/dist/index.js.map +1 -1
  13. package/dist/runtime/oauth-handler.d.ts +6 -0
  14. package/dist/runtime/oauth-handler.d.ts.map +1 -1
  15. package/dist/runtime/oauth-handler.js +96 -21
  16. package/dist/runtime/oauth-handler.js.map +1 -1
  17. package/dist/services/admin.service.d.ts +4 -0
  18. package/dist/services/admin.service.d.ts.map +1 -1
  19. package/dist/services/admin.service.js +170 -77
  20. package/dist/services/admin.service.js.map +1 -1
  21. package/dist/services/consent-config.service.d.ts.map +1 -1
  22. package/dist/services/consent-config.service.js +7 -3
  23. package/dist/services/consent-config.service.js.map +1 -1
  24. package/dist/services/consent-page-renderer.d.ts +8 -2
  25. package/dist/services/consent-page-renderer.d.ts.map +1 -1
  26. package/dist/services/consent-page-renderer.js +42 -8
  27. package/dist/services/consent-page-renderer.js.map +1 -1
  28. package/dist/services/consent.service.d.ts +90 -0
  29. package/dist/services/consent.service.d.ts.map +1 -1
  30. package/dist/services/consent.service.js +556 -99
  31. package/dist/services/consent.service.js.map +1 -1
  32. package/dist/services/delegation.service.d.ts.map +1 -1
  33. package/dist/services/delegation.service.js +54 -19
  34. package/dist/services/delegation.service.js.map +1 -1
  35. package/dist/services/oauth-security.service.d.ts +92 -0
  36. package/dist/services/oauth-security.service.d.ts.map +1 -0
  37. package/dist/services/oauth-security.service.js +260 -0
  38. package/dist/services/oauth-security.service.js.map +1 -0
  39. package/dist/services/rate-limit.service.d.ts +59 -0
  40. package/dist/services/rate-limit.service.d.ts.map +1 -0
  41. package/dist/services/rate-limit.service.js +146 -0
  42. package/dist/services/rate-limit.service.js.map +1 -0
  43. package/dist/types/client.d.ts +10 -0
  44. package/dist/types/client.d.ts.map +1 -1
  45. package/dist/utils/day0-config.d.ts +51 -0
  46. package/dist/utils/day0-config.d.ts.map +1 -0
  47. package/dist/utils/day0-config.js +72 -0
  48. package/dist/utils/day0-config.js.map +1 -0
  49. package/package.json +1 -1
@@ -9,19 +9,260 @@
9
9
  import { ConsentConfigService } from "./consent-config.service";
10
10
  import { ConsentPageRenderer } from "./consent-page-renderer";
11
11
  import { DEFAULT_AGENTSHIELD_URL, DEFAULT_SESSION_CACHE_TTL, } from "../constants";
12
+ import { STORAGE_KEYS } from "../constants/storage-keys";
13
+ import { loadDay0Config, getDelegationFieldName } from "../utils/day0-config";
12
14
  import { validateConsentApprovalRequest, } from "@kya-os/contracts/consent";
13
15
  import { AGENTSHIELD_ENDPOINTS, createDelegationAPIResponseSchema, createDelegationResponseSchema, } from "@kya-os/contracts/agentshield-api";
16
+ import { UserDidManager } from "@kya-os/mcp-i-core";
17
+ import { WebCryptoProvider } from "../providers/crypto";
14
18
  export class ConsentService {
15
19
  configService;
16
20
  renderer;
17
21
  env;
18
22
  runtime;
23
+ userDidManager; // Cached instance for consistent DID generation
19
24
  constructor(env, runtime) {
20
25
  this.env = env;
21
26
  this.runtime = runtime;
22
27
  this.configService = new ConsentConfigService(env);
23
28
  this.renderer = new ConsentPageRenderer();
24
29
  }
30
+ /**
31
+ * Get or generate User DID for a session
32
+ *
33
+ * Phase 4 PR #1: Generates ephemeral DIDs for sessions
34
+ * Phase 4 PR #3: Checks OAuth mappings for persistent DIDs
35
+ *
36
+ * @param sessionId - Session ID
37
+ * @param oauthIdentity - Optional OAuth provider identity
38
+ * @returns User DID (did:key format)
39
+ */
40
+ async getUserDidForSession(sessionId, oauthIdentity) {
41
+ // If OAuth identity provided, check for existing mapping first
42
+ if (oauthIdentity && this.env.DELEGATION_STORAGE) {
43
+ try {
44
+ const oauthKey = STORAGE_KEYS.oauthIdentity(oauthIdentity.provider, oauthIdentity.subject);
45
+ const mappedUserDid = await this.env.DELEGATION_STORAGE.get(oauthKey, "text");
46
+ if (mappedUserDid) {
47
+ console.log("[ConsentService] Found persistent User DID from OAuth mapping");
48
+ return mappedUserDid;
49
+ }
50
+ }
51
+ catch (error) {
52
+ console.warn("[ConsentService] Failed to check OAuth mapping:", error);
53
+ // Continue with ephemeral DID generation
54
+ }
55
+ }
56
+ // Continue with existing ephemeral DID generation logic
57
+ if (!this.env.DELEGATION_STORAGE) {
58
+ // No storage - use cached UserDidManager instance for consistent DID generation
59
+ if (!this.userDidManager) {
60
+ this.userDidManager = new UserDidManager({
61
+ crypto: new WebCryptoProvider(),
62
+ });
63
+ }
64
+ return await this.userDidManager.getOrCreateUserDid(sessionId);
65
+ }
66
+ const sessionKey = STORAGE_KEYS.session(sessionId);
67
+ // Try session cache first
68
+ try {
69
+ const sessionData = (await this.env.DELEGATION_STORAGE.get(sessionKey, "json"));
70
+ if (sessionData?.userDid) {
71
+ return sessionData.userDid;
72
+ }
73
+ }
74
+ catch (error) {
75
+ console.warn("[ConsentService] Failed to read session cache:", error);
76
+ }
77
+ // Generate ephemeral DID using cached UserDidManager instance
78
+ if (!this.userDidManager) {
79
+ this.userDidManager = new UserDidManager({
80
+ crypto: new WebCryptoProvider(),
81
+ storage: {
82
+ get: async (key) => {
83
+ try {
84
+ const data = await this.env.DELEGATION_STORAGE.get(`userDid:${key}`, "text");
85
+ return data || null;
86
+ }
87
+ catch {
88
+ return null;
89
+ }
90
+ },
91
+ set: async (key, value, ttl) => {
92
+ await this.env.DELEGATION_STORAGE.put(`userDid:${key}`, value, {
93
+ expirationTtl: ttl || DEFAULT_SESSION_CACHE_TTL,
94
+ });
95
+ },
96
+ delete: async (key) => {
97
+ await this.env.DELEGATION_STORAGE.delete(`userDid:${key}`);
98
+ },
99
+ },
100
+ });
101
+ }
102
+ const userDid = await this.userDidManager.getOrCreateUserDid(sessionId);
103
+ // Cache in session storage
104
+ try {
105
+ const existingSession = (await this.env.DELEGATION_STORAGE.get(sessionKey, "json"));
106
+ await this.env.DELEGATION_STORAGE.put(sessionKey, JSON.stringify({
107
+ ...existingSession,
108
+ userDid,
109
+ }), { expirationTtl: DEFAULT_SESSION_CACHE_TTL });
110
+ }
111
+ catch (error) {
112
+ console.warn("[ConsentService] Failed to cache userDid in session:", error);
113
+ // Non-fatal - continue with generated DID
114
+ }
115
+ return userDid;
116
+ }
117
+ /**
118
+ * Check if OAuth is required for delegation creation
119
+ *
120
+ * Determines if OAuth flow should be used instead of direct delegation creation.
121
+ * OAuth is required if:
122
+ * - Project has OAuth provider configured in AgentShield
123
+ * - No OAuth identity is present in the request
124
+ *
125
+ * @param projectId - Project ID to check
126
+ * @param oauthIdentity - Optional OAuth identity from cookie
127
+ * @returns True if OAuth redirect is required
128
+ */
129
+ async isOAuthRequired(projectId, oauthIdentity) {
130
+ // If OAuth identity is already present, OAuth is not required
131
+ if (oauthIdentity && oauthIdentity.provider && oauthIdentity.subject) {
132
+ return false;
133
+ }
134
+ // Check project config to see if OAuth is configured
135
+ try {
136
+ const agentShieldUrl = this.env.AGENTSHIELD_API_URL || DEFAULT_AGENTSHIELD_URL;
137
+ const apiKey = this.env.AGENTSHIELD_API_KEY;
138
+ if (!apiKey) {
139
+ // No API key - can't check, assume OAuth not required
140
+ return false;
141
+ }
142
+ // Fetch project config to check for OAuth provider
143
+ const configUrl = `${agentShieldUrl}/api/v1/bouncer/projects/${projectId}/config`;
144
+ const response = await fetch(configUrl, {
145
+ headers: {
146
+ 'X-API-Key': apiKey,
147
+ 'X-Project-Id': projectId,
148
+ "Content-Type": "application/json",
149
+ },
150
+ });
151
+ if (response.ok) {
152
+ const config = await response.json();
153
+ // Check if OAuth provider is configured
154
+ // OAuth is required if project has OAuth provider but no identity is present
155
+ const hasOAuthProvider = config?.oauth?.provider || config?.oauth_provider;
156
+ return !!hasOAuthProvider;
157
+ }
158
+ // If API call fails, default to not requiring OAuth (backward compatibility)
159
+ return false;
160
+ }
161
+ catch (error) {
162
+ console.warn("[ConsentService] Failed to check OAuth requirement:", error);
163
+ // On error, default to not requiring OAuth (backward compatibility)
164
+ return false;
165
+ }
166
+ }
167
+ /**
168
+ * Build OAuth authorization URL
169
+ *
170
+ * Creates the OAuth authorization URL with proper state parameter
171
+ * for redirecting to OAuth provider.
172
+ *
173
+ * @param projectId - Project ID
174
+ * @param agentDid - Agent DID
175
+ * @param sessionId - Session ID
176
+ * @param scopes - Requested scopes
177
+ * @param serverUrl - Server URL for callback
178
+ * @returns OAuth authorization URL
179
+ */
180
+ buildOAuthUrl(projectId, agentDid, sessionId, scopes, serverUrl) {
181
+ const agentShieldUrl = this.env.AGENTSHIELD_API_URL || DEFAULT_AGENTSHIELD_URL;
182
+ // Generate a temporary delegation ID for state (will be created after OAuth)
183
+ const delegationId = `temp-${Date.now()}`;
184
+ // Build state parameter with required fields
185
+ const state = {
186
+ project_id: projectId,
187
+ agent_did: agentDid,
188
+ session_id: sessionId,
189
+ delegation_id: delegationId,
190
+ scopes: scopes,
191
+ };
192
+ // Encode state as base64 (Cloudflare Workers compatible)
193
+ const stateParam = btoa(JSON.stringify(state));
194
+ // Build OAuth authorization URL
195
+ const oauthUrl = new URL(`${agentShieldUrl}/bouncer/oauth/authorize`);
196
+ oauthUrl.searchParams.set("response_type", "code");
197
+ oauthUrl.searchParams.set("client_id", projectId); // Use projectId as client_id
198
+ oauthUrl.searchParams.set("redirect_uri", `${serverUrl}/oauth/callback`);
199
+ oauthUrl.searchParams.set("scope", scopes.join(" "));
200
+ oauthUrl.searchParams.set("state", stateParam);
201
+ return oauthUrl.toString();
202
+ }
203
+ /**
204
+ * Link OAuth identity to User DID
205
+ *
206
+ * Maps OAuth provider identity (provider + subject) to a persistent User DID.
207
+ * If an ephemeral DID exists for the session, it becomes persistent.
208
+ *
209
+ * Phase 4 PR #3: OAuth Identity Linking
210
+ *
211
+ * @param oauthIdentity - OAuth provider identity
212
+ * @param sessionId - Current session ID (for ephemeral DID lookup)
213
+ * @returns Persistent User DID
214
+ */
215
+ async linkOAuthToUserDid(oauthIdentity, sessionId) {
216
+ if (!this.env.DELEGATION_STORAGE) {
217
+ // No storage - can't persist mapping, return ephemeral DID
218
+ console.warn("[ConsentService] No storage available for OAuth linking");
219
+ return await this.getUserDidForSession(sessionId);
220
+ }
221
+ const oauthKey = STORAGE_KEYS.oauthIdentity(oauthIdentity.provider, oauthIdentity.subject);
222
+ // Check if OAuth identity already mapped
223
+ try {
224
+ const existingUserDid = await this.env.DELEGATION_STORAGE.get(oauthKey, "text");
225
+ if (existingUserDid) {
226
+ console.log("[ConsentService] OAuth identity already mapped:", {
227
+ provider: oauthIdentity.provider,
228
+ subject: oauthIdentity.subject.substring(0, 20) + "...",
229
+ userDid: existingUserDid.substring(0, 20) + "...",
230
+ });
231
+ return existingUserDid;
232
+ }
233
+ }
234
+ catch (error) {
235
+ console.warn("[ConsentService] Failed to check OAuth mapping:", error);
236
+ // Continue to create new mapping
237
+ }
238
+ // Get/create User DID for session (may be ephemeral)
239
+ const userDid = await this.getUserDidForSession(sessionId);
240
+ // Store OAuth identity mapping (persistent - 90 days)
241
+ try {
242
+ await this.env.DELEGATION_STORAGE.put(oauthKey, userDid, {
243
+ expirationTtl: 90 * 24 * 60 * 60, // 90 days
244
+ });
245
+ // Also store full OAuth identity info for reference
246
+ const oauthIdentityKey = STORAGE_KEYS.userDid(oauthIdentity.provider, oauthIdentity.subject);
247
+ await this.env.DELEGATION_STORAGE.put(oauthIdentityKey, JSON.stringify(oauthIdentity), {
248
+ expirationTtl: 90 * 24 * 60 * 60, // 90 days
249
+ });
250
+ console.log("[ConsentService] OAuth identity linked to User DID:", {
251
+ provider: oauthIdentity.provider,
252
+ subject: oauthIdentity.subject.substring(0, 20) + "...",
253
+ userDid: userDid.substring(0, 20) + "...",
254
+ });
255
+ }
256
+ catch (error) {
257
+ console.error("[ConsentService] Failed to store OAuth mapping:", error);
258
+ // Non-fatal - continue with User DID
259
+ }
260
+ // Note: Ephemeral → persistent migration happens automatically
261
+ // The ephemeral DID becomes persistent when linked to OAuth identity
262
+ // Existing delegations with ephemeral DID will continue to work
263
+ // New delegations will use the persistent DID
264
+ return userDid;
265
+ }
25
266
  /**
26
267
  * Handle consent requests
27
268
  *
@@ -121,6 +362,48 @@ export class ConsentService {
121
362
  });
122
363
  }
123
364
  }
365
+ // Phase 4 PR #4: Extract OAuth identity from cookie (server-side)
366
+ let oauthIdentity = undefined;
367
+ try {
368
+ const cookieHeader = request.headers.get("Cookie");
369
+ if (cookieHeader) {
370
+ const cookies = cookieHeader.split("; ").map((c) => c.trim());
371
+ const oauthCookie = cookies.find((c) => c.startsWith("oauth_identity="));
372
+ if (oauthCookie) {
373
+ // Extract cookie value correctly - handle values that may contain '=' characters
374
+ // Use indexOf to find first '=' and take everything after it
375
+ const equalsIndex = oauthCookie.indexOf("=");
376
+ const cookieValue = equalsIndex >= 0 ? oauthCookie.substring(equalsIndex + 1) : "";
377
+ // Validate it's valid JSON before passing to client
378
+ try {
379
+ const parsed = JSON.parse(decodeURIComponent(cookieValue));
380
+ // Basic validation - ensure it has required fields
381
+ if (parsed && parsed.provider && parsed.subject) {
382
+ oauthIdentity = parsed;
383
+ }
384
+ }
385
+ catch (parseError) {
386
+ console.warn("[ConsentService] Invalid OAuth cookie format:", parseError);
387
+ }
388
+ }
389
+ }
390
+ }
391
+ catch (error) {
392
+ console.warn("[ConsentService] Failed to extract OAuth cookie:", error);
393
+ // Non-fatal - continue without OAuth identity
394
+ }
395
+ // Check if OAuth is required (after extracting OAuth identity)
396
+ const oauthRequired = await this.isOAuthRequired(projectId, oauthIdentity);
397
+ if (oauthRequired) {
398
+ // OAuth is required - redirect to OAuth provider instead of showing consent page
399
+ const oauthUrl = this.buildOAuthUrl(projectId, agentDid, sessionId, scopes, serverUrl);
400
+ console.log("[ConsentService] OAuth required, redirecting to OAuth provider:", {
401
+ projectId,
402
+ agentDid: agentDid.substring(0, 20) + "...",
403
+ oauthUrl: oauthUrl.substring(0, 100) + "...",
404
+ });
405
+ return Response.redirect(oauthUrl, 302);
406
+ }
124
407
  // Build consent page config
125
408
  const pageConfig = {
126
409
  tool,
@@ -135,8 +418,8 @@ export class ConsentService {
135
418
  customFields: consentConfig.customFields,
136
419
  autoClose: consentConfig.ui?.autoClose,
137
420
  };
138
- // Render page
139
- const html = this.renderer.render(pageConfig);
421
+ // Render page with OAuth identity (if available)
422
+ const html = this.renderer.render(pageConfig, oauthIdentity);
140
423
  return new Response(html, {
141
424
  status: 200,
142
425
  headers: {
@@ -250,78 +533,41 @@ export class ConsentService {
250
533
  };
251
534
  }
252
535
  try {
253
- // Create delegation request per AgentShield API schema
254
- // Schema: https://github.com/modelcontextprotocol-identity/mcp-i/blob/main/packages/contracts/src/agentshield-api/schemas.ts
255
- // AgentShield implementation: apps/web/lib/bouncer/validators/schemas.ts
256
- //
257
- // Required fields:
258
- // - agent_did: string (DID format)
259
- // - scopes: string[] (at least one scope)
260
- //
261
- // Optional fields:
262
- // - expires_in_days: number (1-365, defaults to project default if not provided)
263
- // - expires_at: string (ISO 8601 date string, alternative to expires_in_days)
264
- // NOTE: expires_in_days and expires_at are mutually exclusive - use one or the other
265
- // - session_id: string (optional session identifier)
266
- // - project_id: string (optional project identifier, also comes from API key context)
267
- // - user_id: string (optional user identifier)
268
- // - user_identifier: string (optional user identifier)
269
- // - agent_name: string (optional agent name)
270
- // - constraints: object (optional constraints)
271
- // - custom_fields: object (optional custom fields)
272
- // - metadata: object (optional metadata)
273
- const expiresInDays = 7; // Default to 7 days
274
- const delegationRequest = {
275
- agent_did: request.agent_did,
276
- scopes: request.scopes,
277
- expires_in_days: expiresInDays,
278
- // NOTE: expires_at and expires_in_days are mutually exclusive.
279
- // We use expires_in_days for simplicity. Do not include both fields.
280
- };
281
- // Include session_id if provided
282
- if (request.session_id) {
283
- delegationRequest.session_id = request.session_id;
284
- }
285
- // Include project_id if provided
286
- if (request.project_id) {
287
- delegationRequest.project_id = request.project_id;
288
- }
289
- // Include custom_fields if provided (send as custom_fields, not metadata)
290
- if (request.customFields &&
291
- Object.keys(request.customFields).length > 0) {
292
- delegationRequest.custom_fields = request.customFields;
536
+ // Load Day0 configuration to determine field name and API capabilities
537
+ await loadDay0Config(this.env.DELEGATION_STORAGE);
538
+ const fieldName = await getDelegationFieldName(this.env.DELEGATION_STORAGE);
539
+ // Get userDID from session or generate new ephemeral DID
540
+ // Phase 4 PR #3: Use OAuth identity if provided in approval request
541
+ let userDid;
542
+ if (this.env.DELEGATION_STORAGE && request.session_id) {
543
+ try {
544
+ // Pass OAuth identity if available in approval request
545
+ userDid = await this.getUserDidForSession(request.session_id, request.oauth_identity);
546
+ }
547
+ catch (error) {
548
+ console.warn("[ConsentService] Failed to get/generate userDid:", error);
549
+ // Continue without userDid - delegation will use ephemeral placeholder
550
+ }
293
551
  }
552
+ const expiresInDays = 7; // Default to 7 days
553
+ // Build delegation request with error-based format detection
554
+ // Try full format first, fallback to simplified format on error
555
+ const delegationRequest = await this.buildDelegationRequest(request, userDid, expiresInDays, fieldName);
294
556
  console.log("[ConsentService] Creating delegation:", {
295
557
  url: `${agentShieldUrl}${AGENTSHIELD_ENDPOINTS.DELEGATIONS_CREATE}`,
296
558
  agentDid: request.agent_did.substring(0, 20) + "...",
297
559
  scopes: request.scopes,
298
560
  expiresInDays,
299
561
  hasApiKey: !!apiKey,
300
- // Note: project_id comes from API key, not request body
301
- });
302
- const response = await fetch(`${agentShieldUrl}${AGENTSHIELD_ENDPOINTS.DELEGATIONS_CREATE}`, {
303
- method: "POST",
304
- headers: {
305
- Authorization: `Bearer ${apiKey}`,
306
- "Content-Type": "application/json",
307
- },
308
- body: JSON.stringify(delegationRequest),
562
+ fieldName,
563
+ hasUserDid: !!userDid,
309
564
  });
310
- if (!response.ok) {
311
- const errorText = await response.text();
312
- console.error("[ConsentService] Delegation creation failed:", {
313
- status: response.status,
314
- statusText: response.statusText,
315
- error: errorText,
316
- requestBody: JSON.stringify(delegationRequest, null, 2),
317
- });
318
- return {
319
- success: false,
320
- error: `API error: ${response.status}`,
321
- error_code: "api_error",
322
- };
565
+ // Error-based format detection: try request format, fallback on error
566
+ const response = await this.tryAPICall(agentShieldUrl, apiKey, delegationRequest);
567
+ if (!response.success) {
568
+ return response;
323
569
  }
324
- const responseData = (await response.json());
570
+ const responseData = response.data;
325
571
  // Canonical format per @kya-os/contracts/agentshield-api:
326
572
  // Wrapped: { success: true, data: { delegation_id: string, agent_did: string, ... } }
327
573
  // Unwrapped: { delegation_id: string, agent_did: string, ... }
@@ -415,38 +661,16 @@ export class ConsentService {
415
661
  try {
416
662
  // Default TTL: 7 days (same as delegation expiration)
417
663
  const ttl = 7 * 24 * 60 * 60; // 7 days in seconds
418
- // TODO: Store tokens with userDID when available for proper multi-user support
419
- // Currently, we store tokens with agent-scoped keys which can cause conflicts
420
- // when multiple users delegate to the same agent. The ideal key structure should be:
421
- // - `user:${userDid}:agent:${agentDid}:delegation` (user+agent scoped - preferred)
422
- // - `agent:${agentDid}:delegation` (agent-scoped fallback for single-user agents)
423
- //
424
- // To implement this properly:
425
- // 1. Capture userDID from consent approval request (from OAuth callback or session)
426
- // 2. Store tokens with userDID in the key: `user:${userDid}:agent:${agentDid}:delegation`
427
- // 3. Also store with agent-scoped key for backward compatibility
428
- //
429
- // Note: The consent approval request currently doesn't include userDID, so we need to:
430
- // - Extract userDID from OAuth callback (if using OAuth flow)
431
- // - Or extract userDID from session (if session has userDID)
432
- // - Or add userDID to ConsentApprovalRequest interface
433
- const userDid = undefined; // TODO: Get userDID from approval request or OAuth callback
434
- // Store using agent DID (primary, survives session changes)
435
- // WARNING: This is shared across all users - if multiple users delegate to the same
436
- // agent, each delegation will overwrite the previous one. This is acceptable for
437
- // single-user agents, but multi-user agents should use user+agent scoped keys.
438
- const agentKey = `agent:${agentDid}:delegation`;
439
- await delegationStorage.put(agentKey, token, {
440
- expirationTtl: ttl,
441
- });
442
- console.log("[ConsentService] ✅ Token stored with agent DID:", {
443
- key: agentKey,
444
- ttl,
445
- delegationId,
446
- });
447
- // Store using user+agent DID if userDID is available (preferred for multi-user scenarios)
664
+ // Get userDID from approval request or session (Phase 4)
665
+ // For now, try to extract from request or session cache
666
+ let userDid;
667
+ // Try to get userDID from session cache
668
+ const sessionKey = STORAGE_KEYS.session(sessionId);
669
+ const sessionData = (await delegationStorage.get(sessionKey, "json"));
670
+ userDid = sessionData?.userDid;
671
+ // Primary: User+Agent scoped (no conflicts) - Phase 4
448
672
  if (userDid) {
449
- const userAgentKey = `user:${userDid}:agent:${agentDid}:delegation`;
673
+ const userAgentKey = STORAGE_KEYS.delegation(userDid, agentDid);
450
674
  await delegationStorage.put(userAgentKey, token, {
451
675
  expirationTtl: ttl,
452
676
  });
@@ -456,15 +680,31 @@ export class ConsentService {
456
680
  delegationId,
457
681
  });
458
682
  }
459
- // Store using session ID (secondary cache, shorter TTL for performance)
460
- const sessionKey = `session:${sessionId}`;
461
- await delegationStorage.put(sessionKey, token, {
683
+ // Backward compatibility: Agent-only key (24 hour TTL)
684
+ const legacyKey = STORAGE_KEYS.legacyDelegation(agentDid);
685
+ await delegationStorage.put(legacyKey, token, {
686
+ expirationTtl: 24 * 60 * 60, // 24 hours only
687
+ });
688
+ console.log("[ConsentService] ✅ Token stored with legacy agent key:", {
689
+ key: legacyKey,
690
+ ttl: 24 * 60 * 60,
691
+ delegationId,
692
+ });
693
+ // Session cache for fast lookup (shorter TTL for performance)
694
+ const sessionDataToStore = {
695
+ userDid,
696
+ agentDid,
697
+ delegationToken: token,
698
+ cachedAt: Date.now(),
699
+ };
700
+ await delegationStorage.put(sessionKey, JSON.stringify(sessionDataToStore), {
462
701
  expirationTtl: Math.min(ttl, DEFAULT_SESSION_CACHE_TTL),
463
702
  });
464
703
  console.log("[ConsentService] ✅ Token cached for session:", {
465
704
  key: sessionKey,
466
705
  ttl: Math.min(ttl, DEFAULT_SESSION_CACHE_TTL),
467
706
  sessionId,
707
+ userDid,
468
708
  });
469
709
  }
470
710
  catch (error) {
@@ -506,5 +746,222 @@ export class ConsentService {
506
746
  },
507
747
  });
508
748
  }
749
+ /**
750
+ * Build delegation request with error-based format detection
751
+ *
752
+ * Uses Day0 config to determine field name and includes issuerDid when available.
753
+ */
754
+ async buildDelegationRequest(request, userDid, expiresInDays, fieldName) {
755
+ const baseRequest = {
756
+ agent_did: request.agent_did,
757
+ scopes: request.scopes,
758
+ expires_in_days: expiresInDays,
759
+ };
760
+ // Note: session_id and project_id are NOT in createDelegationSchema
761
+ // - project_id is extracted from API key context by AgentShield middleware
762
+ // - session_id is not needed for delegation creation
763
+ // These fields are removed to match AgentShield API schema exactly
764
+ // Check cached format preference
765
+ const cacheKey = STORAGE_KEYS.formatPreference();
766
+ let cachedFormat = null;
767
+ if (this.env.DELEGATION_STORAGE) {
768
+ try {
769
+ const cached = (await this.env.DELEGATION_STORAGE.get(cacheKey, "json"));
770
+ if (cached &&
771
+ cached.timestamp &&
772
+ Date.now() - cached.timestamp < 3600000) {
773
+ cachedFormat = cached.format;
774
+ }
775
+ }
776
+ catch {
777
+ // Ignore cache errors
778
+ }
779
+ }
780
+ // If we have a cached preference, use it directly
781
+ if (cachedFormat === "full") {
782
+ return this.buildFullFormatRequest(request, userDid, expiresInDays);
783
+ }
784
+ else if (cachedFormat === "simplified") {
785
+ return this.buildSimplifiedFormatRequest(request, userDid, expiresInDays, fieldName);
786
+ }
787
+ // No cache - return request that will be tried with error-based detection
788
+ return {
789
+ _tryFormats: true,
790
+ fullFormat: await this.buildFullFormatRequest(request, userDid, expiresInDays),
791
+ simplifiedFormat: this.buildSimplifiedFormatRequest(request, userDid, expiresInDays, fieldName),
792
+ };
793
+ }
794
+ /**
795
+ * Build full DelegationRecord format request (future format)
796
+ */
797
+ async buildFullFormatRequest(request, userDid, expiresInDays) {
798
+ const notAfter = Date.now() + expiresInDays * 24 * 60 * 60 * 1000;
799
+ return {
800
+ delegation: {
801
+ id: crypto.randomUUID(),
802
+ issuerDid: userDid || "did:key:z6MkEphemeral", // Use ephemeral if no userDid
803
+ subjectDid: request.agent_did,
804
+ constraints: {
805
+ scopes: request.scopes,
806
+ notAfter,
807
+ notBefore: Date.now(),
808
+ },
809
+ status: "active",
810
+ createdAt: Date.now(),
811
+ },
812
+ };
813
+ }
814
+ /**
815
+ * Check if a string is a valid UUID
816
+ *
817
+ * Validates that project_id is a UUID before including it in API requests.
818
+ * If project_id is not a UUID (e.g., project slug), it's omitted and the
819
+ * API extracts it from the API key instead.
820
+ *
821
+ * @param str - String to validate
822
+ * @returns True if string is a valid UUID
823
+ */
824
+ isValidUUID(str) {
825
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
826
+ return uuidRegex.test(str);
827
+ }
828
+ /**
829
+ * Build simplified format request with proper field name
830
+ */
831
+ buildSimplifiedFormatRequest(request, userDid, expiresInDays, fieldName) {
832
+ const simplifiedRequest = {
833
+ agent_did: request.agent_did,
834
+ scopes: request.scopes,
835
+ expires_in_days: expiresInDays,
836
+ };
837
+ // Include user_identifier if we have userDid (matches AgentShield schema)
838
+ // Note: session_id and project_id are NOT in createDelegationSchema
839
+ // - project_id is extracted from API key context by AgentShield
840
+ // - session_id is not needed for delegation creation
841
+ if (userDid) {
842
+ simplifiedRequest.user_identifier = userDid;
843
+ // Use the correct field name (metadata or custom_fields) from Day0 config
844
+ simplifiedRequest[fieldName] = {
845
+ issuer_did: userDid,
846
+ subject_did: request.agent_did,
847
+ format_version: "simplified_v1",
848
+ };
849
+ }
850
+ // Include custom_fields from request if provided
851
+ if (request.customFields && Object.keys(request.customFields).length > 0) {
852
+ simplifiedRequest.custom_fields = {
853
+ ...(simplifiedRequest.custom_fields || {}),
854
+ ...request.customFields,
855
+ };
856
+ }
857
+ return simplifiedRequest;
858
+ }
859
+ /**
860
+ * Try API call with error-based format detection
861
+ */
862
+ async tryAPICall(agentShieldUrl, apiKey, request) {
863
+ // Handle format detection
864
+ if (request._tryFormats && request.fullFormat && request.simplifiedFormat) {
865
+ // Try full format first
866
+ const fullResponse = await this.makeAPICall(agentShieldUrl, apiKey, request.fullFormat);
867
+ if (fullResponse.success ||
868
+ fullResponse.error_code !== "validation_error") {
869
+ // Full format worked or failed for non-format reasons
870
+ await this.cacheFormatPreference("full");
871
+ return fullResponse;
872
+ }
873
+ // Full format failed with validation error, try simplified
874
+ console.log("[ConsentService] Full format failed, trying simplified format...");
875
+ const simplifiedResponse = await this.makeAPICall(agentShieldUrl, apiKey, request.simplifiedFormat);
876
+ if (simplifiedResponse.success) {
877
+ await this.cacheFormatPreference("simplified");
878
+ }
879
+ return simplifiedResponse;
880
+ }
881
+ // Direct call (format already determined)
882
+ return this.makeAPICall(agentShieldUrl, apiKey, request);
883
+ }
884
+ /**
885
+ * Make API call and parse response
886
+ */
887
+ async makeAPICall(agentShieldUrl, apiKey, requestBody) {
888
+ try {
889
+ const response = await fetch(`${agentShieldUrl}${AGENTSHIELD_ENDPOINTS.DELEGATIONS_CREATE}`, {
890
+ method: "POST",
891
+ headers: {
892
+ 'X-API-Key': apiKey,
893
+ "Content-Type": "application/json",
894
+ "X-Request-ID": crypto.randomUUID(),
895
+ },
896
+ body: JSON.stringify(requestBody),
897
+ });
898
+ const responseText = await response.text();
899
+ let responseData;
900
+ try {
901
+ responseData = JSON.parse(responseText);
902
+ }
903
+ catch {
904
+ responseData = responseText;
905
+ }
906
+ // Check for validation error specifically
907
+ if (response.status === 400) {
908
+ const errorMessage = responseData
909
+ ?.error?.message ||
910
+ responseData?.message ||
911
+ "Validation failed";
912
+ if (errorMessage.includes("format") ||
913
+ errorMessage.includes("schema") ||
914
+ errorMessage.includes("invalid")) {
915
+ return {
916
+ success: false,
917
+ error: errorMessage,
918
+ error_code: "validation_error",
919
+ };
920
+ }
921
+ }
922
+ if (!response.ok) {
923
+ const errorData = responseData;
924
+ return {
925
+ success: false,
926
+ error: errorData.error?.message ||
927
+ errorData.message ||
928
+ "API request failed",
929
+ error_code: errorData.error?.code || "api_error",
930
+ };
931
+ }
932
+ return {
933
+ success: true,
934
+ data: responseData,
935
+ };
936
+ }
937
+ catch (error) {
938
+ console.error("[ConsentService] API call failed:", error);
939
+ return {
940
+ success: false,
941
+ error: error instanceof Error ? error.message : "Network request failed",
942
+ error_code: "network_error",
943
+ };
944
+ }
945
+ }
946
+ /**
947
+ * Cache successful format preference
948
+ */
949
+ async cacheFormatPreference(format) {
950
+ if (!this.env.DELEGATION_STORAGE)
951
+ return;
952
+ const cacheKey = STORAGE_KEYS.formatPreference();
953
+ try {
954
+ await this.env.DELEGATION_STORAGE.put(cacheKey, JSON.stringify({
955
+ format,
956
+ timestamp: Date.now(),
957
+ }), {
958
+ expirationTtl: 3600, // 1 hour
959
+ });
960
+ console.log(`[ConsentService] Cached format preference: ${format}`);
961
+ }
962
+ catch (error) {
963
+ console.warn("[ConsentService] Failed to cache format preference:", error);
964
+ }
965
+ }
509
966
  }
510
967
  //# sourceMappingURL=consent.service.js.map