@yolk-sdk/connectors 0.0.1-canary.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/LICENSE +21 -0
- package/README.md +195 -0
- package/dist/action.d.mts +37 -0
- package/dist/action.d.mts.map +1 -0
- package/dist/action.mjs +24 -0
- package/dist/action.mjs.map +1 -0
- package/dist/agent.d.mts +21 -0
- package/dist/agent.d.mts.map +1 -0
- package/dist/agent.mjs +66 -0
- package/dist/agent.mjs.map +1 -0
- package/dist/config.d.mts +10 -0
- package/dist/config.d.mts.map +1 -0
- package/dist/config.mjs +21 -0
- package/dist/config.mjs.map +1 -0
- package/dist/connector.d.mts +27 -0
- package/dist/connector.d.mts.map +1 -0
- package/dist/connector.mjs +32 -0
- package/dist/connector.mjs.map +1 -0
- package/dist/credential.d.mts +62 -0
- package/dist/credential.d.mts.map +1 -0
- package/dist/credential.mjs +62 -0
- package/dist/credential.mjs.map +1 -0
- package/dist/error.d.mts +17 -0
- package/dist/error.d.mts.map +1 -0
- package/dist/error.mjs +22 -0
- package/dist/error.mjs.map +1 -0
- package/dist/figma/index.d.mts +48 -0
- package/dist/figma/index.d.mts.map +1 -0
- package/dist/figma/index.mjs +97 -0
- package/dist/figma/index.mjs.map +1 -0
- package/dist/google/calendar.d.mts +53 -0
- package/dist/google/calendar.d.mts.map +1 -0
- package/dist/google/calendar.mjs +111 -0
- package/dist/google/calendar.mjs.map +1 -0
- package/dist/google/gmail.d.mts +53 -0
- package/dist/google/gmail.d.mts.map +1 -0
- package/dist/google/gmail.mjs +103 -0
- package/dist/google/gmail.mjs.map +1 -0
- package/dist/google/index.d.mts +14 -0
- package/dist/google/index.d.mts.map +1 -0
- package/dist/google/index.mjs +15 -0
- package/dist/google/index.mjs.map +1 -0
- package/dist/google/oauth.d.mts +18 -0
- package/dist/google/oauth.d.mts.map +1 -0
- package/dist/google/oauth.mjs +25 -0
- package/dist/google/oauth.mjs.map +1 -0
- package/dist/google/shared.d.mts +20 -0
- package/dist/google/shared.d.mts.map +1 -0
- package/dist/google/shared.mjs +36 -0
- package/dist/google/shared.mjs.map +1 -0
- package/dist/http.d.mts +32 -0
- package/dist/http.d.mts.map +1 -0
- package/dist/http.mjs +36 -0
- package/dist/http.mjs.map +1 -0
- package/dist/index.d.mts +9 -0
- package/dist/index.mjs +9 -0
- package/dist/integration.d.mts +24 -0
- package/dist/integration.d.mts.map +1 -0
- package/dist/integration.mjs +22 -0
- package/dist/integration.mjs.map +1 -0
- package/dist/linkedin-search/index.d.mts +60 -0
- package/dist/linkedin-search/index.d.mts.map +1 -0
- package/dist/linkedin-search/index.mjs +162 -0
- package/dist/linkedin-search/index.mjs.map +1 -0
- package/dist/notion/index.d.mts +69 -0
- package/dist/notion/index.d.mts.map +1 -0
- package/dist/notion/index.mjs +169 -0
- package/dist/notion/index.mjs.map +1 -0
- package/dist/r2-storage/index.d.mts +48 -0
- package/dist/r2-storage/index.d.mts.map +1 -0
- package/dist/r2-storage/index.mjs +91 -0
- package/dist/r2-storage/index.mjs.map +1 -0
- package/dist/result.d.mts +32 -0
- package/dist/result.d.mts.map +1 -0
- package/dist/result.mjs +23 -0
- package/dist/result.mjs.map +1 -0
- package/dist/telegram/index.d.mts +34 -0
- package/dist/telegram/index.d.mts.map +1 -0
- package/dist/telegram/index.mjs +109 -0
- package/dist/telegram/index.mjs.map +1 -0
- package/dist/todoist/index.d.mts +70 -0
- package/dist/todoist/index.d.mts.map +1 -0
- package/dist/todoist/index.mjs +176 -0
- package/dist/todoist/index.mjs.map +1 -0
- package/package.json +96 -0
- package/src/action.ts +75 -0
- package/src/agent.ts +120 -0
- package/src/config.ts +28 -0
- package/src/connector.ts +62 -0
- package/src/credential.ts +86 -0
- package/src/error.ts +20 -0
- package/src/figma/index.ts +121 -0
- package/src/google/calendar.ts +145 -0
- package/src/google/gmail.ts +127 -0
- package/src/google/index.ts +46 -0
- package/src/google/oauth.ts +26 -0
- package/src/google/shared.ts +56 -0
- package/src/http.ts +51 -0
- package/src/index.ts +36 -0
- package/src/integration.ts +28 -0
- package/src/linkedin-search/index.ts +217 -0
- package/src/notion/index.ts +234 -0
- package/src/r2-storage/index.ts +118 -0
- package/src/result.ts +35 -0
- package/src/telegram/index.ts +144 -0
- package/src/todoist/index.ts +227 -0
package/src/connector.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Effect, Option } from 'effect'
|
|
2
|
+
import type { ConnectorAction } from './action.ts'
|
|
3
|
+
import { ConnectorError } from './error.ts'
|
|
4
|
+
import type { ActionResult } from './result.ts'
|
|
5
|
+
import type { ConnectorIntegration } from './integration.ts'
|
|
6
|
+
|
|
7
|
+
export type ConnectorInvokeInput = {
|
|
8
|
+
readonly integration: ConnectorIntegration
|
|
9
|
+
readonly action: string
|
|
10
|
+
readonly input: unknown
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type Connector<Env = never, Error = never> = {
|
|
14
|
+
readonly id: string
|
|
15
|
+
readonly description?: string
|
|
16
|
+
readonly actions: ReadonlyArray<ConnectorAction<Env, Error>>
|
|
17
|
+
readonly invoke: (
|
|
18
|
+
input: ConnectorInvokeInput
|
|
19
|
+
) => Effect.Effect<ActionResult<unknown>, Error | ConnectorError, Env>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type DefineConnectorOptions<Env, Error> = {
|
|
23
|
+
readonly id: string
|
|
24
|
+
readonly description?: string
|
|
25
|
+
readonly actions: ReadonlyArray<ConnectorAction<Env, Error>>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const connectorMismatchError = (connectorId: string, integration: ConnectorIntegration) =>
|
|
29
|
+
new ConnectorError({
|
|
30
|
+
cause: 'connector_mismatch',
|
|
31
|
+
message: `Integration belongs to ${integration.connectorId}, not ${connectorId}`,
|
|
32
|
+
connectorId
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const missingActionError = (connectorId: string, actionId: string) =>
|
|
36
|
+
new ConnectorError({
|
|
37
|
+
cause: 'action_not_found',
|
|
38
|
+
message: `Connector action is not configured: ${actionId}`,
|
|
39
|
+
connectorId,
|
|
40
|
+
actionId
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
export const defineConnector = <Env = never, Error = never>(
|
|
44
|
+
options: DefineConnectorOptions<Env, Error>
|
|
45
|
+
): Connector<Env, Error> => ({
|
|
46
|
+
id: options.id,
|
|
47
|
+
description: options.description,
|
|
48
|
+
actions: options.actions,
|
|
49
|
+
invoke: input => {
|
|
50
|
+
if (input.integration.connectorId !== options.id) {
|
|
51
|
+
return Effect.fail(connectorMismatchError(options.id, input.integration))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const action = Option.fromNullishOr(options.actions.find(item => item.id === input.action))
|
|
55
|
+
|
|
56
|
+
if (Option.isNone(action)) {
|
|
57
|
+
return Effect.fail(missingActionError(options.id, input.action))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return action.value.execute({ integration: input.integration, input: input.input })
|
|
61
|
+
}
|
|
62
|
+
})
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Context, Effect, Option } from 'effect'
|
|
2
|
+
import * as Schema from 'effect/Schema'
|
|
3
|
+
import { ConnectorError } from './error.ts'
|
|
4
|
+
import type { ConnectorIntegration } from './integration.ts'
|
|
5
|
+
|
|
6
|
+
export const CredentialKind = Schema.Literals(['api_key', 'bearer_token', 'oauth'])
|
|
7
|
+
export type CredentialKind = typeof CredentialKind.Type
|
|
8
|
+
|
|
9
|
+
export class CredentialSlot extends Schema.Class<CredentialSlot>('CredentialSlot')({
|
|
10
|
+
id: Schema.String,
|
|
11
|
+
kind: CredentialKind,
|
|
12
|
+
requiredScopes: Schema.optional(Schema.Array(Schema.String))
|
|
13
|
+
}) {}
|
|
14
|
+
|
|
15
|
+
export class CredentialBinding extends Schema.Class<CredentialBinding>('CredentialBinding')({
|
|
16
|
+
slotId: Schema.String,
|
|
17
|
+
credentialRef: Schema.String,
|
|
18
|
+
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown))
|
|
19
|
+
}) {}
|
|
20
|
+
|
|
21
|
+
export class ApiKeyCredential extends Schema.Class<ApiKeyCredential>('ApiKeyCredential')({
|
|
22
|
+
_tag: Schema.Literal('ApiKeyCredential'),
|
|
23
|
+
key: Schema.String
|
|
24
|
+
}) {}
|
|
25
|
+
|
|
26
|
+
export class BearerTokenCredential extends Schema.Class<BearerTokenCredential>('BearerTokenCredential')({
|
|
27
|
+
_tag: Schema.Literal('BearerTokenCredential'),
|
|
28
|
+
token: Schema.String,
|
|
29
|
+
expiresAt: Schema.optional(Schema.Number)
|
|
30
|
+
}) {}
|
|
31
|
+
|
|
32
|
+
export class OAuthCredential extends Schema.Class<OAuthCredential>('OAuthCredential')({
|
|
33
|
+
_tag: Schema.Literal('OAuthCredential'),
|
|
34
|
+
provider: Schema.String,
|
|
35
|
+
accessToken: Schema.String,
|
|
36
|
+
expiresAt: Schema.Number,
|
|
37
|
+
accountId: Schema.optional(Schema.String),
|
|
38
|
+
scopes: Schema.optional(Schema.Array(Schema.String))
|
|
39
|
+
}) {}
|
|
40
|
+
|
|
41
|
+
export const RuntimeCredential = Schema.Union([ApiKeyCredential, BearerTokenCredential, OAuthCredential])
|
|
42
|
+
export type RuntimeCredential = typeof RuntimeCredential.Type
|
|
43
|
+
|
|
44
|
+
export type CredentialResolveRequest = {
|
|
45
|
+
readonly integration: ConnectorIntegration
|
|
46
|
+
readonly slot: CredentialSlot
|
|
47
|
+
readonly binding: CredentialBinding
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type CredentialResolverApi = {
|
|
51
|
+
readonly resolve: (
|
|
52
|
+
request: CredentialResolveRequest
|
|
53
|
+
) => Effect.Effect<RuntimeCredential, ConnectorError>
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class CredentialResolver extends Context.Service<CredentialResolver, CredentialResolverApi>()(
|
|
57
|
+
'@yolk-sdk/connectors/CredentialResolver'
|
|
58
|
+
) {}
|
|
59
|
+
|
|
60
|
+
export const makeCredentialBinding = (input: {
|
|
61
|
+
readonly slotId: string
|
|
62
|
+
readonly credentialRef: string
|
|
63
|
+
readonly metadata?: Readonly<Record<string, unknown>>
|
|
64
|
+
}) => CredentialBinding.make(input)
|
|
65
|
+
|
|
66
|
+
export const findCredentialBinding = (integration: ConnectorIntegration, slot: CredentialSlot) =>
|
|
67
|
+
Option.fromNullishOr(integration.credentialBindings.find(binding => binding.slotId === slot.id))
|
|
68
|
+
|
|
69
|
+
export const resolveCredential = (integration: ConnectorIntegration, slot: CredentialSlot) =>
|
|
70
|
+
Effect.gen(function* () {
|
|
71
|
+
const binding = findCredentialBinding(integration, slot)
|
|
72
|
+
|
|
73
|
+
if (Option.isNone(binding)) {
|
|
74
|
+
return yield* Effect.fail(
|
|
75
|
+
new ConnectorError({
|
|
76
|
+
cause: 'credential_binding_missing',
|
|
77
|
+
message: `Missing credential binding for slot: ${slot.id}`,
|
|
78
|
+
connectorId: integration.connectorId,
|
|
79
|
+
slotId: slot.id
|
|
80
|
+
})
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const resolver = yield* CredentialResolver
|
|
85
|
+
return yield* resolver.resolve({ integration, slot, binding: binding.value })
|
|
86
|
+
})
|
package/src/error.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as Schema from 'effect/Schema'
|
|
2
|
+
|
|
3
|
+
export const ConnectorErrorCause = Schema.Literals([
|
|
4
|
+
'action_not_found',
|
|
5
|
+
'connector_mismatch',
|
|
6
|
+
'credential_binding_missing',
|
|
7
|
+
'credential_invalid',
|
|
8
|
+
'credential_missing',
|
|
9
|
+
'validation_failed'
|
|
10
|
+
])
|
|
11
|
+
export type ConnectorErrorCause = typeof ConnectorErrorCause.Type
|
|
12
|
+
|
|
13
|
+
export class ConnectorError extends Schema.TaggedErrorClass<ConnectorError>()('ConnectorError', {
|
|
14
|
+
cause: ConnectorErrorCause,
|
|
15
|
+
message: Schema.String,
|
|
16
|
+
connectorId: Schema.optional(Schema.String),
|
|
17
|
+
actionId: Schema.optional(Schema.String),
|
|
18
|
+
slotId: Schema.optional(Schema.String),
|
|
19
|
+
underlying: Schema.optional(Schema.Unknown)
|
|
20
|
+
}) {}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { Effect } from 'effect'
|
|
2
|
+
import * as Schema from 'effect/Schema'
|
|
3
|
+
import { defineAction } from '../action.ts'
|
|
4
|
+
import { optionalStringConfig } from '../config.ts'
|
|
5
|
+
import { defineConnector } from '../connector.ts'
|
|
6
|
+
import { CredentialSlot, resolveCredential } from '../credential.ts'
|
|
7
|
+
import { ConnectorError } from '../error.ts'
|
|
8
|
+
import { ActionResult } from '../result.ts'
|
|
9
|
+
import type { ConnectorIntegration } from '../integration.ts'
|
|
10
|
+
|
|
11
|
+
export const figmaConnectorId = 'figma'
|
|
12
|
+
export const figmaOAuthSlotId = 'figma.oauth'
|
|
13
|
+
export const figmaMcpServerUrl = 'https://mcp.figma.com/mcp'
|
|
14
|
+
export const figmaOAuthRegisterUrl = 'https://api.figma.com/v1/oauth/mcp/register'
|
|
15
|
+
export const figmaOAuthAuthorizeUrl = 'https://www.figma.com/oauth/mcp'
|
|
16
|
+
export const figmaOAuthTokenUrl = 'https://api.figma.com/v1/oauth/token'
|
|
17
|
+
export const figmaMcpScope = 'mcp:connect'
|
|
18
|
+
|
|
19
|
+
export const FigmaOAuthCredentialSlot = CredentialSlot.make({
|
|
20
|
+
id: figmaOAuthSlotId,
|
|
21
|
+
kind: 'oauth',
|
|
22
|
+
requiredScopes: [figmaMcpScope]
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const resolveFigmaAccessToken = (integration: ConnectorIntegration) =>
|
|
26
|
+
Effect.gen(function* () {
|
|
27
|
+
const credential = yield* resolveCredential(integration, FigmaOAuthCredentialSlot)
|
|
28
|
+
|
|
29
|
+
switch (credential._tag) {
|
|
30
|
+
case 'OAuthCredential':
|
|
31
|
+
return {
|
|
32
|
+
accessToken: credential.accessToken,
|
|
33
|
+
expiresAt: credential.expiresAt,
|
|
34
|
+
refreshToken: optionalStringConfig(integration, 'refreshToken')
|
|
35
|
+
}
|
|
36
|
+
case 'BearerTokenCredential':
|
|
37
|
+
return {
|
|
38
|
+
accessToken: credential.token,
|
|
39
|
+
expiresAt: credential.expiresAt,
|
|
40
|
+
refreshToken: optionalStringConfig(integration, 'refreshToken')
|
|
41
|
+
}
|
|
42
|
+
case 'ApiKeyCredential':
|
|
43
|
+
return yield* Effect.fail(
|
|
44
|
+
new ConnectorError({
|
|
45
|
+
cause: 'credential_invalid',
|
|
46
|
+
message: 'Figma connector requires an OAuth or bearer token credential',
|
|
47
|
+
connectorId: integration.connectorId,
|
|
48
|
+
slotId: FigmaOAuthCredentialSlot.id
|
|
49
|
+
})
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
export class FigmaMcpAuthInput extends Schema.Class<FigmaMcpAuthInput>('FigmaMcpAuthInput')({}) {}
|
|
55
|
+
|
|
56
|
+
export class FigmaMcpTokens extends Schema.Class<FigmaMcpTokens>('FigmaMcpTokens')({
|
|
57
|
+
accessToken: Schema.String,
|
|
58
|
+
refreshToken: Schema.optional(Schema.String),
|
|
59
|
+
expiresAt: Schema.optional(Schema.Number)
|
|
60
|
+
}) {}
|
|
61
|
+
|
|
62
|
+
export class FigmaMcpClientInfo extends Schema.Class<FigmaMcpClientInfo>('FigmaMcpClientInfo')({
|
|
63
|
+
clientId: Schema.optional(Schema.String),
|
|
64
|
+
clientSecret: Schema.optional(Schema.String)
|
|
65
|
+
}) {}
|
|
66
|
+
|
|
67
|
+
export class FigmaMcpAuthOutput extends Schema.Class<FigmaMcpAuthOutput>('FigmaMcpAuthOutput')({
|
|
68
|
+
provider: Schema.Literal('figma'),
|
|
69
|
+
serverUrl: Schema.String,
|
|
70
|
+
tokens: FigmaMcpTokens,
|
|
71
|
+
clientInfo: FigmaMcpClientInfo
|
|
72
|
+
}) {}
|
|
73
|
+
|
|
74
|
+
export const makeFigmaMcpAuthData = (input: {
|
|
75
|
+
readonly accessToken: string
|
|
76
|
+
readonly refreshToken?: string
|
|
77
|
+
readonly expiresAt?: number
|
|
78
|
+
readonly clientId?: string
|
|
79
|
+
readonly clientSecret?: string
|
|
80
|
+
}) =>
|
|
81
|
+
FigmaMcpAuthOutput.make({
|
|
82
|
+
provider: 'figma',
|
|
83
|
+
serverUrl: figmaMcpServerUrl,
|
|
84
|
+
tokens: FigmaMcpTokens.make({
|
|
85
|
+
accessToken: input.accessToken,
|
|
86
|
+
refreshToken: input.refreshToken,
|
|
87
|
+
expiresAt: input.expiresAt
|
|
88
|
+
}),
|
|
89
|
+
clientInfo: FigmaMcpClientInfo.make({
|
|
90
|
+
clientId: input.clientId,
|
|
91
|
+
clientSecret: input.clientSecret
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
export const figmaMcpAuthAction = defineAction({
|
|
96
|
+
id: 'figma.mcp_auth',
|
|
97
|
+
description: 'Build Figma remote MCP auth data from host-provided OAuth credentials.',
|
|
98
|
+
inputSchema: FigmaMcpAuthInput,
|
|
99
|
+
outputSchema: FigmaMcpAuthOutput,
|
|
100
|
+
execute: ({ integration }) =>
|
|
101
|
+
Effect.gen(function* () {
|
|
102
|
+
const token = yield* resolveFigmaAccessToken(integration)
|
|
103
|
+
return ActionResult.success(
|
|
104
|
+
makeFigmaMcpAuthData({
|
|
105
|
+
accessToken: token.accessToken,
|
|
106
|
+
refreshToken: token.refreshToken,
|
|
107
|
+
expiresAt: token.expiresAt,
|
|
108
|
+
clientId: optionalStringConfig(integration, 'clientId'),
|
|
109
|
+
clientSecret: optionalStringConfig(integration, 'clientSecret')
|
|
110
|
+
})
|
|
111
|
+
)
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
export const figmaActions = [figmaMcpAuthAction]
|
|
116
|
+
|
|
117
|
+
export const FigmaConnector = defineConnector({
|
|
118
|
+
id: figmaConnectorId,
|
|
119
|
+
description: 'Figma remote MCP connector actions.',
|
|
120
|
+
actions: figmaActions
|
|
121
|
+
})
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { Effect } from 'effect'
|
|
2
|
+
import * as Schema from 'effect/Schema'
|
|
3
|
+
import { defineAction } from '../action.ts'
|
|
4
|
+
import { ConnectorHttpClient, ConnectorHttpRequest, decodeJsonResponse } from '../http.ts'
|
|
5
|
+
import { ActionResult } from '../result.ts'
|
|
6
|
+
import { googleAuthorizationHeaders } from './oauth.ts'
|
|
7
|
+
import {
|
|
8
|
+
appendNumberSearchParam,
|
|
9
|
+
appendSearchParam,
|
|
10
|
+
isSuccessStatus,
|
|
11
|
+
providerFailureFromResponse,
|
|
12
|
+
resolveGoogleAccessToken
|
|
13
|
+
} from './shared.ts'
|
|
14
|
+
|
|
15
|
+
export const googleCalendarApiBaseUrl = 'https://www.googleapis.com/calendar/v3'
|
|
16
|
+
|
|
17
|
+
export class GoogleCalendarEventDateTime extends Schema.Class<GoogleCalendarEventDateTime>(
|
|
18
|
+
'GoogleCalendarEventDateTime'
|
|
19
|
+
)({
|
|
20
|
+
date: Schema.optional(Schema.String),
|
|
21
|
+
dateTime: Schema.optional(Schema.String),
|
|
22
|
+
timeZone: Schema.optional(Schema.String)
|
|
23
|
+
}) {}
|
|
24
|
+
|
|
25
|
+
export class GoogleCalendarEvent extends Schema.Class<GoogleCalendarEvent>('GoogleCalendarEvent')({
|
|
26
|
+
id: Schema.optional(Schema.String),
|
|
27
|
+
status: Schema.optional(Schema.String),
|
|
28
|
+
htmlLink: Schema.optional(Schema.String),
|
|
29
|
+
summary: Schema.optional(Schema.String),
|
|
30
|
+
description: Schema.optional(Schema.String),
|
|
31
|
+
location: Schema.optional(Schema.String),
|
|
32
|
+
start: Schema.optional(GoogleCalendarEventDateTime),
|
|
33
|
+
end: Schema.optional(GoogleCalendarEventDateTime)
|
|
34
|
+
}) {}
|
|
35
|
+
|
|
36
|
+
export class GoogleCalendarListEventsInput extends Schema.Class<GoogleCalendarListEventsInput>(
|
|
37
|
+
'GoogleCalendarListEventsInput'
|
|
38
|
+
)({
|
|
39
|
+
calendarId: Schema.optional(Schema.String),
|
|
40
|
+
timeMin: Schema.optional(Schema.String),
|
|
41
|
+
timeMax: Schema.optional(Schema.String),
|
|
42
|
+
query: Schema.optional(Schema.String),
|
|
43
|
+
maxResults: Schema.optional(Schema.Number)
|
|
44
|
+
}) {}
|
|
45
|
+
|
|
46
|
+
export class GoogleCalendarListEventsOutput extends Schema.Class<GoogleCalendarListEventsOutput>(
|
|
47
|
+
'GoogleCalendarListEventsOutput'
|
|
48
|
+
)({
|
|
49
|
+
items: Schema.optional(Schema.Array(GoogleCalendarEvent)),
|
|
50
|
+
nextPageToken: Schema.optional(Schema.String)
|
|
51
|
+
}) {}
|
|
52
|
+
|
|
53
|
+
export class GoogleCalendarCreateEventInput extends Schema.Class<GoogleCalendarCreateEventInput>(
|
|
54
|
+
'GoogleCalendarCreateEventInput'
|
|
55
|
+
)({
|
|
56
|
+
calendarId: Schema.optional(Schema.String),
|
|
57
|
+
summary: Schema.String,
|
|
58
|
+
description: Schema.optional(Schema.String),
|
|
59
|
+
location: Schema.optional(Schema.String),
|
|
60
|
+
start: GoogleCalendarEventDateTime,
|
|
61
|
+
end: GoogleCalendarEventDateTime
|
|
62
|
+
}) {}
|
|
63
|
+
|
|
64
|
+
const calendarIdOrPrimary = (calendarId: string | undefined) => calendarId ?? 'primary'
|
|
65
|
+
|
|
66
|
+
export const googleCalendarListEventsAction = defineAction({
|
|
67
|
+
id: 'calendar.list_events',
|
|
68
|
+
description: 'List Google Calendar events for the integration account.',
|
|
69
|
+
inputSchema: GoogleCalendarListEventsInput,
|
|
70
|
+
outputSchema: GoogleCalendarListEventsOutput,
|
|
71
|
+
execute: ({ integration, input }) =>
|
|
72
|
+
Effect.gen(function* () {
|
|
73
|
+
const token = yield* resolveGoogleAccessToken(integration)
|
|
74
|
+
const http = yield* ConnectorHttpClient
|
|
75
|
+
const params = new URLSearchParams()
|
|
76
|
+
appendSearchParam(params, 'timeMin', input.timeMin)
|
|
77
|
+
appendSearchParam(params, 'timeMax', input.timeMax)
|
|
78
|
+
appendSearchParam(params, 'q', input.query)
|
|
79
|
+
appendNumberSearchParam(params, 'maxResults', input.maxResults)
|
|
80
|
+
const query = params.toString()
|
|
81
|
+
const url = `${googleCalendarApiBaseUrl}/calendars/${encodeURIComponent(calendarIdOrPrimary(input.calendarId))}/events${query === '' ? '' : `?${query}`}`
|
|
82
|
+
const response = yield* http.request(
|
|
83
|
+
ConnectorHttpRequest.make({
|
|
84
|
+
method: 'GET',
|
|
85
|
+
url,
|
|
86
|
+
headers: googleAuthorizationHeaders(token)
|
|
87
|
+
})
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if (!isSuccessStatus(response.status)) {
|
|
91
|
+
return providerFailureFromResponse({
|
|
92
|
+
code: 'calendar_list_events_failed',
|
|
93
|
+
message: 'Google Calendar list events failed',
|
|
94
|
+
status: response.status,
|
|
95
|
+
body: response.body
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const output = yield* decodeJsonResponse(GoogleCalendarListEventsOutput, response)
|
|
100
|
+
return ActionResult.success(output)
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
export const googleCalendarCreateEventAction = defineAction({
|
|
105
|
+
id: 'calendar.create_event',
|
|
106
|
+
description: 'Create a Google Calendar event for the integration account.',
|
|
107
|
+
inputSchema: GoogleCalendarCreateEventInput,
|
|
108
|
+
outputSchema: GoogleCalendarEvent,
|
|
109
|
+
execute: ({ integration, input }) =>
|
|
110
|
+
Effect.gen(function* () {
|
|
111
|
+
const token = yield* resolveGoogleAccessToken(integration)
|
|
112
|
+
const http = yield* ConnectorHttpClient
|
|
113
|
+
const response = yield* http.request(
|
|
114
|
+
ConnectorHttpRequest.make({
|
|
115
|
+
method: 'POST',
|
|
116
|
+
url: `${googleCalendarApiBaseUrl}/calendars/${encodeURIComponent(calendarIdOrPrimary(input.calendarId))}/events`,
|
|
117
|
+
headers: {
|
|
118
|
+
...googleAuthorizationHeaders(token),
|
|
119
|
+
'content-type': 'application/json'
|
|
120
|
+
},
|
|
121
|
+
body: JSON.stringify({
|
|
122
|
+
summary: input.summary,
|
|
123
|
+
description: input.description,
|
|
124
|
+
location: input.location,
|
|
125
|
+
start: input.start,
|
|
126
|
+
end: input.end
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if (!isSuccessStatus(response.status)) {
|
|
132
|
+
return providerFailureFromResponse({
|
|
133
|
+
code: 'calendar_create_event_failed',
|
|
134
|
+
message: 'Google Calendar create event failed',
|
|
135
|
+
status: response.status,
|
|
136
|
+
body: response.body
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const output = yield* decodeJsonResponse(GoogleCalendarEvent, response)
|
|
141
|
+
return ActionResult.success(output)
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
export const googleCalendarActions = [googleCalendarListEventsAction, googleCalendarCreateEventAction]
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { Effect } from 'effect'
|
|
2
|
+
import * as Schema from 'effect/Schema'
|
|
3
|
+
import { defineAction } from '../action.ts'
|
|
4
|
+
import { ConnectorHttpClient, ConnectorHttpRequest, decodeJsonResponse } from '../http.ts'
|
|
5
|
+
import { ActionResult } from '../result.ts'
|
|
6
|
+
import { googleAuthorizationHeaders } from './oauth.ts'
|
|
7
|
+
import {
|
|
8
|
+
appendNumberSearchParam,
|
|
9
|
+
appendSearchParam,
|
|
10
|
+
isSuccessStatus,
|
|
11
|
+
providerFailureFromResponse,
|
|
12
|
+
resolveGoogleAccessToken
|
|
13
|
+
} from './shared.ts'
|
|
14
|
+
|
|
15
|
+
export const googleGmailApiBaseUrl = 'https://gmail.googleapis.com/gmail/v1'
|
|
16
|
+
|
|
17
|
+
export class GmailMessageRef extends Schema.Class<GmailMessageRef>('GmailMessageRef')({
|
|
18
|
+
id: Schema.String,
|
|
19
|
+
threadId: Schema.optional(Schema.String)
|
|
20
|
+
}) {}
|
|
21
|
+
|
|
22
|
+
export class GmailSearchInput extends Schema.Class<GmailSearchInput>('GmailSearchInput')({
|
|
23
|
+
query: Schema.optional(Schema.String),
|
|
24
|
+
maxResults: Schema.optional(Schema.Number)
|
|
25
|
+
}) {}
|
|
26
|
+
|
|
27
|
+
export class GmailSearchOutput extends Schema.Class<GmailSearchOutput>('GmailSearchOutput')({
|
|
28
|
+
messages: Schema.optional(Schema.Array(GmailMessageRef)),
|
|
29
|
+
nextPageToken: Schema.optional(Schema.String),
|
|
30
|
+
resultSizeEstimate: Schema.optional(Schema.Number)
|
|
31
|
+
}) {}
|
|
32
|
+
|
|
33
|
+
export class GmailGetMessageInput extends Schema.Class<GmailGetMessageInput>('GmailGetMessageInput')({
|
|
34
|
+
id: Schema.String,
|
|
35
|
+
format: Schema.optional(Schema.Literals(['minimal', 'full', 'raw', 'metadata']))
|
|
36
|
+
}) {}
|
|
37
|
+
|
|
38
|
+
export const GmailMessagePayloadHeader = Schema.Struct({
|
|
39
|
+
name: Schema.String,
|
|
40
|
+
value: Schema.String
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
export class GmailMessageOutput extends Schema.Class<GmailMessageOutput>('GmailMessageOutput')({
|
|
44
|
+
id: Schema.String,
|
|
45
|
+
threadId: Schema.optional(Schema.String),
|
|
46
|
+
snippet: Schema.optional(Schema.String),
|
|
47
|
+
labelIds: Schema.optional(Schema.Array(Schema.String)),
|
|
48
|
+
payload: Schema.optional(
|
|
49
|
+
Schema.Struct({
|
|
50
|
+
headers: Schema.optional(Schema.Array(GmailMessagePayloadHeader))
|
|
51
|
+
})
|
|
52
|
+
),
|
|
53
|
+
raw: Schema.optional(Schema.String)
|
|
54
|
+
}) {}
|
|
55
|
+
|
|
56
|
+
export const gmailSearchAction = defineAction({
|
|
57
|
+
id: 'gmail.search',
|
|
58
|
+
description: 'Search Gmail messages for the integration account.',
|
|
59
|
+
inputSchema: GmailSearchInput,
|
|
60
|
+
outputSchema: GmailSearchOutput,
|
|
61
|
+
execute: ({ integration, input }) =>
|
|
62
|
+
Effect.gen(function* () {
|
|
63
|
+
const token = yield* resolveGoogleAccessToken(integration)
|
|
64
|
+
const http = yield* ConnectorHttpClient
|
|
65
|
+
const params = new URLSearchParams()
|
|
66
|
+
appendSearchParam(params, 'q', input.query)
|
|
67
|
+
appendNumberSearchParam(params, 'maxResults', input.maxResults)
|
|
68
|
+
const query = params.toString()
|
|
69
|
+
const url = `${googleGmailApiBaseUrl}/users/me/messages${query === '' ? '' : `?${query}`}`
|
|
70
|
+
const response = yield* http.request(
|
|
71
|
+
ConnectorHttpRequest.make({
|
|
72
|
+
method: 'GET',
|
|
73
|
+
url,
|
|
74
|
+
headers: googleAuthorizationHeaders(token)
|
|
75
|
+
})
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if (!isSuccessStatus(response.status)) {
|
|
79
|
+
return providerFailureFromResponse({
|
|
80
|
+
code: 'gmail_search_failed',
|
|
81
|
+
message: 'Gmail search failed',
|
|
82
|
+
status: response.status,
|
|
83
|
+
body: response.body
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const output = yield* decodeJsonResponse(GmailSearchOutput, response)
|
|
88
|
+
return ActionResult.success(output)
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
export const gmailGetMessageAction = defineAction({
|
|
93
|
+
id: 'gmail.get_message',
|
|
94
|
+
description: 'Get a Gmail message by id for the integration account.',
|
|
95
|
+
inputSchema: GmailGetMessageInput,
|
|
96
|
+
outputSchema: GmailMessageOutput,
|
|
97
|
+
execute: ({ integration, input }) =>
|
|
98
|
+
Effect.gen(function* () {
|
|
99
|
+
const token = yield* resolveGoogleAccessToken(integration)
|
|
100
|
+
const http = yield* ConnectorHttpClient
|
|
101
|
+
const params = new URLSearchParams()
|
|
102
|
+
appendSearchParam(params, 'format', input.format)
|
|
103
|
+
const query = params.toString()
|
|
104
|
+
const url = `${googleGmailApiBaseUrl}/users/me/messages/${encodeURIComponent(input.id)}${query === '' ? '' : `?${query}`}`
|
|
105
|
+
const response = yield* http.request(
|
|
106
|
+
ConnectorHttpRequest.make({
|
|
107
|
+
method: 'GET',
|
|
108
|
+
url,
|
|
109
|
+
headers: googleAuthorizationHeaders(token)
|
|
110
|
+
})
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if (!isSuccessStatus(response.status)) {
|
|
114
|
+
return providerFailureFromResponse({
|
|
115
|
+
code: 'gmail_get_message_failed',
|
|
116
|
+
message: 'Gmail get message failed',
|
|
117
|
+
status: response.status,
|
|
118
|
+
body: response.body
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const output = yield* decodeJsonResponse(GmailMessageOutput, response)
|
|
123
|
+
return ActionResult.success(output)
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
export const gmailActions = [gmailSearchAction, gmailGetMessageAction]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export {
|
|
2
|
+
googleCalendarActions,
|
|
3
|
+
googleCalendarApiBaseUrl,
|
|
4
|
+
googleCalendarCreateEventAction,
|
|
5
|
+
googleCalendarListEventsAction,
|
|
6
|
+
GoogleCalendarCreateEventInput,
|
|
7
|
+
GoogleCalendarEvent,
|
|
8
|
+
GoogleCalendarEventDateTime,
|
|
9
|
+
GoogleCalendarListEventsInput,
|
|
10
|
+
GoogleCalendarListEventsOutput
|
|
11
|
+
} from './calendar.ts'
|
|
12
|
+
export {
|
|
13
|
+
gmailActions,
|
|
14
|
+
gmailGetMessageAction,
|
|
15
|
+
GmailGetMessageInput,
|
|
16
|
+
gmailSearchAction,
|
|
17
|
+
GmailMessageOutput,
|
|
18
|
+
GmailMessageRef,
|
|
19
|
+
GmailSearchInput,
|
|
20
|
+
GmailSearchOutput,
|
|
21
|
+
googleGmailApiBaseUrl
|
|
22
|
+
} from './gmail.ts'
|
|
23
|
+
export {
|
|
24
|
+
GoogleOAuthCredentialSlot,
|
|
25
|
+
googleAuthorizationHeaders,
|
|
26
|
+
googleCalendarEventsScope,
|
|
27
|
+
googleCalendarReadonlyScope,
|
|
28
|
+
googleConnectorId,
|
|
29
|
+
googleGmailReadonlyScope,
|
|
30
|
+
googleGmailSendScope,
|
|
31
|
+
googleOAuthAuthorizeUrl,
|
|
32
|
+
googleOAuthSlotId,
|
|
33
|
+
googleOAuthTokenUrl
|
|
34
|
+
} from './oauth.ts'
|
|
35
|
+
export { resolveGoogleAccessToken } from './shared.ts'
|
|
36
|
+
|
|
37
|
+
import { defineConnector } from '../connector.ts'
|
|
38
|
+
import { googleCalendarActions } from './calendar.ts'
|
|
39
|
+
import { gmailActions } from './gmail.ts'
|
|
40
|
+
import { googleConnectorId } from './oauth.ts'
|
|
41
|
+
|
|
42
|
+
export const GoogleConnector = defineConnector({
|
|
43
|
+
id: googleConnectorId,
|
|
44
|
+
description: 'Google Gmail and Calendar connector actions.',
|
|
45
|
+
actions: [...gmailActions, ...googleCalendarActions]
|
|
46
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { CredentialSlot } from '../credential.ts'
|
|
2
|
+
|
|
3
|
+
export const googleConnectorId = 'google'
|
|
4
|
+
export const googleOAuthSlotId = 'google.oauth'
|
|
5
|
+
export const googleOAuthAuthorizeUrl = 'https://accounts.google.com/o/oauth2/v2/auth'
|
|
6
|
+
export const googleOAuthTokenUrl = 'https://oauth2.googleapis.com/token'
|
|
7
|
+
|
|
8
|
+
export const googleGmailReadonlyScope = 'https://www.googleapis.com/auth/gmail.readonly'
|
|
9
|
+
export const googleGmailSendScope = 'https://www.googleapis.com/auth/gmail.send'
|
|
10
|
+
export const googleCalendarReadonlyScope = 'https://www.googleapis.com/auth/calendar.readonly'
|
|
11
|
+
export const googleCalendarEventsScope = 'https://www.googleapis.com/auth/calendar.events'
|
|
12
|
+
|
|
13
|
+
export const GoogleOAuthCredentialSlot = CredentialSlot.make({
|
|
14
|
+
id: googleOAuthSlotId,
|
|
15
|
+
kind: 'oauth',
|
|
16
|
+
requiredScopes: [
|
|
17
|
+
googleGmailReadonlyScope,
|
|
18
|
+
googleGmailSendScope,
|
|
19
|
+
googleCalendarReadonlyScope,
|
|
20
|
+
googleCalendarEventsScope
|
|
21
|
+
]
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
export const googleAuthorizationHeaders = (accessToken: string) => ({
|
|
25
|
+
authorization: `Bearer ${accessToken}`
|
|
26
|
+
})
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Effect } from 'effect'
|
|
2
|
+
import { resolveCredential } from '../credential.ts'
|
|
3
|
+
import { ConnectorError } from '../error.ts'
|
|
4
|
+
import { ActionResult, ProviderFailure } from '../result.ts'
|
|
5
|
+
import type { ConnectorIntegration } from '../integration.ts'
|
|
6
|
+
import { GoogleOAuthCredentialSlot } from './oauth.ts'
|
|
7
|
+
|
|
8
|
+
export const resolveGoogleAccessToken = (integration: ConnectorIntegration) =>
|
|
9
|
+
Effect.gen(function* () {
|
|
10
|
+
const credential = yield* resolveCredential(integration, GoogleOAuthCredentialSlot)
|
|
11
|
+
|
|
12
|
+
switch (credential._tag) {
|
|
13
|
+
case 'OAuthCredential':
|
|
14
|
+
return credential.accessToken
|
|
15
|
+
case 'BearerTokenCredential':
|
|
16
|
+
return credential.token
|
|
17
|
+
case 'ApiKeyCredential':
|
|
18
|
+
return yield* Effect.fail(
|
|
19
|
+
new ConnectorError({
|
|
20
|
+
cause: 'credential_invalid',
|
|
21
|
+
message: 'Google connector requires an OAuth or bearer token credential',
|
|
22
|
+
connectorId: integration.connectorId,
|
|
23
|
+
slotId: GoogleOAuthCredentialSlot.id
|
|
24
|
+
})
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
export const providerFailureFromResponse = (input: {
|
|
30
|
+
readonly code: string
|
|
31
|
+
readonly message: string
|
|
32
|
+
readonly status: number
|
|
33
|
+
readonly body: string
|
|
34
|
+
}) =>
|
|
35
|
+
ActionResult.failure(
|
|
36
|
+
new ProviderFailure({
|
|
37
|
+
code: input.code,
|
|
38
|
+
message: input.message,
|
|
39
|
+
status: input.status,
|
|
40
|
+
underlying: input.body
|
|
41
|
+
})
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
export const isSuccessStatus = (status: number) => status >= 200 && status < 300
|
|
45
|
+
|
|
46
|
+
export const appendSearchParam = (params: URLSearchParams, key: string, value: string | undefined) => {
|
|
47
|
+
if (value !== undefined && value.trim() !== '') {
|
|
48
|
+
params.set(key, value)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const appendNumberSearchParam = (params: URLSearchParams, key: string, value: number | undefined) => {
|
|
53
|
+
if (value !== undefined) {
|
|
54
|
+
params.set(key, String(value))
|
|
55
|
+
}
|
|
56
|
+
}
|