@elevasis/core 0.11.2 → 0.12.0

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 (36) hide show
  1. package/dist/index.d.ts +2 -1
  2. package/dist/index.js +8 -1
  3. package/dist/organization-model/index.d.ts +2 -1
  4. package/dist/organization-model/index.js +8 -1
  5. package/dist/test-utils/index.d.ts +10 -3
  6. package/dist/test-utils/index.js +6 -0
  7. package/package.json +1 -1
  8. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +27 -270
  9. package/src/auth/multi-tenancy/credentials/server/encryption.ts +83 -39
  10. package/src/auth/multi-tenancy/credentials/server/kek-loader.ts +47 -0
  11. package/src/auth/multi-tenancy/index.ts +3 -0
  12. package/src/auth/multi-tenancy/invitations/api-schemas.ts +104 -107
  13. package/src/auth/multi-tenancy/memberships/api-schemas.ts +6 -5
  14. package/src/auth/multi-tenancy/memberships/membership.ts +130 -138
  15. package/src/auth/multi-tenancy/role-management/api-schemas.ts +78 -0
  16. package/src/auth/multi-tenancy/role-management/index.ts +16 -0
  17. package/src/execution/engine/tools/integration/server/adapters/apify/__tests__/apify-run-actor.integration.test.ts +299 -293
  18. package/src/execution/engine/tools/integration/service.test.ts +214 -0
  19. package/src/execution/engine/tools/integration/service.ts +169 -161
  20. package/src/integrations/credentials/__tests__/api-schemas.test.ts +420 -496
  21. package/src/integrations/credentials/api-schemas.ts +127 -143
  22. package/src/integrations/webhook-endpoints/__tests__/api-schemas.test.ts +327 -318
  23. package/src/integrations/webhook-endpoints/api-schemas.ts +103 -102
  24. package/src/integrations/webhook-endpoints/types.ts +58 -51
  25. package/src/operations/activities/api-schemas.ts +80 -79
  26. package/src/operations/activities/types.ts +64 -63
  27. package/src/organization-model/contracts.ts +1 -0
  28. package/src/organization-model/defaults.ts +6 -0
  29. package/src/organization-model/domains/navigation.ts +37 -23
  30. package/src/organization-model/published.ts +2 -1
  31. package/src/platform/constants/versions.ts +1 -1
  32. package/src/reference/_generated/contracts.md +27 -270
  33. package/src/scaffold-registry/__tests__/index.test.ts +72 -7
  34. package/src/scaffold-registry/index.ts +159 -26
  35. package/src/server.ts +281 -272
  36. package/src/supabase/database.types.ts +7 -3
@@ -0,0 +1,214 @@
1
+ /**
2
+ * IntegrationService — credential cache regression tests
3
+ *
4
+ * Regression guard for the context.store plumbing bug fixed in commit 75205cd8e:
5
+ * IntegrationService.call() reads context.store.get(cacheKey) for per-execution
6
+ * credential caching. Before the fix, dispatchIntegrationTool never threaded `store`
7
+ * into the context, causing a V8 TypeError on every integration tool call.
8
+ *
9
+ * These tests exercise the real IntegrationService.call() code path — not a mock —
10
+ * so any future regression that removes context.store from the dispatch path will
11
+ * cause a real crash here, not a silent type error.
12
+ */
13
+
14
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
15
+ import { IntegrationService } from './service'
16
+ import type { BaseIntegrationAdapter } from './base-integration-adapter'
17
+ import type { ExecutionContext } from '../../base/types'
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Mock the registry that IntegrationService.call() uses internally
21
+ // ---------------------------------------------------------------------------
22
+
23
+ const mockGetCredential = vi.fn()
24
+
25
+ vi.mock('../registry', () => ({
26
+ getToolServices: () => ({
27
+ credentialsService: {
28
+ getCredential: mockGetCredential
29
+ }
30
+ })
31
+ }))
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Helpers
35
+ // ---------------------------------------------------------------------------
36
+
37
+ /** Minimal stub adapter — always validates, always succeeds */
38
+ function makeStubAdapter(name: string): BaseIntegrationAdapter {
39
+ return {
40
+ name,
41
+ validateCredentials: () => true,
42
+ call: vi.fn().mockResolvedValue({ stubResult: true })
43
+ }
44
+ }
45
+
46
+ /** Minimal ExecutionContext with a real Map for store */
47
+ function makeContext(store: Map<string, unknown> = new Map()): ExecutionContext {
48
+ return {
49
+ executionId: 'exec-test-001',
50
+ organizationId: 'org-test-001',
51
+ organizationName: 'Test Org',
52
+ resourceId: 'resource-test-001',
53
+ executionDepth: 0,
54
+ logger: {
55
+ debug: () => {},
56
+ info: () => {},
57
+ warn: () => {},
58
+ error: () => {}
59
+ },
60
+ store
61
+ }
62
+ }
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // Tests
66
+ // ---------------------------------------------------------------------------
67
+
68
+ describe('IntegrationService — credential cache (context.store)', () => {
69
+ beforeEach(() => {
70
+ vi.clearAllMocks()
71
+ })
72
+
73
+ describe('call() with a valid store Map', () => {
74
+ it('does not throw when context.store is a real Map (regression: was crashing with undefined.get)', async () => {
75
+ const adapter = makeStubAdapter('stripe')
76
+ const service = new IntegrationService(new Map([['stripe', adapter]]))
77
+ const credentials = { apiKey: 'sk_test_123' }
78
+
79
+ mockGetCredential.mockResolvedValue(credentials)
80
+
81
+ const context = makeContext()
82
+
83
+ // This call must NOT throw "Cannot read properties of undefined (reading 'get')"
84
+ const result = await service.call('stripe', 'listPaymentLinks', {}, 'stripe-cred', context)
85
+
86
+ expect(result).toEqual({ stubResult: true })
87
+ })
88
+
89
+ it('fetches credentials from credentialsService on first call', async () => {
90
+ const adapter = makeStubAdapter('stripe')
91
+ const service = new IntegrationService(new Map([['stripe', adapter]]))
92
+ const credentials = { apiKey: 'sk_test_123' }
93
+
94
+ mockGetCredential.mockResolvedValue(credentials)
95
+
96
+ await service.call('stripe', 'listPaymentLinks', {}, 'stripe-cred', makeContext())
97
+
98
+ expect(mockGetCredential).toHaveBeenCalledOnce()
99
+ expect(mockGetCredential).toHaveBeenCalledWith('org-test-001', 'stripe-cred')
100
+ })
101
+
102
+ it('hits the cache and skips credentialsService on second call with the same store', async () => {
103
+ // This is the core regression: the second call should reuse the cached credential
104
+ // (store.get returns the value set by the first call) and NOT call credentialsService again.
105
+ const adapter = makeStubAdapter('stripe')
106
+ const service = new IntegrationService(new Map([['stripe', adapter]]))
107
+ const credentials = { apiKey: 'sk_test_123' }
108
+
109
+ mockGetCredential.mockResolvedValue(credentials)
110
+
111
+ // Use a SHARED store Map across both calls — as worker-executor does
112
+ const sharedStore = new Map<string, unknown>()
113
+ const ctx = makeContext(sharedStore)
114
+
115
+ await service.call('stripe', 'listPaymentLinks', {}, 'stripe-cred', ctx)
116
+ await service.call('stripe', 'listPaymentLinks', {}, 'stripe-cred', ctx)
117
+
118
+ // credentialsService should only have been called once — second call hit the cache
119
+ expect(mockGetCredential).toHaveBeenCalledOnce()
120
+ })
121
+
122
+ it('fetches credentials again when a fresh store is used (different execution)', async () => {
123
+ // Two distinct executions each own their own store Map.
124
+ // The credential cache must NOT leak across executions.
125
+ const adapter = makeStubAdapter('stripe')
126
+ const service = new IntegrationService(new Map([['stripe', adapter]]))
127
+ const credentials = { apiKey: 'sk_test_123' }
128
+
129
+ mockGetCredential.mockResolvedValue(credentials)
130
+
131
+ // Execution A — fresh store
132
+ await service.call('stripe', 'listPaymentLinks', {}, 'stripe-cred', makeContext(new Map()))
133
+ // Execution B — different fresh store
134
+ await service.call('stripe', 'listPaymentLinks', {}, 'stripe-cred', makeContext(new Map()))
135
+
136
+ // Each execution must independently fetch the credential
137
+ expect(mockGetCredential).toHaveBeenCalledTimes(2)
138
+ })
139
+
140
+ it('writes the credential to store.set after fetching so subsequent calls can retrieve it', async () => {
141
+ const adapter = makeStubAdapter('stripe')
142
+ const service = new IntegrationService(new Map([['stripe', adapter]]))
143
+ const credentials = { apiKey: 'sk_test_456' }
144
+
145
+ mockGetCredential.mockResolvedValue(credentials)
146
+
147
+ const store = new Map<string, unknown>()
148
+ await service.call('stripe', 'listPaymentLinks', {}, 'stripe-cred', makeContext(store))
149
+
150
+ // The credential must have been written into the store under the expected cache key
151
+ const cacheKey = '__cred_cache__:stripe-cred'
152
+ expect(store.has(cacheKey)).toBe(true)
153
+ expect(store.get(cacheKey)).toEqual(credentials)
154
+ })
155
+ })
156
+
157
+ describe('call() error conditions', () => {
158
+ it('throws with credentials_missing when credentialsService returns null', async () => {
159
+ const adapter = makeStubAdapter('stripe')
160
+ const service = new IntegrationService(new Map([['stripe', adapter]]))
161
+
162
+ mockGetCredential.mockResolvedValue(null)
163
+
164
+ await expect(service.call('stripe', 'listPaymentLinks', {}, 'missing-cred', makeContext())).rejects.toMatchObject(
165
+ {
166
+ errorType: 'credentials_missing'
167
+ }
168
+ )
169
+ })
170
+
171
+ it('throws with adapter_not_found when integration is not registered', async () => {
172
+ const service = new IntegrationService(new Map())
173
+
174
+ await expect(service.call('nonexistent', 'someMethod', {}, 'some-cred', makeContext())).rejects.toMatchObject({
175
+ errorType: 'adapter_not_found'
176
+ })
177
+ })
178
+
179
+ it('throws validation_error when context is undefined', async () => {
180
+ const adapter = makeStubAdapter('stripe')
181
+ const service = new IntegrationService(new Map([['stripe', adapter]]))
182
+
183
+ await expect(service.call('stripe', 'listPaymentLinks', {}, 'stripe-cred', undefined)).rejects.toMatchObject({
184
+ errorType: 'validation_error'
185
+ })
186
+ })
187
+
188
+ it('throws TypeError when context.store is undefined (simulates the pre-fix bug)', async () => {
189
+ // This documents the exact failure mode that shipped: dispatchIntegrationTool
190
+ // constructed a context without `store`. The crash was:
191
+ // "Cannot read properties of undefined (reading 'get')"
192
+ const adapter = makeStubAdapter('stripe')
193
+ const service = new IntegrationService(new Map([['stripe', adapter]]))
194
+ const credentials = { apiKey: 'sk_test_123' }
195
+
196
+ mockGetCredential.mockResolvedValue(credentials)
197
+
198
+ // Deliberately construct a context without `store` (mimics the pre-fix bug)
199
+ const brokenContext = {
200
+ executionId: 'exec-broken',
201
+ organizationId: 'org-test-001',
202
+ organizationName: 'Test Org',
203
+ resourceId: 'resource-broken',
204
+ executionDepth: 0,
205
+ logger: { debug: () => {}, info: () => {}, warn: () => {}, error: () => {} }
206
+ // store is intentionally absent — this was the bug
207
+ } as unknown as ExecutionContext
208
+
209
+ await expect(service.call('stripe', 'listPaymentLinks', {}, 'stripe-cred', brokenContext)).rejects.toThrow(
210
+ /Cannot read properties of undefined \(reading '(get|set)'\)/
211
+ )
212
+ })
213
+ })
214
+ })
@@ -1,161 +1,169 @@
1
- import type { ExecutionContext } from '../../base/types'
2
- import type { BaseIntegrationAdapter } from './base-integration-adapter'
3
- import type { RateLimiter, RetryPolicy } from '../../../../platform/resilience'
4
- import { ToolingError } from '../types'
5
- import { withRetry, InMemoryRateLimiter, DEFAULT_RETRY_POLICY } from '../../../../platform/resilience'
6
- import {
7
- INTEGRATION_RATE_LIMIT_CAPACITY,
8
- INTEGRATION_RATE_LIMIT_WINDOW_MS
9
- } from '../../../../platform/constants/resilience'
10
- import { getToolServices } from '../registry'
11
-
12
- /**
13
- * Integration Service
14
- *
15
- * Central service for managing integration adapters, rate limiting, and retry.
16
- * Credentials accessed via global tool services registry.
17
- *
18
- * Pattern: Active Record (justified - owns state + behavior)
19
- *
20
- * @example
21
- * const service = new IntegrationService(
22
- * new Map([['gmail', gmailAdapter]])
23
- * )
24
- *
25
- * const result = await service.call('gmail', 'sendEmail', params, 'gmail-cred', context)
26
- */
27
- export class IntegrationService {
28
- private readonly rateLimiter: RateLimiter
29
- private readonly retryPolicy: RetryPolicy
30
-
31
- constructor(public readonly adapters: Map<string, BaseIntegrationAdapter>) {
32
- this.rateLimiter = new InMemoryRateLimiter(INTEGRATION_RATE_LIMIT_CAPACITY, INTEGRATION_RATE_LIMIT_WINDOW_MS)
33
- this.retryPolicy = DEFAULT_RETRY_POLICY
34
- }
35
-
36
- /**
37
- * Call integration method with full orchestration
38
- *
39
- * Orchestrates the full integration call flow:
40
- * 1. Validate execution context
41
- * 2. Validate adapter exists
42
- * 3. Check rate limit (per organization per integration)
43
- * 4. Get credentials from registry
44
- * 5. Validate credentials
45
- * 6. Call adapter with retry
46
- *
47
- * @param integration - Integration adapter type (e.g., 'gmail', 'slack')
48
- * @param method - Method name (e.g., 'sendEmail', 'postMessage')
49
- * @param params - Method parameters
50
- * @param credentialName - Credential name from credentials table
51
- * @param context - Execution context (required for multi-tenant isolation)
52
- * @returns Method result
53
- * @throws ToolingError if call fails
54
- */
55
- async call(
56
- integration: string,
57
- method: string,
58
- params: unknown,
59
- credentialName: string,
60
- context: ExecutionContext | undefined
61
- ): Promise<unknown> {
62
- // 1. Validate execution context (required for multi-tenant isolation)
63
- if (!context) {
64
- throw new ToolingError('validation_error', 'ExecutionContext required for integration calls', {
65
- integration,
66
- method,
67
- credentialName
68
- })
69
- }
70
-
71
- // 2. Validate adapter exists
72
- const adapter = this.adapters.get(integration)
73
- if (!adapter) {
74
- throw new ToolingError('adapter_not_found', `Integration adapter not found: ${integration}`, {
75
- integration,
76
- availableAdapters: Array.from(this.adapters.keys())
77
- })
78
- }
79
-
80
- // 3. Check rate limit (per organization per integration)
81
- const rateLimitKey = `${context.organizationId}:${integration}`
82
- const allowed = await this.rateLimiter.allow(rateLimitKey)
83
- if (!allowed) {
84
- const remaining = await this.rateLimiter.remaining(rateLimitKey)
85
- throw new ToolingError('rate_limit_exceeded', 'Rate limit exceeded', {
86
- integration,
87
- organizationId: context.organizationId,
88
- remaining
89
- })
90
- }
91
-
92
- // 4. Get credentials from registry
93
- const { credentialsService } = getToolServices()
94
- if (!credentialsService) {
95
- throw new ToolingError(
96
- 'service_unavailable',
97
- 'CredentialsService not initialized. Ensure toolServicesRegistry.initialize() was called in apps/api/src/main.ts',
98
- { integration, credentialName }
99
- )
100
- }
101
-
102
- const credentials = await credentialsService.getCredential(context.organizationId, credentialName)
103
- if (!credentials) {
104
- throw new ToolingError('credentials_missing', `No credentials found for credential name: ${credentialName}`, {
105
- integration,
106
- credentialName,
107
- organizationId: context.organizationId
108
- })
109
- }
110
-
111
- // Log credential retrieval for OAuth integrations
112
- const isOAuth = !!(credentials as Record<string, unknown>).provider
113
- if (isOAuth) {
114
- const creds = credentials as Record<string, unknown>
115
- context.logger?.debug(
116
- `OAuth credential retrieved: integration=${integration}, credentialName=${credentialName}, provider=${creds.provider}, hasRefreshToken=${!!creds.refreshToken}, hasExpiresAt=${!!creds.expiresAt}, expiresAt=${creds.expiresAt}`
117
- )
118
- }
119
-
120
- // 5. Validate credentials
121
- const isValid = adapter.validateCredentials(credentials)
122
- if (!isValid) {
123
- throw new ToolingError('credentials_invalid', `Invalid credentials for integration: ${integration}`, {
124
- integration,
125
- organizationId: context.organizationId
126
- })
127
- }
128
-
129
- // 6. Call adapter with retry (pass credentialName in context for token refresh persistence)
130
- const contextWithCredential: ExecutionContext = {
131
- ...context,
132
- credentialName
133
- }
134
- return withRetry(() => adapter.call(method, params, credentials, contextWithCredential), this.retryPolicy)
135
- }
136
-
137
- /**
138
- * Get adapter by name
139
- * @param integration - Integration name
140
- * @returns Adapter or undefined
141
- */
142
- getAdapter(integration: string): BaseIntegrationAdapter | undefined {
143
- return this.adapters.get(integration)
144
- }
145
-
146
- /**
147
- * Register adapter
148
- * @param adapter - Adapter to register
149
- */
150
- registerAdapter(adapter: BaseIntegrationAdapter): void {
151
- this.adapters.set(adapter.name, adapter)
152
- }
153
-
154
- /**
155
- * Unregister adapter
156
- * @param integration - Integration name to unregister
157
- */
158
- unregisterAdapter(integration: string): void {
159
- this.adapters.delete(integration)
160
- }
161
- }
1
+ import type { ExecutionContext } from '../../base/types'
2
+ import type { BaseIntegrationAdapter } from './base-integration-adapter'
3
+ import type { RateLimiter, RetryPolicy } from '../../../../platform/resilience'
4
+ import { ToolingError } from '../types'
5
+ import { withRetry, InMemoryRateLimiter, DEFAULT_RETRY_POLICY } from '../../../../platform/resilience'
6
+ import {
7
+ INTEGRATION_RATE_LIMIT_CAPACITY,
8
+ INTEGRATION_RATE_LIMIT_WINDOW_MS
9
+ } from '../../../../platform/constants/resilience'
10
+ import { getToolServices } from '../registry'
11
+
12
+ /**
13
+ * Integration Service
14
+ *
15
+ * Central service for managing integration adapters, rate limiting, and retry.
16
+ * Credentials accessed via global tool services registry.
17
+ *
18
+ * Pattern: Active Record (justified - owns state + behavior)
19
+ *
20
+ * @example
21
+ * const service = new IntegrationService(
22
+ * new Map([['gmail', gmailAdapter]])
23
+ * )
24
+ *
25
+ * const result = await service.call('gmail', 'sendEmail', params, 'gmail-cred', context)
26
+ */
27
+ export class IntegrationService {
28
+ private readonly rateLimiter: RateLimiter
29
+ private readonly retryPolicy: RetryPolicy
30
+
31
+ constructor(public readonly adapters: Map<string, BaseIntegrationAdapter>) {
32
+ this.rateLimiter = new InMemoryRateLimiter(INTEGRATION_RATE_LIMIT_CAPACITY, INTEGRATION_RATE_LIMIT_WINDOW_MS)
33
+ this.retryPolicy = DEFAULT_RETRY_POLICY
34
+ }
35
+
36
+ /**
37
+ * Call integration method with full orchestration
38
+ *
39
+ * Orchestrates the full integration call flow:
40
+ * 1. Validate execution context
41
+ * 2. Validate adapter exists
42
+ * 3. Check rate limit (per organization per integration)
43
+ * 4. Get credentials from registry
44
+ * 5. Validate credentials
45
+ * 6. Call adapter with retry
46
+ *
47
+ * @param integration - Integration adapter type (e.g., 'gmail', 'slack')
48
+ * @param method - Method name (e.g., 'sendEmail', 'postMessage')
49
+ * @param params - Method parameters
50
+ * @param credentialName - Credential name from credentials table
51
+ * @param context - Execution context (required for multi-tenant isolation)
52
+ * @returns Method result
53
+ * @throws ToolingError if call fails
54
+ */
55
+ async call(
56
+ integration: string,
57
+ method: string,
58
+ params: unknown,
59
+ credentialName: string,
60
+ context: ExecutionContext | undefined
61
+ ): Promise<unknown> {
62
+ // 1. Validate execution context (required for multi-tenant isolation)
63
+ if (!context) {
64
+ throw new ToolingError('validation_error', 'ExecutionContext required for integration calls', {
65
+ integration,
66
+ method,
67
+ credentialName
68
+ })
69
+ }
70
+
71
+ // 2. Validate adapter exists
72
+ const adapter = this.adapters.get(integration)
73
+ if (!adapter) {
74
+ throw new ToolingError('adapter_not_found', `Integration adapter not found: ${integration}`, {
75
+ integration,
76
+ availableAdapters: Array.from(this.adapters.keys())
77
+ })
78
+ }
79
+
80
+ // 3. Check rate limit (per organization per integration)
81
+ const rateLimitKey = `${context.organizationId}:${integration}`
82
+ const allowed = await this.rateLimiter.allow(rateLimitKey)
83
+ if (!allowed) {
84
+ const remaining = await this.rateLimiter.remaining(rateLimitKey)
85
+ throw new ToolingError('rate_limit_exceeded', 'Rate limit exceeded', {
86
+ integration,
87
+ organizationId: context.organizationId,
88
+ remaining
89
+ })
90
+ }
91
+
92
+ // 4. Get credentials from registry (per-execution cache keyed by credential name)
93
+ const cacheKey = `__cred_cache__:${credentialName}`
94
+ let credentials = context.store.get(cacheKey) as Record<string, unknown> | undefined
95
+
96
+ if (!credentials) {
97
+ const { credentialsService } = getToolServices()
98
+ if (!credentialsService) {
99
+ throw new ToolingError(
100
+ 'service_unavailable',
101
+ 'CredentialsService not initialized. Ensure toolServicesRegistry.initialize() was called in apps/api/src/main.ts',
102
+ { integration, credentialName }
103
+ )
104
+ }
105
+
106
+ const fetched = await credentialsService.getCredential(context.organizationId, credentialName)
107
+ if (!fetched) {
108
+ throw new ToolingError('credentials_missing', `No credentials found for credential name: ${credentialName}`, {
109
+ integration,
110
+ credentialName,
111
+ organizationId: context.organizationId
112
+ })
113
+ }
114
+
115
+ context.store.set(cacheKey, fetched)
116
+ credentials = fetched
117
+ }
118
+
119
+ // Log credential retrieval for OAuth integrations
120
+ const isOAuth = !!credentials.provider
121
+ if (isOAuth) {
122
+ const creds = credentials
123
+ context.logger?.debug(
124
+ `OAuth credential retrieved: integration=${integration}, credentialName=${credentialName}, provider=${creds.provider}, hasRefreshToken=${!!creds.refreshToken}, hasExpiresAt=${!!creds.expiresAt}, expiresAt=${creds.expiresAt}`
125
+ )
126
+ }
127
+
128
+ // 5. Validate credentials
129
+ const isValid = adapter.validateCredentials(credentials)
130
+ if (!isValid) {
131
+ throw new ToolingError('credentials_invalid', `Invalid credentials for integration: ${integration}`, {
132
+ integration,
133
+ organizationId: context.organizationId
134
+ })
135
+ }
136
+
137
+ // 6. Call adapter with retry (pass credentialName in context for token refresh persistence)
138
+ const contextWithCredential: ExecutionContext = {
139
+ ...context,
140
+ credentialName
141
+ }
142
+ return withRetry(() => adapter.call(method, params, credentials, contextWithCredential), this.retryPolicy)
143
+ }
144
+
145
+ /**
146
+ * Get adapter by name
147
+ * @param integration - Integration name
148
+ * @returns Adapter or undefined
149
+ */
150
+ getAdapter(integration: string): BaseIntegrationAdapter | undefined {
151
+ return this.adapters.get(integration)
152
+ }
153
+
154
+ /**
155
+ * Register adapter
156
+ * @param adapter - Adapter to register
157
+ */
158
+ registerAdapter(adapter: BaseIntegrationAdapter): void {
159
+ this.adapters.set(adapter.name, adapter)
160
+ }
161
+
162
+ /**
163
+ * Unregister adapter
164
+ * @param integration - Integration name to unregister
165
+ */
166
+ unregisterAdapter(integration: string): void {
167
+ this.adapters.delete(integration)
168
+ }
169
+ }