@kya-os/mcp-i-core 1.3.0 → 1.3.2

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 (34) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test$colon$coverage.log +2579 -2669
  3. package/.turbo/turbo-test.log +1251 -1245
  4. package/coverage/coverage-final.json +4 -4
  5. package/dist/services/oauth-config.service.d.ts.map +1 -1
  6. package/dist/services/oauth-config.service.js +5 -3
  7. package/dist/services/oauth-config.service.js.map +1 -1
  8. package/dist/services/oauth-provider-registry.d.ts +11 -0
  9. package/dist/services/oauth-provider-registry.d.ts.map +1 -1
  10. package/dist/services/oauth-provider-registry.js +16 -0
  11. package/dist/services/oauth-provider-registry.js.map +1 -1
  12. package/dist/services/provider-resolver.d.ts +1 -1
  13. package/dist/services/provider-resolver.d.ts.map +1 -1
  14. package/dist/services/provider-resolver.js +14 -13
  15. package/dist/services/provider-resolver.js.map +1 -1
  16. package/dist/services/tool-protection.service.d.ts +13 -10
  17. package/dist/services/tool-protection.service.d.ts.map +1 -1
  18. package/dist/services/tool-protection.service.js +24 -113
  19. package/dist/services/tool-protection.service.js.map +1 -1
  20. package/package.json +2 -2
  21. package/src/__tests__/regression/phase2-regression.test.ts +8 -6
  22. package/src/__tests__/services/cache-no-warming.test.ts +177 -0
  23. package/src/__tests__/services/provider-resolver-edge-cases.test.ts +168 -64
  24. package/src/services/__tests__/provider-resolution.integration.test.ts +9 -5
  25. package/src/services/__tests__/provider-resolver.test.ts +22 -26
  26. package/src/services/oauth-config.service.ts +6 -3
  27. package/src/services/oauth-provider-registry.ts +18 -0
  28. package/src/services/provider-resolver.ts +15 -13
  29. package/src/services/tool-protection.service.ts +25 -136
  30. package/src/cache/oauth-config-cache.js +0 -71
  31. package/src/providers/base.js +0 -38
  32. package/src/services/oauth-config.service.js +0 -113
  33. package/src/services/oauth-provider-registry.js +0 -73
  34. package/src/services/provider-resolver.js +0 -106
@@ -45,6 +45,7 @@ describe("ProviderResolver", () => {
45
45
  getAllProviders: vi.fn().mockReturnValue([]),
46
46
  getProviderNames: vi.fn().mockReturnValue([]),
47
47
  loadFromAgentShield: vi.fn().mockResolvedValue(undefined),
48
+ getConfiguredProvider: vi.fn().mockReturnValue(null), // New method for configuredProvider
48
49
  } as any;
49
50
 
50
51
  resolver = new ProviderResolver(mockRegistry, mockConfigService);
@@ -121,42 +122,37 @@ describe("ProviderResolver", () => {
121
122
  expect(provider).toBe("google");
122
123
  });
123
124
 
124
- it("should return null for ambiguous scopes", async () => {
125
+ it("should fall back to configuredProvider for ambiguous scopes", async () => {
125
126
  const toolProtection: ToolProtection = {
126
127
  requiresDelegation: true,
127
128
  requiredScopes: ["github:read", "google:read"],
128
129
  };
129
130
 
130
- (mockRegistry.hasProvider as any).mockReturnValue(true);
131
-
132
- // Should fall through to Priority 3
133
- (mockRegistry.getAllProviders as any).mockReturnValue([
134
- { clientId: "github_client_id" },
135
- ]);
136
- (mockRegistry.getProviderNames as any).mockReturnValue(["github"]);
131
+ // Ambiguous scopes - both infer different providers
132
+ // Should fall through to Priority 3 (configuredProvider)
133
+ (mockRegistry.hasProvider as any).mockImplementation((name: string) => name === "github");
134
+ (mockRegistry.getConfiguredProvider as any).mockReturnValue("github");
137
135
 
138
136
  const provider = await resolver.resolveProvider(
139
137
  toolProtection,
140
138
  "test-project"
141
139
  );
142
140
 
143
- // Falls back to first configured provider
141
+ // Falls back to configuredProvider
144
142
  expect(provider).toBe("github");
145
143
  });
146
144
  });
147
145
 
148
- describe("resolveProvider - Priority 3: First configured provider", () => {
149
- it("should use first configured provider as fallback", async () => {
146
+ describe("resolveProvider - Priority 3: Project-configured provider", () => {
147
+ it("should use configuredProvider as fallback", async () => {
150
148
  const toolProtection: ToolProtection = {
151
149
  requiresDelegation: true,
152
150
  requiredScopes: ["custom:scope"],
153
151
  };
154
152
 
155
- (mockRegistry.hasProvider as any).mockReturnValue(false);
156
- (mockRegistry.getAllProviders as any).mockReturnValue([
157
- { clientId: "github_client_id" },
158
- ]);
159
- (mockRegistry.getProviderNames as any).mockReturnValue(["github"]);
153
+ // configuredProvider is github
154
+ (mockRegistry.hasProvider as any).mockImplementation((name: string) => name === "github");
155
+ (mockRegistry.getConfiguredProvider as any).mockReturnValue("github");
160
156
 
161
157
  const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
162
158
 
@@ -167,30 +163,28 @@ describe("ProviderResolver", () => {
167
163
 
168
164
  expect(provider).toBe("github");
169
165
  expect(consoleSpy).toHaveBeenCalledWith(
170
- expect.stringContaining("deprecated")
166
+ expect.stringContaining("project-configured provider")
171
167
  );
172
168
 
173
169
  consoleSpy.mockRestore();
174
170
  });
175
171
 
176
- it("should log deprecation warning when using fallback", async () => {
172
+ it("should log warning when using configuredProvider fallback", async () => {
177
173
  const toolProtection: ToolProtection = {
178
174
  requiresDelegation: true,
179
175
  requiredScopes: [],
180
176
  };
181
177
 
182
- (mockRegistry.hasProvider as any).mockReturnValue(false);
183
- (mockRegistry.getAllProviders as any).mockReturnValue([
184
- { clientId: "github_client_id" },
185
- ]);
186
- (mockRegistry.getProviderNames as any).mockReturnValue(["github"]);
178
+ // configuredProvider is github
179
+ (mockRegistry.hasProvider as any).mockImplementation((name: string) => name === "github");
180
+ (mockRegistry.getConfiguredProvider as any).mockReturnValue("github");
187
181
 
188
182
  const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
189
183
 
190
184
  await resolver.resolveProvider(toolProtection, "test-project");
191
185
 
192
186
  expect(consoleSpy).toHaveBeenCalledWith(
193
- expect.stringContaining("deprecated")
187
+ expect.stringContaining("Consider explicitly setting oauthProvider")
194
188
  );
195
189
 
196
190
  consoleSpy.mockRestore();
@@ -198,19 +192,21 @@ describe("ProviderResolver", () => {
198
192
  });
199
193
 
200
194
  describe("resolveProvider - Priority 4: Error if no provider", () => {
201
- it("should throw error if no provider can be resolved", async () => {
195
+ it("should throw error if no provider is configured", async () => {
202
196
  const toolProtection: ToolProtection = {
203
197
  requiresDelegation: true,
204
198
  requiredScopes: [],
205
199
  };
206
200
 
201
+ // No configuredProvider set
207
202
  (mockRegistry.hasProvider as any).mockReturnValue(false);
203
+ (mockRegistry.getConfiguredProvider as any).mockReturnValue(null);
208
204
  (mockRegistry.getAllProviders as any).mockReturnValue([]);
209
205
  (mockRegistry.getProviderNames as any).mockReturnValue([]);
210
206
 
211
207
  await expect(
212
208
  resolver.resolveProvider(toolProtection, "test-project")
213
- ).rejects.toThrow(/no provider could be resolved/);
209
+ ).rejects.toThrow(/no provider is configured/);
214
210
  });
215
211
  });
216
212
  });
@@ -99,6 +99,7 @@ export class OAuthConfigService {
99
99
  success: boolean;
100
100
  data?: {
101
101
  providers?: Record<string, unknown>;
102
+ configuredProvider?: string | null;
102
103
  };
103
104
  };
104
105
 
@@ -116,11 +117,12 @@ export class OAuthConfigService {
116
117
  }
117
118
 
118
119
  // Build OAuthConfig object
119
- // Note: API does NOT return defaultProvider field (Phase 1 architecture)
120
- // Phase 1 uses configured provider as temporary fallback
121
- // Phase 2+ requires tools to explicitly specify oauthProvider
120
+ // Extract configuredProvider from API response - this indicates which provider
121
+ // the user has actually configured in AgentShield dashboard
122
+ const configuredProvider = result.data.configuredProvider || null;
122
123
  const config: OAuthConfig = {
123
124
  providers: providers as Record<string, OAuthProvider>,
125
+ configuredProvider,
124
126
  };
125
127
 
126
128
  // Cache config
@@ -130,6 +132,7 @@ export class OAuthConfigService {
130
132
  projectId,
131
133
  providerCount: Object.keys(providers).length,
132
134
  providers: Object.keys(providers),
135
+ configuredProvider,
133
136
  expiresAt: new Date(
134
137
  Date.now() + this.config.cacheTtl
135
138
  ).toISOString(),
@@ -19,6 +19,7 @@ import type { ProviderValidator } from "./provider-validator.js";
19
19
  */
20
20
  export class OAuthProviderRegistry {
21
21
  private providers: Map<string, OAuthProvider> = new Map();
22
+ private _configuredProvider: string | null = null;
22
23
 
23
24
  constructor(
24
25
  private configService: OAuthConfigService,
@@ -30,6 +31,7 @@ export class OAuthProviderRegistry {
30
31
  *
31
32
  * Fetches OAuth configuration and caches providers in memory.
32
33
  * Clears existing providers before loading new ones.
34
+ * Also stores the configured provider from the API response.
33
35
  *
34
36
  * @param projectId - Project ID to load providers for
35
37
  */
@@ -39,12 +41,28 @@ export class OAuthProviderRegistry {
39
41
  // Clear existing providers
40
42
  this.providers.clear();
41
43
 
44
+ // Store the configured provider from API response
45
+ // This is the provider the user has explicitly configured in AgentShield dashboard
46
+ this._configuredProvider = config.configuredProvider || null;
47
+
42
48
  // Register all providers from config
43
49
  for (const [name, providerConfig] of Object.entries(config.providers)) {
44
50
  this.providers.set(name, providerConfig);
45
51
  }
46
52
  }
47
53
 
54
+ /**
55
+ * Get the explicitly configured provider for this project
56
+ *
57
+ * Returns the provider that the user has configured in AgentShield dashboard.
58
+ * Used by ProviderResolver as fallback when tool doesn't specify oauthProvider.
59
+ *
60
+ * @returns Configured provider name, or null if no provider is configured
61
+ */
62
+ getConfiguredProvider(): string | null {
63
+ return this._configuredProvider;
64
+ }
65
+
48
66
  /**
49
67
  * Get provider by name
50
68
  *
@@ -17,7 +17,7 @@ import type { OAuthConfigService } from "./oauth-config.service.js";
17
17
  * Priority order:
18
18
  * 1. Tool-specific oauthProvider field (Phase 2+ preferred)
19
19
  * 2. Scope prefix inference (fallback)
20
- * 3. First configured provider (Phase 1 compatibility fallback)
20
+ * 3. Project-configured provider from AgentShield dashboard
21
21
  * 4. Error if no provider can be resolved
22
22
  */
23
23
  export class ProviderResolver {
@@ -70,25 +70,27 @@ export class ProviderResolver {
70
70
  }
71
71
  }
72
72
 
73
- // Priority 3: First configured provider (Phase 1 compatibility fallback)
74
- // Ensure registry is loaded
73
+ // Priority 3: Use explicitly configured provider from AgentShield dashboard
74
+ // This is the provider the user has actually configured, not just any available provider
75
75
  await this.registry.loadFromAgentShield(projectId);
76
- const providers = this.registry.getAllProviders();
77
- if (providers.length > 0) {
78
- // Log deprecation warning for Phase 1 tools
79
- const firstProviderName = this.registry.getProviderNames()[0];
76
+ const configuredProvider = this.registry.getConfiguredProvider();
77
+
78
+ if (configuredProvider && this.registry.hasProvider(configuredProvider)) {
80
79
  console.warn(
81
80
  `[ProviderResolver] Tool does not specify oauthProvider. ` +
82
- `Using first configured provider "${firstProviderName}" as fallback. ` +
83
- `This is deprecated - configure oauthProvider in AgentShield dashboard for Phase 2+.`
81
+ `Using project-configured provider "${configuredProvider}" as fallback. ` +
82
+ `Consider explicitly setting oauthProvider in tool protection config.`
84
83
  );
85
- return firstProviderName;
84
+ return configuredProvider;
86
85
  }
87
86
 
88
- // Priority 4: Error if no provider can be resolved
87
+ // Priority 4: Error if no provider is configured
88
+ // NOTE: We intentionally do NOT fall back to "first available provider" anymore
89
+ // because AgentShield returns ALL providers (even unconfigured ones).
90
+ // Only use providers explicitly configured by the user.
89
91
  throw new Error(
90
- `Tool requires OAuth but no provider could be resolved. ` +
91
- `Either specify oauthProvider in tool protection config, or configure at least one provider for project "${projectId}".`
92
+ `Tool requires OAuth but no provider is configured for project "${projectId}". ` +
93
+ `Configure an OAuth provider in AgentShield dashboard.`
92
94
  );
93
95
  }
94
96
 
@@ -797,162 +797,51 @@ export class ToolProtectionService {
797
797
  }
798
798
 
799
799
  /**
800
- * Clear cache and immediately fetch fresh config from API
800
+ * Clear cache WITHOUT warming
801
801
  *
802
- * This method is designed for Cloudflare Workers where KV has edge caching.
803
- * After clearing the KV entry, it fetches fresh data from the API and writes
804
- * it back to KV. This ensures:
805
- * 1. The global KV entry is deleted
806
- * 2. Fresh data is fetched from API
807
- * 3. New data is written to KV (updating edge cache)
802
+ * IMPORTANT: This method intentionally does NOT fetch fresh data after clearing.
808
803
  *
809
- * The next request from the same edge location will get the fresh data.
804
+ * Why no cache warming?
805
+ * - Upstream APIs (AgentShield) may have their own CDN/cache layers
806
+ * - Immediately fetching after clear would get stale CDN data
807
+ * - Storing stale data defeats the purpose of cache invalidation
808
+ * - The next actual tool call will fetch fresh data (with natural delay)
809
+ *
810
+ * This was the behavior in mcp-i-cloudflare@1.6.3 that worked perfectly.
811
+ * Cache warming was added as an "optimization" that backfired when upstream
812
+ * APIs have their own caching.
810
813
  *
811
814
  * @param agentDid DID of the agent (used for cache key)
812
- * @returns The fresh tool protection config from API
815
+ * @returns Cache key that was cleared
813
816
  */
814
817
  async clearAndRefresh(agentDid: string): Promise<{
815
818
  config: ToolProtectionConfig;
816
819
  cacheKey: string;
817
- source: 'api' | 'fallback';
820
+ source: 'cleared';
818
821
  }> {
819
822
  const cacheKey = this.config.projectId
820
823
  ? `config:tool-protections:${this.config.projectId}`
821
824
  : `agent:${agentDid}`;
822
825
 
823
- console.log("[ToolProtectionService] clearAndRefresh starting", {
826
+ console.log("[ToolProtectionService] clearCache (no warming)", {
824
827
  cacheKey,
825
828
  projectId: this.config.projectId || "none",
826
829
  agentDid: agentDid.slice(0, 20) + "...",
827
830
  });
828
831
 
829
- // 1. Delete the cache entry
832
+ // Delete the cache entry - that's it, no warming
830
833
  await this.cache.delete(cacheKey);
831
834
 
832
- console.log("[ToolProtectionService] Cache entry deleted", { cacheKey });
833
-
834
- // 2. Fetch fresh config from API
835
- try {
836
- const response = await this.fetchFromApi(agentDid);
837
-
838
- // Transform API response to internal format (same logic as getToolProtectionConfig)
839
- const toolProtections: Record<string, ToolProtection> = {};
840
-
841
- if (response.data.toolProtections) {
842
- for (const [toolName, toolConfig] of Object.entries(
843
- response.data.toolProtections
844
- )) {
845
- const requiresDelegation =
846
- (toolConfig as any).requiresDelegation ??
847
- (toolConfig as any).requires_delegation ??
848
- false;
849
- const requiredScopes =
850
- (toolConfig as any).requiredScopes ??
851
- (toolConfig as any).required_scopes ??
852
- (toolConfig as any).scopes ??
853
- [];
854
- const oauthProvider =
855
- (toolConfig as any).oauthProvider ??
856
- (toolConfig as any).oauth_provider ??
857
- undefined;
858
- const riskLevel =
859
- (toolConfig as any).riskLevel ??
860
- (toolConfig as any).risk_level ??
861
- undefined;
862
-
863
- toolProtections[toolName] = {
864
- requiresDelegation,
865
- requiredScopes,
866
- ...(oauthProvider && { oauthProvider }),
867
- ...(riskLevel && { riskLevel }),
868
- };
869
- }
870
- } else if (response.data.tools) {
871
- if (Array.isArray(response.data.tools)) {
872
- for (const tool of response.data.tools) {
873
- const toolName = (tool as any).name;
874
- if (!toolName) continue;
875
- const requiresDelegation =
876
- (tool as any).requiresDelegation ?? (tool as any).requires_delegation ?? false;
877
- const requiredScopes =
878
- (tool as any).requiredScopes ??
879
- (tool as any).required_scopes ??
880
- (tool as any).scopes ??
881
- [];
882
- const oauthProvider =
883
- (tool as any).oauthProvider ?? (tool as any).oauth_provider ?? undefined;
884
- const riskLevel = (tool as any).riskLevel ?? (tool as any).risk_level ?? undefined;
885
-
886
- toolProtections[toolName] = {
887
- requiresDelegation,
888
- requiredScopes,
889
- ...(oauthProvider && { oauthProvider }),
890
- ...(riskLevel && { riskLevel }),
891
- };
892
- }
893
- } else {
894
- for (const [toolName, toolConfig] of Object.entries(
895
- response.data.tools
896
- )) {
897
- const requiresDelegation =
898
- (toolConfig as any).requiresDelegation ??
899
- (toolConfig as any).requires_delegation ??
900
- false;
901
- const requiredScopes =
902
- (toolConfig as any).requiredScopes ??
903
- (toolConfig as any).required_scopes ??
904
- (toolConfig as any).scopes ??
905
- [];
906
- const oauthProvider =
907
- (toolConfig as any).oauthProvider ??
908
- (toolConfig as any).oauth_provider ??
909
- undefined;
910
- const riskLevel =
911
- (toolConfig as any).riskLevel ??
912
- (toolConfig as any).risk_level ??
913
- undefined;
914
-
915
- toolProtections[toolName] = {
916
- requiresDelegation,
917
- requiredScopes,
918
- ...(oauthProvider && { oauthProvider }),
919
- ...(riskLevel && { riskLevel }),
920
- };
921
- }
922
- }
923
- }
924
-
925
- // Construct fresh config (ToolProtectionConfig type)
926
- const freshConfig: ToolProtectionConfig = {
927
- toolProtections,
928
- };
929
-
930
- // 3. Write fresh config to cache
931
- const ttl = this.config.cacheTtl ?? 300000;
932
- await this.cache.set(cacheKey, freshConfig, ttl);
933
-
934
- console.log("[ToolProtectionService] Fresh config fetched and cached", {
935
- cacheKey,
936
- toolCount: Object.keys(toolProtections).length,
937
- protectedTools: Object.entries(toolProtections)
938
- .filter(([_, cfg]) => cfg.requiresDelegation)
939
- .map(([name]) => name),
940
- source: "api",
941
- });
942
-
943
- return { config: freshConfig, cacheKey, source: 'api' };
944
- } catch (error) {
945
- console.warn("[ToolProtectionService] API fetch failed during refresh, using fallback", {
946
- error: error instanceof Error ? error.message : String(error),
947
- cacheKey,
948
- });
949
-
950
- // Use fallback config if API fails
951
- const fallbackConfig: ToolProtectionConfig = this.config.fallbackConfig || {
952
- toolProtections: {},
953
- };
835
+ console.log("[ToolProtectionService] Cache entry deleted (next call will fetch fresh)", {
836
+ cacheKey,
837
+ note: "No cache warming - upstream APIs may have CDN caching",
838
+ });
954
839
 
955
- return { config: fallbackConfig, cacheKey, source: 'fallback' };
956
- }
840
+ // Return empty config - next tool call will fetch fresh from API
841
+ return {
842
+ config: { toolProtections: {} },
843
+ cacheKey,
844
+ source: 'cleared'
845
+ };
957
846
  }
958
847
  }
@@ -1,71 +0,0 @@
1
- /**
2
- * Platform-agnostic cache interface for OAuth provider configurations
3
- *
4
- * This interface allows different runtime adapters to provide their own
5
- * caching implementations (e.g., in-memory for Node.js, KV for Cloudflare)
6
- *
7
- * @package @kya-os/mcp-i-core
8
- */
9
- /**
10
- * In-memory cache implementation
11
- *
12
- * Suitable for:
13
- * - Node.js runtimes
14
- * - Development/testing
15
- * - Single-instance deployments
16
- *
17
- * NOT suitable for:
18
- * - Multi-instance deployments (cache not shared)
19
- * - Serverless environments (state not persisted)
20
- */
21
- export class InMemoryOAuthConfigCache {
22
- cache = new Map();
23
- async get(key) {
24
- const entry = this.cache.get(key);
25
- if (!entry) {
26
- return null;
27
- }
28
- // Check if expired
29
- if (Date.now() > entry.expiresAt) {
30
- this.cache.delete(key);
31
- return null;
32
- }
33
- return entry.value;
34
- }
35
- async set(key, value, ttl) {
36
- // If TTL is <= 0, don't store (entry would be immediately expired)
37
- if (ttl <= 0) {
38
- return;
39
- }
40
- const expiresAt = Date.now() + ttl;
41
- this.cache.set(key, { value, expiresAt });
42
- }
43
- async clear() {
44
- this.cache.clear();
45
- }
46
- async delete(key) {
47
- this.cache.delete(key);
48
- }
49
- }
50
- /**
51
- * No-op cache implementation (disables caching)
52
- *
53
- * Use when:
54
- * - You want to disable caching entirely
55
- * - Testing scenarios that require fresh data
56
- */
57
- export class NoOpOAuthConfigCache {
58
- async get(_key) {
59
- return null;
60
- }
61
- async set(_key, _value, _ttl) {
62
- // No-op
63
- }
64
- async clear() {
65
- // No-op
66
- }
67
- async delete(_key) {
68
- // No-op
69
- }
70
- }
71
- //# sourceMappingURL=oauth-config-cache.js.map
@@ -1,38 +0,0 @@
1
- /**
2
- * Base Provider Classes
3
- *
4
- * Abstract classes that define the provider interfaces for
5
- * platform-specific implementations.
6
- */
7
- /**
8
- * Cryptographic operations provider
9
- */
10
- export class CryptoProvider {
11
- }
12
- /**
13
- * Clock/timing operations provider
14
- */
15
- export class ClockProvider {
16
- }
17
- /**
18
- * Network fetch operations provider
19
- */
20
- export class FetchProvider {
21
- }
22
- /**
23
- * Storage operations provider
24
- */
25
- export class StorageProvider {
26
- }
27
- /**
28
- * Nonce cache provider
29
- * Handles replay prevention
30
- *
31
- * Nonces should be scoped per agent to prevent cross-agent replay attacks.
32
- * When agentDid is provided, implementations should use agent-scoped keys.
33
- */
34
- export class NonceCacheProvider {
35
- }
36
- export class IdentityProvider {
37
- }
38
- //# sourceMappingURL=base.js.map
@@ -1,113 +0,0 @@
1
- /**
2
- * OAuth Config Service
3
- *
4
- * Fetches and caches OAuth provider configurations from AgentShield API.
5
- * Provides caching with TTL to reduce API calls.
6
- *
7
- * @package @kya-os/mcp-i-core
8
- */
9
- import { InMemoryOAuthConfigCache } from "../cache/oauth-config-cache.js";
10
- /**
11
- * Service for fetching OAuth provider configurations from AgentShield API
12
- */
13
- export class OAuthConfigService {
14
- config;
15
- constructor(config) {
16
- this.config = {
17
- baseUrl: config.baseUrl,
18
- apiKey: config.apiKey,
19
- fetchProvider: config.fetchProvider,
20
- cacheTtl: config.cacheTtl ?? 5 * 60 * 1000, // Default 5 minutes
21
- logger: config.logger || (() => { }),
22
- cache: config.cache || new InMemoryOAuthConfigCache(),
23
- };
24
- }
25
- /**
26
- * Get OAuth configuration for a project
27
- *
28
- * Fetches from AgentShield API: GET /api/v1/bouncer/projects/{projectId}/providers
29
- * Caches responses with TTL to reduce API calls.
30
- *
31
- * @param projectId - Project ID to fetch config for
32
- * @returns OAuth configuration with providers object
33
- */
34
- async getOAuthConfig(projectId) {
35
- // Check cache
36
- const cached = await this.config.cache.get(projectId);
37
- if (cached) {
38
- this.config.logger("[OAuthConfigService] Cache hit", {
39
- projectId,
40
- });
41
- return cached;
42
- }
43
- // Fetch from AgentShield API
44
- const url = `${this.config.baseUrl}/api/v1/bouncer/projects/${encodeURIComponent(projectId)}/providers`;
45
- this.config.logger("[OAuthConfigService] Fetching config from API", {
46
- projectId,
47
- url,
48
- });
49
- try {
50
- const response = await this.config.fetchProvider.fetch(url, {
51
- method: "GET",
52
- headers: {
53
- Authorization: `Bearer ${this.config.apiKey}`,
54
- "Content-Type": "application/json",
55
- },
56
- });
57
- if (!response.ok) {
58
- const errorText = await response.text().catch(() => "Unknown error");
59
- throw new Error(`Failed to fetch OAuth config: ${response.status} ${response.statusText} - ${errorText}`);
60
- }
61
- const result = await response.json();
62
- // Validate response structure
63
- if (!result.success || !result.data) {
64
- throw new Error("Invalid API response: missing success or data field");
65
- }
66
- // Parse providers object
67
- const providers = result.data.providers || {};
68
- if (typeof providers !== "object" || Array.isArray(providers)) {
69
- throw new Error("Invalid API response: providers must be an object, not an array");
70
- }
71
- // Build OAuthConfig object
72
- // Note: API does NOT return defaultProvider field (Phase 1 architecture)
73
- // Phase 1 uses configured provider as temporary fallback
74
- // Phase 2+ requires tools to explicitly specify oauthProvider
75
- const config = {
76
- providers: providers,
77
- };
78
- // Cache config
79
- await this.config.cache.set(projectId, config, this.config.cacheTtl);
80
- this.config.logger("[OAuthConfigService] Config fetched and cached", {
81
- projectId,
82
- providerCount: Object.keys(providers).length,
83
- providers: Object.keys(providers),
84
- expiresAt: new Date(Date.now() + this.config.cacheTtl).toISOString(),
85
- });
86
- return config;
87
- }
88
- catch (error) {
89
- this.config.logger("[OAuthConfigService] API fetch failed", {
90
- projectId,
91
- error: error instanceof Error ? error.message : String(error),
92
- });
93
- throw error;
94
- }
95
- }
96
- /**
97
- * Clear cached config for a project
98
- *
99
- * @param projectId - Project ID to clear cache for
100
- */
101
- async clearCache(projectId) {
102
- await this.config.cache.delete(projectId);
103
- this.config.logger("[OAuthConfigService] Cache cleared", { projectId });
104
- }
105
- /**
106
- * Clear all cached configs
107
- */
108
- async clearAllCache() {
109
- await this.config.cache.clear();
110
- this.config.logger("[OAuthConfigService] All cache cleared");
111
- }
112
- }
113
- //# sourceMappingURL=oauth-config.service.js.map