@kya-os/mcp-i-core 1.3.10-canary.clientinfo.20251126124133 → 1.3.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.
Files changed (90) hide show
  1. package/.claude/settings.local.json +9 -0
  2. package/.turbo/turbo-build.log +1 -1
  3. package/.turbo/turbo-test$colon$coverage.log +3419 -3072
  4. package/.turbo/turbo-test.log +1805 -1680
  5. package/coverage/coverage-final.json +59 -56
  6. package/dist/config/remote-config.d.ts +51 -0
  7. package/dist/config/remote-config.d.ts.map +1 -1
  8. package/dist/config/remote-config.js +74 -0
  9. package/dist/config/remote-config.js.map +1 -1
  10. package/dist/config.d.ts +1 -1
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/config.js +4 -1
  13. package/dist/config.js.map +1 -1
  14. package/dist/delegation/did-key-resolver.d.ts +64 -0
  15. package/dist/delegation/did-key-resolver.d.ts.map +1 -0
  16. package/dist/delegation/did-key-resolver.js +159 -0
  17. package/dist/delegation/did-key-resolver.js.map +1 -0
  18. package/dist/delegation/utils.d.ts +76 -0
  19. package/dist/delegation/utils.d.ts.map +1 -1
  20. package/dist/delegation/utils.js +117 -0
  21. package/dist/delegation/utils.js.map +1 -1
  22. package/dist/identity/idp-token-resolver.d.ts +17 -1
  23. package/dist/identity/idp-token-resolver.d.ts.map +1 -1
  24. package/dist/identity/idp-token-resolver.js +34 -6
  25. package/dist/identity/idp-token-resolver.js.map +1 -1
  26. package/dist/identity/idp-token-storage.interface.d.ts +38 -7
  27. package/dist/identity/idp-token-storage.interface.d.ts.map +1 -1
  28. package/dist/identity/idp-token-storage.interface.js +2 -0
  29. package/dist/identity/idp-token-storage.interface.js.map +1 -1
  30. package/dist/identity/user-did-manager.d.ts +95 -12
  31. package/dist/identity/user-did-manager.d.ts.map +1 -1
  32. package/dist/identity/user-did-manager.js +107 -25
  33. package/dist/identity/user-did-manager.js.map +1 -1
  34. package/dist/index.d.ts +6 -3
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +24 -2
  37. package/dist/index.js.map +1 -1
  38. package/dist/runtime/base.d.ts +25 -8
  39. package/dist/runtime/base.d.ts.map +1 -1
  40. package/dist/runtime/base.js +74 -21
  41. package/dist/runtime/base.js.map +1 -1
  42. package/dist/services/session-registration.service.d.ts.map +1 -1
  43. package/dist/services/session-registration.service.js +10 -90
  44. package/dist/services/session-registration.service.js.map +1 -1
  45. package/dist/services/tool-context-builder.d.ts +18 -1
  46. package/dist/services/tool-context-builder.d.ts.map +1 -1
  47. package/dist/services/tool-context-builder.js +63 -10
  48. package/dist/services/tool-context-builder.js.map +1 -1
  49. package/dist/services/tool-protection.service.d.ts +6 -3
  50. package/dist/services/tool-protection.service.d.ts.map +1 -1
  51. package/dist/services/tool-protection.service.js +89 -34
  52. package/dist/services/tool-protection.service.js.map +1 -1
  53. package/dist/utils/base58.d.ts +31 -0
  54. package/dist/utils/base58.d.ts.map +1 -0
  55. package/dist/utils/base58.js +103 -0
  56. package/dist/utils/base58.js.map +1 -0
  57. package/dist/utils/did-helpers.d.ts +33 -0
  58. package/dist/utils/did-helpers.d.ts.map +1 -1
  59. package/dist/utils/did-helpers.js +53 -0
  60. package/dist/utils/did-helpers.js.map +1 -1
  61. package/package.json +3 -3
  62. package/src/__tests__/identity/user-did-manager.test.ts +64 -45
  63. package/src/__tests__/integration/full-flow.test.ts +23 -10
  64. package/src/__tests__/runtime/base-extensions.test.ts +23 -21
  65. package/src/__tests__/runtime/proof-client-did.test.ts +19 -18
  66. package/src/__tests__/services/agentshield-integration.test.ts +10 -3
  67. package/src/__tests__/services/tool-protection-merged-config.test.ts +485 -0
  68. package/src/__tests__/services/tool-protection.service.test.ts +18 -11
  69. package/src/config/__tests__/merged-config.spec.ts +445 -0
  70. package/src/config/remote-config.ts +90 -0
  71. package/src/config.ts +3 -0
  72. package/src/delegation/__tests__/did-key-resolver.test.ts +265 -0
  73. package/src/delegation/__tests__/vc-issuer.test.ts +1 -1
  74. package/src/delegation/did-key-resolver.ts +179 -0
  75. package/src/delegation/utils.ts +179 -0
  76. package/src/identity/idp-token-resolver.ts +41 -7
  77. package/src/identity/idp-token-storage.interface.ts +42 -7
  78. package/src/identity/user-did-manager.ts +185 -29
  79. package/src/index.ts +42 -3
  80. package/src/runtime/base.ts +84 -21
  81. package/src/services/session-registration.service.ts +26 -121
  82. package/src/services/tool-context-builder.ts +75 -10
  83. package/src/services/tool-protection.service.ts +176 -88
  84. package/src/utils/__tests__/did-helpers.test.ts +55 -0
  85. package/src/utils/base58.ts +109 -0
  86. package/src/utils/did-helpers.ts +60 -0
  87. package/dist/__tests__/utils/mock-providers.d.ts +0 -103
  88. package/dist/__tests__/utils/mock-providers.d.ts.map +0 -1
  89. package/dist/__tests__/utils/mock-providers.js +0 -293
  90. package/dist/__tests__/utils/mock-providers.js.map +0 -1
@@ -46,7 +46,7 @@
46
46
  * If tool not discovered:
47
47
  * - Tool won't appear in dashboard
48
48
  * - Protection settings can't be configured
49
- * - GET /tool-protections returns empty object
49
+ * - GET /config returns empty toolProtection.tools object
50
50
  *
51
51
  * DEBUGGING:
52
52
  * ----------
@@ -87,34 +87,56 @@ import type {
87
87
  import type { ToolProtectionCache } from "../cache/tool-protection-cache.js";
88
88
  import { InMemoryToolProtectionCache } from "../cache/tool-protection-cache.js";
89
89
 
90
+ /**
91
+ * Tool protection data structure in API responses
92
+ */
93
+ interface ToolProtectionData {
94
+ requiresDelegation?: boolean;
95
+ requires_delegation?: boolean;
96
+ requiredScopes?: string[];
97
+ required_scopes?: string[];
98
+ scopes?: string[];
99
+ riskLevel?: string;
100
+ risk_level?: string;
101
+ oauthProvider?: string;
102
+ oauth_provider?: string;
103
+ authorization?: {
104
+ type: string;
105
+ provider?: string;
106
+ issuer?: string;
107
+ credentialType?: string;
108
+ };
109
+ }
110
+
90
111
  /**
91
112
  * Response from AgentShield API bouncer endpoints
92
113
  *
93
114
  * Supports multiple endpoint formats:
94
- * 1. New endpoint (/projects/{projectId}/tool-protections): { data: { toolProtections: { [toolName]: {...} } } } }
95
- * 2. Old endpoint (/config?agent_did=...): { data: { tools: [{ name: string, ... }] } }
96
- * 3. Legacy format: { data: { tools: { [toolName]: {...} } } }
115
+ * 1. Merged config (/projects/{projectId}/config): { data: { config: { toolProtection: { tools: {...} } } } }
116
+ * 2. Legacy tool-protections endpoint: { data: { toolProtections: { [toolName]: {...} } } }
117
+ * 3. Old config endpoint (/config?agent_did=...): { data: { tools: [{ name: string, ... }] } }
118
+ * 4. Legacy format: { data: { tools: { [toolName]: {...} } } }
97
119
  */
98
120
  interface BouncerConfigApiResponse {
99
121
  success: boolean;
100
122
  data: {
101
123
  agent_did?: string;
102
- // New endpoint format: toolProtections object
103
- toolProtections?: Record<
104
- string,
105
- {
106
- requiresDelegation?: boolean;
107
- requires_delegation?: boolean;
108
- requiredScopes?: string[];
109
- required_scopes?: string[];
110
- scopes?: string[];
111
- riskLevel?: string;
112
- risk_level?: string;
113
- oauthProvider?: string; // Phase 2: Tool-specific OAuth provider
114
- oauth_provider?: string; // Phase 2: snake_case variant
115
- }
116
- >;
117
- // Old endpoint format: tools array or object
124
+
125
+ // NEW: Merged config format (v1.6.0+) - preferred format
126
+ // The entire config is returned with tools embedded at config.toolProtection.tools
127
+ config?: {
128
+ toolProtection?: {
129
+ source?: string;
130
+ tools?: Record<string, ToolProtectionData>;
131
+ };
132
+ // Other config fields we don't need to parse
133
+ [key: string]: unknown;
134
+ };
135
+
136
+ // DEPRECATED: Top-level toolProtections (backward compatibility during transition)
137
+ toolProtections?: Record<string, ToolProtectionData>;
138
+
139
+ // Legacy endpoint formats
118
140
  tools?:
119
141
  | Array<{
120
142
  name: string;
@@ -122,20 +144,10 @@ interface BouncerConfigApiResponse {
122
144
  requires_delegation?: boolean;
123
145
  scopes?: string[];
124
146
  required_scopes?: string[];
125
- oauthProvider?: string; // Phase 2: Tool-specific OAuth provider
126
- oauth_provider?: string; // Phase 2: snake_case variant
147
+ oauthProvider?: string;
148
+ oauth_provider?: string;
127
149
  }>
128
- | Record<
129
- string,
130
- {
131
- requiresDelegation?: boolean;
132
- requires_delegation?: boolean;
133
- scopes?: string[];
134
- required_scopes?: string[];
135
- oauthProvider?: string; // Phase 2: Tool-specific OAuth provider
136
- oauth_provider?: string; // Phase 2: snake_case variant
137
- }
138
- >;
150
+ | Record<string, ToolProtectionData>;
139
151
  reputation_threshold?: number;
140
152
  denied_agents?: string[];
141
153
  };
@@ -254,7 +266,10 @@ export class ToolProtectionService {
254
266
  projectId: this.config.projectId || "none",
255
267
  toolCount: Object.keys(cached.toolProtections).length,
256
268
  protectedTools: Object.entries(cached.toolProtections)
257
- .filter(([_, config]: [string, ToolProtection]) => config.requiresDelegation)
269
+ .filter(
270
+ ([_, config]: [string, ToolProtection]) =>
271
+ config.requiresDelegation
272
+ )
258
273
  .map(([name]) => name),
259
274
  cacheTtlMs: ttl,
260
275
  cachedUntil,
@@ -271,7 +286,7 @@ export class ToolProtectionService {
271
286
  projectId: this.config.projectId || "none",
272
287
  apiUrl: this.config.apiUrl,
273
288
  endpoint: this.config.projectId
274
- ? `/api/v1/bouncer/projects/${this.config.projectId}/tool-protections`
289
+ ? `/api/v1/bouncer/projects/${this.config.projectId}/config`
275
290
  : `/api/v1/bouncer/config?agent_did=${agentDid}`,
276
291
  });
277
292
  }
@@ -287,6 +302,8 @@ export class ToolProtectionService {
287
302
  projectId: this.config.projectId || "none",
288
303
  responseKeys: Object.keys(response),
289
304
  dataKeys: response.data ? Object.keys(response.data) : [],
305
+ rawConfig: response.data?.config || null,
306
+ rawConfigToolProtection: response.data?.config?.toolProtection || null,
290
307
  rawToolProtections: response.data?.toolProtections || null,
291
308
  rawTools: response.data?.tools || null,
292
309
  responseMetadata: response.metadata || null,
@@ -294,15 +311,54 @@ export class ToolProtectionService {
294
311
  }
295
312
 
296
313
  // Transform API response format to internal format
297
- // Supports multiple response formats:
298
- // 1. New endpoint: { data: { toolProtections: { greet: { requiresDelegation: true, ... } } } }
299
- // 2. Old endpoint (array): { data: { tools: [{ name: "greet", requiresDelegation: true, ... }] } }
300
- // 3. Old endpoint (object): { data: { tools: { greet: { requiresDelegation: true, ... } } } }
314
+ // Supports multiple response formats (in priority order):
315
+ // 1. Merged config endpoint: { data: { config: { toolProtection: { tools: {...} } } } }
316
+ // 2. Legacy toolProtections: { data: { toolProtections: { greet: { requiresDelegation: true, ... } } } }
317
+ // 3. Old endpoint (array): { data: { tools: [{ name: "greet", requiresDelegation: true, ... }] } }
318
+ // 4. Old endpoint (object): { data: { tools: { greet: { requiresDelegation: true, ... } } } }
301
319
  const toolProtections: Record<string, ToolProtection> = {};
302
320
 
303
- // Check for new endpoint format first (toolProtections)
304
- if (response.data.toolProtections) {
305
- // New endpoint format: object with tool names as keys
321
+ // Check for merged config format first (data.config.toolProtection.tools)
322
+ if (response.data.config?.toolProtection?.tools) {
323
+ // Merged config endpoint format: object with tool names as keys
324
+ if (this.config.debug) {
325
+ console.log("[ToolProtectionService] Using merged config format (data.config.toolProtection.tools)");
326
+ }
327
+ for (const [toolName, toolConfig] of Object.entries(
328
+ response.data.config.toolProtection.tools
329
+ )) {
330
+ const requiresDelegation =
331
+ (toolConfig as any).requiresDelegation ??
332
+ (toolConfig as any).requires_delegation ??
333
+ false;
334
+ const requiredScopes =
335
+ (toolConfig as any).requiredScopes ??
336
+ (toolConfig as any).required_scopes ??
337
+ (toolConfig as any).scopes ??
338
+ [];
339
+
340
+ const oauthProvider =
341
+ (toolConfig as any).oauthProvider ??
342
+ (toolConfig as any).oauth_provider ??
343
+ undefined;
344
+
345
+ const riskLevel =
346
+ (toolConfig as any).riskLevel ??
347
+ (toolConfig as any).risk_level ??
348
+ undefined;
349
+
350
+ toolProtections[toolName] = {
351
+ requiresDelegation,
352
+ requiredScopes,
353
+ ...(oauthProvider && { oauthProvider }),
354
+ ...(riskLevel && { riskLevel }),
355
+ };
356
+ }
357
+ } else if (response.data.toolProtections) {
358
+ // Legacy toolProtections format: object with tool names as keys
359
+ if (this.config.debug) {
360
+ console.log("[ToolProtectionService] Using legacy toolProtections format (data.toolProtections)");
361
+ }
306
362
  // Prefer camelCase over snake_case when both present
307
363
  for (const [toolName, toolConfig] of Object.entries(
308
364
  response.data.toolProtections
@@ -316,13 +372,13 @@ export class ToolProtectionService {
316
372
  (toolConfig as any).required_scopes ??
317
373
  (toolConfig as any).scopes ??
318
374
  [];
319
-
375
+
320
376
  // NEW: Parse oauthProvider (camelCase and snake_case support)
321
377
  const oauthProvider =
322
378
  (toolConfig as any).oauthProvider ??
323
379
  (toolConfig as any).oauth_provider ??
324
380
  undefined;
325
-
381
+
326
382
  const riskLevel =
327
383
  (toolConfig as any).riskLevel ??
328
384
  (toolConfig as any).risk_level ??
@@ -361,17 +417,15 @@ export class ToolProtectionService {
361
417
  (tool as any).required_scopes ??
362
418
  (tool as any).scopes ??
363
419
  [];
364
-
420
+
365
421
  // NEW: Parse oauthProvider
366
422
  const oauthProvider =
367
423
  (tool as any).oauthProvider ??
368
424
  (tool as any).oauth_provider ??
369
425
  undefined;
370
-
426
+
371
427
  const riskLevel =
372
- (tool as any).riskLevel ??
373
- (tool as any).risk_level ??
374
- undefined;
428
+ (tool as any).riskLevel ?? (tool as any).risk_level ?? undefined;
375
429
 
376
430
  toolProtections[toolName] = {
377
431
  requiresDelegation,
@@ -395,13 +449,13 @@ export class ToolProtectionService {
395
449
  (toolConfig as any).required_scopes ??
396
450
  (toolConfig as any).scopes ??
397
451
  [];
398
-
452
+
399
453
  // NEW: Parse oauthProvider
400
454
  const oauthProvider =
401
455
  (toolConfig as any).oauthProvider ??
402
456
  (toolConfig as any).oauth_provider ??
403
457
  undefined;
404
-
458
+
405
459
  const riskLevel =
406
460
  (toolConfig as any).riskLevel ??
407
461
  (toolConfig as any).risk_level ??
@@ -427,7 +481,11 @@ export class ToolProtectionService {
427
481
  )) {
428
482
  // Skip if localConfig is empty or not a valid ToolProtection object
429
483
  // This prevents empty objects from corrupting the merged config
430
- if (!localConfig || typeof localConfig !== 'object' || Object.keys(localConfig).length === 0) {
484
+ if (
485
+ !localConfig ||
486
+ typeof localConfig !== "object" ||
487
+ Object.keys(localConfig).length === 0
488
+ ) {
431
489
  if (this.config.debug) {
432
490
  console.log(
433
491
  "[ToolProtectionService] Skipping empty/invalid fallback config entry",
@@ -436,13 +494,14 @@ export class ToolProtectionService {
436
494
  }
437
495
  continue;
438
496
  }
439
-
497
+
440
498
  // Ensure requiredScopes exists (default to empty array if missing)
441
499
  const validConfig: ToolProtection = {
442
- requiresDelegation: (localConfig as any).requiresDelegation ?? false,
500
+ requiresDelegation:
501
+ (localConfig as any).requiresDelegation ?? false,
443
502
  requiredScopes: (localConfig as any).requiredScopes ?? [],
444
503
  };
445
-
504
+
446
505
  // Local config overrides API config for this tool
447
506
  mergedToolProtections[toolName] = validConfig;
448
507
  if (this.config.debug) {
@@ -471,8 +530,10 @@ export class ToolProtectionService {
471
530
  console.log("[ToolProtectionService] Config loaded from API", {
472
531
  source: "api",
473
532
  toolCount: Object.keys(mergedToolProtections).length,
474
- protectedTools: Object.entries(mergedToolProtections)
475
- .filter(([_, config]: [string, ToolProtection]) => config.requiresDelegation)
533
+ protectedTools: Object.entries(mergedToolProtections)
534
+ .filter(
535
+ ([_, config]: [string, ToolProtection]) => config.requiresDelegation
536
+ )
476
537
  .map(([name]) => name),
477
538
  agentDid: agentDid.slice(0, 20) + "...",
478
539
  projectId: this.config.projectId || "none",
@@ -665,7 +726,10 @@ export class ToolProtectionService {
665
726
 
666
727
  /**
667
728
  * Fetch tool protection config from AgentShield API
668
- * Uses projectId endpoint if available (preferred, project-scoped), otherwise falls back to agent_did query param
729
+ *
730
+ * Uses the merged /config endpoint which returns tool protections embedded
731
+ * at config.toolProtection.tools. Falls back to legacy formats for backward
732
+ * compatibility.
669
733
  *
670
734
  * @param agentDid DID of the agent to fetch config for
671
735
  * @param options Optional fetch options
@@ -675,17 +739,17 @@ export class ToolProtectionService {
675
739
  agentDid: string,
676
740
  options?: { bypassCDNCache?: boolean }
677
741
  ): Promise<BouncerConfigApiResponse> {
678
- // Prefer new project-scoped endpoint: /api/v1/bouncer/projects/{projectId}/tool-protections
679
- // Falls back to old endpoint: /api/v1/bouncer/config?agent_did={did} for backward compatibility
742
+ // Use the merged /config endpoint which includes embedded tool protections
743
+ // This endpoint returns config.toolProtection.tools with all tool rules
680
744
  let url: string;
681
- let useNewEndpoint = false;
745
+ let useMergedEndpoint = false;
682
746
 
683
747
  if (this.config.projectId) {
684
- // ✅ NEW ENDPOINT: Project-scoped, returns toolProtections object
685
- url = `${this.config.apiUrl}/api/v1/bouncer/projects/${encodeURIComponent(this.config.projectId)}/tool-protections`;
686
- useNewEndpoint = true;
748
+ // ✅ MERGED CONFIG ENDPOINT: Returns config with embedded toolProtection.tools
749
+ url = `${this.config.apiUrl}/api/v1/bouncer/projects/${encodeURIComponent(this.config.projectId)}/config`;
750
+ useMergedEndpoint = true;
687
751
  } else {
688
- // ⚠️ OLD ENDPOINT: Agent-scoped, returns tools array (backward compatibility)
752
+ // ⚠️ LEGACY ENDPOINT: Agent-scoped, returns tools array (backward compatibility)
689
753
  url = `${this.config.apiUrl}/api/v1/bouncer/config?agent_did=${encodeURIComponent(agentDid)}`;
690
754
  }
691
755
 
@@ -712,9 +776,9 @@ export class ToolProtectionService {
712
776
 
713
777
  if (this.config.debug) {
714
778
  console.log("[ToolProtectionService] Fetching from API:", url, {
715
- method: useNewEndpoint
716
- ? "projects/{projectId}/tool-protections (new)"
717
- : "config?agent_did (old)",
779
+ method: useMergedEndpoint
780
+ ? "projects/{projectId}/config (merged)"
781
+ : "config?agent_did (legacy)",
718
782
  projectId: this.config.projectId || "none",
719
783
  apiKeyPresent: !!this.config.apiKey,
720
784
  apiKeyLength,
@@ -736,19 +800,19 @@ export class ToolProtectionService {
736
800
  );
737
801
  }
738
802
 
739
- // Build headers - new endpoint uses X-API-Key, old endpoint uses Authorization Bearer
803
+ // Build headers - merged endpoint uses X-API-Key, legacy uses Authorization Bearer
740
804
  const headers: Record<string, string> = {
741
805
  "Content-Type": "application/json",
742
806
  };
743
807
 
744
- if (useNewEndpoint) {
745
- // ✅ New endpoint headers
808
+ if (useMergedEndpoint) {
809
+ // ✅ Merged config endpoint headers
746
810
  headers["X-API-Key"] = this.config.apiKey;
747
811
  if (this.config.projectId) {
748
812
  headers["X-Project-Id"] = this.config.projectId;
749
813
  }
750
814
  } else {
751
- // ⚠️ Old endpoint headers (backward compatibility)
815
+ // ⚠️ Legacy endpoint headers (backward compatibility)
752
816
  headers["Authorization"] = `Bearer ${this.config.apiKey}`;
753
817
  }
754
818
 
@@ -786,6 +850,19 @@ export class ToolProtectionService {
786
850
  throw new Error("API returned success: false");
787
851
  }
788
852
 
853
+ // Transform merged config format to normalized format
854
+ // If response contains config.toolProtection.tools, extract them to data.toolProtections
855
+ if (useMergedEndpoint && data.data.config?.toolProtection?.tools) {
856
+ // Extract embedded tools to the standard toolProtections field
857
+ data.data.toolProtections = data.data.config.toolProtection.tools;
858
+ if (this.config.debug) {
859
+ console.log("[ToolProtectionService] Extracted tools from merged config", {
860
+ toolCount: Object.keys(data.data.toolProtections).length,
861
+ tools: Object.keys(data.data.toolProtections),
862
+ });
863
+ }
864
+ }
865
+
789
866
  return data;
790
867
  }
791
868
 
@@ -816,28 +893,28 @@ export class ToolProtectionService {
816
893
 
817
894
  /**
818
895
  * Clear cache and immediately fetch fresh config from API
819
- *
896
+ *
820
897
  * This method is designed for Cloudflare Workers where KV has edge caching.
821
898
  * After clearing the KV entry, it fetches fresh data from the API and writes
822
899
  * it back to KV. This ensures:
823
900
  * 1. The global KV entry is deleted
824
901
  * 2. Fresh data is fetched from API (with CDN cache bypass!)
825
902
  * 3. New data is written to KV (updating edge cache)
826
- *
903
+ *
827
904
  * The next request from the same edge location will get the fresh data.
828
- *
905
+ *
829
906
  * IMPORTANT: This method uses bypassCDNCache to ensure we get fresh data
830
907
  * from AgentShield's origin server, not stale CDN-cached data. This is
831
908
  * critical for instant cache invalidation when tool protection settings
832
909
  * are changed in the AgentShield dashboard.
833
- *
910
+ *
834
911
  * @param agentDid DID of the agent (used for cache key)
835
912
  * @returns The fresh tool protection config from API
836
913
  */
837
914
  async clearAndRefresh(agentDid: string): Promise<{
838
915
  config: ToolProtectionConfig;
839
916
  cacheKey: string;
840
- source: 'api' | 'fallback';
917
+ source: "api" | "fallback";
841
918
  }> {
842
919
  const cacheKey = this.config.projectId
843
920
  ? `config:tool-protections:${this.config.projectId}`
@@ -857,7 +934,9 @@ export class ToolProtectionService {
857
934
  // 2. Fetch fresh config from API with CDN cache bypass
858
935
  // This ensures we get fresh data from origin, not stale CDN data
859
936
  try {
860
- const response = await this.fetchFromApi(agentDid, { bypassCDNCache: true });
937
+ const response = await this.fetchFromApi(agentDid, {
938
+ bypassCDNCache: true,
939
+ });
861
940
 
862
941
  // Transform API response to internal format (same logic as getToolProtectionConfig)
863
942
  const toolProtections: Record<string, ToolProtection> = {};
@@ -897,15 +976,20 @@ export class ToolProtectionService {
897
976
  const toolName = (tool as any).name;
898
977
  if (!toolName) continue;
899
978
  const requiresDelegation =
900
- (tool as any).requiresDelegation ?? (tool as any).requires_delegation ?? false;
979
+ (tool as any).requiresDelegation ??
980
+ (tool as any).requires_delegation ??
981
+ false;
901
982
  const requiredScopes =
902
983
  (tool as any).requiredScopes ??
903
984
  (tool as any).required_scopes ??
904
985
  (tool as any).scopes ??
905
986
  [];
906
987
  const oauthProvider =
907
- (tool as any).oauthProvider ?? (tool as any).oauth_provider ?? undefined;
908
- const riskLevel = (tool as any).riskLevel ?? (tool as any).risk_level ?? undefined;
988
+ (tool as any).oauthProvider ??
989
+ (tool as any).oauth_provider ??
990
+ undefined;
991
+ const riskLevel =
992
+ (tool as any).riskLevel ?? (tool as any).risk_level ?? undefined;
909
993
 
910
994
  toolProtections[toolName] = {
911
995
  requiresDelegation,
@@ -964,19 +1048,23 @@ export class ToolProtectionService {
964
1048
  source: "api",
965
1049
  });
966
1050
 
967
- return { config: freshConfig, cacheKey, source: 'api' };
1051
+ return { config: freshConfig, cacheKey, source: "api" };
968
1052
  } catch (error) {
969
- console.warn("[ToolProtectionService] API fetch failed during refresh, using fallback", {
970
- error: error instanceof Error ? error.message : String(error),
971
- cacheKey,
972
- });
1053
+ console.warn(
1054
+ "[ToolProtectionService] API fetch failed during refresh, using fallback",
1055
+ {
1056
+ error: error instanceof Error ? error.message : String(error),
1057
+ cacheKey,
1058
+ }
1059
+ );
973
1060
 
974
1061
  // Use fallback config if API fails
975
- const fallbackConfig: ToolProtectionConfig = this.config.fallbackConfig || {
1062
+ const fallbackConfig: ToolProtectionConfig = this.config
1063
+ .fallbackConfig || {
976
1064
  toolProtections: {},
977
1065
  };
978
1066
 
979
- return { config: fallbackConfig, cacheKey, source: 'fallback' };
1067
+ return { config: fallbackConfig, cacheKey, source: "fallback" };
980
1068
  }
981
1069
  }
982
1070
  }
@@ -11,6 +11,8 @@ import {
11
11
  normalizeDid,
12
12
  compareDids,
13
13
  getServerDid,
14
+ generateDidKeyFromBytes,
15
+ generateDidKeyFromBase64,
14
16
  } from "../did-helpers";
15
17
 
16
18
  describe("DID Helpers", () => {
@@ -97,5 +99,58 @@ describe("DID Helpers", () => {
97
99
  expect(() => getServerDid(config)).toThrow("Server DID not configured");
98
100
  });
99
101
  });
102
+
103
+ describe("generateDidKeyFromBytes", () => {
104
+ it("should generate valid did:key from 32-byte Ed25519 public key", () => {
105
+ // Use a known test key (32 bytes)
106
+ const publicKeyBytes = new Uint8Array(32).fill(0xab);
107
+ const did = generateDidKeyFromBytes(publicKeyBytes);
108
+
109
+ expect(did).toMatch(/^did:key:z6Mk/);
110
+ expect(isValidDid(did)).toBe(true);
111
+ expect(getDidMethod(did)).toBe("key");
112
+ });
113
+
114
+ it("should generate consistent did:key for same input", () => {
115
+ const publicKeyBytes = new Uint8Array(32).fill(0x42);
116
+ const did1 = generateDidKeyFromBytes(publicKeyBytes);
117
+ const did2 = generateDidKeyFromBytes(publicKeyBytes);
118
+
119
+ expect(did1).toBe(did2);
120
+ });
121
+
122
+ it("should generate different did:key for different inputs", () => {
123
+ const key1 = new Uint8Array(32).fill(0x11);
124
+ const key2 = new Uint8Array(32).fill(0x22);
125
+
126
+ const did1 = generateDidKeyFromBytes(key1);
127
+ const did2 = generateDidKeyFromBytes(key2);
128
+
129
+ expect(did1).not.toBe(did2);
130
+ });
131
+ });
132
+
133
+ describe("generateDidKeyFromBase64", () => {
134
+ it("should generate valid did:key from base64-encoded public key", () => {
135
+ // Base64 encode a 32-byte key
136
+ const publicKeyBytes = new Uint8Array(32).fill(0xcd);
137
+ const publicKeyBase64 = btoa(String.fromCharCode(...publicKeyBytes));
138
+
139
+ const did = generateDidKeyFromBase64(publicKeyBase64);
140
+
141
+ expect(did).toMatch(/^did:key:z6Mk/);
142
+ expect(isValidDid(did)).toBe(true);
143
+ });
144
+
145
+ it("should produce same result as generateDidKeyFromBytes", () => {
146
+ const publicKeyBytes = new Uint8Array(32).fill(0xef);
147
+ const publicKeyBase64 = btoa(String.fromCharCode(...publicKeyBytes));
148
+
149
+ const didFromBytes = generateDidKeyFromBytes(publicKeyBytes);
150
+ const didFromBase64 = generateDidKeyFromBase64(publicKeyBase64);
151
+
152
+ expect(didFromBytes).toBe(didFromBase64);
153
+ });
154
+ });
100
155
  });
101
156
 
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Base58 Utilities (Bitcoin alphabet)
3
+ *
4
+ * Encoding and decoding utilities for Base58 (Bitcoin alphabet).
5
+ * Used for did:key multibase encoding (with 'z' prefix for base58btc).
6
+ *
7
+ * The Bitcoin alphabet excludes ambiguous characters (0, O, I, l).
8
+ */
9
+
10
+ const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
11
+ const ALPHABET_MAP = new Map<string, number>();
12
+
13
+ // Build reverse lookup map
14
+ for (let i = 0; i < ALPHABET.length; i++) {
15
+ ALPHABET_MAP.set(ALPHABET[i], i);
16
+ }
17
+
18
+ /**
19
+ * Encode bytes to Base58 (Bitcoin alphabet)
20
+ *
21
+ * @param bytes - Bytes to encode
22
+ * @returns Base58-encoded string
23
+ */
24
+ export function base58Encode(bytes: Uint8Array): string {
25
+ if (bytes.length === 0) return '';
26
+
27
+ // Convert bytes to big integer
28
+ let num = BigInt(0);
29
+ for (let i = 0; i < bytes.length; i++) {
30
+ num = num * BigInt(256) + BigInt(bytes[i]);
31
+ }
32
+
33
+ // Convert to base58
34
+ let result = '';
35
+ while (num > 0) {
36
+ result = ALPHABET[Number(num % BigInt(58))] + result;
37
+ num = num / BigInt(58);
38
+ }
39
+
40
+ // Add leading zeros (encoded as '1' in base58)
41
+ for (let i = 0; i < bytes.length && bytes[i] === 0; i++) {
42
+ result = '1' + result;
43
+ }
44
+
45
+ return result;
46
+ }
47
+
48
+ /**
49
+ * Decode Base58 (Bitcoin alphabet) to bytes
50
+ *
51
+ * @param encoded - Base58-encoded string
52
+ * @returns Decoded bytes
53
+ * @throws Error if input contains invalid characters
54
+ */
55
+ export function base58Decode(encoded: string): Uint8Array {
56
+ if (encoded.length === 0) return new Uint8Array(0);
57
+
58
+ // Convert base58 to big integer
59
+ let num = BigInt(0);
60
+ for (const char of encoded) {
61
+ const value = ALPHABET_MAP.get(char);
62
+ if (value === undefined) {
63
+ throw new Error(`Invalid base58 character: ${char}`);
64
+ }
65
+ num = num * BigInt(58) + BigInt(value);
66
+ }
67
+
68
+ // Convert big integer to bytes
69
+ const bytes: number[] = [];
70
+ while (num > 0) {
71
+ bytes.unshift(Number(num % BigInt(256)));
72
+ num = num / BigInt(256);
73
+ }
74
+
75
+ // Count leading zeros in input (encoded as '1')
76
+ let leadingZeros = 0;
77
+ for (const char of encoded) {
78
+ if (char === '1') {
79
+ leadingZeros++;
80
+ } else {
81
+ break;
82
+ }
83
+ }
84
+
85
+ // Prepend leading zero bytes
86
+ const result = new Uint8Array(leadingZeros + bytes.length);
87
+ // Leading zeros are already 0 in Uint8Array
88
+ result.set(bytes, leadingZeros);
89
+
90
+ return result;
91
+ }
92
+
93
+ /**
94
+ * Validate a Base58 string
95
+ *
96
+ * @param encoded - String to validate
97
+ * @returns true if valid Base58, false otherwise
98
+ */
99
+ export function isValidBase58(encoded: string): boolean {
100
+ if (encoded.length === 0) return true;
101
+
102
+ for (const char of encoded) {
103
+ if (!ALPHABET_MAP.has(char)) {
104
+ return false;
105
+ }
106
+ }
107
+
108
+ return true;
109
+ }