@elevasis/core 0.20.0 → 0.21.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.
- package/dist/index.d.ts +108 -0
- package/dist/index.js +177 -27
- package/dist/knowledge/index.d.ts +54 -0
- package/dist/organization-model/index.d.ts +108 -0
- package/dist/organization-model/index.js +177 -27
- package/dist/test-utils/index.d.ts +54 -0
- package/dist/test-utils/index.js +177 -27
- package/package.json +3 -3
- package/src/_gen/__tests__/__snapshots__/contracts.md.snap +6 -1
- package/src/business/acquisition/api-schemas.test.ts +25 -0
- package/src/business/acquisition/api-schemas.ts +125 -2
- package/src/business/acquisition/build-templates.test.ts +28 -0
- package/src/business/acquisition/build-templates.ts +20 -8
- package/src/business/acquisition/types.ts +6 -1
- package/src/execution/engine/tools/integration/server/adapters/apify/apify-adapter.test.ts +55 -0
- package/src/execution/engine/tools/integration/server/adapters/apify/apify-adapter.ts +107 -41
- package/src/execution/engine/tools/integration/server/adapters/apollo/apollo-adapter.test.ts +48 -0
- package/src/execution/engine/tools/integration/server/adapters/apollo/apollo-adapter.ts +99 -0
- package/src/execution/engine/tools/integration/server/adapters/apollo/index.ts +1 -0
- package/src/execution/engine/tools/integration/server/adapters/clickup/clickup-adapter.test.ts +18 -0
- package/src/execution/engine/tools/integration/server/adapters/clickup/clickup-adapter.ts +194 -0
- package/src/execution/engine/tools/integration/server/adapters/clickup/index.ts +7 -0
- package/src/integrations/credentials/api-schemas.ts +21 -2
- package/src/integrations/credentials/schemas.ts +200 -164
- package/src/organization-model/__tests__/prospecting-ssot.test.ts +7 -4
- package/src/organization-model/domains/prospecting.ts +182 -25
- package/src/organization-model/domains/sales.ts +24 -3
- package/src/platform/constants/versions.ts +1 -1
- package/src/reference/_generated/contracts.md +6 -1
- package/src/server.ts +2 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { ApifyAdapter } from './apify-adapter'
|
|
3
|
+
|
|
4
|
+
describe('ApifyAdapter', () => {
|
|
5
|
+
const adapter = new ApifyAdapter()
|
|
6
|
+
const originalFetch = globalThis.fetch
|
|
7
|
+
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
vi.restoreAllMocks()
|
|
10
|
+
globalThis.fetch = originalFetch
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('accepts Apify tokens stored in provider-specific apiToken shape', () => {
|
|
14
|
+
expect(adapter.validateCredentials({ apiToken: 'apify_api_test' })).toBe(true)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('accepts Apify tokens stored in generic api-key shape', () => {
|
|
18
|
+
expect(adapter.validateCredentials({ apiKey: 'apify_api_test' })).toBe(true)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('rejects blank tokens', () => {
|
|
22
|
+
expect(adapter.validateCredentials({ apiKey: ' ' })).toBe(false)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('verifies credentials against the Apify current-user endpoint', async () => {
|
|
26
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
27
|
+
ok: true,
|
|
28
|
+
json: vi.fn().mockResolvedValue({
|
|
29
|
+
data: {
|
|
30
|
+
username: 'elevasis',
|
|
31
|
+
plan: {
|
|
32
|
+
id: 'TEAM',
|
|
33
|
+
description: 'Team plan'
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
globalThis.fetch = fetchMock as unknown as typeof fetch
|
|
39
|
+
|
|
40
|
+
await expect(adapter.verify({ apiKey: ' apify_api_test ' })).resolves.toEqual({
|
|
41
|
+
ok: true,
|
|
42
|
+
provider: 'apify',
|
|
43
|
+
username: 'elevasis',
|
|
44
|
+
plan: 'TEAM'
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
expect(fetchMock).toHaveBeenCalledWith('https://api.apify.com/v2/users/me', {
|
|
48
|
+
method: 'GET',
|
|
49
|
+
headers: {
|
|
50
|
+
Authorization: 'Bearer apify_api_test',
|
|
51
|
+
'Content-Type': 'application/json'
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
})
|
|
@@ -1,34 +1,60 @@
|
|
|
1
1
|
import type { BaseIntegrationAdapter } from '../../../base-integration-adapter'
|
|
2
2
|
import { ToolingError } from '../../../../types'
|
|
3
|
-
import type { ExecutionContext } from '../../../../../base/types'
|
|
4
|
-
import { runActor } from './fetch/run-actor/index'
|
|
5
|
-
import { getDatasetItems } from './fetch/get-dataset-items/index'
|
|
6
|
-
import { startActor } from './fetch/start-actor/index'
|
|
7
|
-
import type { StartActorParams } from '../../../../integration/types/apify'
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
3
|
+
import type { ExecutionContext } from '../../../../../base/types'
|
|
4
|
+
import { runActor } from './fetch/run-actor/index'
|
|
5
|
+
import { getDatasetItems } from './fetch/get-dataset-items/index'
|
|
6
|
+
import { startActor } from './fetch/start-actor/index'
|
|
7
|
+
import type { StartActorParams } from '../../../../integration/types/apify'
|
|
8
|
+
import { createHttpError, withRetry, DEFAULT_RETRY_POLICY } from '../../../../../../../platform/resilience'
|
|
9
|
+
|
|
10
|
+
interface ApifyCredentials {
|
|
11
|
+
apiToken?: string
|
|
12
|
+
apiKey?: string
|
|
13
|
+
api_token?: string
|
|
14
|
+
token?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ApifyVerifyResult {
|
|
18
|
+
ok: true
|
|
19
|
+
provider: 'apify'
|
|
20
|
+
username?: string
|
|
21
|
+
plan?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type ApifyPlan =
|
|
25
|
+
| string
|
|
26
|
+
| {
|
|
27
|
+
id?: string
|
|
28
|
+
name?: string
|
|
29
|
+
description?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface ApifyUserResponse {
|
|
33
|
+
data?: {
|
|
34
|
+
username?: string
|
|
35
|
+
plan?: ApifyPlan
|
|
36
|
+
id?: string
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface RunActorParams {
|
|
41
|
+
actorId: string
|
|
42
|
+
input?: Record<string, unknown>
|
|
43
|
+
timeoutSecs?: number
|
|
20
44
|
pollIntervalSecs?: number
|
|
21
45
|
maxItems?: number
|
|
22
46
|
}
|
|
23
47
|
|
|
24
48
|
interface GetDatasetItemsParams {
|
|
25
49
|
datasetId: string
|
|
26
|
-
maxItems?: number
|
|
27
|
-
offset?: number
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
50
|
+
maxItems?: number
|
|
51
|
+
offset?: number
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const APIFY_API_BASE_URL = 'https://api.apify.com/v2'
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Apify integration adapter
|
|
32
58
|
*
|
|
33
59
|
* Provides access to Apify actor automation platform for web scraping
|
|
34
60
|
* and browser automation tasks.
|
|
@@ -62,12 +88,14 @@ export class ApifyAdapter implements BaseIntegrationAdapter {
|
|
|
62
88
|
// Normalize credentials to consistent field name
|
|
63
89
|
const normalizedCreds = this.normalizeCredentials(credentials as unknown as ApifyCredentials)
|
|
64
90
|
|
|
65
|
-
// Route to method handler
|
|
66
|
-
switch (method) {
|
|
67
|
-
case '
|
|
68
|
-
return
|
|
69
|
-
case '
|
|
70
|
-
return
|
|
91
|
+
// Route to method handler
|
|
92
|
+
switch (method) {
|
|
93
|
+
case 'verify':
|
|
94
|
+
return this.verify(normalizedCreds, context)
|
|
95
|
+
case 'runActor':
|
|
96
|
+
return runActor(normalizedCreds, params as RunActorParams, context)
|
|
97
|
+
case 'getDatasetItems':
|
|
98
|
+
return getDatasetItems(normalizedCreds, params as GetDatasetItemsParams, context)
|
|
71
99
|
case 'startActor':
|
|
72
100
|
return startActor(normalizedCreds, params as StartActorParams, context)
|
|
73
101
|
default:
|
|
@@ -79,15 +107,47 @@ export class ApifyAdapter implements BaseIntegrationAdapter {
|
|
|
79
107
|
}
|
|
80
108
|
}
|
|
81
109
|
|
|
82
|
-
validateCredentials(credentials: Record<string, unknown>): boolean {
|
|
83
|
-
const creds = credentials as unknown as ApifyCredentials
|
|
84
|
-
const token = creds.apiToken || creds.apiKey || creds.api_token || creds.token
|
|
85
|
-
return
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
110
|
+
validateCredentials(credentials: Record<string, unknown>): boolean {
|
|
111
|
+
const creds = credentials as unknown as ApifyCredentials
|
|
112
|
+
const token = creds.apiToken || creds.apiKey || creds.api_token || creds.token
|
|
113
|
+
return typeof token === 'string' && token.trim().length > 0
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async verify(credentials: ApifyCredentials, context?: ExecutionContext): Promise<ApifyVerifyResult> {
|
|
117
|
+
const normalizedCreds = this.normalizeCredentials(credentials)
|
|
118
|
+
const response = await withRetry(async () => {
|
|
119
|
+
const result = await fetch(`${APIFY_API_BASE_URL}/users/me`, {
|
|
120
|
+
method: 'GET',
|
|
121
|
+
headers: {
|
|
122
|
+
Authorization: `Bearer ${normalizedCreds.apiToken}`,
|
|
123
|
+
'Content-Type': 'application/json'
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
if (!result.ok) {
|
|
128
|
+
throw await createHttpError(result, {
|
|
129
|
+
integration: this.name,
|
|
130
|
+
method: 'verify',
|
|
131
|
+
organizationId: context?.organizationId
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return result
|
|
136
|
+
}, DEFAULT_RETRY_POLICY)
|
|
137
|
+
|
|
138
|
+
const data = (await response.json()) as ApifyUserResponse
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
ok: true,
|
|
142
|
+
provider: 'apify',
|
|
143
|
+
username: data.data?.username ?? data.data?.id,
|
|
144
|
+
plan: formatApifyPlan(data.data?.plan)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Normalize credentials to use consistent field name
|
|
150
|
+
*/
|
|
91
151
|
private normalizeCredentials(credentials: ApifyCredentials): { apiToken: string } {
|
|
92
152
|
const token = credentials.apiToken || credentials.apiKey || credentials.api_token || credentials.token
|
|
93
153
|
if (!token) {
|
|
@@ -95,6 +155,12 @@ export class ApifyAdapter implements BaseIntegrationAdapter {
|
|
|
95
155
|
integration: this.name
|
|
96
156
|
})
|
|
97
157
|
}
|
|
98
|
-
return { apiToken: token }
|
|
99
|
-
}
|
|
100
|
-
}
|
|
158
|
+
return { apiToken: token.trim() }
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function formatApifyPlan(plan: ApifyPlan | undefined): string | undefined {
|
|
163
|
+
if (!plan) return undefined
|
|
164
|
+
if (typeof plan === 'string') return plan
|
|
165
|
+
return plan.id ?? plan.name ?? plan.description
|
|
166
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { ApolloAdapter } from './apollo-adapter'
|
|
3
|
+
|
|
4
|
+
describe('ApolloAdapter', () => {
|
|
5
|
+
const adapter = new ApolloAdapter()
|
|
6
|
+
const originalFetch = globalThis.fetch
|
|
7
|
+
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
vi.restoreAllMocks()
|
|
10
|
+
globalThis.fetch = originalFetch
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('accepts Apollo API keys stored in generic apiKey shape', () => {
|
|
14
|
+
expect(adapter.validateCredentials({ apiKey: 'apollo-key' })).toBe(true)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('accepts Apollo API keys stored in legacy Apollo alias shapes', () => {
|
|
18
|
+
expect(adapter.validateCredentials({ apolloApiKey: 'apollo-key' })).toBe(true)
|
|
19
|
+
expect(adapter.validateCredentials({ APOLLO_API_KEY: 'apollo-key' })).toBe(true)
|
|
20
|
+
expect(adapter.validateCredentials({ key: 'apollo-key' })).toBe(true)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('rejects blank tokens', () => {
|
|
24
|
+
expect(adapter.validateCredentials({ apiKey: ' ' })).toBe(false)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('verifies credentials against the Apollo auth health endpoint', async () => {
|
|
28
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
29
|
+
ok: true
|
|
30
|
+
})
|
|
31
|
+
globalThis.fetch = fetchMock as unknown as typeof fetch
|
|
32
|
+
|
|
33
|
+
await expect(adapter.verify({ apiKey: ' apollo-key ' })).resolves.toEqual({
|
|
34
|
+
ok: true,
|
|
35
|
+
provider: 'apollo',
|
|
36
|
+
authenticated: true
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
expect(fetchMock).toHaveBeenCalledWith('https://api.apollo.io/v1/auth/health', {
|
|
40
|
+
method: 'GET',
|
|
41
|
+
headers: {
|
|
42
|
+
accept: 'application/json',
|
|
43
|
+
'x-api-key': 'apollo-key',
|
|
44
|
+
authorization: 'Bearer apollo-key'
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
})
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { BaseIntegrationAdapter } from '../../../base-integration-adapter'
|
|
2
|
+
import { ToolingError } from '../../../../types'
|
|
3
|
+
import type { ExecutionContext } from '../../../../../base/types'
|
|
4
|
+
import { createHttpError, withRetry, DEFAULT_RETRY_POLICY } from '../../../../../../../platform/resilience'
|
|
5
|
+
|
|
6
|
+
export interface ApolloCredentials {
|
|
7
|
+
apiKey?: string
|
|
8
|
+
apolloApiKey?: string
|
|
9
|
+
APOLLO_API_KEY?: string
|
|
10
|
+
key?: string
|
|
11
|
+
token?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ApolloVerifyResult {
|
|
15
|
+
ok: true
|
|
16
|
+
provider: 'apollo'
|
|
17
|
+
authenticated: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const APOLLO_AUTH_HEALTH_URL = 'https://api.apollo.io/v1/auth/health'
|
|
21
|
+
|
|
22
|
+
export class ApolloAdapter implements BaseIntegrationAdapter {
|
|
23
|
+
readonly name = 'apollo'
|
|
24
|
+
|
|
25
|
+
async call(
|
|
26
|
+
method: string,
|
|
27
|
+
params: unknown,
|
|
28
|
+
credentials: Record<string, unknown>,
|
|
29
|
+
context?: ExecutionContext
|
|
30
|
+
): Promise<unknown> {
|
|
31
|
+
if (!this.validateCredentials(credentials)) {
|
|
32
|
+
throw new ToolingError('credentials_invalid', 'Invalid Apollo credentials', {
|
|
33
|
+
integration: this.name,
|
|
34
|
+
organizationId: context?.organizationId
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
switch (method) {
|
|
39
|
+
case 'verify':
|
|
40
|
+
return this.verify(credentials as unknown as ApolloCredentials, context)
|
|
41
|
+
default:
|
|
42
|
+
throw new ToolingError('method_not_found', `Unknown Apollo method: ${method}`, {
|
|
43
|
+
integration: this.name,
|
|
44
|
+
method,
|
|
45
|
+
organizationId: context?.organizationId
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
validateCredentials(credentials: Record<string, unknown>): boolean {
|
|
51
|
+
const token = this.getToken(credentials as unknown as ApolloCredentials)
|
|
52
|
+
return typeof token === 'string' && token.trim().length > 0
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async verify(credentials: ApolloCredentials, context?: ExecutionContext): Promise<ApolloVerifyResult> {
|
|
56
|
+
const token = this.normalizeCredentials(credentials).apiKey
|
|
57
|
+
await withRetry(async () => {
|
|
58
|
+
const response = await fetch(APOLLO_AUTH_HEALTH_URL, {
|
|
59
|
+
method: 'GET',
|
|
60
|
+
headers: {
|
|
61
|
+
accept: 'application/json',
|
|
62
|
+
'x-api-key': token,
|
|
63
|
+
authorization: `Bearer ${token}`
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
throw await createHttpError(response, {
|
|
69
|
+
integration: this.name,
|
|
70
|
+
method: 'verify',
|
|
71
|
+
organizationId: context?.organizationId
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return response
|
|
76
|
+
}, DEFAULT_RETRY_POLICY)
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
ok: true,
|
|
80
|
+
provider: 'apollo',
|
|
81
|
+
authenticated: true
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private normalizeCredentials(credentials: ApolloCredentials): { apiKey: string } {
|
|
86
|
+
const token = this.getToken(credentials)
|
|
87
|
+
if (!token) {
|
|
88
|
+
throw new ToolingError('credentials_invalid', 'Missing Apollo API key', {
|
|
89
|
+
integration: this.name
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { apiKey: token.trim() }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private getToken(credentials: ApolloCredentials): string | undefined {
|
|
97
|
+
return credentials.apiKey ?? credentials.apolloApiKey ?? credentials.APOLLO_API_KEY ?? credentials.key ?? credentials.token
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ApolloAdapter, type ApolloCredentials, type ApolloVerifyResult } from './apollo-adapter'
|
package/src/execution/engine/tools/integration/server/adapters/clickup/clickup-adapter.test.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { ClickUpAdapter } from './clickup-adapter'
|
|
3
|
+
|
|
4
|
+
describe('ClickUpAdapter', () => {
|
|
5
|
+
const adapter = new ClickUpAdapter()
|
|
6
|
+
|
|
7
|
+
it('accepts ClickUp personal tokens stored in provider-specific apiToken shape', () => {
|
|
8
|
+
expect(adapter.validateCredentials({ apiToken: 'pk_test_123' })).toBe(true)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('accepts ClickUp personal tokens stored in generic api-key shape', () => {
|
|
12
|
+
expect(adapter.validateCredentials({ apiKey: 'pk_test_123' })).toBe(true)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('rejects non-ClickUp API keys', () => {
|
|
16
|
+
expect(adapter.validateCredentials({ apiKey: 'sk_test_123' })).toBe(false)
|
|
17
|
+
})
|
|
18
|
+
})
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import type { BaseIntegrationAdapter } from '../../../base-integration-adapter'
|
|
2
|
+
import { ToolingError } from '../../../../types'
|
|
3
|
+
import type { ExecutionContext } from '../../../../../base/types'
|
|
4
|
+
import { createHttpError, withRetry, DEFAULT_RETRY_POLICY } from '../../../../../../../platform/resilience'
|
|
5
|
+
|
|
6
|
+
export interface ClickUpCredentials {
|
|
7
|
+
apiToken?: string
|
|
8
|
+
apiKey?: string
|
|
9
|
+
api_token?: string
|
|
10
|
+
token?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ClickUpVerifyResult {
|
|
14
|
+
ok: true
|
|
15
|
+
provider: 'clickup'
|
|
16
|
+
teamCount: number
|
|
17
|
+
teams: Array<{
|
|
18
|
+
id: string
|
|
19
|
+
name: string
|
|
20
|
+
}>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ClickUpCreateTaskParams {
|
|
24
|
+
listId: string
|
|
25
|
+
name: string
|
|
26
|
+
markdownContent: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ClickUpCreateTaskResult {
|
|
30
|
+
id: string
|
|
31
|
+
url?: string
|
|
32
|
+
name: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface ClickUpTeamResponse {
|
|
36
|
+
teams?: Array<{
|
|
37
|
+
id?: string | number
|
|
38
|
+
name?: string
|
|
39
|
+
}>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface ClickUpTaskResponse {
|
|
43
|
+
id?: string
|
|
44
|
+
url?: string
|
|
45
|
+
name?: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const CLICKUP_API_BASE_URL = 'https://api.clickup.com/api/v2'
|
|
49
|
+
|
|
50
|
+
export class ClickUpAdapter implements BaseIntegrationAdapter {
|
|
51
|
+
readonly name = 'clickup'
|
|
52
|
+
|
|
53
|
+
async call(
|
|
54
|
+
method: string,
|
|
55
|
+
params: unknown,
|
|
56
|
+
credentials: Record<string, unknown>,
|
|
57
|
+
context?: ExecutionContext
|
|
58
|
+
): Promise<unknown> {
|
|
59
|
+
if (!this.validateCredentials(credentials)) {
|
|
60
|
+
throw new ToolingError('credentials_invalid', 'Invalid ClickUp credentials', {
|
|
61
|
+
integration: this.name,
|
|
62
|
+
organizationId: context?.organizationId
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const creds = this.normalizeCredentials(credentials as unknown as ClickUpCredentials)
|
|
67
|
+
|
|
68
|
+
switch (method) {
|
|
69
|
+
case 'verify':
|
|
70
|
+
return this.verify(creds, context)
|
|
71
|
+
case 'createTask':
|
|
72
|
+
return this.createTask(creds, params as ClickUpCreateTaskParams, context)
|
|
73
|
+
default:
|
|
74
|
+
throw new ToolingError('method_not_found', `Unknown ClickUp method: ${method}`, {
|
|
75
|
+
integration: this.name,
|
|
76
|
+
method,
|
|
77
|
+
organizationId: context?.organizationId
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
validateCredentials(credentials: Record<string, unknown>): boolean {
|
|
83
|
+
const creds = credentials as unknown as ClickUpCredentials
|
|
84
|
+
const token = creds.apiToken || creds.apiKey || creds.api_token || creds.token
|
|
85
|
+
return typeof token === 'string' && token.trim().startsWith('pk_')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async verify(credentials: ClickUpCredentials, context?: ExecutionContext): Promise<ClickUpVerifyResult> {
|
|
89
|
+
const response = await this.request('/team', this.normalizeCredentials(credentials), { method: 'GET' }, 'verify', context)
|
|
90
|
+
const data = (await response.json()) as ClickUpTeamResponse
|
|
91
|
+
const teams = (data.teams ?? []).map((team) => ({
|
|
92
|
+
id: String(team.id ?? ''),
|
|
93
|
+
name: team.name ?? ''
|
|
94
|
+
}))
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
ok: true,
|
|
98
|
+
provider: 'clickup',
|
|
99
|
+
teamCount: teams.length,
|
|
100
|
+
teams: teams.filter((team) => team.id || team.name)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async createTask(
|
|
105
|
+
credentials: { apiToken: string },
|
|
106
|
+
params: ClickUpCreateTaskParams,
|
|
107
|
+
context?: ExecutionContext
|
|
108
|
+
): Promise<ClickUpCreateTaskResult> {
|
|
109
|
+
if (!params.listId || !params.name || !params.markdownContent) {
|
|
110
|
+
throw new ToolingError('validation_error', 'listId, name, and markdownContent are required', {
|
|
111
|
+
integration: this.name,
|
|
112
|
+
method: 'createTask',
|
|
113
|
+
organizationId: context?.organizationId
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const response = await this.request(
|
|
118
|
+
`/list/${encodeURIComponent(params.listId)}/task`,
|
|
119
|
+
this.normalizeCredentials(credentials),
|
|
120
|
+
{
|
|
121
|
+
method: 'POST',
|
|
122
|
+
body: JSON.stringify({
|
|
123
|
+
name: params.name,
|
|
124
|
+
markdown_content: params.markdownContent
|
|
125
|
+
})
|
|
126
|
+
},
|
|
127
|
+
'createTask',
|
|
128
|
+
context
|
|
129
|
+
)
|
|
130
|
+
const data = (await response.json()) as ClickUpTaskResponse
|
|
131
|
+
|
|
132
|
+
if (!data.id) {
|
|
133
|
+
throw new ToolingError('api_error', 'ClickUp create task response did not include a task ID', {
|
|
134
|
+
integration: this.name,
|
|
135
|
+
method: 'createTask',
|
|
136
|
+
organizationId: context?.organizationId
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
id: data.id,
|
|
142
|
+
url: data.url,
|
|
143
|
+
name: data.name ?? params.name
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private async request(
|
|
148
|
+
path: string,
|
|
149
|
+
credentials: { apiToken: string },
|
|
150
|
+
init: RequestInit,
|
|
151
|
+
method: string,
|
|
152
|
+
context?: ExecutionContext
|
|
153
|
+
): Promise<Response> {
|
|
154
|
+
if (!this.validateCredentials(credentials as unknown as Record<string, unknown>)) {
|
|
155
|
+
throw new ToolingError('credentials_invalid', 'ClickUp personal API token must start with pk_', {
|
|
156
|
+
integration: this.name,
|
|
157
|
+
method,
|
|
158
|
+
organizationId: context?.organizationId
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return await withRetry(async () => {
|
|
163
|
+
const response = await fetch(`${CLICKUP_API_BASE_URL}${path}`, {
|
|
164
|
+
...init,
|
|
165
|
+
headers: {
|
|
166
|
+
Authorization: credentials.apiToken,
|
|
167
|
+
'Content-Type': 'application/json',
|
|
168
|
+
...init.headers
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
if (!response.ok) {
|
|
173
|
+
throw await createHttpError(response, {
|
|
174
|
+
integration: this.name,
|
|
175
|
+
method,
|
|
176
|
+
organizationId: context?.organizationId
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return response
|
|
181
|
+
}, DEFAULT_RETRY_POLICY)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private normalizeCredentials(credentials: ClickUpCredentials): { apiToken: string } {
|
|
185
|
+
const token = credentials.apiToken || credentials.apiKey || credentials.api_token || credentials.token
|
|
186
|
+
if (!token || !token.trim().startsWith('pk_')) {
|
|
187
|
+
throw new ToolingError('credentials_invalid', 'ClickUp personal API token must start with pk_', {
|
|
188
|
+
integration: this.name
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return { apiToken: token.trim() }
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -18,11 +18,13 @@ import { UuidSchema, CredentialNameSchema } from '../../platform/utils/validatio
|
|
|
18
18
|
* - 'oauth': All OAuth providers (notion, google-sheets) store this type
|
|
19
19
|
* - 'api-key': Generic single-field API key credentials
|
|
20
20
|
* - 'webhook-secret': Webhook signing secrets for signature validation
|
|
21
|
+
* - 'api-key-secret': API key and secret pair credentials
|
|
22
|
+
* - 'clickup': ClickUp personal-token credentials with provider-specific schema/verification
|
|
21
23
|
*
|
|
22
24
|
* Note: Provider-specific identifiers (notion, google-sheets) are CREDENTIAL_SCHEMAS
|
|
23
25
|
* keys used for UI lookup, NOT stored type values. OAuth credentials store type='oauth'.
|
|
24
26
|
*/
|
|
25
|
-
export const CredentialTypeSchema = z.enum(['oauth', 'api-key', 'webhook-secret', 'api-key-secret'])
|
|
27
|
+
export const CredentialTypeSchema = z.enum(['oauth', 'api-key', 'webhook-secret', 'api-key-secret', 'clickup'])
|
|
26
28
|
|
|
27
29
|
/**
|
|
28
30
|
* Credential value validation
|
|
@@ -114,6 +116,21 @@ export const DeleteCredentialParamsSchema = z.object({
|
|
|
114
116
|
credentialId: UuidSchema
|
|
115
117
|
})
|
|
116
118
|
|
|
119
|
+
/**
|
|
120
|
+
* POST /api/credentials/:credentialId/verify - Verify credential with provider smoke check
|
|
121
|
+
*/
|
|
122
|
+
export const VerifyCredentialParamsSchema = z.object({
|
|
123
|
+
credentialId: UuidSchema
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
export const VerifyCredentialResponseSchema = z.object({
|
|
127
|
+
ok: z.boolean(),
|
|
128
|
+
provider: z.string(),
|
|
129
|
+
checkedAt: z.string().datetime(),
|
|
130
|
+
message: z.string().optional(),
|
|
131
|
+
details: z.record(z.string(), z.unknown()).optional()
|
|
132
|
+
})
|
|
133
|
+
|
|
117
134
|
/**
|
|
118
135
|
* Export all schemas for use in routes
|
|
119
136
|
*/
|
|
@@ -123,5 +140,7 @@ export const CredentialSchemas = {
|
|
|
123
140
|
ListResponse: ListCredentialsResponseSchema,
|
|
124
141
|
UpdateParams: UpdateCredentialParamsSchema,
|
|
125
142
|
UpdateRequest: UpdateCredentialRequestSchema,
|
|
126
|
-
DeleteParams: DeleteCredentialParamsSchema
|
|
143
|
+
DeleteParams: DeleteCredentialParamsSchema,
|
|
144
|
+
VerifyParams: VerifyCredentialParamsSchema,
|
|
145
|
+
VerifyResponse: VerifyCredentialResponseSchema
|
|
127
146
|
}
|