@nuxtagentic/integrations 0.0.0-stage → 0.1.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.md +9 -0
- package/README.md +175 -2
- package/client.ts +124 -0
- package/module.ts +44 -0
- package/package.json +52 -3
- package/runtime/assets/logos/discord.svg +1 -0
- package/runtime/assets/logos/github.svg +1 -0
- package/runtime/assets/logos/gmail.svg +1 -0
- package/runtime/assets/logos/googleads.svg +1 -0
- package/runtime/assets/logos/googleanalytics.svg +1 -0
- package/runtime/assets/logos/googlecalendar.svg +1 -0
- package/runtime/assets/logos/googledrive.svg +1 -0
- package/runtime/assets/logos/googlesearchconsole.svg +1 -0
- package/runtime/assets/logos/hubspot.svg +1 -0
- package/runtime/assets/logos/intercom.svg +1 -0
- package/runtime/assets/logos/linear.svg +1 -0
- package/runtime/assets/logos/mcp.svg +1 -0
- package/runtime/assets/logos/meta.svg +1 -0
- package/runtime/assets/logos/notion.svg +1 -0
- package/runtime/assets/logos/reddit.svg +1 -0
- package/runtime/assets/logos/sentry.svg +1 -0
- package/runtime/assets/logos/shopify.svg +1 -0
- package/runtime/assets/logos/slack.svg +1 -0
- package/runtime/assets/logos/stripe.svg +1 -0
- package/runtime/assets/logos/tiktok-dark.svg +1 -0
- package/runtime/assets/logos/tiktok.svg +1 -0
- package/runtime/assets/logos/vercel.svg +1 -0
- package/runtime/assets/logos/x.svg +1 -0
- package/runtime/components/IntegrationCard.vue +82 -0
- package/runtime/components/IntegrationCatalog.vue +130 -0
- package/runtime/components/IntegrationConnectionPanel.vue +589 -0
- package/runtime/components/IntegrationLogo.vue +41 -0
- package/runtime/pages/integrations.vue +481 -0
- package/runtime/server/api/integrations/approvals/[id].post.ts +69 -0
- package/runtime/server/api/integrations/catalog/[provider]/action.post.ts +35 -0
- package/runtime/server/api/integrations/catalog/[provider]/connection.post.ts +242 -0
- package/runtime/server/api/integrations/catalog/[provider]/tools.get.ts +40 -0
- package/runtime/server/api/integrations/catalog/index.get.ts +48 -0
- package/runtime/server/api/integrations/catalog/tools.get.ts +27 -0
- package/runtime/server/plugins/connect-maintenance.ts +30 -0
- package/runtime/server/utils/approval-continuation.ts +37 -0
- package/runtime/server/utils/google-mcp.ts +296 -0
- package/runtime/server/utils/integration-adapters.ts +836 -0
- package/runtime/server/utils/integration-context.ts +510 -0
- package/runtime/server/utils/integration-crypto.ts +13 -0
- package/runtime/server/utils/request-body.ts +38 -0
- package/runtime/shared/agent.ts +14 -0
- package/runtime/shared/catalog.ts +566 -0
- package/runtime/shared/google-mcp.ts +8 -0
- package/runtime/workspace.ts +15 -0
- 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
|
+
})
|