@kya-os/mcp-i-core 1.2.2-canary.35 → 1.2.2-canary.37

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.
@@ -396,4 +396,183 @@ describe("Proof Submission Response Validation", () => {
396
396
  }
397
397
  });
398
398
  });
399
+
400
+ describe("JSON Deep Clone Fix (Cloudflare Workers Edge Case)", () => {
401
+ it("should correctly extract data from wrapped response after JSON deep clone", async () => {
402
+ const request: ProofSubmissionRequest = {
403
+ session_id: "test-session",
404
+ proofs: [
405
+ {
406
+ jws: "test.jws.signature",
407
+ meta: {
408
+ did: "did:key:test",
409
+ kid: "did:key:test#key-1",
410
+ ts: Date.now(),
411
+ nonce: "test-nonce",
412
+ audience: "https://kya.vouched.id",
413
+ sessionId: "test-session",
414
+ requestHash: "sha256:" + "a".repeat(64),
415
+ responseHash: "sha256:" + "b".repeat(64),
416
+ scopeId: "test:execute",
417
+ },
418
+ },
419
+ ],
420
+ };
421
+
422
+ // Simulate the exact response format from AgentShield API
423
+ // This matches the format seen in production logs
424
+ const wrappedResponse = {
425
+ success: true,
426
+ data: {
427
+ accepted: 1,
428
+ rejected: 0,
429
+ outcomes: {
430
+ success: 1,
431
+ },
432
+ errors: [],
433
+ },
434
+ metadata: {
435
+ requestId: "fc1fa88f-9b22-4161-b4fd-17d8215098ee",
436
+ timestamp: "2025-11-24T21:36:33.029Z",
437
+ },
438
+ };
439
+
440
+ mockFetch.mockResolvedValueOnce({
441
+ ok: true,
442
+ status: 200,
443
+ text: async () => JSON.stringify(wrappedResponse),
444
+ headers: new Headers(),
445
+ });
446
+
447
+ const result = await service.submitProofs(request);
448
+
449
+ // Verify all fields are correctly extracted
450
+ expect(result.success).toBe(true);
451
+ expect(result.accepted).toBe(1);
452
+ expect(result.rejected).toBe(0);
453
+ expect(result.outcomes).toEqual({ success: 1 });
454
+ expect(result.errors).toEqual([]);
455
+ });
456
+
457
+ it("should handle response where data fields are numeric values (not undefined)", async () => {
458
+ const request: ProofSubmissionRequest = {
459
+ session_id: "test-session",
460
+ proofs: [
461
+ {
462
+ jws: "test.jws.signature",
463
+ meta: {
464
+ did: "did:key:test",
465
+ kid: "did:key:test#key-1",
466
+ ts: Date.now(),
467
+ nonce: "test-nonce",
468
+ audience: "https://kya.vouched.id",
469
+ sessionId: "test-session",
470
+ requestHash: "sha256:" + "a".repeat(64),
471
+ responseHash: "sha256:" + "b".repeat(64),
472
+ scopeId: "test:execute",
473
+ },
474
+ },
475
+ ],
476
+ };
477
+
478
+ // Test with zero values (edge case for falsy check)
479
+ const wrappedResponse = {
480
+ success: true,
481
+ data: {
482
+ accepted: 0,
483
+ rejected: 0,
484
+ outcomes: {},
485
+ errors: [],
486
+ },
487
+ metadata: {
488
+ requestId: "test-id",
489
+ timestamp: new Date().toISOString(),
490
+ },
491
+ };
492
+
493
+ mockFetch.mockResolvedValueOnce({
494
+ ok: true,
495
+ status: 200,
496
+ text: async () => JSON.stringify(wrappedResponse),
497
+ headers: new Headers(),
498
+ });
499
+
500
+ const result = await service.submitProofs(request);
501
+
502
+ // Verify zero values are correctly extracted (not treated as undefined)
503
+ expect(result.success).toBe(true);
504
+ expect(result.accepted).toBe(0);
505
+ expect(result.rejected).toBe(0);
506
+ });
507
+
508
+ it("should handle response with nested outcomes object", async () => {
509
+ const request: ProofSubmissionRequest = {
510
+ session_id: "test-session",
511
+ proofs: [
512
+ {
513
+ jws: "test.jws.signature",
514
+ meta: {
515
+ did: "did:key:test",
516
+ kid: "did:key:test#key-1",
517
+ ts: Date.now(),
518
+ nonce: "test-nonce",
519
+ audience: "https://kya.vouched.id",
520
+ sessionId: "test-session",
521
+ requestHash: "sha256:" + "a".repeat(64),
522
+ responseHash: "sha256:" + "b".repeat(64),
523
+ scopeId: "test:execute",
524
+ },
525
+ },
526
+ ],
527
+ };
528
+
529
+ // Test with various outcome types
530
+ const wrappedResponse = {
531
+ success: true,
532
+ data: {
533
+ accepted: 3,
534
+ rejected: 2,
535
+ outcomes: {
536
+ success: 1,
537
+ failed: 1,
538
+ blocked: 1,
539
+ error: 2,
540
+ },
541
+ errors: [
542
+ {
543
+ proof_index: 0,
544
+ error: {
545
+ code: "validation_error",
546
+ message: "Invalid signature",
547
+ },
548
+ },
549
+ ],
550
+ },
551
+ metadata: {
552
+ requestId: "test-id",
553
+ timestamp: new Date().toISOString(),
554
+ },
555
+ };
556
+
557
+ mockFetch.mockResolvedValueOnce({
558
+ ok: true,
559
+ status: 200,
560
+ text: async () => JSON.stringify(wrappedResponse),
561
+ headers: new Headers(),
562
+ });
563
+
564
+ const result = await service.submitProofs(request);
565
+
566
+ expect(result.accepted).toBe(3);
567
+ expect(result.rejected).toBe(2);
568
+ expect(result.outcomes).toEqual({
569
+ success: 1,
570
+ failed: 1,
571
+ blocked: 1,
572
+ error: 2,
573
+ });
574
+ expect(result.errors).toHaveLength(1);
575
+ expect(result.errors![0].proof_index).toBe(0);
576
+ });
577
+ });
399
578
  });
@@ -511,45 +511,49 @@ export class AccessControlApiService {
511
511
  if (wrappedResponse.success !== undefined && wrappedResponse.data) {
512
512
  // Response is wrapped in { success, data }
513
513
  // Extract data and add success field if missing (for schema validation)
514
- // CRITICAL: Ensure data is actually an object, not a string or other type
514
+ // CRITICAL: Use JSON.parse(JSON.stringify()) to create a clean object
515
+ // This handles edge cases in Cloudflare Workers where response.json()
516
+ // might return objects with proxy/getter issues that prevent property access
515
517
  let dataToValidate: Record<string, unknown>;
516
- if (typeof wrappedResponse.data === "string") {
517
- // If data is a string, parse it (shouldn't happen but defensive)
518
- try {
519
- dataToValidate = JSON.parse(wrappedResponse.data) as Record<
520
- string,
521
- unknown
522
- >;
523
- } catch (parseError) {
518
+
519
+ try {
520
+ // Deep clone via JSON to ensure we have a plain JavaScript object
521
+ // This is the most reliable way to handle potential proxy/getter issues
522
+ const rawData = wrappedResponse.data;
523
+ const clonedData = JSON.parse(JSON.stringify(rawData));
524
+
525
+ if (typeof clonedData !== "object" || clonedData === null) {
524
526
  throw new AgentShieldAPIError(
525
527
  "invalid_response",
526
- "Failed to parse response data",
528
+ "Response data is not an object after cloning",
527
529
  {
528
- parseError:
529
- parseError instanceof Error
530
- ? parseError.message
531
- : String(parseError),
530
+ originalType: typeof rawData,
531
+ clonedType: typeof clonedData,
532
+ clonedValue: clonedData,
532
533
  }
533
534
  );
534
535
  }
535
- } else if (
536
- typeof wrappedResponse.data === "object" &&
537
- wrappedResponse.data !== null
538
- ) {
539
- dataToValidate = wrappedResponse.data as Record<string, unknown>;
540
- } else {
536
+
537
+ dataToValidate = clonedData as Record<string, unknown>;
538
+ } catch (cloneError) {
539
+ if (cloneError instanceof AgentShieldAPIError) {
540
+ throw cloneError;
541
+ }
541
542
  throw new AgentShieldAPIError(
542
543
  "invalid_response",
543
- "Response data is not an object",
544
+ "Failed to clone response data",
544
545
  {
546
+ error:
547
+ cloneError instanceof Error
548
+ ? cloneError.message
549
+ : String(cloneError),
545
550
  dataType: typeof wrappedResponse.data,
546
- data: wrappedResponse.data,
547
551
  }
548
552
  );
549
553
  }
550
554
 
551
555
  // CRITICAL: Log the actual data structure for debugging
552
- console.error(`[AccessControl] 🔍 DATA OBJECT STRUCTURE:`, {
556
+ console.error(`[AccessControl] 🔍 DATA OBJECT STRUCTURE (after deep clone):`, {
553
557
  correlationId,
554
558
  dataKeys: Object.keys(dataToValidate),
555
559
  hasAccepted: "accepted" in dataToValidate,
@@ -569,8 +573,7 @@ export class AccessControlApiService {
569
573
 
570
574
  // Ensure success field is present (required by schema)
571
575
  // wrappedResponse.success should be true since we checked it exists
572
- // CRITICAL: Explicitly construct the object to ensure all fields are present
573
- // Don't rely on spread operator which might fail with Proxies or non-enumerable properties
576
+ // CRITICAL: Construct from the cloned data to ensure all fields are present
574
577
  const dataWithSuccess: Record<string, unknown> = {
575
578
  success: wrappedResponse.success === true,
576
579
  accepted: dataToValidate.accepted,