@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.
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
package/schema.ts ADDED
@@ -0,0 +1,179 @@
1
+ import {
2
+ hasOrgRole,
3
+ model,
4
+ t,
5
+ type AccessResult,
6
+ type CloudConfigUser,
7
+ type WhereBuilder,
8
+ } from '@cloudconfig/core'
9
+ import { getIntegrationDefinition } from './runtime/shared/catalog'
10
+
11
+ /** An approval moves pending → approved | rejected, then approved → completed | failed. */
12
+ const approvalTransitions: Record<string, string[]> = {
13
+ pending: ['approved', 'rejected'],
14
+ approved: ['completed', 'failed'],
15
+ }
16
+
17
+ /**
18
+ * Your own connections, plus the workspace's shared ones while you are a
19
+ * member. Membership comes from the session on every request, so leaving an
20
+ * organization hides its connections at once.
21
+ */
22
+ function visibleConnections(user: CloudConfigUser | null, where: WhereBuilder): AccessResult {
23
+ if (!user) {
24
+ return false
25
+ }
26
+ const own = {
27
+ type: 'group' as const,
28
+ logic: 'and' as const,
29
+ children: [where('scope', 'eq', 'user'), where('ownerId', 'eq', user.id)],
30
+ }
31
+ const organization = user.organization?.id
32
+ if (typeof organization !== 'number') {
33
+ return own
34
+ }
35
+ return {
36
+ type: 'group',
37
+ logic: 'or',
38
+ children: [
39
+ own,
40
+ {
41
+ type: 'group',
42
+ logic: 'and',
43
+ children: [where('scope', 'eq', 'workspace'), where('organizationId', 'eq', organization)],
44
+ },
45
+ ],
46
+ }
47
+ }
48
+
49
+ /** Changing a shared connection is for organization owners and admins only. */
50
+ function manageableConnections(user: CloudConfigUser | null, where: WhereBuilder): AccessResult {
51
+ if (!user) {
52
+ return false
53
+ }
54
+ if (hasOrgRole(user, 'owner', 'admin')) {
55
+ return visibleConnections(user, where)
56
+ }
57
+ return {
58
+ type: 'group',
59
+ logic: 'and',
60
+ children: [where('scope', 'eq', 'user'), where('ownerId', 'eq', user.id)],
61
+ }
62
+ }
63
+
64
+ export const integrationsModels = {
65
+ /**
66
+ * One catalog integration bound to a CloudConfig Connect connection, with the
67
+ * actions agents may use through it. Tokens never live here: Connect seals
68
+ * them and checks who may use them on every call.
69
+ */
70
+ integrationConnection: model(
71
+ {
72
+ provider: t.text().required().index(),
73
+ workspace: t.text().required().index(),
74
+ /** `user` acts as the person who connected; `workspace` is shared with the organization. */
75
+ scope: t.select(['user', 'workspace']).default('user'),
76
+ organizationId: t.number().index(),
77
+ /** The Connect connection. Zero for unconnected example records. */
78
+ connectionId: t.number().default(0).index(),
79
+ /** Connect's public uid, for the connection's event endpoint. */
80
+ connectionUid: t.text().default('').serverOnly(),
81
+ account: t.text().default(''),
82
+ label: t.text().default(''),
83
+ enabled: t.boolean().default(true),
84
+ /** Allowed action ids, or MCP tool names. */
85
+ permissions: t.json().default([]),
86
+ /** MCP tool names that need an owner's approval before they run. */
87
+ approvals: t.json().default([]),
88
+ /** Non-secret per-connection values (Google Ads customer, …). */
89
+ settings: t.json().default({}),
90
+ ownerId: t.number().required().serverOnly(),
91
+ },
92
+ {
93
+ access: {
94
+ list: ({ user, where }) => visibleConnections(user, where),
95
+ read: ({ user, where }) => visibleConnections(user, where),
96
+ create: ({ user, input }) =>
97
+ !!user &&
98
+ input?.ownerId === user.id &&
99
+ (input.scope !== 'workspace' ||
100
+ (input.organizationId === user.organization?.id && hasOrgRole(user, 'owner', 'admin'))),
101
+ update: ({ user, where }) => manageableConnections(user, where),
102
+ delete: ({ user, where }) => manageableConnections(user, where),
103
+ },
104
+ validate: (input, existing) => {
105
+ const provider = getIntegrationDefinition(String(input.provider))
106
+ if (!provider || !input.workspace) {
107
+ return [{ path: 'provider', message: 'Choose an integration and workspace.' }]
108
+ }
109
+ if (
110
+ existing &&
111
+ (input.provider !== existing.provider ||
112
+ input.workspace !== existing.workspace ||
113
+ input.scope !== existing.scope ||
114
+ input.connectionId !== existing.connectionId)
115
+ ) {
116
+ return [
117
+ {
118
+ path: 'provider',
119
+ message: 'Connections cannot be moved to another account or workspace.',
120
+ },
121
+ ]
122
+ }
123
+ if (input.scope === 'workspace' && typeof input.organizationId !== 'number') {
124
+ return [{ path: 'scope', message: 'Shared connections belong to an organization.' }]
125
+ }
126
+ const permissions = input.permissions
127
+ if (!Array.isArray(permissions) || permissions.some((id) => typeof id !== 'string')) {
128
+ return [{ path: 'permissions', message: 'Choose the actions agents may use.' }]
129
+ }
130
+ // MCP servers define their own tools; catalog integrations a fixed set.
131
+ if (
132
+ !provider.auth.mcp &&
133
+ permissions.some((id) => !provider.actions.some((action) => action.id === id))
134
+ ) {
135
+ return [
136
+ { path: 'permissions', message: 'Choose permissions supported by this integration.' },
137
+ ]
138
+ }
139
+ return []
140
+ },
141
+ },
142
+ ),
143
+ integrationApproval: model(
144
+ {
145
+ connectionId: t.number().required(),
146
+ action: t.text().required(),
147
+ input: t.json().required(),
148
+ status: t
149
+ .select(['pending', 'approved', 'rejected', 'completed', 'failed'])
150
+ .default('pending'),
151
+ result: t.json(),
152
+ continuation: t.json().serverOnly(),
153
+ ownerId: t.number().required().serverOnly(),
154
+ },
155
+ {
156
+ access: {
157
+ list: ({ user, where }) => (user ? where('ownerId', 'eq', user.id) : false),
158
+ read: ({ user, where }) => (user ? where('ownerId', 'eq', user.id) : false),
159
+ create: ({ user, input }) => !!user && input?.ownerId === user.id,
160
+ update: ({ user, input, where }) =>
161
+ user && input?.ownerId === user.id ? where('ownerId', 'eq', user.id) : false,
162
+ },
163
+ validate: (input, existing) => {
164
+ if (!existing) {
165
+ return []
166
+ }
167
+ if (JSON.stringify(input.continuation) !== JSON.stringify(existing.continuation)) {
168
+ return [{ path: 'continuation', message: 'Keep the original run continuation.' }]
169
+ }
170
+ const allowed = approvalTransitions[String(existing.status)] ?? []
171
+ return allowed.includes(String(input.status))
172
+ ? []
173
+ : [{ path: 'status', message: 'This approval has already been handled.' }]
174
+ },
175
+ },
176
+ ),
177
+ }
178
+
179
+ export type IntegrationsConfig = { models: typeof integrationsModels }