@kya-os/verifier 1.5.9 → 1.6.1

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/core.js ADDED
@@ -0,0 +1,868 @@
1
+ import { importJWK, jwtVerify } from "jose";
2
+ import { canonicalize } from "json-canonicalize";
3
+ import { AGENT_HEADERS, ERROR_HTTP_STATUS, VERIFIER_ERROR_CODES } from "@kya-os/contracts/verifier";
4
+ /**
5
+ * Isomorphic verifier core for XMCP-I proof validation
6
+ *
7
+ * This is the heart of the trust system - it verifies that AI agents
8
+ * are who they claim to be and have the authority to perform actions.
9
+ */
10
+ export class VerifierCore {
11
+ constructor(config = {}) {
12
+ this.didCache = new Map();
13
+ this.delegationCache = new Map();
14
+ this.config = {
15
+ ktaBaseUrl: config.ktaBaseUrl || "https://knowthat.ai",
16
+ enableDelegationCheck: config.enableDelegationCheck ?? true,
17
+ clockSkewTolerance: config.clockSkewTolerance ?? 300, // 5 minutes (generous for clock drift)
18
+ sessionTimeout: config.sessionTimeout ?? 1800, // 30 minutes
19
+ proofMaxAge: config.proofMaxAge ?? 600, // 10 minutes (must be > clockSkewTolerance)
20
+ allowMockData: config.allowMockData ?? false,
21
+ didCacheTtl: config.didCacheTtl ?? 300, // 5 minutes
22
+ delegationCacheTtl: config.delegationCacheTtl ?? 60, // 1 minute
23
+ };
24
+ }
25
+ /**
26
+ * Verify a detached proof and return verification result
27
+ *
28
+ * This is the main entry point for proof verification. It performs
29
+ * a comprehensive validation of the agent's identity and authorization.
30
+ */
31
+ async verify(context) {
32
+ const startTime = Date.now();
33
+ try {
34
+ // 1. Validate proof structure
35
+ const structureError = this.validateProofStructure(context.proof);
36
+ if (structureError) {
37
+ this.logVerificationAttempt(context, false, structureError.code);
38
+ return this.createErrorResult(structureError);
39
+ }
40
+ // 2. Verify timestamp and session validity
41
+ const timestampError = this.validateTimestamp(context.proof.meta, context.timestamp);
42
+ if (timestampError) {
43
+ this.logVerificationAttempt(context, false, timestampError.code);
44
+ return this.createErrorResult(timestampError);
45
+ }
46
+ // 3. Verify audience matches
47
+ const audienceError = this.validateAudience(context.proof.meta, context.audience);
48
+ if (audienceError) {
49
+ this.logVerificationAttempt(context, false, audienceError.code);
50
+ return this.createErrorResult(audienceError);
51
+ }
52
+ // 4. Verify Ed25519 signature (for full JWS, also validates meta matches signed payload)
53
+ const signatureError = await this.verifySignature(context.proof, context.audience);
54
+ if (signatureError) {
55
+ this.logVerificationAttempt(context, false, signatureError.code);
56
+ return this.createErrorResult(signatureError);
57
+ }
58
+ // 5. Check delegation if enabled and present
59
+ if (this.config.enableDelegationCheck &&
60
+ context.proof.meta.delegationRef) {
61
+ const delegationError = await this.verifyDelegation(context.proof.meta.delegationRef, context.proof.meta.did, context.proof.meta.kid);
62
+ if (delegationError) {
63
+ this.logVerificationAttempt(context, false, delegationError.code);
64
+ return this.createErrorResult(delegationError);
65
+ }
66
+ }
67
+ // 6. Generate trusted headers and context
68
+ const headers = this.generateHeaders(context.proof.meta);
69
+ const agentContext = this.generateAgentContext(context.proof.meta);
70
+ const duration = Date.now() - startTime;
71
+ this.logVerificationAttempt(context, true, "SUCCESS", duration);
72
+ return {
73
+ success: true,
74
+ headers,
75
+ agentContext,
76
+ };
77
+ }
78
+ catch (error) {
79
+ const duration = Date.now() - startTime;
80
+ this.logVerificationAttempt(context, false, "UNEXPECTED_ERROR", duration);
81
+ return this.createErrorResult({
82
+ code: "XMCP_I_EVERIFY",
83
+ message: error instanceof Error ? error.message : "Verification failed",
84
+ httpStatus: 500,
85
+ details: {
86
+ reason: "Unexpected error during verification",
87
+ remediation: "Check proof format and try again",
88
+ },
89
+ });
90
+ }
91
+ }
92
+ /**
93
+ * Validate proof structure with comprehensive checks
94
+ */
95
+ validateProofStructure(proof) {
96
+ if (!proof.jws || typeof proof.jws !== "string") {
97
+ return {
98
+ code: "XMCP_I_EBADPROOF",
99
+ message: "Invalid proof: missing or invalid JWS",
100
+ httpStatus: 400,
101
+ details: {
102
+ reason: "JWS field is required and must be a string",
103
+ expected: "string",
104
+ received: typeof proof.jws,
105
+ },
106
+ };
107
+ }
108
+ if (!proof.meta || typeof proof.meta !== "object") {
109
+ return {
110
+ code: "XMCP_I_EBADPROOF",
111
+ message: "Invalid proof: missing or invalid meta",
112
+ httpStatus: 400,
113
+ details: {
114
+ reason: "Meta field is required and must be an object",
115
+ expected: "object",
116
+ received: typeof proof.meta,
117
+ },
118
+ };
119
+ }
120
+ const requiredFields = [
121
+ "did",
122
+ "kid",
123
+ "ts",
124
+ "nonce",
125
+ "audience",
126
+ "sessionId",
127
+ "requestHash",
128
+ "responseHash",
129
+ ];
130
+ for (const field of requiredFields) {
131
+ if (!proof.meta[field]) {
132
+ return {
133
+ code: "XMCP_I_EBADPROOF",
134
+ message: `Invalid proof: missing required field '${field}'`,
135
+ httpStatus: 400,
136
+ details: {
137
+ reason: `Field '${field}' is required in proof meta`,
138
+ expected: "non-empty value",
139
+ received: proof.meta[field],
140
+ },
141
+ };
142
+ }
143
+ }
144
+ // Validate hash format
145
+ const hashRegex = /^sha256:[a-f0-9]{64}$/;
146
+ if (!hashRegex.test(proof.meta.requestHash)) {
147
+ return {
148
+ code: "XMCP_I_EBADPROOF",
149
+ message: "Invalid proof: malformed requestHash",
150
+ httpStatus: 400,
151
+ details: {
152
+ reason: "requestHash must be in format 'sha256:<64-char-hex>'",
153
+ expected: "sha256:[a-f0-9]{64}",
154
+ received: proof.meta.requestHash,
155
+ },
156
+ };
157
+ }
158
+ if (!hashRegex.test(proof.meta.responseHash)) {
159
+ return {
160
+ code: "XMCP_I_EBADPROOF",
161
+ message: "Invalid proof: malformed responseHash",
162
+ httpStatus: 400,
163
+ details: {
164
+ reason: "responseHash must be in format 'sha256:<64-char-hex>'",
165
+ expected: "sha256:[a-f0-9]{64}",
166
+ received: proof.meta.responseHash,
167
+ },
168
+ };
169
+ }
170
+ // Validate DID format
171
+ if (!proof.meta.did.startsWith("did:")) {
172
+ return {
173
+ code: "XMCP_I_EBADPROOF",
174
+ message: "Invalid proof: malformed DID",
175
+ httpStatus: 400,
176
+ details: {
177
+ reason: "DID must start with 'did:'",
178
+ expected: "did:*",
179
+ received: proof.meta.did,
180
+ },
181
+ };
182
+ }
183
+ return null;
184
+ }
185
+ /**
186
+ * Validate timestamp with configurable clock skew tolerance
187
+ */
188
+ validateTimestamp(meta, currentTimestamp) {
189
+ // Validate meta.ts is not NaN (prevents client-controlled NaN attacks)
190
+ if (!Number.isFinite(meta.ts)) {
191
+ return {
192
+ code: VERIFIER_ERROR_CODES.PROOF_INVALID_TS,
193
+ message: "Invalid proof: timestamp is not a valid number",
194
+ httpStatus: ERROR_HTTP_STATUS[VERIFIER_ERROR_CODES.PROOF_INVALID_TS],
195
+ details: {
196
+ reason: "Proof timestamp must be a finite number",
197
+ received: meta.ts,
198
+ remediation: "Ensure proof timestamp is properly formatted",
199
+ },
200
+ };
201
+ }
202
+ const now = currentTimestamp || Math.floor(Date.now() / 1000);
203
+ // Validate server timestamp is valid (defense in depth)
204
+ if (!Number.isFinite(now)) {
205
+ return {
206
+ code: VERIFIER_ERROR_CODES.SERVER_TIME_INVALID,
207
+ message: "Server timestamp validation failed",
208
+ httpStatus: ERROR_HTTP_STATUS[VERIFIER_ERROR_CODES.SERVER_TIME_INVALID],
209
+ details: {
210
+ reason: "Server time is not valid",
211
+ remediation: "Contact system administrator",
212
+ },
213
+ };
214
+ }
215
+ const skew = this.config.clockSkewTolerance;
216
+ // Check for future timestamps
217
+ if (meta.ts > now + skew) {
218
+ return {
219
+ code: VERIFIER_ERROR_CODES.PROOF_FUTURE_TS,
220
+ message: "Invalid proof: timestamp is in the future",
221
+ httpStatus: ERROR_HTTP_STATUS[VERIFIER_ERROR_CODES.PROOF_FUTURE_TS],
222
+ details: {
223
+ reason: `Timestamp ${meta.ts} is ${meta.ts - now}s in the future (skew: ${skew}s)`,
224
+ expected: `≤ ${now + skew}`,
225
+ received: meta.ts,
226
+ remediation: "Check client clock sync",
227
+ },
228
+ };
229
+ }
230
+ // Check for past timestamps (clock skew exceeded)
231
+ if (meta.ts < now - skew) {
232
+ return {
233
+ code: VERIFIER_ERROR_CODES.PROOF_SKEW_EXCEEDED,
234
+ message: "Invalid proof: timestamp outside acceptable range",
235
+ httpStatus: ERROR_HTTP_STATUS[VERIFIER_ERROR_CODES.PROOF_SKEW_EXCEEDED],
236
+ details: {
237
+ reason: `Timestamp ${meta.ts} is ${now - meta.ts}s in the past (skew: ${skew}s)`,
238
+ expected: `≥ ${now - skew}`,
239
+ received: meta.ts,
240
+ remediation: "Check NTP sync; adjust XMCP_I_TS_SKEW_SEC if needed",
241
+ },
242
+ };
243
+ }
244
+ // Check proof age if configured
245
+ if (this.config.proofMaxAge) {
246
+ const age = now - meta.ts;
247
+ if (age > this.config.proofMaxAge) {
248
+ return {
249
+ code: VERIFIER_ERROR_CODES.PROOF_TOO_OLD,
250
+ message: "Invalid proof: proof is too old",
251
+ httpStatus: ERROR_HTTP_STATUS[VERIFIER_ERROR_CODES.PROOF_TOO_OLD],
252
+ details: {
253
+ reason: `Proof age ${age}s exceeds maximum ${this.config.proofMaxAge}s`,
254
+ expected: `Less than ${this.config.proofMaxAge}s old`,
255
+ received: `${age}s old`,
256
+ remediation: "Generate a fresh proof",
257
+ },
258
+ };
259
+ }
260
+ }
261
+ return null;
262
+ }
263
+ /**
264
+ * Validate audience matches expected value
265
+ */
266
+ validateAudience(meta, expectedAudience) {
267
+ if (meta.audience !== expectedAudience) {
268
+ return {
269
+ code: "XMCP_I_EHANDSHAKE",
270
+ message: "Invalid proof: audience mismatch",
271
+ httpStatus: 401,
272
+ details: {
273
+ reason: "Proof audience does not match request audience",
274
+ expected: expectedAudience,
275
+ received: meta.audience,
276
+ },
277
+ };
278
+ }
279
+ return null;
280
+ }
281
+ /**
282
+ * Verify Ed25519 signature using JOSE with proper detached JWS handling
283
+ *
284
+ * This is the cryptographic heart of the verification process.
285
+ * It ensures the proof was signed by the claimed identity.
286
+ *
287
+ * CRITICAL for full JWS: proof.meta must match the signed JWT payload.
288
+ * Otherwise an attacker could tamper with meta (audience, timestamp, nonce)
289
+ * while keeping a valid signature. We validate meta against the decoded
290
+ * payload before trusting meta for any security decisions.
291
+ */
292
+ async verifySignature(proof, expectedAudience) {
293
+ try {
294
+ // For testing with mock data, skip actual signature verification
295
+ if (this.config.allowMockData && proof.meta.did.startsWith("did:test:")) {
296
+ // Parse JWS components for basic validation (support both full and detached formats)
297
+ const jwsParts = proof.jws.split(".");
298
+ if (jwsParts.length !== 3) {
299
+ return {
300
+ code: "XMCP_I_EBADPROOF",
301
+ message: "Invalid JWS format",
302
+ httpStatus: 403,
303
+ details: {
304
+ reason: "JWS must have 3 parts (header.payload.signature)",
305
+ expected: "3 parts",
306
+ received: `${jwsParts.length} parts`,
307
+ },
308
+ };
309
+ }
310
+ const [headerB64] = jwsParts;
311
+ const header = JSON.parse(Buffer.from(headerB64, "base64url").toString());
312
+ if (header.alg !== "EdDSA") {
313
+ return {
314
+ code: "XMCP_I_EBADPROOF",
315
+ message: "Invalid JWS: unsupported algorithm",
316
+ httpStatus: 403,
317
+ details: {
318
+ reason: "Only EdDSA algorithm is supported",
319
+ expected: "EdDSA",
320
+ received: header.alg,
321
+ },
322
+ };
323
+ }
324
+ if (header.kid !== proof.meta.kid) {
325
+ return {
326
+ code: "XMCP_I_EBADPROOF",
327
+ message: "Invalid JWS: key ID mismatch",
328
+ httpStatus: 403,
329
+ details: {
330
+ reason: "JWS header kid must match proof meta kid",
331
+ expected: proof.meta.kid,
332
+ received: header.kid,
333
+ },
334
+ };
335
+ }
336
+ // Mock signature verification passes for test DIDs
337
+ return null;
338
+ }
339
+ // Parse JWS components - support both full JWS and detached format
340
+ const jwsParts = proof.jws.split(".");
341
+ if (jwsParts.length !== 3) {
342
+ return {
343
+ code: "XMCP_I_EBADPROOF",
344
+ message: "Invalid JWS format",
345
+ httpStatus: 403,
346
+ details: {
347
+ reason: "JWS must have 3 parts (header.payload.signature)",
348
+ expected: "3 parts",
349
+ received: `${jwsParts.length} parts`,
350
+ },
351
+ };
352
+ }
353
+ const [headerB64, payloadB64Part, signatureB64] = jwsParts;
354
+ const isDetached = payloadB64Part === "";
355
+ // Parse and validate header
356
+ const header = JSON.parse(Buffer.from(headerB64, "base64url").toString());
357
+ if (header.alg !== "EdDSA") {
358
+ return {
359
+ code: "XMCP_I_EBADPROOF",
360
+ message: "Invalid JWS: unsupported algorithm",
361
+ httpStatus: 403,
362
+ details: {
363
+ reason: "Only EdDSA algorithm is supported",
364
+ expected: "EdDSA",
365
+ received: header.alg,
366
+ },
367
+ };
368
+ }
369
+ if (header.kid !== proof.meta.kid) {
370
+ return {
371
+ code: "XMCP_I_EBADPROOF",
372
+ message: "Invalid JWS: key ID mismatch",
373
+ httpStatus: 403,
374
+ details: {
375
+ reason: "JWS header kid must match proof meta kid",
376
+ expected: proof.meta.kid,
377
+ received: header.kid,
378
+ },
379
+ };
380
+ }
381
+ // Fetch public key from DID document with caching
382
+ const publicKey = await this.fetchPublicKeyWithCache(proof.meta.did, proof.meta.kid);
383
+ if (!publicKey) {
384
+ return {
385
+ code: "XMCP_I_EBADPROOF",
386
+ message: "Unable to resolve public key",
387
+ httpStatus: 403,
388
+ details: {
389
+ reason: "Could not fetch public key from DID document",
390
+ remediation: "Ensure DID document is accessible and contains the key",
391
+ },
392
+ };
393
+ }
394
+ // Handle both full JWS (from generators) and detached JWS formats
395
+ let completeJWS;
396
+ if (isDetached) {
397
+ // Detached format: reconstruct payload from proof meta (meta is cryptographically bound)
398
+ const canonicalPayload = this.createCanonicalPayload(proof.meta);
399
+ const payloadB64 = Buffer.from(canonicalPayload, "utf8").toString("base64url");
400
+ completeJWS = `${headerB64}.${payloadB64}.${signatureB64}`;
401
+ }
402
+ else {
403
+ // Full JWS format: validate proof.meta matches the signed payload.
404
+ // Without this, an attacker could tamper with meta (audience, ts, nonce)
405
+ // while keeping a valid JWS, bypassing security checks.
406
+ const payload = JSON.parse(Buffer.from(payloadB64Part, "base64url").toString("utf8"));
407
+ const metaMismatch = this.validateMetaMatchesPayload(proof.meta, payload);
408
+ if (metaMismatch) {
409
+ return metaMismatch;
410
+ }
411
+ completeJWS = proof.jws;
412
+ }
413
+ // Verify signature using JOSE
414
+ const jwk = await importJWK(publicKey);
415
+ await jwtVerify(completeJWS, jwk, {
416
+ algorithms: ["EdDSA"],
417
+ ...(expectedAudience !== undefined &&
418
+ !isDetached && { audience: expectedAudience }),
419
+ });
420
+ return null;
421
+ }
422
+ catch (error) {
423
+ return {
424
+ code: "XMCP_I_EBADPROOF",
425
+ message: "Signature verification failed",
426
+ httpStatus: 403,
427
+ details: {
428
+ reason: error instanceof Error ? error.message : "Unknown error",
429
+ remediation: "Check proof signature and public key",
430
+ },
431
+ };
432
+ }
433
+ }
434
+ /**
435
+ * Validate that proof.meta matches the decoded JWT payload.
436
+ * Required for full JWS to prevent meta tampering (audience, timestamp, nonce)
437
+ * while keeping a valid signature. Returns StructuredError on mismatch.
438
+ */
439
+ validateMetaMatchesPayload(meta, payload) {
440
+ // JWT aud can be string or string[] (RFC 7519)
441
+ const payloadAud = payload.aud;
442
+ const audienceMatches = typeof payloadAud === "string"
443
+ ? meta.audience === payloadAud
444
+ : Array.isArray(payloadAud) && payloadAud.includes(meta.audience);
445
+ if (!audienceMatches) {
446
+ return {
447
+ code: "XMCP_I_EBADPROOF",
448
+ message: "Invalid proof: meta does not match signed payload",
449
+ httpStatus: 403,
450
+ details: {
451
+ reason: "proof.meta.audience does not match JWT payload aud",
452
+ expected: payloadAud,
453
+ received: meta.audience,
454
+ remediation: "Ensure proof.meta matches the signed JWS payload; tampering detected",
455
+ },
456
+ };
457
+ }
458
+ const checks = [
459
+ { metaVal: meta.did, payloadKey: "sub", payloadVal: payload.sub },
460
+ { metaVal: meta.did, payloadKey: "iss", payloadVal: payload.iss },
461
+ {
462
+ metaVal: meta.requestHash,
463
+ payloadKey: "requestHash",
464
+ payloadVal: payload.requestHash,
465
+ },
466
+ {
467
+ metaVal: meta.responseHash,
468
+ payloadKey: "responseHash",
469
+ payloadVal: payload.responseHash,
470
+ },
471
+ { metaVal: meta.ts, payloadKey: "ts", payloadVal: payload.ts },
472
+ { metaVal: meta.nonce, payloadKey: "nonce", payloadVal: payload.nonce },
473
+ {
474
+ metaVal: meta.sessionId,
475
+ payloadKey: "sessionId",
476
+ payloadVal: payload.sessionId,
477
+ },
478
+ ];
479
+ for (const { metaVal, payloadKey, payloadVal } of checks) {
480
+ if (metaVal !== payloadVal) {
481
+ return {
482
+ code: "XMCP_I_EBADPROOF",
483
+ message: "Invalid proof: meta does not match signed payload",
484
+ httpStatus: 403,
485
+ details: {
486
+ reason: `proof.meta does not match JWT payload (${payloadKey})`,
487
+ expected: payloadVal,
488
+ received: metaVal,
489
+ remediation: "Ensure proof.meta matches the signed JWS payload; tampering detected",
490
+ },
491
+ };
492
+ }
493
+ }
494
+ // Optional fields - use truthiness semantics to match generators.
495
+ // Generators use `...(meta.scopeId && { scopeId })` so falsy values ("" or null)
496
+ // are excluded from the signed payload. We must not report mismatch when
497
+ // meta has falsy value but payload omits the field.
498
+ const checkOptional = (metaVal, payloadVal, field) => {
499
+ const metaHas = !!metaVal;
500
+ const payloadHas = payloadVal !== undefined;
501
+ if (metaHas !== payloadHas) {
502
+ return {
503
+ code: "XMCP_I_EBADPROOF",
504
+ message: "Invalid proof: meta does not match signed payload",
505
+ httpStatus: 403,
506
+ details: {
507
+ reason: `proof.meta.${field} does not match JWT payload ${field}`,
508
+ expected: payloadVal,
509
+ received: metaVal,
510
+ remediation: "Ensure proof.meta matches the signed JWS payload",
511
+ },
512
+ };
513
+ }
514
+ if (metaHas && metaVal !== payloadVal) {
515
+ return {
516
+ code: "XMCP_I_EBADPROOF",
517
+ message: "Invalid proof: meta does not match signed payload",
518
+ httpStatus: 403,
519
+ details: {
520
+ reason: `proof.meta.${field} does not match JWT payload ${field}`,
521
+ expected: payloadVal,
522
+ received: metaVal,
523
+ remediation: "Ensure proof.meta matches the signed JWS payload",
524
+ },
525
+ };
526
+ }
527
+ return null;
528
+ };
529
+ const scopeErr = checkOptional(meta.scopeId, payload.scopeId, "scopeId");
530
+ if (scopeErr)
531
+ return scopeErr;
532
+ const delegErr = checkOptional(meta.delegationRef, payload.delegationRef, "delegationRef");
533
+ if (delegErr)
534
+ return delegErr;
535
+ const clientErr = checkOptional(meta.clientDid, payload.clientDid, "clientDid");
536
+ if (clientErr)
537
+ return clientErr;
538
+ return null;
539
+ }
540
+ /**
541
+ * Create canonical payload that matches runtime implementation
542
+ * Uses JSON Canonicalization Scheme (JCS) RFC 8785 for deterministic ordering
543
+ *
544
+ * CRITICAL: Must match the JWT payload structure used by proof generators.
545
+ * Generators use standard JWT claims (aud, sub, iss) plus custom claims.
546
+ */
547
+ createCanonicalPayload(meta) {
548
+ // Reconstruct the exact JWT payload structure used by generators
549
+ // (see packages/mcp-i/src/runtime/proof.ts and packages/mcp-i-cloudflare/src/proof-generator.ts)
550
+ const payload = {
551
+ // Standard JWT claims (RFC 7519)
552
+ aud: meta.audience, // Audience (who the token is for)
553
+ sub: meta.did, // Subject (agent DID)
554
+ iss: meta.did, // Issuer (agent DID - self-issued)
555
+ // Custom MCP-I proof claims
556
+ requestHash: meta.requestHash,
557
+ responseHash: meta.responseHash,
558
+ ts: meta.ts,
559
+ nonce: meta.nonce,
560
+ sessionId: meta.sessionId,
561
+ // Optional claims (only include if present)
562
+ ...(meta.scopeId && { scopeId: meta.scopeId }),
563
+ ...(meta.delegationRef && { delegationRef: meta.delegationRef }),
564
+ ...(meta.clientDid && { clientDid: meta.clientDid }),
565
+ };
566
+ // Use RFC 8785 compliant JCS canonicalization
567
+ return canonicalize(payload);
568
+ }
569
+ /**
570
+ * Verify delegation status via KTA with caching
571
+ */
572
+ async verifyDelegation(delegationRef, did, kid) {
573
+ try {
574
+ // Check cache first
575
+ const cached = this.delegationCache.get(delegationRef);
576
+ if (cached && cached.expiresAt > Date.now()) {
577
+ return this.validateDelegationResponse(cached.response, did, kid);
578
+ }
579
+ if (this.config.allowMockData) {
580
+ // Mock delegation for testing
581
+ if (delegationRef.startsWith("mock:")) {
582
+ const mockResponse = {
583
+ active: delegationRef !== "mock:revoked",
584
+ did,
585
+ kid,
586
+ scopes: ["*"],
587
+ };
588
+ return this.validateDelegationResponse(mockResponse, did, kid);
589
+ }
590
+ // For other delegation refs in test mode, assume they're valid
591
+ const mockResponse = {
592
+ active: true,
593
+ did,
594
+ kid,
595
+ scopes: ["*"],
596
+ };
597
+ return this.validateDelegationResponse(mockResponse, did, kid);
598
+ }
599
+ const response = await fetch(`${this.config.ktaBaseUrl}/api/v1/delegations/${encodeURIComponent(delegationRef)}`, {
600
+ method: "GET",
601
+ headers: {
602
+ "Content-Type": "application/json",
603
+ "User-Agent": "XMCP-I-Verifier/1.0",
604
+ },
605
+ // Add timeout for production reliability
606
+ signal: AbortSignal.timeout(5000),
607
+ });
608
+ if (!response || !response.ok) {
609
+ if (response && response.status === 404) {
610
+ return {
611
+ code: "XMCP_I_EBADPROOF",
612
+ message: "Delegation not found",
613
+ httpStatus: 403,
614
+ details: {
615
+ reason: "Delegation reference not found in KTA",
616
+ remediation: "Check delegation reference and ensure it exists",
617
+ },
618
+ };
619
+ }
620
+ throw new Error(`KTA API error: ${response.status}`);
621
+ }
622
+ const delegation = (await response.json());
623
+ // Cache the response
624
+ this.delegationCache.set(delegationRef, {
625
+ response: delegation,
626
+ expiresAt: Date.now() + this.config.delegationCacheTtl * 1000,
627
+ });
628
+ return this.validateDelegationResponse(delegation, did, kid);
629
+ }
630
+ catch (error) {
631
+ // Treat delegation check failures as verification failures
632
+ console.warn("Delegation verification failed:", error);
633
+ return {
634
+ code: "XMCP_I_EBADPROOF",
635
+ message: "Delegation verification failed",
636
+ httpStatus: 403,
637
+ details: {
638
+ reason: error instanceof Error ? error.message : "Unknown error",
639
+ remediation: "Check KTA connectivity and delegation status",
640
+ },
641
+ };
642
+ }
643
+ }
644
+ /**
645
+ * Validate delegation response
646
+ */
647
+ validateDelegationResponse(delegation, expectedDid, expectedKeyId) {
648
+ if (!delegation.active) {
649
+ return {
650
+ code: "XMCP_I_EBADPROOF",
651
+ message: "Delegation revoked or inactive",
652
+ httpStatus: 403,
653
+ details: {
654
+ reason: "Delegation is not active",
655
+ remediation: "Renew delegation or use direct identity",
656
+ },
657
+ };
658
+ }
659
+ if (delegation.did !== expectedDid || delegation.kid !== expectedKeyId) {
660
+ return {
661
+ code: "XMCP_I_EBADPROOF",
662
+ message: "Delegation identity mismatch",
663
+ httpStatus: 403,
664
+ details: {
665
+ reason: "Delegation does not match proof identity",
666
+ expected: `${expectedDid}#${expectedKeyId}`,
667
+ received: `${delegation.did}#${delegation.kid}`,
668
+ },
669
+ };
670
+ }
671
+ if (delegation.expiresAt && delegation.expiresAt < Date.now() / 1000) {
672
+ return {
673
+ code: "XMCP_I_EBADPROOF",
674
+ message: "Delegation expired",
675
+ httpStatus: 403,
676
+ details: {
677
+ reason: "Delegation has expired",
678
+ remediation: "Renew delegation",
679
+ },
680
+ };
681
+ }
682
+ return null;
683
+ }
684
+ /**
685
+ * Fetch public key from DID document with caching
686
+ */
687
+ async fetchPublicKeyWithCache(did, kid) {
688
+ const cacheKey = `${did}#${kid}`;
689
+ const cached = this.didCache.get(cacheKey);
690
+ if (cached && cached.expiresAt > Date.now()) {
691
+ return this.extractPublicKey(cached.document, kid);
692
+ }
693
+ const didDoc = await this.fetchDIDDocument(did);
694
+ if (!didDoc) {
695
+ return null;
696
+ }
697
+ // Cache the DID document
698
+ this.didCache.set(cacheKey, {
699
+ document: didDoc,
700
+ expiresAt: Date.now() + this.config.didCacheTtl * 1000,
701
+ });
702
+ return this.extractPublicKey(didDoc, kid);
703
+ }
704
+ /**
705
+ * Fetch DID document from well-known endpoint
706
+ */
707
+ async fetchDIDDocument(did) {
708
+ try {
709
+ if (this.config.allowMockData && did.startsWith("did:test:")) {
710
+ // Mock DID document for testing
711
+ return {
712
+ id: did,
713
+ verificationMethod: [
714
+ {
715
+ id: `#key-test-1`,
716
+ type: "Ed25519VerificationKey2020",
717
+ controller: did,
718
+ publicKeyJwk: {
719
+ kty: "OKP",
720
+ crv: "Ed25519",
721
+ x: "mock-public-key-data",
722
+ },
723
+ },
724
+ ],
725
+ };
726
+ }
727
+ // Convert DID to well-known URL for did:web
728
+ if (!did.startsWith("did:web:")) {
729
+ throw new Error("Only did:web is supported");
730
+ }
731
+ const domain = did.replace("did:web:", "").replace(/:/g, "/");
732
+ const didDocUrl = `https://${domain}/.well-known/did.json`;
733
+ const response = await fetch(didDocUrl, {
734
+ headers: {
735
+ Accept: "application/did+json, application/json",
736
+ "User-Agent": "XMCP-I-Verifier/1.0",
737
+ },
738
+ // Add timeout for production reliability
739
+ signal: AbortSignal.timeout(5000),
740
+ });
741
+ if (!response.ok) {
742
+ throw new Error(`Failed to fetch DID document: ${response.status}`);
743
+ }
744
+ return await response.json();
745
+ }
746
+ catch (error) {
747
+ console.warn("Failed to fetch DID document:", error);
748
+ return null;
749
+ }
750
+ }
751
+ /**
752
+ * Extract public key from DID document
753
+ */
754
+ extractPublicKey(didDoc, kid) {
755
+ try {
756
+ // Find verification method
757
+ const verificationMethod = didDoc.verificationMethod?.find((vm) => vm.id === `#${kid}` || vm.id === `${didDoc.id}#${kid}`);
758
+ if (!verificationMethod) {
759
+ throw new Error(`Key ${kid} not found in DID document`);
760
+ }
761
+ // Convert to JWK format if needed
762
+ if (verificationMethod.publicKeyJwk) {
763
+ return verificationMethod.publicKeyJwk;
764
+ }
765
+ if (verificationMethod.publicKeyMultibase) {
766
+ // Convert multibase to JWK (simplified for Ed25519)
767
+ // This is a placeholder - real implementation would use proper multibase decoding
768
+ return {
769
+ kty: "OKP",
770
+ crv: "Ed25519",
771
+ x: verificationMethod.publicKeyMultibase.slice(1), // Remove multibase prefix
772
+ };
773
+ }
774
+ throw new Error("Unsupported public key format");
775
+ }
776
+ catch (error) {
777
+ console.warn("Failed to extract public key:", error);
778
+ return null;
779
+ }
780
+ }
781
+ /**
782
+ * Generate trusted headers for successful verification
783
+ */
784
+ generateHeaders(meta) {
785
+ const headers = {
786
+ [AGENT_HEADERS.DID]: meta.did,
787
+ [AGENT_HEADERS.KEY_ID]: meta.kid,
788
+ [AGENT_HEADERS.SESSION]: meta.sessionId,
789
+ [AGENT_HEADERS.CONFIDENCE]: "verified",
790
+ [AGENT_HEADERS.VERIFIED_AT]: Math.floor(Date.now() / 1000).toString(),
791
+ };
792
+ if (meta.scopeId) {
793
+ headers[AGENT_HEADERS.SCOPES] = meta.scopeId;
794
+ }
795
+ if (meta.delegationRef) {
796
+ headers[AGENT_HEADERS.DELEGATION_REF] = meta.delegationRef;
797
+ }
798
+ // Add registry URL for traceability
799
+ headers[AGENT_HEADERS.REGISTRY] = `${this.config.ktaBaseUrl}/agents/${encodeURIComponent(meta.did)}`;
800
+ return headers;
801
+ }
802
+ /**
803
+ * Generate agent context for MCP recipients
804
+ */
805
+ generateAgentContext(meta) {
806
+ return {
807
+ did: meta.did,
808
+ kid: meta.kid,
809
+ subject: meta.delegationRef ? meta.did : undefined,
810
+ scopes: meta.scopeId ? meta.scopeId.split(",") : [],
811
+ session: meta.sessionId,
812
+ confidence: "verified",
813
+ delegationRef: meta.delegationRef,
814
+ registry: `${this.config.ktaBaseUrl}/agents/${encodeURIComponent(meta.did)}`,
815
+ verifiedAt: Math.floor(Date.now() / 1000),
816
+ };
817
+ }
818
+ /**
819
+ * Create error result from structured error
820
+ */
821
+ createErrorResult(error) {
822
+ return {
823
+ success: false,
824
+ error: {
825
+ code: error.code,
826
+ message: error.message,
827
+ details: error.details,
828
+ httpStatus: error.httpStatus ||
829
+ ERROR_HTTP_STATUS[error.code] ||
830
+ 500,
831
+ },
832
+ };
833
+ }
834
+ /**
835
+ * Log verification attempt for security monitoring
836
+ */
837
+ logVerificationAttempt(context, success, reason, duration) {
838
+ // In production, this would integrate with your logging system
839
+ const logEntry = {
840
+ timestamp: new Date().toISOString(),
841
+ did: context.proof?.meta?.did || "unknown",
842
+ audience: context.audience,
843
+ success,
844
+ reason,
845
+ duration,
846
+ sessionId: context.proof?.meta?.sessionId,
847
+ };
848
+ // For now, just console.log - in production, use structured logging
849
+ console.log("XMCP-I Verification:", JSON.stringify(logEntry));
850
+ }
851
+ /**
852
+ * Clean up expired cache entries
853
+ */
854
+ cleanupCache() {
855
+ const now = Date.now();
856
+ for (const [key, value] of this.didCache.entries()) {
857
+ if (value.expiresAt <= now) {
858
+ this.didCache.delete(key);
859
+ }
860
+ }
861
+ for (const [key, value] of this.delegationCache.entries()) {
862
+ if (value.expiresAt <= now) {
863
+ this.delegationCache.delete(key);
864
+ }
865
+ }
866
+ }
867
+ }
868
+ //# sourceMappingURL=core.js.map