@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.
- package/dist/__tests__/e2e/test-config.d.ts +37 -0
- package/dist/__tests__/e2e/test-config.d.ts.map +1 -0
- package/dist/__tests__/e2e/test-config.js +62 -0
- package/dist/__tests__/e2e/test-config.js.map +1 -0
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +90 -47
- package/dist/adapter.js.map +1 -1
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +14 -0
- package/dist/app.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +36 -2
- package/dist/config.js.map +1 -1
- package/dist/runtime.d.ts +12 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +36 -1
- package/dist/runtime.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +48 -2
- package/dist/server.js.map +1 -1
- package/dist/services/admin.service.d.ts.map +1 -1
- package/dist/services/admin.service.js +21 -20
- package/dist/services/admin.service.js.map +1 -1
- package/dist/services/consent-audit.service.d.ts +91 -0
- package/dist/services/consent-audit.service.d.ts.map +1 -0
- package/dist/services/consent-audit.service.js +241 -0
- package/dist/services/consent-audit.service.js.map +1 -0
- package/dist/services/consent-config.service.d.ts.map +1 -1
- package/dist/services/consent-config.service.js +3 -7
- package/dist/services/consent-config.service.js.map +1 -1
- package/dist/services/consent-page-renderer.d.ts.map +1 -1
- package/dist/services/consent-page-renderer.js +10 -28
- package/dist/services/consent-page-renderer.js.map +1 -1
- package/dist/services/consent.service.d.ts +53 -0
- package/dist/services/consent.service.d.ts.map +1 -1
- package/dist/services/consent.service.js +1429 -118
- package/dist/services/consent.service.js.map +1 -1
- package/dist/services/proof.service.d.ts +5 -3
- package/dist/services/proof.service.d.ts.map +1 -1
- package/dist/services/proof.service.js +19 -6
- package/dist/services/proof.service.js.map +1 -1
- 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 (
|
|
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
|
-
|
|
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
|
-
*
|
|
605
|
+
* Parse request body from JSON or FormData
|
|
445
606
|
*
|
|
446
|
-
*
|
|
447
|
-
*
|
|
607
|
+
* Handles both JSON and FormData/multipart requests, converting
|
|
608
|
+
* FormData fields to the correct format for ConsentApprovalRequest.
|
|
448
609
|
*
|
|
449
|
-
* @param request -
|
|
450
|
-
* @returns
|
|
610
|
+
* @param request - Request to parse
|
|
611
|
+
* @returns Parsed body object
|
|
451
612
|
*/
|
|
452
|
-
async
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
//
|
|
459
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
626
|
+
// If not JSON, treat as single scope or array
|
|
627
|
+
body["scopes"] = value.includes(",") ? value.split(",") : [value];
|
|
488
628
|
}
|
|
489
629
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
-
|
|
495
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
575
|
-
error_code: "
|
|
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
|
-
|
|
609
|
-
|
|
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.
|
|
613
|
-
// Continue without userDid - delegation will
|
|
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
|
-
//
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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
|
|
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
|
|
2199
|
+
scopes,
|
|
903
2200
|
expires_in_days: expiresInDays,
|
|
904
2201
|
};
|
|
905
2202
|
// Include user_identifier if we have userDid (matches AgentShield schema)
|
|
906
|
-
//
|
|
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
|
-
|
|
912
|
-
|
|
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
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
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
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
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
|
-
|
|
2284
|
+
Authorization: `Bearer ${apiKey}`,
|
|
969
2285
|
"Content-Type": "application/json",
|
|
970
|
-
"X-Request-ID":
|
|
2286
|
+
"X-Request-ID": crypto.randomUUID(),
|
|
971
2287
|
},
|
|
972
|
-
body: JSON.stringify(
|
|
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
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
if (
|
|
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,
|