@kya-os/mcp-i-cloudflare 1.5.7 → 1.5.8-canary.10

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 (42) hide show
  1. package/dist/__tests__/e2e/test-config.d.ts +37 -0
  2. package/dist/__tests__/e2e/test-config.d.ts.map +1 -0
  3. package/dist/__tests__/e2e/test-config.js +62 -0
  4. package/dist/__tests__/e2e/test-config.js.map +1 -0
  5. package/dist/adapter.d.ts.map +1 -1
  6. package/dist/adapter.js +90 -47
  7. package/dist/adapter.js.map +1 -1
  8. package/dist/app.d.ts.map +1 -1
  9. package/dist/app.js +14 -0
  10. package/dist/app.js.map +1 -1
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/config.js +36 -2
  13. package/dist/config.js.map +1 -1
  14. package/dist/runtime.d.ts +12 -0
  15. package/dist/runtime.d.ts.map +1 -1
  16. package/dist/runtime.js +36 -1
  17. package/dist/runtime.js.map +1 -1
  18. package/dist/server.d.ts.map +1 -1
  19. package/dist/server.js +48 -2
  20. package/dist/server.js.map +1 -1
  21. package/dist/services/admin.service.d.ts.map +1 -1
  22. package/dist/services/admin.service.js +21 -20
  23. package/dist/services/admin.service.js.map +1 -1
  24. package/dist/services/consent-audit.service.d.ts +91 -0
  25. package/dist/services/consent-audit.service.d.ts.map +1 -0
  26. package/dist/services/consent-audit.service.js +241 -0
  27. package/dist/services/consent-audit.service.js.map +1 -0
  28. package/dist/services/consent-config.service.d.ts.map +1 -1
  29. package/dist/services/consent-config.service.js +3 -7
  30. package/dist/services/consent-config.service.js.map +1 -1
  31. package/dist/services/consent-page-renderer.d.ts.map +1 -1
  32. package/dist/services/consent-page-renderer.js +10 -28
  33. package/dist/services/consent-page-renderer.js.map +1 -1
  34. package/dist/services/consent.service.d.ts +53 -0
  35. package/dist/services/consent.service.d.ts.map +1 -1
  36. package/dist/services/consent.service.js +1429 -118
  37. package/dist/services/consent.service.js.map +1 -1
  38. package/dist/services/proof.service.d.ts +5 -3
  39. package/dist/services/proof.service.d.ts.map +1 -1
  40. package/dist/services/proof.service.js +19 -6
  41. package/dist/services/proof.service.js.map +1 -1
  42. package/package.json +8 -5
@@ -15,17 +15,144 @@ import { validateConsentApprovalRequest, } from "@kya-os/contracts/consent";
15
15
  import { AGENTSHIELD_ENDPOINTS, createDelegationAPIResponseSchema, createDelegationResponseSchema, } from "@kya-os/contracts/agentshield-api";
16
16
  import { UserDidManager } from "@kya-os/mcp-i-core";
17
17
  import { WebCryptoProvider } from "../providers/crypto";
18
+ import { ConsentAuditService } from "./consent-audit.service";
19
+ import { CloudflareProofGenerator } from "../proof-generator";
20
+ import { ProofService } from "./proof.service";
21
+ import { fetchRemoteConfig } from "@kya-os/mcp-i-core/config/remote-config";
18
22
  export class ConsentService {
19
23
  configService;
20
24
  renderer;
21
25
  env;
22
26
  runtime;
23
27
  userDidManager; // Cached instance for consistent DID generation
28
+ // ✅ Audit service - lazy initialized
29
+ auditService;
30
+ auditInitPromise; // Cache promise to prevent race conditions
31
+ /**
32
+ * ✅ FIXED: Constructor takes env: CloudflareEnv, not config
33
+ */
24
34
  constructor(env, runtime) {
25
35
  this.env = env;
26
36
  this.runtime = runtime;
27
37
  this.configService = new ConsentConfigService(env);
28
38
  this.renderer = new ConsentPageRenderer();
39
+ // No initialization here - keep constructor synchronous
40
+ }
41
+ /**
42
+ * Get or initialize audit service (lazy initialization)
43
+ *
44
+ * Fetches config from remote API when projectId is available.
45
+ * Uses promise caching to prevent race conditions.
46
+ *
47
+ * @param projectId - Project ID from consent request (required for config fetch)
48
+ */
49
+ async getAuditService(projectId) {
50
+ // Already initialized
51
+ if (this.auditService) {
52
+ return this.auditService;
53
+ }
54
+ // No runtime - audit not available
55
+ if (!this.runtime) {
56
+ return undefined;
57
+ }
58
+ // Initialization in progress - wait for it
59
+ if (this.auditInitPromise) {
60
+ await this.auditInitPromise;
61
+ return this.auditService;
62
+ }
63
+ // Start initialization (with projectId for config fetch)
64
+ this.auditInitPromise = this.initializeAuditService(projectId);
65
+ try {
66
+ await this.auditInitPromise;
67
+ }
68
+ catch (error) {
69
+ console.warn("[ConsentService] Audit service initialization failed:", error);
70
+ // Don't throw - audit failures shouldn't break consent flow
71
+ }
72
+ return this.auditService;
73
+ }
74
+ /**
75
+ * Initialize audit service - fetches config from remote API
76
+ *
77
+ * ⚠️ CRITICAL: Fetches config from remote API using fetchRemoteConfig()
78
+ * This is the ONLY way to get CloudflareRuntimeConfig per requirement.
79
+ */
80
+ async initializeAuditService(projectId) {
81
+ if (!this.runtime) {
82
+ return;
83
+ }
84
+ try {
85
+ // ✅ CRITICAL: Fetch config from remote API
86
+ const config = await this.getConfigFromRemoteAPI(projectId);
87
+ if (!config?.proofing?.enabled) {
88
+ console.log("[ConsentService] Proofing not enabled in remote config");
89
+ return; // Proofing not enabled
90
+ }
91
+ // Get identity (async - requires runtime to be initialized)
92
+ const identity = await this.runtime.getIdentity();
93
+ // ✅ FIXED: CloudflareProofGenerator only takes identity, not providers
94
+ const proofGenerator = new CloudflareProofGenerator(identity);
95
+ // Get audit logger
96
+ const auditLogger = this.runtime.getAuditLogger();
97
+ if (!auditLogger) {
98
+ console.warn("[ConsentService] AuditLogger not available");
99
+ return;
100
+ }
101
+ // Create audit service with fetched config
102
+ this.auditService = new ConsentAuditService(new ProofService(config, this.runtime), auditLogger, proofGenerator, config, // ✅ Config fetched from remote API
103
+ this.runtime);
104
+ console.log("[ConsentService] Audit service initialized successfully");
105
+ }
106
+ catch (error) {
107
+ console.error("[ConsentService] Failed to initialize audit service:", error);
108
+ // Don't throw - audit failures shouldn't break consent flow
109
+ }
110
+ }
111
+ /**
112
+ * Fetch CloudflareRuntimeConfig from remote API (AgentShield)
113
+ *
114
+ * ⚠️ CRITICAL: Config MUST be fetched from remote API, not constructed from env.
115
+ *
116
+ * Uses existing `fetchRemoteConfig()` from `@kya-os/mcp-i-core/config/remote-config`
117
+ * which handles caching, error handling, and API communication.
118
+ *
119
+ * @param projectId - Project ID from consent request
120
+ * @returns Runtime config or undefined if unavailable
121
+ */
122
+ async getConfigFromRemoteAPI(projectId) {
123
+ if (!this.env.AGENTSHIELD_API_KEY) {
124
+ console.warn("[ConsentService] No API key for runtime config fetch");
125
+ return undefined;
126
+ }
127
+ try {
128
+ // Create KV cache adapter
129
+ const cache = this.env.TOOL_PROTECTION_KV
130
+ ? {
131
+ get: async (key) => {
132
+ return ((await this.env.TOOL_PROTECTION_KV.get(key, "text")) || null);
133
+ },
134
+ set: async (key, value, ttl) => {
135
+ await this.env.TOOL_PROTECTION_KV.put(key, value, {
136
+ expirationTtl: Math.floor(ttl / 1000),
137
+ });
138
+ },
139
+ }
140
+ : undefined;
141
+ const config = await fetchRemoteConfig({
142
+ apiUrl: this.env.AGENTSHIELD_API_URL || "https://kya.vouched.id",
143
+ apiKey: this.env.AGENTSHIELD_API_KEY,
144
+ projectId, // ✅ Use projectId from consent request
145
+ cacheTtl: 300000, // 5 minutes
146
+ fetchProvider: fetch,
147
+ }, cache);
148
+ // fetchRemoteConfig returns MCPIConfig | null
149
+ // CloudflareRuntimeConfig extends MCPIConfig, so cast is safe
150
+ return config;
151
+ }
152
+ catch (error) {
153
+ console.warn("[ConsentService] Error fetching runtime config:", error);
154
+ return undefined;
155
+ }
29
156
  }
30
157
  /**
31
158
  * Get or generate User DID for a session
@@ -38,13 +165,21 @@ export class ConsentService {
38
165
  * @returns User DID (did:key format)
39
166
  */
40
167
  async getUserDidForSession(sessionId, oauthIdentity) {
168
+ // Handle null explicitly (from JSON parsing)
169
+ const hasOAuthIdentity = oauthIdentity &&
170
+ typeof oauthIdentity === "object" &&
171
+ oauthIdentity.provider &&
172
+ oauthIdentity.subject;
41
173
  // If OAuth identity provided, check for existing mapping first
42
- if (oauthIdentity && this.env.DELEGATION_STORAGE) {
174
+ if (hasOAuthIdentity && this.env.DELEGATION_STORAGE) {
43
175
  try {
44
176
  const oauthKey = STORAGE_KEYS.oauthIdentity(oauthIdentity.provider, oauthIdentity.subject);
45
177
  const mappedUserDid = await this.env.DELEGATION_STORAGE.get(oauthKey, "text");
46
178
  if (mappedUserDid) {
47
- console.log("[ConsentService] Found persistent User DID from OAuth mapping");
179
+ console.log("[ConsentService] Found persistent User DID from OAuth mapping:", {
180
+ provider: oauthIdentity.provider,
181
+ userDid: mappedUserDid.substring(0, 20) + "...",
182
+ });
48
183
  return mappedUserDid;
49
184
  }
50
185
  }
@@ -53,6 +188,10 @@ export class ConsentService {
53
188
  // Continue with ephemeral DID generation
54
189
  }
55
190
  }
191
+ else if (oauthIdentity === null) {
192
+ // Explicitly handle null case (no OAuth)
193
+ console.log("[ConsentService] No OAuth identity provided (null), generating ephemeral DID");
194
+ }
56
195
  // Continue with existing ephemeral DID generation logic
57
196
  if (!this.env.DELEGATION_STORAGE) {
58
197
  // No storage - use cached UserDidManager instance for consistent DID generation
@@ -143,8 +282,7 @@ export class ConsentService {
143
282
  const configUrl = `${agentShieldUrl}/api/v1/bouncer/projects/${projectId}/config`;
144
283
  const response = await fetch(configUrl, {
145
284
  headers: {
146
- "X-API-Key": apiKey,
147
- "X-Project-Id": projectId,
285
+ Authorization: `Bearer ${apiKey}`,
148
286
  "Content-Type": "application/json",
149
287
  },
150
288
  });
@@ -404,6 +542,29 @@ export class ConsentService {
404
542
  });
405
543
  return Response.redirect(oauthUrl, 302);
406
544
  }
545
+ // ✅ Lazy initialization with projectId
546
+ const auditService = await this.getAuditService(projectId);
547
+ // Log page view event (if audit service available)
548
+ if (auditService) {
549
+ await auditService
550
+ .logConsentPageView({
551
+ sessionId,
552
+ agentDid,
553
+ targetTools: [tool], // Wrap in array
554
+ scopes,
555
+ projectId,
556
+ })
557
+ .catch((err) => {
558
+ // Structured error logging
559
+ console.error("[ConsentService] Audit logging failed", {
560
+ eventType: "consent:page_viewed",
561
+ sessionId,
562
+ error: err instanceof Error ? err.message : String(err),
563
+ stack: err instanceof Error ? err.stack : undefined,
564
+ });
565
+ // Don't throw - audit failures shouldn't break consent flow
566
+ });
567
+ }
407
568
  // Build consent page config
408
569
  const pageConfig = {
409
570
  tool,
@@ -441,78 +602,1103 @@ export class ConsentService {
441
602
  }
442
603
  }
443
604
  /**
444
- * Handle consent approval
605
+ * Parse request body from JSON or FormData
445
606
  *
446
- * Validates request, creates delegation via AgentShield API,
447
- * stores token in KV, and returns success response.
607
+ * Handles both JSON and FormData/multipart requests, converting
608
+ * FormData fields to the correct format for ConsentApprovalRequest.
448
609
  *
449
- * @param request - Approval request
450
- * @returns JSON response
610
+ * @param request - Request to parse
611
+ * @returns Parsed body object
451
612
  */
452
- async handleApproval(request) {
453
- try {
454
- // Parse request body - handle both JSON and FormData
455
- const contentType = request.headers.get("content-type") || "";
456
- let body;
457
- if (contentType.includes("application/json")) {
458
- // JSON request (from JavaScript fetch)
459
- body = await request.json();
460
- }
461
- else if (contentType.includes("application/x-www-form-urlencoded") ||
462
- contentType.includes("multipart/form-data")) {
463
- // FormData request (fallback if JavaScript doesn't intercept)
464
- const formData = await request.formData();
465
- body = {
466
- tool: formData.get("tool"),
467
- scopes: formData.get("scopes")
468
- ? JSON.parse(formData.get("scopes"))
469
- : [],
470
- agent_did: formData.get("agent_did"),
471
- session_id: formData.get("session_id"),
472
- project_id: formData.get("project_id"),
473
- termsAccepted: formData.get("termsAccepted") === "on" ||
474
- formData.get("termsAccepted") === "true",
475
- customFields: {},
476
- };
477
- // Extract OAuth identity if present
478
- const oauthIdentityJson = formData.get("oauth_identity_json");
479
- if (oauthIdentityJson && typeof oauthIdentityJson === "string") {
613
+ async parseRequestBody(request) {
614
+ const contentType = request.headers.get("content-type") || "";
615
+ // Helper function to parse form fields into body object
616
+ const parseFormFields = (entries) => {
617
+ const body = {};
618
+ for (const [key, value] of entries) {
619
+ // Handle special fields that need parsing
620
+ if (key === "scopes" || key === "scopes[]") {
621
+ // Scopes come as JSON string
480
622
  try {
481
- const parsed = JSON.parse(oauthIdentityJson);
482
- if (parsed && parsed.provider && parsed.subject) {
483
- body.oauth_identity = parsed;
484
- }
623
+ body["scopes"] = JSON.parse(value);
485
624
  }
486
625
  catch {
487
- // Ignore invalid OAuth identity
626
+ // If not JSON, treat as single scope or array
627
+ body["scopes"] = value.includes(",") ? value.split(",") : [value];
488
628
  }
489
629
  }
490
- // Extract custom fields (if any)
491
- // Note: Custom fields would need to be extracted from formData if present
492
- // For now, we'll use empty object as customFields are optional
630
+ else if (key === "oauth_identity" || key === "oauth_identity_json") {
631
+ // OAuth identity comes as JSON string
632
+ try {
633
+ const parsed = JSON.parse(value);
634
+ body["oauth_identity"] = parsed || null;
635
+ }
636
+ catch {
637
+ // Invalid JSON, set to null
638
+ body["oauth_identity"] = null;
639
+ }
640
+ }
641
+ else if (key === "termsAccepted") {
642
+ // Convert checkbox values to boolean
643
+ body[key] =
644
+ value === "on" ||
645
+ value === "true" ||
646
+ value === "1" ||
647
+ value === "yes";
648
+ }
649
+ else if (key === "customFields") {
650
+ // Custom fields come as JSON string
651
+ try {
652
+ body[key] = JSON.parse(value);
653
+ }
654
+ catch {
655
+ // If not JSON, skip
656
+ }
657
+ }
658
+ else {
659
+ // Regular string fields
660
+ body[key] = value;
661
+ }
493
662
  }
494
- else {
495
- // Try JSON first, fallback to form data
663
+ // Default termsAccepted to true if not provided (common for checkboxes)
664
+ if (!("termsAccepted" in body)) {
665
+ body.termsAccepted = true;
666
+ }
667
+ // Default scopes to empty array if not provided (required by schema but can be empty)
668
+ if (!("scopes" in body)) {
669
+ body.scopes = [];
670
+ }
671
+ return body;
672
+ };
673
+ // Parse URL-encoded form data (application/x-www-form-urlencoded)
674
+ // When Content-Type is URL-encoded but FormData object is passed, try FormData parsing first
675
+ if (contentType.includes("application/x-www-form-urlencoded")) {
676
+ // Try FormData parsing first (FormData object might have been passed)
677
+ try {
678
+ const clonedRequest = request.clone();
679
+ const formData = await clonedRequest.formData();
680
+ const body = {};
681
+ let hasValidFields = false;
682
+ // Check if FormData parsing returned malformed data (entire multipart body as single entry)
683
+ // Type assertion: FormData.entries() exists at runtime in Cloudflare Workers
684
+ const entriesArray = Array.from(formData.entries());
685
+ if (entriesArray.length === 1 &&
686
+ typeof entriesArray[0][0] === "string" &&
687
+ entriesArray[0][0].includes("Content-Disposition") &&
688
+ typeof entriesArray[0][1] === "string" &&
689
+ entriesArray[0][1].toString().includes("Content-Disposition")) {
690
+ // Manually parse multipart format from the value string
691
+ // The key contains the first boundary and Content-Disposition header start
692
+ // The value contains the rest of the first field and all subsequent fields
693
+ const keyPart = entriesArray[0][0];
694
+ const valuePart = entriesArray[0][1];
695
+ // Fix: If key ends with "name" and value starts with '"fieldname"', combine them
696
+ let multipartBody;
697
+ if (keyPart.trim().endsWith("name") &&
698
+ valuePart.trim().startsWith('"')) {
699
+ // Key ends with "name", value starts with '"fieldname"', combine with = between them
700
+ multipartBody = keyPart + "=" + valuePart;
701
+ }
702
+ else {
703
+ multipartBody = keyPart + valuePart;
704
+ }
705
+ // Match: Content-Disposition header with field name, then capture value until next boundary or end
706
+ const fieldRegex = /Content-Disposition:\s*form-data;\s*name="([^"]+)"\r?\n\r?\n([\s\S]*?)(?=\r?\n------|$)/g;
707
+ let match;
708
+ while ((match = fieldRegex.exec(multipartBody)) !== null) {
709
+ const fieldName = match[1];
710
+ const fieldValue = match[2].trim(); // Remove trailing newlines
711
+ if (fieldName === "scopes" || fieldName === "scopes[]") {
712
+ try {
713
+ body["scopes"] = JSON.parse(fieldValue);
714
+ }
715
+ catch {
716
+ body["scopes"] = fieldValue.includes(",")
717
+ ? fieldValue.split(",")
718
+ : [fieldValue];
719
+ }
720
+ }
721
+ else if (fieldName === "oauth_identity" ||
722
+ fieldName === "oauth_identity_json") {
723
+ try {
724
+ body["oauth_identity"] = JSON.parse(fieldValue) || null;
725
+ }
726
+ catch {
727
+ body["oauth_identity"] = null;
728
+ }
729
+ }
730
+ else if (fieldName === "termsAccepted") {
731
+ body[fieldName] =
732
+ fieldValue === "on" ||
733
+ fieldValue === "true" ||
734
+ fieldValue === "1" ||
735
+ fieldValue === "yes";
736
+ }
737
+ else if (fieldName === "customFields") {
738
+ try {
739
+ body[fieldName] = JSON.parse(fieldValue);
740
+ }
741
+ catch {
742
+ // Skip
743
+ }
744
+ }
745
+ else {
746
+ body[fieldName] = fieldValue;
747
+ }
748
+ hasValidFields = true;
749
+ }
750
+ }
751
+ else {
752
+ // Normal FormData parsing
753
+ // Type assertion: FormData.entries() exists at runtime in Cloudflare Workers
754
+ for (const [key, value] of formData.entries()) {
755
+ if (value instanceof File) {
756
+ continue;
757
+ }
758
+ // Extract field name from malformed keys (when FormData is passed with wrong Content-Type)
759
+ let fieldName = key;
760
+ if (key.includes("Content-Disposition") && key.includes('name="')) {
761
+ // Extract field name from Content-Disposition header
762
+ const nameMatch = key.match(/name="([^"]+)"/);
763
+ if (nameMatch && nameMatch[1]) {
764
+ fieldName = nameMatch[1];
765
+ }
766
+ else {
767
+ // Skip if we can't extract a valid field name
768
+ continue;
769
+ }
770
+ }
771
+ else if (key.includes("------") ||
772
+ key.includes("\r\n") ||
773
+ key.trim() === "") {
774
+ // Skip boundary strings and invalid keys
775
+ continue;
776
+ }
777
+ hasValidFields = true;
778
+ const stringValue = value.toString();
779
+ if (fieldName === "scopes" || fieldName === "scopes[]") {
780
+ try {
781
+ body["scopes"] = JSON.parse(stringValue);
782
+ }
783
+ catch {
784
+ body["scopes"] = stringValue.includes(",")
785
+ ? stringValue.split(",")
786
+ : [stringValue];
787
+ }
788
+ }
789
+ else if (fieldName === "oauth_identity" ||
790
+ fieldName === "oauth_identity_json") {
791
+ try {
792
+ body["oauth_identity"] = JSON.parse(stringValue) || null;
793
+ }
794
+ catch {
795
+ body["oauth_identity"] = null;
796
+ }
797
+ }
798
+ else if (fieldName === "termsAccepted") {
799
+ body[fieldName] =
800
+ stringValue === "on" ||
801
+ stringValue === "true" ||
802
+ stringValue === "1" ||
803
+ stringValue === "yes";
804
+ }
805
+ else if (fieldName === "customFields") {
806
+ try {
807
+ body[fieldName] = JSON.parse(stringValue);
808
+ }
809
+ catch {
810
+ // Skip
811
+ }
812
+ }
813
+ else {
814
+ body[fieldName] = stringValue;
815
+ }
816
+ }
817
+ }
818
+ if (hasValidFields && Object.keys(body).length > 0) {
819
+ if (!("termsAccepted" in body)) {
820
+ body.termsAccepted = true;
821
+ }
822
+ // Default scopes to empty array if not provided
823
+ if (!("scopes" in body)) {
824
+ body.scopes = [];
825
+ }
826
+ return body;
827
+ }
828
+ }
829
+ catch (formDataError) {
830
+ // FormData parsing failed, try URL-encoded text parsing
831
+ console.warn("[ConsentService] FormData parsing failed, trying URL-encoded text:", formDataError);
832
+ }
833
+ // Try URL-encoded text parsing as fallback
834
+ try {
835
+ const text = await request.clone().text();
836
+ const params = new URLSearchParams(text);
837
+ // Type assertion: URLSearchParams.entries() exists at runtime
838
+ return parseFormFields(params.entries());
839
+ }
840
+ catch (urlEncodedError) {
841
+ // Both failed, fall through to JSON parsing
842
+ console.warn("[ConsentService] URL-encoded parsing also failed:", urlEncodedError);
843
+ }
844
+ }
845
+ // Parse multipart FormData (multipart/form-data or when FormData object is passed with other Content-Type)
846
+ // Include text/plain here to handle cases where FormData is passed with mismatched Content-Type
847
+ if (contentType.includes("multipart/form-data") ||
848
+ contentType.includes("form") ||
849
+ contentType === "" ||
850
+ contentType.includes("text/plain") ||
851
+ contentType.includes("text")) {
852
+ // Check if multipart/form-data Content-Type is missing boundary
853
+ // If so, try to parse as text first (FormData might have been serialized incorrectly)
854
+ if (contentType.includes("multipart/form-data") &&
855
+ !contentType.includes("boundary=")) {
496
856
  try {
497
- body = await request.json();
857
+ const textRequest = request.clone();
858
+ const text = await textRequest.text();
859
+ if (text && text.length > 0) {
860
+ // Try to manually parse multipart format from text
861
+ const fieldRegex = /Content-Disposition:\s*form-data;\s*name="([^"]+)"\r?\n\r?\n([\s\S]*?)(?=\r?\n------|$)/g;
862
+ const body = {};
863
+ let match;
864
+ let hasValidFields = false;
865
+ while ((match = fieldRegex.exec(text)) !== null) {
866
+ const fieldName = match[1];
867
+ const fieldValue = match[2].trim();
868
+ if (fieldName === "scopes" || fieldName === "scopes[]") {
869
+ try {
870
+ body["scopes"] = JSON.parse(fieldValue);
871
+ }
872
+ catch {
873
+ body["scopes"] = fieldValue.includes(",")
874
+ ? fieldValue.split(",")
875
+ : [fieldValue];
876
+ }
877
+ }
878
+ else if (fieldName === "oauth_identity" ||
879
+ fieldName === "oauth_identity_json") {
880
+ try {
881
+ body["oauth_identity"] = JSON.parse(fieldValue) || null;
882
+ }
883
+ catch {
884
+ body["oauth_identity"] = null;
885
+ }
886
+ }
887
+ else if (fieldName === "termsAccepted") {
888
+ body[fieldName] =
889
+ fieldValue === "on" ||
890
+ fieldValue === "true" ||
891
+ fieldValue === "1" ||
892
+ fieldValue === "yes";
893
+ }
894
+ else if (fieldName === "customFields") {
895
+ try {
896
+ body[fieldName] = JSON.parse(fieldValue);
897
+ }
898
+ catch {
899
+ // Skip
900
+ }
901
+ }
902
+ else {
903
+ body[fieldName] = fieldValue;
904
+ }
905
+ hasValidFields = true;
906
+ }
907
+ if (hasValidFields && Object.keys(body).length > 0) {
908
+ if (!("termsAccepted" in body)) {
909
+ body.termsAccepted = true;
910
+ }
911
+ // Default scopes to empty array if not provided
912
+ if (!("scopes" in body)) {
913
+ body.scopes = [];
914
+ }
915
+ return body;
916
+ }
917
+ }
498
918
  }
499
919
  catch {
500
- const formData = await request.formData();
501
- body = {
502
- tool: formData.get("tool"),
503
- scopes: formData.get("scopes")
504
- ? JSON.parse(formData.get("scopes"))
505
- : [],
506
- agent_did: formData.get("agent_did"),
507
- session_id: formData.get("session_id"),
508
- project_id: formData.get("project_id"),
509
- termsAccepted: formData.get("termsAccepted") === "on",
510
- customFields: {},
511
- };
920
+ // If text parsing fails, fall through to FormData parsing
512
921
  }
513
922
  }
923
+ try {
924
+ const clonedRequest = request.clone();
925
+ let formData;
926
+ try {
927
+ formData = await clonedRequest.formData();
928
+ }
929
+ catch (formDataError) {
930
+ // Handle FormData parsing errors (missing boundary, wrong Content-Type, etc.)
931
+ const errorMessage = formDataError instanceof Error
932
+ ? formDataError.message
933
+ : String(formDataError);
934
+ const errorCause = formDataError instanceof Error && "cause" in formDataError
935
+ ? formDataError.cause instanceof Error
936
+ ? formDataError.cause.message
937
+ : String(formDataError.cause)
938
+ : "";
939
+ if (errorMessage.includes("missing boundary") ||
940
+ errorMessage.includes("boundary") ||
941
+ errorMessage.includes("Content-Type was not one of") ||
942
+ errorCause.includes("missing boundary") ||
943
+ errorCause.includes("boundary")) {
944
+ // When boundary is missing, try to parse as URL-encoded form data instead
945
+ // This handles the case where FormData was passed but Content-Type doesn't include boundary
946
+ try {
947
+ const textRequest = request.clone();
948
+ const text = await textRequest.text();
949
+ // If text parsing succeeds, try URL-encoded parsing
950
+ if (text && text.length > 0) {
951
+ try {
952
+ const params = new URLSearchParams(text);
953
+ return parseFormFields(params.entries());
954
+ }
955
+ catch {
956
+ // If URL-encoded parsing fails, the text might be multipart format
957
+ // Try to manually parse multipart format from text
958
+ const fieldRegex = /Content-Disposition:\s*form-data;\s*name="([^"]+)"\r?\n\r?\n([\s\S]*?)(?=\r?\n------|$)/g;
959
+ const body = {};
960
+ let match;
961
+ let hasValidFields = false;
962
+ while ((match = fieldRegex.exec(text)) !== null) {
963
+ const fieldName = match[1];
964
+ const fieldValue = match[2].trim();
965
+ if (fieldName === "scopes" || fieldName === "scopes[]") {
966
+ try {
967
+ body["scopes"] = JSON.parse(fieldValue);
968
+ }
969
+ catch {
970
+ body["scopes"] = fieldValue.includes(",")
971
+ ? fieldValue.split(",")
972
+ : [fieldValue];
973
+ }
974
+ }
975
+ else if (fieldName === "oauth_identity" ||
976
+ fieldName === "oauth_identity_json") {
977
+ try {
978
+ body["oauth_identity"] = JSON.parse(fieldValue) || null;
979
+ }
980
+ catch {
981
+ body["oauth_identity"] = null;
982
+ }
983
+ }
984
+ else if (fieldName === "termsAccepted") {
985
+ body[fieldName] =
986
+ fieldValue === "on" ||
987
+ fieldValue === "true" ||
988
+ fieldValue === "1" ||
989
+ fieldValue === "yes";
990
+ }
991
+ else if (fieldName === "customFields") {
992
+ try {
993
+ body[fieldName] = JSON.parse(fieldValue);
994
+ }
995
+ catch {
996
+ // Skip
997
+ }
998
+ }
999
+ else {
1000
+ body[fieldName] = fieldValue;
1001
+ }
1002
+ hasValidFields = true;
1003
+ }
1004
+ if (hasValidFields && Object.keys(body).length > 0) {
1005
+ if (!("termsAccepted" in body)) {
1006
+ body.termsAccepted = true;
1007
+ }
1008
+ // Default scopes to empty array if not provided
1009
+ if (!("scopes" in body)) {
1010
+ body.scopes = [];
1011
+ }
1012
+ return body;
1013
+ }
1014
+ }
1015
+ }
1016
+ }
1017
+ catch {
1018
+ // If all parsing attempts fail, rethrow the original error
1019
+ }
1020
+ }
1021
+ throw formDataError;
1022
+ }
1023
+ const body = {};
1024
+ let hasValidFields = false;
1025
+ // Check if FormData parsing returned malformed data (entire multipart body as single entry)
1026
+ // Type assertion: FormData.entries() exists at runtime in Cloudflare Workers
1027
+ const entriesArray = Array.from(formData.entries());
1028
+ if (entriesArray.length === 1 &&
1029
+ typeof entriesArray[0][0] === "string" &&
1030
+ entriesArray[0][0].includes("Content-Disposition") &&
1031
+ typeof entriesArray[0][1] === "string" &&
1032
+ entriesArray[0][1].toString().includes("Content-Disposition")) {
1033
+ // Manually parse multipart format from the value string
1034
+ const keyPart = entriesArray[0][0];
1035
+ const valuePart = entriesArray[0][1];
1036
+ // Fix: If key ends with "name" and value starts with '"fieldname"', combine them
1037
+ let multipartBody;
1038
+ if (keyPart.trim().endsWith("name") &&
1039
+ valuePart.trim().startsWith('"')) {
1040
+ multipartBody = keyPart + "=" + valuePart;
1041
+ }
1042
+ else {
1043
+ multipartBody = keyPart + valuePart;
1044
+ }
1045
+ // Match: Content-Disposition header with field name, then capture value until next boundary or end
1046
+ const fieldRegex = /Content-Disposition:\s*form-data;\s*name="([^"]+)"\r?\n\r?\n([\s\S]*?)(?=\r?\n------|$)/g;
1047
+ let match;
1048
+ while ((match = fieldRegex.exec(multipartBody)) !== null) {
1049
+ const fieldName = match[1];
1050
+ const fieldValue = match[2].trim();
1051
+ if (fieldName === "scopes" || fieldName === "scopes[]") {
1052
+ try {
1053
+ body["scopes"] = JSON.parse(fieldValue);
1054
+ }
1055
+ catch {
1056
+ body["scopes"] = fieldValue.includes(",")
1057
+ ? fieldValue.split(",")
1058
+ : [fieldValue];
1059
+ }
1060
+ }
1061
+ else if (fieldName === "oauth_identity" ||
1062
+ fieldName === "oauth_identity_json") {
1063
+ try {
1064
+ body["oauth_identity"] = JSON.parse(fieldValue) || null;
1065
+ }
1066
+ catch {
1067
+ body["oauth_identity"] = null;
1068
+ }
1069
+ }
1070
+ else if (fieldName === "termsAccepted") {
1071
+ body[fieldName] =
1072
+ fieldValue === "on" ||
1073
+ fieldValue === "true" ||
1074
+ fieldValue === "1" ||
1075
+ fieldValue === "yes";
1076
+ }
1077
+ else if (fieldName === "customFields") {
1078
+ try {
1079
+ body[fieldName] = JSON.parse(fieldValue);
1080
+ }
1081
+ catch {
1082
+ // Skip
1083
+ }
1084
+ }
1085
+ else {
1086
+ body[fieldName] = fieldValue;
1087
+ }
1088
+ hasValidFields = true;
1089
+ }
1090
+ }
1091
+ else {
1092
+ // Normal FormData parsing
1093
+ // Type assertion: FormData.entries() exists at runtime in Cloudflare Workers
1094
+ for (const [key, value] of formData.entries()) {
1095
+ if (value instanceof File) {
1096
+ continue;
1097
+ }
1098
+ // Extract field name from malformed keys (when FormData is passed with wrong Content-Type)
1099
+ let fieldName = key;
1100
+ if (key.includes("Content-Disposition") && key.includes('name="')) {
1101
+ // Extract field name from Content-Disposition header
1102
+ const nameMatch = key.match(/name="([^"]+)"/);
1103
+ if (nameMatch && nameMatch[1]) {
1104
+ fieldName = nameMatch[1];
1105
+ }
1106
+ else {
1107
+ // Skip if we can't extract a valid field name
1108
+ continue;
1109
+ }
1110
+ }
1111
+ else if (key.includes("------") ||
1112
+ key.includes("\r\n") ||
1113
+ key.trim() === "") {
1114
+ // Skip boundary strings and invalid keys
1115
+ continue;
1116
+ }
1117
+ hasValidFields = true;
1118
+ const stringValue = value.toString();
1119
+ if (fieldName === "scopes" || fieldName === "scopes[]") {
1120
+ try {
1121
+ body["scopes"] = JSON.parse(stringValue);
1122
+ }
1123
+ catch {
1124
+ body["scopes"] = stringValue.includes(",")
1125
+ ? stringValue.split(",")
1126
+ : [stringValue];
1127
+ }
1128
+ }
1129
+ else if (fieldName === "oauth_identity" ||
1130
+ fieldName === "oauth_identity_json") {
1131
+ try {
1132
+ body["oauth_identity"] = JSON.parse(stringValue) || null;
1133
+ }
1134
+ catch {
1135
+ body["oauth_identity"] = null;
1136
+ }
1137
+ }
1138
+ else if (fieldName === "termsAccepted") {
1139
+ body[fieldName] =
1140
+ stringValue === "on" ||
1141
+ stringValue === "true" ||
1142
+ stringValue === "1" ||
1143
+ stringValue === "yes";
1144
+ }
1145
+ else if (fieldName === "customFields") {
1146
+ try {
1147
+ body[fieldName] = JSON.parse(stringValue);
1148
+ }
1149
+ catch {
1150
+ // Skip
1151
+ }
1152
+ }
1153
+ else {
1154
+ body[fieldName] = stringValue;
1155
+ }
1156
+ }
1157
+ }
1158
+ if (hasValidFields && Object.keys(body).length > 0) {
1159
+ if (!("termsAccepted" in body)) {
1160
+ body.termsAccepted = true;
1161
+ }
1162
+ // Default scopes to empty array if not provided
1163
+ if (!("scopes" in body)) {
1164
+ body.scopes = [];
1165
+ }
1166
+ return body;
1167
+ }
1168
+ }
1169
+ catch (formDataError) {
1170
+ console.warn("[ConsentService] FormData parsing failed:", formDataError);
1171
+ // Fall through to JSON parsing
1172
+ }
1173
+ }
1174
+ // Default to JSON parsing
1175
+ try {
1176
+ return await request.json();
1177
+ }
1178
+ catch (error) {
1179
+ // If JSON parsing fails and content-type suggests form data, try FormData parsing as fallback
1180
+ // Special handling for text/plain with FormData body - try text parsing first
1181
+ if (contentType === "text/plain") {
1182
+ try {
1183
+ const textRequest = request.clone();
1184
+ const text = await textRequest.text();
1185
+ if (text && text.length > 0) {
1186
+ // Try URL-encoded parsing first
1187
+ try {
1188
+ const params = new URLSearchParams(text);
1189
+ const body = parseFormFields(params.entries());
1190
+ if (Object.keys(body).length > 0) {
1191
+ return body;
1192
+ }
1193
+ }
1194
+ catch {
1195
+ // URL-encoded parsing failed, try multipart format
1196
+ }
1197
+ // Try multipart format parsing (FormData serializes to multipart)
1198
+ const fieldRegex = /Content-Disposition:\s*form-data;\s*name="([^"]+)"\r?\n\r?\n([\s\S]*?)(?=\r?\n------|$)/g;
1199
+ const body = {};
1200
+ let match;
1201
+ let hasValidFields = false;
1202
+ while ((match = fieldRegex.exec(text)) !== null) {
1203
+ const fieldName = match[1];
1204
+ const fieldValue = match[2].trim();
1205
+ if (fieldName === "scopes" || fieldName === "scopes[]") {
1206
+ try {
1207
+ body["scopes"] = JSON.parse(fieldValue);
1208
+ }
1209
+ catch {
1210
+ body["scopes"] = fieldValue.includes(",")
1211
+ ? fieldValue.split(",")
1212
+ : [fieldValue];
1213
+ }
1214
+ }
1215
+ else if (fieldName === "oauth_identity" ||
1216
+ fieldName === "oauth_identity_json") {
1217
+ try {
1218
+ body["oauth_identity"] = JSON.parse(fieldValue) || null;
1219
+ }
1220
+ catch {
1221
+ body["oauth_identity"] = null;
1222
+ }
1223
+ }
1224
+ else if (fieldName === "termsAccepted") {
1225
+ body[fieldName] =
1226
+ fieldValue === "on" ||
1227
+ fieldValue === "true" ||
1228
+ fieldValue === "1" ||
1229
+ fieldValue === "yes";
1230
+ }
1231
+ else if (fieldName === "customFields") {
1232
+ try {
1233
+ body[fieldName] = JSON.parse(fieldValue);
1234
+ }
1235
+ catch {
1236
+ // Skip
1237
+ }
1238
+ }
1239
+ else {
1240
+ body[fieldName] = fieldValue;
1241
+ }
1242
+ hasValidFields = true;
1243
+ }
1244
+ if (hasValidFields && Object.keys(body).length > 0) {
1245
+ if (!("termsAccepted" in body)) {
1246
+ body.termsAccepted = true;
1247
+ }
1248
+ // Default scopes to empty array if not provided
1249
+ if (!("scopes" in body)) {
1250
+ body.scopes = [];
1251
+ }
1252
+ return body;
1253
+ }
1254
+ }
1255
+ }
1256
+ catch {
1257
+ // Text parsing failed, fall through to FormData parsing
1258
+ }
1259
+ }
1260
+ if (!contentType.includes("application/json") &&
1261
+ (contentType.includes("form") ||
1262
+ contentType === "" ||
1263
+ contentType.includes("text"))) {
1264
+ try {
1265
+ // Clone request ONCE before trying any parsing, so we can reuse it if FormData parsing fails
1266
+ const fallbackRequest = request.clone();
1267
+ // Try FormData parsing as fallback (even if Content-Type doesn't match)
1268
+ let formData;
1269
+ try {
1270
+ formData = await fallbackRequest.formData();
1271
+ }
1272
+ catch (formDataParseError) {
1273
+ // If FormData parsing fails, try reading as text and parsing manually
1274
+ // This handles cases where Content-Type doesn't match (e.g., text/plain with FormData body)
1275
+ try {
1276
+ // Use a fresh clone for text parsing (body might be consumed by failed FormData attempt)
1277
+ const textRequest = request.clone();
1278
+ const text = await textRequest.text();
1279
+ if (text && text.length > 0) {
1280
+ // First try URL-encoded parsing (simpler format)
1281
+ try {
1282
+ const params = new URLSearchParams(text);
1283
+ const body = parseFormFields(params.entries());
1284
+ if (Object.keys(body).length > 0) {
1285
+ return body;
1286
+ }
1287
+ }
1288
+ catch {
1289
+ // URL-encoded parsing failed, try multipart format
1290
+ }
1291
+ // Try to manually parse multipart format from text
1292
+ const fieldRegex = /Content-Disposition:\s*form-data;\s*name="([^"]+)"\r?\n\r?\n([\s\S]*?)(?=\r?\n------|$)/g;
1293
+ const body = {};
1294
+ let match;
1295
+ let hasValidFields = false;
1296
+ while ((match = fieldRegex.exec(text)) !== null) {
1297
+ const fieldName = match[1];
1298
+ const fieldValue = match[2].trim();
1299
+ if (fieldName === "scopes" || fieldName === "scopes[]") {
1300
+ try {
1301
+ body["scopes"] = JSON.parse(fieldValue);
1302
+ }
1303
+ catch {
1304
+ body["scopes"] = fieldValue.includes(",")
1305
+ ? fieldValue.split(",")
1306
+ : [fieldValue];
1307
+ }
1308
+ }
1309
+ else if (fieldName === "oauth_identity" ||
1310
+ fieldName === "oauth_identity_json") {
1311
+ try {
1312
+ body["oauth_identity"] = JSON.parse(fieldValue) || null;
1313
+ }
1314
+ catch {
1315
+ body["oauth_identity"] = null;
1316
+ }
1317
+ }
1318
+ else if (fieldName === "termsAccepted") {
1319
+ body[fieldName] =
1320
+ fieldValue === "on" ||
1321
+ fieldValue === "true" ||
1322
+ fieldValue === "1" ||
1323
+ fieldValue === "yes";
1324
+ }
1325
+ else if (fieldName === "customFields") {
1326
+ try {
1327
+ body[fieldName] = JSON.parse(fieldValue);
1328
+ }
1329
+ catch {
1330
+ // Skip
1331
+ }
1332
+ }
1333
+ else {
1334
+ body[fieldName] = fieldValue;
1335
+ }
1336
+ hasValidFields = true;
1337
+ }
1338
+ if (hasValidFields && Object.keys(body).length > 0) {
1339
+ if (!("termsAccepted" in body)) {
1340
+ body.termsAccepted = true;
1341
+ }
1342
+ // Default scopes to empty array if not provided
1343
+ if (!("scopes" in body)) {
1344
+ body.scopes = [];
1345
+ }
1346
+ return body;
1347
+ }
1348
+ }
1349
+ }
1350
+ catch {
1351
+ // If text parsing also fails, rethrow original FormData error
1352
+ }
1353
+ throw formDataParseError;
1354
+ }
1355
+ // Check if FormData has valid entries (might be empty if Content-Type mismatch)
1356
+ const entriesArray = Array.from(formData.entries());
1357
+ if (entriesArray.length === 0) {
1358
+ // FormData parsing succeeded but returned no entries - try text parsing
1359
+ // This handles cases where Content-Type doesn't match (e.g., text/plain with FormData body)
1360
+ try {
1361
+ const textRequest = request.clone();
1362
+ const text = await textRequest.text();
1363
+ if (text && text.length > 0) {
1364
+ // Try URL-encoded parsing first
1365
+ try {
1366
+ const params = new URLSearchParams(text);
1367
+ const body = parseFormFields(params.entries());
1368
+ if (Object.keys(body).length > 0) {
1369
+ return body;
1370
+ }
1371
+ }
1372
+ catch {
1373
+ // URL-encoded parsing failed, try multipart format
1374
+ }
1375
+ // Try multipart format parsing
1376
+ const fieldRegex = /Content-Disposition:\s*form-data;\s*name="([^"]+)"\r?\n\r?\n([\s\S]*?)(?=\r?\n------|$)/g;
1377
+ const body = {};
1378
+ let match;
1379
+ let hasValidFields = false;
1380
+ while ((match = fieldRegex.exec(text)) !== null) {
1381
+ const fieldName = match[1];
1382
+ const fieldValue = match[2].trim();
1383
+ if (fieldName === "scopes" || fieldName === "scopes[]") {
1384
+ try {
1385
+ body["scopes"] = JSON.parse(fieldValue);
1386
+ }
1387
+ catch {
1388
+ body["scopes"] = fieldValue.includes(",")
1389
+ ? fieldValue.split(",")
1390
+ : [fieldValue];
1391
+ }
1392
+ }
1393
+ else if (fieldName === "oauth_identity" ||
1394
+ fieldName === "oauth_identity_json") {
1395
+ try {
1396
+ body["oauth_identity"] = JSON.parse(fieldValue) || null;
1397
+ }
1398
+ catch {
1399
+ body["oauth_identity"] = null;
1400
+ }
1401
+ }
1402
+ else if (fieldName === "termsAccepted") {
1403
+ body[fieldName] =
1404
+ fieldValue === "on" ||
1405
+ fieldValue === "true" ||
1406
+ fieldValue === "1" ||
1407
+ fieldValue === "yes";
1408
+ }
1409
+ else if (fieldName === "customFields") {
1410
+ try {
1411
+ body[fieldName] = JSON.parse(fieldValue);
1412
+ }
1413
+ catch {
1414
+ // Skip
1415
+ }
1416
+ }
1417
+ else {
1418
+ body[fieldName] = fieldValue;
1419
+ }
1420
+ hasValidFields = true;
1421
+ }
1422
+ if (hasValidFields && Object.keys(body).length > 0) {
1423
+ if (!("termsAccepted" in body)) {
1424
+ body.termsAccepted = true;
1425
+ }
1426
+ // Default scopes to empty array if not provided
1427
+ if (!("scopes" in body)) {
1428
+ body.scopes = [];
1429
+ }
1430
+ return body;
1431
+ }
1432
+ }
1433
+ // If text parsing failed or found no valid fields, throw error to trigger fallback
1434
+ throw new Error("FormData parsing returned empty entries and text parsing found no valid fields");
1435
+ }
1436
+ catch (textParseError) {
1437
+ // Text parsing failed or found no valid fields - throw error to trigger JSON fallback
1438
+ // This ensures we don't continue with empty FormData entries
1439
+ throw new Error(`Failed to parse FormData: ${textParseError instanceof Error ? textParseError.message : "Unknown error"}`);
1440
+ }
1441
+ }
1442
+ const body = {};
1443
+ // Type assertion: FormData.entries() exists at runtime in Cloudflare Workers
1444
+ let hasValidEntries = false;
1445
+ for (const [key, value] of formData.entries()) {
1446
+ if (value instanceof File)
1447
+ continue;
1448
+ // Extract field name from malformed keys
1449
+ let fieldName = key;
1450
+ if (key.includes("Content-Disposition") && key.includes('name="')) {
1451
+ const nameMatch = key.match(/name="([^"]+)"/);
1452
+ if (nameMatch && nameMatch[1]) {
1453
+ fieldName = nameMatch[1];
1454
+ }
1455
+ else {
1456
+ continue;
1457
+ }
1458
+ }
1459
+ else if (key.includes("------") ||
1460
+ key.includes("\r\n") ||
1461
+ key.trim() === "" ||
1462
+ key.includes("formdata-undici")) {
1463
+ // Malformed key - likely from text/plain Content-Type mismatch
1464
+ // Skip this entry and fall back to text parsing
1465
+ continue;
1466
+ }
1467
+ // If we have a valid field name, mark as having valid entries
1468
+ if (fieldName && fieldName !== key) {
1469
+ hasValidEntries = true;
1470
+ }
1471
+ else if (fieldName &&
1472
+ !key.includes("Content-Disposition") &&
1473
+ !key.includes("------") &&
1474
+ !key.includes("\r\n")) {
1475
+ hasValidEntries = true;
1476
+ }
1477
+ // Process the field value
1478
+ const stringValue = value.toString();
1479
+ if (fieldName === "scopes" || fieldName === "scopes[]") {
1480
+ try {
1481
+ body["scopes"] = JSON.parse(stringValue);
1482
+ }
1483
+ catch {
1484
+ body["scopes"] = stringValue.includes(",")
1485
+ ? stringValue.split(",")
1486
+ : [stringValue];
1487
+ }
1488
+ }
1489
+ else if (fieldName === "oauth_identity" ||
1490
+ fieldName === "oauth_identity_json") {
1491
+ try {
1492
+ body["oauth_identity"] = JSON.parse(stringValue) || null;
1493
+ }
1494
+ catch {
1495
+ body["oauth_identity"] = null;
1496
+ }
1497
+ }
1498
+ else if (fieldName === "termsAccepted") {
1499
+ body[fieldName] =
1500
+ stringValue === "on" ||
1501
+ stringValue === "true" ||
1502
+ stringValue === "1" ||
1503
+ stringValue === "yes";
1504
+ }
1505
+ else if (fieldName === "customFields") {
1506
+ try {
1507
+ body[fieldName] = JSON.parse(stringValue);
1508
+ }
1509
+ catch {
1510
+ // Skip
1511
+ }
1512
+ }
1513
+ else {
1514
+ body[fieldName] = stringValue;
1515
+ }
1516
+ }
1517
+ // If we didn't get any valid entries (all keys were malformed), try text parsing
1518
+ if (!hasValidEntries || Object.keys(body).length === 0) {
1519
+ // Try text parsing as fallback when FormData parsing returns no valid entries
1520
+ try {
1521
+ const textRequest = request.clone();
1522
+ const text = await textRequest.text();
1523
+ if (text && text.length > 0) {
1524
+ // Try multipart format parsing from text
1525
+ const fieldRegex = /Content-Disposition:\s*form-data;\s*name="([^"]+)"\r?\n\r?\n([\s\S]*?)(?=\r?\n------|$)/g;
1526
+ const textBody = {};
1527
+ let match;
1528
+ let hasValidFields = false;
1529
+ while ((match = fieldRegex.exec(text)) !== null) {
1530
+ const fieldName = match[1];
1531
+ const fieldValue = match[2].trim();
1532
+ if (fieldName === "scopes" || fieldName === "scopes[]") {
1533
+ try {
1534
+ textBody["scopes"] = JSON.parse(fieldValue);
1535
+ }
1536
+ catch {
1537
+ textBody["scopes"] = fieldValue.includes(",")
1538
+ ? fieldValue.split(",")
1539
+ : [fieldValue];
1540
+ }
1541
+ }
1542
+ else if (fieldName === "oauth_identity" ||
1543
+ fieldName === "oauth_identity_json") {
1544
+ try {
1545
+ textBody["oauth_identity"] =
1546
+ JSON.parse(fieldValue) || null;
1547
+ }
1548
+ catch {
1549
+ textBody["oauth_identity"] = null;
1550
+ }
1551
+ }
1552
+ else if (fieldName === "termsAccepted") {
1553
+ textBody[fieldName] =
1554
+ fieldValue === "on" ||
1555
+ fieldValue === "true" ||
1556
+ fieldValue === "1" ||
1557
+ fieldValue === "yes";
1558
+ }
1559
+ else if (fieldName === "customFields") {
1560
+ try {
1561
+ textBody[fieldName] = JSON.parse(fieldValue);
1562
+ }
1563
+ catch {
1564
+ // Skip
1565
+ }
1566
+ }
1567
+ else {
1568
+ textBody[fieldName] = fieldValue;
1569
+ }
1570
+ hasValidFields = true;
1571
+ }
1572
+ if (hasValidFields && Object.keys(textBody).length > 0) {
1573
+ if (!("termsAccepted" in textBody)) {
1574
+ textBody.termsAccepted = true;
1575
+ }
1576
+ if (!("scopes" in textBody)) {
1577
+ textBody.scopes = [];
1578
+ }
1579
+ return textBody;
1580
+ }
1581
+ }
1582
+ }
1583
+ catch {
1584
+ // Text parsing also failed, fall through to throw error
1585
+ }
1586
+ throw new Error("FormData parsing returned only malformed entries - falling back to text parsing");
1587
+ }
1588
+ if (!("termsAccepted" in body)) {
1589
+ body.termsAccepted = true;
1590
+ }
1591
+ // Default scopes to empty array if not provided
1592
+ if (!("scopes" in body)) {
1593
+ body.scopes = [];
1594
+ }
1595
+ return body;
1596
+ }
1597
+ catch (formError) {
1598
+ // FormData parsing failed - if it was due to malformed entries, try text parsing
1599
+ const errorMessage = formError instanceof Error ? formError.message : String(formError);
1600
+ if (errorMessage.includes("malformed entries") ||
1601
+ errorMessage.includes("empty entries") ||
1602
+ errorMessage.includes("falling back to text parsing")) {
1603
+ // Try text parsing as last resort
1604
+ try {
1605
+ const textRequest = request.clone();
1606
+ const text = await textRequest.text();
1607
+ if (text && text.length > 0) {
1608
+ // Try multipart format parsing
1609
+ const fieldRegex = /Content-Disposition:\s*form-data;\s*name="([^"]+)"\r?\n\r?\n([\s\S]*?)(?=\r?\n------|$)/g;
1610
+ const body = {};
1611
+ let match;
1612
+ let hasValidFields = false;
1613
+ while ((match = fieldRegex.exec(text)) !== null) {
1614
+ const fieldName = match[1];
1615
+ const fieldValue = match[2].trim();
1616
+ if (fieldName === "scopes" || fieldName === "scopes[]") {
1617
+ try {
1618
+ body["scopes"] = JSON.parse(fieldValue);
1619
+ }
1620
+ catch {
1621
+ body["scopes"] = fieldValue.includes(",")
1622
+ ? fieldValue.split(",")
1623
+ : [fieldValue];
1624
+ }
1625
+ }
1626
+ else if (fieldName === "oauth_identity" ||
1627
+ fieldName === "oauth_identity_json") {
1628
+ try {
1629
+ body["oauth_identity"] = JSON.parse(fieldValue) || null;
1630
+ }
1631
+ catch {
1632
+ body["oauth_identity"] = null;
1633
+ }
1634
+ }
1635
+ else if (fieldName === "termsAccepted") {
1636
+ body[fieldName] =
1637
+ fieldValue === "on" ||
1638
+ fieldValue === "true" ||
1639
+ fieldValue === "1" ||
1640
+ fieldValue === "yes";
1641
+ }
1642
+ else if (fieldName === "customFields") {
1643
+ try {
1644
+ body[fieldName] = JSON.parse(fieldValue);
1645
+ }
1646
+ catch {
1647
+ // Skip
1648
+ }
1649
+ }
1650
+ else {
1651
+ body[fieldName] = fieldValue;
1652
+ }
1653
+ hasValidFields = true;
1654
+ }
1655
+ if (hasValidFields && Object.keys(body).length > 0) {
1656
+ if (!("termsAccepted" in body)) {
1657
+ body.termsAccepted = true;
1658
+ }
1659
+ // Default scopes to empty array if not provided
1660
+ if (!("scopes" in body)) {
1661
+ body.scopes = [];
1662
+ }
1663
+ return body;
1664
+ }
1665
+ }
1666
+ }
1667
+ catch {
1668
+ // Text parsing also failed
1669
+ }
1670
+ }
1671
+ // Both failed, throw original error
1672
+ }
1673
+ }
1674
+ throw new Error(`Failed to parse request body: ${error instanceof Error ? error.message : "Unknown error"}`);
1675
+ }
1676
+ }
1677
+ /**
1678
+ * Handle consent approval
1679
+ *
1680
+ * Validates request, creates delegation via AgentShield API,
1681
+ * stores token in KV, and returns success response.
1682
+ *
1683
+ * @param request - Approval request
1684
+ * @returns JSON response
1685
+ */
1686
+ async handleApproval(request) {
1687
+ console.log("[ConsentService] Approval request received");
1688
+ try {
1689
+ // Parse and validate request body (supports both JSON and FormData)
1690
+ const body = await this.parseRequestBody(request);
1691
+ console.log("[ConsentService] Request body parsed:", {
1692
+ hasBody: !!body,
1693
+ bodyKeys: Object.keys(body || {}),
1694
+ hasOAuthIdentity: !!body?.oauth_identity,
1695
+ });
514
1696
  const validation = validateConsentApprovalRequest(body);
515
1697
  if (!validation.success) {
1698
+ console.error("[ConsentService] Approval request validation failed:", {
1699
+ errors: validation.error.errors,
1700
+ receivedBody: body,
1701
+ });
516
1702
  return new Response(JSON.stringify({
517
1703
  success: false,
518
1704
  error: "Invalid request",
@@ -524,6 +1710,13 @@ export class ConsentService {
524
1710
  });
525
1711
  }
526
1712
  const approvalRequest = validation.data;
1713
+ console.log("[ConsentService] Approval request validated:", {
1714
+ agentDid: approvalRequest.agent_did?.substring(0, 20) + "...",
1715
+ sessionId: approvalRequest.session_id?.substring(0, 20) + "...",
1716
+ scopes: approvalRequest.scopes,
1717
+ hasOAuthIdentity: !!approvalRequest.oauth_identity,
1718
+ oauthProvider: approvalRequest.oauth_identity?.provider,
1719
+ });
527
1720
  // Validate terms acceptance if required
528
1721
  const consentConfig = await this.configService.getConsentConfig(approvalRequest.project_id);
529
1722
  if (consentConfig.terms?.required && !approvalRequest.termsAccepted) {
@@ -536,9 +1729,40 @@ export class ConsentService {
536
1729
  headers: { "Content-Type": "application/json" },
537
1730
  });
538
1731
  }
1732
+ // ✅ Extract projectId from approval request
1733
+ const projectId = approvalRequest.project_id;
1734
+ // ✅ Lazy initialization with projectId
1735
+ const auditService = await this.getAuditService(projectId);
1736
+ // Check if user needs credentials before delegation
1737
+ const needsCredentials = !approvalRequest.user_did && !approvalRequest.oauth_identity;
1738
+ if (needsCredentials && auditService) {
1739
+ await auditService
1740
+ .logCredentialRequired({
1741
+ sessionId: approvalRequest.session_id,
1742
+ agentDid: approvalRequest.agent_did,
1743
+ targetTools: [approvalRequest.tool], // Array
1744
+ scopes: approvalRequest.scopes,
1745
+ projectId,
1746
+ oauthProvider: approvalRequest.oauth_identity?.provider,
1747
+ })
1748
+ .catch((err) => {
1749
+ console.error("[ConsentService] Failed to log credential required", {
1750
+ eventType: "consent:credential_required",
1751
+ sessionId: approvalRequest.session_id,
1752
+ error: err instanceof Error ? err.message : String(err),
1753
+ });
1754
+ });
1755
+ // Note: We don't redirect here - the consent flow continues
1756
+ // The credential_required event is just for audit tracking
1757
+ }
539
1758
  // Create delegation via AgentShield API
1759
+ console.log("[ConsentService] Creating delegation...");
540
1760
  const delegationResult = await this.createDelegation(approvalRequest);
541
1761
  if (!delegationResult.success) {
1762
+ console.error("[ConsentService] Delegation creation failed:", {
1763
+ error: delegationResult.error,
1764
+ error_code: delegationResult.error_code,
1765
+ });
542
1766
  return new Response(JSON.stringify({
543
1767
  success: false,
544
1768
  error: delegationResult.error || "Failed to create delegation",
@@ -548,8 +1772,57 @@ export class ConsentService {
548
1772
  headers: { "Content-Type": "application/json" },
549
1773
  });
550
1774
  }
1775
+ console.log("[ConsentService] ✅ Delegation created successfully:", {
1776
+ delegationId: delegationResult.delegation_id?.substring(0, 20) + "...",
1777
+ });
551
1778
  // Store delegation token in KV
552
1779
  await this.storeDelegationToken(approvalRequest.session_id, approvalRequest.agent_did, delegationResult.delegation_token, delegationResult.delegation_id);
1780
+ // ✅ After successful delegation creation - log audit events
1781
+ if (auditService && delegationResult.success) {
1782
+ try {
1783
+ // Get userDid (may have been generated during delegation creation)
1784
+ let userDid;
1785
+ if (this.env.DELEGATION_STORAGE && approvalRequest.session_id) {
1786
+ try {
1787
+ userDid = await this.getUserDidForSession(approvalRequest.session_id, approvalRequest.oauth_identity || undefined);
1788
+ }
1789
+ catch (error) {
1790
+ console.warn("[ConsentService] Failed to get userDid for audit logging:", error);
1791
+ // Continue without userDid - audit events can still be logged
1792
+ }
1793
+ }
1794
+ await auditService.logConsentApproval({
1795
+ sessionId: approvalRequest.session_id,
1796
+ userDid,
1797
+ agentDid: approvalRequest.agent_did,
1798
+ targetTools: [approvalRequest.tool], // Array
1799
+ scopes: approvalRequest.scopes,
1800
+ delegationId: delegationResult.delegation_id,
1801
+ projectId,
1802
+ termsAccepted: approvalRequest.termsAccepted || false,
1803
+ oauthIdentity: approvalRequest.oauth_identity ? {
1804
+ provider: approvalRequest.oauth_identity.provider,
1805
+ identifier: approvalRequest.oauth_identity.subject
1806
+ } : undefined,
1807
+ });
1808
+ await auditService.logDelegationCreated({
1809
+ sessionId: approvalRequest.session_id,
1810
+ delegationId: delegationResult.delegation_id,
1811
+ agentDid: approvalRequest.agent_did,
1812
+ userDid,
1813
+ targetTools: [approvalRequest.tool], // Array
1814
+ scopes: approvalRequest.scopes,
1815
+ projectId,
1816
+ });
1817
+ }
1818
+ catch (error) {
1819
+ console.error("[ConsentService] Audit failed but continuing", {
1820
+ sessionId: approvalRequest.session_id,
1821
+ error: error instanceof Error ? error.message : String(error),
1822
+ eventTypes: ["consent:approved", "consent:delegation_created"],
1823
+ });
1824
+ }
1825
+ }
553
1826
  // Return success response
554
1827
  const response = {
555
1828
  success: true,
@@ -563,16 +1836,10 @@ export class ConsentService {
563
1836
  }
564
1837
  catch (error) {
565
1838
  console.error("[ConsentService] Error handling approval:", error);
566
- const errorMessage = error instanceof Error ? error.message : String(error);
567
- const errorStack = error instanceof Error ? error.stack : undefined;
568
- console.error("[ConsentService] Error details:", {
569
- message: errorMessage,
570
- stack: errorStack,
571
- });
572
1839
  return new Response(JSON.stringify({
573
1840
  success: false,
574
- error: errorMessage || "An internal error occurred",
575
- error_code: "INTERNAL_SERVER_ERROR",
1841
+ error: "Internal server error",
1842
+ error_code: "internal_error",
576
1843
  }), {
577
1844
  status: 500,
578
1845
  headers: { "Content-Type": "application/json" },
@@ -602,17 +1869,35 @@ export class ConsentService {
602
1869
  const fieldName = await getDelegationFieldName(this.env.DELEGATION_STORAGE);
603
1870
  // Get userDID from session or generate new ephemeral DID
604
1871
  // Phase 4 PR #3: Use OAuth identity if provided in approval request
1872
+ // CRITICAL: Must work with or without OAuth identity
605
1873
  let userDid;
606
1874
  if (this.env.DELEGATION_STORAGE && request.session_id) {
607
1875
  try {
608
- // Pass OAuth identity if available in approval request
609
- userDid = await this.getUserDidForSession(request.session_id, request.oauth_identity);
1876
+ console.log("[ConsentService] Getting User DID for session:", {
1877
+ sessionId: request.session_id.substring(0, 20) + "...",
1878
+ hasOAuthIdentity: !!request.oauth_identity,
1879
+ oauthProvider: request.oauth_identity?.provider,
1880
+ });
1881
+ // Pass OAuth identity if available in approval request (can be null/undefined)
1882
+ userDid = await this.getUserDidForSession(request.session_id, request.oauth_identity || undefined // Explicitly handle null as undefined
1883
+ );
1884
+ console.log("[ConsentService] User DID retrieved:", {
1885
+ userDid: userDid?.substring(0, 20) + "...",
1886
+ hasUserDid: !!userDid,
1887
+ });
610
1888
  }
611
1889
  catch (error) {
612
- console.warn("[ConsentService] Failed to get/generate userDid:", error);
613
- // Continue without userDid - delegation will use ephemeral placeholder
1890
+ console.error("[ConsentService] Failed to get/generate userDid:", error);
1891
+ // Continue without userDid - delegation will work without user_identifier
1892
+ // This is valid for non-OAuth scenarios
614
1893
  }
615
1894
  }
1895
+ else {
1896
+ console.log("[ConsentService] Skipping User DID retrieval:", {
1897
+ hasStorage: !!this.env.DELEGATION_STORAGE,
1898
+ hasSessionId: !!request.session_id,
1899
+ });
1900
+ }
616
1901
  const expiresInDays = 7; // Default to 7 days
617
1902
  // Build delegation request with error-based format detection
618
1903
  // Try full format first, fallback to simplified format on error
@@ -629,10 +1914,6 @@ export class ConsentService {
629
1914
  // Error-based format detection: try request format, fallback on error
630
1915
  const response = await this.tryAPICall(agentShieldUrl, apiKey, delegationRequest);
631
1916
  if (!response.success) {
632
- console.error("[ConsentService] Delegation creation failed:", {
633
- error: response.error,
634
- error_code: response.error_code,
635
- });
636
1917
  return response;
637
1918
  }
638
1919
  const responseData = response.data;
@@ -825,10 +2106,14 @@ export class ConsentService {
825
2106
  scopes: request.scopes,
826
2107
  expires_in_days: expiresInDays,
827
2108
  };
828
- // Note: session_id and project_id are NOT in createDelegationSchema
829
- // - project_id is extracted from API key context by AgentShield middleware
830
- // - session_id is not needed for delegation creation
831
- // These fields are removed to match AgentShield API schema exactly
2109
+ // Include session_id if provided
2110
+ if (request.session_id) {
2111
+ baseRequest.session_id = request.session_id;
2112
+ }
2113
+ // Include project_id if provided
2114
+ if (request.project_id) {
2115
+ baseRequest.project_id = request.project_id;
2116
+ }
832
2117
  // Check cached format preference
833
2118
  const cacheKey = STORAGE_KEYS.formatPreference();
834
2119
  let cachedFormat = null;
@@ -864,13 +2149,15 @@ export class ConsentService {
864
2149
  */
865
2150
  async buildFullFormatRequest(request, userDid, expiresInDays) {
866
2151
  const notAfter = Date.now() + expiresInDays * 24 * 60 * 60 * 1000;
2152
+ // Defensive check: ensure scopes is always defined (should be guaranteed by validation)
2153
+ const scopes = request.scopes ?? [];
867
2154
  return {
868
2155
  delegation: {
869
2156
  id: crypto.randomUUID(),
870
2157
  issuerDid: userDid || "did:key:z6MkEphemeral", // Use ephemeral if no userDid
871
2158
  subjectDid: request.agent_did,
872
2159
  constraints: {
873
- scopes: request.scopes,
2160
+ scopes,
874
2161
  notAfter,
875
2162
  notBefore: Date.now(),
876
2163
  },
@@ -895,33 +2182,56 @@ export class ConsentService {
895
2182
  }
896
2183
  /**
897
2184
  * Build simplified format request with proper field name
2185
+ *
2186
+ * CRITICAL: This method MUST NOT include session_id or project_id in the request body.
2187
+ * These fields are NOT part of AgentShield's createDelegationSchema:
2188
+ * - project_id is extracted from API key context by AgentShield middleware
2189
+ * - session_id is not needed for delegation creation
2190
+ *
2191
+ * Including these fields will cause validation errors (400 Bad Request).
898
2192
  */
899
2193
  buildSimplifiedFormatRequest(request, userDid, expiresInDays, fieldName) {
2194
+ // Build request with ONLY fields that are in AgentShield's schema
2195
+ // Defensive check: ensure scopes is always defined (should be guaranteed by validation)
2196
+ const scopes = request.scopes ?? [];
900
2197
  const simplifiedRequest = {
901
2198
  agent_did: request.agent_did,
902
- scopes: request.scopes,
2199
+ scopes,
903
2200
  expires_in_days: expiresInDays,
904
2201
  };
905
2202
  // Include user_identifier if we have userDid (matches AgentShield schema)
906
- // Note: session_id and project_id are NOT in createDelegationSchema
907
- // - project_id is extracted from API key context by AgentShield
908
- // - session_id is not needed for delegation creation
2203
+ // CRITICAL: user_identifier is optional - delegation works without it
909
2204
  if (userDid) {
910
2205
  simplifiedRequest.user_identifier = userDid;
911
- // Use the correct field name (metadata or custom_fields) from Day0 config
912
- simplifiedRequest[fieldName] = {
2206
+ console.log("[ConsentService] Including user_identifier in delegation request:", {
2207
+ userDid: userDid.substring(0, 20) + "...",
2208
+ });
2209
+ }
2210
+ else {
2211
+ console.log("[ConsentService] No user_identifier (no OAuth or ephemeral DID) - delegation will proceed without it");
2212
+ }
2213
+ // AgentShield API only accepts "custom_fields", not "metadata"
2214
+ // Always use "custom_fields" regardless of Day0 config
2215
+ if (userDid) {
2216
+ // Include issuer_did and subject_did in custom_fields when we have userDid
2217
+ simplifiedRequest.custom_fields = {
913
2218
  issuer_did: userDid,
914
2219
  subject_did: request.agent_did,
915
2220
  format_version: "simplified_v1",
2221
+ // Merge with any existing custom_fields from request
2222
+ ...(request.customFields || {}),
916
2223
  };
917
2224
  }
918
- // Include custom_fields from request if provided
919
- if (request.customFields && Object.keys(request.customFields).length > 0) {
920
- simplifiedRequest.custom_fields = {
921
- ...(simplifiedRequest.custom_fields || {}),
922
- ...request.customFields,
923
- };
2225
+ else if (request.customFields &&
2226
+ Object.keys(request.customFields).length > 0) {
2227
+ // Include custom_fields from request even if no userDid
2228
+ simplifiedRequest.custom_fields = request.customFields;
924
2229
  }
2230
+ // If no userDid and no customFields, custom_fields is omitted (valid)
2231
+ // EXPLICIT SAFEGUARD: Remove session_id and project_id if they somehow got added
2232
+ // This should never happen, but provides defense-in-depth
2233
+ delete simplifiedRequest.session_id;
2234
+ delete simplifiedRequest.project_id;
925
2235
  return simplifiedRequest;
926
2236
  }
927
2237
  /**
@@ -951,25 +2261,31 @@ export class ConsentService {
951
2261
  }
952
2262
  /**
953
2263
  * Make API call and parse response
2264
+ *
2265
+ * CRITICAL: This method ensures session_id and project_id are never sent to AgentShield.
2266
+ * These fields are NOT part of the createDelegationSchema and will cause validation errors.
954
2267
  */
955
2268
  async makeAPICall(agentShieldUrl, apiKey, requestBody) {
956
2269
  try {
957
- const url = `${agentShieldUrl}${AGENTSHIELD_ENDPOINTS.DELEGATIONS_CREATE}`;
958
- const requestId = crypto.randomUUID();
959
- console.log("[ConsentService] Making API call:", {
960
- url,
961
- method: "POST",
962
- requestId,
963
- bodyKeys: Object.keys(requestBody),
964
- });
965
- const response = await fetch(url, {
2270
+ // FINAL SAFEGUARD: Remove session_id and project_id if they somehow got added
2271
+ // This provides defense-in-depth protection
2272
+ const sanitizedBody = { ...requestBody };
2273
+ if ("session_id" in sanitizedBody) {
2274
+ console.warn("[ConsentService] ⚠️ session_id detected in request body - removing (not in schema)");
2275
+ delete sanitizedBody.session_id;
2276
+ }
2277
+ if ("project_id" in sanitizedBody) {
2278
+ console.warn("[ConsentService] ⚠️ project_id detected in request body - removing (not in schema)");
2279
+ delete sanitizedBody.project_id;
2280
+ }
2281
+ const response = await fetch(`${agentShieldUrl}${AGENTSHIELD_ENDPOINTS.DELEGATIONS_CREATE}`, {
966
2282
  method: "POST",
967
2283
  headers: {
968
- "X-API-Key": apiKey,
2284
+ Authorization: `Bearer ${apiKey}`,
969
2285
  "Content-Type": "application/json",
970
- "X-Request-ID": requestId,
2286
+ "X-Request-ID": crypto.randomUUID(),
971
2287
  },
972
- body: JSON.stringify(requestBody),
2288
+ body: JSON.stringify(sanitizedBody),
973
2289
  });
974
2290
  const responseText = await response.text();
975
2291
  let responseData;
@@ -979,22 +2295,17 @@ export class ConsentService {
979
2295
  catch {
980
2296
  responseData = responseText;
981
2297
  }
982
- console.log("[ConsentService] API response:", {
983
- status: response.status,
984
- statusText: response.statusText,
985
- responseData: typeof responseData === "string"
986
- ? responseData.substring(0, 200)
987
- : JSON.stringify(responseData).substring(0, 200),
988
- });
989
2298
  // Check for validation error specifically
990
2299
  if (response.status === 400) {
991
- const errorMessage = responseData
992
- ?.error?.message ||
993
- responseData?.message ||
994
- "Validation failed";
995
- if (errorMessage.includes("format") ||
2300
+ const errorData = responseData;
2301
+ const errorMessage = errorData.error?.message || errorData.message || "Validation failed";
2302
+ const errorCode = errorData.error_code || errorData.error?.code;
2303
+ // Check if error_code is explicitly set to "validation_error" OR error message suggests validation error
2304
+ if (errorCode === "validation_error" ||
2305
+ errorMessage.includes("format") ||
996
2306
  errorMessage.includes("schema") ||
997
- errorMessage.includes("invalid")) {
2307
+ errorMessage.includes("invalid") ||
2308
+ errorMessage.includes("Validation")) {
998
2309
  return {
999
2310
  success: false,
1000
2311
  error: errorMessage,