@hostwebhook/node-types 1.68.0 → 1.70.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 (94) hide show
  1. package/dist/calendar-toolkit.d.ts +1 -1
  2. package/dist/connections.d.ts +1 -1
  3. package/dist/credentials.d.ts +89 -2
  4. package/dist/credentials.js +31 -4
  5. package/dist/discord-operations.d.ts +1 -1
  6. package/dist/discord-operations.js +4 -4
  7. package/dist/discord-toolkit.d.ts +1 -1
  8. package/dist/discord-toolkit.js +3 -3
  9. package/dist/dispatch.d.ts +1 -1
  10. package/dist/docs-toolkit.d.ts +1 -1
  11. package/dist/drive-toolkit.d.ts +1 -1
  12. package/dist/esm/calendar-operations.d.ts +67 -0
  13. package/dist/esm/calendar-operations.js +176 -0
  14. package/dist/esm/calendar-toolkit.d.ts +50 -0
  15. package/dist/esm/calendar-toolkit.js +152 -0
  16. package/dist/esm/capacidad-de-credencial.d.ts +75 -0
  17. package/dist/esm/capacidad-de-credencial.js +68 -0
  18. package/dist/esm/connections.d.ts +17 -0
  19. package/dist/esm/connections.js +106 -0
  20. package/dist/esm/credentials.d.ts +144 -0
  21. package/dist/esm/credentials.js +199 -0
  22. package/dist/esm/discord-operations.d.ts +104 -0
  23. package/dist/esm/discord-operations.js +407 -0
  24. package/dist/esm/discord-toolkit.d.ts +64 -0
  25. package/dist/esm/discord-toolkit.js +260 -0
  26. package/dist/esm/dispatch.d.ts +16 -0
  27. package/dist/esm/dispatch.js +76 -0
  28. package/dist/esm/docs-operations.d.ts +99 -0
  29. package/dist/esm/docs-operations.js +127 -0
  30. package/dist/esm/docs-toolkit.d.ts +42 -0
  31. package/dist/esm/docs-toolkit.js +97 -0
  32. package/dist/esm/drive-operations.d.ts +103 -0
  33. package/dist/esm/drive-operations.js +319 -0
  34. package/dist/esm/drive-toolkit.d.ts +54 -0
  35. package/dist/esm/drive-toolkit.js +177 -0
  36. package/dist/esm/github-operations.d.ts +147 -0
  37. package/dist/esm/github-operations.js +551 -0
  38. package/dist/esm/gmail-operations.d.ts +190 -0
  39. package/dist/esm/gmail-operations.js +560 -0
  40. package/dist/esm/google-analytics-operations.d.ts +99 -0
  41. package/dist/esm/google-analytics-operations.js +222 -0
  42. package/dist/esm/google-contacts-operations.d.ts +93 -0
  43. package/dist/esm/google-contacts-operations.js +363 -0
  44. package/dist/esm/index.d.ts +66 -0
  45. package/dist/esm/index.js +50 -0
  46. package/dist/esm/jira-operations.d.ts +164 -0
  47. package/dist/esm/jira-operations.js +470 -0
  48. package/dist/esm/llm-models.d.ts +101 -0
  49. package/dist/esm/llm-models.js +230 -0
  50. package/dist/esm/mailchimp-operations.d.ts +56 -0
  51. package/dist/esm/mailchimp-operations.js +155 -0
  52. package/dist/esm/mongo-operations.d.ts +61 -0
  53. package/dist/esm/mongo-operations.js +120 -0
  54. package/dist/esm/notion-operations.d.ts +125 -0
  55. package/dist/esm/notion-operations.js +511 -0
  56. package/dist/esm/openrouter.d.ts +142 -0
  57. package/dist/esm/openrouter.js +99 -0
  58. package/dist/esm/package.json +4 -0
  59. package/dist/esm/postgres-operations.d.ts +60 -0
  60. package/dist/esm/postgres-operations.js +75 -0
  61. package/dist/esm/registry.d.ts +160 -0
  62. package/dist/esm/registry.js +555 -0
  63. package/dist/esm/sheets-operations.d.ts +78 -0
  64. package/dist/esm/sheets-operations.js +150 -0
  65. package/dist/esm/sheets-toolkit.d.ts +49 -0
  66. package/dist/esm/sheets-toolkit.js +127 -0
  67. package/dist/esm/shopify-operations.d.ts +95 -0
  68. package/dist/esm/shopify-operations.js +259 -0
  69. package/dist/esm/slack-operations.d.ts +81 -0
  70. package/dist/esm/slack-operations.js +244 -0
  71. package/dist/esm/slack-toolkit.d.ts +60 -0
  72. package/dist/esm/slack-toolkit.js +235 -0
  73. package/dist/esm/telegram-operations.d.ts +104 -0
  74. package/dist/esm/telegram-operations.js +351 -0
  75. package/dist/esm/telegram-toolkit.d.ts +52 -0
  76. package/dist/esm/telegram-toolkit.js +183 -0
  77. package/dist/esm/types.d.ts +149 -0
  78. package/dist/esm/types.js +8 -0
  79. package/dist/esm/ui.d.ts +13 -0
  80. package/dist/esm/ui.js +150 -0
  81. package/dist/esm/whatsapp-operations.d.ts +23 -0
  82. package/dist/esm/whatsapp-operations.js +36 -0
  83. package/dist/index.d.ts +66 -66
  84. package/dist/index.js +185 -185
  85. package/dist/openrouter.d.ts +1 -1
  86. package/dist/registry.d.ts +1 -1
  87. package/dist/sheets-toolkit.d.ts +1 -1
  88. package/dist/slack-operations.d.ts +1 -1
  89. package/dist/slack-operations.js +4 -4
  90. package/dist/slack-toolkit.d.ts +1 -1
  91. package/dist/slack-toolkit.js +3 -3
  92. package/dist/telegram-toolkit.d.ts +1 -1
  93. package/dist/ui.d.ts +1 -1
  94. package/package.json +21 -3
@@ -0,0 +1,555 @@
1
+ /**
2
+ * Centralized NODE_REGISTRY — single source of truth for all node metadata.
3
+ *
4
+ * When adding a new node type, add ONE entry here. The dashboard derives
5
+ * all per-node maps (colors, URLs, Redux keys, palette, delete, etc.)
6
+ * from this registry. No more 18+ touch points.
7
+ */
8
+ export const NODE_REGISTRY = {
9
+ // ── Sources ──
10
+ webhook: {
11
+ type: 'webhook', prefix: 'wh', label: 'Webhook',
12
+ group: 'Sources', detailPath: '/dashboard/webhooks', apiPath: '/webhooks',
13
+ stateKey: 'webhooks', allStateKey: 'allWebhooks',
14
+ color: '#a78bfa', testable: false,
15
+ },
16
+ scheduledWorkflow: {
17
+ type: 'scheduledWorkflow', prefix: 'swf', label: 'Scheduled Workflow',
18
+ group: 'Sources', detailPath: '/dashboard/scheduled-workflows', apiPath: '/scheduled-workflows',
19
+ stateKey: 'scheduledWorkflows', allStateKey: 'allScheduledWorkflows',
20
+ color: '#2dd4bf', testable: true,
21
+ },
22
+ chatTrigger: {
23
+ type: 'chatTrigger', prefix: 'chat', label: 'Chat Trigger',
24
+ group: 'Sources', detailPath: '/dashboard/chat-triggers', apiPath: '/chat-triggers',
25
+ stateKey: 'chatTriggers', allStateKey: 'allChatTriggers',
26
+ color: '#06b6d4', testable: false,
27
+ },
28
+ trigger: {
29
+ type: 'trigger', prefix: 'trg', label: 'Trigger',
30
+ group: 'Sources', detailPath: '/dashboard/triggers', apiPath: '/triggers',
31
+ stateKey: 'triggers', allStateKey: 'allTriggers',
32
+ color: '#ef4444', testable: false,
33
+ },
34
+ voiceAgent: {
35
+ type: 'voiceAgent', prefix: 'va', label: 'Voice Agent',
36
+ group: 'Sources', detailPath: '/dashboard/voice-agents', apiPath: '/voice-agents',
37
+ stateKey: 'voiceAgents', allStateKey: 'allVoiceAgents',
38
+ color: '#a855f7', testable: false,
39
+ },
40
+ // ── Flow Control ──
41
+ filter: {
42
+ type: 'filter', prefix: 'fi', label: 'Filter',
43
+ group: 'Flow Control', detailPath: '/dashboard/filter-nodes', apiPath: '/filter-nodes',
44
+ stateKey: 'filterNodes', allStateKey: 'allFilterNodes',
45
+ color: '#facc15', testable: true,
46
+ },
47
+ transform: {
48
+ type: 'transform', prefix: 'tr', label: 'Transform',
49
+ group: 'Flow Control', detailPath: '/dashboard/transform-nodes', apiPath: '/transform-nodes',
50
+ stateKey: 'transformNodes', allStateKey: 'allTransformNodes',
51
+ color: '#fbbf24', testable: true,
52
+ },
53
+ schemaValidator: {
54
+ type: 'schemaValidator', prefix: 'sv', label: 'Schema Validator',
55
+ group: 'Flow Control', detailPath: '/dashboard/schema-validators', apiPath: '/schema-validators',
56
+ stateKey: 'schemaValidators', allStateKey: 'allSchemaValidators',
57
+ color: '#818cf8', testable: true,
58
+ },
59
+ conditional: {
60
+ type: 'conditional', prefix: 'cond', label: 'Conditional',
61
+ group: 'Flow Control', detailPath: '/dashboard/conditional-nodes', apiPath: '/conditional-nodes',
62
+ stateKey: 'conditionalNodes', allStateKey: 'allConditionalNodes',
63
+ color: '#c084fc', testable: true,
64
+ },
65
+ delay: {
66
+ type: 'delay', prefix: 'delay', label: 'Delay',
67
+ group: 'Flow Control', detailPath: '/dashboard/delay-nodes', apiPath: '/delay-nodes',
68
+ stateKey: 'delayNodes', allStateKey: 'allDelayNodes',
69
+ color: '#94a3b8', testable: true,
70
+ },
71
+ rateLimiter: {
72
+ type: 'rateLimiter', prefix: 'rl', label: 'Rate Limiter',
73
+ group: 'Flow Control', detailPath: '/dashboard/rate-limiter-nodes', apiPath: '/rate-limiter-nodes',
74
+ stateKey: 'rateLimiterNodes', allStateKey: 'allRateLimiterNodes',
75
+ color: '#f97316', testable: false,
76
+ },
77
+ aggregator: {
78
+ type: 'aggregator', prefix: 'agg', label: 'Aggregator',
79
+ group: 'Flow Control', detailPath: '/dashboard/aggregator-nodes', apiPath: '/aggregator-nodes',
80
+ stateKey: 'aggregatorNodes', allStateKey: 'allAggregatorNodes',
81
+ color: '#84cc16', testable: true,
82
+ },
83
+ cache: {
84
+ type: 'cache', prefix: 'cache', label: 'Cache',
85
+ group: 'Flow Control', detailPath: '/dashboard/cache-nodes', apiPath: '/cache-nodes',
86
+ stateKey: 'cacheNodes', allStateKey: 'allCacheNodes',
87
+ color: '#d946ef', testable: true,
88
+ },
89
+ code: {
90
+ type: 'code', prefix: 'code', label: 'Code',
91
+ group: 'Flow Control', detailPath: '/dashboard/code-nodes', apiPath: '/code-nodes',
92
+ stateKey: 'codeNodes', allStateKey: 'allCodeNodes',
93
+ color: '#a855f7', testable: true,
94
+ },
95
+ ai: {
96
+ type: 'ai', prefix: 'ai', label: 'AI',
97
+ /* 'Actions' y no 'Flow Control': un nodo de IA no encamina ni retiene un
98
+ evento —eso es control de flujo— sino que HACE algo con él y devuelve un
99
+ resultado, igual que enviar un correo o escribir en una hoja. Además es
100
+ lo que decide qué se ofrece como acción en el flujo guiado del primer
101
+ flujo (`FIRST_FLOW_ACTIONS` en el dashboard), y ahí tiene que estar. */
102
+ group: 'Actions', detailPath: '/dashboard/ai-nodes', apiPath: '/ai-nodes',
103
+ stateKey: 'aiNodes', allStateKey: 'allAiNodes',
104
+ color: '#d946ef', testable: true,
105
+ },
106
+ split: {
107
+ type: 'split', prefix: 'split', label: 'Split',
108
+ group: 'Flow Control', detailPath: '/dashboard/split-nodes', apiPath: '/split-nodes',
109
+ stateKey: 'splitNodes', allStateKey: 'allSplitNodes',
110
+ color: '#38bdf8', testable: true,
111
+ },
112
+ merge: {
113
+ type: 'merge', prefix: 'merge', label: 'Merge',
114
+ group: 'Flow Control', detailPath: '/dashboard/merge-nodes', apiPath: '/merge-nodes',
115
+ stateKey: 'mergeNodes', allStateKey: 'allMergeNodes',
116
+ color: '#94a3b8', testable: false,
117
+ },
118
+ approval: {
119
+ type: 'approval', prefix: 'appr', label: 'Approval',
120
+ group: 'Flow Control', detailPath: '/dashboard/approval-nodes', apiPath: '/approval-nodes',
121
+ stateKey: 'approvalNodes', allStateKey: 'allApprovalNodes',
122
+ color: '#94a3b8', testable: true,
123
+ },
124
+ loop: {
125
+ type: 'loop', prefix: 'loop', label: 'Loop',
126
+ group: 'Flow Control', detailPath: '/dashboard/loop-nodes', apiPath: '/loop-nodes',
127
+ stateKey: 'loopNodes', allStateKey: 'allLoopNodes',
128
+ color: '#f59e0b', testable: true,
129
+ },
130
+ markdown: {
131
+ type: 'markdown', prefix: 'md', label: 'Markdown',
132
+ group: 'Flow Control', detailPath: '/dashboard/markdown-nodes', apiPath: '/markdown-nodes',
133
+ stateKey: 'markdownNodes', allStateKey: 'allMarkdownNodes',
134
+ color: '#6366f1', testable: true,
135
+ },
136
+ fileTransform: {
137
+ type: 'fileTransform', prefix: 'ft', label: 'File Transform',
138
+ group: 'Flow Control', detailPath: '/dashboard/file-transform-nodes', apiPath: '/file-transform-nodes',
139
+ stateKey: 'fileTransformNodes', allStateKey: 'allFileTransformNodes',
140
+ color: '#f97316', testable: true,
141
+ },
142
+ limit: {
143
+ type: 'limit', prefix: 'lim', label: 'Limit',
144
+ group: 'Flow Control', detailPath: '/dashboard/limit-nodes', apiPath: '/limit-nodes',
145
+ stateKey: 'limitNodes', allStateKey: 'allLimitNodes',
146
+ // Amber matches filter / aggregator family — visually groups
147
+ // "array-shape transformations" together on the canvas.
148
+ color: '#fbbf24', testable: true,
149
+ },
150
+ // ── Routing ──
151
+ router: {
152
+ type: 'router', prefix: 'ro', label: 'Router',
153
+ group: 'Flow Control', detailPath: '/dashboard/routers', apiPath: '/routers',
154
+ stateKey: 'routers', allStateKey: 'allRouters',
155
+ color: '#38bdf8', testable: true,
156
+ },
157
+ // ── Actions ──
158
+ /**
159
+ * Manda correo por Resend, y sólo por Resend.
160
+ *
161
+ * Tuvo un campo `provider` con Gmail y un Outlook que nunca llegó. Gmail se
162
+ * fue a su propio nodo —ver `gmailAction`— y con él su OAuth, sus dieciséis
163
+ * operaciones y el toolkit de IA, que era suyo: aquí sólo quedan enviar y
164
+ * el modo de IA no tiene dieciséis herramientas que ofrecer.
165
+ */
166
+ emailAction: {
167
+ type: 'emailAction', prefix: 'ea', label: 'Email Action',
168
+ group: 'Actions', detailPath: '/dashboard/email-actions', apiPath: '/email-actions',
169
+ stateKey: 'emailActions', allStateKey: 'allEmailActions',
170
+ color: '#f472b6', testable: true,
171
+ /**
172
+ * Sigue siendo herramienta: hay una ruta native + aiEnabled donde el nodo
173
+ * AI genera el correo y este lo manda por Resend —en produccion existe un
174
+ * «using native email as tool»—. Se la quite al partir Gmail dando por
175
+ * hecho que el toolkit era solo suyo, y sin esto el nodo recupera sus
176
+ * handles en modo herramienta: justo el mal enrutado que este flag evita.
177
+ */
178
+ isToolOnly: (e) => e.aiEnabled === true,
179
+ },
180
+ /**
181
+ * Gmail salio del nodo Email, que ahora manda SOLO por Resend.
182
+ *
183
+ * Estaban juntos bajo un campo `provider`, y eso obligaba a que un nodo de
184
+ * dos operaciones —enviar por Resend— cargara con las dieciseis de Gmail, su
185
+ * OAuth, su toolkit de IA y su send-and-wait. Partidos, cada uno se explica
186
+ * solo y el de Email vuelve a caber en una pantalla.
187
+ */
188
+ gmailAction: {
189
+ type: 'gmailAction', prefix: 'gm', label: 'Gmail',
190
+ group: 'Actions', detailPath: '/dashboard/gmail-actions', apiPath: '/gmail-actions',
191
+ stateKey: 'gmailActions', allStateKey: 'allGmailActions',
192
+ color: '#ea4335', testable: true,
193
+ // El toolkit de IA se vino con Gmail: son sus dieciseis operaciones las
194
+ // que el LLM llama, no las dos de Resend. Ver el comentario de arriba.
195
+ isToolOnly: (e) => e.aiEnabled === true,
196
+ },
197
+ httpAction: {
198
+ type: 'httpAction', prefix: 'ha', label: 'HTTP Action',
199
+ group: 'Actions', detailPath: '/dashboard/http-actions', apiPath: '/http-actions',
200
+ stateKey: 'httpActions', allStateKey: 'allHttpActions',
201
+ color: '#3b82f6', testable: true,
202
+ },
203
+ mongoAction: {
204
+ type: 'mongoAction', prefix: 'ma', label: 'MongoDB Action',
205
+ group: 'Actions', detailPath: '/dashboard/mongo-actions', apiPath: '/mongo-actions',
206
+ stateKey: 'mongoActions', allStateKey: 'allMongoActions',
207
+ color: '#34d399', testable: true,
208
+ },
209
+ postgresAction: {
210
+ type: 'postgresAction', prefix: 'pg', label: 'Postgres Action',
211
+ group: 'Actions', detailPath: '/dashboard/postgres-actions', apiPath: '/postgres-actions',
212
+ stateKey: 'postgresActions', allStateKey: 'allPostgresActions',
213
+ color: '#336791', testable: true,
214
+ },
215
+ notificationAction: {
216
+ type: 'notificationAction', prefix: 'na', label: 'Notification',
217
+ group: 'Actions', detailPath: '/dashboard/notification-actions', apiPath: '/notification-actions',
218
+ stateKey: 'notificationActions', allStateKey: 'allNotificationActions',
219
+ color: '#f472b6', testable: true,
220
+ },
221
+ sheetsAction: {
222
+ type: 'sheetsAction', prefix: 'sheet', label: 'Google Sheets Action',
223
+ group: 'Actions', detailPath: '/dashboard/sheets-actions', apiPath: '/sheets-actions',
224
+ stateKey: 'sheetsActions', allStateKey: 'allSheetsActions',
225
+ color: '#34d399', testable: true,
226
+ },
227
+ calendarAction: {
228
+ type: 'calendarAction', prefix: 'cal', label: 'Google Calendar Action',
229
+ group: 'Actions', detailPath: '/dashboard/calendar-actions', apiPath: '/calendar-actions',
230
+ stateKey: 'calendarActions', allStateKey: 'allCalendarActions',
231
+ color: '#60a5fa', testable: true,
232
+ // AI Toolkit mode wires the node through the upstream AI Node's
233
+ // tools[] list, not via canvas edges — same pattern as emailAction.
234
+ isToolOnly: (e) => e.aiEnabled === true,
235
+ },
236
+ docsAction: {
237
+ type: 'docsAction', prefix: 'doc', label: 'Google Docs',
238
+ group: 'Actions', detailPath: '/dashboard/docs-actions', apiPath: '/docs-actions',
239
+ stateKey: 'docsActions', allStateKey: 'allDocsActions',
240
+ color: '#4285F4', testable: true,
241
+ },
242
+ driveAction: {
243
+ type: 'driveAction', prefix: 'drv', label: 'Google Drive',
244
+ group: 'Actions', detailPath: '/dashboard/drive-actions', apiPath: '/drive-actions',
245
+ stateKey: 'driveActions', allStateKey: 'allDriveActions',
246
+ color: '#22c55e', testable: true,
247
+ // AI Toolkit mode wires the node through the upstream AI Node's
248
+ // tools[] list, not via canvas edges — same pattern as emailAction.
249
+ isToolOnly: (e) => e.aiEnabled === true,
250
+ },
251
+ firecrawlAction: {
252
+ type: 'firecrawlAction', prefix: 'fc', label: 'Firecrawl',
253
+ group: 'Actions', detailPath: '/dashboard/firecrawl-actions', apiPath: '/firecrawl-actions',
254
+ stateKey: 'firecrawlActions', allStateKey: 'allFirecrawlActions',
255
+ color: '#f97316', testable: true,
256
+ },
257
+ mailchimpAction: {
258
+ type: 'mailchimpAction', prefix: 'mc', label: 'Mailchimp',
259
+ group: 'Actions', detailPath: '/dashboard/mailchimp-actions', apiPath: '/mailchimp-actions',
260
+ stateKey: 'mailchimpActions', allStateKey: 'allMailchimpActions',
261
+ // Mailchimp's Cavendish yellow, darkened until it holds contrast on a
262
+ // light canvas. The brand value (#ffe01b) reads as a highlighter there.
263
+ color: '#c9a227', testable: true,
264
+ },
265
+ shopifyAction: {
266
+ type: 'shopifyAction', prefix: 'shp', label: 'Shopify',
267
+ group: 'Actions', detailPath: '/dashboard/shopify-actions', apiPath: '/shopify-actions',
268
+ stateKey: 'shopifyActions', allStateKey: 'allShopifyActions',
269
+ // Shopify's green, abierto un punto. El de marca (#008060) es más oscuro
270
+ // que cualquier otro color de este registro y se hunde contra el lienzo
271
+ // en modo oscuro; éste sigue leyéndose como Shopify y aguanta en los dos
272
+ // temas. Mismo criterio que el amarillo de Mailchimp, al revés.
273
+ color: '#0a9e70', testable: true,
274
+ },
275
+ githubAction: {
276
+ type: 'githubAction', prefix: 'gh', label: 'GitHub',
277
+ group: 'Actions', detailPath: '/dashboard/github-actions', apiPath: '/github-actions',
278
+ stateKey: 'githubActions', allStateKey: 'allGithubActions',
279
+ // El morado de "merged" de GitHub. Su marca real es acromática, y ahí no
280
+ // hay elección posible: en el minimapa lo único que distingue a un nodo es
281
+ // el color, el gris cálido ya lo ocupa Notion y el frío lo llevan tres.
282
+ //
283
+ // ⚠️ Es la banda MÁS disputada de este registro — ocho nodos en morado,
284
+ // con #7c3aed y #a855f7 como vecinos inmediatos. Se eligió por ser el
285
+ // color que de verdad dice GitHub, pero **la decisión se cierra mirando el
286
+ // minimapa**, no leyendo este comentario. Si choca, el plan B es un
287
+ // grafito claro, sabiendo que compite con Notion.
288
+ color: '#8957e5', testable: true,
289
+ /* Sin isToolOnly: la v1 no trae modo toolkit de IA, y aquí pesa más que en
290
+ ningún otro nodo — el MCP oficial de GitHub ya cubre ese caso dentro del
291
+ AI Node. Los rieles son este nodo; la improvisación es el MCP. */
292
+ },
293
+ jiraAction: {
294
+ type: 'jiraAction', prefix: 'jr', label: 'Jira',
295
+ group: 'Actions', detailPath: '/dashboard/jira-actions', apiPath: '/jira-actions',
296
+ stateKey: 'jiraActions', allStateKey: 'allJiraActions',
297
+ // El azul de marca de Jira, y estaba libre. La banda azul es la segunda
298
+ // mas poblada de este registro —nueve nodos— pero ninguno tan profundo:
299
+ // #3b82f6 y #60a5fa son claramente mas claros, y el #336791 de Postgres es
300
+ // mas apagado. En el minimapa es el azul oscuro, y eso lo distingue.
301
+ //
302
+ // Sigue valiendo lo que escribio GitHub aqui al lado: **la decision se
303
+ // cierra mirando el minimapa**, no leyendo este comentario.
304
+ color: '#0052CC', testable: true,
305
+ /* Sin isToolOnly: la v1 no trae modo toolkit de IA. Cada toolkit vive dos
306
+ veces —specs del dashboard para el AI Node y specs de la api para MCP— y
307
+ esa duplicacion no se paga antes de ver el nodo en pantalla. Ademas
308
+ Atlassian tiene servidor MCP propio, ya registrado en
309
+ `MCP_OAUTH_PRESETS.atlassian`, que cubre la cola larga dentro del AI
310
+ Node. Los rieles son este nodo. */
311
+ },
312
+ telegramAction: {
313
+ type: 'telegramAction', prefix: 'tg', label: 'Telegram Action',
314
+ group: 'Actions', detailPath: '/dashboard/telegram-actions', apiPath: '/telegram-actions',
315
+ stateKey: 'telegramActions', allStateKey: 'allTelegramActions',
316
+ color: '#0ea5e9', testable: true,
317
+ // AI Toolkit mode — same pattern as emailAction. One aiEnabled
318
+ // telegramAction expands into N MCP tools (one per operation) in
319
+ // the upstream AI Node's tool list, so we hide canvas handles to
320
+ // prevent accidental misrouting that would silently fire with
321
+ // empty fields.
322
+ isToolOnly: (e) => e.aiEnabled === true,
323
+ },
324
+ whatsappAction: {
325
+ type: 'whatsappAction', prefix: 'wa', label: 'WhatsApp Action',
326
+ group: 'Actions', detailPath: '/dashboard/whatsapp-actions', apiPath: '/whatsapp-actions',
327
+ stateKey: 'whatsappActions', allStateKey: 'allWhatsappActions',
328
+ // WhatsApp brand green (#25d366) — instantly recognisable on the canvas.
329
+ color: '#25d366', testable: true,
330
+ // Same toolkit pattern as telegramAction — when aiEnabled the
331
+ // entity stops being a manually-wired single-op action and becomes
332
+ // a multi-op tool surface for an upstream AI Node. Hide canvas
333
+ // handles to prevent accidental wiring that would fire with empty
334
+ // fields the LLM is supposed to populate.
335
+ isToolOnly: (e) => e.aiEnabled === true,
336
+ },
337
+ discordAction: {
338
+ type: 'discordAction', prefix: 'dc', label: 'Discord Action',
339
+ group: 'Actions', detailPath: '/dashboard/discord-actions', apiPath: '/discord-actions',
340
+ stateKey: 'discordActions', allStateKey: 'allDiscordActions',
341
+ // Discord brand "Blurple" (#5865F2).
342
+ color: '#5865F2', testable: true,
343
+ // Same toolkit pattern as telegramAction / whatsappAction. Hide
344
+ // canvas handles when aiEnabled so the user can't manually wire a
345
+ // multi-op tool surface — the LLM picks the operation per call.
346
+ isToolOnly: (e) => e.aiEnabled === true,
347
+ },
348
+ slackAction: {
349
+ type: 'slackAction', prefix: 'sl', label: 'Slack Action',
350
+ group: 'Actions', detailPath: '/dashboard/slack-actions', apiPath: '/slack-actions',
351
+ stateKey: 'slackActions', allStateKey: 'allSlackActions',
352
+ // Slack brand purple ("aubergine"-derived family). Matches the
353
+ // accent color used by SlackConfigSection / ServiceTriggerNode for
354
+ // visual continuity across Slack-related nodes.
355
+ color: '#7c3aed', testable: true,
356
+ // Same AI-tool gating as Telegram/Discord/WhatsApp/Email/Calendar/
357
+ // Contacts. When aiEnabled the node fans out into 16 MCP tools
358
+ // (one per SLACK_OPERATIONS entry); hide canvas handles to prevent
359
+ // accidental wiring with empty fields the LLM is supposed to fill.
360
+ isToolOnly: (e) => e.aiEnabled === true,
361
+ },
362
+ googleContactsAction: {
363
+ type: 'googleContactsAction', prefix: 'gc', label: 'Google Contacts',
364
+ group: 'Actions', detailPath: '/dashboard/google-contacts-actions', apiPath: '/google-contacts-actions',
365
+ stateKey: 'googleContactsActions', allStateKey: 'allGoogleContactsActions',
366
+ // Google "blue" used by Google Drive / Docs (#4285F4) — same family.
367
+ color: '#4285F4', testable: true,
368
+ // AI toolkit mode mirrors emailAction / calendarAction — when
369
+ // aiEnabled the node expands into N MCP tools (one per People API
370
+ // operation) in the upstream AI Node's tool list. Hide canvas
371
+ // handles to keep manual + tool modes from being mixed.
372
+ isToolOnly: (e) => e.aiEnabled === true,
373
+ },
374
+ googleAnalyticsAction: {
375
+ type: 'googleAnalyticsAction', prefix: 'gaa', label: 'Google Analytics',
376
+ group: 'Actions', detailPath: '/dashboard/google-analytics-actions', apiPath: '/google-analytics-actions',
377
+ stateKey: 'googleAnalyticsActions', allStateKey: 'allGoogleAnalyticsActions',
378
+ // Naranja de marca de Google Analytics (#E37400). No reutiliza el
379
+ // #ff9933 de RSS ni el orange-500 de Tailwind que ya llevan tres nodos:
380
+ // en el minimapa lo unico que distingue a un nodo es el color.
381
+ color: '#E37400', testable: true,
382
+ /* Sin isToolOnly: la v1 no trae modo toolkit de IA. Cada toolkit vive
383
+ dos veces --specs del dashboard para el AI Node y specs de la api para
384
+ MCP, sin nada que las ate-- y esa duplicacion no se paga antes de ver
385
+ el nodo funcionando en pantalla. */
386
+ },
387
+ notionAction: {
388
+ type: 'notionAction', prefix: 'ntn', label: 'Notion',
389
+ group: 'Actions', detailPath: '/dashboard/notion-actions', apiPath: '/notion-actions',
390
+ stateKey: 'notionActions', allStateKey: 'allNotionActions',
391
+ // Notion es monocromo de marca (su negro es #37352F), y un punto negro
392
+ // en el minimapa desaparece en modo oscuro. Este stone-500 es el gris
393
+ // CÁLIDO más cercano a esa marca, y no choca con el #94a3b8 (slate, gris
394
+ // frío) que ya llevan tres nodos: en el minimapa lo único que distingue
395
+ // a un nodo es el color.
396
+ color: '#78716c', testable: true,
397
+ /* Sin isToolOnly: la v1 no trae modo toolkit de IA. Cada toolkit vive dos
398
+ veces --specs del dashboard para el AI Node y specs de la api para
399
+ MCP-- y esa duplicación no se paga antes de ver el nodo en pantalla. */
400
+ },
401
+ vectorStore: {
402
+ type: 'vectorStore', prefix: 'vs', label: 'Vector Store',
403
+ group: 'Actions', detailPath: '/dashboard/vector-store-nodes', apiPath: '/vector-store-nodes',
404
+ stateKey: 'vectorStoreNodes', allStateKey: 'allVectorStoreNodes',
405
+ color: '#06b6d4', testable: true,
406
+ },
407
+ rssAction: {
408
+ type: 'rssAction', prefix: 'rssa', label: 'RSS Fetch',
409
+ group: 'Actions', detailPath: '/dashboard/rss-actions', apiPath: '/rss-actions',
410
+ stateKey: 'rssActions', allStateKey: 'allRssActions',
411
+ // Classic RSS feed orange (#ff9933) — distinguishes from rateLimiter /
412
+ // fileTransform / firecrawl which sit on Tailwind orange-500.
413
+ color: '#ff9933', testable: true,
414
+ },
415
+ socialMediaAction: {
416
+ type: 'socialMediaAction', prefix: 'sm', label: 'Social Media',
417
+ group: 'Actions', detailPath: '/dashboard/social-media-actions', apiPath: '/social-media-actions',
418
+ stateKey: 'socialMediaActions', allStateKey: 'allSocialMediaActions',
419
+ // Pink/magenta (#ec4899) — neutral default; the canvas card overrides
420
+ // with the picked provider's brandColor at render time so each
421
+ // platform reads at a glance (Bluesky #0085ff, Mastodon #6364ff, ...).
422
+ color: '#ec4899', testable: true,
423
+ // AI toolkit mode (Sprint 4) wires through tools[] like the other
424
+ // toolkit-capable actions. Manual + toolkit modes share the same
425
+ // detail page; the segmented control hides ops-config when toolkit.
426
+ isToolOnly: (e) => e.aiEnabled === true,
427
+ },
428
+ // ── Annotations ──
429
+ stickyNote: {
430
+ type: 'stickyNote', prefix: 'note', label: 'Sticky Note',
431
+ group: 'Annotations', detailPath: '', apiPath: '/sticky-notes',
432
+ stateKey: 'stickyNotes', allStateKey: 'allStickyNotes',
433
+ color: '#fbbf24', testable: false,
434
+ },
435
+ };
436
+ // ── Derived helpers ──
437
+ /** Get registry entry by canonical type */
438
+ export function getNodeRegistryEntry(type) {
439
+ return NODE_REGISTRY[type];
440
+ }
441
+ /** type → detail page path */
442
+ export const NODE_DETAIL_PATHS = Object.fromEntries(Object.values(NODE_REGISTRY).filter(n => n.detailPath).map(n => [n.type, n.detailPath]));
443
+ /** type → minimap color */
444
+ export const NODE_COLORS = Object.fromEntries(Object.values(NODE_REGISTRY).map(n => [n.type, n.color]));
445
+ /** type → [stateKey, allStateKey] */
446
+ export const NODE_STATE_KEYS = Object.fromEntries(Object.values(NODE_REGISTRY).map(n => [n.type, [n.stateKey, n.allStateKey]]));
447
+ /** prefix → type */
448
+ export const PREFIX_TO_NODE_TYPE = Object.fromEntries(Object.values(NODE_REGISTRY).map(n => [n.prefix, n.type]));
449
+ /** type → prefix */
450
+ export const NODE_TYPE_TO_PREFIX = Object.fromEntries(Object.values(NODE_REGISTRY).map(n => [n.type, n.prefix]));
451
+ function specsFor(type, catalog) {
452
+ return catalog[type]?.versions;
453
+ }
454
+ /** True when this type ships more than one version. False for every type today. */
455
+ export function isVersioned(type, catalog = NODE_REGISTRY) {
456
+ return (specsFor(type, catalog)?.length ?? 0) > 0;
457
+ }
458
+ /**
459
+ * Every declared version, newest first — the order a picker wants.
460
+ * Empty for a type that does not version.
461
+ */
462
+ export function versionsOf(type, catalog = NODE_REGISTRY) {
463
+ const declared = specsFor(type, catalog);
464
+ if (!declared)
465
+ return [];
466
+ return [...declared].sort((a, b) => b.v - a.v);
467
+ }
468
+ /** One version's spec, or undefined when the type or the number is unknown. */
469
+ export function versionSpec(type, v, catalog = NODE_REGISTRY) {
470
+ return specsFor(type, catalog)?.find(s => s.v === v);
471
+ }
472
+ /**
473
+ * The version a new node of this type is born on.
474
+ *
475
+ * Returns 1 for a type that does not version, which is also how an entity
476
+ * with no `nodeVersion` is read. A node created before its type ever
477
+ * versioned was calling the original endpoints, so calling it version 1
478
+ * is not a convention — it is the truth.
479
+ */
480
+ export function currentVersion(type, catalog = NODE_REGISTRY) {
481
+ const current = specsFor(type, catalog)?.find(s => s.status === 'current');
482
+ return current?.v ?? 1;
483
+ }
484
+ /**
485
+ * Config fields present in `from` that `to` does not declare — the exact
486
+ * list a confirmation dialog should name, and the exact list the server
487
+ * should unset.
488
+ *
489
+ * Returns `null` when the question cannot be answered: the type does not
490
+ * version, or one of the two versions is not declared. `null` is not the
491
+ * same as "nothing is lost", and callers must not treat it that way — a
492
+ * dialog should refuse to offer the switch, and the api should reject the
493
+ * request. Returning `null` rather than `[]` is what stops "I don't know"
494
+ * from being read as "nothing".
495
+ *
496
+ * This lives here rather than in each consumer because the dialog that
497
+ * ANNOUNCES the loss and the server that PERFORMS it have to give the
498
+ * same answer. Two implementations would agree right until the day they
499
+ * did not, and that day the dialog lies.
500
+ */
501
+ export function fieldsLostBetween(type, from, to, catalog = NODE_REGISTRY) {
502
+ const a = versionSpec(type, from, catalog);
503
+ const b = versionSpec(type, to, catalog);
504
+ if (!a || !b)
505
+ return null;
506
+ return fieldsLost(a, b);
507
+ }
508
+ /**
509
+ * The same answer for two specs already in hand — what `fieldsLostBetween`
510
+ * delegates to once it has resolved both, and what a caller that already
511
+ * holds them from `versionsOf()` can use without a second lookup.
512
+ */
513
+ export function fieldsLost(from, to) {
514
+ if (from.v === to.v)
515
+ return [];
516
+ if (to.cleanBreak)
517
+ return [...from.fields];
518
+ const kept = new Set(to.fields);
519
+ return from.fields.filter(f => !kept.has(f));
520
+ }
521
+ /**
522
+ * Declaration mistakes in the version catalogs, as readable lines.
523
+ *
524
+ * A guard rather than a line in a document: what this codebase learned
525
+ * from fifteen nodes skipping a checklist point everyone had read is that
526
+ * the written rule prevented nothing and the guard did. Empty means every
527
+ * catalog is well-formed.
528
+ */
529
+ export function versionCatalogErrors(catalog = NODE_REGISTRY) {
530
+ const errors = [];
531
+ for (const entry of Object.values(catalog)) {
532
+ const versions = entry.versions;
533
+ if (!versions)
534
+ continue;
535
+ if (versions.length === 0) {
536
+ errors.push(`${entry.type}: declares an empty versions array — omit the field instead`);
537
+ continue;
538
+ }
539
+ const numbers = versions.map(s => s.v);
540
+ const repeatedNumbers = [...new Set(numbers.filter((n, i) => numbers.indexOf(n) !== i))];
541
+ if (repeatedNumbers.length)
542
+ errors.push(`${entry.type}: repeated version number ${repeatedNumbers.join(', ')}`);
543
+ for (const spec of versions) {
544
+ if (!Number.isInteger(spec.v) || spec.v < 1)
545
+ errors.push(`${entry.type}: version "${spec.v}" is not a positive integer`);
546
+ const repeatedFields = [...new Set(spec.fields.filter((f, i) => spec.fields.indexOf(f) !== i))];
547
+ if (repeatedFields.length)
548
+ errors.push(`${entry.type} v${spec.v}: repeated field ${repeatedFields.join(', ')}`);
549
+ }
550
+ const current = versions.filter(s => s.status === 'current');
551
+ if (current.length !== 1)
552
+ errors.push(`${entry.type}: needs exactly one version with status "current", found ${current.length}`);
553
+ }
554
+ return errors;
555
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Google Sheets operation enum + form schema — single source of truth across
3
+ * the API, Dashboard, and Message Broker. Used by:
4
+ * - sheetsAction entity / DTO (api): operation field + validation
5
+ * - dashboard SheetsAction detail page: the whole operation form
6
+ *
7
+ * The api already accepts these six; the dashboard's own `SheetsAction` type
8
+ * only listed four (it never learned about appendOrUpdateRow and
9
+ * createSpreadsheet), which is exactly the drift this enum removes.
10
+ */
11
+ export declare const SHEETS_OPERATIONS: readonly ["appendRow", "updateRow", "appendOrUpdateRow", "readRange", "getRows", "createSpreadsheet"];
12
+ export type SheetsOperation = (typeof SHEETS_OPERATIONS)[number];
13
+ /** Type guard — useful when validating untrusted input (DTOs, AI tool calls). */
14
+ export declare function isSheetsOperation(value: unknown): value is SheetsOperation;
15
+ /**
16
+ * Per-operation form schema, same idea as SLACK_OPERATION_SPECS and
17
+ * GOOGLE_CONTACTS_OPERATION_SPECS: the detail page renders it with one loop
18
+ * instead of a branch per operation.
19
+ *
20
+ * Two things make this node's schema different from the others, and both are
21
+ * deliberate:
22
+ *
23
+ * 1. **The values are top-level entity fields, not an `operationConfig`
24
+ * record.** `name` here is the property on the sheetsAction document
25
+ * (`spreadsheetId`, `sheetName`, …), and `columnMapping` is a complex field.
26
+ * A renderer copied from Contacts would write to the wrong place.
27
+ * 2. **Half the fields are discovered at run time.** The column list comes from
28
+ * the chosen sheet's header row, fetched live. So the schema names the
29
+ * *picker* rather than carrying options — the same trick Discord's
30
+ * `channel` / `guild` / `role` types use.
31
+ */
32
+ export interface SheetsParamSpec {
33
+ /** Field key — the property name on the sheetsAction document. */
34
+ name: string;
35
+ /** UI label. Empty string where the control has never shown one. */
36
+ label: string;
37
+ /**
38
+ * Param type for form rendering.
39
+ *
40
+ * - `string` — one-line input with `{{payload.x}}` templating.
41
+ * - `spreadsheet` — the spreadsheet picker **plus** the "or paste ID / URL"
42
+ * input that sits under it. They are one field with two ways in, not two
43
+ * fields, and the paste box extracts the id out of a full URL.
44
+ * - `sheetTab` — the tab picker for the chosen spreadsheet. Picking a tab
45
+ * clears `columnMapping`, because the headers just changed.
46
+ * - `headerColumn` — a dropdown of the sheet's live header row.
47
+ * - `columnMapping` — the header-to-template editor. It owns the three
48
+ * states of the header fetch (loading, loaded, failed), which is what
49
+ * used to be six copies of the same conditional in the page.
50
+ */
51
+ type: 'string' | 'spreadsheet' | 'sheetTab' | 'headerColumn' | 'columnMapping';
52
+ /** Required by the operation. Metadata — the form does not gate on it. */
53
+ required?: boolean;
54
+ /** Help text under the control. Absent where the form shows none. */
55
+ description?: string;
56
+ /** Hint shown inside the input. */
57
+ placeholder?: string;
58
+ /**
59
+ * Section this field is rendered inside. Fields without one share the first
60
+ * section with the operation picker; the mapping fields sit in a second
61
+ * section whose title changes per operation ("Columns to Append",
62
+ * "Update Row", "Match & Upsert").
63
+ */
64
+ section?: string;
65
+ /** `columnMapping` only — the line shown while no column has been added. */
66
+ emptyText?: string;
67
+ }
68
+ export interface SheetsOperationSpec {
69
+ /** UI label for the operation picker. */
70
+ label: string;
71
+ /** Sub-line under the label in the picker. */
72
+ description: string;
73
+ /** googleapis call the operation maps to (sheets v4). */
74
+ apiMethod: string;
75
+ /** Parameter schema. Order matters for form rendering. */
76
+ params: SheetsParamSpec[];
77
+ }
78
+ export declare const SHEETS_OPERATION_SPECS: Record<SheetsOperation, SheetsOperationSpec>;