@kya-os/mcp-i-core 1.8.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/dist/conformance/capability-verifier-adapter.d.ts +66 -0
  2. package/dist/conformance/capability-verifier-adapter.js +61 -0
  3. package/dist/conformance/index.d.ts +3 -1
  4. package/dist/conformance/index.js +7 -1
  5. package/dist/conformance/validate-level2.d.ts +13 -3
  6. package/dist/conformance/validate-level2.js +15 -5
  7. package/dist/conformance/vc-jwt-verifier.d.ts +55 -0
  8. package/dist/conformance/vc-jwt-verifier.js +147 -0
  9. package/dist/delegation/did-web-resolver.d.ts +60 -0
  10. package/dist/delegation/did-web-resolver.js +129 -0
  11. package/dist/index.d.ts +2 -1
  12. package/dist/index.js +9 -1
  13. package/package.json +3 -3
  14. package/dist/auth/handshake.d.ts +0 -119
  15. package/dist/auth/handshake.js +0 -250
  16. package/dist/auth/index.d.ts +0 -6
  17. package/dist/auth/index.js +0 -11
  18. package/dist/auth/types.d.ts +0 -46
  19. package/dist/auth/types.js +0 -10
  20. package/dist/cache/oauth-config-cache.d.ts +0 -69
  21. package/dist/cache/oauth-config-cache.js +0 -76
  22. package/dist/cache/tool-protection-cache.d.ts +0 -89
  23. package/dist/cache/tool-protection-cache.js +0 -118
  24. package/dist/compliance/index.d.ts +0 -8
  25. package/dist/compliance/index.js +0 -24
  26. package/dist/compliance/schema-registry.d.ts +0 -50
  27. package/dist/compliance/schema-registry.js +0 -438
  28. package/dist/compliance/schema-verifier.d.ts +0 -110
  29. package/dist/compliance/schema-verifier.js +0 -518
  30. package/dist/config/remote-config.d.ts +0 -173
  31. package/dist/config/remote-config.js +0 -253
  32. package/dist/config.d.ts +0 -182
  33. package/dist/config.js +0 -148
  34. package/dist/identity/idp-token-resolver.d.ts +0 -69
  35. package/dist/identity/idp-token-resolver.js +0 -136
  36. package/dist/identity/idp-token-storage.interface.d.ts +0 -80
  37. package/dist/identity/idp-token-storage.interface.js +0 -14
  38. package/dist/identity/user-did-manager.d.ts +0 -228
  39. package/dist/identity/user-did-manager.js +0 -356
  40. package/dist/logging/index.d.ts +0 -5
  41. package/dist/logging/index.js +0 -10
  42. package/dist/logging/logger.d.ts +0 -33
  43. package/dist/logging/logger.js +0 -104
  44. package/dist/proof/generator.d.ts +0 -109
  45. package/dist/proof/generator.js +0 -236
  46. package/dist/proof/index.d.ts +0 -5
  47. package/dist/proof/index.js +0 -11
  48. package/dist/providers/base.d.ts +0 -95
  49. package/dist/providers/base.js +0 -47
  50. package/dist/providers/memory.d.ts +0 -42
  51. package/dist/providers/memory.js +0 -121
  52. package/dist/runtime/audit-logger.d.ts +0 -37
  53. package/dist/runtime/audit-logger.js +0 -9
  54. package/dist/runtime/base.d.ts +0 -375
  55. package/dist/runtime/base.js +0 -1491
  56. package/dist/runtime/ext-apps-constants.d.ts +0 -14
  57. package/dist/runtime/ext-apps-constants.js +0 -17
  58. package/dist/services/access-control.service.d.ts +0 -138
  59. package/dist/services/access-control.service.js +0 -721
  60. package/dist/services/authorization/authorization-registry.d.ts +0 -29
  61. package/dist/services/authorization/authorization-registry.js +0 -58
  62. package/dist/services/authorization/types.d.ts +0 -53
  63. package/dist/services/authorization/types.js +0 -10
  64. package/dist/services/batch-delegation.service.d.ts +0 -53
  65. package/dist/services/batch-delegation.service.js +0 -95
  66. package/dist/services/crypto.service.d.ts +0 -69
  67. package/dist/services/crypto.service.js +0 -225
  68. package/dist/services/delegation-error-formatter.d.ts +0 -124
  69. package/dist/services/delegation-error-formatter.js +0 -214
  70. package/dist/services/errors.d.ts +0 -49
  71. package/dist/services/errors.js +0 -66
  72. package/dist/services/index.d.ts +0 -9
  73. package/dist/services/index.js +0 -15
  74. package/dist/services/oauth-config.service.d.ts +0 -53
  75. package/dist/services/oauth-config.service.js +0 -141
  76. package/dist/services/oauth-provider-registry.d.ts +0 -88
  77. package/dist/services/oauth-provider-registry.js +0 -128
  78. package/dist/services/oauth-service.d.ts +0 -122
  79. package/dist/services/oauth-service.js +0 -442
  80. package/dist/services/oauth-token-retrieval.service.d.ts +0 -49
  81. package/dist/services/oauth-token-retrieval.service.js +0 -150
  82. package/dist/services/policy.service.d.ts +0 -130
  83. package/dist/services/policy.service.js +0 -399
  84. package/dist/services/proof-verifier.d.ts +0 -98
  85. package/dist/services/proof-verifier.js +0 -319
  86. package/dist/services/provider-resolver.d.ts +0 -89
  87. package/dist/services/provider-resolver.js +0 -196
  88. package/dist/services/provider-validator.d.ts +0 -55
  89. package/dist/services/provider-validator.js +0 -135
  90. package/dist/services/session-registration.service.d.ts +0 -80
  91. package/dist/services/session-registration.service.js +0 -174
  92. package/dist/services/storage.service.d.ts +0 -116
  93. package/dist/services/storage.service.js +0 -411
  94. package/dist/services/tool-context-builder.d.ts +0 -74
  95. package/dist/services/tool-context-builder.js +0 -215
  96. package/dist/services/tool-protection.service.d.ts +0 -193
  97. package/dist/services/tool-protection.service.js +0 -736
  98. package/dist/session/index.d.ts +0 -5
  99. package/dist/session/index.js +0 -11
  100. package/dist/session/manager.d.ts +0 -113
  101. package/dist/session/manager.js +0 -273
  102. package/dist/types/oauth-required-error.d.ts +0 -46
  103. package/dist/types/oauth-required-error.js +0 -42
  104. package/dist/types/tool-protection.d.ts +0 -144
  105. package/dist/types/tool-protection.js +0 -48
  106. package/dist/utils/cors.d.ts +0 -44
  107. package/dist/utils/cors.js +0 -71
  108. package/dist/utils/index.d.ts +0 -8
  109. package/dist/utils/index.js +0 -24
  110. package/dist/utils/storage-keys.d.ts +0 -120
  111. package/dist/utils/storage-keys.js +0 -217
@@ -1,736 +0,0 @@
1
- "use strict";
2
- /**
3
- * ToolProtectionService - Fetches and caches tool protection configurations
4
- *
5
- * This service manages tool protection configuration from AgentShield API with
6
- * efficient caching and automatic synchronization support.
7
- *
8
- * CONFIGURATION PRECEDENCE (highest to lowest):
9
- * ---------------------------------------------
10
- * 1. Remote config (AgentShield API) - WINS (source of truth)
11
- * 2. Local config (localConfig) - Base defaults, overridden by remote
12
- * 3. Framework defaults
13
- *
14
- * OFFLINE FALLBACK (only when API unavailable):
15
- * ---------------------------------------------
16
- * 1. Stale cache (if allowStaleCache=true and within maxStaleCacheAge)
17
- * 2. offlineFallbackConfig (explicit offline-only config)
18
- * 3. failSafeBehavior (deny-all or allow-all)
19
- *
20
- * CORE FUNCTIONALITY:
21
- * -------------------
22
- * 1. Fetches tool protection config from AgentShield API (remote wins)
23
- * 2. Merges with local config (local serves as base defaults)
24
- * 3. Caches responses with configurable TTL (default 5 minutes)
25
- * 4. Provides delegation requirement checking before tool execution
26
- *
27
- * SYNCHRONIZATION WITH AGENTSHIELD:
28
- * ----------------------------------
29
- * When you update tool protection settings in the AgentShield dashboard:
30
- *
31
- * 1. Dashboard sends PATCH /api/internal/bouncer/tools/{projectId}/{toolName}
32
- * 2. AgentShield updates the database immediately (PostgreSQL JSONB column)
33
- * 3. Dashboard sends POST /admin/clear-cache to this service (automatic)
34
- * 4. This service clears the cached config from KV storage
35
- * 5. Next tool call fetches fresh config from AgentShield API
36
- * 6. New config is cached for the configured TTL period
37
- *
38
- * CACHE INVALIDATION:
39
- * -------------------
40
- * Cache is invalidated via POST /admin/clear-cache endpoint:
41
- * - Triggered automatically by AgentShield dashboard when tool protection changes
42
- * - Can be triggered manually for testing/debugging
43
- * - Requires API key authentication for security
44
- *
45
- * If cache is NOT cleared:
46
- * - Stale config is served until TTL expires (default 5 minutes)
47
- * - Configure shorter TTL via TOOL_PROTECTION_CACHE_TTL env var for faster updates
48
- * - Set to 0 for no cache (not recommended for production)
49
- *
50
- * TOOL DISCOVERY PREREQUISITE:
51
- * ----------------------------
52
- * IMPORTANT: Tools must be discovered before they can be protected!
53
- *
54
- * Discovery happens when:
55
- * - Agent makes first tool call with proof submission
56
- * - AgentShield extracts tool info from cryptographic proof
57
- * - Tool is added to bouncerConfigs.discoveredTools in database
58
- *
59
- * If tool not discovered:
60
- * - Tool won't appear in dashboard
61
- * - Protection settings can't be configured
62
- * - GET /config returns empty toolProtection.tools object
63
- *
64
- * DEBUGGING:
65
- * ----------
66
- * Enable debug logging with:
67
- * toolProtection: { debug: true }
68
- *
69
- * Debug logs show:
70
- * - Cache hits vs API fetches
71
- * - Full API responses
72
- * - Tool protection status for each tool
73
- * - Cache TTL and expiration times
74
- * - Source of config data (cache, api, or fallback)
75
- *
76
- * TROUBLESHOOTING:
77
- * ----------------
78
- * Problem: Dashboard shows protection but tool still executes
79
- * Cause: Stale cache not invalidated
80
- * Solution: POST /admin/clear-cache or wait for TTL expiration
81
- *
82
- * Problem: Empty toolProtections returned from API
83
- * Cause: Tool not discovered yet (no proof submissions)
84
- * Solution: Make at least one tool call to trigger discovery
85
- *
86
- * Problem: Updates take 5+ minutes to apply
87
- * Cause: Long cache TTL and cache clear failed
88
- * Solution: Configure MCP server URL in AgentShield for auto cache clear
89
- *
90
- * @see https://github.com/modelcontextprotocol-identity/agent-shield/docs/bouncer/tool-protection-sync.md
91
- * @package @kya-os/mcp-i-core
92
- */
93
- Object.defineProperty(exports, "__esModule", { value: true });
94
- exports.ToolProtectionService = void 0;
95
- const tool_protection_cache_js_1 = require("../cache/tool-protection-cache.js");
96
- /**
97
- * Service for fetching and checking tool protection configurations
98
- */
99
- class ToolProtectionService {
100
- config;
101
- cache;
102
- constructor(config, cache) {
103
- this.config = config;
104
- this.cache = cache;
105
- }
106
- /**
107
- * Get the project ID from the service configuration
108
- * @returns Project ID or undefined if not configured
109
- */
110
- getProjectId() {
111
- return this.config.projectId;
112
- }
113
- /**
114
- * Get stale cache entry if available and within maxStaleCacheAge
115
- * Only works with InMemoryToolProtectionCache (has internal access to expiresAt)
116
- *
117
- * NOTE: This checks stale cache BEFORE calling cache.get() to avoid deletion of expired entries
118
- *
119
- * @param cacheKey Cache key to check
120
- * @returns Stale cache entry or null if not available/too old
121
- */
122
- async getStaleCache(cacheKey) {
123
- // Only check stale cache if enabled
124
- if (this.config.allowStaleCache === false) {
125
- return null;
126
- }
127
- // Only works with InMemoryToolProtectionCache
128
- if (!(this.cache instanceof tool_protection_cache_js_1.InMemoryToolProtectionCache)) {
129
- return null;
130
- }
131
- // Use public method to get stale cache entry
132
- const staleConfig = this.cache.getStale(cacheKey);
133
- if (!staleConfig) {
134
- return null;
135
- }
136
- // Check if stale cache is within maxStaleCacheAge
137
- const expiresAt = this.cache.getExpiresAt(cacheKey);
138
- if (!expiresAt) {
139
- return null;
140
- }
141
- const now = Date.now();
142
- const maxStaleAge = this.config.maxStaleCacheAge ?? 86400000; // Default 24 hours
143
- // Check if expired but within maxStaleCacheAge
144
- if (now > expiresAt && now - expiresAt <= maxStaleAge) {
145
- if (this.config.debug) {
146
- console.error("[ToolProtectionService] Using stale cache", {
147
- cacheKey,
148
- expiredAt: new Date(expiresAt).toISOString(),
149
- staleAgeMs: now - expiresAt,
150
- maxStaleAgeMs: maxStaleAge,
151
- });
152
- }
153
- return staleConfig;
154
- }
155
- return null;
156
- }
157
- /**
158
- * Get tool protection configuration for the agent
159
- *
160
- * Flow:
161
- * 1. Check cache (project-scoped if projectId available)
162
- * 2. If cache miss, fetch from API
163
- * 3. If API fails, use fallback config
164
- * 4. Cache successful API responses
165
- *
166
- * @param agentDid DID of the agent to fetch config for
167
- */
168
- async getToolProtectionConfig(agentDid) {
169
- // Use project-scoped cache key if projectId is available (preferred)
170
- // Falls back to agent-scoped key for backward compatibility
171
- // The cache implementation (KVToolProtectionCache) will add any necessary prefix
172
- const cacheKey = this.config.projectId
173
- ? `config:tool-protections:${this.config.projectId}`
174
- : `agent:${agentDid}`;
175
- // 1. Check cache
176
- const cached = await this.cache.get(cacheKey);
177
- if (cached) {
178
- const ttl = this.config.cacheTtl ?? 300000;
179
- const cachedUntil = new Date(Date.now() + ttl).toISOString();
180
- if (this.config.debug) {
181
- console.error("[ToolProtectionService] Cache hit", {
182
- source: "cache",
183
- cacheKey,
184
- agentDid: agentDid.slice(0, 20) + "...",
185
- projectId: this.config.projectId || "none",
186
- toolCount: Object.keys(cached.toolProtections).length,
187
- protectedTools: Object.entries(cached.toolProtections)
188
- .filter(([_, config]) => config.requiresDelegation)
189
- .map(([name]) => name),
190
- cacheTtlMs: ttl,
191
- cachedUntil,
192
- });
193
- }
194
- return cached;
195
- }
196
- if (this.config.debug) {
197
- console.error("[ToolProtectionService] Cache miss, fetching from API", {
198
- source: "api-fetch-start",
199
- cacheKey,
200
- agentDid: agentDid.slice(0, 20) + "...",
201
- projectId: this.config.projectId || "none",
202
- apiUrl: this.config.apiUrl,
203
- endpoint: this.config.projectId
204
- ? `/api/v1/bouncer/projects/${this.config.projectId}/config`
205
- : `/api/v1/bouncer/config?agent_did=${agentDid}`,
206
- });
207
- }
208
- // 3. Fetch from API
209
- try {
210
- const response = await this.fetchFromApi(agentDid);
211
- if (this.config.debug) {
212
- console.error("[ToolProtectionService] API response received", {
213
- source: "api-fetch-complete",
214
- agentDid: agentDid.slice(0, 20) + "...",
215
- projectId: this.config.projectId || "none",
216
- responseKeys: Object.keys(response),
217
- dataKeys: response.data ? Object.keys(response.data) : [],
218
- rawConfig: response.data?.config || null,
219
- rawConfigToolProtection: response.data?.config?.toolProtection || null,
220
- responseMetadata: response.metadata || null,
221
- });
222
- }
223
- // Transform API response to internal format
224
- // Uses merged config endpoint: { data: { config: { toolProtection: { tools: {...} } } } }
225
- const toolProtections = this.parseToolProtectionsFromResponse(response);
226
- if (this.config.debug && Object.keys(toolProtections).length > 0) {
227
- console.error("[ToolProtectionService] Parsed tool protections from merged config", {
228
- toolCount: Object.keys(toolProtections).length,
229
- tools: Object.keys(toolProtections),
230
- });
231
- }
232
- // Merge with local config (REMOTE API takes priority over local)
233
- // Standard precedence: Default < Local < Remote
234
- // Remote (AgentShield dashboard) is the source of truth and WINS over local config
235
- const mergedToolProtections = {};
236
- // Step 1: Apply local config as base (if provided)
237
- if (this.config.localConfig?.toolProtections) {
238
- for (const [toolName, localConfig] of Object.entries(this.config.localConfig.toolProtections)) {
239
- // Skip if localConfig is empty or not a valid ToolProtection object
240
- // This prevents empty objects from corrupting the merged config
241
- if (!localConfig ||
242
- typeof localConfig !== "object" ||
243
- Object.keys(localConfig).length === 0) {
244
- if (this.config.debug) {
245
- console.error("[ToolProtectionService] Skipping empty/invalid local config entry", { tool: toolName });
246
- }
247
- continue;
248
- }
249
- // Ensure requiredScopes exists (default to empty array if missing)
250
- // Also preserve optional fields like oauthProvider, riskLevel, authorization
251
- const validConfig = {
252
- requiresDelegation: localConfig.requiresDelegation ?? false,
253
- requiredScopes: localConfig.requiredScopes ?? [],
254
- ...(localConfig.oauthProvider && {
255
- oauthProvider: localConfig.oauthProvider,
256
- }),
257
- ...(localConfig.riskLevel && {
258
- riskLevel: localConfig.riskLevel,
259
- }),
260
- ...(localConfig.authorization && {
261
- authorization: localConfig.authorization,
262
- }),
263
- };
264
- mergedToolProtections[toolName] = validConfig;
265
- if (this.config.debug) {
266
- console.error("[ToolProtectionService] Applied local config as base", {
267
- tool: toolName,
268
- localConfig: validConfig,
269
- });
270
- }
271
- }
272
- }
273
- // Step 2: Apply API config (OVERWRITES local config - remote wins)
274
- for (const [toolName, apiConfig] of Object.entries(toolProtections)) {
275
- mergedToolProtections[toolName] = apiConfig;
276
- if (this.config.debug &&
277
- this.config.localConfig?.toolProtections?.[toolName]) {
278
- console.error("[ToolProtectionService] Remote config overrides local config", {
279
- tool: toolName,
280
- remoteConfig: apiConfig,
281
- });
282
- }
283
- }
284
- const config = {
285
- toolProtections: mergedToolProtections,
286
- };
287
- // 3. Cache the response
288
- const ttl = this.config.cacheTtl ?? 300000; // Default 5 minutes
289
- const cacheExpiration = new Date(Date.now() + ttl);
290
- await this.cache.set(cacheKey, config, ttl);
291
- // Always log tool count and protection status (critical for debugging)
292
- console.error("[ToolProtectionService] Config loaded from API", {
293
- source: "api",
294
- toolCount: Object.keys(mergedToolProtections).length,
295
- protectedTools: Object.entries(mergedToolProtections)
296
- .filter(([_, config]) => config.requiresDelegation)
297
- .map(([name]) => name),
298
- agentDid: agentDid.slice(0, 20) + "...",
299
- projectId: this.config.projectId || "none",
300
- cacheTtlMs: ttl,
301
- cacheExpiresAt: cacheExpiration.toISOString(),
302
- });
303
- if (this.config.debug) {
304
- console.error("[ToolProtectionService] API fetch successful, config cached", {
305
- source: "cache-write",
306
- agentDid: agentDid.slice(0, 20) + "...",
307
- cacheKey,
308
- toolCount: Object.keys(mergedToolProtections).length,
309
- tools: Object.entries(mergedToolProtections).map(([name, config]) => ({
310
- name,
311
- requiresDelegation: config.requiresDelegation,
312
- scopeCount: (config.requiredScopes || []).length, // Safe access
313
- })),
314
- ttlMs: ttl,
315
- ttlMinutes: Math.round(ttl / 60000),
316
- expiresAt: cacheExpiration.toISOString(),
317
- expiresIn: `${Math.round(ttl / 1000)}s`,
318
- });
319
- }
320
- return config;
321
- }
322
- catch (error) {
323
- const errorMessage = error instanceof Error ? error.message : String(error);
324
- // Re-throw API key validation errors (don't fallback)
325
- if (errorMessage.includes("API key is missing or empty")) {
326
- throw error;
327
- }
328
- // Re-throw projectId required errors (security fix - don't fallback)
329
- if (errorMessage.includes("projectId is required")) {
330
- throw error;
331
- }
332
- // Re-throw HTTP errors (4xx, 5xx) - these indicate API issues, not network failures
333
- // Exception: 429 (rate limit) should fallback if offline fallback config is available
334
- if (errorMessage.includes("Failed to fetch bouncer config:")) {
335
- const status = error.status;
336
- // Allow 429 to fallback (rate limiting is temporary, fallback is acceptable)
337
- if (status === 429 && this.config.offlineFallbackConfig) {
338
- // Will fall through to fallback logic below
339
- }
340
- else {
341
- throw error;
342
- }
343
- }
344
- // Re-throw JSON parsing errors
345
- if (errorMessage.includes("Failed to parse API response:")) {
346
- throw error;
347
- }
348
- // Re-throw API success: false errors
349
- if (errorMessage.includes("API returned success: false")) {
350
- throw error;
351
- }
352
- if (this.config.debug) {
353
- console.error("[ToolProtectionService] API fetch failed", {
354
- agentDid: agentDid.slice(0, 20) + "...",
355
- error: errorMessage,
356
- });
357
- }
358
- // 4. Fallback to stale cache if available (before fallback config)
359
- const staleCache = await this.getStaleCache(cacheKey);
360
- if (staleCache) {
361
- console.warn("[ToolProtectionService] API fetch failed, using stale cache", {
362
- agentDid: agentDid.slice(0, 20) + "...",
363
- error: errorMessage,
364
- cacheKey,
365
- });
366
- return staleCache;
367
- }
368
- // 5. Fallback to offline config (only used when API is unavailable)
369
- // Note: offlineFallbackConfig is separate from localConfig
370
- // - localConfig: Merged with remote (remote wins), used always
371
- // - offlineFallbackConfig: Only used when API is completely unavailable
372
- if (this.config.offlineFallbackConfig) {
373
- // Always warn when using offline fallback (not just debug mode)
374
- console.warn("[ToolProtectionService] API fetch failed, using offline fallback config", {
375
- agentDid: agentDid.slice(0, 20) + "...",
376
- error: errorMessage,
377
- });
378
- // Cache the fallback config to avoid repeated API calls
379
- const ttl = this.config.cacheTtl ?? 300000; // Default 5 minutes
380
- await this.cache.set(cacheKey, this.config.offlineFallbackConfig, ttl);
381
- return this.config.offlineFallbackConfig;
382
- }
383
- // 6. Fail-safe behavior: deny-all by default (secure)
384
- // This is the last resort when: API fails AND no stale cache AND no offlineFallbackConfig
385
- const failSafeBehavior = this.config.failSafeBehavior ?? "deny-all";
386
- if (failSafeBehavior === "deny-all") {
387
- console.error("[ToolProtectionService] API fetch failed, no offline fallback, failing closed (deny-all)", {
388
- agentDid: agentDid.slice(0, 20) + "...",
389
- error: errorMessage,
390
- cacheKey,
391
- });
392
- // Create deny-all config (wildcard requires delegation for all tools)
393
- const denyAllConfig = {
394
- toolProtections: {
395
- "*": {
396
- requiresDelegation: true,
397
- requiredScopes: [],
398
- },
399
- },
400
- };
401
- // Cache deny-all config to avoid repeated API calls
402
- const ttl = this.config.cacheTtl ?? 300000; // Default 5 minutes
403
- await this.cache.set(cacheKey, denyAllConfig, ttl);
404
- return denyAllConfig;
405
- }
406
- else {
407
- // failSafeBehavior === 'allow-all' (insecure, not recommended)
408
- console.warn("[ToolProtectionService] API fetch failed, no offline fallback, failing open (allow-all)", {
409
- agentDid: agentDid.slice(0, 20) + "...",
410
- error: errorMessage,
411
- cacheKey,
412
- });
413
- // Cache empty config to avoid repeated API calls
414
- const emptyConfig = { toolProtections: {} };
415
- const ttl = this.config.cacheTtl ?? 300000; // Default 5 minutes
416
- await this.cache.set(cacheKey, emptyConfig, ttl);
417
- return emptyConfig;
418
- }
419
- }
420
- }
421
- /**
422
- * Check if a tool requires delegation
423
- *
424
- * @param toolName Name of the tool to check
425
- * @param agentDid DID of the agent executing the tool
426
- * @returns Tool protection config or null if no protection required
427
- */
428
- async checkToolProtection(toolName, agentDid) {
429
- const config = await this.getToolProtectionConfig(agentDid);
430
- // Check for specific tool protection first
431
- let protection = config.toolProtections[toolName];
432
- // If not found, check for wildcard protection (fail-safe deny-all)
433
- if (!protection && config.toolProtections["*"]) {
434
- protection = config.toolProtections["*"];
435
- }
436
- // Always log the check result (critical for debugging)
437
- if (this.config.debug || !protection || protection.requiresDelegation) {
438
- console.error("[ToolProtectionService] Protection check", {
439
- tool: toolName,
440
- agentDid: agentDid.slice(0, 20) + "...",
441
- found: !!protection,
442
- isWildcard: protection === config.toolProtections["*"],
443
- requiresDelegation: protection?.requiresDelegation ?? false,
444
- availableTools: Object.keys(config.toolProtections),
445
- });
446
- }
447
- if (!protection || !protection.requiresDelegation) {
448
- return null;
449
- }
450
- return protection;
451
- }
452
- /**
453
- * Fetch tool protection config from AgentShield API
454
- *
455
- * Uses the merged /config endpoint which returns tool protections embedded
456
- * at config.toolProtection.tools. Falls back to legacy formats for backward
457
- * compatibility.
458
- *
459
- * @param agentDid DID of the agent to fetch config for
460
- * @param options Optional fetch options
461
- * @param options.bypassCDNCache When true, adds cache-busting to bypass CDN caches (used by clearAndRefresh)
462
- */
463
- async fetchFromApi(agentDid, options) {
464
- // Use the merged /config endpoint which includes embedded tool protections
465
- // This endpoint returns config.toolProtection.tools with all tool rules
466
- let url;
467
- let useMergedEndpoint = false;
468
- // ❌ REMOVED: Legacy agent-scoped endpoint fallback
469
- // Agent-only scoping causes cross-user delegation leakage (Priority 3 fallback issue)
470
- // projectId is now REQUIRED for proper user isolation
471
- if (!this.config.projectId) {
472
- const error = new Error("[ToolProtectionService] projectId is required for user-scoped delegation. " +
473
- "Agent-only scoping has been deprecated due to security concerns (cross-user delegation leakage). " +
474
- "Please configure AGENTSHIELD_PROJECT_ID environment variable.");
475
- console.error("[ToolProtectionService]", error.message);
476
- throw error;
477
- }
478
- // ✅ PROJECT-SCOPED ENDPOINT: Returns config with embedded toolProtection.tools
479
- // This endpoint ensures delegations are properly scoped to user+agent
480
- url = `${this.config.apiUrl}/api/v1/bouncer/projects/${encodeURIComponent(this.config.projectId)}/config`;
481
- useMergedEndpoint = true;
482
- // Add cache-busting query param when bypassing CDN cache
483
- // This is used during cache invalidation (clearAndRefresh) to ensure we get fresh data
484
- // from the origin server, not stale CDN-cached data
485
- if (options?.bypassCDNCache) {
486
- const separator = url.includes("?") ? "&" : "?";
487
- url = `${url}${separator}_t=${Date.now()}`;
488
- }
489
- // Debug: Log API key status (masked for security)
490
- // Format: sk_live... (prefix up to first underscore after type, then ...)
491
- const apiKeyMasked = this.config.apiKey
492
- ? (() => {
493
- const parts = this.config.apiKey.split("_");
494
- if (parts.length >= 2) {
495
- return `${parts[0]}_${parts[1]}...`;
496
- }
497
- return `${this.config.apiKey.substring(0, 8)}...`;
498
- })()
499
- : "(empty)";
500
- const apiKeyLength = this.config.apiKey?.length || 0;
501
- if (this.config.debug) {
502
- console.error("[ToolProtectionService] Fetching from API:", url, {
503
- method: useMergedEndpoint
504
- ? "projects/{projectId}/config (merged)"
505
- : "config?agent_did (legacy)",
506
- projectId: this.config.projectId || "none",
507
- apiKeyPresent: !!this.config.apiKey,
508
- apiKeyLength,
509
- apiKeyMasked,
510
- });
511
- }
512
- // Validate API key is present
513
- if (!this.config.apiKey || this.config.apiKey.trim() === "") {
514
- const error = "API key is missing or empty";
515
- if (this.config.debug) {
516
- console.error("[ToolProtectionService]", error, {
517
- apiKeyPresent: !!this.config.apiKey,
518
- apiKeyLength,
519
- });
520
- }
521
- throw new Error(`ToolProtectionService: ${error}. Check AGENTSHIELD_API_KEY in .dev.vars or wrangler.toml [vars]`);
522
- }
523
- // Build headers - merged endpoint uses X-API-Key, legacy uses Authorization Bearer
524
- const headers = {
525
- "Content-Type": "application/json",
526
- };
527
- if (useMergedEndpoint) {
528
- // ✅ Merged config endpoint headers
529
- headers["X-API-Key"] = this.config.apiKey;
530
- if (this.config.projectId) {
531
- headers["X-Project-Id"] = this.config.projectId;
532
- }
533
- }
534
- else {
535
- // ⚠️ Legacy endpoint headers (backward compatibility)
536
- headers["Authorization"] = `Bearer ${this.config.apiKey}`;
537
- }
538
- // Add cache-control header when bypassing CDN cache
539
- // This tells any intermediate caches (CDN, proxies) not to serve cached content
540
- if (options?.bypassCDNCache) {
541
- headers["Cache-Control"] = "no-cache, no-store, must-revalidate";
542
- headers["Pragma"] = "no-cache"; // HTTP/1.0 backward compatibility
543
- }
544
- const response = await fetch(url, {
545
- method: "GET",
546
- headers,
547
- });
548
- if (!response.ok) {
549
- const errorText = await response.text().catch(() => "Unknown error");
550
- const error = new Error(`Failed to fetch bouncer config: ${response.status} ${response.statusText} - ${errorText}`);
551
- error.status = response.status;
552
- throw error;
553
- }
554
- let data;
555
- try {
556
- data = (await response.json());
557
- }
558
- catch (jsonError) {
559
- throw new Error(`Failed to parse API response: ${jsonError instanceof Error ? jsonError.message : String(jsonError)}`);
560
- }
561
- if (!data.success) {
562
- throw new Error("API returned success: false");
563
- }
564
- return data;
565
- }
566
- /**
567
- * Parse tool protections from API response
568
- *
569
- * Extracts tool protection configs from merged config format:
570
- * { data: { config: { toolProtection: { tools: {...} } } } }
571
- *
572
- * @param response API response from AgentShield
573
- * @returns Record of tool name to ToolProtection
574
- */
575
- parseToolProtectionsFromResponse(response) {
576
- const toolProtections = {};
577
- // Extract from merged config format: data.config.toolProtection.tools
578
- const tools = response.data.config?.toolProtection?.tools;
579
- if (!tools) {
580
- return toolProtections;
581
- }
582
- for (const [toolName, toolConfig] of Object.entries(tools)) {
583
- // Support both camelCase and snake_case for backward compatibility with DB data
584
- const requiresDelegation = toolConfig.requiresDelegation ??
585
- toolConfig.requires_delegation ??
586
- false;
587
- const requiredScopes = toolConfig.requiredScopes ??
588
- toolConfig.required_scopes ??
589
- toolConfig.scopes ??
590
- [];
591
- const oauthProvider = toolConfig.oauthProvider ?? toolConfig.oauth_provider ?? undefined;
592
- const rawRiskLevel = toolConfig.riskLevel ?? toolConfig.risk_level ?? undefined;
593
- // Type-safe cast for riskLevel enum
594
- const riskLevel = rawRiskLevel &&
595
- ["low", "medium", "high", "critical"].includes(rawRiskLevel)
596
- ? rawRiskLevel
597
- : undefined;
598
- // Cast authorization to proper discriminated union type
599
- // The API returns the correct shape, we just need to help TypeScript
600
- const authorization = toolConfig.authorization;
601
- toolProtections[toolName] = {
602
- requiresDelegation,
603
- requiredScopes,
604
- ...(oauthProvider && { oauthProvider }),
605
- ...(riskLevel && { riskLevel }),
606
- ...(authorization && { authorization }),
607
- };
608
- }
609
- return toolProtections;
610
- }
611
- /**
612
- * Clear the cache for a project or agent (useful for testing or manual refresh)
613
- *
614
- * If projectId is configured, clears project-scoped cache.
615
- * Otherwise, clears agent-scoped cache.
616
- *
617
- * @param agentDid DID of the agent (used for fallback if projectId not available)
618
- */
619
- async clearCache(agentDid) {
620
- // Use same cache key strategy as getToolProtectionConfig
621
- const cacheKey = this.config.projectId
622
- ? `config:tool-protections:${this.config.projectId}`
623
- : `agent:${agentDid}`;
624
- if (this.config.debug) {
625
- console.error("[ToolProtectionService] Clearing cache", {
626
- cacheKey,
627
- projectId: this.config.projectId || "none",
628
- agentDid: agentDid.slice(0, 20) + "...",
629
- });
630
- }
631
- await this.cache.delete(cacheKey);
632
- }
633
- /**
634
- * Clear cache and immediately fetch fresh config from API
635
- *
636
- * This method is designed for Cloudflare Workers where KV has edge caching.
637
- * After clearing the KV entry, it fetches fresh data from the API and writes
638
- * it back to KV. This ensures:
639
- * 1. The global KV entry is deleted
640
- * 2. Fresh data is fetched from API (with CDN cache bypass!)
641
- * 3. New data is written to KV (updating edge cache)
642
- *
643
- * The next request from the same edge location will get the fresh data.
644
- *
645
- * IMPORTANT: This method uses bypassCDNCache to ensure we get fresh data
646
- * from AgentShield's origin server, not stale CDN-cached data. This is
647
- * critical for instant cache invalidation when tool protection settings
648
- * are changed in the AgentShield dashboard.
649
- *
650
- * @param agentDid DID of the agent (used for cache key)
651
- * @returns The fresh tool protection config from API
652
- */
653
- async clearAndRefresh(agentDid) {
654
- const cacheKey = this.config.projectId
655
- ? `config:tool-protections:${this.config.projectId}`
656
- : `agent:${agentDid}`;
657
- console.error("[ToolProtectionService] clearAndRefresh starting", {
658
- cacheKey,
659
- projectId: this.config.projectId || "none",
660
- agentDid: agentDid.slice(0, 20) + "...",
661
- });
662
- // 1. Delete the cache entry
663
- await this.cache.delete(cacheKey);
664
- console.error("[ToolProtectionService] Cache entry deleted", { cacheKey });
665
- // 2. Fetch fresh config from API with CDN cache bypass
666
- // This ensures we get fresh data from origin, not stale CDN data
667
- try {
668
- const response = await this.fetchFromApi(agentDid, {
669
- bypassCDNCache: true,
670
- });
671
- // Use shared parsing logic for consistency
672
- const toolProtections = this.parseToolProtectionsFromResponse(response);
673
- // Apply same merge logic as getToolProtectionConfig:
674
- // Step 1: Apply localConfig as base (if provided)
675
- // Step 2: Apply API config (OVERWRITES local config - remote wins)
676
- const mergedToolProtections = {};
677
- // Step 1: Apply local config as base
678
- if (this.config.localConfig?.toolProtections) {
679
- for (const [toolName, localConfig] of Object.entries(this.config.localConfig.toolProtections)) {
680
- if (localConfig &&
681
- typeof localConfig === "object" &&
682
- Object.keys(localConfig).length > 0) {
683
- const validConfig = {
684
- requiresDelegation: localConfig.requiresDelegation ?? false,
685
- requiredScopes: localConfig.requiredScopes ?? [],
686
- ...(localConfig.oauthProvider && {
687
- oauthProvider: localConfig.oauthProvider,
688
- }),
689
- ...(localConfig.riskLevel && {
690
- riskLevel: localConfig.riskLevel,
691
- }),
692
- ...(localConfig.authorization && {
693
- authorization: localConfig.authorization,
694
- }),
695
- };
696
- mergedToolProtections[toolName] = validConfig;
697
- }
698
- }
699
- }
700
- // Step 2: Apply API config (OVERWRITES local config - remote wins)
701
- for (const [toolName, apiConfig] of Object.entries(toolProtections)) {
702
- mergedToolProtections[toolName] = apiConfig;
703
- }
704
- // Construct fresh config (ToolProtectionConfig type)
705
- const freshConfig = {
706
- toolProtections: mergedToolProtections,
707
- };
708
- // 3. Write fresh config to cache
709
- const ttl = this.config.cacheTtl ?? 300000;
710
- await this.cache.set(cacheKey, freshConfig, ttl);
711
- console.error("[ToolProtectionService] Fresh config fetched and cached", {
712
- cacheKey,
713
- toolCount: Object.keys(mergedToolProtections).length,
714
- protectedTools: Object.entries(mergedToolProtections)
715
- .filter(([_, cfg]) => cfg.requiresDelegation)
716
- .map(([name]) => name),
717
- source: "api",
718
- });
719
- return { config: freshConfig, cacheKey, source: "api" };
720
- }
721
- catch (error) {
722
- console.warn("[ToolProtectionService] API fetch failed during refresh, using offline fallback", {
723
- error: error instanceof Error ? error.message : String(error),
724
- cacheKey,
725
- });
726
- // Use offline fallback config if API fails
727
- const offlineFallback = this.config
728
- .offlineFallbackConfig || {
729
- toolProtections: {},
730
- };
731
- return { config: offlineFallback, cacheKey, source: "fallback" };
732
- }
733
- }
734
- }
735
- exports.ToolProtectionService = ToolProtectionService;
736
- //# sourceMappingURL=tool-protection.service.js.map