@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
package/src/http.ts ADDED
@@ -0,0 +1,51 @@
1
+ import { Context, Effect } from 'effect'
2
+ import * as Schema from 'effect/Schema'
3
+ import { ConnectorError } from './error.ts'
4
+
5
+ export const HttpMethod = Schema.Literals(['GET', 'POST', 'PATCH', 'PUT', 'DELETE'])
6
+ export type HttpMethod = typeof HttpMethod.Type
7
+
8
+ export class ConnectorHttpRequest extends Schema.Class<ConnectorHttpRequest>('ConnectorHttpRequest')({
9
+ method: HttpMethod,
10
+ url: Schema.String,
11
+ headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),
12
+ body: Schema.optional(Schema.String)
13
+ }) {}
14
+
15
+ export class ConnectorHttpResponse extends Schema.Class<ConnectorHttpResponse>('ConnectorHttpResponse')({
16
+ status: Schema.Number,
17
+ headers: Schema.Record(Schema.String, Schema.String),
18
+ body: Schema.String
19
+ }) {}
20
+
21
+ export type ConnectorHttpClientApi = {
22
+ readonly request: (request: ConnectorHttpRequest) => Effect.Effect<ConnectorHttpResponse, ConnectorError>
23
+ }
24
+
25
+ export class ConnectorHttpClient extends Context.Service<ConnectorHttpClient, ConnectorHttpClientApi>()(
26
+ '@yolk-sdk/connectors/ConnectorHttpClient'
27
+ ) {}
28
+
29
+ type JsonResponseSchema<A> = Schema.Schema<A> & { readonly DecodingServices: never }
30
+
31
+ export const decodeJsonResponse = <A>(schema: JsonResponseSchema<A>, response: ConnectorHttpResponse) =>
32
+ Schema.decodeUnknownEffect(Schema.UnknownFromJsonString)(response.body).pipe(
33
+ Effect.mapError(error =>
34
+ new ConnectorError({
35
+ cause: 'validation_failed',
36
+ message: 'Invalid JSON response',
37
+ underlying: error
38
+ })
39
+ ),
40
+ Effect.flatMap(value =>
41
+ Schema.decodeUnknownEffect(schema)(value).pipe(
42
+ Effect.mapError(error =>
43
+ new ConnectorError({
44
+ cause: 'validation_failed',
45
+ message: 'Invalid response shape',
46
+ underlying: error
47
+ })
48
+ )
49
+ )
50
+ )
51
+ )
package/src/index.ts ADDED
@@ -0,0 +1,36 @@
1
+ export { defineAction } from './action.ts'
2
+ export type {
3
+ ActionExecutionInput,
4
+ ConnectorAction,
5
+ DefineActionOptions,
6
+ UnknownActionExecutionInput
7
+ } from './action.ts'
8
+ export { defineConnector } from './connector.ts'
9
+ export type { Connector, ConnectorInvokeInput, DefineConnectorOptions } from './connector.ts'
10
+ export {
11
+ ApiKeyCredential,
12
+ BearerTokenCredential,
13
+ CredentialBinding,
14
+ CredentialKind,
15
+ CredentialResolver,
16
+ CredentialSlot,
17
+ OAuthCredential,
18
+ RuntimeCredential,
19
+ findCredentialBinding,
20
+ makeCredentialBinding,
21
+ resolveCredential
22
+ } from './credential.ts'
23
+ export type { CredentialResolveRequest, CredentialResolverApi } from './credential.ts'
24
+ export { ConnectorError, ConnectorErrorCause } from './error.ts'
25
+ export { optionalStringConfig, requiredStringConfig } from './config.ts'
26
+ export {
27
+ ConnectorHttpClient,
28
+ ConnectorHttpRequest,
29
+ ConnectorHttpResponse,
30
+ HttpMethod,
31
+ decodeJsonResponse
32
+ } from './http.ts'
33
+ export type { ConnectorHttpClientApi } from './http.ts'
34
+ export { ConnectorIntegration, IntegrationConfig, makeIntegration } from './integration.ts'
35
+ export { ActionResult, ProviderFailure } from './result.ts'
36
+ export type { ActionResult as ActionResultType } from './result.ts'
@@ -0,0 +1,28 @@
1
+ import * as Schema from 'effect/Schema'
2
+ import { CredentialBinding } from './credential.ts'
3
+
4
+ export const IntegrationConfig = Schema.Record(Schema.String, Schema.Unknown)
5
+ export type IntegrationConfig = typeof IntegrationConfig.Type
6
+
7
+ export class ConnectorIntegration extends Schema.Class<ConnectorIntegration>('ConnectorIntegration')({
8
+ id: Schema.optional(Schema.String),
9
+ connectorId: Schema.String,
10
+ config: IntegrationConfig,
11
+ credentialBindings: Schema.Array(CredentialBinding),
12
+ metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown))
13
+ }) {}
14
+
15
+ export const makeIntegration = (input: {
16
+ readonly id?: string
17
+ readonly connectorId: string
18
+ readonly config?: IntegrationConfig
19
+ readonly credentialBindings?: ReadonlyArray<CredentialBinding>
20
+ readonly metadata?: Readonly<Record<string, unknown>>
21
+ }) =>
22
+ ConnectorIntegration.make({
23
+ id: input.id,
24
+ connectorId: input.connectorId,
25
+ config: input.config ?? {},
26
+ credentialBindings: input.credentialBindings ?? [],
27
+ metadata: input.metadata
28
+ })
@@ -0,0 +1,217 @@
1
+ import { Effect } from 'effect'
2
+ import * as Schema from 'effect/Schema'
3
+ import { defineAction } from '../action.ts'
4
+ import { defineConnector } from '../connector.ts'
5
+ import { CredentialSlot, resolveCredential } from '../credential.ts'
6
+ import { ConnectorError } from '../error.ts'
7
+ import { ConnectorHttpClient, ConnectorHttpRequest, decodeJsonResponse } from '../http.ts'
8
+ import { ActionResult, ProviderFailure } from '../result.ts'
9
+ import type { ConnectorIntegration } from '../integration.ts'
10
+
11
+ export const linkedInSearchConnectorId = 'linkedin-search'
12
+ export const exaApiKeySlotId = 'linkedin-search.exa_api_key'
13
+ export const enrichLayerApiKeySlotId = 'linkedin-search.enrich_layer_api_key'
14
+ export const exaApiBaseUrl = 'https://api.exa.ai'
15
+ export const enrichLayerApiBaseUrl = 'https://enrichlayer.com/api/v2'
16
+
17
+ export const ExaApiKeySlot = CredentialSlot.make({ id: exaApiKeySlotId, kind: 'api_key' })
18
+ export const EnrichLayerApiKeySlot = CredentialSlot.make({
19
+ id: enrichLayerApiKeySlotId,
20
+ kind: 'api_key'
21
+ })
22
+
23
+ const resolveApiToken = (integration: ConnectorIntegration, slot: CredentialSlot) =>
24
+ Effect.gen(function* () {
25
+ const credential = yield* resolveCredential(integration, slot)
26
+
27
+ switch (credential._tag) {
28
+ case 'ApiKeyCredential':
29
+ return credential.key
30
+ case 'BearerTokenCredential':
31
+ return credential.token
32
+ case 'OAuthCredential':
33
+ return credential.accessToken
34
+ }
35
+ })
36
+
37
+ const isSuccessStatus = (status: number) => status >= 200 && status < 300
38
+
39
+ const linkedInProviderFailure = (input: {
40
+ readonly code: string
41
+ readonly message: string
42
+ readonly status: number
43
+ readonly body: string
44
+ }) =>
45
+ ActionResult.failure(
46
+ new ProviderFailure({
47
+ code: input.code,
48
+ message: input.message,
49
+ status: input.status,
50
+ underlying: input.body
51
+ })
52
+ )
53
+
54
+ export class LinkedInSearchInput extends Schema.Class<LinkedInSearchInput>('LinkedInSearchInput')({
55
+ query: Schema.String,
56
+ numResults: Schema.optional(Schema.Number)
57
+ }) {}
58
+
59
+ export const LinkedInSearchResult = Schema.Struct({
60
+ title: Schema.optional(Schema.String),
61
+ url: Schema.optional(Schema.String),
62
+ text: Schema.optional(Schema.String),
63
+ publishedDate: Schema.optional(Schema.String),
64
+ author: Schema.optional(Schema.String)
65
+ })
66
+
67
+ export class LinkedInSearchOutput extends Schema.Class<LinkedInSearchOutput>('LinkedInSearchOutput')({
68
+ results: Schema.Array(LinkedInSearchResult),
69
+ totalResults: Schema.optional(Schema.Number)
70
+ }) {}
71
+
72
+ export class LinkedInProfileInput extends Schema.Class<LinkedInProfileInput>('LinkedInProfileInput')({
73
+ linkedinUrl: Schema.String
74
+ }) {}
75
+
76
+ export class LinkedInProfileOutput extends Schema.Class<LinkedInProfileOutput>('LinkedInProfileOutput')({
77
+ profile: Schema.Unknown
78
+ }) {}
79
+
80
+ export class LinkedInEmailOutput extends Schema.Class<LinkedInEmailOutput>('LinkedInEmailOutput')({
81
+ email: Schema.NullOr(Schema.String),
82
+ status: Schema.optional(Schema.String),
83
+ message: Schema.optional(Schema.String)
84
+ }) {}
85
+
86
+ const missingEnrichLayer = (integration: ConnectorIntegration) =>
87
+ new ConnectorError({
88
+ cause: 'credential_binding_missing',
89
+ message: 'Missing Enrich Layer credential binding',
90
+ connectorId: integration.connectorId,
91
+ slotId: EnrichLayerApiKeySlot.id
92
+ })
93
+
94
+ export const linkedInSearchAction = defineAction({
95
+ id: 'linkedin_search.search',
96
+ description: 'Search LinkedIn people results through Exa.',
97
+ inputSchema: LinkedInSearchInput,
98
+ outputSchema: LinkedInSearchOutput,
99
+ execute: ({ integration, input }) =>
100
+ Effect.gen(function* () {
101
+ const token = yield* resolveApiToken(integration, ExaApiKeySlot)
102
+ const http = yield* ConnectorHttpClient
103
+ const response = yield* http.request(
104
+ ConnectorHttpRequest.make({
105
+ method: 'POST',
106
+ url: `${exaApiBaseUrl}/search`,
107
+ headers: {
108
+ authorization: `Bearer ${token}`,
109
+ 'content-type': 'application/json'
110
+ },
111
+ body: JSON.stringify({
112
+ query: input.query,
113
+ category: 'people',
114
+ numResults: input.numResults ?? 10,
115
+ type: 'auto',
116
+ contents: { text: true }
117
+ })
118
+ })
119
+ )
120
+
121
+ if (!isSuccessStatus(response.status)) {
122
+ return linkedInProviderFailure({
123
+ code: 'linkedin_search_failed',
124
+ message: 'LinkedIn search failed',
125
+ status: response.status,
126
+ body: response.body
127
+ })
128
+ }
129
+
130
+ const decoded = yield* decodeJsonResponse(
131
+ Schema.Struct({ results: Schema.Array(LinkedInSearchResult), totalResults: Schema.optional(Schema.Number) }),
132
+ response
133
+ )
134
+ return ActionResult.success(LinkedInSearchOutput.make(decoded))
135
+ })
136
+ })
137
+
138
+ export const linkedInProfileAction = defineAction({
139
+ id: 'linkedin_search.profile',
140
+ description: 'Fetch a LinkedIn profile through Enrich Layer.',
141
+ inputSchema: LinkedInProfileInput,
142
+ outputSchema: LinkedInProfileOutput,
143
+ execute: ({ integration, input }) =>
144
+ Effect.gen(function* () {
145
+ const token = yield* resolveApiToken(integration, EnrichLayerApiKeySlot).pipe(
146
+ Effect.catchTag('ConnectorError', () => Effect.fail(missingEnrichLayer(integration)))
147
+ )
148
+ const http = yield* ConnectorHttpClient
149
+ const response = yield* http.request(
150
+ ConnectorHttpRequest.make({
151
+ method: 'GET',
152
+ url: `${enrichLayerApiBaseUrl}/profile?linkedin_profile_url=${encodeURIComponent(input.linkedinUrl)}`,
153
+ headers: { authorization: `Bearer ${token}` }
154
+ })
155
+ )
156
+
157
+ if (!isSuccessStatus(response.status)) {
158
+ return linkedInProviderFailure({
159
+ code: 'linkedin_profile_failed',
160
+ message: 'LinkedIn profile fetch failed',
161
+ status: response.status,
162
+ body: response.body
163
+ })
164
+ }
165
+
166
+ const profile = yield* decodeJsonResponse(Schema.Unknown, response)
167
+ return ActionResult.success(LinkedInProfileOutput.make({ profile }))
168
+ })
169
+ })
170
+
171
+ export const linkedInEmailAction = defineAction({
172
+ id: 'linkedin_search.email',
173
+ description: 'Fetch a LinkedIn profile email through Enrich Layer.',
174
+ inputSchema: LinkedInProfileInput,
175
+ outputSchema: LinkedInEmailOutput,
176
+ execute: ({ integration, input }) =>
177
+ Effect.gen(function* () {
178
+ const token = yield* resolveApiToken(integration, EnrichLayerApiKeySlot).pipe(
179
+ Effect.catchTag('ConnectorError', () => Effect.fail(missingEnrichLayer(integration)))
180
+ )
181
+ const http = yield* ConnectorHttpClient
182
+ const response = yield* http.request(
183
+ ConnectorHttpRequest.make({
184
+ method: 'GET',
185
+ url: `${enrichLayerApiBaseUrl}/profile/email?linkedin_profile_url=${encodeURIComponent(input.linkedinUrl)}`,
186
+ headers: { authorization: `Bearer ${token}` }
187
+ })
188
+ )
189
+
190
+ if (!isSuccessStatus(response.status)) {
191
+ return linkedInProviderFailure({
192
+ code: 'linkedin_email_failed',
193
+ message: 'LinkedIn email fetch failed',
194
+ status: response.status,
195
+ body: response.body
196
+ })
197
+ }
198
+
199
+ const decoded = yield* decodeJsonResponse(
200
+ Schema.Struct({
201
+ email: Schema.NullOr(Schema.String),
202
+ status: Schema.optional(Schema.String),
203
+ message: Schema.optional(Schema.String)
204
+ }),
205
+ response
206
+ )
207
+ return ActionResult.success(LinkedInEmailOutput.make(decoded))
208
+ })
209
+ })
210
+
211
+ export const linkedInSearchActions = [linkedInSearchAction, linkedInProfileAction, linkedInEmailAction]
212
+
213
+ export const LinkedInSearchConnector = defineConnector({
214
+ id: linkedInSearchConnectorId,
215
+ description: 'LinkedIn people search and enrichment connector actions.',
216
+ actions: linkedInSearchActions
217
+ })
@@ -0,0 +1,234 @@
1
+ import { Effect } from 'effect'
2
+ import * as Schema from 'effect/Schema'
3
+ import { defineAction } from '../action.ts'
4
+ import { defineConnector } from '../connector.ts'
5
+ import { CredentialSlot, resolveCredential } from '../credential.ts'
6
+ import { ConnectorHttpClient, ConnectorHttpRequest, decodeJsonResponse } from '../http.ts'
7
+ import { ActionResult, ProviderFailure } from '../result.ts'
8
+ import type { ConnectorIntegration } from '../integration.ts'
9
+
10
+ export const notionConnectorId = 'notion'
11
+ export const notionApiTokenSlotId = 'notion.api_token'
12
+ export const notionApiBaseUrl = 'https://api.notion.com/v1'
13
+ export const notionVersion = '2022-06-28'
14
+
15
+ export const NotionApiTokenSlot = CredentialSlot.make({
16
+ id: notionApiTokenSlotId,
17
+ kind: 'api_key'
18
+ })
19
+
20
+ export const notionAuthorizationHeaders = (token: string) => ({
21
+ authorization: `Bearer ${token}`,
22
+ 'notion-version': notionVersion
23
+ })
24
+
25
+ const isSuccessStatus = (status: number) => status >= 200 && status < 300
26
+
27
+ const notionProviderFailure = (input: {
28
+ readonly code: string
29
+ readonly message: string
30
+ readonly status: number
31
+ readonly body: string
32
+ }) =>
33
+ ActionResult.failure(
34
+ new ProviderFailure({
35
+ code: input.code,
36
+ message: input.message,
37
+ status: input.status,
38
+ underlying: input.body
39
+ })
40
+ )
41
+
42
+ const resolveNotionToken = (integration: ConnectorIntegration) =>
43
+ Effect.gen(function* () {
44
+ const credential = yield* resolveCredential(integration, NotionApiTokenSlot)
45
+
46
+ switch (credential._tag) {
47
+ case 'ApiKeyCredential':
48
+ return credential.key
49
+ case 'BearerTokenCredential':
50
+ return credential.token
51
+ case 'OAuthCredential':
52
+ return credential.accessToken
53
+ }
54
+ })
55
+
56
+ export const NotionRichText = Schema.Struct({
57
+ type: Schema.optional(Schema.String),
58
+ plain_text: Schema.optional(Schema.String),
59
+ href: Schema.optional(Schema.NullOr(Schema.String))
60
+ })
61
+
62
+ export const NotionTitleProperty = Schema.Struct({
63
+ title: Schema.Array(NotionRichText)
64
+ })
65
+
66
+ export const NotionProperties = Schema.Record(Schema.String, Schema.Unknown)
67
+
68
+ export class NotionPage extends Schema.Class<NotionPage>('NotionPage')({
69
+ id: Schema.String,
70
+ object: Schema.String,
71
+ url: Schema.optional(Schema.String),
72
+ archived: Schema.optional(Schema.Boolean),
73
+ properties: Schema.optional(NotionProperties)
74
+ }) {}
75
+
76
+ export class NotionSearchInput extends Schema.Class<NotionSearchInput>('NotionSearchInput')({
77
+ query: Schema.optional(Schema.String),
78
+ pageSize: Schema.optional(Schema.Number),
79
+ startCursor: Schema.optional(Schema.String)
80
+ }) {}
81
+
82
+ export class NotionSearchOutput extends Schema.Class<NotionSearchOutput>('NotionSearchOutput')({
83
+ results: Schema.Array(Schema.Unknown),
84
+ nextCursor: Schema.optional(Schema.NullOr(Schema.String)),
85
+ hasMore: Schema.Boolean
86
+ }) {}
87
+
88
+ export class NotionGetPageInput extends Schema.Class<NotionGetPageInput>('NotionGetPageInput')({
89
+ pageId: Schema.String
90
+ }) {}
91
+
92
+ export class NotionCreatePageInput extends Schema.Class<NotionCreatePageInput>('NotionCreatePageInput')({
93
+ parentPageId: Schema.optional(Schema.String),
94
+ parentDatabaseId: Schema.optional(Schema.String),
95
+ title: Schema.String,
96
+ properties: Schema.optional(NotionProperties)
97
+ }) {}
98
+
99
+ const pageParent = (input: NotionCreatePageInput) => {
100
+ if (input.parentDatabaseId !== undefined) {
101
+ return { database_id: input.parentDatabaseId }
102
+ }
103
+
104
+ return { page_id: input.parentPageId ?? '' }
105
+ }
106
+
107
+ const pageProperties = (input: NotionCreatePageInput) => ({
108
+ ...input.properties,
109
+ title: {
110
+ title: [
111
+ {
112
+ text: {
113
+ content: input.title
114
+ }
115
+ }
116
+ ]
117
+ }
118
+ })
119
+
120
+ export const notionSearchAction = defineAction({
121
+ id: 'notion.search',
122
+ description: 'Search pages and databases available to the Notion integration.',
123
+ inputSchema: NotionSearchInput,
124
+ outputSchema: NotionSearchOutput,
125
+ execute: ({ integration, input }) =>
126
+ Effect.gen(function* () {
127
+ const token = yield* resolveNotionToken(integration)
128
+ const http = yield* ConnectorHttpClient
129
+ const response = yield* http.request(
130
+ ConnectorHttpRequest.make({
131
+ method: 'POST',
132
+ url: `${notionApiBaseUrl}/search`,
133
+ headers: {
134
+ ...notionAuthorizationHeaders(token),
135
+ 'content-type': 'application/json'
136
+ },
137
+ body: JSON.stringify({
138
+ query: input.query,
139
+ page_size: input.pageSize,
140
+ start_cursor: input.startCursor
141
+ })
142
+ })
143
+ )
144
+
145
+ if (!isSuccessStatus(response.status)) {
146
+ return notionProviderFailure({
147
+ code: 'notion_search_failed',
148
+ message: 'Notion search failed',
149
+ status: response.status,
150
+ body: response.body
151
+ })
152
+ }
153
+
154
+ const output = yield* decodeJsonResponse(NotionSearchOutput, response)
155
+ return ActionResult.success(output)
156
+ })
157
+ })
158
+
159
+ export const notionGetPageAction = defineAction({
160
+ id: 'notion.get_page',
161
+ description: 'Get a Notion page by id.',
162
+ inputSchema: NotionGetPageInput,
163
+ outputSchema: NotionPage,
164
+ execute: ({ integration, input }) =>
165
+ Effect.gen(function* () {
166
+ const token = yield* resolveNotionToken(integration)
167
+ const http = yield* ConnectorHttpClient
168
+ const response = yield* http.request(
169
+ ConnectorHttpRequest.make({
170
+ method: 'GET',
171
+ url: `${notionApiBaseUrl}/pages/${encodeURIComponent(input.pageId)}`,
172
+ headers: notionAuthorizationHeaders(token)
173
+ })
174
+ )
175
+
176
+ if (!isSuccessStatus(response.status)) {
177
+ return notionProviderFailure({
178
+ code: 'notion_get_page_failed',
179
+ message: 'Notion get page failed',
180
+ status: response.status,
181
+ body: response.body
182
+ })
183
+ }
184
+
185
+ const output = yield* decodeJsonResponse(NotionPage, response)
186
+ return ActionResult.success(output)
187
+ })
188
+ })
189
+
190
+ export const notionCreatePageAction = defineAction({
191
+ id: 'notion.create_page',
192
+ description: 'Create a Notion page under a parent page or database.',
193
+ inputSchema: NotionCreatePageInput,
194
+ outputSchema: NotionPage,
195
+ execute: ({ integration, input }) =>
196
+ Effect.gen(function* () {
197
+ const token = yield* resolveNotionToken(integration)
198
+ const http = yield* ConnectorHttpClient
199
+ const response = yield* http.request(
200
+ ConnectorHttpRequest.make({
201
+ method: 'POST',
202
+ url: `${notionApiBaseUrl}/pages`,
203
+ headers: {
204
+ ...notionAuthorizationHeaders(token),
205
+ 'content-type': 'application/json'
206
+ },
207
+ body: JSON.stringify({
208
+ parent: pageParent(input),
209
+ properties: pageProperties(input)
210
+ })
211
+ })
212
+ )
213
+
214
+ if (!isSuccessStatus(response.status)) {
215
+ return notionProviderFailure({
216
+ code: 'notion_create_page_failed',
217
+ message: 'Notion create page failed',
218
+ status: response.status,
219
+ body: response.body
220
+ })
221
+ }
222
+
223
+ const output = yield* decodeJsonResponse(NotionPage, response)
224
+ return ActionResult.success(output)
225
+ })
226
+ })
227
+
228
+ export const notionActions = [notionSearchAction, notionGetPageAction, notionCreatePageAction]
229
+
230
+ export const NotionConnector = defineConnector({
231
+ id: notionConnectorId,
232
+ description: 'Notion page and search connector actions.',
233
+ actions: notionActions
234
+ })
@@ -0,0 +1,118 @@
1
+ import { Context, Effect } from 'effect'
2
+ import * as Schema from 'effect/Schema'
3
+ import { defineAction } from '../action.ts'
4
+ import { requiredStringConfig } from '../config.ts'
5
+ import { defineConnector } from '../connector.ts'
6
+ import { CredentialSlot, resolveCredential } from '../credential.ts'
7
+ import { ActionResult } from '../result.ts'
8
+ import type { ConnectorError } from '../error.ts'
9
+ import type { ConnectorIntegration } from '../integration.ts'
10
+
11
+ export const r2StorageConnectorId = 'r2-storage'
12
+ export const r2AccessKeyIdSlotId = 'r2-storage.access_key_id'
13
+ export const r2SecretAccessKeySlotId = 'r2-storage.secret_access_key'
14
+
15
+ export const R2AccessKeyIdSlot = CredentialSlot.make({ id: r2AccessKeyIdSlotId, kind: 'api_key' })
16
+ export const R2SecretAccessKeySlot = CredentialSlot.make({
17
+ id: r2SecretAccessKeySlotId,
18
+ kind: 'api_key'
19
+ })
20
+
21
+ export class R2PresignInput extends Schema.Class<R2PresignInput>('R2PresignInput')({
22
+ endpoint: Schema.String,
23
+ accessKeyId: Schema.String,
24
+ secretAccessKey: Schema.String,
25
+ bucket: Schema.String,
26
+ key: Schema.String,
27
+ contentType: Schema.String
28
+ }) {}
29
+
30
+ export class R2PresignOutput extends Schema.Class<R2PresignOutput>('R2PresignOutput')({
31
+ uploadUrl: Schema.String
32
+ }) {}
33
+
34
+ export type R2PresignerApi = {
35
+ readonly presignPutObject: (input: R2PresignInput) => Effect.Effect<R2PresignOutput, ConnectorError>
36
+ }
37
+
38
+ export class R2Presigner extends Context.Service<R2Presigner, R2PresignerApi>()(
39
+ '@yolk-sdk/connectors/R2Presigner'
40
+ ) {}
41
+
42
+ const resolveApiToken = (integration: ConnectorIntegration, slot: CredentialSlot) =>
43
+ Effect.gen(function* () {
44
+ const credential = yield* resolveCredential(integration, slot)
45
+
46
+ switch (credential._tag) {
47
+ case 'ApiKeyCredential':
48
+ return credential.key
49
+ case 'BearerTokenCredential':
50
+ return credential.token
51
+ case 'OAuthCredential':
52
+ return credential.accessToken
53
+ }
54
+ })
55
+
56
+ const joinPublicUrl = (publicUrl: string, key: string) => {
57
+ const base = publicUrl.endsWith('/') ? publicUrl.slice(0, -1) : publicUrl
58
+ return `${base}/${key}`
59
+ }
60
+
61
+ const safeObjectKey = (filename: string) => {
62
+ const trimmed = filename.trim().replace(/^\/+/, '')
63
+ return trimmed === '' ? `uploads/${Date.now()}` : trimmed
64
+ }
65
+
66
+ export class R2UploadUrlInput extends Schema.Class<R2UploadUrlInput>('R2UploadUrlInput')({
67
+ filename: Schema.String,
68
+ contentType: Schema.String
69
+ }) {}
70
+
71
+ export class R2UploadUrlOutput extends Schema.Class<R2UploadUrlOutput>('R2UploadUrlOutput')({
72
+ uploadUrl: Schema.String,
73
+ publicUrl: Schema.String,
74
+ key: Schema.String
75
+ }) {}
76
+
77
+ export const r2StorageUploadUrlAction = defineAction({
78
+ id: 'r2_storage.upload_url',
79
+ description: 'Create a presigned R2 PUT upload URL.',
80
+ inputSchema: R2UploadUrlInput,
81
+ outputSchema: R2UploadUrlOutput,
82
+ execute: ({ integration, input }) =>
83
+ Effect.gen(function* () {
84
+ const endpoint = yield* requiredStringConfig(integration, 'endpoint')
85
+ const bucket = yield* requiredStringConfig(integration, 'bucket')
86
+ const publicUrl = yield* requiredStringConfig(integration, 'publicUrl')
87
+ const accessKeyId = yield* resolveApiToken(integration, R2AccessKeyIdSlot)
88
+ const secretAccessKey = yield* resolveApiToken(integration, R2SecretAccessKeySlot)
89
+ const presigner = yield* R2Presigner
90
+ const key = safeObjectKey(input.filename)
91
+ const presigned = yield* presigner.presignPutObject(
92
+ R2PresignInput.make({
93
+ endpoint,
94
+ accessKeyId,
95
+ secretAccessKey,
96
+ bucket,
97
+ key,
98
+ contentType: input.contentType
99
+ })
100
+ )
101
+
102
+ return ActionResult.success(
103
+ R2UploadUrlOutput.make({
104
+ uploadUrl: presigned.uploadUrl,
105
+ publicUrl: joinPublicUrl(publicUrl, key),
106
+ key
107
+ })
108
+ )
109
+ })
110
+ })
111
+
112
+ export const r2StorageActions = [r2StorageUploadUrlAction]
113
+
114
+ export const R2StorageConnector = defineConnector({
115
+ id: r2StorageConnectorId,
116
+ description: 'Cloudflare R2 storage connector actions.',
117
+ actions: r2StorageActions
118
+ })