@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.
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test$colon$coverage.log +1979 -2089
- package/.turbo/turbo-test.log +2666 -0
- package/coverage/coverage-final.json +1 -1
- package/dist/services/access-control.service.d.ts.map +1 -1
- package/dist/services/access-control.service.js +24 -21
- package/dist/services/access-control.service.js.map +1 -1
- package/package.json +2 -2
- package/src/services/__tests__/access-control.proof-response-validation.test.ts +179 -0
- package/src/services/access-control.service.ts +28 -25
|
@@ -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:
|
|
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
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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
|
-
"
|
|
528
|
+
"Response data is not an object after cloning",
|
|
527
529
|
{
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
: String(parseError),
|
|
530
|
+
originalType: typeof rawData,
|
|
531
|
+
clonedType: typeof clonedData,
|
|
532
|
+
clonedValue: clonedData,
|
|
532
533
|
}
|
|
533
534
|
);
|
|
534
535
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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
|
-
"
|
|
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:
|
|
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,
|