@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.
Files changed (106) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +195 -0
  3. package/dist/action.d.mts +37 -0
  4. package/dist/action.d.mts.map +1 -0
  5. package/dist/action.mjs +24 -0
  6. package/dist/action.mjs.map +1 -0
  7. package/dist/agent.d.mts +21 -0
  8. package/dist/agent.d.mts.map +1 -0
  9. package/dist/agent.mjs +66 -0
  10. package/dist/agent.mjs.map +1 -0
  11. package/dist/config.d.mts +10 -0
  12. package/dist/config.d.mts.map +1 -0
  13. package/dist/config.mjs +21 -0
  14. package/dist/config.mjs.map +1 -0
  15. package/dist/connector.d.mts +27 -0
  16. package/dist/connector.d.mts.map +1 -0
  17. package/dist/connector.mjs +32 -0
  18. package/dist/connector.mjs.map +1 -0
  19. package/dist/credential.d.mts +62 -0
  20. package/dist/credential.d.mts.map +1 -0
  21. package/dist/credential.mjs +62 -0
  22. package/dist/credential.mjs.map +1 -0
  23. package/dist/error.d.mts +17 -0
  24. package/dist/error.d.mts.map +1 -0
  25. package/dist/error.mjs +22 -0
  26. package/dist/error.mjs.map +1 -0
  27. package/dist/figma/index.d.mts +48 -0
  28. package/dist/figma/index.d.mts.map +1 -0
  29. package/dist/figma/index.mjs +97 -0
  30. package/dist/figma/index.mjs.map +1 -0
  31. package/dist/google/calendar.d.mts +53 -0
  32. package/dist/google/calendar.d.mts.map +1 -0
  33. package/dist/google/calendar.mjs +111 -0
  34. package/dist/google/calendar.mjs.map +1 -0
  35. package/dist/google/gmail.d.mts +53 -0
  36. package/dist/google/gmail.d.mts.map +1 -0
  37. package/dist/google/gmail.mjs +103 -0
  38. package/dist/google/gmail.mjs.map +1 -0
  39. package/dist/google/index.d.mts +14 -0
  40. package/dist/google/index.d.mts.map +1 -0
  41. package/dist/google/index.mjs +15 -0
  42. package/dist/google/index.mjs.map +1 -0
  43. package/dist/google/oauth.d.mts +18 -0
  44. package/dist/google/oauth.d.mts.map +1 -0
  45. package/dist/google/oauth.mjs +25 -0
  46. package/dist/google/oauth.mjs.map +1 -0
  47. package/dist/google/shared.d.mts +20 -0
  48. package/dist/google/shared.d.mts.map +1 -0
  49. package/dist/google/shared.mjs +36 -0
  50. package/dist/google/shared.mjs.map +1 -0
  51. package/dist/http.d.mts +32 -0
  52. package/dist/http.d.mts.map +1 -0
  53. package/dist/http.mjs +36 -0
  54. package/dist/http.mjs.map +1 -0
  55. package/dist/index.d.mts +9 -0
  56. package/dist/index.mjs +9 -0
  57. package/dist/integration.d.mts +24 -0
  58. package/dist/integration.d.mts.map +1 -0
  59. package/dist/integration.mjs +22 -0
  60. package/dist/integration.mjs.map +1 -0
  61. package/dist/linkedin-search/index.d.mts +60 -0
  62. package/dist/linkedin-search/index.d.mts.map +1 -0
  63. package/dist/linkedin-search/index.mjs +162 -0
  64. package/dist/linkedin-search/index.mjs.map +1 -0
  65. package/dist/notion/index.d.mts +69 -0
  66. package/dist/notion/index.d.mts.map +1 -0
  67. package/dist/notion/index.mjs +169 -0
  68. package/dist/notion/index.mjs.map +1 -0
  69. package/dist/r2-storage/index.d.mts +48 -0
  70. package/dist/r2-storage/index.d.mts.map +1 -0
  71. package/dist/r2-storage/index.mjs +91 -0
  72. package/dist/r2-storage/index.mjs.map +1 -0
  73. package/dist/result.d.mts +32 -0
  74. package/dist/result.d.mts.map +1 -0
  75. package/dist/result.mjs +23 -0
  76. package/dist/result.mjs.map +1 -0
  77. package/dist/telegram/index.d.mts +34 -0
  78. package/dist/telegram/index.d.mts.map +1 -0
  79. package/dist/telegram/index.mjs +109 -0
  80. package/dist/telegram/index.mjs.map +1 -0
  81. package/dist/todoist/index.d.mts +70 -0
  82. package/dist/todoist/index.d.mts.map +1 -0
  83. package/dist/todoist/index.mjs +176 -0
  84. package/dist/todoist/index.mjs.map +1 -0
  85. package/package.json +96 -0
  86. package/src/action.ts +75 -0
  87. package/src/agent.ts +120 -0
  88. package/src/config.ts +28 -0
  89. package/src/connector.ts +62 -0
  90. package/src/credential.ts +86 -0
  91. package/src/error.ts +20 -0
  92. package/src/figma/index.ts +121 -0
  93. package/src/google/calendar.ts +145 -0
  94. package/src/google/gmail.ts +127 -0
  95. package/src/google/index.ts +46 -0
  96. package/src/google/oauth.ts +26 -0
  97. package/src/google/shared.ts +56 -0
  98. package/src/http.ts +51 -0
  99. package/src/index.ts +36 -0
  100. package/src/integration.ts +28 -0
  101. package/src/linkedin-search/index.ts +217 -0
  102. package/src/notion/index.ts +234 -0
  103. package/src/r2-storage/index.ts +118 -0
  104. package/src/result.ts +35 -0
  105. package/src/telegram/index.ts +144 -0
  106. package/src/todoist/index.ts +227 -0
@@ -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
+ }