@kya-os/mcp-i-cloudflare 1.6.23-canary.0 → 1.6.23

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.
@@ -13,12 +13,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 { UserDidManager } from "@kya-os/mcp-i-core";
16
+ import { fetchRemoteConfig, createUnsignedVCJWT, completeVCJWT, parseVCJWT, base64urlEncodeFromBytes, base64urlDecodeToBytes, bytesToBase64, createDelegationVerifier, createDidKeyResolver, } from "@kya-os/mcp-i-core";
17
+ import { wrapDelegationAsVC } from "@kya-os/contracts";
17
18
  import { WebCryptoProvider } from "../providers/crypto";
18
19
  import { ConsentAuditService } from "./consent-audit.service";
19
20
  import { CloudflareProofGenerator } from "../proof-generator";
20
21
  import { ProofService } from "./proof.service";
21
- import { fetchRemoteConfig } from "@kya-os/mcp-i-core";
22
22
  export class ConsentService {
23
23
  configService;
24
24
  renderer;
@@ -195,9 +195,14 @@ export class ConsentService {
195
195
  * Public method to retrieve user DID from session storage or OAuth identity mapping.
196
196
  * Used by OAuth handler to get userDid when OAuth linking fails.
197
197
  *
198
+ * Phase 5: Anonymous Sessions Until OAuth
199
+ * - Returns existing DID from OAuth mapping or session storage
200
+ * - Returns null if no DID found (session is anonymous)
201
+ * - No ephemeral DID generation
202
+ *
198
203
  * @param sessionId - Session ID
199
204
  * @param oauthIdentity - Optional OAuth provider identity
200
- * @returns User DID (did:key format)
205
+ * @returns User DID (did:key format) or null if session is anonymous
201
206
  */
202
207
  async getUserDidForSession(sessionId, oauthIdentity) {
203
208
  // Handle null explicitly (from JSON parsing)
@@ -205,7 +210,7 @@ export class ConsentService {
205
210
  typeof oauthIdentity === "object" &&
206
211
  oauthIdentity.provider &&
207
212
  oauthIdentity.subject;
208
- // If OAuth identity provided, check for existing mapping first
213
+ // Priority 1: If OAuth identity provided, check for existing mapping
209
214
  if (hasOAuthIdentity && this.env.DELEGATION_STORAGE) {
210
215
  try {
211
216
  const oauthKey = STORAGE_KEYS.oauthIdentity(oauthIdentity.provider, oauthIdentity.subject);
@@ -220,97 +225,273 @@ export class ConsentService {
220
225
  }
221
226
  catch (error) {
222
227
  console.warn("[ConsentService] Failed to check OAuth mapping:", error);
223
- // Continue with ephemeral DID generation
224
228
  }
225
229
  }
226
- else if (oauthIdentity === null) {
227
- // Explicitly handle null case (no OAuth)
228
- console.log("[ConsentService] No OAuth identity provided (null), generating ephemeral DID");
230
+ // Priority 2: Check session storage for existing DID
231
+ if (this.env.DELEGATION_STORAGE) {
232
+ const sessionKey = STORAGE_KEYS.session(sessionId);
233
+ try {
234
+ const sessionData = (await this.env.DELEGATION_STORAGE.get(sessionKey, "json"));
235
+ if (sessionData?.userDid) {
236
+ return sessionData.userDid;
237
+ }
238
+ }
239
+ catch (error) {
240
+ console.warn("[ConsentService] Failed to read session cache:", error);
241
+ }
229
242
  }
230
- // Continue with existing ephemeral DID generation logic
243
+ // Phase 5: No ephemeral DID generation - session stays anonymous
244
+ // User DID will be resolved via AgentShield after OAuth completes
245
+ return null;
246
+ }
247
+ /**
248
+ * Update session with resolved identity (Phase 5)
249
+ *
250
+ * Called after AgentShield identity/resolve returns a persistent user DID.
251
+ * Stores the DID in session storage and creates OAuth mapping.
252
+ *
253
+ * @param sessionId - Session ID
254
+ * @param userDid - Persistent user DID from AgentShield
255
+ * @param oauthIdentity - OAuth identity for creating persistent mapping
256
+ */
257
+ async updateSessionWithIdentity(sessionId, userDid, oauthIdentity) {
231
258
  if (!this.env.DELEGATION_STORAGE) {
232
- // No storage - use cached UserDidManager instance for consistent DID generation
233
- if (!this.userDidManager) {
234
- this.userDidManager = new UserDidManager({
235
- crypto: new WebCryptoProvider(),
236
- });
237
- }
238
- return await this.userDidManager.getOrCreateUserDid(sessionId);
259
+ console.warn("[ConsentService] No DELEGATION_STORAGE - cannot persist session identity");
260
+ return;
239
261
  }
240
262
  const sessionKey = STORAGE_KEYS.session(sessionId);
241
- // Try session cache first
263
+ // Update session storage with resolved identity
242
264
  try {
243
- const sessionData = (await this.env.DELEGATION_STORAGE.get(sessionKey, "json"));
244
- if (sessionData?.userDid) {
245
- return sessionData.userDid;
246
- }
265
+ const existingSession = (await this.env.DELEGATION_STORAGE.get(sessionKey, "json"));
266
+ const sessionData = {
267
+ ...existingSession,
268
+ userDid,
269
+ identityState: "authenticated",
270
+ identityUpdatedAt: Date.now(),
271
+ ...(oauthIdentity && {
272
+ oauthIdentity: {
273
+ provider: oauthIdentity.provider,
274
+ subject: oauthIdentity.subject,
275
+ email: oauthIdentity.email,
276
+ },
277
+ }),
278
+ };
279
+ await this.env.DELEGATION_STORAGE.put(sessionKey, JSON.stringify(sessionData), { expirationTtl: DEFAULT_SESSION_CACHE_TTL });
280
+ console.log("[ConsentService] Session identity updated (Phase 5):", {
281
+ sessionId: sessionId.substring(0, 8) + "...",
282
+ userDid: userDid.substring(0, 20) + "...",
283
+ provider: oauthIdentity?.provider,
284
+ });
247
285
  }
248
286
  catch (error) {
249
- console.warn("[ConsentService] Failed to read session cache:", error);
287
+ console.warn("[ConsentService] Failed to update session with identity:", error);
288
+ throw error;
289
+ }
290
+ // Create OAuth → DID mapping for future lookups
291
+ if (oauthIdentity?.provider && oauthIdentity?.subject) {
292
+ try {
293
+ const oauthKey = STORAGE_KEYS.oauthIdentity(oauthIdentity.provider, oauthIdentity.subject);
294
+ await this.env.DELEGATION_STORAGE.put(oauthKey, userDid, {
295
+ expirationTtl: 90 * 24 * 60 * 60, // 90 days for persistent mapping
296
+ });
297
+ console.log("[ConsentService] Created OAuth → DID mapping:", {
298
+ provider: oauthIdentity.provider,
299
+ userDid: userDid.substring(0, 20) + "...",
300
+ });
301
+ }
302
+ catch (error) {
303
+ console.warn("[ConsentService] Failed to create OAuth mapping:", error);
304
+ // Non-fatal - session was updated successfully
305
+ }
306
+ }
307
+ }
308
+ /**
309
+ * Get key pair for a session (for VC signing)
310
+ *
311
+ * Returns the Ed25519 key pair for a session if available.
312
+ * Key pairs are generated when DIDs are created via UserDidManager.
313
+ *
314
+ * @param sessionId - Session ID
315
+ * @param oauthIdentity - Optional OAuth identity for persistent lookup
316
+ * @returns UserKeyPair or null if not available
317
+ */
318
+ async getKeyPairForSession(sessionId, oauthIdentity) {
319
+ // Ensure UserDidManager is initialized
320
+ if (!this.userDidManager) {
321
+ // Initialize with storage if available
322
+ await this.getUserDidForSession(sessionId, oauthIdentity);
250
323
  }
251
- // Generate ephemeral DID using cached UserDidManager instance
252
324
  if (!this.userDidManager) {
253
- this.userDidManager = new UserDidManager({
254
- crypto: new WebCryptoProvider(),
255
- storage: {
256
- get: async (key) => {
257
- try {
258
- const data = await this.env.DELEGATION_STORAGE.get(`userDid:${key}`, "text");
259
- return data || null;
260
- }
261
- catch {
262
- return null;
263
- }
264
- },
265
- set: async (key, value, ttl) => {
266
- await this.env.DELEGATION_STORAGE.put(`userDid:${key}`, value, {
267
- expirationTtl: ttl || DEFAULT_SESSION_CACHE_TTL,
268
- });
269
- },
270
- delete: async (key) => {
271
- await this.env.DELEGATION_STORAGE.delete(`userDid:${key}`);
272
- },
273
- // OAuth-based lookup for persistent user DID
274
- getByOAuth: async (provider, subject) => {
275
- try {
276
- const oauthKey = STORAGE_KEYS.oauthIdentity(provider, subject);
277
- const userDid = await this.env.DELEGATION_STORAGE.get(oauthKey, "text");
278
- return userDid || null;
279
- }
280
- catch {
281
- return null;
282
- }
283
- },
284
- // OAuth-based storage for persistent user DID mapping
285
- setByOAuth: async (provider, subject, did, ttl) => {
286
- try {
287
- const oauthKey = STORAGE_KEYS.oauthIdentity(provider, subject);
288
- await this.env.DELEGATION_STORAGE.put(oauthKey, did, {
289
- expirationTtl: ttl || 90 * 24 * 60 * 60, // Default 90 days for persistent mapping
290
- });
291
- }
292
- catch (error) {
293
- console.warn("[ConsentService] Failed to store OAuth mapping:", error);
294
- throw error;
295
- }
296
- },
297
- },
298
- });
325
+ console.warn("[ConsentService] UserDidManager not initialized");
326
+ return null;
327
+ }
328
+ return await this.userDidManager.getKeyPairForSession(sessionId, oauthIdentity);
329
+ }
330
+ /**
331
+ * Issue a Delegation Verifiable Credential as JWT
332
+ *
333
+ * Creates a W3C Verifiable Credential for a delegation record and encodes it as a JWT.
334
+ * The JWT is signed with the user's Ed25519 key pair.
335
+ *
336
+ * Phase 2 VC-Only: This enables verifiable delegations without full account-centric identity.
337
+ *
338
+ * @param delegation - The delegation record to wrap as a VC
339
+ * @param sessionId - Session ID for key pair retrieval
340
+ * @param oauthIdentity - Optional OAuth identity for persistent key lookup
341
+ * @returns JWT string (header.payload.signature) or null if key pair unavailable
342
+ */
343
+ async issueDelegationVC(delegation, sessionId, oauthIdentity) {
344
+ // Get the key pair for signing
345
+ const keyPair = await this.getKeyPairForSession(sessionId, oauthIdentity);
346
+ if (!keyPair) {
347
+ console.warn("[ConsentService] Cannot issue VC: no key pair available for session");
348
+ return null;
299
349
  }
300
- const userDid = await this.userDidManager.getOrCreateUserDid(sessionId, oauthIdentity);
301
- // Cache in session storage
302
350
  try {
303
- const existingSession = (await this.env.DELEGATION_STORAGE.get(sessionKey, "json"));
304
- await this.env.DELEGATION_STORAGE.put(sessionKey, JSON.stringify({
305
- ...existingSession,
306
- userDid,
307
- }), { expirationTtl: DEFAULT_SESSION_CACHE_TTL });
351
+ // Step 1: Create the unsigned VC
352
+ const unsignedVC = wrapDelegationAsVC(delegation, {
353
+ id: `urn:uuid:${delegation.id}`,
354
+ issuanceDate: new Date().toISOString(),
355
+ expirationDate: delegation.constraints.notAfter
356
+ ? new Date(delegation.constraints.notAfter).toISOString()
357
+ : undefined,
358
+ });
359
+ // Override issuer with the user's DID (not the agent's DID)
360
+ const vcWithCorrectIssuer = {
361
+ ...unsignedVC,
362
+ issuer: keyPair.did,
363
+ };
364
+ // Step 2: Create unsigned JWT parts
365
+ const { signingInput } = createUnsignedVCJWT(vcWithCorrectIssuer, { keyId: keyPair.keyId });
366
+ // Step 3: Sign the JWT with the user's private key
367
+ const crypto = new WebCryptoProvider();
368
+ const signingInputBytes = new TextEncoder().encode(signingInput);
369
+ const signatureBytes = await crypto.sign(signingInputBytes, keyPair.privateKey);
370
+ const signature = base64urlEncodeFromBytes(signatureBytes);
371
+ // Step 4: Complete the JWT
372
+ const jwt = completeVCJWT(signingInput, signature);
373
+ console.log("[ConsentService] VC issued successfully:", {
374
+ issuerDid: keyPair.did.substring(0, 20) + "...",
375
+ subjectDid: delegation.subjectDid.substring(0, 20) + "...",
376
+ delegationId: delegation.id,
377
+ jwtLength: jwt.length,
378
+ });
379
+ return jwt;
308
380
  }
309
381
  catch (error) {
310
- console.warn("[ConsentService] Failed to cache userDid in session:", error);
311
- // Non-fatal - continue with generated DID
382
+ console.error("[ConsentService] Failed to issue VC:", error);
383
+ return null;
312
384
  }
313
- return userDid;
385
+ }
386
+ /**
387
+ * Verify a VC-JWT delegation credential
388
+ *
389
+ * Verifies the JWT signature and validates the embedded VC using the
390
+ * DelegationCredentialVerifier from mcp-i-core. This implements Phase 3
391
+ * of W3C VC-based delegation verification.
392
+ *
393
+ * Verification stages:
394
+ * 1. Parse JWT format (header.payload.signature)
395
+ * 2. Resolve issuer DID (did:key) to public key
396
+ * 3. Verify JWT signature using Ed25519
397
+ * 4. Validate VC contents (expiration, schema, etc.)
398
+ *
399
+ * @param vcJwt - The VC-JWT string to verify
400
+ * @param options - Optional verification options
401
+ * @returns Verification result or null if JWT parsing fails
402
+ */
403
+ async verifyDelegationVC(vcJwt, options) {
404
+ const startTime = Date.now();
405
+ // Step 1: Parse JWT
406
+ const parsed = parseVCJWT(vcJwt);
407
+ if (!parsed) {
408
+ console.warn("[ConsentService] verifyDelegationVC: Invalid JWT format");
409
+ return {
410
+ valid: false,
411
+ reason: "Invalid JWT format",
412
+ stage: "basic",
413
+ metrics: { totalMs: Date.now() - startTime },
414
+ };
415
+ }
416
+ // Step 2: Extract VC from payload
417
+ const vc = parsed.payload.vc;
418
+ if (!vc) {
419
+ console.warn("[ConsentService] verifyDelegationVC: No VC in JWT payload");
420
+ return {
421
+ valid: false,
422
+ reason: "No VC in JWT payload",
423
+ stage: "basic",
424
+ metrics: { totalMs: Date.now() - startTime },
425
+ };
426
+ }
427
+ // Step 3: Verify JWT signature (unless skipped)
428
+ if (!options?.skipSignature) {
429
+ const issuerDid = parsed.payload.iss;
430
+ if (!issuerDid) {
431
+ return {
432
+ valid: false,
433
+ reason: "Missing issuer (iss) in JWT payload",
434
+ stage: "signature",
435
+ metrics: { totalMs: Date.now() - startTime },
436
+ };
437
+ }
438
+ // Resolve issuer DID to get public key
439
+ const resolver = createDidKeyResolver();
440
+ const didDoc = await resolver.resolve(issuerDid);
441
+ if (!didDoc?.verificationMethod?.[0]?.publicKeyJwk) {
442
+ return {
443
+ valid: false,
444
+ reason: `Cannot resolve issuer DID: ${issuerDid}`,
445
+ stage: "signature",
446
+ metrics: { totalMs: Date.now() - startTime },
447
+ };
448
+ }
449
+ // Get public key bytes from JWK
450
+ const jwk = didDoc.verificationMethod[0].publicKeyJwk;
451
+ const publicKeyBytes = base64urlDecodeToBytes(jwk.x);
452
+ const publicKeyBase64 = bytesToBase64(publicKeyBytes);
453
+ // Verify signature
454
+ const signingInputBytes = new TextEncoder().encode(parsed.signingInput);
455
+ const signatureBytes = base64urlDecodeToBytes(parsed.signature);
456
+ const crypto = new WebCryptoProvider();
457
+ const isValid = await crypto.verify(signingInputBytes, signatureBytes, publicKeyBase64);
458
+ if (!isValid) {
459
+ console.warn("[ConsentService] verifyDelegationVC: JWT signature verification failed");
460
+ return {
461
+ valid: false,
462
+ reason: "JWT signature verification failed",
463
+ stage: "signature",
464
+ metrics: { totalMs: Date.now() - startTime },
465
+ };
466
+ }
467
+ console.log("[ConsentService] verifyDelegationVC: JWT signature verified successfully");
468
+ }
469
+ // Step 4: Run basic VC validation via DelegationCredentialVerifier
470
+ // Skip signature verification since we already verified the JWT signature above
471
+ const verifier = createDelegationVerifier({
472
+ didResolver: createDidKeyResolver(),
473
+ });
474
+ const result = await verifier.verifyDelegationCredential(vc, {
475
+ skipSignature: true, // Already verified above
476
+ skipStatus: options?.skipStatus,
477
+ });
478
+ // Update metrics to include our signature verification time
479
+ if (result.metrics) {
480
+ result.metrics.totalMs = Date.now() - startTime;
481
+ }
482
+ if (result.valid) {
483
+ console.log("[ConsentService] verifyDelegationVC: VC verified successfully", {
484
+ issuer: vc.issuer,
485
+ subject: vc.credentialSubject?.id,
486
+ });
487
+ }
488
+ else {
489
+ console.warn("[ConsentService] verifyDelegationVC: VC validation failed", {
490
+ reason: result.reason,
491
+ stage: result.stage,
492
+ });
493
+ }
494
+ return result;
314
495
  }
315
496
  /**
316
497
  * Check if OAuth is required for delegation creation
@@ -629,9 +810,8 @@ export class ConsentService {
629
810
  */
630
811
  async linkOAuthToUserDid(oauthIdentity, sessionId) {
631
812
  if (!this.env.DELEGATION_STORAGE) {
632
- // No storage - can't persist mapping, return ephemeral DID
633
- console.warn("[ConsentService] No storage available for OAuth linking");
634
- return await this.getUserDidForSession(sessionId);
813
+ // Phase 5: No storage - can't persist mapping
814
+ throw new Error("[ConsentService] No storage available for OAuth linking");
635
815
  }
636
816
  const oauthKey = STORAGE_KEYS.oauthIdentity(oauthIdentity.provider, oauthIdentity.subject);
637
817
  // Check if OAuth identity already mapped
@@ -648,10 +828,14 @@ export class ConsentService {
648
828
  }
649
829
  catch (error) {
650
830
  console.warn("[ConsentService] Failed to check OAuth mapping:", error);
651
- // Continue to create new mapping
831
+ // Continue to check session
652
832
  }
653
- // Get/create User DID for session (may be ephemeral)
833
+ // Phase 5: Check for existing user DID in session
654
834
  const userDid = await this.getUserDidForSession(sessionId);
835
+ if (!userDid) {
836
+ // Phase 5: No existing DID - user must complete OAuth via AgentShield first
837
+ throw new Error("[ConsentService] No user DID found for session - OAuth identity resolution required via AgentShield");
838
+ }
655
839
  // Store OAuth identity mapping (persistent - 90 days)
656
840
  try {
657
841
  await this.env.DELEGATION_STORAGE.put(oauthKey, userDid, {
@@ -672,12 +856,68 @@ export class ConsentService {
672
856
  console.error("[ConsentService] Failed to store OAuth mapping:", error);
673
857
  // Non-fatal - continue with User DID
674
858
  }
675
- // Note: Ephemeral → persistent migration happens automatically
676
- // The ephemeral DID becomes persistent when linked to OAuth identity
677
- // Existing delegations with ephemeral DID will continue to work
678
- // New delegations will use the persistent DID
679
859
  return userDid;
680
860
  }
861
+ /**
862
+ * Cache an externally-resolved User DID (from AgentShield identity resolution)
863
+ *
864
+ * Phase 5: When AgentShield provides a persistent user_did, we cache the
865
+ * OAuth identity → userDid mapping in KV so future lookups don't require
866
+ * calling AgentShield again.
867
+ *
868
+ * @param oauthIdentity - OAuth identity from provider
869
+ * @param userDid - Persistent user DID from AgentShield
870
+ * @returns void
871
+ */
872
+ async cacheExternalUserDid(oauthIdentity, userDid) {
873
+ if (!this.env.DELEGATION_STORAGE) {
874
+ console.warn("[ConsentService] No storage available for caching external User DID");
875
+ return;
876
+ }
877
+ const oauthKey = STORAGE_KEYS.oauthIdentity(oauthIdentity.provider, oauthIdentity.subject);
878
+ // Check if mapping already exists
879
+ try {
880
+ const existingUserDid = await this.env.DELEGATION_STORAGE.get(oauthKey, "text");
881
+ if (existingUserDid) {
882
+ // Mapping already exists - verify it matches
883
+ if (existingUserDid !== userDid) {
884
+ console.warn("[ConsentService] OAuth identity already mapped to different User DID:", {
885
+ provider: oauthIdentity.provider,
886
+ subject: oauthIdentity.subject.substring(0, 20) + "...",
887
+ existingUserDid: existingUserDid.substring(0, 30) + "...",
888
+ newUserDid: userDid.substring(0, 30) + "...",
889
+ });
890
+ // Don't overwrite - AgentShield is source of truth
891
+ }
892
+ return;
893
+ }
894
+ }
895
+ catch (error) {
896
+ console.warn("[ConsentService] Failed to check existing OAuth mapping:", error);
897
+ // Continue to store new mapping
898
+ }
899
+ // Store OAuth identity → userDid mapping (persistent - 90 days)
900
+ try {
901
+ await this.env.DELEGATION_STORAGE.put(oauthKey, userDid, {
902
+ expirationTtl: 90 * 24 * 60 * 60, // 90 days
903
+ });
904
+ // Also store full OAuth identity info for reference
905
+ const oauthIdentityKey = STORAGE_KEYS.userDid(oauthIdentity.provider, oauthIdentity.subject);
906
+ await this.env.DELEGATION_STORAGE.put(oauthIdentityKey, JSON.stringify(oauthIdentity), {
907
+ expirationTtl: 90 * 24 * 60 * 60, // 90 days
908
+ });
909
+ console.log("[ConsentService] Cached external User DID from AgentShield:", {
910
+ provider: oauthIdentity.provider,
911
+ subject: oauthIdentity.subject.substring(0, 20) + "...",
912
+ userDid: userDid.substring(0, 30) + "...",
913
+ source: "agentshield_identity_resolution",
914
+ });
915
+ }
916
+ catch (error) {
917
+ console.error("[ConsentService] Failed to cache external User DID:", error);
918
+ // Non-fatal - the persistent user_did will still be used
919
+ }
920
+ }
681
921
  /**
682
922
  * Handle consent requests
683
923
  *
@@ -2117,12 +2357,13 @@ export class ConsentService {
2117
2357
  // ✅ After successful delegation creation - log audit events
2118
2358
  if (auditService && delegationResult.success) {
2119
2359
  try {
2120
- // Get userDid (may have been generated during delegation creation)
2360
+ // Get userDid (resolved via OAuth identity resolution)
2121
2361
  // getUserDidForSession can work without DELEGATION_STORAGE (uses in-memory UserDidManager)
2122
2362
  let userDid;
2123
2363
  if (approvalRequest.session_id) {
2124
2364
  try {
2125
- userDid = await this.getUserDidForSession(approvalRequest.session_id, approvalRequest.oauth_identity || undefined);
2365
+ userDid =
2366
+ (await this.getUserDidForSession(approvalRequest.session_id, approvalRequest.oauth_identity || undefined)) ?? undefined; // Phase 5: Convert null to undefined
2126
2367
  }
2127
2368
  catch (error) {
2128
2369
  console.warn("[ConsentService] Failed to get userDid for audit logging:", error);
@@ -2222,8 +2463,10 @@ export class ConsentService {
2222
2463
  });
2223
2464
  // Pass OAuth identity if available in approval request (can be null/undefined)
2224
2465
  // getUserDidForSession can work without DELEGATION_STORAGE (uses in-memory UserDidManager)
2225
- userDid = await this.getUserDidForSession(request.session_id, request.oauth_identity || undefined // Explicitly handle null as undefined
2226
- );
2466
+ // Phase 5: Returns null if no identity found (session stays anonymous)
2467
+ userDid =
2468
+ (await this.getUserDidForSession(request.session_id, request.oauth_identity || undefined // Explicitly handle null as undefined
2469
+ )) ?? undefined;
2227
2470
  console.log("[ConsentService] User DID retrieved:", {
2228
2471
  userDid: userDid?.substring(0, 20) + "...",
2229
2472
  hasUserDid: !!userDid,
@@ -2240,9 +2483,45 @@ export class ConsentService {
2240
2483
  console.log("[ConsentService] No session_id provided - skipping User DID generation");
2241
2484
  }
2242
2485
  const expiresInDays = 7; // Default to 7 days
2486
+ // Phase 2 VC-Only: Issue Delegation VC if we have a session and userDid
2487
+ let credentialJwt;
2488
+ if (request.session_id && userDid) {
2489
+ try {
2490
+ // Create a local delegation record for VC issuance
2491
+ const delegationId = crypto.randomUUID();
2492
+ const notAfter = Date.now() + expiresInDays * 24 * 60 * 60 * 1000;
2493
+ const scopes = request.scopes ?? [];
2494
+ const localDelegation = {
2495
+ id: delegationId,
2496
+ issuerDid: userDid,
2497
+ subjectDid: request.agent_did,
2498
+ vcId: `urn:uuid:${delegationId}`,
2499
+ constraints: {
2500
+ scopes,
2501
+ notAfter,
2502
+ notBefore: Date.now(),
2503
+ },
2504
+ signature: "", // Will be in JWT
2505
+ status: "active",
2506
+ createdAt: Date.now(),
2507
+ };
2508
+ const vcResult = await this.issueDelegationVC(localDelegation, request.session_id, request.oauth_identity || undefined);
2509
+ credentialJwt = vcResult ?? undefined;
2510
+ if (credentialJwt) {
2511
+ console.log("[ConsentService] VC issued for delegation:", {
2512
+ delegationId,
2513
+ jwtLength: credentialJwt.length,
2514
+ });
2515
+ }
2516
+ }
2517
+ catch (error) {
2518
+ console.warn("[ConsentService] Failed to issue VC, continuing without it:", error);
2519
+ // Non-fatal - delegation will work without VC
2520
+ }
2521
+ }
2243
2522
  // Build delegation request with error-based format detection
2244
2523
  // Try full format first, fallback to simplified format on error
2245
- const delegationRequest = await this.buildDelegationRequest(request, userDid, expiresInDays, fieldName);
2524
+ const delegationRequest = await this.buildDelegationRequest(request, userDid, expiresInDays, fieldName, credentialJwt);
2246
2525
  console.log("[ConsentService] Creating delegation:", {
2247
2526
  url: `${agentShieldUrl}${AGENTSHIELD_ENDPOINTS.DELEGATIONS_CREATE}`,
2248
2527
  agentDid: request.agent_did.substring(0, 20) + "...",
@@ -2441,7 +2720,7 @@ export class ConsentService {
2441
2720
  *
2442
2721
  * Uses Day0 config to determine field name and includes issuerDid when available.
2443
2722
  */
2444
- async buildDelegationRequest(request, userDid, expiresInDays, fieldName) {
2723
+ async buildDelegationRequest(request, userDid, expiresInDays, fieldName, credentialJwt) {
2445
2724
  const baseRequest = {
2446
2725
  agent_did: request.agent_did,
2447
2726
  scopes: request.scopes,
@@ -2473,39 +2752,45 @@ export class ConsentService {
2473
2752
  }
2474
2753
  // If we have a cached preference, use it directly
2475
2754
  if (cachedFormat === "full") {
2476
- return this.buildFullFormatRequest(request, userDid, expiresInDays);
2755
+ return this.buildFullFormatRequest(request, userDid, expiresInDays, credentialJwt);
2477
2756
  }
2478
2757
  else if (cachedFormat === "simplified") {
2479
- return this.buildSimplifiedFormatRequest(request, userDid, expiresInDays, fieldName);
2758
+ return this.buildSimplifiedFormatRequest(request, userDid, expiresInDays, fieldName, credentialJwt);
2480
2759
  }
2481
2760
  // No cache - return request that will be tried with error-based detection
2482
2761
  return {
2483
2762
  _tryFormats: true,
2484
- fullFormat: await this.buildFullFormatRequest(request, userDid, expiresInDays),
2485
- simplifiedFormat: this.buildSimplifiedFormatRequest(request, userDid, expiresInDays, fieldName),
2763
+ fullFormat: await this.buildFullFormatRequest(request, userDid, expiresInDays, credentialJwt),
2764
+ simplifiedFormat: this.buildSimplifiedFormatRequest(request, userDid, expiresInDays, fieldName, credentialJwt),
2486
2765
  };
2487
2766
  }
2488
2767
  /**
2489
2768
  * Build full DelegationRecord format request (future format)
2490
2769
  */
2491
- async buildFullFormatRequest(request, userDid, expiresInDays) {
2770
+ async buildFullFormatRequest(request, userDid, expiresInDays, credentialJwt) {
2492
2771
  const notAfter = Date.now() + expiresInDays * 24 * 60 * 60 * 1000;
2493
2772
  // Defensive check: ensure scopes is always defined (should be guaranteed by validation)
2494
2773
  const scopes = request.scopes ?? [];
2495
- return {
2496
- delegation: {
2497
- id: crypto.randomUUID(),
2498
- issuerDid: userDid || "did:key:z6MkEphemeral", // Use ephemeral if no userDid
2499
- subjectDid: request.agent_did,
2500
- constraints: {
2501
- scopes,
2502
- notAfter,
2503
- notBefore: Date.now(),
2504
- },
2505
- status: "active",
2506
- createdAt: Date.now(),
2774
+ const delegationRecord = {
2775
+ id: crypto.randomUUID(),
2776
+ issuerDid: userDid || "did:key:z6MkEphemeral", // Use ephemeral if no userDid
2777
+ subjectDid: request.agent_did,
2778
+ constraints: {
2779
+ scopes,
2780
+ notAfter,
2781
+ notBefore: Date.now(),
2507
2782
  },
2783
+ status: "active",
2784
+ createdAt: Date.now(),
2785
+ };
2786
+ const result = {
2787
+ delegation: delegationRecord,
2508
2788
  };
2789
+ // Phase 2 VC-Only: Include credential_jwt if available
2790
+ if (credentialJwt) {
2791
+ result.credential_jwt = credentialJwt;
2792
+ }
2793
+ return result;
2509
2794
  }
2510
2795
  /**
2511
2796
  * Check if a string is a valid UUID
@@ -2531,7 +2816,7 @@ export class ConsentService {
2531
2816
  *
2532
2817
  * Including these fields will cause validation errors (400 Bad Request).
2533
2818
  */
2534
- buildSimplifiedFormatRequest(request, userDid, expiresInDays, fieldName) {
2819
+ buildSimplifiedFormatRequest(request, userDid, expiresInDays, fieldName, credentialJwt) {
2535
2820
  // Build request with ONLY fields that are in AgentShield's schema
2536
2821
  // Defensive check: ensure scopes is always defined (should be guaranteed by validation)
2537
2822
  const scopes = request.scopes ?? [];
@@ -2551,6 +2836,11 @@ export class ConsentService {
2551
2836
  else {
2552
2837
  console.log("[ConsentService] No user_identifier (no OAuth or ephemeral DID) - delegation will proceed without it");
2553
2838
  }
2839
+ // Phase 2 VC-Only: Include credential_jwt if available
2840
+ if (credentialJwt) {
2841
+ simplifiedRequest.credential_jwt = credentialJwt;
2842
+ console.log("[ConsentService] Including credential_jwt in delegation request");
2843
+ }
2554
2844
  // AgentShield API only accepts "custom_fields", not "metadata"
2555
2845
  // Always use "custom_fields" regardless of Day0 config
2556
2846
  if (userDid) {