@kya-os/mcp-i-cloudflare 1.6.23-canary.0 → 1.6.24
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/runtime/oauth-handler.d.ts +10 -0
- package/dist/runtime/oauth-handler.d.ts.map +1 -1
- package/dist/runtime/oauth-handler.js +135 -3
- package/dist/runtime/oauth-handler.js.map +1 -1
- package/dist/services/consent-audit.service.d.ts.map +1 -1
- package/dist/services/consent-audit.service.js +2 -0
- package/dist/services/consent-audit.service.js.map +1 -1
- package/dist/services/consent.service.d.ts +78 -2
- package/dist/services/consent.service.d.ts.map +1 -1
- package/dist/services/consent.service.js +402 -112
- package/dist/services/consent.service.js.map +1 -1
- package/dist/services/delegation.service.d.ts +25 -1
- package/dist/services/delegation.service.d.ts.map +1 -1
- package/dist/services/delegation.service.js +142 -1
- package/dist/services/delegation.service.js.map +1 -1
- package/dist/utils/client-info.d.ts +69 -0
- package/dist/utils/client-info.d.ts.map +1 -0
- package/dist/utils/client-info.js +178 -0
- package/dist/utils/client-info.js.map +1 -0
- package/dist/utils/error-formatter.d.ts +103 -0
- package/dist/utils/error-formatter.d.ts.map +1 -0
- package/dist/utils/error-formatter.js +245 -0
- package/dist/utils/error-formatter.js.map +1 -0
- package/dist/utils/initialize-context.d.ts +91 -0
- package/dist/utils/initialize-context.d.ts.map +1 -0
- package/dist/utils/initialize-context.js +169 -0
- package/dist/utils/initialize-context.js.map +1 -0
- package/dist/utils/oauth-identity.d.ts +58 -0
- package/dist/utils/oauth-identity.d.ts.map +1 -0
- package/dist/utils/oauth-identity.js +215 -0
- package/dist/utils/oauth-identity.js.map +1 -0
- package/package.json +4 -4
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
233
|
-
|
|
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
|
-
//
|
|
263
|
+
// Update session storage with resolved identity
|
|
242
264
|
try {
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
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
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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.
|
|
311
|
-
|
|
382
|
+
console.error("[ConsentService] Failed to issue VC:", error);
|
|
383
|
+
return null;
|
|
312
384
|
}
|
|
313
|
-
|
|
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
|
|
633
|
-
|
|
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
|
|
831
|
+
// Continue to check session
|
|
652
832
|
}
|
|
653
|
-
//
|
|
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 (
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
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) {
|