@kya-os/mcp-i-cloudflare 1.5.1-canary.1 → 1.5.1-canary.11
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/adapter.d.ts +31 -1
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +284 -23
- package/dist/adapter.js.map +1 -1
- package/dist/constants/storage-keys.d.ts +89 -0
- package/dist/constants/storage-keys.d.ts.map +1 -0
- package/dist/constants/storage-keys.js +142 -0
- package/dist/constants/storage-keys.js.map +1 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -3
- package/dist/index.js.map +1 -1
- package/dist/runtime/oauth-handler.d.ts +6 -0
- package/dist/runtime/oauth-handler.d.ts.map +1 -1
- package/dist/runtime/oauth-handler.js +96 -21
- package/dist/runtime/oauth-handler.js.map +1 -1
- package/dist/services/admin.service.d.ts +4 -0
- package/dist/services/admin.service.d.ts.map +1 -1
- package/dist/services/admin.service.js +170 -77
- package/dist/services/admin.service.js.map +1 -1
- package/dist/services/consent-config.service.d.ts.map +1 -1
- package/dist/services/consent-config.service.js +7 -3
- package/dist/services/consent-config.service.js.map +1 -1
- package/dist/services/consent-page-renderer.d.ts +8 -2
- package/dist/services/consent-page-renderer.d.ts.map +1 -1
- package/dist/services/consent-page-renderer.js +42 -8
- package/dist/services/consent-page-renderer.js.map +1 -1
- package/dist/services/consent.service.d.ts +90 -0
- package/dist/services/consent.service.d.ts.map +1 -1
- package/dist/services/consent.service.js +556 -99
- package/dist/services/consent.service.js.map +1 -1
- package/dist/services/delegation.service.d.ts.map +1 -1
- package/dist/services/delegation.service.js +54 -19
- package/dist/services/delegation.service.js.map +1 -1
- package/dist/services/oauth-security.service.d.ts +92 -0
- package/dist/services/oauth-security.service.d.ts.map +1 -0
- package/dist/services/oauth-security.service.js +260 -0
- package/dist/services/oauth-security.service.js.map +1 -0
- package/dist/services/rate-limit.service.d.ts +59 -0
- package/dist/services/rate-limit.service.d.ts.map +1 -0
- package/dist/services/rate-limit.service.js +146 -0
- package/dist/services/rate-limit.service.js.map +1 -0
- package/dist/types/client.d.ts +10 -0
- package/dist/types/client.d.ts.map +1 -1
- package/dist/utils/day0-config.d.ts +51 -0
- package/dist/utils/day0-config.d.ts.map +1 -0
- package/dist/utils/day0-config.js +72 -0
- package/dist/utils/day0-config.js.map +1 -0
- package/package.json +1 -1
|
@@ -9,19 +9,260 @@
|
|
|
9
9
|
import { ConsentConfigService } from "./consent-config.service";
|
|
10
10
|
import { ConsentPageRenderer } from "./consent-page-renderer";
|
|
11
11
|
import { DEFAULT_AGENTSHIELD_URL, DEFAULT_SESSION_CACHE_TTL, } from "../constants";
|
|
12
|
+
import { STORAGE_KEYS } from "../constants/storage-keys";
|
|
13
|
+
import { loadDay0Config, getDelegationFieldName } from "../utils/day0-config";
|
|
12
14
|
import { validateConsentApprovalRequest, } from "@kya-os/contracts/consent";
|
|
13
15
|
import { AGENTSHIELD_ENDPOINTS, createDelegationAPIResponseSchema, createDelegationResponseSchema, } from "@kya-os/contracts/agentshield-api";
|
|
16
|
+
import { UserDidManager } from "@kya-os/mcp-i-core";
|
|
17
|
+
import { WebCryptoProvider } from "../providers/crypto";
|
|
14
18
|
export class ConsentService {
|
|
15
19
|
configService;
|
|
16
20
|
renderer;
|
|
17
21
|
env;
|
|
18
22
|
runtime;
|
|
23
|
+
userDidManager; // Cached instance for consistent DID generation
|
|
19
24
|
constructor(env, runtime) {
|
|
20
25
|
this.env = env;
|
|
21
26
|
this.runtime = runtime;
|
|
22
27
|
this.configService = new ConsentConfigService(env);
|
|
23
28
|
this.renderer = new ConsentPageRenderer();
|
|
24
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Get or generate User DID for a session
|
|
32
|
+
*
|
|
33
|
+
* Phase 4 PR #1: Generates ephemeral DIDs for sessions
|
|
34
|
+
* Phase 4 PR #3: Checks OAuth mappings for persistent DIDs
|
|
35
|
+
*
|
|
36
|
+
* @param sessionId - Session ID
|
|
37
|
+
* @param oauthIdentity - Optional OAuth provider identity
|
|
38
|
+
* @returns User DID (did:key format)
|
|
39
|
+
*/
|
|
40
|
+
async getUserDidForSession(sessionId, oauthIdentity) {
|
|
41
|
+
// If OAuth identity provided, check for existing mapping first
|
|
42
|
+
if (oauthIdentity && this.env.DELEGATION_STORAGE) {
|
|
43
|
+
try {
|
|
44
|
+
const oauthKey = STORAGE_KEYS.oauthIdentity(oauthIdentity.provider, oauthIdentity.subject);
|
|
45
|
+
const mappedUserDid = await this.env.DELEGATION_STORAGE.get(oauthKey, "text");
|
|
46
|
+
if (mappedUserDid) {
|
|
47
|
+
console.log("[ConsentService] Found persistent User DID from OAuth mapping");
|
|
48
|
+
return mappedUserDid;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
console.warn("[ConsentService] Failed to check OAuth mapping:", error);
|
|
53
|
+
// Continue with ephemeral DID generation
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Continue with existing ephemeral DID generation logic
|
|
57
|
+
if (!this.env.DELEGATION_STORAGE) {
|
|
58
|
+
// No storage - use cached UserDidManager instance for consistent DID generation
|
|
59
|
+
if (!this.userDidManager) {
|
|
60
|
+
this.userDidManager = new UserDidManager({
|
|
61
|
+
crypto: new WebCryptoProvider(),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return await this.userDidManager.getOrCreateUserDid(sessionId);
|
|
65
|
+
}
|
|
66
|
+
const sessionKey = STORAGE_KEYS.session(sessionId);
|
|
67
|
+
// Try session cache first
|
|
68
|
+
try {
|
|
69
|
+
const sessionData = (await this.env.DELEGATION_STORAGE.get(sessionKey, "json"));
|
|
70
|
+
if (sessionData?.userDid) {
|
|
71
|
+
return sessionData.userDid;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
console.warn("[ConsentService] Failed to read session cache:", error);
|
|
76
|
+
}
|
|
77
|
+
// Generate ephemeral DID using cached UserDidManager instance
|
|
78
|
+
if (!this.userDidManager) {
|
|
79
|
+
this.userDidManager = new UserDidManager({
|
|
80
|
+
crypto: new WebCryptoProvider(),
|
|
81
|
+
storage: {
|
|
82
|
+
get: async (key) => {
|
|
83
|
+
try {
|
|
84
|
+
const data = await this.env.DELEGATION_STORAGE.get(`userDid:${key}`, "text");
|
|
85
|
+
return data || null;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
set: async (key, value, ttl) => {
|
|
92
|
+
await this.env.DELEGATION_STORAGE.put(`userDid:${key}`, value, {
|
|
93
|
+
expirationTtl: ttl || DEFAULT_SESSION_CACHE_TTL,
|
|
94
|
+
});
|
|
95
|
+
},
|
|
96
|
+
delete: async (key) => {
|
|
97
|
+
await this.env.DELEGATION_STORAGE.delete(`userDid:${key}`);
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const userDid = await this.userDidManager.getOrCreateUserDid(sessionId);
|
|
103
|
+
// Cache in session storage
|
|
104
|
+
try {
|
|
105
|
+
const existingSession = (await this.env.DELEGATION_STORAGE.get(sessionKey, "json"));
|
|
106
|
+
await this.env.DELEGATION_STORAGE.put(sessionKey, JSON.stringify({
|
|
107
|
+
...existingSession,
|
|
108
|
+
userDid,
|
|
109
|
+
}), { expirationTtl: DEFAULT_SESSION_CACHE_TTL });
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
console.warn("[ConsentService] Failed to cache userDid in session:", error);
|
|
113
|
+
// Non-fatal - continue with generated DID
|
|
114
|
+
}
|
|
115
|
+
return userDid;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Check if OAuth is required for delegation creation
|
|
119
|
+
*
|
|
120
|
+
* Determines if OAuth flow should be used instead of direct delegation creation.
|
|
121
|
+
* OAuth is required if:
|
|
122
|
+
* - Project has OAuth provider configured in AgentShield
|
|
123
|
+
* - No OAuth identity is present in the request
|
|
124
|
+
*
|
|
125
|
+
* @param projectId - Project ID to check
|
|
126
|
+
* @param oauthIdentity - Optional OAuth identity from cookie
|
|
127
|
+
* @returns True if OAuth redirect is required
|
|
128
|
+
*/
|
|
129
|
+
async isOAuthRequired(projectId, oauthIdentity) {
|
|
130
|
+
// If OAuth identity is already present, OAuth is not required
|
|
131
|
+
if (oauthIdentity && oauthIdentity.provider && oauthIdentity.subject) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
// Check project config to see if OAuth is configured
|
|
135
|
+
try {
|
|
136
|
+
const agentShieldUrl = this.env.AGENTSHIELD_API_URL || DEFAULT_AGENTSHIELD_URL;
|
|
137
|
+
const apiKey = this.env.AGENTSHIELD_API_KEY;
|
|
138
|
+
if (!apiKey) {
|
|
139
|
+
// No API key - can't check, assume OAuth not required
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
// Fetch project config to check for OAuth provider
|
|
143
|
+
const configUrl = `${agentShieldUrl}/api/v1/bouncer/projects/${projectId}/config`;
|
|
144
|
+
const response = await fetch(configUrl, {
|
|
145
|
+
headers: {
|
|
146
|
+
'X-API-Key': apiKey,
|
|
147
|
+
'X-Project-Id': projectId,
|
|
148
|
+
"Content-Type": "application/json",
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
if (response.ok) {
|
|
152
|
+
const config = await response.json();
|
|
153
|
+
// Check if OAuth provider is configured
|
|
154
|
+
// OAuth is required if project has OAuth provider but no identity is present
|
|
155
|
+
const hasOAuthProvider = config?.oauth?.provider || config?.oauth_provider;
|
|
156
|
+
return !!hasOAuthProvider;
|
|
157
|
+
}
|
|
158
|
+
// If API call fails, default to not requiring OAuth (backward compatibility)
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
console.warn("[ConsentService] Failed to check OAuth requirement:", error);
|
|
163
|
+
// On error, default to not requiring OAuth (backward compatibility)
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Build OAuth authorization URL
|
|
169
|
+
*
|
|
170
|
+
* Creates the OAuth authorization URL with proper state parameter
|
|
171
|
+
* for redirecting to OAuth provider.
|
|
172
|
+
*
|
|
173
|
+
* @param projectId - Project ID
|
|
174
|
+
* @param agentDid - Agent DID
|
|
175
|
+
* @param sessionId - Session ID
|
|
176
|
+
* @param scopes - Requested scopes
|
|
177
|
+
* @param serverUrl - Server URL for callback
|
|
178
|
+
* @returns OAuth authorization URL
|
|
179
|
+
*/
|
|
180
|
+
buildOAuthUrl(projectId, agentDid, sessionId, scopes, serverUrl) {
|
|
181
|
+
const agentShieldUrl = this.env.AGENTSHIELD_API_URL || DEFAULT_AGENTSHIELD_URL;
|
|
182
|
+
// Generate a temporary delegation ID for state (will be created after OAuth)
|
|
183
|
+
const delegationId = `temp-${Date.now()}`;
|
|
184
|
+
// Build state parameter with required fields
|
|
185
|
+
const state = {
|
|
186
|
+
project_id: projectId,
|
|
187
|
+
agent_did: agentDid,
|
|
188
|
+
session_id: sessionId,
|
|
189
|
+
delegation_id: delegationId,
|
|
190
|
+
scopes: scopes,
|
|
191
|
+
};
|
|
192
|
+
// Encode state as base64 (Cloudflare Workers compatible)
|
|
193
|
+
const stateParam = btoa(JSON.stringify(state));
|
|
194
|
+
// Build OAuth authorization URL
|
|
195
|
+
const oauthUrl = new URL(`${agentShieldUrl}/bouncer/oauth/authorize`);
|
|
196
|
+
oauthUrl.searchParams.set("response_type", "code");
|
|
197
|
+
oauthUrl.searchParams.set("client_id", projectId); // Use projectId as client_id
|
|
198
|
+
oauthUrl.searchParams.set("redirect_uri", `${serverUrl}/oauth/callback`);
|
|
199
|
+
oauthUrl.searchParams.set("scope", scopes.join(" "));
|
|
200
|
+
oauthUrl.searchParams.set("state", stateParam);
|
|
201
|
+
return oauthUrl.toString();
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Link OAuth identity to User DID
|
|
205
|
+
*
|
|
206
|
+
* Maps OAuth provider identity (provider + subject) to a persistent User DID.
|
|
207
|
+
* If an ephemeral DID exists for the session, it becomes persistent.
|
|
208
|
+
*
|
|
209
|
+
* Phase 4 PR #3: OAuth Identity Linking
|
|
210
|
+
*
|
|
211
|
+
* @param oauthIdentity - OAuth provider identity
|
|
212
|
+
* @param sessionId - Current session ID (for ephemeral DID lookup)
|
|
213
|
+
* @returns Persistent User DID
|
|
214
|
+
*/
|
|
215
|
+
async linkOAuthToUserDid(oauthIdentity, sessionId) {
|
|
216
|
+
if (!this.env.DELEGATION_STORAGE) {
|
|
217
|
+
// No storage - can't persist mapping, return ephemeral DID
|
|
218
|
+
console.warn("[ConsentService] No storage available for OAuth linking");
|
|
219
|
+
return await this.getUserDidForSession(sessionId);
|
|
220
|
+
}
|
|
221
|
+
const oauthKey = STORAGE_KEYS.oauthIdentity(oauthIdentity.provider, oauthIdentity.subject);
|
|
222
|
+
// Check if OAuth identity already mapped
|
|
223
|
+
try {
|
|
224
|
+
const existingUserDid = await this.env.DELEGATION_STORAGE.get(oauthKey, "text");
|
|
225
|
+
if (existingUserDid) {
|
|
226
|
+
console.log("[ConsentService] OAuth identity already mapped:", {
|
|
227
|
+
provider: oauthIdentity.provider,
|
|
228
|
+
subject: oauthIdentity.subject.substring(0, 20) + "...",
|
|
229
|
+
userDid: existingUserDid.substring(0, 20) + "...",
|
|
230
|
+
});
|
|
231
|
+
return existingUserDid;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
console.warn("[ConsentService] Failed to check OAuth mapping:", error);
|
|
236
|
+
// Continue to create new mapping
|
|
237
|
+
}
|
|
238
|
+
// Get/create User DID for session (may be ephemeral)
|
|
239
|
+
const userDid = await this.getUserDidForSession(sessionId);
|
|
240
|
+
// Store OAuth identity mapping (persistent - 90 days)
|
|
241
|
+
try {
|
|
242
|
+
await this.env.DELEGATION_STORAGE.put(oauthKey, userDid, {
|
|
243
|
+
expirationTtl: 90 * 24 * 60 * 60, // 90 days
|
|
244
|
+
});
|
|
245
|
+
// Also store full OAuth identity info for reference
|
|
246
|
+
const oauthIdentityKey = STORAGE_KEYS.userDid(oauthIdentity.provider, oauthIdentity.subject);
|
|
247
|
+
await this.env.DELEGATION_STORAGE.put(oauthIdentityKey, JSON.stringify(oauthIdentity), {
|
|
248
|
+
expirationTtl: 90 * 24 * 60 * 60, // 90 days
|
|
249
|
+
});
|
|
250
|
+
console.log("[ConsentService] OAuth identity linked to User DID:", {
|
|
251
|
+
provider: oauthIdentity.provider,
|
|
252
|
+
subject: oauthIdentity.subject.substring(0, 20) + "...",
|
|
253
|
+
userDid: userDid.substring(0, 20) + "...",
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
console.error("[ConsentService] Failed to store OAuth mapping:", error);
|
|
258
|
+
// Non-fatal - continue with User DID
|
|
259
|
+
}
|
|
260
|
+
// Note: Ephemeral → persistent migration happens automatically
|
|
261
|
+
// The ephemeral DID becomes persistent when linked to OAuth identity
|
|
262
|
+
// Existing delegations with ephemeral DID will continue to work
|
|
263
|
+
// New delegations will use the persistent DID
|
|
264
|
+
return userDid;
|
|
265
|
+
}
|
|
25
266
|
/**
|
|
26
267
|
* Handle consent requests
|
|
27
268
|
*
|
|
@@ -121,6 +362,48 @@ export class ConsentService {
|
|
|
121
362
|
});
|
|
122
363
|
}
|
|
123
364
|
}
|
|
365
|
+
// Phase 4 PR #4: Extract OAuth identity from cookie (server-side)
|
|
366
|
+
let oauthIdentity = undefined;
|
|
367
|
+
try {
|
|
368
|
+
const cookieHeader = request.headers.get("Cookie");
|
|
369
|
+
if (cookieHeader) {
|
|
370
|
+
const cookies = cookieHeader.split("; ").map((c) => c.trim());
|
|
371
|
+
const oauthCookie = cookies.find((c) => c.startsWith("oauth_identity="));
|
|
372
|
+
if (oauthCookie) {
|
|
373
|
+
// Extract cookie value correctly - handle values that may contain '=' characters
|
|
374
|
+
// Use indexOf to find first '=' and take everything after it
|
|
375
|
+
const equalsIndex = oauthCookie.indexOf("=");
|
|
376
|
+
const cookieValue = equalsIndex >= 0 ? oauthCookie.substring(equalsIndex + 1) : "";
|
|
377
|
+
// Validate it's valid JSON before passing to client
|
|
378
|
+
try {
|
|
379
|
+
const parsed = JSON.parse(decodeURIComponent(cookieValue));
|
|
380
|
+
// Basic validation - ensure it has required fields
|
|
381
|
+
if (parsed && parsed.provider && parsed.subject) {
|
|
382
|
+
oauthIdentity = parsed;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
catch (parseError) {
|
|
386
|
+
console.warn("[ConsentService] Invalid OAuth cookie format:", parseError);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
console.warn("[ConsentService] Failed to extract OAuth cookie:", error);
|
|
393
|
+
// Non-fatal - continue without OAuth identity
|
|
394
|
+
}
|
|
395
|
+
// Check if OAuth is required (after extracting OAuth identity)
|
|
396
|
+
const oauthRequired = await this.isOAuthRequired(projectId, oauthIdentity);
|
|
397
|
+
if (oauthRequired) {
|
|
398
|
+
// OAuth is required - redirect to OAuth provider instead of showing consent page
|
|
399
|
+
const oauthUrl = this.buildOAuthUrl(projectId, agentDid, sessionId, scopes, serverUrl);
|
|
400
|
+
console.log("[ConsentService] OAuth required, redirecting to OAuth provider:", {
|
|
401
|
+
projectId,
|
|
402
|
+
agentDid: agentDid.substring(0, 20) + "...",
|
|
403
|
+
oauthUrl: oauthUrl.substring(0, 100) + "...",
|
|
404
|
+
});
|
|
405
|
+
return Response.redirect(oauthUrl, 302);
|
|
406
|
+
}
|
|
124
407
|
// Build consent page config
|
|
125
408
|
const pageConfig = {
|
|
126
409
|
tool,
|
|
@@ -135,8 +418,8 @@ export class ConsentService {
|
|
|
135
418
|
customFields: consentConfig.customFields,
|
|
136
419
|
autoClose: consentConfig.ui?.autoClose,
|
|
137
420
|
};
|
|
138
|
-
// Render page
|
|
139
|
-
const html = this.renderer.render(pageConfig);
|
|
421
|
+
// Render page with OAuth identity (if available)
|
|
422
|
+
const html = this.renderer.render(pageConfig, oauthIdentity);
|
|
140
423
|
return new Response(html, {
|
|
141
424
|
status: 200,
|
|
142
425
|
headers: {
|
|
@@ -250,78 +533,41 @@ export class ConsentService {
|
|
|
250
533
|
};
|
|
251
534
|
}
|
|
252
535
|
try {
|
|
253
|
-
//
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
// - user_identifier: string (optional user identifier)
|
|
269
|
-
// - agent_name: string (optional agent name)
|
|
270
|
-
// - constraints: object (optional constraints)
|
|
271
|
-
// - custom_fields: object (optional custom fields)
|
|
272
|
-
// - metadata: object (optional metadata)
|
|
273
|
-
const expiresInDays = 7; // Default to 7 days
|
|
274
|
-
const delegationRequest = {
|
|
275
|
-
agent_did: request.agent_did,
|
|
276
|
-
scopes: request.scopes,
|
|
277
|
-
expires_in_days: expiresInDays,
|
|
278
|
-
// NOTE: expires_at and expires_in_days are mutually exclusive.
|
|
279
|
-
// We use expires_in_days for simplicity. Do not include both fields.
|
|
280
|
-
};
|
|
281
|
-
// Include session_id if provided
|
|
282
|
-
if (request.session_id) {
|
|
283
|
-
delegationRequest.session_id = request.session_id;
|
|
284
|
-
}
|
|
285
|
-
// Include project_id if provided
|
|
286
|
-
if (request.project_id) {
|
|
287
|
-
delegationRequest.project_id = request.project_id;
|
|
288
|
-
}
|
|
289
|
-
// Include custom_fields if provided (send as custom_fields, not metadata)
|
|
290
|
-
if (request.customFields &&
|
|
291
|
-
Object.keys(request.customFields).length > 0) {
|
|
292
|
-
delegationRequest.custom_fields = request.customFields;
|
|
536
|
+
// Load Day0 configuration to determine field name and API capabilities
|
|
537
|
+
await loadDay0Config(this.env.DELEGATION_STORAGE);
|
|
538
|
+
const fieldName = await getDelegationFieldName(this.env.DELEGATION_STORAGE);
|
|
539
|
+
// Get userDID from session or generate new ephemeral DID
|
|
540
|
+
// Phase 4 PR #3: Use OAuth identity if provided in approval request
|
|
541
|
+
let userDid;
|
|
542
|
+
if (this.env.DELEGATION_STORAGE && request.session_id) {
|
|
543
|
+
try {
|
|
544
|
+
// Pass OAuth identity if available in approval request
|
|
545
|
+
userDid = await this.getUserDidForSession(request.session_id, request.oauth_identity);
|
|
546
|
+
}
|
|
547
|
+
catch (error) {
|
|
548
|
+
console.warn("[ConsentService] Failed to get/generate userDid:", error);
|
|
549
|
+
// Continue without userDid - delegation will use ephemeral placeholder
|
|
550
|
+
}
|
|
293
551
|
}
|
|
552
|
+
const expiresInDays = 7; // Default to 7 days
|
|
553
|
+
// Build delegation request with error-based format detection
|
|
554
|
+
// Try full format first, fallback to simplified format on error
|
|
555
|
+
const delegationRequest = await this.buildDelegationRequest(request, userDid, expiresInDays, fieldName);
|
|
294
556
|
console.log("[ConsentService] Creating delegation:", {
|
|
295
557
|
url: `${agentShieldUrl}${AGENTSHIELD_ENDPOINTS.DELEGATIONS_CREATE}`,
|
|
296
558
|
agentDid: request.agent_did.substring(0, 20) + "...",
|
|
297
559
|
scopes: request.scopes,
|
|
298
560
|
expiresInDays,
|
|
299
561
|
hasApiKey: !!apiKey,
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const response = await fetch(`${agentShieldUrl}${AGENTSHIELD_ENDPOINTS.DELEGATIONS_CREATE}`, {
|
|
303
|
-
method: "POST",
|
|
304
|
-
headers: {
|
|
305
|
-
Authorization: `Bearer ${apiKey}`,
|
|
306
|
-
"Content-Type": "application/json",
|
|
307
|
-
},
|
|
308
|
-
body: JSON.stringify(delegationRequest),
|
|
562
|
+
fieldName,
|
|
563
|
+
hasUserDid: !!userDid,
|
|
309
564
|
});
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
statusText: response.statusText,
|
|
315
|
-
error: errorText,
|
|
316
|
-
requestBody: JSON.stringify(delegationRequest, null, 2),
|
|
317
|
-
});
|
|
318
|
-
return {
|
|
319
|
-
success: false,
|
|
320
|
-
error: `API error: ${response.status}`,
|
|
321
|
-
error_code: "api_error",
|
|
322
|
-
};
|
|
565
|
+
// Error-based format detection: try request format, fallback on error
|
|
566
|
+
const response = await this.tryAPICall(agentShieldUrl, apiKey, delegationRequest);
|
|
567
|
+
if (!response.success) {
|
|
568
|
+
return response;
|
|
323
569
|
}
|
|
324
|
-
const responseData =
|
|
570
|
+
const responseData = response.data;
|
|
325
571
|
// Canonical format per @kya-os/contracts/agentshield-api:
|
|
326
572
|
// Wrapped: { success: true, data: { delegation_id: string, agent_did: string, ... } }
|
|
327
573
|
// Unwrapped: { delegation_id: string, agent_did: string, ... }
|
|
@@ -415,38 +661,16 @@ export class ConsentService {
|
|
|
415
661
|
try {
|
|
416
662
|
// Default TTL: 7 days (same as delegation expiration)
|
|
417
663
|
const ttl = 7 * 24 * 60 * 60; // 7 days in seconds
|
|
418
|
-
//
|
|
419
|
-
//
|
|
420
|
-
|
|
421
|
-
//
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
//
|
|
426
|
-
// 2. Store tokens with userDID in the key: `user:${userDid}:agent:${agentDid}:delegation`
|
|
427
|
-
// 3. Also store with agent-scoped key for backward compatibility
|
|
428
|
-
//
|
|
429
|
-
// Note: The consent approval request currently doesn't include userDID, so we need to:
|
|
430
|
-
// - Extract userDID from OAuth callback (if using OAuth flow)
|
|
431
|
-
// - Or extract userDID from session (if session has userDID)
|
|
432
|
-
// - Or add userDID to ConsentApprovalRequest interface
|
|
433
|
-
const userDid = undefined; // TODO: Get userDID from approval request or OAuth callback
|
|
434
|
-
// Store using agent DID (primary, survives session changes)
|
|
435
|
-
// WARNING: This is shared across all users - if multiple users delegate to the same
|
|
436
|
-
// agent, each delegation will overwrite the previous one. This is acceptable for
|
|
437
|
-
// single-user agents, but multi-user agents should use user+agent scoped keys.
|
|
438
|
-
const agentKey = `agent:${agentDid}:delegation`;
|
|
439
|
-
await delegationStorage.put(agentKey, token, {
|
|
440
|
-
expirationTtl: ttl,
|
|
441
|
-
});
|
|
442
|
-
console.log("[ConsentService] ✅ Token stored with agent DID:", {
|
|
443
|
-
key: agentKey,
|
|
444
|
-
ttl,
|
|
445
|
-
delegationId,
|
|
446
|
-
});
|
|
447
|
-
// Store using user+agent DID if userDID is available (preferred for multi-user scenarios)
|
|
664
|
+
// Get userDID from approval request or session (Phase 4)
|
|
665
|
+
// For now, try to extract from request or session cache
|
|
666
|
+
let userDid;
|
|
667
|
+
// Try to get userDID from session cache
|
|
668
|
+
const sessionKey = STORAGE_KEYS.session(sessionId);
|
|
669
|
+
const sessionData = (await delegationStorage.get(sessionKey, "json"));
|
|
670
|
+
userDid = sessionData?.userDid;
|
|
671
|
+
// Primary: User+Agent scoped (no conflicts) - Phase 4
|
|
448
672
|
if (userDid) {
|
|
449
|
-
const userAgentKey =
|
|
673
|
+
const userAgentKey = STORAGE_KEYS.delegation(userDid, agentDid);
|
|
450
674
|
await delegationStorage.put(userAgentKey, token, {
|
|
451
675
|
expirationTtl: ttl,
|
|
452
676
|
});
|
|
@@ -456,15 +680,31 @@ export class ConsentService {
|
|
|
456
680
|
delegationId,
|
|
457
681
|
});
|
|
458
682
|
}
|
|
459
|
-
//
|
|
460
|
-
const
|
|
461
|
-
await delegationStorage.put(
|
|
683
|
+
// Backward compatibility: Agent-only key (24 hour TTL)
|
|
684
|
+
const legacyKey = STORAGE_KEYS.legacyDelegation(agentDid);
|
|
685
|
+
await delegationStorage.put(legacyKey, token, {
|
|
686
|
+
expirationTtl: 24 * 60 * 60, // 24 hours only
|
|
687
|
+
});
|
|
688
|
+
console.log("[ConsentService] ✅ Token stored with legacy agent key:", {
|
|
689
|
+
key: legacyKey,
|
|
690
|
+
ttl: 24 * 60 * 60,
|
|
691
|
+
delegationId,
|
|
692
|
+
});
|
|
693
|
+
// Session cache for fast lookup (shorter TTL for performance)
|
|
694
|
+
const sessionDataToStore = {
|
|
695
|
+
userDid,
|
|
696
|
+
agentDid,
|
|
697
|
+
delegationToken: token,
|
|
698
|
+
cachedAt: Date.now(),
|
|
699
|
+
};
|
|
700
|
+
await delegationStorage.put(sessionKey, JSON.stringify(sessionDataToStore), {
|
|
462
701
|
expirationTtl: Math.min(ttl, DEFAULT_SESSION_CACHE_TTL),
|
|
463
702
|
});
|
|
464
703
|
console.log("[ConsentService] ✅ Token cached for session:", {
|
|
465
704
|
key: sessionKey,
|
|
466
705
|
ttl: Math.min(ttl, DEFAULT_SESSION_CACHE_TTL),
|
|
467
706
|
sessionId,
|
|
707
|
+
userDid,
|
|
468
708
|
});
|
|
469
709
|
}
|
|
470
710
|
catch (error) {
|
|
@@ -506,5 +746,222 @@ export class ConsentService {
|
|
|
506
746
|
},
|
|
507
747
|
});
|
|
508
748
|
}
|
|
749
|
+
/**
|
|
750
|
+
* Build delegation request with error-based format detection
|
|
751
|
+
*
|
|
752
|
+
* Uses Day0 config to determine field name and includes issuerDid when available.
|
|
753
|
+
*/
|
|
754
|
+
async buildDelegationRequest(request, userDid, expiresInDays, fieldName) {
|
|
755
|
+
const baseRequest = {
|
|
756
|
+
agent_did: request.agent_did,
|
|
757
|
+
scopes: request.scopes,
|
|
758
|
+
expires_in_days: expiresInDays,
|
|
759
|
+
};
|
|
760
|
+
// Note: session_id and project_id are NOT in createDelegationSchema
|
|
761
|
+
// - project_id is extracted from API key context by AgentShield middleware
|
|
762
|
+
// - session_id is not needed for delegation creation
|
|
763
|
+
// These fields are removed to match AgentShield API schema exactly
|
|
764
|
+
// Check cached format preference
|
|
765
|
+
const cacheKey = STORAGE_KEYS.formatPreference();
|
|
766
|
+
let cachedFormat = null;
|
|
767
|
+
if (this.env.DELEGATION_STORAGE) {
|
|
768
|
+
try {
|
|
769
|
+
const cached = (await this.env.DELEGATION_STORAGE.get(cacheKey, "json"));
|
|
770
|
+
if (cached &&
|
|
771
|
+
cached.timestamp &&
|
|
772
|
+
Date.now() - cached.timestamp < 3600000) {
|
|
773
|
+
cachedFormat = cached.format;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
catch {
|
|
777
|
+
// Ignore cache errors
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
// If we have a cached preference, use it directly
|
|
781
|
+
if (cachedFormat === "full") {
|
|
782
|
+
return this.buildFullFormatRequest(request, userDid, expiresInDays);
|
|
783
|
+
}
|
|
784
|
+
else if (cachedFormat === "simplified") {
|
|
785
|
+
return this.buildSimplifiedFormatRequest(request, userDid, expiresInDays, fieldName);
|
|
786
|
+
}
|
|
787
|
+
// No cache - return request that will be tried with error-based detection
|
|
788
|
+
return {
|
|
789
|
+
_tryFormats: true,
|
|
790
|
+
fullFormat: await this.buildFullFormatRequest(request, userDid, expiresInDays),
|
|
791
|
+
simplifiedFormat: this.buildSimplifiedFormatRequest(request, userDid, expiresInDays, fieldName),
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Build full DelegationRecord format request (future format)
|
|
796
|
+
*/
|
|
797
|
+
async buildFullFormatRequest(request, userDid, expiresInDays) {
|
|
798
|
+
const notAfter = Date.now() + expiresInDays * 24 * 60 * 60 * 1000;
|
|
799
|
+
return {
|
|
800
|
+
delegation: {
|
|
801
|
+
id: crypto.randomUUID(),
|
|
802
|
+
issuerDid: userDid || "did:key:z6MkEphemeral", // Use ephemeral if no userDid
|
|
803
|
+
subjectDid: request.agent_did,
|
|
804
|
+
constraints: {
|
|
805
|
+
scopes: request.scopes,
|
|
806
|
+
notAfter,
|
|
807
|
+
notBefore: Date.now(),
|
|
808
|
+
},
|
|
809
|
+
status: "active",
|
|
810
|
+
createdAt: Date.now(),
|
|
811
|
+
},
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Check if a string is a valid UUID
|
|
816
|
+
*
|
|
817
|
+
* Validates that project_id is a UUID before including it in API requests.
|
|
818
|
+
* If project_id is not a UUID (e.g., project slug), it's omitted and the
|
|
819
|
+
* API extracts it from the API key instead.
|
|
820
|
+
*
|
|
821
|
+
* @param str - String to validate
|
|
822
|
+
* @returns True if string is a valid UUID
|
|
823
|
+
*/
|
|
824
|
+
isValidUUID(str) {
|
|
825
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
826
|
+
return uuidRegex.test(str);
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Build simplified format request with proper field name
|
|
830
|
+
*/
|
|
831
|
+
buildSimplifiedFormatRequest(request, userDid, expiresInDays, fieldName) {
|
|
832
|
+
const simplifiedRequest = {
|
|
833
|
+
agent_did: request.agent_did,
|
|
834
|
+
scopes: request.scopes,
|
|
835
|
+
expires_in_days: expiresInDays,
|
|
836
|
+
};
|
|
837
|
+
// Include user_identifier if we have userDid (matches AgentShield schema)
|
|
838
|
+
// Note: session_id and project_id are NOT in createDelegationSchema
|
|
839
|
+
// - project_id is extracted from API key context by AgentShield
|
|
840
|
+
// - session_id is not needed for delegation creation
|
|
841
|
+
if (userDid) {
|
|
842
|
+
simplifiedRequest.user_identifier = userDid;
|
|
843
|
+
// Use the correct field name (metadata or custom_fields) from Day0 config
|
|
844
|
+
simplifiedRequest[fieldName] = {
|
|
845
|
+
issuer_did: userDid,
|
|
846
|
+
subject_did: request.agent_did,
|
|
847
|
+
format_version: "simplified_v1",
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
// Include custom_fields from request if provided
|
|
851
|
+
if (request.customFields && Object.keys(request.customFields).length > 0) {
|
|
852
|
+
simplifiedRequest.custom_fields = {
|
|
853
|
+
...(simplifiedRequest.custom_fields || {}),
|
|
854
|
+
...request.customFields,
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
return simplifiedRequest;
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Try API call with error-based format detection
|
|
861
|
+
*/
|
|
862
|
+
async tryAPICall(agentShieldUrl, apiKey, request) {
|
|
863
|
+
// Handle format detection
|
|
864
|
+
if (request._tryFormats && request.fullFormat && request.simplifiedFormat) {
|
|
865
|
+
// Try full format first
|
|
866
|
+
const fullResponse = await this.makeAPICall(agentShieldUrl, apiKey, request.fullFormat);
|
|
867
|
+
if (fullResponse.success ||
|
|
868
|
+
fullResponse.error_code !== "validation_error") {
|
|
869
|
+
// Full format worked or failed for non-format reasons
|
|
870
|
+
await this.cacheFormatPreference("full");
|
|
871
|
+
return fullResponse;
|
|
872
|
+
}
|
|
873
|
+
// Full format failed with validation error, try simplified
|
|
874
|
+
console.log("[ConsentService] Full format failed, trying simplified format...");
|
|
875
|
+
const simplifiedResponse = await this.makeAPICall(agentShieldUrl, apiKey, request.simplifiedFormat);
|
|
876
|
+
if (simplifiedResponse.success) {
|
|
877
|
+
await this.cacheFormatPreference("simplified");
|
|
878
|
+
}
|
|
879
|
+
return simplifiedResponse;
|
|
880
|
+
}
|
|
881
|
+
// Direct call (format already determined)
|
|
882
|
+
return this.makeAPICall(agentShieldUrl, apiKey, request);
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* Make API call and parse response
|
|
886
|
+
*/
|
|
887
|
+
async makeAPICall(agentShieldUrl, apiKey, requestBody) {
|
|
888
|
+
try {
|
|
889
|
+
const response = await fetch(`${agentShieldUrl}${AGENTSHIELD_ENDPOINTS.DELEGATIONS_CREATE}`, {
|
|
890
|
+
method: "POST",
|
|
891
|
+
headers: {
|
|
892
|
+
'X-API-Key': apiKey,
|
|
893
|
+
"Content-Type": "application/json",
|
|
894
|
+
"X-Request-ID": crypto.randomUUID(),
|
|
895
|
+
},
|
|
896
|
+
body: JSON.stringify(requestBody),
|
|
897
|
+
});
|
|
898
|
+
const responseText = await response.text();
|
|
899
|
+
let responseData;
|
|
900
|
+
try {
|
|
901
|
+
responseData = JSON.parse(responseText);
|
|
902
|
+
}
|
|
903
|
+
catch {
|
|
904
|
+
responseData = responseText;
|
|
905
|
+
}
|
|
906
|
+
// Check for validation error specifically
|
|
907
|
+
if (response.status === 400) {
|
|
908
|
+
const errorMessage = responseData
|
|
909
|
+
?.error?.message ||
|
|
910
|
+
responseData?.message ||
|
|
911
|
+
"Validation failed";
|
|
912
|
+
if (errorMessage.includes("format") ||
|
|
913
|
+
errorMessage.includes("schema") ||
|
|
914
|
+
errorMessage.includes("invalid")) {
|
|
915
|
+
return {
|
|
916
|
+
success: false,
|
|
917
|
+
error: errorMessage,
|
|
918
|
+
error_code: "validation_error",
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
if (!response.ok) {
|
|
923
|
+
const errorData = responseData;
|
|
924
|
+
return {
|
|
925
|
+
success: false,
|
|
926
|
+
error: errorData.error?.message ||
|
|
927
|
+
errorData.message ||
|
|
928
|
+
"API request failed",
|
|
929
|
+
error_code: errorData.error?.code || "api_error",
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
return {
|
|
933
|
+
success: true,
|
|
934
|
+
data: responseData,
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
catch (error) {
|
|
938
|
+
console.error("[ConsentService] API call failed:", error);
|
|
939
|
+
return {
|
|
940
|
+
success: false,
|
|
941
|
+
error: error instanceof Error ? error.message : "Network request failed",
|
|
942
|
+
error_code: "network_error",
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Cache successful format preference
|
|
948
|
+
*/
|
|
949
|
+
async cacheFormatPreference(format) {
|
|
950
|
+
if (!this.env.DELEGATION_STORAGE)
|
|
951
|
+
return;
|
|
952
|
+
const cacheKey = STORAGE_KEYS.formatPreference();
|
|
953
|
+
try {
|
|
954
|
+
await this.env.DELEGATION_STORAGE.put(cacheKey, JSON.stringify({
|
|
955
|
+
format,
|
|
956
|
+
timestamp: Date.now(),
|
|
957
|
+
}), {
|
|
958
|
+
expirationTtl: 3600, // 1 hour
|
|
959
|
+
});
|
|
960
|
+
console.log(`[ConsentService] Cached format preference: ${format}`);
|
|
961
|
+
}
|
|
962
|
+
catch (error) {
|
|
963
|
+
console.warn("[ConsentService] Failed to cache format preference:", error);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
509
966
|
}
|
|
510
967
|
//# sourceMappingURL=consent.service.js.map
|