@kya-os/mcp-i-core 1.2.3-canary.6 → 1.3.0
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/.claude/settings.local.json +9 -0
- package/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-test$colon$coverage.log +4514 -0
- package/.turbo/turbo-test.log +2973 -0
- package/COMPLIANCE_IMPROVEMENT_REPORT.md +483 -0
- package/Composer 3.md +615 -0
- package/GPT-5.md +1169 -0
- package/OPUS-plan.md +352 -0
- package/PHASE_3_AND_4.1_SUMMARY.md +585 -0
- package/PHASE_3_SUMMARY.md +317 -0
- package/PHASE_4.1.3_SUMMARY.md +428 -0
- package/PHASE_4.1_COMPLETE.md +525 -0
- package/PHASE_4_USER_DID_IDENTITY_LINKING_PLAN.md +1240 -0
- package/SCHEMA_COMPLIANCE_REPORT.md +275 -0
- package/TEST_PLAN.md +571 -0
- package/coverage/coverage-final.json +57 -0
- package/dist/__tests__/utils/mock-providers.d.ts +1 -2
- package/dist/__tests__/utils/mock-providers.d.ts.map +1 -1
- package/dist/__tests__/utils/mock-providers.js.map +1 -1
- package/dist/cache/oauth-config-cache.d.ts +69 -0
- package/dist/cache/oauth-config-cache.d.ts.map +1 -0
- package/dist/cache/oauth-config-cache.js +76 -0
- package/dist/cache/oauth-config-cache.js.map +1 -0
- package/dist/identity/idp-token-resolver.d.ts +53 -0
- package/dist/identity/idp-token-resolver.d.ts.map +1 -0
- package/dist/identity/idp-token-resolver.js +108 -0
- package/dist/identity/idp-token-resolver.js.map +1 -0
- package/dist/identity/idp-token-storage.interface.d.ts +42 -0
- package/dist/identity/idp-token-storage.interface.d.ts.map +1 -0
- package/dist/identity/idp-token-storage.interface.js +12 -0
- package/dist/identity/idp-token-storage.interface.js.map +1 -0
- package/dist/identity/user-did-manager.d.ts +39 -1
- package/dist/identity/user-did-manager.d.ts.map +1 -1
- package/dist/identity/user-did-manager.js +69 -3
- package/dist/identity/user-did-manager.js.map +1 -1
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +39 -1
- package/dist/index.js.map +1 -1
- package/dist/runtime/audit-logger.d.ts +37 -0
- package/dist/runtime/audit-logger.d.ts.map +1 -0
- package/dist/runtime/audit-logger.js +9 -0
- package/dist/runtime/audit-logger.js.map +1 -0
- package/dist/runtime/base.d.ts +58 -2
- package/dist/runtime/base.d.ts.map +1 -1
- package/dist/runtime/base.js +266 -11
- package/dist/runtime/base.js.map +1 -1
- package/dist/services/access-control.service.d.ts.map +1 -1
- package/dist/services/access-control.service.js +200 -35
- package/dist/services/access-control.service.js.map +1 -1
- package/dist/services/authorization/authorization-registry.d.ts +29 -0
- package/dist/services/authorization/authorization-registry.d.ts.map +1 -0
- package/dist/services/authorization/authorization-registry.js +57 -0
- package/dist/services/authorization/authorization-registry.js.map +1 -0
- package/dist/services/authorization/types.d.ts +53 -0
- package/dist/services/authorization/types.d.ts.map +1 -0
- package/dist/services/authorization/types.js +10 -0
- package/dist/services/authorization/types.js.map +1 -0
- package/dist/services/batch-delegation.service.d.ts +53 -0
- package/dist/services/batch-delegation.service.d.ts.map +1 -0
- package/dist/services/batch-delegation.service.js +95 -0
- package/dist/services/batch-delegation.service.js.map +1 -0
- package/dist/services/oauth-config.service.d.ts +53 -0
- package/dist/services/oauth-config.service.d.ts.map +1 -0
- package/dist/services/oauth-config.service.js +117 -0
- package/dist/services/oauth-config.service.js.map +1 -0
- package/dist/services/oauth-provider-registry.d.ts +77 -0
- package/dist/services/oauth-provider-registry.d.ts.map +1 -0
- package/dist/services/oauth-provider-registry.js +112 -0
- package/dist/services/oauth-provider-registry.js.map +1 -0
- package/dist/services/oauth-service.d.ts +77 -0
- package/dist/services/oauth-service.d.ts.map +1 -0
- package/dist/services/oauth-service.js +348 -0
- package/dist/services/oauth-service.js.map +1 -0
- package/dist/services/oauth-token-retrieval.service.d.ts +49 -0
- package/dist/services/oauth-token-retrieval.service.d.ts.map +1 -0
- package/dist/services/oauth-token-retrieval.service.js +150 -0
- package/dist/services/oauth-token-retrieval.service.js.map +1 -0
- package/dist/services/provider-resolver.d.ts +48 -0
- package/dist/services/provider-resolver.d.ts.map +1 -0
- package/dist/services/provider-resolver.js +120 -0
- package/dist/services/provider-resolver.js.map +1 -0
- package/dist/services/provider-validator.d.ts +55 -0
- package/dist/services/provider-validator.d.ts.map +1 -0
- package/dist/services/provider-validator.js +135 -0
- package/dist/services/provider-validator.js.map +1 -0
- package/dist/services/tool-context-builder.d.ts +57 -0
- package/dist/services/tool-context-builder.d.ts.map +1 -0
- package/dist/services/tool-context-builder.js +125 -0
- package/dist/services/tool-context-builder.js.map +1 -0
- package/dist/services/tool-protection.service.d.ts +87 -10
- package/dist/services/tool-protection.service.d.ts.map +1 -1
- package/dist/services/tool-protection.service.js +282 -112
- package/dist/services/tool-protection.service.js.map +1 -1
- package/dist/types/oauth-required-error.d.ts +40 -0
- package/dist/types/oauth-required-error.d.ts.map +1 -0
- package/dist/types/oauth-required-error.js +40 -0
- package/dist/types/oauth-required-error.js.map +1 -0
- package/dist/utils/did-helpers.d.ts +33 -0
- package/dist/utils/did-helpers.d.ts.map +1 -1
- package/dist/utils/did-helpers.js +40 -0
- package/dist/utils/did-helpers.js.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/docs/API_REFERENCE.md +1362 -0
- package/docs/COMPLIANCE_MATRIX.md +691 -0
- package/docs/STATUSLIST2021_GUIDE.md +696 -0
- package/docs/W3C_VC_DELEGATION_GUIDE.md +710 -0
- package/package.json +24 -50
- package/scripts/audit-compliance.ts +724 -0
- package/src/__tests__/cache/tool-protection-cache.test.ts +640 -0
- package/src/__tests__/config/provider-runtime-config.test.ts +309 -0
- package/src/__tests__/delegation-e2e.test.ts +690 -0
- package/src/__tests__/identity/user-did-manager.test.ts +213 -0
- package/src/__tests__/index.test.ts +56 -0
- package/src/__tests__/integration/full-flow.test.ts +776 -0
- package/src/__tests__/integration.test.ts +281 -0
- package/src/__tests__/providers/base.test.ts +173 -0
- package/src/__tests__/providers/memory.test.ts +319 -0
- package/src/__tests__/regression/phase2-regression.test.ts +427 -0
- package/src/__tests__/runtime/audit-logger.test.ts +154 -0
- package/src/__tests__/runtime/base-extensions.test.ts +593 -0
- package/src/__tests__/runtime/base.test.ts +869 -0
- package/src/__tests__/runtime/delegation-flow.test.ts +164 -0
- package/src/__tests__/runtime/proof-client-did.test.ts +375 -0
- package/src/__tests__/runtime/route-interception.test.ts +686 -0
- package/src/__tests__/runtime/tool-protection-enforcement.test.ts +908 -0
- package/src/__tests__/services/agentshield-integration.test.ts +784 -0
- package/src/__tests__/services/provider-resolver-edge-cases.test.ts +487 -0
- package/src/__tests__/services/tool-protection-oauth-provider.test.ts +480 -0
- package/src/__tests__/services/tool-protection.service.test.ts +1366 -0
- package/src/__tests__/utils/mock-providers.ts +340 -0
- package/src/cache/oauth-config-cache.d.ts +69 -0
- package/src/cache/oauth-config-cache.d.ts.map +1 -0
- package/src/cache/oauth-config-cache.js +71 -0
- package/src/cache/oauth-config-cache.js.map +1 -0
- package/src/cache/oauth-config-cache.ts +123 -0
- package/src/cache/tool-protection-cache.ts +171 -0
- package/src/compliance/EXAMPLE.md +412 -0
- package/src/compliance/__tests__/schema-verifier.test.ts +797 -0
- package/src/compliance/index.ts +8 -0
- package/src/compliance/schema-registry.ts +460 -0
- package/src/compliance/schema-verifier.ts +708 -0
- package/src/config/__tests__/remote-config.spec.ts +268 -0
- package/src/config/remote-config.ts +174 -0
- package/src/config.ts +309 -0
- package/src/delegation/__tests__/audience-validator.test.ts +112 -0
- package/src/delegation/__tests__/bitstring.test.ts +346 -0
- package/src/delegation/__tests__/cascading-revocation.test.ts +628 -0
- package/src/delegation/__tests__/delegation-graph.test.ts +584 -0
- package/src/delegation/__tests__/utils.test.ts +152 -0
- package/src/delegation/__tests__/vc-issuer.test.ts +442 -0
- package/src/delegation/__tests__/vc-verifier.test.ts +922 -0
- package/src/delegation/audience-validator.ts +52 -0
- package/src/delegation/bitstring.ts +278 -0
- package/src/delegation/cascading-revocation.ts +370 -0
- package/src/delegation/delegation-graph.ts +299 -0
- package/src/delegation/index.ts +14 -0
- package/src/delegation/statuslist-manager.ts +353 -0
- package/src/delegation/storage/__tests__/memory-graph-storage.test.ts +366 -0
- package/src/delegation/storage/__tests__/memory-statuslist-storage.test.ts +228 -0
- package/src/delegation/storage/index.ts +9 -0
- package/src/delegation/storage/memory-graph-storage.ts +178 -0
- package/src/delegation/storage/memory-statuslist-storage.ts +77 -0
- package/src/delegation/utils.ts +42 -0
- package/src/delegation/vc-issuer.ts +232 -0
- package/src/delegation/vc-verifier.ts +568 -0
- package/src/identity/idp-token-resolver.ts +147 -0
- package/src/identity/idp-token-storage.interface.ts +59 -0
- package/src/identity/user-did-manager.ts +370 -0
- package/src/index.ts +260 -0
- package/src/providers/base.d.ts +91 -0
- package/src/providers/base.d.ts.map +1 -0
- package/src/providers/base.js +38 -0
- package/src/providers/base.js.map +1 -0
- package/src/providers/base.ts +96 -0
- package/src/providers/memory.ts +142 -0
- package/src/runtime/audit-logger.ts +39 -0
- package/src/runtime/base.ts +1329 -0
- package/src/services/__tests__/access-control.integration.test.ts +443 -0
- package/src/services/__tests__/access-control.proof-response-validation.test.ts +578 -0
- package/src/services/__tests__/access-control.service.test.ts +970 -0
- package/src/services/__tests__/batch-delegation.service.test.ts +351 -0
- package/src/services/__tests__/crypto.service.test.ts +531 -0
- package/src/services/__tests__/oauth-provider-registry.test.ts +142 -0
- package/src/services/__tests__/proof-verifier.integration.test.ts +485 -0
- package/src/services/__tests__/proof-verifier.test.ts +489 -0
- package/src/services/__tests__/provider-resolution.integration.test.ts +198 -0
- package/src/services/__tests__/provider-resolver.test.ts +217 -0
- package/src/services/__tests__/storage.service.test.ts +358 -0
- package/src/services/access-control.service.ts +990 -0
- package/src/services/authorization/authorization-registry.ts +66 -0
- package/src/services/authorization/types.ts +71 -0
- package/src/services/batch-delegation.service.ts +137 -0
- package/src/services/crypto.service.ts +302 -0
- package/src/services/errors.ts +76 -0
- package/src/services/index.ts +9 -0
- package/src/services/oauth-config.service.d.ts +53 -0
- package/src/services/oauth-config.service.d.ts.map +1 -0
- package/src/services/oauth-config.service.js +113 -0
- package/src/services/oauth-config.service.js.map +1 -0
- package/src/services/oauth-config.service.ts +166 -0
- package/src/services/oauth-provider-registry.d.ts +57 -0
- package/src/services/oauth-provider-registry.d.ts.map +1 -0
- package/src/services/oauth-provider-registry.js +73 -0
- package/src/services/oauth-provider-registry.js.map +1 -0
- package/src/services/oauth-provider-registry.ts +123 -0
- package/src/services/oauth-service.ts +510 -0
- package/src/services/oauth-token-retrieval.service.ts +245 -0
- package/src/services/proof-verifier.ts +478 -0
- package/src/services/provider-resolver.d.ts +48 -0
- package/src/services/provider-resolver.d.ts.map +1 -0
- package/src/services/provider-resolver.js +106 -0
- package/src/services/provider-resolver.js.map +1 -0
- package/src/services/provider-resolver.ts +144 -0
- package/src/services/provider-validator.ts +170 -0
- package/src/services/storage.service.ts +566 -0
- package/src/services/tool-context-builder.ts +172 -0
- package/src/services/tool-protection.service.ts +958 -0
- package/src/types/oauth-required-error.ts +63 -0
- package/src/types/tool-protection.ts +155 -0
- package/src/utils/__tests__/did-helpers.test.ts +101 -0
- package/src/utils/base64.ts +148 -0
- package/src/utils/cors.ts +83 -0
- package/src/utils/did-helpers.ts +150 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/storage-keys.ts +278 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +56 -0
|
@@ -0,0 +1,990 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Access Control API Service
|
|
3
|
+
*
|
|
4
|
+
* Brand-neutral service for interacting with access-control APIs
|
|
5
|
+
* (currently AgentShield/bouncer, but designed to be provider-agnostic).
|
|
6
|
+
*
|
|
7
|
+
* This service provides:
|
|
8
|
+
* - Delegation verification
|
|
9
|
+
* - Configuration fetching
|
|
10
|
+
* - Proof submission
|
|
11
|
+
*
|
|
12
|
+
* @package @kya-os/mcp-i-core
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type {
|
|
16
|
+
VerifyDelegationRequest,
|
|
17
|
+
VerifyDelegationAPIResponse,
|
|
18
|
+
ToolProtectionConfigAPIResponse,
|
|
19
|
+
ProofSubmissionRequest,
|
|
20
|
+
ProofSubmissionResponse,
|
|
21
|
+
} from "@kya-os/contracts/agentshield-api";
|
|
22
|
+
import {
|
|
23
|
+
verifyDelegationRequestSchema,
|
|
24
|
+
verifyDelegationAPIResponseSchema,
|
|
25
|
+
toolProtectionConfigAPIResponseSchema,
|
|
26
|
+
proofSubmissionRequestSchema,
|
|
27
|
+
proofSubmissionResponseSchema,
|
|
28
|
+
} from "@kya-os/contracts/agentshield-api";
|
|
29
|
+
import { AgentShieldAPIError } from "@kya-os/contracts/agentshield-api";
|
|
30
|
+
import type { FetchProvider } from "../providers/base";
|
|
31
|
+
|
|
32
|
+
// Type for error response
|
|
33
|
+
type AgentShieldAPIErrorResponse = {
|
|
34
|
+
code: string;
|
|
35
|
+
message: string;
|
|
36
|
+
details?: Record<string, unknown>;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export interface AccessControlApiServiceConfig {
|
|
40
|
+
/** Base URL for the access-control API (e.g., "https://kya.vouched.id") */
|
|
41
|
+
baseUrl: string;
|
|
42
|
+
|
|
43
|
+
/** API key for authentication */
|
|
44
|
+
apiKey: string;
|
|
45
|
+
|
|
46
|
+
/** Fetch provider for making HTTP requests */
|
|
47
|
+
fetchProvider: FetchProvider;
|
|
48
|
+
|
|
49
|
+
/** Optional logger callback for diagnostics */
|
|
50
|
+
logger?: (message: string, data?: unknown) => void;
|
|
51
|
+
|
|
52
|
+
/** Optional retry configuration */
|
|
53
|
+
retryConfig?: {
|
|
54
|
+
maxRetries?: number;
|
|
55
|
+
initialDelayMs?: number;
|
|
56
|
+
maxDelayMs?: number;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** Optional sleep function for delays (platform-agnostic) */
|
|
60
|
+
sleepProvider?: (ms: number) => Promise<void>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface AccessControlApiServiceMetrics {
|
|
64
|
+
/** Number of successful requests */
|
|
65
|
+
successCount: number;
|
|
66
|
+
|
|
67
|
+
/** Number of failed requests */
|
|
68
|
+
errorCount: number;
|
|
69
|
+
|
|
70
|
+
/** Number of retries performed */
|
|
71
|
+
retryCount: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Internal type with all required properties
|
|
76
|
+
*/
|
|
77
|
+
type RequiredAccessControlApiServiceConfig = Required<
|
|
78
|
+
Omit<AccessControlApiServiceConfig, "retryConfig" | "sleepProvider">
|
|
79
|
+
> & {
|
|
80
|
+
retryConfig: Required<
|
|
81
|
+
NonNullable<AccessControlApiServiceConfig["retryConfig"]>
|
|
82
|
+
>;
|
|
83
|
+
sleepProvider: NonNullable<AccessControlApiServiceConfig["sleepProvider"]>;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Generate correlation ID for request tracking
|
|
88
|
+
*/
|
|
89
|
+
function generateCorrelationId(): string {
|
|
90
|
+
// Use crypto.randomUUID() if available (Node.js 14.17+, Cloudflare Workers)
|
|
91
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
92
|
+
return crypto.randomUUID();
|
|
93
|
+
}
|
|
94
|
+
// Fallback for older environments
|
|
95
|
+
return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Access Control API Service
|
|
100
|
+
*
|
|
101
|
+
* Handles all interactions with the access-control API (AgentShield/bouncer).
|
|
102
|
+
* Designed to be brand-neutral and work with any access-control provider.
|
|
103
|
+
*/
|
|
104
|
+
export class AccessControlApiService {
|
|
105
|
+
private config: RequiredAccessControlApiServiceConfig;
|
|
106
|
+
private metrics: AccessControlApiServiceMetrics;
|
|
107
|
+
|
|
108
|
+
constructor(config: AccessControlApiServiceConfig) {
|
|
109
|
+
const retryConfig = config.retryConfig || {};
|
|
110
|
+
this.config = {
|
|
111
|
+
retryConfig: {
|
|
112
|
+
maxRetries: retryConfig.maxRetries ?? 3,
|
|
113
|
+
initialDelayMs: retryConfig.initialDelayMs ?? 100,
|
|
114
|
+
maxDelayMs: retryConfig.maxDelayMs ?? 5000,
|
|
115
|
+
},
|
|
116
|
+
logger: config.logger || (() => {}),
|
|
117
|
+
baseUrl: config.baseUrl,
|
|
118
|
+
apiKey: config.apiKey,
|
|
119
|
+
fetchProvider: config.fetchProvider,
|
|
120
|
+
sleepProvider:
|
|
121
|
+
config.sleepProvider ||
|
|
122
|
+
((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))),
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
this.metrics = {
|
|
126
|
+
successCount: 0,
|
|
127
|
+
errorCount: 0,
|
|
128
|
+
retryCount: 0,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Fetch tool protection configuration for an agent
|
|
134
|
+
*
|
|
135
|
+
* GET /api/v1/bouncer/config?agent_did={agentDid}
|
|
136
|
+
*/
|
|
137
|
+
async fetchConfig(options: {
|
|
138
|
+
agentDid: string;
|
|
139
|
+
}): Promise<ToolProtectionConfigAPIResponse> {
|
|
140
|
+
return this.retryWithBackoff(async () => {
|
|
141
|
+
const correlationId = generateCorrelationId();
|
|
142
|
+
const url = `${this.config.baseUrl}/api/v1/bouncer/config?agent_did=${encodeURIComponent(options.agentDid)}`;
|
|
143
|
+
|
|
144
|
+
this.config.logger(
|
|
145
|
+
`[AccessControl] Fetching config for agent: ${options.agentDid}`,
|
|
146
|
+
{
|
|
147
|
+
correlationId,
|
|
148
|
+
url,
|
|
149
|
+
}
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const response = await this.config.fetchProvider.fetch(url, {
|
|
153
|
+
method: "GET",
|
|
154
|
+
headers: {
|
|
155
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
156
|
+
"Content-Type": "application/json",
|
|
157
|
+
"X-Request-ID": correlationId,
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const responseText = await response.text();
|
|
162
|
+
const responseData = this.parseResponseJSON(response, responseText);
|
|
163
|
+
|
|
164
|
+
// Handle error responses
|
|
165
|
+
if (!response.ok) {
|
|
166
|
+
this.handleErrorResponse(response, responseData);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Validate and parse success response
|
|
170
|
+
const parsed =
|
|
171
|
+
toolProtectionConfigAPIResponseSchema.safeParse(responseData);
|
|
172
|
+
if (!parsed.success) {
|
|
173
|
+
throw new AgentShieldAPIError(
|
|
174
|
+
"invalid_response",
|
|
175
|
+
"Response validation failed",
|
|
176
|
+
{ zodErrors: parsed.error.errors }
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
this.config.logger(`[AccessControl] Config fetched successfully`, {
|
|
181
|
+
correlationId,
|
|
182
|
+
agentDid: options.agentDid,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
return parsed.data;
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Verify a delegation token
|
|
191
|
+
*
|
|
192
|
+
* POST /api/v1/bouncer/delegations/verify
|
|
193
|
+
*/
|
|
194
|
+
async verifyDelegation(
|
|
195
|
+
request: VerifyDelegationRequest,
|
|
196
|
+
context?: {
|
|
197
|
+
delegationToken?: string;
|
|
198
|
+
credentialJwt?: string;
|
|
199
|
+
}
|
|
200
|
+
): Promise<VerifyDelegationAPIResponse> {
|
|
201
|
+
return this.retryWithBackoff(async () => {
|
|
202
|
+
const correlationId = generateCorrelationId();
|
|
203
|
+
const url = `${this.config.baseUrl}/api/v1/bouncer/delegations/verify`;
|
|
204
|
+
|
|
205
|
+
// Build request body dynamically to handle optional fields
|
|
206
|
+
const requestBody: Partial<VerifyDelegationRequest> & {
|
|
207
|
+
agent_did: string;
|
|
208
|
+
} = {
|
|
209
|
+
agent_did: request.agent_did,
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// Add optional fields only if they exist
|
|
213
|
+
if (request.scopes !== undefined) {
|
|
214
|
+
requestBody.scopes = request.scopes;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Handle credential_jwt: prefer request, fallback to context
|
|
218
|
+
if (request.credential_jwt !== undefined) {
|
|
219
|
+
requestBody.credential_jwt = request.credential_jwt;
|
|
220
|
+
} else if (context?.credentialJwt) {
|
|
221
|
+
requestBody.credential_jwt = context.credentialJwt;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Handle delegation_token: prefer request, fallback to context
|
|
225
|
+
if (request.delegation_token !== undefined) {
|
|
226
|
+
requestBody.delegation_token = request.delegation_token;
|
|
227
|
+
} else if (context?.delegationToken) {
|
|
228
|
+
requestBody.delegation_token = context.delegationToken;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (request.timestamp !== undefined) {
|
|
232
|
+
requestBody.timestamp = request.timestamp;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (request.client_info) {
|
|
236
|
+
requestBody.client_info = request.client_info;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Remove undefined values to ensure Zod validation passes (optional fields should be omitted, not undefined)
|
|
240
|
+
const cleanedRequestBody = Object.fromEntries(
|
|
241
|
+
Object.entries(requestBody).filter(([_, value]) => value !== undefined)
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
// Validate the cleaned request body
|
|
245
|
+
// Note: Workaround for Zod schema issue where .optional() doesn't properly handle omitted fields
|
|
246
|
+
// See AGENTSHIELD_MCPI_COMPLIANCE_REVIEW.md for details
|
|
247
|
+
const validationResult =
|
|
248
|
+
verifyDelegationRequestSchema.safeParse(cleanedRequestBody);
|
|
249
|
+
if (!validationResult.success) {
|
|
250
|
+
// Check if the error is specifically about scopes being required when it's omitted
|
|
251
|
+
const scopesError = validationResult.error.errors.find(
|
|
252
|
+
(e) => e.path.includes("scopes") && e.message === "Required"
|
|
253
|
+
);
|
|
254
|
+
if (scopesError && !("scopes" in cleanedRequestBody)) {
|
|
255
|
+
// This is a known Zod schema issue in AgentShield - scopes is optional but Zod treats it as required
|
|
256
|
+
// Skip validation for this case since the field is correctly omitted
|
|
257
|
+
// TODO: Remove this workaround once AgentShield fixes their schema
|
|
258
|
+
} else {
|
|
259
|
+
this.config.logger(`[AccessControl] Validation failed:`, {
|
|
260
|
+
errors: validationResult.error.errors,
|
|
261
|
+
requestBody: requestBody,
|
|
262
|
+
});
|
|
263
|
+
throw new AgentShieldAPIError(
|
|
264
|
+
"validation_error",
|
|
265
|
+
"Request validation failed",
|
|
266
|
+
{ zodErrors: validationResult.error.errors }
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
this.config.logger(
|
|
272
|
+
`[AccessControl] Verifying delegation for agent: ${request.agent_did}`,
|
|
273
|
+
{
|
|
274
|
+
correlationId,
|
|
275
|
+
url,
|
|
276
|
+
hasScopes: (request.scopes?.length || 0) > 0,
|
|
277
|
+
}
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
const response = await this.config.fetchProvider.fetch(url, {
|
|
281
|
+
method: "POST",
|
|
282
|
+
headers: {
|
|
283
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
284
|
+
"Content-Type": "application/json",
|
|
285
|
+
"X-Request-ID": correlationId,
|
|
286
|
+
},
|
|
287
|
+
// Use cleanedRequestBody directly instead of validated data
|
|
288
|
+
// because Zod seems to strip optional fields in some cases
|
|
289
|
+
body: JSON.stringify(cleanedRequestBody),
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const responseText = await response.text();
|
|
293
|
+
const responseData = this.parseResponseJSON(response, responseText);
|
|
294
|
+
|
|
295
|
+
// Handle error responses
|
|
296
|
+
if (!response.ok) {
|
|
297
|
+
this.handleErrorResponse(response, responseData);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Validate and parse success response
|
|
301
|
+
const parsed = verifyDelegationAPIResponseSchema.safeParse(responseData);
|
|
302
|
+
if (!parsed.success) {
|
|
303
|
+
throw new AgentShieldAPIError(
|
|
304
|
+
"invalid_response",
|
|
305
|
+
"Response validation failed",
|
|
306
|
+
{ zodErrors: parsed.error.errors }
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
this.config.logger(`[AccessControl] Delegation verified`, {
|
|
311
|
+
correlationId,
|
|
312
|
+
agentDid: request.agent_did,
|
|
313
|
+
valid: parsed.data.data.valid,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
return parsed.data;
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Submit proofs for audit and verification
|
|
322
|
+
*
|
|
323
|
+
* POST /api/v1/bouncer/proofs
|
|
324
|
+
*/
|
|
325
|
+
async submitProofs(
|
|
326
|
+
request: ProofSubmissionRequest
|
|
327
|
+
): Promise<ProofSubmissionResponse> {
|
|
328
|
+
return this.retryWithBackoff(async () => {
|
|
329
|
+
// Validate request directly - Zod's .nullish() should handle null/undefined correctly
|
|
330
|
+
const validationResult = proofSubmissionRequestSchema.safeParse(request);
|
|
331
|
+
if (!validationResult.success) {
|
|
332
|
+
// Log validation errors for debugging
|
|
333
|
+
const errorDetails = JSON.stringify(
|
|
334
|
+
validationResult.error.errors,
|
|
335
|
+
null,
|
|
336
|
+
2
|
|
337
|
+
);
|
|
338
|
+
this.config.logger(
|
|
339
|
+
`[AccessControl] Proof submission validation failed:`,
|
|
340
|
+
{
|
|
341
|
+
errors: validationResult.error.errors,
|
|
342
|
+
request: JSON.stringify(request, null, 2),
|
|
343
|
+
}
|
|
344
|
+
);
|
|
345
|
+
throw new AgentShieldAPIError(
|
|
346
|
+
"validation_error",
|
|
347
|
+
`Request validation failed: ${errorDetails}`,
|
|
348
|
+
{ zodErrors: validationResult.error.errors }
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Use validated request for the API call
|
|
353
|
+
const validatedRequest = validationResult.data;
|
|
354
|
+
|
|
355
|
+
const correlationId = generateCorrelationId();
|
|
356
|
+
const url = `${this.config.baseUrl}/api/v1/bouncer/proofs`;
|
|
357
|
+
|
|
358
|
+
this.config.logger(
|
|
359
|
+
`[AccessControl] Submitting ${request.proofs.length} proof(s)`,
|
|
360
|
+
{
|
|
361
|
+
correlationId,
|
|
362
|
+
url,
|
|
363
|
+
sessionId: request.session_id,
|
|
364
|
+
delegationId: request.delegation_id,
|
|
365
|
+
}
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
const httpResponse = await this.config.fetchProvider.fetch(url, {
|
|
369
|
+
method: "POST",
|
|
370
|
+
headers: {
|
|
371
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
372
|
+
"Content-Type": "application/json",
|
|
373
|
+
"X-Request-ID": correlationId,
|
|
374
|
+
},
|
|
375
|
+
body: JSON.stringify(validatedRequest),
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const responseText = await httpResponse.text();
|
|
379
|
+
|
|
380
|
+
// CRITICAL: Log raw response IMMEDIATELY before parsing/validation
|
|
381
|
+
// This will help us debug validation failures
|
|
382
|
+
console.error(`[AccessControl] 🔍 RAW API RESPONSE (before parsing):`, {
|
|
383
|
+
correlationId,
|
|
384
|
+
status: httpResponse.status,
|
|
385
|
+
statusText: httpResponse.statusText,
|
|
386
|
+
headers: Object.fromEntries(httpResponse.headers.entries()),
|
|
387
|
+
responseTextLength: responseText.length,
|
|
388
|
+
responseTextPreview: responseText.substring(0, 500),
|
|
389
|
+
fullResponseText: responseText, // Full response for debugging
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
const responseData = this.parseResponseJSON(httpResponse, responseText);
|
|
393
|
+
|
|
394
|
+
// Log parsed response immediately after parsing
|
|
395
|
+
console.error(`[AccessControl] 🔍 PARSED RESPONSE DATA:`, {
|
|
396
|
+
correlationId,
|
|
397
|
+
status: httpResponse.status,
|
|
398
|
+
responseDataType: typeof responseData,
|
|
399
|
+
responseDataKeys: Object.keys(
|
|
400
|
+
(responseData as Record<string, unknown>) || {}
|
|
401
|
+
),
|
|
402
|
+
responseData: JSON.stringify(responseData, null, 2),
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
// Handle error responses
|
|
406
|
+
if (!httpResponse.ok) {
|
|
407
|
+
const errorData = responseData as {
|
|
408
|
+
success?: boolean;
|
|
409
|
+
error?: AgentShieldAPIErrorResponse;
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
if (errorData.error) {
|
|
413
|
+
const errorCode = errorData.error.code || "api_error";
|
|
414
|
+
|
|
415
|
+
// Special handling for all_proofs_rejected - return response instead of throwing
|
|
416
|
+
if (
|
|
417
|
+
errorCode === "all_proofs_rejected" &&
|
|
418
|
+
httpResponse.status === 400
|
|
419
|
+
) {
|
|
420
|
+
// Parse the error details to extract accepted/rejected counts
|
|
421
|
+
const errorDetails = errorData.error.details as
|
|
422
|
+
| { rejected?: number; errors?: unknown[] }
|
|
423
|
+
| undefined;
|
|
424
|
+
// Create response matching ProofSubmissionResponse interface
|
|
425
|
+
// Validate structure with Zod to ensure type safety
|
|
426
|
+
const errorResponseData = {
|
|
427
|
+
success: false, // ProofSubmissionResponse has a success field
|
|
428
|
+
accepted: 0,
|
|
429
|
+
rejected: errorDetails?.rejected || request.proofs.length,
|
|
430
|
+
outcomes: {
|
|
431
|
+
success: 0,
|
|
432
|
+
failed: 0,
|
|
433
|
+
blocked: 0,
|
|
434
|
+
error: errorDetails?.rejected || request.proofs.length,
|
|
435
|
+
},
|
|
436
|
+
errors: errorDetails?.errors,
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
// Validate with Zod schema to ensure type safety
|
|
440
|
+
const validated =
|
|
441
|
+
proofSubmissionResponseSchema.safeParse(errorResponseData);
|
|
442
|
+
if (validated.success) {
|
|
443
|
+
return validated.data;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// If validation fails, log and throw error
|
|
447
|
+
throw new AgentShieldAPIError(
|
|
448
|
+
"invalid_response",
|
|
449
|
+
"Error response validation failed",
|
|
450
|
+
{ zodErrors: validated.error.errors }
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Ensure error details include status code for retry detection
|
|
455
|
+
const errorDetails = {
|
|
456
|
+
...(errorData.error.details || {}),
|
|
457
|
+
status: httpResponse.status,
|
|
458
|
+
};
|
|
459
|
+
throw new AgentShieldAPIError(
|
|
460
|
+
errorCode,
|
|
461
|
+
errorData.error.message || `API error: ${httpResponse.status}`,
|
|
462
|
+
errorDetails
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Map status codes to error codes
|
|
467
|
+
let errorCode = "api_error";
|
|
468
|
+
if (httpResponse.status === 400) {
|
|
469
|
+
errorCode = "validation_error";
|
|
470
|
+
} else if (httpResponse.status === 404) {
|
|
471
|
+
errorCode = httpResponse.statusText.includes("delegation")
|
|
472
|
+
? "delegation_not_found"
|
|
473
|
+
: "session_not_found";
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
throw new AgentShieldAPIError(
|
|
477
|
+
errorCode,
|
|
478
|
+
`API request failed: ${httpResponse.status} ${httpResponse.statusText}`,
|
|
479
|
+
{ status: httpResponse.status, responseData }
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Try to handle wrapped response format first
|
|
484
|
+
const wrappedResponse = responseData as {
|
|
485
|
+
success?: boolean;
|
|
486
|
+
data?: unknown;
|
|
487
|
+
metadata?: unknown;
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
// Log raw response for debugging (always log full response on first submission)
|
|
491
|
+
const rawResponseLog = {
|
|
492
|
+
correlationId,
|
|
493
|
+
hasSuccess: wrappedResponse.success !== undefined,
|
|
494
|
+
hasData: wrappedResponse.data !== undefined,
|
|
495
|
+
responseType: typeof responseData,
|
|
496
|
+
responseKeys: Object.keys(
|
|
497
|
+
(responseData as Record<string, unknown>) || {}
|
|
498
|
+
),
|
|
499
|
+
responseData: JSON.stringify(responseData, null, 2).substring(0, 2000), // Increased limit for debugging
|
|
500
|
+
};
|
|
501
|
+
this.config.logger(
|
|
502
|
+
`[AccessControl] Raw response received`,
|
|
503
|
+
rawResponseLog
|
|
504
|
+
);
|
|
505
|
+
// Also log to console.error for visibility (especially for first proof submission errors)
|
|
506
|
+
console.error(
|
|
507
|
+
`[AccessControl] Raw response received:`,
|
|
508
|
+
JSON.stringify(responseData, null, 2)
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
if (wrappedResponse.success !== undefined && wrappedResponse.data) {
|
|
512
|
+
// Response is wrapped in { success, data }
|
|
513
|
+
// Extract data and add success field if missing (for schema validation)
|
|
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
|
|
517
|
+
let dataToValidate: Record<string, unknown>;
|
|
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) {
|
|
526
|
+
throw new AgentShieldAPIError(
|
|
527
|
+
"invalid_response",
|
|
528
|
+
"Response data is not an object after cloning",
|
|
529
|
+
{
|
|
530
|
+
originalType: typeof rawData,
|
|
531
|
+
clonedType: typeof clonedData,
|
|
532
|
+
clonedValue: clonedData,
|
|
533
|
+
}
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
dataToValidate = clonedData as Record<string, unknown>;
|
|
538
|
+
} catch (cloneError) {
|
|
539
|
+
if (cloneError instanceof AgentShieldAPIError) {
|
|
540
|
+
throw cloneError;
|
|
541
|
+
}
|
|
542
|
+
throw new AgentShieldAPIError(
|
|
543
|
+
"invalid_response",
|
|
544
|
+
"Failed to clone response data",
|
|
545
|
+
{
|
|
546
|
+
error:
|
|
547
|
+
cloneError instanceof Error
|
|
548
|
+
? cloneError.message
|
|
549
|
+
: String(cloneError),
|
|
550
|
+
dataType: typeof wrappedResponse.data,
|
|
551
|
+
}
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// CRITICAL: Log the actual data structure for debugging
|
|
556
|
+
console.error(`[AccessControl] 🔍 DATA OBJECT STRUCTURE (after deep clone):`, {
|
|
557
|
+
correlationId,
|
|
558
|
+
dataKeys: Object.keys(dataToValidate),
|
|
559
|
+
hasAccepted: "accepted" in dataToValidate,
|
|
560
|
+
hasRejected: "rejected" in dataToValidate,
|
|
561
|
+
hasOutcomes: "outcomes" in dataToValidate,
|
|
562
|
+
hasErrors: "errors" in dataToValidate,
|
|
563
|
+
acceptedType: typeof dataToValidate.accepted,
|
|
564
|
+
acceptedValue: dataToValidate.accepted,
|
|
565
|
+
rejectedType: typeof dataToValidate.rejected,
|
|
566
|
+
rejectedValue: dataToValidate.rejected,
|
|
567
|
+
outcomesType: typeof dataToValidate.outcomes,
|
|
568
|
+
outcomesValue: dataToValidate.outcomes,
|
|
569
|
+
errorsType: typeof dataToValidate.errors,
|
|
570
|
+
errorsIsArray: Array.isArray(dataToValidate.errors),
|
|
571
|
+
fullData: JSON.stringify(dataToValidate, null, 2),
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
// Ensure success field is present (required by schema)
|
|
575
|
+
// wrappedResponse.success should be true since we checked it exists
|
|
576
|
+
// CRITICAL: Construct from the cloned data to ensure all fields are present
|
|
577
|
+
const dataWithSuccess: Record<string, unknown> = {
|
|
578
|
+
success: wrappedResponse.success === true,
|
|
579
|
+
accepted: dataToValidate.accepted,
|
|
580
|
+
rejected: dataToValidate.rejected,
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
// Add optional fields if present
|
|
584
|
+
if (
|
|
585
|
+
"outcomes" in dataToValidate &&
|
|
586
|
+
dataToValidate.outcomes !== undefined
|
|
587
|
+
) {
|
|
588
|
+
dataWithSuccess.outcomes = dataToValidate.outcomes;
|
|
589
|
+
}
|
|
590
|
+
if ("errors" in dataToValidate && dataToValidate.errors !== undefined) {
|
|
591
|
+
dataWithSuccess.errors = dataToValidate.errors;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// CRITICAL: Log what we're validating
|
|
595
|
+
console.error(`[AccessControl] 🔍 VALIDATING DATA WITH SUCCESS:`, {
|
|
596
|
+
correlationId,
|
|
597
|
+
dataWithSuccessKeys: Object.keys(dataWithSuccess),
|
|
598
|
+
hasSuccess: "success" in dataWithSuccess,
|
|
599
|
+
successValue: dataWithSuccess.success,
|
|
600
|
+
hasAccepted: "accepted" in dataWithSuccess,
|
|
601
|
+
acceptedValue: dataWithSuccess.accepted,
|
|
602
|
+
hasRejected: "rejected" in dataWithSuccess,
|
|
603
|
+
rejectedValue: dataWithSuccess.rejected,
|
|
604
|
+
hasOutcomes: "outcomes" in dataWithSuccess,
|
|
605
|
+
outcomesValue: dataWithSuccess.outcomes,
|
|
606
|
+
fullDataWithSuccess: JSON.stringify(dataWithSuccess, null, 2),
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
// CRITICAL: Ensure all required fields are present before validation
|
|
610
|
+
if (
|
|
611
|
+
typeof dataWithSuccess.accepted !== "number" ||
|
|
612
|
+
typeof dataWithSuccess.rejected !== "number"
|
|
613
|
+
) {
|
|
614
|
+
console.error(
|
|
615
|
+
`[AccessControl] ❌ MISSING REQUIRED FIELDS AFTER CONSTRUCTION:`,
|
|
616
|
+
{
|
|
617
|
+
correlationId,
|
|
618
|
+
hasAccepted: "accepted" in dataWithSuccess,
|
|
619
|
+
acceptedType: typeof dataWithSuccess.accepted,
|
|
620
|
+
acceptedValue: dataWithSuccess.accepted,
|
|
621
|
+
hasRejected: "rejected" in dataWithSuccess,
|
|
622
|
+
rejectedType: typeof dataWithSuccess.rejected,
|
|
623
|
+
rejectedValue: dataWithSuccess.rejected,
|
|
624
|
+
dataWithSuccessKeys: Object.keys(dataWithSuccess),
|
|
625
|
+
fullDataWithSuccess: JSON.stringify(dataWithSuccess, null, 2),
|
|
626
|
+
dataToValidateKeys: Object.keys(dataToValidate),
|
|
627
|
+
fullDataToValidate: JSON.stringify(dataToValidate, null, 2),
|
|
628
|
+
originalResponseData: JSON.stringify(responseData, null, 2),
|
|
629
|
+
}
|
|
630
|
+
);
|
|
631
|
+
throw new AgentShieldAPIError(
|
|
632
|
+
"invalid_response",
|
|
633
|
+
"Response data missing required fields (accepted/rejected)",
|
|
634
|
+
{
|
|
635
|
+
responseData,
|
|
636
|
+
dataToValidate,
|
|
637
|
+
dataWithSuccess,
|
|
638
|
+
}
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const dataParsed =
|
|
643
|
+
proofSubmissionResponseSchema.safeParse(dataWithSuccess);
|
|
644
|
+
if (dataParsed.success) {
|
|
645
|
+
this.config.logger(
|
|
646
|
+
`[AccessControl] Proofs submitted successfully (wrapped)`,
|
|
647
|
+
{
|
|
648
|
+
correlationId,
|
|
649
|
+
accepted: dataParsed.data.accepted,
|
|
650
|
+
rejected: dataParsed.data.rejected,
|
|
651
|
+
}
|
|
652
|
+
);
|
|
653
|
+
return dataParsed.data;
|
|
654
|
+
} else {
|
|
655
|
+
// Log validation errors for wrapped response (always log errors)
|
|
656
|
+
const validationErrorLog = {
|
|
657
|
+
correlationId,
|
|
658
|
+
zodErrors: dataParsed.error.errors,
|
|
659
|
+
zodErrorDetails: JSON.stringify(dataParsed.error.errors, null, 2),
|
|
660
|
+
dataToValidate: JSON.stringify(dataToValidate, null, 2).substring(
|
|
661
|
+
0,
|
|
662
|
+
2000
|
|
663
|
+
),
|
|
664
|
+
dataWithSuccess: JSON.stringify(dataWithSuccess, null, 2).substring(
|
|
665
|
+
0,
|
|
666
|
+
2000
|
|
667
|
+
),
|
|
668
|
+
dataKeys: Object.keys(dataToValidate),
|
|
669
|
+
dataWithSuccessKeys: Object.keys(dataWithSuccess),
|
|
670
|
+
originalResponse: JSON.stringify(responseData, null, 2).substring(
|
|
671
|
+
0,
|
|
672
|
+
2000
|
|
673
|
+
),
|
|
674
|
+
};
|
|
675
|
+
this.config.logger(
|
|
676
|
+
`[AccessControl] Wrapped response validation failed`,
|
|
677
|
+
validationErrorLog
|
|
678
|
+
);
|
|
679
|
+
// Also log to console.error for visibility in production
|
|
680
|
+
console.error(
|
|
681
|
+
`[AccessControl] Wrapped response validation failed`,
|
|
682
|
+
validationErrorLog
|
|
683
|
+
);
|
|
684
|
+
console.error(
|
|
685
|
+
`[AccessControl] Original wrapped response:`,
|
|
686
|
+
JSON.stringify(responseData, null, 2)
|
|
687
|
+
);
|
|
688
|
+
|
|
689
|
+
// CRITICAL: Log each zod error individually for easier debugging
|
|
690
|
+
console.error(
|
|
691
|
+
`[AccessControl] ❌ ZOD VALIDATION FAILED - ${dataParsed.error.errors.length} error(s):`
|
|
692
|
+
);
|
|
693
|
+
dataParsed.error.errors.forEach((err, idx) => {
|
|
694
|
+
const errorDetails: Record<string, unknown> = {
|
|
695
|
+
path: err.path.join(".") || "(root)",
|
|
696
|
+
message: err.message,
|
|
697
|
+
code: err.code,
|
|
698
|
+
};
|
|
699
|
+
// Only include properties that exist on specific error types
|
|
700
|
+
if ("received" in err) errorDetails.received = err.received;
|
|
701
|
+
if ("expected" in err) errorDetails.expected = err.expected;
|
|
702
|
+
if ("input" in err) errorDetails.input = err.input;
|
|
703
|
+
console.error(
|
|
704
|
+
`[AccessControl] Error ${idx + 1}:`,
|
|
705
|
+
JSON.stringify(errorDetails, null, 2)
|
|
706
|
+
);
|
|
707
|
+
});
|
|
708
|
+
console.error(
|
|
709
|
+
`[AccessControl] ❌ Full ZOD errors JSON:`,
|
|
710
|
+
JSON.stringify(dataParsed.error.errors, null, 2)
|
|
711
|
+
);
|
|
712
|
+
// CRITICAL: Throw error instead of falling through to direct parsing
|
|
713
|
+
// The response is wrapped, so direct parsing will also fail
|
|
714
|
+
throw new AgentShieldAPIError(
|
|
715
|
+
"invalid_response",
|
|
716
|
+
"Response validation failed",
|
|
717
|
+
{ zodErrors: dataParsed.error.errors, responseData }
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Try parsing as direct ProofSubmissionResponse
|
|
723
|
+
const parsed = proofSubmissionResponseSchema.safeParse(responseData);
|
|
724
|
+
if (!parsed.success) {
|
|
725
|
+
// Log detailed validation errors (always log errors)
|
|
726
|
+
const validationErrorLog = {
|
|
727
|
+
correlationId,
|
|
728
|
+
zodErrors: parsed.error.errors,
|
|
729
|
+
zodErrorDetails: JSON.stringify(parsed.error.errors, null, 2),
|
|
730
|
+
responseData: JSON.stringify(responseData, null, 2),
|
|
731
|
+
responseDataType: typeof responseData,
|
|
732
|
+
responseKeys: Object.keys(
|
|
733
|
+
(responseData as Record<string, unknown>) || {}
|
|
734
|
+
),
|
|
735
|
+
httpStatus: httpResponse.status,
|
|
736
|
+
httpStatusText: httpResponse.statusText,
|
|
737
|
+
};
|
|
738
|
+
this.config.logger(
|
|
739
|
+
`[AccessControl] Response validation failed`,
|
|
740
|
+
validationErrorLog
|
|
741
|
+
);
|
|
742
|
+
// CRITICAL: Log to console.error with full details for debugging
|
|
743
|
+
// This format matches test expectations: single call with message and error object
|
|
744
|
+
// This log must include 'Response validation failed' in the message for test compatibility
|
|
745
|
+
console.error(`[AccessControl] Response validation failed`, {
|
|
746
|
+
zodErrors: parsed.error.errors,
|
|
747
|
+
responseData: responseData,
|
|
748
|
+
});
|
|
749
|
+
// Additional detailed logging for debugging
|
|
750
|
+
console.error(
|
|
751
|
+
`[AccessControl] Response validation failed`,
|
|
752
|
+
validationErrorLog
|
|
753
|
+
);
|
|
754
|
+
|
|
755
|
+
// CRITICAL: Log each zod error individually for easier debugging
|
|
756
|
+
console.error(
|
|
757
|
+
`[AccessControl] ❌ ZOD VALIDATION FAILED (direct) - ${parsed.error.errors.length} error(s):`
|
|
758
|
+
);
|
|
759
|
+
parsed.error.errors.forEach((err, idx) => {
|
|
760
|
+
const errorDetails: Record<string, unknown> = {
|
|
761
|
+
path: err.path.join(".") || "(root)",
|
|
762
|
+
message: err.message,
|
|
763
|
+
code: err.code,
|
|
764
|
+
};
|
|
765
|
+
// Only include properties that exist on specific error types
|
|
766
|
+
if ("received" in err) errorDetails.received = err.received;
|
|
767
|
+
if ("expected" in err) errorDetails.expected = err.expected;
|
|
768
|
+
if ("input" in err) errorDetails.input = err.input;
|
|
769
|
+
console.error(`[AccessControl] Error ${idx + 1}:`, errorDetails);
|
|
770
|
+
});
|
|
771
|
+
console.error(
|
|
772
|
+
`[AccessControl] ❌ Full ZOD errors JSON:`,
|
|
773
|
+
JSON.stringify(parsed.error.errors, null, 2)
|
|
774
|
+
);
|
|
775
|
+
console.error(
|
|
776
|
+
`[AccessControl] ❌ ACTUAL RESPONSE DATA:`,
|
|
777
|
+
JSON.stringify(responseData, null, 2)
|
|
778
|
+
);
|
|
779
|
+
throw new AgentShieldAPIError(
|
|
780
|
+
"invalid_response",
|
|
781
|
+
"Response validation failed",
|
|
782
|
+
{ zodErrors: parsed.error.errors, responseData }
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
this.config.logger(`[AccessControl] Proofs submitted successfully`, {
|
|
787
|
+
correlationId,
|
|
788
|
+
accepted: parsed.data.accepted,
|
|
789
|
+
rejected: parsed.data.rejected,
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
return parsed.data;
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* Get current metrics
|
|
798
|
+
*/
|
|
799
|
+
getMetrics(): AccessControlApiServiceMetrics {
|
|
800
|
+
return { ...this.metrics };
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Reset metrics
|
|
805
|
+
*/
|
|
806
|
+
resetMetrics(): void {
|
|
807
|
+
this.metrics = {
|
|
808
|
+
successCount: 0,
|
|
809
|
+
errorCount: 0,
|
|
810
|
+
retryCount: 0,
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Retry logic with exponential backoff
|
|
816
|
+
*
|
|
817
|
+
* @internal
|
|
818
|
+
*/
|
|
819
|
+
private async retryWithBackoff<T>(
|
|
820
|
+
operation: () => Promise<T>,
|
|
821
|
+
retryCount = 0
|
|
822
|
+
): Promise<T> {
|
|
823
|
+
try {
|
|
824
|
+
const result = await operation();
|
|
825
|
+
this.metrics.successCount++;
|
|
826
|
+
return result;
|
|
827
|
+
} catch (error: unknown) {
|
|
828
|
+
// Check if error is retryable (5xx status codes)
|
|
829
|
+
const isRetryable = this.isRetryableError(error);
|
|
830
|
+
const { maxRetries, initialDelayMs, maxDelayMs } =
|
|
831
|
+
this.config.retryConfig;
|
|
832
|
+
|
|
833
|
+
if (isRetryable && retryCount < maxRetries) {
|
|
834
|
+
const delay = Math.min(
|
|
835
|
+
initialDelayMs * Math.pow(2, retryCount),
|
|
836
|
+
maxDelayMs
|
|
837
|
+
);
|
|
838
|
+
|
|
839
|
+
this.metrics.retryCount++;
|
|
840
|
+
this.config.logger(
|
|
841
|
+
`Retrying after ${delay}ms (attempt ${retryCount + 1}/${maxRetries})`
|
|
842
|
+
);
|
|
843
|
+
|
|
844
|
+
await this.sleep(delay);
|
|
845
|
+
return this.retryWithBackoff(operation, retryCount + 1);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
this.metrics.errorCount++;
|
|
849
|
+
throw error;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* Check if an error is retryable (5xx status codes)
|
|
855
|
+
*
|
|
856
|
+
* @internal
|
|
857
|
+
*/
|
|
858
|
+
private isRetryableError(error: unknown): boolean {
|
|
859
|
+
// Network errors (fetch failures) are retryable
|
|
860
|
+
if (error instanceof TypeError && error.message.includes("fetch")) {
|
|
861
|
+
return true;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// AgentShieldAPIError with 5xx status codes are retryable
|
|
865
|
+
if (error instanceof AgentShieldAPIError) {
|
|
866
|
+
// Check error code for server errors
|
|
867
|
+
if (error.code === "server_error") {
|
|
868
|
+
return true;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// Check if error details contain status code
|
|
872
|
+
const status = (error.details as { status?: number } | undefined)?.status;
|
|
873
|
+
if (status && status >= 500 && status < 600) {
|
|
874
|
+
return true;
|
|
875
|
+
}
|
|
876
|
+
// Rate limiting (429) is also retryable
|
|
877
|
+
if (status === 429) {
|
|
878
|
+
return true;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Check for timeout errors
|
|
883
|
+
if (error instanceof Error) {
|
|
884
|
+
const message = error.message.toLowerCase();
|
|
885
|
+
if (message.includes("timeout") || message.includes("timed out")) {
|
|
886
|
+
return true;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
return false;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* Sleep utility for retry delays
|
|
895
|
+
* Uses platform-agnostic sleep provider
|
|
896
|
+
*
|
|
897
|
+
* @internal
|
|
898
|
+
*/
|
|
899
|
+
private sleep(ms: number): Promise<void> {
|
|
900
|
+
return this.config.sleepProvider(ms);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* Parse response text to JSON with error handling
|
|
905
|
+
*
|
|
906
|
+
* @internal
|
|
907
|
+
*/
|
|
908
|
+
private parseResponseJSON(response: Response, responseText: string): unknown {
|
|
909
|
+
try {
|
|
910
|
+
return JSON.parse(responseText);
|
|
911
|
+
} catch (error) {
|
|
912
|
+
// Handle non-JSON error responses (e.g., plain text "Internal Server Error")
|
|
913
|
+
if (!response.ok) {
|
|
914
|
+
const errorCode = this.mapStatusToErrorCode(response.status);
|
|
915
|
+
throw new AgentShieldAPIError(
|
|
916
|
+
errorCode,
|
|
917
|
+
`API request failed: ${response.status} ${response.statusText}`,
|
|
918
|
+
{
|
|
919
|
+
status: response.status,
|
|
920
|
+
responseText: responseText.substring(0, 500),
|
|
921
|
+
}
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
// For success responses that aren't JSON, throw invalid_response error
|
|
925
|
+
throw new AgentShieldAPIError(
|
|
926
|
+
"invalid_response",
|
|
927
|
+
`Failed to parse response: ${error instanceof Error ? error.message : String(error)}`,
|
|
928
|
+
{ responseText: responseText.substring(0, 500) }
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* Map HTTP status codes to error codes
|
|
935
|
+
*
|
|
936
|
+
* @internal
|
|
937
|
+
*/
|
|
938
|
+
private mapStatusToErrorCode(status: number, statusText = ""): string {
|
|
939
|
+
if (status === 400) {
|
|
940
|
+
return "validation_error";
|
|
941
|
+
} else if (status === 401 || status === 403) {
|
|
942
|
+
return "authentication_failed";
|
|
943
|
+
} else if (status === 404) {
|
|
944
|
+
return "config_not_found";
|
|
945
|
+
} else if (status >= 500) {
|
|
946
|
+
return "server_error";
|
|
947
|
+
}
|
|
948
|
+
return "api_error";
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* Handle error responses and throw appropriate AgentShieldAPIError
|
|
953
|
+
*
|
|
954
|
+
* @internal
|
|
955
|
+
*/
|
|
956
|
+
private handleErrorResponse(
|
|
957
|
+
response: Response,
|
|
958
|
+
responseData: unknown
|
|
959
|
+
): never {
|
|
960
|
+
const errorData = responseData as {
|
|
961
|
+
success?: boolean;
|
|
962
|
+
error?: AgentShieldAPIErrorResponse;
|
|
963
|
+
};
|
|
964
|
+
|
|
965
|
+
if (errorData.error) {
|
|
966
|
+
const errorCode = errorData.error.code || "api_error";
|
|
967
|
+
// Ensure error details include status code for retry detection
|
|
968
|
+
const errorDetails = {
|
|
969
|
+
...(errorData.error.details || {}),
|
|
970
|
+
status: response.status,
|
|
971
|
+
};
|
|
972
|
+
throw new AgentShieldAPIError(
|
|
973
|
+
errorCode,
|
|
974
|
+
errorData.error.message || `API error: ${response.status}`,
|
|
975
|
+
errorDetails
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// Map status codes to error codes
|
|
980
|
+
const errorCode = this.mapStatusToErrorCode(
|
|
981
|
+
response.status,
|
|
982
|
+
response.statusText
|
|
983
|
+
);
|
|
984
|
+
throw new AgentShieldAPIError(
|
|
985
|
+
errorCode,
|
|
986
|
+
`API request failed: ${response.status} ${response.statusText}`,
|
|
987
|
+
{ status: response.status, responseData }
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
}
|