@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,242 @@
|
|
|
1
|
+
import { readCommandBody } from '../../../../utils/request-body'
|
|
2
|
+
import { defineEventHandler, getRequestURL, getRouterParam } from 'h3'
|
|
3
|
+
import { assertGoogleMutation } from '@nuxtagentic/google/runtime/server/utils/google'
|
|
4
|
+
import type { ConnectionSummary } from '@cloudconfig/core/connect'
|
|
5
|
+
import {
|
|
6
|
+
connectOf,
|
|
7
|
+
defaultMcpApprovals,
|
|
8
|
+
getIntegrationContext,
|
|
9
|
+
listBindings,
|
|
10
|
+
toActionError,
|
|
11
|
+
toPublicConnection,
|
|
12
|
+
type Binding,
|
|
13
|
+
} from '../../../../utils/integration-context'
|
|
14
|
+
import { verifyIntegration, type Credentials } from '../../../../utils/integration-adapters'
|
|
15
|
+
import { getIntegrationDefinition, type IntegrationScope } from '../../../../../shared/catalog'
|
|
16
|
+
|
|
17
|
+
type Operation = 'start' | 'bind' | 'apiKey' | 'permissions' | 'disconnect' | 'target' | 'webhook'
|
|
18
|
+
|
|
19
|
+
interface Body {
|
|
20
|
+
operation: Operation
|
|
21
|
+
scope?: IntegrationScope
|
|
22
|
+
/** MCP server URL for the custom MCP integration. */
|
|
23
|
+
server?: string
|
|
24
|
+
/** Values the provider needs before authorizing (Shopify store). */
|
|
25
|
+
fields?: Record<string, string>
|
|
26
|
+
credentials?: Credentials
|
|
27
|
+
label?: string
|
|
28
|
+
/** A Connect connection id (bind, target). */
|
|
29
|
+
connection?: number
|
|
30
|
+
/** An integration connection id (permissions, disconnect, webhook). */
|
|
31
|
+
binding?: number
|
|
32
|
+
target?: string
|
|
33
|
+
permissions?: string[]
|
|
34
|
+
approvals?: string[]
|
|
35
|
+
settings?: Record<string, string>
|
|
36
|
+
enabled?: boolean
|
|
37
|
+
rotate?: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default defineEventHandler(async (event) => {
|
|
41
|
+
assertGoogleMutation(event)
|
|
42
|
+
try {
|
|
43
|
+
const context = await getIntegrationContext(event)
|
|
44
|
+
const provider = getRouterParam(event, 'provider')!
|
|
45
|
+
const definition = getIntegrationDefinition(provider)
|
|
46
|
+
if (!definition) {
|
|
47
|
+
throw new Error('Choose a supported integration.')
|
|
48
|
+
}
|
|
49
|
+
const {
|
|
50
|
+
ownership,
|
|
51
|
+
name: integrationName,
|
|
52
|
+
auth,
|
|
53
|
+
fields: integrationFields,
|
|
54
|
+
actions: integrationActions,
|
|
55
|
+
} = definition
|
|
56
|
+
const { ownerId, manager, actor, api, workspace } = context
|
|
57
|
+
const body = await readCommandBody<Body>(event, 20000)
|
|
58
|
+
const {
|
|
59
|
+
operation,
|
|
60
|
+
scope: requestedScope,
|
|
61
|
+
settings: connectionSettings,
|
|
62
|
+
label: connectionLabel,
|
|
63
|
+
connection: connectConnectionId,
|
|
64
|
+
binding: bindingId,
|
|
65
|
+
} = body
|
|
66
|
+
const connect = await connectOf(context)
|
|
67
|
+
const request = new Request(getRequestURL(event))
|
|
68
|
+
const viewer = { ownerId, manager }
|
|
69
|
+
const scope: IntegrationScope = requestedScope ?? ownership[0]!
|
|
70
|
+
if (!ownership.includes(scope)) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
scope === 'workspace'
|
|
73
|
+
? `${integrationName} connects for one person only.`
|
|
74
|
+
: `${integrationName} is connected for the whole workspace by an admin.`,
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
const ownBinding = async (id: number | undefined): Promise<Binding> => {
|
|
78
|
+
const row = (await listBindings(context, { provider })).find((entry) => entry.id === id)
|
|
79
|
+
if (!row) {
|
|
80
|
+
throw new Error('Connection not found.')
|
|
81
|
+
}
|
|
82
|
+
return row
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Attach a Connect connection to this integration, with every action allowed. */
|
|
86
|
+
const bind = async (summary: ConnectionSummary) => {
|
|
87
|
+
if (summary.status !== 'active') {
|
|
88
|
+
throw new Error('Finish setting up this connection first.')
|
|
89
|
+
}
|
|
90
|
+
const existing = (await listBindings(context, { provider })).find(
|
|
91
|
+
(row) => row.connectionId === summary.id,
|
|
92
|
+
)
|
|
93
|
+
if (existing) {
|
|
94
|
+
return toPublicConnection(existing, viewer)
|
|
95
|
+
}
|
|
96
|
+
let permissions = integrationActions.map((action) => action.id)
|
|
97
|
+
let approvals: string[] = []
|
|
98
|
+
if (auth.mcp) {
|
|
99
|
+
const tools = await connect.mcp(actor, summary.id).listTools()
|
|
100
|
+
permissions = tools.map((tool) => tool.name)
|
|
101
|
+
approvals = defaultMcpApprovals(tools)
|
|
102
|
+
}
|
|
103
|
+
const row = await api.integrationConnection.create({
|
|
104
|
+
provider,
|
|
105
|
+
workspace,
|
|
106
|
+
scope: summary.scope,
|
|
107
|
+
...(summary.organizationId !== null ? { organizationId: summary.organizationId } : {}),
|
|
108
|
+
connectionId: summary.id,
|
|
109
|
+
connectionUid: summary.uid,
|
|
110
|
+
account: summary.label || summary.account.name,
|
|
111
|
+
enabled: true,
|
|
112
|
+
permissions,
|
|
113
|
+
approvals,
|
|
114
|
+
settings: connectionSettings ?? {},
|
|
115
|
+
ownerId,
|
|
116
|
+
})
|
|
117
|
+
return toPublicConnection(row, viewer)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
switch (operation) {
|
|
121
|
+
case 'start': {
|
|
122
|
+
const { fields } = body
|
|
123
|
+
const returnTo = `/dashboard/integrations?integration=${encodeURIComponent(provider)}`
|
|
124
|
+
if (auth.mcp) {
|
|
125
|
+
const server = auth.mcp.server ?? body.server ?? ''
|
|
126
|
+
return connect.start(actor, request, {
|
|
127
|
+
provider: 'mcp',
|
|
128
|
+
scope,
|
|
129
|
+
returnTo,
|
|
130
|
+
fields: { server },
|
|
131
|
+
...(connectionLabel ? { label: connectionLabel } : {}),
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
if (!auth.oauth) {
|
|
135
|
+
throw new Error(`${integrationName} connects with an API token.`)
|
|
136
|
+
}
|
|
137
|
+
return connect.start(actor, request, {
|
|
138
|
+
provider: auth.oauth.provider,
|
|
139
|
+
scope,
|
|
140
|
+
returnTo,
|
|
141
|
+
...(auth.oauth.scopes ? { scopes: auth.oauth.scopes } : {}),
|
|
142
|
+
...(fields ? { fields } : {}),
|
|
143
|
+
...(connectionLabel ? { label: connectionLabel } : {}),
|
|
144
|
+
...(connectConnectionId ? { connection: connectConnectionId } : {}),
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
case 'bind': {
|
|
148
|
+
const summary = await connect.get(actor, Number(connectConnectionId))
|
|
149
|
+
const expected = auth.mcp ? 'mcp' : auth.oauth?.provider
|
|
150
|
+
if (summary.provider !== expected) {
|
|
151
|
+
throw new Error(`That account is not a ${integrationName} connection.`)
|
|
152
|
+
}
|
|
153
|
+
const fixed = auth.mcp?.server
|
|
154
|
+
if (fixed && summary.server && new URL(summary.server).origin !== new URL(fixed).origin) {
|
|
155
|
+
throw new Error(`That MCP server is not ${integrationName}.`)
|
|
156
|
+
}
|
|
157
|
+
return bind(summary)
|
|
158
|
+
}
|
|
159
|
+
case 'target': {
|
|
160
|
+
const summary = await connect.selectTarget(
|
|
161
|
+
actor,
|
|
162
|
+
Number(connectConnectionId),
|
|
163
|
+
String(body.target ?? ''),
|
|
164
|
+
)
|
|
165
|
+
return bind(summary)
|
|
166
|
+
}
|
|
167
|
+
case 'apiKey': {
|
|
168
|
+
if (!auth.apiKey) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`${integrationName} connects with ${auth.mcp ? 'its MCP server' : 'OAuth'}.`,
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
const credentials = body.credentials ?? {}
|
|
174
|
+
if (
|
|
175
|
+
JSON.stringify(credentials).length > 16000 ||
|
|
176
|
+
Object.values(credentials).some((value) => typeof value !== 'string')
|
|
177
|
+
) {
|
|
178
|
+
throw new Error('Provide valid connection credentials.')
|
|
179
|
+
}
|
|
180
|
+
if (!credentials.token) {
|
|
181
|
+
throw new Error('Enter the API access token for this connection.')
|
|
182
|
+
}
|
|
183
|
+
for (const field of integrationFields) {
|
|
184
|
+
if (!credentials[field.key]) {
|
|
185
|
+
throw new Error(`Enter ${field.label.toLowerCase()}.`)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// Prove the token works before storing it.
|
|
189
|
+
const account = await verifyIntegration(provider, credentials)
|
|
190
|
+
const summary = await connect.saveApiKey(actor, {
|
|
191
|
+
provider: `key-${provider}`,
|
|
192
|
+
scope,
|
|
193
|
+
fields: credentials,
|
|
194
|
+
account: { id: account, name: account },
|
|
195
|
+
...(connectionLabel ? { label: connectionLabel } : {}),
|
|
196
|
+
})
|
|
197
|
+
return bind(summary)
|
|
198
|
+
}
|
|
199
|
+
case 'permissions': {
|
|
200
|
+
const { permissions, approvals, enabled } = body
|
|
201
|
+
const row = await ownBinding(bindingId)
|
|
202
|
+
return toPublicConnection(
|
|
203
|
+
await api.integrationConnection.update(row.id, {
|
|
204
|
+
ownerId: row.ownerId,
|
|
205
|
+
...(permissions ? { permissions } : {}),
|
|
206
|
+
...(approvals ? { approvals } : {}),
|
|
207
|
+
...(connectionSettings ? { settings: connectionSettings } : {}),
|
|
208
|
+
...(enabled !== undefined ? { enabled } : {}),
|
|
209
|
+
...(connectionLabel !== undefined ? { label: connectionLabel.slice(0, 120) } : {}),
|
|
210
|
+
}),
|
|
211
|
+
viewer,
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
case 'disconnect': {
|
|
215
|
+
const row = await ownBinding(bindingId)
|
|
216
|
+
await api.integrationConnection.delete(row.id)
|
|
217
|
+
// Revoke the account at the vendor once no integration in this workspace uses it.
|
|
218
|
+
const stillUsed = (await listBindings(context)).some(
|
|
219
|
+
(entry) => entry.connectionId === row.connectionId,
|
|
220
|
+
)
|
|
221
|
+
if (row.connectionId && !stillUsed) {
|
|
222
|
+
await connect.remove(actor, row.connectionId).catch(() => {})
|
|
223
|
+
}
|
|
224
|
+
return { removed: row.id }
|
|
225
|
+
}
|
|
226
|
+
case 'webhook': {
|
|
227
|
+
const row = await ownBinding(bindingId)
|
|
228
|
+
const secret = body.rotate
|
|
229
|
+
? await connect.rotateWebhookSecret(actor, row.connectionId)
|
|
230
|
+
: await connect.webhookSecret(actor, row.connectionId)
|
|
231
|
+
return {
|
|
232
|
+
url: `${getRequestURL(event).origin}/api/integration-hooks/${row.connectionUid}`,
|
|
233
|
+
secret,
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
default:
|
|
237
|
+
throw new Error('Choose a connection operation.')
|
|
238
|
+
}
|
|
239
|
+
} catch (cause) {
|
|
240
|
+
throw toActionError(cause)
|
|
241
|
+
}
|
|
242
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { defineEventHandler, getQuery, getRouterParam } from 'h3'
|
|
2
|
+
import {
|
|
3
|
+
connectOf,
|
|
4
|
+
getIntegrationContext,
|
|
5
|
+
isMcpIntegration,
|
|
6
|
+
listBindings,
|
|
7
|
+
toActionError,
|
|
8
|
+
} from '../../../../utils/integration-context'
|
|
9
|
+
import { isMcpWrite } from '../../../../../shared/catalog'
|
|
10
|
+
|
|
11
|
+
/** Tools an MCP connection's server offers, for choosing what agents may use. */
|
|
12
|
+
export default defineEventHandler(async (event) => {
|
|
13
|
+
try {
|
|
14
|
+
const context = await getIntegrationContext(event)
|
|
15
|
+
const provider = getRouterParam(event, 'provider')!
|
|
16
|
+
if (!isMcpIntegration(provider)) {
|
|
17
|
+
throw new Error('Only MCP servers list their tools.')
|
|
18
|
+
}
|
|
19
|
+
const binding = (await listBindings(context, { provider })).find(
|
|
20
|
+
(row) => row.id === Number(getQuery(event).binding),
|
|
21
|
+
)
|
|
22
|
+
if (!binding) {
|
|
23
|
+
throw new Error('Connection not found.')
|
|
24
|
+
}
|
|
25
|
+
const tools = await (
|
|
26
|
+
await connectOf(context)
|
|
27
|
+
)
|
|
28
|
+
.mcp(context.actor, binding.connectionId)
|
|
29
|
+
.listTools()
|
|
30
|
+
return tools.map((tool) => ({
|
|
31
|
+
name: tool.name,
|
|
32
|
+
title: tool.title ?? tool.annotations?.title ?? tool.name,
|
|
33
|
+
description: tool.description ?? '',
|
|
34
|
+
write: isMcpWrite(tool.annotations),
|
|
35
|
+
destructive: tool.annotations?.destructiveHint === true,
|
|
36
|
+
}))
|
|
37
|
+
} catch (cause) {
|
|
38
|
+
throw toActionError(cause)
|
|
39
|
+
}
|
|
40
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { defineEventHandler } from 'h3'
|
|
2
|
+
import type { ConnectionSummary } from '@cloudconfig/core/connect'
|
|
3
|
+
import {
|
|
4
|
+
connectOf,
|
|
5
|
+
getIntegrationContext,
|
|
6
|
+
listBindings,
|
|
7
|
+
toPublicConnection,
|
|
8
|
+
} from '../../../utils/integration-context'
|
|
9
|
+
import { INTEGRATION_CATALOG } from '../../../../shared/catalog'
|
|
10
|
+
|
|
11
|
+
export default defineEventHandler(async (event) => {
|
|
12
|
+
const context = await getIntegrationContext(event)
|
|
13
|
+
const connect = await connectOf(context)
|
|
14
|
+
const bindings = await listBindings(context)
|
|
15
|
+
// Connection health (expired, pending a choice) lives in Connect; sealed tokens never leave it.
|
|
16
|
+
let accounts: ConnectionSummary[] = []
|
|
17
|
+
try {
|
|
18
|
+
accounts = await connect.list(context.actor)
|
|
19
|
+
} catch {
|
|
20
|
+
// Without a sealing key there is nothing Connect can report yet.
|
|
21
|
+
}
|
|
22
|
+
const byId = new Map(accounts.map((account) => [account.id, account]))
|
|
23
|
+
return {
|
|
24
|
+
catalog: INTEGRATION_CATALOG,
|
|
25
|
+
connections: bindings.map((row) => {
|
|
26
|
+
const account = byId.get(row.connectionId)
|
|
27
|
+
return {
|
|
28
|
+
...toPublicConnection(row, context),
|
|
29
|
+
status: account?.status ?? (row.connectionId ? 'error' : 'active'),
|
|
30
|
+
statusMessage: account?.statusMessage ?? '',
|
|
31
|
+
}
|
|
32
|
+
}),
|
|
33
|
+
/** Connect connections of this workspace that no integration uses yet, e.g. awaiting a choice. */
|
|
34
|
+
pending: accounts.filter((account) => account.status === 'pending'),
|
|
35
|
+
providers: connect
|
|
36
|
+
.providers()
|
|
37
|
+
.map(({ id, label, configured, missing, allow, startFields }) => ({
|
|
38
|
+
id,
|
|
39
|
+
label,
|
|
40
|
+
configured,
|
|
41
|
+
missing,
|
|
42
|
+
allow,
|
|
43
|
+
startFields,
|
|
44
|
+
})),
|
|
45
|
+
manager: context.manager,
|
|
46
|
+
organization: context.actor.organizationId !== null,
|
|
47
|
+
}
|
|
48
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { defineEventHandler } from 'h3'
|
|
2
|
+
import {
|
|
3
|
+
getIntegrationContext,
|
|
4
|
+
listGrantedTools,
|
|
5
|
+
toActionError,
|
|
6
|
+
} from '../../../utils/integration-context'
|
|
7
|
+
|
|
8
|
+
/** The tools a saved agent may be given here: connected, enabled and permitted. */
|
|
9
|
+
export default defineEventHandler(async (event) => {
|
|
10
|
+
try {
|
|
11
|
+
const context = await getIntegrationContext(event)
|
|
12
|
+
return (await listGrantedTools(context)).map(
|
|
13
|
+
({ provider, action, name, description, parameters, approvalRequired, account, scope }) => ({
|
|
14
|
+
provider,
|
|
15
|
+
action,
|
|
16
|
+
name,
|
|
17
|
+
description,
|
|
18
|
+
parameters,
|
|
19
|
+
approvalRequired,
|
|
20
|
+
account,
|
|
21
|
+
scope,
|
|
22
|
+
}),
|
|
23
|
+
)
|
|
24
|
+
} catch (cause) {
|
|
25
|
+
throw toActionError(cause)
|
|
26
|
+
}
|
|
27
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { defineNitroPlugin, useRuntimeConfig } from 'nitropack/runtime'
|
|
2
|
+
import { connectOf, getDatabaseProvider } from '../utils/integration-context'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* On the Worker's cron trigger: refresh tokens before they lapse, re-seal after
|
|
6
|
+
* a key rotation, and revoke connections whose owner is gone.
|
|
7
|
+
*/
|
|
8
|
+
export default defineNitroPlugin((nitro) => {
|
|
9
|
+
nitro.hooks.hook('cloudflare:scheduled', async ({ env }) => {
|
|
10
|
+
const bindings = env as Record<string, unknown>
|
|
11
|
+
try {
|
|
12
|
+
const settings = useRuntimeConfig().cloudconfig as { d1Binding?: string; localDb?: string }
|
|
13
|
+
const database = await getDatabaseProvider(bindings, settings)
|
|
14
|
+
const connect = await connectOf({
|
|
15
|
+
database,
|
|
16
|
+
env: bindings,
|
|
17
|
+
request: new Request('https://maintenance.internal'),
|
|
18
|
+
})
|
|
19
|
+
const report = await connect.maintain()
|
|
20
|
+
if (Object.values(report).some(Boolean)) {
|
|
21
|
+
console.log('[integrations] Connection maintenance', JSON.stringify(report))
|
|
22
|
+
}
|
|
23
|
+
} catch (cause) {
|
|
24
|
+
console.error(
|
|
25
|
+
'[integrations] Connection maintenance failed:',
|
|
26
|
+
cause instanceof Error ? cause.message : cause,
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { getAgentPlatform, type AgentScope } from '@cloudconfig/core/agents/platform'
|
|
2
|
+
import type { H3Event } from 'h3'
|
|
3
|
+
|
|
4
|
+
/** The continuation is server-only and immutable, and callers already checked the owner and workspace. */
|
|
5
|
+
export async function notifyApproval(
|
|
6
|
+
event: H3Event,
|
|
7
|
+
id: number,
|
|
8
|
+
continuation: unknown,
|
|
9
|
+
env?: Record<string, unknown>,
|
|
10
|
+
) {
|
|
11
|
+
if (!continuation || typeof continuation !== 'object') {
|
|
12
|
+
return
|
|
13
|
+
}
|
|
14
|
+
const { workflowId, automationRunId, agentScope, agentRunId } = continuation as {
|
|
15
|
+
workflowId?: string
|
|
16
|
+
automationRunId?: number
|
|
17
|
+
agentScope?: AgentScope
|
|
18
|
+
agentRunId?: string
|
|
19
|
+
}
|
|
20
|
+
if (workflowId && env?.AUTOMATION_RUNNER) {
|
|
21
|
+
const binding = env.AUTOMATION_RUNNER as {
|
|
22
|
+
get(
|
|
23
|
+
id: string,
|
|
24
|
+
): Promise<{ sendEvent(event: { type: string; payload: unknown }): Promise<void> }>
|
|
25
|
+
}
|
|
26
|
+
await (
|
|
27
|
+
await binding.get(workflowId)
|
|
28
|
+
).sendEvent({ type: `integration-approval-${id}`, payload: { approvalId: id } })
|
|
29
|
+
} else if (automationRunId && !env) {
|
|
30
|
+
await event.$fetch(`/api/automations/runs/${automationRunId}/resume`, { method: 'POST' })
|
|
31
|
+
}
|
|
32
|
+
// Queues the saved agent run on whichever platform holds it.
|
|
33
|
+
const agents = getAgentPlatform(env)
|
|
34
|
+
if (agentScope && agentRunId && agents) {
|
|
35
|
+
await (await agents.runs(agentScope)).resume(agentRunId)
|
|
36
|
+
}
|
|
37
|
+
}
|