@kya-os/mcp-i-cloudflare 1.5.1-canary.6 → 1.5.1-canary.8

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 (44) hide show
  1. package/dist/adapter.d.ts +8 -0
  2. package/dist/adapter.d.ts.map +1 -1
  3. package/dist/adapter.js +102 -87
  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-page-renderer.d.ts +8 -2
  22. package/dist/services/consent-page-renderer.d.ts.map +1 -1
  23. package/dist/services/consent-page-renderer.js +42 -8
  24. package/dist/services/consent-page-renderer.js.map +1 -1
  25. package/dist/services/consent.service.d.ts +90 -0
  26. package/dist/services/consent.service.d.ts.map +1 -1
  27. package/dist/services/consent.service.js +571 -99
  28. package/dist/services/consent.service.js.map +1 -1
  29. package/dist/services/delegation.service.d.ts.map +1 -1
  30. package/dist/services/delegation.service.js +54 -19
  31. package/dist/services/delegation.service.js.map +1 -1
  32. package/dist/services/oauth-security.service.d.ts +92 -0
  33. package/dist/services/oauth-security.service.d.ts.map +1 -0
  34. package/dist/services/oauth-security.service.js +260 -0
  35. package/dist/services/oauth-security.service.js.map +1 -0
  36. package/dist/services/rate-limit.service.d.ts +59 -0
  37. package/dist/services/rate-limit.service.d.ts.map +1 -0
  38. package/dist/services/rate-limit.service.js +146 -0
  39. package/dist/services/rate-limit.service.js.map +1 -0
  40. package/dist/utils/day0-config.d.ts +51 -0
  41. package/dist/utils/day0-config.d.ts.map +1 -0
  42. package/dist/utils/day0-config.js +72 -0
  43. package/dist/utils/day0-config.js.map +1 -0
  44. package/package.json +1 -1
@@ -9,19 +9,259 @@
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
+ Authorization: `Bearer ${apiKey}`,
147
+ "Content-Type": "application/json",
148
+ },
149
+ });
150
+ if (response.ok) {
151
+ const config = await response.json();
152
+ // Check if OAuth provider is configured
153
+ // OAuth is required if project has OAuth provider but no identity is present
154
+ const hasOAuthProvider = config?.oauth?.provider || config?.oauth_provider;
155
+ return !!hasOAuthProvider;
156
+ }
157
+ // If API call fails, default to not requiring OAuth (backward compatibility)
158
+ return false;
159
+ }
160
+ catch (error) {
161
+ console.warn("[ConsentService] Failed to check OAuth requirement:", error);
162
+ // On error, default to not requiring OAuth (backward compatibility)
163
+ return false;
164
+ }
165
+ }
166
+ /**
167
+ * Build OAuth authorization URL
168
+ *
169
+ * Creates the OAuth authorization URL with proper state parameter
170
+ * for redirecting to OAuth provider.
171
+ *
172
+ * @param projectId - Project ID
173
+ * @param agentDid - Agent DID
174
+ * @param sessionId - Session ID
175
+ * @param scopes - Requested scopes
176
+ * @param serverUrl - Server URL for callback
177
+ * @returns OAuth authorization URL
178
+ */
179
+ buildOAuthUrl(projectId, agentDid, sessionId, scopes, serverUrl) {
180
+ const agentShieldUrl = this.env.AGENTSHIELD_API_URL || DEFAULT_AGENTSHIELD_URL;
181
+ // Generate a temporary delegation ID for state (will be created after OAuth)
182
+ const delegationId = `temp-${Date.now()}`;
183
+ // Build state parameter with required fields
184
+ const state = {
185
+ project_id: projectId,
186
+ agent_did: agentDid,
187
+ session_id: sessionId,
188
+ delegation_id: delegationId,
189
+ scopes: scopes,
190
+ };
191
+ // Encode state as base64 (Cloudflare Workers compatible)
192
+ const stateParam = btoa(JSON.stringify(state));
193
+ // Build OAuth authorization URL
194
+ const oauthUrl = new URL(`${agentShieldUrl}/bouncer/oauth/authorize`);
195
+ oauthUrl.searchParams.set("response_type", "code");
196
+ oauthUrl.searchParams.set("client_id", projectId); // Use projectId as client_id
197
+ oauthUrl.searchParams.set("redirect_uri", `${serverUrl}/oauth/callback`);
198
+ oauthUrl.searchParams.set("scope", scopes.join(" "));
199
+ oauthUrl.searchParams.set("state", stateParam);
200
+ return oauthUrl.toString();
201
+ }
202
+ /**
203
+ * Link OAuth identity to User DID
204
+ *
205
+ * Maps OAuth provider identity (provider + subject) to a persistent User DID.
206
+ * If an ephemeral DID exists for the session, it becomes persistent.
207
+ *
208
+ * Phase 4 PR #3: OAuth Identity Linking
209
+ *
210
+ * @param oauthIdentity - OAuth provider identity
211
+ * @param sessionId - Current session ID (for ephemeral DID lookup)
212
+ * @returns Persistent User DID
213
+ */
214
+ async linkOAuthToUserDid(oauthIdentity, sessionId) {
215
+ if (!this.env.DELEGATION_STORAGE) {
216
+ // No storage - can't persist mapping, return ephemeral DID
217
+ console.warn("[ConsentService] No storage available for OAuth linking");
218
+ return await this.getUserDidForSession(sessionId);
219
+ }
220
+ const oauthKey = STORAGE_KEYS.oauthIdentity(oauthIdentity.provider, oauthIdentity.subject);
221
+ // Check if OAuth identity already mapped
222
+ try {
223
+ const existingUserDid = await this.env.DELEGATION_STORAGE.get(oauthKey, "text");
224
+ if (existingUserDid) {
225
+ console.log("[ConsentService] OAuth identity already mapped:", {
226
+ provider: oauthIdentity.provider,
227
+ subject: oauthIdentity.subject.substring(0, 20) + "...",
228
+ userDid: existingUserDid.substring(0, 20) + "...",
229
+ });
230
+ return existingUserDid;
231
+ }
232
+ }
233
+ catch (error) {
234
+ console.warn("[ConsentService] Failed to check OAuth mapping:", error);
235
+ // Continue to create new mapping
236
+ }
237
+ // Get/create User DID for session (may be ephemeral)
238
+ const userDid = await this.getUserDidForSession(sessionId);
239
+ // Store OAuth identity mapping (persistent - 90 days)
240
+ try {
241
+ await this.env.DELEGATION_STORAGE.put(oauthKey, userDid, {
242
+ expirationTtl: 90 * 24 * 60 * 60, // 90 days
243
+ });
244
+ // Also store full OAuth identity info for reference
245
+ const oauthIdentityKey = STORAGE_KEYS.userDid(oauthIdentity.provider, oauthIdentity.subject);
246
+ await this.env.DELEGATION_STORAGE.put(oauthIdentityKey, JSON.stringify(oauthIdentity), {
247
+ expirationTtl: 90 * 24 * 60 * 60, // 90 days
248
+ });
249
+ console.log("[ConsentService] OAuth identity linked to User DID:", {
250
+ provider: oauthIdentity.provider,
251
+ subject: oauthIdentity.subject.substring(0, 20) + "...",
252
+ userDid: userDid.substring(0, 20) + "...",
253
+ });
254
+ }
255
+ catch (error) {
256
+ console.error("[ConsentService] Failed to store OAuth mapping:", error);
257
+ // Non-fatal - continue with User DID
258
+ }
259
+ // Note: Ephemeral → persistent migration happens automatically
260
+ // The ephemeral DID becomes persistent when linked to OAuth identity
261
+ // Existing delegations with ephemeral DID will continue to work
262
+ // New delegations will use the persistent DID
263
+ return userDid;
264
+ }
25
265
  /**
26
266
  * Handle consent requests
27
267
  *
@@ -121,6 +361,48 @@ export class ConsentService {
121
361
  });
122
362
  }
123
363
  }
364
+ // Phase 4 PR #4: Extract OAuth identity from cookie (server-side)
365
+ let oauthIdentity = undefined;
366
+ try {
367
+ const cookieHeader = request.headers.get("Cookie");
368
+ if (cookieHeader) {
369
+ const cookies = cookieHeader.split("; ").map((c) => c.trim());
370
+ const oauthCookie = cookies.find((c) => c.startsWith("oauth_identity="));
371
+ if (oauthCookie) {
372
+ // Extract cookie value correctly - handle values that may contain '=' characters
373
+ // Use indexOf to find first '=' and take everything after it
374
+ const equalsIndex = oauthCookie.indexOf("=");
375
+ const cookieValue = equalsIndex >= 0 ? oauthCookie.substring(equalsIndex + 1) : "";
376
+ // Validate it's valid JSON before passing to client
377
+ try {
378
+ const parsed = JSON.parse(decodeURIComponent(cookieValue));
379
+ // Basic validation - ensure it has required fields
380
+ if (parsed && parsed.provider && parsed.subject) {
381
+ oauthIdentity = parsed;
382
+ }
383
+ }
384
+ catch (parseError) {
385
+ console.warn("[ConsentService] Invalid OAuth cookie format:", parseError);
386
+ }
387
+ }
388
+ }
389
+ }
390
+ catch (error) {
391
+ console.warn("[ConsentService] Failed to extract OAuth cookie:", error);
392
+ // Non-fatal - continue without OAuth identity
393
+ }
394
+ // Check if OAuth is required (after extracting OAuth identity)
395
+ const oauthRequired = await this.isOAuthRequired(projectId, oauthIdentity);
396
+ if (oauthRequired) {
397
+ // OAuth is required - redirect to OAuth provider instead of showing consent page
398
+ const oauthUrl = this.buildOAuthUrl(projectId, agentDid, sessionId, scopes, serverUrl);
399
+ console.log("[ConsentService] OAuth required, redirecting to OAuth provider:", {
400
+ projectId,
401
+ agentDid: agentDid.substring(0, 20) + "...",
402
+ oauthUrl: oauthUrl.substring(0, 100) + "...",
403
+ });
404
+ return Response.redirect(oauthUrl, 302);
405
+ }
124
406
  // Build consent page config
125
407
  const pageConfig = {
126
408
  tool,
@@ -135,8 +417,8 @@ export class ConsentService {
135
417
  customFields: consentConfig.customFields,
136
418
  autoClose: consentConfig.ui?.autoClose,
137
419
  };
138
- // Render page
139
- const html = this.renderer.render(pageConfig);
420
+ // Render page with OAuth identity (if available)
421
+ const html = this.renderer.render(pageConfig, oauthIdentity);
140
422
  return new Response(html, {
141
423
  status: 200,
142
424
  headers: {
@@ -250,78 +532,41 @@ export class ConsentService {
250
532
  };
251
533
  }
252
534
  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;
535
+ // Load Day0 configuration to determine field name and API capabilities
536
+ await loadDay0Config(this.env.DELEGATION_STORAGE);
537
+ const fieldName = await getDelegationFieldName(this.env.DELEGATION_STORAGE);
538
+ // Get userDID from session or generate new ephemeral DID
539
+ // Phase 4 PR #3: Use OAuth identity if provided in approval request
540
+ let userDid;
541
+ if (this.env.DELEGATION_STORAGE && request.session_id) {
542
+ try {
543
+ // Pass OAuth identity if available in approval request
544
+ userDid = await this.getUserDidForSession(request.session_id, request.oauth_identity);
545
+ }
546
+ catch (error) {
547
+ console.warn("[ConsentService] Failed to get/generate userDid:", error);
548
+ // Continue without userDid - delegation will use ephemeral placeholder
549
+ }
293
550
  }
551
+ const expiresInDays = 7; // Default to 7 days
552
+ // Build delegation request with error-based format detection
553
+ // Try full format first, fallback to simplified format on error
554
+ const delegationRequest = await this.buildDelegationRequest(request, userDid, expiresInDays, fieldName);
294
555
  console.log("[ConsentService] Creating delegation:", {
295
556
  url: `${agentShieldUrl}${AGENTSHIELD_ENDPOINTS.DELEGATIONS_CREATE}`,
296
557
  agentDid: request.agent_did.substring(0, 20) + "...",
297
558
  scopes: request.scopes,
298
559
  expiresInDays,
299
560
  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),
561
+ fieldName,
562
+ hasUserDid: !!userDid,
309
563
  });
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
- };
564
+ // Error-based format detection: try request format, fallback on error
565
+ const response = await this.tryAPICall(agentShieldUrl, apiKey, delegationRequest);
566
+ if (!response.success) {
567
+ return response;
323
568
  }
324
- const responseData = (await response.json());
569
+ const responseData = response.data;
325
570
  // Canonical format per @kya-os/contracts/agentshield-api:
326
571
  // Wrapped: { success: true, data: { delegation_id: string, agent_did: string, ... } }
327
572
  // Unwrapped: { delegation_id: string, agent_did: string, ... }
@@ -415,38 +660,16 @@ export class ConsentService {
415
660
  try {
416
661
  // Default TTL: 7 days (same as delegation expiration)
417
662
  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)
663
+ // Get userDID from approval request or session (Phase 4)
664
+ // For now, try to extract from request or session cache
665
+ let userDid;
666
+ // Try to get userDID from session cache
667
+ const sessionKey = STORAGE_KEYS.session(sessionId);
668
+ const sessionData = (await delegationStorage.get(sessionKey, "json"));
669
+ userDid = sessionData?.userDid;
670
+ // Primary: User+Agent scoped (no conflicts) - Phase 4
448
671
  if (userDid) {
449
- const userAgentKey = `user:${userDid}:agent:${agentDid}:delegation`;
672
+ const userAgentKey = STORAGE_KEYS.delegation(userDid, agentDid);
450
673
  await delegationStorage.put(userAgentKey, token, {
451
674
  expirationTtl: ttl,
452
675
  });
@@ -456,15 +679,31 @@ export class ConsentService {
456
679
  delegationId,
457
680
  });
458
681
  }
459
- // Store using session ID (secondary cache, shorter TTL for performance)
460
- const sessionKey = `session:${sessionId}`;
461
- await delegationStorage.put(sessionKey, token, {
682
+ // Backward compatibility: Agent-only key (24 hour TTL)
683
+ const legacyKey = STORAGE_KEYS.legacyDelegation(agentDid);
684
+ await delegationStorage.put(legacyKey, token, {
685
+ expirationTtl: 24 * 60 * 60, // 24 hours only
686
+ });
687
+ console.log("[ConsentService] ✅ Token stored with legacy agent key:", {
688
+ key: legacyKey,
689
+ ttl: 24 * 60 * 60,
690
+ delegationId,
691
+ });
692
+ // Session cache for fast lookup (shorter TTL for performance)
693
+ const sessionDataToStore = {
694
+ userDid,
695
+ agentDid,
696
+ delegationToken: token,
697
+ cachedAt: Date.now(),
698
+ };
699
+ await delegationStorage.put(sessionKey, JSON.stringify(sessionDataToStore), {
462
700
  expirationTtl: Math.min(ttl, DEFAULT_SESSION_CACHE_TTL),
463
701
  });
464
702
  console.log("[ConsentService] ✅ Token cached for session:", {
465
703
  key: sessionKey,
466
704
  ttl: Math.min(ttl, DEFAULT_SESSION_CACHE_TTL),
467
705
  sessionId,
706
+ userDid,
468
707
  });
469
708
  }
470
709
  catch (error) {
@@ -506,5 +745,238 @@ export class ConsentService {
506
745
  },
507
746
  });
508
747
  }
748
+ /**
749
+ * Build delegation request with error-based format detection
750
+ *
751
+ * Uses Day0 config to determine field name and includes issuerDid when available.
752
+ */
753
+ async buildDelegationRequest(request, userDid, expiresInDays, fieldName) {
754
+ const baseRequest = {
755
+ agent_did: request.agent_did,
756
+ scopes: request.scopes,
757
+ expires_in_days: expiresInDays,
758
+ };
759
+ // Include session_id if provided
760
+ if (request.session_id) {
761
+ baseRequest.session_id = request.session_id;
762
+ }
763
+ // Include project_id if provided
764
+ if (request.project_id) {
765
+ baseRequest.project_id = request.project_id;
766
+ }
767
+ // Check cached format preference
768
+ const cacheKey = STORAGE_KEYS.formatPreference();
769
+ let cachedFormat = null;
770
+ if (this.env.DELEGATION_STORAGE) {
771
+ try {
772
+ const cached = (await this.env.DELEGATION_STORAGE.get(cacheKey, "json"));
773
+ if (cached &&
774
+ cached.timestamp &&
775
+ Date.now() - cached.timestamp < 3600000) {
776
+ cachedFormat = cached.format;
777
+ }
778
+ }
779
+ catch {
780
+ // Ignore cache errors
781
+ }
782
+ }
783
+ // If we have a cached preference, use it directly
784
+ if (cachedFormat === "full") {
785
+ return this.buildFullFormatRequest(request, userDid, expiresInDays);
786
+ }
787
+ else if (cachedFormat === "simplified") {
788
+ return this.buildSimplifiedFormatRequest(request, userDid, expiresInDays, fieldName);
789
+ }
790
+ // No cache - return request that will be tried with error-based detection
791
+ return {
792
+ _tryFormats: true,
793
+ fullFormat: await this.buildFullFormatRequest(request, userDid, expiresInDays),
794
+ simplifiedFormat: this.buildSimplifiedFormatRequest(request, userDid, expiresInDays, fieldName),
795
+ };
796
+ }
797
+ /**
798
+ * Build full DelegationRecord format request (future format)
799
+ */
800
+ async buildFullFormatRequest(request, userDid, expiresInDays) {
801
+ const notAfter = Date.now() + expiresInDays * 24 * 60 * 60 * 1000;
802
+ return {
803
+ delegation: {
804
+ id: crypto.randomUUID(),
805
+ issuerDid: userDid || "did:key:z6MkEphemeral", // Use ephemeral if no userDid
806
+ subjectDid: request.agent_did,
807
+ constraints: {
808
+ scopes: request.scopes,
809
+ notAfter,
810
+ notBefore: Date.now(),
811
+ },
812
+ status: "active",
813
+ createdAt: Date.now(),
814
+ },
815
+ };
816
+ }
817
+ /**
818
+ * Check if a string is a valid UUID
819
+ *
820
+ * Validates that project_id is a UUID before including it in API requests.
821
+ * If project_id is not a UUID (e.g., project slug), it's omitted and the
822
+ * API extracts it from the API key instead.
823
+ *
824
+ * @param str - String to validate
825
+ * @returns True if string is a valid UUID
826
+ */
827
+ isValidUUID(str) {
828
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
829
+ return uuidRegex.test(str);
830
+ }
831
+ /**
832
+ * Build simplified format request with proper field name
833
+ */
834
+ buildSimplifiedFormatRequest(request, userDid, expiresInDays, fieldName) {
835
+ const simplifiedRequest = {
836
+ agent_did: request.agent_did,
837
+ scopes: request.scopes,
838
+ expires_in_days: expiresInDays,
839
+ };
840
+ if (request.session_id) {
841
+ simplifiedRequest.session_id = request.session_id;
842
+ }
843
+ // Include project_id if provided
844
+ // Note: AgentShield API schema requires UUID format if provided, but we include it
845
+ // regardless of format to let the API return proper validation errors rather than
846
+ // silently omitting it. If the API accepts non-UUID project_ids or can extract
847
+ // from API key, it will handle accordingly.
848
+ if (request.project_id) {
849
+ simplifiedRequest.project_id = request.project_id;
850
+ // Log warning if not a UUID (for debugging) but still include it
851
+ if (!this.isValidUUID(request.project_id)) {
852
+ console.warn("[ConsentService] project_id is not a UUID format, including anyway (API will validate):", {
853
+ project_id: request.project_id.substring(0, 20) + "...",
854
+ });
855
+ }
856
+ }
857
+ // Use the correct field name (metadata or custom_fields) from Day0 config
858
+ if (userDid) {
859
+ simplifiedRequest[fieldName] = {
860
+ issuer_did: userDid,
861
+ subject_did: request.agent_did,
862
+ format_version: "simplified_v1",
863
+ };
864
+ }
865
+ // Include custom_fields from request if provided
866
+ if (request.customFields && Object.keys(request.customFields).length > 0) {
867
+ simplifiedRequest.custom_fields = {
868
+ ...(simplifiedRequest.custom_fields || {}),
869
+ ...request.customFields,
870
+ };
871
+ }
872
+ return simplifiedRequest;
873
+ }
874
+ /**
875
+ * Try API call with error-based format detection
876
+ */
877
+ async tryAPICall(agentShieldUrl, apiKey, request) {
878
+ // Handle format detection
879
+ if (request._tryFormats && request.fullFormat && request.simplifiedFormat) {
880
+ // Try full format first
881
+ const fullResponse = await this.makeAPICall(agentShieldUrl, apiKey, request.fullFormat);
882
+ if (fullResponse.success ||
883
+ fullResponse.error_code !== "validation_error") {
884
+ // Full format worked or failed for non-format reasons
885
+ await this.cacheFormatPreference("full");
886
+ return fullResponse;
887
+ }
888
+ // Full format failed with validation error, try simplified
889
+ console.log("[ConsentService] Full format failed, trying simplified format...");
890
+ const simplifiedResponse = await this.makeAPICall(agentShieldUrl, apiKey, request.simplifiedFormat);
891
+ if (simplifiedResponse.success) {
892
+ await this.cacheFormatPreference("simplified");
893
+ }
894
+ return simplifiedResponse;
895
+ }
896
+ // Direct call (format already determined)
897
+ return this.makeAPICall(agentShieldUrl, apiKey, request);
898
+ }
899
+ /**
900
+ * Make API call and parse response
901
+ */
902
+ async makeAPICall(agentShieldUrl, apiKey, requestBody) {
903
+ try {
904
+ const response = await fetch(`${agentShieldUrl}${AGENTSHIELD_ENDPOINTS.DELEGATIONS_CREATE}`, {
905
+ method: "POST",
906
+ headers: {
907
+ Authorization: `Bearer ${apiKey}`,
908
+ "Content-Type": "application/json",
909
+ "X-Request-ID": crypto.randomUUID(),
910
+ },
911
+ body: JSON.stringify(requestBody),
912
+ });
913
+ const responseText = await response.text();
914
+ let responseData;
915
+ try {
916
+ responseData = JSON.parse(responseText);
917
+ }
918
+ catch {
919
+ responseData = responseText;
920
+ }
921
+ // Check for validation error specifically
922
+ if (response.status === 400) {
923
+ const errorMessage = responseData
924
+ ?.error?.message ||
925
+ responseData?.message ||
926
+ "Validation failed";
927
+ if (errorMessage.includes("format") ||
928
+ errorMessage.includes("schema") ||
929
+ errorMessage.includes("invalid")) {
930
+ return {
931
+ success: false,
932
+ error: errorMessage,
933
+ error_code: "validation_error",
934
+ };
935
+ }
936
+ }
937
+ if (!response.ok) {
938
+ const errorData = responseData;
939
+ return {
940
+ success: false,
941
+ error: errorData.error?.message ||
942
+ errorData.message ||
943
+ "API request failed",
944
+ error_code: errorData.error?.code || "api_error",
945
+ };
946
+ }
947
+ return {
948
+ success: true,
949
+ data: responseData,
950
+ };
951
+ }
952
+ catch (error) {
953
+ console.error("[ConsentService] API call failed:", error);
954
+ return {
955
+ success: false,
956
+ error: error instanceof Error ? error.message : "Network request failed",
957
+ error_code: "network_error",
958
+ };
959
+ }
960
+ }
961
+ /**
962
+ * Cache successful format preference
963
+ */
964
+ async cacheFormatPreference(format) {
965
+ if (!this.env.DELEGATION_STORAGE)
966
+ return;
967
+ const cacheKey = STORAGE_KEYS.formatPreference();
968
+ try {
969
+ await this.env.DELEGATION_STORAGE.put(cacheKey, JSON.stringify({
970
+ format,
971
+ timestamp: Date.now(),
972
+ }), {
973
+ expirationTtl: 3600, // 1 hour
974
+ });
975
+ console.log(`[ConsentService] Cached format preference: ${format}`);
976
+ }
977
+ catch (error) {
978
+ console.warn("[ConsentService] Failed to cache format preference:", error);
979
+ }
980
+ }
509
981
  }
510
982
  //# sourceMappingURL=consent.service.js.map