@kya-os/mcp-i-cloudflare 1.7.11 → 1.7.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.d.ts +13 -2
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +83 -153
- package/dist/agent.js.map +1 -1
- package/dist/runtime/oauth-handler.d.ts +23 -0
- package/dist/runtime/oauth-handler.d.ts.map +1 -1
- package/dist/runtime/oauth-handler.js +167 -2
- package/dist/runtime/oauth-handler.js.map +1 -1
- package/dist/services/consent-audit.service.d.ts +55 -0
- package/dist/services/consent-audit.service.d.ts.map +1 -1
- package/dist/services/consent-audit.service.js +116 -0
- package/dist/services/consent-audit.service.js.map +1 -1
- package/dist/services/consent-templates/base/base-template.d.ts.map +1 -1
- package/dist/services/consent-templates/base/base-template.js +10 -1
- package/dist/services/consent-templates/base/base-template.js.map +1 -1
- package/dist/services/consent-templates/modes/credentials.template.d.ts.map +1 -1
- package/dist/services/consent-templates/modes/credentials.template.js +4 -1
- package/dist/services/consent-templates/modes/credentials.template.js.map +1 -1
- package/dist/services/consent-templates/registry.d.ts +6 -0
- package/dist/services/consent-templates/registry.d.ts.map +1 -1
- package/dist/services/consent-templates/registry.js +11 -17
- package/dist/services/consent-templates/registry.js.map +1 -1
- package/dist/services/consent-templates/template-renderer.d.ts +3 -0
- package/dist/services/consent-templates/template-renderer.d.ts.map +1 -1
- package/dist/services/consent-templates/template-renderer.js +9 -2
- package/dist/services/consent-templates/template-renderer.js.map +1 -1
- package/dist/services/consent-templates/types.d.ts +8 -0
- package/dist/services/consent-templates/types.d.ts.map +1 -1
- package/dist/services/consent.service.d.ts.map +1 -1
- package/dist/services/consent.service.js +458 -256
- package/dist/services/consent.service.js.map +1 -1
- package/dist/services/credential-auth.handler.d.ts.map +1 -1
- package/dist/services/credential-auth.handler.js +8 -0
- package/dist/services/credential-auth.handler.js.map +1 -1
- package/package.json +5 -5
|
@@ -13,7 +13,7 @@ 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, generateDidKeyFromBase64, createDelegationVerifier, createDidKeyResolver, } from "@kya-os/mcp-i-core";
|
|
16
|
+
import { fetchRemoteConfig, createUnsignedVCJWT, completeVCJWT, parseVCJWT, base64urlEncodeFromBytes, base64urlDecodeToBytes, bytesToBase64, generateDidKeyFromBase64, createDelegationVerifier, createDidKeyResolver, logger, } 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";
|
|
@@ -76,7 +76,7 @@ export class ConsentService {
|
|
|
76
76
|
await this.auditInitPromise;
|
|
77
77
|
}
|
|
78
78
|
catch (error) {
|
|
79
|
-
|
|
79
|
+
logger.warn("[ConsentService] Audit service initialization failed:", error);
|
|
80
80
|
// Don't throw - audit failures shouldn't break consent flow
|
|
81
81
|
}
|
|
82
82
|
return this.auditService;
|
|
@@ -95,7 +95,7 @@ export class ConsentService {
|
|
|
95
95
|
// ✅ CRITICAL: Fetch config from remote API
|
|
96
96
|
const config = await this.getConfigFromRemoteAPI(projectId);
|
|
97
97
|
if (!config?.proofing?.enabled) {
|
|
98
|
-
|
|
98
|
+
logger.debug("[ConsentService] Proofing not enabled in remote config");
|
|
99
99
|
return; // Proofing not enabled
|
|
100
100
|
}
|
|
101
101
|
// Get identity (async - requires runtime to be initialized)
|
|
@@ -105,16 +105,16 @@ export class ConsentService {
|
|
|
105
105
|
// Get audit logger
|
|
106
106
|
const auditLogger = this.runtime.getAuditLogger();
|
|
107
107
|
if (!auditLogger) {
|
|
108
|
-
|
|
108
|
+
logger.warn("[ConsentService] AuditLogger not available");
|
|
109
109
|
return;
|
|
110
110
|
}
|
|
111
111
|
// Create audit service with fetched config
|
|
112
112
|
this.auditService = new ConsentAuditService(new ProofService(config, this.runtime), auditLogger, proofGenerator, config, // ✅ Config fetched from remote API
|
|
113
113
|
this.runtime);
|
|
114
|
-
|
|
114
|
+
logger.debug("[ConsentService] Audit service initialized successfully");
|
|
115
115
|
}
|
|
116
116
|
catch (error) {
|
|
117
|
-
|
|
117
|
+
logger.debug("[ConsentService] Failed to initialize audit service:", error);
|
|
118
118
|
// Don't throw - audit failures shouldn't break consent flow
|
|
119
119
|
}
|
|
120
120
|
}
|
|
@@ -131,7 +131,7 @@ export class ConsentService {
|
|
|
131
131
|
*/
|
|
132
132
|
async getConfigFromRemoteAPI(projectId) {
|
|
133
133
|
if (!this.env.AGENTSHIELD_API_KEY) {
|
|
134
|
-
|
|
134
|
+
logger.warn("[ConsentService] No API key for runtime config fetch");
|
|
135
135
|
return undefined;
|
|
136
136
|
}
|
|
137
137
|
try {
|
|
@@ -166,11 +166,11 @@ export class ConsentService {
|
|
|
166
166
|
const runtimeIdentity = await this.runtime?.getIdentity();
|
|
167
167
|
if (runtimeIdentity?.did) {
|
|
168
168
|
identityConfig.serverDid = runtimeIdentity.did;
|
|
169
|
-
|
|
169
|
+
logger.debug("[ConsentService] Populated serverDid from runtime identity");
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
172
|
catch (error) {
|
|
173
|
-
|
|
173
|
+
logger.warn("[ConsentService] Failed to get runtime identity for serverDid:", error);
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
176
|
}
|
|
@@ -179,7 +179,7 @@ export class ConsentService {
|
|
|
179
179
|
return config;
|
|
180
180
|
}
|
|
181
181
|
catch (error) {
|
|
182
|
-
|
|
182
|
+
logger.warn("[ConsentService] Error fetching runtime config:", error);
|
|
183
183
|
return undefined;
|
|
184
184
|
}
|
|
185
185
|
}
|
|
@@ -210,7 +210,7 @@ export class ConsentService {
|
|
|
210
210
|
const oauthKey = STORAGE_KEYS.oauthIdentity(oauthIdentity.provider, oauthIdentity.subject);
|
|
211
211
|
const mappedUserDid = await this.env.DELEGATION_STORAGE.get(oauthKey, "text");
|
|
212
212
|
if (mappedUserDid) {
|
|
213
|
-
|
|
213
|
+
logger.debug("[ConsentService] Found persistent User DID from OAuth mapping:", {
|
|
214
214
|
provider: oauthIdentity.provider,
|
|
215
215
|
userDid: mappedUserDid.substring(0, 20) + "...",
|
|
216
216
|
});
|
|
@@ -218,7 +218,7 @@ export class ConsentService {
|
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
220
|
catch (error) {
|
|
221
|
-
|
|
221
|
+
logger.warn("[ConsentService] Failed to check OAuth mapping:", error);
|
|
222
222
|
}
|
|
223
223
|
}
|
|
224
224
|
// Priority 2: Check session storage for existing DID
|
|
@@ -231,7 +231,7 @@ export class ConsentService {
|
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
233
|
catch (error) {
|
|
234
|
-
|
|
234
|
+
logger.warn("[ConsentService] Failed to read session cache:", error);
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
237
|
// Phase 5: No ephemeral DID generation - session stays anonymous
|
|
@@ -250,7 +250,7 @@ export class ConsentService {
|
|
|
250
250
|
*/
|
|
251
251
|
async updateSessionWithIdentity(sessionId, userDid, oauthIdentity) {
|
|
252
252
|
if (!this.env.DELEGATION_STORAGE) {
|
|
253
|
-
|
|
253
|
+
logger.warn("[ConsentService] No DELEGATION_STORAGE - cannot persist session identity");
|
|
254
254
|
return;
|
|
255
255
|
}
|
|
256
256
|
const sessionKey = STORAGE_KEYS.session(sessionId);
|
|
@@ -270,7 +270,7 @@ export class ConsentService {
|
|
|
270
270
|
await this.env.DELEGATION_STORAGE.put(userAgentKey, delegationToken, {
|
|
271
271
|
expirationTtl: delegationTtl,
|
|
272
272
|
});
|
|
273
|
-
|
|
273
|
+
logger.debug("[ConsentService] 🔄 DELEGATION UPGRADE: Migrated consent-only delegation to user+agent scoped:", {
|
|
274
274
|
sessionId: sessionId.substring(0, 8) + "...",
|
|
275
275
|
userDid: userDid.substring(0, 20) + "...",
|
|
276
276
|
agentDid: agentDid.substring(0, 20) + "...",
|
|
@@ -294,7 +294,7 @@ export class ConsentService {
|
|
|
294
294
|
}),
|
|
295
295
|
};
|
|
296
296
|
await this.env.DELEGATION_STORAGE.put(sessionKey, JSON.stringify(sessionData), { expirationTtl: DEFAULT_SESSION_CACHE_TTL });
|
|
297
|
-
|
|
297
|
+
logger.debug("[ConsentService] Session identity updated:", {
|
|
298
298
|
sessionId: sessionId.substring(0, 8) + "...",
|
|
299
299
|
userDid: userDid.substring(0, 20) + "...",
|
|
300
300
|
provider: oauthIdentity?.provider,
|
|
@@ -302,7 +302,7 @@ export class ConsentService {
|
|
|
302
302
|
});
|
|
303
303
|
}
|
|
304
304
|
catch (error) {
|
|
305
|
-
|
|
305
|
+
logger.warn("[ConsentService] Failed to update session with identity:", error);
|
|
306
306
|
throw error;
|
|
307
307
|
}
|
|
308
308
|
// Create OAuth → DID mapping for future lookups
|
|
@@ -312,13 +312,13 @@ export class ConsentService {
|
|
|
312
312
|
await this.env.DELEGATION_STORAGE.put(oauthKey, userDid, {
|
|
313
313
|
expirationTtl: 90 * 24 * 60 * 60, // 90 days for persistent mapping
|
|
314
314
|
});
|
|
315
|
-
|
|
315
|
+
logger.debug("[ConsentService] Created OAuth → DID mapping:", {
|
|
316
316
|
provider: oauthIdentity.provider,
|
|
317
317
|
userDid: userDid.substring(0, 20) + "...",
|
|
318
318
|
});
|
|
319
319
|
}
|
|
320
320
|
catch (error) {
|
|
321
|
-
|
|
321
|
+
logger.warn("[ConsentService] Failed to create OAuth mapping:", error);
|
|
322
322
|
// Non-fatal - session was updated successfully
|
|
323
323
|
}
|
|
324
324
|
}
|
|
@@ -343,12 +343,12 @@ export class ConsentService {
|
|
|
343
343
|
const key = STORAGE_KEYS.userKeyPair(oauthIdentity.provider, oauthIdentity.subject);
|
|
344
344
|
const stored = (await this.env.DELEGATION_STORAGE.get(key, "json"));
|
|
345
345
|
if (stored) {
|
|
346
|
-
|
|
346
|
+
logger.debug("[ConsentService] Found key pair in OAuth KV storage");
|
|
347
347
|
return stored;
|
|
348
348
|
}
|
|
349
349
|
}
|
|
350
350
|
catch (error) {
|
|
351
|
-
|
|
351
|
+
logger.warn("[ConsentService] KV key pair lookup failed:", error);
|
|
352
352
|
}
|
|
353
353
|
}
|
|
354
354
|
// Priority 2: Check session storage for ephemeral key pair (consent-only flow)
|
|
@@ -358,12 +358,12 @@ export class ConsentService {
|
|
|
358
358
|
const sessionKey = STORAGE_KEYS.session(sessionId);
|
|
359
359
|
const sessionData = (await this.env.DELEGATION_STORAGE.get(sessionKey, "json"));
|
|
360
360
|
if (sessionData?.keyPair) {
|
|
361
|
-
|
|
361
|
+
logger.debug("[ConsentService] Found ephemeral key pair in session storage (consent-only VC signing)");
|
|
362
362
|
return sessionData.keyPair;
|
|
363
363
|
}
|
|
364
364
|
}
|
|
365
365
|
catch (error) {
|
|
366
|
-
|
|
366
|
+
logger.warn("[ConsentService] Session key pair lookup failed:", error);
|
|
367
367
|
}
|
|
368
368
|
}
|
|
369
369
|
// Priority 3: Try UserDidManager (legacy path, may not be initialized)
|
|
@@ -378,7 +378,7 @@ export class ConsentService {
|
|
|
378
378
|
}
|
|
379
379
|
}
|
|
380
380
|
// No key pair found
|
|
381
|
-
|
|
381
|
+
logger.warn("[ConsentService] No key pair found for session - user cannot sign VCs");
|
|
382
382
|
return null;
|
|
383
383
|
}
|
|
384
384
|
/**
|
|
@@ -400,7 +400,7 @@ export class ConsentService {
|
|
|
400
400
|
// 1. Try existing lookup first
|
|
401
401
|
const existingKeyPair = await this.getKeyPairForSession(sessionId, oauthIdentity);
|
|
402
402
|
if (existingKeyPair) {
|
|
403
|
-
|
|
403
|
+
logger.debug("[ConsentService] Found existing key pair for OAuth identity");
|
|
404
404
|
return existingKeyPair;
|
|
405
405
|
}
|
|
406
406
|
// 2. Check KV storage directly by OAuth identity
|
|
@@ -411,16 +411,16 @@ export class ConsentService {
|
|
|
411
411
|
const key = STORAGE_KEYS.userKeyPair(oauthIdentity.provider, oauthIdentity.subject);
|
|
412
412
|
const stored = (await this.env.DELEGATION_STORAGE.get(key, "json"));
|
|
413
413
|
if (stored) {
|
|
414
|
-
|
|
414
|
+
logger.debug("[ConsentService] Found key pair in KV storage");
|
|
415
415
|
return stored;
|
|
416
416
|
}
|
|
417
417
|
}
|
|
418
418
|
catch (error) {
|
|
419
|
-
|
|
419
|
+
logger.warn("[ConsentService] KV key pair lookup failed:", error);
|
|
420
420
|
}
|
|
421
421
|
}
|
|
422
422
|
// 3. Generate new key pair
|
|
423
|
-
|
|
423
|
+
logger.debug("[ConsentService] Generating new key pair for OAuth identity");
|
|
424
424
|
try {
|
|
425
425
|
const crypto = new WebCryptoProvider();
|
|
426
426
|
const rawKeyPair = await crypto.generateKeyPair();
|
|
@@ -443,20 +443,20 @@ export class ConsentService {
|
|
|
443
443
|
await this.env.DELEGATION_STORAGE.put(key, JSON.stringify(keyPair), {
|
|
444
444
|
expirationTtl: KEY_PAIR_TTL_SECONDS,
|
|
445
445
|
});
|
|
446
|
-
|
|
446
|
+
logger.debug("[ConsentService] Key pair persisted to KV storage");
|
|
447
447
|
}
|
|
448
448
|
catch (persistError) {
|
|
449
|
-
|
|
449
|
+
logger.warn("[ConsentService] KV key pair persistence failed (non-fatal):", persistError);
|
|
450
450
|
}
|
|
451
451
|
}
|
|
452
|
-
|
|
452
|
+
logger.debug("[ConsentService] Key pair generated:", {
|
|
453
453
|
did: did.substring(0, 30) + "...",
|
|
454
454
|
keyId,
|
|
455
455
|
});
|
|
456
456
|
return keyPair;
|
|
457
457
|
}
|
|
458
458
|
catch (error) {
|
|
459
|
-
|
|
459
|
+
logger.error("[ConsentService] Key pair generation failed:", error);
|
|
460
460
|
return null;
|
|
461
461
|
}
|
|
462
462
|
}
|
|
@@ -489,7 +489,7 @@ export class ConsentService {
|
|
|
489
489
|
const sessionKey = STORAGE_KEYS.session(sessionId);
|
|
490
490
|
const existingSession = (await this.env.DELEGATION_STORAGE.get(sessionKey, "json"));
|
|
491
491
|
if (existingSession?.userDid) {
|
|
492
|
-
|
|
492
|
+
logger.debug("[ConsentService] Session already has userDid, reusing:", {
|
|
493
493
|
sessionId: sessionId.substring(0, 20) + "...",
|
|
494
494
|
userDid: existingSession.userDid.substring(0, 20) + "...",
|
|
495
495
|
hasKeyPair: !!existingSession.keyPair,
|
|
@@ -498,11 +498,11 @@ export class ConsentService {
|
|
|
498
498
|
}
|
|
499
499
|
}
|
|
500
500
|
catch (error) {
|
|
501
|
-
|
|
501
|
+
logger.warn("[ConsentService] Failed to check existing session userDid:", error);
|
|
502
502
|
}
|
|
503
503
|
}
|
|
504
504
|
// Generate new ephemeral keypair - THIS TIME WE STORE IT!
|
|
505
|
-
|
|
505
|
+
logger.debug("[ConsentService] Generating ephemeral key pair for consent-only VC signing");
|
|
506
506
|
try {
|
|
507
507
|
const crypto = new WebCryptoProvider();
|
|
508
508
|
const rawKeyPair = await crypto.generateKeyPair();
|
|
@@ -528,13 +528,13 @@ export class ConsentService {
|
|
|
528
528
|
keyPair, // Store the full key pair for VC signing!
|
|
529
529
|
identityState: "ephemeral",
|
|
530
530
|
}), { expirationTtl: DEFAULT_SESSION_CACHE_TTL });
|
|
531
|
-
|
|
531
|
+
logger.debug("[ConsentService] ✅ Ephemeral key pair stored in session for VC signing");
|
|
532
532
|
}
|
|
533
533
|
catch (storeError) {
|
|
534
|
-
|
|
534
|
+
logger.warn("[ConsentService] Failed to store ephemeral key pair (VC signing may fail):", storeError);
|
|
535
535
|
}
|
|
536
536
|
}
|
|
537
|
-
|
|
537
|
+
logger.info("[ConsentService] ✅ Ephemeral key pair generated:", {
|
|
538
538
|
sessionId: sessionId.substring(0, 20) + "...",
|
|
539
539
|
userDid: ephemeralUserDid.substring(0, 30) + "...",
|
|
540
540
|
hasPrivateKey: true,
|
|
@@ -543,7 +543,7 @@ export class ConsentService {
|
|
|
543
543
|
return ephemeralUserDid;
|
|
544
544
|
}
|
|
545
545
|
catch (error) {
|
|
546
|
-
|
|
546
|
+
logger.debug("[ConsentService] Failed to generate ephemeral key pair:", error);
|
|
547
547
|
return null;
|
|
548
548
|
}
|
|
549
549
|
}
|
|
@@ -567,7 +567,7 @@ export class ConsentService {
|
|
|
567
567
|
const keyPair = providedKeyPair ??
|
|
568
568
|
(await this.getKeyPairForSession(sessionId, oauthIdentity));
|
|
569
569
|
if (!keyPair) {
|
|
570
|
-
|
|
570
|
+
logger.warn("[ConsentService] Cannot issue VC: no key pair available for session");
|
|
571
571
|
return null;
|
|
572
572
|
}
|
|
573
573
|
try {
|
|
@@ -592,7 +592,7 @@ export class ConsentService {
|
|
|
592
592
|
const signature = base64urlEncodeFromBytes(signatureBytes);
|
|
593
593
|
// Step 4: Complete the JWT
|
|
594
594
|
const jwt = completeVCJWT(signingInput, signature);
|
|
595
|
-
|
|
595
|
+
logger.debug("[ConsentService] VC issued successfully:", {
|
|
596
596
|
issuerDid: keyPair.did.substring(0, 20) + "...",
|
|
597
597
|
subjectDid: delegation.subjectDid.substring(0, 20) + "...",
|
|
598
598
|
delegationId: delegation.id,
|
|
@@ -601,7 +601,7 @@ export class ConsentService {
|
|
|
601
601
|
return jwt;
|
|
602
602
|
}
|
|
603
603
|
catch (error) {
|
|
604
|
-
|
|
604
|
+
logger.error("[ConsentService] Failed to issue VC:", error);
|
|
605
605
|
return null;
|
|
606
606
|
}
|
|
607
607
|
}
|
|
@@ -627,7 +627,7 @@ export class ConsentService {
|
|
|
627
627
|
// Step 1: Parse JWT
|
|
628
628
|
const parsed = parseVCJWT(vcJwt);
|
|
629
629
|
if (!parsed) {
|
|
630
|
-
|
|
630
|
+
logger.warn("[ConsentService] verifyDelegationVC: Invalid JWT format");
|
|
631
631
|
return {
|
|
632
632
|
valid: false,
|
|
633
633
|
reason: "Invalid JWT format",
|
|
@@ -638,7 +638,7 @@ export class ConsentService {
|
|
|
638
638
|
// Step 2: Extract VC from payload
|
|
639
639
|
const vc = parsed.payload.vc;
|
|
640
640
|
if (!vc) {
|
|
641
|
-
|
|
641
|
+
logger.warn("[ConsentService] verifyDelegationVC: No VC in JWT payload");
|
|
642
642
|
return {
|
|
643
643
|
valid: false,
|
|
644
644
|
reason: "No VC in JWT payload",
|
|
@@ -678,7 +678,7 @@ export class ConsentService {
|
|
|
678
678
|
const crypto = new WebCryptoProvider();
|
|
679
679
|
const isValid = await crypto.verify(signingInputBytes, signatureBytes, publicKeyBase64);
|
|
680
680
|
if (!isValid) {
|
|
681
|
-
|
|
681
|
+
logger.warn("[ConsentService] verifyDelegationVC: JWT signature verification failed");
|
|
682
682
|
return {
|
|
683
683
|
valid: false,
|
|
684
684
|
reason: "JWT signature verification failed",
|
|
@@ -686,7 +686,7 @@ export class ConsentService {
|
|
|
686
686
|
metrics: { totalMs: Date.now() - startTime },
|
|
687
687
|
};
|
|
688
688
|
}
|
|
689
|
-
|
|
689
|
+
logger.debug("[ConsentService] verifyDelegationVC: JWT signature verified successfully");
|
|
690
690
|
}
|
|
691
691
|
// Step 4: Run basic VC validation via DelegationCredentialVerifier
|
|
692
692
|
// Skip signature verification since we already verified the JWT signature above
|
|
@@ -702,13 +702,13 @@ export class ConsentService {
|
|
|
702
702
|
result.metrics.totalMs = Date.now() - startTime;
|
|
703
703
|
}
|
|
704
704
|
if (result.valid) {
|
|
705
|
-
|
|
705
|
+
logger.debug("[ConsentService] verifyDelegationVC: VC verified successfully", {
|
|
706
706
|
issuer: vc.issuer,
|
|
707
707
|
subject: vc.credentialSubject?.id,
|
|
708
708
|
});
|
|
709
709
|
}
|
|
710
710
|
else {
|
|
711
|
-
|
|
711
|
+
logger.warn("[ConsentService] verifyDelegationVC: VC validation failed", {
|
|
712
712
|
reason: result.reason,
|
|
713
713
|
stage: result.stage,
|
|
714
714
|
});
|
|
@@ -732,7 +732,7 @@ export class ConsentService {
|
|
|
732
732
|
// Priority 0: Check for consent-only mode (authorization.type === 'none')
|
|
733
733
|
// Consent-only means delegation is required but NO OAuth provider is needed
|
|
734
734
|
if (toolProtection?.authorization?.type === "none") {
|
|
735
|
-
|
|
735
|
+
logger.debug("[ConsentService] Tool is consent-only mode (authorization.type=none), OAuth NOT required", {
|
|
736
736
|
projectId,
|
|
737
737
|
requiresDelegation: toolProtection.requiresDelegation,
|
|
738
738
|
});
|
|
@@ -755,7 +755,7 @@ export class ConsentService {
|
|
|
755
755
|
try {
|
|
756
756
|
const resolvedProvider = await this.providerResolver.resolveProvider(toolProtection, projectId);
|
|
757
757
|
if (resolvedProvider) {
|
|
758
|
-
|
|
758
|
+
logger.debug("[ConsentService] OAuth required: ProviderResolver resolved provider", {
|
|
759
759
|
projectId,
|
|
760
760
|
provider: resolvedProvider,
|
|
761
761
|
toolRequiresDelegation: toolProtection.requiresDelegation,
|
|
@@ -767,11 +767,22 @@ export class ConsentService {
|
|
|
767
767
|
catch (error) {
|
|
768
768
|
// Check if this is ConsentOnlyModeError - not a real error
|
|
769
769
|
if (error instanceof Error && error.name === "ConsentOnlyModeError") {
|
|
770
|
-
|
|
770
|
+
logger.debug("[ConsentService] Tool is consent-only mode (ConsentOnlyModeError), OAuth NOT required", { projectId });
|
|
771
|
+
return false;
|
|
772
|
+
}
|
|
773
|
+
// Check for CredentialAuthModeError - credential auth does NOT require OAuth
|
|
774
|
+
// Use multiple detection methods to handle bundled code where class names may be mangled
|
|
775
|
+
const isCredentialAuthError = error instanceof Error &&
|
|
776
|
+
(error.name === "CredentialAuthModeError" ||
|
|
777
|
+
error.constructor?.name === "CredentialAuthModeError" ||
|
|
778
|
+
("provider" in error &&
|
|
779
|
+
typeof error.provider === "string"));
|
|
780
|
+
if (isCredentialAuthError) {
|
|
781
|
+
logger.debug("[ConsentService] Tool uses credential auth (CredentialAuthModeError), OAuth NOT required", { projectId });
|
|
771
782
|
return false;
|
|
772
783
|
}
|
|
773
784
|
// ProviderResolver failed - log but continue to fallback check
|
|
774
|
-
|
|
785
|
+
logger.warn("[ConsentService] ProviderResolver failed to resolve provider, falling back to project config check:", {
|
|
775
786
|
projectId,
|
|
776
787
|
error: error instanceof Error ? error.message : String(error),
|
|
777
788
|
});
|
|
@@ -805,7 +816,7 @@ export class ConsentService {
|
|
|
805
816
|
return false;
|
|
806
817
|
}
|
|
807
818
|
catch (error) {
|
|
808
|
-
|
|
819
|
+
logger.warn("[ConsentService] Failed to check OAuth requirement:", error);
|
|
809
820
|
// On error, default to not requiring OAuth (backward compatibility)
|
|
810
821
|
return false;
|
|
811
822
|
}
|
|
@@ -846,7 +857,7 @@ export class ConsentService {
|
|
|
846
857
|
codeChallenge = pkceChallenge.challenge;
|
|
847
858
|
}
|
|
848
859
|
catch (error) {
|
|
849
|
-
|
|
860
|
+
logger.warn("[ConsentService] Failed to generate PKCE challenge:", error);
|
|
850
861
|
}
|
|
851
862
|
}
|
|
852
863
|
// Build state data with required fields
|
|
@@ -881,7 +892,7 @@ export class ConsentService {
|
|
|
881
892
|
// Store state data securely in KV (10 minute TTL for OAuth flow)
|
|
882
893
|
await oauthSecurityService.storeOAuthState(stateValue, stateData, 600);
|
|
883
894
|
stateParam = stateValue;
|
|
884
|
-
|
|
895
|
+
logger.debug("[ConsentService] 🔒 SECURITY EVENT: OAuth state stored securely:", {
|
|
885
896
|
projectId,
|
|
886
897
|
agentDid: agentDid.substring(0, 20) + "...",
|
|
887
898
|
sessionId: sessionId.substring(0, 20) + "...",
|
|
@@ -894,7 +905,7 @@ export class ConsentService {
|
|
|
894
905
|
else {
|
|
895
906
|
// Fallback: Encode state as base64 (less secure, but backward compatible)
|
|
896
907
|
if (!oauthSecurityService) {
|
|
897
|
-
|
|
908
|
+
logger.warn("[ConsentService] ⚠️ SECURITY WARNING: OAuthSecurityService not provided, using insecure state encoding");
|
|
898
909
|
}
|
|
899
910
|
stateParam = btoa(JSON.stringify(stateData));
|
|
900
911
|
}
|
|
@@ -904,20 +915,20 @@ export class ConsentService {
|
|
|
904
915
|
if (provider && this.providerRegistry) {
|
|
905
916
|
// Lazy load providers if registry is empty (first request for this project)
|
|
906
917
|
if (this.providerRegistry.getProviderNames().length === 0) {
|
|
907
|
-
|
|
918
|
+
logger.debug("[ConsentService] Loading providers from AgentShield", {
|
|
908
919
|
projectId,
|
|
909
920
|
provider,
|
|
910
921
|
});
|
|
911
922
|
try {
|
|
912
923
|
await this.providerRegistry.loadFromAgentShield(projectId);
|
|
913
|
-
|
|
924
|
+
logger.debug("[ConsentService] Providers loaded successfully", {
|
|
914
925
|
projectId,
|
|
915
926
|
providers: this.providerRegistry.getProviderNames(),
|
|
916
927
|
configuredProvider: this.providerRegistry.getConfiguredProvider(),
|
|
917
928
|
});
|
|
918
929
|
}
|
|
919
930
|
catch (loadError) {
|
|
920
|
-
|
|
931
|
+
logger.warn("[ConsentService] Failed to load providers from AgentShield", {
|
|
921
932
|
projectId,
|
|
922
933
|
error: loadError instanceof Error
|
|
923
934
|
? loadError.message
|
|
@@ -929,7 +940,7 @@ export class ConsentService {
|
|
|
929
940
|
providerConfig = this.providerRegistry.getProvider(provider);
|
|
930
941
|
}
|
|
931
942
|
// Diagnostic logging to track race condition and OAuth mode selection
|
|
932
|
-
|
|
943
|
+
logger.debug("[ConsentService] OAuth mode decision:", {
|
|
933
944
|
provider: provider || "none",
|
|
934
945
|
hasProviderRegistry: !!this.providerRegistry,
|
|
935
946
|
hasProviderConfig: !!providerConfig,
|
|
@@ -968,7 +979,7 @@ export class ConsentService {
|
|
|
968
979
|
!providerConfig.proxyMode) {
|
|
969
980
|
// Use providerConfig.clientId from AgentShield dashboard config
|
|
970
981
|
const oauthClientId = providerConfig.clientId || projectId;
|
|
971
|
-
|
|
982
|
+
logger.debug("[ConsentService] Using direct OAuth mode (PKCE)", {
|
|
972
983
|
provider: provider || "unknown",
|
|
973
984
|
authorizationUrl: providerConfig.authorizationUrl,
|
|
974
985
|
supportsPKCE: providerConfig.supportsPKCE,
|
|
@@ -1119,7 +1130,7 @@ export class ConsentService {
|
|
|
1119
1130
|
try {
|
|
1120
1131
|
const existingUserDid = await this.env.DELEGATION_STORAGE.get(oauthKey, "text");
|
|
1121
1132
|
if (existingUserDid) {
|
|
1122
|
-
|
|
1133
|
+
logger.debug("[ConsentService] OAuth identity already mapped:", {
|
|
1123
1134
|
provider: oauthIdentity.provider,
|
|
1124
1135
|
subject: oauthIdentity.subject.substring(0, 20) + "...",
|
|
1125
1136
|
userDid: existingUserDid.substring(0, 20) + "...",
|
|
@@ -1128,7 +1139,7 @@ export class ConsentService {
|
|
|
1128
1139
|
}
|
|
1129
1140
|
}
|
|
1130
1141
|
catch (error) {
|
|
1131
|
-
|
|
1142
|
+
logger.warn("[ConsentService] Failed to check OAuth mapping:", error);
|
|
1132
1143
|
// Continue to check session
|
|
1133
1144
|
}
|
|
1134
1145
|
// Phase 5: Check for existing user DID in session
|
|
@@ -1147,14 +1158,14 @@ export class ConsentService {
|
|
|
1147
1158
|
await this.env.DELEGATION_STORAGE.put(oauthIdentityKey, JSON.stringify(oauthIdentity), {
|
|
1148
1159
|
expirationTtl: 90 * 24 * 60 * 60, // 90 days
|
|
1149
1160
|
});
|
|
1150
|
-
|
|
1161
|
+
logger.debug("[ConsentService] OAuth identity linked to User DID:", {
|
|
1151
1162
|
provider: oauthIdentity.provider,
|
|
1152
1163
|
subject: oauthIdentity.subject.substring(0, 20) + "...",
|
|
1153
1164
|
userDid: userDid.substring(0, 20) + "...",
|
|
1154
1165
|
});
|
|
1155
1166
|
}
|
|
1156
1167
|
catch (error) {
|
|
1157
|
-
|
|
1168
|
+
logger.error("[ConsentService] Failed to store OAuth mapping:", error);
|
|
1158
1169
|
// Non-fatal - continue with User DID
|
|
1159
1170
|
}
|
|
1160
1171
|
return userDid;
|
|
@@ -1172,7 +1183,7 @@ export class ConsentService {
|
|
|
1172
1183
|
*/
|
|
1173
1184
|
async cacheExternalUserDid(oauthIdentity, userDid) {
|
|
1174
1185
|
if (!this.env.DELEGATION_STORAGE) {
|
|
1175
|
-
|
|
1186
|
+
logger.warn("[ConsentService] No storage available for caching external User DID");
|
|
1176
1187
|
return;
|
|
1177
1188
|
}
|
|
1178
1189
|
const oauthKey = STORAGE_KEYS.oauthIdentity(oauthIdentity.provider, oauthIdentity.subject);
|
|
@@ -1182,7 +1193,7 @@ export class ConsentService {
|
|
|
1182
1193
|
if (existingUserDid) {
|
|
1183
1194
|
// Mapping already exists - verify it matches
|
|
1184
1195
|
if (existingUserDid !== userDid) {
|
|
1185
|
-
|
|
1196
|
+
logger.warn("[ConsentService] OAuth identity already mapped to different User DID:", {
|
|
1186
1197
|
provider: oauthIdentity.provider,
|
|
1187
1198
|
subject: oauthIdentity.subject.substring(0, 20) + "...",
|
|
1188
1199
|
existingUserDid: existingUserDid.substring(0, 30) + "...",
|
|
@@ -1194,7 +1205,7 @@ export class ConsentService {
|
|
|
1194
1205
|
}
|
|
1195
1206
|
}
|
|
1196
1207
|
catch (error) {
|
|
1197
|
-
|
|
1208
|
+
logger.warn("[ConsentService] Failed to check existing OAuth mapping:", error);
|
|
1198
1209
|
// Continue to store new mapping
|
|
1199
1210
|
}
|
|
1200
1211
|
// Store OAuth identity → userDid mapping (persistent - 90 days)
|
|
@@ -1207,7 +1218,7 @@ export class ConsentService {
|
|
|
1207
1218
|
await this.env.DELEGATION_STORAGE.put(oauthIdentityKey, JSON.stringify(oauthIdentity), {
|
|
1208
1219
|
expirationTtl: 90 * 24 * 60 * 60, // 90 days
|
|
1209
1220
|
});
|
|
1210
|
-
|
|
1221
|
+
logger.debug("[ConsentService] Cached external User DID from AgentShield:", {
|
|
1211
1222
|
provider: oauthIdentity.provider,
|
|
1212
1223
|
subject: oauthIdentity.subject.substring(0, 20) + "...",
|
|
1213
1224
|
userDid: userDid.substring(0, 30) + "...",
|
|
@@ -1215,7 +1226,7 @@ export class ConsentService {
|
|
|
1215
1226
|
});
|
|
1216
1227
|
}
|
|
1217
1228
|
catch (error) {
|
|
1218
|
-
|
|
1229
|
+
logger.debug("[ConsentService] Failed to cache external User DID:", error);
|
|
1219
1230
|
// Non-fatal - the persistent user_did will still be used
|
|
1220
1231
|
}
|
|
1221
1232
|
}
|
|
@@ -1301,6 +1312,21 @@ export class ConsentService {
|
|
|
1301
1312
|
// ✅ Extract agent_name from URL query param (passed from buildConsentUrl)
|
|
1302
1313
|
// Human-readable name for AgentShield dashboard display
|
|
1303
1314
|
const agentName = params.get("agent_name");
|
|
1315
|
+
// ✅ Extract credential auth params for 3-screen flow (Auth → Clickwrap → Success)
|
|
1316
|
+
// These are set when credential auth completes and redirects to clickwrap page
|
|
1317
|
+
// They ensure the delegation is created with 'password' type instead of 'none'
|
|
1318
|
+
const credentialProviderType = params.get("credential_provider_type");
|
|
1319
|
+
const credentialProvider = params.get("credential_provider");
|
|
1320
|
+
// ✅ Extract mode param for post-credential clickwrap detection
|
|
1321
|
+
// When credential auth completes, user is redirected to /consent?mode=consent-only&credential_provider_type=password
|
|
1322
|
+
const mode = params.get("mode");
|
|
1323
|
+
// ✅ LOOP FIX: Detect post-credential clickwrap state to prevent re-rendering credential form
|
|
1324
|
+
// Both conditions must be true to identify post-credential clickwrap:
|
|
1325
|
+
// 1. mode === "consent-only" (indicates we're past auth phase)
|
|
1326
|
+
// 2. credential_provider_type === "password" (confirms this was a credential auth flow)
|
|
1327
|
+
// IMPORTANT: credential_provider_type is a clickwrap-only signal. It is only set by the
|
|
1328
|
+
// credential-auth redirect to clickwrap; it is NOT used for pure consent-only or oauth flows.
|
|
1329
|
+
const isPostCredentialClickwrap = mode === "consent-only" && credentialProviderType === "password";
|
|
1304
1330
|
// Validate required parameters
|
|
1305
1331
|
if (!tool || !agentDid || !sessionId || !projectId) {
|
|
1306
1332
|
return new Response(JSON.stringify({
|
|
@@ -1372,13 +1398,13 @@ export class ConsentService {
|
|
|
1372
1398
|
}
|
|
1373
1399
|
}
|
|
1374
1400
|
catch (parseError) {
|
|
1375
|
-
|
|
1401
|
+
logger.warn("[ConsentService] Invalid OAuth cookie format:", parseError);
|
|
1376
1402
|
}
|
|
1377
1403
|
}
|
|
1378
1404
|
}
|
|
1379
1405
|
}
|
|
1380
1406
|
catch (error) {
|
|
1381
|
-
|
|
1407
|
+
logger.warn("[ConsentService] Failed to extract OAuth cookie:", error);
|
|
1382
1408
|
// Non-fatal - continue without OAuth identity
|
|
1383
1409
|
}
|
|
1384
1410
|
// Check if OAuth is required (after extracting OAuth identity)
|
|
@@ -1400,7 +1426,7 @@ export class ConsentService {
|
|
|
1400
1426
|
// This is the most explicit - passed from buildConsentUrl when tool has oauthProvider configured
|
|
1401
1427
|
if (urlProvider) {
|
|
1402
1428
|
provider = urlProvider;
|
|
1403
|
-
|
|
1429
|
+
logger.debug("[ConsentService] Using provider from URL query param:", {
|
|
1404
1430
|
provider,
|
|
1405
1431
|
projectId,
|
|
1406
1432
|
tool,
|
|
@@ -1417,7 +1443,7 @@ export class ConsentService {
|
|
|
1417
1443
|
if (protection) {
|
|
1418
1444
|
provider = await this.providerResolver.resolveProvider(protection, projectId);
|
|
1419
1445
|
if (provider) {
|
|
1420
|
-
|
|
1446
|
+
logger.debug("[ConsentService] Using provider from resolver:", {
|
|
1421
1447
|
provider,
|
|
1422
1448
|
projectId,
|
|
1423
1449
|
tool,
|
|
@@ -1428,13 +1454,32 @@ export class ConsentService {
|
|
|
1428
1454
|
}
|
|
1429
1455
|
catch (error) {
|
|
1430
1456
|
// Check if this is ConsentOnlyModeError - expected behavior, not an error
|
|
1431
|
-
|
|
1432
|
-
|
|
1457
|
+
// Use multiple detection methods to handle bundled code where class names may be mangled
|
|
1458
|
+
const isConsentOnlyError = error instanceof Error &&
|
|
1459
|
+
(error.name === "ConsentOnlyModeError" ||
|
|
1460
|
+
error.constructor?.name === "ConsentOnlyModeError");
|
|
1461
|
+
// Check for CredentialAuthModeError - expected behavior for password-based auth
|
|
1462
|
+
// Also check for 'provider' property as fallback detection
|
|
1463
|
+
const isCredentialAuthError = error instanceof Error &&
|
|
1464
|
+
(error.name === "CredentialAuthModeError" ||
|
|
1465
|
+
error.constructor?.name === "CredentialAuthModeError" ||
|
|
1466
|
+
// Fallback: check for provider property (unique to CredentialAuthModeError)
|
|
1467
|
+
("provider" in error &&
|
|
1468
|
+
typeof error.provider === "string"));
|
|
1469
|
+
if (isConsentOnlyError) {
|
|
1470
|
+
logger.debug("[ConsentService] Tool is consent-only mode, no provider needed for consent page", { projectId, tool });
|
|
1433
1471
|
// Continue without provider - consent-only mode just needs clickwrap
|
|
1434
1472
|
}
|
|
1473
|
+
else if (isCredentialAuthError) {
|
|
1474
|
+
// CredentialAuthModeError is expected behavior for password-based auth
|
|
1475
|
+
// Extract provider from the error for credential form rendering
|
|
1476
|
+
const credError = error;
|
|
1477
|
+
provider = credError.provider;
|
|
1478
|
+
logger.debug("[ConsentService] Tool uses credential auth, setting provider for credential form", { projectId, tool, provider });
|
|
1479
|
+
}
|
|
1435
1480
|
else {
|
|
1436
1481
|
// Non-fatal - continue without provider info
|
|
1437
|
-
|
|
1482
|
+
logger.warn("[ConsentService] Failed to resolve provider for consent page:", error);
|
|
1438
1483
|
}
|
|
1439
1484
|
}
|
|
1440
1485
|
}
|
|
@@ -1442,7 +1487,7 @@ export class ConsentService {
|
|
|
1442
1487
|
// This is a SEPARATE check (not else-if) to handle when providerResolver exists but returns undefined
|
|
1443
1488
|
if (!provider && toolOAuthProvider) {
|
|
1444
1489
|
provider = toolOAuthProvider;
|
|
1445
|
-
|
|
1490
|
+
logger.debug("[ConsentService] Using provider from tool protection (fallback):", {
|
|
1446
1491
|
provider,
|
|
1447
1492
|
projectId,
|
|
1448
1493
|
tool,
|
|
@@ -1450,10 +1495,29 @@ export class ConsentService {
|
|
|
1450
1495
|
}
|
|
1451
1496
|
let resolvedOAuthUrl;
|
|
1452
1497
|
let isOAuthRequired = false;
|
|
1453
|
-
//
|
|
1454
|
-
|
|
1498
|
+
// ✅ CONFIG-BASED CREDENTIAL DETECTION (replaces string matching)
|
|
1499
|
+
// Fetch credential provider config ONCE and reuse throughout the method
|
|
1500
|
+
// Only fetch when: provider exists AND we're not in post-credential clickwrap state
|
|
1501
|
+
// This avoids unnecessary network calls for pure consent-only tools and post-auth clickwrap
|
|
1502
|
+
let credentialProviderConfig = null;
|
|
1503
|
+
if (provider && projectId && !isPostCredentialClickwrap) {
|
|
1504
|
+
try {
|
|
1505
|
+
credentialProviderConfig = await this.getCredentialProviderConfig(projectId, provider);
|
|
1506
|
+
}
|
|
1507
|
+
catch (err) {
|
|
1508
|
+
// Non-fatal: log and continue - will fall back to OAuth flow
|
|
1509
|
+
logger.warn("[ConsentService] Failed to fetch credential provider config:", err);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
// Determine if this is a credentials provider based on config existence (not string matching)
|
|
1513
|
+
const isCredentialsProvider = !!credentialProviderConfig;
|
|
1455
1514
|
if (isCredentialsProvider && oauthRequired) {
|
|
1456
|
-
|
|
1515
|
+
logger.debug("[ConsentService] Credential provider detected via config - skipping OAuth flow, will render credential form", {
|
|
1516
|
+
provider,
|
|
1517
|
+
projectId,
|
|
1518
|
+
tool,
|
|
1519
|
+
providerType: credentialProviderConfig?.type,
|
|
1520
|
+
});
|
|
1457
1521
|
}
|
|
1458
1522
|
if (oauthRequired && !isCredentialsProvider) {
|
|
1459
1523
|
// OAuth is required - redirect to OAuth provider instead of showing consent page
|
|
@@ -1468,7 +1532,7 @@ export class ConsentService {
|
|
|
1468
1532
|
oauthSecurityService, agentName || undefined // Human-readable agent name for AgentShield dashboard
|
|
1469
1533
|
);
|
|
1470
1534
|
isOAuthRequired = true;
|
|
1471
|
-
|
|
1535
|
+
logger.debug("[ConsentService] 🔒 SECURITY EVENT: OAuth required, preparing consent page with redirect:", {
|
|
1472
1536
|
projectId,
|
|
1473
1537
|
agentDid: agentDid.substring(0, 20) + "...",
|
|
1474
1538
|
oauthUrl: resolvedOAuthUrl.substring(0, 100) + "...",
|
|
@@ -1492,7 +1556,7 @@ export class ConsentService {
|
|
|
1492
1556
|
})
|
|
1493
1557
|
.catch((err) => {
|
|
1494
1558
|
// Structured error logging
|
|
1495
|
-
|
|
1559
|
+
logger.debug("[ConsentService] Audit logging failed", {
|
|
1496
1560
|
eventType: "consent:page_viewed",
|
|
1497
1561
|
sessionId,
|
|
1498
1562
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -1515,10 +1579,12 @@ export class ConsentService {
|
|
|
1515
1579
|
provider, // Phase 2: Include provider if resolved
|
|
1516
1580
|
branding: consentConfig.branding,
|
|
1517
1581
|
// ✅ Ensure terms.required has a default value (contracts requires boolean, not boolean | undefined)
|
|
1518
|
-
terms: consentConfig.terms
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1582
|
+
terms: consentConfig.terms
|
|
1583
|
+
? {
|
|
1584
|
+
...consentConfig.terms,
|
|
1585
|
+
required: consentConfig.terms.required ?? true,
|
|
1586
|
+
}
|
|
1587
|
+
: undefined,
|
|
1522
1588
|
customFields: consentConfig.customFields,
|
|
1523
1589
|
autoClose: consentConfig.ui?.autoClose ?? false,
|
|
1524
1590
|
// ✅ FIX: Pass full UI config (title, description, button text, etc.)
|
|
@@ -1530,35 +1596,34 @@ export class ConsentService {
|
|
|
1530
1596
|
// Pass OAuth details for client-side handling
|
|
1531
1597
|
oauthRequired: isOAuthRequired,
|
|
1532
1598
|
oauthUrl: resolvedOAuthUrl,
|
|
1599
|
+
// ✅ Pass credential auth params for 3-screen flow (Auth → Clickwrap → Success)
|
|
1600
|
+
// These are set when user is redirected from credential auth page to clickwrap
|
|
1601
|
+
// They ensure the delegation is created with 'password' type instead of 'none'
|
|
1602
|
+
credentialProviderType: credentialProviderType || undefined,
|
|
1603
|
+
credentialProvider: credentialProvider || undefined,
|
|
1533
1604
|
};
|
|
1534
|
-
// CRED-003:
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
}
|
|
1557
|
-
}
|
|
1558
|
-
catch (credError) {
|
|
1559
|
-
// Non-fatal: If credential provider check fails, fall through to OAuth
|
|
1560
|
-
console.warn("[ConsentService] Credential provider check failed, falling back to OAuth:", credError);
|
|
1561
|
-
}
|
|
1605
|
+
// CRED-003: Render credential form if this is a credential provider
|
|
1606
|
+
// Guard conditions:
|
|
1607
|
+
// 1. credentialProviderConfig must exist (already fetched above)
|
|
1608
|
+
// 2. Must NOT be in post-credential clickwrap mode (would cause infinite loop)
|
|
1609
|
+
if (credentialProviderConfig && !isPostCredentialClickwrap) {
|
|
1610
|
+
logger.debug("[ConsentService] Credential provider detected, rendering credential page", {
|
|
1611
|
+
projectId,
|
|
1612
|
+
provider,
|
|
1613
|
+
providerType: credentialProviderConfig.type,
|
|
1614
|
+
displayName: credentialProviderConfig.displayName,
|
|
1615
|
+
});
|
|
1616
|
+
// Generate and store CSRF token for form security
|
|
1617
|
+
const csrfToken = await this.storeCredentialCsrfToken(sessionId);
|
|
1618
|
+
// Render credential login page instead of OAuth consent
|
|
1619
|
+
const html = this.renderer.renderCredentialPage(pageConfig, credentialProviderConfig, provider, csrfToken);
|
|
1620
|
+
return new Response(html, {
|
|
1621
|
+
status: 200,
|
|
1622
|
+
headers: {
|
|
1623
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
1624
|
+
"Cache-Control": "no-cache, no-store, must-revalidate",
|
|
1625
|
+
},
|
|
1626
|
+
});
|
|
1562
1627
|
}
|
|
1563
1628
|
// Standard OAuth flow: Render page with OAuth identity (if available)
|
|
1564
1629
|
const html = this.renderer.render(pageConfig, oauthIdentity);
|
|
@@ -1571,7 +1636,7 @@ export class ConsentService {
|
|
|
1571
1636
|
});
|
|
1572
1637
|
}
|
|
1573
1638
|
catch (error) {
|
|
1574
|
-
|
|
1639
|
+
logger.error("[ConsentService] Error rendering consent page:", error);
|
|
1575
1640
|
return new Response(JSON.stringify({
|
|
1576
1641
|
success: false,
|
|
1577
1642
|
error: "Failed to render consent page",
|
|
@@ -1809,7 +1874,7 @@ export class ConsentService {
|
|
|
1809
1874
|
}
|
|
1810
1875
|
catch (formDataError) {
|
|
1811
1876
|
// FormData parsing failed, try URL-encoded text parsing
|
|
1812
|
-
|
|
1877
|
+
logger.warn("[ConsentService] FormData parsing failed, trying URL-encoded text:", formDataError);
|
|
1813
1878
|
}
|
|
1814
1879
|
// Try URL-encoded text parsing as fallback
|
|
1815
1880
|
try {
|
|
@@ -1820,7 +1885,7 @@ export class ConsentService {
|
|
|
1820
1885
|
}
|
|
1821
1886
|
catch (urlEncodedError) {
|
|
1822
1887
|
// Both failed, fall through to JSON parsing
|
|
1823
|
-
|
|
1888
|
+
logger.warn("[ConsentService] URL-encoded parsing also failed:", urlEncodedError);
|
|
1824
1889
|
}
|
|
1825
1890
|
}
|
|
1826
1891
|
// Parse multipart FormData (multipart/form-data or when FormData object is passed with other Content-Type)
|
|
@@ -2148,7 +2213,7 @@ export class ConsentService {
|
|
|
2148
2213
|
}
|
|
2149
2214
|
}
|
|
2150
2215
|
catch (formDataError) {
|
|
2151
|
-
|
|
2216
|
+
logger.warn("[ConsentService] FormData parsing failed:", formDataError);
|
|
2152
2217
|
// Fall through to JSON parsing
|
|
2153
2218
|
}
|
|
2154
2219
|
}
|
|
@@ -2665,58 +2730,115 @@ export class ConsentService {
|
|
|
2665
2730
|
* @returns JSON response
|
|
2666
2731
|
*/
|
|
2667
2732
|
async handleApproval(request) {
|
|
2668
|
-
|
|
2733
|
+
logger.debug("[ConsentService] Approval request received");
|
|
2669
2734
|
try {
|
|
2670
2735
|
// Parse and validate request body (supports both JSON and FormData)
|
|
2671
2736
|
const body = await this.parseRequestBody(request);
|
|
2672
|
-
|
|
2737
|
+
logger.debug("[ConsentService] Request body parsed:", {
|
|
2673
2738
|
hasBody: !!body,
|
|
2674
2739
|
bodyKeys: Object.keys(body || {}),
|
|
2675
2740
|
hasOAuthIdentity: !!body?.oauth_identity,
|
|
2676
2741
|
});
|
|
2677
2742
|
// Handle provider_type routing:
|
|
2678
|
-
// - 'credential': User authenticated via credential form (email/password)
|
|
2743
|
+
// - 'credential' or 'password': User authenticated via credential form (email/password)
|
|
2679
2744
|
// - 'oauth2': User authenticated via OAuth provider (handled below)
|
|
2680
2745
|
// - 'none': Consent-only mode (clickwrap) - user agrees without authentication
|
|
2681
2746
|
const bodyObj = body;
|
|
2682
2747
|
const providerType = bodyObj?.provider_type;
|
|
2683
2748
|
// CRED-003: Check for credential provider submission
|
|
2684
|
-
// Credential submissions include `provider_type: 'credential'` and are handled separately
|
|
2685
|
-
|
|
2686
|
-
|
|
2749
|
+
// Credential submissions include `provider_type: 'credential'` or 'password' and are handled separately
|
|
2750
|
+
// IMPORTANT: AgentShield tool protection uses 'password' for credential-based auth validation,
|
|
2751
|
+
// but we also accept 'credential' for backward compatibility
|
|
2752
|
+
if (providerType === CONSENT_PROVIDER_TYPES.CREDENTIAL ||
|
|
2753
|
+
providerType === CONSENT_PROVIDER_TYPES.PASSWORD) {
|
|
2754
|
+
logger.debug("[ConsentService] Credential submission detected", {
|
|
2755
|
+
provider_type: providerType,
|
|
2756
|
+
});
|
|
2687
2757
|
return this.handleCredentialApproval(bodyObj);
|
|
2688
2758
|
}
|
|
2689
2759
|
// ✅ Consent-only mode (provider_type: 'none')
|
|
2690
|
-
//
|
|
2691
|
-
//
|
|
2760
|
+
// This can be:
|
|
2761
|
+
// 1. Pure consent-only: User consents without authentication (clickwrap only)
|
|
2762
|
+
// 2. Post-credential clickwrap: User already authenticated via credentials, this is the clickwrap step
|
|
2763
|
+
// 3. Post-OAuth clickwrap: User already authenticated via OAuth, this is the clickwrap step
|
|
2692
2764
|
if (providerType === CONSENT_PROVIDER_TYPES.NONE) {
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
}
|
|
2713
|
-
}
|
|
2714
|
-
else {
|
|
2715
|
-
console.warn("[ConsentService] ⚠️ Failed to generate ephemeral userDid - delegation will use session-scoped storage only");
|
|
2765
|
+
const bodyObj = body;
|
|
2766
|
+
const sessionId = bodyObj.session_id;
|
|
2767
|
+
// Check for credential auth parameters passed from handleCredentialApproval
|
|
2768
|
+
// These indicate this is the clickwrap step AFTER credential authentication
|
|
2769
|
+
const credentialProviderType = bodyObj.credential_provider_type;
|
|
2770
|
+
const credentialProvider = bodyObj.credential_provider;
|
|
2771
|
+
if (credentialProviderType &&
|
|
2772
|
+
credentialProviderType === CONSENT_PROVIDER_TYPES.PASSWORD) {
|
|
2773
|
+
// This is the clickwrap step after credential auth
|
|
2774
|
+
// The session already has userDid from credential auth
|
|
2775
|
+
logger.debug("[ConsentService] Consent-only mode (post-credential clickwrap)");
|
|
2776
|
+
// Override provider_type to 'password' so delegation is created with correct authorization
|
|
2777
|
+
// This ensures authorization.type matches tool protection expectation
|
|
2778
|
+
bodyObj.provider_type = CONSENT_PROVIDER_TYPES.PASSWORD;
|
|
2779
|
+
if (credentialProvider) {
|
|
2780
|
+
bodyObj.customFields = {
|
|
2781
|
+
...(bodyObj.customFields || {}),
|
|
2782
|
+
provider: credentialProvider,
|
|
2783
|
+
provider_type: CONSENT_PROVIDER_TYPES.PASSWORD,
|
|
2784
|
+
};
|
|
2716
2785
|
}
|
|
2786
|
+
logger.debug("[ConsentService] ✅ Using credential auth provider_type for delegation:", {
|
|
2787
|
+
sessionId: sessionId?.substring(0, 20) + "...",
|
|
2788
|
+
providerType: CONSENT_PROVIDER_TYPES.PASSWORD,
|
|
2789
|
+
provider: credentialProvider,
|
|
2790
|
+
note: "Delegation will have authorization.type='password' to match tool protection",
|
|
2791
|
+
});
|
|
2792
|
+
// The userDid should already be in the session from credential auth
|
|
2793
|
+
// We don't need to generate ephemeral one
|
|
2717
2794
|
}
|
|
2718
2795
|
else {
|
|
2719
|
-
|
|
2796
|
+
// Pure consent-only mode - no prior authentication
|
|
2797
|
+
logger.debug("[ConsentService] Consent-only mode detected (pure clickwrap)");
|
|
2798
|
+
if (sessionId && this.env.DELEGATION_STORAGE) {
|
|
2799
|
+
// Check if session already has userDid (from credential auth or OAuth)
|
|
2800
|
+
let existingUserDid;
|
|
2801
|
+
try {
|
|
2802
|
+
const sessionKey = STORAGE_KEYS.session(sessionId);
|
|
2803
|
+
const existingSession = (await this.env.DELEGATION_STORAGE.get(sessionKey, "json"));
|
|
2804
|
+
existingUserDid = existingSession?.userDid;
|
|
2805
|
+
}
|
|
2806
|
+
catch (error) {
|
|
2807
|
+
logger.warn("[ConsentService] Failed to read session for userDid:", error);
|
|
2808
|
+
}
|
|
2809
|
+
if (existingUserDid) {
|
|
2810
|
+
// Session has userDid from prior auth step - use it
|
|
2811
|
+
logger.debug("[ConsentService] Session already has userDid, reusing:", {
|
|
2812
|
+
sessionId: sessionId.substring(0, 20) + "...",
|
|
2813
|
+
userDid: existingUserDid.substring(0, 30) + "...",
|
|
2814
|
+
});
|
|
2815
|
+
bodyObj.user_did = existingUserDid;
|
|
2816
|
+
}
|
|
2817
|
+
else {
|
|
2818
|
+
// Generate ephemeral userDid for pure consent-only
|
|
2819
|
+
// This enables PRIORITY 1 delegation key (user+agent scoped) for reliable lookup
|
|
2820
|
+
const ephemeralUserDid = await this.generateEphemeralUserDidForSession(sessionId);
|
|
2821
|
+
if (ephemeralUserDid) {
|
|
2822
|
+
// Store ephemeral userDid in session cache
|
|
2823
|
+
// This ensures storeDelegationToken can find it and use PRIORITY 1 key
|
|
2824
|
+
await this.updateSessionWithIdentity(sessionId, ephemeralUserDid, null // No OAuth identity for consent-only
|
|
2825
|
+
);
|
|
2826
|
+
// Also inject the user_did into the request body so createDelegation receives it
|
|
2827
|
+
bodyObj.user_did = ephemeralUserDid;
|
|
2828
|
+
logger.debug("[ConsentService] ✅ Ephemeral userDid stored in session for consent-only flow:", {
|
|
2829
|
+
sessionId: sessionId.substring(0, 20) + "...",
|
|
2830
|
+
userDid: ephemeralUserDid.substring(0, 30) + "...",
|
|
2831
|
+
note: "Enables reliable delegation lookup via PRIORITY 1 key",
|
|
2832
|
+
});
|
|
2833
|
+
}
|
|
2834
|
+
else {
|
|
2835
|
+
logger.warn("[ConsentService] ⚠️ Failed to generate ephemeral userDid - delegation will use session-scoped storage only");
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
else {
|
|
2840
|
+
logger.warn("[ConsentService] ⚠️ No session_id for consent-only flow - delegation will use session-scoped storage only");
|
|
2841
|
+
}
|
|
2720
2842
|
}
|
|
2721
2843
|
}
|
|
2722
2844
|
// Convert null oauth_identity to undefined for proper schema validation
|
|
@@ -2733,7 +2855,7 @@ export class ConsentService {
|
|
|
2733
2855
|
}
|
|
2734
2856
|
const validation = validateConsentApprovalRequest(body);
|
|
2735
2857
|
if (!validation.success) {
|
|
2736
|
-
|
|
2858
|
+
logger.error("[ConsentService] Approval request validation failed:", {
|
|
2737
2859
|
errors: validation.error.errors,
|
|
2738
2860
|
receivedBody: body,
|
|
2739
2861
|
});
|
|
@@ -2748,7 +2870,7 @@ export class ConsentService {
|
|
|
2748
2870
|
});
|
|
2749
2871
|
}
|
|
2750
2872
|
const approvalRequest = validation.data;
|
|
2751
|
-
|
|
2873
|
+
logger.debug("[ConsentService] Approval request validated:", {
|
|
2752
2874
|
agentDid: approvalRequest.agent_did?.substring(0, 20) + "...",
|
|
2753
2875
|
sessionId: approvalRequest.session_id?.substring(0, 20) + "...",
|
|
2754
2876
|
scopes: approvalRequest.scopes,
|
|
@@ -2788,7 +2910,7 @@ export class ConsentService {
|
|
|
2788
2910
|
oauthProvider: approvalRequest.oauth_identity?.provider,
|
|
2789
2911
|
})
|
|
2790
2912
|
.catch((err) => {
|
|
2791
|
-
|
|
2913
|
+
logger.debug("[ConsentService] Failed to log credential required", {
|
|
2792
2914
|
eventType: "consent:credential_required",
|
|
2793
2915
|
sessionId: approvalRequest.session_id,
|
|
2794
2916
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -2798,10 +2920,10 @@ export class ConsentService {
|
|
|
2798
2920
|
// The credential_required event is just for audit tracking
|
|
2799
2921
|
}
|
|
2800
2922
|
// Create delegation via AgentShield API
|
|
2801
|
-
|
|
2923
|
+
logger.debug("[ConsentService] Creating delegation...");
|
|
2802
2924
|
const delegationResult = await this.createDelegation(approvalRequest);
|
|
2803
2925
|
if (!delegationResult.success) {
|
|
2804
|
-
|
|
2926
|
+
logger.error("[ConsentService] Delegation creation failed:", {
|
|
2805
2927
|
error: delegationResult.error,
|
|
2806
2928
|
error_code: delegationResult.error_code,
|
|
2807
2929
|
});
|
|
@@ -2814,7 +2936,7 @@ export class ConsentService {
|
|
|
2814
2936
|
headers: { "Content-Type": "application/json" },
|
|
2815
2937
|
});
|
|
2816
2938
|
}
|
|
2817
|
-
|
|
2939
|
+
logger.info("[ConsentService] ✅ Delegation created successfully:", {
|
|
2818
2940
|
delegationId: delegationResult.delegation_id?.substring(0, 20) + "...",
|
|
2819
2941
|
});
|
|
2820
2942
|
// Store delegation token in KV
|
|
@@ -2831,7 +2953,7 @@ export class ConsentService {
|
|
|
2831
2953
|
(await this.getUserDidForSession(approvalRequest.session_id, approvalRequest.oauth_identity || undefined)) ?? undefined; // Phase 5: Convert null to undefined
|
|
2832
2954
|
}
|
|
2833
2955
|
catch (error) {
|
|
2834
|
-
|
|
2956
|
+
logger.warn("[ConsentService] Failed to get userDid for audit logging:", error);
|
|
2835
2957
|
// Continue without userDid - audit events can still be logged
|
|
2836
2958
|
}
|
|
2837
2959
|
}
|
|
@@ -2862,7 +2984,7 @@ export class ConsentService {
|
|
|
2862
2984
|
});
|
|
2863
2985
|
}
|
|
2864
2986
|
catch (error) {
|
|
2865
|
-
|
|
2987
|
+
logger.debug("[ConsentService] Audit failed but continuing", {
|
|
2866
2988
|
sessionId: approvalRequest.session_id,
|
|
2867
2989
|
error: error instanceof Error ? error.message : String(error),
|
|
2868
2990
|
eventTypes: ["consent:approved", "consent:delegation_created"],
|
|
@@ -2881,7 +3003,7 @@ export class ConsentService {
|
|
|
2881
3003
|
});
|
|
2882
3004
|
}
|
|
2883
3005
|
catch (error) {
|
|
2884
|
-
|
|
3006
|
+
logger.error("[ConsentService] Error handling approval:", error);
|
|
2885
3007
|
return new Response(JSON.stringify({
|
|
2886
3008
|
success: false,
|
|
2887
3009
|
error: "Internal server error",
|
|
@@ -2902,7 +3024,7 @@ export class ConsentService {
|
|
|
2902
3024
|
const agentShieldUrl = this.env.AGENTSHIELD_API_URL || DEFAULT_AGENTSHIELD_URL;
|
|
2903
3025
|
const apiKey = this.env.AGENTSHIELD_API_KEY;
|
|
2904
3026
|
if (!apiKey) {
|
|
2905
|
-
|
|
3027
|
+
logger.warn("[ConsentService] No API key configured, cannot create delegation");
|
|
2906
3028
|
return {
|
|
2907
3029
|
success: false,
|
|
2908
3030
|
error: "API key not configured",
|
|
@@ -2920,7 +3042,7 @@ export class ConsentService {
|
|
|
2920
3042
|
// Only fetch from storage if not already provided in request
|
|
2921
3043
|
if (!userDid && request.session_id) {
|
|
2922
3044
|
try {
|
|
2923
|
-
|
|
3045
|
+
logger.debug("[ConsentService] Getting User DID for session:", {
|
|
2924
3046
|
sessionId: request.session_id.substring(0, 20) + "...",
|
|
2925
3047
|
hasOAuthIdentity: !!request.oauth_identity,
|
|
2926
3048
|
oauthProvider: request.oauth_identity?.provider,
|
|
@@ -2932,27 +3054,27 @@ export class ConsentService {
|
|
|
2932
3054
|
userDid =
|
|
2933
3055
|
(await this.getUserDidForSession(request.session_id, request.oauth_identity || undefined // Explicitly handle null as undefined
|
|
2934
3056
|
)) ?? undefined;
|
|
2935
|
-
|
|
3057
|
+
logger.debug("[ConsentService] User DID retrieved from storage:", {
|
|
2936
3058
|
userDid: userDid?.substring(0, 20) + "...",
|
|
2937
3059
|
hasUserDid: !!userDid,
|
|
2938
3060
|
});
|
|
2939
3061
|
}
|
|
2940
3062
|
catch (error) {
|
|
2941
|
-
|
|
3063
|
+
logger.debug("[ConsentService] Failed to get/generate userDid:", error);
|
|
2942
3064
|
// Continue without userDid - delegation will work without user_identifier
|
|
2943
3065
|
// This is valid for non-OAuth scenarios, but we should log this as a warning
|
|
2944
|
-
|
|
3066
|
+
logger.warn("[ConsentService] Delegation will be created without user_identifier - this may affect user tracking");
|
|
2945
3067
|
}
|
|
2946
3068
|
}
|
|
2947
3069
|
else if (userDid) {
|
|
2948
3070
|
// userDid was provided in request (e.g., from credential auth flow)
|
|
2949
|
-
|
|
3071
|
+
logger.debug("[ConsentService] Using provided user_did from request:", {
|
|
2950
3072
|
userDid: userDid.substring(0, 20) + "...",
|
|
2951
3073
|
source: "request.user_did",
|
|
2952
3074
|
});
|
|
2953
3075
|
}
|
|
2954
3076
|
else {
|
|
2955
|
-
|
|
3077
|
+
logger.debug("[ConsentService] No session_id provided - skipping User DID generation");
|
|
2956
3078
|
}
|
|
2957
3079
|
const expiresInDays = 7; // Default to 7 days
|
|
2958
3080
|
// Phase 2 VC-Only: Issue Delegation VC if we have a session and userDid
|
|
@@ -2983,21 +3105,21 @@ export class ConsentService {
|
|
|
2983
3105
|
const vcResult = await this.issueDelegationVC(localDelegation, request.session_id, request.oauth_identity || undefined);
|
|
2984
3106
|
credentialJwt = vcResult ?? undefined;
|
|
2985
3107
|
if (credentialJwt) {
|
|
2986
|
-
|
|
3108
|
+
logger.debug("[ConsentService] VC issued for delegation:", {
|
|
2987
3109
|
delegationId,
|
|
2988
3110
|
jwtLength: credentialJwt.length,
|
|
2989
3111
|
});
|
|
2990
3112
|
}
|
|
2991
3113
|
}
|
|
2992
3114
|
catch (error) {
|
|
2993
|
-
|
|
3115
|
+
logger.warn("[ConsentService] Failed to issue VC, continuing without it:", error);
|
|
2994
3116
|
// Non-fatal - delegation will work without VC
|
|
2995
3117
|
}
|
|
2996
3118
|
}
|
|
2997
3119
|
// Build delegation request with error-based format detection
|
|
2998
3120
|
// Try full format first, fallback to simplified format on error
|
|
2999
3121
|
const delegationRequest = await this.buildDelegationRequest(request, userDid, expiresInDays, fieldName, credentialJwt);
|
|
3000
|
-
|
|
3122
|
+
logger.debug("[ConsentService] Creating delegation:", {
|
|
3001
3123
|
url: `${agentShieldUrl}${AGENTSHIELD_ENDPOINTS.DELEGATIONS_CREATE}`,
|
|
3002
3124
|
agentDid: request.agent_did.substring(0, 20) + "...",
|
|
3003
3125
|
scopes: request.scopes,
|
|
@@ -3028,7 +3150,7 @@ export class ConsentService {
|
|
|
3028
3150
|
// AgentShield now returns delegation_token for stateless verification
|
|
3029
3151
|
const responseDataObj = wrappedResponse.data;
|
|
3030
3152
|
const delegationToken = responseDataObj.delegation_token;
|
|
3031
|
-
|
|
3153
|
+
logger.info("[ConsentService] ✅ Delegation created successfully:", {
|
|
3032
3154
|
delegationId,
|
|
3033
3155
|
agentDid: wrappedResponse.data.agent_did.substring(0, 20) + "...",
|
|
3034
3156
|
scopes: wrappedResponse.data.scopes,
|
|
@@ -3051,7 +3173,7 @@ export class ConsentService {
|
|
|
3051
3173
|
// Extract delegation_token (JWT) if present in response
|
|
3052
3174
|
const unwrappedDataObj = unwrappedResponse;
|
|
3053
3175
|
const delegationToken = unwrappedDataObj.delegation_token;
|
|
3054
|
-
|
|
3176
|
+
logger.info("[ConsentService] ✅ Delegation created successfully:", {
|
|
3055
3177
|
delegationId,
|
|
3056
3178
|
agentDid: unwrappedResponse.agent_did.substring(0, 20) + "...",
|
|
3057
3179
|
scopes: unwrappedResponse.scopes,
|
|
@@ -3074,7 +3196,7 @@ export class ConsentService {
|
|
|
3074
3196
|
data?.id ||
|
|
3075
3197
|
delegationObj?.id;
|
|
3076
3198
|
if (!delegationId) {
|
|
3077
|
-
|
|
3199
|
+
logger.debug("[ConsentService] Invalid response format - missing delegation_id:", responseData);
|
|
3078
3200
|
return {
|
|
3079
3201
|
success: false,
|
|
3080
3202
|
error: "Invalid API response format - missing delegation_id",
|
|
@@ -3084,7 +3206,7 @@ export class ConsentService {
|
|
|
3084
3206
|
// Extract delegation_token from fallback parsing
|
|
3085
3207
|
const delegationToken = data?.delegation_token ||
|
|
3086
3208
|
delegationObj?.delegation_token;
|
|
3087
|
-
|
|
3209
|
+
logger.warn("[ConsentService] ⚠️ Response format not fully validated, using fallback parsing:", {
|
|
3088
3210
|
delegationId,
|
|
3089
3211
|
hasDelegationToken: !!delegationToken,
|
|
3090
3212
|
tokenLength: delegationToken?.length,
|
|
@@ -3097,7 +3219,7 @@ export class ConsentService {
|
|
|
3097
3219
|
};
|
|
3098
3220
|
}
|
|
3099
3221
|
catch (error) {
|
|
3100
|
-
|
|
3222
|
+
logger.error("[ConsentService] Error creating delegation:", error);
|
|
3101
3223
|
return {
|
|
3102
3224
|
success: false,
|
|
3103
3225
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
@@ -3118,7 +3240,7 @@ export class ConsentService {
|
|
|
3118
3240
|
async storeDelegationToken(sessionId, agentDid, token, delegationId) {
|
|
3119
3241
|
const delegationStorage = this.env.DELEGATION_STORAGE;
|
|
3120
3242
|
if (!delegationStorage) {
|
|
3121
|
-
|
|
3243
|
+
logger.warn("[ConsentService] No delegation storage configured, token not stored");
|
|
3122
3244
|
return;
|
|
3123
3245
|
}
|
|
3124
3246
|
try {
|
|
@@ -3137,7 +3259,7 @@ export class ConsentService {
|
|
|
3137
3259
|
await delegationStorage.put(userAgentKey, token, {
|
|
3138
3260
|
expirationTtl: ttl,
|
|
3139
3261
|
});
|
|
3140
|
-
|
|
3262
|
+
logger.info("[ConsentService] ✅ Token stored with user+agent DID:", {
|
|
3141
3263
|
key: userAgentKey,
|
|
3142
3264
|
ttl,
|
|
3143
3265
|
delegationId,
|
|
@@ -3162,7 +3284,7 @@ export class ConsentService {
|
|
|
3162
3284
|
await delegationStorage.put(sessionKey, JSON.stringify(sessionDataToStore), {
|
|
3163
3285
|
expirationTtl: ttl, // Use full TTL - this is the primary storage for session
|
|
3164
3286
|
});
|
|
3165
|
-
|
|
3287
|
+
logger.info("[ConsentService] ✅ Token stored for session:", {
|
|
3166
3288
|
key: sessionKey,
|
|
3167
3289
|
ttl,
|
|
3168
3290
|
sessionId: sessionId.substring(0, 20) + "...",
|
|
@@ -3170,7 +3292,7 @@ export class ConsentService {
|
|
|
3170
3292
|
preservedIdentityState: sessionData?.identityState || "none",
|
|
3171
3293
|
});
|
|
3172
3294
|
// Metrics: Log delegation key type for monitoring
|
|
3173
|
-
|
|
3295
|
+
logger.debug("[ConsentService] 📊 Delegation storage metrics:", {
|
|
3174
3296
|
delegationKeyType: userDid ? "user_agent_scoped" : "session_scoped",
|
|
3175
3297
|
hasUserDid: !!userDid,
|
|
3176
3298
|
userDidPrefix: userDid
|
|
@@ -3186,7 +3308,7 @@ export class ConsentService {
|
|
|
3186
3308
|
}
|
|
3187
3309
|
catch (error) {
|
|
3188
3310
|
// Storage errors are non-fatal - log but don't fail the request
|
|
3189
|
-
|
|
3311
|
+
logger.debug("[ConsentService] Storage error (non-fatal):", error);
|
|
3190
3312
|
}
|
|
3191
3313
|
}
|
|
3192
3314
|
/**
|
|
@@ -3208,7 +3330,7 @@ export class ConsentService {
|
|
|
3208
3330
|
}
|
|
3209
3331
|
catch (error) {
|
|
3210
3332
|
// Ignore errors, use default (autoClose = false)
|
|
3211
|
-
|
|
3333
|
+
logger.warn("[ConsentService] Failed to fetch config for autoClose:", error);
|
|
3212
3334
|
}
|
|
3213
3335
|
}
|
|
3214
3336
|
const html = this.renderer.renderSuccess({
|
|
@@ -3285,9 +3407,24 @@ export class ConsentService {
|
|
|
3285
3407
|
* @returns JSON response
|
|
3286
3408
|
*/
|
|
3287
3409
|
async handleCredentialApproval(body) {
|
|
3288
|
-
|
|
3410
|
+
logger.debug("[ConsentService] Processing credential approval");
|
|
3289
3411
|
// Extract standard fields
|
|
3290
3412
|
const { tool, scopes: rawScopes, agent_did, session_id, project_id, provider, provider_type, csrf_token, ...credentials } = body;
|
|
3413
|
+
// DEBUG: Log credentials to diagnose password encoding issues
|
|
3414
|
+
// SECURITY: Mask password, but show length and first/last char for debugging
|
|
3415
|
+
const pwd = credentials.password || "";
|
|
3416
|
+
logger.debug("[ConsentService] DEBUG: Credential values received", {
|
|
3417
|
+
hasUsername: !!credentials.username,
|
|
3418
|
+
usernameLength: (credentials.username || "").length,
|
|
3419
|
+
usernameValue: credentials.username, // Safe to log email
|
|
3420
|
+
hasPassword: !!credentials.password,
|
|
3421
|
+
passwordLength: pwd.length,
|
|
3422
|
+
passwordFirstChar: pwd.length > 0 ? pwd[0] : "",
|
|
3423
|
+
passwordLastChar: pwd.length > 0 ? pwd[pwd.length - 1] : "",
|
|
3424
|
+
// Check for special chars that might be escaped
|
|
3425
|
+
passwordContainsDollar: pwd.includes("$"),
|
|
3426
|
+
passwordContainsDoubleD: pwd.includes("$$"),
|
|
3427
|
+
});
|
|
3291
3428
|
// Parse scopes - handles double JSON encoding from form submission
|
|
3292
3429
|
// The form stores scopes as JSON string, then JS submits it as JSON again
|
|
3293
3430
|
const scopes = this.parseScopes(rawScopes);
|
|
@@ -3308,7 +3445,7 @@ export class ConsentService {
|
|
|
3308
3445
|
}
|
|
3309
3446
|
// Validate CSRF token
|
|
3310
3447
|
if (!csrf_token || typeof csrf_token !== "string") {
|
|
3311
|
-
|
|
3448
|
+
logger.warn("[ConsentService] Missing or invalid CSRF token");
|
|
3312
3449
|
return new Response(JSON.stringify({
|
|
3313
3450
|
success: false,
|
|
3314
3451
|
error: "Invalid or missing CSRF token",
|
|
@@ -3318,7 +3455,7 @@ export class ConsentService {
|
|
|
3318
3455
|
// Validate CSRF token against stored value
|
|
3319
3456
|
const csrfValid = await this.validateCredentialCsrfToken(csrf_token, session_id);
|
|
3320
3457
|
if (!csrfValid) {
|
|
3321
|
-
|
|
3458
|
+
logger.warn("[ConsentService] CSRF token validation failed", {
|
|
3322
3459
|
sessionId: session_id.substring(0, 20) + "...",
|
|
3323
3460
|
});
|
|
3324
3461
|
return new Response(JSON.stringify({
|
|
@@ -3331,7 +3468,7 @@ export class ConsentService {
|
|
|
3331
3468
|
// 1. Fetch credential provider config from AgentShield
|
|
3332
3469
|
const providerConfig = await this.getCredentialProviderConfig(project_id, provider);
|
|
3333
3470
|
if (!providerConfig) {
|
|
3334
|
-
|
|
3471
|
+
logger.debug("[ConsentService] Credential provider config not found", {
|
|
3335
3472
|
projectId: project_id,
|
|
3336
3473
|
provider,
|
|
3337
3474
|
});
|
|
@@ -3347,20 +3484,55 @@ export class ConsentService {
|
|
|
3347
3484
|
// See: https://developers.cloudflare.com/workers/observability/errors/#illegal-invocation-errors
|
|
3348
3485
|
const credentialHandler = createCredentialAuthHandler({
|
|
3349
3486
|
fetch: (...args) => globalThis.fetch(...args),
|
|
3350
|
-
logger: (msg, data) =>
|
|
3487
|
+
logger: (msg, data) => logger.debug(msg, data),
|
|
3351
3488
|
});
|
|
3352
3489
|
const authResult = await credentialHandler.authenticate(providerConfig, credentials);
|
|
3353
3490
|
if (!authResult.success) {
|
|
3354
|
-
|
|
3491
|
+
logger.error("[ConsentService] Credential authentication failed", {
|
|
3355
3492
|
error: authResult.error,
|
|
3356
3493
|
});
|
|
3494
|
+
// Log credential auth failure for audit trail
|
|
3495
|
+
const auditService = await this.getAuditService(project_id);
|
|
3496
|
+
if (auditService) {
|
|
3497
|
+
await auditService
|
|
3498
|
+
.logCredentialAuthFailed({
|
|
3499
|
+
sessionId: session_id,
|
|
3500
|
+
agentDid: agent_did,
|
|
3501
|
+
targetTools: [tool],
|
|
3502
|
+
scopes: scopes,
|
|
3503
|
+
projectId: project_id,
|
|
3504
|
+
provider: provider,
|
|
3505
|
+
errorMessage: authResult.error,
|
|
3506
|
+
})
|
|
3507
|
+
.catch((err) => {
|
|
3508
|
+
logger.warn("[ConsentService] Failed to log credential auth failure:", err);
|
|
3509
|
+
});
|
|
3510
|
+
}
|
|
3357
3511
|
return new Response(JSON.stringify({
|
|
3358
3512
|
success: false,
|
|
3359
3513
|
error: authResult.error || "Authentication failed",
|
|
3360
3514
|
error_code: "auth_failed",
|
|
3361
3515
|
}), { status: 401, headers: { "Content-Type": "application/json" } });
|
|
3362
3516
|
}
|
|
3363
|
-
|
|
3517
|
+
logger.info("[ConsentService] ✅ Credential authentication successful");
|
|
3518
|
+
// Log credential auth success for audit trail
|
|
3519
|
+
// Note: userDid will be resolved after identity resolution, so we don't include it here
|
|
3520
|
+
const auditService = await this.getAuditService(project_id);
|
|
3521
|
+
if (auditService) {
|
|
3522
|
+
await auditService
|
|
3523
|
+
.logCredentialAuthSuccess({
|
|
3524
|
+
sessionId: session_id,
|
|
3525
|
+
agentDid: agent_did,
|
|
3526
|
+
targetTools: [tool],
|
|
3527
|
+
scopes: scopes,
|
|
3528
|
+
projectId: project_id,
|
|
3529
|
+
provider: provider,
|
|
3530
|
+
// userDid will be available after identity resolution
|
|
3531
|
+
})
|
|
3532
|
+
.catch((err) => {
|
|
3533
|
+
logger.warn("[ConsentService] Failed to log credential auth success:", err);
|
|
3534
|
+
});
|
|
3535
|
+
}
|
|
3364
3536
|
// 3. Resolve identity via AgentShield
|
|
3365
3537
|
const identityResult = await this.resolveCredentialIdentity({
|
|
3366
3538
|
projectId: project_id,
|
|
@@ -3376,7 +3548,7 @@ export class ConsentService {
|
|
|
3376
3548
|
error_code: "identity_resolution_failed",
|
|
3377
3549
|
}), { status: 500, headers: { "Content-Type": "application/json" } });
|
|
3378
3550
|
}
|
|
3379
|
-
|
|
3551
|
+
logger.info("[ConsentService] ✅ Identity resolved", {
|
|
3380
3552
|
userDid: identityResult.userDid.substring(0, 30) + "...",
|
|
3381
3553
|
});
|
|
3382
3554
|
// NEW: Update session cache with userDid for proper delegation scoping
|
|
@@ -3384,7 +3556,7 @@ export class ConsentService {
|
|
|
3384
3556
|
// instead of falling back to legacy agent-only keys
|
|
3385
3557
|
await this.updateSessionWithIdentity(session_id, identityResult.userDid, null // No OAuth identity for credential auth
|
|
3386
3558
|
);
|
|
3387
|
-
|
|
3559
|
+
logger.info("[ConsentService] ✅ Session updated with identity");
|
|
3388
3560
|
// 4. Store token in IdpTokenStorage with usage metadata
|
|
3389
3561
|
// Include userId from authResult for ToolExecutionContext.userId
|
|
3390
3562
|
await this.storeCredentialToken({
|
|
@@ -3396,48 +3568,59 @@ export class ConsentService {
|
|
|
3396
3568
|
scopes,
|
|
3397
3569
|
userId: authResult.userId, // Pass extracted userId from credential provider response
|
|
3398
3570
|
});
|
|
3399
|
-
|
|
3400
|
-
//
|
|
3401
|
-
//
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3571
|
+
logger.info("[ConsentService] ✅ Token stored");
|
|
3572
|
+
// ================================================================================
|
|
3573
|
+
// 5. REDIRECT TO CLICKWRAP PAGE (3-screen flow)
|
|
3574
|
+
// ================================================================================
|
|
3575
|
+
// Flow: Credential Auth → Clickwrap (consent-only UI) → Success
|
|
3576
|
+
//
|
|
3577
|
+
// Instead of creating the delegation here, redirect to the clickwrap page
|
|
3578
|
+
// where the user can see their identity and approve the permission request.
|
|
3579
|
+
// The clickwrap approval will then create the delegation with the stored
|
|
3580
|
+
// credential auth info.
|
|
3581
|
+
//
|
|
3582
|
+
// This matches the OAuth flow pattern for consistency.
|
|
3583
|
+
// ================================================================================
|
|
3584
|
+
const serverUrl = this.env.MCP_SERVER_URL || "";
|
|
3585
|
+
// Build clickwrap URL with all necessary parameters
|
|
3586
|
+
const clickwrapUrl = new URL(`${serverUrl}/consent`);
|
|
3587
|
+
// Mode is consent-only since credential auth is complete
|
|
3588
|
+
clickwrapUrl.searchParams.set("mode", "consent-only");
|
|
3589
|
+
// CRITICAL: Pass provider_type so delegation is created with 'password' not 'none'
|
|
3590
|
+
// This ensures authorization.type matches tool protection expectation
|
|
3591
|
+
clickwrapUrl.searchParams.set("credential_provider_type", CONSENT_PROVIDER_TYPES.PASSWORD);
|
|
3592
|
+
clickwrapUrl.searchParams.set("credential_provider", provider);
|
|
3593
|
+
// Preserve important state from credential flow
|
|
3594
|
+
clickwrapUrl.searchParams.set("project_id", project_id);
|
|
3595
|
+
clickwrapUrl.searchParams.set("agent_did", agent_did);
|
|
3596
|
+
clickwrapUrl.searchParams.set("session_id", session_id);
|
|
3597
|
+
// Include tool and scopes for display
|
|
3598
|
+
clickwrapUrl.searchParams.set("tool", tool);
|
|
3599
|
+
if (Array.isArray(scopes) && scopes.length > 0) {
|
|
3600
|
+
clickwrapUrl.searchParams.set("scopes", scopes.join(","));
|
|
3423
3601
|
}
|
|
3424
|
-
|
|
3425
|
-
|
|
3602
|
+
// Include user info for display on clickwrap page
|
|
3603
|
+
if (authResult.userEmail) {
|
|
3604
|
+
clickwrapUrl.searchParams.set("credential_user_email", authResult.userEmail);
|
|
3605
|
+
}
|
|
3606
|
+
if (authResult.userId) {
|
|
3607
|
+
clickwrapUrl.searchParams.set("credential_user_id", authResult.userId);
|
|
3608
|
+
}
|
|
3609
|
+
logger.debug("[ConsentService] ✅ Credential auth complete, redirecting to clickwrap", {
|
|
3610
|
+
sessionId: session_id.substring(0, 20) + "...",
|
|
3611
|
+
userDid: identityResult.userDid.substring(0, 30) + "...",
|
|
3612
|
+
provider: provider,
|
|
3613
|
+
clickwrapUrl: clickwrapUrl.toString().substring(0, 100) + "...",
|
|
3426
3614
|
});
|
|
3427
|
-
//
|
|
3428
|
-
await this.storeDelegationToken(session_id, agent_did, delegationResult.delegation_token, delegationResult.delegation_id);
|
|
3429
|
-
// Return success with redirect URL
|
|
3430
|
-
const serverUrl = this.env.MCP_SERVER_URL || "";
|
|
3431
|
-
const redirectUrl = `${serverUrl}/consent/success?delegation_id=${encodeURIComponent(delegationResult.delegation_id)}&project_id=${encodeURIComponent(project_id)}`;
|
|
3615
|
+
// Return success with redirect to clickwrap page
|
|
3432
3616
|
return new Response(JSON.stringify({
|
|
3433
3617
|
success: true,
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
redirectUrl,
|
|
3618
|
+
redirectUrl: clickwrapUrl.toString(),
|
|
3619
|
+
// Note: delegation_id is NOT returned here - it will be created on clickwrap approval
|
|
3437
3620
|
}), { status: 200, headers: { "Content-Type": "application/json" } });
|
|
3438
3621
|
}
|
|
3439
3622
|
catch (error) {
|
|
3440
|
-
|
|
3623
|
+
logger.error("[ConsentService] Credential approval error:", error);
|
|
3441
3624
|
return new Response(JSON.stringify({
|
|
3442
3625
|
success: false,
|
|
3443
3626
|
error: error instanceof Error ? error.message : "Internal error",
|
|
@@ -3469,7 +3652,7 @@ export class ConsentService {
|
|
|
3469
3652
|
}
|
|
3470
3653
|
}
|
|
3471
3654
|
catch {
|
|
3472
|
-
|
|
3655
|
+
logger.warn("[ConsentService] Failed to parse scopes JSON:", rawScopes);
|
|
3473
3656
|
}
|
|
3474
3657
|
}
|
|
3475
3658
|
// Single string scope - wrap in array
|
|
@@ -3491,7 +3674,7 @@ export class ConsentService {
|
|
|
3491
3674
|
const agentShieldUrl = this.env.AGENTSHIELD_API_URL || DEFAULT_AGENTSHIELD_URL;
|
|
3492
3675
|
const apiKey = this.env.AGENTSHIELD_API_KEY;
|
|
3493
3676
|
if (!apiKey) {
|
|
3494
|
-
|
|
3677
|
+
logger.warn("[ConsentService] No AgentShield API key configured");
|
|
3495
3678
|
return null;
|
|
3496
3679
|
}
|
|
3497
3680
|
try {
|
|
@@ -3505,7 +3688,7 @@ export class ConsentService {
|
|
|
3505
3688
|
},
|
|
3506
3689
|
});
|
|
3507
3690
|
if (!response.ok) {
|
|
3508
|
-
|
|
3691
|
+
logger.warn("[ConsentService] Providers API returned error:", {
|
|
3509
3692
|
status: response.status,
|
|
3510
3693
|
statusText: response.statusText,
|
|
3511
3694
|
});
|
|
@@ -3515,7 +3698,7 @@ export class ConsentService {
|
|
|
3515
3698
|
// { success: true, data: { providers: { ... }, configuredProvider: "..." } }
|
|
3516
3699
|
const result = (await response.json());
|
|
3517
3700
|
if (!result.success || !result.data?.providers) {
|
|
3518
|
-
|
|
3701
|
+
logger.warn("[ConsentService] Invalid providers response:", {
|
|
3519
3702
|
hasSuccess: result.success,
|
|
3520
3703
|
hasData: !!result.data,
|
|
3521
3704
|
hasProviders: !!result.data?.providers,
|
|
@@ -3530,13 +3713,13 @@ export class ConsentService {
|
|
|
3530
3713
|
// AgentShield API may return snake_case fields from database
|
|
3531
3714
|
const provider = this.mapCredentialProviderFields(rawProvider, providerName);
|
|
3532
3715
|
if (!provider) {
|
|
3533
|
-
|
|
3716
|
+
logger.debug("[ConsentService] Credential provider validation failed:", {
|
|
3534
3717
|
projectId,
|
|
3535
3718
|
providerName,
|
|
3536
3719
|
});
|
|
3537
3720
|
return null;
|
|
3538
3721
|
}
|
|
3539
|
-
|
|
3722
|
+
logger.debug("[ConsentService] Found credential provider:", {
|
|
3540
3723
|
providerName,
|
|
3541
3724
|
hasAuthEndpoint: !!provider.authEndpoint,
|
|
3542
3725
|
hasResponseFields: !!provider.responseFields,
|
|
@@ -3547,7 +3730,7 @@ export class ConsentService {
|
|
|
3547
3730
|
return null;
|
|
3548
3731
|
}
|
|
3549
3732
|
catch (error) {
|
|
3550
|
-
|
|
3733
|
+
logger.error("[ConsentService] Failed to fetch provider config:", error);
|
|
3551
3734
|
return null;
|
|
3552
3735
|
}
|
|
3553
3736
|
}
|
|
@@ -3619,7 +3802,7 @@ export class ConsentService {
|
|
|
3619
3802
|
raw.auth_endpoint ??
|
|
3620
3803
|
"");
|
|
3621
3804
|
if (!authEndpoint) {
|
|
3622
|
-
|
|
3805
|
+
logger.debug("[ConsentService] Credential provider missing required authEndpoint", { providerName: providerName ?? "unknown" });
|
|
3623
3806
|
return null;
|
|
3624
3807
|
}
|
|
3625
3808
|
return {
|
|
@@ -3646,7 +3829,7 @@ export class ConsentService {
|
|
|
3646
3829
|
const agentShieldUrl = this.env.AGENTSHIELD_API_URL || DEFAULT_AGENTSHIELD_URL;
|
|
3647
3830
|
const apiKey = this.env.AGENTSHIELD_API_KEY;
|
|
3648
3831
|
if (!apiKey) {
|
|
3649
|
-
|
|
3832
|
+
logger.warn("[ConsentService] No AgentShield API key for identity resolution");
|
|
3650
3833
|
return { success: false };
|
|
3651
3834
|
}
|
|
3652
3835
|
try {
|
|
@@ -3676,7 +3859,7 @@ export class ConsentService {
|
|
|
3676
3859
|
};
|
|
3677
3860
|
}
|
|
3678
3861
|
catch (error) {
|
|
3679
|
-
|
|
3862
|
+
logger.error("[ConsentService] Identity resolution failed:", error);
|
|
3680
3863
|
return { success: false };
|
|
3681
3864
|
}
|
|
3682
3865
|
}
|
|
@@ -3686,7 +3869,7 @@ export class ConsentService {
|
|
|
3686
3869
|
async storeCredentialToken(params) {
|
|
3687
3870
|
const delegationStorage = this.env.DELEGATION_STORAGE;
|
|
3688
3871
|
if (!delegationStorage) {
|
|
3689
|
-
|
|
3872
|
+
logger.warn("[ConsentService] No DELEGATION_STORAGE for credential tokens");
|
|
3690
3873
|
return;
|
|
3691
3874
|
}
|
|
3692
3875
|
const oauthSecurityService = new OAuthSecurityService(delegationStorage, this.env.OAUTH_ENCRYPTION_SECRET);
|
|
@@ -3724,7 +3907,7 @@ export class ConsentService {
|
|
|
3724
3907
|
async validateCredentialCsrfToken(token, sessionId) {
|
|
3725
3908
|
const delegationStorage = this.env.DELEGATION_STORAGE;
|
|
3726
3909
|
if (!delegationStorage) {
|
|
3727
|
-
|
|
3910
|
+
logger.warn("[ConsentService] No DELEGATION_STORAGE for CSRF validation, skipping");
|
|
3728
3911
|
// Return true to allow flow to continue (graceful degradation)
|
|
3729
3912
|
// This matches OAuth behavior when storage is unavailable
|
|
3730
3913
|
return true;
|
|
@@ -3733,7 +3916,7 @@ export class ConsentService {
|
|
|
3733
3916
|
const csrfKey = STORAGE_KEYS.credentialCsrf(sessionId);
|
|
3734
3917
|
const storedToken = await delegationStorage.get(csrfKey);
|
|
3735
3918
|
if (!storedToken) {
|
|
3736
|
-
|
|
3919
|
+
logger.warn("[ConsentService] No stored CSRF token found");
|
|
3737
3920
|
return false;
|
|
3738
3921
|
}
|
|
3739
3922
|
// Constant-time comparison to prevent timing attacks
|
|
@@ -3745,7 +3928,7 @@ export class ConsentService {
|
|
|
3745
3928
|
return isValid;
|
|
3746
3929
|
}
|
|
3747
3930
|
catch (error) {
|
|
3748
|
-
|
|
3931
|
+
logger.error("[ConsentService] CSRF validation error:", error);
|
|
3749
3932
|
return false;
|
|
3750
3933
|
}
|
|
3751
3934
|
}
|
|
@@ -3766,7 +3949,7 @@ export class ConsentService {
|
|
|
3766
3949
|
.replace(/\//g, "_")
|
|
3767
3950
|
.replace(/=/g, "");
|
|
3768
3951
|
if (!delegationStorage) {
|
|
3769
|
-
|
|
3952
|
+
logger.warn("[ConsentService] No DELEGATION_STORAGE for CSRF storage");
|
|
3770
3953
|
// Return token anyway - form will submit but validation will skip
|
|
3771
3954
|
return token;
|
|
3772
3955
|
}
|
|
@@ -3774,10 +3957,10 @@ export class ConsentService {
|
|
|
3774
3957
|
const csrfKey = STORAGE_KEYS.credentialCsrf(sessionId);
|
|
3775
3958
|
// 10 minute TTL matches OAuth state TTL
|
|
3776
3959
|
await delegationStorage.put(csrfKey, token, { expirationTtl: 600 });
|
|
3777
|
-
|
|
3960
|
+
logger.debug("[ConsentService] CSRF token stored for credential form");
|
|
3778
3961
|
}
|
|
3779
3962
|
catch (error) {
|
|
3780
|
-
|
|
3963
|
+
logger.error("[ConsentService] CSRF token storage error:", error);
|
|
3781
3964
|
}
|
|
3782
3965
|
return token;
|
|
3783
3966
|
}
|
|
@@ -3864,26 +4047,40 @@ export class ConsentService {
|
|
|
3864
4047
|
// user_identifier can be the same as user_did or a human-readable version
|
|
3865
4048
|
// If we have a human-readable identifier from OAuth, use it; otherwise use DID
|
|
3866
4049
|
simplifiedRequest.user_identifier = userDid;
|
|
3867
|
-
|
|
4050
|
+
logger.debug("[ConsentService] Including user_did in delegation request:", {
|
|
3868
4051
|
user_did: userDid.substring(0, 20) + "...",
|
|
3869
4052
|
});
|
|
3870
4053
|
}
|
|
3871
4054
|
else {
|
|
3872
|
-
|
|
4055
|
+
logger.debug("[ConsentService] No user_did (session is anonymous) - delegation will proceed without it");
|
|
3873
4056
|
}
|
|
3874
4057
|
// Phase 2 VC-Only: Include credential_jwt if available
|
|
3875
4058
|
if (credentialJwt) {
|
|
3876
4059
|
simplifiedRequest.credential_jwt = credentialJwt;
|
|
3877
|
-
|
|
4060
|
+
logger.debug("[ConsentService] Including credential_jwt in delegation request");
|
|
3878
4061
|
}
|
|
3879
4062
|
// AgentShield API only accepts "custom_fields", not "metadata"
|
|
3880
4063
|
// Always use "custom_fields" regardless of Day0 config
|
|
3881
4064
|
// Include provider_type and provider for dashboard authorization display
|
|
4065
|
+
//
|
|
4066
|
+
// CRITICAL FIX: request.provider_type is at the TOP LEVEL of ConsentApprovalRequest,
|
|
4067
|
+
// NOT inside customFields. We need to read from both locations:
|
|
4068
|
+
// - request.provider_type (set by handleCredentialApproval, handleApproval, etc.)
|
|
4069
|
+
// - customFieldsFromRequest.provider (set in customFields for backward compat)
|
|
3882
4070
|
const customFieldsFromRequest = request.customFields || {};
|
|
3883
|
-
|
|
4071
|
+
// FIX: Read provider_type from request root (where handleCredentialApproval sets it)
|
|
4072
|
+
// Fall back to customFields for backward compatibility
|
|
4073
|
+
const providerType = request.provider_type || customFieldsFromRequest.provider_type;
|
|
4074
|
+
const provider = customFieldsFromRequest.provider;
|
|
4075
|
+
const hasProviderInfo = providerType || provider;
|
|
3884
4076
|
if (userDid || hasProviderInfo) {
|
|
3885
4077
|
// Include issuer_did and subject_did in custom_fields when we have userDid
|
|
3886
4078
|
// Also include provider_type and provider for AgentShield dashboard
|
|
4079
|
+
// CRITICAL: AgentShield derives authorization from metadata.provider_type:
|
|
4080
|
+
// - provider_type === 'password' → authorization.type = 'password' (credential auth)
|
|
4081
|
+
// - provider_type === 'none' → authorization.type = 'none' (consent-only)
|
|
4082
|
+
// - oauth_provider present → authorization.type = 'oauth'
|
|
4083
|
+
// NOTE: Tool protection expects 'password' for credential flows, NOT 'credential'
|
|
3887
4084
|
simplifiedRequest.custom_fields = {
|
|
3888
4085
|
...(userDid
|
|
3889
4086
|
? {
|
|
@@ -3892,13 +4089,18 @@ export class ConsentService {
|
|
|
3892
4089
|
}
|
|
3893
4090
|
: {}),
|
|
3894
4091
|
format_version: "simplified_v1",
|
|
3895
|
-
//
|
|
4092
|
+
// Include provider_type at the custom_fields level (where AgentShield reads it)
|
|
4093
|
+
...(providerType ? { provider_type: providerType } : {}),
|
|
4094
|
+
// Merge with any existing custom_fields from request
|
|
3896
4095
|
...customFieldsFromRequest,
|
|
3897
4096
|
};
|
|
3898
4097
|
if (hasProviderInfo) {
|
|
3899
|
-
|
|
3900
|
-
provider_type:
|
|
3901
|
-
provider:
|
|
4098
|
+
logger.debug("[ConsentService] Including provider info in custom_fields:", {
|
|
4099
|
+
provider_type: providerType,
|
|
4100
|
+
provider: provider,
|
|
4101
|
+
source: request.provider_type
|
|
4102
|
+
? "request.provider_type"
|
|
4103
|
+
: "customFields",
|
|
3902
4104
|
});
|
|
3903
4105
|
}
|
|
3904
4106
|
}
|
|
@@ -3929,7 +4131,7 @@ export class ConsentService {
|
|
|
3929
4131
|
return fullResponse;
|
|
3930
4132
|
}
|
|
3931
4133
|
// Full format failed with validation error, try simplified
|
|
3932
|
-
|
|
4134
|
+
logger.debug("[ConsentService] Full format failed, trying simplified format...");
|
|
3933
4135
|
const simplifiedResponse = await this.makeAPICall(agentShieldUrl, apiKey, request.simplifiedFormat);
|
|
3934
4136
|
if (simplifiedResponse.success) {
|
|
3935
4137
|
await this.cacheFormatPreference("simplified");
|
|
@@ -3951,11 +4153,11 @@ export class ConsentService {
|
|
|
3951
4153
|
// This provides defense-in-depth protection
|
|
3952
4154
|
const sanitizedBody = { ...requestBody };
|
|
3953
4155
|
if ("session_id" in sanitizedBody) {
|
|
3954
|
-
|
|
4156
|
+
logger.warn("[ConsentService] ⚠️ session_id detected in request body - removing (not in schema)");
|
|
3955
4157
|
delete sanitizedBody.session_id;
|
|
3956
4158
|
}
|
|
3957
4159
|
if ("project_id" in sanitizedBody) {
|
|
3958
|
-
|
|
4160
|
+
logger.warn("[ConsentService] ⚠️ project_id detected in request body - removing (not in schema)");
|
|
3959
4161
|
delete sanitizedBody.project_id;
|
|
3960
4162
|
}
|
|
3961
4163
|
const response = await fetch(`${agentShieldUrl}${AGENTSHIELD_ENDPOINTS.DELEGATIONS_CREATE}`, {
|
|
@@ -4009,7 +4211,7 @@ export class ConsentService {
|
|
|
4009
4211
|
};
|
|
4010
4212
|
}
|
|
4011
4213
|
catch (error) {
|
|
4012
|
-
|
|
4214
|
+
logger.error("[ConsentService] API call failed:", error);
|
|
4013
4215
|
return {
|
|
4014
4216
|
success: false,
|
|
4015
4217
|
error: error instanceof Error ? error.message : "Network request failed",
|
|
@@ -4031,10 +4233,10 @@ export class ConsentService {
|
|
|
4031
4233
|
}), {
|
|
4032
4234
|
expirationTtl: 3600, // 1 hour
|
|
4033
4235
|
});
|
|
4034
|
-
|
|
4236
|
+
logger.debug(`[ConsentService] Cached format preference: ${format}`);
|
|
4035
4237
|
}
|
|
4036
4238
|
catch (error) {
|
|
4037
|
-
|
|
4239
|
+
logger.warn("[ConsentService] Failed to cache format preference:", error);
|
|
4038
4240
|
}
|
|
4039
4241
|
}
|
|
4040
4242
|
}
|