@hostwebhook/node-types 1.69.0 → 1.71.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/dist/bucket-operations.d.ts +88 -0
- package/dist/bucket-operations.js +140 -0
- package/dist/calendar-toolkit.d.ts +1 -1
- package/dist/connections.d.ts +1 -1
- package/dist/connections.js +1 -0
- package/dist/discord-operations.d.ts +1 -1
- package/dist/discord-operations.js +4 -4
- package/dist/discord-toolkit.d.ts +1 -1
- package/dist/discord-toolkit.js +3 -3
- package/dist/dispatch.d.ts +1 -1
- package/dist/dispatch.js +1 -0
- package/dist/docs-toolkit.d.ts +1 -1
- package/dist/drive-toolkit.d.ts +1 -1
- package/dist/esm/bucket-operations.d.ts +88 -0
- package/dist/esm/bucket-operations.js +136 -0
- package/dist/esm/calendar-operations.d.ts +67 -0
- package/dist/esm/calendar-operations.js +176 -0
- package/dist/esm/calendar-toolkit.d.ts +50 -0
- package/dist/esm/calendar-toolkit.js +152 -0
- package/dist/esm/capacidad-de-credencial.d.ts +75 -0
- package/dist/esm/capacidad-de-credencial.js +68 -0
- package/dist/esm/connections.d.ts +17 -0
- package/dist/esm/connections.js +107 -0
- package/dist/esm/credentials.d.ts +144 -0
- package/dist/esm/credentials.js +199 -0
- package/dist/esm/discord-operations.d.ts +104 -0
- package/dist/esm/discord-operations.js +407 -0
- package/dist/esm/discord-toolkit.d.ts +64 -0
- package/dist/esm/discord-toolkit.js +260 -0
- package/dist/esm/dispatch.d.ts +16 -0
- package/dist/esm/dispatch.js +77 -0
- package/dist/esm/docs-operations.d.ts +99 -0
- package/dist/esm/docs-operations.js +127 -0
- package/dist/esm/docs-toolkit.d.ts +42 -0
- package/dist/esm/docs-toolkit.js +97 -0
- package/dist/esm/drive-operations.d.ts +103 -0
- package/dist/esm/drive-operations.js +319 -0
- package/dist/esm/drive-toolkit.d.ts +54 -0
- package/dist/esm/drive-toolkit.js +177 -0
- package/dist/esm/github-operations.d.ts +147 -0
- package/dist/esm/github-operations.js +551 -0
- package/dist/esm/gmail-operations.d.ts +190 -0
- package/dist/esm/gmail-operations.js +560 -0
- package/dist/esm/google-analytics-operations.d.ts +99 -0
- package/dist/esm/google-analytics-operations.js +222 -0
- package/dist/esm/google-contacts-operations.d.ts +93 -0
- package/dist/esm/google-contacts-operations.js +363 -0
- package/dist/esm/index.d.ts +68 -0
- package/dist/esm/index.js +51 -0
- package/dist/esm/jira-operations.d.ts +164 -0
- package/dist/esm/jira-operations.js +470 -0
- package/dist/esm/llm-models.d.ts +101 -0
- package/dist/esm/llm-models.js +230 -0
- package/dist/esm/mailchimp-operations.d.ts +56 -0
- package/dist/esm/mailchimp-operations.js +155 -0
- package/dist/esm/mongo-operations.d.ts +61 -0
- package/dist/esm/mongo-operations.js +120 -0
- package/dist/esm/notion-operations.d.ts +125 -0
- package/dist/esm/notion-operations.js +511 -0
- package/dist/esm/openrouter.d.ts +142 -0
- package/dist/esm/openrouter.js +99 -0
- package/dist/esm/package.json +4 -0
- package/dist/esm/postgres-operations.d.ts +60 -0
- package/dist/esm/postgres-operations.js +75 -0
- package/dist/esm/registry.d.ts +160 -0
- package/dist/esm/registry.js +885 -0
- package/dist/esm/sheets-operations.d.ts +78 -0
- package/dist/esm/sheets-operations.js +150 -0
- package/dist/esm/sheets-toolkit.d.ts +49 -0
- package/dist/esm/sheets-toolkit.js +127 -0
- package/dist/esm/shopify-operations.d.ts +95 -0
- package/dist/esm/shopify-operations.js +259 -0
- package/dist/esm/slack-operations.d.ts +81 -0
- package/dist/esm/slack-operations.js +244 -0
- package/dist/esm/slack-toolkit.d.ts +60 -0
- package/dist/esm/slack-toolkit.js +235 -0
- package/dist/esm/telegram-operations.d.ts +104 -0
- package/dist/esm/telegram-operations.js +351 -0
- package/dist/esm/telegram-toolkit.d.ts +52 -0
- package/dist/esm/telegram-toolkit.js +183 -0
- package/dist/esm/types.d.ts +149 -0
- package/dist/esm/types.js +8 -0
- package/dist/esm/ui.d.ts +13 -0
- package/dist/esm/ui.js +152 -0
- package/dist/esm/whatsapp-operations.d.ts +23 -0
- package/dist/esm/whatsapp-operations.js +36 -0
- package/dist/index.d.ts +68 -66
- package/dist/index.js +193 -187
- package/dist/openrouter.d.ts +1 -1
- package/dist/registry.d.ts +4 -4
- package/dist/registry.js +540 -210
- package/dist/sheets-toolkit.d.ts +1 -1
- package/dist/slack-operations.d.ts +1 -1
- package/dist/slack-operations.js +4 -4
- package/dist/slack-toolkit.d.ts +1 -1
- package/dist/slack-toolkit.js +3 -3
- package/dist/telegram-toolkit.d.ts +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +2 -0
- package/package.json +21 -3
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shopify Admin API operations — single source of truth across the api, the
|
|
3
|
+
* dashboard and downstream consumers (MCP server).
|
|
4
|
+
*
|
|
5
|
+
* ## Four, out of several hundred
|
|
6
|
+
*
|
|
7
|
+
* The Admin API has hundreds of mutations. This node exposes four, and the
|
|
8
|
+
* choice is argued in `api/docs/ADR-0007-shopify.md`: a menu of everything is
|
|
9
|
+
* how a node becomes unusable, and these four cover what people actually
|
|
10
|
+
* automate — put a customer in the store, tag something, sync stock, and read
|
|
11
|
+
* back what the store already knows.
|
|
12
|
+
*
|
|
13
|
+
* Product creation and order fulfilment were considered and left out of v1.
|
|
14
|
+
* A product means variants, media and prices; a fulfilment means a location, a
|
|
15
|
+
* fulfilment service and a state machine. Each is a configuration surface of
|
|
16
|
+
* its own, not a field.
|
|
17
|
+
*
|
|
18
|
+
* ## GraphQL only, and the version is not ours to drift on
|
|
19
|
+
*
|
|
20
|
+
* REST went legacy in October 2024 and public apps have been GraphQL-only
|
|
21
|
+
* since April 2025, so there is no REST path here to fall back to. Every
|
|
22
|
+
* `apiRoute` below is a GraphQL mutation or query name, checkable against
|
|
23
|
+
* `shopify.dev/docs/api/admin-graphql`. The API version lives in ONE constant
|
|
24
|
+
* on the api side (`SHOPIFY_API_VERSION`) — never interpolated per call site.
|
|
25
|
+
*
|
|
26
|
+
* ## What was verified, and what was not (2026-08-25)
|
|
27
|
+
*
|
|
28
|
+
* Checked against the Admin GraphQL reference, not from memory:
|
|
29
|
+
*
|
|
30
|
+
* - `tagsAdd(id: ID!, tags: [String!]!)` and `tagsRemove` with the same
|
|
31
|
+
* shape. Both accept Order, DraftOrder, Customer, Product, Article and
|
|
32
|
+
* DiscountNode.
|
|
33
|
+
* - `customerCreate(input: CustomerInput!)` and `customerUpdate(input:
|
|
34
|
+
* CustomerInput!)` — for the update, **the id goes inside the input**, not
|
|
35
|
+
* as a sibling argument.
|
|
36
|
+
* - `inventoryAdjustQuantities(input: InventoryAdjustQuantitiesInput!)`, whose
|
|
37
|
+
* input requires `reason`, `name` and a `changes[]` of
|
|
38
|
+
* `{ delta, inventoryItemId, locationId }`.
|
|
39
|
+
*
|
|
40
|
+
* ⚠️ **NOT verified, and deliberately left loose below** — do not turn either
|
|
41
|
+
* into a hard-coded dropdown until someone reads the enumeration:
|
|
42
|
+
*
|
|
43
|
+
* - The full vocabulary of `reason`. Only `"correction"` is documented by
|
|
44
|
+
* example.
|
|
45
|
+
* - The full vocabulary of `name`. Only `"available"` is documented by
|
|
46
|
+
* example; the docs mention `on_hand` moving as a consequence, which is not
|
|
47
|
+
* the same as it being a valid input.
|
|
48
|
+
* - The exact search syntax for finding a customer by email
|
|
49
|
+
* (`customers(query: "email:…")`). It is the obvious shape and almost
|
|
50
|
+
* certainly right, which is exactly why it is worth confirming rather than
|
|
51
|
+
* assuming.
|
|
52
|
+
*
|
|
53
|
+
* ⚠️ **`@idempotent` is mandatory.** As of API version 2026-04,
|
|
54
|
+
* `inventoryAdjustQuantities` must be sent with the `@idempotent` directive
|
|
55
|
+
* and an idempotency key. A stock adjustment is the one operation here where a
|
|
56
|
+
* silent retry is a real inventory error, so this is not boilerplate.
|
|
57
|
+
*/
|
|
58
|
+
export const SHOPIFY_OPERATIONS = [
|
|
59
|
+
'upsertCustomer',
|
|
60
|
+
'setTags',
|
|
61
|
+
'adjustInventory',
|
|
62
|
+
'findRecords',
|
|
63
|
+
];
|
|
64
|
+
/** Type guard — for DTOs and AI tool calls, where the input is untrusted. */
|
|
65
|
+
export function isShopifyOperation(value) {
|
|
66
|
+
return (typeof value === 'string' &&
|
|
67
|
+
SHOPIFY_OPERATIONS.includes(value));
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The resources `tagsAdd` / `tagsRemove` accept. Taken from the mutation
|
|
71
|
+
* reference — this list is the API's, not a subset we chose.
|
|
72
|
+
*
|
|
73
|
+
* `article` and `discountNode` are left out of the node's own picker on
|
|
74
|
+
* purpose: neither is something a HostWebhook flow tags in practice, and every
|
|
75
|
+
* extra option is a field the user has to read past.
|
|
76
|
+
*/
|
|
77
|
+
export const SHOPIFY_TAGGABLE_RESOURCES = [
|
|
78
|
+
'order',
|
|
79
|
+
'customer',
|
|
80
|
+
'product',
|
|
81
|
+
'draftOrder',
|
|
82
|
+
];
|
|
83
|
+
/** What `findRecords` can read back. */
|
|
84
|
+
export const SHOPIFY_SEARCHABLE_RESOURCES = [
|
|
85
|
+
'orders',
|
|
86
|
+
'customers',
|
|
87
|
+
'products',
|
|
88
|
+
];
|
|
89
|
+
export const SHOPIFY_OPERATION_SPECS = {
|
|
90
|
+
upsertCustomer: {
|
|
91
|
+
label: 'Add or update customer',
|
|
92
|
+
description: 'Look the customer up by email, then create them or update the fields you set.',
|
|
93
|
+
apiRoute: 'mutation customerCreate / customerUpdate (lookup by email first)',
|
|
94
|
+
params: [
|
|
95
|
+
{
|
|
96
|
+
name: 'email',
|
|
97
|
+
label: 'Email address',
|
|
98
|
+
type: 'email',
|
|
99
|
+
required: true,
|
|
100
|
+
description: 'Identifies the customer. This is what decides between creating and updating — there is no separate "id" to keep.',
|
|
101
|
+
placeholder: '{{payload.email}}',
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: 'firstName',
|
|
105
|
+
label: 'First name',
|
|
106
|
+
type: 'string',
|
|
107
|
+
description: 'Left untouched on an existing customer if you leave it empty.',
|
|
108
|
+
placeholder: '{{payload.firstName}}',
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: 'lastName',
|
|
112
|
+
label: 'Last name',
|
|
113
|
+
type: 'string',
|
|
114
|
+
description: 'Left untouched on an existing customer if you leave it empty.',
|
|
115
|
+
placeholder: '{{payload.lastName}}',
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: 'phone',
|
|
119
|
+
label: 'Phone',
|
|
120
|
+
type: 'string',
|
|
121
|
+
description: 'Shopify wants it in E.164 (+52…). A malformed number fails the whole mutation, not just the field.',
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: 'tags',
|
|
125
|
+
label: 'Tags',
|
|
126
|
+
type: 'tags',
|
|
127
|
+
description: 'Tags to set on the customer. Note these REPLACE the customer tags — to add without losing the existing ones, use the tag operation instead.',
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: 'note',
|
|
131
|
+
label: 'Note',
|
|
132
|
+
type: 'string',
|
|
133
|
+
description: 'Internal note, visible to staff in the Shopify admin.',
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: 'emailMarketingConsent',
|
|
137
|
+
label: 'Email marketing consent',
|
|
138
|
+
type: 'json',
|
|
139
|
+
description: 'Leave this EMPTY unless the customer actually consented. Omitting it leaves their consent exactly as it was; writing a subscribed state for someone who never opted in is a legal problem, not a technical one. Shape: {"marketingState":"SUBSCRIBED","marketingOptInLevel":"SINGLE_OPT_IN"}.',
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
setTags: {
|
|
144
|
+
label: 'Add or remove tags',
|
|
145
|
+
description: 'Tag an order, a customer or a product. Adding and removing are separate calls, so one node does one of the two.',
|
|
146
|
+
apiRoute: 'mutation tagsAdd / tagsRemove',
|
|
147
|
+
params: [
|
|
148
|
+
{
|
|
149
|
+
name: 'resource',
|
|
150
|
+
label: 'What to tag',
|
|
151
|
+
type: 'taggableResource',
|
|
152
|
+
required: true,
|
|
153
|
+
description: 'Shopify tags many resource types through the same mutation; this only picks which id you are passing.',
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: 'resourceId',
|
|
157
|
+
label: 'Resource id',
|
|
158
|
+
type: 'gid',
|
|
159
|
+
required: true,
|
|
160
|
+
description: 'The Shopify global id, e.g. gid://shopify/Order/1234. A Shopify trigger gives you this directly in the payload.',
|
|
161
|
+
placeholder: '{{payload.admin_graphql_api_id}}',
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: 'tags',
|
|
165
|
+
label: 'Tags',
|
|
166
|
+
type: 'tags',
|
|
167
|
+
required: true,
|
|
168
|
+
description: 'One or more tags.',
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: 'remove',
|
|
172
|
+
label: 'Remove instead of add',
|
|
173
|
+
type: 'boolean',
|
|
174
|
+
description: 'Off adds the tags (tagsAdd); on removes them (tagsRemove). Unlike Mailchimp, Shopify cannot do both in one call.',
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
adjustInventory: {
|
|
179
|
+
label: 'Adjust inventory',
|
|
180
|
+
description: 'Move stock by a delta at one location. For the case where the real stock lives in another system.',
|
|
181
|
+
apiRoute: 'mutation inventoryAdjustQuantities',
|
|
182
|
+
params: [
|
|
183
|
+
{
|
|
184
|
+
name: 'inventoryItemId',
|
|
185
|
+
label: 'Inventory item id',
|
|
186
|
+
type: 'gid',
|
|
187
|
+
required: true,
|
|
188
|
+
description: 'gid://shopify/InventoryItem/1234. This is the inventory item, NOT the product or the variant — a variant points at one.',
|
|
189
|
+
placeholder: '{{payload.inventory_item_id}}',
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: 'locationId',
|
|
193
|
+
label: 'Location',
|
|
194
|
+
type: 'location',
|
|
195
|
+
required: true,
|
|
196
|
+
description: 'Which location the stock moves at. A store with one location still has to name it.',
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: 'delta',
|
|
200
|
+
label: 'Change by',
|
|
201
|
+
type: 'number',
|
|
202
|
+
required: true,
|
|
203
|
+
description: 'A DELTA, not a total. 5 adds five units and -5 removes five. Sending the absolute stock level here is the mistake that silently multiplies inventory.',
|
|
204
|
+
placeholder: '{{payload.delta}}',
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
name: 'name',
|
|
208
|
+
label: 'Quantity type',
|
|
209
|
+
type: 'string',
|
|
210
|
+
required: true,
|
|
211
|
+
description: 'Which quantity is being changed. "available" is the documented value; check the reference before using another.',
|
|
212
|
+
placeholder: 'available',
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
name: 'reason',
|
|
216
|
+
label: 'Reason',
|
|
217
|
+
type: 'string',
|
|
218
|
+
required: true,
|
|
219
|
+
description: 'Why the stock moved — it lands in the store\'s inventory history. "correction" is the documented value; check the reference before using another.',
|
|
220
|
+
placeholder: 'correction',
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
name: 'referenceDocumentUri',
|
|
224
|
+
label: 'Reference',
|
|
225
|
+
type: 'string',
|
|
226
|
+
description: 'Optional audit-trail link back to whatever caused the change, so someone reading Shopify\'s inventory history can find it.',
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
},
|
|
230
|
+
findRecords: {
|
|
231
|
+
label: 'Find orders, customers or products',
|
|
232
|
+
description: 'Read back what the store knows, to enrich a payload that came from somewhere else.',
|
|
233
|
+
apiRoute: 'query orders / customers / products (with search syntax)',
|
|
234
|
+
params: [
|
|
235
|
+
{
|
|
236
|
+
name: 'resource',
|
|
237
|
+
label: 'What to look for',
|
|
238
|
+
type: 'searchableResource',
|
|
239
|
+
required: true,
|
|
240
|
+
description: 'Which collection to search.',
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
name: 'query',
|
|
244
|
+
label: 'Search',
|
|
245
|
+
type: 'string',
|
|
246
|
+
required: true,
|
|
247
|
+
description: 'Shopify search syntax, e.g. email:someone@example.com or financial_status:paid. Empty would return the newest records, which is rarely what anyone means.',
|
|
248
|
+
placeholder: 'email:{{payload.email}}',
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
name: 'first',
|
|
252
|
+
label: 'How many',
|
|
253
|
+
type: 'number',
|
|
254
|
+
description: 'Defaults to 1, which is the enrichment case. Asking for many costs more against the query-cost budget, not more calls.',
|
|
255
|
+
placeholder: '1',
|
|
256
|
+
},
|
|
257
|
+
],
|
|
258
|
+
},
|
|
259
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slack Web API operation enum — single source of truth across the
|
|
3
|
+
* api, dashboard, and downstream consumers (Message Broker, MCP server).
|
|
4
|
+
* Used by:
|
|
5
|
+
* - slackAction entity / DTO (api): operation field + validation
|
|
6
|
+
* - dashboard SlackAction operation picker + form generation
|
|
7
|
+
* - MCP toolkit-specs expansion (one aiEnabled slackAction → N tools)
|
|
8
|
+
*
|
|
9
|
+
* v1 covers 16 ops — messaging, channel CRUD, members, reactions, files.
|
|
10
|
+
* Future additions (pin / unpin, files.list, conversations.archive, app
|
|
11
|
+
* publish, scheduled.list, etc.) extend this list — bump the package and
|
|
12
|
+
* re-install in consumers, same pattern as Gmail / Calendar / Discord.
|
|
13
|
+
*/
|
|
14
|
+
import { type CapacidadesPorCredencial } from './capacidad-de-credencial.js';
|
|
15
|
+
export declare const SLACK_OPERATIONS: readonly ["sendMessage", "updateMessage", "deleteMessage", "sendEphemeral", "scheduleMessage", "getMessage", "listChannels", "getChannelHistory", "getThreadReplies", "createChannel", "inviteToChannel", "addReaction", "removeReaction", "listUsers", "getUserInfo", "uploadFile"];
|
|
16
|
+
export type SlackOperation = (typeof SLACK_OPERATIONS)[number];
|
|
17
|
+
/** Type guard — useful when validating untrusted input (DTOs, AI tool calls). */
|
|
18
|
+
export declare function isSlackOperation(value: unknown): value is SlackOperation;
|
|
19
|
+
/**
|
|
20
|
+
* Per-operation parameter schema. Drives:
|
|
21
|
+
* 1. Form auto-generation in the SlackAction detail page (dashboard).
|
|
22
|
+
* 2. AI tool argument schema when `aiEnabled === true`.
|
|
23
|
+
* 3. MCP tools/list expansion (one slackAction → N MCP tools).
|
|
24
|
+
*
|
|
25
|
+
* Field types kept narrow on purpose so the dashboard form picker can
|
|
26
|
+
* render the right control (channel-picker vs free text vs number).
|
|
27
|
+
* The api side `SlackOperationsService` accepts the same shapes.
|
|
28
|
+
*/
|
|
29
|
+
export interface SlackParamSpec {
|
|
30
|
+
/** Field key — also the property name on operationConfig / payload. */
|
|
31
|
+
name: string;
|
|
32
|
+
/** UI label. */
|
|
33
|
+
label: string;
|
|
34
|
+
/** Param type for form rendering + AI tool schema. */
|
|
35
|
+
type: 'channel' | 'user' | 'string' | 'text' | 'number' | 'boolean' | 'emoji' | 'blocks' | 'json' | 'fileUrl';
|
|
36
|
+
/** Required at submit time. */
|
|
37
|
+
required?: boolean;
|
|
38
|
+
/** Human description — also used as the AI tool param description. */
|
|
39
|
+
description: string;
|
|
40
|
+
/** Hint shown under the input. */
|
|
41
|
+
placeholder?: string;
|
|
42
|
+
}
|
|
43
|
+
export interface SlackOperationSpec {
|
|
44
|
+
/** UI label for the operation picker. */
|
|
45
|
+
label: string;
|
|
46
|
+
/** Description shown in the picker + reused by the AI tool description
|
|
47
|
+
* when the user hasn't set a custom one on the entity. */
|
|
48
|
+
description: string;
|
|
49
|
+
/** Verb the operation maps to in Slack's docs (slack.com/methods/...). */
|
|
50
|
+
apiMethod: string;
|
|
51
|
+
/** Parameter schema. Order matters for form rendering. */
|
|
52
|
+
params: SlackParamSpec[];
|
|
53
|
+
}
|
|
54
|
+
export declare const SLACK_OPERATION_SPECS: Record<SlackOperation, SlackOperationSpec>;
|
|
55
|
+
/**
|
|
56
|
+
* Lo que puede hacer una credencial de Slack que NO es OAuth.
|
|
57
|
+
*
|
|
58
|
+
* Medido contra la documentación de Slack, no de memoria:
|
|
59
|
+
* https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks
|
|
60
|
+
*
|
|
61
|
+
* Es el caso extremo, y conviene que se lea así de crudo: **una** operación de
|
|
62
|
+
* las 16. Un incoming webhook publica y nada más. Textualmente: «Incoming
|
|
63
|
+
* webhooks do not allow you to delete a message after it's been posted», no
|
|
64
|
+
* lee, no sube ficheros, y editar exige `chat.update`, que es token.
|
|
65
|
+
*
|
|
66
|
+
* Y el campo que se cae es el que más duele, porque no parece una restricción
|
|
67
|
+
* hasta que lo es: **`channel` no se puede elegir**. «You cannot override the
|
|
68
|
+
* default channel (chosen by the user who installed your app)». El destino se
|
|
69
|
+
* decide al crear la credencial, así que ofrecer el campo sería ofrecer una
|
|
70
|
+
* pregunta cuya respuesta se ignora — que es peor que no ofrecerla.
|
|
71
|
+
*
|
|
72
|
+
* `text` y `blocks` se quedan (Block Kit está soportado), y `threadTs` también:
|
|
73
|
+
* la doc lo admite si ya tienes el `ts` del padre por otra vía.
|
|
74
|
+
*/
|
|
75
|
+
export declare const SLACK_CAPACIDADES_POR_CREDENCIAL: CapacidadesPorCredencial<SlackOperation>;
|
|
76
|
+
/** Las operaciones que ofrecer para una credencial de Slack. */
|
|
77
|
+
export declare function operacionesDeSlackPara(tipoDeCredencial: string | null | undefined): readonly SlackOperation[];
|
|
78
|
+
/** Si esa credencial puede ejecutar esa operación de Slack. */
|
|
79
|
+
export declare function slackPuedeEjecutar(tipoDeCredencial: string | null | undefined, operacion: SlackOperation): boolean;
|
|
80
|
+
/** Los campos que esa credencial de Slack no deja elegir. */
|
|
81
|
+
export declare function camposDeSlackNoDisponibles(tipoDeCredencial: string | null | undefined): ReadonlySet<string>;
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slack Web API operation enum — single source of truth across the
|
|
3
|
+
* api, dashboard, and downstream consumers (Message Broker, MCP server).
|
|
4
|
+
* Used by:
|
|
5
|
+
* - slackAction entity / DTO (api): operation field + validation
|
|
6
|
+
* - dashboard SlackAction operation picker + form generation
|
|
7
|
+
* - MCP toolkit-specs expansion (one aiEnabled slackAction → N tools)
|
|
8
|
+
*
|
|
9
|
+
* v1 covers 16 ops — messaging, channel CRUD, members, reactions, files.
|
|
10
|
+
* Future additions (pin / unpin, files.list, conversations.archive, app
|
|
11
|
+
* publish, scheduled.list, etc.) extend this list — bump the package and
|
|
12
|
+
* re-install in consumers, same pattern as Gmail / Calendar / Discord.
|
|
13
|
+
*/
|
|
14
|
+
import { camposNoDisponiblesPara, operacionesPara, puedeEjecutar, } from './capacidad-de-credencial.js';
|
|
15
|
+
export const SLACK_OPERATIONS = [
|
|
16
|
+
// Messages
|
|
17
|
+
'sendMessage',
|
|
18
|
+
'updateMessage',
|
|
19
|
+
'deleteMessage',
|
|
20
|
+
'sendEphemeral',
|
|
21
|
+
'scheduleMessage',
|
|
22
|
+
'getMessage',
|
|
23
|
+
// Channels
|
|
24
|
+
'listChannels',
|
|
25
|
+
'getChannelHistory',
|
|
26
|
+
'getThreadReplies',
|
|
27
|
+
'createChannel',
|
|
28
|
+
'inviteToChannel',
|
|
29
|
+
// Reactions
|
|
30
|
+
'addReaction',
|
|
31
|
+
'removeReaction',
|
|
32
|
+
// Users
|
|
33
|
+
'listUsers',
|
|
34
|
+
'getUserInfo',
|
|
35
|
+
// Files
|
|
36
|
+
'uploadFile',
|
|
37
|
+
];
|
|
38
|
+
/** Type guard — useful when validating untrusted input (DTOs, AI tool calls). */
|
|
39
|
+
export function isSlackOperation(value) {
|
|
40
|
+
return (typeof value === 'string' &&
|
|
41
|
+
SLACK_OPERATIONS.includes(value));
|
|
42
|
+
}
|
|
43
|
+
export const SLACK_OPERATION_SPECS = {
|
|
44
|
+
sendMessage: {
|
|
45
|
+
label: 'Send message',
|
|
46
|
+
description: 'Post a message to a channel, group, or DM.',
|
|
47
|
+
apiMethod: 'chat.postMessage',
|
|
48
|
+
params: [
|
|
49
|
+
{ name: 'channel', label: 'Channel', type: 'channel', required: true, description: 'Channel id (C…) or DM id (D…) where the message will be posted.' },
|
|
50
|
+
{ name: 'text', label: 'Text', type: 'text', description: 'Message text. Supports Slack mrkdwn. Required when blocks is empty.' },
|
|
51
|
+
{ name: 'blocks', label: 'Blocks (optional)', type: 'blocks', description: 'Slack Block Kit JSON for rich layouts. Overrides text rendering when set.' },
|
|
52
|
+
{ name: 'threadTs', label: 'Thread ts (optional)', type: 'string', description: 'When set, the message replies inside an existing thread. Use the parent message timestamp.' },
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
updateMessage: {
|
|
56
|
+
label: 'Update message',
|
|
57
|
+
description: 'Edit a message the bot posted earlier.',
|
|
58
|
+
apiMethod: 'chat.update',
|
|
59
|
+
params: [
|
|
60
|
+
{ name: 'channel', label: 'Channel', type: 'channel', required: true, description: 'Channel where the original message lives.' },
|
|
61
|
+
{ name: 'ts', label: 'Message ts', type: 'string', required: true, description: 'Timestamp of the message to edit (returned by sendMessage).' },
|
|
62
|
+
{ name: 'text', label: 'New text', type: 'text', description: 'Replacement text. Required when blocks is empty.' },
|
|
63
|
+
{ name: 'blocks', label: 'New blocks (optional)', type: 'blocks', description: 'Replacement Block Kit layout.' },
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
deleteMessage: {
|
|
67
|
+
label: 'Delete message',
|
|
68
|
+
description: 'Delete a message the bot posted earlier.',
|
|
69
|
+
apiMethod: 'chat.delete',
|
|
70
|
+
params: [
|
|
71
|
+
{ name: 'channel', label: 'Channel', type: 'channel', required: true, description: 'Channel where the message lives.' },
|
|
72
|
+
{ name: 'ts', label: 'Message ts', type: 'string', required: true, description: 'Timestamp of the message to delete.' },
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
sendEphemeral: {
|
|
76
|
+
label: 'Send ephemeral',
|
|
77
|
+
description: 'Post a message visible only to a single user (the rest of the channel does not see it).',
|
|
78
|
+
apiMethod: 'chat.postEphemeral',
|
|
79
|
+
params: [
|
|
80
|
+
{ name: 'channel', label: 'Channel', type: 'channel', required: true, description: 'Channel where the ephemeral message will appear.' },
|
|
81
|
+
{ name: 'user', label: 'User', type: 'user', required: true, description: 'User id (U…) — only this user sees the message.' },
|
|
82
|
+
{ name: 'text', label: 'Text', type: 'text', description: 'Message text. Required when blocks is empty.' },
|
|
83
|
+
{ name: 'blocks', label: 'Blocks (optional)', type: 'blocks', description: 'Block Kit layout.' },
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
scheduleMessage: {
|
|
87
|
+
label: 'Schedule message',
|
|
88
|
+
description: 'Schedule a message to be posted at a future time.',
|
|
89
|
+
apiMethod: 'chat.scheduleMessage',
|
|
90
|
+
params: [
|
|
91
|
+
{ name: 'channel', label: 'Channel', type: 'channel', required: true, description: 'Channel where the scheduled message will be posted.' },
|
|
92
|
+
{ name: 'postAt', label: 'Post at (unix sec)', type: 'number', required: true, description: 'When Slack should post the message — unix timestamp in seconds.' },
|
|
93
|
+
{ name: 'text', label: 'Text', type: 'text', description: 'Message text. Required when blocks is empty.' },
|
|
94
|
+
{ name: 'blocks', label: 'Blocks (optional)', type: 'blocks', description: 'Block Kit layout.' },
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
getMessage: {
|
|
98
|
+
label: 'Get message',
|
|
99
|
+
description: 'Fetch a single message by its timestamp.',
|
|
100
|
+
apiMethod: 'conversations.history',
|
|
101
|
+
params: [
|
|
102
|
+
{ name: 'channel', label: 'Channel', type: 'channel', required: true, description: 'Channel where the message lives.' },
|
|
103
|
+
{ name: 'ts', label: 'Message ts', type: 'string', required: true, description: 'Timestamp of the message to fetch.' },
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
listChannels: {
|
|
107
|
+
label: 'List channels',
|
|
108
|
+
description: 'List public + private channels the bot can see in the workspace.',
|
|
109
|
+
apiMethod: 'conversations.list',
|
|
110
|
+
params: [
|
|
111
|
+
{ name: 'limit', label: 'Limit', type: 'number', description: 'Max channels to return (1-1000). Default 200.' },
|
|
112
|
+
{ name: 'excludeArchived', label: 'Exclude archived', type: 'boolean', description: 'Hide archived channels. Default true.' },
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
getChannelHistory: {
|
|
116
|
+
label: 'Get channel history',
|
|
117
|
+
description: 'Read recent messages from a channel.',
|
|
118
|
+
apiMethod: 'conversations.history',
|
|
119
|
+
params: [
|
|
120
|
+
{ name: 'channel', label: 'Channel', type: 'channel', required: true, description: 'Channel to read from.' },
|
|
121
|
+
{ name: 'limit', label: 'Limit', type: 'number', description: 'Max messages to return (1-200). Default 50.' },
|
|
122
|
+
{ name: 'oldest', label: 'Oldest ts', type: 'string', description: 'Only messages after this timestamp.' },
|
|
123
|
+
{ name: 'latest', label: 'Latest ts', type: 'string', description: 'Only messages before this timestamp.' },
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
getThreadReplies: {
|
|
127
|
+
label: 'Get thread replies',
|
|
128
|
+
description: 'Fetch replies to a thread.',
|
|
129
|
+
apiMethod: 'conversations.replies',
|
|
130
|
+
params: [
|
|
131
|
+
{ name: 'channel', label: 'Channel', type: 'channel', required: true, description: 'Channel where the thread lives.' },
|
|
132
|
+
{ name: 'ts', label: 'Parent ts', type: 'string', required: true, description: 'Timestamp of the thread parent message.' },
|
|
133
|
+
{ name: 'limit', label: 'Limit', type: 'number', description: 'Max replies to return (1-1000). Default 100.' },
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
createChannel: {
|
|
137
|
+
label: 'Create channel',
|
|
138
|
+
description: 'Create a new public or private channel.',
|
|
139
|
+
apiMethod: 'conversations.create',
|
|
140
|
+
params: [
|
|
141
|
+
{ name: 'name', label: 'Name', type: 'string', required: true, description: 'Channel name (lowercase, no spaces, < 80 chars).' },
|
|
142
|
+
{ name: 'isPrivate', label: 'Private', type: 'boolean', description: 'Create a private channel instead of public. Default false.' },
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
inviteToChannel: {
|
|
146
|
+
label: 'Invite to channel',
|
|
147
|
+
description: 'Invite one or more users to a channel.',
|
|
148
|
+
apiMethod: 'conversations.invite',
|
|
149
|
+
params: [
|
|
150
|
+
{ name: 'channel', label: 'Channel', type: 'channel', required: true, description: 'Channel id to invite users to.' },
|
|
151
|
+
{ name: 'users', label: 'Users', type: 'string', required: true, description: 'Comma-separated user ids (U…) to invite.' },
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
addReaction: {
|
|
155
|
+
label: 'Add reaction',
|
|
156
|
+
description: 'Add an emoji reaction to a message.',
|
|
157
|
+
apiMethod: 'reactions.add',
|
|
158
|
+
params: [
|
|
159
|
+
{ name: 'channel', label: 'Channel', type: 'channel', required: true, description: 'Channel where the message lives.' },
|
|
160
|
+
{ name: 'ts', label: 'Message ts', type: 'string', required: true, description: 'Timestamp of the message to react to.' },
|
|
161
|
+
{ name: 'name', label: 'Emoji name', type: 'emoji', required: true, description: 'Emoji name without colons (e.g. thumbsup, white_check_mark).' },
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
removeReaction: {
|
|
165
|
+
label: 'Remove reaction',
|
|
166
|
+
description: 'Remove an emoji reaction from a message.',
|
|
167
|
+
apiMethod: 'reactions.remove',
|
|
168
|
+
params: [
|
|
169
|
+
{ name: 'channel', label: 'Channel', type: 'channel', required: true, description: 'Channel where the message lives.' },
|
|
170
|
+
{ name: 'ts', label: 'Message ts', type: 'string', required: true, description: 'Timestamp of the message.' },
|
|
171
|
+
{ name: 'name', label: 'Emoji name', type: 'emoji', required: true, description: 'Emoji name without colons.' },
|
|
172
|
+
],
|
|
173
|
+
},
|
|
174
|
+
listUsers: {
|
|
175
|
+
label: 'List users',
|
|
176
|
+
description: 'List human members of the workspace (excludes deleted users + bots by default).',
|
|
177
|
+
apiMethod: 'users.list',
|
|
178
|
+
params: [
|
|
179
|
+
{ name: 'limit', label: 'Limit', type: 'number', description: 'Max users to return (1-1000). Default 200.' },
|
|
180
|
+
{ name: 'includeDeleted', label: 'Include deleted', type: 'boolean', description: 'Include deactivated users. Default false.' },
|
|
181
|
+
{ name: 'includeBots', label: 'Include bots', type: 'boolean', description: 'Include bot users + app users. Default false.' },
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
getUserInfo: {
|
|
185
|
+
label: 'Get user info',
|
|
186
|
+
description: 'Fetch a user profile (name, email, timezone, status).',
|
|
187
|
+
apiMethod: 'users.info',
|
|
188
|
+
params: [
|
|
189
|
+
{ name: 'user', label: 'User', type: 'user', required: true, description: 'User id (U…) to look up.' },
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
uploadFile: {
|
|
193
|
+
label: 'Upload file',
|
|
194
|
+
description: 'Upload a file (PDF, image, anything) and optionally share it in a channel.',
|
|
195
|
+
apiMethod: 'files.uploadV2',
|
|
196
|
+
params: [
|
|
197
|
+
{ name: 'url', label: 'File URL', type: 'fileUrl', description: 'Public URL or HW file _file.downloadUrl that HW will fetch and re-upload.' },
|
|
198
|
+
{ name: 'contentBase64', label: 'File content (base64)', type: 'text', description: 'Inline file content as base64 — alternative to url for small payloads.' },
|
|
199
|
+
{ name: 'filename', label: 'Filename', type: 'string', description: 'Display name in Slack. Auto-derived from URL when omitted.' },
|
|
200
|
+
{ name: 'channel', label: 'Channel (optional)', type: 'channel', description: 'Channel to share the uploaded file in.' },
|
|
201
|
+
{ name: 'title', label: 'Title (optional)', type: 'string', description: 'Title shown above the file in Slack.' },
|
|
202
|
+
{ name: 'initialComment', label: 'Initial comment (optional)', type: 'text', description: 'Message Slack posts together with the file.' },
|
|
203
|
+
{ name: 'threadTs', label: 'Thread ts (optional)', type: 'string', description: 'Attach the file to an existing thread.' },
|
|
204
|
+
],
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
/**
|
|
208
|
+
* Lo que puede hacer una credencial de Slack que NO es OAuth.
|
|
209
|
+
*
|
|
210
|
+
* Medido contra la documentación de Slack, no de memoria:
|
|
211
|
+
* https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks
|
|
212
|
+
*
|
|
213
|
+
* Es el caso extremo, y conviene que se lea así de crudo: **una** operación de
|
|
214
|
+
* las 16. Un incoming webhook publica y nada más. Textualmente: «Incoming
|
|
215
|
+
* webhooks do not allow you to delete a message after it's been posted», no
|
|
216
|
+
* lee, no sube ficheros, y editar exige `chat.update`, que es token.
|
|
217
|
+
*
|
|
218
|
+
* Y el campo que se cae es el que más duele, porque no parece una restricción
|
|
219
|
+
* hasta que lo es: **`channel` no se puede elegir**. «You cannot override the
|
|
220
|
+
* default channel (chosen by the user who installed your app)». El destino se
|
|
221
|
+
* decide al crear la credencial, así que ofrecer el campo sería ofrecer una
|
|
222
|
+
* pregunta cuya respuesta se ignora — que es peor que no ofrecerla.
|
|
223
|
+
*
|
|
224
|
+
* `text` y `blocks` se quedan (Block Kit está soportado), y `threadTs` también:
|
|
225
|
+
* la doc lo admite si ya tienes el `ts` del padre por otra vía.
|
|
226
|
+
*/
|
|
227
|
+
export const SLACK_CAPACIDADES_POR_CREDENCIAL = {
|
|
228
|
+
slack_webhook: {
|
|
229
|
+
operaciones: ['sendMessage'],
|
|
230
|
+
camposNoDisponibles: ['channel'],
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
/** Las operaciones que ofrecer para una credencial de Slack. */
|
|
234
|
+
export function operacionesDeSlackPara(tipoDeCredencial) {
|
|
235
|
+
return operacionesPara(SLACK_OPERATIONS, SLACK_CAPACIDADES_POR_CREDENCIAL, tipoDeCredencial);
|
|
236
|
+
}
|
|
237
|
+
/** Si esa credencial puede ejecutar esa operación de Slack. */
|
|
238
|
+
export function slackPuedeEjecutar(tipoDeCredencial, operacion) {
|
|
239
|
+
return puedeEjecutar(SLACK_OPERATIONS, SLACK_CAPACIDADES_POR_CREDENCIAL, tipoDeCredencial, operacion);
|
|
240
|
+
}
|
|
241
|
+
/** Los campos que esa credencial de Slack no deja elegir. */
|
|
242
|
+
export function camposDeSlackNoDisponibles(tipoDeCredencial) {
|
|
243
|
+
return camposNoDisponiblesPara(SLACK_CAPACIDADES_POR_CREDENCIAL, tipoDeCredencial);
|
|
244
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slack AI Toolkit — las herramientas que un slackAction expone cuando
|
|
3
|
+
* `aiEnabled` está encendido.
|
|
4
|
+
*
|
|
5
|
+
* **Se escribe una sola vez.** Antes vivía dos veces: una copia en
|
|
6
|
+
* `dashboard/components/slack-actions/slack-operations-schemas.ts` y otra en
|
|
7
|
+
* `api/src/mcp-servers/toolkit-specs.ts`, las dos a mano y en repos distintos,
|
|
8
|
+
* así que ningún test podía compararlas. Es la octava y novena mudanza de esta
|
|
9
|
+
* serie; de las siete anteriores, la de Contacts salió de que una copia
|
|
10
|
+
* anunciara 15 herramientas y expusiera una.
|
|
11
|
+
*
|
|
12
|
+
* La deriva aquí era gorda: **14 de las 16 descripciones** diferían. Gana la de
|
|
13
|
+
* la api en todas —es sistemáticamente la más completa y es la que sirve el MCP
|
|
14
|
+
* hoy—, y una no era cosmética: en `deleteMessage` la api nombra el permiso que
|
|
15
|
+
* hace falta (`chat:write.public` + admin) y el dashboard sólo decía «admin
|
|
16
|
+
* perms». Operaciones, nombres de herramienta y parámetros sí coincidían.
|
|
17
|
+
*
|
|
18
|
+
* Los nombres llevan `_slack_` para no chocar con los de Telegram y Discord
|
|
19
|
+
* cuando un mismo AI Node tiene varios toolkits encendidos.
|
|
20
|
+
*
|
|
21
|
+
* La forma es la que ya consume `toolkitSpecToMcpTool` en la api, para que
|
|
22
|
+
* pueda usarse sin adaptador. Los textos van en inglés porque los lee el modelo
|
|
23
|
+
* y quien mire la lista de herramientas del servidor MCP.
|
|
24
|
+
*/
|
|
25
|
+
import type { SlackOperation } from './slack-operations.js';
|
|
26
|
+
export interface SlackToolkitParameter {
|
|
27
|
+
name: string;
|
|
28
|
+
type: 'string' | 'number' | 'boolean';
|
|
29
|
+
description: string;
|
|
30
|
+
required: boolean;
|
|
31
|
+
}
|
|
32
|
+
export interface SlackToolkitSpec {
|
|
33
|
+
operation: SlackOperation;
|
|
34
|
+
/** Etiqueta corta de la fila en la lista de herramientas. */
|
|
35
|
+
label: string;
|
|
36
|
+
/** Nombre con el que el LLM llama a la herramienta. */
|
|
37
|
+
toolName: string;
|
|
38
|
+
/** Descripción (más reglas de uso) que ve el LLM. */
|
|
39
|
+
description: string;
|
|
40
|
+
parameters: SlackToolkitParameter[];
|
|
41
|
+
/** Operaciones irreversibles o que cambian privilegios. La capa MCP las
|
|
42
|
+
* bloquea cuando el nodo de IA lleva `requireConfirmationForDestructive`,
|
|
43
|
+
* salvo que la llamada traiga confirmación explícita. */
|
|
44
|
+
destructive?: boolean;
|
|
45
|
+
}
|
|
46
|
+
export declare const SLACK_TOOLKIT_SPECS: SlackToolkitSpec[];
|
|
47
|
+
export declare const SLACK_TOOLKIT_BY_TOOL_NAME: Record<string, SlackToolkitSpec>;
|
|
48
|
+
/**
|
|
49
|
+
* Las herramientas que ofrecerle al modelo con ESTA credencial.
|
|
50
|
+
*
|
|
51
|
+
* Gemelo de `herramientasDeDiscordPara`, y por la misma razón: los dos
|
|
52
|
+
* consumidores —la expansión MCP de la api y el selector del AI Node del
|
|
53
|
+
* dashboard— filtrarían por su cuenta y acabarían discrepando.
|
|
54
|
+
*
|
|
55
|
+
* En Slack el recorte es brutal y conviene verlo escrito: con un webhook el
|
|
56
|
+
* modelo pasa de **16 herramientas a una**, y esa una pierde el `channel`,
|
|
57
|
+
* porque el destino lo fijó quien instaló la credencial. Dejarle el campo sería
|
|
58
|
+
* invitarle a elegir un canal que Slack va a ignorar sin decir nada.
|
|
59
|
+
*/
|
|
60
|
+
export declare function herramientasDeSlackPara(tipoDeCredencial: string | null | undefined): SlackToolkitSpec[];
|