@kya-os/mcp-i-cloudflare 1.6.25 → 1.6.27

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 (51) hide show
  1. package/dist/constants/storage-keys.d.ts +12 -0
  2. package/dist/constants/storage-keys.d.ts.map +1 -1
  3. package/dist/constants/storage-keys.js +12 -0
  4. package/dist/constants/storage-keys.js.map +1 -1
  5. package/dist/constants.d.ts +10 -0
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/constants.js +10 -0
  8. package/dist/constants.js.map +1 -1
  9. package/dist/runtime/oauth-handler.d.ts.map +1 -1
  10. package/dist/runtime/oauth-handler.js +64 -0
  11. package/dist/runtime/oauth-handler.js.map +1 -1
  12. package/dist/services/admin.service.d.ts.map +1 -1
  13. package/dist/services/admin.service.js +31 -0
  14. package/dist/services/admin.service.js.map +1 -1
  15. package/dist/services/consent-page-renderer.d.ts +55 -0
  16. package/dist/services/consent-page-renderer.d.ts.map +1 -1
  17. package/dist/services/consent-page-renderer.js +284 -0
  18. package/dist/services/consent-page-renderer.js.map +1 -1
  19. package/dist/services/consent.service.d.ts +85 -15
  20. package/dist/services/consent.service.d.ts.map +1 -1
  21. package/dist/services/consent.service.js +530 -37
  22. package/dist/services/consent.service.js.map +1 -1
  23. package/dist/services/credential-auth.handler.d.ts +72 -0
  24. package/dist/services/credential-auth.handler.d.ts.map +1 -0
  25. package/dist/services/credential-auth.handler.js +203 -0
  26. package/dist/services/credential-auth.handler.js.map +1 -0
  27. package/dist/services/idp-token-storage.d.ts +54 -6
  28. package/dist/services/idp-token-storage.d.ts.map +1 -1
  29. package/dist/services/idp-token-storage.js +21 -6
  30. package/dist/services/idp-token-storage.js.map +1 -1
  31. package/package.json +3 -3
  32. package/dist/__tests__/e2e/test-config.d.ts +0 -37
  33. package/dist/__tests__/e2e/test-config.d.ts.map +0 -1
  34. package/dist/__tests__/e2e/test-config.js +0 -62
  35. package/dist/__tests__/e2e/test-config.js.map +0 -1
  36. package/dist/utils/client-info.d.ts +0 -69
  37. package/dist/utils/client-info.d.ts.map +0 -1
  38. package/dist/utils/client-info.js +0 -178
  39. package/dist/utils/client-info.js.map +0 -1
  40. package/dist/utils/error-formatter.d.ts +0 -103
  41. package/dist/utils/error-formatter.d.ts.map +0 -1
  42. package/dist/utils/error-formatter.js +0 -245
  43. package/dist/utils/error-formatter.js.map +0 -1
  44. package/dist/utils/initialize-context.d.ts +0 -91
  45. package/dist/utils/initialize-context.d.ts.map +0 -1
  46. package/dist/utils/initialize-context.js +0 -169
  47. package/dist/utils/initialize-context.js.map +0 -1
  48. package/dist/utils/oauth-identity.d.ts +0 -58
  49. package/dist/utils/oauth-identity.d.ts.map +0 -1
  50. package/dist/utils/oauth-identity.js +0 -215
  51. package/dist/utils/oauth-identity.js.map +0 -1
@@ -8,17 +8,20 @@
8
8
  */
9
9
  import { ConsentConfigService } from "./consent-config.service";
10
10
  import { ConsentPageRenderer } from "./consent-page-renderer";
11
- import { DEFAULT_AGENTSHIELD_URL, DEFAULT_SESSION_CACHE_TTL, } from "../constants";
11
+ import { DEFAULT_AGENTSHIELD_URL, DEFAULT_SESSION_CACHE_TTL, KEY_PAIR_TTL_SECONDS, } from "../constants";
12
12
  import { STORAGE_KEYS } from "../constants/storage-keys";
13
13
  import { loadDay0Config, getDelegationFieldName } from "../utils/day0-config";
14
14
  import { validateConsentApprovalRequest, } from "@kya-os/contracts/consent";
15
15
  import { AGENTSHIELD_ENDPOINTS, createDelegationAPIResponseSchema, createDelegationResponseSchema, } from "@kya-os/contracts/agentshield-api";
16
- import { fetchRemoteConfig, createUnsignedVCJWT, completeVCJWT, parseVCJWT, base64urlEncodeFromBytes, base64urlDecodeToBytes, bytesToBase64, createDelegationVerifier, createDidKeyResolver, } from "@kya-os/mcp-i-core";
16
+ import { fetchRemoteConfig, createUnsignedVCJWT, completeVCJWT, parseVCJWT, base64urlEncodeFromBytes, base64urlDecodeToBytes, bytesToBase64, generateDidKeyFromBase64, createDelegationVerifier, createDidKeyResolver, } from "@kya-os/mcp-i-core";
17
17
  import { wrapDelegationAsVC } from "@kya-os/contracts";
18
18
  import { WebCryptoProvider } from "../providers/crypto";
19
19
  import { ConsentAuditService } from "./consent-audit.service";
20
20
  import { CloudflareProofGenerator } from "../proof-generator";
21
21
  import { ProofService } from "./proof.service";
22
+ import { createCredentialAuthHandler } from "./credential-auth.handler";
23
+ import { IdpTokenStorage } from "./idp-token-storage";
24
+ import { OAuthSecurityService } from "./oauth-security.service";
22
25
  export class ConsentService {
23
26
  configService;
24
27
  renderer;
@@ -179,16 +182,6 @@ export class ConsentService {
179
182
  return undefined;
180
183
  }
181
184
  }
182
- /**
183
- * Get or generate User DID for a session
184
- *
185
- * Phase 4 PR #1: Generates ephemeral DIDs for sessions
186
- * Phase 4 PR #3: Checks OAuth mappings for persistent DIDs
187
- *
188
- * @param sessionId - Session ID
189
- * @param oauthIdentity - Optional OAuth provider identity
190
- * @returns User DID (did:key format)
191
- */
192
185
  /**
193
186
  * Get user DID for a session
194
187
  *
@@ -316,16 +309,123 @@ export class ConsentService {
316
309
  * @returns UserKeyPair or null if not available
317
310
  */
318
311
  async getKeyPairForSession(sessionId, oauthIdentity) {
319
- // Ensure UserDidManager is initialized
312
+ // Priority 1: Check direct KV storage by OAuth identity
313
+ // This is the primary storage location used by getOrCreateKeyPairForUser()
314
+ if (this.env.DELEGATION_STORAGE &&
315
+ oauthIdentity?.provider &&
316
+ oauthIdentity?.subject) {
317
+ try {
318
+ const key = STORAGE_KEYS.userKeyPair(oauthIdentity.provider, oauthIdentity.subject);
319
+ const stored = (await this.env.DELEGATION_STORAGE.get(key, "json"));
320
+ if (stored) {
321
+ console.log("[ConsentService] Found key pair in KV storage");
322
+ return stored;
323
+ }
324
+ }
325
+ catch (error) {
326
+ console.warn("[ConsentService] KV key pair lookup failed:", error);
327
+ }
328
+ }
329
+ // Priority 2: Try UserDidManager (legacy path, may not be initialized)
320
330
  if (!this.userDidManager) {
321
331
  // Initialize with storage if available
322
332
  await this.getUserDidForSession(sessionId, oauthIdentity);
323
333
  }
324
- if (!this.userDidManager) {
325
- console.warn("[ConsentService] UserDidManager not initialized");
334
+ if (this.userDidManager) {
335
+ const keyPair = await this.userDidManager.getKeyPairForSession(sessionId, oauthIdentity);
336
+ if (keyPair) {
337
+ return keyPair;
338
+ }
339
+ }
340
+ // No key pair found
341
+ return null;
342
+ }
343
+ /**
344
+ * Get or create key pair for a user (OAuth flow VC signing)
345
+ *
346
+ * Unlike getKeyPairForSession which only retrieves, this method
347
+ * GENERATES a new key pair if one doesn't exist.
348
+ *
349
+ * NOTE: The generated key pair has its own did:key which may differ
350
+ * from AgentShield's persistentUserDid. Both are tied to the same
351
+ * OAuth identity. See docs/proposals/VC/AGENTSHIELD_SIGNING_ENDPOINT.md
352
+ * for the future cleaner architecture.
353
+ *
354
+ * @param sessionId - Session ID for caching
355
+ * @param oauthIdentity - OAuth identity for persistent storage
356
+ * @returns UserKeyPair or null if generation fails
357
+ */
358
+ async getOrCreateKeyPairForUser(sessionId, oauthIdentity) {
359
+ // 1. Try existing lookup first
360
+ const existingKeyPair = await this.getKeyPairForSession(sessionId, oauthIdentity);
361
+ if (existingKeyPair) {
362
+ console.log("[ConsentService] Found existing key pair for OAuth identity");
363
+ return existingKeyPair;
364
+ }
365
+ // 2. Check KV storage directly by OAuth identity
366
+ if (this.env.DELEGATION_STORAGE &&
367
+ oauthIdentity.provider &&
368
+ oauthIdentity.subject) {
369
+ try {
370
+ const key = STORAGE_KEYS.userKeyPair(oauthIdentity.provider, oauthIdentity.subject);
371
+ const stored = (await this.env.DELEGATION_STORAGE.get(key, "json"));
372
+ if (stored) {
373
+ console.log("[ConsentService] Found key pair in KV storage");
374
+ return stored;
375
+ }
376
+ }
377
+ catch (error) {
378
+ console.warn("[ConsentService] KV key pair lookup failed:", error);
379
+ }
380
+ }
381
+ // 3. Generate new key pair
382
+ console.log("[ConsentService] Generating new key pair for OAuth identity");
383
+ try {
384
+ const crypto = new WebCryptoProvider();
385
+ const rawKeyPair = await crypto.generateKeyPair();
386
+ // Generate did:key from public key
387
+ const did = this.generateDidKeyFromPublicKey(rawKeyPair.publicKey);
388
+ const keyId = `${did}#keys-1`;
389
+ const keyPair = {
390
+ did,
391
+ publicKey: rawKeyPair.publicKey,
392
+ privateKey: rawKeyPair.privateKey,
393
+ keyId,
394
+ };
395
+ // 4. Persist to KV by OAuth identity (1 year TTL)
396
+ // Note: Persistence failure is non-fatal - key pair can still be used
397
+ if (this.env.DELEGATION_STORAGE &&
398
+ oauthIdentity.provider &&
399
+ oauthIdentity.subject) {
400
+ try {
401
+ const key = STORAGE_KEYS.userKeyPair(oauthIdentity.provider, oauthIdentity.subject);
402
+ await this.env.DELEGATION_STORAGE.put(key, JSON.stringify(keyPair), {
403
+ expirationTtl: KEY_PAIR_TTL_SECONDS,
404
+ });
405
+ console.log("[ConsentService] Key pair persisted to KV storage");
406
+ }
407
+ catch (persistError) {
408
+ console.warn("[ConsentService] KV key pair persistence failed (non-fatal):", persistError);
409
+ }
410
+ }
411
+ console.log("[ConsentService] Key pair generated:", {
412
+ did: did.substring(0, 30) + "...",
413
+ keyId,
414
+ });
415
+ return keyPair;
416
+ }
417
+ catch (error) {
418
+ console.error("[ConsentService] Key pair generation failed:", error);
326
419
  return null;
327
420
  }
328
- return await this.userDidManager.getKeyPairForSession(sessionId, oauthIdentity);
421
+ }
422
+ /**
423
+ * Generate did:key from Ed25519 public key (base64)
424
+ * Delegates to shared utility in @kya-os/mcp-i-core
425
+ * Following spec: https://w3c-ccg.github.io/did-method-key/
426
+ */
427
+ generateDidKeyFromPublicKey(publicKeyBase64) {
428
+ return generateDidKeyFromBase64(publicKeyBase64);
329
429
  }
330
430
  /**
331
431
  * Issue a Delegation Verifiable Credential as JWT
@@ -338,23 +438,25 @@ export class ConsentService {
338
438
  * @param delegation - The delegation record to wrap as a VC
339
439
  * @param sessionId - Session ID for key pair retrieval
340
440
  * @param oauthIdentity - Optional OAuth identity for persistent key lookup
441
+ * @param providedKeyPair - Optional key pair to use directly (avoids re-lookup, handles KV persistence failures)
341
442
  * @returns JWT string (header.payload.signature) or null if key pair unavailable
342
443
  */
343
- async issueDelegationVC(delegation, sessionId, oauthIdentity) {
344
- // Get the key pair for signing
345
- const keyPair = await this.getKeyPairForSession(sessionId, oauthIdentity);
444
+ async issueDelegationVC(delegation, sessionId, oauthIdentity, providedKeyPair) {
445
+ // Use provided key pair or look it up
446
+ // This allows callers to pass a key pair directly when KV persistence may have failed
447
+ const keyPair = providedKeyPair ??
448
+ (await this.getKeyPairForSession(sessionId, oauthIdentity));
346
449
  if (!keyPair) {
347
450
  console.warn("[ConsentService] Cannot issue VC: no key pair available for session");
348
451
  return null;
349
452
  }
350
453
  try {
351
454
  // Step 1: Create the unsigned VC
455
+ // Note: wrapDelegationAsVC handles notAfter→expirationDate conversion correctly
456
+ // (multiplies seconds by 1000 for Date constructor)
352
457
  const unsignedVC = wrapDelegationAsVC(delegation, {
353
458
  id: `urn:uuid:${delegation.id}`,
354
459
  issuanceDate: new Date().toISOString(),
355
- expirationDate: delegation.constraints.notAfter
356
- ? new Date(delegation.constraints.notAfter).toISOString()
357
- : undefined,
358
460
  });
359
461
  // Override issuer with the user's DID (not the agent's DID)
360
462
  const vcWithCorrectIssuer = {
@@ -679,7 +781,9 @@ export class ConsentService {
679
781
  //
680
782
  // Examples: GitHub Apps (PKCE, no proxy), Google (PKCE, no proxy)
681
783
  // Counter-example: GitHub OAuth Apps (PKCE supported, but proxyMode=true for client_secret)
682
- if (providerConfig && providerConfig.supportsPKCE && !providerConfig.proxyMode) {
784
+ if (providerConfig &&
785
+ providerConfig.supportsPKCE &&
786
+ !providerConfig.proxyMode) {
683
787
  // Use providerConfig.clientId from AgentShield dashboard config
684
788
  const oauthClientId = providerConfig.clientId || projectId;
685
789
  console.log("[ConsentService] Using direct OAuth mode (PKCE)", {
@@ -800,13 +904,13 @@ export class ConsentService {
800
904
  * Link OAuth identity to User DID
801
905
  *
802
906
  * Maps OAuth provider identity (provider + subject) to a persistent User DID.
803
- * If an ephemeral DID exists for the session, it becomes persistent.
804
- *
805
- * Phase 4 PR #3: OAuth Identity Linking
907
+ * Phase 5: Requires existing userDid from AgentShield /identity/resolve endpoint.
908
+ * Throws error if session has no user DID (enforces OAuth → identity flow).
806
909
  *
807
910
  * @param oauthIdentity - OAuth provider identity
808
- * @param sessionId - Current session ID (for ephemeral DID lookup)
911
+ * @param sessionId - Current session ID
809
912
  * @returns Persistent User DID
913
+ * @throws Error if no userDid exists for the session
810
914
  */
811
915
  async linkOAuthToUserDid(oauthIdentity, sessionId) {
812
916
  if (!this.env.DELEGATION_STORAGE) {
@@ -2258,6 +2362,13 @@ export class ConsentService {
2258
2362
  bodyKeys: Object.keys(body || {}),
2259
2363
  hasOAuthIdentity: !!body?.oauth_identity,
2260
2364
  });
2365
+ // CRED-003: Check for credential provider submission
2366
+ // Credential submissions include `provider_type: 'credential'` and are handled separately
2367
+ const bodyObj = body;
2368
+ if (bodyObj?.provider_type === "credential") {
2369
+ console.log("[ConsentService] Credential submission detected");
2370
+ return this.handleCredentialApproval(bodyObj);
2371
+ }
2261
2372
  // Convert null oauth_identity to undefined for proper schema validation
2262
2373
  // Zod's .nullish() should handle null, but converting to undefined is more explicit
2263
2374
  // and avoids potential edge cases with FormData parsing
@@ -2448,10 +2559,9 @@ export class ConsentService {
2448
2559
  // Load Day0 configuration to determine field name and API capabilities
2449
2560
  await loadDay0Config(this.env.DELEGATION_STORAGE);
2450
2561
  const fieldName = await getDelegationFieldName(this.env.DELEGATION_STORAGE);
2451
- // Get userDID from session or generate new ephemeral DID
2452
- // Phase 4 PR #3: Use OAuth identity if provided in approval request
2453
- // CRITICAL: Must work with or without OAuth identity
2454
- // CRITICAL: Always generate ephemeral DID if session_id is available, even without DELEGATION_STORAGE
2562
+ // Get userDID from session (Phase 5: returns null for anonymous sessions)
2563
+ // OAuth identity is resolved via AgentShield /identity/resolve endpoint
2564
+ // CRITICAL: Must work with or without OAuth identity - delegation is valid even without user_identifier
2455
2565
  let userDid;
2456
2566
  if (request.session_id) {
2457
2567
  try {
@@ -2488,8 +2598,11 @@ export class ConsentService {
2488
2598
  if (request.session_id && userDid) {
2489
2599
  try {
2490
2600
  // Create a local delegation record for VC issuance
2601
+ // NOTE: notAfter/notBefore use SECONDS (per contracts schema)
2602
+ // createdAt uses MILLISECONDS (per contracts schema)
2491
2603
  const delegationId = crypto.randomUUID();
2492
- const notAfter = Date.now() + expiresInDays * 24 * 60 * 60 * 1000;
2604
+ const nowSec = Math.floor(Date.now() / 1000);
2605
+ const notAfter = nowSec + expiresInDays * 24 * 60 * 60; // seconds
2493
2606
  const scopes = request.scopes ?? [];
2494
2607
  const localDelegation = {
2495
2608
  id: delegationId,
@@ -2499,11 +2612,11 @@ export class ConsentService {
2499
2612
  constraints: {
2500
2613
  scopes,
2501
2614
  notAfter,
2502
- notBefore: Date.now(),
2615
+ notBefore: nowSec,
2503
2616
  },
2504
- signature: "", // Will be in JWT
2617
+ signature: "vc-jwt-signed", // Placeholder - actual signature is in the VC-JWT
2505
2618
  status: "active",
2506
- createdAt: Date.now(),
2619
+ createdAt: Date.now(), // milliseconds per schema
2507
2620
  };
2508
2621
  const vcResult = await this.issueDelegationVC(localDelegation, request.session_id, request.oauth_identity || undefined);
2509
2622
  credentialJwt = vcResult ?? undefined;
@@ -2764,6 +2877,386 @@ export class ConsentService {
2764
2877
  simplifiedFormat: this.buildSimplifiedFormatRequest(request, userDid, expiresInDays, fieldName, credentialJwt),
2765
2878
  };
2766
2879
  }
2880
+ // ============================================================================
2881
+ // Credential Provider Handling (CRED-003)
2882
+ // ============================================================================
2883
+ /**
2884
+ * Handle credential-based authentication submission
2885
+ *
2886
+ * Authenticates user credentials against customer's endpoint, resolves identity
2887
+ * via AgentShield, stores token with usage metadata, and creates delegation.
2888
+ *
2889
+ * @param body - Raw request body with credential fields
2890
+ * @returns JSON response
2891
+ */
2892
+ async handleCredentialApproval(body) {
2893
+ console.log("[ConsentService] Processing credential approval");
2894
+ // Extract standard fields
2895
+ const { tool, scopes: rawScopes, agent_did, session_id, project_id, provider, provider_type, csrf_token, ...credentials } = body;
2896
+ // Parse scopes - handles double JSON encoding from form submission
2897
+ // The form stores scopes as JSON string, then JS submits it as JSON again
2898
+ const scopes = this.parseScopes(rawScopes);
2899
+ // Basic validation - includes provider to avoid misleading errors
2900
+ if (!tool || !session_id || !project_id || !agent_did || !provider) {
2901
+ const missingFields = [
2902
+ !tool && "tool",
2903
+ !session_id && "session_id",
2904
+ !project_id && "project_id",
2905
+ !agent_did && "agent_did",
2906
+ !provider && "provider",
2907
+ ].filter(Boolean);
2908
+ return new Response(JSON.stringify({
2909
+ success: false,
2910
+ error: `Missing required fields: ${missingFields.join(", ")}`,
2911
+ error_code: "validation_error",
2912
+ }), { status: 400, headers: { "Content-Type": "application/json" } });
2913
+ }
2914
+ // Validate CSRF token
2915
+ if (!csrf_token || typeof csrf_token !== "string") {
2916
+ console.warn("[ConsentService] Missing or invalid CSRF token");
2917
+ return new Response(JSON.stringify({
2918
+ success: false,
2919
+ error: "Invalid or missing CSRF token",
2920
+ error_code: "csrf_error",
2921
+ }), { status: 403, headers: { "Content-Type": "application/json" } });
2922
+ }
2923
+ // Validate CSRF token against stored value
2924
+ const csrfValid = await this.validateCredentialCsrfToken(csrf_token, session_id);
2925
+ if (!csrfValid) {
2926
+ console.warn("[ConsentService] CSRF token validation failed", {
2927
+ sessionId: session_id.substring(0, 20) + "...",
2928
+ });
2929
+ return new Response(JSON.stringify({
2930
+ success: false,
2931
+ error: "CSRF token validation failed",
2932
+ error_code: "csrf_error",
2933
+ }), { status: 403, headers: { "Content-Type": "application/json" } });
2934
+ }
2935
+ try {
2936
+ // 1. Fetch credential provider config from AgentShield
2937
+ const providerConfig = await this.getCredentialProviderConfig(project_id, provider);
2938
+ if (!providerConfig) {
2939
+ console.error("[ConsentService] Credential provider config not found", {
2940
+ projectId: project_id,
2941
+ provider,
2942
+ });
2943
+ return new Response(JSON.stringify({
2944
+ success: false,
2945
+ error: "Provider configuration not found",
2946
+ error_code: "provider_not_found",
2947
+ }), { status: 400, headers: { "Content-Type": "application/json" } });
2948
+ }
2949
+ // 2. Authenticate with customer's endpoint
2950
+ const credentialHandler = createCredentialAuthHandler({
2951
+ fetch: globalThis.fetch,
2952
+ logger: (msg, data) => console.log(msg, data),
2953
+ });
2954
+ const authResult = await credentialHandler.authenticate(providerConfig, credentials);
2955
+ if (!authResult.success) {
2956
+ console.error("[ConsentService] Credential authentication failed", {
2957
+ error: authResult.error,
2958
+ });
2959
+ return new Response(JSON.stringify({
2960
+ success: false,
2961
+ error: authResult.error || "Authentication failed",
2962
+ error_code: "auth_failed",
2963
+ }), { status: 401, headers: { "Content-Type": "application/json" } });
2964
+ }
2965
+ console.log("[ConsentService] ✅ Credential authentication successful");
2966
+ // 3. Resolve identity via AgentShield
2967
+ const identityResult = await this.resolveCredentialIdentity({
2968
+ projectId: project_id,
2969
+ provider: provider,
2970
+ userId: authResult.userId || "unknown",
2971
+ userEmail: authResult.userEmail,
2972
+ userDisplayName: authResult.userDisplayName,
2973
+ });
2974
+ if (!identityResult.success || !identityResult.userDid) {
2975
+ return new Response(JSON.stringify({
2976
+ success: false,
2977
+ error: "Identity resolution failed",
2978
+ error_code: "identity_resolution_failed",
2979
+ }), { status: 500, headers: { "Content-Type": "application/json" } });
2980
+ }
2981
+ console.log("[ConsentService] ✅ Identity resolved", {
2982
+ userDid: identityResult.userDid.substring(0, 30) + "...",
2983
+ });
2984
+ // 4. Store token in IdpTokenStorage with usage metadata
2985
+ await this.storeCredentialToken({
2986
+ userDid: identityResult.userDid,
2987
+ provider: provider,
2988
+ sessionToken: authResult.sessionToken,
2989
+ providerConfig,
2990
+ expiresIn: authResult.expiresIn,
2991
+ scopes,
2992
+ });
2993
+ console.log("[ConsentService] ✅ Token stored");
2994
+ // 5. Create delegation using standard flow
2995
+ const approvalRequest = {
2996
+ tool: tool,
2997
+ scopes,
2998
+ agent_did: agent_did,
2999
+ session_id: session_id,
3000
+ project_id: project_id,
3001
+ termsAccepted: true,
3002
+ user_did: identityResult.userDid,
3003
+ };
3004
+ const delegationResult = await this.createDelegation(approvalRequest);
3005
+ if (!delegationResult.success) {
3006
+ return new Response(JSON.stringify({
3007
+ success: false,
3008
+ error: delegationResult.error || "Delegation creation failed",
3009
+ error_code: delegationResult.error_code || "delegation_failed",
3010
+ }), { status: 500, headers: { "Content-Type": "application/json" } });
3011
+ }
3012
+ console.log("[ConsentService] ✅ Credential approval complete", {
3013
+ delegationId: delegationResult.delegation_id?.substring(0, 20) + "...",
3014
+ });
3015
+ // Store delegation token
3016
+ await this.storeDelegationToken(session_id, agent_did, delegationResult.delegation_token, delegationResult.delegation_id);
3017
+ // Return success with redirect URL
3018
+ const serverUrl = this.env.MCP_SERVER_URL || "";
3019
+ const redirectUrl = `${serverUrl}/consent/success?delegation_id=${encodeURIComponent(delegationResult.delegation_id)}&project_id=${encodeURIComponent(project_id)}`;
3020
+ return new Response(JSON.stringify({
3021
+ success: true,
3022
+ delegation_id: delegationResult.delegation_id,
3023
+ delegation_token: delegationResult.delegation_token,
3024
+ redirectUrl,
3025
+ }), { status: 200, headers: { "Content-Type": "application/json" } });
3026
+ }
3027
+ catch (error) {
3028
+ console.error("[ConsentService] Credential approval error:", error);
3029
+ return new Response(JSON.stringify({
3030
+ success: false,
3031
+ error: error instanceof Error ? error.message : "Internal error",
3032
+ error_code: "internal_error",
3033
+ }), { status: 500, headers: { "Content-Type": "application/json" } });
3034
+ }
3035
+ }
3036
+ /**
3037
+ * Parse scopes from form data, handling double JSON encoding
3038
+ *
3039
+ * The credential form stores scopes as a JSON string in a hidden input,
3040
+ * then the form submission script JSON-stringifies the entire form data.
3041
+ * This causes double-encoding: "[\"scope1\"]" instead of ["scope1"]
3042
+ *
3043
+ * @param rawScopes - Raw scopes value from form (may be string or array)
3044
+ * @returns Parsed string array of scopes
3045
+ */
3046
+ parseScopes(rawScopes) {
3047
+ // Already an array - return as-is
3048
+ if (Array.isArray(rawScopes)) {
3049
+ return rawScopes;
3050
+ }
3051
+ // String that looks like JSON array - parse it
3052
+ if (typeof rawScopes === "string" && rawScopes.startsWith("[")) {
3053
+ try {
3054
+ const parsed = JSON.parse(rawScopes);
3055
+ if (Array.isArray(parsed)) {
3056
+ return parsed;
3057
+ }
3058
+ }
3059
+ catch {
3060
+ console.warn("[ConsentService] Failed to parse scopes JSON:", rawScopes);
3061
+ }
3062
+ }
3063
+ // Single string scope - wrap in array
3064
+ if (typeof rawScopes === "string" && rawScopes.length > 0) {
3065
+ return [rawScopes];
3066
+ }
3067
+ // Default to empty array
3068
+ return [];
3069
+ }
3070
+ /**
3071
+ * Fetch credential provider config from AgentShield
3072
+ */
3073
+ async getCredentialProviderConfig(projectId, providerName) {
3074
+ // TODO: Implement once CRED-002 V1 API is complete
3075
+ // For now, try to fetch from the config endpoint
3076
+ const agentShieldUrl = this.env.AGENTSHIELD_API_URL || DEFAULT_AGENTSHIELD_URL;
3077
+ const apiKey = this.env.AGENTSHIELD_API_KEY;
3078
+ if (!apiKey) {
3079
+ console.warn("[ConsentService] No AgentShield API key configured");
3080
+ return null;
3081
+ }
3082
+ try {
3083
+ const response = await fetch(`${agentShieldUrl}/api/v1/bouncer/projects/${projectId}/config`, {
3084
+ headers: {
3085
+ "X-API-Key": apiKey,
3086
+ "Content-Type": "application/json",
3087
+ },
3088
+ });
3089
+ if (!response.ok) {
3090
+ return null;
3091
+ }
3092
+ const data = (await response.json());
3093
+ const providers = data?.data?.providers;
3094
+ if (providers && providerName in providers) {
3095
+ const provider = providers[providerName];
3096
+ if (provider.type === "credential") {
3097
+ return provider;
3098
+ }
3099
+ }
3100
+ return null;
3101
+ }
3102
+ catch (error) {
3103
+ console.error("[ConsentService] Failed to fetch provider config:", error);
3104
+ return null;
3105
+ }
3106
+ }
3107
+ /**
3108
+ * Resolve identity via AgentShield for credential-based auth
3109
+ */
3110
+ async resolveCredentialIdentity(params) {
3111
+ const agentShieldUrl = this.env.AGENTSHIELD_API_URL || DEFAULT_AGENTSHIELD_URL;
3112
+ const apiKey = this.env.AGENTSHIELD_API_KEY;
3113
+ if (!apiKey) {
3114
+ console.warn("[ConsentService] No AgentShield API key for identity resolution");
3115
+ return { success: false };
3116
+ }
3117
+ try {
3118
+ const response = await fetch(`${agentShieldUrl}/api/v1/bouncer/identity/resolve`, {
3119
+ method: "POST",
3120
+ headers: {
3121
+ "X-API-Key": apiKey,
3122
+ "Content-Type": "application/json",
3123
+ },
3124
+ body: JSON.stringify({
3125
+ project_id: params.projectId,
3126
+ credential_result: {
3127
+ provider: params.provider,
3128
+ user_id: params.userId,
3129
+ email: params.userEmail,
3130
+ name: params.userDisplayName,
3131
+ },
3132
+ }),
3133
+ });
3134
+ if (!response.ok) {
3135
+ return { success: false };
3136
+ }
3137
+ const data = (await response.json());
3138
+ return {
3139
+ success: true,
3140
+ userDid: data?.data?.user_did,
3141
+ };
3142
+ }
3143
+ catch (error) {
3144
+ console.error("[ConsentService] Identity resolution failed:", error);
3145
+ return { success: false };
3146
+ }
3147
+ }
3148
+ /**
3149
+ * Store credential token in IdpTokenStorage with usage metadata
3150
+ */
3151
+ async storeCredentialToken(params) {
3152
+ const delegationStorage = this.env.DELEGATION_STORAGE;
3153
+ if (!delegationStorage) {
3154
+ console.warn("[ConsentService] No DELEGATION_STORAGE for credential tokens");
3155
+ return;
3156
+ }
3157
+ const oauthSecurityService = new OAuthSecurityService(delegationStorage, this.env.OAUTH_ENCRYPTION_SECRET);
3158
+ const idpTokenStorage = new IdpTokenStorage({
3159
+ storage: delegationStorage,
3160
+ oauthSecurityService,
3161
+ });
3162
+ // Default expiration: 7 days if not specified
3163
+ const expiresAt = params.expiresIn
3164
+ ? Date.now() + params.expiresIn * 1000
3165
+ : Date.now() + 7 * 24 * 60 * 60 * 1000;
3166
+ await idpTokenStorage.storeToken(params.userDid, params.provider, params.scopes, {
3167
+ access_token: params.sessionToken,
3168
+ expires_at: expiresAt,
3169
+ token_type: "credential",
3170
+ // CRED-003: Include usage metadata
3171
+ tokenUsage: params.providerConfig.tokenUsage || "cookie",
3172
+ tokenHeader: params.providerConfig.tokenHeader,
3173
+ cookieFormat: params.providerConfig.cookieFormat,
3174
+ apiHeaders: params.providerConfig.apiHeaders,
3175
+ });
3176
+ }
3177
+ /**
3178
+ * Validate CSRF token for credential form submission
3179
+ *
3180
+ * Uses the same KV-based storage pattern as OAuth state validation.
3181
+ * Token is stored when rendering the credential form and validated on submission.
3182
+ *
3183
+ * @param token - CSRF token from form submission
3184
+ * @param sessionId - Session ID the token was issued for
3185
+ * @returns True if token is valid
3186
+ */
3187
+ async validateCredentialCsrfToken(token, sessionId) {
3188
+ const delegationStorage = this.env.DELEGATION_STORAGE;
3189
+ if (!delegationStorage) {
3190
+ console.warn("[ConsentService] No DELEGATION_STORAGE for CSRF validation, skipping");
3191
+ // Return true to allow flow to continue (graceful degradation)
3192
+ // This matches OAuth behavior when storage is unavailable
3193
+ return true;
3194
+ }
3195
+ try {
3196
+ const csrfKey = STORAGE_KEYS.credentialCsrf(sessionId);
3197
+ const storedToken = await delegationStorage.get(csrfKey);
3198
+ if (!storedToken) {
3199
+ console.warn("[ConsentService] No stored CSRF token found");
3200
+ return false;
3201
+ }
3202
+ // Constant-time comparison to prevent timing attacks
3203
+ const isValid = this.constantTimeCompare(token, storedToken);
3204
+ // Delete token after use (one-time use)
3205
+ if (isValid) {
3206
+ await delegationStorage.delete(csrfKey);
3207
+ }
3208
+ return isValid;
3209
+ }
3210
+ catch (error) {
3211
+ console.error("[ConsentService] CSRF validation error:", error);
3212
+ return false;
3213
+ }
3214
+ }
3215
+ /**
3216
+ * Store CSRF token for credential form
3217
+ *
3218
+ * Called when rendering the credential form page.
3219
+ *
3220
+ * @param sessionId - Session ID to associate token with
3221
+ * @returns Generated CSRF token
3222
+ */
3223
+ async storeCredentialCsrfToken(sessionId) {
3224
+ const delegationStorage = this.env.DELEGATION_STORAGE;
3225
+ // Generate cryptographically secure token
3226
+ const tokenBytes = crypto.getRandomValues(new Uint8Array(32));
3227
+ const token = btoa(String.fromCharCode(...tokenBytes))
3228
+ .replace(/\+/g, "-")
3229
+ .replace(/\//g, "_")
3230
+ .replace(/=/g, "");
3231
+ if (!delegationStorage) {
3232
+ console.warn("[ConsentService] No DELEGATION_STORAGE for CSRF storage");
3233
+ // Return token anyway - form will submit but validation will skip
3234
+ return token;
3235
+ }
3236
+ try {
3237
+ const csrfKey = STORAGE_KEYS.credentialCsrf(sessionId);
3238
+ // 10 minute TTL matches OAuth state TTL
3239
+ await delegationStorage.put(csrfKey, token, { expirationTtl: 600 });
3240
+ console.log("[ConsentService] CSRF token stored for credential form");
3241
+ }
3242
+ catch (error) {
3243
+ console.error("[ConsentService] CSRF token storage error:", error);
3244
+ }
3245
+ return token;
3246
+ }
3247
+ /**
3248
+ * Constant-time string comparison to prevent timing attacks
3249
+ */
3250
+ constantTimeCompare(a, b) {
3251
+ if (a.length !== b.length) {
3252
+ return false;
3253
+ }
3254
+ let result = 0;
3255
+ for (let i = 0; i < a.length; i++) {
3256
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
3257
+ }
3258
+ return result === 0;
3259
+ }
2767
3260
  /**
2768
3261
  * Build full DelegationRecord format request (future format)
2769
3262
  */
@@ -2773,7 +3266,7 @@ export class ConsentService {
2773
3266
  const scopes = request.scopes ?? [];
2774
3267
  const delegationRecord = {
2775
3268
  id: crypto.randomUUID(),
2776
- issuerDid: userDid || "did:key:z6MkEphemeral", // Use ephemeral if no userDid
3269
+ issuerDid: userDid, // Phase 5: userDid is required; undefined if session is anonymous
2777
3270
  subjectDid: request.agent_did,
2778
3271
  constraints: {
2779
3272
  scopes,
@@ -2834,7 +3327,7 @@ export class ConsentService {
2834
3327
  });
2835
3328
  }
2836
3329
  else {
2837
- console.log("[ConsentService] No user_identifier (no OAuth or ephemeral DID) - delegation will proceed without it");
3330
+ console.log("[ConsentService] No user_identifier (session is anonymous) - delegation will proceed without it");
2838
3331
  }
2839
3332
  // Phase 2 VC-Only: Include credential_jwt if available
2840
3333
  if (credentialJwt) {