@kya-os/mcp-i-core 1.2.2-canary.25 → 1.2.2-canary.26

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.
Files changed (147) hide show
  1. package/.claude/settings.local.json +9 -0
  2. package/.turbo/turbo-build.log +4 -0
  3. package/.turbo/turbo-test$colon$coverage.log +3756 -0
  4. package/.turbo/turbo-test.log +2398 -0
  5. package/COMPLIANCE_IMPROVEMENT_REPORT.md +483 -0
  6. package/Composer 3.md +615 -0
  7. package/GPT-5.md +1169 -0
  8. package/OPUS-plan.md +352 -0
  9. package/PHASE_3_AND_4.1_SUMMARY.md +585 -0
  10. package/PHASE_3_SUMMARY.md +317 -0
  11. package/PHASE_4.1.3_SUMMARY.md +428 -0
  12. package/PHASE_4.1_COMPLETE.md +525 -0
  13. package/PHASE_4_USER_DID_IDENTITY_LINKING_PLAN.md +1240 -0
  14. package/SCHEMA_COMPLIANCE_REPORT.md +275 -0
  15. package/TEST_PLAN.md +571 -0
  16. package/dist/services/authorization/authorization-registry.d.ts +29 -0
  17. package/dist/services/authorization/authorization-registry.d.ts.map +1 -0
  18. package/dist/services/authorization/authorization-registry.js +57 -0
  19. package/dist/services/authorization/authorization-registry.js.map +1 -0
  20. package/dist/services/authorization/types.d.ts +53 -0
  21. package/dist/services/authorization/types.d.ts.map +1 -0
  22. package/dist/services/authorization/types.js +10 -0
  23. package/dist/services/authorization/types.js.map +1 -0
  24. package/docs/API_REFERENCE.md +1362 -0
  25. package/docs/COMPLIANCE_MATRIX.md +691 -0
  26. package/docs/STATUSLIST2021_GUIDE.md +696 -0
  27. package/docs/W3C_VC_DELEGATION_GUIDE.md +710 -0
  28. package/package.json +20 -64
  29. package/scripts/audit-compliance.ts +724 -0
  30. package/src/__tests__/cache/tool-protection-cache.test.ts +640 -0
  31. package/src/__tests__/config/provider-runtime-config.test.ts +309 -0
  32. package/src/__tests__/delegation-e2e.test.ts +690 -0
  33. package/src/__tests__/identity/user-did-manager.test.ts +213 -0
  34. package/src/__tests__/index.test.ts +56 -0
  35. package/src/__tests__/integration/full-flow.test.ts +776 -0
  36. package/src/__tests__/integration.test.ts +281 -0
  37. package/src/__tests__/providers/base.test.ts +173 -0
  38. package/src/__tests__/providers/memory.test.ts +319 -0
  39. package/src/__tests__/regression/phase2-regression.test.ts +427 -0
  40. package/src/__tests__/runtime/audit-logger.test.ts +154 -0
  41. package/src/__tests__/runtime/base-extensions.test.ts +593 -0
  42. package/src/__tests__/runtime/base.test.ts +869 -0
  43. package/src/__tests__/runtime/delegation-flow.test.ts +164 -0
  44. package/src/__tests__/runtime/proof-client-did.test.ts +375 -0
  45. package/src/__tests__/runtime/route-interception.test.ts +686 -0
  46. package/src/__tests__/runtime/tool-protection-enforcement.test.ts +908 -0
  47. package/src/__tests__/services/agentshield-integration.test.ts +784 -0
  48. package/src/__tests__/services/provider-resolver-edge-cases.test.ts +487 -0
  49. package/src/__tests__/services/tool-protection-oauth-provider.test.ts +480 -0
  50. package/src/__tests__/services/tool-protection.service.test.ts +1366 -0
  51. package/src/__tests__/utils/mock-providers.ts +340 -0
  52. package/src/cache/oauth-config-cache.d.ts +69 -0
  53. package/src/cache/oauth-config-cache.d.ts.map +1 -0
  54. package/src/cache/oauth-config-cache.js +71 -0
  55. package/src/cache/oauth-config-cache.js.map +1 -0
  56. package/src/cache/oauth-config-cache.ts +123 -0
  57. package/src/cache/tool-protection-cache.ts +171 -0
  58. package/src/compliance/EXAMPLE.md +412 -0
  59. package/src/compliance/__tests__/schema-verifier.test.ts +797 -0
  60. package/src/compliance/index.ts +8 -0
  61. package/src/compliance/schema-registry.ts +460 -0
  62. package/src/compliance/schema-verifier.ts +708 -0
  63. package/src/config/__tests__/remote-config.spec.ts +268 -0
  64. package/src/config/remote-config.ts +174 -0
  65. package/src/config.ts +309 -0
  66. package/src/delegation/__tests__/audience-validator.test.ts +112 -0
  67. package/src/delegation/__tests__/bitstring.test.ts +346 -0
  68. package/src/delegation/__tests__/cascading-revocation.test.ts +628 -0
  69. package/src/delegation/__tests__/delegation-graph.test.ts +584 -0
  70. package/src/delegation/__tests__/utils.test.ts +152 -0
  71. package/src/delegation/__tests__/vc-issuer.test.ts +442 -0
  72. package/src/delegation/__tests__/vc-verifier.test.ts +922 -0
  73. package/src/delegation/audience-validator.ts +52 -0
  74. package/src/delegation/bitstring.ts +278 -0
  75. package/src/delegation/cascading-revocation.ts +370 -0
  76. package/src/delegation/delegation-graph.ts +299 -0
  77. package/src/delegation/index.ts +14 -0
  78. package/src/delegation/statuslist-manager.ts +353 -0
  79. package/src/delegation/storage/__tests__/memory-graph-storage.test.ts +366 -0
  80. package/src/delegation/storage/__tests__/memory-statuslist-storage.test.ts +228 -0
  81. package/src/delegation/storage/index.ts +9 -0
  82. package/src/delegation/storage/memory-graph-storage.ts +178 -0
  83. package/src/delegation/storage/memory-statuslist-storage.ts +77 -0
  84. package/src/delegation/utils.ts +42 -0
  85. package/src/delegation/vc-issuer.ts +232 -0
  86. package/src/delegation/vc-verifier.ts +568 -0
  87. package/src/identity/idp-token-resolver.ts +147 -0
  88. package/src/identity/idp-token-storage.interface.ts +59 -0
  89. package/src/identity/user-did-manager.ts +370 -0
  90. package/src/index.ts +260 -0
  91. package/src/providers/base.d.ts +91 -0
  92. package/src/providers/base.d.ts.map +1 -0
  93. package/src/providers/base.js +38 -0
  94. package/src/providers/base.js.map +1 -0
  95. package/src/providers/base.ts +96 -0
  96. package/src/providers/memory.ts +142 -0
  97. package/src/runtime/audit-logger.ts +39 -0
  98. package/src/runtime/base.ts +1329 -0
  99. package/src/services/__tests__/access-control.integration.test.ts +443 -0
  100. package/src/services/__tests__/access-control.service.test.ts +970 -0
  101. package/src/services/__tests__/batch-delegation.service.test.ts +351 -0
  102. package/src/services/__tests__/crypto.service.test.ts +531 -0
  103. package/src/services/__tests__/oauth-provider-registry.test.ts +142 -0
  104. package/src/services/__tests__/proof-verifier.integration.test.ts +485 -0
  105. package/src/services/__tests__/proof-verifier.test.ts +489 -0
  106. package/src/services/__tests__/provider-resolution.integration.test.ts +198 -0
  107. package/src/services/__tests__/provider-resolver.test.ts +217 -0
  108. package/src/services/__tests__/storage.service.test.ts +358 -0
  109. package/src/services/access-control.service.ts +877 -0
  110. package/src/services/authorization/authorization-registry.ts +66 -0
  111. package/src/services/authorization/types.ts +71 -0
  112. package/src/services/batch-delegation.service.ts +137 -0
  113. package/src/services/crypto.service.ts +302 -0
  114. package/src/services/errors.ts +76 -0
  115. package/src/services/index.ts +9 -0
  116. package/src/services/oauth-config.service.d.ts +53 -0
  117. package/src/services/oauth-config.service.d.ts.map +1 -0
  118. package/src/services/oauth-config.service.js +113 -0
  119. package/src/services/oauth-config.service.js.map +1 -0
  120. package/src/services/oauth-config.service.ts +166 -0
  121. package/src/services/oauth-provider-registry.d.ts +57 -0
  122. package/src/services/oauth-provider-registry.d.ts.map +1 -0
  123. package/src/services/oauth-provider-registry.js +73 -0
  124. package/src/services/oauth-provider-registry.js.map +1 -0
  125. package/src/services/oauth-provider-registry.ts +123 -0
  126. package/src/services/oauth-service.ts +510 -0
  127. package/src/services/oauth-token-retrieval.service.ts +245 -0
  128. package/src/services/proof-verifier.ts +478 -0
  129. package/src/services/provider-resolver.d.ts +48 -0
  130. package/src/services/provider-resolver.d.ts.map +1 -0
  131. package/src/services/provider-resolver.js +106 -0
  132. package/src/services/provider-resolver.js.map +1 -0
  133. package/src/services/provider-resolver.ts +144 -0
  134. package/src/services/provider-validator.ts +170 -0
  135. package/src/services/storage.service.ts +566 -0
  136. package/src/services/tool-context-builder.ts +172 -0
  137. package/src/services/tool-protection.service.ts +798 -0
  138. package/src/types/oauth-required-error.ts +63 -0
  139. package/src/types/tool-protection.ts +155 -0
  140. package/src/utils/__tests__/did-helpers.test.ts +101 -0
  141. package/src/utils/base64.ts +148 -0
  142. package/src/utils/cors.ts +83 -0
  143. package/src/utils/did-helpers.ts +150 -0
  144. package/src/utils/index.ts +8 -0
  145. package/src/utils/storage-keys.ts +278 -0
  146. package/tsconfig.json +21 -0
  147. package/vitest.config.ts +56 -0
@@ -0,0 +1,877 @@
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((responseData as Record<string, unknown>) || {}),
400
+ responseData: JSON.stringify(responseData, null, 2),
401
+ });
402
+
403
+ // Handle error responses
404
+ if (!httpResponse.ok) {
405
+ const errorData = responseData as {
406
+ success?: boolean;
407
+ error?: AgentShieldAPIErrorResponse;
408
+ };
409
+
410
+ if (errorData.error) {
411
+ const errorCode = errorData.error.code || "api_error";
412
+
413
+ // Special handling for all_proofs_rejected - return response instead of throwing
414
+ if (
415
+ errorCode === "all_proofs_rejected" &&
416
+ httpResponse.status === 400
417
+ ) {
418
+ // Parse the error details to extract accepted/rejected counts
419
+ const errorDetails = errorData.error.details as
420
+ | { rejected?: number; errors?: unknown[] }
421
+ | undefined;
422
+ // Create response matching ProofSubmissionResponse interface
423
+ // Validate structure with Zod to ensure type safety
424
+ const errorResponseData = {
425
+ success: false, // ProofSubmissionResponse has a success field
426
+ accepted: 0,
427
+ rejected: errorDetails?.rejected || request.proofs.length,
428
+ outcomes: {
429
+ success: 0,
430
+ failed: 0,
431
+ blocked: 0,
432
+ error: errorDetails?.rejected || request.proofs.length,
433
+ },
434
+ errors: errorDetails?.errors,
435
+ };
436
+
437
+ // Validate with Zod schema to ensure type safety
438
+ const validated =
439
+ proofSubmissionResponseSchema.safeParse(errorResponseData);
440
+ if (validated.success) {
441
+ return validated.data;
442
+ }
443
+
444
+ // If validation fails, log and throw error
445
+ throw new AgentShieldAPIError(
446
+ "invalid_response",
447
+ "Error response validation failed",
448
+ { zodErrors: validated.error.errors }
449
+ );
450
+ }
451
+
452
+ // Ensure error details include status code for retry detection
453
+ const errorDetails = {
454
+ ...(errorData.error.details || {}),
455
+ status: httpResponse.status,
456
+ };
457
+ throw new AgentShieldAPIError(
458
+ errorCode,
459
+ errorData.error.message || `API error: ${httpResponse.status}`,
460
+ errorDetails
461
+ );
462
+ }
463
+
464
+ // Map status codes to error codes
465
+ let errorCode = "api_error";
466
+ if (httpResponse.status === 400) {
467
+ errorCode = "validation_error";
468
+ } else if (httpResponse.status === 404) {
469
+ errorCode = httpResponse.statusText.includes("delegation")
470
+ ? "delegation_not_found"
471
+ : "session_not_found";
472
+ }
473
+
474
+ throw new AgentShieldAPIError(
475
+ errorCode,
476
+ `API request failed: ${httpResponse.status} ${httpResponse.statusText}`,
477
+ { status: httpResponse.status, responseData }
478
+ );
479
+ }
480
+
481
+ // Try to handle wrapped response format first
482
+ const wrappedResponse = responseData as {
483
+ success?: boolean;
484
+ data?: unknown;
485
+ metadata?: unknown;
486
+ };
487
+
488
+ // Log raw response for debugging (always log full response on first submission)
489
+ const rawResponseLog = {
490
+ correlationId,
491
+ hasSuccess: wrappedResponse.success !== undefined,
492
+ hasData: wrappedResponse.data !== undefined,
493
+ responseType: typeof responseData,
494
+ responseKeys: Object.keys(
495
+ (responseData as Record<string, unknown>) || {}
496
+ ),
497
+ responseData: JSON.stringify(responseData, null, 2).substring(0, 2000), // Increased limit for debugging
498
+ };
499
+ this.config.logger(
500
+ `[AccessControl] Raw response received`,
501
+ rawResponseLog
502
+ );
503
+ // Also log to console.error for visibility (especially for first proof submission errors)
504
+ console.error(
505
+ `[AccessControl] Raw response received:`,
506
+ JSON.stringify(responseData, null, 2)
507
+ );
508
+
509
+ if (wrappedResponse.success !== undefined && wrappedResponse.data) {
510
+ // Response is wrapped in { success, data }
511
+ // Extract data and add success field if missing (for schema validation)
512
+ const dataToValidate = wrappedResponse.data as Record<string, unknown>;
513
+
514
+ // CRITICAL: Log the actual data structure for debugging
515
+ console.error(`[AccessControl] 🔍 DATA OBJECT STRUCTURE:`, {
516
+ correlationId,
517
+ dataKeys: Object.keys(dataToValidate),
518
+ hasAccepted: 'accepted' in dataToValidate,
519
+ hasRejected: 'rejected' in dataToValidate,
520
+ hasOutcomes: 'outcomes' in dataToValidate,
521
+ hasErrors: 'errors' in dataToValidate,
522
+ acceptedType: typeof dataToValidate.accepted,
523
+ rejectedType: typeof dataToValidate.rejected,
524
+ outcomesType: typeof dataToValidate.outcomes,
525
+ errorsType: typeof dataToValidate.errors,
526
+ errorsIsArray: Array.isArray(dataToValidate.errors),
527
+ fullData: JSON.stringify(dataToValidate, null, 2),
528
+ });
529
+
530
+ // Ensure success field is present (required by schema)
531
+ // wrappedResponse.success should be true since we checked it exists
532
+ const dataWithSuccess = {
533
+ ...dataToValidate,
534
+ success: wrappedResponse.success === true, // Explicitly use wrapped success value
535
+ };
536
+
537
+ const dataParsed =
538
+ proofSubmissionResponseSchema.safeParse(dataWithSuccess);
539
+ if (dataParsed.success) {
540
+ this.config.logger(
541
+ `[AccessControl] Proofs submitted successfully (wrapped)`,
542
+ {
543
+ correlationId,
544
+ accepted: dataParsed.data.accepted,
545
+ rejected: dataParsed.data.rejected,
546
+ }
547
+ );
548
+ return dataParsed.data;
549
+ } else {
550
+ // Log validation errors for wrapped response (always log errors)
551
+ const validationErrorLog = {
552
+ correlationId,
553
+ zodErrors: dataParsed.error.errors,
554
+ zodErrorDetails: JSON.stringify(dataParsed.error.errors, null, 2),
555
+ dataToValidate: JSON.stringify(dataToValidate, null, 2).substring(
556
+ 0,
557
+ 2000
558
+ ),
559
+ dataWithSuccess: JSON.stringify(dataWithSuccess, null, 2).substring(
560
+ 0,
561
+ 2000
562
+ ),
563
+ dataKeys: Object.keys(dataToValidate),
564
+ originalResponse: JSON.stringify(responseData, null, 2).substring(
565
+ 0,
566
+ 2000
567
+ ),
568
+ };
569
+ this.config.logger(
570
+ `[AccessControl] Wrapped response validation failed`,
571
+ validationErrorLog
572
+ );
573
+ // Also log to console.error for visibility in production
574
+ console.error(
575
+ `[AccessControl] Wrapped response validation failed`,
576
+ validationErrorLog
577
+ );
578
+ console.error(
579
+ `[AccessControl] Original wrapped response:`,
580
+ JSON.stringify(responseData, null, 2)
581
+ );
582
+
583
+ // CRITICAL: Log each zod error individually for easier debugging
584
+ console.error(
585
+ `[AccessControl] ❌ ZOD VALIDATION FAILED - ${dataParsed.error.errors.length} error(s):`
586
+ );
587
+ dataParsed.error.errors.forEach((err, idx) => {
588
+ const errorDetails: Record<string, unknown> = {
589
+ path: err.path.join('.') || '(root)',
590
+ message: err.message,
591
+ code: err.code,
592
+ };
593
+ // Only include properties that exist on specific error types
594
+ if ('received' in err) errorDetails.received = err.received;
595
+ if ('expected' in err) errorDetails.expected = err.expected;
596
+ if ('input' in err) errorDetails.input = err.input;
597
+ console.error(`[AccessControl] Error ${idx + 1}:`, errorDetails);
598
+ });
599
+ console.error(
600
+ `[AccessControl] ❌ Full ZOD errors JSON:`,
601
+ JSON.stringify(dataParsed.error.errors, null, 2)
602
+ );
603
+ }
604
+ }
605
+
606
+ // Try parsing as direct ProofSubmissionResponse
607
+ const parsed = proofSubmissionResponseSchema.safeParse(responseData);
608
+ if (!parsed.success) {
609
+ // Log detailed validation errors (always log errors)
610
+ const validationErrorLog = {
611
+ correlationId,
612
+ zodErrors: parsed.error.errors,
613
+ zodErrorDetails: JSON.stringify(parsed.error.errors, null, 2),
614
+ responseData: JSON.stringify(responseData, null, 2),
615
+ responseDataType: typeof responseData,
616
+ responseKeys: Object.keys(
617
+ (responseData as Record<string, unknown>) || {}
618
+ ),
619
+ httpStatus: httpResponse.status,
620
+ httpStatusText: httpResponse.statusText,
621
+ };
622
+ this.config.logger(
623
+ `[AccessControl] Response validation failed`,
624
+ validationErrorLog
625
+ );
626
+ // CRITICAL: Log to console.error with full details for debugging
627
+ // This format matches test expectations: single call with message and error object
628
+ // This log must include 'Response validation failed' in the message for test compatibility
629
+ console.error(
630
+ `[AccessControl] Response validation failed`,
631
+ {
632
+ zodErrors: parsed.error.errors,
633
+ responseData: responseData,
634
+ }
635
+ );
636
+ // Additional detailed logging for debugging
637
+ console.error(
638
+ `[AccessControl] Response validation failed`,
639
+ validationErrorLog
640
+ );
641
+
642
+ // CRITICAL: Log each zod error individually for easier debugging
643
+ console.error(
644
+ `[AccessControl] ❌ ZOD VALIDATION FAILED (direct) - ${parsed.error.errors.length} error(s):`
645
+ );
646
+ parsed.error.errors.forEach((err, idx) => {
647
+ const errorDetails: Record<string, unknown> = {
648
+ path: err.path.join('.') || '(root)',
649
+ message: err.message,
650
+ code: err.code,
651
+ };
652
+ // Only include properties that exist on specific error types
653
+ if ('received' in err) errorDetails.received = err.received;
654
+ if ('expected' in err) errorDetails.expected = err.expected;
655
+ if ('input' in err) errorDetails.input = err.input;
656
+ console.error(`[AccessControl] Error ${idx + 1}:`, errorDetails);
657
+ });
658
+ console.error(
659
+ `[AccessControl] ❌ Full ZOD errors JSON:`,
660
+ JSON.stringify(parsed.error.errors, null, 2)
661
+ );
662
+ console.error(
663
+ `[AccessControl] ❌ ACTUAL RESPONSE DATA:`,
664
+ JSON.stringify(responseData, null, 2)
665
+ );
666
+ throw new AgentShieldAPIError(
667
+ "invalid_response",
668
+ "Response validation failed",
669
+ { zodErrors: parsed.error.errors, responseData }
670
+ );
671
+ }
672
+
673
+ this.config.logger(`[AccessControl] Proofs submitted successfully`, {
674
+ correlationId,
675
+ accepted: parsed.data.accepted,
676
+ rejected: parsed.data.rejected,
677
+ });
678
+
679
+ return parsed.data;
680
+ });
681
+ }
682
+
683
+ /**
684
+ * Get current metrics
685
+ */
686
+ getMetrics(): AccessControlApiServiceMetrics {
687
+ return { ...this.metrics };
688
+ }
689
+
690
+ /**
691
+ * Reset metrics
692
+ */
693
+ resetMetrics(): void {
694
+ this.metrics = {
695
+ successCount: 0,
696
+ errorCount: 0,
697
+ retryCount: 0,
698
+ };
699
+ }
700
+
701
+ /**
702
+ * Retry logic with exponential backoff
703
+ *
704
+ * @internal
705
+ */
706
+ private async retryWithBackoff<T>(
707
+ operation: () => Promise<T>,
708
+ retryCount = 0
709
+ ): Promise<T> {
710
+ try {
711
+ const result = await operation();
712
+ this.metrics.successCount++;
713
+ return result;
714
+ } catch (error: unknown) {
715
+ // Check if error is retryable (5xx status codes)
716
+ const isRetryable = this.isRetryableError(error);
717
+ const { maxRetries, initialDelayMs, maxDelayMs } =
718
+ this.config.retryConfig;
719
+
720
+ if (isRetryable && retryCount < maxRetries) {
721
+ const delay = Math.min(
722
+ initialDelayMs * Math.pow(2, retryCount),
723
+ maxDelayMs
724
+ );
725
+
726
+ this.metrics.retryCount++;
727
+ this.config.logger(
728
+ `Retrying after ${delay}ms (attempt ${retryCount + 1}/${maxRetries})`
729
+ );
730
+
731
+ await this.sleep(delay);
732
+ return this.retryWithBackoff(operation, retryCount + 1);
733
+ }
734
+
735
+ this.metrics.errorCount++;
736
+ throw error;
737
+ }
738
+ }
739
+
740
+ /**
741
+ * Check if an error is retryable (5xx status codes)
742
+ *
743
+ * @internal
744
+ */
745
+ private isRetryableError(error: unknown): boolean {
746
+ // Network errors (fetch failures) are retryable
747
+ if (error instanceof TypeError && error.message.includes("fetch")) {
748
+ return true;
749
+ }
750
+
751
+ // AgentShieldAPIError with 5xx status codes are retryable
752
+ if (error instanceof AgentShieldAPIError) {
753
+ // Check error code for server errors
754
+ if (error.code === "server_error") {
755
+ return true;
756
+ }
757
+
758
+ // Check if error details contain status code
759
+ const status = (error.details as { status?: number } | undefined)?.status;
760
+ if (status && status >= 500 && status < 600) {
761
+ return true;
762
+ }
763
+ // Rate limiting (429) is also retryable
764
+ if (status === 429) {
765
+ return true;
766
+ }
767
+ }
768
+
769
+ // Check for timeout errors
770
+ if (error instanceof Error) {
771
+ const message = error.message.toLowerCase();
772
+ if (message.includes("timeout") || message.includes("timed out")) {
773
+ return true;
774
+ }
775
+ }
776
+
777
+ return false;
778
+ }
779
+
780
+ /**
781
+ * Sleep utility for retry delays
782
+ * Uses platform-agnostic sleep provider
783
+ *
784
+ * @internal
785
+ */
786
+ private sleep(ms: number): Promise<void> {
787
+ return this.config.sleepProvider(ms);
788
+ }
789
+
790
+ /**
791
+ * Parse response text to JSON with error handling
792
+ *
793
+ * @internal
794
+ */
795
+ private parseResponseJSON(response: Response, responseText: string): unknown {
796
+ try {
797
+ return JSON.parse(responseText);
798
+ } catch (error) {
799
+ // Handle non-JSON error responses (e.g., plain text "Internal Server Error")
800
+ if (!response.ok) {
801
+ const errorCode = this.mapStatusToErrorCode(response.status);
802
+ throw new AgentShieldAPIError(
803
+ errorCode,
804
+ `API request failed: ${response.status} ${response.statusText}`,
805
+ {
806
+ status: response.status,
807
+ responseText: responseText.substring(0, 500),
808
+ }
809
+ );
810
+ }
811
+ // For success responses that aren't JSON, throw invalid_response error
812
+ throw new AgentShieldAPIError(
813
+ "invalid_response",
814
+ `Failed to parse response: ${error instanceof Error ? error.message : String(error)}`,
815
+ { responseText: responseText.substring(0, 500) }
816
+ );
817
+ }
818
+ }
819
+
820
+ /**
821
+ * Map HTTP status codes to error codes
822
+ *
823
+ * @internal
824
+ */
825
+ private mapStatusToErrorCode(status: number, statusText = ""): string {
826
+ if (status === 400) {
827
+ return "validation_error";
828
+ } else if (status === 401 || status === 403) {
829
+ return "authentication_failed";
830
+ } else if (status === 404) {
831
+ return "config_not_found";
832
+ } else if (status >= 500) {
833
+ return "server_error";
834
+ }
835
+ return "api_error";
836
+ }
837
+
838
+ /**
839
+ * Handle error responses and throw appropriate AgentShieldAPIError
840
+ *
841
+ * @internal
842
+ */
843
+ private handleErrorResponse(
844
+ response: Response,
845
+ responseData: unknown
846
+ ): never {
847
+ const errorData = responseData as {
848
+ success?: boolean;
849
+ error?: AgentShieldAPIErrorResponse;
850
+ };
851
+
852
+ if (errorData.error) {
853
+ const errorCode = errorData.error.code || "api_error";
854
+ // Ensure error details include status code for retry detection
855
+ const errorDetails = {
856
+ ...(errorData.error.details || {}),
857
+ status: response.status,
858
+ };
859
+ throw new AgentShieldAPIError(
860
+ errorCode,
861
+ errorData.error.message || `API error: ${response.status}`,
862
+ errorDetails
863
+ );
864
+ }
865
+
866
+ // Map status codes to error codes
867
+ const errorCode = this.mapStatusToErrorCode(
868
+ response.status,
869
+ response.statusText
870
+ );
871
+ throw new AgentShieldAPIError(
872
+ errorCode,
873
+ `API request failed: ${response.status} ${response.statusText}`,
874
+ { status: response.status, responseData }
875
+ );
876
+ }
877
+ }