@nuxtagentic/integrations 0.0.0-stage → 0.1.1

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 (51) hide show
  1. package/LICENSE.md +9 -0
  2. package/README.md +175 -2
  3. package/client.ts +124 -0
  4. package/module.ts +44 -0
  5. package/package.json +52 -3
  6. package/runtime/assets/logos/discord.svg +1 -0
  7. package/runtime/assets/logos/github.svg +1 -0
  8. package/runtime/assets/logos/gmail.svg +1 -0
  9. package/runtime/assets/logos/googleads.svg +1 -0
  10. package/runtime/assets/logos/googleanalytics.svg +1 -0
  11. package/runtime/assets/logos/googlecalendar.svg +1 -0
  12. package/runtime/assets/logos/googledrive.svg +1 -0
  13. package/runtime/assets/logos/googlesearchconsole.svg +1 -0
  14. package/runtime/assets/logos/hubspot.svg +1 -0
  15. package/runtime/assets/logos/intercom.svg +1 -0
  16. package/runtime/assets/logos/linear.svg +1 -0
  17. package/runtime/assets/logos/mcp.svg +1 -0
  18. package/runtime/assets/logos/meta.svg +1 -0
  19. package/runtime/assets/logos/notion.svg +1 -0
  20. package/runtime/assets/logos/reddit.svg +1 -0
  21. package/runtime/assets/logos/sentry.svg +1 -0
  22. package/runtime/assets/logos/shopify.svg +1 -0
  23. package/runtime/assets/logos/slack.svg +1 -0
  24. package/runtime/assets/logos/stripe.svg +1 -0
  25. package/runtime/assets/logos/tiktok-dark.svg +1 -0
  26. package/runtime/assets/logos/tiktok.svg +1 -0
  27. package/runtime/assets/logos/vercel.svg +1 -0
  28. package/runtime/assets/logos/x.svg +1 -0
  29. package/runtime/components/IntegrationCard.vue +82 -0
  30. package/runtime/components/IntegrationCatalog.vue +130 -0
  31. package/runtime/components/IntegrationConnectionPanel.vue +589 -0
  32. package/runtime/components/IntegrationLogo.vue +41 -0
  33. package/runtime/pages/integrations.vue +481 -0
  34. package/runtime/server/api/integrations/approvals/[id].post.ts +69 -0
  35. package/runtime/server/api/integrations/catalog/[provider]/action.post.ts +35 -0
  36. package/runtime/server/api/integrations/catalog/[provider]/connection.post.ts +242 -0
  37. package/runtime/server/api/integrations/catalog/[provider]/tools.get.ts +40 -0
  38. package/runtime/server/api/integrations/catalog/index.get.ts +48 -0
  39. package/runtime/server/api/integrations/catalog/tools.get.ts +27 -0
  40. package/runtime/server/plugins/connect-maintenance.ts +30 -0
  41. package/runtime/server/utils/approval-continuation.ts +37 -0
  42. package/runtime/server/utils/google-mcp.ts +296 -0
  43. package/runtime/server/utils/integration-adapters.ts +836 -0
  44. package/runtime/server/utils/integration-context.ts +510 -0
  45. package/runtime/server/utils/integration-crypto.ts +13 -0
  46. package/runtime/server/utils/request-body.ts +38 -0
  47. package/runtime/shared/agent.ts +14 -0
  48. package/runtime/shared/catalog.ts +566 -0
  49. package/runtime/shared/google-mcp.ts +8 -0
  50. package/runtime/workspace.ts +15 -0
  51. package/schema.ts +179 -0
@@ -0,0 +1,481 @@
1
+ <script setup lang="ts">
2
+ import { computed, nextTick, onMounted, ref, watch } from 'vue'
3
+ import {
4
+ definePageMeta,
5
+ useAsyncData,
6
+ useCloudConfig,
7
+ useRequestFetch,
8
+ useRoute,
9
+ useRouter,
10
+ useToast,
11
+ } from '#imports'
12
+ import { useWorkspaceScope } from '@nuxtagentic/identity/runtime/composables/useWorkspaceScope'
13
+ import type { IntegrationsConfig } from '../../schema'
14
+ import {
15
+ createIntegrationCommands,
16
+ type IntegrationConnection,
17
+ type McpServerTool,
18
+ } from '../../client'
19
+ import IntegrationCatalog from '../components/IntegrationCatalog.vue'
20
+ import IntegrationConnectionPanel from '../components/IntegrationConnectionPanel.vue'
21
+ import {
22
+ INTEGRATION_CATALOG,
23
+ type IntegrationDefinition,
24
+ type IntegrationScope,
25
+ } from '../shared/catalog'
26
+
27
+ definePageMeta({ layout: 'dashboard', middleware: ['auth'] })
28
+ const route = useRoute()
29
+ const router = useRouter()
30
+ const scope = useWorkspaceScope()
31
+ const fetcher = useRequestFetch()
32
+ const toast = useToast()
33
+ const commands = createIntegrationCommands(fetcher)
34
+ const { data, status, error, refresh } = await useAsyncData(
35
+ `integrations:${scope.value}`,
36
+ commands.catalog,
37
+ )
38
+ const api = useCloudConfig<IntegrationsConfig>()
39
+ const { data: approvals, refresh: refreshApprovals } = await useAsyncData(
40
+ `integration-approvals:${scope.value}`,
41
+ () =>
42
+ api.integrationApproval.list({
43
+ pageSize: 100,
44
+ filters: {
45
+ type: 'group',
46
+ logic: 'and',
47
+ children: [{ type: 'condition', field: 'status', operator: 'eq', value: 'pending' }],
48
+ },
49
+ }),
50
+ )
51
+ const selected = computed(() =>
52
+ INTEGRATION_CATALOG.find((entry) => entry.id === route.query.integration),
53
+ )
54
+ const open = computed({
55
+ get: () => !!selected.value,
56
+ set: (value: boolean) => {
57
+ if (!value) {
58
+ void router.replace({ query: { ...route.query, integration: undefined } })
59
+ }
60
+ },
61
+ })
62
+ const tab = computed(() =>
63
+ route.query.tab === 'connected' || route.query.tab === 'available' ? route.query.tab : 'all',
64
+ )
65
+ const category = computed({
66
+ get: () => String(route.query.category ?? 'All'),
67
+ set: (value) => {
68
+ void router.replace({
69
+ query: { ...route.query, category: value === 'All' ? undefined : value },
70
+ })
71
+ },
72
+ })
73
+ const search = computed({
74
+ get: () => String(route.query.search ?? ''),
75
+ set: (value) => {
76
+ void router.replace({ query: { ...route.query, search: value || undefined } })
77
+ },
78
+ })
79
+ const connectionsOf = (provider: string) =>
80
+ (data.value?.connections ?? []).filter((entry) => entry.provider === provider)
81
+ const providerOf = (entry: IntegrationDefinition) =>
82
+ data.value?.providers.find((provider) => provider.id === entry.auth.oauth?.provider)
83
+
84
+ const activeId = ref<number>()
85
+ const adding = ref(false)
86
+ const ownership = ref<IntegrationScope>('user')
87
+ const server = ref('')
88
+ const permissions = ref<string[]>([])
89
+ const approvalList = ref<string[]>([])
90
+ const settings = ref<Record<string, string>>({})
91
+ const credentials = ref<Record<string, string>>({})
92
+ const tools = ref<McpServerTool[]>()
93
+ const endpoint = ref<{ url: string; secret: string }>()
94
+ const choosing = ref<number>()
95
+ const isBusy = ref(false)
96
+ const message = ref('')
97
+ const active = computed(() =>
98
+ selected.value
99
+ ? connectionsOf(selected.value.id).find((entry) => entry.id === activeId.value)
100
+ : undefined,
101
+ )
102
+ const pendingAccount = computed(() =>
103
+ choosing.value ? data.value?.pending.find((entry) => entry.id === choosing.value) : undefined,
104
+ )
105
+ const pendingApprovals = computed(() =>
106
+ (approvals.value?.data ?? []).filter((approval) =>
107
+ data.value?.connections.some((row) => row.id === approval.connectionId),
108
+ ),
109
+ )
110
+ const dialog = ref<InstanceType<typeof IntegrationConnectionPanel>>()
111
+ const source = ref<DOMRect>()
112
+
113
+ function reset(entry?: IntegrationDefinition) {
114
+ activeId.value = undefined
115
+ adding.value = false
116
+ credentials.value = {}
117
+ server.value = ''
118
+ tools.value = undefined
119
+ endpoint.value = undefined
120
+ message.value = ''
121
+ // Shared first where the provider is a workspace install and the viewer may add one.
122
+ const workspaceFirst =
123
+ entry?.ownership[0] === 'workspace' && !!data.value?.organization && !!data.value.manager
124
+ ownership.value = workspaceFirst || !entry?.ownership.includes('user') ? 'workspace' : 'user'
125
+ }
126
+ watch(selected, (entry) => reset(entry), { immediate: true })
127
+
128
+ async function manage(connection: IntegrationConnection) {
129
+ activeId.value = connection.id
130
+ adding.value = false
131
+ permissions.value = [...connection.permissions]
132
+ approvalList.value = [...connection.approvals]
133
+ settings.value = { ...connection.settings }
134
+ endpoint.value = undefined
135
+ tools.value = undefined
136
+ message.value = ''
137
+ if (selected.value?.auth.mcp) {
138
+ try {
139
+ tools.value = await commands.mcpTools(selected.value.id, connection.id)
140
+ } catch (cause) {
141
+ tools.value = []
142
+ message.value = errorMessage(cause)
143
+ }
144
+ }
145
+ }
146
+
147
+ function errorMessage(cause: unknown) {
148
+ const value = cause as { data?: { statusMessage?: string; message?: string }; message?: string }
149
+ return (
150
+ value.data?.statusMessage ??
151
+ value.data?.message ??
152
+ value.message ??
153
+ 'Could not save this connection.'
154
+ )
155
+ }
156
+ function selectTab(value: string | number) {
157
+ void router.replace({
158
+ query: { ...route.query, tab: value === 'all' ? undefined : String(value) },
159
+ })
160
+ }
161
+ async function show(entry: IntegrationDefinition, event: MouseEvent) {
162
+ source.value = (event.currentTarget as HTMLElement)
163
+ .closest('[data-integration-card]')
164
+ ?.getBoundingClientRect()
165
+ await router.replace({ query: { ...route.query, integration: entry.id } })
166
+ }
167
+ function morph() {
168
+ if (
169
+ !dialog.value?.element ||
170
+ !source.value ||
171
+ matchMedia('(prefers-reduced-motion: reduce)').matches
172
+ ) {
173
+ return
174
+ }
175
+ const element = dialog.value.element.closest('[role=dialog]') as HTMLElement | null
176
+ if (!element) {
177
+ return
178
+ }
179
+ const target = element.getBoundingClientRect()
180
+ const origin = source.value
181
+ element.animate(
182
+ [
183
+ {
184
+ transform: `translate(${origin.left - target.left}px, ${origin.top - target.top}px) scale(${origin.width / target.width}, ${origin.height / target.height})`,
185
+ transformOrigin: 'top left',
186
+ borderRadius: '12px',
187
+ },
188
+ { transform: 'none', transformOrigin: 'top left', borderRadius: '16px' },
189
+ ],
190
+ { duration: 260, easing: 'cubic-bezier(.2,0,0,1)' },
191
+ )
192
+ source.value = undefined
193
+ }
194
+
195
+ async function run<T>(work: () => Promise<T>): Promise<T | undefined> {
196
+ if (isBusy.value) {
197
+ return
198
+ }
199
+ isBusy.value = true
200
+ message.value = ''
201
+ try {
202
+ return await work()
203
+ } catch (cause) {
204
+ message.value = errorMessage(cause)
205
+ } finally {
206
+ isBusy.value = false
207
+ }
208
+ }
209
+
210
+ /** OAuth and MCP sign-in leave the app; the callback returns to this page. */
211
+ async function connect(reconnect?: IntegrationConnection) {
212
+ const entry = selected.value
213
+ if (!entry) {
214
+ return
215
+ }
216
+ const result = await run(() =>
217
+ commands.start(entry.id, {
218
+ scope: reconnect?.scope ?? ownership.value,
219
+ ...(server.value ? { server: server.value } : {}),
220
+ ...(Object.keys(credentials.value).length ? { fields: credentials.value } : {}),
221
+ ...(reconnect ? { connection: reconnect.connectionId } : {}),
222
+ }),
223
+ )
224
+ if (result?.url) {
225
+ window.location.assign(result.url)
226
+ }
227
+ }
228
+
229
+ async function connectWithToken() {
230
+ const entry = selected.value
231
+ if (!entry) {
232
+ return
233
+ }
234
+ const row = await run(() =>
235
+ commands.apiKey(entry.id, { scope: ownership.value, credentials: credentials.value }),
236
+ )
237
+ if (!row) {
238
+ return
239
+ }
240
+ credentials.value = {}
241
+ await refresh()
242
+ toast.add({ title: `${entry.name} connected`, color: 'success' })
243
+ await manage(row)
244
+ }
245
+
246
+ async function save() {
247
+ const entry = selected.value
248
+ const connection = active.value
249
+ if (!entry || !connection) {
250
+ return
251
+ }
252
+ const row = await run(() =>
253
+ commands.update(entry.id, connection.id, {
254
+ permissions: permissions.value,
255
+ approvals: approvalList.value,
256
+ settings: settings.value,
257
+ }),
258
+ )
259
+ if (!row) {
260
+ return
261
+ }
262
+ await refresh()
263
+ toast.add({ title: `${entry.name} saved`, color: 'success' })
264
+ activeId.value = undefined
265
+ }
266
+
267
+ async function disconnect(connection: IntegrationConnection) {
268
+ const entry = selected.value
269
+ if (!entry) {
270
+ return
271
+ }
272
+ const done = await run(() => commands.disconnect(entry.id, connection.id))
273
+ if (!done) {
274
+ return
275
+ }
276
+ await refresh()
277
+ toast.add({ title: `${connection.label || connection.account} disconnected`, color: 'success' })
278
+ activeId.value = undefined
279
+ }
280
+
281
+ async function loadEndpoint(rotate: boolean) {
282
+ const entry = selected.value
283
+ const connection = active.value
284
+ if (!entry || !connection) {
285
+ return
286
+ }
287
+ if (endpoint.value && !rotate) {
288
+ endpoint.value = undefined
289
+ return
290
+ }
291
+ endpoint.value = await run(() => commands.webhook(entry.id, connection.id, rotate))
292
+ }
293
+
294
+ async function copy(value: string) {
295
+ try {
296
+ await navigator.clipboard.writeText(value)
297
+ toast.add({ title: 'Copied', color: 'neutral' })
298
+ } catch {
299
+ message.value = 'Select the value and copy it.'
300
+ }
301
+ }
302
+
303
+ async function choose(target: string) {
304
+ const entry = selected.value
305
+ if (!entry || !choosing.value) {
306
+ return
307
+ }
308
+ const row = await run(() => commands.target(entry.id, choosing.value!, target))
309
+ if (!row) {
310
+ return
311
+ }
312
+ choosing.value = undefined
313
+ await refresh()
314
+ toast.add({ title: `${entry.name} connected`, color: 'success' })
315
+ await manage(row)
316
+ }
317
+
318
+ async function approval(id: number, approve: boolean) {
319
+ isBusy.value = true
320
+ try {
321
+ await commands.approve(id, approve)
322
+ await refreshApprovals()
323
+ } catch (cause) {
324
+ toast.add({
325
+ title: 'Could not complete approval',
326
+ description: errorMessage(cause),
327
+ color: 'error',
328
+ })
329
+ await refreshApprovals()
330
+ } finally {
331
+ isBusy.value = false
332
+ }
333
+ }
334
+
335
+ /** The OAuth or MCP callback returned here: attach the new account to the integration. */
336
+ onMounted(async () => {
337
+ const result = route.query.connect
338
+ const connection = Number(route.query.connection)
339
+ const failure = route.query.connect_error
340
+ if (!result && !failure) {
341
+ return
342
+ }
343
+ await router.replace({
344
+ query: { ...route.query, connect: undefined, connection: undefined, connect_error: undefined },
345
+ })
346
+ const entry = selected.value
347
+ if (failure || !entry) {
348
+ message.value = String(failure ?? 'The connection could not be completed.')
349
+ return
350
+ }
351
+ if (result === 'choose') {
352
+ await refresh()
353
+ choosing.value = connection
354
+ return
355
+ }
356
+ const row = await run(() => commands.bind(entry.id, connection))
357
+ if (!row) {
358
+ return
359
+ }
360
+ await refresh()
361
+ toast.add({ title: `${entry.name} connected`, color: 'success' })
362
+ await manage(row)
363
+ })
364
+ </script>
365
+
366
+ <template>
367
+ <div class="min-h-0 flex-1 overflow-y-auto">
368
+ <div class="mx-auto flex max-w-[960px] flex-col gap-5 px-6 pt-7 pb-30">
369
+ <div class="flex flex-col gap-1.5">
370
+ <h1 class="text-2xl leading-8 font-medium tracking-[-.01em]">Integrations</h1>
371
+ <p class="text-sm leading-5 text-muted text-pretty">
372
+ Connect the tools your agents work with. Events from a connected app can start an agent,
373
+ and agents can act inside it with the permissions you grant.
374
+ </p>
375
+ </div>
376
+ <UAlert
377
+ v-if="error"
378
+ color="error"
379
+ variant="subtle"
380
+ title="Could not load connections"
381
+ :description="errorMessage(error)"
382
+ />
383
+ <IntegrationCatalog
384
+ v-model:category="category"
385
+ v-model:search="search"
386
+ :tab="tab"
387
+ :connections="data?.connections ?? []"
388
+ :loading="status === 'pending'"
389
+ :error="!!error"
390
+ @update:tab="selectTab"
391
+ @select="show"
392
+ />
393
+ <UCollapsible
394
+ v-if="pendingApprovals.length"
395
+ class="rounded-xl border border-default p-4"
396
+ >
397
+ <UButton
398
+ color="neutral"
399
+ variant="ghost"
400
+ :label="`${pendingApprovals.length} actions awaiting your approval`"
401
+ trailing-icon="i-lucide-chevron-down"
402
+ />
403
+ <template #content>
404
+ <div
405
+ v-for="item in pendingApprovals"
406
+ :key="item.id"
407
+ class="mt-3 flex flex-col gap-2 border-t border-default pt-3"
408
+ >
409
+ <span class="text-sm">{{ item.action.replaceAll('_', ' ') }}</span>
410
+ <pre class="max-h-40 overflow-auto text-xs text-muted">{{
411
+ JSON.stringify(item.input, null, 2)
412
+ }}</pre>
413
+ <div class="flex gap-2">
414
+ <UButton
415
+ label="Approve"
416
+ color="neutral"
417
+ :disabled="isBusy"
418
+ @click="approval(item.id, true)"
419
+ />
420
+ <UButton
421
+ label="Reject"
422
+ color="neutral"
423
+ variant="outline"
424
+ :disabled="isBusy"
425
+ @click="approval(item.id, false)"
426
+ />
427
+ </div>
428
+ </div>
429
+ </template>
430
+ </UCollapsible>
431
+ </div>
432
+ </div>
433
+ <UModal
434
+ v-model:open="open"
435
+ :title="selected?.name"
436
+ :description="selected?.details"
437
+ :ui="{
438
+ content:
439
+ 'w-[480px] max-w-[calc(100vw-32px)] rounded-2xl border border-(--gray-500) bg-(--gray-200) shadow-2xl data-[state=open]:animate-none motion-reduce:animate-none',
440
+ overlay: 'bg-black/55 motion-reduce:animate-none',
441
+ }"
442
+ :content="{ onOpenAutoFocus: () => nextTick(morph) }"
443
+ >
444
+ <template #content>
445
+ <IntegrationConnectionPanel
446
+ v-if="selected"
447
+ ref="dialog"
448
+ v-model:permissions="permissions"
449
+ v-model:approvals="approvalList"
450
+ v-model:credentials="credentials"
451
+ v-model:settings="settings"
452
+ v-model:scope="ownership"
453
+ v-model:server="server"
454
+ :integration="selected"
455
+ :connections="connectionsOf(selected.id)"
456
+ :active="active"
457
+ :adding="adding"
458
+ :provider="providerOf(selected)"
459
+ :manager="!!data?.manager"
460
+ :organization="!!data?.organization"
461
+ :pending="pendingAccount"
462
+ :tools="tools"
463
+ :endpoint="endpoint"
464
+ :busy="isBusy"
465
+ :message="message"
466
+ @close="open = false"
467
+ @add="adding = true"
468
+ @back="reset(selected)"
469
+ @manage="manage"
470
+ @connect="connect()"
471
+ @reconnect="connect"
472
+ @api-key="connectWithToken"
473
+ @save="save"
474
+ @disconnect="disconnect"
475
+ @endpoint="loadEndpoint"
476
+ @copy="copy"
477
+ @choose="choose"
478
+ />
479
+ </template>
480
+ </UModal>
481
+ </template>
@@ -0,0 +1,69 @@
1
+ import { readCommandBody } from '../../../utils/request-body'
2
+ import { defineEventHandler, getRouterParam } from 'h3'
3
+ import { assertGoogleMutation } from '@nuxtagentic/google/runtime/server/utils/google'
4
+ import { notifyApproval } from '../../../utils/approval-continuation'
5
+ import {
6
+ getIntegrationContext,
7
+ performIntegrationAction,
8
+ toActionError,
9
+ } from '../../../utils/integration-context'
10
+ export default defineEventHandler(async (event) => {
11
+ assertGoogleMutation(event)
12
+ try {
13
+ const context = await getIntegrationContext(event)
14
+ const approval = await context.api.integrationApproval.get(Number(getRouterParam(event, 'id')))
15
+ const connection = await context.api.integrationConnection.get(approval.connectionId)
16
+ if (connection.workspace !== context.workspace) {
17
+ throw new Error('This approval is no longer pending in this workspace.')
18
+ }
19
+ if (['completed', 'rejected', 'failed'].includes(approval.status)) {
20
+ await notifyApproval(event, approval.id, approval.continuation, context.env)
21
+ return approval
22
+ }
23
+ if (approval.status !== 'pending') {
24
+ throw new Error('This action is already being handled. Check its result before retrying.')
25
+ }
26
+ const body = await readCommandBody<{ approve: boolean }>(event, 1000)
27
+ if (typeof body.approve !== 'boolean') {
28
+ throw new Error('Choose approve or reject.')
29
+ }
30
+ await context.api.integrationApproval.update(approval.id, {
31
+ status: body.approve ? 'approved' : 'rejected',
32
+ ownerId: context.ownerId,
33
+ })
34
+ if (!body.approve) {
35
+ await notifyApproval(event, approval.id, approval.continuation, context.env)
36
+ return { status: 'rejected' }
37
+ }
38
+ let completed
39
+ try {
40
+ const result = await performIntegrationAction(
41
+ context,
42
+ {
43
+ provider: connection.provider,
44
+ action: approval.action,
45
+ input: approval.input as Record<string, unknown>,
46
+ bindingId: connection.id,
47
+ },
48
+ { approved: true, idempotencyKey: `approval-${approval.id}` },
49
+ )
50
+ completed = await context.api.integrationApproval.update(approval.id, {
51
+ status: 'completed',
52
+ result,
53
+ ownerId: context.ownerId,
54
+ })
55
+ } catch (cause) {
56
+ await context.api.integrationApproval.update(approval.id, {
57
+ status: 'failed',
58
+ result: { error: cause instanceof Error ? cause.message : 'Action failed.' },
59
+ ownerId: context.ownerId,
60
+ })
61
+ await notifyApproval(event, approval.id, approval.continuation, context.env)
62
+ throw cause
63
+ }
64
+ await notifyApproval(event, approval.id, approval.continuation, context.env)
65
+ return completed
66
+ } catch (cause) {
67
+ throw toActionError(cause)
68
+ }
69
+ })
@@ -0,0 +1,35 @@
1
+ import { readCommandBody } from '../../../../utils/request-body'
2
+ import { defineEventHandler, getRouterParam } from 'h3'
3
+ import { assertGoogleMutation } from '@nuxtagentic/google/runtime/server/utils/google'
4
+ import {
5
+ toActionError,
6
+ getIntegrationContext,
7
+ performIntegrationAction,
8
+ } from '../../../../utils/integration-context'
9
+
10
+ export default defineEventHandler(async (event) => {
11
+ assertGoogleMutation(event)
12
+ try {
13
+ const context = await getIntegrationContext(event)
14
+ const body = await readCommandBody<{
15
+ action: string
16
+ input: Record<string, unknown>
17
+ binding?: number
18
+ }>(event, 100000)
19
+ if (
20
+ !body.input ||
21
+ typeof body.input !== 'object' ||
22
+ JSON.stringify(body.input).length > 100000
23
+ ) {
24
+ throw new Error('Provide action parameters under input, up to 100 KB.')
25
+ }
26
+ return await performIntegrationAction(context, {
27
+ provider: getRouterParam(event, 'provider')!,
28
+ action: body.action,
29
+ input: body.input,
30
+ ...(typeof body.binding === 'number' ? { bindingId: body.binding } : {}),
31
+ })
32
+ } catch (cause) {
33
+ throw toActionError(cause)
34
+ }
35
+ })